1 package v2
2
3 import (
4 "encoding/json"
5 "fmt"
6 "math/rand"
7 "net/http"
8 "net/http/httptest"
9 "reflect"
10 "strings"
11 "testing"
12
13 "github.com/gorilla/mux"
14 )
15
16 type routeTestCase struct {
17 RequestURI string
18 ExpectedURI string
19 Vars map[string]string
20 RouteName string
21 StatusCode int
22 }
23
24
25
26
27
28
29
30 func TestRouter(t *testing.T) {
31 testCases := []routeTestCase{
32 {
33 RouteName: RouteNameBase,
34 RequestURI: "/v2/",
35 Vars: map[string]string{},
36 },
37 {
38 RouteName: RouteNameManifest,
39 RequestURI: "/v2/foo/manifests/bar",
40 Vars: map[string]string{
41 "name": "foo",
42 "reference": "bar",
43 },
44 },
45 {
46 RouteName: RouteNameManifest,
47 RequestURI: "/v2/foo/bar/manifests/tag",
48 Vars: map[string]string{
49 "name": "foo/bar",
50 "reference": "tag",
51 },
52 },
53 {
54 RouteName: RouteNameManifest,
55 RequestURI: "/v2/foo/bar/manifests/sha256:abcdef01234567890",
56 Vars: map[string]string{
57 "name": "foo/bar",
58 "reference": "sha256:abcdef01234567890",
59 },
60 },
61 {
62 RouteName: RouteNameTags,
63 RequestURI: "/v2/foo/bar/tags/list",
64 Vars: map[string]string{
65 "name": "foo/bar",
66 },
67 },
68 {
69 RouteName: RouteNameTags,
70 RequestURI: "/v2/docker.com/foo/tags/list",
71 Vars: map[string]string{
72 "name": "docker.com/foo",
73 },
74 },
75 {
76 RouteName: RouteNameTags,
77 RequestURI: "/v2/docker.com/foo/bar/tags/list",
78 Vars: map[string]string{
79 "name": "docker.com/foo/bar",
80 },
81 },
82 {
83 RouteName: RouteNameTags,
84 RequestURI: "/v2/docker.com/foo/bar/baz/tags/list",
85 Vars: map[string]string{
86 "name": "docker.com/foo/bar/baz",
87 },
88 },
89 {
90 RouteName: RouteNameBlob,
91 RequestURI: "/v2/foo/bar/blobs/sha256:abcdef0919234",
92 Vars: map[string]string{
93 "name": "foo/bar",
94 "digest": "sha256:abcdef0919234",
95 },
96 },
97 {
98 RouteName: RouteNameBlobUpload,
99 RequestURI: "/v2/foo/bar/blobs/uploads/",
100 Vars: map[string]string{
101 "name": "foo/bar",
102 },
103 },
104 {
105 RouteName: RouteNameBlobUploadChunk,
106 RequestURI: "/v2/foo/bar/blobs/uploads/uuid",
107 Vars: map[string]string{
108 "name": "foo/bar",
109 "uuid": "uuid",
110 },
111 },
112 {
113
114 RouteName: RouteNameBlobUploadChunk,
115 RequestURI: "/v2/foo/bar/blobs/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
116 Vars: map[string]string{
117 "name": "foo/bar",
118 "uuid": "D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
119 },
120 },
121 {
122 RouteName: RouteNameBlobUploadChunk,
123 RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==",
124 Vars: map[string]string{
125 "name": "foo/bar",
126 "uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==",
127 },
128 },
129 {
130
131 RouteName: RouteNameBlobUploadChunk,
132 RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA_-==",
133 Vars: map[string]string{
134 "name": "foo/bar",
135 "uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA_-==",
136 },
137 },
138 {
139
140 RouteName: RouteNameBlobUploadChunk,
141 RequestURI: "/v2/foo/bar/blobs/uploads/totalandcompletejunk++$$-==",
142 StatusCode: http.StatusNotFound,
143 },
144 {
145
146
147
148 RouteName: RouteNameManifest,
149 RequestURI: "/v2/foo/bar/manifests/manifests/tags",
150 Vars: map[string]string{
151 "name": "foo/bar/manifests",
152 "reference": "tags",
153 },
154 },
155 {
156
157
158 RouteName: RouteNameTags,
159 RequestURI: "/v2/foo/bar/manifests/tags/list",
160 Vars: map[string]string{
161 "name": "foo/bar/manifests",
162 },
163 },
164 {
165 RouteName: RouteNameManifest,
166 RequestURI: "/v2/locahost:8080/foo/bar/baz/manifests/tag",
167 Vars: map[string]string{
168 "name": "locahost:8080/foo/bar/baz",
169 "reference": "tag",
170 },
171 },
172 }
173
174 checkTestRouter(t, testCases, "", true)
175 checkTestRouter(t, testCases, "/prefix/", true)
176 }
177
178 func TestRouterWithPathTraversals(t *testing.T) {
179 testCases := []routeTestCase{
180 {
181 RouteName: RouteNameBlobUploadChunk,
182 RequestURI: "/v2/foo/../../blobs/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
183 ExpectedURI: "/blobs/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
184 StatusCode: http.StatusNotFound,
185 },
186 {
187
188 RouteName: RouteNameTags,
189 RequestURI: "/v2/foo/../bar/baz/tags/list",
190 ExpectedURI: "/v2/bar/baz/tags/list",
191 Vars: map[string]string{
192 "name": "bar/baz",
193 },
194 },
195 }
196 checkTestRouter(t, testCases, "", false)
197 }
198
199 func TestRouterWithBadCharacters(t *testing.T) {
200 if testing.Short() {
201 testCases := []routeTestCase{
202 {
203 RouteName: RouteNameBlobUploadChunk,
204 RequestURI: "/v2/foo/blobs/uploads/不95306FA-FAD3-4E36-8D41-CF1C93EF8286",
205 StatusCode: http.StatusNotFound,
206 },
207 {
208
209 RouteName: RouteNameTags,
210 RequestURI: "/v2/foo/不bar/tags/list",
211 StatusCode: http.StatusNotFound,
212 },
213 }
214 checkTestRouter(t, testCases, "", true)
215 } else {
216
217
218
219
220 testCases := make([]routeTestCase, 1000)
221 for idx := range testCases {
222 testCases[idx] = routeTestCase{
223 RouteName: RouteNameTags,
224 RequestURI: fmt.Sprintf("/v2/%v/%v/tags/list", randomString(10), randomString(10)),
225 StatusCode: http.StatusNotFound,
226 }
227 }
228 checkTestRouter(t, testCases, "", true)
229 }
230 }
231
232 func checkTestRouter(t *testing.T, testCases []routeTestCase, prefix string, deeplyEqual bool) {
233 router := RouterWithPrefix(prefix)
234
235 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
236 testCase := routeTestCase{
237 RequestURI: r.RequestURI,
238 Vars: mux.Vars(r),
239 RouteName: mux.CurrentRoute(r).GetName(),
240 }
241
242 enc := json.NewEncoder(w)
243
244 if err := enc.Encode(testCase); err != nil {
245 http.Error(w, err.Error(), http.StatusInternalServerError)
246 return
247 }
248 })
249
250
251 server := httptest.NewServer(router)
252
253 for _, testcase := range testCases {
254 testcase.RequestURI = strings.TrimSuffix(prefix, "/") + testcase.RequestURI
255
256 route := router.GetRoute(testcase.RouteName)
257 if route == nil {
258 t.Fatalf("route for name %q not found", testcase.RouteName)
259 }
260
261 route.Handler(testHandler)
262
263 u := server.URL + testcase.RequestURI
264
265 resp, err := http.Get(u)
266
267 if err != nil {
268 t.Fatalf("error issuing get request: %v", err)
269 }
270
271 if testcase.StatusCode == 0 {
272
273 testcase.StatusCode = http.StatusOK
274 }
275 if testcase.ExpectedURI == "" {
276
277 testcase.ExpectedURI = testcase.RequestURI
278 }
279
280 if resp.StatusCode != testcase.StatusCode {
281 t.Fatalf("unexpected status for %s: %v %v", u, resp.Status, resp.StatusCode)
282 }
283
284 if testcase.StatusCode != http.StatusOK {
285 resp.Body.Close()
286
287 continue
288 }
289
290 dec := json.NewDecoder(resp.Body)
291
292 var actualRouteInfo routeTestCase
293 if err := dec.Decode(&actualRouteInfo); err != nil {
294 t.Fatalf("error reading json response: %v", err)
295 }
296
297 actualRouteInfo.StatusCode = resp.StatusCode
298
299 if actualRouteInfo.RequestURI != testcase.ExpectedURI {
300 t.Fatalf("URI %v incorrectly parsed, expected %v", actualRouteInfo.RequestURI, testcase.ExpectedURI)
301 }
302
303 if actualRouteInfo.RouteName != testcase.RouteName {
304 t.Fatalf("incorrect route %q matched, expected %q", actualRouteInfo.RouteName, testcase.RouteName)
305 }
306
307
308
309
310 testcase.ExpectedURI = ""
311 if deeplyEqual && !reflect.DeepEqual(actualRouteInfo, testcase) {
312 t.Fatalf("actual does not equal expected: %#v != %#v", actualRouteInfo, testcase)
313 }
314
315 resp.Body.Close()
316 }
317
318 }
319
320
321
322
323
324
325
326
327
328
329 type charRange struct {
330 first, last rune
331 }
332
333
334
335 func (r *charRange) choose() rune {
336 count := int64(r.last - r.first)
337 return r.first + rune(rand.Int63n(count))
338 }
339
340 var unicodeRanges = []charRange{
341 {'\u00a0', '\u02af'},
342 {'\u4e00', '\u9fff'},
343 }
344
345 func randomString(length int) string {
346 runes := make([]rune, length)
347 for i := range runes {
348 runes[i] = unicodeRanges[rand.Intn(len(unicodeRanges))].choose()
349 }
350 return string(runes)
351 }
352
353
354
View as plain text