Files
teleport/lib/httplib/reverseproxy/reverse_proxy.go
T
Andrew LeFevreandAndrew LeFevre b97c865995 use httputil Rewrite instead of Director (#42800)
* use httputil Rewrite instead of Director

Director will remove HTTP headers if they are
deemed to be hop-by-hop headers which could result
in a security vulnerability. Use httputil Rewrite
instead to ensure that HTTP headers added by our
reverse proxy are not unintentionally removed.

* ensure removed forwarded headers are added back

* use a separate request object in TestHeaderRewriter

---------

Co-authored-by: Andrew LeFevre <Andrew LeFevre>
2024-06-21 16:42:57 +00:00

236 lines
6.4 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package reverseproxy
import (
"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 logrus.FieldLogger
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)
}
// Rewrite is called by the ReverseProxy to modify the request.
fwd.Rewrite = func(request *httputil.ProxyRequest) {
modifyRequest(request.Out)
if fwd.headerRewriter != nil {
fwd.headerRewriter.Rewrite(request)
}
if !fwd.passHostHeader {
request.Out.Host = request.Out.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 logrus.FieldLogger) Option {
return func(rp *Forwarder) {
rp.log = logger
}
}
// 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
}
}
// WithResponseModifier sets the response modifier for the forwarder.
func WithResponseModifier(m func(*http.Response) error) Option {
return func(rp *Forwarder) {
rp.ModifyResponse = m
}
}
// 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
}