From 52f652fbafeca4a548f60a354d80cd264ec78077 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Wed, 17 Sep 2025 07:37:21 +0800 Subject: [PATCH 01/13] Enable SNI offload in HTTPS proxy connections Updated the ReverseProxy's ProxyHTTPS method to use tls.Dial with SNI support when connecting to upstream servers. Also incremented SYSTEM_VERSION to 3.2.7. --- src/def.go | 2 +- src/mod/dynamicproxy/dpcore/dpcore.go | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/def.go b/src/def.go index 35b58eb..99f3122 100644 --- a/src/def.go +++ b/src/def.go @@ -44,7 +44,7 @@ import ( const ( /* Build Constants */ SYSTEM_NAME = "Zoraxy" - SYSTEM_VERSION = "3.2.6" + SYSTEM_VERSION = "3.2.7" DEVELOPMENT_BUILD = false /* System Constants */ diff --git a/src/mod/dynamicproxy/dpcore/dpcore.go b/src/mod/dynamicproxy/dpcore/dpcore.go index 23f1034..bc2cc54 100644 --- a/src/mod/dynamicproxy/dpcore/dpcore.go +++ b/src/mod/dynamicproxy/dpcore/dpcore.go @@ -2,10 +2,10 @@ package dpcore import ( "context" + "crypto/tls" "errors" "io" "log" - "net" "net/http" "net/url" "strings" @@ -391,7 +391,6 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr return res.StatusCode, nil } - func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) (int, error) { hij, ok := rw.(http.Hijacker) if !ok { @@ -407,12 +406,23 @@ func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) (in return http.StatusInternalServerError, err } - proxyConn, err := net.Dial("tcp", req.URL.Host) + // Extract SNI/hostname for TLS handshake + host := req.URL.Host + if !strings.Contains(host, ":") { + host += ":443" + } + serverName := req.URL.Hostname() + + // Connect with SNI offload + tlsConfig := &tls.Config{ + ServerName: serverName, + } + proxyConn, err := tls.Dial("tcp", host, tlsConfig) if err != nil { if p.Verbal { p.logf("http: proxy error: %v", err) } - + clientConn.Close() return http.StatusInternalServerError, err } From 0805da9d133b11686548f779202dcfa6b21d26df Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Fri, 19 Sep 2025 21:14:20 +0800 Subject: [PATCH 02/13] Added more test cases for netutil ipmatch --- src/mod/netutils/ipmatch.go | 14 ++++++-- src/mod/netutils/netutils_test.go | 53 +++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/mod/netutils/ipmatch.go b/src/mod/netutils/ipmatch.go index 942bf56..a0c65a1 100644 --- a/src/mod/netutils/ipmatch.go +++ b/src/mod/netutils/ipmatch.go @@ -108,14 +108,24 @@ func MatchIpCIDR(ip string, cidr string) bool { // Check if a ip is private IP range func IsPrivateIP(ipStr string) bool { if ipStr == "127.0.0.1" || ipStr == "::1" { - //local loopback + // local loopback return true } ip := net.ParseIP(ipStr) if ip == nil { return false } - return ip.IsPrivate() + if ip.IsPrivate() { + return true + } + // Check for IPv6 link-local addresses (fe80::/10) + if ip.To16() != nil && ip.To4() == nil { + // IPv6 only + if ip[0] == 0xfe && (ip[1]&0xc0) == 0x80 { + return true + } + } + return false } // Check if an Ip string is ipv6 diff --git a/src/mod/netutils/netutils_test.go b/src/mod/netutils/netutils_test.go index 118249d..0ba3eec 100644 --- a/src/mod/netutils/netutils_test.go +++ b/src/mod/netutils/netutils_test.go @@ -26,3 +26,56 @@ func TestHandlePing(t *testing.T) { t.Log(realIP, pingTime, ttl) } +func TestMatchIpWildcard_IPv6(t *testing.T) { + // IPv6 wildcards are not supported by MatchIpWildcard, so these should all return false + tests := []struct { + ip string + wildcard string + want bool + }{ + {"fd7a:115c:a1e0::e101:6f0f", "fd7a:115c:a1e0::e101:6f0f", false}, // not supported + {"fd7a:115c:a1e0::e101:6f0f", "*:*:*:*:*:*:*:*", false}, + } + for _, tt := range tests { + got := netutils.MatchIpWildcard(tt.ip, tt.wildcard) + if got != tt.want { + t.Errorf("MatchIpWildcard(%q, %q) = %v, want %v", tt.ip, tt.wildcard, got, tt.want) + } + } +} + +func TestMatchIpCIDR_IPv6(t *testing.T) { + tests := []struct { + ip string + cidr string + want bool + }{ + {"fd7a:115c:a1e0::e101:6f0f", "fd7a:115c:a1e0::/48", true}, + {"fd7a:115c:a1e0::e101:6f0f", "fd7a:115c:a1e0::/64", true}, + {"fd7a:115c:a1e0::e101:6f0f", "fd7a:115c:a1e1::/48", false}, + {"fd7a:115c:a1e0::e101:6f0f", "fd7a:115c:a1e0::/128", false}, + } + for _, tt := range tests { + got := netutils.MatchIpCIDR(tt.ip, tt.cidr) + if got != tt.want { + t.Errorf("MatchIpCIDR(%q, %q) = %v, want %v", tt.ip, tt.cidr, got, tt.want) + } + } +} + +func TestIsPrivateIP_IPv6(t *testing.T) { + tests := []struct { + ip string + want bool + }{ + {"fd7a:115c:a1e0::e101:6f0f", true}, // Unique local address (fc00::/7) + {"fe80::1", true}, // Link-local + {"2001:db8::1", false}, // Documentation address + } + for _, tt := range tests { + got := netutils.IsPrivateIP(tt.ip) + if got != tt.want { + t.Errorf("IsPrivateIP(%q) = %v, want %v", tt.ip, got, tt.want) + } + } +} From 53c73e1e7733a19bc3bf5f5c2cecf4b3d4faa346 Mon Sep 17 00:00:00 2001 From: Saera Date: Fri, 19 Sep 2025 11:44:20 -0500 Subject: [PATCH 03/13] update styling --- src/web/darktheme.css | 2 +- src/web/index.html | 2 +- src/web/main.css | 15 +++++++++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/web/darktheme.css b/src/web/darktheme.css index f1b9925..4ac287a 100644 --- a/src/web/darktheme.css +++ b/src/web/darktheme.css @@ -336,7 +336,7 @@ body.darkTheme .ui.segment.advanceoptions { body.darkTheme .ui.segment{ background-color: transparent !important; color: var(--text_color) !important; - border: 1px solid transparent !important; + border: 0 /*solid transparent*/ !important; } body.darkTheme .sub.header { diff --git a/src/web/index.html b/src/web/index.html index 8f90e76..34c9a7f 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -173,7 +173,7 @@

-
+

Zoraxy © 2021 - tobychui. Licensed under AGPL

diff --git a/src/web/main.css b/src/web/main.css index 7359506..5854446 100644 --- a/src/web/main.css +++ b/src/web/main.css @@ -72,15 +72,17 @@ body.darkTheme .menubar{ width: 240px; position: sticky; top: 4em; + padding: 0 1em; } .contentWindow{ display: inline-block; - width: calc(100% - 240px); + width: calc(100% - 244px); vertical-align: top; background-color: var(--theme_bg_primary); border-radius: 1em; - margin-right: 2em; + margin-right: 1em; + flex-shrink: 0; } .menutoggle{ @@ -309,8 +311,17 @@ body.darkTheme .menubar{ font-size: 0.8em !important; color: #9c9c9c !important; padding-left: 0.6em; + /* fix the divider being really weird in the sidebar menu */ + margin: 1rem 0 0.25rem 0 !important; + height: 2em !important; + border-bottom: 1px solid rgba(34,36,38,.15) !important; + border-top: 1px solid rgba(255,255,255,.1) !important; } +body.darkTheme .menudivider{ + border-bottom: 1px solid rgba(255,255,255,.15) !important; + border-top: 1px solid rgba(34,36,38,.1) !important; +} /* Global rules overwrite */ From b0922c466d237c787ef031b4fe7c5a9f65e1fef6 Mon Sep 17 00:00:00 2001 From: jimmyGALLAND Date: Mon, 22 Sep 2025 00:38:57 +0200 Subject: [PATCH 04/13] fix: restart issue after ACME certificate update with DNS challenge --- src/mod/dynamicproxy/dynamicproxy.go | 93 ++++++++++++++++------------ 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/src/mod/dynamicproxy/dynamicproxy.go b/src/mod/dynamicproxy/dynamicproxy.go index 8b234f5..9ecb50e 100644 --- a/src/mod/dynamicproxy/dynamicproxy.go +++ b/src/mod/dynamicproxy/dynamicproxy.go @@ -81,11 +81,14 @@ func (router *Router) StartProxyService() error { if router.Option.ForceTLSLatest { minVersion = tls.VersionTLS12 } + config := &tls.Config{ GetCertificate: router.Option.TlsManager.GetCert, MinVersion: uint16(minVersion), } + //config := router.Option.TlsManager.ServerTLSConfig + //Start rate limitor err := router.startRateLimterCounterResetTicker() if err != nil { @@ -265,63 +268,77 @@ func (router *Router) StartProxyService() error { return nil } +// StopProxyService stops the proxy server and waits for all listeners to close func (router *Router) StopProxyService() error { - if router.server == nil { + if router.server == nil && router.tlsListener == nil && router.tlsRedirectStop == nil { return errors.New("reverse proxy server already stopped") } - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - err := router.server.Shutdown(ctx) - if err != nil { - return err + + var wg sync.WaitGroup + + // Stop main TLS/HTTP server + if router.server != nil { + wg.Add(1) + go func(srv *http.Server) { + defer wg.Done() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + err := srv.Shutdown(ctx) + if err != nil { + router.Option.Logger.PrintAndLog("dprouter", "Error shutting down main server", err) + } + }(router.server) } - //Stop TLS listener - if router.tlsListener != nil { - router.tlsListener.Close() - } - - //Stop rate limiter - if router.rateLimterStop != nil { - go func() { - // As the rate timer loop has a 1 sec ticker - // stop the rate limiter in go routine can prevent - // front end from freezing for 1 sec - router.rateLimterStop <- true - }() - - } - - //Stop TLS redirection (from port 80) + // Stop TLS redirect server if router.tlsRedirectStop != nil { - router.tlsRedirectStop <- true + wg.Add(1) + go func(ch chan bool) { + defer wg.Done() + ch <- true + }(router.tlsRedirectStop) } - //Discard the server object - router.tlsListener = nil + // Stop rate limiter ticker if exists + if router.rateLimterStop != nil { + wg.Add(1) + go func(ch chan bool) { + defer wg.Done() + ch <- true + }(router.rateLimterStop) + } + + // Wait for all shutdown goroutines to finish + wg.Wait() + + // Clear server references router.server = nil - router.Running = false + router.tlsListener = nil router.tlsRedirectStop = nil + router.rateLimterStop = nil + router.Running = false + + router.Option.Logger.PrintAndLog("dprouter", "Proxy service stopped successfully", nil) return nil } -// Restart the current router if it is running. +// Restart safely restarts the proxy server func (router *Router) Restart() error { - //Stop the router if it is already running if router.Running { - err := router.StopProxyService() - if err != nil { - return err - } - - time.Sleep(100 * time.Millisecond) - // Start the server - err = router.StartProxyService() - if err != nil { + router.Option.Logger.PrintAndLog("dprouter", "Restarting proxy server...", nil) + if err := router.StopProxyService(); err != nil { return err } + // Ensure ports are released + time.Sleep(200 * time.Millisecond) } + if err := router.StartProxyService(); err != nil { + router.Option.Logger.PrintAndLog("dprouter", "Failed to restart proxy server", err) + return err + } + + router.Option.Logger.PrintAndLog("dprouter", "Proxy server restarted successfully", nil) return nil } From e9c1d14e2334a7b7c3c8a68de2a3c670d2c1d49e Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Mon, 22 Sep 2025 20:07:38 +0800 Subject: [PATCH 05/13] Removed unused code from PR --- src/mod/dynamicproxy/dynamicproxy.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/mod/dynamicproxy/dynamicproxy.go b/src/mod/dynamicproxy/dynamicproxy.go index 9ecb50e..5526fb0 100644 --- a/src/mod/dynamicproxy/dynamicproxy.go +++ b/src/mod/dynamicproxy/dynamicproxy.go @@ -87,8 +87,6 @@ func (router *Router) StartProxyService() error { MinVersion: uint16(minVersion), } - //config := router.Option.TlsManager.ServerTLSConfig - //Start rate limitor err := router.startRateLimterCounterResetTicker() if err != nil { From 2140e5b0b512ff97acb323cea6b8378f424c00e8 Mon Sep 17 00:00:00 2001 From: jimmyGALLAND Date: Mon, 22 Sep 2025 00:30:51 +0200 Subject: [PATCH 06/13] -Add support for including Subject Alternative Names (SANs) from existing certificates during both manual and automatic renewals. -Enhance filtering and normalization of domain names from the UI to ensure only valid domains are included when requesting certificates. --- src/mod/acme/acme.go | 43 ++++++++++++++++++++++--- src/mod/acme/autorenew.go | 9 ++++++ src/mod/acme/utils.go | 67 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 5 deletions(-) diff --git a/src/mod/acme/acme.go b/src/mod/acme/acme.go index 99f98f6..e38415a 100644 --- a/src/mod/acme/acme.go +++ b/src/mod/acme/acme.go @@ -432,6 +432,21 @@ func (a *ACMEHandler) HandleGetExpiredDomains(w http.ResponseWriter, r *http.Req // to renew the certificate, and sends a JSON response indicating the result of the renewal process. func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Request) { domainPara, err := utils.PostPara(r, "domains") + + //Clean each domain + cleanedDomains := []string{} + if (domainPara != "") { + for _, d := range strings.Split(domainPara, ",") { + // Apply normalization on each domain + nd, err := NormalizeDomain(d) + if err != nil { + utils.SendErrorResponse(w, jsonEscape(err.Error())) + return + } + cleanedDomains = append(cleanedDomains, nd) + } + } + if err != nil { utils.SendErrorResponse(w, jsonEscape(err.Error())) return @@ -492,7 +507,6 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ dns = true } - domains := strings.Split(domainPara, ",") // Default propagation timeout is 300 seconds propagationTimeout := 300 @@ -511,12 +525,31 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ } } - //Clean spaces in front or behind each domain - cleanedDomains := []string{} - for _, domain := range domains { - cleanedDomains = append(cleanedDomains, strings.TrimSpace(domain)) + // Extract SANs from existing PEM to ensure all domains are included + pemPath := fmt.Sprintf("./conf/certs/%s.pem", filename) + sanDomains, err := ExtractDomainsFromPEM(pemPath) + if err == nil { + // Merge domainPara + SANs + domainSet := map[string]struct{}{} + for _, d := range cleanedDomains { + domainSet[d] = struct{}{} + } + for _, d := range sanDomains { + domainSet[d] = struct{}{} + } + + // Rebuild cleanedDomains with all unique domains + cleanedDomains = []string{} + for d := range domainSet { + cleanedDomains = append(cleanedDomains, d) + } + + a.Logf("Renewal domains including SANs from PEM: "+strings.Join(cleanedDomains, ","), nil) + } else { + a.Logf("Could not extract SANs from PEM, using domainPara only", err) } + // Extract DNS servers from the request var dnsServers []string dnsServersPara, err := utils.PostPara(r, "dnsServers") diff --git a/src/mod/acme/autorenew.go b/src/mod/acme/autorenew.go index 2595ac2..db10d6d 100644 --- a/src/mod/acme/autorenew.go +++ b/src/mod/acme/autorenew.go @@ -397,6 +397,15 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro dnsServers = strings.Join(certInfo.DNSServers, ",") } + // Extract SANs from the existing PEM to ensure all domains are included + sanDomains, errSan := ExtractDomainsFromPEM(expiredCert.Filepath) + if errSan == nil && len(sanDomains) > 0 { + expiredCert.Domains = sanDomains + a.Logf("Using SANs from PEM for renewal: "+strings.Join(sanDomains, ","), nil) + } else { + a.Logf("Could not extract SANs from PEM for "+fileName+", using original domains", errSan) + } + _, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS, certInfo.PropTimeout, dnsServers) if err != nil { a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err) diff --git a/src/mod/acme/utils.go b/src/mod/acme/utils.go index fb41135..26a7f2c 100644 --- a/src/mod/acme/utils.go +++ b/src/mod/acme/utils.go @@ -7,6 +7,8 @@ import ( "fmt" "os" "time" + "strings" + "unicode" ) // Get the issuer name from pem file @@ -40,6 +42,8 @@ func ExtractDomains(certBytes []byte) ([]string, error) { return []string{}, errors.New("decode cert bytes failed") } + + func ExtractIssuerName(certBytes []byte) (string, error) { // Parse the PEM block block, _ := pem.Decode(certBytes) @@ -64,6 +68,20 @@ func ExtractIssuerName(certBytes []byte) (string, error) { return issuer, nil } +// ExtractDomainsFromPEM reads a PEM certificate file and returns all SANs +func ExtractDomainsFromPEM(pemFilePath string) ([]string, error) { + + certBytes, err := os.ReadFile(pemFilePath) + if err != nil { + return nil, err + } + domains,err := ExtractDomains(certBytes) + if err != nil { + return nil, err + } + return domains, nil +} + // Check if a cert is expired by public key func CertIsExpired(certBytes []byte) bool { block, _ := pem.Decode(certBytes) @@ -98,3 +116,52 @@ func CertExpireSoon(certBytes []byte, numberOfDays int) bool { } return false } + + +// NormalizeDomain cleans and validates a domain string. +// - Trims spaces around the domain +// - Converts to lowercase +// - Removes trailing dot (FQDN canonicalization) +// - Checks that the domain conforms to standard rules: +// * Each label ≤ 63 characters +// * Only letters, digits, and hyphens +// * Labels do not start or end with a hyphen +// * Labels must have >= 2 characters +// * Full domain ≤ 253 characters +// Returns an empty string if the domain is invalid. +func NormalizeDomain(d string) (string, error) { + d = strings.TrimSpace(d) + d = strings.ToLower(d) + d = strings.TrimSuffix(d, ".") + + if len(d) == 0 { + return "", errors.New("domain is empty") + } + if len(d) > 253 { + return "", errors.New("domain exceeds 253 characters") + } + + labels := strings.Split(d, ".") + for _, label := range labels { + if len(label) == 0 { + return "", errors.New("Domain '" + d + "' not valid: Empty label") + } + if len(label) < 2 { + return "", errors.New("Domain '" + d + "' not valid: label '" + label + "' is too short") + } + if len(label) > 63 { + return "", errors.New("Domain not valid: label exceeds 63 characters") + } + + for i, r := range label { + if !(unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-') { + return "", errors.New("Domain '" + d + "' not valid: Invalid character '" + string(r) + "' in label") + } + if (i == 0 || i == len(label)-1) && r == '-' { + return "", errors.New("Domain '" + d + "' not valid: label '"+ label +"' starts or ends with hyphen") + } + } + } + + return d, nil +} From 778df1af0fc3160de89627c7ba64768674e38376 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Wed, 24 Sep 2025 20:31:53 +0800 Subject: [PATCH 07/13] Updated #411 - Added support for human readable units in -logrotate flag --- src/def.go | 2 +- src/mod/utils/conv.go | 66 ++++++++++++++++++++++++++++++++++++++ src/mod/utils/conv_test.go | 41 +++++++++++++++++++++++ src/start.go | 15 +++++++-- 4 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 src/mod/utils/conv_test.go diff --git a/src/def.go b/src/def.go index 99f3122..8f7e0cb 100644 --- a/src/def.go +++ b/src/def.go @@ -97,7 +97,7 @@ var ( /* Logging Configuration Flags */ enableLog = flag.Bool("enablelog", true, "Enable system wide logging, set to false for writing log to STDOUT only") enableLogCompression = flag.Bool("enablelogcompress", true, "Enable log compression for rotated log files") - logRotate = flag.Int("logrotate", 0, "Enable log rotation and set the maximum log file size in KB (e.g. 25 for 25KB), set to 0 for disable") + logRotate = flag.String("logrotate", "0", "Enable log rotation and set the maximum log file size in KB, also support K, M, G suffix (e.g. 200M), set to 0 to disable") /* Default Configuration Flags */ defaultInboundPort = flag.Int("default_inbound_port", 443, "Default web server listening port") diff --git a/src/mod/utils/conv.go b/src/mod/utils/conv.go index 6adf753..d84cfd8 100644 --- a/src/mod/utils/conv.go +++ b/src/mod/utils/conv.go @@ -22,6 +22,72 @@ func Int64ToString(number int64) string { return convedNumber } +func SizeStringToBytes(sizeStr string) (int64, error) { + sizeStr = strings.TrimSpace(sizeStr) + if len(sizeStr) == 0 { + return 0, nil + } + // Extract unit (1 or 2 characters) from the end of the string + var unit string + var sizeValue string + + sizeStrLower := strings.ToLower(sizeStr) + if len(sizeStrLower) > 2 && (strings.HasSuffix(sizeStrLower, "kb") || strings.HasSuffix(sizeStrLower, "mb") || strings.HasSuffix(sizeStrLower, "gb") || strings.HasSuffix(sizeStrLower, "tb") || strings.HasSuffix(sizeStrLower, "pb")) { + unit = sizeStrLower[len(sizeStrLower)-2:] + sizeValue = sizeStrLower[:len(sizeStrLower)-2] + } else if len(sizeStrLower) > 1 && (strings.HasSuffix(sizeStrLower, "k") || strings.HasSuffix(sizeStrLower, "m") || strings.HasSuffix(sizeStrLower, "g") || strings.HasSuffix(sizeStrLower, "t") || strings.HasSuffix(sizeStrLower, "p")) { + unit = sizeStrLower[len(sizeStrLower)-1:] + sizeValue = sizeStrLower[:len(sizeStrLower)-1] + } else { + unit = "" + sizeValue = sizeStrLower + } + + size, err := strconv.ParseFloat(sizeValue, 64) + if err != nil { + return 0, err + } + switch unit { + case "k", "kb": + size *= 1024 + case "m", "mb": + size *= 1024 * 1024 + case "g", "gb": + size *= 1024 * 1024 * 1024 + case "t", "tb": + size *= 1024 * 1024 * 1024 * 1024 + case "p", "pb": + size *= 1024 * 1024 * 1024 * 1024 * 1024 + case "": + // No unit, size is already in bytes + default: + return 0, nil // Unknown unit + } + return int64(size), nil +} + +func BytesToHumanReadable(bytes int64) string { + const ( + KB = 1024 + MB = KB * 1024 + GB = MB * 1024 + TB = GB * 1024 + ) + + switch { + case bytes >= TB: + return strconv.FormatFloat(float64(bytes)/float64(TB), 'f', 2, 64) + " TB" + case bytes >= GB: + return strconv.FormatFloat(float64(bytes)/float64(GB), 'f', 2, 64) + " GB" + case bytes >= MB: + return strconv.FormatFloat(float64(bytes)/float64(MB), 'f', 2, 64) + " MB" + case bytes >= KB: + return strconv.FormatFloat(float64(bytes)/float64(KB), 'f', 2, 64) + " KB" + default: + return strconv.FormatInt(bytes, 10) + " Bytes" + } +} + func ReplaceSpecialCharacters(filename string) string { replacements := map[string]string{ "#": "%pound%", diff --git a/src/mod/utils/conv_test.go b/src/mod/utils/conv_test.go new file mode 100644 index 0000000..1abdf0c --- /dev/null +++ b/src/mod/utils/conv_test.go @@ -0,0 +1,41 @@ +package utils_test + +import ( + "testing" + + "imuslab.com/zoraxy/mod/utils" + + "github.com/stretchr/testify/assert" +) + +func TestSizeStringToBytes(t *testing.T) { + tests := []struct { + input string + expected int64 + hasError bool + }{ + {"1024", 1024, false}, + {"1k", 1024, false}, + {"1K", 1024, false}, + {"2kb", 2 * 1024, false}, + {"1m", 1024 * 1024, false}, + {"3mb", 3 * 1024 * 1024, false}, + {"1g", 1024 * 1024 * 1024, false}, + {"2gb", 2 * 1024 * 1024 * 1024, false}, + {"", 0, false}, + {" 5mb ", 5 * 1024 * 1024, false}, + {"invalid", 0, true}, + {"1tb", 1099511627776, false}, // Unknown unit returns 0, nil + {"1.5mb", int64(1.5 * 1024 * 1024), false}, + } + + for _, tt := range tests { + got, err := utils.SizeStringToBytes(tt.input) + if tt.hasError { + assert.Error(t, err, "input: %s", tt.input) + } else { + assert.NoError(t, err, "input: %s", tt.input) + assert.Equal(t, tt.expected, got, "input: %s", tt.input) + } + } +} diff --git a/src/start.go b/src/start.go index d1afd93..b2fb0cf 100644 --- a/src/start.go +++ b/src/start.go @@ -12,6 +12,7 @@ import ( "imuslab.com/zoraxy/mod/auth/sso/oauth2" "imuslab.com/zoraxy/mod/eventsystem" + "imuslab.com/zoraxy/mod/utils" "github.com/gorilla/csrf" "imuslab.com/zoraxy/mod/access" @@ -76,14 +77,24 @@ func startupSequence() { SystemWideLogger = l SystemWideLogger.Println("System wide logging is disabled, all logs will be printed to STDOUT only") } else { + logRotateSize, err := utils.SizeStringToBytes(*logRotate) + if err != nil { + //Default disable + logRotateSize = 0 + } l.SetRotateOption(&logger.RotateOption{ - Enabled: *logRotate != 0, - MaxSize: int64(*logRotate) * 1024, //Convert to bytes + Enabled: logRotateSize != 0, + MaxSize: int64(logRotateSize), MaxBackups: 10, Compress: *enableLogCompression, BackupDir: "", }) SystemWideLogger = l + if logRotateSize == 0 { + SystemWideLogger.Println("Log rotation is disabled") + } else { + SystemWideLogger.Println("Log rotation is enabled, max log file size " + utils.BytesToHumanReadable(int64(logRotateSize))) + } SystemWideLogger.Println("System wide logging is enabled") } From f98e1b82185a316c6c57fdeba74338266eadd084 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Wed, 24 Sep 2025 21:53:11 +0800 Subject: [PATCH 08/13] Fixed #706 - Added conditional injection of x-proxy-by zoraxy header by only injecting tracker when -dev flag is set to true --- src/mod/dynamicproxy/dpcore/dpcore.go | 9 ++++++--- src/mod/dynamicproxy/dynamicproxy.go | 1 + src/mod/dynamicproxy/proxyRequestHandler.go | 2 ++ src/mod/dynamicproxy/typedef.go | 3 ++- src/reverseproxy.go | 3 ++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/mod/dynamicproxy/dpcore/dpcore.go b/src/mod/dynamicproxy/dpcore/dpcore.go index bc2cc54..f59ead5 100644 --- a/src/mod/dynamicproxy/dpcore/dpcore.go +++ b/src/mod/dynamicproxy/dpcore/dpcore.go @@ -75,7 +75,8 @@ type ResponseRewriteRuleSet struct { DisableChunkedTransferEncoding bool //Disable chunked transfer encoding /* System Information Payload */ - Version string //Version number of Zoraxy, use for X-Proxy-By + DevelopmentMode bool //Inject dev mode information to requests + Version string //Version number of Zoraxy, use for X-Proxy-By } type requestCanceler interface { @@ -284,7 +285,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr // Add user defined headers (to upstream) injectUserDefinedHeaders(outreq.Header, rrr.UpstreamHeaders) - // Rewrite outbound UA, must be after user headers + // Rewrite outbound UA top upstream, must be after user headers rewriteUserAgent(outreq.Header, "Zoraxy/"+rrr.Version) //Fix proxmox transfer encoding bug if detected Proxmox Cookie @@ -323,7 +324,9 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr } //Add debug X-Proxy-By tracker - res.Header.Set("x-proxy-by", "zoraxy/"+rrr.Version) + if rrr.DevelopmentMode { + res.Header.Set("x-proxy-by", "zoraxy/"+rrr.Version) + } //Custom Location header rewriter functions if res.Header.Get("Location") != "" { diff --git a/src/mod/dynamicproxy/dynamicproxy.go b/src/mod/dynamicproxy/dynamicproxy.go index 5526fb0..39443e5 100644 --- a/src/mod/dynamicproxy/dynamicproxy.go +++ b/src/mod/dynamicproxy/dynamicproxy.go @@ -172,6 +172,7 @@ func (router *Router) StartProxyService() error { NoRemoveHopByHop: endpointProxyRewriteRules.DisableHopByHopHeaderRemoval, PathPrefix: "", Version: sep.parent.Option.HostVersion, + DevelopmentMode: sep.parent.Option.DevelopmentMode, }) return } diff --git a/src/mod/dynamicproxy/proxyRequestHandler.go b/src/mod/dynamicproxy/proxyRequestHandler.go index 65cb14f..c1bb15e 100644 --- a/src/mod/dynamicproxy/proxyRequestHandler.go +++ b/src/mod/dynamicproxy/proxyRequestHandler.go @@ -196,6 +196,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe HostHeaderOverwrite: headerRewriteOptions.RequestHostOverwrite, NoRemoveHopByHop: headerRewriteOptions.DisableHopByHopHeaderRemoval, Version: target.parent.Option.HostVersion, + DevelopmentMode: target.parent.Option.DevelopmentMode, }) //validate the error @@ -289,6 +290,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe DisableChunkedTransferEncoding: target.parent.DisableChunkedTransferEncoding, HostHeaderOverwrite: headerRewriteOptions.RequestHostOverwrite, Version: target.parent.parent.Option.HostVersion, + DevelopmentMode: target.parent.parent.Option.DevelopmentMode, }) var dnsError *net.DNSError diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index f285a42..2f6528d 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -69,7 +69,8 @@ type RouterOption struct { OAuth2Router *oauth2.OAuth2Router //OAuth2Router router for OAuth2Router authentication /* Utilities */ - Logger *logger.Logger //Logger for reverse proxy requets + DevelopmentMode bool //Enable development mode, provide more debug information in headers + Logger *logger.Logger //Logger for reverse proxy requests } /* Router Object */ diff --git a/src/reverseproxy.go b/src/reverseproxy.go index aeb4e2b..a978d65 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -122,7 +122,8 @@ func ReverseProxyInit() { LoadBalancer: loadBalancer, PluginManager: pluginManager, /* Utilities */ - Logger: SystemWideLogger, + DevelopmentMode: *development_build, + Logger: SystemWideLogger, }) if err != nil { SystemWideLogger.PrintAndLog("proxy-config", "Unable to create dynamic proxy router", err) From 84a4eaaf95d6f0f59f91e249ae0ae5da416a2f46 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Thu, 25 Sep 2025 21:11:10 +0800 Subject: [PATCH 09/13] Fixed #821 - Added the recommended code snippet --- src/mod/dynamicproxy/dpcore/dpcore.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/mod/dynamicproxy/dpcore/dpcore.go b/src/mod/dynamicproxy/dpcore/dpcore.go index f59ead5..6c56f47 100644 --- a/src/mod/dynamicproxy/dpcore/dpcore.go +++ b/src/mod/dynamicproxy/dpcore/dpcore.go @@ -6,6 +6,7 @@ import ( "errors" "io" "log" + "net" "net/http" "net/url" "strings" @@ -301,6 +302,29 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr return http.StatusBadGateway, err } + //Fix for issue #821 + if outreq.URL != nil && strings.EqualFold(outreq.URL.Scheme, "https") { + if tr, ok := transport.(*http.Transport); ok { + serverName := outreq.Host + if h, _, err := net.SplitHostPort(serverName); err == nil { + serverName = h + } + + if tr.TLSClientConfig == nil || tr.TLSClientConfig.ServerName != serverName { + trc := tr.Clone() + var cfg *tls.Config + if tr.TLSClientConfig != nil { + cfg = tr.TLSClientConfig.Clone() + } else { + cfg = &tls.Config{} + } + cfg.ServerName = serverName + trc.TLSClientConfig = cfg + transport = trc + } + } + } + // Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers. if !rrr.NoRemoveHopByHop { removeHeaders(res.Header, rrr.NoCache) From 94afb6e3a57f8da9f7e6607331e665886703c3da Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Thu, 25 Sep 2025 21:11:28 +0800 Subject: [PATCH 10/13] Optimized mobile side menu - Optimized width for mobile side menu items --- src/web/main.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/main.css b/src/web/main.css index 5854446..cca5701 100644 --- a/src/web/main.css +++ b/src/web/main.css @@ -280,7 +280,7 @@ body.darkTheme .menubar{ } #mainmenu{ - width: calc(100% - 1em); + width: calc(100% - 0.1em); } .contentWindow{ From 85cad1e2b6fbe9f70a03b65e90c24d797641d62b Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Fri, 26 Sep 2025 07:12:17 +0800 Subject: [PATCH 11/13] Updated #821 --- src/mod/dynamicproxy/dpcore/dpcore.go | 16 +++++++-------- src/mod/eventsystem/event_system_doc.txt | 25 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 src/mod/eventsystem/event_system_doc.txt diff --git a/src/mod/dynamicproxy/dpcore/dpcore.go b/src/mod/dynamicproxy/dpcore/dpcore.go index 6c56f47..9b6157e 100644 --- a/src/mod/dynamicproxy/dpcore/dpcore.go +++ b/src/mod/dynamicproxy/dpcore/dpcore.go @@ -294,14 +294,6 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr outreq.TransferEncoding = []string{"identity"} } - res, err := transport.RoundTrip(outreq) - if err != nil { - if p.Verbal { - p.logf("http: proxy error: %v", err) - } - return http.StatusBadGateway, err - } - //Fix for issue #821 if outreq.URL != nil && strings.EqualFold(outreq.URL.Scheme, "https") { if tr, ok := transport.(*http.Transport); ok { @@ -325,6 +317,14 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr } } + res, err := transport.RoundTrip(outreq) + if err != nil { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + return http.StatusBadGateway, err + } + // Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers. if !rrr.NoRemoveHopByHop { removeHeaders(res.Header, rrr.NoCache) diff --git a/src/mod/eventsystem/event_system_doc.txt b/src/mod/eventsystem/event_system_doc.txt new file mode 100644 index 0000000..43a7c2f --- /dev/null +++ b/src/mod/eventsystem/event_system_doc.txt @@ -0,0 +1,25 @@ +package eventsystem // import "imuslab.com/zoraxy/mod/eventsystem" + + +VARIABLES + +var ( + // Publisher is the singleton instance of the event manager + Publisher *eventManager +) + +FUNCTIONS + +func InitEventSystem(logger *logger.Logger) + InitEventSystem initializes the event manager with the plugin manager + + +TYPES + +type Listener interface { + Notify(event events.Event) error + GetID() ListenerID +} + +type ListenerID string + From 030ef2e01cc63537643a0a01f21b6ad42ab268be Mon Sep 17 00:00:00 2001 From: jimmyGALLAND Date: Thu, 2 Oct 2025 12:42:13 +0200 Subject: [PATCH 12/13] allow domain labels with no minimum length --- src/mod/acme/utils.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/mod/acme/utils.go b/src/mod/acme/utils.go index 26a7f2c..0a2c3e3 100644 --- a/src/mod/acme/utils.go +++ b/src/mod/acme/utils.go @@ -126,7 +126,6 @@ func CertExpireSoon(certBytes []byte, numberOfDays int) bool { // * Each label ≤ 63 characters // * Only letters, digits, and hyphens // * Labels do not start or end with a hyphen -// * Labels must have >= 2 characters // * Full domain ≤ 253 characters // Returns an empty string if the domain is invalid. func NormalizeDomain(d string) (string, error) { @@ -146,9 +145,6 @@ func NormalizeDomain(d string) (string, error) { if len(label) == 0 { return "", errors.New("Domain '" + d + "' not valid: Empty label") } - if len(label) < 2 { - return "", errors.New("Domain '" + d + "' not valid: label '" + label + "' is too short") - } if len(label) > 63 { return "", errors.New("Domain not valid: label exceeds 63 characters") } From 95375d32987fb0e8b676610f310d36a770b319df Mon Sep 17 00:00:00 2001 From: PassiveLemon Date: Thu, 9 Oct 2025 00:08:28 -0400 Subject: [PATCH 13/13] Update logrotate flag description --- docker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 23d93b5..a642d72 100644 --- a/docker/README.md +++ b/docker/README.md @@ -90,7 +90,7 @@ Variables are the same as those in [Start Parameters](https://github.com/tobychu | `ENABLELOG` | `true` (Boolean) | Enable system wide logging, set to false for writing log to STDOUT only. | | `ENABLELOGCOMPRESS` | `true` (Boolean) | Enable log compression for rotated log files. | | `FASTGEOIP` | `false` (Boolean) | Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices). | -| `LOGROTATE` | `0` (Integer) | Enable log rotation and set the maximum log file size in KB (e.g. 25 for 25KB), set to 0 for disable. | +| `LOGROTATE` | `0` (String) | Enable log rotation and set the maximum log file size (Supports K, M, G suffixes). Set to 0 to disable. | | `MDNS` | `true` (Boolean) | Enable mDNS scanner and transponder. | | `MDNSNAME` | `''` (String) | mDNS name, leave empty to use default (zoraxy_{node-uuid}.local). | | `NOAUTH` | `false` (Boolean) | Disable authentication for management interface. |