20 Commits

Author SHA1 Message Date
Toby Chui
cf2cf18136 Added check for loopback proxy enable state
- Added check and show 521 if the loopback proxy endpoint is disabled
2025-10-10 15:51:00 +08:00
Toby Chui
e77f947d1d Added loopback proxy support
- Added support for shortcut loopback setup in local setups
2025-10-10 14:43:38 +08:00
Toby Chui
ca12facaf2 Fixed bug in sidebar plugin list update
- Fixed remove plugin when the plugin is still running but plugin in sidebar not automatically removed bug
2025-10-09 19:14:12 +08:00
Toby Chui
88aba38495 Merge pull request #837 from tobychui/v3.2.7
V3.2.7
2025-10-09 18:43:50 +08:00
PassiveLemon
95375d3298 Update logrotate flag description 2025-10-09 00:08:28 -04:00
Toby Chui
2c3f36d9a3 Merge pull request #829 from jimmyGALLAND/fix-acme-renew
fix acme renew
2025-10-03 07:16:20 +08:00
jimmyGALLAND
030ef2e01c allow domain labels with no minimum length 2025-10-02 12:42:13 +02:00
Toby Chui
85cad1e2b6 Updated #821 2025-09-26 07:12:17 +08:00
Toby Chui
94afb6e3a5 Optimized mobile side menu
- Optimized width for mobile side menu items
2025-09-25 21:11:28 +08:00
Toby Chui
84a4eaaf95 Fixed #821
- Added the recommended code snippet
2025-09-25 21:11:10 +08:00
Toby Chui
f98e1b8218 Fixed #706
- Added conditional injection of x-proxy-by zoraxy header by only injecting tracker when -dev flag is set to true
2025-09-24 21:53:11 +08:00
Toby Chui
778df1af0f Updated #411
- Added support for human readable units in -logrotate flag
2025-09-24 20:31:53 +08:00
jimmyGALLAND
2140e5b0b5 -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.
2025-09-23 23:36:49 +02:00
Toby Chui
e9c1d14e23 Removed unused code from PR 2025-09-22 20:07:38 +08:00
Toby Chui
5477822015 Merge pull request #828 from jimmyGALLAND/fix-restart-after-acme-DNS-challenge
Fix restart after acme dns challenge
2025-09-22 20:05:20 +08:00
jimmyGALLAND
b0922c466d fix: restart issue after ACME certificate update with DNS challenge 2025-09-22 01:21:21 +02:00
Toby Chui
1faaae21d7 Merge pull request #827 from Saeraphinx/webui-style-changes
Update Sidebar CSS
2025-09-20 12:32:42 +08:00
Saera
53c73e1e77 update styling 2025-09-19 11:44:20 -05:00
Toby Chui
0805da9d13 Added more test cases for netutil ipmatch 2025-09-19 21:14:20 +08:00
Toby Chui
52f652fbaf 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.
2025-09-17 07:37:21 +08:00
23 changed files with 489 additions and 71 deletions

View File

@@ -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. |

View File

@@ -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")

View File

@@ -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")

View File

@@ -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)

View File

@@ -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
}

View File

@@ -92,7 +92,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
//Plugin routing
if h.Parent.Option.PluginManager != nil && h.Parent.Option.PluginManager.HandleRoute(w, r, sep.Tags) {
//Request handled by subroute
return

View File

@@ -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,31 @@ 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 := ""
//if p.Transport != nil {
// if tr, ok := p.Transport.(*http.Transport); ok && tr.TLSClientConfig != nil && tr.TLSClientConfig.ServerName != "" {
// serverName = tr.TLSClientConfig.ServerName
// }
//}
if serverName == "" {
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
}

View File

@@ -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
}

View File

@@ -272,6 +272,11 @@ func (ep *ProxyEndpoint) Remove() error {
return nil
}
// Check if the proxy endpoint is enabled
func (ep *ProxyEndpoint) IsEnabled() bool {
return !ep.Disabled
}
// Write changes to runtime without respawning the proxy handler
// use prepare -> remove -> add if you change anything in the endpoint
// that effects the proxy routing src / dest

View File

@@ -12,6 +12,7 @@ import (
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/statistic"
@@ -95,27 +96,47 @@ func (router *Router) GetProxyEndpointFromHostname(hostname string) *ProxyEndpoi
return targetSubdomainEndpoint
}
// Clearn URL Path (without the http:// part) replaces // in a URL to /
func (router *Router) clearnURL(targetUrlOPath string) string {
return strings.ReplaceAll(targetUrlOPath, "//", "/")
}
// Rewrite URL rewrite the prefix part of a virtual directory URL with /
func (router *Router) rewriteURL(rooturl string, requestURL string) string {
rewrittenURL := requestURL
rewrittenURL = strings.TrimPrefix(rewrittenURL, strings.TrimSuffix(rooturl, "/"))
if strings.Contains(rewrittenURL, "//") {
rewrittenURL = router.clearnURL(rewrittenURL)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
}
return rewrittenURL
}
// upstreamHostSwap check if this loopback to one of the proxy rule in the system. If yes, do a shortcut target swap
// this prevents unnecessary external DNS lookup and connection, return true if swapped and request is already handled
// by the loopback handler. Only continue if return is false
func (h *ProxyHandler) upstreamHostSwap(w http.ResponseWriter, r *http.Request, selectedUpstream *loadbalance.Upstream) bool {
upstreamHostname := selectedUpstream.OriginIpOrDomain
if strings.Contains(upstreamHostname, ":") {
upstreamHostname = strings.Split(upstreamHostname, ":")[0]
}
loopbackProxyEndpoint := h.Parent.GetProxyEndpointFromHostname(upstreamHostname)
if loopbackProxyEndpoint != nil {
//This is a loopback request. Swap the target to the loopback target
//h.Parent.Option.Logger.PrintAndLog("proxy", "Detected a loopback request to self. Swap the target to "+loopbackProxyEndpoint.RootOrMatchingDomain, nil)
if loopbackProxyEndpoint.IsEnabled() {
h.hostRequest(w, r, loopbackProxyEndpoint)
} else {
//Endpoint disabled, return 503
http.ServeFile(w, r, "./web/rperror.html")
h.Parent.logRequest(r, false, 521, "host-http", r.Host, upstreamHostname)
}
return true
}
return false
}
// Handle host request
func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
reqHostname := r.Host
/* Load balancing */
selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(w, r, target.ActiveOrigins, target.UseStickySession)
if err != nil {
@@ -125,6 +146,12 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
return
}
/* Upstream Host Swap (use to detect loopback to self) */
if h.upstreamHostSwap(w, r, selectedUpstream) {
//Request handled by the loopback handler
return
}
/* WebSocket automatic proxy */
requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
@@ -196,6 +223,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 +317,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

View File

@@ -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 */

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}
}
}

View File

@@ -211,7 +211,6 @@ func getWebsiteStatus(url string) (int, error) {
}
resp, err := client.Do(req)
//resp, err := client.Get(url)
if err != nil {
//Try replace the http with https and vise versa
rewriteURL := ""

View File

@@ -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%",

View File

@@ -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)
}
}
}

View File

@@ -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)

View File

@@ -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")
}

View File

@@ -615,6 +615,7 @@ function uninstallPlugin(pluginId, pluginName, btn=undefined) {
} else {
msgbox(pluginName + " uninstalled successfully", true);
initiatePluginList();
initPluginSideMenu();
}
}
});

View File

@@ -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 {

View File

@@ -173,7 +173,7 @@
</div>
<br><br>
<div class="ui divider"></div>
<div class="ui container" style="color: grey; font-size: 90%">
<div class="ui container" style="color: grey; font-size: 90%; padding-bottom: 1em;">
<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>

View File

@@ -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{
@@ -278,7 +280,7 @@ body.darkTheme .menubar{
}
#mainmenu{
width: calc(100% - 1em);
width: calc(100% - 0.1em);
}
.contentWindow{
@@ -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
*/