315 lines
7.8 KiB
Go
315 lines
7.8 KiB
Go
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),
|
|
}
|
|
}
|