...

Source file src/github.com/docker/distribution/registry/api/v2/routes_test.go

Documentation: github.com/docker/distribution/registry/api/v2

     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  // TestRouter registers a test handler with all the routes and ensures that
    25  // each route returns the expected path variables. Not method verification is
    26  // present. This not meant to be exhaustive but as check to ensure that the
    27  // expected variables are extracted.
    28  //
    29  // This may go away as the application structure comes together.
    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  			// support uuid proper
   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  			// supports urlsafe base64
   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  			// does not match
   140  			RouteName:  RouteNameBlobUploadChunk,
   141  			RequestURI: "/v2/foo/bar/blobs/uploads/totalandcompletejunk++$$-==",
   142  			StatusCode: http.StatusNotFound,
   143  		},
   144  		{
   145  			// Check ambiguity: ensure we can distinguish between tags for
   146  			// "foo/bar/image/image" and image for "foo/bar/image" with tag
   147  			// "tags"
   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  			// This case presents an ambiguity between foo/bar with tag="tags"
   157  			// and list tags for "foo/bar/manifest"
   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  			// Testing for path traversal attack handling
   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  				// Testing for path traversal attack handling
   209  				RouteName:  RouteNameTags,
   210  				RequestURI: "/v2/foo/不bar/tags/list",
   211  				StatusCode: http.StatusNotFound,
   212  			},
   213  		}
   214  		checkTestRouter(t, testCases, "", true)
   215  	} else {
   216  		// in the long version we're going to fuzz the router
   217  		// with random UTF8 characters not in the 128 bit ASCII range.
   218  		// These are not valid characters for the router and we expect
   219  		// 404s on every test.
   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  	// Startup test server
   251  	server := httptest.NewServer(router)
   252  
   253  	for _, testcase := range testCases {
   254  		testcase.RequestURI = strings.TrimSuffix(prefix, "/") + testcase.RequestURI
   255  		// Register the endpoint
   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  			// Override default, zero-value
   273  			testcase.StatusCode = http.StatusOK
   274  		}
   275  		if testcase.ExpectedURI == "" {
   276  			// Override default, zero-value
   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  			// We don't care about json response.
   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  		// Needs to be set out of band
   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  		// when testing deep equality, the actualRouteInfo has an empty ExpectedURI, we don't want
   308  		// that to make the comparison fail. We're otherwise done with the testcase so empty the
   309  		// testcase.ExpectedURI
   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  // -------------- START LICENSED CODE --------------
   321  // The following code is derivative of https://github.com/google/gofuzz
   322  // gofuzz is licensed under the Apache License, Version 2.0, January 2004,
   323  // a copy of which can be found in the LICENSE file at the root of this
   324  // repository.
   325  
   326  // These functions allow us to generate strings containing only multibyte
   327  // characters that are invalid in our URLs. They are used above for fuzzing
   328  // to ensure we always get 404s on these invalid strings
   329  type charRange struct {
   330  	first, last rune
   331  }
   332  
   333  // choose returns a random unicode character from the given range, using the
   334  // given randomness source.
   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'}, // Multi-byte encoded characters
   342  	{'\u4e00', '\u9fff'}, // Common CJK (even longer encodings)
   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  // -------------- END LICENSED CODE --------------
   354  

View as plain text