Optimized memory usage and root routing

+ Added unset subdomain custom redirection feature #46
+ Optimized memory usage by space time tradeoff in geoip lookup to fix #52
+ Replaced all stori/go.uuid to google/uuid for security reasons #55
This commit is contained in:
Toby Chui
2023-08-27 10:18:49 +08:00
parent 4f7f60188f
commit 73ab9ca778
22 changed files with 9701 additions and 212 deletions

View File

@@ -1,7 +1,11 @@
package dynamicproxy
import (
_ "embed"
"errors"
"log"
"net/http"
"net/url"
"os"
"strings"
@@ -21,6 +25,11 @@ 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
@@ -108,10 +117,69 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
} else {
//Passthrough the request to root
h.proxyRequest(w, r, h.Parent.Root)
h.handleRootRouting(w, r)
}
} else {
//No routing rules found. Route to root.
//No routing rules found.
h.handleRootRouting(w, r)
}
}
/*
handleRootRouting
This function handle root routing situations where there are no subdomain
, vdir or special routing rule matches the requested URI.
Once entered this routing segment, the root routing options will take over
for the routing logic.
*/
func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) {
domainOnly := r.Host
if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0]
}
if h.Parent.RootRoutingOptions.EnableRedirectForUnsetRules {
//Route to custom domain
if h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget == "" {
//Not set. Redirect to first level of domain redirectable
fld, err := h.getTopLevelRedirectableDomain(domainOnly)
if err != nil {
//Redirect to proxy root
h.proxyRequest(w, r, h.Parent.Root)
} else {
log.Println("[Router] Redirecting request from " + domainOnly + " to " + fld)
h.logRequest(r, false, 307, "root-redirect", domainOnly)
http.Redirect(w, r, fld, http.StatusTemporaryRedirect)
}
return
} else if h.isTopLevelRedirectableDomain(domainOnly) {
//This is requesting a top level private domain that should be serving root
h.proxyRequest(w, r, h.Parent.Root)
} else {
//Validate the redirection target URL
parsedURL, err := url.Parse(h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget)
if err != nil {
//Error when parsing target. Send to root
h.proxyRequest(w, r, h.Parent.Root)
return
}
hostname := parsedURL.Hostname()
if domainOnly != hostname {
//Redirect to target
h.logRequest(r, false, 307, "root-redirect", domainOnly)
http.Redirect(w, r, h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget, http.StatusTemporaryRedirect)
return
} else {
//Loopback request due to bad settings (Shd leave it empty)
//Forward it to root proxy
h.proxyRequest(w, r, h.Parent.Root)
}
}
} else {
//Route to root
h.proxyRequest(w, r, h.Parent.Root)
}
}
@@ -150,3 +218,44 @@ 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")
}

View File

@@ -3,6 +3,7 @@ package dynamicproxy
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"log"
"net/http"
@@ -29,12 +30,19 @@ func NewDynamicProxy(option RouterOption) (*Router, error) {
Running: false,
server: nil,
routingRules: []*RoutingRule{},
tldMap: map[string]int{},
}
thisRouter.mux = &ProxyHandler{
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
}
@@ -65,10 +73,18 @@ func (router *Router) StartProxyService() error {
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")
}
//Load root options from file
loadedRootOption, err := loadRootRoutingOptionsFromFile()
if err != nil {
return err
}
router.RootRoutingOptions = loadedRootOption
minVersion := tls.VersionTLS10
if router.Option.ForceTLSLatest {
minVersion = tls.VersionTLS12
@@ -314,14 +330,15 @@ func (router *Router) SetRootProxy(options *RootOptions) error {
proxy := dpcore.NewDynamicProxyCore(path, "", options.SkipCertValidations)
rootEndpoint := ProxyEndpoint{
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: "/",
Domain: proxyLocation,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
Proxy: proxy,
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: "/",
Domain: proxyLocation,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
Proxy: proxy,
}
router.Root = &rootEndpoint

View File

@@ -0,0 +1,51 @@
package dynamicproxy
import (
"encoding/json"
"errors"
"log"
"os"
"imuslab.com/zoraxy/mod/utils"
)
/*
rootRoute.go
This script handle special case in routing where the root proxy
entity is involved. This also include its setting object
RootRoutingOptions
*/
var rootConfigFilepath string = "conf/root_config.json"
func loadRootRoutingOptionsFromFile() (*RootRoutingOptions, error) {
if !utils.FileExists(rootConfigFilepath) {
//Not found. Create a root option
js, _ := json.MarshalIndent(RootRoutingOptions{}, "", " ")
err := os.WriteFile(rootConfigFilepath, js, 0775)
if err != nil {
return nil, errors.New("Unable to write root config to file: " + err.Error())
}
}
newRootOption := RootRoutingOptions{}
rootOptionsBytes, err := os.ReadFile(rootConfigFilepath)
if err != nil {
log.Println("[Error] Unable to read root config file at " + rootConfigFilepath + ": " + err.Error())
return nil, err
}
err = json.Unmarshal(rootOptionsBytes, &newRootOption)
if err != nil {
log.Println("[Error] Unable to parse root config file: " + err.Error())
return nil, err
}
return &newRootOption, nil
}
// Save the new config to file. Note that this will not overwrite the runtime one
func (opt *RootRoutingOptions) SaveToFile() error {
js, _ := json.MarshalIndent(opt, "", " ")
err := os.WriteFile(rootConfigFilepath, js, 0775)
return err
}

File diff suppressed because it is too large Load Diff

View File

@@ -34,17 +34,19 @@ type RouterOption struct {
}
type Router struct {
Option *RouterOption
ProxyEndpoints *sync.Map
SubdomainEndpoint *sync.Map
Running bool
Root *ProxyEndpoint
mux http.Handler
server *http.Server
tlsListener net.Listener
routingRules []*RoutingRule
Option *RouterOption
ProxyEndpoints *sync.Map
SubdomainEndpoint *sync.Map
Running bool
Root *ProxyEndpoint
RootRoutingOptions *RootRoutingOptions
mux http.Handler
server *http.Server
tlsListener net.Listener
routingRules []*RoutingRule
tlsRedirectStop chan bool
tlsRedirectStop chan bool //Stop channel for tls redirection server
tldMap map[string]int //Top level domain map, see tld.json
}
// Auth credential for basic auth on certain endpoints
@@ -70,6 +72,7 @@ type ProxyEndpoint struct {
RootOrMatchingDomain string //Root for vdir or Matching domain for subd, also act as key
Domain string //Domain or IP to proxy to
RequireTLS bool //Target domain require TLS
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
SkipCertValidations bool //Set to true to accept self signed certs
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials `json:"-"` //Basic auth credentials
@@ -79,19 +82,31 @@ type ProxyEndpoint struct {
parent *Router
}
// Root options are those that are required for reverse proxy handler to work
type RootOptions struct {
ProxyLocation string
RequireTLS bool
SkipCertValidations bool
RequireBasicAuth bool
ProxyLocation string //Proxy Root target, all unset traffic will be forward to here
RequireTLS bool //Proxy root target require TLS connection (not recommended)
BypassGlobalTLS bool //Bypass global TLS setting and make root http only (not recommended)
SkipCertValidations bool //Skip cert validation, suitable for self-signed certs, CURRENTLY NOT USED
//Basic Auth Related
RequireBasicAuth bool //Require basic auth, CURRENTLY NOT USED
BasicAuthCredentials []*BasicAuthCredentials
BasicAuthExceptionRules []*BasicAuthExceptionRule
}
// Additional options are here for letting router knows how to route exception cases for root
type RootRoutingOptions struct {
//Root only configs
EnableRedirectForUnsetRules bool //Force unset rules to redirect to custom domain
UnsetRuleRedirectTarget string //Custom domain to redirect to for unset rules
}
type VdirOptions struct {
RootName string
Domain string
RequireTLS bool
BypassGlobalTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
@@ -102,6 +117,7 @@ type SubdOptions struct {
MatchingDomain string
Domain string
RequireTLS bool
BypassGlobalTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials