├── .gitignore ├── README.md ├── config ├── README.md ├── config.go ├── config_test.go └── gen.go ├── examples └── customers │ ├── .gitignore │ ├── Dockerfile │ ├── Makefile │ ├── deploy.yaml │ └── main.go ├── gen.go ├── go.mod ├── go.sum ├── httputil ├── README.md ├── gen.go └── httputil.go └── proxy.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | config.yaml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goproxy 2 | -- 3 | import "github.com/autom8ter/goproxy" 4 | 5 | 6 | ## Usage 7 | 8 | #### type GoProxy 9 | 10 | ```go 11 | type GoProxy struct { 12 | } 13 | ``` 14 | 15 | GoProxy is a configurable single-target reverse-proxy HTTP handler compatible 16 | with the net/http http.Handler interface 17 | 18 | #### func NewGoProxy 19 | 20 | ```go 21 | func NewGoProxy(config *config.Config) *GoProxy 22 | ``` 23 | NewGoProxy registers a new reverseproxy handler for each provided config with 24 | the specified path prefix 25 | 26 | #### func (*GoProxy) ServeHTTP 27 | 28 | ```go 29 | func (g *GoProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) 30 | ``` 31 | -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | # config 2 | -- 3 | import "github.com/autom8ter/goproxy/config" 4 | 5 | 6 | ## Usage 7 | 8 | #### type Config 9 | 10 | ```go 11 | type Config struct { 12 | TargetUrl string `validate:"required"` 13 | Headers map[string]string 14 | FormValues map[string]string 15 | FlushInterval time.Duration 16 | WebHookURL string 17 | } 18 | ``` 19 | 20 | Config is used to configure a reverse proxy handler(one route) 21 | 22 | #### func (*Config) DirectorFunc 23 | 24 | ```go 25 | func (c *Config) DirectorFunc() func(req *http.Request) 26 | ``` 27 | 28 | #### func (*Config) Entry 29 | 30 | ```go 31 | func (c *Config) Entry() *logrus.Entry 32 | ``` 33 | 34 | #### func (*Config) JSONString 35 | 36 | ```go 37 | func (c *Config) JSONString() string 38 | ``` 39 | 40 | #### func (*Config) WebHook 41 | 42 | ```go 43 | func (c *Config) WebHook() func(r *http.Response) error 44 | ``` 45 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/autom8ter/api/go/api" 5 | "github.com/sirupsen/logrus" 6 | "net/http" 7 | "net/url" 8 | "time" 9 | ) 10 | 11 | //Config is used to configure a reverse proxy handler(one route) 12 | type Config struct { 13 | TargetUrl string `validate:"required"` 14 | Headers map[string]string 15 | FormValues map[string]string 16 | FlushInterval time.Duration 17 | WebHookURL string 18 | } 19 | 20 | func (c *Config) DirectorFunc() func(req *http.Request) { 21 | target, err := url.Parse(c.TargetUrl) 22 | if err != nil { 23 | api.Util.Entry().Fatalln(err.Error()) 24 | } 25 | targetQuery := target.RawQuery 26 | return func(req *http.Request) { 27 | start := time.Now() 28 | req.URL.Scheme = target.Scheme 29 | req.URL.Host = target.Host 30 | req.URL.Path = target.Path 31 | if c.Headers != nil { 32 | for k, v := range c.Headers { 33 | req.Header.Set(k, v) 34 | } 35 | } 36 | if c.FormValues != nil { 37 | for k, v := range c.FormValues { 38 | req.Form.Set(k, v) 39 | } 40 | } 41 | if targetQuery == "" || req.URL.RawQuery == "" { 42 | req.URL.RawQuery = targetQuery + req.URL.RawQuery 43 | } else { 44 | req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery 45 | } 46 | 47 | api.Util.Entry().Debugf("proxied request: %s\n", api.Util.MarshalJSON(&requestLog{ 48 | Received: api.Util.HumanizeTime(start), 49 | Method: req.Method, 50 | URL: req.URL.String(), 51 | UserAgent: req.UserAgent(), 52 | Referer: req.Referer(), 53 | Proto: req.Proto, 54 | RemoteIP: req.RemoteAddr, 55 | Latency: time.Since(start).String(), 56 | })) 57 | } 58 | } 59 | 60 | func (c *Config) JSONString() string { 61 | return string(api.Util.MarshalJSON(c)) 62 | } 63 | 64 | func (c *Config) WebHook() func(r *http.Response) error { 65 | if c.WebHookURL == "" { 66 | return nil 67 | } 68 | return func(r *http.Response) error { 69 | u, err := url.Parse(c.WebHookURL) 70 | if err != nil { 71 | api.Util.Entry().Fatalln("failed to parse response callback url", err.Error()) 72 | } 73 | 74 | resp, err := http.DefaultClient.Do(&http.Request{ 75 | Method: "POST", 76 | URL: u, 77 | Header: r.Header, 78 | Body: r.Body, 79 | Trailer: r.Trailer, 80 | RemoteAddr: r.Request.RemoteAddr, 81 | }) 82 | r = resp 83 | if err != nil { 84 | return err 85 | } 86 | return nil 87 | } 88 | } 89 | 90 | func (c *Config) Entry() *logrus.Entry { 91 | return api.Util.Entry() 92 | } 93 | 94 | type requestLog struct { 95 | Received string `json:"received"` 96 | Method string `json:"method"` 97 | URL string `json:"url"` 98 | Body string `json:"body"` 99 | UserAgent string `json:"user_agent"` 100 | Referer string `json:"referer"` 101 | Proto string `json:"proto"` 102 | RemoteIP string `json:"remote_ip"` 103 | Latency string `json:"latency"` 104 | } 105 | 106 | func copyHeader(dst, src http.Header) { 107 | for k, vv := range src { 108 | for _, v := range vv { 109 | dst.Add(k, v) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/url" 7 | "testing" 8 | ) 9 | 10 | func Test(t *testing.T) { 11 | u, err := url.Parse("https://api.twilio.com/2010-04-01/Calls.json") 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | fmt.Printf("Full: %v\n", u) 16 | fmt.Printf("User: %v\n", u.User) 17 | fmt.Printf("Host: %v\n", u.Host) 18 | fmt.Printf("ForcQuery: %v\n", u.ForceQuery) 19 | fmt.Printf("Scheme: %v\n", u.Scheme) 20 | fmt.Printf("RawPath: %v\n", u.RawPath) 21 | fmt.Printf("RawQuery: %v\n", u.RawQuery) 22 | fmt.Printf("Path: %v\n", u.Path) 23 | fmt.Printf("Fragment: %v\n", u.Fragment) 24 | fmt.Printf("Opaque: %v\n", u.Opaque) 25 | 26 | } 27 | -------------------------------------------------------------------------------- /config/gen.go: -------------------------------------------------------------------------------- 1 | //go:generate godocdown -o README.md 2 | 3 | package config 4 | -------------------------------------------------------------------------------- /examples/customers/.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .env -------------------------------------------------------------------------------- /examples/customers/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | COPY ./main.go /go/src/github.com/autom8ter/customers/main.go 3 | COPY ./vendor /go/src/github.com/autom8ter/customers/vendor 4 | 5 | RUN set -ex && \ 6 | cd /go/src/github.com/autom8ter/customers && \ 7 | CGO_ENABLED=0 go build \ 8 | -tags netgo \ 9 | -v -a \ 10 | -ldflags '-extldflags "-static"' && \ 11 | mv ./customers /usr/bin/customers 12 | 13 | FROM busybox 14 | ENV SECRET=somesecret 15 | # Retrieve the binary from the previous stage 16 | COPY --from=builder /usr/bin/customers /usr/local/bin/customers 17 | 18 | # Set the binary as the entrypoint of the container 19 | ENTRYPOINT [ "customers" ] -------------------------------------------------------------------------------- /examples/customers/Makefile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autom8ter/goproxy/c915d9f3bce55ed4291c37345f1f59b8e1ace1ff/examples/customers/Makefile -------------------------------------------------------------------------------- /examples/customers/deploy.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autom8ter/goproxy/c915d9f3bce55ed4291c37345f1f59b8e1ace1ff/examples/customers/deploy.yaml -------------------------------------------------------------------------------- /examples/customers/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/autom8ter/api/go/api" 5 | "github.com/autom8ter/goproxy" 6 | "github.com/autom8ter/goproxy/config" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | var BaseURL = "https://api.stripe.com/v1/customers" 12 | 13 | var proxy = goproxy.NewGoProxy(&config.Config{ 14 | TargetUrl: BaseURL, 15 | WebHookURL: os.Getenv("WEBHOOK"), 16 | }) 17 | 18 | func main() { 19 | if err := http.ListenAndServe(":8080", proxy); err != nil { 20 | api.Util.Entry().Fatalln(err.Error()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /gen.go: -------------------------------------------------------------------------------- 1 | //go:generate godocdown -o README.md 2 | 3 | package goproxy 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/autom8ter/goproxy 2 | 3 | require ( 4 | github.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7 // indirect 5 | github.com/autom8ter/api v0.0.0-20190422204252-e86456c85903 6 | github.com/codegangsta/negroni v1.0.0 // indirect 7 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 8 | github.com/gorilla/context v1.1.1 // indirect 9 | github.com/gorilla/mux v1.7.1 // indirect 10 | github.com/rs/cors v1.6.0 // indirect 11 | github.com/sirupsen/logrus v1.4.1 12 | github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect 13 | golang.org/x/net v0.0.0-20190324223953-e3b2ff56ed87 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= 4 | github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 5 | github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= 6 | github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 7 | github.com/Masterminds/sprig v2.18.0+incompatible h1:QoGhlbC6pter1jxKnjMFxT8EqsLuDE6FEcNbWEpw+lI= 8 | github.com/Masterminds/sprig v2.18.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 9 | github.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7 h1:irR1cO6eek3n5uquIVaRAsQmZnlsfPuHNz31cXo4eyk= 10 | github.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7/go.mod h1:LWMyo4iOLWXHGdBki7NIht1kHru/0wM179h+d3g8ATM= 11 | github.com/autom8ter/api v0.0.0-20190422204252-e86456c85903 h1:buwOYDAIqUcD9pPwbyrumxkvKu0wKE50v6aORQBtuhc= 12 | github.com/autom8ter/api v0.0.0-20190422204252-e86456c85903/go.mod h1:udsaoq0WRtTQ/l5jvmzGf6QnBd5Lie7XxJgNZ4LBaqc= 13 | github.com/autom8ter/objectify v0.0.0-20190416235606-534b2554f5fe h1:ACIuNoSbc2oYdcyTHkMepxtJ2ntnwmygFjD+3Ty6HEY= 14 | github.com/autom8ter/objectify v0.0.0-20190416235606-534b2554f5fe/go.mod h1:Hg+n5d+Qj3M0I+vnIo+009AoX2qzOtQJ20UVHYnjF3Q= 15 | github.com/autom8ter/objectify v0.0.0-20190422193702-3644923e4f46 h1:zvQFrbj9TYxw1rDSB2s2YM5LANR6Ggx6oawljn4QJ9c= 16 | github.com/autom8ter/objectify v0.0.0-20190422193702-3644923e4f46/go.mod h1:Hg+n5d+Qj3M0I+vnIo+009AoX2qzOtQJ20UVHYnjF3Q= 17 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 18 | github.com/codegangsta/negroni v1.0.0 h1:+aYywywx4bnKXWvoWtRfJ91vC59NbEhEY03sZjQhbVY= 19 | github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0= 20 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 24 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 25 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 26 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 27 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 28 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 29 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 30 | github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= 31 | github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= 32 | github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= 33 | github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= 34 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 35 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 36 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 37 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 38 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 39 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 40 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 41 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 42 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 43 | github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= 44 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 45 | github.com/gorilla/mux v1.7.1 h1:Dw4jY2nghMMRsh1ol8dv1axHkDwMQK2DHerMNJsIpJU= 46 | github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 47 | github.com/grpc-ecosystem/grpc-gateway v1.8.5 h1:2+KSC78XiO6Qy0hIjfc1OD9H+hsaJdJlb8Kqsd41CTE= 48 | github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 49 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 50 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 51 | github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= 52 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 53 | github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= 54 | github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= 55 | github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= 56 | github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 57 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 58 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 59 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 60 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 61 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 62 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 63 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 64 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 65 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 66 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 67 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 68 | github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= 69 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= 70 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 71 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 72 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 73 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 74 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 75 | github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= 76 | github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 77 | github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= 78 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 79 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 80 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 81 | github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs= 82 | github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 83 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 84 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 85 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 86 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 87 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 88 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 89 | golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 h1:bselrhR0Or1vomJZC8ZIjWtbDmn9OYFLX5Ik9alpJpE= 90 | golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 91 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 92 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 93 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 94 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 95 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 96 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 97 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 98 | golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= 99 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 100 | golang.org/x/net v0.0.0-20190324223953-e3b2ff56ed87 h1:yh5/K199RObPR6zqVBYf+AyJuweAqx+fOe9s3cekn1Y= 101 | golang.org/x/net v0.0.0-20190324223953-e3b2ff56ed87/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 102 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 103 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 104 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 105 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 106 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 107 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 108 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 109 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 110 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts= 111 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 113 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 114 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= 115 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 116 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 117 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 118 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 119 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 120 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 121 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 122 | google.golang.org/genproto v0.0.0-20190401181712-f467c93bbac2 h1:8FyEBtGg6Px24p+H2AkuVWqhj4+R9fo+fZD17mg+lzk= 123 | google.golang.org/genproto v0.0.0-20190401181712-f467c93bbac2/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 124 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 125 | google.golang.org/grpc v1.19.1 h1:TrBcJ1yqAl1G++wO39nD/qtgpsW9/1+QGrluyMGEYgM= 126 | google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 127 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 128 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 129 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 130 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 131 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 132 | gopkg.in/go-playground/validator.v9 v9.28.0 h1:6pzvnzx1RWaaQiAmv6e1DvCFULRaz5cKoP5j1VcrLsc= 133 | gopkg.in/go-playground/validator.v9 v9.28.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 134 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 135 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 136 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 137 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 138 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 139 | -------------------------------------------------------------------------------- /httputil/README.md: -------------------------------------------------------------------------------- 1 | # httputil 2 | -- 3 | import "github.com/autom8ter/goproxy/httputil" 4 | 5 | 6 | ## Usage 7 | 8 | #### type BufferPool 9 | 10 | ```go 11 | type BufferPool interface { 12 | Get() []byte 13 | Put([]byte) 14 | } 15 | ``` 16 | 17 | A BufferPool is an interface for getting and returning temporary byte slices for 18 | use by io.CopyBuffer. 19 | 20 | #### type ReverseProxy 21 | 22 | ```go 23 | type ReverseProxy struct { 24 | // Director must be a function which modifies 25 | // the request into a new request to be sent 26 | // using Transport. Its response is then copied 27 | // back to the original client unmodified. 28 | // Director must not access the provided Request 29 | // after returning. 30 | Director func(*http.Request) 31 | 32 | // The transport used to perform proxy requests. 33 | // If nil, http.DefaultTransport is used. 34 | Transport http.RoundTripper 35 | 36 | // FlushInterval specifies the flush interval 37 | // to flush to the client while copying the 38 | // response body. 39 | // If zero, no periodic flushing is done. 40 | // A negative value means to flush immediately 41 | // after each write to the client. 42 | // The FlushInterval is ignored when ReverseProxy 43 | // recognizes a response as a streaming response; 44 | // for such responses, writes are flushed to the client 45 | // immediately. 46 | FlushInterval time.Duration 47 | 48 | // ErrorLog specifies an optional logger for errors 49 | // that occur when attempting to proxy the request. 50 | // If nil, logging goes to os.Stderr via the log package's 51 | // standard logger. 52 | ErrorLog *logrus.Entry 53 | 54 | // BufferPool optionally specifies a buffer pool to 55 | // get byte slices for use by io.CopyBuffer when 56 | // copying HTTP response bodies. 57 | BufferPool BufferPool 58 | 59 | // ResponseHook is an optional function that modifies the 60 | // Response from the backend. It is called if the backend 61 | // returns a response at all, with any HTTP status code. 62 | // If the backend is unreachable, the optional ErrorHandler is 63 | // called without any call to ResponseHook. 64 | // 65 | // If ResponseHook returns an error, ErrorHandler is called 66 | // with its error value. If ErrorHandler is nil, its default 67 | // implementation is used. 68 | ResponseHook func(*http.Response) error 69 | 70 | // ErrorHandler is an optional function that handles errors 71 | // reaching the backend or errors from ResponseHook. 72 | // 73 | // If nil, the default is to log the provided error and return 74 | // a 502 Status Bad Gateway response. 75 | ErrorHandler func(http.ResponseWriter, *http.Request, error) 76 | } 77 | ``` 78 | 79 | ReverseProxy is an HTTP Handler that takes an incoming request and sends it to 80 | another server, proxying the response back to the client. 81 | 82 | #### func (*ReverseProxy) ServeHTTP 83 | 84 | ```go 85 | func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) 86 | ``` 87 | -------------------------------------------------------------------------------- /httputil/gen.go: -------------------------------------------------------------------------------- 1 | //go:generate godocdown -o README.md 2 | 3 | package httputil 4 | -------------------------------------------------------------------------------- /httputil/httputil.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // HTTP reverse proxy handler 6 | 7 | package httputil 8 | 9 | import ( 10 | "context" 11 | "fmt" 12 | "github.com/sirupsen/logrus" 13 | "io" 14 | "log" 15 | "net" 16 | "net/http" 17 | "strings" 18 | "sync" 19 | "time" 20 | 21 | "golang.org/x/net/http/httpguts" 22 | ) 23 | 24 | // ReverseProxy is an HTTP Handler that takes an incoming request and 25 | // sends it to another server, proxying the response back to the 26 | // client. 27 | type ReverseProxy struct { 28 | // Director must be a function which modifies 29 | // the request into a new request to be sent 30 | // using Transport. Its response is then copied 31 | // back to the original client unmodified. 32 | // Director must not access the provided Request 33 | // after returning. 34 | Director func(*http.Request) 35 | 36 | // The transport used to perform proxy requests. 37 | // If nil, http.DefaultTransport is used. 38 | Transport http.RoundTripper 39 | 40 | // FlushInterval specifies the flush interval 41 | // to flush to the client while copying the 42 | // response body. 43 | // If zero, no periodic flushing is done. 44 | // A negative value means to flush immediately 45 | // after each write to the client. 46 | // The FlushInterval is ignored when ReverseProxy 47 | // recognizes a response as a streaming response; 48 | // for such responses, writes are flushed to the client 49 | // immediately. 50 | FlushInterval time.Duration 51 | 52 | // ErrorLog specifies an optional logger for errors 53 | // that occur when attempting to proxy the request. 54 | // If nil, logging goes to os.Stderr via the log package's 55 | // standard logger. 56 | ErrorLog *logrus.Entry 57 | 58 | // BufferPool optionally specifies a buffer pool to 59 | // get byte slices for use by io.CopyBuffer when 60 | // copying HTTP response bodies. 61 | BufferPool BufferPool 62 | 63 | // ResponseHook is an optional function that modifies the 64 | // Response from the backend. It is called if the backend 65 | // returns a response at all, with any HTTP status code. 66 | // If the backend is unreachable, the optional ErrorHandler is 67 | // called without any call to ResponseHook. 68 | // 69 | // If ResponseHook returns an error, ErrorHandler is called 70 | // with its error value. If ErrorHandler is nil, its default 71 | // implementation is used. 72 | ResponseHook func(*http.Response) error 73 | 74 | // ErrorHandler is an optional function that handles errors 75 | // reaching the backend or errors from ResponseHook. 76 | // 77 | // If nil, the default is to log the provided error and return 78 | // a 502 Status Bad Gateway response. 79 | ErrorHandler func(http.ResponseWriter, *http.Request, error) 80 | } 81 | 82 | // A BufferPool is an interface for getting and returning temporary 83 | // byte slices for use by io.CopyBuffer. 84 | type BufferPool interface { 85 | Get() []byte 86 | Put([]byte) 87 | } 88 | 89 | func singleJoiningSlash(a, b string) string { 90 | aslash := strings.HasSuffix(a, "/") 91 | bslash := strings.HasPrefix(b, "/") 92 | switch { 93 | case aslash && bslash: 94 | return a + b[1:] 95 | case !aslash && !bslash: 96 | return a + "/" + b 97 | } 98 | return a + b 99 | } 100 | 101 | func copyHeader(dst, src http.Header) { 102 | for k, vv := range src { 103 | for _, v := range vv { 104 | dst.Add(k, v) 105 | } 106 | } 107 | } 108 | 109 | func cloneHeader(h http.Header) http.Header { 110 | h2 := make(http.Header, len(h)) 111 | for k, vv := range h { 112 | vv2 := make([]string, len(vv)) 113 | copy(vv2, vv) 114 | h2[k] = vv2 115 | } 116 | return h2 117 | } 118 | 119 | // Hop-by-hop headers. These are removed when sent to the backend. 120 | // As of RFC 7230, hop-by-hop headers are required to appear in the 121 | // Connection header field. These are the headers defined by the 122 | // obsoleted RFC 2616 (section 13.5.1) and are used for backward 123 | // compatibility. 124 | var hopHeaders = []string{ 125 | "Connection", 126 | "Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google 127 | "Keep-Alive", 128 | "Proxy-Authenticate", 129 | "Proxy-Authorization", 130 | "Te", // canonicalized version of "TE" 131 | "Trailer", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522 132 | "Transfer-Encoding", 133 | "Upgrade", 134 | } 135 | 136 | func (p *ReverseProxy) defaultErrorHandler(rw http.ResponseWriter, req *http.Request, err error) { 137 | p.logf("❌ | %v", err) 138 | rw.WriteHeader(http.StatusBadGateway) 139 | } 140 | 141 | func (p *ReverseProxy) getErrorHandler() func(http.ResponseWriter, *http.Request, error) { 142 | if p.ErrorHandler != nil { 143 | return p.ErrorHandler 144 | } 145 | return p.defaultErrorHandler 146 | } 147 | 148 | // modifyResponse conditionally runs the optional ResponseHook hook 149 | // and reports whether the request should proceed. 150 | func (p *ReverseProxy) modifyResponse(rw http.ResponseWriter, res *http.Response, req *http.Request) bool { 151 | if p.ResponseHook == nil { 152 | return true 153 | } 154 | if err := p.ResponseHook(res); err != nil { 155 | res.Body.Close() 156 | p.getErrorHandler()(rw, req, err) 157 | return false 158 | } 159 | return true 160 | } 161 | 162 | func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 163 | transport := p.Transport 164 | if transport == nil { 165 | transport = http.DefaultTransport 166 | } 167 | 168 | ctx := req.Context() 169 | if cn, ok := rw.(http.CloseNotifier); ok { 170 | var cancel context.CancelFunc 171 | ctx, cancel = context.WithCancel(ctx) 172 | defer cancel() 173 | notifyChan := cn.CloseNotify() 174 | go func() { 175 | select { 176 | case <-notifyChan: 177 | cancel() 178 | case <-ctx.Done(): 179 | } 180 | }() 181 | } 182 | 183 | outreq := req.WithContext(ctx) // includes shallow copies of maps, but okay 184 | if req.ContentLength == 0 { 185 | outreq.Body = nil // Issue 16036: nil Body for http.Transport retries 186 | } 187 | 188 | outreq.Header = cloneHeader(req.Header) 189 | 190 | p.Director(outreq) 191 | outreq.Close = false 192 | 193 | reqUpType := upgradeType(outreq.Header) 194 | removeConnectionHeaders(outreq.Header) 195 | 196 | // Remove hop-by-hop headers to the backend. Especially 197 | // important is "Connection" because we want a persistent 198 | // connection, regardless of what the client sent to us. 199 | for _, h := range hopHeaders { 200 | hv := outreq.Header.Get(h) 201 | if hv == "" { 202 | continue 203 | } 204 | if h == "Te" && hv == "trailers" { 205 | // Issue 21096: tell backend applications that 206 | // care about trailer support that we support 207 | // trailers. (We do, but we don't go out of 208 | // our way to advertise that unless the 209 | // incoming client request thought it was 210 | // worth mentioning) 211 | continue 212 | } 213 | outreq.Header.Del(h) 214 | } 215 | 216 | // After stripping all the hop-by-hop connection headers above, add back any 217 | // necessary for protocol upgrades, such as for websockets. 218 | if reqUpType != "" { 219 | outreq.Header.Set("Connection", "Upgrade") 220 | outreq.Header.Set("Upgrade", reqUpType) 221 | } 222 | 223 | if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { 224 | // If we aren't the first proxy retain prior 225 | // X-Forwarded-For information as a comma+space 226 | // separated list and fold multiple headers into one. 227 | if prior, ok := outreq.Header["X-Forwarded-For"]; ok { 228 | clientIP = strings.Join(prior, ", ") + ", " + clientIP 229 | } 230 | outreq.Header.Set("X-Forwarded-For", clientIP) 231 | } 232 | 233 | res, err := transport.RoundTrip(outreq) 234 | if err != nil { 235 | p.getErrorHandler()(rw, outreq, err) 236 | return 237 | } 238 | 239 | // Deal with 101 Switching Protocols responses: (WebSocket, h2c, etc) 240 | if res.StatusCode == http.StatusSwitchingProtocols { 241 | if !p.modifyResponse(rw, res, outreq) { 242 | return 243 | } 244 | p.handleUpgradeResponse(rw, outreq, res) 245 | return 246 | } 247 | 248 | removeConnectionHeaders(res.Header) 249 | 250 | for _, h := range hopHeaders { 251 | res.Header.Del(h) 252 | } 253 | 254 | if !p.modifyResponse(rw, res, outreq) { 255 | return 256 | } 257 | 258 | copyHeader(rw.Header(), res.Header) 259 | 260 | // The "Trailer" header isn't included in the Transport's response, 261 | // at least for *http.Transport. Build it up from Trailer. 262 | announcedTrailers := len(res.Trailer) 263 | if announcedTrailers > 0 { 264 | trailerKeys := make([]string, 0, len(res.Trailer)) 265 | for k := range res.Trailer { 266 | trailerKeys = append(trailerKeys, k) 267 | } 268 | rw.Header().Add("Trailer", strings.Join(trailerKeys, ", ")) 269 | } 270 | 271 | rw.WriteHeader(res.StatusCode) 272 | 273 | err = p.copyResponse(rw, res.Body, p.flushInterval(req, res)) 274 | if err != nil { 275 | defer res.Body.Close() 276 | // Since we're streaming the response, if we run into an error all we can do 277 | // is abort the request. Issue 23643: ReverseProxy should use ErrAbortHandler 278 | // on read error while copying body. 279 | if !shouldPanicOnCopyError(req) { 280 | p.logf("suppressing panic for copyResponse error in test; copy error: %v", err) 281 | return 282 | } 283 | panic(http.ErrAbortHandler) 284 | } 285 | res.Body.Close() // close now, instead of defer, to populate res.Trailer 286 | 287 | if len(res.Trailer) > 0 { 288 | // Force chunking if we saw a response trailer. 289 | // This prevents net/http from calculating the length for short 290 | // bodies and adding a Content-Length. 291 | if fl, ok := rw.(http.Flusher); ok { 292 | fl.Flush() 293 | } 294 | } 295 | 296 | if len(res.Trailer) == announcedTrailers { 297 | copyHeader(rw.Header(), res.Trailer) 298 | return 299 | } 300 | 301 | for k, vv := range res.Trailer { 302 | k = http.TrailerPrefix + k 303 | for _, v := range vv { 304 | rw.Header().Add(k, v) 305 | } 306 | } 307 | } 308 | 309 | var inOurTests bool // whether we're in our own tests 310 | 311 | // shouldPanicOnCopyError reports whether the reverse proxy should 312 | // panic with http.ErrAbortHandler. This is the right thing to do by 313 | // default, but Go 1.10 and earlier did not, so existing unit tests 314 | // weren't expecting panics. Only panic in our own tests, or when 315 | // running under the HTTP server. 316 | func shouldPanicOnCopyError(req *http.Request) bool { 317 | if inOurTests { 318 | // Our tests know to handle this panic. 319 | return true 320 | } 321 | if req.Context().Value(http.ServerContextKey) != nil { 322 | // We seem to be running under an HTTP server, so 323 | // it'll recover the panic. 324 | return true 325 | } 326 | // Otherwise act like Go 1.10 and earlier to not break 327 | // existing tests. 328 | return false 329 | } 330 | 331 | // removeConnectionHeaders removes hop-by-hop headers listed in the "Connection" header of h. 332 | // See RFC 7230, section 6.1 333 | func removeConnectionHeaders(h http.Header) { 334 | if c := h.Get("Connection"); c != "" { 335 | for _, f := range strings.Split(c, ",") { 336 | if f = strings.TrimSpace(f); f != "" { 337 | h.Del(f) 338 | } 339 | } 340 | } 341 | } 342 | 343 | // flushInterval returns the p.FlushInterval value, conditionally 344 | // overriding its value for a specific request/response. 345 | func (p *ReverseProxy) flushInterval(req *http.Request, res *http.Response) time.Duration { 346 | resCT := res.Header.Get("Content-Type") 347 | 348 | // For Server-Sent Events responses, flush immediately. 349 | // The MIME type is defined in https://www.w3.org/TR/eventsource/#text-event-stream 350 | if resCT == "text/event-stream" { 351 | return -1 // negative means immediately 352 | } 353 | 354 | // TODO: more specific cases? e.g. res.ContentLength == -1? 355 | return p.FlushInterval 356 | } 357 | 358 | func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader, flushInterval time.Duration) error { 359 | if flushInterval != 0 { 360 | if wf, ok := dst.(writeFlusher); ok { 361 | mlw := &maxLatencyWriter{ 362 | dst: wf, 363 | latency: flushInterval, 364 | } 365 | defer mlw.stop() 366 | 367 | // set up initial timer so headers get flushed even if body writes are delayed 368 | mlw.flushPending = true 369 | mlw.t = time.AfterFunc(flushInterval, mlw.delayedFlush) 370 | 371 | dst = mlw 372 | } 373 | } 374 | 375 | var buf []byte 376 | if p.BufferPool != nil { 377 | buf = p.BufferPool.Get() 378 | defer p.BufferPool.Put(buf) 379 | } 380 | _, err := p.copyBuffer(dst, src, buf) 381 | return err 382 | } 383 | 384 | // copyBuffer returns any write errors or non-EOF read errors, and the amount 385 | // of bytes written. 386 | func (p *ReverseProxy) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) { 387 | if len(buf) == 0 { 388 | buf = make([]byte, 32*1024) 389 | } 390 | var written int64 391 | for { 392 | nr, rerr := src.Read(buf) 393 | if rerr != nil && rerr != io.EOF && rerr != context.Canceled { 394 | p.logf("❌ | read error during body copy: %v", rerr) 395 | } 396 | if nr > 0 { 397 | nw, werr := dst.Write(buf[:nr]) 398 | if nw > 0 { 399 | written += int64(nw) 400 | } 401 | if werr != nil { 402 | return written, werr 403 | } 404 | if nr != nw { 405 | return written, io.ErrShortWrite 406 | } 407 | } 408 | if rerr != nil { 409 | if rerr == io.EOF { 410 | rerr = nil 411 | } 412 | return written, rerr 413 | } 414 | } 415 | } 416 | 417 | func (p *ReverseProxy) logf(format string, args ...interface{}) { 418 | if p.ErrorLog != nil { 419 | p.ErrorLog.Printf(format, args...) 420 | } else { 421 | log.Printf(format, args...) 422 | } 423 | } 424 | 425 | type writeFlusher interface { 426 | io.Writer 427 | http.Flusher 428 | } 429 | 430 | type maxLatencyWriter struct { 431 | dst writeFlusher 432 | latency time.Duration // non-zero; negative means to flush immediately 433 | 434 | mu sync.Mutex // protects t, flushPending, and dst.Flush 435 | t *time.Timer 436 | flushPending bool 437 | } 438 | 439 | func (m *maxLatencyWriter) Write(p []byte) (n int, err error) { 440 | m.mu.Lock() 441 | defer m.mu.Unlock() 442 | n, err = m.dst.Write(p) 443 | if m.latency < 0 { 444 | m.dst.Flush() 445 | return 446 | } 447 | if m.flushPending { 448 | return 449 | } 450 | if m.t == nil { 451 | m.t = time.AfterFunc(m.latency, m.delayedFlush) 452 | } else { 453 | m.t.Reset(m.latency) 454 | } 455 | m.flushPending = true 456 | return 457 | } 458 | 459 | func (m *maxLatencyWriter) delayedFlush() { 460 | m.mu.Lock() 461 | defer m.mu.Unlock() 462 | if !m.flushPending { // if stop was called but AfterFunc already started this goroutine 463 | return 464 | } 465 | m.dst.Flush() 466 | m.flushPending = false 467 | } 468 | 469 | func (m *maxLatencyWriter) stop() { 470 | m.mu.Lock() 471 | defer m.mu.Unlock() 472 | m.flushPending = false 473 | if m.t != nil { 474 | m.t.Stop() 475 | } 476 | } 477 | 478 | func upgradeType(h http.Header) string { 479 | if !httpguts.HeaderValuesContainsToken(h["Connection"], "Upgrade") { 480 | return "" 481 | } 482 | return strings.ToLower(h.Get("Upgrade")) 483 | } 484 | 485 | func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.Request, res *http.Response) { 486 | reqUpType := upgradeType(req.Header) 487 | resUpType := upgradeType(res.Header) 488 | if reqUpType != resUpType { 489 | p.getErrorHandler()(rw, req, fmt.Errorf("backend tried to switch protocol %q when %q was requested", resUpType, reqUpType)) 490 | return 491 | } 492 | 493 | copyHeader(res.Header, rw.Header()) 494 | 495 | hj, ok := rw.(http.Hijacker) 496 | if !ok { 497 | p.getErrorHandler()(rw, req, fmt.Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw)) 498 | return 499 | } 500 | backConn, ok := res.Body.(io.ReadWriteCloser) 501 | if !ok { 502 | p.getErrorHandler()(rw, req, fmt.Errorf("internal error: 101 switching protocols response with non-writable body")) 503 | return 504 | } 505 | defer backConn.Close() 506 | conn, brw, err := hj.Hijack() 507 | if err != nil { 508 | p.getErrorHandler()(rw, req, fmt.Errorf("Hijack failed on protocol switch: %v", err)) 509 | return 510 | } 511 | defer conn.Close() 512 | res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above 513 | if err := res.Write(brw); err != nil { 514 | p.getErrorHandler()(rw, req, fmt.Errorf("response write: %v", err)) 515 | return 516 | } 517 | if err := brw.Flush(); err != nil { 518 | p.getErrorHandler()(rw, req, fmt.Errorf("response flush: %v", err)) 519 | return 520 | } 521 | errc := make(chan error, 1) 522 | spc := switchProtocolCopier{user: conn, backend: backConn} 523 | go spc.copyToBackend(errc) 524 | go spc.copyFromBackend(errc) 525 | <-errc 526 | return 527 | } 528 | 529 | // switchProtocolCopier exists so goroutines proxying data back and 530 | // forth have nice names in stacks. 531 | type switchProtocolCopier struct { 532 | user, backend io.ReadWriter 533 | } 534 | 535 | func (c switchProtocolCopier) copyFromBackend(errc chan<- error) { 536 | _, err := io.Copy(c.user, c.backend) 537 | errc <- err 538 | } 539 | 540 | func (c switchProtocolCopier) copyToBackend(errc chan<- error) { 541 | _, err := io.Copy(c.backend, c.user) 542 | errc <- err 543 | } 544 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package goproxy 2 | 3 | import ( 4 | "github.com/autom8ter/api/go/api" 5 | "github.com/autom8ter/goproxy/config" 6 | "github.com/autom8ter/goproxy/httputil" 7 | "net/http" 8 | ) 9 | 10 | //GoProxy is a configurable single-target reverse-proxy HTTP handler compatible with the net/http http.Handler interface 11 | type GoProxy struct { 12 | r *httputil.ReverseProxy 13 | config *config.Config 14 | } 15 | 16 | func (g *GoProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { 17 | g.r.ServeHTTP(w, r) 18 | } 19 | 20 | //NewGoProxy registers a new reverseproxy handler for each provided config with the specified path prefix 21 | func NewGoProxy(config *config.Config) *GoProxy { 22 | if err := api.Util.Validate(config); err != nil { 23 | api.Util.Entry().Fatalln(err.Error()) 24 | } 25 | 26 | return &GoProxy{ 27 | config: config, 28 | r: &httputil.ReverseProxy{ 29 | Director: config.DirectorFunc(), 30 | Transport: http.DefaultTransport, 31 | FlushInterval: config.FlushInterval, 32 | ErrorLog: config.Entry(), 33 | ResponseHook: config.WebHook(), 34 | }, 35 | } 36 | } 37 | --------------------------------------------------------------------------------