diff --git a/x/examples/http2transport/main.go b/x/examples/http2transport/main.go index 1647cd71..f78e9b81 100644 --- a/x/examples/http2transport/main.go +++ b/x/examples/http2transport/main.go @@ -45,7 +45,7 @@ func main() { defer listener.Close() log.Printf("Proxy listening on %v", listener.Addr().String()) - server := http.Server{Handler: httpproxy.NewProxyHandler(dialer)} + server := http.Server{Handler: httpproxy.NewConnectHandler(dialer)} go func() { if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { log.Fatalf("Error running web server: %v", err) diff --git a/x/httpproxy/connect_handler.go b/x/httpproxy/connect_handler.go index f1dc2f47..52aa9946 100644 --- a/x/httpproxy/connect_handler.go +++ b/x/httpproxy/connect_handler.go @@ -15,26 +15,68 @@ package httpproxy import ( + "context" "fmt" "io" "net" "net/http" + "strings" "github.com/Jigsaw-Code/outline-sdk/transport" ) -type connectHandler struct { +type handler struct { dialer transport.StreamDialer + client http.Client } -var _ http.Handler = (*connectHandler)(nil) +var _ http.Handler = (*handler)(nil) -func (h *connectHandler) ServeHTTP(proxyResp http.ResponseWriter, proxyReq *http.Request) { - if proxyReq.Method != http.MethodConnect { - proxyResp.Header().Add("Allow", "CONNECT") - http.Error(proxyResp, fmt.Sprintf("Method %v is not supported", proxyReq.Method), http.StatusMethodNotAllowed) +// ServeHTTP implements [http.Handler].ServeHTTP for CONNECT requests, using the internal [transport.StreamDialer]. +func (h *handler) ServeHTTP(proxyResp http.ResponseWriter, proxyReq *http.Request) { + // TODO(fortuna): For public services (not local), we need authentication and drain on failures to avoid fingerprinting. + if proxyReq.Method == http.MethodConnect { + h.handleConnect(proxyResp, proxyReq) return } + if proxyReq.URL.Host != "" { + h.handleHTTPProxyRequest(proxyResp, proxyReq) + return + } + http.Error(proxyResp, "Not Found", http.StatusNotFound) +} + +func (h *handler) handleHTTPProxyRequest(proxyResp http.ResponseWriter, proxyReq *http.Request) { + // We create a new request that uses a relative path + Host header, instead of the absolute URL in the proxy request. + targetReq, err := http.NewRequestWithContext(proxyReq.Context(), proxyReq.Method, proxyReq.URL.String(), proxyReq.Body) + if err != nil { + http.Error(proxyResp, "Error creating target request", http.StatusInternalServerError) + return + } + for key, values := range proxyReq.Header { + for _, value := range values { + targetReq.Header.Add(key, value) + } + } + targetResp, err := h.client.Do(targetReq) + if err != nil { + http.Error(proxyResp, "Failed to fetch destination", http.StatusServiceUnavailable) + return + } + defer targetResp.Body.Close() + for key, values := range targetResp.Header { + for _, value := range values { + proxyResp.Header().Add(key, value) + } + } + _, err = io.Copy(proxyResp, targetResp.Body) + if err != nil { + http.Error(proxyResp, "Failed write response", http.StatusServiceUnavailable) + return + } +} + +func (h *handler) handleConnect(proxyResp http.ResponseWriter, proxyReq *http.Request) { // Validate the target address. _, portStr, err := net.SplitHostPort(proxyReq.Host) if err != nil { @@ -86,5 +128,11 @@ func (h *connectHandler) ServeHTTP(proxyResp http.ResponseWriter, proxyReq *http // The resulting handler is currently vulnerable to probing attacks. It's ok as a localhost proxy // but it may be vulnerable if used as a public proxy. func NewConnectHandler(dialer transport.StreamDialer) http.Handler { - return &connectHandler{dialer} + dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) { + if !strings.HasPrefix(network, "tcp") { + return nil, fmt.Errorf("protocol not supported: %v", network) + } + return dialer.Dial(ctx, addr) + } + return &handler{dialer, http.Client{Transport: &http.Transport{DialContext: dialContext}}} } diff --git a/x/httpproxy/forward_handler.go b/x/httpproxy/forward_handler.go deleted file mode 100644 index c9b964dc..00000000 --- a/x/httpproxy/forward_handler.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2023 Jigsaw Operations LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package httpproxy - -import ( - "context" - "fmt" - "io" - "net" - "net/http" - "strings" - - "github.com/Jigsaw-Code/outline-sdk/transport" -) - -type forwardHandler struct { - client http.Client -} - -var _ http.Handler = (*forwardHandler)(nil) - -func (h *forwardHandler) ServeHTTP(proxyResp http.ResponseWriter, proxyReq *http.Request) { - if proxyReq.URL.Host == "" { - http.Error(proxyResp, "Must specify an absolute request target", http.StatusNotFound) - return - } - // We create a new request that uses a relative path + Host header, instead of the absolute URL in the proxy request. - targetReq, err := http.NewRequestWithContext(proxyReq.Context(), proxyReq.Method, proxyReq.URL.String(), proxyReq.Body) - if err != nil { - http.Error(proxyResp, "Error creating target request", http.StatusInternalServerError) - return - } - for key, values := range proxyReq.Header { - for _, value := range values { - targetReq.Header.Add(key, value) - } - } - targetResp, err := h.client.Do(targetReq) - if err != nil { - http.Error(proxyResp, "Failed to fetch destination", http.StatusServiceUnavailable) - return - } - defer targetResp.Body.Close() - for key, values := range targetResp.Header { - for _, value := range values { - proxyResp.Header().Add(key, value) - } - } - _, err = io.Copy(proxyResp, targetResp.Body) - if err != nil { - http.Error(proxyResp, "Failed write response", http.StatusServiceUnavailable) - return - } -} - -// NewForwardHandler creates a [http.Handler] that handles absolute HTTP requests using the given [http.Client]. -func NewForwardHandler(dialer transport.StreamDialer) http.Handler { - dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) { - if !strings.HasPrefix(network, "tcp") { - return nil, fmt.Errorf("protocol not supported: %v", network) - } - return dialer.Dial(ctx, addr) - } - return &forwardHandler{http.Client{Transport: &http.Transport{DialContext: dialContext}}} -} diff --git a/x/httpproxy/proxy_handler.go b/x/httpproxy/proxy_handler.go deleted file mode 100644 index 8ce4ce8a..00000000 --- a/x/httpproxy/proxy_handler.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2023 Jigsaw Operations LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package httpproxy - -import ( - "net/http" - - "github.com/Jigsaw-Code/outline-sdk/transport" -) - -type proxyHandler struct { - connectHandler http.Handler - forwardHandler http.Handler -} - -// ServeHTTP implements [http.Handler].ServeHTTP for CONNECT and absolute URL requests, using the internal [transport.StreamDialer]. -func (h *proxyHandler) ServeHTTP(proxyResp http.ResponseWriter, proxyReq *http.Request) { - // TODO(fortuna): For public services (not local), we need authentication and drain on failures to avoid fingerprinting. - if proxyReq.Method == http.MethodConnect { - h.connectHandler.ServeHTTP(proxyResp, proxyReq) - return - } - if proxyReq.URL.Host != "" { - h.forwardHandler.ServeHTTP(proxyResp, proxyReq) - return - } - http.Error(proxyResp, "Not Found", http.StatusNotFound) -} - -// NewProxyHandler creates a [http.Handler] that works as a web proxy using the given dialer to deach the destination. -func NewProxyHandler(dialer transport.StreamDialer) http.Handler { - return &proxyHandler{ - connectHandler: NewConnectHandler(dialer), - forwardHandler: NewForwardHandler(dialer), - } -} diff --git a/x/mobileproxy/mobileproxy.go b/x/mobileproxy/mobileproxy.go index 3f89648a..51d976a6 100644 --- a/x/mobileproxy/mobileproxy.go +++ b/x/mobileproxy/mobileproxy.go @@ -77,7 +77,7 @@ func RunProxy(localAddress string, transportConfig string) (*Proxy, error) { return nil, fmt.Errorf("could not listen on address %v: %v", localAddress, err) } - server := &http.Server{Handler: httpproxy.NewProxyHandler(dialer)} + server := &http.Server{Handler: httpproxy.NewConnectHandler(dialer)} go server.Serve(listener) host, portStr, err := net.SplitHostPort(listener.Addr().String())