PageRenderTime 961ms CodeModel.GetById 39ms RepoModel.GetById 1ms app.codeStats 0ms

/registry/api/v2/routes_test.go

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