Files
teleport/lib/httplib/reverseproxy/reverse_proxy.go
T
Tiago Silva ebecb59468 Replace graviational/oxy.Forwarder with httputil.ReverseProxy (#27761)
This PR replaces the usage of `gravitational/oxy.Forwarder` in our codebase with `httputil.ReverseProxy`.

`httputil.ReverseProxy` is part of Go's standard library and thus it is
 maintained by the Go team. Converting everything to use the std lib
 allows us to drop our `oxy` fork.

 There are some operational changes between the two versions:

 1. `httputil.ReverseProxy` handles WebSockets upgrades internally and
    reuses the `Transport` provided for the initial request. Thus, it does
    not require any special dialer or rewriter mechanisms for WebSockets only.

 2. `httputil.ReverseProxy` calls the `ErrorHandler` with the request
       after it was rewritten and after roundtrip was called. This was a
       problem because our `web.transport` deletes Teleport associated
       cookies and when the request failed, the request received by the
       `ErrorHandler` missed the cookies to generate a new session. This was
       fixed by cloning the request at `web.transport` round tripper.

 3. `httputil.ReverseProxy` uses std's `log.Logger`. `logrus` exposes a
       method `Writer` that allows it to be used with standard lib logger.

The API is similar to what our `oxy` fork has available.

Fixes #27412
2023-06-29 13:50:07 +00:00

242 lines
6.5 KiB
Go

/*
Copyright 2023 Gravitational, Inc.
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
http://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 reverseproxy
import (
"io"
"log"
"net/http"
"net/http/httputil"
"net/url"
"time"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/utils"
)
// X-* Header names.
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"
)
// XHeaders X-* headers.
var XHeaders = []string{
XForwardedProto,
XForwardedFor,
XForwardedHost,
XForwardedPort,
XForwardedServer,
XRealIP,
}
const (
// ContentLength is the Content-Length header.
ContentLength = "Content-Length"
)
// Forwarder is a reverse proxy that forwards http requests to another server.
type Forwarder struct {
passHostHeader bool
headerRewriter Rewriter
*httputil.ReverseProxy
log utils.FieldLoggerWithWriter
transport http.RoundTripper
}
// New returns a new reverse proxy that forwards to the given url.
// If passHostHeader is true, the Host header will be copied from the
// request to the forwarded request. Otherwise, the Host header will be
// set to the host portion of the url.
func New(opts ...Option) (*Forwarder, error) {
fwd := &Forwarder{
headerRewriter: NewHeaderRewriter(),
ReverseProxy: &httputil.ReverseProxy{
ErrorHandler: DefaultHandler.ServeHTTP,
},
log: utils.NewLogger(),
}
// Apply options.
for _, opt := range opts {
opt(fwd)
}
// Director is called by the ReverseProxy to modify the request.
fwd.Director = func(request *http.Request) {
modifyRequest(request)
if fwd.headerRewriter != nil {
fwd.headerRewriter.Rewrite(request)
}
if !fwd.passHostHeader {
request.Host = request.URL.Host
}
}
if fwd.transport == nil {
tr, err := defaults.Transport()
if err != nil {
return nil, trace.Wrap(err)
}
fwd.transport = tr
}
// Set the transport for the reverse proxy to use a round tripper
// that logs the request and response.
fwd.ReverseProxy.Transport = &roundTripperWithLogger{transport: fwd.transport, log: fwd.log}
return fwd, nil
}
// Option is a functional option for the forwarder.
type Option func(*Forwarder)
// WithFlushInterval sets the flush interval for the forwarder.
func WithFlushInterval(interval time.Duration) Option {
return func(rp *Forwarder) {
rp.FlushInterval = interval
}
}
// WithLogger sets the logger for the forwarder. It uses the logger.Writer()
// method to get the io.Writer to use for the stdlib logger.
func WithLogger(logger utils.FieldLoggerWithWriter) Option {
return func(rp *Forwarder) {
rp.log = logger
// TODO: this is a hack to get the stdlib logger to work with the
// logrus logger.
if rp.ReverseProxy.ErrorLog == nil {
rp.ReverseProxy.ErrorLog = log.New(logger.WriterLevel(logrus.DebugLevel), "", log.LstdFlags)
}
}
}
// WithLogWriter sets the writer for the stdlib logger to use a different
// io.Writer than the default one created when using WithLogger.
func WithLogWriter(w *io.PipeWriter) Option {
return func(rp *Forwarder) {
rp.ReverseProxy.ErrorLog = log.New(w, "", log.LstdFlags)
}
}
// WithRoundTripper sets the round tripper for the forwarder.
func WithRoundTripper(transport http.RoundTripper) Option {
return func(rp *Forwarder) {
rp.transport = transport
}
}
// WithErrorHandler sets the error handler for the forwarder.
func WithErrorHandler(e ErrorHandlerFunc) Option {
return func(rp *Forwarder) {
rp.ErrorHandler = e
}
}
// WithRewriter sets the header rewriter for the forwarder.
func WithRewriter(h Rewriter) Option {
return func(rp *Forwarder) {
rp.headerRewriter = h
}
}
// WithPassHostHeader sets whether the Host header should be passed to the
// forwarded request.
func WithPassHostHeader() Option {
return func(rp *Forwarder) {
rp.passHostHeader = true
}
}
// Modify the request to handle the target URL.
func modifyRequest(outReq *http.Request) {
u := 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.Proto = "HTTP/1.1"
outReq.ProtoMajor = 1
outReq.ProtoMinor = 1
}
// getURLFromRequest returns the URL from the request object. If the request
// RequestURI is non-empty and parsable, it will be used. Otherwise, the URL
// will be used.
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
}
}
return u
}
type roundTripperWithLogger struct {
log logrus.FieldLogger
transport http.RoundTripper
}
// CloseIdleConnections ensures idle connections of the wrapped
// [http.RoundTripper] are closed.
func (r *roundTripperWithLogger) CloseIdleConnections() {
type closeIdler interface {
CloseIdleConnections()
}
if tr, ok := r.transport.(closeIdler); ok {
tr.CloseIdleConnections()
}
}
// RoundTrip forwards the request on to the provided http.RoundTripper and logs
// the request and response.
func (r *roundTripperWithLogger) RoundTrip(req *http.Request) (*http.Response, error) {
start := time.Now()
rsp, err := r.transport.RoundTrip(req)
if err != nil {
r.log.Errorf("Error forwarding to %v, err: %v", req.URL, err)
return rsp, err
}
if req.TLS != nil {
r.log.Infof("Round trip: %v %v, code: %v, duration: %v tls:version: %x, tls:resume:%t, tls:csuite:%x, tls:server:%v",
req.Method, req.URL, rsp.StatusCode, time.Now().UTC().Sub(start),
req.TLS.Version,
req.TLS.DidResume,
req.TLS.CipherSuite,
req.TLS.ServerName)
} else {
r.log.Infof("Round trip: %v %v, code: %v, duration: %v",
req.Method, req.URL, rsp.StatusCode, time.Now().UTC().Sub(start))
}
return rsp, nil
}