Toby Chui 73ab9ca778 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
2023-08-27 10:18:49 +08:00

262 lines
7.1 KiB
Go

package dynamicproxy
import (
_ "embed"
"errors"
"log"
"net/http"
"net/url"
"os"
"strings"
"imuslab.com/zoraxy/mod/geodb"
)
/*
Server.go
Main server for dynamic proxy core
Routing Handler Priority (High to Low)
- Blacklist
- Whitelist
- Redirectable
- Subdomain Routing
- 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
*/
//Check if there are external routing rule matches.
//If yes, route them via external rr
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
if matchedRoutingRule != nil {
//Matching routing rule found. Let the sub-router handle it
if matchedRoutingRule.UseSystemAccessControl {
//This matching rule request system access control.
//check access logic
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
}
}
matchedRoutingRule.Route(w, r)
return
}
/*
General Access Check
*/
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
}
/*
Redirection Routing
*/
//Check if this is a redirection url
if h.Parent.Option.RedirectRuleTable.IsRedirectable(r) {
statusCode := h.Parent.Option.RedirectRuleTable.HandleRedirect(w, r)
h.logRequest(r, statusCode != 500, statusCode, "redirect", "")
return
}
//Extract request host to see if it is virtual directory or subdomain
domainOnly := r.Host
if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0]
}
/*
Subdomain Routing
*/
if strings.Contains(r.Host, ".") {
//This might be a subdomain. See if there are any subdomain proxy router for this
sep := h.Parent.getSubdomainProxyEndpointFromHostname(domainOnly)
if sep != nil {
if sep.RequireBasicAuth {
err := h.handleBasicAuthRouting(w, r, sep)
if err != nil {
return
}
}
h.subdomainRequest(w, r, sep)
return
}
}
/*
Virtual Directory Routing
*/
//Clean up the request URI
proxyingPath := strings.TrimSpace(r.RequestURI)
targetProxyEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath)
if targetProxyEndpoint != nil {
if targetProxyEndpoint.RequireBasicAuth {
err := h.handleBasicAuthRouting(w, r, targetProxyEndpoint)
if err != nil {
return
}
}
h.proxyRequest(w, r, targetProxyEndpoint)
} else if !strings.HasSuffix(proxyingPath, "/") {
potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil {
//Missing tailing slash. Redirect to target proxy endpoint
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
} else {
//Passthrough the request to root
h.handleRootRouting(w, r)
}
} else {
//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)
}
}
// Handle access routing logic. Return true if the request is handled or blocked by the access control logic
// if the return value is false, you can continue process the response writer
func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Request) bool {
//Check if this ip is in blacklist
clientIpAddr := geodb.GetRequesterIP(r)
if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile("./web/forbidden.html")
if err != nil {
w.Write([]byte("403 - Forbidden"))
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "blacklist", "")
return true
}
//Check if this ip is in whitelist
if !h.Parent.Option.GeodbStore.IsWhitelisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile("./web/forbidden.html")
if err != nil {
w.Write([]byte("403 - Forbidden"))
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "whitelist", "")
return true
}
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")
}