mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-01 13:17:21 +02:00
commit
4577fb1f2f
@ -51,7 +51,7 @@ If you have no background in setting up reverse proxy or web routing, you should
|
||||
|
||||
## Build from Source
|
||||
|
||||
Requires Go 1.22 or higher
|
||||
Requires Go 1.23 or higher
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tobychui/zoraxy
|
||||
|
31
src/acme.go
31
src/acme.go
@ -85,9 +85,20 @@ func acmeRegisterSpecialRoutingRule() {
|
||||
// This function check if the renew setup is satisfied. If not, toggle them automatically
|
||||
func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
isForceHttpsRedirectEnabledOriginally := false
|
||||
requireRestorePort80 := false
|
||||
dnsPara, _ := utils.PostBool(r, "dns")
|
||||
if !dnsPara {
|
||||
|
||||
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
|
||||
if !dynamicProxyRouter.Option.ForceHttpsRedirect {
|
||||
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
|
||||
time.Sleep(3 * time.Second)
|
||||
//Add a 2 second delay to make sure everything is settle down
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Pass over to the acmeHandler to deal with the communication
|
||||
acmeHandler.HandleRenewCertificate(w, r)
|
||||
@ -117,13 +128,17 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
|
||||
tlsCertManager.UpdateLoadedCertList()
|
||||
|
||||
//Restore original settings
|
||||
if dynamicProxyRouter.Option.Port == 443 && !dnsPara {
|
||||
if !isForceHttpsRedirectEnabledOriginally {
|
||||
//Default is off. Turn the redirection off
|
||||
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
|
||||
}
|
||||
if requireRestorePort80 {
|
||||
//Restore port 80 listener
|
||||
SystemWideLogger.PrintAndLog("ACME", "Restoring previous port 80 listener settings", nil)
|
||||
dynamicProxyRouter.UpdatePort80ListenerState(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
|
||||
|
19
src/api.go
19
src/api.go
@ -8,6 +8,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||
"imuslab.com/zoraxy/mod/acme/acmewizard"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/ipscan"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
@ -95,6 +96,21 @@ func initAPIs(targetMux *http.ServeMux) {
|
||||
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
|
||||
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
|
||||
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
|
||||
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
|
||||
@ -172,7 +188,8 @@ func initAPIs(targetMux *http.ServeMux) {
|
||||
authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset)
|
||||
|
||||
//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/ping", netutils.HandlePing)
|
||||
authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois)
|
||||
|
204
src/go.mod
204
src/go.mod
@ -1,107 +1,127 @@
|
||||
module imuslab.com/zoraxy
|
||||
|
||||
go 1.21
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.22.2
|
||||
|
||||
require (
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/docker/docker v27.0.0+incompatible
|
||||
github.com/go-acme/lego/v4 v4.16.1
|
||||
github.com/go-acme/lego/v4 v4.19.2
|
||||
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/gorilla/sessions v1.2.2
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/grandcat/zeroconf v1.0.0
|
||||
github.com/likexian/whois v1.15.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/sys v0.20.0
|
||||
golang.org/x/text v0.15.0
|
||||
golang.org/x/net v0.29.0
|
||||
golang.org/x/sys v0.25.0
|
||||
golang.org/x/text v0.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.25.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/auth v0.9.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/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/azidentity v1.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.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/privatedns/armprivatedns v1.1.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.7.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.29 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // 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/to v0.4.0 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // 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/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 // indirect
|
||||
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // 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.2.10 // 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.7.3 // 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.10.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.34.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.37.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.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.26.7 // indirect
|
||||
github.com/aws/smithy-go v1.19.0 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.33 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect
|
||||
github.com/aws/smithy-go v1.20.4 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/civo/civogo v0.3.11 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.86.0 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.104.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.9.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // 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-units v0.5.0 // indirect
|
||||
github.com/exoscale/egoscale v0.102.3 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-resty/resty/v2 v2.11.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/go-oauth2/oauth2/v4 v4.5.2
|
||||
github.com/go-resty/resty/v2 v2.13.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.2 // indirect
|
||||
github.com/gophercloud/gophercloud v1.0.0 // indirect
|
||||
github.com/gorilla/csrf v1.7.2 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||
github.com/gophercloud/gophercloud v1.14.0 // indirect
|
||||
github.com/gorilla/csrf v1.7.2
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // 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/infobloxopen/infoblox-go-client v1.1.1 // 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/labbsr0x/bindman-dns-webhook v1.0.2 // 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-go v1.6.4 // 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/mitchellh/go-homedir v1.1.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/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||
github.com/nrdcg/auroradns v1.1.0 // indirect
|
||||
github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 // indirect
|
||||
github.com/nrdcg/desec v0.7.0 // indirect
|
||||
github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect
|
||||
github.com/nrdcg/desec v0.8.0 // indirect
|
||||
github.com/nrdcg/dnspod-go v0.4.0 // indirect
|
||||
github.com/nrdcg/freemyip v0.2.0 // indirect
|
||||
github.com/nrdcg/goinwx v0.10.0 // indirect
|
||||
github.com/nrdcg/mailinabox v0.2.0 // indirect
|
||||
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||
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/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/ovh/go-ovh v1.4.3 // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/ovh/go-ovh v1.6.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // 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/sacloud/api-client-go v0.2.8 // indirect
|
||||
github.com/sacloud/go-http v0.1.6 // indirect
|
||||
github.com/sacloud/iaas-api-go v1.11.1 // indirect
|
||||
github.com/sacloud/packages-go v0.0.9 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22 // indirect
|
||||
github.com/sacloud/api-client-go v0.2.10 // indirect
|
||||
github.com/sacloud/go-http v0.1.8 // indirect
|
||||
github.com/sacloud/iaas-api-go v1.12.0 // indirect
|
||||
github.com/sacloud/packages-go v0.0.10 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||
github.com/softlayer/softlayer-go v1.1.3 // indirect
|
||||
github.com/softlayer/softlayer-go v1.1.5 // indirect
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/spf13/cast v1.6.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/dnspod v1.0.490 // indirect
|
||||
github.com/transip/gotransip/v6 v6.23.0 // indirect
|
||||
github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 // indirect
|
||||
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||
github.com/ultradns/ultradns-go-sdk v1.7.0-20240913052650-970ca9a // indirect
|
||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||
github.com/vultr/govultr/v2 v2.17.2 // indirect
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f // indirect
|
||||
github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 // indirect
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2 // indirect
|
||||
github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
|
||||
go.opentelemetry.io/otel v1.27.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
|
||||
go.opentelemetry.io/otel v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.27.0 // indirect
|
||||
go.uber.org/ratelimit v0.2.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/oauth2 v0.18.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
google.golang.org/api v0.169.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect
|
||||
google.golang.org/grpc v1.64.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||
go.uber.org/ratelimit v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
golang.org/x/tools v0.25.0 // indirect
|
||||
google.golang.org/api v0.197.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/grpc v1.66.1 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
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.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
|
597
src/go.sum
597
src/go.sum
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/auth/sso"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/dockerux"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
@ -59,7 +60,7 @@ var enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade
|
||||
|
||||
var (
|
||||
name = "Zoraxy"
|
||||
version = "3.1.1"
|
||||
version = "3.1.2"
|
||||
nodeUUID = "generic" //System uuid, in uuidv4 format
|
||||
development = false //Set this to false to use embedded web fs
|
||||
bootTime = time.Now().Unix()
|
||||
@ -95,6 +96,7 @@ var (
|
||||
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
|
||||
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
|
||||
loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing
|
||||
ssoHandler *sso.SSOHandler //Single Sign On handler
|
||||
|
||||
//Helper modules
|
||||
EmailSender *email.Sender //Email sender that handle email sending
|
||||
|
@ -30,10 +30,11 @@ import (
|
||||
)
|
||||
|
||||
type CertificateInfoJSON struct {
|
||||
AcmeName string `json:"acme_name"`
|
||||
AcmeUrl string `json:"acme_url"`
|
||||
SkipTLS bool `json:"skip_tls"`
|
||||
UseDNS bool `json:"dns"`
|
||||
AcmeName string `json:"acme_name"` //ACME provider name
|
||||
AcmeUrl string `json:"acme_url"` //Custom ACME URL (if any)
|
||||
SkipTLS bool `json:"skip_tls"` //Skip TLS verification of upstream
|
||||
UseDNS bool `json:"dns"` //Use DNS challenge
|
||||
PropTimeout int `json:"prop_time"` //Propagation timeout
|
||||
}
|
||||
|
||||
// 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.
|
||||
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)
|
||||
|
||||
// generate private key
|
||||
@ -181,7 +182,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
return false, err
|
||||
}
|
||||
|
||||
provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials)
|
||||
provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials, propagationTimeout)
|
||||
if err != nil {
|
||||
a.Logf("Unable to resolve DNS challenge provider", 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
|
||||
certInfo := &CertificateInfoJSON{
|
||||
AcmeName: caName,
|
||||
AcmeUrl: caUrl,
|
||||
SkipTLS: skipTLS,
|
||||
UseDNS: useDNS,
|
||||
AcmeName: caName,
|
||||
AcmeUrl: caUrl,
|
||||
SkipTLS: skipTLS,
|
||||
UseDNS: useDNS,
|
||||
PropTimeout: propagationTimeout,
|
||||
}
|
||||
|
||||
certInfoBytes, err := json.Marshal(certInfo)
|
||||
@ -452,12 +454,30 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
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
|
||||
cleanedDomains := []string{}
|
||||
for _, domain := range domains {
|
||||
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 {
|
||||
utils.SendErrorResponse(w, jsonEscape(err.Error()))
|
||||
return
|
||||
|
@ -1,70 +1,56 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||
)
|
||||
|
||||
func GetDnsChallengeProviderByName(dnsProvider string, dnsCredentials string) (challenge.Provider, error) {
|
||||
|
||||
//Original Implementation
|
||||
/*credentials, err := extractDnsCredentials(dnsCredentials)
|
||||
// Preprocessor function to get DNS challenge provider by name
|
||||
func GetDnsChallengeProviderByName(dnsProvider string, dnsCredentials string, ppgTimeout int) (challenge.Provider, error) {
|
||||
//Unpack the dnsCredentials (json string) to map
|
||||
var dnsCredentialsMap map[string]interface{}
|
||||
err := json.Unmarshal([]byte(dnsCredentials), &dnsCredentialsMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setCredentialsIntoEnvironmentVariables(credentials)
|
||||
|
||||
provider, err := dns.NewDNSChallengeProviderByName(dnsProvider)
|
||||
*/
|
||||
|
||||
//New implementation using acmedns CICD pipeline generated datatype
|
||||
return acmedns.GetDNSProviderByJsonConfig(dnsProvider, dnsCredentials)
|
||||
}
|
||||
|
||||
/*
|
||||
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")
|
||||
}
|
||||
//Clear the PollingInterval and PropagationTimeout field and conert to int
|
||||
userDefinedPollingInterval := 2
|
||||
if dnsCredentialsMap["PollingInterval"] != nil {
|
||||
userDefinedPollingIntervalRaw := dnsCredentialsMap["PollingInterval"].(string)
|
||||
delete(dnsCredentialsMap, "PollingInterval")
|
||||
convertedPollingInterval, err := strconv.Atoi(userDefinedPollingIntervalRaw)
|
||||
if err == nil {
|
||||
userDefinedPollingInterval = convertedPollingInterval
|
||||
}
|
||||
}
|
||||
|
||||
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
@ -88,9 +88,12 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
|
||||
AcmeHandler: AcmeHandler,
|
||||
RenewerConfig: &renewerConfig,
|
||||
RenewTickInterval: renewCheckInterval,
|
||||
EarlyRenewDays: earlyRenewDays,
|
||||
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 {
|
||||
//Start the renew ticker
|
||||
thisRenewer.StartAutoRenewTicker()
|
||||
@ -103,7 +106,7 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -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 {
|
||||
a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err)
|
||||
} else {
|
||||
|
@ -5,14 +5,14 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Get the issuer name from pem file
|
||||
func ExtractIssuerNameFromPEM(pemFilePath string) (string, error) {
|
||||
// Read the PEM file
|
||||
pemData, err := ioutil.ReadFile(pemFilePath)
|
||||
pemData, err := os.ReadFile(pemFilePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
34
src/mod/auth/sso/app.go
Normal file
34
src/mod/auth/sso/app.go
Normal 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
|
||||
}
|
271
src/mod/auth/sso/handlers.go
Normal file
271
src/mod/auth/sso/handlers.go
Normal 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
295
src/mod/auth/sso/oauth2.go
Normal 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)
|
||||
}
|
1
src/mod/auth/sso/oauth_test.go
Normal file
1
src/mod/auth/sso/oauth_test.go
Normal file
@ -0,0 +1 @@
|
||||
package sso
|
58
src/mod/auth/sso/openid.go
Normal file
58
src/mod/auth/sso/openid.go
Normal 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
132
src/mod/auth/sso/server.go
Normal 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
158
src/mod/auth/sso/sso.go
Normal 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)
|
||||
}
|
33
src/mod/auth/sso/static/auth.html
Normal file
33
src/mod/auth/sso/static/auth.html
Normal 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>
|
43
src/mod/auth/sso/static/index.html
Normal file
43
src/mod/auth/sso/static/index.html
Normal 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>
|
29
src/mod/auth/sso/static/login.html
Normal file
29
src/mod/auth/sso/static/login.html
Normal 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>
|
309
src/mod/auth/sso/userHandlers.go
Normal file
309
src/mod/auth/sso/userHandlers.go
Normal 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
141
src/mod/auth/sso/users.go
Normal 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: "",
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ import (
|
||||
- Blacklist
|
||||
- Whitelist
|
||||
- Rate Limitor
|
||||
- SSO Auth
|
||||
- Basic Auth
|
||||
- Vitrual Directory Proxy
|
||||
- Subdomain Proxy
|
||||
@ -77,7 +78,16 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if sep.RequireRateLimit {
|
||||
err := h.handleRateLimitRouting(w, r, sep)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -163,7 +173,6 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
||||
fallthrough
|
||||
case DefaultSite_ReverseProxy:
|
||||
//They both share the same behavior
|
||||
|
||||
//Check if any virtual directory rules matches
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
|
||||
@ -188,8 +197,13 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
||||
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
|
||||
parsedURL, err := url.Parse(proot.DefaultSiteValue)
|
||||
parsedURL, err := url.Parse(redirectTarget)
|
||||
if err != nil {
|
||||
//Error when parsing target. Send to root
|
||||
h.hostRequest(w, r, h.Parent.Root)
|
||||
|
@ -49,6 +49,9 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint)
|
||||
for _, cred := range pe.BasicAuthCredentials {
|
||||
if u == cred.Username && hashedPassword == cred.PasswordHash {
|
||||
matchingFound = true
|
||||
|
||||
//Set the X-Remote-User header
|
||||
r.Header.Set("X-Remote-User", u)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -1,80 +1,12 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
)
|
||||
|
||||
/*
|
||||
CustomHeader.go
|
||||
|
||||
This script handle parsing and injecting custom headers
|
||||
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
|
||||
}
|
||||
|
@ -1,11 +1,19 @@
|
||||
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 (
|
||||
"net"
|
||||
"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 {
|
||||
timeout := 1 * time.Second
|
||||
conn, err := net.DialTimeout("tcp", domain, timeout)
|
||||
@ -17,7 +25,7 @@ func DomainReachableWithError(domain string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//Check if domain reachable
|
||||
// Check if domain reachable
|
||||
func DomainReachable(domain string) bool {
|
||||
return DomainReachableWithError(domain) == nil
|
||||
}
|
||||
|
@ -1,29 +1,67 @@
|
||||
package dpcore_test
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
)
|
||||
|
||||
func TestReplaceLocationHost(t *testing.T) {
|
||||
urlString := "http://private.com/test/newtarget/"
|
||||
rrr := &dpcore.ResponseRewriteRuleSet{
|
||||
OriginalHost: "test.example.com",
|
||||
ProxyDomain: "private.com/test",
|
||||
UseTLS: true,
|
||||
}
|
||||
useTLS := true
|
||||
tests := []struct {
|
||||
name string
|
||||
urlString string
|
||||
rrr *dpcore.ResponseRewriteRuleSet
|
||||
useTLS bool
|
||||
expectedResult string
|
||||
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/"
|
||||
|
||||
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred: %v", err)
|
||||
{
|
||||
name: "Basic HTTPS to HTTP redirection",
|
||||
urlString: "https://proxy.example.com/resource",
|
||||
rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "proxy.example.com", OriginalHost: "proxy.example.com", UseTLS: false},
|
||||
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 {
|
||||
t.Errorf("Expected: %s, but got: %s", expectedResult, result)
|
||||
for _, tt := range tests {
|
||||
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
|
||||
|
||||
expectedResult := "https://test.example.com/api/"
|
||||
expectedResult := "api/"
|
||||
|
||||
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
|
||||
if err != nil {
|
||||
|
@ -60,7 +60,7 @@ func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS b
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// Debug functions
|
||||
// Debug functions for replaceLocationHost
|
||||
func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
|
||||
return replaceLocationHost(urlString, rrr, useTLS)
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
"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
|
||||
func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
|
||||
newHeaderList := []*UserDefinedHeader{}
|
||||
newHeaderList := []*rewrite.UserDefinedHeader{}
|
||||
for _, header := range ep.UserDefinedHeaders {
|
||||
if !strings.EqualFold(header.Key, key) {
|
||||
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
|
||||
func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *UserDefinedHeader) error {
|
||||
func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *rewrite.UserDefinedHeader) error {
|
||||
if ep.UserDefinedHeaderExists(newHeaderRule.Key) {
|
||||
ep.RemoveUserDefinedHeader(newHeaderRule.Key)
|
||||
}
|
||||
|
100
src/mod/dynamicproxy/loadbalance/originPicker_test.go
Normal file
100
src/mod/dynamicproxy/loadbalance/originPicker_test.go
Normal 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)
|
||||
}
|
@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
"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)
|
||||
}
|
||||
|
||||
//Build downstream and upstream header rules
|
||||
upstreamHeaders, downstreamHeaders := target.SplitInboundOutboundHeaders()
|
||||
//Populate the user-defined headers with the values from the request
|
||||
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{
|
||||
ProxyDomain: selectedUpstream.OriginIpOrDomain,
|
||||
OriginalHost: originalHostHeader,
|
||||
@ -226,9 +237,19 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
//Build downstream and upstream header rules
|
||||
upstreamHeaders, downstreamHeaders := target.parent.SplitInboundOutboundHeaders()
|
||||
//Populate the user-defined headers with the values from the request
|
||||
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{
|
||||
ProxyDomain: target.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
|
63
src/mod/dynamicproxy/rewrite/headervars.go
Normal file
63
src/mod/dynamicproxy/rewrite/headervars.go
Normal 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
|
||||
}
|
172
src/mod/dynamicproxy/rewrite/headervars_test.go
Normal file
172
src/mod/dynamicproxy/rewrite/headervars_test.go
Normal 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])
|
||||
}
|
||||
}
|
||||
}
|
79
src/mod/dynamicproxy/rewrite/rewrite.go
Normal file
79
src/mod/dynamicproxy/rewrite/rewrite.go
Normal 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
|
||||
}
|
51
src/mod/dynamicproxy/rewrite/typedef.go
Normal file
51
src/mod/dynamicproxy/rewrite/typedef.go
Normal 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
|
||||
}
|
@ -7,10 +7,12 @@ import (
|
||||
"sync"
|
||||
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/auth/sso"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
@ -44,6 +46,7 @@ type RouterOption struct {
|
||||
StatisticCollector *statistic.Collector //Statistic collector for storing stats on incoming visitors
|
||||
WebDirectory string //The static web server directory containing the templates folder
|
||||
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
|
||||
}
|
||||
|
||||
@ -82,23 +85,6 @@ type BasicAuthExceptionRule struct {
|
||||
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 */
|
||||
|
||||
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
|
||||
@ -131,7 +117,7 @@ type ProxyEndpoint struct {
|
||||
VirtualDirectories []*VirtualDirectoryEndpoint
|
||||
|
||||
//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
|
||||
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
|
||||
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
|
||||
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
|
||||
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
|
||||
RequireRateLimit bool
|
||||
|
@ -16,7 +16,7 @@ type Sender struct {
|
||||
Port int //E.g. 587
|
||||
Username string //Username 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
|
||||
|
@ -1,6 +1,7 @@
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
@ -85,6 +86,7 @@ func NewNetworkManager(option *NetworkManagerOptions) *NetworkManager {
|
||||
//Get controller info
|
||||
instanceInfo, err := getControllerInfo(option.AuthToken, option.ApiPort)
|
||||
if err != nil {
|
||||
log.Println("ZeroTier connection failed: ", err.Error())
|
||||
return &NetworkManager{
|
||||
authToken: option.AuthToken,
|
||||
apiPort: option.ApiPort,
|
||||
|
@ -28,11 +28,17 @@ type NodeInfo struct {
|
||||
Clock int64 `json:"clock"`
|
||||
Config struct {
|
||||
Settings struct {
|
||||
AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay"`
|
||||
PortMappingEnabled bool `json:"portMappingEnabled"`
|
||||
PrimaryPort int `json:"primaryPort"`
|
||||
SoftwareUpdate string `json:"softwareUpdate"`
|
||||
SoftwareUpdateChannel string `json:"softwareUpdateChannel"`
|
||||
AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay,omitempty"`
|
||||
ForceTCPRelay bool `json:"forceTcpRelay,omitempty"`
|
||||
HomeDir string `json:"homeDir,omitempty"`
|
||||
ListeningOn []string `json:"listeningOn,omitempty"`
|
||||
PortMappingEnabled bool `json:"portMappingEnabled,omitempty"`
|
||||
PrimaryPort int `json:"primaryPort,omitempty"`
|
||||
SecondaryPort int `json:"secondaryPort,omitempty"`
|
||||
SoftwareUpdate string `json:"softwareUpdate,omitempty"`
|
||||
SoftwareUpdateChannel string `json:"softwareUpdateChannel,omitempty"`
|
||||
SurfaceAddresses []string `json:"surfaceAddresses,omitempty"`
|
||||
TertiaryPort int `json:"tertiaryPort,omitempty"`
|
||||
} `json:"settings"`
|
||||
} `json:"config"`
|
||||
Online bool `json:"online"`
|
||||
@ -46,7 +52,6 @@ type NodeInfo struct {
|
||||
VersionMinor int `json:"versionMinor"`
|
||||
VersionRev int `json:"versionRev"`
|
||||
}
|
||||
|
||||
type ErrResp struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
111228
src/mod/geodb/geoipv4.csv
111228
src/mod/geodb/geoipv4.csv
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
67
src/mod/geodb/locale.go
Normal 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
|
||||
}
|
79
src/mod/ipscan/handlers.go
Normal file
79
src/mod/ipscan/handlers.go
Normal 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))
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ type DiscoveredHost struct {
|
||||
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) {
|
||||
ipStart := net.ParseIP(start)
|
||||
ipEnd := net.ParseIP(end)
|
||||
@ -57,7 +57,6 @@ func ScanIpRange(start, end string) ([]*DiscoveredHost, error) {
|
||||
host.CheckHostname()
|
||||
host.CheckPort("http", 80, &host.HttpPortDetected)
|
||||
host.CheckPort("https", 443, &host.HttpsPortDetected)
|
||||
fmt.Println("OK", host)
|
||||
hosts = append(hosts, host)
|
||||
|
||||
}(thisIp)
|
||||
@ -118,7 +117,7 @@ func (host *DiscoveredHost) CheckPing() error {
|
||||
func (host *DiscoveredHost) CheckHostname() {
|
||||
// lookup the hostname for the IP address
|
||||
names, err := net.LookupAddr(host.IP)
|
||||
fmt.Println(names, err)
|
||||
//fmt.Println(names, err)
|
||||
if err == nil && len(names) > 0 {
|
||||
host.Hostname = names[0]
|
||||
}
|
||||
|
48
src/mod/ipscan/portscan.go
Normal file
48
src/mod/ipscan/portscan.go
Normal 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
|
||||
}
|
@ -1,15 +1,18 @@
|
||||
package streamproxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"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 {
|
||||
Database *database.Database
|
||||
DefaultTimeout int
|
||||
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 {
|
||||
@ -63,13 +67,37 @@ type Manager struct {
|
||||
|
||||
}
|
||||
|
||||
func NewStreamProxy(options *Options) *Manager {
|
||||
options.Database.NewTable("tcprox")
|
||||
func NewStreamProxy(options *Options) (*Manager, error) {
|
||||
if !utils.FileExists(options.ConfigStore) {
|
||||
err := os.MkdirAll(options.ConfigStore, 0775)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
//Load relay configs from db
|
||||
previousRules := []*ProxyRelayConfig{}
|
||||
if options.Database.KeyExists("tcprox", "rules") {
|
||||
options.Database.Read("tcprox", "rules", &previousRules)
|
||||
streamProxyConfigFiles, err := filepath.Glob(options.ConfigStore + "/*.config")
|
||||
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
|
||||
@ -91,14 +119,27 @@ func NewStreamProxy(options *Options) *Manager {
|
||||
rule.parent = &thisManager
|
||||
if rule.Running {
|
||||
//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()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -179,6 +220,11 @@ func (m *Manager) EditConfig(configUUID string, newName string, newListeningAddr
|
||||
}
|
||||
|
||||
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
|
||||
for i, config := range m.Configs {
|
||||
if config.UUID == configUUID {
|
||||
@ -190,8 +236,19 @@ func (m *Manager) RemoveConfig(configUUID string) error {
|
||||
return errors.New("config not found")
|
||||
}
|
||||
|
||||
// Save all configs to ConfigStore folder
|
||||
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 !c.UseTCP {
|
||||
c.Running = false
|
||||
c.udpStopChan = nil
|
||||
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)
|
||||
if err != nil {
|
||||
c.Running = false
|
||||
c.tcpStopChan = nil
|
||||
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() {
|
||||
c.Stop()
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
time.Sleep(3000 * time.Millisecond)
|
||||
c.Start()
|
||||
}
|
||||
|
||||
// Stop a running proxy if running
|
||||
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 {
|
||||
log.Println("[STREAM PROXY] Stopping UDP for " + c.Name)
|
||||
c.parent.logf("Stopping UDP for "+c.Name, nil)
|
||||
c.udpStopChan <- true
|
||||
c.udpStopChan = 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 = nil
|
||||
}
|
||||
|
||||
log.Println("[STREAM PROXY] Stopped Stream Proxy " + c.Name)
|
||||
c.parent.logf("Stopped Stream Proxy "+c.Name, nil)
|
||||
c.Running = false
|
||||
|
||||
//Update the running status
|
||||
|
@ -51,7 +51,7 @@
|
||||
You can upload your html files to your web directory via the <b>Web Directory Manager</b>.
|
||||
</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!
|
||||
</p>
|
||||
</div>
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||
"imuslab.com/zoraxy/mod/uptime"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
@ -98,6 +99,7 @@ func ReverseProxtInit() {
|
||||
WebDirectory: *staticWebServerRoot,
|
||||
AccessController: accessController,
|
||||
LoadBalancer: loadBalancer,
|
||||
SSOHandler: ssoHandler,
|
||||
Logger: SystemWideLogger,
|
||||
})
|
||||
if err != nil {
|
||||
@ -331,7 +333,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
//VDir
|
||||
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
||||
//Custom headers
|
||||
UserDefinedHeaders: []*dynamicproxy.UserDefinedHeader{},
|
||||
UserDefinedHeaders: []*rewrite.UserDefinedHeader{},
|
||||
//Auth
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: basicAuthCredentials,
|
||||
@ -1126,7 +1128,7 @@ func HandleCustomHeaderList(w http.ResponseWriter, r *http.Request) {
|
||||
//List all custom headers
|
||||
customHeaderList := targetProxyEndpoint.UserDefinedHeaders
|
||||
if customHeaderList == nil {
|
||||
customHeaderList = []*dynamicproxy.UserDefinedHeader{}
|
||||
customHeaderList = []*rewrite.UserDefinedHeader{}
|
||||
}
|
||||
js, _ := json.Marshal(customHeaderList)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
@ -1171,12 +1173,12 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//Create a Custom Header Definition type
|
||||
var rewriteDirection dynamicproxy.HeaderDirection
|
||||
//Create a Custom Header Defination type
|
||||
var rewriteDirection rewrite.HeaderDirection
|
||||
if direction == "toOrigin" {
|
||||
rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToUpstream
|
||||
rewriteDirection = rewrite.HeaderDirection_ZoraxyToUpstream
|
||||
} else if direction == "toClient" {
|
||||
rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToDownstream
|
||||
rewriteDirection = rewrite.HeaderDirection_ZoraxyToDownstream
|
||||
} else {
|
||||
//Unknown direction
|
||||
utils.SendErrorResponse(w, "header rewrite direction not supported")
|
||||
@ -1187,7 +1189,8 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
|
||||
if rewriteType == "remove" {
|
||||
isRemove = true
|
||||
}
|
||||
headerRewriteDefinition := dynamicproxy.UserDefinedHeader{
|
||||
|
||||
headerRewriteDefinition := rewrite.UserDefinedHeader{
|
||||
Key: name,
|
||||
Value: value,
|
||||
Direction: rewriteDirection,
|
||||
|
31
src/start.go
31
src/start.go
@ -36,7 +36,10 @@ import (
|
||||
Startup Sequence
|
||||
|
||||
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 (
|
||||
@ -124,6 +127,22 @@ func startupSequence() {
|
||||
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
|
||||
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
|
||||
Database: sysdb,
|
||||
@ -187,7 +206,7 @@ func startupSequence() {
|
||||
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
|
||||
HostName: hostName,
|
||||
Port: portInt,
|
||||
Domain: "zoraxy.arozos.com",
|
||||
Domain: "zoraxy.aroz.org",
|
||||
Model: "Network Gateway",
|
||||
UUID: nodeUUID,
|
||||
Vendor: "imuslab.com",
|
||||
@ -244,10 +263,14 @@ func startupSequence() {
|
||||
webSshManager = sshprox.NewSSHProxyManager()
|
||||
|
||||
//Create TCP Proxy Manager
|
||||
streamProxyManager = streamproxy.NewStreamProxy(&streamproxy.Options{
|
||||
Database: sysdb,
|
||||
streamProxyManager, err = streamproxy.NewStreamProxy(&streamproxy.Options{
|
||||
AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess,
|
||||
ConfigStore: "./conf/streamproxy",
|
||||
Logger: SystemWideLogger,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create WoL MAC storage table
|
||||
sysdb.NewTable("wolmac")
|
||||
|
@ -61,7 +61,7 @@
|
||||
<p>Current list of loaded certificates</p>
|
||||
<div tourstep="certTable">
|
||||
<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>
|
||||
<tr><th>Domain</th>
|
||||
<th>Last Update</th>
|
||||
@ -161,7 +161,9 @@
|
||||
msgbox("Requesting certificate via " + defaultCA +"...");
|
||||
|
||||
//Request ACME for certificate
|
||||
let buttonOriginalHTML = "";
|
||||
if (btn != undefined){
|
||||
buttonOriginalHTML = $(btn).html();
|
||||
$(btn).addClass('disabled');
|
||||
$(btn).html(`<i class="ui loading spinner icon"></i>`);
|
||||
}
|
||||
@ -169,11 +171,26 @@
|
||||
obtainCertificate(domain, dns, defaultCA.trim(), function(succ){
|
||||
if (btn != undefined){
|
||||
$(btn).removeClass('disabled');
|
||||
if (succ){
|
||||
$(btn).html(`<i class="ui green check icon"></i>`);
|
||||
if ($(btn).hasClass("icon")){
|
||||
//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{
|
||||
$(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(){
|
||||
initManagedDomainCertificateList();
|
||||
|
@ -350,15 +350,27 @@
|
||||
let originalContent = $(column).html();
|
||||
|
||||
//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
|
||||
for (var i = 0; i < payload.MatchingDomainAlias.length; i++){
|
||||
let thisAliasName = payload.MatchingDomainAlias[i];
|
||||
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
|
||||
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
|
||||
|
||||
@ -371,9 +383,8 @@
|
||||
</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="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();
|
||||
}else{
|
||||
@ -536,9 +547,29 @@
|
||||
Certificate Shortcut
|
||||
*/
|
||||
|
||||
function requestCertificateForExistingHost(hostUUID, RootAndAliasDomains){
|
||||
function requestCertificateForExistingHost(hostUUID, RootAndAliasDomains, btn=undefined){
|
||||
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
|
||||
|
@ -17,10 +17,21 @@
|
||||
<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>
|
||||
<div class="ui divider"></div>
|
||||
<!-- IP Scanner-->
|
||||
<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 class="ui stackable grid">
|
||||
<div class="eight wide column">
|
||||
<!-- IP Scanner-->
|
||||
<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>
|
||||
<!-- Traceroute-->
|
||||
<h2>Traceroute / Ping</h2>
|
||||
|
@ -165,18 +165,18 @@
|
||||
<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>
|
||||
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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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="item"><code>www.arozos.com</code></div>
|
||||
<div class="item"><code>foo.bar.arozos.com</code></div>
|
||||
<div class="item"><code>www.aroz.org</code></div>
|
||||
<div class="item"><code>foo.bar.aroz.org</code></div>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
|
@ -1,10 +1,381 @@
|
||||
<div class="standardContainer">
|
||||
<div class="ui basic segment">
|
||||
<h2>Single-Sign-On</h2>
|
||||
<p>Create and manage accounts with Zoraxy!</p>
|
||||
<div class="ui message">
|
||||
<div class="header">
|
||||
Work in Progress
|
||||
</div>
|
||||
<p>The SSO feature is currently under development.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui message">
|
||||
<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>
|
||||
<!--
|
||||
<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>
|
||||
|
||||
<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>
|
||||
-->
|
@ -74,7 +74,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
@ -88,7 +88,7 @@
|
||||
|
||||
//Check if update mode
|
||||
if ($("#editStreamProxyButton").is(":visible")){
|
||||
confirmEditTCPProxyConfig(event);
|
||||
confirmEditTCPProxyConfig(event,$("#editStreamProxyButton")[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -274,13 +274,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
function confirmEditTCPProxyConfig(event){
|
||||
function confirmEditTCPProxyConfig(event, btn){
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
var form = $("#streamProxyForm");
|
||||
let originalButtonHTML = $(btn).html();
|
||||
$(btn).html(`<i class="ui loading spinner icon"></i> Updating`);
|
||||
$(btn).addClass("disabled");
|
||||
|
||||
var formValid = validateTCPProxyConfig(form);
|
||||
if (!formValid){
|
||||
$(btn).html(originalButtonHTML);
|
||||
$(btn).removeClass("disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -299,6 +304,8 @@
|
||||
timeout: parseInt($("#streamProxyForm input[name=timeout]").val().trim()),
|
||||
},
|
||||
success: function(response) {
|
||||
$(btn).html(originalButtonHTML);
|
||||
$(btn).removeClass("disabled");
|
||||
if (response.error) {
|
||||
msgbox(response.error, false, 6000);
|
||||
}else{
|
||||
@ -310,6 +317,8 @@
|
||||
|
||||
},
|
||||
error: function() {
|
||||
$(btn).html(originalButtonHTML);
|
||||
$(btn).removeClass("disabled");
|
||||
msgbox('An error occurred while processing the request', false);
|
||||
}
|
||||
});
|
||||
|
@ -46,7 +46,7 @@
|
||||
<form id="email-form" class="ui form">
|
||||
<div class="field">
|
||||
<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 class="field">
|
||||
<p><i class="caret down icon"></i> Connection setup for email service provider</p>
|
||||
|
@ -160,7 +160,7 @@
|
||||
<br><br>
|
||||
<div class="ui divider"></div>
|
||||
<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 id="messageBox" class="ui green floating big compact message">
|
||||
|
@ -140,7 +140,7 @@
|
||||
<div class="field registerOnly">
|
||||
<div class="ui left icon input">
|
||||
<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 class="field loginOnly" style="text-align: left;">
|
||||
@ -175,11 +175,11 @@
|
||||
</svg>
|
||||
</div>
|
||||
<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>
|
||||
<script>
|
||||
AOS.init();
|
||||
|
||||
var registerMode = false;
|
||||
var redirectionAddress = "/";
|
||||
var loginAddress = "/api/auth/login";
|
||||
$(".checkbox").checkbox();
|
||||
@ -197,6 +197,7 @@
|
||||
$.get("/api/auth/userCount", function(data){
|
||||
if (data == 0){
|
||||
//Allow user creation
|
||||
registerMode = true;
|
||||
$(".loginOnly").hide();
|
||||
$(".registerOnly").show();
|
||||
}
|
||||
@ -240,13 +241,23 @@
|
||||
$("input").on("keydown",function(event){
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
if ($(this).attr("id") == "magic"){
|
||||
login();
|
||||
if (registerMode){
|
||||
//Register mode
|
||||
if ($(this).attr("id") == "repeatMagic"){
|
||||
$("#regsiterbtn").click();
|
||||
}else{
|
||||
//Focus to next field
|
||||
$(this).next().focus();
|
||||
}
|
||||
}else{
|
||||
//Fuocus to password field
|
||||
$("#magic").focus();
|
||||
//Login mode
|
||||
if ($(this).attr("id") == "magic"){
|
||||
login();
|
||||
}else{
|
||||
//Fuocus to password field
|
||||
$("#magic").focus();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -614,6 +614,29 @@ body{
|
||||
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
|
||||
*/
|
||||
|
@ -201,7 +201,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<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>
|
||||
<script>
|
||||
AOS.init();
|
||||
|
@ -91,8 +91,11 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Domain(s)</label>
|
||||
<input id="domainsInput" type="text" placeholder="example.com" onkeyup="checkIfInputDomainIsMultiple();">
|
||||
<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>
|
||||
<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)
|
||||
<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 class="field multiDomainOnly" style="display:none;">
|
||||
<label>Matching Rule</label>
|
||||
@ -113,7 +116,6 @@
|
||||
<div class="item" data-value="Buypass">Buypass</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="Google">Google</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -136,7 +138,7 @@
|
||||
</div>
|
||||
<div class="field dnsChallengeOnly" style="display:none;">
|
||||
<div class="ui divider"></div>
|
||||
<p>DNS Credentials</p>
|
||||
<p>DNS Credentials</p>
|
||||
<div id="dnsProviderAPIFields">
|
||||
<p><i class="ui loading circle notch icon"></i> Generating WebForm</p>
|
||||
</div>
|
||||
@ -389,7 +391,7 @@
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
//On CA change in dropdown
|
||||
$("input[name=ca]").on('change', function() {
|
||||
if(this.value == "Custom ACME Server") {
|
||||
@ -432,6 +434,7 @@
|
||||
$("#dnsProviderAPIFields").html("");
|
||||
//Generate a form for this config
|
||||
let booleanFieldsHTML = "";
|
||||
let optionalFieldsHTML = "";
|
||||
for (const [key, datatype] of Object.entries(data)) {
|
||||
if (datatype == "int"){
|
||||
$("#dnsProviderAPIFields").append(`<div class="ui fluid labeled dnsConfigField input" key="${key}" style="margin-top: 0.2em;">
|
||||
@ -445,6 +448,26 @@
|
||||
<input type="checkbox">
|
||||
<label>${key}</label>
|
||||
</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{
|
||||
//Default to string
|
||||
$("#dnsProviderAPIFields").append(`<div class="ui fluid labeled input dnsConfigField" key="${key}" style="margin-top: 0.2em;">
|
||||
@ -461,6 +484,9 @@
|
||||
if (booleanFieldsHTML != ""){
|
||||
$(".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(){
|
||||
var inputDomains = $("#domainsInput").val();
|
||||
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(){
|
||||
if ( $("#useDnsChallenge")[0].checked){
|
||||
$(".dnsChallengeOnly").show();
|
||||
|
27
src/web/snippet/sso_app.html
Normal file
27
src/web/snippet/sso_app.html
Normal 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>
|
27
src/web/snippet/sso_user.html
Normal file
27
src/web/snippet/sso_user.html
Normal 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>
|
@ -10,7 +10,6 @@
|
||||
<title>IP 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/ao_module.js"></script>
|
||||
<script src="../script/semantic/semantic.min.js"></script>
|
||||
<script src="../script/tablesort.js"></script>
|
||||
<link rel="stylesheet" href="../main.css">
|
||||
@ -149,13 +148,23 @@
|
||||
|
||||
function displayResults(data) {
|
||||
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);
|
||||
var body = $('<tbody></tbody>');
|
||||
var offlineHostCounter = 0;
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var classname = "offlinehost";
|
||||
if (data[i].Ping>=0){
|
||||
let hostIsOnline = data[i].Ping >= 0;
|
||||
if (hostIsOnline){
|
||||
classname = "onlinehost";
|
||||
}else{
|
||||
offlineHostCounter++;
|
||||
@ -167,6 +176,7 @@
|
||||
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].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);
|
||||
}
|
||||
|
||||
@ -198,6 +208,21 @@
|
||||
function toggleOfflineHost(){
|
||||
$(".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>
|
||||
</body>
|
||||
</html>
|
120
src/web/tools/portscan.html
Normal file
120
src/web/tools/portscan.html
Normal 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>
|
@ -26,7 +26,6 @@ import (
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
"imuslab.com/zoraxy/mod/ipscan"
|
||||
"imuslab.com/zoraxy/mod/mdns"
|
||||
"imuslab.com/zoraxy/mod/uptime"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
@ -267,44 +266,6 @@ func HandleMdnsScanning(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -36,9 +36,11 @@ import (
|
||||
|
||||
//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"}
|
||||
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 {
|
||||
{{magic}}
|
||||
{{magic}}
|
||||
default:
|
||||
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
|
||||
"designate", //OpenStack, if you are using this you shd not be using zoraxy
|
||||
"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
|
||||
func getExcludedDNSProvidersNT61() []string {
|
||||
fmt.Println("Windows 7 support is deprecated, please consider upgrading to a newer version of Windows.")
|
||||
return append(getExcludedDNSProviders(), []string{"cpanel",
|
||||
"mailinabox",
|
||||
"shellrent",
|
||||
@ -251,6 +260,14 @@ func main() {
|
||||
Datatype: fields[1],
|
||||
})
|
||||
}
|
||||
case "time.Duration":
|
||||
if fields[0] == "PropagationTimeout" || fields[0] == "PollingInterval" {
|
||||
configKeys = append(configKeys, &Field{
|
||||
Title: fields[0],
|
||||
Datatype: fields[1],
|
||||
})
|
||||
}
|
||||
|
||||
default:
|
||||
//Not used fields
|
||||
hiddenKeys = append(hiddenKeys, &Field{
|
||||
@ -275,21 +292,9 @@ func main() {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = 5*time.Minute
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
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
|
||||
importList += ` "github.com/go-acme/lego/v4/providers/dns/` + providerName + "\"\n"
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ fi
|
||||
# Run the extract.go to get all the config from lego source code
|
||||
echo "Generating code"
|
||||
go run ./extract.go
|
||||
go run ./extract.go -- "win7"
|
||||
# go run ./extract.go -- "win7"
|
||||
|
||||
echo "Cleaning up lego"
|
||||
sleep 2
|
||||
|
Loading…
x
Reference in New Issue
Block a user