feat(module,app): iface-based app url resolving
Some checks failed
arcad/emissary/pipeline/head There was a failure building this commit
Some checks failed
arcad/emissary/pipeline/head There was a failure building this commit
This commit is contained in:
parent
242a247222
commit
2e1ee44e6a
2
go.mod
2
go.mod
@ -3,7 +3,7 @@ module forge.cadoles.com/Cadoles/emissary
|
||||
go 1.19
|
||||
|
||||
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/alecthomas/participle/v2 v2.0.0-beta.5
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
|
||||
|
2
go.sum
2
go.sum
@ -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-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-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=
|
||||
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=
|
||||
|
@ -4,8 +4,8 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"text/template"
|
||||
|
||||
"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/cast"
|
||||
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"
|
||||
"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"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func createGetAppURL(specs *spec.Spec) GetURLFunc {
|
||||
var (
|
||||
compileOnce sync.Once
|
||||
urlTemplate *template.Template
|
||||
err error
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
return func(ctx context.Context, manifest *app.Manifest) (string, error) {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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(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(),
|
||||
cast.CastModuleFactory(),
|
||||
module.LifecycleModuleFactory(),
|
||||
net.ModuleFactory(bus),
|
||||
netModule.ModuleFactory(bus),
|
||||
module.RPCModuleFactory(bus),
|
||||
module.StoreModuleFactory(ds),
|
||||
blob.ModuleFactory(bus, bs),
|
||||
|
66
internal/agent/controller/app/app_handler_test.go
Normal file
66
internal/agent/controller/app/app_handler_test.go
Normal 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)
|
||||
}
|
||||
}
|
@ -11,12 +11,12 @@ import (
|
||||
"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 {
|
||||
getURL GetURLFunc
|
||||
bundles []string
|
||||
mutex sync.RWMutex
|
||||
resolveAppURL ResolveAppURLFunc
|
||||
bundles []string
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// Get implements app.Repository
|
||||
@ -33,7 +33,7 @@ func (r *AppRepository) Get(ctx context.Context, id app.ID) (*app.Manifest, erro
|
||||
}
|
||||
|
||||
// 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()
|
||||
defer r.mutex.RUnlock()
|
||||
|
||||
@ -42,7 +42,7 @@ func (r *AppRepository) GetURL(ctx context.Context, id app.ID) (string, error) {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
url, err := r.getURL(ctx, manifest)
|
||||
url, err := r.resolveAppURL(ctx, manifest, from)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
@ -80,11 +80,11 @@ func (r *AppRepository) List(ctx context.Context) ([]*app.Manifest, error) {
|
||||
return manifests, nil
|
||||
}
|
||||
|
||||
func (r *AppRepository) Update(getURL GetURLFunc, bundles []string) {
|
||||
func (r *AppRepository) Update(resolveAppURL ResolveAppURLFunc, bundles []string) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
r.getURL = getURL
|
||||
r.resolveAppURL = resolveAppURL
|
||||
r.bundles = bundles
|
||||
}
|
||||
|
||||
@ -118,7 +118,7 @@ func (r *AppRepository) findManifest(ctx context.Context, id app.ID) (*app.Manif
|
||||
|
||||
func NewAppRepository() *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")
|
||||
},
|
||||
bundles: []string{},
|
||||
|
@ -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
|
||||
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))
|
||||
for appKey, app := range specs.Apps {
|
||||
path := c.getAppBundlePath(appKey, app.Format)
|
||||
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) {
|
||||
appEntry := specs.Apps[appKey]
|
||||
|
||||
var auth *spec.Auth
|
||||
if specs.Config != nil {
|
||||
auth = specs.Config.Auth
|
||||
}
|
||||
|
||||
appDef := struct {
|
||||
App spec.AppEntry
|
||||
Auth *spec.Auth
|
||||
App spec.AppEntry
|
||||
Config *spec.Config
|
||||
}{
|
||||
App: appEntry,
|
||||
Auth: auth,
|
||||
App: appEntry,
|
||||
Config: specs.Config,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if server == nil {
|
||||
newServerEntry := func() (*serverEntry, error) {
|
||||
options, err := c.getHandlerOptions(ctx, appKey, specs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create handler options")
|
||||
}
|
||||
|
||||
var auth *spec.Auth
|
||||
if specs.Config != nil {
|
||||
auth = specs.Config.Auth
|
||||
return nil, errors.Wrap(err, "could not create handler options")
|
||||
}
|
||||
|
||||
server = &serverEntry{
|
||||
Server: NewServer(bundle, auth, options...),
|
||||
Server: NewServer(bundle, specs.Config, options...),
|
||||
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
|
||||
|
||||
if server.Server.Running() && !defChanged {
|
||||
return nil
|
||||
}
|
||||
@ -194,6 +204,17 @@ func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey str
|
||||
ctx, "restarting app",
|
||||
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 {
|
||||
logger.Info(
|
||||
ctx, "starting app",
|
||||
|
@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -31,7 +32,7 @@ type Server struct {
|
||||
handlerOptions []edgeHTTP.HandlerOptionFunc
|
||||
server *http.Server
|
||||
serverMutex sync.RWMutex
|
||||
auth *appSpec.Auth
|
||||
config *appSpec.Config
|
||||
}
|
||||
|
||||
func (s *Server) Start(ctx context.Context, addr string) (err error) {
|
||||
@ -53,8 +54,19 @@ func (s *Server) Start(ctx context.Context, addr string) (err error) {
|
||||
return errors.Wrap(err, "could not load app bundle")
|
||||
}
|
||||
|
||||
if err := s.configureAuth(router, s.auth); err != nil {
|
||||
return errors.WithStack(err)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
router.Handle("/*", handler)
|
||||
@ -124,13 +136,9 @@ func (s *Server) Stop() error {
|
||||
}
|
||||
|
||||
func (s *Server) configureAuth(router chi.Router, auth *spec.Auth) error {
|
||||
if auth == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case auth.Local != nil:
|
||||
var rawKey any = s.auth.Local.Key
|
||||
var rawKey any = auth.Local.Key
|
||||
if strKey, ok := rawKey.(string); ok {
|
||||
rawKey = []byte(strKey)
|
||||
}
|
||||
@ -141,54 +149,75 @@ func (s *Server) configureAuth(router chi.Router, auth *spec.Auth) error {
|
||||
}
|
||||
|
||||
cookieDuration := defaultCookieDuration
|
||||
if s.auth.Local.CookieDuration != "" {
|
||||
cookieDuration, err = time.ParseDuration(s.auth.Local.CookieDuration)
|
||||
if auth.Local.CookieDuration != "" {
|
||||
cookieDuration, err = time.ParseDuration(auth.Local.CookieDuration)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.auth.Local.CookieDomain != "" {
|
||||
router.Use(invalidCookieDomainRedirect(s.auth.Local.CookieDomain))
|
||||
}
|
||||
|
||||
router.Handle("/auth/*", authHTTP.NewLocalHandler(
|
||||
jwa.HS256, key,
|
||||
authHTTP.WithRoutePrefix("/auth"),
|
||||
authHTTP.WithAccounts(s.auth.Local.Accounts...),
|
||||
authHTTP.WithCookieOptions(s.auth.Local.CookieDomain, cookieDuration),
|
||||
authHTTP.WithAccounts(auth.Local.Accounts...),
|
||||
authHTTP.WithCookieOptions(getCookieDomain, cookieDuration),
|
||||
))
|
||||
}
|
||||
|
||||
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{
|
||||
bundle: bundle,
|
||||
auth: auth,
|
||||
config: config,
|
||||
handlerOptions: handlerOptions,
|
||||
}
|
||||
}
|
||||
|
||||
func invalidCookieDomainRedirect(cookieDomain string) func(http.Handler) http.Handler {
|
||||
domain := strings.TrimPrefix(cookieDomain, ".")
|
||||
hostPattern := "*" + domain
|
||||
func getCookieDomain(r *http.Request) (string, error) {
|
||||
host, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
hostParts := strings.SplitN(r.Host, ":", 2)
|
||||
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)
|
||||
|
||||
if !wildcard.Match(hostParts[0], hostPattern) {
|
||||
return
|
||||
}
|
||||
|
||||
matched := wildcard.MatchAny(host, acceptedHostPatterns...)
|
||||
|
||||
if !matched {
|
||||
url := r.URL
|
||||
|
||||
newHost := domain
|
||||
if len(hostParts) > 1 {
|
||||
newHost += ":" + hostParts[1]
|
||||
url.Host = hostTarget
|
||||
if port != "" {
|
||||
url.Host += ":" + port
|
||||
}
|
||||
|
||||
url.Host = newHost
|
||||
|
||||
http.Redirect(w, r, url.String(), http.StatusTemporaryRedirect)
|
||||
|
||||
return
|
||||
|
@ -38,57 +38,94 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"local": {
|
||||
"appUrlResolving": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": ["object", "string"]
|
||||
},
|
||||
"accounts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"algo": {
|
||||
"type": "string"
|
||||
},
|
||||
"claims": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"username",
|
||||
"password",
|
||||
"algo"
|
||||
]
|
||||
"ifaceMappings": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cookieDomain": {
|
||||
"type": "string"
|
||||
},
|
||||
"cookieDuration": {
|
||||
"defaultUrlTemplate": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
]
|
||||
"required": ["defaultUrlTemplate"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"unexpectedHostRedirect": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"acceptedHostPatterns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"hostTarget": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["acceptedHostPatterns", "hostTarget"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"local": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": ["object", "string"]
|
||||
},
|
||||
"accounts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"algo": {
|
||||
"type": "string"
|
||||
},
|
||||
"claims": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"username",
|
||||
"password",
|
||||
"algo"
|
||||
]
|
||||
}
|
||||
},
|
||||
"cookieDomain": {
|
||||
"type": "string"
|
||||
},
|
||||
"cookieDuration": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"appUrlTemplate": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -32,8 +32,19 @@ type LocalAuth struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Auth *Auth `json:"auth"`
|
||||
AppURLTemplate string `json:"appUrlTemplate"`
|
||||
Auth *Auth `json:"auth"`
|
||||
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 {
|
||||
|
@ -9,32 +9,44 @@
|
||||
"format": "zip"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"local": {
|
||||
"key": {
|
||||
"d": "YOre0WZefGfUGFvDg42oL5Oad5Zsb1N_hqPyLVM5ajpTZzcHpB3wT6In9tFO_VshB6lxVtPA9ckPkpMTFY7ygt1Yomc1HkoOKRtmIaqdr4VgNQifU-4yiLiJkSbdYSeMV-KkkN8mGR1keJpJeS34W1X0W6CkU2nw7F5VueBCJfWJA0funRfuWdI68MTUgT9kRZFp-SfvptvRL6jVYHV_5hqxzHCvgEdBSF6QKwx4M6P6QBMt7ft6uMLmFx9abKFw2V51hX3PkxiSepVB3w5CYg4HtS3AHX6bILL4m0R2pdTIkap7i3tkH_xAOuKWt8D6JhadI8X1rEAwXmCS5KrRgQ",
|
||||
"dp": "U0HfvBC6hk-SCpuotGIv3vbHCVt1aF3SHK0y32EYCOe8e_9G6YCEILfcvEJ5fiOCc2kvx6TasHQu4qj1uWRKenZlK1sJ6KDybGCkZL1D3jYnbeLZYBuWBL__YbZiST3ewbxzj_EDMWiZ8sUltahza_1weSgg8auSzTHS2LJBHIE",
|
||||
"dq": "hVom4ScDxgqhCsQNVpZlN7M3v0tgWjl_gTOHjOyzKCHQJeC0QmJJaMKkQZPWJ8jjLqy7VwVpqC2nZU7QDuX1Cq5eJDQcXi9XtaAfIBico9WcYDre6mDyhL588YHpekyRke8HnZ810iesr0G3gU1h0QvZVVuW-pXTJOXhZTt6nFc",
|
||||
"e": "AQAB",
|
||||
"kty": "RSA",
|
||||
"n": "vPnpkE3-HfNgJSru_K40LstkjiG2Bq_Tt-m0d_yUBBSbirFxF3qH4EXi7WrtZdeDahg2iV2BvpbVVj9GlmGo9OLol6jc7AP2yvZrkbABiiJhCbuPdkYbNpx6B7Itl8RT_bUSYAMZhmux5lpsn4weQ01fzjICi1rA-bIJpOfotdOjP4_lol-LxGZOGJQv9kndP8bgmssJb3Y_2s4gPtkmXySLrhpr5So-_6dVksyuBD9aLcnsMLDbywusjEMCdhqzQbvOjryomnmEXwyz_Ewb5HFK2PfgFtoHkdjqDz-mrEs3tw5g4TdYhCftzJxgbyNAEq4aEiOQrAncYyrXlotP_w",
|
||||
"p": "8TNMF0WUe7CEeNVUTsuEcBAAXRguNtpvVifIjlwzFRGOYVGIpKuHsqQPKlZL07I9gPr9LifQnyQus3oEmTOrVs6LB9sfbukbg43ZRKoGVM40JYF5Xjs7R3mEZhgU0WaYOVe3iLtBGMfXNWFwlbfQP-zEb-dPCBX1jWT3LdgNBcE",
|
||||
"q": "yJJLNc9w6O4y2icME8k99FugV9E7ObwUxF3v5JN3y1cmAT0h2njyE3iAGqaDZwcY1_jGCisjwoqX6i5E8xqhxX3Gcy3J7SmUAf8fhY8wU3zv9DK7skg2IdvanDb8Y1OM6GchbYZAOVPEg2IvVio8zI-Ih3DDwDk8Df0ufzoHRb8",
|
||||
"qi": "zOE-4R3cjPesm3MX-4PdwmsaF9QZLUVRUvvHJ08pKs6kAXP18hzjctAoOjhQDxlTYqNYNePfKzKwost3OJoPgRIc9w9qwUCK1gNOS4Z_xozCIaXgMddNFhkoAfZ4JaKjNCiinzjGfqG99Lf-yzmmREuuhRv7SdS3ST4VQjiJQew"
|
||||
},
|
||||
"accounts": [
|
||||
{
|
||||
"username": "foo",
|
||||
"algo": "plain",
|
||||
"password": "bar",
|
||||
"claims": {
|
||||
"arcad_role": "user",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"preferred_username": "Foo",
|
||||
"sub": "foo"
|
||||
"config": {
|
||||
"auth": {
|
||||
"local": {
|
||||
"key": {
|
||||
"d": "YOre0WZefGfUGFvDg42oL5Oad5Zsb1N_hqPyLVM5ajpTZzcHpB3wT6In9tFO_VshB6lxVtPA9ckPkpMTFY7ygt1Yomc1HkoOKRtmIaqdr4VgNQifU-4yiLiJkSbdYSeMV-KkkN8mGR1keJpJeS34W1X0W6CkU2nw7F5VueBCJfWJA0funRfuWdI68MTUgT9kRZFp-SfvptvRL6jVYHV_5hqxzHCvgEdBSF6QKwx4M6P6QBMt7ft6uMLmFx9abKFw2V51hX3PkxiSepVB3w5CYg4HtS3AHX6bILL4m0R2pdTIkap7i3tkH_xAOuKWt8D6JhadI8X1rEAwXmCS5KrRgQ",
|
||||
"dp": "U0HfvBC6hk-SCpuotGIv3vbHCVt1aF3SHK0y32EYCOe8e_9G6YCEILfcvEJ5fiOCc2kvx6TasHQu4qj1uWRKenZlK1sJ6KDybGCkZL1D3jYnbeLZYBuWBL__YbZiST3ewbxzj_EDMWiZ8sUltahza_1weSgg8auSzTHS2LJBHIE",
|
||||
"dq": "hVom4ScDxgqhCsQNVpZlN7M3v0tgWjl_gTOHjOyzKCHQJeC0QmJJaMKkQZPWJ8jjLqy7VwVpqC2nZU7QDuX1Cq5eJDQcXi9XtaAfIBico9WcYDre6mDyhL588YHpekyRke8HnZ810iesr0G3gU1h0QvZVVuW-pXTJOXhZTt6nFc",
|
||||
"e": "AQAB",
|
||||
"kty": "RSA",
|
||||
"n": "vPnpkE3-HfNgJSru_K40LstkjiG2Bq_Tt-m0d_yUBBSbirFxF3qH4EXi7WrtZdeDahg2iV2BvpbVVj9GlmGo9OLol6jc7AP2yvZrkbABiiJhCbuPdkYbNpx6B7Itl8RT_bUSYAMZhmux5lpsn4weQ01fzjICi1rA-bIJpOfotdOjP4_lol-LxGZOGJQv9kndP8bgmssJb3Y_2s4gPtkmXySLrhpr5So-_6dVksyuBD9aLcnsMLDbywusjEMCdhqzQbvOjryomnmEXwyz_Ewb5HFK2PfgFtoHkdjqDz-mrEs3tw5g4TdYhCftzJxgbyNAEq4aEiOQrAncYyrXlotP_w",
|
||||
"p": "8TNMF0WUe7CEeNVUTsuEcBAAXRguNtpvVifIjlwzFRGOYVGIpKuHsqQPKlZL07I9gPr9LifQnyQus3oEmTOrVs6LB9sfbukbg43ZRKoGVM40JYF5Xjs7R3mEZhgU0WaYOVe3iLtBGMfXNWFwlbfQP-zEb-dPCBX1jWT3LdgNBcE",
|
||||
"q": "yJJLNc9w6O4y2icME8k99FugV9E7ObwUxF3v5JN3y1cmAT0h2njyE3iAGqaDZwcY1_jGCisjwoqX6i5E8xqhxX3Gcy3J7SmUAf8fhY8wU3zv9DK7skg2IdvanDb8Y1OM6GchbYZAOVPEg2IvVio8zI-Ih3DDwDk8Df0ufzoHRb8",
|
||||
"qi": "zOE-4R3cjPesm3MX-4PdwmsaF9QZLUVRUvvHJ08pKs6kAXP18hzjctAoOjhQDxlTYqNYNePfKzKwost3OJoPgRIc9w9qwUCK1gNOS4Z_xozCIaXgMddNFhkoAfZ4JaKjNCiinzjGfqG99Lf-yzmmREuuhRv7SdS3ST4VQjiJQew"
|
||||
},
|
||||
"accounts": [
|
||||
{
|
||||
"username": "foo",
|
||||
"algo": "plain",
|
||||
"password": "bar",
|
||||
"claims": {
|
||||
"arcad_role": "user",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"preferred_username": "Foo",
|
||||
"sub": "foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1,36 +1,46 @@
|
||||
{
|
||||
"apps": {
|
||||
"portal": {
|
||||
"url": "https://emissary.cadol.es/files/apps/arcad.portal_v2023.3.28-3feda80.zip",
|
||||
"sha256sum": "921402c44a5fa554d5b630d1284957b05416aa6872b402314cf52e964e06fac5",
|
||||
"address": "127.0.0.1:8082",
|
||||
"edge.portal": {
|
||||
"url": "https://emissary.cadol.es/files/apps/edge.portal_v2023.4.5-45546c4.zip",
|
||||
"sha256sum": "c83e7e4b3785f5f4d3fcae7cad334819626015b11b446520aa79f42176a2744d",
|
||||
"address": ":8082",
|
||||
"format": "zip"
|
||||
},
|
||||
"hextris": {
|
||||
"app.arcad.edge.hextris": {
|
||||
"url": "https://emissary.cadol.es/files/apps/app.arcad.edge.hextris_v2023.3.22-33ece28.zip",
|
||||
"sha256sum": "5f9f3c8d6f22796beb051d747d7ff12efa17af9d1552c0ab08baef13703a2aba",
|
||||
"address": "127.0.0.1:8083",
|
||||
"address": ":8083",
|
||||
"format": "zip"
|
||||
},
|
||||
"test": {
|
||||
"url": "https://emissary.cadol.es/files/apps/edge.sdk.client.test_v2023.3.24-ed535b6.zip",
|
||||
"sha256sum": "e97b7b79159bb5d6a13b05644c091272b02a1a3cbb1b613dd5eda37e1eb84623",
|
||||
"address": "127.0.0.1:8084",
|
||||
"edge.sdk.client.test": {
|
||||
"url": "https://emissary.cadol.es/files/apps/edge.sdk.client.test_v2023.4.2-f08f645.zip",
|
||||
"sha256sum": "8b48388c817802ebeb38907b3a42f1189dc0759f94c5f33de4546c1a7ebfc784",
|
||||
"address": ":8084",
|
||||
"format": "zip"
|
||||
},
|
||||
"diffusion": {
|
||||
"url": "https://emissary.cadol.es/files/apps/arcad.diffusion_v2023.3.29-5b3fab4.zip",
|
||||
"sha256sum": "1282e75719beedbc7c7e67879389d0f3e11c86d3d2c37cf13da624a66faaeb58",
|
||||
"address": "127.0.0.1:8085",
|
||||
"arcad.diffusion": {
|
||||
"url": "https://emissary.cadol.es/files/apps/arcad.diffusion_v2023.4.5-ffcd1c7.zip",
|
||||
"sha256sum": "a51a961212470ce1de4527aaaec9e8e0286a978ec675ff9df29b2029daf05a55",
|
||||
"address": ":8085",
|
||||
"format": "zip"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"local": {
|
||||
"key": "absolutlynotsecret",
|
||||
"cookieDomain": ".arcad.local",
|
||||
"cookieDuration": "1h",
|
||||
"accounts": [
|
||||
{
|
||||
|
@ -3,36 +3,27 @@
|
||||
"arcad": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "arcad",
|
||||
"ifaces": ["wlp4s0"]
|
||||
"host": "arcad"
|
||||
},
|
||||
"portal": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "portal",
|
||||
"domain": "arcad.local",
|
||||
"ifaces": ["wlp4s0"]
|
||||
"host": "arcad-portal"
|
||||
},
|
||||
"hextris": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "hextris",
|
||||
"domain": "arcad.local",
|
||||
"ifaces": ["wlp4s0"]
|
||||
"host": "arcad-hextris"
|
||||
},
|
||||
"test": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "test",
|
||||
"domain": "arcad.local",
|
||||
"ifaces": ["wlp4s0"]
|
||||
"host": "arcad-test"
|
||||
},
|
||||
"diffusion": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "diffusion",
|
||||
"domain": "arcad.local",
|
||||
"ifaces": ["wlp4s0"]
|
||||
"host": "arcad-diffusion"
|
||||
}
|
||||
}
|
||||
}
|
@ -4,19 +4,19 @@
|
||||
"address": ":8080",
|
||||
"mappings": [
|
||||
{
|
||||
"hostPattern": "portal.arcad.local:*",
|
||||
"hostPattern": "portal.localhost.arcad.lan:*",
|
||||
"target": "http://localhost:8082"
|
||||
},
|
||||
{
|
||||
"hostPattern": "hextris.arcad.local:*",
|
||||
"hostPattern": "hextris.localhost.arcad.lan:*",
|
||||
"target": "http://localhost:8083"
|
||||
},
|
||||
{
|
||||
"hostPattern": "test.arcad.local:*",
|
||||
"hostPattern": "test.localhost.arcad.lan:*",
|
||||
"target": "http://localhost:8084"
|
||||
},
|
||||
{
|
||||
"hostPattern": "diffusion.arcad.local:*",
|
||||
"hostPattern": "diffusion.localhost.arcad.lan:*",
|
||||
"target": "http://localhost:8085"
|
||||
},
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user