ssl2 1.0.0__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.
- ssl2/__init__.py +106 -0
- ssl2/asynchronous.py +217 -0
- ssl2/client_hello.py +353 -0
- ssl2/credentials.py +125 -0
- ssl2/errors.py +22 -0
- ssl2/fingerprint.py +326 -0
- ssl2/interface.py +398 -0
- ssl2/parameters.py +731 -0
- ssl2/proxy.py +308 -0
- ssl2/py.typed +0 -0
- ssl2-1.0.0.dist-info/METADATA +180 -0
- ssl2-1.0.0.dist-info/RECORD +13 -0
- ssl2-1.0.0.dist-info/WHEEL +4 -0
ssl2/credentials.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import ctypes
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Callable, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Final, Self
|
|
9
|
+
|
|
10
|
+
from ssl2.errors import ConfigurationError
|
|
11
|
+
|
|
12
|
+
LINUX_CANDIDATES: Final = (
|
|
13
|
+
Path("/etc/ssl/certs/ca-certificates.crt"),
|
|
14
|
+
Path("/etc/pki/tls/certs/ca-bundle.crt"),
|
|
15
|
+
Path("/etc/ssl/ca-bundle.pem"),
|
|
16
|
+
Path("/etc/pki/tls/cacert.pem"),
|
|
17
|
+
Path("/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"),
|
|
18
|
+
Path("/etc/ssl/cert.pem"),
|
|
19
|
+
Path("/etc/ssl/certs"),
|
|
20
|
+
Path("/etc/pki/tls/certs"),
|
|
21
|
+
Path("/system/etc/security/cacerts"),
|
|
22
|
+
)
|
|
23
|
+
DARWIN_CANDIDATES: Final = (
|
|
24
|
+
Path("/etc/ssl/cert.pem"),
|
|
25
|
+
Path("/private/etc/ssl/cert.pem"),
|
|
26
|
+
)
|
|
27
|
+
X509_ASN_ENCODING: Final = 0x00000001
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class Credential:
|
|
32
|
+
buffer: bytes | None = None
|
|
33
|
+
path: str | os.PathLike[str] | None = None
|
|
34
|
+
id: bytes | None = None
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_buffer(cls, buffer: bytes) -> Self:
|
|
38
|
+
return cls(buffer=buffer)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_file(cls, path: str | os.PathLike[str]) -> Self:
|
|
42
|
+
return cls(path=path)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_id(cls, id: bytes) -> Self:
|
|
46
|
+
return cls(id=id)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class Certificate(Credential):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class PrivateKey(Credential):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True, slots=True)
|
|
60
|
+
class TrustStore(Credential):
|
|
61
|
+
@classmethod
|
|
62
|
+
def system(cls) -> Self:
|
|
63
|
+
for variable in ("SSL_CERT_FILE", "SSL_CERT_DIR"):
|
|
64
|
+
if variable in os.environ:
|
|
65
|
+
return cls(path=os.environ[variable])
|
|
66
|
+
if sys.platform == "win32":
|
|
67
|
+
return cls(buffer=cls._windows_bundle(ctypes.WinDLL("crypt32.dll")))
|
|
68
|
+
candidates = DARWIN_CANDIDATES if sys.platform == "darwin" else LINUX_CANDIDATES
|
|
69
|
+
for candidate in candidates:
|
|
70
|
+
if candidate.exists():
|
|
71
|
+
return cls(path=candidate)
|
|
72
|
+
raise ConfigurationError(f"no system trust store found on {sys.platform}")
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _crypt32(
|
|
76
|
+
dll: ctypes.CDLL,
|
|
77
|
+
) -> tuple[
|
|
78
|
+
Callable[[None, str], int | None],
|
|
79
|
+
Callable[[int, int | None], int | None],
|
|
80
|
+
Callable[[int, int], int],
|
|
81
|
+
]:
|
|
82
|
+
dll.CertOpenSystemStoreW.restype = ctypes.c_void_p
|
|
83
|
+
dll.CertOpenSystemStoreW.argtypes = (ctypes.c_void_p, ctypes.c_wchar_p)
|
|
84
|
+
dll.CertEnumCertificatesInStore.restype = ctypes.c_void_p
|
|
85
|
+
dll.CertEnumCertificatesInStore.argtypes = (ctypes.c_void_p, ctypes.c_void_p)
|
|
86
|
+
dll.CertCloseStore.argtypes = (ctypes.c_void_p, ctypes.c_uint32)
|
|
87
|
+
return (
|
|
88
|
+
dll.CertOpenSystemStoreW,
|
|
89
|
+
dll.CertEnumCertificatesInStore,
|
|
90
|
+
dll.CertCloseStore,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def _windows_bundle(cls, dll: ctypes.CDLL) -> bytes:
|
|
95
|
+
open_store, enumerate_store, close_store = cls._crypt32(dll)
|
|
96
|
+
# CERT_CONTEXT starts with a u32 encoding type, a pointer to the DER bytes and their u32 length, each at pointer alignment
|
|
97
|
+
pointer = ctypes.sizeof(ctypes.c_void_p)
|
|
98
|
+
chunks: list[bytes] = []
|
|
99
|
+
for name in ("ROOT", "CA"):
|
|
100
|
+
store = open_store(None, name)
|
|
101
|
+
if store is None:
|
|
102
|
+
raise ConfigurationError(
|
|
103
|
+
f"cannot open the Windows {name} certificate store"
|
|
104
|
+
)
|
|
105
|
+
context = enumerate_store(store, None)
|
|
106
|
+
while context is not None:
|
|
107
|
+
encoding_type = ctypes.c_uint32.from_address(context).value
|
|
108
|
+
encoded = ctypes.c_void_p.from_address(context + pointer).value
|
|
109
|
+
length = ctypes.c_uint32.from_address(context + 2 * pointer).value
|
|
110
|
+
if encoding_type & X509_ASN_ENCODING and encoded is not None:
|
|
111
|
+
der = ctypes.string_at(encoded, length)
|
|
112
|
+
chunks.append(
|
|
113
|
+
b"-----BEGIN CERTIFICATE-----\n"
|
|
114
|
+
+ base64.encodebytes(der)
|
|
115
|
+
+ b"-----END CERTIFICATE-----\n"
|
|
116
|
+
)
|
|
117
|
+
context = enumerate_store(store, context)
|
|
118
|
+
_ = close_store(store, 0)
|
|
119
|
+
return b"".join(chunks)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True, slots=True)
|
|
123
|
+
class SigningChain:
|
|
124
|
+
leaf: tuple[Certificate, PrivateKey | None]
|
|
125
|
+
chain: Sequence[Certificate] | None = None
|
ssl2/errors.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class TLSError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class WantWriteError(TLSError):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WantReadError(TLSError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RaggedEOF(TLSError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfigurationError(TLSError):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EarlyDataRejected(TLSError):
|
|
22
|
+
pass
|
ssl2/fingerprint.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from typing import Final, Self
|
|
5
|
+
|
|
6
|
+
from ssl2.client_hello import ClientHello, ClientHelloBuilder
|
|
7
|
+
from ssl2.parameters import (
|
|
8
|
+
CipherSuite,
|
|
9
|
+
CodePoint,
|
|
10
|
+
ECPointFormat,
|
|
11
|
+
ExtensionType,
|
|
12
|
+
Http2SettingIdentifier,
|
|
13
|
+
NamedGroup,
|
|
14
|
+
ProtocolVersion,
|
|
15
|
+
SignatureScheme,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
JA4_HASH_LENGTH: Final = 12
|
|
19
|
+
JA4_UNHASHED_EXTENSIONS: Final = frozenset(
|
|
20
|
+
{ExtensionType.server_name, ExtensionType.application_layer_protocol_negotiation}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class JA3:
|
|
26
|
+
version: ProtocolVersion
|
|
27
|
+
cipher_suites: tuple[CipherSuite, ...]
|
|
28
|
+
extensions: tuple[ExtensionType, ...]
|
|
29
|
+
supported_groups: tuple[NamedGroup, ...]
|
|
30
|
+
ec_point_formats: tuple[ECPointFormat, ...]
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def of(cls, client_hello: ClientHello) -> Self:
|
|
34
|
+
return cls(
|
|
35
|
+
client_hello.legacy_version,
|
|
36
|
+
client_hello.cipher_suites,
|
|
37
|
+
client_hello.extensions,
|
|
38
|
+
client_hello.supported_groups,
|
|
39
|
+
client_hello.ec_point_formats,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def parse(cls, text: str) -> Self:
|
|
44
|
+
fields = text.split(",")
|
|
45
|
+
if len(fields) != 5:
|
|
46
|
+
raise ValueError("JA3 text needs five comma separated fields")
|
|
47
|
+
version, cipher_suites, extensions, supported_groups, ec_point_formats = fields
|
|
48
|
+
return cls(
|
|
49
|
+
ProtocolVersion(int(version)),
|
|
50
|
+
cls._values(CipherSuite, cipher_suites),
|
|
51
|
+
cls._values(ExtensionType, extensions),
|
|
52
|
+
cls._values(NamedGroup, supported_groups),
|
|
53
|
+
cls._values(ECPointFormat, ec_point_formats),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def builder(self) -> ClientHelloBuilder:
|
|
57
|
+
return (
|
|
58
|
+
ClientHelloBuilder()
|
|
59
|
+
.legacy_version(self.version)
|
|
60
|
+
.cipher_suites(*self.cipher_suites)
|
|
61
|
+
.extensions(*self.extensions)
|
|
62
|
+
.supported_groups(*self.supported_groups)
|
|
63
|
+
.ec_point_formats(*self.ec_point_formats)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def text(self) -> str:
|
|
68
|
+
fields = (
|
|
69
|
+
self.cipher_suites,
|
|
70
|
+
self.extensions,
|
|
71
|
+
self.supported_groups,
|
|
72
|
+
self.ec_point_formats,
|
|
73
|
+
)
|
|
74
|
+
return ",".join(
|
|
75
|
+
(str(int(self.version)), *(self._join(field) for field in fields))
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def digest(self) -> str:
|
|
80
|
+
return hashlib.md5(self.text.encode(), usedforsecurity=False).hexdigest()
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def _join(values: tuple[CodePoint, ...]) -> str:
|
|
84
|
+
return "-".join(str(int(value)) for value in values)
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _values[CodePointT: CodePoint](
|
|
88
|
+
registry: type[CodePointT], text: str
|
|
89
|
+
) -> tuple[CodePointT, ...]:
|
|
90
|
+
return tuple(registry(int(value)) for value in text.split("-") if value)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class JA4Transport(StrEnum):
|
|
94
|
+
TCP = "t"
|
|
95
|
+
QUIC = "q"
|
|
96
|
+
DTLS = "d"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class JA4Version(StrEnum):
|
|
100
|
+
TLS_1_3 = "13"
|
|
101
|
+
TLS_1_2 = "12"
|
|
102
|
+
TLS_1_1 = "11"
|
|
103
|
+
TLS_1_0 = "10"
|
|
104
|
+
SSL_3_0 = "s3"
|
|
105
|
+
DTLS_1_0 = "d1"
|
|
106
|
+
DTLS_1_2 = "d2"
|
|
107
|
+
DTLS_1_3 = "d3"
|
|
108
|
+
UNKNOWN = "00"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True, slots=True)
|
|
112
|
+
class JA4:
|
|
113
|
+
transport: JA4Transport
|
|
114
|
+
version: JA4Version
|
|
115
|
+
server_name_indicated: bool
|
|
116
|
+
cipher_count: int
|
|
117
|
+
extension_count: int
|
|
118
|
+
application_protocol_code: str
|
|
119
|
+
cipher_suites: tuple[CipherSuite, ...]
|
|
120
|
+
extensions: tuple[ExtensionType, ...]
|
|
121
|
+
signature_algorithms: tuple[SignatureScheme, ...]
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def of(
|
|
125
|
+
cls, client_hello: ClientHello, transport: JA4Transport = JA4Transport.TCP
|
|
126
|
+
) -> Self:
|
|
127
|
+
return cls(
|
|
128
|
+
transport,
|
|
129
|
+
JA4Version.__members__.get(
|
|
130
|
+
client_hello.highest_version.name, JA4Version.UNKNOWN
|
|
131
|
+
),
|
|
132
|
+
client_hello.offers(ExtensionType.server_name),
|
|
133
|
+
len(client_hello.cipher_suites),
|
|
134
|
+
len(client_hello.extensions),
|
|
135
|
+
cls._protocol_code(client_hello.application_protocols),
|
|
136
|
+
client_hello.cipher_suites,
|
|
137
|
+
client_hello.extensions,
|
|
138
|
+
client_hello.signature_algorithms,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def parse(cls, text: str) -> Self:
|
|
143
|
+
sections = text.split("_")
|
|
144
|
+
if len(sections) not in (3, 4):
|
|
145
|
+
raise ValueError(
|
|
146
|
+
"JA4 text needs three or four underscore separated sections"
|
|
147
|
+
)
|
|
148
|
+
prefix = sections[0]
|
|
149
|
+
if len(prefix) != 10:
|
|
150
|
+
raise ValueError("the JA4 prefix is ten characters")
|
|
151
|
+
if any(cls._is_hash(section) for section in sections[1:3]):
|
|
152
|
+
raise ValueError(
|
|
153
|
+
"hashed JA4 carries no cipher or extension lists; parse the raw or original form"
|
|
154
|
+
)
|
|
155
|
+
signature_algorithms = (
|
|
156
|
+
cls._values(SignatureScheme, sections[3]) if len(sections) == 4 else ()
|
|
157
|
+
)
|
|
158
|
+
return cls(
|
|
159
|
+
JA4Transport(prefix[0]),
|
|
160
|
+
JA4Version(prefix[1:3]),
|
|
161
|
+
prefix[3] == "d",
|
|
162
|
+
int(prefix[4:6]),
|
|
163
|
+
int(prefix[6:8]),
|
|
164
|
+
prefix[8:10],
|
|
165
|
+
cls._values(CipherSuite, sections[1]),
|
|
166
|
+
cls._values(ExtensionType, sections[2]),
|
|
167
|
+
signature_algorithms,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def prefix(self) -> str:
|
|
172
|
+
server_name = "d" if self.server_name_indicated else "i"
|
|
173
|
+
counts = f"{min(self.cipher_count, 99):02d}{min(self.extension_count, 99):02d}"
|
|
174
|
+
return f"{self.transport}{self.version}{server_name}{counts}{self.application_protocol_code}"
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def text(self) -> str:
|
|
178
|
+
return "_".join(
|
|
179
|
+
(
|
|
180
|
+
self.prefix,
|
|
181
|
+
self._hash(self._sorted_ciphers),
|
|
182
|
+
self._hash(self._sorted_extensions),
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def raw(self) -> str:
|
|
188
|
+
return f"{self.prefix}_{self._sorted_ciphers}_{self._sorted_extensions}"
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def original(self) -> str:
|
|
192
|
+
return "_".join(
|
|
193
|
+
(
|
|
194
|
+
self.prefix,
|
|
195
|
+
self._join(self.cipher_suites),
|
|
196
|
+
self._with_signature_algorithms(self._join(self.extensions)),
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def _sorted_ciphers(self) -> str:
|
|
202
|
+
return ",".join(sorted(cipher.hex for cipher in self.cipher_suites))
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def _sorted_extensions(self) -> str:
|
|
206
|
+
kept = sorted(
|
|
207
|
+
extension.hex
|
|
208
|
+
for extension in self.extensions
|
|
209
|
+
if extension not in JA4_UNHASHED_EXTENSIONS
|
|
210
|
+
)
|
|
211
|
+
return self._with_signature_algorithms(",".join(kept))
|
|
212
|
+
|
|
213
|
+
def _with_signature_algorithms(self, extensions: str) -> str:
|
|
214
|
+
if not extensions or not self.signature_algorithms:
|
|
215
|
+
return extensions
|
|
216
|
+
return extensions + "_" + self._join(self.signature_algorithms)
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def _join(values: tuple[CodePoint, ...]) -> str:
|
|
220
|
+
return ",".join(value.hex for value in values)
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def _values[CodePointT: CodePoint](
|
|
224
|
+
registry: type[CodePointT], text: str
|
|
225
|
+
) -> tuple[CodePointT, ...]:
|
|
226
|
+
return tuple(registry(int(value, 16)) for value in text.split(",") if value)
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def _hash(section: str) -> str:
|
|
230
|
+
if not section:
|
|
231
|
+
return "0" * JA4_HASH_LENGTH
|
|
232
|
+
return hashlib.sha256(section.encode()).hexdigest()[:JA4_HASH_LENGTH]
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def _is_hash(section: str) -> bool:
|
|
236
|
+
return len(section) == JA4_HASH_LENGTH and "," not in section
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _protocol_code(protocols: tuple[bytes, ...]) -> str:
|
|
240
|
+
if not protocols or not protocols[0]:
|
|
241
|
+
return "00"
|
|
242
|
+
first, last = protocols[0][0], protocols[0][-1]
|
|
243
|
+
if bytes((first,)).isalnum() and bytes((last,)).isalnum():
|
|
244
|
+
return chr(first) + chr(last)
|
|
245
|
+
return f"{first:02x}"[0] + f"{last:02x}"[1]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class PseudoHeader(StrEnum):
|
|
249
|
+
METHOD = "m"
|
|
250
|
+
AUTHORITY = "a"
|
|
251
|
+
SCHEME = "s"
|
|
252
|
+
PATH = "p"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@dataclass(frozen=True, slots=True)
|
|
256
|
+
class Http2Setting:
|
|
257
|
+
identifier: Http2SettingIdentifier
|
|
258
|
+
value: int
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def text(self) -> str:
|
|
262
|
+
return f"{int(self.identifier)}:{self.value}"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@dataclass(frozen=True, slots=True)
|
|
266
|
+
class Http2Priority:
|
|
267
|
+
stream_id: int
|
|
268
|
+
exclusive: bool
|
|
269
|
+
dependency: int
|
|
270
|
+
weight: int
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def text(self) -> str:
|
|
274
|
+
return f"{self.stream_id}:{int(self.exclusive)}:{self.dependency}:{self.weight}"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@dataclass(frozen=True, slots=True)
|
|
278
|
+
class AkamaiFingerprint:
|
|
279
|
+
settings: tuple[Http2Setting, ...]
|
|
280
|
+
window_update: int
|
|
281
|
+
priorities: tuple[Http2Priority, ...]
|
|
282
|
+
pseudo_header_order: tuple[PseudoHeader, ...]
|
|
283
|
+
|
|
284
|
+
@classmethod
|
|
285
|
+
def parse(cls, text: str) -> Self:
|
|
286
|
+
parts = text.split("|")
|
|
287
|
+
if len(parts) != 4:
|
|
288
|
+
raise ValueError("Akamai fingerprint text needs four pipe separated parts")
|
|
289
|
+
settings, window_update, priorities, order = parts
|
|
290
|
+
return cls(
|
|
291
|
+
tuple(cls._setting(item) for item in settings.split(";") if item),
|
|
292
|
+
int(window_update),
|
|
293
|
+
()
|
|
294
|
+
if priorities == "0"
|
|
295
|
+
else tuple(cls._priority(item) for item in priorities.split(",")),
|
|
296
|
+
tuple(PseudoHeader(letter) for letter in order.split(",") if letter),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def text(self) -> str:
|
|
301
|
+
priorities = ",".join(priority.text for priority in self.priorities) or "0"
|
|
302
|
+
settings = ";".join(setting.text for setting in self.settings)
|
|
303
|
+
return "|".join(
|
|
304
|
+
(
|
|
305
|
+
settings,
|
|
306
|
+
str(self.window_update),
|
|
307
|
+
priorities,
|
|
308
|
+
",".join(header.value for header in self.pseudo_header_order),
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
def digest(self) -> str:
|
|
314
|
+
return hashlib.md5(self.text.encode(), usedforsecurity=False).hexdigest()
|
|
315
|
+
|
|
316
|
+
@staticmethod
|
|
317
|
+
def _setting(item: str) -> Http2Setting:
|
|
318
|
+
identifier, value = item.split(":")
|
|
319
|
+
return Http2Setting(Http2SettingIdentifier(int(identifier)), int(value))
|
|
320
|
+
|
|
321
|
+
@staticmethod
|
|
322
|
+
def _priority(item: str) -> Http2Priority:
|
|
323
|
+
stream_id, exclusive, dependency, weight = item.split(":")
|
|
324
|
+
return Http2Priority(
|
|
325
|
+
int(stream_id), bool(int(exclusive)), int(dependency), int(weight)
|
|
326
|
+
)
|