Merge remote-tracking branch 'origin/master' into 14484-collection-record-update
[arvados.git] / lib / controller / proxy.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package controller
6
7 import (
8         "io"
9         "net/http"
10         "net/url"
11
12         "git.curoverse.com/arvados.git/sdk/go/httpserver"
13 )
14
15 type proxy struct {
16         Name string // to use in Via header
17 }
18
19 type HTTPError struct {
20         Message string
21         Code    int
22 }
23
24 func (h HTTPError) Error() string {
25         return h.Message
26 }
27
28 // headers that shouldn't be forwarded when proxying. See
29 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
30 var dropHeaders = map[string]bool{
31         "Connection":          true,
32         "Keep-Alive":          true,
33         "Proxy-Authenticate":  true,
34         "Proxy-Authorization": true,
35         // this line makes gofmt 1.10 and 1.11 agree
36         "TE":                true,
37         "Trailer":           true,
38         "Transfer-Encoding": true, // *-Encoding headers interfer with Go's automatic compression/decompression
39         "Content-Encoding":  true,
40         "Accept-Encoding":   true,
41         "Upgrade":           true,
42 }
43
44 type ResponseFilter func(*http.Response, error) (*http.Response, error)
45
46 // Forward a request to upstream service, and return response or error.
47 func (p *proxy) Do(
48         reqIn *http.Request,
49         urlOut *url.URL,
50         client *http.Client) (*http.Response, error) {
51
52         // Copy headers from incoming request, then add/replace proxy
53         // headers like Via and X-Forwarded-For.
54         hdrOut := http.Header{}
55         for k, v := range reqIn.Header {
56                 if !dropHeaders[k] {
57                         hdrOut[k] = v
58                 }
59         }
60         xff := reqIn.RemoteAddr
61         if xffIn := reqIn.Header.Get("X-Forwarded-For"); xffIn != "" {
62                 xff = xffIn + "," + xff
63         }
64         hdrOut.Set("X-Forwarded-For", xff)
65         if hdrOut.Get("X-Forwarded-Proto") == "" {
66                 hdrOut.Set("X-Forwarded-Proto", reqIn.URL.Scheme)
67         }
68         hdrOut.Add("Via", reqIn.Proto+" arvados-controller")
69
70         reqOut := (&http.Request{
71                 Method: reqIn.Method,
72                 URL:    urlOut,
73                 Host:   reqIn.Host,
74                 Header: hdrOut,
75                 Body:   reqIn.Body,
76         }).WithContext(reqIn.Context())
77
78         resp, err := client.Do(reqOut)
79         return resp, err
80 }
81
82 // Copy a response (or error) to the downstream client
83 func (p *proxy) ForwardResponse(w http.ResponseWriter, resp *http.Response, err error) (int64, error) {
84         if err != nil {
85                 if he, ok := err.(HTTPError); ok {
86                         httpserver.Error(w, he.Message, he.Code)
87                 } else {
88                         httpserver.Error(w, err.Error(), http.StatusBadGateway)
89                 }
90                 return 0, nil
91         }
92
93         defer resp.Body.Close()
94         for k, v := range resp.Header {
95                 for _, v := range v {
96                         w.Header().Add(k, v)
97                 }
98         }
99         w.WriteHeader(resp.StatusCode)
100         return io.Copy(w, resp.Body)
101 }