Skip to content

Commit 74882c5

Browse files
authored
Merge pull request #2612 from jku/support-app-user-agent
Support app-specific user-agents
2 parents c625687 + fb58145 commit 74882c5

4 files changed

Lines changed: 47 additions & 11 deletions

File tree

tests/test_updater_ng.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from securesystemslib.signer import SSlibSigner
1919

2020
from tests import utils
21-
from tuf import ngclient
2221
from tuf.api import exceptions
2322
from tuf.api.metadata import (
2423
Metadata,
@@ -28,6 +27,7 @@
2827
Targets,
2928
Timestamp,
3029
)
30+
from tuf.ngclient import Updater, UpdaterConfig
3131

3232
logger = logging.getLogger(__name__)
3333

@@ -107,7 +107,7 @@ def setUp(self) -> None:
107107
self.dl_dir = tempfile.mkdtemp(dir=self.tmp_test_dir)
108108
# Creating a repository instance. The test cases will use this client
109109
# updater to refresh metadata, fetch target files, etc.
110-
self.updater = ngclient.Updater(
110+
self.updater = Updater(
111111
metadata_dir=self.client_directory,
112112
metadata_base_url=self.metadata_url,
113113
target_dir=self.dl_dir,
@@ -242,16 +242,14 @@ def test_implicit_refresh_with_only_local_root(self) -> None:
242242

243243
def test_both_target_urls_not_set(self) -> None:
244244
# target_base_url = None and Updater._target_base_url = None
245-
updater = ngclient.Updater(
246-
self.client_directory, self.metadata_url, self.dl_dir
247-
)
245+
updater = Updater(self.client_directory, self.metadata_url, self.dl_dir)
248246
info = TargetFile(1, {"sha256": ""}, "targetpath")
249247
with self.assertRaises(ValueError):
250248
updater.download_target(info)
251249

252250
def test_no_target_dir_no_filepath(self) -> None:
253251
# filepath = None and Updater.target_dir = None
254-
updater = ngclient.Updater(self.client_directory, self.metadata_url)
252+
updater = Updater(self.client_directory, self.metadata_url)
255253
info = TargetFile(1, {"sha256": ""}, "targetpath")
256254
with self.assertRaises(ValueError):
257255
updater.find_cached_target(info)
@@ -323,6 +321,27 @@ def test_non_existing_target_file(self) -> None:
323321
with self.assertRaises(exceptions.DownloadHTTPError):
324322
self.updater.download_target(info)
325323

324+
def test_user_agent(self) -> None:
325+
# test default
326+
self.updater.refresh()
327+
session = next(iter(self.updater._fetcher._sessions.values()))
328+
ua = session.headers["User-Agent"]
329+
self.assertEqual(ua[:4], "tuf/")
330+
331+
# test custom UA
332+
updater = Updater(
333+
self.client_directory,
334+
self.metadata_url,
335+
self.dl_dir,
336+
self.targets_url,
337+
config=UpdaterConfig(app_user_agent="MyApp/1.2.3"),
338+
)
339+
updater.refresh()
340+
session = next(iter(updater._fetcher._sessions.values()))
341+
ua = session.headers["User-Agent"]
342+
343+
self.assertEqual(ua[:16], "MyApp/1.2.3 tuf/")
344+
326345

327346
if __name__ == "__main__":
328347
utils.configure_test_logging(sys.argv)

tuf/ngclient/_internal/requests_fetcher.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# can be moved out of _internal once sigstore-python 1.0 is not relevant.
1111

1212
import logging
13-
from typing import Dict, Iterator, Tuple
13+
from typing import Dict, Iterator, Optional, Tuple
1414
from urllib import parse
1515

1616
# Imports
@@ -35,7 +35,10 @@ class RequestsFetcher(FetcherInterface):
3535
"""
3636

3737
def __init__(
38-
self, socket_timeout: int = 30, chunk_size: int = 400000
38+
self,
39+
socket_timeout: int = 30,
40+
chunk_size: int = 400000,
41+
app_user_agent: Optional[str] = None,
3942
) -> None:
4043
# http://docs.python-requests.org/en/master/user/advanced/#session-objects:
4144
#
@@ -56,6 +59,7 @@ def __init__(
5659
# Default settings
5760
self.socket_timeout: int = socket_timeout # seconds
5861
self.chunk_size: int = chunk_size # bytes
62+
self.app_user_agent = app_user_agent
5963

6064
def _fetch(self, url: str) -> Iterator[bytes]:
6165
"""Fetch the contents of HTTP/HTTPS url from a remote server.
@@ -138,6 +142,8 @@ def _get_session(self, url: str) -> requests.Session:
138142
self._sessions[session_index] = session
139143

140144
ua = f"tuf/{tuf.__version__} {session.headers['User-Agent']}"
145+
if self.app_user_agent is not None:
146+
ua = f"{self.app_user_agent} {ua}"
141147
session.headers["User-Agent"] = ua
142148

143149
logger.debug("Made new session %s", session_index)

tuf/ngclient/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from dataclasses import dataclass
77
from enum import Flag, unique
8+
from typing import Optional
89

910

1011
@unique
@@ -39,6 +40,8 @@ class UpdaterConfig:
3940
envelope_type: Configures deserialization and verification mode of TUF
4041
metadata. Per default, it is treated as traditional canonical JSON
4142
-based TUF Metadata.
43+
app_user_agent: Application user agent, e.g. "MyApp/1.0.0". This will be
44+
prefixed to ngclient user agent when the default fetcher is used.
4245
"""
4346

4447
max_root_rotations: int = 32
@@ -49,3 +52,4 @@ class UpdaterConfig:
4952
targets_max_length: int = 5000000 # bytes
5053
prefix_targets_with_hash: bool = True
5154
envelope_type: EnvelopeType = EnvelopeType.METADATA
55+
app_user_agent: Optional[str] = None

tuf/ngclient/updater.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,18 +93,25 @@ def __init__(
9393
else:
9494
self._target_base_url = _ensure_trailing_slash(target_base_url)
9595

96-
# Read trusted local root metadata
97-
data = self._load_local_metadata(Root.type)
98-
self._fetcher = fetcher or requests_fetcher.RequestsFetcher()
9996
self.config = config or UpdaterConfig()
10097

98+
if fetcher is not None:
99+
self._fetcher = fetcher
100+
else:
101+
self._fetcher = requests_fetcher.RequestsFetcher(
102+
app_user_agent=self.config.app_user_agent
103+
)
104+
101105
supported_envelopes = [EnvelopeType.METADATA, EnvelopeType.SIMPLE]
102106
if self.config.envelope_type not in supported_envelopes:
103107
raise ValueError(
104108
f"config: envelope_type must be one of {supported_envelopes}, "
105109
f"got '{self.config.envelope_type}'"
106110
)
107111

112+
# Read trusted local root metadata
113+
data = self._load_local_metadata(Root.type)
114+
108115
self._trusted_set = trusted_metadata_set.TrustedMetadataSet(
109116
data, self.config.envelope_type
110117
)

0 commit comments

Comments
 (0)