13493: Salt tokens when forwarding requests to remote clusters.
[arvados.git] / lib / controller / handler.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         "context"
9         "io"
10         "net"
11         "net/http"
12         "net/url"
13         "regexp"
14         "strings"
15         "sync"
16         "time"
17
18         "git.curoverse.com/arvados.git/sdk/go/arvados"
19         "git.curoverse.com/arvados.git/sdk/go/health"
20         "git.curoverse.com/arvados.git/sdk/go/httpserver"
21 )
22
23 type Handler struct {
24         Cluster     *arvados.Cluster
25         NodeProfile *arvados.NodeProfile
26
27         setupOnce    sync.Once
28         handlerStack http.Handler
29         proxyClient  *arvados.Client
30 }
31
32 func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
33         h.setupOnce.Do(h.setup)
34         h.handlerStack.ServeHTTP(w, req)
35 }
36
37 func (h *Handler) CheckHealth() error {
38         h.setupOnce.Do(h.setup)
39         _, err := findRailsAPI(h.Cluster, h.NodeProfile)
40         return err
41 }
42
43 func (h *Handler) setup() {
44         mux := http.NewServeMux()
45         mux.Handle("/_health/", &health.Handler{
46                 Token:  h.Cluster.ManagementToken,
47                 Prefix: "/_health/",
48         })
49         hs := http.NotFoundHandler()
50         hs = prepend(hs, h.proxyRailsAPI)
51         hs = prepend(hs, h.proxyRemoteCluster)
52         mux.Handle("/", hs)
53         h.handlerStack = mux
54 }
55
56 // headers that shouldn't be forwarded when proxying. See
57 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
58 var dropHeaders = map[string]bool{
59         "Connection":          true,
60         "Keep-Alive":          true,
61         "Proxy-Authenticate":  true,
62         "Proxy-Authorization": true,
63         "TE":                true,
64         "Trailer":           true,
65         "Transfer-Encoding": true,
66         "Upgrade":           true,
67 }
68
69 type middlewareFunc func(http.ResponseWriter, *http.Request, http.Handler)
70
71 func prepend(next http.Handler, middleware middlewareFunc) http.Handler {
72         return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
73                 middleware(w, req, next)
74         })
75 }
76
77 var wfRe = regexp.MustCompile(`^/arvados/v1/workflows/([0-9a-z]{5})-[^/]+$`)
78
79 func (h *Handler) proxyRemoteCluster(w http.ResponseWriter, req *http.Request, next http.Handler) {
80         m := wfRe.FindStringSubmatch(req.URL.Path)
81         if len(m) < 2 || m[1] == h.Cluster.ClusterID {
82                 next.ServeHTTP(w, req)
83                 return
84         }
85         remoteID := m[1]
86         remote, ok := h.Cluster.RemoteClusters[remoteID]
87         if !ok {
88                 httpserver.Error(w, "no proxy available for cluster "+remoteID, http.StatusNotFound)
89                 return
90         }
91         scheme := remote.Scheme
92         if scheme == "" {
93                 scheme = "https"
94         }
95         urlOut := &url.URL{
96                 Scheme:   scheme,
97                 Host:     remote.Host,
98                 Path:     req.URL.Path,
99                 RawPath:  req.URL.RawPath,
100                 RawQuery: req.URL.RawQuery,
101         }
102         err := h.saltAuthToken(req, remoteID)
103         if err != nil {
104                 httpserver.Error(w, err.Error(), http.StatusBadRequest)
105                 return
106         }
107         h.proxy(w, req, urlOut)
108 }
109
110 func (h *Handler) proxyRailsAPI(w http.ResponseWriter, req *http.Request, next http.Handler) {
111         urlOut, err := findRailsAPI(h.Cluster, h.NodeProfile)
112         if err != nil {
113                 httpserver.Error(w, err.Error(), http.StatusInternalServerError)
114                 return
115         }
116         urlOut = &url.URL{
117                 Scheme:   urlOut.Scheme,
118                 Host:     urlOut.Host,
119                 Path:     req.URL.Path,
120                 RawPath:  req.URL.RawPath,
121                 RawQuery: req.URL.RawQuery,
122         }
123         h.proxy(w, req, urlOut)
124 }
125
126 func (h *Handler) proxy(w http.ResponseWriter, reqIn *http.Request, urlOut *url.URL) {
127         // Copy headers from incoming request, then add/replace proxy
128         // headers like Via and X-Forwarded-For.
129         hdrOut := http.Header{}
130         for k, v := range reqIn.Header {
131                 if !dropHeaders[k] {
132                         hdrOut[k] = v
133                 }
134         }
135         xff := reqIn.RemoteAddr
136         if xffIn := reqIn.Header.Get("X-Forwarded-For"); xffIn != "" {
137                 xff = xffIn + "," + xff
138         }
139         hdrOut.Set("X-Forwarded-For", xff)
140         hdrOut.Add("Via", reqIn.Proto+" arvados-controller")
141
142         ctx := reqIn.Context()
143         if timeout := h.Cluster.HTTPRequestTimeout; timeout > 0 {
144                 var cancel context.CancelFunc
145                 ctx, cancel = context.WithDeadline(ctx, time.Now().Add(time.Duration(timeout)))
146                 defer cancel()
147         }
148
149         reqOut := (&http.Request{
150                 Method: reqIn.Method,
151                 URL:    urlOut,
152                 Header: hdrOut,
153                 Body:   reqIn.Body,
154         }).WithContext(ctx)
155         resp, err := arvados.InsecureHTTPClient.Do(reqOut)
156         if err != nil {
157                 httpserver.Error(w, err.Error(), http.StatusInternalServerError)
158                 return
159         }
160         for k, v := range resp.Header {
161                 for _, v := range v {
162                         w.Header().Add(k, v)
163                 }
164         }
165         w.WriteHeader(resp.StatusCode)
166         n, err := io.Copy(w, resp.Body)
167         if err != nil {
168                 httpserver.Logger(reqIn).WithError(err).WithField("bytesCopied", n).Error("error copying response body")
169         }
170 }
171
172 // For now, findRailsAPI always uses the rails API running on this
173 // node.
174 func findRailsAPI(cluster *arvados.Cluster, np *arvados.NodeProfile) (*url.URL, error) {
175         hostport := np.RailsAPI.Listen
176         if len(hostport) > 1 && hostport[0] == ':' && strings.TrimRight(hostport[1:], "0123456789") == "" {
177                 // ":12345" => connect to indicated port on localhost
178                 hostport = "localhost" + hostport
179         } else if _, _, err := net.SplitHostPort(hostport); err == nil {
180                 // "[::1]:12345" => connect to indicated address & port
181         } else {
182                 return nil, err
183         }
184         proto := "http"
185         if np.RailsAPI.TLS {
186                 proto = "https"
187         }
188         return url.Parse(proto + "://" + hostport)
189 }