Compare commits

...

24 Commits

Author SHA1 Message Date
8e574c299b feat(storage): rpc based implementation
All checks were successful
arcad/edge/pipeline/pr-master This commit looks good
2023-09-28 12:36:30 -06:00
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
f99b1ac6ac feat(module,share): cross-app resource sharing module
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-21 12:40:09 +02:00
1606ff5937 chore: use go 1.20.2
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-20 19:27:05 +02:00
90020d6ea6 feat(module,cast): enhance casting device discovery speed
Some checks failed
arcad/edge/pipeline/head There was a failure building this commit
2023-04-20 19:20:52 +02:00
7b6e39088d feat(cli,run): use 127.0.0.1 as default host value if empty
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-20 14:17:03 +02:00
9944a37670 chore: cache modd tool
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-20 13:57:46 +02:00
78307b6850 feat(cli,app): filter out ipv6 adresses for url resolving 2023-04-20 13:57:46 +02:00
2543386e5c Mise à jour de 'doc/apps/client-api/edge-menu.md'
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-20 13:14:12 +02:00
20c4189599 feat(sdk,client): minify generated script
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-20 10:41:03 +02:00
c7b639b643 feat(cli,app): compress http responses
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-20 10:36:20 +02:00
b5b4042cc7 feat(sdk,client): add menu to help navigation between apps
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-20 10:17:37 +02:00
9e3fc427bb feat(sdk,client): target es2015 for old chromecast compatibility
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-14 16:24:34 +02:00
d0b57ab15f fix(client,sdk): accept empty token from parent frame
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-13 13:41:35 +02:00
dc93c585eb fix(sdk,client): add listener to current frame window
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-13 12:07:52 +02:00
de330c0042 fix(sdk,client): use origin as postmessage target
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-13 11:35:31 +02:00
310dac296f feat(storage,sqlite): begin tx with context
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-13 11:23:34 +02:00
4db7576b12 feat(client,sdk): retrieve auth token from parent frame + better resize detection
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-13 11:02:24 +02:00
187 changed files with 7663 additions and 4911 deletions

3
.env.dist Normal file
View File

@ -0,0 +1,3 @@
RUN_APP_ARGS=""
#EDGE_DOCUMENTSTORE_DSN="rpc://localhost:3001/documentstore?tenant=local&appId=%APPID%"
#EDGE_BLOBSTORE_DSN="rpc://localhost:3001/blobstore?tenant=local&appId=%APPID%"

1
.gitignore vendored
View File

@ -5,3 +5,4 @@
*.sqlite *.sqlite
/.gitea-release /.gitea-release
/.edge /.edge
/data

View File

@ -9,27 +9,39 @@ ESBUILD_VERSION ?= v0.17.5
GIT_VERSION := $(shell git describe --always) GIT_VERSION := $(shell git describe --always)
DATE_VERSION := $(shell date +%Y.%-m.%-d) DATE_VERSION := $(shell date +%Y.%-m.%-d)
FULL_VERSION := v$(DATE_VERSION)-$(GIT_VERSION)$(if $(shell git diff --stat),-dirty,) FULL_VERSION := v$(DATE_VERSION)-$(GIT_VERSION)$(if $(shell git diff --stat),-dirty,)
APP_PATH ?= misc/client-sdk-testsuite/dist
RUN_APP_ARGS ?=
RUN_STORAGE_SERVER_ARGS ?=
build: build-edge-cli build-client-sdk-test-app SHELL := bash
watch:
go run -mod=readonly github.com/cortesi/modd/cmd/modd@latest build: build-cli build-storage-server build-client-sdk-test-app
watch: tools/modd/bin/modd
tools/modd/bin/modd
.PHONY: test .PHONY: test
test: test-go test: test-go
test-go: test-go:
go test -v -count=1 $(GOTEST_ARGS) ./... go test -count=1 $(GOTEST_ARGS) ./...
lint: lint:
golangci-lint run --enable-all $(LINT_ARGS) golangci-lint run --enable-all $(LINT_ARGS)
build-edge-cli: build-sdk build-cli: build-sdk
CGO_ENABLED=0 go build \ CGO_ENABLED=0 go build \
-v \ -v \
-o ./bin/cli \ -o ./bin/cli \
./cmd/cli ./cmd/cli
build-storage-server: build-sdk
CGO_ENABLED=0 go build \
-v \
-o ./bin/storage-server \
./cmd/storage-server
build-client-sdk-test-app: build-client-sdk-test-app:
cd misc/client-sdk-testsuite && $(MAKE) dist cd misc/client-sdk-testsuite && $(MAKE) dist
@ -48,19 +60,29 @@ pkg/sdk/client/dist/client.js: tools/esbuild/bin/esbuild node_modules
mkdir -p pkg/sdk/client/dist mkdir -p pkg/sdk/client/dist
tools/esbuild/bin/esbuild \ tools/esbuild/bin/esbuild \
pkg/sdk/client/src/index.ts \ pkg/sdk/client/src/index.ts \
--minify \
--bundle \ --bundle \
--sourcemap \ --sourcemap \
--target=es2020 \ --target=es2015 \
--format=iife \ --format=iife \
--global-name=Edge \ --global-name=Edge \
--define:global=window \ --define:global=window \
--platform=browser \ --platform=browser \
--footer:js="EdgeFrame=Edge.crossFrameMessenger;Edge=Edge.client" \ --loader:.svg=dataurl \
--outfile=pkg/sdk/client/dist/client.js --outfile=pkg/sdk/client/dist/client.js
node_modules: node_modules:
npm ci npm ci
run-app: .env
( set -o allexport && source .env && set +o allexport && bin/cli app run -p $(APP_PATH) $$RUN_APP_ARGS )
run-storage-server: .env
( set -o allexport && source .env && set +o allexport && bin/storage-server run $$RUN_STORAGE_SERVER_ARGS )
.env:
cp .env.dist .env
gitea-release: tools/yq/bin/yq tools/gitea-release/bin/gitea-release.sh build gitea-release: tools/yq/bin/yq tools/gitea-release/bin/gitea-release.sh build
mkdir -p .gitea-release mkdir -p .gitea-release
rm -rf .gitea-release/* rm -rf .gitea-release/*
@ -92,3 +114,7 @@ tools/yq/bin/yq:
mkdir -p tools/yq/bin mkdir -p tools/yq/bin
curl -L --output tools/yq/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_amd64 curl -L --output tools/yq/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_amd64
chmod +x tools/yq/bin/yq chmod +x tools/yq/bin/yq
tools/modd/bin/modd:
mkdir -p tools/modd/bin
GOBIN=$(PWD)/tools/modd/bin go install -mod=readonly github.com/cortesi/modd/cmd/modd@latest

View File

@ -4,9 +4,9 @@
"algo": "argon2id", "algo": "argon2id",
"password": "$argon2id$v=19$m=65536,t=3,p=2$cWOxfEyBy4EyKZR5usB2Pw$xG+Z/E2DUJP9kF0s1fhZjIuP03gFQ65dP7pHRJz7eR8", "password": "$argon2id$v=19$m=65536,t=3,p=2$cWOxfEyBy4EyKZR5usB2Pw$xG+Z/E2DUJP9kF0s1fhZjIuP03gFQ65dP7pHRJz7eR8",
"claims": { "claims": {
"arcad_entrypoint": "edge", "edge_entrypoint": "edge",
"arcad_role": "superadmin", "edge_role": "superadmin",
"arcad_tenant": "dev.cli", "edge_tenant": "dev.cli",
"preferred_username": "SuperAdmin", "preferred_username": "SuperAdmin",
"sub": "superadmin" "sub": "superadmin"
} }
@ -16,9 +16,9 @@
"algo": "argon2id", "algo": "argon2id",
"password": "$argon2id$v=19$m=65536,t=3,p=2$WXXc4ECnkej6WO7f0Xya6Q$UG2wcGltJcuW0cNTR85mAl65tI1kFWMMw7ADS2FMOvY", "password": "$argon2id$v=19$m=65536,t=3,p=2$WXXc4ECnkej6WO7f0Xya6Q$UG2wcGltJcuW0cNTR85mAl65tI1kFWMMw7ADS2FMOvY",
"claims": { "claims": {
"arcad_entrypoint": "edge", "edge_entrypoint": "edge",
"arcad_role": "admin", "edge_role": "admin",
"arcad_tenant": "dev.cli", "edge_tenant": "dev.cli",
"preferred_username": "Admin", "preferred_username": "Admin",
"sub": "admin" "sub": "admin"
} }
@ -28,9 +28,9 @@
"algo": "argon2id", "algo": "argon2id",
"password": "$argon2id$v=19$m=65536,t=3,p=2$gkDAWCzfU23+un3x0ny+YA$L/NSPrd5iKPK/UnSCKfSz4EO+v94N3LTLky4QGJOfpI", "password": "$argon2id$v=19$m=65536,t=3,p=2$gkDAWCzfU23+un3x0ny+YA$L/NSPrd5iKPK/UnSCKfSz4EO+v94N3LTLky4QGJOfpI",
"claims": { "claims": {
"arcad_entrypoint": "edge", "edge_entrypoint": "edge",
"arcad_role": "superuser", "edge_role": "superuser",
"arcad_tenant": "dev.cli", "edge_tenant": "dev.cli",
"preferred_username": "SuperUser", "preferred_username": "SuperUser",
"sub": "superuser" "sub": "superuser"
} }
@ -40,9 +40,9 @@
"algo": "argon2id", "algo": "argon2id",
"password": "$argon2id$v=19$m=65536,t=3,p=2$DhUm9qXUKP35Lzp5M37eZA$2+h6yDxSTHZqFZIuI7JZfFWozwrObna8a8yCgEEPlPE", "password": "$argon2id$v=19$m=65536,t=3,p=2$DhUm9qXUKP35Lzp5M37eZA$2+h6yDxSTHZqFZIuI7JZfFWozwrObna8a8yCgEEPlPE",
"claims": { "claims": {
"arcad_entrypoint": "edge", "edge_entrypoint": "edge",
"arcad_role": "user", "edge_role": "user",
"arcad_tenant": "dev.cli", "edge_tenant": "dev.cli",
"preferred_username": "User", "preferred_username": "User",
"sub": "user" "sub": "user"
} }

View File

@ -4,12 +4,13 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"sync"
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bus" "forge.cadoles.com/arcad/edge/pkg/bus"
@ -18,18 +19,18 @@ import (
"forge.cadoles.com/arcad/edge/pkg/module" "forge.cadoles.com/arcad/edge/pkg/module"
appModule "forge.cadoles.com/arcad/edge/pkg/module/app" appModule "forge.cadoles.com/arcad/edge/pkg/module/app"
appModuleMemory "forge.cadoles.com/arcad/edge/pkg/module/app/memory" appModuleMemory "forge.cadoles.com/arcad/edge/pkg/module/app/memory"
"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"
netModule "forge.cadoles.com/arcad/edge/pkg/module/net" netModule "forge.cadoles.com/arcad/edge/pkg/module/net"
shareModule "forge.cadoles.com/arcad/edge/pkg/module/share"
"forge.cadoles.com/arcad/edge/pkg/storage" "forge.cadoles.com/arcad/edge/pkg/storage"
"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/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwa"
@ -41,6 +42,12 @@ import (
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id" _ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id"
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain" _ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain"
// Register storage drivers
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
) )
func RunCommand() *cli.Command { func RunCommand() *cli.Command {
@ -48,15 +55,15 @@ func RunCommand() *cli.Command {
Name: "run", Name: "run",
Usage: "Run the specified app bundle", Usage: "Run the specified app bundle",
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringSliceFlag{
Name: "path", Name: "path",
Usage: "use `PATH` as app bundle (zipped bundle or directory)", Usage: "use `PATH` as app bundle (zipped bundle or directory)",
Aliases: []string{"p"}, Aliases: []string{"p"},
Value: ".", Value: cli.NewStringSlice("."),
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "address", Name: "address",
Usage: "use `ADDRESS` as http server listening address", Usage: "use `ADDRESS` as http server base listening address",
Aliases: []string{"a"}, Aliases: []string{"a"},
Value: ":8080", Value: ":8080",
}, },
@ -71,9 +78,22 @@ func RunCommand() *cli.Command {
Value: 0, Value: 0,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "storage-file", Name: "blobstore-dsn",
Usage: "use `FILE` for SQLite storage database", Usage: "use `DSN` for blob storage",
Value: ".edge/%APPID%/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000", EnvVars: []string{"EDGE_BLOBSTORE_DSN"},
Value: "sqlite://.edge/%APPID%/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
},
&cli.StringFlag{
Name: "documentstore-dsn",
Usage: "use `DSN` for document storage",
EnvVars: []string{"EDGE_DOCUMENTSTORE_DSN"},
Value: "sqlite://.edge/%APPID%/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
},
&cli.StringFlag{
Name: "sharestore-dsn",
Usage: "use `DSN` for share storage",
EnvVars: []string{"EDGE_SHARESTORE_DSN"},
Value: "sqlite://.edge/share.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "accounts-file", Name: "accounts-file",
@ -83,149 +103,184 @@ func RunCommand() *cli.Command {
}, },
Action: func(ctx *cli.Context) error { Action: func(ctx *cli.Context) error {
address := ctx.String("address") address := ctx.String("address")
path := ctx.String("path") paths := ctx.StringSlice("path")
logFormat := ctx.String("log-format") logFormat := ctx.String("log-format")
logLevel := ctx.Int("log-level") logLevel := ctx.Int("log-level")
blobstoreDSN := ctx.String("blobstore-dsn")
documentstoreDSN := ctx.String("documentstore-dsn")
shareStoreDSN := ctx.String("sharestore-dsn")
accountsFile := ctx.String("accounts-file")
logger.SetFormat(logger.Format(logFormat)) logger.SetFormat(logger.Format(logFormat))
logger.SetLevel(logger.Level(logLevel)) logger.SetLevel(logger.Level(logLevel))
cmdCtx := ctx.Context cmdCtx := ctx.Context
absPath, err := filepath.Abs(path) host, portStr, err := net.SplitHostPort(address)
if err != nil {
return errors.Wrapf(err, "could not resolve path '%s'", path)
}
logger.Info(cmdCtx, "opening app bundle", logger.F("path", absPath))
bundle, err := bundle.FromPath(path)
if err != nil {
return errors.Wrapf(err, "could not open path '%s' as an app bundle", path)
}
manifest, err := app.LoadManifest(bundle)
if err != nil {
return errors.Wrap(err, "could not load manifest from app bundle")
}
if valid, err := manifest.Validate(manifestMetadataValidators...); !valid {
return errors.Wrap(err, "invalid app manifest")
}
storageFile := injectAppID(ctx.String("storage-file"), manifest.ID)
if err := ensureDir(storageFile); err != nil {
return errors.WithStack(err)
}
db, err := sqlite.Open(storageFile)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
ds := sqlite.NewDocumentStoreWithDB(db) port, err := strconv.ParseUint(portStr, 10, 32)
bs := sqlite.NewBlobStoreWithDB(db)
bus := memory.NewBus()
handler := appHTTP.NewHandler(
appHTTP.WithBus(bus),
appHTTP.WithServerModules(getServerModules(bus, ds, bs, manifest, address)...),
)
if err := handler.Load(bundle); err != nil {
return errors.Wrap(err, "could not load app bundle")
}
router := chi.NewRouter()
router.Use(middleware.Logger)
accountsFile := injectAppID(ctx.String("accounts-file"), manifest.ID)
accounts, err := loadLocalAccounts(accountsFile)
if err != nil {
return errors.Wrap(err, "could not load local accounts")
}
// Add auth handler
key, err := dummyKey()
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
router.Handle("/auth/*", authHTTP.NewLocalHandler(
jwa.HS256, key,
authHTTP.WithRoutePrefix("/auth"),
authHTTP.WithAccounts(accounts...),
))
// Add app handler manifests := make([]*app.Manifest, len(paths))
router.Handle("/*", handler) for idx, pth := range paths {
bdl, err := bundle.FromPath(pth)
if err != nil {
return errors.WithStack(err)
}
logger.Info(cmdCtx, "listening", logger.F("address", address)) manifest, err := app.LoadManifest(bdl)
if err != nil {
return errors.WithStack(err)
}
if err := http.ListenAndServe(address, router); err != nil { manifests[idx] = manifest
return errors.WithStack(err)
} }
var wg sync.WaitGroup
for idx, p := range paths {
wg.Add(1)
go func(path string, basePort uint64, appIndex int) {
defer wg.Done()
port := basePort + uint64(appIndex)
address := fmt.Sprintf("%s:%d", host, port)
appsRepository := newAppRepository(host, basePort, manifests...)
appCtx := logger.With(cmdCtx, logger.F("address", address))
if err := runApp(appCtx, path, address, documentstoreDSN, blobstoreDSN, shareStoreDSN, accountsFile, appsRepository); err != nil {
logger.Error(appCtx, "could not run app", logger.E(errors.WithStack(err)))
}
}(p, port, idx)
}
wg.Wait()
return nil return nil
}, },
} }
} }
func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStore, manifest *app.Manifest, address string) []app.ServerModuleFactory { func runApp(ctx context.Context, path, address, documentStoreDSN, blobStoreDSN, shareStoreDSN, accountsFile string, appRepository appModule.Repository) error {
absPath, err := filepath.Abs(path)
if err != nil {
return errors.Wrapf(err, "could not resolve path '%s'", path)
}
logger.Info(ctx, "opening app bundle", logger.F("path", absPath))
bundle, err := bundle.FromPath(path)
if err != nil {
return errors.Wrapf(err, "could not open path '%s' as an app bundle", path)
}
manifest, err := app.LoadManifest(bundle)
if err != nil {
return errors.Wrap(err, "could not load manifest from app bundle")
}
if valid, err := manifest.Validate(manifestMetadataValidators...); !valid {
return errors.Wrap(err, "invalid app manifest")
}
ctx = logger.With(ctx, logger.F("appID", manifest.ID))
// Add auth handler
key, err := dummyKey()
if err != nil {
return errors.WithStack(err)
}
deps := &moduleDeps{}
funcs := []ModuleDepFunc{
initMemoryBus,
initDatastores(documentStoreDSN, blobStoreDSN, shareStoreDSN, manifest.ID),
initAccounts(accountsFile, manifest.ID),
initAppRepository(appRepository),
}
for _, fn := range funcs {
if err := fn(deps); err != nil {
return errors.WithStack(err)
}
}
handler := appHTTP.NewHandler(
appHTTP.WithBus(deps.Bus),
appHTTP.WithServerModules(getServerModules(deps)...),
appHTTP.WithHTTPMounts(
appModule.Mount(appRepository),
authModule.Mount(
authHTTP.NewLocalHandler(
jwa.HS256, key,
authHTTP.WithRoutePrefix("/auth"),
authHTTP.WithAccounts(deps.Accounts...),
),
authModule.WithJWT(dummyKeySet),
),
),
appHTTP.WithHTTPMiddlewares(
authModuleMiddleware.AnonymousUser(
jwa.HS256, key,
),
),
)
if err := handler.Load(bundle); err != nil {
return errors.Wrap(err, "could not load app bundle")
}
router := chi.NewRouter()
router.Use(middleware.Logger)
router.Use(middleware.Compress(5))
// Add app handler
router.Handle("/*", handler)
logger.Info(ctx, "listening", logger.F("address", address))
if err := http.ListenAndServe(address, router); err != nil {
return errors.WithStack(err)
}
return nil
}
type moduleDeps struct {
AppID app.ID
Bus bus.Bus
DocumentStore storage.DocumentStore
BlobStore storage.BlobStore
AppRepository appModule.Repository
ShareStore share.Store
Accounts []authHTTP.LocalAccount
}
type ModuleDepFunc func(*moduleDeps) error
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(bus), module.RPCModuleFactory(deps.Bus),
module.RPCModuleFactory(bus), module.StoreModuleFactory(deps.DocumentStore),
module.StoreModuleFactory(ds), blob.ModuleFactory(deps.Bus, deps.BlobStore),
blob.ModuleFactory(bus, bs), authModule.ModuleFactory(
module.Extends( authModule.WithJWT(dummyKeySet),
auth.ModuleFactory(
auth.WithJWT(dummyKeySet),
),
func(o *goja.Object) {
if err := o.Set("CLAIM_TENANT", "arcad_tenant"); err != nil {
panic(errors.New("could not set 'CLAIM_TENANT' property"))
}
if err := o.Set("CLAIM_ENTRYPOINT", "arcad_entrypoint"); err != nil {
panic(errors.New("could not set 'CLAIM_ENTRYPOINT' property"))
}
if err := o.Set("CLAIM_ROLE", "arcad_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"))
}
},
), ),
appModule.ModuleFactory(appModuleMemory.NewRepository( appModule.ModuleFactory(deps.AppRepository),
func(ctx context.Context, id app.ID, from string) (string, error) { fetch.ModuleFactory(deps.Bus),
addr := address shareModule.ModuleFactory(deps.AppID, deps.ShareStore),
if strings.HasPrefix(addr, ":") {
addr = "0.0.0.0" + addr
}
host, port, err := net.SplitHostPort(addr)
if err != nil {
return "", errors.WithStack(err)
}
addr, err = findMatchingDeviceAddress(ctx, from, host)
if err != nil {
return "", errors.WithStack(err)
}
return fmt.Sprintf("http://%s:%s", addr, port), nil
},
manifest,
)),
fetch.ModuleFactory(bus),
} }
} }
@ -279,10 +334,10 @@ func loadLocalAccounts(path string) ([]authHTTP.LocalAccount, error) {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
data, err := ioutil.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
if err := ioutil.WriteFile(path, defaultAccounts, 0o640); err != nil { if err := os.WriteFile(path, defaultAccounts, 0o640); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
@ -343,9 +398,98 @@ func findMatchingDeviceAddress(ctx context.Context, from string, defaultAddr str
continue continue
} }
return ip.String(), nil if ip.To4() == nil {
continue
}
return ip.To4().String(), nil
} }
} }
return defaultAddr, nil return defaultAddr, nil
} }
func newAppRepository(host string, basePort uint64, manifests ...*app.Manifest) *appModuleMemory.Repository {
if host == "" {
host = "127.0.0.1"
}
return appModuleMemory.NewRepository(
func(ctx context.Context, id app.ID, from string) (string, error) {
appIndex := 0
for i := 0; i < len(manifests); i++ {
if manifests[i].ID == id {
appIndex = i
break
}
}
addr, err := findMatchingDeviceAddress(ctx, from, host)
if err != nil {
return "", errors.WithStack(err)
}
return fmt.Sprintf("http://%s:%d", addr, int(basePort)+appIndex), nil
},
manifests...,
)
}
func initAppRepository(repo appModule.Repository) ModuleDepFunc {
return func(deps *moduleDeps) error {
deps.AppRepository = repo
return nil
}
}
func initMemoryBus(deps *moduleDeps) error {
deps.Bus = memory.NewBus()
return nil
}
func initDatastores(documentStoreDSN, blobStoreDSN, shareStoreDSN string, appID app.ID) ModuleDepFunc {
return func(deps *moduleDeps) error {
documentStoreDSN = injectAppID(documentStoreDSN, appID)
documentStore, err := driver.NewDocumentStore(documentStoreDSN)
if err != nil {
return errors.WithStack(err)
}
deps.DocumentStore = documentStore
blobStoreDSN = injectAppID(blobStoreDSN, appID)
blobStore, err := driver.NewBlobStore(blobStoreDSN)
if err != nil {
return errors.WithStack(err)
}
deps.BlobStore = blobStore
shareStore, err := driver.NewShareStore(shareStoreDSN)
if err != nil {
return errors.WithStack(err)
}
deps.ShareStore = shareStore
return nil
}
}
func initAccounts(accountsFile string, appID app.ID) ModuleDepFunc {
return func(deps *moduleDeps) error {
accountsFile = injectAppID(accountsFile, appID)
accounts, err := loadLocalAccounts(accountsFile)
if err != nil {
return errors.Wrap(err, "could not load local accounts")
}
deps.Accounts = accounts
return nil
}
}

View File

@ -5,7 +5,7 @@ import (
"log" "log"
"time" "time"
"github.com/barnybug/go-cast/discovery" "forge.cadoles.com/arcad/edge/pkg/module/cast"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -14,21 +14,26 @@ func ScanCommand() *cli.Command {
return &cli.Command{ return &cli.Command{
Name: "scan", Name: "scan",
Usage: "Scan network for casting devices", Usage: "Scan network for casting devices",
Flags: []cli.Flag{}, Flags: []cli.Flag{
&cli.DurationFlag{
Name: "timeout",
Aliases: []string{"t"},
Value: 30 * time.Second,
},
},
Action: func(ctx *cli.Context) error { Action: func(ctx *cli.Context) error {
service := discovery.NewService(ctx.Context) timeout := ctx.Duration("timeout")
defer service.Stop()
go func() { searchCtx, cancel := context.WithTimeout(ctx.Context, timeout)
if err := service.Run(ctx.Context, time.Second); err != nil && !errors.Is(err, context.DeadlineExceeded) { defer cancel()
log.Fatalf("%+v", errors.WithStack(err))
}
}()
found := service.Found() devices, err := cast.SearchDevices(searchCtx)
if err != nil {
log.Fatalf("%+v", errors.WithStack(err))
}
for device := range found { for dev := range devices {
log.Printf("[DEVICE] %s %s %s:%d", device.Uuid(), device.Name(), device.IP().String(), device.Port()) log.Printf("[DEVICE] %s %s %s:%d", dev.UUID, dev.Name, dev.Host.String(), dev.Port)
} }
return nil return nil

View File

@ -0,0 +1,13 @@
package auth
import (
"github.com/urfave/cli/v2"
)
func Root() *cli.Command {
return &cli.Command{
Name: "auth",
Usage: "Auth related command",
Subcommands: []*cli.Command{},
}
}

View File

@ -0,0 +1,48 @@
package command
import (
"context"
"fmt"
"os"
"sort"
"github.com/urfave/cli/v2"
)
func Main(commands ...*cli.Command) {
ctx := context.Background()
app := &cli.App{
Name: "storage-server",
Usage: "Edge storage server",
Commands: commands,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "debug",
EnvVars: []string{"DEBUG"},
Value: false,
},
},
}
app.ExitErrHandler = func(ctx *cli.Context, err error) {
if err == nil {
return
}
debug := ctx.Bool("debug")
if !debug {
fmt.Printf("[ERROR] %v\n", err)
} else {
fmt.Printf("%+v", err)
}
}
sort.Sort(cli.FlagsByName(app.Flags))
sort.Sort(cli.CommandsByName(app.Commands))
if err := app.RunContext(ctx, os.Args); err != nil {
os.Exit(1)
}
}

View File

@ -0,0 +1,179 @@
package command
import (
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/hashicorp/golang-lru/v2/expirable"
"github.com/keegancsmith/rpc"
"gitlab.com/wpetit/goweb/logger"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
// Register storage drivers
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
)
func Run() *cli.Command {
return &cli.Command{
Name: "run",
Usage: "Run server",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "address",
Aliases: []string{"addr"},
Value: ":3001",
},
&cli.StringFlag{
Name: "blobstore-dsn-pattern",
EnvVars: []string{"STORAGE_SERVER_BLOBSTORE_DSN_PATTERN"},
Value: fmt.Sprintf("sqlite://data/%%TENANT%%/%%APPID%%/blobstore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", (60 * time.Second).Milliseconds()),
},
&cli.StringFlag{
Name: "documentstore-dsn-pattern",
EnvVars: []string{"STORAGE_SERVER_DOCUMENTSTORE_DSN_PATTERN"},
Value: fmt.Sprintf("sqlite://data/%%TENANT%%/%%APPID%%/documentstore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", (60 * time.Second).Milliseconds()),
},
&cli.StringFlag{
Name: "sharestore-dsn-pattern",
EnvVars: []string{"STORAGE_SERVER_SHARESTORE_DSN_PATTERN"},
Value: fmt.Sprintf("sqlite://data/%%TENANT%%/sharestore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", (60 * time.Second).Milliseconds()),
},
&cli.DurationFlag{
Name: "cache-ttl",
EnvVars: []string{"STORAGE_SERVER_CACHE_TTL"},
Value: time.Hour,
},
&cli.IntFlag{
Name: "cache-size",
EnvVars: []string{"STORAGE_SERVER_CACHE_SIZE"},
Value: 32,
},
},
Action: func(ctx *cli.Context) error {
addr := ctx.String("address")
blobStoreDSNPattern := ctx.String("blobstore-dsn-pattern")
documentStoreDSNPattern := ctx.String("documentstore-dsn-pattern")
shareStoreDSNPattern := ctx.String("sharestore-dsn-pattern")
cacheSize := ctx.Int("cache-size")
cacheTTL := ctx.Duration("cache-ttl")
router := chi.NewRouter()
getBlobStoreServer := createGetCachedStoreServer(
func(dsn string) (storage.BlobStore, error) {
return driver.NewBlobStore(dsn)
},
func(store storage.BlobStore) *rpc.Server {
return server.NewBlobStoreServer(store)
},
)
getShareStoreServer := createGetCachedStoreServer(
func(dsn string) (share.Store, error) {
return driver.NewShareStore(dsn)
},
func(store share.Store) *rpc.Server {
return server.NewShareStoreServer(store)
},
)
getDocumentStoreServer := createGetCachedStoreServer(
func(dsn string) (storage.DocumentStore, error) {
return driver.NewDocumentStore(dsn)
},
func(store storage.DocumentStore) *rpc.Server {
return server.NewDocumentStoreServer(store)
},
)
router.Use(middleware.RealIP)
router.Use(middleware.Logger)
router.Handle("/blobstore", createStoreHandler(getBlobStoreServer, blobStoreDSNPattern, cacheSize, cacheTTL))
router.Handle("/documentstore", createStoreHandler(getDocumentStoreServer, documentStoreDSNPattern, cacheSize, cacheTTL))
router.Handle("/sharestore", createStoreHandler(getShareStoreServer, shareStoreDSNPattern, cacheSize, cacheTTL))
if err := http.ListenAndServe(addr, router); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}
type getRPCServerFunc func(cacheSize int, cacheTTL time.Duration, tenant, appID, dsnPattern string) (*rpc.Server, error)
func createGetCachedStoreServer[T any](storeFactory func(dsn string) (T, error), serverFactory func(store T) *rpc.Server) getRPCServerFunc {
var (
cache *expirable.LRU[string, *rpc.Server]
initCache sync.Once
)
return func(cacheSize int, cacheTTL time.Duration, tenant, appID, dsnPattern string) (*rpc.Server, error) {
initCache.Do(func() {
cache = expirable.NewLRU[string, *rpc.Server](cacheSize, nil, cacheTTL)
})
key := fmt.Sprintf("%s:%s", tenant, appID)
storeServer, _ := cache.Get(key)
if storeServer != nil {
return storeServer, nil
}
dsn := strings.ReplaceAll(dsnPattern, "%TENANT%", tenant)
dsn = strings.ReplaceAll(dsn, "%APPID%", appID)
store, err := storeFactory(dsn)
if err != nil {
return nil, errors.WithStack(err)
}
storeServer = serverFactory(store)
cache.Add(key, storeServer)
return storeServer, nil
}
}
func createStoreHandler(getStoreServer getRPCServerFunc, dsnPattern string, cacheSize int, cacheTTL time.Duration) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenant := r.URL.Query().Get("tenant")
if tenant == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
appID := r.URL.Query().Get("appId")
if tenant == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
server, err := getStoreServer(cacheSize, cacheTTL, tenant, appID, dsnPattern)
if err != nil {
logger.Error(r.Context(), "could not retrieve store server", logger.E(errors.WithStack(err)), logger.F("tenant", tenant))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
server.ServeHTTP(w, r)
})
}

View File

@ -0,0 +1,13 @@
package main
import (
"forge.cadoles.com/arcad/edge/cmd/storage-server/command"
"forge.cadoles.com/arcad/edge/cmd/storage-server/command/auth"
)
func main() {
command.Main(
command.Run(),
auth.Root(),
)
}

View File

@ -10,5 +10,6 @@ Afin de pouvoir utiliser le SDK "client", vous devez inclure dans la page HTML d
Vous pourrez ensuite accéder aux variables globales suivantes: Vous pourrez ensuite accéder aux variables globales suivantes:
- [`Edge`](./edge.md) - Client principal d'échange avec le serveur - [`Edge.Client`](./edge-client.md) - Client principal d'échange avec le serveur
- [`EdgeFrame`](./edge-frame.md) - [`Edge.Frame`](./edge-frame.md) - Utilitaire de communication avec une frame parente
- [`Edge.Menu`](./edge-menu.md) - Gestionnaire de menu

View File

@ -1,22 +1,22 @@
# `Edge` # `Edge.Client`
## Méthodes ## Méthodes
### `Edge.connect(): Promise` ### `Edge.Client.connect(): Promise`
> `TODO` > `TODO`
### `Edge.disconnect(): void` ### `Edge.Client.disconnect(): void`
> `TODO` > `TODO`
### `Edge.send(message: Object): void` ### `Edge.Client.send(message: Object): void`
> `TODO` > `TODO`
### `Edge.rpc(method: string, params: Object): Promise` ### `Edge.Client.rpc(method: string, params: Object): Promise`
> `TODO` > `TODO`
#### Exemple #### Exemple
@ -36,22 +36,22 @@ function echo(ctx, params) {
**Côté client** **Côté client**
```js ```js
Edge.connect().then(() => { Edge.Client.connect().then(() => {
Edge.rpc("echo", { hello: "world!" }) Edge.Client.rpc("echo", { hello: "world!" })
.then(result => console.log(result)) .then(result => console.log(result))
.catch(err => console.error(err)); .catch(err => console.error(err));
}); });
``` ```
### `Edge.upload(blob: Blob, metadata: Object): Promise` ### `Edge.Client.upload(blob: Blob, metadata: Object): Promise`
> `TODO` > `TODO`
### `Edge.blobUrl(bucketName: string, blobId: string): string` ### `Edge.Client.blobUrl(bucketName: string, blobId: string): string`
> `TODO` > `TODO`
### `Edge.externalUrl(url: string): string` ### `Edge.Client.externalUrl(url: string): string`
Retourne une URL "locale" permettant d'accéder à une ressource externe, en fonction de règles propres à l'application. Voir module [`fetch`](../server-api/fetch.md). Retourne une URL "locale" permettant d'accéder à une ressource externe, en fonction de règles propres à l'application. Voir module [`fetch`](../server-api/fetch.md).
@ -64,5 +64,5 @@ Retourne une URL "locale" permettant d'accéder à une ressource externe, en fon
#### Exemple #### Exemple
```js ```js
Edge.addEventListener("message", evt => console.log(evt.detail)); Edge.Client.addEventListener("message", evt => console.log(evt.detail));
``` ```

View File

@ -1,8 +1,8 @@
# `EdgeFrame` # `Edge.Frame`
## Méthodes ## Méthodes
### `EdgeFrame.addEventListener(name: string, listener: (event) => void)` ### `Edge.Frame.addEventListener(name: string, listener: (event) => void)`
> `TODO` > `TODO`

View File

@ -0,0 +1,27 @@
# `Edge.Menu`
## Méthodes
### `Edge.Menu.show()`
Afficher le menu.
### `Edge.Menu.hide()`
Cacher le menu.
### `Edge.Menu.setItem(name: string, label:string, options?: { iconUrl?: string, linkUrl?: string, order?: number })`
Créer/mettre à jour l'item nommé de la section du menu associée à l'application.
### `Edge.Menu.removeItem(name: string)`
Supprimer l'item de la section du menu associée à l'application.
### `Edge.Menu.setAppIconUrl(url: string)`
Mettre à jour l'URL de l'icône de la section du menu associée à l'application.
### `Edge.Menu.setAppTitle(title: string)`
Mettre à jour le titre de la section du menu associée à l'application.

View File

@ -40,13 +40,13 @@ Créer le fichier `my-app/public/index.html`:
<script type="text/javascript"> <script type="text/javascript">
// On utilise le SDK via la variable globale "Edge" // On utilise le SDK via la variable globale "Edge"
// pour se connecter au serveur de notre application. // pour se connecter au serveur de notre application.
Edge.connect().then(() => { Edge.Client.connect().then(() => {
// Une fois connecté, on envoie un message au serveur. // Une fois connecté, on envoie un message au serveur.
Edge.send({ "hello": "world" }); Edge.Client.send({ "hello": "world" });
}); });
// On écoute les messages en provenance du serveur. // On écoute les messages en provenance du serveur.
Edge.addEventListener("message", (evt) => { Edge.Client.addEventListener("message", (evt) => {
console.log("New server message", evt.detail) console.log("New server message", evt.detail)
}); });
</script> </script>

View File

@ -29,4 +29,5 @@ Listes des modules disponibles côté serveur.
- [`fetch`](./fetch.md) - [`fetch`](./fetch.md)
- [`net`](./net.md) - [`net`](./net.md)
- [`rpc`](./rpc.md) - [`rpc`](./rpc.md)
- [`share`](./share.md)
- [`store`](./store.md) - [`store`](./store.md)

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

@ -86,4 +86,4 @@ interface BlobInfo {
### `Metadata` ### `Metadata`
L'objet `Metadata` est un objet clé/valeur arbitraire transmis avec la requête de téléversement. Voir la méthode [`Edge.upload(blob, metadata)`](../client-api/README.md#edge-upload-blob-blob-metadata-object-promise) du SDK client. L'objet `Metadata` est un objet clé/valeur arbitraire transmis avec la requête de téléversement. Voir la méthode [`Edge.Client.upload(blob, metadata)`](../client-api/README.md#edge-upload-blob-blob-metadata-object-promise) du SDK client.

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

@ -13,7 +13,7 @@ Pour permettre aux utilisateurs d'accéder à des ressources distantes, vous dev
**Côté client** **Côté client**
```js ```js
// Création d'une URL "locale" permettant d'accéder à la ressource distante // Création d'une URL "locale" permettant d'accéder à la ressource distante
var url = Edge.externalUrl("http://example.com") var url = Edge.Client.externalUrl("http://example.com")
// Vous pouvez utiliser l'URL comme attribut `src` d'une balise <img> par exemple // Vous pouvez utiliser l'URL comme attribut `src` d'une balise <img> par exemple
// ou effectuer une requête fetch() avec celle ci. // ou effectuer une requête fetch() avec celle ci.

View File

@ -32,9 +32,9 @@ Aucune
```js ```js
// Les données envoyées par le serveur sont accessibles // Les données envoyées par le serveur sont accessibles
// via la propriété evt.detail. // via la propriété evt.detail.
Edge.on('message', evt => console.log(evt.detail)); Edge.Client.on('message', evt => console.log(evt.detail));
Edge.connect(); Edge.Client.connect();
``` ```
**Côté serveur** **Côté serveur**

View File

@ -1,6 +1,6 @@
# Module `rpc` # Module `rpc`
Ce module permet de déclarer des méthodes côté serveur qui seront "invoquable" côté client via la méthode [`Edge.rpc(method: string, params: Object): Promise`](../client-api/README.md#edgerpcmethod-string-params-object-promise). Ce module permet de déclarer des méthodes côté serveur qui seront "invoquable" côté client via la méthode [`Edge.Client.rpc(method: string, params: Object): Promise`](../client-api/README.md#edgerpcmethod-string-params-object-promise).
## Méthodes ## Méthodes
@ -31,8 +31,8 @@ function echo(ctx, params) {
**Côté client** **Côté client**
```js ```js
Edge.connect().then(() => { Edge.Client.connect().then(() => {
Edge.rpc("echo", { hello: "world!" }) Edge.Client.rpc("echo", { hello: "world!" })
.then(result => console.log(result)) .then(result => console.log(result))
.catch(err => console.error(err)); .catch(err => console.error(err));
}); });

View File

@ -0,0 +1,143 @@
# Module `share`
Ce module permet partager des ressources à destination des autres applications exécutées dans l'environnement Edge.
## Propriétés
### `share.ANY_TYPE`, `share.ANY_NAME`
Les propriétés `share.ANY_TYPE` et `share.ANY_NAME` sont utilisées dans la méthode `share.findResources()` pour récupérer ne pas appliquer de filtre spécifique au type ou au nom des attributs respectivement.
### `share.TYPE_TEXT`, `share.TYPE_NUMBER`, `share.TYPE_PATH`, `share.TYPE_BOOL`
Ces propriétés correspondant aux types d'attributs possibles dans une ressource.
Le type `share.TYPE_PATH` décrit un "chemin" destiné à être transformé en URL par l'application consommatrice de la ressource sous la forme `${APP_URL}${PATH}`. Ce type d'attribut peut être utilisé pour partager des URLs (médias, pages, etc) entre applications.
## Méthodes
### `share.upsertResource(ctx: Context, resourceId: string, ...attributes: Attribute[]): Resource`
Cette méthode permet de créer une ressource ou de la mettre à jour si elle existe déjà. Elle prend en paramètre le contexte d'exécution, l'identifiant de la ressource et une liste d'attributs.
Si la ressource n'existe pas, elle sera créée avec les attributs fournis. Si elle existe, les attributs existants seront mis à jour avec les valeurs fournies.
#### Arguments
- `ctx: Context`: Le contexte d'exécution.
- `resourceId: string`: L'identifiant de la ressource.
- `...attributes: Attribute[]`: Une liste d'attributs. Chaque attribut est représenté par un objet de type `Attribute`.
#### Valeur de retour
La méthode retourne un objet de type `Resource` qui représente la ressource créée ou mise à jour.
#### Usage
```javascript
const resource = share.upsertResource(ctx, "my-resource", { name: "color", type: share.TYPE_TEXT, value: "red" });
console.log(resource);
// Output: { id: "my-resource", origin: "my.app", attributes: [{ name: "color", type: "text", value: "red", createdAt: "2023-04-21T14:30:00Z", updatedAt: "2023-04-21T14:30:00Z" }] }
```
### `share.findResources(ctx: Context, withAttribute?: string, withType?: string): []Resource`
Cette méthode permet de rechercher des ressources en fonction de leurs attributs. Elle prend en paramètre le contexte d'exécution et deux paramètres optionnels qui permettent de filtrer les ressources.
#### Arguments
- `ctx: Context`: Le contexte d'exécution.
- `withAttribute?: string`: (optionnel) Le nom de l'attribut à rechercher (`share.ANY_NAME` par défaut)
- `withType?: string`: (optionnel) Le type de l'attribut à rechercher (`share.ANY_TYPE` par défaut)
#### Valeur de retour
La méthode retourne un tableau d'objets de type `Resource` qui représentent les ressources correspondant aux critères de recherche.
#### Usage
```typescript
const resources = share.findResources(ctx, "color", share.TYPE_TEXT);
console.log(resources);
// Output: [{ id: "my-resource", origin: "my/app", attributes: [{ name: "color", type: "text", value: "red", createdAt: "2023-04-21T14:30:00Z", updatedAt: "2023-04-21T14:30:00Z" }] }]
```
### `share.deleteAttributes(ctx: Context, resourceId: string, ...names: string[]): Resource`
Cette méthode supprime un ou plusieurs attributs de la ressource spécifiée.
#### Arguments
- `ctx: Context`: contexte d'exécution
- `resourceId: string`: identifiant unique de la ressource à modifier
- `...names: string[]`: tableau de noms d'attributs à supprimer
#### Valeur de retour
La méthode retourne un objet de type `Resource` qui représente la ressource modifiée.
#### Usage
```typescript
const resource = share.upsertResource(ctx, "my-resource", { name: "color", type: share.TYPE_TEXT, value: "red" });
console.log(resource);
// Output: { id: "my-resource", origin: "my.app", attributes: [{ name: "color", type: "text", value: "red", createdAt: "2023-04-21T14:30:00Z", updatedAt: "2023-04-21T14:30:00Z" }] }
```
### `share.deleteResource(ctx: Context, resourceId: string)`
Cette méthode supprime la ressource spécifiée.
#### Arguments
- `ctx: Context`: contexte d'exécution
- `resourceId: string`: identifiant unique de la ressource à supprimer
#### Valeur de retour
La méthode ne retourne pas de valeur.
#### Usage
```typescript
const resource = share.deleteResource(ctx, "my-resource");
```
## Objets
### `Context`
Voir la documentation du module [`context`](./context.md)
### `Resource`
```typescript
interface Resource {
id: string
origin: string
attributes: Attribute[]
}
```
### `Attribute`
```typescript
interface Attribute {
name: string
type: ValueType
createdAt: string
updatedAt: string
}
```
### `ValueType`
```typescript
enum ValueType {
TYPE_TEXT = "text",
TYPE_PATH = "path",
TYPE_NUMBER = "number",
TYPE_BOOL = "bool"
}
```

37
go.mod
View File

@ -3,33 +3,39 @@ module forge.cadoles.com/arcad/edge
go 1.19 go 1.19
require ( require (
github.com/hashicorp/mdns v1.0.5
github.com/lestrrat-go/jwx/v2 v2.0.8 github.com/lestrrat-go/jwx/v2 v2.0.8
modernc.org/sqlite v1.20.4 modernc.org/sqlite v1.20.4
) )
require ( require (
github.com/brutella/dnssd v1.2.6 // indirect cloud.google.com/go v0.75.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/go-playground/locales v0.12.1 // indirect
github.com/go-playground/universal-translator v0.16.0 // indirect
github.com/goccy/go-json v0.9.11 // indirect github.com/goccy/go-json v0.9.11 // indirect
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e // indirect github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/hashicorp/golang-lru/v2 v2.0.6 // indirect
github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073 // indirect github.com/keegancsmith/rpc v1.3.0 // indirect
github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8 // indirect github.com/leodido/go-urn v1.1.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.0 // indirect github.com/lestrrat-go/option v1.0.0 // indirect
github.com/miekg/dns v1.1.50 // indirect github.com/miekg/dns v1.1.53 // indirect
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705 // indirect
google.golang.org/grpc v1.35.0 // indirect
gopkg.in/go-playground/validator.v9 v9.29.1 // indirect
) )
require ( require (
cdr.dev/slog v1.4.0 // indirect cdr.dev/slog v1.4.0
github.com/alecthomas/chroma v0.7.0 // indirect github.com/alecthomas/chroma v0.7.0 // indirect
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692 github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1
github.com/dlclark/regexp2 v1.7.0 // indirect github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/dop251/goja v0.0.0-20230203172422-5460598cfa32 github.com/dop251/goja v0.0.0-20230203172422-5460598cfa32
github.com/dop251/goja_nodejs v0.0.0-20230207183254-2229640ea097 github.com/dop251/goja_nodejs v0.0.0-20230207183254-2229640ea097
@ -51,18 +57,17 @@ require (
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/urfave/cli/v2 v2.24.3 github.com/urfave/cli/v2 v2.24.3
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
gitlab.com/wpetit/goweb v0.0.0-20230206085656-dec695f0e2e9 gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b
go.opencensus.io v0.22.5 // indirect go.opencensus.io v0.22.5 // indirect
golang.org/x/crypto v0.7.0 // indirect golang.org/x/crypto v0.7.0
golang.org/x/mod v0.8.0 // indirect golang.org/x/mod v0.10.0
golang.org/x/net v0.8.0 // indirect golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.6.0 // indirect golang.org/x/sys v0.7.0 // indirect
golang.org/x/term v0.6.0 // indirect golang.org/x/term v0.7.0 // indirect
golang.org/x/text v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.6.0 // indirect golang.org/x/tools v0.8.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
lukechampine.com/uint128 v1.2.0 // indirect lukechampine.com/uint128 v1.2.0 // indirect

124
go.sum
View File

@ -5,7 +5,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY= cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY=
@ -18,7 +17,7 @@ cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZ
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0 h1:XgtDnVJRCPEUG21gjFiRPz4zI1Mjg16R+NYQjfmU4XY=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
@ -37,25 +36,25 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw= github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw=
github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY= github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA= github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692 h1:JW4WZlqyaNWUUahfr7MigeDW6jmtam5cTzzo1lwsFhE= github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692 h1:JW4WZlqyaNWUUahfr7MigeDW6jmtam5cTzzo1lwsFhE=
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692/go.mod h1:Au0ipPuCBA7zsOC61SnyrYetm8VT3vo1UJtwHeYke44= github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692/go.mod h1:Au0ipPuCBA7zsOC61SnyrYetm8VT3vo1UJtwHeYke44=
github.com/brutella/dnssd v1.2.6 h1:/0P13JkHLRzeLQkWRPEn4hJCr4T3NfknIFw3aNPIC34=
github.com/brutella/dnssd v1.2.6/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
@ -78,7 +77,6 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
@ -109,7 +107,9 @@ github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITL
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
@ -117,8 +117,6 @@ github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e h1:eeyMpoxANuWNQ9O2auv4wXxJsrXzLUhdHaOmNWEGkRY= github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e h1:eeyMpoxANuWNQ9O2auv4wXxJsrXzLUhdHaOmNWEGkRY=
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -145,6 +143,7 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@ -158,6 +157,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -169,8 +169,8 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -178,7 +178,6 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@ -186,13 +185,15 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073 h1:9dodOMuH6u7LvPEkVydBv6KTHdm+SqsHOxHTzRW+1+w=
github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.6 h1:3xi/Cafd1NaoEnS/yDssIiuVeDVywU0QdFGl3aQaQHM=
github.com/hashicorp/golang-lru/v2 v2.0.6/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8 h1:yupxZNIxm5U8Tfb8g65irIuHkgF8c4koHC7daPSyMTE=
github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8/go.mod h1:aa76Av3qgPeIQp9Y3qIkTBPieQYNkQ13Kxe7pze9Wb0= github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8/go.mod h1:aa76Av3qgPeIQp9Y3qIkTBPieQYNkQ13Kxe7pze9Wb0=
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/igm/sockjs-go/v3 v3.0.2 h1:2m0k53w0DBiGozeQUIEPR6snZFmpFpYvVsGnfLPNXbE= github.com/igm/sockjs-go/v3 v3.0.2 h1:2m0k53w0DBiGozeQUIEPR6snZFmpFpYvVsGnfLPNXbE=
@ -202,14 +203,18 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/keegancsmith/rpc v1.3.0 h1:wGWOpjcNrZaY8GDYZJfvyxmlLljm3YQWF+p918DXtDk=
github.com/keegancsmith/rpc v1.3.0/go.mod h1:6O2xnOGjPyvIPbvp0MdrOe5r6cu1GZ4JoTzpzDhWeo0=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
@ -228,14 +233,14 @@ github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaa
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8 h1:ALvJ9V8nNf04PFHMR2sot56N/pjrx5LzZGvUlnhdiCE= github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
github.com/miekg/dns v1.1.53/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
@ -250,19 +255,19 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@ -272,9 +277,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli/v2 v2.24.3 h1:7Q1w8VN8yE0MJEHP06bv89PjYsN4IHWED2s1v/Zlfm0= github.com/urfave/cli/v2 v2.24.3 h1:7Q1w8VN8yE0MJEHP06bv89PjYsN4IHWED2s1v/Zlfm0=
@ -287,10 +292,9 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/wpetit/goweb v0.0.0-20230206085656-dec695f0e2e9 h1:6JlkcdjYVQglPWYuemK2MoZAtRE4vFx85zLXflGIyI8= gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b h1:nkvOl8TCj/mErADnwFFynjxBtC+hHsrESw6rw56JGmg=
gitlab.com/wpetit/goweb v0.0.0-20230206085656-dec695f0e2e9/go.mod h1:3sus4zjoUv1GB7eDLL60QaPkUnXJCWBpjvbe0jWifeY= gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b/go.mod h1:3sus4zjoUv1GB7eDLL60QaPkUnXJCWBpjvbe0jWifeY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@ -304,11 +308,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
@ -344,12 +344,9 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20161013035702-8b4af36cd21a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20161013035702-8b4af36cd21a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -379,18 +376,15 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -399,7 +393,6 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -409,9 +402,9 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -445,44 +438,32 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -529,15 +510,10 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -561,7 +537,6 @@ google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSr
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -601,9 +576,8 @@ google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705 h1:PYBmACG+YEv8uQPW0r1kJj8tR+gkF0UWq7iFdUezwEw=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@ -620,6 +594,7 @@ google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@ -630,17 +605,22 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@ -655,6 +635,8 @@ modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0= modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
@ -667,8 +649,10 @@ modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE=
modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A= modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@ -9,4 +9,4 @@ tags: ["test"]
metadata: metadata:
paths: paths:
icon: /icon.png icon: /icon.png
minimumRole: visitor minimumRole: superadmin

View File

@ -8,7 +8,10 @@
<link rel="stylesheet" href="/vendor/mocha.css" /> <link rel="stylesheet" href="/vendor/mocha.css" />
<style> <style>
body { body {
background-color: white; background-color: #f7f7f7;
}
body:not([edge-auto-padding="false"]) #mocha-stats {
top: 75px !important;
} }
</style> </style>
</head> </head>
@ -30,6 +33,17 @@
<script src="/test/fetch-module.js"></script> <script src="/test/fetch-module.js"></script>
<script class="mocha-exec"> <script class="mocha-exec">
mocha.run(); mocha.run();
Edge.Menu
.setAppIconUrl('/icon.png')
.setAppTitle('SDK Tests')
.setItem('client', 'Client', { linkUrl: '/?grep=Edge', order: 1 })
.setItem('auth-module', 'Auth Module', { linkUrl: '/?grep=Auth%20Module' , order: 4})
.setItem('net-module', 'Net Module', { linkUrl: '/?grep=Net%20Module' , order: 3})
.setItem('rpc', 'Remote Procedure Call', { linkUrl: '/?grep=Remote%20Procedure%20Call' , order: 5})
.setItem('file-module', 'File Module', { linkUrl: '/?grep=File%20Module', order: 6})
.setItem('app-module', 'App Module', { linkUrl: '/?grep=App%20Module' , order: 7})
.setItem('fetch-module', 'Fetch Module', { linkUrl: '/?grep=Fetch%20Module' , order: 8})
</script> </script>
</body> </body>
</html> </html>

View File

@ -1,15 +1,15 @@
describe('App Module', function() { describe('App Module', function() {
before(() => { before(() => {
return Edge.connect(); return Edge.Client.connect();
}); });
after(() => { after(() => {
Edge.disconnect(); Edge.Client.disconnect();
}); });
it('should list apps', function() { it('should list apps', function() {
return Edge.rpc("listApps") return Edge.Client.rpc("listApps")
.then(apps => { .then(apps => {
console.log("listApps result:", apps); console.log("listApps result:", apps);
chai.assert.isNotNull(apps); chai.assert.isNotNull(apps);
@ -18,7 +18,7 @@ describe('App Module', function() {
}); });
it('should retrieve requested app', function() { it('should retrieve requested app', function() {
return Edge.rpc("getApp", { appId: "edge.sdk.client.test" }) return Edge.Client.rpc("getApp", { appId: "edge.sdk.client.test" })
.then(app => { .then(app => {
console.log("getApp result:", app); console.log("getApp result:", app);
chai.assert.isNotNull(app); chai.assert.isNotNull(app);
@ -27,7 +27,7 @@ describe('App Module', function() {
}); });
it('should retrieve requested app url without from address', function() { it('should retrieve requested app url without from address', function() {
return Edge.rpc("getAppUrl", { appId: "edge.sdk.client.test" }) return Edge.Client.rpc("getAppUrl", { appId: "edge.sdk.client.test" })
.then(url => { .then(url => {
console.log("getAppUrl result:", url); console.log("getAppUrl result:", url);
chai.assert.isNotEmpty(url); chai.assert.isNotEmpty(url);
@ -35,7 +35,7 @@ describe('App Module', function() {
}); });
it('should retrieve requested app url with from address', function() { it('should retrieve requested app url with from address', function() {
return Edge.rpc("getAppUrl", { appId: "edge.sdk.client.test", from: "127.0.0.2" }) return Edge.Client.rpc("getAppUrl", { appId: "edge.sdk.client.test", from: "127.0.0.2" })
.then(url => { .then(url => {
console.log("getAppUrl result:", url); console.log("getAppUrl result:", url);
chai.assert.isNotEmpty(url); chai.assert.isNotEmpty(url);

View File

@ -1,15 +1,15 @@
describe('Auth Module', function() { describe('Auth Module', function() {
before(() => { before(() => {
return Edge.connect(); return Edge.Client.connect();
}); });
after(() => { after(() => {
Edge.disconnect(); Edge.Client.disconnect();
}); });
it('should retrieve user informations', function() { it('should retrieve user informations', function() {
return Edge.rpc("getUserInfo") return Edge.Client.rpc("getUserInfo")
.then(userInfo => { .then(userInfo => {
console.log("getUserInfo result:", userInfo); console.log("getUserInfo result:", userInfo);
chai.assert.property(userInfo, 'subject'); chai.assert.property(userInfo, 'subject');

View File

@ -1,25 +1,25 @@
Edge.debug = true; Edge.Client.debug = true;
EdgeFrame.debug = true; Edge.Frame.debug = true;
describe('Edge', function() { describe('Edge', function() {
describe('#connect()', function() { describe('#connect()', function() {
after(() => { after(() => {
Edge.disconnect(); Edge.Client.disconnect();
}); });
it('should open the connection', function() { it('should open the connection', function() {
return Edge.connect() return Edge.Client.connect()
.then(() => { .then(() => {
chai.assert.isNotNull(Edge._conn); chai.assert.isNotNull(Edge.Client._conn);
}); });
}); });
}); });
describe('#disconnect()', function() { describe('#disconnect()', function() {
it('should close the connection', function() { it('should close the connection', function() {
Edge.disconnect(); Edge.Client.disconnect();
chai.assert.isNull(Edge._conn); chai.assert.isNull(Edge.Client._conn);
}); });
}); });

View File

@ -1,15 +1,15 @@
describe('Fetch Module', function () { describe('Fetch Module', function () {
before(() => { before(() => {
return Edge.connect(); return Edge.Client.connect();
}); });
after(() => { after(() => {
Edge.disconnect(); Edge.Client.disconnect();
}); });
it('should fetch an authorized external url', function () { it('should fetch an authorized external url', function () {
var externalUrl = Edge.externalUrl("http://example.com"); var externalUrl = Edge.Client.externalUrl("http://example.com");
return fetch(externalUrl) return fetch(externalUrl)
.then(res => { .then(res => {
@ -22,7 +22,7 @@ describe('Fetch Module', function () {
}); });
it('should not fetch an unauthorized external url', function () { it('should not fetch an unauthorized external url', function () {
var externalUrl = Edge.externalUrl("https://google.com"); var externalUrl = Edge.Client.externalUrl("https://google.com");
return fetch(externalUrl) return fetch(externalUrl)
.then(res => { .then(res => {

View File

@ -1,25 +1,25 @@
describe('File Module', function () { describe('File Module', function () {
before(() => { before(() => {
return Edge.connect(); return Edge.Client.connect();
}); });
after(() => { after(() => {
Edge.disconnect(); Edge.Client.disconnect();
}); });
it('should upload then download a blob', function () { it('should upload then download a blob', function () {
const content = JSON.stringify({ "date": new Date() }); const content = JSON.stringify({ "date": new Date() });
const blob = new Blob([content], { type: "application/json" }); const blob = new Blob([content], { type: "application/json" });
return Edge.upload(blob) return Edge.Client.upload(blob)
.then(upload => upload.result()) .then(upload => upload.result())
.then(result => { .then(result => {
chai.assert.isNotEmpty(result.blobId); chai.assert.isNotEmpty(result.blobId);
chai.assert.isNotEmpty(result.bucket); chai.assert.isNotEmpty(result.bucket);
const blobUrl = Edge.blobUrl(result.bucket, result.blobId); const blobUrl = Edge.Client.blobUrl(result.bucket, result.blobId);
chai.assert.isNotEmpty(blobUrl); chai.assert.isNotEmpty(blobUrl);
return fetch(blobUrl) return fetch(blobUrl)

View File

@ -2,11 +2,11 @@ describe('Net Module', function () {
this.timeout(5000); this.timeout(5000);
before(() => { before(() => {
return Edge.connect(); return Edge.Client.connect();
}); });
after(() => { after(() => {
Edge.disconnect(); Edge.Client.disconnect();
}); });
it('should broadcast a message from server', function (done) { it('should broadcast a message from server', function (done) {
@ -18,12 +18,12 @@ describe('Net Module', function () {
chai.assert.deepEqual(message, evt.detail); chai.assert.deepEqual(message, evt.detail);
Edge.removeEventListener('message', handler); Edge.Client.removeEventListener('message', handler);
done(); done();
}; };
Edge.addEventListener("message", handler); Edge.Client.addEventListener("message", handler);
Edge.send(message); Edge.Client.send(message);
}); });
it('should send a message to the server and echo back', function(done) { it('should send a message to the server and echo back', function(done) {
@ -35,15 +35,15 @@ describe('Net Module', function () {
chai.assert.equal(receivedMessage.now, now.toJSON()); chai.assert.equal(receivedMessage.now, now.toJSON());
Edge.removeEventListener('message', handler); Edge.Client.removeEventListener('message', handler);
done(); done();
} }
// Server should echo back message // Server should echo back message
Edge.addEventListener('message', handler); Edge.Client.addEventListener('message', handler);
// Send message to server // Send message to server
Edge.send({ test: 'echo', now }); Edge.Client.send({ test: 'echo', now });
}); });
}); });

View File

@ -1,17 +1,17 @@
describe('Remote Procedure Call', function () { describe('Remote Procedure Call', function () {
before(() => { before(() => {
return Edge.connect(); return Edge.Client.connect();
}); });
after(() => { after(() => {
Edge.disconnect(); Edge.Client.disconnect();
}); });
it('should call the remote echo() method and resolve the returned value', function () { it('should call the remote echo() method and resolve the returned value', function () {
const foo = "bar"; const foo = "bar";
return Edge.rpc('echo', { foo }) return Edge.Client.rpc('echo', { foo })
.then(result => { .then(result => {
console.log(result); console.log(result);
chai.assert.equal(result.foo, foo); chai.assert.equal(result.foo, foo);
@ -19,7 +19,7 @@ describe('Remote Procedure Call', function () {
}); });
it('should call the remote throwErrorFromClient() method and reject with an error', function () { it('should call the remote throwErrorFromClient() method and reject with an error', function () {
return Edge.rpc('throwErrorFromClient') return Edge.Client.rpc('throwErrorFromClient')
.catch(err => { .catch(err => {
// Assert that it's an "internal" error // Assert that it's an "internal" error
// See https://www.jsonrpc.org/specification#error_object // See https://www.jsonrpc.org/specification#error_object
@ -28,7 +28,7 @@ describe('Remote Procedure Call', function () {
}); });
it('should call an unregistered method and reject with an error', function () { it('should call an unregistered method and reject with an error', function () {
return Edge.rpc('unregisteredMethod') return Edge.Client.rpc('unregisteredMethod')
.catch(err => { .catch(err => {
// Assert that it's an "method not found" error // Assert that it's an "method not found" error
// See https://www.jsonrpc.org/specification#error_object // See https://www.jsonrpc.org/specification#error_object
@ -38,17 +38,17 @@ describe('Remote Procedure Call', function () {
it('should call the add() method repetitively and keep count of the sent values', function () { it('should call the add() method repetitively and keep count of the sent values', function () {
this.timeout(10000); this.timeout(30000);
const values = []; const values = [];
for (let i = 0; i <= 1000; i++) { for (let i = 0; i <= 1000; i++) {
values.push((Math.random() * 1000 | 0)); values.push((Math.random() * 1000 | 0));
} }
return Edge.rpc('reset') return Edge.Client.rpc('reset')
.then(() => { .then(() => {
return Promise.all(values.map(v => Edge.rpc("add", { value: v }))); return Promise.all(values.map(v => Edge.Client.rpc("add", { value: v })));
}) })
.then(() => Edge.rpc('total')) .then(() => Edge.Client.rpc('total'))
.then(remoteTotal => { .then(remoteTotal => {
const localTotal = values.reduce((t, v) => t + v); const localTotal = values.reduce((t, v) => t + v);
console.log("Remote total:", remoteTotal, "Local total:", localTotal); console.log("Remote total:", remoteTotal, "Local total:", localTotal);

View File

@ -4,7 +4,7 @@ ARG HTTP_PROXY=
ARG HTTPS_PROXY= ARG HTTPS_PROXY=
ARG http_proxy= ARG http_proxy=
ARG https_proxy= ARG https_proxy=
ARG GO_VERSION=1.19.2 ARG GO_VERSION=1.20.2
# Install dev environment dependencies # Install dev environment dependencies
RUN export DEBIAN_FRONTEND=noninteractive &&\ RUN export DEBIAN_FRONTEND=noninteractive &&\

View File

@ -6,8 +6,12 @@ misc/client-sdk-testsuite/src/**/*
modd.conf modd.conf
{ {
prep: make build-sdk prep: make build-sdk
prep: cd misc/client-sdk-testsuite && make dist prep: make build-client-sdk-test-app
prep: make build prep: make build
prep: make GOTEST_ARGS="-short" test daemon: make run-app
daemon: bin/cli app run -p misc/client-sdk-testsuite/dist daemon: make run-storage-server
}
**/*.go {
prep: make GOTEST_ARGS="-short" test
} }

120
package-lock.json generated
View File

@ -10,14 +10,50 @@
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@webcomponents/webcomponentsjs": "^2.8.0",
"core-js": "^3.30.1",
"lit": "^2.7.2",
"sockjs-client": "^1.6.1" "sockjs-client": "^1.6.1"
} }
}, },
"node_modules/@lit-labs/ssr-dom-shim": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.0.tgz",
"integrity": "sha512-92uQ5ARf7UXYrzaFcAX3T2rTvaS9Z1//ukV+DqjACM4c8s0ZBQd7ayJU5Dh2AFLD/Ayuyz4uMmxQec8q3U4Ong=="
},
"node_modules/@lit/reactive-element": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.1.tgz",
"integrity": "sha512-va15kYZr7KZNNPZdxONGQzpUr+4sxVu7V/VG7a8mRfPPXUyhEYj5RzXCQmGrlP3tAh0L3HHm5AjBMFYRqlM9SA==",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.0.0"
}
},
"node_modules/@types/sockjs-client": { "node_modules/@types/sockjs-client": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/@types/sockjs-client/-/sockjs-client-1.5.1.tgz", "resolved": "https://registry.npmjs.org/@types/sockjs-client/-/sockjs-client-1.5.1.tgz",
"integrity": "sha512-bmZM6A1GPdjF0bcuIUC+50hZEMGkzMsiG9by6X9U+7IZFOiPtz7MJ9h05FSpPVxlj4i+TzzoG3ESo1FJlbLb6A==" "integrity": "sha512-bmZM6A1GPdjF0bcuIUC+50hZEMGkzMsiG9by6X9U+7IZFOiPtz7MJ9h05FSpPVxlj4i+TzzoG3ESo1FJlbLb6A=="
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz",
"integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g=="
},
"node_modules/@webcomponents/webcomponentsjs": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.8.0.tgz",
"integrity": "sha512-loGD63sacRzOzSJgQnB9ZAhaQGkN7wl2Zuw7tsphI5Isa0irijrRo6EnJii/GgjGefIFO8AIO7UivzRhFaEk9w=="
},
"node_modules/core-js": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz",
"integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "3.2.7", "version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
@ -55,6 +91,34 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}, },
"node_modules/lit": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/lit/-/lit-2.7.2.tgz",
"integrity": "sha512-9QnZmG5mIKPRja96cpndMclLSi0Qrz2BXD6EbqNqCKMMjOWVm/BwAeXufFk2jqFsNmY07HOzU8X+8aTSVt3yrA==",
"dependencies": {
"@lit/reactive-element": "^1.6.0",
"lit-element": "^3.3.0",
"lit-html": "^2.7.0"
}
},
"node_modules/lit-element": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.1.tgz",
"integrity": "sha512-Gl+2409uXWbf7n6cCl7Kzasm7zjT9xmdwi2BhLNi70sRKAgRkqueDu5mSIH3hPYMM0/vqBCdPXod3NbGkRA2ww==",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.1.0",
"@lit/reactive-element": "^1.3.0",
"lit-html": "^2.7.0"
}
},
"node_modules/lit-html": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.7.2.tgz",
"integrity": "sha512-ZJCfKlA2XELu5tn7XuzOziGFGvf1SeQm+ngLWoJ8bXtSkRrrR3ms6SWy+gsdxeYwySLij5xAhdd2C3EX0ftxdQ==",
"dependencies": {
"@types/trusted-types": "^2.0.2"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -139,11 +203,39 @@
} }
}, },
"dependencies": { "dependencies": {
"@lit-labs/ssr-dom-shim": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.0.tgz",
"integrity": "sha512-92uQ5ARf7UXYrzaFcAX3T2rTvaS9Z1//ukV+DqjACM4c8s0ZBQd7ayJU5Dh2AFLD/Ayuyz4uMmxQec8q3U4Ong=="
},
"@lit/reactive-element": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.1.tgz",
"integrity": "sha512-va15kYZr7KZNNPZdxONGQzpUr+4sxVu7V/VG7a8mRfPPXUyhEYj5RzXCQmGrlP3tAh0L3HHm5AjBMFYRqlM9SA==",
"requires": {
"@lit-labs/ssr-dom-shim": "^1.0.0"
}
},
"@types/sockjs-client": { "@types/sockjs-client": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/@types/sockjs-client/-/sockjs-client-1.5.1.tgz", "resolved": "https://registry.npmjs.org/@types/sockjs-client/-/sockjs-client-1.5.1.tgz",
"integrity": "sha512-bmZM6A1GPdjF0bcuIUC+50hZEMGkzMsiG9by6X9U+7IZFOiPtz7MJ9h05FSpPVxlj4i+TzzoG3ESo1FJlbLb6A==" "integrity": "sha512-bmZM6A1GPdjF0bcuIUC+50hZEMGkzMsiG9by6X9U+7IZFOiPtz7MJ9h05FSpPVxlj4i+TzzoG3ESo1FJlbLb6A=="
}, },
"@types/trusted-types": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz",
"integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g=="
},
"@webcomponents/webcomponentsjs": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.8.0.tgz",
"integrity": "sha512-loGD63sacRzOzSJgQnB9ZAhaQGkN7wl2Zuw7tsphI5Isa0irijrRo6EnJii/GgjGefIFO8AIO7UivzRhFaEk9w=="
},
"core-js": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz",
"integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ=="
},
"debug": { "debug": {
"version": "3.2.7", "version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
@ -175,6 +267,34 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}, },
"lit": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/lit/-/lit-2.7.2.tgz",
"integrity": "sha512-9QnZmG5mIKPRja96cpndMclLSi0Qrz2BXD6EbqNqCKMMjOWVm/BwAeXufFk2jqFsNmY07HOzU8X+8aTSVt3yrA==",
"requires": {
"@lit/reactive-element": "^1.6.0",
"lit-element": "^3.3.0",
"lit-html": "^2.7.0"
}
},
"lit-element": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.1.tgz",
"integrity": "sha512-Gl+2409uXWbf7n6cCl7Kzasm7zjT9xmdwi2BhLNi70sRKAgRkqueDu5mSIH3hPYMM0/vqBCdPXod3NbGkRA2ww==",
"requires": {
"@lit-labs/ssr-dom-shim": "^1.1.0",
"@lit/reactive-element": "^1.3.0",
"lit-html": "^2.7.0"
}
},
"lit-html": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.7.2.tgz",
"integrity": "sha512-ZJCfKlA2XELu5tn7XuzOziGFGvf1SeQm+ngLWoJ8bXtSkRrrR3ms6SWy+gsdxeYwySLij5xAhdd2C3EX0ftxdQ==",
"requires": {
"@types/trusted-types": "^2.0.2"
}
},
"ms": { "ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View File

@ -10,6 +10,9 @@
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@webcomponents/webcomponentsjs": "^2.8.0",
"core-js": "^3.30.1",
"lit": "^2.7.2",
"sockjs-client": "^1.6.1" "sockjs-client": "^1.6.1"
} }
} }

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,19 +97,29 @@ 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)
r.Get("/client.js.map", handler.handleSDKClientMap) r.Get("/client.js.map", handler.handleSDKClientMap)
}) })
r.Route("/api/v1", func(r chi.Router) { r.Route("/api", func(r chi.Router) {
r.Post("/upload", handler.handleAppUpload) r.Post("/v1/upload", handler.handleAppUpload)
r.Get("/download/{bucket}/{blobID}", handler.handleAppDownload) r.Get("/v1/download/{bucket}/{blobID}", handler.handleAppDownload)
r.Get("/fetch", handler.handleAppFetch) r.Get("/v1/fetch", handler.handleAppFetch)
}) })
for _, fn := range opts.HTTPMounts {
r.Group(func(r chi.Router) {
fn(r)
})
}
r.HandleFunc("/sock/*", handler.handleSockJS) r.HandleFunc("/sock/*", handler.handleSockJS)
}) })

View File

@ -7,6 +7,7 @@ import (
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bus" "forge.cadoles.com/arcad/edge/pkg/bus"
"forge.cadoles.com/arcad/edge/pkg/bus/memory" "forge.cadoles.com/arcad/edge/pkg/bus/memory"
"github.com/go-chi/chi/v5"
"github.com/igm/sockjs-go/v3/sockjs" "github.com/igm/sockjs-go/v3/sockjs"
) )
@ -16,6 +17,8 @@ type HandlerOptions struct {
ServerModuleFactories []app.ServerModuleFactory ServerModuleFactories []app.ServerModuleFactory
UploadMaxFileSize int64 UploadMaxFileSize int64
HTTPClient *http.Client HTTPClient *http.Client
HTTPMounts []func(r chi.Router)
HTTPMiddlewares []func(next http.Handler) http.Handler
} }
func defaultHandlerOptions() *HandlerOptions { func defaultHandlerOptions() *HandlerOptions {
@ -32,6 +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),
HTTPMiddlewares: make([]func(http.Handler) http.Handler, 0),
} }
} }
@ -66,3 +71,15 @@ func WithHTTPClient(client *http.Client) HandlerOptionFunc {
opts.HTTPClient = client opts.HTTPClient = client
} }
} }
func WithHTTPMounts(mounts ...func(r chi.Router)) HandlerOptionFunc {
return func(opts *HandlerOptions) {
opts.HTTPMounts = mounts
}
}
func WithHTTPMiddlewares(middlewares ...func(http.Handler) http.Handler) HandlerOptionFunc {
return func(opts *HandlerOptions) {
opts.HTTPMiddlewares = middlewares
}
}

View File

@ -44,6 +44,10 @@ func (r *Repository) List(ctx context.Context) ([]*app.Manifest, error) {
} }
func NewRepository(getURL GetURLFunc, manifests ...*app.Manifest) *Repository { func NewRepository(getURL GetURLFunc, manifests ...*app.Manifest) *Repository {
if manifests == nil {
manifests = make([]*app.Manifest, 0)
}
return &Repository{getURL, manifests} return &Repository{getURL, manifests}
} }

116
pkg/module/app/mount.go Normal file
View File

@ -0,0 +1,116 @@
package app
import (
"net"
"net/http"
"forge.cadoles.com/arcad/edge/pkg/app"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
)
type MountFunc func(r chi.Router)
type Handler struct {
repo Repository
}
func (h *Handler) serveApps(w http.ResponseWriter, r *http.Request) {
manifests, err := h.repo.List(r.Context())
if err != nil {
logger.Error(r.Context(), "could not retrieve app manifest", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
}
api.DataResponse(w, http.StatusOK, struct {
Manifests []*app.Manifest `json:"manifests"`
}{
Manifests: manifests,
})
}
func (h *Handler) serveApp(w http.ResponseWriter, r *http.Request) {
appID := app.ID(chi.URLParam(r, "appID"))
manifest, err := h.repo.Get(r.Context(), appID)
if err != nil {
if errors.Is(err, ErrNotFound) {
api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)
return
}
logger.Error(r.Context(), "could not retrieve app manifest", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
}
api.DataResponse(w, http.StatusOK, struct {
Manifest *app.Manifest `json:"manifest"`
}{
Manifest: manifest,
})
}
type serveAppURLRequest struct {
From string `json:"from,omitempty"`
}
func (h *Handler) serveAppURL(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req := &serveAppURLRequest{}
if ok := api.Bind(w, r, req); !ok {
return
}
appID := app.ID(chi.URLParam(r, "appID"))
from := req.From
if from == "" {
from = retrieveRemoteAddr(r)
}
url, err := h.repo.GetURL(ctx, appID, from)
if err != nil {
if errors.Is(err, ErrNotFound) {
api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)
return
}
logger.Error(r.Context(), "could not retrieve app url", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
}
api.DataResponse(w, http.StatusOK, struct {
URL string `json:"url"`
}{
URL: url,
})
}
func Mount(repository Repository) MountFunc {
handler := &Handler{repository}
return func(r chi.Router) {
r.Get("/api/v1/apps", handler.serveApps)
r.Get("/api/v1/apps/{appID}", handler.serveApp)
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,5 +9,5 @@ import (
type Repository interface { type Repository interface {
List(context.Context) ([]*app.Manifest, error) List(context.Context) ([]*app.Manifest, error)
Get(context.Context, app.ID) (*app.Manifest, error) Get(context.Context, app.ID) (*app.Manifest, error)
GetURL(context.Context, app.ID, string) (string, error) GetURL(ctx context.Context, id app.ID, from string) (string, error)
} }

View File

@ -2,7 +2,4 @@ package auth
import "errors" import "errors"
var ( var ErrUnauthenticated = errors.New("unauthenticated")
ErrUnauthenticated = errors.New("unauthenticated")
ErrClaimNotFound = errors.New("claim not found")
)

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"
@ -110,7 +111,9 @@ func (h *LocalHandler) handleForm(w http.ResponseWriter, r *http.Request) {
return return
} }
token, err := generateSignedToken(h.algo, h.key, account.Claims) account.Claims[auth.ClaimIssuer] = "local"
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

@ -91,7 +91,7 @@
<form method="post" action="{{ .URL }}"> <form method="post" action="{{ .URL }}">
<div class="form-control"> <div class="form-control">
<label for="username">Username</label> <label for="username">Username</label>
<input type="text" id="username" name="username" value="{{ .Username }}" required /> <input type="text" id="username" name="username" value="{{ .Username }}" required autofocus />
</div> </div>
<div class="form-control"> <div class="form-control">
<label for="password">Password</label> <label for="password">Password</label>

View File

@ -19,10 +19,10 @@ type GetKeySetFunc func() (jwk.Set, error)
func WithJWT(getKeySet GetKeySetFunc) OptionFunc { func WithJWT(getKeySet GetKeySetFunc) OptionFunc {
return func(o *Option) { return func(o *Option) {
o.GetClaim = func(ctx context.Context, r *http.Request, claimName string) (string, error) { o.GetClaims = func(ctx context.Context, r *http.Request, names ...string) ([]string, error) {
claim, err := getClaim[string](r, claimName, getKeySet) claim, err := getClaims[string](r, getKeySet, names...)
if err != nil { if err != nil {
return "", errors.WithStack(err) return nil, errors.WithStack(err)
} }
return claim, nil return claim, nil
@ -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()
@ -76,28 +85,34 @@ func FindToken(r *http.Request, getKeySet GetKeySetFunc) (jwt.Token, error) {
return token, nil return token, nil
} }
func getClaim[T any](r *http.Request, claimAttr string, getKeySet GetKeySetFunc) (T, error) { func getClaims[T any](r *http.Request, getKeySet GetKeySetFunc, names ...string) ([]T, error) {
token, err := FindToken(r, getKeySet) token, err := FindToken(r, getKeySet)
if err != nil { if err != nil {
return *new(T), errors.WithStack(err) return nil, errors.WithStack(err)
} }
ctx := r.Context() ctx := r.Context()
mapClaims, err := token.AsMap(ctx) mapClaims, err := token.AsMap(ctx)
if err != nil { if err != nil {
return *new(T), errors.WithStack(err) return nil, errors.WithStack(err)
} }
rawClaim, exists := mapClaims[claimAttr] claims := make([]T, len(names))
if !exists {
return *new(T), errors.WithStack(ErrClaimNotFound) for idx, n := range names {
rawClaim, exists := mapClaims[n]
if !exists {
continue
}
claim, ok := rawClaim.(T)
if !ok {
return nil, errors.Errorf("unexpected claim '%s' to be of type '%T', got '%T'", n, new(T), rawClaim)
}
claims[idx] = claim
} }
claim, ok := rawClaim.(T) return claims, nil
if !ok {
return *new(T), errors.Errorf("unexpected claim '%s' to be of type '%T', got '%T'", claimAttr, new(T), rawClaim)
}
return claim, nil
} }

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

@ -8,15 +8,21 @@ import (
"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/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
) )
const ( const (
ClaimSubject = "sub" ClaimSubject = "sub"
ClaimIssuer = "iss"
ClaimPreferredUsername = "preferred_username"
ClaimEdgeRole = "edge_role"
ClaimEdgeTenant = "edge_tenant"
ClaimEdgeEntrypoint = "edge_entrypoint"
) )
type Module struct { type Module struct {
server *app.Server server *app.Server
getClaimFunc GetClaimFunc getClaims GetClaimsFunc
} }
func (m *Module) Name() string { func (m *Module) Name() string {
@ -31,6 +37,26 @@ func (m *Module) Export(export *goja.Object) {
if err := export.Set("CLAIM_SUBJECT", ClaimSubject); err != nil { if err := export.Set("CLAIM_SUBJECT", ClaimSubject); err != nil {
panic(errors.Wrap(err, "could not set 'CLAIM_SUBJECT' property")) panic(errors.Wrap(err, "could not set 'CLAIM_SUBJECT' property"))
} }
if err := export.Set("CLAIM_TENANT", ClaimEdgeTenant); err != nil {
panic(errors.Wrap(err, "could not set 'CLAIM_TENANT' property"))
}
if err := export.Set("CLAIM_ENTRYPOINT", ClaimEdgeEntrypoint); err != nil {
panic(errors.Wrap(err, "could not set 'CLAIM_ENTRYPOINT' property"))
}
if err := export.Set("CLAIM_ROLE", ClaimEdgeRole); err != nil {
panic(errors.Wrap(err, "could not set 'CLAIM_ROLE' property"))
}
if err := export.Set("CLAIM_PREFERRED_USERNAME", ClaimPreferredUsername); err != nil {
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 {
@ -42,16 +68,21 @@ func (m *Module) getClaim(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
panic(rt.ToValue(errors.New("could not find http request in context"))) panic(rt.ToValue(errors.New("could not find http request in context")))
} }
claim, err := m.getClaimFunc(ctx, req, claimName) claim, err := m.getClaims(ctx, req, claimName)
if err != nil { if err != nil {
if errors.Is(err, ErrUnauthenticated) || errors.Is(err, ErrClaimNotFound) { if errors.Is(err, ErrUnauthenticated) {
return nil return nil
} }
panic(rt.ToValue(errors.WithStack(err))) logger.Error(ctx, "could not retrieve claim", logger.E(errors.WithStack(err)))
return nil
} }
return rt.ToValue(claim) if len(claim) == 0 || claim[0] == "" {
return nil
}
return rt.ToValue(claim[0])
} }
func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory { func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory {
@ -62,8 +93,8 @@ func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule { return func(server *app.Server) app.ServerModule {
return &Module{ return &Module{
server: server, server: server,
getClaimFunc: opt.GetClaim, getClaims: opt.GetClaims,
} }
} }
} }

72
pkg/module/auth/mount.go Normal file
View File

@ -0,0 +1,72 @@
package auth
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
)
type MountFunc func(r chi.Router)
type Handler struct {
getClaims GetClaimsFunc
profileClaims []string
}
func (h *Handler) serveProfile(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := h.getClaims(ctx, r, h.profileClaims...)
if err != nil {
if errors.Is(err, ErrUnauthenticated) {
api.ErrorResponse(
w, http.StatusUnauthorized,
api.ErrCodeUnauthorized,
nil,
)
return
}
logger.Error(ctx, "could not retrieve claims", logger.E(errors.WithStack(err)))
api.ErrorResponse(
w, http.StatusInternalServerError,
api.ErrCodeUnknownError,
nil,
)
return
}
profile := make(map[string]any)
for idx, cl := range h.profileClaims {
profile[cl] = claims[idx]
}
api.DataResponse(w, http.StatusOK, struct {
Profile map[string]any `json:"profile"`
}{
Profile: profile,
})
}
func Mount(authHandler http.Handler, funcs ...OptionFunc) MountFunc {
opt := defaultOptions()
for _, fn := range funcs {
fn(opt)
}
handler := &Handler{
profileClaims: opt.ProfileClaims,
getClaims: opt.GetClaims,
}
return func(r chi.Router) {
r.Get("/api/v1/profile", handler.serveProfile)
r.Handle("/auth/*", authHandler)
}
}

View File

@ -7,26 +7,41 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type GetClaimFunc func(ctx context.Context, r *http.Request, claimName string) (string, error) type GetClaimsFunc func(ctx context.Context, r *http.Request, claims ...string) ([]string, error)
type Option struct { type Option struct {
GetClaim GetClaimFunc GetClaims GetClaimsFunc
ProfileClaims []string
} }
type OptionFunc func(*Option) type OptionFunc func(*Option)
func defaultOptions() *Option { func defaultOptions() *Option {
return &Option{ return &Option{
GetClaim: dummyGetClaim, GetClaims: dummyGetClaims,
ProfileClaims: []string{
ClaimSubject,
ClaimIssuer,
ClaimEdgeEntrypoint,
ClaimEdgeRole,
ClaimPreferredUsername,
ClaimEdgeTenant,
},
} }
} }
func dummyGetClaim(ctx context.Context, r *http.Request, claimName string) (string, error) { func dummyGetClaims(ctx context.Context, r *http.Request, claims ...string) ([]string, error) {
return "", errors.Errorf("dummy getclaim func cannot retrieve claim '%s'", claimName) return nil, errors.Errorf("dummy getclaim func cannot retrieve claims '%s'", claims)
} }
func WithGetClaim(fn GetClaimFunc) OptionFunc { func WithGetClaims(fn GetClaimsFunc) OptionFunc {
return func(o *Option) { return func(o *Option) {
o.GetClaim = fn o.GetClaims = fn
}
}
func WithProfileClaims(claims ...string) OptionFunc {
return func(o *Option) {
o.ProfileClaims = claims
} }
} }

View File

@ -1,14 +1,14 @@
package blob package blob
import ( import (
"io/ioutil" "os"
"testing" "testing"
"cdr.dev/slog" "cdr.dev/slog"
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bus/memory" "forge.cadoles.com/arcad/edge/pkg/bus/memory"
"forge.cadoles.com/arcad/edge/pkg/module" "forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite" "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
@ -27,7 +27,7 @@ func TestBlobModule(t *testing.T) {
ModuleFactory(bus, store), ModuleFactory(bus, store),
) )
data, err := ioutil.ReadFile("testdata/blob.js") data, err := os.ReadFile("testdata/blob.js")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -3,10 +3,10 @@ package cast
import ( import (
"context" "context"
"net" "net"
"sync"
"time" "time"
"github.com/barnybug/go-cast" "github.com/barnybug/go-cast"
"github.com/barnybug/go-cast/discovery"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
@ -18,6 +18,15 @@ type Device struct {
Name string `goja:"name" json:"name"` Name string `goja:"name" json:"name"`
} }
type CachedDevice struct {
Device
UpdatedAt time.Time
}
func (d CachedDevice) Expired() bool {
return d.UpdatedAt.Add(30 * time.Minute).Before(time.Now())
}
type DeviceStatus struct { type DeviceStatus struct {
CurrentApp DeviceStatusCurrentApp `goja:"currentApp" json:"currentApp"` CurrentApp DeviceStatusCurrentApp `goja:"currentApp" json:"currentApp"`
Volume DeviceStatusVolume `goja:"volume" json:"volume"` Volume DeviceStatusVolume `goja:"volume" json:"volume"`
@ -35,9 +44,36 @@ type DeviceStatusVolume struct {
} }
const ( const (
serviceDiscoveryPollingInterval time.Duration = 2 * time.Second serviceDiscoveryPollingInterval time.Duration = 500 * time.Millisecond
) )
var cache sync.Map
func getCachedDevice(uuid string) (Device, bool) {
value, exists := cache.Load(uuid)
if !exists {
return Device{}, false
}
cachedDevice, ok := value.(CachedDevice)
if !ok {
return Device{}, false
}
if cachedDevice.Expired() {
return Device{}, false
}
return cachedDevice.Device, true
}
func cacheDevice(dev Device) {
cache.Store(dev.UUID, CachedDevice{
Device: dev,
UpdatedAt: time.Now(),
})
}
func getDeviceClientByUUID(ctx context.Context, uuid string) (*cast.Client, error) { func getDeviceClientByUUID(ctx context.Context, uuid string) (*cast.Client, error) {
device, err := FindDeviceByUUID(ctx, uuid) device, err := FindDeviceByUUID(ctx, uuid)
if err != nil { if err != nil {
@ -49,82 +85,114 @@ func getDeviceClientByUUID(ctx context.Context, uuid string) (*cast.Client, erro
return client, nil return client, nil
} }
func FindDeviceByUUID(ctx context.Context, uuid string) (*Device, error) { func FindDeviceByUUID(ctx context.Context, uuid string) (Device, error) {
service := discovery.NewService(ctx) device, exists := getCachedDevice(uuid)
defer service.Stop() if exists {
return device, nil
}
go func() { ctx, cancel := context.WithCancel(ctx)
if err := service.Run(ctx, serviceDiscoveryPollingInterval); err != nil { defer cancel()
logger.Error(ctx, "error while running cast service discovery", logger.E(errors.WithStack(err)))
}
}()
LOOP: devices, err := SearchDevices(ctx)
for { if err != nil {
select { return Device{}, nil
case c := <-service.Found(): }
if c.Uuid() == uuid {
return &Device{ for dev := range devices {
Host: c.IP().To4(), if dev.UUID == uuid {
Port: c.Port(), return dev, nil
Name: c.Name(),
UUID: c.Uuid(),
}, nil
}
case <-ctx.Done():
break LOOP
} }
} }
if err := ctx.Err(); err != nil { return Device{}, errors.Errorf("could not find device '%s'", uuid)
return nil, errors.WithStack(err)
}
return nil, errors.WithStack(ErrDeviceNotFound)
} }
func FindDevices(ctx context.Context) ([]*Device, error) { func ListDevices(ctx context.Context, refresh bool) ([]Device, error) {
service := discovery.NewService(ctx) devices := make([]Device, 0)
defer service.Stop()
go func() { if !refresh {
if err := service.Run(ctx, serviceDiscoveryPollingInterval); err != nil && !errors.Is(err, context.DeadlineExceeded) { cache.Range(func(key, value any) bool {
logger.Error(ctx, "error while running cast service discovery", logger.E(errors.WithStack(err))) cached, ok := value.(CachedDevice)
} if !ok || cached.Expired() {
}() return true
devices := make([]*Device, 0)
found := make(map[string]struct{})
LOOP:
for {
select {
case c := <-service.Found():
if _, exists := found[c.Uuid()]; exists {
continue
} }
devices = append(devices, &Device{ devices = append(devices, cached.Device)
Host: c.IP().To4(), return true
Port: c.Port(), })
Name: c.Name(),
UUID: c.Uuid(),
})
found[c.Uuid()] = struct{}{}
case <-ctx.Done(): return devices, nil
break LOOP
}
} }
if err := ctx.Err(); err != nil && !errors.Is(err, context.DeadlineExceeded) { ch, err := SearchDevices(ctx)
if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
for dev := range ch {
devices = append(devices, dev)
}
return devices, nil return devices, nil
} }
var searchDevicesMutex sync.Mutex
func SearchDevices(ctx context.Context) (chan Device, error) {
service := NewService(ctx)
defer service.Stop()
go func() {
searchDevicesMutex.Lock()
defer searchDevicesMutex.Unlock()
if err := service.Run(ctx, serviceDiscoveryPollingInterval); err != nil && !errors.Is(err, context.DeadlineExceeded) {
logger.Error(ctx, "error while running cast service discovery", logger.E(errors.WithStack(err)))
}
}()
devices := make(chan Device)
go func() {
defer close(devices)
found := make(map[string]struct{})
LOOP:
for {
select {
case c := <-service.Found():
dev := Device{
Host: c.IP().To4(),
Port: c.Port(),
Name: c.Name(),
UUID: c.Uuid(),
}
if _, exists := found[dev.UUID]; !exists {
devices <- dev
found[dev.UUID] = struct{}{}
}
cacheDevice(dev)
case <-ctx.Done():
break LOOP
}
}
}()
return devices, nil
}
var loadURLMutex sync.Mutex
func LoadURL(ctx context.Context, deviceUUID string, url string) error { func LoadURL(ctx context.Context, deviceUUID string, url string) error {
loadURLMutex.Lock()
defer loadURLMutex.Unlock()
client, err := getDeviceClientByUUID(ctx, deviceUUID) client, err := getDeviceClientByUUID(ctx, deviceUUID)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
@ -153,7 +221,12 @@ func isLoadURLContextExceeded(err error) bool {
return err.Error() == "Failed to send load command: context deadline exceeded" return err.Error() == "Failed to send load command: context deadline exceeded"
} }
var stopCastMutex sync.Mutex
func StopCast(ctx context.Context, deviceUUID string) error { func StopCast(ctx context.Context, deviceUUID string) error {
stopCastMutex.Lock()
defer stopCastMutex.Unlock()
client, err := getDeviceClientByUUID(ctx, deviceUUID) client, err := getDeviceClientByUUID(ctx, deviceUUID)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
@ -171,7 +244,12 @@ func StopCast(ctx context.Context, deviceUUID string) error {
return nil return nil
} }
var getStatusMutex sync.Mutex
func getStatus(ctx context.Context, deviceUUID string) (*DeviceStatus, error) { func getStatus(ctx context.Context, deviceUUID string) (*DeviceStatus, error) {
getStatusMutex.Lock()
defer getStatusMutex.Unlock()
client, err := getDeviceClientByUUID(ctx, deviceUUID) client, err := getDeviceClientByUUID(ctx, deviceUUID)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)

View File

@ -26,11 +26,24 @@ func TestCastLoadURL(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
devices, err := FindDevices(ctx) devices, err := ListDevices(ctx, true)
if err != nil { if err != nil {
t.Error(errors.WithStack(err)) t.Error(errors.WithStack(err))
} }
t.Logf("DEVICES: %s", spew.Sdump(devices))
if e, g := 1, len(devices); e != g {
t.Fatalf("len(devices): expected '%v', got '%v'", e, g)
}
devices, err = ListDevices(ctx, false)
if err != nil {
t.Error(errors.WithStack(err))
}
t.Logf("CACHED DEVICES: %s", spew.Sdump(devices))
if e, g := 1, len(devices); e != g { if e, g := 1, len(devices); e != g {
t.Fatalf("len(devices): expected '%v', got '%v'", e, g) t.Fatalf("len(devices): expected '%v', got '%v'", e, g)
} }
@ -52,7 +65,7 @@ func TestCastLoadURL(t *testing.T) {
t.Error(errors.WithStack(err)) t.Error(errors.WithStack(err))
} }
spew.Dump(status) t.Logf("DEVICE STATUS: %s", spew.Sdump(status))
ctx, cancel4 := context.WithTimeout(context.Background(), 15*time.Second) ctx, cancel4 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel4() defer cancel4()

View File

@ -0,0 +1,250 @@
package cast
import (
"context"
"net"
"regexp"
"strconv"
"strings"
"sync"
"time"
"gitlab.com/wpetit/goweb/logger"
"github.com/barnybug/go-cast"
"github.com/barnybug/go-cast/log"
"github.com/hashicorp/mdns"
"github.com/pkg/errors"
)
type Service struct {
found chan *cast.Client
entriesCh chan *mdns.ServiceEntry
stopPeriodic chan struct{}
}
func NewService(ctx context.Context) *Service {
s := &Service{
found: make(chan *cast.Client),
entriesCh: make(chan *mdns.ServiceEntry, 10),
}
go s.listener(ctx)
return s
}
func (d *Service) Run(ctx context.Context, interval time.Duration) error {
ifaces, err := findMulticastInterfaces(ctx)
if err != nil {
return errors.WithStack(err)
}
var wg sync.WaitGroup
for _, iface := range ifaces {
hasIPv4, hasIPv6, err := retrieveSupportedProtocols(iface)
if err != nil {
return errors.WithStack(err)
}
if !hasIPv4 && !hasIPv6 {
continue
}
if err := d.queryIface(iface, !hasIPv4, !hasIPv6); err != nil {
return errors.WithStack(err)
}
pollCtx, cancel := context.WithCancel(ctx)
defer cancel()
wg.Add(1)
go func(ctx context.Context, iface net.Interface) {
defer wg.Done()
if err := d.pollInterface(ctx, iface, interval, !hasIPv4, !hasIPv6); err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
return
}
logger.Error(
ctx, "could not poll interface",
logger.E(errors.WithStack(err)), logger.F("iface", iface.Name),
)
}
}(pollCtx, iface)
}
wg.Wait()
return nil
}
func (d *Service) queryIface(iface net.Interface, disableIPv4, disableIPv6 bool) error {
err := mdns.Query(&mdns.QueryParam{
Service: "_googlecast._tcp",
Domain: "local",
Timeout: 3 * time.Second,
Entries: d.entriesCh,
Interface: &iface,
DisableIPv6: disableIPv6,
DisableIPv4: disableIPv4,
})
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (d *Service) pollInterface(ctx context.Context, iface net.Interface, interval time.Duration, disableIPv4, disableIPv6 bool) error {
ticker := time.NewTicker(interval)
for {
select {
case <-ticker.C:
if err := d.queryIface(iface, disableIPv4, disableIPv6); err != nil {
return errors.WithStack(err)
}
case <-ctx.Done():
if err := ctx.Err(); err != nil {
return errors.WithStack(err)
}
return nil
}
}
}
func (d *Service) Stop() {
if d.stopPeriodic != nil {
close(d.stopPeriodic)
d.stopPeriodic = nil
}
}
func (d *Service) Found() chan *cast.Client {
return d.found
}
func (d *Service) listener(ctx context.Context) {
for entry := range d.entriesCh {
name := strings.Split(entry.Name, "._googlecast")
// Skip everything that doesn't have googlecast in the fdqn
if len(name) < 2 {
continue
}
log.Printf("New entry: %#v\n", entry)
client := cast.NewClient(entry.AddrV4, entry.Port)
info := decodeTxtRecord(entry.Info)
client.SetName(info["fn"])
client.SetInfo(info)
select {
case d.found <- client:
case <-time.After(time.Second):
case <-ctx.Done():
break
}
}
}
func decodeDnsEntry(text string) string {
text = strings.Replace(text, `\.`, ".", -1)
text = strings.Replace(text, `\ `, " ", -1)
re := regexp.MustCompile(`([\\][0-9][0-9][0-9])`)
text = re.ReplaceAllStringFunc(text, func(source string) string {
i, err := strconv.Atoi(source[1:])
if err != nil {
return ""
}
return string([]byte{byte(i)})
})
return text
}
func decodeTxtRecord(txt string) map[string]string {
m := make(map[string]string)
s := strings.Split(txt, "|")
for _, v := range s {
s := strings.Split(v, "=")
if len(s) == 2 {
m[s[0]] = s[1]
}
}
return m
}
func isIPv4(ip net.IP) bool {
return strings.Count(ip.String(), ":") < 2
}
func isIPv6(ip net.IP) bool {
return strings.Count(ip.String(), ":") >= 2
}
func findMulticastInterfaces(ctx context.Context) ([]net.Interface, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, nil
}
multicastIfaces := make([]net.Interface, 0)
for _, iface := range ifaces {
if iface.Flags&net.FlagLoopback == net.FlagLoopback {
continue
}
if iface.Flags&net.FlagRunning != net.FlagRunning {
continue
}
if iface.Flags&net.FlagMulticast != net.FlagMulticast {
continue
}
multicastIfaces = append(multicastIfaces, iface)
}
return multicastIfaces, nil
}
func retrieveSupportedProtocols(iface net.Interface) (bool, bool, error) {
adresses, err := iface.Addrs()
if err != nil {
return false, false, errors.WithStack(err)
}
hasIPv4 := false
hasIPv6 := false
for _, addr := range adresses {
ip, _, err := net.ParseCIDR(addr.String())
if err != nil {
return false, false, errors.WithStack(err)
}
if isIPv4(ip) {
hasIPv4 = true
}
if isIPv6(ip) {
hasIPv6 = true
}
if hasIPv4 && hasIPv6 {
return hasIPv4, hasIPv6, nil
}
}
return hasIPv4, hasIPv6, nil
}

View File

@ -2,7 +2,6 @@ package cast
import ( import (
"context" "context"
"sync"
"time" "time"
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
@ -16,18 +15,7 @@ const (
defaultTimeout = 30 * time.Second defaultTimeout = 30 * time.Second
) )
type Module struct { type Module struct{}
ctx context.Context
server *app.Server
mutex struct {
devices sync.RWMutex
refreshDevices sync.Mutex
loadURL sync.Mutex
quitApp sync.Mutex
getStatus sync.Mutex
}
devices []*Device
}
func (m *Module) Name() string { func (m *Module) Name() string {
return "cast" return "cast"
@ -63,17 +51,14 @@ 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() {
m.mutex.refreshDevices.Lock()
defer m.mutex.refreshDevices.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
devices, err := FindDevices(ctx) devices, err := ListDevices(ctx, true)
if err != nil && !errors.Is(err, context.DeadlineExceeded) { if err != nil {
err = errors.WithStack(err) err = errors.WithStack(err)
logger.Error(ctx, "error refreshing casting devices list", logger.E(errors.WithStack(err))) logger.Error(ctx, "error refreshing casting devices list", logger.E(errors.WithStack(err)))
@ -82,24 +67,19 @@ func (m *Module) refreshDevices(call goja.FunctionCall, rt *goja.Runtime) goja.V
return return
} }
if err == nil { promise.Resolve(devices)
m.mutex.devices.Lock()
m.devices = devices
m.mutex.devices.Unlock()
}
devicesCopy := m.getDevicesCopy(devices)
promise.Resolve(devicesCopy)
}() }()
return rt.ToValue(promise) return rt.ToValue(promise)
} }
func (m *Module) getDevices(call goja.FunctionCall, rt *goja.Runtime) goja.Value { func (m *Module) getDevices(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
m.mutex.devices.RLock() ctx := context.Background()
defer m.mutex.devices.RUnlock()
devices := m.getDevicesCopy(m.devices) devices, err := ListDevices(ctx, false)
if err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return rt.ToValue(devices) return rt.ToValue(devices)
} }
@ -119,12 +99,9 @@ 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() {
m.mutex.loadURL.Lock()
defer m.mutex.loadURL.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
@ -141,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 {
@ -157,12 +134,9 @@ 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() {
m.mutex.quitApp.Lock()
defer m.mutex.quitApp.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
@ -179,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 {
@ -195,12 +169,9 @@ 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() {
m.mutex.getStatus.Lock()
defer m.mutex.getStatus.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
@ -217,22 +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) getDevicesCopy(devices []*Device) []Device {
devicesCopy := make([]Device, 0, len(m.devices))
for _, d := range devices {
devicesCopy = append(devicesCopy, Device{
UUID: d.UUID,
Name: d.Name,
Host: d.Host,
Port: d.Port,
})
}
return devicesCopy
} }
func (m *Module) parseTimeout(rawTimeout string) (time.Duration, error) { func (m *Module) parseTimeout(rawTimeout string) (time.Duration, error) {
@ -255,9 +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,
devices: make([]*Device, 0),
}
} }
} }

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{}

342
pkg/module/share/module.go Normal file
View File

@ -0,0 +1,342 @@
package share
import (
"time"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module/util"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/dop251/goja"
"github.com/pkg/errors"
)
const (
AnyType share.ValueType = "*"
AnyName string = "*"
)
type Module struct {
appID app.ID
store share.Store
}
func (m *Module) Name() string {
return "share"
}
func (m *Module) Export(export *goja.Object) {
if err := export.Set("upsertResource", m.upsertResource); err != nil {
panic(errors.Wrap(err, "could not set 'upsertResource' function"))
}
if err := export.Set("findResources", m.findResources); err != nil {
panic(errors.Wrap(err, "could not set 'findResources' function"))
}
if err := export.Set("deleteAttributes", m.deleteAttributes); err != nil {
panic(errors.Wrap(err, "could not set 'deleteAttributes' function"))
}
if err := export.Set("deleteResource", m.deleteResource); err != nil {
panic(errors.Wrap(err, "could not set 'deleteResource' function"))
}
if err := export.Set("ANY_TYPE", AnyType); err != nil {
panic(errors.Wrap(err, "could not set 'ANY_TYPE' property"))
}
if err := export.Set("ANY_NAME", AnyName); err != nil {
panic(errors.Wrap(err, "could not set 'ANY_NAME' property"))
}
if err := export.Set("TYPE_TEXT", share.TypeText); err != nil {
panic(errors.Wrap(err, "could not set 'TYPE_TEXT' property"))
}
if err := export.Set("TYPE_NUMBER", share.TypeNumber); err != nil {
panic(errors.Wrap(err, "could not set 'TYPE_NUMBER' property"))
}
if err := export.Set("TYPE_BOOL", share.TypeBool); err != nil {
panic(errors.Wrap(err, "could not set 'TYPE_BOOL' property"))
}
if err := export.Set("TYPE_PATH", share.TypePath); err != nil {
panic(errors.Wrap(err, "could not set 'TYPE_PATH' property"))
}
}
func (m *Module) upsertResource(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt)
resourceID := assertResourceID(call.Argument(1), rt)
var attributes []share.Attribute
if len(call.Arguments) > 2 {
attributes = assertAttributes(call.Arguments[2:], rt)
} else {
attributes = make([]share.Attribute, 0)
}
for _, attr := range attributes {
if err := share.AssertType(attr.Value(), attr.Type()); err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
}
resource, err := m.store.UpdateAttributes(ctx, m.appID, resourceID, attributes...)
if err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return rt.ToValue(toGojaResource(resource))
}
func (m *Module) deleteAttributes(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt)
resourceID := assertResourceID(call.Argument(1), rt)
var names []string
if len(call.Arguments) > 2 {
names = assertStrings(call.Arguments[2:], rt)
} else {
names = make([]string, 0)
}
err := m.store.DeleteAttributes(ctx, m.appID, resourceID, names...)
if err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return nil
}
func (m *Module) findResources(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt)
funcs := make([]share.FindResourcesOptionFunc, 0)
if len(call.Arguments) > 1 {
name := util.AssertString(call.Argument(1), rt)
if name != AnyName {
funcs = append(funcs, share.WithName(name))
}
}
if len(call.Arguments) > 2 {
valueType := assertValueType(call.Argument(2), rt)
if valueType != AnyType {
funcs = append(funcs, share.WithType(valueType))
}
}
resources, err := m.store.FindResources(ctx, funcs...)
if err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return rt.ToValue(toGojaResources(resources))
}
func (m *Module) deleteResource(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt)
resourceID := assertResourceID(call.Argument(1), rt)
err := m.store.DeleteResource(ctx, m.appID, resourceID)
if err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return nil
}
func ModuleFactory(appID app.ID, store share.Store) app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule {
return &Module{
appID: appID,
store: store,
}
}
}
func assertResourceID(v goja.Value, r *goja.Runtime) share.ResourceID {
value := v.Export()
switch typ := value.(type) {
case string:
return share.ResourceID(typ)
case share.ResourceID:
return typ
default:
panic(r.ToValue(errors.Errorf("expected value to be a string or ResourceID, got '%T'", value)))
}
}
func assertAttributes(values []goja.Value, r *goja.Runtime) []share.Attribute {
attributes := make([]share.Attribute, len(values))
for idx, val := range values {
export := val.Export()
rawAttr, ok := export.(map[string]any)
if !ok {
panic(r.ToValue(errors.Errorf("unexpected attribute value, got '%v'", export)))
}
rawName, exists := rawAttr["name"]
if !exists {
panic(r.ToValue(errors.Errorf("could not find 'name' property on attribute '%v'", export)))
}
name, ok := rawName.(string)
if !ok {
panic(r.ToValue(errors.Errorf("unexpected value for attribute property 'name': expected 'string', got '%T'", rawName)))
}
rawType, exists := rawAttr["type"]
if !exists {
panic(r.ToValue(errors.Errorf("could not find 'type' property on attribute '%v'", export)))
}
var valueType share.ValueType
switch typ := rawType.(type) {
case share.ValueType:
valueType = typ
case string:
valueType = share.ValueType(typ)
default:
panic(r.ToValue(errors.Errorf("unexpected value for attribute property 'type': expected 'string' or 'ValueType', got '%T'", rawType)))
}
value, exists := rawAttr["value"]
if !exists {
panic(r.ToValue(errors.Errorf("could not find 'value' property on attribute '%v'", export)))
}
attributes[idx] = share.NewBaseAttribute(
name,
valueType,
value,
)
}
return attributes
}
func assertStrings(values []goja.Value, r *goja.Runtime) []string {
strings := make([]string, len(values))
for idx, v := range values {
strings[idx] = util.AssertString(v, r)
}
return strings
}
func assertValueType(v goja.Value, r *goja.Runtime) share.ValueType {
value := v.Export()
switch typ := value.(type) {
case string:
return share.ValueType(typ)
case share.ValueType:
return typ
default:
panic(r.ToValue(errors.Errorf("expected value to be a string or ValueType, got '%T'", value)))
}
}
type gojaResource struct {
ID share.ResourceID `goja:"id" json:"id"`
Origin app.ID `goja:"origin" json:"origin"`
Attributes []*gojaAttribute `goja:"attributes" json:"attributes"`
}
func (r *gojaResource) Has(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
name := util.AssertString(call.Argument(0), rt)
valueType := assertValueType(call.Argument(1), rt)
hasAttr := share.HasAttribute(toResource(r), name, valueType)
return rt.ToValue(hasAttr)
}
func (r *gojaResource) Get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
name := util.AssertString(call.Argument(0), rt)
valueType := assertValueType(call.Argument(1), rt)
var defaultValue any
if len(call.Arguments) > 2 {
defaultValue = call.Argument(2).Export()
}
attr := share.GetAttribute(toResource(r), name, valueType)
if attr == nil {
return rt.ToValue(defaultValue)
}
return rt.ToValue(attr.Value())
}
type gojaAttribute struct {
Name string `goja:"name" json:"name"`
Type share.ValueType `goja:"type" json:"type"`
Value any `goja:"value" json:"value"`
CreatedAt time.Time `goja:"createdAt" json:"createdAt"`
UpdatedAt time.Time `goja:"updatedAt" json:"updatedAt"`
}
func toGojaResource(res share.Resource) *gojaResource {
attributes := make([]*gojaAttribute, len(res.Attributes()))
for idx, attr := range res.Attributes() {
attributes[idx] = &gojaAttribute{
Name: attr.Name(),
Type: attr.Type(),
Value: attr.Value(),
CreatedAt: attr.CreatedAt(),
UpdatedAt: attr.UpdatedAt(),
}
}
return &gojaResource{
ID: res.ID(),
Origin: res.Origin(),
Attributes: attributes,
}
}
func toGojaResources(resources []share.Resource) []*gojaResource {
gojaResources := make([]*gojaResource, len(resources))
for idx, res := range resources {
gojaResources[idx] = toGojaResource(res)
}
return gojaResources
}
func toResource(res *gojaResource) share.Resource {
return share.NewBaseResource(
res.Origin,
res.ID,
toAttributes(res.Attributes)...,
)
}
func toAttributes(gojaAttributes []*gojaAttribute) []share.Attribute {
attributes := make([]share.Attribute, len(gojaAttributes))
for idx, gojaAttr := range gojaAttributes {
attr := share.NewBaseAttribute(
gojaAttr.Name,
gojaAttr.Type,
gojaAttr.Value,
)
attr.SetCreatedAt(gojaAttr.CreatedAt)
attr.SetUpdatedAt(gojaAttr.UpdatedAt)
attributes[idx] = attr
}
return attributes
}

View File

@ -0,0 +1,49 @@
package share
import (
"context"
"os"
"testing"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
)
func TestModule(t *testing.T) {
logger.SetLevel(logger.LevelDebug)
store, err := driver.NewShareStore("sqlite://testdata/test_share_module.sqlite")
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
server := app.NewServer(
module.ContextModuleFactory(),
module.ConsoleModuleFactory(),
ModuleFactory("test.app.edge", store),
)
data, err := os.ReadFile("testdata/share.js")
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if err := server.Load("testdata/share.js", string(data)); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if err := server.Start(); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if _, err := server.ExecFuncByName(context.Background(), "testModule"); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
server.Stop()
}

82
pkg/module/share/testdata/share.js vendored Normal file
View File

@ -0,0 +1,82 @@
function testModule() {
var ctx = context.new();
var resourceId = "my-first-res";
var attributes = [
{ name: "my_text", type: share.TYPE_TEXT, value: "my_text" },
{ name: "my_number", type: share.TYPE_NUMBER, value: 5 },
{ name: "my_path", type: share.TYPE_PATH, value: "/my/path" },
{ name: "my_bool", type: share.TYPE_BOOL, value: true },
]
// Create resource with attributes
var resource = share.upsertResource(
ctx, resourceId,
attributes[0],
attributes[1],
attributes[2],
attributes[3]
);
if (resource.id != resourceId) {
throw new Error("resource.id: expected '"+resourceId+"', got '"+resource.id+"'")
}
if (resource.origin != "test.app.edge") {
throw new Error("resource.origin: expected 'test.app.edge', got '"+resource.origin+"'")
}
if (resource.attributes.length != 4) {
throw new Error("resource.attributes.length: expected '1', got '"+resource.attributes.length+"'")
}
for(var attr, i = 0;( attr = attributes[i] ); i++) {
var exists = resource.has(attr.name, attr.type);
if (!exists) {
throw new Error("resource.has('"+attr.name+"'): expected 'true', got '"+hasAttr+"'")
}
var value = resource.get(attr.name, attr.type);
if (value != attr.value) {
throw new Error("value: expected '"+attr.value+"', got '"+value+"'")
}
}
// Test acces of unexistant attribute
var unexistantAttr = "unexistant_attr"
var exists = resource.has(unexistantAttr, share.TYPE_TEXT);
if (exists) {
throw new Error("attr '"+unexistantAttr+"' should not exist")
}
var expected = "foo"
var value = resource.get(unexistantAttr, share.TYPE_TEXT, expected);
if (value != expected) {
throw new Error("resource.get('"+attr.name+"', share.TYPE_TEXT, '"+expected+"'): expected '"+expected+"', got '"+value+"'")
}
// Search resources
// With any attribute
var results = share.findResources(ctx, share.ANY_NAME, share.ANY_TYPE);
if (results.length != 1) {
throw new Error("results.length: expected '1', got '"+results.length+"'")
}
// With an unexistant attribute
var results = share.findResources(ctx, unexistantAttr, share.ANY_TYPE);
if (results.length != 0) {
throw new Error("results.length: expected '0', got '"+results.length+"'")
}
// With a wrong type
var results = share.findResources(ctx, "my_text", share.TYPE_NUMBER);
if (results.length != 0) {
throw new Error("results.length: expected '0', got '"+results.length+"'")
}
}

View File

@ -2,12 +2,12 @@ package store
import ( import (
"context" "context"
"io/ioutil" "os"
"testing" "testing"
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module" "forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite" "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
@ -22,7 +22,7 @@ func TestStoreModule(t *testing.T) {
ModuleFactory(store), ModuleFactory(store),
) )
data, err := ioutil.ReadFile("testdata/store.js") data, err := os.ReadFile("testdata/store.js")
if err != nil { if err != nil {
t.Fatalf("%+v", errors.WithStack(err)) t.Fatalf("%+v", errors.WithStack(err))
} }

View File

@ -3,6 +3,7 @@ package util
import ( import (
"context" "context"
"forge.cadoles.com/arcad/edge/pkg/app"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -26,3 +27,15 @@ func AssertObject(v goja.Value, r *goja.Runtime) map[string]any {
func AssertString(v goja.Value, r *goja.Runtime) string { func AssertString(v goja.Value, r *goja.Runtime) string {
return AssertType[string](v, r) return AssertType[string](v, r)
} }
func AssertAppID(v goja.Value, r *goja.Runtime) app.ID {
value := v.Export()
switch typ := value.(type) {
case string:
return app.ID(typ)
case app.ID:
return typ
default:
panic(r.ToValue(errors.Errorf("expected value to be a string or app.ID, got '%T'", value)))
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,8 @@ import SockJS from 'sockjs-client';
const EventTypeMessage = "message"; const EventTypeMessage = "message";
const EdgeAuth = "edge-auth" const EdgeAuth = "edge-auth"
const EdgeAuthTokenRequest = "edge_auth_token_request"
const EdgeAuthTokenResponse = "edge_auth_token_reponse"
export class Client extends EventTarget { export class Client extends EventTarget {
@ -23,6 +25,7 @@ export class Client extends EventTarget {
this._onConnectionClose = this._onConnectionClose.bind(this); this._onConnectionClose = this._onConnectionClose.bind(this);
this._onConnectionMessage = this._onConnectionMessage.bind(this); this._onConnectionMessage = this._onConnectionMessage.bind(this);
this._handleRPCResponse = this._handleRPCResponse.bind(this); this._handleRPCResponse = this._handleRPCResponse.bind(this);
this._handleEdgeAuthTokenRequest = this._handleEdgeAuthTokenRequest.bind(this);
this._rpcID = 0; this._rpcID = 0;
this._pendingRPC = {}; this._pendingRPC = {};
@ -37,63 +40,142 @@ export class Client extends EventTarget {
this.send = this.send.bind(this); this.send = this.send.bind(this);
this.upload = this.upload.bind(this); this.upload = this.upload.bind(this);
this.addEventListener(EventTypeMessage, this._handleRPCResponse); this.addEventListener(EventTypeMessage, this._handleRPCResponse);
window.addEventListener('message', this._handleEdgeAuthTokenRequest);
} }
connect(token = "") { connect(token = ""): Promise<Client> {
return new Promise((resolve, reject) => { let getToken: Promise<string>
if (token == "") {
token = this._getAuthCookieToken()
}
const url = `//${document.location.host}/edge/sock?${EdgeAuth}=${token}`; if (token) {
this._log("opening connection to", url); getToken = Promise.resolve(token)
const conn: any = new SockJS(url); } else {
getToken = this._retrieveToken()
}
const onOpen = () => { return getToken.then(token => this._connect(token))
this._log('client connected');
resetHandlers();
conn.onclose = this._onConnectionClose;
conn.onmessage = this._onConnectionMessage;
this._conn = conn;
this._sendQueued();
setTimeout(() => {
this._dispatchConnect();
}, 0);
return resolve(this);
};
const onError = (evt) => {
resetHandlers();
this._scheduleReconnection();
return reject(evt);
};
const resetHandlers = () => {
conn.removeEventListener('open', onOpen);
conn.removeEventListener('close', onError);
conn.removeEventListener('error', onError);
};
conn.addEventListener('open', onOpen);
conn.addEventListener('error', onError);
conn.addEventListener('close', onError);
});
} }
disconnect() { disconnect() {
this._cleanupConnection(); this._cleanupConnection();
} }
_getAuthCookieToken() { _connect(token: string): Promise<Client> {
return new Promise((resolve, reject) => {
const url = `//${document.location.host}/edge/sock?${EdgeAuth}=${token}`;
this._log("opening connection to", url);
const conn: any = new SockJS(url);
const onOpen = () => {
this._log('client connected');
resetHandlers();
conn.onclose = this._onConnectionClose;
conn.onmessage = this._onConnectionMessage;
this._conn = conn;
this._sendQueued();
setTimeout(() => {
this._dispatchConnect();
}, 0);
return resolve(this);
};
const onError = (evt) => {
resetHandlers();
this._scheduleReconnection();
return reject(evt);
};
const resetHandlers = () => {
conn.removeEventListener('open', onOpen);
conn.removeEventListener('close', onError);
conn.removeEventListener('error', onError);
};
conn.addEventListener('open', onOpen);
conn.addEventListener('error', onError);
conn.addEventListener('close', onError);
})
}
_retrieveToken(): Promise<string> {
let token = this._getAuthCookieToken();
if (token) {
return Promise.resolve(token);
}
return this._getParentFrameToken();;
}
_getAuthCookieToken(): string {
const cookie = document.cookie.split("; ") const cookie = document.cookie.split("; ")
.find((row) => row.startsWith(EdgeAuth)); .find((row) => row.startsWith(EdgeAuth));
let token = "";
if (cookie) { if (cookie) {
return cookie.split("=")[1]; token = cookie.split("=")[1];
} }
return ""; return token;
}
_getParentFrameToken(timeout = 5000): Promise<string> {
if (!window.parent || window.parent === window) {
return Promise.resolve("");
}
return new Promise((resolve, reject) => {
let timedOut = false;
const timeoutId = setTimeout(() => {
timedOut = true;
reject(new Error("Edge auth token request timed out !"));
}, timeout);
const listener = (evt) => {
const message = evt.data;
if (!message || !message.type || !message.data) {
return
}
if (message.type !== EdgeAuthTokenResponse) {
return;
}
window.removeEventListener('message', listener);
clearTimeout(timeoutId);
if (timedOut) return;
if (!message.data) {
reject("Unexpected auth token request response !");
return;
}
resolve(message.data?.token || "");
}
window.addEventListener('message', listener);
const message = { type: EdgeAuthTokenRequest };
window.parent.postMessage(message, '*');
})
}
_handleEdgeAuthTokenRequest(evt: MessageEvent) {
const message = evt.data;
if (!message || !message.type || message.type !== EdgeAuthTokenRequest) {
return;
}
if (!evt.source) {
return;
}
const token = this._getAuthCookieToken();
// @ts-ignore
evt.source.postMessage({ type: EdgeAuthTokenResponse, data: { token }}, evt.origin);
} }
_onConnectionMessage(evt) { _onConnectionMessage(evt) {
@ -216,7 +298,7 @@ export class Client extends EventTarget {
return this._conn !== null; return this._conn !== null;
} }
upload(blob: string|Blob, metadata: any) { upload(blob: string | Blob, metadata: any) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const formData = new FormData(); const formData = new FormData();
formData.set("file", blob); formData.set("file", blob);
@ -224,7 +306,7 @@ export class Client extends EventTarget {
if (metadata) { if (metadata) {
try { try {
formData.set("metadata", JSON.stringify(metadata)); formData.set("metadata", JSON.stringify(metadata));
} catch(err) { } catch (err) {
return reject(err); return reject(err);
} }
} }
@ -239,7 +321,7 @@ export class Client extends EventTarget {
let data; let data;
try { try {
data = JSON.parse(xhr.responseText); data = JSON.parse(xhr.responseText);
} catch(err) { } catch (err) {
reject(err); reject(err);
return; return;
} }

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z" />
</svg>

After

Width:  |  Height:  |  Size: 346 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
</svg>

After

Width:  |  Height:  |  Size: 450 B

View File

@ -0,0 +1,9 @@
import UserCircleIcon from './user-circle.svg';
import MenuIcon from './menu.svg';
import CloudIcon from './cloud.svg';
import LoginIcon from './login.svg';
import HomeIcon from './home.svg';
import LinkIcon from './link.svg';
import LogoutIcon from './logout.svg';
export { UserCircleIcon, MenuIcon, CloudIcon, LoginIcon, HomeIcon, LinkIcon, LogoutIcon }

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
</svg>

After

Width:  |  Height:  |  Size: 376 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
</svg>

After

Width:  |  Height:  |  Size: 356 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
</svg>

After

Width:  |  Height:  |  Size: 362 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>

After

Width:  |  Height:  |  Size: 260 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
</svg>

After

Width:  |  Height:  |  Size: 687 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>

After

Width:  |  Height:  |  Size: 405 B

View File

@ -0,0 +1,130 @@
import { LitElement, html, css } from 'lit';
import { property, state } from 'lit/decorators.js';
export const EVENT_MENU_ITEM_SELECTED = 'menu-item-selected';
export const EVENT_MENU_ITEM_UNSELECTED = 'menu-item-unselected';
export class MenuItem extends LitElement {
@property({ attribute: 'icon-url', type: String })
iconUrl: string;
@property({ attribute: 'label', type: String })
label: string;
static styles = css`
:host {
display: inline-block;
height: 100%;
flex: 1;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-bottom: 1px solid rgb(229,231,235);
border-top: 10px solid transparent;
transition: all 100ms ease-out;
background-color: #fff;
}
:host(:hover) {
background-color: rgb(249,250,251);
}
:host(.selected) {
border-top: 10px solid #03A9F4;
border-bottom: 1px solid transparent;
background-color: #fff;
}
:host(.unselected) {
background-color: hsl(210 20% 95% / 1);
}
.menu-item-icon {
height: 30px;
width: 30px;
overflow: hidden;
}
.menu-item-icon > img {
width: 100%;
height: 100%;
}
.menu-item-panel {
display: none;
position: fixed;
top: 65px;
left: 0;
right: 0;
z-index: 9999;
background-color: #fff;
box-shadow: 0px 4px 5px 0px hsl(0deg 0% 0% / 10%);
max-height: 75%;
overflow-y: auto;
}
:host(.selected) .menu-item-panel {
display: block;
}
.menu-item-label {
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
color: black;
font-size: 14px;
margin: 3px 0;
}
`;
@state()
selected: boolean
constructor() {
super();
this.addEventListener('click', this._handleClick.bind(this));
}
render() {
return html`
<div class="menu-item-icon">
${
this.iconUrl ?
html`<img src="${this.iconUrl}" />` :
''
}
</div>
<div class="menu-item-label">
${this.label}
</div>
<div class="menu-item-panel">
<slot></slot>
</div>
`
}
_handleClick() {
if (this.selected) {
this.unselect();
} else {
this.select();
}
}
select() {
this.selected = true;
const event = new CustomEvent(EVENT_MENU_ITEM_SELECTED, {
bubbles: true,
composed: true,
detail: {
element: this,
}
});
this.dispatchEvent(event);
}
unselect() {
this.selected = false;
const event = new CustomEvent(EVENT_MENU_ITEM_UNSELECTED, {
bubbles: true,
composed: true,
detail: {
element: this,
}
});
this.dispatchEvent(event);
}
}

View File

@ -0,0 +1,63 @@
import { LitElement, html, css } from 'lit';
import { property, state } from 'lit/decorators.js';
import { LinkIcon } from './icons';
export class MenuSubItem extends LitElement {
@property({ attribute: 'label' })
label: string;
@property({ attribute: 'icon-url' })
iconUrl: string;
@property({ attribute: 'link-url' })
linkUrl: string;
@property({ attribute: 'inactive', type: Boolean })
inactive: boolean;
static styles = css`
:host {
display: block;
flex: 1;
cursor: pointer;
transition: all 100ms ease-out;
border-bottom: 1px solid rgb(229,231,235);
padding: 5px 0 5px 7px;
border-left: 5px solid transparent;
}
:host([inactive]) {
cursor: initial;
}
:host(:hover) {
border-left: 5px solid #03A9F4;
background-color: rgb(28 169 247 / 10%);
}
a {
font-size: 20px;
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
text-decoration: none;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
height: 40px;
color: black;
}
.edge-menu-sub-item-icon {
height: 25px;
width: 25px;
}
.edge-menu-sub-item-label {
margin-left: 5px;
}
`;
render() {
return html`
<a href="${this.linkUrl ? this.linkUrl : '#'}">
<img class="edge-menu-sub-item-icon" src="${this.iconUrl ? this.iconUrl : LinkIcon}" />
<span class="edge-menu-sub-item-label">${this.label}</span>
</a>
`
}
}

View File

@ -0,0 +1,239 @@
import { LitElement, html, css } from 'lit';
import { property, queryAll } from 'lit/decorators.js';
import { CloudIcon, HomeIcon, LoginIcon, LinkIcon, MenuIcon, UserCircleIcon, LogoutIcon } from './icons'
import { EVENT_MENU_ITEM_SELECTED, EVENT_MENU_ITEM_UNSELECTED, MenuItem } from './menu-item';
import { MenuSubItem } from './menu-sub-item';
interface Manifest {
id: string
description: string
metadata: { [key: string]: any }
tags: string[]
title: string
version: string
url?: string
}
interface Profile {
sub?: string
preferred_username?: string
iss?: string
edge_role?: string
edge_tenant?: string
edge_entrypoint?: string
}
const BASE_API_URL = '/edge/api/v1';
enum Roles {
visitor = 0,
user = 1,
superuser = 2,
admin = 3,
superadmin = 4
}
export class Menu extends LitElement {
@property({ attribute: 'app-icon-url', type: String })
appIconUrl: string;
@property({ attribute: 'app-title', type: String })
appTitle: string;
@property({ attribute: 'hidden', type: Boolean })
hidden: boolean;
@property()
_apps: Manifest[] = []
@property()
_profile: Profile
static styles = css`
:host {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
background-color: #fff;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
z-index: 9999;
}
:host([hidden]) {
display: none;
}
`;
@queryAll('edge-menu-item')
_menuItems: NodeListOf<MenuItem>
constructor() {
super();
this.addEventListener(EVENT_MENU_ITEM_SELECTED, this._handleMenuItemSelected.bind(this));
this.addEventListener(EVENT_MENU_ITEM_UNSELECTED, this._handleMenuItemUnselected.bind(this));
this._fetchApps();
this._fetchProfile();
}
render() {
const apps = this._renderApps()
return html`
<edge-menu-item name='menu' label="${ this.appTitle || "App" }" icon-url='${ this.appIconUrl || MenuIcon }'>
<edge-menu-sub-item name='home' label='Home' icon-url='${HomeIcon}' link-url='/'></edge-menu-sub-item>
<slot></slot>
</edge-menu-item>
${ this._renderApps() }
${ this._renderProfile() }
`;
}
_fetchApps() {
return fetch(`${BASE_API_URL}/apps`)
.then(res => res.json())
.then(result => {
if (result.error) {
throw new Error(`Unexpected server error: ${result.error.code}`);
}
return result.data?.manifests || [];
})
.then((manifests: Manifest[]) => {
const promises = manifests.map((m: Manifest) => {
const fetchOptions: RequestInit = {
method: 'POST',
body: JSON.stringify({}),
headers: {
'Content-Type': 'application/json',
}
};
return fetch(`${BASE_API_URL}/apps/${m.id}/url`, fetchOptions)
.then(res => res.json())
.then(result => {
if (result.error) {
throw new Error(`Unexpected server error: ${result.error.code}`);
}
m.url = result.data?.url;
return m;
})
;
});
return Promise.all(promises);
})
.then((manifests: Manifest[]) => {
this._apps = manifests;
})
.catch(err => console.error(err))
}
_fetchProfile() {
return fetch(`${BASE_API_URL}/profile`)
.then(res => res.json())
.then(result => {
if (result.error) {
switch (result.error.code) {
case "unauthorized":
return null;
default:
throw new Error(`Unexpected server error: ${result.error.code}`);
}
}
return result.data?.profile;
})
.then(profile => {
this._profile = profile;
})
.catch(err => console.error(err))
;
}
_renderApps() {
const apps = this._apps
.filter(manifest => this._canAccess(manifest))
.map(manifest => {
const iconUrl = ( ( manifest.url || '') + ( manifest.metadata?.paths?.icon || '' ) ) || LinkIcon;
return html`
<edge-menu-sub-item
name='${ manifest.id }'
label='${ manifest.title }'
icon-url='${ iconUrl }'
link-url='${ manifest.url || '#' }'>
</edge-menu-sub-item>
`
});
return html`
<edge-menu-item name='apps' label='Apps' icon-url='${CloudIcon}'>
${ apps }
</edge-menu-item>
`;
}
_canAccess(manifest: Manifest): boolean {
const currentRole = this._profile?.edge_role || 'visitor';
const minimumRole = manifest.metadata?.minimumRole || 'visitor';
return Roles[currentRole] >= Roles[minimumRole];
}
_renderProfile() {
const profile = this._profile;
return html`
<edge-menu-item name='profile' label="${profile?.preferred_username || 'Profile'}" icon-url='${UserCircleIcon}'>
${
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='Login' icon-url='${LoginIcon}' link-url='/edge/auth/login'></edge-menu-sub-item>`
}
</edge-menu-item>
`;
}
_handleMenuItemSelected(evt: CustomEvent) {
const selectedMenuItem: HTMLElement = evt.detail.element;
selectedMenuItem.classList.add('selected');
selectedMenuItem.classList.remove('unselected');
for (let item, i = 0; (item = this._menuItems[i]); i++) {
if (item === selectedMenuItem) continue;
item.unselect();
item.classList.add('unselected');
}
}
_handleMenuItemUnselected(evt: CustomEvent) {
const unselectedMenuItem: HTMLElement = evt.detail.element;
unselectedMenuItem.classList.remove('selected');
const hasSelectedItem = this.renderRoot.querySelectorAll('edge-menu-item.selected').length !== 0
if (hasSelectedItem) {
return
}
for (let item, i = 0; (item = this._menuItems[i]); i++) {
item.classList.remove('unselected');
}
}
}
declare global {
interface HTMLElementTagNameMap {
"edge-menu": Menu;
"edge-menu-item": MenuItem;
"edge-menu-sub-item": MenuSubItem;
}
}

View File

@ -37,6 +37,10 @@ export class CrossFrameMessenger extends EventTarget {
_handleWindowMessage(evt: MessageEvent) { _handleWindowMessage(evt: MessageEvent) {
const message = evt.data; const message = evt.data;
if (!message || !message.type || !message.data) {
return;
}
const event = new CustomEvent(message.type, { const event = new CustomEvent(message.type, {
cancelable: true, cancelable: true,
detail: message.data detail: message.data
@ -67,14 +71,9 @@ export class CrossFrameMessenger extends EventTarget {
_initResizeObserver() { _initResizeObserver() {
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
const body = document.body, const rect = document.documentElement.getBoundingClientRect();
html = document.documentElement; const height = rect.height;
const width = rect.width;
const height = Math.max( body.scrollHeight, body.offsetHeight,
html.clientHeight, html.scrollHeight, html.offsetHeight );
const width = Math.max( body.scrollWidth, body.offsetWidth,
html.clientWidth, html.scrollWidth, html.offsetWidth );
this.post({ type: CrossFrameMessageType.SIZE_CHANGED, data: { height, width }}); this.post({ type: CrossFrameMessageType.SIZE_CHANGED, data: { height, width }});
}); });
@ -83,6 +82,6 @@ export class CrossFrameMessenger extends EventTarget {
if (!body) return; if (!body) return;
resizeObserver.observe(body); resizeObserver.observe(document.documentElement);
} }
} }

4
pkg/sdk/client/src/index.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.svg' {
const content: string;
export default content;
}

View File

@ -1,5 +1,16 @@
import { Client } from './client.js'; import './polyfill';
import { CrossFrameMessenger } from './crossframe-messenger.js';
export const client = new Client(); import { Client as EdgeClient } from './client.js';
export const crossFrameMessenger = new CrossFrameMessenger(); import { Menu as MenuElement } from './components/menu.js';
import { MenuItem as MenuItemElement } from './components/menu-item.js';
import { MenuSubItem as MenuSubItemElement } from './components/menu-sub-item.js';
import { CrossFrameMessenger } from './crossframe-messenger.js';
import { MenuManager } from './menu-manager.js';
customElements.define('edge-menu', MenuElement);
customElements.define('edge-menu-item', MenuItemElement);
customElements.define('edge-menu-sub-item', MenuSubItemElement);
export const Client = new EdgeClient();
export const Frame = new CrossFrameMessenger();
export const Menu = new MenuManager();

View File

@ -0,0 +1,144 @@
import { Menu } from "./components/menu"
export interface MenuItem {
label: string
iconUrl: string
linkUrl: string
order: number
}
const EdgeBodyAutoPaddingAttrName = 'edge-auto-padding'
export class MenuManager {
_items: { [name:string]: MenuItem }
_menu: Menu
_appIconUrl: string
_appTitle: string
_hidden: boolean
_previousBodyAutoPadding: string
constructor() {
this._items = {};
this._handleLoad = this._handleLoad.bind(this);
window.addEventListener('load', this._handleLoad);
}
setItem(name: string, label:string, options?: { iconUrl?: string, linkUrl?: string, order?: number }) {
this._items[name] = {
label: label,
iconUrl: options?.iconUrl ? options?.iconUrl : '',
linkUrl: options?.linkUrl ? options?.linkUrl : '#',
order: options?.order ? options?.order : 0,
}
this._render();
return this;
}
removeItem(name: string) {
delete this._items[name];
this._render();
return this;
}
setAppIconUrl(url: string) {
this._appIconUrl = url;
this._render();
return this;
}
setAppTitle(title: string) {
this._appTitle = title;
this._render();
return this;
}
show() {
if (!this._hidden) return;
this._hidden = false;
if (this._previousBodyAutoPadding) {
document.body.setAttribute(EdgeBodyAutoPaddingAttrName, this._previousBodyAutoPadding);
} else {
document.body.removeAttribute(EdgeBodyAutoPaddingAttrName);
}
this._render();
}
hide() {
if (this._hidden) return;
this._hidden = true;
this._previousBodyAutoPadding = document.body.getAttribute(EdgeBodyAutoPaddingAttrName);
document.body.setAttribute(EdgeBodyAutoPaddingAttrName, "false");
this._render();
}
_handleLoad() {
this._init();
}
_init() {
this._initMenu();
this._initGlobalStyle();
}
_initMenu() {
const menu = document.createElement('edge-menu');
document.body.appendChild(menu);
this._menu = menu;
this._render();
}
_initGlobalStyle() {
const style = document.createElement('style');
style.textContent = `
body:not([${EdgeBodyAutoPaddingAttrName}="false"]) {
padding-top: 60px;
}
`;
document.head.appendChild(style);
}
_render() {
if (!this._menu) return;
if (this._hidden) {
this._menu.setAttribute("hidden", "true");
} else {
this._menu.removeAttribute("hidden");
}
if (this._appIconUrl) {
this._menu.setAttribute("app-icon-url", this._appIconUrl);
} else {
this._menu.removeAttribute("app-icon-url");
}
if (this._appTitle) {
this._menu.setAttribute("app-title", this._appTitle);
} else {
this._menu.removeAttribute("app-title");
}
const children: Node[] = [];
const items: MenuItem[] = Object.keys(this._items)
.map(key => ({ name: key, ...this._items[key] }))
.sort((a, b) => a.order - b.order)
;
for (let item: MenuItem, i = 0; (item = items[i]); i++) {
const node = document.createElement('edge-menu-sub-item');
node.label = item.label;
node.iconUrl = item.iconUrl;
node.linkUrl = item.linkUrl;
children.push(node);
}
this._menu.replaceChildren(...children);
}
}

View File

@ -0,0 +1,4 @@
import 'core-js/actual';
import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js';
import 'lit/polyfill-support.js'
import '@webcomponents/webcomponentsjs/webcomponents-loader.js';

View File

@ -0,0 +1,35 @@
package driver
import (
"net/url"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
var blobStoreFactories = make(map[string]BlobStoreFactory, 0)
type BlobStoreFactory func(url *url.URL) (storage.BlobStore, error)
func RegisterBlobStoreFactory(scheme string, factory BlobStoreFactory) {
blobStoreFactories[scheme] = factory
}
func NewBlobStore(dsn string) (storage.BlobStore, error) {
url, err := url.Parse(dsn)
if err != nil {
return nil, errors.WithStack(err)
}
factory, exists := blobStoreFactories[url.Scheme]
if !exists {
return nil, errors.WithStack(ErrSchemeNotRegistered)
}
store, err := factory(url)
if err != nil {
return nil, errors.WithStack(err)
}
return store, nil
}

View File

@ -0,0 +1,35 @@
package driver
import (
"net/url"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
var documentStoreFactories = make(map[string]DocumentStoreFactory, 0)
type DocumentStoreFactory func(url *url.URL) (storage.DocumentStore, error)
func RegisterDocumentStoreFactory(scheme string, factory DocumentStoreFactory) {
documentStoreFactories[scheme] = factory
}
func NewDocumentStore(dsn string) (storage.DocumentStore, error) {
url, err := url.Parse(dsn)
if err != nil {
return nil, errors.WithStack(err)
}
factory, exists := documentStoreFactories[url.Scheme]
if !exists {
return nil, errors.WithStack(ErrSchemeNotRegistered)
}
store, err := factory(url)
if err != nil {
return nil, errors.WithStack(err)
}
return store, nil
}

View File

@ -0,0 +1,5 @@
package driver
import "errors"
var ErrSchemeNotRegistered = errors.New("scheme was not registered")

View File

@ -0,0 +1,239 @@
package client
import (
"context"
"io"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/blob"
"github.com/pkg/errors"
)
type BlobBucket struct {
name string
id blob.BucketID
call CallFunc
}
// Size implements storage.BlobBucket
func (b *BlobBucket) Size(ctx context.Context) (int64, error) {
args := blob.GetBucketSizeArgs{
BucketID: b.id,
}
reply := blob.GetBucketSizeReply{}
if err := b.call(ctx, "Service.GetBucketSize", args, &reply); err != nil {
return 0, errors.WithStack(err)
}
return reply.Size, nil
}
// Name implements storage.BlobBucket
func (b *BlobBucket) Name() string {
return b.name
}
// Close implements storage.BlobBucket
func (b *BlobBucket) Close() error {
args := blob.CloseBucketArgs{
BucketID: b.id,
}
reply := blob.CloseBucketReply{}
if err := b.call(context.Background(), "Service.CloseBucket", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
// Delete implements storage.BlobBucket
func (b *BlobBucket) Delete(ctx context.Context, id storage.BlobID) error {
args := blob.DeleteBucketArgs{
BucketName: b.name,
}
reply := blob.DeleteBucketReply{}
if err := b.call(context.Background(), "Service.DeleteBucket", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
// Get implements storage.BlobBucket
func (b *BlobBucket) Get(ctx context.Context, id storage.BlobID) (storage.BlobInfo, error) {
args := blob.GetBlobInfoArgs{
BucketID: b.id,
BlobID: id,
}
reply := blob.GetBlobInfoReply{}
if err := b.call(context.Background(), "Service.GetBlobInfo", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.BlobInfo, nil
}
// List implements storage.BlobBucket
func (b *BlobBucket) List(ctx context.Context) ([]storage.BlobInfo, error) {
args := blob.ListBlobInfoArgs{
BucketID: b.id,
}
reply := blob.ListBlobInfoReply{}
if err := b.call(context.Background(), "Service.ListBlobInfo", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.BlobInfos, nil
}
// NewReader implements storage.BlobBucket
func (b *BlobBucket) NewReader(ctx context.Context, id storage.BlobID) (io.ReadSeekCloser, error) {
args := blob.NewBlobReaderArgs{
BucketID: b.id,
BlobID: id,
}
reply := blob.NewBlobReaderReply{}
if err := b.call(context.Background(), "Service.NewBlobReader", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return &blobReaderCloser{
readerID: reply.ReaderID,
call: b.call,
}, nil
}
// NewWriter implements storage.BlobBucket
func (b *BlobBucket) NewWriter(ctx context.Context, id storage.BlobID) (io.WriteCloser, error) {
args := blob.NewBlobWriterArgs{
BucketID: b.id,
BlobID: id,
}
reply := blob.NewBlobWriterReply{}
if err := b.call(context.Background(), "Service.NewBlobWriter", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return &blobWriterCloser{
blobID: id,
writerID: reply.WriterID,
call: b.call,
}, nil
}
type blobWriterCloser struct {
blobID storage.BlobID
writerID blob.WriterID
call CallFunc
}
// Write implements io.WriteCloser
func (bwc *blobWriterCloser) Write(data []byte) (int, error) {
args := blob.WriteBlobArgs{
WriterID: bwc.writerID,
Data: data,
}
reply := blob.WriteBlobReply{}
if err := bwc.call(context.Background(), "Service.WriteBlob", args, &reply); err != nil {
return 0, errors.WithStack(err)
}
return reply.Written, nil
}
// Close implements io.WriteCloser
func (bwc *blobWriterCloser) Close() error {
args := blob.CloseWriterArgs{
WriterID: bwc.writerID,
}
reply := blob.CloseBucketReply{}
if err := bwc.call(context.Background(), "Service.CloseWriter", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
type blobReaderCloser struct {
readerID blob.ReaderID
call func(ctx context.Context, serviceMethod string, args any, reply any) error
}
// Read implements io.ReadSeekCloser
func (brc *blobReaderCloser) Read(p []byte) (int, error) {
args := blob.ReadBlobArgs{
ReaderID: brc.readerID,
Length: len(p),
}
reply := blob.ReadBlobReply{}
if err := brc.call(context.Background(), "Service.ReadBlob", args, &reply); err != nil {
return 0, errors.WithStack(err)
}
copy(p, reply.Data)
if reply.EOF {
return reply.Read, io.EOF
}
return reply.Read, nil
}
// Seek implements io.ReadSeekCloser
func (brc *blobReaderCloser) Seek(offset int64, whence int) (int64, error) {
args := blob.SeekBlobArgs{
ReaderID: brc.readerID,
Offset: offset,
Whence: whence,
}
reply := blob.SeekBlobReply{}
if err := brc.call(context.Background(), "Service.SeekBlob", args, &reply); err != nil {
return 0, errors.WithStack(err)
}
return reply.Read, nil
}
// Close implements io.ReadSeekCloser
func (brc *blobReaderCloser) Close() error {
args := blob.CloseReaderArgs{
ReaderID: brc.readerID,
}
reply := blob.CloseReaderReply{}
if err := brc.call(context.Background(), "Service.CloseReader", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
var (
_ storage.BlobBucket = &BlobBucket{}
_ storage.BlobInfo = &BlobInfo{}
_ io.WriteCloser = &blobWriterCloser{}
_ io.ReadSeekCloser = &blobReaderCloser{}
)

View File

@ -0,0 +1,40 @@
package client
import (
"time"
"forge.cadoles.com/arcad/edge/pkg/storage"
)
type BlobInfo struct {
id storage.BlobID
bucket string
contentType string
modTime time.Time
size int64
}
// Bucket implements storage.BlobInfo
func (i *BlobInfo) Bucket() string {
return i.bucket
}
// ID implements storage.BlobInfo
func (i *BlobInfo) ID() storage.BlobID {
return i.id
}
// ContentType implements storage.BlobInfo
func (i *BlobInfo) ContentType() string {
return i.contentType
}
// ModTime implements storage.BlobInfo
func (i *BlobInfo) ModTime() time.Time {
return i.modTime
}
// Size implements storage.BlobInfo
func (i *BlobInfo) Size() int64 {
return i.size
}

View File

@ -0,0 +1,101 @@
package client
import (
"context"
"net/url"
"github.com/keegancsmith/rpc"
"gitlab.com/wpetit/goweb/logger"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/blob"
"github.com/pkg/errors"
)
type BlobStore struct {
serverURL *url.URL
}
// DeleteBucket implements storage.BlobStore.
func (s *BlobStore) DeleteBucket(ctx context.Context, name string) error {
args := &blob.DeleteBucketArgs{
BucketName: name,
}
if err := s.call(ctx, "Service.DeleteBucket", args, nil); err != nil {
return errors.WithStack(err)
}
return nil
}
// ListBuckets implements storage.BlobStore.
func (s *BlobStore) ListBuckets(ctx context.Context) ([]string, error) {
args := &blob.ListBucketsArgs{}
reply := blob.ListBucketsReply{}
if err := s.call(ctx, "Service.ListBuckets", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.Buckets, nil
}
// OpenBucket implements storage.BlobStore.
func (s *BlobStore) OpenBucket(ctx context.Context, name string) (storage.BlobBucket, error) {
args := &blob.OpenBucketArgs{
BucketName: name,
}
reply := &blob.OpenBucketReply{}
if err := s.call(ctx, "Service.OpenBucket", args, reply); err != nil {
return nil, errors.WithStack(err)
}
return &BlobBucket{
name: name,
id: reply.BucketID,
call: s.call,
}, nil
}
func (s *BlobStore) call(ctx context.Context, serviceMethod string, args any, reply any) error {
err := s.withClient(ctx, func(ctx context.Context, client *rpc.Client) error {
if err := client.Call(ctx, serviceMethod, args, reply); err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *BlobStore) withClient(ctx context.Context, fn func(ctx context.Context, client *rpc.Client) error) error {
client, err := rpc.DialHTTPPath("tcp", s.serverURL.Host, s.serverURL.Path+"?"+s.serverURL.RawQuery)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := client.Close(); err != nil {
logger.Error(ctx, "could not close rpc client", logger.E(errors.WithStack(err)))
}
}()
if err := fn(ctx, client); err != nil {
return errors.WithStack(err)
}
return nil
}
func NewBlobStore(serverURL *url.URL) *BlobStore {
return &BlobStore{serverURL}
}
var _ storage.BlobStore = &BlobStore{}

View File

@ -0,0 +1,87 @@
package client
import (
"context"
"fmt"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage/testsuite"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func TestBlobStore(t *testing.T) {
t.Parallel()
if testing.Verbose() {
logger.SetLevel(logger.LevelDebug)
}
httpServer, err := startNewBlobStoreServer()
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
defer httpServer.Close()
serverAddr := httpServer.Listener.Addr()
serverURL := &url.URL{
Host: serverAddr.String(),
}
store := NewBlobStore(serverURL)
testsuite.TestBlobStore(context.Background(), t, store)
}
func BenchmarkBlobStore(t *testing.B) {
logger.SetLevel(logger.LevelError)
httpServer, err := startNewBlobStoreServer()
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
defer httpServer.Close()
serverAddr := httpServer.Listener.Addr()
serverURL := &url.URL{
Host: serverAddr.String(),
}
store := NewBlobStore(serverURL)
testsuite.BenchmarkBlobStore(t, store)
}
func getSQLiteBlobStore() (*sqlite.BlobStore, error) {
file := "./testdata/blobstore_test.sqlite"
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, errors.WithStack(err)
}
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
store := sqlite.NewBlobStore(dsn)
return store, nil
}
func startNewBlobStoreServer() (*httptest.Server, error) {
store, err := getSQLiteBlobStore()
if err != nil {
return nil, errors.WithStack(err)
}
server := server.NewBlobStoreServer(store)
httpServer := httptest.NewServer(server)
return httpServer, nil
}

View File

@ -0,0 +1,134 @@
package client
import (
"context"
"net/url"
"github.com/keegancsmith/rpc"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/document"
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
)
type DocumentStore struct {
serverURL *url.URL
}
// Delete implements storage.DocumentStore.
func (s *DocumentStore) Delete(ctx context.Context, collection string, id storage.DocumentID) error {
args := document.DeleteDocumentArgs{
Collection: collection,
DocumentID: id,
}
reply := document.DeleteDocumentReply{}
if err := s.call(ctx, "Service.DeleteDocument", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
// Get implements storage.DocumentStore.
func (s *DocumentStore) Get(ctx context.Context, collection string, id storage.DocumentID) (storage.Document, error) {
args := document.GetDocumentArgs{
Collection: collection,
DocumentID: id,
}
reply := document.GetDocumentReply{}
if err := s.call(ctx, "Service.GetDocument", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.Document, nil
}
// Query implements storage.DocumentStore.
func (s *DocumentStore) Query(ctx context.Context, collection string, filter *filter.Filter, funcs ...storage.QueryOptionFunc) ([]storage.Document, error) {
opts := &storage.QueryOptions{}
for _, fn := range funcs {
fn(opts)
}
args := document.QueryDocumentsArgs{
Collection: collection,
Filter: nil,
Options: opts,
}
if filter != nil {
args.Filter = filter.AsMap()
}
reply := document.QueryDocumentsReply{
Documents: []storage.Document{},
}
if err := s.call(ctx, "Service.QueryDocuments", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.Documents, nil
}
// Upsert implements storage.DocumentStore.
func (s *DocumentStore) Upsert(ctx context.Context, collection string, doc storage.Document) (storage.Document, error) {
args := document.UpsertDocumentArgs{
Collection: collection,
Document: doc,
}
reply := document.UpsertDocumentReply{}
if err := s.call(ctx, "Service.UpsertDocument", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.Document, nil
}
func (s *DocumentStore) call(ctx context.Context, serviceMethod string, args any, reply any) error {
err := s.withClient(ctx, func(ctx context.Context, client *rpc.Client) error {
if err := client.Call(ctx, serviceMethod, args, reply); err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *DocumentStore) withClient(ctx context.Context, fn func(ctx context.Context, client *rpc.Client) error) error {
client, err := rpc.DialHTTPPath("tcp", s.serverURL.Host, s.serverURL.Path+"?"+s.serverURL.RawQuery)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := client.Close(); err != nil {
logger.Error(ctx, "could not close rpc client", logger.E(errors.WithStack(err)))
}
}()
if err := fn(ctx, client); err != nil {
return errors.WithStack(err)
}
return nil
}
func NewDocumentStore(url *url.URL) *DocumentStore {
return &DocumentStore{url}
}
var _ storage.DocumentStore = &DocumentStore{}

Some files were not shown because too many files have changed in this diff Show More