package app import ( "bytes" "context" "net" "text/template" "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" shareModule "forge.cadoles.com/arcad/edge/pkg/module/share" "forge.cadoles.com/arcad/edge/pkg/storage" "forge.cadoles.com/arcad/edge/pkg/storage/driver" "forge.cadoles.com/arcad/edge/pkg/storage/share" "github.com/Masterminds/sprig/v3" "github.com/getsentry/sentry-go" "github.com/go-chi/chi/v5" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/logger" // Register storage drivers _ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc" _ "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite" ) type Dependencies struct { Bus bus.Bus DocumentStore storage.DocumentStore BlobStore storage.BlobStore ShareStore share.Store KeySet jwk.Set AppRepository appModule.Repository AppID app.ID } func (c *Controller) getHandlerOptions(ctx context.Context, appKey string, specs *spec.Spec) ([]edgeHTTP.HandlerOptionFunc, error) { appEntry, exists := specs.Apps[appKey] if !exists { return nil, errors.Errorf("could not find app entry '%s'", appKey) } storage := appEntry.Storage if storage == nil { return nil, errors.Errorf("could not find app entry '%s' storage configuration", appKey) } documentStore, err := driver.NewDocumentStore(appEntry.Storage.DocumentStoreDSN) if err != nil { return nil, errors.WithStack(err) } blobStore, err := driver.NewBlobStore(appEntry.Storage.BlobStoreDSN) if err != nil { return nil, errors.WithStack(err) } shareStore, err := driver.NewShareStore(appEntry.Storage.ShareStoreDSN) if err != nil { return nil, errors.WithStack(err) } 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)) deps := Dependencies{ Bus: memory.NewBus(), DocumentStore: documentStore, BlobStore: blobStore, ShareStore: shareStore, KeySet: keySet, AppRepository: c.appRepository, AppID: app.ID(appKey), } modules := c.getAppModules(deps) anonymousUserMiddleware, err := getAnonymousUserMiddleware(specs.Config.Auth) if err != nil { return nil, errors.Wrap(err, "could not get anonymous user middleware") } options := []edgeHTTP.HandlerOptionFunc{ edgeHTTP.WithBus(deps.Bus), edgeHTTP.WithServerModules(modules...), edgeHTTP.WithHTTPMounts(mounts...), edgeHTTP.WithHTTPMiddlewares( anonymousUserMiddleware, ), } 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 { err = errors.WithStack(err) logger.Warn( ctx, "could not find interface", logger.E(err), logger.F("iface", ifaceName), ) sentry.CaptureException(err) continue } addresses, err := iface.Addrs() if err != nil { err = errors.WithStack(err) logger.Error( ctx, "could not list interface addresses", logger.E(err), logger.F("iface", iface.Name), ) sentry.CaptureException(err) continue } for _, addr := range addresses { ifaIP, network, err := net.ParseCIDR(addr.String()) if err != nil { err = errors.WithStack(err) logger.Error( ctx, "could not parse interface ip", logger.E(err), logger.F("iface", iface.Name), ) sentry.CaptureException(err) 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(deps Dependencies) []app.ServerModuleFactory { return []app.ServerModuleFactory{ module.ContextModuleFactory(), module.ConsoleModuleFactory(), cast.CastModuleFactory(), module.LifecycleModuleFactory(), netModule.ModuleFactory(deps.Bus), module.RPCModuleFactory(deps.Bus), module.StoreModuleFactory(deps.DocumentStore), blob.ModuleFactory(deps.Bus, deps.BlobStore), authModuleFactory(deps.KeySet), appModule.ModuleFactory(deps.AppRepository), fetchModule.ModuleFactory(deps.Bus), shareModule.ModuleFactory(deps.AppID, deps.ShareStore), } }