From e980bc847b09d726fbd61073952d55b89107d85c Mon Sep 17 00:00:00 2001
From: Toby Chui
Date: Fri, 16 Feb 2024 15:44:09 +0800
Subject: [PATCH] Updated a lot of stuffs
+ Added comments for whitelist
+ Added automatic cert pick for multi-host certs (SNI)
+ Renamed .crt to .pem for cert store
+ Added best-fit selection for wildcard matching rules
+ Added x-proxy-by header
+ Added X-real-Ip header
+ Added Development Mode (Cache-Control: no-store)
+ Updated utm timeout to 10 seconds instead of 90
---
src/accesslist.go | 14 +-
src/api.go | 3 +-
src/cert.go | 7 +-
src/go.mod | 1 +
src/go.sum | 2 +
src/mod/acme/acme.go | 2 +-
src/mod/dynamicproxy/Server.go | 53 +---
src/mod/dynamicproxy/dpcore/dpcore.go | 25 +-
src/mod/dynamicproxy/dynamicproxy.go | 23 +-
src/mod/dynamicproxy/endpoints.go | 21 +-
src/mod/dynamicproxy/proxyRequestHandler.go | 23 +-
src/mod/dynamicproxy/router.go | 10 +
src/mod/dynamicproxy/typedef.go | 2 +
src/mod/geodb/whitelist.go | 76 +++--
src/mod/reverseproxy/reverse.go | 6 +-
src/mod/sshprox/sshprox.go | 2 +-
src/mod/tlscert/helper.go | 29 +-
src/mod/tlscert/localhost.pem | 22 ++
src/mod/tlscert/tlscert.go | 193 ++++++++++---
src/mod/uptime/uptime.go | 8 +-
src/mod/webserv/handler.go | 1 +
src/reverseproxy.go | 41 ++-
src/router.go | 2 +
src/routingrule.go | 35 +++
src/start.go | 3 +
src/web/components/access.html | 19 +-
src/web/components/cert.html | 215 +++++++++++---
src/web/components/httprp.html | 7 +-
src/web/components/redirection.html | 3 +-
src/web/components/rproot.html | 17 +-
src/web/components/rules.html | 75 +----
src/web/components/status.html | 33 ++-
src/web/components/tcpprox.html | 85 +++---
src/web/components/utils.html | 304 ++++++++++----------
src/web/components/vdir.html | 3 +-
src/web/components/webserv.html | 8 +-
src/web/index.html | 28 +-
src/web/main.css | 130 +++++++--
src/web/snippet/acme.html | 11 +-
src/web/snippet/configTools.html | 2 +-
src/wrappers.go | 43 +++
41 files changed, 1056 insertions(+), 531 deletions(-)
create mode 100644 src/mod/tlscert/localhost.pem
create mode 100644 src/routingrule.go
diff --git a/src/accesslist.go b/src/accesslist.go
index dc26fb4..a52d878 100644
--- a/src/accesslist.go
+++ b/src/accesslist.go
@@ -4,6 +4,8 @@ import (
"encoding/json"
"net/http"
+ strip "github.com/grokify/html-strip-tags-go"
+ "imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/utils"
)
@@ -115,7 +117,7 @@ func handleListWhitelisted(w http.ResponseWriter, r *http.Request) {
bltype = "country"
}
- resulst := []string{}
+ resulst := []*geodb.WhitelistEntry{}
if bltype == "country" {
resulst = geodbStore.GetAllWhitelistedCountryCode()
} else if bltype == "ip" {
@@ -134,7 +136,10 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
return
}
- geodbStore.AddCountryCodeToWhitelist(countryCode)
+ comment, _ := utils.PostPara(r, "comment")
+ comment = strip.StripTags(comment)
+
+ geodbStore.AddCountryCodeToWhitelist(countryCode, comment)
utils.SendOK(w)
}
@@ -158,7 +163,10 @@ func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) {
return
}
- geodbStore.AddIPToWhiteList(ipAddr)
+ comment, _ := utils.PostPara(r, "comment")
+ comment = strip.StripTags(comment)
+
+ geodbStore.AddIPToWhiteList(ipAddr, comment)
}
func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
diff --git a/src/api.go b/src/api.go
index 638fbdf..1ac2eda 100644
--- a/src/api.go
+++ b/src/api.go
@@ -56,6 +56,7 @@ func initAPIs() {
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
+ authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
//Reverse proxy virtual directory APIs
authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
@@ -178,7 +179,7 @@ func initAPIs() {
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
- authRouter.HandleFunc("/api/webserv/setPort", staticWebServer.HandlePortChange)
+ authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
if *allowWebFileManager {
//Web Directory Manager file operation functions
diff --git a/src/cert.go b/src/cert.go
index 567fd7c..2f70825 100644
--- a/src/cert.go
+++ b/src/cert.go
@@ -51,7 +51,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
results := []*CertInfo{}
for _, filename := range filenames {
- certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".crt")
+ certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".pem")
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
fileInfo, err := os.Stat(certFilepath)
if err != nil {
@@ -248,7 +248,7 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
}
if keytype == "pub" {
- overWriteFilename = domain + ".crt"
+ overWriteFilename = domain + ".pem"
} else if keytype == "pri" {
overWriteFilename = domain + ".key"
} else {
@@ -287,6 +287,9 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
return
}
+ //Update cert list
+ tlsCertManager.UpdateLoadedCertList()
+
// send response
fmt.Fprintln(w, "File upload successful!")
}
diff --git a/src/go.mod b/src/go.mod
index ad9214f..79e1127 100644
--- a/src/go.mod
+++ b/src/go.mod
@@ -10,6 +10,7 @@ require (
github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.5.0
github.com/grandcat/zeroconf v1.0.0
+ github.com/grokify/html-strip-tags-go v0.1.0 // indirect
github.com/likexian/whois v1.15.1
github.com/microcosm-cc/bluemonday v1.0.25
golang.org/x/net v0.14.0
diff --git a/src/go.sum b/src/go.sum
index e984002..cad7eab 100644
--- a/src/go.sum
+++ b/src/go.sum
@@ -740,6 +740,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
+github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
+github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
diff --git a/src/mod/acme/acme.go b/src/mod/acme/acme.go
index efbd325..665541c 100644
--- a/src/mod/acme/acme.go
+++ b/src/mod/acme/acme.go
@@ -163,7 +163,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
// Each certificate comes back with the cert bytes, the bytes of the client's
// private key, and a certificate URL.
- err = os.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777)
+ err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777)
if err != nil {
log.Println(err)
return false, err
diff --git a/src/mod/dynamicproxy/Server.go b/src/mod/dynamicproxy/Server.go
index 6e771fd..696df35 100644
--- a/src/mod/dynamicproxy/Server.go
+++ b/src/mod/dynamicproxy/Server.go
@@ -1,8 +1,6 @@
package dynamicproxy
import (
- _ "embed"
- "errors"
"net/http"
"net/url"
"os"
@@ -25,11 +23,6 @@ import (
- Vitrual Directory Routing
*/
-var (
- //go:embed tld.json
- rawTldMap []byte
-)
-
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/*
Special Routing Rules, bypass most of the limitations
@@ -52,10 +45,12 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
+ //Inject debug headers
+ w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
+
/*
General Access Check
*/
-
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
@@ -81,6 +76,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/*
Host Routing
*/
+
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
if sep != nil && !sep.Disabled {
if sep.RequireBasicAuth {
@@ -235,44 +231,3 @@ func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Reques
return false
}
-
-// Return if the given host is already topped (e.g. example.com or example.co.uk) instead of
-// a host with subdomain (e.g. test.example.com)
-func (h *ProxyHandler) isTopLevelRedirectableDomain(requestHost string) bool {
- parts := strings.Split(requestHost, ".")
- if len(parts) > 2 {
- //Cases where strange tld is used like .co.uk or .com.hk
- _, ok := h.Parent.tldMap[strings.Join(parts[1:], ".")]
- if ok {
- //Already topped
- return true
- }
- } else {
- //Already topped
- return true
- }
-
- return false
-}
-
-// GetTopLevelRedirectableDomain returns the toppest level of domain
-// that is redirectable. E.g. a.b.c.example.co.uk will return example.co.uk
-func (h *ProxyHandler) getTopLevelRedirectableDomain(unsetSubdomainHost string) (string, error) {
- parts := strings.Split(unsetSubdomainHost, ".")
- if h.isTopLevelRedirectableDomain(unsetSubdomainHost) {
- //Already topped
- return "", errors.New("already at top level domain")
- }
-
- for i := 0; i < len(parts); i++ {
- possibleTld := parts[i:]
- _, ok := h.Parent.tldMap[strings.Join(possibleTld, ".")]
- if ok {
- //This is tld length
- tld := strings.Join(parts[i-1:], ".")
- return "//" + tld, nil
- }
- }
-
- return "", errors.New("unsupported top level domain given")
-}
diff --git a/src/mod/dynamicproxy/dpcore/dpcore.go b/src/mod/dynamicproxy/dpcore/dpcore.go
index 1b1b11a..321f031 100644
--- a/src/mod/dynamicproxy/dpcore/dpcore.go
+++ b/src/mod/dynamicproxy/dpcore/dpcore.go
@@ -60,6 +60,7 @@ type ResponseRewriteRuleSet struct {
ProxyDomain string
OriginalHost string
UseTLS bool
+ NoCache bool
PathPrefix string //Vdir prefix for root, / will be rewrite to this
}
@@ -243,7 +244,7 @@ func (p *ReverseProxy) logf(format string, args ...interface{}) {
}
}
-func removeHeaders(header http.Header) {
+func removeHeaders(header http.Header, noCache bool) {
// Remove hop-by-hop headers listed in the "Connection" header.
if c := header.Get("Connection"); c != "" {
for _, f := range strings.Split(c, ",") {
@@ -260,9 +261,16 @@ func removeHeaders(header http.Header) {
}
}
- if header.Get("A-Upgrade") != "" {
- header.Set("Upgrade", header.Get("A-Upgrade"))
- header.Del("A-Upgrade")
+ //Restore the Upgrade header if any
+ if header.Get("Zr-Origin-Upgrade") != "" {
+ header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
+ header.Del("Zr-Origin-Upgrade")
+ }
+
+ //Disable cache if nocache is set
+ if noCache {
+ header.Del("Cache-Control")
+ header.Set("Cache-Control", "no-store")
}
}
@@ -281,6 +289,11 @@ func addXForwardedForHeader(req *http.Request) {
req.Header.Set("X-Forwarded-Proto", "http")
}
+ if req.Header.Get("X-Real-Ip") == "" {
+ //Not exists. Fill it in with client IP
+ req.Header.Set("X-Real-Ip", clientIP)
+ }
+
}
}
@@ -323,7 +336,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
copyHeader(outreq.Header, req.Header)
// Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers.
- removeHeaders(outreq.Header)
+ removeHeaders(outreq.Header, rrr.NoCache)
// Add X-Forwarded-For Header.
addXForwardedForHeader(outreq)
@@ -339,7 +352,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
}
// Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers.
- removeHeaders(res.Header)
+ removeHeaders(res.Header, rrr.NoCache)
if p.ModifyResponse != nil {
if err := p.ModifyResponse(res); err != nil {
diff --git a/src/mod/dynamicproxy/dynamicproxy.go b/src/mod/dynamicproxy/dynamicproxy.go
index 39f4982..e071181 100644
--- a/src/mod/dynamicproxy/dynamicproxy.go
+++ b/src/mod/dynamicproxy/dynamicproxy.go
@@ -35,12 +35,6 @@ func NewDynamicProxy(option RouterOption) (*Router, error) {
Parent: &thisRouter,
}
- //Prase the tld map for tld redirection in main router
- //See Server.go declarations
- if len(rawTldMap) > 0 {
- json.Unmarshal(rawTldMap, &thisRouter.tldMap)
- }
-
return &thisRouter, nil
}
@@ -74,12 +68,12 @@ func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
func (router *Router) StartProxyService() error {
//Create a new server object
if router.server != nil {
- return errors.New("Reverse proxy server already running")
+ return errors.New("reverse proxy server already running")
}
//Check if root route is set
if router.Root == nil {
- return errors.New("Reverse proxy router root not set")
+ return errors.New("reverse proxy router root not set")
}
minVersion := tls.VersionTLS10
@@ -92,16 +86,6 @@ func (router *Router) StartProxyService() error {
}
if router.Option.UseTls {
- /*
- //Serve with TLS mode
- ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config)
- if err != nil {
- log.Println(err)
- router.Running = false
- return err
- }
- router.tlsListener = ln
- */
router.server = &http.Server{
Addr: ":" + strconv.Itoa(router.Option.Port),
Handler: router.mux,
@@ -216,7 +200,7 @@ func (router *Router) StartProxyService() error {
func (router *Router) StopProxyService() error {
if router.server == nil {
- return errors.New("Reverse proxy server already stopped")
+ return errors.New("reverse proxy server already stopped")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@@ -251,6 +235,7 @@ func (router *Router) Restart() error {
return err
}
+ time.Sleep(300 * time.Millisecond)
// Start the server
err = router.StartProxyService()
if err != nil {
diff --git a/src/mod/dynamicproxy/endpoints.go b/src/mod/dynamicproxy/endpoints.go
index 0d8a760..825cfaf 100644
--- a/src/mod/dynamicproxy/endpoints.go
+++ b/src/mod/dynamicproxy/endpoints.go
@@ -7,7 +7,13 @@ import (
)
/*
- Endpoint Functions
+ endpoint.go
+ author: tobychui
+
+ This script handle the proxy endpoint object actions
+ so proxyEndpoint can be handled like a proper oop object
+
+ Most of the functions are implemented in dynamicproxy.go
*/
// Get virtual directory handler from given URI
@@ -87,3 +93,16 @@ func (ep *ProxyEndpoint) Clone() *ProxyEndpoint {
json.Unmarshal(js, &clonedProxyEndpoint)
return &clonedProxyEndpoint
}
+
+// Remove this proxy endpoint from running proxy endpoint list
+func (ep *ProxyEndpoint) Remove() error {
+ ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
+ return nil
+}
+
+// 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
+func (ep *ProxyEndpoint) UpdateToRuntime() {
+ ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep)
+}
diff --git a/src/mod/dynamicproxy/proxyRequestHandler.go b/src/mod/dynamicproxy/proxyRequestHandler.go
index 3c6a119..bc28b8b 100644
--- a/src/mod/dynamicproxy/proxyRequestHandler.go
+++ b/src/mod/dynamicproxy/proxyRequestHandler.go
@@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"path/filepath"
+ "sort"
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
@@ -37,6 +38,7 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi
}
//No hit. Try with wildcard
+ matchProxyEndpoints := []*ProxyEndpoint{}
router.ProxyEndpoints.Range(func(k, v interface{}) bool {
ep := v.(*ProxyEndpoint)
match, err := filepath.Match(ep.RootOrMatchingDomain, hostname)
@@ -45,12 +47,24 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi
return true
}
if match {
- targetSubdomainEndpoint = ep
- return false
+ //targetSubdomainEndpoint = ep
+ matchProxyEndpoints = append(matchProxyEndpoints, ep)
+ return true
}
return true
})
+ if len(matchProxyEndpoints) == 1 {
+ //Only 1 match
+ return matchProxyEndpoints[0]
+ } else if len(matchProxyEndpoints) > 1 {
+ //More than one match. Get the best match one
+ sort.Slice(matchProxyEndpoints, func(i, j int) bool {
+ return matchProxyEndpoints[i].RootOrMatchingDomain < matchProxyEndpoints[j].RootOrMatchingDomain
+ })
+ return matchProxyEndpoints[0]
+ }
+
return targetSubdomainEndpoint
}
@@ -77,7 +91,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
- r.Header.Set("A-Upgrade", "websocket")
+ r.Header.Set("Zr-Origin-Upgrade", "websocket")
wsRedirectionEndpoint := target.Domain
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
//Append / to the end of the redirection endpoint if not exists
@@ -109,6 +123,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
ProxyDomain: target.Domain,
OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS,
+ NoCache: h.Parent.Option.NoCache,
PathPrefix: "",
})
@@ -137,7 +152,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
- r.Header.Set("A-Upgrade", "websocket")
+ r.Header.Set("Zr-Origin-Upgrade", "websocket")
wsRedirectionEndpoint := target.Domain
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
diff --git a/src/mod/dynamicproxy/router.go b/src/mod/dynamicproxy/router.go
index dcb1eef..868c365 100644
--- a/src/mod/dynamicproxy/router.go
+++ b/src/mod/dynamicproxy/router.go
@@ -97,3 +97,13 @@ func (router *Router) SetProxyRouteAsRoot(endpoint *ProxyEndpoint) error {
router.Root = endpoint
return nil
}
+
+// ProxyEndpoint remove provide global access by key
+func (router *Router) RemoveProxyEndpointByRootname(rootnameOrMatchingDomain string) error {
+ targetEpt, err := router.LoadProxy(rootnameOrMatchingDomain)
+ if err != nil {
+ return err
+ }
+
+ return targetEpt.Remove()
+}
diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go
index 1ae7288..eab3c44 100644
--- a/src/mod/dynamicproxy/typedef.go
+++ b/src/mod/dynamicproxy/typedef.go
@@ -25,9 +25,11 @@ type ProxyHandler struct {
type RouterOption struct {
HostUUID string //The UUID of Zoraxy, use for heading mod
+ HostVersion string //The version of Zoraxy, use for heading mod
Port int //Incoming port
UseTls bool //Use TLS to serve incoming requsts
ForceTLSLatest bool //Force TLS1.2 or above
+ NoCache bool //Force set Cache-Control: no-store
ListenOnPort80 bool //Enable port 80 http listener
ForceHttpsRedirect bool //Force redirection of http to https endpoint
TlsManager *tlscert.Manager
diff --git a/src/mod/geodb/whitelist.go b/src/mod/geodb/whitelist.go
index c643b60..5873b50 100644
--- a/src/mod/geodb/whitelist.go
+++ b/src/mod/geodb/whitelist.go
@@ -1,6 +1,9 @@
package geodb
-import "strings"
+import (
+ "encoding/json"
+ "strings"
+)
/*
Whitelist.go
@@ -8,11 +11,29 @@ import "strings"
This script handles whitelist related functions
*/
+const (
+ EntryType_CountryCode int = 0
+ EntryType_IP int = 1
+)
+
+type WhitelistEntry struct {
+ EntryType int //Entry type of whitelist, Country Code or IP
+ CC string //ISO Country Code
+ IP string //IP address or range
+ Comment string //Comment for this entry
+}
+
//Geo Whitelist
-func (s *Store) AddCountryCodeToWhitelist(countryCode string) {
+func (s *Store) AddCountryCodeToWhitelist(countryCode string, comment string) {
countryCode = strings.ToLower(countryCode)
- s.sysdb.Write("whitelist-cn", countryCode, true)
+ entry := WhitelistEntry{
+ EntryType: EntryType_CountryCode,
+ CC: countryCode,
+ Comment: comment,
+ }
+
+ s.sysdb.Write("whitelist-cn", countryCode, entry)
}
func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
@@ -22,20 +43,19 @@ func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
- var isWhitelisted bool = false
- s.sysdb.Read("whitelist-cn", countryCode, &isWhitelisted)
- return isWhitelisted
+ return s.sysdb.KeyExists("whitelist-cn", countryCode)
}
-func (s *Store) GetAllWhitelistedCountryCode() []string {
- whitelistedCountryCode := []string{}
+func (s *Store) GetAllWhitelistedCountryCode() []*WhitelistEntry {
+ whitelistedCountryCode := []*WhitelistEntry{}
entries, err := s.sysdb.ListTable("whitelist-cn")
if err != nil {
return whitelistedCountryCode
}
for _, keypairs := range entries {
- ip := string(keypairs[0])
- whitelistedCountryCode = append(whitelistedCountryCode, ip)
+ thisWhitelistEntry := WhitelistEntry{}
+ json.Unmarshal(keypairs[1], &thisWhitelistEntry)
+ whitelistedCountryCode = append(whitelistedCountryCode, &thisWhitelistEntry)
}
return whitelistedCountryCode
@@ -43,8 +63,14 @@ func (s *Store) GetAllWhitelistedCountryCode() []string {
//IP Whitelist
-func (s *Store) AddIPToWhiteList(ipAddr string) {
- s.sysdb.Write("whitelist-ip", ipAddr, true)
+func (s *Store) AddIPToWhiteList(ipAddr string, comment string) {
+ thisIpEntry := WhitelistEntry{
+ EntryType: EntryType_IP,
+ IP: ipAddr,
+ Comment: comment,
+ }
+
+ s.sysdb.Write("whitelist-ip", ipAddr, thisIpEntry)
}
func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
@@ -52,14 +78,14 @@ func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
}
func (s *Store) IsIPWhitelisted(ipAddr string) bool {
- var isWhitelisted bool = false
- s.sysdb.Read("whitelist-ip", ipAddr, &isWhitelisted)
+ isWhitelisted := s.sysdb.KeyExists("whitelist-ip", ipAddr)
if isWhitelisted {
+ //single IP whitelist entry
return true
}
//Check for IP wildcard and CIRD rules
- AllWhitelistedIps := s.GetAllWhitelistedIp()
+ AllWhitelistedIps := s.GetAllWhitelistedIpAsStringSlice()
for _, whitelistRules := range AllWhitelistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, whitelistRules)
if wildcardMatch {
@@ -75,17 +101,29 @@ func (s *Store) IsIPWhitelisted(ipAddr string) bool {
return false
}
-func (s *Store) GetAllWhitelistedIp() []string {
- whitelistedIp := []string{}
+func (s *Store) GetAllWhitelistedIp() []*WhitelistEntry {
+ whitelistedIp := []*WhitelistEntry{}
entries, err := s.sysdb.ListTable("whitelist-ip")
if err != nil {
return whitelistedIp
}
for _, keypairs := range entries {
- ip := string(keypairs[0])
- whitelistedIp = append(whitelistedIp, ip)
+ //ip := string(keypairs[0])
+ thisEntry := WhitelistEntry{}
+ json.Unmarshal(keypairs[1], &thisEntry)
+ whitelistedIp = append(whitelistedIp, &thisEntry)
}
return whitelistedIp
}
+
+func (s *Store) GetAllWhitelistedIpAsStringSlice() []string {
+ allWhitelistedIPs := []string{}
+ entries := s.GetAllWhitelistedIp()
+ for _, entry := range entries {
+ allWhitelistedIPs = append(allWhitelistedIPs, entry.IP)
+ }
+
+ return allWhitelistedIPs
+}
diff --git a/src/mod/reverseproxy/reverse.go b/src/mod/reverseproxy/reverse.go
index 678287e..6ded0ae 100644
--- a/src/mod/reverseproxy/reverse.go
+++ b/src/mod/reverseproxy/reverse.go
@@ -211,9 +211,9 @@ func removeHeaders(header http.Header) {
}
}
- if header.Get("A-Upgrade") != "" {
- header.Set("Upgrade", header.Get("A-Upgrade"))
- header.Del("A-Upgrade")
+ if header.Get("Zr-Origin-Upgrade") != "" {
+ header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
+ header.Del("Zr-Origin-Upgrade")
}
}
diff --git a/src/mod/sshprox/sshprox.go b/src/mod/sshprox/sshprox.go
index 75b6874..429013c 100644
--- a/src/mod/sshprox/sshprox.go
+++ b/src/mod/sshprox/sshprox.go
@@ -82,7 +82,7 @@ func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWrite
requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
- r.Header.Set("A-Upgrade", "websocket")
+ r.Header.Set("Zr-Origin-Upgrade", "websocket")
requestURL = strings.TrimPrefix(requestURL, "/")
u, _ := url.Parse("ws://127.0.0.1:" + strconv.Itoa(targetInstance.AssignedPort) + "/" + requestURL)
wspHandler := websocketproxy.NewProxy(u, false)
diff --git a/src/mod/tlscert/helper.go b/src/mod/tlscert/helper.go
index 10c52da..a637d65 100644
--- a/src/mod/tlscert/helper.go
+++ b/src/mod/tlscert/helper.go
@@ -5,22 +5,22 @@ import (
"strings"
)
-//This remove the certificates in the list where either the
-//public key or the private key is missing
+// This remove the certificates in the list where either the
+// public key or the private key is missing
func getCertPairs(certFiles []string) []string {
- crtMap := make(map[string]bool)
+ pemMap := make(map[string]bool)
keyMap := make(map[string]bool)
for _, filename := range certFiles {
- if filepath.Ext(filename) == ".crt" {
- crtMap[strings.TrimSuffix(filename, ".crt")] = true
+ if filepath.Ext(filename) == ".pem" {
+ pemMap[strings.TrimSuffix(filename, ".pem")] = true
} else if filepath.Ext(filename) == ".key" {
keyMap[strings.TrimSuffix(filename, ".key")] = true
}
}
var result []string
- for domain := range crtMap {
+ for domain := range pemMap {
if keyMap[domain] {
result = append(result, domain)
}
@@ -29,7 +29,7 @@ func getCertPairs(certFiles []string) []string {
return result
}
-//Get the cloest subdomain certificate from a list of domains
+// Get the cloest subdomain certificate from a list of domains
func matchClosestDomainCertificate(subdomain string, domains []string) string {
var matchingDomain string = ""
maxLength := 0
@@ -43,18 +43,3 @@ func matchClosestDomainCertificate(subdomain string, domains []string) string {
return matchingDomain
}
-
-//Check if a requesting domain is a subdomain of a given domain
-func isSubdomain(subdomain, domain string) bool {
- subdomainParts := strings.Split(subdomain, ".")
- domainParts := strings.Split(domain, ".")
- if len(subdomainParts) < len(domainParts) {
- return false
- }
- for i := range domainParts {
- if subdomainParts[len(subdomainParts)-1-i] != domainParts[len(domainParts)-1-i] {
- return false
- }
- }
- return true
-}
diff --git a/src/mod/tlscert/localhost.pem b/src/mod/tlscert/localhost.pem
new file mode 100644
index 0000000..569603f
--- /dev/null
+++ b/src/mod/tlscert/localhost.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDuTCCAqCgAwIBAgIBADANBgkqhkiG9w0BAQ0FADB2MQswCQYDVQQGEwJoazES
+MBAGA1UECAwJSG9uZyBLb25nMRQwEgYDVQQKDAtpbXVzbGFiLmNvbTEZMBcGA1UE
+AwwQWm9yYXh5IFNlbGYtaG9zdDEQMA4GA1UEBwwHSU1VU0xBQjEQMA4GA1UECwwH
+SU1VU0xBQjAeFw0yMzA1MjcxMDQyNDJaFw0zODA1MjgxMDQyNDJaMHYxCzAJBgNV
+BAYTAmhrMRIwEAYDVQQIDAlIb25nIEtvbmcxFDASBgNVBAoMC2ltdXNsYWIuY29t
+MRkwFwYDVQQDDBBab3JheHkgU2VsZi1ob3N0MRAwDgYDVQQHDAdJTVVTTEFCMRAw
+DgYDVQQLDAdJTVVTTEFCMIIBIzANBgkqhkiG9w0BAQEFAAOCARAAMIIBCwKCAQIA
+xav3Qq4DBooHsGW9m+r0dgjI832grX2c0Z6MJQQoE7B6wfpUI0OyfRugTXyXoiRZ
+gLxuROgiCUmp8FaLbl7RsvbImMbCPo3D/RbCT1aJCNXLZ0a7yvcDYc6woQW4nUyk
+ohHfT2otcu+OYS6aYRZuXGsKTAqPSwEXRMtr89wkPgZPsrCD27LFHBOmIcVABDvF
+KRuiwHWSHhFfU5n1AZLyYeYoLNQ9fZPvzPpkMD+HMKi4MMwr/vLE0DwU5jSfVFq+
+cd68zVihp9N/T77yah5EIH9CYm4m8Acs4bfL8DALxnaSN3KmGw6J35rOXrJvJLdh
+t42PDROmQrXN8uG8wGkBiBkCAwEAAaNQME4wHQYDVR0OBBYEFLhXihE+1K6MoL0P
+Nx5htfuSatpiMB8GA1UdIwQYMBaAFLhXihE+1K6MoL0PNx5htfuSatpiMAwGA1Ud
+EwQFMAMBAf8wDQYJKoZIhvcNAQENBQADggECAMCn0ed1bfLefGvoQJV/q+X9p61U
+HunSFJAAhp0N2Q3tq/zjIu0kJX7N0JBciEw2c0ZmqJIqR8V8Im/h/4XuuOR+53hg
+opOSPo39ww7mpxyBlQm63v1nXcNQcvw4U0JqXQ4Kyv8cgX7DIuyjRWHQpc5+6joy
+L5Nz5hzQbgpnPdHQEMorfnm8q6bWg/291IAV3ZA9Z6T5gn4YuyjeUdDczQtpT6nu
+1iTNPqtO6R3aeTVT+OSJT9sH2MHfDAsf371HBM6MzM/5QBc/62Bgau7NUjNKeSEA
+EtUBil8wBHwT7vOtqbyNk5FHEfoCpYsQtP7AtEo10izKCQpDXPftfiJefkOY
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/src/mod/tlscert/tlscert.go b/src/mod/tlscert/tlscert.go
index 6a739eb..fbd0a13 100644
--- a/src/mod/tlscert/tlscert.go
+++ b/src/mod/tlscert/tlscert.go
@@ -6,7 +6,6 @@ import (
"embed"
"encoding/pem"
"io"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -15,12 +14,19 @@ import (
"imuslab.com/zoraxy/mod/utils"
)
-type Manager struct {
- CertStore string
- verbal bool
+type CertCache struct {
+ Cert *x509.Certificate
+ PubKey string
+ PriKey string
}
-//go:embed localhost.crt localhost.key
+type Manager struct {
+ CertStore string //Path where all the certs are stored
+ LoadedCerts []*CertCache //A list of loaded certs
+ verbal bool
+}
+
+//go:embed localhost.pem localhost.key
var buildinCertStore embed.FS
func NewManager(certStore string, verbal bool) (*Manager, error) {
@@ -28,14 +34,99 @@ func NewManager(certStore string, verbal bool) (*Manager, error) {
os.MkdirAll(certStore, 0775)
}
+ pubKey := "./tmp/localhost.pem"
+ priKey := "./tmp/localhost.key"
+
+ //Check if this is initial setup
+ if !utils.FileExists(pubKey) {
+ buildInPubKey, _ := buildinCertStore.ReadFile(filepath.Base(pubKey))
+ os.WriteFile(pubKey, buildInPubKey, 0775)
+ }
+
+ if !utils.FileExists(priKey) {
+ buildInPriKey, _ := buildinCertStore.ReadFile(filepath.Base(priKey))
+ os.WriteFile(priKey, buildInPriKey, 0775)
+ }
+
thisManager := Manager{
- CertStore: certStore,
- verbal: verbal,
+ CertStore: certStore,
+ LoadedCerts: []*CertCache{},
+ verbal: verbal,
+ }
+
+ err := thisManager.UpdateLoadedCertList()
+ if err != nil {
+ return nil, err
}
return &thisManager, nil
}
+// Update domain mapping from file
+func (m *Manager) UpdateLoadedCertList() error {
+ //Get a list of certificates from file
+ domainList, err := m.ListCertDomains()
+ if err != nil {
+ return err
+ }
+
+ //Load each of the certificates into memory
+ certList := []*CertCache{}
+ for _, certname := range domainList {
+ //Read their certificate into memory
+ pubKey := filepath.Join(m.CertStore, certname+".pem")
+ priKey := filepath.Join(m.CertStore, certname+".key")
+ certificate, err := tls.LoadX509KeyPair(pubKey, priKey)
+ if err != nil {
+ log.Println("Certificate loaded failed: " + certname)
+ continue
+ }
+
+ for _, thisCert := range certificate.Certificate {
+ loadedCert, err := x509.ParseCertificate(thisCert)
+ if err != nil {
+ //Error pasring cert, skip this byte segment
+ continue
+ }
+
+ thisCacheEntry := CertCache{
+ Cert: loadedCert,
+ PubKey: pubKey,
+ PriKey: priKey,
+ }
+ certList = append(certList, &thisCacheEntry)
+ }
+ }
+
+ //Replace runtime cert array
+ m.LoadedCerts = certList
+
+ return nil
+}
+
+// Match cert by CN
+func (m *Manager) CertMatchExists(serverName string) bool {
+ for _, certCacheEntry := range m.LoadedCerts {
+ if certCacheEntry.Cert.VerifyHostname(serverName) == nil || certCacheEntry.Cert.Issuer.CommonName == serverName {
+ return true
+ }
+ }
+ return false
+}
+
+// Get cert entry by matching server name, return pubKey and priKey if found
+// check with CertMatchExists before calling to the load function
+func (m *Manager) GetCertByX509CNHostname(serverName string) (string, string) {
+ for _, certCacheEntry := range m.LoadedCerts {
+ if certCacheEntry.Cert.VerifyHostname(serverName) == nil || certCacheEntry.Cert.Issuer.CommonName == serverName {
+ return certCacheEntry.PubKey, certCacheEntry.PriKey
+ }
+ }
+
+ return "", ""
+}
+
+// Return a list of domains by filename
func (m *Manager) ListCertDomains() ([]string, error) {
filenames, err := m.ListCerts()
if err != nil {
@@ -48,8 +139,9 @@ func (m *Manager) ListCertDomains() ([]string, error) {
return filenames, nil
}
+// Return a list of cert files (public and private keys)
func (m *Manager) ListCerts() ([]string, error) {
- certs, err := ioutil.ReadDir(m.CertStore)
+ certs, err := os.ReadDir(m.CertStore)
if err != nil {
return []string{}, err
}
@@ -64,44 +156,52 @@ func (m *Manager) ListCerts() ([]string, error) {
return filenames, nil
}
+// Get a certificate from disk where its certificate matches with the helloinfo
func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) {
//Check if the domain corrisponding cert exists
- pubKey := "./tmp/localhost.crt"
+ pubKey := "./tmp/localhost.pem"
priKey := "./tmp/localhost.key"
- //Check if this is initial setup
- if !utils.FileExists(pubKey) {
- buildInPubKey, _ := buildinCertStore.ReadFile(filepath.Base(pubKey))
- os.WriteFile(pubKey, buildInPubKey, 0775)
- }
-
- if !utils.FileExists(priKey) {
- buildInPriKey, _ := buildinCertStore.ReadFile(filepath.Base(priKey))
- os.WriteFile(priKey, buildInPriKey, 0775)
- }
-
- if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".crt")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
- pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".crt")
+ if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".pem")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
+ //Direct hit
+ pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".pem")
priKey = filepath.Join(m.CertStore, helloInfo.ServerName+".key")
-
+ } else if m.CertMatchExists(helloInfo.ServerName) {
+ //Use x509
+ pubKey, priKey = m.GetCertByX509CNHostname(helloInfo.ServerName)
} else {
- domainCerts, _ := m.ListCertDomains()
- cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts)
- if cloestDomainCert != "" {
- //There is a matching parent domain for this subdomain. Use this instead.
- pubKey = filepath.Join(m.CertStore, cloestDomainCert+".crt")
- priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
- } else if m.DefaultCertExists() {
- //Use default.crt and default.key
- pubKey = filepath.Join(m.CertStore, "default.crt")
+ //Fallback to legacy method of matching certificates
+ /*
+ domainCerts, _ := m.ListCertDomains()
+ cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts)
+ if cloestDomainCert != "" {
+ //There is a matching parent domain for this subdomain. Use this instead.
+ pubKey = filepath.Join(m.CertStore, cloestDomainCert+".pem")
+ priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
+ } else if m.DefaultCertExists() {
+ //Use default.pem and default.key
+ pubKey = filepath.Join(m.CertStore, "default.pem")
+ priKey = filepath.Join(m.CertStore, "default.key")
+ if m.verbal {
+ log.Println("No matching certificate found. Serving with default")
+ }
+ } else {
+ if m.verbal {
+ log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName)
+ }
+ }*/
+
+ if m.DefaultCertExists() {
+ //Use default.pem and default.key
+ pubKey = filepath.Join(m.CertStore, "default.pem")
priKey = filepath.Join(m.CertStore, "default.key")
- if m.verbal {
- log.Println("No matching certificate found. Serving with default")
- }
+ //if m.verbal {
+ // log.Println("No matching certificate found. Serving with default")
+ //}
} else {
- if m.verbal {
- log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName)
- }
+ //if m.verbal {
+ // log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName)
+ //}
}
}
@@ -117,17 +217,17 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err
// Check if both the default cert public key and private key exists
func (m *Manager) DefaultCertExists() bool {
- return utils.FileExists(filepath.Join(m.CertStore, "default.crt")) && utils.FileExists(filepath.Join(m.CertStore, "default.key"))
+ return utils.FileExists(filepath.Join(m.CertStore, "default.pem")) && utils.FileExists(filepath.Join(m.CertStore, "default.key"))
}
// Check if the default cert exists returning seperate results for pubkey and prikey
func (m *Manager) DefaultCertExistsSep() (bool, bool) {
- return utils.FileExists(filepath.Join(m.CertStore, "default.crt")), utils.FileExists(filepath.Join(m.CertStore, "default.key"))
+ return utils.FileExists(filepath.Join(m.CertStore, "default.pem")), utils.FileExists(filepath.Join(m.CertStore, "default.key"))
}
// Delete the cert if exists
func (m *Manager) RemoveCert(domain string) error {
- pubKey := filepath.Join(m.CertStore, domain+".crt")
+ pubKey := filepath.Join(m.CertStore, domain+".pem")
priKey := filepath.Join(m.CertStore, domain+".key")
if utils.FileExists(pubKey) {
err := os.Remove(pubKey)
@@ -143,6 +243,9 @@ func (m *Manager) RemoveCert(domain string) error {
}
}
+ //Update the cert list
+ m.UpdateLoadedCertList()
+
return nil
}
@@ -171,15 +274,11 @@ func IsValidTLSFile(file io.Reader) bool {
return false
}
// Check if the certificate is a valid TLS/SSL certificate
- return cert.IsCA == false && cert.KeyUsage&x509.KeyUsageDigitalSignature != 0 && cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0
+ return !cert.IsCA && cert.KeyUsage&x509.KeyUsageDigitalSignature != 0 && cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0
} else if strings.Contains(block.Type, "PRIVATE KEY") {
// The file contains a private key
_, err := x509.ParsePKCS1PrivateKey(block.Bytes)
- if err != nil {
- // Handle the error
- return false
- }
- return true
+ return err == nil
} else {
return false
}
diff --git a/src/mod/uptime/uptime.go b/src/mod/uptime/uptime.go
index 1308592..256b317 100644
--- a/src/mod/uptime/uptime.go
+++ b/src/mod/uptime/uptime.go
@@ -217,7 +217,11 @@ func getWebsiteStatusWithLatency(url string) (bool, int64, int) {
}
func getWebsiteStatus(url string) (int, error) {
- resp, err := http.Get(url)
+ client := http.Client{
+ Timeout: 10 * time.Second,
+ }
+
+ resp, err := client.Get(url)
if err != nil {
//Try replace the http with https and vise versa
rewriteURL := ""
@@ -227,7 +231,7 @@ func getWebsiteStatus(url string) (int, error) {
rewriteURL = strings.ReplaceAll(url, "http://", "https://")
}
- resp, err = http.Get(rewriteURL)
+ resp, err = client.Get(rewriteURL)
if err != nil {
if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") {
//Invalid downstream reverse proxy settings, but it is online
diff --git a/src/mod/webserv/handler.go b/src/mod/webserv/handler.go
index 5db1837..1aa96e6 100644
--- a/src/mod/webserv/handler.go
+++ b/src/mod/webserv/handler.go
@@ -72,6 +72,7 @@ func (ws *WebServer) HandlePortChange(w http.ResponseWriter, r *http.Request) {
utils.SendErrorResponse(w, err.Error())
return
}
+
utils.SendOK(w)
}
diff --git a/src/reverseproxy.go b/src/reverseproxy.go
index 2c97e3d..cc5b1dc 100644
--- a/src/reverseproxy.go
+++ b/src/reverseproxy.go
@@ -48,6 +48,14 @@ func ReverseProxtInit() {
SystemWideLogger.Println("Force latest TLS mode disabled. Minimum TLS version is set to v1.0")
}
+ developmentMode := false
+ sysdb.Read("settings", "devMode", &developmentMode)
+ if useTls {
+ SystemWideLogger.Println("Development mode enabled. Using no-store Cache Control policy")
+ } else {
+ SystemWideLogger.Println("Development mode disabled. Proxying with default Cache Control policy")
+ }
+
listenOnPort80 := false
sysdb.Read("settings", "listenP80", &listenOnPort80)
if listenOnPort80 {
@@ -74,9 +82,11 @@ func ReverseProxtInit() {
dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
HostUUID: nodeUUID,
+ HostVersion: version,
Port: inboundPort,
UseTls: useTls,
ForceTLSLatest: forceLatestTLSVersion,
+ NoCache: developmentMode,
ListenOnPort80: listenOnPort80,
ForceHttpsRedirect: forceHttpsRedirect,
TlsManager: tlsCertManager,
@@ -325,10 +335,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
}
//Update utm if exists
- if uptimeMonitor != nil {
- uptimeMonitor.Config.Targets = GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter)
- uptimeMonitor.CleanRecords()
- }
+ UpdateUptimeMonitorTargets()
utils.SendOK(w)
}
@@ -741,7 +748,7 @@ func HandleUpdatePort80Listener(w http.ResponseWriter, r *http.Request) {
} else if enabled == "false" {
sysdb.Write("settings", "listenP80", false)
SystemWideLogger.Println("Disabling port 80 listener")
- dynamicProxyRouter.UpdatePort80ListenerState(true)
+ dynamicProxyRouter.UpdatePort80ListenerState(false)
} else {
utils.SendErrorResponse(w, "invalid mode given: "+enabled)
}
@@ -789,6 +796,30 @@ func HandleManagementProxyCheck(w http.ResponseWriter, r *http.Request) {
utils.SendJSONResponse(w, string(js))
}
+func HandleDevelopmentModeChange(w http.ResponseWriter, r *http.Request) {
+ enableDevelopmentModeStr, err := utils.GetPara(r, "enable")
+ if err != nil {
+ //Load the current development mode toggle state
+ js, _ := json.Marshal(dynamicProxyRouter.Option.NoCache)
+ utils.SendJSONResponse(w, string(js))
+ } else {
+ //Write changes to runtime
+ enableDevelopmentMode := false
+ if enableDevelopmentModeStr == "true" {
+ enableDevelopmentMode = true
+ }
+
+ //Write changes to runtime
+ dynamicProxyRouter.Option.NoCache = enableDevelopmentMode
+
+ //Write changes to database
+ sysdb.Write("settings", "devMode", enableDevelopmentMode)
+
+ utils.SendOK(w)
+ }
+
+}
+
// Handle incoming port set. Change the current proxy incoming port
func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
newIncomingPort, err := utils.PostPara(r, "incoming")
diff --git a/src/router.go b/src/router.go
index d02befc..3b64237 100644
--- a/src/router.go
+++ b/src/router.go
@@ -15,6 +15,8 @@ import (
This script holds the static resources router
for the reverse proxy service
+
+ If you are looking for reverse proxy handler, see Server.go in mod/dynamicproxy/
*/
func FSHandler(handler http.Handler) http.Handler {
diff --git a/src/routingrule.go b/src/routingrule.go
new file mode 100644
index 0000000..f704376
--- /dev/null
+++ b/src/routingrule.go
@@ -0,0 +1,35 @@
+package main
+
+import (
+ "net/http"
+ "strings"
+
+ "imuslab.com/zoraxy/mod/dynamicproxy"
+)
+
+/*
+ Routing Rule
+
+ This script handle special routing rules for some utilities functions
+*/
+
+// Register the system build-in routing rules into the core
+func registerBuildInRoutingRules() {
+ //Cloudflare email decoder
+ //It decode the email address if you are proxying a cloudflare protected site
+ //[email-protected] -> real@email.com
+ dynamicProxyRouter.AddRoutingRules(&dynamicproxy.RoutingRule{
+ ID: "cloudflare-decoder",
+ MatchRule: func(r *http.Request) bool {
+ return strings.HasSuffix(r.RequestURI, "cloudflare-static/email-decode.min.js")
+ },
+ RoutingHandler: func(w http.ResponseWriter, r *http.Request) {
+ decoder := "function fixObfuscatedEmails(){let t=document.getElementsByClassName(\"__cf_email__\");for(let e=0;eWhitelist a certain IP or IP range
-
-
+
+
+
+
+
+
-
Default Certificates
-
When there are no matching certificate for the requested server name, reverse proxy router will always fallback to this one. Note that you need both of them uploaded for it to fallback properly
-
-
-
Key Type
-
Exists
-
-
-
-
Default Public Key
-
-
-
-
Default Private Key
-
-
-
-
-
Upload Default Keypairs
-
- Public Key
- Private Key
-
-
-
-
Sub-domain Certificates
+
Hosts Certificates
Provide certificates for multiple domains reverse proxy
+ Match the server name with your CN/DNS entry in certificate for faster resolve time
+ or .crt files in order systems
@@ -63,14 +40,33 @@
Certificate for domain uploaded.
-
+
+
Tips about Server Names & SNI
+
+
+ If you have two subdomains like a.example.com and b.example.com ,
+ for faster response speed, you might want to setup them one by one (i.e. having two seperate certificate for
+ a.example.com and b.example.com).
+
+
+ If you have a wildcard certificate that covers *.example.com,
+ you can just enter example.com as server name to add a certificate.
+
+
+ If you have a certificate contain multiple host, you can enter the first domain in your certificate
+ and Zoraxy will try to match the remaining CN/DNS for you.
+
+
+
+
Current list of loaded certificates
-
+
Domain
Last Update
Expire At
+
Renew
Remove
@@ -81,15 +77,34 @@
Refresh List
-
-
Sub-domain Certificates
- If you have 3rd or even 4th level subdomains like blog.example.com or en.blog.example.com ,
- depending on your certificates coverage, you might need to setup them one by one (i.e. having two seperate certificate for a.example.com and b.example.com).
- If you have a wildcard certificate that covers *.example.com, you can just enter example.com as server name in the form below to add a certificate.
-
-
Certificate Authority (CA) and Auto Renew (ACME)
+
Fallback Certificate
+
When there are no matching certificate for the requested server name, reverse proxy router will always fallback to this one. Note that you need both of them uploaded for it to fallback properly
+
+
+
Key Type
+
Found
+
+
+
+
Fallback Public Key
+
+
+
+
Fallback Private Key
+
+
+
+
+
Upload Default Keypairs
+
+ Public Key
+ Private Key
+
+
+
Certificate Authority (CA) and Auto Renew (ACME)
Management features regarding CA and ACME
+
Prefered Certificate Authority
The default CA to use when create a new subdomain proxy endpoint with TLS certificate
@@ -112,12 +127,12 @@
Save Settings
Certificate Renew / Generation (ACME) Settings
-
+
-
+
Disabled
-
Auto-Renewer Status
+
ACME Auto-Renewer
@@ -130,6 +145,110 @@
$("#defaultCA").dropdown();
+
+ //Renew certificate by button press
+ function renewCertificate(domain, btn=undefined){
+ let defaultCA = $("#defaultCA").dropdown("get value");
+ if (defaultCA.trim() == ""){
+ defaultCA = "Let's Encrypt";
+ }
+ //Get a new cert using ACME
+ msgbox("Requesting certificate via " + defaultCA +"...");
+
+ //Request ACME for certificate
+ if (btn != undefined){
+ $(btn).addClass('disabled');
+ $(btn).html(``);
+ }
+ obtainCertificate(domain, defaultCA.trim(), function(succ){
+ if (btn != undefined){
+ $(btn).removeClass('disabled');
+ if (succ){
+ $(btn).html(``);
+ }else{
+ $(btn).html(``);
+ }
+
+ setTimeout(function(){
+ initManagedDomainCertificateList();
+ }, 3000);
+ }
+ });
+ }
+
+ /*
+ Obtain Certificate via ACME
+ */
+
+ // Obtain certificate from API, only support one domain
+ function obtainCertificate(domains, usingCa = "Let's Encrypt", callback=undefined) {
+ //Load the ACME email from server side
+ let acmeEmail = "";
+ $.get("/api/acme/autoRenew/email", function(data){
+ if (data != "" && data != undefined && data != null){
+ acmeEmail = data;
+ }
+
+ let filename = "";
+ let email = acmeEmail;
+ if (acmeEmail == ""){
+ msgbox("Unable to obtain certificate: ACME email not set", false, 8000);
+ if (callback != undefined){
+ callback(false);
+ }
+ return;
+ }
+ if (filename.trim() == "" && !domains.includes(",")){
+ //Zoraxy filename are the matching name for domains.
+ //Use the same as domains
+ filename = domains;
+ }else if (filename != "" && !domains.includes(",")){
+ //Invalid settings. Force the filename to be same as domain
+ //if there are only 1 domain
+ filename = domains;
+ }else{
+ msgbox("Filename cannot be empty for certs containing multiple domains.")
+ if (callback != undefined){
+ callback(false);
+ }
+ return;
+ }
+
+ $.ajax({
+ url: "/api/acme/obtainCert",
+ method: "GET",
+ data: {
+ domains: domains,
+ filename: filename,
+ email: email,
+ ca: usingCa,
+ },
+ success: function(response) {
+ if (response.error) {
+ console.log("Error:", response.error);
+ // Show error message
+ msgbox(response.error, false, 12000);
+ if (callback != undefined){
+ callback(false);
+ }
+ } else {
+ console.log("Certificate installed successfully");
+ // Show success message
+ msgbox("Certificate installed successfully");
+
+ if (callback != undefined){
+ callback(false);
+ }
+ }
+ },
+ error: function(error) {
+ console.log("Failed to install certificate:", error);
+ }
+ });
+ });
+ }
+
+
//Delete the certificate by its domain
function deleteCertificate(domain){
if (confirm("Confirm delete certificate for " + domain + " ?")){
@@ -154,6 +273,12 @@
//Initialize the current default CA options
$.get("/api/acme/autoRenew/email", function(data){
$("#prefACMEEmail").val(data);
+ if (data.trim() == ""){
+ //acme email is not yet set
+ $(".renewButton").addClass('disabled');
+ }else{
+ $(".renewButton").removeClass('disabled');
+ }
});
$.get("/api/acme/autoRenew/ca", function(data){
@@ -167,7 +292,13 @@
//Set the status of the acme enable icon
function setACMEEnableStates(enabled){
$("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled");
- $("#acmeAutoRenewer").find("i").attr("class", enabled?"green circle icon":"red circle icon");
+ if (enabled){
+ $(".acmeRenewStateWrapper").addClass("enabled");
+ }else{
+ $(".acmeRenewStateWrapper").removeClass("enabled");
+ }
+
+ $("#acmeAutoRenewer").find("i").attr("class", enabled?"white circle check icon":"white circle times icon");
}
initAcmeStatus();
@@ -187,6 +318,9 @@
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
+ }else{
+ //Update the renew button states
+ $(".renewButton").removeClass('disabled');
}
}
});
@@ -223,13 +357,14 @@
${entry.Domain}
${entry.LastModifiedDate}
${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})
+
`);
});
if (data.length == 0){
$("#certifiedDomainList").append(`