rocketflag 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rocketflag
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Python SDK of the RocketFlag Service
5
5
  Author: RocketFlag Developers
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rocketflag"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Python SDK of the RocketFlag Service"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -27,4 +27,4 @@ __all__ = [
27
27
  "create_client",
28
28
  ]
29
29
 
30
- __version__ = "0.2.0"
30
+ __version__ = "0.2.1"
@@ -24,6 +24,8 @@ from .errors import APIError, InvalidResponseError, NetworkError
24
24
  DEFAULT_API_URL = "https://api.rocketflag.app"
25
25
  DEFAULT_VERSION = "v1"
26
26
  DEFAULT_MAX_CACHE_ENTRIES = 10_000
27
+ DEFAULT_TIMEOUT_SECONDS = 10.0
28
+ MAX_RESPONSE_BYTES = 1_048_576
27
29
  _ENV_NAME = re.compile(r"[A-Za-z0-9_-]+")
28
30
 
29
31
  ContextValue = Union[str, int, float, bool]
@@ -83,17 +85,24 @@ class Client:
83
85
  *,
84
86
  ttl_seconds: Optional[float] = None,
85
87
  max_entries: int = DEFAULT_MAX_CACHE_ENTRIES,
88
+ timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
86
89
  opener: Optional[urllib.request.OpenerDirector] = None,
87
90
  ) -> None:
88
91
  if isinstance(max_entries, bool) or not isinstance(max_entries, int) or max_entries < 1:
89
92
  raise ValueError("max_entries must be a positive integer")
90
- self.version = version
91
- self.api_url = api_url.rstrip("/")
93
+ cleaned_url = api_url.rstrip("/")
94
+ parsed_url = urllib.parse.urlparse(cleaned_url)
95
+ if parsed_url.scheme not in ("http", "https") or not parsed_url.netloc:
96
+ raise ValueError("api_url must use http:// or https://")
97
+ self.version = version.strip("/")
98
+ self.api_url = cleaned_url
92
99
  self._default_ttl_ms = 0.0 if ttl_seconds is None else float(ttl_seconds) * 1000.0
93
100
  self._max_entries = max_entries
101
+ self._timeout_seconds = float(timeout_seconds)
94
102
  # Least recently used first; a hit moves its entry to the end.
95
103
  self._cache: OrderedDict[str, tuple[Flag, float]] = OrderedDict()
96
104
  self._cache_lock = threading.Lock()
105
+ self._uses_default_opener = opener is None
97
106
  self._opener = opener or urllib.request.build_opener()
98
107
 
99
108
  def get_flag(
@@ -107,6 +116,8 @@ class Client:
107
116
  raise ValueError("flagId is required")
108
117
  if not isinstance(flag_id, str):
109
118
  raise TypeError("flagId must be a string")
119
+ if flag_id in (".", "..") or "/" in flag_id or "\\" in flag_id:
120
+ raise ValueError("flagId must not contain path separators or relative segments")
110
121
 
111
122
  context: UserContext = user_context or {}
112
123
  if not isinstance(context, Mapping):
@@ -132,12 +143,22 @@ class Client:
132
143
 
133
144
  request = urllib.request.Request(url, method="GET")
134
145
  try:
135
- with self._opener.open(request) as response:
146
+ open_ctx = (
147
+ self._opener.open(request, timeout=self._timeout_seconds)
148
+ if self._uses_default_opener
149
+ else self._opener.open(request)
150
+ )
151
+ with open_ctx as response:
136
152
  status = getattr(response, "status", None) or response.getcode()
137
153
  if status != 200:
138
154
  status_text = getattr(response, "reason", "") or ""
139
155
  raise APIError(f"API request failed with status {status}", status, status_text)
140
- raw = response.read()
156
+ try:
157
+ raw = response.read(MAX_RESPONSE_BYTES + 1)
158
+ except TypeError:
159
+ raw = response.read()
160
+ if len(raw) > MAX_RESPONSE_BYTES:
161
+ raise InvalidResponseError("Response body exceeds maximum size")
141
162
  except APIError:
142
163
  raise
143
164
  except urllib.error.HTTPError as exc:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rocketflag
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Python SDK of the RocketFlag Service
5
5
  Author: RocketFlag Developers
6
6
  License: MIT
@@ -264,3 +264,19 @@ def test_network_error():
264
264
  client = create_client(api_url="http://127.0.0.1:1")
265
265
  with pytest.raises(NetworkError):
266
266
  client.get_flag("abc123")
267
+
268
+
269
+ @pytest.mark.parametrize("bad_id", [".", "..", "../admin", "a/b", "a\\b"])
270
+ def test_rejects_traversal_and_slash_flag_ids(bad_id):
271
+ opener = StubOpener()
272
+ client = Client(opener=opener)
273
+ with pytest.raises(ValueError, match="flagId must not contain path separators or relative segments"):
274
+ client.get_flag(bad_id)
275
+ assert opener.urls == []
276
+
277
+
278
+ @pytest.mark.parametrize("bad_url", ["file:///etc/passwd", "ftp://example.com", "javascript:alert(1)"])
279
+ def test_rejects_non_http_api_urls(bad_url):
280
+ with pytest.raises(ValueError, match="api_url must use http:// or https://"):
281
+ Client(api_url=bad_url)
282
+
File without changes
File without changes
File without changes