feat(module,auth): authentication module with arbitrary claims support
This commit is contained in:
parent
7f07e52ae0
commit
640f429580
|
@ -2,19 +2,28 @@ package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
||||||
appHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
appHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/net"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||||
|
"github.com/dop251/goja"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
"github.com/golang-jwt/jwt"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
|
@ -53,14 +62,35 @@ func RunCommand() *cli.Command {
|
||||||
Usage: "use `FILE` for SQLite storage database",
|
Usage: "use `FILE` for SQLite storage database",
|
||||||
Value: "data.sqlite",
|
Value: "data.sqlite",
|
||||||
},
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "auth-subject",
|
||||||
|
Usage: "set the `SUBJECT` associated with the simulated connected user",
|
||||||
|
Value: "jdoe",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "auth-role",
|
||||||
|
Usage: "set the `ROLE` associated with the simulated connected user",
|
||||||
|
Value: "user",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "auth-preferred-username",
|
||||||
|
Usage: "set the `PREFERRED_USERNAME` associated with the simulated connected user",
|
||||||
|
Value: "Jane Doe",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Action: func(ctx *cli.Context) error {
|
Action: func(ctx *cli.Context) error {
|
||||||
address := ctx.String("address")
|
address := ctx.String("address")
|
||||||
path := ctx.String("path")
|
path := ctx.String("path")
|
||||||
|
|
||||||
logFormat := ctx.String("log-format")
|
logFormat := ctx.String("log-format")
|
||||||
logLevel := ctx.Int("log-level")
|
logLevel := ctx.Int("log-level")
|
||||||
|
|
||||||
storageFile := ctx.String("storage-file")
|
storageFile := ctx.String("storage-file")
|
||||||
|
|
||||||
|
authSubject := ctx.String("auth-subject")
|
||||||
|
authRole := ctx.String("auth-role")
|
||||||
|
authPreferredUsername := ctx.String("auth-preferred-username")
|
||||||
|
|
||||||
logger.SetFormat(logger.Format(logFormat))
|
logger.SetFormat(logger.Format(logFormat))
|
||||||
logger.SetLevel(logger.Level(logLevel))
|
logger.SetLevel(logger.Level(logLevel))
|
||||||
|
|
||||||
|
@ -81,6 +111,7 @@ func RunCommand() *cli.Command {
|
||||||
mux := chi.NewMux()
|
mux := chi.NewMux()
|
||||||
|
|
||||||
mux.Use(middleware.Logger)
|
mux.Use(middleware.Logger)
|
||||||
|
mux.Use(dummyAuthMiddleware(authSubject, authRole, authPreferredUsername))
|
||||||
|
|
||||||
bus := memory.NewBus()
|
bus := memory.NewBus()
|
||||||
|
|
||||||
|
@ -89,21 +120,12 @@ func RunCommand() *cli.Command {
|
||||||
return errors.Wrapf(err, "could not open database with path '%s'", storageFile)
|
return errors.Wrapf(err, "could not open database with path '%s'", storageFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
documentStore := sqlite.NewDocumentStoreWithDB(db)
|
ds := sqlite.NewDocumentStoreWithDB(db)
|
||||||
blobStore := sqlite.NewBlobStoreWithDB(db)
|
bs := sqlite.NewBlobStoreWithDB(db)
|
||||||
|
|
||||||
handler := appHTTP.NewHandler(
|
handler := appHTTP.NewHandler(
|
||||||
appHTTP.WithBus(bus),
|
appHTTP.WithBus(bus),
|
||||||
appHTTP.WithServerModules(
|
appHTTP.WithServerModules(getServerModules(bus, ds, bs)...),
|
||||||
module.ContextModuleFactory(),
|
|
||||||
module.ConsoleModuleFactory(),
|
|
||||||
cast.CastModuleFactory(),
|
|
||||||
module.LifecycleModuleFactory(bus),
|
|
||||||
module.NetModuleFactory(bus),
|
|
||||||
module.RPCModuleFactory(bus),
|
|
||||||
module.StoreModuleFactory(documentStore),
|
|
||||||
module.BlobModuleFactory(bus, blobStore),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
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")
|
||||||
|
@ -121,3 +143,89 @@ func RunCommand() *cli.Command {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStore) []app.ServerModuleFactory {
|
||||||
|
return []app.ServerModuleFactory{
|
||||||
|
module.ContextModuleFactory(),
|
||||||
|
module.ConsoleModuleFactory(),
|
||||||
|
cast.CastModuleFactory(),
|
||||||
|
module.LifecycleModuleFactory(),
|
||||||
|
net.ModuleFactory(bus),
|
||||||
|
module.RPCModuleFactory(bus),
|
||||||
|
module.StoreModuleFactory(ds),
|
||||||
|
module.BlobModuleFactory(bus, bs),
|
||||||
|
module.Extends(
|
||||||
|
auth.ModuleFactory(
|
||||||
|
auth.WithJWT(dummyKeyFunc),
|
||||||
|
),
|
||||||
|
func(o *goja.Object) {
|
||||||
|
if err := o.Set("CLAIM_ROLE", "role"); err != nil {
|
||||||
|
panic(errors.New("could not set 'CLAIM_ROLE' property"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := o.Set("CLAIM_PREFERRED_USERNAME", "preferred_username"); err != nil {
|
||||||
|
panic(errors.New("could not set 'CLAIM_PREFERRED_USERNAME' property"))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dummySecret = []byte("not_so_secret")
|
||||||
|
|
||||||
|
func dummyKeyFunc(t *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return dummySecret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dummyAuthMiddleware(subject, role, username string) func(http.Handler) http.Handler {
|
||||||
|
return func(h http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
unauthenticated := subject == "" && role == "" && username == ""
|
||||||
|
|
||||||
|
if unauthenticated {
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"nbf": time.Now().UTC().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if subject != "" {
|
||||||
|
claims["sub"] = subject
|
||||||
|
}
|
||||||
|
|
||||||
|
if role != "" {
|
||||||
|
claims["role"] = role
|
||||||
|
}
|
||||||
|
|
||||||
|
if username != "" {
|
||||||
|
claims["preferred_username"] = username
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
rawToken, err := token.SignedString(dummySecret)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not sign token", logger.E(errors.WithStack(err)))
|
||||||
|
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Add("Authorization", "Bearer "+rawToken)
|
||||||
|
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -20,10 +20,11 @@ function onInit() {
|
||||||
|
|
||||||
Listes des modules disponibles côté serveur.
|
Listes des modules disponibles côté serveur.
|
||||||
|
|
||||||
|
- [`auth`](./auth.md)
|
||||||
|
- [`blob`](./blob.md)
|
||||||
|
- [`cast`](./cast.md)
|
||||||
- [`console`](./console.md)
|
- [`console`](./console.md)
|
||||||
- [`context`](./context.md)
|
- [`context`](./context.md)
|
||||||
- [`net`](./net.md)
|
- [`net`](./net.md)
|
||||||
- [`rpc`](./rpc.md)
|
- [`rpc`](./rpc.md)
|
||||||
- [`store`](./store.md)
|
- [`store`](./store.md)
|
||||||
- [`blob`](./blob.md)
|
|
||||||
- [`cast`](./cast.md)
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Module `auth`
|
||||||
|
|
||||||
|
Ce module permet de récupérer des informations concernant l'utilisateur connecté et ses attributs.
|
||||||
|
|
||||||
|
## Méthodes
|
||||||
|
|
||||||
|
### `auth.getClaim(ctx: Context, name: string): string`
|
||||||
|
|
||||||
|
Récupère un attribut associé à l'utilisateur.
|
||||||
|
|
||||||
|
#### Arguments
|
||||||
|
|
||||||
|
- `ctx` **Context** Le contexte d'exécution. Voir la documentation du module [`context`](./context.md)
|
||||||
|
- `name` **string** Nom de l'attribut à retrouver
|
||||||
|
|
||||||
|
#### Valeur de retour
|
||||||
|
|
||||||
|
Valeur de l'attribut associé ou vide si la requête est non authentifiée ou que l'attribut n'a pas été trouvé.
|
||||||
|
|
||||||
|
#### Usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
function onClientMessage(ctx, message) {
|
||||||
|
var subject = auth.getClaim(ctx, auth.CLAIM_SUBJECT);
|
||||||
|
console.log("Connected user is", subject);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Propriétés
|
||||||
|
|
||||||
|
### `auth.CLAIM_SUBJECT`
|
||||||
|
|
||||||
|
Cette propriété identifie l'utilisateur connecté. Si la valeur retournée par la méthode `getClaim()` est vide, alors l'utilisateur n'est pas connecté.
|
||||||
|
|
||||||
|
### `auth.CLAIM_ROLE`
|
||||||
|
|
||||||
|
Cette propriété retourne le rôle de l'utilisateur connecté au sein du "tenant" courant. Si la valeur retournée par la méthode `getClaim()` est vide, alors l'utilisateur n'est pas connecté.
|
||||||
|
|
||||||
|
### `auth.PREFERRED_USERNAME`
|
||||||
|
|
||||||
|
Cette propriété retourne le nom "préféré pour l'affichage" de l'utilisateur connecté. Si la valeur retournée par la méthode `getClaim()` est vide, alors l'utilisateur n'est pas connecté ou l'utilisateur n'a pas défini de nom d'utilisateur particulier.
|
|
@ -21,6 +21,7 @@
|
||||||
</script>
|
</script>
|
||||||
<script src="/edge/sdk/client.js"></script>
|
<script src="/edge/sdk/client.js"></script>
|
||||||
<script src="test/client-sdk.js"></script>
|
<script src="test/client-sdk.js"></script>
|
||||||
|
<script src="test/auth-module.js"></script>
|
||||||
<script class="mocha-exec">
|
<script class="mocha-exec">
|
||||||
mocha.run();
|
mocha.run();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
describe('Auth Module', function() {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
return Edge.connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
Edge.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve user informations', function() {
|
||||||
|
return Edge.rpc("getUserInfo")
|
||||||
|
.then(userInfo => {
|
||||||
|
chai.assert.isNotNull(userInfo.subject);
|
||||||
|
chai.assert.isNotNull(userInfo.role);
|
||||||
|
chai.assert.isNotNull(userInfo.preferredUsername);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -10,12 +10,12 @@ function onInit() {
|
||||||
rpc.register("add", add);
|
rpc.register("add", add);
|
||||||
rpc.register("reset", reset);
|
rpc.register("reset", reset);
|
||||||
rpc.register("total", total);
|
rpc.register("total", total);
|
||||||
|
rpc.register("getUserInfo", getUserInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called for each client message
|
// Called for each client message
|
||||||
function onClientMessage(ctx, data) {
|
function onClientMessage(ctx, data) {
|
||||||
var sessionId = context.get(ctx, context.SESSION_ID);
|
console.log("onClientMessage", data.now);
|
||||||
console.log("onClientMessage", sessionId, data.now);
|
|
||||||
net.send(ctx, { now: data.now });
|
net.send(ctx, { now: data.now });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,3 +61,15 @@ function reset(ctx, params) {
|
||||||
function total(ctx, params) {
|
function total(ctx, params) {
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUserInfo(ctx, params) {
|
||||||
|
var subject = auth.getClaim(ctx, auth.CLAIM_SUBJECT);
|
||||||
|
var role = auth.getClaim(ctx, auth.CLAIM_ROLE);
|
||||||
|
var preferredUsername = auth.getClaim(ctx, auth.CLAIM_PREFERRED_USERNAME);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subject: subject,
|
||||||
|
role: role,
|
||||||
|
preferredUsername: preferredUsername,
|
||||||
|
};
|
||||||
|
}
|
|
@ -65,7 +65,7 @@ func (h *Handler) handleAppUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
||||||
module.ContextKeyOriginRequest: r,
|
ContextKeyOriginRequest: r,
|
||||||
})
|
})
|
||||||
|
|
||||||
requestMsg := module.NewMessageUploadRequest(ctx, fileHeader, metadata)
|
requestMsg := module.NewMessageUploadRequest(ctx, fileHeader, metadata)
|
||||||
|
@ -117,7 +117,7 @@ func (h *Handler) handleAppDownload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
ctx := logger.With(r.Context(), logger.F("blobID", blobID), logger.F("bucket", bucket))
|
ctx := logger.With(r.Context(), logger.F("blobID", blobID), logger.F("bucket", bucket))
|
||||||
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
||||||
module.ContextKeyOriginRequest: r,
|
ContextKeyOriginRequest: r,
|
||||||
})
|
})
|
||||||
|
|
||||||
requestMsg := module.NewMessageDownloadRequest(ctx, bucket, storage.BlobID(blobID))
|
requestMsg := module.NewMessageDownloadRequest(ctx, bucket, storage.BlobID(blobID))
|
||||||
|
|
|
@ -15,6 +15,11 @@ const (
|
||||||
statusChannelClosed = iota
|
statusChannelClosed = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContextKeySessionID module.ContextKey = "sessionId"
|
||||||
|
ContextKeyOriginRequest module.ContextKey = "originRequest"
|
||||||
|
)
|
||||||
|
|
||||||
func (h *Handler) handleSockJS(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleSockJS(w http.ResponseWriter, r *http.Request) {
|
||||||
h.mutex.RLock()
|
h.mutex.RLock()
|
||||||
defer h.mutex.RUnlock()
|
defer h.mutex.RUnlock()
|
||||||
|
@ -79,7 +84,7 @@ func (h *Handler) handleServerMessages(ctx context.Context, sess sockjs.Session)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionID := module.ContextValue[string](serverMessage.Context, module.ContextKeySessionID)
|
sessionID := module.ContextValue[string](serverMessage.Context, ContextKeySessionID)
|
||||||
|
|
||||||
isDest := sessionID == "" || sessionID == sess.ID()
|
isDest := sessionID == "" || sessionID == sess.ID()
|
||||||
if !isDest {
|
if !isDest {
|
||||||
|
@ -182,8 +187,8 @@ func (h *Handler) handleClientMessages(ctx context.Context, sess sockjs.Session)
|
||||||
|
|
||||||
ctx := logger.With(ctx, logger.F("payload", payload))
|
ctx := logger.With(ctx, logger.F("payload", payload))
|
||||||
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
||||||
module.ContextKeySessionID: sess.ID(),
|
ContextKeySessionID: sess.ID(),
|
||||||
module.ContextKeyOriginRequest: sess.Request(),
|
ContextKeyOriginRequest: sess.Request(),
|
||||||
})
|
})
|
||||||
|
|
||||||
clientMessage := module.NewClientMessage(ctx, payload)
|
clientMessage := module.NewClientMessage(ctx, payload)
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnauthenticated = errors.New("unauthenticated")
|
||||||
|
ErrClaimNotFound = errors.New("claim not found")
|
||||||
|
)
|
|
@ -0,0 +1,60 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WithJWT(keyFunc jwt.Keyfunc) OptionFunc {
|
||||||
|
return func(o *Option) {
|
||||||
|
o.GetClaim = func(ctx context.Context, r *http.Request, claimName string) (string, error) {
|
||||||
|
claim, err := getClaim[string](r, claimName, keyFunc)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return claim, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClaim[T any](r *http.Request, claimAttr string, keyFunc jwt.Keyfunc) (T, error) {
|
||||||
|
rawToken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||||
|
if rawToken == "" {
|
||||||
|
rawToken = r.URL.Query().Get("token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rawToken == "" {
|
||||||
|
return *new(T), errors.WithStack(ErrUnauthenticated)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwt.Parse(rawToken, keyFunc)
|
||||||
|
if err != nil {
|
||||||
|
return *new(T), errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !token.Valid {
|
||||||
|
return *new(T), errors.Errorf("invalid jwt token: '%v'", token.Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
mapClaims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
return *new(T), errors.Errorf("unexpected claims type '%T'", token.Claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawClaim, exists := mapClaims[claimAttr]
|
||||||
|
if !exists {
|
||||||
|
return *new(T), errors.WithStack(ErrClaimNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
claim, ok := rawClaim.(T)
|
||||||
|
if !ok {
|
||||||
|
return *new(T), errors.Errorf("unexpected claim '%s' to be of type '%T', got '%T'", claimAttr, new(T), rawClaim)
|
||||||
|
}
|
||||||
|
|
||||||
|
return claim, nil
|
||||||
|
}
|
|
@ -2,23 +2,21 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/golang-jwt/jwt"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AnonymousSubject = "anonymous"
|
ClaimSubject = "sub"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Module struct {
|
type Module struct {
|
||||||
server *app.Server
|
server *app.Server
|
||||||
keyFunc jwt.Keyfunc
|
getClaimFunc GetClaimFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) Name() string {
|
func (m *Module) Name() string {
|
||||||
|
@ -26,59 +24,46 @@ func (m *Module) Name() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) Export(export *goja.Object) {
|
func (m *Module) Export(export *goja.Object) {
|
||||||
if err := export.Set("getSubject", m.getSubject); err != nil {
|
if err := export.Set("getClaim", m.getClaim); err != nil {
|
||||||
panic(errors.Wrap(err, "could not set 'getSubject' function"))
|
panic(errors.Wrap(err, "could not set 'getClaim' function"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := export.Set("ANONYMOUS", AnonymousSubject); err != nil {
|
if err := export.Set("CLAIM_SUBJECT", ClaimSubject); err != nil {
|
||||||
panic(errors.Wrap(err, "could not set 'ANONYMOUS_USER' property"))
|
panic(errors.Wrap(err, "could not set 'CLAIM_SUBJECT' property"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) getSubject(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *Module) getClaim(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
ctx := util.AssertContext(call.Argument(0), rt)
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
|
claimName := util.AssertString(call.Argument(1), rt)
|
||||||
|
|
||||||
req, ok := ctx.Value(module.ContextKeyOriginRequest).(*http.Request)
|
req, ok := ctx.Value(edgeHTTP.ContextKeyOriginRequest).(*http.Request)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(errors.New("could not find http request in context"))
|
panic(rt.ToValue(errors.New("could not find http request in context")))
|
||||||
}
|
}
|
||||||
|
|
||||||
rawToken := strings.TrimPrefix(req.Header.Get("Authorization"), "Bearer ")
|
claim, err := m.getClaimFunc(ctx, req, claimName)
|
||||||
if rawToken == "" {
|
|
||||||
rawToken = req.URL.Query().Get("token")
|
|
||||||
}
|
|
||||||
|
|
||||||
if rawToken == "" {
|
|
||||||
return rt.ToValue(AnonymousSubject)
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := jwt.Parse(rawToken, m.keyFunc)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.WithStack(err))
|
if errors.Is(err, ErrUnauthenticated) || errors.Is(err, ErrClaimNotFound) {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !token.Valid {
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
panic(errors.Errorf("invalid jwt token: '%v'", token.Raw))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mapClaims, ok := token.Claims.(jwt.MapClaims)
|
return rt.ToValue(claim)
|
||||||
if !ok {
|
|
||||||
panic(errors.Errorf("unexpected claims type '%T'", token.Claims))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subject, exists := mapClaims["sub"]
|
func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory {
|
||||||
if !exists {
|
opt := &Option{}
|
||||||
return rt.ToValue(AnonymousSubject)
|
for _, fn := range funcs {
|
||||||
|
fn(opt)
|
||||||
}
|
}
|
||||||
|
|
||||||
return rt.ToValue(subject)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ModuleFactory(keyFunc jwt.Keyfunc) app.ServerModuleFactory {
|
|
||||||
return func(server *app.Server) app.ServerModule {
|
return func(server *app.Server) app.ServerModule {
|
||||||
return &Module{
|
return &Module{
|
||||||
server: server,
|
server: server,
|
||||||
keyFunc: keyFunc,
|
getClaimFunc: opt.GetClaim,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||||
"github.com/golang-jwt/jwt"
|
"github.com/golang-jwt/jwt"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -25,7 +26,9 @@ func TestAuthModule(t *testing.T) {
|
||||||
|
|
||||||
server := app.NewServer(
|
server := app.NewServer(
|
||||||
module.ConsoleModuleFactory(),
|
module.ConsoleModuleFactory(),
|
||||||
ModuleFactory(keyFunc),
|
ModuleFactory(
|
||||||
|
WithJWT(keyFunc),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
data, err := ioutil.ReadFile("testdata/auth.js")
|
data, err := ioutil.ReadFile("testdata/auth.js")
|
||||||
|
@ -60,7 +63,7 @@ func TestAuthModule(t *testing.T) {
|
||||||
|
|
||||||
req.Header.Add("Authorization", "Bearer "+rawToken)
|
req.Header.Add("Authorization", "Bearer "+rawToken)
|
||||||
|
|
||||||
ctx := context.WithValue(context.Background(), module.ContextKeyOriginRequest, req)
|
ctx := context.WithValue(context.Background(), edgeHTTP.ContextKeyOriginRequest, req)
|
||||||
|
|
||||||
if _, err := server.ExecFuncByName("testAuth", ctx); err != nil {
|
if _, err := server.ExecFuncByName("testAuth", ctx); err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
@ -76,7 +79,7 @@ func TestAuthAnonymousModule(t *testing.T) {
|
||||||
|
|
||||||
server := app.NewServer(
|
server := app.NewServer(
|
||||||
module.ConsoleModuleFactory(),
|
module.ConsoleModuleFactory(),
|
||||||
ModuleFactory(keyFunc),
|
ModuleFactory(WithJWT(keyFunc)),
|
||||||
)
|
)
|
||||||
|
|
||||||
data, err := ioutil.ReadFile("testdata/auth_anonymous.js")
|
data, err := ioutil.ReadFile("testdata/auth_anonymous.js")
|
||||||
|
@ -99,7 +102,7 @@ func TestAuthAnonymousModule(t *testing.T) {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.WithValue(context.Background(), module.ContextKeyOriginRequest, req)
|
ctx := context.WithValue(context.Background(), edgeHTTP.ContextKeyOriginRequest, req)
|
||||||
|
|
||||||
if _, err := server.ExecFuncByName("testAuth", ctx); err != nil {
|
if _, err := server.ExecFuncByName("testAuth", ctx); err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetClaimFunc func(ctx context.Context, r *http.Request, claimName string) (string, error)
|
||||||
|
|
||||||
|
type Option struct {
|
||||||
|
GetClaim GetClaimFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type OptionFunc func(*Option)
|
||||||
|
|
||||||
|
func WithGetClaim(fn GetClaimFunc) OptionFunc {
|
||||||
|
return func(o *Option) {
|
||||||
|
o.GetClaim = fn
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
|
|
||||||
function testAuth(ctx) {
|
function testAuth(ctx) {
|
||||||
var subject = auth.getSubject(ctx);
|
var subject = auth.getClaim(ctx, auth.CLAIM_SUBJECT);
|
||||||
|
|
||||||
if (subject !== "jdoe") {
|
if (subject !== "jdoe") {
|
||||||
throw new Error("subject: expected 'jdoe', got '"+subject+"'");
|
throw new Error("subject: expected 'jdoe', got '"+subject+"'");
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
|
|
||||||
|
|
||||||
function testAuth(ctx) {
|
function testAuth(ctx) {
|
||||||
var subject = auth.getSubject(ctx);
|
var subject = auth.getClaim(ctx, auth.CLAIM_SUBJECT);
|
||||||
|
|
||||||
if (subject !== auth.ANONYMOUS) {
|
if (subject !== undefined) {
|
||||||
throw new Error("subject: expected '"+auth.ANONYMOUS+"', got '"+subject+"'");
|
throw new Error("subject: expected undefined, got '"+subject+"'");
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -60,7 +60,7 @@ func (m *Module) refreshDevices(call goja.FunctionCall, rt *goja.Runtime) goja.V
|
||||||
|
|
||||||
timeout, err := m.parseTimeout(rawTimeout)
|
timeout, err := m.parseTimeout(rawTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
promise := m.server.NewPromise()
|
promise := m.server.NewPromise()
|
||||||
|
@ -106,7 +106,7 @@ func (m *Module) getDevices(call goja.FunctionCall, rt *goja.Runtime) goja.Value
|
||||||
|
|
||||||
func (m *Module) loadUrl(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *Module) loadUrl(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
if len(call.Arguments) < 2 {
|
if len(call.Arguments) < 2 {
|
||||||
panic(errors.WithStack(module.ErrUnexpectedArgumentsNumber))
|
panic(rt.ToValue(errors.WithStack(module.ErrUnexpectedArgumentsNumber)))
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceUUID := call.Argument(0).String()
|
deviceUUID := call.Argument(0).String()
|
||||||
|
@ -116,7 +116,7 @@ func (m *Module) loadUrl(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
|
||||||
timeout, err := m.parseTimeout(rawTimeout)
|
timeout, err := m.parseTimeout(rawTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
promise := m.server.NewPromise()
|
promise := m.server.NewPromise()
|
||||||
|
@ -144,9 +144,9 @@ func (m *Module) loadUrl(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
return m.server.ToValue(promise)
|
return m.server.ToValue(promise)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) stopCast(call goja.FunctionCall) goja.Value {
|
func (m *Module) stopCast(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
if len(call.Arguments) < 1 {
|
if len(call.Arguments) < 1 {
|
||||||
panic(errors.WithStack(module.ErrUnexpectedArgumentsNumber))
|
panic(rt.ToValue(errors.WithStack(module.ErrUnexpectedArgumentsNumber)))
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceUUID := call.Argument(0).String()
|
deviceUUID := call.Argument(0).String()
|
||||||
|
@ -154,7 +154,7 @@ func (m *Module) stopCast(call goja.FunctionCall) goja.Value {
|
||||||
|
|
||||||
timeout, err := m.parseTimeout(rawTimeout)
|
timeout, err := m.parseTimeout(rawTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
promise := m.server.NewPromise()
|
promise := m.server.NewPromise()
|
||||||
|
@ -182,9 +182,9 @@ func (m *Module) stopCast(call goja.FunctionCall) goja.Value {
|
||||||
return m.server.ToValue(promise)
|
return m.server.ToValue(promise)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) getStatus(call goja.FunctionCall) goja.Value {
|
func (m *Module) getStatus(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
if len(call.Arguments) < 1 {
|
if len(call.Arguments) < 1 {
|
||||||
panic(errors.WithStack(module.ErrUnexpectedArgumentsNumber))
|
panic(rt.ToValue(errors.WithStack(module.ErrUnexpectedArgumentsNumber)))
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceUUID := call.Argument(0).String()
|
deviceUUID := call.Argument(0).String()
|
||||||
|
@ -192,7 +192,7 @@ func (m *Module) getStatus(call goja.FunctionCall) goja.Value {
|
||||||
|
|
||||||
timeout, err := m.parseTimeout(rawTimeout)
|
timeout, err := m.parseTimeout(rawTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
promise := m.server.NewPromise()
|
promise := m.server.NewPromise()
|
||||||
|
|
|
@ -11,11 +11,6 @@ import (
|
||||||
|
|
||||||
type ContextKey string
|
type ContextKey string
|
||||||
|
|
||||||
const (
|
|
||||||
ContextKeySessionID ContextKey = "sessionId"
|
|
||||||
ContextKeyOriginRequest ContextKey = "originRequest"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ContextModule struct{}
|
type ContextModule struct{}
|
||||||
|
|
||||||
func (m *ContextModule) Name() string {
|
func (m *ContextModule) Name() string {
|
||||||
|
@ -61,14 +56,6 @@ func (m *ContextModule) Export(export *goja.Object) {
|
||||||
if err := export.Set("with", m.with); err != nil {
|
if err := export.Set("with", m.with); err != nil {
|
||||||
panic(errors.Wrap(err, "could not set 'with' function"))
|
panic(errors.Wrap(err, "could not set 'with' function"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := export.Set("ORIGIN_REQUEST", string(ContextKeyOriginRequest)); err != nil {
|
|
||||||
panic(errors.Wrap(err, "could not set 'ORIGIN_REQUEST' property"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := export.Set("SESSION_ID", string(ContextKeySessionID)); err != nil {
|
|
||||||
panic(errors.Wrap(err, "could not set 'SESSION_ID' property"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ContextModuleFactory() app.ServerModuleFactory {
|
func ContextModuleFactory() app.ServerModuleFactory {
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package module
|
||||||
|
|
||||||
|
import (
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExtensionFunc func(*goja.Object)
|
||||||
|
|
||||||
|
type ExtendedModule struct {
|
||||||
|
module app.ServerModule
|
||||||
|
extensions []ExtensionFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export implements app.ServerModule.
|
||||||
|
func (m *ExtendedModule) Export(exports *goja.Object) {
|
||||||
|
m.module.Export(exports)
|
||||||
|
|
||||||
|
for _, ext := range m.extensions {
|
||||||
|
ext(exports)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implements app.ServerModule.
|
||||||
|
func (m *ExtendedModule) Name() string {
|
||||||
|
return m.module.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Extends(factory app.ServerModuleFactory, extensions ...ExtensionFunc) app.ServerModuleFactory {
|
||||||
|
return func(s *app.Server) app.ServerModule {
|
||||||
|
module := factory(s)
|
||||||
|
|
||||||
|
return &ExtendedModule{
|
||||||
|
module: module,
|
||||||
|
extensions: extensions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ app.ServerModule = &ExtendedModule{}
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
@ -12,7 +11,6 @@ import (
|
||||||
|
|
||||||
type LifecycleModule struct {
|
type LifecycleModule struct {
|
||||||
server *app.Server
|
server *app.Server
|
||||||
bus bus.Bus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *LifecycleModule) Name() string {
|
func (m *LifecycleModule) Name() string {
|
||||||
|
@ -36,84 +34,12 @@ func (m *LifecycleModule) OnInit() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *LifecycleModule) handleMessages() {
|
func LifecycleModuleFactory() app.ServerModuleFactory {
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
logger.Debug(
|
|
||||||
ctx,
|
|
||||||
"subscribing to bus messages",
|
|
||||||
)
|
|
||||||
|
|
||||||
clientMessages, err := m.bus.Subscribe(ctx, MessageNamespaceClient)
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
logger.Debug(
|
|
||||||
ctx,
|
|
||||||
"unsubscribing from bus messages",
|
|
||||||
)
|
|
||||||
|
|
||||||
m.bus.Unsubscribe(ctx, MessageNamespaceClient, clientMessages)
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
logger.Debug(
|
|
||||||
ctx,
|
|
||||||
"waiting for next message",
|
|
||||||
)
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
logger.Debug(
|
|
||||||
ctx,
|
|
||||||
"context done",
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
case msg := <-clientMessages:
|
|
||||||
clientMessage, ok := msg.(*ClientMessage)
|
|
||||||
if !ok {
|
|
||||||
logger.Error(
|
|
||||||
ctx,
|
|
||||||
"unexpected message type",
|
|
||||||
logger.F("message", msg),
|
|
||||||
)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug(
|
|
||||||
ctx,
|
|
||||||
"received client message",
|
|
||||||
logger.F("message", clientMessage),
|
|
||||||
)
|
|
||||||
|
|
||||||
if _, err := m.server.ExecFuncByName("onClientMessage", clientMessage.Context, clientMessage.Data); err != nil {
|
|
||||||
if errors.Is(err, app.ErrFuncDoesNotExist) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Error(
|
|
||||||
ctx,
|
|
||||||
"on client message error",
|
|
||||||
logger.E(err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LifecycleModuleFactory(bus bus.Bus) app.ServerModuleFactory {
|
|
||||||
return func(server *app.Server) app.ServerModule {
|
return func(server *app.Server) app.ServerModule {
|
||||||
module := &LifecycleModule{
|
module := &LifecycleModule{
|
||||||
server: server,
|
server: server,
|
||||||
bus: bus,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go module.handleMessages()
|
|
||||||
|
|
||||||
return module
|
return module
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
package module
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NetModule struct {
|
|
||||||
server *app.Server
|
|
||||||
bus bus.Bus
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *NetModule) Name() string {
|
|
||||||
return "net"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *NetModule) Export(export *goja.Object) {
|
|
||||||
if err := export.Set("broadcast", m.broadcast); err != nil {
|
|
||||||
panic(errors.Wrap(err, "could not set 'broadcast' function"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := export.Set("send", m.send); err != nil {
|
|
||||||
panic(errors.Wrap(err, "could not set 'send' function"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *NetModule) broadcast(call goja.FunctionCall) goja.Value {
|
|
||||||
if len(call.Arguments) < 1 {
|
|
||||||
panic(m.server.ToValue("invalid number of argument"))
|
|
||||||
}
|
|
||||||
|
|
||||||
data := call.Argument(0).Export()
|
|
||||||
|
|
||||||
msg := NewServerMessage(nil, data)
|
|
||||||
if err := m.bus.Publish(context.Background(), msg); err != nil {
|
|
||||||
panic(errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *NetModule) send(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
|
||||||
if len(call.Arguments) < 2 {
|
|
||||||
panic(m.server.ToValue("invalid number of argument"))
|
|
||||||
}
|
|
||||||
|
|
||||||
var ctx context.Context
|
|
||||||
|
|
||||||
firstArg := call.Argument(0)
|
|
||||||
|
|
||||||
sessionID, ok := firstArg.Export().(string)
|
|
||||||
if ok {
|
|
||||||
ctx = WithContext(context.Background(), map[ContextKey]any{
|
|
||||||
ContextKeySessionID: sessionID,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
ctx = util.AssertContext(firstArg, rt)
|
|
||||||
}
|
|
||||||
|
|
||||||
data := call.Argument(1).Export()
|
|
||||||
|
|
||||||
msg := NewServerMessage(ctx, data)
|
|
||||||
if err := m.bus.Publish(ctx, msg); err != nil {
|
|
||||||
panic(errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NetModuleFactory(bus bus.Bus) app.ServerModuleFactory {
|
|
||||||
return func(server *app.Server) app.ServerModule {
|
|
||||||
return &NetModule{
|
|
||||||
server: server,
|
|
||||||
bus: bus,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
|
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Module struct {
|
||||||
|
server *app.Server
|
||||||
|
bus bus.Bus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) Name() string {
|
||||||
|
return "net"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) Export(export *goja.Object) {
|
||||||
|
if err := export.Set("broadcast", m.broadcast); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'broadcast' function"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := export.Set("send", m.send); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'send' function"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) broadcast(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
if len(call.Arguments) < 1 {
|
||||||
|
panic(rt.ToValue(errors.New("invalid number of argument")))
|
||||||
|
}
|
||||||
|
|
||||||
|
data := call.Argument(0).Export()
|
||||||
|
|
||||||
|
msg := module.NewServerMessage(nil, data)
|
||||||
|
if err := m.bus.Publish(context.Background(), msg); err != nil {
|
||||||
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) send(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
if len(call.Arguments) < 2 {
|
||||||
|
panic(rt.ToValue(errors.New("invalid number of argument")))
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctx context.Context
|
||||||
|
|
||||||
|
firstArg := call.Argument(0)
|
||||||
|
|
||||||
|
sessionID, ok := firstArg.Export().(string)
|
||||||
|
if ok {
|
||||||
|
ctx = module.WithContext(context.Background(), map[module.ContextKey]any{
|
||||||
|
edgeHTTP.ContextKeySessionID: sessionID,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ctx = util.AssertContext(firstArg, rt)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := call.Argument(1).Export()
|
||||||
|
|
||||||
|
msg := module.NewServerMessage(ctx, data)
|
||||||
|
if err := m.bus.Publish(ctx, msg); err != nil {
|
||||||
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) handleClientMessages() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
logger.Debug(
|
||||||
|
ctx,
|
||||||
|
"subscribing to bus messages",
|
||||||
|
)
|
||||||
|
|
||||||
|
clientMessages, err := m.bus.Subscribe(ctx, module.MessageNamespaceClient)
|
||||||
|
if err != nil {
|
||||||
|
panic(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
logger.Debug(
|
||||||
|
ctx,
|
||||||
|
"unsubscribing from bus messages",
|
||||||
|
)
|
||||||
|
|
||||||
|
m.bus.Unsubscribe(ctx, module.MessageNamespaceClient, clientMessages)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
logger.Debug(
|
||||||
|
ctx,
|
||||||
|
"waiting for next message",
|
||||||
|
)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Debug(
|
||||||
|
ctx,
|
||||||
|
"context done",
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
case msg := <-clientMessages:
|
||||||
|
clientMessage, ok := msg.(*module.ClientMessage)
|
||||||
|
if !ok {
|
||||||
|
logger.Error(
|
||||||
|
ctx,
|
||||||
|
"unexpected message type",
|
||||||
|
logger.F("message", msg),
|
||||||
|
)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(
|
||||||
|
ctx,
|
||||||
|
"received client message",
|
||||||
|
logger.F("message", clientMessage),
|
||||||
|
)
|
||||||
|
|
||||||
|
if _, err := m.server.ExecFuncByName("onClientMessage", clientMessage.Context, clientMessage.Data); err != nil {
|
||||||
|
if errors.Is(err, app.ErrFuncDoesNotExist) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error(
|
||||||
|
ctx,
|
||||||
|
"on client message error",
|
||||||
|
logger.E(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ModuleFactory(bus bus.Bus) app.ServerModuleFactory {
|
||||||
|
return func(server *app.Server) app.ServerModule {
|
||||||
|
module := &Module{
|
||||||
|
server: server,
|
||||||
|
bus: bus,
|
||||||
|
}
|
||||||
|
|
||||||
|
go module.handleClientMessages()
|
||||||
|
|
||||||
|
return module
|
||||||
|
}
|
||||||
|
}
|
|
@ -161,8 +161,14 @@ func (m *RPCModule) handleMessages() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := m.server.Exec(callable, ctx, req.Params)
|
result, err := m.server.Exec(callable, clientMessage.Context, req.Params)
|
||||||
if err != nil {
|
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 {
|
if err := m.sendErrorResponse(clientMessage.Context, req, err); err != nil {
|
||||||
logger.Error(
|
logger.Error(
|
||||||
ctx, "could not send error response",
|
ctx, "could not send error response",
|
||||||
|
|
|
@ -54,7 +54,7 @@ func (m *StoreModule) upsert(call goja.FunctionCall, rt *goja.Runtime) goja.Valu
|
||||||
|
|
||||||
document, err := m.store.Upsert(ctx, collection, document)
|
document, err := m.store.Upsert(ctx, collection, document)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.Wrapf(err, "error while upserting document in collection '%s'", collection))
|
panic(rt.ToValue(errors.Wrapf(err, "error while upserting document in collection '%s'", collection)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return rt.ToValue(map[string]interface{}(document))
|
return rt.ToValue(map[string]interface{}(document))
|
||||||
|
@ -71,7 +71,7 @@ func (m *StoreModule) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
panic(errors.Wrapf(err, "error while getting document '%s' in collection '%s'", documentID, collection))
|
panic(rt.ToValue(errors.Wrapf(err, "error while getting document '%s' in collection '%s'", documentID, collection)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return rt.ToValue(map[string]interface{}(document))
|
return rt.ToValue(map[string]interface{}(document))
|
||||||
|
@ -114,7 +114,7 @@ func (m *StoreModule) query(call goja.FunctionCall, rt *goja.Runtime) goja.Value
|
||||||
|
|
||||||
documents, err := m.store.Query(ctx, collection, filter, queryOptionsFuncs...)
|
documents, err := m.store.Query(ctx, collection, filter, queryOptionsFuncs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.Wrapf(err, "error while querying documents in collection '%s'", collection))
|
panic(rt.ToValue(errors.Wrapf(err, "error while querying documents in collection '%s'", collection)))
|
||||||
}
|
}
|
||||||
|
|
||||||
rawDocuments := make([]map[string]interface{}, len(documents))
|
rawDocuments := make([]map[string]interface{}, len(documents))
|
||||||
|
@ -131,7 +131,7 @@ func (m *StoreModule) delete(call goja.FunctionCall, rt *goja.Runtime) goja.Valu
|
||||||
documentID := m.assertDocumentID(call.Argument(2), rt)
|
documentID := m.assertDocumentID(call.Argument(2), rt)
|
||||||
|
|
||||||
if err := m.store.Delete(ctx, collection, documentID); err != nil {
|
if err := m.store.Delete(ctx, collection, documentID); err != nil {
|
||||||
panic(errors.Wrapf(err, "error while deleting document '%s' in collection '%s'", documentID, collection))
|
panic(rt.ToValue(errors.Wrapf(err, "error while deleting document '%s' in collection '%s'", documentID, collection)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -158,7 +158,7 @@ func (m *StoreModule) assertFilter(value goja.Value, rt *goja.Runtime) *filter.F
|
||||||
|
|
||||||
filter, err := filter.NewFrom(rawFilter)
|
filter, err := filter.NewFrom(rawFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.Wrap(err, "could not convert object to filter"))
|
panic(rt.ToValue(errors.Wrap(err, "could not convert object to filter")))
|
||||||
}
|
}
|
||||||
|
|
||||||
return filter
|
return filter
|
||||||
|
@ -191,7 +191,7 @@ func (m *StoreModule) assertQueryOptions(value goja.Value, rt *goja.Runtime) *qu
|
||||||
queryOptions := &queryOptions{}
|
queryOptions := &queryOptions{}
|
||||||
|
|
||||||
if err := mapstructure.Decode(rawQueryOptions, queryOptions); err != nil {
|
if err := mapstructure.Decode(rawQueryOptions, queryOptions); err != nil {
|
||||||
panic(errors.Wrap(err, "could not convert object to query options"))
|
panic(rt.ToValue(errors.Wrap(err, "could not convert object to query options")))
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryOptions
|
return queryOptions
|
||||||
|
|
|
@ -2,9 +2,9 @@ package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AssertType[T any](v goja.Value, rt *goja.Runtime) T {
|
func AssertType[T any](v goja.Value, rt *goja.Runtime) T {
|
||||||
|
@ -12,7 +12,7 @@ func AssertType[T any](v goja.Value, rt *goja.Runtime) T {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
panic(rt.NewTypeError(fmt.Sprintf("expected value to be a '%T', got '%T'", new(T), v.Export())))
|
panic(rt.ToValue(errors.Errorf("expected value to be a '%T', got '%T'", *new(T), v.Export())))
|
||||||
}
|
}
|
||||||
|
|
||||||
func AssertContext(v goja.Value, r *goja.Runtime) context.Context {
|
func AssertContext(v goja.Value, r *goja.Runtime) context.Context {
|
||||||
|
|
|
@ -90,8 +90,6 @@ export class Client extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleRPCResponse(evt) {
|
_handleRPCResponse(evt) {
|
||||||
console.log(evt);
|
|
||||||
|
|
||||||
const { jsonrpc, id, error, result } = evt.detail;
|
const { jsonrpc, id, error, result } = evt.detail;
|
||||||
|
|
||||||
if (jsonrpc !== '2.0' || id === undefined) return;
|
if (jsonrpc !== '2.0' || id === undefined) return;
|
||||||
|
|
Loading…
Reference in New Issue