grafana/pkg/util/proxyutil/reverse_proxy.go

172 lines
4.5 KiB
Go

package proxyutil
import (
"context"
"errors"
"log"
"net/http"
"net/http/httputil"
"strings"
"time"
glog "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/contexthandler"
)
// StatusClientClosedRequest A non-standard status code introduced by nginx
// for the case when a client closes the connection while nginx is processing
// the request.
// https://httpstatus.in/499/
const StatusClientClosedRequest = 499
// ReverseProxyOption reverse proxy option to configure a httputil.ReverseProxy.
type ReverseProxyOption func(*httputil.ReverseProxy)
// NewReverseProxy creates a new httputil.ReverseProxy with sane default configuration.
func NewReverseProxy(logger glog.Logger, director func(*http.Request), opts ...ReverseProxyOption) *httputil.ReverseProxy {
if logger == nil {
panic("logger cannot be nil")
}
if director == nil {
panic("director cannot be nil")
}
p := &httputil.ReverseProxy{
FlushInterval: time.Millisecond * 200,
ErrorHandler: errorHandler(logger),
ErrorLog: log.New(&logWrapper{logger: logger}, "", 0),
Director: director,
}
for _, opt := range opts {
opt(p)
}
origDirector := p.Director
p.Director = wrapDirector(origDirector)
if p.ModifyResponse == nil {
// nolint:bodyclose
p.ModifyResponse = modifyResponse(logger)
} else {
modResponse := p.ModifyResponse
p.ModifyResponse = func(resp *http.Response) error {
if err := modResponse(resp); err != nil {
return err
}
// nolint:bodyclose
return modifyResponse(logger)(resp)
}
}
return p
}
// wrapDirector wraps a director and adds additional functionality.
func wrapDirector(d func(*http.Request)) func(req *http.Request) {
return func(req *http.Request) {
list := contexthandler.AuthHTTPHeaderListFromContext(req.Context())
if list != nil {
for _, name := range list.Items {
req.Header.Del(name)
}
}
d(req)
PrepareProxyRequest(req)
}
}
// deletedHeaders lists a number of headers that we don't want to
// pass-through from the upstream when using a reverse proxy.
//
// These are related to the connection between Grafana and the proxy
// or instructions that would alter how a browser will interact with
// future requests to Grafana (such as enabling Strict Transport
// Security)
var deletedHeaders = []string{
"Alt-Svc",
"Close",
"Server",
"Set-Cookie",
"Strict-Transport-Security",
}
// modifyResponse enforces certain constraints on http.Response.
func modifyResponse(logger glog.Logger) func(resp *http.Response) error {
return func(resp *http.Response) error {
for _, header := range deletedHeaders {
resp.Header.Del(header)
}
SetProxyResponseHeaders(resp.Header)
SetViaHeader(resp.Header, resp.ProtoMajor, resp.ProtoMinor)
return nil
}
}
type timeoutError interface {
error
Timeout() bool
}
// errorHandler handles any errors happening while proxying a request and enforces
// certain HTTP status based on the kind of error.
// If client cancel/close the request we return 499 StatusClientClosedRequest.
// If timeout happens while communicating with upstream server we return http.StatusGatewayTimeout.
// If any other error we return http.StatusBadGateway.
func errorHandler(logger glog.Logger) func(http.ResponseWriter, *http.Request, error) {
return func(w http.ResponseWriter, r *http.Request, err error) {
ctxLogger := logger.FromContext(r.Context())
if errors.Is(err, context.Canceled) {
ctxLogger.Debug("Proxy request cancelled by client")
w.WriteHeader(StatusClientClosedRequest)
return
}
// nolint:errorlint
if timeoutErr, ok := err.(timeoutError); ok && timeoutErr.Timeout() {
ctxLogger.Error("Proxy request timed out", "err", err)
w.WriteHeader(http.StatusGatewayTimeout)
return
}
ctxLogger.Error("Proxy request failed", "err", err)
w.WriteHeader(http.StatusBadGateway)
}
}
type logWrapper struct {
logger glog.Logger
}
// Write writes log messages as bytes from proxy.
func (lw *logWrapper) Write(p []byte) (n int, err error) {
withoutNewline := strings.TrimSuffix(string(p), "\n")
lw.logger.Error("Proxy request error", "error", withoutNewline)
return len(p), nil
}
func WithTransport(transport http.RoundTripper) ReverseProxyOption {
if transport == nil {
panic("transport cannot be nil")
}
return ReverseProxyOption(func(rp *httputil.ReverseProxy) {
rp.Transport = transport
})
}
func WithModifyResponse(fn func(*http.Response) error) ReverseProxyOption {
if fn == nil {
panic("fn cannot be nil")
}
return ReverseProxyOption(func(rp *httputil.ReverseProxy) {
rp.ModifyResponse = fn
})
}