forward: rewrite to use httputil.ReverseProxy (#229)

Co-authored-by: Fernandez Ludovic <[email protected]>
This commit is contained in:
Julien Salleyron
2022-11-07 09:41:14 +01:00
committed by GitHub
co-authored by Fernandez Ludovic
parent 9260f86ffc
commit d996bf5376
18 changed files with 313 additions and 1318 deletions
+3 -3
View File
@@ -38,7 +38,7 @@ import (
)
// Forwards incoming requests to whatever location URL points to, adds proper forwarding headers
fwd, _ := forward.New()
fwd := forward.New(false)
redirect := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// let us forward this request to another server
@@ -66,7 +66,7 @@ import (
)
// Forwards incoming requests to whatever location URL points to, adds proper forwarding headers
fwd, _ := forward.New()
fwd := forward.New(false)
lb, _ := roundrobin.New(fwd)
lb.UpsertServer(url1)
@@ -93,7 +93,7 @@ import (
// Forwards incoming requests to whatever location URL points to, adds proper forwarding headers
fwd, _ := forward.New()
fwd := forward.New(false)
lb, _ := roundrobin.New(fwd)
// buffer will read the request body and will replay the request again in case if forward returned status
+12 -24
View File
@@ -25,8 +25,7 @@ func TestSimple(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -60,8 +59,7 @@ func TestChunkedEncodingSuccess(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -95,8 +93,7 @@ func TestChunkedEncodingLimitReached(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -129,8 +126,7 @@ func TestChunkedResponse(t *testing.T) {
})
defer srv.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI(srv.URL)
@@ -156,8 +152,7 @@ func TestRequestLimitReached(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -184,8 +179,7 @@ func TestResponseLimitReached(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -212,8 +206,7 @@ func TestFileStreamingResponse(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -241,8 +234,7 @@ func TestCustomErrorHandler(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -273,8 +265,7 @@ func TestNotModified(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -301,8 +292,7 @@ func TestNoBody(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -331,8 +321,7 @@ func TestPreservesTLS(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
var cs *tls.ConnectionState
// this is our redirect to server
@@ -363,8 +352,7 @@ func TestNotNilBody(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+1 -2
View File
@@ -76,8 +76,7 @@ func newBufferMiddleware(t *testing.T, p string) (*roundrobin.RoundRobin, *Buffe
t.Helper()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
// load balancer will round robin request
lb, err := roundrobin.New(fwd)
+149
View File
@@ -0,0 +1,149 @@
package forward
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
)
func ExampleNew_customErrHandler() {
f := New(true)
f.ErrorHandler = func(w http.ResponseWriter, req *http.Request, err error) {
w.WriteHeader(http.StatusTeapot)
_, _ = w.Write([]byte(http.StatusText(http.StatusTeapot)))
}
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL, _ = url.ParseRequestURI("http://localhost:63450")
f.ServeHTTP(w, req)
}))
defer proxy.Close()
resp, err := http.Get(proxy.URL)
if err != nil {
fmt.Println(err)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(resp.StatusCode)
fmt.Println(string(body))
// output:
// 418
// I'm a teapot
}
func ExampleNew_responseModifier() {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
_, _ = w.Write([]byte("hello"))
}))
defer srv.Close()
f := New(true)
f.ModifyResponse = func(resp *http.Response) error {
resp.Header.Add("X-Test", "CUSTOM")
return nil
}
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL, _ = url.ParseRequestURI(srv.URL)
f.ServeHTTP(w, req)
}))
defer proxy.Close()
resp, err := http.Get(proxy.URL)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(resp.StatusCode)
fmt.Println(resp.Header.Get("X-Test"))
// Output:
// 200
// CUSTOM
}
func ExampleNew_customTransport() {
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
_, _ = w.Write([]byte("hello"))
}))
defer srv.Close()
f := New(true)
f.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL, _ = url.ParseRequestURI(srv.URL)
f.ServeHTTP(w, req)
}))
defer proxy.Close()
resp, err := http.Get(proxy.URL)
if err != nil {
fmt.Println(err)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(resp.StatusCode)
fmt.Println(string(body))
// Output:
// 200
// hello
}
func ExampleNewStateListener() {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
_, _ = w.Write([]byte("hello"))
}))
defer srv.Close()
f := New(true)
f.ModifyResponse = func(resp *http.Response) error {
resp.Header.Add("X-Test", "CUSTOM")
return nil
}
stateLn := NewStateListener(f, func(u *url.URL, i int) {
fmt.Println(u.Hostname(), i)
})
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL, _ = url.ParseRequestURI(srv.URL)
stateLn.ServeHTTP(w, req)
}))
defer proxy.Close()
resp, err := http.Get(proxy.URL)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(resp.StatusCode)
// Output:
// 127.0.0.1 0
// 127.0.0.1 1
// 200
}
+26 -525
View File
@@ -1,290 +1,35 @@
// Package forward implements http handler that forwards requests to remote server
// and serves back the response
// websocket proxying support based on https://github.com/yhat/wsutil
// Package forward creates a pre-configured httputil.ReverseProxy.
package forward
import (
"bytes"
"crypto/tls"
"errors"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"reflect"
"strings"
"time"
"github.com/gorilla/websocket"
log "github.com/sirupsen/logrus"
"github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/utils"
)
// OxyLogger interface of the internal.
type OxyLogger interface {
log.FieldLogger
GetLevel() log.Level
}
// New creates a new ReverseProxy.
func New(passHostHeader bool) *httputil.ReverseProxy {
h := NewHeaderRewriter()
type internalLogger struct {
*log.Logger
}
return &httputil.ReverseProxy{
Director: func(request *http.Request) {
modifyRequest(request)
func (i *internalLogger) GetLevel() log.Level {
return i.Level
}
h.Rewrite(request)
// ReqRewriter can alter request headers and body.
type ReqRewriter interface {
Rewrite(r *http.Request)
}
type optSetter func(f *Forwarder) error
// PassHostHeader specifies if a client's Host header field should be delegated.
func PassHostHeader(b bool) optSetter {
return func(f *Forwarder) error {
f.httpForwarder.passHost = b
return nil
if !passHostHeader {
request.Host = request.URL.Host
}
},
ErrorHandler: utils.DefaultHandler.ServeHTTP,
}
}
// RoundTripper sets a new http.RoundTripper
// Forwarder will use http.DefaultTransport as a default round tripper.
func RoundTripper(r http.RoundTripper) optSetter {
return func(f *Forwarder) error {
f.httpForwarder.roundTripper = r
return nil
}
}
// Rewriter defines a request rewriter for the HTTP forwarder.
func Rewriter(r ReqRewriter) optSetter {
return func(f *Forwarder) error {
f.httpForwarder.rewriter = r
return nil
}
}
// WebsocketTLSClientConfig define the websocker client TLS configuration.
func WebsocketTLSClientConfig(tcc *tls.Config) optSetter {
return func(f *Forwarder) error {
f.httpForwarder.tlsClientConfig = tcc
return nil
}
}
// ErrorHandler is a functional argument that sets error handler of the server.
func ErrorHandler(h utils.ErrorHandler) optSetter {
return func(f *Forwarder) error {
f.errHandler = h
return nil
}
}
// BufferPool specifies a buffer pool for httputil.ReverseProxy.
func BufferPool(pool httputil.BufferPool) optSetter {
return func(f *Forwarder) error {
f.bufferPool = pool
return nil
}
}
// Stream specifies if HTTP responses should be streamed.
func Stream(stream bool) optSetter {
return func(f *Forwarder) error {
f.stream = stream
return nil
}
}
// Logger defines the logger the forwarder will use.
//
// It defaults to logrus.StandardLogger(), the global logger used by logrus.
func Logger(l log.FieldLogger) optSetter {
return func(f *Forwarder) error {
if logger, ok := l.(OxyLogger); ok {
f.log = logger
return nil
}
if logger, ok := l.(*log.Logger); ok {
f.log = &internalLogger{Logger: logger}
return nil
}
return errors.New("the type of the logger must be OxyLogger or logrus.Logger")
}
}
// StateListener defines a state listener for the HTTP forwarder.
func StateListener(stateListener URLForwardingStateListener) optSetter {
return func(f *Forwarder) error {
f.stateListener = stateListener
return nil
}
}
// WebsocketConnectionClosedHook defines a hook called when websocket connection is closed.
func WebsocketConnectionClosedHook(hook func(req *http.Request, conn net.Conn)) optSetter {
return func(f *Forwarder) error {
f.httpForwarder.websocketConnectionClosedHook = hook
return nil
}
}
// ResponseModifier defines a response modifier for the HTTP forwarder.
func ResponseModifier(responseModifier func(*http.Response) error) optSetter {
return func(f *Forwarder) error {
f.httpForwarder.modifyResponse = responseModifier
return nil
}
}
// StreamingFlushInterval defines a streaming flush interval for the HTTP forwarder.
func StreamingFlushInterval(flushInterval time.Duration) optSetter {
return func(f *Forwarder) error {
f.httpForwarder.flushInterval = flushInterval
return nil
}
}
// Forwarder wraps two traffic forwarding implementations: HTTP and websockets.
// It decides based on the specified request which implementation to use.
type Forwarder struct {
*httpForwarder
*handlerContext
stateListener URLForwardingStateListener
stream bool
}
// handlerContext defines a handler context for error reporting and logging.
type handlerContext struct {
errHandler utils.ErrorHandler
}
// httpForwarder is a handler that can reverse proxy HTTP traffic.
type httpForwarder struct {
roundTripper http.RoundTripper
rewriter ReqRewriter
passHost bool
flushInterval time.Duration
modifyResponse func(*http.Response) error
tlsClientConfig *tls.Config
log OxyLogger
bufferPool httputil.BufferPool
websocketConnectionClosedHook func(req *http.Request, conn net.Conn)
}
const defaultFlushInterval = 100 * clock.Millisecond
// Connection states.
const (
StateConnected = iota
StateDisconnected
)
// UrlForwardingStateListener alias on URLForwardingStateListener.
// Deprecated: use URLForwardingStateListener instead.
type UrlForwardingStateListener = URLForwardingStateListener
// URLForwardingStateListener URL forwarding state listener.
type URLForwardingStateListener func(*url.URL, int)
// New creates an instance of Forwarder based on the provided list of configuration options.
func New(setters ...optSetter) (*Forwarder, error) {
f := &Forwarder{
httpForwarder: &httpForwarder{log: &internalLogger{Logger: log.StandardLogger()}},
handlerContext: &handlerContext{},
}
for _, s := range setters {
if err := s(f); err != nil {
return nil, err
}
}
if !f.stream {
f.flushInterval = 0
} else if f.flushInterval == 0 {
f.flushInterval = defaultFlushInterval
}
if f.httpForwarder.rewriter == nil {
h, err := os.Hostname()
if err != nil {
h = "localhost"
}
f.httpForwarder.rewriter = &HeaderRewriter{TrustForwardHeader: true, Hostname: h}
}
if f.httpForwarder.roundTripper == nil {
f.httpForwarder.roundTripper = http.DefaultTransport
}
if f.errHandler == nil {
f.errHandler = utils.DefaultHandler
}
if f.tlsClientConfig == nil {
if ht, ok := f.httpForwarder.roundTripper.(*http.Transport); ok {
f.tlsClientConfig = ht.TLSClientConfig
}
}
f.postConfig()
return f, nil
}
// ServeHTTP decides which forwarder to use based on the specified
// request and delegates to the proper implementation.
func (f *Forwarder) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if f.log.GetLevel() >= log.DebugLevel {
logEntry := f.log.WithField("Request", utils.DumpHTTPRequest(req))
logEntry.Debug("vulcand/oxy/forward: begin ServeHttp on request")
defer logEntry.Debug("vulcand/oxy/forward: completed ServeHttp on request")
}
if f.stateListener != nil {
f.stateListener(req.URL, StateConnected)
defer f.stateListener(req.URL, StateDisconnected)
}
if IsWebsocketRequest(req) {
f.httpForwarder.serveWebSocket(w, req, f.handlerContext)
} else {
f.httpForwarder.serveHTTP(w, req, f.handlerContext)
}
}
func (f *httpForwarder) getURLFromRequest(req *http.Request) *url.URL {
// If the Request was created by Go via a real HTTP request, RequestURI will
// contain the original query string. If the Request was created in code, RequestURI
// will be empty, and we will use the URL object instead
u := req.URL
if req.RequestURI != "" {
parsedURL, err := url.ParseRequestURI(req.RequestURI)
if err == nil {
u = parsedURL
} else {
f.log.Warnf("vulcand/oxy/forward: error when parsing RequestURI: %s", err)
}
}
return u
}
// Modify the request to handle the target URL.
func (f *httpForwarder) modifyRequest(outReq *http.Request, target *url.URL) {
outReq.URL = utils.CopyURL(outReq.URL)
outReq.URL.Scheme = target.Scheme
outReq.URL.Host = target.Host
u := f.getURLFromRequest(outReq)
func modifyRequest(outReq *http.Request) {
u := getURLFromRequest(outReq)
outReq.URL.Path = u.Path
outReq.URL.RawPath = u.RawPath
@@ -294,263 +39,19 @@ func (f *httpForwarder) modifyRequest(outReq *http.Request, target *url.URL) {
outReq.Proto = "HTTP/1.1"
outReq.ProtoMajor = 1
outReq.ProtoMinor = 1
if f.rewriter != nil {
f.rewriter.Rewrite(outReq)
}
// Do not pass client Host header unless optsetter PassHostHeader is set.
if !f.passHost {
outReq.Host = target.Host
}
}
// serveWebSocket forwards websocket traffic.
func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request, ctx *handlerContext) {
if f.log.GetLevel() >= log.DebugLevel {
logEntry := f.log.WithField("Request", utils.DumpHTTPRequest(req))
logEntry.Debug("vulcand/oxy/forward/websocket: begin ServeHttp on request")
defer logEntry.Debug("vulcand/oxy/forward/websocket: completed ServeHttp on request")
}
outReq := f.copyWebSocketRequest(req)
dialer := websocket.DefaultDialer
if outReq.URL.Scheme == "wss" && f.tlsClientConfig != nil {
dialer.TLSClientConfig = f.tlsClientConfig.Clone()
// WebSocket is only in http/1.1
dialer.TLSClientConfig.NextProtos = []string{"http/1.1"}
}
targetConn, resp, err := dialer.DialContext(outReq.Context(), outReq.URL.String(), outReq.Header)
if err != nil {
if resp == nil {
ctx.errHandler.ServeHTTP(w, req, err)
return
}
f.log.Errorf("vulcand/oxy/forward/websocket: Error dialing %q: %v with resp: %d %s", outReq.Host, err, resp.StatusCode, resp.Status)
hijacker, ok := w.(http.Hijacker)
if !ok {
f.log.Errorf("vulcand/oxy/forward/websocket: %s can not be hijack", reflect.TypeOf(w))
ctx.errHandler.ServeHTTP(w, req, err)
return
}
conn, _, errHijack := hijacker.Hijack()
if errHijack != nil {
f.log.Errorf("vulcand/oxy/forward/websocket: Failed to hijack responseWriter")
ctx.errHandler.ServeHTTP(w, req, errHijack)
return
}
defer func() {
_ = conn.Close()
if f.websocketConnectionClosedHook != nil {
f.websocketConnectionClosedHook(req, conn)
}
}()
errWrite := resp.Write(conn)
if errWrite != nil {
f.log.Errorf("vulcand/oxy/forward/websocket: Failed to forward response")
ctx.errHandler.ServeHTTP(w, req, errWrite)
return
}
return
}
// Only the targetConn choose to CheckOrigin or not
upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
return true
}}
utils.RemoveHeaders(resp.Header, WebsocketUpgradeHeaders...)
utils.CopyHeaders(resp.Header, w.Header())
underlyingConn, err := upgrader.Upgrade(w, req, resp.Header)
if err != nil {
f.log.Errorf("vulcand/oxy/forward/websocket: Error while upgrading connection : %v", err)
return
}
defer func() {
_ = underlyingConn.Close()
_ = targetConn.Close()
if f.websocketConnectionClosedHook != nil {
f.websocketConnectionClosedHook(req, underlyingConn.UnderlyingConn())
}
}()
errClient := make(chan error, 1)
errBackend := make(chan error, 1)
replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error) {
forward := func(messageType int, reader io.Reader) error {
writer, err := dst.NextWriter(messageType)
if err != nil {
return err
}
_, err = io.Copy(writer, reader)
if err != nil {
return err
}
return writer.Close()
}
src.SetPingHandler(func(data string) error {
return forward(websocket.PingMessage, bytes.NewReader([]byte(data)))
})
src.SetPongHandler(func(data string) error {
return forward(websocket.PongMessage, bytes.NewReader([]byte(data)))
})
for {
msgType, reader, err := src.NextReader()
if err != nil {
m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error())
if e, ok := err.(*websocket.CloseError); ok {
if e.Code != websocket.CloseNoStatusReceived {
m = nil
// Following codes are not valid on the wire so just close the
// underlying TCP connection without sending a close frame.
if e.Code != websocket.CloseAbnormalClosure && e.Code != websocket.CloseTLSHandshake {
m = websocket.FormatCloseMessage(e.Code, e.Text)
}
}
}
errc <- err
if m != nil {
_ = forward(websocket.CloseMessage, bytes.NewReader(m))
}
break
}
err = forward(msgType, reader)
if err != nil {
errc <- err
break
}
func getURLFromRequest(req *http.Request) *url.URL {
// If the Request was created by Go via a real HTTP request,
// RequestURI will contain the original query string.
// If the Request was created in code,
// RequestURI will be empty, and we will use the URL object instead
u := req.URL
if req.RequestURI != "" {
parsedURL, err := url.ParseRequestURI(req.RequestURI)
if err == nil {
return parsedURL
}
}
go replicateWebsocketConn(underlyingConn, targetConn, errClient)
go replicateWebsocketConn(targetConn, underlyingConn, errBackend)
var message string
select {
case err = <-errClient:
message = "vulcand/oxy/forward/websocket: Error when copying from backend to client: %v"
case err = <-errBackend:
message = "vulcand/oxy/forward/websocket: Error when copying from client to backend: %v"
}
if e, ok := err.(*websocket.CloseError); !ok || e.Code == websocket.CloseAbnormalClosure {
f.log.Errorf(message, err)
}
}
// copyWebsocketRequest makes a copy of the specified request.
func (f *httpForwarder) copyWebSocketRequest(req *http.Request) (outReq *http.Request) {
outReq = new(http.Request)
*outReq = *req // includes shallow copies of maps, but we handle this below
outReq.URL = utils.CopyURL(req.URL)
outReq.URL.Scheme = req.URL.Scheme
// sometimes backends might be registered as HTTP/HTTPS servers so translate URLs to websocket URLs.
switch req.URL.Scheme {
case "https":
outReq.URL.Scheme = "wss"
case "http":
outReq.URL.Scheme = "ws"
}
u := f.getURLFromRequest(outReq)
outReq.URL.Path = u.Path
outReq.URL.RawPath = u.RawPath
outReq.URL.RawQuery = u.RawQuery
outReq.RequestURI = "" // Outgoing request should not have RequestURI
outReq.URL.Host = req.URL.Host
if !f.passHost {
outReq.Host = req.URL.Host
}
outReq.Header = make(http.Header)
// gorilla websocket use this header to set the request.Host tested in checkSameOrigin
outReq.Header.Set("Host", outReq.Host)
utils.CopyHeaders(outReq.Header, req.Header)
utils.RemoveHeaders(outReq.Header, WebsocketDialHeaders...)
if f.rewriter != nil {
f.rewriter.Rewrite(outReq)
}
return outReq
}
// serveHTTP forwards HTTP traffic using the configured transport.
func (f *httpForwarder) serveHTTP(w http.ResponseWriter, inReq *http.Request, ctx *handlerContext) {
if f.log.GetLevel() >= log.DebugLevel {
logEntry := f.log.WithField("Request", utils.DumpHTTPRequest(inReq))
logEntry.Debug("vulcand/oxy/forward/http: begin ServeHttp on request")
defer logEntry.Debug("vulcand/oxy/forward/http: completed ServeHttp on request")
}
start := clock.Now().UTC()
outReq := new(http.Request)
*outReq = *inReq // includes shallow copies of maps, but we handle this in Director
revproxy := httputil.ReverseProxy{
Director: func(req *http.Request) {
f.modifyRequest(req, inReq.URL)
},
Transport: f.roundTripper,
FlushInterval: f.flushInterval,
ModifyResponse: f.modifyResponse,
BufferPool: f.bufferPool,
ErrorHandler: ctx.errHandler.ServeHTTP,
}
if f.log.GetLevel() >= log.DebugLevel {
pw := utils.NewProxyWriter(w)
revproxy.ServeHTTP(pw, outReq)
if inReq.TLS != nil {
f.log.Debugf("vulcand/oxy/forward/http: Round trip: %v, code: %v, Length: %v, duration: %v tls:version: %x, tls:resume:%t, tls:csuite:%x, tls:server:%v",
inReq.URL, pw.StatusCode(), pw.GetLength(), clock.Now().UTC().Sub(start),
inReq.TLS.Version,
inReq.TLS.DidResume,
inReq.TLS.CipherSuite,
inReq.TLS.ServerName)
} else {
f.log.Debugf("vulcand/oxy/forward/http: Round trip: %v, code: %v, Length: %v, duration: %v",
inReq.URL, pw.StatusCode(), pw.GetLength(), clock.Now().UTC().Sub(start))
}
} else {
revproxy.ServeHTTP(w, outReq)
}
for key := range w.Header() {
if strings.HasPrefix(key, http.TrailerPrefix) {
if fl, ok := w.(http.Flusher); ok {
fl.Flush()
}
break
}
}
}
// IsWebsocketRequest determines if the specified HTTP request is a websocket handshake request.
func IsWebsocketRequest(req *http.Request) bool {
containsHeader := func(name, value string) bool {
items := strings.Split(req.Header.Get(name), ",")
for _, item := range items {
if value == strings.ToLower(strings.TrimSpace(item)) {
return true
}
}
return false
}
return containsHeader(Connection, "upgrade") && containsHeader(Upgrade, "websocket")
return u
}
-38
View File
@@ -1,38 +0,0 @@
//go:build !go1.16
// +build !go1.16
package forward
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vulcand/oxy/testutils"
)
func TestChunkedResponseConversion(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
h := w.(http.Hijacker)
conn, _, _ := h.Hijack()
fmt.Fprintf(conn, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n5\r\ntest1\r\n5\r\ntest2\r\n0\r\n\r\n")
conn.Close()
})
defer srv.Close()
f, err := New()
require.NoError(t, err)
proxy := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI(srv.URL)
f.ServeHTTP(w, req)
})
defer proxy.Close()
re, body, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, "testtest1test2", string(body))
assert.Equal(t, http.StatusOK, re.StatusCode)
}
-44
View File
@@ -1,44 +0,0 @@
//go:build go1.16
// +build go1.16
package forward
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vulcand/oxy/testutils"
)
func TestChunkedResponseConversion(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
h := w.(http.Hijacker)
conn, _, _ := h.Hijack()
fmt.Fprintf(conn, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n5\r\ntest1\r\n5\r\ntest2\r\n0\r\n\r\n")
conn.Close()
})
defer srv.Close()
f, err := New()
require.NoError(t, err)
proxy := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI(srv.URL)
f.ServeHTTP(w, req)
})
defer proxy.Close()
re, body, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, "testtest1test2", string(body))
assert.Equal(t, http.StatusOK, re.StatusCode)
// Also as per RFC2616 (https://tools.ietf.org/html/rfc2616#section-4.4)
// "Messages MUST NOT include both a Content-Length header field and a non-identity transfer-coding.
// If the message does include a non-identity transfer-coding, the Content-Length MUST be ignored."
_, ok := re.Header["Content-Length"]
assert.False(t, ok)
}
+18 -337
View File
@@ -1,8 +1,6 @@
package forward
import (
"context"
"io"
"net/http"
"net/http/httptest"
"net/url"
@@ -10,105 +8,21 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vulcand/oxy/internal/holsterv4/clock"
"github.com/vulcand/oxy/testutils"
"github.com/vulcand/oxy/utils"
)
// Makes sure hop-by-hop headers are removed.
func TestForwardHopHeaders(t *testing.T) {
called := false
var outHeaders http.Header
var outHost, expectedHost string
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
called = true
outHeaders = req.Header
outHost = req.Host
_, _ = w.Write([]byte("hello"))
})
defer srv.Close()
f, err := New()
require.NoError(t, err)
proxy := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI(srv.URL)
expectedHost = req.URL.Host
f.ServeHTTP(w, req)
})
defer proxy.Close()
headers := http.Header{
Connection: []string{"close"},
KeepAlive: []string{"timeout=600"},
}
re, body, err := testutils.Get(proxy.URL, testutils.Headers(headers))
require.NoError(t, err)
assert.Equal(t, "hello", string(body))
assert.Equal(t, http.StatusOK, re.StatusCode)
assert.Equal(t, true, called)
assert.Equal(t, "", outHeaders.Get(Connection))
assert.Equal(t, "", outHeaders.Get(KeepAlive))
assert.Equal(t, expectedHost, outHost)
}
func TestDefaultErrHandler(t *testing.T) {
f, err := New()
require.NoError(t, err)
f := New(true)
proxy := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI("http://localhost:63450")
f.ServeHTTP(w, req)
})
defer proxy.Close()
re, _, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusBadGateway, re.StatusCode)
}
func TestCustomErrHandler(t *testing.T) {
f, err := New(ErrorHandler(utils.ErrorHandlerFunc(func(w http.ResponseWriter, req *http.Request, err error) {
w.WriteHeader(http.StatusTeapot)
_, _ = w.Write([]byte(http.StatusText(http.StatusTeapot)))
})))
require.NoError(t, err)
proxy := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI("http://localhost:63450")
f.ServeHTTP(w, req)
})
defer proxy.Close()
re, body, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusTeapot, re.StatusCode)
assert.Equal(t, http.StatusText(http.StatusTeapot), string(body))
}
func TestResponseModifier(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
_, _ = w.Write([]byte("hello"))
})
defer srv.Close()
f, err := New(ResponseModifier(func(resp *http.Response) error {
resp.Header.Add("X-Test", "CUSTOM")
return nil
}))
require.NoError(t, err)
proxy := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI(srv.URL)
f.ServeHTTP(w, req)
})
defer proxy.Close()
re, _, err := testutils.Get(proxy.URL)
resp, err := http.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
assert.Equal(t, "CUSTOM", re.Header.Get("X-Test"))
assert.Equal(t, http.StatusBadGateway, resp.StatusCode)
}
func TestXForwardedHostHeader(t *testing.T) {
@@ -140,274 +54,41 @@ func TestXForwardedHostHeader(t *testing.T) {
t.Run(test.Description, func(t *testing.T) {
t.Parallel()
f, err := New(PassHostHeader(test.PassHostHeader))
require.NoError(t, err)
f := New(true)
r, err := http.NewRequest(http.MethodGet, test.TargetURL, nil)
require.NoError(t, err)
backendURL, err := url.Parse(test.ProxyfiedURL)
require.NoError(t, err)
f.modifyRequest(r, backendURL)
r.URL = backendURL
f.Director(r)
require.Equal(t, test.ExpectedXForwardedHost, r.Header.Get(XForwardedHost))
})
}
}
// Makes sure hop-by-hop headers are removed.
func TestForwardedHeaders(t *testing.T) {
var outHeaders http.Header
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
outHeaders = req.Header
_, _ = w.Write([]byte("hello"))
})
defer srv.Close()
f, err := New(Rewriter(&HeaderRewriter{TrustForwardHeader: true, Hostname: "hello"}))
require.NoError(t, err)
proxy := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI(srv.URL)
f.ServeHTTP(w, req)
})
defer proxy.Close()
headers := http.Header{
XForwardedProto: []string{"httpx"},
XForwardedFor: []string{"192.168.1.1"},
XForwardedServer: []string{"foobar"},
XForwardedHost: []string{"upstream-foobar"},
}
re, _, err := testutils.Get(proxy.URL, testutils.Headers(headers))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
assert.Equal(t, "httpx", outHeaders.Get(XForwardedProto))
assert.Contains(t, outHeaders.Get(XForwardedFor), "192.168.1.1")
assert.Contains(t, "upstream-foobar", outHeaders.Get(XForwardedHost))
assert.Equal(t, "hello", outHeaders.Get(XForwardedServer))
}
func TestCustomRewriter(t *testing.T) {
var outHeaders http.Header
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
outHeaders = req.Header
_, _ = w.Write([]byte("hello"))
})
defer srv.Close()
f, err := New(Rewriter(&HeaderRewriter{TrustForwardHeader: false, Hostname: "hello"}))
require.NoError(t, err)
proxy := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI(srv.URL)
f.ServeHTTP(w, req)
})
defer proxy.Close()
headers := http.Header{
XForwardedProto: []string{"httpx"},
XForwardedFor: []string{"192.168.1.1"},
}
re, _, err := testutils.Get(proxy.URL, testutils.Headers(headers))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
assert.Equal(t, "http", outHeaders.Get(XForwardedProto))
assert.NotContains(t, outHeaders.Get(XForwardedFor), "192.168.1.1")
}
func TestCustomTransportTimeout(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
clock.Sleep(20 * clock.Millisecond)
_, _ = w.Write([]byte("hello"))
})
defer srv.Close()
f, err := New(RoundTripper(
&http.Transport{
ResponseHeaderTimeout: 5 * clock.Millisecond,
}))
require.NoError(t, err)
proxy := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI(srv.URL)
f.ServeHTTP(w, req)
})
defer proxy.Close()
re, _, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusGatewayTimeout, re.StatusCode)
}
func TestCustomLogger(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
_, _ = w.Write([]byte("hello"))
})
defer srv.Close()
f, err := New()
require.NoError(t, err)
proxy := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI(srv.URL)
f.ServeHTTP(w, req)
})
defer proxy.Close()
re, _, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
}
func TestRouteForwarding(t *testing.T) {
var outPath string
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
outPath = req.RequestURI
_, _ = w.Write([]byte("hello"))
})
defer srv.Close()
f, err := New()
require.NoError(t, err)
proxy := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI(srv.URL)
f.ServeHTTP(w, req)
})
defer proxy.Close()
tests := []struct {
Path string
Query string
ExpectedPath string
}{
{"/hello", "", "/hello"},
{"//hello", "", "//hello"},
{"///hello", "", "///hello"},
{"/hello", "abc=def&def=123", "/hello?abc=def&def=123"},
{"/log/http%3A%2F%2Fwww.site.com%2Fsomething?a=b", "", "/log/http%3A%2F%2Fwww.site.com%2Fsomething?a=b"},
}
for _, test := range tests {
proxyURL := proxy.URL + test.Path
if test.Query != "" {
proxyURL = proxyURL + "?" + test.Query
}
request, err := http.NewRequest("GET", proxyURL, nil)
require.NoError(t, err)
re, err := http.DefaultClient.Do(request)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
assert.Equal(t, test.ExpectedPath, outPath)
}
}
func TestForwardedProto(t *testing.T) {
var proto string
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
proto = req.Header.Get(XForwardedProto)
_, _ = w.Write([]byte("hello"))
})
}))
defer srv.Close()
f, err := New()
require.NoError(t, err)
f := New(true)
proxy := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
proxy := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI(srv.URL)
f.ServeHTTP(w, req)
})
tproxy := httptest.NewUnstartedServer(proxy)
tproxy.StartTLS()
defer tproxy.Close()
}))
proxy.StartTLS()
defer proxy.Close()
re, _, err := testutils.Get(tproxy.URL)
re, _, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
assert.Equal(t, "https", proto)
}
func TestContextWithValueInErrHandler(t *testing.T) {
originalBool := false
originalPBool := &originalBool
type MyKey string
const key MyKey = "test"
f, err := New(ErrorHandler(utils.ErrorHandlerFunc(func(rw http.ResponseWriter, req *http.Request, err error) {
test, isBool := req.Context().Value(key).(*bool)
if isBool {
*test = true
}
if err != nil {
rw.WriteHeader(http.StatusBadGateway)
}
})))
require.NoError(t, err)
proxy := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
// We need a network error
req.URL = testutils.ParseURI("http://localhost:63450")
newReq := req.WithContext(context.WithValue(req.Context(), key, originalPBool))
f.ServeHTTP(w, newReq)
})
defer proxy.Close()
re, _, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusBadGateway, re.StatusCode)
assert.True(t, *originalPBool)
}
func TestTeTrailer(t *testing.T) {
var teHeader string
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
teHeader = req.Header.Get(Te)
_, _ = w.Write([]byte("hello"))
})
defer srv.Close()
f, err := New()
require.NoError(t, err)
proxy := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI(srv.URL)
f.ServeHTTP(w, req)
})
tproxy := httptest.NewUnstartedServer(proxy)
tproxy.StartTLS()
defer tproxy.Close()
re, _, err := testutils.Get(tproxy.URL, testutils.Header("Te", "trailers"))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
assert.Equal(t, "trailers", teHeader)
}
func TestUnannouncedTrailer(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(200)
rw.(http.Flusher).Flush()
rw.Header().Add(http.TrailerPrefix+"X-Trailer", "foo")
}))
proxy, err := New()
require.NoError(t, err)
proxySrv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI(srv.URL)
proxy.ServeHTTP(rw, req)
}))
resp, _ := http.Get(proxySrv.URL)
_, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, resp.Trailer.Get("X-Trailer"), "foo")
}
+19 -156
View File
@@ -7,7 +7,6 @@ import (
"net"
"net/http"
"net/http/httptest"
"runtime"
"testing"
gorillawebsocket "github.com/gorilla/websocket"
@@ -19,8 +18,7 @@ import (
)
func TestWebSocketTCPClose(t *testing.T) {
f, err := New(PassHostHeader(true))
require.NoError(t, err)
f := New(true)
errChan := make(chan error, 1)
upgrader := gorillawebsocket.Upgrader{}
@@ -57,56 +55,8 @@ func TestWebSocketTCPClose(t *testing.T) {
assert.Equal(t, 1006, wsErr.Code)
}
func TestWebsocketConnectionClosedHook(t *testing.T) {
closed := make(chan struct{})
f, err := New(WebsocketConnectionClosedHook(func(req *http.Request, conn net.Conn) {
close(closed)
}))
require.NoError(t, err)
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
msg := make([]byte, 4)
_, _ = conn.Read(msg)
_, _ = conn.Write(msg)
_ = conn.Close()
}))
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
mux.ServeHTTP(w, req)
})
defer srv.Close()
proxy := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI(srv.URL)
f.ServeHTTP(w, req)
})
defer proxy.Close()
serverAddr := proxy.Listener.Addr().String()
headers := http.Header{}
webSocketURL := "ws://" + serverAddr + "/ws"
headers.Add("Origin", webSocketURL)
conn, resp, err := gorillawebsocket.DefaultDialer.Dial(webSocketURL, headers)
require.NoError(t, err, "Error during Dial with response: %+v", resp)
_ = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
t.Log(conn.ReadMessage())
_ = conn.Close()
select {
case <-clock.After(clock.Second):
t.Errorf("Websocket Hook not called")
case <-closed:
}
}
func TestWebSocketPingPong(t *testing.T) {
f, err := New()
require.NoError(t, err)
f := New(true)
upgrader := gorillawebsocket.Upgrader{
HandshakeTimeout: 10 * clock.Second,
@@ -167,8 +117,7 @@ func TestWebSocketPingPong(t *testing.T) {
}
func TestWebSocketEcho(t *testing.T) {
f, err := New()
require.NoError(t, err)
f := New(true)
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
@@ -224,10 +173,7 @@ func TestWebSocketPassHost(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
f, err := New()
f.passHost = test.passHost
require.NoError(t, err)
f := New(test.passHost)
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
@@ -275,54 +221,8 @@ func TestWebSocketPassHost(t *testing.T) {
}
}
func TestWebSocketNumGoRoutine(t *testing.T) {
t.Skip("Flaky on goroutine")
f, err := New()
require.NoError(t, err)
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
msg := make([]byte, 4)
_, _ = conn.Read(msg)
t.Log(string(msg))
_, _ = conn.Write(msg)
_ = conn.Close()
}))
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
mux.ServeHTTP(w, req)
})
defer srv.Close()
proxy := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.ParseURI(srv.URL)
f.ServeHTTP(w, req)
})
defer proxy.Close()
serverAddr := proxy.Listener.Addr().String()
num := runtime.NumGoroutine()
headers := http.Header{}
webSocketURL := "ws://" + serverAddr + "/ws"
headers.Add("Origin", webSocketURL)
conn, resp, err := gorillawebsocket.DefaultDialer.Dial(webSocketURL, headers)
require.NoError(t, err, "Error during Dial with response: %+v", resp)
_ = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
t.Log(conn.ReadMessage())
_ = conn.Close()
clock.Sleep(clock.Second)
assert.Equal(t, num, runtime.NumGoroutine())
}
func TestWebSocketServerWithoutCheckOrigin(t *testing.T) {
f, err := New()
require.NoError(t, err)
f := New(true)
upgrader := gorillawebsocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
return true
@@ -362,8 +262,7 @@ func TestWebSocketServerWithoutCheckOrigin(t *testing.T) {
}
func TestWebSocketRequestWithOrigin(t *testing.T) {
f, err := New(PassHostHeader(true))
require.NoError(t, err)
f := New(true)
upgrader := gorillawebsocket.Upgrader{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -389,7 +288,7 @@ func TestWebSocketRequestWithOrigin(t *testing.T) {
defer proxy.Close()
proxyAddr := proxy.Listener.Addr().String()
_, err = newWebsocketRequest(
_, err := newWebsocketRequest(
withServer(proxyAddr),
withPath("/ws"),
withData("echo"),
@@ -408,8 +307,7 @@ func TestWebSocketRequestWithOrigin(t *testing.T) {
}
func TestWebSocketRequestWithQueryParams(t *testing.T) {
f, err := New(PassHostHeader(true))
require.NoError(t, err)
f := New(true)
upgrader := gorillawebsocket.Upgrader{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -448,8 +346,7 @@ func TestWebSocketRequestWithQueryParams(t *testing.T) {
}
func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) {
f, err := New()
require.NoError(t, err)
f := New(true)
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
@@ -480,8 +377,7 @@ func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) {
}
func TestWebSocketRequestWithEncodedChar(t *testing.T) {
f, err := New(PassHostHeader(true))
require.NoError(t, err)
f := New(true)
upgrader := gorillawebsocket.Upgrader{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -519,38 +415,12 @@ func TestWebSocketRequestWithEncodedChar(t *testing.T) {
assert.Equal(t, "ok", resp)
}
func TestDetectsWebSocketRequest(t *testing.T) {
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
_, _ = conn.Write([]byte("ok"))
_ = conn.Close()
}))
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
websocketRequest := IsWebsocketRequest(req)
assert.Equal(t, true, websocketRequest)
mux.ServeHTTP(w, req)
})
defer srv.Close()
serverAddr := srv.Listener.Addr().String()
resp, err := newWebsocketRequest(
withServer(serverAddr),
withPath("/ws"),
withData("echo"),
).send()
require.NoError(t, err)
assert.Equal(t, "ok", resp)
}
func TestWebSocketUpgradeFailed(t *testing.T) {
f, err := New()
require.NoError(t, err)
f := New(true)
mux := http.NewServeMux()
mux.HandleFunc("/ws", func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(400)
w.WriteHeader(http.StatusBadRequest)
})
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
mux.ServeHTTP(w, req)
@@ -564,11 +434,7 @@ func TestWebSocketUpgradeFailed(t *testing.T) {
// Set new backend URL
req.URL = testutils.ParseURI(srv.URL)
req.URL.Path = path
websocketRequest := IsWebsocketRequest(req)
assert.Equal(t, true, websocketRequest)
f.ServeHTTP(w, req)
} else {
w.WriteHeader(200)
}
})
defer proxy.Close()
@@ -596,8 +462,7 @@ func TestWebSocketUpgradeFailed(t *testing.T) {
}
func TestForwardsWebsocketTraffic(t *testing.T) {
f, err := New()
require.NoError(t, err)
f := New(true)
mux := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
@@ -645,7 +510,7 @@ func createTLSWebsocketServer() *httptest.Server {
return srv
}
func createProxyWithForwarder(forwarder *Forwarder, uri string) *httptest.Server {
func createProxyWithForwarder(forwarder http.Handler, uri string) *httptest.Server {
return testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path // keep the original path
// Set new backend URL
@@ -660,15 +525,14 @@ func TestWebSocketTransferTLSConfig(t *testing.T) {
srv := createTLSWebsocketServer()
defer srv.Close()
forwarderWithoutTLSConfig, err := New(PassHostHeader(true))
require.NoError(t, err)
forwarderWithoutTLSConfig := New(true)
proxyWithoutTLSConfig := createProxyWithForwarder(forwarderWithoutTLSConfig, srv.URL)
defer proxyWithoutTLSConfig.Close()
proxyAddr := proxyWithoutTLSConfig.Listener.Addr().String()
_, err = newWebsocketRequest(
_, err := newWebsocketRequest(
withServer(proxyAddr),
withPath("/ws"),
withData("ok"),
@@ -679,8 +543,8 @@ func TestWebSocketTransferTLSConfig(t *testing.T) {
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
forwarderWithTLSConfig, err := New(PassHostHeader(true), RoundTripper(transport))
require.NoError(t, err)
forwarderWithTLSConfig := New(true)
forwarderWithTLSConfig.Transport = transport
proxyWithTLSConfig := createProxyWithForwarder(forwarderWithTLSConfig, srv.URL)
defer proxyWithTLSConfig.Close()
@@ -698,8 +562,7 @@ func TestWebSocketTransferTLSConfig(t *testing.T) {
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
forwarderWithTLSConfigFromDefaultTransport, err := New(PassHostHeader(true))
require.NoError(t, err)
forwarderWithTLSConfigFromDefaultTransport := New(true)
proxyWithTLSConfigFromDefaultTransport := createProxyWithForwarder(forwarderWithTLSConfigFromDefaultTransport, srv.URL)
defer proxyWithTLSConfig.Close()
+6 -51
View File
@@ -2,59 +2,14 @@ package forward
// Headers.
const (
XForwardedProto = "X-Forwarded-Proto"
XForwardedFor = "X-Forwarded-For"
XForwardedHost = "X-Forwarded-Host"
XForwardedPort = "X-Forwarded-Port"
XForwardedServer = "X-Forwarded-Server"
XRealIp = "X-Real-Ip"
Connection = "Connection"
KeepAlive = "Keep-Alive"
ProxyAuthenticate = "Proxy-Authenticate"
ProxyAuthorization = "Proxy-Authorization"
Te = "Te" // canonicalized version of "TE"
Trailers = "Trailers"
TransferEncoding = "Transfer-Encoding"
Upgrade = "Upgrade"
ContentLength = "Content-Length"
SecWebsocketKey = "Sec-Websocket-Key"
SecWebsocketVersion = "Sec-Websocket-Version"
SecWebsocketExtensions = "Sec-Websocket-Extensions"
SecWebsocketAccept = "Sec-Websocket-Accept"
XForwardedProto = "X-Forwarded-Proto"
XForwardedFor = "X-Forwarded-For"
XForwardedHost = "X-Forwarded-Host"
XForwardedPort = "X-Forwarded-Port"
XForwardedServer = "X-Forwarded-Server"
XRealIp = "X-Real-Ip"
)
// HopHeaders Hop-by-hop headers. These are removed when sent to the backend.
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
// Copied from reverseproxy.go, too bad.
var HopHeaders = []string{
Connection,
KeepAlive,
ProxyAuthenticate,
ProxyAuthorization,
Te, // canonicalized version of "TE"
Trailers,
TransferEncoding,
Upgrade,
}
// WebsocketDialHeaders Websocket dial headers.
var WebsocketDialHeaders = []string{
Upgrade,
Connection,
SecWebsocketKey,
SecWebsocketVersion,
SecWebsocketExtensions,
SecWebsocketAccept,
}
// WebsocketUpgradeHeaders Websocket upgrade headers.
var WebsocketUpgradeHeaders = []string{
Upgrade,
Connection,
SecWebsocketAccept,
SecWebsocketExtensions,
}
// XHeaders X-* headers.
var XHeaders = []string{
XForwardedProto,
+32
View File
@@ -0,0 +1,32 @@
package forward
import (
"net/http"
"net/url"
)
// Connection states.
const (
StateConnected = iota
StateDisconnected
)
// URLForwardingStateListener URL forwarding state listener.
type URLForwardingStateListener func(*url.URL, int)
// StateListener listens on state change for urls.
type StateListener struct {
next http.Handler
stateListener URLForwardingStateListener
}
// NewStateListener creates a new StateListener middleware.
func NewStateListener(next http.Handler, stateListener URLForwardingStateListener) *StateListener {
return &StateListener{next: next, stateListener: stateListener}
}
func (s *StateListener) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
s.stateListener(req.URL, StateConnected)
s.next.ServeHTTP(rw, req)
s.stateListener(req.URL, StateDisconnected)
}
-6
View File
@@ -1,6 +0,0 @@
//go:build go1.11
// +build go1.11
package forward
func (f *Forwarder) postConfig() {}
-43
View File
@@ -1,43 +0,0 @@
//go:build !go1.11
// +build !go1.11
package forward
import (
"context"
"net/http"
)
type key string
const (
teHeader key = "TeHeader"
)
type TeTrailerRoundTripper struct {
http.RoundTripper
}
func (t *TeTrailerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
teHeader := req.Context().Value(teHeader)
if teHeader != nil {
req.Header.Set("Te", teHeader.(string))
}
return t.RoundTripper.RoundTrip(req)
}
type TeTrailerRewriter struct {
ReqRewriter
}
func (t *TeTrailerRewriter) Rewrite(req *http.Request) {
if req.Header.Get("Te") == "trailers" {
*req = *req.WithContext(context.WithValue(req.Context(), teHeader, req.Header.Get("Te")))
}
t.ReqRewriter.Rewrite(req)
}
func (f *Forwarder) postConfig() {
f.roundTripper = &TeTrailerRoundTripper{RoundTripper: f.roundTripper}
f.rewriter = &TeTrailerRewriter{ReqRewriter: f.rewriter}
}
+11 -17
View File
@@ -3,11 +3,21 @@ package forward
import (
"net"
"net/http"
"os"
"strings"
"github.com/vulcand/oxy/utils"
)
// NewHeaderRewriter creates a new HeaderRewriter middleware.
func NewHeaderRewriter() *HeaderRewriter {
h, err := os.Hostname()
if err != nil {
h = "localhost"
}
return &HeaderRewriter{TrustForwardHeader: true, Hostname: h}
}
// HeaderRewriter is responsible for removing hop-by-hop headers and setting forwarding headers.
type HeaderRewriter struct {
TrustForwardHeader bool
@@ -19,7 +29,7 @@ func ipv6fix(clientIP string) string {
return strings.Split(clientIP, "%")[0]
}
// Rewrite rewrite request headers.
// Rewrite request headers.
func (rw *HeaderRewriter) Rewrite(req *http.Request) {
if !rw.TrustForwardHeader {
utils.RemoveHeaders(req.Header, XHeaders...)
@@ -27,14 +37,6 @@ func (rw *HeaderRewriter) Rewrite(req *http.Request) {
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
clientIP = ipv6fix(clientIP)
// If not websocket, done in http.ReverseProxy
if IsWebsocketRequest(req) {
if prior, ok := req.Header[XForwardedFor]; ok {
req.Header.Set(XForwardedFor, strings.Join(prior, ", ")+", "+clientIP)
} else {
req.Header.Set(XForwardedFor, clientIP)
}
}
if req.Header.Get(XRealIp) == "" {
req.Header.Set(XRealIp, clientIP)
@@ -50,14 +52,6 @@ func (rw *HeaderRewriter) Rewrite(req *http.Request) {
}
}
if IsWebsocketRequest(req) {
if req.Header.Get(XForwardedProto) == "https" {
req.Header.Set(XForwardedProto, "wss")
} else {
req.Header.Set(XForwardedProto, "ws")
}
}
if xfPort := req.Header.Get(XForwardedPort); xfPort == "" {
req.Header.Set(XForwardedPort, forwardedPort(req))
}
+10 -20
View File
@@ -19,8 +19,7 @@ func TestRebalancerNormalOperation(t *testing.T) {
defer a.Close()
defer b.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
@@ -40,8 +39,7 @@ func TestRebalancerNormalOperation(t *testing.T) {
}
func TestRebalancerNoServers(t *testing.T) {
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
@@ -62,8 +60,7 @@ func TestRebalancerRemoveServer(t *testing.T) {
defer a.Close()
defer b.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
@@ -90,8 +87,7 @@ func TestRebalancerRecovery(t *testing.T) {
defer a.Close()
defer b.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
@@ -156,8 +152,7 @@ func TestRebalancerCascading(t *testing.T) {
defer b.Close()
defer d.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
@@ -223,8 +218,7 @@ func TestRebalancerAllBad(t *testing.T) {
defer b.Close()
defer d.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
@@ -274,8 +268,7 @@ func TestRebalancerReset(t *testing.T) {
defer b.Close()
defer d.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
@@ -330,8 +323,7 @@ func TestRebalancerRequestRewriteListenerLive(t *testing.T) {
defer a.Close()
defer b.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
@@ -371,8 +363,7 @@ func TestRebalancerRequestRewriteListener(t *testing.T) {
defer a.Close()
defer b.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
@@ -394,8 +385,7 @@ func TestRebalancerStickySession(t *testing.T) {
sticky := NewStickySession("test")
require.NotNil(t, sticky)
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
+9 -18
View File
@@ -13,8 +13,7 @@ import (
)
func TestNoServers(t *testing.T) {
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
@@ -40,8 +39,7 @@ func TestCustomErrHandler(t *testing.T) {
_, _ = w.Write([]byte(http.StatusText(http.StatusTeapot)))
})
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd, ErrorHandler(errHandler))
require.NoError(t, err)
@@ -58,8 +56,7 @@ func TestOneServer(t *testing.T) {
a := testutils.NewResponder("a")
defer a.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
@@ -79,8 +76,7 @@ func TestSimple(t *testing.T) {
b := testutils.NewResponder("b")
defer b.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
@@ -101,8 +97,7 @@ func TestRemoveServer(t *testing.T) {
b := testutils.NewResponder("b")
defer b.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
@@ -125,8 +120,7 @@ func TestUpsertSame(t *testing.T) {
a := testutils.NewResponder("a")
defer a.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
@@ -147,8 +141,7 @@ func TestUpsertWeight(t *testing.T) {
b := testutils.NewResponder("b")
defer b.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
@@ -179,8 +172,7 @@ func TestWeighted(t *testing.T) {
z := testutils.NewResponder("z")
defer z.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd)
require.NoError(t, err)
@@ -218,8 +210,7 @@ func TestRequestRewriteListener(t *testing.T) {
b := testutils.NewResponder("b")
defer b.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
lb, err := New(fwd,
RoundRobinRequestRewriteListener(func(oldReq *http.Request, newReq *http.Request) {}))
+8 -16
View File
@@ -23,8 +23,7 @@ func TestBasic(t *testing.T) {
defer a.Close()
defer b.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
sticky := NewStickySession("test")
require.NotNil(t, sticky)
@@ -65,8 +64,7 @@ func TestBasicWithHashValue(t *testing.T) {
defer a.Close()
defer b.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
sticky := NewStickySession("test")
require.NotNil(t, sticky)
@@ -119,8 +117,7 @@ func TestBasicWithAESValue(t *testing.T) {
defer a.Close()
defer b.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
sticky := NewStickySession("test")
require.NotNil(t, sticky)
@@ -175,8 +172,7 @@ func TestStickyCookie(t *testing.T) {
defer a.Close()
defer b.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
sticky := NewStickySession("test")
require.NotNil(t, sticky)
@@ -327,8 +323,7 @@ func TestStickyCookieWithOptions(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
sticky := NewStickySessionWithOptions(test.name, test.options)
require.NotNil(t, sticky)
@@ -360,8 +355,7 @@ func TestRemoveRespondingServer(t *testing.T) {
defer a.Close()
defer b.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
sticky := NewStickySession("test")
require.NotNil(t, sticky)
@@ -430,8 +424,7 @@ func TestRemoveAllServers(t *testing.T) {
defer a.Close()
defer b.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
sticky := NewStickySession("test")
require.NotNil(t, sticky)
@@ -483,8 +476,7 @@ func TestBadCookieVal(t *testing.T) {
defer a.Close()
fwd, err := forward.New()
require.NoError(t, err)
fwd := forward.New(false)
sticky := NewStickySession("test")
require.NotNil(t, sticky)
+9 -18
View File
@@ -35,8 +35,7 @@ func TestSimple(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New(forward.Stream(true))
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -83,8 +82,7 @@ func TestChunkedEncodingSuccess(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New(forward.Stream(true))
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -127,8 +125,7 @@ func TestRequestLimitReached(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New(forward.Stream(true))
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -155,8 +152,7 @@ func TestResponseLimitReached(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New(forward.Stream(true))
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -183,8 +179,7 @@ func TestFileStreamingResponse(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New(forward.Stream(true))
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -212,8 +207,7 @@ func TestCustomErrorHandler(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New(forward.Stream(true))
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -239,8 +233,7 @@ func TestNotModified(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New(forward.Stream(true))
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -267,8 +260,7 @@ func TestNoBody(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New(forward.Stream(true))
require.NoError(t, err)
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -297,8 +289,7 @@ func TestPreservesTLS(t *testing.T) {
defer srv.Close()
// forwarder will proxy the request to whatever destination
fwd, err := forward.New(forward.Stream(true))
require.NoError(t, err)
fwd := forward.New(false)
var cs *tls.ConnectionState
// this is our redirect to server