mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-03 07:40:28 +08:00
When creating a reverse proxy the ErrorLog was being created with the writer returned from logrus.WriterLevel. This writer uses an io.Pipe and a goroutine to get any log messages written from a standard log.Logger to our custom formated logrus.Entry. The ErrorLog only ever omits output in a few very rare (hopefully) error cases, which means we were creating an extra goroutine per reverse proxy which in most cases would never be used to produce any added output. Additionally, now that we are starting to support log/slog, any output from a log.Logger will now be formatted via our registered default slog handler. Meaning that we can still get the benefits of producing output from the ErrorLog in the desired format without the penalty incurred by using logrus.WriterLevel. Partially addresses #36541.
236 lines
6.3 KiB
Go
236 lines
6.3 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)
|
|
}
|
|
|
|
// 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 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
|
|
}
|