mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-01 13:39:37 +08:00
The `http.Client.Transport` created in `newWebClient` wraps a chain of `http.RoundTripper` over an underlying `http.Transport`. Since we cannot guarantee that each `http.RoundTipper` has a `CloseIdleConnections` method the usage of `defer clt.CloseIdleConnections()` does not guarantee that the http connections created during Find/Ping/etc are closed. To prevent leaking connections implementations of `http.RoundTripper` have added a `CloseIdleConnections` method added that forwards the request on to the wrapped interface. The `otelhttp.Transport` does not implement this method either so care has been taken to wrap it in a `http.RoundTripper` which will call the root transport in `enforceCloseIdleConnections`. An upstream issue has been filed with otelhttp: https://github.com/open-telemetry/opentelemetry-go-contrib/issues/3543 to get the method added to their `Transport` implementation. This wasn't noticed prior to upgrading to go1.20 becuase prior to [this](https://github.com/golang/go/commit/4e7e7ae1406c70d9cc0809ec11105a55a60a0b70) commit the `ReadHeaderTimeout` set on the Proxy web api http.Server would cause the connection to appear idle and get terminated by the server. `TestWebClientClosesIdleConnections` was added to capture the leak in connections as reported in #22757 and prevent any regressions.
298 lines
9.7 KiB
Go
298 lines
9.7 KiB
Go
/*
|
|
Copyright 2015 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 httplib implements common utility functions for writing
|
|
// classic HTTP handlers
|
|
package httplib
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"mime"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
|
|
"github.com/gravitational/roundtrip"
|
|
"github.com/gravitational/trace"
|
|
"github.com/julienschmidt/httprouter"
|
|
log "github.com/sirupsen/logrus"
|
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/api/observability/tracing"
|
|
tracehttp "github.com/gravitational/teleport/api/observability/tracing/http"
|
|
"github.com/gravitational/teleport/lib/httplib/csrf"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
// timeoutMessage is a generic "timeout" error message that is displayed as a more user-friendly alternative to
|
|
// the timeout errors returned by net/http
|
|
const timeoutMessage = "unable to complete the request due to a timeout, please try again in a few minutes"
|
|
|
|
// HandlerFunc specifies HTTP handler function that returns error
|
|
type HandlerFunc func(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error)
|
|
|
|
// StdHandlerFunc specifies HTTP handler function that returns error
|
|
type StdHandlerFunc func(w http.ResponseWriter, r *http.Request) (interface{}, error)
|
|
|
|
// ErrorWriter is a function responsible for writing the error into response
|
|
// body.
|
|
type ErrorWriter func(w http.ResponseWriter, err error)
|
|
|
|
// MakeHandler returns a new httprouter.Handle func from a handler func
|
|
func MakeHandler(fn HandlerFunc) httprouter.Handle {
|
|
return MakeHandlerWithErrorWriter(fn, trace.WriteError)
|
|
}
|
|
|
|
// MakeSecurityHeaderHandler returns a new httprouter.Handle func that wraps the provided handler func
|
|
// with one that will ensure the headers from SetDefaultSecurityHeaders are applied.
|
|
func MakeSecurityHeaderHandler(h http.Handler) http.Handler {
|
|
handler := func(w http.ResponseWriter, r *http.Request) {
|
|
SetDefaultSecurityHeaders(w.Header())
|
|
|
|
h.ServeHTTP(w, r)
|
|
}
|
|
return http.HandlerFunc(handler)
|
|
}
|
|
|
|
// MakeTracingHandler returns a new httprouter.Handle func that wraps the provided handler func
|
|
// with one that will add a tracing span for each request.
|
|
func MakeTracingHandler(h http.Handler, component string) http.Handler {
|
|
// Wrap the provided handler with one that will inject
|
|
// any propagated tracing context provided via a query parameter
|
|
// if there isn't already a header containing tracing context.
|
|
// This is required for scenarios using web sockets as headers
|
|
// cannot be modified to inject the tracing context.
|
|
handler := func(w http.ResponseWriter, r *http.Request) {
|
|
// ensure headers have priority over query parameters
|
|
if r.Header.Get(tracing.TraceParent) != "" {
|
|
h.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
traceParent := r.URL.Query()[tracing.TraceParent]
|
|
if len(traceParent) > 0 {
|
|
r.Header.Add(tracing.TraceParent, traceParent[0])
|
|
}
|
|
|
|
h.ServeHTTP(w, r)
|
|
}
|
|
|
|
return otelhttp.NewHandler(http.HandlerFunc(handler), component, otelhttp.WithSpanNameFormatter(tracehttp.HandlerFormatter))
|
|
}
|
|
|
|
// MakeHandlerWithErrorWriter returns a httprouter.Handle from the HandlerFunc,
|
|
// and sends all errors to ErrorWriter.
|
|
func MakeHandlerWithErrorWriter(fn HandlerFunc, errWriter ErrorWriter) httprouter.Handle {
|
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
// ensure that neither proxies nor browsers cache http traffic
|
|
SetNoCacheHeaders(w.Header())
|
|
// ensure that default security headers are set
|
|
SetDefaultSecurityHeaders(w.Header())
|
|
|
|
out, err := fn(w, r, p)
|
|
if err != nil {
|
|
errWriter(w, err)
|
|
return
|
|
}
|
|
if out != nil {
|
|
roundtrip.ReplyJSON(w, http.StatusOK, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MakeStdHandlerWithErrorWriter returns a http.HandlerFunc from the
|
|
// StdHandlerFunc, and sends all errors to ErrorWriter.
|
|
func MakeStdHandlerWithErrorWriter(fn StdHandlerFunc, errWriter ErrorWriter) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// ensure that neither proxies nor browsers cache http traffic
|
|
SetNoCacheHeaders(w.Header())
|
|
// ensure that default security headers are set
|
|
SetDefaultSecurityHeaders(w.Header())
|
|
|
|
out, err := fn(w, r)
|
|
if err != nil {
|
|
errWriter(w, err)
|
|
return
|
|
}
|
|
if out != nil {
|
|
roundtrip.ReplyJSON(w, http.StatusOK, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithCSRFProtection ensures that request to unauthenticated API is checked against CSRF attacks
|
|
func WithCSRFProtection(fn HandlerFunc) httprouter.Handle {
|
|
hanlderFn := MakeHandler(fn)
|
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
err := csrf.VerifyHTTPHeader(r)
|
|
if err != nil {
|
|
log.Warningf("unable to validate CSRF token %v", err)
|
|
trace.WriteError(w, trace.AccessDenied("access denied"))
|
|
return
|
|
}
|
|
hanlderFn(w, r, p)
|
|
}
|
|
}
|
|
|
|
// ReadJSON reads HTTP json request and unmarshals it
|
|
// into passed interface{} obj
|
|
func ReadJSON(r *http.Request, val interface{}) error {
|
|
// Check content type to mitigate CSRF attack.
|
|
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
|
if err != nil {
|
|
log.Warningf("Error parsing media type for reading JSON: %v", err)
|
|
return trace.BadParameter("invalid request")
|
|
}
|
|
|
|
if contentType != "application/json" {
|
|
log.Warningf("Invalid HTTP request header content-type %q for reading JSON", contentType)
|
|
return trace.BadParameter("invalid request")
|
|
}
|
|
|
|
data, err := utils.ReadAtMost(r.Body, teleport.MaxHTTPRequestSize)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if err := json.Unmarshal(data, &val); err != nil {
|
|
return trace.BadParameter("request: %v", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ConvertResponse converts http error to internal error type
|
|
// based on HTTP response code and HTTP body contents
|
|
func ConvertResponse(re *roundtrip.Response, err error) (*roundtrip.Response, error) {
|
|
if err != nil {
|
|
var uErr *url.Error
|
|
if errors.As(err, &uErr) && uErr.Err != nil {
|
|
return nil, trace.ConnectionProblem(uErr.Err, "")
|
|
}
|
|
var nErr net.Error
|
|
if errors.As(err, &nErr) && nErr.Timeout() {
|
|
// Using `ConnectionProblem` instead of `LimitExceeded` allows us to preserve the original error
|
|
// while adding a more user-friendly message.
|
|
return nil, trace.ConnectionProblem(err, timeoutMessage)
|
|
}
|
|
return nil, trace.ConvertSystemError(err)
|
|
}
|
|
return re, trace.ReadError(re.Code(), re.Bytes())
|
|
}
|
|
|
|
// ParseBool will parse boolean variable from url query
|
|
// returns value, ok, error
|
|
func ParseBool(q url.Values, name string) (bool, bool, error) {
|
|
stringVal := q.Get(name)
|
|
if stringVal == "" {
|
|
return false, false, nil
|
|
}
|
|
|
|
val, err := strconv.ParseBool(stringVal)
|
|
if err != nil {
|
|
return false, false, trace.BadParameter(
|
|
"'%v': expected 'true' or 'false', got %v", name, stringVal)
|
|
}
|
|
return val, true, nil
|
|
}
|
|
|
|
// RewritePair is a rewrite expression
|
|
type RewritePair struct {
|
|
// Expr is matching expression
|
|
Expr *regexp.Regexp
|
|
// Replacement is replacement
|
|
Replacement string
|
|
}
|
|
|
|
// Rewrite creates a rewrite pair, panics if in epxression
|
|
// is not a valid regular expressoin
|
|
func Rewrite(in, out string) RewritePair {
|
|
return RewritePair{
|
|
Expr: regexp.MustCompile(in),
|
|
Replacement: out,
|
|
}
|
|
}
|
|
|
|
// RewritePaths creates a middleware that rewrites paths in incoming request
|
|
func RewritePaths(next http.Handler, rewrites ...RewritePair) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
for _, rewrite := range rewrites {
|
|
req.URL.Path = rewrite.Expr.ReplaceAllString(req.URL.Path, rewrite.Replacement)
|
|
}
|
|
next.ServeHTTP(w, req)
|
|
})
|
|
}
|
|
|
|
// SafeRedirect performs a relative redirect to the URI part of the provided redirect URL
|
|
func SafeRedirect(w http.ResponseWriter, r *http.Request, redirectURL string) error {
|
|
parsedURL, err := url.Parse(redirectURL)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
http.Redirect(w, r, parsedURL.RequestURI(), http.StatusFound)
|
|
return nil
|
|
}
|
|
|
|
// ResponseStatusRecorder is an http.ResponseWriter that records the response status code.
|
|
type ResponseStatusRecorder struct {
|
|
http.ResponseWriter
|
|
flusher http.Flusher
|
|
status int
|
|
}
|
|
|
|
// NewResponseStatusRecorder makes and returns a ResponseStatusRecorder.
|
|
func NewResponseStatusRecorder(w http.ResponseWriter) *ResponseStatusRecorder {
|
|
rec := &ResponseStatusRecorder{ResponseWriter: w}
|
|
if flusher, ok := w.(http.Flusher); ok {
|
|
rec.flusher = flusher
|
|
}
|
|
return rec
|
|
}
|
|
|
|
// WriteHeader sends an HTTP response header with the provided
|
|
// status code and save the status code in the recorder.
|
|
func (r *ResponseStatusRecorder) WriteHeader(status int) {
|
|
r.status = status
|
|
r.ResponseWriter.WriteHeader(status)
|
|
}
|
|
|
|
// Flush optionally flushes the inner ResponseWriter if it supports that.
|
|
// Otherwise, Flush is a noop.
|
|
//
|
|
// Flush is optionally used by github.com/gravitational/oxy/forward to flush
|
|
// pending data on streaming HTTP responses (like streaming pod logs).
|
|
//
|
|
// Without this, oxy/forward will handle streaming responses by accumulating
|
|
// ~32kb of response in a buffer before flushing it.
|
|
func (r *ResponseStatusRecorder) Flush() {
|
|
if r.flusher != nil {
|
|
r.flusher.Flush()
|
|
}
|
|
}
|
|
|
|
// Status returns the recorded status after WriteHeader is called, or StatusOK if WriteHeader hasn't been called
|
|
// explicitly.
|
|
func (r *ResponseStatusRecorder) Status() int {
|
|
// http.ResponseWriter implicitly sets StatusOK, if WriteHeader hasn't been
|
|
// explicitly called.
|
|
if r.status == 0 {
|
|
return http.StatusOK
|
|
}
|
|
return r.status
|
|
}
|