// Copyright 2011 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.// HTTP reverse proxy handlerpackage httputilimport ()// A ProxyRequest contains a request to be rewritten by a [ReverseProxy].typeProxyRequeststruct {// In is the request received by the proxy. // The Rewrite function must not modify In. In *http.Request// Out is the request which will be sent by the proxy. // The Rewrite function may modify or replace this request. // Hop-by-hop headers are removed from this request // before Rewrite is called. Out *http.Request}// SetURL routes the outbound request to the scheme, host, and base path// provided in target. If the target's path is "/base" and the incoming// request was for "/dir", the target request will be for "/base/dir".//// SetURL rewrites the outbound Host header to match the target's host.// To preserve the inbound request's Host header (the default behavior// of [NewSingleHostReverseProxy])://// rewriteFunc := func(r *httputil.ProxyRequest) {// r.SetURL(url)// r.Out.Host = r.In.Host// }func ( *ProxyRequest) ( *url.URL) {rewriteRequestURL(.Out, ) .Out.Host = ""}// SetXForwarded sets the X-Forwarded-For, X-Forwarded-Host, and// X-Forwarded-Proto headers of the outbound request.//// - The X-Forwarded-For header is set to the client IP address.// - The X-Forwarded-Host header is set to the host name requested// by the client.// - The X-Forwarded-Proto header is set to "http" or "https", depending// on whether the inbound request was made on a TLS-enabled connection.//// If the outbound request contains an existing X-Forwarded-For header,// SetXForwarded appends the client IP address to it. To append to the// inbound request's X-Forwarded-For header (the default behavior of// [ReverseProxy] when using a Director function), copy the header// from the inbound request before calling SetXForwarded://// rewriteFunc := func(r *httputil.ProxyRequest) {// r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"]// r.SetXForwarded()// }func ( *ProxyRequest) () { , , := net.SplitHostPort(.In.RemoteAddr)if == nil { := .Out.Header["X-Forwarded-For"]iflen() > 0 { = strings.Join(, ", ") + ", " + } .Out.Header.Set("X-Forwarded-For", ) } else { .Out.Header.Del("X-Forwarded-For") } .Out.Header.Set("X-Forwarded-Host", .In.Host)if .In.TLS == nil { .Out.Header.Set("X-Forwarded-Proto", "http") } else { .Out.Header.Set("X-Forwarded-Proto", "https") }}// ReverseProxy is an HTTP Handler that takes an incoming request and// sends it to another server, proxying the response back to the// client.//// 1xx responses are forwarded to the client if the underlying// transport supports ClientTrace.Got1xxResponse.typeReverseProxystruct {// Rewrite must be a function which modifies // the request into a new request to be sent // using Transport. Its response is then copied // back to the original client unmodified. // Rewrite must not access the provided ProxyRequest // or its contents after returning. // // The Forwarded, X-Forwarded, X-Forwarded-Host, // and X-Forwarded-Proto headers are removed from the // outbound request before Rewrite is called. See also // the ProxyRequest.SetXForwarded method. // // Unparsable query parameters are removed from the // outbound request before Rewrite is called. // The Rewrite function may copy the inbound URL's // RawQuery to the outbound URL to preserve the original // parameter string. Note that this can lead to security // issues if the proxy's interpretation of query parameters // does not match that of the downstream server. // // At most one of Rewrite or Director may be set. Rewrite func(*ProxyRequest)// Director is a function which modifies // the request into a new request to be sent // using Transport. Its response is then copied // back to the original client unmodified. // Director must not access the provided Request // after returning. // // By default, the X-Forwarded-For header is set to the // value of the client IP address. If an X-Forwarded-For // header already exists, the client IP is appended to the // existing values. As a special case, if the header // exists in the Request.Header map but has a nil value // (such as when set by the Director func), the X-Forwarded-For // header is not modified. // // To prevent IP spoofing, be sure to delete any pre-existing // X-Forwarded-For header coming from the client or // an untrusted proxy. // // Hop-by-hop headers are removed from the request after // Director returns, which can remove headers added by // Director. Use a Rewrite function instead to ensure // modifications to the request are preserved. // // Unparsable query parameters are removed from the outbound // request if Request.Form is set after Director returns. // // At most one of Rewrite or Director may be set. Director func(*http.Request)// The transport used to perform proxy requests. // If nil, http.DefaultTransport is used. Transport http.RoundTripper// FlushInterval specifies the flush interval // to flush to the client while copying the // response body. // If zero, no periodic flushing is done. // A negative value means to flush immediately // after each write to the client. // The FlushInterval is ignored when ReverseProxy // recognizes a response as a streaming response, or // if its ContentLength is -1; for such responses, writes // are flushed to the client immediately. FlushInterval time.Duration// ErrorLog specifies an optional logger for errors // that occur when attempting to proxy the request. // If nil, logging is done via the log package's standard logger. ErrorLog *log.Logger// BufferPool optionally specifies a buffer pool to // get byte slices for use by io.CopyBuffer when // copying HTTP response bodies. BufferPool BufferPool// ModifyResponse is an optional function that modifies the // Response from the backend. It is called if the backend // returns a response at all, with any HTTP status code. // If the backend is unreachable, the optional ErrorHandler is // called without any call to ModifyResponse. // // If ModifyResponse returns an error, ErrorHandler is called // with its error value. If ErrorHandler is nil, its default // implementation is used. ModifyResponse func(*http.Response) error// ErrorHandler is an optional function that handles errors // reaching the backend or errors from ModifyResponse. // // If nil, the default is to log the provided error and return // a 502 Status Bad Gateway response. ErrorHandler func(http.ResponseWriter, *http.Request, error)}// A BufferPool is an interface for getting and returning temporary// byte slices for use by [io.CopyBuffer].typeBufferPoolinterface {Get() []bytePut([]byte)}func singleJoiningSlash(, string) string { := strings.HasSuffix(, "/") := strings.HasPrefix(, "/")switch {case && :return + [1:]case ! && !:return + "/" + }return + }func joinURLPath(, *url.URL) (, string) {if .RawPath == "" && .RawPath == "" {returnsingleJoiningSlash(.Path, .Path), "" }// Same as singleJoiningSlash, but uses EscapedPath to determine // whether a slash should be added := .EscapedPath() := .EscapedPath() := strings.HasSuffix(, "/") := strings.HasPrefix(, "/")switch {case && :return .Path + .Path[1:], + [1:]case ! && !:return .Path + "/" + .Path, + "/" + }return .Path + .Path, + }// NewSingleHostReverseProxy returns a new [ReverseProxy] that routes// URLs to the scheme, host, and base path provided in target. If the// target's path is "/base" and the incoming request was for "/dir",// the target request will be for /base/dir.//// NewSingleHostReverseProxy does not rewrite the Host header.//// To customize the ReverseProxy behavior beyond what// NewSingleHostReverseProxy provides, use ReverseProxy directly// with a Rewrite function. The ProxyRequest SetURL method// may be used to route the outbound request. (Note that SetURL,// unlike NewSingleHostReverseProxy, rewrites the Host header// of the outbound request by default.)//// proxy := &ReverseProxy{// Rewrite: func(r *ProxyRequest) {// r.SetURL(target)// r.Out.Host = r.In.Host // if desired// },// }func ( *url.URL) *ReverseProxy { := func( *http.Request) {rewriteRequestURL(, ) }return &ReverseProxy{Director: }}func rewriteRequestURL( *http.Request, *url.URL) { := .RawQuery .URL.Scheme = .Scheme .URL.Host = .Host .URL.Path, .URL.RawPath = joinURLPath(, .URL)if == "" || .URL.RawQuery == "" { .URL.RawQuery = + .URL.RawQuery } else { .URL.RawQuery = + "&" + .URL.RawQuery }}func copyHeader(, http.Header) {for , := range {for , := range { .Add(, ) } }}// Hop-by-hop headers. These are removed when sent to the backend.// As of RFC 7230, hop-by-hop headers are required to appear in the// Connection header field. These are the headers defined by the// obsoleted RFC 2616 (section 13.5.1) and are used for backward// compatibility.var hopHeaders = []string{"Connection","Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google"Keep-Alive","Proxy-Authenticate","Proxy-Authorization","Te", // canonicalized version of "TE""Trailer", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522"Transfer-Encoding","Upgrade",}func ( *ReverseProxy) ( http.ResponseWriter, *http.Request, error) { .logf("http: proxy error: %v", ) .WriteHeader(http.StatusBadGateway)}func ( *ReverseProxy) () func(http.ResponseWriter, *http.Request, error) {if .ErrorHandler != nil {return .ErrorHandler }return .defaultErrorHandler}// modifyResponse conditionally runs the optional ModifyResponse hook// and reports whether the request should proceed.func ( *ReverseProxy) ( http.ResponseWriter, *http.Response, *http.Request) bool {if .ModifyResponse == nil {returntrue }if := .ModifyResponse(); != nil { .Body.Close() .getErrorHandler()(, , )returnfalse }returntrue}func ( *ReverseProxy) ( http.ResponseWriter, *http.Request) { := .Transportif == nil { = http.DefaultTransport } := .Context()if .Done() != nil {// CloseNotifier predates context.Context, and has been // entirely superseded by it. If the request contains // a Context that carries a cancellation signal, don't // bother spinning up a goroutine to watch the CloseNotify // channel (if any). // // If the request Context has a nil Done channel (which // means it is either context.Background, or a custom // Context implementation with no cancellation signal), // then consult the CloseNotifier if available. } elseif , := .(http.CloseNotifier); {varcontext.CancelFunc , = context.WithCancel()defer () := .CloseNotify()gofunc() {select {case<-: ()case<-.Done(): } }() } := .Clone()if .ContentLength == 0 { .Body = nil// Issue 16036: nil Body for http.Transport retries }if .Body != nil {// Reading from the request body after returning from a handler is not // allowed, and the RoundTrip goroutine that reads the Body can outlive // this handler. This can lead to a crash if the handler panics (see // Issue 46866). Although calling Close doesn't guarantee there isn't // any Read in flight after the handle returns, in practice it's safe to // read after closing it.defer .Body.Close() }if .Header == nil { .Header = make(http.Header) // Issue 33142: historical behavior was to always allocate }if (.Director != nil) == (.Rewrite != nil) { .getErrorHandler()(, , errors.New("ReverseProxy must have exactly one of Director or Rewrite set"))return }if .Director != nil { .Director()if .Form != nil { .URL.RawQuery = cleanQueryParams(.URL.RawQuery) } } .Close = false := upgradeType(.Header)if !ascii.IsPrint() { .getErrorHandler()(, , fmt.Errorf("client tried to switch to invalid protocol %q", ))return }removeHopByHopHeaders(.Header)// Issue 21096: tell backend applications that care about trailer support // that we support trailers. (We do, but we don't go out of our way to // advertise that unless the incoming client request thought it was worth // mentioning.) Note that we look at req.Header, not outreq.Header, since // the latter has passed through removeHopByHopHeaders.ifhttpguts.HeaderValuesContainsToken(.Header["Te"], "trailers") { .Header.Set("Te", "trailers") }// After stripping all the hop-by-hop connection headers above, add back any // necessary for protocol upgrades, such as for websockets.if != "" { .Header.Set("Connection", "Upgrade") .Header.Set("Upgrade", ) }if .Rewrite != nil {// Strip client-provided forwarding headers. // The Rewrite func may use SetXForwarded to set new values // for these or copy the previous values from the inbound request. .Header.Del("Forwarded") .Header.Del("X-Forwarded-For") .Header.Del("X-Forwarded-Host") .Header.Del("X-Forwarded-Proto")// Remove unparsable query parameters from the outbound request. .URL.RawQuery = cleanQueryParams(.URL.RawQuery) := &ProxyRequest{In: ,Out: , } .Rewrite() = .Out } else {if , , := net.SplitHostPort(.RemoteAddr); == nil {// If we aren't the first proxy retain prior // X-Forwarded-For information as a comma+space // separated list and fold multiple headers into one. , := .Header["X-Forwarded-For"] := && == nil// Issue 38079: nil now means don't populate the headeriflen() > 0 { = strings.Join(, ", ") + ", " + }if ! { .Header.Set("X-Forwarded-For", ) } } }if , := .Header["User-Agent"]; ! {// If the outbound request doesn't have a User-Agent header set, // don't send the default Go HTTP client User-Agent. .Header.Set("User-Agent", "") }var (sync.Mutexbool ) := &httptrace.ClientTrace{Got1xxResponse: func( int, textproto.MIMEHeader) error { .Lock()defer .Unlock()if {// If RoundTrip has returned, don't try to further modify // the ResponseWriter's header map.returnnil } := .Header()copyHeader(, http.Header()) .WriteHeader()// Clear headers, it's not automatically done by ResponseWriter.WriteHeader() for 1xx responsesclear()returnnil }, } = .WithContext(httptrace.WithClientTrace(.Context(), )) , := .RoundTrip() .Lock() = true .Unlock()if != nil { .getErrorHandler()(, , )return }// Deal with 101 Switching Protocols responses: (WebSocket, h2c, etc)if .StatusCode == http.StatusSwitchingProtocols {if !.modifyResponse(, , ) {return } .handleUpgradeResponse(, , )return }removeHopByHopHeaders(.Header)if !.modifyResponse(, , ) {return }copyHeader(.Header(), .Header)// The "Trailer" header isn't included in the Transport's response, // at least for *http.Transport. Build it up from Trailer. := len(.Trailer)if > 0 { := make([]string, 0, len(.Trailer))for := range .Trailer { = append(, ) } .Header().Add("Trailer", strings.Join(, ", ")) } .WriteHeader(.StatusCode) = .copyResponse(, .Body, .flushInterval())if != nil {defer .Body.Close()// Since we're streaming the response, if we run into an error all we can do // is abort the request. Issue 23643: ReverseProxy should use ErrAbortHandler // on read error while copying body.if !shouldPanicOnCopyError() { .logf("suppressing panic for copyResponse error in test; copy error: %v", )return }panic(http.ErrAbortHandler) } .Body.Close() // close now, instead of defer, to populate res.Traileriflen(.Trailer) > 0 {// Force chunking if we saw a response trailer. // This prevents net/http from calculating the length for short // bodies and adding a Content-Length.http.NewResponseController().Flush() }iflen(.Trailer) == {copyHeader(.Header(), .Trailer)return }for , := range .Trailer { = http.TrailerPrefix + for , := range { .Header().Add(, ) } }}var inOurTests bool// whether we're in our own tests// shouldPanicOnCopyError reports whether the reverse proxy should// panic with http.ErrAbortHandler. This is the right thing to do by// default, but Go 1.10 and earlier did not, so existing unit tests// weren't expecting panics. Only panic in our own tests, or when// running under the HTTP server.func shouldPanicOnCopyError( *http.Request) bool {ifinOurTests {// Our tests know to handle this panic.returntrue }if .Context().Value(http.ServerContextKey) != nil {// We seem to be running under an HTTP server, so // it'll recover the panic.returntrue }// Otherwise act like Go 1.10 and earlier to not break // existing tests.returnfalse}// removeHopByHopHeaders removes hop-by-hop headers.func removeHopByHopHeaders( http.Header) {// RFC 7230, section 6.1: Remove headers listed in the "Connection" header.for , := range ["Connection"] {for , := rangestrings.Split(, ",") {if = textproto.TrimString(); != "" { .Del() } } }// RFC 2616, section 13.5.1: Remove a set of known hop-by-hop headers. // This behavior is superseded by the RFC 7230 Connection header, but // preserve it for backwards compatibility.for , := rangehopHeaders { .Del() }}// flushInterval returns the p.FlushInterval value, conditionally// overriding its value for a specific request/response.func ( *ReverseProxy) ( *http.Response) time.Duration { := .Header.Get("Content-Type")// For Server-Sent Events responses, flush immediately. // The MIME type is defined in https://www.w3.org/TR/eventsource/#text-event-streamif , , := mime.ParseMediaType(); == "text/event-stream" {return -1// negative means immediately }// We might have the case of streaming for which Content-Length might be unset.if .ContentLength == -1 {return -1 }return .FlushInterval}func ( *ReverseProxy) ( http.ResponseWriter, io.Reader, time.Duration) error {vario.Writer = if != 0 { := &maxLatencyWriter{dst: ,flush: http.NewResponseController().Flush,latency: , }defer .stop()// set up initial timer so headers get flushed even if body writes are delayed .flushPending = true .t = time.AfterFunc(, .delayedFlush) = }var []byteif .BufferPool != nil { = .BufferPool.Get()defer .BufferPool.Put() } , := .copyBuffer(, , )return}// copyBuffer returns any write errors or non-EOF read errors, and the amount// of bytes written.func ( *ReverseProxy) ( io.Writer, io.Reader, []byte) (int64, error) {iflen() == 0 { = make([]byte, 32*1024) }varint64for { , := .Read()if != nil && != io.EOF && != context.Canceled { .logf("httputil: ReverseProxy read error during body copy: %v", ) }if > 0 { , := .Write([:])if > 0 { += int64() }if != nil {return , }if != {return , io.ErrShortWrite } }if != nil {if == io.EOF { = nil }return , } }}func ( *ReverseProxy) ( string, ...any) {if .ErrorLog != nil { .ErrorLog.Printf(, ...) } else {log.Printf(, ...) }}type maxLatencyWriter struct { dst io.Writer flush func() error latency time.Duration// non-zero; negative means to flush immediately mu sync.Mutex// protects t, flushPending, and dst.Flush t *time.Timer flushPending bool}func ( *maxLatencyWriter) ( []byte) ( int, error) { .mu.Lock()defer .mu.Unlock() , = .dst.Write()if .latency < 0 { .flush()return }if .flushPending {return }if .t == nil { .t = time.AfterFunc(.latency, .delayedFlush) } else { .t.Reset(.latency) } .flushPending = truereturn}func ( *maxLatencyWriter) () { .mu.Lock()defer .mu.Unlock()if !.flushPending { // if stop was called but AfterFunc already started this goroutinereturn } .flush() .flushPending = false}func ( *maxLatencyWriter) () { .mu.Lock()defer .mu.Unlock() .flushPending = falseif .t != nil { .t.Stop() }}func upgradeType( http.Header) string {if !httpguts.HeaderValuesContainsToken(["Connection"], "Upgrade") {return"" }return .Get("Upgrade")}func ( *ReverseProxy) ( http.ResponseWriter, *http.Request, *http.Response) { := upgradeType(.Header) := upgradeType(.Header)if !ascii.IsPrint() { // We know reqUpType is ASCII, it's checked by the caller. .getErrorHandler()(, , fmt.Errorf("backend tried to switch to invalid protocol %q", )) }if !ascii.EqualFold(, ) { .getErrorHandler()(, , fmt.Errorf("backend tried to switch protocol %q when %q was requested", , ))return } , := .Body.(io.ReadWriteCloser)if ! { .getErrorHandler()(, , fmt.Errorf("internal error: 101 switching protocols response with non-writable body"))return } := http.NewResponseController() , , := .Hijack()iferrors.Is(, http.ErrNotSupported) { .getErrorHandler()(, , fmt.Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", ))return } := make(chanbool)gofunc() {// Ensure that the cancellation of a request closes the backend. // See issue https://golang.org/issue/35559.select {case<-.Context().Done():case<-: } .Close() }()deferclose()if != nil { .getErrorHandler()(, , fmt.Errorf("Hijack failed on protocol switch: %v", ))return }defer .Close()copyHeader(.Header(), .Header) .Header = .Header() .Body = nil// so res.Write only writes the headers; we have res.Body in backConn aboveif := .Write(); != nil { .getErrorHandler()(, , fmt.Errorf("response write: %v", ))return }if := .Flush(); != nil { .getErrorHandler()(, , fmt.Errorf("response flush: %v", ))return } := make(chanerror, 1) := switchProtocolCopier{user: , backend: }go .copyToBackend()go .copyFromBackend() <-}// switchProtocolCopier exists so goroutines proxying data back and// forth have nice names in stacks.type switchProtocolCopier struct { user, backend io.ReadWriter}func ( switchProtocolCopier) ( chan<- error) { , := io.Copy(.user, .backend) <- }func ( switchProtocolCopier) ( chan<- error) { , := io.Copy(.backend, .user) <- }func cleanQueryParams( string) string { := func( string) string { , := url.ParseQuery()return .Encode() }for := 0; < len(); {switch [] {case';':return ()case'%':if +2 >= len() || !ishex([+1]) || !ishex([+2]) {return () } += 3default: ++ } }return}func ishex( byte) bool {switch {case'0' <= && <= '9':returntruecase'a' <= && <= 'f':returntruecase'A' <= && <= 'F':returntrue }returnfalse}
The pages are generated with Goldsv0.7.0-preview. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.