Skip to content

Commit a553645

Browse files
authored
Add source field to user-agent header (#1825)
1 parent 42db177 commit a553645

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

stripe/_api_requestor.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from io import BytesIO, IOBase
2+
import functools
3+
import hashlib
24
import json
35
import os
46
import platform
7+
import socket
58
from typing import (
69
Any,
710
AsyncIterable,
@@ -72,6 +75,19 @@
7275
_default_proxy: Optional[str] = None
7376

7477

78+
@functools.lru_cache(maxsize=None)
79+
def _get_uname_hash() -> Optional[str]:
80+
try:
81+
parts: List[str] = list(platform.uname())
82+
try:
83+
parts.append(socket.gethostname())
84+
except Exception:
85+
pass
86+
return hashlib.md5(" ".join(parts).encode()).hexdigest()
87+
except Exception:
88+
return None
89+
90+
7591
def _maybe_emit_stripe_notice(rheaders: Mapping[str, str]) -> None:
7692
notice = rheaders.get("Stripe-Notice")
7793
if notice:
@@ -525,6 +541,9 @@ def request_headers(
525541
"lang": "python",
526542
"httplib": self._get_http_client().name,
527543
}
544+
uname_hash = _get_uname_hash()
545+
if uname_hash is not None:
546+
ua["source"] = uname_hash
528547
attr_funcs: List[Tuple[str, Callable[[], str]]] = [
529548
("lang_version", platform.python_version),
530549
]

tests/test_api_requestor.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime
22
import json
3+
import re
34
import tempfile
45
import uuid
56
from collections import OrderedDict
@@ -788,6 +789,36 @@ def fail():
788789
last_call.get_raw_header("X-Stripe-Client-User-Agent")
789790
)
790791

792+
def test_source_field_is_md5_hex(self, requestor, http_client_mock):
793+
http_client_mock.stub_request(
794+
"get", path=self.v1_path, rbody="{}", rcode=200
795+
)
796+
requestor.request("get", self.v1_path, {}, base_address="api")
797+
798+
last_call = http_client_mock.get_last_call()
799+
client_ua = json.loads(
800+
last_call.get_raw_header("X-Stripe-Client-User-Agent")
801+
)
802+
assert "source" in client_ua
803+
assert re.fullmatch(r"[0-9a-f]{32}", client_ua["source"])
804+
805+
def test_source_field_absent_when_uname_fails(
806+
self, requestor, mocker, http_client_mock
807+
):
808+
http_client_mock.stub_request(
809+
"get", path=self.v1_path, rbody="{}", rcode=200
810+
)
811+
mocker.patch(
812+
"stripe._api_requestor._get_uname_hash", return_value=None
813+
)
814+
requestor.request("get", self.v1_path, {}, base_address="api")
815+
816+
last_call = http_client_mock.get_last_call()
817+
client_ua = json.loads(
818+
last_call.get_raw_header("X-Stripe-Client-User-Agent")
819+
)
820+
assert "source" not in client_ua
821+
791822
def test_uses_given_idempotency_key(self, requestor, http_client_mock):
792823
method = "post"
793824
http_client_mock.stub_request(

0 commit comments

Comments
 (0)