JeffJing commited on
Commit
a9ee289
·
1 Parent(s): 3360b74

Upload 25 files

Browse files
.gitattributes CHANGED
@@ -32,3 +32,10 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
32
  *.zip filter=lfs diff=lfs merge=lfs -text
33
  *.zst filter=lfs diff=lfs merge=lfs -text
34
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
32
  *.zip filter=lfs diff=lfs merge=lfs -text
33
  *.zst filter=lfs diff=lfs merge=lfs -text
34
  *tfevents* filter=lfs diff=lfs merge=lfs -text
35
+ tls_client/dependencies/tls-client-32.dll filter=lfs diff=lfs merge=lfs -text
36
+ tls_client/dependencies/tls-client-64.dll filter=lfs diff=lfs merge=lfs -text
37
+ tls_client/dependencies/tls-client-amd64.so filter=lfs diff=lfs merge=lfs -text
38
+ tls_client/dependencies/tls-client-arm64.dylib filter=lfs diff=lfs merge=lfs -text
39
+ tls_client/dependencies/tls-client-arm64.so filter=lfs diff=lfs merge=lfs -text
40
+ tls_client/dependencies/tls-client-x86.dylib filter=lfs diff=lfs merge=lfs -text
41
+ tls_client/dependencies/tls-client-x86.so filter=lfs diff=lfs merge=lfs -text
tls_client/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # _____ __ __ ___ _ _ _
2
+ # /__ \/ / / _\ / __\ (_) ___ _ __ | |_
3
+ # / /\/ / \ \ _____ / / | | |/ _ \ '_ \| __|
4
+ # / / / /____\ \_____/ /___| | | __/ | | | |_
5
+ # \/ \____/\__/ \____/|_|_|\___|_| |_|\__|
6
+
7
+ # Disclaimer:
8
+ # Big shout out to Bogdanfinn for open sourcing his tls-client in Golang.
9
+ # Also to requests, as most of the cookie handling is copied from it. :'D
10
+ # I wanted to keep the syntax as similar as possible to requests, as most people use it and are familiar with it!
11
+ # Links:
12
+ # tls-client: https://github.com/bogdanfinn/tls-client
13
+ # requests: https://github.com/psf/requests
14
+
15
+ from .sessions import Session
tls_client/__pycache__/__init__.cpython-39.pyc ADDED
Binary file (209 Bytes). View file
 
tls_client/__pycache__/__version__.cpython-39.pyc ADDED
Binary file (332 Bytes). View file
 
tls_client/__pycache__/cffi.cpython-39.pyc ADDED
Binary file (744 Bytes). View file
 
tls_client/__pycache__/cookies.cpython-39.pyc ADDED
Binary file (15.9 kB). View file
 
tls_client/__pycache__/exceptions.cpython-39.pyc ADDED
Binary file (389 Bytes). View file
 
tls_client/__pycache__/response.cpython-39.pyc ADDED
Binary file (1.77 kB). View file
 
tls_client/__pycache__/sessions.cpython-39.pyc ADDED
Binary file (5.75 kB). View file
 
tls_client/__pycache__/structures.cpython-39.pyc ADDED
Binary file (3.6 kB). View file
 
tls_client/__version__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # _____ __ __ ___ _ _ _
2
+ # /__ \/ / / _\ / __\ (_) ___ _ __ | |_
3
+ # / /\/ / \ \ _____ / / | | |/ _ \ '_ \| __|
4
+ # / / / /____\ \_____/ /___| | | __/ | | | |_
5
+ # \/ \____/\__/ \____/|_|_|\___|_| |_|\__|
6
+
7
+ __title__ = "tls_client"
8
+ __description__ = "Advanced Python HTTP Client."
9
+ __version__ = "0.1.8"
10
+ __author__ = "Florian Zager"
11
+ __license__ = "MIT"
tls_client/cffi.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sys import platform
2
+ from platform import machine
3
+ import ctypes
4
+ import os
5
+
6
+
7
+ if platform == 'darwin':
8
+ file_ext = '-arm64.dylib' if machine() == "arm64" else '-x86.dylib'
9
+ elif platform in ('win32', 'cygwin'):
10
+ file_ext = '-64.dll' if 8 == ctypes.sizeof(ctypes.c_voidp) else '-32.dll'
11
+ else:
12
+ if machine() == "aarch64":
13
+ file_ext = '-arm64.so'
14
+ elif "x86" in machine():
15
+ file_ext = '-x86.so'
16
+ else:
17
+ file_ext = '-amd64.so'
18
+
19
+ root_dir = os.path.abspath(os.path.dirname(__file__))
20
+ library = ctypes.cdll.LoadLibrary(f'{root_dir}/dependencies/tls-client{file_ext}')
21
+
22
+ # extract the exposed request function from the shared package
23
+ request = library.request
24
+ request.argtypes = [ctypes.c_char_p]
25
+ request.restype = ctypes.c_char_p
tls_client/cookies.py ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .structures import CaseInsensitiveDict
2
+
3
+ from http.cookiejar import CookieJar, Cookie
4
+ from typing import MutableMapping, Union, Any
5
+ from urllib.parse import urlparse, urlunparse
6
+ from http.client import HTTPMessage
7
+ import copy
8
+
9
+ try:
10
+ import threading
11
+ except ImportError:
12
+ import dummy_threading as threading
13
+
14
+
15
+ class MockRequest:
16
+ """
17
+ Mimic a urllib2.Request to get the correct cookie string for the request.
18
+ """
19
+
20
+ def __init__(self, request_url: str, request_headers: CaseInsensitiveDict):
21
+ self.request_url = request_url
22
+ self.request_headers = request_headers
23
+ self._new_headers = {}
24
+ self.type = urlparse(self.request_url).scheme
25
+
26
+ def get_type(self):
27
+ return self.type
28
+
29
+ def get_host(self):
30
+ return urlparse(self.request_url).netloc
31
+
32
+ def get_origin_req_host(self):
33
+ return self.get_host()
34
+
35
+ def get_full_url(self):
36
+ # Only return the response's URL if the user hadn't set the Host
37
+ # header
38
+ if not self.request_headers.get("Host"):
39
+ return self.request_url
40
+ # If they did set it, retrieve it and reconstruct the expected domain
41
+ host = self.request_headers["Host"]
42
+ parsed = urlparse(self.request_url)
43
+ # Reconstruct the URL as we expect it
44
+ return urlunparse(
45
+ [
46
+ parsed.scheme,
47
+ host,
48
+ parsed.path,
49
+ parsed.params,
50
+ parsed.query,
51
+ parsed.fragment,
52
+ ]
53
+ )
54
+
55
+ def is_unverifiable(self):
56
+ return True
57
+
58
+ def has_header(self, name):
59
+ return name in self.request_headers or name in self._new_headers
60
+
61
+ def get_header(self, name, default=None):
62
+ return self.request_headers.get(name, self._new_headers.get(name, default))
63
+
64
+ def add_unredirected_header(self, name, value):
65
+ self._new_headers[name] = value
66
+
67
+ def get_new_headers(self):
68
+ return self._new_headers
69
+
70
+ @property
71
+ def unverifiable(self):
72
+ return self.is_unverifiable()
73
+
74
+ @property
75
+ def origin_req_host(self):
76
+ return self.get_origin_req_host()
77
+
78
+ @property
79
+ def host(self):
80
+ return self.get_host()
81
+
82
+
83
+ class MockResponse:
84
+ """
85
+ Wraps a httplib.HTTPMessage to mimic a urllib.addinfourl.
86
+ The objective is to retrieve the response cookies correctly.
87
+ """
88
+
89
+ def __init__(self, headers):
90
+ self._headers = headers
91
+
92
+ def info(self):
93
+ return self._headers
94
+
95
+ def getheaders(self, name):
96
+ self._headers.getheaders(name)
97
+
98
+
99
+ class CookieConflictError(RuntimeError):
100
+ """There are two cookies that meet the criteria specified in the cookie jar.
101
+ Use .get and .set and include domain and path args in order to be more specific.
102
+ """
103
+
104
+
105
+ class RequestsCookieJar(CookieJar, MutableMapping):
106
+ """ Origin: requests library (https://github.com/psf/requests)
107
+ Compatibility class; is a cookielib.CookieJar, but exposes a dict
108
+ interface.
109
+
110
+ This is the CookieJar we create by default for requests and sessions that
111
+ don't specify one, since some clients may expect response.cookies and
112
+ session.cookies to support dict operations.
113
+
114
+ Requests does not use the dict interface internally; it's just for
115
+ compatibility with external client code. All requests code should work
116
+ out of the box with externally provided instances of ``CookieJar``, e.g.
117
+ ``LWPCookieJar`` and ``FileCookieJar``.
118
+
119
+ Unlike a regular CookieJar, this class is pickleable.
120
+
121
+ .. warning:: dictionary operations that are normally O(1) may be O(n).
122
+ """
123
+
124
+ def get(self, name, default=None, domain=None, path=None):
125
+ """Dict-like get() that also supports optional domain and path args in
126
+ order to resolve naming collisions from using one cookie jar over
127
+ multiple domains.
128
+
129
+ .. warning:: operation is O(n), not O(1).
130
+ """
131
+ try:
132
+ return self._find_no_duplicates(name, domain, path)
133
+ except KeyError:
134
+ return default
135
+
136
+ def set(self, name, value, **kwargs):
137
+ """Dict-like set() that also supports optional domain and path args in
138
+ order to resolve naming collisions from using one cookie jar over
139
+ multiple domains.
140
+ """
141
+ # support client code that unsets cookies by assignment of a None value:
142
+ if value is None:
143
+ remove_cookie_by_name(
144
+ self, name, domain=kwargs.get("domain"), path=kwargs.get("path")
145
+ )
146
+ return
147
+
148
+ c = create_cookie(name, value, **kwargs)
149
+ self.set_cookie(c)
150
+ return c
151
+
152
+ def iterkeys(self):
153
+ """Dict-like iterkeys() that returns an iterator of names of cookies
154
+ from the jar.
155
+
156
+ .. seealso:: itervalues() and iteritems().
157
+ """
158
+ for cookie in iter(self):
159
+ yield cookie.name
160
+
161
+ def keys(self):
162
+ """Dict-like keys() that returns a list of names of cookies from the
163
+ jar.
164
+
165
+ .. seealso:: values() and items().
166
+ """
167
+ return list(self.iterkeys())
168
+
169
+ def itervalues(self):
170
+ """Dict-like itervalues() that returns an iterator of values of cookies
171
+ from the jar.
172
+
173
+ .. seealso:: iterkeys() and iteritems().
174
+ """
175
+ for cookie in iter(self):
176
+ yield cookie.value
177
+
178
+ def values(self):
179
+ """Dict-like values() that returns a list of values of cookies from the
180
+ jar.
181
+
182
+ .. seealso:: keys() and items().
183
+ """
184
+ return list(self.itervalues())
185
+
186
+ def iteritems(self):
187
+ """Dict-like iteritems() that returns an iterator of name-value tuples
188
+ from the jar.
189
+
190
+ .. seealso:: iterkeys() and itervalues().
191
+ """
192
+ for cookie in iter(self):
193
+ yield cookie.name, cookie.value
194
+
195
+ def items(self):
196
+ """Dict-like items() that returns a list of name-value tuples from the
197
+ jar. Allows client-code to call ``dict(RequestsCookieJar)`` and get a
198
+ vanilla python dict of key value pairs.
199
+
200
+ .. seealso:: keys() and values().
201
+ """
202
+ return list(self.iteritems())
203
+
204
+ def list_domains(self):
205
+ """Utility method to list all the domains in the jar."""
206
+ domains = []
207
+ for cookie in iter(self):
208
+ if cookie.domain not in domains:
209
+ domains.append(cookie.domain)
210
+ return domains
211
+
212
+ def list_paths(self):
213
+ """Utility method to list all the paths in the jar."""
214
+ paths = []
215
+ for cookie in iter(self):
216
+ if cookie.path not in paths:
217
+ paths.append(cookie.path)
218
+ return paths
219
+
220
+ def multiple_domains(self):
221
+ """Returns True if there are multiple domains in the jar.
222
+ Returns False otherwise.
223
+
224
+ :rtype: bool
225
+ """
226
+ domains = []
227
+ for cookie in iter(self):
228
+ if cookie.domain is not None and cookie.domain in domains:
229
+ return True
230
+ domains.append(cookie.domain)
231
+ return False # there is only one domain in jar
232
+
233
+ def get_dict(self, domain=None, path=None):
234
+ """Takes as an argument an optional domain and path and returns a plain
235
+ old Python dict of name-value pairs of cookies that meet the
236
+ requirements.
237
+
238
+ :rtype: dict
239
+ """
240
+ dictionary = {}
241
+ for cookie in iter(self):
242
+ if (domain is None or cookie.domain == domain) and (
243
+ path is None or cookie.path == path
244
+ ):
245
+ dictionary[cookie.name] = cookie.value
246
+ return dictionary
247
+
248
+ def __contains__(self, name):
249
+ try:
250
+ return super().__contains__(name)
251
+ except CookieConflictError:
252
+ return True
253
+
254
+ def __getitem__(self, name):
255
+ """Dict-like __getitem__() for compatibility with client code. Throws
256
+ exception if there are more than one cookie with name. In that case,
257
+ use the more explicit get() method instead.
258
+
259
+ .. warning:: operation is O(n), not O(1).
260
+ """
261
+ return self._find_no_duplicates(name)
262
+
263
+ def __setitem__(self, name, value):
264
+ """Dict-like __setitem__ for compatibility with client code. Throws
265
+ exception if there is already a cookie of that name in the jar. In that
266
+ case, use the more explicit set() method instead.
267
+ """
268
+ self.set(name, value)
269
+
270
+ def __delitem__(self, name):
271
+ """Deletes a cookie given a name. Wraps ``cookielib.CookieJar``'s
272
+ ``remove_cookie_by_name()``.
273
+ """
274
+ remove_cookie_by_name(self, name)
275
+
276
+ def set_cookie(self, cookie, *args, **kwargs):
277
+ if (
278
+ hasattr(cookie.value, "startswith")
279
+ and cookie.value.startswith('"')
280
+ and cookie.value.endswith('"')
281
+ ):
282
+ cookie.value = cookie.value.replace('\\"', "")
283
+ return super().set_cookie(cookie, *args, **kwargs)
284
+
285
+ def update(self, other):
286
+ """Updates this jar with cookies from another CookieJar or dict-like"""
287
+ if isinstance(other, CookieJar):
288
+ for cookie in other:
289
+ self.set_cookie(copy.copy(cookie))
290
+ else:
291
+ super().update(other)
292
+
293
+ def _find(self, name, domain=None, path=None):
294
+ """Requests uses this method internally to get cookie values.
295
+
296
+ If there are conflicting cookies, _find arbitrarily chooses one.
297
+ See _find_no_duplicates if you want an exception thrown if there are
298
+ conflicting cookies.
299
+
300
+ :param name: a string containing name of cookie
301
+ :param domain: (optional) string containing domain of cookie
302
+ :param path: (optional) string containing path of cookie
303
+ :return: cookie.value
304
+ """
305
+ for cookie in iter(self):
306
+ if cookie.name == name:
307
+ if domain is None or cookie.domain == domain:
308
+ if path is None or cookie.path == path:
309
+ return cookie.value
310
+
311
+ raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}")
312
+
313
+ def _find_no_duplicates(self, name, domain=None, path=None):
314
+ """Both ``__get_item__`` and ``get`` call this function: it's never
315
+ used elsewhere in Requests.
316
+
317
+ :param name: a string containing name of cookie
318
+ :param domain: (optional) string containing domain of cookie
319
+ :param path: (optional) string containing path of cookie
320
+ :raises KeyError: if cookie is not found
321
+ :raises CookieConflictError: if there are multiple cookies
322
+ that match name and optionally domain and path
323
+ :return: cookie.value
324
+ """
325
+ toReturn = None
326
+ for cookie in iter(self):
327
+ if cookie.name == name:
328
+ if domain is None or cookie.domain == domain:
329
+ if path is None or cookie.path == path:
330
+ if toReturn is not None:
331
+ # if there are multiple cookies that meet passed in criteria
332
+ raise CookieConflictError(
333
+ f"There are multiple cookies with name, {name!r}"
334
+ )
335
+ # we will eventually return this as long as no cookie conflict
336
+ toReturn = cookie.value
337
+
338
+ if toReturn:
339
+ return toReturn
340
+ raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}")
341
+
342
+ def __getstate__(self):
343
+ """Unlike a normal CookieJar, this class is pickleable."""
344
+ state = self.__dict__.copy()
345
+ # remove the unpickleable RLock object
346
+ state.pop("_cookies_lock")
347
+ return state
348
+
349
+ def __setstate__(self, state):
350
+ """Unlike a normal CookieJar, this class is pickleable."""
351
+ self.__dict__.update(state)
352
+ if "_cookies_lock" not in self.__dict__:
353
+ self._cookies_lock = threading.RLock()
354
+
355
+ def copy(self):
356
+ """Return a copy of this RequestsCookieJar."""
357
+ new_cj = RequestsCookieJar()
358
+ new_cj.set_policy(self.get_policy())
359
+ new_cj.update(self)
360
+ return new_cj
361
+
362
+ def get_policy(self):
363
+ """Return the CookiePolicy instance used."""
364
+ return self._policy
365
+
366
+
367
+ def remove_cookie_by_name(cookiejar: RequestsCookieJar, name: str, domain: str = None, path: str = None):
368
+ """Removes a cookie by name, by default over all domains and paths."""
369
+ clearables = []
370
+ for cookie in cookiejar:
371
+ if cookie.name != name:
372
+ continue
373
+ if domain is not None and domain != cookie.domain:
374
+ continue
375
+ if path is not None and path != cookie.path:
376
+ continue
377
+ clearables.append((cookie.domain, cookie.path, cookie.name))
378
+
379
+ for domain, path, name in clearables:
380
+ cookiejar.clear(domain, path, name)
381
+
382
+
383
+ def create_cookie(name: str, value: str, **kwargs: Any) -> Cookie:
384
+ """Make a cookie from underspecified parameters."""
385
+ result = {
386
+ "version": 0,
387
+ "name": name,
388
+ "value": value,
389
+ "port": None,
390
+ "domain": "",
391
+ "path": "/",
392
+ "secure": False,
393
+ "expires": None,
394
+ "discard": True,
395
+ "comment": None,
396
+ "comment_url": None,
397
+ "rest": {"HttpOnly": None},
398
+ "rfc2109": False,
399
+ }
400
+
401
+ badargs = set(kwargs) - set(result)
402
+ if badargs:
403
+ raise TypeError(
404
+ f"create_cookie() got unexpected keyword arguments: {list(badargs)}"
405
+ )
406
+
407
+ result.update(kwargs)
408
+ result["port_specified"] = bool(result["port"])
409
+ result["domain_specified"] = bool(result["domain"])
410
+ result["domain_initial_dot"] = result["domain"].startswith(".")
411
+ result["path_specified"] = bool(result["path"])
412
+
413
+ return Cookie(**result)
414
+
415
+
416
+ def cookiejar_from_dict(cookie_dict: dict) -> RequestsCookieJar:
417
+ """transform a dict to CookieJar"""
418
+ cookie_jar = RequestsCookieJar()
419
+ if cookie_dict is not None:
420
+ for name, value in cookie_dict.items():
421
+ cookie_jar.set_cookie(create_cookie(name=name, value=value))
422
+ return cookie_jar
423
+
424
+
425
+ def merge_cookies(cookiejar: RequestsCookieJar, cookies: Union[dict, RequestsCookieJar]) -> RequestsCookieJar:
426
+ """Merge cookies in session and cookies provided in request"""
427
+ if type(cookies) is dict:
428
+ cookies = cookiejar_from_dict(cookies)
429
+
430
+ for cookie in cookies:
431
+ cookiejar.set_cookie(cookie)
432
+
433
+ return cookiejar
434
+
435
+
436
+ def get_cookie_header(request_url: str, request_headers: CaseInsensitiveDict, cookie_jar: RequestsCookieJar) -> str:
437
+ r = MockRequest(request_url, request_headers)
438
+ cookie_jar.add_cookie_header(r)
439
+ return r.get_new_headers().get("Cookie")
440
+
441
+
442
+ def extract_cookies_to_jar(
443
+ request_url: str,
444
+ request_headers: CaseInsensitiveDict,
445
+ cookie_jar: RequestsCookieJar,
446
+ response_headers: dict
447
+ ) -> RequestsCookieJar:
448
+ response_cookie_jar = cookiejar_from_dict({})
449
+
450
+ req = MockRequest(request_url, request_headers)
451
+ # mimic HTTPMessage
452
+ http_message = HTTPMessage()
453
+ http_message._headers = []
454
+ for header_name, header_values in response_headers.items():
455
+ for header_value in header_values:
456
+ http_message._headers.append(
457
+ (header_name, header_value)
458
+ )
459
+ res = MockResponse(http_message)
460
+ response_cookie_jar.extract_cookies(res, req)
461
+
462
+ merge_cookies(cookie_jar, response_cookie_jar)
463
+ return response_cookie_jar
tls_client/dependencies/__init__.py ADDED
File without changes
tls_client/dependencies/__pycache__/__init__.cpython-39.pyc ADDED
Binary file (182 Bytes). View file
 
tls_client/dependencies/tls-client-32.dll ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2f260ec3d5f38369097370c2c21b2336f33c6a4fa818f01332c6994f8f089452
3
+ size 13640554
tls_client/dependencies/tls-client-64.dll ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:eec65f115ef72e29b435c0092b9bb1e1d34174254b845b07bbb2259826553c29
3
+ size 15315371
tls_client/dependencies/tls-client-amd64.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c0b43fd2e4b138472033a0ea39cc7f0a3fd53d676a8d7f71708ab04542bed3ec
3
+ size 9981944
tls_client/dependencies/tls-client-arm64.dylib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:689960ef10d1cc73a5de4da7afc8ca3dba9dd351a197cd0a371a6fbace56a0dd
3
+ size 9283378
tls_client/dependencies/tls-client-arm64.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1f148e72bfbb23f12dbbb204f66aba32a7d52f83df75bca3b3d97c40a7f46698
3
+ size 9519616
tls_client/dependencies/tls-client-x86.dylib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0ae2652c9bb208f405bdc275ef3aba7b929fe0573f99389b643cb9860a7f6986
3
+ size 9733512
tls_client/dependencies/tls-client-x86.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1eb4c8a7f69b698efa4858b249fcf5d8ae8fbc7f48edbce25ac1b01ab5052a59
3
+ size 9987288
tls_client/exceptions.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+
2
+ class TLSClientExeption(IOError):
3
+ """General error with the TLS client"""
tls_client/response.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .cookies import cookiejar_from_dict, RequestsCookieJar
2
+ from .structures import CaseInsensitiveDict
3
+
4
+ from http.cookiejar import CookieJar
5
+ from typing import Union
6
+ import json
7
+
8
+
9
+ class Response:
10
+ """object, which contains the response to an HTTP request."""
11
+
12
+ def __init__(self):
13
+
14
+ # Reference of URL the response is coming from (especially useful with redirects)
15
+ self.url = None
16
+
17
+ # Integer Code of responded HTTP Status, e.g. 404 or 200.
18
+ self.status_code = None
19
+
20
+ # String of responded HTTP Body.
21
+ self.text = None
22
+
23
+ # Case-insensitive Dictionary of Response Headers.
24
+ self.headers = CaseInsensitiveDict()
25
+
26
+ # A CookieJar of Cookies the server sent back.
27
+ self.cookies = cookiejar_from_dict({})
28
+
29
+ def __enter__(self):
30
+ return self
31
+
32
+ def __repr__(self):
33
+ return f"<Response [{self.status_code}]>"
34
+
35
+ def json(self, **kwargs):
36
+ """parse response body to json (dict/list)"""
37
+ return json.loads(self.text, **kwargs)
38
+
39
+
40
+ def build_response(res: Union[dict, list], res_cookies: RequestsCookieJar) -> Response:
41
+ """Builds a Response object """
42
+ response = Response()
43
+ # Add target / url
44
+ response.url = res["target"]
45
+ # Add status code
46
+ response.status_code = res["status"]
47
+ # Add headers
48
+ response_headers = {}
49
+ if res["headers"] is not None:
50
+ for header_key, header_value in res["headers"].items():
51
+ if len(header_value) == 1:
52
+ response_headers[header_key] = header_value[0]
53
+ else:
54
+ response_headers[header_key] = header_value
55
+ response.headers = response_headers
56
+ # Add cookies
57
+ response.cookies = res_cookies
58
+ # Add response body
59
+ response.text = res["body"]
60
+ return response
tls_client/sessions.py ADDED
@@ -0,0 +1,417 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .cffi import request
2
+ from .cookies import cookiejar_from_dict, get_cookie_header, merge_cookies, extract_cookies_to_jar
3
+ from .exceptions import TLSClientExeption
4
+ from .response import build_response
5
+ from .structures import CaseInsensitiveDict
6
+ from .__version__ import __version__
7
+
8
+ from typing import Any, Optional, Union
9
+ from json import dumps, loads
10
+ import urllib.parse
11
+ import base64
12
+ import ctypes
13
+ import uuid
14
+
15
+
16
+ class Session:
17
+
18
+ def __init__(
19
+ self,
20
+ client_identifier: Optional[str] = None,
21
+ ja3_string: Optional[str] = None,
22
+ h2_settings: Optional[dict] = None, # Optional[dict[str, int]]
23
+ h2_settings_order: Optional[list] = None, # Optional[list[str]]
24
+ supported_signature_algorithms: Optional[list] = None, # Optional[list[str]]
25
+ supported_versions: Optional[list] = None, # Optional[list[str]]
26
+ key_share_curves: Optional[list] = None, # Optional[list[str]]
27
+ cert_compression_algo: str = None,
28
+ pseudo_header_order: Optional[list] = None, # Optional[list[str]
29
+ connection_flow: Optional[int] = None,
30
+ priority_frames: Optional[list] = None,
31
+ header_order: Optional[list] = None, # Optional[list[str]]
32
+ header_priority: Optional[dict] = None, # Optional[list[str]]
33
+ random_tls_extension_order: Optional = False,
34
+ force_http1: Optional = False,
35
+ ) -> None:
36
+ self._session_id = str(uuid.uuid4())
37
+ # --- Standard Settings ----------------------------------------------------------------------------------------
38
+
39
+ # Case-insensitive dictionary of headers, send on each request
40
+ self.headers = CaseInsensitiveDict(
41
+ {
42
+ "User-Agent": f"tls-client/{__version__}",
43
+ "Accept-Encoding": "gzip, deflate, br",
44
+ "Accept": "*/*",
45
+ "Connection": "keep-alive",
46
+ }
47
+ )
48
+
49
+ # Example:
50
+ # {
51
+ # "http": "http://user:pass@ip:port",
52
+ # "https": "http://user:pass@ip:port"
53
+ # }
54
+ self.proxies = {}
55
+
56
+ # Dictionary of querystring data to attach to each request. The dictionary values may be lists for representing
57
+ # multivalued query parameters.
58
+ self.params = {}
59
+
60
+ # CookieJar containing all currently outstanding cookies set on this session
61
+ self.cookies = cookiejar_from_dict({})
62
+
63
+ # --- Advanced Settings ----------------------------------------------------------------------------------------
64
+
65
+ # Examples:
66
+ # Chrome --> chrome_103, chrome_104, chrome_105, chrome_106
67
+ # Firefox --> firefox_102, firefox_104
68
+ # Opera --> opera_89, opera_90
69
+ # Safari --> safari_15_3, safari_15_6_1, safari_16_0
70
+ # iOS --> safari_ios_15_5, safari_ios_15_6, safari_ios_16_0
71
+ # iPadOS --> safari_ios_15_6
72
+ self.client_identifier = client_identifier
73
+
74
+ # Set JA3 --> TLSVersion, Ciphers, Extensions, EllipticCurves, EllipticCurvePointFormats
75
+ # Example:
76
+ # 771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0
77
+ self.ja3_string = ja3_string
78
+
79
+ # HTTP2 Header Frame Settings
80
+ # Possible Settings:
81
+ # HEADER_TABLE_SIZE
82
+ # SETTINGS_ENABLE_PUSH
83
+ # MAX_CONCURRENT_STREAMS
84
+ # INITIAL_WINDOW_SIZE
85
+ # MAX_FRAME_SIZE
86
+ # MAX_HEADER_LIST_SIZE
87
+ #
88
+ # Example:
89
+ # {
90
+ # "HEADER_TABLE_SIZE": 65536,
91
+ # "MAX_CONCURRENT_STREAMS": 1000,
92
+ # "INITIAL_WINDOW_SIZE": 6291456,
93
+ # "MAX_HEADER_LIST_SIZE": 262144
94
+ # }
95
+ self.h2_settings = h2_settings
96
+
97
+ # HTTP2 Header Frame Settings Order
98
+ # Example:
99
+ # [
100
+ # "HEADER_TABLE_SIZE",
101
+ # "MAX_CONCURRENT_STREAMS",
102
+ # "INITIAL_WINDOW_SIZE",
103
+ # "MAX_HEADER_LIST_SIZE"
104
+ # ]
105
+ self.h2_settings_order = h2_settings_order
106
+
107
+ # Supported Signature Algorithms
108
+ # Possible Settings:
109
+ # PKCS1WithSHA256
110
+ # PKCS1WithSHA384
111
+ # PKCS1WithSHA512
112
+ # PSSWithSHA256
113
+ # PSSWithSHA384
114
+ # PSSWithSHA512
115
+ # ECDSAWithP256AndSHA256
116
+ # ECDSAWithP384AndSHA384
117
+ # ECDSAWithP521AndSHA512
118
+ # PKCS1WithSHA1
119
+ # ECDSAWithSHA1
120
+ #
121
+ # Example:
122
+ # [
123
+ # "ECDSAWithP256AndSHA256",
124
+ # "PSSWithSHA256",
125
+ # "PKCS1WithSHA256",
126
+ # "ECDSAWithP384AndSHA384",
127
+ # "PSSWithSHA384",
128
+ # "PKCS1WithSHA384",
129
+ # "PSSWithSHA512",
130
+ # "PKCS1WithSHA512",
131
+ # ]
132
+ self.supported_signature_algorithms = supported_signature_algorithms
133
+
134
+ # Supported Versions
135
+ # Possible Settings:
136
+ # GREASE
137
+ # 1.3
138
+ # 1.2
139
+ # 1.1
140
+ # 1.0
141
+ #
142
+ # Example:
143
+ # [
144
+ # "GREASE",
145
+ # "1.3",
146
+ # "1.2"
147
+ # ]
148
+ self.supported_versions = supported_versions
149
+
150
+ # Key Share Curves
151
+ # Possible Settings:
152
+ # GREASE
153
+ # P256
154
+ # P384
155
+ # P521
156
+ # X25519
157
+ #
158
+ # Example:
159
+ # [
160
+ # "GREASE",
161
+ # "X25519"
162
+ # ]
163
+ self.key_share_curves = key_share_curves
164
+
165
+ # Cert Compression Algorithm
166
+ # Examples: "zlib", "brotli", "zstd"
167
+ self.cert_compression_algo = cert_compression_algo
168
+
169
+ # Pseudo Header Order (:authority, :method, :path, :scheme)
170
+ # Example:
171
+ # [
172
+ # ":method",
173
+ # ":authority",
174
+ # ":scheme",
175
+ # ":path"
176
+ # ]
177
+ self.pseudo_header_order = pseudo_header_order
178
+
179
+ # Connection Flow / Window Size Increment
180
+ # Example:
181
+ # 15663105
182
+ self.connection_flow = connection_flow
183
+
184
+ # Example:
185
+ # [
186
+ # {
187
+ # "streamID": 3,
188
+ # "priorityParam": {
189
+ # "weight": 201,
190
+ # "streamDep": 0,
191
+ # "exclusive": false
192
+ # }
193
+ # },
194
+ # {
195
+ # "streamID": 5,
196
+ # "priorityParam": {
197
+ # "weight": 101,
198
+ # "streamDep": false,
199
+ # "exclusive": 0
200
+ # }
201
+ # }
202
+ # ]
203
+ self.priority_frames = priority_frames
204
+
205
+ # Order of your headers
206
+ # Example:
207
+ # [
208
+ # "key1",
209
+ # "key2"
210
+ # ]
211
+ self.header_order = header_order
212
+
213
+ # Header Priority
214
+ # Example:
215
+ # {
216
+ # "streamDep": 1,
217
+ # "exclusive": true,
218
+ # "weight": 1
219
+ # }
220
+ self.header_priority = header_priority
221
+
222
+ # randomize tls extension order
223
+ self.random_tls_extension_order = random_tls_extension_order
224
+
225
+ # force HTTP1
226
+ self.force_http1 = force_http1
227
+
228
+ def execute_request(
229
+ self,
230
+ method: str,
231
+ url: str,
232
+ params: Optional[dict] = None, # Optional[dict[str, str]]
233
+ data: Optional[Union[str, dict]] = None,
234
+ headers: Optional[dict] = None, # Optional[dict[str, str]]
235
+ cookies: Optional[dict] = None, # Optional[dict[str, str]]
236
+ json: Optional[dict] = None, # Optional[dict]
237
+ allow_redirects: Optional[bool] = False,
238
+ insecure_skip_verify: Optional[bool] = False,
239
+ timeout_seconds: Optional[int] = 30,
240
+ proxy: Optional[dict] = None # Optional[dict[str, str]]
241
+ ):
242
+ # --- URL ------------------------------------------------------------------------------------------------------
243
+ # Prepare URL - add params to url
244
+ if params is not None:
245
+ url = f"{url}?{urllib.parse.urlencode(params, doseq=True)}"
246
+
247
+ # --- Request Body ---------------------------------------------------------------------------------------------
248
+ # Prepare request body - build request body
249
+ # Data has priority. JSON is only used if data is None.
250
+ if data is None and json is not None:
251
+ if type(json) in [dict, list]:
252
+ json = dumps(json)
253
+ request_body = json
254
+ content_type = "application/json"
255
+ elif data is not None and type(data) not in [str, bytes]:
256
+ request_body = urllib.parse.urlencode(data, doseq=True)
257
+ content_type = "application/x-www-form-urlencoded"
258
+ else:
259
+ request_body = data
260
+ content_type = None
261
+ # set content type if it isn't set
262
+ if content_type is not None and "content-type" not in self.headers:
263
+ self.headers["Content-Type"] = content_type
264
+
265
+ # --- Headers --------------------------------------------------------------------------------------------------
266
+ # merge headers of session and of the request
267
+ if headers is not None:
268
+ for header_key, header_value in headers.items():
269
+ # check if all header keys and values are strings
270
+ if type(header_key) is str and type(header_value) is str:
271
+ self.headers[header_key] = header_value
272
+ headers = self.headers
273
+ else:
274
+ headers = self.headers
275
+
276
+ # --- Cookies --------------------------------------------------------------------------------------------------
277
+ cookies = cookies or {}
278
+ # Merge with session cookies
279
+ cookies = merge_cookies(self.cookies, cookies)
280
+
281
+ cookie_header = get_cookie_header(
282
+ request_url=url,
283
+ request_headers=headers,
284
+ cookie_jar=cookies
285
+ )
286
+ if cookie_header is not None:
287
+ headers["Cookie"] = cookie_header
288
+
289
+ # --- Proxy ----------------------------------------------------------------------------------------------------
290
+ proxy = proxy or self.proxies
291
+
292
+ if type(proxy) is dict and "http" in proxy:
293
+ proxy = proxy["http"]
294
+ elif type(proxy) is str:
295
+ proxy = proxy
296
+ else:
297
+ proxy = ""
298
+
299
+ # --- Request --------------------------------------------------------------------------------------------------
300
+ is_byte_request = isinstance(request_body, (bytes, bytearray))
301
+ request_payload = {
302
+ "sessionId": self._session_id,
303
+ "followRedirects": allow_redirects,
304
+ "forceHttp1": self.force_http1,
305
+ "headers": dict(headers),
306
+ "headerOrder": self.header_order,
307
+ "insecureSkipVerify": insecure_skip_verify,
308
+ "isByteRequest": is_byte_request,
309
+ "proxyUrl": proxy,
310
+ "requestUrl": url,
311
+ "requestMethod": method,
312
+ "requestBody": base64.b64encode(request_body).decode() if is_byte_request else request_body,
313
+ "requestCookies": [], # Empty because it's handled in python
314
+ "timeoutSeconds": timeout_seconds,
315
+ }
316
+ if self.client_identifier is None:
317
+ request_payload["customTlsClient"] = {
318
+ "ja3String": self.ja3_string,
319
+ "h2Settings": self.h2_settings,
320
+ "h2SettingsOrder": self.h2_settings_order,
321
+ "pseudoHeaderOrder": self.pseudo_header_order,
322
+ "connectionFlow": self.connection_flow,
323
+ "priorityFrames": self.priority_frames,
324
+ "headerPriority": self.header_priority,
325
+ "certCompressionAlgo": self.cert_compression_algo,
326
+ "supportedVersions": self.supported_versions,
327
+ "supportedSignatureAlgorithms": self.supported_signature_algorithms,
328
+ "keyShareCurves": self.key_share_curves,
329
+ }
330
+ else:
331
+ request_payload["tlsClientIdentifier"] = self.client_identifier
332
+ request_payload["withRandomTLSExtensionOrder"] = self.random_tls_extension_order
333
+
334
+ # this is a pointer to the response
335
+ response = request(dumps(request_payload).encode('utf-8'))
336
+ # dereference the pointer to a byte array
337
+ response_bytes = ctypes.string_at(response)
338
+ # convert our byte array to a string (tls client returns json)
339
+ response_string = response_bytes.decode('utf-8')
340
+ # convert response string to json
341
+ response_object = loads(response_string)
342
+
343
+ # --- Response -------------------------------------------------------------------------------------------------
344
+ # Error handling
345
+ if response_object["status"] == 0:
346
+ raise TLSClientExeption(response_object["body"])
347
+ # Set response cookies
348
+ response_cookie_jar = extract_cookies_to_jar(
349
+ request_url=url,
350
+ request_headers=headers,
351
+ cookie_jar=cookies,
352
+ response_headers=response_object["headers"]
353
+ )
354
+ # build response class
355
+ return build_response(response_object, response_cookie_jar)
356
+
357
+ def get(
358
+ self,
359
+ url: str,
360
+ **kwargs: Any
361
+ ):
362
+ """Sends a GET request"""
363
+ return self.execute_request(method="GET", url=url, **kwargs)
364
+
365
+ def options(
366
+ self,
367
+ url: str,
368
+ **kwargs: Any
369
+ ):
370
+ """Sends a OPTIONS request"""
371
+ return self.execute_request(method="OPTIONS", url=url, **kwargs)
372
+
373
+ def head(
374
+ self,
375
+ url: str,
376
+ **kwargs: Any
377
+ ):
378
+ """Sends a HEAD request"""
379
+ return self.execute_request(method="HEAD", url=url, **kwargs)
380
+
381
+ def post(
382
+ self,
383
+ url: str,
384
+ data: Optional[Union[str, dict]] = None,
385
+ json: Optional[dict] = None,
386
+ **kwargs: Any
387
+ ):
388
+ """Sends a POST request"""
389
+ return self.execute_request(method="POST", url=url, data=data, json=json, **kwargs)
390
+
391
+ def put(
392
+ self,
393
+ url: str,
394
+ data: Optional[Union[str, dict]] = None,
395
+ json: Optional[dict] = None,
396
+ **kwargs: Any
397
+ ):
398
+ """Sends a PUT request"""
399
+ return self.execute_request(method="PUT", url=url, data=data, json=json, **kwargs)
400
+
401
+ def patch(
402
+ self,
403
+ url: str,
404
+ data: Optional[Union[str, dict]] = None,
405
+ json: Optional[dict] = None,
406
+ **kwargs: Any
407
+ ):
408
+ """Sends a PUT request"""
409
+ return self.execute_request(method="PATCH", url=url, data=data, json=json, **kwargs)
410
+
411
+ def delete(
412
+ self,
413
+ url: str,
414
+ **kwargs: Any
415
+ ):
416
+ """Sends a DELETE request"""
417
+ return self.execute_request(method="DELETE", url=url, **kwargs)
tls_client/structures.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import MutableMapping, Mapping
2
+ from collections import OrderedDict
3
+
4
+
5
+ class CaseInsensitiveDict(MutableMapping):
6
+ """Origin: requests library (https://github.com/psf/requests)
7
+
8
+ A case-insensitive ``dict``-like object.
9
+
10
+ Implements all methods and operations of
11
+ ``MutableMapping`` as well as dict's ``copy``. Also
12
+ provides ``lower_items``.
13
+
14
+ All keys are expected to be strings. The structure remembers the
15
+ case of the last key to be set, and ``iter(instance)``,
16
+ ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
17
+ will contain case-sensitive keys. However, querying and contains
18
+ testing is case insensitive::
19
+
20
+ cid = CaseInsensitiveDict()
21
+ cid['Accept'] = 'application/json'
22
+ cid['aCCEPT'] == 'application/json' # True
23
+ list(cid) == ['Accept'] # True
24
+
25
+ For example, ``headers['content-encoding']`` will return the
26
+ value of a ``'Content-Encoding'`` response header, regardless
27
+ of how the header name was originally stored.
28
+
29
+ If the constructor, ``.update``, or equality comparison
30
+ operations are given keys that have equal ``.lower()``s, the
31
+ behavior is undefined.
32
+ """
33
+
34
+ def __init__(self, data=None, **kwargs):
35
+ self._store = OrderedDict()
36
+ if data is None:
37
+ data = {}
38
+ self.update(data, **kwargs)
39
+
40
+ def __setitem__(self, key, value):
41
+ # Use the lowercased key for lookups, but store the actual
42
+ # key alongside the value.
43
+ self._store[key.lower()] = (key, value)
44
+
45
+ def __getitem__(self, key):
46
+ return self._store[key.lower()][1]
47
+
48
+ def __delitem__(self, key):
49
+ del self._store[key.lower()]
50
+
51
+ def __iter__(self):
52
+ return (casedkey for casedkey, mappedvalue in self._store.values())
53
+
54
+ def __len__(self):
55
+ return len(self._store)
56
+
57
+ def lower_items(self):
58
+ """Like iteritems(), but with all lowercase keys."""
59
+ return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items())
60
+
61
+ def __eq__(self, other):
62
+ if isinstance(other, Mapping):
63
+ other = CaseInsensitiveDict(other)
64
+ else:
65
+ return NotImplemented
66
+ # Compare insensitively
67
+ return dict(self.lower_items()) == dict(other.lower_items())
68
+
69
+ # Copy is required
70
+ def copy(self):
71
+ return CaseInsensitiveDict(self._store.values())
72
+
73
+ def __repr__(self):
74
+ return str(dict(self.items()))