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 --------------