-
Notifications
You must be signed in to change notification settings - Fork 0
/
hydrate.go
285 lines (232 loc) · 7.94 KB
/
hydrate.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
package mid
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
"github.com/gorilla/schema"
"github.com/julienschmidt/httprouter"
)
const (
// TagQuery is the field tag for looking up Query Parameters
TagQuery = "query"
// TagParam is the field tag for looking up URL Parameters
TagParam = "param"
)
// ValidationErrorMessage sent to client on validation fail
var ValidationErrorMessage = "Invalid Request"
// "A JSON response should contain either a data object or an error object,
// but not both. If both data and error are present, the error object takes
// precedence." - https://google.github.io/styleguide/jsoncstyleguide.xml
// JSONResponse for validation errors or service responses
type JSONResponse struct {
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
Fields map[string]string `json:"fields,omitempty"`
}
// Hydrate and validate a http.Handler with input from HTTP GET/POST requests
func Hydrate(function interface{}) http.HandlerFunc {
// Improve performance (and clarity) by pre-computing needed variables
funcType := reflect.TypeOf(function)
if funcType.Kind() != reflect.Func {
panic(fmt.Errorf("wrap was called with a non-function type: %+v", funcType))
}
funcValue := reflect.ValueOf(function)
if funcType.NumIn() != 3 {
panic(errors.New("mid.Hydrate expects handler to have three arguments"))
}
if funcType.NumOut() == 0 {
panic(errors.New("mid.Hydrate expects handler to return (error) or ({interface}, error)"))
}
// TODO more error checking here
paramType := funcType.In(2)
if paramType.Kind() == reflect.Ptr {
panic(errors.New("mid.Hydrate expect handler input use struct params, not struct pointers"))
}
structType := paramType
if paramType.Kind() == reflect.Ptr {
structType = paramType.Elem()
}
// We can't detect the router type until the first request
// 0 = false, 1 = true, 2 = not checked
isHttpRouter := 2
isGorillaMux := 2
// Abstraction for fetching from either router's parameters
var params func(name string) string
// Cache setup finished, now get ready to process requests
return func(w http.ResponseWriter, r *http.Request) {
if isHttpRouter == 2 {
if httprouter.ParamsFromContext(r.Context()) != nil {
isHttpRouter = 1
isGorillaMux = 0
} else {
isHttpRouter = 0
}
}
if isGorillaMux == 2 {
if mux.Vars(r) != nil {
isGorillaMux = 1
isHttpRouter = 0
}
}
if isHttpRouter == 1 {
vars := httprouter.ParamsFromContext(r.Context())
params = func(name string) string {
return vars.ByName(name)
}
} else if isGorillaMux == 1 {
vars := mux.Vars(r)
params = func(name string) string {
if param, ok := vars[name]; ok {
return param
}
return ""
}
} else {
params = func(string) string {
return ""
}
}
// Struct pointers are not allowed, so create a pointer to the struct
object := newReflectType(paramType).Elem()
oi := object.Addr().Interface()
if r.Method == "POST" {
// TODO: this is the job of a middleware
// Limit the size of the request body to avoid a DOS with a large nested
// JSON structure: https://golang.org/src/net/http/request.go#L1148
// r := io.LimitReader(r.Body, MaxBodySize)
// TODO should we check the r.Body type to see if it's a LimitedReader?
if r.Header.Get("Content-Type") == "application/json" {
// We don't care about JSON type errors nor want to give app details out
// The validator will handle those messages better below
_ = json.NewDecoder(r.Body).Decode(oi)
// The validator will handle those messages better below
// if err != nil {
// switch err.(type) {
// // json: cannot unmarshal string into Go struct field A.Foo of type foo.Bar
// case *json.UnmarshalTypeError:
// // fmt.Printf("Decoded JSON: %+v\n", b)
// // err = fmt.Errorf("JSON: Unexpected type '%s' for field '%s'", e.Value, e.Field)
// // log.Printf("UnmarshalTypeError: Value[%s] Type[%v]\n", e.Value, e.Type)
// case *json.InvalidUnmarshalError:
// // log.Printf("InvalidUnmarshalError: Type[%v]\n", e.Type)
// // unexpected EOF
// default:
// // We could just ignore all JSON errors like we do with gorilla/schema
// // However, JSON errors should be rare and could make development
// // a lot harder if something weird happens. Better alert the client.
// // return fmt.Errorf("Invalid JSON: %s", err.Error()), validation
// return
// }
// }
} else {
if r.Header.Get("Content-Type") == "multipart/form-data" {
// 10MB: https://golang.org/src/net/http/request.go#L1137
_ = r.ParseMultipartForm(int64(10 << 20))
} else {
// application/x-www-form-urlencoded
r.ParseForm()
}
// 1. Try to insert form data into the struct
decoder := schema.NewDecoder()
// A) Developer forgot about a field
// B) Client is messing with the request fields
decoder.IgnoreUnknownKeys(true)
// Edge Case: https://github.com/gorilla/schema/blob/master/decoder.go#L203
// "schema: converter not found for..."
// gorilla/schema errors share application handler structure which is
// not safe for us, nor helpful to our clients
decoder.Decode(oi, r.Form)
}
}
// TODO: decoding order
// the encoding/json package does not overwrite existing struct values.
// (Proof: https://play.golang.org/p/aMEN66_P75w)
// However, we seem to be having issues with overwriting so we are placing
// the URL parameter & Query parameter parsing code *after* the body parsing
// This seems like an issue; attackers could craft query strings that would
// change what the client was posting. CSRF possible?
queryValues := r.URL.Query()
for j := 0; j < structType.NumField(); j++ {
field := structType.Field(j)
var s string
var location string
// Look in the route parameters first
if tag, ok := field.Tag.Lookup(TagParam); ok {
s = params(tag)
location = "Route Parameter"
} else if tag, ok := field.Tag.Lookup(TagQuery); ok {
s = queryValues.Get(tag)
location = "Query Parameter"
} else {
// Consider this case when deciding decoding order.
// In the two conditions above, we require the authors consideration.
// This is automatic and therefore more unsafe/unexpected.
s = queryValues.Get(field.Name)
location = "Query Parameter"
}
if s == "" {
continue
}
val := object.Field(j)
// TODO remove error handling since we only use govalidator's messages
err := parseSimpleParam(s, location, field, &val)
if err != nil {
// TODO ignore this since validation will handle this error
// Skip the rest of the input since this one field is invalid
// Saves resources - but produces less-useful error messages
break
}
}
// 2. Validate the struct data rules
isValid, err := govalidator.ValidateStruct(oi)
if !isValid {
validationErrors := govalidator.ErrorsByField(err)
// https://gist.github.com/Xeoncross/e592755a1e5fecf6a1cc25fc593b1239
// w.WriteHeader(http.StatusBadRequest)
JSON(w, http.StatusOK, JSONResponse{
Error: ValidationErrorMessage,
Fields: validationErrors,
})
return
}
in := []reflect.Value{
reflect.ValueOf(w),
reflect.ValueOf(r),
object,
}
response := funcValue.Call(in)
// Expect all service methods in one of two forms:
// func (...) error
// func (...) (interface{}, error)
ek := 0
if funcType.NumOut() == 2 {
ek = 1
}
if err, ok := response[ek].Interface().(error); ok {
if err != nil {
// http.Error(w, err.Error(), http.StatusBadRequest)
JSON(w, http.StatusOK, JSONResponse{
Error: err.Error(),
})
return
}
}
if ek == 0 {
return
}
JSON(w, http.StatusOK, JSONResponse{
Data: response[0].Interface(),
})
}
}
func newReflectType(t reflect.Type) reflect.Value {
// Dereference pointers
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
return reflect.New(t)
}