github.com/mika/distribution@v2.2.2-0.20160108133430-a75790e3d8e0+incompatible/registry/api/v2/routes_test.go (about) 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 "time" 13 14 "github.com/gorilla/mux" 15 ) 16 17 type routeTestCase struct { 18 RequestURI string 19 ExpectedURI string 20 Vars map[string]string 21 RouteName string 22 StatusCode int 23 } 24 25 // TestRouter registers a test handler with all the routes and ensures that 26 // each route returns the expected path variables. Not method verification is 27 // present. This not meant to be exhaustive but as check to ensure that the 28 // expected variables are extracted. 29 // 30 // This may go away as the application structure comes together. 31 func TestRouter(t *testing.T) { 32 testCases := []routeTestCase{ 33 { 34 RouteName: RouteNameBase, 35 RequestURI: "/v2/", 36 Vars: map[string]string{}, 37 }, 38 { 39 RouteName: RouteNameManifest, 40 RequestURI: "/v2/foo/manifests/bar", 41 Vars: map[string]string{ 42 "name": "foo", 43 "reference": "bar", 44 }, 45 }, 46 { 47 RouteName: RouteNameManifest, 48 RequestURI: "/v2/foo/bar/manifests/tag", 49 Vars: map[string]string{ 50 "name": "foo/bar", 51 "reference": "tag", 52 }, 53 }, 54 { 55 RouteName: RouteNameManifest, 56 RequestURI: "/v2/foo/bar/manifests/sha256:abcdef01234567890", 57 Vars: map[string]string{ 58 "name": "foo/bar", 59 "reference": "sha256:abcdef01234567890", 60 }, 61 }, 62 { 63 RouteName: RouteNameTags, 64 RequestURI: "/v2/foo/bar/tags/list", 65 Vars: map[string]string{ 66 "name": "foo/bar", 67 }, 68 }, 69 { 70 RouteName: RouteNameTags, 71 RequestURI: "/v2/docker.com/foo/tags/list", 72 Vars: map[string]string{ 73 "name": "docker.com/foo", 74 }, 75 }, 76 { 77 RouteName: RouteNameTags, 78 RequestURI: "/v2/docker.com/foo/bar/tags/list", 79 Vars: map[string]string{ 80 "name": "docker.com/foo/bar", 81 }, 82 }, 83 { 84 RouteName: RouteNameTags, 85 RequestURI: "/v2/docker.com/foo/bar/baz/tags/list", 86 Vars: map[string]string{ 87 "name": "docker.com/foo/bar/baz", 88 }, 89 }, 90 { 91 RouteName: RouteNameBlob, 92 RequestURI: "/v2/foo/bar/blobs/sha256:abcdef0919234", 93 Vars: map[string]string{ 94 "name": "foo/bar", 95 "digest": "sha256:abcdef0919234", 96 }, 97 }, 98 { 99 RouteName: RouteNameBlobUpload, 100 RequestURI: "/v2/foo/bar/blobs/uploads/", 101 Vars: map[string]string{ 102 "name": "foo/bar", 103 }, 104 }, 105 { 106 RouteName: RouteNameBlobUploadChunk, 107 RequestURI: "/v2/foo/bar/blobs/uploads/uuid", 108 Vars: map[string]string{ 109 "name": "foo/bar", 110 "uuid": "uuid", 111 }, 112 }, 113 { 114 // support uuid proper 115 RouteName: RouteNameBlobUploadChunk, 116 RequestURI: "/v2/foo/bar/blobs/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286", 117 Vars: map[string]string{ 118 "name": "foo/bar", 119 "uuid": "D95306FA-FAD3-4E36-8D41-CF1C93EF8286", 120 }, 121 }, 122 { 123 RouteName: RouteNameBlobUploadChunk, 124 RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==", 125 Vars: map[string]string{ 126 "name": "foo/bar", 127 "uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==", 128 }, 129 }, 130 { 131 // supports urlsafe base64 132 RouteName: RouteNameBlobUploadChunk, 133 RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA_-==", 134 Vars: map[string]string{ 135 "name": "foo/bar", 136 "uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA_-==", 137 }, 138 }, 139 { 140 // does not match 141 RouteName: RouteNameBlobUploadChunk, 142 RequestURI: "/v2/foo/bar/blobs/uploads/totalandcompletejunk++$$-==", 143 StatusCode: http.StatusNotFound, 144 }, 145 { 146 // Check ambiguity: ensure we can distinguish between tags for 147 // "foo/bar/image/image" and image for "foo/bar/image" with tag 148 // "tags" 149 RouteName: RouteNameManifest, 150 RequestURI: "/v2/foo/bar/manifests/manifests/tags", 151 Vars: map[string]string{ 152 "name": "foo/bar/manifests", 153 "reference": "tags", 154 }, 155 }, 156 { 157 // This case presents an ambiguity between foo/bar with tag="tags" 158 // and list tags for "foo/bar/manifest" 159 RouteName: RouteNameTags, 160 RequestURI: "/v2/foo/bar/manifests/tags/list", 161 Vars: map[string]string{ 162 "name": "foo/bar/manifests", 163 }, 164 }, 165 { 166 RouteName: RouteNameManifest, 167 RequestURI: "/v2/locahost:8080/foo/bar/baz/manifests/tag", 168 Vars: map[string]string{ 169 "name": "locahost:8080/foo/bar/baz", 170 "reference": "tag", 171 }, 172 }, 173 } 174 175 checkTestRouter(t, testCases, "", true) 176 checkTestRouter(t, testCases, "/prefix/", true) 177 } 178 179 func TestRouterWithPathTraversals(t *testing.T) { 180 testCases := []routeTestCase{ 181 { 182 RouteName: RouteNameBlobUploadChunk, 183 RequestURI: "/v2/foo/../../blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286", 184 ExpectedURI: "/blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286", 185 StatusCode: http.StatusNotFound, 186 }, 187 { 188 // Testing for path traversal attack handling 189 RouteName: RouteNameTags, 190 RequestURI: "/v2/foo/../bar/baz/tags/list", 191 ExpectedURI: "/v2/bar/baz/tags/list", 192 Vars: map[string]string{ 193 "name": "bar/baz", 194 }, 195 }, 196 } 197 checkTestRouter(t, testCases, "", false) 198 } 199 200 func TestRouterWithBadCharacters(t *testing.T) { 201 if testing.Short() { 202 testCases := []routeTestCase{ 203 { 204 RouteName: RouteNameBlobUploadChunk, 205 RequestURI: "/v2/foo/blob/uploads/不95306FA-FAD3-4E36-8D41-CF1C93EF8286", 206 StatusCode: http.StatusNotFound, 207 }, 208 { 209 // Testing for path traversal attack handling 210 RouteName: RouteNameTags, 211 RequestURI: "/v2/foo/不bar/tags/list", 212 StatusCode: http.StatusNotFound, 213 }, 214 } 215 checkTestRouter(t, testCases, "", true) 216 } else { 217 // in the long version we're going to fuzz the router 218 // with random UTF8 characters not in the 128 bit ASCII range. 219 // These are not valid characters for the router and we expect 220 // 404s on every test. 221 rand.Seed(time.Now().UTC().UnixNano()) 222 testCases := make([]routeTestCase, 1000) 223 for idx := range testCases { 224 testCases[idx] = routeTestCase{ 225 RouteName: RouteNameTags, 226 RequestURI: fmt.Sprintf("/v2/%v/%v/tags/list", randomString(10), randomString(10)), 227 StatusCode: http.StatusNotFound, 228 } 229 } 230 checkTestRouter(t, testCases, "", true) 231 } 232 } 233 234 func checkTestRouter(t *testing.T, testCases []routeTestCase, prefix string, deeplyEqual bool) { 235 router := RouterWithPrefix(prefix) 236 237 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 238 testCase := routeTestCase{ 239 RequestURI: r.RequestURI, 240 Vars: mux.Vars(r), 241 RouteName: mux.CurrentRoute(r).GetName(), 242 } 243 244 enc := json.NewEncoder(w) 245 246 if err := enc.Encode(testCase); err != nil { 247 http.Error(w, err.Error(), http.StatusInternalServerError) 248 return 249 } 250 }) 251 252 // Startup test server 253 server := httptest.NewServer(router) 254 255 for _, testcase := range testCases { 256 testcase.RequestURI = strings.TrimSuffix(prefix, "/") + testcase.RequestURI 257 // Register the endpoint 258 route := router.GetRoute(testcase.RouteName) 259 if route == nil { 260 t.Fatalf("route for name %q not found", testcase.RouteName) 261 } 262 263 route.Handler(testHandler) 264 265 u := server.URL + testcase.RequestURI 266 267 resp, err := http.Get(u) 268 269 if err != nil { 270 t.Fatalf("error issuing get request: %v", err) 271 } 272 273 if testcase.StatusCode == 0 { 274 // Override default, zero-value 275 testcase.StatusCode = http.StatusOK 276 } 277 if testcase.ExpectedURI == "" { 278 // Override default, zero-value 279 testcase.ExpectedURI = testcase.RequestURI 280 } 281 282 if resp.StatusCode != testcase.StatusCode { 283 t.Fatalf("unexpected status for %s: %v %v", u, resp.Status, resp.StatusCode) 284 } 285 286 if testcase.StatusCode != http.StatusOK { 287 resp.Body.Close() 288 // We don't care about json response. 289 continue 290 } 291 292 dec := json.NewDecoder(resp.Body) 293 294 var actualRouteInfo routeTestCase 295 if err := dec.Decode(&actualRouteInfo); err != nil { 296 t.Fatalf("error reading json response: %v", err) 297 } 298 // Needs to be set out of band 299 actualRouteInfo.StatusCode = resp.StatusCode 300 301 if actualRouteInfo.RequestURI != testcase.ExpectedURI { 302 t.Fatalf("URI %v incorrectly parsed, expected %v", actualRouteInfo.RequestURI, testcase.ExpectedURI) 303 } 304 305 if actualRouteInfo.RouteName != testcase.RouteName { 306 t.Fatalf("incorrect route %q matched, expected %q", actualRouteInfo.RouteName, testcase.RouteName) 307 } 308 309 // when testing deep equality, the actualRouteInfo has an empty ExpectedURI, we don't want 310 // that to make the comparison fail. We're otherwise done with the testcase so empty the 311 // testcase.ExpectedURI 312 testcase.ExpectedURI = "" 313 if deeplyEqual && !reflect.DeepEqual(actualRouteInfo, testcase) { 314 t.Fatalf("actual does not equal expected: %#v != %#v", actualRouteInfo, testcase) 315 } 316 317 resp.Body.Close() 318 } 319 320 } 321 322 // -------------- START LICENSED CODE -------------- 323 // The following code is derivative of https://github.com/google/gofuzz 324 // gofuzz is licensed under the Apache License, Version 2.0, January 2004, 325 // a copy of which can be found in the LICENSE file at the root of this 326 // repository. 327 328 // These functions allow us to generate strings containing only multibyte 329 // characters that are invalid in our URLs. They are used above for fuzzing 330 // to ensure we always get 404s on these invalid strings 331 type charRange struct { 332 first, last rune 333 } 334 335 // choose returns a random unicode character from the given range, using the 336 // given randomness source. 337 func (r *charRange) choose() rune { 338 count := int64(r.last - r.first) 339 return r.first + rune(rand.Int63n(count)) 340 } 341 342 var unicodeRanges = []charRange{ 343 {'\u00a0', '\u02af'}, // Multi-byte encoded characters 344 {'\u4e00', '\u9fff'}, // Common CJK (even longer encodings) 345 } 346 347 func randomString(length int) string { 348 runes := make([]rune, length) 349 for i := range runes { 350 runes[i] = unicodeRanges[rand.Intn(len(unicodeRanges))].choose() 351 } 352 return string(runes) 353 } 354 355 // -------------- END LICENSED CODE --------------