From de9d3bfb6519691c72d87259f1ab7ef4ea5846e6 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Sun, 16 Feb 2025 21:10:56 +0800 Subject: [PATCH 01/14] Fixed netstat underflow bug - Fixed netstat sometime underflow to a large negative number bug --- src/mod/netstat/netstat.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mod/netstat/netstat.go b/src/mod/netstat/netstat.go index 078c1f6..67724c1 100644 --- a/src/mod/netstat/netstat.go +++ b/src/mod/netstat/netstat.go @@ -212,6 +212,10 @@ func (n *NetStatBuffers) GetNetworkInterfaceStats() (int64, int64, error) { totalTx += counter.BytesSent } - // Convert bytes to bits + // Convert bytes to bits with overflow check + const maxInt64 = int64(^uint64(0) >> 1) + if totalRx*8 > uint64(maxInt64) || totalTx*8 > uint64(maxInt64) { + return 0, 0, errors.New("overflow detected when converting uint64 to int64") + } return int64(totalRx * 8), int64(totalTx * 8), nil } From 1116b643b5bd9a9566f83703a067d01a6eaf8f7e Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Wed, 19 Feb 2025 21:25:50 +0800 Subject: [PATCH 02/14] Added plugin interface definations - Added wip plugin interface - Merged in PR for lego update - Minor code optimization --- src/go.mod | 115 +++++------ src/go.sum | 298 ++++++++++++--------------- src/mod/dynamicproxy/dynamicproxy.go | 2 +- src/mod/plugins/includes.go | 118 +++++++++++ src/mod/plugins/introspect.go | 54 +++++ src/mod/plugins/lifecycle.go | 67 ++++++ src/mod/plugins/plugins.go | 117 +++++++++++ src/mod/plugins/utils.go | 62 ++++++ src/reverseproxy.go | 10 +- 9 files changed, 620 insertions(+), 223 deletions(-) create mode 100644 src/mod/plugins/includes.go create mode 100644 src/mod/plugins/introspect.go create mode 100644 src/mod/plugins/lifecycle.go create mode 100644 src/mod/plugins/plugins.go create mode 100644 src/mod/plugins/utils.go diff --git a/src/go.mod b/src/go.mod index 9ee7ede..76762b1 100644 --- a/src/go.mod +++ b/src/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.2 require ( github.com/boltdb/bolt v1.3.1 github.com/docker/docker v27.0.0+incompatible - github.com/go-acme/lego/v4 v4.19.2 + github.com/go-acme/lego/v4 v4.21.0 github.com/go-ping/ping v1.1.0 github.com/go-session/session v3.1.2+incompatible github.com/google/uuid v1.6.0 @@ -18,14 +18,14 @@ require ( github.com/microcosm-cc/bluemonday v1.0.26 github.com/shirou/gopsutil/v4 v4.25.1 github.com/syndtr/goleveldb v1.0.0 - golang.org/x/net v0.29.0 + golang.org/x/net v0.33.0 golang.org/x/sys v0.28.0 - golang.org/x/text v0.18.0 + golang.org/x/text v0.21.0 ) require ( - cloud.google.com/go/auth v0.9.3 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect + cloud.google.com/go/auth v0.13.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect github.com/benbjohnson/clock v1.3.0 // indirect github.com/ebitengine/purego v0.8.2 // indirect @@ -33,8 +33,9 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/snappy v0.0.1 // indirect - github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 // indirect + github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect + github.com/peterhellberg/link v1.2.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect @@ -52,14 +53,14 @@ require ( ) require ( - cloud.google.com/go/compute/metadata v0.5.1 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.29 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect @@ -72,28 +73,28 @@ require ( github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/Microsoft/go-winio v0.4.14 // indirect github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect - github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 // indirect - github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect - github.com/aws/aws-sdk-go-v2/config v1.27.33 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect + github.com/aliyun/alibaba-cloud-sdk-go v1.63.72 // indirect + github.com/aws/aws-sdk-go-v2 v1.32.7 // indirect + github.com/aws/aws-sdk-go-v2/config v1.28.7 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.48 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect - github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6 // indirect - github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect - github.com/aws/smithy-go v1.20.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 // indirect + github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.8 // indirect + github.com/aws/aws-sdk-go-v2/service/route53 v1.46.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 // indirect + github.com/aws/smithy-go v1.22.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/civo/civogo v0.3.11 // indirect - github.com/cloudflare/cloudflare-go v0.104.0 // indirect + github.com/cloudflare/cloudflare-go v0.112.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dimchansky/utfbom v1.1.1 // indirect @@ -103,25 +104,24 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-errors/errors v1.0.1 // indirect github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-oauth2/oauth2/v4 v4.5.2 - github.com/go-resty/resty/v2 v2.13.1 // indirect - github.com/go-viper/mapstructure/v2 v2.1.0 // indirect - github.com/goccy/go-json v0.10.3 // indirect + github.com/go-resty/resty/v2 v2.16.2 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-json v0.10.4 // indirect github.com/gofrs/uuid v4.4.0+incompatible github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect - github.com/googleapis/gax-go/v2 v2.13.0 // indirect - github.com/gophercloud/gophercloud v1.14.0 // indirect + github.com/googleapis/gax-go/v2 v2.14.0 // indirect + github.com/gophercloud/gophercloud v1.14.1 // indirect github.com/gorilla/csrf v1.7.2 github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect @@ -138,7 +138,7 @@ require ( github.com/kylelemons/godebug v1.1.0 // indirect github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect github.com/labbsr0x/goh v1.0.1 // indirect - github.com/linode/linodego v1.40.0 // indirect + github.com/linode/linodego v1.44.0 // indirect github.com/liquidweb/liquidweb-cli v0.6.9 // indirect github.com/liquidweb/liquidweb-go v1.6.4 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -154,9 +154,9 @@ require ( github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect github.com/nrdcg/auroradns v1.1.0 // indirect github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect - github.com/nrdcg/desec v0.8.0 // indirect + github.com/nrdcg/desec v0.10.0 // indirect github.com/nrdcg/dnspod-go v0.4.0 // indirect - github.com/nrdcg/freemyip v0.2.0 // indirect + github.com/nrdcg/freemyip v0.3.0 // indirect github.com/nrdcg/goinwx v0.10.0 // indirect github.com/nrdcg/mailinabox v0.2.0 // indirect github.com/nrdcg/namesilo v0.2.1 // indirect @@ -172,24 +172,23 @@ require ( github.com/pquerna/otp v1.4.0 // indirect github.com/sacloud/api-client-go v0.2.10 // indirect github.com/sacloud/go-http v0.1.8 // indirect - github.com/sacloud/iaas-api-go v1.12.0 // indirect + github.com/sacloud/iaas-api-go v1.14.0 // indirect github.com/sacloud/packages-go v0.0.10 // indirect github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect - github.com/softlayer/softlayer-go v1.1.5 // indirect + github.com/softlayer/softlayer-go v1.1.7 // indirect github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect github.com/spf13/cast v1.6.0 // indirect github.com/stretchr/testify v1.10.0 // indirect - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 // indirect - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 // indirect github.com/transip/gotransip/v6 v6.26.0 // indirect - github.com/ultradns/ultradns-go-sdk v1.7.0-20240913052650-970ca9a // indirect + github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect github.com/vinyldns/go-vinyldns v0.9.16 // indirect github.com/xlzd/gotp v0.1.0 - github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2 // indirect - github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5 // indirect - go.opencensus.io v0.24.0 // indirect + github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c // indirect + github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect go.opentelemetry.io/otel v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect @@ -197,20 +196,20 @@ require ( go.opentelemetry.io/otel/sdk v1.28.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect go.uber.org/ratelimit v0.3.0 // indirect - golang.org/x/crypto v0.27.0 // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/time v0.6.0 // indirect - golang.org/x/tools v0.25.0 // indirect - google.golang.org/api v0.197.0 // indirect - google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect - google.golang.org/grpc v1.66.1 // indirect - google.golang.org/protobuf v1.34.2 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/time v0.8.0 // indirect + golang.org/x/tools v0.28.0 // indirect + google.golang.org/api v0.214.0 // indirect + google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/ns1/ns1-go.v2 v2.12.0 // indirect + gopkg.in/ns1/ns1-go.v2 v2.13.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.1 // indirect diff --git a/src/go.sum b/src/go.sum index fcef8ae..d1a5dde 100644 --- a/src/go.sum +++ b/src/go.sum @@ -5,13 +5,13 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U= -cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk= -cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= -cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= +cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs= +cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q= +cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU= +cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/compute/metadata v0.5.1 h1:NM6oZeZNlYjiwYje+sYFjEpP0Q0zCan1bmQW/KmIrGs= -cloud.google.com/go/compute/metadata v0.5.1/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= @@ -21,22 +21,24 @@ github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYs github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0 h1:9Eih8XcEeQnFD0ntMlUDleKMzfeCeUfa+VbnDCI4AZs= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0/go.mod h1:wGPyTi+aURdqPAGMZDQqnNs9IrShADF8w2WZb6bKeq0= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 h1:yzrctSl9GMIQ5lHu7jc8olOsGjWDCsBpJhWqfGa/YIM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0/go.mod h1:GE4m0rnnfwLGX0Y9A9A25Zx5N/90jneT5ABevqzhuFQ= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 h1:zLzoX5+W2l95UJoVwiyNS4dX8vHyQ6x2xRLoBBL9wMk= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 h1:7CBQ+Ei8SP2c6ydQTGCCrS35bDxgTMfoP2miAwK++OU= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1/go.mod h1:c/wcGeGx5FUPbM/JltUYHZcKmigwyVLJlDq+4HdtXaw= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= @@ -62,6 +64,8 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -77,44 +81,43 @@ github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 h1:r2uwBUQhLhcPzaWz9tRJqc8MjYwHb+oF2+Q6467BF14= -github.com/aliyun/alibaba-cloud-sdk-go v1.63.15/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.72 h1:HvFZUzEbNvfe8F2Mg0wBGv90bPhWDxgVtDHR5zoBOU0= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.72/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g= -github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= -github.com/aws/aws-sdk-go-v2/config v1.27.33 h1:Nof9o/MsmH4oa0s2q9a0k7tMz5x/Yj5k06lDODWz3BU= -github.com/aws/aws-sdk-go-v2/config v1.27.33/go.mod h1:kEqdYzRb8dd8Sy2pOdEbExTTF5v7ozEXX0McgPE7xks= -github.com/aws/aws-sdk-go-v2/credentials v1.17.32 h1:7Cxhp/BnT2RcGy4VisJ9miUPecY+lyE9I8JvcZofn9I= -github.com/aws/aws-sdk-go-v2/credentials v1.17.32/go.mod h1:P5/QMF3/DCHbXGEGkdbilXHsyTBX5D3HSwcrSc9p20I= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU= +github.com/aws/aws-sdk-go-v2 v1.32.7 h1:ky5o35oENWi0JYWUZkB7WYvVPP+bcRF5/Iq7JWSb5Rw= +github.com/aws/aws-sdk-go-v2 v1.32.7/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2/config v1.28.7 h1:GduUnoTXlhkgnxTD93g1nv4tVPILbdNQOzav+Wpg7AE= +github.com/aws/aws-sdk-go-v2/config v1.28.7/go.mod h1:vZGX6GVkIE8uECSUHB6MWAUsd4ZcG2Yq/dMa4refR3M= +github.com/aws/aws-sdk-go-v2/credentials v1.17.48 h1:IYdLD1qTJ0zanRavulofmqut4afs45mOWEI+MzZtTfQ= +github.com/aws/aws-sdk-go-v2/credentials v1.17.48/go.mod h1:tOscxHN3CGmuX9idQ3+qbkzrjVIx32lqDSU1/0d/qXs= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 h1:kqOrpojG71DxJm/KDPO+Z/y1phm1JlC8/iT+5XRmAn8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22/go.mod h1:NtSFajXVVL8TA2QNngagVZmUtXciyrHOt7xgz4faS/M= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 h1:I/5wmGMffY4happ8NOCuIUEWGUvvFp5NSeQcXl9RHcI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26/go.mod h1:FR8f4turZtNy6baO0KJ5FJUmXH/cSkI9fOngs0yl6mA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 h1:zXFLuEuMMUOvEARXFUVJdfqZ4bvvSgdGRq/ATcrQxzM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26/go.mod h1:3o2Wpy0bogG1kyOPrgkXA8pgIfEEv0+m19O9D5+W8y8= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6 h1:ea6TO3HgVeVTB2Ie1djyBFWBOc9CohpKbo/QZbGTCJQ= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6/go.mod h1:D2TUTD3v6AWmE5LzdCXLWNFtoYbSf6IEjKh1ggbuVdw= -github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 h1:957e1/SwXIfPi/0OUJkH9YnPZRe9G6Kisd/xUhF7AUE= -github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2/go.mod h1:343vcjcyOTuHTBBgUrOxPM36/jE96qLZnGL447ldrB0= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 h1:pIaGg+08llrP7Q5aiz9ICWbY8cqhTkyy+0SHvfzQpTc= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.7/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 h1:/Cfdu0XV3mONYKaOt1Gr0k1KvQzkzPyiKUdlWJqy+J4= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 h1:NKTa1eqZYw8tiHSRGpP0VtTdub/8KNk8sDkNPFaOKDE= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.7/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o= -github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= -github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 h1:8eUsivBQzZHqe/3FE+cqwfH+0p5Jo8PFM/QYQSmeZ+M= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7/go.mod h1:kLPQvGUmxn/fqiCrDeohwG33bq2pQpGeY62yRO6Nrh0= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.8 h1:+lmJoqxuUoPlSfGk5JYQQivd9YFjUvRZR6RPY+Wcx48= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.8/go.mod h1:Gg8/myP4+rgRi4+j9gQdbOEnMtwMAUUIeXo+nKCFVj8= +github.com/aws/aws-sdk-go-v2/service/route53 v1.46.4 h1:0jMtawybbfpFEIMy4wvfyW2Z4YLr7mnuzT0fhR67Nrc= +github.com/aws/aws-sdk-go-v2/service/route53 v1.46.4/go.mod h1:xlMODgumb0Pp8bzfpojqelDrf8SL9rb5ovwmwKJl+oU= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 h1:CvuUmnXI7ebaUAhbJcDy9YQx8wHR69eZ9I7q5hszt/g= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.8/go.mod h1:XDeGv1opzwm8ubxddF0cgqkZWsyOtw4lr6dxwmb6YQg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 h1:F2rBfNAL5UyswqoeWv9zs74N/NanhK16ydHW1pahX6E= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7/go.mod h1:JfyQ0g2JG8+Krq0EuZNnRwX0mU0HrwY/tG6JNfcqh4k= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 h1:Xgv/hyNgvLda/M9l9qxXc4UFSgppnRczLxlMs5Ae/QY= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.3/go.mod h1:5Gn+d+VaaRgsjewpMvGazt0WfcFO+Md4wLOuBfGR9Bc= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= @@ -129,26 +132,24 @@ github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx2 github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= -github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/civo/civogo v0.3.11 h1:mON/fyrV946Sbk6paRtOSGsN+asCgCmHCgArf5xmGxM= github.com/civo/civogo v0.3.11/go.mod h1:7+GeeFwc4AYTULaEshpT2vIcl3Qq8HPoxA17viX3l6g= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/cloudflare-go v0.104.0 h1:R/lB0dZupaZbOgibAH/BRrkFbZ6Acn/WsKg2iX2xXuY= -github.com/cloudflare/cloudflare-go v0.104.0/go.mod h1:pfUQ4PIG4ISI0/Mmc21Bp86UnFU0ktmPf3iTgbSL+cM= +github.com/cloudflare/cloudflare-go v0.112.0 h1:caFwqXdGJCl3rjVMgbPEn8iCYAg9JsRYV3dIVQE5d7g= +github.com/cloudflare/cloudflare-go v0.112.0/go.mod h1:QB55kuJ5ZTeLNFcLJePfMuBilhu/LDKpLBmKFQIoSZ0= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -163,6 +164,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= @@ -179,11 +182,7 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -199,14 +198,14 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-acme/lego/v4 v4.19.2 h1:Y8hrmMvWETdqzzkRly7m98xtPJJivWFsgWi8fcvZo+Y= -github.com/go-acme/lego/v4 v4.19.2/go.mod h1:wtDe3dDkmV4/oI2nydpNXSJpvV10J9RCyZ6MbYxNtlQ= +github.com/go-acme/lego/v4 v4.21.0 h1:arEW+8o5p7VI8Bk1kr/PDlgD1DrxtTH1gJ4b7mehL8o= +github.com/go-acme/lego/v4 v4.21.0/go.mod h1:HrSWzm3Ckj45Ie3i+p1zKVobbQoMOaGu9m4up0dUeDI= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= @@ -227,20 +226,20 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw= github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= -github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g= -github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0= +github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg= +github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg= github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= -github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -252,18 +251,15 @@ github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzq github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -276,9 +272,7 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -290,9 +284,7 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= @@ -314,7 +306,6 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -323,10 +314,10 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gT github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= -github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= -github.com/gophercloud/gophercloud v1.14.0 h1:Bt9zQDhPrbd4qX7EILGmy+i7GP35cc+AAL2+wIJpUE8= -github.com/gophercloud/gophercloud v1.14.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= +github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o= +github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= +github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw= +github.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI= @@ -345,9 +336,8 @@ github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6 github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0 h1:bM6ZAFZmc/wPFaRDi0d5L7hGEZEx/2u+Tmr2evNHDiI= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= @@ -380,8 +370,8 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 h1:X3E16S6AUZsQKhJIQ5kNnylnp0GtSy2YhIbxfvDavtU= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114/go.mod h1:JWz2ujO9X3oU5wb6kXp+DpR2UuDj2SldDbX8T0FSuhI= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 h1:kQ2Agpfy7Ze1ajn9xCQG9G6T7XIbqv+FBDS/U98W9Mk= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128/go.mod h1:JWz2ujO9X3oU5wb6kXp+DpR2UuDj2SldDbX8T0FSuhI= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df h1:MZf03xP9WdakyXhOWuAD5uPK3wHh96wCsqe3hCMKh8E= github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4= @@ -410,6 +400,8 @@ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -438,8 +430,8 @@ github.com/likexian/gokit v0.25.13 h1:p2Uw3+6fGG53CwdU2Dz0T6bOycdb2+bAFAa3ymwWVk github.com/likexian/gokit v0.25.13/go.mod h1:qQhEWFBEfqLCO3/vOEo2EDKd+EycekVtUK4tex+l2H4= github.com/likexian/whois v1.15.1 h1:6vTMI8n9s1eJdmcO4R9h1x99aQWIZZX1CD3am68gApU= github.com/likexian/whois v1.15.1/go.mod h1:/nxmQ6YXvLz+qTxC/QFtEJNAt0zLuRxJrKiWpBJX8X0= -github.com/linode/linodego v1.40.0 h1:7ESY0PwK94hoggoCtIroT1Xk6b1flrFBNZ6KwqbTqlI= -github.com/linode/linodego v1.40.0/go.mod h1:NsUw4l8QrLdIofRg1NYFBbW5ZERnmbZykVBszPZLORM= +github.com/linode/linodego v1.44.0 h1:JZLLWzCAx3CmHSV9NmCoXisuqKtrmPhfY9MrgvaHMUY= +github.com/linode/linodego v1.44.0/go.mod h1:umdoNOmtbqAdGQbmQnPFZ2YS4US+/mU/1bA7MjoKAvg= github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM= github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ= @@ -478,8 +470,9 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -511,12 +504,12 @@ github.com/nrdcg/auroradns v1.1.0 h1:KekGh8kmf2MNwqZVVYo/fw/ZONt8QMEmbMFOeljteWo github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk= github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 h1:ouZ2JWDl8IW5k1qugYbmpbmW8hn85Ig6buSMBRlz3KI= github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3/go.mod h1:ZwadWt7mVhMHMbAQ1w8IhDqtWO3eWqWq72W7trnaiE8= -github.com/nrdcg/desec v0.8.0 h1:FJbRWUAluTCUi9nHFnhqPhLSIHiNnB9elZVWYgFtIqA= -github.com/nrdcg/desec v0.8.0/go.mod h1:BsnYPtSlBttJL3Gyzv0kDH7zkk60obwThlnqiiKzn+o= +github.com/nrdcg/desec v0.10.0 h1:qrEDiqnsvNU9QE7lXIXi/tIHAfyaFXKxF2/8/52O8uM= +github.com/nrdcg/desec v0.10.0/go.mod h1:5+4vyhMRTs49V9CNoODF/HwT8Mwxv9DJ6j+7NekUnBs= github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U= github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ= -github.com/nrdcg/freemyip v0.2.0 h1:/GscavT4GVqAY13HExl5UyoB4wlchv6Cg5NYDGsUoJ8= -github.com/nrdcg/freemyip v0.2.0/go.mod h1:HjF0Yz0lSb37HD2ihIyGz9esyGcxbCrrGFLPpKevbx4= +github.com/nrdcg/freemyip v0.3.0 h1:0D2rXgvLwe2RRaVIjyUcQ4S26+cIS2iFwnhzDsEuuwc= +github.com/nrdcg/freemyip v0.3.0/go.mod h1:c1PscDvA0ukBF0dwelU/IwOakNKnVxetpAQ863RMJoM= github.com/nrdcg/goinwx v0.10.0 h1:6W630bjDxQD6OuXKqrFRYVpTt0G/9GXXm3CeOrN0zJM= github.com/nrdcg/goinwx v0.10.0/go.mod h1:mnMSTi7CXBu2io4DzdOBoGFA1XclD0sEPWJaDhNgkA4= github.com/nrdcg/mailinabox v0.2.0 h1:IKq8mfKiVwNW2hQii/ng1dJ4yYMMv3HAP3fMFIq2CFk= @@ -562,6 +555,8 @@ github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c= +github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -594,8 +589,9 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= @@ -605,8 +601,8 @@ github.com/sacloud/api-client-go v0.2.10 h1:+rv3jDohD+pkdYwOTBiB+jZsM0xK3AxadXRz github.com/sacloud/api-client-go v0.2.10/go.mod h1:Jj3CTy2+O4bcMedVDXlbHuqqche85HEPuVXoQFhLaRc= github.com/sacloud/go-http v0.1.8 h1:ynreWA/vnM8G2ksbMlmefBHsXURKPz49qlPRqQ9IQdw= github.com/sacloud/go-http v0.1.8/go.mod h1:7TL7TN1fnPKHsMifIqURDkGujnKViCgEz5Ei/LQdFK8= -github.com/sacloud/iaas-api-go v1.12.0 h1:kqXFn3HzCiawlX6hVJb1GVqcSJqcmiGHB4Zp14sxiI8= -github.com/sacloud/iaas-api-go v1.12.0/go.mod h1:SZLXeWOdXk3WReIS557sbU1gkOgrE4rseIBQV1B3b7o= +github.com/sacloud/iaas-api-go v1.14.0 h1:xjkFWqdo4ilTrKPNNYBNWR/CZ/kVRsJrdAHAad6J/AQ= +github.com/sacloud/iaas-api-go v1.14.0/go.mod h1:C8os2Mnj0TOmMdSllwhaDWKMVG2ysFnpe69kyA4M3V0= github.com/sacloud/packages-go v0.0.10 h1:UiQGjy8LretewkRhsuna1TBM9Vz/l9FoYpQx+D+AOck= github.com/sacloud/packages-go v0.0.10/go.mod h1:f8QITBh9z4IZc4yE9j21Q8b0sXEMwRlRmhhjWeDVTYs= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 h1:yoKAVkEVwAqbGbR8n87rHQ1dulL25rKloGadb3vm770= @@ -634,8 +630,8 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/gunit v1.0.4 h1:tpTjnuH7MLlqhoD21vRoMZbMIi5GmBsAJDFyF67GhZA= github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ= -github.com/softlayer/softlayer-go v1.1.5 h1:UFFtgKxiw0yIuUw93XBCFIiIMYR5eLgmm4a5DqMHXGg= -github.com/softlayer/softlayer-go v1.1.5/go.mod h1:WeJrBLoTJcaT8nO1azeyHyNpo/fDLtbpbvh+pzts+Qw= +github.com/softlayer/softlayer-go v1.1.7 h1:SgTL+pQZt1h+5QkAhVmHORM/7N9c1X0sljJhuOIHxWE= +github.com/softlayer/softlayer-go v1.1.7/go.mod h1:WeJrBLoTJcaT8nO1azeyHyNpo/fDLtbpbvh+pzts+Qw= github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ= github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -667,17 +663,16 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 h1:RE84sHFFx6t24DJvSnF9fS1DzBNv9OpctzHK3t7AY+I= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 h1:QwE0dRkAAbdf+eACnkNULgDn9ZKUJpPWRyXdqJolP5E= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002/go.mod h1:WdC0FYbqYhJwQ3kbqri6hVP5HAEp+rzX9FToItTAzUg= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065 h1:krcqtAmexnHHBm/4ge4tr2b1cn/a7JGBESVGoZYXQAE= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 h1:aEFtLD1ceyeljQXB1S2BjN0zjTkf0X3XmpuxFIiC29w= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065/go.mod h1:HWvwy09hFSMXrj9SMvVRWV4U7rZO3l+WuogyNuxiT3M= github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E= github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo= @@ -706,8 +701,8 @@ github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaO github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -github.com/ultradns/ultradns-go-sdk v1.7.0-20240913052650-970ca9a h1:R6IR+Vj/RnGZLnX8PpPQsbbQthctO7Ah2q4tj5eoe2o= -github.com/ultradns/ultradns-go-sdk v1.7.0-20240913052650-970ca9a/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss= +github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec h1:2s/ghQ8wKE+UzD/hf3P4Gd1j0JI9ncbxv+nsypPoUYI= +github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= @@ -731,10 +726,10 @@ github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po= github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= -github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2 h1:WgeEP+8WizCQyccJNHOMLONq23qVAzYHtyg5qTdUWmg= -github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= -github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5 h1:Q4LvUMF4kzaGtopoIdXReL9/qGtmzOewBhF3dQvuHMU= -github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5/go.mod h1:9dt2V80cfJGRZA+5SKP3Ky+R/DxH02XfKObi2Uy2uPc= +github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c h1:Rnr+lDYXVkP+3eT8/d68iq4G/UeIhyCQk+HKa8toTvg= +github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= +github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 h1:qmpz0Kvr9GAng8LAhRcKIpY71CEAcL3EBkftVlsP5Cw= +github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134/go.mod h1:KgZCJrxdhdw/sKhTQ/M3S9WOLri2PCnBlc4C3s+PfKY= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= @@ -752,8 +747,6 @@ go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0P go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= @@ -768,7 +761,6 @@ go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBq go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -797,8 +789,8 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -827,8 +819,8 @@ golang.org/x/mod v0.3.0/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/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -849,10 +841,8 @@ golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= @@ -864,15 +854,14 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 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= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -883,8 +872,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -948,14 +937,13 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= @@ -963,14 +951,13 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 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.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -999,8 +986,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= -golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= 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= @@ -1014,8 +1001,8 @@ google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.197.0 h1:x6CwqQLsFiA5JKAiGyGBjc2bNtHtLddhJCE2IKuhhcQ= -google.golang.org/api v0.197.0/go.mod h1:AuOuo20GoQ331nq7DquGHlU6d+2wN2fZ8O0ta60nRNw= +google.golang.org/api v0.214.0 h1:h2Gkq07OYi6kusGOaT/9rnNljuXmqPnaig7WGPmKbwA= +google.golang.org/api v0.214.0/go.mod h1:bYPpLG8AyeMWwDU6NXoB00xC0DFkikVvd5MfwoxjLqE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1030,43 +1017,31 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20211021150943-2b146023228c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU= -google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4= -google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0= -google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU= +google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4= +google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ= +google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= -google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM= -google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1080,15 +1055,14 @@ gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ns1/ns1-go.v2 v2.12.0 h1:cqdqQoTx17JmTusfxh5m3e2b36jfUzFAZedv89pFX18= -gopkg.in/ns1/ns1-go.v2 v2.12.0/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= +gopkg.in/ns1/ns1-go.v2 v2.13.0 h1:I5NNqI9Bi1SGK92TVkOvLTwux5LNrix/99H2datVh48= +gopkg.in/ns1/ns1-go.v2 v2.13.0/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/src/mod/dynamicproxy/dynamicproxy.go b/src/mod/dynamicproxy/dynamicproxy.go index f5c906b..677881a 100644 --- a/src/mod/dynamicproxy/dynamicproxy.go +++ b/src/mod/dynamicproxy/dynamicproxy.go @@ -314,7 +314,7 @@ func (router *Router) Restart() error { return err } - time.Sleep(800 * time.Millisecond) + time.Sleep(100 * time.Millisecond) // Start the server err = router.StartProxyService() if err != nil { diff --git a/src/mod/plugins/includes.go b/src/mod/plugins/includes.go new file mode 100644 index 0000000..38f4d8d --- /dev/null +++ b/src/mod/plugins/includes.go @@ -0,0 +1,118 @@ +package plugins + +/* + Plugins Includes.go + + This file contains the common types and structs that are used by the plugins + If you are building a Zoraxy plugin with Golang, you can use this file to include + the common types and structs that are used by the plugins +*/ + +type PluginType int + +const ( + PluginType_Router PluginType = 0 //Router Plugin, used for handling / routing / forwarding traffic + PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore +) + +type CaptureRule struct { + CapturePath string `json:"capture_path"` + IncludeSubPaths bool `json:"include_sub_paths"` +} + +type ControlStatusCode int + +const ( + ControlStatusCode_CAPTURED ControlStatusCode = 280 //Traffic captured by plugin, ask Zoraxy not to process the traffic + ControlStatusCode_UNHANDLED ControlStatusCode = 284 //Traffic not handled by plugin, ask Zoraxy to process the traffic + ControlStatusCode_ERROR ControlStatusCode = 580 //Error occurred while processing the traffic, ask Zoraxy to process the traffic and log the error +) + +type SubscriptionEvent struct { + EventName string `json:"event_name"` + EventSource string `json:"event_source"` + Payload string `json:"payload"` //Payload of the event, can be empty +} + +type RuntimeConstantValue struct { + ZoraxyVersion string `json:"zoraxy_version"` + ZoraxyUUID string `json:"zoraxy_uuid"` +} + +/* +IntroSpect Payload + +When the plugin is initialized with -introspect flag, +the plugin shell return this payload as JSON and exit +*/ +type IntroSpect struct { + /* Plugin metadata */ + ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname + Name string `json:"name"` //Name of your plugin + Author string `json:"author"` //Author name of your plugin + AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email + Description string `json:"description"` //Description of your plugin + URL string `json:"url"` //URL of your plugin + Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1) + VersionMajor int `json:"version_major"` //Major version of your plugin + VersionMinor int `json:"version_minor"` //Minor version of your plugin + VersionPatch int `json:"version_patch"` //Patch version of your plugin + + /* + + Endpoint Settings + + */ + + /* + Global Capture Settings + + Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on + This captures the whole traffic of Zoraxy + + Notes: Will raise a warning on the UI when the user enables the plugin on a HTTP Proxy rule + */ + GlobalCapturePath []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin + GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler) + + /* + Always Capture Settings + + Once the plugin is enabled on a given HTTP Proxy rule, + these always applies + */ + AlwaysCapturePath []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp) + AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler) + + /* + Dynamic Capture Settings + + Once the plugin is enabled on a given HTTP Proxy rule, + the plugin can capture the request and decided if the request + shall be handled by itself or let it pass through + + */ + DynmaicCaptureIngress string `json:"capture_path"` //Traffic capture path of your plugin (e.g. /capture) + DynamicHandleIngress string `json:"handle_path"` //Traffic handle path of your plugin (e.g. /handler) + + /* UI Path for your plugin */ + UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI + + /* Subscriptions Settings */ + SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered + SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details +} + +/* +ConfigureSpec Payload + +Zoraxy will start your plugin with -configure flag, +the plugin shell read this payload as JSON and configure itself +by the supplied values like starting a web server at given port +that listens to 127.0.0.1:port +*/ +type ConfigureSpec struct { + Port int `json:"port"` //Port to listen + RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values + //To be expanded +} diff --git a/src/mod/plugins/introspect.go b/src/mod/plugins/introspect.go new file mode 100644 index 0000000..0143434 --- /dev/null +++ b/src/mod/plugins/introspect.go @@ -0,0 +1,54 @@ +package plugins + +import ( + "context" + "fmt" + "os/exec" + "time" +) + +// LoadPlugin loads a plugin from the plugin directory +func (m *Manager) IsValidPluginFolder(path string) bool { + _, err := m.GetPluginEntryPoint(path) + return err == nil +} + +/* +LoadPluginSpec loads a plugin specification from the plugin directory +Zoraxy will start the plugin binary or the entry point script +with -introspect flag to get the plugin specification +*/ +func (m *Manager) LoadPluginSpec(pluginPath string) (*Plugin, error) { + pluginEntryPoint, err := m.GetPluginEntryPoint(pluginPath) + if err != nil { + return nil, err + } + + pluginSpec, err := m.GetPluginSpec(pluginEntryPoint) + if err != nil { + return nil, err + } + + return &Plugin{ + Spec: pluginSpec, + Enabled: false, + }, nil +} + +// GetPluginEntryPoint returns the plugin entry point +func (m *Manager) GetPluginSpec(entryPoint string) (*IntroSpect, error) { + pluginSpec := &IntroSpect{} + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, entryPoint, "-introspect") + err := cmd.Run() + if ctx.Err() == context.DeadlineExceeded { + return nil, fmt.Errorf("plugin introspect timed out") + } + if err != nil { + return nil, err + } + + return pluginSpec, nil +} diff --git a/src/mod/plugins/lifecycle.go b/src/mod/plugins/lifecycle.go new file mode 100644 index 0000000..4c41453 --- /dev/null +++ b/src/mod/plugins/lifecycle.go @@ -0,0 +1,67 @@ +package plugins + +import ( + "encoding/json" + "errors" + "os/exec" + "path/filepath" +) + +func (m *Manager) StartPlugin(pluginID string) error { + plugin, ok := m.LoadedPlugins.Load(pluginID) + if !ok { + return errors.New("plugin not found") + } + + //Get the plugin Entry point + pluginEntryPoint, err := m.GetPluginEntryPoint(pluginID) + if err != nil { + //Plugin removed after introspect? + return err + } + + //Get the absolute path of the plugin entry point to prevent messing up with the cwd + absolutePath, err := filepath.Abs(pluginEntryPoint) + if err != nil { + return err + } + + //Prepare plugin start configuration + pluginConfiguration := ConfigureSpec{ + Port: getRandomPortNumber(), + RuntimeConst: *m.Options.SystemConst, + } + js, _ := json.Marshal(pluginConfiguration) + + cmd := exec.Command(absolutePath, "-configure="+string(js)) + cmd.Dir = filepath.Dir(absolutePath) + if err := cmd.Start(); err != nil { + return err + } + + // Store the cmd object so it can be accessed later for stopping the plugin + plugin.(*Plugin).Process = cmd + plugin.(*Plugin).Enabled = true + return nil +} + +// Check if the plugin is still running +func (m *Manager) PluginStillRunning(pluginID string) bool { + plugin, ok := m.LoadedPlugins.Load(pluginID) + if !ok { + return false + } + return plugin.(*Plugin).Process.ProcessState == nil +} + +// BlockUntilAllProcessExited blocks until all the plugins processes have exited +func (m *Manager) BlockUntilAllProcessExited() { + m.LoadedPlugins.Range(func(key, value interface{}) bool { + plugin := value.(*Plugin) + if m.PluginStillRunning(value.(*Plugin).Spec.ID) { + //Wait for the plugin to exit + plugin.Process.Wait() + } + return true + }) +} diff --git a/src/mod/plugins/plugins.go b/src/mod/plugins/plugins.go new file mode 100644 index 0000000..1a2bf0d --- /dev/null +++ b/src/mod/plugins/plugins.go @@ -0,0 +1,117 @@ +package plugins + +import ( + "errors" + "os" + "os/exec" + "path/filepath" + "sync" + "time" + + "imuslab.com/zoraxy/mod/database" + "imuslab.com/zoraxy/mod/info/logger" +) + +type Plugin struct { + Spec *IntroSpect //The plugin specification + Process *exec.Cmd //The process of the plugin + Enabled bool //Whether the plugin is enabled +} + +type ManagerOptions struct { + ZoraxyVersion string + PluginDir string + SystemConst *RuntimeConstantValue + Database database.Database + Logger *logger.Logger +} + +type Manager struct { + LoadedPlugins sync.Map //Storing *Plugin + Options *ManagerOptions +} + +func NewPluginManager(options *ManagerOptions) *Manager { + return &Manager{ + LoadedPlugins: sync.Map{}, + Options: options, + } +} + +// LoadPlugins loads all plugins from the plugin directory +func (m *Manager) LoadPlugins() error { + // Load all plugins from the plugin directory + foldersInPluginDir, err := os.ReadDir(m.Options.PluginDir) + if err != nil { + return err + } + + for _, folder := range foldersInPluginDir { + if folder.IsDir() { + pluginPath := filepath.Join(m.Options.PluginDir, folder.Name()) + thisPlugin, err := m.LoadPluginSpec(pluginPath) + if err != nil { + m.Log("Failed to load plugin: "+filepath.Base(pluginPath), err) + continue + } + m.LoadedPlugins.Store(thisPlugin.Spec.ID, thisPlugin) + } + } + return nil +} + +// GetPluginByID returns a plugin by its ID +func (m *Manager) GetPluginByID(pluginID string) (*Plugin, error) { + plugin, ok := m.LoadedPlugins.Load(pluginID) + if !ok { + return nil, errors.New("plugin not found") + } + return plugin.(*Plugin), nil +} + +// EnablePlugin enables a plugin +func (m *Manager) EnablePlugin(pluginID string) error { + plugin, ok := m.LoadedPlugins.Load(pluginID) + if !ok { + return errors.New("plugin not found") + } + plugin.(*Plugin).Enabled = true + return nil +} + +// DisablePlugin disables a plugin +func (m *Manager) DisablePlugin(pluginID string) error { + plugin, ok := m.LoadedPlugins.Load(pluginID) + if !ok { + return errors.New("plugin not found") + } + + thisPlugin := plugin.(*Plugin) + thisPlugin.Process.Process.Signal(os.Interrupt) + go func() { + //Wait for 10 seconds for the plugin to stop gracefully + time.Sleep(10 * time.Second) + if thisPlugin.Process.ProcessState == nil || !thisPlugin.Process.ProcessState.Exited() { + m.Log("Plugin "+thisPlugin.Spec.Name+" failed to stop gracefully, killing it", nil) + thisPlugin.Process.Process.Kill() + } else { + m.Log("Plugin "+thisPlugin.Spec.Name+" background process stopped", nil) + } + }() + thisPlugin.Enabled = false + return nil +} + +// Terminate all plugins and exit +func (m *Manager) Close() { + m.LoadedPlugins.Range(func(key, value interface{}) bool { + plugin := value.(*Plugin) + if plugin.Enabled { + m.DisablePlugin(plugin.Spec.ID) + } + return true + }) + + //Wait until all loaded plugin process are terminated + m.BlockUntilAllProcessExited() +} diff --git a/src/mod/plugins/utils.go b/src/mod/plugins/utils.go new file mode 100644 index 0000000..a6671ea --- /dev/null +++ b/src/mod/plugins/utils.go @@ -0,0 +1,62 @@ +package plugins + +import ( + "errors" + "math/rand" + "os" + "path/filepath" + "runtime" + + "imuslab.com/zoraxy/mod/netutils" +) + +/* +Check if the folder contains a valid plugin in either one of the forms + +1. Contain a file that have the same name as its parent directory, either executable or .exe on Windows +2. Contain a start.sh or start.bat file + +Return the path of the plugin entry point if found +*/ +func (m *Manager) GetPluginEntryPoint(folderpath string) (string, error) { + info, err := os.Stat(folderpath) + if err != nil { + return "", err + } + if !info.IsDir() { + return "", errors.New("path is not a directory") + } + expectedBinaryPath := filepath.Join(folderpath, filepath.Base(folderpath)) + if runtime.GOOS == "windows" { + expectedBinaryPath += ".exe" + } + + if _, err := os.Stat(expectedBinaryPath); err == nil { + return expectedBinaryPath, nil + } + + if _, err := os.Stat(filepath.Join(folderpath, "start.sh")); err == nil { + return filepath.Join(folderpath, "start.sh"), nil + } + + if _, err := os.Stat(filepath.Join(folderpath, "start.bat")); err == nil { + return filepath.Join(folderpath, "start.bat"), nil + } + + return "", errors.New("No valid entry point found") +} + +// Log logs a message with an optional error +func (m *Manager) Log(message string, err error) { + m.Options.Logger.PrintAndLog("plugin-manager", message, err) +} + +// getRandomPortNumber generates a random port number between 49152 and 65535 +func getRandomPortNumber() int { + portNo := rand.Intn(65535-49152) + 49152 + //Check if the port is already in use + for netutils.CheckIfPortOccupied(portNo) { + portNo = rand.Intn(65535-49152) + 49152 + } + return portNo +} diff --git a/src/reverseproxy.go b/src/reverseproxy.go index e25e405..459c49e 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -174,9 +174,15 @@ func ReverseProxtInit() { }() } +// Toggle the reverse proxy service on and off func ReverseProxyHandleOnOff(w http.ResponseWriter, r *http.Request) { - enable, _ := utils.PostPara(r, "enable") //Support root, vdir and subd - if enable == "true" { + enable, err := utils.PostBool(r, "enable") + if err != nil { + utils.SendErrorResponse(w, "enable not defined") + return + } + + if enable { err := dynamicProxyRouter.StartProxyService() if err != nil { utils.SendErrorResponse(w, err.Error()) From 394cf50e1dd56e509764a3890c6558b18b2a186f Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Wed, 19 Feb 2025 21:38:27 +0800 Subject: [PATCH 03/14] #550 - Instead of clearing the Zoraxy cookie on the client side, set the Zoraxy session in the server side to an empty value instead --- .../dynamicproxy/loadbalance/originPicker.go | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/src/mod/dynamicproxy/loadbalance/originPicker.go b/src/mod/dynamicproxy/loadbalance/originPicker.go index 4c4ac75..8e1f5ee 100644 --- a/src/mod/dynamicproxy/loadbalance/originPicker.go +++ b/src/mod/dynamicproxy/loadbalance/originPicker.go @@ -13,6 +13,10 @@ import ( by this request. */ +const ( + STICKY_SESSION_NAME = "zr_sticky_session" +) + // GetRequestUpstreamTarget return the upstream target where this // request should be routed func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.Request, origins []*Upstream, useStickySession bool) (*Upstream, error) { @@ -50,7 +54,7 @@ func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.R return origins[targetOriginId], nil } //No sticky session, get a random origin - m.clearSessionHandler(w, r) //Clear the session + m.clearSessionHandler(w, r) //Filter the offline origins origins = m.FilterOfflineOrigins(origins) @@ -78,7 +82,7 @@ func (m *RouteManager) GetUsableUpstreamCounts(origins []*Upstream) int { /* Features related to session access */ //Set a new origin for this connection by session func (m *RouteManager) setSessionHandler(w http.ResponseWriter, r *http.Request, originIpOrDomain string, index int) error { - session, err := m.SessionStore.Get(r, "STICKYSESSION") + session, err := m.SessionStore.Get(r, STICKY_SESSION_NAME) if err != nil { return err } @@ -93,13 +97,15 @@ func (m *RouteManager) setSessionHandler(w http.ResponseWriter, r *http.Request, return nil } +// Clear the zoraxy only session values func (m *RouteManager) clearSessionHandler(w http.ResponseWriter, r *http.Request) error { - session, err := m.SessionStore.Get(r, "STICKYSESSION") + session, err := m.SessionStore.Get(r, STICKY_SESSION_NAME) if err != nil { return err } - session.Options.MaxAge = -1 - session.Options.Path = "/" + + session.Values["zr_sid_origin"] = "" + session.Values["zr_sid_index"] = -1 err = session.Save(r, w) if err != nil { return err @@ -110,7 +116,7 @@ func (m *RouteManager) clearSessionHandler(w http.ResponseWriter, r *http.Reques // Get the previous connected origin from session func (m *RouteManager) getSessionHandler(r *http.Request, upstreams []*Upstream) (int, error) { // Get existing session - session, err := m.SessionStore.Get(r, "STICKYSESSION") + session, err := m.SessionStore.Get(r, STICKY_SESSION_NAME) if err != nil { return -1, err } @@ -119,7 +125,7 @@ func (m *RouteManager) getSessionHandler(r *http.Request, upstreams []*Upstream) originDomainRaw := session.Values["zr_sid_origin"] originIDRaw := session.Values["zr_sid_index"] - if originDomainRaw == nil || originIDRaw == nil { + if originDomainRaw == nil || originIDRaw == nil || originIDRaw == -1 { return -1, errors.New("no session has been set") } originDomain := originDomainRaw.(string) @@ -201,21 +207,3 @@ func getRandomUpstreamByWeight(upstreams []*Upstream) (*Upstream, int, error) { return nil, -1, errors.New("failed to pick an upstream origin server") } - -// IntRange returns a random integer in the range from min to max. -/* -func intRange(min, max int) (int, error) { - var result int - switch { - case min > max: - // Fail with error - return result, errors.New("min is greater than max") - case max == min: - result = max - case max > min: - b := rand.Intn(max-min) + min - result = min + int(b) - } - return result, nil -} -*/ From 20959cd6ccb72a2b52eec189b8450e46d00296e4 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Thu, 20 Feb 2025 20:25:20 +0800 Subject: [PATCH 04/14] Fixed #554 - Removed passive load balancer and default to active lb only --- src/mod/dynamicproxy/loadbalance/onlineStatus.go | 3 ++- src/mod/dynamicproxy/proxyRequestHandler.go | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/mod/dynamicproxy/loadbalance/onlineStatus.go b/src/mod/dynamicproxy/loadbalance/onlineStatus.go index 6d2d591..a30217a 100644 --- a/src/mod/dynamicproxy/loadbalance/onlineStatus.go +++ b/src/mod/dynamicproxy/loadbalance/onlineStatus.go @@ -3,7 +3,6 @@ package loadbalance import ( "strconv" "strings" - "time" ) // Return if the target host is online @@ -36,6 +35,7 @@ func (m *RouteManager) NotifyHostOnlineState(upstreamIP string, isOnline bool) { // Set this host unreachable for a given amount of time defined in timeout // this shall be used in passive fallback. The uptime monitor should call to NotifyHostOnlineState() instead +/* func (m *RouteManager) NotifyHostUnreachableWithTimeout(upstreamIp string, timeout int64) { //if the upstream IP contains http or https, strip it upstreamIp = strings.TrimPrefix(upstreamIp, "http://") @@ -58,6 +58,7 @@ func (m *RouteManager) NotifyHostUnreachableWithTimeout(upstreamIp string, timeo m.NotifyHostOnlineState(upstreamIp, true) }() } +*/ // FilterOfflineOrigins return only online origins from a list of origins func (m *RouteManager) FilterOfflineOrigins(origins []*Upstream) []*Upstream { diff --git a/src/mod/dynamicproxy/proxyRequestHandler.go b/src/mod/dynamicproxy/proxyRequestHandler.go index 142972d..f4d8fea 100644 --- a/src/mod/dynamicproxy/proxyRequestHandler.go +++ b/src/mod/dynamicproxy/proxyRequestHandler.go @@ -3,7 +3,6 @@ package dynamicproxy import ( "context" "errors" - "fmt" "log" "net" "net/http" @@ -211,9 +210,6 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe http.Error(w, "Request canceled", http.StatusRequestTimeout) h.Parent.logRequest(r, false, http.StatusRequestTimeout, "host-http", r.URL.Hostname()) } else { - //Notify the load balancer that the host is unreachable - fmt.Println(err.Error()) - h.Parent.loadBalancer.NotifyHostUnreachableWithTimeout(selectedUpstream.OriginIpOrDomain, PassiveLoadBalanceNotifyTimeout) http.ServeFile(w, r, "./web/rperror.html") h.Parent.logRequest(r, false, 521, "host-http", r.URL.Hostname()) } From ad13b33283d8285513803b42f0fe61af8ac50181 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Tue, 25 Feb 2025 21:14:03 +0800 Subject: [PATCH 05/14] Added plugin prototype - Added proof of concept plugin prototype - Added wip plugin page --- example/plugins/helloworld/go.mod | 3 + example/plugins/helloworld/index.html | 24 +++ example/plugins/helloworld/main.go | 49 +++++ .../helloworld/zoraxy_plugin/zoraxy_plugin.go | 186 ++++++++++++++++++ src/def.go | 6 +- src/mod/plugins/includes.go | 68 +++++++ src/mod/plugins/introspect.go | 18 +- src/mod/plugins/lifecycle.go | 76 ++++++- src/mod/plugins/plugins.go | 65 +++--- src/mod/plugins/utils.go | 19 ++ src/start.go | 20 ++ src/web/components/plugins.html | 40 ++++ src/web/index.html | 8 +- 13 files changed, 546 insertions(+), 36 deletions(-) create mode 100644 example/plugins/helloworld/go.mod create mode 100644 example/plugins/helloworld/index.html create mode 100644 example/plugins/helloworld/main.go create mode 100644 example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go create mode 100644 src/web/components/plugins.html diff --git a/example/plugins/helloworld/go.mod b/example/plugins/helloworld/go.mod new file mode 100644 index 0000000..acfde6b --- /dev/null +++ b/example/plugins/helloworld/go.mod @@ -0,0 +1,3 @@ +module example.com/zoraxy/helloworld + +go 1.23.6 diff --git a/example/plugins/helloworld/index.html b/example/plugins/helloworld/index.html new file mode 100644 index 0000000..3edafe1 --- /dev/null +++ b/example/plugins/helloworld/index.html @@ -0,0 +1,24 @@ + + + + + + Hello World + + + +
+

Hello World

+

Welcome to your first Zoraxy plugin

+
+ + \ No newline at end of file diff --git a/example/plugins/helloworld/main.go b/example/plugins/helloworld/main.go new file mode 100644 index 0000000..b05ab65 --- /dev/null +++ b/example/plugins/helloworld/main.go @@ -0,0 +1,49 @@ +package main + +import ( + _ "embed" + "fmt" + "net/http" + "strconv" + + plugin "example.com/zoraxy/helloworld/zoraxy_plugin" +) + +//go:embed index.html +var indexHTML string + +func helloWorldHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, indexHTML) +} + +func main() { + // Serve the plugin intro spect + // This will print the plugin intro spect and exit if the -introspect flag is provided + runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{ + ID: "com.example.helloworld", + Name: "Hello World Plugin", + Author: "foobar", + AuthorContact: "admin@example.com", + Description: "A simple hello world plugin", + URL: "https://example.com", + Type: plugin.PluginType_Utilities, + VersionMajor: 1, + VersionMinor: 0, + VersionPatch: 0, + + // As this is a utility plugin, we don't need to capture any traffic + // but only serve the UI, so we set the UI (relative to the plugin path) to "/" + UIPath: "/", + }) + + if err != nil { + //Terminate or enter standalone mode here + panic(err) + } + + // Serve the hello world page + // This will serve the index.html file embedded in the binary + http.HandleFunc("/", helloWorldHandler) + fmt.Println("Server started at http://localhost:" + strconv.Itoa(runtimeCfg.Port)) + http.ListenAndServe(":"+strconv.Itoa(runtimeCfg.Port), nil) +} diff --git a/example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go b/example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go new file mode 100644 index 0000000..4778e4e --- /dev/null +++ b/example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go @@ -0,0 +1,186 @@ +package zoraxy_plugin + +import ( + "encoding/json" + "fmt" + "os" + "strings" +) + +/* + Plugins Includes.go + + This file is copied from Zoraxy source code + You can always find the latest version under mod/plugins/includes.go + Usually this file are backward compatible +*/ + +type PluginType int + +const ( + PluginType_Router PluginType = 0 //Router Plugin, used for handling / routing / forwarding traffic + PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore +) + +type CaptureRule struct { + CapturePath string `json:"capture_path"` + IncludeSubPaths bool `json:"include_sub_paths"` +} + +type ControlStatusCode int + +const ( + ControlStatusCode_CAPTURED ControlStatusCode = 280 //Traffic captured by plugin, ask Zoraxy not to process the traffic + ControlStatusCode_UNHANDLED ControlStatusCode = 284 //Traffic not handled by plugin, ask Zoraxy to process the traffic + ControlStatusCode_ERROR ControlStatusCode = 580 //Error occurred while processing the traffic, ask Zoraxy to process the traffic and log the error +) + +type SubscriptionEvent struct { + EventName string `json:"event_name"` + EventSource string `json:"event_source"` + Payload string `json:"payload"` //Payload of the event, can be empty +} + +type RuntimeConstantValue struct { + ZoraxyVersion string `json:"zoraxy_version"` + ZoraxyUUID string `json:"zoraxy_uuid"` +} + +/* +IntroSpect Payload + +When the plugin is initialized with -introspect flag, +the plugin shell return this payload as JSON and exit +*/ +type IntroSpect struct { + /* Plugin metadata */ + ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname + Name string `json:"name"` //Name of your plugin + Author string `json:"author"` //Author name of your plugin + AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email + Description string `json:"description"` //Description of your plugin + URL string `json:"url"` //URL of your plugin + Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1) + VersionMajor int `json:"version_major"` //Major version of your plugin + VersionMinor int `json:"version_minor"` //Minor version of your plugin + VersionPatch int `json:"version_patch"` //Patch version of your plugin + + /* + + Endpoint Settings + + */ + + /* + Global Capture Settings + + Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on + This captures the whole traffic of Zoraxy + + Notes: Will raise a warning on the UI when the user enables the plugin on a HTTP Proxy rule + */ + GlobalCapturePath []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin + GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler) + + /* + Always Capture Settings + + Once the plugin is enabled on a given HTTP Proxy rule, + these always applies + */ + AlwaysCapturePath []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp) + AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler) + + /* + Dynamic Capture Settings + + Once the plugin is enabled on a given HTTP Proxy rule, + the plugin can capture the request and decided if the request + shall be handled by itself or let it pass through + + */ + DynmaicCaptureIngress string `json:"capture_path"` //Traffic capture path of your plugin (e.g. /capture) + DynamicHandleIngress string `json:"handle_path"` //Traffic handle path of your plugin (e.g. /handler) + + /* UI Path for your plugin */ + UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI + + /* Subscriptions Settings */ + SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered + SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details +} + +/* +ServeIntroSpect Function + +This function will check if the plugin is initialized with -introspect flag, +if so, it will print the intro spect and exit + +Place this function at the beginning of your plugin main function +*/ +func ServeIntroSpect(pluginSpect *IntroSpect) { + if len(os.Args) > 1 && os.Args[1] == "-introspect" { + //Print the intro spect and exit + jsonData, _ := json.MarshalIndent(pluginSpect, "", " ") + fmt.Println(string(jsonData)) + os.Exit(0) + } +} + +/* +ConfigureSpec Payload + +Zoraxy will start your plugin with -configure flag, +the plugin shell read this payload as JSON and configure itself +by the supplied values like starting a web server at given port +that listens to 127.0.0.1:port +*/ +type ConfigureSpec struct { + Port int `json:"port"` //Port to listen + RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values + //To be expanded +} + +/* +RecvExecuteConfigureSpec Function + +This function will read the configure spec from Zoraxy +and return the ConfigureSpec object + +Place this function after ServeIntroSpect function in your plugin main function +*/ +func RecvConfigureSpec() (*ConfigureSpec, error) { + for i, arg := range os.Args { + if strings.HasPrefix(arg, "-configure=") { + var configSpec ConfigureSpec + if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil { + return nil, err + } + return &configSpec, nil + } else if arg == "-configure" { + var configSpec ConfigureSpec + var nextArg string + if len(os.Args) > i+1 { + nextArg = os.Args[i+1] + if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("No port specified after -configure flag") + } + return &configSpec, nil + } + } + return nil, fmt.Errorf("No -configure flag found") +} + +/* +ServeAndRecvSpec Function + +This function will serve the intro spect and return the configure spec +See the ServeIntroSpect and RecvConfigureSpec for more details +*/ +func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) { + ServeIntroSpect(pluginSpect) + return RecvConfigureSpec() +} diff --git a/src/def.go b/src/def.go index 23d2ae4..b03fd8d 100644 --- a/src/def.go +++ b/src/def.go @@ -30,6 +30,7 @@ import ( "imuslab.com/zoraxy/mod/mdns" "imuslab.com/zoraxy/mod/netstat" "imuslab.com/zoraxy/mod/pathrule" + "imuslab.com/zoraxy/mod/plugins" "imuslab.com/zoraxy/mod/sshprox" "imuslab.com/zoraxy/mod/statistic" "imuslab.com/zoraxy/mod/statistic/analytic" @@ -42,8 +43,8 @@ import ( const ( /* Build Constants */ SYSTEM_NAME = "Zoraxy" - SYSTEM_VERSION = "3.1.8" - DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */ + SYSTEM_VERSION = "3.1.9" + DEVELOPMENT_BUILD = true /* Development: Set to false to use embedded web fs */ /* System Constants */ TMP_FOLDER = "./tmp" @@ -139,6 +140,7 @@ var ( staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing + pluginManager *plugins.Manager //Plugin manager for managing plugins //Authentication Provider autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication diff --git a/src/mod/plugins/includes.go b/src/mod/plugins/includes.go index 38f4d8d..89fb5f9 100644 --- a/src/mod/plugins/includes.go +++ b/src/mod/plugins/includes.go @@ -1,5 +1,12 @@ package plugins +import ( + "encoding/json" + "fmt" + "os" + "strings" +) + /* Plugins Includes.go @@ -103,6 +110,23 @@ type IntroSpect struct { SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details } +/* +ServeIntroSpect Function + +This function will check if the plugin is initialized with -introspect flag, +if so, it will print the intro spect and exit + +Place this function at the beginning of your plugin main function +*/ +func ServeIntroSpect(pluginSpect *IntroSpect) { + if len(os.Args) > 1 && os.Args[1] == "-introspect" { + //Print the intro spect and exit + jsonData, _ := json.MarshalIndent(pluginSpect, "", " ") + fmt.Println(string(jsonData)) + os.Exit(0) + } +} + /* ConfigureSpec Payload @@ -116,3 +140,47 @@ type ConfigureSpec struct { RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values //To be expanded } + +/* +RecvExecuteConfigureSpec Function + +This function will read the configure spec from Zoraxy +and return the ConfigureSpec object + +Place this function after ServeIntroSpect function in your plugin main function +*/ +func RecvConfigureSpec() (*ConfigureSpec, error) { + for i, arg := range os.Args { + if strings.HasPrefix(arg, "-configure=") { + var configSpec ConfigureSpec + if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil { + return nil, err + } + return &configSpec, nil + } else if arg == "-configure" { + var configSpec ConfigureSpec + var nextArg string + if len(os.Args) > i+1 { + nextArg = os.Args[i+1] + if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("No port specified after -configure flag") + } + return &configSpec, nil + } + } + return nil, fmt.Errorf("No -configure flag found") +} + +/* +ServeAndRecvSpec Function + +This function will serve the intro spect and return the configure spec +See the ServeIntroSpect and RecvConfigureSpec for more details +*/ +func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) { + ServeIntroSpect(pluginSpect) + return RecvConfigureSpec() +} diff --git a/src/mod/plugins/introspect.go b/src/mod/plugins/introspect.go index 0143434..4c40776 100644 --- a/src/mod/plugins/introspect.go +++ b/src/mod/plugins/introspect.go @@ -2,6 +2,7 @@ package plugins import ( "context" + "encoding/json" "fmt" "os/exec" "time" @@ -29,6 +30,11 @@ func (m *Manager) LoadPluginSpec(pluginPath string) (*Plugin, error) { return nil, err } + err = validatePluginSpec(pluginSpec) + if err != nil { + return nil, err + } + return &Plugin{ Spec: pluginSpec, Enabled: false, @@ -37,12 +43,12 @@ func (m *Manager) LoadPluginSpec(pluginPath string) (*Plugin, error) { // GetPluginEntryPoint returns the plugin entry point func (m *Manager) GetPluginSpec(entryPoint string) (*IntroSpect, error) { - pluginSpec := &IntroSpect{} + pluginSpec := IntroSpect{} ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() cmd := exec.CommandContext(ctx, entryPoint, "-introspect") - err := cmd.Run() + output, err := cmd.Output() if ctx.Err() == context.DeadlineExceeded { return nil, fmt.Errorf("plugin introspect timed out") } @@ -50,5 +56,11 @@ func (m *Manager) GetPluginSpec(entryPoint string) (*IntroSpect, error) { return nil, err } - return pluginSpec, nil + // Assuming the output is JSON and needs to be unmarshaled into pluginSpec + err = json.Unmarshal(output, &pluginSpec) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal plugin spec: %v", err) + } + + return &pluginSpec, nil } diff --git a/src/mod/plugins/lifecycle.go b/src/mod/plugins/lifecycle.go index 4c41453..6b4ea75 100644 --- a/src/mod/plugins/lifecycle.go +++ b/src/mod/plugins/lifecycle.go @@ -3,8 +3,13 @@ package plugins import ( "encoding/json" "errors" + "io" + "os" "os/exec" "path/filepath" + "strconv" + "strings" + "time" ) func (m *Manager) StartPlugin(pluginID string) error { @@ -13,8 +18,10 @@ func (m *Manager) StartPlugin(pluginID string) error { return errors.New("plugin not found") } + thisPlugin := plugin.(*Plugin) + //Get the plugin Entry point - pluginEntryPoint, err := m.GetPluginEntryPoint(pluginID) + pluginEntryPoint, err := m.GetPluginEntryPoint(thisPlugin.RootDir) if err != nil { //Plugin removed after introspect? return err @@ -33,18 +40,85 @@ func (m *Manager) StartPlugin(pluginID string) error { } js, _ := json.Marshal(pluginConfiguration) + m.Log("Starting plugin "+thisPlugin.Spec.Name+" at :"+strconv.Itoa(pluginConfiguration.Port), nil) cmd := exec.Command(absolutePath, "-configure="+string(js)) cmd.Dir = filepath.Dir(absolutePath) + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + return err + } + if err := cmd.Start(); err != nil { return err } + go func() { + buf := make([]byte, 1) + lineBuf := "" + for { + n, err := stdoutPipe.Read(buf) + if n > 0 { + lineBuf += string(buf[:n]) + for { + if idx := strings.IndexByte(lineBuf, '\n'); idx != -1 { + m.handlePluginSTDOUT(pluginID, lineBuf[:idx]) + lineBuf = lineBuf[idx+1:] + } else { + break + } + } + } + if err != nil { + if err != io.EOF { + m.handlePluginSTDOUT(pluginID, lineBuf) // handle any remaining data + } + break + } + } + }() + // Store the cmd object so it can be accessed later for stopping the plugin plugin.(*Plugin).Process = cmd plugin.(*Plugin).Enabled = true return nil } +func (m *Manager) handlePluginSTDOUT(pluginID string, line string) { + thisPlugin, err := m.GetPluginByID(pluginID) + processID := -1 + if thisPlugin.Process != nil && thisPlugin.Process.Process != nil { + // Get the process ID of the plugin + processID = thisPlugin.Process.Process.Pid + } + if err != nil { + m.Log("[unknown:"+strconv.Itoa(processID)+"] "+line, err) + return + } + m.Log("["+thisPlugin.Spec.Name+":"+strconv.Itoa(processID)+"] "+line, nil) +} + +func (m *Manager) StopPlugin(pluginID string) error { + plugin, ok := m.LoadedPlugins.Load(pluginID) + if !ok { + return errors.New("plugin not found") + } + + thisPlugin := plugin.(*Plugin) + thisPlugin.Process.Process.Signal(os.Interrupt) + go func() { + //Wait for 10 seconds for the plugin to stop gracefully + time.Sleep(10 * time.Second) + if thisPlugin.Process.ProcessState == nil || !thisPlugin.Process.ProcessState.Exited() { + m.Log("Plugin "+thisPlugin.Spec.Name+" failed to stop gracefully, killing it", nil) + thisPlugin.Process.Process.Kill() + } else { + m.Log("Plugin "+thisPlugin.Spec.Name+" background process stopped", nil) + } + }() + plugin.(*Plugin).Enabled = false + return nil +} + // Check if the plugin is still running func (m *Manager) PluginStillRunning(pluginID string) bool { plugin, ok := m.LoadedPlugins.Load(pluginID) diff --git a/src/mod/plugins/plugins.go b/src/mod/plugins/plugins.go index 1a2bf0d..957eaa4 100644 --- a/src/mod/plugins/plugins.go +++ b/src/mod/plugins/plugins.go @@ -6,24 +6,24 @@ import ( "os/exec" "path/filepath" "sync" - "time" "imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/info/logger" + "imuslab.com/zoraxy/mod/utils" ) type Plugin struct { + RootDir string //The root directory of the plugin Spec *IntroSpect //The plugin specification Process *exec.Cmd //The process of the plugin Enabled bool //Whether the plugin is enabled } type ManagerOptions struct { - ZoraxyVersion string - PluginDir string - SystemConst *RuntimeConstantValue - Database database.Database - Logger *logger.Logger + PluginDir string + SystemConst *RuntimeConstantValue + Database *database.Database + Logger *logger.Logger } type Manager struct { @@ -31,15 +31,24 @@ type Manager struct { Options *ManagerOptions } +// NewPluginManager creates a new plugin manager func NewPluginManager(options *ManagerOptions) *Manager { + if options.PluginDir == "" { + options.PluginDir = "./plugins" + } + + if !utils.FileExists(options.PluginDir) { + os.MkdirAll(options.PluginDir, 0755) + } + return &Manager{ LoadedPlugins: sync.Map{}, Options: options, } } -// LoadPlugins loads all plugins from the plugin directory -func (m *Manager) LoadPlugins() error { +// LoadPluginsFromDisk loads all plugins from the plugin directory +func (m *Manager) LoadPluginsFromDisk() error { // Load all plugins from the plugin directory foldersInPluginDir, err := os.ReadDir(m.Options.PluginDir) if err != nil { @@ -54,9 +63,20 @@ func (m *Manager) LoadPlugins() error { m.Log("Failed to load plugin: "+filepath.Base(pluginPath), err) continue } + thisPlugin.RootDir = pluginPath m.LoadedPlugins.Store(thisPlugin.Spec.ID, thisPlugin) + m.Log("Loaded plugin: "+thisPlugin.Spec.Name, nil) + + //TODO: Move this to a separate function + // Enable the plugin if it is enabled in the database + err = m.StartPlugin(thisPlugin.Spec.ID) + if err != nil { + m.Log("Failed to enable plugin: "+thisPlugin.Spec.Name, err) + } + } } + return nil } @@ -71,34 +91,21 @@ func (m *Manager) GetPluginByID(pluginID string) (*Plugin, error) { // EnablePlugin enables a plugin func (m *Manager) EnablePlugin(pluginID string) error { - plugin, ok := m.LoadedPlugins.Load(pluginID) - if !ok { - return errors.New("plugin not found") + err := m.StartPlugin(pluginID) + if err != nil { + return err } - plugin.(*Plugin).Enabled = true + //TODO: Add database record return nil } // DisablePlugin disables a plugin func (m *Manager) DisablePlugin(pluginID string) error { - plugin, ok := m.LoadedPlugins.Load(pluginID) - if !ok { - return errors.New("plugin not found") + err := m.StopPlugin(pluginID) + //TODO: Add database record + if err != nil { + return err } - - thisPlugin := plugin.(*Plugin) - thisPlugin.Process.Process.Signal(os.Interrupt) - go func() { - //Wait for 10 seconds for the plugin to stop gracefully - time.Sleep(10 * time.Second) - if thisPlugin.Process.ProcessState == nil || !thisPlugin.Process.ProcessState.Exited() { - m.Log("Plugin "+thisPlugin.Spec.Name+" failed to stop gracefully, killing it", nil) - thisPlugin.Process.Process.Kill() - } else { - m.Log("Plugin "+thisPlugin.Spec.Name+" background process stopped", nil) - } - }() - thisPlugin.Enabled = false return nil } diff --git a/src/mod/plugins/utils.go b/src/mod/plugins/utils.go index a6671ea..aeb0e1f 100644 --- a/src/mod/plugins/utils.go +++ b/src/mod/plugins/utils.go @@ -60,3 +60,22 @@ func getRandomPortNumber() int { } return portNo } + +func validatePluginSpec(pluginSpec *IntroSpect) error { + if pluginSpec.Name == "" { + return errors.New("plugin name is empty") + } + if pluginSpec.Description == "" { + return errors.New("plugin description is empty") + } + if pluginSpec.Author == "" { + return errors.New("plugin author is empty") + } + if pluginSpec.UIPath == "" { + return errors.New("plugin UI path is empty") + } + if pluginSpec.ID == "" { + return errors.New("plugin ID is empty") + } + return nil +} diff --git a/src/start.go b/src/start.go index d237b8a..7a7171d 100644 --- a/src/start.go +++ b/src/start.go @@ -26,6 +26,7 @@ import ( "imuslab.com/zoraxy/mod/mdns" "imuslab.com/zoraxy/mod/netstat" "imuslab.com/zoraxy/mod/pathrule" + "imuslab.com/zoraxy/mod/plugins" "imuslab.com/zoraxy/mod/sshprox" "imuslab.com/zoraxy/mod/statistic" "imuslab.com/zoraxy/mod/statistic/analytic" @@ -317,6 +318,25 @@ func startupSequence() { log.Fatal(err) } + /* + Plugin Manager + */ + + pluginManager = plugins.NewPluginManager(&plugins.ManagerOptions{ + PluginDir: "./plugins", + SystemConst: &plugins.RuntimeConstantValue{ + ZoraxyVersion: SYSTEM_VERSION, + ZoraxyUUID: nodeUUID, + }, + Database: sysdb, + Logger: SystemWideLogger, + }) + + err = pluginManager.LoadPluginsFromDisk() + if err != nil { + SystemWideLogger.PrintAndLog("Plugin Manager", "Failed to load plugins", err) + } + /* Docker UX Optimizer */ if runtime.GOOS == "windows" && *runningInDocker { SystemWideLogger.PrintAndLog("warning", "Invalid start flag combination: docker=true && runtime.GOOS == windows. Running in docker UX development mode.", nil) diff --git a/src/web/components/plugins.html b/src/web/components/plugins.html new file mode 100644 index 0000000..15a6775 --- /dev/null +++ b/src/web/components/plugins.html @@ -0,0 +1,40 @@ +
+
+

Plugins Manager

+

Add custom features to Zoraxy

+
+ + + + + + + + + + + + + + + + + + + + +
Plugin NameDescriptionsCatergoryVersionAuthorAction
{{plugin.name}}{{plugin.description}}{{plugin.category}}{{plugin.version}}{{plugin.author}} +
+ +
+ +
+
+ + + + + diff --git a/src/web/index.html b/src/web/index.html index 2880c16..8527b00 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -78,6 +78,9 @@ SSO / Oauth + + Plugins Manager + Static Web Server @@ -138,7 +141,10 @@
- + +
+ +
From 85709dacf647c756617ce26cf54f5070388d3b08 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Wed, 26 Feb 2025 21:19:41 +0800 Subject: [PATCH 06/14] Fixed #550 - Updated to not set the session cookie and lets the fallback method to detect for change in upstreams --- src/mod/dynamicproxy/loadbalance/originPicker.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mod/dynamicproxy/loadbalance/originPicker.go b/src/mod/dynamicproxy/loadbalance/originPicker.go index 8e1f5ee..babf0c3 100644 --- a/src/mod/dynamicproxy/loadbalance/originPicker.go +++ b/src/mod/dynamicproxy/loadbalance/originPicker.go @@ -54,7 +54,8 @@ func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.R return origins[targetOriginId], nil } //No sticky session, get a random origin - m.clearSessionHandler(w, r) + //Commented due to issue #550 + //m.clearSessionHandler(w, r) //Filter the offline origins origins = m.FilterOfflineOrigins(origins) From dd4df0b4dba6cb8e9b0aa5924dcf0f23ffec4061 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Wed, 26 Feb 2025 21:20:35 +0800 Subject: [PATCH 07/14] Update originPicker.go - Removed unused function --- .../dynamicproxy/loadbalance/originPicker.go | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/src/mod/dynamicproxy/loadbalance/originPicker.go b/src/mod/dynamicproxy/loadbalance/originPicker.go index babf0c3..d5229a8 100644 --- a/src/mod/dynamicproxy/loadbalance/originPicker.go +++ b/src/mod/dynamicproxy/loadbalance/originPicker.go @@ -53,10 +53,8 @@ func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.R //fmt.Println("DEBUG: (Sticky Session) Picking origin " + origins[targetOriginId].OriginIpOrDomain) return origins[targetOriginId], nil } - //No sticky session, get a random origin - //Commented due to issue #550 - //m.clearSessionHandler(w, r) + //No sticky session, get a random origin //Filter the offline origins origins = m.FilterOfflineOrigins(origins) if len(origins) == 0 { @@ -98,22 +96,6 @@ func (m *RouteManager) setSessionHandler(w http.ResponseWriter, r *http.Request, return nil } -// Clear the zoraxy only session values -func (m *RouteManager) clearSessionHandler(w http.ResponseWriter, r *http.Request) error { - session, err := m.SessionStore.Get(r, STICKY_SESSION_NAME) - if err != nil { - return err - } - - session.Values["zr_sid_origin"] = "" - session.Values["zr_sid_index"] = -1 - err = session.Save(r, w) - if err != nil { - return err - } - return nil -} - // Get the previous connected origin from session func (m *RouteManager) getSessionHandler(r *http.Request, upstreams []*Upstream) (int, error) { // Get existing session From bddff0cf2f93d28f6b0db861c4e2d5417ad9828f Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Thu, 27 Feb 2025 22:27:13 +0800 Subject: [PATCH 08/14] Added working plugin manager prototype - Added experimental plugin UI proxy - Added plugin icon loader - Added plugin table renderer --- example/plugins/helloworld/icon.png | Bin 0 -> 14120 bytes example/plugins/helloworld/icon.psd | Bin 0 -> 134796 bytes src/api.go | 8 +++ src/mod/plugins/handler.go | 83 ++++++++++++++++++++++++++++ src/mod/plugins/lifecycle.go | 56 ++++++++++++++++--- src/mod/plugins/no_img.png | Bin 0 -> 8404 bytes src/mod/plugins/no_img.psd | Bin 0 -> 96484 bytes src/mod/plugins/plugins.go | 59 ++++++++++++++++---- src/mod/plugins/uirouter.go | 41 ++++++++++++++ src/router.go | 13 +++++ src/start.go | 4 ++ src/web/components/plugins.html | 67 ++++++++++++++++------ src/web/index.html | 2 +- 13 files changed, 294 insertions(+), 39 deletions(-) create mode 100644 example/plugins/helloworld/icon.png create mode 100644 example/plugins/helloworld/icon.psd create mode 100644 src/mod/plugins/handler.go create mode 100644 src/mod/plugins/no_img.png create mode 100644 src/mod/plugins/no_img.psd create mode 100644 src/mod/plugins/uirouter.go diff --git a/example/plugins/helloworld/icon.png b/example/plugins/helloworld/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..69c3e2984c2b0a95bbd0f9102f590a43edd4de05 GIT binary patch literal 14120 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 literal 0 HcmV?d00001 diff --git a/src/mod/plugins/plugins.go b/src/mod/plugins/plugins.go index 957eaa4..181d3e5 100644 --- a/src/mod/plugins/plugins.go +++ b/src/mod/plugins/plugins.go @@ -2,12 +2,16 @@ package plugins import ( "errors" + "fmt" "os" "os/exec" "path/filepath" "sync" + _ "embed" + "imuslab.com/zoraxy/mod/database" + "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/info/logger" "imuslab.com/zoraxy/mod/utils" ) @@ -15,8 +19,10 @@ import ( type Plugin struct { RootDir string //The root directory of the plugin Spec *IntroSpect //The plugin specification - Process *exec.Cmd //The process of the plugin Enabled bool //Whether the plugin is enabled + + uiProxy *dpcore.ReverseProxy //The reverse proxy for the plugin UI + process *exec.Cmd //The process of the plugin } type ManagerOptions struct { @@ -31,16 +37,22 @@ type Manager struct { Options *ManagerOptions } +//go:embed no_img.png +var noImg []byte + // NewPluginManager creates a new plugin manager func NewPluginManager(options *ManagerOptions) *Manager { + //Create plugin directory if not exists if options.PluginDir == "" { options.PluginDir = "./plugins" } - if !utils.FileExists(options.PluginDir) { os.MkdirAll(options.PluginDir, 0755) } + //Create database table + options.Database.NewTable("plugins") + return &Manager{ LoadedPlugins: sync.Map{}, Options: options, @@ -63,17 +75,18 @@ func (m *Manager) LoadPluginsFromDisk() error { m.Log("Failed to load plugin: "+filepath.Base(pluginPath), err) continue } - thisPlugin.RootDir = pluginPath + thisPlugin.RootDir = filepath.ToSlash(pluginPath) m.LoadedPlugins.Store(thisPlugin.Spec.ID, thisPlugin) m.Log("Loaded plugin: "+thisPlugin.Spec.Name, nil) - //TODO: Move this to a separate function - // Enable the plugin if it is enabled in the database - err = m.StartPlugin(thisPlugin.Spec.ID) - if err != nil { - m.Log("Failed to enable plugin: "+thisPlugin.Spec.Name, err) + // If the plugin was enabled, start it now + fmt.Println(m.GetPluginPreviousEnableState(thisPlugin.Spec.ID)) + if m.GetPluginPreviousEnableState(thisPlugin.Spec.ID) { + err = m.StartPlugin(thisPlugin.Spec.ID) + if err != nil { + m.Log("Failed to enable plugin: "+thisPlugin.Spec.Name, err) + } } - } } @@ -95,26 +108,48 @@ func (m *Manager) EnablePlugin(pluginID string) error { if err != nil { return err } - //TODO: Add database record + m.Options.Database.Write("plugins", pluginID, true) return nil } // DisablePlugin disables a plugin func (m *Manager) DisablePlugin(pluginID string) error { err := m.StopPlugin(pluginID) - //TODO: Add database record + m.Options.Database.Write("plugins", pluginID, false) if err != nil { return err } return nil } +// GetPluginPreviousEnableState returns the previous enable state of a plugin +func (m *Manager) GetPluginPreviousEnableState(pluginID string) bool { + enableState := true + err := m.Options.Database.Read("plugins", pluginID, &enableState) + if err != nil { + //Default to true + return true + } + return enableState +} + +// ListLoadedPlugins returns a list of loaded plugins +func (m *Manager) ListLoadedPlugins() ([]*Plugin, error) { + var plugins []*Plugin + m.LoadedPlugins.Range(func(key, value interface{}) bool { + plugin := value.(*Plugin) + plugins = append(plugins, plugin) + return true + }) + return plugins, nil +} + // Terminate all plugins and exit func (m *Manager) Close() { m.LoadedPlugins.Range(func(key, value interface{}) bool { plugin := value.(*Plugin) if plugin.Enabled { - m.DisablePlugin(plugin.Spec.ID) + m.StopPlugin(plugin.Spec.ID) } return true }) diff --git a/src/mod/plugins/uirouter.go b/src/mod/plugins/uirouter.go new file mode 100644 index 0000000..62414de --- /dev/null +++ b/src/mod/plugins/uirouter.go @@ -0,0 +1,41 @@ +package plugins + +import ( + "net/http" + + "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" + "imuslab.com/zoraxy/mod/utils" +) + +// HandlePluginUI handles the request to the plugin UI +// This function will route the request to the correct plugin UI handler +func (m *Manager) HandlePluginUI(pluginID string, w http.ResponseWriter, r *http.Request) { + plugin, err := m.GetPluginByID(pluginID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + //Check if the plugin has UI + if plugin.Spec.UIPath == "" { + utils.SendErrorResponse(w, "Plugin does not have UI") + return + } + + //Check if the plugin has UI handler + if plugin.uiProxy == nil { + utils.SendErrorResponse(w, "Plugin does not have UI handler") + return + } + + //Call the plugin UI handler + plugin.uiProxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ + UseTLS: false, + OriginalHost: r.Host, + ProxyDomain: r.Host, + NoCache: true, + PathPrefix: "/plugin.ui/" + pluginID, + Version: m.Options.SystemConst.ZoraxyVersion, + }) + +} diff --git a/src/router.go b/src/router.go index 7fab6cf..e7a4645 100644 --- a/src/router.go +++ b/src/router.go @@ -58,6 +58,19 @@ func FSHandler(handler http.Handler) http.Handler { return } + //For Plugin Routing + if strings.HasPrefix(r.URL.Path, "/plugin.ui/") { + //Extract the plugin ID from the request path + parts := strings.Split(r.URL.Path, "/") + if len(parts) > 2 { + pluginID := parts[2] + pluginManager.HandlePluginUI(pluginID, w, r) + } else { + http.Error(w, "Invalid Usage", http.StatusInternalServerError) + } + return + } + //For WebSSH Routing //Example URL Path: /web.ssh/{{instance_uuid}}/* if strings.HasPrefix(r.URL.Path, "/web.ssh/") { diff --git a/src/start.go b/src/start.go index 7a7171d..f9d757d 100644 --- a/src/start.go +++ b/src/start.go @@ -384,6 +384,10 @@ func ShutdownSeq() { if acmeAutoRenewer != nil { acmeAutoRenewer.Close() } + //Close the plugin manager + SystemWideLogger.Println("Shutting down plugin manager") + pluginManager.Close() + //Remove the tmp folder SystemWideLogger.Println("Cleaning up tmp files") os.RemoveAll("./tmp") diff --git a/src/web/components/plugins.html b/src/web/components/plugins.html index 15a6775..7d0e780 100644 --- a/src/web/components/plugins.html +++ b/src/web/components/plugins.html @@ -1,7 +1,7 @@
-

Plugins Manager

-

Add custom features to Zoraxy

+

Plugins

+

Custom features on Zoraxy

@@ -9,30 +9,61 @@ - - - - - - - - - - - + +
Plugin Name Descriptions CatergoryVersionAuthor Action
{{plugin.name}}{{plugin.description}}{{plugin.category}}{{plugin.version}}{{plugin.author}} -
- -
- -
diff --git a/src/web/index.html b/src/web/index.html index 8527b00..c2aaca3 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -79,7 +79,7 @@ - Plugins Manager + Plugins Static Web Server From 53657e8716c79e33702205e6d21e71c6cd96c890 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Fri, 28 Feb 2025 15:46:57 +0800 Subject: [PATCH 09/14] Added embed server for plugin library - Added embeded resources server for plugin library - Added ztnc plugin for global area network - Added wide mode for side wrapper --- .gitignore | 3 +- example/plugins/helloworld/main.go | 33 +- example/plugins/helloworld/www/index.html | 34 + .../helloworld/zoraxy_plugin/README.txt | 19 + .../zoraxy_plugin/embed_webserver.go | 106 +++ .../helloworld/zoraxy_plugin/zoraxy_plugin.go | 24 + example/plugins/ztnc/README.md | 11 + example/plugins/ztnc/authtoken.secret | 1 + example/plugins/ztnc/go.mod | 11 + example/plugins/ztnc/go.sum | 30 + example/plugins/ztnc/icon.png | Bin 0 -> 7839 bytes example/plugins/ztnc/icon.psd | Bin 0 -> 127851 bytes example/plugins/ztnc/main.go | 81 ++ example/plugins/ztnc/mod/database/database.go | 146 ++++ .../ztnc/mod/database/database_core.go | 70 ++ .../ztnc/mod/database/database_openwrt.go | 196 +++++ .../ztnc/mod/database/dbbolt/dbbolt.go | 141 ++++ .../ztnc/mod/database/dbbolt/dbbolt_test.go | 67 ++ .../plugins/ztnc/mod/database/dbinc/dbinc.go | 39 + .../ztnc/mod/database/dbleveldb/dbleveldb.go | 152 ++++ .../mod/database/dbleveldb/dbleveldb_test.go | 141 ++++ example/plugins/ztnc/mod/ganserv/authkey.go | 80 ++ .../plugins/ztnc/mod/ganserv/authkeyLinux.go | 37 + .../plugins/ztnc/mod/ganserv/authkeyWin.go | 73 ++ example/plugins/ztnc/mod/ganserv/ganserv.go | 130 +++ example/plugins/ztnc/mod/ganserv/handlers.go | 504 ++++++++++++ example/plugins/ztnc/mod/ganserv/network.go | 39 + .../plugins/ztnc/mod/ganserv/network_test.go | 55 ++ example/plugins/ztnc/mod/ganserv/utils.go | 55 ++ example/plugins/ztnc/mod/ganserv/zerotier.go | 669 ++++++++++++++++ example/plugins/ztnc/mod/utils/conv.go | 105 +++ example/plugins/ztnc/mod/utils/template.go | 19 + example/plugins/ztnc/mod/utils/utils.go | 202 +++++ .../ztnc/mod/zoraxy_plugin/embed_webserver.go | 106 +++ .../ztnc/mod/zoraxy_plugin/zoraxy_plugin.go | 32 +- example/plugins/ztnc/start.go | 69 ++ example/plugins/ztnc/web/details.html | 747 ++++++++++++++++++ example/plugins/ztnc/web/index.html | 257 ++++++ example/plugins/ztnc/ztnc.db | Bin 0 -> 32768 bytes example/plugins/ztnc/ztnc.db.lock | 0 src/mod/plugins/introspect.go | 6 +- src/mod/plugins/lifecycle.go | 12 +- src/mod/plugins/no_img.png | Bin 8404 -> 43595 bytes src/mod/plugins/no_img.psd | Bin 96484 -> 189610 bytes src/mod/plugins/plugins.go | 25 +- src/mod/plugins/uirouter.go | 20 +- src/mod/plugins/utils.go | 3 +- src/mod/plugins/zoraxy_plugin/README.txt | 19 + .../plugins/zoraxy_plugin/embed_webserver.go | 106 +++ .../plugins/zoraxy_plugin/zoraxy_plugin.go | 210 +++++ src/start.go | 7 +- src/web/components/plugins.html | 8 +- src/web/index.html | 8 +- src/web/main.css | 4 + 54 files changed, 4870 insertions(+), 42 deletions(-) create mode 100644 example/plugins/helloworld/www/index.html create mode 100644 example/plugins/helloworld/zoraxy_plugin/README.txt create mode 100644 example/plugins/helloworld/zoraxy_plugin/embed_webserver.go create mode 100644 example/plugins/ztnc/README.md create mode 100644 example/plugins/ztnc/authtoken.secret create mode 100644 example/plugins/ztnc/go.mod create mode 100644 example/plugins/ztnc/go.sum create mode 100644 example/plugins/ztnc/icon.png create mode 100644 example/plugins/ztnc/icon.psd create mode 100644 example/plugins/ztnc/main.go create mode 100644 example/plugins/ztnc/mod/database/database.go create mode 100644 example/plugins/ztnc/mod/database/database_core.go create mode 100644 example/plugins/ztnc/mod/database/database_openwrt.go create mode 100644 example/plugins/ztnc/mod/database/dbbolt/dbbolt.go create mode 100644 example/plugins/ztnc/mod/database/dbbolt/dbbolt_test.go create mode 100644 example/plugins/ztnc/mod/database/dbinc/dbinc.go create mode 100644 example/plugins/ztnc/mod/database/dbleveldb/dbleveldb.go create mode 100644 example/plugins/ztnc/mod/database/dbleveldb/dbleveldb_test.go create mode 100644 example/plugins/ztnc/mod/ganserv/authkey.go create mode 100644 example/plugins/ztnc/mod/ganserv/authkeyLinux.go create mode 100644 example/plugins/ztnc/mod/ganserv/authkeyWin.go create mode 100644 example/plugins/ztnc/mod/ganserv/ganserv.go create mode 100644 example/plugins/ztnc/mod/ganserv/handlers.go create mode 100644 example/plugins/ztnc/mod/ganserv/network.go create mode 100644 example/plugins/ztnc/mod/ganserv/network_test.go create mode 100644 example/plugins/ztnc/mod/ganserv/utils.go create mode 100644 example/plugins/ztnc/mod/ganserv/zerotier.go create mode 100644 example/plugins/ztnc/mod/utils/conv.go create mode 100644 example/plugins/ztnc/mod/utils/template.go create mode 100644 example/plugins/ztnc/mod/utils/utils.go create mode 100644 example/plugins/ztnc/mod/zoraxy_plugin/embed_webserver.go rename src/mod/plugins/includes.go => example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go (89%) create mode 100644 example/plugins/ztnc/start.go create mode 100644 example/plugins/ztnc/web/details.html create mode 100644 example/plugins/ztnc/web/index.html create mode 100644 example/plugins/ztnc/ztnc.db create mode 100644 example/plugins/ztnc/ztnc.db.lock create mode 100644 src/mod/plugins/zoraxy_plugin/README.txt create mode 100644 src/mod/plugins/zoraxy_plugin/embed_webserver.go create mode 100644 src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go diff --git a/.gitignore b/.gitignore index 8cdd224..8eea333 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,5 @@ src/log/ # dev-tags /Dockerfile -/Entrypoint.sh \ No newline at end of file +/Entrypoint.sh +example/plugins/zerotiernc/authtoken.secret diff --git a/example/plugins/helloworld/main.go b/example/plugins/helloworld/main.go index b05ab65..d4aec15 100644 --- a/example/plugins/helloworld/main.go +++ b/example/plugins/helloworld/main.go @@ -1,6 +1,7 @@ package main import ( + "embed" _ "embed" "fmt" "net/http" @@ -9,12 +10,14 @@ import ( plugin "example.com/zoraxy/helloworld/zoraxy_plugin" ) -//go:embed index.html -var indexHTML string +const ( + PLUGIN_ID = "com.example.helloworld" + UI_PATH = "/" + WEB_ROOT = "/www" +) -func helloWorldHandler(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, indexHTML) -} +//go:embed www/* +var content embed.FS func main() { // Serve the plugin intro spect @@ -33,17 +36,23 @@ func main() { // As this is a utility plugin, we don't need to capture any traffic // but only serve the UI, so we set the UI (relative to the plugin path) to "/" - UIPath: "/", + UIPath: UI_PATH, }) - if err != nil { //Terminate or enter standalone mode here panic(err) } - // Serve the hello world page - // This will serve the index.html file embedded in the binary - http.HandleFunc("/", helloWorldHandler) - fmt.Println("Server started at http://localhost:" + strconv.Itoa(runtimeCfg.Port)) - http.ListenAndServe(":"+strconv.Itoa(runtimeCfg.Port), nil) + // Register the shutdown handler + plugin.RegisterShutdownHandler(func() { + // Do cleanup here if needed + fmt.Println("Hello World Plugin Exited") + }) + + embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH) + + // Serve the hello world page in the www folder + http.Handle(UI_PATH, embedWebRouter.Handler()) + fmt.Println("Hello World started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port)) + http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil) } diff --git a/example/plugins/helloworld/www/index.html b/example/plugins/helloworld/www/index.html new file mode 100644 index 0000000..2dcf1f1 --- /dev/null +++ b/example/plugins/helloworld/www/index.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + Hello World + + + + + + +
+

Hello World

+

Welcome to your first Zoraxy plugin

+
+ + \ No newline at end of file diff --git a/example/plugins/helloworld/zoraxy_plugin/README.txt b/example/plugins/helloworld/zoraxy_plugin/README.txt new file mode 100644 index 0000000..ed8a405 --- /dev/null +++ b/example/plugins/helloworld/zoraxy_plugin/README.txt @@ -0,0 +1,19 @@ +# Zoraxy Plugin + +## Overview +This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components. + +## Instructions + +1. **Copy the Module:** + - Copy the entire `zoraxy_plugin` module to your plugin mod folder. + +2. **Include the Structure:** + - Ensure that you maintain the directory structure and file organization as provided in this module. + +3. **Modify as Needed:** + - Customize the copied module to implement the desired functionality for your plugin. + +## Directory Structure + zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup + embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages \ No newline at end of file diff --git a/example/plugins/helloworld/zoraxy_plugin/embed_webserver.go b/example/plugins/helloworld/zoraxy_plugin/embed_webserver.go new file mode 100644 index 0000000..35580dd --- /dev/null +++ b/example/plugins/helloworld/zoraxy_plugin/embed_webserver.go @@ -0,0 +1,106 @@ +package zoraxy_plugin + +import ( + "embed" + "fmt" + "io/fs" + "net/http" + "net/url" + "strings" + "time" +) + +type PluginUiRouter struct { + PluginID string //The ID of the plugin + TargetFs *embed.FS //The embed.FS where the UI files are stored + TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web + HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui +} + +// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS +// The targetFsPrefix is the prefix of the embed.FS where the UI files are stored +// The targetFsPrefix should be relative to the root of the embed.FS +// The targetFsPrefix should start with a slash (e.g. /web) that corresponds to the root folder of the embed.FS +// The handlerPrefix is the prefix of the handler used to route this router +// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path +// All prefix should not end with a slash +func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter { + //Make sure all prefix are in /prefix format + if !strings.HasPrefix(targetFsPrefix, "/") { + targetFsPrefix = "/" + targetFsPrefix + } + targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/") + + if !strings.HasPrefix(handlerPrefix, "/") { + handlerPrefix = "/" + handlerPrefix + } + handlerPrefix = strings.TrimSuffix(handlerPrefix, "/") + + //Return the PluginUiRouter + return &PluginUiRouter{ + PluginID: pluginID, + TargetFs: targetFs, + TargetFsPrefix: targetFsPrefix, + HandlerPrefix: handlerPrefix, + } +} + +func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler { + //Get the CSRF token from header + csrfToken := r.Header.Get("X-Zoraxy-Csrf") + if csrfToken == "" { + csrfToken = "missing-csrf-token" + } + + //Return the middleware + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if the request is for an HTML file + if strings.HasSuffix(r.URL.Path, "/") { + // Redirect to the index.html + http.Redirect(w, r, r.URL.Path+"index.html", http.StatusFound) + return + } + if strings.HasSuffix(r.URL.Path, ".html") { + //Read the target file from embed.FS + targetFilePath := strings.TrimPrefix(r.URL.Path, "/") + targetFilePath = p.TargetFsPrefix + "/" + targetFilePath + targetFilePath = strings.TrimPrefix(targetFilePath, "/") + fmt.Println(targetFilePath) + targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath) + if err != nil { + http.Error(w, "File not found", http.StatusNotFound) + return + } + body := string(targetFileContent) + body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken) + http.ServeContent(w, r, r.URL.Path, time.Now(), strings.NewReader(body)) + return + } + + //Call the next handler + fsHandler.ServeHTTP(w, r) + }) + +} + +// GetHttpHandler returns the http.Handler for the PluginUiRouter +func (p *PluginUiRouter) Handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + //Remove the plugin UI handler path prefix + rewrittenURL := r.RequestURI + rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix) + rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + r.URL, _ = url.Parse(rewrittenURL) + r.RequestURI = rewrittenURL + //Serve the file from the embed.FS + subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/")) + if err != nil { + fmt.Println(err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Replace {{csrf_token}} with the actual CSRF token and serve the file + p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r) + }) +} diff --git a/example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go b/example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go index 4778e4e..1691591 100644 --- a/example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go +++ b/example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go @@ -4,7 +4,9 @@ import ( "encoding/json" "fmt" "os" + "os/signal" "strings" + "syscall" ) /* @@ -184,3 +186,25 @@ func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) { ServeIntroSpect(pluginSpect) return RecvConfigureSpec() } + +/* + +Shutdown handler + +This function will register a shutdown handler for the plugin +The shutdown callback will be called when the plugin is shutting down +You can use this to clean up resources like closing database connections +*/ + +func RegisterShutdownHandler(shutdownCallback func()) { + // Set up a channel to receive OS signals + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + // Start a goroutine to listen for signals + go func() { + <-sigChan + shutdownCallback() + os.Exit(0) + }() +} diff --git a/example/plugins/ztnc/README.md b/example/plugins/ztnc/README.md new file mode 100644 index 0000000..a942efd --- /dev/null +++ b/example/plugins/ztnc/README.md @@ -0,0 +1,11 @@ +## Global Area Network Plugin + +This plugin implements a user interface for ZeroTier Network Controller in Zoraxy + + + + + +## License + +AGPL \ No newline at end of file diff --git a/example/plugins/ztnc/authtoken.secret b/example/plugins/ztnc/authtoken.secret new file mode 100644 index 0000000..fa08db2 --- /dev/null +++ b/example/plugins/ztnc/authtoken.secret @@ -0,0 +1 @@ +hgaode9ptnpuaoi1ilbdw9i4 \ No newline at end of file diff --git a/example/plugins/ztnc/go.mod b/example/plugins/ztnc/go.mod new file mode 100644 index 0000000..aa0cc97 --- /dev/null +++ b/example/plugins/ztnc/go.mod @@ -0,0 +1,11 @@ +module aroz.org/zoraxy/ztnc + +go 1.23.6 + +require ( + github.com/boltdb/bolt v1.3.1 + github.com/syndtr/goleveldb v1.0.0 + golang.org/x/sys v0.30.0 +) + +require github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect diff --git a/example/plugins/ztnc/go.sum b/example/plugins/ztnc/go.sum new file mode 100644 index 0000000..875979f --- /dev/null +++ b/example/plugins/ztnc/go.sum @@ -0,0 +1,30 @@ +github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/example/plugins/ztnc/icon.png b/example/plugins/ztnc/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e19e043a8d2695583a0216ced15162e20008bc64 GIT binary patch literal 7839 zcmc(DcU05C^6w@Dr4xFG0D=V+qzHm^5mb<(Gywroic|%GP=iQOsv-)~6cj}1U0MQy zprHr|NDtDacS1|@`{KRty!-Auzu&p%{qu58&Q4}$K07lzJF}Z7hSzi$>3Hb?0F0M) zFB$^?he$Y}IUE9bwF;nt)>GHQ3jlhy!!HcHPU8lEPTyHe%h1rt!`s8l$-@(MSxXD$ z`M|@$+4U{}enaWkea#bzoT}r*yUeDVzgA0LxrPD}(R`XVq(v6;Ik0<-WL0kokD@bW z=U_?GHEDj-N2i+}_500J0P|pBCgmso4mS7Cp&fI8Y2W{M-_8ed7B?@eQiET4 zi6dO#R~COB4Tyz-loL9~;Oe!&)+E$e5{_#EZ}Qbe&LD7wFpNc|j1C-APYrzH%oCW^ zZ2=}lQ?ebP6yZQikgbCXrAPHfx(7?hv+)eC)`bBaKB?o?)gD>)>ZBjMzU!-m-@?C} zTuU_VxK1J+Xs1NdUZsk%>CRH=bus|JyW8DJR#Z!votc`LahP>o*=)%oyR3SMs0y!* zlj~yO2!OoSwK8NgJ>5^`*$OLuh;_-c1Vg64X?)jOc9{0fO}ZccyU}RH19icSw}x#O z1o-Lb>6gTAzpa;^j8>({PIwTagSVks^05CZxc^!`r2v;5M!Ik3?o((SH;YGTo^>bA zijrHyLr8HayMN1-SV?j`T{C}DZ78DOivDc;?4t0uq~EM_Au?-1FG;kCzKkG0!T@)z z@@NI#6&Jh=gGV!&_xJ$y;EmHUy!6ii(os$ZO#~njyBn`A0RtVc-c15f_lMi+!wW9G zuQUK$d=q-ASmP)|BYQrMDXmeU4#x$zJ91H-;TY}$QuD|ZhM(>&_e%|)8#tw3h>Bt= z-Zw0-Cy?chXB8P|39MG33!Yp@Y`HUDM=2-QIG{)|i)BF5SH4f-0EGtlaLae;#^+pr>*6nC`Po4l#pvRxy#TR8(HOQxnp=;NO|Fj;5_HV<%(Xs*Hjx({@BVGv&=kqmI0cpEMt%lrnE#c6;MvD0MwDk4^63Yq5@0PA&Yq)7gDL zUOPKF6P={yylZGj!vwWo{dm>m^5^K>>|EHUF9Tx)V_2*1?GrpMJOMnp+*LeMDSA27 zy{}T*Zb%Qv@TE8=GbekPn4j7{70B`IJ8P0sl4(+K((K0@CSE3Ledv$$H$Is#f4XHF zlyk|X%y2S~_9y43zB~bw+PrgxoyNY#@rGSSHrWj}q(^)9jHMCbm)=C^X5X-{`pOe4 z%=uO3?wYw~{{vI11xc4={o9tMyNh~0)cbrsQUQ9`wY)w$ef(sOe{D&HbZZADp;#^u7T6yT1uAVNLzNO@*M0`kF zbFRjyCcI|&Vb{EBul)jgj&|$&7Sr5lx6YQ^X7(1>{7U~yw~&Ig9L>1|9@n#q_N(m) z_J3TGovXW7A5VN>!8_GFYj(=+zMOr$T6{UuaZ=uoOVuIYga3BE+}Pa_mkV~con?mK zHNR__YB<^P!sWF?O?P>(cYAxAxK_v!{2MCwOoll7v1`D z=w%?D`lbF$Vt%rzswvuZ%XA?Ba=+Dr)SQYyakLqTdmvrnqqIQwt* z_>(;+x30Snon1Uwq+5Kwcx37Pw%xYaw%c}>Ks!IHK)kz0SFI zjoKF02%Co0hw_G&QMphFAyN?UkIGZWQr8}lVo>GKWT0iV5PFd4f#X`|+qIV+ag==B z&GDTko#Q6oo^-poo?t&qTtpYmI5OBHXIF2oy^*rcJ%R@zUGAaC#)`GuO;qCqBdXf`zz5v_$MIwX&!n3sZ7Yr{2Wjm&MHpOq_*ItHz*qwP zjOY1h(G7jmi~VEyU4!=rPoIC0_989Yn9=!Vxx|2U)rl$t2MLKp#wqlS+jqXi$nV4{ z(Os{NVGh@Dz3%LC<3d9wPCcm)5`LE4mVVW`^}08I%8omb=XXWvA8`iOClni27nl92 z_OnRn(bJ!)_F1#Y3;hK9zj zRDEAs&|P@9;GR#kz!qJpzo*gnBVv`YfNw2!n$O1V%9!_C|9e|;9pxS0JE-LCJ7#+#A~K2_?fxXYX@z)_bfje zjpx11SA(4i9qrq6TT1I@d@fc{SIKia72`3Ky7bz& z!t>@SrY4Q)#^d8TfdP|-f8KU}8TryNo)S{EMYEubmgrH!KFmLz?e<3HZT-rhnCx_c zTLL-_Rg~bBvy=7@Tjk@;GTx-0P+8lZdXzYsdnY#|{hFG}&y*1~QSTtd#oEZa-chS7 zLygOOS+B~5_-$isHr{%*&T~_Qczal^>S}>Xp^8qhCuu^r$kJosE)lTUzNM~D1WtB&DM_3_7ayb-Ll?@wST-{(LuuyY5wRQ z^L$7-x#TX+jj&^jv+ep~!Mwsm8<|0VLFtX0=+SA{=@Y#q8XKQ3B*e$aTfcs%GjSrf zG?%UGd$1`5z2Nm}cAWHk|7w@{!LL>N-RLfCAv$m4!OEw+BBfT*R-w91w8~n}K|$wm z&`R}6&3J3QP|Aq|b+X|`_nv)g?hgZ^>TDqSK#KTyV_&Q%Qx>{yqU?2z^#Sk~0{{~S zz%B(E=Ky#p4ZyrD07}UKaCtnpX}JUdTl?jU8aMog7DwG&O^89bwX2Us98e6po{Z(^ zo?O=Zkou#|D@yDA?H&w(4yAHtvXPYxbofdV}`K##yWVy}dCn)l4%tFRc#rP7q~w zBp|-kYP9wgO#NWQfek=67C68_E`a|h^8c?oWc0rn-!0QGj=Ca#VQX9Wpv1b?>{sQ* zuH;VvW`lIxYwH+*=`K1-1CsasA@a#za!2Mb7jek_KS|roY=}Ff3UO`!9FmvRWYi z1zHNiY`u>p%-KspNJ=s5*GhB{>VSb)RJ?(wI1B+cT0Bcqk)WO3zJ;s_r1fK1m6>2T zn^jKJUBQyr%r_c zUa);$7)l8r!lprG1w}bWjrR=2y3%x>6WDg+_ zm_O;Wisz8vf$J`FoCyNQ%6w^_rW?RewJ&c#C>Wslsj>SJXm;3TX0k@0;&KzH1>$%R zxXs8I!Sjbu3yS<|G~nem#pf(Qay;sr(K~2iwj&UJ@j>gvV_;kGn2HaE^JhHD0k=(> zy4W%i3lvrKLaY!dfH7f$6PkP+Sc!J9acT0C1u+@&xVuBT;9-M;1 z!NoZdoHYuDOHxFEvkoU8iwePT>q1BnjVeZHKyOnN0z^ykN77)XnHA}g0DlI+idSKH z76d5f)&!E(xU)}y`8q}x4lDi%N8rSZf5EOo?^(#oHh60;05Nh^m#6_=LLEqY;bf=} zXC(6h0z`2t=u!g+3>V5jpX9ulU;$Va}IzYZ0`kN|bO z+T-LQzyCV&k@EXPHS^KG@*^eehvt1cMboHad}vz!eSp9`u2omO!gA>LI-N+Shq3nRQ zVu9@%b(}h+48>Bi5!a&wfTSt{vuI9tp1I@z`h^cc;IyG*1G!+*J5GW&J&OSnjV^{J zJy`$^CK@o}=%`>gi>G2xE-}F9=@DS|)MxLD79%vElT9xK0k~l1y(pk)SNvQZc+TIJ z4+DyBa=?WKp-z?d#hfe(I)(nii6^$S#!N?p1P;e%M{M^X{yk|D3ntkb;RNE42pkWS zsGqAjnI2g9v%G;Ro>1=B%0+)ux-H& zwEQ|eio-!}zcyPrKuLyVM6>{OtiLDA4EmAoU}-RbTOp;^+WvRU-kD-4B)Aj;QZy#8*nG@sVf% z3%{F%454)QK39$!T-|EM+Ckj5&=uAz1XDj^Ecom*xk`2qf<*bm1Vf6_dm11CnnVB| zX_5x`0kZ#+kc1^nOd1*zU~ED#pdw8?B(otBQaL0cJctXChHyv#^UD#}tOHPIRHWBf zK-5)cy+8!`{ru?RRNG8r6qJZjrBLzz_jCI%NBBP>G2DDEYzd6pKhLSvFRkiRj7IVF zSEI#;FG6kLFLc|(*4N5z2qgIZp=|$YEL0RR0Liy9P&G`00?kJDF7f~5F>mP$oP+=K z&VY0~Zh2-9k^pz2azi!0&!;L~tF%(;9~m4BnkMaSle>z=@$@;@>gt__9-p^K-6V`& z&(Y_7#i%y*UW&3am=bqUqmx0#U*?o!^(XHLg5T|dHEOZz8T&q$Bcj8B`u6JLdoxRYzD2!ff99y~Y-~tvO~iK9 zwnBKF%jOE!ndbo*^P9b~GlzY1=ll4X&$Fgn{7{Mwh|Ml5_mrZEYYT_^D; zFWXaZk(xy9$n9Om6l%rM`XI_`(Ux%04NWqR1O>R(-{?Mc#-lej`Xx`vk?dPeqcqVA z>emS!%bEFT>Dyh27B=SttRJ}VygQwu5EL}Mh^xKh*p++0x!fRn+HTDCL}%Bh!Gj&7 zJK+VVeqqqv%fyg<0#*X-P@qE&S7SenMPfXs_MAuk-RTj12_fCBM>9p7V{ALGsWNl7 zclVaZ8aisju(4KYnZ)SWao+W%{k6%jHCs}^leB5EHCcmuRbT3V>xDbn)#ALbQ)XCN zlA5r^E$JN>-iGS5`E|ASSydZL%NZV38Zft+JwF-R>*#1=0mWH)Tt9G$5!Phgcyhm<76{}OJ#oA!nO)rU5P~MijVlbJK3=DuqjZeU;(cd zanQRR#DCCHbeB1#&p`+1rm9qQgxa{pj&)>~>rSRlK$U=MT-w}FMmvlJ^yDnv_8{=Z zXK*LNGSirXYj?g;+myXUf}WZ8Rr6FH@Ak;9YBuf*NCBY%8^+ z9>F()3j2@&u6`omUMiK4!U|t*3i}Srn>;p;sw5x_y0->6Lfh&pfxrN}jGdR?T1i~9vy9|NUTfqR z_%L4s+CY7KmoC1QvV~;z?o)WkQ(X!U)Pb#j`?Vcs zv5nR@<<7TD1v|SgSrB_gzYLl}z4B`Cs)QLUT>U51LlCNKZZ{yep`;|ZYzsI)*oJ}&G_yM%P*hUk_$4lT%$O5oiKYq>)dKbeLW|J;!xPCN zKkLh#)?aJCEo|Rbu=-p4&xPZ2US~U+)oo5xC`3YBLo4W2)^uSxq&#?FGXH=!lv(B4 zG1l?HbvtXVeUudULFOf#TH$N1ohNyr->jg;p^Q2DId|Nr#Mi)WBhzEg1Zo%yOw28@ zv|WBr8l>a*4UVy@9Zcupj(&On5$eOzQkHuik8rB;mD`eIn;L&>@=U!fc_nM&4*oq< zjj=l)-j`I&ZY~&H+^On|7gE@+sJI)g5QJRc+UGm7>Cp%EN5KY<`2l*$=8zh6Y1r-d z=wfc@P^jOX>&htd+@k^KC84y`87Pg!)O&SL zoI4t0mPshFzQ|6K{&NAGy5*m#+3(vyk)C&$HlRU=1$Z704mrE7?2Xo*c$viGpB=oh zJgLfY3QQH3kN5Eio8V^{D9nSz`;x&b{bfmBGW|g4C@W}d*2Z;yEVUyGLk2hBY1wm~ zPMh#3Y!t)O-#e$aj5dUWl!TY5e3@wt$jlBCtebN(vms+6q>`y(!!qIflLeICw^MFE z(mdgY34NvS5kj$?t7kVa`>+Z{#c#z7*n0C8mwl8xI679umRJQVU?f4P@7=dYL6h z9@ptAzdUIwbt-thJ8Ou|?cwpE>;AJ?$|nx`h76D8)*t`)JLOKnUQS%%=~qn9{f4}r z6gm!|f*+8-S(xR_<}jAxyAq>(1-c)aD!5lcTbVv;Hdf>zv|~iLgKqr!q)N@7Qa*Qm zWBFvV(=t5(C6g-uxJCXaSJD6V^87d1!-w9#vHtl){g1e?V%q-(_R UU0+Qx0DUfNU%ObSY5VBE0NQu90{{R3 literal 0 HcmV?d00001 diff --git a/example/plugins/ztnc/icon.psd b/example/plugins/ztnc/icon.psd new file mode 100644 index 0000000000000000000000000000000000000000..e8c221b8fa05ef43254524d65d6632579454e001 GIT binary patch literal 127851 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 Ve4#v2cNw3q!SjF5SD=3d{vY1s-r@iN literal 0 HcmV?d00001 diff --git a/example/plugins/ztnc/main.go b/example/plugins/ztnc/main.go new file mode 100644 index 0000000..b302275 --- /dev/null +++ b/example/plugins/ztnc/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + "net/http" + "strconv" + + "embed" + + "aroz.org/zoraxy/ztnc/mod/database" + "aroz.org/zoraxy/ztnc/mod/ganserv" + plugin "aroz.org/zoraxy/ztnc/mod/zoraxy_plugin" +) + +const ( + PLUGIN_ID = "org.aroz.zoraxy.ztnc" + UI_RELPATH = "/ui" + EMBED_FS_ROOT = "/web" + DB_FILE_PATH = "ztnc.db" + AUTH_TOKEN_PATH = "./authtoken.secret" +) + +//go:embed web/* +var content embed.FS + +var ( + sysdb *database.Database + ganManager *ganserv.NetworkManager +) + +func main() { + // Serve the plugin intro spect + runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{ + ID: PLUGIN_ID, + Name: "ztnc", + Author: "aroz.org", + AuthorContact: "zoraxy.aroz.org", + Description: "UI for ZeroTier Network Controller", + URL: "https://zoraxy.aroz.org", + Type: plugin.PluginType_Utilities, + VersionMajor: 1, + VersionMinor: 0, + VersionPatch: 0, + + // As this is a utility plugin, we don't need to capture any traffic + // but only serve the UI, so we set the UI (relative to the plugin path) to "/ui/" to match the HTTP Handler + UIPath: UI_RELPATH, + }) + if err != nil { + //Terminate or enter standalone mode here + panic(err) + } + + // Register the shutdown handler + plugin.RegisterShutdownHandler(func() { + fmt.Println("Shutting down ZeroTier Network Controller") + if sysdb != nil { + sysdb.Close() + } + fmt.Println("ZeroTier Network Controller Exited") + }) + + // Create a new PluginEmbedUIRouter that will serve the UI from web folder + uiRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, EMBED_FS_ROOT, UI_RELPATH) + + // This will serve the index.html file embedded in the binary + http.Handle(UI_RELPATH+"/", uiRouter.Handler()) + + // Start the GAN Network Controller + err = startGanNetworkController() + if err != nil { + panic(err) + } + + // Initiate the API endpoints + initApiEndpoints() + + // Start the HTTP server, only listen to loopback interface + fmt.Println("Plugin UI server started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port) + UI_RELPATH) + http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil) +} diff --git a/example/plugins/ztnc/mod/database/database.go b/example/plugins/ztnc/mod/database/database.go new file mode 100644 index 0000000..bf82ae0 --- /dev/null +++ b/example/plugins/ztnc/mod/database/database.go @@ -0,0 +1,146 @@ +package database + +/* + ArOZ Online Database Access Module + author: tobychui + + This is an improved Object oriented base solution to the original + aroz online database script. +*/ + +import ( + "log" + "runtime" + + "aroz.org/zoraxy/ztnc/mod/database/dbinc" +) + +type Database struct { + Db interface{} //This will be nil on openwrt, leveldb.DB on x64 platforms or bolt.DB on other platforms + BackendType dbinc.BackendType + Backend dbinc.Backend +} + +func NewDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) { + if runtime.GOARCH == "riscv64" { + log.Println("RISCV hardware detected, ignoring the backend type and using FS emulated database") + } + return newDatabase(dbfile, backendType) +} + +// Get the recommended backend type for the current system +func GetRecommendedBackendType() dbinc.BackendType { + //Check if the system is running on RISCV hardware + if runtime.GOARCH == "riscv64" { + //RISCV hardware, currently only support FS emulated database + return dbinc.BackendFSOnly + } else if runtime.GOOS == "windows" || (runtime.GOOS == "linux" && runtime.GOARCH == "amd64") { + //Powerful hardware + return dbinc.BackendBoltDB + //return dbinc.BackendLevelDB + } + + //Default to BoltDB, the safest option + return dbinc.BackendBoltDB +} + +/* + Create / Drop a table + Usage: + err := sysdb.NewTable("MyTable") + err := sysdb.DropTable("MyTable") +*/ + +// Create a new table +func (d *Database) NewTable(tableName string) error { + return d.newTable(tableName) +} + +// Check is table exists +func (d *Database) TableExists(tableName string) bool { + return d.tableExists(tableName) +} + +// Drop the given table +func (d *Database) DropTable(tableName string) error { + return d.dropTable(tableName) +} + +/* +Write to database with given tablename and key. Example Usage: + + type demo struct{ + content string + } + + thisDemo := demo{ + content: "Hello World", + } + +err := sysdb.Write("MyTable", "username/message",thisDemo); +*/ +func (d *Database) Write(tableName string, key string, value interface{}) error { + return d.write(tableName, key, value) +} + +/* + Read from database and assign the content to a given datatype. Example Usage: + + type demo struct{ + content string + } + thisDemo := new(demo) + err := sysdb.Read("MyTable", "username/message",&thisDemo); +*/ + +func (d *Database) Read(tableName string, key string, assignee interface{}) error { + return d.read(tableName, key, assignee) +} + +/* +Check if a key exists in the database table given tablename and key + + if sysdb.KeyExists("MyTable", "username/message"){ + log.Println("Key exists") + } +*/ +func (d *Database) KeyExists(tableName string, key string) bool { + return d.keyExists(tableName, key) +} + +/* +Delete a value from the database table given tablename and key + +err := sysdb.Delete("MyTable", "username/message"); +*/ +func (d *Database) Delete(tableName string, key string) error { + return d.delete(tableName, key) +} + +/* + //List table example usage + //Assume the value is stored as a struct named "groupstruct" + + entries, err := sysdb.ListTable("test") + if err != nil { + panic(err) + } + for _, keypairs := range entries{ + log.Println(string(keypairs[0])) + group := new(groupstruct) + json.Unmarshal(keypairs[1], &group) + log.Println(group); + } + +*/ + +func (d *Database) ListTable(tableName string) ([][][]byte, error) { + return d.listTable(tableName) +} + +/* +Close the database connection +*/ +func (d *Database) Close() { + d.close() +} diff --git a/example/plugins/ztnc/mod/database/database_core.go b/example/plugins/ztnc/mod/database/database_core.go new file mode 100644 index 0000000..347b000 --- /dev/null +++ b/example/plugins/ztnc/mod/database/database_core.go @@ -0,0 +1,70 @@ +//go:build !mipsle && !riscv64 +// +build !mipsle,!riscv64 + +package database + +import ( + "errors" + + "aroz.org/zoraxy/ztnc/mod/database/dbbolt" + "aroz.org/zoraxy/ztnc/mod/database/dbinc" + "aroz.org/zoraxy/ztnc/mod/database/dbleveldb" +) + +func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) { + if backendType == dbinc.BackendFSOnly { + return nil, errors.New("Unsupported backend type for this platform") + } + + if backendType == dbinc.BackendLevelDB { + db, err := dbleveldb.NewDB(dbfile) + return &Database{ + Db: nil, + BackendType: backendType, + Backend: db, + }, err + } + + db, err := dbbolt.NewBoltDatabase(dbfile) + return &Database{ + Db: nil, + BackendType: backendType, + Backend: db, + }, err +} + +func (d *Database) newTable(tableName string) error { + return d.Backend.NewTable(tableName) +} + +func (d *Database) tableExists(tableName string) bool { + return d.Backend.TableExists(tableName) +} + +func (d *Database) dropTable(tableName string) error { + return d.Backend.DropTable(tableName) +} + +func (d *Database) write(tableName string, key string, value interface{}) error { + return d.Backend.Write(tableName, key, value) +} + +func (d *Database) read(tableName string, key string, assignee interface{}) error { + return d.Backend.Read(tableName, key, assignee) +} + +func (d *Database) keyExists(tableName string, key string) bool { + return d.Backend.KeyExists(tableName, key) +} + +func (d *Database) delete(tableName string, key string) error { + return d.Backend.Delete(tableName, key) +} + +func (d *Database) listTable(tableName string) ([][][]byte, error) { + return d.Backend.ListTable(tableName) +} + +func (d *Database) close() { + d.Backend.Close() +} diff --git a/example/plugins/ztnc/mod/database/database_openwrt.go b/example/plugins/ztnc/mod/database/database_openwrt.go new file mode 100644 index 0000000..e128a3a --- /dev/null +++ b/example/plugins/ztnc/mod/database/database_openwrt.go @@ -0,0 +1,196 @@ +//go:build mipsle || riscv64 +// +build mipsle riscv64 + +package database + +import ( + "encoding/json" + "errors" + "log" + "os" + "path/filepath" + "strings" + + "aroz.org/zoraxy/zerotiernc/mod/database/dbinc" +) + +/* + OpenWRT or RISCV backend + + For OpenWRT or RISCV platform, we will use the filesystem as the database backend + as boltdb or leveldb is not supported on these platforms, including boltDB and LevelDB + in conditional compilation will create a build error on these platforms +*/ + +func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) { + dbRootPath := filepath.ToSlash(filepath.Clean(dbfile)) + dbRootPath = "fsdb/" + dbRootPath + err := os.MkdirAll(dbRootPath, 0755) + if err != nil { + return nil, err + } + + log.Println("Filesystem Emulated Key-value Database Service Started: " + dbRootPath) + return &Database{ + Db: dbRootPath, + BackendType: dbinc.BackendFSOnly, + Backend: nil, + }, nil +} + +func (d *Database) dump(filename string) ([]string, error) { + //Get all file objects from root + rootfiles, err := filepath.Glob(filepath.Join(d.Db.(string), "/*")) + if err != nil { + return []string{}, err + } + + //Filter out the folders + rootFolders := []string{} + for _, file := range rootfiles { + if !isDirectory(file) { + rootFolders = append(rootFolders, filepath.Base(file)) + } + } + + return rootFolders, nil +} + +func (d *Database) newTable(tableName string) error { + + tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) + if !fileExists(tablePath) { + return os.MkdirAll(tablePath, 0755) + } + return nil +} + +func (d *Database) tableExists(tableName string) bool { + tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) + if _, err := os.Stat(tablePath); errors.Is(err, os.ErrNotExist) { + return false + } + + if !isDirectory(tablePath) { + return false + } + + return true +} + +func (d *Database) dropTable(tableName string) error { + + tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) + if d.tableExists(tableName) { + return os.RemoveAll(tablePath) + } else { + return errors.New("table not exists") + } + +} + +func (d *Database) write(tableName string, key string, value interface{}) error { + + tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) + js, err := json.Marshal(value) + if err != nil { + return err + } + + key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-") + + return os.WriteFile(filepath.Join(tablePath, key+".entry"), js, 0755) +} + +func (d *Database) read(tableName string, key string, assignee interface{}) error { + if !d.keyExists(tableName, key) { + return errors.New("key not exists") + } + + key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-") + + tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) + entryPath := filepath.Join(tablePath, key+".entry") + content, err := os.ReadFile(entryPath) + if err != nil { + return err + } + + err = json.Unmarshal(content, &assignee) + return err +} + +func (d *Database) keyExists(tableName string, key string) bool { + key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-") + tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) + entryPath := filepath.Join(tablePath, key+".entry") + return fileExists(entryPath) +} + +func (d *Database) delete(tableName string, key string) error { + + if !d.keyExists(tableName, key) { + return errors.New("key not exists") + } + key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-") + tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) + entryPath := filepath.Join(tablePath, key+".entry") + + return os.Remove(entryPath) +} + +func (d *Database) listTable(tableName string) ([][][]byte, error) { + if !d.tableExists(tableName) { + return [][][]byte{}, errors.New("table not exists") + } + tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) + entries, err := filepath.Glob(filepath.Join(tablePath, "/*.entry")) + if err != nil { + return [][][]byte{}, err + } + + var results [][][]byte = [][][]byte{} + for _, entry := range entries { + if !isDirectory(entry) { + //Read it + key := filepath.Base(entry) + key = strings.TrimSuffix(key, filepath.Ext(key)) + key = strings.ReplaceAll(key, "-SLASH_SIGN-", "/") + + bkey := []byte(key) + bval := []byte("") + c, err := os.ReadFile(entry) + if err != nil { + break + } + + bval = c + results = append(results, [][]byte{bkey, bval}) + } + } + return results, nil +} + +func (d *Database) close() { + //Nothing to close as it is file system +} + +func isDirectory(path string) bool { + fileInfo, err := os.Stat(path) + if err != nil { + return false + } + + return fileInfo.IsDir() +} + +func fileExists(name string) bool { + _, err := os.Stat(name) + if err == nil { + return true + } + if errors.Is(err, os.ErrNotExist) { + return false + } + return false +} diff --git a/example/plugins/ztnc/mod/database/dbbolt/dbbolt.go b/example/plugins/ztnc/mod/database/dbbolt/dbbolt.go new file mode 100644 index 0000000..8cf7ec0 --- /dev/null +++ b/example/plugins/ztnc/mod/database/dbbolt/dbbolt.go @@ -0,0 +1,141 @@ +package dbbolt + +import ( + "encoding/json" + "errors" + + "github.com/boltdb/bolt" +) + +type Database struct { + Db interface{} //This is the bolt database object +} + +func NewBoltDatabase(dbfile string) (*Database, error) { + db, err := bolt.Open(dbfile, 0600, nil) + if err != nil { + return nil, err + } + + return &Database{ + Db: db, + }, err +} + +// Create a new table +func (d *Database) NewTable(tableName string) error { + err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists([]byte(tableName)) + if err != nil { + return err + } + return nil + }) + + return err +} + +// Check is table exists +func (d *Database) TableExists(tableName string) bool { + return d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(tableName)) + if b == nil { + return errors.New("table not exists") + } + return nil + }) == nil +} + +// Drop the given table +func (d *Database) DropTable(tableName string) error { + err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error { + err := tx.DeleteBucket([]byte(tableName)) + if err != nil { + return err + } + return nil + }) + return err +} + +// Write to table +func (d *Database) Write(tableName string, key string, value interface{}) error { + jsonString, err := json.Marshal(value) + if err != nil { + return err + } + err = d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists([]byte(tableName)) + if err != nil { + return err + } + b := tx.Bucket([]byte(tableName)) + err = b.Put([]byte(key), jsonString) + return err + }) + return err +} + +func (d *Database) Read(tableName string, key string, assignee interface{}) error { + err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(tableName)) + v := b.Get([]byte(key)) + json.Unmarshal(v, &assignee) + return nil + }) + return err +} + +func (d *Database) KeyExists(tableName string, key string) bool { + resultIsNil := false + if !d.TableExists(tableName) { + //Table not exists. Do not proceed accessing key + //log.Println("[DB] ERROR: Requesting key from table that didn't exist!!!") + return false + } + err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(tableName)) + v := b.Get([]byte(key)) + if v == nil { + resultIsNil = true + } + return nil + }) + + if err != nil { + return false + } else { + if resultIsNil { + return false + } else { + return true + } + } +} + +func (d *Database) Delete(tableName string, key string) error { + err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error { + tx.Bucket([]byte(tableName)).Delete([]byte(key)) + return nil + }) + + return err +} + +func (d *Database) ListTable(tableName string) ([][][]byte, error) { + var results [][][]byte + err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(tableName)) + c := b.Cursor() + + for k, v := c.First(); k != nil; k, v = c.Next() { + results = append(results, [][]byte{k, v}) + } + return nil + }) + return results, err +} + +func (d *Database) Close() { + d.Db.(*bolt.DB).Close() +} diff --git a/example/plugins/ztnc/mod/database/dbbolt/dbbolt_test.go b/example/plugins/ztnc/mod/database/dbbolt/dbbolt_test.go new file mode 100644 index 0000000..05e708a --- /dev/null +++ b/example/plugins/ztnc/mod/database/dbbolt/dbbolt_test.go @@ -0,0 +1,67 @@ +package dbbolt_test + +import ( + "os" + "testing" + + "aroz.org/zoraxy/ztnc/mod/database/dbbolt" +) + +func TestNewBoltDatabase(t *testing.T) { + dbfile := "test.db" + defer os.Remove(dbfile) + + db, err := dbbolt.NewBoltDatabase(dbfile) + if err != nil { + t.Fatalf("Failed to create new Bolt database: %v", err) + } + defer db.Close() + + if db.Db == nil { + t.Fatalf("Expected non-nil database object") + } +} + +func TestNewTable(t *testing.T) { + dbfile := "test.db" + defer os.Remove(dbfile) + + db, err := dbbolt.NewBoltDatabase(dbfile) + if err != nil { + t.Fatalf("Failed to create new Bolt database: %v", err) + } + defer db.Close() + + err = db.NewTable("testTable") + if err != nil { + t.Fatalf("Failed to create new table: %v", err) + } +} + +func TestTableExists(t *testing.T) { + dbfile := "test.db" + defer os.Remove(dbfile) + + db, err := dbbolt.NewBoltDatabase(dbfile) + if err != nil { + t.Fatalf("Failed to create new Bolt database: %v", err) + } + defer db.Close() + + tableName := "testTable" + err = db.NewTable(tableName) + if err != nil { + t.Fatalf("Failed to create new table: %v", err) + } + + exists := db.TableExists(tableName) + if !exists { + t.Fatalf("Expected table %s to exist", tableName) + } + + nonExistentTable := "nonExistentTable" + exists = db.TableExists(nonExistentTable) + if exists { + t.Fatalf("Expected table %s to not exist", nonExistentTable) + } +} diff --git a/example/plugins/ztnc/mod/database/dbinc/dbinc.go b/example/plugins/ztnc/mod/database/dbinc/dbinc.go new file mode 100644 index 0000000..8e60ba0 --- /dev/null +++ b/example/plugins/ztnc/mod/database/dbinc/dbinc.go @@ -0,0 +1,39 @@ +package dbinc + +/* + dbinc is the interface for all database backend +*/ +type BackendType int + +const ( + BackendBoltDB BackendType = iota //Default backend + BackendFSOnly //OpenWRT or RISCV backend + BackendLevelDB //LevelDB backend + + BackEndAuto = BackendBoltDB +) + +type Backend interface { + NewTable(tableName string) error + TableExists(tableName string) bool + DropTable(tableName string) error + Write(tableName string, key string, value interface{}) error + Read(tableName string, key string, assignee interface{}) error + KeyExists(tableName string, key string) bool + Delete(tableName string, key string) error + ListTable(tableName string) ([][][]byte, error) + Close() +} + +func (b BackendType) String() string { + switch b { + case BackendBoltDB: + return "BoltDB" + case BackendFSOnly: + return "File System Emulated Key-Value Store" + case BackendLevelDB: + return "LevelDB" + default: + return "Unknown" + } +} diff --git a/example/plugins/ztnc/mod/database/dbleveldb/dbleveldb.go b/example/plugins/ztnc/mod/database/dbleveldb/dbleveldb.go new file mode 100644 index 0000000..59b9667 --- /dev/null +++ b/example/plugins/ztnc/mod/database/dbleveldb/dbleveldb.go @@ -0,0 +1,152 @@ +package dbleveldb + +import ( + "encoding/json" + "log" + "path/filepath" + "strings" + "sync" + "time" + + "aroz.org/zoraxy/ztnc/mod/database/dbinc" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/util" +) + +// Ensure the DB struct implements the Backend interface +var _ dbinc.Backend = (*DB)(nil) + +type DB struct { + db *leveldb.DB + Table sync.Map //For emulating table creation + batch leveldb.Batch //Batch write + writeFlushTicker *time.Ticker //Ticker for flushing data into disk + writeFlushStop chan bool //Stop channel for write flush ticker +} + +func NewDB(path string) (*DB, error) { + //If the path is not a directory (e.g. /tmp/dbfile.db), convert the filename to directory + if filepath.Ext(path) != "" { + path = strings.ReplaceAll(path, ".", "_") + } + + db, err := leveldb.OpenFile(path, nil) + if err != nil { + return nil, err + } + + thisDB := &DB{ + db: db, + Table: sync.Map{}, + batch: leveldb.Batch{}, + } + + //Create a ticker to flush data into disk every 1 seconds + writeFlushTicker := time.NewTicker(1 * time.Second) + writeFlushStop := make(chan bool) + go func() { + for { + select { + case <-writeFlushTicker.C: + if thisDB.batch.Len() == 0 { + //No flushing needed + continue + } + err = db.Write(&thisDB.batch, nil) + if err != nil { + log.Println("[LevelDB] Failed to flush data into disk: ", err) + } + thisDB.batch.Reset() + case <-writeFlushStop: + return + } + } + }() + + thisDB.writeFlushTicker = writeFlushTicker + thisDB.writeFlushStop = writeFlushStop + + return thisDB, nil +} + +func (d *DB) NewTable(tableName string) error { + //Create a table entry in the sync.Map + d.Table.Store(tableName, true) + return nil +} + +func (d *DB) TableExists(tableName string) bool { + _, ok := d.Table.Load(tableName) + return ok +} + +func (d *DB) DropTable(tableName string) error { + d.Table.Delete(tableName) + iter := d.db.NewIterator(nil, nil) + defer iter.Release() + + for iter.Next() { + key := iter.Key() + if filepath.Dir(string(key)) == tableName { + err := d.db.Delete(key, nil) + if err != nil { + return err + } + } + } + + return nil +} + +func (d *DB) Write(tableName string, key string, value interface{}) error { + data, err := json.Marshal(value) + if err != nil { + return err + } + d.batch.Put([]byte(filepath.ToSlash(filepath.Join(tableName, key))), data) + return nil +} + +func (d *DB) Read(tableName string, key string, assignee interface{}) error { + data, err := d.db.Get([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil) + if err != nil { + return err + } + return json.Unmarshal(data, assignee) +} + +func (d *DB) KeyExists(tableName string, key string) bool { + _, err := d.db.Get([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil) + return err == nil +} + +func (d *DB) Delete(tableName string, key string) error { + return d.db.Delete([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil) +} + +func (d *DB) ListTable(tableName string) ([][][]byte, error) { + iter := d.db.NewIterator(util.BytesPrefix([]byte(tableName+"/")), nil) + defer iter.Release() + + var result [][][]byte + for iter.Next() { + key := iter.Key() + //The key contains the table name as prefix. Trim it before returning + value := iter.Value() + result = append(result, [][]byte{[]byte(strings.TrimPrefix(string(key), tableName+"/")), value}) + } + + err := iter.Error() + if err != nil { + return nil, err + } + return result, nil +} + +func (d *DB) Close() { + //Write the remaining data in batch back into disk + d.writeFlushStop <- true + d.writeFlushTicker.Stop() + d.db.Write(&d.batch, nil) + d.db.Close() +} diff --git a/example/plugins/ztnc/mod/database/dbleveldb/dbleveldb_test.go b/example/plugins/ztnc/mod/database/dbleveldb/dbleveldb_test.go new file mode 100644 index 0000000..c091684 --- /dev/null +++ b/example/plugins/ztnc/mod/database/dbleveldb/dbleveldb_test.go @@ -0,0 +1,141 @@ +package dbleveldb_test + +import ( + "os" + "testing" + + "aroz.org/zoraxy/ztnc/mod/database/dbleveldb" +) + +func TestNewDB(t *testing.T) { + path := "/tmp/testdb" + defer os.RemoveAll(path) + + db, err := dbleveldb.NewDB(path) + if err != nil { + t.Fatalf("Failed to create new DB: %v", err) + } + defer db.Close() +} + +func TestNewTable(t *testing.T) { + path := "/tmp/testdb" + defer os.RemoveAll(path) + + db, err := dbleveldb.NewDB(path) + if err != nil { + t.Fatalf("Failed to create new DB: %v", err) + } + defer db.Close() + + err = db.NewTable("testTable") + if err != nil { + t.Fatalf("Failed to create new table: %v", err) + } +} + +func TestTableExists(t *testing.T) { + path := "/tmp/testdb" + defer os.RemoveAll(path) + + db, err := dbleveldb.NewDB(path) + if err != nil { + t.Fatalf("Failed to create new DB: %v", err) + } + defer db.Close() + + db.NewTable("testTable") + if !db.TableExists("testTable") { + t.Fatalf("Table should exist") + } +} + +func TestDropTable(t *testing.T) { + path := "/tmp/testdb" + defer os.RemoveAll(path) + + db, err := dbleveldb.NewDB(path) + if err != nil { + t.Fatalf("Failed to create new DB: %v", err) + } + defer db.Close() + + db.NewTable("testTable") + err = db.DropTable("testTable") + if err != nil { + t.Fatalf("Failed to drop table: %v", err) + } + + if db.TableExists("testTable") { + t.Fatalf("Table should not exist") + } +} + +func TestWriteAndRead(t *testing.T) { + path := "/tmp/testdb" + defer os.RemoveAll(path) + + db, err := dbleveldb.NewDB(path) + if err != nil { + t.Fatalf("Failed to create new DB: %v", err) + } + defer db.Close() + + db.NewTable("testTable") + err = db.Write("testTable", "testKey", "testValue") + if err != nil { + t.Fatalf("Failed to write to table: %v", err) + } + + var value string + err = db.Read("testTable", "testKey", &value) + if err != nil { + t.Fatalf("Failed to read from table: %v", err) + } + + if value != "testValue" { + t.Fatalf("Expected 'testValue', got '%v'", value) + } +} +func TestListTable(t *testing.T) { + path := "/tmp/testdb" + defer os.RemoveAll(path) + + db, err := dbleveldb.NewDB(path) + if err != nil { + t.Fatalf("Failed to create new DB: %v", err) + } + defer db.Close() + + db.NewTable("testTable") + err = db.Write("testTable", "testKey1", "testValue1") + if err != nil { + t.Fatalf("Failed to write to table: %v", err) + } + err = db.Write("testTable", "testKey2", "testValue2") + if err != nil { + t.Fatalf("Failed to write to table: %v", err) + } + + result, err := db.ListTable("testTable") + if err != nil { + t.Fatalf("Failed to list table: %v", err) + } + + if len(result) != 2 { + t.Fatalf("Expected 2 entries, got %v", len(result)) + } + + expected := map[string]string{ + "testTable/testKey1": "\"testValue1\"", + "testTable/testKey2": "\"testValue2\"", + } + + for _, entry := range result { + key := string(entry[0]) + value := string(entry[1]) + if expected[key] != value { + t.Fatalf("Expected value '%v' for key '%v', got '%v'", expected[key], key, value) + } + } +} diff --git a/example/plugins/ztnc/mod/ganserv/authkey.go b/example/plugins/ztnc/mod/ganserv/authkey.go new file mode 100644 index 0000000..006e90d --- /dev/null +++ b/example/plugins/ztnc/mod/ganserv/authkey.go @@ -0,0 +1,80 @@ +package ganserv + +import ( + "errors" + "log" + "os" + "runtime" + "strings" +) + +func TryLoadorAskUserForAuthkey() (string, error) { + //Check for zt auth token + value, exists := os.LookupEnv("ZT_AUTH") + if !exists { + log.Println("Environment variable ZT_AUTH not defined. Trying to load authtoken from file.") + } else { + return value, nil + } + + authKey := "" + if runtime.GOOS == "windows" { + if isAdmin() { + //Read the secret file directly + b, err := os.ReadFile("C:\\ProgramData\\ZeroTier\\One\\authtoken.secret") + if err == nil { + log.Println("Zerotier authkey loaded") + authKey = string(b) + } else { + log.Println("Unable to read authkey at C:\\ProgramData\\ZeroTier\\One\\authtoken.secret: ", err.Error()) + } + } else { + //Elavate the permission to admin + ak, err := readAuthTokenAsAdmin() + if err == nil { + log.Println("Zerotier authkey loaded") + authKey = ak + } else { + log.Println("Unable to read authkey at C:\\ProgramData\\ZeroTier\\One\\authtoken.secret: ", err.Error()) + } + } + + } else if runtime.GOOS == "linux" { + if isAdmin() { + //Try to read from source using sudo + ak, err := readAuthTokenAsAdmin() + if err == nil { + log.Println("Zerotier authkey loaded") + authKey = strings.TrimSpace(ak) + } else { + log.Println("Unable to read authkey at /var/lib/zerotier-one/authtoken.secret: ", err.Error()) + } + } else { + //Try read from source + b, err := os.ReadFile("/var/lib/zerotier-one/authtoken.secret") + if err == nil { + log.Println("Zerotier authkey loaded") + authKey = string(b) + } else { + log.Println("Unable to read authkey at /var/lib/zerotier-one/authtoken.secret: ", err.Error()) + } + } + + } else if runtime.GOOS == "darwin" { + b, err := os.ReadFile("/Library/Application Support/ZeroTier/One/authtoken.secret") + if err == nil { + log.Println("Zerotier authkey loaded") + authKey = string(b) + } else { + log.Println("Unable to read authkey at /Library/Application Support/ZeroTier/One/authtoken.secret ", err.Error()) + } + } + + authKey = strings.TrimSpace(authKey) + + if authKey == "" { + return "", errors.New("Unable to load authkey from file") + } + + return authKey, nil +} diff --git a/example/plugins/ztnc/mod/ganserv/authkeyLinux.go b/example/plugins/ztnc/mod/ganserv/authkeyLinux.go new file mode 100644 index 0000000..8423c56 --- /dev/null +++ b/example/plugins/ztnc/mod/ganserv/authkeyLinux.go @@ -0,0 +1,37 @@ +//go:build linux +// +build linux + +package ganserv + +import ( + "os" + "os/exec" + "os/user" + "strings" + + "aroz.org/zoraxy/zerotiernc/mod/utils" +) + +func readAuthTokenAsAdmin() (string, error) { + if utils.FileExists("./conf/authtoken.secret") { + authKey, err := os.ReadFile("./conf/authtoken.secret") + if err == nil { + return strings.TrimSpace(string(authKey)), nil + } + } + + cmd := exec.Command("sudo", "cat", "/var/lib/zerotier-one/authtoken.secret") + output, err := cmd.Output() + if err != nil { + return "", err + } + return string(output), nil +} + +func isAdmin() bool { + currentUser, err := user.Current() + if err != nil { + return false + } + return currentUser.Username == "root" +} diff --git a/example/plugins/ztnc/mod/ganserv/authkeyWin.go b/example/plugins/ztnc/mod/ganserv/authkeyWin.go new file mode 100644 index 0000000..aa03e31 --- /dev/null +++ b/example/plugins/ztnc/mod/ganserv/authkeyWin.go @@ -0,0 +1,73 @@ +//go:build windows +// +build windows + +package ganserv + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + "syscall" + "time" + + "aroz.org/zoraxy/ztnc/mod/utils" + "golang.org/x/sys/windows" +) + +// Use admin permission to read auth token on Windows +func readAuthTokenAsAdmin() (string, error) { + //Check if the previous startup already extracted the authkey + if utils.FileExists("./conf/authtoken.secret") { + authKey, err := os.ReadFile("./conf/authtoken.secret") + if err == nil { + return strings.TrimSpace(string(authKey)), nil + } + } + + verb := "runas" + exe := "cmd.exe" + cwd, _ := os.Getwd() + + output, _ := filepath.Abs(filepath.Join("./conf/", "authtoken.secret")) + os.WriteFile(output, []byte(""), 0775) + args := fmt.Sprintf("/C type \"C:\\ProgramData\\ZeroTier\\One\\authtoken.secret\" > \"" + output + "\"") + + verbPtr, _ := syscall.UTF16PtrFromString(verb) + exePtr, _ := syscall.UTF16PtrFromString(exe) + cwdPtr, _ := syscall.UTF16PtrFromString(cwd) + argPtr, _ := syscall.UTF16PtrFromString(args) + + var showCmd int32 = 1 //SW_NORMAL + + err := windows.ShellExecute(0, verbPtr, exePtr, argPtr, cwdPtr, showCmd) + if err != nil { + return "", err + } + + log.Println("Please click agree to allow access to ZeroTier authtoken from ProgramData") + retry := 0 + time.Sleep(3 * time.Second) + for !utils.FileExists("./conf/authtoken.secret") && retry < 10 { + time.Sleep(3 * time.Second) + log.Println("Waiting for ZeroTier authtoken extraction...") + retry++ + } + + authKey, err := os.ReadFile("./conf/authtoken.secret") + if err != nil { + return "", err + } + + return strings.TrimSpace(string(authKey)), nil +} + +// Check if admin on Windows +func isAdmin() bool { + _, err := os.Open("\\\\.\\PHYSICALDRIVE0") + if err != nil { + return false + } + return true +} diff --git a/example/plugins/ztnc/mod/ganserv/ganserv.go b/example/plugins/ztnc/mod/ganserv/ganserv.go new file mode 100644 index 0000000..f81e39b --- /dev/null +++ b/example/plugins/ztnc/mod/ganserv/ganserv.go @@ -0,0 +1,130 @@ +package ganserv + +import ( + "log" + "net" + + "aroz.org/zoraxy/ztnc/mod/database" +) + +/* + Global Area Network + Server side implementation + + This module do a few things to help manage + the system GANs + + - Provide DHCP assign to client + - Provide a list of connected nodes in the same VLAN + - Provide proxy of packet if the target VLAN is online but not reachable + + Also provide HTTP Handler functions for management + - Create Network + - Update Network Properties (Name / Desc) + - Delete Network + + - Authorize Node + - Deauthorize Node + - Set / Get Network Prefered Subnet Mask + - Handle Node ping +*/ + +type Node struct { + Auth bool //If the node is authorized in this network + ClientID string //The client ID + MAC string //The tap MAC this client is using + Name string //Name of the client in this network + Description string //Description text + ManagedIP net.IP //The IP address assigned by this network + LastSeen int64 //Last time it is seen from this host + ClientVersion string //Client application version + PublicIP net.IP //Public IP address as seen from this host +} + +type Network struct { + UID string //UUID of the network, must be a 16 char random ASCII string + Name string //Name of the network, ASCII only + Description string //Description of the network + CIDR string //The subnet masked use by this network + Nodes []*Node //The nodes currently attached in this network +} + +type NetworkManagerOptions struct { + Database *database.Database + AuthToken string + ApiPort int +} + +type NetworkMetaData struct { + Desc string +} + +type MemberMetaData struct { + Name string +} + +type NetworkManager struct { + authToken string + apiPort int + ControllerID string + option *NetworkManagerOptions + networksMetadata map[string]NetworkMetaData +} + +// Create a new GAN manager +func NewNetworkManager(option *NetworkManagerOptions) *NetworkManager { + option.Database.NewTable("ganserv") + + //Load network metadata + networkMeta := map[string]NetworkMetaData{} + if option.Database.KeyExists("ganserv", "networkmeta") { + option.Database.Read("ganserv", "networkmeta", &networkMeta) + } + + //Start the zerotier instance if not exists + + //Get controller info + instanceInfo, err := getControllerInfo(option.AuthToken, option.ApiPort) + if err != nil { + log.Println("ZeroTier connection failed: ", err.Error()) + return &NetworkManager{ + authToken: option.AuthToken, + apiPort: option.ApiPort, + ControllerID: "", + option: option, + networksMetadata: networkMeta, + } + } + + return &NetworkManager{ + authToken: option.AuthToken, + apiPort: option.ApiPort, + ControllerID: instanceInfo.Address, + option: option, + networksMetadata: networkMeta, + } +} + +func (m *NetworkManager) GetNetworkMetaData(netid string) *NetworkMetaData { + md, ok := m.networksMetadata[netid] + if !ok { + return &NetworkMetaData{} + } + + return &md +} + +func (m *NetworkManager) WriteNetworkMetaData(netid string, meta *NetworkMetaData) { + m.networksMetadata[netid] = *meta + m.option.Database.Write("ganserv", "networkmeta", m.networksMetadata) +} + +func (m *NetworkManager) GetMemberMetaData(netid string, memid string) *MemberMetaData { + thisMemberData := MemberMetaData{} + m.option.Database.Read("ganserv", "memberdata_"+netid+"_"+memid, &thisMemberData) + return &thisMemberData +} + +func (m *NetworkManager) WriteMemeberMetaData(netid string, memid string, meta *MemberMetaData) { + m.option.Database.Write("ganserv", "memberdata_"+netid+"_"+memid, meta) +} diff --git a/example/plugins/ztnc/mod/ganserv/handlers.go b/example/plugins/ztnc/mod/ganserv/handlers.go new file mode 100644 index 0000000..4ab76da --- /dev/null +++ b/example/plugins/ztnc/mod/ganserv/handlers.go @@ -0,0 +1,504 @@ +package ganserv + +import ( + "encoding/json" + "net" + "net/http" + "regexp" + "strings" + + "aroz.org/zoraxy/ztnc/mod/utils" +) + +func (m *NetworkManager) HandleGetNodeID(w http.ResponseWriter, r *http.Request) { + if m.ControllerID == "" { + //Node id not exists. Check again + instanceInfo, err := getControllerInfo(m.option.AuthToken, m.option.ApiPort) + if err != nil { + utils.SendErrorResponse(w, "unable to access node id information") + return + } + + m.ControllerID = instanceInfo.Address + } + + js, _ := json.Marshal(m.ControllerID) + utils.SendJSONResponse(w, string(js)) +} + +func (m *NetworkManager) HandleAddNetwork(w http.ResponseWriter, r *http.Request) { + networkInfo, err := m.createNetwork() + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + //Network created. Assign it the standard network settings + err = m.configureNetwork(networkInfo.Nwid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24") + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + // Return the new network ID + js, _ := json.Marshal(networkInfo.Nwid) + utils.SendJSONResponse(w, string(js)) +} + +func (m *NetworkManager) HandleRemoveNetwork(w http.ResponseWriter, r *http.Request) { + networkID, err := utils.PostPara(r, "id") + if err != nil { + utils.SendErrorResponse(w, "invalid or empty network id given") + return + } + + if !m.networkExists(networkID) { + utils.SendErrorResponse(w, "network id not exists") + return + } + + err = m.deleteNetwork(networkID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + } + + utils.SendOK(w) +} + +func (m *NetworkManager) HandleListNetwork(w http.ResponseWriter, r *http.Request) { + netid, _ := utils.GetPara(r, "netid") + if netid != "" { + targetNetInfo, err := m.getNetworkInfoById(netid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + js, _ := json.Marshal(targetNetInfo) + utils.SendJSONResponse(w, string(js)) + + } else { + // Return the list of networks as JSON + networkIds, err := m.listNetworkIds() + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + networkInfos := []*NetworkInfo{} + for _, id := range networkIds { + thisNetInfo, err := m.getNetworkInfoById(id) + if err == nil { + networkInfos = append(networkInfos, thisNetInfo) + } + } + + js, _ := json.Marshal(networkInfos) + utils.SendJSONResponse(w, string(js)) + } + +} + +func (m *NetworkManager) HandleNetworkNaming(w http.ResponseWriter, r *http.Request) { + netid, err := utils.PostPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "network id not given") + return + } + + if !m.networkExists(netid) { + utils.SendErrorResponse(w, "network not eixsts") + } + + newName, _ := utils.PostPara(r, "name") + newDesc, _ := utils.PostPara(r, "desc") + if newName != "" && newDesc != "" { + //Strip away html from name and desc + re := regexp.MustCompile("<[^>]*>") + newName := re.ReplaceAllString(newName, "") + newDesc := re.ReplaceAllString(newDesc, "") + + //Set the new network name and desc + err = m.setNetworkNameAndDescription(netid, newName, newDesc) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) + } else { + //Get current name and description + name, desc, err := m.getNetworkNameAndDescription(netid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + js, _ := json.Marshal([]string{name, desc}) + utils.SendJSONResponse(w, string(js)) + } +} + +func (m *NetworkManager) HandleNetworkDetails(w http.ResponseWriter, r *http.Request) { + netid, err := utils.PostPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "netid not given") + return + } + + targetNetwork, err := m.getNetworkInfoById(netid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + js, _ := json.Marshal(targetNetwork) + utils.SendJSONResponse(w, string(js)) +} + +func (m *NetworkManager) HandleSetRanges(w http.ResponseWriter, r *http.Request) { + netid, err := utils.PostPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "netid not given") + return + } + cidr, err := utils.PostPara(r, "cidr") + if err != nil { + utils.SendErrorResponse(w, "cidr not given") + return + } + ipstart, err := utils.PostPara(r, "ipstart") + if err != nil { + utils.SendErrorResponse(w, "ipstart not given") + return + } + ipend, err := utils.PostPara(r, "ipend") + if err != nil { + utils.SendErrorResponse(w, "ipend not given") + return + } + + //Validate the CIDR is real, the ip range is within the CIDR range + _, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + utils.SendErrorResponse(w, "invalid cidr string given") + return + } + + startIP := net.ParseIP(ipstart) + endIP := net.ParseIP(ipend) + if startIP == nil || endIP == nil { + utils.SendErrorResponse(w, "invalid start or end ip given") + return + } + + withinRange := ipnet.Contains(startIP) && ipnet.Contains(endIP) + if !withinRange { + utils.SendErrorResponse(w, "given CIDR did not cover all of the start to end ip range") + return + } + + err = m.configureNetwork(netid, startIP.String(), endIP.String(), strings.TrimSpace(cidr)) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) +} + +// Handle listing of network members. Set details=true for listing all details +func (m *NetworkManager) HandleMemberList(w http.ResponseWriter, r *http.Request) { + netid, err := utils.GetPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "netid is empty") + return + } + + details, _ := utils.GetPara(r, "detail") + + memberIds, err := m.getNetworkMembers(netid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + if details == "" { + //Only show client ids + js, _ := json.Marshal(memberIds) + utils.SendJSONResponse(w, string(js)) + } else { + //Show detail members info + detailMemberInfo := []*MemberInfo{} + for _, thisMemberId := range memberIds { + memInfo, err := m.getNetworkMemberInfo(netid, thisMemberId) + if err == nil { + detailMemberInfo = append(detailMemberInfo, memInfo) + } + } + + js, _ := json.Marshal(detailMemberInfo) + utils.SendJSONResponse(w, string(js)) + } +} + +// Handle Authorization of members +func (m *NetworkManager) HandleMemberAuthorization(w http.ResponseWriter, r *http.Request) { + netid, err := utils.PostPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "net id not set") + return + } + + memberid, err := utils.PostPara(r, "memid") + if err != nil { + utils.SendErrorResponse(w, "memid not set") + return + } + + //Check if the target memeber exists + if !m.memberExistsInNetwork(netid, memberid) { + utils.SendErrorResponse(w, "member not exists in given network") + return + } + + setAuthorized, err := utils.PostPara(r, "auth") + if err != nil || setAuthorized == "" { + //Get the member authorization state + memberInfo, err := m.getNetworkMemberInfo(netid, memberid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + js, _ := json.Marshal(memberInfo.Authorized) + utils.SendJSONResponse(w, string(js)) + } else if setAuthorized == "true" { + m.AuthorizeMember(netid, memberid, true) + } else if setAuthorized == "false" { + m.AuthorizeMember(netid, memberid, false) + } else { + utils.SendErrorResponse(w, "unknown operation state: "+setAuthorized) + } +} + +// Handle Delete or Add IP for a member in a network +func (m *NetworkManager) HandleMemberIP(w http.ResponseWriter, r *http.Request) { + netid, err := utils.PostPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "net id not set") + return + } + + memberid, err := utils.PostPara(r, "memid") + if err != nil { + utils.SendErrorResponse(w, "memid not set") + return + } + + opr, err := utils.PostPara(r, "opr") + if err != nil { + utils.SendErrorResponse(w, "opr not defined") + return + } + + targetip, _ := utils.PostPara(r, "ip") + + memberInfo, err := m.getNetworkMemberInfo(netid, memberid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + if opr == "add" { + if targetip == "" { + utils.SendErrorResponse(w, "ip not set") + return + } + + if !isValidIPAddr(targetip) { + utils.SendErrorResponse(w, "ip address not valid") + return + } + + newIpList := append(memberInfo.IPAssignments, targetip) + err = m.setAssignedIps(netid, memberid, newIpList) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + utils.SendOK(w) + + } else if opr == "del" { + if targetip == "" { + utils.SendErrorResponse(w, "ip not set") + return + } + + //Delete user ip from the list + newIpList := []string{} + for _, thisIp := range memberInfo.IPAssignments { + if thisIp != targetip { + newIpList = append(newIpList, thisIp) + } + } + + err = m.setAssignedIps(netid, memberid, newIpList) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + utils.SendOK(w) + } else if opr == "get" { + js, _ := json.Marshal(memberInfo.IPAssignments) + utils.SendJSONResponse(w, string(js)) + } else { + utils.SendErrorResponse(w, "unsupported opr type: "+opr) + } +} + +// Handle naming for members +func (m *NetworkManager) HandleMemberNaming(w http.ResponseWriter, r *http.Request) { + netid, err := utils.PostPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "net id not set") + return + } + + memberid, err := utils.PostPara(r, "memid") + if err != nil { + utils.SendErrorResponse(w, "memid not set") + return + } + + if !m.memberExistsInNetwork(netid, memberid) { + utils.SendErrorResponse(w, "target member not exists in given network") + return + } + + //Read memeber data + targetMemberData := m.GetMemberMetaData(netid, memberid) + + newname, err := utils.PostPara(r, "name") + if err != nil { + //Send over the member data + js, _ := json.Marshal(targetMemberData) + utils.SendJSONResponse(w, string(js)) + } else { + //Write member data + targetMemberData.Name = newname + m.WriteMemeberMetaData(netid, memberid, targetMemberData) + utils.SendOK(w) + } +} + +// Handle delete of a given memver +func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Request) { + netid, err := utils.PostPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "net id not set") + return + } + + memberid, err := utils.PostPara(r, "memid") + if err != nil { + utils.SendErrorResponse(w, "memid not set") + return + } + + //Check if that member is authorized. + memberInfo, err := m.getNetworkMemberInfo(netid, memberid) + if err != nil { + utils.SendErrorResponse(w, "member not exists in given GANet") + return + } + + if memberInfo.Authorized { + //Deauthorized this member before deleting + m.AuthorizeMember(netid, memberid, false) + } + + //Remove the memeber + err = m.deleteMember(netid, memberid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) +} + +// Check if a given network id is a network hosted on this zoraxy node +func (m *NetworkManager) IsLocalGAN(networkId string) bool { + networks, err := m.listNetworkIds() + if err != nil { + return false + } + + for _, network := range networks { + if network == networkId { + return true + } + } + + return false +} + +// Handle server instant joining a given network +func (m *NetworkManager) HandleServerJoinNetwork(w http.ResponseWriter, r *http.Request) { + netid, err := utils.PostPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "net id not set") + return + } + + //Check if the target network is a network hosted on this server + if !m.IsLocalGAN(netid) { + utils.SendErrorResponse(w, "given network is not a GAN hosted on this node") + return + } + + if m.memberExistsInNetwork(netid, m.ControllerID) { + utils.SendErrorResponse(w, "controller already inside network") + return + } + + //Join the network + err = m.joinNetwork(netid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) +} + +// Handle server instant leaving a given network +func (m *NetworkManager) HandleServerLeaveNetwork(w http.ResponseWriter, r *http.Request) { + netid, err := utils.PostPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "net id not set") + return + } + + //Check if the target network is a network hosted on this server + if !m.IsLocalGAN(netid) { + utils.SendErrorResponse(w, "given network is not a GAN hosted on this node") + return + } + + //Leave the network + err = m.leaveNetwork(netid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + //Remove it from target network if it is authorized + err = m.deleteMember(netid, m.ControllerID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) +} diff --git a/example/plugins/ztnc/mod/ganserv/network.go b/example/plugins/ztnc/mod/ganserv/network.go new file mode 100644 index 0000000..9f4ec73 --- /dev/null +++ b/example/plugins/ztnc/mod/ganserv/network.go @@ -0,0 +1,39 @@ +package ganserv + +import ( + "fmt" + "math/rand" + "net" + "time" +) + +//Get a random free IP from the pool +func (n *Network) GetRandomFreeIP() (net.IP, error) { + // Get all IP addresses in the subnet + ips, err := GetAllAddressFromCIDR(n.CIDR) + if err != nil { + return nil, err + } + + // Filter out used IPs + usedIPs := make(map[string]bool) + for _, node := range n.Nodes { + usedIPs[node.ManagedIP.String()] = true + } + availableIPs := []string{} + for _, ip := range ips { + if !usedIPs[ip] { + availableIPs = append(availableIPs, ip) + } + } + + // Randomly choose an available IP + if len(availableIPs) == 0 { + return nil, fmt.Errorf("no available IP") + } + rand.Seed(time.Now().UnixNano()) + randIndex := rand.Intn(len(availableIPs)) + pickedFreeIP := availableIPs[randIndex] + + return net.ParseIP(pickedFreeIP), nil +} diff --git a/example/plugins/ztnc/mod/ganserv/network_test.go b/example/plugins/ztnc/mod/ganserv/network_test.go new file mode 100644 index 0000000..2002b9f --- /dev/null +++ b/example/plugins/ztnc/mod/ganserv/network_test.go @@ -0,0 +1,55 @@ +package ganserv_test + +import ( + "fmt" + "net" + "strconv" + "testing" + + "aroz.org/zoraxy/ztnc/mod/ganserv" +) + +func TestGetRandomFreeIP(t *testing.T) { + n := ganserv.Network{ + CIDR: "172.16.0.0/12", + Nodes: []*ganserv.Node{ + { + Name: "nodeC1", + ManagedIP: net.ParseIP("172.16.1.142"), + }, + { + Name: "nodeC2", + ManagedIP: net.ParseIP("172.16.5.174"), + }, + }, + } + + // Call the function for 10 times + for i := 0; i < 10; i++ { + freeIP, err := n.GetRandomFreeIP() + fmt.Println("["+strconv.Itoa(i)+"] Free IP address assigned: ", freeIP) + + // Assert that no error occurred + if err != nil { + t.Errorf("Unexpected error: %s", err.Error()) + } + + // Assert that the returned IP is a valid IPv4 address + if freeIP.To4() == nil { + t.Errorf("Invalid IP address format: %s", freeIP.String()) + } + + // Assert that the returned IP is not already used by a node + for _, node := range n.Nodes { + if freeIP.Equal(node.ManagedIP) { + t.Errorf("Returned IP is already in use: %s", freeIP.String()) + } + } + + n.Nodes = append(n.Nodes, &ganserv.Node{ + Name: "NodeT" + strconv.Itoa(i), + ManagedIP: freeIP, + }) + } + +} diff --git a/example/plugins/ztnc/mod/ganserv/utils.go b/example/plugins/ztnc/mod/ganserv/utils.go new file mode 100644 index 0000000..684f597 --- /dev/null +++ b/example/plugins/ztnc/mod/ganserv/utils.go @@ -0,0 +1,55 @@ +package ganserv + +import ( + "net" +) + +//Generate all ip address from a CIDR +func GetAllAddressFromCIDR(cidr string) ([]string, error) { + ip, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + return nil, err + } + + var ips []string + for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) { + ips = append(ips, ip.String()) + } + // remove network address and broadcast address + return ips[1 : len(ips)-1], nil +} + +func inc(ip net.IP) { + for j := len(ip) - 1; j >= 0; j-- { + ip[j]++ + if ip[j] > 0 { + break + } + } +} + +func isValidIPAddr(ipAddr string) bool { + ip := net.ParseIP(ipAddr) + if ip == nil { + return false + } + + return true +} + +func ipWithinCIDR(ipAddr string, cidr string) bool { + // Parse the CIDR string + _, ipNet, err := net.ParseCIDR(cidr) + if err != nil { + return false + } + + // Parse the IP address + ip := net.ParseIP(ipAddr) + if ip == nil { + return false + } + + // Check if the IP address is in the CIDR range + return ipNet.Contains(ip) +} diff --git a/example/plugins/ztnc/mod/ganserv/zerotier.go b/example/plugins/ztnc/mod/ganserv/zerotier.go new file mode 100644 index 0000000..fa1fd0b --- /dev/null +++ b/example/plugins/ztnc/mod/ganserv/zerotier.go @@ -0,0 +1,669 @@ +package ganserv + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "strconv" + "strings" +) + +/* + zerotier.go + + This hold the functions that required to communicate with + a zerotier instance + + See more on + https://docs.zerotier.com/self-hosting/network-controllers/ + +*/ + +type NodeInfo struct { + Address string `json:"address"` + Clock int64 `json:"clock"` + Config struct { + Settings struct { + AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay,omitempty"` + ForceTCPRelay bool `json:"forceTcpRelay,omitempty"` + HomeDir string `json:"homeDir,omitempty"` + ListeningOn []string `json:"listeningOn,omitempty"` + PortMappingEnabled bool `json:"portMappingEnabled,omitempty"` + PrimaryPort int `json:"primaryPort,omitempty"` + SecondaryPort int `json:"secondaryPort,omitempty"` + SoftwareUpdate string `json:"softwareUpdate,omitempty"` + SoftwareUpdateChannel string `json:"softwareUpdateChannel,omitempty"` + SurfaceAddresses []string `json:"surfaceAddresses,omitempty"` + TertiaryPort int `json:"tertiaryPort,omitempty"` + } `json:"settings"` + } `json:"config"` + Online bool `json:"online"` + PlanetWorldID int `json:"planetWorldId"` + PlanetWorldTimestamp int64 `json:"planetWorldTimestamp"` + PublicIdentity string `json:"publicIdentity"` + TCPFallbackActive bool `json:"tcpFallbackActive"` + Version string `json:"version"` + VersionBuild int `json:"versionBuild"` + VersionMajor int `json:"versionMajor"` + VersionMinor int `json:"versionMinor"` + VersionRev int `json:"versionRev"` +} +type ErrResp struct { + Message string `json:"message"` +} + +type NetworkInfo struct { + AuthTokens []interface{} `json:"authTokens"` + AuthorizationEndpoint string `json:"authorizationEndpoint"` + Capabilities []interface{} `json:"capabilities"` + ClientID string `json:"clientId"` + CreationTime int64 `json:"creationTime"` + DNS []interface{} `json:"dns"` + EnableBroadcast bool `json:"enableBroadcast"` + ID string `json:"id"` + IPAssignmentPools []interface{} `json:"ipAssignmentPools"` + Mtu int `json:"mtu"` + MulticastLimit int `json:"multicastLimit"` + Name string `json:"name"` + Nwid string `json:"nwid"` + Objtype string `json:"objtype"` + Private bool `json:"private"` + RemoteTraceLevel int `json:"remoteTraceLevel"` + RemoteTraceTarget interface{} `json:"remoteTraceTarget"` + Revision int `json:"revision"` + Routes []interface{} `json:"routes"` + Rules []struct { + Not bool `json:"not"` + Or bool `json:"or"` + Type string `json:"type"` + } `json:"rules"` + RulesSource string `json:"rulesSource"` + SsoEnabled bool `json:"ssoEnabled"` + Tags []interface{} `json:"tags"` + V4AssignMode struct { + Zt bool `json:"zt"` + } `json:"v4AssignMode"` + V6AssignMode struct { + SixPlane bool `json:"6plane"` + Rfc4193 bool `json:"rfc4193"` + Zt bool `json:"zt"` + } `json:"v6AssignMode"` +} + +type MemberInfo struct { + ActiveBridge bool `json:"activeBridge"` + Address string `json:"address"` + AuthenticationExpiryTime int `json:"authenticationExpiryTime"` + Authorized bool `json:"authorized"` + Capabilities []interface{} `json:"capabilities"` + CreationTime int64 `json:"creationTime"` + ID string `json:"id"` + Identity string `json:"identity"` + IPAssignments []string `json:"ipAssignments"` + LastAuthorizedCredential interface{} `json:"lastAuthorizedCredential"` + LastAuthorizedCredentialType string `json:"lastAuthorizedCredentialType"` + LastAuthorizedTime int `json:"lastAuthorizedTime"` + LastDeauthorizedTime int `json:"lastDeauthorizedTime"` + NoAutoAssignIps bool `json:"noAutoAssignIps"` + Nwid string `json:"nwid"` + Objtype string `json:"objtype"` + RemoteTraceLevel int `json:"remoteTraceLevel"` + RemoteTraceTarget interface{} `json:"remoteTraceTarget"` + Revision int `json:"revision"` + SsoExempt bool `json:"ssoExempt"` + Tags []interface{} `json:"tags"` + VMajor int `json:"vMajor"` + VMinor int `json:"vMinor"` + VProto int `json:"vProto"` + VRev int `json:"vRev"` +} + +// Get the zerotier node info from local service +func getControllerInfo(token string, apiPort int) (*NodeInfo, error) { + url := "http://localhost:" + strconv.Itoa(apiPort) + "/status" + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("X-ZT1-AUTH", token) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + //Read from zerotier service instance + + defer resp.Body.Close() + payload, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + //Parse the payload into struct + thisInstanceInfo := NodeInfo{} + err = json.Unmarshal(payload, &thisInstanceInfo) + if err != nil { + return nil, err + } + + return &thisInstanceInfo, nil +} + +/* + Network Functions +*/ +//Create a zerotier network +func (m *NetworkManager) createNetwork() (*NetworkInfo, error) { + url := fmt.Sprintf("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/%s______", m.ControllerID) + + data := []byte(`{}`) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) + if err != nil { + return nil, err + } + + req.Header.Set("X-ZT1-AUTH", m.authToken) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + payload, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + networkInfo := NetworkInfo{} + err = json.Unmarshal(payload, &networkInfo) + if err != nil { + return nil, err + } + + return &networkInfo, nil +} + +// List network details +func (m *NetworkManager) getNetworkInfoById(networkId string) (*NetworkInfo, error) { + req, err := http.NewRequest("GET", os.ExpandEnv("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+networkId+"/"), nil) + if err != nil { + return nil, err + } + req.Header.Set("X-Zt1-Auth", m.authToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return nil, errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode)) + } + + thisNetworkInfo := NetworkInfo{} + payload, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + err = json.Unmarshal(payload, &thisNetworkInfo) + if err != nil { + return nil, err + } + + return &thisNetworkInfo, nil +} + +func (m *NetworkManager) setNetworkInfoByID(networkId string, newNetworkInfo *NetworkInfo) error { + payloadBytes, err := json.Marshal(newNetworkInfo) + if err != nil { + return err + } + payloadBuffer := bytes.NewBuffer(payloadBytes) + + // Create the HTTP request + url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkId + "/" + req, err := http.NewRequest("POST", url, payloadBuffer) + if err != nil { + return err + } + req.Header.Set("X-Zt1-Auth", m.authToken) + req.Header.Set("Content-Type", "application/json") + + // Send the HTTP request + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + // Print the response status code + if resp.StatusCode != 200 { + return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode)) + } + + return nil +} + +// List network IDs +func (m *NetworkManager) listNetworkIds() ([]string, error) { + req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/", nil) + if err != nil { + return []string{}, err + } + req.Header.Set("X-Zt1-Auth", m.authToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return []string{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return []string{}, errors.New("network error") + } + + networkIds := []string{} + payload, err := io.ReadAll(resp.Body) + if err != nil { + return []string{}, err + } + + err = json.Unmarshal(payload, &networkIds) + if err != nil { + return []string{}, err + } + + return networkIds, nil +} + +// wrapper for checking if a network id exists +func (m *NetworkManager) networkExists(networkId string) bool { + networkIds, err := m.listNetworkIds() + if err != nil { + return false + } + + for _, thisid := range networkIds { + if thisid == networkId { + return true + } + } + + return false +} + +// delete a network +func (m *NetworkManager) deleteNetwork(networkID string) error { + url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/" + client := &http.Client{} + + // Create a new DELETE request + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + return err + } + + // Add the required authorization header + req.Header.Set("X-Zt1-Auth", m.authToken) + + // Send the request and get the response + resp, err := client.Do(req) + if err != nil { + return err + } + + // Close the response body when we're done + defer resp.Body.Close() + s, err := io.ReadAll(resp.Body) + fmt.Println(string(s), err, resp.StatusCode) + + // Print the response status code + if resp.StatusCode != 200 { + return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode)) + } + + return nil +} + +// Configure network +// Example: configureNetwork(netid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24") +func (m *NetworkManager) configureNetwork(networkID string, ipRangeStart string, ipRangeEnd string, routeTarget string) error { + url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/" + data := map[string]interface{}{ + "ipAssignmentPools": []map[string]string{ + { + "ipRangeStart": ipRangeStart, + "ipRangeEnd": ipRangeEnd, + }, + }, + "routes": []map[string]interface{}{ + { + "target": routeTarget, + "via": nil, + }, + }, + "v4AssignMode": "zt", + "private": true, + } + + payload, err := json.Marshal(data) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-ZT1-AUTH", m.authToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + // Print the response status code + if resp.StatusCode != 200 { + return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode)) + } + + return nil +} + +func (m *NetworkManager) setAssignedIps(networkID string, memid string, newIps []string) error { + url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/member/" + memid + data := map[string]interface{}{ + "ipAssignments": newIps, + } + + payload, err := json.Marshal(data) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-ZT1-AUTH", m.authToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + // Print the response status code + if resp.StatusCode != 200 { + return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode)) + } + + return nil +} + +func (m *NetworkManager) setNetworkNameAndDescription(netid string, name string, desc string) error { + // Convert string to rune slice + r := []rune(name) + + // Loop over runes and remove non-ASCII characters + for i, v := range r { + if v > 127 { + r[i] = ' ' + } + } + + // Convert back to string and trim whitespace + name = strings.TrimSpace(string(r)) + + url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/" + data := map[string]interface{}{ + "name": name, + } + + payload, err := json.Marshal(data) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-ZT1-AUTH", m.authToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + // Print the response status code + if resp.StatusCode != 200 { + return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode)) + } + + meta := m.GetNetworkMetaData(netid) + if meta != nil { + meta.Desc = desc + m.WriteNetworkMetaData(netid, meta) + } + + return nil +} + +func (m *NetworkManager) getNetworkNameAndDescription(netid string) (string, string, error) { + //Get name from network info + netinfo, err := m.getNetworkInfoById(netid) + if err != nil { + return "", "", err + } + + name := netinfo.Name + + //Get description from meta + desc := "" + networkMeta := m.GetNetworkMetaData(netid) + if networkMeta != nil { + desc = networkMeta.Desc + } + + return name, desc, nil +} + +/* + Member functions +*/ + +func (m *NetworkManager) getNetworkMembers(networkId string) ([]string, error) { + url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkId + "/member" + reqBody := bytes.NewBuffer([]byte{}) + req, err := http.NewRequest("GET", url, reqBody) + if err != nil { + return nil, err + } + + req.Header.Set("X-ZT1-AUTH", m.authToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, errors.New("failed to get network members") + } + + memberList := map[string]int{} + payload, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + err = json.Unmarshal(payload, &memberList) + if err != nil { + return nil, err + } + + members := make([]string, 0, len(memberList)) + for k := range memberList { + members = append(members, k) + } + + return members, nil +} + +func (m *NetworkManager) memberExistsInNetwork(netid string, memid string) bool { + //Get a list of member + memberids, err := m.getNetworkMembers(netid) + if err != nil { + return false + } + for _, thisMemberId := range memberids { + if thisMemberId == memid { + return true + } + } + + return false +} + +// Get a network memeber info by netid and memberid +func (m *NetworkManager) getNetworkMemberInfo(netid string, memberid string) (*MemberInfo, error) { + req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memberid, nil) + if err != nil { + return nil, err + } + req.Header.Set("X-Zt1-Auth", m.authToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + thisMemeberInfo := &MemberInfo{} + payload, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + err = json.Unmarshal(payload, &thisMemeberInfo) + if err != nil { + return nil, err + } + + return thisMemeberInfo, nil +} + +// Set the authorization state of a member +func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAuthorized bool) error { + url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/member/" + memberid + payload := []byte(`{"authorized": true}`) + if !setAuthorized { + payload = []byte(`{"authorized": false}`) + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + if err != nil { + return err + } + req.Header.Set("X-ZT1-AUTH", m.authToken) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode)) + } + + return nil +} + +// Delete a member from the network +func (m *NetworkManager) deleteMember(netid string, memid string) error { + req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memid, nil) + if err != nil { + return err + } + req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken)) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode)) + } + + return nil +} + +// Make the host to join a given network +func (m *NetworkManager) joinNetwork(netid string) error { + req, err := http.NewRequest("POST", "http://localhost:"+strconv.Itoa(m.apiPort)+"/network/"+netid, nil) + if err != nil { + return err + } + req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken)) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode)) + } + + return nil +} + +// Make the host to leave a given network +func (m *NetworkManager) leaveNetwork(netid string) error { + req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/network/"+netid, nil) + if err != nil { + return err + } + req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken)) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode)) + } + + return nil +} diff --git a/example/plugins/ztnc/mod/utils/conv.go b/example/plugins/ztnc/mod/utils/conv.go new file mode 100644 index 0000000..6adf753 --- /dev/null +++ b/example/plugins/ztnc/mod/utils/conv.go @@ -0,0 +1,105 @@ +package utils + +import ( + "archive/zip" + "io" + "os" + "path/filepath" + "strconv" + "strings" +) + +func StringToInt64(number string) (int64, error) { + i, err := strconv.ParseInt(number, 10, 64) + if err != nil { + return -1, err + } + return i, nil +} + +func Int64ToString(number int64) string { + convedNumber := strconv.FormatInt(number, 10) + return convedNumber +} + +func ReplaceSpecialCharacters(filename string) string { + replacements := map[string]string{ + "#": "%pound%", + "&": "%amp%", + "{": "%left_cur%", + "}": "%right_cur%", + "\\": "%backslash%", + "<": "%left_ang%", + ">": "%right_ang%", + "*": "%aster%", + "?": "%quest%", + " ": "%space%", + "$": "%dollar%", + "!": "%exclan%", + "'": "%sin_q%", + "\"": "%dou_q%", + ":": "%colon%", + "@": "%at%", + "+": "%plus%", + "`": "%backtick%", + "|": "%pipe%", + "=": "%equal%", + ".": "_", + "/": "-", + } + + for char, replacement := range replacements { + filename = strings.ReplaceAll(filename, char, replacement) + } + + return filename +} + +/* Zip File Handler */ +// zipFiles compresses multiple files into a single zip archive file +func ZipFiles(filename string, files ...string) error { + newZipFile, err := os.Create(filename) + if err != nil { + return err + } + defer newZipFile.Close() + + zipWriter := zip.NewWriter(newZipFile) + defer zipWriter.Close() + + for _, file := range files { + if err := addFileToZip(zipWriter, file); err != nil { + return err + } + } + return nil +} + +// addFileToZip adds an individual file to a zip archive +func addFileToZip(zipWriter *zip.Writer, filename string) error { + fileToZip, err := os.Open(filename) + if err != nil { + return err + } + defer fileToZip.Close() + + info, err := fileToZip.Stat() + if err != nil { + return err + } + + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + + header.Name = filepath.Base(filename) + header.Method = zip.Deflate + + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + _, err = io.Copy(writer, fileToZip) + return err +} diff --git a/example/plugins/ztnc/mod/utils/template.go b/example/plugins/ztnc/mod/utils/template.go new file mode 100644 index 0000000..e5772a8 --- /dev/null +++ b/example/plugins/ztnc/mod/utils/template.go @@ -0,0 +1,19 @@ +package utils + +import ( + "net/http" +) + +/* + Web Template Generator + + This is the main system core module that perform function similar to what PHP did. + To replace part of the content of any file, use {{paramter}} to replace it. + + +*/ + +func SendHTMLResponse(w http.ResponseWriter, msg string) { + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(msg)) +} diff --git a/example/plugins/ztnc/mod/utils/utils.go b/example/plugins/ztnc/mod/utils/utils.go new file mode 100644 index 0000000..2fe1ffd --- /dev/null +++ b/example/plugins/ztnc/mod/utils/utils.go @@ -0,0 +1,202 @@ +package utils + +import ( + "errors" + "log" + "net" + "net/http" + "os" + "strconv" + "strings" + "time" +) + +/* + Common + + Some commonly used functions in ArozOS + +*/ + +// Response related +func SendTextResponse(w http.ResponseWriter, msg string) { + w.Write([]byte(msg)) +} + +// Send JSON response, with an extra json header +func SendJSONResponse(w http.ResponseWriter, json string) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(json)) +} + +func SendErrorResponse(w http.ResponseWriter, errMsg string) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"error\":\"" + errMsg + "\"}")) +} + +func SendOK(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("\"OK\"")) +} + +// Get GET parameter +func GetPara(r *http.Request, key string) (string, error) { + // Get first value from the URL query + value := r.URL.Query().Get(key) + if len(value) == 0 { + return "", errors.New("invalid " + key + " given") + } + return value, nil +} + +// Get GET paramter as boolean, accept 1 or true +func GetBool(r *http.Request, key string) (bool, error) { + x, err := GetPara(r, key) + if err != nil { + return false, err + } + + // Convert to lowercase and trim spaces just once to compare + switch strings.ToLower(strings.TrimSpace(x)) { + case "1", "true", "on": + return true, nil + case "0", "false", "off": + return false, nil + } + + return false, errors.New("invalid boolean given") +} + +// Get POST parameter +func PostPara(r *http.Request, key string) (string, error) { + // Try to parse the form + if err := r.ParseForm(); err != nil { + return "", err + } + // Get first value from the form + x := r.Form.Get(key) + if len(x) == 0 { + return "", errors.New("invalid " + key + " given") + } + return x, nil +} + +// Get POST paramter as boolean, accept 1 or true +func PostBool(r *http.Request, key string) (bool, error) { + x, err := PostPara(r, key) + if err != nil { + return false, err + } + + // Convert to lowercase and trim spaces just once to compare + switch strings.ToLower(strings.TrimSpace(x)) { + case "1", "true", "on": + return true, nil + case "0", "false", "off": + return false, nil + } + + return false, errors.New("invalid boolean given") +} + +// Get POST paramter as int +func PostInt(r *http.Request, key string) (int, error) { + x, err := PostPara(r, key) + if err != nil { + return 0, err + } + + x = strings.TrimSpace(x) + rx, err := strconv.Atoi(x) + if err != nil { + return 0, err + } + + return rx, nil +} + +func FileExists(filename string) bool { + _, err := os.Stat(filename) + if err == nil { + // File exists + return true + } else if errors.Is(err, os.ErrNotExist) { + // File does not exist + return false + } + // Some other error + return false +} + +func IsDir(path string) bool { + if !FileExists(path) { + return false + } + fi, err := os.Stat(path) + if err != nil { + log.Fatal(err) + return false + } + switch mode := fi.Mode(); { + case mode.IsDir(): + return true + case mode.IsRegular(): + return false + } + return false +} + +func TimeToString(targetTime time.Time) string { + return targetTime.Format("2006-01-02 15:04:05") +} + +// Check if given string in a given slice +func StringInArray(arr []string, str string) bool { + for _, a := range arr { + if a == str { + return true + } + } + return false +} + +func StringInArrayIgnoreCase(arr []string, str string) bool { + smallArray := []string{} + for _, item := range arr { + smallArray = append(smallArray, strings.ToLower(item)) + } + + return StringInArray(smallArray, strings.ToLower(str)) +} + +// Validate if the listening address is correct +func ValidateListeningAddress(address string) bool { + // Check if the address starts with a colon, indicating it's just a port + if strings.HasPrefix(address, ":") { + return true + } + + // Split the address into host and port parts + host, port, err := net.SplitHostPort(address) + if err != nil { + // Try to parse it as just a port + if _, err := strconv.Atoi(address); err == nil { + return false // It's just a port number + } + return false // It's an invalid address + } + + // Check if the port part is a valid number + if _, err := strconv.Atoi(port); err != nil { + return false + } + + // Check if the host part is a valid IP address or empty (indicating any IP) + if host != "" { + if net.ParseIP(host) == nil { + return false + } + } + + return true +} \ No newline at end of file diff --git a/example/plugins/ztnc/mod/zoraxy_plugin/embed_webserver.go b/example/plugins/ztnc/mod/zoraxy_plugin/embed_webserver.go new file mode 100644 index 0000000..d9b3fde --- /dev/null +++ b/example/plugins/ztnc/mod/zoraxy_plugin/embed_webserver.go @@ -0,0 +1,106 @@ +package zoraxy_plugin + +import ( + "embed" + "fmt" + "io/fs" + "net/http" + "net/url" + "strings" + "time" +) + +type PluginUiRouter struct { + PluginID string //The ID of the plugin + TargetFs *embed.FS //The embed.FS where the UI files are stored + TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web + HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui +} + +// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS +// The targetFsPrefix is the prefix of the embed.FS where the UI files are stored +// The targetFsPrefix should be relative to the root of the embed.FS +// The targetFsPrefix should start with a slash (e.g. /web) that corresponds to the root folder of the embed.FS +// The handlerPrefix is the prefix of the handler used to route this router +// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path +// All prefix should not end with a slash +func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter { + //Make sure all prefix are in /prefix format + if !strings.HasPrefix(targetFsPrefix, "/") { + targetFsPrefix = "/" + targetFsPrefix + } + targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/") + + if !strings.HasPrefix(handlerPrefix, "/") { + handlerPrefix = "/" + handlerPrefix + } + handlerPrefix = strings.TrimSuffix(handlerPrefix, "/") + + //Return the PluginUiRouter + return &PluginUiRouter{ + PluginID: pluginID, + TargetFs: targetFs, + TargetFsPrefix: targetFsPrefix, + HandlerPrefix: handlerPrefix, + } +} + +func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler { + //Get the CSRF token from header + csrfToken := r.Header.Get("X-Zoraxy-Csrf") + if csrfToken == "" { + csrfToken = "missing-csrf-token" + } + + //Return the middleware + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if the request is for an HTML file + if strings.HasSuffix(r.URL.Path, "/") { + // Redirect to the index.html + http.Redirect(w, r, r.URL.Path+"index.html", http.StatusFound) + return + } + if strings.HasSuffix(r.URL.Path, ".html") { + //Read the target file from embed.FS + targetFilePath := strings.TrimPrefix(r.URL.Path, "/") + targetFilePath = p.TargetFsPrefix + "/" + targetFilePath + targetFilePath = strings.TrimPrefix(targetFilePath, "/") + targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath) + if err != nil { + http.Error(w, "File not found", http.StatusNotFound) + return + } + body := string(targetFileContent) + body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken) + http.ServeContent(w, r, r.URL.Path, time.Now(), strings.NewReader(body)) + return + } + + //Call the next handler + fsHandler.ServeHTTP(w, r) + }) + +} + +// GetHttpHandler returns the http.Handler for the PluginUiRouter +func (p *PluginUiRouter) Handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + //Remove the plugin UI handler path prefix + rewrittenURL := r.RequestURI + rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix) + rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + r.URL, _ = url.Parse(rewrittenURL) + r.RequestURI = rewrittenURL + + //Serve the file from the embed.FS + subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/")) + if err != nil { + fmt.Println(err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Replace {{csrf_token}} with the actual CSRF token and serve the file + p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r) + }) +} diff --git a/src/mod/plugins/includes.go b/example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go similarity index 89% rename from src/mod/plugins/includes.go rename to example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go index 89fb5f9..1691591 100644 --- a/src/mod/plugins/includes.go +++ b/example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go @@ -1,18 +1,20 @@ -package plugins +package zoraxy_plugin import ( "encoding/json" "fmt" "os" + "os/signal" "strings" + "syscall" ) /* Plugins Includes.go - This file contains the common types and structs that are used by the plugins - If you are building a Zoraxy plugin with Golang, you can use this file to include - the common types and structs that are used by the plugins + This file is copied from Zoraxy source code + You can always find the latest version under mod/plugins/includes.go + Usually this file are backward compatible */ type PluginType int @@ -184,3 +186,25 @@ func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) { ServeIntroSpect(pluginSpect) return RecvConfigureSpec() } + +/* + +Shutdown handler + +This function will register a shutdown handler for the plugin +The shutdown callback will be called when the plugin is shutting down +You can use this to clean up resources like closing database connections +*/ + +func RegisterShutdownHandler(shutdownCallback func()) { + // Set up a channel to receive OS signals + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + // Start a goroutine to listen for signals + go func() { + <-sigChan + shutdownCallback() + os.Exit(0) + }() +} diff --git a/example/plugins/ztnc/start.go b/example/plugins/ztnc/start.go new file mode 100644 index 0000000..1090031 --- /dev/null +++ b/example/plugins/ztnc/start.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" + "net/http" + "os" + + "aroz.org/zoraxy/ztnc/mod/database" + "aroz.org/zoraxy/ztnc/mod/database/dbinc" + "aroz.org/zoraxy/ztnc/mod/ganserv" + "aroz.org/zoraxy/ztnc/mod/utils" +) + +func startGanNetworkController() error { + fmt.Println("Starting ZeroTier Network Controller") + //Create a new database + var err error + sysdb, err = database.NewDatabase(DB_FILE_PATH, dbinc.BackendBoltDB) + if err != nil { + return err + } + + //Initiate the GAN server manager + usingZtAuthToken := "" + ztAPIPort := 9993 + + if utils.FileExists(AUTH_TOKEN_PATH) { + authToken, err := os.ReadFile(AUTH_TOKEN_PATH) + if err != nil { + fmt.Println("Error reading auth config file:", err) + return err + } + usingZtAuthToken = string(authToken) + fmt.Println("Loaded ZeroTier Auth Token from file") + } + + if usingZtAuthToken == "" { + usingZtAuthToken, err = ganserv.TryLoadorAskUserForAuthkey() + if err != nil { + fmt.Println("Error getting ZeroTier Auth Token:", err) + } + } + + ganManager = ganserv.NewNetworkManager(&ganserv.NetworkManagerOptions{ + AuthToken: usingZtAuthToken, + ApiPort: ztAPIPort, + Database: sysdb, + }) + + return nil +} + +func initApiEndpoints() { + //UI_RELPATH must be the same as the one in the plugin intro spect + // as Zoraxy plugin UI proxy will only forward the UI path to your plugin + http.HandleFunc(UI_RELPATH+"/api/gan/network/info", ganManager.HandleGetNodeID) + http.HandleFunc(UI_RELPATH+"/api/gan/network/add", ganManager.HandleAddNetwork) + http.HandleFunc(UI_RELPATH+"/api/gan/network/remove", ganManager.HandleRemoveNetwork) + http.HandleFunc(UI_RELPATH+"/api/gan/network/list", ganManager.HandleListNetwork) + http.HandleFunc(UI_RELPATH+"/api/gan/network/name", ganManager.HandleNetworkNaming) + http.HandleFunc(UI_RELPATH+"/api/gan/network/setRange", ganManager.HandleSetRanges) + http.HandleFunc(UI_RELPATH+"/api/gan/network/join", ganManager.HandleServerJoinNetwork) + http.HandleFunc(UI_RELPATH+"/api/gan/network/leave", ganManager.HandleServerLeaveNetwork) + http.HandleFunc(UI_RELPATH+"/api/gan/members/list", ganManager.HandleMemberList) + http.HandleFunc(UI_RELPATH+"/api/gan/members/ip", ganManager.HandleMemberIP) + http.HandleFunc(UI_RELPATH+"/api/gan/members/name", ganManager.HandleMemberNaming) + http.HandleFunc(UI_RELPATH+"/api/gan/members/authorize", ganManager.HandleMemberAuthorization) + http.HandleFunc(UI_RELPATH+"/api/gan/members/delete", ganManager.HandleMemberDelete) +} diff --git a/example/plugins/ztnc/web/details.html b/example/plugins/ztnc/web/details.html new file mode 100644 index 0000000..37db9a0 --- /dev/null +++ b/example/plugins/ztnc/web/details.html @@ -0,0 +1,747 @@ + +

+ +
+ +

+ +
+

+
+

+ +
+ +
+

Settings

+
+ + + + + + + + + +
IPv4 Auto-Assign
+
+
+
+

Custom IP Range

+

Manual IP Range Configuration. The IP range must be within the selected CIDR range. +
Use Utilities > IP to CIDR tool if you are not too familiar with CIDR notations.

+
+
+ + +
+
+ + +
+
+
+ + +
+

Members

+

To join this network using command line, type sudo zerotier-cli join on your device terminal

+
+ + +
+
+ + + + + + + + + + + + + + + + + +
AuthAddressNameManaged IPAuthorized SinceVersionRemove
+
+
+

Add Controller as Member

+

Optionally you can add the network controller (ZeroTier running on the Zoraxy node) as member for cross GAN reverse proxy to bypass NAT limitations.

+ + +

+
+ diff --git a/example/plugins/ztnc/web/index.html b/example/plugins/ztnc/web/index.html new file mode 100644 index 0000000..97108bd --- /dev/null +++ b/example/plugins/ztnc/web/index.html @@ -0,0 +1,257 @@ + + + + + + + + + Global Area Network | Zoraxy + + + + + + + + + + + + + +
+
+

Global Area Network

+

Virtual Network Hub that allows all networked devices to communicate as if they all reside in the same physical data center or cloud region

+
+
+
+
+

+ +
Network Controller ID
+

+
+
+
+ +
+
0
+
Networks
+
+
+
+ +
+
0
+
Connected Nodes
+
+
+
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + +
Network IDNameDescriptionSubnet (Assign Range)NodesActions
No Global Area Network Found on this host
+
+
+
+
+ + + \ No newline at end of file diff --git a/example/plugins/ztnc/ztnc.db b/example/plugins/ztnc/ztnc.db new file mode 100644 index 0000000000000000000000000000000000000000..70a17b524a4707a4c9821322a3697e7882bd5eb6 GIT binary patch literal 32768 zcmeI&zfJ-%8~|X%e~CH}pMc!vrg6Zai;07i58#GS2vH9LCD(df)bITd5Rfqih$idy8t`uN4F9<#=r3ysdKJq-^)EhL<1D2TyUH zmH+_)1PBlyK!5-N0t5&USRH}2aw##d|1JOj+8fAXV*cMK<4@ma-S?fB>(%))D*hwYNfad zkU#UQ|JzxeX0_K!$6@rC^?ocdEPt7Y?Pm2Xt7(`_2cz5jFlo(_(CBoI+M#jN4Tqs~ zbDT71$we5qlV);WpM>$GpZ4lyM7=v0)rWTvy?&a^=h>GO0t5&UAV7cs0RjXF5FkKc zX@Pj&AJ6~eeE{eGOVj!Q0RjXF5FkK+009C72oP8qftcri&;R4Tz{)JtLI@BbK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7e?KNk1|A8ace literal 0 HcmV?d00001 diff --git a/example/plugins/ztnc/ztnc.db.lock b/example/plugins/ztnc/ztnc.db.lock new file mode 100644 index 0000000..e69de29 diff --git a/src/mod/plugins/introspect.go b/src/mod/plugins/introspect.go index 4c40776..988289e 100644 --- a/src/mod/plugins/introspect.go +++ b/src/mod/plugins/introspect.go @@ -6,6 +6,8 @@ import ( "fmt" "os/exec" "time" + + zoraxyPlugin "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin" ) // LoadPlugin loads a plugin from the plugin directory @@ -42,8 +44,8 @@ func (m *Manager) LoadPluginSpec(pluginPath string) (*Plugin, error) { } // GetPluginEntryPoint returns the plugin entry point -func (m *Manager) GetPluginSpec(entryPoint string) (*IntroSpect, error) { - pluginSpec := IntroSpect{} +func (m *Manager) GetPluginSpec(entryPoint string) (*zoraxyPlugin.IntroSpect, error) { + pluginSpec := zoraxyPlugin.IntroSpect{} ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() diff --git a/src/mod/plugins/lifecycle.go b/src/mod/plugins/lifecycle.go index 3c13069..7c7ebbf 100644 --- a/src/mod/plugins/lifecycle.go +++ b/src/mod/plugins/lifecycle.go @@ -13,6 +13,7 @@ import ( "time" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" + zoraxyPlugin "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin" ) func (m *Manager) StartPlugin(pluginID string) error { @@ -37,7 +38,7 @@ func (m *Manager) StartPlugin(pluginID string) error { } //Prepare plugin start configuration - pluginConfiguration := ConfigureSpec{ + pluginConfiguration := zoraxyPlugin.ConfigureSpec{ Port: getRandomPortNumber(), RuntimeConst: *m.Options.SystemConst, } @@ -100,18 +101,25 @@ func (m *Manager) StartUIHandlerForPlugin(targetPlugin *Plugin, pluginListeningP pluginUIRelPath = "/" + pluginUIRelPath } + // Remove the trailing slash if it exists + pluginUIRelPath = strings.TrimSuffix(pluginUIRelPath, "/") + pluginUIURL, err := url.Parse("http://127.0.0.1:" + strconv.Itoa(pluginListeningPort) + pluginUIRelPath) if err != nil { return err } + + // Generate the plugin subpath to be trimmed + pluginMatchingPath := filepath.ToSlash(filepath.Join("/plugin.ui/"+targetPlugin.Spec.ID+"/")) + "/" if targetPlugin.Spec.UIPath != "" { targetPlugin.uiProxy = dpcore.NewDynamicProxyCore( pluginUIURL, - "", + pluginMatchingPath, &dpcore.DpcoreOptions{ IgnoreTLSVerification: true, }, ) + targetPlugin.AssignedPort = pluginListeningPort m.LoadedPlugins.Store(targetPlugin.Spec.ID, targetPlugin) } return nil diff --git a/src/mod/plugins/no_img.png b/src/mod/plugins/no_img.png index 2b85efbe22680e56055620a0ca853b2f946d3683..dcf5bda079604f12bff3f8c22a668c41a638c6be 100644 GIT binary patch delta 41176 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 J-s)I{{1-W39MS*) delta 5708 zcmbtYc|4SR+aFZ6N+C`OEvKd=Dk2SrnMzqZ6d^{-gHV<s0kxstV3C5Y-1hGV&=Jfp7*?;bDs0QXTATJxj&!#zJ9;!zP{hzb$zdk_CfN4 zc@b3(P?0!y%KSn||ICoN_s&Uj!$RAq^z!3m>2Gv%o_JUuN%fQ3huCn<0HI&Wl2!2w zrN0W&HGi$L+4RN1J<)rBAnC#U}YieAP@z5asz&&@%v|FrkOFF*B9+yByh-JcE~@L!Ss>%srYlb`pv9S!H#8o=EjaqtWH%<^9ZmUeJQtWoVOVIri)}L*~vMI~IcipY}-K zz~cpdC0+^|#tZ?)g~FZ6FU&Z32bju*ud!p4ptX(`v_l=4@KvpuJ5wL>7=?B=Hr>-J z#u~Mb`9rl>y@Xj&mPNdyqa*j$Lc+!T?%R{?adC0mVw>x5Wll~`wDF)6i`J>bzP<*j za6w-Og4s4@qpd8rw6sK{(a4;zg=STgf%Y)oI4NEeZX0q;@Ypg(UR4K3g9CNb>aj$O z9(8v1=0F9(TH=lm7RyWZuVW9miJF_6OT0RIrdQf4L{W$<1OnB)Fg`xMkU}}0+V%=h zLt`){xYB3^P40-1VQSmhra31Rp*wQ2sJtA#@UeN73Dhr90P=Zn{T-OOK_fhOrZ8+J zer{Jii<3Qjbd1ulbLgCA){(#h#t=3Ff7&CSh~ z_P>4*qFY#1Rb|RE1D{X#7Hawshht!vxy$uo1P_sSN&jWf6OYb@*}<^LrI2HG8OBv% zYq*ra>#P+`O-*2NEXF1pvhnVjtj^S`5!-T+(QHCNVWEP(lxRL0o(()mOia`;?v|Pa zfS2k$Gedk-U8qrRyhB!JWhn1upc(LTV`*x{EW0ZgH6rM)Dtz3e(3guQz8<5YLPnwr|`+>jNcfKpgk_~J$OU@$k5LZN`1&vri97a3(3)gZJx zG}>9g1|d{ZQc4!m72v`n*lCnjVu4$YN~EG$S#NuFL_N}|5O!OP5oP?N_@ zv1s*R%NCQ6N5xBUiqeZ`s16?$*4$9d8<jY=+ zS#|x(R9v?T5&~>!lXq6^_cMU=Sy@D&sHo_Okx}1hK+Pp>TU%QlZSAjxsVFhOSLz5} zz|PHle(;w61W>oe9bl>2WttK`zK)@$e&BxAUVAm$*49RR(_o)hT3y}GYspKG=r9$$ zaG{iIjQ$v>wjWqG;(aL5vKWY{?Y-)C7FUHC^fT~h)~WO7gu+l&S20>japLYRW*wn) z10b+x{%QZJVBOYP)gSgIC$wS1+8xE?(7DtBTsKX_` z8DDFkt`8cw z`IB(55!rhCnSxj-J@Z+K8JOKqd}zndd`tFPf_t^6^$FnDTJ(q7B=^l&zkBzt@-z`r z#AJ%N6-0A0g(TuVF0f*mV}DPI3sQiADhkNs1jv+YM2ci$ZvBp-1i`lS}SA6I3O0PC(%i zbr*F=SxXb?UVa#iAzB1on%oW(J7^_xZKlD>!H})i0UCG8ndR6AmQx@PzSfs%y#wN? z?0_eqbyU=tZ%yB}(`|b<@74FLj5~Q3ruu|-GmT4 zWlI!*Y?mzCuXk;23<_T$=|F;wWLo{tu`o=6D&KJ;dPY$t9BLdV3I-1vOZz-jl^a7{ zvufNFQKJGAD^1~Mj_;O>PFrvJLkR1Llw<<{!V)zI#&yCSk#FE2XEO6m?UBpDF4ug0 zryFR9*X`o5KzCno_cE7FcTBi4ZDhbR0*&qJXcb3OPCCze8O;&csMB{tKYocy(A(i>?}uVSUSliSNYs>+gsot{%WWuC&iv z_7g#?A;HWH3zkFg=$_K^)T@PtEe(3>KTr=j6}&F^AvHBsZTt44S^dzfh1p!N@X=+1 z>(^%eR|)%ajQ}De;_Qqho!XubQx)V5Fc_@9M|2lTPxFpPmN|-e-jBIB( zl?qq=!7K=pY_RIK7gH^GWvZJR?UP}QRU~L$Q`nuYoFZli6dVk;bZLx;D`#~0kU1TT zu7I0cS#`h6Q#(T)tgh@PN~cJ`6lb*fFVFOqAWh=-k`r8CpXwD`+t`c;V#nx0yGAfp zh};?UF?nT|vpD6B#U`1jx}{0PGrgYR?{ie@R+p^wu4XJk0iR9!4os95()I#$a+Xs} z>!-!csQDNf@GPs><&w>>^!i4K0A2^cN2)-om0c8ZoMGy+gbbycChQq|qk#WF`p(YU zdPdOAZI?XVA%4qV%q&*2IlO{18IROFA+LYzm?BIT`vZ*t)n#3Fb+r=5($$`azufAg zSY1|DwwjhTXK>(4c|(op0}w5Nf$jlLO#=M@N&L<+nzVP;Hl@tW zf>n?yW5$#{EK9|3+(E?0_oXOlfQm&p`YLD?AvlcfrpV7yyO{VFH zA5057hO_qxBNf9$7ycMwg&2qo=MXsfA(Nm+3C2cU*u(krMwDpwnws*E&?HQF z;^bV6iN^!?;U#$b_wx89=Z|NLi zVXPN(XgJFd;LLwN*d*5`zL4Jy3N0OtfWW{&X5Y=(?0umOMcbRM&d%G_)NZRI_d=>M z<8Kq`yGF`--Dv^6cbM~g*4l4)<8VGcJ{|^gO-)T?DXp7EZlc}#;;xl=`E)LY{kk(2z87g^A}-=@m&N zQjKXtWo6}8>C&9BXpL`*S7ZEW>;R0n2+G}zN|#sfGzmCow|wTt&FN`SC!=~H5>N5P zRtSgTi-Ezx`d3;g?uD4naC*e+B=F#t2&`A^y_cM{5`*{zq`!?fpiz&MwFpk|ia;nO z;?+hnR3yzviUo=(UAvxkhx2&6&8tJ8)&X@(V^9701EKS;q@{Rs#PU>W4GorUBM(Kk z!lO-lHpcDVTx-GT&UaF}9}VhKRBgyYQH?Sv_t~8D@t_i?$9;0JMGaWI5GdkfRdqF{s)K4TwM(@+v9!oHtgWq$34Hx49n=-fK!07@$vabE#-%9H zMQa>D{7F;H05j~^bA3HMJt-qzmjPg4P|ms2%3f56nitybSG{CY8QT!y6s<#|$;-=g z>cwG=^?m*QKATYg?4C;2^SZAND_^~f0CNbFusQ_xT8YzWv5>dK*3ABQpV)Qg90Zqp zyep+PYbc7%W+TWCjPVZN!kY7t;Fc84qYck;%g2Lx(L`RR^({k3K!=1}TF=O+s=7Mm4fAG>y|bHJ+YQ~% zIf+NaU8CyZu!jLT&*^K?S+;2>ic3o$*S;Ppf{DO(7i{*V0(J*}DKC6b00-gbgQ^Dk zHM7-6Dew<=ro7j;TqY1p@ErStO-?nJ0PTCWTi5|AKdhYqdli7+m1*CulVq9G*VhNW z`*m)U*7aud5sl<0PcAt-zhQgsqSOx*7Z-!AH`w!{U$*9&=z}=`tgnF*t!cyw;23;# zS8xflN1IO8nK-JiuWw>P5D9q}@frjXZ!-GiJnZ=K6)@U@Q|j9ajR6rbn193rB>0ye zqvyL01i7fVxYK3N`#KsA6z~h5T9_f9HUT-oqQL&);V|w@UwgYP9{${gA{vo~e+E^% zFiUfdHTEXq%mYbCL+8j>kZ=fO7X%^$fk^(5Ti{nO|2g~r!#w2wpQDAFF2D8lzetrE ztQOb16C*I6fcN5%tw$uk%#Hr$^y{yg|6>00H>Y1enQvhHXOIK=O5#`H`EOKy9si%b z@wcJ)-R&X3Z*%3hpZ>0Yzi*EGO{(sKxJ&-;Omo9~@g~S7r1WGVs`4rXyw07rJyn44 Gxce_{;g)Rx diff --git a/src/mod/plugins/no_img.psd b/src/mod/plugins/no_img.psd index a5691e2257ab1319d9cc2f329a0c89b707ab6b84..8e83901b130f7b1511773b3a869ed86d02fec718 100644 GIT binary patch literal 189610 zcmeFa2UrwI_cz+ZkaL#I^ne%!hp8C^3` zj3A0cL5Y(3o~mv}-1U85?>{{Ee$NNv41KD8UDJK)#I93lF?rqsfT~5rE*n{NkoH!16VmZab!Pd*oW1*Xy^91J3=dE+|a`Rm0=FXe4U=(lQ5+_%Cx6Ph| z#+m#b{_Xp}jpQ)Z*2`hSaMR)D#-_uKhmT%3e7L2lnWdTO5K{|FQ`269Fv!1+VFPnv zZ|i0I{U{a-&0rK$OS4hENBIu>_i^0poz`yssZqWm*)hhMASU->I-XI)ufMhbr^#^d zVdRgeMA-e$i4k^LPH}T~^RRKVcbG6@oXL-xd)@fAZtE#i=6SfSb#iu?;5mQBWS;f3 zDWlBBj2dM;e3)79VS3^Ir*GcgPI%Yu-X6|a-tFy79Go3o99+FT;hxRD7e;$K%e8JE zF1B71Y~9_Ro$PFpyqj!twP&SRcz5GWzCr(|uNp+$#zuJIKhz)>7n2`po~PHeO`w#l zlK=2b&xKpv9Zcpsc)EFe*f~tw1bq0W>8w85*jQS-dV1Nq+BsNHodB0J%*n|f%!{d+ z=`;&-3ybOI)25r5O&&dR#H8sX$4nkQY1)XXBc@}u`u@~ja)EEka;lr1H)1_ItUU~C zWjQ~xpSTn{I(!?w=7FzBlP-wgkQ zA^!0-Go3uaN^R{?MaJx5guO?7s{3hHj_;pu=lKc3&i`rby683~<(XVB8t z4$0jFJ7g9e?8ljW*ZS?U{(hR%KPT-!bC(&r*Qx({{LSkeT>qU}Wro72e?5q&+gh*9 zwjK_X96=8LleK3?{Qi``56zfLXQ%GlSt^xtFuf7Y4*GW-AOkN&@5|2L_%TW9O) z=wLs=q(@+T8h&oZn0fqOlX{f@Z<_aG@5r=5zyqrVmyi`dbI`}G zpW%`a)5k6$D}Lsnk6k~*B_XDdT|!p;%t0T!euhgzOdq?1toWIOK6d>KmxP!;b_rSW zGY5U_`WY?>F@5Y3vf^hB`q=d|ToPjX*d=7e&m8oz>u0zm#PqRC$cmpi=wsKi zW0#N>KXcH>uAkwO5YxvlAuE37ppRWY!zCf6k6l7m{LDcgyMBgCLQEgKgsk|PgFbfs z43~tMK6VLN@iPZ~?D`ol2{C=_60+iF4*J;jGh7m4`q(98#m^k{vFm5JB*gTwOUR0! zIp|~8&u~eI>0_6W6+d(EZ`mdNZTq5wE9_a^44V`2?nXaB}sU z>+R+4?FEfu2r|#pYrz`Ws)rHQu3iofuHG)F10CbvE{;6-t$@#6;IsuHCOdhdb5J)k zrh~^Xwk{3}r!8FwO)A6`!j^C)+~Cb4W)Q;(Q^E}1Ji?Q3AUp^s`1C~QvV+3LGPku0 z9K5{U=dM|A2hC#Z$dz)3jaOX4AZd4DKhXF(Cp&w&vezlQ#@or+%gGfF0{v1LJ;i3( zY-R`OSTu%+F?@XEgK{rE%*DGP(dG0*$SLk#u06L#OmSZ0AtdHIdfEtyDITsp$8iJO2|m*~5O}B3G~J2J@ZKj0=AzJKOVq)IQ(S*$cPN+u}TNz7Q_G z$-xdbOTq3(*eS}&$~?z;o;|XHju9Asqsu1dIILxc`PIv6g!|;#FNI%Lf5Kj3bk-N#CQT4D{$W~0 z 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%0AHK@ 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 literal 96484 zcmeHw2|!fU{{NW)RKyLJvN9cU!%Ak@(cBPR$TbnH)NF(q7=?jm1`&Hk_IyRFWrd-Y zm6T6Rd)RX+B3o>+sH{-Sq$pDp71wbA;r>70bC;Q6QS9}r*ZbdrnS1Y9zRTx)&-X0% z+;gKwO^Ih3CVspa)8Y)@HVo(VnNc1!DsG~8KYr1P|GNF~xV{t+l{|FBf=px5Jd;B= z$CP5R4huM5vMoSoNgft3J$#~JVrGmf)iQ3O%@n_I(zK+7^O6Q92aFircIbj33(_;w zO%9`OL3*0iK4ihL0Aq5-9Mce-Q?Ncj$CEhb4GV~-2;FTHr|4obY$jcJQ0Sl}LvXMz zA|fa_EIcA2_$FP5Avi>Dz<EbS{? z#yJ^Tj$r`-#7Ox_dZlM7jI8z`u47PAM!J51F;gELWY8;=k#Q(684_bN866q6X&D)5 z!$r-fq-HoW?5P=lt9W!NTWB%6kZx@U6*yTowI6&DjT#g<{Vq?v}>r;Z(^ ziyIvi5j;2|Vo*p>usa#IbicO&B_}DClbK~p<31xfNpDIsrJJk{JM1RdvqC2)4KZif z(v6Pc#>~t#OOla%hJL;^S@Hx*Nel=%;50gNbZ^@PIS-eW)~va*o>qsf?gt>e6QP$?^XQ!I1mui|sQan}jMC=)6N4C*sicWzYT&~3wDSBpUNcvJf&$C=) z3vE!2O}AHKD7ql%{pgkMPoq*+%Ha1rX{h4wNn4?%GEtyt$%zVAOHfkMsBkGzwB$sE zt0gEYX;iorC|Yu&!qpO#lr$<_3KT6lQQ>L{N=h0PE(MB~oTzZM1SKVn3YP*!OHNd{ zT7r_2Mukg(q9rFPTrELKNu$E0K+%#D6|R<`q@+>dQlMzbi3(RsP*T#Ua4Ar<AfiZC0G3-ad;hwujwYw=6ZBZ|*1 z`eoWIR>$NlM`o4-fj*REiro=E2M^Ej6meFE$z;t+rwBUd$Mh5(jvaa4c*_DxG0Nhg zJQOX`G}-8xt7)Ten}(n+EQT4Gm1W?oV`EteGq7NMb~SFFvpu5 zS(%gP%uPbDPipU(?H?XAV+BvtHdBemHJ5W#n!_r|*=|mjCCy>6aw3TL<*8#P-aejj z9Bw8W+Clhn;n3L)hspf8L=xv}N*R;su*#~jn6x=II7 za9gRK!eDt?Cr>qw>CI-uq~W&}579^=FBPm%LP9vPJMb%~3FjeX`BDg{c^=n{Wilo- z`SJRdqMM8o{*yS41(D_tPV*~zh?+4%N<_{;XH2Wn@K}&p8M2%=JrLrH}Niw|prb-4ZAlr9j$p)b<*8CAmWRLwmdi#d7A_(-RWV?G-O7#!B~A9kvL@BdP7VeDt&>e=V-|P3zVl5sM-#~tha6c4#YQyBb zCI%8C)(&6oh!gx=tgjt;FuY>BaT6@o3@yw*9jzKYYkb@7cj#Jw0rU4ot%IQ(lkAxj z^W=0Z3|vbxXC|j7)A@Fs=grR|!}pqi z^9SZzOxZXu$9dnhtaJ;`$-j0@HyKeGy$Q3v4pUMp&J8$kXPY(^?W6ZlY#q@q#qE5K z+qnZ4h_oywBQuAWi7qf{pblFtMCrzvveQfs$Dk?LLBeQD#{LfJnMP|4W1`NKi*;`F zus07De=Zs6mhMugRJLOL>05c3X|5`Bg}HLN!ZKclheuoH^@J;IP61HrY&g23(g2D?{f8#cfTt@Le=4R*%Q|-6;IVY;F|j zRJ3cHbSbQ7U1tZePG5FqUdPIqcNahAHS-0;Xq0lB;G4*(=1uQ)NVI#LBcA_y{Aicr zQfs%Q=qNa5>NH(amTf+rOD&-fYs)&auIw7tll5VNY!HSHk?dwRl8t7!u*qy1yPe(1 zl2|HB$1ozB{f#YV53onr6YLqbf~{hOY%SZsHnJ`3ZT24fh<(O(vR&+ZR>=;qBkTk_ z%W5?mO>0eiO&85InqHcInj1ACnkdb1&1lU8%~Z|pn!7Y98mlHt^Eb`kHIHbPX;x@n z(yY}KYu?nnr`e(TO7orOC(U8aDNT*mOWRJ{ReQa*zgDk}(nf2?Yo}>vX;ZX!YZq#l zXdl-;t9@Dfns&4HJ?&@OUD`_R5v|jUdHH!=<<;A3kXMvfjMrqZJG{(ZHm`fV9`?%j zdf97(*IQm6dwt_o>2=iWf_E$LF5WutLEblekMmCOPV&Cn`(E!yy;peu(|fb`cJFVz zfAT)zUGLN0r-#ptK0|!sd~Wke@yYgiz$f45Ri8~hANuU_IpA})RjXE4wHnYWvemd& zx3`+x>Yi4QwtBwR>#g2x^;N6=t(>h}x4x$Jjje~Zp4{5l+R^%f)&;F!YrU=Y*R2n< zKHtW#O|Lc~ZN{{@qm8xA{cZBw6t#J$&DU)XwW;;(}_jtQC?cQzoUAxom+qduEeq{SQ+B@1m-hOTS58D6G{(Ofn9fCT< zbx7`z+hJvg%^kk(P~FkDW515k9q;V8uw#D5H#&aV@n|RCPW?N@bTW3jx6{f_Z+6<# z$?4z4KiGe=f4cvp{zd*f{D1A-x^w@|qdO;eUebAW=MOsX@8Z>^PnVc3NnQTlrLfC~ zT@G|@)pbDExUO@%KGJo4*Dt!B=+?PgShvJ(3%afBR@$xdDzB^hUln&%+Eq)h+IZFO zt7@*k{_5zfO;$;EZZtDJU_t(4c>Rxk=?wZlp%)92vYqnfdajo~Y zH(Yz`wb|FcaP5cJ9=)#XbvIv^blt<(ZM<%84{eVddQ9zcPmk3-%6d4j?|J>$>ocz} zxc-CdkM_K(=g6LOdp_Osot}q#b?G&{m!;R!y|(qL(sk2C>(X@3>bC1n^zPAnT<@&j zFZKSScWuCcfP{ei16~iP=+m}OR3CGnr~AC$=S1ILeJA$)Tiz9eAMC%S|E~kO4~QGEaKO3&`vThsjtIOv@a4erfvpBc4V*Xd`GGrc(B2S! zL+TAHZ}{>??TryPT5f#)#;Ng*pk%0t_SjtR{T-5Po_?8Y!t z*b8CbhIa{{82(`R`{8vFkrB3t4H1VU`$pas`CR0lsIE~{q8^R`PZQXhMI;J4&6U2V3={(OT#LM>xSPo{KeswBYKZ8j(B;* z&m;ScOdk2_$V1TsqvuAy7F|8cFv>P+)2MSXLt+-iyc_EkJ2v*=*e^zR9-T0H#poZ# z1dK@;vu@0ZvEgIy8T;-ypK;^IJvnYq-1TuuackpF+!A@qqFc6)Z#O=E{EG2EPq<-% zZNgg73zE22D$VzxWv18)Q#I=d%rpHcSHofvT{cVeG`|S2>Z@1jO^^Vqe zB;N7r9nKk}XFN0Gz|8QO56s*>Yrw1pvv%Bh?VV|Nmd@@t+c^7;yL|4Nepk_57mbsR zFB_e6;^sUz=R{IW((u61vlFx5Sm3|Fx}YrQhMb3T4lNwB@RfVK z?@7An{lE48+v30dv?zMfOZRH-opbN|xqWh%4AxPRIGXZ}9@ z?{6)+e#w1H_Wxt-Kh{0a;Q`wNdmg;`!Bu(Qd6v8{9twS^;Gz15lOO){5yK<-k6e5- z>CsOg3x4d`$ArhtkAJZ=V(AM{w0gq&#O^0YJo(RM{>v6F+yB(Wr#3&Wd-~z0&pb2x znH~9I`Ky+la48uwm8p zs~%ZZ^P=U&Z(kbw(&m@@zns6i)#~}H4;CgCe*8+*D{Ei9_SHvTty^PVQ~A%S|J=Sd za_!op9z{#nY1d_~JN(+r*LJRtUB7k1pbam*e%0%b{EPi7>t9vHcNOp6IDX^%Z$!PZ z;otrK{oJN5n;zb**_^Za#Fo@8m0NG$`qi7ay!rlHH@~&1B&cM~+r8gjS=z01={s%T zS@MpsZQ(ZOyP5AEeJ|y`{qN6y|N9Sa`=EULt=qr&aQug#d^F~x4?d3mc-tq#K6!h` zkR5M+8ujUx&munCTozuo>GSZQD#pW*~zudZW@Xois8v4~cUyu0uy>DW_`KUau z{Igw?c73%wVfVLtX6~u{Hu>9Mzf1e>_x=|Ip)ymwz1i&-&wMQQ~ z=6~$D>cHxMA0K(V?8I#+4xF@|6iz*Q`kK>g&V-$L-#Nwk!`bw+wdWo_f6e)SUWmHz zan1CaL$%o#TVH&(ZeZP8_2cXJ3F(5s#~NLg&VU!crXDo&TaO>=PrN98HI+<5-F3ew zS$C!pz7>wKc6{rv*)bDwUClc1E0%m85^03**%H>SO`A5pZQA+zw)6LE>*wFCW4m@8 zyLIo12{U{Hm8X{=I+fQ`3@1zrWfMarM^;KbuayvE+kqvi6@IGvMjMfBgHI?d89m zd1cdwyAC)L&GR3~e|7UmyAPg?96LQF`@!XFwtT$j&^dpm)k0ctE>r7PKH*%1-oc%{ zA;H}NU3@|oKiQQecyH=gCqnzq`N?+wvY7a8N%pXl{aTR#t^0?+|22eIeRcBagb0V} zXGMhOB^clmLD2!CP+ov|`@o#GaL3AL>PNA271b*9~I`}eY^mv#T-#UJke z@X^Qzj)z;X`@kG|v}Q(^=}#>+z5Z&+ZMAdjZJW1V9g(|e^}g*-KhyWdQ+J>IVAhlK z26njljCQ8D7_@(u4i zy72j|vk@x_*1eX~KDGXhieqtcokwo}{DaTe?f&4Mw^zJ6?3O#fzi!0t*WN6O$O?LX zM#ZdxX&XQ4ZofV$y7emi_+{~?zBh#2ed^j-OXmg6o3!WJeLXY}?VW$|)~UQq1SWhV z;*%+`{WoeitbTH*A?==`+DS92(qF5YaLhX!6ZJn9DXJhNzYSxXoVE(eU z>K0qgkF(xbpZiG9e}=BOW|zS)c;Pes`)%2kH7FuIEi(PbFSexodanMlRiEEDd&Y*( zKKrQVP3P=u_ayDw{nMNIi6e8rJG^1b?D3B5)33cDFzp$Et#Ec5adPE1M-M;x`H`qy z%YSnIEu+{uWbe54Z~kMmW8bOpj7?>=H*P!g%8{5Oceb9=eZ=(o;xl_^J(C-A)5d*s z>z{f0j!(aT@^IaNjPGYx3v9`vRnIPaD(m>kMa~6|@ss1WTU*`R&3v>+k>ARB>Hl1~ z>3B)@TJxb$Ywe<=HEDGLdumqh4my&XI3zdzlijuJ0uH4{4&QQf&(9*Z%$rd)eEpGC zRRE{g>Z}e=nXld@uo)v>OWAGtc)MxxydznU-ITR_@KGp`c=6cM+`3IQrmyPafBo{; z-07JMXDu1BZ=t|$8MC@PCu>iuRr}I6kC=PQ-NEk)?Bk>x1vcRWfsOe3Up0$9KfG$c zS7cG^(YIee?#$bpw;z5gKIpxF-Fy7Vvn4m>p4xqIaIf=659A(xHuq3XqIE>vu(`)y zzkc`|S8oegvw!RLg|lW%{PyGAgZI9$x$xE3&wsk9bnMy{Tb9PWRQJp)&sRLNa$b+_ zV-H2V-7foffz6sV{rr1hY`Ly#*SA~u9&k>7W6hi`=TFfk!`{w`k0Fv)9f3^|hK_pU<9iOIqEO4Ua$Md}RHi>94%=Lg=EPm+t#! z$A*ulk4fBCGVAF2qQ@hAdd;lg;+(N?^O3ivM;C8>c0u1&aeLZ-5KtR4?uUc#Y^%w7 z<6`HIw;$fKXj;vOJ8PU3F$0D_vSHYWkbPVJl|MZ*`<3eT&)oaKmYO#^{#gG|cXMu8 z?)qWz1J>?IIok8hB-@Q&tlv=l?;oopb}ifc{hKeWTV?)nO}$@zubsEduNg7EX8D7= zzL-7wjcZ@~tSD?n@CNIiF#n{b-_5)C^P}IL*&H;Y{)6kBUmodQQ$BF7b@fqmW%Z)a zv(LY<{G+9X>m9ilHa+h=^2#rZ;y<#yzkN-gW8Zb1QgdY9(WI3li_b=E%Y_MU_~?Tp zFp8SUJtJN_wBg&`e$^j$uPs{i&BiS?KYl(vd(FiIaRZOmzm~LU*1A{y>NkXJzRNUh zck6_$;a^y4Z?34BoPGZKM`oE8%|24$w?65%`iJh?_-J<8g7l1khJR7l|Itm)+077OgDneWGC zKK|*?!+uyLupZks^g0)?wDzvtU%vJr9$waWd7Gmf`czcEvwqQ{nlH#gA*K4+`je|GaW%fb57xu1_e`$ER? z$cwuY`flxAyZY$#9@`ewt>2t=)B9)dKlq-m*WA<3eR*=JbMtHaR;>Hv#gKLD*Wb6( zckjpVY(BE>+#MyK3hcRz?e`|^FO8|cwM<|iL<;QC_g!=P_)pVbv^`nU@7EERLFBnU zZ|ME2;UCT)sDqDvvup16UIOdCynb-*%n{Y2d!25R`R%vplLhwCo~o3(_j2pE|B`!r)a>(n>m~?n zt6pH=KDV^)*YS@oSa;t$5zEd_zLLUQgBg@ykd{^D5n)TnDI#RZMdsa@@FJC>qsQR(bv#e*ovwwG{=cZh9fbr!1mwTilMPKI^jS^#=y}ZS2)O?w$el5!>QNRQn0+?0`N3TiCe) zVkzBE&VG0Hb(c>#GwH*bJ}3XZd}HqZdCt)K%54IhV;gbqC4B>AKN_0w?rdR?XVv{( zyGh@PcL#d&j%)&cd9Su3i)P7K5ike${&koxOvSkaUpqg`M6xcINuk54f_N4|1Vs)3 z#6c4@aU#0`Cv8}3O=o_?935taU(1&_P=|Vs<~rqkt|m}QqeYrveuIwg7}_);{G7%g zPdsShH|8jBA-+3V+l%)xhI_Qq@sisNGuk_Nh#`cwRF?M+Mu+}IjZ3x{wK9Dz#sjW% z@S;yE4JHb4)X*JEmmZp)EJK;-t@R4q1l0+^s_PcF*cbu-O`k)Pl&O)$w+!l&EPj<+YnXya137J zr#;fmHr%!0W$&i5totjtL$oNB$MWQ2LYNLmqgx22!!H85g%DLMHm?2wjjjoD zy0%!&3#52{JlrqQ0y(=6#GTDcPa-H9H)@0@s=%VAz%%aros;PLqsS2I8W6g00eZ^ErB z@Wa(xL>0eOYu_SiMWjjCfPIt#AEay<(rx6ep^=GbP0NW-WvurTOe5dlpHw*0;$Gh3G)zvqYZxIf{J0-LsA0%3@HiyPw$96ITiu`ip1yB9hZ$0owbjm6yz z`n01vePeBwkh@SdtuVeJKDT zxc8J{!x_Ky)e}8kV7<^txAp^(X!vl-GsS2#rr3;`sZ+D8uAI_6HG~mkgi9pd(#NNo zOyKN_^3_YZ;vG3@CLXOb@C3+(#Xz`^ZZX-#08v0m2IZiSPM!<%gEB~rU26X>YWV7Jf!r3ev>mpr zwy%DS#b$Tl*1IVV#cFksjH1}S`gn)Y=4c!|+M3)rFrMybjxu56ZIcHH3XV2AOg8t> zXsrJjmy?M#Sy%v~bJ(&>GD{^id3uH|Io`6+q>GT_rr1nkv@S#m%CI{chT#UM-ISE& zu*{c|+jUWL`a7`FK^Gd~Qjv3kjC5yohAt?Ims@a9XsBFxzWNEcS#GkCeqtiIAYFwC z8y%YlbzEtv^yRN)1-^RHdVu-*5ZG*M}>s`Gl;!fNiS!(3sxzQStdx-B~frB2D}wxIU8sI;6q zDpLm4Ah`hOikom`a8S4*I4V+(aJK}vG9gyg;Vh`>BOY|B@lXiUp8$=)g`I6NXMLg#sqjaD*~rhsdC`BHTi%a`vxhaf~wBO^sSzCI*iWV6^Jb`uL?L)gu|y?Xnymjxf4L6!;q z50XgNUDrcsW9TZ}EX**pF|;+bGjuR?VlxBL`@csxAb7LYf{v{*bQZkCoa0TVOrEiu zuAdkpH?op`V0aTnaFDMQxOf0)W)4}%O8|E{&}0&q)Kg4!Wh#*c=_h8)wT#Cq!v+RI z4{F%(_KiyzN(2TT8Eh`hWjqWe9mjag!#G%QL(KjIS|Pt9tKj@ zljiCsa8IgNB9uXkYn0KTN8g|TWZ>r|-5G^Z-c*e6MiJ9y$1Dv-ERs`fvYX#*C%?%q zK89@#JNWvhy7ywBXXl=Ga+OZJ+)zOYC9Gk`9%^`Y>*2o}bn4M8G}EQmdv@scm+Q{A zpfi8@u6%=zyf_$Fjt623$R2>aV!> zig|ZK?t;Ki*fQo}Ac=Q~kx%;1h>={>{dEILq|XE0K;j8$e!?x~?XD6L2g`aqg+K+w z`^GN&iNTZ=`pzbs3t`D*#^GIlmPuLZbI@0Ihikmio@z-pN@H++ti^~I4#EcEk3`TL z)H_~1n(1auLQ6?^CgS-sjCS00xCO0dD!N#M(2AOH7sEnrsOaG1`HKxj9^5Fo6XEut zkH_l}Q%#V98)TC`6W2YFICSLZgR-5C0dK3^8jJF9e zz5GTo`Gsj1t8jNG``u>TW)nA<)V{Jj7Et@jYG1jT@qpS_ZafvP_Lcdhx75dRpYfI( z73tWqJjV^z$%q$xflX?qCzXrQHHQ8>Ys{$ihzoM zihzoMihzoMiopK_0=>(7Z&UF)dv1l8&Jn6Dgr73e|ZGd8Ph*{hDznf5m0AL|4Zg(e}}&LU)~F<7^n!S2&f3C z2&f3C2&f4BParU|eB_4H8&B^Xxfp-yxr&2o1AnRwsG}qm0TqG2JOY?8?aSZe5Y0xh zSp40IznR#*){Gb2+{I>N>o_AnOF;;27&jLi$j!kSM{@E4Pf~F+k zgiNGNByx;tva62>n(Bmo?liJNqfDY<292_xnI@qE@4L%B$q69He2hzRoN4@v4hfn9 z{~z25l!18wQjrivXCc%9q$@zWGXK*O1x>YoH79WbQDp}x>PLkHWjO*phy;%QPOv`6 zw4r6fK?oaqhSNZzY>~uCBC(%HRJxPML)Hq3gc&5t4l%FLY5`jqgSnH_Kxn{mF2*UM zaTa(MW!J@OvWqCa2epc$jD>0uBr0k-+M0msdf_~ZGY=WH*#*@AE?blkG>T;E ztl*rWsW6|%8$it@RNhaDs7MxijIlf-4krG!f~F`N#IvhNraGaPN;%*h%Af?HMW@M_ zVyhTl9~OIp%YK@%1W?a|QX2m}5Rd)Yop=F=S4hOoAf6oxGE_uR1B&FKOf(5lf|rg$ zyG*2g1|%z>lKCWK=JQa&f4@8J>T2+Y0wj0=1Px%-1PiVbY6Xz2hKSz&WkPl6DM@Tt zJS71=stspEYc(I|)LFKRIMR^135Y3xlqD4xMK0B-Qx%XMaoMn6e-IPMD5`;MLBdIe zi>3q^qM%Q4K7>p)bVC-FcY(rxQPdzvK$Xgh(GeK25~PC$UIma3mu0xjt`W4*W3R$N zTS3fOw#md$~+0c!|EBRI-uVq|(*b^1RSy(s4qw1I7 z7<#}B9&HK7rR0=ooaR_!Kn0IuSsgEURt5&7cYqrMf35($o5X+wFoS^^$Lt0S2q36R z;A{M41{9bF1_^`%)FTNC_9z;7#fmDFDICg>u=)^G$Fa=Cfv_Z+*|v z799MM0p5*lgT;Dq;5wL{k`5#Q=oefN+^NGelX*f87Jy1%7RdtDC6nju9h8lW(QXc4pI#HCU1l52SmnC3BI2D17WG~Dg z7M|zEhGoMHC^B2&LS+#EE(9rjtK1I3#$y>7P#(CHfeR`C2~S)ID>Jx+5+NW7JJcjJ z;KHpKzF$@bcpjkg%5!Ida^rYa!03RN7v4k1d7O|+x<=$x1|ASoJvB}#C08!&buhamMM(LeDk0Zq+-?XD)E9O4%O#T4y0Q>7Y?RH zbtD#i$dbtriBFLSpAwWERS@$9Ng*nFZ~?}Rc#vfw9b^+&1(&h}JP5UF=txx1*jx~_ z*^M~F!o;inYc9_MCC`e$f?O^tIY<%4x&c6{LPbbTN0ghy4drw?o97_gpbAvhBrlja zRSHEjR3Pr;Jp3q}Gqizs^9O{~UxXDBA5_iI{ooD`$R{u~=qP`(K^Z%sO46IjX9IiE zhPMUq*3|STG}4BAC^`}ZU4d97$fIGr;E!S;4;tDu7wUObxN+fa0k|Lw3-#oJ8b_@G z911}et8l3R7j$ZQTaeuZ7nB{eBk8y*Qe`Ls6DqK3_ePYeGo{k_2Y^L|=%kQ86mJ~{ zT2P~?Nd~ZM=wBh45cOk<$XL+LAfqX2#DTikW@--Ll&L(xA+J(|if3$G9sm=zj8>g@ zOTd8ep)nF2&7KTUn|U7$V6gy&K?N8PN<3p@1~Bw8bQ>xZ1|)d}7*L1GzY&83^yQ_t z0>Y3Bqs|vql_KyUmsP?1(F1Q_R^URu+0472(3-S0lQ1|Ixw?}elMOP}7)Ibo2!RPT zMAU^$Ab}7|d9xd{fX_p}96cN|Td+WK*gfy1BSS(XCKwYDIf)5XRdnN#lCYw@0*#9r zr~w{ix4e^%k|BtSkcuR#aE%^FUJwbz$i>xDAgC%REsj+GsD=w=7>73Gfp&<@l^iS8 zSB!hO_60N!^5g+3Bmw*dXi`ljq-elJs1j0u9K(|`e}LCO5gN5t)6fY8Q$if5fyJ@J zHFyK9Y|unTR;~2h2&M#Zpn@hp1;lCWhQWP94hW%fFEI$k_^^nN4~u|#KqD3eX@SUs zU?bsCyi`X46mNLyP?Q+zXaW%E?yaC0#?6cd2NoJPg+w@QEcpgpyNeU6h>=8=H__0{seH<8+CQnH-4N zWrsEdST@*@^F;f|%yn+#^*jjA=K^^11XCbDj-~f`iCP|3ger_`$LpYS7x>FUdKEt$TagG8FAG|A<3&ovB zrUCi#4nYE{VdBJ*2O-il4}_?>NWjQFv;y-bW#W8^LFP(}Y|t^Fd%yNkzTjT~<<%9s3#W-r4Gp{97EjIB*NT>hFJwfLe0@DT05N6{;oo zmn}JLcT9UaPcb?iwDmE6H}?WZ2x9~0>-2-8bVG;g^afp|ZuoFI(p9h&HbXj6*xi6x~q&w5wrVS(!-3ZSj9taY(cSo8dI#3c}76~QC zat+0dq0MD%;w3%Jy{9k?GKEWY3_5*i(}f}yaO2lxHb*tIA=R*^(7aNrAq}a9{vK3= z8&Y*EAeUK+vfhr;q!)sw^BavB{KcSFgz(TSpfi@`NVegCwPdj?qI(IgFGu$d*k!Zw zUZRW|%6CaRZq>T6<%)=Mh{aQ$T0G^7iKko^@id$YSv-w!opM(tR)er+s{wf=D>Jzj z4Ez+Xg2`Xu7$O}*rDIsr{-ZfNZTiJmuKGkyk-xcBdTT3QweOcF)-gc@p&|6&T z6|#6GEWIYSEq_gHwV+cyFw5K!yf__S5yp6$eOu+byoc+`X7sHsUX-+}KTc z0wuo`)&nR)qzNqOg}JqoX3gl;K$)&k14^_&1vfiwsDk{$Uk?%ge=N)Y!m|3$e2VBY zuBrm|oU|kqY5mdNI z!q)gvDY!_*Fd`Mhh>VmZjg;k!9L%eMOR?sGr4o`~nyLUBrDzq!wTcSSg_0amq0&*2 zCyLjAD6!hP_{C(|9U19kO#C&n9L^|IF9TO5N|F*Fi~kLL`Q5$I6mYVfs*>!IZbbAF za_zsImyo>)RDyn_nFP3mH!3#;%B4nzNZtX)E_nxfFD~*LbP@RFH3ZIqe2(lKLL-O( z{DYJ-LWeq|52kVtisFC=+E_p#p&6VscJen!VmfVd@YBPV=R*Ki6Kim69a4Fz+sdhK%cHy(AQt~SDTk8XQ z7xENG3mlj{ujB5(#MuUFIe2r*e~6Sg-xG#V@=jtgDb5lnqDvH80t0e4aI+W$PhmC; zFKMN%851EGcj0^zA1s>?W5GIHdRej^uYiunH;1Jm#WW0^Q&5l5X=>ViH$yjpW#W(M z(i&eo&Z0pt6YG18m*gTlm6l0A0prK)-@rv4p|^}nV_jSmM%07*kvg49v!bkPo!r-rD!{Syh^10&40;S_2&tbaD3-fkyq2a0y=Eo-l z7-oVL{#55FPy%K{dHKi8vUKIDlq38&p z8-)Qmy&atT7~(ZL2>d!y7lbBPcFxof5V7FxC3MvF00TY^qPreve4MZAjmSK#i}FvhtA)=gJz41^>;Ew?usMO~>kpkbDnrzs4Y@wu`c7!*Hk zKyJnLVVow?G_;f5r2!3{EB=z@+q4V9K<|omqt>dSZiMEg+%^V~VxA_Qa5J*W<$DD_ zhz6%uESp@uS0Y;z<$GBSaovQ~^UXv~2=EytE-YL#&kXv=*o zv4=&uCb7v($Kr77H<~}wNF7RjER8=@-;cw_y_a4W^(=%~W|3MS02- zTHG8WgM-2i!5EaH<0g&D!*$_&V(tpY(m}!D5ty&@Jjfkz$S3}LN{jA?C>n|}M4W(8 zl8F&AIMU5-q!L4DN^nYyGN0!P9E0-WGoF0*m`1lmh>}kwwLLByC7FQC82Cnn1Pu-e zraUP6$grTOh!C-H5GT}J@idB8;$bQ7^yA$O2b6dxU7loxk{)TtA}vHCaxUVbVZlLR z!4a7Ab!QQdbeIjtR5+#<>=>FvQa3UP8T_V#l1XdN&2ePX;M}W!Iq(SMR1M zbM^#|0?Z$EHw8U0vAXisfHV-{T zI!{HKOupDFiC-mSmkK$CVrfr`xkxh=eZ|T6jb{n$1~{HRFjsGzCJuSR^a^LvTV zxj`1H%WW{F{DhMt;CW?ct4xD+10yX*0A6!&Y&k5L&u?opZn!K}+5Bp3y^N}vq zdESkTCfZpo(m8?n)B_a%qeO>tohLfQMCS;!azY5~;ZRBGqAM>bbda)GB6E_+>?bmn zE;5CjOrk`l6l7Ad{bpq}(nULK6fy{}Ixb3cipZP=$a%$e<(lF=%Hl>a@w|YoHY&lF z;b8t=W--FzkeJk!M^AGdTk@IU#BQYuEt>g-$2(6oP*F z&o27;oPMH2zZCR~D?uxj3X5kt4eHVLa%quV68Th#{25S9gm$GT1y*_`r*9K~qZ# zNSU0!NXig0AWNHefE%3wES7_GJ8A>H@&$p4B^8&&oY#*YT(pT!(z;mTLaA6PEGChd zx$^)t$;2SIOl;-uTnv%s^Nwy%2@h?8;_bmjZjRnr)5Pr>2k0rCsuAO@`!LL zXF>LwN99d=kc3X$S||H02G%5EPK5^#t-$rjlb8UAN=gKWm4jpD0R<~hNPz>Od3Qnjbqy%vy;9!ERk|@DJ zk+3@&bAq8G0X&?{3IKD^3SDeajsjtqSSMfy9AM|QL*jEnDt`nMzf@`#!3T|{%L5R9 z6d-AY9fD;xIk?Qh&P4p^$%??+QBSM@9*YJVTqp1%^irW}K8XyURfuEa-bTDcoyyO* zgc;$N&zX^iI8&IBLWN*f3MR#1#w8_Y2XXx}8UTbBVpArHPj&GkI18Z|$rkNWjF&30 zfId_zl8yiY21j+M9IVL5QCT>vyaudL(YaxkUXaxyTL2>f-hd5^4c1L;3Z+sL*${lu zuqLu^G*y(p2`1&Jj?_|1CDkk!szZ643;7|~AgGcugd1%IO$ClBN%>C;#{+9Tk;_5x z<{?G#Wf;M7Q2H9`RPlO>T19+LiJ+B&4;gsgCHYiAhN{|2D+E5|-b#s2KJ1k4KgRxT(g$vYvGmK;Bh0K~T+rE1_SP#`{V}tkb6btCG_%ZWG;n212q%`IoC>b9Gm7teV0Uc zKD@aAAD+#f4=Nuu7pT4jTe-p~5q!`~=7OOE+!P;_A-I!>qB@f4X$hiFxgQT2>H>RkQUIIoE!DwsQS=8d~jvx{-bN(ggW3vzPl7?z$pL% zQoWRp5d1mQFBFI(j6oe;wf)&(|q82(e2kH3Njz$L6wk30c=MD zPWhY@XiA)@5@Vo1vVjkVD6EqD^o?r{_>e`HayBR{!YdC+Nwh?HghFD2Ol5tb*z6D! zR7b)sk<#Kw)f1g(3{%mcY=RHkEb5+-R}Q2qjUg6?BA-S??tHMxWMUxkflrb65N!D* zK4c=;$}!?fbpppG`M|2r5)+hE4lw~|Vv-||7C{Vi0@~uhKv-GyS(ynTwgXJ49V2&! zsS8bN75YO>FhOXk%%c*+%RD|N#Xyr)HQ-Xkx#Wvn2trZ;B~G;%kU>@&Jz_wmsX|ag zw+0n4IEF4LCCnMn3<-9bb{0p2Q>i?@jW!6`z#Qsn!ma1alOqy!Cs9X}iOU@L1H!8c{)-yUeAKqm%BCn* zNfa?5a{%e&c^e{mMmQnUB1lr<5~!L-lL^vn3EZ-$fG`0XF$a>Qg@+(Z1#f9+B;Y}f z^ezN7YG!s|@}M&DAPh#D!*S`$D=ZvFAcm%z@6#A&8&NGUfi}4PY_D)z&PIO>kbWTq3 z4(B@IMZpI_c{z#of!?V(`Nfzq`CphfRdw({pm%xi4Qn^9-PwCF{?v072h|4tR2xuz ztcrk&fQo>MfQo>MfQo>MfQkS|Kwa;nuJ=*b`_SYwW<1sPK72k8Ym3zNK6JZSUGEb} z%TaLWgBBO?nPGmbTwU)&_oZ-~T3zoW-c)F2agn;-$9?@0Eh5-E==L5hI)+zO z*Za_2MRmQ8Y!0}yjmB18@52`rsq1}cU6H!pM_uouuJ=*b`_KbNXdBe^J|62#)LGpB z4g?J4h7DhB{A#CRG5*wZ6$jM@{!|Utk_ zy^p%yM_uou?(d`S@53JoR`>Ujp21c3_rc@bd-3qD+xi6TB_!>C;@Wekne9kup_sbf zM_unj3p%jFi@Lv0VN-kHV506Z-sQde< vYkU482#hQrx#9H2(>q5l#-Dnw;-K2VpK1et`hBIph~H9iQW5wsM&SPe6#l%E diff --git a/src/mod/plugins/plugins.go b/src/mod/plugins/plugins.go index 181d3e5..6353ad1 100644 --- a/src/mod/plugins/plugins.go +++ b/src/mod/plugins/plugins.go @@ -2,7 +2,7 @@ package plugins import ( "errors" - "fmt" + "net/http" "os" "os/exec" "path/filepath" @@ -13,23 +13,27 @@ import ( "imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/info/logger" + zoraxyPlugin "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin" "imuslab.com/zoraxy/mod/utils" ) type Plugin struct { - RootDir string //The root directory of the plugin - Spec *IntroSpect //The plugin specification - Enabled bool //Whether the plugin is enabled + RootDir string //The root directory of the plugin + Spec *zoraxyPlugin.IntroSpect //The plugin specification + Enabled bool //Whether the plugin is enabled - uiProxy *dpcore.ReverseProxy //The reverse proxy for the plugin UI - process *exec.Cmd //The process of the plugin + //Runtime + AssignedPort int //The assigned port for the plugin + uiProxy *dpcore.ReverseProxy //The reverse proxy for the plugin UI + process *exec.Cmd //The process of the plugin } type ManagerOptions struct { - PluginDir string - SystemConst *RuntimeConstantValue - Database *database.Database - Logger *logger.Logger + PluginDir string + SystemConst *zoraxyPlugin.RuntimeConstantValue + Database *database.Database + Logger *logger.Logger + CSRFTokenGen func(*http.Request) string //The CSRF token generator function } type Manager struct { @@ -80,7 +84,6 @@ func (m *Manager) LoadPluginsFromDisk() error { m.Log("Loaded plugin: "+thisPlugin.Spec.Name, nil) // If the plugin was enabled, start it now - fmt.Println(m.GetPluginPreviousEnableState(thisPlugin.Spec.ID)) if m.GetPluginPreviousEnableState(thisPlugin.Spec.ID) { err = m.StartPlugin(thisPlugin.Spec.ID) if err != nil { diff --git a/src/mod/plugins/uirouter.go b/src/mod/plugins/uirouter.go index 62414de..d2ac1c9 100644 --- a/src/mod/plugins/uirouter.go +++ b/src/mod/plugins/uirouter.go @@ -2,6 +2,9 @@ package plugins import ( "net/http" + "net/url" + "strconv" + "strings" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/utils" @@ -28,14 +31,25 @@ func (m *Manager) HandlePluginUI(pluginID string, w http.ResponseWriter, r *http return } + upstreamOrigin := "127.0.0.1:" + strconv.Itoa(plugin.AssignedPort) + matchingPath := "/plugin.ui/" + plugin.Spec.ID + + //Rewrite the request path to the plugin UI path + rewrittenURL := r.RequestURI + rewrittenURL = strings.TrimPrefix(rewrittenURL, matchingPath) + rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + r.URL, _ = url.Parse(rewrittenURL) + //Call the plugin UI handler plugin.uiProxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ UseTLS: false, OriginalHost: r.Host, - ProxyDomain: r.Host, + ProxyDomain: upstreamOrigin, NoCache: true, - PathPrefix: "/plugin.ui/" + pluginID, + PathPrefix: matchingPath, Version: m.Options.SystemConst.ZoraxyVersion, + UpstreamHeaders: [][]string{ + {"X-Zoraxy-Csrf", m.Options.CSRFTokenGen(r)}, + }, }) - } diff --git a/src/mod/plugins/utils.go b/src/mod/plugins/utils.go index aeb0e1f..a4e89a1 100644 --- a/src/mod/plugins/utils.go +++ b/src/mod/plugins/utils.go @@ -8,6 +8,7 @@ import ( "runtime" "imuslab.com/zoraxy/mod/netutils" + zoraxyPlugin "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin" ) /* @@ -61,7 +62,7 @@ func getRandomPortNumber() int { return portNo } -func validatePluginSpec(pluginSpec *IntroSpect) error { +func validatePluginSpec(pluginSpec *zoraxyPlugin.IntroSpect) error { if pluginSpec.Name == "" { return errors.New("plugin name is empty") } diff --git a/src/mod/plugins/zoraxy_plugin/README.txt b/src/mod/plugins/zoraxy_plugin/README.txt new file mode 100644 index 0000000..ed8a405 --- /dev/null +++ b/src/mod/plugins/zoraxy_plugin/README.txt @@ -0,0 +1,19 @@ +# Zoraxy Plugin + +## Overview +This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components. + +## Instructions + +1. **Copy the Module:** + - Copy the entire `zoraxy_plugin` module to your plugin mod folder. + +2. **Include the Structure:** + - Ensure that you maintain the directory structure and file organization as provided in this module. + +3. **Modify as Needed:** + - Customize the copied module to implement the desired functionality for your plugin. + +## Directory Structure + zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup + embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages \ No newline at end of file diff --git a/src/mod/plugins/zoraxy_plugin/embed_webserver.go b/src/mod/plugins/zoraxy_plugin/embed_webserver.go new file mode 100644 index 0000000..d9b3fde --- /dev/null +++ b/src/mod/plugins/zoraxy_plugin/embed_webserver.go @@ -0,0 +1,106 @@ +package zoraxy_plugin + +import ( + "embed" + "fmt" + "io/fs" + "net/http" + "net/url" + "strings" + "time" +) + +type PluginUiRouter struct { + PluginID string //The ID of the plugin + TargetFs *embed.FS //The embed.FS where the UI files are stored + TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web + HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui +} + +// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS +// The targetFsPrefix is the prefix of the embed.FS where the UI files are stored +// The targetFsPrefix should be relative to the root of the embed.FS +// The targetFsPrefix should start with a slash (e.g. /web) that corresponds to the root folder of the embed.FS +// The handlerPrefix is the prefix of the handler used to route this router +// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path +// All prefix should not end with a slash +func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter { + //Make sure all prefix are in /prefix format + if !strings.HasPrefix(targetFsPrefix, "/") { + targetFsPrefix = "/" + targetFsPrefix + } + targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/") + + if !strings.HasPrefix(handlerPrefix, "/") { + handlerPrefix = "/" + handlerPrefix + } + handlerPrefix = strings.TrimSuffix(handlerPrefix, "/") + + //Return the PluginUiRouter + return &PluginUiRouter{ + PluginID: pluginID, + TargetFs: targetFs, + TargetFsPrefix: targetFsPrefix, + HandlerPrefix: handlerPrefix, + } +} + +func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler { + //Get the CSRF token from header + csrfToken := r.Header.Get("X-Zoraxy-Csrf") + if csrfToken == "" { + csrfToken = "missing-csrf-token" + } + + //Return the middleware + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if the request is for an HTML file + if strings.HasSuffix(r.URL.Path, "/") { + // Redirect to the index.html + http.Redirect(w, r, r.URL.Path+"index.html", http.StatusFound) + return + } + if strings.HasSuffix(r.URL.Path, ".html") { + //Read the target file from embed.FS + targetFilePath := strings.TrimPrefix(r.URL.Path, "/") + targetFilePath = p.TargetFsPrefix + "/" + targetFilePath + targetFilePath = strings.TrimPrefix(targetFilePath, "/") + targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath) + if err != nil { + http.Error(w, "File not found", http.StatusNotFound) + return + } + body := string(targetFileContent) + body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken) + http.ServeContent(w, r, r.URL.Path, time.Now(), strings.NewReader(body)) + return + } + + //Call the next handler + fsHandler.ServeHTTP(w, r) + }) + +} + +// GetHttpHandler returns the http.Handler for the PluginUiRouter +func (p *PluginUiRouter) Handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + //Remove the plugin UI handler path prefix + rewrittenURL := r.RequestURI + rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix) + rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + r.URL, _ = url.Parse(rewrittenURL) + r.RequestURI = rewrittenURL + + //Serve the file from the embed.FS + subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/")) + if err != nil { + fmt.Println(err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Replace {{csrf_token}} with the actual CSRF token and serve the file + p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r) + }) +} diff --git a/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go b/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go new file mode 100644 index 0000000..1691591 --- /dev/null +++ b/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go @@ -0,0 +1,210 @@ +package zoraxy_plugin + +import ( + "encoding/json" + "fmt" + "os" + "os/signal" + "strings" + "syscall" +) + +/* + Plugins Includes.go + + This file is copied from Zoraxy source code + You can always find the latest version under mod/plugins/includes.go + Usually this file are backward compatible +*/ + +type PluginType int + +const ( + PluginType_Router PluginType = 0 //Router Plugin, used for handling / routing / forwarding traffic + PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore +) + +type CaptureRule struct { + CapturePath string `json:"capture_path"` + IncludeSubPaths bool `json:"include_sub_paths"` +} + +type ControlStatusCode int + +const ( + ControlStatusCode_CAPTURED ControlStatusCode = 280 //Traffic captured by plugin, ask Zoraxy not to process the traffic + ControlStatusCode_UNHANDLED ControlStatusCode = 284 //Traffic not handled by plugin, ask Zoraxy to process the traffic + ControlStatusCode_ERROR ControlStatusCode = 580 //Error occurred while processing the traffic, ask Zoraxy to process the traffic and log the error +) + +type SubscriptionEvent struct { + EventName string `json:"event_name"` + EventSource string `json:"event_source"` + Payload string `json:"payload"` //Payload of the event, can be empty +} + +type RuntimeConstantValue struct { + ZoraxyVersion string `json:"zoraxy_version"` + ZoraxyUUID string `json:"zoraxy_uuid"` +} + +/* +IntroSpect Payload + +When the plugin is initialized with -introspect flag, +the plugin shell return this payload as JSON and exit +*/ +type IntroSpect struct { + /* Plugin metadata */ + ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname + Name string `json:"name"` //Name of your plugin + Author string `json:"author"` //Author name of your plugin + AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email + Description string `json:"description"` //Description of your plugin + URL string `json:"url"` //URL of your plugin + Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1) + VersionMajor int `json:"version_major"` //Major version of your plugin + VersionMinor int `json:"version_minor"` //Minor version of your plugin + VersionPatch int `json:"version_patch"` //Patch version of your plugin + + /* + + Endpoint Settings + + */ + + /* + Global Capture Settings + + Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on + This captures the whole traffic of Zoraxy + + Notes: Will raise a warning on the UI when the user enables the plugin on a HTTP Proxy rule + */ + GlobalCapturePath []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin + GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler) + + /* + Always Capture Settings + + Once the plugin is enabled on a given HTTP Proxy rule, + these always applies + */ + AlwaysCapturePath []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp) + AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler) + + /* + Dynamic Capture Settings + + Once the plugin is enabled on a given HTTP Proxy rule, + the plugin can capture the request and decided if the request + shall be handled by itself or let it pass through + + */ + DynmaicCaptureIngress string `json:"capture_path"` //Traffic capture path of your plugin (e.g. /capture) + DynamicHandleIngress string `json:"handle_path"` //Traffic handle path of your plugin (e.g. /handler) + + /* UI Path for your plugin */ + UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI + + /* Subscriptions Settings */ + SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered + SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details +} + +/* +ServeIntroSpect Function + +This function will check if the plugin is initialized with -introspect flag, +if so, it will print the intro spect and exit + +Place this function at the beginning of your plugin main function +*/ +func ServeIntroSpect(pluginSpect *IntroSpect) { + if len(os.Args) > 1 && os.Args[1] == "-introspect" { + //Print the intro spect and exit + jsonData, _ := json.MarshalIndent(pluginSpect, "", " ") + fmt.Println(string(jsonData)) + os.Exit(0) + } +} + +/* +ConfigureSpec Payload + +Zoraxy will start your plugin with -configure flag, +the plugin shell read this payload as JSON and configure itself +by the supplied values like starting a web server at given port +that listens to 127.0.0.1:port +*/ +type ConfigureSpec struct { + Port int `json:"port"` //Port to listen + RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values + //To be expanded +} + +/* +RecvExecuteConfigureSpec Function + +This function will read the configure spec from Zoraxy +and return the ConfigureSpec object + +Place this function after ServeIntroSpect function in your plugin main function +*/ +func RecvConfigureSpec() (*ConfigureSpec, error) { + for i, arg := range os.Args { + if strings.HasPrefix(arg, "-configure=") { + var configSpec ConfigureSpec + if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil { + return nil, err + } + return &configSpec, nil + } else if arg == "-configure" { + var configSpec ConfigureSpec + var nextArg string + if len(os.Args) > i+1 { + nextArg = os.Args[i+1] + if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("No port specified after -configure flag") + } + return &configSpec, nil + } + } + return nil, fmt.Errorf("No -configure flag found") +} + +/* +ServeAndRecvSpec Function + +This function will serve the intro spect and return the configure spec +See the ServeIntroSpect and RecvConfigureSpec for more details +*/ +func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) { + ServeIntroSpect(pluginSpect) + return RecvConfigureSpec() +} + +/* + +Shutdown handler + +This function will register a shutdown handler for the plugin +The shutdown callback will be called when the plugin is shutting down +You can use this to clean up resources like closing database connections +*/ + +func RegisterShutdownHandler(shutdownCallback func()) { + // Set up a channel to receive OS signals + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + // Start a goroutine to listen for signals + go func() { + <-sigChan + shutdownCallback() + os.Exit(0) + }() +} diff --git a/src/start.go b/src/start.go index f9d757d..6f20c93 100644 --- a/src/start.go +++ b/src/start.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/gorilla/csrf" "imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/auth" @@ -27,6 +28,7 @@ import ( "imuslab.com/zoraxy/mod/netstat" "imuslab.com/zoraxy/mod/pathrule" "imuslab.com/zoraxy/mod/plugins" + "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin" "imuslab.com/zoraxy/mod/sshprox" "imuslab.com/zoraxy/mod/statistic" "imuslab.com/zoraxy/mod/statistic/analytic" @@ -324,12 +326,15 @@ func startupSequence() { pluginManager = plugins.NewPluginManager(&plugins.ManagerOptions{ PluginDir: "./plugins", - SystemConst: &plugins.RuntimeConstantValue{ + SystemConst: &zoraxy_plugin.RuntimeConstantValue{ ZoraxyVersion: SYSTEM_VERSION, ZoraxyUUID: nodeUUID, }, Database: sysdb, Logger: SystemWideLogger, + CSRFTokenGen: func(r *http.Request) string { + return csrf.Token(r) + }, }) err = pluginManager.LoadPluginsFromDisk() diff --git a/src/web/components/plugins.html b/src/web/components/plugins.html index 7d0e780..a509beb 100644 --- a/src/web/components/plugins.html +++ b/src/web/components/plugins.html @@ -3,7 +3,7 @@

Plugins

Custom features on Zoraxy

- +
@@ -35,9 +35,9 @@ function initiatePluginList(){ + @@ -1489,15 +1490,30 @@ //Load the summary to ip access table function initBlacklistQuickBanTable(){ - $.get("/api/stats/summary", function(data){ - initIpAccessTable(data.RequestClientIp); + $.get("/api/quickban/list", function(data){ + //Convert the data to a dictionary + var ipAccessCounts = {}; + access_ip_country_map = {}; + data.forEach(function(entry){ + ipAccessCounts[entry.IpAddr] = entry.Count + access_ip_country_map[entry.IpAddr] = entry.CountryCode; + }); + initIpAccessTable(ipAccessCounts); }) } initBlacklistQuickBanTable(); + function getCountryISOFromQuickBan(ip){ + if (access_ip_country_map[ip] === "") { + return "LAN / Reserved"; + } + return access_ip_country_map[ip]; + } + var blacklist_entriesPerPage = 30; var blacklist_currentPage = 1; var blacklist_totalPages = 0; + var access_ip_country_map = {}; function initIpAccessTable(ipAccessCounts){ blacklist_totalPages = Math.ceil(Object.keys(ipAccessCounts).length / blacklist_entriesPerPage); @@ -1533,6 +1549,7 @@ var row = $("").appendTo(tableBody); $("").appendTo(tableBody); - $("
Plugin Name

- +
- ${plugin.Spec.name} + ${plugin.Spec.name}
${versionString} by ${plugin.Spec.author}

@@ -60,7 +60,7 @@ function initiatePluginList(){ } function openPluginUI(pluginid){ - showSideWrapper(`/plugin.ui/${pluginid}/`); + showSideWrapper(`/plugin.ui/${pluginid}/`, true); } initiatePluginList(); diff --git a/src/web/index.html b/src/web/index.html index c2aaca3..3975c15 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -414,7 +414,7 @@ Toggles for side wrapper */ - function showSideWrapper(scriptpath=""){ + function showSideWrapper(scriptpath="", extendedMode=false){ if (scriptpath != ""){ $(".sideWrapper iframe").attr("src", scriptpath); } @@ -422,6 +422,12 @@ if ($(".sideWrapper .content").transition("is animating") || $(".sideWrapper .content").transition("is visible")){ return } + + if (extendedMode){ + $(".sideWrapper").addClass("extendedMode"); + }else{ + $(".sideWrapper").removeClass("extendedMode"); + } $(".sideWrapper").show(); $(".sideWrapper .fadingBackground").fadeIn("fast"); $(".sideWrapper .content").transition('slide left in', 300); diff --git a/src/web/main.css b/src/web/main.css index 3989f5b..b69c0f6 100644 --- a/src/web/main.css +++ b/src/web/main.css @@ -188,6 +188,10 @@ body{ z-index: 10; } +.sideWrapper.extendedMode{ + max-width: calc(80% - 1em); +} + .sideWrapper .content{ height: 100%; width: 100%; From 3993ac954c01096e4e2d2a3cc49ba31020f8d8d9 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Fri, 28 Feb 2025 22:01:11 +0800 Subject: [PATCH 10/14] Fixed #247 - Added country of origin row for quickban list --- src/accesslist.go | 37 ++++++++++++++++++++++++++++++++++ src/api.go | 3 +++ src/web/components/access.html | 23 ++++++++++++++++++--- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/accesslist.go b/src/accesslist.go index 2df35d7..6cdb34e 100644 --- a/src/accesslist.go +++ b/src/accesslist.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "net/http" + "sort" "strings" "github.com/google/uuid" @@ -545,3 +546,39 @@ func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) { utils.SendOK(w) } } + +// List all quick ban ip address +func handleListQuickBan(w http.ResponseWriter, r *http.Request) { + currentSummary := statisticCollector.GetCurrentDailySummary() + type quickBanEntry struct { + IpAddr string + Count int + CountryCode string + } + result := []quickBanEntry{} + currentSummary.RequestClientIp.Range(func(key, value interface{}) bool { + ip := key.(string) + count := value.(int) + thisEntry := quickBanEntry{ + IpAddr: ip, + Count: count, + } + + //Get the country code + geoinfo, err := geodbStore.ResolveCountryCodeFromIP(ip) + if err == nil { + thisEntry.CountryCode = geoinfo.CountryIsoCode + } + + result = append(result, thisEntry) + return true + }) + + //Sort result based on count + sort.Slice(result, func(i, j int) bool { + return result[i].Count > result[j].Count + }) + + js, _ := json.Marshal(result) + utils.SendJSONResponse(w, string(js)) +} diff --git a/src/api.go b/src/api.go index 7afa247..112d5b0 100644 --- a/src/api.go +++ b/src/api.go @@ -114,6 +114,9 @@ func RegisterAccessRuleAPIs(authRouter *auth.RouterDef) { authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd) authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove) authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable) + + /* Quick Ban List */ + authRouter.HandleFunc("/api/quickban/list", handleListQuickBan) } // Register the APIs for path blocking rules management functions, WIP diff --git a/src/web/components/access.html b/src/web/components/access.html index 6a59453..4ed89fa 100644 --- a/src/web/components/access.html +++ b/src/web/components/access.html @@ -694,6 +694,7 @@
IP Access CountCountry of Origin Blacklist
").text(ip).appendTo(row); $("").text(accessCount).appendTo(row); + $("").text(getCountryISOFromQuickBan(ip)).appendTo(row); if (ipInBlacklist(ip)){ $("").html(``).appendTo(row); }else{ @@ -1542,7 +1559,7 @@ if (slicedEntries.length == 0){ var row = $("
").html(` + $("").html(` There are no HTTP requests recorded today `).appendTo(row); From 214b69b0b8ec53c5d218459ee0498887bb2471f2 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Fri, 28 Feb 2025 22:03:08 +0800 Subject: [PATCH 11/14] Updated example plugins - Updated example plugins - Added debugger - Removed some trash files --- .gitignore | 1 + example/plugins/build_all.sh | 22 ++ example/plugins/debugger/go.mod | 3 + example/plugins/debugger/main.go | 70 +++++++ .../debugger/mod/zoraxy_plugin/README.txt | 19 ++ .../mod/zoraxy_plugin/embed_webserver.go | 106 ++++++++++ .../mod/zoraxy_plugin/zoraxy_plugin.go | 198 ++++++++++++++++++ example/plugins/debugger/ui_info.go | 26 +++ example/plugins/helloworld/www/index.html | 1 + .../zoraxy_plugin/embed_webserver.go | 1 - example/plugins/ztnc/authtoken.secret | 1 - .../ztnc/mod/database/database_openwrt.go | 2 +- .../plugins/ztnc/mod/ganserv/authkeyLinux.go | 2 +- example/plugins/ztnc/web/index.html | 5 + example/plugins/ztnc/ztnc.db | Bin 32768 -> 0 bytes example/plugins/ztnc/ztnc.db.lock | 0 16 files changed, 453 insertions(+), 4 deletions(-) create mode 100644 example/plugins/build_all.sh create mode 100644 example/plugins/debugger/go.mod create mode 100644 example/plugins/debugger/main.go create mode 100644 example/plugins/debugger/mod/zoraxy_plugin/README.txt create mode 100644 example/plugins/debugger/mod/zoraxy_plugin/embed_webserver.go create mode 100644 example/plugins/debugger/mod/zoraxy_plugin/zoraxy_plugin.go create mode 100644 example/plugins/debugger/ui_info.go delete mode 100644 example/plugins/ztnc/authtoken.secret delete mode 100644 example/plugins/ztnc/ztnc.db delete mode 100644 example/plugins/ztnc/ztnc.db.lock diff --git a/.gitignore b/.gitignore index 8eea333..2c708ee 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ src/log/ /Dockerfile /Entrypoint.sh example/plugins/zerotiernc/authtoken.secret +example/plugins/ztnc/ztnc.db diff --git a/example/plugins/build_all.sh b/example/plugins/build_all.sh new file mode 100644 index 0000000..76d3792 --- /dev/null +++ b/example/plugins/build_all.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Iterate over all directories in the current directory +for dir in */; do + if [ -d "$dir" ]; then + echo "Processing directory: $dir" + cd "$dir" + + # Execute go mod tidy + echo "Running go mod tidy in $dir" + go mod tidy + + # Execute go build + echo "Running go build in $dir" + go build + + # Return to the parent directory + cd .. + fi +done + +echo "Build process completed for all directories." \ No newline at end of file diff --git a/example/plugins/debugger/go.mod b/example/plugins/debugger/go.mod new file mode 100644 index 0000000..ec640ed --- /dev/null +++ b/example/plugins/debugger/go.mod @@ -0,0 +1,3 @@ +module aroz.org/zoraxy/debugger + +go 1.23.6 diff --git a/example/plugins/debugger/main.go b/example/plugins/debugger/main.go new file mode 100644 index 0000000..4e1b15d --- /dev/null +++ b/example/plugins/debugger/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + "net/http" + "strconv" + + plugin "aroz.org/zoraxy/debugger/mod/zoraxy_plugin" +) + +const ( + PLUGIN_ID = "org.aroz.zoraxy.debugger" + UI_PATH = "/debug" +) + +func main() { + // Serve the plugin intro spect + // This will print the plugin intro spect and exit if the -introspect flag is provided + runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{ + ID: "org.aroz.zoraxy.debugger", + Name: "Plugin Debugger", + Author: "aroz.org", + AuthorContact: "https://aroz.org", + Description: "A debugger for Zoraxy <-> plugin communication pipeline", + URL: "https://zoraxy.aroz.org", + Type: plugin.PluginType_Router, + VersionMajor: 1, + VersionMinor: 0, + VersionPatch: 0, + + GlobalCapturePaths: []plugin.CaptureRule{ + { + CapturePath: "/debug_test", //Capture all traffic of all HTTP proxy rule + IncludeSubPaths: true, + }, + }, + GlobalCaptureIngress: "", + AlwaysCapturePaths: []plugin.CaptureRule{}, + AlwaysCaptureIngress: "", + + UIPath: UI_PATH, + + /* + SubscriptionPath: "/subept", + SubscriptionsEvents: []plugin.SubscriptionEvent{ + */ + }) + if err != nil { + //Terminate or enter standalone mode here + panic(err) + } + + // Register the shutdown handler + plugin.RegisterShutdownHandler(func() { + // Do cleanup here if needed + fmt.Println("Debugger Terminated") + }) + + http.HandleFunc(UI_PATH+"/", RenderDebugUI) + http.HandleFunc("/gcapture", HandleIngressCapture) + fmt.Println("Debugger started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port)) + http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil) +} + +// Handle the captured request +func HandleIngressCapture(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "Capture request received") + w.Header().Set("Content-Type", "text/html") + w.Write([]byte("This request is captured by the debugger")) +} diff --git a/example/plugins/debugger/mod/zoraxy_plugin/README.txt b/example/plugins/debugger/mod/zoraxy_plugin/README.txt new file mode 100644 index 0000000..ed8a405 --- /dev/null +++ b/example/plugins/debugger/mod/zoraxy_plugin/README.txt @@ -0,0 +1,19 @@ +# Zoraxy Plugin + +## Overview +This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components. + +## Instructions + +1. **Copy the Module:** + - Copy the entire `zoraxy_plugin` module to your plugin mod folder. + +2. **Include the Structure:** + - Ensure that you maintain the directory structure and file organization as provided in this module. + +3. **Modify as Needed:** + - Customize the copied module to implement the desired functionality for your plugin. + +## Directory Structure + zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup + embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages \ No newline at end of file diff --git a/example/plugins/debugger/mod/zoraxy_plugin/embed_webserver.go b/example/plugins/debugger/mod/zoraxy_plugin/embed_webserver.go new file mode 100644 index 0000000..d9b3fde --- /dev/null +++ b/example/plugins/debugger/mod/zoraxy_plugin/embed_webserver.go @@ -0,0 +1,106 @@ +package zoraxy_plugin + +import ( + "embed" + "fmt" + "io/fs" + "net/http" + "net/url" + "strings" + "time" +) + +type PluginUiRouter struct { + PluginID string //The ID of the plugin + TargetFs *embed.FS //The embed.FS where the UI files are stored + TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web + HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui +} + +// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS +// The targetFsPrefix is the prefix of the embed.FS where the UI files are stored +// The targetFsPrefix should be relative to the root of the embed.FS +// The targetFsPrefix should start with a slash (e.g. /web) that corresponds to the root folder of the embed.FS +// The handlerPrefix is the prefix of the handler used to route this router +// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path +// All prefix should not end with a slash +func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter { + //Make sure all prefix are in /prefix format + if !strings.HasPrefix(targetFsPrefix, "/") { + targetFsPrefix = "/" + targetFsPrefix + } + targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/") + + if !strings.HasPrefix(handlerPrefix, "/") { + handlerPrefix = "/" + handlerPrefix + } + handlerPrefix = strings.TrimSuffix(handlerPrefix, "/") + + //Return the PluginUiRouter + return &PluginUiRouter{ + PluginID: pluginID, + TargetFs: targetFs, + TargetFsPrefix: targetFsPrefix, + HandlerPrefix: handlerPrefix, + } +} + +func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler { + //Get the CSRF token from header + csrfToken := r.Header.Get("X-Zoraxy-Csrf") + if csrfToken == "" { + csrfToken = "missing-csrf-token" + } + + //Return the middleware + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if the request is for an HTML file + if strings.HasSuffix(r.URL.Path, "/") { + // Redirect to the index.html + http.Redirect(w, r, r.URL.Path+"index.html", http.StatusFound) + return + } + if strings.HasSuffix(r.URL.Path, ".html") { + //Read the target file from embed.FS + targetFilePath := strings.TrimPrefix(r.URL.Path, "/") + targetFilePath = p.TargetFsPrefix + "/" + targetFilePath + targetFilePath = strings.TrimPrefix(targetFilePath, "/") + targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath) + if err != nil { + http.Error(w, "File not found", http.StatusNotFound) + return + } + body := string(targetFileContent) + body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken) + http.ServeContent(w, r, r.URL.Path, time.Now(), strings.NewReader(body)) + return + } + + //Call the next handler + fsHandler.ServeHTTP(w, r) + }) + +} + +// GetHttpHandler returns the http.Handler for the PluginUiRouter +func (p *PluginUiRouter) Handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + //Remove the plugin UI handler path prefix + rewrittenURL := r.RequestURI + rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix) + rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + r.URL, _ = url.Parse(rewrittenURL) + r.RequestURI = rewrittenURL + + //Serve the file from the embed.FS + subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/")) + if err != nil { + fmt.Println(err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Replace {{csrf_token}} with the actual CSRF token and serve the file + p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r) + }) +} diff --git a/example/plugins/debugger/mod/zoraxy_plugin/zoraxy_plugin.go b/example/plugins/debugger/mod/zoraxy_plugin/zoraxy_plugin.go new file mode 100644 index 0000000..f3865ea --- /dev/null +++ b/example/plugins/debugger/mod/zoraxy_plugin/zoraxy_plugin.go @@ -0,0 +1,198 @@ +package zoraxy_plugin + +import ( + "encoding/json" + "fmt" + "os" + "os/signal" + "strings" + "syscall" +) + +/* + Plugins Includes.go + + This file is copied from Zoraxy source code + You can always find the latest version under mod/plugins/includes.go + Usually this file are backward compatible +*/ + +type PluginType int + +const ( + PluginType_Router PluginType = 0 //Router Plugin, used for handling / routing / forwarding traffic + PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore +) + +type CaptureRule struct { + CapturePath string `json:"capture_path"` + IncludeSubPaths bool `json:"include_sub_paths"` +} + +type ControlStatusCode int + +const ( + ControlStatusCode_CAPTURED ControlStatusCode = 280 //Traffic captured by plugin, ask Zoraxy not to process the traffic + ControlStatusCode_UNHANDLED ControlStatusCode = 284 //Traffic not handled by plugin, ask Zoraxy to process the traffic + ControlStatusCode_ERROR ControlStatusCode = 580 //Error occurred while processing the traffic, ask Zoraxy to process the traffic and log the error +) + +type SubscriptionEvent struct { + EventName string `json:"event_name"` + EventSource string `json:"event_source"` + Payload string `json:"payload"` //Payload of the event, can be empty +} + +type RuntimeConstantValue struct { + ZoraxyVersion string `json:"zoraxy_version"` + ZoraxyUUID string `json:"zoraxy_uuid"` +} + +/* +IntroSpect Payload + +When the plugin is initialized with -introspect flag, +the plugin shell return this payload as JSON and exit +*/ +type IntroSpect struct { + /* Plugin metadata */ + ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname + Name string `json:"name"` //Name of your plugin + Author string `json:"author"` //Author name of your plugin + AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email + Description string `json:"description"` //Description of your plugin + URL string `json:"url"` //URL of your plugin + Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1) + VersionMajor int `json:"version_major"` //Major version of your plugin + VersionMinor int `json:"version_minor"` //Minor version of your plugin + VersionPatch int `json:"version_patch"` //Patch version of your plugin + + /* + + Endpoint Settings + + */ + + /* + Global Capture Settings + + Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on + This captures the whole traffic of Zoraxy + + */ + GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin + GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler) + + /* + Always Capture Settings + + Once the plugin is enabled on a given HTTP Proxy rule, + these always applies + */ + AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp) + AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler) + + /* UI Path for your plugin */ + UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI + + /* Subscriptions Settings */ + SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered + SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details +} + +/* +ServeIntroSpect Function + +This function will check if the plugin is initialized with -introspect flag, +if so, it will print the intro spect and exit + +Place this function at the beginning of your plugin main function +*/ +func ServeIntroSpect(pluginSpect *IntroSpect) { + if len(os.Args) > 1 && os.Args[1] == "-introspect" { + //Print the intro spect and exit + jsonData, _ := json.MarshalIndent(pluginSpect, "", " ") + fmt.Println(string(jsonData)) + os.Exit(0) + } +} + +/* +ConfigureSpec Payload + +Zoraxy will start your plugin with -configure flag, +the plugin shell read this payload as JSON and configure itself +by the supplied values like starting a web server at given port +that listens to 127.0.0.1:port +*/ +type ConfigureSpec struct { + Port int `json:"port"` //Port to listen + RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values + //To be expanded +} + +/* +RecvExecuteConfigureSpec Function + +This function will read the configure spec from Zoraxy +and return the ConfigureSpec object + +Place this function after ServeIntroSpect function in your plugin main function +*/ +func RecvConfigureSpec() (*ConfigureSpec, error) { + for i, arg := range os.Args { + if strings.HasPrefix(arg, "-configure=") { + var configSpec ConfigureSpec + if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil { + return nil, err + } + return &configSpec, nil + } else if arg == "-configure" { + var configSpec ConfigureSpec + var nextArg string + if len(os.Args) > i+1 { + nextArg = os.Args[i+1] + if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("No port specified after -configure flag") + } + return &configSpec, nil + } + } + return nil, fmt.Errorf("No -configure flag found") +} + +/* +ServeAndRecvSpec Function + +This function will serve the intro spect and return the configure spec +See the ServeIntroSpect and RecvConfigureSpec for more details +*/ +func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) { + ServeIntroSpect(pluginSpect) + return RecvConfigureSpec() +} + +/* + +Shutdown handler + +This function will register a shutdown handler for the plugin +The shutdown callback will be called when the plugin is shutting down +You can use this to clean up resources like closing database connections +*/ + +func RegisterShutdownHandler(shutdownCallback func()) { + // Set up a channel to receive OS signals + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + // Start a goroutine to listen for signals + go func() { + <-sigChan + shutdownCallback() + os.Exit(0) + }() +} diff --git a/example/plugins/debugger/ui_info.go b/example/plugins/debugger/ui_info.go new file mode 100644 index 0000000..6c53aeb --- /dev/null +++ b/example/plugins/debugger/ui_info.go @@ -0,0 +1,26 @@ +package main + +import ( + _ "embed" + "fmt" + "net/http" + "sort" +) + +// Render the debug UI +func RenderDebugUI(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "**Plugin UI Debug Interface**\n\n[Recv Headers] \n") + + headerKeys := make([]string, 0, len(r.Header)) + for name := range r.Header { + headerKeys = append(headerKeys, name) + } + sort.Strings(headerKeys) + for _, name := range headerKeys { + values := r.Header[name] + for _, value := range values { + fmt.Fprintf(w, "%s: %s\n", name, value) + } + } + w.Header().Set("Content-Type", "text/html") +} diff --git a/example/plugins/helloworld/www/index.html b/example/plugins/helloworld/www/index.html index 2dcf1f1..bc48067 100644 --- a/example/plugins/helloworld/www/index.html +++ b/example/plugins/helloworld/www/index.html @@ -19,6 +19,7 @@ height: 100vh; margin: 0; font-family: Arial, sans-serif; + background:none; } diff --git a/example/plugins/helloworld/zoraxy_plugin/embed_webserver.go b/example/plugins/helloworld/zoraxy_plugin/embed_webserver.go index 35580dd..2a264a2 100644 --- a/example/plugins/helloworld/zoraxy_plugin/embed_webserver.go +++ b/example/plugins/helloworld/zoraxy_plugin/embed_webserver.go @@ -65,7 +65,6 @@ func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handl targetFilePath := strings.TrimPrefix(r.URL.Path, "/") targetFilePath = p.TargetFsPrefix + "/" + targetFilePath targetFilePath = strings.TrimPrefix(targetFilePath, "/") - fmt.Println(targetFilePath) targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath) if err != nil { http.Error(w, "File not found", http.StatusNotFound) diff --git a/example/plugins/ztnc/authtoken.secret b/example/plugins/ztnc/authtoken.secret deleted file mode 100644 index fa08db2..0000000 --- a/example/plugins/ztnc/authtoken.secret +++ /dev/null @@ -1 +0,0 @@ -hgaode9ptnpuaoi1ilbdw9i4 \ No newline at end of file diff --git a/example/plugins/ztnc/mod/database/database_openwrt.go b/example/plugins/ztnc/mod/database/database_openwrt.go index e128a3a..fd3d8b2 100644 --- a/example/plugins/ztnc/mod/database/database_openwrt.go +++ b/example/plugins/ztnc/mod/database/database_openwrt.go @@ -11,7 +11,7 @@ import ( "path/filepath" "strings" - "aroz.org/zoraxy/zerotiernc/mod/database/dbinc" + "aroz.org/zoraxy/ztnc/mod/database/dbinc" ) /* diff --git a/example/plugins/ztnc/mod/ganserv/authkeyLinux.go b/example/plugins/ztnc/mod/ganserv/authkeyLinux.go index 8423c56..91ce202 100644 --- a/example/plugins/ztnc/mod/ganserv/authkeyLinux.go +++ b/example/plugins/ztnc/mod/ganserv/authkeyLinux.go @@ -9,7 +9,7 @@ import ( "os/user" "strings" - "aroz.org/zoraxy/zerotiernc/mod/utils" + "aroz.org/zoraxy/ztnc/mod/utils" ) func readAuthTokenAsAdmin() (string, error) { diff --git a/example/plugins/ztnc/web/index.html b/example/plugins/ztnc/web/index.html index 97108bd..34d2974 100644 --- a/example/plugins/ztnc/web/index.html +++ b/example/plugins/ztnc/web/index.html @@ -15,6 +15,11 @@ + diff --git a/example/plugins/ztnc/ztnc.db b/example/plugins/ztnc/ztnc.db deleted file mode 100644 index 70a17b524a4707a4c9821322a3697e7882bd5eb6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI&zfJ-%8~|X%e~CH}pMc!vrg6Zai;07i58#GS2vH9LCD(df)bITd5Rfqih$idy8t`uN4F9<#=r3ysdKJq-^)EhL<1D2TyUH zmH+_)1PBlyK!5-N0t5&USRH}2aw##d|1JOj+8fAXV*cMK<4@ma-S?fB>(%))D*hwYNfad zkU#UQ|JzxeX0_K!$6@rC^?ocdEPt7Y?Pm2Xt7(`_2cz5jFlo(_(CBoI+M#jN4Tqs~ zbDT71$we5qlV);WpM>$GpZ4lyM7=v0)rWTvy?&a^=h>GO0t5&UAV7cs0RjXF5FkKc zX@Pj&AJ6~eeE{eGOVj!Q0RjXF5FkK+009C72oP8qftcri&;R4Tz{)JtLI@BbK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7e?KNk1|A8ace diff --git a/example/plugins/ztnc/ztnc.db.lock b/example/plugins/ztnc/ztnc.db.lock deleted file mode 100644 index e69de29..0000000 From 5abc4ac60608a7b33fb2e5b31da54dfd881b880d Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Fri, 28 Feb 2025 22:05:14 +0800 Subject: [PATCH 12/14] Added plugin context view - Added plugin context view - Moved plugin type definition to separate file - Added wip request forwarder --- src/mod/plugins/forwarder.go | 26 +++++++ src/mod/plugins/handler.go | 6 ++ src/mod/plugins/plugins.go | 44 +++--------- src/mod/plugins/typdef.go | 40 +++++++++++ .../plugins/zoraxy_plugin/zoraxy_plugin.go | 16 +---- src/web/components/gan.html | 4 ++ src/web/components/plugincontext.html | 52 ++++++++++++++ src/web/components/plugins.html | 34 +++++++-- src/web/index.html | 71 ++++++++++++++++--- 9 files changed, 229 insertions(+), 64 deletions(-) create mode 100644 src/mod/plugins/forwarder.go create mode 100644 src/mod/plugins/typdef.go create mode 100644 src/web/components/plugincontext.html diff --git a/src/mod/plugins/forwarder.go b/src/mod/plugins/forwarder.go new file mode 100644 index 0000000..5089b27 --- /dev/null +++ b/src/mod/plugins/forwarder.go @@ -0,0 +1,26 @@ +package plugins + +import "net/http" + +/* + Forwarder.go + + This file handles the dynamic proxy routing forwarding + request to plugin capture path that handles the matching + request path registered when the plugin started +*/ + +func (m *Manager) GetHandlerPlugins(w http.ResponseWriter, r *http.Request) { + +} + +func (m *Manager) GetHandlerPluginsSubsets(w http.ResponseWriter, r *http.Request) { + +} + +func (p *Plugin) HandlePluginRoute(w http.ResponseWriter, r *http.Request) { + //Find the plugin that matches the request path + //If no plugin found, return 404 + //If found, forward the request to the plugin + +} diff --git a/src/mod/plugins/handler.go b/src/mod/plugins/handler.go index 0795313..fb1a651 100644 --- a/src/mod/plugins/handler.go +++ b/src/mod/plugins/handler.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net/http" "path/filepath" + "sort" "time" "imuslab.com/zoraxy/mod/utils" @@ -18,6 +19,11 @@ func (m *Manager) HandleListPlugins(w http.ResponseWriter, r *http.Request) { return } + //Sort the plugin by its name + sort.Slice(plugins, func(i, j int) bool { + return plugins[i].Spec.Name < plugins[j].Spec.Name + }) + js, err := json.Marshal(plugins) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/src/mod/plugins/plugins.go b/src/mod/plugins/plugins.go index 6353ad1..cc51cd0 100644 --- a/src/mod/plugins/plugins.go +++ b/src/mod/plugins/plugins.go @@ -1,49 +1,23 @@ package plugins +/* + Zoraxy Plugin Manager + + This module is responsible for managing plugins + loading plugins from the disk + enable / disable plugins + and forwarding traffic to plugins +*/ + import ( "errors" - "net/http" "os" - "os/exec" "path/filepath" "sync" - _ "embed" - - "imuslab.com/zoraxy/mod/database" - "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" - "imuslab.com/zoraxy/mod/info/logger" - zoraxyPlugin "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin" "imuslab.com/zoraxy/mod/utils" ) -type Plugin struct { - RootDir string //The root directory of the plugin - Spec *zoraxyPlugin.IntroSpect //The plugin specification - Enabled bool //Whether the plugin is enabled - - //Runtime - AssignedPort int //The assigned port for the plugin - uiProxy *dpcore.ReverseProxy //The reverse proxy for the plugin UI - process *exec.Cmd //The process of the plugin -} - -type ManagerOptions struct { - PluginDir string - SystemConst *zoraxyPlugin.RuntimeConstantValue - Database *database.Database - Logger *logger.Logger - CSRFTokenGen func(*http.Request) string //The CSRF token generator function -} - -type Manager struct { - LoadedPlugins sync.Map //Storing *Plugin - Options *ManagerOptions -} - -//go:embed no_img.png -var noImg []byte - // NewPluginManager creates a new plugin manager func NewPluginManager(options *ManagerOptions) *Manager { //Create plugin directory if not exists diff --git a/src/mod/plugins/typdef.go b/src/mod/plugins/typdef.go new file mode 100644 index 0000000..240742b --- /dev/null +++ b/src/mod/plugins/typdef.go @@ -0,0 +1,40 @@ +package plugins + +import ( + _ "embed" + "net/http" + "os/exec" + "sync" + + "imuslab.com/zoraxy/mod/database" + "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" + "imuslab.com/zoraxy/mod/info/logger" + zoraxyPlugin "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin" +) + +//go:embed no_img.png +var noImg []byte + +type Plugin struct { + RootDir string //The root directory of the plugin + Spec *zoraxyPlugin.IntroSpect //The plugin specification + Enabled bool //Whether the plugin is enabled + + //Runtime + AssignedPort int //The assigned port for the plugin + uiProxy *dpcore.ReverseProxy //The reverse proxy for the plugin UI + process *exec.Cmd //The process of the plugin +} + +type ManagerOptions struct { + PluginDir string + SystemConst *zoraxyPlugin.RuntimeConstantValue + Database *database.Database + Logger *logger.Logger + CSRFTokenGen func(*http.Request) string //The CSRF token generator function +} + +type Manager struct { + LoadedPlugins sync.Map //Storing *Plugin + Options *ManagerOptions +} diff --git a/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go b/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go index 1691591..f3865ea 100644 --- a/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go +++ b/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go @@ -79,9 +79,8 @@ type IntroSpect struct { Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on This captures the whole traffic of Zoraxy - Notes: Will raise a warning on the UI when the user enables the plugin on a HTTP Proxy rule */ - GlobalCapturePath []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin + GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler) /* @@ -90,20 +89,9 @@ type IntroSpect struct { Once the plugin is enabled on a given HTTP Proxy rule, these always applies */ - AlwaysCapturePath []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp) + AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp) AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler) - /* - Dynamic Capture Settings - - Once the plugin is enabled on a given HTTP Proxy rule, - the plugin can capture the request and decided if the request - shall be handled by itself or let it pass through - - */ - DynmaicCaptureIngress string `json:"capture_path"` //Traffic capture path of your plugin (e.g. /capture) - DynamicHandleIngress string `json:"handle_path"` //Traffic handle path of your plugin (e.g. /handler) - /* UI Path for your plugin */ UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI diff --git a/src/web/components/gan.html b/src/web/components/gan.html index 12402fa..4c89b49 100644 --- a/src/web/components/gan.html +++ b/src/web/components/gan.html @@ -3,6 +3,10 @@

Global Area Network

Virtual Network Hub that allows all networked devices to communicate as if they all reside in the same physical data center or cloud region

+
+ Deprecation Notice +

Global Area Network will be deprecating in v3.2.x and moved to Plugin

+
diff --git a/src/web/components/plugincontext.html b/src/web/components/plugincontext.html new file mode 100644 index 0000000..3e49a52 --- /dev/null +++ b/src/web/components/plugincontext.html @@ -0,0 +1,52 @@ +
+ +
+ \ No newline at end of file diff --git a/src/web/components/plugins.html b/src/web/components/plugins.html index a509beb..e230553 100644 --- a/src/web/components/plugins.html +++ b/src/web/components/plugins.html @@ -1,7 +1,11 @@

Plugins

-

Custom features on Zoraxy

+

Add custom features to your Zoraxy!

+
+
+
Experimental Feature
+

This feature is experimental and may not work as expected. Use with caution.

@@ -19,6 +23,29 @@ diff --git a/src/web/index.html b/src/web/index.html index 3975c15..9498308 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -78,9 +78,6 @@ SSO / Oauth - - Plugins - Static Web Server @@ -96,6 +93,15 @@ Utilities + + Plugins Manager + + + + + No Installed Plugins + + @@ -155,6 +161,12 @@
+ + +
+ + + @@ -246,7 +258,26 @@ if (window.location.hash.length > 1){ let tabID = window.location.hash.substr(1); - openTabById(tabID); + if (tabID.startsWith("{")) { + tabID = decodeURIComponent(tabID); + //Zoraxy v3.2.x plugin context window + try { + let parsedData = JSON.parse(tabID); + tabID = parsedData.tabID; + + //Open the plugin context window + if (tabID == "pluginContextWindow"){ + let pluginID = parsedData.pluginID; + let button = $("#pluginMenu").find(`[pluginid="${pluginID}"]`); + openTabById(tabID, button); + loadPluginUIContextIfAvailable(); + } + } catch (e) { + console.error("Invalid JSON data:", e); + } + }else{ + openTabById(tabID); + } }else{ openTabById("status"); } @@ -257,7 +288,7 @@ $("#mainmenu").find(".item").each(function(){ $(this).on("click", function(event){ let tabid = $(this).attr("tag"); - openTabById(tabid); + openTabById(tabid, $(this)); }); }); @@ -282,13 +313,19 @@ if ($(".sideWrapper").is(":visible")){ $(".sideWrapper iframe")[0].contentWindow.setDarkTheme(false); } + + if ($("#pluginContextLoader").is(":visible")){ + $("#pluginContextLoader")[0].contentWindow.setDarkTheme(false); + } }else{ setDarkTheme(true); //Check if the snippet iframe is opened. If yes, set the dark theme to the iframe if ($(".sideWrapper").is(":visible")){ $(".sideWrapper iframe")[0].contentWindow.setDarkTheme(true); } - + if ($("#pluginContextLoader").is(":visible")){ + $("#pluginContextLoader")[0].contentWindow.setDarkTheme(true); + } } } @@ -307,8 +344,12 @@ //Select and open a tab by its tag id let tabSwitchEventBind = {}; //Bind event to tab switch by tabid - function openTabById(tabID){ - let targetBtn = getTabButtonById(tabID); + function openTabById(tabID, object=undefined){ + let targetBtn = object; + if (object == undefined){ + //Search tab by its tap id + targetBtn = getTabButtonById(tabID); + } if (targetBtn == undefined){ alert("Invalid tabid given"); return; @@ -329,7 +370,19 @@ },100) }); $('html,body').animate({scrollTop: 0}, 'fast'); - window.location.hash = tabID; + + if (tabID == "pluginContextWindow"){ + let statePayload = { + tabID: tabID, + pluginID: $(targetBtn).attr("pluginid") + } + + window.location.hash = JSON.stringify(statePayload); + loadPluginUIContextIfAvailable(); + }else{ + window.location.hash = tabID; + } + } $(window).on("resize", function(){ From 14e1341c343fb3a70e0a1a291a76c9a590d9d7a0 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Fri, 28 Feb 2025 22:07:08 +0800 Subject: [PATCH 13/14] Removed legacy example plugin files --- .gitignore | 4 +++- example/plugins/helloworld/icon.psd | Bin 134796 -> 0 bytes example/plugins/helloworld/index.html | 24 ------------------------ 3 files changed, 3 insertions(+), 25 deletions(-) delete mode 100644 example/plugins/helloworld/icon.psd delete mode 100644 example/plugins/helloworld/index.html diff --git a/.gitignore b/.gitignore index 2c708ee..36003b0 100644 --- a/.gitignore +++ b/.gitignore @@ -45,5 +45,7 @@ src/log/ # dev-tags /Dockerfile /Entrypoint.sh -example/plugins/zerotiernc/authtoken.secret + +# plugins example/plugins/ztnc/ztnc.db +example/plugins/ztnc/authtoken.secret diff --git a/example/plugins/helloworld/icon.psd b/example/plugins/helloworld/icon.psd deleted file mode 100644 index eae71eafc9b2d9cc9c1de57948767dc02e723325..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134796 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 - - - - - Hello World - - - -
-

Hello World

-

Welcome to your first Zoraxy plugin

-
- - \ No newline at end of file From 28a0a837baab4ba732ac4a3896e78f21caaa2ba6 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Sat, 1 Mar 2025 10:00:33 +0800 Subject: [PATCH 14/14] Plugin lifecycle optimization - Added term flow before plugin is killed - Updated example implementations - Added SIGINT to Zoraxy for shutdown sequence (Fixes #561 ?) --- example/plugins/helloworld/main.go | 17 ++-- .../zoraxy_plugin/embed_webserver.go | 23 +++++ .../helloworld/zoraxy_plugin/zoraxy_plugin.go | 40 +------- example/plugins/ztnc/main.go | 14 +-- .../plugins/ztnc/mod/zoraxy_plugin/README.txt | 19 ++++ .../ztnc/mod/zoraxy_plugin/embed_webserver.go | 22 +++++ .../ztnc/mod/zoraxy_plugin/zoraxy_plugin.go | 40 +------- src/def.go | 2 +- src/main.go | 2 +- src/mod/plugins/lifecycle.go | 57 +++++++++-- src/mod/plugins/plugins.go | 2 +- .../plugins/zoraxy_plugin/embed_webserver.go | 22 +++++ .../plugins/zoraxy_plugin/zoraxy_plugin.go | 24 ----- src/web/components/plugincontext.html | 12 ++- src/web/components/plugins.html | 96 ++++++++++++++++--- src/web/index.html | 2 +- 16 files changed, 255 insertions(+), 139 deletions(-) create mode 100644 example/plugins/ztnc/mod/zoraxy_plugin/README.txt diff --git a/example/plugins/helloworld/main.go b/example/plugins/helloworld/main.go index d4aec15..74188cf 100644 --- a/example/plugins/helloworld/main.go +++ b/example/plugins/helloworld/main.go @@ -43,16 +43,21 @@ func main() { panic(err) } - // Register the shutdown handler - plugin.RegisterShutdownHandler(func() { + // Create a new PluginEmbedUIRouter that will serve the UI from web folder + // The router will also help to handle the termination of the plugin when + // a user wants to stop the plugin via Zoraxy Web UI + embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH) + embedWebRouter.RegisterTerminateHandler(func() { // Do cleanup here if needed fmt.Println("Hello World Plugin Exited") - }) - - embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH) + }, nil) // Serve the hello world page in the www folder http.Handle(UI_PATH, embedWebRouter.Handler()) fmt.Println("Hello World started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port)) - http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil) + err = http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil) + if err != nil { + panic(err) + } + } diff --git a/example/plugins/helloworld/zoraxy_plugin/embed_webserver.go b/example/plugins/helloworld/zoraxy_plugin/embed_webserver.go index 2a264a2..c529e99 100644 --- a/example/plugins/helloworld/zoraxy_plugin/embed_webserver.go +++ b/example/plugins/helloworld/zoraxy_plugin/embed_webserver.go @@ -6,6 +6,7 @@ import ( "io/fs" "net/http" "net/url" + "os" "strings" "time" ) @@ -15,6 +16,8 @@ type PluginUiRouter struct { TargetFs *embed.FS //The embed.FS where the UI files are stored TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui + + terminateHandler func() //The handler to be called when the plugin is terminated } // NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS @@ -91,6 +94,7 @@ func (p *PluginUiRouter) Handler() http.Handler { rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") r.URL, _ = url.Parse(rewrittenURL) r.RequestURI = rewrittenURL + //Serve the file from the embed.FS subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/")) if err != nil { @@ -103,3 +107,22 @@ func (p *PluginUiRouter) Handler() http.Handler { p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r) }) } + +// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter +// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager +// if mux is nil, the handler will be registered to http.DefaultServeMux +func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) { + p.terminateHandler = termFunc + if mux == nil { + mux = http.DefaultServeMux + } + mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) { + p.terminateHandler() + w.WriteHeader(http.StatusOK) + go func() { + //Make sure the response is sent before the plugin is terminated + time.Sleep(100 * time.Millisecond) + os.Exit(0) + }() + }) +} diff --git a/example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go b/example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go index 1691591..b316e6d 100644 --- a/example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go +++ b/example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go @@ -4,9 +4,7 @@ import ( "encoding/json" "fmt" "os" - "os/signal" "strings" - "syscall" ) /* @@ -79,9 +77,8 @@ type IntroSpect struct { Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on This captures the whole traffic of Zoraxy - Notes: Will raise a warning on the UI when the user enables the plugin on a HTTP Proxy rule */ - GlobalCapturePath []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin + GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler) /* @@ -90,20 +87,9 @@ type IntroSpect struct { Once the plugin is enabled on a given HTTP Proxy rule, these always applies */ - AlwaysCapturePath []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp) + AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp) AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler) - /* - Dynamic Capture Settings - - Once the plugin is enabled on a given HTTP Proxy rule, - the plugin can capture the request and decided if the request - shall be handled by itself or let it pass through - - */ - DynmaicCaptureIngress string `json:"capture_path"` //Traffic capture path of your plugin (e.g. /capture) - DynamicHandleIngress string `json:"handle_path"` //Traffic handle path of your plugin (e.g. /handler) - /* UI Path for your plugin */ UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI @@ -186,25 +172,3 @@ func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) { ServeIntroSpect(pluginSpect) return RecvConfigureSpec() } - -/* - -Shutdown handler - -This function will register a shutdown handler for the plugin -The shutdown callback will be called when the plugin is shutting down -You can use this to clean up resources like closing database connections -*/ - -func RegisterShutdownHandler(shutdownCallback func()) { - // Set up a channel to receive OS signals - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - // Start a goroutine to listen for signals - go func() { - <-sigChan - shutdownCallback() - os.Exit(0) - }() -} diff --git a/example/plugins/ztnc/main.go b/example/plugins/ztnc/main.go index b302275..ee96033 100644 --- a/example/plugins/ztnc/main.go +++ b/example/plugins/ztnc/main.go @@ -51,17 +51,17 @@ func main() { panic(err) } + // Create a new PluginEmbedUIRouter that will serve the UI from web folder + uiRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, EMBED_FS_ROOT, UI_RELPATH) + // Register the shutdown handler - plugin.RegisterShutdownHandler(func() { - fmt.Println("Shutting down ZeroTier Network Controller") + uiRouter.RegisterTerminateHandler(func() { + // Do cleanup here if needed if sysdb != nil { sysdb.Close() } - fmt.Println("ZeroTier Network Controller Exited") - }) - - // Create a new PluginEmbedUIRouter that will serve the UI from web folder - uiRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, EMBED_FS_ROOT, UI_RELPATH) + fmt.Println("ztnc Exited") + }, nil) // This will serve the index.html file embedded in the binary http.Handle(UI_RELPATH+"/", uiRouter.Handler()) diff --git a/example/plugins/ztnc/mod/zoraxy_plugin/README.txt b/example/plugins/ztnc/mod/zoraxy_plugin/README.txt new file mode 100644 index 0000000..ed8a405 --- /dev/null +++ b/example/plugins/ztnc/mod/zoraxy_plugin/README.txt @@ -0,0 +1,19 @@ +# Zoraxy Plugin + +## Overview +This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components. + +## Instructions + +1. **Copy the Module:** + - Copy the entire `zoraxy_plugin` module to your plugin mod folder. + +2. **Include the Structure:** + - Ensure that you maintain the directory structure and file organization as provided in this module. + +3. **Modify as Needed:** + - Customize the copied module to implement the desired functionality for your plugin. + +## Directory Structure + zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup + embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages \ No newline at end of file diff --git a/example/plugins/ztnc/mod/zoraxy_plugin/embed_webserver.go b/example/plugins/ztnc/mod/zoraxy_plugin/embed_webserver.go index d9b3fde..c529e99 100644 --- a/example/plugins/ztnc/mod/zoraxy_plugin/embed_webserver.go +++ b/example/plugins/ztnc/mod/zoraxy_plugin/embed_webserver.go @@ -6,6 +6,7 @@ import ( "io/fs" "net/http" "net/url" + "os" "strings" "time" ) @@ -15,6 +16,8 @@ type PluginUiRouter struct { TargetFs *embed.FS //The embed.FS where the UI files are stored TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui + + terminateHandler func() //The handler to be called when the plugin is terminated } // NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS @@ -104,3 +107,22 @@ func (p *PluginUiRouter) Handler() http.Handler { p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r) }) } + +// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter +// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager +// if mux is nil, the handler will be registered to http.DefaultServeMux +func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) { + p.terminateHandler = termFunc + if mux == nil { + mux = http.DefaultServeMux + } + mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) { + p.terminateHandler() + w.WriteHeader(http.StatusOK) + go func() { + //Make sure the response is sent before the plugin is terminated + time.Sleep(100 * time.Millisecond) + os.Exit(0) + }() + }) +} diff --git a/example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go b/example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go index 1691591..b316e6d 100644 --- a/example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go +++ b/example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go @@ -4,9 +4,7 @@ import ( "encoding/json" "fmt" "os" - "os/signal" "strings" - "syscall" ) /* @@ -79,9 +77,8 @@ type IntroSpect struct { Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on This captures the whole traffic of Zoraxy - Notes: Will raise a warning on the UI when the user enables the plugin on a HTTP Proxy rule */ - GlobalCapturePath []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin + GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler) /* @@ -90,20 +87,9 @@ type IntroSpect struct { Once the plugin is enabled on a given HTTP Proxy rule, these always applies */ - AlwaysCapturePath []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp) + AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp) AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler) - /* - Dynamic Capture Settings - - Once the plugin is enabled on a given HTTP Proxy rule, - the plugin can capture the request and decided if the request - shall be handled by itself or let it pass through - - */ - DynmaicCaptureIngress string `json:"capture_path"` //Traffic capture path of your plugin (e.g. /capture) - DynamicHandleIngress string `json:"handle_path"` //Traffic handle path of your plugin (e.g. /handler) - /* UI Path for your plugin */ UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI @@ -186,25 +172,3 @@ func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) { ServeIntroSpect(pluginSpect) return RecvConfigureSpec() } - -/* - -Shutdown handler - -This function will register a shutdown handler for the plugin -The shutdown callback will be called when the plugin is shutting down -You can use this to clean up resources like closing database connections -*/ - -func RegisterShutdownHandler(shutdownCallback func()) { - // Set up a channel to receive OS signals - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - // Start a goroutine to listen for signals - go func() { - <-sigChan - shutdownCallback() - os.Exit(0) - }() -} diff --git a/src/def.go b/src/def.go index b03fd8d..9e355c9 100644 --- a/src/def.go +++ b/src/def.go @@ -44,7 +44,7 @@ const ( /* Build Constants */ SYSTEM_NAME = "Zoraxy" SYSTEM_VERSION = "3.1.9" - DEVELOPMENT_BUILD = true /* Development: Set to false to use embedded web fs */ + DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */ /* System Constants */ TMP_FOLDER = "./tmp" diff --git a/src/main.go b/src/main.go index 67b71bf..18dd086 100644 --- a/src/main.go +++ b/src/main.go @@ -50,7 +50,7 @@ import ( /* SIGTERM handler, do shutdown sequences before closing */ func SetupCloseHandler() { c := make(chan os.Signal, 2) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) + signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) go func() { <-c ShutdownSeq() diff --git a/src/mod/plugins/lifecycle.go b/src/mod/plugins/lifecycle.go index 7c7ebbf..88c5b23 100644 --- a/src/mod/plugins/lifecycle.go +++ b/src/mod/plugins/lifecycle.go @@ -4,12 +4,14 @@ import ( "encoding/json" "errors" "io" + "net/http" "net/url" - "os" "os/exec" "path/filepath" + "runtime" "strconv" "strings" + "syscall" "time" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" @@ -146,20 +148,55 @@ func (m *Manager) StopPlugin(pluginID string) error { } thisPlugin := plugin.(*Plugin) - thisPlugin.process.Process.Signal(os.Interrupt) - go func() { - //Wait for 10 seconds for the plugin to stop gracefully - time.Sleep(10 * time.Second) + var err error + + //Make a GET request to plugin ui path /term to gracefully stop the plugin + if thisPlugin.uiProxy != nil { + requestURI := "http://127.0.0.1:" + strconv.Itoa(thisPlugin.AssignedPort) + "/" + thisPlugin.Spec.UIPath + "/term" + resp, err := http.Get(requestURI) + if err != nil { + //Plugin do not support termination request, do it the hard way + m.Log("Plugin "+thisPlugin.Spec.ID+" termination request failed. Force shutting down", nil) + } else { + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusNotFound { + m.Log("Plugin "+thisPlugin.Spec.ID+" does not support termination request", nil) + } else { + m.Log("Plugin "+thisPlugin.Spec.ID+" termination request returned status: "+resp.Status, nil) + } + + } + } + } + + if runtime.GOOS == "windows" && thisPlugin.process != nil { + //There is no SIGTERM in windows, kill the process directly + time.Sleep(300 * time.Millisecond) + thisPlugin.process.Process.Kill() + } else { + //Send SIGTERM to the plugin process, if it is still running + err = thisPlugin.process.Process.Signal(syscall.SIGTERM) + if err != nil { + m.Log("Failed to send Interrupt signal to plugin "+thisPlugin.Spec.Name+": "+err.Error(), nil) + } + + //Wait for the plugin to stop + for range 5 { + time.Sleep(1 * time.Second) + if thisPlugin.process.ProcessState != nil && thisPlugin.process.ProcessState.Exited() { + m.Log("Plugin "+thisPlugin.Spec.Name+" background process stopped", nil) + break + } + } if thisPlugin.process.ProcessState == nil || !thisPlugin.process.ProcessState.Exited() { m.Log("Plugin "+thisPlugin.Spec.Name+" failed to stop gracefully, killing it", nil) thisPlugin.process.Process.Kill() - } else { - m.Log("Plugin "+thisPlugin.Spec.Name+" background process stopped", nil) } + } - //Remove the UI proxy - thisPlugin.uiProxy = nil - }() + //Remove the UI proxy + thisPlugin.uiProxy = nil plugin.(*Plugin).Enabled = false return nil } diff --git a/src/mod/plugins/plugins.go b/src/mod/plugins/plugins.go index cc51cd0..5be4af4 100644 --- a/src/mod/plugins/plugins.go +++ b/src/mod/plugins/plugins.go @@ -112,7 +112,7 @@ func (m *Manager) GetPluginPreviousEnableState(pluginID string) bool { // ListLoadedPlugins returns a list of loaded plugins func (m *Manager) ListLoadedPlugins() ([]*Plugin, error) { - var plugins []*Plugin + var plugins []*Plugin = []*Plugin{} m.LoadedPlugins.Range(func(key, value interface{}) bool { plugin := value.(*Plugin) plugins = append(plugins, plugin) diff --git a/src/mod/plugins/zoraxy_plugin/embed_webserver.go b/src/mod/plugins/zoraxy_plugin/embed_webserver.go index d9b3fde..c529e99 100644 --- a/src/mod/plugins/zoraxy_plugin/embed_webserver.go +++ b/src/mod/plugins/zoraxy_plugin/embed_webserver.go @@ -6,6 +6,7 @@ import ( "io/fs" "net/http" "net/url" + "os" "strings" "time" ) @@ -15,6 +16,8 @@ type PluginUiRouter struct { TargetFs *embed.FS //The embed.FS where the UI files are stored TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui + + terminateHandler func() //The handler to be called when the plugin is terminated } // NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS @@ -104,3 +107,22 @@ func (p *PluginUiRouter) Handler() http.Handler { p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r) }) } + +// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter +// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager +// if mux is nil, the handler will be registered to http.DefaultServeMux +func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) { + p.terminateHandler = termFunc + if mux == nil { + mux = http.DefaultServeMux + } + mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) { + p.terminateHandler() + w.WriteHeader(http.StatusOK) + go func() { + //Make sure the response is sent before the plugin is terminated + time.Sleep(100 * time.Millisecond) + os.Exit(0) + }() + }) +} diff --git a/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go b/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go index f3865ea..b316e6d 100644 --- a/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go +++ b/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go @@ -4,9 +4,7 @@ import ( "encoding/json" "fmt" "os" - "os/signal" "strings" - "syscall" ) /* @@ -174,25 +172,3 @@ func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) { ServeIntroSpect(pluginSpect) return RecvConfigureSpec() } - -/* - -Shutdown handler - -This function will register a shutdown handler for the plugin -The shutdown callback will be called when the plugin is shutting down -You can use this to clean up resources like closing database connections -*/ - -func RegisterShutdownHandler(shutdownCallback func()) { - // Set up a channel to receive OS signals - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - // Start a goroutine to listen for signals - go func() { - <-sigChan - shutdownCallback() - os.Exit(0) - }() -} diff --git a/src/web/components/plugincontext.html b/src/web/components/plugincontext.html index 3e49a52..f8e2bde 100644 --- a/src/web/components/plugincontext.html +++ b/src/web/components/plugincontext.html @@ -4,13 +4,23 @@ diff --git a/src/web/index.html b/src/web/index.html index 9498308..8c71281 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -99,7 +99,7 @@ - No Installed Plugins + No Plugins Installed