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. |
diff --git a/src/def.go b/src/def.go
index 35b58eb..8f7e0cb 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 */
@@ -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/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..0a2c3e3 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,48 @@ 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
+// * 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) > 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
+}
diff --git a/src/mod/dynamicproxy/dpcore/dpcore.go b/src/mod/dynamicproxy/dpcore/dpcore.go
index 23f1034..9b6157e 100644
--- a/src/mod/dynamicproxy/dpcore/dpcore.go
+++ b/src/mod/dynamicproxy/dpcore/dpcore.go
@@ -2,6 +2,7 @@ package dpcore
import (
"context"
+ "crypto/tls"
"errors"
"io"
"log"
@@ -75,7 +76,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 +286,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
@@ -292,6 +294,29 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
outreq.TransferEncoding = []string{"identity"}
}
+ //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
+ }
+ }
+ }
+
res, err := transport.RoundTrip(outreq)
if err != nil {
if p.Verbal {
@@ -323,7 +348,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") != "" {
@@ -391,7 +418,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 +433,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
}
diff --git a/src/mod/dynamicproxy/dynamicproxy.go b/src/mod/dynamicproxy/dynamicproxy.go
index 8b234f5..39443e5 100644
--- a/src/mod/dynamicproxy/dynamicproxy.go
+++ b/src/mod/dynamicproxy/dynamicproxy.go
@@ -81,6 +81,7 @@ func (router *Router) StartProxyService() error {
if router.Option.ForceTLSLatest {
minVersion = tls.VersionTLS12
}
+
config := &tls.Config{
GetCertificate: router.Option.TlsManager.GetCert,
MinVersion: uint16(minVersion),
@@ -171,6 +172,7 @@ func (router *Router) StartProxyService() error {
NoRemoveHopByHop: endpointProxyRewriteRules.DisableHopByHopHeaderRemoval,
PathPrefix: "",
Version: sep.parent.Option.HostVersion,
+ DevelopmentMode: sep.parent.Option.DevelopmentMode,
})
return
}
@@ -265,63 +267,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
}
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/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
+
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)
+ }
+ }
+}
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/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)
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")
}
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