Compare commits

...

7 Commits

Author SHA1 Message Date
c3535a4a9b feat(http): allow passing middlewares via options
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-09-20 09:23:53 -06:00
7e58551f6a docs(context): remove reference to obsolete attribute
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-09-20 09:02:27 -06:00
41d5db6321 docs(auth): add informations about anonymous users
ref arcad/edge-menu#86
2023-09-20 09:01:36 -06:00
8eb441daee feat(auth): automatically generate anonymous user session
All checks were successful
arcad/edge/pipeline/head This commit looks good
ref arcad/edge-menu#86
2023-09-20 08:55:49 -06:00
17808d14c9 fix: prevent bus congestion by flushing out messages
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-26 15:53:23 +02:00
ba9ae6e391 fix(app): use event loop runtime for every operations
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-24 12:16:30 +02:00
abc60b9ae3 fix(module,app): use whole remote address if splitting fail
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-21 20:01:43 +02:00
25 changed files with 503 additions and 173 deletions

View File

@ -22,6 +22,7 @@ import (
appModuleMemory "forge.cadoles.com/arcad/edge/pkg/module/app/memory" appModuleMemory "forge.cadoles.com/arcad/edge/pkg/module/app/memory"
authModule "forge.cadoles.com/arcad/edge/pkg/module/auth" authModule "forge.cadoles.com/arcad/edge/pkg/module/auth"
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http" authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
authModuleMiddleware "forge.cadoles.com/arcad/edge/pkg/module/auth/middleware"
"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"
"forge.cadoles.com/arcad/edge/pkg/module/fetch" "forge.cadoles.com/arcad/edge/pkg/module/fetch"
@ -215,6 +216,11 @@ func runApp(ctx context.Context, path string, address string, storageFile string
authModule.WithJWT(dummyKeySet), authModule.WithJWT(dummyKeySet),
), ),
), ),
appHTTP.WithHTTPMiddlewares(
authModuleMiddleware.AnonymousUser(
jwa.HS256, key,
),
),
) )
if err := handler.Load(bundle); err != nil { if err := handler.Load(bundle); err != nil {
return errors.Wrap(err, "could not load app bundle") return errors.Wrap(err, "could not load app bundle")
@ -250,10 +256,10 @@ type ModuleDepFunc func(*moduleDeps) error
func getServerModules(deps *moduleDeps) []app.ServerModuleFactory { func getServerModules(deps *moduleDeps) []app.ServerModuleFactory {
return []app.ServerModuleFactory{ return []app.ServerModuleFactory{
module.LifecycleModuleFactory(),
module.ContextModuleFactory(), module.ContextModuleFactory(),
module.ConsoleModuleFactory(), module.ConsoleModuleFactory(),
cast.CastModuleFactory(), cast.CastModuleFactory(),
module.LifecycleModuleFactory(),
netModule.ModuleFactory(deps.Bus), netModule.ModuleFactory(deps.Bus),
module.RPCModuleFactory(deps.Bus), module.RPCModuleFactory(deps.Bus),
module.StoreModuleFactory(deps.DocumentStore), module.StoreModuleFactory(deps.DocumentStore),

View File

@ -2,6 +2,14 @@
Ce module permet de récupérer des informations concernant l'utilisateur connecté et ses attributs. Ce module permet de récupérer des informations concernant l'utilisateur connecté et ses attributs.
### Utilisateurs anonymes
Edge génère automatiquement une session pour les utilisateurs anonymes. Ainsi, qu'un utilisateur soit identifié ou non les `claims` suivants seront toujours valués:
- `auth.CLAIM_SUBJECT`
- `auth.CLAIM_PREFERRED_USERNAME`
- `auth.CLAIM_ISSUER` (prendra la valeur `anon` dans le cas d'un utilisateur anonyme)
## Méthodes ## Méthodes
### `auth.getClaim(ctx: Context, name: string): string` ### `auth.getClaim(ctx: Context, name: string): string`

View File

@ -43,12 +43,6 @@ function onClientMessage(ctx, message) {
} }
``` ```
## Propriétés
### `context.SESSION_ID`
Clé permettant de récupérer la clé de session associé au client émetteur du message courant.
#### Usage #### Usage
```js ```js

View File

@ -39,3 +39,14 @@ func NewPromiseProxy(promise *goja.Promise, resolve func(result interface{}), re
return proxy return proxy
} }
func NewPromiseProxyFrom(rt *goja.Runtime) *PromiseProxy {
promise, resolve, reject := rt.NewPromise()
return NewPromiseProxy(promise, resolve, reject)
}
func IsPromise(v goja.Value) (*goja.Promise, bool) {
promise, ok := v.Export().(*goja.Promise)
return promise, ok
}

View File

@ -17,15 +17,22 @@ var (
) )
type Server struct { type Server struct {
runtime *goja.Runtime loop *eventloop.EventLoop
loop *eventloop.EventLoop factories []ServerModuleFactory
modules []ServerModule modules []ServerModule
} }
func (s *Server) Load(name string, src string) error { func (s *Server) Load(name string, src string) error {
_, err := s.runtime.RunScript(name, src) var err error
s.loop.RunOnLoop(func(rt *goja.Runtime) {
_, err = rt.RunScript(name, src)
if err != nil {
err = errors.Wrap(err, "could not run js script")
}
})
if err != nil { if err != nil {
return errors.Wrap(err, "could not run js script") return errors.WithStack(err)
} }
return nil return nil
@ -34,15 +41,15 @@ func (s *Server) Load(name string, src string) error {
func (s *Server) ExecFuncByName(ctx context.Context, funcName string, args ...interface{}) (goja.Value, error) { func (s *Server) ExecFuncByName(ctx context.Context, funcName string, args ...interface{}) (goja.Value, error) {
ctx = logger.With(ctx, logger.F("function", funcName), logger.F("args", args)) ctx = logger.With(ctx, logger.F("function", funcName), logger.F("args", args))
callable, ok := goja.AssertFunction(s.runtime.Get(funcName)) ret, err := s.Exec(ctx, funcName, args...)
if !ok { if err != nil {
return nil, errors.WithStack(ErrFuncDoesNotExist) return nil, errors.WithStack(err)
} }
return s.Exec(ctx, callable, args...) return ret, nil
} }
func (s *Server) Exec(ctx context.Context, callable goja.Callable, args ...interface{}) (goja.Value, error) { func (s *Server) Exec(ctx context.Context, callableOrFuncname any, args ...interface{}) (goja.Value, error) {
var ( var (
wg sync.WaitGroup wg sync.WaitGroup
value goja.Value value goja.Value
@ -51,7 +58,28 @@ func (s *Server) Exec(ctx context.Context, callable goja.Callable, args ...inter
wg.Add(1) wg.Add(1)
s.loop.RunOnLoop(func(vm *goja.Runtime) { s.loop.RunOnLoop(func(rt *goja.Runtime) {
var callable goja.Callable
switch typ := callableOrFuncname.(type) {
case goja.Callable:
callable = typ
case string:
call, ok := goja.AssertFunction(rt.Get(typ))
if !ok {
err = errors.WithStack(ErrFuncDoesNotExist)
return
}
callable = call
default:
err = errors.Errorf("callableOrFuncname: expected callable or function name, got '%T'", callableOrFuncname)
return
}
logger.Debug(ctx, "executing callable") logger.Debug(ctx, "executing callable")
defer wg.Done() defer wg.Done()
@ -73,7 +101,7 @@ func (s *Server) Exec(ctx context.Context, callable goja.Callable, args ...inter
jsArgs := make([]goja.Value, 0, len(args)) jsArgs := make([]goja.Value, 0, len(args))
for _, a := range args { for _, a := range args {
jsArgs = append(jsArgs, vm.ToValue(a)) jsArgs = append(jsArgs, rt.ToValue(a))
} }
value, err = callable(nil, jsArgs...) value, err = callable(nil, jsArgs...)
@ -84,12 +112,11 @@ func (s *Server) Exec(ctx context.Context, callable goja.Callable, args ...inter
wg.Wait() wg.Wait()
return value, err if err != nil {
} return nil, errors.WithStack(err)
}
func (s *Server) IsPromise(v goja.Value) (*goja.Promise, bool) { return value, nil
promise, ok := v.Export().(*goja.Promise)
return promise, ok
} }
func (s *Server) WaitForPromise(promise *goja.Promise) goja.Value { func (s *Server) WaitForPromise(promise *goja.Promise) goja.Value {
@ -135,28 +162,21 @@ func (s *Server) WaitForPromise(promise *goja.Promise) goja.Value {
return value return value
} }
func (s *Server) NewPromise() *PromiseProxy {
promise, resolve, reject := s.runtime.NewPromise()
return NewPromiseProxy(promise, resolve, reject)
}
func (s *Server) ToValue(v interface{}) goja.Value {
return s.runtime.ToValue(v)
}
func (s *Server) Start() error { func (s *Server) Start() error {
s.loop.Start() s.loop.Start()
for _, mod := range s.modules { var err error
initMod, ok := mod.(InitializableModule)
if !ok {
continue
}
if err := initMod.OnInit(); err != nil { s.loop.RunOnLoop(func(rt *goja.Runtime) {
return errors.WithStack(err) rt.SetFieldNameMapper(goja.TagFieldNameMapper("goja", true))
rt.SetRandSource(createRandomSource())
if err = s.initModules(rt); err != nil {
err = errors.WithStack(err)
} }
})
if err != nil {
return errors.WithStack(err)
} }
return nil return nil
@ -166,36 +186,46 @@ func (s *Server) Stop() {
s.loop.Stop() s.loop.Stop()
} }
func (s *Server) initModules(factories ...ServerModuleFactory) { func (s *Server) initModules(rt *goja.Runtime) error {
runtime := goja.New() modules := make([]ServerModule, 0, len(s.factories))
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("goja", true)) for _, moduleFactory := range s.factories {
runtime.SetRandSource(createRandomSource())
modules := make([]ServerModule, 0, len(factories))
for _, moduleFactory := range factories {
mod := moduleFactory(s) mod := moduleFactory(s)
export := runtime.NewObject()
export := rt.NewObject()
mod.Export(export) mod.Export(export)
runtime.Set(mod.Name(), export)
rt.Set(mod.Name(), export)
modules = append(modules, mod) modules = append(modules, mod)
} }
s.runtime = runtime for _, mod := range modules {
initMod, ok := mod.(InitializableModule)
if !ok {
continue
}
logger.Debug(context.Background(), "initializing module", logger.F("module", initMod.Name()))
if err := initMod.OnInit(rt); err != nil {
return errors.WithStack(err)
}
}
s.modules = modules s.modules = modules
return nil
} }
func NewServer(factories ...ServerModuleFactory) *Server { func NewServer(factories ...ServerModuleFactory) *Server {
server := &Server{ server := &Server{
factories: factories,
loop: eventloop.NewEventLoop( loop: eventloop.NewEventLoop(
eventloop.EnableConsole(false), eventloop.EnableConsole(false),
), ),
} }
server.initModules(factories...)
return server return server
} }

View File

@ -13,5 +13,5 @@ type ServerModule interface {
type InitializableModule interface { type InitializableModule interface {
ServerModule ServerModule
OnInit() error OnInit(rt *goja.Runtime) error
} }

View File

@ -22,13 +22,13 @@ func (b *Bus) Subscribe(ctx context.Context, ns bus.MessageNamespace) (<-chan bu
) )
dispatchers := b.getDispatchers(ns) dispatchers := b.getDispatchers(ns)
d := newEventDispatcher(b.opt.BufferSize) disp := newEventDispatcher(b.opt.BufferSize)
go d.Run() go disp.Run(ctx)
dispatchers.Add(d) dispatchers.Add(disp)
return d.Out(), nil return disp.Out(), nil
} }
func (b *Bus) Unsubscribe(ctx context.Context, ns bus.MessageNamespace, ch <-chan bus.Message) { func (b *Bus) Unsubscribe(ctx context.Context, ns bus.MessageNamespace, ch <-chan bus.Message) {
@ -52,6 +52,12 @@ func (b *Bus) Publish(ctx context.Context, msg bus.Message) error {
) )
for _, d := range dispatchersList { for _, d := range dispatchersList {
if d.Closed() {
dispatchers.Remove(d)
continue
}
if err := d.In(msg); err != nil { if err := d.In(msg); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }

View File

@ -1,9 +1,13 @@
package memory package memory
import ( import (
"context"
"sync" "sync"
"time"
"forge.cadoles.com/arcad/edge/pkg/bus" "forge.cadoles.com/arcad/edge/pkg/bus"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
) )
type eventDispatcherSet struct { type eventDispatcherSet struct {
@ -18,13 +22,21 @@ func (s *eventDispatcherSet) Add(d *eventDispatcher) {
s.items[d] = struct{}{} s.items[d] = struct{}{}
} }
func (s *eventDispatcherSet) Remove(d *eventDispatcher) {
s.mutex.Lock()
defer s.mutex.Unlock()
d.close()
delete(s.items, d)
}
func (s *eventDispatcherSet) RemoveByOutChannel(out <-chan bus.Message) { func (s *eventDispatcherSet) RemoveByOutChannel(out <-chan bus.Message) {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
for d := range s.items { for d := range s.items {
if d.IsOut(out) { if d.IsOut(out) {
d.Close() d.close()
delete(s.items, d) delete(s.items, d)
} }
} }
@ -56,10 +68,21 @@ type eventDispatcher struct {
closed bool closed bool
} }
func (d *eventDispatcher) Closed() bool {
d.mutex.RLock()
defer d.mutex.RUnlock()
return d.closed
}
func (d *eventDispatcher) Close() { func (d *eventDispatcher) Close() {
d.mutex.Lock() d.mutex.Lock()
defer d.mutex.Unlock() defer d.mutex.Unlock()
d.close()
}
func (d *eventDispatcher) close() {
d.closed = true d.closed = true
close(d.in) close(d.in)
} }
@ -85,16 +108,52 @@ func (d *eventDispatcher) IsOut(out <-chan bus.Message) bool {
return d.out == out return d.out == out
} }
func (d *eventDispatcher) Run() { func (d *eventDispatcher) Run(ctx context.Context) {
defer func() {
for {
logger.Debug(ctx, "closing dispatcher, flushing out incoming messages")
close(d.out)
// Flush all incoming messages
for {
_, ok := <-d.in
if !ok {
return
}
}
}
}()
for { for {
msg, ok := <-d.in msg, ok := <-d.in
if !ok { if !ok {
close(d.out)
return return
} }
d.out <- msg timeout := time.After(time.Second)
select {
case d.out <- msg:
case <-timeout:
logger.Error(
ctx,
"out message channel timeout",
logger.F("message", msg),
)
return
case <-ctx.Done():
logger.Error(
ctx,
"message subscription context canceled",
logger.F("message", msg),
logger.E(errors.WithStack(ctx.Err())),
)
return
}
} }
} }

View File

@ -97,6 +97,10 @@ func NewHandler(funcs ...HandlerOptionFunc) *Handler {
bus: opts.Bus, bus: opts.Bus,
} }
for _, middleware := range opts.HTTPMiddlewares {
router.Use(middleware)
}
router.Route("/edge", func(r chi.Router) { router.Route("/edge", func(r chi.Router) {
r.Route("/sdk", func(r chi.Router) { r.Route("/sdk", func(r chi.Router) {
r.Get("/client.js", handler.handleSDKClient) r.Get("/client.js", handler.handleSDKClient)

View File

@ -18,6 +18,7 @@ type HandlerOptions struct {
UploadMaxFileSize int64 UploadMaxFileSize int64
HTTPClient *http.Client HTTPClient *http.Client
HTTPMounts []func(r chi.Router) HTTPMounts []func(r chi.Router)
HTTPMiddlewares []func(next http.Handler) http.Handler
} }
func defaultHandlerOptions() *HandlerOptions { func defaultHandlerOptions() *HandlerOptions {
@ -34,7 +35,8 @@ func defaultHandlerOptions() *HandlerOptions {
HTTPClient: &http.Client{ HTTPClient: &http.Client{
Timeout: time.Second * 30, Timeout: time.Second * 30,
}, },
HTTPMounts: make([]func(r chi.Router), 0), HTTPMounts: make([]func(r chi.Router), 0),
HTTPMiddlewares: make([]func(http.Handler) http.Handler, 0),
} }
} }
@ -75,3 +77,9 @@ func WithHTTPMounts(mounts ...func(r chi.Router)) HandlerOptionFunc {
opts.HTTPMounts = mounts opts.HTTPMounts = mounts
} }
} }
func WithHTTPMiddlewares(middlewares ...func(http.Handler) http.Handler) HandlerOptionFunc {
return func(opts *HandlerOptions) {
opts.HTTPMiddlewares = middlewares
}
}

View File

@ -73,12 +73,7 @@ func (h *Handler) serveAppURL(w http.ResponseWriter, r *http.Request) {
from := req.From from := req.From
if from == "" { if from == "" {
host, _, err := net.SplitHostPort(r.RemoteAddr) from = retrieveRemoteAddr(r)
if err != nil {
logger.Warn(ctx, "could not split remote address", logger.E(errors.WithStack(err)))
} else {
from = host
}
} }
url, err := h.repo.GetURL(ctx, appID, from) url, err := h.repo.GetURL(ctx, appID, from)
@ -110,3 +105,12 @@ func Mount(repository Repository) MountFunc {
r.Post("/api/v1/apps/{appID}/url", handler.serveAppURL) r.Post("/api/v1/apps/{appID}/url", handler.serveAppURL)
} }
} }
func retrieveRemoteAddr(r *http.Request) string {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
host = r.RemoteAddr
}
return host
}

View File

@ -9,6 +9,7 @@ import (
"forge.cadoles.com/arcad/edge/pkg/module/auth" "forge.cadoles.com/arcad/edge/pkg/module/auth"
"forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd" "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd"
"forge.cadoles.com/arcad/edge/pkg/module/auth/jwt"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwk"
@ -112,7 +113,7 @@ func (h *LocalHandler) handleForm(w http.ResponseWriter, r *http.Request) {
account.Claims[auth.ClaimIssuer] = "local" account.Claims[auth.ClaimIssuer] = "local"
token, err := generateSignedToken(h.algo, h.key, account.Claims) token, err := jwt.GenerateSignedToken(h.algo, h.key, account.Claims)
if err != nil { if err != nil {
logger.Error(ctx, "could not generate signed token", logger.E(errors.WithStack(err))) logger.Error(ctx, "could not generate signed token", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)

View File

@ -30,7 +30,7 @@ func WithJWT(getKeySet GetKeySetFunc) OptionFunc {
} }
} }
func FindToken(r *http.Request, getKeySet GetKeySetFunc) (jwt.Token, error) { func FindRawToken(r *http.Request) (string, error) {
authorization := r.Header.Get("Authorization") authorization := r.Header.Get("Authorization")
// Retrieve token from Authorization header // Retrieve token from Authorization header
@ -44,7 +44,7 @@ func FindToken(r *http.Request, getKeySet GetKeySetFunc) (jwt.Token, error) {
if rawToken == "" { if rawToken == "" {
cookie, err := r.Cookie(CookieName) cookie, err := r.Cookie(CookieName)
if err != nil && !errors.Is(err, http.ErrNoCookie) { if err != nil && !errors.Is(err, http.ErrNoCookie) {
return nil, errors.WithStack(err) return "", errors.WithStack(err)
} }
if cookie != nil { if cookie != nil {
@ -53,7 +53,16 @@ func FindToken(r *http.Request, getKeySet GetKeySetFunc) (jwt.Token, error) {
} }
if rawToken == "" { if rawToken == "" {
return nil, errors.WithStack(ErrUnauthenticated) return "", errors.WithStack(ErrUnauthenticated)
}
return rawToken, nil
}
func FindToken(r *http.Request, getKeySet GetKeySetFunc) (jwt.Token, error) {
rawToken, err := FindRawToken(r)
if err != nil {
return nil, errors.WithStack(err)
} }
keySet, err := getKeySet() keySet, err := getKeySet()

View File

@ -1,4 +1,4 @@
package http package jwt
import ( import (
"time" "time"
@ -9,7 +9,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func generateSignedToken(algo jwa.KeyAlgorithm, key jwk.Key, claims map[string]any) ([]byte, error) { func GenerateSignedToken(algo jwa.KeyAlgorithm, key jwk.Key, claims map[string]any) ([]byte, error) {
token := jwt.New() token := jwt.New()
if err := token.Set(jwt.NotBeforeKey, time.Now()); err != nil { if err := token.Set(jwt.NotBeforeKey, time.Now()); err != nil {

View File

@ -0,0 +1,113 @@
package middleware
import (
"crypto/rand"
"fmt"
"math/big"
"net/http"
"time"
"forge.cadoles.com/arcad/edge/pkg/module/auth"
"forge.cadoles.com/arcad/edge/pkg/module/auth/jwt"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
const AnonIssuer = "anon"
func AnonymousUser(algo jwa.KeyAlgorithm, key jwk.Key, funcs ...AnonymousUserOptionFunc) func(next http.Handler) http.Handler {
opts := defaultAnonymousUserOptions()
for _, fn := range funcs {
fn(opts)
}
return func(next http.Handler) http.Handler {
handler := func(w http.ResponseWriter, r *http.Request) {
rawToken, err := auth.FindRawToken(r)
// If request already has a raw token, we do nothing
if rawToken != "" && err == nil {
next.ServeHTTP(w, r)
return
}
ctx := r.Context()
uuid, err := uuid.NewUUID()
if err != nil {
logger.Error(ctx, "could not generate uuid for anonymous user", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
subject := fmt.Sprintf("%s-%s", AnonIssuer, uuid.String())
preferredUsername, err := generateRandomPreferredUsername(8)
if err != nil {
logger.Error(ctx, "could not generate preferred username for anonymous user", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
claims := map[string]any{
auth.ClaimSubject: subject,
auth.ClaimIssuer: AnonIssuer,
auth.ClaimPreferredUsername: preferredUsername,
auth.ClaimEdgeRole: opts.Role,
auth.ClaimEdgeEntrypoint: opts.Entrypoint,
auth.ClaimEdgeTenant: opts.Tenant,
}
token, err := jwt.GenerateSignedToken(algo, key, claims)
if err != nil {
logger.Error(ctx, "could not generate signed token", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
cookieDomain, err := opts.GetCookieDomain(r)
if err != nil {
logger.Error(ctx, "could not retrieve cookie domain", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
cookie := http.Cookie{
Name: auth.CookieName,
Value: string(token),
Domain: cookieDomain,
HttpOnly: false,
Expires: time.Now().Add(opts.CookieDuration),
Path: "/",
}
http.SetCookie(w, &cookie)
next.ServeHTTP(w, r)
}
return http.HandlerFunc(handler)
}
}
func generateRandomPreferredUsername(size int) (string, error) {
var letters = []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
max := big.NewInt(int64(len(letters)))
b := make([]rune, size)
for i := range b {
idx, err := rand.Int(rand.Reader, max)
if err != nil {
return "", errors.WithStack(err)
}
b[i] = letters[idx.Int64()]
}
return fmt.Sprintf("Anon %s", string(b)), nil
}

View File

@ -0,0 +1,57 @@
package middleware
import (
"net/http"
"time"
)
type GetCookieDomainFunc func(r *http.Request) (string, error)
func defaultGetCookieDomain(r *http.Request) (string, error) {
return "", nil
}
type AnonymousUserOptions struct {
GetCookieDomain GetCookieDomainFunc
CookieDuration time.Duration
Tenant string
Entrypoint string
Role string
}
type AnonymousUserOptionFunc func(*AnonymousUserOptions)
func defaultAnonymousUserOptions() *AnonymousUserOptions {
return &AnonymousUserOptions{
GetCookieDomain: defaultGetCookieDomain,
CookieDuration: 24 * time.Hour,
Tenant: "",
Entrypoint: "",
Role: "",
}
}
func WithCookieOptions(getCookieDomain GetCookieDomainFunc, duration time.Duration) AnonymousUserOptionFunc {
return func(opts *AnonymousUserOptions) {
opts.GetCookieDomain = getCookieDomain
opts.CookieDuration = duration
}
}
func WithTenant(tenant string) AnonymousUserOptionFunc {
return func(opts *AnonymousUserOptions) {
opts.Tenant = tenant
}
}
func WithEntrypoint(entrypoint string) AnonymousUserOptionFunc {
return func(opts *AnonymousUserOptions) {
opts.Entrypoint = entrypoint
}
}
func WithRole(role string) AnonymousUserOptionFunc {
return func(opts *AnonymousUserOptions) {
opts.Role = role
}
}

View File

@ -53,6 +53,10 @@ func (m *Module) Export(export *goja.Object) {
if err := export.Set("CLAIM_PREFERRED_USERNAME", ClaimPreferredUsername); err != nil { if err := export.Set("CLAIM_PREFERRED_USERNAME", ClaimPreferredUsername); err != nil {
panic(errors.Wrap(err, "could not set 'CLAIM_PREFERRED_USERNAME' property")) panic(errors.Wrap(err, "could not set 'CLAIM_PREFERRED_USERNAME' property"))
} }
if err := export.Set("CLAIM_ISSUER", ClaimIssuer); err != nil {
panic(errors.Wrap(err, "could not set 'CLAIM_ISSUER' property"))
}
} }
func (m *Module) getClaim(call goja.FunctionCall, rt *goja.Runtime) goja.Value { func (m *Module) getClaim(call goja.FunctionCall, rt *goja.Runtime) goja.Value {

View File

@ -15,10 +15,7 @@ const (
defaultTimeout = 30 * time.Second defaultTimeout = 30 * time.Second
) )
type Module struct { type Module struct{}
ctx context.Context
server *app.Server
}
func (m *Module) Name() string { func (m *Module) Name() string {
return "cast" return "cast"
@ -54,7 +51,7 @@ func (m *Module) refreshDevices(call goja.FunctionCall, rt *goja.Runtime) goja.V
panic(rt.ToValue(errors.WithStack(err))) panic(rt.ToValue(errors.WithStack(err)))
} }
promise := m.server.NewPromise() promise := app.NewPromiseProxyFrom(rt)
go func() { go func() {
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
@ -102,7 +99,7 @@ func (m *Module) loadUrl(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
panic(rt.ToValue(errors.WithStack(err))) panic(rt.ToValue(errors.WithStack(err)))
} }
promise := m.server.NewPromise() promise := app.NewPromiseProxyFrom(rt)
go func() { go func() {
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
@ -121,7 +118,7 @@ func (m *Module) loadUrl(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
promise.Resolve(nil) promise.Resolve(nil)
}() }()
return m.server.ToValue(promise) return rt.ToValue(promise)
} }
func (m *Module) stopCast(call goja.FunctionCall, rt *goja.Runtime) goja.Value { func (m *Module) stopCast(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
@ -137,7 +134,7 @@ func (m *Module) stopCast(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
panic(rt.ToValue(errors.WithStack(err))) panic(rt.ToValue(errors.WithStack(err)))
} }
promise := m.server.NewPromise() promise := app.NewPromiseProxyFrom(rt)
go func() { go func() {
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
@ -156,7 +153,7 @@ func (m *Module) stopCast(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
promise.Resolve(nil) promise.Resolve(nil)
}() }()
return m.server.ToValue(promise) return rt.ToValue(promise)
} }
func (m *Module) getStatus(call goja.FunctionCall, rt *goja.Runtime) goja.Value { func (m *Module) getStatus(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
@ -172,7 +169,7 @@ func (m *Module) getStatus(call goja.FunctionCall, rt *goja.Runtime) goja.Value
panic(rt.ToValue(errors.WithStack(err))) panic(rt.ToValue(errors.WithStack(err)))
} }
promise := m.server.NewPromise() promise := app.NewPromiseProxyFrom(rt)
go func() { go func() {
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
@ -191,7 +188,7 @@ func (m *Module) getStatus(call goja.FunctionCall, rt *goja.Runtime) goja.Value
promise.Resolve(status) promise.Resolve(status)
}() }()
return m.server.ToValue(promise) return rt.ToValue(promise)
} }
func (m *Module) parseTimeout(rawTimeout string) (time.Duration, error) { func (m *Module) parseTimeout(rawTimeout string) (time.Duration, error) {
@ -214,8 +211,6 @@ func (m *Module) parseTimeout(rawTimeout string) (time.Duration, error) {
func CastModuleFactory() app.ServerModuleFactory { func CastModuleFactory() app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule { return func(server *app.Server) app.ServerModule {
return &Module{ return &Module{}
server: server,
}
} }
} }

View File

@ -85,7 +85,7 @@ func TestCastModuleRefreshDevices(t *testing.T) {
t.Error(errors.WithStack(err)) t.Error(errors.WithStack(err))
} }
promise, ok := server.IsPromise(result) promise, ok := app.IsPromise(result)
if !ok { if !ok {
t.Fatal("expected promise") t.Fatal("expected promise")
} }

View File

@ -9,9 +9,7 @@ import (
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
type LifecycleModule struct { type LifecycleModule struct{}
server *app.Server
}
func (m *LifecycleModule) Name() string { func (m *LifecycleModule) Name() string {
return "lifecycle" return "lifecycle"
@ -20,25 +18,37 @@ func (m *LifecycleModule) Name() string {
func (m *LifecycleModule) Export(export *goja.Object) { func (m *LifecycleModule) Export(export *goja.Object) {
} }
func (m *LifecycleModule) OnInit() error { func (m *LifecycleModule) OnInit(rt *goja.Runtime) (err error) {
if _, err := m.server.ExecFuncByName(context.Background(), "onInit"); err != nil { call, ok := goja.AssertFunction(rt.Get("onInit"))
if errors.Is(err, app.ErrFuncDoesNotExist) { if !ok {
logger.Warn(context.Background(), "could not find onInit() function", logger.E(errors.WithStack(err))) logger.Warn(context.Background(), "could not find onInit() function")
return nil return nil
}
return errors.WithStack(err)
} }
defer func() {
if recovered := recover(); recovered != nil {
revoveredErr, ok := recovered.(error)
if ok {
logger.Error(context.Background(), "recovered runtime error", logger.E(errors.WithStack(revoveredErr)))
err = errors.WithStack(app.ErUnknownError)
return
}
panic(recovered)
}
}()
call(nil)
return nil return nil
} }
func LifecycleModuleFactory() app.ServerModuleFactory { func LifecycleModuleFactory() app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule { return func(server *app.Server) app.ServerModule {
module := &LifecycleModule{ module := &LifecycleModule{}
server: server,
}
return module return module
} }

View File

@ -51,6 +51,12 @@ func (m *RPCModule) Export(export *goja.Object) {
} }
} }
func (m *RPCModule) OnInit(rt *goja.Runtime) error {
go m.handleMessages()
return nil
}
func (m *RPCModule) register(call goja.FunctionCall, rt *goja.Runtime) goja.Value { func (m *RPCModule) register(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
fnName := util.AssertString(call.Argument(0), rt) fnName := util.AssertString(call.Argument(0), rt)
@ -117,79 +123,83 @@ func (m *RPCModule) handleMessages() {
} }
for msg := range clientMessages { for msg := range clientMessages {
clientMessage, ok := msg.(*ClientMessage) go m.handleMessage(ctx, msg, sendRes)
if !ok { }
logger.Warn(ctx, "unexpected bus message", logger.F("message", msg)) }
continue func (m *RPCModule) handleMessage(ctx context.Context, msg bus.Message, sendRes func(ctx context.Context, req *RPCRequest, result goja.Value)) {
} clientMessage, ok := msg.(*ClientMessage)
if !ok {
logger.Warn(ctx, "unexpected bus message", logger.F("message", msg))
ok, req := m.isRPCRequest(clientMessage) return
if !ok { }
continue
}
logger.Debug(ctx, "received rpc request", logger.F("request", req)) ok, req := m.isRPCRequest(clientMessage)
if !ok {
return
}
rawCallable, exists := m.callbacks.Load(req.Method) logger.Debug(ctx, "received rpc request", logger.F("request", req))
if !exists {
logger.Debug(ctx, "method not found", logger.F("req", req))
if err := m.sendMethodNotFoundResponse(clientMessage.Context, req); err != nil { rawCallable, exists := m.callbacks.Load(req.Method)
logger.Error( if !exists {
ctx, "could not send method not found response", logger.Debug(ctx, "method not found", logger.F("req", req))
logger.E(errors.WithStack(err)),
logger.F("request", req),
)
}
continue if err := m.sendMethodNotFoundResponse(clientMessage.Context, req); err != nil {
}
callable, ok := rawCallable.(goja.Callable)
if !ok {
logger.Debug(ctx, "invalid method", logger.F("req", req))
if err := m.sendMethodNotFoundResponse(clientMessage.Context, req); err != nil {
logger.Error(
ctx, "could not send method not found response",
logger.E(errors.WithStack(err)),
logger.F("request", req),
)
}
continue
}
result, err := m.server.Exec(clientMessage.Context, callable, clientMessage.Context, req.Params)
if err != nil {
logger.Error( logger.Error(
ctx, "rpc call error", ctx, "could not send method not found response",
logger.E(errors.WithStack(err)), logger.E(errors.WithStack(err)),
logger.F("request", req), logger.F("request", req),
) )
if err := m.sendErrorResponse(clientMessage.Context, req, err); err != nil {
logger.Error(
ctx, "could not send error response",
logger.E(errors.WithStack(err)),
logger.F("originalError", err),
logger.F("request", req),
)
}
continue
} }
promise, ok := m.server.IsPromise(result) return
if ok { }
go func(ctx context.Context, req *RPCRequest, promise *goja.Promise) {
result := m.server.WaitForPromise(promise) callable, ok := rawCallable.(goja.Callable)
sendRes(ctx, req, result) if !ok {
}(clientMessage.Context, req, promise) logger.Debug(ctx, "invalid method", logger.F("req", req))
} else {
sendRes(clientMessage.Context, req, result) if err := m.sendMethodNotFoundResponse(clientMessage.Context, req); err != nil {
logger.Error(
ctx, "could not send method not found response",
logger.E(errors.WithStack(err)),
logger.F("request", req),
)
} }
return
}
result, err := m.server.Exec(clientMessage.Context, callable, clientMessage.Context, req.Params)
if err != nil {
logger.Error(
ctx, "rpc call error",
logger.E(errors.WithStack(err)),
logger.F("request", req),
)
if err := m.sendErrorResponse(clientMessage.Context, req, err); err != nil {
logger.Error(
ctx, "could not send error response",
logger.E(errors.WithStack(err)),
logger.F("originalError", err),
logger.F("request", req),
)
}
return
}
promise, ok := app.IsPromise(result)
if ok {
go func(ctx context.Context, req *RPCRequest, promise *goja.Promise) {
result := m.server.WaitForPromise(promise)
sendRes(ctx, req, result)
}(clientMessage.Context, req, promise)
} else {
sendRes(clientMessage.Context, req, result)
} }
} }
@ -263,8 +273,8 @@ func RPCModuleFactory(bus bus.Bus) app.ServerModuleFactory {
bus: bus, bus: bus,
} }
go mod.handleMessages()
return mod return mod
} }
} }
var _ app.InitializableModule = &RPCModule{}

View File

@ -92,7 +92,7 @@ var Edge=(()=>{var K3=Object.create;var Mi=Object.defineProperty,Y3=Object.defin
</edge-menu-item> </edge-menu-item>
`}_canAccess(t){var a,o;let i=((a=this._profile)==null?void 0:a.edge_role)||"visitor",n=((o=t.metadata)==null?void 0:o.minimumRole)||"visitor";return sb[i]>=sb[n]}_renderProfile(){let t=this._profile;return re` `}_canAccess(t){var a,o;let i=((a=this._profile)==null?void 0:a.edge_role)||"visitor",n=((o=t.metadata)==null?void 0:o.minimumRole)||"visitor";return sb[i]>=sb[n]}_renderProfile(){let t=this._profile;return re`
<edge-menu-item name='profile' label="${(t==null?void 0:t.preferred_username)||"Profile"}" icon-url='${Zm}'> <edge-menu-item name='profile' label="${(t==null?void 0:t.preferred_username)||"Profile"}" icon-url='${Zm}'>
${t?re`<edge-menu-sub-item name='login' label='Logout' icon-url='${nb}' link-url='/edge/auth/logout'></edge-menu-sub-item>`:re`<edge-menu-sub-item name='login' label='Login' icon-url='${tb}' link-url='/edge/auth/login'></edge-menu-sub-item>`} ${t&&t.iss!="anon"?re`<edge-menu-sub-item name='login' label='Logout' icon-url='${nb}' link-url='/edge/auth/logout'></edge-menu-sub-item>`:re`<edge-menu-sub-item name='login' label='Login' icon-url='${tb}' link-url='/edge/auth/login'></edge-menu-sub-item>`}
</edge-menu-item> </edge-menu-item>
`}_handleMenuItemSelected(t){let i=t.detail.element;i.classList.add("selected"),i.classList.remove("unselected");for(let n,a=0;n=this._menuItems[a];a++)n!==i&&(n.unselect(),n.classList.add("unselected"))}_handleMenuItemUnselected(t){if(t.detail.element.classList.remove("selected"),this.renderRoot.querySelectorAll("edge-menu-item.selected").length===0)for(let a,o=0;a=this._menuItems[o];o++)a.classList.remove("unselected")}};le.styles=Ti` `}_handleMenuItemSelected(t){let i=t.detail.element;i.classList.add("selected"),i.classList.remove("unselected");for(let n,a=0;n=this._menuItems[a];a++)n!==i&&(n.unselect(),n.classList.add("unselected"))}_handleMenuItemUnselected(t){if(t.detail.element.classList.remove("selected"),this.renderRoot.querySelectorAll("edge-menu-item.selected").length===0)for(let a,o=0;a=this._menuItems[o];o++)a.classList.remove("unselected")}};le.styles=Ti`
:host { :host {

File diff suppressed because one or more lines are too long

View File

@ -191,7 +191,7 @@ export class Menu extends LitElement {
return html` return html`
<edge-menu-item name='profile' label="${profile?.preferred_username || 'Profile'}" icon-url='${UserCircleIcon}'> <edge-menu-item name='profile' label="${profile?.preferred_username || 'Profile'}" icon-url='${UserCircleIcon}'>
${ ${
profile ? profile && profile.iss != "anon" ?
html`<edge-menu-sub-item name='login' label='Logout' icon-url='${LogoutIcon}' link-url='/edge/auth/logout'></edge-menu-sub-item>` : html`<edge-menu-sub-item name='login' label='Logout' icon-url='${LogoutIcon}' link-url='/edge/auth/logout'></edge-menu-sub-item>` :
html`<edge-menu-sub-item name='login' label='Login' icon-url='${LoginIcon}' link-url='/edge/auth/login'></edge-menu-sub-item>` html`<edge-menu-sub-item name='login' label='Login' icon-url='${LoginIcon}' link-url='/edge/auth/login'></edge-menu-sub-item>`
} }

View File

@ -1,7 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["ES2015", "DOM"], "lib": ["ES2015", "DOM"],
"experimentalDecorators": true "experimentalDecorators": true,
"esModuleInterop": true
}, },
"include": ["pkg/sdk/client/src/index.d.ts", "pkg/sdk/client/src/**/*.ts", "pkg/sdk/client/src/**/*.svg"] "include": ["pkg/sdk/client/src/index.d.ts", "pkg/sdk/client/src/**/*.ts", "pkg/sdk/client/src/**/*.svg"]
} }