diff --git a/src/sentry/api/endpoints/chunk.py b/src/sentry/api/endpoints/chunk.py index d0ccb85d0f7e9f..6556ebfe9d9c16 100644 --- a/src/sentry/api/endpoints/chunk.py +++ b/src/sentry/api/endpoints/chunk.py @@ -1,4 +1,5 @@ import logging +import re from gzip import GzipFile from io import BytesIO from urllib.parse import urljoin @@ -18,6 +19,7 @@ MAX_REQUEST_SIZE = 32 * 1024 * 1024 MAX_CONCURRENCY = settings.DEBUG and 1 or 8 HASH_ALGORITHM = "sha1" +SENTRYCLI_SEMVER_RE = re.compile(r"^sentry-cli\/(?P\d+)\.(?P\d+)\.(?P\d+).*$") CHUNK_UPLOAD_ACCEPT = ( "debug_files", # DIF assemble @@ -48,12 +50,29 @@ def get(self, request, organization): endpoint = options.get("system.upload-url-prefix") relative_url = reverse("sentry-api-0-chunk-upload", args=[organization.slug]) + # Starting `sentry-cli@1.70.1` we added a support for relative chunk-uploads urls + # User-Agent: sentry-cli/1.70.1 + user_agent = request.headers.get("User-Agent", "") + sentrycli_version = SENTRYCLI_SEMVER_RE.search(user_agent) + supports_relative_url = ( + (sentrycli_version is not None) + and (int(sentrycli_version.group("major")) >= 1) + and (int(sentrycli_version.group("minor")) >= 70) + and (int(sentrycli_version.group("patch")) >= 1) + ) + + # If user do not overwritten upload url prefix if len(endpoint) == 0: - # If config is not set, we return relative, versionless endpoint (with `/api/0` stripped) - prefix = "/api/0" - url = relative_url.replace(prefix, "/") + # And we support relative url uploads, return a relative, versionless endpoint (with `/api/0` stripped) + if supports_relative_url: + prefix = "/api/0" + url = relative_url.replace(prefix, "/") + # Otherwise, if we do not support them, return an absolute, versioned endpoint with a default, system-wide prefix + else: + endpoint = options.get("system.url-prefix") + url = urljoin(endpoint.rstrip("/") + "/", relative_url.lstrip("/")) else: - # Otherwise, we want a relative, versioned endpoint, with user-configured prefix + # If user overriden upload url prefix, we want an absolute, versioned endpoint, with user-configured prefix url = urljoin(endpoint.rstrip("/") + "/", relative_url.lstrip("/")) return Response( diff --git a/tests/sentry/api/endpoints/test_chunk_upload.py b/tests/sentry/api/endpoints/test_chunk_upload.py index a2f3436506d0ce..030fecf5d60327 100644 --- a/tests/sentry/api/endpoints/test_chunk_upload.py +++ b/tests/sentry/api/endpoints/test_chunk_upload.py @@ -1,5 +1,6 @@ from hashlib import sha1 +import pytest from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile from django.urls import reverse @@ -16,6 +17,10 @@ class ChunkUploadTest(APITestCase): + @pytest.fixture(autouse=True) + def _restore_upload_url_options(self): + options.delete("system.upload-url-prefix") + def setUp(self): self.organization = self.create_organization(owner=self.user) self.token = ApiToken.objects.create(user=self.user, scope_list=["project:write"]) @@ -33,7 +38,7 @@ def test_chunk_parameters(self): assert response.data["maxFileSize"] == options.get("system.maximum-file-size") assert response.data["concurrency"] == MAX_CONCURRENCY assert response.data["hashAlgorithm"] == HASH_ALGORITHM - assert response.data["url"] == self.url.replace("/api/0", "/") + assert response.data["url"] == options.get("system.url-prefix") + self.url options.set("system.upload-url-prefix", "test") response = self.client.get( @@ -42,6 +47,53 @@ def test_chunk_parameters(self): assert response.data["url"] == options.get("system.upload-url-prefix") + self.url + def test_relative_url_support(self): + # Starting `sentry-cli@1.70.1` we added a support for relative chunk-uploads urls + + # >= 1.70.1 + response = self.client.get( + self.url, + HTTP_AUTHORIZATION=f"Bearer {self.token.token}", + HTTP_USER_AGENT="sentry-cli/1.70.1", + format="json", + ) + assert response.data["url"] == self.url.replace("/api/0", "/") + + response = self.client.get( + self.url, + HTTP_AUTHORIZATION=f"Bearer {self.token.token}", + HTTP_USER_AGENT="sentry-cli/2.77.4", + format="json", + ) + assert response.data["url"] == self.url.replace("/api/0", "/") + + # < 1.70.1 + response = self.client.get( + self.url, + HTTP_AUTHORIZATION=f"Bearer {self.token.token}", + HTTP_USER_AGENT="sentry-cli/1.70.0", + format="json", + ) + assert response.data["url"] == options.get("system.url-prefix") + self.url + + response = self.client.get( + self.url, + HTTP_AUTHORIZATION=f"Bearer {self.token.token}", + HTTP_USER_AGENT="sentry-cli/0.69.3", + format="json", + ) + assert response.data["url"] == options.get("system.url-prefix") + self.url + + # user overriden upload url prefix has priority, even when calling from sentry-cli that supports relative urls + options.set("system.upload-url-prefix", "test") + response = self.client.get( + self.url, + HTTP_AUTHORIZATION=f"Bearer {self.token.token}", + HTTP_USER_AGENT="sentry-cli/1.70.1", + format="json", + ) + assert response.data["url"] == options.get("system.upload-url-prefix") + self.url + def test_large_uploads(self): with self.feature("organizations:large-debug-files"): response = self.client.get(