TUN-7324: Add http.Hijacker to connection.ResponseWriter
Allows connection.ResponseWriter implemenations to be Hijacked to properly handle WebSocket connection downgrades from proper HTTP requests.
This commit is contained in:
		
							parent
							
								
									be64362fdb
								
							
						
					
					
						commit
						87f81cc57c
					
				|  | @ -6,6 +6,7 @@ import ( | |||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"math" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | @ -197,10 +198,55 @@ func (h *HTTPResponseReadWriteAcker) AckConnection(tracePropagation string) erro | |||
| 	return h.w.WriteRespHeaders(resp.StatusCode, resp.Header) | ||||
| } | ||||
| 
 | ||||
| // localProxyConnection emulates an incoming connection to cloudflared as a net.Conn.
 | ||||
| // Used when handling a "hijacked" connection from connection.ResponseWriter
 | ||||
| type localProxyConnection struct { | ||||
| 	io.ReadWriteCloser | ||||
| } | ||||
| 
 | ||||
| func (c *localProxyConnection) Read(b []byte) (int, error) { | ||||
| 	return c.ReadWriteCloser.Read(b) | ||||
| } | ||||
| 
 | ||||
| func (c *localProxyConnection) Write(b []byte) (int, error) { | ||||
| 	return c.ReadWriteCloser.Write(b) | ||||
| } | ||||
| 
 | ||||
| func (c *localProxyConnection) Close() error { | ||||
| 	return c.ReadWriteCloser.Close() | ||||
| } | ||||
| 
 | ||||
| func (c *localProxyConnection) LocalAddr() net.Addr { | ||||
| 	// Unused LocalAddr
 | ||||
| 	return &net.TCPAddr{IP: net.IPv6loopback, Port: 0, Zone: ""} | ||||
| } | ||||
| 
 | ||||
| func (c *localProxyConnection) RemoteAddr() net.Addr { | ||||
| 	// Unused RemoteAddr
 | ||||
| 	return &net.TCPAddr{IP: net.IPv6loopback, Port: 0, Zone: ""} | ||||
| } | ||||
| 
 | ||||
| func (c *localProxyConnection) SetDeadline(t time.Time) error { | ||||
| 	// ignored since we can't set the read/write Deadlines for the tunnel back to origintunneld
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (c *localProxyConnection) SetReadDeadline(t time.Time) error { | ||||
| 	// ignored since we can't set the read/write Deadlines for the tunnel back to origintunneld
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (c *localProxyConnection) SetWriteDeadline(t time.Time) error { | ||||
| 	// ignored since we can't set the read/write Deadlines for the tunnel back to origintunneld
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // ResponseWriter is the response path for a request back through cloudflared's tunnel.
 | ||||
| type ResponseWriter interface { | ||||
| 	WriteRespHeaders(status int, header http.Header) error | ||||
| 	AddTrailer(trailerName, trailerValue string) | ||||
| 	http.ResponseWriter | ||||
| 	http.Hijacker | ||||
| 	io.Writer | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| package connection | ||||
| 
 | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"context" | ||||
| 	gojson "encoding/json" | ||||
| 	"fmt" | ||||
|  | @ -198,6 +199,8 @@ type http2RespWriter struct { | |||
| 	shouldFlush   bool | ||||
| 	statusWritten bool | ||||
| 	respHeaders   http.Header | ||||
| 	hijackedMutex sync.Mutex | ||||
| 	hijackedv     bool | ||||
| 	log           *zerolog.Logger | ||||
| } | ||||
| 
 | ||||
|  | @ -233,6 +236,10 @@ func (rp *http2RespWriter) AddTrailer(trailerName, trailerValue string) { | |||
| } | ||||
| 
 | ||||
| func (rp *http2RespWriter) WriteRespHeaders(status int, header http.Header) error { | ||||
| 	if rp.hijacked() { | ||||
| 		rp.log.Warn().Msg("WriteRespHeaders after hijack") | ||||
| 		return nil | ||||
| 	} | ||||
| 	dest := rp.w.Header() | ||||
| 	userHeaders := make(http.Header, len(header)) | ||||
| 	for name, values := range header { | ||||
|  | @ -283,9 +290,43 @@ func (rp *http2RespWriter) Header() http.Header { | |||
| } | ||||
| 
 | ||||
| func (rp *http2RespWriter) WriteHeader(status int) { | ||||
| 	if rp.hijacked() { | ||||
| 		rp.log.Warn().Msg("WriteHeader after hijack") | ||||
| 		return | ||||
| 	} | ||||
| 	rp.WriteRespHeaders(status, rp.respHeaders) | ||||
| } | ||||
| 
 | ||||
| func (rp *http2RespWriter) hijacked() bool { | ||||
| 	rp.hijackedMutex.Lock() | ||||
| 	defer rp.hijackedMutex.Unlock() | ||||
| 	return rp.hijackedv | ||||
| } | ||||
| 
 | ||||
| func (rp *http2RespWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { | ||||
| 	if !rp.statusWritten { | ||||
| 		return nil, nil, fmt.Errorf("status not yet written before attempting to hijack connection") | ||||
| 	} | ||||
| 	// Make sure to flush anything left in the buffer before hijacking
 | ||||
| 	if rp.shouldFlush { | ||||
| 		rp.flusher.Flush() | ||||
| 	} | ||||
| 	rp.hijackedMutex.Lock() | ||||
| 	defer rp.hijackedMutex.Unlock() | ||||
| 	if rp.hijackedv { | ||||
| 		return nil, nil, http.ErrHijacked | ||||
| 	} | ||||
| 	rp.hijackedv = true | ||||
| 	conn := &localProxyConnection{rp} | ||||
| 	// We return the http2RespWriter here because we want to make sure that we flush after every write
 | ||||
| 	// otherwise the HTTP2 write buffer waits a few seconds before sending.
 | ||||
| 	readWriter := bufio.NewReadWriter( | ||||
| 		bufio.NewReader(rp), | ||||
| 		bufio.NewWriter(rp), | ||||
| 	) | ||||
| 	return conn, readWriter, nil | ||||
| } | ||||
| 
 | ||||
| func (rp *http2RespWriter) WriteErrorResponse() bool { | ||||
| 	if rp.statusWritten { | ||||
| 		return false | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| package connection | ||||
| 
 | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"fmt" | ||||
|  | @ -435,6 +436,15 @@ func (hrw *httpResponseAdapter) WriteHeader(status int) { | |||
| 	hrw.WriteRespHeaders(status, hrw.headers) | ||||
| } | ||||
| 
 | ||||
| func (hrw *httpResponseAdapter) Hijack() (net.Conn, *bufio.ReadWriter, error) { | ||||
| 	conn := &localProxyConnection{hrw.ReadWriteCloser} | ||||
| 	readWriter := bufio.NewReadWriter( | ||||
| 		bufio.NewReader(hrw.ReadWriteCloser), | ||||
| 		bufio.NewWriter(hrw.ReadWriteCloser), | ||||
| 	) | ||||
| 	return conn, readWriter, nil | ||||
| } | ||||
| 
 | ||||
| func (hrw *httpResponseAdapter) WriteErrorResponse(err error) { | ||||
| 	hrw.WriteConnectResponseData(err, quicpogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(http.StatusBadGateway)}) | ||||
| } | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| package proxy | ||||
| 
 | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"flag" | ||||
|  | @ -76,6 +77,10 @@ func (w *mockHTTPRespWriter) headers() http.Header { | |||
| 	return w.Header() | ||||
| } | ||||
| 
 | ||||
| func (m *mockHTTPRespWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { | ||||
| 	panic("Hijack not implemented") | ||||
| } | ||||
| 
 | ||||
| type mockWSRespWriter struct { | ||||
| 	*mockHTTPRespWriter | ||||
| 	writeNotification chan []byte | ||||
|  | @ -109,6 +114,10 @@ func (w *mockWSRespWriter) Read(data []byte) (int, error) { | |||
| 	return w.reader.Read(data) | ||||
| } | ||||
| 
 | ||||
| func (m *mockWSRespWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { | ||||
| 	panic("Hijack not implemented") | ||||
| } | ||||
| 
 | ||||
| type mockSSERespWriter struct { | ||||
| 	*mockHTTPRespWriter | ||||
| 	writeNotification chan []byte | ||||
|  | @ -840,6 +849,10 @@ func (w *wsRespWriter) WriteHeader(status int) { | |||
| 	// unused
 | ||||
| } | ||||
| 
 | ||||
| func (m *wsRespWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { | ||||
| 	panic("Hijack not implemented") | ||||
| } | ||||
| 
 | ||||
| type mockTCPRespWriter struct { | ||||
| 	w               io.Writer | ||||
| 	responseHeaders http.Header | ||||
|  | @ -879,6 +892,10 @@ func (m *mockTCPRespWriter) WriteHeader(status int) { | |||
| 	// do nothing
 | ||||
| } | ||||
| 
 | ||||
| func (m *mockTCPRespWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { | ||||
| 	panic("Hijack not implemented") | ||||
| } | ||||
| 
 | ||||
| func createSingleIngressConfig(t *testing.T, service string) ingress.Ingress { | ||||
| 	ingressConfig := &config.Configuration{ | ||||
| 		Ingress: []config.UnvalidatedIngressRule{ | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue