diff --git a/x/examples/http2transport/main.go b/x/examples/http2transport/main.go index f78e9b81..1647cd71 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.NewConnectHandler(dialer)} + server := http.Server{Handler: httpproxy.NewProxyHandler(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 52aa9946..f1dc2f47 100644 --- a/x/httpproxy/connect_handler.go +++ b/x/httpproxy/connect_handler.go @@ -15,68 +15,26 @@ package httpproxy import ( - "context" "fmt" "io" "net" "net/http" - "strings" "github.com/Jigsaw-Code/outline-sdk/transport" ) -type handler struct { +type connectHandler struct { dialer transport.StreamDialer - client http.Client } -var _ http.Handler = (*handler)(nil) +var _ http.Handler = (*connectHandler)(nil) -// 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) +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) 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 { @@ -128,11 +86,5 @@ func (h *handler) handleConnect(proxyResp http.ResponseWriter, proxyReq *http.Re // 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 { - 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}}} + return &connectHandler{dialer} } diff --git a/x/httpproxy/forward_handler.go b/x/httpproxy/forward_handler.go new file mode 100644 index 00000000..c9b964dc --- /dev/null +++ b/x/httpproxy/forward_handler.go @@ -0,0 +1,77 @@ +// 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 new file mode 100644 index 00000000..8ce4ce8a --- /dev/null +++ b/x/httpproxy/proxy_handler.go @@ -0,0 +1,48 @@ +// 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 51d976a6..3f89648a 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.NewConnectHandler(dialer)} + server := &http.Server{Handler: httpproxy.NewProxyHandler(dialer)} go server.Serve(listener) host, portStr, err := net.SplitHostPort(listener.Addr().String())