mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-10-13 14:19:43 +02:00
Compare commits
27 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9ed9d9ede4 | ||
![]() |
ed1b0ec673 | ||
![]() |
b22e011131 | ||
![]() |
d155ea3795 | ||
![]() |
dd610e5f75 | ||
![]() |
c424b92036 | ||
![]() |
2c270640e9 | ||
![]() |
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")
|
||||
|
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@@ -432,6 +433,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 := netutils.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,8 +508,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
|
||||
if dns {
|
||||
@@ -511,10 +525,28 @@ 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
|
||||
|
@@ -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)
|
||||
|
@@ -64,6 +64,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)
|
||||
|
@@ -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
|
||||
|
@@ -2,10 +2,13 @@ package netutils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/likexian/whois"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
@@ -167,3 +170,53 @@ func CheckIfPortOccupied(portNumber int) bool {
|
||||
listener.Close()
|
||||
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 index, label := range labels {
|
||||
if index == 0 {
|
||||
if len(label) == 1 && label == "*" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -199,4 +199,4 @@ func ValidateListeningAddress(address string) bool {
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
@@ -237,6 +238,13 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
bypassGlobalTLS, _ := utils.PostPara(r, "bypassGlobalTLS")
|
||||
if bypassGlobalTLS == "" {
|
||||
bypassGlobalTLS = "false"
|
||||
|
||||
}
|
||||
|
||||
// Enable uptime monitor?
|
||||
enableUtm, err := utils.PostBool(r, "enableUtm")
|
||||
if err != nil {
|
||||
enableUtm = true
|
||||
}
|
||||
|
||||
useBypassGlobalTLS := bypassGlobalTLS == "true"
|
||||
@@ -409,7 +417,8 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
RequireRateLimit: requireRateLimit,
|
||||
RateLimit: int64(proxyRateLimit),
|
||||
|
||||
Tags: tags,
|
||||
Tags: tags,
|
||||
DisableUptimeMonitor: !enableUtm,
|
||||
}
|
||||
|
||||
preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisProxyEndpoint)
|
||||
|
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -62,6 +62,13 @@
|
||||
<input type="checkbox" id="useStickySessionLB">
|
||||
<label>Sticky Session<br><small>Enable stick session on upstream load balancing</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="enableUtm" checked>
|
||||
<label>Enable uptime monitor<br><small>Automatically check upstream status and switch to another if offline</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Tags</label>
|
||||
@@ -168,22 +175,78 @@
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<div class="ui basic segment rulesInstructions">
|
||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Domain</span><br>
|
||||
Example of domain matching keyword:<br>
|
||||
<code>aroz.org</code> <br>Any acess requesting aroz.org will be proxy to the IP address below<br>
|
||||
<div class="ui divider"></div>
|
||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Subdomain</span><br>
|
||||
Example of subdomain matching keyword:<br>
|
||||
<code>s1.aroz.org</code> <br>Any request starting with s1.aroz.org will be proxy to the IP address below<br>
|
||||
<div class="ui divider"></div>
|
||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Wildcard</span><br>
|
||||
Example of wildcard matching keyword:<br>
|
||||
<code>*.aroz.org</code> <br>Any request with a host name matching *.aroz.org will be proxy to the IP address below. Here are some examples.<br>
|
||||
<div class="ui list">
|
||||
<div class="item"><code>www.aroz.org</code></div>
|
||||
<div class="item"><code>foo.bar.aroz.org</code></div>
|
||||
<div class="ui fluid styled accordion" id="matchingKeywordExamplesAccordion" style="background-color: transparent !important;">
|
||||
<div class="title active" style="color: white;">
|
||||
<i class="dropdown icon"></i>
|
||||
Matching Keyword Examples
|
||||
</div>
|
||||
<div class="content active">
|
||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Domain</span><br>
|
||||
Example of domain matching keyword:<br>
|
||||
<code>aroz.org</code> <br>Any acess requesting aroz.org will be proxy to the IP address below<br>
|
||||
<div class="ui divider"></div>
|
||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Subdomain</span><br>
|
||||
Example of subdomain matching keyword:<br>
|
||||
<code>s1.aroz.org</code> <br>Any request starting with s1.aroz.org will be proxy to the IP address below<br>
|
||||
<div class="ui divider"></div>
|
||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Wildcard</span><br>
|
||||
Example of wildcard matching keyword:<br>
|
||||
<code>*.aroz.org</code> <br>Any request with a host name matching *.aroz.org will be proxy to the IP address below. Here are some examples.<br>
|
||||
<div class="ui list">
|
||||
<div class="item"><code>www.aroz.org</code></div>
|
||||
<div class="item"><code>foo.bar.aroz.org</code></div>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<div class="title" style="color: white;">
|
||||
<i class="dropdown icon"></i>
|
||||
Remote Target Require TLS
|
||||
</div>
|
||||
<div class="content">
|
||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui green lock icon"></i> Upstream TLS Requirement</span><br>
|
||||
<p>
|
||||
When you enable <b>Proxy Target require TLS Connection</b>, it means the <b>upstream server</b> (the target you are proxying to) requires a secure (HTTPS) connection.<br>
|
||||
<b>This does not affect whether clients connect to this proxy endpoint using HTTP or HTTPS.</b>
|
||||
</p>
|
||||
<div class="ui divider"></div>
|
||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Example</span><br>
|
||||
<code>Matching Keyword: mydomain.com<br>
|
||||
Target: example.com:443 (TLS enabled)</code><br>
|
||||
<ul>
|
||||
<li>Client connects to <b>mydomain.com</b> (HTTP or HTTPS, depending on your proxy setup)</li>
|
||||
<li>Proxy forwards requests to <b>example.com:443</b> using <b>HTTPS</b></li>
|
||||
</ul>
|
||||
<small>
|
||||
Use this option if your upstream server only accepts secure connections.<br>
|
||||
If your upstream uses a self-signed certificate, check the <b>Ignore TLS/SSL Verification Error</b> option in Advance Settings.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="title" style="color: white;">
|
||||
<i class="dropdown icon"></i>
|
||||
What is Sticky Session?
|
||||
</div>
|
||||
<div class="content">
|
||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui green sync icon"></i> Sticky Session (Session Affinity)</span><br>
|
||||
<p>
|
||||
Sticky session ensures that requests from the same client are always forwarded to the same upstream server. This is useful for applications that store session data locally and require the client to consistently connect to the same backend.<br>
|
||||
</p>
|
||||
<div class="ui divider"></div>
|
||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> How to Add Multiple Upstreams</span><br>
|
||||
<ul>
|
||||
<li>Go to <b>HTTP Proxy</b> in the sidebar.</li>
|
||||
<li>Click <b>Edit</b> on your proxy rule.</li>
|
||||
<li>Use the <b>Upstreams</b> section to add more upstream endpoints for load balancing.</li>
|
||||
</ul>
|
||||
<small>
|
||||
Sticky session will only work if you have more than one upstream endpoint configured.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<script>
|
||||
$('#matchingKeywordExamplesAccordion').accordion();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,6 +267,7 @@
|
||||
let accessRuleToUse = $("#newProxyRuleAccessFilter").val();
|
||||
let useStickySessionLB = $("#useStickySessionLB")[0].checked;
|
||||
let tags = $("#proxyTags").val().trim();
|
||||
let enableUtm = $("#enableUtm")[0].checked;
|
||||
|
||||
if (rootname.trim() == ""){
|
||||
$("#rootname").parent().addClass("error");
|
||||
@@ -238,6 +302,7 @@
|
||||
access: accessRuleToUse,
|
||||
stickysess: useStickySessionLB,
|
||||
tags: tags,
|
||||
enableUtm: enableUtm,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
|
@@ -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