exp-mitmproxy 12.2.3.post1__py3-none-any.whl
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.
- exp_mitmproxy-12.2.3.post1.dist-info/METADATA +120 -0
- exp_mitmproxy-12.2.3.post1.dist-info/RECORD +291 -0
- exp_mitmproxy-12.2.3.post1.dist-info/WHEEL +5 -0
- exp_mitmproxy-12.2.3.post1.dist-info/entry_points.txt +7 -0
- exp_mitmproxy-12.2.3.post1.dist-info/licenses/LICENSE +19 -0
- exp_mitmproxy-12.2.3.post1.dist-info/top_level.txt +1 -0
- mitmproxy/__init__.py +0 -0
- mitmproxy/addonmanager.py +308 -0
- mitmproxy/addons/__init__.py +67 -0
- mitmproxy/addons/anticache.py +18 -0
- mitmproxy/addons/anticomp.py +15 -0
- mitmproxy/addons/asgiapp.py +144 -0
- mitmproxy/addons/block.py +48 -0
- mitmproxy/addons/blocklist.py +81 -0
- mitmproxy/addons/browser.py +186 -0
- mitmproxy/addons/clientplayback.py +298 -0
- mitmproxy/addons/command_history.py +96 -0
- mitmproxy/addons/comment.py +19 -0
- mitmproxy/addons/core.py +286 -0
- mitmproxy/addons/cut.py +176 -0
- mitmproxy/addons/disable_h2c.py +42 -0
- mitmproxy/addons/dns_resolver.py +182 -0
- mitmproxy/addons/dumper.py +427 -0
- mitmproxy/addons/errorcheck.py +55 -0
- mitmproxy/addons/eventstore.py +56 -0
- mitmproxy/addons/export.py +232 -0
- mitmproxy/addons/intercept.py +63 -0
- mitmproxy/addons/keepserving.py +54 -0
- mitmproxy/addons/maplocal.py +151 -0
- mitmproxy/addons/mapremote.py +68 -0
- mitmproxy/addons/modifybody.py +86 -0
- mitmproxy/addons/modifyheaders.py +117 -0
- mitmproxy/addons/next_layer.py +474 -0
- mitmproxy/addons/onboarding.py +34 -0
- mitmproxy/addons/onboardingapp/__init__.py +63 -0
- mitmproxy/addons/onboardingapp/static/bootstrap.min.css +6 -0
- mitmproxy/addons/onboardingapp/static/images/favicon.ico +0 -0
- mitmproxy/addons/onboardingapp/static/images/mitmproxy-long.png +0 -0
- mitmproxy/addons/onboardingapp/static/mitmproxy.css +44 -0
- mitmproxy/addons/onboardingapp/templates/icons/android-brands.svg +1 -0
- mitmproxy/addons/onboardingapp/templates/icons/apple-brands.svg +1 -0
- mitmproxy/addons/onboardingapp/templates/icons/certificate-solid.svg +1 -0
- mitmproxy/addons/onboardingapp/templates/icons/firefox-browser-brands.svg +1 -0
- mitmproxy/addons/onboardingapp/templates/icons/linux-brands.svg +1 -0
- mitmproxy/addons/onboardingapp/templates/icons/windows-brands.svg +1 -0
- mitmproxy/addons/onboardingapp/templates/index.html +143 -0
- mitmproxy/addons/onboardingapp/templates/layout.html +29 -0
- mitmproxy/addons/proxyauth.py +298 -0
- mitmproxy/addons/proxyserver.py +396 -0
- mitmproxy/addons/readfile.py +98 -0
- mitmproxy/addons/save.py +197 -0
- mitmproxy/addons/savehar.py +312 -0
- mitmproxy/addons/script.py +229 -0
- mitmproxy/addons/server_side_events.py +23 -0
- mitmproxy/addons/serverplayback.py +305 -0
- mitmproxy/addons/stickyauth.py +38 -0
- mitmproxy/addons/stickycookie.py +97 -0
- mitmproxy/addons/strip_dns_https_records.py +37 -0
- mitmproxy/addons/termlog.py +50 -0
- mitmproxy/addons/tlsconfig.py +654 -0
- mitmproxy/addons/update_alt_svc.py +33 -0
- mitmproxy/addons/upstream_auth.py +62 -0
- mitmproxy/addons/view.py +749 -0
- mitmproxy/certs.py +741 -0
- mitmproxy/command.py +342 -0
- mitmproxy/command_lexer.py +41 -0
- mitmproxy/connection.py +359 -0
- mitmproxy/contentviews/__init__.py +195 -0
- mitmproxy/contentviews/_api.py +116 -0
- mitmproxy/contentviews/_compat.py +74 -0
- mitmproxy/contentviews/_registry.py +73 -0
- mitmproxy/contentviews/_utils.py +106 -0
- mitmproxy/contentviews/_view_css.py +76 -0
- mitmproxy/contentviews/_view_dns.py +53 -0
- mitmproxy/contentviews/_view_graphql.py +72 -0
- mitmproxy/contentviews/_view_http3.py +153 -0
- mitmproxy/contentviews/_view_image/__init__.py +3 -0
- mitmproxy/contentviews/_view_image/image_parser.py +119 -0
- mitmproxy/contentviews/_view_image/view.py +57 -0
- mitmproxy/contentviews/_view_javascript.py +68 -0
- mitmproxy/contentviews/_view_json.py +31 -0
- mitmproxy/contentviews/_view_mqtt.py +277 -0
- mitmproxy/contentviews/_view_multipart.py +32 -0
- mitmproxy/contentviews/_view_query.py +31 -0
- mitmproxy/contentviews/_view_raw.py +17 -0
- mitmproxy/contentviews/_view_socketio.py +98 -0
- mitmproxy/contentviews/_view_urlencoded.py +33 -0
- mitmproxy/contentviews/_view_wbxml.py +25 -0
- mitmproxy/contentviews/_view_xml_html.py +278 -0
- mitmproxy/contentviews/_view_zip.py +22 -0
- mitmproxy/contentviews/base.py +129 -0
- mitmproxy/contrib/README.md +4 -0
- mitmproxy/contrib/__init__.py +0 -0
- mitmproxy/contrib/click/LICENSE.BSD-3 +28 -0
- mitmproxy/contrib/click/__init__.py +159 -0
- mitmproxy/contrib/imghdr.py +142 -0
- mitmproxy/contrib/kaitaistruct/LICENSE +1 -0
- mitmproxy/contrib/kaitaistruct/README.md +3 -0
- mitmproxy/contrib/kaitaistruct/__init__.py +0 -0
- mitmproxy/contrib/kaitaistruct/dtls_client_hello.ksy +140 -0
- mitmproxy/contrib/kaitaistruct/dtls_client_hello.py +202 -0
- mitmproxy/contrib/kaitaistruct/exif.py +654 -0
- mitmproxy/contrib/kaitaistruct/gif.py +357 -0
- mitmproxy/contrib/kaitaistruct/google_protobuf.py +126 -0
- mitmproxy/contrib/kaitaistruct/ico.py +93 -0
- mitmproxy/contrib/kaitaistruct/jpeg.py +258 -0
- mitmproxy/contrib/kaitaistruct/make.sh +14 -0
- mitmproxy/contrib/kaitaistruct/png.py +527 -0
- mitmproxy/contrib/kaitaistruct/tls_client_hello.ksy +132 -0
- mitmproxy/contrib/kaitaistruct/tls_client_hello.py +189 -0
- mitmproxy/contrib/kaitaistruct/vlq_base128_le.py +115 -0
- mitmproxy/contrib/wbxml/ASCommandResponse.py +73 -0
- mitmproxy/contrib/wbxml/ASWBXML.py +903 -0
- mitmproxy/contrib/wbxml/ASWBXMLByteQueue.py +102 -0
- mitmproxy/contrib/wbxml/ASWBXMLCodePage.py +52 -0
- mitmproxy/contrib/wbxml/GlobalTokens.py +50 -0
- mitmproxy/contrib/wbxml/InvalidDataException.py +31 -0
- mitmproxy/contrib/wbxml/__init__.py +0 -0
- mitmproxy/coretypes/__init__.py +0 -0
- mitmproxy/coretypes/bidi.py +26 -0
- mitmproxy/coretypes/multidict.py +206 -0
- mitmproxy/coretypes/serializable.py +199 -0
- mitmproxy/ctx.py +14 -0
- mitmproxy/dns.py +588 -0
- mitmproxy/eventsequence.py +84 -0
- mitmproxy/exceptions.py +51 -0
- mitmproxy/flow.py +288 -0
- mitmproxy/flowfilter.py +705 -0
- mitmproxy/hooks.py +95 -0
- mitmproxy/http.py +1295 -0
- mitmproxy/io/__init__.py +6 -0
- mitmproxy/io/compat.py +528 -0
- mitmproxy/io/har.py +159 -0
- mitmproxy/io/io.py +114 -0
- mitmproxy/io/tnetstring.py +261 -0
- mitmproxy/log.py +246 -0
- mitmproxy/master.py +145 -0
- mitmproxy/net/__init__.py +0 -0
- mitmproxy/net/check.py +43 -0
- mitmproxy/net/dns/__init__.py +0 -0
- mitmproxy/net/dns/classes.py +19 -0
- mitmproxy/net/dns/domain_names.py +169 -0
- mitmproxy/net/dns/https_records.py +134 -0
- mitmproxy/net/dns/op_codes.py +27 -0
- mitmproxy/net/dns/response_codes.py +58 -0
- mitmproxy/net/dns/types.py +193 -0
- mitmproxy/net/encoding.py +239 -0
- mitmproxy/net/free_port.py +25 -0
- mitmproxy/net/http/__init__.py +0 -0
- mitmproxy/net/http/cookies.py +387 -0
- mitmproxy/net/http/headers.py +113 -0
- mitmproxy/net/http/http1/__init__.py +21 -0
- mitmproxy/net/http/http1/assemble.py +99 -0
- mitmproxy/net/http/http1/read.py +308 -0
- mitmproxy/net/http/multipart.py +95 -0
- mitmproxy/net/http/status_codes.py +146 -0
- mitmproxy/net/http/url.py +200 -0
- mitmproxy/net/http/user_agents.py +60 -0
- mitmproxy/net/http/validate.py +141 -0
- mitmproxy/net/local_ip.py +40 -0
- mitmproxy/net/server_spec.py +85 -0
- mitmproxy/net/tls.py +330 -0
- mitmproxy/options.py +249 -0
- mitmproxy/optmanager.py +637 -0
- mitmproxy/platform/__init__.py +40 -0
- mitmproxy/platform/linux.py +33 -0
- mitmproxy/platform/openbsd.py +2 -0
- mitmproxy/platform/osx.py +39 -0
- mitmproxy/platform/pf.py +42 -0
- mitmproxy/platform/windows.py +599 -0
- mitmproxy/proxy/__init__.py +20 -0
- mitmproxy/proxy/commands.py +159 -0
- mitmproxy/proxy/context.py +58 -0
- mitmproxy/proxy/events.py +138 -0
- mitmproxy/proxy/layer.py +340 -0
- mitmproxy/proxy/layers/__init__.py +27 -0
- mitmproxy/proxy/layers/dns.py +190 -0
- mitmproxy/proxy/layers/http/__init__.py +1210 -0
- mitmproxy/proxy/layers/http/_base.py +61 -0
- mitmproxy/proxy/layers/http/_events.py +167 -0
- mitmproxy/proxy/layers/http/_hooks.py +122 -0
- mitmproxy/proxy/layers/http/_http1.py +502 -0
- mitmproxy/proxy/layers/http/_http2.py +714 -0
- mitmproxy/proxy/layers/http/_http3.py +309 -0
- mitmproxy/proxy/layers/http/_http_h2.py +207 -0
- mitmproxy/proxy/layers/http/_http_h3.py +321 -0
- mitmproxy/proxy/layers/http/_upstream_proxy.py +105 -0
- mitmproxy/proxy/layers/modes.py +303 -0
- mitmproxy/proxy/layers/quic/__init__.py +41 -0
- mitmproxy/proxy/layers/quic/_client_hello_parser.py +111 -0
- mitmproxy/proxy/layers/quic/_commands.py +92 -0
- mitmproxy/proxy/layers/quic/_events.py +70 -0
- mitmproxy/proxy/layers/quic/_hooks.py +77 -0
- mitmproxy/proxy/layers/quic/_raw_layers.py +433 -0
- mitmproxy/proxy/layers/quic/_stream_layers.py +638 -0
- mitmproxy/proxy/layers/tcp.py +143 -0
- mitmproxy/proxy/layers/tls.py +692 -0
- mitmproxy/proxy/layers/udp.py +132 -0
- mitmproxy/proxy/layers/websocket.py +272 -0
- mitmproxy/proxy/mode_servers.py +546 -0
- mitmproxy/proxy/mode_specs.py +336 -0
- mitmproxy/proxy/server.py +619 -0
- mitmproxy/proxy/server_hooks.py +76 -0
- mitmproxy/proxy/tunnel.py +201 -0
- mitmproxy/proxy/utils.py +68 -0
- mitmproxy/py.typed +0 -0
- mitmproxy/script/__init__.py +5 -0
- mitmproxy/script/concurrent.py +32 -0
- mitmproxy/tcp.py +75 -0
- mitmproxy/test/taddons.py +82 -0
- mitmproxy/test/tflow.py +300 -0
- mitmproxy/test/tutils.py +95 -0
- mitmproxy/tls.py +152 -0
- mitmproxy/tools/__init__.py +0 -0
- mitmproxy/tools/cmdline.py +152 -0
- mitmproxy/tools/console/__init__.py +3 -0
- mitmproxy/tools/console/commander/__init__.py +0 -0
- mitmproxy/tools/console/commander/commander.py +260 -0
- mitmproxy/tools/console/commandexecutor.py +35 -0
- mitmproxy/tools/console/commands.py +156 -0
- mitmproxy/tools/console/common.py +863 -0
- mitmproxy/tools/console/consoleaddons.py +763 -0
- mitmproxy/tools/console/defaultkeys.py +277 -0
- mitmproxy/tools/console/eventlog.py +63 -0
- mitmproxy/tools/console/flowdetailview.py +149 -0
- mitmproxy/tools/console/flowlist.py +108 -0
- mitmproxy/tools/console/flowview.py +497 -0
- mitmproxy/tools/console/grideditor/__init__.py +29 -0
- mitmproxy/tools/console/grideditor/base.py +469 -0
- mitmproxy/tools/console/grideditor/col_bytes.py +49 -0
- mitmproxy/tools/console/grideditor/col_subgrid.py +40 -0
- mitmproxy/tools/console/grideditor/col_text.py +48 -0
- mitmproxy/tools/console/grideditor/col_viewany.py +34 -0
- mitmproxy/tools/console/grideditor/editors.py +220 -0
- mitmproxy/tools/console/help.py +113 -0
- mitmproxy/tools/console/keybindings.py +163 -0
- mitmproxy/tools/console/keymap.py +262 -0
- mitmproxy/tools/console/layoutwidget.py +40 -0
- mitmproxy/tools/console/master.py +263 -0
- mitmproxy/tools/console/options.py +272 -0
- mitmproxy/tools/console/overlay.py +195 -0
- mitmproxy/tools/console/palettes.py +546 -0
- mitmproxy/tools/console/quickhelp.py +191 -0
- mitmproxy/tools/console/searchable.py +89 -0
- mitmproxy/tools/console/signals.py +66 -0
- mitmproxy/tools/console/statusbar.py +366 -0
- mitmproxy/tools/console/tabs.py +58 -0
- mitmproxy/tools/console/window.py +318 -0
- mitmproxy/tools/dump.py +26 -0
- mitmproxy/tools/main.py +174 -0
- mitmproxy/tools/web/__init__.py +3 -0
- mitmproxy/tools/web/app.py +939 -0
- mitmproxy/tools/web/index.html +16 -0
- mitmproxy/tools/web/master.py +122 -0
- mitmproxy/tools/web/static/favicon.ico +0 -0
- mitmproxy/tools/web/static/fontawesome-webfont-B-jkhYfk.woff2 +0 -0
- mitmproxy/tools/web/static/fontawesome-webfont-CDK5bt4p.woff +0 -0
- mitmproxy/tools/web/static/fontawesome-webfont-CQDK8MU3.ttf +0 -0
- mitmproxy/tools/web/static/fontawesome-webfont-D13rzr4g.svg +2671 -0
- mitmproxy/tools/web/static/fontawesome-webfont-G5YE5S7X.eot +0 -0
- mitmproxy/tools/web/static/index-Be7e-cwP.js +10 -0
- mitmproxy/tools/web/static/index-DhPPoJ7G.css +1 -0
- mitmproxy/tools/web/static/vendor-BS4xPthR.js +62 -0
- mitmproxy/tools/web/static/vendor-Cg3S-P9H.css +1 -0
- mitmproxy/tools/web/static_viewer.py +118 -0
- mitmproxy/tools/web/templates/login.html +35 -0
- mitmproxy/tools/web/web_columns.py +15 -0
- mitmproxy/tools/web/webaddons.py +151 -0
- mitmproxy/types.py +497 -0
- mitmproxy/udp.py +72 -0
- mitmproxy/utils/__init__.py +0 -0
- mitmproxy/utils/arg_check.py +167 -0
- mitmproxy/utils/asyncio_utils.py +100 -0
- mitmproxy/utils/data.py +37 -0
- mitmproxy/utils/debug.py +131 -0
- mitmproxy/utils/emoji.py +1893 -0
- mitmproxy/utils/htpasswd.py +81 -0
- mitmproxy/utils/human.py +99 -0
- mitmproxy/utils/magisk.py +112 -0
- mitmproxy/utils/pyinstaller/__init__.py +7 -0
- mitmproxy/utils/pyinstaller/hook-mitmproxy.addons.onboardingapp.py +3 -0
- mitmproxy/utils/pyinstaller/hook-mitmproxy.py +1 -0
- mitmproxy/utils/pyinstaller/hook-mitmproxy.tools.web.py +3 -0
- mitmproxy/utils/signals.py +137 -0
- mitmproxy/utils/sliding_window.py +32 -0
- mitmproxy/utils/spec.py +21 -0
- mitmproxy/utils/strutils.py +289 -0
- mitmproxy/utils/typecheck.py +72 -0
- mitmproxy/utils/vt_codes.py +60 -0
- mitmproxy/version.py +54 -0
- mitmproxy/websocket.py +182 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import email.utils
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
|
|
6
|
+
from mitmproxy.coretypes import multidict
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
A flexible module for cookie parsing and manipulation.
|
|
10
|
+
|
|
11
|
+
This module differs from usual standards-compliant cookie modules in a number
|
|
12
|
+
of ways. We try to be as permissive as possible, and to retain even mal-formed
|
|
13
|
+
information. Duplicate cookies are preserved in parsing, and can be set in
|
|
14
|
+
formatting. We do attempt to escape and quote values where needed, but will not
|
|
15
|
+
reject data that violate the specs.
|
|
16
|
+
|
|
17
|
+
Parsing accepts the formats in RFC6265 and partially RFC2109 and RFC2965. We
|
|
18
|
+
also parse the comma-separated variant of Set-Cookie that allows multiple
|
|
19
|
+
cookies to be set in a single header. Serialization follows RFC6265.
|
|
20
|
+
|
|
21
|
+
http://tools.ietf.org/html/rfc6265
|
|
22
|
+
http://tools.ietf.org/html/rfc2109
|
|
23
|
+
http://tools.ietf.org/html/rfc2965
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
_cookie_params = {
|
|
27
|
+
"expires",
|
|
28
|
+
"path",
|
|
29
|
+
"comment",
|
|
30
|
+
"max-age",
|
|
31
|
+
"secure",
|
|
32
|
+
"httponly",
|
|
33
|
+
"version",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ESCAPE = re.compile(r"([\"\\])")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CookieAttrs(multidict.MultiDict):
|
|
40
|
+
@staticmethod
|
|
41
|
+
def _kconv(key):
|
|
42
|
+
return key.lower()
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def _reduce_values(values):
|
|
46
|
+
# See the StickyCookieTest for a weird cookie that only makes sense
|
|
47
|
+
# if we take the last part.
|
|
48
|
+
return values[-1]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
TSetCookie = tuple[str, str | None, CookieAttrs]
|
|
52
|
+
TPairs = list[tuple[str, str | None]]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _read_until(s, start, term):
|
|
56
|
+
"""
|
|
57
|
+
Read until one of the characters in term is reached.
|
|
58
|
+
"""
|
|
59
|
+
if start == len(s):
|
|
60
|
+
return "", start + 1
|
|
61
|
+
for i in range(start, len(s)):
|
|
62
|
+
if s[i] in term:
|
|
63
|
+
return s[start:i], i
|
|
64
|
+
return s[start : i + 1], i + 1
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _read_quoted_string(s, start):
|
|
68
|
+
"""
|
|
69
|
+
start: offset to the first quote of the string to be read
|
|
70
|
+
|
|
71
|
+
A sort of loose super-set of the various quoted string specifications.
|
|
72
|
+
|
|
73
|
+
RFC6265 disallows backslashes or double quotes within quoted strings.
|
|
74
|
+
Prior RFCs use backslashes to escape. This leaves us free to apply
|
|
75
|
+
backslash escaping by default and be compatible with everything.
|
|
76
|
+
"""
|
|
77
|
+
escaping = False
|
|
78
|
+
ret = []
|
|
79
|
+
# Skip the first quote
|
|
80
|
+
i = start # initialize in case the loop doesn't run.
|
|
81
|
+
for i in range(start + 1, len(s)):
|
|
82
|
+
if escaping:
|
|
83
|
+
ret.append(s[i])
|
|
84
|
+
escaping = False
|
|
85
|
+
elif s[i] == '"':
|
|
86
|
+
break
|
|
87
|
+
elif s[i] == "\\":
|
|
88
|
+
escaping = True
|
|
89
|
+
else:
|
|
90
|
+
ret.append(s[i])
|
|
91
|
+
return "".join(ret), i + 1
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _read_key(s, start, delims=";="):
|
|
95
|
+
"""
|
|
96
|
+
Read a key - the LHS of a token/value pair in a cookie.
|
|
97
|
+
"""
|
|
98
|
+
return _read_until(s, start, delims)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _read_value(s, start, delims):
|
|
102
|
+
"""
|
|
103
|
+
Reads a value - the RHS of a token/value pair in a cookie.
|
|
104
|
+
"""
|
|
105
|
+
if start >= len(s):
|
|
106
|
+
return "", start
|
|
107
|
+
elif s[start] == '"':
|
|
108
|
+
return _read_quoted_string(s, start)
|
|
109
|
+
else:
|
|
110
|
+
return _read_until(s, start, delims)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _read_cookie_pairs(s, off=0):
|
|
114
|
+
"""
|
|
115
|
+
Read pairs of lhs=rhs values from Cookie headers.
|
|
116
|
+
|
|
117
|
+
off: start offset
|
|
118
|
+
"""
|
|
119
|
+
pairs = []
|
|
120
|
+
|
|
121
|
+
while True:
|
|
122
|
+
lhs, off = _read_key(s, off)
|
|
123
|
+
lhs = lhs.lstrip()
|
|
124
|
+
|
|
125
|
+
rhs = ""
|
|
126
|
+
if off < len(s) and s[off] == "=":
|
|
127
|
+
rhs, off = _read_value(s, off + 1, ";")
|
|
128
|
+
if rhs or lhs:
|
|
129
|
+
pairs.append([lhs, rhs])
|
|
130
|
+
|
|
131
|
+
off += 1
|
|
132
|
+
|
|
133
|
+
if not off < len(s):
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
return pairs, off
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _read_set_cookie_pairs(s: str, off=0) -> tuple[list[TPairs], int]:
|
|
140
|
+
"""
|
|
141
|
+
Read pairs of lhs=rhs values from SetCookie headers while handling multiple cookies.
|
|
142
|
+
|
|
143
|
+
off: start offset
|
|
144
|
+
specials: attributes that are treated specially
|
|
145
|
+
"""
|
|
146
|
+
cookies: list[TPairs] = []
|
|
147
|
+
pairs: TPairs = []
|
|
148
|
+
|
|
149
|
+
while True:
|
|
150
|
+
lhs, off = _read_key(s, off, ";=,")
|
|
151
|
+
lhs = lhs.lstrip()
|
|
152
|
+
|
|
153
|
+
rhs = ""
|
|
154
|
+
if off < len(s) and s[off] == "=":
|
|
155
|
+
rhs, off = _read_value(s, off + 1, ";,")
|
|
156
|
+
|
|
157
|
+
# Special handling of attributes
|
|
158
|
+
if lhs.lower() == "expires":
|
|
159
|
+
# 'expires' values can contain commas in them so they need to
|
|
160
|
+
# be handled separately.
|
|
161
|
+
|
|
162
|
+
# We actually bank on the fact that the expires value WILL
|
|
163
|
+
# contain a comma. Things will fail, if they don't.
|
|
164
|
+
|
|
165
|
+
# '3' is just a heuristic we use to determine whether we've
|
|
166
|
+
# only read a part of the expires value and we should read more.
|
|
167
|
+
if len(rhs) <= 3:
|
|
168
|
+
trail, off = _read_value(s, off + 1, ";,")
|
|
169
|
+
rhs = rhs + "," + trail
|
|
170
|
+
|
|
171
|
+
# as long as there's a "=", we consider it a pair
|
|
172
|
+
pairs.append((lhs, rhs))
|
|
173
|
+
|
|
174
|
+
elif lhs:
|
|
175
|
+
pairs.append((lhs, None))
|
|
176
|
+
|
|
177
|
+
# comma marks the beginning of a new cookie
|
|
178
|
+
if off < len(s) and s[off] == ",":
|
|
179
|
+
cookies.append(pairs)
|
|
180
|
+
pairs = []
|
|
181
|
+
|
|
182
|
+
off += 1
|
|
183
|
+
|
|
184
|
+
if not off < len(s):
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
if pairs or not cookies:
|
|
188
|
+
cookies.append(pairs)
|
|
189
|
+
|
|
190
|
+
return cookies, off
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _has_special(s: str) -> bool:
|
|
194
|
+
for i in s:
|
|
195
|
+
if i in '",;\\':
|
|
196
|
+
return True
|
|
197
|
+
o = ord(i)
|
|
198
|
+
if o < 0x21 or o > 0x7E:
|
|
199
|
+
return True
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _format_pairs(pairs, specials=(), sep="; "):
|
|
204
|
+
"""
|
|
205
|
+
specials: A lower-cased list of keys that will not be quoted.
|
|
206
|
+
"""
|
|
207
|
+
vals = []
|
|
208
|
+
for k, v in pairs:
|
|
209
|
+
if v is None:
|
|
210
|
+
val = k
|
|
211
|
+
elif k.lower() not in specials and _has_special(v):
|
|
212
|
+
v = ESCAPE.sub(r"\\\1", v)
|
|
213
|
+
v = '"%s"' % v
|
|
214
|
+
val = f"{k}={v}"
|
|
215
|
+
else:
|
|
216
|
+
val = f"{k}={v}"
|
|
217
|
+
vals.append(val)
|
|
218
|
+
return sep.join(vals)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _format_set_cookie_pairs(lst):
|
|
222
|
+
return _format_pairs(lst, specials=("expires", "path"))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def parse_cookie_header(line):
|
|
226
|
+
"""
|
|
227
|
+
Parse a Cookie header value.
|
|
228
|
+
Returns a list of (lhs, rhs) tuples.
|
|
229
|
+
"""
|
|
230
|
+
pairs, off_ = _read_cookie_pairs(line)
|
|
231
|
+
return pairs
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def parse_cookie_headers(cookie_headers):
|
|
235
|
+
cookie_list = []
|
|
236
|
+
for header in cookie_headers:
|
|
237
|
+
cookie_list.extend(parse_cookie_header(header))
|
|
238
|
+
return cookie_list
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def format_cookie_header(lst):
|
|
242
|
+
"""
|
|
243
|
+
Formats a Cookie header value.
|
|
244
|
+
"""
|
|
245
|
+
return _format_pairs(lst)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def parse_set_cookie_header(line: str) -> list[TSetCookie]:
|
|
249
|
+
"""
|
|
250
|
+
Parse a Set-Cookie header value
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
A list of (name, value, attrs) tuples, where attrs is a
|
|
254
|
+
CookieAttrs dict of attributes. No attempt is made to parse attribute
|
|
255
|
+
values - they are treated purely as strings.
|
|
256
|
+
"""
|
|
257
|
+
cookie_pairs, off = _read_set_cookie_pairs(line)
|
|
258
|
+
cookies = []
|
|
259
|
+
for pairs in cookie_pairs:
|
|
260
|
+
if pairs:
|
|
261
|
+
cookie, *attrs = pairs
|
|
262
|
+
cookies.append((cookie[0], cookie[1], CookieAttrs(attrs)))
|
|
263
|
+
return cookies
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def parse_set_cookie_headers(headers: Iterable[str]) -> list[TSetCookie]:
|
|
267
|
+
rv = []
|
|
268
|
+
for header in headers:
|
|
269
|
+
cookies = parse_set_cookie_header(header)
|
|
270
|
+
rv.extend(cookies)
|
|
271
|
+
return rv
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def format_set_cookie_header(set_cookies: list[TSetCookie]) -> str:
|
|
275
|
+
"""
|
|
276
|
+
Formats a Set-Cookie header value.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
rv = []
|
|
280
|
+
|
|
281
|
+
for name, value, attrs in set_cookies:
|
|
282
|
+
pairs = [(name, value)]
|
|
283
|
+
pairs.extend(attrs.fields if hasattr(attrs, "fields") else attrs)
|
|
284
|
+
|
|
285
|
+
rv.append(_format_set_cookie_pairs(pairs))
|
|
286
|
+
|
|
287
|
+
return ", ".join(rv)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def refresh_set_cookie_header(c: str, delta: int) -> str:
|
|
291
|
+
"""
|
|
292
|
+
Args:
|
|
293
|
+
c: A Set-Cookie string
|
|
294
|
+
delta: Time delta in seconds
|
|
295
|
+
Returns:
|
|
296
|
+
A refreshed Set-Cookie string
|
|
297
|
+
Raises:
|
|
298
|
+
ValueError, if the cookie is invalid.
|
|
299
|
+
"""
|
|
300
|
+
cookies = parse_set_cookie_header(c)
|
|
301
|
+
for cookie in cookies:
|
|
302
|
+
name, value, attrs = cookie
|
|
303
|
+
if not name or not value:
|
|
304
|
+
raise ValueError("Invalid Cookie")
|
|
305
|
+
|
|
306
|
+
if "expires" in attrs:
|
|
307
|
+
e = email.utils.parsedate_tz(attrs["expires"])
|
|
308
|
+
if e:
|
|
309
|
+
f = email.utils.mktime_tz(e) + delta
|
|
310
|
+
attrs.set_all("expires", [email.utils.formatdate(f, usegmt=True)])
|
|
311
|
+
else:
|
|
312
|
+
# This can happen when the expires tag is invalid.
|
|
313
|
+
# reddit.com sends a an expires tag like this: "Thu, 31 Dec
|
|
314
|
+
# 2037 23:59:59 GMT", which is valid RFC 1123, but not
|
|
315
|
+
# strictly correct according to the cookie spec. Browsers
|
|
316
|
+
# appear to parse this tolerantly - maybe we should too.
|
|
317
|
+
# For now, we just ignore this.
|
|
318
|
+
del attrs["expires"]
|
|
319
|
+
return format_set_cookie_header(cookies)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def get_expiration_ts(cookie_attrs):
|
|
323
|
+
"""
|
|
324
|
+
Determines the time when the cookie will be expired.
|
|
325
|
+
|
|
326
|
+
Considering both 'expires' and 'max-age' parameters.
|
|
327
|
+
|
|
328
|
+
Returns: timestamp of when the cookie will expire.
|
|
329
|
+
None, if no expiration time is set.
|
|
330
|
+
"""
|
|
331
|
+
if "expires" in cookie_attrs:
|
|
332
|
+
e = email.utils.parsedate_tz(cookie_attrs["expires"])
|
|
333
|
+
if e:
|
|
334
|
+
return email.utils.mktime_tz(e)
|
|
335
|
+
|
|
336
|
+
elif "max-age" in cookie_attrs:
|
|
337
|
+
try:
|
|
338
|
+
max_age = int(cookie_attrs["Max-Age"])
|
|
339
|
+
except ValueError:
|
|
340
|
+
pass
|
|
341
|
+
else:
|
|
342
|
+
now_ts = time.time()
|
|
343
|
+
return now_ts + max_age
|
|
344
|
+
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def is_expired(cookie_attrs):
|
|
349
|
+
"""
|
|
350
|
+
Determines whether a cookie has expired.
|
|
351
|
+
|
|
352
|
+
Returns: boolean
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
exp_ts = get_expiration_ts(cookie_attrs)
|
|
356
|
+
now_ts = time.time()
|
|
357
|
+
|
|
358
|
+
# If no expiration information was provided with the cookie
|
|
359
|
+
if exp_ts is None:
|
|
360
|
+
return False
|
|
361
|
+
else:
|
|
362
|
+
return exp_ts <= now_ts
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def group_cookies(pairs):
|
|
366
|
+
"""
|
|
367
|
+
Converts a list of pairs to a (name, value, attrs) for each cookie.
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
if not pairs:
|
|
371
|
+
return []
|
|
372
|
+
|
|
373
|
+
cookie_list = []
|
|
374
|
+
|
|
375
|
+
# First pair is always a new cookie
|
|
376
|
+
name, value = pairs[0]
|
|
377
|
+
attrs = []
|
|
378
|
+
|
|
379
|
+
for k, v in pairs[1:]:
|
|
380
|
+
if k.lower() in _cookie_params:
|
|
381
|
+
attrs.append((k, v))
|
|
382
|
+
else:
|
|
383
|
+
cookie_list.append((name, value, CookieAttrs(attrs)))
|
|
384
|
+
name, value, attrs = k, v, []
|
|
385
|
+
|
|
386
|
+
cookie_list.append((name, value, CookieAttrs(attrs)))
|
|
387
|
+
return cookie_list
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def parse_content_type(c: str) -> tuple[str, str, dict[str, str]] | None:
|
|
6
|
+
"""
|
|
7
|
+
A simple parser for content-type values. Returns a (type, subtype,
|
|
8
|
+
parameters) tuple, where type and subtype are strings, and parameters
|
|
9
|
+
is a dict. If the string could not be parsed, return None.
|
|
10
|
+
|
|
11
|
+
E.g. the following string:
|
|
12
|
+
|
|
13
|
+
text/html; charset=UTF-8
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
|
|
17
|
+
("text", "html", {"charset": "UTF-8"})
|
|
18
|
+
"""
|
|
19
|
+
parts = c.split(";", 1)
|
|
20
|
+
ts = parts[0].split("/", 1)
|
|
21
|
+
if len(ts) != 2:
|
|
22
|
+
return None
|
|
23
|
+
d = collections.OrderedDict()
|
|
24
|
+
if len(parts) == 2:
|
|
25
|
+
for i in parts[1].split(";"):
|
|
26
|
+
clause = i.split("=", 1)
|
|
27
|
+
if len(clause) == 2:
|
|
28
|
+
d[clause[0].strip()] = clause[1].strip()
|
|
29
|
+
return ts[0].lower(), ts[1].lower(), d
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def assemble_content_type(type, subtype, parameters):
|
|
33
|
+
if not parameters:
|
|
34
|
+
return f"{type}/{subtype}"
|
|
35
|
+
params = "; ".join(f"{k}={v}" for k, v in parameters.items())
|
|
36
|
+
return f"{type}/{subtype}; {params}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def infer_content_encoding(content_type: str, content: bytes = b"") -> str:
|
|
40
|
+
"""
|
|
41
|
+
Infer the encoding of content from the content-type header.
|
|
42
|
+
"""
|
|
43
|
+
enc = None
|
|
44
|
+
|
|
45
|
+
# BOM has the highest priority
|
|
46
|
+
if content.startswith(b"\x00\x00\xfe\xff"):
|
|
47
|
+
enc = "utf-32be"
|
|
48
|
+
elif content.startswith(b"\xff\xfe\x00\x00"):
|
|
49
|
+
enc = "utf-32le"
|
|
50
|
+
elif content.startswith(b"\xfe\xff"):
|
|
51
|
+
enc = "utf-16be"
|
|
52
|
+
elif content.startswith(b"\xff\xfe"):
|
|
53
|
+
enc = "utf-16le"
|
|
54
|
+
elif content.startswith(b"\xef\xbb\xbf"):
|
|
55
|
+
# 'utf-8-sig' will strip the BOM on decode
|
|
56
|
+
enc = "utf-8-sig"
|
|
57
|
+
elif parsed_content_type := parse_content_type(content_type):
|
|
58
|
+
# Use the charset from the header if possible
|
|
59
|
+
enc = parsed_content_type[2].get("charset")
|
|
60
|
+
|
|
61
|
+
# Otherwise, infer the encoding
|
|
62
|
+
if not enc and "json" in content_type:
|
|
63
|
+
enc = "utf8"
|
|
64
|
+
|
|
65
|
+
if not enc and "html" in content_type:
|
|
66
|
+
meta_charset = re.search(
|
|
67
|
+
rb"""<meta[^>]+charset=['"]?([^'">]+)""", content, re.IGNORECASE
|
|
68
|
+
)
|
|
69
|
+
if meta_charset:
|
|
70
|
+
enc = meta_charset.group(1).decode("ascii", "ignore")
|
|
71
|
+
else:
|
|
72
|
+
# Fallback to utf8 for html
|
|
73
|
+
# Ref: https://html.spec.whatwg.org/multipage/parsing.html#determining-the-character-encoding
|
|
74
|
+
# > 9. [snip] the comprehensive UTF-8 encoding is suggested.
|
|
75
|
+
enc = "utf8"
|
|
76
|
+
|
|
77
|
+
if not enc and "xml" in content_type:
|
|
78
|
+
if xml_encoding := re.search(
|
|
79
|
+
rb"""<\?xml[^\?>]+encoding=['"]([^'"\?>]+)""", content, re.IGNORECASE
|
|
80
|
+
):
|
|
81
|
+
enc = xml_encoding.group(1).decode("ascii", "ignore")
|
|
82
|
+
else:
|
|
83
|
+
# Fallback to utf8 for xml
|
|
84
|
+
# Ref: https://datatracker.ietf.org/doc/html/rfc7303#section-8.5
|
|
85
|
+
# > the XML processor [snip] to determine an encoding of UTF-8.
|
|
86
|
+
enc = "utf8"
|
|
87
|
+
|
|
88
|
+
if not enc and ("javascript" in content_type or "ecmascript" in content_type):
|
|
89
|
+
# Fallback to utf8 for javascript
|
|
90
|
+
# Ref: https://datatracker.ietf.org/doc/html/rfc9239#section-4.2
|
|
91
|
+
# > 3. Else, the character encoding scheme is assumed to be UTF-8
|
|
92
|
+
enc = "utf8"
|
|
93
|
+
|
|
94
|
+
if not enc and "text/css" in content_type:
|
|
95
|
+
# @charset rule must be the very first thing.
|
|
96
|
+
css_charset = re.match(rb"""@charset "([^"]+)";""", content, re.IGNORECASE)
|
|
97
|
+
if css_charset:
|
|
98
|
+
enc = css_charset.group(1).decode("ascii", "ignore")
|
|
99
|
+
else:
|
|
100
|
+
# Fallback to utf8 for css
|
|
101
|
+
# Ref: https://drafts.csswg.org/css-syntax/#determine-the-fallback-encoding
|
|
102
|
+
# > 4. Otherwise, return utf-8
|
|
103
|
+
enc = "utf8"
|
|
104
|
+
|
|
105
|
+
# Fallback to latin-1
|
|
106
|
+
if not enc:
|
|
107
|
+
enc = "latin-1"
|
|
108
|
+
|
|
109
|
+
# Use GB 18030 as the superset of GB2312 and GBK to fix common encoding problems on Chinese websites.
|
|
110
|
+
if enc.lower() in ("gb2312", "gbk"):
|
|
111
|
+
enc = "gb18030"
|
|
112
|
+
|
|
113
|
+
return enc
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .assemble import assemble_body
|
|
2
|
+
from .assemble import assemble_request
|
|
3
|
+
from .assemble import assemble_request_head
|
|
4
|
+
from .assemble import assemble_response
|
|
5
|
+
from .assemble import assemble_response_head
|
|
6
|
+
from .read import connection_close
|
|
7
|
+
from .read import expected_http_body_size
|
|
8
|
+
from .read import read_request_head
|
|
9
|
+
from .read import read_response_head
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"read_request_head",
|
|
13
|
+
"read_response_head",
|
|
14
|
+
"connection_close",
|
|
15
|
+
"expected_http_body_size",
|
|
16
|
+
"assemble_request",
|
|
17
|
+
"assemble_request_head",
|
|
18
|
+
"assemble_response",
|
|
19
|
+
"assemble_response_head",
|
|
20
|
+
"assemble_body",
|
|
21
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
def assemble_request(request):
|
|
2
|
+
if request.data.content is None:
|
|
3
|
+
raise ValueError("Cannot assemble flow with missing content")
|
|
4
|
+
head = assemble_request_head(request)
|
|
5
|
+
body = b"".join(
|
|
6
|
+
assemble_body(
|
|
7
|
+
request.data.headers, [request.data.content], request.data.trailers
|
|
8
|
+
)
|
|
9
|
+
)
|
|
10
|
+
return head + body
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def assemble_request_head(request):
|
|
14
|
+
first_line = _assemble_request_line(request.data)
|
|
15
|
+
headers = _assemble_request_headers(request.data)
|
|
16
|
+
return b"%s\r\n%s\r\n" % (first_line, headers)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def assemble_response(response):
|
|
20
|
+
if response.data.content is None:
|
|
21
|
+
raise ValueError("Cannot assemble flow with missing content")
|
|
22
|
+
head = assemble_response_head(response)
|
|
23
|
+
body = b"".join(
|
|
24
|
+
assemble_body(
|
|
25
|
+
response.data.headers, [response.data.content], response.data.trailers
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
return head + body
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def assemble_response_head(response):
|
|
32
|
+
first_line = _assemble_response_line(response.data)
|
|
33
|
+
headers = _assemble_response_headers(response.data)
|
|
34
|
+
return b"%s\r\n%s\r\n" % (first_line, headers)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def assemble_body(headers, body_chunks, trailers):
|
|
38
|
+
if "chunked" in headers.get("transfer-encoding", "").lower():
|
|
39
|
+
for chunk in body_chunks:
|
|
40
|
+
if chunk:
|
|
41
|
+
yield b"%x\r\n%s\r\n" % (len(chunk), chunk)
|
|
42
|
+
if trailers:
|
|
43
|
+
yield b"0\r\n%s\r\n" % trailers
|
|
44
|
+
else:
|
|
45
|
+
yield b"0\r\n\r\n"
|
|
46
|
+
else:
|
|
47
|
+
if trailers:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"Sending HTTP/1.1 trailer headers requires transfer-encoding: chunked"
|
|
50
|
+
)
|
|
51
|
+
for chunk in body_chunks:
|
|
52
|
+
yield chunk
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _assemble_request_line(request_data):
|
|
56
|
+
"""
|
|
57
|
+
Args:
|
|
58
|
+
request_data (mitmproxy.net.http.request.RequestData)
|
|
59
|
+
"""
|
|
60
|
+
if request_data.method.upper() == b"CONNECT":
|
|
61
|
+
return b"%s %s %s" % (
|
|
62
|
+
request_data.method,
|
|
63
|
+
request_data.authority,
|
|
64
|
+
request_data.http_version,
|
|
65
|
+
)
|
|
66
|
+
elif request_data.authority:
|
|
67
|
+
return b"%s %s://%s%s %s" % (
|
|
68
|
+
request_data.method,
|
|
69
|
+
request_data.scheme,
|
|
70
|
+
request_data.authority,
|
|
71
|
+
request_data.path,
|
|
72
|
+
request_data.http_version,
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
return b"%s %s %s" % (
|
|
76
|
+
request_data.method,
|
|
77
|
+
request_data.path,
|
|
78
|
+
request_data.http_version,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _assemble_request_headers(request_data):
|
|
83
|
+
"""
|
|
84
|
+
Args:
|
|
85
|
+
request_data (mitmproxy.net.http.request.RequestData)
|
|
86
|
+
"""
|
|
87
|
+
return bytes(request_data.headers)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _assemble_response_line(response_data):
|
|
91
|
+
return b"%s %d %s" % (
|
|
92
|
+
response_data.http_version,
|
|
93
|
+
response_data.status_code,
|
|
94
|
+
response_data.reason,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _assemble_response_headers(response):
|
|
99
|
+
return bytes(response.headers)
|