feat(module,app): iface-based app url resolving
arcad/emissary/pipeline/head There was a failure building this commit Details

This commit is contained in:
wpetit 2023-04-05 23:21:43 +02:00
parent 242a247222
commit 2e1ee44e6a
13 changed files with 454 additions and 188 deletions

2
go.mod
View File

@ -3,7 +3,7 @@ module forge.cadoles.com/Cadoles/emissary
go 1.19 go 1.19
require ( require (
forge.cadoles.com/arcad/edge v0.0.0-20230402160147-f08f645432c6 forge.cadoles.com/arcad/edge v0.0.0-20230405131922-006f13bc7b88
github.com/Masterminds/sprig/v3 v3.2.3 github.com/Masterminds/sprig/v3 v3.2.3
github.com/alecthomas/participle/v2 v2.0.0-beta.5 github.com/alecthomas/participle/v2 v2.0.0-beta.5
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883

2
go.sum
View File

@ -58,6 +58,8 @@ forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab h1:xOtzLAYOUcKd/
forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw= forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw=
forge.cadoles.com/arcad/edge v0.0.0-20230402160147-f08f645432c6 h1:MxMEBSEvwagUrFORUJ9snZekFIKkaV3OB0EplXra+LU= forge.cadoles.com/arcad/edge v0.0.0-20230402160147-f08f645432c6 h1:MxMEBSEvwagUrFORUJ9snZekFIKkaV3OB0EplXra+LU=
forge.cadoles.com/arcad/edge v0.0.0-20230402160147-f08f645432c6/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw= forge.cadoles.com/arcad/edge v0.0.0-20230402160147-f08f645432c6/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw=
forge.cadoles.com/arcad/edge v0.0.0-20230405131922-006f13bc7b88 h1:d+AXw/Kx8X7M4GvtPC/mkZWK3tRMT051XORAiQAnClA=
forge.cadoles.com/arcad/edge v0.0.0-20230405131922-006f13bc7b88/go.mod h1:Vx4iq/oewXUOkGyi8QKc14clTLNO1sWpb0SjBYELlAs=
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg=
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=

View File

@ -4,8 +4,8 @@ import (
"bytes" "bytes"
"context" "context"
"database/sql" "database/sql"
"net"
"path/filepath" "path/filepath"
"sync"
"text/template" "text/template"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec" "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
@ -21,12 +21,13 @@ import (
"forge.cadoles.com/arcad/edge/pkg/module/blob" "forge.cadoles.com/arcad/edge/pkg/module/blob"
"forge.cadoles.com/arcad/edge/pkg/module/cast" "forge.cadoles.com/arcad/edge/pkg/module/cast"
fetchModule "forge.cadoles.com/arcad/edge/pkg/module/fetch" fetchModule "forge.cadoles.com/arcad/edge/pkg/module/fetch"
"forge.cadoles.com/arcad/edge/pkg/module/net" netModule "forge.cadoles.com/arcad/edge/pkg/module/net"
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite" "forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
"github.com/Masterminds/sprig/v3" "github.com/Masterminds/sprig/v3"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwa"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
) )
const defaultSQLiteParams = "?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_txlock=immediate" const defaultSQLiteParams = "?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_txlock=immediate"
@ -107,46 +108,132 @@ func getAuthKeySet(config *spec.Config) (jwk.Set, error) {
return keySet, nil return keySet, nil
} }
func createGetAppURL(specs *spec.Spec) GetURLFunc { 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 ( var (
compileOnce sync.Once
urlTemplate *template.Template urlTemplate *template.Template
err error deviceIP net.IP
) )
return func(ctx context.Context, manifest *app.Manifest) (string, error) { 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
}
}
}
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
}
if appEntry == nil {
return "", errors.Errorf("could not find app '%s' in specs", manifest.ID)
}
_, port, err := net.SplitHostPort(appEntry.Address)
if err != nil { if err != nil {
return "", errors.WithStack(err) return "", errors.WithStack(err)
} }
var appURLTemplate string
if specs.Config == nil || specs.Config.AppURLTemplate == "" {
appURLTemplate = `http://{{ last ( splitList "." ( toString .Manifest.ID ) ) }}.local`
} else {
appURLTemplate = specs.Config.AppURLTemplate
}
compileOnce.Do(func() {
urlTemplate, err = template.New("").Funcs(sprig.TxtFuncMap()).Parse(appURLTemplate)
})
var buf bytes.Buffer
data := struct { data := struct {
Manifest *app.Manifest Manifest *app.Manifest
Specs *spec.Spec Specs *spec.Spec
DeviceIP string
AppPort string
}{ }{
Manifest: manifest, Manifest: manifest,
Specs: specs, Specs: specs,
DeviceIP: deviceIP.String(),
AppPort: port,
} }
var buf bytes.Buffer
if err := urlTemplate.Execute(&buf, data); err != nil { if err := urlTemplate.Execute(&buf, data); err != nil {
return "", errors.WithStack(err) return "", errors.WithStack(err)
} }
return buf.String(), nil return buf.String(), nil
} }, nil
} }
func (c *Controller) getAppModules(bus bus.Bus, db *sql.DB, spec *appSpec.Spec, keySet jwk.Set) []app.ServerModuleFactory { func (c *Controller) getAppModules(bus bus.Bus, db *sql.DB, spec *appSpec.Spec, keySet jwk.Set) []app.ServerModuleFactory {
@ -158,7 +245,7 @@ func (c *Controller) getAppModules(bus bus.Bus, db *sql.DB, spec *appSpec.Spec,
module.ConsoleModuleFactory(), module.ConsoleModuleFactory(),
cast.CastModuleFactory(), cast.CastModuleFactory(),
module.LifecycleModuleFactory(), module.LifecycleModuleFactory(),
net.ModuleFactory(bus), netModule.ModuleFactory(bus),
module.RPCModuleFactory(bus), module.RPCModuleFactory(bus),
module.StoreModuleFactory(ds), module.StoreModuleFactory(ds),
blob.ModuleFactory(bus, bs), blob.ModuleFactory(bus, bs),

View File

@ -0,0 +1,66 @@
package app
import (
"context"
"testing"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
"forge.cadoles.com/arcad/edge/pkg/app"
"github.com/pkg/errors"
)
func TestCreateResolveAppURL(t *testing.T) {
specs := &spec.Spec{
Apps: map[string]spec.AppEntry{
"app.arcad.test": {
Address: ":8080",
},
},
Config: &spec.Config{
AppURLResolving: &spec.AppURLResolving{
IfaceMappings: map[string]string{
"lo": "http://{{ .DeviceIP }}:{{ .AppPort }}",
},
DefaultURLTemplate: `http://{{ last ( splitList "." ( toString .Manifest.ID ) ) }}.arcad.local`,
},
},
}
resolveAppURL, err := createResolveAppURL(specs)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
manifest := &app.Manifest{
ID: "app.arcad.test",
}
ctx := context.Background()
url, err := resolveAppURL(ctx, manifest, "127.0.0.2")
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := "http://127.0.0.1:8080", url; e != g {
t.Errorf("url: expected '%s', got '%s", e, g)
}
url, err = resolveAppURL(ctx, manifest, "")
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := "http://test.arcad.local", url; e != g {
t.Errorf("url: expected '%s', got '%s", e, g)
}
url, err = resolveAppURL(ctx, manifest, "192.168.0.100")
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := "http://test.arcad.local", url; e != g {
t.Errorf("url: expected '%s', got '%s", e, g)
}
}

View File

@ -11,10 +11,10 @@ import (
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
type GetURLFunc func(context.Context, *app.Manifest) (string, error) type ResolveAppURLFunc func(context.Context, *app.Manifest, string) (string, error)
type AppRepository struct { type AppRepository struct {
getURL GetURLFunc resolveAppURL ResolveAppURLFunc
bundles []string bundles []string
mutex sync.RWMutex mutex sync.RWMutex
} }
@ -33,7 +33,7 @@ func (r *AppRepository) Get(ctx context.Context, id app.ID) (*app.Manifest, erro
} }
// GetURL implements app.Repository // GetURL implements app.Repository
func (r *AppRepository) GetURL(ctx context.Context, id app.ID) (string, error) { func (r *AppRepository) GetURL(ctx context.Context, id app.ID, from string) (string, error) {
r.mutex.RLock() r.mutex.RLock()
defer r.mutex.RUnlock() defer r.mutex.RUnlock()
@ -42,7 +42,7 @@ func (r *AppRepository) GetURL(ctx context.Context, id app.ID) (string, error) {
return "", errors.WithStack(err) return "", errors.WithStack(err)
} }
url, err := r.getURL(ctx, manifest) url, err := r.resolveAppURL(ctx, manifest, from)
if err != nil { if err != nil {
return "", errors.WithStack(err) return "", errors.WithStack(err)
} }
@ -80,11 +80,11 @@ func (r *AppRepository) List(ctx context.Context) ([]*app.Manifest, error) {
return manifests, nil return manifests, nil
} }
func (r *AppRepository) Update(getURL GetURLFunc, bundles []string) { func (r *AppRepository) Update(resolveAppURL ResolveAppURLFunc, bundles []string) {
r.mutex.Lock() r.mutex.Lock()
defer r.mutex.Unlock() defer r.mutex.Unlock()
r.getURL = getURL r.resolveAppURL = resolveAppURL
r.bundles = bundles r.bundles = bundles
} }
@ -118,7 +118,7 @@ func (r *AppRepository) findManifest(ctx context.Context, id app.ID) (*app.Manif
func NewAppRepository() *AppRepository { func NewAppRepository() *AppRepository {
return &AppRepository{ return &AppRepository{
getURL: func(ctx context.Context, m *app.Manifest) (string, error) { resolveAppURL: func(ctx context.Context, m *app.Manifest, from string) (string, error) {
return "", errors.New("unavailable") return "", errors.New("unavailable")
}, },
bundles: []string{}, bundles: []string{},

View File

@ -96,7 +96,14 @@ func (c *Controller) updateApps(ctx context.Context, specs *spec.Spec) {
} }
} }
c.updateAppRepository(ctx, specs) if err := c.updateAppRepository(ctx, specs); err != nil {
logger.Error(
ctx, "could not update app repository",
logger.E(errors.WithStack(err)),
)
return
}
// (Re)start apps if necessary // (Re)start apps if necessary
for appKey := range specs.Apps { for appKey := range specs.Apps {
@ -109,32 +116,32 @@ func (c *Controller) updateApps(ctx context.Context, specs *spec.Spec) {
} }
} }
func (c *Controller) updateAppRepository(ctx context.Context, specs *spec.Spec) { func (c *Controller) updateAppRepository(ctx context.Context, specs *spec.Spec) error {
bundles := make([]string, 0, len(specs.Apps)) bundles := make([]string, 0, len(specs.Apps))
for appKey, app := range specs.Apps { for appKey, app := range specs.Apps {
path := c.getAppBundlePath(appKey, app.Format) path := c.getAppBundlePath(appKey, app.Format)
bundles = append(bundles, path) bundles = append(bundles, path)
} }
getURL := createGetAppURL(specs) resolveAppURL, err := createResolveAppURL(specs)
if err != nil {
return errors.WithStack(err)
}
c.appRepository.Update(getURL, bundles) c.appRepository.Update(resolveAppURL, bundles)
return nil
} }
func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey string) (err error) { func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey string) (err error) {
appEntry := specs.Apps[appKey] appEntry := specs.Apps[appKey]
var auth *spec.Auth
if specs.Config != nil {
auth = specs.Config.Auth
}
appDef := struct { appDef := struct {
App spec.AppEntry App spec.AppEntry
Auth *spec.Auth Config *spec.Config
}{ }{
App: appEntry, App: appEntry,
Auth: auth, Config: specs.Config,
} }
newAppDefHash, err := hashstructure.Hash(appDef, hashstructure.FormatV2, nil) newAppDefHash, err := hashstructure.Hash(appDef, hashstructure.FormatV2, nil)
@ -164,27 +171,30 @@ func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey str
server = nil server = nil
} }
if server == nil { newServerEntry := func() (*serverEntry, error) {
options, err := c.getHandlerOptions(ctx, appKey, specs) options, err := c.getHandlerOptions(ctx, appKey, specs)
if err != nil { if err != nil {
return errors.Wrap(err, "could not create handler options") return nil, errors.Wrap(err, "could not create handler options")
}
var auth *spec.Auth
if specs.Config != nil {
auth = specs.Config.Auth
} }
server = &serverEntry{ server = &serverEntry{
Server: NewServer(bundle, auth, options...), Server: NewServer(bundle, specs.Config, options...),
AppDefHash: 0, AppDefHash: 0,
} }
c.servers[appKey] = server return server, nil
}
if server == nil {
serverEntry, err := newServerEntry()
if err != nil {
return errors.WithStack(err)
}
c.servers[appKey] = serverEntry
} }
defChanged := newAppDefHash != server.AppDefHash defChanged := newAppDefHash != server.AppDefHash
if server.Server.Running() && !defChanged { if server.Server.Running() && !defChanged {
return nil return nil
} }
@ -194,6 +204,17 @@ func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey str
ctx, "restarting app", ctx, "restarting app",
logger.F("address", appEntry.Address), logger.F("address", appEntry.Address),
) )
if err := server.Server.Stop(); err != nil {
return errors.WithStack(err)
}
serverEntry, err := newServerEntry()
if err != nil {
return errors.WithStack(err)
}
c.servers[appKey] = serverEntry
} else { } else {
logger.Info( logger.Info(
ctx, "starting app", ctx, "starting app",

View File

@ -2,6 +2,7 @@ package app
import ( import (
"context" "context"
"net"
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
@ -31,7 +32,7 @@ type Server struct {
handlerOptions []edgeHTTP.HandlerOptionFunc handlerOptions []edgeHTTP.HandlerOptionFunc
server *http.Server server *http.Server
serverMutex sync.RWMutex serverMutex sync.RWMutex
auth *appSpec.Auth config *appSpec.Config
} }
func (s *Server) Start(ctx context.Context, addr string) (err error) { func (s *Server) Start(ctx context.Context, addr string) (err error) {
@ -53,9 +54,20 @@ func (s *Server) Start(ctx context.Context, addr string) (err error) {
return errors.Wrap(err, "could not load app bundle") return errors.Wrap(err, "could not load app bundle")
} }
if err := s.configureAuth(router, s.auth); err != nil { if s.config != nil {
if s.config.UnexpectedHostRedirect != nil {
router.Use(unexpectedHostRedirect(
s.config.UnexpectedHostRedirect.HostTarget,
s.config.UnexpectedHostRedirect.AcceptedHostPatterns...,
))
}
if s.config.Auth != nil {
if err := s.configureAuth(router, s.config.Auth); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
}
}
router.Handle("/*", handler) router.Handle("/*", handler)
@ -124,13 +136,9 @@ func (s *Server) Stop() error {
} }
func (s *Server) configureAuth(router chi.Router, auth *spec.Auth) error { func (s *Server) configureAuth(router chi.Router, auth *spec.Auth) error {
if auth == nil {
return nil
}
switch { switch {
case auth.Local != nil: case auth.Local != nil:
var rawKey any = s.auth.Local.Key var rawKey any = auth.Local.Key
if strKey, ok := rawKey.(string); ok { if strKey, ok := rawKey.(string); ok {
rawKey = []byte(strKey) rawKey = []byte(strKey)
} }
@ -141,53 +149,74 @@ func (s *Server) configureAuth(router chi.Router, auth *spec.Auth) error {
} }
cookieDuration := defaultCookieDuration cookieDuration := defaultCookieDuration
if s.auth.Local.CookieDuration != "" { if auth.Local.CookieDuration != "" {
cookieDuration, err = time.ParseDuration(s.auth.Local.CookieDuration) cookieDuration, err = time.ParseDuration(auth.Local.CookieDuration)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
} }
if s.auth.Local.CookieDomain != "" {
router.Use(invalidCookieDomainRedirect(s.auth.Local.CookieDomain))
}
router.Handle("/auth/*", authHTTP.NewLocalHandler( router.Handle("/auth/*", authHTTP.NewLocalHandler(
jwa.HS256, key, jwa.HS256, key,
authHTTP.WithRoutePrefix("/auth"), authHTTP.WithRoutePrefix("/auth"),
authHTTP.WithAccounts(s.auth.Local.Accounts...), authHTTP.WithAccounts(auth.Local.Accounts...),
authHTTP.WithCookieOptions(s.auth.Local.CookieDomain, cookieDuration), authHTTP.WithCookieOptions(getCookieDomain, cookieDuration),
)) ))
} }
return nil return nil
} }
func NewServer(bundle bundle.Bundle, auth *appSpec.Auth, handlerOptions ...edgeHTTP.HandlerOptionFunc) *Server { func NewServer(bundle bundle.Bundle, config *spec.Config, handlerOptions ...edgeHTTP.HandlerOptionFunc) *Server {
return &Server{ return &Server{
bundle: bundle, bundle: bundle,
auth: auth, config: config,
handlerOptions: handlerOptions, handlerOptions: handlerOptions,
} }
} }
func invalidCookieDomainRedirect(cookieDomain string) func(http.Handler) http.Handler { func getCookieDomain(r *http.Request) (string, error) {
domain := strings.TrimPrefix(cookieDomain, ".") host, _, err := net.SplitHostPort(r.Host)
hostPattern := "*" + domain if err != nil {
return "", errors.WithStack(err)
return func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
hostParts := strings.SplitN(r.Host, ":", 2)
if !wildcard.Match(hostParts[0], hostPattern) {
url := r.URL
newHost := domain
if len(hostParts) > 1 {
newHost += ":" + hostParts[1]
} }
url.Host = newHost // If host is an IP address
if wildcard.Match(host, "*.*.*.*") {
return "", nil
}
// If host is an domain, return top level domain
domainParts := strings.Split(host, ".")
if len(domainParts) >= 2 {
topLevelDomain := strings.Join(domainParts[len(domainParts)-2:], ".")
return topLevelDomain, nil
}
// By default, return host
return host, nil
}
func unexpectedHostRedirect(hostTarget string, acceptedHostPatterns ...string) func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
host, port, err := net.SplitHostPort(r.Host)
if err != nil {
logger.Error(r.Context(), "could not split host/port", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
matched := wildcard.MatchAny(host, acceptedHostPatterns...)
if !matched {
url := r.URL
url.Host = hostTarget
if port != "" {
url.Host += ":" + port
}
http.Redirect(w, r, url.String(), http.StatusTemporaryRedirect) http.Redirect(w, r, url.String(), http.StatusTemporaryRedirect)

View File

@ -38,6 +38,43 @@
} }
} }
}, },
"config": {
"type": "object",
"properties": {
"appUrlResolving": {
"type": "object",
"properties": {
"ifaceMappings": {
"type": "object",
"patternProperties": {
".*": {
"type": "string"
}
}
},
"defaultUrlTemplate": {
"type": "string"
}
},
"required": ["defaultUrlTemplate"],
"additionalProperties": false
},
"unexpectedHostRedirect": {
"type": "object",
"properties": {
"acceptedHostPatterns": {
"type": "array",
"items": {
"type": "string"
}
},
"hostTarget": {
"type": "string"
}
},
"required": ["acceptedHostPatterns", "hostTarget"],
"additionalProperties": false
},
"auth": { "auth": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -81,14 +118,14 @@
}, },
"required": [ "required": [
"key" "key"
] ],
} "additionalProperties": false
} }
}, },
"config": { "additionalProperties": false
"appUrlTemplate": {
"type": "string"
} }
},
"additionalProperties": false
} }
}, },
"required": [ "required": [

View File

@ -33,7 +33,18 @@ type LocalAuth struct {
type Config struct { type Config struct {
Auth *Auth `json:"auth"` Auth *Auth `json:"auth"`
AppURLTemplate string `json:"appUrlTemplate"` UnexpectedHostRedirect *UnexpectedHostRedirect `json:"unexpectedHostRedirect"`
AppURLResolving *AppURLResolving `json:"appUrlResolving"`
}
type UnexpectedHostRedirect struct {
AcceptedHostPatterns []string `json:"acceptedHostPatterns"`
HostTarget string `json:"hostTarget"`
}
type AppURLResolving struct {
IfaceMappings map[string]string `json:"ifaceMappings"`
DefaultURLTemplate string `json:"defaultUrlTemplate"`
} }
func (s *Spec) SpecName() spec.Name { func (s *Spec) SpecName() spec.Name {

View File

@ -9,6 +9,7 @@
"format": "zip" "format": "zip"
} }
}, },
"config": {
"auth": { "auth": {
"local": { "local": {
"key": { "key": {
@ -36,6 +37,17 @@
} }
] ]
} }
},
"unexpectedHostRedirect": {
"acceptedHostPatterns": ["arcad.local", "*.arcad.local", "arcad-*.local", "*.*.*.*"],
"hostTarget": "arcad.local"
},
"appUrlResolving": {
"ifaceMappings": {
"eth0": "http://{{ .DeviceIP }}:{{ .AppHost }}"
},
"defaultUrlTemplate": "http://{{ last ( splitList \".\" ( toString .Manifest.ID ) ) }}.arcad.local"
}
} }
}, },
"revision": 0 "revision": 0

View File

@ -1,36 +1,46 @@
{ {
"apps": { "apps": {
"portal": { "edge.portal": {
"url": "https://emissary.cadol.es/files/apps/arcad.portal_v2023.3.28-3feda80.zip", "url": "https://emissary.cadol.es/files/apps/edge.portal_v2023.4.5-45546c4.zip",
"sha256sum": "921402c44a5fa554d5b630d1284957b05416aa6872b402314cf52e964e06fac5", "sha256sum": "c83e7e4b3785f5f4d3fcae7cad334819626015b11b446520aa79f42176a2744d",
"address": "127.0.0.1:8082", "address": ":8082",
"format": "zip" "format": "zip"
}, },
"hextris": { "app.arcad.edge.hextris": {
"url": "https://emissary.cadol.es/files/apps/app.arcad.edge.hextris_v2023.3.22-33ece28.zip", "url": "https://emissary.cadol.es/files/apps/app.arcad.edge.hextris_v2023.3.22-33ece28.zip",
"sha256sum": "5f9f3c8d6f22796beb051d747d7ff12efa17af9d1552c0ab08baef13703a2aba", "sha256sum": "5f9f3c8d6f22796beb051d747d7ff12efa17af9d1552c0ab08baef13703a2aba",
"address": "127.0.0.1:8083", "address": ":8083",
"format": "zip" "format": "zip"
}, },
"test": { "edge.sdk.client.test": {
"url": "https://emissary.cadol.es/files/apps/edge.sdk.client.test_v2023.3.24-ed535b6.zip", "url": "https://emissary.cadol.es/files/apps/edge.sdk.client.test_v2023.4.2-f08f645.zip",
"sha256sum": "e97b7b79159bb5d6a13b05644c091272b02a1a3cbb1b613dd5eda37e1eb84623", "sha256sum": "8b48388c817802ebeb38907b3a42f1189dc0759f94c5f33de4546c1a7ebfc784",
"address": "127.0.0.1:8084", "address": ":8084",
"format": "zip" "format": "zip"
}, },
"diffusion": { "arcad.diffusion": {
"url": "https://emissary.cadol.es/files/apps/arcad.diffusion_v2023.3.29-5b3fab4.zip", "url": "https://emissary.cadol.es/files/apps/arcad.diffusion_v2023.4.5-ffcd1c7.zip",
"sha256sum": "1282e75719beedbc7c7e67879389d0f3e11c86d3d2c37cf13da624a66faaeb58", "sha256sum": "a51a961212470ce1de4527aaaec9e8e0286a978ec675ff9df29b2029daf05a55",
"address": "127.0.0.1:8085", "address": ":8085",
"format": "zip" "format": "zip"
} }
}, },
"config": { "config": {
"appUrlTemplate": "http://{{ last ( splitList \".\" ( toString .Manifest.ID ) ) }}.arcad.local:8080", "appUrlResolving": {
"ifaceMappings": {
"lo": "http://{{ .DeviceIP }}:{{ .AppPort }}",
"wlp4s0": "http://{{ .DeviceIP }}:{{ .AppPort }}",
"enp0s31f6": "http://{{ .DeviceIP }}:{{ .AppPort }}"
},
"defaultUrlTemplate": "http://{{ last ( splitList \".\" ( toString .Manifest.ID ) ) }}.localhost.arcad.lan:8080"
},
"unexpectedHostRedirect": {
"acceptedHostPatterns": ["arcad.lan", "*.arcad.lan", "arcad-*.local", "*.*.*.*"],
"hostTarget": "localhost.arcad.lan"
},
"auth": { "auth": {
"local": { "local": {
"key": "absolutlynotsecret", "key": "absolutlynotsecret",
"cookieDomain": ".arcad.local",
"cookieDuration": "1h", "cookieDuration": "1h",
"accounts": [ "accounts": [
{ {

View File

@ -3,36 +3,27 @@
"arcad": { "arcad": {
"type": "_http._tcp", "type": "_http._tcp",
"port": 8080, "port": 8080,
"host": "arcad", "host": "arcad"
"ifaces": ["wlp4s0"]
}, },
"portal": { "portal": {
"type": "_http._tcp", "type": "_http._tcp",
"port": 8080, "port": 8080,
"host": "portal", "host": "arcad-portal"
"domain": "arcad.local",
"ifaces": ["wlp4s0"]
}, },
"hextris": { "hextris": {
"type": "_http._tcp", "type": "_http._tcp",
"port": 8080, "port": 8080,
"host": "hextris", "host": "arcad-hextris"
"domain": "arcad.local",
"ifaces": ["wlp4s0"]
}, },
"test": { "test": {
"type": "_http._tcp", "type": "_http._tcp",
"port": 8080, "port": 8080,
"host": "test", "host": "arcad-test"
"domain": "arcad.local",
"ifaces": ["wlp4s0"]
}, },
"diffusion": { "diffusion": {
"type": "_http._tcp", "type": "_http._tcp",
"port": 8080, "port": 8080,
"host": "diffusion", "host": "arcad-diffusion"
"domain": "arcad.local",
"ifaces": ["wlp4s0"]
} }
} }
} }

View File

@ -4,19 +4,19 @@
"address": ":8080", "address": ":8080",
"mappings": [ "mappings": [
{ {
"hostPattern": "portal.arcad.local:*", "hostPattern": "portal.localhost.arcad.lan:*",
"target": "http://localhost:8082" "target": "http://localhost:8082"
}, },
{ {
"hostPattern": "hextris.arcad.local:*", "hostPattern": "hextris.localhost.arcad.lan:*",
"target": "http://localhost:8083" "target": "http://localhost:8083"
}, },
{ {
"hostPattern": "test.arcad.local:*", "hostPattern": "test.localhost.arcad.lan:*",
"target": "http://localhost:8084" "target": "http://localhost:8084"
}, },
{ {
"hostPattern": "diffusion.arcad.local:*", "hostPattern": "diffusion.localhost.arcad.lan:*",
"target": "http://localhost:8085" "target": "http://localhost:8085"
}, },
{ {