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.
- {authcaptureproxy-1.3.7 → authcaptureproxy-1.3.9}/PKG-INFO +6 -4
- {authcaptureproxy-1.3.7 → authcaptureproxy-1.3.9}/authcaptureproxy/__init__.py +5 -9
- {authcaptureproxy-1.3.7 → authcaptureproxy-1.3.9}/authcaptureproxy/auth_capture_proxy.py +38 -25
- {authcaptureproxy-1.3.7 → authcaptureproxy-1.3.9}/authcaptureproxy/cli.py +1 -1
- {authcaptureproxy-1.3.7 → authcaptureproxy-1.3.9}/authcaptureproxy/const.py +1 -0
- {authcaptureproxy-1.3.7 → authcaptureproxy-1.3.9}/authcaptureproxy/examples/amazon_waf.py +1 -0
- {authcaptureproxy-1.3.7 → authcaptureproxy-1.3.9}/authcaptureproxy/examples/modifiers.py +8 -3
- {authcaptureproxy-1.3.7 → authcaptureproxy-1.3.9}/authcaptureproxy/helper.py +4 -7
- {authcaptureproxy-1.3.7 → authcaptureproxy-1.3.9}/authcaptureproxy/interceptor.py +1 -0
- {authcaptureproxy-1.3.7 → authcaptureproxy-1.3.9}/authcaptureproxy/stackoverflow.py +1 -0
- {authcaptureproxy-1.3.7 → authcaptureproxy-1.3.9}/pyproject.toml +19 -11
- {authcaptureproxy-1.3.7 → authcaptureproxy-1.3.9}/CHANGELOG.md +0 -0
- {authcaptureproxy-1.3.7 → authcaptureproxy-1.3.9}/LICENSE.txt +0 -0
- {authcaptureproxy-1.3.7 → authcaptureproxy-1.3.9}/README.md +0 -0
- {authcaptureproxy-1.3.7 → authcaptureproxy-1.3.9}/authcaptureproxy/examples/__init__.py +0 -0
- {authcaptureproxy-1.3.7 → authcaptureproxy-1.3.9}/authcaptureproxy/examples/testers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: authcaptureproxy
|
|
3
|
-
Version: 1.3.
|
|
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.
|
|
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.
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.
|
|
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
|
|
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
|
|
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
|
|
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=
|
|
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
|
-
|
|
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()
|
|
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
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
"
|
|
903
|
-
"
|
|
904
|
-
"
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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)
|
|
@@ -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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "authcaptureproxy"
|
|
7
|
-
version = "1.3.
|
|
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.
|
|
22
|
-
"Programming Language :: Python :: 3.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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 = ["
|
|
173
|
+
target-version = ["py311"]
|
|
166
174
|
|
|
167
175
|
[tool.isort]
|
|
168
176
|
line_length = 100
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|