package dynamicproxy import ( _ "embed" "errors" "fmt" "log" "net/http" "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 log.Println("[Router] Unable to resolve top level redirectable domain: " + err.Error()) 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 { //Redirect to target h.logRequest(r, false, 307, "root-redirect", domainOnly) http.Redirect(w, r, h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget, http.StatusTemporaryRedirect) return } } 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 } // 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 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 "", errors.New("already at top level domain") } } else { //Already topped return "", errors.New("already at top level domain") } for i := 0; i < len(parts); i++ { possibleTld := parts[i:] fmt.Println(possibleTld) _, 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") }