Created
December 25, 2015 08:31
-
-
Save starchou/3fa257ea20fdf6babeab to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 handler | |
package httputil | |
import ( | |
"io" | |
"log" | |
"net" | |
"net/http" | |
"net/url" | |
"strings" | |
"sync" | |
"time" | |
) | |
// onExitFlushLoop is a callback set by tests to detect the state of the | |
// flushLoop() goroutine. | |
var onExitFlushLoop func() | |
// ReverseProxy is an HTTP Handler that takes an incoming request and | |
// sends it to another server, proxying the response back to the | |
// client. | |
type ReverseProxy struct { | |
// Director 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. | |
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. | |
FlushInterval time.Duration | |
// ErrorLog specifies an optional logger for errors | |
// that occur when attempting to proxy the request. | |
// If nil, logging goes to os.Stderr 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 | |
} | |
// A BufferPool is an interface for getting and returning temporary | |
// byte slices for use by io.CopyBuffer. | |
type BufferPool interface { | |
Get() []byte | |
Put([]byte) | |
} | |
func singleJoiningSlash(a, b string) string { | |
aslash := strings.HasSuffix(a, "/") | |
bslash := strings.HasPrefix(b, "/") | |
switch { | |
case aslash && bslash: | |
return a + b[1:] | |
case !aslash && !bslash: | |
return a + "/" + b | |
} | |
return a + b | |
} | |
// 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 rewrite Host headers, use ReverseProxy directly with a custom | |
// Director policy. | |
func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy { | |
targetQuery := target.RawQuery | |
director := func(req *http.Request) { | |
req.URL.Scheme = target.Scheme | |
req.URL.Host = target.Host | |
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path) | |
if targetQuery == "" || req.URL.RawQuery == "" { | |
req.URL.RawQuery = targetQuery + req.URL.RawQuery | |
} else { | |
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery | |
} | |
} | |
return &ReverseProxy{Director: director} | |
} | |
func copyHeader(dst, src http.Header) { | |
for k, vv := range src { | |
for _, v := range vv { | |
dst.Add(k, v) | |
} | |
} | |
} | |
// Hop-by-hop headers. These are removed when sent to the backend. | |
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html | |
var hopHeaders = []string{ | |
"Connection", | |
"Keep-Alive", | |
"Proxy-Authenticate", | |
"Proxy-Authorization", | |
"Te", // canonicalized version of "TE" | |
"Trailers", | |
"Transfer-Encoding", | |
"Upgrade", | |
} | |
type requestCanceler interface { | |
CancelRequest(*http.Request) | |
} | |
type runOnFirstRead struct { | |
io.Reader // optional; nil means empty body | |
fn func() // Run before first Read, then set to nil | |
} | |
func (c *runOnFirstRead) Read(bs []byte) (int, error) { | |
if c.fn != nil { | |
c.fn() | |
c.fn = nil | |
} | |
if c.Reader == nil { | |
return 0, io.EOF | |
} | |
return c.Reader.Read(bs) | |
} | |
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { | |
transport := p.Transport | |
if transport == nil { | |
transport = http.DefaultTransport | |
} | |
outreq := new(http.Request) | |
*outreq = *req // includes shallow copies of maps, but okay | |
p.Director(outreq) | |
outreq.Proto = "HTTP/1.1" | |
outreq.ProtoMajor = 1 | |
outreq.ProtoMinor = 1 | |
outreq.Close = false | |
// Remove hop-by-hop headers to the backend. Especially | |
// important is "Connection" because we want a persistent | |
// connection, regardless of what the client sent to us. This | |
// is modifying the same underlying map from req (shallow | |
// copied above) so we only copy it if necessary. | |
copiedHeaders := false | |
for _, h := range hopHeaders { | |
if outreq.Header.Get(h) != "" { | |
if !copiedHeaders { | |
outreq.Header = make(http.Header) | |
copyHeader(outreq.Header, req.Header) | |
copiedHeaders = true | |
} | |
outreq.Header.Del(h) | |
} | |
} | |
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == 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. | |
if prior, ok := outreq.Header["X-Forwarded-For"]; ok { | |
clientIP = strings.Join(prior, ", ") + ", " + clientIP | |
} | |
outreq.Header.Set("X-Forwarded-For", clientIP) | |
} | |
res, err := transport.RoundTrip(outreq) | |
if err != nil { | |
p.logf("http: proxy error: %v", err) | |
rw.WriteHeader(http.StatusInternalServerError) | |
return | |
} | |
for _, h := range hopHeaders { | |
res.Header.Del(h) | |
} | |
copyHeader(rw.Header(), res.Header) | |
// The "Trailer" header isn't included in the Transport's response, | |
// at least for *http.Transport. Build it up from Trailer. | |
if len(res.Trailer) > 0 { | |
var trailerKeys []string | |
for k := range res.Trailer { | |
trailerKeys = append(trailerKeys, k) | |
} | |
rw.Header().Add("Trailer", strings.Join(trailerKeys, ", ")) | |
} | |
rw.WriteHeader(res.StatusCode) | |
if len(res.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. | |
if fl, ok := rw.(http.Flusher); ok { | |
fl.Flush() | |
} | |
} | |
if closeNotifier, ok := rw.(http.CloseNotifier); ok { | |
if requestCanceler, ok := transport.(requestCanceler); ok { | |
reqDone := make(chan struct{}) | |
defer close(reqDone) | |
clientGone := closeNotifier.CloseNotify() | |
outreq.Body = struct { | |
io.Reader | |
io.Closer | |
}{ | |
Reader: &runOnFirstRead{ | |
Reader: outreq.Body, | |
fn: func() { | |
go func() { | |
select { | |
case <-clientGone: | |
requestCanceler.CancelRequest(outreq) | |
case <-reqDone: | |
} | |
}() | |
}, | |
}, | |
Closer: outreq.Body, | |
} | |
} | |
} | |
p.copyResponse(rw, res.Body) | |
res.Body.Close() // close now, instead of defer, to populate res.Trailer | |
copyHeader(rw.Header(), res.Trailer) | |
} | |
func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) { | |
if p.FlushInterval != 0 { | |
if wf, ok := dst.(writeFlusher); ok { | |
mlw := &maxLatencyWriter{ | |
dst: wf, | |
latency: p.FlushInterval, | |
done: make(chan bool), | |
} | |
go mlw.flushLoop() | |
defer mlw.stop() | |
dst = mlw | |
} | |
} | |
var buf []byte | |
if p.BufferPool != nil { | |
buf = p.BufferPool.Get() | |
} | |
io.CopyBuffer(dst, src, buf) | |
if p.BufferPool != nil { | |
p.BufferPool.Put(buf) | |
} | |
} | |
func (p *ReverseProxy) logf(format string, args ...interface{}) { | |
if p.ErrorLog != nil { | |
p.ErrorLog.Printf(format, args...) | |
} else { | |
log.Printf(format, args...) | |
} | |
} | |
type writeFlusher interface { | |
io.Writer | |
http.Flusher | |
} | |
type maxLatencyWriter struct { | |
dst writeFlusher | |
latency time.Duration | |
lk sync.Mutex // protects Write + Flush | |
done chan bool | |
} | |
func (m *maxLatencyWriter) Write(p []byte) (int, error) { | |
m.lk.Lock() | |
defer m.lk.Unlock() | |
return m.dst.Write(p) | |
} | |
func (m *maxLatencyWriter) flushLoop() { | |
t := time.NewTicker(m.latency) | |
defer t.Stop() | |
for { | |
select { | |
case <-m.done: | |
if onExitFlushLoop != nil { | |
onExitFlushLoop() | |
} | |
return | |
case <-t.C: | |
m.lk.Lock() | |
m.dst.Flush() | |
m.lk.Unlock() | |
} | |
} | |
} | |
func (m *maxLatencyWriter) stop() { m.done <- true } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment