diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-23 12:09:33 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-23 12:09:33 +0000 |
commit | b38fc20ae0e90d5b1c538a139aa0a7da1b7b5726 (patch) | |
tree | 3ce77cdb707b75c9d74c6ff2a8386dd06bd48b44 /workhorse | |
parent | b3647b2a67930e8aa3c1b1dd9bda29c368c862ba (diff) | |
download | gitlab-ce-b38fc20ae0e90d5b1c538a139aa0a7da1b7b5726.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'workhorse')
-rw-r--r-- | workhorse/internal/api/api.go | 30 | ||||
-rw-r--r-- | workhorse/internal/upstream/routes.go | 26 | ||||
-rw-r--r-- | workhorse/upload_test.go | 84 |
3 files changed, 123 insertions, 17 deletions
diff --git a/workhorse/internal/api/api.go b/workhorse/internal/api/api.go index a420288a95a..e434a847bf2 100644 --- a/workhorse/internal/api/api.go +++ b/workhorse/internal/api/api.go @@ -151,7 +151,7 @@ type Response struct { MaximumSize int64 } -// singleJoiningSlash is taken from reverseproxy.go:NewSingleHostReverseProxy +// singleJoiningSlash is taken from reverseproxy.go:singleJoiningSlash func singleJoiningSlash(a, b string) string { aslash := strings.HasSuffix(a, "/") bslash := strings.HasPrefix(b, "/") @@ -164,14 +164,36 @@ func singleJoiningSlash(a, b string) string { return a + b } +// joinURLPath is taken from reverseproxy.go:joinURLPath +func joinURLPath(a *url.URL, b string) (path string, rawpath string) { + if a.RawPath == "" && b == "" { + return singleJoiningSlash(a.Path, b), "" + } + + // Same as singleJoiningSlash, but uses EscapedPath to determine + // whether a slash should be added + apath := a.EscapedPath() + bpath := b + + aslash := strings.HasSuffix(apath, "/") + bslash := strings.HasPrefix(bpath, "/") + + switch { + case aslash && bslash: + return a.Path + bpath[1:], apath + bpath[1:] + case !aslash && !bslash: + return a.Path + "/" + bpath, apath + "/" + bpath + } + return a.Path + bpath, apath + bpath +} + // rebaseUrl is taken from reverseproxy.go:NewSingleHostReverseProxy func rebaseUrl(url *url.URL, onto *url.URL, suffix string) *url.URL { newUrl := *url newUrl.Scheme = onto.Scheme newUrl.Host = onto.Host - if suffix != "" { - newUrl.Path = singleJoiningSlash(url.Path, suffix) - } + newUrl.Path, newUrl.RawPath = joinURLPath(url, suffix) + if onto.RawQuery == "" || newUrl.RawQuery == "" { newUrl.RawQuery = onto.RawQuery + newUrl.RawQuery } else { diff --git a/workhorse/internal/upstream/routes.go b/workhorse/internal/upstream/routes.go index fb8a07a8031..b77cb06d1d0 100644 --- a/workhorse/internal/upstream/routes.go +++ b/workhorse/internal/upstream/routes.go @@ -57,6 +57,7 @@ const ( ciAPIPattern = `^/ci/api/` gitProjectPattern = `^/.+\.git/` projectPattern = `^/([^/]+/){1,}[^/]+/` + apiProjectPattern = apiPattern + `v4/projects/[^/]+/` // API: Projects can be encoded via group%2Fsubgroup%2Fproject snippetUploadPattern = `^/uploads/personal_snippet` userUploadPattern = `^/uploads/user` importPattern = `^/import/` @@ -253,32 +254,39 @@ func configureRoutes(u *upstream) { u.route("", apiPattern+`v4/jobs/request\z`, ciAPILongPolling), u.route("", ciAPIPattern+`v1/builds/register.json\z`, ciAPILongPolling), + // Not all API endpoints support encoded project IDs + // (e.g. `group%2Fproject`), but for the sake of consistency we + // use the apiProjectPattern regex throughout. API endpoints + // that do not support this will return 400 regardless of + // whether they are accelerated by Workhorse or not. See + // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56731. + // Maven Artifact Repository - u.route("PUT", apiPattern+`v4/projects/[0-9]+/packages/maven/`, upload.BodyUploader(api, signingProxy, preparers.packages)), + u.route("PUT", apiProjectPattern+`packages/maven/`, upload.BodyUploader(api, signingProxy, preparers.packages)), // Conan Artifact Repository u.route("PUT", apiPattern+`v4/packages/conan/`, upload.BodyUploader(api, signingProxy, preparers.packages)), - u.route("PUT", apiPattern+`v4/projects/[0-9]+/packages/conan/`, upload.BodyUploader(api, signingProxy, preparers.packages)), + u.route("PUT", apiProjectPattern+`packages/conan/`, upload.BodyUploader(api, signingProxy, preparers.packages)), // Generic Packages Repository - u.route("PUT", apiPattern+`v4/projects/[0-9]+/packages/generic/`, upload.BodyUploader(api, signingProxy, preparers.packages)), + u.route("PUT", apiProjectPattern+`packages/generic/`, upload.BodyUploader(api, signingProxy, preparers.packages)), // NuGet Artifact Repository - u.route("PUT", apiPattern+`v4/projects/[0-9]+/packages/nuget/`, upload.Accelerate(api, signingProxy, preparers.packages)), + u.route("PUT", apiProjectPattern+`packages/nuget/`, upload.Accelerate(api, signingProxy, preparers.packages)), // PyPI Artifact Repository - u.route("POST", apiPattern+`v4/projects/[0-9]+/packages/pypi`, upload.Accelerate(api, signingProxy, preparers.packages)), + u.route("POST", apiProjectPattern+`packages/pypi`, upload.Accelerate(api, signingProxy, preparers.packages)), // Debian Artifact Repository - u.route("PUT", apiPattern+`v4/projects/[0-9]+/packages/debian/`, upload.BodyUploader(api, signingProxy, preparers.packages)), + u.route("PUT", apiProjectPattern+`packages/debian/`, upload.BodyUploader(api, signingProxy, preparers.packages)), // Gem Artifact Repository - u.route("POST", apiPattern+`v4/projects/[0-9]+/packages/rubygems/`, upload.BodyUploader(api, signingProxy, preparers.packages)), + u.route("POST", apiProjectPattern+`packages/rubygems/`, upload.BodyUploader(api, signingProxy, preparers.packages)), // We are porting API to disk acceleration // we need to declare each routes until we have fixed all the routes on the rails codebase. // Overall status can be seen at https://gitlab.com/groups/gitlab-org/-/epics/1802#current-status - u.route("POST", apiPattern+`v4/projects/[0-9]+/wikis/attachments\z`, uploadAccelerateProxy), + u.route("POST", apiProjectPattern+`wikis/attachments\z`, uploadAccelerateProxy), u.route("POST", apiPattern+`graphql\z`, uploadAccelerateProxy), u.route("POST", apiPattern+`v4/groups/import`, upload.Accelerate(api, signingProxy, preparers.uploads)), u.route("POST", apiPattern+`v4/projects/import`, upload.Accelerate(api, signingProxy, preparers.uploads)), @@ -289,7 +297,7 @@ func configureRoutes(u *upstream) { u.route("POST", importPattern+`gitlab_group`, upload.Accelerate(api, signingProxy, preparers.uploads)), // Metric image upload - u.route("POST", apiPattern+`v4/projects/[0-9]+/issues/[0-9]+/metric_images\z`, upload.Accelerate(api, signingProxy, preparers.uploads)), + u.route("POST", apiProjectPattern+`issues/[0-9]+/metric_images\z`, upload.Accelerate(api, signingProxy, preparers.uploads)), // Requirements Import via UI upload acceleration u.route("POST", projectPattern+`requirements_management/requirements/import_csv`, upload.Accelerate(api, signingProxy, preparers.uploads)), diff --git a/workhorse/upload_test.go b/workhorse/upload_test.go index 6d118119dff..fdd4605dbc4 100644 --- a/workhorse/upload_test.go +++ b/workhorse/upload_test.go @@ -41,7 +41,7 @@ func testArtifactsUpload(t *testing.T, uploadArtifacts uploadArtifactsFunction) reqBody, contentType, err := multipartBodyWithFile() require.NoError(t, err) - ts := signedUploadTestServer(t, nil) + ts := signedUploadTestServer(t, nil, nil) defer ts.Close() ws := startWorkhorseServer(ts.URL) @@ -66,7 +66,7 @@ func expectSignedRequest(t *testing.T, r *http.Request) { require.NoError(t, err) } -func uploadTestServer(t *testing.T, extraTests func(r *http.Request)) *httptest.Server { +func uploadTestServer(t *testing.T, authorizeTests func(r *http.Request), extraTests func(r *http.Request)) *httptest.Server { return testhelper.TestServerWithHandler(regexp.MustCompile(`.`), func(w http.ResponseWriter, r *http.Request) { if strings.HasSuffix(r.URL.Path, "/authorize") { expectSignedRequest(t, r) @@ -74,6 +74,10 @@ func uploadTestServer(t *testing.T, extraTests func(r *http.Request)) *httptest. w.Header().Set("Content-Type", api.ResponseContentType) _, err := fmt.Fprintf(w, `{"TempPath":"%s"}`, scratchDir) require.NoError(t, err) + + if authorizeTests != nil { + authorizeTests(r) + } return } @@ -91,10 +95,10 @@ func uploadTestServer(t *testing.T, extraTests func(r *http.Request)) *httptest. }) } -func signedUploadTestServer(t *testing.T, extraTests func(r *http.Request)) *httptest.Server { +func signedUploadTestServer(t *testing.T, authorizeTests func(r *http.Request), extraTests func(r *http.Request)) *httptest.Server { t.Helper() - return uploadTestServer(t, func(r *http.Request) { + return uploadTestServer(t, authorizeTests, func(r *http.Request) { expectSignedRequest(t, r) if extraTests != nil { @@ -113,21 +117,39 @@ func TestAcceleratedUpload(t *testing.T) { {"POST", `/uploads/personal_snippet`, true}, {"POST", `/uploads/user`, true}, {"POST", `/api/v4/projects/1/wikis/attachments`, false}, + {"POST", `/api/v4/projects/group%2Fproject/wikis/attachments`, false}, + {"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/wikis/attachments`, false}, {"POST", `/api/graphql`, false}, {"PUT", "/api/v4/projects/9001/packages/nuget/v1/files", true}, + {"PUT", "/api/v4/projects/group%2Fproject/packages/nuget/v1/files", true}, + {"PUT", "/api/v4/projects/group%2Fsubgroup%2Fproject/packages/nuget/v1/files", true}, {"POST", `/api/v4/groups/import`, true}, + {"POST", `/api/v4/groups/import/`, true}, {"POST", `/api/v4/projects/import`, true}, + {"POST", `/api/v4/projects/import/`, true}, {"POST", `/import/gitlab_project`, true}, + {"POST", `/import/gitlab_project/`, true}, {"POST", `/import/gitlab_group`, true}, + {"POST", `/import/gitlab_group/`, true}, {"POST", `/api/v4/projects/9001/packages/pypi`, true}, + {"POST", `/api/v4/projects/group%2Fproject/packages/pypi`, true}, + {"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/packages/pypi`, true}, {"POST", `/api/v4/projects/9001/issues/30/metric_images`, true}, + {"POST", `/api/v4/projects/group%2Fproject/issues/30/metric_images`, true}, + {"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/issues/30/metric_images`, true}, {"POST", `/my/project/-/requirements_management/requirements/import_csv`, true}, + {"POST", `/my/project/-/requirements_management/requirements/import_csv/`, true}, } for _, tt := range tests { t.Run(tt.resource, func(t *testing.T) { ts := uploadTestServer(t, func(r *http.Request) { + resource := strings.TrimRight(tt.resource, "/") + // Validate %2F characters haven't been unescaped + require.Equal(t, resource+"/authorize", r.URL.String()) + }, + func(r *http.Request) { if tt.signedFinalization { expectSignedRequest(t, r) } @@ -186,6 +208,55 @@ func multipartBodyWithFile() (io.Reader, string, error) { return result, writer.FormDataContentType(), writer.Close() } +func unacceleratedUploadTestServer(t *testing.T) *httptest.Server { + return testhelper.TestServerWithHandler(regexp.MustCompile(`.`), func(w http.ResponseWriter, r *http.Request) { + require.False(t, strings.HasSuffix(r.URL.Path, "/authorize")) + require.Empty(t, r.Header.Get(upload.RewrittenFieldsHeader)) + + w.WriteHeader(200) + }) +} + +func TestUnacceleratedUploads(t *testing.T) { + tests := []struct { + method string + resource string + }{ + {"POST", `/api/v4/projects/group/subgroup/project/wikis/attachments`}, + {"POST", `/api/v4/projects/group/project/wikis/attachments`}, + {"PUT", "/api/v4/projects/group/subgroup/project/packages/nuget/v1/files"}, + {"PUT", "/api/v4/projects/group/project/packages/nuget/v1/files"}, + {"POST", `/api/v4/projects/group/subgroup/project/packages/pypi`}, + {"POST", `/api/v4/projects/group/project/packages/pypi`}, + {"POST", `/api/v4/projects/group/subgroup/project/packages/pypi`}, + {"POST", `/api/v4/projects/group/project/issues/30/metric_images`}, + {"POST", `/api/v4/projects/group/subgroup/project/issues/30/metric_images`}, + } + + for _, tt := range tests { + t.Run(tt.resource, func(t *testing.T) { + ts := unacceleratedUploadTestServer(t) + + defer ts.Close() + ws := startWorkhorseServer(ts.URL) + defer ws.Close() + + reqBody, contentType, err := multipartBodyWithFile() + require.NoError(t, err) + + req, err := http.NewRequest(tt.method, ws.URL+tt.resource, reqBody) + require.NoError(t, err) + + req.Header.Set("Content-Type", contentType) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + + resp.Body.Close() + }) + } +} + func TestBlockingRewrittenFieldsHeader(t *testing.T) { canary := "untrusted header passed by user" testCases := []struct { @@ -433,6 +504,11 @@ func TestPackageFilesUpload(t *testing.T) { {"PUT", "/api/v4/projects/2412/packages/generic/mypackage/0.0.1/myfile.tar.gz"}, {"PUT", "/api/v4/projects/2412/packages/debian/libsample0_1.2.3~alpha2-1_amd64.deb"}, {"POST", "/api/v4/projects/2412/packages/rubygems/api/v1/gems/sample.gem"}, + {"PUT", "/api/v4/projects/group%2Fproject/packages/conan/v1/files"}, + {"PUT", "/api/v4/projects/group%2Fproject/packages/maven/v1/files"}, + {"PUT", "/api/v4/projects/group%2Fproject/packages/generic/mypackage/0.0.1/myfile.tar.gz"}, + {"PUT", "/api/v4/projects/group%2Fproject/packages/debian/libsample0_1.2.3~alpha2-1_amd64.deb"}, + {"POST", "/api/v4/projects/group%2Fproject/packages/rubygems/api/v1/gems/sample.gem"}, } for _, r := range routes { |