authcaptureproxy 1.3.7__tar.gz → 1.3.9__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: authcaptureproxy
3
- Version: 1.3.7
3
+ Version: 1.3.9
4
4
  Summary: A Python project to create a proxy to capture authentication information from a webpage. This is useful to capture oauth login details without access to a third-party oauth.
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE.txt
@@ -8,12 +8,14 @@ Author: Alan D. Tse
8
8
  Author-email: alandtse@gmail.com
9
9
  Maintainer: Alan D. Tse
10
10
  Maintainer-email: alandtse@gmail.com
11
- Requires-Python: >=3.10
11
+ Requires-Python: >=3.11
12
12
  Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Natural Language :: English
14
14
  Classifier: Programming Language :: Python :: 3 :: Only
15
- Classifier: Programming Language :: Python :: 3.8
16
- Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
17
19
  Classifier: Intended Audience :: Developers
18
20
  Classifier: Operating System :: OS Independent
19
21
  Classifier: Environment :: Console
@@ -4,9 +4,10 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import logging
7
- from pathlib import Path
8
7
  from email.message import Message
9
- from importlib.metadata import PackageNotFoundError, metadata as __load
8
+ from importlib.metadata import PackageNotFoundError
9
+ from importlib.metadata import metadata as __load
10
+ from pathlib import Path
10
11
  from typing import cast
11
12
 
12
13
  from authcaptureproxy import const
@@ -36,7 +37,7 @@ metadata: Message | None = None
36
37
 
37
38
  try:
38
39
  md = cast(Message, __load(pkg)) # typed as Message so .get() is valid
39
- metadata = md # keep public handle for cli.py
40
+ metadata = md # keep public handle for cli.py
40
41
 
41
42
  __uri__ = md.get("Home-page") or md.get("Home-Page") or ""
42
43
  __title__ = md.get("Name") or ""
@@ -45,12 +46,7 @@ try:
45
46
  __version__ = md.get("Version") or ""
46
47
  __author__ = md.get("Author") or ""
47
48
  __maintainer__ = md.get("Maintainer") or ""
48
- __contact__ = (
49
- md.get("Author-email")
50
- or md.get("Maintainer-email")
51
- or md.get("Maintainer")
52
- or ""
53
- )
49
+ __contact__ = md.get("Author-email") or md.get("Maintainer-email") or md.get("Maintainer") or ""
54
50
 
55
51
  except PackageNotFoundError: # pragma: no cover
56
52
  logger.error("Could not load package metadata for %s. Is it installed?", pkg)
@@ -1,12 +1,13 @@
1
1
  # SPDX-License-Identifier: Apache-2.0
2
2
  """Python Package for auth capture proxy."""
3
+
3
4
  import asyncio
4
5
  import logging
5
- import posixpath
6
6
  import re
7
7
  from functools import partial
8
+ from html import escape, unescape
8
9
  from json import JSONDecodeError
9
- from ssl import SSLContext, create_default_context
10
+ from ssl import SSLContext
10
11
  from typing import Any, Callable, Dict, List, Optional, Set, Text, Tuple, Union
11
12
 
12
13
  import httpx
@@ -31,9 +32,6 @@ from authcaptureproxy.helper import (
31
32
  from authcaptureproxy.interceptor import BaseInterceptor, InterceptContext
32
33
  from authcaptureproxy.stackoverflow import get_open_port
33
34
 
34
- # Pre-configure SSL context
35
- ssl_context = create_default_context()
36
-
37
35
  _LOGGER = logging.getLogger(__name__)
38
36
 
39
37
 
@@ -62,7 +60,7 @@ class AuthCaptureProxy:
62
60
  """
63
61
  self._preserve_headers = preserve_headers
64
62
  self.session_factory: Callable[[], httpx.AsyncClient] = session_factory or (
65
- lambda: httpx.AsyncClient(verify=ssl_context)
63
+ lambda: httpx.AsyncClient(verify=True)
66
64
  )
67
65
  self.session: httpx.AsyncClient = session if session else self.session_factory()
68
66
  self._proxy_url: URL = proxy_url
@@ -686,7 +684,6 @@ class AuthCaptureProxy:
686
684
  # Resolve relative form actions BEFORE modifiers run.
687
685
  if text and content_type == "text/html" and resp and resp.url:
688
686
  _resp_url = URL(str(resp.url))
689
- _resp_dir = _resp_url.path.rsplit("/", 1)[0] + "/" if "/" in _resp_url.path else "/"
690
687
 
691
688
  def _resolve_form_action(form_match):
692
689
  """Resolve relative action URLs only inside <form> tags."""
@@ -698,10 +695,13 @@ class AuthCaptureProxy:
698
695
  if action and not action.startswith(
699
696
  ("http://", "https://", "//", "#", "javascript:", "/")
700
697
  ):
701
- resolved_path = posixpath.normpath(_resp_dir + action)
698
+ resolved_url = _resp_url.join(URL(unescape(action)))
702
699
  _proxy_base = self.access_url().path.rstrip("/")
703
700
  abs_url = str(
704
- self.access_url().with_path(_proxy_base + resolved_path).with_query({})
701
+ self.access_url()
702
+ .with_path(_proxy_base + resolved_url.path)
703
+ .with_query(resolved_url.query)
704
+ .with_fragment(resolved_url.fragment)
705
705
  )
706
706
  _LOGGER.debug(
707
707
  "Resolved relative form action '%s' -> '%s' (page: %s)",
@@ -709,7 +709,11 @@ class AuthCaptureProxy:
709
709
  abs_url,
710
710
  _resp_url.path,
711
711
  )
712
- return form_tag[: action_m.start(2)] + abs_url + form_tag[action_m.end(2) :]
712
+ return (
713
+ form_tag[: action_m.start(2)]
714
+ + escape(abs_url, quote=True)
715
+ + form_tag[action_m.end(2) :]
716
+ )
713
717
  return form_tag
714
718
 
715
719
  text = re.sub(
@@ -754,9 +758,11 @@ class AuthCaptureProxy:
754
758
  # pass through non parsed content
755
759
  _LOGGER.debug(
756
760
  "Passing through %s as %s",
757
- URL(str(request.url)).name
758
- if URL(str(request.url)).name
759
- else URL(str(request.url)).path,
761
+ (
762
+ URL(str(request.url)).name
763
+ if URL(str(request.url)).name
764
+ else URL(str(request.url)).path
765
+ ),
760
766
  content_type,
761
767
  )
762
768
  return await self._build_response(resp, body=resp.content, content_type=content_type)
@@ -893,18 +899,25 @@ class AuthCaptureProxy:
893
899
  result["Referer"] = self._swap_proxy_and_host(
894
900
  result.get("Referer", ""), domain_only=True
895
901
  )
896
- for item in [
897
- "Content-Length",
898
- "X-Forwarded-For",
899
- "X-Forwarded-Host",
900
- "X-Forwarded-Port",
901
- "X-Forwarded-Proto",
902
- "X-Forwarded-Scheme",
903
- "X-Forwarded-Server",
904
- "X-Real-IP",
905
- ]:
906
- # remove proxy headers
907
- if result.get(item):
902
+ # Remove hop-by-hop and reverse-proxy/CDN headers that must not be forwarded
903
+ # upstream. Besides leaking client info, these can carry non-ASCII values (e.g.
904
+ # Cloudflare's "cf-ipcity: Mérida") that break httpx header encoding. Matching is
905
+ # case-insensitive because HTTP/2 front-ends (e.g. Cloudflare) lowercase header
906
+ # names, so exact-case matching would silently miss them.
907
+ proxy_headers = {
908
+ "content-length",
909
+ "x-forwarded-for",
910
+ "x-forwarded-host",
911
+ "x-forwarded-port",
912
+ "x-forwarded-proto",
913
+ "x-forwarded-scheme",
914
+ "x-forwarded-server",
915
+ "x-real-ip",
916
+ "cdn-loop",
917
+ }
918
+ for item in list(result):
919
+ lowered = item.lower()
920
+ if lowered in proxy_headers or lowered.startswith("cf-"):
908
921
  result.pop(item)
909
922
  result.update(self.headers if self.headers else {})
910
923
  _LOGGER.debug("Final headers %s", result)
@@ -119,7 +119,7 @@ async def proxy_example(
119
119
  "autofill": partial(
120
120
  autofill,
121
121
  {
122
- "password": "CHANGEME",
122
+ "password": "CHANGEME", # nosec B105: Example autofill placeholder.
123
123
  },
124
124
  )
125
125
  }
@@ -4,4 +4,5 @@ Python Package for auth capture proxy.
4
4
 
5
5
  Constants
6
6
  """
7
+
7
8
  SKIP_AUTO_HEADERS = "_skip_auto_headers_"
@@ -10,6 +10,7 @@ This interceptor handles:
10
10
  - P shim and jQuery shim injection for Amazon's A-framework in AJAX responses
11
11
  - Submit blocker and AJAX proxy injection for CVF full-page navigations
12
12
  """
13
+
13
14
  import base64
14
15
  import json as _json
15
16
  import logging
@@ -4,10 +4,11 @@ Python Package for auth capture proxy.
4
4
 
5
5
  Example modifiers.
6
6
  """
7
+
7
8
  import logging
8
9
  import re
9
10
  from functools import partial
10
- from typing import Callable, Dict, List, Optional, Text
11
+ from typing import Callable, Dict, List, Optional, Text, cast
11
12
 
12
13
  from bs4 import BeautifulSoup # type: ignore[import]
13
14
  from yarl import URL
@@ -207,7 +208,7 @@ async def find_urls_bs4(
207
208
  # this currently only handles background-image as the first attribute
208
209
  # TODO: Rewrite regex to handle general case
209
210
  pattern = r"(?<=style=[\"']background-image:url\([\"']).*(?=[\"']\))"
210
- attribute_value = html_tag.get(attribute)
211
+ attribute_value = cast(str, html_tag.get(attribute))
211
212
  url: Optional[URL] = URL(str(re.search(pattern, attribute_value)))
212
213
  if url is not None and url not in exceptions.get(tag, []):
213
214
  new_value = re.sub(
@@ -224,7 +225,11 @@ async def find_urls_bs4(
224
225
  html_tag[attribute],
225
226
  )
226
227
  else:
227
- url = URL(html_tag.get(attribute)) if html_tag.get(attribute) is not None else None
228
+ url = (
229
+ URL(cast(str, html_tag.get(attribute)))
230
+ if html_tag.get(attribute) is not None
231
+ else None
232
+ )
228
233
  if (
229
234
  url is not None
230
235
  and not str(url).startswith("data:")
@@ -5,7 +5,6 @@ Python Package for auth capture proxy.
5
5
  Helper files.
6
6
  """
7
7
 
8
- import ast
9
8
  import json
10
9
  import logging
11
10
  import re
@@ -39,11 +38,11 @@ def print_resp(resp: Union[httpx.Response, ClientResponse]) -> None:
39
38
  method = resp.request.method
40
39
  status = resp.status_code
41
40
  reason = resp.reason_phrase
42
- headers = ast.literal_eval(str(resp.request.headers)[8:-1])
41
+ headers: Dict[str, Any] = dict(resp.request.headers)
43
42
  cookies = {}
44
43
  if headers.get("cookie"):
45
44
  cookie: SimpleCookie = SimpleCookie()
46
- cookie.load(headers.get("cookie"))
45
+ cookie.load(headers["cookie"])
47
46
  for key, morsel in cookie.items():
48
47
  cookies[key] = morsel.value
49
48
  headers["cookie"] = cookies
@@ -74,13 +73,11 @@ def print_resp_aiohttp(resp: ClientResponse) -> None:
74
73
  method = resp.request_info.method
75
74
  status = resp.status
76
75
  reason = resp.reason
77
- headers = ast.literal_eval(
78
- str(resp.request_info.headers).replace("<CIMultiDictProxy(", "{").replace(")>", "}")
79
- )
76
+ headers: Dict[str, Any] = dict(resp.request_info.headers)
80
77
  cookies = {}
81
78
  if headers.get("Cookie"):
82
79
  cookie: SimpleCookie = SimpleCookie()
83
- cookie.load(headers.get("Cookie"))
80
+ cookie.load(headers["Cookie"])
84
81
  for key, morsel in cookie.items():
85
82
  cookies[key] = morsel.value
86
83
  headers["Cookie"] = cookies
@@ -4,6 +4,7 @@ Python Package for auth capture proxy.
4
4
 
5
5
  Interceptor base classes for extending the proxy pipeline.
6
6
  """
7
+
7
8
  from dataclasses import dataclass
8
9
  from typing import Any, Dict, Optional, Text
9
10
 
@@ -4,6 +4,7 @@ Python Package auth capture proxy.
4
4
 
5
5
  This is code borrowed from stack overflow.
6
6
  """
7
+
7
8
  import socket
8
9
  from typing import Text
9
10
 
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "authcaptureproxy"
7
- version = "1.3.7"
7
+ version = "1.3.9"
8
8
  description = "A Python project to create a proxy to capture authentication information from a webpage. This is useful to capture oauth login details without access to a third-party oauth."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -18,15 +18,17 @@ classifiers = [
18
18
  "Development Status :: 3 - Alpha",
19
19
  "Natural Language :: English",
20
20
  "Programming Language :: Python :: 3 :: Only",
21
- "Programming Language :: Python :: 3.8",
22
- "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
23
25
  "Intended Audience :: Developers",
24
26
  "Operating System :: OS Independent",
25
27
  "Environment :: Console",
26
28
  "Topic :: Software Development :: Libraries :: Python Modules",
27
29
  ]
28
30
  keywords = [""]
29
- requires-python = ">=3.10"
31
+ requires-python = ">=3.11"
30
32
  dependencies = [
31
33
  "typer>=0.3, <1.0",
32
34
  "aiohttp>=3.0.0, <4.0.0",
@@ -58,7 +60,7 @@ coverage = { extras = ["toml"], version = "^5" }
58
60
  flake8 = ">=3.9, <8.0"
59
61
  flake8-bugbear = ">=20.11, <25.0"
60
62
  flake8-docstrings = "^1"
61
- pre-commit = ">=3.2, <4.0"
63
+ pre-commit = ">=3.5.0"
62
64
  pre-commit-hooks = ">=3.4, <4.0"
63
65
  pytest = "^7"
64
66
  pytest-cov = "^2"
@@ -88,7 +90,7 @@ align = true
88
90
  [tool.tyrannosaurus.sources]
89
91
  pycompatversions = "project.dependencies"
90
92
  pytoxversions = "project.dependencies"
91
- pymetaversion = "'3.9'"
93
+ pymetaversion = "'3.11'"
92
94
  metavm = "'ubuntu-latest'"
93
95
  devdeps = "tool.poetry.group.dev.dependencies"
94
96
  status = "'Development'"
@@ -145,10 +147,16 @@ filename = "NEWS.md"
145
147
  [tool.semantic_release]
146
148
  version_toml = ["pyproject.toml:project.version"]
147
149
  major_on_zero = true
148
- branch = "main"
149
- upload_to_pypi = true
150
- upload_to_release = true
151
- build_command = "pip install poetry && poetry build"
150
+ assets = ["poetry.lock"]
151
+ build_command = "pip install poetry && poetry lock && poetry build"
152
+
153
+ [tool.semantic_release.publish]
154
+ upload_to_vcs_release = true
155
+
156
+ [tool.semantic_release.branches.main]
157
+ match = "(main|master)"
158
+ prerelease_token = "rc"
159
+ prerelease = false
152
160
 
153
161
  [tool.coverage.paths]
154
162
  source = ["authcaptureproxy"]
@@ -162,7 +170,7 @@ show_missing = true
162
170
 
163
171
  [tool.black]
164
172
  line-length = 100
165
- target-version = ["py38"]
173
+ target-version = ["py311"]
166
174
 
167
175
  [tool.isort]
168
176
  line_length = 100