18874: Merge branch 'main' from arvados-workbench2.git
[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.arvados.org/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 var dropHeaders = map[string]bool{
29         // Headers that shouldn't be forwarded when proxying. See
30         // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
31         "Connection":          true,
32         "Keep-Alive":          true,
33         "Proxy-Authenticate":  true,
34         "Proxy-Authorization": true,
35         // (comment/space here makes gofmt1.10 agree with gofmt1.11)
36         "TE":      true,
37         "Trailer": true,
38         "Upgrade": true,
39
40         // Headers that would interfere with Go's automatic
41         // compression/decompression if we forwarded them.
42         "Accept-Encoding":   true,
43         "Content-Encoding":  true,
44         "Transfer-Encoding": true,
45
46         // Content-Length depends on encoding.
47         "Content-Length": true,
48
49         // Defend against Rails vulnerability CVE-2023-22795 -
50         // we don't use this functionality anyway, so it costs us nothing.
51         // <https://discuss.rubyonrails.org/t/cve-2023-22795-possible-redos-based-dos-vulnerability-in-action-dispatch/82118>
52         "If-None-Match": true,
53 }
54
55 type ResponseFilter func(*http.Response, error) (*http.Response, error)
56
57 // Forward a request to upstream service, and return response or error.
58 func (p *proxy) Do(
59         reqIn *http.Request,
60         urlOut *url.URL,
61         client *http.Client) (*http.Response, error) {
62
63         // Copy headers from incoming request, then add/replace proxy
64         // headers like Via and X-Forwarded-For.
65         hdrOut := http.Header{}
66         for k, v := range reqIn.Header {
67                 if !dropHeaders[k] {
68                         hdrOut[k] = v
69                 }
70         }
71         xff := ""
72         for _, xffIn := range reqIn.Header["X-Forwarded-For"] {
73                 if xffIn != "" {
74                         xff += xffIn + ","
75                 }
76         }
77         xff += reqIn.RemoteAddr
78         hdrOut.Set("X-Forwarded-For", xff)
79         if hdrOut.Get("X-Forwarded-Proto") == "" {
80                 hdrOut.Set("X-Forwarded-Proto", reqIn.URL.Scheme)
81         }
82         hdrOut.Add("Via", reqIn.Proto+" arvados-controller")
83
84         reqOut := (&http.Request{
85                 Method: reqIn.Method,
86                 URL:    urlOut,
87                 Host:   reqIn.Host,
88                 Header: hdrOut,
89                 Body:   reqIn.Body,
90         }).WithContext(reqIn.Context())
91         return client.Do(reqOut)
92 }
93
94 // Copy a response (or error) to the downstream client
95 func (p *proxy) ForwardResponse(w http.ResponseWriter, resp *http.Response, err error) (int64, error) {
96         if err != nil {
97                 if he, ok := err.(HTTPError); ok {
98                         httpserver.Error(w, he.Message, he.Code)
99                 } else {
100                         httpserver.Error(w, err.Error(), http.StatusBadGateway)
101                 }
102                 return 0, nil
103         }
104
105         defer resp.Body.Close()
106         for k, v := range resp.Header {
107                 for _, v := range v {
108                         w.Header().Add(k, v)
109                 }
110         }
111         w.WriteHeader(resp.StatusCode)
112         return io.Copy(w, resp.Body)
113 }