232 lines
5.8 KiB
Go
232 lines
5.8 KiB
Go
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)
|
|
}
|
|
}
|