package app import ( "bytes" "context" "database/sql" "net" "path/filepath" "text/template" "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec" appSpec "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec" "forge.cadoles.com/Cadoles/emissary/internal/jwk" "forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/bus" "forge.cadoles.com/arcad/edge/pkg/bus/memory" edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http" "forge.cadoles.com/arcad/edge/pkg/module" appModule "forge.cadoles.com/arcad/edge/pkg/module/app" "forge.cadoles.com/arcad/edge/pkg/module/blob" "forge.cadoles.com/arcad/edge/pkg/module/cast" fetchModule "forge.cadoles.com/arcad/edge/pkg/module/fetch" netModule "forge.cadoles.com/arcad/edge/pkg/module/net" "forge.cadoles.com/arcad/edge/pkg/storage/sqlite" "github.com/Masterminds/sprig/v3" "github.com/go-chi/chi/v5" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/logger" ) const defaultSQLiteParams = "?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000" func (c *Controller) getHandlerOptions(ctx context.Context, appKey string, specs *spec.Spec) ([]edgeHTTP.HandlerOptionFunc, error) { dataDir, err := c.ensureAppDataDir(ctx, appKey) if err != nil { return nil, errors.Wrap(err, "could not retrieve app data dir") } dbFile := filepath.Join(dataDir, appKey+".sqlite") db, err := sqlite.Open(dbFile + defaultSQLiteParams) if err != nil { return nil, errors.Wrapf(err, "could not open database file '%s'", dbFile) } keySet, err := getAuthKeySet(specs.Config) if err != nil { return nil, errors.Wrap(err, "could not retrieve auth key set") } mounts := make([]func(r chi.Router), 0) authMount, err := getAuthMount(specs.Config.Auth, keySet) if err != nil { return nil, errors.WithStack(err) } if authMount != nil { mounts = append(mounts, authMount) } mounts = append(mounts, appModule.Mount(c.appRepository)) bus := memory.NewBus() modules := c.getAppModules(bus, db, specs, keySet) options := []edgeHTTP.HandlerOptionFunc{ edgeHTTP.WithBus(bus), edgeHTTP.WithServerModules(modules...), edgeHTTP.WithHTTPMounts(mounts...), } return options, nil } func getAuthKeySet(config *spec.Config) (jwk.Set, error) { keySet := jwk.NewSet() if config == nil { return nil, nil } auth := config.Auth if auth == nil { return nil, nil } switch { case auth.Local != nil: var ( key jwk.Key err error ) switch typedKey := auth.Local.Key.(type) { case string: key, err = jwk.FromRaw([]byte(typedKey)) if err != nil { return nil, errors.Wrap(err, "could not parse local auth key") } if err := key.Set(jwk.AlgorithmKey, jwa.HS256); err != nil { return nil, errors.WithStack(err) } default: return nil, errors.Errorf("unexpected key type '%T'", auth.Local.Key) } if err := keySet.AddKey(key); err != nil { return nil, errors.WithStack(err) } } return keySet, nil } func createResolveAppURL(specs *spec.Spec) (ResolveAppURLFunc, error) { rawIfaceMappings := make(map[string]string, 0) if specs.Config != nil && specs.Config.AppURLResolving != nil && specs.Config.AppURLResolving.IfaceMappings != nil { rawIfaceMappings = specs.Config.AppURLResolving.IfaceMappings } ifaceMappings := make(map[string]*template.Template, len(rawIfaceMappings)) for iface, rawTemplate := range rawIfaceMappings { tmpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(rawTemplate) if err != nil { return nil, errors.Wrapf(err, "could not parse iface '%s' template", iface) } ifaceMappings[iface] = tmpl } defaultRawTemplate := `http://{{ .DeviceIP }}:{{ .AppPort }}` if specs.Config != nil && specs.Config.AppURLResolving != nil && specs.Config.AppURLResolving.DefaultURLTemplate != "" { defaultRawTemplate = specs.Config.AppURLResolving.DefaultURLTemplate } defaultTemplate, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(defaultRawTemplate) if err != nil { return nil, errors.WithStack(err) } return func(ctx context.Context, manifest *app.Manifest, from string) (string, error) { var ( urlTemplate *template.Template deviceIP net.IP ) fromIP := net.ParseIP(from) if fromIP != nil { LOOP: for ifaceName, ifaceTmpl := range ifaceMappings { iface, err := net.InterfaceByName(ifaceName) if err != nil { logger.Error( ctx, "could not find interface", logger.E(errors.WithStack(err)), logger.F("iface", ifaceName), ) continue } addresses, err := iface.Addrs() if err != nil { logger.Error( ctx, "could not list interface addresses", logger.E(errors.WithStack(err)), logger.F("iface", iface.Name), ) continue } for _, addr := range addresses { ifaIP, network, err := net.ParseCIDR(addr.String()) if err != nil { logger.Error( ctx, "could not parse interface ip", logger.E(errors.WithStack(err)), logger.F("iface", iface.Name), ) continue } if !network.Contains(fromIP) { continue } deviceIP = ifaIP urlTemplate = ifaceTmpl break LOOP } } } if urlTemplate == nil { urlTemplate = defaultTemplate } if deviceIP == nil { deviceIP = net.ParseIP("127.0.0.1") } var appEntry *spec.AppEntry for appID, entry := range specs.Apps { if manifest.ID != app.ID(appID) { continue } appEntry = &entry break } if appEntry == nil { return "", errors.Errorf("could not find app '%s' in specs", manifest.ID) } _, port, err := net.SplitHostPort(appEntry.Address) if err != nil { return "", errors.WithStack(err) } data := struct { Manifest *app.Manifest Specs *spec.Spec DeviceIP string AppPort string }{ Manifest: manifest, Specs: specs, DeviceIP: deviceIP.String(), AppPort: port, } var buf bytes.Buffer if err := urlTemplate.Execute(&buf, data); err != nil { return "", errors.WithStack(err) } return buf.String(), nil }, nil } func (c *Controller) getAppModules(bus bus.Bus, db *sql.DB, spec *appSpec.Spec, keySet jwk.Set) []app.ServerModuleFactory { ds := sqlite.NewDocumentStoreWithDB(db) bs := sqlite.NewBlobStoreWithDB(db) return []app.ServerModuleFactory{ module.ContextModuleFactory(), module.ConsoleModuleFactory(), cast.CastModuleFactory(), module.LifecycleModuleFactory(), netModule.ModuleFactory(bus), module.RPCModuleFactory(bus), module.StoreModuleFactory(ds), blob.ModuleFactory(bus, bs), authModuleFactory(keySet), appModule.ModuleFactory(c.appRepository), fetchModule.ModuleFactory(bus), } }