Merge pull request #368 from tobychui/v3.1.2

v3.1.2
This commit is contained in:
Toby Chui 2024-11-03 10:57:06 +08:00 committed by GitHub
commit 4577fb1f2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 180074 additions and 35117 deletions

View File

@ -51,7 +51,7 @@ If you have no background in setting up reverse proxy or web routing, you should
## Build from Source ## Build from Source
Requires Go 1.22 or higher Requires Go 1.23 or higher
```bash ```bash
git clone https://github.com/tobychui/zoraxy git clone https://github.com/tobychui/zoraxy

View File

@ -85,9 +85,20 @@ func acmeRegisterSpecialRoutingRule() {
// This function check if the renew setup is satisfied. If not, toggle them automatically // This function check if the renew setup is satisfied. If not, toggle them automatically
func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) { func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
isForceHttpsRedirectEnabledOriginally := false isForceHttpsRedirectEnabledOriginally := false
requireRestorePort80 := false
dnsPara, _ := utils.PostBool(r, "dns") dnsPara, _ := utils.PostBool(r, "dns")
if !dnsPara { if !dnsPara {
if dynamicProxyRouter.Option.Port == 443 { if dynamicProxyRouter.Option.Port == 443 {
//Check if port 80 is enabled
if !dynamicProxyRouter.Option.ListenOnPort80 {
//Enable port 80 temporarily
SystemWideLogger.PrintAndLog("ACME", "Temporarily enabling port 80 listener to handle ACME request ", nil)
dynamicProxyRouter.UpdatePort80ListenerState(true)
requireRestorePort80 = true
time.Sleep(2 * time.Second)
}
//Enable port 80 to 443 redirect //Enable port 80 to 443 redirect
if !dynamicProxyRouter.Option.ForceHttpsRedirect { if !dynamicProxyRouter.Option.ForceHttpsRedirect {
SystemWideLogger.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests") SystemWideLogger.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests")
@ -107,8 +118,8 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
} }
} }
//Add a 3 second delay to make sure everything is settle down //Add a 2 second delay to make sure everything is settle down
time.Sleep(3 * time.Second) time.Sleep(2 * time.Second)
// Pass over to the acmeHandler to deal with the communication // Pass over to the acmeHandler to deal with the communication
acmeHandler.HandleRenewCertificate(w, r) acmeHandler.HandleRenewCertificate(w, r)
@ -117,13 +128,17 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
tlsCertManager.UpdateLoadedCertList() tlsCertManager.UpdateLoadedCertList()
//Restore original settings //Restore original settings
if dynamicProxyRouter.Option.Port == 443 && !dnsPara { if requireRestorePort80 {
if !isForceHttpsRedirectEnabledOriginally { //Restore port 80 listener
//Default is off. Turn the redirection off SystemWideLogger.PrintAndLog("ACME", "Restoring previous port 80 listener settings", nil)
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil) dynamicProxyRouter.UpdatePort80ListenerState(false)
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
}
} }
if !isForceHttpsRedirectEnabledOriginally {
//Default is off. Turn the redirection off
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
}
} }
// HandleACMEPreferredCA return the user preferred / default CA for new subdomain auto creation // HandleACMEPreferredCA return the user preferred / default CA for new subdomain auto creation

View File

@ -8,6 +8,7 @@ import (
"imuslab.com/zoraxy/mod/acme/acmedns" "imuslab.com/zoraxy/mod/acme/acmedns"
"imuslab.com/zoraxy/mod/acme/acmewizard" "imuslab.com/zoraxy/mod/acme/acmewizard"
"imuslab.com/zoraxy/mod/auth" "imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/ipscan"
"imuslab.com/zoraxy/mod/netstat" "imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/netutils" "imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
@ -95,6 +96,21 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck) authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
authRouter.HandleFunc("/api/cert/delete", handleCertRemove) authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
//SSO and Oauth
authRouter.HandleFunc("/api/sso/status", ssoHandler.HandleSSOStatus)
authRouter.HandleFunc("/api/sso/enable", ssoHandler.HandleSSOEnable)
authRouter.HandleFunc("/api/sso/setPort", ssoHandler.HandlePortChange)
authRouter.HandleFunc("/api/sso/setAuthURL", ssoHandler.HandleSetAuthURL)
authRouter.HandleFunc("/api/sso/app/register", ssoHandler.HandleRegisterApp)
//authRouter.HandleFunc("/api/sso/app/list", ssoHandler.HandleListApp)
//authRouter.HandleFunc("/api/sso/app/remove", ssoHandler.HandleRemoveApp)
authRouter.HandleFunc("/api/sso/user/list", ssoHandler.HandleListUser)
authRouter.HandleFunc("/api/sso/user/add", ssoHandler.HandleAddUser)
authRouter.HandleFunc("/api/sso/user/edit", ssoHandler.HandleEditUser)
authRouter.HandleFunc("/api/sso/user/remove", ssoHandler.HandleRemoveUser)
//Redirection config //Redirection config
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules) authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule) authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
@ -172,7 +188,8 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset) authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset)
//Network utilities //Network utilities
authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan) authRouter.HandleFunc("/api/tools/ipscan", ipscan.HandleIpScan)
authRouter.HandleFunc("/api/tools/portscan", ipscan.HandleScanPort)
authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute) authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute)
authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing) authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing)
authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois) authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois)

View File

@ -1,107 +1,127 @@
module imuslab.com/zoraxy module imuslab.com/zoraxy
go 1.21 go 1.22.0
toolchain go1.22.2 toolchain go1.22.2
require ( require (
github.com/boltdb/bolt v1.3.1 github.com/boltdb/bolt v1.3.1
github.com/docker/docker v27.0.0+incompatible github.com/docker/docker v27.0.0+incompatible
github.com/go-acme/lego/v4 v4.16.1 github.com/go-acme/lego/v4 v4.19.2
github.com/go-ping/ping v1.1.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 github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.2.2 github.com/gorilla/sessions v1.2.2
github.com/gorilla/websocket v1.5.1 github.com/gorilla/websocket v1.5.1
github.com/grandcat/zeroconf v1.0.0 github.com/grandcat/zeroconf v1.0.0
github.com/likexian/whois v1.15.1 github.com/likexian/whois v1.15.1
github.com/microcosm-cc/bluemonday v1.0.26 github.com/microcosm-cc/bluemonday v1.0.26
golang.org/x/net v0.25.0 golang.org/x/net v0.29.0
golang.org/x/sys v0.20.0 golang.org/x/sys v0.25.0
golang.org/x/text v0.15.0 golang.org/x/text v0.18.0
) )
require ( require (
cloud.google.com/go/compute v1.25.1 // indirect cloud.google.com/go/auth v0.9.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.4 // 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/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
github.com/tidwall/buntdb v1.1.2 // indirect
github.com/tidwall/gjson v1.12.1 // indirect
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e // indirect
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/vultr/govultr/v3 v3.9.1 // indirect
go.mongodb.org/mongo-driver v1.12.0 // indirect
)
require (
cloud.google.com/go/compute/metadata v0.5.1 // indirect
github.com/AdamSLevy/jsonrpc2/v14 v14.1.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 v68.0.0+incompatible // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.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.1.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.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // 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 v0.11.29 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/Microsoft/go-winio v0.4.14 // indirect github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 // indirect github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 // indirect
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect
github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect github.com/aws/aws-sdk-go-v2/config v1.27.33 // indirect
github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // 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/accept-encoding v1.10.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/internal/presigned-url v1.10.10 // indirect github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.34.0 // indirect github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.37.0 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect github.com/aws/smithy-go v1.20.4 // indirect
github.com/aws/smithy-go v1.19.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // 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 v2.2.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/civo/civogo v0.3.11 // indirect github.com/civo/civogo v0.3.11 // indirect
github.com/cloudflare/cloudflare-go v0.86.0 // indirect github.com/cloudflare/cloudflare-go v0.104.0 // indirect
github.com/containerd/log v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/deepmap/oapi-codegen v1.9.1 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/dnsimple/dnsimple-go v1.2.0 // indirect github.com/dnsimple/dnsimple-go v1.7.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/exoscale/egoscale v0.102.3 // indirect
github.com/fatih/structs v1.1.0 // indirect github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect github.com/go-errors/errors v1.0.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-resty/resty/v2 v2.11.0 // indirect github.com/go-oauth2/oauth2/v4 v4.5.2
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect github.com/go-resty/resty/v2 v2.13.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/goccy/go-json v0.10.3 // indirect
github.com/gofrs/uuid v4.4.0+incompatible
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.12.2 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/gophercloud/gophercloud v1.0.0 // indirect github.com/gophercloud/gophercloud v1.14.0 // indirect
github.com/gorilla/csrf v1.7.2 // indirect github.com/gorilla/csrf v1.7.2
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
@ -111,11 +131,11 @@ require (
github.com/kylelemons/godebug v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
github.com/labbsr0x/goh v1.0.1 // indirect github.com/labbsr0x/goh v1.0.1 // indirect
github.com/linode/linodego v1.28.0 // indirect github.com/linode/linodego v1.40.0 // indirect
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
github.com/liquidweb/liquidweb-go v1.6.4 // indirect github.com/liquidweb/liquidweb-go v1.6.4 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.58 // indirect github.com/miekg/dns v1.1.62 // indirect
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -126,66 +146,64 @@ require (
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
github.com/nrdcg/auroradns v1.1.0 // indirect github.com/nrdcg/auroradns v1.1.0 // indirect
github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 // indirect github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect
github.com/nrdcg/desec v0.7.0 // indirect github.com/nrdcg/desec v0.8.0 // indirect
github.com/nrdcg/dnspod-go v0.4.0 // indirect github.com/nrdcg/dnspod-go v0.4.0 // indirect
github.com/nrdcg/freemyip v0.2.0 // indirect github.com/nrdcg/freemyip v0.2.0 // indirect
github.com/nrdcg/goinwx v0.10.0 // indirect github.com/nrdcg/goinwx v0.10.0 // indirect
github.com/nrdcg/mailinabox v0.2.0 // indirect github.com/nrdcg/mailinabox v0.2.0 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/nrdcg/nodion v0.1.0 // indirect github.com/nrdcg/nodion v0.1.0 // indirect
github.com/nrdcg/porkbun v0.3.0 // indirect github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/nzdjb/go-metaname v1.0.0 // indirect github.com/nzdjb/go-metaname v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/ovh/go-ovh v1.4.3 // indirect github.com/ovh/go-ovh v1.6.0 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pquerna/otp v1.4.0 // indirect github.com/pquerna/otp v1.4.0 // indirect
github.com/sacloud/api-client-go v0.2.8 // indirect github.com/sacloud/api-client-go v0.2.10 // indirect
github.com/sacloud/go-http v0.1.6 // indirect github.com/sacloud/go-http v0.1.8 // indirect
github.com/sacloud/iaas-api-go v1.11.1 // indirect github.com/sacloud/iaas-api-go v1.12.0 // indirect
github.com/sacloud/packages-go v0.0.9 // indirect github.com/sacloud/packages-go v0.0.10 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22 // indirect github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
github.com/softlayer/softlayer-go v1.1.3 // indirect github.com/softlayer/softlayer-go v1.1.5 // indirect
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect github.com/stretchr/testify v1.9.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 // indirect
github.com/transip/gotransip/v6 v6.23.0 // indirect github.com/transip/gotransip/v6 v6.26.0 // indirect
github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a // indirect github.com/ultradns/ultradns-go-sdk v1.7.0-20240913052650-970ca9a // indirect
github.com/vinyldns/go-vinyldns v0.9.16 // indirect github.com/vinyldns/go-vinyldns v0.9.16 // indirect
github.com/vultr/govultr/v2 v2.17.2 // indirect github.com/xlzd/gotp v0.1.0
github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f // indirect github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2 // indirect
github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 // indirect github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel v1.27.0 // indirect go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect
go.opentelemetry.io/otel/metric v1.27.0 // indirect go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/sdk v1.27.0 // indirect go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.27.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.uber.org/ratelimit v0.2.0 // indirect go.uber.org/ratelimit v0.3.0 // indirect
golang.org/x/crypto v0.23.0 // indirect golang.org/x/crypto v0.27.0 // indirect
golang.org/x/mod v0.16.0 // indirect golang.org/x/mod v0.21.0 // indirect
golang.org/x/oauth2 v0.18.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.6.0 // indirect golang.org/x/sync v0.8.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.6.0 // indirect
golang.org/x/tools v0.19.0 // indirect golang.org/x/tools v0.25.0 // indirect
google.golang.org/api v0.169.0 // indirect google.golang.org/api v0.197.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect google.golang.org/grpc v1.66.1 // indirect
google.golang.org/grpc v1.64.0 // indirect google.golang.org/protobuf v1.34.2 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.7.13 // indirect gopkg.in/ns1/ns1-go.v2 v2.12.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.1 // indirect gotest.tools/v3 v3.5.1 // indirect

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@ import (
"imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth" "imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/auth/sso"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dockerux" "imuslab.com/zoraxy/mod/dockerux"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
@ -59,7 +60,7 @@ var enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade
var ( var (
name = "Zoraxy" name = "Zoraxy"
version = "3.1.1" version = "3.1.2"
nodeUUID = "generic" //System uuid, in uuidv4 format nodeUUID = "generic" //System uuid, in uuidv4 format
development = false //Set this to false to use embedded web fs development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix() bootTime = time.Now().Unix()
@ -95,6 +96,7 @@ var (
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing
ssoHandler *sso.SSOHandler //Single Sign On handler
//Helper modules //Helper modules
EmailSender *email.Sender //Email sender that handle email sending EmailSender *email.Sender //Email sender that handle email sending

View File

@ -30,10 +30,11 @@ import (
) )
type CertificateInfoJSON struct { type CertificateInfoJSON struct {
AcmeName string `json:"acme_name"` AcmeName string `json:"acme_name"` //ACME provider name
AcmeUrl string `json:"acme_url"` AcmeUrl string `json:"acme_url"` //Custom ACME URL (if any)
SkipTLS bool `json:"skip_tls"` SkipTLS bool `json:"skip_tls"` //Skip TLS verification of upstream
UseDNS bool `json:"dns"` UseDNS bool `json:"dns"` //Use DNS challenge
PropTimeout int `json:"prop_time"` //Propagation timeout
} }
// ACMEUser represents a user in the ACME system. // ACMEUser represents a user in the ACME system.
@ -86,7 +87,7 @@ func (a *ACMEHandler) Logf(message string, err error) {
} }
// ObtainCert obtains a certificate for the specified domains. // ObtainCert obtains a certificate for the specified domains.
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool) (bool, error) { func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool, propagationTimeout int) (bool, error) {
a.Logf("Obtaining certificate for: "+strings.Join(domains, ", "), nil) a.Logf("Obtaining certificate for: "+strings.Join(domains, ", "), nil)
// generate private key // generate private key
@ -181,7 +182,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
return false, err return false, err
} }
provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials) provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials, propagationTimeout)
if err != nil { if err != nil {
a.Logf("Unable to resolve DNS challenge provider", err) a.Logf("Unable to resolve DNS challenge provider", err)
return false, err return false, err
@ -285,10 +286,11 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
// Save certificate's ACME info for renew usage // Save certificate's ACME info for renew usage
certInfo := &CertificateInfoJSON{ certInfo := &CertificateInfoJSON{
AcmeName: caName, AcmeName: caName,
AcmeUrl: caUrl, AcmeUrl: caUrl,
SkipTLS: skipTLS, SkipTLS: skipTLS,
UseDNS: useDNS, UseDNS: useDNS,
PropTimeout: propagationTimeout,
} }
certInfoBytes, err := json.Marshal(certInfo) certInfoBytes, err := json.Marshal(certInfo)
@ -452,12 +454,30 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
} }
domains := strings.Split(domainPara, ",") domains := strings.Split(domainPara, ",")
// Default propagation timeout is 300 seconds
propagationTimeout := 300
if dns {
ppgTimeout, err := utils.PostPara(r, "ppgTimeout")
if err == nil {
propagationTimeout, err = strconv.Atoi(ppgTimeout)
if err != nil {
utils.SendErrorResponse(w, "Invalid propagation timeout value")
return
}
if propagationTimeout < 60 {
//Minimum propagation timeout is 60 seconds
propagationTimeout = 60
}
}
}
//Clean spaces in front or behind each domain //Clean spaces in front or behind each domain
cleanedDomains := []string{} cleanedDomains := []string{}
for _, domain := range domains { for _, domain := range domains {
cleanedDomains = append(cleanedDomains, strings.TrimSpace(domain)) cleanedDomains = append(cleanedDomains, strings.TrimSpace(domain))
} }
result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns) result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns, propagationTimeout)
if err != nil { if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error())) utils.SendErrorResponse(w, jsonEscape(err.Error()))
return return

View File

@ -1,70 +1,56 @@
package acme package acme
import ( import (
"encoding/json"
"strconv"
"github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge"
"imuslab.com/zoraxy/mod/acme/acmedns" "imuslab.com/zoraxy/mod/acme/acmedns"
) )
func GetDnsChallengeProviderByName(dnsProvider string, dnsCredentials string) (challenge.Provider, error) { // Preprocessor function to get DNS challenge provider by name
func GetDnsChallengeProviderByName(dnsProvider string, dnsCredentials string, ppgTimeout int) (challenge.Provider, error) {
//Original Implementation //Unpack the dnsCredentials (json string) to map
/*credentials, err := extractDnsCredentials(dnsCredentials) var dnsCredentialsMap map[string]interface{}
err := json.Unmarshal([]byte(dnsCredentials), &dnsCredentialsMap)
if err != nil { if err != nil {
return nil, err return nil, err
} }
setCredentialsIntoEnvironmentVariables(credentials)
provider, err := dns.NewDNSChallengeProviderByName(dnsProvider) //Clear the PollingInterval and PropagationTimeout field and conert to int
*/ userDefinedPollingInterval := 2
if dnsCredentialsMap["PollingInterval"] != nil {
//New implementation using acmedns CICD pipeline generated datatype userDefinedPollingIntervalRaw := dnsCredentialsMap["PollingInterval"].(string)
return acmedns.GetDNSProviderByJsonConfig(dnsProvider, dnsCredentials) delete(dnsCredentialsMap, "PollingInterval")
} convertedPollingInterval, err := strconv.Atoi(userDefinedPollingIntervalRaw)
if err == nil {
/* userDefinedPollingInterval = convertedPollingInterval
Original implementation of DNS ACME using OS.Env as payload
*/
/*
func setCredentialsIntoEnvironmentVariables(credentials map[string]string) {
for key, value := range credentials {
err := os.Setenv(key, value)
if err != nil {
log.Println("[ERR] Failed to set environment variable %s: %v", key, err)
} else {
log.Println("[INFO] Environment variable %s set successfully", key)
}
}
}
func extractDnsCredentials(input string) (map[string]string, error) {
result := make(map[string]string)
// Split the input string by newline character
lines := strings.Split(input, "\n")
// Iterate over each line
for _, line := range lines {
// Split the line by "=" character
//use SpliyN to make sure not to split the value if the value is base64
parts := strings.SplitN(line, "=", 1)
// Check if the line is in the correct format
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
// Add the key-value pair to the map
result[key] = value
if value == "" || key == "" {
//invalid config
return result, errors.New("DNS credential extract failed")
}
} }
} }
return result, nil userDefinedPropagationTimeout := ppgTimeout
} if dnsCredentialsMap["PropagationTimeout"] != nil {
userDefinedPropagationTimeoutRaw := dnsCredentialsMap["PropagationTimeout"].(string)
delete(dnsCredentialsMap, "PropagationTimeout")
convertedPropagationTimeout, err := strconv.Atoi(userDefinedPropagationTimeoutRaw)
if err == nil {
//Overwrite the default propagation timeout if it is requeted from UI
userDefinedPropagationTimeout = convertedPropagationTimeout
}
}
*/ //Restructure dnsCredentials string from map
dnsCredentialsBytes, err := json.Marshal(dnsCredentialsMap)
if err != nil {
return nil, err
}
dnsCredentials = string(dnsCredentialsBytes)
//Using acmedns CICD pipeline generated datatype to optain the DNS provider
return acmedns.GetDNSProviderByJsonConfig(
dnsProvider,
dnsCredentials,
int64(userDefinedPropagationTimeout),
int64(userDefinedPollingInterval),
)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -88,9 +88,12 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
AcmeHandler: AcmeHandler, AcmeHandler: AcmeHandler,
RenewerConfig: &renewerConfig, RenewerConfig: &renewerConfig,
RenewTickInterval: renewCheckInterval, RenewTickInterval: renewCheckInterval,
EarlyRenewDays: earlyRenewDays,
Logger: logger, Logger: logger,
} }
thisRenewer.Logf("ACME early renew set to "+fmt.Sprint(earlyRenewDays)+" days and check interval set to "+fmt.Sprint(renewCheckInterval)+" seconds", nil)
if thisRenewer.RenewerConfig.Enabled { if thisRenewer.RenewerConfig.Enabled {
//Start the renew ticker //Start the renew ticker
thisRenewer.StartAutoRenewTicker() thisRenewer.StartAutoRenewTicker()
@ -103,7 +106,7 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
} }
func (a *AutoRenewer) Logf(message string, err error) { func (a *AutoRenewer) Logf(message string, err error) {
a.Logger.PrintAndLog("CertRenew", message, err) a.Logger.PrintAndLog("cert-renew", message, err)
} }
func (a *AutoRenewer) StartAutoRenewTicker() { func (a *AutoRenewer) StartAutoRenewTicker() {
@ -381,7 +384,13 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
} }
} }
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS) //For upgrading config from older version of Zoraxy which don't have timeout
if certInfo.PropTimeout == 0 {
//Set default timeout
certInfo.PropTimeout = 300
}
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS, certInfo.PropTimeout)
if err != nil { if err != nil {
a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err) a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err)
} else { } else {

View File

@ -5,14 +5,14 @@ import (
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "os"
"time" "time"
) )
// Get the issuer name from pem file // Get the issuer name from pem file
func ExtractIssuerNameFromPEM(pemFilePath string) (string, error) { func ExtractIssuerNameFromPEM(pemFilePath string) (string, error) {
// Read the PEM file // Read the PEM file
pemData, err := ioutil.ReadFile(pemFilePath) pemData, err := os.ReadFile(pemFilePath)
if err != nil { if err != nil {
return "", err return "", err
} }

34
src/mod/auth/sso/app.go Normal file
View File

@ -0,0 +1,34 @@
package sso
/*
app.go
This file contains the app structure and app management
functions for the SSO module.
*/
// RegisteredUpstreamApp is a structure that contains the information of an
// upstream app that is registered with the SSO server
type RegisteredUpstreamApp struct {
ID string
Secret string
Domain []string
Scopes []string
SessionDuration int //in seconds, default to 1 hour
}
// RegisterUpstreamApp registers an upstream app with the SSO server
func (s *SSOHandler) ListRegisteredApps() []*RegisteredUpstreamApp {
apps := make([]*RegisteredUpstreamApp, 0)
for _, app := range s.Apps {
apps = append(apps, &app)
}
return apps
}
// RegisterUpstreamApp registers an upstream app with the SSO server
func (s *SSOHandler) GetAppByID(appID string) (*RegisteredUpstreamApp, bool) {
app, ok := s.Apps[appID]
return &app, ok
}

View File

@ -0,0 +1,271 @@
package sso
/*
handlers.go
This file contains the handlers for the SSO module.
If you are looking for handlers for SSO user management,
please refer to userHandlers.go.
*/
import (
"encoding/json"
"net/http"
"strings"
"github.com/gofrs/uuid"
"imuslab.com/zoraxy/mod/utils"
)
// HandleSSOStatus handle the request to get the status of the SSO portal server
func (s *SSOHandler) HandleSSOStatus(w http.ResponseWriter, r *http.Request) {
type SSOStatus struct {
Enabled bool
SSOInterceptEnabled bool
ListeningPort int
AuthURL string
}
status := SSOStatus{
Enabled: s.ssoPortalServer != nil,
//SSOInterceptEnabled: s.ssoInterceptEnabled,
ListeningPort: s.Config.PortalServerPort,
AuthURL: s.Config.AuthURL,
}
js, _ := json.Marshal(status)
utils.SendJSONResponse(w, string(js))
}
// Wrapper for starting and stopping the SSO portal server
// require POST request with key "enable" and value "true" or "false"
func (s *SSOHandler) HandleSSOEnable(w http.ResponseWriter, r *http.Request) {
enable, err := utils.PostBool(r, "enable")
if err != nil {
utils.SendErrorResponse(w, "invalid enable value")
return
}
if enable {
s.HandleStartSSOPortal(w, r)
} else {
s.HandleStopSSOPortal(w, r)
}
}
// HandleStartSSOPortal handle the request to start the SSO portal server
func (s *SSOHandler) HandleStartSSOPortal(w http.ResponseWriter, r *http.Request) {
if s.ssoPortalServer != nil {
//Already enabled. Do restart instead.
err := s.RestartSSOServer()
if err != nil {
utils.SendErrorResponse(w, "failed to start SSO server")
return
}
utils.SendOK(w)
return
}
//Check if the authURL is set correctly. If not, return error
if s.Config.AuthURL == "" {
utils.SendErrorResponse(w, "auth URL not set")
return
}
//Start the SSO portal server in go routine
go s.StartSSOPortal()
//Write current state to database
err := s.Config.Database.Write("sso_conf", "enabled", true)
if err != nil {
utils.SendErrorResponse(w, "failed to update SSO state")
return
}
utils.SendOK(w)
}
// HandleStopSSOPortal handle the request to stop the SSO portal server
func (s *SSOHandler) HandleStopSSOPortal(w http.ResponseWriter, r *http.Request) {
if s.ssoPortalServer == nil {
//Already disabled
utils.SendOK(w)
return
}
err := s.ssoPortalServer.Close()
if err != nil {
s.Log("Failed to stop SSO portal server", err)
utils.SendErrorResponse(w, "failed to stop SSO portal server")
return
}
s.ssoPortalServer = nil
//Write current state to database
err = s.Config.Database.Write("sso_conf", "enabled", false)
if err != nil {
utils.SendErrorResponse(w, "failed to update SSO state")
return
}
utils.SendOK(w)
}
// HandlePortChange handle the request to change the SSO portal server port
func (s *SSOHandler) HandlePortChange(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current port
js, _ := json.Marshal(s.Config.PortalServerPort)
utils.SendJSONResponse(w, string(js))
return
}
port, err := utils.PostInt(r, "port")
if err != nil {
utils.SendErrorResponse(w, "invalid port given")
return
}
s.Config.PortalServerPort = port
//Write to the database
err = s.Config.Database.Write("sso_conf", "port", port)
if err != nil {
utils.SendErrorResponse(w, "failed to update port")
return
}
if s.IsRunning() {
//Restart the server if it is running
err = s.RestartSSOServer()
if err != nil {
utils.SendErrorResponse(w, "failed to restart SSO server")
return
}
}
utils.SendOK(w)
}
// HandleSetAuthURL handle the request to change the SSO auth URL
// This is the URL that the SSO portal server will redirect to for authentication
// e.g. auth.yourdomain.com
func (s *SSOHandler) HandleSetAuthURL(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current auth URL
js, _ := json.Marshal(s.Config.AuthURL)
utils.SendJSONResponse(w, string(js))
return
}
//Get the auth URL
authURL, err := utils.PostPara(r, "auth_url")
if err != nil {
utils.SendErrorResponse(w, "invalid auth URL given")
return
}
s.Config.AuthURL = authURL
//Write to the database
err = s.Config.Database.Write("sso_conf", "authurl", authURL)
if err != nil {
utils.SendErrorResponse(w, "failed to update auth URL")
return
}
//Clear the cookie store and restart the server
err = s.RestartSSOServer()
if err != nil {
utils.SendErrorResponse(w, "failed to restart SSO server")
return
}
utils.SendOK(w)
}
// HandleRegisterApp handle the request to register a new app to the SSO portal
func (s *SSOHandler) HandleRegisterApp(w http.ResponseWriter, r *http.Request) {
appName, err := utils.PostPara(r, "app_name")
if err != nil {
utils.SendErrorResponse(w, "invalid app name given")
return
}
id, err := utils.PostPara(r, "app_id")
if err != nil {
//If id is not given, use the app name with a random UUID
newID, err := uuid.NewV4()
if err != nil {
utils.SendErrorResponse(w, "failed to generate new app ID")
return
}
id = strings.ReplaceAll(appName, " ", "") + "-" + newID.String()
}
//Check if the given appid is already in use
if _, ok := s.Apps[id]; ok {
utils.SendErrorResponse(w, "app ID already in use")
return
}
/*
Process the app domain
An app can have multiple domains, separated by commas
Usually the app domain is the proxy rule that points to the app
For example, if the app is hosted at app.yourdomain.com, the app domain is app.yourdomain.com
*/
appDomain, err := utils.PostPara(r, "app_domain")
if err != nil {
utils.SendErrorResponse(w, "invalid app URL given")
return
}
appURLs := strings.Split(appDomain, ",")
//Remove padding and trailing spaces in each URL
for i := range appURLs {
appURLs[i] = strings.TrimSpace(appURLs[i])
}
//Create a new app entry
thisAppEntry := RegisteredUpstreamApp{
ID: id,
Secret: "",
Domain: appURLs,
Scopes: []string{},
SessionDuration: 3600,
}
js, _ := json.Marshal(thisAppEntry)
//Create a new app in the database
err = s.Config.Database.Write("sso_apps", appName, string(js))
if err != nil {
utils.SendErrorResponse(w, "failed to create new app")
return
}
//Also add the app to runtime config
s.Apps[appName] = thisAppEntry
utils.SendOK(w)
}
// HandleAppRemove handle the request to remove an app from the SSO portal
func (s *SSOHandler) HandleAppRemove(w http.ResponseWriter, r *http.Request) {
appID, err := utils.PostPara(r, "app_id")
if err != nil {
utils.SendErrorResponse(w, "invalid app ID given")
return
}
//Check if the app actually exists
if _, ok := s.Apps[appID]; !ok {
utils.SendErrorResponse(w, "app not found")
return
}
delete(s.Apps, appID)
//Also remove it from the database
err = s.Config.Database.Delete("sso_apps", appID)
if err != nil {
s.Log("Failed to remove app from database", err)
}
}

295
src/mod/auth/sso/oauth2.go Normal file
View File

@ -0,0 +1,295 @@
package sso
import (
"context"
_ "embed"
"encoding/json"
"log"
"net/http"
"net/url"
"time"
"github.com/go-oauth2/oauth2/v4/errors"
"github.com/go-oauth2/oauth2/v4/generates"
"github.com/go-oauth2/oauth2/v4/manage"
"github.com/go-oauth2/oauth2/v4/models"
"github.com/go-oauth2/oauth2/v4/server"
"github.com/go-oauth2/oauth2/v4/store"
"github.com/go-session/session"
"imuslab.com/zoraxy/mod/utils"
)
const (
SSO_SESSION_NAME = "ZoraxySSO"
)
type OAuth2Server struct {
srv *server.Server //oAuth server instance
config *SSOConfig
parent *SSOHandler
}
//go:embed static/auth.html
var authHtml []byte
//go:embed static/login.html
var loginHtml []byte
// NewOAuth2Server creates a new OAuth2 server instance
func NewOAuth2Server(config *SSOConfig, parent *SSOHandler) (*OAuth2Server, error) {
manager := manage.NewDefaultManager()
manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
// token store
manager.MustTokenStorage(store.NewFileTokenStore("./conf/sso.db"))
// generate jwt access token
manager.MapAccessGenerate(generates.NewAccessGenerate())
//Load the information of registered app within the OAuth2 server
clientStore := store.NewClientStore()
clientStore.Set("myapp", &models.Client{
ID: "myapp",
Secret: "verysecurepassword",
Domain: "localhost:9094",
})
//TODO: LOAD THIS DYNAMICALLY FROM DATABASE
manager.MapClientStorage(clientStore)
thisServer := OAuth2Server{
config: config,
parent: parent,
}
//Create a new oauth server
srv := server.NewServer(server.NewConfig(), manager)
srv.SetPasswordAuthorizationHandler(thisServer.PasswordAuthorizationHandler)
srv.SetUserAuthorizationHandler(thisServer.UserAuthorizeHandler)
srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
log.Println("Internal Error:", err.Error())
return
})
srv.SetResponseErrorHandler(func(re *errors.Response) {
log.Println("Response Error:", re.Error.Error())
})
//Set the access scope handler
srv.SetAuthorizeScopeHandler(thisServer.AuthorizationScopeHandler)
//Set the access token expiration handler based on requesting domain / hostname
srv.SetAccessTokenExpHandler(thisServer.ExpireHandler)
thisServer.srv = srv
return &thisServer, nil
}
// Password handler, validate if the given username and password are correct
func (oas *OAuth2Server) PasswordAuthorizationHandler(ctx context.Context, clientID, username, password string) (userID string, err error) {
//TODO: LOAD THIS DYNAMICALLY FROM DATABASE
if username == "test" && password == "test" {
userID = "test"
}
return
}
// User Authorization Handler, handle auth request from user
func (oas *OAuth2Server) UserAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string, err error) {
store, err := session.Start(r.Context(), w, r)
if err != nil {
return
}
uid, ok := store.Get(SSO_SESSION_NAME)
if !ok {
if r.Form == nil {
r.ParseForm()
}
store.Set("ReturnUri", r.Form)
store.Save()
w.Header().Set("Location", "/oauth2/login")
w.WriteHeader(http.StatusFound)
return
}
userID = uid.(string)
store.Delete(SSO_SESSION_NAME)
store.Save()
return
}
// AccessTokenExpHandler, set the SSO session length default value
func (oas *OAuth2Server) ExpireHandler(w http.ResponseWriter, r *http.Request) (exp time.Duration, err error) {
requestHostname := r.Host
if requestHostname == "" {
//Use default value
return time.Hour, nil
}
//Get the Registered App Config from parent
appConfig, ok := oas.parent.Apps[requestHostname]
if !ok {
//Use default value
return time.Hour, nil
}
//Use the app's session length
return time.Second * time.Duration(appConfig.SessionDuration), nil
}
// AuthorizationScopeHandler, handle the scope of the request
func (oas *OAuth2Server) AuthorizationScopeHandler(w http.ResponseWriter, r *http.Request) (scope string, err error) {
//Get the scope from post or GEt request
if r.Form == nil {
if err := r.ParseForm(); err != nil {
return "none", err
}
}
//Get the hostname of the request
requestHostname := r.Host
if requestHostname == "" {
//No rule set. Use default
return "none", nil
}
//Get the Registered App Config from parent
appConfig, ok := oas.parent.Apps[requestHostname]
if !ok {
//No rule set. Use default
return "none", nil
}
//Check if the scope is set in the request
if v, ok := r.Form["scope"]; ok {
//Check if the requested scope is in the appConfig scope
if utils.StringInArray(appConfig.Scopes, v[0]) {
return v[0], nil
}
return "none", nil
}
return "none", nil
}
/* SSO Web Server Toggle Functions */
func (oas *OAuth2Server) RegisterOauthEndpoints(primaryMux *http.ServeMux) {
primaryMux.HandleFunc("/oauth2/login", oas.loginHandler)
primaryMux.HandleFunc("/oauth2/auth", oas.authHandler)
primaryMux.HandleFunc("/oauth2/authorize", func(w http.ResponseWriter, r *http.Request) {
store, err := session.Start(r.Context(), w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var form url.Values
if v, ok := store.Get("ReturnUri"); ok {
form = v.(url.Values)
}
r.Form = form
store.Delete("ReturnUri")
store.Save()
err = oas.srv.HandleAuthorizeRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
})
primaryMux.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) {
err := oas.srv.HandleTokenRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
primaryMux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
token, err := oas.srv.ValidationBearerToken(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
data := map[string]interface{}{
"expires_in": int64(time.Until(token.GetAccessCreateAt().Add(token.GetAccessExpiresIn())).Seconds()),
"client_id": token.GetClientID(),
"user_id": token.GetUserID(),
}
e := json.NewEncoder(w)
e.SetIndent("", " ")
e.Encode(data)
})
}
func (oas *OAuth2Server) loginHandler(w http.ResponseWriter, r *http.Request) {
store, err := session.Start(r.Context(), w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if r.Method == "POST" {
if r.Form == nil {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
//Load username and password from form post
username, err := utils.PostPara(r, "username")
if err != nil {
w.Write([]byte("invalid username or password"))
return
}
password, err := utils.PostPara(r, "password")
if err != nil {
w.Write([]byte("invalid username or password"))
return
}
//Validate the user
if !oas.parent.ValidateUsernameAndPassword(username, password) {
//Wrong password
w.Write([]byte("invalid username or password"))
return
}
store.Set(SSO_SESSION_NAME, r.Form.Get("username"))
store.Save()
w.Header().Set("Location", "/oauth2/auth")
w.WriteHeader(http.StatusFound)
return
} else if r.Method == "GET" {
//Check if the user is logged in
if _, ok := store.Get(SSO_SESSION_NAME); ok {
w.Header().Set("Location", "/oauth2/auth")
w.WriteHeader(http.StatusFound)
return
}
}
//User not logged in. Show login page
w.Write(loginHtml)
}
func (oas *OAuth2Server) authHandler(w http.ResponseWriter, r *http.Request) {
store, err := session.Start(context.TODO(), w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, ok := store.Get(SSO_SESSION_NAME); !ok {
w.Header().Set("Location", "/oauth2/login")
w.WriteHeader(http.StatusFound)
return
}
//User logged in. Check if this user have previously authorized the app
//TODO: Check if the user have previously authorized the app
//User have not authorized the app. Show the authorization page
w.Write(authHtml)
}

View File

@ -0,0 +1 @@
package sso

View File

@ -0,0 +1,58 @@
package sso
import (
"encoding/json"
"net/http"
"strings"
)
type OpenIDConfiguration struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
JwksUri string `json:"jwks_uri"`
ResponseTypesSupported []string `json:"response_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
ClaimsSupported []string `json:"claims_supported"`
}
func (h *SSOHandler) HandleDiscoveryRequest(w http.ResponseWriter, r *http.Request) {
//Prepend https:// if not present
authBaseURL := h.Config.AuthURL
if !strings.HasPrefix(authBaseURL, "http://") && !strings.HasPrefix(authBaseURL, "https://") {
authBaseURL = "https://" + authBaseURL
}
//Handle the discovery request
discovery := OpenIDConfiguration{
Issuer: authBaseURL,
AuthorizationEndpoint: authBaseURL + "/oauth2/authorize",
TokenEndpoint: authBaseURL + "/oauth2/token",
JwksUri: authBaseURL + "/jwks.json",
ResponseTypesSupported: []string{"code", "token"},
SubjectTypesSupported: []string{"public"},
IDTokenSigningAlgValuesSupported: []string{
"RS256",
},
ClaimsSupported: []string{
"sub", //Subject, usually the user ID
"iss", //Issuer, usually the server URL
"aud", //Audience, usually the client ID
"exp", //Expiration Time
"iat", //Issued At
"email", //Email
"locale", //Locale
"name", //Full Name
"nickname", //Nickname
"preferred_username", //Preferred Username
"website", //Website
},
}
//Write the response
js, _ := json.Marshal(discovery)
w.Header().Set("Content-Type", "application/json")
w.Write(js)
}

132
src/mod/auth/sso/server.go Normal file
View File

@ -0,0 +1,132 @@
package sso
import (
"context"
"net/http"
"strconv"
"time"
"github.com/go-oauth2/oauth2/v4/errors"
"imuslab.com/zoraxy/mod/utils"
)
/*
server.go
This is the web server for the SSO portal. It contains the
HTTP server and the handlers for the SSO portal.
If you are looking for handlers that changes the settings
of the SSO portale or user management, please refer to
handlers.go.
*/
func (h *SSOHandler) InitSSOPortal(portalServerPort int) {
//Create a new web server for the SSO portal
pmux := http.NewServeMux()
fs := http.FileServer(http.FS(staticFiles))
pmux.Handle("/", fs)
//Register API endpoint for the SSO portal
pmux.HandleFunc("/sso/login", h.HandleLogin)
//Register API endpoint for autodiscovery
pmux.HandleFunc("/.well-known/openid-configuration", h.HandleDiscoveryRequest)
//Register OAuth2 endpoints
h.Oauth2Server.RegisterOauthEndpoints(pmux)
h.ssoPortalMux = pmux
}
// StartSSOPortal start the SSO portal server
// This function will block the main thread, call it in a goroutine
func (h *SSOHandler) StartSSOPortal() error {
if h.ssoPortalServer != nil {
return errors.New("SSO portal server already running")
}
h.ssoPortalServer = &http.Server{
Addr: ":" + strconv.Itoa(h.Config.PortalServerPort),
Handler: h.ssoPortalMux,
}
err := h.ssoPortalServer.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
h.Log("Failed to start SSO portal server", err)
}
return err
}
// StopSSOPortal stop the SSO portal server
func (h *SSOHandler) StopSSOPortal() error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := h.ssoPortalServer.Shutdown(ctx)
if err != nil {
h.Log("Failed to stop SSO portal server", err)
return err
}
h.ssoPortalServer = nil
return nil
}
// StartSSOPortal start the SSO portal server
func (h *SSOHandler) RestartSSOServer() error {
if h.ssoPortalServer != nil {
err := h.StopSSOPortal()
if err != nil {
return err
}
}
go h.StartSSOPortal()
return nil
}
func (h *SSOHandler) IsRunning() bool {
return h.ssoPortalServer != nil
}
// HandleLogin handle the login request
func (h *SSOHandler) HandleLogin(w http.ResponseWriter, r *http.Request) {
//Handle the login request
username, err := utils.PostPara(r, "username")
if err != nil {
utils.SendErrorResponse(w, "invalid username or password")
return
}
password, err := utils.PostPara(r, "password")
if err != nil {
utils.SendErrorResponse(w, "invalid username or password")
return
}
rememberMe, err := utils.PostBool(r, "remember_me")
if err != nil {
rememberMe = false
}
//Check if the user exists
userEntry, err := h.GetSSOUser(username)
if err != nil {
utils.SendErrorResponse(w, "user not found")
return
}
//Check if the password is correct
if !userEntry.VerifyPassword(password) {
utils.SendErrorResponse(w, "incorrect password")
return
}
//Create a new session for the user
session, _ := h.cookieStore.Get(r, "Zoraxy-SSO")
session.Values["username"] = username
if rememberMe {
session.Options.MaxAge = 86400 * 15 //15 days
} else {
session.Options.MaxAge = 3600 //1 hour
}
session.Save(r, w) //Save the session
utils.SendOK(w)
}

158
src/mod/auth/sso/sso.go Normal file
View File

@ -0,0 +1,158 @@
package sso
import (
"embed"
"net/http"
"github.com/gorilla/sessions"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
)
/*
sso.go
This file contains the main SSO handler and the SSO configuration
structure. It also contains the main SSO handler functions.
SSO web interface are stored in the static folder, which is embedded
into the binary.
*/
//go:embed static/*
var staticFiles embed.FS //Static files for the SSO portal
type SSOConfig struct {
SystemUUID string //System UUID, should be passed in from main scope
AuthURL string //Authentication subdomain URL, e.g. auth.example.com
PortalServerPort int //SSO portal server port
Database *database.Database //System master key-value database
Logger *logger.Logger
}
// SSOHandler is the main SSO handler structure
type SSOHandler struct {
cookieStore *sessions.CookieStore
ssoPortalServer *http.Server
ssoPortalMux *http.ServeMux
Oauth2Server *OAuth2Server
Config *SSOConfig
Apps map[string]RegisteredUpstreamApp
}
// Create a new Zoraxy SSO handler
func NewSSOHandler(config *SSOConfig) (*SSOHandler, error) {
//Create a cookie store for the SSO handler
cookieStore := sessions.NewCookieStore([]byte(config.SystemUUID))
cookieStore.Options = &sessions.Options{
Path: "",
Domain: "",
MaxAge: 0,
Secure: false,
HttpOnly: false,
SameSite: 0,
}
config.Database.NewTable("sso_users") //For storing user information
config.Database.NewTable("sso_conf") //For storing SSO configuration
config.Database.NewTable("sso_apps") //For storing registered apps
//Create the SSO Handler
thisHandler := SSOHandler{
cookieStore: cookieStore,
Config: config,
}
//Read the app info from database
thisHandler.Apps = make(map[string]RegisteredUpstreamApp)
//Create an oauth2 server
oauth2Server, err := NewOAuth2Server(config, &thisHandler)
if err != nil {
return nil, err
}
//Register endpoints
thisHandler.Oauth2Server = oauth2Server
thisHandler.InitSSOPortal(config.PortalServerPort)
return &thisHandler, nil
}
func (h *SSOHandler) RestorePreviousRunningState() {
//Load the previous SSO state
ssoEnabled := false
ssoPort := 5488
ssoAuthURL := ""
h.Config.Database.Read("sso_conf", "enabled", &ssoEnabled)
h.Config.Database.Read("sso_conf", "port", &ssoPort)
h.Config.Database.Read("sso_conf", "authurl", &ssoAuthURL)
if ssoAuthURL == "" {
//Cannot enable SSO without auth URL
ssoEnabled = false
}
h.Config.PortalServerPort = ssoPort
h.Config.AuthURL = ssoAuthURL
if ssoEnabled {
go h.StartSSOPortal()
}
}
// ServeForwardAuth handle the SSO request in interception mode
// Suppose to be called in dynamicproxy.
// Return true if the request is allowed to pass, false if the request is blocked and shall not be further processed
func (h *SSOHandler) ServeForwardAuth(w http.ResponseWriter, r *http.Request) bool {
//Get the current uri for appending to the auth subdomain
originalRequestURL := r.RequestURI
redirectAuthURL := h.Config.AuthURL
if redirectAuthURL == "" || !h.IsRunning() {
//Redirect not set or auth server is offlined
w.Write([]byte("SSO auth URL not set or SSO server offline."))
//TODO: Use better looking template if exists
return false
}
//Check if the user have the cookie "Zoraxy-SSO" set
session, err := h.cookieStore.Get(r, "Zoraxy-SSO")
if err != nil {
//Redirect to auth subdomain
http.Redirect(w, r, redirectAuthURL+"/sso/login?m=new&t="+originalRequestURL, http.StatusFound)
return false
}
//Check if the user is logged in
if session.Values["username"] != true {
//Redirect to auth subdomain
http.Redirect(w, r, redirectAuthURL+"/sso/login?m=expired&t="+originalRequestURL, http.StatusFound)
return false
}
//Check if the current request subdomain is allowed
userName := session.Values["username"].(string)
user, err := h.GetSSOUser(userName)
if err != nil {
//User might have been removed from SSO. Redirect to auth subdomain
http.Redirect(w, r, redirectAuthURL, http.StatusFound)
return false
}
//Check if the user have access to the current subdomain
if !user.Subdomains[r.Host].AllowAccess {
//User is not allowed to access the current subdomain. Sent 403
http.Error(w, "Forbidden", http.StatusForbidden)
//TODO: Use better looking template if exists
return false
}
//User is logged in, continue to the next handler
return true
}
// Log a message with the SSO module tag
func (h *SSOHandler) Log(message string, err error) {
h.Config.Logger.PrintAndLog("SSO", message, err)
}

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Auth</title>
<link
rel="stylesheet"
href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
/>
<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<div class="jumbotron">
<form action="/oauth2/authorize" method="POST">
<h1>Authorize</h1>
<p>The client would like to perform actions on your behalf.</p>
<p>
<button
type="submit"
class="btn btn-primary btn-lg"
style="width:200px;"
>
Allow
</button>
</p>
</form>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<title>Login Page</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
</head>
<body>
<div class="ui container">
<div class="ui middle aligned center aligned grid">
<div class="column">
<h2 class="ui teal image header">
<div class="content">
Log in to your account
</div>
</h2>
<form class="ui large form">
<div class="ui stacked segment">
<div class="field">
<div class="ui left icon input">
<i class="user icon"></i>
<input type="text" name="username" placeholder="Username">
</div>
</div>
<div class="field">
<div class="ui left icon input">
<i class="lock icon"></i>
<input type="password" name="password" placeholder="Password">
</div>
</div>
<div class="ui fluid large teal submit button">Login</div>
</div>
<div class="ui error message"></div>
</form>
<div class="ui message">
New to us? <a href="#">Sign Up</a>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<h1>Login In</h1>
<form action="/oauth2/login" method="POST">
<div class="form-group">
<label for="username">User Name</label>
<input type="text" class="form-control" name="username" required placeholder="Please enter your user name">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" placeholder="Please enter your password">
</div>
<button type="submit" class="btn btn-success">Login</button>
</form>
</div>
</body>
</html>

View File

@ -0,0 +1,309 @@
package sso
/*
userHandlers.go
Handlers for SSO user management
If you are looking for handlers that changes the settings
of the SSO portal (e.g. authURL or port), please refer to
handlers.go.
*/
import (
"encoding/json"
"errors"
"net/http"
"github.com/gofrs/uuid"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/utils"
)
// HandleAddUser handle the request to add a new user to the SSO system
func (s *SSOHandler) HandleAddUser(w http.ResponseWriter, r *http.Request) {
username, err := utils.PostPara(r, "username")
if err != nil {
utils.SendErrorResponse(w, "invalid username given")
return
}
password, err := utils.PostPara(r, "password")
if err != nil {
utils.SendErrorResponse(w, "invalid password given")
return
}
newUserId, err := uuid.NewV4()
if err != nil {
utils.SendErrorResponse(w, "failed to generate new user ID")
return
}
//Create a new user entry
thisUserEntry := UserEntry{
UserID: newUserId.String(),
Username: username,
PasswordHash: auth.Hash(password),
TOTPCode: "",
Enable2FA: false,
}
js, _ := json.Marshal(thisUserEntry)
//Create a new user in the database
err = s.Config.Database.Write("sso_users", newUserId.String(), string(js))
if err != nil {
utils.SendErrorResponse(w, "failed to create new user")
return
}
utils.SendOK(w)
}
// Edit user information, only accept change of username, password and enabled subdomain filed
func (s *SSOHandler) HandleEditUser(w http.ResponseWriter, r *http.Request) {
userID, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userID)) {
utils.SendErrorResponse(w, "user not found")
return
}
//Load the user entry from database
userEntry, err := s.GetSSOUser(userID)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return
}
//Update each of the fields if it is provided
username, err := utils.PostPara(r, "username")
if err == nil {
userEntry.Username = username
}
password, err := utils.PostPara(r, "password")
if err == nil {
userEntry.PasswordHash = auth.Hash(password)
}
//Update the user entry in the database
js, _ := json.Marshal(userEntry)
err = s.Config.Database.Write("sso_users", userID, string(js))
if err != nil {
utils.SendErrorResponse(w, "failed to update user entry")
return
}
utils.SendOK(w)
}
// HandleRemoveUser remove a user from the SSO system
func (s *SSOHandler) HandleRemoveUser(w http.ResponseWriter, r *http.Request) {
userID, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userID)) {
utils.SendErrorResponse(w, "user not found")
return
}
//Remove the user from the database
err = s.Config.Database.Delete("sso_users", userID)
if err != nil {
utils.SendErrorResponse(w, "failed to remove user")
return
}
utils.SendOK(w)
}
// HandleListUser list all users in the SSO system
func (s *SSOHandler) HandleListUser(w http.ResponseWriter, r *http.Request) {
ssoUsers, err := s.ListSSOUsers()
if err != nil {
utils.SendErrorResponse(w, "failed to list users")
return
}
js, _ := json.Marshal(ssoUsers)
utils.SendJSONResponse(w, string(js))
}
// HandleAddSubdomain add a subdomain to a user
func (s *SSOHandler) HandleAddSubdomain(w http.ResponseWriter, r *http.Request) {
userid, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userid)) {
utils.SendErrorResponse(w, "user not found")
return
}
UserEntry, err := s.GetSSOUser(userid)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return
}
subdomain, err := utils.PostPara(r, "subdomain")
if err != nil {
utils.SendErrorResponse(w, "invalid subdomain given")
return
}
allowAccess, err := utils.PostBool(r, "allow_access")
if err != nil {
utils.SendErrorResponse(w, "invalid allow access value given")
return
}
UserEntry.Subdomains[subdomain] = &SubdomainAccessRule{
Subdomain: subdomain,
AllowAccess: allowAccess,
}
err = UserEntry.Update()
if err != nil {
utils.SendErrorResponse(w, "failed to update user entry")
return
}
utils.SendOK(w)
}
// HandleRemoveSubdomain remove a subdomain from a user
func (s *SSOHandler) HandleRemoveSubdomain(w http.ResponseWriter, r *http.Request) {
userid, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userid)) {
utils.SendErrorResponse(w, "user not found")
return
}
UserEntry, err := s.GetSSOUser(userid)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return
}
subdomain, err := utils.PostPara(r, "subdomain")
if err != nil {
utils.SendErrorResponse(w, "invalid subdomain given")
return
}
delete(UserEntry.Subdomains, subdomain)
err = UserEntry.Update()
if err != nil {
utils.SendErrorResponse(w, "failed to update user entry")
return
}
utils.SendOK(w)
}
// HandleEnable2FA enable 2FA for a user
func (s *SSOHandler) HandleEnable2FA(w http.ResponseWriter, r *http.Request) {
userid, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userid)) {
utils.SendErrorResponse(w, "user not found")
return
}
UserEntry, err := s.GetSSOUser(userid)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return
}
UserEntry.Enable2FA = true
provisionUri, err := UserEntry.ResetTotp(UserEntry.UserID, "Zoraxy-SSO")
if err != nil {
utils.SendErrorResponse(w, "failed to reset TOTP")
return
}
//As the ResetTotp function will update the user entry in the database, no need to call Update here
js, _ := json.Marshal(provisionUri)
utils.SendJSONResponse(w, string(js))
}
// Handle Disable 2FA for a user
func (s *SSOHandler) HandleDisable2FA(w http.ResponseWriter, r *http.Request) {
userid, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userid)) {
utils.SendErrorResponse(w, "user not found")
return
}
UserEntry, err := s.GetSSOUser(userid)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return
}
UserEntry.Enable2FA = false
UserEntry.TOTPCode = ""
err = UserEntry.Update()
if err != nil {
utils.SendErrorResponse(w, "failed to update user entry")
return
}
utils.SendOK(w)
}
// HandleVerify2FA verify the 2FA code for a user
func (s *SSOHandler) HandleVerify2FA(w http.ResponseWriter, r *http.Request) (bool, error) {
userid, err := utils.PostPara(r, "user_id")
if err != nil {
return false, errors.New("invalid user ID given")
}
if !(s.SSOUserExists(userid)) {
utils.SendErrorResponse(w, "user not found")
return false, errors.New("user not found")
}
UserEntry, err := s.GetSSOUser(userid)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return false, errors.New("failed to load user entry")
}
totpCode, _ := utils.PostPara(r, "totp_code")
if !UserEntry.Enable2FA {
//If 2FA is not enabled, return true
return true, nil
}
if !UserEntry.VerifyTotp(totpCode) {
return false, nil
}
return true, nil
}

141
src/mod/auth/sso/users.go Normal file
View File

@ -0,0 +1,141 @@
package sso
import (
"encoding/json"
"time"
"github.com/xlzd/gotp"
"imuslab.com/zoraxy/mod/auth"
)
/*
users.go
This file contains the user structure and user management
functions for the SSO module.
If you are looking for handlers, please refer to handlers.go.
*/
type SubdomainAccessRule struct {
Subdomain string
AllowAccess bool
}
type UserEntry struct {
UserID string `json:sub` //User ID
Username string `json:"name"` //Username
Email string `json:"email"` //Email
PasswordHash string `json:"passwordhash"` //Password hash
TOTPCode string `json:"totpcode"` //TOTP code
Enable2FA bool `json:"enable2fa"` //Enable 2FA
Subdomains map[string]*SubdomainAccessRule `json:"subdomains"` //Subdomain access rules
LastLogin int64 `json:"lastlogin"` //Last login time
LastLoginIP string `json:"lastloginip"` //Last login IP
LastLoginCountry string `json:"lastlogincountry"` //Last login country
parent *SSOHandler //Parent SSO handler
}
type ClientResponse struct {
Sub string `json:"sub"` //User ID
Name string `json:"name"` //Username
Nickname string `json:"nickname"` //Nickname
PreferredUsername string `json:"preferred_username"` //Preferred Username
Email string `json:"email"` //Email
Locale string `json:"locale"` //Locale
Website string `json:"website"` //Website
}
func (s *SSOHandler) SSOUserExists(userid string) bool {
//Check if the user exists in the database
var userEntry UserEntry
err := s.Config.Database.Read("sso_users", userid, &userEntry)
return err == nil
}
func (s *SSOHandler) GetSSOUser(userid string) (UserEntry, error) {
//Load the user entry from database
var userEntry UserEntry
err := s.Config.Database.Read("sso_users", userid, &userEntry)
if err != nil {
return UserEntry{}, err
}
userEntry.parent = s
return userEntry, nil
}
func (s *SSOHandler) ListSSOUsers() ([]*UserEntry, error) {
entries, err := s.Config.Database.ListTable("sso_users")
if err != nil {
return nil, err
}
ssoUsers := []*UserEntry{}
for _, keypairs := range entries {
group := new(UserEntry)
json.Unmarshal(keypairs[1], &group)
group.parent = s
ssoUsers = append(ssoUsers, group)
}
return ssoUsers, nil
}
// Validate the username and password
func (s *SSOHandler) ValidateUsernameAndPassword(username string, password string) bool {
//Validate the username and password
var userEntry UserEntry
err := s.Config.Database.Read("sso_users", username, &userEntry)
if err != nil {
return false
}
//TODO: Remove after testing
if (username == "test") && (password == "test") {
return true
}
return userEntry.VerifyPassword(password)
}
func (s *UserEntry) VerifyPassword(password string) bool {
return s.PasswordHash == auth.Hash(password)
}
// Write changes in the user entry back to the database
func (u *UserEntry) Update() error {
js, _ := json.Marshal(u)
err := u.parent.Config.Database.Write("sso_users", u.UserID, string(js))
if err != nil {
return err
}
return nil
}
// Reset and update the TOTP code for the current user
// Return the provision uri of the new TOTP code for Google Authenticator
func (u *UserEntry) ResetTotp(accountName string, issuerName string) (string, error) {
u.TOTPCode = gotp.RandomSecret(16)
totp := gotp.NewDefaultTOTP(u.TOTPCode)
err := u.Update()
if err != nil {
return "", err
}
return totp.ProvisioningUri(accountName, issuerName), nil
}
// Verify the TOTP code at current time
func (u *UserEntry) VerifyTotp(enteredCode string) bool {
totp := gotp.NewDefaultTOTP(u.TOTPCode)
return totp.Verify(enteredCode, time.Now().Unix())
}
func (u *UserEntry) GetClientResponse() ClientResponse {
return ClientResponse{
Sub: u.UserID,
Name: u.Username,
Nickname: u.Username,
PreferredUsername: u.Username,
Email: u.Email,
Locale: "en",
Website: "",
}
}

View File

@ -21,6 +21,7 @@ import (
- Blacklist - Blacklist
- Whitelist - Whitelist
- Rate Limitor - Rate Limitor
- SSO Auth
- Basic Auth - Basic Auth
- Vitrual Directory Proxy - Vitrual Directory Proxy
- Subdomain Proxy - Subdomain Proxy
@ -77,7 +78,16 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if sep.RequireRateLimit { if sep.RequireRateLimit {
err := h.handleRateLimitRouting(w, r, sep) err := h.handleRateLimitRouting(w, r, sep)
if err != nil { if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 429) h.Parent.Option.Logger.LogHTTPRequest(r, "host", 307)
return
}
}
//SSO Interception Mode
if sep.UseSSOIntercept {
allowPass := h.Parent.Option.SSOHandler.ServeForwardAuth(w, r)
if !allowPass {
h.Parent.Option.Logger.LogHTTPRequest(r, "sso-x", 307)
return return
} }
} }
@ -163,7 +173,6 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
fallthrough fallthrough
case DefaultSite_ReverseProxy: case DefaultSite_ReverseProxy:
//They both share the same behavior //They both share the same behavior
//Check if any virtual directory rules matches //Check if any virtual directory rules matches
proxyingPath := strings.TrimSpace(r.RequestURI) proxyingPath := strings.TrimSpace(r.RequestURI)
targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath) targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
@ -188,8 +197,13 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
redirectTarget = "about:blank" redirectTarget = "about:blank"
} }
//Check if the default site values start with http or https
if !strings.HasPrefix(redirectTarget, "http://") && !strings.HasPrefix(redirectTarget, "https://") {
redirectTarget = "http://" + redirectTarget
}
//Check if it is an infinite loopback redirect //Check if it is an infinite loopback redirect
parsedURL, err := url.Parse(proot.DefaultSiteValue) parsedURL, err := url.Parse(redirectTarget)
if err != nil { if err != nil {
//Error when parsing target. Send to root //Error when parsing target. Send to root
h.hostRequest(w, r, h.Parent.Root) h.hostRequest(w, r, h.Parent.Root)

View File

@ -49,6 +49,9 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint)
for _, cred := range pe.BasicAuthCredentials { for _, cred := range pe.BasicAuthCredentials {
if u == cred.Username && hashedPassword == cred.PasswordHash { if u == cred.Username && hashedPassword == cred.PasswordHash {
matchingFound = true matchingFound = true
//Set the X-Remote-User header
r.Header.Set("X-Remote-User", u)
break break
} }
} }

View File

@ -1,80 +1,12 @@
package dynamicproxy package dynamicproxy
import (
"strconv"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
)
/* /*
CustomHeader.go CustomHeader.go
This script handle parsing and injecting custom headers This script handle parsing and injecting custom headers
into the dpcore routing logic into the dpcore routing logic
Updates: 2024-10-26
Contents from this file has been moved to rewrite/rewrite.go
This file is kept for contributors to understand the structure
*/ */
// SplitInboundOutboundHeaders split user defined headers into upstream and downstream headers
// return upstream header and downstream header key-value pairs
// if the header is expected to be deleted, the value will be set to empty string
func (ept *ProxyEndpoint) SplitInboundOutboundHeaders() ([][]string, [][]string) {
if len(ept.UserDefinedHeaders) == 0 && ept.HSTSMaxAge == 0 && !ept.EnablePermissionPolicyHeader {
//Early return if there are no defined headers
return [][]string{}, [][]string{}
}
//Use pre-allocation for faster performance
//Downstream +2 for Permission Policy and HSTS
upstreamHeaders := make([][]string, len(ept.UserDefinedHeaders))
downstreamHeaders := make([][]string, len(ept.UserDefinedHeaders)+2)
upstreamHeaderCounter := 0
downstreamHeaderCounter := 0
//Sort the headers into upstream or downstream
for _, customHeader := range ept.UserDefinedHeaders {
thisHeaderSet := make([]string, 2)
thisHeaderSet[0] = customHeader.Key
thisHeaderSet[1] = customHeader.Value
if customHeader.IsRemove {
//Prevent invalid config
thisHeaderSet[1] = ""
}
//Assign to slice
if customHeader.Direction == HeaderDirection_ZoraxyToUpstream {
upstreamHeaders[upstreamHeaderCounter] = thisHeaderSet
upstreamHeaderCounter++
} else if customHeader.Direction == HeaderDirection_ZoraxyToDownstream {
downstreamHeaders[downstreamHeaderCounter] = thisHeaderSet
downstreamHeaderCounter++
}
}
//Check if the endpoint require HSTS headers
if ept.HSTSMaxAge > 0 {
if ept.ContainsWildcardName(true) {
//Endpoint listening domain includes wildcards.
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(ept.HSTSMaxAge)) + "; includeSubdomains"}
} else {
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(ept.HSTSMaxAge))}
}
downstreamHeaderCounter++
}
//Check if the endpoint require Permission Policy
if ept.EnablePermissionPolicyHeader {
var usingPermissionPolicy *permissionpolicy.PermissionsPolicy
if ept.PermissionPolicy != nil {
//Custom permission policy
usingPermissionPolicy = ept.PermissionPolicy
} else {
//Permission policy is enabled but not customized. Use default
usingPermissionPolicy = permissionpolicy.GetDefaultPermissionPolicy()
}
downstreamHeaders[downstreamHeaderCounter] = usingPermissionPolicy.ToKeyValueHeader()
downstreamHeaderCounter++
}
return upstreamHeaders, downstreamHeaders
}

View File

@ -1,11 +1,19 @@
package domainsniff package domainsniff
/*
Domainsniff
This package contain codes that perform project / domain specific behavior in Zoraxy
If you want Zoraxy to handle a particular domain or open source project in a special way,
you can add the checking logic here.
*/
import ( import (
"net" "net"
"time" "time"
) )
//Check if the domain is reachable and return err if not reachable // Check if the domain is reachable and return err if not reachable
func DomainReachableWithError(domain string) error { func DomainReachableWithError(domain string) error {
timeout := 1 * time.Second timeout := 1 * time.Second
conn, err := net.DialTimeout("tcp", domain, timeout) conn, err := net.DialTimeout("tcp", domain, timeout)
@ -17,7 +25,7 @@ func DomainReachableWithError(domain string) error {
return nil return nil
} }
//Check if domain reachable // Check if domain reachable
func DomainReachable(domain string) bool { func DomainReachable(domain string) bool {
return DomainReachableWithError(domain) == nil return DomainReachableWithError(domain) == nil
} }

View File

@ -1,29 +1,67 @@
package dpcore_test package dpcore_test
import ( import (
"net/url"
"testing" "testing"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
) )
func TestReplaceLocationHost(t *testing.T) { func TestReplaceLocationHost(t *testing.T) {
urlString := "http://private.com/test/newtarget/" tests := []struct {
rrr := &dpcore.ResponseRewriteRuleSet{ name string
OriginalHost: "test.example.com", urlString string
ProxyDomain: "private.com/test", rrr *dpcore.ResponseRewriteRuleSet
UseTLS: true, useTLS bool
} expectedResult string
useTLS := true expectError bool
}{
{
name: "Basic HTTP to HTTPS redirection",
urlString: "http://example.com/resource",
rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "example.com", OriginalHost: "proxy.example.com", UseTLS: true},
useTLS: true,
expectedResult: "https://proxy.example.com/resource",
expectError: false,
},
expectedResult := "https://test.example.com/newtarget/" {
name: "Basic HTTPS to HTTP redirection",
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS) urlString: "https://proxy.example.com/resource",
if err != nil { rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "proxy.example.com", OriginalHost: "proxy.example.com", UseTLS: false},
t.Errorf("Error occurred: %v", err) useTLS: false,
expectedResult: "http://proxy.example.com/resource",
expectError: false,
},
{
name: "No rewrite on mismatched domain",
urlString: "http://anotherdomain.com/resource",
rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "proxy.example.com", OriginalHost: "proxy.example.com", UseTLS: true},
useTLS: true,
expectedResult: "http://anotherdomain.com/resource",
expectError: false,
},
{
name: "Subpath trimming with HTTPS",
urlString: "https://blog.example.com/post?id=1",
rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "blog.example.com", OriginalHost: "proxy.example.com/blog", UseTLS: true},
useTLS: true,
expectedResult: "https://proxy.example.com/blog/post?id=1",
expectError: false,
},
} }
if result != expectedResult { for _, tt := range tests {
t.Errorf("Expected: %s, but got: %s", expectedResult, result) t.Run(tt.name, func(t *testing.T) {
result, err := dpcore.ReplaceLocationHost(tt.urlString, tt.rrr, tt.useTLS)
if (err != nil) != tt.expectError {
t.Errorf("Expected error: %v, got: %v", tt.expectError, err)
}
if result != tt.expectedResult {
result, _ = url.QueryUnescape(result)
t.Errorf("Expected result: %s, got: %s", tt.expectedResult, result)
}
})
} }
} }
@ -36,7 +74,7 @@ func TestReplaceLocationHostRelative(t *testing.T) {
} }
useTLS := true useTLS := true
expectedResult := "https://test.example.com/api/" expectedResult := "api/"
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS) result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
if err != nil { if err != nil {

View File

@ -60,7 +60,7 @@ func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS b
return u.String(), nil return u.String(), nil
} }
// Debug functions // Debug functions for replaceLocationHost
func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) { func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
return replaceLocationHost(urlString, rrr, useTLS) return replaceLocationHost(urlString, rrr, useTLS)
} }

View File

@ -8,6 +8,7 @@ import (
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
) )
/* /*
@ -36,7 +37,7 @@ func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
// Remvoe a user defined header from the list // Remvoe a user defined header from the list
func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error { func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
newHeaderList := []*UserDefinedHeader{} newHeaderList := []*rewrite.UserDefinedHeader{}
for _, header := range ep.UserDefinedHeaders { for _, header := range ep.UserDefinedHeaders {
if !strings.EqualFold(header.Key, key) { if !strings.EqualFold(header.Key, key) {
newHeaderList = append(newHeaderList, header) newHeaderList = append(newHeaderList, header)
@ -49,7 +50,7 @@ func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
} }
// Add a user defined header to the list, duplicates will be automatically removed // Add a user defined header to the list, duplicates will be automatically removed
func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *UserDefinedHeader) error { func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *rewrite.UserDefinedHeader) error {
if ep.UserDefinedHeaderExists(newHeaderRule.Key) { if ep.UserDefinedHeaderExists(newHeaderRule.Key) {
ep.RemoveUserDefinedHeader(newHeaderRule.Key) ep.RemoveUserDefinedHeader(newHeaderRule.Key)
} }

View File

@ -0,0 +1,100 @@
package loadbalance
import (
"fmt"
"math"
"math/rand"
"testing"
"time"
)
// func getRandomUpstreamByWeight(upstreams []*Upstream) (*Upstream, int, error) { ... }
func TestRandomUpstreamSelection(t *testing.T) {
rand.Seed(time.Now().UnixNano()) // Seed for randomness
// Define some test upstreams
upstreams := []*Upstream{
{
OriginIpOrDomain: "192.168.1.1:8080",
RequireTLS: false,
SkipCertValidations: false,
SkipWebSocketOriginCheck: false,
Weight: 1,
MaxConn: 0, // No connection limit for now
},
{
OriginIpOrDomain: "192.168.1.2:8080",
RequireTLS: false,
SkipCertValidations: false,
SkipWebSocketOriginCheck: false,
Weight: 1,
MaxConn: 0,
},
{
OriginIpOrDomain: "192.168.1.3:8080",
RequireTLS: true,
SkipCertValidations: true,
SkipWebSocketOriginCheck: true,
Weight: 1,
MaxConn: 0,
},
{
OriginIpOrDomain: "192.168.1.4:8080",
RequireTLS: true,
SkipCertValidations: true,
SkipWebSocketOriginCheck: true,
Weight: 1,
MaxConn: 0,
},
}
// Track how many times each upstream is selected
selectionCount := make(map[string]int)
totalPicks := 10000 // Number of times to call getRandomUpstreamByWeight
//expectedPickCount := totalPicks / len(upstreams) // Ideal count for each upstream
// Pick upstreams and record their selection count
for i := 0; i < totalPicks; i++ {
upstream, _, err := getRandomUpstreamByWeight(upstreams)
if err != nil {
t.Fatalf("Error getting random upstream: %v", err)
}
selectionCount[upstream.OriginIpOrDomain]++
}
// Condition 1: Ensure every upstream has been picked at least once
for _, upstream := range upstreams {
if selectionCount[upstream.OriginIpOrDomain] == 0 {
t.Errorf("Upstream %s was never selected", upstream.OriginIpOrDomain)
}
}
// Condition 2: Check that the distribution is within 1-2 standard deviations
counts := make([]float64, len(upstreams))
for i, upstream := range upstreams {
counts[i] = float64(selectionCount[upstream.OriginIpOrDomain])
}
mean := float64(totalPicks) / float64(len(upstreams))
stddev := calculateStdDev(counts, mean)
tolerance := 2 * stddev // Allowing up to 2 standard deviations
for i, count := range counts {
if math.Abs(count-mean) > tolerance {
t.Errorf("Selection of upstream %s is outside acceptable range: %v picks (mean: %v, stddev: %v)", upstreams[i].OriginIpOrDomain, count, mean, stddev)
}
}
fmt.Println("Selection count:", selectionCount)
fmt.Printf("Mean: %.2f, StdDev: %.2f\n", mean, stddev)
}
// Helper function to calculate standard deviation
func calculateStdDev(data []float64, mean float64) float64 {
var sumOfSquares float64
for _, value := range data {
sumOfSquares += (value - mean) * (value - mean)
}
variance := sumOfSquares / float64(len(data))
return math.Sqrt(variance)
}

View File

@ -11,6 +11,7 @@ import (
"strings" "strings"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/netutils" "imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/statistic" "imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/websocketproxy" "imuslab.com/zoraxy/mod/websocketproxy"
@ -158,9 +159,19 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
r.URL, _ = url.Parse(originalHostHeader) r.URL, _ = url.Parse(originalHostHeader)
} }
//Build downstream and upstream header rules //Populate the user-defined headers with the values from the request
upstreamHeaders, downstreamHeaders := target.SplitInboundOutboundHeaders() rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, target.UserDefinedHeaders)
//Build downstream and upstream header rules
upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
UserDefinedHeaders: rewrittenUserDefinedHeaders,
HSTSMaxAge: target.HSTSMaxAge,
HSTSIncludeSubdomains: target.ContainsWildcardName(true),
EnablePermissionPolicyHeader: target.EnablePermissionPolicyHeader,
PermissionPolicy: target.PermissionPolicy,
})
//Handle the request reverse proxy
statusCode, err := selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ statusCode, err := selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: selectedUpstream.OriginIpOrDomain, ProxyDomain: selectedUpstream.OriginIpOrDomain,
OriginalHost: originalHostHeader, OriginalHost: originalHostHeader,
@ -226,9 +237,19 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
r.URL, _ = url.Parse(originalHostHeader) r.URL, _ = url.Parse(originalHostHeader)
} }
//Build downstream and upstream header rules //Populate the user-defined headers with the values from the request
upstreamHeaders, downstreamHeaders := target.parent.SplitInboundOutboundHeaders() rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, target.parent.UserDefinedHeaders)
//Build downstream and upstream header rules, use the parent (subdomain) endpoint's headers
upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
UserDefinedHeaders: rewrittenUserDefinedHeaders,
HSTSMaxAge: target.parent.HSTSMaxAge,
HSTSIncludeSubdomains: target.parent.ContainsWildcardName(true),
EnablePermissionPolicyHeader: target.parent.EnablePermissionPolicyHeader,
PermissionPolicy: target.parent.PermissionPolicy,
})
//Handle the virtual directory reverse proxy request
statusCode, err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ statusCode, err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain, ProxyDomain: target.Domain,
OriginalHost: originalHostHeader, OriginalHost: originalHostHeader,

View File

@ -0,0 +1,63 @@
package rewrite
import (
"fmt"
"net/http"
"strings"
)
// GetHeaderVariableValuesFromRequest returns a map of header variables and their values
// note that variables behavior is not exactly identical to nginx variables
func GetHeaderVariableValuesFromRequest(r *http.Request) map[string]string {
vars := make(map[string]string)
// Request-specific variables
vars["$host"] = r.Host
vars["$remote_addr"] = r.RemoteAddr
vars["$request_uri"] = r.RequestURI
vars["$request_method"] = r.Method
vars["$content_length"] = fmt.Sprintf("%d", r.ContentLength)
vars["$content_type"] = r.Header.Get("Content-Type")
// Parsed URI elements
vars["$uri"] = r.URL.Path
vars["$args"] = r.URL.RawQuery
vars["$scheme"] = r.URL.Scheme
vars["$query_string"] = r.URL.RawQuery
// User agent and referer
vars["$http_user_agent"] = r.UserAgent()
vars["$http_referer"] = r.Referer()
return vars
}
// CustomHeadersIncludeDynamicVariables checks if the user-defined headers contain dynamic variables
// use for early exit when processing the headers
func CustomHeadersIncludeDynamicVariables(userDefinedHeaders []*UserDefinedHeader) bool {
for _, header := range userDefinedHeaders {
if strings.Contains(header.Value, "$") {
return true
}
}
return false
}
// PopulateRequestHeaderVariables populates the user-defined headers with the values from the request
func PopulateRequestHeaderVariables(r *http.Request, userDefinedHeaders []*UserDefinedHeader) []*UserDefinedHeader {
if !CustomHeadersIncludeDynamicVariables(userDefinedHeaders) {
// Early exit if there are no dynamic variables
return userDefinedHeaders
}
vars := GetHeaderVariableValuesFromRequest(r)
populatedHeaders := []*UserDefinedHeader{}
// Populate the user-defined headers with the values from the request
for _, header := range userDefinedHeaders {
thisHeader := header.Copy()
for key, value := range vars {
thisHeader.Value = strings.ReplaceAll(thisHeader.Value, key, value)
}
populatedHeaders = append(populatedHeaders, thisHeader)
}
return populatedHeaders
}

View File

@ -0,0 +1,172 @@
package rewrite
import (
"net/http/httptest"
"testing"
)
func TestGetHeaderVariableValuesFromRequest(t *testing.T) {
// Create a sample request
req := httptest.NewRequest("GET", "https://example.com/test?foo=bar", nil)
req.Host = "example.com"
req.RemoteAddr = "192.168.1.1:12345"
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "TestAgent")
req.Header.Set("Referer", "https://referer.com")
// Call the function
vars := GetHeaderVariableValuesFromRequest(req)
// Expected results
expected := map[string]string{
"$host": "example.com",
"$remote_addr": "192.168.1.1:12345",
"$request_uri": "https://example.com/test?foo=bar",
"$request_method": "GET",
"$content_length": "0", // ContentLength is 0 because there's no body in the request
"$content_type": "application/json",
"$uri": "/test",
"$args": "foo=bar",
"$scheme": "https",
"$query_string": "foo=bar",
"$http_user_agent": "TestAgent",
"$http_referer": "https://referer.com",
}
// Check each expected variable
for key, expectedValue := range expected {
if vars[key] != expectedValue {
t.Errorf("Expected %s to be %s, but got %s", key, expectedValue, vars[key])
}
}
}
func TestCustomHeadersIncludeDynamicVariables(t *testing.T) {
tests := []struct {
name string
headers []*UserDefinedHeader
expectedHasVar bool
}{
{
name: "No headers",
headers: []*UserDefinedHeader{},
expectedHasVar: false,
},
{
name: "Headers without dynamic variables",
headers: []*UserDefinedHeader{
{
Direction: HeaderDirection_ZoraxyToUpstream,
Key: "X-Custom-Header",
Value: "staticValue",
IsRemove: false,
},
{
Direction: HeaderDirection_ZoraxyToDownstream,
Key: "X-Another-Header",
Value: "staticValue",
IsRemove: false,
},
},
expectedHasVar: false,
},
{
name: "Headers with one dynamic variable",
headers: []*UserDefinedHeader{
{
Direction: HeaderDirection_ZoraxyToUpstream,
Key: "X-Custom-Header",
Value: "$dynamicValue",
IsRemove: false,
},
},
expectedHasVar: true,
},
{
name: "Headers with multiple dynamic variables",
headers: []*UserDefinedHeader{
{
Direction: HeaderDirection_ZoraxyToUpstream,
Key: "X-Custom-Header",
Value: "$dynamicValue1",
IsRemove: false,
},
{
Direction: HeaderDirection_ZoraxyToDownstream,
Key: "X-Another-Header",
Value: "$dynamicValue2",
IsRemove: false,
},
},
expectedHasVar: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hasVar := CustomHeadersIncludeDynamicVariables(tt.headers)
if hasVar != tt.expectedHasVar {
t.Errorf("Expected %v, but got %v", tt.expectedHasVar, hasVar)
}
})
}
}
func TestPopulateRequestHeaderVariables(t *testing.T) {
// Create a sample request with specific values
req := httptest.NewRequest("GET", "https://example.com/test?foo=bar", nil)
req.Host = "example.com"
req.RemoteAddr = "192.168.1.1:12345"
req.Header.Set("User-Agent", "TestAgent")
req.Header.Set("Referer", "https://referer.com")
// Define user-defined headers with dynamic variables
userDefinedHeaders := []*UserDefinedHeader{
{
Direction: HeaderDirection_ZoraxyToUpstream,
Key: "X-Forwarded-Host",
Value: "$host",
},
{
Direction: HeaderDirection_ZoraxyToDownstream,
Key: "X-Client-IP",
Value: "$remote_addr",
},
{
Direction: HeaderDirection_ZoraxyToDownstream,
Key: "X-Custom-Header",
Value: "$request_uri",
},
}
// Call the function with the test data
resultHeaders := PopulateRequestHeaderVariables(req, userDefinedHeaders)
// Expected results after variable substitution
expectedHeaders := []*UserDefinedHeader{
{
Direction: HeaderDirection_ZoraxyToUpstream,
Key: "X-Forwarded-Host",
Value: "example.com",
},
{
Direction: HeaderDirection_ZoraxyToDownstream,
Key: "X-Client-IP",
Value: "192.168.1.1:12345",
},
{
Direction: HeaderDirection_ZoraxyToDownstream,
Key: "X-Custom-Header",
Value: "https://example.com/test?foo=bar",
},
}
// Validate results
for i, expected := range expectedHeaders {
if resultHeaders[i].Direction != expected.Direction ||
resultHeaders[i].Key != expected.Key ||
resultHeaders[i].Value != expected.Value {
t.Errorf("Expected header %v, but got %v", expected, resultHeaders[i])
}
}
}

View File

@ -0,0 +1,79 @@
package rewrite
/*
rewrite.go
This script handle the rewrite logic for custom headers
*/
import (
"strconv"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
)
// SplitInboundOutboundHeaders split user defined headers into upstream and downstream headers
// return upstream header and downstream header key-value pairs
// if the header is expected to be deleted, the value will be set to empty string
func SplitUpDownStreamHeaders(rewriteOptions *HeaderRewriteOptions) ([][]string, [][]string) {
if len(rewriteOptions.UserDefinedHeaders) == 0 && rewriteOptions.HSTSMaxAge == 0 && !rewriteOptions.EnablePermissionPolicyHeader {
//Early return if there are no defined headers
return [][]string{}, [][]string{}
}
//Use pre-allocation for faster performance
//Downstream +2 for Permission Policy and HSTS
upstreamHeaders := make([][]string, len(rewriteOptions.UserDefinedHeaders))
downstreamHeaders := make([][]string, len(rewriteOptions.UserDefinedHeaders)+2)
upstreamHeaderCounter := 0
downstreamHeaderCounter := 0
//Sort the headers into upstream or downstream
for _, customHeader := range rewriteOptions.UserDefinedHeaders {
thisHeaderSet := make([]string, 2)
thisHeaderSet[0] = customHeader.Key
thisHeaderSet[1] = customHeader.Value
if customHeader.IsRemove {
//Prevent invalid config
thisHeaderSet[1] = ""
}
//Assign to slice
if customHeader.Direction == HeaderDirection_ZoraxyToUpstream {
upstreamHeaders[upstreamHeaderCounter] = thisHeaderSet
upstreamHeaderCounter++
} else if customHeader.Direction == HeaderDirection_ZoraxyToDownstream {
downstreamHeaders[downstreamHeaderCounter] = thisHeaderSet
downstreamHeaderCounter++
}
}
//Check if the endpoint require HSTS headers
if rewriteOptions.HSTSMaxAge > 0 {
if rewriteOptions.HSTSIncludeSubdomains {
//Endpoint listening domain includes wildcards.
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(rewriteOptions.HSTSMaxAge)) + "; includeSubdomains"}
} else {
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(rewriteOptions.HSTSMaxAge))}
}
downstreamHeaderCounter++
}
//Check if the endpoint require Permission Policy
if rewriteOptions.EnablePermissionPolicyHeader {
var usingPermissionPolicy *permissionpolicy.PermissionsPolicy
if rewriteOptions.PermissionPolicy != nil {
//Custom permission policy
usingPermissionPolicy = rewriteOptions.PermissionPolicy
} else {
//Permission policy is enabled but not customized. Use default
usingPermissionPolicy = permissionpolicy.GetDefaultPermissionPolicy()
}
downstreamHeaders[downstreamHeaderCounter] = usingPermissionPolicy.ToKeyValueHeader()
downstreamHeaderCounter++
}
return upstreamHeaders, downstreamHeaders
}

View File

@ -0,0 +1,51 @@
package rewrite
import (
"encoding/json"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
)
/*
typdef.go
This script handle the type definition for custom headers
*/
/* Custom Header Related Data structure */
// Header injection direction type
type HeaderDirection int
const (
HeaderDirection_ZoraxyToUpstream HeaderDirection = 0 //Inject (or remove) header to request out-going from Zoraxy to backend server
HeaderDirection_ZoraxyToDownstream HeaderDirection = 1 //Inject (or remove) header to request out-going from Zoraxy to client (e.g. browser)
)
// User defined headers to add into a proxy endpoint
type UserDefinedHeader struct {
Direction HeaderDirection
Key string
Value string
IsRemove bool //Instead of set, remove this key instead
}
type HeaderRewriteOptions struct {
UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
HSTSIncludeSubdomains bool //Include subdomains in HSTS header
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
}
// Utilities for header rewrite
func (h *UserDefinedHeader) GetDirection() HeaderDirection {
return h.Direction
}
// Copy eturns a deep copy of the UserDefinedHeader
func (h *UserDefinedHeader) Copy() *UserDefinedHeader {
result := UserDefinedHeader{}
js, _ := json.Marshal(h)
json.Unmarshal(js, &result)
return &result
}

View File

@ -7,10 +7,12 @@ import (
"sync" "sync"
"imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/auth/sso"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection" "imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger" "imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/statistic" "imuslab.com/zoraxy/mod/statistic"
@ -44,6 +46,7 @@ type RouterOption struct {
StatisticCollector *statistic.Collector //Statistic collector for storing stats on incoming visitors StatisticCollector *statistic.Collector //Statistic collector for storing stats on incoming visitors
WebDirectory string //The static web server directory containing the templates folder WebDirectory string //The static web server directory containing the templates folder
LoadBalancer *loadbalance.RouteManager //Load balancer that handle load balancing of proxy target LoadBalancer *loadbalance.RouteManager //Load balancer that handle load balancing of proxy target
SSOHandler *sso.SSOHandler //SSO handler for handling SSO requests, interception mode only
Logger *logger.Logger //Logger for reverse proxy requets Logger *logger.Logger //Logger for reverse proxy requets
} }
@ -82,23 +85,6 @@ type BasicAuthExceptionRule struct {
PathPrefix string PathPrefix string
} }
/* Custom Header Related Data structure */
// Header injection direction type
type HeaderDirection int
const (
HeaderDirection_ZoraxyToUpstream HeaderDirection = 0 //Inject (or remove) header to request out-going from Zoraxy to backend server
HeaderDirection_ZoraxyToDownstream HeaderDirection = 1 //Inject (or remove) header to request out-going from Zoraxy to client (e.g. browser)
)
// User defined headers to add into a proxy endpoint
type UserDefinedHeader struct {
Direction HeaderDirection
Key string
Value string
IsRemove bool //Instead of set, remove this key instead
}
/* Routing Rule Data Structures */ /* Routing Rule Data Structures */
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better // A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
@ -131,7 +117,7 @@ type ProxyEndpoint struct {
VirtualDirectories []*VirtualDirectoryEndpoint VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers //Custom Headers
UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint UserDefinedHeaders []*rewrite.UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
EnablePermissionPolicyHeader bool //Enable injection of permission policy header EnablePermissionPolicyHeader bool //Enable injection of permission policy header
@ -142,6 +128,7 @@ type ProxyEndpoint struct {
RequireBasicAuth bool //Set to true to request basic auth before proxy RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
UseSSOIntercept bool //Allow SSO to intercept this endpoint and provide authentication via Oauth2 credentials
// Rate Limiting // Rate Limiting
RequireRateLimit bool RequireRateLimit bool

View File

@ -16,7 +16,7 @@ type Sender struct {
Port int //E.g. 587 Port int //E.g. 587
Username string //Username of the email account Username string //Username of the email account
Password string //Password of the email account Password string //Password of the email account
SenderAddr string //e.g. admin@arozos.com SenderAddr string //e.g. admin@aroz.org
} }
// Create a new email sender object // Create a new email sender object

View File

@ -1,6 +1,7 @@
package ganserv package ganserv
import ( import (
"log"
"net" "net"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database"
@ -85,6 +86,7 @@ func NewNetworkManager(option *NetworkManagerOptions) *NetworkManager {
//Get controller info //Get controller info
instanceInfo, err := getControllerInfo(option.AuthToken, option.ApiPort) instanceInfo, err := getControllerInfo(option.AuthToken, option.ApiPort)
if err != nil { if err != nil {
log.Println("ZeroTier connection failed: ", err.Error())
return &NetworkManager{ return &NetworkManager{
authToken: option.AuthToken, authToken: option.AuthToken,
apiPort: option.ApiPort, apiPort: option.ApiPort,

View File

@ -28,11 +28,17 @@ type NodeInfo struct {
Clock int64 `json:"clock"` Clock int64 `json:"clock"`
Config struct { Config struct {
Settings struct { Settings struct {
AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay"` AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay,omitempty"`
PortMappingEnabled bool `json:"portMappingEnabled"` ForceTCPRelay bool `json:"forceTcpRelay,omitempty"`
PrimaryPort int `json:"primaryPort"` HomeDir string `json:"homeDir,omitempty"`
SoftwareUpdate string `json:"softwareUpdate"` ListeningOn []string `json:"listeningOn,omitempty"`
SoftwareUpdateChannel string `json:"softwareUpdateChannel"` 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:"settings"`
} `json:"config"` } `json:"config"`
Online bool `json:"online"` Online bool `json:"online"`
@ -46,7 +52,6 @@ type NodeInfo struct {
VersionMinor int `json:"versionMinor"` VersionMinor int `json:"versionMinor"`
VersionRev int `json:"versionRev"` VersionRev int `json:"versionRev"`
} }
type ErrResp struct { type ErrResp struct {
Message string `json:"message"` Message string `json:"message"`
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

67
src/mod/geodb/locale.go Normal file
View File

@ -0,0 +1,67 @@
package geodb
import "net/http"
// GetRequesterCountryISOCode get the locale of the requester
func (s *Store) GetLocaleFromRequest(r *http.Request) (string, error) {
cc := s.GetRequesterCountryISOCode(r)
return GetLocaleFromCountryCode(cc), nil
}
// GetLocaleFromCountryCode get the locale given the country code
func GetLocaleFromCountryCode(cc string) string {
//If you find your country is not in the list, please add it here
mapCountryToLocale := map[string]string{
"aa": "ar_AA",
"by": "be_BY",
"bg": "bg_BG",
"es": "ca_ES",
"cz": "cs_CZ",
"dk": "da_DK",
"ch": "de_CH",
"de": "de_DE",
"gr": "el_GR",
"au": "en_AU",
"be": "en_BE",
"gb": "en_GB",
"jp": "en_JP",
"us": "en_US",
"za": "en_ZA",
"fi": "fi_FI",
"ca": "fr_CA",
"fr": "fr_FR",
"hr": "hr_HR",
"hu": "hu_HU",
"is": "is_IS",
"it": "it_IT",
"il": "iw_IL",
"kr": "ko_KR",
"lt": "lt_LT",
"lv": "lv_LV",
"mk": "mk_MK",
"nl": "nl_NL",
"no": "no_NO",
"pl": "pl_PL",
"br": "pt_BR",
"pt": "pt_PT",
"ro": "ro_RO",
"ru": "ru_RU",
"sp": "sh_SP",
"sk": "sk_SK",
"sl": "sl_SL",
"al": "sq_AL",
"se": "sv_SE",
"th": "th_TH",
"tr": "tr_TR",
"ua": "uk_UA",
"cn": "zh_CN",
"tw": "zh_TW",
"hk": "zh_HK",
}
locale, ok := mapCountryToLocale[cc]
if !ok {
return "en-US"
}
return locale
}

View File

@ -0,0 +1,79 @@
package ipscan
/*
ipscan http handlers
This script provide http handlers for ipscan module
*/
import (
"encoding/json"
"net"
"net/http"
"imuslab.com/zoraxy/mod/utils"
)
// HandleScanPort is the HTTP handler for scanning opened ports on a given IP address
func HandleScanPort(w http.ResponseWriter, r *http.Request) {
targetIp, err := utils.GetPara(r, "ip")
if err != nil {
utils.SendErrorResponse(w, "target IP address not given")
return
}
// Check if the IP is a valid IP address
ip := net.ParseIP(targetIp)
if ip == nil {
utils.SendErrorResponse(w, "invalid IP address")
return
}
// Scan the ports
openPorts := ScanPorts(targetIp)
jsonData, err := json.Marshal(openPorts)
if err != nil {
utils.SendErrorResponse(w, "failed to marshal JSON")
return
}
utils.SendJSONResponse(w, string(jsonData))
}
// HandleIpScan is the HTTP handler for scanning IP addresses in a given range or CIDR
func HandleIpScan(w http.ResponseWriter, r *http.Request) {
cidr, err := utils.PostPara(r, "cidr")
if err != nil {
//Ip range mode
start, err := utils.PostPara(r, "start")
if err != nil {
utils.SendErrorResponse(w, "missing start ip")
return
}
end, err := utils.PostPara(r, "end")
if err != nil {
utils.SendErrorResponse(w, "missing end ip")
return
}
discoveredHosts, err := ScanIpRange(start, end)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(discoveredHosts)
utils.SendJSONResponse(w, string(js))
} else {
//CIDR mode
discoveredHosts, err := ScanCIDRRange(cidr)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(discoveredHosts)
utils.SendJSONResponse(w, string(js))
}
}

View File

@ -27,7 +27,7 @@ type DiscoveredHost struct {
HttpsPortDetected bool HttpsPortDetected bool
} }
//Scan an IP range given the start and ending ip address // Scan an IP range given the start and ending ip address
func ScanIpRange(start, end string) ([]*DiscoveredHost, error) { func ScanIpRange(start, end string) ([]*DiscoveredHost, error) {
ipStart := net.ParseIP(start) ipStart := net.ParseIP(start)
ipEnd := net.ParseIP(end) ipEnd := net.ParseIP(end)
@ -57,7 +57,6 @@ func ScanIpRange(start, end string) ([]*DiscoveredHost, error) {
host.CheckHostname() host.CheckHostname()
host.CheckPort("http", 80, &host.HttpPortDetected) host.CheckPort("http", 80, &host.HttpPortDetected)
host.CheckPort("https", 443, &host.HttpsPortDetected) host.CheckPort("https", 443, &host.HttpsPortDetected)
fmt.Println("OK", host)
hosts = append(hosts, host) hosts = append(hosts, host)
}(thisIp) }(thisIp)
@ -118,7 +117,7 @@ func (host *DiscoveredHost) CheckPing() error {
func (host *DiscoveredHost) CheckHostname() { func (host *DiscoveredHost) CheckHostname() {
// lookup the hostname for the IP address // lookup the hostname for the IP address
names, err := net.LookupAddr(host.IP) names, err := net.LookupAddr(host.IP)
fmt.Println(names, err) //fmt.Println(names, err)
if err == nil && len(names) > 0 { if err == nil && len(names) > 0 {
host.Hostname = names[0] host.Hostname = names[0]
} }

View File

@ -0,0 +1,48 @@
package ipscan
/*
Port Scanner
This module scan the given IP address and scan all the opened port
*/
import (
"fmt"
"net"
"sync"
"time"
)
// OpenedPort holds information about an open port and its service type
type OpenedPort struct {
Port int
IsTCP bool
}
// ScanPorts scans all the opened ports on a given host IP (both IPv4 and IPv6)
func ScanPorts(host string) []*OpenedPort {
var openPorts []*OpenedPort
var wg sync.WaitGroup
var mu sync.Mutex
for port := 1; port <= 65535; port++ {
wg.Add(1)
go func(port int) {
defer wg.Done()
address := fmt.Sprintf("%s:%d", host, port)
// Check TCP
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
if err == nil {
mu.Lock()
openPorts = append(openPorts, &OpenedPort{Port: port, IsTCP: true})
mu.Unlock()
conn.Close()
}
}(port)
}
wg.Wait()
return openPorts
}

View File

@ -1,15 +1,18 @@
package streamproxy package streamproxy
import ( import (
"encoding/json"
"errors" "errors"
"log"
"net" "net"
"os"
"path/filepath"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
) )
/* /*
@ -48,9 +51,10 @@ type ProxyRelayConfig struct {
} }
type Options struct { type Options struct {
Database *database.Database
DefaultTimeout int DefaultTimeout int
AccessControlHandler func(net.Conn) bool AccessControlHandler func(net.Conn) bool
ConfigStore string //Folder to store the config files, will be created if not exists
Logger *logger.Logger //Logger for the stream proxy
} }
type Manager struct { type Manager struct {
@ -63,13 +67,37 @@ type Manager struct {
} }
func NewStreamProxy(options *Options) *Manager { func NewStreamProxy(options *Options) (*Manager, error) {
options.Database.NewTable("tcprox") if !utils.FileExists(options.ConfigStore) {
err := os.MkdirAll(options.ConfigStore, 0775)
if err != nil {
return nil, err
}
}
//Load relay configs from db //Load relay configs from db
previousRules := []*ProxyRelayConfig{} previousRules := []*ProxyRelayConfig{}
if options.Database.KeyExists("tcprox", "rules") { streamProxyConfigFiles, err := filepath.Glob(options.ConfigStore + "/*.config")
options.Database.Read("tcprox", "rules", &previousRules) if err != nil {
return nil, err
}
for _, configFile := range streamProxyConfigFiles {
//Read file into bytes
configBytes, err := os.ReadFile(configFile)
if err != nil {
options.Logger.PrintAndLog("stream-prox", "Read stream proxy config failed", err)
continue
}
thisRelayConfig := &ProxyRelayConfig{}
err = json.Unmarshal(configBytes, thisRelayConfig)
if err != nil {
options.Logger.PrintAndLog("stream-prox", "Unmarshal stream proxy config failed", err)
continue
}
//Append the config to the list
previousRules = append(previousRules, thisRelayConfig)
} }
//Check if the AccessControlHandler is empty. If yes, set it to always allow access //Check if the AccessControlHandler is empty. If yes, set it to always allow access
@ -91,14 +119,27 @@ func NewStreamProxy(options *Options) *Manager {
rule.parent = &thisManager rule.parent = &thisManager
if rule.Running { if rule.Running {
//This was previously running. Start it again //This was previously running. Start it again
log.Println("[Stream Proxy] Resuming stream proxy rule " + rule.Name) thisManager.logf("Resuming stream proxy rule "+rule.Name, nil)
rule.Start() rule.Start()
} }
} }
thisManager.Configs = previousRules thisManager.Configs = previousRules
return &thisManager return &thisManager, nil
}
// Wrapper function to log error
func (m *Manager) logf(message string, originalError error) {
if m.Options.Logger == nil {
//Print to fmt
if originalError != nil {
message += ": " + originalError.Error()
}
println(message)
return
}
m.Options.Logger.PrintAndLog("stream-prox", message, originalError)
} }
func (m *Manager) NewConfig(config *ProxyRelayOptions) string { func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
@ -179,6 +220,11 @@ func (m *Manager) EditConfig(configUUID string, newName string, newListeningAddr
} }
func (m *Manager) RemoveConfig(configUUID string) error { func (m *Manager) RemoveConfig(configUUID string) error {
//Remove the config from file
err := os.Remove(filepath.Join(m.Options.ConfigStore, configUUID+".config"))
if err != nil {
return err
}
// Find and remove the config with the specified UUID // Find and remove the config with the specified UUID
for i, config := range m.Configs { for i, config := range m.Configs {
if config.UUID == configUUID { if config.UUID == configUUID {
@ -190,8 +236,19 @@ func (m *Manager) RemoveConfig(configUUID string) error {
return errors.New("config not found") return errors.New("config not found")
} }
// Save all configs to ConfigStore folder
func (m *Manager) SaveConfigToDatabase() { func (m *Manager) SaveConfigToDatabase() {
m.Options.Database.Write("tcprox", "rules", m.Configs) for _, config := range m.Configs {
configBytes, err := json.Marshal(config)
if err != nil {
m.logf("Failed to marshal stream proxy config", err)
continue
}
err = os.WriteFile(m.Options.ConfigStore+"/"+config.UUID+".config", configBytes, 0775)
if err != nil {
m.logf("Failed to save stream proxy config", err)
}
}
} }
/* /*
@ -217,9 +274,10 @@ func (c *ProxyRelayConfig) Start() error {
if err != nil { if err != nil {
if !c.UseTCP { if !c.UseTCP {
c.Running = false c.Running = false
c.udpStopChan = nil
c.parent.SaveConfigToDatabase() c.parent.SaveConfigToDatabase()
} }
log.Println("[TCP] Error starting stream proxy " + c.Name + "(" + c.UUID + "): " + err.Error()) c.parent.logf("[proto:udp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
} }
}() }()
} }
@ -231,8 +289,9 @@ func (c *ProxyRelayConfig) Start() error {
err := c.Port2host(c.ListeningAddress, c.ProxyTargetAddr, tcpStopChan) err := c.Port2host(c.ListeningAddress, c.ProxyTargetAddr, tcpStopChan)
if err != nil { if err != nil {
c.Running = false c.Running = false
c.tcpStopChan = nil
c.parent.SaveConfigToDatabase() c.parent.SaveConfigToDatabase()
log.Println("[TCP] Error starting stream proxy " + c.Name + "(" + c.UUID + "): " + err.Error()) c.parent.logf("[proto:tcp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
} }
}() }()
} }
@ -253,27 +312,27 @@ func (c *ProxyRelayConfig) Restart() {
if c.IsRunning() { if c.IsRunning() {
c.Stop() c.Stop()
} }
time.Sleep(300 * time.Millisecond) time.Sleep(3000 * time.Millisecond)
c.Start() c.Start()
} }
// Stop a running proxy if running // Stop a running proxy if running
func (c *ProxyRelayConfig) Stop() { func (c *ProxyRelayConfig) Stop() {
log.Println("[STREAM PROXY] Stopping Stream Proxy " + c.Name) c.parent.logf("Stopping Stream Proxy "+c.Name, nil)
if c.udpStopChan != nil { if c.udpStopChan != nil {
log.Println("[STREAM PROXY] Stopping UDP for " + c.Name) c.parent.logf("Stopping UDP for "+c.Name, nil)
c.udpStopChan <- true c.udpStopChan <- true
c.udpStopChan = nil c.udpStopChan = nil
} }
if c.tcpStopChan != nil { if c.tcpStopChan != nil {
log.Println("[STREAM PROXY] Stopping TCP for " + c.Name) c.parent.logf("Stopping TCP for "+c.Name, nil)
c.tcpStopChan <- true c.tcpStopChan <- true
c.tcpStopChan = nil c.tcpStopChan = nil
} }
log.Println("[STREAM PROXY] Stopped Stream Proxy " + c.Name) c.parent.logf("Stopped Stream Proxy "+c.Name, nil)
c.Running = false c.Running = false
//Update the running status //Update the running status

View File

@ -51,7 +51,7 @@
You can upload your html files to your web directory via the <b>Web Directory Manager</b>. You can upload your html files to your web directory via the <b>Web Directory Manager</b>.
</p> </p>
<p> <p>
For online documentation, please refer to <a href="//zoraxy.arozos.com">zoraxy.arozos.com</a> or the <a href="https://github.com/tobychui/zoraxy/wiki">project wiki</a>.<br> For online documentation, please refer to <a href="//zoraxy.aroz.org">zoraxy.aroz.org</a> or the <a href="https://github.com/tobychui/zoraxy/wiki">project wiki</a>.<br>
Thank you for using Zoraxy! Thank you for using Zoraxy!
</p> </p>
</div> </div>

View File

@ -13,6 +13,7 @@ import (
"imuslab.com/zoraxy/mod/dynamicproxy" "imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/uptime" "imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
@ -98,6 +99,7 @@ func ReverseProxtInit() {
WebDirectory: *staticWebServerRoot, WebDirectory: *staticWebServerRoot,
AccessController: accessController, AccessController: accessController,
LoadBalancer: loadBalancer, LoadBalancer: loadBalancer,
SSOHandler: ssoHandler,
Logger: SystemWideLogger, Logger: SystemWideLogger,
}) })
if err != nil { if err != nil {
@ -331,7 +333,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
//VDir //VDir
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{}, VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
//Custom headers //Custom headers
UserDefinedHeaders: []*dynamicproxy.UserDefinedHeader{}, UserDefinedHeaders: []*rewrite.UserDefinedHeader{},
//Auth //Auth
RequireBasicAuth: requireBasicAuth, RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: basicAuthCredentials, BasicAuthCredentials: basicAuthCredentials,
@ -1126,7 +1128,7 @@ func HandleCustomHeaderList(w http.ResponseWriter, r *http.Request) {
//List all custom headers //List all custom headers
customHeaderList := targetProxyEndpoint.UserDefinedHeaders customHeaderList := targetProxyEndpoint.UserDefinedHeaders
if customHeaderList == nil { if customHeaderList == nil {
customHeaderList = []*dynamicproxy.UserDefinedHeader{} customHeaderList = []*rewrite.UserDefinedHeader{}
} }
js, _ := json.Marshal(customHeaderList) js, _ := json.Marshal(customHeaderList)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
@ -1171,12 +1173,12 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
return return
} }
//Create a Custom Header Definition type //Create a Custom Header Defination type
var rewriteDirection dynamicproxy.HeaderDirection var rewriteDirection rewrite.HeaderDirection
if direction == "toOrigin" { if direction == "toOrigin" {
rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToUpstream rewriteDirection = rewrite.HeaderDirection_ZoraxyToUpstream
} else if direction == "toClient" { } else if direction == "toClient" {
rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToDownstream rewriteDirection = rewrite.HeaderDirection_ZoraxyToDownstream
} else { } else {
//Unknown direction //Unknown direction
utils.SendErrorResponse(w, "header rewrite direction not supported") utils.SendErrorResponse(w, "header rewrite direction not supported")
@ -1187,7 +1189,8 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
if rewriteType == "remove" { if rewriteType == "remove" {
isRemove = true isRemove = true
} }
headerRewriteDefinition := dynamicproxy.UserDefinedHeader{
headerRewriteDefinition := rewrite.UserDefinedHeader{
Key: name, Key: name,
Value: value, Value: value,
Direction: rewriteDirection, Direction: rewriteDirection,

View File

@ -36,7 +36,10 @@ import (
Startup Sequence Startup Sequence
This function starts the startup sequence of all This function starts the startup sequence of all
required modules required modules. Their startup sequences are inter-dependent
and must be started in a specific order.
Don't touch this function unless you know what you are doing
*/ */
var ( var (
@ -124,6 +127,22 @@ func startupSequence() {
panic(err) panic(err)
} }
/*
//Create an SSO handler
ssoHandler, err = sso.NewSSOHandler(&sso.SSOConfig{
SystemUUID: nodeUUID,
PortalServerPort: 5488,
AuthURL: "http://auth.localhost",
Database: sysdb,
Logger: SystemWideLogger,
})
if err != nil {
log.Fatal(err)
}
//Restore the SSO handler to previous state before shutdown
ssoHandler.RestorePreviousRunningState()
*/
//Create a statistic collector //Create a statistic collector
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{ statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
Database: sysdb, Database: sysdb,
@ -187,7 +206,7 @@ func startupSequence() {
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{ mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
HostName: hostName, HostName: hostName,
Port: portInt, Port: portInt,
Domain: "zoraxy.arozos.com", Domain: "zoraxy.aroz.org",
Model: "Network Gateway", Model: "Network Gateway",
UUID: nodeUUID, UUID: nodeUUID,
Vendor: "imuslab.com", Vendor: "imuslab.com",
@ -244,10 +263,14 @@ func startupSequence() {
webSshManager = sshprox.NewSSHProxyManager() webSshManager = sshprox.NewSSHProxyManager()
//Create TCP Proxy Manager //Create TCP Proxy Manager
streamProxyManager = streamproxy.NewStreamProxy(&streamproxy.Options{ streamProxyManager, err = streamproxy.NewStreamProxy(&streamproxy.Options{
Database: sysdb,
AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess, AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess,
ConfigStore: "./conf/streamproxy",
Logger: SystemWideLogger,
}) })
if err != nil {
panic(err)
}
//Create WoL MAC storage table //Create WoL MAC storage table
sysdb.NewTable("wolmac") sysdb.NewTable("wolmac")

View File

@ -61,7 +61,7 @@
<p>Current list of loaded certificates</p> <p>Current list of loaded certificates</p>
<div tourstep="certTable"> <div tourstep="certTable">
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;"> <div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table class="ui sortable unstackable basic celled table"> <table class="ui unstackable basic celled table">
<thead> <thead>
<tr><th>Domain</th> <tr><th>Domain</th>
<th>Last Update</th> <th>Last Update</th>
@ -161,7 +161,9 @@
msgbox("Requesting certificate via " + defaultCA +"..."); msgbox("Requesting certificate via " + defaultCA +"...");
//Request ACME for certificate //Request ACME for certificate
let buttonOriginalHTML = "";
if (btn != undefined){ if (btn != undefined){
buttonOriginalHTML = $(btn).html();
$(btn).addClass('disabled'); $(btn).addClass('disabled');
$(btn).html(`<i class="ui loading spinner icon"></i>`); $(btn).html(`<i class="ui loading spinner icon"></i>`);
} }
@ -169,11 +171,26 @@
obtainCertificate(domain, dns, defaultCA.trim(), function(succ){ obtainCertificate(domain, dns, defaultCA.trim(), function(succ){
if (btn != undefined){ if (btn != undefined){
$(btn).removeClass('disabled'); $(btn).removeClass('disabled');
if (succ){ if ($(btn).hasClass("icon")){
$(btn).html(`<i class="ui green check icon"></i>`); //Only change the button icon
if (succ){
$(btn).html(`<i class="ui green check icon"></i>`);
}else{
$(btn).html(`<i class="ui red times icon"></i>`);
}
}else{ }else{
$(btn).html(`<i class="ui red times icon"></i>`); //Show error or success icon with text
if (succ){
$(btn).html(`<i class="ui green check icon"></i> Requested`);
}else{
$(btn).html(`<i class="ui red times icon"></i> Error`);
}
} }
//Restore the button after 3 seconds
setTimeout(function(){
$(btn).html(buttonOriginalHTML);
}, 3000);
setTimeout(function(){ setTimeout(function(){
initManagedDomainCertificateList(); initManagedDomainCertificateList();

View File

@ -350,15 +350,27 @@
let originalContent = $(column).html(); let originalContent = $(column).html();
//Check if this host is covered within one of the certificates. If not, show the icon //Check if this host is covered within one of the certificates. If not, show the icon
let domainIsCovered = true; let enableQuickRequestButton = true;
let domains = [payload.RootOrMatchingDomain]; //Domain for getting certificate if needed let domains = [payload.RootOrMatchingDomain]; //Domain for getting certificate if needed
for (var i = 0; i < payload.MatchingDomainAlias.length; i++){ for (var i = 0; i < payload.MatchingDomainAlias.length; i++){
let thisAliasName = payload.MatchingDomainAlias[i]; let thisAliasName = payload.MatchingDomainAlias[i];
domains.push(thisAliasName); domains.push(thisAliasName);
} }
if (true){
domainIsCovered = false; //Check if the domain or alias contains wildcard, if yes, disabled the get certificate button
if (payload.RootOrMatchingDomain.indexOf("*") > -1){
enableQuickRequestButton = false;
} }
if (payload.MatchingDomainAlias != undefined){
for (var i = 0; i < payload.MatchingDomainAlias.length; i++){
if (payload.MatchingDomainAlias[i].indexOf("*") > -1){
enableQuickRequestButton = false;
break;
}
}
}
//encode the domain to DOM //encode the domain to DOM
let certificateDomains = encodeURIComponent(JSON.stringify(domains)); let certificateDomains = encodeURIComponent(JSON.stringify(domains));
@ -371,9 +383,8 @@
</div><br> </div><br>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAliasHostnames('${uuid}');"><i class=" blue at icon"></i> Alias</button> <button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAliasHostnames('${uuid}');"><i class=" blue at icon"></i> Alias</button>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAccessRule('${uuid}');"><i class="ui filter icon"></i> Access Rule</button> <button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAccessRule('${uuid}');"><i class="ui filter icon"></i> Access Rule</button>
<button class="ui basic compact tiny ${domainIsCovered?"disabled":""} button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="requestCertificateForExistingHost('${uuid}', '${certificateDomains}');"><i class="green lock icon"></i> Get Certificate</button> <button class="ui basic compact tiny ${enableQuickRequestButton?"":"disabled"} button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="requestCertificateForExistingHost('${uuid}', '${certificateDomains}', this);"><i class="green lock icon"></i> Get Certificate</button>
`); `);
$(".hostAccessRuleSelector").dropdown(); $(".hostAccessRuleSelector").dropdown();
}else{ }else{
@ -536,9 +547,29 @@
Certificate Shortcut Certificate Shortcut
*/ */
function requestCertificateForExistingHost(hostUUID, RootAndAliasDomains){ function requestCertificateForExistingHost(hostUUID, RootAndAliasDomains, btn=undefined){
RootAndAliasDomains = JSON.parse(decodeURIComponent(RootAndAliasDomains)) RootAndAliasDomains = JSON.parse(decodeURIComponent(RootAndAliasDomains))
alert(RootAndAliasDomains.join(", ")) let renewDomainKey = RootAndAliasDomains.join(",");
let preferedACMEEmail = $("#prefACMEEmail").val();
if (preferedACMEEmail == ""){
msgbox("Preferred email for ACME registration not set", false);
return;
}
let defaultCA = $("#defaultCA").dropdown("get value");
if (defaultCA == ""){
defaultCA = "Let's Encrypt";
}
//Check if the root or the alias domain contain wildcard character, if yes, return error
for (var i = 0; i < RootAndAliasDomains.length; i++){
if (RootAndAliasDomains[i].indexOf("*") != -1){
msgbox("Wildcard domain can only be setup via ACME tool", false);
return;
}
}
//Renew the certificate
renewCertificate(renewDomainKey, false, btn);
} }
//Bind on tab switch events //Bind on tab switch events

View File

@ -17,10 +17,21 @@
<p>Discover mDNS enabled service in this gateway forwarded network</p> <p>Discover mDNS enabled service in this gateway forwarded network</p>
<button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/mdns.html',1000, 640);">View Discovery</button> <button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/mdns.html',1000, 640);">View Discovery</button>
<div class="ui divider"></div> <div class="ui divider"></div>
<!-- IP Scanner--> <div class="ui stackable grid">
<h2>IP Scanner</h2> <div class="eight wide column">
<p>Discover local area network devices by pinging them one by one</p> <!-- IP Scanner-->
<button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/ipscan.html',1000, 640);">Start Scanner</button> <h2>IP Scanner</h2>
<p>Discover local area network devices by pinging them one by one</p>
<button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/ipscan.html',1000, 640);">Start Scanner</button>
</div>
<div class="eight wide column">
<!-- Port Scanner-->
<h2>Port Scanner</h2>
<p>Scan for open ports on a given IP address</p>
<button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/portscan.html',1000, 640);">Start Scanner</button>
</div>
</div>
<div class="ui divider"></div> <div class="ui divider"></div>
<!-- Traceroute--> <!-- Traceroute-->
<h2>Traceroute / Ping</h2> <h2>Traceroute / Ping</h2>

View File

@ -165,18 +165,18 @@
<div class="ui basic segment rulesInstructions"> <div class="ui basic segment rulesInstructions">
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Domain</span><br> <span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Domain</span><br>
Example of domain matching keyword:<br> Example of domain matching keyword:<br>
<code>arozos.com</code> <br>Any acess requesting arozos.com will be proxy to the IP address below<br> <code>aroz.org</code> <br>Any acess requesting aroz.org will be proxy to the IP address below<br>
<div class="ui divider"></div> <div class="ui divider"></div>
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Subdomain</span><br> <span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Subdomain</span><br>
Example of subdomain matching keyword:<br> Example of subdomain matching keyword:<br>
<code>s1.arozos.com</code> <br>Any request starting with s1.arozos.com will be proxy to the IP address below<br> <code>s1.aroz.org</code> <br>Any request starting with s1.aroz.org will be proxy to the IP address below<br>
<div class="ui divider"></div> <div class="ui divider"></div>
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Wildcard</span><br> <span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Wildcard</span><br>
Example of wildcard matching keyword:<br> Example of wildcard matching keyword:<br>
<code>*.arozos.com</code> <br>Any request with a host name matching *.arozos.com will be proxy to the IP address below. Here are some examples.<br> <code>*.aroz.org</code> <br>Any request with a host name matching *.aroz.org will be proxy to the IP address below. Here are some examples.<br>
<div class="ui list"> <div class="ui list">
<div class="item"><code>www.arozos.com</code></div> <div class="item"><code>www.aroz.org</code></div>
<div class="item"><code>foo.bar.arozos.com</code></div> <div class="item"><code>foo.bar.aroz.org</code></div>
</div> </div>
<br> <br>
</div> </div>

View File

@ -1,10 +1,381 @@
<div class="standardContainer"> <div class="standardContainer">
<div class="ui basic segment"> <div class="ui basic segment">
<h2>Single-Sign-On</h2> <div class="ui message">
<p>Create and manage accounts with Zoraxy!</p> <div class="header">
Work in Progress
</div>
<p>The SSO feature is currently under development.</p>
</div>
</div> </div>
<div class="ui message"> </div>
<h4>Work In Progress</h4> <!--
We are looking for someone to help with implementing this feature in Zoraxy. <br>If you know how to write Golang and want to contribute, feel free to create a pull request to this feature! <div class="standardContainer">
<div class="ui basic segment">
<h2>Zoraxy SSO / Oauth</h2>
<p>A centralized authentication system for all your subdomains</p>
<div class="ui divider"></div>
<div class="ui basic segment enabled ssoRunningState">
<h4 class="ui header" id="ssoRunningState">
<i class="circle check icon"></i>
<div class="content">
<span class="webserv_status">Running</span>
<div class="sub header">Listen port :<span class="oauthserv_port">8081</span></div>
</div>
</h4>
</div>
<div class="ui form">
<h3 class="ui dividing header">Oauth2 Server Settings</h3>
<div class="field">
<div class="ui toggle checkbox">
<input type="checkbox" name="enableOauth2">
<label>Enable Oauth2 Server<br>
<small>Oauth2 server for handling external authentication requests</small></label>
</div>
</div>
<div class="field">
<label>Oauth2 Server Port</label>
<div class="ui action input">
<input type="number" name="oauth2Port" placeholder="Port" value="5488">
<button id="saveOauthServerPortBtn" class="ui basic green button"><i class="ui green circle check icon"></i> Update</button>
</div>
<small>Listening port of the Zoraxy internal Oauth2 Server.You can create a subdomain proxy rule to <code>127.0.0.1:<span class="ssoPort">5488</span></code></small>
</div>
<div class="field">
<label>Auth URL</label>
<div class="ui action input">
<input type="text" name="authURL" placeholder="https://auth.yourdomain.com">
<button id="saveAuthURLBtn" class="ui basic blue button"><i class="ui blue save icon"></i> Save</button>
</div>
<small>The exposed authentication URL of the Oauth2 server, usually <code>https://auth.example.com</code> or <code>https://sso.yourdomain.com</code>. <b>Remember to include the http:// or https:// in your URL.</b></small>
</div>
</div>
<br>
<div class="ui form">
<h3 class="ui dividing header">Zoraxy SSO Settings</h3>
<div class="field">
<label>Default Redirection URL </label>
<div class="ui fluid input">
<input type="text" name="defaultSiteURL" placeholder="https://yourdomain.com">
</div>
<small>The default URL to redirect to after login if redirection target is not set</small>
</div>
<button class="ui basic button"> <i class="ui green check icon"></i> Apply Changes </button>
</div>
<div class="ui basic message">
<div class="header">
<i class="ui yellow exclamation triangle icon"></i> Important Notes about Zoraxy SSO
</div>
<p>Zoraxy SSO, if enabled in HTTP Proxy rule, will automatically intercept the proxy request and provide an SSO interface on upstreams that do not support OAuth natively.
It is basically like basic auth with a login page. <b> The same user credential can be used in OAuth sign-in and Zoraxy SSO sign-in.</b>
</p>
</div>
<div class="ui divider"></div>
<div>
<h3 class="ui header">
<i class="ui blue user circle icon"></i>
<div class="content">
Registered Users
<div class="sub header">A list of users that are registered with the SSO server</div>
</div>
</h3>
<table class="ui celled table">
<thead>
<tr>
<th>Username</th>
<th>Registered On</th>
<th>Reset Password</th>
<th>Remove</th>
</tr>
</thead>
<tbody id="registeredSsoUsers">
<tr>
<td>admin</td>
<td>2020-01-01</td>
<td><button class="ui blue basic small icon button"><i class="ui blue key icon"></i></button></td>
<td><button class="ui red basic small icon button"><i class="ui red trash icon"></i></button></td>
</tr>
</tbody>
</table>
<button onclick="handleUserListRefresh();" class="ui basic right floated button"><i class="ui green refresh icon"></i> Refresh</button>
<button onclick="openRegisteredUserManager();" class="ui basic button"><i class="ui blue users icon"></i> Manage Registered Users</button>
</div>
<div class="ui divider"></div>
<div>
<h3 class="ui header">
<i class="ui green th icon"></i>
<div class="content">
Registered Apps
<div class="sub header">A list of apps that are registered with the SSO server</div>
</div>
</h3>
<table class="ui celled table">
<thead>
<tr>
<th>App Name</th>
<th>Domain</th>
<th>App ID</th>
<th>Registered On</th>
<th>Remove</th>
</tr>
</thead>
<tbody id="registeredSsoApps">
<tr>
<td>My App</td>
<td><a href="//example.com" target="_blank">example.com</a></td>
<td>123456</td>
<td>2020-01-01</td>
<td><button class="ui red basic small icon button"><i class="ui red trash icon"></i></button></td>
</tr>
</tbody>
</table>
<button onclick="handleRegisterAppListRefresh();" class="ui basic right floated button"><i class="ui green refresh icon"></i> Refresh</button>
<button onclick="openRegisterAppManagementSnippet();" class="ui basic button"><i style="font-size: 1em; margin-top: -0.2em;" class="ui green th large icon"></i> Manage Registered App</button>
<p></p>
</div>
</div> </div>
</div> </div>
<script>
$("input[name=oauth2Port]").on("change", function() {
$(".ssoPort").text($(this).val());
});
function updateSSOStatus(){
$.get("/api/sso/status", function(data){
if(data.error != undefined){
//Show error message
$(".ssoRunningState").removeClass("enabled").addClass("disabled");
$("#ssoRunningState .webserv_status").html('Error: '+data.error);
}else{
if (data.Enabled){
$(".ssoRunningState").addClass("enabled");
$("#ssoRunningState .webserv_status").html('Running');
$(".ssoRunningState i").attr("class", "circle check icon");
$("input[name=enableOauth2]").parent().checkbox("set checked");
}else{
$(".ssoRunningState").removeClass("enabled");
$("#ssoRunningState .webserv_status").html('Stopped');
$(".ssoRunningState i").attr("class", "circle times icon");
$("input[name=enableOauth2]").parent().checkbox("set unchecked");
}
$("input[name=oauth2Port]").val(data.ListeningPort);
$(".oauthserv_port").text(data.ListeningPort);
$("input[name=authURL]").val(data.AuthURL);
}
});
}
function initSSOStatus(){
$.get("/api/sso/status", function(data){
//Update the SSO status from the server
updateSSOStatus();
//Bind events to the enable checkbox
$("input[name=enableOauth2]").off("change").on("change", function(){
var checked = $(this).prop("checked");
$.cjax({
url: "/api/sso/enable",
method: "POST",
data: {
enable: checked
},
success: function(data){
if(data.error != undefined){
msgbox("Failed to toggle SSO: " + data.error, false);
//Unbind the event to prevent infinite loop
$("input[name=enableOauth2]").off("change");
}else{
initSSOStatus();
}
}
});
});
});
}
initSSOStatus();
/* Save the Oauth server port */
function saveOauthServerPort(){
var port = $("input[name=oauth2Port]").val();
//Check if the port is valid
if (port < 1 || port > 65535){
msgbox("Invalid port number", false);
return;
}
//Use cjax to send the port to the server with csrf token
$.cjax({
url: "/api/sso/setPort",
method: "POST",
data: {
port: port
},
success: function(data) {
if (data.error != undefined) {
msgbox("Failed to update Oauth server port: " + data.error, false);
} else {
msgbox("Oauth server port updated", true);
}
updateSSOStatus();
}
});
}
//Bind the save button to the saveOauthServerPort function
$("#saveOauthServerPortBtn").on("click", function() {
saveOauthServerPort();
});
$("input[name=oauth2Port]").on("keypress", function(e) {
if (e.which == 13) {
saveOauthServerPort();
}
});
/* Save the Oauth server URL (aka AuthURL) */
function saveAuthURL(){
var url = $("input[name=authURL]").val();
//Make sure the url contains http:// or https://
if (!url.startsWith("http://") && !url.startsWith("https://")){
msgbox("Invalid URL. Make sure to include http:// or https://", false);
$("input[name=authURL]").parent().parent().addClass("error");
return;
}else{
$("input[name=authURL]").parent().parent().removeClass("error");
}
//Use cjax to send the port to the server with csrf token
$.cjax({
url: "/api/sso/setAuthURL",
method: "POST",
data: {
"auth_url": url
},
success: function(data) {
if (data.error != undefined) {
msgbox("Failed to update Oauth server port: " + data.error, false);
} else {
msgbox("Oauth server port updated", true);
}
updateSSOStatus();
}
});
}
//Bind the save button to the saveAuthURL function
$("#saveAuthURLBtn").on("click", function() {
saveAuthURL();
});
$("input[name=authURL]").on("keypress", function(e) {
if (e.which == 13) {
saveAuthURL();
}
});
/* Registered Apps Event Handlers */
//Function to initialize the registered app table
function initRegisteredAppTable(){
$.get("/api/sso/app/list", function(data){
if(data.error != undefined){
msgbox("Failed to get registered apps: " + data.error, false);
}else{
var tbody = $("#registeredSsoApps");
tbody.empty();
for(var i = 0; i < data.length; i++){
var app = data[i];
var tr = $("<tr>");
tr.append($("<td>").text(app.AppName));
tr.append($("<td>").html('<a href="//'+app.Domain+'" target="_blank">'+app.Domain+'</a>'));
tr.append($("<td>").text(app.AppID));
tr.append($("<td>").text(app.RegisteredOn));
var removeBtn = $("<button>").addClass("ui red basic small icon button").html('<i class="ui red trash icon"></i>');
removeBtn.on("click", function(){
removeApp(app.AppID);
});
tr.append($("<td>").append(removeBtn));
tbody.append(tr);
}
if (data.length == 0){
tbody.append($("<tr>").append($("<td>").attr("colspan", 4).html(`<i class="ui green circle check icon"></i> No registered users`)));
}
}
});
}
initRegisteredAppTable();
//Also bind the refresh button to the initRegisteredAppTable function
function handleRegisterAppListRefresh(){
initRegisteredAppTable();
}
function openRegisterAppManagementSnippet(){
//Open the register app management snippet
showSideWrapper("snippet/sso_app.html");
}
//Bind the remove button to the removeApp function
function removeApp(appID){
$.cjax({
url: "/api/sso/removeApp",
method: "POST",
data: {
appID: appID
},
success: function(data){
if(data.error != undefined){
msgbox("Failed to remove app: " + data.error, false);
}else{
msgbox("App removed", true);
updateSSOStatus();
}
}
});
}
/* Registered Users Event Handlers */
function initUserList(){
$.get("/api/sso/user/list", function(data){
if(data.error != undefined){
msgbox("Failed to get registered users: " + data.error, false);
}else{
var tbody = $("#registeredSsoUsers");
tbody.empty();
for(var i = 0; i < data.length; i++){
var user = data[i];
var tr = $("<tr>");
tr.append($("<td>").text(user.Username));
tr.append($("<td>").text(user.RegisteredOn));
var resetBtn = $("<button>").addClass("ui blue basic small icon button").html('<i class="ui blue key icon"></i>');
resetBtn.on("click", function(){
resetPassword(user.Username);
});
tr.append($("<td>").append(resetBtn));
var removeBtn = $("<button>").addClass("ui red basic small icon button").html('<i class="ui red trash icon"></i>');
removeBtn.on("click", function(){
removeUser(user.Username);
});
tr.append($("<td>").append(removeBtn));
tbody.append(tr);
}
if (data.length == 0){
tbody.append($("<tr>").append($("<td>").attr("colspan", 4).html(`<i class="ui green circle check icon"></i> No registered users`)));
}
}
});
}
//Bind the refresh button to the initUserList function
function handleUserListRefresh(){
initUserList();
}
function openRegisteredUserManager(){
//Open the registered user management snippet
showSideWrapper("snippet/sso_user.html");
}
</script>
-->

View File

@ -74,7 +74,7 @@
</div> </div>
</div> </div>
<button id="addStreamProxyButton" class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button> <button id="addStreamProxyButton" class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
<button id="editStreamProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);" style="display:none;"><i class="ui green check icon"></i> Update</button> <button id="editStreamProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event, this);" style="display:none;"><i class="ui green check icon"></i> Update</button>
<button class="ui basic red button" onclick="event.preventDefault(); cancelStreamProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button> <button class="ui basic red button" onclick="event.preventDefault(); cancelStreamProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
</form> </form>
</div> </div>
@ -88,7 +88,7 @@
//Check if update mode //Check if update mode
if ($("#editStreamProxyButton").is(":visible")){ if ($("#editStreamProxyButton").is(":visible")){
confirmEditTCPProxyConfig(event); confirmEditTCPProxyConfig(event,$("#editStreamProxyButton")[0]);
return; return;
} }
@ -274,13 +274,18 @@
} }
} }
function confirmEditTCPProxyConfig(event){ function confirmEditTCPProxyConfig(event, btn){
event.preventDefault(); event.preventDefault();
event.stopImmediatePropagation(); event.stopImmediatePropagation();
var form = $("#streamProxyForm"); var form = $("#streamProxyForm");
let originalButtonHTML = $(btn).html();
$(btn).html(`<i class="ui loading spinner icon"></i> Updating`);
$(btn).addClass("disabled");
var formValid = validateTCPProxyConfig(form); var formValid = validateTCPProxyConfig(form);
if (!formValid){ if (!formValid){
$(btn).html(originalButtonHTML);
$(btn).removeClass("disabled");
return; return;
} }
@ -299,6 +304,8 @@
timeout: parseInt($("#streamProxyForm input[name=timeout]").val().trim()), timeout: parseInt($("#streamProxyForm input[name=timeout]").val().trim()),
}, },
success: function(response) { success: function(response) {
$(btn).html(originalButtonHTML);
$(btn).removeClass("disabled");
if (response.error) { if (response.error) {
msgbox(response.error, false, 6000); msgbox(response.error, false, 6000);
}else{ }else{
@ -310,6 +317,8 @@
}, },
error: function() { error: function() {
$(btn).html(originalButtonHTML);
$(btn).removeClass("disabled");
msgbox('An error occurred while processing the request', false); msgbox('An error occurred while processing the request', false);
} }
}); });

View File

@ -46,7 +46,7 @@
<form id="email-form" class="ui form"> <form id="email-form" class="ui form">
<div class="field"> <div class="field">
<label>Sender Address</label> <label>Sender Address</label>
<input type="text" name="senderAddr" placeholder="E.g. noreply@zoraxy.arozos.com"> <input type="text" name="senderAddr" placeholder="E.g. noreply@zoraxy.aroz.org">
</div> </div>
<div class="field"> <div class="field">
<p><i class="caret down icon"></i> Connection setup for email service provider</p> <p><i class="caret down icon"></i> Connection setup for email service provider</p>

View File

@ -160,7 +160,7 @@
<br><br> <br><br>
<div class="ui divider"></div> <div class="ui divider"></div>
<div class="ui container" style="color: grey; font-size: 90%"> <div class="ui container" style="color: grey; font-size: 90%">
<p><a href="https://zoraxy.arozos.com" target="_blank">Zoraxy</a> <span class="zrversion"></span> © 2021 - <span class="year"></span> tobychui. Licensed under AGPL</p> <p><a href="https://zoraxy.aroz.org" target="_blank">Zoraxy</a> <span class="zrversion"></span> © 2021 - <span class="year"></span> tobychui. Licensed under AGPL</p>
</div> </div>
<div id="messageBox" class="ui green floating big compact message"> <div id="messageBox" class="ui green floating big compact message">

View File

@ -140,7 +140,7 @@
<div class="field registerOnly"> <div class="field registerOnly">
<div class="ui left icon input"> <div class="ui left icon input">
<i class="lock icon"></i> <i class="lock icon"></i>
<input id="repeatMagic" type="password" name="passwordconfirm" placeholder="Confirm Password"> <input id="repeatMagic" type="password" name="passwordconfirm" placeholder="Confirm Password" >
</div> </div>
</div> </div>
<div class="field loginOnly" style="text-align: left;"> <div class="field loginOnly" style="text-align: left;">
@ -175,11 +175,11 @@
</svg> </svg>
</div> </div>
<div class="wavebase"> <div class="wavebase">
<p>Proudly powered by <a href="https://zoraxy.arozos.com" target="_blank">Zoraxy</a></p> <p>Proudly powered by <a href="https://zoraxy.aroz.org" target="_blank">Zoraxy</a></p>
</div> </div>
<script> <script>
AOS.init(); AOS.init();
var registerMode = false;
var redirectionAddress = "/"; var redirectionAddress = "/";
var loginAddress = "/api/auth/login"; var loginAddress = "/api/auth/login";
$(".checkbox").checkbox(); $(".checkbox").checkbox();
@ -197,6 +197,7 @@
$.get("/api/auth/userCount", function(data){ $.get("/api/auth/userCount", function(data){
if (data == 0){ if (data == 0){
//Allow user creation //Allow user creation
registerMode = true;
$(".loginOnly").hide(); $(".loginOnly").hide();
$(".registerOnly").show(); $(".registerOnly").show();
} }
@ -240,13 +241,23 @@
$("input").on("keydown",function(event){ $("input").on("keydown",function(event){
if (event.keyCode === 13) { if (event.keyCode === 13) {
event.preventDefault(); event.preventDefault();
if ($(this).attr("id") == "magic"){ if (registerMode){
login(); //Register mode
if ($(this).attr("id") == "repeatMagic"){
$("#regsiterbtn").click();
}else{
//Focus to next field
$(this).next().focus();
}
}else{ }else{
//Fuocus to password field //Login mode
$("#magic").focus(); if ($(this).attr("id") == "magic"){
login();
}else{
//Fuocus to password field
$("#magic").focus();
}
} }
} }
}); });

View File

@ -614,6 +614,29 @@ body{
background: var(--theme_green) !important; background: var(--theme_green) !important;
} }
/*
SSO Panel
*/
.ssoRunningState{
padding: 1em;
border-radius: 1em !important;
}
.ssoRunningState .ui.header, .ssoRunningState .sub.header{
color: white !important;
}
.ssoRunningState:not(.enabled){
background: var(--theme_red) !important;
}
.ssoRunningState.enabled{
background: var(--theme_green) !important;
}
/* /*
Static Web Server Static Web Server
*/ */

View File

@ -201,7 +201,7 @@
</svg> </svg>
</div> </div>
<div class="wavebase"> <div class="wavebase">
<p>Proudly powered by <a href="https://zoraxy.arozos.com" target="_blank">Zoraxy</a></p> <p>Proudly powered by <a href="https://zoraxy.aroz.org" target="_blank">Zoraxy</a></p>
</div> </div>
<script> <script>
AOS.init(); AOS.init();

View File

@ -91,8 +91,11 @@
<div class="ui form"> <div class="ui form">
<div class="field"> <div class="field">
<label>Domain(s)</label> <label>Domain(s)</label>
<input id="domainsInput" type="text" placeholder="example.com" onkeyup="checkIfInputDomainIsMultiple();"> <input id="domainsInput" type="text" placeholder="example.com" onkeyup="handlePostInputAutomation();">
<small>If you have more than one domain in a single certificate, enter the domains separated by commas (e.g. s1.dev.example.com,s2.dev.example.com)</small> <small>If you have more than one domain in a single certificate, enter the domains separated by commas (e.g. s1.dev.example.com,s2.dev.example.com)
<span id="caNoDNSSupportWarning" style="color: #ffaf2e; display:none;"><br> <i class="exclamation triangle icon"></i> Current selected CA do not support DNS challenge</span>
</small>
</div> </div>
<div class="field multiDomainOnly" style="display:none;"> <div class="field multiDomainOnly" style="display:none;">
<label>Matching Rule</label> <label>Matching Rule</label>
@ -113,7 +116,6 @@
<div class="item" data-value="Buypass">Buypass</div> <div class="item" data-value="Buypass">Buypass</div>
<div class="item" data-value="ZeroSSL">ZeroSSL</div> <div class="item" data-value="ZeroSSL">ZeroSSL</div>
<div class="item" data-value="Custom ACME Server">Custom ACME Server</div> <div class="item" data-value="Custom ACME Server">Custom ACME Server</div>
<!-- <div class="item" data-value="Google">Google</div> -->
</div> </div>
</div> </div>
</div> </div>
@ -136,7 +138,7 @@
</div> </div>
<div class="field dnsChallengeOnly" style="display:none;"> <div class="field dnsChallengeOnly" style="display:none;">
<div class="ui divider"></div> <div class="ui divider"></div>
<p>DNS Credentials</p> <p>DNS Credentials</p>
<div id="dnsProviderAPIFields"> <div id="dnsProviderAPIFields">
<p><i class="ui loading circle notch icon"></i> Generating WebForm</p> <p><i class="ui loading circle notch icon"></i> Generating WebForm</p>
</div> </div>
@ -389,7 +391,7 @@
}); });
//On CA change in dropdown //On CA change in dropdown
$("input[name=ca]").on('change', function() { $("input[name=ca]").on('change', function() {
if(this.value == "Custom ACME Server") { if(this.value == "Custom ACME Server") {
@ -432,6 +434,7 @@
$("#dnsProviderAPIFields").html(""); $("#dnsProviderAPIFields").html("");
//Generate a form for this config //Generate a form for this config
let booleanFieldsHTML = ""; let booleanFieldsHTML = "";
let optionalFieldsHTML = "";
for (const [key, datatype] of Object.entries(data)) { for (const [key, datatype] of Object.entries(data)) {
if (datatype == "int"){ if (datatype == "int"){
$("#dnsProviderAPIFields").append(`<div class="ui fluid labeled dnsConfigField input" key="${key}" style="margin-top: 0.2em;"> $("#dnsProviderAPIFields").append(`<div class="ui fluid labeled dnsConfigField input" key="${key}" style="margin-top: 0.2em;">
@ -445,6 +448,26 @@
<input type="checkbox"> <input type="checkbox">
<label>${key}</label> <label>${key}</label>
</div>`); </div>`);
}else if (datatype == "time.Duration"){
let defaultIntValue = 120;
let defaultMinValue = 30;
if (key == "PollingInterval"){
defaultIntValue = 2;
defaultMinValue = 1;
}else if (key == "PropagationTimeout"){
defaultIntValue = 120;
defaultMinValue = 30;
}
optionalFieldsHTML += (`<div class="ui fluid labeled dnsConfigField small input" key="${key}" style="margin-top: 0.2em;">
<div class="ui basic blue label" style="font-weight: 300;">
${key}
</div>
<input type="number" min="${defaultMinValue}" value="${defaultIntValue}">
<div class="ui basic label" style="font-weight: 300;">
secs
</div>
</div>`);
}else{ }else{
//Default to string //Default to string
$("#dnsProviderAPIFields").append(`<div class="ui fluid labeled input dnsConfigField" key="${key}" style="margin-top: 0.2em;"> $("#dnsProviderAPIFields").append(`<div class="ui fluid labeled input dnsConfigField" key="${key}" style="margin-top: 0.2em;">
@ -461,6 +484,9 @@
if (booleanFieldsHTML != ""){ if (booleanFieldsHTML != ""){
$(".dnsConfigField.checkbox").checkbox(); $(".dnsConfigField.checkbox").checkbox();
} }
//Append the optional fields at the bottom, if exists
$("#dnsProviderAPIFields").append(optionalFieldsHTML);
}); });
}); });
@ -740,6 +766,7 @@
}); });
} }
//Check if the entered domain contains multiple domains
function checkIfInputDomainIsMultiple(){ function checkIfInputDomainIsMultiple(){
var inputDomains = $("#domainsInput").val(); var inputDomains = $("#domainsInput").val();
if (inputDomains.includes(",")){ if (inputDomains.includes(",")){
@ -749,6 +776,35 @@
} }
} }
//Validate if the current combinations of domain and CA supports DNS challenge
function validateDNSChallengeSupport(){
if ($("#domainsInput").val().includes("*")){
var ca = $("#ca").dropdown("get value");
if (ca == "Let's Encrypt" || ca == ""){
$("#caNoDNSSupportWarning").hide();
}else{
$("#caNoDNSSupportWarning").show();
}
}else{
$("#caNoDNSSupportWarning").hide();
}
}
//call to validateDNSChallengeSupport() on #ca value change
$("#ca").dropdown({
onChange: function(value, text, $selectedItem) {
validateDNSChallengeSupport();
}
});
//Handle the input change event on domain input
function handlePostInputAutomation(){
checkIfInputDomainIsMultiple();
validateDNSChallengeSupport();
}
function toggleDnsChallenge(){ function toggleDnsChallenge(){
if ( $("#useDnsChallenge")[0].checked){ if ( $("#useDnsChallenge")[0].checked){
$(".dnsChallengeOnly").show(); $(".dnsChallengeOnly").show();

View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
<script src="../script/utils.js"></script>
<style>
body{
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<br>
<div class="ui container">
<div class="ui basic segment">
<h2 class="ui header">SSO App Management</h2>
<div class="ui divider"></div>
<h3>Work in progress</h3>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
<script src="../script/utils.js"></script>
<style>
body{
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<br>
<div class="ui container">
<div class="ui basic segment">
<h2 class="ui header">SSO User Management</h2>
<div class="ui divider"></div>
<h3>Work in progress</h3>
</div>
</div>
</body>
</html>

View File

@ -10,7 +10,6 @@
<title>IP Scanner | Zoraxy</title> <title>IP Scanner | Zoraxy</title>
<link rel="stylesheet" href="../script/semantic/semantic.min.css"> <link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script> <script src="../script/jquery-3.6.0.min.js"></script>
<script src="../../script/ao_module.js"></script>
<script src="../script/semantic/semantic.min.js"></script> <script src="../script/semantic/semantic.min.js"></script>
<script src="../script/tablesort.js"></script> <script src="../script/tablesort.js"></script>
<link rel="stylesheet" href="../main.css"> <link rel="stylesheet" href="../main.css">
@ -149,13 +148,23 @@
function displayResults(data) { function displayResults(data) {
var table = $('<table class="ui celled unstackable table"></table>'); var table = $('<table class="ui celled unstackable table"></table>');
var header = $('<thead><tr><th>IP Address</th><th>Ping</th><th>Hostname</th><th>HTTP Detected</th><th>HTTPS Detected</th></tr></thead>'); var header = $(`<thead>
<tr>
<th>IP Address</th>
<th>Ping</th>
<th>Hostname</th>
<th>HTTP Detected</th>
<th>HTTPS Detected</th>
<th>Port Scan</th>
</tr>
</thead>`);
table.append(header); table.append(header);
var body = $('<tbody></tbody>'); var body = $('<tbody></tbody>');
var offlineHostCounter = 0; var offlineHostCounter = 0;
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
var classname = "offlinehost"; var classname = "offlinehost";
if (data[i].Ping>=0){ let hostIsOnline = data[i].Ping >= 0;
if (hostIsOnline){
classname = "onlinehost"; classname = "onlinehost";
}else{ }else{
offlineHostCounter++; offlineHostCounter++;
@ -167,6 +176,7 @@
row.append($('<td>' + data[i].Hostname + '</td>')); row.append($('<td>' + data[i].Hostname + '</td>'));
row.append($('<td>' + (data[i].HttpPortDetected ? '<i class="green check icon"></i>' : '') + '</td>')); row.append($('<td>' + (data[i].HttpPortDetected ? '<i class="green check icon"></i>' : '') + '</td>'));
row.append($('<td>' + (data[i].HttpsPortDetected ? '<i class="green check icon"></i>' : '') + '</td>')); row.append($('<td>' + (data[i].HttpsPortDetected ? '<i class="green check icon"></i>' : '') + '</td>'));
row.append($(`<td>${hostIsOnline ? `<button class="ui small basic button" onclick='launchToolWithSize("portscan.html?ip=${data[i].IP}", 1000, 640);'>Scan</button>`:''}</td>`));
body.append(row); body.append(row);
} }
@ -198,6 +208,21 @@
function toggleOfflineHost(){ function toggleOfflineHost(){
$(".offlinehost").toggle(); $(".offlinehost").toggle();
} }
function launchToolWithSize(url, width, height){
let windowName = Date.now();
window.open(url,'w'+windowName,
`toolbar=no,
location=no,
status=no,
menubar=no,
scrollbars=yes,
resizable=yes,
width=${width},
height=${height}`);
}
</script> </script>
</body> </body>
</html> </html>

120
src/web/tools/portscan.html Normal file
View File

@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
<meta name="theme-color" content="#4b75ff">
<link rel="icon" type="image/png" href="./favicon.png" />
<title>Port Scanner | Zoraxy</title>
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
<script src="../script/tablesort.js"></script>
<link rel="stylesheet" href="../main.css">
<script src="../script/utils.js"></script>
</head>
</head>
<body>
<div class="ui container">
<br>
<div class="ui segment">
<p>Enter the IP address you want to scan for open ports. This tool only scans for open TCP ports.</p>
<div class="ui fluid action input">
<input id="scanningIP" type="text" placeholder="IP Address">
<button class="ui basic blue button" onclick="startScan()">Start Scan</button>
</div>
<div class="ui yellow message">
<h4 class="ui header">
<i class="exclamation triangle icon"></i>
<div class="content">
Port Scan Warning
<div class="sub header">Please ensure that you only scan IP addresses that you own or have explicit permission to scan. Unauthorized scanning may be considered a network attack and could result in legal consequences.</div>
</div>
</h4>
</div>
<table class="ui celled compact table">
<thead>
<tr>
<th>Port</th>
<th>TCP Port Open</th>
<th>Full Path</th>
</tr>
</thead>
<tbody id="resultTable">
<tr>
<td colspan="3"><i class="ui green circle check icon"></i> Click the "Start Scan" to start port scan on given IP address</td>
</tr>
</tbody>
</table>
<button class="ui right floated basic button" onclick="exitTool();">Exit</button>
<br><br>
</div>
</div>
<br>
<br>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js"></script>
<script>
//If the URL has a query parameter, fill the input box with that value
$(document).ready(function(){
let urlParams = new URLSearchParams(window.location.search);
let ipToScan = urlParams.get("ip");
if (ipToScan != null){
$("#scanningIP").val(ipToScan);
startScan();
}
});
function startScan(button=undefined){
let ipToScan = $("#scanningIP").val();
ipToScan = ipToScan.trim();
let table = $("#resultTable");
table.empty();
table.append($("<tr><td colspan='3'><i class='ui loading spinner icon'></i> Scanning</td></tr>"));
if (button != undefined){
button.addClass("loading");
}
$.get("/api/tools/portscan?ip=" + ipToScan, function(data){
if (button != undefined){
button.removeClass("loading");
}
if (data.error != undefined){
alert(data.error);
return;
}else{
table.empty();
//Entries are in the form of {Port: 80, IsTCP: true, IsUDP: false}
//if both TCP and UDP are open, there will be two entries
for (let i = 0; i < data.length; i++){
let row = $("<tr></tr>");
row.append($("<td></td>").text(data[i].Port));
row.append($("<td><i class='ui green check icon'></i></td>"));
row.append($("<td></td>").html(`<a href="//${ipToScan + ":" + data[i].Port}" target="_blank">${ipToScan + ":" + data[i].Port}</a>`));
table.append(row);
}
if (data.length == 0){
table.append($("<tr><td colspan='3'><i class='ui green circle check icon'></i> No open ports found on given IP address</td></tr>"));
}
}
console.log(data);
});
}
function exitTool(){
//Close the current window
window.open('', '_self', '');
window.close();
}
</script>
</body>
</html>

View File

@ -26,7 +26,6 @@ import (
"imuslab.com/zoraxy/mod/dynamicproxy" "imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/ipscan"
"imuslab.com/zoraxy/mod/mdns" "imuslab.com/zoraxy/mod/mdns"
"imuslab.com/zoraxy/mod/uptime" "imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
@ -267,44 +266,6 @@ func HandleMdnsScanning(w http.ResponseWriter, r *http.Request) {
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
} }
// handle ip scanning
func HandleIpScan(w http.ResponseWriter, r *http.Request) {
cidr, err := utils.PostPara(r, "cidr")
if err != nil {
//Ip range mode
start, err := utils.PostPara(r, "start")
if err != nil {
utils.SendErrorResponse(w, "missing start ip")
return
}
end, err := utils.PostPara(r, "end")
if err != nil {
utils.SendErrorResponse(w, "missing end ip")
return
}
discoveredHosts, err := ipscan.ScanIpRange(start, end)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(discoveredHosts)
utils.SendJSONResponse(w, string(js))
} else {
//CIDR mode
discoveredHosts, err := ipscan.ScanCIDRRange(cidr)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(discoveredHosts)
utils.SendJSONResponse(w, string(js))
}
}
/* /*
WAKE ON LAN WAKE ON LAN

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -36,9 +36,11 @@ import (
//name is the DNS provider name, e.g. cloudflare or gandi //name is the DNS provider name, e.g. cloudflare or gandi
//JSON (js) must be in key-value string that match ConfigableFields Title in providers.json, e.g. {"Username":"far","Password":"boo"} //JSON (js) must be in key-value string that match ConfigableFields Title in providers.json, e.g. {"Username":"far","Password":"boo"}
func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, error){ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64, pollingInterval int64)(challenge.Provider, error){
pgDuration := time.Duration(propagationTimeout) * time.Second
plInterval := time.Duration(pollingInterval) * time.Second
switch name { switch name {
{{magic}} {{magic}}
default: default:
return nil, fmt.Errorf("unrecognized DNS provider: %s", name) return nil, fmt.Errorf("unrecognized DNS provider: %s", name)
} }
@ -79,12 +81,19 @@ func getExcludedDNSProviders() []string {
"selectelv2", //Not sure why not working with our code generator "selectelv2", //Not sure why not working with our code generator
"designate", //OpenStack, if you are using this you shd not be using zoraxy "designate", //OpenStack, if you are using this you shd not be using zoraxy
"mythicbeasts", //Module require url.URL, which cannot be automatically parsed "mythicbeasts", //Module require url.URL, which cannot be automatically parsed
"directadmin", //Reserve for next dependency update
//The following are incomaptible with Zoraxy due to dependencies issue,
//might be resolved in future
"corenetworks",
"timewebcloud",
"volcengine",
"exoscale",
} }
} }
// Exclude list for Windows build, due to limitations for lego versions // Exclude list for Windows build, due to limitations for lego versions
func getExcludedDNSProvidersNT61() []string { func getExcludedDNSProvidersNT61() []string {
fmt.Println("Windows 7 support is deprecated, please consider upgrading to a newer version of Windows.")
return append(getExcludedDNSProviders(), []string{"cpanel", return append(getExcludedDNSProviders(), []string{"cpanel",
"mailinabox", "mailinabox",
"shellrent", "shellrent",
@ -251,6 +260,14 @@ func main() {
Datatype: fields[1], Datatype: fields[1],
}) })
} }
case "time.Duration":
if fields[0] == "PropagationTimeout" || fields[0] == "PollingInterval" {
configKeys = append(configKeys, &Field{
Title: fields[0],
Datatype: fields[1],
})
}
default: default:
//Not used fields //Not used fields
hiddenKeys = append(hiddenKeys, &Field{ hiddenKeys = append(hiddenKeys, &Field{
@ -275,21 +292,9 @@ func main() {
if err != nil { if err != nil {
return nil, err return nil, err
} }
cfg.PropagationTimeout = 5*time.Minute cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return ` + providerName + `.NewDNSProviderConfig(cfg)` return ` + providerName + `.NewDNSProviderConfig(cfg)`
//Add fixed for Netcup timeout
if strings.ToLower(providerName) == "netcup" {
codeSegment = `
case "` + providerName + `":
cfg := ` + providerName + `.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 20*time.Minute
return ` + providerName + `.NewDNSProviderConfig(cfg)`
}
generatedConvertcode += codeSegment generatedConvertcode += codeSegment
importList += ` "github.com/go-acme/lego/v4/providers/dns/` + providerName + "\"\n" importList += ` "github.com/go-acme/lego/v4/providers/dns/` + providerName + "\"\n"
} }

View File

@ -18,7 +18,7 @@ fi
# Run the extract.go to get all the config from lego source code # Run the extract.go to get all the config from lego source code
echo "Generating code" echo "Generating code"
go run ./extract.go go run ./extract.go
go run ./extract.go -- "win7" # go run ./extract.go -- "win7"
echo "Cleaning up lego" echo "Cleaning up lego"
sleep 2 sleep 2