2023-03-28 20:43:45 +02:00
|
|
|
package app
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"database/sql"
|
2023-04-05 23:21:43 +02:00
|
|
|
"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"
|
2023-04-02 18:05:53 +02:00
|
|
|
fetchModule "forge.cadoles.com/arcad/edge/pkg/module/fetch"
|
2023-04-05 23:21:43 +02:00
|
|
|
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"
|
2023-04-05 23:21:43 +02:00
|
|
|
"gitlab.com/wpetit/goweb/logger"
|
2023-03-28 20:43:45 +02:00
|
|
|
)
|
|
|
|
|
2023-04-06 15:06:16 +02:00
|
|
|
const defaultSQLiteParams = "?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000"
|
2023-03-31 17:27:54 +02:00
|
|
|
|
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")
|
2023-03-31 17:27:54 +02:00
|
|
|
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()
|
2023-03-29 11:27:47 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-04-05 23:21:43 +02:00
|
|
|
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
|
|
|
|
2023-04-05 23:21:43 +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 {
|
2023-04-05 23:21:43 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2023-04-05 23:21:43 +02:00
|
|
|
if urlTemplate == nil {
|
|
|
|
urlTemplate = defaultTemplate
|
|
|
|
}
|
2023-03-28 20:43:45 +02:00
|
|
|
|
2023-04-05 23:21:43 +02:00
|
|
|
if deviceIP == nil {
|
|
|
|
deviceIP = net.ParseIP("127.0.0.1")
|
2023-03-28 20:43:45 +02:00
|
|
|
}
|
|
|
|
|
2023-04-05 23:21:43 +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
|
|
|
|
2023-04-05 23:21:43 +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
|
2023-04-05 23:21:43 +02:00
|
|
|
DeviceIP string
|
|
|
|
AppPort string
|
2023-03-28 20:43:45 +02:00
|
|
|
}{
|
|
|
|
Manifest: manifest,
|
|
|
|
Specs: specs,
|
2023-04-05 23:21:43 +02:00
|
|
|
DeviceIP: deviceIP.String(),
|
|
|
|
AppPort: port,
|
2023-03-28 20:43:45 +02:00
|
|
|
}
|
|
|
|
|
2023-04-05 23:21:43 +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
|
2023-04-05 23:21:43 +02:00
|
|
|
}, nil
|
2023-03-28 20:43:45 +02:00
|
|
|
}
|
|
|
|
|
2023-03-29 11:27:47 +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(),
|
2023-04-05 23:21:43 +02:00
|
|
|
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"))
|
|
|
|
}
|
|
|
|
},
|
|
|
|
),
|
2023-03-29 11:27:47 +02:00
|
|
|
appModule.ModuleFactory(c.appRepository),
|
2023-04-02 18:05:53 +02:00
|
|
|
fetchModule.ModuleFactory(bus),
|
2023-03-28 20:43:45 +02:00
|
|
|
}
|
|
|
|
}
|