Merge branch '20318-disk-cache'
[arvados.git] / lib / controller / router / request.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package router
6
7 import (
8         "encoding/json"
9         "fmt"
10         "io"
11         "mime"
12         "net/http"
13         "strconv"
14         "strings"
15
16         "github.com/gorilla/mux"
17 )
18
19 func guessAndParse(k, v string) (interface{}, error) {
20         // All of these form values arrive as strings, so we need some
21         // type-guessing to accept non-string inputs:
22         //
23         // Values for parameters that take ints (limit=1) or bools
24         // (include_trash=1) are parsed accordingly.
25         //
26         // "null" and "" are nil.
27         //
28         // Values that look like JSON objects, arrays, or strings are
29         // parsed as JSON.
30         //
31         // The rest are left as strings.
32         switch {
33         case intParams[k]:
34                 return strconv.ParseInt(v, 10, 64)
35         case boolParams[k]:
36                 return stringToBool(v), nil
37         case v == "null" || v == "":
38                 return nil, nil
39         case strings.HasPrefix(v, "["):
40                 var j []interface{}
41                 err := json.Unmarshal([]byte(v), &j)
42                 return j, err
43         case strings.HasPrefix(v, "{"):
44                 var j map[string]interface{}
45                 err := json.Unmarshal([]byte(v), &j)
46                 return j, err
47         case strings.HasPrefix(v, "\""):
48                 var j string
49                 err := json.Unmarshal([]byte(v), &j)
50                 return j, err
51         default:
52                 return v, nil
53         }
54         // TODO: Need to accept "?foo[]=bar&foo[]=baz" as
55         // foo=["bar","baz"]?
56 }
57
58 // Return a map of incoming HTTP request parameters. Also load
59 // parameters into opts, unless opts is nil.
60 //
61 // If the request has a parameter whose name is attrsKey (e.g.,
62 // "collection"), it is renamed to "attrs".
63 func (rtr *router) loadRequestParams(req *http.Request, attrsKey string, opts interface{}) (map[string]interface{}, error) {
64         // Here we call ParseForm and ParseMultipartForm explicitly
65         // (even though ParseMultipartForm calls ParseForm if
66         // necessary) to ensure we catch errors encountered in
67         // ParseForm. In the non-multipart-form case,
68         // ParseMultipartForm returns ErrNotMultipart and hides the
69         // ParseForm error.
70         err := req.ParseForm()
71         if err == nil {
72                 err = req.ParseMultipartForm(int64(rtr.config.MaxRequestSize))
73                 if err == http.ErrNotMultipart {
74                         err = nil
75                 }
76         }
77         if err != nil {
78                 if err.Error() == "http: request body too large" {
79                         return nil, httpError(http.StatusRequestEntityTooLarge, err)
80                 } else {
81                         return nil, httpError(http.StatusBadRequest, err)
82                 }
83         }
84         params := map[string]interface{}{}
85
86         // Load parameters from req.Form, which (after
87         // req.ParseForm()) includes the query string and -- when
88         // Content-Type is application/x-www-form-urlencoded -- the
89         // request body.
90         for k, values := range req.Form {
91                 for _, v := range values {
92                         params[k], err = guessAndParse(k, v)
93                         if err != nil {
94                                 return nil, err
95                         }
96                 }
97         }
98
99         // Decode body as JSON if Content-Type request header is
100         // missing or application/json.
101         mt := req.Header.Get("Content-Type")
102         if ct, _, err := mime.ParseMediaType(mt); err != nil && mt != "" {
103                 return nil, fmt.Errorf("error parsing media type %q: %s", mt, err)
104         } else if (ct == "application/json" || mt == "") && req.ContentLength != 0 {
105                 jsonParams := map[string]interface{}{}
106                 err := json.NewDecoder(req.Body).Decode(&jsonParams)
107                 if err != nil {
108                         return nil, httpError(http.StatusBadRequest, err)
109                 }
110                 for k, v := range jsonParams {
111                         switch v := v.(type) {
112                         case string:
113                                 // The Ruby "arv" cli tool sends a
114                                 // JSON-encode params map with
115                                 // JSON-encoded values.
116                                 dec, err := guessAndParse(k, v)
117                                 if err != nil {
118                                         return nil, err
119                                 }
120                                 jsonParams[k] = dec
121                                 params[k] = dec
122                         default:
123                                 params[k] = v
124                         }
125                 }
126                 if attrsKey != "" && params[attrsKey] == nil {
127                         // Copy top-level parameters from JSON request
128                         // body into params[attrsKey]. Some SDKs rely
129                         // on this Rails API feature; see
130                         // https://api.rubyonrails.org/v5.2.1/classes/ActionController/ParamsWrapper.html
131                         params[attrsKey] = jsonParams
132                 }
133         }
134
135         for k, v := range mux.Vars(req) {
136                 params[k] = v
137         }
138
139         if v, ok := params[attrsKey]; ok && attrsKey != "" {
140                 params["attrs"] = v
141                 delete(params, attrsKey)
142         }
143
144         if order, ok := params["order"].(string); ok {
145                 // We must accept strings ("foo, bar desc") and arrays
146                 // (["foo", "bar desc"]) because RailsAPI does.
147                 // Convert to an array here before trying to unmarshal
148                 // into options structs.
149                 if order == "" {
150                         delete(params, "order")
151                 } else {
152                         params["order"] = strings.Split(order, ",")
153                 }
154         }
155
156         if opts != nil {
157                 // Load all path, query, and form params into opts.
158                 err = rtr.transcode(params, opts)
159                 if err != nil {
160                         return nil, fmt.Errorf("transcode: %w", err)
161                 }
162
163                 // Special case: if opts has Method or Header fields, load the
164                 // request method/header.
165                 err = rtr.transcode(struct {
166                         Method string
167                         Header http.Header
168                 }{req.Method, req.Header}, opts)
169                 if err != nil {
170                         return nil, fmt.Errorf("transcode: %w", err)
171                 }
172         }
173
174         return params, nil
175 }
176
177 // Copy src to dst, using json as an intermediate format in order to
178 // invoke src's json-marshaling and dst's json-unmarshaling behaviors.
179 func (rtr *router) transcode(src interface{}, dst interface{}) error {
180         var errw error
181         pr, pw := io.Pipe()
182         go func() {
183                 defer pw.Close()
184                 errw = json.NewEncoder(pw).Encode(src)
185         }()
186         defer pr.Close()
187         err := json.NewDecoder(pr).Decode(dst)
188         if errw != nil {
189                 return errw
190         }
191         return err
192 }
193
194 var intParams = map[string]bool{
195         "limit":  true,
196         "offset": true,
197 }
198
199 var boolParams = map[string]bool{
200         "distinct":                true,
201         "ensure_unique_name":      true,
202         "include_trash":           true,
203         "include_old_versions":    true,
204         "redirect_to_new_user":    true,
205         "send_notification_email": true,
206         "bypass_federation":       true,
207         "recursive":               true,
208         "exclude_home_project":    true,
209         "no_forward":              true,
210 }
211
212 func stringToBool(s string) bool {
213         switch s {
214         case "", "false", "0":
215                 return false
216         default:
217                 return true
218         }
219 }