From 3392013a5cb12fdcc1f3c03817d52016aaf6d622 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Mon, 9 Sep 2024 21:12:12 +0800 Subject: [PATCH] Fixed #297 - Added UI to showcase ZeroSSL do not support DNS challenge - Added test case for origin picker - Updated zerotier struct info (wip) --- src/main.go | 4 +- .../dynamicproxy/domainsniff/domainsniff.go | 12 ++- src/mod/dynamicproxy/dpcore/dpcore_test.go | 68 +++++++++--- src/mod/dynamicproxy/dpcore/utils.go | 2 +- .../loadbalance/originPicker_test.go | 100 ++++++++++++++++++ src/mod/ganserv/ganserv.go | 2 + src/mod/ganserv/zerotier.go | 17 +-- src/web/snippet/acme.html | 40 ++++++- 8 files changed, 215 insertions(+), 30 deletions(-) create mode 100644 src/mod/dynamicproxy/loadbalance/originPicker_test.go diff --git a/src/main.go b/src/main.go index 3a2741f..6c64296 100644 --- a/src/main.go +++ b/src/main.go @@ -59,9 +59,9 @@ 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 + development = true //Set this to false to use embedded web fs bootTime = time.Now().Unix() /* diff --git a/src/mod/dynamicproxy/domainsniff/domainsniff.go b/src/mod/dynamicproxy/domainsniff/domainsniff.go index 52ba9a4..f5c42cd 100644 --- a/src/mod/dynamicproxy/domainsniff/domainsniff.go +++ b/src/mod/dynamicproxy/domainsniff/domainsniff.go @@ -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 } diff --git a/src/mod/dynamicproxy/dpcore/dpcore_test.go b/src/mod/dynamicproxy/dpcore/dpcore_test.go index 297be72..839c3cc 100644 --- a/src/mod/dynamicproxy/dpcore/dpcore_test.go +++ b/src/mod/dynamicproxy/dpcore/dpcore_test.go @@ -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 { diff --git a/src/mod/dynamicproxy/dpcore/utils.go b/src/mod/dynamicproxy/dpcore/utils.go index 1974146..ab878e2 100644 --- a/src/mod/dynamicproxy/dpcore/utils.go +++ b/src/mod/dynamicproxy/dpcore/utils.go @@ -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) } diff --git a/src/mod/dynamicproxy/loadbalance/originPicker_test.go b/src/mod/dynamicproxy/loadbalance/originPicker_test.go new file mode 100644 index 0000000..e63f2e2 --- /dev/null +++ b/src/mod/dynamicproxy/loadbalance/originPicker_test.go @@ -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) +} diff --git a/src/mod/ganserv/ganserv.go b/src/mod/ganserv/ganserv.go index 34697b8..11933ff 100644 --- a/src/mod/ganserv/ganserv.go +++ b/src/mod/ganserv/ganserv.go @@ -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, diff --git a/src/mod/ganserv/zerotier.go b/src/mod/ganserv/zerotier.go index 3fd6cbd..fa1fd0b 100644 --- a/src/mod/ganserv/zerotier.go +++ b/src/mod/ganserv/zerotier.go @@ -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"` } diff --git a/src/web/snippet/acme.html b/src/web/snippet/acme.html index a0fdd69..e635529 100644 --- a/src/web/snippet/acme.html +++ b/src/web/snippet/acme.html @@ -91,8 +91,11 @@
- - 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) + + 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) + + +
@@ -389,7 +391,7 @@ }); - + //On CA change in dropdown $("input[name=ca]").on('change', function() { if(this.value == "Custom ACME Server") { @@ -740,6 +742,7 @@ }); } + //Check if the entered domain contains multiple domains function checkIfInputDomainIsMultiple(){ var inputDomains = $("#domainsInput").val(); if (inputDomains.includes(",")){ @@ -749,6 +752,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();