From 4dc54ace8a1b9fe312b0b1112353e971c7691a1f Mon Sep 17 00:00:00 2001 From: Mondo Diaz Date: Thu, 29 Jan 2026 18:02:21 -0600 Subject: [PATCH] Fix HTTPS scheme detection behind reverse proxy When behind a reverse proxy that terminates SSL, the server sees HTTP requests internally. Added _get_base_url() helper that respects the X-Forwarded-Proto header to generate correct external HTTPS URLs. This fixes links in the PyPI simple index showing http:// instead of https:// when accessed via HTTPS through a load balancer. --- backend/app/pypi_proxy.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/backend/app/pypi_proxy.py b/backend/app/pypi_proxy.py index 942bfdb..9cbaa9a 100644 --- a/backend/app/pypi_proxy.py +++ b/backend/app/pypi_proxy.py @@ -81,6 +81,26 @@ def _get_basic_auth(source) -> Optional[tuple[str, str]]: return None +def _get_base_url(request: Request) -> str: + """ + Get the external base URL, respecting X-Forwarded-Proto header. + + When behind a reverse proxy that terminates SSL, the request.base_url + will show http:// even though the external URL is https://. This function + checks the X-Forwarded-Proto header to determine the correct scheme. + """ + base_url = str(request.base_url).rstrip('/') + + # Check for X-Forwarded-Proto header (set by reverse proxies) + forwarded_proto = request.headers.get('x-forwarded-proto') + if forwarded_proto: + # Replace the scheme with the forwarded protocol + parsed = urlparse(base_url) + base_url = f"{forwarded_proto}://{parsed.netloc}{parsed.path}" + + return base_url + + def _rewrite_package_links(html: str, base_url: str, package_name: str, upstream_base_url: str) -> str: """ Rewrite download links in a PyPI simple page to go through our proxy. @@ -189,7 +209,7 @@ async def pypi_simple_index( content = response.text # Rewrite package links to go through our proxy - base_url = str(request.base_url).rstrip('/') + base_url = _get_base_url(request) content = re.sub( r'href="([^"]+)/"', lambda m: f'href="{base_url}/pypi/simple/{m.group(1)}/"', @@ -235,7 +255,7 @@ async def pypi_package_versions( detail="No PyPI upstream sources configured" ) - base_url = str(request.base_url).rstrip('/') + base_url = _get_base_url(request) # Normalize package name (PEP 503) normalized_name = re.sub(r'[-_.]+', '-', package_name).lower()