package app import ( "database/sql" "fmt" "net/http" "path/filepath" "time" "forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/bus" "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" "forge.cadoles.com/arcad/edge/pkg/module/cast" "forge.cadoles.com/arcad/edge/pkg/module/net" "forge.cadoles.com/arcad/edge/pkg/storage" "forge.cadoles.com/arcad/edge/pkg/storage/sqlite" "gitlab.com/wpetit/goweb/logger" "forge.cadoles.com/arcad/edge/pkg/bundle" "github.com/dop251/goja" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/golang-jwt/jwt" "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", }, }, Action: func(ctx *cli.Context) error { address := ctx.String("address") path := ctx.String("path") logFormat := ctx.String("log-format") logLevel := ctx.Int("log-level") storageFile := ctx.String("storage-file") authSubject := ctx.String("auth-subject") authRole := ctx.String("auth-role") authPreferredUsername := ctx.String("auth-preferred-username") 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)) 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) handler := appHTTP.NewHandler( appHTTP.WithBus(bus), appHTTP.WithServerModules(getServerModules(bus, ds, bs)...), ) 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) } }