Skip to content

Commit c512bbf

Browse files
committed
Properly handle repository URLs with auth in them
1 parent e0ed808 commit c512bbf

File tree

3 files changed

+58
-30
lines changed

3 files changed

+58
-30
lines changed

tests/test_utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,31 @@ def test_get_repository_config_missing(config_file):
150150
assert utils.get_repository_from_config(config_file, "pypi") == exp
151151

152152

153+
def test_get_repository_config_url_with_auth(config_file):
154+
repository_url = "https://user:[email protected]/pypi"
155+
exp = {
156+
"repository": "https://notexisting.python.org/pypi",
157+
"username": "user",
158+
"password": "pass",
159+
}
160+
assert utils.get_repository_from_config(config_file, "foo", repository_url) == exp
161+
assert utils.get_repository_from_config(config_file, "pypi", repository_url) == exp
162+
163+
164+
@pytest.mark.parametrize(
165+
"input_url, expected_url",
166+
[
167+
("https://upload.pypi.org/legacy/", "https://upload.pypi.org/legacy/"),
168+
(
169+
"https://user:[email protected]/legacy/",
170+
"https://********@upload.pypi.org/legacy/",
171+
),
172+
],
173+
)
174+
def test_sanitize_url(input_url: str, expected_url: str) -> None:
175+
assert utils.sanitize_url(input_url) == expected_url
176+
177+
153178
@pytest.mark.parametrize(
154179
"repo_url, message",
155180
[

twine/commands/upload.py

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import fnmatch
1818
import logging
1919
import os.path
20-
import re
2120
from typing import Dict, List, NamedTuple, cast
2221

2322
import requests
@@ -149,27 +148,6 @@ def _split_inputs(
149148
return Inputs(dists, signatures, attestations_by_dist)
150149

151150

152-
def _sanitize_url(url: str) -> str:
153-
"""Sanitize a URL.
154-
155-
Sanitize URLs, removing any user:password combinations and replacing them with
156-
asterisks. Returns the original URL if the string is a non-matching pattern.
157-
158-
:param url:
159-
str containing a URL to sanitize.
160-
161-
return:
162-
str either sanitized or as entered depending on pattern match.
163-
"""
164-
pattern = r"(.*https?://)(\w+:\w+)@(\w+\..*)"
165-
m = re.match(pattern, url)
166-
if m:
167-
newurl = f"{m.group(1)}*****:*****@{m.group(3)}"
168-
return newurl
169-
else:
170-
return url
171-
172-
173151
def upload(upload_settings: settings.Settings, dists: List[str]) -> None:
174152
"""Upload one or more distributions to a repository, and display the progress.
175153
@@ -211,7 +189,7 @@ def upload(upload_settings: settings.Settings, dists: List[str]) -> None:
211189
# Determine if the user has passed in pre-signed distributions or any attestations.
212190
uploads, signatures, attestations_by_dist = _split_inputs(dists)
213191

214-
print(f"Uploading distributions to {_sanitize_url(repository_url)}")
192+
print(f"Uploading distributions to {utils.sanitize_url(repository_url)}")
215193

216194
packages_to_upload = [
217195
_make_package(
@@ -272,8 +250,8 @@ def upload(upload_settings: settings.Settings, dists: List[str]) -> None:
272250
# redirects as well.
273251
if resp.is_redirect:
274252
raise exceptions.RedirectDetected.from_args(
275-
repository_url,
276-
resp.headers["location"],
253+
utils.sanitize_url(repository_url),
254+
utils.sanitize_url(resp.headers["location"]),
277255
)
278256

279257
if skip_upload(resp, upload_settings.skip_existing, package):

twine/utils.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,24 @@ def get_config(path: str) -> Dict[str, RepositoryConfig]:
100100
return dict(config)
101101

102102

103+
def sanitize_url(url: str) -> str:
104+
"""Sanitize a URL.
105+
106+
Sanitize URLs, removing any user:password combinations and replacing them with
107+
asterisks. Returns the original URL if the string is a non-matching pattern.
108+
109+
:param url:
110+
str containing a URL to sanitize.
111+
112+
return:
113+
str either sanitized or as entered depending on pattern match.
114+
"""
115+
uri = rfc3986.urlparse(url)
116+
if uri.userinfo:
117+
return cast(str, uri.copy_with(userinfo="*" * 8).unsplit())
118+
return url
119+
120+
103121
def _validate_repository_url(repository_url: str) -> None:
104122
"""Validate the given url for allowed schemes and components."""
105123
# Allowed schemes are http and https, based on whether the repository
@@ -126,11 +144,7 @@ def get_repository_from_config(
126144
# Prefer CLI `repository_url` over `repository` or .pypirc
127145
if repository_url:
128146
_validate_repository_url(repository_url)
129-
return {
130-
"repository": repository_url,
131-
"username": None,
132-
"password": None,
133-
}
147+
return _config_from_repository_url(repository_url)
134148

135149
try:
136150
config = get_config(config_file)[repository]
@@ -154,6 +168,17 @@ def get_repository_from_config(
154168
}
155169

156170

171+
def _config_from_repository_url(url: str) -> RepositoryConfig:
172+
parsed = urlparse(url)
173+
config = {"repository": url, "username": None, "password": None}
174+
if parsed.username:
175+
config["username"] = parsed.username
176+
config["password"] = parsed.password
177+
config["repository"] = urlunparse((parsed.scheme, parsed.hostname) + parsed[2:])
178+
config["repository"] = normalize_repository_url(cast(str, config["repository"]))
179+
return config
180+
181+
157182
def normalize_repository_url(url: str) -> str:
158183
parsed = urlparse(url)
159184
if parsed.netloc in _HOSTNAMES:

0 commit comments

Comments
 (0)