Compare commits
36 Commits
4eb1f8f
...
v2023.4.6-
Author | SHA1 | Date | |
---|---|---|---|
240b07af66 | |||
68e35bf5a6 | |||
4bc2d864ad | |||
dc18381dea | |||
1dde96043a | |||
f758acb4e5 | |||
054e80bbfb | |||
32c6f0a77e | |||
050e529f0a | |||
006f13bc7b | |||
84c8fd51f6 | |||
f08f645432 | |||
fbb27d6ea4 | |||
d8ce2901d2 | |||
1996f4dc56 | |||
e09de0b0a4 | |||
72765de20b | |||
ed535b6f5d | |||
07452ad8ab | |||
0f0fdfb02b | |||
9eefce9b41 | |||
0577762be9 | |||
cf8a3f8ac0 | |||
1f4f795d43 | |||
fd12d2ba42 | |||
6399196fe5 | |||
fef0321475 | |||
a9d2c282f2 | |||
0bb7f2cd85 | |||
b61bf52df9 | |||
f1dd467c95 | |||
3136d71032 | |||
8680e139e7 | |||
8789b85d92 | |||
fefcba5901 | |||
5ad4ab2e23 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,7 +1,7 @@
|
||||
/node_modules
|
||||
/bin
|
||||
/pkg/sdk/client/dist
|
||||
/.env
|
||||
/tools
|
||||
*.sqlite
|
||||
/.gitea-release
|
||||
/.edge
|
49
Jenkinsfile
vendored
Normal file
49
Jenkinsfile
vendored
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
26
Makefile
26
Makefile
@ -2,13 +2,15 @@ LINT_ARGS ?= --timeout 5m
|
||||
GITCHLOG_ARGS ?=
|
||||
SHELL := /bin/bash
|
||||
|
||||
GOTEST_ARGS ?= -short
|
||||
GOTEST_ARGS ?= -short -timeout 60s
|
||||
|
||||
ESBUILD_VERSION ?= v0.17.5
|
||||
|
||||
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,)
|
||||
|
||||
build: build-edge-cli
|
||||
build: build-edge-cli build-client-sdk-test-app
|
||||
|
||||
watch:
|
||||
go run -mod=readonly github.com/cortesi/modd/cmd/modd@latest
|
||||
@ -28,10 +30,12 @@ build-edge-cli: build-sdk
|
||||
-o ./bin/cli \
|
||||
./cmd/cli
|
||||
|
||||
build-client-sdk-test-app:
|
||||
cd misc/client-sdk-testsuite && $(MAKE) dist
|
||||
|
||||
install-git-hooks:
|
||||
git config core.hooksPath .githooks
|
||||
|
||||
|
||||
tools/esbuild/bin/esbuild:
|
||||
mkdir -p tools/esbuild/bin
|
||||
curl -fsSL https://esbuild.github.io/dl/$(ESBUILD_VERSION) | sh
|
||||
@ -51,34 +55,40 @@ pkg/sdk/client/dist/client.js: tools/esbuild/bin/esbuild node_modules
|
||||
--global-name=Edge \
|
||||
--define:global=window \
|
||||
--platform=browser \
|
||||
--footer:js="Edge=Edge.default;" \
|
||||
--footer:js="EdgeFrame=Edge.crossFrameMessenger;Edge=Edge.client" \
|
||||
--outfile=pkg/sdk/client/dist/client.js
|
||||
|
||||
node_modules:
|
||||
npm ci
|
||||
|
||||
gitea-release: tools/gitea-release/bin/gitea-release.sh build
|
||||
gitea-release: tools/yq/bin/yq tools/gitea-release/bin/gitea-release.sh build
|
||||
mkdir -p .gitea-release
|
||||
rm -rf .gitea-release/*
|
||||
|
||||
cp bin/cli .gitea-release/edge_cli_amd64
|
||||
|
||||
# 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_PROJECT="edge" \
|
||||
GITEA_RELEASE_ORG="arcad" \
|
||||
GITEA_RELEASE_BASE_URL="https://forge.cadoles.com" \
|
||||
GITEA_RELEASE_VERSION="$(GIT_VERSION)" \
|
||||
GITEA_RELEASE_NAME="$(GIT_VERSION)" \
|
||||
GITEA_RELEASE_VERSION="$(FULL_VERSION)" \
|
||||
GITEA_RELEASE_NAME="$(FULL_VERSION)" \
|
||||
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \
|
||||
GITEA_RELEASE_IS_DRAFT="false" \
|
||||
GITEA_RELEASE_IS_PRERELEASE="true" \
|
||||
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:
|
||||
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
|
||||
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
|
50
cmd/cli/command/app/default-accounts.json
Normal file
50
cmd/cli/command/app/default-accounts.json
Normal file
@ -0,0 +1,50 @@
|
||||
[
|
||||
{
|
||||
"username": "superadmin",
|
||||
"algo": "argon2id",
|
||||
"password": "$argon2id$v=19$m=65536,t=3,p=2$cWOxfEyBy4EyKZR5usB2Pw$xG+Z/E2DUJP9kF0s1fhZjIuP03gFQ65dP7pHRJz7eR8",
|
||||
"claims": {
|
||||
"arcad_entrypoint": "edge",
|
||||
"arcad_role": "superadmin",
|
||||
"arcad_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": {
|
||||
"arcad_entrypoint": "edge",
|
||||
"arcad_role": "admin",
|
||||
"arcad_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": {
|
||||
"arcad_entrypoint": "edge",
|
||||
"arcad_role": "superuser",
|
||||
"arcad_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": {
|
||||
"arcad_entrypoint": "edge",
|
||||
"arcad_role": "user",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"preferred_username": "User",
|
||||
"sub": "user"
|
||||
}
|
||||
}
|
||||
]
|
47
cmd/cli/command/app/hash-password.go
Normal file
47
cmd/cli/command/app/hash-password.go
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
@ -47,7 +47,7 @@ func PackageCommand() *cli.Command {
|
||||
|
||||
bundle := bundle.NewDirectoryBundle(appDir)
|
||||
|
||||
manifest, err := app.LoadAppManifest(bundle)
|
||||
manifest, err := app.LoadManifest(bundle)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load app manifest")
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ func Root() *cli.Command {
|
||||
Subcommands: []*cli.Command{
|
||||
RunCommand(),
|
||||
PackageCommand(),
|
||||
HashPasswordCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,29 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
||||
appHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||
"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"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/blob"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/net"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/fetch"
|
||||
netModule "forge.cadoles.com/arcad/edge/pkg/module/net"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
@ -23,11 +32,15 @@ import (
|
||||
"github.com/dop251/goja"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
"github.com/pkg/errors"
|
||||
"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 {
|
||||
@ -60,22 +73,12 @@ func RunCommand() *cli.Command {
|
||||
&cli.StringFlag{
|
||||
Name: "storage-file",
|
||||
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: "auth-subject",
|
||||
Usage: "set the `SUBJECT` associated with the simulated connected user",
|
||||
Value: "jdoe",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth-role",
|
||||
Usage: "set the `ROLE` associated with the simulated connected user",
|
||||
Value: "user",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth-preferred-username",
|
||||
Usage: "set the `PREFERRED_USERNAME` associated with the simulated connected user",
|
||||
Value: "Jane Doe",
|
||||
Name: "accounts-file",
|
||||
Usage: "use `FILE` as local accounts",
|
||||
Value: ".edge/%APPID%/accounts.json",
|
||||
},
|
||||
},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
@ -85,12 +88,6 @@ func RunCommand() *cli.Command {
|
||||
logFormat := ctx.String("log-format")
|
||||
logLevel := ctx.Int("log-level")
|
||||
|
||||
storageFile := ctx.String("storage-file")
|
||||
|
||||
authSubject := ctx.String("auth-subject")
|
||||
authRole := ctx.String("auth-role")
|
||||
authPreferredUsername := ctx.String("auth-preferred-username")
|
||||
|
||||
logger.SetFormat(logger.Format(logFormat))
|
||||
logger.SetLevel(logger.Level(logLevel))
|
||||
|
||||
@ -108,34 +105,61 @@ func RunCommand() *cli.Command {
|
||||
return errors.Wrapf(err, "could not open path '%s' as an app bundle", path)
|
||||
}
|
||||
|
||||
mux := chi.NewMux()
|
||||
|
||||
mux.Use(middleware.Logger)
|
||||
mux.Use(dummyAuthMiddleware(authSubject, authRole, authPreferredUsername))
|
||||
|
||||
bus := memory.NewBus()
|
||||
|
||||
db, err := sql.Open("sqlite", storageFile)
|
||||
manifest, err := app.LoadManifest(bundle)
|
||||
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")
|
||||
}
|
||||
|
||||
storageFile := injectAppID(ctx.String("storage-file"), manifest.ID)
|
||||
|
||||
if err := ensureDir(storageFile); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
db, err := sqlite.Open(storageFile)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
ds := sqlite.NewDocumentStoreWithDB(db)
|
||||
bs := sqlite.NewBlobStoreWithDB(db)
|
||||
bus := memory.NewBus()
|
||||
|
||||
handler := appHTTP.NewHandler(
|
||||
appHTTP.WithBus(bus),
|
||||
appHTTP.WithServerModules(getServerModules(bus, ds, bs)...),
|
||||
appHTTP.WithServerModules(getServerModules(bus, ds, bs, manifest, address)...),
|
||||
)
|
||||
if err := handler.Load(bundle); err != nil {
|
||||
return errors.Wrap(err, "could not load app bundle")
|
||||
}
|
||||
|
||||
mux.Handle("/*", handler)
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.Logger)
|
||||
|
||||
accountsFile := injectAppID(ctx.String("accounts-file"), manifest.ID)
|
||||
|
||||
accounts, err := loadLocalAccounts(accountsFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load local accounts")
|
||||
}
|
||||
|
||||
// Add auth handler
|
||||
key, err := dummyKey()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
router.Handle("/auth/*", authHTTP.NewLocalHandler(
|
||||
jwa.HS256, key,
|
||||
authHTTP.WithRoutePrefix("/auth"),
|
||||
authHTTP.WithAccounts(accounts...),
|
||||
))
|
||||
|
||||
// Add app handler
|
||||
router.Handle("/*", handler)
|
||||
|
||||
logger.Info(cmdCtx, "listening", logger.F("address", address))
|
||||
|
||||
if err := http.ListenAndServe(address, mux); err != nil {
|
||||
if err := http.ListenAndServe(address, router); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
@ -144,22 +168,30 @@ func RunCommand() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStore) []app.ServerModuleFactory {
|
||||
func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStore, manifest *app.Manifest, address string) []app.ServerModuleFactory {
|
||||
return []app.ServerModuleFactory{
|
||||
module.ContextModuleFactory(),
|
||||
module.ConsoleModuleFactory(),
|
||||
cast.CastModuleFactory(),
|
||||
module.LifecycleModuleFactory(),
|
||||
net.ModuleFactory(bus),
|
||||
netModule.ModuleFactory(bus),
|
||||
module.RPCModuleFactory(bus),
|
||||
module.StoreModuleFactory(ds),
|
||||
module.BlobModuleFactory(bus, bs),
|
||||
blob.ModuleFactory(bus, bs),
|
||||
module.Extends(
|
||||
auth.ModuleFactory(
|
||||
auth.WithJWT(dummyKeyFunc),
|
||||
auth.WithJWT(dummyKeySet),
|
||||
),
|
||||
func(o *goja.Object) {
|
||||
if err := o.Set("CLAIM_ROLE", "role"); err != nil {
|
||||
if err := o.Set("CLAIM_TENANT", "arcad_tenant"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_TENANT' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_ENTRYPOINT", "arcad_entrypoint"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ENTRYPOINT' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_ROLE", "arcad_role"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ROLE' property"))
|
||||
}
|
||||
|
||||
@ -168,64 +200,148 @@ func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStor
|
||||
}
|
||||
},
|
||||
),
|
||||
appModule.ModuleFactory(appModuleMemory.NewRepository(
|
||||
func(ctx context.Context, id app.ID, from string) (string, error) {
|
||||
addr := address
|
||||
if strings.HasPrefix(addr, ":") {
|
||||
addr = "0.0.0.0" + addr
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
addr, err = findMatchingDeviceAddress(ctx, from, host)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("http://%s:%s", addr, port), nil
|
||||
},
|
||||
manifest,
|
||||
)),
|
||||
fetch.ModuleFactory(bus),
|
||||
}
|
||||
}
|
||||
|
||||
var dummySecret = []byte("not_so_secret")
|
||||
|
||||
func dummyKeyFunc(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
|
||||
return dummySecret, nil
|
||||
}
|
||||
|
||||
func dummyAuthMiddleware(subject, role, username string) func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
unauthenticated := subject == "" && role == "" && username == ""
|
||||
|
||||
if unauthenticated {
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"nbf": time.Now().UTC().Unix(),
|
||||
}
|
||||
|
||||
if subject != "" {
|
||||
claims["sub"] = subject
|
||||
}
|
||||
|
||||
if role != "" {
|
||||
claims["role"] = role
|
||||
}
|
||||
|
||||
if username != "" {
|
||||
claims["preferred_username"] = username
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
rawToken, err := token.SignedString(dummySecret)
|
||||
func dummyKey() (jwk.Key, error) {
|
||||
key, err := jwk.FromRaw(dummySecret)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not sign token", logger.E(errors.WithStack(err)))
|
||||
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
r.Header.Add("Authorization", "Bearer "+rawToken)
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
return ip.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return defaultAddr, nil
|
||||
}
|
||||
|
40
cmd/cli/command/cast/load_url.go
Normal file
40
cmd/cli/command/cast/load_url.go
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
16
cmd/cli/command/cast/root.go
Normal file
16
cmd/cli/command/cast/root.go
Normal 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(),
|
||||
},
|
||||
}
|
||||
}
|
37
cmd/cli/command/cast/scan.go
Normal file
37
cmd/cli/command/cast/scan.go
Normal file
@ -0,0 +1,37 @@
|
||||
package cast
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/barnybug/go-cast/discovery"
|
||||
"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{},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
service := discovery.NewService(ctx.Context)
|
||||
defer service.Stop()
|
||||
|
||||
go func() {
|
||||
if err := service.Run(ctx.Context, time.Second); err != nil && !errors.Is(err, context.DeadlineExceeded) {
|
||||
log.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
}()
|
||||
|
||||
found := service.Found()
|
||||
|
||||
for device := range found {
|
||||
log.Printf("[DEVICE] %s %s %s:%d", device.Uuid(), device.Name(), device.IP().String(), device.Port())
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
@ -3,8 +3,9 @@ package main
|
||||
import (
|
||||
"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/cast"
|
||||
)
|
||||
|
||||
func main() {
|
||||
command.Main(app.Root())
|
||||
command.Main(app.Root(), cast.Root())
|
||||
}
|
||||
|
@ -1,64 +1,14 @@
|
||||
# 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`
|
||||
|
||||
### `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;
|
||||
}
|
||||
```html
|
||||
<script src="/edge/sdk/client.js"></script>
|
||||
```
|
||||
|
||||
**Côté client**
|
||||
Vous pourrez ensuite accéder aux variables globales suivantes:
|
||||
|
||||
```js
|
||||
Edge.connect().then(() => {
|
||||
Edge.rpc("echo", { hello: "world!" })
|
||||
.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));
|
||||
```
|
||||
- [`Edge`](./edge.md) - Client principal d'échange avec le serveur
|
||||
- [`EdgeFrame`](./edge-frame.md)
|
30
doc/apps/client-api/edge-frame.md
Normal file
30
doc/apps/client-api/edge-frame.md
Normal file
@ -0,0 +1,30 @@
|
||||
# `EdgeFrame`
|
||||
|
||||
## Méthodes
|
||||
|
||||
### `EdgeFrame.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
|
||||
}
|
||||
}
|
||||
```
|
68
doc/apps/client-api/edge.md
Normal file
68
doc/apps/client-api/edge.md
Normal file
@ -0,0 +1,68 @@
|
||||
# `Edge`
|
||||
|
||||
## Méthodes
|
||||
|
||||
### `Edge.connect(): Promise`
|
||||
|
||||
> `TODO`
|
||||
|
||||
### `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**
|
||||
|
||||
```js
|
||||
Edge.connect().then(() => {
|
||||
Edge.rpc("echo", { hello: "world!" })
|
||||
.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`
|
||||
|
||||
### `Edge.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.addEventListener("message", evt => console.log(evt.detail));
|
||||
```
|
@ -20,11 +20,13 @@ function onInit() {
|
||||
|
||||
Listes des modules disponibles côté serveur.
|
||||
|
||||
- [`app`](./app.md)
|
||||
- [`auth`](./auth.md)
|
||||
- [`blob`](./blob.md)
|
||||
- [`cast`](./cast.md)
|
||||
- [`console`](./console.md)
|
||||
- [`context`](./context.md)
|
||||
- [`fetch`](./fetch.md)
|
||||
- [`net`](./net.md)
|
||||
- [`rpc`](./rpc.md)
|
||||
- [`store`](./store.md)
|
||||
|
58
doc/apps/server-api/app.md
Normal file
58
doc/apps/server-api/app.md
Normal file
@ -0,0 +1,58 @@
|
||||
# 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
|
||||
}
|
||||
```
|
@ -38,11 +38,15 @@ function onBlobDownload(ctx, bucketName, blobId) {
|
||||
|
||||
> `TODO`
|
||||
|
||||
### `blob.writeBlob(ctx: Context, bucketName: string, blobId: string)`
|
||||
### `blob.getBlobInfo(ctx: Context, bucketName: string, blobId: string): BlobInfo`
|
||||
|
||||
> `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`
|
||||
|
||||
@ -58,7 +62,7 @@ function onBlobDownload(ctx, bucketName, blobId) {
|
||||
|
||||
> `TODO`
|
||||
|
||||
### `blob.getBlobInfo(ctx: Context, bucketName: string, blobId: string): BlobInfo`
|
||||
### `blob.getBucketSize(ctx: Context, bucketName: string): number`
|
||||
|
||||
> `TODO`
|
||||
|
||||
@ -70,4 +74,16 @@ Voir la documentation de l'objet [`Context`](./context.md#Context).
|
||||
|
||||
### `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`
|
||||
|
||||
L'objet `Metadata` est un objet clé/valeur arbitraire transmis avec la requête de téléversement. Voir la méthode [`Edge.upload(blob, metadata)`](../client-api/README.md#edge-upload-blob-blob-metadata-object-promise) du SDK client.
|
33
doc/apps/server-api/fetch.md
Normal file
33
doc/apps/server-api/fetch.md
Normal 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.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 };
|
||||
}
|
||||
```
|
@ -178,6 +178,6 @@ var results = store.query(ctx, "myCollection", {
|
||||
limit: 10,
|
||||
offset: 5,
|
||||
orderBy: "foo",
|
||||
orderDirection: store.ASC,
|
||||
orderDirection: store.DIRECTION_ASC,
|
||||
});
|
||||
```
|
||||
|
53
go.mod
53
go.mod
@ -2,14 +2,25 @@ module forge.cadoles.com/arcad/edge
|
||||
|
||||
go 1.19
|
||||
|
||||
require modernc.org/sqlite v1.20.4
|
||||
require (
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.8
|
||||
modernc.org/sqlite v1.20.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/brutella/dnssd v1.2.6 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
|
||||
github.com/goccy/go-json v0.9.11 // indirect
|
||||
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073 // indirect
|
||||
github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8 // indirect
|
||||
github.com/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8 // 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.50 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@ -20,40 +31,40 @@ require (
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/dop251/goja v0.0.0-20230203172422-5460598cfa32 // indirect
|
||||
github.com/dop251/goja_nodejs v0.0.0-20230207183254-2229640ea097 // indirect
|
||||
github.com/dop251/goja v0.0.0-20230203172422-5460598cfa32
|
||||
github.com/dop251/goja_nodejs v0.0.0-20230207183254-2229640ea097
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/fatih/color v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.8 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.1
|
||||
github.com/go-chi/chi/v5 v5.0.8
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||
github.com/google/uuid v1.3.0 // 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/mattn/go-colorable v0.1.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.0 // indirect
|
||||
github.com/orcaman/concurrent-map v1.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/orcaman/concurrent-map v1.0.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // 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 // indirect
|
||||
github.com/urfave/cli/v2 v2.24.3
|
||||
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-20230206085656-dec695f0e2e9
|
||||
go.opencensus.io v0.22.5 // indirect
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/net v0.4.0 // indirect
|
||||
golang.org/x/sys v0.3.0 // indirect
|
||||
golang.org/x/term v0.3.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
golang.org/x/crypto v0.7.0 // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
golang.org/x/tools v0.6.0 // 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
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
|
55
go.sum
55
go.sum
@ -54,6 +54,8 @@ github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MR
|
||||
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/go.mod h1:Au0ipPuCBA7zsOC61SnyrYetm8VT3vo1UJtwHeYke44=
|
||||
github.com/brutella/dnssd v1.2.6 h1:/0P13JkHLRzeLQkWRPEn4hJCr4T3NfknIFw3aNPIC34=
|
||||
github.com/brutella/dnssd v1.2.6/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
@ -72,6 +74,9 @@ 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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
|
||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
@ -108,6 +113,8 @@ github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3yg
|
||||
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/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/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
@ -204,6 +211,18 @@ 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.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
@ -215,6 +234,8 @@ github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peK
|
||||
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/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||
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/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
@ -243,12 +264,18 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
|
||||
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.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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
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/v2 v2.24.3 h1:7Q1w8VN8yE0MJEHP06bv89PjYsN4IHWED2s1v/Zlfm0=
|
||||
github.com/urfave/cli/v2 v2.24.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
@ -260,6 +287,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20230206085656-dec695f0e2e9 h1:6JlkcdjYVQglPWYuemK2MoZAtRE4vFx85zLXflGIyI8=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20230206085656-dec695f0e2e9/go.mod h1:3sus4zjoUv1GB7eDLL60QaPkUnXJCWBpjvbe0jWifeY=
|
||||
@ -280,6 +308,10 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -313,8 +345,11 @@ 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.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.8.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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -347,10 +382,15 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-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.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -370,6 +410,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
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-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/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=
|
||||
@ -407,19 +448,27 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-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.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-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.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -432,6 +481,8 @@ 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.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.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@ -482,8 +533,11 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
|
||||
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.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -587,6 +641,7 @@ 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/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.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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<title>Client SDK Test suite</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="vendor/mocha.css" />
|
||||
<link rel="stylesheet" href="/vendor/mocha.css" />
|
||||
<style>
|
||||
body {
|
||||
background-color: white;
|
||||
@ -13,15 +13,20 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="mocha"></div>
|
||||
<script src="vendor/chai.js"></script>
|
||||
<script src="vendor/mocha.js"></script>
|
||||
<script src="/vendor/chai.js"></script>
|
||||
<script src="/vendor/mocha.js"></script>
|
||||
<script class="mocha-init">
|
||||
mocha.setup('bdd');
|
||||
mocha.checkLeaks();
|
||||
</script>
|
||||
<script src="/edge/sdk/client.js"></script>
|
||||
<script src="test/client-sdk.js"></script>
|
||||
<script src="test/auth-module.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">
|
||||
mocha.run();
|
||||
</script>
|
||||
|
46
misc/client-sdk-testsuite/src/public/test/app-module.js
Normal file
46
misc/client-sdk-testsuite/src/public/test/app-module.js
Normal file
@ -0,0 +1,46 @@
|
||||
describe('App Module', function() {
|
||||
|
||||
before(() => {
|
||||
return Edge.connect();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
Edge.disconnect();
|
||||
});
|
||||
|
||||
it('should list apps', function() {
|
||||
return Edge.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.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.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.rpc("getAppUrl", { appId: "edge.sdk.client.test", from: "127.0.0.2" })
|
||||
.then(url => {
|
||||
console.log("getAppUrl result:", url);
|
||||
chai.assert.isNotEmpty(url);
|
||||
chai.assert.match(url, /^http:\/\/127\.0\.0\.1/)
|
||||
})
|
||||
});
|
||||
|
||||
});
|
@ -11,9 +11,10 @@ describe('Auth Module', function() {
|
||||
it('should retrieve user informations', function() {
|
||||
return Edge.rpc("getUserInfo")
|
||||
.then(userInfo => {
|
||||
chai.assert.isNotNull(userInfo.subject);
|
||||
chai.assert.isNotNull(userInfo.role);
|
||||
chai.assert.isNotNull(userInfo.preferredUsername);
|
||||
console.log("getUserInfo result:", userInfo);
|
||||
chai.assert.property(userInfo, 'subject');
|
||||
chai.assert.property(userInfo, 'role');
|
||||
chai.assert.property(userInfo, 'preferredUsername');
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
Edge.debug = true;
|
||||
EdgeFrame.debug = true;
|
||||
|
||||
describe('Edge', function() {
|
||||
|
||||
@ -22,127 +23,4 @@ describe('Edge', function() {
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
|
33
misc/client-sdk-testsuite/src/public/test/fetch-module.js
Normal file
33
misc/client-sdk-testsuite/src/public/test/fetch-module.js
Normal file
@ -0,0 +1,33 @@
|
||||
describe('Fetch Module', function () {
|
||||
|
||||
before(() => {
|
||||
return Edge.connect();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
Edge.disconnect();
|
||||
});
|
||||
|
||||
it('should fetch an authorized external url', function () {
|
||||
var externalUrl = Edge.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.externalUrl("https://google.com");
|
||||
|
||||
return fetch(externalUrl)
|
||||
.then(res => {
|
||||
chai.assert.equal(res.status, 403)
|
||||
})
|
||||
});
|
||||
|
||||
});
|
36
misc/client-sdk-testsuite/src/public/test/file-module.js
Normal file
36
misc/client-sdk-testsuite/src/public/test/file-module.js
Normal file
@ -0,0 +1,36 @@
|
||||
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);
|
||||
})
|
||||
});
|
||||
|
||||
});
|
49
misc/client-sdk-testsuite/src/public/test/net-module.js
Normal file
49
misc/client-sdk-testsuite/src/public/test/net-module.js
Normal file
@ -0,0 +1,49 @@
|
||||
describe('Net Module', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
before(() => {
|
||||
return Edge.connect();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
Edge.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.removeEventListener('message', handler);
|
||||
done();
|
||||
};
|
||||
|
||||
Edge.addEventListener("message", handler);
|
||||
Edge.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.removeEventListener('message', handler);
|
||||
done();
|
||||
}
|
||||
|
||||
// Server should echo back message
|
||||
Edge.addEventListener('message', handler);
|
||||
|
||||
// Send message to server
|
||||
Edge.send({ test: 'echo', now });
|
||||
});
|
||||
|
||||
});
|
59
misc/client-sdk-testsuite/src/public/test/rpc-module.js
Normal file
59
misc/client-sdk-testsuite/src/public/test/rpc-module.js
Normal file
@ -0,0 +1,59 @@
|
||||
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 => {
|
||||
console.log(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)
|
||||
})
|
||||
});
|
||||
|
||||
});
|
@ -11,12 +11,23 @@ function onInit() {
|
||||
rpc.register("reset", reset);
|
||||
rpc.register("total", total);
|
||||
rpc.register("getUserInfo", getUserInfo);
|
||||
|
||||
rpc.register("listApps");
|
||||
rpc.register("getApp");
|
||||
rpc.register("getAppUrl");
|
||||
}
|
||||
|
||||
// Called for each client message
|
||||
function onClientMessage(ctx, data) {
|
||||
console.log("onClientMessage", data.now);
|
||||
net.send(ctx, { now: data.now });
|
||||
function onClientMessage(ctx, message) {
|
||||
console.log("onClientMessage", message);
|
||||
|
||||
switch (message.test) {
|
||||
case "broadcast":
|
||||
net.broadcast(message);
|
||||
break;
|
||||
default:
|
||||
net.send(ctx, message);
|
||||
}
|
||||
}
|
||||
|
||||
// Called for each blob upload request
|
||||
@ -73,3 +84,23 @@ function getUserInfo(ctx, params) {
|
||||
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
28
misc/jenkins/Dockerfile
Normal 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.19.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
|
@ -1,12 +1,13 @@
|
||||
**/*.go
|
||||
pkg/app/sdk/client/src/**/*.js
|
||||
pkg/app/sdk/client/src/**/*.ts
|
||||
**/*.tmpl
|
||||
pkg/sdk/client/src/**/*.js
|
||||
pkg/sdk/client/src/**/*.ts
|
||||
misc/client-sdk-testsuite/src/**/*
|
||||
modd.conf
|
||||
{
|
||||
prep: make build-sdk
|
||||
prep: cd misc/client-sdk-testsuite && make dist
|
||||
prep: make GOTEST_ARGS="-short" test
|
||||
prep: make build
|
||||
daemon: bin/cli app run -p misc/client-sdk-testsuite/dist --storage-file ./sdk-testsuite.sqlite
|
||||
prep: make GOTEST_ARGS="-short" test
|
||||
daemon: bin/cli app run -p misc/client-sdk-testsuite/dist
|
||||
}
|
||||
|
@ -1,16 +1,39 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type App struct {
|
||||
ID ID
|
||||
Manifest *Manifest
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -1,15 +1,20 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"sync"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/dop251/goja_nodejs/eventloop"
|
||||
"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 {
|
||||
runtime *goja.Runtime
|
||||
@ -26,16 +31,18 @@ func (s *Server) Load(name string, src string) error {
|
||||
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) {
|
||||
ctx = logger.With(ctx, logger.F("function", funcName), logger.F("args", args))
|
||||
|
||||
callable, ok := goja.AssertFunction(s.runtime.Get(funcName))
|
||||
if !ok {
|
||||
return nil, errors.WithStack(ErrFuncDoesNotExist)
|
||||
}
|
||||
|
||||
return s.Exec(callable, args...)
|
||||
return s.Exec(ctx, callable, args...)
|
||||
}
|
||||
|
||||
func (s *Server) Exec(callable goja.Callable, args ...interface{}) (goja.Value, error) {
|
||||
func (s *Server) Exec(ctx context.Context, callable goja.Callable, args ...interface{}) (goja.Value, error) {
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
value goja.Value
|
||||
@ -45,6 +52,25 @@ func (s *Server) Exec(callable goja.Callable, args ...interface{}) (goja.Value,
|
||||
wg.Add(1)
|
||||
|
||||
s.loop.RunOnLoop(func(vm *goja.Runtime) {
|
||||
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))
|
||||
for _, a := range args {
|
||||
jsArgs = append(jsArgs, vm.ToValue(a))
|
||||
@ -54,8 +80,6 @@ func (s *Server) Exec(callable goja.Callable, args ...interface{}) (goja.Value,
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
wg.Wait()
|
||||
|
@ -99,3 +99,5 @@ func (f *File) Readdir(count int) ([]os.FileInfo, error) {
|
||||
func (f *File) Stat() (os.FileInfo, error) {
|
||||
return f.fi, nil
|
||||
}
|
||||
|
||||
var _ http.FileSystem = &FileSystem{}
|
||||
|
@ -1,12 +1,9 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type eventDispatcherSet struct {
|
||||
@ -89,8 +86,6 @@ func (d *eventDispatcher) IsOut(out <-chan bus.Message) bool {
|
||||
}
|
||||
|
||||
func (d *eventDispatcher) Run() {
|
||||
ctx := context.Background()
|
||||
|
||||
for {
|
||||
msg, ok := <-d.in
|
||||
if !ok {
|
||||
@ -99,12 +94,7 @@ func (d *eventDispatcher) Run() {
|
||||
return
|
||||
}
|
||||
|
||||
timeout := time.After(2 * time.Second)
|
||||
select {
|
||||
case d.out <- msg:
|
||||
case <-timeout:
|
||||
logger.Error(ctx, "message out chan timed out", logger.F("message", msg))
|
||||
}
|
||||
d.out <- msg
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/blob"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
@ -68,7 +69,7 @@ func (h *Handler) handleAppUpload(w http.ResponseWriter, r *http.Request) {
|
||||
ContextKeyOriginRequest: r,
|
||||
})
|
||||
|
||||
requestMsg := module.NewMessageUploadRequest(ctx, fileHeader, metadata)
|
||||
requestMsg := blob.NewMessageUploadRequest(ctx, fileHeader, metadata)
|
||||
|
||||
reply, err := h.bus.Request(ctx, requestMsg)
|
||||
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))
|
||||
|
||||
responseMsg, ok := reply.(*module.MessageUploadResponse)
|
||||
responseMsg, ok := reply.(*blob.MessageUploadResponse)
|
||||
if !ok {
|
||||
logger.Error(
|
||||
ctx, "unexpected upload response message",
|
||||
@ -120,7 +121,7 @@ func (h *Handler) handleAppDownload(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
if err != nil {
|
||||
@ -130,7 +131,7 @@ func (h *Handler) handleAppDownload(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
replyMsg, ok := reply.(*module.MessageDownloadResponse)
|
||||
replyMsg, ok := reply.(*blob.MessageDownloadResponse)
|
||||
if !ok {
|
||||
logger.Error(
|
||||
ctx, "unexpected download response message",
|
||||
|
112
pkg/http/fetch.go
Normal file
112
pkg/http/fetch.go
Normal 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))
|
||||
}
|
||||
}
|
@ -31,6 +31,8 @@ type Handler struct {
|
||||
server *app.Server
|
||||
serverModuleFactories []app.ServerModuleFactory
|
||||
|
||||
httpClient *http.Client
|
||||
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
@ -59,7 +61,7 @@ func (h *Handler) Load(bdle bundle.Bundle) error {
|
||||
}
|
||||
|
||||
fs := bundle.NewFileSystem("public", bdle)
|
||||
public := http.FileServer(fs)
|
||||
public := HTML5Fileserver(fs)
|
||||
sockjs := sockjs.NewHandler(sockJSPathPrefix, h.sockjsOpts, h.handleSockJSSession)
|
||||
|
||||
if h.server != nil {
|
||||
@ -91,6 +93,7 @@ func NewHandler(funcs ...HandlerOptionFunc) *Handler {
|
||||
sockjsOpts: opts.SockJS,
|
||||
router: router,
|
||||
serverModuleFactories: opts.ServerModuleFactories,
|
||||
httpClient: opts.HTTPClient,
|
||||
bus: opts.Bus,
|
||||
}
|
||||
|
||||
@ -103,6 +106,8 @@ func NewHandler(funcs ...HandlerOptionFunc) *Handler {
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
r.Post("/upload", handler.handleAppUpload)
|
||||
r.Get("/download/{bucket}/{blobID}", handler.handleAppDownload)
|
||||
|
||||
r.Get("/fetch", handler.handleAppFetch)
|
||||
})
|
||||
|
||||
r.HandleFunc("/sock/*", handler.handleSockJS)
|
||||
|
54
pkg/http/html5_fileserver.go
Normal file
54
pkg/http/html5_fileserver.go
Normal 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)
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
@ -14,6 +15,7 @@ type HandlerOptions struct {
|
||||
SockJS sockjs.Options
|
||||
ServerModuleFactories []app.ServerModuleFactory
|
||||
UploadMaxFileSize int64
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
func defaultHandlerOptions() *HandlerOptions {
|
||||
@ -27,6 +29,9 @@ func defaultHandlerOptions() *HandlerOptions {
|
||||
SockJS: sockjsOptions,
|
||||
ServerModuleFactories: make([]app.ServerModuleFactory, 0),
|
||||
UploadMaxFileSize: 10 << (10 * 2), // 10Mb
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,3 +60,9 @@ func WithUploadMaxFileSize(size int64) HandlerOptionFunc {
|
||||
opts.UploadMaxFileSize = size
|
||||
}
|
||||
}
|
||||
|
||||
func WithHTTPClient(client *http.Client) HandlerOptionFunc {
|
||||
return func(opts *HandlerOptions) {
|
||||
opts.HTTPClient = client
|
||||
}
|
||||
}
|
||||
|
5
pkg/module/app/error.go
Normal file
5
pkg/module/app/error.go
Normal file
@ -0,0 +1,5 @@
|
||||
package app
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
58
pkg/module/app/memory/module_test.go
Normal file
58
pkg/module/app/memory/module_test.go
Normal 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))
|
||||
}
|
||||
}
|
50
pkg/module/app/memory/repository.go
Normal file
50
pkg/module/app/memory/repository.go
Normal file
@ -0,0 +1,50 @@
|
||||
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 {
|
||||
return &Repository{getURL, manifests}
|
||||
}
|
||||
|
||||
var _ module.Repository = &Repository{}
|
17
pkg/module/app/memory/testdata/app.js
vendored
Normal file
17
pkg/module/app/memory/testdata/app.js
vendored
Normal 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+"'");
|
||||
}
|
122
pkg/module/app/module.go
Normal file
122
pkg/module/app/module.go
Normal file
@ -0,0 +1,122 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
func toGojaManifest(manifest *app.Manifest) *gojaManifest {
|
||||
return &gojaManifest{
|
||||
ID: string(manifest.ID),
|
||||
Version: manifest.Version,
|
||||
Title: manifest.Title,
|
||||
Description: manifest.Description,
|
||||
Tags: manifest.Tags,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
13
pkg/module/app/repository.go
Normal file
13
pkg/module/app/repository.go
Normal 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(context.Context, app.ID, string) (string, error)
|
||||
}
|
35
pkg/module/auth/http/jwt.go
Normal file
35
pkg/module/auth/http/jwt.go
Normal file
@ -0,0 +1,35 @@
|
||||
package http
|
||||
|
||||
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
|
||||
}
|
27
pkg/module/auth/http/local_account.go
Normal file
27
pkg/module/auth/http/local_account.go
Normal 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
|
||||
}
|
206
pkg/module/auth/http/local_handler.go
Normal file
206
pkg/module/auth/http/local_handler.go
Normal file
@ -0,0 +1,206 @@
|
||||
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"
|
||||
"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
|
||||
}
|
||||
|
||||
token, err := 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{}
|
49
pkg/module/auth/http/options.go
Normal file
49
pkg/module/auth/http/options.go
Normal 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
|
||||
}
|
||||
}
|
136
pkg/module/auth/http/passwd/argon2id/hasher.go
Normal file
136
pkg/module/auth/http/passwd/argon2id/hasher.go
Normal 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 = ¶ms{}
|
||||
_, 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
|
||||
}
|
8
pkg/module/auth/http/passwd/hasher.go
Normal file
8
pkg/module/auth/http/passwd/hasher.go
Normal 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)
|
||||
}
|
31
pkg/module/auth/http/passwd/plain/hasher.go
Normal file
31
pkg/module/auth/http/passwd/plain/hasher.go
Normal 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{}
|
87
pkg/module/auth/http/passwd/registry.go
Normal file
87
pkg/module/auth/http/passwd/registry.go
Normal 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)
|
||||
}
|
105
pkg/module/auth/http/templates/login.html.tmpl
Normal file
105
pkg/module/auth/http/templates/login.html.tmpl
Normal 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 />
|
||||
</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>
|
@ -5,14 +5,22 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"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"
|
||||
)
|
||||
|
||||
func WithJWT(keyFunc jwt.Keyfunc) OptionFunc {
|
||||
const (
|
||||
CookieName string = "edge-auth"
|
||||
)
|
||||
|
||||
type GetKeySetFunc func() (jwk.Set, error)
|
||||
|
||||
func WithJWT(getKeySet GetKeySetFunc) OptionFunc {
|
||||
return func(o *Option) {
|
||||
o.GetClaim = func(ctx context.Context, r *http.Request, claimName string) (string, error) {
|
||||
claim, err := getClaim[string](r, claimName, keyFunc)
|
||||
claim, err := getClaim[string](r, claimName, getKeySet)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
@ -22,28 +30,63 @@ func WithJWT(keyFunc jwt.Keyfunc) OptionFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func getClaim[T any](r *http.Request, claimAttr string, keyFunc jwt.Keyfunc) (T, error) {
|
||||
rawToken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||
func FindToken(r *http.Request, getKeySet GetKeySetFunc) (jwt.Token, 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("token")
|
||||
rawToken = r.URL.Query().Get(CookieName)
|
||||
}
|
||||
|
||||
if rawToken == "" {
|
||||
return *new(T), errors.WithStack(ErrUnauthenticated)
|
||||
cookie, err := r.Cookie(CookieName)
|
||||
if err != nil && !errors.Is(err, http.ErrNoCookie) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(rawToken, keyFunc)
|
||||
if cookie != nil {
|
||||
rawToken = cookie.Value
|
||||
}
|
||||
}
|
||||
|
||||
if rawToken == "" {
|
||||
return nil, errors.WithStack(ErrUnauthenticated)
|
||||
}
|
||||
|
||||
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 getClaim[T any](r *http.Request, claimAttr string, getKeySet GetKeySetFunc) (T, error) {
|
||||
token, err := FindToken(r, getKeySet)
|
||||
if err != nil {
|
||||
return *new(T), errors.WithStack(err)
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return *new(T), errors.Errorf("invalid jwt token: '%v'", token.Raw)
|
||||
}
|
||||
ctx := r.Context()
|
||||
|
||||
mapClaims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return *new(T), errors.Errorf("unexpected claims type '%T'", token.Claims)
|
||||
mapClaims, err := token.AsMap(ctx)
|
||||
if err != nil {
|
||||
return *new(T), errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawClaim, exists := mapClaims[claimAttr]
|
||||
|
@ -55,7 +55,7 @@ func (m *Module) getClaim(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
}
|
||||
|
||||
func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory {
|
||||
opt := &Option{}
|
||||
opt := defaultOptions()
|
||||
for _, fn := range funcs {
|
||||
fn(opt)
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
@ -12,7 +11,9 @@ import (
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||
"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"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
@ -22,12 +23,12 @@ func TestAuthModule(t *testing.T) {
|
||||
|
||||
logger.SetLevel(slog.LevelDebug)
|
||||
|
||||
keyFunc, secret := getKeyFunc()
|
||||
key := getDummyKey()
|
||||
|
||||
server := app.NewServer(
|
||||
module.ConsoleModuleFactory(),
|
||||
ModuleFactory(
|
||||
WithJWT(keyFunc),
|
||||
WithJWT(getDummyKeySet(key)),
|
||||
),
|
||||
)
|
||||
|
||||
@ -51,21 +52,26 @@ func TestAuthModule(t *testing.T) {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"sub": "jdoe",
|
||||
"nbf": time.Now().UTC().Unix(),
|
||||
})
|
||||
token := jwt.New()
|
||||
|
||||
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 {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Bearer "+rawToken)
|
||||
req.Header.Add("Authorization", "Bearer "+string(rawToken))
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
@ -75,11 +81,11 @@ func TestAuthAnonymousModule(t *testing.T) {
|
||||
|
||||
logger.SetLevel(slog.LevelDebug)
|
||||
|
||||
keyFunc, _ := getKeyFunc()
|
||||
key := getDummyKey()
|
||||
|
||||
server := app.NewServer(
|
||||
module.ConsoleModuleFactory(),
|
||||
ModuleFactory(WithJWT(keyFunc)),
|
||||
ModuleFactory(WithJWT(getDummyKeySet(key))),
|
||||
)
|
||||
|
||||
data, err := ioutil.ReadFile("testdata/auth_anonymous.js")
|
||||
@ -104,21 +110,34 @@ func TestAuthAnonymousModule(t *testing.T) {
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
func getKeyFunc() (jwt.Keyfunc, []byte) {
|
||||
func getDummyKey() jwk.Key {
|
||||
secret := []byte("not_so_secret")
|
||||
|
||||
keyFunc := func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"])
|
||||
key, err := jwk.FromRaw(secret)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetClaimFunc func(ctx context.Context, r *http.Request, claimName string) (string, error)
|
||||
@ -13,6 +15,16 @@ type Option struct {
|
||||
|
||||
type OptionFunc func(*Option)
|
||||
|
||||
func defaultOptions() *Option {
|
||||
return &Option{
|
||||
GetClaim: dummyGetClaim,
|
||||
}
|
||||
}
|
||||
|
||||
func dummyGetClaim(ctx context.Context, r *http.Request, claimName string) (string, error) {
|
||||
return "", errors.Errorf("dummy getclaim func cannot retrieve claim '%s'", claimName)
|
||||
}
|
||||
|
||||
func WithGetClaim(fn GetClaimFunc) OptionFunc {
|
||||
return func(o *Option) {
|
||||
o.GetClaim = fn
|
||||
|
@ -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
|
||||
}
|
||||
}
|
21
pkg/module/blob/blob_info.go
Normal file
21
pkg/module/blob/blob_info.go
Normal 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(),
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package module
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
499
pkg/module/blob/module.go
Normal file
499
pkg/module/blob/module.go
Normal file
@ -0,0 +1,499 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||
"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 Module struct {
|
||||
server *app.Server
|
||||
bus bus.Bus
|
||||
store storage.BlobStore
|
||||
}
|
||||
|
||||
func (m *Module) Name() string {
|
||||
return "blob"
|
||||
}
|
||||
|
||||
func (m *Module) Export(export *goja.Object) {
|
||||
funcs := map[string]any{
|
||||
"listBuckets": m.listBuckets,
|
||||
"deleteBucket": m.deleteBucket,
|
||||
"getBucketSize": m.getBucketSize,
|
||||
"listBlobs": m.listBlobs,
|
||||
"getBlobInfo": m.getBlobInfo,
|
||||
"readBlob": m.readBlob,
|
||||
"writeBlob": m.writeBlob,
|
||||
"deleteBlob": m.deleteBlob,
|
||||
}
|
||||
|
||||
for name, fn := range funcs {
|
||||
if err := export.Set(name, fn); err != nil {
|
||||
panic(errors.Wrapf(err, "could not set '%s' function", name))
|
||||
}
|
||||
}
|
||||
|
||||
if err := export.Set("DEFAULT_BUCKET", DefaultBlobBucket); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'DEFAULT_BUCKET' property"))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) listBuckets(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx := util.AssertContext(call.Argument(0), rt)
|
||||
|
||||
buckets, err := m.store.ListBuckets(ctx)
|
||||
if err != nil {
|
||||
panic(rt.ToValue(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
defaultBucketIndex := sort.SearchStrings(buckets, DefaultBlobBucket)
|
||||
if defaultBucketIndex == 0 {
|
||||
buckets = append(buckets, DefaultBlobBucket)
|
||||
} else {
|
||||
buckets[defaultBucketIndex] = DefaultBlobBucket
|
||||
}
|
||||
|
||||
return rt.ToValue(buckets)
|
||||
}
|
||||
|
||||
func (m *Module) writeBlob(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx := util.AssertContext(call.Argument(0), rt)
|
||||
bucketName := util.AssertString(call.Argument(1), rt)
|
||||
blobID := assertBlobID(call.Argument(2), rt)
|
||||
rawData := call.Argument(3).Export()
|
||||
|
||||
var data []byte
|
||||
switch typ := rawData.(type) {
|
||||
case []byte:
|
||||
data = typ
|
||||
case string:
|
||||
data = []byte(typ)
|
||||
default:
|
||||
data = []byte(fmt.Sprintf("%v", typ))
|
||||
}
|
||||
|
||||
bucket, err := m.store.OpenBucket(ctx, bucketName)
|
||||
if err != nil {
|
||||
panic(rt.ToValue(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 {
|
||||
panic(rt.ToValue(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := writer.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
|
||||
logger.Error(ctx, "could not close blob writer", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := writer.Write(data); err != nil {
|
||||
panic(rt.ToValue(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Module) getBlobInfo(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx := util.AssertContext(call.Argument(0), rt)
|
||||
bucketName := util.AssertString(call.Argument(1), rt)
|
||||
blobID := assertBlobID(call.Argument(2), rt)
|
||||
|
||||
bucket, err := m.store.OpenBucket(ctx, bucketName)
|
||||
if err != nil {
|
||||
panic(rt.ToValue(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := bucket.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close bucket", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
blobInfo, err := bucket.Get(ctx, blobID)
|
||||
if err != nil {
|
||||
panic(rt.ToValue(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
return rt.ToValue(toGojaBlobInfo(blobInfo))
|
||||
}
|
||||
|
||||
func (m *Module) readBlob(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx := util.AssertContext(call.Argument(0), rt)
|
||||
bucketName := util.AssertString(call.Argument(1), rt)
|
||||
blobID := assertBlobID(call.Argument(2), rt)
|
||||
|
||||
reader, _, err := m.openBlob(ctx, bucketName, blobID)
|
||||
if err != nil {
|
||||
panic(rt.ToValue(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := reader.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
|
||||
logger.Error(ctx, "could not close blob reader", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
panic(rt.ToValue(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
return rt.ToValue(rt.NewArrayBuffer(data))
|
||||
}
|
||||
|
||||
func (m *Module) deleteBlob(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx := util.AssertContext(call.Argument(0), rt)
|
||||
bucketName := util.AssertString(call.Argument(1), rt)
|
||||
blobID := assertBlobID(call.Argument(2), rt)
|
||||
|
||||
bucket, err := m.store.OpenBucket(ctx, bucketName)
|
||||
if err != nil {
|
||||
panic(rt.ToValue(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
if err := bucket.Delete(ctx, blobID); err != nil {
|
||||
panic(rt.ToValue(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Module) listBlobs(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx := util.AssertContext(call.Argument(0), rt)
|
||||
bucketName := util.AssertString(call.Argument(1), rt)
|
||||
|
||||
bucket, err := m.store.OpenBucket(ctx, bucketName)
|
||||
if err != nil {
|
||||
panic(rt.ToValue(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
blobInfos, err := bucket.List(ctx)
|
||||
if err != nil {
|
||||
panic(rt.ToValue(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
gojaBlobInfos := make([]blobInfo, len(blobInfos))
|
||||
|
||||
for i, b := range blobInfos {
|
||||
gojaBlobInfos[i] = toGojaBlobInfo(b)
|
||||
}
|
||||
|
||||
return rt.ToValue(gojaBlobInfos)
|
||||
}
|
||||
|
||||
func (m *Module) deleteBucket(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx := util.AssertContext(call.Argument(0), rt)
|
||||
bucketName := util.AssertString(call.Argument(1), rt)
|
||||
|
||||
if err := m.store.DeleteBucket(ctx, bucketName); err != nil {
|
||||
panic(rt.ToValue(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Module) getBucketSize(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx := util.AssertContext(call.Argument(0), rt)
|
||||
bucketName := util.AssertString(call.Argument(1), rt)
|
||||
|
||||
bucket, err := m.store.OpenBucket(ctx, bucketName)
|
||||
if err != nil {
|
||||
panic(rt.ToValue(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
size, err := bucket.Size(ctx)
|
||||
if err != nil {
|
||||
panic(rt.ToValue(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
return rt.ToValue(size)
|
||||
}
|
||||
|
||||
func (m *Module) 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 *Module) 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(ctx, "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 *Module) 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 *Module) handleDownloadRequest(req *MessageDownloadRequest) (*MessageDownloadResponse, error) {
|
||||
res := NewMessageDownloadResponse(req.RequestID)
|
||||
|
||||
rawResult, err := m.server.ExecFuncByName(req.Context, "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 *Module) 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 ModuleFactory(bus bus.Bus, store storage.BlobStore) app.ServerModuleFactory {
|
||||
return func(server *app.Server) app.ServerModule {
|
||||
mod := &Module{
|
||||
store: store,
|
||||
bus: bus,
|
||||
server: server,
|
||||
}
|
||||
|
||||
go mod.handleMessages()
|
||||
|
||||
return mod
|
||||
}
|
||||
}
|
||||
|
||||
func assertBlobID(value goja.Value, rt *goja.Runtime) storage.BlobID {
|
||||
blobID, ok := value.Export().(storage.BlobID)
|
||||
if !ok {
|
||||
rawBlobID, ok := value.Export().(string)
|
||||
if !ok {
|
||||
panic(rt.NewTypeError(fmt.Sprintf("blob id must be a blob or a string, got '%T'", value.Export())))
|
||||
}
|
||||
|
||||
blobID = storage.BlobID(rawBlobID)
|
||||
}
|
||||
|
||||
return blobID
|
||||
}
|
44
pkg/module/blob/module_test.go
Normal file
44
pkg/module/blob/module_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
package blob
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func TestBlobModule(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger.SetLevel(slog.LevelDebug)
|
||||
|
||||
bus := memory.NewBus()
|
||||
store := sqlite.NewBlobStore(":memory:?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000")
|
||||
|
||||
server := app.NewServer(
|
||||
module.ContextModuleFactory(),
|
||||
module.ConsoleModuleFactory(),
|
||||
ModuleFactory(bus, store),
|
||||
)
|
||||
|
||||
data, err := ioutil.ReadFile("testdata/blob.js")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := server.Load("testdata/blob.js", string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer server.Stop()
|
||||
|
||||
if err := server.Start(); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
}
|
79
pkg/module/blob/testdata/blob.js
vendored
Normal file
79
pkg/module/blob/testdata/blob.js
vendored
Normal file
@ -0,0 +1,79 @@
|
||||
|
||||
var ctx = context.new();
|
||||
var buckets = blob.listBuckets(ctx);
|
||||
|
||||
if (!buckets || buckets.length === 0) {
|
||||
throw new Error("buckets should not be empty");
|
||||
}
|
||||
|
||||
var size = blob.getBucketSize(ctx, blob.DEFAULT_BUCKET);
|
||||
|
||||
if (size !== 0) {
|
||||
throw new Error("bucket size: expected '0', got '"+size+"'");
|
||||
}
|
||||
|
||||
var newBucket = "mybucket"
|
||||
var blobId = "foo"
|
||||
var data = (new Date()).toString();
|
||||
|
||||
blob.writeBlob(ctx, newBucket, blobId, data)
|
||||
|
||||
buckets = blob.listBuckets(ctx);
|
||||
|
||||
if (buckets.length !== 2) {
|
||||
throw new Error("buckets.length: expected '2', got '"+buckets.length+"'");
|
||||
}
|
||||
|
||||
size = blob.getBucketSize(ctx, newBucket);
|
||||
|
||||
if (size !== data.length) {
|
||||
throw new Error("bucket size: expected '"+data.length+"', got '"+size+"'");
|
||||
}
|
||||
|
||||
var blobInfos = blob.listBlobs(ctx, newBucket);
|
||||
|
||||
if (blobInfos.length !== 1) {
|
||||
throw new Error("blobInfos.length: expected '1', got '"+blobInfos.length+"'");
|
||||
}
|
||||
|
||||
if (blobInfos[0].id != blobId) {
|
||||
throw new Error("blobInfos[0].id: expected '"+blobId+"', got '"+blobInfos[0].id+"'");
|
||||
}
|
||||
|
||||
if (blobInfos[0].contentType != "text/plain; charset=utf-8") {
|
||||
throw new Error("blobInfos[0].contentType: expected 'text/plain; charset=utf-8', got '"+blobInfos[0].contentType+"'");
|
||||
}
|
||||
|
||||
if (blobInfos[0].size != data.length) {
|
||||
throw new Error("blobInfos[0].size: expected '"+data.length+"', got '"+blobInfos[0].size+"'");
|
||||
}
|
||||
|
||||
var readData = blob.readBlob(ctx, newBucket, blobId)
|
||||
|
||||
if (!readData) {
|
||||
throw new Error("readData should not be nil");
|
||||
}
|
||||
|
||||
var buckets = blob.listBuckets(ctx);
|
||||
|
||||
if (!buckets || buckets.length !== 2) {
|
||||
throw new Error("buckets.length should be 2");
|
||||
}
|
||||
|
||||
blob.deleteBlob(ctx, newBucket, blobId)
|
||||
|
||||
blobInfos = blob.listBlobs(ctx, newBucket);
|
||||
|
||||
console.log(blobInfos);
|
||||
|
||||
if (blobInfos.length !== 0) {
|
||||
throw new Error("blobInfos.length: expected '0', got '"+blobInfos.length+"'");
|
||||
}
|
||||
|
||||
blob.deleteBucket(ctx, newBucket)
|
||||
|
||||
buckets = blob.listBuckets(ctx);
|
||||
|
||||
if (buckets.length !== 1) {
|
||||
throw new Error("buckets.length: expected '1', got '"+buckets.length+"'");
|
||||
}
|
@ -39,7 +39,7 @@ const (
|
||||
)
|
||||
|
||||
func getDeviceClientByUUID(ctx context.Context, uuid string) (*cast.Client, error) {
|
||||
device, err := findDeviceByUUID(ctx, uuid)
|
||||
device, err := FindDeviceByUUID(ctx, uuid)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
@ -49,7 +49,7 @@ func getDeviceClientByUUID(ctx context.Context, uuid string) (*cast.Client, erro
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func findDeviceByUUID(ctx context.Context, uuid string) (*Device, error) {
|
||||
func FindDeviceByUUID(ctx context.Context, uuid string) (*Device, error) {
|
||||
service := discovery.NewService(ctx)
|
||||
defer service.Stop()
|
||||
|
||||
@ -83,7 +83,7 @@ LOOP:
|
||||
return nil, errors.WithStack(ErrDeviceNotFound)
|
||||
}
|
||||
|
||||
func findDevices(ctx context.Context) ([]*Device, error) {
|
||||
func FindDevices(ctx context.Context) ([]*Device, error) {
|
||||
service := discovery.NewService(ctx)
|
||||
defer service.Stop()
|
||||
|
||||
@ -124,7 +124,7 @@ LOOP:
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func loadURL(ctx context.Context, deviceUUID string, url string) error {
|
||||
func LoadURL(ctx context.Context, deviceUUID string, url string) error {
|
||||
client, err := getDeviceClientByUUID(ctx, deviceUUID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
@ -153,7 +153,7 @@ func isLoadURLContextExceeded(err error) bool {
|
||||
return err.Error() == "Failed to send load command: context deadline exceeded"
|
||||
}
|
||||
|
||||
func stopCast(ctx context.Context, deviceUUID string) error {
|
||||
func StopCast(ctx context.Context, deviceUUID string) error {
|
||||
client, err := getDeviceClientByUUID(ctx, deviceUUID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -26,7 +26,7 @@ func TestCastLoadURL(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
devices, err := findDevices(ctx)
|
||||
devices, err := FindDevices(ctx)
|
||||
if err != nil {
|
||||
t.Error(errors.WithStack(err))
|
||||
}
|
||||
@ -40,7 +40,7 @@ func TestCastLoadURL(t *testing.T) {
|
||||
ctx, cancel2 := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel2()
|
||||
|
||||
if err := loadURL(ctx, dev.UUID, "https://go.dev"); err != nil {
|
||||
if err := LoadURL(ctx, dev.UUID, "https://go.dev"); err != nil {
|
||||
t.Error(errors.WithStack(err))
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@ func TestCastLoadURL(t *testing.T) {
|
||||
ctx, cancel4 := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel4()
|
||||
|
||||
if err := stopCast(ctx, dev.UUID); err != nil {
|
||||
if err := StopCast(ctx, dev.UUID); err != nil {
|
||||
t.Error(errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ func (m *Module) refreshDevices(call goja.FunctionCall, rt *goja.Runtime) goja.V
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
devices, err := findDevices(ctx)
|
||||
devices, err := FindDevices(ctx)
|
||||
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "error refreshing casting devices list", logger.E(errors.WithStack(err)))
|
||||
@ -128,7 +128,7 @@ func (m *Module) loadUrl(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
err := loadURL(ctx, deviceUUID, url)
|
||||
err := LoadURL(ctx, deviceUUID, url)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "error while casting url", logger.E(err))
|
||||
@ -166,7 +166,7 @@ func (m *Module) stopCast(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
err := stopCast(ctx, deviceUUID)
|
||||
err := StopCast(ctx, deviceUUID)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "error while quitting casting device app", logger.E(errors.WithStack(err)))
|
||||
|
@ -1,6 +1,7 @@
|
||||
package cast
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
@ -79,7 +80,7 @@ func TestCastModuleRefreshDevices(t *testing.T) {
|
||||
|
||||
defer server.Stop()
|
||||
|
||||
result, err := server.ExecFuncByName("refreshDevices")
|
||||
result, err := server.ExecFuncByName(context.Background(), "refreshDevices")
|
||||
if err != nil {
|
||||
t.Error(errors.WithStack(err))
|
||||
}
|
||||
|
49
pkg/module/fetch/fetch_message.go
Normal file
49
pkg/module/fetch/fetch_message.go
Normal file
@ -0,0 +1,49 @@
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
MessageNamespaceFetchRequest bus.MessageNamespace = "fetchRequest"
|
||||
MessageNamespaceFetchResponse bus.MessageNamespace = "fetchResponse"
|
||||
)
|
||||
|
||||
type MessageFetchRequest struct {
|
||||
Context context.Context
|
||||
RequestID string
|
||||
URL *url.URL
|
||||
RemoteAddr string
|
||||
}
|
||||
|
||||
func (m *MessageFetchRequest) MessageNamespace() bus.MessageNamespace {
|
||||
return MessageNamespaceFetchRequest
|
||||
}
|
||||
|
||||
func NewMessageFetchRequest(ctx context.Context, remoteAddr string, url *url.URL) *MessageFetchRequest {
|
||||
return &MessageFetchRequest{
|
||||
Context: ctx,
|
||||
RequestID: ulid.Make().String(),
|
||||
RemoteAddr: remoteAddr,
|
||||
URL: url,
|
||||
}
|
||||
}
|
||||
|
||||
type MessageFetchResponse struct {
|
||||
RequestID string
|
||||
Allow bool
|
||||
}
|
||||
|
||||
func (m *MessageFetchResponse) MessageNamespace() bus.MessageNamespace {
|
||||
return MessageNamespaceFetchResponse
|
||||
}
|
||||
|
||||
func NewMessageFetchResponse(requestID string) *MessageFetchResponse {
|
||||
return &MessageFetchResponse{
|
||||
RequestID: requestID,
|
||||
}
|
||||
}
|
122
pkg/module/fetch/module.go
Normal file
122
pkg/module/fetch/module.go
Normal file
@ -0,0 +1,122 @@
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
server *app.Server
|
||||
bus bus.Bus
|
||||
}
|
||||
|
||||
func (m *Module) Name() string {
|
||||
return "fetch"
|
||||
}
|
||||
|
||||
func (m *Module) Export(export *goja.Object) {
|
||||
funcs := map[string]any{
|
||||
"get": m.get,
|
||||
}
|
||||
|
||||
for name, fn := range funcs {
|
||||
if err := export.Set(name, fn); err != nil {
|
||||
panic(errors.Wrapf(err, "could not set '%s' function", name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
// ctx := util.AssertContext(call.Argument(0), rt)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Module) handleMessages() {
|
||||
ctx := context.Background()
|
||||
|
||||
err := m.bus.Reply(ctx, MessageNamespaceFetchRequest, func(msg bus.Message) (bus.Message, error) {
|
||||
fetchRequest, ok := msg.(*MessageFetchRequest)
|
||||
if !ok {
|
||||
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message fetch request, got '%T'", msg)
|
||||
}
|
||||
|
||||
res, err := m.handleFetchRequest(fetchRequest)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not handle fetch request", logger.E(errors.WithStack(err)))
|
||||
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "fetch request response", logger.F("response", res))
|
||||
|
||||
return res, nil
|
||||
})
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) handleFetchRequest(req *MessageFetchRequest) (*MessageFetchResponse, error) {
|
||||
res := NewMessageFetchResponse(req.RequestID)
|
||||
|
||||
ctx := logger.With(
|
||||
req.Context,
|
||||
logger.F("url", req.URL.String()),
|
||||
logger.F("remoteAddr", req.RemoteAddr),
|
||||
logger.F("requestID", req.RequestID),
|
||||
)
|
||||
|
||||
rawResult, err := m.server.ExecFuncByName(ctx, "onClientFetch", ctx, req.URL.String(), req.RemoteAddr)
|
||||
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 onClientFetch 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
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func ModuleFactory(bus bus.Bus) app.ServerModuleFactory {
|
||||
return func(server *app.Server) app.ServerModule {
|
||||
mod := &Module{
|
||||
bus: bus,
|
||||
server: server,
|
||||
}
|
||||
|
||||
go mod.handleMessages()
|
||||
|
||||
return mod
|
||||
}
|
||||
}
|
84
pkg/module/fetch/module_test.go
Normal file
84
pkg/module/fetch/module_test.go
Normal file
@ -0,0 +1,84 @@
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func TestFetchModule(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger.SetLevel(slog.LevelDebug)
|
||||
|
||||
bus := memory.NewBus()
|
||||
|
||||
server := app.NewServer(
|
||||
module.ContextModuleFactory(),
|
||||
module.ConsoleModuleFactory(),
|
||||
ModuleFactory(bus),
|
||||
)
|
||||
|
||||
data, err := ioutil.ReadFile("testdata/fetch.js")
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if err := server.Load("testdata/fetch.js", string(data)); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
defer server.Stop()
|
||||
|
||||
if err := server.Start(); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
// Wait for module to startup
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
remoteAddr := "127.0.0.1"
|
||||
url, _ := url.Parse("http://example.com")
|
||||
|
||||
rawReply, err := bus.Request(ctx, NewMessageFetchRequest(ctx, remoteAddr, url))
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
reply, ok := rawReply.(*MessageFetchResponse)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected reply type '%T'", rawReply)
|
||||
}
|
||||
|
||||
if e, g := true, reply.Allow; e != g {
|
||||
t.Errorf("reply.Allow: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
url, _ = url.Parse("https://google.com")
|
||||
|
||||
rawReply, err = bus.Request(ctx, NewMessageFetchRequest(ctx, remoteAddr, url))
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
reply, ok = rawReply.(*MessageFetchResponse)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected reply type '%T'", rawReply)
|
||||
}
|
||||
|
||||
if e, g := false, reply.Allow; e != g {
|
||||
t.Errorf("reply.Allow: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
}
|
7
pkg/module/fetch/testdata/fetch.js
vendored
Normal file
7
pkg/module/fetch/testdata/fetch.js
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
var ctx = context.new();
|
||||
|
||||
function onClientFetch(ctx, url, remoteAddr) {
|
||||
if (url === 'http://example.com') return { allow: true };
|
||||
return { allow: false };
|
||||
}
|
@ -21,7 +21,7 @@ func (m *LifecycleModule) Export(export *goja.Object) {
|
||||
}
|
||||
|
||||
func (m *LifecycleModule) OnInit() error {
|
||||
if _, err := m.server.ExecFuncByName("onInit"); err != nil {
|
||||
if _, err := m.server.ExecFuncByName(context.Background(), "onInit"); err != nil {
|
||||
if errors.Is(err, app.ErrFuncDoesNotExist) {
|
||||
logger.Warn(context.Background(), "could not find onInit() function", logger.E(errors.WithStack(err)))
|
||||
|
||||
|
@ -38,9 +38,10 @@ func (m *Module) broadcast(call goja.FunctionCall, rt *goja.Runtime) goja.Value
|
||||
}
|
||||
|
||||
data := call.Argument(0).Export()
|
||||
ctx := context.Background()
|
||||
|
||||
msg := module.NewServerMessage(nil, data)
|
||||
if err := m.bus.Publish(context.Background(), msg); err != nil {
|
||||
msg := module.NewServerMessage(ctx, data)
|
||||
if err := m.bus.Publish(ctx, msg); err != nil {
|
||||
panic(rt.ToValue(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
@ -129,7 +130,7 @@ func (m *Module) handleClientMessages() {
|
||||
logger.F("message", clientMessage),
|
||||
)
|
||||
|
||||
if _, err := m.server.ExecFuncByName("onClientMessage", clientMessage.Context, clientMessage.Data); err != nil {
|
||||
if _, err := m.server.ExecFuncByName(clientMessage.Context, "onClientMessage", clientMessage.Context, clientMessage.Data); err != nil {
|
||||
if errors.Is(err, app.ErrFuncDoesNotExist) {
|
||||
continue
|
||||
}
|
||||
|
@ -161,7 +161,7 @@ func (m *RPCModule) handleMessages() {
|
||||
continue
|
||||
}
|
||||
|
||||
result, err := m.server.Exec(callable, clientMessage.Context, req.Params)
|
||||
result, err := m.server.Exec(clientMessage.Context, callable, clientMessage.Context, req.Params)
|
||||
if err != nil {
|
||||
logger.Error(
|
||||
ctx, "rpc call error",
|
||||
|
@ -102,7 +102,7 @@ func (m *Module) query(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
}
|
||||
|
||||
if queryOptions.Offset != nil {
|
||||
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOffset(*queryOptions.Limit))
|
||||
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOffset(*queryOptions.Offset))
|
||||
}
|
||||
|
||||
if queryOptions.OrderDirection != nil {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
@ -14,7 +15,7 @@ import (
|
||||
func TestStoreModule(t *testing.T) {
|
||||
logger.SetLevel(logger.LevelDebug)
|
||||
|
||||
store := sqlite.NewDocumentStore(":memory:")
|
||||
store := sqlite.NewDocumentStore(":memory:?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000")
|
||||
server := app.NewServer(
|
||||
module.ContextModuleFactory(),
|
||||
module.ConsoleModuleFactory(),
|
||||
@ -34,7 +35,7 @@ func TestStoreModule(t *testing.T) {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if _, err := server.ExecFuncByName("testStore"); err != nil {
|
||||
if _, err := server.ExecFuncByName(context.Background(), "testStore"); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,7 @@ func AssertType[T any](v goja.Value, rt *goja.Runtime) T {
|
||||
return c
|
||||
}
|
||||
|
||||
panic(rt.ToValue(errors.Errorf("expected value to be a '%T', got '%T'", *new(T), v.Export())))
|
||||
panic(rt.ToValue(errors.Errorf("expected value to be a '%T', got '%T'", new(T), v.Export())))
|
||||
}
|
||||
|
||||
func AssertContext(v goja.Value, r *goja.Runtime) context.Context {
|
||||
|
4162
pkg/sdk/client/dist/client.js
vendored
Normal file
4162
pkg/sdk/client/dist/client.js
vendored
Normal file
@ -0,0 +1,4162 @@
|
||||
var Edge = (() => {
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __commonJS = (cb, mod) => function __require() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
};
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// node_modules/sockjs-client/lib/utils/browser-crypto.js
|
||||
var require_browser_crypto = __commonJS({
|
||||
"node_modules/sockjs-client/lib/utils/browser-crypto.js"(exports, module) {
|
||||
"use strict";
|
||||
if (window.crypto && window.crypto.getRandomValues) {
|
||||
module.exports.randomBytes = function(length) {
|
||||
var bytes = new Uint8Array(length);
|
||||
window.crypto.getRandomValues(bytes);
|
||||
return bytes;
|
||||
};
|
||||
} else {
|
||||
module.exports.randomBytes = function(length) {
|
||||
var bytes = new Array(length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
bytes[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/utils/random.js
|
||||
var require_random = __commonJS({
|
||||
"node_modules/sockjs-client/lib/utils/random.js"(exports, module) {
|
||||
"use strict";
|
||||
var crypto = require_browser_crypto();
|
||||
var _randomStringChars = "abcdefghijklmnopqrstuvwxyz012345";
|
||||
module.exports = {
|
||||
string: function(length) {
|
||||
var max = _randomStringChars.length;
|
||||
var bytes = crypto.randomBytes(length);
|
||||
var ret = [];
|
||||
for (var i = 0; i < length; i++) {
|
||||
ret.push(_randomStringChars.substr(bytes[i] % max, 1));
|
||||
}
|
||||
return ret.join("");
|
||||
},
|
||||
number: function(max) {
|
||||
return Math.floor(Math.random() * max);
|
||||
},
|
||||
numberString: function(max) {
|
||||
var t = ("" + (max - 1)).length;
|
||||
var p = new Array(t + 1).join("0");
|
||||
return (p + this.number(max)).slice(-t);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/utils/event.js
|
||||
var require_event = __commonJS({
|
||||
"node_modules/sockjs-client/lib/utils/event.js"(exports, module) {
|
||||
"use strict";
|
||||
var random = require_random();
|
||||
var onUnload = {};
|
||||
var afterUnload = false;
|
||||
var isChromePackagedApp = window.chrome && window.chrome.app && window.chrome.app.runtime;
|
||||
module.exports = {
|
||||
attachEvent: function(event, listener) {
|
||||
if (typeof window.addEventListener !== "undefined") {
|
||||
window.addEventListener(event, listener, false);
|
||||
} else if (window.document && window.attachEvent) {
|
||||
window.document.attachEvent("on" + event, listener);
|
||||
window.attachEvent("on" + event, listener);
|
||||
}
|
||||
},
|
||||
detachEvent: function(event, listener) {
|
||||
if (typeof window.addEventListener !== "undefined") {
|
||||
window.removeEventListener(event, listener, false);
|
||||
} else if (window.document && window.detachEvent) {
|
||||
window.document.detachEvent("on" + event, listener);
|
||||
window.detachEvent("on" + event, listener);
|
||||
}
|
||||
},
|
||||
unloadAdd: function(listener) {
|
||||
if (isChromePackagedApp) {
|
||||
return null;
|
||||
}
|
||||
var ref = random.string(8);
|
||||
onUnload[ref] = listener;
|
||||
if (afterUnload) {
|
||||
setTimeout(this.triggerUnloadCallbacks, 0);
|
||||
}
|
||||
return ref;
|
||||
},
|
||||
unloadDel: function(ref) {
|
||||
if (ref in onUnload) {
|
||||
delete onUnload[ref];
|
||||
}
|
||||
},
|
||||
triggerUnloadCallbacks: function() {
|
||||
for (var ref in onUnload) {
|
||||
onUnload[ref]();
|
||||
delete onUnload[ref];
|
||||
}
|
||||
}
|
||||
};
|
||||
var unloadTriggered = function() {
|
||||
if (afterUnload) {
|
||||
return;
|
||||
}
|
||||
afterUnload = true;
|
||||
module.exports.triggerUnloadCallbacks();
|
||||
};
|
||||
if (!isChromePackagedApp) {
|
||||
module.exports.attachEvent("unload", unloadTriggered);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/requires-port/index.js
|
||||
var require_requires_port = __commonJS({
|
||||
"node_modules/requires-port/index.js"(exports, module) {
|
||||
"use strict";
|
||||
module.exports = function required(port, protocol) {
|
||||
protocol = protocol.split(":")[0];
|
||||
port = +port;
|
||||
if (!port)
|
||||
return false;
|
||||
switch (protocol) {
|
||||
case "http":
|
||||
case "ws":
|
||||
return port !== 80;
|
||||
case "https":
|
||||
case "wss":
|
||||
return port !== 443;
|
||||
case "ftp":
|
||||
return port !== 21;
|
||||
case "gopher":
|
||||
return port !== 70;
|
||||
case "file":
|
||||
return false;
|
||||
}
|
||||
return port !== 0;
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/querystringify/index.js
|
||||
var require_querystringify = __commonJS({
|
||||
"node_modules/querystringify/index.js"(exports) {
|
||||
"use strict";
|
||||
var has = Object.prototype.hasOwnProperty;
|
||||
var undef;
|
||||
function decode(input) {
|
||||
try {
|
||||
return decodeURIComponent(input.replace(/\+/g, " "));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function encode(input) {
|
||||
try {
|
||||
return encodeURIComponent(input);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function querystring(query) {
|
||||
var parser = /([^=?#&]+)=?([^&]*)/g, result = {}, part;
|
||||
while (part = parser.exec(query)) {
|
||||
var key = decode(part[1]), value = decode(part[2]);
|
||||
if (key === null || value === null || key in result)
|
||||
continue;
|
||||
result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function querystringify(obj, prefix) {
|
||||
prefix = prefix || "";
|
||||
var pairs = [], value, key;
|
||||
if ("string" !== typeof prefix)
|
||||
prefix = "?";
|
||||
for (key in obj) {
|
||||
if (has.call(obj, key)) {
|
||||
value = obj[key];
|
||||
if (!value && (value === null || value === undef || isNaN(value))) {
|
||||
value = "";
|
||||
}
|
||||
key = encode(key);
|
||||
value = encode(value);
|
||||
if (key === null || value === null)
|
||||
continue;
|
||||
pairs.push(key + "=" + value);
|
||||
}
|
||||
}
|
||||
return pairs.length ? prefix + pairs.join("&") : "";
|
||||
}
|
||||
exports.stringify = querystringify;
|
||||
exports.parse = querystring;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/url-parse/index.js
|
||||
var require_url_parse = __commonJS({
|
||||
"node_modules/url-parse/index.js"(exports, module) {
|
||||
"use strict";
|
||||
var required = require_requires_port();
|
||||
var qs = require_querystringify();
|
||||
var controlOrWhitespace = /^[\x00-\x20\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/;
|
||||
var CRHTLF = /[\n\r\t]/g;
|
||||
var slashes = /^[A-Za-z][A-Za-z0-9+-.]*:\/\//;
|
||||
var port = /:\d+$/;
|
||||
var protocolre = /^([a-z][a-z0-9.+-]*:)?(\/\/)?([\\/]+)?([\S\s]*)/i;
|
||||
var windowsDriveLetter = /^[a-zA-Z]:/;
|
||||
function trimLeft(str) {
|
||||
return (str ? str : "").toString().replace(controlOrWhitespace, "");
|
||||
}
|
||||
var rules = [
|
||||
["#", "hash"],
|
||||
// Extract from the back.
|
||||
["?", "query"],
|
||||
// Extract from the back.
|
||||
function sanitize(address, url) {
|
||||
return isSpecial(url.protocol) ? address.replace(/\\/g, "/") : address;
|
||||
},
|
||||
["/", "pathname"],
|
||||
// Extract from the back.
|
||||
["@", "auth", 1],
|
||||
// Extract from the front.
|
||||
[NaN, "host", void 0, 1, 1],
|
||||
// Set left over value.
|
||||
[/:(\d*)$/, "port", void 0, 1],
|
||||
// RegExp the back.
|
||||
[NaN, "hostname", void 0, 1, 1]
|
||||
// Set left over.
|
||||
];
|
||||
var ignore = { hash: 1, query: 1 };
|
||||
function lolcation(loc) {
|
||||
var globalVar;
|
||||
if (typeof window !== "undefined")
|
||||
globalVar = window;
|
||||
else if (typeof window !== "undefined")
|
||||
globalVar = window;
|
||||
else if (typeof self !== "undefined")
|
||||
globalVar = self;
|
||||
else
|
||||
globalVar = {};
|
||||
var location = globalVar.location || {};
|
||||
loc = loc || location;
|
||||
var finaldestination = {}, type = typeof loc, key;
|
||||
if ("blob:" === loc.protocol) {
|
||||
finaldestination = new Url(unescape(loc.pathname), {});
|
||||
} else if ("string" === type) {
|
||||
finaldestination = new Url(loc, {});
|
||||
for (key in ignore)
|
||||
delete finaldestination[key];
|
||||
} else if ("object" === type) {
|
||||
for (key in loc) {
|
||||
if (key in ignore)
|
||||
continue;
|
||||
finaldestination[key] = loc[key];
|
||||
}
|
||||
if (finaldestination.slashes === void 0) {
|
||||
finaldestination.slashes = slashes.test(loc.href);
|
||||
}
|
||||
}
|
||||
return finaldestination;
|
||||
}
|
||||
function isSpecial(scheme) {
|
||||
return scheme === "file:" || scheme === "ftp:" || scheme === "http:" || scheme === "https:" || scheme === "ws:" || scheme === "wss:";
|
||||
}
|
||||
function extractProtocol(address, location) {
|
||||
address = trimLeft(address);
|
||||
address = address.replace(CRHTLF, "");
|
||||
location = location || {};
|
||||
var match = protocolre.exec(address);
|
||||
var protocol = match[1] ? match[1].toLowerCase() : "";
|
||||
var forwardSlashes = !!match[2];
|
||||
var otherSlashes = !!match[3];
|
||||
var slashesCount = 0;
|
||||
var rest;
|
||||
if (forwardSlashes) {
|
||||
if (otherSlashes) {
|
||||
rest = match[2] + match[3] + match[4];
|
||||
slashesCount = match[2].length + match[3].length;
|
||||
} else {
|
||||
rest = match[2] + match[4];
|
||||
slashesCount = match[2].length;
|
||||
}
|
||||
} else {
|
||||
if (otherSlashes) {
|
||||
rest = match[3] + match[4];
|
||||
slashesCount = match[3].length;
|
||||
} else {
|
||||
rest = match[4];
|
||||
}
|
||||
}
|
||||
if (protocol === "file:") {
|
||||
if (slashesCount >= 2) {
|
||||
rest = rest.slice(2);
|
||||
}
|
||||
} else if (isSpecial(protocol)) {
|
||||
rest = match[4];
|
||||
} else if (protocol) {
|
||||
if (forwardSlashes) {
|
||||
rest = rest.slice(2);
|
||||
}
|
||||
} else if (slashesCount >= 2 && isSpecial(location.protocol)) {
|
||||
rest = match[4];
|
||||
}
|
||||
return {
|
||||
protocol,
|
||||
slashes: forwardSlashes || isSpecial(protocol),
|
||||
slashesCount,
|
||||
rest
|
||||
};
|
||||
}
|
||||
function resolve(relative, base) {
|
||||
if (relative === "")
|
||||
return base;
|
||||
var path = (base || "/").split("/").slice(0, -1).concat(relative.split("/")), i = path.length, last = path[i - 1], unshift = false, up = 0;
|
||||
while (i--) {
|
||||
if (path[i] === ".") {
|
||||
path.splice(i, 1);
|
||||
} else if (path[i] === "..") {
|
||||
path.splice(i, 1);
|
||||
up++;
|
||||
} else if (up) {
|
||||
if (i === 0)
|
||||
unshift = true;
|
||||
path.splice(i, 1);
|
||||
up--;
|
||||
}
|
||||
}
|
||||
if (unshift)
|
||||
path.unshift("");
|
||||
if (last === "." || last === "..")
|
||||
path.push("");
|
||||
return path.join("/");
|
||||
}
|
||||
function Url(address, location, parser) {
|
||||
address = trimLeft(address);
|
||||
address = address.replace(CRHTLF, "");
|
||||
if (!(this instanceof Url)) {
|
||||
return new Url(address, location, parser);
|
||||
}
|
||||
var relative, extracted, parse, instruction, index, key, instructions = rules.slice(), type = typeof location, url = this, i = 0;
|
||||
if ("object" !== type && "string" !== type) {
|
||||
parser = location;
|
||||
location = null;
|
||||
}
|
||||
if (parser && "function" !== typeof parser)
|
||||
parser = qs.parse;
|
||||
location = lolcation(location);
|
||||
extracted = extractProtocol(address || "", location);
|
||||
relative = !extracted.protocol && !extracted.slashes;
|
||||
url.slashes = extracted.slashes || relative && location.slashes;
|
||||
url.protocol = extracted.protocol || location.protocol || "";
|
||||
address = extracted.rest;
|
||||
if (extracted.protocol === "file:" && (extracted.slashesCount !== 2 || windowsDriveLetter.test(address)) || !extracted.slashes && (extracted.protocol || extracted.slashesCount < 2 || !isSpecial(url.protocol))) {
|
||||
instructions[3] = [/(.*)/, "pathname"];
|
||||
}
|
||||
for (; i < instructions.length; i++) {
|
||||
instruction = instructions[i];
|
||||
if (typeof instruction === "function") {
|
||||
address = instruction(address, url);
|
||||
continue;
|
||||
}
|
||||
parse = instruction[0];
|
||||
key = instruction[1];
|
||||
if (parse !== parse) {
|
||||
url[key] = address;
|
||||
} else if ("string" === typeof parse) {
|
||||
index = parse === "@" ? address.lastIndexOf(parse) : address.indexOf(parse);
|
||||
if (~index) {
|
||||
if ("number" === typeof instruction[2]) {
|
||||
url[key] = address.slice(0, index);
|
||||
address = address.slice(index + instruction[2]);
|
||||
} else {
|
||||
url[key] = address.slice(index);
|
||||
address = address.slice(0, index);
|
||||
}
|
||||
}
|
||||
} else if (index = parse.exec(address)) {
|
||||
url[key] = index[1];
|
||||
address = address.slice(0, index.index);
|
||||
}
|
||||
url[key] = url[key] || (relative && instruction[3] ? location[key] || "" : "");
|
||||
if (instruction[4])
|
||||
url[key] = url[key].toLowerCase();
|
||||
}
|
||||
if (parser)
|
||||
url.query = parser(url.query);
|
||||
if (relative && location.slashes && url.pathname.charAt(0) !== "/" && (url.pathname !== "" || location.pathname !== "")) {
|
||||
url.pathname = resolve(url.pathname, location.pathname);
|
||||
}
|
||||
if (url.pathname.charAt(0) !== "/" && isSpecial(url.protocol)) {
|
||||
url.pathname = "/" + url.pathname;
|
||||
}
|
||||
if (!required(url.port, url.protocol)) {
|
||||
url.host = url.hostname;
|
||||
url.port = "";
|
||||
}
|
||||
url.username = url.password = "";
|
||||
if (url.auth) {
|
||||
index = url.auth.indexOf(":");
|
||||
if (~index) {
|
||||
url.username = url.auth.slice(0, index);
|
||||
url.username = encodeURIComponent(decodeURIComponent(url.username));
|
||||
url.password = url.auth.slice(index + 1);
|
||||
url.password = encodeURIComponent(decodeURIComponent(url.password));
|
||||
} else {
|
||||
url.username = encodeURIComponent(decodeURIComponent(url.auth));
|
||||
}
|
||||
url.auth = url.password ? url.username + ":" + url.password : url.username;
|
||||
}
|
||||
url.origin = url.protocol !== "file:" && isSpecial(url.protocol) && url.host ? url.protocol + "//" + url.host : "null";
|
||||
url.href = url.toString();
|
||||
}
|
||||
function set(part, value, fn) {
|
||||
var url = this;
|
||||
switch (part) {
|
||||
case "query":
|
||||
if ("string" === typeof value && value.length) {
|
||||
value = (fn || qs.parse)(value);
|
||||
}
|
||||
url[part] = value;
|
||||
break;
|
||||
case "port":
|
||||
url[part] = value;
|
||||
if (!required(value, url.protocol)) {
|
||||
url.host = url.hostname;
|
||||
url[part] = "";
|
||||
} else if (value) {
|
||||
url.host = url.hostname + ":" + value;
|
||||
}
|
||||
break;
|
||||
case "hostname":
|
||||
url[part] = value;
|
||||
if (url.port)
|
||||
value += ":" + url.port;
|
||||
url.host = value;
|
||||
break;
|
||||
case "host":
|
||||
url[part] = value;
|
||||
if (port.test(value)) {
|
||||
value = value.split(":");
|
||||
url.port = value.pop();
|
||||
url.hostname = value.join(":");
|
||||
} else {
|
||||
url.hostname = value;
|
||||
url.port = "";
|
||||
}
|
||||
break;
|
||||
case "protocol":
|
||||
url.protocol = value.toLowerCase();
|
||||
url.slashes = !fn;
|
||||
break;
|
||||
case "pathname":
|
||||
case "hash":
|
||||
if (value) {
|
||||
var char = part === "pathname" ? "/" : "#";
|
||||
url[part] = value.charAt(0) !== char ? char + value : value;
|
||||
} else {
|
||||
url[part] = value;
|
||||
}
|
||||
break;
|
||||
case "username":
|
||||
case "password":
|
||||
url[part] = encodeURIComponent(value);
|
||||
break;
|
||||
case "auth":
|
||||
var index = value.indexOf(":");
|
||||
if (~index) {
|
||||
url.username = value.slice(0, index);
|
||||
url.username = encodeURIComponent(decodeURIComponent(url.username));
|
||||
url.password = value.slice(index + 1);
|
||||
url.password = encodeURIComponent(decodeURIComponent(url.password));
|
||||
} else {
|
||||
url.username = encodeURIComponent(decodeURIComponent(value));
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < rules.length; i++) {
|
||||
var ins = rules[i];
|
||||
if (ins[4])
|
||||
url[ins[1]] = url[ins[1]].toLowerCase();
|
||||
}
|
||||
url.auth = url.password ? url.username + ":" + url.password : url.username;
|
||||
url.origin = url.protocol !== "file:" && isSpecial(url.protocol) && url.host ? url.protocol + "//" + url.host : "null";
|
||||
url.href = url.toString();
|
||||
return url;
|
||||
}
|
||||
function toString(stringify) {
|
||||
if (!stringify || "function" !== typeof stringify)
|
||||
stringify = qs.stringify;
|
||||
var query, url = this, host = url.host, protocol = url.protocol;
|
||||
if (protocol && protocol.charAt(protocol.length - 1) !== ":")
|
||||
protocol += ":";
|
||||
var result = protocol + (url.protocol && url.slashes || isSpecial(url.protocol) ? "//" : "");
|
||||
if (url.username) {
|
||||
result += url.username;
|
||||
if (url.password)
|
||||
result += ":" + url.password;
|
||||
result += "@";
|
||||
} else if (url.password) {
|
||||
result += ":" + url.password;
|
||||
result += "@";
|
||||
} else if (url.protocol !== "file:" && isSpecial(url.protocol) && !host && url.pathname !== "/") {
|
||||
result += "@";
|
||||
}
|
||||
if (host[host.length - 1] === ":" || port.test(url.hostname) && !url.port) {
|
||||
host += ":";
|
||||
}
|
||||
result += host + url.pathname;
|
||||
query = "object" === typeof url.query ? stringify(url.query) : url.query;
|
||||
if (query)
|
||||
result += "?" !== query.charAt(0) ? "?" + query : query;
|
||||
if (url.hash)
|
||||
result += url.hash;
|
||||
return result;
|
||||
}
|
||||
Url.prototype = { set, toString };
|
||||
Url.extractProtocol = extractProtocol;
|
||||
Url.location = lolcation;
|
||||
Url.trimLeft = trimLeft;
|
||||
Url.qs = qs;
|
||||
module.exports = Url;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/ms/index.js
|
||||
var require_ms = __commonJS({
|
||||
"node_modules/ms/index.js"(exports, module) {
|
||||
var s = 1e3;
|
||||
var m = s * 60;
|
||||
var h = m * 60;
|
||||
var d = h * 24;
|
||||
var w = d * 7;
|
||||
var y = d * 365.25;
|
||||
module.exports = function(val, options) {
|
||||
options = options || {};
|
||||
var type = typeof val;
|
||||
if (type === "string" && val.length > 0) {
|
||||
return parse(val);
|
||||
} else if (type === "number" && isFinite(val)) {
|
||||
return options.long ? fmtLong(val) : fmtShort(val);
|
||||
}
|
||||
throw new Error(
|
||||
"val is not a non-empty string or a valid number. val=" + JSON.stringify(val)
|
||||
);
|
||||
};
|
||||
function parse(str) {
|
||||
str = String(str);
|
||||
if (str.length > 100) {
|
||||
return;
|
||||
}
|
||||
var match = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(
|
||||
str
|
||||
);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
var n = parseFloat(match[1]);
|
||||
var type = (match[2] || "ms").toLowerCase();
|
||||
switch (type) {
|
||||
case "years":
|
||||
case "year":
|
||||
case "yrs":
|
||||
case "yr":
|
||||
case "y":
|
||||
return n * y;
|
||||
case "weeks":
|
||||
case "week":
|
||||
case "w":
|
||||
return n * w;
|
||||
case "days":
|
||||
case "day":
|
||||
case "d":
|
||||
return n * d;
|
||||
case "hours":
|
||||
case "hour":
|
||||
case "hrs":
|
||||
case "hr":
|
||||
case "h":
|
||||
return n * h;
|
||||
case "minutes":
|
||||
case "minute":
|
||||
case "mins":
|
||||
case "min":
|
||||
case "m":
|
||||
return n * m;
|
||||
case "seconds":
|
||||
case "second":
|
||||
case "secs":
|
||||
case "sec":
|
||||
case "s":
|
||||
return n * s;
|
||||
case "milliseconds":
|
||||
case "millisecond":
|
||||
case "msecs":
|
||||
case "msec":
|
||||
case "ms":
|
||||
return n;
|
||||
default:
|
||||
return void 0;
|
||||
}
|
||||
}
|
||||
function fmtShort(ms) {
|
||||
var msAbs = Math.abs(ms);
|
||||
if (msAbs >= d) {
|
||||
return Math.round(ms / d) + "d";
|
||||
}
|
||||
if (msAbs >= h) {
|
||||
return Math.round(ms / h) + "h";
|
||||
}
|
||||
if (msAbs >= m) {
|
||||
return Math.round(ms / m) + "m";
|
||||
}
|
||||
if (msAbs >= s) {
|
||||
return Math.round(ms / s) + "s";
|
||||
}
|
||||
return ms + "ms";
|
||||
}
|
||||
function fmtLong(ms) {
|
||||
var msAbs = Math.abs(ms);
|
||||
if (msAbs >= d) {
|
||||
return plural(ms, msAbs, d, "day");
|
||||
}
|
||||
if (msAbs >= h) {
|
||||
return plural(ms, msAbs, h, "hour");
|
||||
}
|
||||
if (msAbs >= m) {
|
||||
return plural(ms, msAbs, m, "minute");
|
||||
}
|
||||
if (msAbs >= s) {
|
||||
return plural(ms, msAbs, s, "second");
|
||||
}
|
||||
return ms + " ms";
|
||||
}
|
||||
function plural(ms, msAbs, n, name) {
|
||||
var isPlural = msAbs >= n * 1.5;
|
||||
return Math.round(ms / n) + " " + name + (isPlural ? "s" : "");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/debug/src/common.js
|
||||
var require_common = __commonJS({
|
||||
"node_modules/debug/src/common.js"(exports, module) {
|
||||
"use strict";
|
||||
function setup(env) {
|
||||
createDebug.debug = createDebug;
|
||||
createDebug.default = createDebug;
|
||||
createDebug.coerce = coerce;
|
||||
createDebug.disable = disable;
|
||||
createDebug.enable = enable;
|
||||
createDebug.enabled = enabled;
|
||||
createDebug.humanize = require_ms();
|
||||
Object.keys(env).forEach(function(key) {
|
||||
createDebug[key] = env[key];
|
||||
});
|
||||
createDebug.instances = [];
|
||||
createDebug.names = [];
|
||||
createDebug.skips = [];
|
||||
createDebug.formatters = {};
|
||||
function selectColor(namespace) {
|
||||
var hash = 0;
|
||||
for (var i = 0; i < namespace.length; i++) {
|
||||
hash = (hash << 5) - hash + namespace.charCodeAt(i);
|
||||
hash |= 0;
|
||||
}
|
||||
return createDebug.colors[Math.abs(hash) % createDebug.colors.length];
|
||||
}
|
||||
createDebug.selectColor = selectColor;
|
||||
function createDebug(namespace) {
|
||||
var prevTime;
|
||||
function debug() {
|
||||
if (!debug.enabled) {
|
||||
return;
|
||||
}
|
||||
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
||||
args[_key] = arguments[_key];
|
||||
}
|
||||
var self2 = debug;
|
||||
var curr = Number(/* @__PURE__ */ new Date());
|
||||
var ms = curr - (prevTime || curr);
|
||||
self2.diff = ms;
|
||||
self2.prev = prevTime;
|
||||
self2.curr = curr;
|
||||
prevTime = curr;
|
||||
args[0] = createDebug.coerce(args[0]);
|
||||
if (typeof args[0] !== "string") {
|
||||
args.unshift("%O");
|
||||
}
|
||||
var index = 0;
|
||||
args[0] = args[0].replace(/%([a-zA-Z%])/g, function(match, format) {
|
||||
if (match === "%%") {
|
||||
return match;
|
||||
}
|
||||
index++;
|
||||
var formatter = createDebug.formatters[format];
|
||||
if (typeof formatter === "function") {
|
||||
var val = args[index];
|
||||
match = formatter.call(self2, val);
|
||||
args.splice(index, 1);
|
||||
index--;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
createDebug.formatArgs.call(self2, args);
|
||||
var logFn = self2.log || createDebug.log;
|
||||
logFn.apply(self2, args);
|
||||
}
|
||||
debug.namespace = namespace;
|
||||
debug.enabled = createDebug.enabled(namespace);
|
||||
debug.useColors = createDebug.useColors();
|
||||
debug.color = selectColor(namespace);
|
||||
debug.destroy = destroy;
|
||||
debug.extend = extend;
|
||||
if (typeof createDebug.init === "function") {
|
||||
createDebug.init(debug);
|
||||
}
|
||||
createDebug.instances.push(debug);
|
||||
return debug;
|
||||
}
|
||||
function destroy() {
|
||||
var index = createDebug.instances.indexOf(this);
|
||||
if (index !== -1) {
|
||||
createDebug.instances.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function extend(namespace, delimiter) {
|
||||
return createDebug(this.namespace + (typeof delimiter === "undefined" ? ":" : delimiter) + namespace);
|
||||
}
|
||||
function enable(namespaces) {
|
||||
createDebug.save(namespaces);
|
||||
createDebug.names = [];
|
||||
createDebug.skips = [];
|
||||
var i;
|
||||
var split = (typeof namespaces === "string" ? namespaces : "").split(/[\s,]+/);
|
||||
var len = split.length;
|
||||
for (i = 0; i < len; i++) {
|
||||
if (!split[i]) {
|
||||
continue;
|
||||
}
|
||||
namespaces = split[i].replace(/\*/g, ".*?");
|
||||
if (namespaces[0] === "-") {
|
||||
createDebug.skips.push(new RegExp("^" + namespaces.substr(1) + "$"));
|
||||
} else {
|
||||
createDebug.names.push(new RegExp("^" + namespaces + "$"));
|
||||
}
|
||||
}
|
||||
for (i = 0; i < createDebug.instances.length; i++) {
|
||||
var instance = createDebug.instances[i];
|
||||
instance.enabled = createDebug.enabled(instance.namespace);
|
||||
}
|
||||
}
|
||||
function disable() {
|
||||
createDebug.enable("");
|
||||
}
|
||||
function enabled(name) {
|
||||
if (name[name.length - 1] === "*") {
|
||||
return true;
|
||||
}
|
||||
var i;
|
||||
var len;
|
||||
for (i = 0, len = createDebug.skips.length; i < len; i++) {
|
||||
if (createDebug.skips[i].test(name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (i = 0, len = createDebug.names.length; i < len; i++) {
|
||||
if (createDebug.names[i].test(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function coerce(val) {
|
||||
if (val instanceof Error) {
|
||||
return val.stack || val.message;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
createDebug.enable(createDebug.load());
|
||||
return createDebug;
|
||||
}
|
||||
module.exports = setup;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/debug/src/browser.js
|
||||
var require_browser = __commonJS({
|
||||
"node_modules/debug/src/browser.js"(exports, module) {
|
||||
"use strict";
|
||||
function _typeof(obj) {
|
||||
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
|
||||
_typeof = function _typeof2(obj2) {
|
||||
return typeof obj2;
|
||||
};
|
||||
} else {
|
||||
_typeof = function _typeof2(obj2) {
|
||||
return obj2 && typeof Symbol === "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype ? "symbol" : typeof obj2;
|
||||
};
|
||||
}
|
||||
return _typeof(obj);
|
||||
}
|
||||
exports.log = log;
|
||||
exports.formatArgs = formatArgs;
|
||||
exports.save = save;
|
||||
exports.load = load;
|
||||
exports.useColors = useColors;
|
||||
exports.storage = localstorage();
|
||||
exports.colors = ["#0000CC", "#0000FF", "#0033CC", "#0033FF", "#0066CC", "#0066FF", "#0099CC", "#0099FF", "#00CC00", "#00CC33", "#00CC66", "#00CC99", "#00CCCC", "#00CCFF", "#3300CC", "#3300FF", "#3333CC", "#3333FF", "#3366CC", "#3366FF", "#3399CC", "#3399FF", "#33CC00", "#33CC33", "#33CC66", "#33CC99", "#33CCCC", "#33CCFF", "#6600CC", "#6600FF", "#6633CC", "#6633FF", "#66CC00", "#66CC33", "#9900CC", "#9900FF", "#9933CC", "#9933FF", "#99CC00", "#99CC33", "#CC0000", "#CC0033", "#CC0066", "#CC0099", "#CC00CC", "#CC00FF", "#CC3300", "#CC3333", "#CC3366", "#CC3399", "#CC33CC", "#CC33FF", "#CC6600", "#CC6633", "#CC9900", "#CC9933", "#CCCC00", "#CCCC33", "#FF0000", "#FF0033", "#FF0066", "#FF0099", "#FF00CC", "#FF00FF", "#FF3300", "#FF3333", "#FF3366", "#FF3399", "#FF33CC", "#FF33FF", "#FF6600", "#FF6633", "#FF9900", "#FF9933", "#FFCC00", "#FFCC33"];
|
||||
function useColors() {
|
||||
if (typeof window !== "undefined" && window.process && (window.process.type === "renderer" || window.process.__nwjs)) {
|
||||
return true;
|
||||
}
|
||||
if (typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) {
|
||||
return false;
|
||||
}
|
||||
return typeof document !== "undefined" && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance || // Is firebug? http://stackoverflow.com/a/398120/376773
|
||||
typeof window !== "undefined" && window.console && (window.console.firebug || window.console.exception && window.console.table) || // Is firefox >= v31?
|
||||
// https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages
|
||||
typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31 || // Double check webkit in userAgent just in case we are in a worker
|
||||
typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/);
|
||||
}
|
||||
function formatArgs(args) {
|
||||
args[0] = (this.useColors ? "%c" : "") + this.namespace + (this.useColors ? " %c" : " ") + args[0] + (this.useColors ? "%c " : " ") + "+" + module.exports.humanize(this.diff);
|
||||
if (!this.useColors) {
|
||||
return;
|
||||
}
|
||||
var c = "color: " + this.color;
|
||||
args.splice(1, 0, c, "color: inherit");
|
||||
var index = 0;
|
||||
var lastC = 0;
|
||||
args[0].replace(/%[a-zA-Z%]/g, function(match) {
|
||||
if (match === "%%") {
|
||||
return;
|
||||
}
|
||||
index++;
|
||||
if (match === "%c") {
|
||||
lastC = index;
|
||||
}
|
||||
});
|
||||
args.splice(lastC, 0, c);
|
||||
}
|
||||
function log() {
|
||||
var _console;
|
||||
return (typeof console === "undefined" ? "undefined" : _typeof(console)) === "object" && console.log && (_console = console).log.apply(_console, arguments);
|
||||
}
|
||||
function save(namespaces) {
|
||||
try {
|
||||
if (namespaces) {
|
||||
exports.storage.setItem("debug", namespaces);
|
||||
} else {
|
||||
exports.storage.removeItem("debug");
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
function load() {
|
||||
var r;
|
||||
try {
|
||||
r = exports.storage.getItem("debug");
|
||||
} catch (error) {
|
||||
}
|
||||
if (!r && typeof process !== "undefined" && "env" in process) {
|
||||
r = process.env.DEBUG;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
function localstorage() {
|
||||
try {
|
||||
return localStorage;
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
module.exports = require_common()(exports);
|
||||
var formatters = module.exports.formatters;
|
||||
formatters.j = function(v) {
|
||||
try {
|
||||
return JSON.stringify(v);
|
||||
} catch (error) {
|
||||
return "[UnexpectedJSONParseError]: " + error.message;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/utils/url.js
|
||||
var require_url = __commonJS({
|
||||
"node_modules/sockjs-client/lib/utils/url.js"(exports, module) {
|
||||
"use strict";
|
||||
var URL = require_url_parse();
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:utils:url");
|
||||
}
|
||||
module.exports = {
|
||||
getOrigin: function(url) {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
var p = new URL(url);
|
||||
if (p.protocol === "file:") {
|
||||
return null;
|
||||
}
|
||||
var port = p.port;
|
||||
if (!port) {
|
||||
port = p.protocol === "https:" ? "443" : "80";
|
||||
}
|
||||
return p.protocol + "//" + p.hostname + ":" + port;
|
||||
},
|
||||
isOriginEqual: function(a, b) {
|
||||
var res = this.getOrigin(a) === this.getOrigin(b);
|
||||
debug("same", a, b, res);
|
||||
return res;
|
||||
},
|
||||
isSchemeEqual: function(a, b) {
|
||||
return a.split(":")[0] === b.split(":")[0];
|
||||
},
|
||||
addPath: function(url, path) {
|
||||
var qs = url.split("?");
|
||||
return qs[0] + path + (qs[1] ? "?" + qs[1] : "");
|
||||
},
|
||||
addQuery: function(url, q) {
|
||||
return url + (url.indexOf("?") === -1 ? "?" + q : "&" + q);
|
||||
},
|
||||
isLoopbackAddr: function(addr) {
|
||||
return /^127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) || /^\[::1\]$/.test(addr);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/inherits/inherits_browser.js
|
||||
var require_inherits_browser = __commonJS({
|
||||
"node_modules/inherits/inherits_browser.js"(exports, module) {
|
||||
if (typeof Object.create === "function") {
|
||||
module.exports = function inherits(ctor, superCtor) {
|
||||
if (superCtor) {
|
||||
ctor.super_ = superCtor;
|
||||
ctor.prototype = Object.create(superCtor.prototype, {
|
||||
constructor: {
|
||||
value: ctor,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
configurable: true
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
} else {
|
||||
module.exports = function inherits(ctor, superCtor) {
|
||||
if (superCtor) {
|
||||
ctor.super_ = superCtor;
|
||||
var TempCtor = function() {
|
||||
};
|
||||
TempCtor.prototype = superCtor.prototype;
|
||||
ctor.prototype = new TempCtor();
|
||||
ctor.prototype.constructor = ctor;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/event/eventtarget.js
|
||||
var require_eventtarget = __commonJS({
|
||||
"node_modules/sockjs-client/lib/event/eventtarget.js"(exports, module) {
|
||||
"use strict";
|
||||
function EventTarget2() {
|
||||
this._listeners = {};
|
||||
}
|
||||
EventTarget2.prototype.addEventListener = function(eventType, listener) {
|
||||
if (!(eventType in this._listeners)) {
|
||||
this._listeners[eventType] = [];
|
||||
}
|
||||
var arr = this._listeners[eventType];
|
||||
if (arr.indexOf(listener) === -1) {
|
||||
arr = arr.concat([listener]);
|
||||
}
|
||||
this._listeners[eventType] = arr;
|
||||
};
|
||||
EventTarget2.prototype.removeEventListener = function(eventType, listener) {
|
||||
var arr = this._listeners[eventType];
|
||||
if (!arr) {
|
||||
return;
|
||||
}
|
||||
var idx = arr.indexOf(listener);
|
||||
if (idx !== -1) {
|
||||
if (arr.length > 1) {
|
||||
this._listeners[eventType] = arr.slice(0, idx).concat(arr.slice(idx + 1));
|
||||
} else {
|
||||
delete this._listeners[eventType];
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
EventTarget2.prototype.dispatchEvent = function() {
|
||||
var event = arguments[0];
|
||||
var t = event.type;
|
||||
var args = arguments.length === 1 ? [event] : Array.apply(null, arguments);
|
||||
if (this["on" + t]) {
|
||||
this["on" + t].apply(this, args);
|
||||
}
|
||||
if (t in this._listeners) {
|
||||
var listeners = this._listeners[t];
|
||||
for (var i = 0; i < listeners.length; i++) {
|
||||
listeners[i].apply(this, args);
|
||||
}
|
||||
}
|
||||
};
|
||||
module.exports = EventTarget2;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/event/emitter.js
|
||||
var require_emitter = __commonJS({
|
||||
"node_modules/sockjs-client/lib/event/emitter.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var EventTarget2 = require_eventtarget();
|
||||
function EventEmitter() {
|
||||
EventTarget2.call(this);
|
||||
}
|
||||
inherits(EventEmitter, EventTarget2);
|
||||
EventEmitter.prototype.removeAllListeners = function(type) {
|
||||
if (type) {
|
||||
delete this._listeners[type];
|
||||
} else {
|
||||
this._listeners = {};
|
||||
}
|
||||
};
|
||||
EventEmitter.prototype.once = function(type, listener) {
|
||||
var self2 = this, fired = false;
|
||||
function g() {
|
||||
self2.removeListener(type, g);
|
||||
if (!fired) {
|
||||
fired = true;
|
||||
listener.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
this.on(type, g);
|
||||
};
|
||||
EventEmitter.prototype.emit = function() {
|
||||
var type = arguments[0];
|
||||
var listeners = this._listeners[type];
|
||||
if (!listeners) {
|
||||
return;
|
||||
}
|
||||
var l = arguments.length;
|
||||
var args = new Array(l - 1);
|
||||
for (var ai = 1; ai < l; ai++) {
|
||||
args[ai - 1] = arguments[ai];
|
||||
}
|
||||
for (var i = 0; i < listeners.length; i++) {
|
||||
listeners[i].apply(this, args);
|
||||
}
|
||||
};
|
||||
EventEmitter.prototype.on = EventEmitter.prototype.addListener = EventTarget2.prototype.addEventListener;
|
||||
EventEmitter.prototype.removeListener = EventTarget2.prototype.removeEventListener;
|
||||
module.exports.EventEmitter = EventEmitter;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/browser/websocket.js
|
||||
var require_websocket = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/browser/websocket.js"(exports, module) {
|
||||
"use strict";
|
||||
var Driver = window.WebSocket || window.MozWebSocket;
|
||||
if (Driver) {
|
||||
module.exports = function WebSocketBrowserDriver(url) {
|
||||
return new Driver(url);
|
||||
};
|
||||
} else {
|
||||
module.exports = void 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/websocket.js
|
||||
var require_websocket2 = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/websocket.js"(exports, module) {
|
||||
"use strict";
|
||||
var utils = require_event();
|
||||
var urlUtils = require_url();
|
||||
var inherits = require_inherits_browser();
|
||||
var EventEmitter = require_emitter().EventEmitter;
|
||||
var WebsocketDriver = require_websocket();
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:websocket");
|
||||
}
|
||||
function WebSocketTransport(transUrl, ignore, options) {
|
||||
if (!WebSocketTransport.enabled()) {
|
||||
throw new Error("Transport created when disabled");
|
||||
}
|
||||
EventEmitter.call(this);
|
||||
debug("constructor", transUrl);
|
||||
var self2 = this;
|
||||
var url = urlUtils.addPath(transUrl, "/websocket");
|
||||
if (url.slice(0, 5) === "https") {
|
||||
url = "wss" + url.slice(5);
|
||||
} else {
|
||||
url = "ws" + url.slice(4);
|
||||
}
|
||||
this.url = url;
|
||||
this.ws = new WebsocketDriver(this.url, [], options);
|
||||
this.ws.onmessage = function(e) {
|
||||
debug("message event", e.data);
|
||||
self2.emit("message", e.data);
|
||||
};
|
||||
this.unloadRef = utils.unloadAdd(function() {
|
||||
debug("unload");
|
||||
self2.ws.close();
|
||||
});
|
||||
this.ws.onclose = function(e) {
|
||||
debug("close event", e.code, e.reason);
|
||||
self2.emit("close", e.code, e.reason);
|
||||
self2._cleanup();
|
||||
};
|
||||
this.ws.onerror = function(e) {
|
||||
debug("error event", e);
|
||||
self2.emit("close", 1006, "WebSocket connection broken");
|
||||
self2._cleanup();
|
||||
};
|
||||
}
|
||||
inherits(WebSocketTransport, EventEmitter);
|
||||
WebSocketTransport.prototype.send = function(data) {
|
||||
var msg = "[" + data + "]";
|
||||
debug("send", msg);
|
||||
this.ws.send(msg);
|
||||
};
|
||||
WebSocketTransport.prototype.close = function() {
|
||||
debug("close");
|
||||
var ws = this.ws;
|
||||
this._cleanup();
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
};
|
||||
WebSocketTransport.prototype._cleanup = function() {
|
||||
debug("_cleanup");
|
||||
var ws = this.ws;
|
||||
if (ws) {
|
||||
ws.onmessage = ws.onclose = ws.onerror = null;
|
||||
}
|
||||
utils.unloadDel(this.unloadRef);
|
||||
this.unloadRef = this.ws = null;
|
||||
this.removeAllListeners();
|
||||
};
|
||||
WebSocketTransport.enabled = function() {
|
||||
debug("enabled");
|
||||
return !!WebsocketDriver;
|
||||
};
|
||||
WebSocketTransport.transportName = "websocket";
|
||||
WebSocketTransport.roundTrips = 2;
|
||||
module.exports = WebSocketTransport;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/lib/buffered-sender.js
|
||||
var require_buffered_sender = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/lib/buffered-sender.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var EventEmitter = require_emitter().EventEmitter;
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:buffered-sender");
|
||||
}
|
||||
function BufferedSender(url, sender) {
|
||||
debug(url);
|
||||
EventEmitter.call(this);
|
||||
this.sendBuffer = [];
|
||||
this.sender = sender;
|
||||
this.url = url;
|
||||
}
|
||||
inherits(BufferedSender, EventEmitter);
|
||||
BufferedSender.prototype.send = function(message) {
|
||||
debug("send", message);
|
||||
this.sendBuffer.push(message);
|
||||
if (!this.sendStop) {
|
||||
this.sendSchedule();
|
||||
}
|
||||
};
|
||||
BufferedSender.prototype.sendScheduleWait = function() {
|
||||
debug("sendScheduleWait");
|
||||
var self2 = this;
|
||||
var tref;
|
||||
this.sendStop = function() {
|
||||
debug("sendStop");
|
||||
self2.sendStop = null;
|
||||
clearTimeout(tref);
|
||||
};
|
||||
tref = setTimeout(function() {
|
||||
debug("timeout");
|
||||
self2.sendStop = null;
|
||||
self2.sendSchedule();
|
||||
}, 25);
|
||||
};
|
||||
BufferedSender.prototype.sendSchedule = function() {
|
||||
debug("sendSchedule", this.sendBuffer.length);
|
||||
var self2 = this;
|
||||
if (this.sendBuffer.length > 0) {
|
||||
var payload = "[" + this.sendBuffer.join(",") + "]";
|
||||
this.sendStop = this.sender(this.url, payload, function(err) {
|
||||
self2.sendStop = null;
|
||||
if (err) {
|
||||
debug("error", err);
|
||||
self2.emit("close", err.code || 1006, "Sending error: " + err);
|
||||
self2.close();
|
||||
} else {
|
||||
self2.sendScheduleWait();
|
||||
}
|
||||
});
|
||||
this.sendBuffer = [];
|
||||
}
|
||||
};
|
||||
BufferedSender.prototype._cleanup = function() {
|
||||
debug("_cleanup");
|
||||
this.removeAllListeners();
|
||||
};
|
||||
BufferedSender.prototype.close = function() {
|
||||
debug("close");
|
||||
this._cleanup();
|
||||
if (this.sendStop) {
|
||||
this.sendStop();
|
||||
this.sendStop = null;
|
||||
}
|
||||
};
|
||||
module.exports = BufferedSender;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/lib/polling.js
|
||||
var require_polling = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/lib/polling.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var EventEmitter = require_emitter().EventEmitter;
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:polling");
|
||||
}
|
||||
function Polling(Receiver, receiveUrl, AjaxObject) {
|
||||
debug(receiveUrl);
|
||||
EventEmitter.call(this);
|
||||
this.Receiver = Receiver;
|
||||
this.receiveUrl = receiveUrl;
|
||||
this.AjaxObject = AjaxObject;
|
||||
this._scheduleReceiver();
|
||||
}
|
||||
inherits(Polling, EventEmitter);
|
||||
Polling.prototype._scheduleReceiver = function() {
|
||||
debug("_scheduleReceiver");
|
||||
var self2 = this;
|
||||
var poll = this.poll = new this.Receiver(this.receiveUrl, this.AjaxObject);
|
||||
poll.on("message", function(msg) {
|
||||
debug("message", msg);
|
||||
self2.emit("message", msg);
|
||||
});
|
||||
poll.once("close", function(code, reason) {
|
||||
debug("close", code, reason, self2.pollIsClosing);
|
||||
self2.poll = poll = null;
|
||||
if (!self2.pollIsClosing) {
|
||||
if (reason === "network") {
|
||||
self2._scheduleReceiver();
|
||||
} else {
|
||||
self2.emit("close", code || 1006, reason);
|
||||
self2.removeAllListeners();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
Polling.prototype.abort = function() {
|
||||
debug("abort");
|
||||
this.removeAllListeners();
|
||||
this.pollIsClosing = true;
|
||||
if (this.poll) {
|
||||
this.poll.abort();
|
||||
}
|
||||
};
|
||||
module.exports = Polling;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/lib/sender-receiver.js
|
||||
var require_sender_receiver = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/lib/sender-receiver.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var urlUtils = require_url();
|
||||
var BufferedSender = require_buffered_sender();
|
||||
var Polling = require_polling();
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:sender-receiver");
|
||||
}
|
||||
function SenderReceiver(transUrl, urlSuffix, senderFunc, Receiver, AjaxObject) {
|
||||
var pollUrl = urlUtils.addPath(transUrl, urlSuffix);
|
||||
debug(pollUrl);
|
||||
var self2 = this;
|
||||
BufferedSender.call(this, transUrl, senderFunc);
|
||||
this.poll = new Polling(Receiver, pollUrl, AjaxObject);
|
||||
this.poll.on("message", function(msg) {
|
||||
debug("poll message", msg);
|
||||
self2.emit("message", msg);
|
||||
});
|
||||
this.poll.once("close", function(code, reason) {
|
||||
debug("poll close", code, reason);
|
||||
self2.poll = null;
|
||||
self2.emit("close", code, reason);
|
||||
self2.close();
|
||||
});
|
||||
}
|
||||
inherits(SenderReceiver, BufferedSender);
|
||||
SenderReceiver.prototype.close = function() {
|
||||
BufferedSender.prototype.close.call(this);
|
||||
debug("close");
|
||||
this.removeAllListeners();
|
||||
if (this.poll) {
|
||||
this.poll.abort();
|
||||
this.poll = null;
|
||||
}
|
||||
};
|
||||
module.exports = SenderReceiver;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/lib/ajax-based.js
|
||||
var require_ajax_based = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/lib/ajax-based.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var urlUtils = require_url();
|
||||
var SenderReceiver = require_sender_receiver();
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:ajax-based");
|
||||
}
|
||||
function createAjaxSender(AjaxObject) {
|
||||
return function(url, payload, callback) {
|
||||
debug("create ajax sender", url, payload);
|
||||
var opt = {};
|
||||
if (typeof payload === "string") {
|
||||
opt.headers = { "Content-type": "text/plain" };
|
||||
}
|
||||
var ajaxUrl = urlUtils.addPath(url, "/xhr_send");
|
||||
var xo = new AjaxObject("POST", ajaxUrl, payload, opt);
|
||||
xo.once("finish", function(status) {
|
||||
debug("finish", status);
|
||||
xo = null;
|
||||
if (status !== 200 && status !== 204) {
|
||||
return callback(new Error("http status " + status));
|
||||
}
|
||||
callback();
|
||||
});
|
||||
return function() {
|
||||
debug("abort");
|
||||
xo.close();
|
||||
xo = null;
|
||||
var err = new Error("Aborted");
|
||||
err.code = 1e3;
|
||||
callback(err);
|
||||
};
|
||||
};
|
||||
}
|
||||
function AjaxBasedTransport(transUrl, urlSuffix, Receiver, AjaxObject) {
|
||||
SenderReceiver.call(this, transUrl, urlSuffix, createAjaxSender(AjaxObject), Receiver, AjaxObject);
|
||||
}
|
||||
inherits(AjaxBasedTransport, SenderReceiver);
|
||||
module.exports = AjaxBasedTransport;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/receiver/xhr.js
|
||||
var require_xhr = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/receiver/xhr.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var EventEmitter = require_emitter().EventEmitter;
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:receiver:xhr");
|
||||
}
|
||||
function XhrReceiver(url, AjaxObject) {
|
||||
debug(url);
|
||||
EventEmitter.call(this);
|
||||
var self2 = this;
|
||||
this.bufferPosition = 0;
|
||||
this.xo = new AjaxObject("POST", url, null);
|
||||
this.xo.on("chunk", this._chunkHandler.bind(this));
|
||||
this.xo.once("finish", function(status, text) {
|
||||
debug("finish", status, text);
|
||||
self2._chunkHandler(status, text);
|
||||
self2.xo = null;
|
||||
var reason = status === 200 ? "network" : "permanent";
|
||||
debug("close", reason);
|
||||
self2.emit("close", null, reason);
|
||||
self2._cleanup();
|
||||
});
|
||||
}
|
||||
inherits(XhrReceiver, EventEmitter);
|
||||
XhrReceiver.prototype._chunkHandler = function(status, text) {
|
||||
debug("_chunkHandler", status);
|
||||
if (status !== 200 || !text) {
|
||||
return;
|
||||
}
|
||||
for (var idx = -1; ; this.bufferPosition += idx + 1) {
|
||||
var buf = text.slice(this.bufferPosition);
|
||||
idx = buf.indexOf("\n");
|
||||
if (idx === -1) {
|
||||
break;
|
||||
}
|
||||
var msg = buf.slice(0, idx);
|
||||
if (msg) {
|
||||
debug("message", msg);
|
||||
this.emit("message", msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
XhrReceiver.prototype._cleanup = function() {
|
||||
debug("_cleanup");
|
||||
this.removeAllListeners();
|
||||
};
|
||||
XhrReceiver.prototype.abort = function() {
|
||||
debug("abort");
|
||||
if (this.xo) {
|
||||
this.xo.close();
|
||||
debug("close");
|
||||
this.emit("close", null, "user");
|
||||
this.xo = null;
|
||||
}
|
||||
this._cleanup();
|
||||
};
|
||||
module.exports = XhrReceiver;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/browser/abstract-xhr.js
|
||||
var require_abstract_xhr = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/browser/abstract-xhr.js"(exports, module) {
|
||||
"use strict";
|
||||
var EventEmitter = require_emitter().EventEmitter;
|
||||
var inherits = require_inherits_browser();
|
||||
var utils = require_event();
|
||||
var urlUtils = require_url();
|
||||
var XHR = window.XMLHttpRequest;
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:browser:xhr");
|
||||
}
|
||||
function AbstractXHRObject(method, url, payload, opts) {
|
||||
debug(method, url);
|
||||
var self2 = this;
|
||||
EventEmitter.call(this);
|
||||
setTimeout(function() {
|
||||
self2._start(method, url, payload, opts);
|
||||
}, 0);
|
||||
}
|
||||
inherits(AbstractXHRObject, EventEmitter);
|
||||
AbstractXHRObject.prototype._start = function(method, url, payload, opts) {
|
||||
var self2 = this;
|
||||
try {
|
||||
this.xhr = new XHR();
|
||||
} catch (x) {
|
||||
}
|
||||
if (!this.xhr) {
|
||||
debug("no xhr");
|
||||
this.emit("finish", 0, "no xhr support");
|
||||
this._cleanup();
|
||||
return;
|
||||
}
|
||||
url = urlUtils.addQuery(url, "t=" + +/* @__PURE__ */ new Date());
|
||||
this.unloadRef = utils.unloadAdd(function() {
|
||||
debug("unload cleanup");
|
||||
self2._cleanup(true);
|
||||
});
|
||||
try {
|
||||
this.xhr.open(method, url, true);
|
||||
if (this.timeout && "timeout" in this.xhr) {
|
||||
this.xhr.timeout = this.timeout;
|
||||
this.xhr.ontimeout = function() {
|
||||
debug("xhr timeout");
|
||||
self2.emit("finish", 0, "");
|
||||
self2._cleanup(false);
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
debug("exception", e);
|
||||
this.emit("finish", 0, "");
|
||||
this._cleanup(false);
|
||||
return;
|
||||
}
|
||||
if ((!opts || !opts.noCredentials) && AbstractXHRObject.supportsCORS) {
|
||||
debug("withCredentials");
|
||||
this.xhr.withCredentials = true;
|
||||
}
|
||||
if (opts && opts.headers) {
|
||||
for (var key in opts.headers) {
|
||||
this.xhr.setRequestHeader(key, opts.headers[key]);
|
||||
}
|
||||
}
|
||||
this.xhr.onreadystatechange = function() {
|
||||
if (self2.xhr) {
|
||||
var x = self2.xhr;
|
||||
var text, status;
|
||||
debug("readyState", x.readyState);
|
||||
switch (x.readyState) {
|
||||
case 3:
|
||||
try {
|
||||
status = x.status;
|
||||
text = x.responseText;
|
||||
} catch (e) {
|
||||
}
|
||||
debug("status", status);
|
||||
if (status === 1223) {
|
||||
status = 204;
|
||||
}
|
||||
if (status === 200 && text && text.length > 0) {
|
||||
debug("chunk");
|
||||
self2.emit("chunk", status, text);
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
status = x.status;
|
||||
debug("status", status);
|
||||
if (status === 1223) {
|
||||
status = 204;
|
||||
}
|
||||
if (status === 12005 || status === 12029) {
|
||||
status = 0;
|
||||
}
|
||||
debug("finish", status, x.responseText);
|
||||
self2.emit("finish", status, x.responseText);
|
||||
self2._cleanup(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
self2.xhr.send(payload);
|
||||
} catch (e) {
|
||||
self2.emit("finish", 0, "");
|
||||
self2._cleanup(false);
|
||||
}
|
||||
};
|
||||
AbstractXHRObject.prototype._cleanup = function(abort) {
|
||||
debug("cleanup");
|
||||
if (!this.xhr) {
|
||||
return;
|
||||
}
|
||||
this.removeAllListeners();
|
||||
utils.unloadDel(this.unloadRef);
|
||||
this.xhr.onreadystatechange = function() {
|
||||
};
|
||||
if (this.xhr.ontimeout) {
|
||||
this.xhr.ontimeout = null;
|
||||
}
|
||||
if (abort) {
|
||||
try {
|
||||
this.xhr.abort();
|
||||
} catch (x) {
|
||||
}
|
||||
}
|
||||
this.unloadRef = this.xhr = null;
|
||||
};
|
||||
AbstractXHRObject.prototype.close = function() {
|
||||
debug("close");
|
||||
this._cleanup(true);
|
||||
};
|
||||
AbstractXHRObject.enabled = !!XHR;
|
||||
var axo = ["Active"].concat("Object").join("X");
|
||||
if (!AbstractXHRObject.enabled && axo in window) {
|
||||
debug("overriding xmlhttprequest");
|
||||
XHR = function() {
|
||||
try {
|
||||
return new window[axo]("Microsoft.XMLHTTP");
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
AbstractXHRObject.enabled = !!new XHR();
|
||||
}
|
||||
var cors = false;
|
||||
try {
|
||||
cors = "withCredentials" in new XHR();
|
||||
} catch (ignored) {
|
||||
}
|
||||
AbstractXHRObject.supportsCORS = cors;
|
||||
module.exports = AbstractXHRObject;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/sender/xhr-cors.js
|
||||
var require_xhr_cors = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/sender/xhr-cors.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var XhrDriver = require_abstract_xhr();
|
||||
function XHRCorsObject(method, url, payload, opts) {
|
||||
XhrDriver.call(this, method, url, payload, opts);
|
||||
}
|
||||
inherits(XHRCorsObject, XhrDriver);
|
||||
XHRCorsObject.enabled = XhrDriver.enabled && XhrDriver.supportsCORS;
|
||||
module.exports = XHRCorsObject;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/sender/xhr-local.js
|
||||
var require_xhr_local = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/sender/xhr-local.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var XhrDriver = require_abstract_xhr();
|
||||
function XHRLocalObject(method, url, payload) {
|
||||
XhrDriver.call(this, method, url, payload, {
|
||||
noCredentials: true
|
||||
});
|
||||
}
|
||||
inherits(XHRLocalObject, XhrDriver);
|
||||
XHRLocalObject.enabled = XhrDriver.enabled;
|
||||
module.exports = XHRLocalObject;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/utils/browser.js
|
||||
var require_browser2 = __commonJS({
|
||||
"node_modules/sockjs-client/lib/utils/browser.js"(exports, module) {
|
||||
"use strict";
|
||||
module.exports = {
|
||||
isOpera: function() {
|
||||
return window.navigator && /opera/i.test(window.navigator.userAgent);
|
||||
},
|
||||
isKonqueror: function() {
|
||||
return window.navigator && /konqueror/i.test(window.navigator.userAgent);
|
||||
},
|
||||
hasDomain: function() {
|
||||
if (!window.document) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return !!window.document.domain;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/xhr-streaming.js
|
||||
var require_xhr_streaming = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/xhr-streaming.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var AjaxBasedTransport = require_ajax_based();
|
||||
var XhrReceiver = require_xhr();
|
||||
var XHRCorsObject = require_xhr_cors();
|
||||
var XHRLocalObject = require_xhr_local();
|
||||
var browser = require_browser2();
|
||||
function XhrStreamingTransport(transUrl) {
|
||||
if (!XHRLocalObject.enabled && !XHRCorsObject.enabled) {
|
||||
throw new Error("Transport created when disabled");
|
||||
}
|
||||
AjaxBasedTransport.call(this, transUrl, "/xhr_streaming", XhrReceiver, XHRCorsObject);
|
||||
}
|
||||
inherits(XhrStreamingTransport, AjaxBasedTransport);
|
||||
XhrStreamingTransport.enabled = function(info) {
|
||||
if (info.nullOrigin) {
|
||||
return false;
|
||||
}
|
||||
if (browser.isOpera()) {
|
||||
return false;
|
||||
}
|
||||
return XHRCorsObject.enabled;
|
||||
};
|
||||
XhrStreamingTransport.transportName = "xhr-streaming";
|
||||
XhrStreamingTransport.roundTrips = 2;
|
||||
XhrStreamingTransport.needBody = !!window.document;
|
||||
module.exports = XhrStreamingTransport;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/sender/xdr.js
|
||||
var require_xdr = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/sender/xdr.js"(exports, module) {
|
||||
"use strict";
|
||||
var EventEmitter = require_emitter().EventEmitter;
|
||||
var inherits = require_inherits_browser();
|
||||
var eventUtils = require_event();
|
||||
var browser = require_browser2();
|
||||
var urlUtils = require_url();
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:sender:xdr");
|
||||
}
|
||||
function XDRObject(method, url, payload) {
|
||||
debug(method, url);
|
||||
var self2 = this;
|
||||
EventEmitter.call(this);
|
||||
setTimeout(function() {
|
||||
self2._start(method, url, payload);
|
||||
}, 0);
|
||||
}
|
||||
inherits(XDRObject, EventEmitter);
|
||||
XDRObject.prototype._start = function(method, url, payload) {
|
||||
debug("_start");
|
||||
var self2 = this;
|
||||
var xdr = new window.XDomainRequest();
|
||||
url = urlUtils.addQuery(url, "t=" + +/* @__PURE__ */ new Date());
|
||||
xdr.onerror = function() {
|
||||
debug("onerror");
|
||||
self2._error();
|
||||
};
|
||||
xdr.ontimeout = function() {
|
||||
debug("ontimeout");
|
||||
self2._error();
|
||||
};
|
||||
xdr.onprogress = function() {
|
||||
debug("progress", xdr.responseText);
|
||||
self2.emit("chunk", 200, xdr.responseText);
|
||||
};
|
||||
xdr.onload = function() {
|
||||
debug("load");
|
||||
self2.emit("finish", 200, xdr.responseText);
|
||||
self2._cleanup(false);
|
||||
};
|
||||
this.xdr = xdr;
|
||||
this.unloadRef = eventUtils.unloadAdd(function() {
|
||||
self2._cleanup(true);
|
||||
});
|
||||
try {
|
||||
this.xdr.open(method, url);
|
||||
if (this.timeout) {
|
||||
this.xdr.timeout = this.timeout;
|
||||
}
|
||||
this.xdr.send(payload);
|
||||
} catch (x) {
|
||||
this._error();
|
||||
}
|
||||
};
|
||||
XDRObject.prototype._error = function() {
|
||||
this.emit("finish", 0, "");
|
||||
this._cleanup(false);
|
||||
};
|
||||
XDRObject.prototype._cleanup = function(abort) {
|
||||
debug("cleanup", abort);
|
||||
if (!this.xdr) {
|
||||
return;
|
||||
}
|
||||
this.removeAllListeners();
|
||||
eventUtils.unloadDel(this.unloadRef);
|
||||
this.xdr.ontimeout = this.xdr.onerror = this.xdr.onprogress = this.xdr.onload = null;
|
||||
if (abort) {
|
||||
try {
|
||||
this.xdr.abort();
|
||||
} catch (x) {
|
||||
}
|
||||
}
|
||||
this.unloadRef = this.xdr = null;
|
||||
};
|
||||
XDRObject.prototype.close = function() {
|
||||
debug("close");
|
||||
this._cleanup(true);
|
||||
};
|
||||
XDRObject.enabled = !!(window.XDomainRequest && browser.hasDomain());
|
||||
module.exports = XDRObject;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/xdr-streaming.js
|
||||
var require_xdr_streaming = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/xdr-streaming.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var AjaxBasedTransport = require_ajax_based();
|
||||
var XhrReceiver = require_xhr();
|
||||
var XDRObject = require_xdr();
|
||||
function XdrStreamingTransport(transUrl) {
|
||||
if (!XDRObject.enabled) {
|
||||
throw new Error("Transport created when disabled");
|
||||
}
|
||||
AjaxBasedTransport.call(this, transUrl, "/xhr_streaming", XhrReceiver, XDRObject);
|
||||
}
|
||||
inherits(XdrStreamingTransport, AjaxBasedTransport);
|
||||
XdrStreamingTransport.enabled = function(info) {
|
||||
if (info.cookie_needed || info.nullOrigin) {
|
||||
return false;
|
||||
}
|
||||
return XDRObject.enabled && info.sameScheme;
|
||||
};
|
||||
XdrStreamingTransport.transportName = "xdr-streaming";
|
||||
XdrStreamingTransport.roundTrips = 2;
|
||||
module.exports = XdrStreamingTransport;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/browser/eventsource.js
|
||||
var require_eventsource = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/browser/eventsource.js"(exports, module) {
|
||||
module.exports = window.EventSource;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/receiver/eventsource.js
|
||||
var require_eventsource2 = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/receiver/eventsource.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var EventEmitter = require_emitter().EventEmitter;
|
||||
var EventSourceDriver = require_eventsource();
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:receiver:eventsource");
|
||||
}
|
||||
function EventSourceReceiver(url) {
|
||||
debug(url);
|
||||
EventEmitter.call(this);
|
||||
var self2 = this;
|
||||
var es = this.es = new EventSourceDriver(url);
|
||||
es.onmessage = function(e) {
|
||||
debug("message", e.data);
|
||||
self2.emit("message", decodeURI(e.data));
|
||||
};
|
||||
es.onerror = function(e) {
|
||||
debug("error", es.readyState, e);
|
||||
var reason = es.readyState !== 2 ? "network" : "permanent";
|
||||
self2._cleanup();
|
||||
self2._close(reason);
|
||||
};
|
||||
}
|
||||
inherits(EventSourceReceiver, EventEmitter);
|
||||
EventSourceReceiver.prototype.abort = function() {
|
||||
debug("abort");
|
||||
this._cleanup();
|
||||
this._close("user");
|
||||
};
|
||||
EventSourceReceiver.prototype._cleanup = function() {
|
||||
debug("cleanup");
|
||||
var es = this.es;
|
||||
if (es) {
|
||||
es.onmessage = es.onerror = null;
|
||||
es.close();
|
||||
this.es = null;
|
||||
}
|
||||
};
|
||||
EventSourceReceiver.prototype._close = function(reason) {
|
||||
debug("close", reason);
|
||||
var self2 = this;
|
||||
setTimeout(function() {
|
||||
self2.emit("close", null, reason);
|
||||
self2.removeAllListeners();
|
||||
}, 200);
|
||||
};
|
||||
module.exports = EventSourceReceiver;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/eventsource.js
|
||||
var require_eventsource3 = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/eventsource.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var AjaxBasedTransport = require_ajax_based();
|
||||
var EventSourceReceiver = require_eventsource2();
|
||||
var XHRCorsObject = require_xhr_cors();
|
||||
var EventSourceDriver = require_eventsource();
|
||||
function EventSourceTransport(transUrl) {
|
||||
if (!EventSourceTransport.enabled()) {
|
||||
throw new Error("Transport created when disabled");
|
||||
}
|
||||
AjaxBasedTransport.call(this, transUrl, "/eventsource", EventSourceReceiver, XHRCorsObject);
|
||||
}
|
||||
inherits(EventSourceTransport, AjaxBasedTransport);
|
||||
EventSourceTransport.enabled = function() {
|
||||
return !!EventSourceDriver;
|
||||
};
|
||||
EventSourceTransport.transportName = "eventsource";
|
||||
EventSourceTransport.roundTrips = 2;
|
||||
module.exports = EventSourceTransport;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/version.js
|
||||
var require_version = __commonJS({
|
||||
"node_modules/sockjs-client/lib/version.js"(exports, module) {
|
||||
module.exports = "1.6.1";
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/utils/iframe.js
|
||||
var require_iframe = __commonJS({
|
||||
"node_modules/sockjs-client/lib/utils/iframe.js"(exports, module) {
|
||||
"use strict";
|
||||
var eventUtils = require_event();
|
||||
var browser = require_browser2();
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:utils:iframe");
|
||||
}
|
||||
module.exports = {
|
||||
WPrefix: "_jp",
|
||||
currentWindowId: null,
|
||||
polluteGlobalNamespace: function() {
|
||||
if (!(module.exports.WPrefix in window)) {
|
||||
window[module.exports.WPrefix] = {};
|
||||
}
|
||||
},
|
||||
postMessage: function(type, data) {
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage(JSON.stringify({
|
||||
windowId: module.exports.currentWindowId,
|
||||
type,
|
||||
data: data || ""
|
||||
}), "*");
|
||||
} else {
|
||||
debug("Cannot postMessage, no parent window.", type, data);
|
||||
}
|
||||
},
|
||||
createIframe: function(iframeUrl, errorCallback) {
|
||||
var iframe = window.document.createElement("iframe");
|
||||
var tref, unloadRef;
|
||||
var unattach = function() {
|
||||
debug("unattach");
|
||||
clearTimeout(tref);
|
||||
try {
|
||||
iframe.onload = null;
|
||||
} catch (x) {
|
||||
}
|
||||
iframe.onerror = null;
|
||||
};
|
||||
var cleanup = function() {
|
||||
debug("cleanup");
|
||||
if (iframe) {
|
||||
unattach();
|
||||
setTimeout(function() {
|
||||
if (iframe) {
|
||||
iframe.parentNode.removeChild(iframe);
|
||||
}
|
||||
iframe = null;
|
||||
}, 0);
|
||||
eventUtils.unloadDel(unloadRef);
|
||||
}
|
||||
};
|
||||
var onerror = function(err) {
|
||||
debug("onerror", err);
|
||||
if (iframe) {
|
||||
cleanup();
|
||||
errorCallback(err);
|
||||
}
|
||||
};
|
||||
var post = function(msg, origin) {
|
||||
debug("post", msg, origin);
|
||||
setTimeout(function() {
|
||||
try {
|
||||
if (iframe && iframe.contentWindow) {
|
||||
iframe.contentWindow.postMessage(msg, origin);
|
||||
}
|
||||
} catch (x) {
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
iframe.src = iframeUrl;
|
||||
iframe.style.display = "none";
|
||||
iframe.style.position = "absolute";
|
||||
iframe.onerror = function() {
|
||||
onerror("onerror");
|
||||
};
|
||||
iframe.onload = function() {
|
||||
debug("onload");
|
||||
clearTimeout(tref);
|
||||
tref = setTimeout(function() {
|
||||
onerror("onload timeout");
|
||||
}, 2e3);
|
||||
};
|
||||
window.document.body.appendChild(iframe);
|
||||
tref = setTimeout(function() {
|
||||
onerror("timeout");
|
||||
}, 15e3);
|
||||
unloadRef = eventUtils.unloadAdd(cleanup);
|
||||
return {
|
||||
post,
|
||||
cleanup,
|
||||
loaded: unattach
|
||||
};
|
||||
},
|
||||
createHtmlfile: function(iframeUrl, errorCallback) {
|
||||
var axo = ["Active"].concat("Object").join("X");
|
||||
var doc = new window[axo]("htmlfile");
|
||||
var tref, unloadRef;
|
||||
var iframe;
|
||||
var unattach = function() {
|
||||
clearTimeout(tref);
|
||||
iframe.onerror = null;
|
||||
};
|
||||
var cleanup = function() {
|
||||
if (doc) {
|
||||
unattach();
|
||||
eventUtils.unloadDel(unloadRef);
|
||||
iframe.parentNode.removeChild(iframe);
|
||||
iframe = doc = null;
|
||||
CollectGarbage();
|
||||
}
|
||||
};
|
||||
var onerror = function(r) {
|
||||
debug("onerror", r);
|
||||
if (doc) {
|
||||
cleanup();
|
||||
errorCallback(r);
|
||||
}
|
||||
};
|
||||
var post = function(msg, origin) {
|
||||
try {
|
||||
setTimeout(function() {
|
||||
if (iframe && iframe.contentWindow) {
|
||||
iframe.contentWindow.postMessage(msg, origin);
|
||||
}
|
||||
}, 0);
|
||||
} catch (x) {
|
||||
}
|
||||
};
|
||||
doc.open();
|
||||
doc.write('<html><script>document.domain="' + window.document.domain + '";<\/script></html>');
|
||||
doc.close();
|
||||
doc.parentWindow[module.exports.WPrefix] = window[module.exports.WPrefix];
|
||||
var c = doc.createElement("div");
|
||||
doc.body.appendChild(c);
|
||||
iframe = doc.createElement("iframe");
|
||||
c.appendChild(iframe);
|
||||
iframe.src = iframeUrl;
|
||||
iframe.onerror = function() {
|
||||
onerror("onerror");
|
||||
};
|
||||
tref = setTimeout(function() {
|
||||
onerror("timeout");
|
||||
}, 15e3);
|
||||
unloadRef = eventUtils.unloadAdd(cleanup);
|
||||
return {
|
||||
post,
|
||||
cleanup,
|
||||
loaded: unattach
|
||||
};
|
||||
}
|
||||
};
|
||||
module.exports.iframeEnabled = false;
|
||||
if (window.document) {
|
||||
module.exports.iframeEnabled = (typeof window.postMessage === "function" || typeof window.postMessage === "object") && !browser.isKonqueror();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/iframe.js
|
||||
var require_iframe2 = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/iframe.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var EventEmitter = require_emitter().EventEmitter;
|
||||
var version = require_version();
|
||||
var urlUtils = require_url();
|
||||
var iframeUtils = require_iframe();
|
||||
var eventUtils = require_event();
|
||||
var random = require_random();
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:transport:iframe");
|
||||
}
|
||||
function IframeTransport(transport, transUrl, baseUrl) {
|
||||
if (!IframeTransport.enabled()) {
|
||||
throw new Error("Transport created when disabled");
|
||||
}
|
||||
EventEmitter.call(this);
|
||||
var self2 = this;
|
||||
this.origin = urlUtils.getOrigin(baseUrl);
|
||||
this.baseUrl = baseUrl;
|
||||
this.transUrl = transUrl;
|
||||
this.transport = transport;
|
||||
this.windowId = random.string(8);
|
||||
var iframeUrl = urlUtils.addPath(baseUrl, "/iframe.html") + "#" + this.windowId;
|
||||
debug(transport, transUrl, iframeUrl);
|
||||
this.iframeObj = iframeUtils.createIframe(iframeUrl, function(r) {
|
||||
debug("err callback");
|
||||
self2.emit("close", 1006, "Unable to load an iframe (" + r + ")");
|
||||
self2.close();
|
||||
});
|
||||
this.onmessageCallback = this._message.bind(this);
|
||||
eventUtils.attachEvent("message", this.onmessageCallback);
|
||||
}
|
||||
inherits(IframeTransport, EventEmitter);
|
||||
IframeTransport.prototype.close = function() {
|
||||
debug("close");
|
||||
this.removeAllListeners();
|
||||
if (this.iframeObj) {
|
||||
eventUtils.detachEvent("message", this.onmessageCallback);
|
||||
try {
|
||||
this.postMessage("c");
|
||||
} catch (x) {
|
||||
}
|
||||
this.iframeObj.cleanup();
|
||||
this.iframeObj = null;
|
||||
this.onmessageCallback = this.iframeObj = null;
|
||||
}
|
||||
};
|
||||
IframeTransport.prototype._message = function(e) {
|
||||
debug("message", e.data);
|
||||
if (!urlUtils.isOriginEqual(e.origin, this.origin)) {
|
||||
debug("not same origin", e.origin, this.origin);
|
||||
return;
|
||||
}
|
||||
var iframeMessage;
|
||||
try {
|
||||
iframeMessage = JSON.parse(e.data);
|
||||
} catch (ignored) {
|
||||
debug("bad json", e.data);
|
||||
return;
|
||||
}
|
||||
if (iframeMessage.windowId !== this.windowId) {
|
||||
debug("mismatched window id", iframeMessage.windowId, this.windowId);
|
||||
return;
|
||||
}
|
||||
switch (iframeMessage.type) {
|
||||
case "s":
|
||||
this.iframeObj.loaded();
|
||||
this.postMessage("s", JSON.stringify([
|
||||
version,
|
||||
this.transport,
|
||||
this.transUrl,
|
||||
this.baseUrl
|
||||
]));
|
||||
break;
|
||||
case "t":
|
||||
this.emit("message", iframeMessage.data);
|
||||
break;
|
||||
case "c":
|
||||
var cdata;
|
||||
try {
|
||||
cdata = JSON.parse(iframeMessage.data);
|
||||
} catch (ignored) {
|
||||
debug("bad json", iframeMessage.data);
|
||||
return;
|
||||
}
|
||||
this.emit("close", cdata[0], cdata[1]);
|
||||
this.close();
|
||||
break;
|
||||
}
|
||||
};
|
||||
IframeTransport.prototype.postMessage = function(type, data) {
|
||||
debug("postMessage", type, data);
|
||||
this.iframeObj.post(JSON.stringify({
|
||||
windowId: this.windowId,
|
||||
type,
|
||||
data: data || ""
|
||||
}), this.origin);
|
||||
};
|
||||
IframeTransport.prototype.send = function(message) {
|
||||
debug("send", message);
|
||||
this.postMessage("m", message);
|
||||
};
|
||||
IframeTransport.enabled = function() {
|
||||
return iframeUtils.iframeEnabled;
|
||||
};
|
||||
IframeTransport.transportName = "iframe";
|
||||
IframeTransport.roundTrips = 2;
|
||||
module.exports = IframeTransport;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/utils/object.js
|
||||
var require_object = __commonJS({
|
||||
"node_modules/sockjs-client/lib/utils/object.js"(exports, module) {
|
||||
"use strict";
|
||||
module.exports = {
|
||||
isObject: function(obj) {
|
||||
var type = typeof obj;
|
||||
return type === "function" || type === "object" && !!obj;
|
||||
},
|
||||
extend: function(obj) {
|
||||
if (!this.isObject(obj)) {
|
||||
return obj;
|
||||
}
|
||||
var source, prop;
|
||||
for (var i = 1, length = arguments.length; i < length; i++) {
|
||||
source = arguments[i];
|
||||
for (prop in source) {
|
||||
if (Object.prototype.hasOwnProperty.call(source, prop)) {
|
||||
obj[prop] = source[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/lib/iframe-wrap.js
|
||||
var require_iframe_wrap = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/lib/iframe-wrap.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var IframeTransport = require_iframe2();
|
||||
var objectUtils = require_object();
|
||||
module.exports = function(transport) {
|
||||
function IframeWrapTransport(transUrl, baseUrl) {
|
||||
IframeTransport.call(this, transport.transportName, transUrl, baseUrl);
|
||||
}
|
||||
inherits(IframeWrapTransport, IframeTransport);
|
||||
IframeWrapTransport.enabled = function(url, info) {
|
||||
if (!window.document) {
|
||||
return false;
|
||||
}
|
||||
var iframeInfo = objectUtils.extend({}, info);
|
||||
iframeInfo.sameOrigin = true;
|
||||
return transport.enabled(iframeInfo) && IframeTransport.enabled();
|
||||
};
|
||||
IframeWrapTransport.transportName = "iframe-" + transport.transportName;
|
||||
IframeWrapTransport.needBody = true;
|
||||
IframeWrapTransport.roundTrips = IframeTransport.roundTrips + transport.roundTrips - 1;
|
||||
IframeWrapTransport.facadeTransport = transport;
|
||||
return IframeWrapTransport;
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/receiver/htmlfile.js
|
||||
var require_htmlfile = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/receiver/htmlfile.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var iframeUtils = require_iframe();
|
||||
var urlUtils = require_url();
|
||||
var EventEmitter = require_emitter().EventEmitter;
|
||||
var random = require_random();
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:receiver:htmlfile");
|
||||
}
|
||||
function HtmlfileReceiver(url) {
|
||||
debug(url);
|
||||
EventEmitter.call(this);
|
||||
var self2 = this;
|
||||
iframeUtils.polluteGlobalNamespace();
|
||||
this.id = "a" + random.string(6);
|
||||
url = urlUtils.addQuery(url, "c=" + decodeURIComponent(iframeUtils.WPrefix + "." + this.id));
|
||||
debug("using htmlfile", HtmlfileReceiver.htmlfileEnabled);
|
||||
var constructFunc = HtmlfileReceiver.htmlfileEnabled ? iframeUtils.createHtmlfile : iframeUtils.createIframe;
|
||||
window[iframeUtils.WPrefix][this.id] = {
|
||||
start: function() {
|
||||
debug("start");
|
||||
self2.iframeObj.loaded();
|
||||
},
|
||||
message: function(data) {
|
||||
debug("message", data);
|
||||
self2.emit("message", data);
|
||||
},
|
||||
stop: function() {
|
||||
debug("stop");
|
||||
self2._cleanup();
|
||||
self2._close("network");
|
||||
}
|
||||
};
|
||||
this.iframeObj = constructFunc(url, function() {
|
||||
debug("callback");
|
||||
self2._cleanup();
|
||||
self2._close("permanent");
|
||||
});
|
||||
}
|
||||
inherits(HtmlfileReceiver, EventEmitter);
|
||||
HtmlfileReceiver.prototype.abort = function() {
|
||||
debug("abort");
|
||||
this._cleanup();
|
||||
this._close("user");
|
||||
};
|
||||
HtmlfileReceiver.prototype._cleanup = function() {
|
||||
debug("_cleanup");
|
||||
if (this.iframeObj) {
|
||||
this.iframeObj.cleanup();
|
||||
this.iframeObj = null;
|
||||
}
|
||||
delete window[iframeUtils.WPrefix][this.id];
|
||||
};
|
||||
HtmlfileReceiver.prototype._close = function(reason) {
|
||||
debug("_close", reason);
|
||||
this.emit("close", null, reason);
|
||||
this.removeAllListeners();
|
||||
};
|
||||
HtmlfileReceiver.htmlfileEnabled = false;
|
||||
var axo = ["Active"].concat("Object").join("X");
|
||||
if (axo in window) {
|
||||
try {
|
||||
HtmlfileReceiver.htmlfileEnabled = !!new window[axo]("htmlfile");
|
||||
} catch (x) {
|
||||
}
|
||||
}
|
||||
HtmlfileReceiver.enabled = HtmlfileReceiver.htmlfileEnabled || iframeUtils.iframeEnabled;
|
||||
module.exports = HtmlfileReceiver;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/htmlfile.js
|
||||
var require_htmlfile2 = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/htmlfile.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var HtmlfileReceiver = require_htmlfile();
|
||||
var XHRLocalObject = require_xhr_local();
|
||||
var AjaxBasedTransport = require_ajax_based();
|
||||
function HtmlFileTransport(transUrl) {
|
||||
if (!HtmlfileReceiver.enabled) {
|
||||
throw new Error("Transport created when disabled");
|
||||
}
|
||||
AjaxBasedTransport.call(this, transUrl, "/htmlfile", HtmlfileReceiver, XHRLocalObject);
|
||||
}
|
||||
inherits(HtmlFileTransport, AjaxBasedTransport);
|
||||
HtmlFileTransport.enabled = function(info) {
|
||||
return HtmlfileReceiver.enabled && info.sameOrigin;
|
||||
};
|
||||
HtmlFileTransport.transportName = "htmlfile";
|
||||
HtmlFileTransport.roundTrips = 2;
|
||||
module.exports = HtmlFileTransport;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/xhr-polling.js
|
||||
var require_xhr_polling = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/xhr-polling.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var AjaxBasedTransport = require_ajax_based();
|
||||
var XhrReceiver = require_xhr();
|
||||
var XHRCorsObject = require_xhr_cors();
|
||||
var XHRLocalObject = require_xhr_local();
|
||||
function XhrPollingTransport(transUrl) {
|
||||
if (!XHRLocalObject.enabled && !XHRCorsObject.enabled) {
|
||||
throw new Error("Transport created when disabled");
|
||||
}
|
||||
AjaxBasedTransport.call(this, transUrl, "/xhr", XhrReceiver, XHRCorsObject);
|
||||
}
|
||||
inherits(XhrPollingTransport, AjaxBasedTransport);
|
||||
XhrPollingTransport.enabled = function(info) {
|
||||
if (info.nullOrigin) {
|
||||
return false;
|
||||
}
|
||||
if (XHRLocalObject.enabled && info.sameOrigin) {
|
||||
return true;
|
||||
}
|
||||
return XHRCorsObject.enabled;
|
||||
};
|
||||
XhrPollingTransport.transportName = "xhr-polling";
|
||||
XhrPollingTransport.roundTrips = 2;
|
||||
module.exports = XhrPollingTransport;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/xdr-polling.js
|
||||
var require_xdr_polling = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/xdr-polling.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var AjaxBasedTransport = require_ajax_based();
|
||||
var XdrStreamingTransport = require_xdr_streaming();
|
||||
var XhrReceiver = require_xhr();
|
||||
var XDRObject = require_xdr();
|
||||
function XdrPollingTransport(transUrl) {
|
||||
if (!XDRObject.enabled) {
|
||||
throw new Error("Transport created when disabled");
|
||||
}
|
||||
AjaxBasedTransport.call(this, transUrl, "/xhr", XhrReceiver, XDRObject);
|
||||
}
|
||||
inherits(XdrPollingTransport, AjaxBasedTransport);
|
||||
XdrPollingTransport.enabled = XdrStreamingTransport.enabled;
|
||||
XdrPollingTransport.transportName = "xdr-polling";
|
||||
XdrPollingTransport.roundTrips = 2;
|
||||
module.exports = XdrPollingTransport;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/receiver/jsonp.js
|
||||
var require_jsonp = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/receiver/jsonp.js"(exports, module) {
|
||||
"use strict";
|
||||
var utils = require_iframe();
|
||||
var random = require_random();
|
||||
var browser = require_browser2();
|
||||
var urlUtils = require_url();
|
||||
var inherits = require_inherits_browser();
|
||||
var EventEmitter = require_emitter().EventEmitter;
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:receiver:jsonp");
|
||||
}
|
||||
function JsonpReceiver(url) {
|
||||
debug(url);
|
||||
var self2 = this;
|
||||
EventEmitter.call(this);
|
||||
utils.polluteGlobalNamespace();
|
||||
this.id = "a" + random.string(6);
|
||||
var urlWithId = urlUtils.addQuery(url, "c=" + encodeURIComponent(utils.WPrefix + "." + this.id));
|
||||
window[utils.WPrefix][this.id] = this._callback.bind(this);
|
||||
this._createScript(urlWithId);
|
||||
this.timeoutId = setTimeout(function() {
|
||||
debug("timeout");
|
||||
self2._abort(new Error("JSONP script loaded abnormally (timeout)"));
|
||||
}, JsonpReceiver.timeout);
|
||||
}
|
||||
inherits(JsonpReceiver, EventEmitter);
|
||||
JsonpReceiver.prototype.abort = function() {
|
||||
debug("abort");
|
||||
if (window[utils.WPrefix][this.id]) {
|
||||
var err = new Error("JSONP user aborted read");
|
||||
err.code = 1e3;
|
||||
this._abort(err);
|
||||
}
|
||||
};
|
||||
JsonpReceiver.timeout = 35e3;
|
||||
JsonpReceiver.scriptErrorTimeout = 1e3;
|
||||
JsonpReceiver.prototype._callback = function(data) {
|
||||
debug("_callback", data);
|
||||
this._cleanup();
|
||||
if (this.aborting) {
|
||||
return;
|
||||
}
|
||||
if (data) {
|
||||
debug("message", data);
|
||||
this.emit("message", data);
|
||||
}
|
||||
this.emit("close", null, "network");
|
||||
this.removeAllListeners();
|
||||
};
|
||||
JsonpReceiver.prototype._abort = function(err) {
|
||||
debug("_abort", err);
|
||||
this._cleanup();
|
||||
this.aborting = true;
|
||||
this.emit("close", err.code, err.message);
|
||||
this.removeAllListeners();
|
||||
};
|
||||
JsonpReceiver.prototype._cleanup = function() {
|
||||
debug("_cleanup");
|
||||
clearTimeout(this.timeoutId);
|
||||
if (this.script2) {
|
||||
this.script2.parentNode.removeChild(this.script2);
|
||||
this.script2 = null;
|
||||
}
|
||||
if (this.script) {
|
||||
var script = this.script;
|
||||
script.parentNode.removeChild(script);
|
||||
script.onreadystatechange = script.onerror = script.onload = script.onclick = null;
|
||||
this.script = null;
|
||||
}
|
||||
delete window[utils.WPrefix][this.id];
|
||||
};
|
||||
JsonpReceiver.prototype._scriptError = function() {
|
||||
debug("_scriptError");
|
||||
var self2 = this;
|
||||
if (this.errorTimer) {
|
||||
return;
|
||||
}
|
||||
this.errorTimer = setTimeout(function() {
|
||||
if (!self2.loadedOkay) {
|
||||
self2._abort(new Error("JSONP script loaded abnormally (onerror)"));
|
||||
}
|
||||
}, JsonpReceiver.scriptErrorTimeout);
|
||||
};
|
||||
JsonpReceiver.prototype._createScript = function(url) {
|
||||
debug("_createScript", url);
|
||||
var self2 = this;
|
||||
var script = this.script = window.document.createElement("script");
|
||||
var script2;
|
||||
script.id = "a" + random.string(8);
|
||||
script.src = url;
|
||||
script.type = "text/javascript";
|
||||
script.charset = "UTF-8";
|
||||
script.onerror = this._scriptError.bind(this);
|
||||
script.onload = function() {
|
||||
debug("onload");
|
||||
self2._abort(new Error("JSONP script loaded abnormally (onload)"));
|
||||
};
|
||||
script.onreadystatechange = function() {
|
||||
debug("onreadystatechange", script.readyState);
|
||||
if (/loaded|closed/.test(script.readyState)) {
|
||||
if (script && script.htmlFor && script.onclick) {
|
||||
self2.loadedOkay = true;
|
||||
try {
|
||||
script.onclick();
|
||||
} catch (x) {
|
||||
}
|
||||
}
|
||||
if (script) {
|
||||
self2._abort(new Error("JSONP script loaded abnormally (onreadystatechange)"));
|
||||
}
|
||||
}
|
||||
};
|
||||
if (typeof script.async === "undefined" && window.document.attachEvent) {
|
||||
if (!browser.isOpera()) {
|
||||
try {
|
||||
script.htmlFor = script.id;
|
||||
script.event = "onclick";
|
||||
} catch (x) {
|
||||
}
|
||||
script.async = true;
|
||||
} else {
|
||||
script2 = this.script2 = window.document.createElement("script");
|
||||
script2.text = "try{var a = document.getElementById('" + script.id + "'); if(a)a.onerror();}catch(x){};";
|
||||
script.async = script2.async = false;
|
||||
}
|
||||
}
|
||||
if (typeof script.async !== "undefined") {
|
||||
script.async = true;
|
||||
}
|
||||
var head = window.document.getElementsByTagName("head")[0];
|
||||
head.insertBefore(script, head.firstChild);
|
||||
if (script2) {
|
||||
head.insertBefore(script2, head.firstChild);
|
||||
}
|
||||
};
|
||||
module.exports = JsonpReceiver;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/sender/jsonp.js
|
||||
var require_jsonp2 = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/sender/jsonp.js"(exports, module) {
|
||||
"use strict";
|
||||
var random = require_random();
|
||||
var urlUtils = require_url();
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:sender:jsonp");
|
||||
}
|
||||
var form;
|
||||
var area;
|
||||
function createIframe(id) {
|
||||
debug("createIframe", id);
|
||||
try {
|
||||
return window.document.createElement('<iframe name="' + id + '">');
|
||||
} catch (x) {
|
||||
var iframe = window.document.createElement("iframe");
|
||||
iframe.name = id;
|
||||
return iframe;
|
||||
}
|
||||
}
|
||||
function createForm() {
|
||||
debug("createForm");
|
||||
form = window.document.createElement("form");
|
||||
form.style.display = "none";
|
||||
form.style.position = "absolute";
|
||||
form.method = "POST";
|
||||
form.enctype = "application/x-www-form-urlencoded";
|
||||
form.acceptCharset = "UTF-8";
|
||||
area = window.document.createElement("textarea");
|
||||
area.name = "d";
|
||||
form.appendChild(area);
|
||||
window.document.body.appendChild(form);
|
||||
}
|
||||
module.exports = function(url, payload, callback) {
|
||||
debug(url, payload);
|
||||
if (!form) {
|
||||
createForm();
|
||||
}
|
||||
var id = "a" + random.string(8);
|
||||
form.target = id;
|
||||
form.action = urlUtils.addQuery(urlUtils.addPath(url, "/jsonp_send"), "i=" + id);
|
||||
var iframe = createIframe(id);
|
||||
iframe.id = id;
|
||||
iframe.style.display = "none";
|
||||
form.appendChild(iframe);
|
||||
try {
|
||||
area.value = payload;
|
||||
} catch (e) {
|
||||
}
|
||||
form.submit();
|
||||
var completed = function(err) {
|
||||
debug("completed", id, err);
|
||||
if (!iframe.onerror) {
|
||||
return;
|
||||
}
|
||||
iframe.onreadystatechange = iframe.onerror = iframe.onload = null;
|
||||
setTimeout(function() {
|
||||
debug("cleaning up", id);
|
||||
iframe.parentNode.removeChild(iframe);
|
||||
iframe = null;
|
||||
}, 500);
|
||||
area.value = "";
|
||||
callback(err);
|
||||
};
|
||||
iframe.onerror = function() {
|
||||
debug("onerror", id);
|
||||
completed();
|
||||
};
|
||||
iframe.onload = function() {
|
||||
debug("onload", id);
|
||||
completed();
|
||||
};
|
||||
iframe.onreadystatechange = function(e) {
|
||||
debug("onreadystatechange", id, iframe.readyState, e);
|
||||
if (iframe.readyState === "complete") {
|
||||
completed();
|
||||
}
|
||||
};
|
||||
return function() {
|
||||
debug("aborted", id);
|
||||
completed(new Error("Aborted"));
|
||||
};
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/jsonp-polling.js
|
||||
var require_jsonp_polling = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/jsonp-polling.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var SenderReceiver = require_sender_receiver();
|
||||
var JsonpReceiver = require_jsonp();
|
||||
var jsonpSender = require_jsonp2();
|
||||
function JsonPTransport(transUrl) {
|
||||
if (!JsonPTransport.enabled()) {
|
||||
throw new Error("Transport created when disabled");
|
||||
}
|
||||
SenderReceiver.call(this, transUrl, "/jsonp", jsonpSender, JsonpReceiver);
|
||||
}
|
||||
inherits(JsonPTransport, SenderReceiver);
|
||||
JsonPTransport.enabled = function() {
|
||||
return !!window.document;
|
||||
};
|
||||
JsonPTransport.transportName = "jsonp-polling";
|
||||
JsonPTransport.roundTrips = 1;
|
||||
JsonPTransport.needBody = true;
|
||||
module.exports = JsonPTransport;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport-list.js
|
||||
var require_transport_list = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport-list.js"(exports, module) {
|
||||
"use strict";
|
||||
module.exports = [
|
||||
// streaming transports
|
||||
require_websocket2(),
|
||||
require_xhr_streaming(),
|
||||
require_xdr_streaming(),
|
||||
require_eventsource3(),
|
||||
require_iframe_wrap()(require_eventsource3()),
|
||||
require_htmlfile2(),
|
||||
require_iframe_wrap()(require_htmlfile2()),
|
||||
require_xhr_polling(),
|
||||
require_xdr_polling(),
|
||||
require_iframe_wrap()(require_xhr_polling()),
|
||||
require_jsonp_polling()
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/shims.js
|
||||
var require_shims = __commonJS({
|
||||
"node_modules/sockjs-client/lib/shims.js"() {
|
||||
"use strict";
|
||||
var ArrayPrototype = Array.prototype;
|
||||
var ObjectPrototype = Object.prototype;
|
||||
var FunctionPrototype = Function.prototype;
|
||||
var StringPrototype = String.prototype;
|
||||
var array_slice = ArrayPrototype.slice;
|
||||
var _toString = ObjectPrototype.toString;
|
||||
var isFunction = function(val) {
|
||||
return ObjectPrototype.toString.call(val) === "[object Function]";
|
||||
};
|
||||
var isArray = function isArray2(obj) {
|
||||
return _toString.call(obj) === "[object Array]";
|
||||
};
|
||||
var isString = function isString2(obj) {
|
||||
return _toString.call(obj) === "[object String]";
|
||||
};
|
||||
var supportsDescriptors = Object.defineProperty && function() {
|
||||
try {
|
||||
Object.defineProperty({}, "x", {});
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}();
|
||||
var defineProperty;
|
||||
if (supportsDescriptors) {
|
||||
defineProperty = function(object, name, method, forceAssign) {
|
||||
if (!forceAssign && name in object) {
|
||||
return;
|
||||
}
|
||||
Object.defineProperty(object, name, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: method
|
||||
});
|
||||
};
|
||||
} else {
|
||||
defineProperty = function(object, name, method, forceAssign) {
|
||||
if (!forceAssign && name in object) {
|
||||
return;
|
||||
}
|
||||
object[name] = method;
|
||||
};
|
||||
}
|
||||
var defineProperties = function(object, map, forceAssign) {
|
||||
for (var name in map) {
|
||||
if (ObjectPrototype.hasOwnProperty.call(map, name)) {
|
||||
defineProperty(object, name, map[name], forceAssign);
|
||||
}
|
||||
}
|
||||
};
|
||||
var toObject = function(o) {
|
||||
if (o == null) {
|
||||
throw new TypeError("can't convert " + o + " to object");
|
||||
}
|
||||
return Object(o);
|
||||
};
|
||||
function toInteger(num) {
|
||||
var n = +num;
|
||||
if (n !== n) {
|
||||
n = 0;
|
||||
} else if (n !== 0 && n !== 1 / 0 && n !== -(1 / 0)) {
|
||||
n = (n > 0 || -1) * Math.floor(Math.abs(n));
|
||||
}
|
||||
return n;
|
||||
}
|
||||
function ToUint32(x) {
|
||||
return x >>> 0;
|
||||
}
|
||||
function Empty() {
|
||||
}
|
||||
defineProperties(FunctionPrototype, {
|
||||
bind: function bind(that) {
|
||||
var target = this;
|
||||
if (!isFunction(target)) {
|
||||
throw new TypeError("Function.prototype.bind called on incompatible " + target);
|
||||
}
|
||||
var args = array_slice.call(arguments, 1);
|
||||
var binder = function() {
|
||||
if (this instanceof bound) {
|
||||
var result = target.apply(
|
||||
this,
|
||||
args.concat(array_slice.call(arguments))
|
||||
);
|
||||
if (Object(result) === result) {
|
||||
return result;
|
||||
}
|
||||
return this;
|
||||
} else {
|
||||
return target.apply(
|
||||
that,
|
||||
args.concat(array_slice.call(arguments))
|
||||
);
|
||||
}
|
||||
};
|
||||
var boundLength = Math.max(0, target.length - args.length);
|
||||
var boundArgs = [];
|
||||
for (var i = 0; i < boundLength; i++) {
|
||||
boundArgs.push("$" + i);
|
||||
}
|
||||
var bound = Function("binder", "return function (" + boundArgs.join(",") + "){ return binder.apply(this, arguments); }")(binder);
|
||||
if (target.prototype) {
|
||||
Empty.prototype = target.prototype;
|
||||
bound.prototype = new Empty();
|
||||
Empty.prototype = null;
|
||||
}
|
||||
return bound;
|
||||
}
|
||||
});
|
||||
defineProperties(Array, { isArray });
|
||||
var boxedString = Object("a");
|
||||
var splitString = boxedString[0] !== "a" || !(0 in boxedString);
|
||||
var properlyBoxesContext = function properlyBoxed(method) {
|
||||
var properlyBoxesNonStrict = true;
|
||||
var properlyBoxesStrict = true;
|
||||
if (method) {
|
||||
method.call("foo", function(_, __, context) {
|
||||
if (typeof context !== "object") {
|
||||
properlyBoxesNonStrict = false;
|
||||
}
|
||||
});
|
||||
method.call([1], function() {
|
||||
"use strict";
|
||||
properlyBoxesStrict = typeof this === "string";
|
||||
}, "x");
|
||||
}
|
||||
return !!method && properlyBoxesNonStrict && properlyBoxesStrict;
|
||||
};
|
||||
defineProperties(ArrayPrototype, {
|
||||
forEach: function forEach(fun) {
|
||||
var object = toObject(this), self2 = splitString && isString(this) ? this.split("") : object, thisp = arguments[1], i = -1, length = self2.length >>> 0;
|
||||
if (!isFunction(fun)) {
|
||||
throw new TypeError();
|
||||
}
|
||||
while (++i < length) {
|
||||
if (i in self2) {
|
||||
fun.call(thisp, self2[i], i, object);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, !properlyBoxesContext(ArrayPrototype.forEach));
|
||||
var hasFirefox2IndexOfBug = Array.prototype.indexOf && [0, 1].indexOf(1, 2) !== -1;
|
||||
defineProperties(ArrayPrototype, {
|
||||
indexOf: function indexOf(sought) {
|
||||
var self2 = splitString && isString(this) ? this.split("") : toObject(this), length = self2.length >>> 0;
|
||||
if (!length) {
|
||||
return -1;
|
||||
}
|
||||
var i = 0;
|
||||
if (arguments.length > 1) {
|
||||
i = toInteger(arguments[1]);
|
||||
}
|
||||
i = i >= 0 ? i : Math.max(0, length + i);
|
||||
for (; i < length; i++) {
|
||||
if (i in self2 && self2[i] === sought) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}, hasFirefox2IndexOfBug);
|
||||
var string_split = StringPrototype.split;
|
||||
if ("ab".split(/(?:ab)*/).length !== 2 || ".".split(/(.?)(.?)/).length !== 4 || "tesst".split(/(s)*/)[1] === "t" || "test".split(/(?:)/, -1).length !== 4 || "".split(/.?/).length || ".".split(/()()/).length > 1) {
|
||||
(function() {
|
||||
var compliantExecNpcg = /()??/.exec("")[1] === void 0;
|
||||
StringPrototype.split = function(separator, limit) {
|
||||
var string = this;
|
||||
if (separator === void 0 && limit === 0) {
|
||||
return [];
|
||||
}
|
||||
if (_toString.call(separator) !== "[object RegExp]") {
|
||||
return string_split.call(this, separator, limit);
|
||||
}
|
||||
var output = [], flags = (separator.ignoreCase ? "i" : "") + (separator.multiline ? "m" : "") + (separator.extended ? "x" : "") + // Proposed for ES6
|
||||
(separator.sticky ? "y" : ""), lastLastIndex = 0, separator2, match, lastIndex, lastLength;
|
||||
separator = new RegExp(separator.source, flags + "g");
|
||||
string += "";
|
||||
if (!compliantExecNpcg) {
|
||||
separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
|
||||
}
|
||||
limit = limit === void 0 ? -1 >>> 0 : (
|
||||
// Math.pow(2, 32) - 1
|
||||
ToUint32(limit)
|
||||
);
|
||||
while (match = separator.exec(string)) {
|
||||
lastIndex = match.index + match[0].length;
|
||||
if (lastIndex > lastLastIndex) {
|
||||
output.push(string.slice(lastLastIndex, match.index));
|
||||
if (!compliantExecNpcg && match.length > 1) {
|
||||
match[0].replace(separator2, function() {
|
||||
for (var i = 1; i < arguments.length - 2; i++) {
|
||||
if (arguments[i] === void 0) {
|
||||
match[i] = void 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (match.length > 1 && match.index < string.length) {
|
||||
ArrayPrototype.push.apply(output, match.slice(1));
|
||||
}
|
||||
lastLength = match[0].length;
|
||||
lastLastIndex = lastIndex;
|
||||
if (output.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (separator.lastIndex === match.index) {
|
||||
separator.lastIndex++;
|
||||
}
|
||||
}
|
||||
if (lastLastIndex === string.length) {
|
||||
if (lastLength || !separator.test("")) {
|
||||
output.push("");
|
||||
}
|
||||
} else {
|
||||
output.push(string.slice(lastLastIndex));
|
||||
}
|
||||
return output.length > limit ? output.slice(0, limit) : output;
|
||||
};
|
||||
})();
|
||||
} else if ("0".split(void 0, 0).length) {
|
||||
StringPrototype.split = function split(separator, limit) {
|
||||
if (separator === void 0 && limit === 0) {
|
||||
return [];
|
||||
}
|
||||
return string_split.call(this, separator, limit);
|
||||
};
|
||||
}
|
||||
var string_substr = StringPrototype.substr;
|
||||
var hasNegativeSubstrBug = "".substr && "0b".substr(-1) !== "b";
|
||||
defineProperties(StringPrototype, {
|
||||
substr: function substr(start, length) {
|
||||
return string_substr.call(
|
||||
this,
|
||||
start < 0 ? (start = this.length + start) < 0 ? 0 : start : start,
|
||||
length
|
||||
);
|
||||
}
|
||||
}, hasNegativeSubstrBug);
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/utils/escape.js
|
||||
var require_escape = __commonJS({
|
||||
"node_modules/sockjs-client/lib/utils/escape.js"(exports, module) {
|
||||
"use strict";
|
||||
var extraEscapable = /[\x00-\x1f\ud800-\udfff\ufffe\uffff\u0300-\u0333\u033d-\u0346\u034a-\u034c\u0350-\u0352\u0357-\u0358\u035c-\u0362\u0374\u037e\u0387\u0591-\u05af\u05c4\u0610-\u0617\u0653-\u0654\u0657-\u065b\u065d-\u065e\u06df-\u06e2\u06eb-\u06ec\u0730\u0732-\u0733\u0735-\u0736\u073a\u073d\u073f-\u0741\u0743\u0745\u0747\u07eb-\u07f1\u0951\u0958-\u095f\u09dc-\u09dd\u09df\u0a33\u0a36\u0a59-\u0a5b\u0a5e\u0b5c-\u0b5d\u0e38-\u0e39\u0f43\u0f4d\u0f52\u0f57\u0f5c\u0f69\u0f72-\u0f76\u0f78\u0f80-\u0f83\u0f93\u0f9d\u0fa2\u0fa7\u0fac\u0fb9\u1939-\u193a\u1a17\u1b6b\u1cda-\u1cdb\u1dc0-\u1dcf\u1dfc\u1dfe\u1f71\u1f73\u1f75\u1f77\u1f79\u1f7b\u1f7d\u1fbb\u1fbe\u1fc9\u1fcb\u1fd3\u1fdb\u1fe3\u1feb\u1fee-\u1fef\u1ff9\u1ffb\u1ffd\u2000-\u2001\u20d0-\u20d1\u20d4-\u20d7\u20e7-\u20e9\u2126\u212a-\u212b\u2329-\u232a\u2adc\u302b-\u302c\uaab2-\uaab3\uf900-\ufa0d\ufa10\ufa12\ufa15-\ufa1e\ufa20\ufa22\ufa25-\ufa26\ufa2a-\ufa2d\ufa30-\ufa6d\ufa70-\ufad9\ufb1d\ufb1f\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4e\ufff0-\uffff]/g;
|
||||
var extraLookup;
|
||||
var unrollLookup = function(escapable) {
|
||||
var i;
|
||||
var unrolled = {};
|
||||
var c = [];
|
||||
for (i = 0; i < 65536; i++) {
|
||||
c.push(String.fromCharCode(i));
|
||||
}
|
||||
escapable.lastIndex = 0;
|
||||
c.join("").replace(escapable, function(a) {
|
||||
unrolled[a] = "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4);
|
||||
return "";
|
||||
});
|
||||
escapable.lastIndex = 0;
|
||||
return unrolled;
|
||||
};
|
||||
module.exports = {
|
||||
quote: function(string) {
|
||||
var quoted = JSON.stringify(string);
|
||||
extraEscapable.lastIndex = 0;
|
||||
if (!extraEscapable.test(quoted)) {
|
||||
return quoted;
|
||||
}
|
||||
if (!extraLookup) {
|
||||
extraLookup = unrollLookup(extraEscapable);
|
||||
}
|
||||
return quoted.replace(extraEscapable, function(a) {
|
||||
return extraLookup[a];
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/utils/transport.js
|
||||
var require_transport = __commonJS({
|
||||
"node_modules/sockjs-client/lib/utils/transport.js"(exports, module) {
|
||||
"use strict";
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:utils:transport");
|
||||
}
|
||||
module.exports = function(availableTransports) {
|
||||
return {
|
||||
filterToEnabled: function(transportsWhitelist, info) {
|
||||
var transports = {
|
||||
main: [],
|
||||
facade: []
|
||||
};
|
||||
if (!transportsWhitelist) {
|
||||
transportsWhitelist = [];
|
||||
} else if (typeof transportsWhitelist === "string") {
|
||||
transportsWhitelist = [transportsWhitelist];
|
||||
}
|
||||
availableTransports.forEach(function(trans) {
|
||||
if (!trans) {
|
||||
return;
|
||||
}
|
||||
if (trans.transportName === "websocket" && info.websocket === false) {
|
||||
debug("disabled from server", "websocket");
|
||||
return;
|
||||
}
|
||||
if (transportsWhitelist.length && transportsWhitelist.indexOf(trans.transportName) === -1) {
|
||||
debug("not in whitelist", trans.transportName);
|
||||
return;
|
||||
}
|
||||
if (trans.enabled(info)) {
|
||||
debug("enabled", trans.transportName);
|
||||
transports.main.push(trans);
|
||||
if (trans.facadeTransport) {
|
||||
transports.facade.push(trans.facadeTransport);
|
||||
}
|
||||
} else {
|
||||
debug("disabled", trans.transportName);
|
||||
}
|
||||
});
|
||||
return transports;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/utils/log.js
|
||||
var require_log = __commonJS({
|
||||
"node_modules/sockjs-client/lib/utils/log.js"(exports, module) {
|
||||
"use strict";
|
||||
var logObject = {};
|
||||
["log", "debug", "warn"].forEach(function(level) {
|
||||
var levelExists;
|
||||
try {
|
||||
levelExists = window.console && window.console[level] && window.console[level].apply;
|
||||
} catch (e) {
|
||||
}
|
||||
logObject[level] = levelExists ? function() {
|
||||
return window.console[level].apply(window.console, arguments);
|
||||
} : level === "log" ? function() {
|
||||
} : logObject.log;
|
||||
});
|
||||
module.exports = logObject;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/event/event.js
|
||||
var require_event2 = __commonJS({
|
||||
"node_modules/sockjs-client/lib/event/event.js"(exports, module) {
|
||||
"use strict";
|
||||
function Event(eventType) {
|
||||
this.type = eventType;
|
||||
}
|
||||
Event.prototype.initEvent = function(eventType, canBubble, cancelable) {
|
||||
this.type = eventType;
|
||||
this.bubbles = canBubble;
|
||||
this.cancelable = cancelable;
|
||||
this.timeStamp = +/* @__PURE__ */ new Date();
|
||||
return this;
|
||||
};
|
||||
Event.prototype.stopPropagation = function() {
|
||||
};
|
||||
Event.prototype.preventDefault = function() {
|
||||
};
|
||||
Event.CAPTURING_PHASE = 1;
|
||||
Event.AT_TARGET = 2;
|
||||
Event.BUBBLING_PHASE = 3;
|
||||
module.exports = Event;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/location.js
|
||||
var require_location = __commonJS({
|
||||
"node_modules/sockjs-client/lib/location.js"(exports, module) {
|
||||
"use strict";
|
||||
module.exports = window.location || {
|
||||
origin: "http://localhost:80",
|
||||
protocol: "http:",
|
||||
host: "localhost",
|
||||
port: 80,
|
||||
href: "http://localhost/",
|
||||
hash: ""
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/event/close.js
|
||||
var require_close = __commonJS({
|
||||
"node_modules/sockjs-client/lib/event/close.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var Event = require_event2();
|
||||
function CloseEvent() {
|
||||
Event.call(this);
|
||||
this.initEvent("close", false, false);
|
||||
this.wasClean = false;
|
||||
this.code = 0;
|
||||
this.reason = "";
|
||||
}
|
||||
inherits(CloseEvent, Event);
|
||||
module.exports = CloseEvent;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/event/trans-message.js
|
||||
var require_trans_message = __commonJS({
|
||||
"node_modules/sockjs-client/lib/event/trans-message.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var Event = require_event2();
|
||||
function TransportMessageEvent(data) {
|
||||
Event.call(this);
|
||||
this.initEvent("message", false, false);
|
||||
this.data = data;
|
||||
}
|
||||
inherits(TransportMessageEvent, Event);
|
||||
module.exports = TransportMessageEvent;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/transport/sender/xhr-fake.js
|
||||
var require_xhr_fake = __commonJS({
|
||||
"node_modules/sockjs-client/lib/transport/sender/xhr-fake.js"(exports, module) {
|
||||
"use strict";
|
||||
var EventEmitter = require_emitter().EventEmitter;
|
||||
var inherits = require_inherits_browser();
|
||||
function XHRFake() {
|
||||
var self2 = this;
|
||||
EventEmitter.call(this);
|
||||
this.to = setTimeout(function() {
|
||||
self2.emit("finish", 200, "{}");
|
||||
}, XHRFake.timeout);
|
||||
}
|
||||
inherits(XHRFake, EventEmitter);
|
||||
XHRFake.prototype.close = function() {
|
||||
clearTimeout(this.to);
|
||||
};
|
||||
XHRFake.timeout = 2e3;
|
||||
module.exports = XHRFake;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/info-ajax.js
|
||||
var require_info_ajax = __commonJS({
|
||||
"node_modules/sockjs-client/lib/info-ajax.js"(exports, module) {
|
||||
"use strict";
|
||||
var EventEmitter = require_emitter().EventEmitter;
|
||||
var inherits = require_inherits_browser();
|
||||
var objectUtils = require_object();
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:info-ajax");
|
||||
}
|
||||
function InfoAjax(url, AjaxObject) {
|
||||
EventEmitter.call(this);
|
||||
var self2 = this;
|
||||
var t0 = +/* @__PURE__ */ new Date();
|
||||
this.xo = new AjaxObject("GET", url);
|
||||
this.xo.once("finish", function(status, text) {
|
||||
var info, rtt;
|
||||
if (status === 200) {
|
||||
rtt = +/* @__PURE__ */ new Date() - t0;
|
||||
if (text) {
|
||||
try {
|
||||
info = JSON.parse(text);
|
||||
} catch (e) {
|
||||
debug("bad json", text);
|
||||
}
|
||||
}
|
||||
if (!objectUtils.isObject(info)) {
|
||||
info = {};
|
||||
}
|
||||
}
|
||||
self2.emit("finish", info, rtt);
|
||||
self2.removeAllListeners();
|
||||
});
|
||||
}
|
||||
inherits(InfoAjax, EventEmitter);
|
||||
InfoAjax.prototype.close = function() {
|
||||
this.removeAllListeners();
|
||||
this.xo.close();
|
||||
};
|
||||
module.exports = InfoAjax;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/info-iframe-receiver.js
|
||||
var require_info_iframe_receiver = __commonJS({
|
||||
"node_modules/sockjs-client/lib/info-iframe-receiver.js"(exports, module) {
|
||||
"use strict";
|
||||
var inherits = require_inherits_browser();
|
||||
var EventEmitter = require_emitter().EventEmitter;
|
||||
var XHRLocalObject = require_xhr_local();
|
||||
var InfoAjax = require_info_ajax();
|
||||
function InfoReceiverIframe(transUrl) {
|
||||
var self2 = this;
|
||||
EventEmitter.call(this);
|
||||
this.ir = new InfoAjax(transUrl, XHRLocalObject);
|
||||
this.ir.once("finish", function(info, rtt) {
|
||||
self2.ir = null;
|
||||
self2.emit("message", JSON.stringify([info, rtt]));
|
||||
});
|
||||
}
|
||||
inherits(InfoReceiverIframe, EventEmitter);
|
||||
InfoReceiverIframe.transportName = "iframe-info-receiver";
|
||||
InfoReceiverIframe.prototype.close = function() {
|
||||
if (this.ir) {
|
||||
this.ir.close();
|
||||
this.ir = null;
|
||||
}
|
||||
this.removeAllListeners();
|
||||
};
|
||||
module.exports = InfoReceiverIframe;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/info-iframe.js
|
||||
var require_info_iframe = __commonJS({
|
||||
"node_modules/sockjs-client/lib/info-iframe.js"(exports, module) {
|
||||
"use strict";
|
||||
var EventEmitter = require_emitter().EventEmitter;
|
||||
var inherits = require_inherits_browser();
|
||||
var utils = require_event();
|
||||
var IframeTransport = require_iframe2();
|
||||
var InfoReceiverIframe = require_info_iframe_receiver();
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:info-iframe");
|
||||
}
|
||||
function InfoIframe(baseUrl, url) {
|
||||
var self2 = this;
|
||||
EventEmitter.call(this);
|
||||
var go = function() {
|
||||
var ifr = self2.ifr = new IframeTransport(InfoReceiverIframe.transportName, url, baseUrl);
|
||||
ifr.once("message", function(msg) {
|
||||
if (msg) {
|
||||
var d;
|
||||
try {
|
||||
d = JSON.parse(msg);
|
||||
} catch (e) {
|
||||
debug("bad json", msg);
|
||||
self2.emit("finish");
|
||||
self2.close();
|
||||
return;
|
||||
}
|
||||
var info = d[0], rtt = d[1];
|
||||
self2.emit("finish", info, rtt);
|
||||
}
|
||||
self2.close();
|
||||
});
|
||||
ifr.once("close", function() {
|
||||
self2.emit("finish");
|
||||
self2.close();
|
||||
});
|
||||
};
|
||||
if (!window.document.body) {
|
||||
utils.attachEvent("load", go);
|
||||
} else {
|
||||
go();
|
||||
}
|
||||
}
|
||||
inherits(InfoIframe, EventEmitter);
|
||||
InfoIframe.enabled = function() {
|
||||
return IframeTransport.enabled();
|
||||
};
|
||||
InfoIframe.prototype.close = function() {
|
||||
if (this.ifr) {
|
||||
this.ifr.close();
|
||||
}
|
||||
this.removeAllListeners();
|
||||
this.ifr = null;
|
||||
};
|
||||
module.exports = InfoIframe;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/info-receiver.js
|
||||
var require_info_receiver = __commonJS({
|
||||
"node_modules/sockjs-client/lib/info-receiver.js"(exports, module) {
|
||||
"use strict";
|
||||
var EventEmitter = require_emitter().EventEmitter;
|
||||
var inherits = require_inherits_browser();
|
||||
var urlUtils = require_url();
|
||||
var XDR = require_xdr();
|
||||
var XHRCors = require_xhr_cors();
|
||||
var XHRLocal = require_xhr_local();
|
||||
var XHRFake = require_xhr_fake();
|
||||
var InfoIframe = require_info_iframe();
|
||||
var InfoAjax = require_info_ajax();
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:info-receiver");
|
||||
}
|
||||
function InfoReceiver(baseUrl, urlInfo) {
|
||||
debug(baseUrl);
|
||||
var self2 = this;
|
||||
EventEmitter.call(this);
|
||||
setTimeout(function() {
|
||||
self2.doXhr(baseUrl, urlInfo);
|
||||
}, 0);
|
||||
}
|
||||
inherits(InfoReceiver, EventEmitter);
|
||||
InfoReceiver._getReceiver = function(baseUrl, url, urlInfo) {
|
||||
if (urlInfo.sameOrigin) {
|
||||
return new InfoAjax(url, XHRLocal);
|
||||
}
|
||||
if (XHRCors.enabled) {
|
||||
return new InfoAjax(url, XHRCors);
|
||||
}
|
||||
if (XDR.enabled && urlInfo.sameScheme) {
|
||||
return new InfoAjax(url, XDR);
|
||||
}
|
||||
if (InfoIframe.enabled()) {
|
||||
return new InfoIframe(baseUrl, url);
|
||||
}
|
||||
return new InfoAjax(url, XHRFake);
|
||||
};
|
||||
InfoReceiver.prototype.doXhr = function(baseUrl, urlInfo) {
|
||||
var self2 = this, url = urlUtils.addPath(baseUrl, "/info");
|
||||
debug("doXhr", url);
|
||||
this.xo = InfoReceiver._getReceiver(baseUrl, url, urlInfo);
|
||||
this.timeoutRef = setTimeout(function() {
|
||||
debug("timeout");
|
||||
self2._cleanup(false);
|
||||
self2.emit("finish");
|
||||
}, InfoReceiver.timeout);
|
||||
this.xo.once("finish", function(info, rtt) {
|
||||
debug("finish", info, rtt);
|
||||
self2._cleanup(true);
|
||||
self2.emit("finish", info, rtt);
|
||||
});
|
||||
};
|
||||
InfoReceiver.prototype._cleanup = function(wasClean) {
|
||||
debug("_cleanup");
|
||||
clearTimeout(this.timeoutRef);
|
||||
this.timeoutRef = null;
|
||||
if (!wasClean && this.xo) {
|
||||
this.xo.close();
|
||||
}
|
||||
this.xo = null;
|
||||
};
|
||||
InfoReceiver.prototype.close = function() {
|
||||
debug("close");
|
||||
this.removeAllListeners();
|
||||
this._cleanup(false);
|
||||
};
|
||||
InfoReceiver.timeout = 8e3;
|
||||
module.exports = InfoReceiver;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/facade.js
|
||||
var require_facade = __commonJS({
|
||||
"node_modules/sockjs-client/lib/facade.js"(exports, module) {
|
||||
"use strict";
|
||||
var iframeUtils = require_iframe();
|
||||
function FacadeJS(transport) {
|
||||
this._transport = transport;
|
||||
transport.on("message", this._transportMessage.bind(this));
|
||||
transport.on("close", this._transportClose.bind(this));
|
||||
}
|
||||
FacadeJS.prototype._transportClose = function(code, reason) {
|
||||
iframeUtils.postMessage("c", JSON.stringify([code, reason]));
|
||||
};
|
||||
FacadeJS.prototype._transportMessage = function(frame) {
|
||||
iframeUtils.postMessage("t", frame);
|
||||
};
|
||||
FacadeJS.prototype._send = function(data) {
|
||||
this._transport.send(data);
|
||||
};
|
||||
FacadeJS.prototype._close = function() {
|
||||
this._transport.close();
|
||||
this._transport.removeAllListeners();
|
||||
};
|
||||
module.exports = FacadeJS;
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/iframe-bootstrap.js
|
||||
var require_iframe_bootstrap = __commonJS({
|
||||
"node_modules/sockjs-client/lib/iframe-bootstrap.js"(exports, module) {
|
||||
"use strict";
|
||||
var urlUtils = require_url();
|
||||
var eventUtils = require_event();
|
||||
var FacadeJS = require_facade();
|
||||
var InfoIframeReceiver = require_info_iframe_receiver();
|
||||
var iframeUtils = require_iframe();
|
||||
var loc = require_location();
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:iframe-bootstrap");
|
||||
}
|
||||
module.exports = function(SockJS2, availableTransports) {
|
||||
var transportMap = {};
|
||||
availableTransports.forEach(function(at) {
|
||||
if (at.facadeTransport) {
|
||||
transportMap[at.facadeTransport.transportName] = at.facadeTransport;
|
||||
}
|
||||
});
|
||||
transportMap[InfoIframeReceiver.transportName] = InfoIframeReceiver;
|
||||
var parentOrigin;
|
||||
SockJS2.bootstrap_iframe = function() {
|
||||
var facade;
|
||||
iframeUtils.currentWindowId = loc.hash.slice(1);
|
||||
var onMessage = function(e) {
|
||||
if (e.source !== parent) {
|
||||
return;
|
||||
}
|
||||
if (typeof parentOrigin === "undefined") {
|
||||
parentOrigin = e.origin;
|
||||
}
|
||||
if (e.origin !== parentOrigin) {
|
||||
return;
|
||||
}
|
||||
var iframeMessage;
|
||||
try {
|
||||
iframeMessage = JSON.parse(e.data);
|
||||
} catch (ignored) {
|
||||
debug("bad json", e.data);
|
||||
return;
|
||||
}
|
||||
if (iframeMessage.windowId !== iframeUtils.currentWindowId) {
|
||||
return;
|
||||
}
|
||||
switch (iframeMessage.type) {
|
||||
case "s":
|
||||
var p;
|
||||
try {
|
||||
p = JSON.parse(iframeMessage.data);
|
||||
} catch (ignored) {
|
||||
debug("bad json", iframeMessage.data);
|
||||
break;
|
||||
}
|
||||
var version = p[0];
|
||||
var transport = p[1];
|
||||
var transUrl = p[2];
|
||||
var baseUrl = p[3];
|
||||
debug(version, transport, transUrl, baseUrl);
|
||||
if (version !== SockJS2.version) {
|
||||
throw new Error('Incompatible SockJS! Main site uses: "' + version + '", the iframe: "' + SockJS2.version + '".');
|
||||
}
|
||||
if (!urlUtils.isOriginEqual(transUrl, loc.href) || !urlUtils.isOriginEqual(baseUrl, loc.href)) {
|
||||
throw new Error("Can't connect to different domain from within an iframe. (" + loc.href + ", " + transUrl + ", " + baseUrl + ")");
|
||||
}
|
||||
facade = new FacadeJS(new transportMap[transport](transUrl, baseUrl));
|
||||
break;
|
||||
case "m":
|
||||
facade._send(iframeMessage.data);
|
||||
break;
|
||||
case "c":
|
||||
if (facade) {
|
||||
facade._close();
|
||||
}
|
||||
facade = null;
|
||||
break;
|
||||
}
|
||||
};
|
||||
eventUtils.attachEvent("message", onMessage);
|
||||
iframeUtils.postMessage("s");
|
||||
};
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/main.js
|
||||
var require_main = __commonJS({
|
||||
"node_modules/sockjs-client/lib/main.js"(exports, module) {
|
||||
"use strict";
|
||||
require_shims();
|
||||
var URL = require_url_parse();
|
||||
var inherits = require_inherits_browser();
|
||||
var random = require_random();
|
||||
var escape = require_escape();
|
||||
var urlUtils = require_url();
|
||||
var eventUtils = require_event();
|
||||
var transport = require_transport();
|
||||
var objectUtils = require_object();
|
||||
var browser = require_browser2();
|
||||
var log = require_log();
|
||||
var Event = require_event2();
|
||||
var EventTarget2 = require_eventtarget();
|
||||
var loc = require_location();
|
||||
var CloseEvent = require_close();
|
||||
var TransportMessageEvent = require_trans_message();
|
||||
var InfoReceiver = require_info_receiver();
|
||||
var debug = function() {
|
||||
};
|
||||
if (true) {
|
||||
debug = require_browser()("sockjs-client:main");
|
||||
}
|
||||
var transports;
|
||||
function SockJS2(url, protocols, options) {
|
||||
if (!(this instanceof SockJS2)) {
|
||||
return new SockJS2(url, protocols, options);
|
||||
}
|
||||
if (arguments.length < 1) {
|
||||
throw new TypeError("Failed to construct 'SockJS: 1 argument required, but only 0 present");
|
||||
}
|
||||
EventTarget2.call(this);
|
||||
this.readyState = SockJS2.CONNECTING;
|
||||
this.extensions = "";
|
||||
this.protocol = "";
|
||||
options = options || {};
|
||||
if (options.protocols_whitelist) {
|
||||
log.warn("'protocols_whitelist' is DEPRECATED. Use 'transports' instead.");
|
||||
}
|
||||
this._transportsWhitelist = options.transports;
|
||||
this._transportOptions = options.transportOptions || {};
|
||||
this._timeout = options.timeout || 0;
|
||||
var sessionId = options.sessionId || 8;
|
||||
if (typeof sessionId === "function") {
|
||||
this._generateSessionId = sessionId;
|
||||
} else if (typeof sessionId === "number") {
|
||||
this._generateSessionId = function() {
|
||||
return random.string(sessionId);
|
||||
};
|
||||
} else {
|
||||
throw new TypeError("If sessionId is used in the options, it needs to be a number or a function.");
|
||||
}
|
||||
this._server = options.server || random.numberString(1e3);
|
||||
var parsedUrl = new URL(url);
|
||||
if (!parsedUrl.host || !parsedUrl.protocol) {
|
||||
throw new SyntaxError("The URL '" + url + "' is invalid");
|
||||
} else if (parsedUrl.hash) {
|
||||
throw new SyntaxError("The URL must not contain a fragment");
|
||||
} else if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
||||
throw new SyntaxError("The URL's scheme must be either 'http:' or 'https:'. '" + parsedUrl.protocol + "' is not allowed.");
|
||||
}
|
||||
var secure = parsedUrl.protocol === "https:";
|
||||
if (loc.protocol === "https:" && !secure) {
|
||||
if (!urlUtils.isLoopbackAddr(parsedUrl.hostname)) {
|
||||
throw new Error("SecurityError: An insecure SockJS connection may not be initiated from a page loaded over HTTPS");
|
||||
}
|
||||
}
|
||||
if (!protocols) {
|
||||
protocols = [];
|
||||
} else if (!Array.isArray(protocols)) {
|
||||
protocols = [protocols];
|
||||
}
|
||||
var sortedProtocols = protocols.sort();
|
||||
sortedProtocols.forEach(function(proto, i) {
|
||||
if (!proto) {
|
||||
throw new SyntaxError("The protocols entry '" + proto + "' is invalid.");
|
||||
}
|
||||
if (i < sortedProtocols.length - 1 && proto === sortedProtocols[i + 1]) {
|
||||
throw new SyntaxError("The protocols entry '" + proto + "' is duplicated.");
|
||||
}
|
||||
});
|
||||
var o = urlUtils.getOrigin(loc.href);
|
||||
this._origin = o ? o.toLowerCase() : null;
|
||||
parsedUrl.set("pathname", parsedUrl.pathname.replace(/\/+$/, ""));
|
||||
this.url = parsedUrl.href;
|
||||
debug("using url", this.url);
|
||||
this._urlInfo = {
|
||||
nullOrigin: !browser.hasDomain(),
|
||||
sameOrigin: urlUtils.isOriginEqual(this.url, loc.href),
|
||||
sameScheme: urlUtils.isSchemeEqual(this.url, loc.href)
|
||||
};
|
||||
this._ir = new InfoReceiver(this.url, this._urlInfo);
|
||||
this._ir.once("finish", this._receiveInfo.bind(this));
|
||||
}
|
||||
inherits(SockJS2, EventTarget2);
|
||||
function userSetCode(code) {
|
||||
return code === 1e3 || code >= 3e3 && code <= 4999;
|
||||
}
|
||||
SockJS2.prototype.close = function(code, reason) {
|
||||
if (code && !userSetCode(code)) {
|
||||
throw new Error("InvalidAccessError: Invalid code");
|
||||
}
|
||||
if (reason && reason.length > 123) {
|
||||
throw new SyntaxError("reason argument has an invalid length");
|
||||
}
|
||||
if (this.readyState === SockJS2.CLOSING || this.readyState === SockJS2.CLOSED) {
|
||||
return;
|
||||
}
|
||||
var wasClean = true;
|
||||
this._close(code || 1e3, reason || "Normal closure", wasClean);
|
||||
};
|
||||
SockJS2.prototype.send = function(data) {
|
||||
if (typeof data !== "string") {
|
||||
data = "" + data;
|
||||
}
|
||||
if (this.readyState === SockJS2.CONNECTING) {
|
||||
throw new Error("InvalidStateError: The connection has not been established yet");
|
||||
}
|
||||
if (this.readyState !== SockJS2.OPEN) {
|
||||
return;
|
||||
}
|
||||
this._transport.send(escape.quote(data));
|
||||
};
|
||||
SockJS2.version = require_version();
|
||||
SockJS2.CONNECTING = 0;
|
||||
SockJS2.OPEN = 1;
|
||||
SockJS2.CLOSING = 2;
|
||||
SockJS2.CLOSED = 3;
|
||||
SockJS2.prototype._receiveInfo = function(info, rtt) {
|
||||
debug("_receiveInfo", rtt);
|
||||
this._ir = null;
|
||||
if (!info) {
|
||||
this._close(1002, "Cannot connect to server");
|
||||
return;
|
||||
}
|
||||
this._rto = this.countRTO(rtt);
|
||||
this._transUrl = info.base_url ? info.base_url : this.url;
|
||||
info = objectUtils.extend(info, this._urlInfo);
|
||||
debug("info", info);
|
||||
var enabledTransports = transports.filterToEnabled(this._transportsWhitelist, info);
|
||||
this._transports = enabledTransports.main;
|
||||
debug(this._transports.length + " enabled transports");
|
||||
this._connect();
|
||||
};
|
||||
SockJS2.prototype._connect = function() {
|
||||
for (var Transport = this._transports.shift(); Transport; Transport = this._transports.shift()) {
|
||||
debug("attempt", Transport.transportName);
|
||||
if (Transport.needBody) {
|
||||
if (!window.document.body || typeof window.document.readyState !== "undefined" && window.document.readyState !== "complete" && window.document.readyState !== "interactive") {
|
||||
debug("waiting for body");
|
||||
this._transports.unshift(Transport);
|
||||
eventUtils.attachEvent("load", this._connect.bind(this));
|
||||
return;
|
||||
}
|
||||
}
|
||||
var timeoutMs = Math.max(this._timeout, this._rto * Transport.roundTrips || 5e3);
|
||||
this._transportTimeoutId = setTimeout(this._transportTimeout.bind(this), timeoutMs);
|
||||
debug("using timeout", timeoutMs);
|
||||
var transportUrl = urlUtils.addPath(this._transUrl, "/" + this._server + "/" + this._generateSessionId());
|
||||
var options = this._transportOptions[Transport.transportName];
|
||||
debug("transport url", transportUrl);
|
||||
var transportObj = new Transport(transportUrl, this._transUrl, options);
|
||||
transportObj.on("message", this._transportMessage.bind(this));
|
||||
transportObj.once("close", this._transportClose.bind(this));
|
||||
transportObj.transportName = Transport.transportName;
|
||||
this._transport = transportObj;
|
||||
return;
|
||||
}
|
||||
this._close(2e3, "All transports failed", false);
|
||||
};
|
||||
SockJS2.prototype._transportTimeout = function() {
|
||||
debug("_transportTimeout");
|
||||
if (this.readyState === SockJS2.CONNECTING) {
|
||||
if (this._transport) {
|
||||
this._transport.close();
|
||||
}
|
||||
this._transportClose(2007, "Transport timed out");
|
||||
}
|
||||
};
|
||||
SockJS2.prototype._transportMessage = function(msg) {
|
||||
debug("_transportMessage", msg);
|
||||
var self2 = this, type = msg.slice(0, 1), content = msg.slice(1), payload;
|
||||
switch (type) {
|
||||
case "o":
|
||||
this._open();
|
||||
return;
|
||||
case "h":
|
||||
this.dispatchEvent(new Event("heartbeat"));
|
||||
debug("heartbeat", this.transport);
|
||||
return;
|
||||
}
|
||||
if (content) {
|
||||
try {
|
||||
payload = JSON.parse(content);
|
||||
} catch (e) {
|
||||
debug("bad json", content);
|
||||
}
|
||||
}
|
||||
if (typeof payload === "undefined") {
|
||||
debug("empty payload", content);
|
||||
return;
|
||||
}
|
||||
switch (type) {
|
||||
case "a":
|
||||
if (Array.isArray(payload)) {
|
||||
payload.forEach(function(p) {
|
||||
debug("message", self2.transport, p);
|
||||
self2.dispatchEvent(new TransportMessageEvent(p));
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "m":
|
||||
debug("message", this.transport, payload);
|
||||
this.dispatchEvent(new TransportMessageEvent(payload));
|
||||
break;
|
||||
case "c":
|
||||
if (Array.isArray(payload) && payload.length === 2) {
|
||||
this._close(payload[0], payload[1], true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
SockJS2.prototype._transportClose = function(code, reason) {
|
||||
debug("_transportClose", this.transport, code, reason);
|
||||
if (this._transport) {
|
||||
this._transport.removeAllListeners();
|
||||
this._transport = null;
|
||||
this.transport = null;
|
||||
}
|
||||
if (!userSetCode(code) && code !== 2e3 && this.readyState === SockJS2.CONNECTING) {
|
||||
this._connect();
|
||||
return;
|
||||
}
|
||||
this._close(code, reason);
|
||||
};
|
||||
SockJS2.prototype._open = function() {
|
||||
debug("_open", this._transport && this._transport.transportName, this.readyState);
|
||||
if (this.readyState === SockJS2.CONNECTING) {
|
||||
if (this._transportTimeoutId) {
|
||||
clearTimeout(this._transportTimeoutId);
|
||||
this._transportTimeoutId = null;
|
||||
}
|
||||
this.readyState = SockJS2.OPEN;
|
||||
this.transport = this._transport.transportName;
|
||||
this.dispatchEvent(new Event("open"));
|
||||
debug("connected", this.transport);
|
||||
} else {
|
||||
this._close(1006, "Server lost session");
|
||||
}
|
||||
};
|
||||
SockJS2.prototype._close = function(code, reason, wasClean) {
|
||||
debug("_close", this.transport, code, reason, wasClean, this.readyState);
|
||||
var forceFail = false;
|
||||
if (this._ir) {
|
||||
forceFail = true;
|
||||
this._ir.close();
|
||||
this._ir = null;
|
||||
}
|
||||
if (this._transport) {
|
||||
this._transport.close();
|
||||
this._transport = null;
|
||||
this.transport = null;
|
||||
}
|
||||
if (this.readyState === SockJS2.CLOSED) {
|
||||
throw new Error("InvalidStateError: SockJS has already been closed");
|
||||
}
|
||||
this.readyState = SockJS2.CLOSING;
|
||||
setTimeout(function() {
|
||||
this.readyState = SockJS2.CLOSED;
|
||||
if (forceFail) {
|
||||
this.dispatchEvent(new Event("error"));
|
||||
}
|
||||
var e = new CloseEvent("close");
|
||||
e.wasClean = wasClean || false;
|
||||
e.code = code || 1e3;
|
||||
e.reason = reason;
|
||||
this.dispatchEvent(e);
|
||||
this.onmessage = this.onclose = this.onerror = null;
|
||||
debug("disconnected");
|
||||
}.bind(this), 0);
|
||||
};
|
||||
SockJS2.prototype.countRTO = function(rtt) {
|
||||
if (rtt > 100) {
|
||||
return 4 * rtt;
|
||||
}
|
||||
return 300 + rtt;
|
||||
};
|
||||
module.exports = function(availableTransports) {
|
||||
transports = transport(availableTransports);
|
||||
require_iframe_bootstrap()(SockJS2, availableTransports);
|
||||
return SockJS2;
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/sockjs-client/lib/entry.js
|
||||
var require_entry = __commonJS({
|
||||
"node_modules/sockjs-client/lib/entry.js"(exports, module) {
|
||||
"use strict";
|
||||
var transportList = require_transport_list();
|
||||
module.exports = require_main()(transportList);
|
||||
if ("_sockjs_onload" in window) {
|
||||
setTimeout(window._sockjs_onload, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// pkg/sdk/client/src/index.ts
|
||||
var src_exports = {};
|
||||
__export(src_exports, {
|
||||
client: () => client,
|
||||
crossFrameMessenger: () => crossFrameMessenger
|
||||
});
|
||||
|
||||
// pkg/sdk/client/src/event-target.ts
|
||||
var EventTarget = class {
|
||||
constructor() {
|
||||
this.listeners = {};
|
||||
}
|
||||
addEventListener(type, callback) {
|
||||
if (!(type in this.listeners)) {
|
||||
this.listeners[type] = [];
|
||||
}
|
||||
this.listeners[type].push(callback);
|
||||
}
|
||||
removeEventListener(type, callback) {
|
||||
if (!(type in this.listeners)) {
|
||||
return;
|
||||
}
|
||||
const stack = this.listeners[type];
|
||||
for (var i = 0, l = stack.length; i < l; i++) {
|
||||
if (stack[i] === callback) {
|
||||
stack.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatchEvent(event) {
|
||||
if (!(event.type in this.listeners)) {
|
||||
return true;
|
||||
}
|
||||
const stack = this.listeners[event.type].slice();
|
||||
for (let i = 0, l = stack.length; i < l; i++) {
|
||||
stack[i].call(this, event);
|
||||
if (event.cancelBubble)
|
||||
return;
|
||||
}
|
||||
return !event.defaultPrevented;
|
||||
}
|
||||
};
|
||||
|
||||
// pkg/sdk/client/src/message.ts
|
||||
var TypeMessage = "message";
|
||||
var Message = class {
|
||||
constructor(type, payload) {
|
||||
this._type = type;
|
||||
this._payload = payload;
|
||||
}
|
||||
getType() {
|
||||
return this._type;
|
||||
}
|
||||
getPayload() {
|
||||
return this._payload;
|
||||
}
|
||||
toJSON() {
|
||||
return {
|
||||
t: this._type,
|
||||
p: this._payload
|
||||
};
|
||||
}
|
||||
};
|
||||
function messageFrom(raw) {
|
||||
return new Message(raw.t, raw.p);
|
||||
}
|
||||
|
||||
// pkg/sdk/client/src/rpc-error.ts
|
||||
var RPCError = class extends Error {
|
||||
constructor(code, message, data) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.data = data;
|
||||
if (Error.captureStackTrace)
|
||||
Error.captureStackTrace(this, RPCError);
|
||||
}
|
||||
};
|
||||
|
||||
// pkg/sdk/client/src/client.ts
|
||||
var import_sockjs_client = __toESM(require_entry());
|
||||
var EventTypeMessage = "message";
|
||||
var EdgeAuth = "edge-auth";
|
||||
var Client = class extends EventTarget {
|
||||
constructor(autoReconnect = true) {
|
||||
super();
|
||||
this._conn = null;
|
||||
this._onConnectionClose = this._onConnectionClose.bind(this);
|
||||
this._onConnectionMessage = this._onConnectionMessage.bind(this);
|
||||
this._handleRPCResponse = this._handleRPCResponse.bind(this);
|
||||
this._rpcID = 0;
|
||||
this._pendingRPC = {};
|
||||
this._queue = [];
|
||||
this._reconnectionDelay = 250;
|
||||
this._autoReconnect = autoReconnect;
|
||||
this.debug = false;
|
||||
this.connect = this.connect.bind(this);
|
||||
this.disconnect = this.disconnect.bind(this);
|
||||
this.rpc = this.rpc.bind(this);
|
||||
this.send = this.send.bind(this);
|
||||
this.upload = this.upload.bind(this);
|
||||
this.addEventListener(EventTypeMessage, this._handleRPCResponse);
|
||||
}
|
||||
connect(token = "") {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (token == "") {
|
||||
token = this._getAuthCookieToken();
|
||||
}
|
||||
const url = `//${document.location.host}/edge/sock?${EdgeAuth}=${token}`;
|
||||
this._log("opening connection to", url);
|
||||
const conn = new import_sockjs_client.default(url);
|
||||
const onOpen = () => {
|
||||
this._log("client connected");
|
||||
resetHandlers();
|
||||
conn.onclose = this._onConnectionClose;
|
||||
conn.onmessage = this._onConnectionMessage;
|
||||
this._conn = conn;
|
||||
this._sendQueued();
|
||||
setTimeout(() => {
|
||||
this._dispatchConnect();
|
||||
}, 0);
|
||||
return resolve(this);
|
||||
};
|
||||
const onError = (evt) => {
|
||||
resetHandlers();
|
||||
this._scheduleReconnection();
|
||||
return reject(evt);
|
||||
};
|
||||
const resetHandlers = () => {
|
||||
conn.removeEventListener("open", onOpen);
|
||||
conn.removeEventListener("close", onError);
|
||||
conn.removeEventListener("error", onError);
|
||||
};
|
||||
conn.addEventListener("open", onOpen);
|
||||
conn.addEventListener("error", onError);
|
||||
conn.addEventListener("close", onError);
|
||||
});
|
||||
}
|
||||
disconnect() {
|
||||
this._cleanupConnection();
|
||||
}
|
||||
_getAuthCookieToken() {
|
||||
const cookie = document.cookie.split("; ").find((row) => row.startsWith(EdgeAuth));
|
||||
if (cookie) {
|
||||
return cookie.split("=")[1];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
_onConnectionMessage(evt) {
|
||||
const rawMessage = JSON.parse(evt.data);
|
||||
const message = messageFrom(rawMessage);
|
||||
const event = new CustomEvent(message.getType(), {
|
||||
cancelable: true,
|
||||
detail: message.getPayload()
|
||||
});
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
_handleRPCResponse(evt) {
|
||||
const { jsonrpc, id, error, result } = evt.detail;
|
||||
if (jsonrpc !== "2.0" || id === void 0)
|
||||
return;
|
||||
if (!evt.detail.hasOwnProperty("error") && !evt.detail.hasOwnProperty("result"))
|
||||
return;
|
||||
evt.stopImmediatePropagation();
|
||||
const pending = this._pendingRPC[id];
|
||||
if (!pending)
|
||||
return;
|
||||
delete this._pendingRPC[id];
|
||||
if (error) {
|
||||
pending.reject(new RPCError(error.code, error.message, error.data));
|
||||
return;
|
||||
}
|
||||
pending.resolve(result);
|
||||
}
|
||||
_onConnectionClose(evt) {
|
||||
this._log("client disconnected");
|
||||
this._dispatchDisconnect();
|
||||
this._cleanupConnection();
|
||||
this._scheduleReconnection();
|
||||
}
|
||||
_dispatchDisconnect() {
|
||||
const event = new CustomEvent("disconnect");
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
_dispatchConnect() {
|
||||
const event = new CustomEvent("connect");
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
_scheduleReconnection() {
|
||||
if (!this._autoReconnect)
|
||||
return;
|
||||
this._reconnectionDelay = this._reconnectionDelay * 2 + Math.random();
|
||||
this._log("client will try to reconnect in %dms", this._reconnectionDelay);
|
||||
setTimeout(this.connect.bind(this), this._reconnectionDelay);
|
||||
}
|
||||
_cleanupConnection() {
|
||||
if (!this._conn)
|
||||
return;
|
||||
this._conn.onopen = null;
|
||||
this._conn.onerror = null;
|
||||
this._conn.onclose = null;
|
||||
this._conn.onmessage = null;
|
||||
this._conn.close();
|
||||
this._conn = null;
|
||||
}
|
||||
_send(message) {
|
||||
if (!this._conn)
|
||||
return false;
|
||||
this._log("sending message", message);
|
||||
this._conn.send(JSON.stringify(message));
|
||||
return true;
|
||||
}
|
||||
_sendQueued() {
|
||||
this._log("sending queued messages", this._queue.length);
|
||||
let msg = this._queue.shift();
|
||||
while (msg) {
|
||||
const sent = this._send(msg);
|
||||
if (!sent)
|
||||
return;
|
||||
msg = this._queue.shift();
|
||||
}
|
||||
}
|
||||
_log(...args) {
|
||||
if (!this.debug)
|
||||
return;
|
||||
console.log(...args);
|
||||
}
|
||||
_sendOrQueue(msg) {
|
||||
if (this.isConnected()) {
|
||||
this._sendQueued();
|
||||
this._send(msg);
|
||||
} else {
|
||||
this._log("queuing message", msg);
|
||||
this._queue.push(msg);
|
||||
}
|
||||
}
|
||||
send(data) {
|
||||
const msg = new Message(TypeMessage, data);
|
||||
this._sendOrQueue(msg);
|
||||
}
|
||||
rpc(method, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = this._rpcID++;
|
||||
const rpc = new Message(TypeMessage, {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
method,
|
||||
params
|
||||
});
|
||||
this._sendOrQueue(rpc);
|
||||
this._pendingRPC[id.toString()] = { resolve, reject };
|
||||
});
|
||||
}
|
||||
isConnected() {
|
||||
return this._conn !== null;
|
||||
}
|
||||
upload(blob, metadata) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const formData = new FormData();
|
||||
formData.set("file", blob);
|
||||
if (metadata) {
|
||||
try {
|
||||
formData.set("metadata", JSON.stringify(metadata));
|
||||
} catch (err) {
|
||||
return reject(err);
|
||||
}
|
||||
}
|
||||
const xhr = new XMLHttpRequest();
|
||||
const result = {
|
||||
onProgress: null,
|
||||
abort: () => xhr.abort(),
|
||||
result: () => {
|
||||
return new Promise((resolve2, reject2) => {
|
||||
xhr.onload = () => {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(xhr.responseText);
|
||||
} catch (err) {
|
||||
reject2(err);
|
||||
return;
|
||||
}
|
||||
resolve2(data);
|
||||
};
|
||||
xhr.onerror = reject2;
|
||||
xhr.onabort = reject2;
|
||||
});
|
||||
}
|
||||
};
|
||||
xhr.upload.onprogress = (evt) => {
|
||||
if (typeof result.onProgress !== "function")
|
||||
return;
|
||||
result.onProgress(evt.loaded, evt.total);
|
||||
};
|
||||
xhr.onabort = reject;
|
||||
xhr.onerror = reject;
|
||||
xhr.open("POST", `/edge/api/v1/upload`);
|
||||
xhr.send(formData);
|
||||
resolve(result);
|
||||
});
|
||||
}
|
||||
blobUrl(bucket, blobId) {
|
||||
return `/edge/api/v1/download/${bucket}/${blobId}`;
|
||||
}
|
||||
externalUrl(url) {
|
||||
return `/edge/api/v1/fetch?url=${encodeURIComponent(url)}`;
|
||||
}
|
||||
};
|
||||
|
||||
// pkg/sdk/client/src/crossframe-messenger.ts
|
||||
var CrossFrameMessenger = class extends EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
this.debug = false;
|
||||
this._initResizeObserver();
|
||||
this._initTitleMutationObserver();
|
||||
this._handleWindowMessage = this._handleWindowMessage.bind(this);
|
||||
window.addEventListener("message", this._handleWindowMessage);
|
||||
}
|
||||
post(message, target = window.parent) {
|
||||
if (!target)
|
||||
return;
|
||||
this._log("sending crossframe message", message);
|
||||
target.postMessage(message);
|
||||
}
|
||||
_log(...args) {
|
||||
if (!this.debug)
|
||||
return;
|
||||
console.log(...args);
|
||||
}
|
||||
_handleWindowMessage(evt) {
|
||||
const message = evt.data;
|
||||
const event = new CustomEvent(message.type, {
|
||||
cancelable: true,
|
||||
detail: message.data
|
||||
});
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
_initTitleMutationObserver() {
|
||||
const titleObserver = new MutationObserver((mutations) => {
|
||||
const title2 = mutations[0].target.textContent;
|
||||
this.post({ type: "title_changed" /* TITLE_CHANGED */, data: { title: title2 } });
|
||||
});
|
||||
const title = document.querySelector("title");
|
||||
if (!title)
|
||||
return;
|
||||
this.post({ type: "title_changed" /* TITLE_CHANGED */, data: { title: title.textContent } });
|
||||
titleObserver.observe(title, { subtree: true, characterData: true, childList: true });
|
||||
}
|
||||
_initResizeObserver() {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const body = document.body, html = document.documentElement;
|
||||
const height = Math.max(
|
||||
body.scrollHeight,
|
||||
body.offsetHeight,
|
||||
html.clientHeight,
|
||||
html.scrollHeight,
|
||||
html.offsetHeight
|
||||
);
|
||||
const width = Math.max(
|
||||
body.scrollWidth,
|
||||
body.offsetWidth,
|
||||
html.clientWidth,
|
||||
html.scrollWidth,
|
||||
html.offsetWidth
|
||||
);
|
||||
this.post({ type: "size_changed" /* SIZE_CHANGED */, data: { height, width } });
|
||||
});
|
||||
resizeObserver.observe(document.body);
|
||||
}
|
||||
};
|
||||
|
||||
// pkg/sdk/client/src/index.ts
|
||||
var client = new Client();
|
||||
var crossFrameMessenger = new CrossFrameMessenger();
|
||||
return __toCommonJS(src_exports);
|
||||
})();
|
||||
EdgeFrame=Edge.crossFrameMessenger;Edge=Edge.client
|
||||
//# sourceMappingURL=client.js.map
|
7
pkg/sdk/client/dist/client.js.map
vendored
Normal file
7
pkg/sdk/client/dist/client.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,9 +1,10 @@
|
||||
import { EventTarget } from "./event-target";
|
||||
import { messageFrom,Message, TypeMessage } from "./message";
|
||||
import { messageFrom, Message, TypeMessage } from "./message";
|
||||
import { RPCError } from "./rpc-error";
|
||||
import SockJS from 'sockjs-client';
|
||||
|
||||
const EventTypeMessage = "message";
|
||||
const EdgeAuth = "edge-auth"
|
||||
|
||||
export class Client extends EventTarget {
|
||||
|
||||
@ -18,15 +19,16 @@ export class Client extends EventTarget {
|
||||
constructor(autoReconnect = true) {
|
||||
super();
|
||||
this._conn = null;
|
||||
|
||||
this._onConnectionClose = this._onConnectionClose.bind(this);
|
||||
this._onConnectionMessage = this._onConnectionMessage.bind(this);
|
||||
this._handleRPCResponse = this._handleRPCResponse.bind(this);
|
||||
|
||||
this._rpcID = 0;
|
||||
this._pendingRPC = {};
|
||||
this._queue = [];
|
||||
this._reconnectionDelay = 250;
|
||||
this._autoReconnect = autoReconnect;
|
||||
|
||||
this.debug = false;
|
||||
|
||||
this.connect = this.connect.bind(this);
|
||||
@ -35,12 +37,16 @@ export class Client extends EventTarget {
|
||||
this.send = this.send.bind(this);
|
||||
this.upload = this.upload.bind(this);
|
||||
|
||||
this.addEventListener("message", this._handleRPCResponse);
|
||||
this.addEventListener(EventTypeMessage, this._handleRPCResponse);
|
||||
}
|
||||
|
||||
connect(token = "") {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `//${document.location.host}/edge/sock?token=${token}`;
|
||||
if (token == "") {
|
||||
token = this._getAuthCookieToken()
|
||||
}
|
||||
|
||||
const url = `//${document.location.host}/edge/sock?${EdgeAuth}=${token}`;
|
||||
this._log("opening connection to", url);
|
||||
const conn: any = new SockJS(url);
|
||||
|
||||
@ -79,6 +85,17 @@ export class Client extends EventTarget {
|
||||
this._cleanupConnection();
|
||||
}
|
||||
|
||||
_getAuthCookieToken() {
|
||||
const cookie = document.cookie.split("; ")
|
||||
.find((row) => row.startsWith(EdgeAuth));
|
||||
|
||||
if (cookie) {
|
||||
return cookie.split("=")[1];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
_onConnectionMessage(evt) {
|
||||
const rawMessage = JSON.parse(evt.data);
|
||||
const message = messageFrom(rawMessage);
|
||||
@ -93,6 +110,7 @@ export class Client extends EventTarget {
|
||||
const { jsonrpc, id, error, result } = evt.detail;
|
||||
|
||||
if (jsonrpc !== '2.0' || id === undefined) return;
|
||||
if (!evt.detail.hasOwnProperty("error") && !evt.detail.hasOwnProperty("result")) return;
|
||||
|
||||
// Prevent additional handlers to catch this event
|
||||
evt.stopImmediatePropagation();
|
||||
@ -178,7 +196,7 @@ export class Client extends EventTarget {
|
||||
}
|
||||
|
||||
send(data) {
|
||||
const msg = new Message("message", data);
|
||||
const msg = new Message(TypeMessage, data);
|
||||
this._sendOrQueue(msg);
|
||||
}
|
||||
|
||||
@ -247,7 +265,11 @@ export class Client extends EventTarget {
|
||||
});
|
||||
}
|
||||
|
||||
blobUrl(bucket: string, blobId: string) {
|
||||
blobUrl(bucket: string, blobId: string): string {
|
||||
return `/edge/api/v1/download/${bucket}/${blobId}`;
|
||||
}
|
||||
|
||||
externalUrl(url: string): string {
|
||||
return `/edge/api/v1/fetch?url=${encodeURIComponent(url)}`
|
||||
}
|
||||
}
|
80
pkg/sdk/client/src/crossframe-messenger.ts
Normal file
80
pkg/sdk/client/src/crossframe-messenger.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { EventTarget } from "./event-target";
|
||||
|
||||
enum CrossFrameMessageType {
|
||||
SIZE_CHANGED = "size_changed",
|
||||
TITLE_CHANGED = "title_changed"
|
||||
}
|
||||
|
||||
interface CrossFrameMessage {
|
||||
type: CrossFrameMessageType
|
||||
data: { [key: string]: any }
|
||||
}
|
||||
|
||||
export class CrossFrameMessenger extends EventTarget {
|
||||
debug: boolean;
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.debug = false;
|
||||
|
||||
this._initResizeObserver();
|
||||
this._initTitleMutationObserver();
|
||||
|
||||
this._handleWindowMessage = this._handleWindowMessage.bind(this);
|
||||
|
||||
window.addEventListener('message', this._handleWindowMessage)
|
||||
}
|
||||
|
||||
post(message: CrossFrameMessage, target: Window = window.parent) {
|
||||
if (!target) return;
|
||||
this._log("sending crossframe message", message);
|
||||
target.postMessage(message)
|
||||
}
|
||||
|
||||
_log(...args) {
|
||||
if (!this.debug) return;
|
||||
console.log(...args);
|
||||
}
|
||||
|
||||
_handleWindowMessage(evt: MessageEvent) {
|
||||
const message = evt.data;
|
||||
const event = new CustomEvent(message.type, {
|
||||
cancelable: true,
|
||||
detail: message.data
|
||||
});
|
||||
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
_initTitleMutationObserver() {
|
||||
const titleObserver = new MutationObserver((mutations) => {
|
||||
const title = mutations[0].target.textContent;
|
||||
this.post({ type: CrossFrameMessageType.TITLE_CHANGED, data: { title }});
|
||||
});
|
||||
|
||||
const title = document.querySelector('title');
|
||||
|
||||
if (!title) return;
|
||||
|
||||
this.post({ type: CrossFrameMessageType.TITLE_CHANGED, data: { title: title.textContent }});
|
||||
|
||||
titleObserver.observe(title, { subtree: true, characterData: true, childList: true });
|
||||
}
|
||||
|
||||
_initResizeObserver() {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const body = document.body,
|
||||
html = document.documentElement;
|
||||
|
||||
const height = Math.max( body.scrollHeight, body.offsetHeight,
|
||||
html.clientHeight, html.scrollHeight, html.offsetHeight );
|
||||
|
||||
const width = Math.max( body.scrollWidth, body.offsetWidth,
|
||||
html.clientWidth, html.scrollWidth, html.offsetWidth );
|
||||
|
||||
this.post({ type: CrossFrameMessageType.SIZE_CHANGED, data: { height, width }});
|
||||
});
|
||||
|
||||
resizeObserver.observe(document.body);
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { Client } from './client.js';
|
||||
import { CrossFrameMessenger } from './crossframe-messenger.js';
|
||||
|
||||
export default new Client();
|
||||
export const client = new Client();
|
||||
export const crossFrameMessenger = new CrossFrameMessenger();
|
@ -7,35 +7,35 @@ const (
|
||||
OrderDirectionDesc OrderDirection = "DESC"
|
||||
)
|
||||
|
||||
type QueryOption struct {
|
||||
type QueryOptions struct {
|
||||
Limit *int
|
||||
Offset *int
|
||||
OrderBy *string
|
||||
OrderDirection *OrderDirection
|
||||
}
|
||||
|
||||
type QueryOptionFunc func(o *QueryOption)
|
||||
type QueryOptionFunc func(o *QueryOptions)
|
||||
|
||||
func WithLimit(limit int) QueryOptionFunc {
|
||||
return func(o *QueryOption) {
|
||||
return func(o *QueryOptions) {
|
||||
o.Limit = &limit
|
||||
}
|
||||
}
|
||||
|
||||
func WithOffset(offset int) QueryOptionFunc {
|
||||
return func(o *QueryOption) {
|
||||
return func(o *QueryOptions) {
|
||||
o.Offset = &offset
|
||||
}
|
||||
}
|
||||
|
||||
func WithOrderBy(orderBy string) QueryOptionFunc {
|
||||
return func(o *QueryOption) {
|
||||
return func(o *QueryOptions) {
|
||||
o.OrderBy = &orderBy
|
||||
}
|
||||
}
|
||||
|
||||
func WithOrderDirection(direction OrderDirection) QueryOptionFunc {
|
||||
return func(o *QueryOption) {
|
||||
return func(o *QueryOptions) {
|
||||
o.OrderDirection = &direction
|
||||
}
|
||||
}
|
@ -35,6 +35,10 @@ func (b *BlobBucket) Size(ctx context.Context) (int64, error) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := row.Err(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
size = nullSize.Int64
|
||||
|
||||
return nil
|
||||
@ -68,8 +72,11 @@ func (b *BlobBucket) Close() error {
|
||||
func (b *BlobBucket) Delete(ctx context.Context, id storage.BlobID) error {
|
||||
err := b.withTx(ctx, func(tx *sql.Tx) error {
|
||||
query := `DELETE FROM blobs WHERE bucket = $1 AND id = $2`
|
||||
args := []any{b.name, id}
|
||||
|
||||
if _, err := tx.ExecContext(ctx, query, b.name, id); err != nil {
|
||||
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
|
||||
|
||||
if _, err := tx.ExecContext(ctx, query, args...); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
@ -88,7 +95,11 @@ func (b *BlobBucket) Get(ctx context.Context, id storage.BlobID) (storage.BlobIn
|
||||
|
||||
err := b.withTx(ctx, func(tx *sql.Tx) error {
|
||||
query := `SELECT content_type, mod_time, size FROM blobs WHERE bucket = $1 AND id = $2`
|
||||
row := tx.QueryRowContext(ctx, query, b.name, id)
|
||||
args := []any{b.name, id}
|
||||
|
||||
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
|
||||
|
||||
row := tx.QueryRowContext(ctx, query, args...)
|
||||
|
||||
var (
|
||||
contentType string
|
||||
@ -104,6 +115,10 @@ func (b *BlobBucket) Get(ctx context.Context, id storage.BlobID) (storage.BlobIn
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := row.Err(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
blobInfo = &BlobInfo{
|
||||
id: id,
|
||||
bucket: b.name,
|
||||
@ -127,12 +142,21 @@ func (b *BlobBucket) List(ctx context.Context) ([]storage.BlobInfo, error) {
|
||||
|
||||
err := b.withTx(ctx, func(tx *sql.Tx) error {
|
||||
query := `SELECT id, content_type, mod_time, size FROM blobs WHERE bucket = $1`
|
||||
args := []any{b.name}
|
||||
|
||||
rows, err := tx.QueryContext(ctx, query, b.name)
|
||||
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
|
||||
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close rows", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
blobs = make([]storage.BlobInfo, 0)
|
||||
|
||||
for rows.Next() {
|
||||
@ -229,7 +253,12 @@ type blobWriterCloser struct {
|
||||
|
||||
// Write implements io.WriteCloser
|
||||
func (wbc *blobWriterCloser) Write(p []byte) (int, error) {
|
||||
logger.Debug(context.Background(), "writing data to blob", logger.F("data", p))
|
||||
logger.Debug(
|
||||
context.Background(), "writing data to blob",
|
||||
logger.F("size", len(p)),
|
||||
logger.F("blobID", wbc.id),
|
||||
logger.F("bucket", wbc.bucket),
|
||||
)
|
||||
|
||||
n, err := wbc.buf.Write(p)
|
||||
if err != nil {
|
||||
@ -266,14 +295,20 @@ func (wbc *blobWriterCloser) Close() error {
|
||||
mime := mimetype.Detect(data)
|
||||
modTime := time.Now().UTC()
|
||||
|
||||
_, err := tx.Exec(
|
||||
query,
|
||||
args := []any{
|
||||
wbc.bucket,
|
||||
wbc.id,
|
||||
data,
|
||||
mime.String(),
|
||||
modTime,
|
||||
len(data),
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "executing query", logger.F("query", query))
|
||||
|
||||
_, err := tx.Exec(
|
||||
query,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -36,7 +36,7 @@ func (s *BlobStore) ListBuckets(ctx context.Context) ([]string, error) {
|
||||
buckets := make([]string, 0)
|
||||
|
||||
err := s.withTx(ctx, func(tx *sql.Tx) error {
|
||||
query := `SELECT DISTINCT name FROM blobs`
|
||||
query := `SELECT DISTINCT bucket FROM blobs`
|
||||
rows, err := tx.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -1,8 +1,10 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/testsuite"
|
||||
"github.com/pkg/errors"
|
||||
@ -19,7 +21,8 @@ func TestBlobStore(t *testing.T) {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
store := NewBlobStore(file)
|
||||
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||
store := NewBlobStore(dsn)
|
||||
|
||||
testsuite.TestBlobStore(t, store)
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sync"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
@ -17,10 +17,7 @@ import (
|
||||
)
|
||||
|
||||
type DocumentStore struct {
|
||||
db *sql.DB
|
||||
path string
|
||||
openOnce sync.Once
|
||||
mutex sync.RWMutex
|
||||
getDB getDBFunc
|
||||
}
|
||||
|
||||
// Delete implements storage.DocumentStore
|
||||
@ -73,6 +70,10 @@ func (s *DocumentStore) Get(ctx context.Context, collection string, id storage.D
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := row.Err(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
document = storage.Document(data)
|
||||
|
||||
document[storage.DocumentAttrID] = id
|
||||
@ -90,6 +91,11 @@ func (s *DocumentStore) Get(ctx context.Context, collection string, id storage.D
|
||||
|
||||
// Query implements storage.DocumentStore
|
||||
func (s *DocumentStore) Query(ctx context.Context, collection string, filter *filter.Filter, funcs ...storage.QueryOptionFunc) ([]storage.Document, error) {
|
||||
opts := &storage.QueryOptions{}
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
var documents []storage.Document
|
||||
|
||||
err := s.withTx(ctx, func(tx *sql.Tx) error {
|
||||
@ -120,6 +126,29 @@ func (s *DocumentStore) Query(ctx context.Context, collection string, filter *fi
|
||||
|
||||
args = append([]interface{}{collection}, args...)
|
||||
|
||||
if opts.OrderBy != nil {
|
||||
direction := storage.OrderDirectionAsc
|
||||
if opts.OrderDirection != nil {
|
||||
direction = *opts.OrderDirection
|
||||
}
|
||||
|
||||
query, args = withOrderByClause(query, args, *opts.OrderBy, direction)
|
||||
}
|
||||
|
||||
if opts.Offset != nil || opts.Limit != nil {
|
||||
offset := 0
|
||||
if opts.Offset != nil {
|
||||
offset = *opts.Offset
|
||||
}
|
||||
|
||||
limit := math.MaxInt
|
||||
if opts.Limit != nil {
|
||||
limit = *opts.Limit
|
||||
}
|
||||
|
||||
query, args = withLimitOffsetClause(query, args, limit, offset)
|
||||
}
|
||||
|
||||
logger.Debug(
|
||||
ctx, "executing query",
|
||||
logger.F("query", query),
|
||||
@ -131,7 +160,11 @@ func (s *DocumentStore) Query(ctx context.Context, collection string, filter *fi
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close rows", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
documents = make([]storage.Document, 0)
|
||||
|
||||
@ -209,6 +242,10 @@ func (s *DocumentStore) Upsert(ctx context.Context, collection string, document
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := row.Err(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
upsertedDocument = storage.Document(data)
|
||||
|
||||
upsertedDocument[storage.DocumentAttrID] = id
|
||||
@ -227,7 +264,7 @@ func (s *DocumentStore) Upsert(ctx context.Context, collection string, document
|
||||
func (s *DocumentStore) withTx(ctx context.Context, fn func(tx *sql.Tx) error) error {
|
||||
var db *sql.DB
|
||||
|
||||
db, err := s.getDatabase(ctx)
|
||||
db, err := s.getDB(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@ -239,67 +276,7 @@ func (s *DocumentStore) withTx(ctx context.Context, fn func(tx *sql.Tx) error) e
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DocumentStore) getDatabase(ctx context.Context) (*sql.DB, error) {
|
||||
s.mutex.RLock()
|
||||
if s.db != nil {
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
var err error
|
||||
|
||||
s.openOnce.Do(func() {
|
||||
if err = s.ensureTables(ctx, s.db); err != nil {
|
||||
err = errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return s.db, nil
|
||||
}
|
||||
|
||||
s.mutex.RUnlock()
|
||||
|
||||
var (
|
||||
db *sql.DB
|
||||
err error
|
||||
)
|
||||
|
||||
s.openOnce.Do(func() {
|
||||
db, err = sql.Open("sqlite", s.path)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.ensureTables(ctx, db); err != nil {
|
||||
err = errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if db != nil {
|
||||
s.mutex.Lock()
|
||||
s.db = db
|
||||
s.mutex.Unlock()
|
||||
}
|
||||
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
return s.db, nil
|
||||
}
|
||||
|
||||
func (s *DocumentStore) ensureTables(ctx context.Context, db *sql.DB) error {
|
||||
func ensureTables(ctx context.Context, db *sql.DB) error {
|
||||
err := withTx(ctx, db, func(tx *sql.Tx) error {
|
||||
query := `
|
||||
CREATE TABLE IF NOT EXISTS documents (
|
||||
@ -331,19 +308,54 @@ func (s *DocumentStore) ensureTables(ctx context.Context, db *sql.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func withOrderByClause(query string, args []any, orderBy string, orderDirection storage.OrderDirection) (string, []any) {
|
||||
direction := "ASC"
|
||||
if orderDirection == storage.OrderDirectionDesc {
|
||||
direction = "DESC"
|
||||
}
|
||||
|
||||
var column string
|
||||
|
||||
switch orderBy {
|
||||
case storage.DocumentAttrID:
|
||||
column = "id"
|
||||
|
||||
case storage.DocumentAttrCreatedAt:
|
||||
column = "created_at"
|
||||
|
||||
case storage.DocumentAttrUpdatedAt:
|
||||
column = "updated_at"
|
||||
|
||||
default:
|
||||
column = fmt.Sprintf("json_extract(data, '$.' || $%d)", len(args)+1)
|
||||
args = append(args, orderBy)
|
||||
}
|
||||
|
||||
query += fmt.Sprintf(` ORDER BY %s %s`, column, direction)
|
||||
|
||||
return query, args
|
||||
}
|
||||
|
||||
func withLimitOffsetClause(query string, args []any, limit int, offset int) (string, []any) {
|
||||
query += fmt.Sprintf(` LIMIT $%d OFFSET $%d`, len(args)+1, len(args)+2)
|
||||
args = append(args, limit, offset)
|
||||
|
||||
return query, args
|
||||
}
|
||||
|
||||
func NewDocumentStore(path string) *DocumentStore {
|
||||
getDB := newGetDBFunc(path, ensureTables)
|
||||
|
||||
return &DocumentStore{
|
||||
db: nil,
|
||||
path: path,
|
||||
openOnce: sync.Once{},
|
||||
getDB: getDB,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDocumentStoreWithDB(db *sql.DB) *DocumentStore {
|
||||
getDB := newGetDBFuncFromDB(db, ensureTables)
|
||||
|
||||
return &DocumentStore{
|
||||
db: db,
|
||||
path: "",
|
||||
openOnce: sync.Once{},
|
||||
getDB: getDB,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/testsuite"
|
||||
"github.com/pkg/errors"
|
||||
@ -10,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
func TestDocumentStore(t *testing.T) {
|
||||
// t.Parallel()
|
||||
t.Parallel()
|
||||
logger.SetLevel(logger.LevelDebug)
|
||||
|
||||
file := "./testdata/documentstore_test.sqlite"
|
||||
@ -19,7 +21,8 @@ func TestDocumentStore(t *testing.T) {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
store := NewDocumentStore(file)
|
||||
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||
store := NewDocumentStore(dsn)
|
||||
|
||||
testsuite.TestDocumentStore(t, store)
|
||||
}
|
||||
|
@ -7,8 +7,21 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
"modernc.org/sqlite"
|
||||
_ "modernc.org/sqlite"
|
||||
sqlite3 "modernc.org/sqlite/lib"
|
||||
)
|
||||
|
||||
func Open(path string) (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not open database with path '%s'", path)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func withTx(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) error {
|
||||
var tx *sql.Tx
|
||||
|
||||
@ -27,10 +40,29 @@ func withTx(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) error {
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
if err = fn(tx); err != nil {
|
||||
var sqlErr *sqlite.Error
|
||||
if errors.As(err, &sqlErr) {
|
||||
if sqlErr.Code() == sqlite3.SQLITE_BUSY {
|
||||
logger.Warn(ctx, "database busy, retrying transaction")
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
logger.Error(ctx, "could not execute transaction", logger.E(errors.WithStack(err)))
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
2
pkg/storage/sqlite/testdata/.gitignore
vendored
2
pkg/storage/sqlite/testdata/.gitignore
vendored
@ -1 +1 @@
|
||||
/*.sqlite
|
||||
/*.sqlite*
|
@ -8,7 +8,7 @@ import (
|
||||
|
||||
func TestBlobStore(t *testing.T, store storage.BlobStore) {
|
||||
t.Run("Ops", func(t *testing.T) {
|
||||
// t.Parallel()
|
||||
t.Parallel()
|
||||
testBlobStoreOps(t, store)
|
||||
})
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
|
||||
func TestDocumentStore(t *testing.T, store storage.DocumentStore) {
|
||||
t.Run("Ops", func(t *testing.T) {
|
||||
// t.Parallel()
|
||||
t.Parallel()
|
||||
testDocumentStoreOps(t, store)
|
||||
})
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ var documentStoreOpsTestCases = []documentStoreOpsTestCase{
|
||||
}),
|
||||
)
|
||||
|
||||
results, err := store.Query(ctx, collection, filter, nil)
|
||||
results, err := store.Query(ctx, collection, filter)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@ -82,7 +82,7 @@ var documentStoreOpsTestCases = []documentStoreOpsTestCase{
|
||||
}),
|
||||
)
|
||||
|
||||
results, err := store.Query(ctx, collection, filter, nil)
|
||||
results, err := store.Query(ctx, collection, filter)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@ -127,7 +127,7 @@ var documentStoreOpsTestCases = []documentStoreOpsTestCase{
|
||||
),
|
||||
)
|
||||
|
||||
results, err := store.Query(ctx, collection, filter, nil)
|
||||
results, err := store.Query(ctx, collection, filter)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@ -219,7 +219,7 @@ var documentStoreOpsTestCases = []documentStoreOpsTestCase{
|
||||
|
||||
// Verify that there is no additional created document in the collection
|
||||
|
||||
results, err := store.Query(ctx, collection, nil, nil)
|
||||
results, err := store.Query(ctx, collection, nil)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@ -228,6 +228,206 @@ var documentStoreOpsTestCases = []documentStoreOpsTestCase{
|
||||
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Query order by document field",
|
||||
Run: func(ctx context.Context, store storage.DocumentStore) error {
|
||||
docs := []storage.Document{
|
||||
{
|
||||
"sortedField": 0,
|
||||
"name": "Item 1",
|
||||
},
|
||||
{
|
||||
"sortedField": 1,
|
||||
"name": "Item 2",
|
||||
},
|
||||
{
|
||||
"sortedField": 2,
|
||||
"name": "Item 3",
|
||||
},
|
||||
}
|
||||
|
||||
collection := "ordered_query_by_document_field"
|
||||
|
||||
for _, doc := range docs {
|
||||
if _, err := store.Upsert(ctx, collection, doc); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
results, err := store.Query(
|
||||
ctx, collection, nil,
|
||||
storage.WithOrderBy("sortedField"),
|
||||
storage.WithOrderDirection(storage.OrderDirectionAsc),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := 3, len(results); e != g {
|
||||
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := docs[0]["name"], results[0]["name"]; e != g {
|
||||
return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := docs[2]["name"], results[2]["name"]; e != g {
|
||||
return errors.Errorf("results[2][\"name\"]: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
results, err = store.Query(
|
||||
ctx, collection, nil,
|
||||
storage.WithOrderBy("sortedField"),
|
||||
storage.WithOrderDirection(storage.OrderDirectionDesc),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := 3, len(results); e != g {
|
||||
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := docs[2]["name"], results[0]["name"]; e != g {
|
||||
return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := docs[0]["name"], results[2]["name"]; e != g {
|
||||
return errors.Errorf("results[2][\"name\"]: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Query order by special attr",
|
||||
Run: func(ctx context.Context, store storage.DocumentStore) error {
|
||||
docs := []storage.Document{
|
||||
{
|
||||
"name": "Item 1",
|
||||
},
|
||||
{
|
||||
"name": "Item 2",
|
||||
},
|
||||
{
|
||||
"name": "Item 3",
|
||||
},
|
||||
}
|
||||
|
||||
collection := "ordered_query_by_special_attr"
|
||||
|
||||
for _, doc := range docs {
|
||||
if _, err := store.Upsert(ctx, collection, doc); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
results, err := store.Query(
|
||||
ctx, collection, nil,
|
||||
storage.WithOrderBy(storage.DocumentAttrCreatedAt),
|
||||
storage.WithOrderDirection(storage.OrderDirectionAsc),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := 3, len(results); e != g {
|
||||
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := docs[0]["name"], results[0]["name"]; e != g {
|
||||
return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := docs[2]["name"], results[2]["name"]; e != g {
|
||||
return errors.Errorf("results[2][\"name\"]: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
results, err = store.Query(
|
||||
ctx, collection, nil,
|
||||
storage.WithOrderBy(storage.DocumentAttrCreatedAt),
|
||||
storage.WithOrderDirection(storage.OrderDirectionDesc),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := 3, len(results); e != g {
|
||||
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := docs[2]["name"], results[0]["name"]; e != g {
|
||||
return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := docs[0]["name"], results[2]["name"]; e != g {
|
||||
return errors.Errorf("results[2][\"name\"]: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Query limit and offset",
|
||||
Run: func(ctx context.Context, store storage.DocumentStore) error {
|
||||
docs := []storage.Document{
|
||||
{"name": "Item 1"},
|
||||
{"name": "Item 2"},
|
||||
{"name": "Item 3"},
|
||||
{"name": "Item 4"},
|
||||
}
|
||||
|
||||
collection := "query_limit_and_offset"
|
||||
|
||||
for _, doc := range docs {
|
||||
if _, err := store.Upsert(ctx, collection, doc); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
results, err := store.Query(
|
||||
ctx, collection, nil,
|
||||
storage.WithLimit(2),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := 2, len(results); e != g {
|
||||
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := docs[0]["name"], results[0]["name"]; e != g {
|
||||
return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := docs[1]["name"], results[1]["name"]; e != g {
|
||||
return errors.Errorf("results[1][\"name\"]: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
results, err = store.Query(
|
||||
ctx, collection, nil,
|
||||
storage.WithOffset(2),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := 2, len(results); e != g {
|
||||
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := docs[2]["name"], results[0]["name"]; e != g {
|
||||
return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := docs[3]["name"], results[1]["name"]; e != g {
|
||||
return errors.Errorf("results[1][\"name\"]: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@ -237,6 +437,7 @@ func testDocumentStoreOps(t *testing.T, store storage.DocumentStore) {
|
||||
for _, tc := range documentStoreOpsTestCases {
|
||||
func(tc documentStoreOpsTestCase) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if err := tc.Run(context.Background(), store); err != nil {
|
||||
t.Errorf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
Reference in New Issue
Block a user