Compare commits

...

70 Commits

Author SHA1 Message Date
c3535a4a9b feat(http): allow passing middlewares via options
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-09-20 09:23:53 -06:00
7e58551f6a docs(context): remove reference to obsolete attribute
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-09-20 09:02:27 -06:00
41d5db6321 docs(auth): add informations about anonymous users
ref arcad/edge-menu#86
2023-09-20 09:01:36 -06:00
8eb441daee feat(auth): automatically generate anonymous user session
All checks were successful
arcad/edge/pipeline/head This commit looks good
ref arcad/edge-menu#86
2023-09-20 08:55:49 -06:00
17808d14c9 fix: prevent bus congestion by flushing out messages
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-26 15:53:23 +02:00
ba9ae6e391 fix(app): use event loop runtime for every operations
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-24 12:16:30 +02:00
abc60b9ae3 fix(module,app): use whole remote address if splitting fail
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-21 20:01:43 +02:00
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
f5283b86ed fix(app,manifest): manifest serialization
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-11 15:08:07 +02:00
98ebd7a168 doc(app,manifest): add metadata attribute in manifest schema
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-11 11:11:00 +02:00
8ca31d05c0 feat(app,manifest): validation + extendable metadatas
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-11 11:05:09 +02:00
34c6a089b5 fix(client,sdk): permit cross-domain message communication
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-06 20:54:01 +02:00
da73b842e1 fix(sdk,client): initialize crossframe observers after window load event
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-06 19:18:36 +02:00
55d7241d95 chore(sdk,client): remove restrictive assertion
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-06 18:18:12 +02:00
240b07af66 feat(sdk,client): add edgeframe sdk api
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-06 18:16:17 +02:00
68e35bf5a6 fix(client,sdk): remove too specific assertion
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-06 15:59:09 +02:00
4bc2d864ad chore: add jenkins ci pipeline
All checks were successful
arcad/edge/pipeline/head This commit looks good
2023-04-06 14:58:12 +02:00
dc18381dea chore: add test timeout 2023-04-06 14:47:37 +02:00
1dde96043a chore: reenable tests in watch mode 2023-04-06 14:47:13 +02:00
f758acb4e5 fix(module,fetch): wait for module initialization to prevent false failure in test 2023-04-06 14:46:46 +02:00
054e80bbfb fix(storage,sqlite): prevent 'database is busy' error by using busy_timeout pragma 2023-04-06 14:45:50 +02:00
32c6f0a77e feat(cli,run): resolve app url based on available network interfaces 2023-04-06 11:52:04 +02:00
050e529f0a doc(module,app): add new parameter 'from' to app.getUrl() 2023-04-06 11:27:27 +02:00
006f13bc7b feat(module,auth): dynamically define authentication cookie domain 2023-04-05 15:19:22 +02:00
84c8fd51f6 chore: add cast commands for testing purpose 2023-04-05 15:12:51 +02:00
f08f645432 chore: fix gitea-release task 2023-04-02 18:01:47 +02:00
fbb27d6ea4 feat(app,module): fetch basic module 2023-04-02 17:59:33 +02:00
d8ce2901d2 feat(jwt): handle nil keyset 2023-03-28 20:38:29 +02:00
1996f4dc56 feat(auth,local_handler): cookie configuration 2023-03-28 20:37:57 +02:00
e09de0b0a4 chore: move proxy package to arcad/emissary 2023-03-28 10:15:49 +02:00
72765de20b fix(doc): typo 2023-03-24 11:05:17 +01:00
ed535b6f5d feat(module,app): basic module to list apps 2023-03-24 10:59:15 +01:00
07452ad8ab chore: automatically update sdk-testsuite app version 2023-03-23 19:13:47 +01:00
0f0fdfb02b chore: use date based versioning 2023-03-23 19:04:29 +01:00
9eefce9b41 feat: remove arbitrary timeout 2023-03-23 19:01:48 +01:00
0577762be9 feat(module,blob): implements full api 2023-03-23 19:01:20 +01:00
cf8a3f8ac0 feat: add proxy package 2023-03-22 18:05:44 +01:00
1f4f795d43 feat: add basic local login handler in dev cli 2023-03-20 21:42:39 +01:00
fd12d2ba42 feat(module,auth): use dummy getclaim func 2023-03-10 14:33:12 +01:00
6399196fe5 chore(sqlite): add Open() utility func 2023-03-03 16:37:19 +01:00
fef0321475 fix(http,client): add js generated client to repository 2023-03-03 16:33:24 +01:00
a9d2c282f2 chore: remove obsolete api 2023-03-03 15:55:23 +01:00
0bb7f2cd85 chore: refactor client sdk tests 2023-03-03 11:42:20 +01:00
b61bf52df9 fix(module,net): add missing context to server message
fix #6
2023-03-03 10:43:13 +01:00
f1dd467c95 fix(doc): typo 2023-03-01 15:15:47 +01:00
3136d71032 feat(http): handle html5 routing
ref #5
2023-03-01 14:39:45 +01:00
8680e139e7 fix(storage,document,sqlite): fix nil pointer exception in query options 2023-03-01 13:06:32 +01:00
8789b85d92 feat(server): handle panic in runtime
ref #2
2023-03-01 13:04:40 +01:00
fefcba5901 Merge pull request 'feat(storage,document,sqlite): implements order by and limit' (#4) from issue-3 into master
Reviewed-on: #4
2023-03-01 12:13:32 +01:00
5ad4ab2e23 feat(storage,document,sqlite): implements order by and limit 2023-03-01 12:12:11 +01:00
4eb1f8fc90 fix(doc): typo 2023-02-24 14:44:20 +01:00
640f429580 feat(module,auth): authentication module with arbitrary claims support 2023-02-24 14:40:28 +01:00
7f07e52ae0 chore: remove obsolete modules 2023-02-24 10:39:46 +01:00
c4865c149f feat(store,document,sqlite): log upsert query in debug level 2023-02-24 10:38:48 +01:00
b9f985ab0c fix(module,store): allow querying on _id, _createdAt, _updatedAt attributes 2023-02-21 15:05:01 +01:00
182 changed files with 9520 additions and 1837 deletions

1
.env.dist Normal file
View File

@ -0,0 +1 @@
RUN_APP_ARGS=""

2
.gitignore vendored
View File

@ -1,7 +1,7 @@
/node_modules /node_modules
/bin /bin
/pkg/sdk/client/dist
/.env /.env
/tools /tools
*.sqlite *.sqlite
/.gitea-release /.gitea-release
/.edge

49
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,49 @@
@Library('cadoles') _
pipeline {
agent {
dockerfile {
label 'docker'
filename 'Dockerfile'
dir 'misc/jenkins'
}
}
stages {
stage('Run unit tests') {
steps {
script {
sh 'make GOTEST_ARGS="-timeout 10m -count=1 -v" test'
}
}
}
stage('Release') {
when {
anyOf {
branch 'master'
branch 'develop'
}
}
steps {
script {
withCredentials([
usernamePassword([
credentialsId: 'forge-jenkins',
usernameVariable: 'GITEA_RELEASE_USERNAME',
passwordVariable: 'GITEA_RELEASE_PASSWORD'
])
]) {
sh 'make gitea-release'
}
}
}
}
}
post {
always {
cleanWs()
}
}
}

View File

@ -2,16 +2,21 @@ LINT_ARGS ?= --timeout 5m
GITCHLOG_ARGS ?= GITCHLOG_ARGS ?=
SHELL := /bin/bash SHELL := /bin/bash
GOTEST_ARGS ?= -short GOTEST_ARGS ?= -short -timeout 60s
ESBUILD_VERSION ?= v0.17.5 ESBUILD_VERSION ?= v0.17.5
GIT_VERSION := $(shell git describe --always) GIT_VERSION := $(shell git describe --always)
DATE_VERSION := $(shell date +%Y.%-m.%-d)
FULL_VERSION := v$(DATE_VERSION)-$(GIT_VERSION)$(if $(shell git diff --stat),-dirty,)
APP_PATH ?= misc/client-sdk-testsuite/dist
RUN_APP_ARGS ?=
SHELL := bash
build: build-edge-cli build: build-edge-cli build-client-sdk-test-app
watch: watch: tools/modd/bin/modd
go run -mod=readonly github.com/cortesi/modd/cmd/modd@latest tools/modd/bin/modd
.PHONY: test .PHONY: test
test: test-go test: test-go
@ -28,10 +33,12 @@ build-edge-cli: build-sdk
-o ./bin/cli \ -o ./bin/cli \
./cmd/cli ./cmd/cli
build-client-sdk-test-app:
cd misc/client-sdk-testsuite && $(MAKE) dist
install-git-hooks: install-git-hooks:
git config core.hooksPath .githooks git config core.hooksPath .githooks
tools/esbuild/bin/esbuild: tools/esbuild/bin/esbuild:
mkdir -p tools/esbuild/bin mkdir -p tools/esbuild/bin
curl -fsSL https://esbuild.github.io/dl/$(ESBUILD_VERSION) | sh curl -fsSL https://esbuild.github.io/dl/$(ESBUILD_VERSION) | sh
@ -44,41 +51,58 @@ 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="Edge=Edge.default;" \ --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
gitea-release: tools/gitea-release/bin/gitea-release.sh build run-app: .env
( set -o allexport && source .env && set +o allexport && bin/cli app run -p $(APP_PATH) $$RUN_APP_ARGS )
.env:
cp .env.dist .env
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/*
cp bin/cli .gitea-release/edge_cli_amd64 cp bin/cli .gitea-release/edge_cli_amd64
# Create client-sdk-testsuite package # Create client-sdk-testsuite package
tools/yq/bin/yq -i '.version = "$(FULL_VERSION)"' ./misc/client-sdk-testsuite/dist/manifest.yml
.gitea-release/edge_cli_amd64 app package -d ./misc/client-sdk-testsuite/dist -o .gitea-release .gitea-release/edge_cli_amd64 app package -d ./misc/client-sdk-testsuite/dist -o .gitea-release
GITEA_RELEASE_PROJECT="edge" \ GITEA_RELEASE_PROJECT="edge" \
GITEA_RELEASE_ORG="arcad" \ GITEA_RELEASE_ORG="arcad" \
GITEA_RELEASE_BASE_URL="https://forge.cadoles.com" \ GITEA_RELEASE_BASE_URL="https://forge.cadoles.com" \
GITEA_RELEASE_VERSION="$(GIT_VERSION)" \ GITEA_RELEASE_VERSION="$(FULL_VERSION)" \
GITEA_RELEASE_NAME="$(GIT_VERSION)" \ GITEA_RELEASE_NAME="$(FULL_VERSION)" \
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \ GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \
GITEA_RELEASE_IS_DRAFT="false" \ GITEA_RELEASE_IS_DRAFT="false" \
GITEA_RELEASE_IS_PRERELEASE="true" \ GITEA_RELEASE_IS_PRERELEASE="true" \
GITEA_RELEASE_BODY="" \ GITEA_RELEASE_BODY="" \
GITEA_RELEASE_ATTACHMENTS="$(shell find .gitea-release/* -type f)" \ GITEA_RELEASE_ATTACHMENTS="$$(find .gitea-release/* -type f)" \
tools/gitea-release/bin/gitea-release.sh tools/gitea-release/bin/gitea-release.sh
tools/gitea-release/bin/gitea-release.sh: tools/gitea-release/bin/gitea-release.sh:
mkdir -p tools/gitea-release/bin mkdir -p tools/gitea-release/bin
curl --output tools/gitea-release/bin/gitea-release.sh https://forge.cadoles.com/Cadoles/Jenkins/raw/branch/master/resources/com/cadoles/gitea/gitea-release.sh curl --output tools/gitea-release/bin/gitea-release.sh https://forge.cadoles.com/Cadoles/Jenkins/raw/branch/master/resources/com/cadoles/gitea/gitea-release.sh
chmod +x tools/gitea-release/bin/gitea-release.sh chmod +x tools/gitea-release/bin/gitea-release.sh
tools/yq/bin/yq:
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
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

@ -0,0 +1,11 @@
package app
import (
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/app/metadata"
)
var manifestMetadataValidators = []app.MetadataValidator{
metadata.WithMinimumRoleValidator("visitor", "user", "superuser", "admin", "superadmin"),
metadata.WithNamedPathsValidator(metadata.NamedPathAdmin, metadata.NamedPathIcon),
}

View File

@ -0,0 +1,50 @@
[
{
"username": "superadmin",
"algo": "argon2id",
"password": "$argon2id$v=19$m=65536,t=3,p=2$cWOxfEyBy4EyKZR5usB2Pw$xG+Z/E2DUJP9kF0s1fhZjIuP03gFQ65dP7pHRJz7eR8",
"claims": {
"edge_entrypoint": "edge",
"edge_role": "superadmin",
"edge_tenant": "dev.cli",
"preferred_username": "SuperAdmin",
"sub": "superadmin"
}
},
{
"username": "admin",
"algo": "argon2id",
"password": "$argon2id$v=19$m=65536,t=3,p=2$WXXc4ECnkej6WO7f0Xya6Q$UG2wcGltJcuW0cNTR85mAl65tI1kFWMMw7ADS2FMOvY",
"claims": {
"edge_entrypoint": "edge",
"edge_role": "admin",
"edge_tenant": "dev.cli",
"preferred_username": "Admin",
"sub": "admin"
}
},
{
"username": "superuser",
"algo": "argon2id",
"password": "$argon2id$v=19$m=65536,t=3,p=2$gkDAWCzfU23+un3x0ny+YA$L/NSPrd5iKPK/UnSCKfSz4EO+v94N3LTLky4QGJOfpI",
"claims": {
"edge_entrypoint": "edge",
"edge_role": "superuser",
"edge_tenant": "dev.cli",
"preferred_username": "SuperUser",
"sub": "superuser"
}
},
{
"username": "user",
"algo": "argon2id",
"password": "$argon2id$v=19$m=65536,t=3,p=2$DhUm9qXUKP35Lzp5M37eZA$2+h6yDxSTHZqFZIuI7JZfFWozwrObna8a8yCgEEPlPE",
"claims": {
"edge_entrypoint": "edge",
"edge_role": "user",
"edge_tenant": "dev.cli",
"preferred_username": "User",
"sub": "user"
}
}
]

View File

@ -0,0 +1,47 @@
package app
import (
"fmt"
"forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id"
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain"
)
func HashPasswordCommand() *cli.Command {
return &cli.Command{
Name: "hash-password",
Usage: "Hash the provided password with the specified algorithm",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "password",
Usage: "hash `PASSWORD`",
Aliases: []string{"p"},
Value: "",
Required: true,
},
&cli.StringFlag{
Name: "algorithm",
Usage: fmt.Sprintf("use `ALGORITHM` to hash password (available: %v)", passwd.Algorithms()),
Aliases: []string{"a"},
Value: string(argon2id.Algo),
},
},
Action: func(ctx *cli.Context) error {
algo := ctx.String("algorithm")
password := ctx.String("password")
hash, err := passwd.Hash(passwd.Algo(algo), password)
if err != nil {
return errors.WithStack(err)
}
fmt.Println(hash)
return nil
},
}
}

View File

@ -47,11 +47,15 @@ func PackageCommand() *cli.Command {
bundle := bundle.NewDirectoryBundle(appDir) bundle := bundle.NewDirectoryBundle(appDir)
manifest, err := app.LoadAppManifest(bundle) manifest, err := app.LoadManifest(bundle)
if err != nil { if err != nil {
return errors.Wrap(err, "could not load app manifest") return errors.Wrap(err, "could not load app manifest")
} }
if valid, err := manifest.Validate(manifestMetadataValidators...); !valid {
return errors.Wrap(err, "invalid app manifest")
}
if err := os.MkdirAll(outputDir, 0o755); err != nil { if err := os.MkdirAll(outputDir, 0o755); err != nil {
return errors.Wrapf(err, "could not create directory ''%s'", outputDir) return errors.Wrapf(err, "could not create directory ''%s'", outputDir)
} }

View File

@ -11,6 +11,7 @@ func Root() *cli.Command {
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
RunCommand(), RunCommand(),
PackageCommand(), PackageCommand(),
HashPasswordCommand(),
}, },
} }
} }

View File

@ -1,24 +1,50 @@
package app package app
import ( import (
"database/sql" "context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"strconv"
"strings"
"sync"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bus"
"forge.cadoles.com/arcad/edge/pkg/bus/memory" "forge.cadoles.com/arcad/edge/pkg/bus/memory"
appHTTP "forge.cadoles.com/arcad/edge/pkg/http" appHTTP "forge.cadoles.com/arcad/edge/pkg/http"
"forge.cadoles.com/arcad/edge/pkg/module" "forge.cadoles.com/arcad/edge/pkg/module"
appModule "forge.cadoles.com/arcad/edge/pkg/module/app"
appModuleMemory "forge.cadoles.com/arcad/edge/pkg/module/app/memory"
authModule "forge.cadoles.com/arcad/edge/pkg/module/auth"
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/cast" "forge.cadoles.com/arcad/edge/pkg/module/cast"
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite" "forge.cadoles.com/arcad/edge/pkg/module/fetch"
netModule "forge.cadoles.com/arcad/edge/pkg/module/net"
shareModule "forge.cadoles.com/arcad/edge/pkg/module/share"
shareSqlite "forge.cadoles.com/arcad/edge/pkg/module/share/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage"
storageSqlite "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/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/jwk"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
_ "modernc.org/sqlite" _ "embed"
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id"
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain"
) )
func RunCommand() *cli.Command { func RunCommand() *cli.Command {
@ -26,15 +52,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",
}, },
@ -51,73 +77,411 @@ func RunCommand() *cli.Command {
&cli.StringFlag{ &cli.StringFlag{
Name: "storage-file", Name: "storage-file",
Usage: "use `FILE` for SQLite storage database", Usage: "use `FILE` for SQLite storage database",
Value: "data.sqlite", Value: ".edge/%APPID%/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
},
&cli.StringFlag{
Name: "shared-resources-file",
Usage: "use `FILE` for SQLite shared resources database",
Value: ".edge/shared-resources.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
},
&cli.StringFlag{
Name: "accounts-file",
Usage: "use `FILE` as local accounts",
Value: ".edge/%APPID%/accounts.json",
}, },
}, },
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")
storageFile := ctx.String("storage-file") storageFile := ctx.String("storage-file")
accountsFile := ctx.String("accounts-file")
sharedResourcesFile := ctx.String("shared-resources-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
host, portStr, err := net.SplitHostPort(address)
if err != nil {
return errors.WithStack(err)
}
port, err := strconv.ParseUint(portStr, 10, 32)
if err != nil {
return errors.WithStack(err)
}
manifests := make([]*app.Manifest, len(paths))
for idx, pth := range paths {
bdl, err := bundle.FromPath(pth)
if err != nil {
return errors.WithStack(err)
}
manifest, err := app.LoadManifest(bdl)
if err != nil {
return errors.WithStack(err)
}
manifests[idx] = manifest
}
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, storageFile, accountsFile, appsRepository, sharedResourcesFile); err != nil {
logger.Error(appCtx, "could not run app", logger.E(errors.WithStack(err)))
}
}(p, port, idx)
}
wg.Wait()
return nil
},
}
}
func runApp(ctx context.Context, path string, address string, storageFile string, accountsFile string, appRepository appModule.Repository, sharedResourcesFile string) error {
absPath, err := filepath.Abs(path) absPath, err := filepath.Abs(path)
if err != nil { if err != nil {
return errors.Wrapf(err, "could not resolve path '%s'", path) return errors.Wrapf(err, "could not resolve path '%s'", path)
} }
logger.Info(cmdCtx, "opening app bundle", logger.F("path", absPath)) logger.Info(ctx, "opening app bundle", logger.F("path", absPath))
bundle, err := bundle.FromPath(path) bundle, err := bundle.FromPath(path)
if err != nil { if err != nil {
return errors.Wrapf(err, "could not open path '%s' as an app bundle", path) return errors.Wrapf(err, "could not open path '%s' as an app bundle", path)
} }
mux := chi.NewMux() manifest, err := app.LoadManifest(bundle)
mux.Use(middleware.Logger)
bus := memory.NewBus()
db, err := sql.Open("sqlite", storageFile)
if err != nil { if err != nil {
return errors.Wrapf(err, "could not open database with path '%s'", storageFile) return errors.Wrap(err, "could not load manifest from app bundle")
} }
documentStore := sqlite.NewDocumentStoreWithDB(db) if valid, err := manifest.Validate(manifestMetadataValidators...); !valid {
blobStore := sqlite.NewBlobStoreWithDB(db) 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(storageFile, manifest.ID),
initAccounts(accountsFile, manifest.ID),
initShareRepository(sharedResourcesFile),
initAppRepository(appRepository),
}
for _, fn := range funcs {
if err := fn(deps); err != nil {
return errors.WithStack(err)
}
}
handler := appHTTP.NewHandler( handler := appHTTP.NewHandler(
appHTTP.WithBus(bus), appHTTP.WithBus(deps.Bus),
appHTTP.WithServerModules( appHTTP.WithServerModules(getServerModules(deps)...),
module.ContextModuleFactory(), appHTTP.WithHTTPMounts(
module.ConsoleModuleFactory(), appModule.Mount(appRepository),
cast.CastModuleFactory(), authModule.Mount(
module.LifecycleModuleFactory(bus), authHTTP.NewLocalHandler(
module.NetModuleFactory(bus), jwa.HS256, key,
module.RPCModuleFactory(bus), authHTTP.WithRoutePrefix("/auth"),
module.StoreModuleFactory(documentStore), authHTTP.WithAccounts(deps.Accounts...),
module.BlobModuleFactory(bus, blobStore), ),
authModule.WithJWT(dummyKeySet),
),
),
appHTTP.WithHTTPMiddlewares(
authModuleMiddleware.AnonymousUser(
jwa.HS256, key,
),
), ),
) )
if err := handler.Load(bundle); err != nil { if err := handler.Load(bundle); err != nil {
return errors.Wrap(err, "could not load app bundle") return errors.Wrap(err, "could not load app bundle")
} }
mux.Handle("/*", handler) router := chi.NewRouter()
router.Use(middleware.Logger)
router.Use(middleware.Compress(5))
logger.Info(cmdCtx, "listening", logger.F("address", address)) // Add app handler
router.Handle("/*", handler)
if err := http.ListenAndServe(address, mux); err != nil { logger.Info(ctx, "listening", logger.F("address", address))
if err := http.ListenAndServe(address, router); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
return nil return nil
}
type moduleDeps struct {
AppID app.ID
Bus bus.Bus
DocumentStore storage.DocumentStore
BlobStore storage.BlobStore
AppRepository appModule.Repository
ShareRepository shareModule.Repository
Accounts []authHTTP.LocalAccount
}
type ModuleDepFunc func(*moduleDeps) error
func getServerModules(deps *moduleDeps) []app.ServerModuleFactory {
return []app.ServerModuleFactory{
module.LifecycleModuleFactory(),
module.ContextModuleFactory(),
module.ConsoleModuleFactory(),
cast.CastModuleFactory(),
netModule.ModuleFactory(deps.Bus),
module.RPCModuleFactory(deps.Bus),
module.StoreModuleFactory(deps.DocumentStore),
blob.ModuleFactory(deps.Bus, deps.BlobStore),
authModule.ModuleFactory(
authModule.WithJWT(dummyKeySet),
),
appModule.ModuleFactory(deps.AppRepository),
fetch.ModuleFactory(deps.Bus),
shareModule.ModuleFactory(deps.AppID, deps.ShareRepository),
}
}
var dummySecret = []byte("not_so_secret")
func dummyKey() (jwk.Key, error) {
key, err := jwk.FromRaw(dummySecret)
if err != nil {
return nil, errors.WithStack(err)
}
return key, nil
}
func dummyKeySet() (jwk.Set, error) {
key, err := dummyKey()
if err != nil {
return nil, errors.WithStack(err)
}
if err := key.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
return nil, errors.WithStack(err)
}
set := jwk.NewSet()
if err := set.AddKey(key); err != nil {
return nil, errors.WithStack(err)
}
return set, nil
}
func ensureDir(path string) error {
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return errors.WithStack(err)
}
return nil
}
func injectAppID(str string, appID app.ID) string {
return strings.ReplaceAll(str, "%APPID%", string(appID))
}
//go:embed default-accounts.json
var defaultAccounts []byte
func loadLocalAccounts(path string) ([]authHTTP.LocalAccount, error) {
if err := ensureDir(path); err != nil {
return nil, errors.WithStack(err)
}
data, err := ioutil.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
if err := ioutil.WriteFile(path, defaultAccounts, 0o640); err != nil {
return nil, errors.WithStack(err)
}
data = defaultAccounts
} else {
return nil, errors.WithStack(err)
}
}
var accounts []authHTTP.LocalAccount
if err := json.Unmarshal(data, &accounts); err != nil {
return nil, errors.WithStack(err)
}
return accounts, nil
}
func findMatchingDeviceAddress(ctx context.Context, from string, defaultAddr string) (string, error) {
if from == "" {
return defaultAddr, nil
}
fromIP := net.ParseIP(from)
if fromIP == nil {
return defaultAddr, nil
}
ifaces, err := net.Interfaces()
if err != nil {
return "", errors.WithStack(err)
}
for _, ifa := range ifaces {
addrs, err := ifa.Addrs()
if err != nil {
logger.Error(
ctx, "could not retrieve iface adresses",
logger.E(errors.WithStack(err)), logger.F("iface", ifa.Name),
)
continue
}
for _, addr := range addrs {
ip, network, err := net.ParseCIDR(addr.String())
if err != nil {
logger.Error(
ctx, "could not parse address",
logger.E(errors.WithStack(err)), logger.F("address", addr.String()),
)
continue
}
if !network.Contains(fromIP) {
continue
}
if ip.To4() == nil {
continue
}
return ip.To4().String(), 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(storageFile string, appID app.ID) ModuleDepFunc {
return func(deps *moduleDeps) error {
storageFile = injectAppID(storageFile, appID)
if err := ensureDir(storageFile); err != nil {
return errors.WithStack(err)
}
db, err := storageSqlite.Open(storageFile)
if err != nil {
return errors.WithStack(err)
}
deps.DocumentStore = storageSqlite.NewDocumentStoreWithDB(db)
deps.BlobStore = storageSqlite.NewBlobStoreWithDB(db)
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
}
}
func initShareRepository(shareRepositoryFile string) ModuleDepFunc {
return func(deps *moduleDeps) error {
if err := ensureDir(shareRepositoryFile); err != nil {
return errors.WithStack(err)
}
repo := shareSqlite.NewRepository(shareRepositoryFile)
deps.ShareRepository = repo
return nil
} }
} }

View File

@ -0,0 +1,40 @@
package cast
import (
"forge.cadoles.com/arcad/edge/pkg/module/cast"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
func LoadURLCommand() *cli.Command {
return &cli.Command{
Name: "load-url",
Usage: "Load `URL` in casting device",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "device",
Aliases: []string{"d"},
Required: true,
},
&cli.StringFlag{
Name: "url",
Aliases: []string{"u"},
Required: true,
},
},
Action: func(ctx *cli.Context) error {
device := ctx.String("device")
url := ctx.String("url")
if err := cast.StopCast(ctx.Context, device); err != nil {
return errors.WithStack(err)
}
if err := cast.LoadURL(ctx.Context, device, url); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,16 @@
package cast
import (
"github.com/urfave/cli/v2"
)
func Root() *cli.Command {
return &cli.Command{
Name: "cast",
Usage: "Cast related commands",
Subcommands: []*cli.Command{
ScanCommand(),
LoadURLCommand(),
},
}
}

View File

@ -0,0 +1,42 @@
package cast
import (
"context"
"log"
"time"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
func ScanCommand() *cli.Command {
return &cli.Command{
Name: "scan",
Usage: "Scan network for casting devices",
Flags: []cli.Flag{
&cli.DurationFlag{
Name: "timeout",
Aliases: []string{"t"},
Value: 30 * time.Second,
},
},
Action: func(ctx *cli.Context) error {
timeout := ctx.Duration("timeout")
searchCtx, cancel := context.WithTimeout(ctx.Context, timeout)
defer cancel()
devices, err := cast.SearchDevices(searchCtx)
if err != nil {
log.Fatalf("%+v", errors.WithStack(err))
}
for dev := range devices {
log.Printf("[DEVICE] %s %s %s:%d", dev.UUID, dev.Name, dev.Host.String(), dev.Port)
}
return nil
},
}
}

View File

@ -3,8 +3,9 @@ package main
import ( import (
"forge.cadoles.com/arcad/edge/cmd/cli/command" "forge.cadoles.com/arcad/edge/cmd/cli/command"
"forge.cadoles.com/arcad/edge/cmd/cli/command/app" "forge.cadoles.com/arcad/edge/cmd/cli/command/app"
"forge.cadoles.com/arcad/edge/cmd/cli/command/cast"
) )
func main() { func main() {
command.Main(app.Root()) command.Main(app.Root(), cast.Root())
} }

View File

@ -6,6 +6,7 @@ Une **Edge App** est une application capable de s'exécuter dans un environnemen
### Référence ### Référence
- [Fichier `manifest.yml`](./apps/manifest.md)
- [API Client](./apps/client-api/README.md) - [API Client](./apps/client-api/README.md)
- [API Serveur](./apps/server-api/README.md) - [API Serveur](./apps/server-api/README.md)

View File

@ -1,64 +1,15 @@
# API Client # API Client
## Méthodes ## Usage
### `Edge.connect(): Promise` Afin de pouvoir utiliser le SDK "client", vous devez inclure dans la page HTML de votre application la balise `<script>` suivante:
> `TODO` ```html
<script src="/edge/sdk/client.js"></script>
### `Edge.disconnect(): void`
> `TODO`
### `Edge.send(message: Object): void`
> `TODO`
### `Edge.rpc(method: string, params: Object): Promise`
> `TODO`
#### Exemple
**Côté serveur**
```js
function onInit() {
rpc.register(echo);
}
function echo(ctx, params) {
return params;
}
``` ```
**Côté client** Vous pourrez ensuite accéder aux variables globales suivantes:
```js - [`Edge.Client`](./edge-client.md) - Client principal d'échange avec le serveur
Edge.connect().then(() => { - [`Edge.Frame`](./edge-frame.md) - Utilitaire de communication avec une frame parente
Edge.rpc("echo", { hello: "world!" }) - [`Edge.Menu`](./edge-menu.md) - Gestionnaire de menu
.then(result => console.log(result))
.catch(err => console.error(err));
});
```
### `Edge.upload(blob: Blob, metadata: Object): Promise`
> `TODO`
### `Edge.blobUrl(bucketName: string, blobId: string): string`
> `TODO`
## Événements
### `"message"`
> `TODO`
#### Exemple
```js
Edge.addEventListener("message", evt => console.log(evt.detail));
```

View File

@ -0,0 +1,68 @@
# `Edge.Client`
## Méthodes
### `Edge.Client.connect(): Promise`
> `TODO`
### `Edge.Client.disconnect(): void`
> `TODO`
### `Edge.Client.send(message: Object): void`
> `TODO`
### `Edge.Client.rpc(method: string, params: Object): Promise`
> `TODO`
#### Exemple
**Côté serveur**
```js
function onInit() {
rpc.register(echo);
}
function echo(ctx, params) {
return params;
}
```
**Côté client**
```js
Edge.Client.connect().then(() => {
Edge.Client.rpc("echo", { hello: "world!" })
.then(result => console.log(result))
.catch(err => console.error(err));
});
```
### `Edge.Client.upload(blob: Blob, metadata: Object): Promise`
> `TODO`
### `Edge.Client.blobUrl(bucketName: string, blobId: string): string`
> `TODO`
### `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).
## Événements
### `"message"`
> `TODO`
#### Exemple
```js
Edge.Client.addEventListener("message", evt => console.log(evt.detail));
```

View File

@ -0,0 +1,30 @@
# `Edge.Frame`
## Méthodes
### `Edge.Frame.addEventListener(name: string, listener: (event) => void)`
> `TODO`
## Événements
### `"title_changed"`
```typescript
interface TitleChangedEvent {
detail: {
title: string
}
}
```
### `"size_changed"`
```typescript
interface SizeChangedEvent {
detail: {
width: number
height: number
}
}
```

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.

36
doc/apps/manifest.md Normal file
View File

@ -0,0 +1,36 @@
# Le fichier `manifest.yml`
Le fichier `manifest.yml` à la racine du bundle de votre application contient des informations décrivant celles ci. Vous trouverez ci dessous un exemple commenté.
```yaml
# REQUIS - L'identifiant de votre application. Il doit être globalement unique.
# Un identifiant du type nom de domaine inversé est en général conseillé (ex: tld.mycompany.myapp)
id: tld.mycompany.myapp
# REQUIS - Le numéro de version de votre application
# Celui ci devrait respecter le format "semver 2" (voir https://semver.org/)
version: 0.0.0
# REQUIS - Le titre de votre application.
title: My App
# OPTIONNEL - Les mots-clés associés à votre applications.
tags: ["chat"]
# OPTIONNEL - La description de votre application.
# Vous pouvez utiliser la syntaxe Markdown pour la mettre en forme.
description: |>
A simple demo application
# OPTIONNEL - Métadonnées associées à l'application
metadata:
# OPTIONNEL - Liste des chemins permettant d'accéder à certains URLs identifiées (page d'administration, icône si existante, etc)
paths:
# Si défini, chemin vers la page d'administration de l'application
admin: /admin
# Si défini, chemin vers l'icône associée à l'application
icon: /my-app-icon.png
# OPTIONNEL - Role minimum requis pour pouvoir accéder à l'application
minimumRole: visitor
```

View File

@ -22,23 +22,7 @@ my-app
Ce fichier est le manifeste de votre application. Il permet au serveur d'identifier celle ci et de récupérer des informations la concernant. Ce fichier est le manifeste de votre application. Il permet au serveur d'identifier celle ci et de récupérer des informations la concernant.
```yaml [Voir le fichier `manifest.yml` d'exemple](./manifest.md)
---
# L'identifiant de votre application. Il doit être globalement unique.
# Un identifiant du type nom de domaine inversé est en général conseillé (ex: tld.mycompany.myapp)
id: tld.mycompany.myapp
# Le titre de votre application.
title: My App
# Les mots-clés associés à votre applications.
tags: ["chat"]
# La description de votre application.
# Vous pouvez utiliser la syntaxe Markdown pour la mettre en forme.
description: |>
A simple demo application
```
## 4. Créer la page d'accueil ## 4. Créer la page d'accueil
@ -56,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

@ -20,10 +20,14 @@ function onInit() {
Listes des modules disponibles côté serveur. Listes des modules disponibles côté serveur.
- [`console`](./console.md) - [`app`](./app.md)
- [`context`](./context.md) - [`auth`](./auth.md)
- [`net`](./net.md)
- [`rpc`](./rpc.md)
- [`store`](./store.md)
- [`blob`](./blob.md) - [`blob`](./blob.md)
- [`cast`](./cast.md) - [`cast`](./cast.md)
- [`console`](./console.md)
- [`context`](./context.md)
- [`fetch`](./fetch.md)
- [`net`](./net.md)
- [`rpc`](./rpc.md)
- [`share`](./share.md)
- [`store`](./store.md)

View File

@ -0,0 +1,59 @@
# Module `app`
Ce module permet de récupérer des informations sur les applications actives dans l'environnement Edge courant.
## Méthodes
### `app.list(ctx: Context): []Manifest`
Récupère la liste des applications actives.
#### Arguments
- `ctx` **Context** Le contexte d'exécution. Voir la documentation du module [`context`](./context.md)
#### Valeur de retour
Liste des objets `Manifest` décrivant chaque application active.
### `app.get(ctx: Context, appId: string): Manifest`
Récupère les informations de l'application identifiée par `appId`.
#### Arguments
- `ctx` **Context** Le contexte d'exécution. Voir la documentation du module [`context`](./context.md)
- `appId` **string** Identifiant de l'application
#### Valeur de retour
Objet `Manifest` associé à l'application, ou `null` si aucune application n'a été trouvée correspondant à l'identifiant.
### `app.getUrl(ctx: Context, appId: string, from: string = ''): Manifest`
Retourne l'URL permettant d'accéder à l'application identifiée par `appId`.
#### Arguments
- `ctx` **Context** Le contexte d'exécution. Voir la documentation du module [`context`](./context.md)
- `appId` **string** Identifiant de l'application
- `from` **string** Adresse IP qui accédera à l'application (permet de générer la bonne URL vis à vis du réseau d'origine)
#### Valeur de retour
URL associée à l'application, ou `null` si aucune application n'a été trouvée correspondant à l'identifiant.
## Objets
### `Manifest`
```typescript
interface Manifest {
id: string // Identifiant de l'application
version: string // Version de l'application
title: string // Titre associé à l'application
description: string // Description associée à l'application
tags: string[] // Mots clés associés à l'application
metadata: { [key: string]: any } // Métadonnées associées à l'application. Voir ../manifest.md
}
```

View File

@ -0,0 +1,49 @@
# Module `auth`
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
### `auth.getClaim(ctx: Context, name: string): string`
Récupère un attribut associé à l'utilisateur.
#### Arguments
- `ctx` **Context** Le contexte d'exécution. Voir la documentation du module [`context`](./context.md)
- `name` **string** Nom de l'attribut à retrouver
#### Valeur de retour
Valeur de l'attribut associé ou vide si la requête est non authentifiée ou que l'attribut n'a pas été trouvé.
#### Usage
```js
function onClientMessage(ctx, message) {
var subject = auth.getClaim(ctx, auth.CLAIM_SUBJECT);
console.log("Connected user is", subject);
}
```
## Propriétés
### `auth.CLAIM_SUBJECT`
Cette propriété identifie l'utilisateur connecté. Si la valeur retournée par la méthode `getClaim()` est vide, alors l'utilisateur n'est pas connecté.
### `auth.CLAIM_ROLE`
Cette propriété retourne le rôle de l'utilisateur connecté au sein du "tenant" courant. Si la valeur retournée par la méthode `getClaim()` est vide, alors l'utilisateur n'est pas connecté.
### `auth.CLAIM_PREFERRED_USERNAME`
Cette propriété retourne le nom "préféré pour l'affichage" de l'utilisateur connecté. Si la valeur retournée par la méthode `getClaim()` est vide, alors l'utilisateur n'est pas connecté ou l'utilisateur n'a pas défini de nom d'utilisateur particulier.

View File

@ -38,11 +38,15 @@ function onBlobDownload(ctx, bucketName, blobId) {
> `TODO` > `TODO`
### `blob.writeBlob(ctx: Context, bucketName: string, blobId: string)` ### `blob.getBlobInfo(ctx: Context, bucketName: string, blobId: string): BlobInfo`
> `TODO` > `TODO`
### `blob.readBlob(ctx: Context, bucketName: string, blobId: string)` ### `blob.writeBlob(ctx: Context, bucketName: string, blobId: string, data: any)`
> `TODO`
### `blob.readBlob(ctx: Context, bucketName: string, blobId: string): ArrayBuffer`
> `TODO` > `TODO`
@ -58,7 +62,7 @@ function onBlobDownload(ctx, bucketName, blobId) {
> `TODO` > `TODO`
### `blob.getBlobInfo(ctx: Context, bucketName: string, blobId: string): BlobInfo` ### `blob.getBucketSize(ctx: Context, bucketName: string): number`
> `TODO` > `TODO`
@ -70,4 +74,16 @@ Voir la documentation de l'objet [`Context`](./context.md#Context).
### `BlobInfo` ### `BlobInfo`
```typescript
interface BlobInfo {
id: string // Identifiant du blob
bucket: string // Nom du bucket contenant le blob
size: number // Taille du blob
modTime: number // Timestamp Unix de dernière modification du blob
contentType: string // Type MIME du contenu du blob
}
```
### `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.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

@ -0,0 +1,33 @@
# Module `fetch`
Ce module permet l'accès à des ressources distantes (sur Internet) depuis votre application.
## Fonctions de rappel
Pour permettre aux utilisateurs d'accéder à des ressources distantes, vous devez déclarer la fonction `onClientFetch(ctx: Context, url: string, remoteAddr: string)` dans le fichier `server/main.js` de votre application.
### `onClientFetch(ctx: Context, url: string, remoteAddr: string)`
#### Usage
**Côté client**
```js
// Création d'une URL "locale" permettant d'accéder à la ressource distante
var url = Edge.Client.externalUrl("http://example.com")
// Vous pouvez utiliser l'URL comme attribut `src` d'une balise <img> par exemple
// ou effectuer une requête fetch() avec celle ci.
fetch(url).then(res => res.text()).then(content => console.log(content));
```
**Côté serveur**
```js
function onClientFetch(ctx, url, remoteAddr) {
// Autoriser la récupération de l'URL demandée ou non
// Dans cet exemple, seule l'URL externe 'http://example.com' est autorisée
// Les autres URLs recevront une erreur HTTP 403 - Forbidden
var authorized = url === "http://example.com"
return { allow: authorized };
}
```

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"
}
```

View File

@ -178,6 +178,6 @@ var results = store.query(ctx, "myCollection", {
limit: 10, limit: 10,
offset: 5, offset: 5,
orderBy: "foo", orderBy: "foo",
orderDirection: store.ASC, orderDirection: store.DIRECTION_ASC,
}); });
``` ```

72
go.mod
View File

@ -2,58 +2,72 @@ module forge.cadoles.com/arcad/edge
go 1.19 go 1.19
require modernc.org/sqlite v1.20.4
require ( require (
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e // indirect github.com/hashicorp/mdns v1.0.5
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/lestrrat-go/jwx/v2 v2.0.8
github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073 // indirect modernc.org/sqlite v1.20.4
github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8 // indirect
github.com/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8 // indirect
) )
require ( require (
cdr.dev/slog v1.4.0 // indirect cloud.google.com/go v0.75.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/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e // indirect
github.com/leodido/go-urn v1.1.0 // indirect
github.com/lestrrat-go/blackmagic 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/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.0 // 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 (
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 // indirect github.com/dop251/goja v0.0.0-20230203172422-5460598cfa32
github.com/dop251/goja_nodejs v0.0.0-20230207183254-2229640ea097 // indirect github.com/dop251/goja_nodejs v0.0.0-20230207183254-2229640ea097
github.com/dustin/go-humanize v1.0.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fatih/color v1.7.0 // indirect github.com/fatih/color v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.1 // indirect github.com/gabriel-vasile/mimetype v1.4.1
github.com/go-chi/chi/v5 v5.0.8 // indirect github.com/go-chi/chi/v5 v5.0.8
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect github.com/gorilla/websocket v1.4.2 // indirect
github.com/igm/sockjs-go/v3 v3.0.2 // indirect github.com/igm/sockjs-go/v3 v3.0.2
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0
github.com/oklog/ulid/v2 v2.1.0 // indirect github.com/oklog/ulid/v2 v2.1.0
github.com/orcaman/concurrent-map v1.0.0 // indirect github.com/orcaman/concurrent-map v1.0.0
github.com/pkg/errors v0.9.1 // indirect 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 // indirect
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 // indirect 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.0.0-20211108221036-ceb1ce70b4fa // indirect golang.org/x/crypto v0.7.0
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/mod v0.10.0
golang.org/x/net v0.4.0 // indirect golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.3.0 // indirect golang.org/x/sys v0.7.0 // indirect
golang.org/x/term v0.3.0 // indirect golang.org/x/term v0.7.0 // indirect
golang.org/x/text v0.5.0 // indirect golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.1.12 // 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 // indirect gopkg.in/yaml.v2 v2.4.0
lukechampine.com/uint128 v1.2.0 // indirect lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect modernc.org/ccgo/v3 v3.16.13 // indirect

127
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,20 +36,22 @@ 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=
@ -72,8 +73,10 @@ github.com/davecgh/go-spew v1.0.1-0.20160907170601-6d212800a42e/go.mod h1:J7Y8Yc
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
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/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=
@ -104,14 +107,16 @@ 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=
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/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=
@ -138,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=
@ -151,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=
@ -162,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=
@ -171,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=
@ -179,13 +185,13 @@ 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/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=
@ -196,25 +202,41 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
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/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/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.0.8 h1:jCFT8oc0hEDVjgUgsBy1F9cbjsjAVZSXNi7JaU9HR/Q=
github.com/lestrrat-go/jwx/v2 v2.0.8/go.mod h1:zLxnyv9rTlEvOUHbc48FAfIL8iYu2hHvIRaTFGc8mT0=
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
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.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
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=
@ -229,26 +251,32 @@ 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.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.1.5-0.20160925220609-976c720a22c8/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.1.5-0.20160925220609-976c720a22c8/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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.7.0/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.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.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
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=
github.com/urfave/cli/v2 v2.24.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= github.com/urfave/cli/v2 v2.24.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
@ -261,8 +289,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
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.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=
@ -276,10 +304,10 @@ 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-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -312,9 +340,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.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.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
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=
@ -344,13 +372,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-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
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.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
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=
@ -359,7 +389,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=
@ -369,8 +398,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-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=
@ -404,34 +434,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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/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-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-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/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
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.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
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=
@ -478,12 +506,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.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.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
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=
@ -507,7 +533,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=
@ -547,9 +572,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=
@ -566,6 +590,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=
@ -576,17 +601,23 @@ 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=
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=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@ -600,6 +631,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=
@ -612,8 +645,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

@ -5,3 +5,8 @@ version: 0.0.0
description: | description: |
Suite de tests pour le SDK client Suite de tests pour le SDK client
tags: ["test"] tags: ["test"]
metadata:
paths:
icon: /icon.png
minimumRole: superadmin

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -4,25 +4,46 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Client SDK Test suite</title> <title>Client SDK Test suite</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="vendor/mocha.css" /> <link rel="icon" type="image/png" href="/icon.png">
<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>
<body> <body>
<div id="mocha"></div> <div id="mocha"></div>
<script src="vendor/chai.js"></script> <script src="/vendor/chai.js"></script>
<script src="vendor/mocha.js"></script> <script src="/vendor/mocha.js"></script>
<script class="mocha-init"> <script class="mocha-init">
mocha.setup('bdd'); mocha.setup('bdd');
mocha.checkLeaks(); mocha.checkLeaks();
</script> </script>
<script src="/edge/sdk/client.js"></script> <script src="/edge/sdk/client.js"></script>
<script src="test/client-sdk.js"></script> <script src="/test/client-sdk.js"></script>
<script src="/test/auth-module.js"></script>
<script src="/test/net-module.js"></script>
<script src="/test/rpc-module.js"></script>
<script src="/test/file-module.js"></script>
<script src="/test/app-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

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

View File

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

View File

@ -1,148 +1,26 @@
Edge.debug = true; Edge.Client.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);
});
});
describe('#send()', function() {
this.timeout(5000);
before(() => {
return Edge.connect();
});
after(() => {
Edge.disconnect();
});
it('should send a message to the server and echo back', function(done) {
const now = new Date();
const handler = evt => {
chai.assert.equal(evt.detail.now, now.toJSON());
Edge.removeEventListener('message', handler);
done();
}
// Server should echo back message
Edge.addEventListener('message', handler);
// Send message to server
Edge.send({ now });
}); });
}); });
}); });
describe('Remote Procedure Call', function() {
before(() => {
return Edge.connect();
});
after(() => {
Edge.disconnect();
});
it('should call the remote echo() method and resolve the returned value', function() {
const foo = "bar";
return Edge.rpc('echo', { foo })
.then(result => {
chai.assert.equal(result.foo, foo);
});
});
it('should call the remote throwErrorFromClient() method and reject with an error', function() {
return Edge.rpc('throwErrorFromClient')
.catch(err => {
// Assert that it's an "internal" error
// See https://www.jsonrpc.org/specification#error_object
chai.assert.equal(err.code, -32603);
});
});
it('should call an unregistered method and reject with an error', function() {
return Edge.rpc('unregisteredMethod')
.catch(err => {
// Assert that it's an "method not found" error
// See https://www.jsonrpc.org/specification#error_object
chai.assert.equal(err.code, -32601);
});
});
it('should call the add() method repetitively and keep count of the sent values', function() {
this.timeout(10000);
const values = [];
for(let i = 0; i <= 1000; i++) {
values.push((Math.random() * 1000 | 0));
}
return Edge.rpc('reset')
.then(() => {
return Promise.all(values.map(v => Edge.rpc("add", {value: v})));
})
.then(() => Edge.rpc('total'))
.then(remoteTotal => {
const localTotal = values.reduce((t, v) => t+v);
console.log("Remote total:", remoteTotal, "Local total:", localTotal);
chai.assert.equal(remoteTotal, localTotal)
})
});
});
describe('File Module', function() {
before(() => {
return Edge.connect();
});
after(() => {
Edge.disconnect();
});
it('should upload then download a blob', function() {
const content = JSON.stringify({"date": new Date()});
const blob = new Blob([content], {type: "application/json"});
return Edge.upload(blob)
.then(upload => upload.result())
.then(result => {
chai.assert.isNotEmpty(result.blobId);
chai.assert.isNotEmpty(result.bucket);
const blobUrl = Edge.blobUrl(result.bucket, result.blobId);
chai.assert.isNotEmpty(blobUrl);
return fetch(blobUrl)
.then(res => res.text())
.then(blobContent => {
chai.assert.equal(content, blobContent);
});
})
.catch(err => {
chai.assert.fail(err);
})
});
});

View File

@ -0,0 +1,33 @@
describe('Fetch Module', function () {
before(() => {
return Edge.Client.connect();
});
after(() => {
Edge.Client.disconnect();
});
it('should fetch an authorized external url', function () {
var externalUrl = Edge.Client.externalUrl("http://example.com");
return fetch(externalUrl)
.then(res => {
chai.assert.equal(res.status, 200)
return res.text()
})
.then(content => {
chai.assert.include(content, '<h1>Example Domain</h1>')
})
});
it('should not fetch an unauthorized external url', function () {
var externalUrl = Edge.Client.externalUrl("https://google.com");
return fetch(externalUrl)
.then(res => {
chai.assert.equal(res.status, 403)
})
});
});

View File

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

View File

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

View File

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

View File

@ -10,13 +10,24 @@ function onInit() {
rpc.register("add", add); rpc.register("add", add);
rpc.register("reset", reset); rpc.register("reset", reset);
rpc.register("total", total); rpc.register("total", total);
rpc.register("getUserInfo", getUserInfo);
rpc.register("listApps");
rpc.register("getApp");
rpc.register("getAppUrl");
} }
// Called for each client message // Called for each client message
function onClientMessage(ctx, data) { function onClientMessage(ctx, message) {
var sessionId = context.get(ctx, context.SESSION_ID); console.log("onClientMessage", message);
console.log("onClientMessage", sessionId, data.now);
net.send(ctx, { now: data.now }); switch (message.test) {
case "broadcast":
net.broadcast(message);
break;
default:
net.send(ctx, message);
}
} }
// Called for each blob upload request // Called for each blob upload request
@ -61,3 +72,35 @@ function reset(ctx, params) {
function total(ctx, params) { function total(ctx, params) {
return count; return count;
} }
function getUserInfo(ctx, params) {
var subject = auth.getClaim(ctx, auth.CLAIM_SUBJECT);
var role = auth.getClaim(ctx, auth.CLAIM_ROLE);
var preferredUsername = auth.getClaim(ctx, auth.CLAIM_PREFERRED_USERNAME);
return {
subject: subject,
role: role,
preferredUsername: preferredUsername,
};
}
function listApps(ctx) {
return app.list(ctx);
}
function getApp(ctx, params) {
var appId = params.appId;
return app.get(ctx, appId);
}
function getAppUrl(ctx, params) {
var appId = params.appId;
var from = params.from;
return app.getUrl(ctx, appId, from ? from : '');
}
function onClientFetch(ctx, url, remoteAddr) {
return { allow: url === 'http://example.com' };
}

28
misc/jenkins/Dockerfile Normal file
View File

@ -0,0 +1,28 @@
FROM reg.cadoles.com/proxy_cache/library/ubuntu:22.04
ARG HTTP_PROXY=
ARG HTTPS_PROXY=
ARG http_proxy=
ARG https_proxy=
ARG GO_VERSION=1.20.2
# Install dev environment dependencies
RUN export DEBIAN_FRONTEND=noninteractive &&\
apt-get update -y &&\
apt-get install -y --no-install-recommends curl ca-certificates build-essential wget unzip tar git jq
# Install Go
RUN mkdir -p /tmp \
&& wget -O /tmp/go${GO_VERSION}.linux-amd64.tar.gz https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz \
&& rm -rf /usr/local/go \
&& mkdir -p /usr/local \
&& tar -C /usr/local -xzf /tmp/go${GO_VERSION}.linux-amd64.tar.gz
ENV PATH="${PATH}:/usr/local/go/bin"
# Add LetsEncrypt certificates
RUN curl -k https://forge.cadoles.com/Cadoles/Jenkins/raw/branch/master/resources/com/cadoles/common/add-letsencrypt-ca.sh | bash
# Install NodeJS
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y nodejs

View File

@ -1,12 +1,16 @@
**/*.go **/*.go
pkg/app/sdk/client/src/**/*.js **/*.tmpl
pkg/app/sdk/client/src/**/*.ts pkg/sdk/client/src/**/*.js
pkg/sdk/client/src/**/*.ts
misc/client-sdk-testsuite/src/**/* 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 GOTEST_ARGS="-short" test
prep: make build prep: make build
daemon: bin/cli app run -p misc/client-sdk-testsuite/dist --storage-file ./sdk-testsuite.sqlite daemon: make run-app
}
**/*.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

@ -1,16 +0,0 @@
package app
type ID string
type Manifest struct {
ID ID `yaml:"id"`
Version string `yaml:"version"`
Title string `yaml:"title"`
Description string `yaml:"description"`
Tags []string `yaml:"tags"`
}
type App struct {
ID ID
Manifest *Manifest
}

View File

@ -1,101 +0,0 @@
package app
import (
"context"
"path/filepath"
"gitlab.com/wpetit/goweb/logger"
"gopkg.in/yaml.v2"
"forge.cadoles.com/arcad/edge/pkg/bundle"
"github.com/pkg/errors"
)
type FilesystemLoader struct {
searchPatterns []string
}
type LoadedApp struct {
App *App
Bundle bundle.Bundle
}
func (l *FilesystemLoader) Load(ctx context.Context) ([]*LoadedApp, error) {
apps := make([]*LoadedApp, 0)
for _, seachPattern := range l.searchPatterns {
absSearchPattern, err := filepath.Abs(seachPattern)
if err != nil {
return nil, errors.Wrapf(err, "could not generate absolute path for '%s'", seachPattern)
}
logger.Debug(ctx, "searching apps in filesystem", logger.F("searchPattern", absSearchPattern))
files, err := filepath.Glob(absSearchPattern)
if err != nil {
return nil, errors.Wrapf(err, "could not search files with pattern '%s'", absSearchPattern)
}
for _, f := range files {
loopCtx := logger.With(ctx, logger.F("file", f))
logger.Debug(loopCtx, "found app bundle")
b, err := bundle.FromPath(f)
if err != nil {
logger.Error(loopCtx, "could not load bundle", logger.E(errors.WithStack(err)))
continue
}
logger.Debug(loopCtx, "loading app manifest")
appManifest, err := LoadAppManifest(b)
if err != nil {
logger.Error(loopCtx, "could not load app manifest", logger.E(errors.WithStack(err)))
continue
}
g := &App{
ID: appManifest.ID,
Manifest: appManifest,
}
apps = append(apps, &LoadedApp{
App: g,
Bundle: b,
})
}
}
return apps, nil
}
func NewFilesystemLoader(searchPatterns ...string) *FilesystemLoader {
return &FilesystemLoader{
searchPatterns: searchPatterns,
}
}
func LoadAppManifest(b bundle.Bundle) (*Manifest, error) {
reader, _, err := b.File("manifest.yml")
if err != nil {
return nil, errors.Wrap(err, "could not read manifest.yml")
}
defer func() {
if err := reader.Close(); err != nil {
panic(errors.WithStack(err))
}
}()
manifest := &Manifest{}
decoder := yaml.NewDecoder(reader)
if err := decoder.Decode(manifest); err != nil {
return nil, errors.Wrap(err, "could not decode manifest.yml")
}
return manifest, nil
}

85
pkg/app/manifest.go Normal file
View File

@ -0,0 +1,85 @@
package app
import (
"strings"
"forge.cadoles.com/arcad/edge/pkg/bundle"
"github.com/pkg/errors"
"golang.org/x/mod/semver"
"gopkg.in/yaml.v2"
)
type ID string
type Manifest struct {
ID ID `yaml:"id" json:"id"`
Version string `yaml:"version" json:"version"`
Title string `yaml:"title" json:"title"`
Description string `yaml:"description" json:"description"`
Tags []string `yaml:"tags" json:"tags"`
Metadata MapStr `yaml:"metadata" json:"metadata"`
}
type MetadataValidator func(map[string]any) (bool, error)
func (m *Manifest) Validate(validators ...MetadataValidator) (bool, error) {
if m.ID == "" {
return false, errors.New("'id' property should not be empty")
}
if m.Version == "" {
return false, errors.New("'version' property should not be empty")
}
version := m.Version
if !strings.HasPrefix(version, "v") {
version = "v" + version
}
if !semver.IsValid(version) {
return false, errors.Errorf("version '%s' does not respect semver format", m.Version)
}
if m.Title == "" {
return false, errors.New("'title' property should not be empty")
}
if m.Tags != nil {
for _, t := range m.Tags {
if strings.ContainsAny(t, " \t\n\r") {
return false, errors.Errorf("tag '%s' should not contain any space or new line", t)
}
}
}
for _, v := range validators {
valid, err := v(m.Metadata)
if !valid || err != nil {
return valid, errors.WithStack(err)
}
}
return true, nil
}
func LoadManifest(b bundle.Bundle) (*Manifest, error) {
reader, _, err := b.File("manifest.yml")
if err != nil {
return nil, errors.Wrap(err, "could not read manifest.yml")
}
defer func() {
if err := reader.Close(); err != nil {
panic(errors.WithStack(err))
}
}()
manifest := &Manifest{}
decoder := yaml.NewDecoder(reader)
if err := decoder.Decode(manifest); err != nil {
return nil, errors.Wrap(err, "could not decode manifest.yml")
}
return manifest, nil
}

61
pkg/app/map_str.go Normal file
View File

@ -0,0 +1,61 @@
package app
import (
"fmt"
"github.com/pkg/errors"
)
type MapStr map[string]interface{}
func MapStrUnion(dict1 MapStr, dict2 MapStr) MapStr {
dict := MapStr{}
for k, v := range dict1 {
dict[k] = v
}
for k, v := range dict2 {
dict[k] = v
}
return dict
}
func (ms *MapStr) UnmarshalYAML(unmarshal func(interface{}) error) error {
var result map[interface{}]interface{}
err := unmarshal(&result)
if err != nil {
return errors.WithStack(err)
}
*ms = cleanUpInterfaceMap(result)
return nil
}
func cleanUpInterfaceArray(in []interface{}) []interface{} {
result := make([]interface{}, len(in))
for i, v := range in {
result[i] = cleanUpMapValue(v)
}
return result
}
func cleanUpInterfaceMap(in map[interface{}]interface{}) MapStr {
result := make(MapStr)
for k, v := range in {
result[fmt.Sprintf("%v", k)] = cleanUpMapValue(v)
}
return result
}
func cleanUpMapValue(v interface{}) interface{} {
switch v := v.(type) {
case []interface{}:
return cleanUpInterfaceArray(v)
case map[interface{}]interface{}:
return cleanUpInterfaceMap(v)
case string:
return v
default:
return fmt.Sprintf("%v", v)
}
}

View File

@ -0,0 +1,28 @@
package metadata
import (
"forge.cadoles.com/arcad/edge/pkg/app"
"github.com/pkg/errors"
)
func WithMinimumRoleValidator(roles ...string) app.MetadataValidator {
return func(metadata map[string]any) (bool, error) {
rawMinimumRole, exists := metadata["minimumRole"]
if !exists {
return true, nil
}
minimumRole, ok := rawMinimumRole.(string)
if !ok {
return false, errors.Errorf("metadata['minimumRole']: unexpected value type '%T'", rawMinimumRole)
}
for _, r := range roles {
if minimumRole == r {
return true, nil
}
}
return false, errors.Errorf("metadata['minimumRole']: unexpected role '%s'", minimumRole)
}
}

View File

@ -0,0 +1,51 @@
package metadata
import (
"strings"
"forge.cadoles.com/arcad/edge/pkg/app"
"github.com/pkg/errors"
)
type NamedPath string
const (
NamedPathAdmin NamedPath = "admin"
NamedPathIcon NamedPath = "icon"
)
func WithNamedPathsValidator(names ...NamedPath) app.MetadataValidator {
set := map[NamedPath]struct{}{}
for _, n := range names {
set[n] = struct{}{}
}
return func(metadata map[string]any) (bool, error) {
rawPaths, exists := metadata["paths"]
if !exists {
return true, nil
}
paths, ok := rawPaths.(app.MapStr)
if !ok {
return false, errors.Errorf("metadata['paths']: unexpected named path value type '%T'", rawPaths)
}
for n, p := range paths {
if _, exists := set[NamedPath(n)]; !exists {
return false, errors.Errorf("metadata['paths']: unexpected named path '%s'", n)
}
path, ok := p.(string)
if !ok {
return false, errors.Errorf("metadata['paths']['%s']: unexpected named path value type '%T'", n, path)
}
if !strings.HasPrefix(path, "/") {
return false, errors.Errorf("metadata['paths']['%s']: named path value should start with a '/'", n)
}
}
return true, nil
}
}

View File

@ -0,0 +1,7 @@
id: foo.arcad.app
version: v0.0.0
title: Foo
description: A test app
tags: ["test"]
metadata:
minimumRole: foo

View File

@ -0,0 +1,10 @@
id: foo.arcad.app
version: v0.0.0
title: Foo
description: A test app
tags: ["test"]
metadata:
paths:
invalid: /admin
icon: /my-app-icon.png
minimumRole: visitor

View File

@ -0,0 +1,10 @@
id: foo.arcad.app
version: v0.0.0
title: Foo
description: A test app
tags: ["test"]
metadata:
paths:
admin: /admin
icon: /my-app-icon.png
minimumRole: visitor

View File

@ -0,0 +1,74 @@
package metadata
import (
"io/ioutil"
"path/filepath"
"testing"
"forge.cadoles.com/arcad/edge/pkg/app"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
)
type validatorTestCase struct {
File string
ExpectValid bool
ExpectError bool
}
var validatorTestCases = []validatorTestCase{
{
File: "valid.yml",
ExpectValid: true,
},
{
File: "invalid-paths.yml",
ExpectValid: false,
ExpectError: true,
},
{
File: "invalid-minimum-role.yml",
ExpectValid: false,
ExpectError: true,
},
}
var validators = []app.MetadataValidator{
WithMinimumRoleValidator("visitor", "user", "superuser", "admin", "superadmin"),
WithNamedPathsValidator(NamedPathAdmin, NamedPathIcon),
}
func TestManifestValidate(t *testing.T) {
for _, tc := range validatorTestCases {
func(tc *validatorTestCase) {
t.Run(tc.File, func(t *testing.T) {
data, err := ioutil.ReadFile(filepath.Join("testdata/manifests", tc.File))
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
var manifest app.Manifest
if err := yaml.Unmarshal(data, &manifest); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
valid, err := manifest.Validate(validators...)
t.Logf("[RESULT] valid:%v, err:%v", valid, err)
if e, g := tc.ExpectValid, valid; e != g {
t.Errorf("valid: expected '%v', got '%v'", e, g)
}
if tc.ExpectError && err == nil {
t.Error("err should not be nil")
}
if !tc.ExpectError && err != nil {
t.Errorf("err: expected nil, got '%+v'", err)
}
})
}(&tc)
}
}

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

@ -1,41 +1,55 @@
package app package app
import ( import (
"context"
"math/rand" "math/rand"
"sync" "sync"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/dop251/goja_nodejs/eventloop" "github.com/dop251/goja_nodejs/eventloop"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
) )
var ErrFuncDoesNotExist = errors.New("function does not exist") var (
ErrFuncDoesNotExist = errors.New("function does not exist")
ErUnknownError = errors.New("unknown error")
)
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 { if err != nil {
return errors.Wrap(err, "could not run js script") err = errors.Wrap(err, "could not run js script")
}
})
if err != nil {
return errors.WithStack(err)
} }
return nil return nil
} }
func (s *Server) ExecFuncByName(funcName string, args ...interface{}) (goja.Value, error) { func (s *Server) ExecFuncByName(ctx context.Context, funcName string, args ...interface{}) (goja.Value, error) {
callable, ok := goja.AssertFunction(s.runtime.Get(funcName)) ctx = logger.With(ctx, logger.F("function", funcName), logger.F("args", args))
if !ok {
return nil, errors.WithStack(ErrFuncDoesNotExist) ret, err := s.Exec(ctx, funcName, args...)
if err != nil {
return nil, errors.WithStack(err)
} }
return s.Exec(callable, args...) return ret, nil
} }
func (s *Server) Exec(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
@ -44,28 +58,65 @@ func (s *Server) Exec(callable goja.Callable, args ...interface{}) (goja.Value,
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")
defer wg.Done()
defer func() {
if recovered := recover(); recovered != nil {
revoveredErr, ok := recovered.(error)
if ok {
logger.Error(ctx, "recovered runtime error", logger.E(errors.WithStack(revoveredErr)))
err = errors.WithStack(ErUnknownError)
return
}
panic(recovered)
}
}()
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...)
if err != nil { if err != nil {
err = errors.WithStack(err) err = errors.WithStack(err)
} }
wg.Done()
}) })
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 {
@ -111,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
@ -142,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

@ -99,3 +99,5 @@ func (f *File) Readdir(count int) ([]os.FileInfo, error) {
func (f *File) Stat() (os.FileInfo, error) { func (f *File) Stat() (os.FileInfo, error) {
return f.fi, nil return f.fi, nil
} }
var _ http.FileSystem = &FileSystem{}

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

@ -6,6 +6,7 @@ import (
"time" "time"
"forge.cadoles.com/arcad/edge/pkg/bus" "forge.cadoles.com/arcad/edge/pkg/bus"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
@ -21,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)
} }
} }
@ -59,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)
} }
@ -88,22 +108,51 @@ 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) {
ctx := context.Background() 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
} }
timeout := time.After(2 * time.Second) timeout := time.After(time.Second)
select { select {
case d.out <- msg: case d.out <- msg:
case <-timeout: case <-timeout:
logger.Error(ctx, "message out chan timed out", logger.F("message", msg)) 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

@ -11,6 +11,7 @@ import (
"forge.cadoles.com/arcad/edge/pkg/bus" "forge.cadoles.com/arcad/edge/pkg/bus"
"forge.cadoles.com/arcad/edge/pkg/module" "forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/module/blob"
"forge.cadoles.com/arcad/edge/pkg/storage" "forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -65,10 +66,10 @@ func (h *Handler) handleAppUpload(w http.ResponseWriter, r *http.Request) {
} }
ctx = module.WithContext(ctx, map[module.ContextKey]any{ ctx = module.WithContext(ctx, map[module.ContextKey]any{
module.ContextKeyOriginRequest: r, ContextKeyOriginRequest: r,
}) })
requestMsg := module.NewMessageUploadRequest(ctx, fileHeader, metadata) requestMsg := blob.NewMessageUploadRequest(ctx, fileHeader, metadata)
reply, err := h.bus.Request(ctx, requestMsg) reply, err := h.bus.Request(ctx, requestMsg)
if err != nil { if err != nil {
@ -80,7 +81,7 @@ func (h *Handler) handleAppUpload(w http.ResponseWriter, r *http.Request) {
logger.Debug(ctx, "upload reply", logger.F("reply", reply)) logger.Debug(ctx, "upload reply", logger.F("reply", reply))
responseMsg, ok := reply.(*module.MessageUploadResponse) responseMsg, ok := reply.(*blob.MessageUploadResponse)
if !ok { if !ok {
logger.Error( logger.Error(
ctx, "unexpected upload response message", ctx, "unexpected upload response message",
@ -117,10 +118,10 @@ func (h *Handler) handleAppDownload(w http.ResponseWriter, r *http.Request) {
ctx := logger.With(r.Context(), logger.F("blobID", blobID), logger.F("bucket", bucket)) ctx := logger.With(r.Context(), logger.F("blobID", blobID), logger.F("bucket", bucket))
ctx = module.WithContext(ctx, map[module.ContextKey]any{ ctx = module.WithContext(ctx, map[module.ContextKey]any{
module.ContextKeyOriginRequest: r, ContextKeyOriginRequest: r,
}) })
requestMsg := module.NewMessageDownloadRequest(ctx, bucket, storage.BlobID(blobID)) requestMsg := blob.NewMessageDownloadRequest(ctx, bucket, storage.BlobID(blobID))
reply, err := h.bus.Request(ctx, requestMsg) reply, err := h.bus.Request(ctx, requestMsg)
if err != nil { if err != nil {
@ -130,7 +131,7 @@ func (h *Handler) handleAppDownload(w http.ResponseWriter, r *http.Request) {
return return
} }
replyMsg, ok := reply.(*module.MessageDownloadResponse) replyMsg, ok := reply.(*blob.MessageDownloadResponse)
if !ok { if !ok {
logger.Error( logger.Error(
ctx, "unexpected download response message", ctx, "unexpected download response message",

112
pkg/http/fetch.go Normal file
View File

@ -0,0 +1,112 @@
package http
import (
"io"
"net/http"
"net/url"
"forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/module/fetch"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func (h *Handler) handleAppFetch(w http.ResponseWriter, r *http.Request) {
h.mutex.RLock()
defer h.mutex.RUnlock()
ctx := r.Context()
ctx = module.WithContext(ctx, map[module.ContextKey]any{
ContextKeyOriginRequest: r,
})
rawURL := r.URL.Query().Get("url")
url, err := url.Parse(rawURL)
if err != nil {
jsonError(w, http.StatusBadRequest, errorCodeBadRequest)
return
}
requestMsg := fetch.NewMessageFetchRequest(ctx, r.RemoteAddr, url)
reply, err := h.bus.Request(ctx, requestMsg)
if err != nil {
logger.Error(ctx, "could not retrieve fetch request reply", logger.E(errors.WithStack(err)))
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
return
}
logger.Debug(ctx, "fetch reply", logger.F("reply", reply))
responseMsg, ok := reply.(*fetch.MessageFetchResponse)
if !ok {
logger.Error(
ctx, "unexpected fetch response message",
logger.F("message", reply),
)
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
return
}
if !responseMsg.Allow {
jsonError(w, http.StatusForbidden, errorCodeForbidden)
return
}
proxyReq, err := http.NewRequest(http.MethodGet, url.String(), nil)
if err != nil {
logger.Error(
ctx, "could not create proxy request",
logger.E(errors.WithStack(err)),
)
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
return
}
for header, values := range r.Header {
for _, value := range values {
proxyReq.Header.Add(header, value)
}
}
proxyReq.Header.Add("X-Forwarded-From", r.RemoteAddr)
res, err := h.httpClient.Do(proxyReq)
if err != nil {
logger.Error(
ctx, "could not execute proxy request",
logger.E(errors.WithStack(err)),
)
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
return
}
defer func() {
if err := res.Body.Close(); err != nil {
logger.Error(
ctx, "could not close response body",
logger.E(errors.WithStack(err)),
)
}
}()
for header, values := range res.Header {
for _, value := range values {
w.Header().Add(header, value)
}
}
w.WriteHeader(res.StatusCode)
if _, err := io.Copy(w, res.Body); err != nil {
panic(errors.WithStack(err))
}
}

View File

@ -31,6 +31,8 @@ type Handler struct {
server *app.Server server *app.Server
serverModuleFactories []app.ServerModuleFactory serverModuleFactories []app.ServerModuleFactory
httpClient *http.Client
mutex sync.RWMutex mutex sync.RWMutex
} }
@ -59,7 +61,7 @@ func (h *Handler) Load(bdle bundle.Bundle) error {
} }
fs := bundle.NewFileSystem("public", bdle) fs := bundle.NewFileSystem("public", bdle)
public := http.FileServer(fs) public := HTML5Fileserver(fs)
sockjs := sockjs.NewHandler(sockJSPathPrefix, h.sockjsOpts, h.handleSockJSSession) sockjs := sockjs.NewHandler(sockJSPathPrefix, h.sockjsOpts, h.handleSockJSSession)
if h.server != nil { if h.server != nil {
@ -91,20 +93,33 @@ func NewHandler(funcs ...HandlerOptionFunc) *Handler {
sockjsOpts: opts.SockJS, sockjsOpts: opts.SockJS,
router: router, router: router,
serverModuleFactories: opts.ServerModuleFactories, serverModuleFactories: opts.ServerModuleFactories,
httpClient: opts.HTTPClient,
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("/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

@ -0,0 +1,54 @@
package http
import (
"net/http"
"os"
"path"
"strings"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func HTML5Fileserver(fs http.FileSystem) http.Handler {
handler := http.FileServer(fs)
fn := func(w http.ResponseWriter, r *http.Request) {
urlPath := r.URL.Path
if !strings.HasPrefix(urlPath, "/") {
urlPath = "/" + urlPath
r.URL.Path = urlPath
}
urlPath = path.Clean(urlPath)
file, err := fs.Open(urlPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
r.URL.Path = "/"
handler.ServeHTTP(w, r)
return
}
logger.Error(r.Context(), "could not open bundle file", logger.E(err))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
defer func() {
if err := file.Close(); err != nil {
logger.Error(r.Context(), "could not close file", logger.E(err))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}()
handler.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}

View File

@ -1,11 +1,13 @@
package http package http
import ( import (
"net/http"
"time" "time"
"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"
) )
@ -14,6 +16,9 @@ type HandlerOptions struct {
SockJS sockjs.Options SockJS sockjs.Options
ServerModuleFactories []app.ServerModuleFactory ServerModuleFactories []app.ServerModuleFactory
UploadMaxFileSize int64 UploadMaxFileSize int64
HTTPClient *http.Client
HTTPMounts []func(r chi.Router)
HTTPMiddlewares []func(next http.Handler) http.Handler
} }
func defaultHandlerOptions() *HandlerOptions { func defaultHandlerOptions() *HandlerOptions {
@ -27,6 +32,11 @@ func defaultHandlerOptions() *HandlerOptions {
SockJS: sockjsOptions, SockJS: sockjsOptions,
ServerModuleFactories: make([]app.ServerModuleFactory, 0), ServerModuleFactories: make([]app.ServerModuleFactory, 0),
UploadMaxFileSize: 10 << (10 * 2), // 10Mb UploadMaxFileSize: 10 << (10 * 2), // 10Mb
HTTPClient: &http.Client{
Timeout: time.Second * 30,
},
HTTPMounts: make([]func(r chi.Router), 0),
HTTPMiddlewares: make([]func(http.Handler) http.Handler, 0),
} }
} }
@ -55,3 +65,21 @@ func WithUploadMaxFileSize(size int64) HandlerOptionFunc {
opts.UploadMaxFileSize = size opts.UploadMaxFileSize = size
} }
} }
func WithHTTPClient(client *http.Client) HandlerOptionFunc {
return func(opts *HandlerOptions) {
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

@ -15,6 +15,11 @@ const (
statusChannelClosed = iota statusChannelClosed = iota
) )
const (
ContextKeySessionID module.ContextKey = "sessionId"
ContextKeyOriginRequest module.ContextKey = "originRequest"
)
func (h *Handler) handleSockJS(w http.ResponseWriter, r *http.Request) { func (h *Handler) handleSockJS(w http.ResponseWriter, r *http.Request) {
h.mutex.RLock() h.mutex.RLock()
defer h.mutex.RUnlock() defer h.mutex.RUnlock()
@ -79,7 +84,7 @@ func (h *Handler) handleServerMessages(ctx context.Context, sess sockjs.Session)
continue continue
} }
sessionID := module.ContextValue[string](serverMessage.Context, module.ContextKeySessionID) sessionID := module.ContextValue[string](serverMessage.Context, ContextKeySessionID)
isDest := sessionID == "" || sessionID == sess.ID() isDest := sessionID == "" || sessionID == sess.ID()
if !isDest { if !isDest {
@ -182,8 +187,8 @@ func (h *Handler) handleClientMessages(ctx context.Context, sess sockjs.Session)
ctx := logger.With(ctx, logger.F("payload", payload)) ctx := logger.With(ctx, logger.F("payload", payload))
ctx = module.WithContext(ctx, map[module.ContextKey]any{ ctx = module.WithContext(ctx, map[module.ContextKey]any{
module.ContextKeySessionID: sess.ID(), ContextKeySessionID: sess.ID(),
module.ContextKeyOriginRequest: sess.Request(), ContextKeyOriginRequest: sess.Request(),
}) })
clientMessage := module.NewClientMessage(ctx, payload) clientMessage := module.NewClientMessage(ctx, payload)

5
pkg/module/app/error.go Normal file
View File

@ -0,0 +1,5 @@
package app
import "errors"
var ErrNotFound = errors.New("not found")

View File

@ -0,0 +1,58 @@
package memory
import (
"context"
"fmt"
"io/ioutil"
"testing"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module"
appModule "forge.cadoles.com/arcad/edge/pkg/module/app"
"github.com/pkg/errors"
)
func TestAppModuleWithMemoryRepository(t *testing.T) {
t.Parallel()
server := app.NewServer(
module.ContextModuleFactory(),
module.ConsoleModuleFactory(),
appModule.ModuleFactory(NewRepository(
func(ctx context.Context, id app.ID, from string) (string, error) {
return fmt.Sprintf("http//%s.example.com?from=%s", id, from), nil
},
&app.Manifest{
ID: "dummy1.arcad.app",
Version: "0.0.0",
Title: "Dummy 1",
Description: "Dummy App 1",
Tags: []string{"dummy", "first"},
},
&app.Manifest{
ID: "dummy2.arcad.app",
Version: "0.0.0",
Title: "Dummy 2",
Description: "Dummy App 2",
Tags: []string{"dummy", "second"},
},
)),
)
file := "testdata/app.js"
data, err := ioutil.ReadFile(file)
if err != nil {
t.Fatal(err)
}
if err := server.Load(file, string(data)); err != nil {
t.Fatal(err)
}
defer server.Stop()
if err := server.Start(); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
}

View File

@ -0,0 +1,54 @@
package memory
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/app"
module "forge.cadoles.com/arcad/edge/pkg/module/app"
"github.com/pkg/errors"
)
type GetURLFunc func(context.Context, app.ID, string) (string, error)
type Repository struct {
getURL GetURLFunc
apps []*app.Manifest
}
// GetURL implements app.Repository
func (r *Repository) GetURL(ctx context.Context, id app.ID, from string) (string, error) {
url, err := r.getURL(ctx, id, from)
if err != nil {
return "", errors.WithStack(err)
}
return url, nil
}
// Get implements app.Repository
func (r *Repository) Get(ctx context.Context, id app.ID) (*app.Manifest, error) {
for _, app := range r.apps {
if app.ID != id {
continue
}
return app, nil
}
return nil, module.ErrNotFound
}
// List implements app.Repository
func (r *Repository) List(ctx context.Context) ([]*app.Manifest, error) {
return r.apps, nil
}
func NewRepository(getURL GetURLFunc, manifests ...*app.Manifest) *Repository {
if manifests == nil {
manifests = make([]*app.Manifest, 0)
}
return &Repository{getURL, manifests}
}
var _ module.Repository = &Repository{}

17
pkg/module/app/memory/testdata/app.js vendored Normal file
View File

@ -0,0 +1,17 @@
var ctx = context.new();
var manifests = app.list(ctx);
if (manifests.length !== 2) {
throw new Error("apps.length: expected '2', got '"+manifests.length+"'");
}
var manifest = app.get(ctx, 'dummy2.arcad.app');
if (!manifest) {
throw new Error("manifest should not be null");
}
if (manifest.id !== "dummy2.arcad.app") {
throw new Error("manifest.id: expected 'dummy2.arcad.app', got '"+manifest.id+"'");
}

124
pkg/module/app/module.go Normal file
View File

@ -0,0 +1,124 @@
package app
import (
"fmt"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module/util"
"github.com/dop251/goja"
"github.com/pkg/errors"
)
type Module struct {
repository Repository
}
type gojaManifest struct {
ID string `goja:"id" json:"id"`
Version string `goja:"version" json:"version"`
Title string `goja:"title" json:"title"`
Description string `goja:"description" json:"description"`
Tags []string `goja:"tags" json:"tags"`
Metadata map[string]any `goja:"metadata" json:"metadata"`
}
func toGojaManifest(manifest *app.Manifest) *gojaManifest {
return &gojaManifest{
ID: string(manifest.ID),
Version: manifest.Version,
Title: manifest.Title,
Description: manifest.Description,
Tags: manifest.Tags,
Metadata: manifest.Metadata,
}
}
func toGojaManifests(manifests []*app.Manifest) []*gojaManifest {
gojaManifests := make([]*gojaManifest, len(manifests))
for i, m := range manifests {
gojaManifests[i] = toGojaManifest(m)
}
return gojaManifests
}
func (m *Module) Name() string {
return "app"
}
func (m *Module) Export(export *goja.Object) {
if err := export.Set("list", m.list); err != nil {
panic(errors.Wrap(err, "could not set 'list' function"))
}
if err := export.Set("get", m.get); err != nil {
panic(errors.Wrap(err, "could not set 'get' function"))
}
if err := export.Set("getUrl", m.getURL); err != nil {
panic(errors.Wrap(err, "could not set 'list' function"))
}
}
func (m *Module) list(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt)
manifests, err := m.repository.List(ctx)
if err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return rt.ToValue(toGojaManifests(manifests))
}
func (m *Module) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt)
appID := assertAppID(call.Argument(1), rt)
manifest, err := m.repository.Get(ctx, appID)
if err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return rt.ToValue(toGojaManifest(manifest))
}
func (m *Module) getURL(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt)
appID := assertAppID(call.Argument(1), rt)
var from string
if len(call.Arguments) > 2 {
from = util.AssertString(call.Argument(2), rt)
}
url, err := m.repository.GetURL(ctx, appID, from)
if err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return rt.ToValue(url)
}
func ModuleFactory(repository Repository) app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule {
return &Module{
repository: repository,
}
}
}
func assertAppID(value goja.Value, rt *goja.Runtime) app.ID {
appID, ok := value.Export().(app.ID)
if !ok {
rawAppID, ok := value.Export().(string)
if !ok {
panic(rt.NewTypeError(fmt.Sprintf("app id must be an appid or a string, got '%T'", value.Export())))
}
appID = app.ID(rawAppID)
}
return appID
}

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

@ -0,0 +1,13 @@
package app
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/app"
)
type Repository interface {
List(context.Context) ([]*app.Manifest, error)
Get(context.Context, app.ID) (*app.Manifest, error)
GetURL(ctx context.Context, id app.ID, from string) (string, error)
}

5
pkg/module/auth/error.go Normal file
View File

@ -0,0 +1,5 @@
package auth
import "errors"
var ErrUnauthenticated = errors.New("unauthenticated")

View File

@ -0,0 +1,27 @@
package http
import "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd"
type LocalAccount struct {
Username string `json:"username"`
Algo passwd.Algo `json:"algo"`
Password string `json:"password"`
Claims map[string]any `json:"claims"`
}
func NewLocalAccount(username, password string, algo passwd.Algo, claims map[string]any) LocalAccount {
return LocalAccount{
Username: username,
Password: password,
Algo: algo,
Claims: claims,
}
}
func toAccountsMap(accounts []LocalAccount) map[string]LocalAccount {
accountsMap := make(map[string]LocalAccount)
for _, acc := range accounts {
accountsMap[acc.Username] = acc
}
return accountsMap
}

View File

@ -0,0 +1,209 @@
package http
import (
"html/template"
"net/http"
"time"
_ "embed"
"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/jwt"
"github.com/go-chi/chi/v5"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
//go:embed templates/login.html.tmpl
var rawLoginTemplate string
var loginTemplate *template.Template
var (
errNotFound = errors.New("not found")
errInvalidPassword = errors.New("invalid password")
)
func init() {
loginTemplate = template.Must(template.New("").Parse(rawLoginTemplate))
}
type LocalHandler struct {
router chi.Router
algo jwa.KeyAlgorithm
key jwk.Key
getCookieDomain GetCookieDomainFunc
cookieDuration time.Duration
accounts map[string]LocalAccount
}
func (h *LocalHandler) initRouter(prefix string) {
router := chi.NewRouter()
router.Route(prefix, func(r chi.Router) {
r.Get("/login", h.serveForm)
r.Post("/login", h.handleForm)
r.Get("/logout", h.handleLogout)
})
h.router = router
}
type loginTemplateData struct {
URL string
Username string
Password string
Message string
}
func (h *LocalHandler) serveForm(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
data := loginTemplateData{
URL: r.URL.String(),
Username: "",
Password: "",
Message: "",
}
if err := loginTemplate.Execute(w, data); err != nil {
logger.Error(ctx, "could not execute login page template", logger.E(errors.WithStack(err)))
}
}
func (h *LocalHandler) handleForm(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := r.ParseForm(); err != nil {
logger.Error(ctx, "could not parse form", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
username := r.Form.Get("username")
password := r.Form.Get("password")
data := loginTemplateData{
URL: r.URL.String(),
Username: username,
Password: password,
Message: "",
}
account, err := h.authenticate(username, password)
if err != nil {
if errors.Is(err, errNotFound) || errors.Is(err, errInvalidPassword) {
data.Message = "Invalid username or password."
if err := loginTemplate.Execute(w, data); err != nil {
logger.Error(ctx, "could not execute login page template", logger.E(errors.WithStack(err)))
}
return
}
logger.Error(ctx, "could not authenticate account", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
account.Claims[auth.ClaimIssuer] = "local"
token, err := jwt.GenerateSignedToken(h.algo, h.key, account.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 := h.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(h.cookieDuration),
Path: "/",
}
http.SetCookie(w, &cookie)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (h *LocalHandler) handleLogout(w http.ResponseWriter, r *http.Request) {
cookieDomain, err := h.getCookieDomain(r)
if err != nil {
logger.Error(r.Context(), "could not retrieve cookie domain", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: auth.CookieName,
Value: "",
HttpOnly: false,
Expires: time.Unix(0, 0),
Domain: cookieDomain,
Path: "/",
})
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (h *LocalHandler) authenticate(username, password string) (*LocalAccount, error) {
account, exists := h.accounts[username]
if !exists {
return nil, errors.WithStack(errNotFound)
}
matches, err := passwd.Match(account.Algo, password, account.Password)
if err != nil {
return nil, errors.WithStack(err)
}
if !matches {
return nil, errors.WithStack(errInvalidPassword)
}
return &account, nil
}
func NewLocalHandler(algo jwa.KeyAlgorithm, key jwk.Key, funcs ...LocalHandlerOptionFunc) *LocalHandler {
opts := defaultLocalHandlerOptions()
for _, fn := range funcs {
fn(opts)
}
handler := &LocalHandler{
algo: algo,
key: key,
accounts: toAccountsMap(opts.Accounts),
getCookieDomain: opts.GetCookieDomain,
cookieDuration: opts.CookieDuration,
}
handler.initRouter(opts.RoutePrefix)
return handler
}
// ServeHTTP implements http.Handler.
func (h *LocalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.router.ServeHTTP(w, r)
}
var _ http.Handler = &LocalHandler{}

View File

@ -0,0 +1,49 @@
package http
import (
"net/http"
"time"
)
type GetCookieDomainFunc func(r *http.Request) (string, error)
func defaultGetCookieDomain(r *http.Request) (string, error) {
return "", nil
}
type LocalHandlerOptions struct {
RoutePrefix string
Accounts []LocalAccount
GetCookieDomain GetCookieDomainFunc
CookieDuration time.Duration
}
type LocalHandlerOptionFunc func(*LocalHandlerOptions)
func defaultLocalHandlerOptions() *LocalHandlerOptions {
return &LocalHandlerOptions{
RoutePrefix: "",
Accounts: make([]LocalAccount, 0),
GetCookieDomain: defaultGetCookieDomain,
CookieDuration: 24 * time.Hour,
}
}
func WithAccounts(accounts ...LocalAccount) LocalHandlerOptionFunc {
return func(opts *LocalHandlerOptions) {
opts.Accounts = accounts
}
}
func WithRoutePrefix(prefix string) LocalHandlerOptionFunc {
return func(opts *LocalHandlerOptions) {
opts.RoutePrefix = prefix
}
}
func WithCookieOptions(getCookieDomain GetCookieDomainFunc, duration time.Duration) LocalHandlerOptionFunc {
return func(opts *LocalHandlerOptions) {
opts.GetCookieDomain = getCookieDomain
opts.CookieDuration = duration
}
}

View File

@ -0,0 +1,136 @@
package argon2id
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt"
"strings"
"forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd"
"github.com/pkg/errors"
"golang.org/x/crypto/argon2"
)
const (
Algo passwd.Algo = "argon2id"
)
func init() {
passwd.Register(Algo, &Hasher{})
}
var (
ErrInvalidHash = errors.New("invalid hash")
ErrIncompatibleVersion = errors.New("incompatible version")
)
type params struct {
memory uint32
iterations uint32
parallelism uint8
saltLength uint32
keyLength uint32
}
var defaultParams = params{
memory: 64 * 1024,
iterations: 3,
parallelism: 2,
saltLength: 16,
keyLength: 32,
}
type Hasher struct{}
// Hash implements passwd.Hasher
func (*Hasher) Hash(plaintext string) (string, error) {
salt, err := generateRandomBytes(defaultParams.saltLength)
if err != nil {
return "", errors.WithStack(err)
}
hash := argon2.IDKey([]byte(plaintext), salt, defaultParams.iterations, defaultParams.memory, defaultParams.parallelism, defaultParams.keyLength)
// Base64 encode the salt and hashed password.
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
// Return a string using the standard encoded hash representation.
encodedHash := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, defaultParams.memory, defaultParams.iterations, defaultParams.parallelism, b64Salt, b64Hash)
return encodedHash, nil
}
// Match implements passwd.Hasher.
func (*Hasher) Match(plaintext string, hash string) (bool, error) {
matches, err := comparePasswordAndHash(plaintext, hash)
if err != nil {
return false, errors.WithStack(err)
}
return matches, nil
}
var _ passwd.Hasher = &Hasher{}
func generateRandomBytes(n uint32) ([]byte, error) {
buf := make([]byte, n)
if _, err := rand.Read(buf); err != nil {
return nil, errors.WithStack(err)
}
return buf, nil
}
func comparePasswordAndHash(password, encodedHash string) (match bool, err error) {
p, salt, hash, err := decodeHash(encodedHash)
if err != nil {
return false, errors.WithStack(err)
}
otherHash := argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength)
if subtle.ConstantTimeCompare(hash, otherHash) == 1 {
return true, nil
}
return false, nil
}
func decodeHash(encodedHash string) (p *params, salt, hash []byte, err error) {
vals := strings.Split(encodedHash, "$")
if len(vals) != 6 {
return nil, nil, nil, ErrInvalidHash
}
var version int
_, err = fmt.Sscanf(vals[2], "v=%d", &version)
if err != nil {
return nil, nil, nil, err
}
if version != argon2.Version {
return nil, nil, nil, ErrIncompatibleVersion
}
p = &params{}
_, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", &p.memory, &p.iterations, &p.parallelism)
if err != nil {
return nil, nil, nil, err
}
salt, err = base64.RawStdEncoding.Strict().DecodeString(vals[4])
if err != nil {
return nil, nil, nil, err
}
p.saltLength = uint32(len(salt))
hash, err = base64.RawStdEncoding.Strict().DecodeString(vals[5])
if err != nil {
return nil, nil, nil, err
}
p.keyLength = uint32(len(hash))
return p, salt, hash, nil
}

View File

@ -0,0 +1,8 @@
package passwd
type Algo string
type Hasher interface {
Hash(plaintext string) (string, error)
Match(plaintext string, hash string) (bool, error)
}

View File

@ -0,0 +1,31 @@
package plain
import (
"crypto/subtle"
"forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd"
)
const (
Algo passwd.Algo = "plain"
)
func init() {
passwd.Register(Algo, &Hasher{})
}
type Hasher struct{}
// Hash implements passwd.Hasher
func (*Hasher) Hash(plaintext string) (string, error) {
return plaintext, nil
}
// Match implements passwd.Hasher.
func (*Hasher) Match(plaintext string, hash string) (bool, error) {
matches := subtle.ConstantTimeCompare([]byte(plaintext), []byte(hash)) == 1
return matches, nil
}
var _ passwd.Hasher = &Hasher{}

View File

@ -0,0 +1,87 @@
package passwd
import (
"github.com/pkg/errors"
)
var ErrAlgoNotFound = errors.New("algo not found")
type Registry struct {
hashers map[Algo]Hasher
}
func (r *Registry) Register(algo Algo, hasher Hasher) {
r.hashers[algo] = hasher
}
func (r *Registry) Match(algo Algo, plaintext string, hash string) (bool, error) {
hasher, exists := r.hashers[algo]
if !exists {
return false, errors.WithStack(ErrAlgoNotFound)
}
matches, err := hasher.Match(plaintext, hash)
if err != nil {
return false, errors.WithStack(err)
}
return matches, nil
}
func (r *Registry) Hash(algo Algo, plaintext string) (string, error) {
hasher, exists := r.hashers[algo]
if !exists {
return "", errors.WithStack(ErrAlgoNotFound)
}
hash, err := hasher.Hash(plaintext)
if err != nil {
return "", errors.WithStack(err)
}
return hash, nil
}
func (r *Registry) Algorithms() []Algo {
algorithms := make([]Algo, 0, len(r.hashers))
for algo := range r.hashers {
algorithms = append(algorithms, algo)
}
return algorithms
}
func NewRegistry() *Registry {
return &Registry{
hashers: make(map[Algo]Hasher),
}
}
var defaultRegistry = NewRegistry()
func Match(algo Algo, plaintext string, hash string) (bool, error) {
matches, err := defaultRegistry.Match(algo, plaintext, hash)
if err != nil {
return false, errors.WithStack(err)
}
return matches, nil
}
func Hash(algo Algo, plaintext string) (string, error) {
hash, err := defaultRegistry.Hash(algo, plaintext)
if err != nil {
return "", errors.WithStack(err)
}
return hash, nil
}
func Algorithms() []Algo {
return defaultRegistry.Algorithms()
}
func Register(algo Algo, hasher Hasher) {
defaultRegistry.Register(algo, hasher)
}

View File

@ -0,0 +1,105 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Login</title>
<style>
html {
box-sizing: border-box;
font-size: 16px;
}
*, *:before, *:after {
box-sizing: inherit;
}
body, h1, h2, h3, h4, h5, h6, p, ol, ul {
margin: 0;
padding: 0;
font-weight: normal;
}
html, body {
width: 100%;
height: 100%;
font-family: Arial, Helvetica, sans-serif;
background-color: #f7f7f7;
}
#container {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
flex-direction: column;
}
.form-control {
margin-bottom: 0.5em;
}
.form-control > label {
font-weight: bold;
}
.form-control > input {
width: 100%;
line-height: 1.4em;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 1.2em;
padding: 0 5px;
margin: 5px 0;
}
#submit {
float: right;
background-color: #5e77ff;
padding: 5px 10px;
border: none;
border-radius: 5px;
color: white;
font-size: 1em;
cursor: pointer;
}
#submit:hover {
background-color: hsl(231deg 100% 71%);
}
#login {
padding: 1.5em 1em;
border: 1px solid #e0e0e0;
background-color: white;
border-radius: 5px;
box-shadow: 2px 2px #cccccc1c;
color: #333333 !important;
}
#message {
margin-bottom: 10px;
color: red;
text-shadow: 1px 1px #fff0f0;
}
</style>
</head>
<body>
<div id="container">
<p id="message">{{ .Message }}</p>
<div id="login">
<form method="post" action="{{ .URL }}">
<div class="form-control">
<label for="username">Username</label>
<input type="text" id="username" name="username" value="{{ .Username }}" required autofocus />
</div>
<div class="form-control">
<label for="password">Password</label>
<input type="password" id="password" name="password" value="{{ .Password }}" required />
</div>
<input id="submit" type="submit" value="Login" />
</form>
</div>
</div>
</body>
</html>

118
pkg/module/auth/jwt.go Normal file
View File

@ -0,0 +1,118 @@
package auth
import (
"context"
"net/http"
"strings"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/pkg/errors"
)
const (
CookieName string = "edge-auth"
)
type GetKeySetFunc func() (jwk.Set, error)
func WithJWT(getKeySet GetKeySetFunc) OptionFunc {
return func(o *Option) {
o.GetClaims = func(ctx context.Context, r *http.Request, names ...string) ([]string, error) {
claim, err := getClaims[string](r, getKeySet, names...)
if err != nil {
return nil, errors.WithStack(err)
}
return claim, nil
}
}
}
func FindRawToken(r *http.Request) (string, error) {
authorization := r.Header.Get("Authorization")
// Retrieve token from Authorization header
rawToken := strings.TrimPrefix(authorization, "Bearer ")
// Retrieve token from ?edge-auth=<value>
if rawToken == "" {
rawToken = r.URL.Query().Get(CookieName)
}
if rawToken == "" {
cookie, err := r.Cookie(CookieName)
if err != nil && !errors.Is(err, http.ErrNoCookie) {
return "", errors.WithStack(err)
}
if cookie != nil {
rawToken = cookie.Value
}
}
if rawToken == "" {
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()
if err != nil {
return nil, errors.WithStack(err)
}
if keySet == nil {
return nil, errors.New("no keyset")
}
token, err := jwt.Parse([]byte(rawToken),
jwt.WithKeySet(keySet, jws.WithRequireKid(false)),
jwt.WithValidate(true),
)
if err != nil {
return nil, errors.WithStack(err)
}
return token, nil
}
func getClaims[T any](r *http.Request, getKeySet GetKeySetFunc, names ...string) ([]T, error) {
token, err := FindToken(r, getKeySet)
if err != nil {
return nil, errors.WithStack(err)
}
ctx := r.Context()
mapClaims, err := token.AsMap(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
claims := make([]T, len(names))
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
}
return claims, nil
}

View File

@ -0,0 +1,35 @@
package jwt
import (
"time"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/pkg/errors"
)
func GenerateSignedToken(algo jwa.KeyAlgorithm, key jwk.Key, claims map[string]any) ([]byte, error) {
token := jwt.New()
if err := token.Set(jwt.NotBeforeKey, time.Now()); err != nil {
return nil, errors.WithStack(err)
}
for key, value := range claims {
if err := token.Set(key, value); err != nil {
return nil, errors.Wrapf(err, "could not set claim '%s' with value '%v'", key, value)
}
}
if err := token.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
return nil, errors.WithStack(err)
}
rawToken, err := jwt.Sign(token, jwt.WithKey(algo, key))
if err != nil {
return nil, errors.WithStack(err)
}
return rawToken, 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

@ -2,23 +2,27 @@ package auth
import ( import (
"net/http" "net/http"
"strings"
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module" edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
"forge.cadoles.com/arcad/edge/pkg/module/util" "forge.cadoles.com/arcad/edge/pkg/module/util"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/golang-jwt/jwt"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
) )
const ( const (
AnonymousSubject = "anonymous" 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
keyFunc jwt.Keyfunc getClaims GetClaimsFunc
} }
func (m *Module) Name() string { func (m *Module) Name() string {
@ -26,59 +30,71 @@ func (m *Module) Name() string {
} }
func (m *Module) Export(export *goja.Object) { func (m *Module) Export(export *goja.Object) {
if err := export.Set("getSubject", m.getSubject); err != nil { if err := export.Set("getClaim", m.getClaim); err != nil {
panic(errors.Wrap(err, "could not set 'getSubject' function")) panic(errors.Wrap(err, "could not set 'getClaim' function"))
} }
if err := export.Set("ANONYMOUS", AnonymousSubject); err != nil { if err := export.Set("CLAIM_SUBJECT", ClaimSubject); err != nil {
panic(errors.Wrap(err, "could not set 'ANONYMOUS_USER' property")) panic(errors.Wrap(err, "could not set 'CLAIM_SUBJECT' property"))
}
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) getSubject(call goja.FunctionCall, rt *goja.Runtime) goja.Value { func (m *Module) getClaim(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt) ctx := util.AssertContext(call.Argument(0), rt)
claimName := util.AssertString(call.Argument(1), rt)
req, ok := ctx.Value(module.ContextKeyOriginRequest).(*http.Request) req, ok := ctx.Value(edgeHTTP.ContextKeyOriginRequest).(*http.Request)
if !ok { if !ok {
panic(errors.New("could not find http request in context")) panic(rt.ToValue(errors.New("could not find http request in context")))
} }
rawToken := strings.TrimPrefix(req.Header.Get("Authorization"), "Bearer ") claim, err := m.getClaims(ctx, req, claimName)
if rawToken == "" {
rawToken = req.URL.Query().Get("token")
}
if rawToken == "" {
return rt.ToValue(AnonymousSubject)
}
token, err := jwt.Parse(rawToken, m.keyFunc)
if err != nil { if err != nil {
panic(errors.WithStack(err)) if errors.Is(err, ErrUnauthenticated) {
return nil
} }
if !token.Valid { logger.Error(ctx, "could not retrieve claim", logger.E(errors.WithStack(err)))
panic(errors.Errorf("invalid jwt token: '%v'", token.Raw)) return nil
} }
mapClaims, ok := token.Claims.(jwt.MapClaims) if len(claim) == 0 || claim[0] == "" {
if !ok { return nil
panic(errors.Errorf("unexpected claims type '%T'", token.Claims))
} }
subject, exists := mapClaims["sub"] return rt.ToValue(claim[0])
if !exists {
return rt.ToValue(AnonymousSubject)
} }
return rt.ToValue(subject) func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory {
opt := defaultOptions()
for _, fn := range funcs {
fn(opt)
} }
func ModuleFactory(keyFunc jwt.Keyfunc) app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule { return func(server *app.Server) app.ServerModule {
return &Module{ return &Module{
server: server, server: server,
keyFunc: keyFunc, getClaims: opt.GetClaims,
} }
} }
} }

View File

@ -2,7 +2,6 @@ package auth
import ( import (
"context" "context"
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"testing" "testing"
@ -10,8 +9,11 @@ import (
"cdr.dev/slog" "cdr.dev/slog"
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
"forge.cadoles.com/arcad/edge/pkg/module" "forge.cadoles.com/arcad/edge/pkg/module"
"github.com/golang-jwt/jwt" "github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
@ -21,11 +23,13 @@ func TestAuthModule(t *testing.T) {
logger.SetLevel(slog.LevelDebug) logger.SetLevel(slog.LevelDebug)
keyFunc, secret := getKeyFunc() key := getDummyKey()
server := app.NewServer( server := app.NewServer(
module.ConsoleModuleFactory(), module.ConsoleModuleFactory(),
ModuleFactory(keyFunc), ModuleFactory(
WithJWT(getDummyKeySet(key)),
),
) )
data, err := ioutil.ReadFile("testdata/auth.js") data, err := ioutil.ReadFile("testdata/auth.js")
@ -48,21 +52,26 @@ func TestAuthModule(t *testing.T) {
t.Fatalf("%+v", errors.WithStack(err)) t.Fatalf("%+v", errors.WithStack(err))
} }
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ token := jwt.New()
"sub": "jdoe",
"nbf": time.Now().UTC().Unix(),
})
rawToken, err := token.SignedString(secret) if err := token.Set(jwt.SubjectKey, "jdoe"); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if err := token.Set(jwt.NotBeforeKey, time.Now()); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
rawToken, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, key))
if err != nil { if err != nil {
t.Fatalf("%+v", errors.WithStack(err)) t.Fatalf("%+v", errors.WithStack(err))
} }
req.Header.Add("Authorization", "Bearer "+rawToken) req.Header.Add("Authorization", "Bearer "+string(rawToken))
ctx := context.WithValue(context.Background(), module.ContextKeyOriginRequest, req) ctx := context.WithValue(context.Background(), edgeHTTP.ContextKeyOriginRequest, req)
if _, err := server.ExecFuncByName("testAuth", ctx); err != nil { if _, err := server.ExecFuncByName(ctx, "testAuth", ctx); err != nil {
t.Fatalf("%+v", errors.WithStack(err)) t.Fatalf("%+v", errors.WithStack(err))
} }
} }
@ -72,11 +81,11 @@ func TestAuthAnonymousModule(t *testing.T) {
logger.SetLevel(slog.LevelDebug) logger.SetLevel(slog.LevelDebug)
keyFunc, _ := getKeyFunc() key := getDummyKey()
server := app.NewServer( server := app.NewServer(
module.ConsoleModuleFactory(), module.ConsoleModuleFactory(),
ModuleFactory(keyFunc), ModuleFactory(WithJWT(getDummyKeySet(key))),
) )
data, err := ioutil.ReadFile("testdata/auth_anonymous.js") data, err := ioutil.ReadFile("testdata/auth_anonymous.js")
@ -99,23 +108,36 @@ func TestAuthAnonymousModule(t *testing.T) {
t.Fatalf("%+v", errors.WithStack(err)) t.Fatalf("%+v", errors.WithStack(err))
} }
ctx := context.WithValue(context.Background(), module.ContextKeyOriginRequest, req) ctx := context.WithValue(context.Background(), edgeHTTP.ContextKeyOriginRequest, req)
if _, err := server.ExecFuncByName("testAuth", ctx); err != nil { if _, err := server.ExecFuncByName(ctx, "testAuth", ctx); err != nil {
t.Fatalf("%+v", errors.WithStack(err)) t.Fatalf("%+v", errors.WithStack(err))
} }
} }
func getKeyFunc() (jwt.Keyfunc, []byte) { func getDummyKey() jwk.Key {
secret := []byte("not_so_secret") secret := []byte("not_so_secret")
keyFunc := func(t *jwt.Token) (interface{}, error) { key, err := jwk.FromRaw(secret)
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { if err != nil {
return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"]) panic(errors.WithStack(err))
} }
return secret, nil if err := key.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
panic(errors.WithStack(err))
} }
return keyFunc, secret return key
}
func getDummyKeySet(key jwk.Key) GetKeySetFunc {
return func() (jwk.Set, error) {
set := jwk.NewSet()
if err := set.AddKey(key); err != nil {
return nil, errors.WithStack(err)
}
return set, nil
}
} }

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

47
pkg/module/auth/option.go Normal file
View File

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

View File

@ -1,7 +1,7 @@
function testAuth(ctx) { function testAuth(ctx) {
var subject = auth.getSubject(ctx); var subject = auth.getClaim(ctx, auth.CLAIM_SUBJECT);
if (subject !== "jdoe") { if (subject !== "jdoe") {
throw new Error("subject: expected 'jdoe', got '"+subject+"'"); throw new Error("subject: expected 'jdoe', got '"+subject+"'");

View File

@ -1,9 +1,9 @@
function testAuth(ctx) { function testAuth(ctx) {
var subject = auth.getSubject(ctx); var subject = auth.getClaim(ctx, auth.CLAIM_SUBJECT);
if (subject !== auth.ANONYMOUS) { if (subject !== undefined) {
throw new Error("subject: expected '"+auth.ANONYMOUS+"', got '"+subject+"'"); throw new Error("subject: expected undefined, got '"+subject+"'");
} }
} }

View File

@ -1,109 +0,0 @@
package module
// import (
// "context"
// "sync"
// "forge.cadoles.com/arcad/edge/pkg/app"
// "forge.cadoles.com/arcad/edge/pkg/bus"
// "forge.cadoles.com/arcad/edge/pkg/repository"
// "github.com/dop251/goja"
// "github.com/pkg/errors"
// "gitlab.com/wpetit/goweb/logger"
// )
// type AuthorizationModule struct {
// appID app.ID
// bus bus.Bus
// backend *app.Server
// admins sync.Map
// }
// func (m *AuthorizationModule) Name() string {
// return "authorization"
// }
// func (m *AuthorizationModule) Export(export *goja.Object) {
// if err := export.Set("isAdmin", m.isAdmin); err != nil {
// panic(errors.Wrap(err, "could not set 'register' function"))
// }
// }
// func (m *AuthorizationModule) isAdmin(call goja.FunctionCall) goja.Value {
// userID := call.Argument(0).String()
// if userID == "" {
// panic(errors.New("first argument must be a user id"))
// }
// rawValue, exists := m.admins.Load(repository.UserID(userID))
// if !exists {
// return m.backend.ToValue(false)
// }
// isAdmin, ok := rawValue.(bool)
// if !ok {
// return m.backend.ToValue(false)
// }
// return m.backend.ToValue(isAdmin)
// }
// func (m *AuthorizationModule) handleEvents() {
// ctx := logger.With(context.Background(), logger.F("moduleAppID", m.appID))
// ns := AppMessageNamespace(m.appID)
// userConnectedMessages, err := m.bus.Subscribe(ctx, ns, MessageTypeUserConnected)
// if err != nil {
// panic(errors.WithStack(err))
// }
// userDisconnectedMessages, err := m.bus.Subscribe(ctx, ns, MessageTypeUserDisconnected)
// if err != nil {
// panic(errors.WithStack(err))
// }
// defer func() {
// m.bus.Unsubscribe(ctx, ns, MessageTypeUserConnected, userConnectedMessages)
// m.bus.Unsubscribe(ctx, ns, MessageTypeUserDisconnected, userDisconnectedMessages)
// }()
// for {
// select {
// case msg := <-userConnectedMessages:
// userConnectedMsg, ok := msg.(*MessageUserConnected)
// if !ok {
// continue
// }
// logger.Debug(ctx, "user connected", logger.F("msg", userConnectedMsg))
// m.admins.Store(userConnectedMsg.UserID, userConnectedMsg.IsAdmin)
// case msg := <-userDisconnectedMessages:
// userDisconnectedMsg, ok := msg.(*MessageUserDisconnected)
// if !ok {
// continue
// }
// logger.Debug(ctx, "user disconnected", logger.F("msg", userDisconnectedMsg))
// m.admins.Delete(userDisconnectedMsg.UserID)
// }
// }
// }
// func AuthorizationModuleFactory(b bus.Bus) app.ServerModuleFactory {
// return func(appID app.ID, backend *app.Server) app.ServerModule {
// mod := &AuthorizationModule{
// appID: appID,
// bus: b,
// backend: backend,
// admins: sync.Map{},
// }
// go mod.handleEvents()
// return mod
// }
// }

View File

@ -1,103 +0,0 @@
package module
// import (
// "context"
// "io/ioutil"
// "testing"
// "time"
// "forge.cadoles.com/arcad/edge/pkg/app"
// "forge.cadoles.com/arcad/edge/pkg/bus/memory"
// )
// func TestAuthorizationModule(t *testing.T) {
// t.Parallel()
// testAppID := app.ID("test-app")
// b := memory.NewBus()
// backend := app.NewServer(testAppID,
// ConsoleModuleFactory(),
// AuthorizationModuleFactory(b),
// )
// data, err := ioutil.ReadFile("testdata/authorization.js")
// if err != nil {
// t.Fatal(err)
// }
// if err := backend.Load(string(data)); err != nil {
// t.Fatal(err)
// }
// backend.Start()
// defer backend.Stop()
// if err := backend.OnInit(); err != nil {
// t.Error(err)
// }
// // Test non connected user
// retValue, err := backend.ExecFuncByName("isAdmin", testUserID)
// if err != nil {
// t.Error(err)
// }
// isAdmin := retValue.ToBoolean()
// if e, g := false, isAdmin; e != g {
// t.Errorf("isAdmin: expected '%v', got '%v'", e, g)
// }
// // Test user connection as normal user
// ctx := context.Background()
// b.Publish(ctx, NewMessageUserConnected(testAppID, testUserID, false))
// time.Sleep(2 * time.Second)
// retValue, err = backend.ExecFuncByName("isAdmin", testUserID)
// if err != nil {
// t.Error(err)
// }
// isAdmin = retValue.ToBoolean()
// if e, g := false, isAdmin; e != g {
// t.Errorf("isAdmin: expected '%v', got '%v'", e, g)
// }
// // Test user connection as admin
// b.Publish(ctx, NewMessageUserConnected(testAppID, testUserID, true))
// time.Sleep(2 * time.Second)
// retValue, err = backend.ExecFuncByName("isAdmin", testUserID)
// if err != nil {
// t.Error(err)
// }
// isAdmin = retValue.ToBoolean()
// if e, g := true, isAdmin; e != g {
// t.Errorf("isAdmin: expected '%v', got '%v'", e, g)
// }
// // Test user disconnection
// b.Publish(ctx, NewMessageUserDisconnected(testAppID, testUserID))
// time.Sleep(2 * time.Second)
// retValue, err = backend.ExecFuncByName("isAdmin", testUserID)
// if err != nil {
// t.Error(err)
// }
// isAdmin = retValue.ToBoolean()
// if e, g := false, isAdmin; e != g {
// t.Errorf("isAdmin: expected '%v', got '%v'", e, g)
// }
// }

View File

@ -1,282 +0,0 @@
package module
import (
"context"
"io"
"mime/multipart"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bus"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/dop251/goja"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
const (
DefaultBlobBucket string = "default"
)
type BlobModule struct {
server *app.Server
bus bus.Bus
store storage.BlobStore
}
func (m *BlobModule) Name() string {
return "blob"
}
func (m *BlobModule) Export(export *goja.Object) {
}
func (m *BlobModule) handleMessages() {
ctx := context.Background()
go func() {
err := m.bus.Reply(ctx, MessageNamespaceUploadRequest, func(msg bus.Message) (bus.Message, error) {
uploadRequest, ok := msg.(*MessageUploadRequest)
if !ok {
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message upload request, got '%T'", msg)
}
res, err := m.handleUploadRequest(uploadRequest)
if err != nil {
logger.Error(ctx, "could not handle upload request", logger.E(errors.WithStack(err)))
return nil, errors.WithStack(err)
}
logger.Debug(ctx, "upload request response", logger.F("response", res))
return res, nil
})
if err != nil {
panic(errors.WithStack(err))
}
}()
err := m.bus.Reply(ctx, MessageNamespaceDownloadRequest, func(msg bus.Message) (bus.Message, error) {
downloadRequest, ok := msg.(*MessageDownloadRequest)
if !ok {
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message download request, got '%T'", msg)
}
res, err := m.handleDownloadRequest(downloadRequest)
if err != nil {
logger.Error(ctx, "could not handle download request", logger.E(errors.WithStack(err)))
return nil, errors.WithStack(err)
}
return res, nil
})
if err != nil {
panic(errors.WithStack(err))
}
}
func (m *BlobModule) handleUploadRequest(req *MessageUploadRequest) (*MessageUploadResponse, error) {
blobID := storage.NewBlobID()
res := NewMessageUploadResponse(req.RequestID)
ctx := logger.With(req.Context, logger.F("blobID", blobID))
blobInfo := map[string]interface{}{
"size": req.FileHeader.Size,
"filename": req.FileHeader.Filename,
"contentType": req.FileHeader.Header.Get("Content-Type"),
}
rawResult, err := m.server.ExecFuncByName("onBlobUpload", ctx, blobID, blobInfo, req.Metadata)
if err != nil {
if errors.Is(err, app.ErrFuncDoesNotExist) {
res.Allow = false
return res, nil
}
return nil, errors.WithStack(err)
}
result, ok := rawResult.Export().(map[string]interface{})
if !ok {
return nil, errors.Errorf(
"unexpected onBlobUpload result: expected 'map[string]interface{}', got '%T'",
rawResult.Export(),
)
}
var allow bool
rawAllow, exists := result["allow"]
if !exists {
allow = false
} else {
allow, ok = rawAllow.(bool)
if !ok {
return nil, errors.Errorf("invalid 'allow' result property: got type '%T', expected type '%T'", rawAllow, false)
}
}
res.Allow = allow
if res.Allow {
bucket := DefaultBlobBucket
rawBucket, exists := result["bucket"]
if exists {
bucket, ok = rawBucket.(string)
if !ok {
return nil, errors.Errorf("invalid 'bucket' result property: got type '%T', expected type '%T'", bucket, "")
}
}
if err := m.saveBlob(ctx, bucket, blobID, *req.FileHeader); err != nil {
return nil, errors.WithStack(err)
}
res.Bucket = bucket
res.BlobID = blobID
}
return res, nil
}
func (m *BlobModule) saveBlob(ctx context.Context, bucketName string, blobID storage.BlobID, fileHeader multipart.FileHeader) error {
file, err := fileHeader.Open()
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := file.Close(); err != nil {
logger.Error(ctx, "could not close file", logger.E(errors.WithStack(err)))
}
}()
bucket, err := m.store.OpenBucket(ctx, bucketName)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := bucket.Close(); err != nil {
logger.Error(ctx, "could not close bucket", logger.E(errors.WithStack(err)))
}
}()
writer, err := bucket.NewWriter(ctx, blobID)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := file.Close(); err != nil {
logger.Error(ctx, "could not close file", logger.E(errors.WithStack(err)))
}
}()
defer func() {
if err := writer.Close(); err != nil {
logger.Error(ctx, "could not close writer", logger.E(errors.WithStack(err)))
}
}()
if _, err := io.Copy(writer, file); err != nil {
return errors.WithStack(err)
}
return nil
}
func (m *BlobModule) handleDownloadRequest(req *MessageDownloadRequest) (*MessageDownloadResponse, error) {
res := NewMessageDownloadResponse(req.RequestID)
rawResult, err := m.server.ExecFuncByName("onBlobDownload", req.Context, req.Bucket, req.BlobID)
if err != nil {
if errors.Is(err, app.ErrFuncDoesNotExist) {
res.Allow = false
return res, nil
}
return nil, errors.WithStack(err)
}
result, ok := rawResult.Export().(map[string]interface{})
if !ok {
return nil, errors.Errorf(
"unexpected onBlobDownload result: expected 'map[string]interface{}', got '%T'",
rawResult.Export(),
)
}
var allow bool
rawAllow, exists := result["allow"]
if !exists {
allow = false
} else {
allow, ok = rawAllow.(bool)
if !ok {
return nil, errors.Errorf("invalid 'allow' result property: got type '%T', expected type '%T'", rawAllow, false)
}
}
res.Allow = allow
reader, info, err := m.openBlob(req.Context, req.Bucket, req.BlobID)
if err != nil && !errors.Is(err, storage.ErrBlobNotFound) {
return nil, errors.WithStack(err)
}
if reader != nil {
res.Blob = reader
}
if info != nil {
res.BlobInfo = info
}
return res, nil
}
func (m *BlobModule) openBlob(ctx context.Context, bucketName string, blobID storage.BlobID) (io.ReadSeekCloser, storage.BlobInfo, error) {
bucket, err := m.store.OpenBucket(ctx, bucketName)
if err != nil {
return nil, nil, errors.WithStack(err)
}
defer func() {
if err := bucket.Close(); err != nil {
logger.Error(ctx, "could not close bucket", logger.E(errors.WithStack(err)), logger.F("bucket", bucket))
}
}()
info, err := bucket.Get(ctx, blobID)
if err != nil {
return nil, nil, errors.WithStack(err)
}
reader, err := bucket.NewReader(ctx, blobID)
if err != nil {
return nil, nil, errors.WithStack(err)
}
return reader, info, nil
}
func BlobModuleFactory(bus bus.Bus, store storage.BlobStore) app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule {
mod := &BlobModule{
store: store,
bus: bus,
server: server,
}
go mod.handleMessages()
return mod
}
}

View File

@ -0,0 +1,21 @@
package blob
import "forge.cadoles.com/arcad/edge/pkg/storage"
type blobInfo struct {
ID storage.BlobID `goja:"id"`
Bucket string `goja:"bucket"`
ModTime int64 `goja:"modTime"`
Size int64 `goja:"size"`
ContentType string `goja:"contentType"`
}
func toGojaBlobInfo(blob storage.BlobInfo) blobInfo {
return blobInfo{
ID: blob.ID(),
Bucket: blob.Bucket(),
ModTime: blob.ModTime().Unix(),
Size: blob.Size(),
ContentType: blob.ContentType(),
}
}

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