forked from rs/rest-layer
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathutil.go
232 lines (220 loc) · 7.03 KB
/
util.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
package rest
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/rs/rest-layer/resource"
"github.com/rs/rest-layer/schema"
)
// getMethodHandler returns the method handler for a given HTTP method in item or resource mode.
func getMethodHandler(isItem bool, method string) methodHandler {
if isItem {
switch method {
case http.MethodOptions:
return itemOptions
case http.MethodHead, http.MethodGet:
return itemGet
case http.MethodPut:
return itemPut
case http.MethodPatch:
return itemPatch
case http.MethodDelete:
return itemDelete
}
} else {
switch method {
case http.MethodOptions:
return listOptions
case http.MethodHead, http.MethodGet:
return listGet
case http.MethodPost:
return listPost
case http.MethodDelete:
return listDelete
}
}
return nil
}
// isMethodAllowed returns true if the method is allowed by the configuration
func isMethodAllowed(isItem bool, method string, conf resource.Conf) bool {
if isItem {
switch method {
case http.MethodOptions:
return true
case http.MethodHead, http.MethodGet:
return conf.IsModeAllowed(resource.Read)
case http.MethodPut:
return conf.IsModeAllowed(resource.Create) || conf.IsModeAllowed(resource.Replace)
case http.MethodPatch:
return conf.IsModeAllowed(resource.Update)
case http.MethodDelete:
return conf.IsModeAllowed(resource.Delete)
}
} else {
switch method {
case http.MethodOptions:
return true
case http.MethodHead, http.MethodGet:
return conf.IsModeAllowed(resource.List)
case http.MethodPost:
return conf.IsModeAllowed(resource.Create)
case http.MethodDelete:
return conf.IsModeAllowed(resource.Clear)
}
}
return false
}
// getAllowedMethodHandler returns the method handler for the requested method if the resource configuration
// allows it.
func getAllowedMethodHandler(isItem bool, method string, conf resource.Conf) methodHandler {
if isMethodAllowed(isItem, method, conf) {
return getMethodHandler(isItem, method)
}
return nil
}
// setAllowHeader builds a Allow header based on the resource configuration.
func setAllowHeader(headers http.Header, isItem bool, conf resource.Conf) {
methods := []string{}
if isItem {
// Methods are sorted
if conf.IsModeAllowed(resource.Update) {
methods = append(methods, "DELETE")
}
if conf.IsModeAllowed(resource.Read) {
methods = append(methods, "GET, HEAD")
}
if conf.IsModeAllowed(resource.Update) {
methods = append(methods, "PATCH")
// See http://tools.ietf.org/html/rfc5789#section-3
headers.Set("Allow-Patch", "application/json")
}
if conf.IsModeAllowed(resource.Create) || conf.IsModeAllowed(resource.Replace) {
methods = append(methods, "PUT")
}
} else {
// Methods are sorted
if conf.IsModeAllowed(resource.Clear) {
methods = append(methods, "DELETE")
}
if conf.IsModeAllowed(resource.List) {
methods = append(methods, "GET, HEAD")
}
if conf.IsModeAllowed(resource.Create) {
methods = append(methods, "POST")
}
}
if len(methods) > 0 {
headers.Set("Allow", strings.Join(methods, ", "))
}
}
// compareEtag compares a client provided etag with a base etag. The client provided
// etag may or may not have quotes while the base etag is never quoted. This loose
// comparison of etag allows clients not stricly respecting RFC to send the etag with
// or without quotes when the etag comes from, for instance, the API JSON response.
func compareEtag(etag, baseEtag string) bool {
if etag == "" {
return false
}
if etag == baseEtag {
return true
}
if l := len(etag); l == len(baseEtag)+2 && l > 3 && etag[0] == '"' && etag[l-1] == '"' && etag[1:l-1] == baseEtag {
return true
}
return false
}
// decodePayload decodes the payload from the provided request
func decodePayload(r *http.Request, payload *map[string]interface{}) *Error {
// Check content-type, if not specified, assume it's JSON and fail later
if ct := r.Header.Get("Content-Type"); ct != "" && strings.TrimSpace(strings.SplitN(ct, ";", 2)[0]) != "application/json" {
return &Error{501, fmt.Sprintf("Invalid Content-Type header: `%s' not supported", ct), nil}
}
decoder := json.NewDecoder(r.Body)
defer r.Body.Close()
if err := decoder.Decode(payload); err != nil {
return &Error{400, fmt.Sprintf("Malformed body: %v", err), nil}
}
return nil
}
// checkIntegrityRequest ensures that orignal item exists and complies with conditions
// expressed by If-Match and/or If-Unmodified-Since headers if present.
func checkIntegrityRequest(r *http.Request, original *resource.Item) *Error {
ifMatch := r.Header.Get("If-Match")
ifUnmod := r.Header.Get("If-Unmodified-Since")
if ifMatch != "" || ifUnmod != "" {
if original == nil {
return ErrNotFound
}
if ifMatch != "" && !compareEtag(ifMatch, original.ETag) {
return ErrPreconditionFailed
}
if ifUnmod != "" {
if ifUnmodTime, err := time.Parse(time.RFC1123, ifUnmod); err != nil {
return &Error{400, "Invalid If-Unmodified-Since header", nil}
} else if original.Updated.Truncate(time.Second).After(ifUnmodTime) {
// Item's update time is truncated to the second because RFC1123 doesn't support more
return ErrPreconditionFailed
}
}
}
return nil
}
// checkReferences ensures that fields with the Reference validator reference an existing object
func checkReferences(ctx context.Context, payload map[string]interface{}, s schema.Validator) *Error {
for name, value := range payload {
field := s.GetField(name)
if field == nil {
continue
}
// Check reference if validator is of type Reference
if field.Validator != nil {
if ref, ok := field.Validator.(*schema.Reference); ok {
router, ok := IndexFromContext(ctx)
if !ok {
return &Error{500, "Router not available in context", nil}
}
rsrc, found := router.GetResource(ref.Path, nil)
if !found {
return &Error{500, fmt.Sprintf("Invalid resource reference for field `%s': %s", name, ref.Path), nil}
}
_, err := rsrc.Get(ctx, value)
if err == resource.ErrNotFound {
return &Error{404, fmt.Sprintf("Resource reference not found for field `%s'", name), nil}
} else if err != nil {
return &Error{500, fmt.Sprintf("Error fetching resource reference for field `%s': %v", name, err), nil}
}
}
}
// Check sub-schema if any
if field.Schema != nil && value != nil {
if subPayload, ok := value.(map[string]interface{}); ok {
if err := checkReferences(ctx, subPayload, field.Schema); err != nil {
return err
}
}
}
}
return nil
}
func getReferenceResolver(ctx context.Context, r *resource.Resource) resource.ReferenceResolver {
return func(path string) (*resource.Resource, error) {
router, ok := IndexFromContext(ctx)
if !ok {
return nil, errors.New("router not available in context")
}
rsrc, found := router.GetResource(path, r)
if !found {
return nil, fmt.Errorf("invalid resource reference: %s", path)
}
return rsrc, nil
}
}
func logErrorf(ctx context.Context, format string, a ...interface{}) {
if resource.Logger != nil {
resource.Logger(ctx, resource.LogLevelError, fmt.Sprintf(format, a...), nil)
}
}