mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-01 13:17:21 +02:00
397 lines
10 KiB
Go
397 lines
10 KiB
Go
package dynamicproxy
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"errors"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
|
)
|
|
|
|
/*
|
|
Zoraxy Dynamic Proxy
|
|
*/
|
|
|
|
func NewDynamicProxy(option RouterOption) (*Router, error) {
|
|
proxyMap := sync.Map{}
|
|
thisRouter := Router{
|
|
Option: &option,
|
|
ProxyEndpoints: &proxyMap,
|
|
Running: false,
|
|
server: nil,
|
|
routingRules: []*RoutingRule{},
|
|
loadBalancer: option.LoadBalancer,
|
|
rateLimitCounter: RequestCountPerIpTable{},
|
|
}
|
|
|
|
thisRouter.mux = &ProxyHandler{
|
|
Parent: &thisRouter,
|
|
}
|
|
|
|
return &thisRouter, nil
|
|
}
|
|
|
|
// Update TLS setting in runtime. Will restart the proxy server
|
|
// if it is already running in the background
|
|
func (router *Router) UpdateTLSSetting(tlsEnabled bool) {
|
|
router.Option.UseTls = tlsEnabled
|
|
router.Restart()
|
|
}
|
|
|
|
// Update TLS Version in runtime. Will restart proxy server if running.
|
|
// Set this to true to force TLS 1.2 or above
|
|
func (router *Router) UpdateTLSVersion(requireLatest bool) {
|
|
router.Option.ForceTLSLatest = requireLatest
|
|
router.Restart()
|
|
}
|
|
|
|
// Update port 80 listener state
|
|
func (router *Router) UpdatePort80ListenerState(useRedirect bool) {
|
|
router.Option.ListenOnPort80 = useRedirect
|
|
router.Restart()
|
|
}
|
|
|
|
// Update https redirect, which will require updates
|
|
func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
|
|
router.Option.ForceHttpsRedirect = useRedirect
|
|
router.Restart()
|
|
}
|
|
|
|
// Start the dynamic routing
|
|
func (router *Router) StartProxyService() error {
|
|
//Create a new server object
|
|
if router.server != nil {
|
|
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")
|
|
}
|
|
|
|
minVersion := tls.VersionTLS10
|
|
if router.Option.ForceTLSLatest {
|
|
minVersion = tls.VersionTLS12
|
|
}
|
|
config := &tls.Config{
|
|
GetCertificate: router.Option.TlsManager.GetCert,
|
|
MinVersion: uint16(minVersion),
|
|
}
|
|
|
|
//Start rate limitor
|
|
err := router.startRateLimterCounterResetTicker()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if router.Option.UseTls {
|
|
router.server = &http.Server{
|
|
Addr: ":" + strconv.Itoa(router.Option.Port),
|
|
Handler: router.mux,
|
|
TLSConfig: config,
|
|
}
|
|
router.Running = true
|
|
|
|
if router.Option.Port != 80 && router.Option.ListenOnPort80 {
|
|
//Add a 80 to 443 redirector
|
|
httpServer := &http.Server{
|
|
Addr: ":80",
|
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
//Check if the domain requesting allow non TLS mode
|
|
domainOnly := r.Host
|
|
if strings.Contains(r.Host, ":") {
|
|
hostPath := strings.Split(r.Host, ":")
|
|
domainOnly = hostPath[0]
|
|
}
|
|
sep := router.getProxyEndpointFromHostname(domainOnly)
|
|
if sep != nil && sep.BypassGlobalTLS {
|
|
//Allow routing via non-TLS handler
|
|
originalHostHeader := r.Host
|
|
if r.URL != nil {
|
|
r.Host = r.URL.Host
|
|
} else {
|
|
//Fallback when the upstream proxy screw something up in the header
|
|
r.URL, _ = url.Parse(originalHostHeader)
|
|
}
|
|
|
|
//Access Check (blacklist / whitelist)
|
|
ruleID := sep.AccessFilterUUID
|
|
if sep.AccessFilterUUID == "" {
|
|
//Use default rule
|
|
ruleID = "default"
|
|
}
|
|
accessRule, err := router.Option.AccessController.GetAccessRuleByID(ruleID)
|
|
if err == nil {
|
|
isBlocked, _ := accessRequestBlocked(accessRule, router.Option.WebDirectory, w, r)
|
|
if isBlocked {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Rate Limit
|
|
if sep.RequireRateLimit {
|
|
if err := router.handleRateLimit(w, r, sep); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
//Validate basic auth
|
|
if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic {
|
|
err := handleBasicAuth(w, r, sep)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
selectedUpstream, err := router.loadBalancer.GetRequestUpstreamTarget(w, r, sep.ActiveOrigins, sep.UseStickySession)
|
|
if err != nil {
|
|
http.ServeFile(w, r, "./web/hosterror.html")
|
|
router.Option.Logger.PrintAndLog("dprouter", "failed to get upstream for hostname", err)
|
|
router.logRequest(r, false, 404, "vdir-http", r.Host, "")
|
|
}
|
|
|
|
endpointProxyRewriteRules := GetDefaultHeaderRewriteRules()
|
|
if sep.HeaderRewriteRules != nil {
|
|
endpointProxyRewriteRules = sep.HeaderRewriteRules
|
|
}
|
|
|
|
selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
|
ProxyDomain: selectedUpstream.OriginIpOrDomain,
|
|
OriginalHost: originalHostHeader,
|
|
UseTLS: selectedUpstream.RequireTLS,
|
|
HostHeaderOverwrite: endpointProxyRewriteRules.RequestHostOverwrite,
|
|
NoRemoveHopByHop: endpointProxyRewriteRules.DisableHopByHopHeaderRemoval,
|
|
PathPrefix: "",
|
|
Version: sep.parent.Option.HostVersion,
|
|
})
|
|
return
|
|
}
|
|
|
|
if router.Option.ForceHttpsRedirect {
|
|
//Redirect to https is enabled
|
|
protocol := "https://"
|
|
if router.Option.Port == 443 {
|
|
http.Redirect(w, r, protocol+r.Host+r.RequestURI, http.StatusTemporaryRedirect)
|
|
} else {
|
|
http.Redirect(w, r, protocol+r.Host+":"+strconv.Itoa(router.Option.Port)+r.RequestURI, http.StatusTemporaryRedirect)
|
|
}
|
|
} else {
|
|
//Do not do redirection
|
|
if sep != nil {
|
|
//Sub-domain exists but not allow non-TLS access
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte("400 - Bad Request"))
|
|
} else {
|
|
//No defined sub-domain
|
|
if router.Root.DefaultSiteOption == DefaultSite_NoResponse {
|
|
//No response. Just close the connection
|
|
hijacker, ok := w.(http.Hijacker)
|
|
if !ok {
|
|
w.Header().Set("Connection", "close")
|
|
return
|
|
}
|
|
conn, _, err := hijacker.Hijack()
|
|
if err != nil {
|
|
w.Header().Set("Connection", "close")
|
|
return
|
|
}
|
|
conn.Close()
|
|
} else {
|
|
//Default behavior
|
|
http.NotFound(w, r)
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}),
|
|
ReadTimeout: 3 * time.Second,
|
|
WriteTimeout: 3 * time.Second,
|
|
IdleTimeout: 120 * time.Second,
|
|
}
|
|
|
|
router.Option.Logger.PrintAndLog("dprouter", "Starting HTTP-to-HTTPS redirector (port 80)", nil)
|
|
|
|
//Create a redirection stop channel
|
|
stopChan := make(chan bool)
|
|
|
|
//Start a blocking wait for shutting down the http to https redirection server
|
|
go func() {
|
|
<-stopChan
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
httpServer.Shutdown(ctx)
|
|
router.Option.Logger.PrintAndLog("dprouter", "HTTP to HTTPS redirection listener stopped", nil)
|
|
}()
|
|
|
|
//Start the http server that listens to port 80 and redirect to 443
|
|
go func() {
|
|
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
//Unable to startup port 80 listener. Handle shutdown process gracefully
|
|
stopChan <- true
|
|
log.Fatalf("Could not start redirection server: %v\n", err)
|
|
}
|
|
}()
|
|
router.tlsRedirectStop = stopChan
|
|
}
|
|
|
|
//Start the TLS server
|
|
router.Option.Logger.PrintAndLog("dprouter", "Reverse proxy service started in the background (TLS mode)", nil)
|
|
go func() {
|
|
if err := router.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
|
|
router.Option.Logger.PrintAndLog("dprouter", "Could not start proxy server", err)
|
|
}
|
|
}()
|
|
} else {
|
|
//Serve with non TLS mode
|
|
router.tlsListener = nil
|
|
router.server = &http.Server{Addr: ":" + strconv.Itoa(router.Option.Port), Handler: router.mux}
|
|
router.Running = true
|
|
router.Option.Logger.PrintAndLog("dprouter", "Reverse proxy service started in the background (Plain HTTP mode)", nil)
|
|
go func() {
|
|
router.server.ListenAndServe()
|
|
}()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (router *Router) StopProxyService() error {
|
|
if router.server == 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
|
|
}
|
|
|
|
//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)
|
|
if router.tlsRedirectStop != nil {
|
|
router.tlsRedirectStop <- true
|
|
}
|
|
|
|
//Discard the server object
|
|
router.tlsListener = nil
|
|
router.server = nil
|
|
router.Running = false
|
|
router.tlsRedirectStop = nil
|
|
return nil
|
|
}
|
|
|
|
// Restart the current router if it is running.
|
|
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 {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/*
|
|
Check if a given request is accessed via a proxied subdomain
|
|
*/
|
|
|
|
func (router *Router) IsProxiedSubdomain(r *http.Request) bool {
|
|
hostname := r.Header.Get("X-Forwarded-Host")
|
|
if hostname == "" {
|
|
hostname = r.Host
|
|
}
|
|
hostname = strings.Split(hostname, ":")[0]
|
|
subdEndpoint := router.getProxyEndpointFromHostname(hostname)
|
|
return subdEndpoint != nil
|
|
}
|
|
|
|
/*
|
|
Load routing from RP
|
|
*/
|
|
func (router *Router) LoadProxy(matchingDomain string) (*ProxyEndpoint, error) {
|
|
var targetProxyEndpoint *ProxyEndpoint
|
|
router.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
|
key, ok := key.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
v, ok := value.(*ProxyEndpoint)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
if key == strings.ToLower(matchingDomain) {
|
|
targetProxyEndpoint = v
|
|
}
|
|
return true
|
|
})
|
|
|
|
if targetProxyEndpoint == nil {
|
|
return nil, errors.New("target routing rule not found")
|
|
}
|
|
|
|
return targetProxyEndpoint, nil
|
|
}
|
|
|
|
// Deep copy a proxy endpoint, excluding runtime paramters
|
|
func CopyEndpoint(endpoint *ProxyEndpoint) *ProxyEndpoint {
|
|
js, _ := json.Marshal(endpoint)
|
|
newProxyEndpoint := ProxyEndpoint{}
|
|
err := json.Unmarshal(js, &newProxyEndpoint)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return &newProxyEndpoint
|
|
}
|
|
|
|
func (r *Router) GetProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
|
m := make(map[string]*ProxyEndpoint)
|
|
r.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
|
k, ok := key.(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
v, ok := value.(*ProxyEndpoint)
|
|
if !ok {
|
|
return true
|
|
}
|
|
m[k] = v
|
|
return true
|
|
})
|
|
return m
|
|
}
|