mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-10-10 20:59:33 +02:00
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cf2cf18136 | ||
![]() |
e77f947d1d | ||
![]() |
ca12facaf2 | ||
![]() |
88aba38495 | ||
![]() |
95375d3298 | ||
![]() |
2c3f36d9a3 | ||
![]() |
030ef2e01c | ||
![]() |
85cad1e2b6 | ||
![]() |
94afb6e3a5 | ||
![]() |
84a4eaaf95 | ||
![]() |
f98e1b8218 | ||
![]() |
778df1af0f | ||
![]() |
2140e5b0b5 | ||
![]() |
e9c1d14e23 | ||
![]() |
5477822015 | ||
![]() |
b0922c466d | ||
![]() |
1faaae21d7 | ||
![]() |
53c73e1e77 | ||
![]() |
0805da9d13 | ||
![]() |
52f652fbaf |
@@ -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. |
|
||||
|
@@ -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")
|
||||
|
@@ -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")
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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 */
|
||||
|
25
src/mod/eventsystem/event_system_doc.txt
Normal file
25
src/mod/eventsystem/event_system_doc.txt
Normal 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
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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 := ""
|
||||
|
@@ -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%",
|
||||
|
41
src/mod/utils/conv_test.go
Normal file
41
src/mod/utils/conv_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
|
15
src/start.go
15
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")
|
||||
}
|
||||
|
||||
|
@@ -615,6 +615,7 @@ function uninstallPlugin(pluginId, pluginName, btn=undefined) {
|
||||
} else {
|
||||
msgbox(pluginName + " uninstalled successfully", true);
|
||||
initiatePluginList();
|
||||
initPluginSideMenu();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -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 {
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -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
|
||||
*/
|
||||
|
Reference in New Issue
Block a user