├── .gitignore ├── Makefile ├── README.md ├── glide.lock ├── glide.yaml ├── images ├── aws-es-proxy │ └── Dockerfile └── builder │ ├── Dockerfile │ └── onbuild.sh ├── main.go └── signing_transport.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .build/ 3 | 4 | *.iml 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: image 2 | 3 | code: 4 | glide install 5 | go install . 6 | 7 | builder-image: 8 | docker build -f images/builder/Dockerfile -t builder . 9 | 10 | build-in-docker: builder-image 11 | docker run -it -v `pwd`:/src builder /onbuild.sh 12 | 13 | image: build-in-docker 14 | docker build -t kope/aws-es-proxy -f images/aws-es-proxy/Dockerfile . 15 | 16 | push: image 17 | docker push kope/aws-es-proxy:latest 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple proxy that signs requests to an AWS ElasticSearch service 2 | 3 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 34b66567d16cc3e3190db66148c6610b1d85fe089b242e7ba303249d5777d95b 2 | updated: 2016-05-17T11:16:33.480146521-04:00 3 | imports: 4 | - name: github.com/aws/aws-sdk-go 5 | version: 7bad22e9cff8295edef29844b4daa55ecfb057b9 6 | subpackages: 7 | - aws 8 | - aws/credentials 9 | - aws/defaults 10 | - aws/request 11 | - private/signer/v4 12 | - aws/awserr 13 | - aws/corehandlers 14 | - aws/credentials/ec2rolecreds 15 | - aws/ec2metadata 16 | - private/endpoints 17 | - aws/awsutil 18 | - aws/client/metadata 19 | - private/protocol/rest 20 | - aws/client 21 | - name: github.com/go-ini/ini 22 | version: 12f418cc7edc5a618a51407b7ac1f1f512139df3 23 | - name: github.com/golang/glog 24 | version: 23def4e6c14b4da8ac2ed8007337bc5eb5007998 25 | - name: github.com/jmespath/go-jmespath 26 | version: 0b12d6b521d83fc7f755e7cfc1b1fbdd35a01a74 27 | devImports: [] 28 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/kopeio/aws-es-proxy 2 | import: 3 | - package: github.com/aws/aws-sdk-go 4 | subpackages: 5 | - aws 6 | - aws/credentials 7 | - aws/defaults 8 | - aws/request 9 | - private/signer/v4 10 | - package: github.com/golang/glog 11 | -------------------------------------------------------------------------------- /images/aws-es-proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:jessie 2 | 3 | RUN apt-get update && apt-get install --yes ca-certificates 4 | 5 | COPY /.build/artifacts/aws-es-proxy /usr/bin/aws-es-proxy 6 | 7 | CMD /usr/bin/aws-es-proxy 8 | 9 | -------------------------------------------------------------------------------- /images/builder/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:jessie 2 | 3 | # Install packages: 4 | # curl (to download glide & golang) 5 | # git (for go get) 6 | RUN apt-get update 7 | RUN apt-get install --yes curl git 8 | 9 | # Install golang 10 | RUN curl -L https://storage.googleapis.com/golang/go1.6.2.linux-amd64.tar.gz | tar zx -C /usr/local 11 | ENV PATH $PATH:/usr/local/go/bin 12 | # Install glide 13 | RUN curl -L https://github.com/Masterminds/glide/releases/download/0.10.2/glide-0.10.2-linux-amd64.tar.gz | tar zx --strip-components 1 -C /usr/bin 14 | 15 | COPY images/builder/onbuild.sh /onbuild.sh -------------------------------------------------------------------------------- /images/builder/onbuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p /go 4 | export GOPATH=/go 5 | 6 | mkdir -p /go/src/github.com/kopeio/ 7 | ln -s /src /go/src/github.com/kopeio/aws-es-proxy 8 | 9 | cd /go/src/github.com/kopeio/aws-es-proxy 10 | /usr/bin/glide install 11 | 12 | go install . 13 | 14 | mkdir -p /src/.build/artifacts/ 15 | cp /go/bin/aws-es-proxy /src/.build/artifacts/ 16 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "net/http" 6 | "net/http/httputil" 7 | "net/url" 8 | "os" 9 | "time" 10 | 11 | "github.com/aws/aws-sdk-go/aws/defaults" 12 | "github.com/golang/glog" 13 | ) 14 | 15 | var esEndpoint string 16 | var listenHostAddr string 17 | var awsRegion string 18 | 19 | func init() { 20 | flag.StringVar(&esEndpoint, "es", "", "elasticsearch endpoint") 21 | flag.StringVar(&listenHostAddr, "listen", ":9200", "endpoint on which to listen") 22 | 23 | flag.StringVar(&awsRegion, "region", "", "AWS Region") 24 | } 25 | 26 | func envToFlag(envName, flagName string) { 27 | v := os.Getenv(envName) 28 | if v != "" { 29 | flag.Set(flagName, v) 30 | } 31 | } 32 | 33 | func main() { 34 | flag.Set("logtostderr", "1") 35 | 36 | flag.Parse() 37 | 38 | envToFlag("AWS_REGION", "region") 39 | envToFlag("ES", "es") 40 | envToFlag("LISTEN", "listen") 41 | envToFlag("GLOG_v", "v") 42 | 43 | if esEndpoint == "" { 44 | glog.Fatal("elasticsearch endpoint flag (es) is required") 45 | } 46 | if awsRegion == "" { 47 | glog.Fatal("AWS region flag (region) is required") 48 | } 49 | 50 | target, err := url.Parse(esEndpoint) 51 | if err != nil { 52 | glog.Fatalf("cannot parse es argument (%q) as URL", esEndpoint) 53 | } 54 | 55 | proxy := httputil.NewSingleHostReverseProxy(target) 56 | 57 | credentials := defaults.CredChain(defaults.Config(), defaults.Handlers()) 58 | 59 | signingRoundTripper := NewSigningRoundTripper(proxy.Transport, awsRegion, credentials) 60 | proxy.Transport = signingRoundTripper 61 | 62 | s := &http.Server{ 63 | Addr: listenHostAddr, 64 | Handler: proxy, 65 | ReadTimeout: 120 * time.Second, 66 | WriteTimeout: 120 * time.Second, 67 | MaxHeaderBytes: 1 << 20, 68 | } 69 | glog.Infof("Listening on %s", listenHostAddr) 70 | err = s.ListenAndServe() 71 | glog.Fatalf("error listening on %q for http requests: %v", listenHostAddr, err) 72 | } 73 | -------------------------------------------------------------------------------- /signing_transport.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | "github.com/aws/aws-sdk-go/aws" 11 | "github.com/aws/aws-sdk-go/aws/credentials" 12 | "github.com/aws/aws-sdk-go/aws/request" 13 | "github.com/aws/aws-sdk-go/private/signer/v4" 14 | "github.com/golang/glog" 15 | ) 16 | 17 | const SERVICE_NAME = "es" 18 | 19 | type SigningRoundTripper struct { 20 | region string 21 | inner http.RoundTripper 22 | credentials *credentials.Credentials 23 | } 24 | 25 | var _ http.RoundTripper = &SigningRoundTripper{} 26 | 27 | func NewSigningRoundTripper(inner http.RoundTripper, region string, credentials *credentials.Credentials) *SigningRoundTripper { 28 | if inner == nil { 29 | inner = http.DefaultTransport 30 | } 31 | p := &SigningRoundTripper{inner: inner, region: region, credentials: credentials} 32 | return p 33 | } 34 | 35 | func (p *SigningRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 36 | glog.V(2).Infof("Got request: %s %s", req.Method, req.URL) 37 | 38 | // Fix the host header in case broken by proxy-rewrite 39 | if req.URL.Host != "" { 40 | req.Host = req.URL.Host 41 | } 42 | 43 | // I think the AWS authentication proxy does not like forwarded headers 44 | for k := range req.Header { 45 | lk := strings.ToLower(k) 46 | if lk == "x-forwarded-host" { 47 | delete(req.Header, k) 48 | } 49 | if lk == "x-forwarded-for" { 50 | delete(req.Header, k) 51 | } 52 | if lk == "x-forwarded-proto" { 53 | delete(req.Header, k) 54 | } 55 | if lk == "x-forward-for" { 56 | delete(req.Header, k) 57 | } 58 | if lk == "x-forward-proto" { 59 | delete(req.Header, k) 60 | } 61 | if lk == "x-forward-port" { 62 | delete(req.Header, k) 63 | } 64 | } 65 | 66 | // We're going to put our own auth headers on here 67 | delete(req.Header, "Authorization") 68 | 69 | var body []byte 70 | var err error 71 | 72 | if req.Body != nil { 73 | body, err = ioutil.ReadAll(req.Body) 74 | if err != nil { 75 | glog.Infof("error reading request body: %v", err) 76 | return nil, err 77 | } 78 | } 79 | 80 | if req.Method == "GET" || req.Method == "HEAD" { 81 | delete(req.Header, "Content-Length") 82 | } 83 | 84 | oldPath := req.URL.Path 85 | if oldPath != "" { 86 | // Escape the path before signing so that the path in the signature and 87 | // the path in the request match. 88 | req.URL.Path = req.URL.EscapedPath() 89 | glog.V(4).Infof("Path -> %q", req.URL.Path) 90 | } 91 | 92 | awsReq := &request.Request{} 93 | awsReq.Config.Credentials = p.credentials 94 | awsReq.Config.Region = aws.String(p.region) 95 | awsReq.ClientInfo.ServiceName = SERVICE_NAME 96 | awsReq.HTTPRequest = req 97 | awsReq.Time = time.Now() 98 | awsReq.ExpireTime = 0 99 | if body != nil { 100 | awsReq.Body = bytes.NewReader(body) 101 | } 102 | 103 | if glog.V(4) { 104 | awsReq.Config.LogLevel = aws.LogLevel(aws.LogDebugWithSigning) 105 | awsReq.Config.Logger = aws.NewDefaultLogger() 106 | } 107 | 108 | v4.Sign(awsReq) 109 | 110 | if awsReq.Error != nil { 111 | glog.Warningf("error signing request: %v", awsReq.Error) 112 | return nil, awsReq.Error 113 | } 114 | 115 | req.URL.Path = oldPath 116 | 117 | if body != nil { 118 | req.Body = ioutil.NopCloser(bytes.NewReader(body)) 119 | } 120 | 121 | response, err := p.inner.RoundTrip(req) 122 | 123 | if err != nil { 124 | glog.Warning("Request error: ", err) 125 | return nil, err 126 | } else { 127 | glog.V(2).Infof("response %s", response.Status) 128 | return response, err 129 | } 130 | } 131 | --------------------------------------------------------------------------------