emissary/internal/agent/controller/app/app_handler.go

280 lines
7.1 KiB
Go
Raw Normal View History

2023-03-28 20:43:45 +02:00
package app
import (
"bytes"
"context"
"database/sql"
"net"
2023-03-28 20:43:45 +02:00
"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/auth"
"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"
2023-03-28 20:43:45 +02:00
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
"github.com/Masterminds/sprig/v3"
"github.com/dop251/goja"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
2023-03-28 20:43:45 +02:00
)
const defaultSQLiteParams = "?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_txlock=immediate"
2023-03-28 20:43:45 +02:00
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)
2023-03-28 20:43:45 +02:00
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")
}
bundles := make([]string, 0, len(specs.Apps))
for appKey, app := range specs.Apps {
path := c.getAppBundlePath(appKey, app.Format)
bundles = append(bundles, path)
}
bus := memory.NewBus()
modules := c.getAppModules(bus, db, specs, keySet)
2023-03-28 20:43:45 +02:00
options := []edgeHTTP.HandlerOptionFunc{
edgeHTTP.WithBus(bus),
edgeHTTP.WithServerModules(modules...),
}
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
}
2023-03-28 20:43:45 +02:00
ifaceMappings := make(map[string]*template.Template, len(rawIfaceMappings))
for iface, rawTemplate := range rawIfaceMappings {
tmpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(rawTemplate)
2023-03-28 20:43:45 +02:00
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),
)
}
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
}
}
2023-03-28 20:43:45 +02:00
}
if urlTemplate == nil {
urlTemplate = defaultTemplate
}
2023-03-28 20:43:45 +02:00
if deviceIP == nil {
deviceIP = net.ParseIP("127.0.0.1")
2023-03-28 20:43:45 +02:00
}
var appEntry *spec.AppEntry
for appID, entry := range specs.Apps {
if manifest.ID != app.ID(appID) {
continue
}
2023-03-28 20:43:45 +02:00
appEntry = &entry
}
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)
}
2023-03-28 20:43:45 +02:00
data := struct {
Manifest *app.Manifest
Specs *spec.Spec
DeviceIP string
AppPort string
2023-03-28 20:43:45 +02:00
}{
Manifest: manifest,
Specs: specs,
DeviceIP: deviceIP.String(),
AppPort: port,
2023-03-28 20:43:45 +02:00
}
var buf bytes.Buffer
2023-03-28 20:43:45 +02:00
if err := urlTemplate.Execute(&buf, data); err != nil {
return "", errors.WithStack(err)
}
return buf.String(), nil
}, nil
2023-03-28 20:43:45 +02:00
}
func (c *Controller) getAppModules(bus bus.Bus, db *sql.DB, spec *appSpec.Spec, keySet jwk.Set) []app.ServerModuleFactory {
2023-03-28 20:43:45 +02:00
ds := sqlite.NewDocumentStoreWithDB(db)
bs := sqlite.NewBlobStoreWithDB(db)
return []app.ServerModuleFactory{
module.ContextModuleFactory(),
module.ConsoleModuleFactory(),
cast.CastModuleFactory(),
module.LifecycleModuleFactory(),
netModule.ModuleFactory(bus),
2023-03-28 20:43:45 +02:00
module.RPCModuleFactory(bus),
module.StoreModuleFactory(ds),
blob.ModuleFactory(bus, bs),
module.Extends(
auth.ModuleFactory(
auth.WithJWT(func() (jwk.Set, error) {
return keySet, nil
}),
),
func(o *goja.Object) {
if err := o.Set("CLAIM_TENANT", "arcad_tenant"); err != nil {
panic(errors.New("could not set 'CLAIM_TENANT' property"))
}
if err := o.Set("CLAIM_ENTRYPOINT", "arcad_entrypoint"); err != nil {
panic(errors.New("could not set 'CLAIM_ENTRYPOINT' property"))
}
if err := o.Set("CLAIM_ROLE", "arcad_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"))
}
},
),
appModule.ModuleFactory(c.appRepository),
fetchModule.ModuleFactory(bus),
2023-03-28 20:43:45 +02:00
}
}