diff --git a/src/mod/dynamicproxy/domainsniff/proxmox.go b/src/mod/dynamicproxy/domainsniff/proxmox.go new file mode 100644 index 0000000..e5aa1cb --- /dev/null +++ b/src/mod/dynamicproxy/domainsniff/proxmox.go @@ -0,0 +1,21 @@ +package domainsniff + +import "net/http" + +/* + Promox API sniffer + + This handler sniff proxmox API endpoint and + adjust the request accordingly to fix shits + in the proxmox API server +*/ + +func IsProxmox(r *http.Request) bool { + // Check if any of the cookies is named PVEAuthCookie + for _, cookie := range r.Cookies() { + if cookie.Name == "PVEAuthCookie" { + return true + } + } + return false +} diff --git a/src/mod/dynamicproxy/dpcore/dpcore.go b/src/mod/dynamicproxy/dpcore/dpcore.go index c659435..54bbaa0 100644 --- a/src/mod/dynamicproxy/dpcore/dpcore.go +++ b/src/mod/dynamicproxy/dpcore/dpcore.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "imuslab.com/zoraxy/mod/dynamicproxy/domainsniff" "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" ) @@ -50,7 +51,6 @@ type ReverseProxy struct { ModifyResponse func(*http.Response) error //Prepender is an optional prepend text for URL rewrite - // Prepender string Verbal bool @@ -258,7 +258,7 @@ func (p *ReverseProxy) logf(format string, args ...interface{}) { } } -func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) error { +func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) (int, error) { transport := p.Transport outreq := new(http.Request) @@ -313,6 +313,11 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr // Rewrite outbound UA, must be after user headers rewriteUserAgent(outreq.Header, "Zoraxy/"+rrr.Version) + //Fix proxmox transfer encoding bug if detected Proxmox Cookie + if domainsniff.IsProxmox(req) { + outreq.TransferEncoding = []string{"identity"} + } + res, err := transport.RoundTrip(outreq) if err != nil { if p.Verbal { @@ -320,7 +325,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr } //rw.WriteHeader(http.StatusBadGateway) - return err + return http.StatusBadGateway, err } // Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers. @@ -341,7 +346,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr } //rw.WriteHeader(http.StatusBadGateway) - return err + return http.StatusBadGateway, err } } @@ -388,7 +393,6 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr copyHeader(rw.Header(), res.Header) // inject permission policy headers - //TODO: Load permission policy from rrr permissionpolicy.InjectPermissionPolicyHeader(rw, nil) // The "Trailer" header isn't included in the Transport's response, Build it up from Trailer. @@ -418,14 +422,14 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr res.Body.Close() copyHeader(rw.Header(), res.Trailer) - return nil + return res.StatusCode, nil } -func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) error { +func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) (int, error) { hij, ok := rw.(http.Hijacker) if !ok { p.logf("http server does not support hijacker") - return errors.New("http server does not support hijacker") + return http.StatusNotImplemented, errors.New("http server does not support hijacker") } clientConn, _, err := hij.Hijack() @@ -433,7 +437,7 @@ func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) err if p.Verbal { p.logf("http: proxy error: %v", err) } - return err + return http.StatusInternalServerError, err } proxyConn, err := net.Dial("tcp", req.URL.Host) @@ -442,7 +446,7 @@ func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) err p.logf("http: proxy error: %v", err) } - return err + return http.StatusInternalServerError, err } // The returned net.Conn may have read or write deadlines @@ -461,7 +465,7 @@ func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) err if p.Verbal { p.logf("http: proxy error: %v", err) } - return err + return http.StatusGatewayTimeout, err } err = proxyConn.SetDeadline(deadline) @@ -470,7 +474,7 @@ func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) err p.logf("http: proxy error: %v", err) } - return err + return http.StatusGatewayTimeout, err } _, err = clientConn.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) @@ -479,7 +483,7 @@ func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) err p.logf("http: proxy error: %v", err) } - return err + return http.StatusInternalServerError, err } go func() { @@ -492,15 +496,13 @@ func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) err proxyConn.Close() clientConn.Close() - return nil + return http.StatusOK, nil } -func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) error { +func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) (int, error) { if req.Method == "CONNECT" { - err := p.ProxyHTTPS(rw, req) - return err + return p.ProxyHTTPS(rw, req) } else { - err := p.ProxyHTTP(rw, req, rrr) - return err + return p.ProxyHTTP(rw, req, rrr) } } diff --git a/src/mod/dynamicproxy/dpcore/utils.go b/src/mod/dynamicproxy/dpcore/utils.go index 03cf20a..1974146 100644 --- a/src/mod/dynamicproxy/dpcore/utils.go +++ b/src/mod/dynamicproxy/dpcore/utils.go @@ -1,7 +1,10 @@ package dpcore import ( + "bytes" + "io" "net" + "net/http" "net/url" "strings" ) @@ -92,3 +95,63 @@ func isExternalDomainName(hostname string) bool { return true } + +// DeepCopyRequest returns a deep copy of the given http.Request. +func DeepCopyRequest(req *http.Request) (*http.Request, error) { + // Copy the URL + urlCopy := *req.URL + + // Copy the headers + headersCopy := make(http.Header, len(req.Header)) + for k, vv := range req.Header { + vvCopy := make([]string, len(vv)) + copy(vvCopy, vv) + headersCopy[k] = vvCopy + } + + // Copy the cookies + cookiesCopy := make([]*http.Cookie, len(req.Cookies())) + for i, cookie := range req.Cookies() { + cookieCopy := *cookie + cookiesCopy[i] = &cookieCopy + } + + // Copy the body, if present + var bodyCopy io.ReadCloser + if req.Body != nil { + var buf bytes.Buffer + if _, err := buf.ReadFrom(req.Body); err != nil { + return nil, err + } + // Reset the request body so it can be read again + if err := req.Body.Close(); err != nil { + return nil, err + } + req.Body = io.NopCloser(&buf) + bodyCopy = io.NopCloser(bytes.NewReader(buf.Bytes())) + } + + // Create the new request + reqCopy := &http.Request{ + Method: req.Method, + URL: &urlCopy, + Proto: req.Proto, + ProtoMajor: req.ProtoMajor, + ProtoMinor: req.ProtoMinor, + Header: headersCopy, + Body: bodyCopy, + ContentLength: req.ContentLength, + TransferEncoding: append([]string(nil), req.TransferEncoding...), + Close: req.Close, + Host: req.Host, + Form: req.Form, + PostForm: req.PostForm, + MultipartForm: req.MultipartForm, + Trailer: req.Trailer, + RemoteAddr: req.RemoteAddr, + TLS: req.TLS, + // Cancel and Context are not copied as it might cause issues + } + + return reqCopy, nil +} diff --git a/src/mod/dynamicproxy/loadbalance/upstream.go b/src/mod/dynamicproxy/loadbalance/upstream.go index e04b7f4..dc0436b 100644 --- a/src/mod/dynamicproxy/loadbalance/upstream.go +++ b/src/mod/dynamicproxy/loadbalance/upstream.go @@ -61,8 +61,8 @@ func (u *Upstream) Clone() *Upstream { return &newUpstream } -// ServeHTTP uses this upstream proxy router to route the current request -func (u *Upstream) ServeHTTP(w http.ResponseWriter, r *http.Request, rrr *dpcore.ResponseRewriteRuleSet) error { +// ServeHTTP uses this upstream proxy router to route the current request, return the status code and error if any +func (u *Upstream) ServeHTTP(w http.ResponseWriter, r *http.Request, rrr *dpcore.ResponseRewriteRuleSet) (int, error) { //Auto rewrite to upstream origin if not set if rrr.ProxyDomain == "" { rrr.ProxyDomain = u.OriginIpOrDomain diff --git a/src/mod/dynamicproxy/proxyRequestHandler.go b/src/mod/dynamicproxy/proxyRequestHandler.go index f4ae58d..2cba30b 100644 --- a/src/mod/dynamicproxy/proxyRequestHandler.go +++ b/src/mod/dynamicproxy/proxyRequestHandler.go @@ -112,6 +112,8 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string { func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) { r.Header.Set("X-Forwarded-Host", r.Host) r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID) + + /* Load balancing */ selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(w, r, target.ActiveOrigins, target.UseStickySession) if err != nil { http.ServeFile(w, r, "./web/rperror.html") @@ -119,30 +121,34 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname()) return } - 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("Zr-Origin-Upgrade", "websocket") - wsRedirectionEndpoint := selectedUpstream.OriginIpOrDomain - if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" { - //Append / to the end of the redirection endpoint if not exists - wsRedirectionEndpoint = wsRedirectionEndpoint + "/" + + /* WebSocket automatic proxy */ + if !target.DisableAutoWebSockeyProxy { + 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("Zr-Origin-Upgrade", "websocket") + wsRedirectionEndpoint := selectedUpstream.OriginIpOrDomain + if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" { + //Append / to the end of the redirection endpoint if not exists + wsRedirectionEndpoint = wsRedirectionEndpoint + "/" + } + if len(requestURL) > 0 && requestURL[:1] == "/" { + //Remove starting / from request URL if exists + requestURL = requestURL[1:] + } + u, _ := url.Parse("ws://" + wsRedirectionEndpoint + requestURL) + if selectedUpstream.RequireTLS { + u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL) + } + h.Parent.logRequest(r, true, 101, "host-websocket", selectedUpstream.OriginIpOrDomain) + wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{ + SkipTLSValidation: selectedUpstream.SkipCertValidations, + SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck, + }) + wspHandler.ServeHTTP(w, r) + return } - if len(requestURL) > 0 && requestURL[:1] == "/" { - //Remove starting / from request URL if exists - requestURL = requestURL[1:] - } - u, _ := url.Parse("ws://" + wsRedirectionEndpoint + requestURL) - if selectedUpstream.RequireTLS { - u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL) - } - h.Parent.logRequest(r, true, 101, "host-websocket", selectedUpstream.OriginIpOrDomain) - wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{ - SkipTLSValidation: selectedUpstream.SkipCertValidations, - SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck, - }) - wspHandler.ServeHTTP(w, r) - return } originalHostHeader := r.Host @@ -156,7 +162,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe //Build downstream and upstream header rules upstreamHeaders, downstreamHeaders := target.SplitInboundOutboundHeaders() - err = selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ + statusCode, err := selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ ProxyDomain: selectedUpstream.OriginIpOrDomain, OriginalHost: originalHostHeader, UseTLS: selectedUpstream.RequireTLS, @@ -182,7 +188,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe } } - h.Parent.logRequest(r, true, 200, "host-http", r.URL.Hostname()) + h.Parent.logRequest(r, true, statusCode, "host-http", r.URL.Hostname()) } // Handle vdir type request @@ -224,7 +230,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe //Build downstream and upstream header rules upstreamHeaders, downstreamHeaders := target.parent.SplitInboundOutboundHeaders() - err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ + statusCode, err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ ProxyDomain: target.Domain, OriginalHost: originalHostHeader, UseTLS: target.RequireTLS, @@ -247,7 +253,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe h.Parent.logRequest(r, false, 521, "vdir-http", target.Domain) } } - h.Parent.logRequest(r, true, 200, "vdir-http", target.Domain) + h.Parent.logRequest(r, true, statusCode, "vdir-http", target.Domain) } diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index 16ebd45..a1ceddb 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -136,7 +136,8 @@ type ProxyEndpoint struct { HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers EnablePermissionPolicyHeader bool //Enable injection of permission policy header PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header - DisableHopByHopHeaderRemoval bool //TODO: Do not remove hop-by-hop headers + DisableHopByHopHeaderRemoval bool //Do not remove hop-by-hop headers + DisableAutoWebSockeyProxy bool //Disable auto sniffing logic for websocket upgrade //Authentication RequireBasicAuth bool //Set to true to request basic auth before proxy diff --git a/src/reverseproxy.go b/src/reverseproxy.go index 369b7f1..f2bd0b1 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -1349,7 +1349,8 @@ func HandleHopByHop(w http.ResponseWriter, r *http.Request) { //we need to clone and respawn this proxy endpoint newProxyEndpoint := targetProxyEndpoint.Clone() //Storage file use false as default, so disable removal = not enable remover - targetProxyEndpoint.DisableHopByHopHeaderRemoval = !enableHopByHopRemover + newProxyEndpoint.DisableHopByHopHeaderRemoval = !enableHopByHopRemover + //Save proxy endpoint err = SaveReverseProxyConfig(newProxyEndpoint) if err != nil {