edge/cmd/cli/command/app/run.go

232 lines
5.8 KiB
Go
Raw Normal View History

2023-02-09 12:16:36 +01:00
package app
import (
"database/sql"
"fmt"
2023-02-09 12:16:36 +01:00
"net/http"
"path/filepath"
"time"
2023-02-09 12:16:36 +01:00
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bus"
2023-02-09 12:16:36 +01:00
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
appHTTP "forge.cadoles.com/arcad/edge/pkg/http"
"forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/module/auth"
2023-02-17 10:38:45 +01:00
"forge.cadoles.com/arcad/edge/pkg/module/cast"
"forge.cadoles.com/arcad/edge/pkg/module/net"
"forge.cadoles.com/arcad/edge/pkg/storage"
2023-02-09 12:16:36 +01:00
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
"gitlab.com/wpetit/goweb/logger"
"forge.cadoles.com/arcad/edge/pkg/bundle"
"github.com/dop251/goja"
2023-02-09 12:16:36 +01:00
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/golang-jwt/jwt"
2023-02-09 12:16:36 +01:00
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
_ "modernc.org/sqlite"
)
func RunCommand() *cli.Command {
return &cli.Command{
Name: "run",
Usage: "Run the specified app bundle",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "path",
Usage: "use `PATH` as app bundle (zipped bundle or directory)",
Aliases: []string{"p"},
Value: ".",
},
&cli.StringFlag{
Name: "address",
Usage: "use `ADDRESS` as http server listening address",
Aliases: []string{"a"},
Value: ":8080",
},
&cli.StringFlag{
Name: "log-format",
Usage: "use `LOG-FORMAT` ('json' or 'human')",
Value: "human",
},
&cli.IntFlag{
Name: "log-level",
Usage: "use `LOG-LEVEL` (0: debug -> 5: fatal)",
Value: 0,
},
&cli.StringFlag{
Name: "storage-file",
Usage: "use `FILE` for SQLite storage database",
Value: "data.sqlite",
},
&cli.StringFlag{
Name: "auth-subject",
Usage: "set the `SUBJECT` associated with the simulated connected user",
Value: "jdoe",
},
&cli.StringFlag{
Name: "auth-role",
Usage: "set the `ROLE` associated with the simulated connected user",
Value: "user",
},
&cli.StringFlag{
Name: "auth-preferred-username",
Usage: "set the `PREFERRED_USERNAME` associated with the simulated connected user",
Value: "Jane Doe",
},
2023-02-09 12:16:36 +01:00
},
Action: func(ctx *cli.Context) error {
address := ctx.String("address")
path := ctx.String("path")
2023-02-09 12:16:36 +01:00
logFormat := ctx.String("log-format")
logLevel := ctx.Int("log-level")
2023-02-09 12:16:36 +01:00
storageFile := ctx.String("storage-file")
authSubject := ctx.String("auth-subject")
authRole := ctx.String("auth-role")
authPreferredUsername := ctx.String("auth-preferred-username")
2023-02-09 12:16:36 +01:00
logger.SetFormat(logger.Format(logFormat))
logger.SetLevel(logger.Level(logLevel))
cmdCtx := ctx.Context
absPath, err := filepath.Abs(path)
if err != nil {
return errors.Wrapf(err, "could not resolve path '%s'", path)
}
logger.Info(cmdCtx, "opening app bundle", logger.F("path", absPath))
bundle, err := bundle.FromPath(path)
if err != nil {
return errors.Wrapf(err, "could not open path '%s' as an app bundle", path)
}
mux := chi.NewMux()
mux.Use(middleware.Logger)
mux.Use(dummyAuthMiddleware(authSubject, authRole, authPreferredUsername))
2023-02-09 12:16:36 +01:00
bus := memory.NewBus()
db, err := sql.Open("sqlite", storageFile)
if err != nil {
return errors.Wrapf(err, "could not open database with path '%s'", storageFile)
}
ds := sqlite.NewDocumentStoreWithDB(db)
bs := sqlite.NewBlobStoreWithDB(db)
2023-02-09 12:16:36 +01:00
handler := appHTTP.NewHandler(
appHTTP.WithBus(bus),
appHTTP.WithServerModules(getServerModules(bus, ds, bs)...),
2023-02-09 12:16:36 +01:00
)
if err := handler.Load(bundle); err != nil {
return errors.Wrap(err, "could not load app bundle")
}
mux.Handle("/*", handler)
logger.Info(cmdCtx, "listening", logger.F("address", address))
if err := http.ListenAndServe(address, mux); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}
func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStore) []app.ServerModuleFactory {
return []app.ServerModuleFactory{
module.ContextModuleFactory(),
module.ConsoleModuleFactory(),
cast.CastModuleFactory(),
module.LifecycleModuleFactory(),
net.ModuleFactory(bus),
module.RPCModuleFactory(bus),
module.StoreModuleFactory(ds),
module.BlobModuleFactory(bus, bs),
module.Extends(
auth.ModuleFactory(
auth.WithJWT(dummyKeyFunc),
),
func(o *goja.Object) {
if err := o.Set("CLAIM_ROLE", "role"); err != nil {
panic(errors.New("could not set 'CLAIM_ROLE' property"))
}
if err := o.Set("CLAIM_PREFERRED_USERNAME", "preferred_username"); err != nil {
panic(errors.New("could not set 'CLAIM_PREFERRED_USERNAME' property"))
}
},
),
}
}
var dummySecret = []byte("not_so_secret")
func dummyKeyFunc(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"])
}
return dummySecret, nil
}
func dummyAuthMiddleware(subject, role, username string) func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
unauthenticated := subject == "" && role == "" && username == ""
if unauthenticated {
h.ServeHTTP(w, r)
return
}
claims := jwt.MapClaims{
"nbf": time.Now().UTC().Unix(),
}
if subject != "" {
claims["sub"] = subject
}
if role != "" {
claims["role"] = role
}
if username != "" {
claims["preferred_username"] = username
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ctx := r.Context()
rawToken, err := token.SignedString(dummySecret)
if err != nil {
logger.Error(ctx, "could not sign token", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
r.Header.Add("Authorization", "Bearer "+rawToken)
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}