Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- aiohttp-3.13.3.dist-info/INSTALLER +1 -0
- aiohttp-3.13.3.dist-info/METADATA +262 -0
- aiohttp-3.13.3.dist-info/RECORD +84 -0
- aiohttp-3.13.3.dist-info/REQUESTED +0 -0
- aiohttp-3.13.3.dist-info/WHEEL +7 -0
- aiohttp-3.13.3.dist-info/top_level.txt +1 -0
- aiohttp/_cookie_helpers.py +338 -0
- aiohttp/_cparser.pxd +158 -0
- aiohttp/abc.py +268 -0
- aiohttp/client_exceptions.py +421 -0
- aiohttp/client_middleware_digest_auth.py +480 -0
- aiohttp/client_reqrep.py +1536 -0
- aiohttp/connector.py +1842 -0
- aiohttp/cookiejar.py +522 -0
- aiohttp/formdata.py +179 -0
- aiohttp/hdrs.py +121 -0
- aiohttp/helpers.py +986 -0
- aiohttp/http_parser.py +1086 -0
- aiohttp/http_writer.py +378 -0
- aiohttp/payload_streamer.py +78 -0
- aiohttp/py.typed +1 -0
- aiohttp/pytest_plugin.py +444 -0
- aiohttp/resolver.py +274 -0
- aiohttp/tcp_helpers.py +37 -0
- aiohttp/typedefs.py +69 -0
- aiohttp/web_fileresponse.py +418 -0
- aiohttp/web_log.py +216 -0
- aiohttp/web_routedef.py +214 -0
- bin/accelerate +10 -0
- bin/accelerate-config +10 -0
- bin/accelerate-estimate-memory +10 -0
- bin/accelerate-launch +10 -0
- bin/accelerate-merge-weights +10 -0
- bin/datasets-cli +10 -0
- bin/diffusers-cli +10 -0
- bin/f2py +10 -0
- bin/get_gprof +75 -0
- bin/get_objgraph +54 -0
- bin/hf +10 -0
- bin/httpx +10 -0
- bin/isympy +10 -0
- bin/markdown-it +10 -0
- bin/normalizer +10 -0
- bin/proton +10 -0
- bin/proton-viewer +10 -0
- bin/pygmentize +10 -0
- bin/tiny-agents +10 -0
- bin/torchfrtrace +10 -0
- bin/torchrun +10 -0
- bin/tqdm +10 -0
aiohttp-3.13.3.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
uv
|
aiohttp-3.13.3.dist-info/METADATA
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.4
|
| 2 |
+
Name: aiohttp
|
| 3 |
+
Version: 3.13.3
|
| 4 |
+
Summary: Async http client/server framework (asyncio)
|
| 5 |
+
Maintainer-email: aiohttp team <team@aiohttp.org>
|
| 6 |
+
License: Apache-2.0 AND MIT
|
| 7 |
+
Project-URL: Homepage, https://github.com/aio-libs/aiohttp
|
| 8 |
+
Project-URL: Chat: Matrix, https://matrix.to/#/#aio-libs:matrix.org
|
| 9 |
+
Project-URL: Chat: Matrix Space, https://matrix.to/#/#aio-libs-space:matrix.org
|
| 10 |
+
Project-URL: CI: GitHub Actions, https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI
|
| 11 |
+
Project-URL: Coverage: codecov, https://codecov.io/github/aio-libs/aiohttp
|
| 12 |
+
Project-URL: Docs: Changelog, https://docs.aiohttp.org/en/stable/changes.html
|
| 13 |
+
Project-URL: Docs: RTD, https://docs.aiohttp.org
|
| 14 |
+
Project-URL: GitHub: issues, https://github.com/aio-libs/aiohttp/issues
|
| 15 |
+
Project-URL: GitHub: repo, https://github.com/aio-libs/aiohttp
|
| 16 |
+
Classifier: Development Status :: 5 - Production/Stable
|
| 17 |
+
Classifier: Framework :: AsyncIO
|
| 18 |
+
Classifier: Intended Audience :: Developers
|
| 19 |
+
Classifier: Operating System :: POSIX
|
| 20 |
+
Classifier: Operating System :: MacOS :: MacOS X
|
| 21 |
+
Classifier: Operating System :: Microsoft :: Windows
|
| 22 |
+
Classifier: Programming Language :: Python
|
| 23 |
+
Classifier: Programming Language :: Python :: 3
|
| 24 |
+
Classifier: Programming Language :: Python :: 3.9
|
| 25 |
+
Classifier: Programming Language :: Python :: 3.10
|
| 26 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 27 |
+
Classifier: Programming Language :: Python :: 3.12
|
| 28 |
+
Classifier: Programming Language :: Python :: 3.13
|
| 29 |
+
Classifier: Programming Language :: Python :: 3.14
|
| 30 |
+
Classifier: Topic :: Internet :: WWW/HTTP
|
| 31 |
+
Requires-Python: >=3.9
|
| 32 |
+
Description-Content-Type: text/x-rst
|
| 33 |
+
License-File: LICENSE.txt
|
| 34 |
+
License-File: vendor/llhttp/LICENSE
|
| 35 |
+
Requires-Dist: aiohappyeyeballs>=2.5.0
|
| 36 |
+
Requires-Dist: aiosignal>=1.4.0
|
| 37 |
+
Requires-Dist: async-timeout<6.0,>=4.0; python_version < "3.11"
|
| 38 |
+
Requires-Dist: attrs>=17.3.0
|
| 39 |
+
Requires-Dist: frozenlist>=1.1.1
|
| 40 |
+
Requires-Dist: multidict<7.0,>=4.5
|
| 41 |
+
Requires-Dist: propcache>=0.2.0
|
| 42 |
+
Requires-Dist: yarl<2.0,>=1.17.0
|
| 43 |
+
Provides-Extra: speedups
|
| 44 |
+
Requires-Dist: aiodns>=3.3.0; extra == "speedups"
|
| 45 |
+
Requires-Dist: Brotli>=1.2; platform_python_implementation == "CPython" and extra == "speedups"
|
| 46 |
+
Requires-Dist: brotlicffi>=1.2; platform_python_implementation != "CPython" and extra == "speedups"
|
| 47 |
+
Requires-Dist: backports.zstd; (platform_python_implementation == "CPython" and python_version < "3.14") and extra == "speedups"
|
| 48 |
+
Dynamic: license-file
|
| 49 |
+
|
| 50 |
+
==================================
|
| 51 |
+
Async http client/server framework
|
| 52 |
+
==================================
|
| 53 |
+
|
| 54 |
+
.. image:: https://raw.githubusercontent.com/aio-libs/aiohttp/master/docs/aiohttp-plain.svg
|
| 55 |
+
:height: 64px
|
| 56 |
+
:width: 64px
|
| 57 |
+
:alt: aiohttp logo
|
| 58 |
+
|
| 59 |
+
|
|
| 60 |
+
|
| 61 |
+
.. image:: https://github.com/aio-libs/aiohttp/workflows/CI/badge.svg
|
| 62 |
+
:target: https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI
|
| 63 |
+
:alt: GitHub Actions status for master branch
|
| 64 |
+
|
| 65 |
+
.. image:: https://codecov.io/gh/aio-libs/aiohttp/branch/master/graph/badge.svg
|
| 66 |
+
:target: https://codecov.io/gh/aio-libs/aiohttp
|
| 67 |
+
:alt: codecov.io status for master branch
|
| 68 |
+
|
| 69 |
+
.. image:: https://badge.fury.io/py/aiohttp.svg
|
| 70 |
+
:target: https://pypi.org/project/aiohttp
|
| 71 |
+
:alt: Latest PyPI package version
|
| 72 |
+
|
| 73 |
+
.. image:: https://img.shields.io/pypi/dm/aiohttp
|
| 74 |
+
:target: https://pypistats.org/packages/aiohttp
|
| 75 |
+
:alt: Downloads count
|
| 76 |
+
|
| 77 |
+
.. image:: https://readthedocs.org/projects/aiohttp/badge/?version=latest
|
| 78 |
+
:target: https://docs.aiohttp.org/
|
| 79 |
+
:alt: Latest Read The Docs
|
| 80 |
+
|
| 81 |
+
.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json
|
| 82 |
+
:target: https://codspeed.io/aio-libs/aiohttp
|
| 83 |
+
:alt: Codspeed.io status for aiohttp
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
Key Features
|
| 87 |
+
============
|
| 88 |
+
|
| 89 |
+
- Supports both client and server side of HTTP protocol.
|
| 90 |
+
- Supports both client and server Web-Sockets out-of-the-box and avoids
|
| 91 |
+
Callback Hell.
|
| 92 |
+
- Provides Web-server with middleware and pluggable routing.
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
Getting started
|
| 96 |
+
===============
|
| 97 |
+
|
| 98 |
+
Client
|
| 99 |
+
------
|
| 100 |
+
|
| 101 |
+
To get something from the web:
|
| 102 |
+
|
| 103 |
+
.. code-block:: python
|
| 104 |
+
|
| 105 |
+
import aiohttp
|
| 106 |
+
import asyncio
|
| 107 |
+
|
| 108 |
+
async def main():
|
| 109 |
+
|
| 110 |
+
async with aiohttp.ClientSession() as session:
|
| 111 |
+
async with session.get('http://python.org') as response:
|
| 112 |
+
|
| 113 |
+
print("Status:", response.status)
|
| 114 |
+
print("Content-type:", response.headers['content-type'])
|
| 115 |
+
|
| 116 |
+
html = await response.text()
|
| 117 |
+
print("Body:", html[:15], "...")
|
| 118 |
+
|
| 119 |
+
asyncio.run(main())
|
| 120 |
+
|
| 121 |
+
This prints:
|
| 122 |
+
|
| 123 |
+
.. code-block::
|
| 124 |
+
|
| 125 |
+
Status: 200
|
| 126 |
+
Content-type: text/html; charset=utf-8
|
| 127 |
+
Body: <!doctype html> ...
|
| 128 |
+
|
| 129 |
+
Coming from `requests <https://requests.readthedocs.io/>`_ ? Read `why we need so many lines <https://aiohttp.readthedocs.io/en/latest/http_request_lifecycle.html>`_.
|
| 130 |
+
|
| 131 |
+
Server
|
| 132 |
+
------
|
| 133 |
+
|
| 134 |
+
An example using a simple server:
|
| 135 |
+
|
| 136 |
+
.. code-block:: python
|
| 137 |
+
|
| 138 |
+
# examples/server_simple.py
|
| 139 |
+
from aiohttp import web
|
| 140 |
+
|
| 141 |
+
async def handle(request):
|
| 142 |
+
name = request.match_info.get('name', "Anonymous")
|
| 143 |
+
text = "Hello, " + name
|
| 144 |
+
return web.Response(text=text)
|
| 145 |
+
|
| 146 |
+
async def wshandle(request):
|
| 147 |
+
ws = web.WebSocketResponse()
|
| 148 |
+
await ws.prepare(request)
|
| 149 |
+
|
| 150 |
+
async for msg in ws:
|
| 151 |
+
if msg.type == web.WSMsgType.text:
|
| 152 |
+
await ws.send_str("Hello, {}".format(msg.data))
|
| 153 |
+
elif msg.type == web.WSMsgType.binary:
|
| 154 |
+
await ws.send_bytes(msg.data)
|
| 155 |
+
elif msg.type == web.WSMsgType.close:
|
| 156 |
+
break
|
| 157 |
+
|
| 158 |
+
return ws
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
app = web.Application()
|
| 162 |
+
app.add_routes([web.get('/', handle),
|
| 163 |
+
web.get('/echo', wshandle),
|
| 164 |
+
web.get('/{name}', handle)])
|
| 165 |
+
|
| 166 |
+
if __name__ == '__main__':
|
| 167 |
+
web.run_app(app)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
Documentation
|
| 171 |
+
=============
|
| 172 |
+
|
| 173 |
+
https://aiohttp.readthedocs.io/
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
Demos
|
| 177 |
+
=====
|
| 178 |
+
|
| 179 |
+
https://github.com/aio-libs/aiohttp-demos
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
External links
|
| 183 |
+
==============
|
| 184 |
+
|
| 185 |
+
* `Third party libraries
|
| 186 |
+
<http://aiohttp.readthedocs.io/en/latest/third_party.html>`_
|
| 187 |
+
* `Built with aiohttp
|
| 188 |
+
<http://aiohttp.readthedocs.io/en/latest/built_with.html>`_
|
| 189 |
+
* `Powered by aiohttp
|
| 190 |
+
<http://aiohttp.readthedocs.io/en/latest/powered_by.html>`_
|
| 191 |
+
|
| 192 |
+
Feel free to make a Pull Request for adding your link to these pages!
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
Communication channels
|
| 196 |
+
======================
|
| 197 |
+
|
| 198 |
+
*aio-libs Discussions*: https://github.com/aio-libs/aiohttp/discussions
|
| 199 |
+
|
| 200 |
+
*Matrix*: `#aio-libs:matrix.org <https://matrix.to/#/#aio-libs:matrix.org>`_
|
| 201 |
+
|
| 202 |
+
We support `Stack Overflow
|
| 203 |
+
<https://stackoverflow.com/questions/tagged/aiohttp>`_.
|
| 204 |
+
Please add *aiohttp* tag to your question there.
|
| 205 |
+
|
| 206 |
+
Requirements
|
| 207 |
+
============
|
| 208 |
+
|
| 209 |
+
- attrs_
|
| 210 |
+
- multidict_
|
| 211 |
+
- yarl_
|
| 212 |
+
- frozenlist_
|
| 213 |
+
|
| 214 |
+
Optionally you may install the aiodns_ library (highly recommended for sake of speed).
|
| 215 |
+
|
| 216 |
+
.. _aiodns: https://pypi.python.org/pypi/aiodns
|
| 217 |
+
.. _attrs: https://github.com/python-attrs/attrs
|
| 218 |
+
.. _multidict: https://pypi.python.org/pypi/multidict
|
| 219 |
+
.. _frozenlist: https://pypi.org/project/frozenlist/
|
| 220 |
+
.. _yarl: https://pypi.python.org/pypi/yarl
|
| 221 |
+
.. _async-timeout: https://pypi.python.org/pypi/async_timeout
|
| 222 |
+
|
| 223 |
+
License
|
| 224 |
+
=======
|
| 225 |
+
|
| 226 |
+
``aiohttp`` is offered under the Apache 2 license.
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
Keepsafe
|
| 230 |
+
========
|
| 231 |
+
|
| 232 |
+
The aiohttp community would like to thank Keepsafe
|
| 233 |
+
(https://www.getkeepsafe.com) for its support in the early days of
|
| 234 |
+
the project.
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
Source code
|
| 238 |
+
===========
|
| 239 |
+
|
| 240 |
+
The latest developer version is available in a GitHub repository:
|
| 241 |
+
https://github.com/aio-libs/aiohttp
|
| 242 |
+
|
| 243 |
+
Benchmarks
|
| 244 |
+
==========
|
| 245 |
+
|
| 246 |
+
If you are interested in efficiency, the AsyncIO community maintains a
|
| 247 |
+
list of benchmarks on the official wiki:
|
| 248 |
+
https://github.com/python/asyncio/wiki/Benchmarks
|
| 249 |
+
|
| 250 |
+
--------
|
| 251 |
+
|
| 252 |
+
.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
|
| 253 |
+
:target: https://matrix.to/#/%23aio-libs:matrix.org
|
| 254 |
+
:alt: Matrix Room — #aio-libs:matrix.org
|
| 255 |
+
|
| 256 |
+
.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
|
| 257 |
+
:target: https://matrix.to/#/%23aio-libs-space:matrix.org
|
| 258 |
+
:alt: Matrix Space — #aio-libs-space:matrix.org
|
| 259 |
+
|
| 260 |
+
.. image:: https://insights.linuxfoundation.org/api/badge/health-score?project=aiohttp
|
| 261 |
+
:target: https://insights.linuxfoundation.org/project/aiohttp
|
| 262 |
+
:alt: LFX Health Score
|
aiohttp-3.13.3.dist-info/RECORD
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
aiohttp-3.13.3.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
| 2 |
+
aiohttp-3.13.3.dist-info/METADATA,sha256=CQROZCStho-eb7xiFIuAzj30JuupEU_jHpYDFiG_HhM,8145
|
| 3 |
+
aiohttp-3.13.3.dist-info/RECORD,,
|
| 4 |
+
aiohttp-3.13.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 5 |
+
aiohttp-3.13.3.dist-info/WHEEL,sha256=DxRnWQz-Kp9-4a4hdDHsSv0KUC3H7sN9Nbef3-8RjXU,190
|
| 6 |
+
aiohttp-3.13.3.dist-info/licenses/LICENSE.txt,sha256=n4DQ2311WpQdtFchcsJw7L2PCCuiFd3QlZhZQu2Uqes,588
|
| 7 |
+
aiohttp-3.13.3.dist-info/licenses/vendor/llhttp/LICENSE,sha256=68qFTgE0zSVtZzYnwgSZ9CV363S6zwi58ltianPJEnc,1105
|
| 8 |
+
aiohttp-3.13.3.dist-info/top_level.txt,sha256=iv-JIaacmTl-hSho3QmphcKnbRRYx1st47yjz_178Ro,8
|
| 9 |
+
aiohttp/.hash/_cparser.pxd.hash,sha256=pjs-sEXNw_eijXGAedwG-BHnlFp8B7sOCgUagIWaU2A,121
|
| 10 |
+
aiohttp/.hash/_find_header.pxd.hash,sha256=_mbpD6vM-CVCKq3ulUvsOAz5Wdo88wrDzfpOsMQaMNA,125
|
| 11 |
+
aiohttp/.hash/_http_parser.pyx.hash,sha256=RKkD9x-EhXksvXrpCaTNWYtffb52urLvuTnxbTN2Lmw,125
|
| 12 |
+
aiohttp/.hash/_http_writer.pyx.hash,sha256=9txOh7t7c3y-vLmiuEY5dltmXvEo0CYyU4U853yyv9E,125
|
| 13 |
+
aiohttp/.hash/hdrs.py.hash,sha256=v6IaKbsxjsdQxBzhb5AjP0x_9G3rUe84D7avf7AI4cs,116
|
| 14 |
+
aiohttp/__init__.py,sha256=QWssFaD-DaFFcwP36lLUQzRmlSZ5KxivJBU-yg5C1wg,8302
|
| 15 |
+
aiohttp/_cookie_helpers.py,sha256=_p7y-B8OCAk7FLjByiuwFIpDLGuNoJn3_vixzymAFnE,13659
|
| 16 |
+
aiohttp/_cparser.pxd,sha256=UnbUYCHg4NdXfgyRVYAMv2KTLWClB4P-xCrvtj_r7ew,4295
|
| 17 |
+
aiohttp/_find_header.pxd,sha256=0GfwFCPN2zxEKTO1_MA5sYq2UfzsG8kcV3aTqvwlz3g,68
|
| 18 |
+
aiohttp/_headers.pxi,sha256=n701k28dVPjwRnx5j6LpJhLTfj7dqu2vJt7f0O60Oyg,2007
|
| 19 |
+
aiohttp/_http_parser.cpython-312-x86_64-linux-gnu.so,sha256=WZP45rtTvKwOq_1uXO_1L84Kz6I0AnqYZn0b5L-6HkA,2833152
|
| 20 |
+
aiohttp/_http_parser.pyx,sha256=-YI8YIY4uKd_7Bwr0o3FwEPwjHdexZ5-Ji3XS067c4Q,28261
|
| 21 |
+
aiohttp/_http_writer.cpython-312-x86_64-linux-gnu.so,sha256=mWC-4rsbntVD1V5ZEKEpSW_sm63V0XRe1fDR0lygipo,539144
|
| 22 |
+
aiohttp/_http_writer.pyx,sha256=VlFEBM6HoVv8a0AAJtc6JwFlsv2-cDE8-gB94p3dfhQ,4664
|
| 23 |
+
aiohttp/_websocket/.hash/mask.pxd.hash,sha256=Y0zBddk_ck3pi9-BFzMcpkcvCKvwvZ4GTtZFb9u1nxQ,128
|
| 24 |
+
aiohttp/_websocket/.hash/mask.pyx.hash,sha256=90owpXYM8_kIma4KUcOxhWSk-Uv4NVMBoCYeFM1B3d0,128
|
| 25 |
+
aiohttp/_websocket/.hash/reader_c.pxd.hash,sha256=5xf3oobk6vx4xbJm-xtZ1_QufB8fYFtLQV2MNdqUc1w,132
|
| 26 |
+
aiohttp/_websocket/__init__.py,sha256=Mar3R9_vBN_Ea4lsW7iTAVXD7OKswKPGqF5xgSyt77k,44
|
| 27 |
+
aiohttp/_websocket/helpers.py,sha256=P-XLv8IUaihKzDenVUqfKU5DJbWE5HvG8uhvUZK8Ic4,5038
|
| 28 |
+
aiohttp/_websocket/mask.cpython-312-x86_64-linux-gnu.so,sha256=EpRwPJm1K1yavMCd9llAWdT4AsqKx_QEN0rb0eJH_Kc,263512
|
| 29 |
+
aiohttp/_websocket/mask.pxd,sha256=sBmZ1Amym9kW4Ge8lj1fLZ7mPPya4LzLdpkQExQXv5M,112
|
| 30 |
+
aiohttp/_websocket/mask.pyx,sha256=BHjOtV0O0w7xp9p0LNADRJvGmgfPn9sGeJvSs0fL__4,1397
|
| 31 |
+
aiohttp/_websocket/models.py,sha256=XAzjs_8JYszWXIgZ6R3ZRrF-tX9Q_6LiD49WRYojopM,2121
|
| 32 |
+
aiohttp/_websocket/reader.py,sha256=eC4qS0c5sOeQ2ebAHLaBpIaTVFaSKX79pY2xvh3Pqyw,1030
|
| 33 |
+
aiohttp/_websocket/reader_c.cpython-312-x86_64-linux-gnu.so,sha256=GYd-y-IkGkOIp3vmma5BmZgmG9Py9fLTybcmMWyHNf0,1822128
|
| 34 |
+
aiohttp/_websocket/reader_c.pxd,sha256=nl_njtDrzlQU0rjgGGjZDB-swguE0tX_bCPobkShVa4,2625
|
| 35 |
+
aiohttp/_websocket/reader_c.py,sha256=V5YtZ2gj2BjE2Q-W9sR_MdAl1VAm1pB7ZjozVJcOpbg,18868
|
| 36 |
+
aiohttp/_websocket/reader_py.py,sha256=V5YtZ2gj2BjE2Q-W9sR_MdAl1VAm1pB7ZjozVJcOpbg,18868
|
| 37 |
+
aiohttp/_websocket/writer.py,sha256=2OvSktPmNh_g20h1cXJt2Xu8u6IvswnPjdur7OwBbJk,11261
|
| 38 |
+
aiohttp/abc.py,sha256=M66F4S6m00bIEn7y4ha_XLTMDmVQ9dPihfOVB0pGfOo,7149
|
| 39 |
+
aiohttp/base_protocol.py,sha256=Tp8cxUPQvv9kUPk3w6lAzk6d2MAzV3scwI_3Go3C47c,3025
|
| 40 |
+
aiohttp/client.py,sha256=fOQfwcIUL1NGAVRV4DDj6-wipBzeD8KZpmzhO-LLKp4,58357
|
| 41 |
+
aiohttp/client_exceptions.py,sha256=uyKbxI2peZhKl7lELBMx3UeusNkfpemPWpGFq0r6JeM,11367
|
| 42 |
+
aiohttp/client_middleware_digest_auth.py,sha256=G5JM9YtzL9AWklz6NP28xEOBeAvrAZgDzU657JqO4qs,17627
|
| 43 |
+
aiohttp/client_middlewares.py,sha256=kP5N9CMzQPMGPIEydeVUiLUTLsw8Vl8Gr4qAWYdu3vM,1918
|
| 44 |
+
aiohttp/client_proto.py,sha256=56_WtLStZGBFPYKzgEgY6v24JkhV1y6JEmmuxeJT2So,12110
|
| 45 |
+
aiohttp/client_reqrep.py,sha256=eEREDrZ0M8ZFTt1wjHduR-P8_sm40K65gNz-iMGYask,53391
|
| 46 |
+
aiohttp/client_ws.py,sha256=1CIjIXwyzOMIYw6AjUES4-qUwbyVHW1seJKQfg_Rta8,15109
|
| 47 |
+
aiohttp/compression_utils.py,sha256=hJ2LXhN2OWukFHm5b78TJFGKcAiL2kthi9Sf5PRYO-U,11738
|
| 48 |
+
aiohttp/connector.py,sha256=vT22BNuCDtbadE1Uq7HC7zpOWCHMxI4n3PtCz7zZZkw,69004
|
| 49 |
+
aiohttp/cookiejar.py,sha256=e28ZMQwJ5P0vbPX1OX4Se7-k3zeGvocFEqzGhwpG53k,18922
|
| 50 |
+
aiohttp/formdata.py,sha256=xqYMbUo1qoLYPuzY92XeR4pyEe-w-DNcToARDF3GUhA,6384
|
| 51 |
+
aiohttp/hdrs.py,sha256=2rj5MyA-6yRdYPhW5UKkW4iNWhEAlGIOSBH5D4FmKNE,5111
|
| 52 |
+
aiohttp/helpers.py,sha256=Q1307PCEnWz4RP8crUw8dk58c0YF2Ei3JywkKfRxz5E,30629
|
| 53 |
+
aiohttp/http.py,sha256=8o8j8xH70OWjnfTWA9V44NR785QPxEPrUtzMXiAVpwc,1842
|
| 54 |
+
aiohttp/http_exceptions.py,sha256=BjIxD4LtrQgytqoR5lOI9zAttNmSygRgksUsMRy7sss,3069
|
| 55 |
+
aiohttp/http_parser.py,sha256=z6djZDOUs7hdPzplTEsAVyz0of-rQAwT7xz8OpXhnuY,38177
|
| 56 |
+
aiohttp/http_websocket.py,sha256=8VXFKw6KQUEmPg48GtRMB37v0gTK7A0inoxXuDxMZEc,842
|
| 57 |
+
aiohttp/http_writer.py,sha256=fbRtKPYSqRbtAdr_gqpjF2-4sI1ESL8dPDF-xY_mAMY,12446
|
| 58 |
+
aiohttp/log.py,sha256=BbNKx9e3VMIm0xYjZI0IcBBoS7wjdeIeSaiJE7-qK2g,325
|
| 59 |
+
aiohttp/multipart.py,sha256=326npYdWxYI3raoRfmpBeUV_ef3-LRn8sV9WqcIOoPk,40482
|
| 60 |
+
aiohttp/payload.py,sha256=O6nsYNULL7AeM2cyJ6TYX73ncVnL5xJwt5AegxwMKqw,40874
|
| 61 |
+
aiohttp/payload_streamer.py,sha256=ZzEYyfzcjGWkVkK3XR2pBthSCSIykYvY3Wr5cGQ2eTc,2211
|
| 62 |
+
aiohttp/py.typed,sha256=sow9soTwP9T_gEAQSVh7Gb8855h04Nwmhs2We-JRgZM,7
|
| 63 |
+
aiohttp/pytest_plugin.py,sha256=z4XwqmsKdyJCKxbGiA5kFf90zcedvomqk4RqjZbhKNk,12901
|
| 64 |
+
aiohttp/resolver.py,sha256=gsrfUpFf8iHlcHfJvY-1fiBHW3PRvRVNb5lNZBg3zlY,10031
|
| 65 |
+
aiohttp/streams.py,sha256=rlwL7ek6CkMMYil_e_EokWv26uHmtzi3lKqlnLNrXCc,23666
|
| 66 |
+
aiohttp/tcp_helpers.py,sha256=BSadqVWaBpMFDRWnhaaR941N9MiDZ7bdTrxgCb0CW-M,961
|
| 67 |
+
aiohttp/test_utils.py,sha256=ZJSzZWjC76KSbtwddTKcP6vHpUl_ozfAf3F93ewmHRU,23016
|
| 68 |
+
aiohttp/tracing.py,sha256=-6aaW6l0J9uJD45LzR4cijYH0j62pt0U_nn_aVzFku4,14558
|
| 69 |
+
aiohttp/typedefs.py,sha256=wUlqwe9Mw9W8jT3HsYJcYk00qP3EMPz3nTkYXmeNN48,1657
|
| 70 |
+
aiohttp/web.py,sha256=JzSNmejg5G6YeFAnkIgZfytqbU86sNu844yYKmoUpqs,17852
|
| 71 |
+
aiohttp/web_app.py,sha256=lGU_aAMN-h3wy-LTTHi6SeKH8ydt1G51BXcCspgD5ZA,19452
|
| 72 |
+
aiohttp/web_exceptions.py,sha256=7nIuiwhZ39vJJ9KrWqArA5QcWbUdqkz2CLwEpJapeN8,10360
|
| 73 |
+
aiohttp/web_fileresponse.py,sha256=Xzau8EMrWNrFg3u46h4UEteg93G4zYq94CU6vy0HiqE,16362
|
| 74 |
+
aiohttp/web_log.py,sha256=rX5D7xLOX2B6BMdiZ-chme_KfJfW5IXEoFwLfkfkajs,7865
|
| 75 |
+
aiohttp/web_middlewares.py,sha256=sFI0AgeNjdyAjuz92QtMIpngmJSOxrqe2Jfbs4BNUu0,4165
|
| 76 |
+
aiohttp/web_protocol.py,sha256=6s9dMzmaqW77bzM1T111uGNSLFo6gNmfDg7XzYnA8xk,27010
|
| 77 |
+
aiohttp/web_request.py,sha256=KqrOp6AeWB5e6tKrG55Lo7Zbwq49DxdrKniuW2t2u04,29849
|
| 78 |
+
aiohttp/web_response.py,sha256=PKcziNU4LmftXqKVvoRMrAbOeVClpSN-iznHsiWezmU,29341
|
| 79 |
+
aiohttp/web_routedef.py,sha256=VT1GAx6BrawoDh5RwBwBu5wSABSqgWwAe74AUCyZAEo,6110
|
| 80 |
+
aiohttp/web_runner.py,sha256=v1G1nKiOOQgFnTSR4IMc6I9ReEFDMaHtMLvO_roDM-A,11786
|
| 81 |
+
aiohttp/web_server.py,sha256=-9WDKUAiR9ll-rSdwXSqG6YjaoW79d1R4y0BGSqgUMA,2888
|
| 82 |
+
aiohttp/web_urldispatcher.py,sha256=JM-TlriKCNbTLNL43Ra9sdZ0zChxZmIEYQM6ZpbyjI4,44290
|
| 83 |
+
aiohttp/web_ws.py,sha256=lItgmyatkXh0M6EY7JoZnSZkUl6R0wv8B88X4ILqQbU,22739
|
| 84 |
+
aiohttp/worker.py,sha256=zT0iWN5Xze194bO6_VjHou0x7lR_k0MviN6Kadnk22g,8152
|
aiohttp-3.13.3.dist-info/REQUESTED
ADDED
|
File without changes
|
aiohttp-3.13.3.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: setuptools (80.9.0)
|
| 3 |
+
Root-Is-Purelib: false
|
| 4 |
+
Tag: cp312-cp312-manylinux_2_17_x86_64
|
| 5 |
+
Tag: cp312-cp312-manylinux2014_x86_64
|
| 6 |
+
Tag: cp312-cp312-manylinux_2_28_x86_64
|
| 7 |
+
|
aiohttp-3.13.3.dist-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
aiohttp
|
aiohttp/_cookie_helpers.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Internal cookie handling helpers.
|
| 3 |
+
|
| 4 |
+
This module contains internal utilities for cookie parsing and manipulation.
|
| 5 |
+
These are not part of the public API and may change without notice.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import re
|
| 9 |
+
from http.cookies import Morsel
|
| 10 |
+
from typing import List, Optional, Sequence, Tuple, cast
|
| 11 |
+
|
| 12 |
+
from .log import internal_logger
|
| 13 |
+
|
| 14 |
+
__all__ = (
|
| 15 |
+
"parse_set_cookie_headers",
|
| 16 |
+
"parse_cookie_header",
|
| 17 |
+
"preserve_morsel_with_coded_value",
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# Cookie parsing constants
|
| 21 |
+
# Allow more characters in cookie names to handle real-world cookies
|
| 22 |
+
# that don't strictly follow RFC standards (fixes #2683)
|
| 23 |
+
# RFC 6265 defines cookie-name token as per RFC 2616 Section 2.2,
|
| 24 |
+
# but many servers send cookies with characters like {} [] () etc.
|
| 25 |
+
# This makes the cookie parser more tolerant of real-world cookies
|
| 26 |
+
# while still providing some validation to catch obviously malformed names.
|
| 27 |
+
_COOKIE_NAME_RE = re.compile(r"^[!#$%&\'()*+\-./0-9:<=>?@A-Z\[\]^_`a-z{|}~]+$")
|
| 28 |
+
_COOKIE_KNOWN_ATTRS = frozenset( # AKA Morsel._reserved
|
| 29 |
+
(
|
| 30 |
+
"path",
|
| 31 |
+
"domain",
|
| 32 |
+
"max-age",
|
| 33 |
+
"expires",
|
| 34 |
+
"secure",
|
| 35 |
+
"httponly",
|
| 36 |
+
"samesite",
|
| 37 |
+
"partitioned",
|
| 38 |
+
"version",
|
| 39 |
+
"comment",
|
| 40 |
+
)
|
| 41 |
+
)
|
| 42 |
+
_COOKIE_BOOL_ATTRS = frozenset( # AKA Morsel._flags
|
| 43 |
+
("secure", "httponly", "partitioned")
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
# SimpleCookie's pattern for parsing cookies with relaxed validation
|
| 47 |
+
# Based on http.cookies pattern but extended to allow more characters in cookie names
|
| 48 |
+
# to handle real-world cookies (fixes #2683)
|
| 49 |
+
_COOKIE_PATTERN = re.compile(
|
| 50 |
+
r"""
|
| 51 |
+
\s* # Optional whitespace at start of cookie
|
| 52 |
+
(?P<key> # Start of group 'key'
|
| 53 |
+
# aiohttp has extended to include [] for compatibility with real-world cookies
|
| 54 |
+
[\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\[\]]+ # Any word of at least one letter
|
| 55 |
+
) # End of group 'key'
|
| 56 |
+
( # Optional group: there may not be a value.
|
| 57 |
+
\s*=\s* # Equal Sign
|
| 58 |
+
(?P<val> # Start of group 'val'
|
| 59 |
+
"(?:[^\\"]|\\.)*" # Any double-quoted string (properly closed)
|
| 60 |
+
| # or
|
| 61 |
+
"[^";]* # Unmatched opening quote (differs from SimpleCookie - issue #7993)
|
| 62 |
+
| # or
|
| 63 |
+
# Special case for "expires" attr - RFC 822, RFC 850, RFC 1036, RFC 1123
|
| 64 |
+
(\w{3,6}day|\w{3}),\s # Day of the week or abbreviated day (with comma)
|
| 65 |
+
[\w\d\s-]{9,11}\s[\d:]{8}\s # Date and time in specific format
|
| 66 |
+
(GMT|[+-]\d{4}) # Timezone: GMT or RFC 2822 offset like -0000, +0100
|
| 67 |
+
# NOTE: RFC 2822 timezone support is an aiohttp extension
|
| 68 |
+
# for issue #4493 - SimpleCookie does NOT support this
|
| 69 |
+
| # or
|
| 70 |
+
# ANSI C asctime() format: "Wed Jun 9 10:18:14 2021"
|
| 71 |
+
# NOTE: This is an aiohttp extension for issue #4327 - SimpleCookie does NOT support this format
|
| 72 |
+
\w{3}\s+\w{3}\s+[\s\d]\d\s+\d{2}:\d{2}:\d{2}\s+\d{4}
|
| 73 |
+
| # or
|
| 74 |
+
[\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=\[\]]* # Any word or empty string
|
| 75 |
+
) # End of group 'val'
|
| 76 |
+
)? # End of optional value group
|
| 77 |
+
\s* # Any number of spaces.
|
| 78 |
+
(\s+|;|$) # Ending either at space, semicolon, or EOS.
|
| 79 |
+
""",
|
| 80 |
+
re.VERBOSE | re.ASCII,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def preserve_morsel_with_coded_value(cookie: Morsel[str]) -> Morsel[str]:
|
| 85 |
+
"""
|
| 86 |
+
Preserve a Morsel's coded_value exactly as received from the server.
|
| 87 |
+
|
| 88 |
+
This function ensures that cookie encoding is preserved exactly as sent by
|
| 89 |
+
the server, which is critical for compatibility with old servers that have
|
| 90 |
+
strict requirements about cookie formats.
|
| 91 |
+
|
| 92 |
+
This addresses the issue described in https://github.com/aio-libs/aiohttp/pull/1453
|
| 93 |
+
where Python's SimpleCookie would re-encode cookies, breaking authentication
|
| 94 |
+
with certain servers.
|
| 95 |
+
|
| 96 |
+
Args:
|
| 97 |
+
cookie: A Morsel object from SimpleCookie
|
| 98 |
+
|
| 99 |
+
Returns:
|
| 100 |
+
A Morsel object with preserved coded_value
|
| 101 |
+
|
| 102 |
+
"""
|
| 103 |
+
mrsl_val = cast("Morsel[str]", cookie.get(cookie.key, Morsel()))
|
| 104 |
+
# We use __setstate__ instead of the public set() API because it allows us to
|
| 105 |
+
# bypass validation and set already validated state. This is more stable than
|
| 106 |
+
# setting protected attributes directly and unlikely to change since it would
|
| 107 |
+
# break pickling.
|
| 108 |
+
mrsl_val.__setstate__( # type: ignore[attr-defined]
|
| 109 |
+
{"key": cookie.key, "value": cookie.value, "coded_value": cookie.coded_value}
|
| 110 |
+
)
|
| 111 |
+
return mrsl_val
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
_unquote_sub = re.compile(r"\\(?:([0-3][0-7][0-7])|(.))").sub
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def _unquote_replace(m: re.Match[str]) -> str:
|
| 118 |
+
"""
|
| 119 |
+
Replace function for _unquote_sub regex substitution.
|
| 120 |
+
|
| 121 |
+
Handles escaped characters in cookie values:
|
| 122 |
+
- Octal sequences are converted to their character representation
|
| 123 |
+
- Other escaped characters are unescaped by removing the backslash
|
| 124 |
+
"""
|
| 125 |
+
if m[1]:
|
| 126 |
+
return chr(int(m[1], 8))
|
| 127 |
+
return m[2]
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _unquote(value: str) -> str:
|
| 131 |
+
"""
|
| 132 |
+
Unquote a cookie value.
|
| 133 |
+
|
| 134 |
+
Vendored from http.cookies._unquote to ensure compatibility.
|
| 135 |
+
|
| 136 |
+
Note: The original implementation checked for None, but we've removed
|
| 137 |
+
that check since all callers already ensure the value is not None.
|
| 138 |
+
"""
|
| 139 |
+
# If there aren't any doublequotes,
|
| 140 |
+
# then there can't be any special characters. See RFC 2109.
|
| 141 |
+
if len(value) < 2:
|
| 142 |
+
return value
|
| 143 |
+
if value[0] != '"' or value[-1] != '"':
|
| 144 |
+
return value
|
| 145 |
+
|
| 146 |
+
# We have to assume that we must decode this string.
|
| 147 |
+
# Down to work.
|
| 148 |
+
|
| 149 |
+
# Remove the "s
|
| 150 |
+
value = value[1:-1]
|
| 151 |
+
|
| 152 |
+
# Check for special sequences. Examples:
|
| 153 |
+
# \012 --> \n
|
| 154 |
+
# \" --> "
|
| 155 |
+
#
|
| 156 |
+
return _unquote_sub(_unquote_replace, value)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def parse_cookie_header(header: str) -> List[Tuple[str, Morsel[str]]]:
|
| 160 |
+
"""
|
| 161 |
+
Parse a Cookie header according to RFC 6265 Section 5.4.
|
| 162 |
+
|
| 163 |
+
Cookie headers contain only name-value pairs separated by semicolons.
|
| 164 |
+
There are no attributes in Cookie headers - even names that match
|
| 165 |
+
attribute names (like 'path' or 'secure') should be treated as cookies.
|
| 166 |
+
|
| 167 |
+
This parser uses the same regex-based approach as parse_set_cookie_headers
|
| 168 |
+
to properly handle quoted values that may contain semicolons. When the
|
| 169 |
+
regex fails to match a malformed cookie, it falls back to simple parsing
|
| 170 |
+
to ensure subsequent cookies are not lost
|
| 171 |
+
https://github.com/aio-libs/aiohttp/issues/11632
|
| 172 |
+
|
| 173 |
+
Args:
|
| 174 |
+
header: The Cookie header value to parse
|
| 175 |
+
|
| 176 |
+
Returns:
|
| 177 |
+
List of (name, Morsel) tuples for compatibility with SimpleCookie.update()
|
| 178 |
+
"""
|
| 179 |
+
if not header:
|
| 180 |
+
return []
|
| 181 |
+
|
| 182 |
+
cookies: List[Tuple[str, Morsel[str]]] = []
|
| 183 |
+
morsel: Morsel[str]
|
| 184 |
+
i = 0
|
| 185 |
+
n = len(header)
|
| 186 |
+
|
| 187 |
+
invalid_names = []
|
| 188 |
+
while i < n:
|
| 189 |
+
# Use the same pattern as parse_set_cookie_headers to find cookies
|
| 190 |
+
match = _COOKIE_PATTERN.match(header, i)
|
| 191 |
+
if not match:
|
| 192 |
+
# Fallback for malformed cookies https://github.com/aio-libs/aiohttp/issues/11632
|
| 193 |
+
# Find next semicolon to skip or attempt simple key=value parsing
|
| 194 |
+
next_semi = header.find(";", i)
|
| 195 |
+
eq_pos = header.find("=", i)
|
| 196 |
+
|
| 197 |
+
# Try to extract key=value if '=' comes before ';'
|
| 198 |
+
if eq_pos != -1 and (next_semi == -1 or eq_pos < next_semi):
|
| 199 |
+
end_pos = next_semi if next_semi != -1 else n
|
| 200 |
+
key = header[i:eq_pos].strip()
|
| 201 |
+
value = header[eq_pos + 1 : end_pos].strip()
|
| 202 |
+
|
| 203 |
+
# Validate the name (same as regex path)
|
| 204 |
+
if not _COOKIE_NAME_RE.match(key):
|
| 205 |
+
invalid_names.append(key)
|
| 206 |
+
else:
|
| 207 |
+
morsel = Morsel()
|
| 208 |
+
morsel.__setstate__( # type: ignore[attr-defined]
|
| 209 |
+
{"key": key, "value": _unquote(value), "coded_value": value}
|
| 210 |
+
)
|
| 211 |
+
cookies.append((key, morsel))
|
| 212 |
+
|
| 213 |
+
# Move to next cookie or end
|
| 214 |
+
i = next_semi + 1 if next_semi != -1 else n
|
| 215 |
+
continue
|
| 216 |
+
|
| 217 |
+
key = match.group("key")
|
| 218 |
+
value = match.group("val") or ""
|
| 219 |
+
i = match.end(0)
|
| 220 |
+
|
| 221 |
+
# Validate the name
|
| 222 |
+
if not key or not _COOKIE_NAME_RE.match(key):
|
| 223 |
+
invalid_names.append(key)
|
| 224 |
+
continue
|
| 225 |
+
|
| 226 |
+
# Create new morsel
|
| 227 |
+
morsel = Morsel()
|
| 228 |
+
# Preserve the original value as coded_value (with quotes if present)
|
| 229 |
+
# We use __setstate__ instead of the public set() API because it allows us to
|
| 230 |
+
# bypass validation and set already validated state. This is more stable than
|
| 231 |
+
# setting protected attributes directly and unlikely to change since it would
|
| 232 |
+
# break pickling.
|
| 233 |
+
morsel.__setstate__( # type: ignore[attr-defined]
|
| 234 |
+
{"key": key, "value": _unquote(value), "coded_value": value}
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
cookies.append((key, morsel))
|
| 238 |
+
|
| 239 |
+
if invalid_names:
|
| 240 |
+
internal_logger.debug(
|
| 241 |
+
"Cannot load cookie. Illegal cookie names: %r", invalid_names
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
return cookies
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def parse_set_cookie_headers(headers: Sequence[str]) -> List[Tuple[str, Morsel[str]]]:
|
| 248 |
+
"""
|
| 249 |
+
Parse cookie headers using a vendored version of SimpleCookie parsing.
|
| 250 |
+
|
| 251 |
+
This implementation is based on SimpleCookie.__parse_string to ensure
|
| 252 |
+
compatibility with how SimpleCookie parses cookies, including handling
|
| 253 |
+
of malformed cookies with missing semicolons.
|
| 254 |
+
|
| 255 |
+
This function is used for both Cookie and Set-Cookie headers in order to be
|
| 256 |
+
forgiving. Ideally we would have followed RFC 6265 Section 5.2 (for Cookie
|
| 257 |
+
headers) and RFC 6265 Section 4.2.1 (for Set-Cookie headers), but the
|
| 258 |
+
real world data makes it impossible since we need to be a bit more forgiving.
|
| 259 |
+
|
| 260 |
+
NOTE: This implementation differs from SimpleCookie in handling unmatched quotes.
|
| 261 |
+
SimpleCookie will stop parsing when it encounters a cookie value with an unmatched
|
| 262 |
+
quote (e.g., 'cookie="value'), causing subsequent cookies to be silently dropped.
|
| 263 |
+
This implementation handles unmatched quotes more gracefully to prevent cookie loss.
|
| 264 |
+
See https://github.com/aio-libs/aiohttp/issues/7993
|
| 265 |
+
"""
|
| 266 |
+
parsed_cookies: List[Tuple[str, Morsel[str]]] = []
|
| 267 |
+
|
| 268 |
+
for header in headers:
|
| 269 |
+
if not header:
|
| 270 |
+
continue
|
| 271 |
+
|
| 272 |
+
# Parse cookie string using SimpleCookie's algorithm
|
| 273 |
+
i = 0
|
| 274 |
+
n = len(header)
|
| 275 |
+
current_morsel: Optional[Morsel[str]] = None
|
| 276 |
+
morsel_seen = False
|
| 277 |
+
|
| 278 |
+
while 0 <= i < n:
|
| 279 |
+
# Start looking for a cookie
|
| 280 |
+
match = _COOKIE_PATTERN.match(header, i)
|
| 281 |
+
if not match:
|
| 282 |
+
# No more cookies
|
| 283 |
+
break
|
| 284 |
+
|
| 285 |
+
key, value = match.group("key"), match.group("val")
|
| 286 |
+
i = match.end(0)
|
| 287 |
+
lower_key = key.lower()
|
| 288 |
+
|
| 289 |
+
if key[0] == "$":
|
| 290 |
+
if not morsel_seen:
|
| 291 |
+
# We ignore attributes which pertain to the cookie
|
| 292 |
+
# mechanism as a whole, such as "$Version".
|
| 293 |
+
continue
|
| 294 |
+
# Process as attribute
|
| 295 |
+
if current_morsel is not None:
|
| 296 |
+
attr_lower_key = lower_key[1:]
|
| 297 |
+
if attr_lower_key in _COOKIE_KNOWN_ATTRS:
|
| 298 |
+
current_morsel[attr_lower_key] = value or ""
|
| 299 |
+
elif lower_key in _COOKIE_KNOWN_ATTRS:
|
| 300 |
+
if not morsel_seen:
|
| 301 |
+
# Invalid cookie string - attribute before cookie
|
| 302 |
+
break
|
| 303 |
+
if lower_key in _COOKIE_BOOL_ATTRS:
|
| 304 |
+
# Boolean attribute with any value should be True
|
| 305 |
+
if current_morsel is not None and current_morsel.isReservedKey(key):
|
| 306 |
+
current_morsel[lower_key] = True
|
| 307 |
+
elif value is None:
|
| 308 |
+
# Invalid cookie string - non-boolean attribute without value
|
| 309 |
+
break
|
| 310 |
+
elif current_morsel is not None:
|
| 311 |
+
# Regular attribute with value
|
| 312 |
+
current_morsel[lower_key] = _unquote(value)
|
| 313 |
+
elif value is not None:
|
| 314 |
+
# This is a cookie name=value pair
|
| 315 |
+
# Validate the name
|
| 316 |
+
if key in _COOKIE_KNOWN_ATTRS or not _COOKIE_NAME_RE.match(key):
|
| 317 |
+
internal_logger.warning(
|
| 318 |
+
"Can not load cookies: Illegal cookie name %r", key
|
| 319 |
+
)
|
| 320 |
+
current_morsel = None
|
| 321 |
+
else:
|
| 322 |
+
# Create new morsel
|
| 323 |
+
current_morsel = Morsel()
|
| 324 |
+
# Preserve the original value as coded_value (with quotes if present)
|
| 325 |
+
# We use __setstate__ instead of the public set() API because it allows us to
|
| 326 |
+
# bypass validation and set already validated state. This is more stable than
|
| 327 |
+
# setting protected attributes directly and unlikely to change since it would
|
| 328 |
+
# break pickling.
|
| 329 |
+
current_morsel.__setstate__( # type: ignore[attr-defined]
|
| 330 |
+
{"key": key, "value": _unquote(value), "coded_value": value}
|
| 331 |
+
)
|
| 332 |
+
parsed_cookies.append((key, current_morsel))
|
| 333 |
+
morsel_seen = True
|
| 334 |
+
else:
|
| 335 |
+
# Invalid cookie string - no value for non-attribute
|
| 336 |
+
break
|
| 337 |
+
|
| 338 |
+
return parsed_cookies
|
aiohttp/_cparser.pxd
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from libc.stdint cimport int32_t, uint8_t, uint16_t, uint64_t
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
cdef extern from "llhttp.h":
|
| 5 |
+
|
| 6 |
+
struct llhttp__internal_s:
|
| 7 |
+
int32_t _index
|
| 8 |
+
void* _span_pos0
|
| 9 |
+
void* _span_cb0
|
| 10 |
+
int32_t error
|
| 11 |
+
const char* reason
|
| 12 |
+
const char* error_pos
|
| 13 |
+
void* data
|
| 14 |
+
void* _current
|
| 15 |
+
uint64_t content_length
|
| 16 |
+
uint8_t type
|
| 17 |
+
uint8_t method
|
| 18 |
+
uint8_t http_major
|
| 19 |
+
uint8_t http_minor
|
| 20 |
+
uint8_t header_state
|
| 21 |
+
uint8_t lenient_flags
|
| 22 |
+
uint8_t upgrade
|
| 23 |
+
uint8_t finish
|
| 24 |
+
uint16_t flags
|
| 25 |
+
uint16_t status_code
|
| 26 |
+
void* settings
|
| 27 |
+
|
| 28 |
+
ctypedef llhttp__internal_s llhttp__internal_t
|
| 29 |
+
ctypedef llhttp__internal_t llhttp_t
|
| 30 |
+
|
| 31 |
+
ctypedef int (*llhttp_data_cb)(llhttp_t*, const char *at, size_t length) except -1
|
| 32 |
+
ctypedef int (*llhttp_cb)(llhttp_t*) except -1
|
| 33 |
+
|
| 34 |
+
struct llhttp_settings_s:
|
| 35 |
+
llhttp_cb on_message_begin
|
| 36 |
+
llhttp_data_cb on_url
|
| 37 |
+
llhttp_data_cb on_status
|
| 38 |
+
llhttp_data_cb on_header_field
|
| 39 |
+
llhttp_data_cb on_header_value
|
| 40 |
+
llhttp_cb on_headers_complete
|
| 41 |
+
llhttp_data_cb on_body
|
| 42 |
+
llhttp_cb on_message_complete
|
| 43 |
+
llhttp_cb on_chunk_header
|
| 44 |
+
llhttp_cb on_chunk_complete
|
| 45 |
+
|
| 46 |
+
llhttp_cb on_url_complete
|
| 47 |
+
llhttp_cb on_status_complete
|
| 48 |
+
llhttp_cb on_header_field_complete
|
| 49 |
+
llhttp_cb on_header_value_complete
|
| 50 |
+
|
| 51 |
+
ctypedef llhttp_settings_s llhttp_settings_t
|
| 52 |
+
|
| 53 |
+
enum llhttp_errno:
|
| 54 |
+
HPE_OK,
|
| 55 |
+
HPE_INTERNAL,
|
| 56 |
+
HPE_STRICT,
|
| 57 |
+
HPE_LF_EXPECTED,
|
| 58 |
+
HPE_UNEXPECTED_CONTENT_LENGTH,
|
| 59 |
+
HPE_CLOSED_CONNECTION,
|
| 60 |
+
HPE_INVALID_METHOD,
|
| 61 |
+
HPE_INVALID_URL,
|
| 62 |
+
HPE_INVALID_CONSTANT,
|
| 63 |
+
HPE_INVALID_VERSION,
|
| 64 |
+
HPE_INVALID_HEADER_TOKEN,
|
| 65 |
+
HPE_INVALID_CONTENT_LENGTH,
|
| 66 |
+
HPE_INVALID_CHUNK_SIZE,
|
| 67 |
+
HPE_INVALID_STATUS,
|
| 68 |
+
HPE_INVALID_EOF_STATE,
|
| 69 |
+
HPE_INVALID_TRANSFER_ENCODING,
|
| 70 |
+
HPE_CB_MESSAGE_BEGIN,
|
| 71 |
+
HPE_CB_HEADERS_COMPLETE,
|
| 72 |
+
HPE_CB_MESSAGE_COMPLETE,
|
| 73 |
+
HPE_CB_CHUNK_HEADER,
|
| 74 |
+
HPE_CB_CHUNK_COMPLETE,
|
| 75 |
+
HPE_PAUSED,
|
| 76 |
+
HPE_PAUSED_UPGRADE,
|
| 77 |
+
HPE_USER
|
| 78 |
+
|
| 79 |
+
ctypedef llhttp_errno llhttp_errno_t
|
| 80 |
+
|
| 81 |
+
enum llhttp_flags:
|
| 82 |
+
F_CHUNKED,
|
| 83 |
+
F_CONTENT_LENGTH
|
| 84 |
+
|
| 85 |
+
enum llhttp_type:
|
| 86 |
+
HTTP_REQUEST,
|
| 87 |
+
HTTP_RESPONSE,
|
| 88 |
+
HTTP_BOTH
|
| 89 |
+
|
| 90 |
+
enum llhttp_method:
|
| 91 |
+
HTTP_DELETE,
|
| 92 |
+
HTTP_GET,
|
| 93 |
+
HTTP_HEAD,
|
| 94 |
+
HTTP_POST,
|
| 95 |
+
HTTP_PUT,
|
| 96 |
+
HTTP_CONNECT,
|
| 97 |
+
HTTP_OPTIONS,
|
| 98 |
+
HTTP_TRACE,
|
| 99 |
+
HTTP_COPY,
|
| 100 |
+
HTTP_LOCK,
|
| 101 |
+
HTTP_MKCOL,
|
| 102 |
+
HTTP_MOVE,
|
| 103 |
+
HTTP_PROPFIND,
|
| 104 |
+
HTTP_PROPPATCH,
|
| 105 |
+
HTTP_SEARCH,
|
| 106 |
+
HTTP_UNLOCK,
|
| 107 |
+
HTTP_BIND,
|
| 108 |
+
HTTP_REBIND,
|
| 109 |
+
HTTP_UNBIND,
|
| 110 |
+
HTTP_ACL,
|
| 111 |
+
HTTP_REPORT,
|
| 112 |
+
HTTP_MKACTIVITY,
|
| 113 |
+
HTTP_CHECKOUT,
|
| 114 |
+
HTTP_MERGE,
|
| 115 |
+
HTTP_MSEARCH,
|
| 116 |
+
HTTP_NOTIFY,
|
| 117 |
+
HTTP_SUBSCRIBE,
|
| 118 |
+
HTTP_UNSUBSCRIBE,
|
| 119 |
+
HTTP_PATCH,
|
| 120 |
+
HTTP_PURGE,
|
| 121 |
+
HTTP_MKCALENDAR,
|
| 122 |
+
HTTP_LINK,
|
| 123 |
+
HTTP_UNLINK,
|
| 124 |
+
HTTP_SOURCE,
|
| 125 |
+
HTTP_PRI,
|
| 126 |
+
HTTP_DESCRIBE,
|
| 127 |
+
HTTP_ANNOUNCE,
|
| 128 |
+
HTTP_SETUP,
|
| 129 |
+
HTTP_PLAY,
|
| 130 |
+
HTTP_PAUSE,
|
| 131 |
+
HTTP_TEARDOWN,
|
| 132 |
+
HTTP_GET_PARAMETER,
|
| 133 |
+
HTTP_SET_PARAMETER,
|
| 134 |
+
HTTP_REDIRECT,
|
| 135 |
+
HTTP_RECORD,
|
| 136 |
+
HTTP_FLUSH
|
| 137 |
+
|
| 138 |
+
ctypedef llhttp_method llhttp_method_t;
|
| 139 |
+
|
| 140 |
+
void llhttp_settings_init(llhttp_settings_t* settings)
|
| 141 |
+
void llhttp_init(llhttp_t* parser, llhttp_type type,
|
| 142 |
+
const llhttp_settings_t* settings)
|
| 143 |
+
|
| 144 |
+
llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len)
|
| 145 |
+
|
| 146 |
+
int llhttp_should_keep_alive(const llhttp_t* parser)
|
| 147 |
+
|
| 148 |
+
void llhttp_resume_after_upgrade(llhttp_t* parser)
|
| 149 |
+
|
| 150 |
+
llhttp_errno_t llhttp_get_errno(const llhttp_t* parser)
|
| 151 |
+
const char* llhttp_get_error_reason(const llhttp_t* parser)
|
| 152 |
+
const char* llhttp_get_error_pos(const llhttp_t* parser)
|
| 153 |
+
|
| 154 |
+
const char* llhttp_method_name(llhttp_method_t method)
|
| 155 |
+
|
| 156 |
+
void llhttp_set_lenient_headers(llhttp_t* parser, int enabled)
|
| 157 |
+
void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled)
|
| 158 |
+
void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled)
|
aiohttp/abc.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import logging
|
| 3 |
+
import socket
|
| 4 |
+
from abc import ABC, abstractmethod
|
| 5 |
+
from collections.abc import Sized
|
| 6 |
+
from http.cookies import BaseCookie, Morsel
|
| 7 |
+
from typing import (
|
| 8 |
+
TYPE_CHECKING,
|
| 9 |
+
Any,
|
| 10 |
+
Awaitable,
|
| 11 |
+
Callable,
|
| 12 |
+
Dict,
|
| 13 |
+
Generator,
|
| 14 |
+
Iterable,
|
| 15 |
+
List,
|
| 16 |
+
Optional,
|
| 17 |
+
Sequence,
|
| 18 |
+
Tuple,
|
| 19 |
+
TypedDict,
|
| 20 |
+
Union,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
from multidict import CIMultiDict
|
| 24 |
+
from yarl import URL
|
| 25 |
+
|
| 26 |
+
from ._cookie_helpers import parse_set_cookie_headers
|
| 27 |
+
from .typedefs import LooseCookies
|
| 28 |
+
|
| 29 |
+
if TYPE_CHECKING:
|
| 30 |
+
from .web_app import Application
|
| 31 |
+
from .web_exceptions import HTTPException
|
| 32 |
+
from .web_request import BaseRequest, Request
|
| 33 |
+
from .web_response import StreamResponse
|
| 34 |
+
else:
|
| 35 |
+
BaseRequest = Request = Application = StreamResponse = None
|
| 36 |
+
HTTPException = None
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class AbstractRouter(ABC):
|
| 40 |
+
def __init__(self) -> None:
|
| 41 |
+
self._frozen = False
|
| 42 |
+
|
| 43 |
+
def post_init(self, app: Application) -> None:
|
| 44 |
+
"""Post init stage.
|
| 45 |
+
|
| 46 |
+
Not an abstract method for sake of backward compatibility,
|
| 47 |
+
but if the router wants to be aware of the application
|
| 48 |
+
it can override this.
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
@property
|
| 52 |
+
def frozen(self) -> bool:
|
| 53 |
+
return self._frozen
|
| 54 |
+
|
| 55 |
+
def freeze(self) -> None:
|
| 56 |
+
"""Freeze router."""
|
| 57 |
+
self._frozen = True
|
| 58 |
+
|
| 59 |
+
@abstractmethod
|
| 60 |
+
async def resolve(self, request: Request) -> "AbstractMatchInfo":
|
| 61 |
+
"""Return MATCH_INFO for given request"""
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class AbstractMatchInfo(ABC):
|
| 65 |
+
|
| 66 |
+
__slots__ = ()
|
| 67 |
+
|
| 68 |
+
@property # pragma: no branch
|
| 69 |
+
@abstractmethod
|
| 70 |
+
def handler(self) -> Callable[[Request], Awaitable[StreamResponse]]:
|
| 71 |
+
"""Execute matched request handler"""
|
| 72 |
+
|
| 73 |
+
@property
|
| 74 |
+
@abstractmethod
|
| 75 |
+
def expect_handler(
|
| 76 |
+
self,
|
| 77 |
+
) -> Callable[[Request], Awaitable[Optional[StreamResponse]]]:
|
| 78 |
+
"""Expect handler for 100-continue processing"""
|
| 79 |
+
|
| 80 |
+
@property # pragma: no branch
|
| 81 |
+
@abstractmethod
|
| 82 |
+
def http_exception(self) -> Optional[HTTPException]:
|
| 83 |
+
"""HTTPException instance raised on router's resolving, or None"""
|
| 84 |
+
|
| 85 |
+
@abstractmethod # pragma: no branch
|
| 86 |
+
def get_info(self) -> Dict[str, Any]:
|
| 87 |
+
"""Return a dict with additional info useful for introspection"""
|
| 88 |
+
|
| 89 |
+
@property # pragma: no branch
|
| 90 |
+
@abstractmethod
|
| 91 |
+
def apps(self) -> Tuple[Application, ...]:
|
| 92 |
+
"""Stack of nested applications.
|
| 93 |
+
|
| 94 |
+
Top level application is left-most element.
|
| 95 |
+
|
| 96 |
+
"""
|
| 97 |
+
|
| 98 |
+
@abstractmethod
|
| 99 |
+
def add_app(self, app: Application) -> None:
|
| 100 |
+
"""Add application to the nested apps stack."""
|
| 101 |
+
|
| 102 |
+
@abstractmethod
|
| 103 |
+
def freeze(self) -> None:
|
| 104 |
+
"""Freeze the match info.
|
| 105 |
+
|
| 106 |
+
The method is called after route resolution.
|
| 107 |
+
|
| 108 |
+
After the call .add_app() is forbidden.
|
| 109 |
+
|
| 110 |
+
"""
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class AbstractView(ABC):
|
| 114 |
+
"""Abstract class based view."""
|
| 115 |
+
|
| 116 |
+
def __init__(self, request: Request) -> None:
|
| 117 |
+
self._request = request
|
| 118 |
+
|
| 119 |
+
@property
|
| 120 |
+
def request(self) -> Request:
|
| 121 |
+
"""Request instance."""
|
| 122 |
+
return self._request
|
| 123 |
+
|
| 124 |
+
@abstractmethod
|
| 125 |
+
def __await__(self) -> Generator[None, None, StreamResponse]:
|
| 126 |
+
"""Execute the view handler."""
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
class ResolveResult(TypedDict):
|
| 130 |
+
"""Resolve result.
|
| 131 |
+
|
| 132 |
+
This is the result returned from an AbstractResolver's
|
| 133 |
+
resolve method.
|
| 134 |
+
|
| 135 |
+
:param hostname: The hostname that was provided.
|
| 136 |
+
:param host: The IP address that was resolved.
|
| 137 |
+
:param port: The port that was resolved.
|
| 138 |
+
:param family: The address family that was resolved.
|
| 139 |
+
:param proto: The protocol that was resolved.
|
| 140 |
+
:param flags: The flags that were resolved.
|
| 141 |
+
"""
|
| 142 |
+
|
| 143 |
+
hostname: str
|
| 144 |
+
host: str
|
| 145 |
+
port: int
|
| 146 |
+
family: int
|
| 147 |
+
proto: int
|
| 148 |
+
flags: int
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
class AbstractResolver(ABC):
|
| 152 |
+
"""Abstract DNS resolver."""
|
| 153 |
+
|
| 154 |
+
@abstractmethod
|
| 155 |
+
async def resolve(
|
| 156 |
+
self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET
|
| 157 |
+
) -> List[ResolveResult]:
|
| 158 |
+
"""Return IP address for given hostname"""
|
| 159 |
+
|
| 160 |
+
@abstractmethod
|
| 161 |
+
async def close(self) -> None:
|
| 162 |
+
"""Release resolver"""
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
if TYPE_CHECKING:
|
| 166 |
+
IterableBase = Iterable[Morsel[str]]
|
| 167 |
+
else:
|
| 168 |
+
IterableBase = Iterable
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
ClearCookiePredicate = Callable[["Morsel[str]"], bool]
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
class AbstractCookieJar(Sized, IterableBase):
|
| 175 |
+
"""Abstract Cookie Jar."""
|
| 176 |
+
|
| 177 |
+
def __init__(self, *, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
|
| 178 |
+
self._loop = loop or asyncio.get_running_loop()
|
| 179 |
+
|
| 180 |
+
@property
|
| 181 |
+
@abstractmethod
|
| 182 |
+
def quote_cookie(self) -> bool:
|
| 183 |
+
"""Return True if cookies should be quoted."""
|
| 184 |
+
|
| 185 |
+
@abstractmethod
|
| 186 |
+
def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
|
| 187 |
+
"""Clear all cookies if no predicate is passed."""
|
| 188 |
+
|
| 189 |
+
@abstractmethod
|
| 190 |
+
def clear_domain(self, domain: str) -> None:
|
| 191 |
+
"""Clear all cookies for domain and all subdomains."""
|
| 192 |
+
|
| 193 |
+
@abstractmethod
|
| 194 |
+
def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> None:
|
| 195 |
+
"""Update cookies."""
|
| 196 |
+
|
| 197 |
+
def update_cookies_from_headers(
|
| 198 |
+
self, headers: Sequence[str], response_url: URL
|
| 199 |
+
) -> None:
|
| 200 |
+
"""Update cookies from raw Set-Cookie headers."""
|
| 201 |
+
if headers and (cookies_to_update := parse_set_cookie_headers(headers)):
|
| 202 |
+
self.update_cookies(cookies_to_update, response_url)
|
| 203 |
+
|
| 204 |
+
@abstractmethod
|
| 205 |
+
def filter_cookies(self, request_url: URL) -> "BaseCookie[str]":
|
| 206 |
+
"""Return the jar's cookies filtered by their attributes."""
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
class AbstractStreamWriter(ABC):
|
| 210 |
+
"""Abstract stream writer."""
|
| 211 |
+
|
| 212 |
+
buffer_size: int = 0
|
| 213 |
+
output_size: int = 0
|
| 214 |
+
length: Optional[int] = 0
|
| 215 |
+
|
| 216 |
+
@abstractmethod
|
| 217 |
+
async def write(self, chunk: Union[bytes, bytearray, memoryview]) -> None:
|
| 218 |
+
"""Write chunk into stream."""
|
| 219 |
+
|
| 220 |
+
@abstractmethod
|
| 221 |
+
async def write_eof(self, chunk: bytes = b"") -> None:
|
| 222 |
+
"""Write last chunk."""
|
| 223 |
+
|
| 224 |
+
@abstractmethod
|
| 225 |
+
async def drain(self) -> None:
|
| 226 |
+
"""Flush the write buffer."""
|
| 227 |
+
|
| 228 |
+
@abstractmethod
|
| 229 |
+
def enable_compression(
|
| 230 |
+
self, encoding: str = "deflate", strategy: Optional[int] = None
|
| 231 |
+
) -> None:
|
| 232 |
+
"""Enable HTTP body compression"""
|
| 233 |
+
|
| 234 |
+
@abstractmethod
|
| 235 |
+
def enable_chunking(self) -> None:
|
| 236 |
+
"""Enable HTTP chunked mode"""
|
| 237 |
+
|
| 238 |
+
@abstractmethod
|
| 239 |
+
async def write_headers(
|
| 240 |
+
self, status_line: str, headers: "CIMultiDict[str]"
|
| 241 |
+
) -> None:
|
| 242 |
+
"""Write HTTP headers"""
|
| 243 |
+
|
| 244 |
+
def send_headers(self) -> None:
|
| 245 |
+
"""Force sending buffered headers if not already sent.
|
| 246 |
+
|
| 247 |
+
Required only if write_headers() buffers headers instead of sending immediately.
|
| 248 |
+
For backwards compatibility, this method does nothing by default.
|
| 249 |
+
"""
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
class AbstractAccessLogger(ABC):
|
| 253 |
+
"""Abstract writer to access log."""
|
| 254 |
+
|
| 255 |
+
__slots__ = ("logger", "log_format")
|
| 256 |
+
|
| 257 |
+
def __init__(self, logger: logging.Logger, log_format: str) -> None:
|
| 258 |
+
self.logger = logger
|
| 259 |
+
self.log_format = log_format
|
| 260 |
+
|
| 261 |
+
@abstractmethod
|
| 262 |
+
def log(self, request: BaseRequest, response: StreamResponse, time: float) -> None:
|
| 263 |
+
"""Emit log to logger."""
|
| 264 |
+
|
| 265 |
+
@property
|
| 266 |
+
def enabled(self) -> bool:
|
| 267 |
+
"""Check if logger is enabled."""
|
| 268 |
+
return True
|
aiohttp/client_exceptions.py
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HTTP related errors."""
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import warnings
|
| 5 |
+
from typing import TYPE_CHECKING, Optional, Tuple, Union
|
| 6 |
+
|
| 7 |
+
from multidict import MultiMapping
|
| 8 |
+
|
| 9 |
+
from .typedefs import StrOrURL
|
| 10 |
+
|
| 11 |
+
if TYPE_CHECKING:
|
| 12 |
+
import ssl
|
| 13 |
+
|
| 14 |
+
SSLContext = ssl.SSLContext
|
| 15 |
+
else:
|
| 16 |
+
try:
|
| 17 |
+
import ssl
|
| 18 |
+
|
| 19 |
+
SSLContext = ssl.SSLContext
|
| 20 |
+
except ImportError: # pragma: no cover
|
| 21 |
+
ssl = SSLContext = None # type: ignore[assignment]
|
| 22 |
+
|
| 23 |
+
if TYPE_CHECKING:
|
| 24 |
+
from .client_reqrep import ClientResponse, ConnectionKey, Fingerprint, RequestInfo
|
| 25 |
+
from .http_parser import RawResponseMessage
|
| 26 |
+
else:
|
| 27 |
+
RequestInfo = ClientResponse = ConnectionKey = RawResponseMessage = None
|
| 28 |
+
|
| 29 |
+
__all__ = (
|
| 30 |
+
"ClientError",
|
| 31 |
+
"ClientConnectionError",
|
| 32 |
+
"ClientConnectionResetError",
|
| 33 |
+
"ClientOSError",
|
| 34 |
+
"ClientConnectorError",
|
| 35 |
+
"ClientProxyConnectionError",
|
| 36 |
+
"ClientSSLError",
|
| 37 |
+
"ClientConnectorDNSError",
|
| 38 |
+
"ClientConnectorSSLError",
|
| 39 |
+
"ClientConnectorCertificateError",
|
| 40 |
+
"ConnectionTimeoutError",
|
| 41 |
+
"SocketTimeoutError",
|
| 42 |
+
"ServerConnectionError",
|
| 43 |
+
"ServerTimeoutError",
|
| 44 |
+
"ServerDisconnectedError",
|
| 45 |
+
"ServerFingerprintMismatch",
|
| 46 |
+
"ClientResponseError",
|
| 47 |
+
"ClientHttpProxyError",
|
| 48 |
+
"WSServerHandshakeError",
|
| 49 |
+
"ContentTypeError",
|
| 50 |
+
"ClientPayloadError",
|
| 51 |
+
"InvalidURL",
|
| 52 |
+
"InvalidUrlClientError",
|
| 53 |
+
"RedirectClientError",
|
| 54 |
+
"NonHttpUrlClientError",
|
| 55 |
+
"InvalidUrlRedirectClientError",
|
| 56 |
+
"NonHttpUrlRedirectClientError",
|
| 57 |
+
"WSMessageTypeError",
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class ClientError(Exception):
|
| 62 |
+
"""Base class for client connection errors."""
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class ClientResponseError(ClientError):
|
| 66 |
+
"""Base class for exceptions that occur after getting a response.
|
| 67 |
+
|
| 68 |
+
request_info: An instance of RequestInfo.
|
| 69 |
+
history: A sequence of responses, if redirects occurred.
|
| 70 |
+
status: HTTP status code.
|
| 71 |
+
message: Error message.
|
| 72 |
+
headers: Response headers.
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
def __init__(
|
| 76 |
+
self,
|
| 77 |
+
request_info: RequestInfo,
|
| 78 |
+
history: Tuple[ClientResponse, ...],
|
| 79 |
+
*,
|
| 80 |
+
code: Optional[int] = None,
|
| 81 |
+
status: Optional[int] = None,
|
| 82 |
+
message: str = "",
|
| 83 |
+
headers: Optional[MultiMapping[str]] = None,
|
| 84 |
+
) -> None:
|
| 85 |
+
self.request_info = request_info
|
| 86 |
+
if code is not None:
|
| 87 |
+
if status is not None:
|
| 88 |
+
raise ValueError(
|
| 89 |
+
"Both code and status arguments are provided; "
|
| 90 |
+
"code is deprecated, use status instead"
|
| 91 |
+
)
|
| 92 |
+
warnings.warn(
|
| 93 |
+
"code argument is deprecated, use status instead",
|
| 94 |
+
DeprecationWarning,
|
| 95 |
+
stacklevel=2,
|
| 96 |
+
)
|
| 97 |
+
if status is not None:
|
| 98 |
+
self.status = status
|
| 99 |
+
elif code is not None:
|
| 100 |
+
self.status = code
|
| 101 |
+
else:
|
| 102 |
+
self.status = 0
|
| 103 |
+
self.message = message
|
| 104 |
+
self.headers = headers
|
| 105 |
+
self.history = history
|
| 106 |
+
self.args = (request_info, history)
|
| 107 |
+
|
| 108 |
+
def __str__(self) -> str:
|
| 109 |
+
return "{}, message={!r}, url={!r}".format(
|
| 110 |
+
self.status,
|
| 111 |
+
self.message,
|
| 112 |
+
str(self.request_info.real_url),
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
def __repr__(self) -> str:
|
| 116 |
+
args = f"{self.request_info!r}, {self.history!r}"
|
| 117 |
+
if self.status != 0:
|
| 118 |
+
args += f", status={self.status!r}"
|
| 119 |
+
if self.message != "":
|
| 120 |
+
args += f", message={self.message!r}"
|
| 121 |
+
if self.headers is not None:
|
| 122 |
+
args += f", headers={self.headers!r}"
|
| 123 |
+
return f"{type(self).__name__}({args})"
|
| 124 |
+
|
| 125 |
+
@property
|
| 126 |
+
def code(self) -> int:
|
| 127 |
+
warnings.warn(
|
| 128 |
+
"code property is deprecated, use status instead",
|
| 129 |
+
DeprecationWarning,
|
| 130 |
+
stacklevel=2,
|
| 131 |
+
)
|
| 132 |
+
return self.status
|
| 133 |
+
|
| 134 |
+
@code.setter
|
| 135 |
+
def code(self, value: int) -> None:
|
| 136 |
+
warnings.warn(
|
| 137 |
+
"code property is deprecated, use status instead",
|
| 138 |
+
DeprecationWarning,
|
| 139 |
+
stacklevel=2,
|
| 140 |
+
)
|
| 141 |
+
self.status = value
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class ContentTypeError(ClientResponseError):
|
| 145 |
+
"""ContentType found is not valid."""
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
class WSServerHandshakeError(ClientResponseError):
|
| 149 |
+
"""websocket server handshake error."""
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class ClientHttpProxyError(ClientResponseError):
|
| 153 |
+
"""HTTP proxy error.
|
| 154 |
+
|
| 155 |
+
Raised in :class:`aiohttp.connector.TCPConnector` if
|
| 156 |
+
proxy responds with status other than ``200 OK``
|
| 157 |
+
on ``CONNECT`` request.
|
| 158 |
+
"""
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
class TooManyRedirects(ClientResponseError):
|
| 162 |
+
"""Client was redirected too many times."""
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
class ClientConnectionError(ClientError):
|
| 166 |
+
"""Base class for client socket errors."""
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
class ClientConnectionResetError(ClientConnectionError, ConnectionResetError):
|
| 170 |
+
"""ConnectionResetError"""
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
class ClientOSError(ClientConnectionError, OSError):
|
| 174 |
+
"""OSError error."""
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
class ClientConnectorError(ClientOSError):
|
| 178 |
+
"""Client connector error.
|
| 179 |
+
|
| 180 |
+
Raised in :class:`aiohttp.connector.TCPConnector` if
|
| 181 |
+
a connection can not be established.
|
| 182 |
+
"""
|
| 183 |
+
|
| 184 |
+
def __init__(self, connection_key: ConnectionKey, os_error: OSError) -> None:
|
| 185 |
+
self._conn_key = connection_key
|
| 186 |
+
self._os_error = os_error
|
| 187 |
+
super().__init__(os_error.errno, os_error.strerror)
|
| 188 |
+
self.args = (connection_key, os_error)
|
| 189 |
+
|
| 190 |
+
@property
|
| 191 |
+
def os_error(self) -> OSError:
|
| 192 |
+
return self._os_error
|
| 193 |
+
|
| 194 |
+
@property
|
| 195 |
+
def host(self) -> str:
|
| 196 |
+
return self._conn_key.host
|
| 197 |
+
|
| 198 |
+
@property
|
| 199 |
+
def port(self) -> Optional[int]:
|
| 200 |
+
return self._conn_key.port
|
| 201 |
+
|
| 202 |
+
@property
|
| 203 |
+
def ssl(self) -> Union[SSLContext, bool, "Fingerprint"]:
|
| 204 |
+
return self._conn_key.ssl
|
| 205 |
+
|
| 206 |
+
def __str__(self) -> str:
|
| 207 |
+
return "Cannot connect to host {0.host}:{0.port} ssl:{1} [{2}]".format(
|
| 208 |
+
self, "default" if self.ssl is True else self.ssl, self.strerror
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
# OSError.__reduce__ does too much black magick
|
| 212 |
+
__reduce__ = BaseException.__reduce__
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
class ClientConnectorDNSError(ClientConnectorError):
|
| 216 |
+
"""DNS resolution failed during client connection.
|
| 217 |
+
|
| 218 |
+
Raised in :class:`aiohttp.connector.TCPConnector` if
|
| 219 |
+
DNS resolution fails.
|
| 220 |
+
"""
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
class ClientProxyConnectionError(ClientConnectorError):
|
| 224 |
+
"""Proxy connection error.
|
| 225 |
+
|
| 226 |
+
Raised in :class:`aiohttp.connector.TCPConnector` if
|
| 227 |
+
connection to proxy can not be established.
|
| 228 |
+
"""
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
class UnixClientConnectorError(ClientConnectorError):
|
| 232 |
+
"""Unix connector error.
|
| 233 |
+
|
| 234 |
+
Raised in :py:class:`aiohttp.connector.UnixConnector`
|
| 235 |
+
if connection to unix socket can not be established.
|
| 236 |
+
"""
|
| 237 |
+
|
| 238 |
+
def __init__(
|
| 239 |
+
self, path: str, connection_key: ConnectionKey, os_error: OSError
|
| 240 |
+
) -> None:
|
| 241 |
+
self._path = path
|
| 242 |
+
super().__init__(connection_key, os_error)
|
| 243 |
+
|
| 244 |
+
@property
|
| 245 |
+
def path(self) -> str:
|
| 246 |
+
return self._path
|
| 247 |
+
|
| 248 |
+
def __str__(self) -> str:
|
| 249 |
+
return "Cannot connect to unix socket {0.path} ssl:{1} [{2}]".format(
|
| 250 |
+
self, "default" if self.ssl is True else self.ssl, self.strerror
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
class ServerConnectionError(ClientConnectionError):
|
| 255 |
+
"""Server connection errors."""
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
class ServerDisconnectedError(ServerConnectionError):
|
| 259 |
+
"""Server disconnected."""
|
| 260 |
+
|
| 261 |
+
def __init__(self, message: Union[RawResponseMessage, str, None] = None) -> None:
|
| 262 |
+
if message is None:
|
| 263 |
+
message = "Server disconnected"
|
| 264 |
+
|
| 265 |
+
self.args = (message,)
|
| 266 |
+
self.message = message
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
class ServerTimeoutError(ServerConnectionError, asyncio.TimeoutError):
|
| 270 |
+
"""Server timeout error."""
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
class ConnectionTimeoutError(ServerTimeoutError):
|
| 274 |
+
"""Connection timeout error."""
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
class SocketTimeoutError(ServerTimeoutError):
|
| 278 |
+
"""Socket timeout error."""
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
class ServerFingerprintMismatch(ServerConnectionError):
|
| 282 |
+
"""SSL certificate does not match expected fingerprint."""
|
| 283 |
+
|
| 284 |
+
def __init__(self, expected: bytes, got: bytes, host: str, port: int) -> None:
|
| 285 |
+
self.expected = expected
|
| 286 |
+
self.got = got
|
| 287 |
+
self.host = host
|
| 288 |
+
self.port = port
|
| 289 |
+
self.args = (expected, got, host, port)
|
| 290 |
+
|
| 291 |
+
def __repr__(self) -> str:
|
| 292 |
+
return "<{} expected={!r} got={!r} host={!r} port={!r}>".format(
|
| 293 |
+
self.__class__.__name__, self.expected, self.got, self.host, self.port
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
class ClientPayloadError(ClientError):
|
| 298 |
+
"""Response payload error."""
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
class InvalidURL(ClientError, ValueError):
|
| 302 |
+
"""Invalid URL.
|
| 303 |
+
|
| 304 |
+
URL used for fetching is malformed, e.g. it doesn't contains host
|
| 305 |
+
part.
|
| 306 |
+
"""
|
| 307 |
+
|
| 308 |
+
# Derive from ValueError for backward compatibility
|
| 309 |
+
|
| 310 |
+
def __init__(self, url: StrOrURL, description: Union[str, None] = None) -> None:
|
| 311 |
+
# The type of url is not yarl.URL because the exception can be raised
|
| 312 |
+
# on URL(url) call
|
| 313 |
+
self._url = url
|
| 314 |
+
self._description = description
|
| 315 |
+
|
| 316 |
+
if description:
|
| 317 |
+
super().__init__(url, description)
|
| 318 |
+
else:
|
| 319 |
+
super().__init__(url)
|
| 320 |
+
|
| 321 |
+
@property
|
| 322 |
+
def url(self) -> StrOrURL:
|
| 323 |
+
return self._url
|
| 324 |
+
|
| 325 |
+
@property
|
| 326 |
+
def description(self) -> "str | None":
|
| 327 |
+
return self._description
|
| 328 |
+
|
| 329 |
+
def __repr__(self) -> str:
|
| 330 |
+
return f"<{self.__class__.__name__} {self}>"
|
| 331 |
+
|
| 332 |
+
def __str__(self) -> str:
|
| 333 |
+
if self._description:
|
| 334 |
+
return f"{self._url} - {self._description}"
|
| 335 |
+
return str(self._url)
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
class InvalidUrlClientError(InvalidURL):
|
| 339 |
+
"""Invalid URL client error."""
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
class RedirectClientError(ClientError):
|
| 343 |
+
"""Client redirect error."""
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
class NonHttpUrlClientError(ClientError):
|
| 347 |
+
"""Non http URL client error."""
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
class InvalidUrlRedirectClientError(InvalidUrlClientError, RedirectClientError):
|
| 351 |
+
"""Invalid URL redirect client error."""
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
class NonHttpUrlRedirectClientError(NonHttpUrlClientError, RedirectClientError):
|
| 355 |
+
"""Non http URL redirect client error."""
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
class ClientSSLError(ClientConnectorError):
|
| 359 |
+
"""Base error for ssl.*Errors."""
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
if ssl is not None:
|
| 363 |
+
cert_errors = (ssl.CertificateError,)
|
| 364 |
+
cert_errors_bases = (
|
| 365 |
+
ClientSSLError,
|
| 366 |
+
ssl.CertificateError,
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
ssl_errors = (ssl.SSLError,)
|
| 370 |
+
ssl_error_bases = (ClientSSLError, ssl.SSLError)
|
| 371 |
+
else: # pragma: no cover
|
| 372 |
+
cert_errors = tuple()
|
| 373 |
+
cert_errors_bases = (
|
| 374 |
+
ClientSSLError,
|
| 375 |
+
ValueError,
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
ssl_errors = tuple()
|
| 379 |
+
ssl_error_bases = (ClientSSLError,)
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
class ClientConnectorSSLError(*ssl_error_bases): # type: ignore[misc]
|
| 383 |
+
"""Response ssl error."""
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
class ClientConnectorCertificateError(*cert_errors_bases): # type: ignore[misc]
|
| 387 |
+
"""Response certificate error."""
|
| 388 |
+
|
| 389 |
+
def __init__(
|
| 390 |
+
self, connection_key: ConnectionKey, certificate_error: Exception
|
| 391 |
+
) -> None:
|
| 392 |
+
self._conn_key = connection_key
|
| 393 |
+
self._certificate_error = certificate_error
|
| 394 |
+
self.args = (connection_key, certificate_error)
|
| 395 |
+
|
| 396 |
+
@property
|
| 397 |
+
def certificate_error(self) -> Exception:
|
| 398 |
+
return self._certificate_error
|
| 399 |
+
|
| 400 |
+
@property
|
| 401 |
+
def host(self) -> str:
|
| 402 |
+
return self._conn_key.host
|
| 403 |
+
|
| 404 |
+
@property
|
| 405 |
+
def port(self) -> Optional[int]:
|
| 406 |
+
return self._conn_key.port
|
| 407 |
+
|
| 408 |
+
@property
|
| 409 |
+
def ssl(self) -> bool:
|
| 410 |
+
return self._conn_key.is_ssl
|
| 411 |
+
|
| 412 |
+
def __str__(self) -> str:
|
| 413 |
+
return (
|
| 414 |
+
"Cannot connect to host {0.host}:{0.port} ssl:{0.ssl} "
|
| 415 |
+
"[{0.certificate_error.__class__.__name__}: "
|
| 416 |
+
"{0.certificate_error.args}]".format(self)
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
class WSMessageTypeError(TypeError):
|
| 421 |
+
"""WebSocket message type is not valid."""
|
aiohttp/client_middleware_digest_auth.py
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Digest authentication middleware for aiohttp client.
|
| 3 |
+
|
| 4 |
+
This middleware implements HTTP Digest Authentication according to RFC 7616,
|
| 5 |
+
providing a more secure alternative to Basic Authentication. It supports all
|
| 6 |
+
standard hash algorithms including MD5, SHA, SHA-256, SHA-512 and their session
|
| 7 |
+
variants, as well as both 'auth' and 'auth-int' quality of protection (qop) options.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import hashlib
|
| 11 |
+
import os
|
| 12 |
+
import re
|
| 13 |
+
import sys
|
| 14 |
+
import time
|
| 15 |
+
from typing import (
|
| 16 |
+
Callable,
|
| 17 |
+
Dict,
|
| 18 |
+
Final,
|
| 19 |
+
FrozenSet,
|
| 20 |
+
List,
|
| 21 |
+
Literal,
|
| 22 |
+
Tuple,
|
| 23 |
+
TypedDict,
|
| 24 |
+
Union,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
from yarl import URL
|
| 28 |
+
|
| 29 |
+
from . import hdrs
|
| 30 |
+
from .client_exceptions import ClientError
|
| 31 |
+
from .client_middlewares import ClientHandlerType
|
| 32 |
+
from .client_reqrep import ClientRequest, ClientResponse
|
| 33 |
+
from .payload import Payload
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class DigestAuthChallenge(TypedDict, total=False):
|
| 37 |
+
realm: str
|
| 38 |
+
nonce: str
|
| 39 |
+
qop: str
|
| 40 |
+
algorithm: str
|
| 41 |
+
opaque: str
|
| 42 |
+
domain: str
|
| 43 |
+
stale: str
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
DigestFunctions: Dict[str, Callable[[bytes], "hashlib._Hash"]] = {
|
| 47 |
+
"MD5": hashlib.md5,
|
| 48 |
+
"MD5-SESS": hashlib.md5,
|
| 49 |
+
"SHA": hashlib.sha1,
|
| 50 |
+
"SHA-SESS": hashlib.sha1,
|
| 51 |
+
"SHA256": hashlib.sha256,
|
| 52 |
+
"SHA256-SESS": hashlib.sha256,
|
| 53 |
+
"SHA-256": hashlib.sha256,
|
| 54 |
+
"SHA-256-SESS": hashlib.sha256,
|
| 55 |
+
"SHA512": hashlib.sha512,
|
| 56 |
+
"SHA512-SESS": hashlib.sha512,
|
| 57 |
+
"SHA-512": hashlib.sha512,
|
| 58 |
+
"SHA-512-SESS": hashlib.sha512,
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# Compile the regex pattern once at module level for performance
|
| 63 |
+
_HEADER_PAIRS_PATTERN = re.compile(
|
| 64 |
+
r'(?:^|\s|,\s*)(\w+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))'
|
| 65 |
+
if sys.version_info < (3, 11)
|
| 66 |
+
else r'(?:^|\s|,\s*)((?>\w+))\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))'
|
| 67 |
+
# +------------|--------|--|-|-|--|----|------|----|--||-----|-> Match valid start/sep
|
| 68 |
+
# +--------|--|-|-|--|----|------|----|--||-----|-> alphanumeric key (atomic
|
| 69 |
+
# | | | | | | | | || | group reduces backtracking)
|
| 70 |
+
# +--|-|-|--|----|------|----|--||-----|-> maybe whitespace
|
| 71 |
+
# | | | | | | | || |
|
| 72 |
+
# +-|-|--|----|------|----|--||-----|-> = (delimiter)
|
| 73 |
+
# +-|--|----|------|----|--||-----|-> maybe whitespace
|
| 74 |
+
# | | | | | || |
|
| 75 |
+
# +--|----|------|----|--||-----|-> group quoted or unquoted
|
| 76 |
+
# | | | | || |
|
| 77 |
+
# +----|------|----|--||-----|-> if quoted...
|
| 78 |
+
# +------|----|--||-----|-> anything but " or \
|
| 79 |
+
# +----|--||-----|-> escaped characters allowed
|
| 80 |
+
# +--||-----|-> or can be empty string
|
| 81 |
+
# || |
|
| 82 |
+
# +|-----|-> if unquoted...
|
| 83 |
+
# +-----|-> anything but , or <space>
|
| 84 |
+
# +-> at least one char req'd
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# RFC 7616: Challenge parameters to extract
|
| 89 |
+
CHALLENGE_FIELDS: Final[
|
| 90 |
+
Tuple[
|
| 91 |
+
Literal["realm", "nonce", "qop", "algorithm", "opaque", "domain", "stale"], ...
|
| 92 |
+
]
|
| 93 |
+
] = (
|
| 94 |
+
"realm",
|
| 95 |
+
"nonce",
|
| 96 |
+
"qop",
|
| 97 |
+
"algorithm",
|
| 98 |
+
"opaque",
|
| 99 |
+
"domain",
|
| 100 |
+
"stale",
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
# Supported digest authentication algorithms
|
| 104 |
+
# Use a tuple of sorted keys for predictable documentation and error messages
|
| 105 |
+
SUPPORTED_ALGORITHMS: Final[Tuple[str, ...]] = tuple(sorted(DigestFunctions.keys()))
|
| 106 |
+
|
| 107 |
+
# RFC 7616: Fields that require quoting in the Digest auth header
|
| 108 |
+
# These fields must be enclosed in double quotes in the Authorization header.
|
| 109 |
+
# Algorithm, qop, and nc are never quoted per RFC specifications.
|
| 110 |
+
# This frozen set is used by the template-based header construction to
|
| 111 |
+
# automatically determine which fields need quotes.
|
| 112 |
+
QUOTED_AUTH_FIELDS: Final[FrozenSet[str]] = frozenset(
|
| 113 |
+
{"username", "realm", "nonce", "uri", "response", "opaque", "cnonce"}
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def escape_quotes(value: str) -> str:
|
| 118 |
+
"""Escape double quotes for HTTP header values."""
|
| 119 |
+
return value.replace('"', '\\"')
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def unescape_quotes(value: str) -> str:
|
| 123 |
+
"""Unescape double quotes in HTTP header values."""
|
| 124 |
+
return value.replace('\\"', '"')
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def parse_header_pairs(header: str) -> Dict[str, str]:
|
| 128 |
+
"""
|
| 129 |
+
Parse key-value pairs from WWW-Authenticate or similar HTTP headers.
|
| 130 |
+
|
| 131 |
+
This function handles the complex format of WWW-Authenticate header values,
|
| 132 |
+
supporting both quoted and unquoted values, proper handling of commas in
|
| 133 |
+
quoted values, and whitespace variations per RFC 7616.
|
| 134 |
+
|
| 135 |
+
Examples of supported formats:
|
| 136 |
+
- key1="value1", key2=value2
|
| 137 |
+
- key1 = "value1" , key2="value, with, commas"
|
| 138 |
+
- key1=value1,key2="value2"
|
| 139 |
+
- realm="example.com", nonce="12345", qop="auth"
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
header: The header value string to parse
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
Dictionary mapping parameter names to their values
|
| 146 |
+
"""
|
| 147 |
+
return {
|
| 148 |
+
stripped_key: unescape_quotes(quoted_val) if quoted_val else unquoted_val
|
| 149 |
+
for key, quoted_val, unquoted_val in _HEADER_PAIRS_PATTERN.findall(header)
|
| 150 |
+
if (stripped_key := key.strip())
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
class DigestAuthMiddleware:
|
| 155 |
+
"""
|
| 156 |
+
HTTP digest authentication middleware for aiohttp client.
|
| 157 |
+
|
| 158 |
+
This middleware intercepts 401 Unauthorized responses containing a Digest
|
| 159 |
+
authentication challenge, calculates the appropriate digest credentials,
|
| 160 |
+
and automatically retries the request with the proper Authorization header.
|
| 161 |
+
|
| 162 |
+
Features:
|
| 163 |
+
- Handles all aspects of Digest authentication handshake automatically
|
| 164 |
+
- Supports all standard hash algorithms:
|
| 165 |
+
- MD5, MD5-SESS
|
| 166 |
+
- SHA, SHA-SESS
|
| 167 |
+
- SHA256, SHA256-SESS, SHA-256, SHA-256-SESS
|
| 168 |
+
- SHA512, SHA512-SESS, SHA-512, SHA-512-SESS
|
| 169 |
+
- Supports 'auth' and 'auth-int' quality of protection modes
|
| 170 |
+
- Properly handles quoted strings and parameter parsing
|
| 171 |
+
- Includes replay attack protection with client nonce count tracking
|
| 172 |
+
- Supports preemptive authentication per RFC 7616 Section 3.6
|
| 173 |
+
|
| 174 |
+
Standards compliance:
|
| 175 |
+
- RFC 7616: HTTP Digest Access Authentication (primary reference)
|
| 176 |
+
- RFC 2617: HTTP Authentication (deprecated by RFC 7616)
|
| 177 |
+
- RFC 1945: Section 11.1 (username restrictions)
|
| 178 |
+
|
| 179 |
+
Implementation notes:
|
| 180 |
+
The core digest calculation is inspired by the implementation in
|
| 181 |
+
https://github.com/requests/requests/blob/v2.18.4/requests/auth.py
|
| 182 |
+
with added support for modern digest auth features and error handling.
|
| 183 |
+
"""
|
| 184 |
+
|
| 185 |
+
def __init__(
|
| 186 |
+
self,
|
| 187 |
+
login: str,
|
| 188 |
+
password: str,
|
| 189 |
+
preemptive: bool = True,
|
| 190 |
+
) -> None:
|
| 191 |
+
if login is None:
|
| 192 |
+
raise ValueError("None is not allowed as login value")
|
| 193 |
+
|
| 194 |
+
if password is None:
|
| 195 |
+
raise ValueError("None is not allowed as password value")
|
| 196 |
+
|
| 197 |
+
if ":" in login:
|
| 198 |
+
raise ValueError('A ":" is not allowed in username (RFC 1945#section-11.1)')
|
| 199 |
+
|
| 200 |
+
self._login_str: Final[str] = login
|
| 201 |
+
self._login_bytes: Final[bytes] = login.encode("utf-8")
|
| 202 |
+
self._password_bytes: Final[bytes] = password.encode("utf-8")
|
| 203 |
+
|
| 204 |
+
self._last_nonce_bytes = b""
|
| 205 |
+
self._nonce_count = 0
|
| 206 |
+
self._challenge: DigestAuthChallenge = {}
|
| 207 |
+
self._preemptive: bool = preemptive
|
| 208 |
+
# Set of URLs defining the protection space
|
| 209 |
+
self._protection_space: List[str] = []
|
| 210 |
+
|
| 211 |
+
async def _encode(
|
| 212 |
+
self, method: str, url: URL, body: Union[Payload, Literal[b""]]
|
| 213 |
+
) -> str:
|
| 214 |
+
"""
|
| 215 |
+
Build digest authorization header for the current challenge.
|
| 216 |
+
|
| 217 |
+
Args:
|
| 218 |
+
method: The HTTP method (GET, POST, etc.)
|
| 219 |
+
url: The request URL
|
| 220 |
+
body: The request body (used for qop=auth-int)
|
| 221 |
+
|
| 222 |
+
Returns:
|
| 223 |
+
A fully formatted Digest authorization header string
|
| 224 |
+
|
| 225 |
+
Raises:
|
| 226 |
+
ClientError: If the challenge is missing required parameters or
|
| 227 |
+
contains unsupported values
|
| 228 |
+
|
| 229 |
+
"""
|
| 230 |
+
challenge = self._challenge
|
| 231 |
+
if "realm" not in challenge:
|
| 232 |
+
raise ClientError(
|
| 233 |
+
"Malformed Digest auth challenge: Missing 'realm' parameter"
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
if "nonce" not in challenge:
|
| 237 |
+
raise ClientError(
|
| 238 |
+
"Malformed Digest auth challenge: Missing 'nonce' parameter"
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
# Empty realm values are allowed per RFC 7616 (SHOULD, not MUST, contain host name)
|
| 242 |
+
realm = challenge["realm"]
|
| 243 |
+
nonce = challenge["nonce"]
|
| 244 |
+
|
| 245 |
+
# Empty nonce values are not allowed as they are security-critical for replay protection
|
| 246 |
+
if not nonce:
|
| 247 |
+
raise ClientError(
|
| 248 |
+
"Security issue: Digest auth challenge contains empty 'nonce' value"
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
qop_raw = challenge.get("qop", "")
|
| 252 |
+
# Preserve original algorithm case for response while using uppercase for processing
|
| 253 |
+
algorithm_original = challenge.get("algorithm", "MD5")
|
| 254 |
+
algorithm = algorithm_original.upper()
|
| 255 |
+
opaque = challenge.get("opaque", "")
|
| 256 |
+
|
| 257 |
+
# Convert string values to bytes once
|
| 258 |
+
nonce_bytes = nonce.encode("utf-8")
|
| 259 |
+
realm_bytes = realm.encode("utf-8")
|
| 260 |
+
path = URL(url).path_qs
|
| 261 |
+
|
| 262 |
+
# Process QoP
|
| 263 |
+
qop = ""
|
| 264 |
+
qop_bytes = b""
|
| 265 |
+
if qop_raw:
|
| 266 |
+
valid_qops = {"auth", "auth-int"}.intersection(
|
| 267 |
+
{q.strip() for q in qop_raw.split(",") if q.strip()}
|
| 268 |
+
)
|
| 269 |
+
if not valid_qops:
|
| 270 |
+
raise ClientError(
|
| 271 |
+
f"Digest auth error: Unsupported Quality of Protection (qop) value(s): {qop_raw}"
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
qop = "auth-int" if "auth-int" in valid_qops else "auth"
|
| 275 |
+
qop_bytes = qop.encode("utf-8")
|
| 276 |
+
|
| 277 |
+
if algorithm not in DigestFunctions:
|
| 278 |
+
raise ClientError(
|
| 279 |
+
f"Digest auth error: Unsupported hash algorithm: {algorithm}. "
|
| 280 |
+
f"Supported algorithms: {', '.join(SUPPORTED_ALGORITHMS)}"
|
| 281 |
+
)
|
| 282 |
+
hash_fn: Final = DigestFunctions[algorithm]
|
| 283 |
+
|
| 284 |
+
def H(x: bytes) -> bytes:
|
| 285 |
+
"""RFC 7616 Section 3: Hash function H(data) = hex(hash(data))."""
|
| 286 |
+
return hash_fn(x).hexdigest().encode()
|
| 287 |
+
|
| 288 |
+
def KD(s: bytes, d: bytes) -> bytes:
|
| 289 |
+
"""RFC 7616 Section 3: KD(secret, data) = H(concat(secret, ":", data))."""
|
| 290 |
+
return H(b":".join((s, d)))
|
| 291 |
+
|
| 292 |
+
# Calculate A1 and A2
|
| 293 |
+
A1 = b":".join((self._login_bytes, realm_bytes, self._password_bytes))
|
| 294 |
+
A2 = f"{method.upper()}:{path}".encode()
|
| 295 |
+
if qop == "auth-int":
|
| 296 |
+
if isinstance(body, Payload): # will always be empty bytes unless Payload
|
| 297 |
+
entity_bytes = await body.as_bytes() # Get bytes from Payload
|
| 298 |
+
else:
|
| 299 |
+
entity_bytes = body
|
| 300 |
+
entity_hash = H(entity_bytes)
|
| 301 |
+
A2 = b":".join((A2, entity_hash))
|
| 302 |
+
|
| 303 |
+
HA1 = H(A1)
|
| 304 |
+
HA2 = H(A2)
|
| 305 |
+
|
| 306 |
+
# Nonce count handling
|
| 307 |
+
if nonce_bytes == self._last_nonce_bytes:
|
| 308 |
+
self._nonce_count += 1
|
| 309 |
+
else:
|
| 310 |
+
self._nonce_count = 1
|
| 311 |
+
|
| 312 |
+
self._last_nonce_bytes = nonce_bytes
|
| 313 |
+
ncvalue = f"{self._nonce_count:08x}"
|
| 314 |
+
ncvalue_bytes = ncvalue.encode("utf-8")
|
| 315 |
+
|
| 316 |
+
# Generate client nonce
|
| 317 |
+
cnonce = hashlib.sha1(
|
| 318 |
+
b"".join(
|
| 319 |
+
[
|
| 320 |
+
str(self._nonce_count).encode("utf-8"),
|
| 321 |
+
nonce_bytes,
|
| 322 |
+
time.ctime().encode("utf-8"),
|
| 323 |
+
os.urandom(8),
|
| 324 |
+
]
|
| 325 |
+
)
|
| 326 |
+
).hexdigest()[:16]
|
| 327 |
+
cnonce_bytes = cnonce.encode("utf-8")
|
| 328 |
+
|
| 329 |
+
# Special handling for session-based algorithms
|
| 330 |
+
if algorithm.upper().endswith("-SESS"):
|
| 331 |
+
HA1 = H(b":".join((HA1, nonce_bytes, cnonce_bytes)))
|
| 332 |
+
|
| 333 |
+
# Calculate the response digest
|
| 334 |
+
if qop:
|
| 335 |
+
noncebit = b":".join(
|
| 336 |
+
(nonce_bytes, ncvalue_bytes, cnonce_bytes, qop_bytes, HA2)
|
| 337 |
+
)
|
| 338 |
+
response_digest = KD(HA1, noncebit)
|
| 339 |
+
else:
|
| 340 |
+
response_digest = KD(HA1, b":".join((nonce_bytes, HA2)))
|
| 341 |
+
|
| 342 |
+
# Define a dict mapping of header fields to their values
|
| 343 |
+
# Group fields into always-present, optional, and qop-dependent
|
| 344 |
+
header_fields = {
|
| 345 |
+
# Always present fields
|
| 346 |
+
"username": escape_quotes(self._login_str),
|
| 347 |
+
"realm": escape_quotes(realm),
|
| 348 |
+
"nonce": escape_quotes(nonce),
|
| 349 |
+
"uri": path,
|
| 350 |
+
"response": response_digest.decode(),
|
| 351 |
+
"algorithm": algorithm_original,
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
# Optional fields
|
| 355 |
+
if opaque:
|
| 356 |
+
header_fields["opaque"] = escape_quotes(opaque)
|
| 357 |
+
|
| 358 |
+
# QoP-dependent fields
|
| 359 |
+
if qop:
|
| 360 |
+
header_fields["qop"] = qop
|
| 361 |
+
header_fields["nc"] = ncvalue
|
| 362 |
+
header_fields["cnonce"] = cnonce
|
| 363 |
+
|
| 364 |
+
# Build header using templates for each field type
|
| 365 |
+
pairs: List[str] = []
|
| 366 |
+
for field, value in header_fields.items():
|
| 367 |
+
if field in QUOTED_AUTH_FIELDS:
|
| 368 |
+
pairs.append(f'{field}="{value}"')
|
| 369 |
+
else:
|
| 370 |
+
pairs.append(f"{field}={value}")
|
| 371 |
+
|
| 372 |
+
return f"Digest {', '.join(pairs)}"
|
| 373 |
+
|
| 374 |
+
def _in_protection_space(self, url: URL) -> bool:
|
| 375 |
+
"""
|
| 376 |
+
Check if the given URL is within the current protection space.
|
| 377 |
+
|
| 378 |
+
According to RFC 7616, a URI is in the protection space if any URI
|
| 379 |
+
in the protection space is a prefix of it (after both have been made absolute).
|
| 380 |
+
"""
|
| 381 |
+
request_str = str(url)
|
| 382 |
+
for space_str in self._protection_space:
|
| 383 |
+
# Check if request starts with space URL
|
| 384 |
+
if not request_str.startswith(space_str):
|
| 385 |
+
continue
|
| 386 |
+
# Exact match or space ends with / (proper directory prefix)
|
| 387 |
+
if len(request_str) == len(space_str) or space_str[-1] == "/":
|
| 388 |
+
return True
|
| 389 |
+
# Check next char is / to ensure proper path boundary
|
| 390 |
+
if request_str[len(space_str)] == "/":
|
| 391 |
+
return True
|
| 392 |
+
return False
|
| 393 |
+
|
| 394 |
+
def _authenticate(self, response: ClientResponse) -> bool:
|
| 395 |
+
"""
|
| 396 |
+
Takes the given response and tries digest-auth, if needed.
|
| 397 |
+
|
| 398 |
+
Returns true if the original request must be resent.
|
| 399 |
+
"""
|
| 400 |
+
if response.status != 401:
|
| 401 |
+
return False
|
| 402 |
+
|
| 403 |
+
auth_header = response.headers.get("www-authenticate", "")
|
| 404 |
+
if not auth_header:
|
| 405 |
+
return False # No authentication header present
|
| 406 |
+
|
| 407 |
+
method, sep, headers = auth_header.partition(" ")
|
| 408 |
+
if not sep:
|
| 409 |
+
# No space found in www-authenticate header
|
| 410 |
+
return False # Malformed auth header, missing scheme separator
|
| 411 |
+
|
| 412 |
+
if method.lower() != "digest":
|
| 413 |
+
# Not a digest auth challenge (could be Basic, Bearer, etc.)
|
| 414 |
+
return False
|
| 415 |
+
|
| 416 |
+
if not headers:
|
| 417 |
+
# We have a digest scheme but no parameters
|
| 418 |
+
return False # Malformed digest header, missing parameters
|
| 419 |
+
|
| 420 |
+
# We have a digest auth header with content
|
| 421 |
+
if not (header_pairs := parse_header_pairs(headers)):
|
| 422 |
+
# Failed to parse any key-value pairs
|
| 423 |
+
return False # Malformed digest header, no valid parameters
|
| 424 |
+
|
| 425 |
+
# Extract challenge parameters
|
| 426 |
+
self._challenge = {}
|
| 427 |
+
for field in CHALLENGE_FIELDS:
|
| 428 |
+
if value := header_pairs.get(field):
|
| 429 |
+
self._challenge[field] = value
|
| 430 |
+
|
| 431 |
+
# Update protection space based on domain parameter or default to origin
|
| 432 |
+
origin = response.url.origin()
|
| 433 |
+
|
| 434 |
+
if domain := self._challenge.get("domain"):
|
| 435 |
+
# Parse space-separated list of URIs
|
| 436 |
+
self._protection_space = []
|
| 437 |
+
for uri in domain.split():
|
| 438 |
+
# Remove quotes if present
|
| 439 |
+
uri = uri.strip('"')
|
| 440 |
+
if uri.startswith("/"):
|
| 441 |
+
# Path-absolute, relative to origin
|
| 442 |
+
self._protection_space.append(str(origin.join(URL(uri))))
|
| 443 |
+
else:
|
| 444 |
+
# Absolute URI
|
| 445 |
+
self._protection_space.append(str(URL(uri)))
|
| 446 |
+
else:
|
| 447 |
+
# No domain specified, protection space is entire origin
|
| 448 |
+
self._protection_space = [str(origin)]
|
| 449 |
+
|
| 450 |
+
# Return True only if we found at least one challenge parameter
|
| 451 |
+
return bool(self._challenge)
|
| 452 |
+
|
| 453 |
+
async def __call__(
|
| 454 |
+
self, request: ClientRequest, handler: ClientHandlerType
|
| 455 |
+
) -> ClientResponse:
|
| 456 |
+
"""Run the digest auth middleware."""
|
| 457 |
+
response = None
|
| 458 |
+
for retry_count in range(2):
|
| 459 |
+
# Apply authorization header if:
|
| 460 |
+
# 1. This is a retry after 401 (retry_count > 0), OR
|
| 461 |
+
# 2. Preemptive auth is enabled AND we have a challenge AND the URL is in protection space
|
| 462 |
+
if retry_count > 0 or (
|
| 463 |
+
self._preemptive
|
| 464 |
+
and self._challenge
|
| 465 |
+
and self._in_protection_space(request.url)
|
| 466 |
+
):
|
| 467 |
+
request.headers[hdrs.AUTHORIZATION] = await self._encode(
|
| 468 |
+
request.method, request.url, request.body
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
# Send the request
|
| 472 |
+
response = await handler(request)
|
| 473 |
+
|
| 474 |
+
# Check if we need to authenticate
|
| 475 |
+
if not self._authenticate(response):
|
| 476 |
+
break
|
| 477 |
+
|
| 478 |
+
# At this point, response is guaranteed to be defined
|
| 479 |
+
assert response is not None
|
| 480 |
+
return response
|
aiohttp/client_reqrep.py
ADDED
|
@@ -0,0 +1,1536 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import codecs
|
| 3 |
+
import contextlib
|
| 4 |
+
import functools
|
| 5 |
+
import io
|
| 6 |
+
import re
|
| 7 |
+
import sys
|
| 8 |
+
import traceback
|
| 9 |
+
import warnings
|
| 10 |
+
from collections.abc import Mapping
|
| 11 |
+
from hashlib import md5, sha1, sha256
|
| 12 |
+
from http.cookies import Morsel, SimpleCookie
|
| 13 |
+
from types import MappingProxyType, TracebackType
|
| 14 |
+
from typing import (
|
| 15 |
+
TYPE_CHECKING,
|
| 16 |
+
Any,
|
| 17 |
+
Callable,
|
| 18 |
+
Dict,
|
| 19 |
+
Iterable,
|
| 20 |
+
List,
|
| 21 |
+
Literal,
|
| 22 |
+
NamedTuple,
|
| 23 |
+
Optional,
|
| 24 |
+
Tuple,
|
| 25 |
+
Type,
|
| 26 |
+
Union,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
import attr
|
| 30 |
+
from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy
|
| 31 |
+
from yarl import URL
|
| 32 |
+
|
| 33 |
+
from . import hdrs, helpers, http, multipart, payload
|
| 34 |
+
from ._cookie_helpers import (
|
| 35 |
+
parse_cookie_header,
|
| 36 |
+
parse_set_cookie_headers,
|
| 37 |
+
preserve_morsel_with_coded_value,
|
| 38 |
+
)
|
| 39 |
+
from .abc import AbstractStreamWriter
|
| 40 |
+
from .client_exceptions import (
|
| 41 |
+
ClientConnectionError,
|
| 42 |
+
ClientOSError,
|
| 43 |
+
ClientResponseError,
|
| 44 |
+
ContentTypeError,
|
| 45 |
+
InvalidURL,
|
| 46 |
+
ServerFingerprintMismatch,
|
| 47 |
+
)
|
| 48 |
+
from .compression_utils import HAS_BROTLI, HAS_ZSTD
|
| 49 |
+
from .formdata import FormData
|
| 50 |
+
from .helpers import (
|
| 51 |
+
_SENTINEL,
|
| 52 |
+
BaseTimerContext,
|
| 53 |
+
BasicAuth,
|
| 54 |
+
HeadersMixin,
|
| 55 |
+
TimerNoop,
|
| 56 |
+
noop,
|
| 57 |
+
reify,
|
| 58 |
+
sentinel,
|
| 59 |
+
set_exception,
|
| 60 |
+
set_result,
|
| 61 |
+
)
|
| 62 |
+
from .http import (
|
| 63 |
+
SERVER_SOFTWARE,
|
| 64 |
+
HttpVersion,
|
| 65 |
+
HttpVersion10,
|
| 66 |
+
HttpVersion11,
|
| 67 |
+
StreamWriter,
|
| 68 |
+
)
|
| 69 |
+
from .streams import StreamReader
|
| 70 |
+
from .typedefs import (
|
| 71 |
+
DEFAULT_JSON_DECODER,
|
| 72 |
+
JSONDecoder,
|
| 73 |
+
LooseCookies,
|
| 74 |
+
LooseHeaders,
|
| 75 |
+
Query,
|
| 76 |
+
RawHeaders,
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
if TYPE_CHECKING:
|
| 80 |
+
import ssl
|
| 81 |
+
from ssl import SSLContext
|
| 82 |
+
else:
|
| 83 |
+
try:
|
| 84 |
+
import ssl
|
| 85 |
+
from ssl import SSLContext
|
| 86 |
+
except ImportError: # pragma: no cover
|
| 87 |
+
ssl = None # type: ignore[assignment]
|
| 88 |
+
SSLContext = object # type: ignore[misc,assignment]
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
__all__ = ("ClientRequest", "ClientResponse", "RequestInfo", "Fingerprint")
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
if TYPE_CHECKING:
|
| 95 |
+
from .client import ClientSession
|
| 96 |
+
from .connector import Connection
|
| 97 |
+
from .tracing import Trace
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
_CONNECTION_CLOSED_EXCEPTION = ClientConnectionError("Connection closed")
|
| 101 |
+
_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]")
|
| 102 |
+
json_re = re.compile(r"^application/(?:[\w.+-]+?\+)?json")
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _gen_default_accept_encoding() -> str:
|
| 106 |
+
encodings = [
|
| 107 |
+
"gzip",
|
| 108 |
+
"deflate",
|
| 109 |
+
]
|
| 110 |
+
if HAS_BROTLI:
|
| 111 |
+
encodings.append("br")
|
| 112 |
+
if HAS_ZSTD:
|
| 113 |
+
encodings.append("zstd")
|
| 114 |
+
return ", ".join(encodings)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
@attr.s(auto_attribs=True, frozen=True, slots=True)
|
| 118 |
+
class ContentDisposition:
|
| 119 |
+
type: Optional[str]
|
| 120 |
+
parameters: "MappingProxyType[str, str]"
|
| 121 |
+
filename: Optional[str]
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class _RequestInfo(NamedTuple):
|
| 125 |
+
url: URL
|
| 126 |
+
method: str
|
| 127 |
+
headers: "CIMultiDictProxy[str]"
|
| 128 |
+
real_url: URL
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
class RequestInfo(_RequestInfo):
|
| 132 |
+
|
| 133 |
+
def __new__(
|
| 134 |
+
cls,
|
| 135 |
+
url: URL,
|
| 136 |
+
method: str,
|
| 137 |
+
headers: "CIMultiDictProxy[str]",
|
| 138 |
+
real_url: Union[URL, _SENTINEL] = sentinel,
|
| 139 |
+
) -> "RequestInfo":
|
| 140 |
+
"""Create a new RequestInfo instance.
|
| 141 |
+
|
| 142 |
+
For backwards compatibility, the real_url parameter is optional.
|
| 143 |
+
"""
|
| 144 |
+
return tuple.__new__(
|
| 145 |
+
cls, (url, method, headers, url if real_url is sentinel else real_url)
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
class Fingerprint:
|
| 150 |
+
HASHFUNC_BY_DIGESTLEN = {
|
| 151 |
+
16: md5,
|
| 152 |
+
20: sha1,
|
| 153 |
+
32: sha256,
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
def __init__(self, fingerprint: bytes) -> None:
|
| 157 |
+
digestlen = len(fingerprint)
|
| 158 |
+
hashfunc = self.HASHFUNC_BY_DIGESTLEN.get(digestlen)
|
| 159 |
+
if not hashfunc:
|
| 160 |
+
raise ValueError("fingerprint has invalid length")
|
| 161 |
+
elif hashfunc is md5 or hashfunc is sha1:
|
| 162 |
+
raise ValueError("md5 and sha1 are insecure and not supported. Use sha256.")
|
| 163 |
+
self._hashfunc = hashfunc
|
| 164 |
+
self._fingerprint = fingerprint
|
| 165 |
+
|
| 166 |
+
@property
|
| 167 |
+
def fingerprint(self) -> bytes:
|
| 168 |
+
return self._fingerprint
|
| 169 |
+
|
| 170 |
+
def check(self, transport: asyncio.Transport) -> None:
|
| 171 |
+
if not transport.get_extra_info("sslcontext"):
|
| 172 |
+
return
|
| 173 |
+
sslobj = transport.get_extra_info("ssl_object")
|
| 174 |
+
cert = sslobj.getpeercert(binary_form=True)
|
| 175 |
+
got = self._hashfunc(cert).digest()
|
| 176 |
+
if got != self._fingerprint:
|
| 177 |
+
host, port, *_ = transport.get_extra_info("peername")
|
| 178 |
+
raise ServerFingerprintMismatch(self._fingerprint, got, host, port)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
if ssl is not None:
|
| 182 |
+
SSL_ALLOWED_TYPES = (ssl.SSLContext, bool, Fingerprint, type(None))
|
| 183 |
+
else: # pragma: no cover
|
| 184 |
+
SSL_ALLOWED_TYPES = (bool, type(None))
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def _merge_ssl_params(
|
| 188 |
+
ssl: Union["SSLContext", bool, Fingerprint],
|
| 189 |
+
verify_ssl: Optional[bool],
|
| 190 |
+
ssl_context: Optional["SSLContext"],
|
| 191 |
+
fingerprint: Optional[bytes],
|
| 192 |
+
) -> Union["SSLContext", bool, Fingerprint]:
|
| 193 |
+
if ssl is None:
|
| 194 |
+
ssl = True # Double check for backwards compatibility
|
| 195 |
+
if verify_ssl is not None and not verify_ssl:
|
| 196 |
+
warnings.warn(
|
| 197 |
+
"verify_ssl is deprecated, use ssl=False instead",
|
| 198 |
+
DeprecationWarning,
|
| 199 |
+
stacklevel=3,
|
| 200 |
+
)
|
| 201 |
+
if ssl is not True:
|
| 202 |
+
raise ValueError(
|
| 203 |
+
"verify_ssl, ssl_context, fingerprint and ssl "
|
| 204 |
+
"parameters are mutually exclusive"
|
| 205 |
+
)
|
| 206 |
+
else:
|
| 207 |
+
ssl = False
|
| 208 |
+
if ssl_context is not None:
|
| 209 |
+
warnings.warn(
|
| 210 |
+
"ssl_context is deprecated, use ssl=context instead",
|
| 211 |
+
DeprecationWarning,
|
| 212 |
+
stacklevel=3,
|
| 213 |
+
)
|
| 214 |
+
if ssl is not True:
|
| 215 |
+
raise ValueError(
|
| 216 |
+
"verify_ssl, ssl_context, fingerprint and ssl "
|
| 217 |
+
"parameters are mutually exclusive"
|
| 218 |
+
)
|
| 219 |
+
else:
|
| 220 |
+
ssl = ssl_context
|
| 221 |
+
if fingerprint is not None:
|
| 222 |
+
warnings.warn(
|
| 223 |
+
"fingerprint is deprecated, use ssl=Fingerprint(fingerprint) instead",
|
| 224 |
+
DeprecationWarning,
|
| 225 |
+
stacklevel=3,
|
| 226 |
+
)
|
| 227 |
+
if ssl is not True:
|
| 228 |
+
raise ValueError(
|
| 229 |
+
"verify_ssl, ssl_context, fingerprint and ssl "
|
| 230 |
+
"parameters are mutually exclusive"
|
| 231 |
+
)
|
| 232 |
+
else:
|
| 233 |
+
ssl = Fingerprint(fingerprint)
|
| 234 |
+
if not isinstance(ssl, SSL_ALLOWED_TYPES):
|
| 235 |
+
raise TypeError(
|
| 236 |
+
"ssl should be SSLContext, bool, Fingerprint or None, "
|
| 237 |
+
"got {!r} instead.".format(ssl)
|
| 238 |
+
)
|
| 239 |
+
return ssl
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
_SSL_SCHEMES = frozenset(("https", "wss"))
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
# ConnectionKey is a NamedTuple because it is used as a key in a dict
|
| 246 |
+
# and a set in the connector. Since a NamedTuple is a tuple it uses
|
| 247 |
+
# the fast native tuple __hash__ and __eq__ implementation in CPython.
|
| 248 |
+
class ConnectionKey(NamedTuple):
|
| 249 |
+
# the key should contain an information about used proxy / TLS
|
| 250 |
+
# to prevent reusing wrong connections from a pool
|
| 251 |
+
host: str
|
| 252 |
+
port: Optional[int]
|
| 253 |
+
is_ssl: bool
|
| 254 |
+
ssl: Union[SSLContext, bool, Fingerprint]
|
| 255 |
+
proxy: Optional[URL]
|
| 256 |
+
proxy_auth: Optional[BasicAuth]
|
| 257 |
+
proxy_headers_hash: Optional[int] # hash(CIMultiDict)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def _is_expected_content_type(
|
| 261 |
+
response_content_type: str, expected_content_type: str
|
| 262 |
+
) -> bool:
|
| 263 |
+
if expected_content_type == "application/json":
|
| 264 |
+
return json_re.match(response_content_type) is not None
|
| 265 |
+
return expected_content_type in response_content_type
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def _warn_if_unclosed_payload(payload: payload.Payload, stacklevel: int = 2) -> None:
|
| 269 |
+
"""Warn if the payload is not closed.
|
| 270 |
+
|
| 271 |
+
Callers must check that the body is a Payload before calling this method.
|
| 272 |
+
|
| 273 |
+
Args:
|
| 274 |
+
payload: The payload to check
|
| 275 |
+
stacklevel: Stack level for the warning (default 2 for direct callers)
|
| 276 |
+
"""
|
| 277 |
+
if not payload.autoclose and not payload.consumed:
|
| 278 |
+
warnings.warn(
|
| 279 |
+
"The previous request body contains unclosed resources. "
|
| 280 |
+
"Use await request.update_body() instead of setting request.body "
|
| 281 |
+
"directly to properly close resources and avoid leaks.",
|
| 282 |
+
ResourceWarning,
|
| 283 |
+
stacklevel=stacklevel,
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
class ClientResponse(HeadersMixin):
|
| 288 |
+
|
| 289 |
+
# Some of these attributes are None when created,
|
| 290 |
+
# but will be set by the start() method.
|
| 291 |
+
# As the end user will likely never see the None values, we cheat the types below.
|
| 292 |
+
# from the Status-Line of the response
|
| 293 |
+
version: Optional[HttpVersion] = None # HTTP-Version
|
| 294 |
+
status: int = None # type: ignore[assignment] # Status-Code
|
| 295 |
+
reason: Optional[str] = None # Reason-Phrase
|
| 296 |
+
|
| 297 |
+
content: StreamReader = None # type: ignore[assignment] # Payload stream
|
| 298 |
+
_body: Optional[bytes] = None
|
| 299 |
+
_headers: CIMultiDictProxy[str] = None # type: ignore[assignment]
|
| 300 |
+
_history: Tuple["ClientResponse", ...] = ()
|
| 301 |
+
_raw_headers: RawHeaders = None # type: ignore[assignment]
|
| 302 |
+
|
| 303 |
+
_connection: Optional["Connection"] = None # current connection
|
| 304 |
+
_cookies: Optional[SimpleCookie] = None
|
| 305 |
+
_raw_cookie_headers: Optional[Tuple[str, ...]] = None
|
| 306 |
+
_continue: Optional["asyncio.Future[bool]"] = None
|
| 307 |
+
_source_traceback: Optional[traceback.StackSummary] = None
|
| 308 |
+
_session: Optional["ClientSession"] = None
|
| 309 |
+
# set up by ClientRequest after ClientResponse object creation
|
| 310 |
+
# post-init stage allows to not change ctor signature
|
| 311 |
+
_closed = True # to allow __del__ for non-initialized properly response
|
| 312 |
+
_released = False
|
| 313 |
+
_in_context = False
|
| 314 |
+
|
| 315 |
+
_resolve_charset: Callable[["ClientResponse", bytes], str] = lambda *_: "utf-8"
|
| 316 |
+
|
| 317 |
+
__writer: Optional["asyncio.Task[None]"] = None
|
| 318 |
+
|
| 319 |
+
def __init__(
|
| 320 |
+
self,
|
| 321 |
+
method: str,
|
| 322 |
+
url: URL,
|
| 323 |
+
*,
|
| 324 |
+
writer: "Optional[asyncio.Task[None]]",
|
| 325 |
+
continue100: Optional["asyncio.Future[bool]"],
|
| 326 |
+
timer: BaseTimerContext,
|
| 327 |
+
request_info: RequestInfo,
|
| 328 |
+
traces: List["Trace"],
|
| 329 |
+
loop: asyncio.AbstractEventLoop,
|
| 330 |
+
session: "ClientSession",
|
| 331 |
+
) -> None:
|
| 332 |
+
# URL forbids subclasses, so a simple type check is enough.
|
| 333 |
+
assert type(url) is URL
|
| 334 |
+
|
| 335 |
+
self.method = method
|
| 336 |
+
|
| 337 |
+
self._real_url = url
|
| 338 |
+
self._url = url.with_fragment(None) if url.raw_fragment else url
|
| 339 |
+
if writer is not None:
|
| 340 |
+
self._writer = writer
|
| 341 |
+
if continue100 is not None:
|
| 342 |
+
self._continue = continue100
|
| 343 |
+
self._request_info = request_info
|
| 344 |
+
self._timer = timer if timer is not None else TimerNoop()
|
| 345 |
+
self._cache: Dict[str, Any] = {}
|
| 346 |
+
self._traces = traces
|
| 347 |
+
self._loop = loop
|
| 348 |
+
# Save reference to _resolve_charset, so that get_encoding() will still
|
| 349 |
+
# work after the response has finished reading the body.
|
| 350 |
+
# TODO: Fix session=None in tests (see ClientRequest.__init__).
|
| 351 |
+
if session is not None:
|
| 352 |
+
# store a reference to session #1985
|
| 353 |
+
self._session = session
|
| 354 |
+
self._resolve_charset = session._resolve_charset
|
| 355 |
+
if loop.get_debug():
|
| 356 |
+
self._source_traceback = traceback.extract_stack(sys._getframe(1))
|
| 357 |
+
|
| 358 |
+
def __reset_writer(self, _: object = None) -> None:
|
| 359 |
+
self.__writer = None
|
| 360 |
+
|
| 361 |
+
@property
|
| 362 |
+
def _writer(self) -> Optional["asyncio.Task[None]"]:
|
| 363 |
+
"""The writer task for streaming data.
|
| 364 |
+
|
| 365 |
+
_writer is only provided for backwards compatibility
|
| 366 |
+
for subclasses that may need to access it.
|
| 367 |
+
"""
|
| 368 |
+
return self.__writer
|
| 369 |
+
|
| 370 |
+
@_writer.setter
|
| 371 |
+
def _writer(self, writer: Optional["asyncio.Task[None]"]) -> None:
|
| 372 |
+
"""Set the writer task for streaming data."""
|
| 373 |
+
if self.__writer is not None:
|
| 374 |
+
self.__writer.remove_done_callback(self.__reset_writer)
|
| 375 |
+
self.__writer = writer
|
| 376 |
+
if writer is None:
|
| 377 |
+
return
|
| 378 |
+
if writer.done():
|
| 379 |
+
# The writer is already done, so we can clear it immediately.
|
| 380 |
+
self.__writer = None
|
| 381 |
+
else:
|
| 382 |
+
writer.add_done_callback(self.__reset_writer)
|
| 383 |
+
|
| 384 |
+
@property
|
| 385 |
+
def cookies(self) -> SimpleCookie:
|
| 386 |
+
if self._cookies is None:
|
| 387 |
+
if self._raw_cookie_headers is not None:
|
| 388 |
+
# Parse cookies for response.cookies (SimpleCookie for backward compatibility)
|
| 389 |
+
cookies = SimpleCookie()
|
| 390 |
+
# Use parse_set_cookie_headers for more lenient parsing that handles
|
| 391 |
+
# malformed cookies better than SimpleCookie.load
|
| 392 |
+
cookies.update(parse_set_cookie_headers(self._raw_cookie_headers))
|
| 393 |
+
self._cookies = cookies
|
| 394 |
+
else:
|
| 395 |
+
self._cookies = SimpleCookie()
|
| 396 |
+
return self._cookies
|
| 397 |
+
|
| 398 |
+
@cookies.setter
|
| 399 |
+
def cookies(self, cookies: SimpleCookie) -> None:
|
| 400 |
+
self._cookies = cookies
|
| 401 |
+
# Generate raw cookie headers from the SimpleCookie
|
| 402 |
+
if cookies:
|
| 403 |
+
self._raw_cookie_headers = tuple(
|
| 404 |
+
morsel.OutputString() for morsel in cookies.values()
|
| 405 |
+
)
|
| 406 |
+
else:
|
| 407 |
+
self._raw_cookie_headers = None
|
| 408 |
+
|
| 409 |
+
@reify
|
| 410 |
+
def url(self) -> URL:
|
| 411 |
+
return self._url
|
| 412 |
+
|
| 413 |
+
@reify
|
| 414 |
+
def url_obj(self) -> URL:
|
| 415 |
+
warnings.warn("Deprecated, use .url #1654", DeprecationWarning, stacklevel=2)
|
| 416 |
+
return self._url
|
| 417 |
+
|
| 418 |
+
@reify
|
| 419 |
+
def real_url(self) -> URL:
|
| 420 |
+
return self._real_url
|
| 421 |
+
|
| 422 |
+
@reify
|
| 423 |
+
def host(self) -> str:
|
| 424 |
+
assert self._url.host is not None
|
| 425 |
+
return self._url.host
|
| 426 |
+
|
| 427 |
+
@reify
|
| 428 |
+
def headers(self) -> "CIMultiDictProxy[str]":
|
| 429 |
+
return self._headers
|
| 430 |
+
|
| 431 |
+
@reify
|
| 432 |
+
def raw_headers(self) -> RawHeaders:
|
| 433 |
+
return self._raw_headers
|
| 434 |
+
|
| 435 |
+
@reify
|
| 436 |
+
def request_info(self) -> RequestInfo:
|
| 437 |
+
return self._request_info
|
| 438 |
+
|
| 439 |
+
@reify
|
| 440 |
+
def content_disposition(self) -> Optional[ContentDisposition]:
|
| 441 |
+
raw = self._headers.get(hdrs.CONTENT_DISPOSITION)
|
| 442 |
+
if raw is None:
|
| 443 |
+
return None
|
| 444 |
+
disposition_type, params_dct = multipart.parse_content_disposition(raw)
|
| 445 |
+
params = MappingProxyType(params_dct)
|
| 446 |
+
filename = multipart.content_disposition_filename(params)
|
| 447 |
+
return ContentDisposition(disposition_type, params, filename)
|
| 448 |
+
|
| 449 |
+
def __del__(self, _warnings: Any = warnings) -> None:
|
| 450 |
+
if self._closed:
|
| 451 |
+
return
|
| 452 |
+
|
| 453 |
+
if self._connection is not None:
|
| 454 |
+
self._connection.release()
|
| 455 |
+
self._cleanup_writer()
|
| 456 |
+
|
| 457 |
+
if self._loop.get_debug():
|
| 458 |
+
kwargs = {"source": self}
|
| 459 |
+
_warnings.warn(f"Unclosed response {self!r}", ResourceWarning, **kwargs)
|
| 460 |
+
context = {"client_response": self, "message": "Unclosed response"}
|
| 461 |
+
if self._source_traceback:
|
| 462 |
+
context["source_traceback"] = self._source_traceback
|
| 463 |
+
self._loop.call_exception_handler(context)
|
| 464 |
+
|
| 465 |
+
def __repr__(self) -> str:
|
| 466 |
+
out = io.StringIO()
|
| 467 |
+
ascii_encodable_url = str(self.url)
|
| 468 |
+
if self.reason:
|
| 469 |
+
ascii_encodable_reason = self.reason.encode(
|
| 470 |
+
"ascii", "backslashreplace"
|
| 471 |
+
).decode("ascii")
|
| 472 |
+
else:
|
| 473 |
+
ascii_encodable_reason = "None"
|
| 474 |
+
print(
|
| 475 |
+
"<ClientResponse({}) [{} {}]>".format(
|
| 476 |
+
ascii_encodable_url, self.status, ascii_encodable_reason
|
| 477 |
+
),
|
| 478 |
+
file=out,
|
| 479 |
+
)
|
| 480 |
+
print(self.headers, file=out)
|
| 481 |
+
return out.getvalue()
|
| 482 |
+
|
| 483 |
+
@property
|
| 484 |
+
def connection(self) -> Optional["Connection"]:
|
| 485 |
+
return self._connection
|
| 486 |
+
|
| 487 |
+
@reify
|
| 488 |
+
def history(self) -> Tuple["ClientResponse", ...]:
|
| 489 |
+
"""A sequence of of responses, if redirects occurred."""
|
| 490 |
+
return self._history
|
| 491 |
+
|
| 492 |
+
@reify
|
| 493 |
+
def links(self) -> "MultiDictProxy[MultiDictProxy[Union[str, URL]]]":
|
| 494 |
+
links_str = ", ".join(self.headers.getall("link", []))
|
| 495 |
+
|
| 496 |
+
if not links_str:
|
| 497 |
+
return MultiDictProxy(MultiDict())
|
| 498 |
+
|
| 499 |
+
links: MultiDict[MultiDictProxy[Union[str, URL]]] = MultiDict()
|
| 500 |
+
|
| 501 |
+
for val in re.split(r",(?=\s*<)", links_str):
|
| 502 |
+
match = re.match(r"\s*<(.*)>(.*)", val)
|
| 503 |
+
if match is None: # pragma: no cover
|
| 504 |
+
# the check exists to suppress mypy error
|
| 505 |
+
continue
|
| 506 |
+
url, params_str = match.groups()
|
| 507 |
+
params = params_str.split(";")[1:]
|
| 508 |
+
|
| 509 |
+
link: MultiDict[Union[str, URL]] = MultiDict()
|
| 510 |
+
|
| 511 |
+
for param in params:
|
| 512 |
+
match = re.match(r"^\s*(\S*)\s*=\s*(['\"]?)(.*?)(\2)\s*$", param, re.M)
|
| 513 |
+
if match is None: # pragma: no cover
|
| 514 |
+
# the check exists to suppress mypy error
|
| 515 |
+
continue
|
| 516 |
+
key, _, value, _ = match.groups()
|
| 517 |
+
|
| 518 |
+
link.add(key, value)
|
| 519 |
+
|
| 520 |
+
key = link.get("rel", url)
|
| 521 |
+
|
| 522 |
+
link.add("url", self.url.join(URL(url)))
|
| 523 |
+
|
| 524 |
+
links.add(str(key), MultiDictProxy(link))
|
| 525 |
+
|
| 526 |
+
return MultiDictProxy(links)
|
| 527 |
+
|
| 528 |
+
async def start(self, connection: "Connection") -> "ClientResponse":
|
| 529 |
+
"""Start response processing."""
|
| 530 |
+
self._closed = False
|
| 531 |
+
self._protocol = connection.protocol
|
| 532 |
+
self._connection = connection
|
| 533 |
+
|
| 534 |
+
with self._timer:
|
| 535 |
+
while True:
|
| 536 |
+
# read response
|
| 537 |
+
try:
|
| 538 |
+
protocol = self._protocol
|
| 539 |
+
message, payload = await protocol.read() # type: ignore[union-attr]
|
| 540 |
+
except http.HttpProcessingError as exc:
|
| 541 |
+
raise ClientResponseError(
|
| 542 |
+
self.request_info,
|
| 543 |
+
self.history,
|
| 544 |
+
status=exc.code,
|
| 545 |
+
message=exc.message,
|
| 546 |
+
headers=exc.headers,
|
| 547 |
+
) from exc
|
| 548 |
+
|
| 549 |
+
if message.code < 100 or message.code > 199 or message.code == 101:
|
| 550 |
+
break
|
| 551 |
+
|
| 552 |
+
if self._continue is not None:
|
| 553 |
+
set_result(self._continue, True)
|
| 554 |
+
self._continue = None
|
| 555 |
+
|
| 556 |
+
# payload eof handler
|
| 557 |
+
payload.on_eof(self._response_eof)
|
| 558 |
+
|
| 559 |
+
# response status
|
| 560 |
+
self.version = message.version
|
| 561 |
+
self.status = message.code
|
| 562 |
+
self.reason = message.reason
|
| 563 |
+
|
| 564 |
+
# headers
|
| 565 |
+
self._headers = message.headers # type is CIMultiDictProxy
|
| 566 |
+
self._raw_headers = message.raw_headers # type is Tuple[bytes, bytes]
|
| 567 |
+
|
| 568 |
+
# payload
|
| 569 |
+
self.content = payload
|
| 570 |
+
|
| 571 |
+
# cookies
|
| 572 |
+
if cookie_hdrs := self.headers.getall(hdrs.SET_COOKIE, ()):
|
| 573 |
+
# Store raw cookie headers for CookieJar
|
| 574 |
+
self._raw_cookie_headers = tuple(cookie_hdrs)
|
| 575 |
+
return self
|
| 576 |
+
|
| 577 |
+
def _response_eof(self) -> None:
|
| 578 |
+
if self._closed:
|
| 579 |
+
return
|
| 580 |
+
|
| 581 |
+
# protocol could be None because connection could be detached
|
| 582 |
+
protocol = self._connection and self._connection.protocol
|
| 583 |
+
if protocol is not None and protocol.upgraded:
|
| 584 |
+
return
|
| 585 |
+
|
| 586 |
+
self._closed = True
|
| 587 |
+
self._cleanup_writer()
|
| 588 |
+
self._release_connection()
|
| 589 |
+
|
| 590 |
+
@property
|
| 591 |
+
def closed(self) -> bool:
|
| 592 |
+
return self._closed
|
| 593 |
+
|
| 594 |
+
def close(self) -> None:
|
| 595 |
+
if not self._released:
|
| 596 |
+
self._notify_content()
|
| 597 |
+
|
| 598 |
+
self._closed = True
|
| 599 |
+
if self._loop is None or self._loop.is_closed():
|
| 600 |
+
return
|
| 601 |
+
|
| 602 |
+
self._cleanup_writer()
|
| 603 |
+
if self._connection is not None:
|
| 604 |
+
self._connection.close()
|
| 605 |
+
self._connection = None
|
| 606 |
+
|
| 607 |
+
def release(self) -> Any:
|
| 608 |
+
if not self._released:
|
| 609 |
+
self._notify_content()
|
| 610 |
+
|
| 611 |
+
self._closed = True
|
| 612 |
+
|
| 613 |
+
self._cleanup_writer()
|
| 614 |
+
self._release_connection()
|
| 615 |
+
return noop()
|
| 616 |
+
|
| 617 |
+
@property
|
| 618 |
+
def ok(self) -> bool:
|
| 619 |
+
"""Returns ``True`` if ``status`` is less than ``400``, ``False`` if not.
|
| 620 |
+
|
| 621 |
+
This is **not** a check for ``200 OK`` but a check that the response
|
| 622 |
+
status is under 400.
|
| 623 |
+
"""
|
| 624 |
+
return 400 > self.status
|
| 625 |
+
|
| 626 |
+
def raise_for_status(self) -> None:
|
| 627 |
+
if not self.ok:
|
| 628 |
+
# reason should always be not None for a started response
|
| 629 |
+
assert self.reason is not None
|
| 630 |
+
|
| 631 |
+
# If we're in a context we can rely on __aexit__() to release as the
|
| 632 |
+
# exception propagates.
|
| 633 |
+
if not self._in_context:
|
| 634 |
+
self.release()
|
| 635 |
+
|
| 636 |
+
raise ClientResponseError(
|
| 637 |
+
self.request_info,
|
| 638 |
+
self.history,
|
| 639 |
+
status=self.status,
|
| 640 |
+
message=self.reason,
|
| 641 |
+
headers=self.headers,
|
| 642 |
+
)
|
| 643 |
+
|
| 644 |
+
def _release_connection(self) -> None:
|
| 645 |
+
if self._connection is not None:
|
| 646 |
+
if self.__writer is None:
|
| 647 |
+
self._connection.release()
|
| 648 |
+
self._connection = None
|
| 649 |
+
else:
|
| 650 |
+
self.__writer.add_done_callback(lambda f: self._release_connection())
|
| 651 |
+
|
| 652 |
+
async def _wait_released(self) -> None:
|
| 653 |
+
if self.__writer is not None:
|
| 654 |
+
try:
|
| 655 |
+
await self.__writer
|
| 656 |
+
except asyncio.CancelledError:
|
| 657 |
+
if (
|
| 658 |
+
sys.version_info >= (3, 11)
|
| 659 |
+
and (task := asyncio.current_task())
|
| 660 |
+
and task.cancelling()
|
| 661 |
+
):
|
| 662 |
+
raise
|
| 663 |
+
self._release_connection()
|
| 664 |
+
|
| 665 |
+
def _cleanup_writer(self) -> None:
|
| 666 |
+
if self.__writer is not None:
|
| 667 |
+
self.__writer.cancel()
|
| 668 |
+
self._session = None
|
| 669 |
+
|
| 670 |
+
def _notify_content(self) -> None:
|
| 671 |
+
content = self.content
|
| 672 |
+
if content and content.exception() is None:
|
| 673 |
+
set_exception(content, _CONNECTION_CLOSED_EXCEPTION)
|
| 674 |
+
self._released = True
|
| 675 |
+
|
| 676 |
+
async def wait_for_close(self) -> None:
|
| 677 |
+
if self.__writer is not None:
|
| 678 |
+
try:
|
| 679 |
+
await self.__writer
|
| 680 |
+
except asyncio.CancelledError:
|
| 681 |
+
if (
|
| 682 |
+
sys.version_info >= (3, 11)
|
| 683 |
+
and (task := asyncio.current_task())
|
| 684 |
+
and task.cancelling()
|
| 685 |
+
):
|
| 686 |
+
raise
|
| 687 |
+
self.release()
|
| 688 |
+
|
| 689 |
+
async def read(self) -> bytes:
|
| 690 |
+
"""Read response payload."""
|
| 691 |
+
if self._body is None:
|
| 692 |
+
try:
|
| 693 |
+
self._body = await self.content.read()
|
| 694 |
+
for trace in self._traces:
|
| 695 |
+
await trace.send_response_chunk_received(
|
| 696 |
+
self.method, self.url, self._body
|
| 697 |
+
)
|
| 698 |
+
except BaseException:
|
| 699 |
+
self.close()
|
| 700 |
+
raise
|
| 701 |
+
elif self._released: # Response explicitly released
|
| 702 |
+
raise ClientConnectionError("Connection closed")
|
| 703 |
+
|
| 704 |
+
protocol = self._connection and self._connection.protocol
|
| 705 |
+
if protocol is None or not protocol.upgraded:
|
| 706 |
+
await self._wait_released() # Underlying connection released
|
| 707 |
+
return self._body
|
| 708 |
+
|
| 709 |
+
def get_encoding(self) -> str:
|
| 710 |
+
ctype = self.headers.get(hdrs.CONTENT_TYPE, "").lower()
|
| 711 |
+
mimetype = helpers.parse_mimetype(ctype)
|
| 712 |
+
|
| 713 |
+
encoding = mimetype.parameters.get("charset")
|
| 714 |
+
if encoding:
|
| 715 |
+
with contextlib.suppress(LookupError, ValueError):
|
| 716 |
+
return codecs.lookup(encoding).name
|
| 717 |
+
|
| 718 |
+
if mimetype.type == "application" and (
|
| 719 |
+
mimetype.subtype == "json" or mimetype.subtype == "rdap"
|
| 720 |
+
):
|
| 721 |
+
# RFC 7159 states that the default encoding is UTF-8.
|
| 722 |
+
# RFC 7483 defines application/rdap+json
|
| 723 |
+
return "utf-8"
|
| 724 |
+
|
| 725 |
+
if self._body is None:
|
| 726 |
+
raise RuntimeError(
|
| 727 |
+
"Cannot compute fallback encoding of a not yet read body"
|
| 728 |
+
)
|
| 729 |
+
|
| 730 |
+
return self._resolve_charset(self, self._body)
|
| 731 |
+
|
| 732 |
+
async def text(self, encoding: Optional[str] = None, errors: str = "strict") -> str:
|
| 733 |
+
"""Read response payload and decode."""
|
| 734 |
+
if self._body is None:
|
| 735 |
+
await self.read()
|
| 736 |
+
|
| 737 |
+
if encoding is None:
|
| 738 |
+
encoding = self.get_encoding()
|
| 739 |
+
|
| 740 |
+
return self._body.decode(encoding, errors=errors) # type: ignore[union-attr]
|
| 741 |
+
|
| 742 |
+
async def json(
|
| 743 |
+
self,
|
| 744 |
+
*,
|
| 745 |
+
encoding: Optional[str] = None,
|
| 746 |
+
loads: JSONDecoder = DEFAULT_JSON_DECODER,
|
| 747 |
+
content_type: Optional[str] = "application/json",
|
| 748 |
+
) -> Any:
|
| 749 |
+
"""Read and decodes JSON response."""
|
| 750 |
+
if self._body is None:
|
| 751 |
+
await self.read()
|
| 752 |
+
|
| 753 |
+
if content_type:
|
| 754 |
+
ctype = self.headers.get(hdrs.CONTENT_TYPE, "").lower()
|
| 755 |
+
if not _is_expected_content_type(ctype, content_type):
|
| 756 |
+
raise ContentTypeError(
|
| 757 |
+
self.request_info,
|
| 758 |
+
self.history,
|
| 759 |
+
status=self.status,
|
| 760 |
+
message=(
|
| 761 |
+
"Attempt to decode JSON with unexpected mimetype: %s" % ctype
|
| 762 |
+
),
|
| 763 |
+
headers=self.headers,
|
| 764 |
+
)
|
| 765 |
+
|
| 766 |
+
stripped = self._body.strip() # type: ignore[union-attr]
|
| 767 |
+
if not stripped:
|
| 768 |
+
return None
|
| 769 |
+
|
| 770 |
+
if encoding is None:
|
| 771 |
+
encoding = self.get_encoding()
|
| 772 |
+
|
| 773 |
+
return loads(stripped.decode(encoding))
|
| 774 |
+
|
| 775 |
+
async def __aenter__(self) -> "ClientResponse":
|
| 776 |
+
self._in_context = True
|
| 777 |
+
return self
|
| 778 |
+
|
| 779 |
+
async def __aexit__(
|
| 780 |
+
self,
|
| 781 |
+
exc_type: Optional[Type[BaseException]],
|
| 782 |
+
exc_val: Optional[BaseException],
|
| 783 |
+
exc_tb: Optional[TracebackType],
|
| 784 |
+
) -> None:
|
| 785 |
+
self._in_context = False
|
| 786 |
+
# similar to _RequestContextManager, we do not need to check
|
| 787 |
+
# for exceptions, response object can close connection
|
| 788 |
+
# if state is broken
|
| 789 |
+
self.release()
|
| 790 |
+
await self.wait_for_close()
|
| 791 |
+
|
| 792 |
+
|
| 793 |
+
class ClientRequest:
|
| 794 |
+
GET_METHODS = {
|
| 795 |
+
hdrs.METH_GET,
|
| 796 |
+
hdrs.METH_HEAD,
|
| 797 |
+
hdrs.METH_OPTIONS,
|
| 798 |
+
hdrs.METH_TRACE,
|
| 799 |
+
}
|
| 800 |
+
POST_METHODS = {hdrs.METH_PATCH, hdrs.METH_POST, hdrs.METH_PUT}
|
| 801 |
+
ALL_METHODS = GET_METHODS.union(POST_METHODS).union({hdrs.METH_DELETE})
|
| 802 |
+
|
| 803 |
+
DEFAULT_HEADERS = {
|
| 804 |
+
hdrs.ACCEPT: "*/*",
|
| 805 |
+
hdrs.ACCEPT_ENCODING: _gen_default_accept_encoding(),
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
# Type of body depends on PAYLOAD_REGISTRY, which is dynamic.
|
| 809 |
+
_body: Union[None, payload.Payload] = None
|
| 810 |
+
auth = None
|
| 811 |
+
response = None
|
| 812 |
+
|
| 813 |
+
__writer: Optional["asyncio.Task[None]"] = None # async task for streaming data
|
| 814 |
+
|
| 815 |
+
# These class defaults help create_autospec() work correctly.
|
| 816 |
+
# If autospec is improved in future, maybe these can be removed.
|
| 817 |
+
url = URL()
|
| 818 |
+
method = "GET"
|
| 819 |
+
|
| 820 |
+
_continue = None # waiter future for '100 Continue' response
|
| 821 |
+
|
| 822 |
+
_skip_auto_headers: Optional["CIMultiDict[None]"] = None
|
| 823 |
+
|
| 824 |
+
# N.B.
|
| 825 |
+
# Adding __del__ method with self._writer closing doesn't make sense
|
| 826 |
+
# because _writer is instance method, thus it keeps a reference to self.
|
| 827 |
+
# Until writer has finished finalizer will not be called.
|
| 828 |
+
|
| 829 |
+
def __init__(
|
| 830 |
+
self,
|
| 831 |
+
method: str,
|
| 832 |
+
url: URL,
|
| 833 |
+
*,
|
| 834 |
+
params: Query = None,
|
| 835 |
+
headers: Optional[LooseHeaders] = None,
|
| 836 |
+
skip_auto_headers: Optional[Iterable[str]] = None,
|
| 837 |
+
data: Any = None,
|
| 838 |
+
cookies: Optional[LooseCookies] = None,
|
| 839 |
+
auth: Optional[BasicAuth] = None,
|
| 840 |
+
version: http.HttpVersion = http.HttpVersion11,
|
| 841 |
+
compress: Union[str, bool, None] = None,
|
| 842 |
+
chunked: Optional[bool] = None,
|
| 843 |
+
expect100: bool = False,
|
| 844 |
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
| 845 |
+
response_class: Optional[Type["ClientResponse"]] = None,
|
| 846 |
+
proxy: Optional[URL] = None,
|
| 847 |
+
proxy_auth: Optional[BasicAuth] = None,
|
| 848 |
+
timer: Optional[BaseTimerContext] = None,
|
| 849 |
+
session: Optional["ClientSession"] = None,
|
| 850 |
+
ssl: Union[SSLContext, bool, Fingerprint] = True,
|
| 851 |
+
proxy_headers: Optional[LooseHeaders] = None,
|
| 852 |
+
traces: Optional[List["Trace"]] = None,
|
| 853 |
+
trust_env: bool = False,
|
| 854 |
+
server_hostname: Optional[str] = None,
|
| 855 |
+
):
|
| 856 |
+
if loop is None:
|
| 857 |
+
loop = asyncio.get_event_loop()
|
| 858 |
+
if match := _CONTAINS_CONTROL_CHAR_RE.search(method):
|
| 859 |
+
raise ValueError(
|
| 860 |
+
f"Method cannot contain non-token characters {method!r} "
|
| 861 |
+
f"(found at least {match.group()!r})"
|
| 862 |
+
)
|
| 863 |
+
# URL forbids subclasses, so a simple type check is enough.
|
| 864 |
+
assert type(url) is URL, url
|
| 865 |
+
if proxy is not None:
|
| 866 |
+
assert type(proxy) is URL, proxy
|
| 867 |
+
# FIXME: session is None in tests only, need to fix tests
|
| 868 |
+
# assert session is not None
|
| 869 |
+
if TYPE_CHECKING:
|
| 870 |
+
assert session is not None
|
| 871 |
+
self._session = session
|
| 872 |
+
if params:
|
| 873 |
+
url = url.extend_query(params)
|
| 874 |
+
self.original_url = url
|
| 875 |
+
self.url = url.with_fragment(None) if url.raw_fragment else url
|
| 876 |
+
self.method = method.upper()
|
| 877 |
+
self.chunked = chunked
|
| 878 |
+
self.compress = compress
|
| 879 |
+
self.loop = loop
|
| 880 |
+
self.length = None
|
| 881 |
+
if response_class is None:
|
| 882 |
+
real_response_class = ClientResponse
|
| 883 |
+
else:
|
| 884 |
+
real_response_class = response_class
|
| 885 |
+
self.response_class: Type[ClientResponse] = real_response_class
|
| 886 |
+
self._timer = timer if timer is not None else TimerNoop()
|
| 887 |
+
self._ssl = ssl if ssl is not None else True
|
| 888 |
+
self.server_hostname = server_hostname
|
| 889 |
+
|
| 890 |
+
if loop.get_debug():
|
| 891 |
+
self._source_traceback = traceback.extract_stack(sys._getframe(1))
|
| 892 |
+
|
| 893 |
+
self.update_version(version)
|
| 894 |
+
self.update_host(url)
|
| 895 |
+
self.update_headers(headers)
|
| 896 |
+
self.update_auto_headers(skip_auto_headers)
|
| 897 |
+
self.update_cookies(cookies)
|
| 898 |
+
self.update_content_encoding(data)
|
| 899 |
+
self.update_auth(auth, trust_env)
|
| 900 |
+
self.update_proxy(proxy, proxy_auth, proxy_headers)
|
| 901 |
+
|
| 902 |
+
self.update_body_from_data(data)
|
| 903 |
+
if data is not None or self.method not in self.GET_METHODS:
|
| 904 |
+
self.update_transfer_encoding()
|
| 905 |
+
self.update_expect_continue(expect100)
|
| 906 |
+
self._traces = [] if traces is None else traces
|
| 907 |
+
|
| 908 |
+
def __reset_writer(self, _: object = None) -> None:
|
| 909 |
+
self.__writer = None
|
| 910 |
+
|
| 911 |
+
def _get_content_length(self) -> Optional[int]:
|
| 912 |
+
"""Extract and validate Content-Length header value.
|
| 913 |
+
|
| 914 |
+
Returns parsed Content-Length value or None if not set.
|
| 915 |
+
Raises ValueError if header exists but cannot be parsed as an integer.
|
| 916 |
+
"""
|
| 917 |
+
if hdrs.CONTENT_LENGTH not in self.headers:
|
| 918 |
+
return None
|
| 919 |
+
|
| 920 |
+
content_length_hdr = self.headers[hdrs.CONTENT_LENGTH]
|
| 921 |
+
try:
|
| 922 |
+
return int(content_length_hdr)
|
| 923 |
+
except ValueError:
|
| 924 |
+
raise ValueError(
|
| 925 |
+
f"Invalid Content-Length header: {content_length_hdr}"
|
| 926 |
+
) from None
|
| 927 |
+
|
| 928 |
+
@property
|
| 929 |
+
def skip_auto_headers(self) -> CIMultiDict[None]:
|
| 930 |
+
return self._skip_auto_headers or CIMultiDict()
|
| 931 |
+
|
| 932 |
+
@property
|
| 933 |
+
def _writer(self) -> Optional["asyncio.Task[None]"]:
|
| 934 |
+
return self.__writer
|
| 935 |
+
|
| 936 |
+
@_writer.setter
|
| 937 |
+
def _writer(self, writer: "asyncio.Task[None]") -> None:
|
| 938 |
+
if self.__writer is not None:
|
| 939 |
+
self.__writer.remove_done_callback(self.__reset_writer)
|
| 940 |
+
self.__writer = writer
|
| 941 |
+
writer.add_done_callback(self.__reset_writer)
|
| 942 |
+
|
| 943 |
+
def is_ssl(self) -> bool:
|
| 944 |
+
return self.url.scheme in _SSL_SCHEMES
|
| 945 |
+
|
| 946 |
+
@property
|
| 947 |
+
def ssl(self) -> Union["SSLContext", bool, Fingerprint]:
|
| 948 |
+
return self._ssl
|
| 949 |
+
|
| 950 |
+
@property
|
| 951 |
+
def connection_key(self) -> ConnectionKey:
|
| 952 |
+
if proxy_headers := self.proxy_headers:
|
| 953 |
+
h: Optional[int] = hash(tuple(proxy_headers.items()))
|
| 954 |
+
else:
|
| 955 |
+
h = None
|
| 956 |
+
url = self.url
|
| 957 |
+
return tuple.__new__(
|
| 958 |
+
ConnectionKey,
|
| 959 |
+
(
|
| 960 |
+
url.raw_host or "",
|
| 961 |
+
url.port,
|
| 962 |
+
url.scheme in _SSL_SCHEMES,
|
| 963 |
+
self._ssl,
|
| 964 |
+
self.proxy,
|
| 965 |
+
self.proxy_auth,
|
| 966 |
+
h,
|
| 967 |
+
),
|
| 968 |
+
)
|
| 969 |
+
|
| 970 |
+
@property
|
| 971 |
+
def host(self) -> str:
|
| 972 |
+
ret = self.url.raw_host
|
| 973 |
+
assert ret is not None
|
| 974 |
+
return ret
|
| 975 |
+
|
| 976 |
+
@property
|
| 977 |
+
def port(self) -> Optional[int]:
|
| 978 |
+
return self.url.port
|
| 979 |
+
|
| 980 |
+
@property
|
| 981 |
+
def body(self) -> Union[payload.Payload, Literal[b""]]:
|
| 982 |
+
"""Request body."""
|
| 983 |
+
# empty body is represented as bytes for backwards compatibility
|
| 984 |
+
return self._body or b""
|
| 985 |
+
|
| 986 |
+
@body.setter
|
| 987 |
+
def body(self, value: Any) -> None:
|
| 988 |
+
"""Set request body with warning for non-autoclose payloads.
|
| 989 |
+
|
| 990 |
+
WARNING: This setter must be called from within an event loop and is not
|
| 991 |
+
thread-safe. Setting body outside of an event loop may raise RuntimeError
|
| 992 |
+
when closing file-based payloads.
|
| 993 |
+
|
| 994 |
+
DEPRECATED: Direct assignment to body is deprecated and will be removed
|
| 995 |
+
in a future version. Use await update_body() instead for proper resource
|
| 996 |
+
management.
|
| 997 |
+
"""
|
| 998 |
+
# Close existing payload if present
|
| 999 |
+
if self._body is not None:
|
| 1000 |
+
# Warn if the payload needs manual closing
|
| 1001 |
+
# stacklevel=3: user code -> body setter -> _warn_if_unclosed_payload
|
| 1002 |
+
_warn_if_unclosed_payload(self._body, stacklevel=3)
|
| 1003 |
+
# NOTE: In the future, when we remove sync close support,
|
| 1004 |
+
# this setter will need to be removed and only the async
|
| 1005 |
+
# update_body() method will be available. For now, we call
|
| 1006 |
+
# _close() for backwards compatibility.
|
| 1007 |
+
self._body._close()
|
| 1008 |
+
self._update_body(value)
|
| 1009 |
+
|
| 1010 |
+
@property
|
| 1011 |
+
def request_info(self) -> RequestInfo:
|
| 1012 |
+
headers: CIMultiDictProxy[str] = CIMultiDictProxy(self.headers)
|
| 1013 |
+
# These are created on every request, so we use a NamedTuple
|
| 1014 |
+
# for performance reasons. We don't use the RequestInfo.__new__
|
| 1015 |
+
# method because it has a different signature which is provided
|
| 1016 |
+
# for backwards compatibility only.
|
| 1017 |
+
return tuple.__new__(
|
| 1018 |
+
RequestInfo, (self.url, self.method, headers, self.original_url)
|
| 1019 |
+
)
|
| 1020 |
+
|
| 1021 |
+
@property
|
| 1022 |
+
def session(self) -> "ClientSession":
|
| 1023 |
+
"""Return the ClientSession instance.
|
| 1024 |
+
|
| 1025 |
+
This property provides access to the ClientSession that initiated
|
| 1026 |
+
this request, allowing middleware to make additional requests
|
| 1027 |
+
using the same session.
|
| 1028 |
+
"""
|
| 1029 |
+
return self._session
|
| 1030 |
+
|
| 1031 |
+
def update_host(self, url: URL) -> None:
|
| 1032 |
+
"""Update destination host, port and connection type (ssl)."""
|
| 1033 |
+
# get host/port
|
| 1034 |
+
if not url.raw_host:
|
| 1035 |
+
raise InvalidURL(url)
|
| 1036 |
+
|
| 1037 |
+
# basic auth info
|
| 1038 |
+
if url.raw_user or url.raw_password:
|
| 1039 |
+
self.auth = helpers.BasicAuth(url.user or "", url.password or "")
|
| 1040 |
+
|
| 1041 |
+
def update_version(self, version: Union[http.HttpVersion, str]) -> None:
|
| 1042 |
+
"""Convert request version to two elements tuple.
|
| 1043 |
+
|
| 1044 |
+
parser HTTP version '1.1' => (1, 1)
|
| 1045 |
+
"""
|
| 1046 |
+
if isinstance(version, str):
|
| 1047 |
+
v = [part.strip() for part in version.split(".", 1)]
|
| 1048 |
+
try:
|
| 1049 |
+
version = http.HttpVersion(int(v[0]), int(v[1]))
|
| 1050 |
+
except ValueError:
|
| 1051 |
+
raise ValueError(
|
| 1052 |
+
f"Can not parse http version number: {version}"
|
| 1053 |
+
) from None
|
| 1054 |
+
self.version = version
|
| 1055 |
+
|
| 1056 |
+
def update_headers(self, headers: Optional[LooseHeaders]) -> None:
|
| 1057 |
+
"""Update request headers."""
|
| 1058 |
+
self.headers: CIMultiDict[str] = CIMultiDict()
|
| 1059 |
+
|
| 1060 |
+
# Build the host header
|
| 1061 |
+
host = self.url.host_port_subcomponent
|
| 1062 |
+
|
| 1063 |
+
# host_port_subcomponent is None when the URL is a relative URL.
|
| 1064 |
+
# but we know we do not have a relative URL here.
|
| 1065 |
+
assert host is not None
|
| 1066 |
+
self.headers[hdrs.HOST] = host
|
| 1067 |
+
|
| 1068 |
+
if not headers:
|
| 1069 |
+
return
|
| 1070 |
+
|
| 1071 |
+
if isinstance(headers, (dict, MultiDictProxy, MultiDict)):
|
| 1072 |
+
headers = headers.items()
|
| 1073 |
+
|
| 1074 |
+
for key, value in headers: # type: ignore[misc]
|
| 1075 |
+
# A special case for Host header
|
| 1076 |
+
if key in hdrs.HOST_ALL:
|
| 1077 |
+
self.headers[key] = value
|
| 1078 |
+
else:
|
| 1079 |
+
self.headers.add(key, value)
|
| 1080 |
+
|
| 1081 |
+
def update_auto_headers(self, skip_auto_headers: Optional[Iterable[str]]) -> None:
|
| 1082 |
+
if skip_auto_headers is not None:
|
| 1083 |
+
self._skip_auto_headers = CIMultiDict(
|
| 1084 |
+
(hdr, None) for hdr in sorted(skip_auto_headers)
|
| 1085 |
+
)
|
| 1086 |
+
used_headers = self.headers.copy()
|
| 1087 |
+
used_headers.extend(self._skip_auto_headers) # type: ignore[arg-type]
|
| 1088 |
+
else:
|
| 1089 |
+
# Fast path when there are no headers to skip
|
| 1090 |
+
# which is the most common case.
|
| 1091 |
+
used_headers = self.headers
|
| 1092 |
+
|
| 1093 |
+
for hdr, val in self.DEFAULT_HEADERS.items():
|
| 1094 |
+
if hdr not in used_headers:
|
| 1095 |
+
self.headers[hdr] = val
|
| 1096 |
+
|
| 1097 |
+
if hdrs.USER_AGENT not in used_headers:
|
| 1098 |
+
self.headers[hdrs.USER_AGENT] = SERVER_SOFTWARE
|
| 1099 |
+
|
| 1100 |
+
def update_cookies(self, cookies: Optional[LooseCookies]) -> None:
|
| 1101 |
+
"""Update request cookies header."""
|
| 1102 |
+
if not cookies:
|
| 1103 |
+
return
|
| 1104 |
+
|
| 1105 |
+
c = SimpleCookie()
|
| 1106 |
+
if hdrs.COOKIE in self.headers:
|
| 1107 |
+
# parse_cookie_header for RFC 6265 compliant Cookie header parsing
|
| 1108 |
+
c.update(parse_cookie_header(self.headers.get(hdrs.COOKIE, "")))
|
| 1109 |
+
del self.headers[hdrs.COOKIE]
|
| 1110 |
+
|
| 1111 |
+
if isinstance(cookies, Mapping):
|
| 1112 |
+
iter_cookies = cookies.items()
|
| 1113 |
+
else:
|
| 1114 |
+
iter_cookies = cookies # type: ignore[assignment]
|
| 1115 |
+
for name, value in iter_cookies:
|
| 1116 |
+
if isinstance(value, Morsel):
|
| 1117 |
+
# Use helper to preserve coded_value exactly as sent by server
|
| 1118 |
+
c[name] = preserve_morsel_with_coded_value(value)
|
| 1119 |
+
else:
|
| 1120 |
+
c[name] = value # type: ignore[assignment]
|
| 1121 |
+
|
| 1122 |
+
self.headers[hdrs.COOKIE] = c.output(header="", sep=";").strip()
|
| 1123 |
+
|
| 1124 |
+
def update_content_encoding(self, data: Any) -> None:
|
| 1125 |
+
"""Set request content encoding."""
|
| 1126 |
+
if not data:
|
| 1127 |
+
# Don't compress an empty body.
|
| 1128 |
+
self.compress = None
|
| 1129 |
+
return
|
| 1130 |
+
|
| 1131 |
+
if self.headers.get(hdrs.CONTENT_ENCODING):
|
| 1132 |
+
if self.compress:
|
| 1133 |
+
raise ValueError(
|
| 1134 |
+
"compress can not be set if Content-Encoding header is set"
|
| 1135 |
+
)
|
| 1136 |
+
elif self.compress:
|
| 1137 |
+
if not isinstance(self.compress, str):
|
| 1138 |
+
self.compress = "deflate"
|
| 1139 |
+
self.headers[hdrs.CONTENT_ENCODING] = self.compress
|
| 1140 |
+
self.chunked = True # enable chunked, no need to deal with length
|
| 1141 |
+
|
| 1142 |
+
def update_transfer_encoding(self) -> None:
|
| 1143 |
+
"""Analyze transfer-encoding header."""
|
| 1144 |
+
te = self.headers.get(hdrs.TRANSFER_ENCODING, "").lower()
|
| 1145 |
+
|
| 1146 |
+
if "chunked" in te:
|
| 1147 |
+
if self.chunked:
|
| 1148 |
+
raise ValueError(
|
| 1149 |
+
"chunked can not be set "
|
| 1150 |
+
'if "Transfer-Encoding: chunked" header is set'
|
| 1151 |
+
)
|
| 1152 |
+
|
| 1153 |
+
elif self.chunked:
|
| 1154 |
+
if hdrs.CONTENT_LENGTH in self.headers:
|
| 1155 |
+
raise ValueError(
|
| 1156 |
+
"chunked can not be set if Content-Length header is set"
|
| 1157 |
+
)
|
| 1158 |
+
|
| 1159 |
+
self.headers[hdrs.TRANSFER_ENCODING] = "chunked"
|
| 1160 |
+
|
| 1161 |
+
def update_auth(self, auth: Optional[BasicAuth], trust_env: bool = False) -> None:
|
| 1162 |
+
"""Set basic auth."""
|
| 1163 |
+
if auth is None:
|
| 1164 |
+
auth = self.auth
|
| 1165 |
+
if auth is None:
|
| 1166 |
+
return
|
| 1167 |
+
|
| 1168 |
+
if not isinstance(auth, helpers.BasicAuth):
|
| 1169 |
+
raise TypeError("BasicAuth() tuple is required instead")
|
| 1170 |
+
|
| 1171 |
+
self.headers[hdrs.AUTHORIZATION] = auth.encode()
|
| 1172 |
+
|
| 1173 |
+
def update_body_from_data(self, body: Any, _stacklevel: int = 3) -> None:
|
| 1174 |
+
"""Update request body from data."""
|
| 1175 |
+
if self._body is not None:
|
| 1176 |
+
_warn_if_unclosed_payload(self._body, stacklevel=_stacklevel)
|
| 1177 |
+
|
| 1178 |
+
if body is None:
|
| 1179 |
+
self._body = None
|
| 1180 |
+
# Set Content-Length to 0 when body is None for methods that expect a body
|
| 1181 |
+
if (
|
| 1182 |
+
self.method not in self.GET_METHODS
|
| 1183 |
+
and not self.chunked
|
| 1184 |
+
and hdrs.CONTENT_LENGTH not in self.headers
|
| 1185 |
+
):
|
| 1186 |
+
self.headers[hdrs.CONTENT_LENGTH] = "0"
|
| 1187 |
+
return
|
| 1188 |
+
|
| 1189 |
+
# FormData
|
| 1190 |
+
maybe_payload = body() if isinstance(body, FormData) else body
|
| 1191 |
+
|
| 1192 |
+
try:
|
| 1193 |
+
body_payload = payload.PAYLOAD_REGISTRY.get(maybe_payload, disposition=None)
|
| 1194 |
+
except payload.LookupError:
|
| 1195 |
+
body_payload = FormData(maybe_payload)() # type: ignore[arg-type]
|
| 1196 |
+
|
| 1197 |
+
self._body = body_payload
|
| 1198 |
+
# enable chunked encoding if needed
|
| 1199 |
+
if not self.chunked and hdrs.CONTENT_LENGTH not in self.headers:
|
| 1200 |
+
if (size := body_payload.size) is not None:
|
| 1201 |
+
self.headers[hdrs.CONTENT_LENGTH] = str(size)
|
| 1202 |
+
else:
|
| 1203 |
+
self.chunked = True
|
| 1204 |
+
|
| 1205 |
+
# copy payload headers
|
| 1206 |
+
assert body_payload.headers
|
| 1207 |
+
headers = self.headers
|
| 1208 |
+
skip_headers = self._skip_auto_headers
|
| 1209 |
+
for key, value in body_payload.headers.items():
|
| 1210 |
+
if key in headers or (skip_headers is not None and key in skip_headers):
|
| 1211 |
+
continue
|
| 1212 |
+
headers[key] = value
|
| 1213 |
+
|
| 1214 |
+
def _update_body(self, body: Any) -> None:
|
| 1215 |
+
"""Update request body after its already been set."""
|
| 1216 |
+
# Remove existing Content-Length header since body is changing
|
| 1217 |
+
if hdrs.CONTENT_LENGTH in self.headers:
|
| 1218 |
+
del self.headers[hdrs.CONTENT_LENGTH]
|
| 1219 |
+
|
| 1220 |
+
# Remove existing Transfer-Encoding header to avoid conflicts
|
| 1221 |
+
if self.chunked and hdrs.TRANSFER_ENCODING in self.headers:
|
| 1222 |
+
del self.headers[hdrs.TRANSFER_ENCODING]
|
| 1223 |
+
|
| 1224 |
+
# Now update the body using the existing method
|
| 1225 |
+
# Called from _update_body, add 1 to stacklevel from caller
|
| 1226 |
+
self.update_body_from_data(body, _stacklevel=4)
|
| 1227 |
+
|
| 1228 |
+
# Update transfer encoding headers if needed (same logic as __init__)
|
| 1229 |
+
if body is not None or self.method not in self.GET_METHODS:
|
| 1230 |
+
self.update_transfer_encoding()
|
| 1231 |
+
|
| 1232 |
+
async def update_body(self, body: Any) -> None:
|
| 1233 |
+
"""
|
| 1234 |
+
Update request body and close previous payload if needed.
|
| 1235 |
+
|
| 1236 |
+
This method safely updates the request body by first closing any existing
|
| 1237 |
+
payload to prevent resource leaks, then setting the new body.
|
| 1238 |
+
|
| 1239 |
+
IMPORTANT: Always use this method instead of setting request.body directly.
|
| 1240 |
+
Direct assignment to request.body will leak resources if the previous body
|
| 1241 |
+
contains file handles, streams, or other resources that need cleanup.
|
| 1242 |
+
|
| 1243 |
+
Args:
|
| 1244 |
+
body: The new body content. Can be:
|
| 1245 |
+
- bytes/bytearray: Raw binary data
|
| 1246 |
+
- str: Text data (will be encoded using charset from Content-Type)
|
| 1247 |
+
- FormData: Form data that will be encoded as multipart/form-data
|
| 1248 |
+
- Payload: A pre-configured payload object
|
| 1249 |
+
- AsyncIterable: An async iterable of bytes chunks
|
| 1250 |
+
- File-like object: Will be read and sent as binary data
|
| 1251 |
+
- None: Clears the body
|
| 1252 |
+
|
| 1253 |
+
Usage:
|
| 1254 |
+
# CORRECT: Use update_body
|
| 1255 |
+
await request.update_body(b"new request data")
|
| 1256 |
+
|
| 1257 |
+
# WRONG: Don't set body directly
|
| 1258 |
+
# request.body = b"new request data" # This will leak resources!
|
| 1259 |
+
|
| 1260 |
+
# Update with form data
|
| 1261 |
+
form_data = FormData()
|
| 1262 |
+
form_data.add_field('field', 'value')
|
| 1263 |
+
await request.update_body(form_data)
|
| 1264 |
+
|
| 1265 |
+
# Clear body
|
| 1266 |
+
await request.update_body(None)
|
| 1267 |
+
|
| 1268 |
+
Note:
|
| 1269 |
+
This method is async because it may need to close file handles or
|
| 1270 |
+
other resources associated with the previous payload. Always await
|
| 1271 |
+
this method to ensure proper cleanup.
|
| 1272 |
+
|
| 1273 |
+
Warning:
|
| 1274 |
+
Setting request.body directly is highly discouraged and can lead to:
|
| 1275 |
+
- Resource leaks (unclosed file handles, streams)
|
| 1276 |
+
- Memory leaks (unreleased buffers)
|
| 1277 |
+
- Unexpected behavior with streaming payloads
|
| 1278 |
+
|
| 1279 |
+
It is not recommended to change the payload type in middleware. If the
|
| 1280 |
+
body was already set (e.g., as bytes), it's best to keep the same type
|
| 1281 |
+
rather than converting it (e.g., to str) as this may result in unexpected
|
| 1282 |
+
behavior.
|
| 1283 |
+
|
| 1284 |
+
See Also:
|
| 1285 |
+
- update_body_from_data: Synchronous body update without cleanup
|
| 1286 |
+
- body property: Direct body access (STRONGLY DISCOURAGED)
|
| 1287 |
+
|
| 1288 |
+
"""
|
| 1289 |
+
# Close existing payload if it exists and needs closing
|
| 1290 |
+
if self._body is not None:
|
| 1291 |
+
await self._body.close()
|
| 1292 |
+
self._update_body(body)
|
| 1293 |
+
|
| 1294 |
+
def update_expect_continue(self, expect: bool = False) -> None:
|
| 1295 |
+
if expect:
|
| 1296 |
+
self.headers[hdrs.EXPECT] = "100-continue"
|
| 1297 |
+
elif (
|
| 1298 |
+
hdrs.EXPECT in self.headers
|
| 1299 |
+
and self.headers[hdrs.EXPECT].lower() == "100-continue"
|
| 1300 |
+
):
|
| 1301 |
+
expect = True
|
| 1302 |
+
|
| 1303 |
+
if expect:
|
| 1304 |
+
self._continue = self.loop.create_future()
|
| 1305 |
+
|
| 1306 |
+
def update_proxy(
|
| 1307 |
+
self,
|
| 1308 |
+
proxy: Optional[URL],
|
| 1309 |
+
proxy_auth: Optional[BasicAuth],
|
| 1310 |
+
proxy_headers: Optional[LooseHeaders],
|
| 1311 |
+
) -> None:
|
| 1312 |
+
self.proxy = proxy
|
| 1313 |
+
if proxy is None:
|
| 1314 |
+
self.proxy_auth = None
|
| 1315 |
+
self.proxy_headers = None
|
| 1316 |
+
return
|
| 1317 |
+
|
| 1318 |
+
if proxy_auth and not isinstance(proxy_auth, helpers.BasicAuth):
|
| 1319 |
+
raise ValueError("proxy_auth must be None or BasicAuth() tuple")
|
| 1320 |
+
self.proxy_auth = proxy_auth
|
| 1321 |
+
|
| 1322 |
+
if proxy_headers is not None and not isinstance(
|
| 1323 |
+
proxy_headers, (MultiDict, MultiDictProxy)
|
| 1324 |
+
):
|
| 1325 |
+
proxy_headers = CIMultiDict(proxy_headers)
|
| 1326 |
+
self.proxy_headers = proxy_headers
|
| 1327 |
+
|
| 1328 |
+
async def write_bytes(
|
| 1329 |
+
self,
|
| 1330 |
+
writer: AbstractStreamWriter,
|
| 1331 |
+
conn: "Connection",
|
| 1332 |
+
content_length: Optional[int] = None,
|
| 1333 |
+
) -> None:
|
| 1334 |
+
"""
|
| 1335 |
+
Write the request body to the connection stream.
|
| 1336 |
+
|
| 1337 |
+
This method handles writing different types of request bodies:
|
| 1338 |
+
1. Payload objects (using their specialized write_with_length method)
|
| 1339 |
+
2. Bytes/bytearray objects
|
| 1340 |
+
3. Iterable body content
|
| 1341 |
+
|
| 1342 |
+
Args:
|
| 1343 |
+
writer: The stream writer to write the body to
|
| 1344 |
+
conn: The connection being used for this request
|
| 1345 |
+
content_length: Optional maximum number of bytes to write from the body
|
| 1346 |
+
(None means write the entire body)
|
| 1347 |
+
|
| 1348 |
+
The method properly handles:
|
| 1349 |
+
- Waiting for 100-Continue responses if required
|
| 1350 |
+
- Content length constraints for chunked encoding
|
| 1351 |
+
- Error handling for network issues, cancellation, and other exceptions
|
| 1352 |
+
- Signaling EOF and timeout management
|
| 1353 |
+
|
| 1354 |
+
Raises:
|
| 1355 |
+
ClientOSError: When there's an OS-level error writing the body
|
| 1356 |
+
ClientConnectionError: When there's a general connection error
|
| 1357 |
+
asyncio.CancelledError: When the operation is cancelled
|
| 1358 |
+
|
| 1359 |
+
"""
|
| 1360 |
+
# 100 response
|
| 1361 |
+
if self._continue is not None:
|
| 1362 |
+
# Force headers to be sent before waiting for 100-continue
|
| 1363 |
+
writer.send_headers()
|
| 1364 |
+
await writer.drain()
|
| 1365 |
+
await self._continue
|
| 1366 |
+
|
| 1367 |
+
protocol = conn.protocol
|
| 1368 |
+
assert protocol is not None
|
| 1369 |
+
try:
|
| 1370 |
+
# This should be a rare case but the
|
| 1371 |
+
# self._body can be set to None while
|
| 1372 |
+
# the task is being started or we wait above
|
| 1373 |
+
# for the 100-continue response.
|
| 1374 |
+
# The more likely case is we have an empty
|
| 1375 |
+
# payload, but 100-continue is still expected.
|
| 1376 |
+
if self._body is not None:
|
| 1377 |
+
await self._body.write_with_length(writer, content_length)
|
| 1378 |
+
except OSError as underlying_exc:
|
| 1379 |
+
reraised_exc = underlying_exc
|
| 1380 |
+
|
| 1381 |
+
# Distinguish between timeout and other OS errors for better error reporting
|
| 1382 |
+
exc_is_not_timeout = underlying_exc.errno is not None or not isinstance(
|
| 1383 |
+
underlying_exc, asyncio.TimeoutError
|
| 1384 |
+
)
|
| 1385 |
+
if exc_is_not_timeout:
|
| 1386 |
+
reraised_exc = ClientOSError(
|
| 1387 |
+
underlying_exc.errno,
|
| 1388 |
+
f"Can not write request body for {self.url !s}",
|
| 1389 |
+
)
|
| 1390 |
+
|
| 1391 |
+
set_exception(protocol, reraised_exc, underlying_exc)
|
| 1392 |
+
except asyncio.CancelledError:
|
| 1393 |
+
# Body hasn't been fully sent, so connection can't be reused
|
| 1394 |
+
conn.close()
|
| 1395 |
+
raise
|
| 1396 |
+
except Exception as underlying_exc:
|
| 1397 |
+
set_exception(
|
| 1398 |
+
protocol,
|
| 1399 |
+
ClientConnectionError(
|
| 1400 |
+
"Failed to send bytes into the underlying connection "
|
| 1401 |
+
f"{conn !s}: {underlying_exc!r}",
|
| 1402 |
+
),
|
| 1403 |
+
underlying_exc,
|
| 1404 |
+
)
|
| 1405 |
+
else:
|
| 1406 |
+
# Successfully wrote the body, signal EOF and start response timeout
|
| 1407 |
+
await writer.write_eof()
|
| 1408 |
+
protocol.start_timeout()
|
| 1409 |
+
|
| 1410 |
+
async def send(self, conn: "Connection") -> "ClientResponse":
|
| 1411 |
+
# Specify request target:
|
| 1412 |
+
# - CONNECT request must send authority form URI
|
| 1413 |
+
# - not CONNECT proxy must send absolute form URI
|
| 1414 |
+
# - most common is origin form URI
|
| 1415 |
+
if self.method == hdrs.METH_CONNECT:
|
| 1416 |
+
connect_host = self.url.host_subcomponent
|
| 1417 |
+
assert connect_host is not None
|
| 1418 |
+
path = f"{connect_host}:{self.url.port}"
|
| 1419 |
+
elif self.proxy and not self.is_ssl():
|
| 1420 |
+
path = str(self.url)
|
| 1421 |
+
else:
|
| 1422 |
+
path = self.url.raw_path_qs
|
| 1423 |
+
|
| 1424 |
+
protocol = conn.protocol
|
| 1425 |
+
assert protocol is not None
|
| 1426 |
+
writer = StreamWriter(
|
| 1427 |
+
protocol,
|
| 1428 |
+
self.loop,
|
| 1429 |
+
on_chunk_sent=(
|
| 1430 |
+
functools.partial(self._on_chunk_request_sent, self.method, self.url)
|
| 1431 |
+
if self._traces
|
| 1432 |
+
else None
|
| 1433 |
+
),
|
| 1434 |
+
on_headers_sent=(
|
| 1435 |
+
functools.partial(self._on_headers_request_sent, self.method, self.url)
|
| 1436 |
+
if self._traces
|
| 1437 |
+
else None
|
| 1438 |
+
),
|
| 1439 |
+
)
|
| 1440 |
+
|
| 1441 |
+
if self.compress:
|
| 1442 |
+
writer.enable_compression(self.compress) # type: ignore[arg-type]
|
| 1443 |
+
|
| 1444 |
+
if self.chunked is not None:
|
| 1445 |
+
writer.enable_chunking()
|
| 1446 |
+
|
| 1447 |
+
# set default content-type
|
| 1448 |
+
if (
|
| 1449 |
+
self.method in self.POST_METHODS
|
| 1450 |
+
and (
|
| 1451 |
+
self._skip_auto_headers is None
|
| 1452 |
+
or hdrs.CONTENT_TYPE not in self._skip_auto_headers
|
| 1453 |
+
)
|
| 1454 |
+
and hdrs.CONTENT_TYPE not in self.headers
|
| 1455 |
+
):
|
| 1456 |
+
self.headers[hdrs.CONTENT_TYPE] = "application/octet-stream"
|
| 1457 |
+
|
| 1458 |
+
v = self.version
|
| 1459 |
+
if hdrs.CONNECTION not in self.headers:
|
| 1460 |
+
if conn._connector.force_close:
|
| 1461 |
+
if v == HttpVersion11:
|
| 1462 |
+
self.headers[hdrs.CONNECTION] = "close"
|
| 1463 |
+
elif v == HttpVersion10:
|
| 1464 |
+
self.headers[hdrs.CONNECTION] = "keep-alive"
|
| 1465 |
+
|
| 1466 |
+
# status + headers
|
| 1467 |
+
status_line = f"{self.method} {path} HTTP/{v.major}.{v.minor}"
|
| 1468 |
+
|
| 1469 |
+
# Buffer headers for potential coalescing with body
|
| 1470 |
+
await writer.write_headers(status_line, self.headers)
|
| 1471 |
+
|
| 1472 |
+
task: Optional["asyncio.Task[None]"]
|
| 1473 |
+
if self._body or self._continue is not None or protocol.writing_paused:
|
| 1474 |
+
coro = self.write_bytes(writer, conn, self._get_content_length())
|
| 1475 |
+
if sys.version_info >= (3, 12):
|
| 1476 |
+
# Optimization for Python 3.12, try to write
|
| 1477 |
+
# bytes immediately to avoid having to schedule
|
| 1478 |
+
# the task on the event loop.
|
| 1479 |
+
task = asyncio.Task(coro, loop=self.loop, eager_start=True)
|
| 1480 |
+
else:
|
| 1481 |
+
task = self.loop.create_task(coro)
|
| 1482 |
+
if task.done():
|
| 1483 |
+
task = None
|
| 1484 |
+
else:
|
| 1485 |
+
self._writer = task
|
| 1486 |
+
else:
|
| 1487 |
+
# We have nothing to write because
|
| 1488 |
+
# - there is no body
|
| 1489 |
+
# - the protocol does not have writing paused
|
| 1490 |
+
# - we are not waiting for a 100-continue response
|
| 1491 |
+
protocol.start_timeout()
|
| 1492 |
+
writer.set_eof()
|
| 1493 |
+
task = None
|
| 1494 |
+
response_class = self.response_class
|
| 1495 |
+
assert response_class is not None
|
| 1496 |
+
self.response = response_class(
|
| 1497 |
+
self.method,
|
| 1498 |
+
self.original_url,
|
| 1499 |
+
writer=task,
|
| 1500 |
+
continue100=self._continue,
|
| 1501 |
+
timer=self._timer,
|
| 1502 |
+
request_info=self.request_info,
|
| 1503 |
+
traces=self._traces,
|
| 1504 |
+
loop=self.loop,
|
| 1505 |
+
session=self._session,
|
| 1506 |
+
)
|
| 1507 |
+
return self.response
|
| 1508 |
+
|
| 1509 |
+
async def close(self) -> None:
|
| 1510 |
+
if self.__writer is not None:
|
| 1511 |
+
try:
|
| 1512 |
+
await self.__writer
|
| 1513 |
+
except asyncio.CancelledError:
|
| 1514 |
+
if (
|
| 1515 |
+
sys.version_info >= (3, 11)
|
| 1516 |
+
and (task := asyncio.current_task())
|
| 1517 |
+
and task.cancelling()
|
| 1518 |
+
):
|
| 1519 |
+
raise
|
| 1520 |
+
|
| 1521 |
+
def terminate(self) -> None:
|
| 1522 |
+
if self.__writer is not None:
|
| 1523 |
+
if not self.loop.is_closed():
|
| 1524 |
+
self.__writer.cancel()
|
| 1525 |
+
self.__writer.remove_done_callback(self.__reset_writer)
|
| 1526 |
+
self.__writer = None
|
| 1527 |
+
|
| 1528 |
+
async def _on_chunk_request_sent(self, method: str, url: URL, chunk: bytes) -> None:
|
| 1529 |
+
for trace in self._traces:
|
| 1530 |
+
await trace.send_request_chunk_sent(method, url, chunk)
|
| 1531 |
+
|
| 1532 |
+
async def _on_headers_request_sent(
|
| 1533 |
+
self, method: str, url: URL, headers: "CIMultiDict[str]"
|
| 1534 |
+
) -> None:
|
| 1535 |
+
for trace in self._traces:
|
| 1536 |
+
await trace.send_request_headers(method, url, headers)
|
aiohttp/connector.py
ADDED
|
@@ -0,0 +1,1842 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import functools
|
| 3 |
+
import random
|
| 4 |
+
import socket
|
| 5 |
+
import sys
|
| 6 |
+
import traceback
|
| 7 |
+
import warnings
|
| 8 |
+
from collections import OrderedDict, defaultdict, deque
|
| 9 |
+
from contextlib import suppress
|
| 10 |
+
from http import HTTPStatus
|
| 11 |
+
from itertools import chain, cycle, islice
|
| 12 |
+
from time import monotonic
|
| 13 |
+
from types import TracebackType
|
| 14 |
+
from typing import (
|
| 15 |
+
TYPE_CHECKING,
|
| 16 |
+
Any,
|
| 17 |
+
Awaitable,
|
| 18 |
+
Callable,
|
| 19 |
+
DefaultDict,
|
| 20 |
+
Deque,
|
| 21 |
+
Dict,
|
| 22 |
+
Iterator,
|
| 23 |
+
List,
|
| 24 |
+
Literal,
|
| 25 |
+
Optional,
|
| 26 |
+
Sequence,
|
| 27 |
+
Set,
|
| 28 |
+
Tuple,
|
| 29 |
+
Type,
|
| 30 |
+
Union,
|
| 31 |
+
cast,
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
import aiohappyeyeballs
|
| 35 |
+
from aiohappyeyeballs import AddrInfoType, SocketFactoryType
|
| 36 |
+
|
| 37 |
+
from . import hdrs, helpers
|
| 38 |
+
from .abc import AbstractResolver, ResolveResult
|
| 39 |
+
from .client_exceptions import (
|
| 40 |
+
ClientConnectionError,
|
| 41 |
+
ClientConnectorCertificateError,
|
| 42 |
+
ClientConnectorDNSError,
|
| 43 |
+
ClientConnectorError,
|
| 44 |
+
ClientConnectorSSLError,
|
| 45 |
+
ClientHttpProxyError,
|
| 46 |
+
ClientProxyConnectionError,
|
| 47 |
+
ServerFingerprintMismatch,
|
| 48 |
+
UnixClientConnectorError,
|
| 49 |
+
cert_errors,
|
| 50 |
+
ssl_errors,
|
| 51 |
+
)
|
| 52 |
+
from .client_proto import ResponseHandler
|
| 53 |
+
from .client_reqrep import ClientRequest, Fingerprint, _merge_ssl_params
|
| 54 |
+
from .helpers import (
|
| 55 |
+
_SENTINEL,
|
| 56 |
+
ceil_timeout,
|
| 57 |
+
is_ip_address,
|
| 58 |
+
noop,
|
| 59 |
+
sentinel,
|
| 60 |
+
set_exception,
|
| 61 |
+
set_result,
|
| 62 |
+
)
|
| 63 |
+
from .log import client_logger
|
| 64 |
+
from .resolver import DefaultResolver
|
| 65 |
+
|
| 66 |
+
if sys.version_info >= (3, 12):
|
| 67 |
+
from collections.abc import Buffer
|
| 68 |
+
else:
|
| 69 |
+
Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"]
|
| 70 |
+
|
| 71 |
+
if TYPE_CHECKING:
|
| 72 |
+
import ssl
|
| 73 |
+
|
| 74 |
+
SSLContext = ssl.SSLContext
|
| 75 |
+
else:
|
| 76 |
+
try:
|
| 77 |
+
import ssl
|
| 78 |
+
|
| 79 |
+
SSLContext = ssl.SSLContext
|
| 80 |
+
except ImportError: # pragma: no cover
|
| 81 |
+
ssl = None # type: ignore[assignment]
|
| 82 |
+
SSLContext = object # type: ignore[misc,assignment]
|
| 83 |
+
|
| 84 |
+
EMPTY_SCHEMA_SET = frozenset({""})
|
| 85 |
+
HTTP_SCHEMA_SET = frozenset({"http", "https"})
|
| 86 |
+
WS_SCHEMA_SET = frozenset({"ws", "wss"})
|
| 87 |
+
|
| 88 |
+
HTTP_AND_EMPTY_SCHEMA_SET = HTTP_SCHEMA_SET | EMPTY_SCHEMA_SET
|
| 89 |
+
HIGH_LEVEL_SCHEMA_SET = HTTP_AND_EMPTY_SCHEMA_SET | WS_SCHEMA_SET
|
| 90 |
+
|
| 91 |
+
NEEDS_CLEANUP_CLOSED = (3, 13, 0) <= sys.version_info < (
|
| 92 |
+
3,
|
| 93 |
+
13,
|
| 94 |
+
1,
|
| 95 |
+
) or sys.version_info < (3, 12, 7)
|
| 96 |
+
# Cleanup closed is no longer needed after https://github.com/python/cpython/pull/118960
|
| 97 |
+
# which first appeared in Python 3.12.7 and 3.13.1
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
__all__ = (
|
| 101 |
+
"BaseConnector",
|
| 102 |
+
"TCPConnector",
|
| 103 |
+
"UnixConnector",
|
| 104 |
+
"NamedPipeConnector",
|
| 105 |
+
"AddrInfoType",
|
| 106 |
+
"SocketFactoryType",
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
if TYPE_CHECKING:
|
| 111 |
+
from .client import ClientTimeout
|
| 112 |
+
from .client_reqrep import ConnectionKey
|
| 113 |
+
from .tracing import Trace
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
class _DeprecationWaiter:
|
| 117 |
+
__slots__ = ("_awaitable", "_awaited")
|
| 118 |
+
|
| 119 |
+
def __init__(self, awaitable: Awaitable[Any]) -> None:
|
| 120 |
+
self._awaitable = awaitable
|
| 121 |
+
self._awaited = False
|
| 122 |
+
|
| 123 |
+
def __await__(self) -> Any:
|
| 124 |
+
self._awaited = True
|
| 125 |
+
return self._awaitable.__await__()
|
| 126 |
+
|
| 127 |
+
def __del__(self) -> None:
|
| 128 |
+
if not self._awaited:
|
| 129 |
+
warnings.warn(
|
| 130 |
+
"Connector.close() is a coroutine, "
|
| 131 |
+
"please use await connector.close()",
|
| 132 |
+
DeprecationWarning,
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
async def _wait_for_close(waiters: List[Awaitable[object]]) -> None:
|
| 137 |
+
"""Wait for all waiters to finish closing."""
|
| 138 |
+
results = await asyncio.gather(*waiters, return_exceptions=True)
|
| 139 |
+
for res in results:
|
| 140 |
+
if isinstance(res, Exception):
|
| 141 |
+
client_logger.debug("Error while closing connector: %r", res)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class Connection:
|
| 145 |
+
|
| 146 |
+
_source_traceback = None
|
| 147 |
+
|
| 148 |
+
def __init__(
|
| 149 |
+
self,
|
| 150 |
+
connector: "BaseConnector",
|
| 151 |
+
key: "ConnectionKey",
|
| 152 |
+
protocol: ResponseHandler,
|
| 153 |
+
loop: asyncio.AbstractEventLoop,
|
| 154 |
+
) -> None:
|
| 155 |
+
self._key = key
|
| 156 |
+
self._connector = connector
|
| 157 |
+
self._loop = loop
|
| 158 |
+
self._protocol: Optional[ResponseHandler] = protocol
|
| 159 |
+
self._callbacks: List[Callable[[], None]] = []
|
| 160 |
+
|
| 161 |
+
if loop.get_debug():
|
| 162 |
+
self._source_traceback = traceback.extract_stack(sys._getframe(1))
|
| 163 |
+
|
| 164 |
+
def __repr__(self) -> str:
|
| 165 |
+
return f"Connection<{self._key}>"
|
| 166 |
+
|
| 167 |
+
def __del__(self, _warnings: Any = warnings) -> None:
|
| 168 |
+
if self._protocol is not None:
|
| 169 |
+
kwargs = {"source": self}
|
| 170 |
+
_warnings.warn(f"Unclosed connection {self!r}", ResourceWarning, **kwargs)
|
| 171 |
+
if self._loop.is_closed():
|
| 172 |
+
return
|
| 173 |
+
|
| 174 |
+
self._connector._release(self._key, self._protocol, should_close=True)
|
| 175 |
+
|
| 176 |
+
context = {"client_connection": self, "message": "Unclosed connection"}
|
| 177 |
+
if self._source_traceback is not None:
|
| 178 |
+
context["source_traceback"] = self._source_traceback
|
| 179 |
+
self._loop.call_exception_handler(context)
|
| 180 |
+
|
| 181 |
+
def __bool__(self) -> Literal[True]:
|
| 182 |
+
"""Force subclasses to not be falsy, to make checks simpler."""
|
| 183 |
+
return True
|
| 184 |
+
|
| 185 |
+
@property
|
| 186 |
+
def loop(self) -> asyncio.AbstractEventLoop:
|
| 187 |
+
warnings.warn(
|
| 188 |
+
"connector.loop property is deprecated", DeprecationWarning, stacklevel=2
|
| 189 |
+
)
|
| 190 |
+
return self._loop
|
| 191 |
+
|
| 192 |
+
@property
|
| 193 |
+
def transport(self) -> Optional[asyncio.Transport]:
|
| 194 |
+
if self._protocol is None:
|
| 195 |
+
return None
|
| 196 |
+
return self._protocol.transport
|
| 197 |
+
|
| 198 |
+
@property
|
| 199 |
+
def protocol(self) -> Optional[ResponseHandler]:
|
| 200 |
+
return self._protocol
|
| 201 |
+
|
| 202 |
+
def add_callback(self, callback: Callable[[], None]) -> None:
|
| 203 |
+
if callback is not None:
|
| 204 |
+
self._callbacks.append(callback)
|
| 205 |
+
|
| 206 |
+
def _notify_release(self) -> None:
|
| 207 |
+
callbacks, self._callbacks = self._callbacks[:], []
|
| 208 |
+
|
| 209 |
+
for cb in callbacks:
|
| 210 |
+
with suppress(Exception):
|
| 211 |
+
cb()
|
| 212 |
+
|
| 213 |
+
def close(self) -> None:
|
| 214 |
+
self._notify_release()
|
| 215 |
+
|
| 216 |
+
if self._protocol is not None:
|
| 217 |
+
self._connector._release(self._key, self._protocol, should_close=True)
|
| 218 |
+
self._protocol = None
|
| 219 |
+
|
| 220 |
+
def release(self) -> None:
|
| 221 |
+
self._notify_release()
|
| 222 |
+
|
| 223 |
+
if self._protocol is not None:
|
| 224 |
+
self._connector._release(self._key, self._protocol)
|
| 225 |
+
self._protocol = None
|
| 226 |
+
|
| 227 |
+
@property
|
| 228 |
+
def closed(self) -> bool:
|
| 229 |
+
return self._protocol is None or not self._protocol.is_connected()
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
class _ConnectTunnelConnection(Connection):
|
| 233 |
+
"""Special connection wrapper for CONNECT tunnels that must never be pooled.
|
| 234 |
+
|
| 235 |
+
This connection wraps the proxy connection that will be upgraded with TLS.
|
| 236 |
+
It must never be released to the pool because:
|
| 237 |
+
1. Its 'closed' future will never complete, causing session.close() to hang
|
| 238 |
+
2. It represents an intermediate state, not a reusable connection
|
| 239 |
+
3. The real connection (with TLS) will be created separately
|
| 240 |
+
"""
|
| 241 |
+
|
| 242 |
+
def release(self) -> None:
|
| 243 |
+
"""Do nothing - don't pool or close the connection.
|
| 244 |
+
|
| 245 |
+
These connections are an intermediate state during the CONNECT tunnel
|
| 246 |
+
setup and will be cleaned up naturally after the TLS upgrade. If they
|
| 247 |
+
were to be pooled, they would never be properly closed, causing
|
| 248 |
+
session.close() to wait forever for their 'closed' future.
|
| 249 |
+
"""
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
class _TransportPlaceholder:
|
| 253 |
+
"""placeholder for BaseConnector.connect function"""
|
| 254 |
+
|
| 255 |
+
__slots__ = ("closed", "transport")
|
| 256 |
+
|
| 257 |
+
def __init__(self, closed_future: asyncio.Future[Optional[Exception]]) -> None:
|
| 258 |
+
"""Initialize a placeholder for a transport."""
|
| 259 |
+
self.closed = closed_future
|
| 260 |
+
self.transport = None
|
| 261 |
+
|
| 262 |
+
def close(self) -> None:
|
| 263 |
+
"""Close the placeholder."""
|
| 264 |
+
|
| 265 |
+
def abort(self) -> None:
|
| 266 |
+
"""Abort the placeholder (does nothing)."""
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
class BaseConnector:
|
| 270 |
+
"""Base connector class.
|
| 271 |
+
|
| 272 |
+
keepalive_timeout - (optional) Keep-alive timeout.
|
| 273 |
+
force_close - Set to True to force close and do reconnect
|
| 274 |
+
after each request (and between redirects).
|
| 275 |
+
limit - The total number of simultaneous connections.
|
| 276 |
+
limit_per_host - Number of simultaneous connections to one host.
|
| 277 |
+
enable_cleanup_closed - Enables clean-up closed ssl transports.
|
| 278 |
+
Disabled by default.
|
| 279 |
+
timeout_ceil_threshold - Trigger ceiling of timeout values when
|
| 280 |
+
it's above timeout_ceil_threshold.
|
| 281 |
+
loop - Optional event loop.
|
| 282 |
+
"""
|
| 283 |
+
|
| 284 |
+
_closed = True # prevent AttributeError in __del__ if ctor was failed
|
| 285 |
+
_source_traceback = None
|
| 286 |
+
|
| 287 |
+
# abort transport after 2 seconds (cleanup broken connections)
|
| 288 |
+
_cleanup_closed_period = 2.0
|
| 289 |
+
|
| 290 |
+
allowed_protocol_schema_set = HIGH_LEVEL_SCHEMA_SET
|
| 291 |
+
|
| 292 |
+
def __init__(
|
| 293 |
+
self,
|
| 294 |
+
*,
|
| 295 |
+
keepalive_timeout: Union[object, None, float] = sentinel,
|
| 296 |
+
force_close: bool = False,
|
| 297 |
+
limit: int = 100,
|
| 298 |
+
limit_per_host: int = 0,
|
| 299 |
+
enable_cleanup_closed: bool = False,
|
| 300 |
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
| 301 |
+
timeout_ceil_threshold: float = 5,
|
| 302 |
+
) -> None:
|
| 303 |
+
|
| 304 |
+
if force_close:
|
| 305 |
+
if keepalive_timeout is not None and keepalive_timeout is not sentinel:
|
| 306 |
+
raise ValueError(
|
| 307 |
+
"keepalive_timeout cannot be set if force_close is True"
|
| 308 |
+
)
|
| 309 |
+
else:
|
| 310 |
+
if keepalive_timeout is sentinel:
|
| 311 |
+
keepalive_timeout = 15.0
|
| 312 |
+
|
| 313 |
+
loop = loop or asyncio.get_running_loop()
|
| 314 |
+
self._timeout_ceil_threshold = timeout_ceil_threshold
|
| 315 |
+
|
| 316 |
+
self._closed = False
|
| 317 |
+
if loop.get_debug():
|
| 318 |
+
self._source_traceback = traceback.extract_stack(sys._getframe(1))
|
| 319 |
+
|
| 320 |
+
# Connection pool of reusable connections.
|
| 321 |
+
# We use a deque to store connections because it has O(1) popleft()
|
| 322 |
+
# and O(1) append() operations to implement a FIFO queue.
|
| 323 |
+
self._conns: DefaultDict[
|
| 324 |
+
ConnectionKey, Deque[Tuple[ResponseHandler, float]]
|
| 325 |
+
] = defaultdict(deque)
|
| 326 |
+
self._limit = limit
|
| 327 |
+
self._limit_per_host = limit_per_host
|
| 328 |
+
self._acquired: Set[ResponseHandler] = set()
|
| 329 |
+
self._acquired_per_host: DefaultDict[ConnectionKey, Set[ResponseHandler]] = (
|
| 330 |
+
defaultdict(set)
|
| 331 |
+
)
|
| 332 |
+
self._keepalive_timeout = cast(float, keepalive_timeout)
|
| 333 |
+
self._force_close = force_close
|
| 334 |
+
|
| 335 |
+
# {host_key: FIFO list of waiters}
|
| 336 |
+
# The FIFO is implemented with an OrderedDict with None keys because
|
| 337 |
+
# python does not have an ordered set.
|
| 338 |
+
self._waiters: DefaultDict[
|
| 339 |
+
ConnectionKey, OrderedDict[asyncio.Future[None], None]
|
| 340 |
+
] = defaultdict(OrderedDict)
|
| 341 |
+
|
| 342 |
+
self._loop = loop
|
| 343 |
+
self._factory = functools.partial(ResponseHandler, loop=loop)
|
| 344 |
+
|
| 345 |
+
# start keep-alive connection cleanup task
|
| 346 |
+
self._cleanup_handle: Optional[asyncio.TimerHandle] = None
|
| 347 |
+
|
| 348 |
+
# start cleanup closed transports task
|
| 349 |
+
self._cleanup_closed_handle: Optional[asyncio.TimerHandle] = None
|
| 350 |
+
|
| 351 |
+
if enable_cleanup_closed and not NEEDS_CLEANUP_CLOSED:
|
| 352 |
+
warnings.warn(
|
| 353 |
+
"enable_cleanup_closed ignored because "
|
| 354 |
+
"https://github.com/python/cpython/pull/118960 is fixed "
|
| 355 |
+
f"in Python version {sys.version_info}",
|
| 356 |
+
DeprecationWarning,
|
| 357 |
+
stacklevel=2,
|
| 358 |
+
)
|
| 359 |
+
enable_cleanup_closed = False
|
| 360 |
+
|
| 361 |
+
self._cleanup_closed_disabled = not enable_cleanup_closed
|
| 362 |
+
self._cleanup_closed_transports: List[Optional[asyncio.Transport]] = []
|
| 363 |
+
self._placeholder_future: asyncio.Future[Optional[Exception]] = (
|
| 364 |
+
loop.create_future()
|
| 365 |
+
)
|
| 366 |
+
self._placeholder_future.set_result(None)
|
| 367 |
+
self._cleanup_closed()
|
| 368 |
+
|
| 369 |
+
def __del__(self, _warnings: Any = warnings) -> None:
|
| 370 |
+
if self._closed:
|
| 371 |
+
return
|
| 372 |
+
if not self._conns:
|
| 373 |
+
return
|
| 374 |
+
|
| 375 |
+
conns = [repr(c) for c in self._conns.values()]
|
| 376 |
+
|
| 377 |
+
self._close()
|
| 378 |
+
|
| 379 |
+
kwargs = {"source": self}
|
| 380 |
+
_warnings.warn(f"Unclosed connector {self!r}", ResourceWarning, **kwargs)
|
| 381 |
+
context = {
|
| 382 |
+
"connector": self,
|
| 383 |
+
"connections": conns,
|
| 384 |
+
"message": "Unclosed connector",
|
| 385 |
+
}
|
| 386 |
+
if self._source_traceback is not None:
|
| 387 |
+
context["source_traceback"] = self._source_traceback
|
| 388 |
+
self._loop.call_exception_handler(context)
|
| 389 |
+
|
| 390 |
+
def __enter__(self) -> "BaseConnector":
|
| 391 |
+
warnings.warn(
|
| 392 |
+
'"with Connector():" is deprecated, '
|
| 393 |
+
'use "async with Connector():" instead',
|
| 394 |
+
DeprecationWarning,
|
| 395 |
+
)
|
| 396 |
+
return self
|
| 397 |
+
|
| 398 |
+
def __exit__(self, *exc: Any) -> None:
|
| 399 |
+
self._close()
|
| 400 |
+
|
| 401 |
+
async def __aenter__(self) -> "BaseConnector":
|
| 402 |
+
return self
|
| 403 |
+
|
| 404 |
+
async def __aexit__(
|
| 405 |
+
self,
|
| 406 |
+
exc_type: Optional[Type[BaseException]] = None,
|
| 407 |
+
exc_value: Optional[BaseException] = None,
|
| 408 |
+
exc_traceback: Optional[TracebackType] = None,
|
| 409 |
+
) -> None:
|
| 410 |
+
await self.close()
|
| 411 |
+
|
| 412 |
+
@property
|
| 413 |
+
def force_close(self) -> bool:
|
| 414 |
+
"""Ultimately close connection on releasing if True."""
|
| 415 |
+
return self._force_close
|
| 416 |
+
|
| 417 |
+
@property
|
| 418 |
+
def limit(self) -> int:
|
| 419 |
+
"""The total number for simultaneous connections.
|
| 420 |
+
|
| 421 |
+
If limit is 0 the connector has no limit.
|
| 422 |
+
The default limit size is 100.
|
| 423 |
+
"""
|
| 424 |
+
return self._limit
|
| 425 |
+
|
| 426 |
+
@property
|
| 427 |
+
def limit_per_host(self) -> int:
|
| 428 |
+
"""The limit for simultaneous connections to the same endpoint.
|
| 429 |
+
|
| 430 |
+
Endpoints are the same if they are have equal
|
| 431 |
+
(host, port, is_ssl) triple.
|
| 432 |
+
"""
|
| 433 |
+
return self._limit_per_host
|
| 434 |
+
|
| 435 |
+
def _cleanup(self) -> None:
|
| 436 |
+
"""Cleanup unused transports."""
|
| 437 |
+
if self._cleanup_handle:
|
| 438 |
+
self._cleanup_handle.cancel()
|
| 439 |
+
# _cleanup_handle should be unset, otherwise _release() will not
|
| 440 |
+
# recreate it ever!
|
| 441 |
+
self._cleanup_handle = None
|
| 442 |
+
|
| 443 |
+
now = monotonic()
|
| 444 |
+
timeout = self._keepalive_timeout
|
| 445 |
+
|
| 446 |
+
if self._conns:
|
| 447 |
+
connections = defaultdict(deque)
|
| 448 |
+
deadline = now - timeout
|
| 449 |
+
for key, conns in self._conns.items():
|
| 450 |
+
alive: Deque[Tuple[ResponseHandler, float]] = deque()
|
| 451 |
+
for proto, use_time in conns:
|
| 452 |
+
if proto.is_connected() and use_time - deadline >= 0:
|
| 453 |
+
alive.append((proto, use_time))
|
| 454 |
+
continue
|
| 455 |
+
transport = proto.transport
|
| 456 |
+
proto.close()
|
| 457 |
+
if not self._cleanup_closed_disabled and key.is_ssl:
|
| 458 |
+
self._cleanup_closed_transports.append(transport)
|
| 459 |
+
|
| 460 |
+
if alive:
|
| 461 |
+
connections[key] = alive
|
| 462 |
+
|
| 463 |
+
self._conns = connections
|
| 464 |
+
|
| 465 |
+
if self._conns:
|
| 466 |
+
self._cleanup_handle = helpers.weakref_handle(
|
| 467 |
+
self,
|
| 468 |
+
"_cleanup",
|
| 469 |
+
timeout,
|
| 470 |
+
self._loop,
|
| 471 |
+
timeout_ceil_threshold=self._timeout_ceil_threshold,
|
| 472 |
+
)
|
| 473 |
+
|
| 474 |
+
def _cleanup_closed(self) -> None:
|
| 475 |
+
"""Double confirmation for transport close.
|
| 476 |
+
|
| 477 |
+
Some broken ssl servers may leave socket open without proper close.
|
| 478 |
+
"""
|
| 479 |
+
if self._cleanup_closed_handle:
|
| 480 |
+
self._cleanup_closed_handle.cancel()
|
| 481 |
+
|
| 482 |
+
for transport in self._cleanup_closed_transports:
|
| 483 |
+
if transport is not None:
|
| 484 |
+
transport.abort()
|
| 485 |
+
|
| 486 |
+
self._cleanup_closed_transports = []
|
| 487 |
+
|
| 488 |
+
if not self._cleanup_closed_disabled:
|
| 489 |
+
self._cleanup_closed_handle = helpers.weakref_handle(
|
| 490 |
+
self,
|
| 491 |
+
"_cleanup_closed",
|
| 492 |
+
self._cleanup_closed_period,
|
| 493 |
+
self._loop,
|
| 494 |
+
timeout_ceil_threshold=self._timeout_ceil_threshold,
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
def close(self, *, abort_ssl: bool = False) -> Awaitable[None]:
|
| 498 |
+
"""Close all opened transports.
|
| 499 |
+
|
| 500 |
+
:param abort_ssl: If True, SSL connections will be aborted immediately
|
| 501 |
+
without performing the shutdown handshake. This provides
|
| 502 |
+
faster cleanup at the cost of less graceful disconnection.
|
| 503 |
+
"""
|
| 504 |
+
if not (waiters := self._close(abort_ssl=abort_ssl)):
|
| 505 |
+
# If there are no connections to close, we can return a noop
|
| 506 |
+
# awaitable to avoid scheduling a task on the event loop.
|
| 507 |
+
return _DeprecationWaiter(noop())
|
| 508 |
+
coro = _wait_for_close(waiters)
|
| 509 |
+
if sys.version_info >= (3, 12):
|
| 510 |
+
# Optimization for Python 3.12, try to close connections
|
| 511 |
+
# immediately to avoid having to schedule the task on the event loop.
|
| 512 |
+
task = asyncio.Task(coro, loop=self._loop, eager_start=True)
|
| 513 |
+
else:
|
| 514 |
+
task = self._loop.create_task(coro)
|
| 515 |
+
return _DeprecationWaiter(task)
|
| 516 |
+
|
| 517 |
+
def _close(self, *, abort_ssl: bool = False) -> List[Awaitable[object]]:
|
| 518 |
+
waiters: List[Awaitable[object]] = []
|
| 519 |
+
|
| 520 |
+
if self._closed:
|
| 521 |
+
return waiters
|
| 522 |
+
|
| 523 |
+
self._closed = True
|
| 524 |
+
|
| 525 |
+
try:
|
| 526 |
+
if self._loop.is_closed():
|
| 527 |
+
return waiters
|
| 528 |
+
|
| 529 |
+
# cancel cleanup task
|
| 530 |
+
if self._cleanup_handle:
|
| 531 |
+
self._cleanup_handle.cancel()
|
| 532 |
+
|
| 533 |
+
# cancel cleanup close task
|
| 534 |
+
if self._cleanup_closed_handle:
|
| 535 |
+
self._cleanup_closed_handle.cancel()
|
| 536 |
+
|
| 537 |
+
for data in self._conns.values():
|
| 538 |
+
for proto, _ in data:
|
| 539 |
+
if (
|
| 540 |
+
abort_ssl
|
| 541 |
+
and proto.transport
|
| 542 |
+
and proto.transport.get_extra_info("sslcontext") is not None
|
| 543 |
+
):
|
| 544 |
+
proto.abort()
|
| 545 |
+
else:
|
| 546 |
+
proto.close()
|
| 547 |
+
if closed := proto.closed:
|
| 548 |
+
waiters.append(closed)
|
| 549 |
+
|
| 550 |
+
for proto in self._acquired:
|
| 551 |
+
if (
|
| 552 |
+
abort_ssl
|
| 553 |
+
and proto.transport
|
| 554 |
+
and proto.transport.get_extra_info("sslcontext") is not None
|
| 555 |
+
):
|
| 556 |
+
proto.abort()
|
| 557 |
+
else:
|
| 558 |
+
proto.close()
|
| 559 |
+
if closed := proto.closed:
|
| 560 |
+
waiters.append(closed)
|
| 561 |
+
|
| 562 |
+
for transport in self._cleanup_closed_transports:
|
| 563 |
+
if transport is not None:
|
| 564 |
+
transport.abort()
|
| 565 |
+
|
| 566 |
+
return waiters
|
| 567 |
+
|
| 568 |
+
finally:
|
| 569 |
+
self._conns.clear()
|
| 570 |
+
self._acquired.clear()
|
| 571 |
+
for keyed_waiters in self._waiters.values():
|
| 572 |
+
for keyed_waiter in keyed_waiters:
|
| 573 |
+
keyed_waiter.cancel()
|
| 574 |
+
self._waiters.clear()
|
| 575 |
+
self._cleanup_handle = None
|
| 576 |
+
self._cleanup_closed_transports.clear()
|
| 577 |
+
self._cleanup_closed_handle = None
|
| 578 |
+
|
| 579 |
+
@property
|
| 580 |
+
def closed(self) -> bool:
|
| 581 |
+
"""Is connector closed.
|
| 582 |
+
|
| 583 |
+
A readonly property.
|
| 584 |
+
"""
|
| 585 |
+
return self._closed
|
| 586 |
+
|
| 587 |
+
def _available_connections(self, key: "ConnectionKey") -> int:
|
| 588 |
+
"""
|
| 589 |
+
Return number of available connections.
|
| 590 |
+
|
| 591 |
+
The limit, limit_per_host and the connection key are taken into account.
|
| 592 |
+
|
| 593 |
+
If it returns less than 1 means that there are no connections
|
| 594 |
+
available.
|
| 595 |
+
"""
|
| 596 |
+
# check total available connections
|
| 597 |
+
# If there are no limits, this will always return 1
|
| 598 |
+
total_remain = 1
|
| 599 |
+
|
| 600 |
+
if self._limit and (total_remain := self._limit - len(self._acquired)) <= 0:
|
| 601 |
+
return total_remain
|
| 602 |
+
|
| 603 |
+
# check limit per host
|
| 604 |
+
if host_remain := self._limit_per_host:
|
| 605 |
+
if acquired := self._acquired_per_host.get(key):
|
| 606 |
+
host_remain -= len(acquired)
|
| 607 |
+
if total_remain > host_remain:
|
| 608 |
+
return host_remain
|
| 609 |
+
|
| 610 |
+
return total_remain
|
| 611 |
+
|
| 612 |
+
def _update_proxy_auth_header_and_build_proxy_req(
|
| 613 |
+
self, req: ClientRequest
|
| 614 |
+
) -> ClientRequest:
|
| 615 |
+
"""Set Proxy-Authorization header for non-SSL proxy requests and builds the proxy request for SSL proxy requests."""
|
| 616 |
+
url = req.proxy
|
| 617 |
+
assert url is not None
|
| 618 |
+
headers: Dict[str, str] = {}
|
| 619 |
+
if req.proxy_headers is not None:
|
| 620 |
+
headers = req.proxy_headers # type: ignore[assignment]
|
| 621 |
+
headers[hdrs.HOST] = req.headers[hdrs.HOST]
|
| 622 |
+
proxy_req = ClientRequest(
|
| 623 |
+
hdrs.METH_GET,
|
| 624 |
+
url,
|
| 625 |
+
headers=headers,
|
| 626 |
+
auth=req.proxy_auth,
|
| 627 |
+
loop=self._loop,
|
| 628 |
+
ssl=req.ssl,
|
| 629 |
+
)
|
| 630 |
+
auth = proxy_req.headers.pop(hdrs.AUTHORIZATION, None)
|
| 631 |
+
if auth is not None:
|
| 632 |
+
if not req.is_ssl():
|
| 633 |
+
req.headers[hdrs.PROXY_AUTHORIZATION] = auth
|
| 634 |
+
else:
|
| 635 |
+
proxy_req.headers[hdrs.PROXY_AUTHORIZATION] = auth
|
| 636 |
+
return proxy_req
|
| 637 |
+
|
| 638 |
+
async def connect(
|
| 639 |
+
self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout"
|
| 640 |
+
) -> Connection:
|
| 641 |
+
"""Get from pool or create new connection."""
|
| 642 |
+
key = req.connection_key
|
| 643 |
+
if (conn := await self._get(key, traces)) is not None:
|
| 644 |
+
# If we do not have to wait and we can get a connection from the pool
|
| 645 |
+
# we can avoid the timeout ceil logic and directly return the connection
|
| 646 |
+
if req.proxy:
|
| 647 |
+
self._update_proxy_auth_header_and_build_proxy_req(req)
|
| 648 |
+
return conn
|
| 649 |
+
|
| 650 |
+
async with ceil_timeout(timeout.connect, timeout.ceil_threshold):
|
| 651 |
+
if self._available_connections(key) <= 0:
|
| 652 |
+
await self._wait_for_available_connection(key, traces)
|
| 653 |
+
if (conn := await self._get(key, traces)) is not None:
|
| 654 |
+
if req.proxy:
|
| 655 |
+
self._update_proxy_auth_header_and_build_proxy_req(req)
|
| 656 |
+
return conn
|
| 657 |
+
|
| 658 |
+
placeholder = cast(
|
| 659 |
+
ResponseHandler, _TransportPlaceholder(self._placeholder_future)
|
| 660 |
+
)
|
| 661 |
+
self._acquired.add(placeholder)
|
| 662 |
+
if self._limit_per_host:
|
| 663 |
+
self._acquired_per_host[key].add(placeholder)
|
| 664 |
+
|
| 665 |
+
try:
|
| 666 |
+
# Traces are done inside the try block to ensure that the
|
| 667 |
+
# that the placeholder is still cleaned up if an exception
|
| 668 |
+
# is raised.
|
| 669 |
+
if traces:
|
| 670 |
+
for trace in traces:
|
| 671 |
+
await trace.send_connection_create_start()
|
| 672 |
+
proto = await self._create_connection(req, traces, timeout)
|
| 673 |
+
if traces:
|
| 674 |
+
for trace in traces:
|
| 675 |
+
await trace.send_connection_create_end()
|
| 676 |
+
except BaseException:
|
| 677 |
+
self._release_acquired(key, placeholder)
|
| 678 |
+
raise
|
| 679 |
+
else:
|
| 680 |
+
if self._closed:
|
| 681 |
+
proto.close()
|
| 682 |
+
raise ClientConnectionError("Connector is closed.")
|
| 683 |
+
|
| 684 |
+
# The connection was successfully created, drop the placeholder
|
| 685 |
+
# and add the real connection to the acquired set. There should
|
| 686 |
+
# be no awaits after the proto is added to the acquired set
|
| 687 |
+
# to ensure that the connection is not left in the acquired set
|
| 688 |
+
# on cancellation.
|
| 689 |
+
self._acquired.remove(placeholder)
|
| 690 |
+
self._acquired.add(proto)
|
| 691 |
+
if self._limit_per_host:
|
| 692 |
+
acquired_per_host = self._acquired_per_host[key]
|
| 693 |
+
acquired_per_host.remove(placeholder)
|
| 694 |
+
acquired_per_host.add(proto)
|
| 695 |
+
return Connection(self, key, proto, self._loop)
|
| 696 |
+
|
| 697 |
+
async def _wait_for_available_connection(
|
| 698 |
+
self, key: "ConnectionKey", traces: List["Trace"]
|
| 699 |
+
) -> None:
|
| 700 |
+
"""Wait for an available connection slot."""
|
| 701 |
+
# We loop here because there is a race between
|
| 702 |
+
# the connection limit check and the connection
|
| 703 |
+
# being acquired. If the connection is acquired
|
| 704 |
+
# between the check and the await statement, we
|
| 705 |
+
# need to loop again to check if the connection
|
| 706 |
+
# slot is still available.
|
| 707 |
+
attempts = 0
|
| 708 |
+
while True:
|
| 709 |
+
fut: asyncio.Future[None] = self._loop.create_future()
|
| 710 |
+
keyed_waiters = self._waiters[key]
|
| 711 |
+
keyed_waiters[fut] = None
|
| 712 |
+
if attempts:
|
| 713 |
+
# If we have waited before, we need to move the waiter
|
| 714 |
+
# to the front of the queue as otherwise we might get
|
| 715 |
+
# starved and hit the timeout.
|
| 716 |
+
keyed_waiters.move_to_end(fut, last=False)
|
| 717 |
+
|
| 718 |
+
try:
|
| 719 |
+
# Traces happen in the try block to ensure that the
|
| 720 |
+
# the waiter is still cleaned up if an exception is raised.
|
| 721 |
+
if traces:
|
| 722 |
+
for trace in traces:
|
| 723 |
+
await trace.send_connection_queued_start()
|
| 724 |
+
await fut
|
| 725 |
+
if traces:
|
| 726 |
+
for trace in traces:
|
| 727 |
+
await trace.send_connection_queued_end()
|
| 728 |
+
finally:
|
| 729 |
+
# pop the waiter from the queue if its still
|
| 730 |
+
# there and not already removed by _release_waiter
|
| 731 |
+
keyed_waiters.pop(fut, None)
|
| 732 |
+
if not self._waiters.get(key, True):
|
| 733 |
+
del self._waiters[key]
|
| 734 |
+
|
| 735 |
+
if self._available_connections(key) > 0:
|
| 736 |
+
break
|
| 737 |
+
attempts += 1
|
| 738 |
+
|
| 739 |
+
async def _get(
|
| 740 |
+
self, key: "ConnectionKey", traces: List["Trace"]
|
| 741 |
+
) -> Optional[Connection]:
|
| 742 |
+
"""Get next reusable connection for the key or None.
|
| 743 |
+
|
| 744 |
+
The connection will be marked as acquired.
|
| 745 |
+
"""
|
| 746 |
+
if (conns := self._conns.get(key)) is None:
|
| 747 |
+
return None
|
| 748 |
+
|
| 749 |
+
t1 = monotonic()
|
| 750 |
+
while conns:
|
| 751 |
+
proto, t0 = conns.popleft()
|
| 752 |
+
# We will we reuse the connection if its connected and
|
| 753 |
+
# the keepalive timeout has not been exceeded
|
| 754 |
+
if proto.is_connected() and t1 - t0 <= self._keepalive_timeout:
|
| 755 |
+
if not conns:
|
| 756 |
+
# The very last connection was reclaimed: drop the key
|
| 757 |
+
del self._conns[key]
|
| 758 |
+
self._acquired.add(proto)
|
| 759 |
+
if self._limit_per_host:
|
| 760 |
+
self._acquired_per_host[key].add(proto)
|
| 761 |
+
if traces:
|
| 762 |
+
for trace in traces:
|
| 763 |
+
try:
|
| 764 |
+
await trace.send_connection_reuseconn()
|
| 765 |
+
except BaseException:
|
| 766 |
+
self._release_acquired(key, proto)
|
| 767 |
+
raise
|
| 768 |
+
return Connection(self, key, proto, self._loop)
|
| 769 |
+
|
| 770 |
+
# Connection cannot be reused, close it
|
| 771 |
+
transport = proto.transport
|
| 772 |
+
proto.close()
|
| 773 |
+
# only for SSL transports
|
| 774 |
+
if not self._cleanup_closed_disabled and key.is_ssl:
|
| 775 |
+
self._cleanup_closed_transports.append(transport)
|
| 776 |
+
|
| 777 |
+
# No more connections: drop the key
|
| 778 |
+
del self._conns[key]
|
| 779 |
+
return None
|
| 780 |
+
|
| 781 |
+
def _release_waiter(self) -> None:
|
| 782 |
+
"""
|
| 783 |
+
Iterates over all waiters until one to be released is found.
|
| 784 |
+
|
| 785 |
+
The one to be released is not finished and
|
| 786 |
+
belongs to a host that has available connections.
|
| 787 |
+
"""
|
| 788 |
+
if not self._waiters:
|
| 789 |
+
return
|
| 790 |
+
|
| 791 |
+
# Having the dict keys ordered this avoids to iterate
|
| 792 |
+
# at the same order at each call.
|
| 793 |
+
queues = list(self._waiters)
|
| 794 |
+
random.shuffle(queues)
|
| 795 |
+
|
| 796 |
+
for key in queues:
|
| 797 |
+
if self._available_connections(key) < 1:
|
| 798 |
+
continue
|
| 799 |
+
|
| 800 |
+
waiters = self._waiters[key]
|
| 801 |
+
while waiters:
|
| 802 |
+
waiter, _ = waiters.popitem(last=False)
|
| 803 |
+
if not waiter.done():
|
| 804 |
+
waiter.set_result(None)
|
| 805 |
+
return
|
| 806 |
+
|
| 807 |
+
def _release_acquired(self, key: "ConnectionKey", proto: ResponseHandler) -> None:
|
| 808 |
+
"""Release acquired connection."""
|
| 809 |
+
if self._closed:
|
| 810 |
+
# acquired connection is already released on connector closing
|
| 811 |
+
return
|
| 812 |
+
|
| 813 |
+
self._acquired.discard(proto)
|
| 814 |
+
if self._limit_per_host and (conns := self._acquired_per_host.get(key)):
|
| 815 |
+
conns.discard(proto)
|
| 816 |
+
if not conns:
|
| 817 |
+
del self._acquired_per_host[key]
|
| 818 |
+
self._release_waiter()
|
| 819 |
+
|
| 820 |
+
def _release(
|
| 821 |
+
self,
|
| 822 |
+
key: "ConnectionKey",
|
| 823 |
+
protocol: ResponseHandler,
|
| 824 |
+
*,
|
| 825 |
+
should_close: bool = False,
|
| 826 |
+
) -> None:
|
| 827 |
+
if self._closed:
|
| 828 |
+
# acquired connection is already released on connector closing
|
| 829 |
+
return
|
| 830 |
+
|
| 831 |
+
self._release_acquired(key, protocol)
|
| 832 |
+
|
| 833 |
+
if self._force_close or should_close or protocol.should_close:
|
| 834 |
+
transport = protocol.transport
|
| 835 |
+
protocol.close()
|
| 836 |
+
|
| 837 |
+
if key.is_ssl and not self._cleanup_closed_disabled:
|
| 838 |
+
self._cleanup_closed_transports.append(transport)
|
| 839 |
+
return
|
| 840 |
+
|
| 841 |
+
self._conns[key].append((protocol, monotonic()))
|
| 842 |
+
|
| 843 |
+
if self._cleanup_handle is None:
|
| 844 |
+
self._cleanup_handle = helpers.weakref_handle(
|
| 845 |
+
self,
|
| 846 |
+
"_cleanup",
|
| 847 |
+
self._keepalive_timeout,
|
| 848 |
+
self._loop,
|
| 849 |
+
timeout_ceil_threshold=self._timeout_ceil_threshold,
|
| 850 |
+
)
|
| 851 |
+
|
| 852 |
+
async def _create_connection(
|
| 853 |
+
self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout"
|
| 854 |
+
) -> ResponseHandler:
|
| 855 |
+
raise NotImplementedError()
|
| 856 |
+
|
| 857 |
+
|
| 858 |
+
class _DNSCacheTable:
|
| 859 |
+
def __init__(self, ttl: Optional[float] = None) -> None:
|
| 860 |
+
self._addrs_rr: Dict[Tuple[str, int], Tuple[Iterator[ResolveResult], int]] = {}
|
| 861 |
+
self._timestamps: Dict[Tuple[str, int], float] = {}
|
| 862 |
+
self._ttl = ttl
|
| 863 |
+
|
| 864 |
+
def __contains__(self, host: object) -> bool:
|
| 865 |
+
return host in self._addrs_rr
|
| 866 |
+
|
| 867 |
+
def add(self, key: Tuple[str, int], addrs: List[ResolveResult]) -> None:
|
| 868 |
+
self._addrs_rr[key] = (cycle(addrs), len(addrs))
|
| 869 |
+
|
| 870 |
+
if self._ttl is not None:
|
| 871 |
+
self._timestamps[key] = monotonic()
|
| 872 |
+
|
| 873 |
+
def remove(self, key: Tuple[str, int]) -> None:
|
| 874 |
+
self._addrs_rr.pop(key, None)
|
| 875 |
+
|
| 876 |
+
if self._ttl is not None:
|
| 877 |
+
self._timestamps.pop(key, None)
|
| 878 |
+
|
| 879 |
+
def clear(self) -> None:
|
| 880 |
+
self._addrs_rr.clear()
|
| 881 |
+
self._timestamps.clear()
|
| 882 |
+
|
| 883 |
+
def next_addrs(self, key: Tuple[str, int]) -> List[ResolveResult]:
|
| 884 |
+
loop, length = self._addrs_rr[key]
|
| 885 |
+
addrs = list(islice(loop, length))
|
| 886 |
+
# Consume one more element to shift internal state of `cycle`
|
| 887 |
+
next(loop)
|
| 888 |
+
return addrs
|
| 889 |
+
|
| 890 |
+
def expired(self, key: Tuple[str, int]) -> bool:
|
| 891 |
+
if self._ttl is None:
|
| 892 |
+
return False
|
| 893 |
+
|
| 894 |
+
return self._timestamps[key] + self._ttl < monotonic()
|
| 895 |
+
|
| 896 |
+
|
| 897 |
+
def _make_ssl_context(verified: bool) -> SSLContext:
|
| 898 |
+
"""Create SSL context.
|
| 899 |
+
|
| 900 |
+
This method is not async-friendly and should be called from a thread
|
| 901 |
+
because it will load certificates from disk and do other blocking I/O.
|
| 902 |
+
"""
|
| 903 |
+
if ssl is None:
|
| 904 |
+
# No ssl support
|
| 905 |
+
return None
|
| 906 |
+
if verified:
|
| 907 |
+
sslcontext = ssl.create_default_context()
|
| 908 |
+
else:
|
| 909 |
+
sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
| 910 |
+
sslcontext.options |= ssl.OP_NO_SSLv2
|
| 911 |
+
sslcontext.options |= ssl.OP_NO_SSLv3
|
| 912 |
+
sslcontext.check_hostname = False
|
| 913 |
+
sslcontext.verify_mode = ssl.CERT_NONE
|
| 914 |
+
sslcontext.options |= ssl.OP_NO_COMPRESSION
|
| 915 |
+
sslcontext.set_default_verify_paths()
|
| 916 |
+
sslcontext.set_alpn_protocols(("http/1.1",))
|
| 917 |
+
return sslcontext
|
| 918 |
+
|
| 919 |
+
|
| 920 |
+
# The default SSLContext objects are created at import time
|
| 921 |
+
# since they do blocking I/O to load certificates from disk,
|
| 922 |
+
# and imports should always be done before the event loop starts
|
| 923 |
+
# or in a thread.
|
| 924 |
+
_SSL_CONTEXT_VERIFIED = _make_ssl_context(True)
|
| 925 |
+
_SSL_CONTEXT_UNVERIFIED = _make_ssl_context(False)
|
| 926 |
+
|
| 927 |
+
|
| 928 |
+
class TCPConnector(BaseConnector):
|
| 929 |
+
"""TCP connector.
|
| 930 |
+
|
| 931 |
+
verify_ssl - Set to True to check ssl certifications.
|
| 932 |
+
fingerprint - Pass the binary sha256
|
| 933 |
+
digest of the expected certificate in DER format to verify
|
| 934 |
+
that the certificate the server presents matches. See also
|
| 935 |
+
https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning
|
| 936 |
+
resolver - Enable DNS lookups and use this
|
| 937 |
+
resolver
|
| 938 |
+
use_dns_cache - Use memory cache for DNS lookups.
|
| 939 |
+
ttl_dns_cache - Max seconds having cached a DNS entry, None forever.
|
| 940 |
+
family - socket address family
|
| 941 |
+
local_addr - local tuple of (host, port) to bind socket to
|
| 942 |
+
|
| 943 |
+
keepalive_timeout - (optional) Keep-alive timeout.
|
| 944 |
+
force_close - Set to True to force close and do reconnect
|
| 945 |
+
after each request (and between redirects).
|
| 946 |
+
limit - The total number of simultaneous connections.
|
| 947 |
+
limit_per_host - Number of simultaneous connections to one host.
|
| 948 |
+
enable_cleanup_closed - Enables clean-up closed ssl transports.
|
| 949 |
+
Disabled by default.
|
| 950 |
+
happy_eyeballs_delay - This is the “Connection Attempt Delay”
|
| 951 |
+
as defined in RFC 8305. To disable
|
| 952 |
+
the happy eyeballs algorithm, set to None.
|
| 953 |
+
interleave - “First Address Family Count” as defined in RFC 8305
|
| 954 |
+
loop - Optional event loop.
|
| 955 |
+
socket_factory - A SocketFactoryType function that, if supplied,
|
| 956 |
+
will be used to create sockets given an
|
| 957 |
+
AddrInfoType.
|
| 958 |
+
ssl_shutdown_timeout - DEPRECATED. Will be removed in aiohttp 4.0.
|
| 959 |
+
Grace period for SSL shutdown handshake on TLS
|
| 960 |
+
connections. Default is 0 seconds (immediate abort).
|
| 961 |
+
This parameter allowed for a clean SSL shutdown by
|
| 962 |
+
notifying the remote peer of connection closure,
|
| 963 |
+
while avoiding excessive delays during connector cleanup.
|
| 964 |
+
Note: Only takes effect on Python 3.11+.
|
| 965 |
+
"""
|
| 966 |
+
|
| 967 |
+
allowed_protocol_schema_set = HIGH_LEVEL_SCHEMA_SET | frozenset({"tcp"})
|
| 968 |
+
|
| 969 |
+
def __init__(
|
| 970 |
+
self,
|
| 971 |
+
*,
|
| 972 |
+
verify_ssl: bool = True,
|
| 973 |
+
fingerprint: Optional[bytes] = None,
|
| 974 |
+
use_dns_cache: bool = True,
|
| 975 |
+
ttl_dns_cache: Optional[int] = 10,
|
| 976 |
+
family: socket.AddressFamily = socket.AddressFamily.AF_UNSPEC,
|
| 977 |
+
ssl_context: Optional[SSLContext] = None,
|
| 978 |
+
ssl: Union[bool, Fingerprint, SSLContext] = True,
|
| 979 |
+
local_addr: Optional[Tuple[str, int]] = None,
|
| 980 |
+
resolver: Optional[AbstractResolver] = None,
|
| 981 |
+
keepalive_timeout: Union[None, float, object] = sentinel,
|
| 982 |
+
force_close: bool = False,
|
| 983 |
+
limit: int = 100,
|
| 984 |
+
limit_per_host: int = 0,
|
| 985 |
+
enable_cleanup_closed: bool = False,
|
| 986 |
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
| 987 |
+
timeout_ceil_threshold: float = 5,
|
| 988 |
+
happy_eyeballs_delay: Optional[float] = 0.25,
|
| 989 |
+
interleave: Optional[int] = None,
|
| 990 |
+
socket_factory: Optional[SocketFactoryType] = None,
|
| 991 |
+
ssl_shutdown_timeout: Union[_SENTINEL, None, float] = sentinel,
|
| 992 |
+
):
|
| 993 |
+
super().__init__(
|
| 994 |
+
keepalive_timeout=keepalive_timeout,
|
| 995 |
+
force_close=force_close,
|
| 996 |
+
limit=limit,
|
| 997 |
+
limit_per_host=limit_per_host,
|
| 998 |
+
enable_cleanup_closed=enable_cleanup_closed,
|
| 999 |
+
loop=loop,
|
| 1000 |
+
timeout_ceil_threshold=timeout_ceil_threshold,
|
| 1001 |
+
)
|
| 1002 |
+
|
| 1003 |
+
self._ssl = _merge_ssl_params(ssl, verify_ssl, ssl_context, fingerprint)
|
| 1004 |
+
|
| 1005 |
+
self._resolver: AbstractResolver
|
| 1006 |
+
if resolver is None:
|
| 1007 |
+
self._resolver = DefaultResolver(loop=self._loop)
|
| 1008 |
+
self._resolver_owner = True
|
| 1009 |
+
else:
|
| 1010 |
+
self._resolver = resolver
|
| 1011 |
+
self._resolver_owner = False
|
| 1012 |
+
|
| 1013 |
+
self._use_dns_cache = use_dns_cache
|
| 1014 |
+
self._cached_hosts = _DNSCacheTable(ttl=ttl_dns_cache)
|
| 1015 |
+
self._throttle_dns_futures: Dict[
|
| 1016 |
+
Tuple[str, int], Set["asyncio.Future[None]"]
|
| 1017 |
+
] = {}
|
| 1018 |
+
self._family = family
|
| 1019 |
+
self._local_addr_infos = aiohappyeyeballs.addr_to_addr_infos(local_addr)
|
| 1020 |
+
self._happy_eyeballs_delay = happy_eyeballs_delay
|
| 1021 |
+
self._interleave = interleave
|
| 1022 |
+
self._resolve_host_tasks: Set["asyncio.Task[List[ResolveResult]]"] = set()
|
| 1023 |
+
self._socket_factory = socket_factory
|
| 1024 |
+
self._ssl_shutdown_timeout: Optional[float]
|
| 1025 |
+
# Handle ssl_shutdown_timeout with warning for Python < 3.11
|
| 1026 |
+
if ssl_shutdown_timeout is sentinel:
|
| 1027 |
+
self._ssl_shutdown_timeout = 0
|
| 1028 |
+
else:
|
| 1029 |
+
# Deprecation warning for ssl_shutdown_timeout parameter
|
| 1030 |
+
warnings.warn(
|
| 1031 |
+
"The ssl_shutdown_timeout parameter is deprecated and will be removed in aiohttp 4.0",
|
| 1032 |
+
DeprecationWarning,
|
| 1033 |
+
stacklevel=2,
|
| 1034 |
+
)
|
| 1035 |
+
if (
|
| 1036 |
+
sys.version_info < (3, 11)
|
| 1037 |
+
and ssl_shutdown_timeout is not None
|
| 1038 |
+
and ssl_shutdown_timeout != 0
|
| 1039 |
+
):
|
| 1040 |
+
warnings.warn(
|
| 1041 |
+
f"ssl_shutdown_timeout={ssl_shutdown_timeout} is ignored on Python < 3.11; "
|
| 1042 |
+
"only ssl_shutdown_timeout=0 is supported. The timeout will be ignored.",
|
| 1043 |
+
RuntimeWarning,
|
| 1044 |
+
stacklevel=2,
|
| 1045 |
+
)
|
| 1046 |
+
self._ssl_shutdown_timeout = ssl_shutdown_timeout
|
| 1047 |
+
|
| 1048 |
+
def _close(self, *, abort_ssl: bool = False) -> List[Awaitable[object]]:
|
| 1049 |
+
"""Close all ongoing DNS calls."""
|
| 1050 |
+
for fut in chain.from_iterable(self._throttle_dns_futures.values()):
|
| 1051 |
+
fut.cancel()
|
| 1052 |
+
|
| 1053 |
+
waiters = super()._close(abort_ssl=abort_ssl)
|
| 1054 |
+
|
| 1055 |
+
for t in self._resolve_host_tasks:
|
| 1056 |
+
t.cancel()
|
| 1057 |
+
waiters.append(t)
|
| 1058 |
+
|
| 1059 |
+
return waiters
|
| 1060 |
+
|
| 1061 |
+
async def close(self, *, abort_ssl: bool = False) -> None:
|
| 1062 |
+
"""
|
| 1063 |
+
Close all opened transports.
|
| 1064 |
+
|
| 1065 |
+
:param abort_ssl: If True, SSL connections will be aborted immediately
|
| 1066 |
+
without performing the shutdown handshake. If False (default),
|
| 1067 |
+
the behavior is determined by ssl_shutdown_timeout:
|
| 1068 |
+
- If ssl_shutdown_timeout=0: connections are aborted
|
| 1069 |
+
- If ssl_shutdown_timeout>0: graceful shutdown is performed
|
| 1070 |
+
"""
|
| 1071 |
+
if self._resolver_owner:
|
| 1072 |
+
await self._resolver.close()
|
| 1073 |
+
# Use abort_ssl param if explicitly set, otherwise use ssl_shutdown_timeout default
|
| 1074 |
+
await super().close(abort_ssl=abort_ssl or self._ssl_shutdown_timeout == 0)
|
| 1075 |
+
|
| 1076 |
+
@property
|
| 1077 |
+
def family(self) -> int:
|
| 1078 |
+
"""Socket family like AF_INET."""
|
| 1079 |
+
return self._family
|
| 1080 |
+
|
| 1081 |
+
@property
|
| 1082 |
+
def use_dns_cache(self) -> bool:
|
| 1083 |
+
"""True if local DNS caching is enabled."""
|
| 1084 |
+
return self._use_dns_cache
|
| 1085 |
+
|
| 1086 |
+
def clear_dns_cache(
|
| 1087 |
+
self, host: Optional[str] = None, port: Optional[int] = None
|
| 1088 |
+
) -> None:
|
| 1089 |
+
"""Remove specified host/port or clear all dns local cache."""
|
| 1090 |
+
if host is not None and port is not None:
|
| 1091 |
+
self._cached_hosts.remove((host, port))
|
| 1092 |
+
elif host is not None or port is not None:
|
| 1093 |
+
raise ValueError("either both host and port or none of them are allowed")
|
| 1094 |
+
else:
|
| 1095 |
+
self._cached_hosts.clear()
|
| 1096 |
+
|
| 1097 |
+
async def _resolve_host(
|
| 1098 |
+
self, host: str, port: int, traces: Optional[Sequence["Trace"]] = None
|
| 1099 |
+
) -> List[ResolveResult]:
|
| 1100 |
+
"""Resolve host and return list of addresses."""
|
| 1101 |
+
if is_ip_address(host):
|
| 1102 |
+
return [
|
| 1103 |
+
{
|
| 1104 |
+
"hostname": host,
|
| 1105 |
+
"host": host,
|
| 1106 |
+
"port": port,
|
| 1107 |
+
"family": self._family,
|
| 1108 |
+
"proto": 0,
|
| 1109 |
+
"flags": 0,
|
| 1110 |
+
}
|
| 1111 |
+
]
|
| 1112 |
+
|
| 1113 |
+
if not self._use_dns_cache:
|
| 1114 |
+
|
| 1115 |
+
if traces:
|
| 1116 |
+
for trace in traces:
|
| 1117 |
+
await trace.send_dns_resolvehost_start(host)
|
| 1118 |
+
|
| 1119 |
+
res = await self._resolver.resolve(host, port, family=self._family)
|
| 1120 |
+
|
| 1121 |
+
if traces:
|
| 1122 |
+
for trace in traces:
|
| 1123 |
+
await trace.send_dns_resolvehost_end(host)
|
| 1124 |
+
|
| 1125 |
+
return res
|
| 1126 |
+
|
| 1127 |
+
key = (host, port)
|
| 1128 |
+
if key in self._cached_hosts and not self._cached_hosts.expired(key):
|
| 1129 |
+
# get result early, before any await (#4014)
|
| 1130 |
+
result = self._cached_hosts.next_addrs(key)
|
| 1131 |
+
|
| 1132 |
+
if traces:
|
| 1133 |
+
for trace in traces:
|
| 1134 |
+
await trace.send_dns_cache_hit(host)
|
| 1135 |
+
return result
|
| 1136 |
+
|
| 1137 |
+
futures: Set["asyncio.Future[None]"]
|
| 1138 |
+
#
|
| 1139 |
+
# If multiple connectors are resolving the same host, we wait
|
| 1140 |
+
# for the first one to resolve and then use the result for all of them.
|
| 1141 |
+
# We use a throttle to ensure that we only resolve the host once
|
| 1142 |
+
# and then use the result for all the waiters.
|
| 1143 |
+
#
|
| 1144 |
+
if key in self._throttle_dns_futures:
|
| 1145 |
+
# get futures early, before any await (#4014)
|
| 1146 |
+
futures = self._throttle_dns_futures[key]
|
| 1147 |
+
future: asyncio.Future[None] = self._loop.create_future()
|
| 1148 |
+
futures.add(future)
|
| 1149 |
+
if traces:
|
| 1150 |
+
for trace in traces:
|
| 1151 |
+
await trace.send_dns_cache_hit(host)
|
| 1152 |
+
try:
|
| 1153 |
+
await future
|
| 1154 |
+
finally:
|
| 1155 |
+
futures.discard(future)
|
| 1156 |
+
return self._cached_hosts.next_addrs(key)
|
| 1157 |
+
|
| 1158 |
+
# update dict early, before any await (#4014)
|
| 1159 |
+
self._throttle_dns_futures[key] = futures = set()
|
| 1160 |
+
# In this case we need to create a task to ensure that we can shield
|
| 1161 |
+
# the task from cancellation as cancelling this lookup should not cancel
|
| 1162 |
+
# the underlying lookup or else the cancel event will get broadcast to
|
| 1163 |
+
# all the waiters across all connections.
|
| 1164 |
+
#
|
| 1165 |
+
coro = self._resolve_host_with_throttle(key, host, port, futures, traces)
|
| 1166 |
+
loop = asyncio.get_running_loop()
|
| 1167 |
+
if sys.version_info >= (3, 12):
|
| 1168 |
+
# Optimization for Python 3.12, try to send immediately
|
| 1169 |
+
resolved_host_task = asyncio.Task(coro, loop=loop, eager_start=True)
|
| 1170 |
+
else:
|
| 1171 |
+
resolved_host_task = loop.create_task(coro)
|
| 1172 |
+
|
| 1173 |
+
if not resolved_host_task.done():
|
| 1174 |
+
self._resolve_host_tasks.add(resolved_host_task)
|
| 1175 |
+
resolved_host_task.add_done_callback(self._resolve_host_tasks.discard)
|
| 1176 |
+
|
| 1177 |
+
try:
|
| 1178 |
+
return await asyncio.shield(resolved_host_task)
|
| 1179 |
+
except asyncio.CancelledError:
|
| 1180 |
+
|
| 1181 |
+
def drop_exception(fut: "asyncio.Future[List[ResolveResult]]") -> None:
|
| 1182 |
+
with suppress(Exception, asyncio.CancelledError):
|
| 1183 |
+
fut.result()
|
| 1184 |
+
|
| 1185 |
+
resolved_host_task.add_done_callback(drop_exception)
|
| 1186 |
+
raise
|
| 1187 |
+
|
| 1188 |
+
async def _resolve_host_with_throttle(
|
| 1189 |
+
self,
|
| 1190 |
+
key: Tuple[str, int],
|
| 1191 |
+
host: str,
|
| 1192 |
+
port: int,
|
| 1193 |
+
futures: Set["asyncio.Future[None]"],
|
| 1194 |
+
traces: Optional[Sequence["Trace"]],
|
| 1195 |
+
) -> List[ResolveResult]:
|
| 1196 |
+
"""Resolve host and set result for all waiters.
|
| 1197 |
+
|
| 1198 |
+
This method must be run in a task and shielded from cancellation
|
| 1199 |
+
to avoid cancelling the underlying lookup.
|
| 1200 |
+
"""
|
| 1201 |
+
try:
|
| 1202 |
+
if traces:
|
| 1203 |
+
for trace in traces:
|
| 1204 |
+
await trace.send_dns_cache_miss(host)
|
| 1205 |
+
|
| 1206 |
+
for trace in traces:
|
| 1207 |
+
await trace.send_dns_resolvehost_start(host)
|
| 1208 |
+
|
| 1209 |
+
addrs = await self._resolver.resolve(host, port, family=self._family)
|
| 1210 |
+
if traces:
|
| 1211 |
+
for trace in traces:
|
| 1212 |
+
await trace.send_dns_resolvehost_end(host)
|
| 1213 |
+
|
| 1214 |
+
self._cached_hosts.add(key, addrs)
|
| 1215 |
+
for fut in futures:
|
| 1216 |
+
set_result(fut, None)
|
| 1217 |
+
except BaseException as e:
|
| 1218 |
+
# any DNS exception is set for the waiters to raise the same exception.
|
| 1219 |
+
# This coro is always run in task that is shielded from cancellation so
|
| 1220 |
+
# we should never be propagating cancellation here.
|
| 1221 |
+
for fut in futures:
|
| 1222 |
+
set_exception(fut, e)
|
| 1223 |
+
raise
|
| 1224 |
+
finally:
|
| 1225 |
+
self._throttle_dns_futures.pop(key)
|
| 1226 |
+
|
| 1227 |
+
return self._cached_hosts.next_addrs(key)
|
| 1228 |
+
|
| 1229 |
+
async def _create_connection(
|
| 1230 |
+
self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout"
|
| 1231 |
+
) -> ResponseHandler:
|
| 1232 |
+
"""Create connection.
|
| 1233 |
+
|
| 1234 |
+
Has same keyword arguments as BaseEventLoop.create_connection.
|
| 1235 |
+
"""
|
| 1236 |
+
if req.proxy:
|
| 1237 |
+
_, proto = await self._create_proxy_connection(req, traces, timeout)
|
| 1238 |
+
else:
|
| 1239 |
+
_, proto = await self._create_direct_connection(req, traces, timeout)
|
| 1240 |
+
|
| 1241 |
+
return proto
|
| 1242 |
+
|
| 1243 |
+
def _get_ssl_context(self, req: ClientRequest) -> Optional[SSLContext]:
|
| 1244 |
+
"""Logic to get the correct SSL context
|
| 1245 |
+
|
| 1246 |
+
0. if req.ssl is false, return None
|
| 1247 |
+
|
| 1248 |
+
1. if ssl_context is specified in req, use it
|
| 1249 |
+
2. if _ssl_context is specified in self, use it
|
| 1250 |
+
3. otherwise:
|
| 1251 |
+
1. if verify_ssl is not specified in req, use self.ssl_context
|
| 1252 |
+
(will generate a default context according to self.verify_ssl)
|
| 1253 |
+
2. if verify_ssl is True in req, generate a default SSL context
|
| 1254 |
+
3. if verify_ssl is False in req, generate a SSL context that
|
| 1255 |
+
won't verify
|
| 1256 |
+
"""
|
| 1257 |
+
if not req.is_ssl():
|
| 1258 |
+
return None
|
| 1259 |
+
|
| 1260 |
+
if ssl is None: # pragma: no cover
|
| 1261 |
+
raise RuntimeError("SSL is not supported.")
|
| 1262 |
+
sslcontext = req.ssl
|
| 1263 |
+
if isinstance(sslcontext, ssl.SSLContext):
|
| 1264 |
+
return sslcontext
|
| 1265 |
+
if sslcontext is not True:
|
| 1266 |
+
# not verified or fingerprinted
|
| 1267 |
+
return _SSL_CONTEXT_UNVERIFIED
|
| 1268 |
+
sslcontext = self._ssl
|
| 1269 |
+
if isinstance(sslcontext, ssl.SSLContext):
|
| 1270 |
+
return sslcontext
|
| 1271 |
+
if sslcontext is not True:
|
| 1272 |
+
# not verified or fingerprinted
|
| 1273 |
+
return _SSL_CONTEXT_UNVERIFIED
|
| 1274 |
+
return _SSL_CONTEXT_VERIFIED
|
| 1275 |
+
|
| 1276 |
+
def _get_fingerprint(self, req: ClientRequest) -> Optional["Fingerprint"]:
|
| 1277 |
+
ret = req.ssl
|
| 1278 |
+
if isinstance(ret, Fingerprint):
|
| 1279 |
+
return ret
|
| 1280 |
+
ret = self._ssl
|
| 1281 |
+
if isinstance(ret, Fingerprint):
|
| 1282 |
+
return ret
|
| 1283 |
+
return None
|
| 1284 |
+
|
| 1285 |
+
async def _wrap_create_connection(
|
| 1286 |
+
self,
|
| 1287 |
+
*args: Any,
|
| 1288 |
+
addr_infos: List[AddrInfoType],
|
| 1289 |
+
req: ClientRequest,
|
| 1290 |
+
timeout: "ClientTimeout",
|
| 1291 |
+
client_error: Type[Exception] = ClientConnectorError,
|
| 1292 |
+
**kwargs: Any,
|
| 1293 |
+
) -> Tuple[asyncio.Transport, ResponseHandler]:
|
| 1294 |
+
try:
|
| 1295 |
+
async with ceil_timeout(
|
| 1296 |
+
timeout.sock_connect, ceil_threshold=timeout.ceil_threshold
|
| 1297 |
+
):
|
| 1298 |
+
sock = await aiohappyeyeballs.start_connection(
|
| 1299 |
+
addr_infos=addr_infos,
|
| 1300 |
+
local_addr_infos=self._local_addr_infos,
|
| 1301 |
+
happy_eyeballs_delay=self._happy_eyeballs_delay,
|
| 1302 |
+
interleave=self._interleave,
|
| 1303 |
+
loop=self._loop,
|
| 1304 |
+
socket_factory=self._socket_factory,
|
| 1305 |
+
)
|
| 1306 |
+
# Add ssl_shutdown_timeout for Python 3.11+ when SSL is used
|
| 1307 |
+
if (
|
| 1308 |
+
kwargs.get("ssl")
|
| 1309 |
+
and self._ssl_shutdown_timeout
|
| 1310 |
+
and sys.version_info >= (3, 11)
|
| 1311 |
+
):
|
| 1312 |
+
kwargs["ssl_shutdown_timeout"] = self._ssl_shutdown_timeout
|
| 1313 |
+
return await self._loop.create_connection(*args, **kwargs, sock=sock)
|
| 1314 |
+
except cert_errors as exc:
|
| 1315 |
+
raise ClientConnectorCertificateError(req.connection_key, exc) from exc
|
| 1316 |
+
except ssl_errors as exc:
|
| 1317 |
+
raise ClientConnectorSSLError(req.connection_key, exc) from exc
|
| 1318 |
+
except OSError as exc:
|
| 1319 |
+
if exc.errno is None and isinstance(exc, asyncio.TimeoutError):
|
| 1320 |
+
raise
|
| 1321 |
+
raise client_error(req.connection_key, exc) from exc
|
| 1322 |
+
|
| 1323 |
+
async def _wrap_existing_connection(
|
| 1324 |
+
self,
|
| 1325 |
+
*args: Any,
|
| 1326 |
+
req: ClientRequest,
|
| 1327 |
+
timeout: "ClientTimeout",
|
| 1328 |
+
client_error: Type[Exception] = ClientConnectorError,
|
| 1329 |
+
**kwargs: Any,
|
| 1330 |
+
) -> Tuple[asyncio.Transport, ResponseHandler]:
|
| 1331 |
+
try:
|
| 1332 |
+
async with ceil_timeout(
|
| 1333 |
+
timeout.sock_connect, ceil_threshold=timeout.ceil_threshold
|
| 1334 |
+
):
|
| 1335 |
+
return await self._loop.create_connection(*args, **kwargs)
|
| 1336 |
+
except cert_errors as exc:
|
| 1337 |
+
raise ClientConnectorCertificateError(req.connection_key, exc) from exc
|
| 1338 |
+
except ssl_errors as exc:
|
| 1339 |
+
raise ClientConnectorSSLError(req.connection_key, exc) from exc
|
| 1340 |
+
except OSError as exc:
|
| 1341 |
+
if exc.errno is None and isinstance(exc, asyncio.TimeoutError):
|
| 1342 |
+
raise
|
| 1343 |
+
raise client_error(req.connection_key, exc) from exc
|
| 1344 |
+
|
| 1345 |
+
def _fail_on_no_start_tls(self, req: "ClientRequest") -> None:
|
| 1346 |
+
"""Raise a :py:exc:`RuntimeError` on missing ``start_tls()``.
|
| 1347 |
+
|
| 1348 |
+
It is necessary for TLS-in-TLS so that it is possible to
|
| 1349 |
+
send HTTPS queries through HTTPS proxies.
|
| 1350 |
+
|
| 1351 |
+
This doesn't affect regular HTTP requests, though.
|
| 1352 |
+
"""
|
| 1353 |
+
if not req.is_ssl():
|
| 1354 |
+
return
|
| 1355 |
+
|
| 1356 |
+
proxy_url = req.proxy
|
| 1357 |
+
assert proxy_url is not None
|
| 1358 |
+
if proxy_url.scheme != "https":
|
| 1359 |
+
return
|
| 1360 |
+
|
| 1361 |
+
self._check_loop_for_start_tls()
|
| 1362 |
+
|
| 1363 |
+
def _check_loop_for_start_tls(self) -> None:
|
| 1364 |
+
try:
|
| 1365 |
+
self._loop.start_tls
|
| 1366 |
+
except AttributeError as attr_exc:
|
| 1367 |
+
raise RuntimeError(
|
| 1368 |
+
"An HTTPS request is being sent through an HTTPS proxy. "
|
| 1369 |
+
"This needs support for TLS in TLS but it is not implemented "
|
| 1370 |
+
"in your runtime for the stdlib asyncio.\n\n"
|
| 1371 |
+
"Please upgrade to Python 3.11 or higher. For more details, "
|
| 1372 |
+
"please see:\n"
|
| 1373 |
+
"* https://bugs.python.org/issue37179\n"
|
| 1374 |
+
"* https://github.com/python/cpython/pull/28073\n"
|
| 1375 |
+
"* https://docs.aiohttp.org/en/stable/"
|
| 1376 |
+
"client_advanced.html#proxy-support\n"
|
| 1377 |
+
"* https://github.com/aio-libs/aiohttp/discussions/6044\n",
|
| 1378 |
+
) from attr_exc
|
| 1379 |
+
|
| 1380 |
+
def _loop_supports_start_tls(self) -> bool:
|
| 1381 |
+
try:
|
| 1382 |
+
self._check_loop_for_start_tls()
|
| 1383 |
+
except RuntimeError:
|
| 1384 |
+
return False
|
| 1385 |
+
else:
|
| 1386 |
+
return True
|
| 1387 |
+
|
| 1388 |
+
def _warn_about_tls_in_tls(
|
| 1389 |
+
self,
|
| 1390 |
+
underlying_transport: asyncio.Transport,
|
| 1391 |
+
req: ClientRequest,
|
| 1392 |
+
) -> None:
|
| 1393 |
+
"""Issue a warning if the requested URL has HTTPS scheme."""
|
| 1394 |
+
if req.request_info.url.scheme != "https":
|
| 1395 |
+
return
|
| 1396 |
+
|
| 1397 |
+
# Check if uvloop is being used, which supports TLS in TLS,
|
| 1398 |
+
# otherwise assume that asyncio's native transport is being used.
|
| 1399 |
+
if type(underlying_transport).__module__.startswith("uvloop"):
|
| 1400 |
+
return
|
| 1401 |
+
|
| 1402 |
+
# Support in asyncio was added in Python 3.11 (bpo-44011)
|
| 1403 |
+
asyncio_supports_tls_in_tls = sys.version_info >= (3, 11) or getattr(
|
| 1404 |
+
underlying_transport,
|
| 1405 |
+
"_start_tls_compatible",
|
| 1406 |
+
False,
|
| 1407 |
+
)
|
| 1408 |
+
|
| 1409 |
+
if asyncio_supports_tls_in_tls:
|
| 1410 |
+
return
|
| 1411 |
+
|
| 1412 |
+
warnings.warn(
|
| 1413 |
+
"An HTTPS request is being sent through an HTTPS proxy. "
|
| 1414 |
+
"This support for TLS in TLS is known to be disabled "
|
| 1415 |
+
"in the stdlib asyncio (Python <3.11). This is why you'll probably see "
|
| 1416 |
+
"an error in the log below.\n\n"
|
| 1417 |
+
"It is possible to enable it via monkeypatching. "
|
| 1418 |
+
"For more details, see:\n"
|
| 1419 |
+
"* https://bugs.python.org/issue37179\n"
|
| 1420 |
+
"* https://github.com/python/cpython/pull/28073\n\n"
|
| 1421 |
+
"You can temporarily patch this as follows:\n"
|
| 1422 |
+
"* https://docs.aiohttp.org/en/stable/client_advanced.html#proxy-support\n"
|
| 1423 |
+
"* https://github.com/aio-libs/aiohttp/discussions/6044\n",
|
| 1424 |
+
RuntimeWarning,
|
| 1425 |
+
source=self,
|
| 1426 |
+
# Why `4`? At least 3 of the calls in the stack originate
|
| 1427 |
+
# from the methods in this class.
|
| 1428 |
+
stacklevel=3,
|
| 1429 |
+
)
|
| 1430 |
+
|
| 1431 |
+
async def _start_tls_connection(
|
| 1432 |
+
self,
|
| 1433 |
+
underlying_transport: asyncio.Transport,
|
| 1434 |
+
req: ClientRequest,
|
| 1435 |
+
timeout: "ClientTimeout",
|
| 1436 |
+
client_error: Type[Exception] = ClientConnectorError,
|
| 1437 |
+
) -> Tuple[asyncio.BaseTransport, ResponseHandler]:
|
| 1438 |
+
"""Wrap the raw TCP transport with TLS."""
|
| 1439 |
+
tls_proto = self._factory() # Create a brand new proto for TLS
|
| 1440 |
+
sslcontext = self._get_ssl_context(req)
|
| 1441 |
+
if TYPE_CHECKING:
|
| 1442 |
+
# _start_tls_connection is unreachable in the current code path
|
| 1443 |
+
# if sslcontext is None.
|
| 1444 |
+
assert sslcontext is not None
|
| 1445 |
+
|
| 1446 |
+
try:
|
| 1447 |
+
async with ceil_timeout(
|
| 1448 |
+
timeout.sock_connect, ceil_threshold=timeout.ceil_threshold
|
| 1449 |
+
):
|
| 1450 |
+
try:
|
| 1451 |
+
# ssl_shutdown_timeout is only available in Python 3.11+
|
| 1452 |
+
if sys.version_info >= (3, 11) and self._ssl_shutdown_timeout:
|
| 1453 |
+
tls_transport = await self._loop.start_tls(
|
| 1454 |
+
underlying_transport,
|
| 1455 |
+
tls_proto,
|
| 1456 |
+
sslcontext,
|
| 1457 |
+
server_hostname=req.server_hostname or req.host,
|
| 1458 |
+
ssl_handshake_timeout=timeout.total,
|
| 1459 |
+
ssl_shutdown_timeout=self._ssl_shutdown_timeout,
|
| 1460 |
+
)
|
| 1461 |
+
else:
|
| 1462 |
+
tls_transport = await self._loop.start_tls(
|
| 1463 |
+
underlying_transport,
|
| 1464 |
+
tls_proto,
|
| 1465 |
+
sslcontext,
|
| 1466 |
+
server_hostname=req.server_hostname or req.host,
|
| 1467 |
+
ssl_handshake_timeout=timeout.total,
|
| 1468 |
+
)
|
| 1469 |
+
except BaseException:
|
| 1470 |
+
# We need to close the underlying transport since
|
| 1471 |
+
# `start_tls()` probably failed before it had a
|
| 1472 |
+
# chance to do this:
|
| 1473 |
+
if self._ssl_shutdown_timeout == 0:
|
| 1474 |
+
underlying_transport.abort()
|
| 1475 |
+
else:
|
| 1476 |
+
underlying_transport.close()
|
| 1477 |
+
raise
|
| 1478 |
+
if isinstance(tls_transport, asyncio.Transport):
|
| 1479 |
+
fingerprint = self._get_fingerprint(req)
|
| 1480 |
+
if fingerprint:
|
| 1481 |
+
try:
|
| 1482 |
+
fingerprint.check(tls_transport)
|
| 1483 |
+
except ServerFingerprintMismatch:
|
| 1484 |
+
tls_transport.close()
|
| 1485 |
+
if not self._cleanup_closed_disabled:
|
| 1486 |
+
self._cleanup_closed_transports.append(tls_transport)
|
| 1487 |
+
raise
|
| 1488 |
+
except cert_errors as exc:
|
| 1489 |
+
raise ClientConnectorCertificateError(req.connection_key, exc) from exc
|
| 1490 |
+
except ssl_errors as exc:
|
| 1491 |
+
raise ClientConnectorSSLError(req.connection_key, exc) from exc
|
| 1492 |
+
except OSError as exc:
|
| 1493 |
+
if exc.errno is None and isinstance(exc, asyncio.TimeoutError):
|
| 1494 |
+
raise
|
| 1495 |
+
raise client_error(req.connection_key, exc) from exc
|
| 1496 |
+
except TypeError as type_err:
|
| 1497 |
+
# Example cause looks like this:
|
| 1498 |
+
# TypeError: transport <asyncio.sslproto._SSLProtocolTransport
|
| 1499 |
+
# object at 0x7f760615e460> is not supported by start_tls()
|
| 1500 |
+
|
| 1501 |
+
raise ClientConnectionError(
|
| 1502 |
+
"Cannot initialize a TLS-in-TLS connection to host "
|
| 1503 |
+
f"{req.host!s}:{req.port:d} through an underlying connection "
|
| 1504 |
+
f"to an HTTPS proxy {req.proxy!s} ssl:{req.ssl or 'default'} "
|
| 1505 |
+
f"[{type_err!s}]"
|
| 1506 |
+
) from type_err
|
| 1507 |
+
else:
|
| 1508 |
+
if tls_transport is None:
|
| 1509 |
+
msg = "Failed to start TLS (possibly caused by closing transport)"
|
| 1510 |
+
raise client_error(req.connection_key, OSError(msg))
|
| 1511 |
+
tls_proto.connection_made(
|
| 1512 |
+
tls_transport
|
| 1513 |
+
) # Kick the state machine of the new TLS protocol
|
| 1514 |
+
|
| 1515 |
+
return tls_transport, tls_proto
|
| 1516 |
+
|
| 1517 |
+
def _convert_hosts_to_addr_infos(
|
| 1518 |
+
self, hosts: List[ResolveResult]
|
| 1519 |
+
) -> List[AddrInfoType]:
|
| 1520 |
+
"""Converts the list of hosts to a list of addr_infos.
|
| 1521 |
+
|
| 1522 |
+
The list of hosts is the result of a DNS lookup. The list of
|
| 1523 |
+
addr_infos is the result of a call to `socket.getaddrinfo()`.
|
| 1524 |
+
"""
|
| 1525 |
+
addr_infos: List[AddrInfoType] = []
|
| 1526 |
+
for hinfo in hosts:
|
| 1527 |
+
host = hinfo["host"]
|
| 1528 |
+
is_ipv6 = ":" in host
|
| 1529 |
+
family = socket.AF_INET6 if is_ipv6 else socket.AF_INET
|
| 1530 |
+
if self._family and self._family != family:
|
| 1531 |
+
continue
|
| 1532 |
+
addr = (host, hinfo["port"], 0, 0) if is_ipv6 else (host, hinfo["port"])
|
| 1533 |
+
addr_infos.append(
|
| 1534 |
+
(family, socket.SOCK_STREAM, socket.IPPROTO_TCP, "", addr)
|
| 1535 |
+
)
|
| 1536 |
+
return addr_infos
|
| 1537 |
+
|
| 1538 |
+
async def _create_direct_connection(
|
| 1539 |
+
self,
|
| 1540 |
+
req: ClientRequest,
|
| 1541 |
+
traces: List["Trace"],
|
| 1542 |
+
timeout: "ClientTimeout",
|
| 1543 |
+
*,
|
| 1544 |
+
client_error: Type[Exception] = ClientConnectorError,
|
| 1545 |
+
) -> Tuple[asyncio.Transport, ResponseHandler]:
|
| 1546 |
+
sslcontext = self._get_ssl_context(req)
|
| 1547 |
+
fingerprint = self._get_fingerprint(req)
|
| 1548 |
+
|
| 1549 |
+
host = req.url.raw_host
|
| 1550 |
+
assert host is not None
|
| 1551 |
+
# Replace multiple trailing dots with a single one.
|
| 1552 |
+
# A trailing dot is only present for fully-qualified domain names.
|
| 1553 |
+
# See https://github.com/aio-libs/aiohttp/pull/7364.
|
| 1554 |
+
if host.endswith(".."):
|
| 1555 |
+
host = host.rstrip(".") + "."
|
| 1556 |
+
port = req.port
|
| 1557 |
+
assert port is not None
|
| 1558 |
+
try:
|
| 1559 |
+
# Cancelling this lookup should not cancel the underlying lookup
|
| 1560 |
+
# or else the cancel event will get broadcast to all the waiters
|
| 1561 |
+
# across all connections.
|
| 1562 |
+
hosts = await self._resolve_host(host, port, traces=traces)
|
| 1563 |
+
except OSError as exc:
|
| 1564 |
+
if exc.errno is None and isinstance(exc, asyncio.TimeoutError):
|
| 1565 |
+
raise
|
| 1566 |
+
# in case of proxy it is not ClientProxyConnectionError
|
| 1567 |
+
# it is problem of resolving proxy ip itself
|
| 1568 |
+
raise ClientConnectorDNSError(req.connection_key, exc) from exc
|
| 1569 |
+
|
| 1570 |
+
last_exc: Optional[Exception] = None
|
| 1571 |
+
addr_infos = self._convert_hosts_to_addr_infos(hosts)
|
| 1572 |
+
while addr_infos:
|
| 1573 |
+
# Strip trailing dots, certificates contain FQDN without dots.
|
| 1574 |
+
# See https://github.com/aio-libs/aiohttp/issues/3636
|
| 1575 |
+
server_hostname = (
|
| 1576 |
+
(req.server_hostname or host).rstrip(".") if sslcontext else None
|
| 1577 |
+
)
|
| 1578 |
+
|
| 1579 |
+
try:
|
| 1580 |
+
transp, proto = await self._wrap_create_connection(
|
| 1581 |
+
self._factory,
|
| 1582 |
+
timeout=timeout,
|
| 1583 |
+
ssl=sslcontext,
|
| 1584 |
+
addr_infos=addr_infos,
|
| 1585 |
+
server_hostname=server_hostname,
|
| 1586 |
+
req=req,
|
| 1587 |
+
client_error=client_error,
|
| 1588 |
+
)
|
| 1589 |
+
except (ClientConnectorError, asyncio.TimeoutError) as exc:
|
| 1590 |
+
last_exc = exc
|
| 1591 |
+
aiohappyeyeballs.pop_addr_infos_interleave(addr_infos, self._interleave)
|
| 1592 |
+
continue
|
| 1593 |
+
|
| 1594 |
+
if req.is_ssl() and fingerprint:
|
| 1595 |
+
try:
|
| 1596 |
+
fingerprint.check(transp)
|
| 1597 |
+
except ServerFingerprintMismatch as exc:
|
| 1598 |
+
transp.close()
|
| 1599 |
+
if not self._cleanup_closed_disabled:
|
| 1600 |
+
self._cleanup_closed_transports.append(transp)
|
| 1601 |
+
last_exc = exc
|
| 1602 |
+
# Remove the bad peer from the list of addr_infos
|
| 1603 |
+
sock: socket.socket = transp.get_extra_info("socket")
|
| 1604 |
+
bad_peer = sock.getpeername()
|
| 1605 |
+
aiohappyeyeballs.remove_addr_infos(addr_infos, bad_peer)
|
| 1606 |
+
continue
|
| 1607 |
+
|
| 1608 |
+
return transp, proto
|
| 1609 |
+
else:
|
| 1610 |
+
assert last_exc is not None
|
| 1611 |
+
raise last_exc
|
| 1612 |
+
|
| 1613 |
+
async def _create_proxy_connection(
|
| 1614 |
+
self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout"
|
| 1615 |
+
) -> Tuple[asyncio.BaseTransport, ResponseHandler]:
|
| 1616 |
+
self._fail_on_no_start_tls(req)
|
| 1617 |
+
runtime_has_start_tls = self._loop_supports_start_tls()
|
| 1618 |
+
proxy_req = self._update_proxy_auth_header_and_build_proxy_req(req)
|
| 1619 |
+
|
| 1620 |
+
# create connection to proxy server
|
| 1621 |
+
transport, proto = await self._create_direct_connection(
|
| 1622 |
+
proxy_req, [], timeout, client_error=ClientProxyConnectionError
|
| 1623 |
+
)
|
| 1624 |
+
|
| 1625 |
+
if req.is_ssl():
|
| 1626 |
+
if runtime_has_start_tls:
|
| 1627 |
+
self._warn_about_tls_in_tls(transport, req)
|
| 1628 |
+
|
| 1629 |
+
# For HTTPS requests over HTTP proxy
|
| 1630 |
+
# we must notify proxy to tunnel connection
|
| 1631 |
+
# so we send CONNECT command:
|
| 1632 |
+
# CONNECT www.python.org:443 HTTP/1.1
|
| 1633 |
+
# Host: www.python.org
|
| 1634 |
+
#
|
| 1635 |
+
# next we must do TLS handshake and so on
|
| 1636 |
+
# to do this we must wrap raw socket into secure one
|
| 1637 |
+
# asyncio handles this perfectly
|
| 1638 |
+
proxy_req.method = hdrs.METH_CONNECT
|
| 1639 |
+
proxy_req.url = req.url
|
| 1640 |
+
key = req.connection_key._replace(
|
| 1641 |
+
proxy=None, proxy_auth=None, proxy_headers_hash=None
|
| 1642 |
+
)
|
| 1643 |
+
conn = _ConnectTunnelConnection(self, key, proto, self._loop)
|
| 1644 |
+
proxy_resp = await proxy_req.send(conn)
|
| 1645 |
+
try:
|
| 1646 |
+
protocol = conn._protocol
|
| 1647 |
+
assert protocol is not None
|
| 1648 |
+
|
| 1649 |
+
# read_until_eof=True will ensure the connection isn't closed
|
| 1650 |
+
# once the response is received and processed allowing
|
| 1651 |
+
# START_TLS to work on the connection below.
|
| 1652 |
+
protocol.set_response_params(
|
| 1653 |
+
read_until_eof=runtime_has_start_tls,
|
| 1654 |
+
timeout_ceil_threshold=self._timeout_ceil_threshold,
|
| 1655 |
+
)
|
| 1656 |
+
resp = await proxy_resp.start(conn)
|
| 1657 |
+
except BaseException:
|
| 1658 |
+
proxy_resp.close()
|
| 1659 |
+
conn.close()
|
| 1660 |
+
raise
|
| 1661 |
+
else:
|
| 1662 |
+
conn._protocol = None
|
| 1663 |
+
try:
|
| 1664 |
+
if resp.status != 200:
|
| 1665 |
+
message = resp.reason
|
| 1666 |
+
if message is None:
|
| 1667 |
+
message = HTTPStatus(resp.status).phrase
|
| 1668 |
+
raise ClientHttpProxyError(
|
| 1669 |
+
proxy_resp.request_info,
|
| 1670 |
+
resp.history,
|
| 1671 |
+
status=resp.status,
|
| 1672 |
+
message=message,
|
| 1673 |
+
headers=resp.headers,
|
| 1674 |
+
)
|
| 1675 |
+
if not runtime_has_start_tls:
|
| 1676 |
+
rawsock = transport.get_extra_info("socket", default=None)
|
| 1677 |
+
if rawsock is None:
|
| 1678 |
+
raise RuntimeError(
|
| 1679 |
+
"Transport does not expose socket instance"
|
| 1680 |
+
)
|
| 1681 |
+
# Duplicate the socket, so now we can close proxy transport
|
| 1682 |
+
rawsock = rawsock.dup()
|
| 1683 |
+
except BaseException:
|
| 1684 |
+
# It shouldn't be closed in `finally` because it's fed to
|
| 1685 |
+
# `loop.start_tls()` and the docs say not to touch it after
|
| 1686 |
+
# passing there.
|
| 1687 |
+
transport.close()
|
| 1688 |
+
raise
|
| 1689 |
+
finally:
|
| 1690 |
+
if not runtime_has_start_tls:
|
| 1691 |
+
transport.close()
|
| 1692 |
+
|
| 1693 |
+
if not runtime_has_start_tls:
|
| 1694 |
+
# HTTP proxy with support for upgrade to HTTPS
|
| 1695 |
+
sslcontext = self._get_ssl_context(req)
|
| 1696 |
+
return await self._wrap_existing_connection(
|
| 1697 |
+
self._factory,
|
| 1698 |
+
timeout=timeout,
|
| 1699 |
+
ssl=sslcontext,
|
| 1700 |
+
sock=rawsock,
|
| 1701 |
+
server_hostname=req.host,
|
| 1702 |
+
req=req,
|
| 1703 |
+
)
|
| 1704 |
+
|
| 1705 |
+
return await self._start_tls_connection(
|
| 1706 |
+
# Access the old transport for the last time before it's
|
| 1707 |
+
# closed and forgotten forever:
|
| 1708 |
+
transport,
|
| 1709 |
+
req=req,
|
| 1710 |
+
timeout=timeout,
|
| 1711 |
+
)
|
| 1712 |
+
finally:
|
| 1713 |
+
proxy_resp.close()
|
| 1714 |
+
|
| 1715 |
+
return transport, proto
|
| 1716 |
+
|
| 1717 |
+
|
| 1718 |
+
class UnixConnector(BaseConnector):
|
| 1719 |
+
"""Unix socket connector.
|
| 1720 |
+
|
| 1721 |
+
path - Unix socket path.
|
| 1722 |
+
keepalive_timeout - (optional) Keep-alive timeout.
|
| 1723 |
+
force_close - Set to True to force close and do reconnect
|
| 1724 |
+
after each request (and between redirects).
|
| 1725 |
+
limit - The total number of simultaneous connections.
|
| 1726 |
+
limit_per_host - Number of simultaneous connections to one host.
|
| 1727 |
+
loop - Optional event loop.
|
| 1728 |
+
"""
|
| 1729 |
+
|
| 1730 |
+
allowed_protocol_schema_set = HIGH_LEVEL_SCHEMA_SET | frozenset({"unix"})
|
| 1731 |
+
|
| 1732 |
+
def __init__(
|
| 1733 |
+
self,
|
| 1734 |
+
path: str,
|
| 1735 |
+
force_close: bool = False,
|
| 1736 |
+
keepalive_timeout: Union[object, float, None] = sentinel,
|
| 1737 |
+
limit: int = 100,
|
| 1738 |
+
limit_per_host: int = 0,
|
| 1739 |
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
| 1740 |
+
) -> None:
|
| 1741 |
+
super().__init__(
|
| 1742 |
+
force_close=force_close,
|
| 1743 |
+
keepalive_timeout=keepalive_timeout,
|
| 1744 |
+
limit=limit,
|
| 1745 |
+
limit_per_host=limit_per_host,
|
| 1746 |
+
loop=loop,
|
| 1747 |
+
)
|
| 1748 |
+
self._path = path
|
| 1749 |
+
|
| 1750 |
+
@property
|
| 1751 |
+
def path(self) -> str:
|
| 1752 |
+
"""Path to unix socket."""
|
| 1753 |
+
return self._path
|
| 1754 |
+
|
| 1755 |
+
async def _create_connection(
|
| 1756 |
+
self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout"
|
| 1757 |
+
) -> ResponseHandler:
|
| 1758 |
+
try:
|
| 1759 |
+
async with ceil_timeout(
|
| 1760 |
+
timeout.sock_connect, ceil_threshold=timeout.ceil_threshold
|
| 1761 |
+
):
|
| 1762 |
+
_, proto = await self._loop.create_unix_connection(
|
| 1763 |
+
self._factory, self._path
|
| 1764 |
+
)
|
| 1765 |
+
except OSError as exc:
|
| 1766 |
+
if exc.errno is None and isinstance(exc, asyncio.TimeoutError):
|
| 1767 |
+
raise
|
| 1768 |
+
raise UnixClientConnectorError(self.path, req.connection_key, exc) from exc
|
| 1769 |
+
|
| 1770 |
+
return proto
|
| 1771 |
+
|
| 1772 |
+
|
| 1773 |
+
class NamedPipeConnector(BaseConnector):
|
| 1774 |
+
"""Named pipe connector.
|
| 1775 |
+
|
| 1776 |
+
Only supported by the proactor event loop.
|
| 1777 |
+
See also: https://docs.python.org/3/library/asyncio-eventloop.html
|
| 1778 |
+
|
| 1779 |
+
path - Windows named pipe path.
|
| 1780 |
+
keepalive_timeout - (optional) Keep-alive timeout.
|
| 1781 |
+
force_close - Set to True to force close and do reconnect
|
| 1782 |
+
after each request (and between redirects).
|
| 1783 |
+
limit - The total number of simultaneous connections.
|
| 1784 |
+
limit_per_host - Number of simultaneous connections to one host.
|
| 1785 |
+
loop - Optional event loop.
|
| 1786 |
+
"""
|
| 1787 |
+
|
| 1788 |
+
allowed_protocol_schema_set = HIGH_LEVEL_SCHEMA_SET | frozenset({"npipe"})
|
| 1789 |
+
|
| 1790 |
+
def __init__(
|
| 1791 |
+
self,
|
| 1792 |
+
path: str,
|
| 1793 |
+
force_close: bool = False,
|
| 1794 |
+
keepalive_timeout: Union[object, float, None] = sentinel,
|
| 1795 |
+
limit: int = 100,
|
| 1796 |
+
limit_per_host: int = 0,
|
| 1797 |
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
| 1798 |
+
) -> None:
|
| 1799 |
+
super().__init__(
|
| 1800 |
+
force_close=force_close,
|
| 1801 |
+
keepalive_timeout=keepalive_timeout,
|
| 1802 |
+
limit=limit,
|
| 1803 |
+
limit_per_host=limit_per_host,
|
| 1804 |
+
loop=loop,
|
| 1805 |
+
)
|
| 1806 |
+
if not isinstance(
|
| 1807 |
+
self._loop,
|
| 1808 |
+
asyncio.ProactorEventLoop, # type: ignore[attr-defined]
|
| 1809 |
+
):
|
| 1810 |
+
raise RuntimeError(
|
| 1811 |
+
"Named Pipes only available in proactor loop under windows"
|
| 1812 |
+
)
|
| 1813 |
+
self._path = path
|
| 1814 |
+
|
| 1815 |
+
@property
|
| 1816 |
+
def path(self) -> str:
|
| 1817 |
+
"""Path to the named pipe."""
|
| 1818 |
+
return self._path
|
| 1819 |
+
|
| 1820 |
+
async def _create_connection(
|
| 1821 |
+
self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout"
|
| 1822 |
+
) -> ResponseHandler:
|
| 1823 |
+
try:
|
| 1824 |
+
async with ceil_timeout(
|
| 1825 |
+
timeout.sock_connect, ceil_threshold=timeout.ceil_threshold
|
| 1826 |
+
):
|
| 1827 |
+
_, proto = await self._loop.create_pipe_connection( # type: ignore[attr-defined]
|
| 1828 |
+
self._factory, self._path
|
| 1829 |
+
)
|
| 1830 |
+
# the drain is required so that the connection_made is called
|
| 1831 |
+
# and transport is set otherwise it is not set before the
|
| 1832 |
+
# `assert conn.transport is not None`
|
| 1833 |
+
# in client.py's _request method
|
| 1834 |
+
await asyncio.sleep(0)
|
| 1835 |
+
# other option is to manually set transport like
|
| 1836 |
+
# `proto.transport = trans`
|
| 1837 |
+
except OSError as exc:
|
| 1838 |
+
if exc.errno is None and isinstance(exc, asyncio.TimeoutError):
|
| 1839 |
+
raise
|
| 1840 |
+
raise ClientConnectorError(req.connection_key, exc) from exc
|
| 1841 |
+
|
| 1842 |
+
return cast(ResponseHandler, proto)
|
aiohttp/cookiejar.py
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import calendar
|
| 3 |
+
import contextlib
|
| 4 |
+
import datetime
|
| 5 |
+
import heapq
|
| 6 |
+
import itertools
|
| 7 |
+
import os # noqa
|
| 8 |
+
import pathlib
|
| 9 |
+
import pickle
|
| 10 |
+
import re
|
| 11 |
+
import time
|
| 12 |
+
import warnings
|
| 13 |
+
from collections import defaultdict
|
| 14 |
+
from collections.abc import Mapping
|
| 15 |
+
from http.cookies import BaseCookie, Morsel, SimpleCookie
|
| 16 |
+
from typing import (
|
| 17 |
+
DefaultDict,
|
| 18 |
+
Dict,
|
| 19 |
+
Iterable,
|
| 20 |
+
Iterator,
|
| 21 |
+
List,
|
| 22 |
+
Optional,
|
| 23 |
+
Set,
|
| 24 |
+
Tuple,
|
| 25 |
+
Union,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
from yarl import URL
|
| 29 |
+
|
| 30 |
+
from ._cookie_helpers import preserve_morsel_with_coded_value
|
| 31 |
+
from .abc import AbstractCookieJar, ClearCookiePredicate
|
| 32 |
+
from .helpers import is_ip_address
|
| 33 |
+
from .typedefs import LooseCookies, PathLike, StrOrURL
|
| 34 |
+
|
| 35 |
+
__all__ = ("CookieJar", "DummyCookieJar")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
CookieItem = Union[str, "Morsel[str]"]
|
| 39 |
+
|
| 40 |
+
# We cache these string methods here as their use is in performance critical code.
|
| 41 |
+
_FORMAT_PATH = "{}/{}".format
|
| 42 |
+
_FORMAT_DOMAIN_REVERSED = "{1}.{0}".format
|
| 43 |
+
|
| 44 |
+
# The minimum number of scheduled cookie expirations before we start cleaning up
|
| 45 |
+
# the expiration heap. This is a performance optimization to avoid cleaning up the
|
| 46 |
+
# heap too often when there are only a few scheduled expirations.
|
| 47 |
+
_MIN_SCHEDULED_COOKIE_EXPIRATION = 100
|
| 48 |
+
_SIMPLE_COOKIE = SimpleCookie()
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class CookieJar(AbstractCookieJar):
|
| 52 |
+
"""Implements cookie storage adhering to RFC 6265."""
|
| 53 |
+
|
| 54 |
+
DATE_TOKENS_RE = re.compile(
|
| 55 |
+
r"[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]*"
|
| 56 |
+
r"(?P<token>[\x00-\x08\x0A-\x1F\d:a-zA-Z\x7F-\xFF]+)"
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
DATE_HMS_TIME_RE = re.compile(r"(\d{1,2}):(\d{1,2}):(\d{1,2})")
|
| 60 |
+
|
| 61 |
+
DATE_DAY_OF_MONTH_RE = re.compile(r"(\d{1,2})")
|
| 62 |
+
|
| 63 |
+
DATE_MONTH_RE = re.compile(
|
| 64 |
+
"(jan)|(feb)|(mar)|(apr)|(may)|(jun)|(jul)|(aug)|(sep)|(oct)|(nov)|(dec)",
|
| 65 |
+
re.I,
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
DATE_YEAR_RE = re.compile(r"(\d{2,4})")
|
| 69 |
+
|
| 70 |
+
# calendar.timegm() fails for timestamps after datetime.datetime.max
|
| 71 |
+
# Minus one as a loss of precision occurs when timestamp() is called.
|
| 72 |
+
MAX_TIME = (
|
| 73 |
+
int(datetime.datetime.max.replace(tzinfo=datetime.timezone.utc).timestamp()) - 1
|
| 74 |
+
)
|
| 75 |
+
try:
|
| 76 |
+
calendar.timegm(time.gmtime(MAX_TIME))
|
| 77 |
+
except (OSError, ValueError):
|
| 78 |
+
# Hit the maximum representable time on Windows
|
| 79 |
+
# https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/localtime-localtime32-localtime64
|
| 80 |
+
# Throws ValueError on PyPy 3.9, OSError elsewhere
|
| 81 |
+
MAX_TIME = calendar.timegm((3000, 12, 31, 23, 59, 59, -1, -1, -1))
|
| 82 |
+
except OverflowError:
|
| 83 |
+
# #4515: datetime.max may not be representable on 32-bit platforms
|
| 84 |
+
MAX_TIME = 2**31 - 1
|
| 85 |
+
# Avoid minuses in the future, 3x faster
|
| 86 |
+
SUB_MAX_TIME = MAX_TIME - 1
|
| 87 |
+
|
| 88 |
+
def __init__(
|
| 89 |
+
self,
|
| 90 |
+
*,
|
| 91 |
+
unsafe: bool = False,
|
| 92 |
+
quote_cookie: bool = True,
|
| 93 |
+
treat_as_secure_origin: Union[StrOrURL, List[StrOrURL], None] = None,
|
| 94 |
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
| 95 |
+
) -> None:
|
| 96 |
+
super().__init__(loop=loop)
|
| 97 |
+
self._cookies: DefaultDict[Tuple[str, str], SimpleCookie] = defaultdict(
|
| 98 |
+
SimpleCookie
|
| 99 |
+
)
|
| 100 |
+
self._morsel_cache: DefaultDict[Tuple[str, str], Dict[str, Morsel[str]]] = (
|
| 101 |
+
defaultdict(dict)
|
| 102 |
+
)
|
| 103 |
+
self._host_only_cookies: Set[Tuple[str, str]] = set()
|
| 104 |
+
self._unsafe = unsafe
|
| 105 |
+
self._quote_cookie = quote_cookie
|
| 106 |
+
if treat_as_secure_origin is None:
|
| 107 |
+
treat_as_secure_origin = []
|
| 108 |
+
elif isinstance(treat_as_secure_origin, URL):
|
| 109 |
+
treat_as_secure_origin = [treat_as_secure_origin.origin()]
|
| 110 |
+
elif isinstance(treat_as_secure_origin, str):
|
| 111 |
+
treat_as_secure_origin = [URL(treat_as_secure_origin).origin()]
|
| 112 |
+
else:
|
| 113 |
+
treat_as_secure_origin = [
|
| 114 |
+
URL(url).origin() if isinstance(url, str) else url.origin()
|
| 115 |
+
for url in treat_as_secure_origin
|
| 116 |
+
]
|
| 117 |
+
self._treat_as_secure_origin = treat_as_secure_origin
|
| 118 |
+
self._expire_heap: List[Tuple[float, Tuple[str, str, str]]] = []
|
| 119 |
+
self._expirations: Dict[Tuple[str, str, str], float] = {}
|
| 120 |
+
|
| 121 |
+
@property
|
| 122 |
+
def quote_cookie(self) -> bool:
|
| 123 |
+
return self._quote_cookie
|
| 124 |
+
|
| 125 |
+
def save(self, file_path: PathLike) -> None:
|
| 126 |
+
file_path = pathlib.Path(file_path)
|
| 127 |
+
with file_path.open(mode="wb") as f:
|
| 128 |
+
pickle.dump(self._cookies, f, pickle.HIGHEST_PROTOCOL)
|
| 129 |
+
|
| 130 |
+
def load(self, file_path: PathLike) -> None:
|
| 131 |
+
file_path = pathlib.Path(file_path)
|
| 132 |
+
with file_path.open(mode="rb") as f:
|
| 133 |
+
self._cookies = pickle.load(f)
|
| 134 |
+
|
| 135 |
+
def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
|
| 136 |
+
if predicate is None:
|
| 137 |
+
self._expire_heap.clear()
|
| 138 |
+
self._cookies.clear()
|
| 139 |
+
self._morsel_cache.clear()
|
| 140 |
+
self._host_only_cookies.clear()
|
| 141 |
+
self._expirations.clear()
|
| 142 |
+
return
|
| 143 |
+
|
| 144 |
+
now = time.time()
|
| 145 |
+
to_del = [
|
| 146 |
+
key
|
| 147 |
+
for (domain, path), cookie in self._cookies.items()
|
| 148 |
+
for name, morsel in cookie.items()
|
| 149 |
+
if (
|
| 150 |
+
(key := (domain, path, name)) in self._expirations
|
| 151 |
+
and self._expirations[key] <= now
|
| 152 |
+
)
|
| 153 |
+
or predicate(morsel)
|
| 154 |
+
]
|
| 155 |
+
if to_del:
|
| 156 |
+
self._delete_cookies(to_del)
|
| 157 |
+
|
| 158 |
+
def clear_domain(self, domain: str) -> None:
|
| 159 |
+
self.clear(lambda x: self._is_domain_match(domain, x["domain"]))
|
| 160 |
+
|
| 161 |
+
def __iter__(self) -> "Iterator[Morsel[str]]":
|
| 162 |
+
self._do_expiration()
|
| 163 |
+
for val in self._cookies.values():
|
| 164 |
+
yield from val.values()
|
| 165 |
+
|
| 166 |
+
def __len__(self) -> int:
|
| 167 |
+
"""Return number of cookies.
|
| 168 |
+
|
| 169 |
+
This function does not iterate self to avoid unnecessary expiration
|
| 170 |
+
checks.
|
| 171 |
+
"""
|
| 172 |
+
return sum(len(cookie.values()) for cookie in self._cookies.values())
|
| 173 |
+
|
| 174 |
+
def _do_expiration(self) -> None:
|
| 175 |
+
"""Remove expired cookies."""
|
| 176 |
+
if not (expire_heap_len := len(self._expire_heap)):
|
| 177 |
+
return
|
| 178 |
+
|
| 179 |
+
# If the expiration heap grows larger than the number expirations
|
| 180 |
+
# times two, we clean it up to avoid keeping expired entries in
|
| 181 |
+
# the heap and consuming memory. We guard this with a minimum
|
| 182 |
+
# threshold to avoid cleaning up the heap too often when there are
|
| 183 |
+
# only a few scheduled expirations.
|
| 184 |
+
if (
|
| 185 |
+
expire_heap_len > _MIN_SCHEDULED_COOKIE_EXPIRATION
|
| 186 |
+
and expire_heap_len > len(self._expirations) * 2
|
| 187 |
+
):
|
| 188 |
+
# Remove any expired entries from the expiration heap
|
| 189 |
+
# that do not match the expiration time in the expirations
|
| 190 |
+
# as it means the cookie has been re-added to the heap
|
| 191 |
+
# with a different expiration time.
|
| 192 |
+
self._expire_heap = [
|
| 193 |
+
entry
|
| 194 |
+
for entry in self._expire_heap
|
| 195 |
+
if self._expirations.get(entry[1]) == entry[0]
|
| 196 |
+
]
|
| 197 |
+
heapq.heapify(self._expire_heap)
|
| 198 |
+
|
| 199 |
+
now = time.time()
|
| 200 |
+
to_del: List[Tuple[str, str, str]] = []
|
| 201 |
+
# Find any expired cookies and add them to the to-delete list
|
| 202 |
+
while self._expire_heap:
|
| 203 |
+
when, cookie_key = self._expire_heap[0]
|
| 204 |
+
if when > now:
|
| 205 |
+
break
|
| 206 |
+
heapq.heappop(self._expire_heap)
|
| 207 |
+
# Check if the cookie hasn't been re-added to the heap
|
| 208 |
+
# with a different expiration time as it will be removed
|
| 209 |
+
# later when it reaches the top of the heap and its
|
| 210 |
+
# expiration time is met.
|
| 211 |
+
if self._expirations.get(cookie_key) == when:
|
| 212 |
+
to_del.append(cookie_key)
|
| 213 |
+
|
| 214 |
+
if to_del:
|
| 215 |
+
self._delete_cookies(to_del)
|
| 216 |
+
|
| 217 |
+
def _delete_cookies(self, to_del: List[Tuple[str, str, str]]) -> None:
|
| 218 |
+
for domain, path, name in to_del:
|
| 219 |
+
self._host_only_cookies.discard((domain, name))
|
| 220 |
+
self._cookies[(domain, path)].pop(name, None)
|
| 221 |
+
self._morsel_cache[(domain, path)].pop(name, None)
|
| 222 |
+
self._expirations.pop((domain, path, name), None)
|
| 223 |
+
|
| 224 |
+
def _expire_cookie(self, when: float, domain: str, path: str, name: str) -> None:
|
| 225 |
+
cookie_key = (domain, path, name)
|
| 226 |
+
if self._expirations.get(cookie_key) == when:
|
| 227 |
+
# Avoid adding duplicates to the heap
|
| 228 |
+
return
|
| 229 |
+
heapq.heappush(self._expire_heap, (when, cookie_key))
|
| 230 |
+
self._expirations[cookie_key] = when
|
| 231 |
+
|
| 232 |
+
def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> None:
|
| 233 |
+
"""Update cookies."""
|
| 234 |
+
hostname = response_url.raw_host
|
| 235 |
+
|
| 236 |
+
if not self._unsafe and is_ip_address(hostname):
|
| 237 |
+
# Don't accept cookies from IPs
|
| 238 |
+
return
|
| 239 |
+
|
| 240 |
+
if isinstance(cookies, Mapping):
|
| 241 |
+
cookies = cookies.items()
|
| 242 |
+
|
| 243 |
+
for name, cookie in cookies:
|
| 244 |
+
if not isinstance(cookie, Morsel):
|
| 245 |
+
tmp = SimpleCookie()
|
| 246 |
+
tmp[name] = cookie # type: ignore[assignment]
|
| 247 |
+
cookie = tmp[name]
|
| 248 |
+
|
| 249 |
+
domain = cookie["domain"]
|
| 250 |
+
|
| 251 |
+
# ignore domains with trailing dots
|
| 252 |
+
if domain and domain[-1] == ".":
|
| 253 |
+
domain = ""
|
| 254 |
+
del cookie["domain"]
|
| 255 |
+
|
| 256 |
+
if not domain and hostname is not None:
|
| 257 |
+
# Set the cookie's domain to the response hostname
|
| 258 |
+
# and set its host-only-flag
|
| 259 |
+
self._host_only_cookies.add((hostname, name))
|
| 260 |
+
domain = cookie["domain"] = hostname
|
| 261 |
+
|
| 262 |
+
if domain and domain[0] == ".":
|
| 263 |
+
# Remove leading dot
|
| 264 |
+
domain = domain[1:]
|
| 265 |
+
cookie["domain"] = domain
|
| 266 |
+
|
| 267 |
+
if hostname and not self._is_domain_match(domain, hostname):
|
| 268 |
+
# Setting cookies for different domains is not allowed
|
| 269 |
+
continue
|
| 270 |
+
|
| 271 |
+
path = cookie["path"]
|
| 272 |
+
if not path or path[0] != "/":
|
| 273 |
+
# Set the cookie's path to the response path
|
| 274 |
+
path = response_url.path
|
| 275 |
+
if not path.startswith("/"):
|
| 276 |
+
path = "/"
|
| 277 |
+
else:
|
| 278 |
+
# Cut everything from the last slash to the end
|
| 279 |
+
path = "/" + path[1 : path.rfind("/")]
|
| 280 |
+
cookie["path"] = path
|
| 281 |
+
path = path.rstrip("/")
|
| 282 |
+
|
| 283 |
+
if max_age := cookie["max-age"]:
|
| 284 |
+
try:
|
| 285 |
+
delta_seconds = int(max_age)
|
| 286 |
+
max_age_expiration = min(time.time() + delta_seconds, self.MAX_TIME)
|
| 287 |
+
self._expire_cookie(max_age_expiration, domain, path, name)
|
| 288 |
+
except ValueError:
|
| 289 |
+
cookie["max-age"] = ""
|
| 290 |
+
|
| 291 |
+
elif expires := cookie["expires"]:
|
| 292 |
+
if expire_time := self._parse_date(expires):
|
| 293 |
+
self._expire_cookie(expire_time, domain, path, name)
|
| 294 |
+
else:
|
| 295 |
+
cookie["expires"] = ""
|
| 296 |
+
|
| 297 |
+
key = (domain, path)
|
| 298 |
+
if self._cookies[key].get(name) != cookie:
|
| 299 |
+
# Don't blow away the cache if the same
|
| 300 |
+
# cookie gets set again
|
| 301 |
+
self._cookies[key][name] = cookie
|
| 302 |
+
self._morsel_cache[key].pop(name, None)
|
| 303 |
+
|
| 304 |
+
self._do_expiration()
|
| 305 |
+
|
| 306 |
+
def filter_cookies(self, request_url: URL = URL()) -> "BaseCookie[str]":
|
| 307 |
+
"""Returns this jar's cookies filtered by their attributes."""
|
| 308 |
+
# We always use BaseCookie now since all
|
| 309 |
+
# cookies set on on filtered are fully constructed
|
| 310 |
+
# Morsels, not just names and values.
|
| 311 |
+
filtered: BaseCookie[str] = BaseCookie()
|
| 312 |
+
if not self._cookies:
|
| 313 |
+
# Skip do_expiration() if there are no cookies.
|
| 314 |
+
return filtered
|
| 315 |
+
self._do_expiration()
|
| 316 |
+
if not self._cookies:
|
| 317 |
+
# Skip rest of function if no non-expired cookies.
|
| 318 |
+
return filtered
|
| 319 |
+
if type(request_url) is not URL:
|
| 320 |
+
warnings.warn(
|
| 321 |
+
"filter_cookies expects yarl.URL instances only,"
|
| 322 |
+
f"and will stop working in 4.x, got {type(request_url)}",
|
| 323 |
+
DeprecationWarning,
|
| 324 |
+
stacklevel=2,
|
| 325 |
+
)
|
| 326 |
+
request_url = URL(request_url)
|
| 327 |
+
hostname = request_url.raw_host or ""
|
| 328 |
+
|
| 329 |
+
is_not_secure = request_url.scheme not in ("https", "wss")
|
| 330 |
+
if is_not_secure and self._treat_as_secure_origin:
|
| 331 |
+
request_origin = URL()
|
| 332 |
+
with contextlib.suppress(ValueError):
|
| 333 |
+
request_origin = request_url.origin()
|
| 334 |
+
is_not_secure = request_origin not in self._treat_as_secure_origin
|
| 335 |
+
|
| 336 |
+
# Send shared cookie
|
| 337 |
+
key = ("", "")
|
| 338 |
+
for c in self._cookies[key].values():
|
| 339 |
+
# Check cache first
|
| 340 |
+
if c.key in self._morsel_cache[key]:
|
| 341 |
+
filtered[c.key] = self._morsel_cache[key][c.key]
|
| 342 |
+
continue
|
| 343 |
+
|
| 344 |
+
# Build and cache the morsel
|
| 345 |
+
mrsl_val = self._build_morsel(c)
|
| 346 |
+
self._morsel_cache[key][c.key] = mrsl_val
|
| 347 |
+
filtered[c.key] = mrsl_val
|
| 348 |
+
|
| 349 |
+
if is_ip_address(hostname):
|
| 350 |
+
if not self._unsafe:
|
| 351 |
+
return filtered
|
| 352 |
+
domains: Iterable[str] = (hostname,)
|
| 353 |
+
else:
|
| 354 |
+
# Get all the subdomains that might match a cookie (e.g. "foo.bar.com", "bar.com", "com")
|
| 355 |
+
domains = itertools.accumulate(
|
| 356 |
+
reversed(hostname.split(".")), _FORMAT_DOMAIN_REVERSED
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
# Get all the path prefixes that might match a cookie (e.g. "", "/foo", "/foo/bar")
|
| 360 |
+
paths = itertools.accumulate(request_url.path.split("/"), _FORMAT_PATH)
|
| 361 |
+
# Create every combination of (domain, path) pairs.
|
| 362 |
+
pairs = itertools.product(domains, paths)
|
| 363 |
+
|
| 364 |
+
path_len = len(request_url.path)
|
| 365 |
+
# Point 2: https://www.rfc-editor.org/rfc/rfc6265.html#section-5.4
|
| 366 |
+
for p in pairs:
|
| 367 |
+
if p not in self._cookies:
|
| 368 |
+
continue
|
| 369 |
+
for name, cookie in self._cookies[p].items():
|
| 370 |
+
domain = cookie["domain"]
|
| 371 |
+
|
| 372 |
+
if (domain, name) in self._host_only_cookies and domain != hostname:
|
| 373 |
+
continue
|
| 374 |
+
|
| 375 |
+
# Skip edge case when the cookie has a trailing slash but request doesn't.
|
| 376 |
+
if len(cookie["path"]) > path_len:
|
| 377 |
+
continue
|
| 378 |
+
|
| 379 |
+
if is_not_secure and cookie["secure"]:
|
| 380 |
+
continue
|
| 381 |
+
|
| 382 |
+
# We already built the Morsel so reuse it here
|
| 383 |
+
if name in self._morsel_cache[p]:
|
| 384 |
+
filtered[name] = self._morsel_cache[p][name]
|
| 385 |
+
continue
|
| 386 |
+
|
| 387 |
+
# Build and cache the morsel
|
| 388 |
+
mrsl_val = self._build_morsel(cookie)
|
| 389 |
+
self._morsel_cache[p][name] = mrsl_val
|
| 390 |
+
filtered[name] = mrsl_val
|
| 391 |
+
|
| 392 |
+
return filtered
|
| 393 |
+
|
| 394 |
+
def _build_morsel(self, cookie: Morsel[str]) -> Morsel[str]:
|
| 395 |
+
"""Build a morsel for sending, respecting quote_cookie setting."""
|
| 396 |
+
if self._quote_cookie and cookie.coded_value and cookie.coded_value[0] == '"':
|
| 397 |
+
return preserve_morsel_with_coded_value(cookie)
|
| 398 |
+
morsel: Morsel[str] = Morsel()
|
| 399 |
+
if self._quote_cookie:
|
| 400 |
+
value, coded_value = _SIMPLE_COOKIE.value_encode(cookie.value)
|
| 401 |
+
else:
|
| 402 |
+
coded_value = value = cookie.value
|
| 403 |
+
# We use __setstate__ instead of the public set() API because it allows us to
|
| 404 |
+
# bypass validation and set already validated state. This is more stable than
|
| 405 |
+
# setting protected attributes directly and unlikely to change since it would
|
| 406 |
+
# break pickling.
|
| 407 |
+
morsel.__setstate__({"key": cookie.key, "value": value, "coded_value": coded_value}) # type: ignore[attr-defined]
|
| 408 |
+
return morsel
|
| 409 |
+
|
| 410 |
+
@staticmethod
|
| 411 |
+
def _is_domain_match(domain: str, hostname: str) -> bool:
|
| 412 |
+
"""Implements domain matching adhering to RFC 6265."""
|
| 413 |
+
if hostname == domain:
|
| 414 |
+
return True
|
| 415 |
+
|
| 416 |
+
if not hostname.endswith(domain):
|
| 417 |
+
return False
|
| 418 |
+
|
| 419 |
+
non_matching = hostname[: -len(domain)]
|
| 420 |
+
|
| 421 |
+
if not non_matching.endswith("."):
|
| 422 |
+
return False
|
| 423 |
+
|
| 424 |
+
return not is_ip_address(hostname)
|
| 425 |
+
|
| 426 |
+
@classmethod
|
| 427 |
+
def _parse_date(cls, date_str: str) -> Optional[int]:
|
| 428 |
+
"""Implements date string parsing adhering to RFC 6265."""
|
| 429 |
+
if not date_str:
|
| 430 |
+
return None
|
| 431 |
+
|
| 432 |
+
found_time = False
|
| 433 |
+
found_day = False
|
| 434 |
+
found_month = False
|
| 435 |
+
found_year = False
|
| 436 |
+
|
| 437 |
+
hour = minute = second = 0
|
| 438 |
+
day = 0
|
| 439 |
+
month = 0
|
| 440 |
+
year = 0
|
| 441 |
+
|
| 442 |
+
for token_match in cls.DATE_TOKENS_RE.finditer(date_str):
|
| 443 |
+
|
| 444 |
+
token = token_match.group("token")
|
| 445 |
+
|
| 446 |
+
if not found_time:
|
| 447 |
+
time_match = cls.DATE_HMS_TIME_RE.match(token)
|
| 448 |
+
if time_match:
|
| 449 |
+
found_time = True
|
| 450 |
+
hour, minute, second = (int(s) for s in time_match.groups())
|
| 451 |
+
continue
|
| 452 |
+
|
| 453 |
+
if not found_day:
|
| 454 |
+
day_match = cls.DATE_DAY_OF_MONTH_RE.match(token)
|
| 455 |
+
if day_match:
|
| 456 |
+
found_day = True
|
| 457 |
+
day = int(day_match.group())
|
| 458 |
+
continue
|
| 459 |
+
|
| 460 |
+
if not found_month:
|
| 461 |
+
month_match = cls.DATE_MONTH_RE.match(token)
|
| 462 |
+
if month_match:
|
| 463 |
+
found_month = True
|
| 464 |
+
assert month_match.lastindex is not None
|
| 465 |
+
month = month_match.lastindex
|
| 466 |
+
continue
|
| 467 |
+
|
| 468 |
+
if not found_year:
|
| 469 |
+
year_match = cls.DATE_YEAR_RE.match(token)
|
| 470 |
+
if year_match:
|
| 471 |
+
found_year = True
|
| 472 |
+
year = int(year_match.group())
|
| 473 |
+
|
| 474 |
+
if 70 <= year <= 99:
|
| 475 |
+
year += 1900
|
| 476 |
+
elif 0 <= year <= 69:
|
| 477 |
+
year += 2000
|
| 478 |
+
|
| 479 |
+
if False in (found_day, found_month, found_year, found_time):
|
| 480 |
+
return None
|
| 481 |
+
|
| 482 |
+
if not 1 <= day <= 31:
|
| 483 |
+
return None
|
| 484 |
+
|
| 485 |
+
if year < 1601 or hour > 23 or minute > 59 or second > 59:
|
| 486 |
+
return None
|
| 487 |
+
|
| 488 |
+
return calendar.timegm((year, month, day, hour, minute, second, -1, -1, -1))
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
class DummyCookieJar(AbstractCookieJar):
|
| 492 |
+
"""Implements a dummy cookie storage.
|
| 493 |
+
|
| 494 |
+
It can be used with the ClientSession when no cookie processing is needed.
|
| 495 |
+
|
| 496 |
+
"""
|
| 497 |
+
|
| 498 |
+
def __init__(self, *, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
|
| 499 |
+
super().__init__(loop=loop)
|
| 500 |
+
|
| 501 |
+
def __iter__(self) -> "Iterator[Morsel[str]]":
|
| 502 |
+
while False:
|
| 503 |
+
yield None
|
| 504 |
+
|
| 505 |
+
def __len__(self) -> int:
|
| 506 |
+
return 0
|
| 507 |
+
|
| 508 |
+
@property
|
| 509 |
+
def quote_cookie(self) -> bool:
|
| 510 |
+
return True
|
| 511 |
+
|
| 512 |
+
def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
|
| 513 |
+
pass
|
| 514 |
+
|
| 515 |
+
def clear_domain(self, domain: str) -> None:
|
| 516 |
+
pass
|
| 517 |
+
|
| 518 |
+
def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> None:
|
| 519 |
+
pass
|
| 520 |
+
|
| 521 |
+
def filter_cookies(self, request_url: URL) -> "BaseCookie[str]":
|
| 522 |
+
return SimpleCookie()
|
aiohttp/formdata.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import warnings
|
| 3 |
+
from typing import Any, Iterable, List, Optional
|
| 4 |
+
from urllib.parse import urlencode
|
| 5 |
+
|
| 6 |
+
from multidict import MultiDict, MultiDictProxy
|
| 7 |
+
|
| 8 |
+
from . import hdrs, multipart, payload
|
| 9 |
+
from .helpers import guess_filename
|
| 10 |
+
from .payload import Payload
|
| 11 |
+
|
| 12 |
+
__all__ = ("FormData",)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class FormData:
|
| 16 |
+
"""Helper class for form body generation.
|
| 17 |
+
|
| 18 |
+
Supports multipart/form-data and application/x-www-form-urlencoded.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
def __init__(
|
| 22 |
+
self,
|
| 23 |
+
fields: Iterable[Any] = (),
|
| 24 |
+
quote_fields: bool = True,
|
| 25 |
+
charset: Optional[str] = None,
|
| 26 |
+
*,
|
| 27 |
+
default_to_multipart: bool = False,
|
| 28 |
+
) -> None:
|
| 29 |
+
self._writer = multipart.MultipartWriter("form-data")
|
| 30 |
+
self._fields: List[Any] = []
|
| 31 |
+
self._is_multipart = default_to_multipart
|
| 32 |
+
self._quote_fields = quote_fields
|
| 33 |
+
self._charset = charset
|
| 34 |
+
|
| 35 |
+
if isinstance(fields, dict):
|
| 36 |
+
fields = list(fields.items())
|
| 37 |
+
elif not isinstance(fields, (list, tuple)):
|
| 38 |
+
fields = (fields,)
|
| 39 |
+
self.add_fields(*fields)
|
| 40 |
+
|
| 41 |
+
@property
|
| 42 |
+
def is_multipart(self) -> bool:
|
| 43 |
+
return self._is_multipart
|
| 44 |
+
|
| 45 |
+
def add_field(
|
| 46 |
+
self,
|
| 47 |
+
name: str,
|
| 48 |
+
value: Any,
|
| 49 |
+
*,
|
| 50 |
+
content_type: Optional[str] = None,
|
| 51 |
+
filename: Optional[str] = None,
|
| 52 |
+
content_transfer_encoding: Optional[str] = None,
|
| 53 |
+
) -> None:
|
| 54 |
+
|
| 55 |
+
if isinstance(value, io.IOBase):
|
| 56 |
+
self._is_multipart = True
|
| 57 |
+
elif isinstance(value, (bytes, bytearray, memoryview)):
|
| 58 |
+
msg = (
|
| 59 |
+
"In v4, passing bytes will no longer create a file field. "
|
| 60 |
+
"Please explicitly use the filename parameter or pass a BytesIO object."
|
| 61 |
+
)
|
| 62 |
+
if filename is None and content_transfer_encoding is None:
|
| 63 |
+
warnings.warn(msg, DeprecationWarning)
|
| 64 |
+
filename = name
|
| 65 |
+
|
| 66 |
+
type_options: MultiDict[str] = MultiDict({"name": name})
|
| 67 |
+
if filename is not None and not isinstance(filename, str):
|
| 68 |
+
raise TypeError("filename must be an instance of str. Got: %s" % filename)
|
| 69 |
+
if filename is None and isinstance(value, io.IOBase):
|
| 70 |
+
filename = guess_filename(value, name)
|
| 71 |
+
if filename is not None:
|
| 72 |
+
type_options["filename"] = filename
|
| 73 |
+
self._is_multipart = True
|
| 74 |
+
|
| 75 |
+
headers = {}
|
| 76 |
+
if content_type is not None:
|
| 77 |
+
if not isinstance(content_type, str):
|
| 78 |
+
raise TypeError(
|
| 79 |
+
"content_type must be an instance of str. Got: %s" % content_type
|
| 80 |
+
)
|
| 81 |
+
headers[hdrs.CONTENT_TYPE] = content_type
|
| 82 |
+
self._is_multipart = True
|
| 83 |
+
if content_transfer_encoding is not None:
|
| 84 |
+
if not isinstance(content_transfer_encoding, str):
|
| 85 |
+
raise TypeError(
|
| 86 |
+
"content_transfer_encoding must be an instance"
|
| 87 |
+
" of str. Got: %s" % content_transfer_encoding
|
| 88 |
+
)
|
| 89 |
+
msg = (
|
| 90 |
+
"content_transfer_encoding is deprecated. "
|
| 91 |
+
"To maintain compatibility with v4 please pass a BytesPayload."
|
| 92 |
+
)
|
| 93 |
+
warnings.warn(msg, DeprecationWarning)
|
| 94 |
+
self._is_multipart = True
|
| 95 |
+
|
| 96 |
+
self._fields.append((type_options, headers, value))
|
| 97 |
+
|
| 98 |
+
def add_fields(self, *fields: Any) -> None:
|
| 99 |
+
to_add = list(fields)
|
| 100 |
+
|
| 101 |
+
while to_add:
|
| 102 |
+
rec = to_add.pop(0)
|
| 103 |
+
|
| 104 |
+
if isinstance(rec, io.IOBase):
|
| 105 |
+
k = guess_filename(rec, "unknown")
|
| 106 |
+
self.add_field(k, rec) # type: ignore[arg-type]
|
| 107 |
+
|
| 108 |
+
elif isinstance(rec, (MultiDictProxy, MultiDict)):
|
| 109 |
+
to_add.extend(rec.items())
|
| 110 |
+
|
| 111 |
+
elif isinstance(rec, (list, tuple)) and len(rec) == 2:
|
| 112 |
+
k, fp = rec
|
| 113 |
+
self.add_field(k, fp)
|
| 114 |
+
|
| 115 |
+
else:
|
| 116 |
+
raise TypeError(
|
| 117 |
+
"Only io.IOBase, multidict and (name, file) "
|
| 118 |
+
"pairs allowed, use .add_field() for passing "
|
| 119 |
+
"more complex parameters, got {!r}".format(rec)
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
def _gen_form_urlencoded(self) -> payload.BytesPayload:
|
| 123 |
+
# form data (x-www-form-urlencoded)
|
| 124 |
+
data = []
|
| 125 |
+
for type_options, _, value in self._fields:
|
| 126 |
+
data.append((type_options["name"], value))
|
| 127 |
+
|
| 128 |
+
charset = self._charset if self._charset is not None else "utf-8"
|
| 129 |
+
|
| 130 |
+
if charset == "utf-8":
|
| 131 |
+
content_type = "application/x-www-form-urlencoded"
|
| 132 |
+
else:
|
| 133 |
+
content_type = "application/x-www-form-urlencoded; charset=%s" % charset
|
| 134 |
+
|
| 135 |
+
return payload.BytesPayload(
|
| 136 |
+
urlencode(data, doseq=True, encoding=charset).encode(),
|
| 137 |
+
content_type=content_type,
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
def _gen_form_data(self) -> multipart.MultipartWriter:
|
| 141 |
+
"""Encode a list of fields using the multipart/form-data MIME format"""
|
| 142 |
+
for dispparams, headers, value in self._fields:
|
| 143 |
+
try:
|
| 144 |
+
if hdrs.CONTENT_TYPE in headers:
|
| 145 |
+
part = payload.get_payload(
|
| 146 |
+
value,
|
| 147 |
+
content_type=headers[hdrs.CONTENT_TYPE],
|
| 148 |
+
headers=headers,
|
| 149 |
+
encoding=self._charset,
|
| 150 |
+
)
|
| 151 |
+
else:
|
| 152 |
+
part = payload.get_payload(
|
| 153 |
+
value, headers=headers, encoding=self._charset
|
| 154 |
+
)
|
| 155 |
+
except Exception as exc:
|
| 156 |
+
raise TypeError(
|
| 157 |
+
"Can not serialize value type: %r\n "
|
| 158 |
+
"headers: %r\n value: %r" % (type(value), headers, value)
|
| 159 |
+
) from exc
|
| 160 |
+
|
| 161 |
+
if dispparams:
|
| 162 |
+
part.set_content_disposition(
|
| 163 |
+
"form-data", quote_fields=self._quote_fields, **dispparams
|
| 164 |
+
)
|
| 165 |
+
# FIXME cgi.FieldStorage doesn't likes body parts with
|
| 166 |
+
# Content-Length which were sent via chunked transfer encoding
|
| 167 |
+
assert part.headers is not None
|
| 168 |
+
part.headers.popall(hdrs.CONTENT_LENGTH, None)
|
| 169 |
+
|
| 170 |
+
self._writer.append_payload(part)
|
| 171 |
+
|
| 172 |
+
self._fields.clear()
|
| 173 |
+
return self._writer
|
| 174 |
+
|
| 175 |
+
def __call__(self) -> Payload:
|
| 176 |
+
if self._is_multipart:
|
| 177 |
+
return self._gen_form_data()
|
| 178 |
+
else:
|
| 179 |
+
return self._gen_form_urlencoded()
|
aiohttp/hdrs.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HTTP Headers constants."""
|
| 2 |
+
|
| 3 |
+
# After changing the file content call ./tools/gen.py
|
| 4 |
+
# to regenerate the headers parser
|
| 5 |
+
import itertools
|
| 6 |
+
from typing import Final, Set
|
| 7 |
+
|
| 8 |
+
from multidict import istr
|
| 9 |
+
|
| 10 |
+
METH_ANY: Final[str] = "*"
|
| 11 |
+
METH_CONNECT: Final[str] = "CONNECT"
|
| 12 |
+
METH_HEAD: Final[str] = "HEAD"
|
| 13 |
+
METH_GET: Final[str] = "GET"
|
| 14 |
+
METH_DELETE: Final[str] = "DELETE"
|
| 15 |
+
METH_OPTIONS: Final[str] = "OPTIONS"
|
| 16 |
+
METH_PATCH: Final[str] = "PATCH"
|
| 17 |
+
METH_POST: Final[str] = "POST"
|
| 18 |
+
METH_PUT: Final[str] = "PUT"
|
| 19 |
+
METH_TRACE: Final[str] = "TRACE"
|
| 20 |
+
|
| 21 |
+
METH_ALL: Final[Set[str]] = {
|
| 22 |
+
METH_CONNECT,
|
| 23 |
+
METH_HEAD,
|
| 24 |
+
METH_GET,
|
| 25 |
+
METH_DELETE,
|
| 26 |
+
METH_OPTIONS,
|
| 27 |
+
METH_PATCH,
|
| 28 |
+
METH_POST,
|
| 29 |
+
METH_PUT,
|
| 30 |
+
METH_TRACE,
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
ACCEPT: Final[istr] = istr("Accept")
|
| 34 |
+
ACCEPT_CHARSET: Final[istr] = istr("Accept-Charset")
|
| 35 |
+
ACCEPT_ENCODING: Final[istr] = istr("Accept-Encoding")
|
| 36 |
+
ACCEPT_LANGUAGE: Final[istr] = istr("Accept-Language")
|
| 37 |
+
ACCEPT_RANGES: Final[istr] = istr("Accept-Ranges")
|
| 38 |
+
ACCESS_CONTROL_MAX_AGE: Final[istr] = istr("Access-Control-Max-Age")
|
| 39 |
+
ACCESS_CONTROL_ALLOW_CREDENTIALS: Final[istr] = istr("Access-Control-Allow-Credentials")
|
| 40 |
+
ACCESS_CONTROL_ALLOW_HEADERS: Final[istr] = istr("Access-Control-Allow-Headers")
|
| 41 |
+
ACCESS_CONTROL_ALLOW_METHODS: Final[istr] = istr("Access-Control-Allow-Methods")
|
| 42 |
+
ACCESS_CONTROL_ALLOW_ORIGIN: Final[istr] = istr("Access-Control-Allow-Origin")
|
| 43 |
+
ACCESS_CONTROL_EXPOSE_HEADERS: Final[istr] = istr("Access-Control-Expose-Headers")
|
| 44 |
+
ACCESS_CONTROL_REQUEST_HEADERS: Final[istr] = istr("Access-Control-Request-Headers")
|
| 45 |
+
ACCESS_CONTROL_REQUEST_METHOD: Final[istr] = istr("Access-Control-Request-Method")
|
| 46 |
+
AGE: Final[istr] = istr("Age")
|
| 47 |
+
ALLOW: Final[istr] = istr("Allow")
|
| 48 |
+
AUTHORIZATION: Final[istr] = istr("Authorization")
|
| 49 |
+
CACHE_CONTROL: Final[istr] = istr("Cache-Control")
|
| 50 |
+
CONNECTION: Final[istr] = istr("Connection")
|
| 51 |
+
CONTENT_DISPOSITION: Final[istr] = istr("Content-Disposition")
|
| 52 |
+
CONTENT_ENCODING: Final[istr] = istr("Content-Encoding")
|
| 53 |
+
CONTENT_LANGUAGE: Final[istr] = istr("Content-Language")
|
| 54 |
+
CONTENT_LENGTH: Final[istr] = istr("Content-Length")
|
| 55 |
+
CONTENT_LOCATION: Final[istr] = istr("Content-Location")
|
| 56 |
+
CONTENT_MD5: Final[istr] = istr("Content-MD5")
|
| 57 |
+
CONTENT_RANGE: Final[istr] = istr("Content-Range")
|
| 58 |
+
CONTENT_TRANSFER_ENCODING: Final[istr] = istr("Content-Transfer-Encoding")
|
| 59 |
+
CONTENT_TYPE: Final[istr] = istr("Content-Type")
|
| 60 |
+
COOKIE: Final[istr] = istr("Cookie")
|
| 61 |
+
DATE: Final[istr] = istr("Date")
|
| 62 |
+
DESTINATION: Final[istr] = istr("Destination")
|
| 63 |
+
DIGEST: Final[istr] = istr("Digest")
|
| 64 |
+
ETAG: Final[istr] = istr("Etag")
|
| 65 |
+
EXPECT: Final[istr] = istr("Expect")
|
| 66 |
+
EXPIRES: Final[istr] = istr("Expires")
|
| 67 |
+
FORWARDED: Final[istr] = istr("Forwarded")
|
| 68 |
+
FROM: Final[istr] = istr("From")
|
| 69 |
+
HOST: Final[istr] = istr("Host")
|
| 70 |
+
IF_MATCH: Final[istr] = istr("If-Match")
|
| 71 |
+
IF_MODIFIED_SINCE: Final[istr] = istr("If-Modified-Since")
|
| 72 |
+
IF_NONE_MATCH: Final[istr] = istr("If-None-Match")
|
| 73 |
+
IF_RANGE: Final[istr] = istr("If-Range")
|
| 74 |
+
IF_UNMODIFIED_SINCE: Final[istr] = istr("If-Unmodified-Since")
|
| 75 |
+
KEEP_ALIVE: Final[istr] = istr("Keep-Alive")
|
| 76 |
+
LAST_EVENT_ID: Final[istr] = istr("Last-Event-ID")
|
| 77 |
+
LAST_MODIFIED: Final[istr] = istr("Last-Modified")
|
| 78 |
+
LINK: Final[istr] = istr("Link")
|
| 79 |
+
LOCATION: Final[istr] = istr("Location")
|
| 80 |
+
MAX_FORWARDS: Final[istr] = istr("Max-Forwards")
|
| 81 |
+
ORIGIN: Final[istr] = istr("Origin")
|
| 82 |
+
PRAGMA: Final[istr] = istr("Pragma")
|
| 83 |
+
PROXY_AUTHENTICATE: Final[istr] = istr("Proxy-Authenticate")
|
| 84 |
+
PROXY_AUTHORIZATION: Final[istr] = istr("Proxy-Authorization")
|
| 85 |
+
RANGE: Final[istr] = istr("Range")
|
| 86 |
+
REFERER: Final[istr] = istr("Referer")
|
| 87 |
+
RETRY_AFTER: Final[istr] = istr("Retry-After")
|
| 88 |
+
SEC_WEBSOCKET_ACCEPT: Final[istr] = istr("Sec-WebSocket-Accept")
|
| 89 |
+
SEC_WEBSOCKET_VERSION: Final[istr] = istr("Sec-WebSocket-Version")
|
| 90 |
+
SEC_WEBSOCKET_PROTOCOL: Final[istr] = istr("Sec-WebSocket-Protocol")
|
| 91 |
+
SEC_WEBSOCKET_EXTENSIONS: Final[istr] = istr("Sec-WebSocket-Extensions")
|
| 92 |
+
SEC_WEBSOCKET_KEY: Final[istr] = istr("Sec-WebSocket-Key")
|
| 93 |
+
SEC_WEBSOCKET_KEY1: Final[istr] = istr("Sec-WebSocket-Key1")
|
| 94 |
+
SERVER: Final[istr] = istr("Server")
|
| 95 |
+
SET_COOKIE: Final[istr] = istr("Set-Cookie")
|
| 96 |
+
TE: Final[istr] = istr("TE")
|
| 97 |
+
TRAILER: Final[istr] = istr("Trailer")
|
| 98 |
+
TRANSFER_ENCODING: Final[istr] = istr("Transfer-Encoding")
|
| 99 |
+
UPGRADE: Final[istr] = istr("Upgrade")
|
| 100 |
+
URI: Final[istr] = istr("URI")
|
| 101 |
+
USER_AGENT: Final[istr] = istr("User-Agent")
|
| 102 |
+
VARY: Final[istr] = istr("Vary")
|
| 103 |
+
VIA: Final[istr] = istr("Via")
|
| 104 |
+
WANT_DIGEST: Final[istr] = istr("Want-Digest")
|
| 105 |
+
WARNING: Final[istr] = istr("Warning")
|
| 106 |
+
WWW_AUTHENTICATE: Final[istr] = istr("WWW-Authenticate")
|
| 107 |
+
X_FORWARDED_FOR: Final[istr] = istr("X-Forwarded-For")
|
| 108 |
+
X_FORWARDED_HOST: Final[istr] = istr("X-Forwarded-Host")
|
| 109 |
+
X_FORWARDED_PROTO: Final[istr] = istr("X-Forwarded-Proto")
|
| 110 |
+
|
| 111 |
+
# These are the upper/lower case variants of the headers/methods
|
| 112 |
+
# Example: {'hOst', 'host', 'HoST', 'HOSt', 'hOsT', 'HosT', 'hoSt', ...}
|
| 113 |
+
METH_HEAD_ALL: Final = frozenset(
|
| 114 |
+
map("".join, itertools.product(*zip(METH_HEAD.upper(), METH_HEAD.lower())))
|
| 115 |
+
)
|
| 116 |
+
METH_CONNECT_ALL: Final = frozenset(
|
| 117 |
+
map("".join, itertools.product(*zip(METH_CONNECT.upper(), METH_CONNECT.lower())))
|
| 118 |
+
)
|
| 119 |
+
HOST_ALL: Final = frozenset(
|
| 120 |
+
map("".join, itertools.product(*zip(HOST.upper(), HOST.lower())))
|
| 121 |
+
)
|
aiohttp/helpers.py
ADDED
|
@@ -0,0 +1,986 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Various helper functions"""
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import base64
|
| 5 |
+
import binascii
|
| 6 |
+
import contextlib
|
| 7 |
+
import datetime
|
| 8 |
+
import enum
|
| 9 |
+
import functools
|
| 10 |
+
import inspect
|
| 11 |
+
import netrc
|
| 12 |
+
import os
|
| 13 |
+
import platform
|
| 14 |
+
import re
|
| 15 |
+
import sys
|
| 16 |
+
import time
|
| 17 |
+
import weakref
|
| 18 |
+
from collections import namedtuple
|
| 19 |
+
from contextlib import suppress
|
| 20 |
+
from email.message import EmailMessage
|
| 21 |
+
from email.parser import HeaderParser
|
| 22 |
+
from email.policy import HTTP
|
| 23 |
+
from email.utils import parsedate
|
| 24 |
+
from math import ceil
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
from types import MappingProxyType, TracebackType
|
| 27 |
+
from typing import (
|
| 28 |
+
Any,
|
| 29 |
+
Callable,
|
| 30 |
+
ContextManager,
|
| 31 |
+
Dict,
|
| 32 |
+
Generator,
|
| 33 |
+
Generic,
|
| 34 |
+
Iterable,
|
| 35 |
+
Iterator,
|
| 36 |
+
List,
|
| 37 |
+
Mapping,
|
| 38 |
+
Optional,
|
| 39 |
+
Protocol,
|
| 40 |
+
Tuple,
|
| 41 |
+
Type,
|
| 42 |
+
TypeVar,
|
| 43 |
+
Union,
|
| 44 |
+
get_args,
|
| 45 |
+
overload,
|
| 46 |
+
)
|
| 47 |
+
from urllib.parse import quote
|
| 48 |
+
from urllib.request import getproxies, proxy_bypass
|
| 49 |
+
|
| 50 |
+
import attr
|
| 51 |
+
from multidict import MultiDict, MultiDictProxy, MultiMapping
|
| 52 |
+
from propcache.api import under_cached_property as reify
|
| 53 |
+
from yarl import URL
|
| 54 |
+
|
| 55 |
+
from . import hdrs
|
| 56 |
+
from .log import client_logger
|
| 57 |
+
|
| 58 |
+
if sys.version_info >= (3, 11):
|
| 59 |
+
import asyncio as async_timeout
|
| 60 |
+
else:
|
| 61 |
+
import async_timeout
|
| 62 |
+
|
| 63 |
+
__all__ = ("BasicAuth", "ChainMapProxy", "ETag", "reify")
|
| 64 |
+
|
| 65 |
+
IS_MACOS = platform.system() == "Darwin"
|
| 66 |
+
IS_WINDOWS = platform.system() == "Windows"
|
| 67 |
+
|
| 68 |
+
PY_310 = sys.version_info >= (3, 10)
|
| 69 |
+
PY_311 = sys.version_info >= (3, 11)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
_T = TypeVar("_T")
|
| 73 |
+
_S = TypeVar("_S")
|
| 74 |
+
|
| 75 |
+
_SENTINEL = enum.Enum("_SENTINEL", "sentinel")
|
| 76 |
+
sentinel = _SENTINEL.sentinel
|
| 77 |
+
|
| 78 |
+
NO_EXTENSIONS = bool(os.environ.get("AIOHTTP_NO_EXTENSIONS"))
|
| 79 |
+
|
| 80 |
+
# https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.1
|
| 81 |
+
EMPTY_BODY_STATUS_CODES = frozenset((204, 304, *range(100, 200)))
|
| 82 |
+
# https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.1
|
| 83 |
+
# https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.2
|
| 84 |
+
EMPTY_BODY_METHODS = hdrs.METH_HEAD_ALL
|
| 85 |
+
|
| 86 |
+
DEBUG = sys.flags.dev_mode or (
|
| 87 |
+
not sys.flags.ignore_environment and bool(os.environ.get("PYTHONASYNCIODEBUG"))
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
CHAR = {chr(i) for i in range(0, 128)}
|
| 92 |
+
CTL = {chr(i) for i in range(0, 32)} | {
|
| 93 |
+
chr(127),
|
| 94 |
+
}
|
| 95 |
+
SEPARATORS = {
|
| 96 |
+
"(",
|
| 97 |
+
")",
|
| 98 |
+
"<",
|
| 99 |
+
">",
|
| 100 |
+
"@",
|
| 101 |
+
",",
|
| 102 |
+
";",
|
| 103 |
+
":",
|
| 104 |
+
"\\",
|
| 105 |
+
'"',
|
| 106 |
+
"/",
|
| 107 |
+
"[",
|
| 108 |
+
"]",
|
| 109 |
+
"?",
|
| 110 |
+
"=",
|
| 111 |
+
"{",
|
| 112 |
+
"}",
|
| 113 |
+
" ",
|
| 114 |
+
chr(9),
|
| 115 |
+
}
|
| 116 |
+
TOKEN = CHAR ^ CTL ^ SEPARATORS
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class noop:
|
| 120 |
+
def __await__(self) -> Generator[None, None, None]:
|
| 121 |
+
yield
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class BasicAuth(namedtuple("BasicAuth", ["login", "password", "encoding"])):
|
| 125 |
+
"""Http basic authentication helper."""
|
| 126 |
+
|
| 127 |
+
def __new__(
|
| 128 |
+
cls, login: str, password: str = "", encoding: str = "latin1"
|
| 129 |
+
) -> "BasicAuth":
|
| 130 |
+
if login is None:
|
| 131 |
+
raise ValueError("None is not allowed as login value")
|
| 132 |
+
|
| 133 |
+
if password is None:
|
| 134 |
+
raise ValueError("None is not allowed as password value")
|
| 135 |
+
|
| 136 |
+
if ":" in login:
|
| 137 |
+
raise ValueError('A ":" is not allowed in login (RFC 1945#section-11.1)')
|
| 138 |
+
|
| 139 |
+
return super().__new__(cls, login, password, encoding)
|
| 140 |
+
|
| 141 |
+
@classmethod
|
| 142 |
+
def decode(cls, auth_header: str, encoding: str = "latin1") -> "BasicAuth":
|
| 143 |
+
"""Create a BasicAuth object from an Authorization HTTP header."""
|
| 144 |
+
try:
|
| 145 |
+
auth_type, encoded_credentials = auth_header.split(" ", 1)
|
| 146 |
+
except ValueError:
|
| 147 |
+
raise ValueError("Could not parse authorization header.")
|
| 148 |
+
|
| 149 |
+
if auth_type.lower() != "basic":
|
| 150 |
+
raise ValueError("Unknown authorization method %s" % auth_type)
|
| 151 |
+
|
| 152 |
+
try:
|
| 153 |
+
decoded = base64.b64decode(
|
| 154 |
+
encoded_credentials.encode("ascii"), validate=True
|
| 155 |
+
).decode(encoding)
|
| 156 |
+
except binascii.Error:
|
| 157 |
+
raise ValueError("Invalid base64 encoding.")
|
| 158 |
+
|
| 159 |
+
try:
|
| 160 |
+
# RFC 2617 HTTP Authentication
|
| 161 |
+
# https://www.ietf.org/rfc/rfc2617.txt
|
| 162 |
+
# the colon must be present, but the username and password may be
|
| 163 |
+
# otherwise blank.
|
| 164 |
+
username, password = decoded.split(":", 1)
|
| 165 |
+
except ValueError:
|
| 166 |
+
raise ValueError("Invalid credentials.")
|
| 167 |
+
|
| 168 |
+
return cls(username, password, encoding=encoding)
|
| 169 |
+
|
| 170 |
+
@classmethod
|
| 171 |
+
def from_url(cls, url: URL, *, encoding: str = "latin1") -> Optional["BasicAuth"]:
|
| 172 |
+
"""Create BasicAuth from url."""
|
| 173 |
+
if not isinstance(url, URL):
|
| 174 |
+
raise TypeError("url should be yarl.URL instance")
|
| 175 |
+
# Check raw_user and raw_password first as yarl is likely
|
| 176 |
+
# to already have these values parsed from the netloc in the cache.
|
| 177 |
+
if url.raw_user is None and url.raw_password is None:
|
| 178 |
+
return None
|
| 179 |
+
return cls(url.user or "", url.password or "", encoding=encoding)
|
| 180 |
+
|
| 181 |
+
def encode(self) -> str:
|
| 182 |
+
"""Encode credentials."""
|
| 183 |
+
creds = (f"{self.login}:{self.password}").encode(self.encoding)
|
| 184 |
+
return "Basic %s" % base64.b64encode(creds).decode(self.encoding)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def strip_auth_from_url(url: URL) -> Tuple[URL, Optional[BasicAuth]]:
|
| 188 |
+
"""Remove user and password from URL if present and return BasicAuth object."""
|
| 189 |
+
# Check raw_user and raw_password first as yarl is likely
|
| 190 |
+
# to already have these values parsed from the netloc in the cache.
|
| 191 |
+
if url.raw_user is None and url.raw_password is None:
|
| 192 |
+
return url, None
|
| 193 |
+
return url.with_user(None), BasicAuth(url.user or "", url.password or "")
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def netrc_from_env() -> Optional[netrc.netrc]:
|
| 197 |
+
"""Load netrc from file.
|
| 198 |
+
|
| 199 |
+
Attempt to load it from the path specified by the env-var
|
| 200 |
+
NETRC or in the default location in the user's home directory.
|
| 201 |
+
|
| 202 |
+
Returns None if it couldn't be found or fails to parse.
|
| 203 |
+
"""
|
| 204 |
+
netrc_env = os.environ.get("NETRC")
|
| 205 |
+
|
| 206 |
+
if netrc_env is not None:
|
| 207 |
+
netrc_path = Path(netrc_env)
|
| 208 |
+
else:
|
| 209 |
+
try:
|
| 210 |
+
home_dir = Path.home()
|
| 211 |
+
except RuntimeError as e: # pragma: no cover
|
| 212 |
+
# if pathlib can't resolve home, it may raise a RuntimeError
|
| 213 |
+
client_logger.debug(
|
| 214 |
+
"Could not resolve home directory when "
|
| 215 |
+
"trying to look for .netrc file: %s",
|
| 216 |
+
e,
|
| 217 |
+
)
|
| 218 |
+
return None
|
| 219 |
+
|
| 220 |
+
netrc_path = home_dir / ("_netrc" if IS_WINDOWS else ".netrc")
|
| 221 |
+
|
| 222 |
+
try:
|
| 223 |
+
return netrc.netrc(str(netrc_path))
|
| 224 |
+
except netrc.NetrcParseError as e:
|
| 225 |
+
client_logger.warning("Could not parse .netrc file: %s", e)
|
| 226 |
+
except OSError as e:
|
| 227 |
+
netrc_exists = False
|
| 228 |
+
with contextlib.suppress(OSError):
|
| 229 |
+
netrc_exists = netrc_path.is_file()
|
| 230 |
+
# we couldn't read the file (doesn't exist, permissions, etc.)
|
| 231 |
+
if netrc_env or netrc_exists:
|
| 232 |
+
# only warn if the environment wanted us to load it,
|
| 233 |
+
# or it appears like the default file does actually exist
|
| 234 |
+
client_logger.warning("Could not read .netrc file: %s", e)
|
| 235 |
+
|
| 236 |
+
return None
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
@attr.s(auto_attribs=True, frozen=True, slots=True)
|
| 240 |
+
class ProxyInfo:
|
| 241 |
+
proxy: URL
|
| 242 |
+
proxy_auth: Optional[BasicAuth]
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def basicauth_from_netrc(netrc_obj: Optional[netrc.netrc], host: str) -> BasicAuth:
|
| 246 |
+
"""
|
| 247 |
+
Return :py:class:`~aiohttp.BasicAuth` credentials for ``host`` from ``netrc_obj``.
|
| 248 |
+
|
| 249 |
+
:raises LookupError: if ``netrc_obj`` is :py:data:`None` or if no
|
| 250 |
+
entry is found for the ``host``.
|
| 251 |
+
"""
|
| 252 |
+
if netrc_obj is None:
|
| 253 |
+
raise LookupError("No .netrc file found")
|
| 254 |
+
auth_from_netrc = netrc_obj.authenticators(host)
|
| 255 |
+
|
| 256 |
+
if auth_from_netrc is None:
|
| 257 |
+
raise LookupError(f"No entry for {host!s} found in the `.netrc` file.")
|
| 258 |
+
login, account, password = auth_from_netrc
|
| 259 |
+
|
| 260 |
+
# TODO(PY311): username = login or account
|
| 261 |
+
# Up to python 3.10, account could be None if not specified,
|
| 262 |
+
# and login will be empty string if not specified. From 3.11,
|
| 263 |
+
# login and account will be empty string if not specified.
|
| 264 |
+
username = login if (login or account is None) else account
|
| 265 |
+
|
| 266 |
+
# TODO(PY311): Remove this, as password will be empty string
|
| 267 |
+
# if not specified
|
| 268 |
+
if password is None:
|
| 269 |
+
password = ""
|
| 270 |
+
|
| 271 |
+
return BasicAuth(username, password)
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def proxies_from_env() -> Dict[str, ProxyInfo]:
|
| 275 |
+
proxy_urls = {
|
| 276 |
+
k: URL(v)
|
| 277 |
+
for k, v in getproxies().items()
|
| 278 |
+
if k in ("http", "https", "ws", "wss")
|
| 279 |
+
}
|
| 280 |
+
netrc_obj = netrc_from_env()
|
| 281 |
+
stripped = {k: strip_auth_from_url(v) for k, v in proxy_urls.items()}
|
| 282 |
+
ret = {}
|
| 283 |
+
for proto, val in stripped.items():
|
| 284 |
+
proxy, auth = val
|
| 285 |
+
if proxy.scheme in ("https", "wss"):
|
| 286 |
+
client_logger.warning(
|
| 287 |
+
"%s proxies %s are not supported, ignoring", proxy.scheme.upper(), proxy
|
| 288 |
+
)
|
| 289 |
+
continue
|
| 290 |
+
if netrc_obj and auth is None:
|
| 291 |
+
if proxy.host is not None:
|
| 292 |
+
try:
|
| 293 |
+
auth = basicauth_from_netrc(netrc_obj, proxy.host)
|
| 294 |
+
except LookupError:
|
| 295 |
+
auth = None
|
| 296 |
+
ret[proto] = ProxyInfo(proxy, auth)
|
| 297 |
+
return ret
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def get_env_proxy_for_url(url: URL) -> Tuple[URL, Optional[BasicAuth]]:
|
| 301 |
+
"""Get a permitted proxy for the given URL from the env."""
|
| 302 |
+
if url.host is not None and proxy_bypass(url.host):
|
| 303 |
+
raise LookupError(f"Proxying is disallowed for `{url.host!r}`")
|
| 304 |
+
|
| 305 |
+
proxies_in_env = proxies_from_env()
|
| 306 |
+
try:
|
| 307 |
+
proxy_info = proxies_in_env[url.scheme]
|
| 308 |
+
except KeyError:
|
| 309 |
+
raise LookupError(f"No proxies found for `{url!s}` in the env")
|
| 310 |
+
else:
|
| 311 |
+
return proxy_info.proxy, proxy_info.proxy_auth
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
@attr.s(auto_attribs=True, frozen=True, slots=True)
|
| 315 |
+
class MimeType:
|
| 316 |
+
type: str
|
| 317 |
+
subtype: str
|
| 318 |
+
suffix: str
|
| 319 |
+
parameters: "MultiDictProxy[str]"
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
@functools.lru_cache(maxsize=56)
|
| 323 |
+
def parse_mimetype(mimetype: str) -> MimeType:
|
| 324 |
+
"""Parses a MIME type into its components.
|
| 325 |
+
|
| 326 |
+
mimetype is a MIME type string.
|
| 327 |
+
|
| 328 |
+
Returns a MimeType object.
|
| 329 |
+
|
| 330 |
+
Example:
|
| 331 |
+
|
| 332 |
+
>>> parse_mimetype('text/html; charset=utf-8')
|
| 333 |
+
MimeType(type='text', subtype='html', suffix='',
|
| 334 |
+
parameters={'charset': 'utf-8'})
|
| 335 |
+
|
| 336 |
+
"""
|
| 337 |
+
if not mimetype:
|
| 338 |
+
return MimeType(
|
| 339 |
+
type="", subtype="", suffix="", parameters=MultiDictProxy(MultiDict())
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
parts = mimetype.split(";")
|
| 343 |
+
params: MultiDict[str] = MultiDict()
|
| 344 |
+
for item in parts[1:]:
|
| 345 |
+
if not item:
|
| 346 |
+
continue
|
| 347 |
+
key, _, value = item.partition("=")
|
| 348 |
+
params.add(key.lower().strip(), value.strip(' "'))
|
| 349 |
+
|
| 350 |
+
fulltype = parts[0].strip().lower()
|
| 351 |
+
if fulltype == "*":
|
| 352 |
+
fulltype = "*/*"
|
| 353 |
+
|
| 354 |
+
mtype, _, stype = fulltype.partition("/")
|
| 355 |
+
stype, _, suffix = stype.partition("+")
|
| 356 |
+
|
| 357 |
+
return MimeType(
|
| 358 |
+
type=mtype, subtype=stype, suffix=suffix, parameters=MultiDictProxy(params)
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
class EnsureOctetStream(EmailMessage):
|
| 363 |
+
def __init__(self) -> None:
|
| 364 |
+
super().__init__()
|
| 365 |
+
# https://www.rfc-editor.org/rfc/rfc9110#section-8.3-5
|
| 366 |
+
self.set_default_type("application/octet-stream")
|
| 367 |
+
|
| 368 |
+
def get_content_type(self) -> str:
|
| 369 |
+
"""Re-implementation from Message
|
| 370 |
+
|
| 371 |
+
Returns application/octet-stream in place of plain/text when
|
| 372 |
+
value is wrong.
|
| 373 |
+
|
| 374 |
+
The way this class is used guarantees that content-type will
|
| 375 |
+
be present so simplify the checks wrt to the base implementation.
|
| 376 |
+
"""
|
| 377 |
+
value = self.get("content-type", "").lower()
|
| 378 |
+
|
| 379 |
+
# Based on the implementation of _splitparam in the standard library
|
| 380 |
+
ctype, _, _ = value.partition(";")
|
| 381 |
+
ctype = ctype.strip()
|
| 382 |
+
if ctype.count("/") != 1:
|
| 383 |
+
return self.get_default_type()
|
| 384 |
+
return ctype
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
@functools.lru_cache(maxsize=56)
|
| 388 |
+
def parse_content_type(raw: str) -> Tuple[str, MappingProxyType[str, str]]:
|
| 389 |
+
"""Parse Content-Type header.
|
| 390 |
+
|
| 391 |
+
Returns a tuple of the parsed content type and a
|
| 392 |
+
MappingProxyType of parameters. The default returned value
|
| 393 |
+
is `application/octet-stream`
|
| 394 |
+
"""
|
| 395 |
+
msg = HeaderParser(EnsureOctetStream, policy=HTTP).parsestr(f"Content-Type: {raw}")
|
| 396 |
+
content_type = msg.get_content_type()
|
| 397 |
+
params = msg.get_params(())
|
| 398 |
+
content_dict = dict(params[1:]) # First element is content type again
|
| 399 |
+
return content_type, MappingProxyType(content_dict)
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def guess_filename(obj: Any, default: Optional[str] = None) -> Optional[str]:
|
| 403 |
+
name = getattr(obj, "name", None)
|
| 404 |
+
if name and isinstance(name, str) and name[0] != "<" and name[-1] != ">":
|
| 405 |
+
return Path(name).name
|
| 406 |
+
return default
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
not_qtext_re = re.compile(r"[^\041\043-\133\135-\176]")
|
| 410 |
+
QCONTENT = {chr(i) for i in range(0x20, 0x7F)} | {"\t"}
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
def quoted_string(content: str) -> str:
|
| 414 |
+
"""Return 7-bit content as quoted-string.
|
| 415 |
+
|
| 416 |
+
Format content into a quoted-string as defined in RFC5322 for
|
| 417 |
+
Internet Message Format. Notice that this is not the 8-bit HTTP
|
| 418 |
+
format, but the 7-bit email format. Content must be in usascii or
|
| 419 |
+
a ValueError is raised.
|
| 420 |
+
"""
|
| 421 |
+
if not (QCONTENT > set(content)):
|
| 422 |
+
raise ValueError(f"bad content for quoted-string {content!r}")
|
| 423 |
+
return not_qtext_re.sub(lambda x: "\\" + x.group(0), content)
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
def content_disposition_header(
|
| 427 |
+
disptype: str, quote_fields: bool = True, _charset: str = "utf-8", **params: str
|
| 428 |
+
) -> str:
|
| 429 |
+
"""Sets ``Content-Disposition`` header for MIME.
|
| 430 |
+
|
| 431 |
+
This is the MIME payload Content-Disposition header from RFC 2183
|
| 432 |
+
and RFC 7579 section 4.2, not the HTTP Content-Disposition from
|
| 433 |
+
RFC 6266.
|
| 434 |
+
|
| 435 |
+
disptype is a disposition type: inline, attachment, form-data.
|
| 436 |
+
Should be valid extension token (see RFC 2183)
|
| 437 |
+
|
| 438 |
+
quote_fields performs value quoting to 7-bit MIME headers
|
| 439 |
+
according to RFC 7578. Set to quote_fields to False if recipient
|
| 440 |
+
can take 8-bit file names and field values.
|
| 441 |
+
|
| 442 |
+
_charset specifies the charset to use when quote_fields is True.
|
| 443 |
+
|
| 444 |
+
params is a dict with disposition params.
|
| 445 |
+
"""
|
| 446 |
+
if not disptype or not (TOKEN > set(disptype)):
|
| 447 |
+
raise ValueError(f"bad content disposition type {disptype!r}")
|
| 448 |
+
|
| 449 |
+
value = disptype
|
| 450 |
+
if params:
|
| 451 |
+
lparams = []
|
| 452 |
+
for key, val in params.items():
|
| 453 |
+
if not key or not (TOKEN > set(key)):
|
| 454 |
+
raise ValueError(f"bad content disposition parameter {key!r}={val!r}")
|
| 455 |
+
if quote_fields:
|
| 456 |
+
if key.lower() == "filename":
|
| 457 |
+
qval = quote(val, "", encoding=_charset)
|
| 458 |
+
lparams.append((key, '"%s"' % qval))
|
| 459 |
+
else:
|
| 460 |
+
try:
|
| 461 |
+
qval = quoted_string(val)
|
| 462 |
+
except ValueError:
|
| 463 |
+
qval = "".join(
|
| 464 |
+
(_charset, "''", quote(val, "", encoding=_charset))
|
| 465 |
+
)
|
| 466 |
+
lparams.append((key + "*", qval))
|
| 467 |
+
else:
|
| 468 |
+
lparams.append((key, '"%s"' % qval))
|
| 469 |
+
else:
|
| 470 |
+
qval = val.replace("\\", "\\\\").replace('"', '\\"')
|
| 471 |
+
lparams.append((key, '"%s"' % qval))
|
| 472 |
+
sparams = "; ".join("=".join(pair) for pair in lparams)
|
| 473 |
+
value = "; ".join((value, sparams))
|
| 474 |
+
return value
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def is_ip_address(host: Optional[str]) -> bool:
|
| 478 |
+
"""Check if host looks like an IP Address.
|
| 479 |
+
|
| 480 |
+
This check is only meant as a heuristic to ensure that
|
| 481 |
+
a host is not a domain name.
|
| 482 |
+
"""
|
| 483 |
+
if not host:
|
| 484 |
+
return False
|
| 485 |
+
# For a host to be an ipv4 address, it must be all numeric.
|
| 486 |
+
# The host must contain a colon to be an IPv6 address.
|
| 487 |
+
return ":" in host or host.replace(".", "").isdigit()
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
_cached_current_datetime: Optional[int] = None
|
| 491 |
+
_cached_formatted_datetime = ""
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
def rfc822_formatted_time() -> str:
|
| 495 |
+
global _cached_current_datetime
|
| 496 |
+
global _cached_formatted_datetime
|
| 497 |
+
|
| 498 |
+
now = int(time.time())
|
| 499 |
+
if now != _cached_current_datetime:
|
| 500 |
+
# Weekday and month names for HTTP date/time formatting;
|
| 501 |
+
# always English!
|
| 502 |
+
# Tuples are constants stored in codeobject!
|
| 503 |
+
_weekdayname = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
|
| 504 |
+
_monthname = (
|
| 505 |
+
"", # Dummy so we can use 1-based month numbers
|
| 506 |
+
"Jan",
|
| 507 |
+
"Feb",
|
| 508 |
+
"Mar",
|
| 509 |
+
"Apr",
|
| 510 |
+
"May",
|
| 511 |
+
"Jun",
|
| 512 |
+
"Jul",
|
| 513 |
+
"Aug",
|
| 514 |
+
"Sep",
|
| 515 |
+
"Oct",
|
| 516 |
+
"Nov",
|
| 517 |
+
"Dec",
|
| 518 |
+
)
|
| 519 |
+
|
| 520 |
+
year, month, day, hh, mm, ss, wd, *tail = time.gmtime(now)
|
| 521 |
+
_cached_formatted_datetime = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
|
| 522 |
+
_weekdayname[wd],
|
| 523 |
+
day,
|
| 524 |
+
_monthname[month],
|
| 525 |
+
year,
|
| 526 |
+
hh,
|
| 527 |
+
mm,
|
| 528 |
+
ss,
|
| 529 |
+
)
|
| 530 |
+
_cached_current_datetime = now
|
| 531 |
+
return _cached_formatted_datetime
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
def _weakref_handle(info: "Tuple[weakref.ref[object], str]") -> None:
|
| 535 |
+
ref, name = info
|
| 536 |
+
ob = ref()
|
| 537 |
+
if ob is not None:
|
| 538 |
+
with suppress(Exception):
|
| 539 |
+
getattr(ob, name)()
|
| 540 |
+
|
| 541 |
+
|
| 542 |
+
def weakref_handle(
|
| 543 |
+
ob: object,
|
| 544 |
+
name: str,
|
| 545 |
+
timeout: float,
|
| 546 |
+
loop: asyncio.AbstractEventLoop,
|
| 547 |
+
timeout_ceil_threshold: float = 5,
|
| 548 |
+
) -> Optional[asyncio.TimerHandle]:
|
| 549 |
+
if timeout is not None and timeout > 0:
|
| 550 |
+
when = loop.time() + timeout
|
| 551 |
+
if timeout >= timeout_ceil_threshold:
|
| 552 |
+
when = ceil(when)
|
| 553 |
+
|
| 554 |
+
return loop.call_at(when, _weakref_handle, (weakref.ref(ob), name))
|
| 555 |
+
return None
|
| 556 |
+
|
| 557 |
+
|
| 558 |
+
def call_later(
|
| 559 |
+
cb: Callable[[], Any],
|
| 560 |
+
timeout: float,
|
| 561 |
+
loop: asyncio.AbstractEventLoop,
|
| 562 |
+
timeout_ceil_threshold: float = 5,
|
| 563 |
+
) -> Optional[asyncio.TimerHandle]:
|
| 564 |
+
if timeout is None or timeout <= 0:
|
| 565 |
+
return None
|
| 566 |
+
now = loop.time()
|
| 567 |
+
when = calculate_timeout_when(now, timeout, timeout_ceil_threshold)
|
| 568 |
+
return loop.call_at(when, cb)
|
| 569 |
+
|
| 570 |
+
|
| 571 |
+
def calculate_timeout_when(
|
| 572 |
+
loop_time: float,
|
| 573 |
+
timeout: float,
|
| 574 |
+
timeout_ceiling_threshold: float,
|
| 575 |
+
) -> float:
|
| 576 |
+
"""Calculate when to execute a timeout."""
|
| 577 |
+
when = loop_time + timeout
|
| 578 |
+
if timeout > timeout_ceiling_threshold:
|
| 579 |
+
return ceil(when)
|
| 580 |
+
return when
|
| 581 |
+
|
| 582 |
+
|
| 583 |
+
class TimeoutHandle:
|
| 584 |
+
"""Timeout handle"""
|
| 585 |
+
|
| 586 |
+
__slots__ = ("_timeout", "_loop", "_ceil_threshold", "_callbacks")
|
| 587 |
+
|
| 588 |
+
def __init__(
|
| 589 |
+
self,
|
| 590 |
+
loop: asyncio.AbstractEventLoop,
|
| 591 |
+
timeout: Optional[float],
|
| 592 |
+
ceil_threshold: float = 5,
|
| 593 |
+
) -> None:
|
| 594 |
+
self._timeout = timeout
|
| 595 |
+
self._loop = loop
|
| 596 |
+
self._ceil_threshold = ceil_threshold
|
| 597 |
+
self._callbacks: List[
|
| 598 |
+
Tuple[Callable[..., None], Tuple[Any, ...], Dict[str, Any]]
|
| 599 |
+
] = []
|
| 600 |
+
|
| 601 |
+
def register(
|
| 602 |
+
self, callback: Callable[..., None], *args: Any, **kwargs: Any
|
| 603 |
+
) -> None:
|
| 604 |
+
self._callbacks.append((callback, args, kwargs))
|
| 605 |
+
|
| 606 |
+
def close(self) -> None:
|
| 607 |
+
self._callbacks.clear()
|
| 608 |
+
|
| 609 |
+
def start(self) -> Optional[asyncio.TimerHandle]:
|
| 610 |
+
timeout = self._timeout
|
| 611 |
+
if timeout is not None and timeout > 0:
|
| 612 |
+
when = self._loop.time() + timeout
|
| 613 |
+
if timeout >= self._ceil_threshold:
|
| 614 |
+
when = ceil(when)
|
| 615 |
+
return self._loop.call_at(when, self.__call__)
|
| 616 |
+
else:
|
| 617 |
+
return None
|
| 618 |
+
|
| 619 |
+
def timer(self) -> "BaseTimerContext":
|
| 620 |
+
if self._timeout is not None and self._timeout > 0:
|
| 621 |
+
timer = TimerContext(self._loop)
|
| 622 |
+
self.register(timer.timeout)
|
| 623 |
+
return timer
|
| 624 |
+
else:
|
| 625 |
+
return TimerNoop()
|
| 626 |
+
|
| 627 |
+
def __call__(self) -> None:
|
| 628 |
+
for cb, args, kwargs in self._callbacks:
|
| 629 |
+
with suppress(Exception):
|
| 630 |
+
cb(*args, **kwargs)
|
| 631 |
+
|
| 632 |
+
self._callbacks.clear()
|
| 633 |
+
|
| 634 |
+
|
| 635 |
+
class BaseTimerContext(ContextManager["BaseTimerContext"]):
|
| 636 |
+
|
| 637 |
+
__slots__ = ()
|
| 638 |
+
|
| 639 |
+
def assert_timeout(self) -> None:
|
| 640 |
+
"""Raise TimeoutError if timeout has been exceeded."""
|
| 641 |
+
|
| 642 |
+
|
| 643 |
+
class TimerNoop(BaseTimerContext):
|
| 644 |
+
|
| 645 |
+
__slots__ = ()
|
| 646 |
+
|
| 647 |
+
def __enter__(self) -> BaseTimerContext:
|
| 648 |
+
return self
|
| 649 |
+
|
| 650 |
+
def __exit__(
|
| 651 |
+
self,
|
| 652 |
+
exc_type: Optional[Type[BaseException]],
|
| 653 |
+
exc_val: Optional[BaseException],
|
| 654 |
+
exc_tb: Optional[TracebackType],
|
| 655 |
+
) -> None:
|
| 656 |
+
return
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
class TimerContext(BaseTimerContext):
|
| 660 |
+
"""Low resolution timeout context manager"""
|
| 661 |
+
|
| 662 |
+
__slots__ = ("_loop", "_tasks", "_cancelled", "_cancelling")
|
| 663 |
+
|
| 664 |
+
def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
|
| 665 |
+
self._loop = loop
|
| 666 |
+
self._tasks: List[asyncio.Task[Any]] = []
|
| 667 |
+
self._cancelled = False
|
| 668 |
+
self._cancelling = 0
|
| 669 |
+
|
| 670 |
+
def assert_timeout(self) -> None:
|
| 671 |
+
"""Raise TimeoutError if timer has already been cancelled."""
|
| 672 |
+
if self._cancelled:
|
| 673 |
+
raise asyncio.TimeoutError from None
|
| 674 |
+
|
| 675 |
+
def __enter__(self) -> BaseTimerContext:
|
| 676 |
+
task = asyncio.current_task(loop=self._loop)
|
| 677 |
+
if task is None:
|
| 678 |
+
raise RuntimeError("Timeout context manager should be used inside a task")
|
| 679 |
+
|
| 680 |
+
if sys.version_info >= (3, 11):
|
| 681 |
+
# Remember if the task was already cancelling
|
| 682 |
+
# so when we __exit__ we can decide if we should
|
| 683 |
+
# raise asyncio.TimeoutError or let the cancellation propagate
|
| 684 |
+
self._cancelling = task.cancelling()
|
| 685 |
+
|
| 686 |
+
if self._cancelled:
|
| 687 |
+
raise asyncio.TimeoutError from None
|
| 688 |
+
|
| 689 |
+
self._tasks.append(task)
|
| 690 |
+
return self
|
| 691 |
+
|
| 692 |
+
def __exit__(
|
| 693 |
+
self,
|
| 694 |
+
exc_type: Optional[Type[BaseException]],
|
| 695 |
+
exc_val: Optional[BaseException],
|
| 696 |
+
exc_tb: Optional[TracebackType],
|
| 697 |
+
) -> Optional[bool]:
|
| 698 |
+
enter_task: Optional[asyncio.Task[Any]] = None
|
| 699 |
+
if self._tasks:
|
| 700 |
+
enter_task = self._tasks.pop()
|
| 701 |
+
|
| 702 |
+
if exc_type is asyncio.CancelledError and self._cancelled:
|
| 703 |
+
assert enter_task is not None
|
| 704 |
+
# The timeout was hit, and the task was cancelled
|
| 705 |
+
# so we need to uncancel the last task that entered the context manager
|
| 706 |
+
# since the cancellation should not leak out of the context manager
|
| 707 |
+
if sys.version_info >= (3, 11):
|
| 708 |
+
# If the task was already cancelling don't raise
|
| 709 |
+
# asyncio.TimeoutError and instead return None
|
| 710 |
+
# to allow the cancellation to propagate
|
| 711 |
+
if enter_task.uncancel() > self._cancelling:
|
| 712 |
+
return None
|
| 713 |
+
raise asyncio.TimeoutError from exc_val
|
| 714 |
+
return None
|
| 715 |
+
|
| 716 |
+
def timeout(self) -> None:
|
| 717 |
+
if not self._cancelled:
|
| 718 |
+
for task in set(self._tasks):
|
| 719 |
+
task.cancel()
|
| 720 |
+
|
| 721 |
+
self._cancelled = True
|
| 722 |
+
|
| 723 |
+
|
| 724 |
+
def ceil_timeout(
|
| 725 |
+
delay: Optional[float], ceil_threshold: float = 5
|
| 726 |
+
) -> async_timeout.Timeout:
|
| 727 |
+
if delay is None or delay <= 0:
|
| 728 |
+
return async_timeout.timeout(None)
|
| 729 |
+
|
| 730 |
+
loop = asyncio.get_running_loop()
|
| 731 |
+
now = loop.time()
|
| 732 |
+
when = now + delay
|
| 733 |
+
if delay > ceil_threshold:
|
| 734 |
+
when = ceil(when)
|
| 735 |
+
return async_timeout.timeout_at(when)
|
| 736 |
+
|
| 737 |
+
|
| 738 |
+
class HeadersMixin:
|
| 739 |
+
"""Mixin for handling headers."""
|
| 740 |
+
|
| 741 |
+
ATTRS = frozenset(["_content_type", "_content_dict", "_stored_content_type"])
|
| 742 |
+
|
| 743 |
+
_headers: MultiMapping[str]
|
| 744 |
+
_content_type: Optional[str] = None
|
| 745 |
+
_content_dict: Optional[Dict[str, str]] = None
|
| 746 |
+
_stored_content_type: Union[str, None, _SENTINEL] = sentinel
|
| 747 |
+
|
| 748 |
+
def _parse_content_type(self, raw: Optional[str]) -> None:
|
| 749 |
+
self._stored_content_type = raw
|
| 750 |
+
if raw is None:
|
| 751 |
+
# default value according to RFC 2616
|
| 752 |
+
self._content_type = "application/octet-stream"
|
| 753 |
+
self._content_dict = {}
|
| 754 |
+
else:
|
| 755 |
+
content_type, content_mapping_proxy = parse_content_type(raw)
|
| 756 |
+
self._content_type = content_type
|
| 757 |
+
# _content_dict needs to be mutable so we can update it
|
| 758 |
+
self._content_dict = content_mapping_proxy.copy()
|
| 759 |
+
|
| 760 |
+
@property
|
| 761 |
+
def content_type(self) -> str:
|
| 762 |
+
"""The value of content part for Content-Type HTTP header."""
|
| 763 |
+
raw = self._headers.get(hdrs.CONTENT_TYPE)
|
| 764 |
+
if self._stored_content_type != raw:
|
| 765 |
+
self._parse_content_type(raw)
|
| 766 |
+
assert self._content_type is not None
|
| 767 |
+
return self._content_type
|
| 768 |
+
|
| 769 |
+
@property
|
| 770 |
+
def charset(self) -> Optional[str]:
|
| 771 |
+
"""The value of charset part for Content-Type HTTP header."""
|
| 772 |
+
raw = self._headers.get(hdrs.CONTENT_TYPE)
|
| 773 |
+
if self._stored_content_type != raw:
|
| 774 |
+
self._parse_content_type(raw)
|
| 775 |
+
assert self._content_dict is not None
|
| 776 |
+
return self._content_dict.get("charset")
|
| 777 |
+
|
| 778 |
+
@property
|
| 779 |
+
def content_length(self) -> Optional[int]:
|
| 780 |
+
"""The value of Content-Length HTTP header."""
|
| 781 |
+
content_length = self._headers.get(hdrs.CONTENT_LENGTH)
|
| 782 |
+
return None if content_length is None else int(content_length)
|
| 783 |
+
|
| 784 |
+
|
| 785 |
+
def set_result(fut: "asyncio.Future[_T]", result: _T) -> None:
|
| 786 |
+
if not fut.done():
|
| 787 |
+
fut.set_result(result)
|
| 788 |
+
|
| 789 |
+
|
| 790 |
+
_EXC_SENTINEL = BaseException()
|
| 791 |
+
|
| 792 |
+
|
| 793 |
+
class ErrorableProtocol(Protocol):
|
| 794 |
+
def set_exception(
|
| 795 |
+
self,
|
| 796 |
+
exc: BaseException,
|
| 797 |
+
exc_cause: BaseException = ...,
|
| 798 |
+
) -> None: ... # pragma: no cover
|
| 799 |
+
|
| 800 |
+
|
| 801 |
+
def set_exception(
|
| 802 |
+
fut: "asyncio.Future[_T] | ErrorableProtocol",
|
| 803 |
+
exc: BaseException,
|
| 804 |
+
exc_cause: BaseException = _EXC_SENTINEL,
|
| 805 |
+
) -> None:
|
| 806 |
+
"""Set future exception.
|
| 807 |
+
|
| 808 |
+
If the future is marked as complete, this function is a no-op.
|
| 809 |
+
|
| 810 |
+
:param exc_cause: An exception that is a direct cause of ``exc``.
|
| 811 |
+
Only set if provided.
|
| 812 |
+
"""
|
| 813 |
+
if asyncio.isfuture(fut) and fut.done():
|
| 814 |
+
return
|
| 815 |
+
|
| 816 |
+
exc_is_sentinel = exc_cause is _EXC_SENTINEL
|
| 817 |
+
exc_causes_itself = exc is exc_cause
|
| 818 |
+
if not exc_is_sentinel and not exc_causes_itself:
|
| 819 |
+
exc.__cause__ = exc_cause
|
| 820 |
+
|
| 821 |
+
fut.set_exception(exc)
|
| 822 |
+
|
| 823 |
+
|
| 824 |
+
@functools.total_ordering
|
| 825 |
+
class AppKey(Generic[_T]):
|
| 826 |
+
"""Keys for static typing support in Application."""
|
| 827 |
+
|
| 828 |
+
__slots__ = ("_name", "_t", "__orig_class__")
|
| 829 |
+
|
| 830 |
+
# This may be set by Python when instantiating with a generic type. We need to
|
| 831 |
+
# support this, in order to support types that are not concrete classes,
|
| 832 |
+
# like Iterable, which can't be passed as the second parameter to __init__.
|
| 833 |
+
__orig_class__: Type[object]
|
| 834 |
+
|
| 835 |
+
def __init__(self, name: str, t: Optional[Type[_T]] = None):
|
| 836 |
+
# Prefix with module name to help deduplicate key names.
|
| 837 |
+
frame = inspect.currentframe()
|
| 838 |
+
while frame:
|
| 839 |
+
if frame.f_code.co_name == "<module>":
|
| 840 |
+
module: str = frame.f_globals["__name__"]
|
| 841 |
+
break
|
| 842 |
+
frame = frame.f_back
|
| 843 |
+
|
| 844 |
+
self._name = module + "." + name
|
| 845 |
+
self._t = t
|
| 846 |
+
|
| 847 |
+
def __lt__(self, other: object) -> bool:
|
| 848 |
+
if isinstance(other, AppKey):
|
| 849 |
+
return self._name < other._name
|
| 850 |
+
return True # Order AppKey above other types.
|
| 851 |
+
|
| 852 |
+
def __repr__(self) -> str:
|
| 853 |
+
t = self._t
|
| 854 |
+
if t is None:
|
| 855 |
+
with suppress(AttributeError):
|
| 856 |
+
# Set to type arg.
|
| 857 |
+
t = get_args(self.__orig_class__)[0]
|
| 858 |
+
|
| 859 |
+
if t is None:
|
| 860 |
+
t_repr = "<<Unknown>>"
|
| 861 |
+
elif isinstance(t, type):
|
| 862 |
+
if t.__module__ == "builtins":
|
| 863 |
+
t_repr = t.__qualname__
|
| 864 |
+
else:
|
| 865 |
+
t_repr = f"{t.__module__}.{t.__qualname__}"
|
| 866 |
+
else:
|
| 867 |
+
t_repr = repr(t)
|
| 868 |
+
return f"<AppKey({self._name}, type={t_repr})>"
|
| 869 |
+
|
| 870 |
+
|
| 871 |
+
class ChainMapProxy(Mapping[Union[str, AppKey[Any]], Any]):
|
| 872 |
+
__slots__ = ("_maps",)
|
| 873 |
+
|
| 874 |
+
def __init__(self, maps: Iterable[Mapping[Union[str, AppKey[Any]], Any]]) -> None:
|
| 875 |
+
self._maps = tuple(maps)
|
| 876 |
+
|
| 877 |
+
def __init_subclass__(cls) -> None:
|
| 878 |
+
raise TypeError(
|
| 879 |
+
"Inheritance class {} from ChainMapProxy "
|
| 880 |
+
"is forbidden".format(cls.__name__)
|
| 881 |
+
)
|
| 882 |
+
|
| 883 |
+
@overload # type: ignore[override]
|
| 884 |
+
def __getitem__(self, key: AppKey[_T]) -> _T: ...
|
| 885 |
+
|
| 886 |
+
@overload
|
| 887 |
+
def __getitem__(self, key: str) -> Any: ...
|
| 888 |
+
|
| 889 |
+
def __getitem__(self, key: Union[str, AppKey[_T]]) -> Any:
|
| 890 |
+
for mapping in self._maps:
|
| 891 |
+
try:
|
| 892 |
+
return mapping[key]
|
| 893 |
+
except KeyError:
|
| 894 |
+
pass
|
| 895 |
+
raise KeyError(key)
|
| 896 |
+
|
| 897 |
+
@overload # type: ignore[override]
|
| 898 |
+
def get(self, key: AppKey[_T], default: _S) -> Union[_T, _S]: ...
|
| 899 |
+
|
| 900 |
+
@overload
|
| 901 |
+
def get(self, key: AppKey[_T], default: None = ...) -> Optional[_T]: ...
|
| 902 |
+
|
| 903 |
+
@overload
|
| 904 |
+
def get(self, key: str, default: Any = ...) -> Any: ...
|
| 905 |
+
|
| 906 |
+
def get(self, key: Union[str, AppKey[_T]], default: Any = None) -> Any:
|
| 907 |
+
try:
|
| 908 |
+
return self[key]
|
| 909 |
+
except KeyError:
|
| 910 |
+
return default
|
| 911 |
+
|
| 912 |
+
def __len__(self) -> int:
|
| 913 |
+
# reuses stored hash values if possible
|
| 914 |
+
return len(set().union(*self._maps))
|
| 915 |
+
|
| 916 |
+
def __iter__(self) -> Iterator[Union[str, AppKey[Any]]]:
|
| 917 |
+
d: Dict[Union[str, AppKey[Any]], Any] = {}
|
| 918 |
+
for mapping in reversed(self._maps):
|
| 919 |
+
# reuses stored hash values if possible
|
| 920 |
+
d.update(mapping)
|
| 921 |
+
return iter(d)
|
| 922 |
+
|
| 923 |
+
def __contains__(self, key: object) -> bool:
|
| 924 |
+
return any(key in m for m in self._maps)
|
| 925 |
+
|
| 926 |
+
def __bool__(self) -> bool:
|
| 927 |
+
return any(self._maps)
|
| 928 |
+
|
| 929 |
+
def __repr__(self) -> str:
|
| 930 |
+
content = ", ".join(map(repr, self._maps))
|
| 931 |
+
return f"ChainMapProxy({content})"
|
| 932 |
+
|
| 933 |
+
|
| 934 |
+
# https://tools.ietf.org/html/rfc7232#section-2.3
|
| 935 |
+
_ETAGC = r"[!\x23-\x7E\x80-\xff]+"
|
| 936 |
+
_ETAGC_RE = re.compile(_ETAGC)
|
| 937 |
+
_QUOTED_ETAG = rf'(W/)?"({_ETAGC})"'
|
| 938 |
+
QUOTED_ETAG_RE = re.compile(_QUOTED_ETAG)
|
| 939 |
+
LIST_QUOTED_ETAG_RE = re.compile(rf"({_QUOTED_ETAG})(?:\s*,\s*|$)|(.)")
|
| 940 |
+
|
| 941 |
+
ETAG_ANY = "*"
|
| 942 |
+
|
| 943 |
+
|
| 944 |
+
@attr.s(auto_attribs=True, frozen=True, slots=True)
|
| 945 |
+
class ETag:
|
| 946 |
+
value: str
|
| 947 |
+
is_weak: bool = False
|
| 948 |
+
|
| 949 |
+
|
| 950 |
+
def validate_etag_value(value: str) -> None:
|
| 951 |
+
if value != ETAG_ANY and not _ETAGC_RE.fullmatch(value):
|
| 952 |
+
raise ValueError(
|
| 953 |
+
f"Value {value!r} is not a valid etag. Maybe it contains '\"'?"
|
| 954 |
+
)
|
| 955 |
+
|
| 956 |
+
|
| 957 |
+
def parse_http_date(date_str: Optional[str]) -> Optional[datetime.datetime]:
|
| 958 |
+
"""Process a date string, return a datetime object"""
|
| 959 |
+
if date_str is not None:
|
| 960 |
+
timetuple = parsedate(date_str)
|
| 961 |
+
if timetuple is not None:
|
| 962 |
+
with suppress(ValueError):
|
| 963 |
+
return datetime.datetime(*timetuple[:6], tzinfo=datetime.timezone.utc)
|
| 964 |
+
return None
|
| 965 |
+
|
| 966 |
+
|
| 967 |
+
@functools.lru_cache
|
| 968 |
+
def must_be_empty_body(method: str, code: int) -> bool:
|
| 969 |
+
"""Check if a request must return an empty body."""
|
| 970 |
+
return (
|
| 971 |
+
code in EMPTY_BODY_STATUS_CODES
|
| 972 |
+
or method in EMPTY_BODY_METHODS
|
| 973 |
+
or (200 <= code < 300 and method in hdrs.METH_CONNECT_ALL)
|
| 974 |
+
)
|
| 975 |
+
|
| 976 |
+
|
| 977 |
+
def should_remove_content_length(method: str, code: int) -> bool:
|
| 978 |
+
"""Check if a Content-Length header should be removed.
|
| 979 |
+
|
| 980 |
+
This should always be a subset of must_be_empty_body
|
| 981 |
+
"""
|
| 982 |
+
# https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6-8
|
| 983 |
+
# https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.5-4
|
| 984 |
+
return code in EMPTY_BODY_STATUS_CODES or (
|
| 985 |
+
200 <= code < 300 and method in hdrs.METH_CONNECT_ALL
|
| 986 |
+
)
|
aiohttp/http_parser.py
ADDED
|
@@ -0,0 +1,1086 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import abc
|
| 2 |
+
import asyncio
|
| 3 |
+
import re
|
| 4 |
+
import string
|
| 5 |
+
from contextlib import suppress
|
| 6 |
+
from enum import IntEnum
|
| 7 |
+
from typing import (
|
| 8 |
+
Any,
|
| 9 |
+
ClassVar,
|
| 10 |
+
Final,
|
| 11 |
+
Generic,
|
| 12 |
+
List,
|
| 13 |
+
Literal,
|
| 14 |
+
NamedTuple,
|
| 15 |
+
Optional,
|
| 16 |
+
Pattern,
|
| 17 |
+
Set,
|
| 18 |
+
Tuple,
|
| 19 |
+
Type,
|
| 20 |
+
TypeVar,
|
| 21 |
+
Union,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
from multidict import CIMultiDict, CIMultiDictProxy, istr
|
| 25 |
+
from yarl import URL
|
| 26 |
+
|
| 27 |
+
from . import hdrs
|
| 28 |
+
from .base_protocol import BaseProtocol
|
| 29 |
+
from .compression_utils import (
|
| 30 |
+
DEFAULT_MAX_DECOMPRESS_SIZE,
|
| 31 |
+
HAS_BROTLI,
|
| 32 |
+
HAS_ZSTD,
|
| 33 |
+
BrotliDecompressor,
|
| 34 |
+
ZLibDecompressor,
|
| 35 |
+
ZSTDDecompressor,
|
| 36 |
+
)
|
| 37 |
+
from .helpers import (
|
| 38 |
+
_EXC_SENTINEL,
|
| 39 |
+
DEBUG,
|
| 40 |
+
EMPTY_BODY_METHODS,
|
| 41 |
+
EMPTY_BODY_STATUS_CODES,
|
| 42 |
+
NO_EXTENSIONS,
|
| 43 |
+
BaseTimerContext,
|
| 44 |
+
set_exception,
|
| 45 |
+
)
|
| 46 |
+
from .http_exceptions import (
|
| 47 |
+
BadHttpMessage,
|
| 48 |
+
BadHttpMethod,
|
| 49 |
+
BadStatusLine,
|
| 50 |
+
ContentEncodingError,
|
| 51 |
+
ContentLengthError,
|
| 52 |
+
DecompressSizeError,
|
| 53 |
+
InvalidHeader,
|
| 54 |
+
InvalidURLError,
|
| 55 |
+
LineTooLong,
|
| 56 |
+
TransferEncodingError,
|
| 57 |
+
)
|
| 58 |
+
from .http_writer import HttpVersion, HttpVersion10
|
| 59 |
+
from .streams import EMPTY_PAYLOAD, StreamReader
|
| 60 |
+
from .typedefs import RawHeaders
|
| 61 |
+
|
| 62 |
+
__all__ = (
|
| 63 |
+
"HeadersParser",
|
| 64 |
+
"HttpParser",
|
| 65 |
+
"HttpRequestParser",
|
| 66 |
+
"HttpResponseParser",
|
| 67 |
+
"RawRequestMessage",
|
| 68 |
+
"RawResponseMessage",
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
_SEP = Literal[b"\r\n", b"\n"]
|
| 72 |
+
|
| 73 |
+
ASCIISET: Final[Set[str]] = set(string.printable)
|
| 74 |
+
|
| 75 |
+
# See https://www.rfc-editor.org/rfc/rfc9110.html#name-overview
|
| 76 |
+
# and https://www.rfc-editor.org/rfc/rfc9110.html#name-tokens
|
| 77 |
+
#
|
| 78 |
+
# method = token
|
| 79 |
+
# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
|
| 80 |
+
# "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
|
| 81 |
+
# token = 1*tchar
|
| 82 |
+
_TCHAR_SPECIALS: Final[str] = re.escape("!#$%&'*+-.^_`|~")
|
| 83 |
+
TOKENRE: Final[Pattern[str]] = re.compile(f"[0-9A-Za-z{_TCHAR_SPECIALS}]+")
|
| 84 |
+
VERSRE: Final[Pattern[str]] = re.compile(r"HTTP/(\d)\.(\d)", re.ASCII)
|
| 85 |
+
DIGITS: Final[Pattern[str]] = re.compile(r"\d+", re.ASCII)
|
| 86 |
+
HEXDIGITS: Final[Pattern[bytes]] = re.compile(rb"[0-9a-fA-F]+")
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class RawRequestMessage(NamedTuple):
|
| 90 |
+
method: str
|
| 91 |
+
path: str
|
| 92 |
+
version: HttpVersion
|
| 93 |
+
headers: "CIMultiDictProxy[str]"
|
| 94 |
+
raw_headers: RawHeaders
|
| 95 |
+
should_close: bool
|
| 96 |
+
compression: Optional[str]
|
| 97 |
+
upgrade: bool
|
| 98 |
+
chunked: bool
|
| 99 |
+
url: URL
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class RawResponseMessage(NamedTuple):
|
| 103 |
+
version: HttpVersion
|
| 104 |
+
code: int
|
| 105 |
+
reason: str
|
| 106 |
+
headers: CIMultiDictProxy[str]
|
| 107 |
+
raw_headers: RawHeaders
|
| 108 |
+
should_close: bool
|
| 109 |
+
compression: Optional[str]
|
| 110 |
+
upgrade: bool
|
| 111 |
+
chunked: bool
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
_MsgT = TypeVar("_MsgT", RawRequestMessage, RawResponseMessage)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
class ParseState(IntEnum):
|
| 118 |
+
|
| 119 |
+
PARSE_NONE = 0
|
| 120 |
+
PARSE_LENGTH = 1
|
| 121 |
+
PARSE_CHUNKED = 2
|
| 122 |
+
PARSE_UNTIL_EOF = 3
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
class ChunkState(IntEnum):
|
| 126 |
+
PARSE_CHUNKED_SIZE = 0
|
| 127 |
+
PARSE_CHUNKED_CHUNK = 1
|
| 128 |
+
PARSE_CHUNKED_CHUNK_EOF = 2
|
| 129 |
+
PARSE_MAYBE_TRAILERS = 3
|
| 130 |
+
PARSE_TRAILERS = 4
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
class HeadersParser:
|
| 134 |
+
def __init__(
|
| 135 |
+
self,
|
| 136 |
+
max_line_size: int = 8190,
|
| 137 |
+
max_headers: int = 32768,
|
| 138 |
+
max_field_size: int = 8190,
|
| 139 |
+
lax: bool = False,
|
| 140 |
+
) -> None:
|
| 141 |
+
self.max_line_size = max_line_size
|
| 142 |
+
self.max_headers = max_headers
|
| 143 |
+
self.max_field_size = max_field_size
|
| 144 |
+
self._lax = lax
|
| 145 |
+
|
| 146 |
+
def parse_headers(
|
| 147 |
+
self, lines: List[bytes]
|
| 148 |
+
) -> Tuple["CIMultiDictProxy[str]", RawHeaders]:
|
| 149 |
+
headers: CIMultiDict[str] = CIMultiDict()
|
| 150 |
+
# note: "raw" does not mean inclusion of OWS before/after the field value
|
| 151 |
+
raw_headers = []
|
| 152 |
+
|
| 153 |
+
lines_idx = 0
|
| 154 |
+
line = lines[lines_idx]
|
| 155 |
+
line_count = len(lines)
|
| 156 |
+
|
| 157 |
+
while line:
|
| 158 |
+
# Parse initial header name : value pair.
|
| 159 |
+
try:
|
| 160 |
+
bname, bvalue = line.split(b":", 1)
|
| 161 |
+
except ValueError:
|
| 162 |
+
raise InvalidHeader(line) from None
|
| 163 |
+
|
| 164 |
+
if len(bname) == 0:
|
| 165 |
+
raise InvalidHeader(bname)
|
| 166 |
+
|
| 167 |
+
# https://www.rfc-editor.org/rfc/rfc9112.html#section-5.1-2
|
| 168 |
+
if {bname[0], bname[-1]} & {32, 9}: # {" ", "\t"}
|
| 169 |
+
raise InvalidHeader(line)
|
| 170 |
+
|
| 171 |
+
bvalue = bvalue.lstrip(b" \t")
|
| 172 |
+
if len(bname) > self.max_field_size:
|
| 173 |
+
raise LineTooLong(
|
| 174 |
+
"request header name {}".format(
|
| 175 |
+
bname.decode("utf8", "backslashreplace")
|
| 176 |
+
),
|
| 177 |
+
str(self.max_field_size),
|
| 178 |
+
str(len(bname)),
|
| 179 |
+
)
|
| 180 |
+
name = bname.decode("utf-8", "surrogateescape")
|
| 181 |
+
if not TOKENRE.fullmatch(name):
|
| 182 |
+
raise InvalidHeader(bname)
|
| 183 |
+
|
| 184 |
+
header_length = len(bvalue)
|
| 185 |
+
|
| 186 |
+
# next line
|
| 187 |
+
lines_idx += 1
|
| 188 |
+
line = lines[lines_idx]
|
| 189 |
+
|
| 190 |
+
# consume continuation lines
|
| 191 |
+
continuation = self._lax and line and line[0] in (32, 9) # (' ', '\t')
|
| 192 |
+
|
| 193 |
+
# Deprecated: https://www.rfc-editor.org/rfc/rfc9112.html#name-obsolete-line-folding
|
| 194 |
+
if continuation:
|
| 195 |
+
bvalue_lst = [bvalue]
|
| 196 |
+
while continuation:
|
| 197 |
+
header_length += len(line)
|
| 198 |
+
if header_length > self.max_field_size:
|
| 199 |
+
raise LineTooLong(
|
| 200 |
+
"request header field {}".format(
|
| 201 |
+
bname.decode("utf8", "backslashreplace")
|
| 202 |
+
),
|
| 203 |
+
str(self.max_field_size),
|
| 204 |
+
str(header_length),
|
| 205 |
+
)
|
| 206 |
+
bvalue_lst.append(line)
|
| 207 |
+
|
| 208 |
+
# next line
|
| 209 |
+
lines_idx += 1
|
| 210 |
+
if lines_idx < line_count:
|
| 211 |
+
line = lines[lines_idx]
|
| 212 |
+
if line:
|
| 213 |
+
continuation = line[0] in (32, 9) # (' ', '\t')
|
| 214 |
+
else:
|
| 215 |
+
line = b""
|
| 216 |
+
break
|
| 217 |
+
bvalue = b"".join(bvalue_lst)
|
| 218 |
+
else:
|
| 219 |
+
if header_length > self.max_field_size:
|
| 220 |
+
raise LineTooLong(
|
| 221 |
+
"request header field {}".format(
|
| 222 |
+
bname.decode("utf8", "backslashreplace")
|
| 223 |
+
),
|
| 224 |
+
str(self.max_field_size),
|
| 225 |
+
str(header_length),
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
bvalue = bvalue.strip(b" \t")
|
| 229 |
+
value = bvalue.decode("utf-8", "surrogateescape")
|
| 230 |
+
|
| 231 |
+
# https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-5
|
| 232 |
+
if "\n" in value or "\r" in value or "\x00" in value:
|
| 233 |
+
raise InvalidHeader(bvalue)
|
| 234 |
+
|
| 235 |
+
headers.add(name, value)
|
| 236 |
+
raw_headers.append((bname, bvalue))
|
| 237 |
+
|
| 238 |
+
return (CIMultiDictProxy(headers), tuple(raw_headers))
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def _is_supported_upgrade(headers: CIMultiDictProxy[str]) -> bool:
|
| 242 |
+
"""Check if the upgrade header is supported."""
|
| 243 |
+
u = headers.get(hdrs.UPGRADE, "")
|
| 244 |
+
# .lower() can transform non-ascii characters.
|
| 245 |
+
return u.isascii() and u.lower() in {"tcp", "websocket"}
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
class HttpParser(abc.ABC, Generic[_MsgT]):
|
| 249 |
+
lax: ClassVar[bool] = False
|
| 250 |
+
|
| 251 |
+
def __init__(
|
| 252 |
+
self,
|
| 253 |
+
protocol: Optional[BaseProtocol] = None,
|
| 254 |
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
| 255 |
+
limit: int = 2**16,
|
| 256 |
+
max_line_size: int = 8190,
|
| 257 |
+
max_headers: int = 32768,
|
| 258 |
+
max_field_size: int = 8190,
|
| 259 |
+
timer: Optional[BaseTimerContext] = None,
|
| 260 |
+
code: Optional[int] = None,
|
| 261 |
+
method: Optional[str] = None,
|
| 262 |
+
payload_exception: Optional[Type[BaseException]] = None,
|
| 263 |
+
response_with_body: bool = True,
|
| 264 |
+
read_until_eof: bool = False,
|
| 265 |
+
auto_decompress: bool = True,
|
| 266 |
+
) -> None:
|
| 267 |
+
self.protocol = protocol
|
| 268 |
+
self.loop = loop
|
| 269 |
+
self.max_line_size = max_line_size
|
| 270 |
+
self.max_headers = max_headers
|
| 271 |
+
self.max_field_size = max_field_size
|
| 272 |
+
self.timer = timer
|
| 273 |
+
self.code = code
|
| 274 |
+
self.method = method
|
| 275 |
+
self.payload_exception = payload_exception
|
| 276 |
+
self.response_with_body = response_with_body
|
| 277 |
+
self.read_until_eof = read_until_eof
|
| 278 |
+
|
| 279 |
+
self._lines: List[bytes] = []
|
| 280 |
+
self._tail = b""
|
| 281 |
+
self._upgraded = False
|
| 282 |
+
self._payload = None
|
| 283 |
+
self._payload_parser: Optional[HttpPayloadParser] = None
|
| 284 |
+
self._auto_decompress = auto_decompress
|
| 285 |
+
self._limit = limit
|
| 286 |
+
self._headers_parser = HeadersParser(
|
| 287 |
+
max_line_size, max_headers, max_field_size, self.lax
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
@abc.abstractmethod
|
| 291 |
+
def parse_message(self, lines: List[bytes]) -> _MsgT: ...
|
| 292 |
+
|
| 293 |
+
@abc.abstractmethod
|
| 294 |
+
def _is_chunked_te(self, te: str) -> bool: ...
|
| 295 |
+
|
| 296 |
+
def feed_eof(self) -> Optional[_MsgT]:
|
| 297 |
+
if self._payload_parser is not None:
|
| 298 |
+
self._payload_parser.feed_eof()
|
| 299 |
+
self._payload_parser = None
|
| 300 |
+
else:
|
| 301 |
+
# try to extract partial message
|
| 302 |
+
if self._tail:
|
| 303 |
+
self._lines.append(self._tail)
|
| 304 |
+
|
| 305 |
+
if self._lines:
|
| 306 |
+
if self._lines[-1] != "\r\n":
|
| 307 |
+
self._lines.append(b"")
|
| 308 |
+
with suppress(Exception):
|
| 309 |
+
return self.parse_message(self._lines)
|
| 310 |
+
return None
|
| 311 |
+
|
| 312 |
+
def feed_data(
|
| 313 |
+
self,
|
| 314 |
+
data: bytes,
|
| 315 |
+
SEP: _SEP = b"\r\n",
|
| 316 |
+
EMPTY: bytes = b"",
|
| 317 |
+
CONTENT_LENGTH: istr = hdrs.CONTENT_LENGTH,
|
| 318 |
+
METH_CONNECT: str = hdrs.METH_CONNECT,
|
| 319 |
+
SEC_WEBSOCKET_KEY1: istr = hdrs.SEC_WEBSOCKET_KEY1,
|
| 320 |
+
) -> Tuple[List[Tuple[_MsgT, StreamReader]], bool, bytes]:
|
| 321 |
+
|
| 322 |
+
messages = []
|
| 323 |
+
|
| 324 |
+
if self._tail:
|
| 325 |
+
data, self._tail = self._tail + data, b""
|
| 326 |
+
|
| 327 |
+
data_len = len(data)
|
| 328 |
+
start_pos = 0
|
| 329 |
+
loop = self.loop
|
| 330 |
+
|
| 331 |
+
should_close = False
|
| 332 |
+
while start_pos < data_len:
|
| 333 |
+
|
| 334 |
+
# read HTTP message (request/response line + headers), \r\n\r\n
|
| 335 |
+
# and split by lines
|
| 336 |
+
if self._payload_parser is None and not self._upgraded:
|
| 337 |
+
pos = data.find(SEP, start_pos)
|
| 338 |
+
# consume \r\n
|
| 339 |
+
if pos == start_pos and not self._lines:
|
| 340 |
+
start_pos = pos + len(SEP)
|
| 341 |
+
continue
|
| 342 |
+
|
| 343 |
+
if pos >= start_pos:
|
| 344 |
+
if should_close:
|
| 345 |
+
raise BadHttpMessage("Data after `Connection: close`")
|
| 346 |
+
|
| 347 |
+
# line found
|
| 348 |
+
line = data[start_pos:pos]
|
| 349 |
+
if SEP == b"\n": # For lax response parsing
|
| 350 |
+
line = line.rstrip(b"\r")
|
| 351 |
+
self._lines.append(line)
|
| 352 |
+
start_pos = pos + len(SEP)
|
| 353 |
+
|
| 354 |
+
# \r\n\r\n found
|
| 355 |
+
if self._lines[-1] == EMPTY:
|
| 356 |
+
try:
|
| 357 |
+
msg: _MsgT = self.parse_message(self._lines)
|
| 358 |
+
finally:
|
| 359 |
+
self._lines.clear()
|
| 360 |
+
|
| 361 |
+
def get_content_length() -> Optional[int]:
|
| 362 |
+
# payload length
|
| 363 |
+
length_hdr = msg.headers.get(CONTENT_LENGTH)
|
| 364 |
+
if length_hdr is None:
|
| 365 |
+
return None
|
| 366 |
+
|
| 367 |
+
# Shouldn't allow +/- or other number formats.
|
| 368 |
+
# https://www.rfc-editor.org/rfc/rfc9110#section-8.6-2
|
| 369 |
+
# msg.headers is already stripped of leading/trailing wsp
|
| 370 |
+
if not DIGITS.fullmatch(length_hdr):
|
| 371 |
+
raise InvalidHeader(CONTENT_LENGTH)
|
| 372 |
+
|
| 373 |
+
return int(length_hdr)
|
| 374 |
+
|
| 375 |
+
length = get_content_length()
|
| 376 |
+
# do not support old websocket spec
|
| 377 |
+
if SEC_WEBSOCKET_KEY1 in msg.headers:
|
| 378 |
+
raise InvalidHeader(SEC_WEBSOCKET_KEY1)
|
| 379 |
+
|
| 380 |
+
self._upgraded = msg.upgrade and _is_supported_upgrade(
|
| 381 |
+
msg.headers
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
method = getattr(msg, "method", self.method)
|
| 385 |
+
# code is only present on responses
|
| 386 |
+
code = getattr(msg, "code", 0)
|
| 387 |
+
|
| 388 |
+
assert self.protocol is not None
|
| 389 |
+
# calculate payload
|
| 390 |
+
empty_body = code in EMPTY_BODY_STATUS_CODES or bool(
|
| 391 |
+
method and method in EMPTY_BODY_METHODS
|
| 392 |
+
)
|
| 393 |
+
if not empty_body and (
|
| 394 |
+
((length is not None and length > 0) or msg.chunked)
|
| 395 |
+
and not self._upgraded
|
| 396 |
+
):
|
| 397 |
+
payload = StreamReader(
|
| 398 |
+
self.protocol,
|
| 399 |
+
timer=self.timer,
|
| 400 |
+
loop=loop,
|
| 401 |
+
limit=self._limit,
|
| 402 |
+
)
|
| 403 |
+
payload_parser = HttpPayloadParser(
|
| 404 |
+
payload,
|
| 405 |
+
length=length,
|
| 406 |
+
chunked=msg.chunked,
|
| 407 |
+
method=method,
|
| 408 |
+
compression=msg.compression,
|
| 409 |
+
code=self.code,
|
| 410 |
+
response_with_body=self.response_with_body,
|
| 411 |
+
auto_decompress=self._auto_decompress,
|
| 412 |
+
lax=self.lax,
|
| 413 |
+
headers_parser=self._headers_parser,
|
| 414 |
+
)
|
| 415 |
+
if not payload_parser.done:
|
| 416 |
+
self._payload_parser = payload_parser
|
| 417 |
+
elif method == METH_CONNECT:
|
| 418 |
+
assert isinstance(msg, RawRequestMessage)
|
| 419 |
+
payload = StreamReader(
|
| 420 |
+
self.protocol,
|
| 421 |
+
timer=self.timer,
|
| 422 |
+
loop=loop,
|
| 423 |
+
limit=self._limit,
|
| 424 |
+
)
|
| 425 |
+
self._upgraded = True
|
| 426 |
+
self._payload_parser = HttpPayloadParser(
|
| 427 |
+
payload,
|
| 428 |
+
method=msg.method,
|
| 429 |
+
compression=msg.compression,
|
| 430 |
+
auto_decompress=self._auto_decompress,
|
| 431 |
+
lax=self.lax,
|
| 432 |
+
headers_parser=self._headers_parser,
|
| 433 |
+
)
|
| 434 |
+
elif not empty_body and length is None and self.read_until_eof:
|
| 435 |
+
payload = StreamReader(
|
| 436 |
+
self.protocol,
|
| 437 |
+
timer=self.timer,
|
| 438 |
+
loop=loop,
|
| 439 |
+
limit=self._limit,
|
| 440 |
+
)
|
| 441 |
+
payload_parser = HttpPayloadParser(
|
| 442 |
+
payload,
|
| 443 |
+
length=length,
|
| 444 |
+
chunked=msg.chunked,
|
| 445 |
+
method=method,
|
| 446 |
+
compression=msg.compression,
|
| 447 |
+
code=self.code,
|
| 448 |
+
response_with_body=self.response_with_body,
|
| 449 |
+
auto_decompress=self._auto_decompress,
|
| 450 |
+
lax=self.lax,
|
| 451 |
+
headers_parser=self._headers_parser,
|
| 452 |
+
)
|
| 453 |
+
if not payload_parser.done:
|
| 454 |
+
self._payload_parser = payload_parser
|
| 455 |
+
else:
|
| 456 |
+
payload = EMPTY_PAYLOAD
|
| 457 |
+
|
| 458 |
+
messages.append((msg, payload))
|
| 459 |
+
should_close = msg.should_close
|
| 460 |
+
else:
|
| 461 |
+
self._tail = data[start_pos:]
|
| 462 |
+
data = EMPTY
|
| 463 |
+
break
|
| 464 |
+
|
| 465 |
+
# no parser, just store
|
| 466 |
+
elif self._payload_parser is None and self._upgraded:
|
| 467 |
+
assert not self._lines
|
| 468 |
+
break
|
| 469 |
+
|
| 470 |
+
# feed payload
|
| 471 |
+
elif data and start_pos < data_len:
|
| 472 |
+
assert not self._lines
|
| 473 |
+
assert self._payload_parser is not None
|
| 474 |
+
try:
|
| 475 |
+
eof, data = self._payload_parser.feed_data(data[start_pos:], SEP)
|
| 476 |
+
except BaseException as underlying_exc:
|
| 477 |
+
reraised_exc = underlying_exc
|
| 478 |
+
if self.payload_exception is not None:
|
| 479 |
+
reraised_exc = self.payload_exception(str(underlying_exc))
|
| 480 |
+
|
| 481 |
+
set_exception(
|
| 482 |
+
self._payload_parser.payload,
|
| 483 |
+
reraised_exc,
|
| 484 |
+
underlying_exc,
|
| 485 |
+
)
|
| 486 |
+
|
| 487 |
+
eof = True
|
| 488 |
+
data = b""
|
| 489 |
+
if isinstance(
|
| 490 |
+
underlying_exc, (InvalidHeader, TransferEncodingError)
|
| 491 |
+
):
|
| 492 |
+
raise
|
| 493 |
+
|
| 494 |
+
if eof:
|
| 495 |
+
start_pos = 0
|
| 496 |
+
data_len = len(data)
|
| 497 |
+
self._payload_parser = None
|
| 498 |
+
continue
|
| 499 |
+
else:
|
| 500 |
+
break
|
| 501 |
+
|
| 502 |
+
if data and start_pos < data_len:
|
| 503 |
+
data = data[start_pos:]
|
| 504 |
+
else:
|
| 505 |
+
data = EMPTY
|
| 506 |
+
|
| 507 |
+
return messages, self._upgraded, data
|
| 508 |
+
|
| 509 |
+
def parse_headers(
|
| 510 |
+
self, lines: List[bytes]
|
| 511 |
+
) -> Tuple[
|
| 512 |
+
"CIMultiDictProxy[str]", RawHeaders, Optional[bool], Optional[str], bool, bool
|
| 513 |
+
]:
|
| 514 |
+
"""Parses RFC 5322 headers from a stream.
|
| 515 |
+
|
| 516 |
+
Line continuations are supported. Returns list of header name
|
| 517 |
+
and value pairs. Header name is in upper case.
|
| 518 |
+
"""
|
| 519 |
+
headers, raw_headers = self._headers_parser.parse_headers(lines)
|
| 520 |
+
close_conn = None
|
| 521 |
+
encoding = None
|
| 522 |
+
upgrade = False
|
| 523 |
+
chunked = False
|
| 524 |
+
|
| 525 |
+
# https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-6
|
| 526 |
+
# https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf
|
| 527 |
+
singletons = (
|
| 528 |
+
hdrs.CONTENT_LENGTH,
|
| 529 |
+
hdrs.CONTENT_LOCATION,
|
| 530 |
+
hdrs.CONTENT_RANGE,
|
| 531 |
+
hdrs.CONTENT_TYPE,
|
| 532 |
+
hdrs.ETAG,
|
| 533 |
+
hdrs.HOST,
|
| 534 |
+
hdrs.MAX_FORWARDS,
|
| 535 |
+
hdrs.SERVER,
|
| 536 |
+
hdrs.TRANSFER_ENCODING,
|
| 537 |
+
hdrs.USER_AGENT,
|
| 538 |
+
)
|
| 539 |
+
bad_hdr = next((h for h in singletons if len(headers.getall(h, ())) > 1), None)
|
| 540 |
+
if bad_hdr is not None:
|
| 541 |
+
raise BadHttpMessage(f"Duplicate '{bad_hdr}' header found.")
|
| 542 |
+
|
| 543 |
+
# keep-alive
|
| 544 |
+
conn = headers.get(hdrs.CONNECTION)
|
| 545 |
+
if conn:
|
| 546 |
+
v = conn.lower()
|
| 547 |
+
if v == "close":
|
| 548 |
+
close_conn = True
|
| 549 |
+
elif v == "keep-alive":
|
| 550 |
+
close_conn = False
|
| 551 |
+
# https://www.rfc-editor.org/rfc/rfc9110.html#name-101-switching-protocols
|
| 552 |
+
elif v == "upgrade" and headers.get(hdrs.UPGRADE):
|
| 553 |
+
upgrade = True
|
| 554 |
+
|
| 555 |
+
# encoding
|
| 556 |
+
enc = headers.get(hdrs.CONTENT_ENCODING, "")
|
| 557 |
+
if enc.isascii() and enc.lower() in {"gzip", "deflate", "br", "zstd"}:
|
| 558 |
+
encoding = enc
|
| 559 |
+
|
| 560 |
+
# chunking
|
| 561 |
+
te = headers.get(hdrs.TRANSFER_ENCODING)
|
| 562 |
+
if te is not None:
|
| 563 |
+
if self._is_chunked_te(te):
|
| 564 |
+
chunked = True
|
| 565 |
+
|
| 566 |
+
if hdrs.CONTENT_LENGTH in headers:
|
| 567 |
+
raise BadHttpMessage(
|
| 568 |
+
"Transfer-Encoding can't be present with Content-Length",
|
| 569 |
+
)
|
| 570 |
+
|
| 571 |
+
return (headers, raw_headers, close_conn, encoding, upgrade, chunked)
|
| 572 |
+
|
| 573 |
+
def set_upgraded(self, val: bool) -> None:
|
| 574 |
+
"""Set connection upgraded (to websocket) mode.
|
| 575 |
+
|
| 576 |
+
:param bool val: new state.
|
| 577 |
+
"""
|
| 578 |
+
self._upgraded = val
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
class HttpRequestParser(HttpParser[RawRequestMessage]):
|
| 582 |
+
"""Read request status line.
|
| 583 |
+
|
| 584 |
+
Exception .http_exceptions.BadStatusLine
|
| 585 |
+
could be raised in case of any errors in status line.
|
| 586 |
+
Returns RawRequestMessage.
|
| 587 |
+
"""
|
| 588 |
+
|
| 589 |
+
def parse_message(self, lines: List[bytes]) -> RawRequestMessage:
|
| 590 |
+
# request line
|
| 591 |
+
line = lines[0].decode("utf-8", "surrogateescape")
|
| 592 |
+
try:
|
| 593 |
+
method, path, version = line.split(" ", maxsplit=2)
|
| 594 |
+
except ValueError:
|
| 595 |
+
raise BadHttpMethod(line) from None
|
| 596 |
+
|
| 597 |
+
if len(path) > self.max_line_size:
|
| 598 |
+
raise LineTooLong(
|
| 599 |
+
"Status line is too long", str(self.max_line_size), str(len(path))
|
| 600 |
+
)
|
| 601 |
+
|
| 602 |
+
# method
|
| 603 |
+
if not TOKENRE.fullmatch(method):
|
| 604 |
+
raise BadHttpMethod(method)
|
| 605 |
+
|
| 606 |
+
# version
|
| 607 |
+
match = VERSRE.fullmatch(version)
|
| 608 |
+
if match is None:
|
| 609 |
+
raise BadStatusLine(line)
|
| 610 |
+
version_o = HttpVersion(int(match.group(1)), int(match.group(2)))
|
| 611 |
+
|
| 612 |
+
if method == "CONNECT":
|
| 613 |
+
# authority-form,
|
| 614 |
+
# https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3
|
| 615 |
+
url = URL.build(authority=path, encoded=True)
|
| 616 |
+
elif path.startswith("/"):
|
| 617 |
+
# origin-form,
|
| 618 |
+
# https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1
|
| 619 |
+
path_part, _hash_separator, url_fragment = path.partition("#")
|
| 620 |
+
path_part, _question_mark_separator, qs_part = path_part.partition("?")
|
| 621 |
+
|
| 622 |
+
# NOTE: `yarl.URL.build()` is used to mimic what the Cython-based
|
| 623 |
+
# NOTE: parser does, otherwise it results into the same
|
| 624 |
+
# NOTE: HTTP Request-Line input producing different
|
| 625 |
+
# NOTE: `yarl.URL()` objects
|
| 626 |
+
url = URL.build(
|
| 627 |
+
path=path_part,
|
| 628 |
+
query_string=qs_part,
|
| 629 |
+
fragment=url_fragment,
|
| 630 |
+
encoded=True,
|
| 631 |
+
)
|
| 632 |
+
elif path == "*" and method == "OPTIONS":
|
| 633 |
+
# asterisk-form,
|
| 634 |
+
url = URL(path, encoded=True)
|
| 635 |
+
else:
|
| 636 |
+
# absolute-form for proxy maybe,
|
| 637 |
+
# https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2
|
| 638 |
+
url = URL(path, encoded=True)
|
| 639 |
+
if url.scheme == "":
|
| 640 |
+
# not absolute-form
|
| 641 |
+
raise InvalidURLError(
|
| 642 |
+
path.encode(errors="surrogateescape").decode("latin1")
|
| 643 |
+
)
|
| 644 |
+
|
| 645 |
+
# read headers
|
| 646 |
+
(
|
| 647 |
+
headers,
|
| 648 |
+
raw_headers,
|
| 649 |
+
close,
|
| 650 |
+
compression,
|
| 651 |
+
upgrade,
|
| 652 |
+
chunked,
|
| 653 |
+
) = self.parse_headers(lines[1:])
|
| 654 |
+
|
| 655 |
+
if close is None: # then the headers weren't set in the request
|
| 656 |
+
if version_o <= HttpVersion10: # HTTP 1.0 must asks to not close
|
| 657 |
+
close = True
|
| 658 |
+
else: # HTTP 1.1 must ask to close.
|
| 659 |
+
close = False
|
| 660 |
+
|
| 661 |
+
return RawRequestMessage(
|
| 662 |
+
method,
|
| 663 |
+
path,
|
| 664 |
+
version_o,
|
| 665 |
+
headers,
|
| 666 |
+
raw_headers,
|
| 667 |
+
close,
|
| 668 |
+
compression,
|
| 669 |
+
upgrade,
|
| 670 |
+
chunked,
|
| 671 |
+
url,
|
| 672 |
+
)
|
| 673 |
+
|
| 674 |
+
def _is_chunked_te(self, te: str) -> bool:
|
| 675 |
+
te = te.rsplit(",", maxsplit=1)[-1].strip(" \t")
|
| 676 |
+
# .lower() transforms some non-ascii chars, so must check first.
|
| 677 |
+
if te.isascii() and te.lower() == "chunked":
|
| 678 |
+
return True
|
| 679 |
+
# https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.3
|
| 680 |
+
raise BadHttpMessage("Request has invalid `Transfer-Encoding`")
|
| 681 |
+
|
| 682 |
+
|
| 683 |
+
class HttpResponseParser(HttpParser[RawResponseMessage]):
|
| 684 |
+
"""Read response status line and headers.
|
| 685 |
+
|
| 686 |
+
BadStatusLine could be raised in case of any errors in status line.
|
| 687 |
+
Returns RawResponseMessage.
|
| 688 |
+
"""
|
| 689 |
+
|
| 690 |
+
# Lax mode should only be enabled on response parser.
|
| 691 |
+
lax = not DEBUG
|
| 692 |
+
|
| 693 |
+
def feed_data(
|
| 694 |
+
self,
|
| 695 |
+
data: bytes,
|
| 696 |
+
SEP: Optional[_SEP] = None,
|
| 697 |
+
*args: Any,
|
| 698 |
+
**kwargs: Any,
|
| 699 |
+
) -> Tuple[List[Tuple[RawResponseMessage, StreamReader]], bool, bytes]:
|
| 700 |
+
if SEP is None:
|
| 701 |
+
SEP = b"\r\n" if DEBUG else b"\n"
|
| 702 |
+
return super().feed_data(data, SEP, *args, **kwargs)
|
| 703 |
+
|
| 704 |
+
def parse_message(self, lines: List[bytes]) -> RawResponseMessage:
|
| 705 |
+
line = lines[0].decode("utf-8", "surrogateescape")
|
| 706 |
+
try:
|
| 707 |
+
version, status = line.split(maxsplit=1)
|
| 708 |
+
except ValueError:
|
| 709 |
+
raise BadStatusLine(line) from None
|
| 710 |
+
|
| 711 |
+
try:
|
| 712 |
+
status, reason = status.split(maxsplit=1)
|
| 713 |
+
except ValueError:
|
| 714 |
+
status = status.strip()
|
| 715 |
+
reason = ""
|
| 716 |
+
|
| 717 |
+
if len(reason) > self.max_line_size:
|
| 718 |
+
raise LineTooLong(
|
| 719 |
+
"Status line is too long", str(self.max_line_size), str(len(reason))
|
| 720 |
+
)
|
| 721 |
+
|
| 722 |
+
# version
|
| 723 |
+
match = VERSRE.fullmatch(version)
|
| 724 |
+
if match is None:
|
| 725 |
+
raise BadStatusLine(line)
|
| 726 |
+
version_o = HttpVersion(int(match.group(1)), int(match.group(2)))
|
| 727 |
+
|
| 728 |
+
# The status code is a three-digit ASCII number, no padding
|
| 729 |
+
if len(status) != 3 or not DIGITS.fullmatch(status):
|
| 730 |
+
raise BadStatusLine(line)
|
| 731 |
+
status_i = int(status)
|
| 732 |
+
|
| 733 |
+
# read headers
|
| 734 |
+
(
|
| 735 |
+
headers,
|
| 736 |
+
raw_headers,
|
| 737 |
+
close,
|
| 738 |
+
compression,
|
| 739 |
+
upgrade,
|
| 740 |
+
chunked,
|
| 741 |
+
) = self.parse_headers(lines[1:])
|
| 742 |
+
|
| 743 |
+
if close is None:
|
| 744 |
+
if version_o <= HttpVersion10:
|
| 745 |
+
close = True
|
| 746 |
+
# https://www.rfc-editor.org/rfc/rfc9112.html#name-message-body-length
|
| 747 |
+
elif 100 <= status_i < 200 or status_i in {204, 304}:
|
| 748 |
+
close = False
|
| 749 |
+
elif hdrs.CONTENT_LENGTH in headers or hdrs.TRANSFER_ENCODING in headers:
|
| 750 |
+
close = False
|
| 751 |
+
else:
|
| 752 |
+
# https://www.rfc-editor.org/rfc/rfc9112.html#section-6.3-2.8
|
| 753 |
+
close = True
|
| 754 |
+
|
| 755 |
+
return RawResponseMessage(
|
| 756 |
+
version_o,
|
| 757 |
+
status_i,
|
| 758 |
+
reason.strip(),
|
| 759 |
+
headers,
|
| 760 |
+
raw_headers,
|
| 761 |
+
close,
|
| 762 |
+
compression,
|
| 763 |
+
upgrade,
|
| 764 |
+
chunked,
|
| 765 |
+
)
|
| 766 |
+
|
| 767 |
+
def _is_chunked_te(self, te: str) -> bool:
|
| 768 |
+
# https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.2
|
| 769 |
+
return te.rsplit(",", maxsplit=1)[-1].strip(" \t").lower() == "chunked"
|
| 770 |
+
|
| 771 |
+
|
| 772 |
+
class HttpPayloadParser:
|
| 773 |
+
def __init__(
|
| 774 |
+
self,
|
| 775 |
+
payload: StreamReader,
|
| 776 |
+
length: Optional[int] = None,
|
| 777 |
+
chunked: bool = False,
|
| 778 |
+
compression: Optional[str] = None,
|
| 779 |
+
code: Optional[int] = None,
|
| 780 |
+
method: Optional[str] = None,
|
| 781 |
+
response_with_body: bool = True,
|
| 782 |
+
auto_decompress: bool = True,
|
| 783 |
+
lax: bool = False,
|
| 784 |
+
*,
|
| 785 |
+
headers_parser: HeadersParser,
|
| 786 |
+
) -> None:
|
| 787 |
+
self._length = 0
|
| 788 |
+
self._type = ParseState.PARSE_UNTIL_EOF
|
| 789 |
+
self._chunk = ChunkState.PARSE_CHUNKED_SIZE
|
| 790 |
+
self._chunk_size = 0
|
| 791 |
+
self._chunk_tail = b""
|
| 792 |
+
self._auto_decompress = auto_decompress
|
| 793 |
+
self._lax = lax
|
| 794 |
+
self._headers_parser = headers_parser
|
| 795 |
+
self._trailer_lines: list[bytes] = []
|
| 796 |
+
self.done = False
|
| 797 |
+
|
| 798 |
+
# payload decompression wrapper
|
| 799 |
+
if response_with_body and compression and self._auto_decompress:
|
| 800 |
+
real_payload: Union[StreamReader, DeflateBuffer] = DeflateBuffer(
|
| 801 |
+
payload, compression
|
| 802 |
+
)
|
| 803 |
+
else:
|
| 804 |
+
real_payload = payload
|
| 805 |
+
|
| 806 |
+
# payload parser
|
| 807 |
+
if not response_with_body:
|
| 808 |
+
# don't parse payload if it's not expected to be received
|
| 809 |
+
self._type = ParseState.PARSE_NONE
|
| 810 |
+
real_payload.feed_eof()
|
| 811 |
+
self.done = True
|
| 812 |
+
elif chunked:
|
| 813 |
+
self._type = ParseState.PARSE_CHUNKED
|
| 814 |
+
elif length is not None:
|
| 815 |
+
self._type = ParseState.PARSE_LENGTH
|
| 816 |
+
self._length = length
|
| 817 |
+
if self._length == 0:
|
| 818 |
+
real_payload.feed_eof()
|
| 819 |
+
self.done = True
|
| 820 |
+
|
| 821 |
+
self.payload = real_payload
|
| 822 |
+
|
| 823 |
+
def feed_eof(self) -> None:
|
| 824 |
+
if self._type == ParseState.PARSE_UNTIL_EOF:
|
| 825 |
+
self.payload.feed_eof()
|
| 826 |
+
elif self._type == ParseState.PARSE_LENGTH:
|
| 827 |
+
raise ContentLengthError(
|
| 828 |
+
"Not enough data to satisfy content length header."
|
| 829 |
+
)
|
| 830 |
+
elif self._type == ParseState.PARSE_CHUNKED:
|
| 831 |
+
raise TransferEncodingError(
|
| 832 |
+
"Not enough data to satisfy transfer length header."
|
| 833 |
+
)
|
| 834 |
+
|
| 835 |
+
def feed_data(
|
| 836 |
+
self, chunk: bytes, SEP: _SEP = b"\r\n", CHUNK_EXT: bytes = b";"
|
| 837 |
+
) -> Tuple[bool, bytes]:
|
| 838 |
+
# Read specified amount of bytes
|
| 839 |
+
if self._type == ParseState.PARSE_LENGTH:
|
| 840 |
+
required = self._length
|
| 841 |
+
chunk_len = len(chunk)
|
| 842 |
+
|
| 843 |
+
if required >= chunk_len:
|
| 844 |
+
self._length = required - chunk_len
|
| 845 |
+
self.payload.feed_data(chunk, chunk_len)
|
| 846 |
+
if self._length == 0:
|
| 847 |
+
self.payload.feed_eof()
|
| 848 |
+
return True, b""
|
| 849 |
+
else:
|
| 850 |
+
self._length = 0
|
| 851 |
+
self.payload.feed_data(chunk[:required], required)
|
| 852 |
+
self.payload.feed_eof()
|
| 853 |
+
return True, chunk[required:]
|
| 854 |
+
|
| 855 |
+
# Chunked transfer encoding parser
|
| 856 |
+
elif self._type == ParseState.PARSE_CHUNKED:
|
| 857 |
+
if self._chunk_tail:
|
| 858 |
+
chunk = self._chunk_tail + chunk
|
| 859 |
+
self._chunk_tail = b""
|
| 860 |
+
|
| 861 |
+
while chunk:
|
| 862 |
+
|
| 863 |
+
# read next chunk size
|
| 864 |
+
if self._chunk == ChunkState.PARSE_CHUNKED_SIZE:
|
| 865 |
+
pos = chunk.find(SEP)
|
| 866 |
+
if pos >= 0:
|
| 867 |
+
i = chunk.find(CHUNK_EXT, 0, pos)
|
| 868 |
+
if i >= 0:
|
| 869 |
+
size_b = chunk[:i] # strip chunk-extensions
|
| 870 |
+
# Verify no LF in the chunk-extension
|
| 871 |
+
if b"\n" in (ext := chunk[i:pos]):
|
| 872 |
+
exc = TransferEncodingError(
|
| 873 |
+
f"Unexpected LF in chunk-extension: {ext!r}"
|
| 874 |
+
)
|
| 875 |
+
set_exception(self.payload, exc)
|
| 876 |
+
raise exc
|
| 877 |
+
else:
|
| 878 |
+
size_b = chunk[:pos]
|
| 879 |
+
|
| 880 |
+
if self._lax: # Allow whitespace in lax mode.
|
| 881 |
+
size_b = size_b.strip()
|
| 882 |
+
|
| 883 |
+
if not re.fullmatch(HEXDIGITS, size_b):
|
| 884 |
+
exc = TransferEncodingError(
|
| 885 |
+
chunk[:pos].decode("ascii", "surrogateescape")
|
| 886 |
+
)
|
| 887 |
+
set_exception(self.payload, exc)
|
| 888 |
+
raise exc
|
| 889 |
+
size = int(bytes(size_b), 16)
|
| 890 |
+
|
| 891 |
+
chunk = chunk[pos + len(SEP) :]
|
| 892 |
+
if size == 0: # eof marker
|
| 893 |
+
self._chunk = ChunkState.PARSE_TRAILERS
|
| 894 |
+
if self._lax and chunk.startswith(b"\r"):
|
| 895 |
+
chunk = chunk[1:]
|
| 896 |
+
else:
|
| 897 |
+
self._chunk = ChunkState.PARSE_CHUNKED_CHUNK
|
| 898 |
+
self._chunk_size = size
|
| 899 |
+
self.payload.begin_http_chunk_receiving()
|
| 900 |
+
else:
|
| 901 |
+
self._chunk_tail = chunk
|
| 902 |
+
return False, b""
|
| 903 |
+
|
| 904 |
+
# read chunk and feed buffer
|
| 905 |
+
if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK:
|
| 906 |
+
required = self._chunk_size
|
| 907 |
+
chunk_len = len(chunk)
|
| 908 |
+
|
| 909 |
+
if required > chunk_len:
|
| 910 |
+
self._chunk_size = required - chunk_len
|
| 911 |
+
self.payload.feed_data(chunk, chunk_len)
|
| 912 |
+
return False, b""
|
| 913 |
+
else:
|
| 914 |
+
self._chunk_size = 0
|
| 915 |
+
self.payload.feed_data(chunk[:required], required)
|
| 916 |
+
chunk = chunk[required:]
|
| 917 |
+
self._chunk = ChunkState.PARSE_CHUNKED_CHUNK_EOF
|
| 918 |
+
self.payload.end_http_chunk_receiving()
|
| 919 |
+
|
| 920 |
+
# toss the CRLF at the end of the chunk
|
| 921 |
+
if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK_EOF:
|
| 922 |
+
if self._lax and chunk.startswith(b"\r"):
|
| 923 |
+
chunk = chunk[1:]
|
| 924 |
+
if chunk[: len(SEP)] == SEP:
|
| 925 |
+
chunk = chunk[len(SEP) :]
|
| 926 |
+
self._chunk = ChunkState.PARSE_CHUNKED_SIZE
|
| 927 |
+
else:
|
| 928 |
+
self._chunk_tail = chunk
|
| 929 |
+
return False, b""
|
| 930 |
+
|
| 931 |
+
if self._chunk == ChunkState.PARSE_TRAILERS:
|
| 932 |
+
pos = chunk.find(SEP)
|
| 933 |
+
if pos < 0: # No line found
|
| 934 |
+
self._chunk_tail = chunk
|
| 935 |
+
return False, b""
|
| 936 |
+
|
| 937 |
+
line = chunk[:pos]
|
| 938 |
+
chunk = chunk[pos + len(SEP) :]
|
| 939 |
+
if SEP == b"\n": # For lax response parsing
|
| 940 |
+
line = line.rstrip(b"\r")
|
| 941 |
+
self._trailer_lines.append(line)
|
| 942 |
+
|
| 943 |
+
# \r\n\r\n found, end of stream
|
| 944 |
+
if self._trailer_lines[-1] == b"":
|
| 945 |
+
# Headers and trailers are defined the same way,
|
| 946 |
+
# so we reuse the HeadersParser here.
|
| 947 |
+
try:
|
| 948 |
+
trailers, raw_trailers = self._headers_parser.parse_headers(
|
| 949 |
+
self._trailer_lines
|
| 950 |
+
)
|
| 951 |
+
finally:
|
| 952 |
+
self._trailer_lines.clear()
|
| 953 |
+
self.payload.feed_eof()
|
| 954 |
+
return True, chunk
|
| 955 |
+
|
| 956 |
+
# Read all bytes until eof
|
| 957 |
+
elif self._type == ParseState.PARSE_UNTIL_EOF:
|
| 958 |
+
self.payload.feed_data(chunk, len(chunk))
|
| 959 |
+
|
| 960 |
+
return False, b""
|
| 961 |
+
|
| 962 |
+
|
| 963 |
+
class DeflateBuffer:
|
| 964 |
+
"""DeflateStream decompress stream and feed data into specified stream."""
|
| 965 |
+
|
| 966 |
+
decompressor: Any
|
| 967 |
+
|
| 968 |
+
def __init__(
|
| 969 |
+
self,
|
| 970 |
+
out: StreamReader,
|
| 971 |
+
encoding: Optional[str],
|
| 972 |
+
max_decompress_size: int = DEFAULT_MAX_DECOMPRESS_SIZE,
|
| 973 |
+
) -> None:
|
| 974 |
+
self.out = out
|
| 975 |
+
self.size = 0
|
| 976 |
+
out.total_compressed_bytes = self.size
|
| 977 |
+
self.encoding = encoding
|
| 978 |
+
self._started_decoding = False
|
| 979 |
+
|
| 980 |
+
self.decompressor: Union[BrotliDecompressor, ZLibDecompressor, ZSTDDecompressor]
|
| 981 |
+
if encoding == "br":
|
| 982 |
+
if not HAS_BROTLI: # pragma: no cover
|
| 983 |
+
raise ContentEncodingError(
|
| 984 |
+
"Can not decode content-encoding: brotli (br). "
|
| 985 |
+
"Please install `Brotli`"
|
| 986 |
+
)
|
| 987 |
+
self.decompressor = BrotliDecompressor()
|
| 988 |
+
elif encoding == "zstd":
|
| 989 |
+
if not HAS_ZSTD:
|
| 990 |
+
raise ContentEncodingError(
|
| 991 |
+
"Can not decode content-encoding: zstandard (zstd). "
|
| 992 |
+
"Please install `backports.zstd`"
|
| 993 |
+
)
|
| 994 |
+
self.decompressor = ZSTDDecompressor()
|
| 995 |
+
else:
|
| 996 |
+
self.decompressor = ZLibDecompressor(encoding=encoding)
|
| 997 |
+
|
| 998 |
+
self._max_decompress_size = max_decompress_size
|
| 999 |
+
|
| 1000 |
+
def set_exception(
|
| 1001 |
+
self,
|
| 1002 |
+
exc: BaseException,
|
| 1003 |
+
exc_cause: BaseException = _EXC_SENTINEL,
|
| 1004 |
+
) -> None:
|
| 1005 |
+
set_exception(self.out, exc, exc_cause)
|
| 1006 |
+
|
| 1007 |
+
def feed_data(self, chunk: bytes, size: int) -> None:
|
| 1008 |
+
if not size:
|
| 1009 |
+
return
|
| 1010 |
+
|
| 1011 |
+
self.size += size
|
| 1012 |
+
self.out.total_compressed_bytes = self.size
|
| 1013 |
+
|
| 1014 |
+
# RFC1950
|
| 1015 |
+
# bits 0..3 = CM = 0b1000 = 8 = "deflate"
|
| 1016 |
+
# bits 4..7 = CINFO = 1..7 = windows size.
|
| 1017 |
+
if (
|
| 1018 |
+
not self._started_decoding
|
| 1019 |
+
and self.encoding == "deflate"
|
| 1020 |
+
and chunk[0] & 0xF != 8
|
| 1021 |
+
):
|
| 1022 |
+
# Change the decoder to decompress incorrectly compressed data
|
| 1023 |
+
# Actually we should issue a warning about non-RFC-compliant data.
|
| 1024 |
+
self.decompressor = ZLibDecompressor(
|
| 1025 |
+
encoding=self.encoding, suppress_deflate_header=True
|
| 1026 |
+
)
|
| 1027 |
+
|
| 1028 |
+
try:
|
| 1029 |
+
# Decompress with limit + 1 so we can detect if output exceeds limit
|
| 1030 |
+
chunk = self.decompressor.decompress_sync(
|
| 1031 |
+
chunk, max_length=self._max_decompress_size + 1
|
| 1032 |
+
)
|
| 1033 |
+
except Exception:
|
| 1034 |
+
raise ContentEncodingError(
|
| 1035 |
+
"Can not decode content-encoding: %s" % self.encoding
|
| 1036 |
+
)
|
| 1037 |
+
|
| 1038 |
+
self._started_decoding = True
|
| 1039 |
+
|
| 1040 |
+
# Check if decompression limit was exceeded
|
| 1041 |
+
if len(chunk) > self._max_decompress_size:
|
| 1042 |
+
raise DecompressSizeError(
|
| 1043 |
+
"Decompressed data exceeds the configured limit of %d bytes"
|
| 1044 |
+
% self._max_decompress_size
|
| 1045 |
+
)
|
| 1046 |
+
|
| 1047 |
+
if chunk:
|
| 1048 |
+
self.out.feed_data(chunk, len(chunk))
|
| 1049 |
+
|
| 1050 |
+
def feed_eof(self) -> None:
|
| 1051 |
+
chunk = self.decompressor.flush()
|
| 1052 |
+
|
| 1053 |
+
if chunk or self.size > 0:
|
| 1054 |
+
self.out.feed_data(chunk, len(chunk))
|
| 1055 |
+
if self.encoding == "deflate" and not self.decompressor.eof:
|
| 1056 |
+
raise ContentEncodingError("deflate")
|
| 1057 |
+
|
| 1058 |
+
self.out.feed_eof()
|
| 1059 |
+
|
| 1060 |
+
def begin_http_chunk_receiving(self) -> None:
|
| 1061 |
+
self.out.begin_http_chunk_receiving()
|
| 1062 |
+
|
| 1063 |
+
def end_http_chunk_receiving(self) -> None:
|
| 1064 |
+
self.out.end_http_chunk_receiving()
|
| 1065 |
+
|
| 1066 |
+
|
| 1067 |
+
HttpRequestParserPy = HttpRequestParser
|
| 1068 |
+
HttpResponseParserPy = HttpResponseParser
|
| 1069 |
+
RawRequestMessagePy = RawRequestMessage
|
| 1070 |
+
RawResponseMessagePy = RawResponseMessage
|
| 1071 |
+
|
| 1072 |
+
try:
|
| 1073 |
+
if not NO_EXTENSIONS:
|
| 1074 |
+
from ._http_parser import ( # type: ignore[import-not-found,no-redef]
|
| 1075 |
+
HttpRequestParser,
|
| 1076 |
+
HttpResponseParser,
|
| 1077 |
+
RawRequestMessage,
|
| 1078 |
+
RawResponseMessage,
|
| 1079 |
+
)
|
| 1080 |
+
|
| 1081 |
+
HttpRequestParserC = HttpRequestParser
|
| 1082 |
+
HttpResponseParserC = HttpResponseParser
|
| 1083 |
+
RawRequestMessageC = RawRequestMessage
|
| 1084 |
+
RawResponseMessageC = RawResponseMessage
|
| 1085 |
+
except ImportError: # pragma: no cover
|
| 1086 |
+
pass
|
aiohttp/http_writer.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Http related parsers and protocol."""
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import sys
|
| 5 |
+
from typing import ( # noqa
|
| 6 |
+
TYPE_CHECKING,
|
| 7 |
+
Any,
|
| 8 |
+
Awaitable,
|
| 9 |
+
Callable,
|
| 10 |
+
Iterable,
|
| 11 |
+
List,
|
| 12 |
+
NamedTuple,
|
| 13 |
+
Optional,
|
| 14 |
+
Union,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
from multidict import CIMultiDict
|
| 18 |
+
|
| 19 |
+
from .abc import AbstractStreamWriter
|
| 20 |
+
from .base_protocol import BaseProtocol
|
| 21 |
+
from .client_exceptions import ClientConnectionResetError
|
| 22 |
+
from .compression_utils import ZLibCompressor
|
| 23 |
+
from .helpers import NO_EXTENSIONS
|
| 24 |
+
|
| 25 |
+
__all__ = ("StreamWriter", "HttpVersion", "HttpVersion10", "HttpVersion11")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
MIN_PAYLOAD_FOR_WRITELINES = 2048
|
| 29 |
+
IS_PY313_BEFORE_313_2 = (3, 13, 0) <= sys.version_info < (3, 13, 2)
|
| 30 |
+
IS_PY_BEFORE_312_9 = sys.version_info < (3, 12, 9)
|
| 31 |
+
SKIP_WRITELINES = IS_PY313_BEFORE_313_2 or IS_PY_BEFORE_312_9
|
| 32 |
+
# writelines is not safe for use
|
| 33 |
+
# on Python 3.12+ until 3.12.9
|
| 34 |
+
# on Python 3.13+ until 3.13.2
|
| 35 |
+
# and on older versions it not any faster than write
|
| 36 |
+
# CVE-2024-12254: https://github.com/python/cpython/pull/127656
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class HttpVersion(NamedTuple):
|
| 40 |
+
major: int
|
| 41 |
+
minor: int
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
HttpVersion10 = HttpVersion(1, 0)
|
| 45 |
+
HttpVersion11 = HttpVersion(1, 1)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
_T_OnChunkSent = Optional[Callable[[bytes], Awaitable[None]]]
|
| 49 |
+
_T_OnHeadersSent = Optional[Callable[["CIMultiDict[str]"], Awaitable[None]]]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class StreamWriter(AbstractStreamWriter):
|
| 53 |
+
|
| 54 |
+
length: Optional[int] = None
|
| 55 |
+
chunked: bool = False
|
| 56 |
+
_eof: bool = False
|
| 57 |
+
_compress: Optional[ZLibCompressor] = None
|
| 58 |
+
|
| 59 |
+
def __init__(
|
| 60 |
+
self,
|
| 61 |
+
protocol: BaseProtocol,
|
| 62 |
+
loop: asyncio.AbstractEventLoop,
|
| 63 |
+
on_chunk_sent: _T_OnChunkSent = None,
|
| 64 |
+
on_headers_sent: _T_OnHeadersSent = None,
|
| 65 |
+
) -> None:
|
| 66 |
+
self._protocol = protocol
|
| 67 |
+
self.loop = loop
|
| 68 |
+
self._on_chunk_sent: _T_OnChunkSent = on_chunk_sent
|
| 69 |
+
self._on_headers_sent: _T_OnHeadersSent = on_headers_sent
|
| 70 |
+
self._headers_buf: Optional[bytes] = None
|
| 71 |
+
self._headers_written: bool = False
|
| 72 |
+
|
| 73 |
+
@property
|
| 74 |
+
def transport(self) -> Optional[asyncio.Transport]:
|
| 75 |
+
return self._protocol.transport
|
| 76 |
+
|
| 77 |
+
@property
|
| 78 |
+
def protocol(self) -> BaseProtocol:
|
| 79 |
+
return self._protocol
|
| 80 |
+
|
| 81 |
+
def enable_chunking(self) -> None:
|
| 82 |
+
self.chunked = True
|
| 83 |
+
|
| 84 |
+
def enable_compression(
|
| 85 |
+
self, encoding: str = "deflate", strategy: Optional[int] = None
|
| 86 |
+
) -> None:
|
| 87 |
+
self._compress = ZLibCompressor(encoding=encoding, strategy=strategy)
|
| 88 |
+
|
| 89 |
+
def _write(self, chunk: Union[bytes, bytearray, memoryview]) -> None:
|
| 90 |
+
size = len(chunk)
|
| 91 |
+
self.buffer_size += size
|
| 92 |
+
self.output_size += size
|
| 93 |
+
transport = self._protocol.transport
|
| 94 |
+
if transport is None or transport.is_closing():
|
| 95 |
+
raise ClientConnectionResetError("Cannot write to closing transport")
|
| 96 |
+
transport.write(chunk)
|
| 97 |
+
|
| 98 |
+
def _writelines(self, chunks: Iterable[bytes]) -> None:
|
| 99 |
+
size = 0
|
| 100 |
+
for chunk in chunks:
|
| 101 |
+
size += len(chunk)
|
| 102 |
+
self.buffer_size += size
|
| 103 |
+
self.output_size += size
|
| 104 |
+
transport = self._protocol.transport
|
| 105 |
+
if transport is None or transport.is_closing():
|
| 106 |
+
raise ClientConnectionResetError("Cannot write to closing transport")
|
| 107 |
+
if SKIP_WRITELINES or size < MIN_PAYLOAD_FOR_WRITELINES:
|
| 108 |
+
transport.write(b"".join(chunks))
|
| 109 |
+
else:
|
| 110 |
+
transport.writelines(chunks)
|
| 111 |
+
|
| 112 |
+
def _write_chunked_payload(
|
| 113 |
+
self, chunk: Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"]
|
| 114 |
+
) -> None:
|
| 115 |
+
"""Write a chunk with proper chunked encoding."""
|
| 116 |
+
chunk_len_pre = f"{len(chunk):x}\r\n".encode("ascii")
|
| 117 |
+
self._writelines((chunk_len_pre, chunk, b"\r\n"))
|
| 118 |
+
|
| 119 |
+
def _send_headers_with_payload(
|
| 120 |
+
self,
|
| 121 |
+
chunk: Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"],
|
| 122 |
+
is_eof: bool,
|
| 123 |
+
) -> None:
|
| 124 |
+
"""Send buffered headers with payload, coalescing into single write."""
|
| 125 |
+
# Mark headers as written
|
| 126 |
+
self._headers_written = True
|
| 127 |
+
headers_buf = self._headers_buf
|
| 128 |
+
self._headers_buf = None
|
| 129 |
+
|
| 130 |
+
if TYPE_CHECKING:
|
| 131 |
+
# Safe because callers (write() and write_eof()) only invoke this method
|
| 132 |
+
# after checking that self._headers_buf is truthy
|
| 133 |
+
assert headers_buf is not None
|
| 134 |
+
|
| 135 |
+
if not self.chunked:
|
| 136 |
+
# Non-chunked: coalesce headers with body
|
| 137 |
+
if chunk:
|
| 138 |
+
self._writelines((headers_buf, chunk))
|
| 139 |
+
else:
|
| 140 |
+
self._write(headers_buf)
|
| 141 |
+
return
|
| 142 |
+
|
| 143 |
+
# Coalesce headers with chunked data
|
| 144 |
+
if chunk:
|
| 145 |
+
chunk_len_pre = f"{len(chunk):x}\r\n".encode("ascii")
|
| 146 |
+
if is_eof:
|
| 147 |
+
self._writelines((headers_buf, chunk_len_pre, chunk, b"\r\n0\r\n\r\n"))
|
| 148 |
+
else:
|
| 149 |
+
self._writelines((headers_buf, chunk_len_pre, chunk, b"\r\n"))
|
| 150 |
+
elif is_eof:
|
| 151 |
+
self._writelines((headers_buf, b"0\r\n\r\n"))
|
| 152 |
+
else:
|
| 153 |
+
self._write(headers_buf)
|
| 154 |
+
|
| 155 |
+
async def write(
|
| 156 |
+
self,
|
| 157 |
+
chunk: Union[bytes, bytearray, memoryview],
|
| 158 |
+
*,
|
| 159 |
+
drain: bool = True,
|
| 160 |
+
LIMIT: int = 0x10000,
|
| 161 |
+
) -> None:
|
| 162 |
+
"""
|
| 163 |
+
Writes chunk of data to a stream.
|
| 164 |
+
|
| 165 |
+
write_eof() indicates end of stream.
|
| 166 |
+
writer can't be used after write_eof() method being called.
|
| 167 |
+
write() return drain future.
|
| 168 |
+
"""
|
| 169 |
+
if self._on_chunk_sent is not None:
|
| 170 |
+
await self._on_chunk_sent(chunk)
|
| 171 |
+
|
| 172 |
+
if isinstance(chunk, memoryview):
|
| 173 |
+
if chunk.nbytes != len(chunk):
|
| 174 |
+
# just reshape it
|
| 175 |
+
chunk = chunk.cast("c")
|
| 176 |
+
|
| 177 |
+
if self._compress is not None:
|
| 178 |
+
chunk = await self._compress.compress(chunk)
|
| 179 |
+
if not chunk:
|
| 180 |
+
return
|
| 181 |
+
|
| 182 |
+
if self.length is not None:
|
| 183 |
+
chunk_len = len(chunk)
|
| 184 |
+
if self.length >= chunk_len:
|
| 185 |
+
self.length = self.length - chunk_len
|
| 186 |
+
else:
|
| 187 |
+
chunk = chunk[: self.length]
|
| 188 |
+
self.length = 0
|
| 189 |
+
if not chunk:
|
| 190 |
+
return
|
| 191 |
+
|
| 192 |
+
# Handle buffered headers for small payload optimization
|
| 193 |
+
if self._headers_buf and not self._headers_written:
|
| 194 |
+
self._send_headers_with_payload(chunk, False)
|
| 195 |
+
if drain and self.buffer_size > LIMIT:
|
| 196 |
+
self.buffer_size = 0
|
| 197 |
+
await self.drain()
|
| 198 |
+
return
|
| 199 |
+
|
| 200 |
+
if chunk:
|
| 201 |
+
if self.chunked:
|
| 202 |
+
self._write_chunked_payload(chunk)
|
| 203 |
+
else:
|
| 204 |
+
self._write(chunk)
|
| 205 |
+
|
| 206 |
+
if drain and self.buffer_size > LIMIT:
|
| 207 |
+
self.buffer_size = 0
|
| 208 |
+
await self.drain()
|
| 209 |
+
|
| 210 |
+
async def write_headers(
|
| 211 |
+
self, status_line: str, headers: "CIMultiDict[str]"
|
| 212 |
+
) -> None:
|
| 213 |
+
"""Write headers to the stream."""
|
| 214 |
+
if self._on_headers_sent is not None:
|
| 215 |
+
await self._on_headers_sent(headers)
|
| 216 |
+
# status + headers
|
| 217 |
+
buf = _serialize_headers(status_line, headers)
|
| 218 |
+
self._headers_written = False
|
| 219 |
+
self._headers_buf = buf
|
| 220 |
+
|
| 221 |
+
def send_headers(self) -> None:
|
| 222 |
+
"""Force sending buffered headers if not already sent."""
|
| 223 |
+
if not self._headers_buf or self._headers_written:
|
| 224 |
+
return
|
| 225 |
+
|
| 226 |
+
self._headers_written = True
|
| 227 |
+
headers_buf = self._headers_buf
|
| 228 |
+
self._headers_buf = None
|
| 229 |
+
|
| 230 |
+
if TYPE_CHECKING:
|
| 231 |
+
# Safe because we only enter this block when self._headers_buf is truthy
|
| 232 |
+
assert headers_buf is not None
|
| 233 |
+
|
| 234 |
+
self._write(headers_buf)
|
| 235 |
+
|
| 236 |
+
def set_eof(self) -> None:
|
| 237 |
+
"""Indicate that the message is complete."""
|
| 238 |
+
if self._eof:
|
| 239 |
+
return
|
| 240 |
+
|
| 241 |
+
# If headers haven't been sent yet, send them now
|
| 242 |
+
# This handles the case where there's no body at all
|
| 243 |
+
if self._headers_buf and not self._headers_written:
|
| 244 |
+
self._headers_written = True
|
| 245 |
+
headers_buf = self._headers_buf
|
| 246 |
+
self._headers_buf = None
|
| 247 |
+
|
| 248 |
+
if TYPE_CHECKING:
|
| 249 |
+
# Safe because we only enter this block when self._headers_buf is truthy
|
| 250 |
+
assert headers_buf is not None
|
| 251 |
+
|
| 252 |
+
# Combine headers and chunked EOF marker in a single write
|
| 253 |
+
if self.chunked:
|
| 254 |
+
self._writelines((headers_buf, b"0\r\n\r\n"))
|
| 255 |
+
else:
|
| 256 |
+
self._write(headers_buf)
|
| 257 |
+
elif self.chunked and self._headers_written:
|
| 258 |
+
# Headers already sent, just send the final chunk marker
|
| 259 |
+
self._write(b"0\r\n\r\n")
|
| 260 |
+
|
| 261 |
+
self._eof = True
|
| 262 |
+
|
| 263 |
+
async def write_eof(self, chunk: bytes = b"") -> None:
|
| 264 |
+
if self._eof:
|
| 265 |
+
return
|
| 266 |
+
|
| 267 |
+
if chunk and self._on_chunk_sent is not None:
|
| 268 |
+
await self._on_chunk_sent(chunk)
|
| 269 |
+
|
| 270 |
+
# Handle body/compression
|
| 271 |
+
if self._compress:
|
| 272 |
+
chunks: List[bytes] = []
|
| 273 |
+
chunks_len = 0
|
| 274 |
+
if chunk and (compressed_chunk := await self._compress.compress(chunk)):
|
| 275 |
+
chunks_len = len(compressed_chunk)
|
| 276 |
+
chunks.append(compressed_chunk)
|
| 277 |
+
|
| 278 |
+
flush_chunk = self._compress.flush()
|
| 279 |
+
chunks_len += len(flush_chunk)
|
| 280 |
+
chunks.append(flush_chunk)
|
| 281 |
+
assert chunks_len
|
| 282 |
+
|
| 283 |
+
# Send buffered headers with compressed data if not yet sent
|
| 284 |
+
if self._headers_buf and not self._headers_written:
|
| 285 |
+
self._headers_written = True
|
| 286 |
+
headers_buf = self._headers_buf
|
| 287 |
+
self._headers_buf = None
|
| 288 |
+
|
| 289 |
+
if self.chunked:
|
| 290 |
+
# Coalesce headers with compressed chunked data
|
| 291 |
+
chunk_len_pre = f"{chunks_len:x}\r\n".encode("ascii")
|
| 292 |
+
self._writelines(
|
| 293 |
+
(headers_buf, chunk_len_pre, *chunks, b"\r\n0\r\n\r\n")
|
| 294 |
+
)
|
| 295 |
+
else:
|
| 296 |
+
# Coalesce headers with compressed data
|
| 297 |
+
self._writelines((headers_buf, *chunks))
|
| 298 |
+
await self.drain()
|
| 299 |
+
self._eof = True
|
| 300 |
+
return
|
| 301 |
+
|
| 302 |
+
# Headers already sent, just write compressed data
|
| 303 |
+
if self.chunked:
|
| 304 |
+
chunk_len_pre = f"{chunks_len:x}\r\n".encode("ascii")
|
| 305 |
+
self._writelines((chunk_len_pre, *chunks, b"\r\n0\r\n\r\n"))
|
| 306 |
+
elif len(chunks) > 1:
|
| 307 |
+
self._writelines(chunks)
|
| 308 |
+
else:
|
| 309 |
+
self._write(chunks[0])
|
| 310 |
+
await self.drain()
|
| 311 |
+
self._eof = True
|
| 312 |
+
return
|
| 313 |
+
|
| 314 |
+
# No compression - send buffered headers if not yet sent
|
| 315 |
+
if self._headers_buf and not self._headers_written:
|
| 316 |
+
# Use helper to send headers with payload
|
| 317 |
+
self._send_headers_with_payload(chunk, True)
|
| 318 |
+
await self.drain()
|
| 319 |
+
self._eof = True
|
| 320 |
+
return
|
| 321 |
+
|
| 322 |
+
# Handle remaining body
|
| 323 |
+
if self.chunked:
|
| 324 |
+
if chunk:
|
| 325 |
+
# Write final chunk with EOF marker
|
| 326 |
+
self._writelines(
|
| 327 |
+
(f"{len(chunk):x}\r\n".encode("ascii"), chunk, b"\r\n0\r\n\r\n")
|
| 328 |
+
)
|
| 329 |
+
else:
|
| 330 |
+
self._write(b"0\r\n\r\n")
|
| 331 |
+
await self.drain()
|
| 332 |
+
self._eof = True
|
| 333 |
+
return
|
| 334 |
+
|
| 335 |
+
if chunk:
|
| 336 |
+
self._write(chunk)
|
| 337 |
+
await self.drain()
|
| 338 |
+
|
| 339 |
+
self._eof = True
|
| 340 |
+
|
| 341 |
+
async def drain(self) -> None:
|
| 342 |
+
"""Flush the write buffer.
|
| 343 |
+
|
| 344 |
+
The intended use is to write
|
| 345 |
+
|
| 346 |
+
await w.write(data)
|
| 347 |
+
await w.drain()
|
| 348 |
+
"""
|
| 349 |
+
protocol = self._protocol
|
| 350 |
+
if protocol.transport is not None and protocol._paused:
|
| 351 |
+
await protocol._drain_helper()
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
def _safe_header(string: str) -> str:
|
| 355 |
+
if "\r" in string or "\n" in string:
|
| 356 |
+
raise ValueError(
|
| 357 |
+
"Newline or carriage return detected in headers. "
|
| 358 |
+
"Potential header injection attack."
|
| 359 |
+
)
|
| 360 |
+
return string
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def _py_serialize_headers(status_line: str, headers: "CIMultiDict[str]") -> bytes:
|
| 364 |
+
headers_gen = (_safe_header(k) + ": " + _safe_header(v) for k, v in headers.items())
|
| 365 |
+
line = status_line + "\r\n" + "\r\n".join(headers_gen) + "\r\n\r\n"
|
| 366 |
+
return line.encode("utf-8")
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
_serialize_headers = _py_serialize_headers
|
| 370 |
+
|
| 371 |
+
try:
|
| 372 |
+
import aiohttp._http_writer as _http_writer # type: ignore[import-not-found]
|
| 373 |
+
|
| 374 |
+
_c_serialize_headers = _http_writer._serialize_headers
|
| 375 |
+
if not NO_EXTENSIONS:
|
| 376 |
+
_serialize_headers = _c_serialize_headers
|
| 377 |
+
except ImportError:
|
| 378 |
+
pass
|
aiohttp/payload_streamer.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Payload implementation for coroutines as data provider.
|
| 3 |
+
|
| 4 |
+
As a simple case, you can upload data from file::
|
| 5 |
+
|
| 6 |
+
@aiohttp.streamer
|
| 7 |
+
async def file_sender(writer, file_name=None):
|
| 8 |
+
with open(file_name, 'rb') as f:
|
| 9 |
+
chunk = f.read(2**16)
|
| 10 |
+
while chunk:
|
| 11 |
+
await writer.write(chunk)
|
| 12 |
+
|
| 13 |
+
chunk = f.read(2**16)
|
| 14 |
+
|
| 15 |
+
Then you can use `file_sender` like this:
|
| 16 |
+
|
| 17 |
+
async with session.post('http://httpbin.org/post',
|
| 18 |
+
data=file_sender(file_name='huge_file')) as resp:
|
| 19 |
+
print(await resp.text())
|
| 20 |
+
|
| 21 |
+
..note:: Coroutine must accept `writer` as first argument
|
| 22 |
+
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
import types
|
| 26 |
+
import warnings
|
| 27 |
+
from typing import Any, Awaitable, Callable, Dict, Tuple
|
| 28 |
+
|
| 29 |
+
from .abc import AbstractStreamWriter
|
| 30 |
+
from .payload import Payload, payload_type
|
| 31 |
+
|
| 32 |
+
__all__ = ("streamer",)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class _stream_wrapper:
|
| 36 |
+
def __init__(
|
| 37 |
+
self,
|
| 38 |
+
coro: Callable[..., Awaitable[None]],
|
| 39 |
+
args: Tuple[Any, ...],
|
| 40 |
+
kwargs: Dict[str, Any],
|
| 41 |
+
) -> None:
|
| 42 |
+
self.coro = types.coroutine(coro)
|
| 43 |
+
self.args = args
|
| 44 |
+
self.kwargs = kwargs
|
| 45 |
+
|
| 46 |
+
async def __call__(self, writer: AbstractStreamWriter) -> None:
|
| 47 |
+
await self.coro(writer, *self.args, **self.kwargs)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class streamer:
|
| 51 |
+
def __init__(self, coro: Callable[..., Awaitable[None]]) -> None:
|
| 52 |
+
warnings.warn(
|
| 53 |
+
"@streamer is deprecated, use async generators instead",
|
| 54 |
+
DeprecationWarning,
|
| 55 |
+
stacklevel=2,
|
| 56 |
+
)
|
| 57 |
+
self.coro = coro
|
| 58 |
+
|
| 59 |
+
def __call__(self, *args: Any, **kwargs: Any) -> _stream_wrapper:
|
| 60 |
+
return _stream_wrapper(self.coro, args, kwargs)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@payload_type(_stream_wrapper)
|
| 64 |
+
class StreamWrapperPayload(Payload):
|
| 65 |
+
async def write(self, writer: AbstractStreamWriter) -> None:
|
| 66 |
+
await self._value(writer)
|
| 67 |
+
|
| 68 |
+
def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str:
|
| 69 |
+
raise TypeError("Unable to decode.")
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@payload_type(streamer)
|
| 73 |
+
class StreamPayload(StreamWrapperPayload):
|
| 74 |
+
def __init__(self, value: Any, *args: Any, **kwargs: Any) -> None:
|
| 75 |
+
super().__init__(value(), *args, **kwargs)
|
| 76 |
+
|
| 77 |
+
async def write(self, writer: AbstractStreamWriter) -> None:
|
| 78 |
+
await self._value(writer)
|
aiohttp/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Marker
|
aiohttp/pytest_plugin.py
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import contextlib
|
| 3 |
+
import inspect
|
| 4 |
+
import warnings
|
| 5 |
+
from typing import (
|
| 6 |
+
Any,
|
| 7 |
+
Awaitable,
|
| 8 |
+
Callable,
|
| 9 |
+
Dict,
|
| 10 |
+
Iterator,
|
| 11 |
+
Optional,
|
| 12 |
+
Protocol,
|
| 13 |
+
Union,
|
| 14 |
+
overload,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
import pytest
|
| 18 |
+
|
| 19 |
+
from .test_utils import (
|
| 20 |
+
BaseTestServer,
|
| 21 |
+
RawTestServer,
|
| 22 |
+
TestClient,
|
| 23 |
+
TestServer,
|
| 24 |
+
loop_context,
|
| 25 |
+
setup_test_loop,
|
| 26 |
+
teardown_test_loop,
|
| 27 |
+
unused_port as _unused_port,
|
| 28 |
+
)
|
| 29 |
+
from .web import Application, BaseRequest, Request
|
| 30 |
+
from .web_protocol import _RequestHandler
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
import uvloop
|
| 34 |
+
except ImportError: # pragma: no cover
|
| 35 |
+
uvloop = None # type: ignore[assignment]
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class AiohttpClient(Protocol):
|
| 39 |
+
@overload
|
| 40 |
+
async def __call__(
|
| 41 |
+
self,
|
| 42 |
+
__param: Application,
|
| 43 |
+
*,
|
| 44 |
+
server_kwargs: Optional[Dict[str, Any]] = None,
|
| 45 |
+
**kwargs: Any,
|
| 46 |
+
) -> TestClient[Request, Application]: ...
|
| 47 |
+
@overload
|
| 48 |
+
async def __call__(
|
| 49 |
+
self,
|
| 50 |
+
__param: BaseTestServer,
|
| 51 |
+
*,
|
| 52 |
+
server_kwargs: Optional[Dict[str, Any]] = None,
|
| 53 |
+
**kwargs: Any,
|
| 54 |
+
) -> TestClient[BaseRequest, None]: ...
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class AiohttpServer(Protocol):
|
| 58 |
+
def __call__(
|
| 59 |
+
self, app: Application, *, port: Optional[int] = None, **kwargs: Any
|
| 60 |
+
) -> Awaitable[TestServer]: ...
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class AiohttpRawServer(Protocol):
|
| 64 |
+
def __call__(
|
| 65 |
+
self, handler: _RequestHandler, *, port: Optional[int] = None, **kwargs: Any
|
| 66 |
+
) -> Awaitable[RawTestServer]: ...
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def pytest_addoption(parser): # type: ignore[no-untyped-def]
|
| 70 |
+
parser.addoption(
|
| 71 |
+
"--aiohttp-fast",
|
| 72 |
+
action="store_true",
|
| 73 |
+
default=False,
|
| 74 |
+
help="run tests faster by disabling extra checks",
|
| 75 |
+
)
|
| 76 |
+
parser.addoption(
|
| 77 |
+
"--aiohttp-loop",
|
| 78 |
+
action="store",
|
| 79 |
+
default="pyloop",
|
| 80 |
+
help="run tests with specific loop: pyloop, uvloop or all",
|
| 81 |
+
)
|
| 82 |
+
parser.addoption(
|
| 83 |
+
"--aiohttp-enable-loop-debug",
|
| 84 |
+
action="store_true",
|
| 85 |
+
default=False,
|
| 86 |
+
help="enable event loop debug mode",
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def pytest_fixture_setup(fixturedef): # type: ignore[no-untyped-def]
|
| 91 |
+
"""Set up pytest fixture.
|
| 92 |
+
|
| 93 |
+
Allow fixtures to be coroutines. Run coroutine fixtures in an event loop.
|
| 94 |
+
"""
|
| 95 |
+
func = fixturedef.func
|
| 96 |
+
|
| 97 |
+
if inspect.isasyncgenfunction(func):
|
| 98 |
+
# async generator fixture
|
| 99 |
+
is_async_gen = True
|
| 100 |
+
elif inspect.iscoroutinefunction(func):
|
| 101 |
+
# regular async fixture
|
| 102 |
+
is_async_gen = False
|
| 103 |
+
else:
|
| 104 |
+
# not an async fixture, nothing to do
|
| 105 |
+
return
|
| 106 |
+
|
| 107 |
+
strip_request = False
|
| 108 |
+
if "request" not in fixturedef.argnames:
|
| 109 |
+
fixturedef.argnames += ("request",)
|
| 110 |
+
strip_request = True
|
| 111 |
+
|
| 112 |
+
def wrapper(*args, **kwargs): # type: ignore[no-untyped-def]
|
| 113 |
+
request = kwargs["request"]
|
| 114 |
+
if strip_request:
|
| 115 |
+
del kwargs["request"]
|
| 116 |
+
|
| 117 |
+
# if neither the fixture nor the test use the 'loop' fixture,
|
| 118 |
+
# 'getfixturevalue' will fail because the test is not parameterized
|
| 119 |
+
# (this can be removed someday if 'loop' is no longer parameterized)
|
| 120 |
+
if "loop" not in request.fixturenames:
|
| 121 |
+
raise Exception(
|
| 122 |
+
"Asynchronous fixtures must depend on the 'loop' fixture or "
|
| 123 |
+
"be used in tests depending from it."
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
_loop = request.getfixturevalue("loop")
|
| 127 |
+
|
| 128 |
+
if is_async_gen:
|
| 129 |
+
# for async generators, we need to advance the generator once,
|
| 130 |
+
# then advance it again in a finalizer
|
| 131 |
+
gen = func(*args, **kwargs)
|
| 132 |
+
|
| 133 |
+
def finalizer(): # type: ignore[no-untyped-def]
|
| 134 |
+
try:
|
| 135 |
+
return _loop.run_until_complete(gen.__anext__())
|
| 136 |
+
except StopAsyncIteration:
|
| 137 |
+
pass
|
| 138 |
+
|
| 139 |
+
request.addfinalizer(finalizer)
|
| 140 |
+
return _loop.run_until_complete(gen.__anext__())
|
| 141 |
+
else:
|
| 142 |
+
return _loop.run_until_complete(func(*args, **kwargs))
|
| 143 |
+
|
| 144 |
+
fixturedef.func = wrapper
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
@pytest.fixture
|
| 148 |
+
def fast(request): # type: ignore[no-untyped-def]
|
| 149 |
+
"""--fast config option"""
|
| 150 |
+
return request.config.getoption("--aiohttp-fast")
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
@pytest.fixture
|
| 154 |
+
def loop_debug(request): # type: ignore[no-untyped-def]
|
| 155 |
+
"""--enable-loop-debug config option"""
|
| 156 |
+
return request.config.getoption("--aiohttp-enable-loop-debug")
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
@contextlib.contextmanager
|
| 160 |
+
def _runtime_warning_context(): # type: ignore[no-untyped-def]
|
| 161 |
+
"""Context manager which checks for RuntimeWarnings.
|
| 162 |
+
|
| 163 |
+
This exists specifically to
|
| 164 |
+
avoid "coroutine 'X' was never awaited" warnings being missed.
|
| 165 |
+
|
| 166 |
+
If RuntimeWarnings occur in the context a RuntimeError is raised.
|
| 167 |
+
"""
|
| 168 |
+
with warnings.catch_warnings(record=True) as _warnings:
|
| 169 |
+
yield
|
| 170 |
+
rw = [
|
| 171 |
+
"{w.filename}:{w.lineno}:{w.message}".format(w=w)
|
| 172 |
+
for w in _warnings
|
| 173 |
+
if w.category == RuntimeWarning
|
| 174 |
+
]
|
| 175 |
+
if rw:
|
| 176 |
+
raise RuntimeError(
|
| 177 |
+
"{} Runtime Warning{},\n{}".format(
|
| 178 |
+
len(rw), "" if len(rw) == 1 else "s", "\n".join(rw)
|
| 179 |
+
)
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
@contextlib.contextmanager
|
| 184 |
+
def _passthrough_loop_context(loop, fast=False): # type: ignore[no-untyped-def]
|
| 185 |
+
"""Passthrough loop context.
|
| 186 |
+
|
| 187 |
+
Sets up and tears down a loop unless one is passed in via the loop
|
| 188 |
+
argument when it's passed straight through.
|
| 189 |
+
"""
|
| 190 |
+
if loop:
|
| 191 |
+
# loop already exists, pass it straight through
|
| 192 |
+
yield loop
|
| 193 |
+
else:
|
| 194 |
+
# this shadows loop_context's standard behavior
|
| 195 |
+
loop = setup_test_loop()
|
| 196 |
+
yield loop
|
| 197 |
+
teardown_test_loop(loop, fast=fast)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def pytest_pycollect_makeitem(collector, name, obj): # type: ignore[no-untyped-def]
|
| 201 |
+
"""Fix pytest collecting for coroutines."""
|
| 202 |
+
if collector.funcnamefilter(name) and inspect.iscoroutinefunction(obj):
|
| 203 |
+
return list(collector._genfunctions(name, obj))
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def pytest_pyfunc_call(pyfuncitem): # type: ignore[no-untyped-def]
|
| 207 |
+
"""Run coroutines in an event loop instead of a normal function call."""
|
| 208 |
+
fast = pyfuncitem.config.getoption("--aiohttp-fast")
|
| 209 |
+
if inspect.iscoroutinefunction(pyfuncitem.function):
|
| 210 |
+
existing_loop = (
|
| 211 |
+
pyfuncitem.funcargs.get("proactor_loop")
|
| 212 |
+
or pyfuncitem.funcargs.get("selector_loop")
|
| 213 |
+
or pyfuncitem.funcargs.get("uvloop_loop")
|
| 214 |
+
or pyfuncitem.funcargs.get("loop", None)
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
with _runtime_warning_context():
|
| 218 |
+
with _passthrough_loop_context(existing_loop, fast=fast) as _loop:
|
| 219 |
+
testargs = {
|
| 220 |
+
arg: pyfuncitem.funcargs[arg]
|
| 221 |
+
for arg in pyfuncitem._fixtureinfo.argnames
|
| 222 |
+
}
|
| 223 |
+
_loop.run_until_complete(pyfuncitem.obj(**testargs))
|
| 224 |
+
|
| 225 |
+
return True
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def pytest_generate_tests(metafunc): # type: ignore[no-untyped-def]
|
| 229 |
+
if "loop_factory" not in metafunc.fixturenames:
|
| 230 |
+
return
|
| 231 |
+
|
| 232 |
+
loops = metafunc.config.option.aiohttp_loop
|
| 233 |
+
avail_factories: dict[str, Callable[[], asyncio.AbstractEventLoop]]
|
| 234 |
+
avail_factories = {"pyloop": asyncio.new_event_loop}
|
| 235 |
+
|
| 236 |
+
if uvloop is not None: # pragma: no cover
|
| 237 |
+
avail_factories["uvloop"] = uvloop.new_event_loop
|
| 238 |
+
|
| 239 |
+
if loops == "all":
|
| 240 |
+
loops = "pyloop,uvloop?"
|
| 241 |
+
|
| 242 |
+
factories = {} # type: ignore[var-annotated]
|
| 243 |
+
for name in loops.split(","):
|
| 244 |
+
required = not name.endswith("?")
|
| 245 |
+
name = name.strip(" ?")
|
| 246 |
+
if name not in avail_factories: # pragma: no cover
|
| 247 |
+
if required:
|
| 248 |
+
raise ValueError(
|
| 249 |
+
"Unknown loop '%s', available loops: %s"
|
| 250 |
+
% (name, list(factories.keys()))
|
| 251 |
+
)
|
| 252 |
+
else:
|
| 253 |
+
continue
|
| 254 |
+
factories[name] = avail_factories[name]
|
| 255 |
+
metafunc.parametrize(
|
| 256 |
+
"loop_factory", list(factories.values()), ids=list(factories.keys())
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
@pytest.fixture
|
| 261 |
+
def loop(
|
| 262 |
+
loop_factory: Callable[[], asyncio.AbstractEventLoop],
|
| 263 |
+
fast: bool,
|
| 264 |
+
loop_debug: bool,
|
| 265 |
+
) -> Iterator[asyncio.AbstractEventLoop]:
|
| 266 |
+
"""Return an instance of the event loop."""
|
| 267 |
+
with loop_context(loop_factory, fast=fast) as _loop:
|
| 268 |
+
if loop_debug:
|
| 269 |
+
_loop.set_debug(True) # pragma: no cover
|
| 270 |
+
asyncio.set_event_loop(_loop)
|
| 271 |
+
yield _loop
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
@pytest.fixture
|
| 275 |
+
def proactor_loop() -> Iterator[asyncio.AbstractEventLoop]:
|
| 276 |
+
factory = asyncio.ProactorEventLoop # type: ignore[attr-defined]
|
| 277 |
+
|
| 278 |
+
with loop_context(factory) as _loop:
|
| 279 |
+
asyncio.set_event_loop(_loop)
|
| 280 |
+
yield _loop
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
@pytest.fixture
|
| 284 |
+
def unused_port(aiohttp_unused_port: Callable[[], int]) -> Callable[[], int]:
|
| 285 |
+
warnings.warn(
|
| 286 |
+
"Deprecated, use aiohttp_unused_port fixture instead",
|
| 287 |
+
DeprecationWarning,
|
| 288 |
+
stacklevel=2,
|
| 289 |
+
)
|
| 290 |
+
return aiohttp_unused_port
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
@pytest.fixture
|
| 294 |
+
def aiohttp_unused_port() -> Callable[[], int]:
|
| 295 |
+
"""Return a port that is unused on the current host."""
|
| 296 |
+
return _unused_port
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
@pytest.fixture
|
| 300 |
+
def aiohttp_server(loop: asyncio.AbstractEventLoop) -> Iterator[AiohttpServer]:
|
| 301 |
+
"""Factory to create a TestServer instance, given an app.
|
| 302 |
+
|
| 303 |
+
aiohttp_server(app, **kwargs)
|
| 304 |
+
"""
|
| 305 |
+
servers = []
|
| 306 |
+
|
| 307 |
+
async def go(
|
| 308 |
+
app: Application,
|
| 309 |
+
*,
|
| 310 |
+
host: str = "127.0.0.1",
|
| 311 |
+
port: Optional[int] = None,
|
| 312 |
+
**kwargs: Any,
|
| 313 |
+
) -> TestServer:
|
| 314 |
+
server = TestServer(app, host=host, port=port)
|
| 315 |
+
await server.start_server(loop=loop, **kwargs)
|
| 316 |
+
servers.append(server)
|
| 317 |
+
return server
|
| 318 |
+
|
| 319 |
+
yield go
|
| 320 |
+
|
| 321 |
+
async def finalize() -> None:
|
| 322 |
+
while servers:
|
| 323 |
+
await servers.pop().close()
|
| 324 |
+
|
| 325 |
+
loop.run_until_complete(finalize())
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
@pytest.fixture
|
| 329 |
+
def test_server(aiohttp_server): # type: ignore[no-untyped-def] # pragma: no cover
|
| 330 |
+
warnings.warn(
|
| 331 |
+
"Deprecated, use aiohttp_server fixture instead",
|
| 332 |
+
DeprecationWarning,
|
| 333 |
+
stacklevel=2,
|
| 334 |
+
)
|
| 335 |
+
return aiohttp_server
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
@pytest.fixture
|
| 339 |
+
def aiohttp_raw_server(loop: asyncio.AbstractEventLoop) -> Iterator[AiohttpRawServer]:
|
| 340 |
+
"""Factory to create a RawTestServer instance, given a web handler.
|
| 341 |
+
|
| 342 |
+
aiohttp_raw_server(handler, **kwargs)
|
| 343 |
+
"""
|
| 344 |
+
servers = []
|
| 345 |
+
|
| 346 |
+
async def go(
|
| 347 |
+
handler: _RequestHandler, *, port: Optional[int] = None, **kwargs: Any
|
| 348 |
+
) -> RawTestServer:
|
| 349 |
+
server = RawTestServer(handler, port=port)
|
| 350 |
+
await server.start_server(loop=loop, **kwargs)
|
| 351 |
+
servers.append(server)
|
| 352 |
+
return server
|
| 353 |
+
|
| 354 |
+
yield go
|
| 355 |
+
|
| 356 |
+
async def finalize() -> None:
|
| 357 |
+
while servers:
|
| 358 |
+
await servers.pop().close()
|
| 359 |
+
|
| 360 |
+
loop.run_until_complete(finalize())
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
@pytest.fixture
|
| 364 |
+
def raw_test_server( # type: ignore[no-untyped-def] # pragma: no cover
|
| 365 |
+
aiohttp_raw_server,
|
| 366 |
+
):
|
| 367 |
+
warnings.warn(
|
| 368 |
+
"Deprecated, use aiohttp_raw_server fixture instead",
|
| 369 |
+
DeprecationWarning,
|
| 370 |
+
stacklevel=2,
|
| 371 |
+
)
|
| 372 |
+
return aiohttp_raw_server
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
@pytest.fixture
|
| 376 |
+
def aiohttp_client(loop: asyncio.AbstractEventLoop) -> Iterator[AiohttpClient]:
|
| 377 |
+
"""Factory to create a TestClient instance.
|
| 378 |
+
|
| 379 |
+
aiohttp_client(app, **kwargs)
|
| 380 |
+
aiohttp_client(server, **kwargs)
|
| 381 |
+
aiohttp_client(raw_server, **kwargs)
|
| 382 |
+
"""
|
| 383 |
+
clients = []
|
| 384 |
+
|
| 385 |
+
@overload
|
| 386 |
+
async def go(
|
| 387 |
+
__param: Application,
|
| 388 |
+
*,
|
| 389 |
+
server_kwargs: Optional[Dict[str, Any]] = None,
|
| 390 |
+
**kwargs: Any,
|
| 391 |
+
) -> TestClient[Request, Application]: ...
|
| 392 |
+
|
| 393 |
+
@overload
|
| 394 |
+
async def go(
|
| 395 |
+
__param: BaseTestServer,
|
| 396 |
+
*,
|
| 397 |
+
server_kwargs: Optional[Dict[str, Any]] = None,
|
| 398 |
+
**kwargs: Any,
|
| 399 |
+
) -> TestClient[BaseRequest, None]: ...
|
| 400 |
+
|
| 401 |
+
async def go(
|
| 402 |
+
__param: Union[Application, BaseTestServer],
|
| 403 |
+
*args: Any,
|
| 404 |
+
server_kwargs: Optional[Dict[str, Any]] = None,
|
| 405 |
+
**kwargs: Any,
|
| 406 |
+
) -> TestClient[Any, Any]:
|
| 407 |
+
if isinstance(__param, Callable) and not isinstance( # type: ignore[arg-type]
|
| 408 |
+
__param, (Application, BaseTestServer)
|
| 409 |
+
):
|
| 410 |
+
__param = __param(loop, *args, **kwargs)
|
| 411 |
+
kwargs = {}
|
| 412 |
+
else:
|
| 413 |
+
assert not args, "args should be empty"
|
| 414 |
+
|
| 415 |
+
if isinstance(__param, Application):
|
| 416 |
+
server_kwargs = server_kwargs or {}
|
| 417 |
+
server = TestServer(__param, loop=loop, **server_kwargs)
|
| 418 |
+
client = TestClient(server, loop=loop, **kwargs)
|
| 419 |
+
elif isinstance(__param, BaseTestServer):
|
| 420 |
+
client = TestClient(__param, loop=loop, **kwargs)
|
| 421 |
+
else:
|
| 422 |
+
raise ValueError("Unknown argument type: %r" % type(__param))
|
| 423 |
+
|
| 424 |
+
await client.start_server()
|
| 425 |
+
clients.append(client)
|
| 426 |
+
return client
|
| 427 |
+
|
| 428 |
+
yield go
|
| 429 |
+
|
| 430 |
+
async def finalize() -> None:
|
| 431 |
+
while clients:
|
| 432 |
+
await clients.pop().close()
|
| 433 |
+
|
| 434 |
+
loop.run_until_complete(finalize())
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
@pytest.fixture
|
| 438 |
+
def test_client(aiohttp_client): # type: ignore[no-untyped-def] # pragma: no cover
|
| 439 |
+
warnings.warn(
|
| 440 |
+
"Deprecated, use aiohttp_client fixture instead",
|
| 441 |
+
DeprecationWarning,
|
| 442 |
+
stacklevel=2,
|
| 443 |
+
)
|
| 444 |
+
return aiohttp_client
|
aiohttp/resolver.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import socket
|
| 3 |
+
import weakref
|
| 4 |
+
from typing import Any, Dict, Final, List, Optional, Tuple, Type, Union
|
| 5 |
+
|
| 6 |
+
from .abc import AbstractResolver, ResolveResult
|
| 7 |
+
|
| 8 |
+
__all__ = ("ThreadedResolver", "AsyncResolver", "DefaultResolver")
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
try:
|
| 12 |
+
import aiodns
|
| 13 |
+
|
| 14 |
+
aiodns_default = hasattr(aiodns.DNSResolver, "getaddrinfo")
|
| 15 |
+
except ImportError: # pragma: no cover
|
| 16 |
+
aiodns = None # type: ignore[assignment]
|
| 17 |
+
aiodns_default = False
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
_NUMERIC_SOCKET_FLAGS = socket.AI_NUMERICHOST | socket.AI_NUMERICSERV
|
| 21 |
+
_NAME_SOCKET_FLAGS = socket.NI_NUMERICHOST | socket.NI_NUMERICSERV
|
| 22 |
+
_AI_ADDRCONFIG = socket.AI_ADDRCONFIG
|
| 23 |
+
if hasattr(socket, "AI_MASK"):
|
| 24 |
+
_AI_ADDRCONFIG &= socket.AI_MASK
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class ThreadedResolver(AbstractResolver):
|
| 28 |
+
"""Threaded resolver.
|
| 29 |
+
|
| 30 |
+
Uses an Executor for synchronous getaddrinfo() calls.
|
| 31 |
+
concurrent.futures.ThreadPoolExecutor is used by default.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
def __init__(self, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
|
| 35 |
+
self._loop = loop or asyncio.get_running_loop()
|
| 36 |
+
|
| 37 |
+
async def resolve(
|
| 38 |
+
self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET
|
| 39 |
+
) -> List[ResolveResult]:
|
| 40 |
+
infos = await self._loop.getaddrinfo(
|
| 41 |
+
host,
|
| 42 |
+
port,
|
| 43 |
+
type=socket.SOCK_STREAM,
|
| 44 |
+
family=family,
|
| 45 |
+
flags=_AI_ADDRCONFIG,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
hosts: List[ResolveResult] = []
|
| 49 |
+
for family, _, proto, _, address in infos:
|
| 50 |
+
if family == socket.AF_INET6:
|
| 51 |
+
if len(address) < 3:
|
| 52 |
+
# IPv6 is not supported by Python build,
|
| 53 |
+
# or IPv6 is not enabled in the host
|
| 54 |
+
continue
|
| 55 |
+
if address[3]:
|
| 56 |
+
# This is essential for link-local IPv6 addresses.
|
| 57 |
+
# LL IPv6 is a VERY rare case. Strictly speaking, we should use
|
| 58 |
+
# getnameinfo() unconditionally, but performance makes sense.
|
| 59 |
+
resolved_host, _port = await self._loop.getnameinfo(
|
| 60 |
+
address, _NAME_SOCKET_FLAGS
|
| 61 |
+
)
|
| 62 |
+
port = int(_port)
|
| 63 |
+
else:
|
| 64 |
+
resolved_host, port = address[:2]
|
| 65 |
+
else: # IPv4
|
| 66 |
+
assert family == socket.AF_INET
|
| 67 |
+
resolved_host, port = address # type: ignore[misc]
|
| 68 |
+
hosts.append(
|
| 69 |
+
ResolveResult(
|
| 70 |
+
hostname=host,
|
| 71 |
+
host=resolved_host,
|
| 72 |
+
port=port,
|
| 73 |
+
family=family,
|
| 74 |
+
proto=proto,
|
| 75 |
+
flags=_NUMERIC_SOCKET_FLAGS,
|
| 76 |
+
)
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
return hosts
|
| 80 |
+
|
| 81 |
+
async def close(self) -> None:
|
| 82 |
+
pass
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class AsyncResolver(AbstractResolver):
|
| 86 |
+
"""Use the `aiodns` package to make asynchronous DNS lookups"""
|
| 87 |
+
|
| 88 |
+
def __init__(
|
| 89 |
+
self,
|
| 90 |
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
| 91 |
+
*args: Any,
|
| 92 |
+
**kwargs: Any,
|
| 93 |
+
) -> None:
|
| 94 |
+
if aiodns is None:
|
| 95 |
+
raise RuntimeError("Resolver requires aiodns library")
|
| 96 |
+
|
| 97 |
+
self._loop = loop or asyncio.get_running_loop()
|
| 98 |
+
self._manager: Optional[_DNSResolverManager] = None
|
| 99 |
+
# If custom args are provided, create a dedicated resolver instance
|
| 100 |
+
# This means each AsyncResolver with custom args gets its own
|
| 101 |
+
# aiodns.DNSResolver instance
|
| 102 |
+
if args or kwargs:
|
| 103 |
+
self._resolver = aiodns.DNSResolver(*args, **kwargs)
|
| 104 |
+
return
|
| 105 |
+
# Use the shared resolver from the manager for default arguments
|
| 106 |
+
self._manager = _DNSResolverManager()
|
| 107 |
+
self._resolver = self._manager.get_resolver(self, self._loop)
|
| 108 |
+
|
| 109 |
+
if not hasattr(self._resolver, "gethostbyname"):
|
| 110 |
+
# aiodns 1.1 is not available, fallback to DNSResolver.query
|
| 111 |
+
self.resolve = self._resolve_with_query # type: ignore
|
| 112 |
+
|
| 113 |
+
async def resolve(
|
| 114 |
+
self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET
|
| 115 |
+
) -> List[ResolveResult]:
|
| 116 |
+
try:
|
| 117 |
+
resp = await self._resolver.getaddrinfo(
|
| 118 |
+
host,
|
| 119 |
+
port=port,
|
| 120 |
+
type=socket.SOCK_STREAM,
|
| 121 |
+
family=family,
|
| 122 |
+
flags=_AI_ADDRCONFIG,
|
| 123 |
+
)
|
| 124 |
+
except aiodns.error.DNSError as exc:
|
| 125 |
+
msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed"
|
| 126 |
+
raise OSError(None, msg) from exc
|
| 127 |
+
hosts: List[ResolveResult] = []
|
| 128 |
+
for node in resp.nodes:
|
| 129 |
+
address: Union[Tuple[bytes, int], Tuple[bytes, int, int, int]] = node.addr
|
| 130 |
+
family = node.family
|
| 131 |
+
if family == socket.AF_INET6:
|
| 132 |
+
if len(address) > 3 and address[3]:
|
| 133 |
+
# This is essential for link-local IPv6 addresses.
|
| 134 |
+
# LL IPv6 is a VERY rare case. Strictly speaking, we should use
|
| 135 |
+
# getnameinfo() unconditionally, but performance makes sense.
|
| 136 |
+
result = await self._resolver.getnameinfo(
|
| 137 |
+
(address[0].decode("ascii"), *address[1:]),
|
| 138 |
+
_NAME_SOCKET_FLAGS,
|
| 139 |
+
)
|
| 140 |
+
resolved_host = result.node
|
| 141 |
+
else:
|
| 142 |
+
resolved_host = address[0].decode("ascii")
|
| 143 |
+
port = address[1]
|
| 144 |
+
else: # IPv4
|
| 145 |
+
assert family == socket.AF_INET
|
| 146 |
+
resolved_host = address[0].decode("ascii")
|
| 147 |
+
port = address[1]
|
| 148 |
+
hosts.append(
|
| 149 |
+
ResolveResult(
|
| 150 |
+
hostname=host,
|
| 151 |
+
host=resolved_host,
|
| 152 |
+
port=port,
|
| 153 |
+
family=family,
|
| 154 |
+
proto=0,
|
| 155 |
+
flags=_NUMERIC_SOCKET_FLAGS,
|
| 156 |
+
)
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
if not hosts:
|
| 160 |
+
raise OSError(None, "DNS lookup failed")
|
| 161 |
+
|
| 162 |
+
return hosts
|
| 163 |
+
|
| 164 |
+
async def _resolve_with_query(
|
| 165 |
+
self, host: str, port: int = 0, family: int = socket.AF_INET
|
| 166 |
+
) -> List[Dict[str, Any]]:
|
| 167 |
+
qtype: Final = "AAAA" if family == socket.AF_INET6 else "A"
|
| 168 |
+
|
| 169 |
+
try:
|
| 170 |
+
resp = await self._resolver.query(host, qtype)
|
| 171 |
+
except aiodns.error.DNSError as exc:
|
| 172 |
+
msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed"
|
| 173 |
+
raise OSError(None, msg) from exc
|
| 174 |
+
|
| 175 |
+
hosts = []
|
| 176 |
+
for rr in resp:
|
| 177 |
+
hosts.append(
|
| 178 |
+
{
|
| 179 |
+
"hostname": host,
|
| 180 |
+
"host": rr.host,
|
| 181 |
+
"port": port,
|
| 182 |
+
"family": family,
|
| 183 |
+
"proto": 0,
|
| 184 |
+
"flags": socket.AI_NUMERICHOST,
|
| 185 |
+
}
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
if not hosts:
|
| 189 |
+
raise OSError(None, "DNS lookup failed")
|
| 190 |
+
|
| 191 |
+
return hosts
|
| 192 |
+
|
| 193 |
+
async def close(self) -> None:
|
| 194 |
+
if self._manager:
|
| 195 |
+
# Release the resolver from the manager if using the shared resolver
|
| 196 |
+
self._manager.release_resolver(self, self._loop)
|
| 197 |
+
self._manager = None # Clear reference to manager
|
| 198 |
+
self._resolver = None # type: ignore[assignment] # Clear reference to resolver
|
| 199 |
+
return
|
| 200 |
+
# Otherwise cancel our dedicated resolver
|
| 201 |
+
if self._resolver is not None:
|
| 202 |
+
self._resolver.cancel()
|
| 203 |
+
self._resolver = None # type: ignore[assignment] # Clear reference
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
class _DNSResolverManager:
|
| 207 |
+
"""Manager for aiodns.DNSResolver objects.
|
| 208 |
+
|
| 209 |
+
This class manages shared aiodns.DNSResolver instances
|
| 210 |
+
with no custom arguments across different event loops.
|
| 211 |
+
"""
|
| 212 |
+
|
| 213 |
+
_instance: Optional["_DNSResolverManager"] = None
|
| 214 |
+
|
| 215 |
+
def __new__(cls) -> "_DNSResolverManager":
|
| 216 |
+
if cls._instance is None:
|
| 217 |
+
cls._instance = super().__new__(cls)
|
| 218 |
+
cls._instance._init()
|
| 219 |
+
return cls._instance
|
| 220 |
+
|
| 221 |
+
def _init(self) -> None:
|
| 222 |
+
# Use WeakKeyDictionary to allow event loops to be garbage collected
|
| 223 |
+
self._loop_data: weakref.WeakKeyDictionary[
|
| 224 |
+
asyncio.AbstractEventLoop,
|
| 225 |
+
tuple["aiodns.DNSResolver", weakref.WeakSet["AsyncResolver"]],
|
| 226 |
+
] = weakref.WeakKeyDictionary()
|
| 227 |
+
|
| 228 |
+
def get_resolver(
|
| 229 |
+
self, client: "AsyncResolver", loop: asyncio.AbstractEventLoop
|
| 230 |
+
) -> "aiodns.DNSResolver":
|
| 231 |
+
"""Get or create the shared aiodns.DNSResolver instance for a specific event loop.
|
| 232 |
+
|
| 233 |
+
Args:
|
| 234 |
+
client: The AsyncResolver instance requesting the resolver.
|
| 235 |
+
This is required to track resolver usage.
|
| 236 |
+
loop: The event loop to use for the resolver.
|
| 237 |
+
"""
|
| 238 |
+
# Create a new resolver and client set for this loop if it doesn't exist
|
| 239 |
+
if loop not in self._loop_data:
|
| 240 |
+
resolver = aiodns.DNSResolver(loop=loop)
|
| 241 |
+
client_set: weakref.WeakSet["AsyncResolver"] = weakref.WeakSet()
|
| 242 |
+
self._loop_data[loop] = (resolver, client_set)
|
| 243 |
+
else:
|
| 244 |
+
# Get the existing resolver and client set
|
| 245 |
+
resolver, client_set = self._loop_data[loop]
|
| 246 |
+
|
| 247 |
+
# Register this client with the loop
|
| 248 |
+
client_set.add(client)
|
| 249 |
+
return resolver
|
| 250 |
+
|
| 251 |
+
def release_resolver(
|
| 252 |
+
self, client: "AsyncResolver", loop: asyncio.AbstractEventLoop
|
| 253 |
+
) -> None:
|
| 254 |
+
"""Release the resolver for an AsyncResolver client when it's closed.
|
| 255 |
+
|
| 256 |
+
Args:
|
| 257 |
+
client: The AsyncResolver instance to release.
|
| 258 |
+
loop: The event loop the resolver was using.
|
| 259 |
+
"""
|
| 260 |
+
# Remove client from its loop's tracking
|
| 261 |
+
current_loop_data = self._loop_data.get(loop)
|
| 262 |
+
if current_loop_data is None:
|
| 263 |
+
return
|
| 264 |
+
resolver, client_set = current_loop_data
|
| 265 |
+
client_set.discard(client)
|
| 266 |
+
# If no more clients for this loop, cancel and remove its resolver
|
| 267 |
+
if not client_set:
|
| 268 |
+
if resolver is not None:
|
| 269 |
+
resolver.cancel()
|
| 270 |
+
del self._loop_data[loop]
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
_DefaultType = Type[Union[AsyncResolver, ThreadedResolver]]
|
| 274 |
+
DefaultResolver: _DefaultType = AsyncResolver if aiodns_default else ThreadedResolver
|
aiohttp/tcp_helpers.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Helper methods to tune a TCP connection"""
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import socket
|
| 5 |
+
from contextlib import suppress
|
| 6 |
+
from typing import Optional # noqa
|
| 7 |
+
|
| 8 |
+
__all__ = ("tcp_keepalive", "tcp_nodelay")
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
if hasattr(socket, "SO_KEEPALIVE"):
|
| 12 |
+
|
| 13 |
+
def tcp_keepalive(transport: asyncio.Transport) -> None:
|
| 14 |
+
sock = transport.get_extra_info("socket")
|
| 15 |
+
if sock is not None:
|
| 16 |
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
| 17 |
+
|
| 18 |
+
else:
|
| 19 |
+
|
| 20 |
+
def tcp_keepalive(transport: asyncio.Transport) -> None: # pragma: no cover
|
| 21 |
+
pass
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def tcp_nodelay(transport: asyncio.Transport, value: bool) -> None:
|
| 25 |
+
sock = transport.get_extra_info("socket")
|
| 26 |
+
|
| 27 |
+
if sock is None:
|
| 28 |
+
return
|
| 29 |
+
|
| 30 |
+
if sock.family not in (socket.AF_INET, socket.AF_INET6):
|
| 31 |
+
return
|
| 32 |
+
|
| 33 |
+
value = bool(value)
|
| 34 |
+
|
| 35 |
+
# socket may be closed already, on windows OSError get raised
|
| 36 |
+
with suppress(OSError):
|
| 37 |
+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, value)
|
aiohttp/typedefs.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from typing import (
|
| 4 |
+
TYPE_CHECKING,
|
| 5 |
+
Any,
|
| 6 |
+
Awaitable,
|
| 7 |
+
Callable,
|
| 8 |
+
Iterable,
|
| 9 |
+
Mapping,
|
| 10 |
+
Protocol,
|
| 11 |
+
Tuple,
|
| 12 |
+
Union,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy, istr
|
| 16 |
+
from yarl import URL, Query as _Query
|
| 17 |
+
|
| 18 |
+
Query = _Query
|
| 19 |
+
|
| 20 |
+
DEFAULT_JSON_ENCODER = json.dumps
|
| 21 |
+
DEFAULT_JSON_DECODER = json.loads
|
| 22 |
+
|
| 23 |
+
if TYPE_CHECKING:
|
| 24 |
+
_CIMultiDict = CIMultiDict[str]
|
| 25 |
+
_CIMultiDictProxy = CIMultiDictProxy[str]
|
| 26 |
+
_MultiDict = MultiDict[str]
|
| 27 |
+
_MultiDictProxy = MultiDictProxy[str]
|
| 28 |
+
from http.cookies import BaseCookie, Morsel
|
| 29 |
+
|
| 30 |
+
from .web import Request, StreamResponse
|
| 31 |
+
else:
|
| 32 |
+
_CIMultiDict = CIMultiDict
|
| 33 |
+
_CIMultiDictProxy = CIMultiDictProxy
|
| 34 |
+
_MultiDict = MultiDict
|
| 35 |
+
_MultiDictProxy = MultiDictProxy
|
| 36 |
+
|
| 37 |
+
Byteish = Union[bytes, bytearray, memoryview]
|
| 38 |
+
JSONEncoder = Callable[[Any], str]
|
| 39 |
+
JSONDecoder = Callable[[str], Any]
|
| 40 |
+
LooseHeaders = Union[
|
| 41 |
+
Mapping[str, str],
|
| 42 |
+
Mapping[istr, str],
|
| 43 |
+
_CIMultiDict,
|
| 44 |
+
_CIMultiDictProxy,
|
| 45 |
+
Iterable[Tuple[Union[str, istr], str]],
|
| 46 |
+
]
|
| 47 |
+
RawHeaders = Tuple[Tuple[bytes, bytes], ...]
|
| 48 |
+
StrOrURL = Union[str, URL]
|
| 49 |
+
|
| 50 |
+
LooseCookiesMappings = Mapping[str, Union[str, "BaseCookie[str]", "Morsel[Any]"]]
|
| 51 |
+
LooseCookiesIterables = Iterable[
|
| 52 |
+
Tuple[str, Union[str, "BaseCookie[str]", "Morsel[Any]"]]
|
| 53 |
+
]
|
| 54 |
+
LooseCookies = Union[
|
| 55 |
+
LooseCookiesMappings,
|
| 56 |
+
LooseCookiesIterables,
|
| 57 |
+
"BaseCookie[str]",
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
Handler = Callable[["Request"], Awaitable["StreamResponse"]]
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class Middleware(Protocol):
|
| 64 |
+
def __call__(
|
| 65 |
+
self, request: "Request", handler: Handler
|
| 66 |
+
) -> Awaitable["StreamResponse"]: ...
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
PathLike = Union[str, "os.PathLike[str]"]
|
aiohttp/web_fileresponse.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import io
|
| 3 |
+
import os
|
| 4 |
+
import pathlib
|
| 5 |
+
import sys
|
| 6 |
+
from contextlib import suppress
|
| 7 |
+
from enum import Enum, auto
|
| 8 |
+
from mimetypes import MimeTypes
|
| 9 |
+
from stat import S_ISREG
|
| 10 |
+
from types import MappingProxyType
|
| 11 |
+
from typing import ( # noqa
|
| 12 |
+
IO,
|
| 13 |
+
TYPE_CHECKING,
|
| 14 |
+
Any,
|
| 15 |
+
Awaitable,
|
| 16 |
+
Callable,
|
| 17 |
+
Final,
|
| 18 |
+
Iterator,
|
| 19 |
+
List,
|
| 20 |
+
Optional,
|
| 21 |
+
Set,
|
| 22 |
+
Tuple,
|
| 23 |
+
Union,
|
| 24 |
+
cast,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
from . import hdrs
|
| 28 |
+
from .abc import AbstractStreamWriter
|
| 29 |
+
from .helpers import ETAG_ANY, ETag, must_be_empty_body
|
| 30 |
+
from .typedefs import LooseHeaders, PathLike
|
| 31 |
+
from .web_exceptions import (
|
| 32 |
+
HTTPForbidden,
|
| 33 |
+
HTTPNotFound,
|
| 34 |
+
HTTPNotModified,
|
| 35 |
+
HTTPPartialContent,
|
| 36 |
+
HTTPPreconditionFailed,
|
| 37 |
+
HTTPRequestRangeNotSatisfiable,
|
| 38 |
+
)
|
| 39 |
+
from .web_response import StreamResponse
|
| 40 |
+
|
| 41 |
+
__all__ = ("FileResponse",)
|
| 42 |
+
|
| 43 |
+
if TYPE_CHECKING:
|
| 44 |
+
from .web_request import BaseRequest
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
_T_OnChunkSent = Optional[Callable[[bytes], Awaitable[None]]]
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
NOSENDFILE: Final[bool] = bool(os.environ.get("AIOHTTP_NOSENDFILE"))
|
| 51 |
+
|
| 52 |
+
CONTENT_TYPES: Final[MimeTypes] = MimeTypes()
|
| 53 |
+
|
| 54 |
+
# File extension to IANA encodings map that will be checked in the order defined.
|
| 55 |
+
ENCODING_EXTENSIONS = MappingProxyType(
|
| 56 |
+
{ext: CONTENT_TYPES.encodings_map[ext] for ext in (".br", ".gz")}
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
FALLBACK_CONTENT_TYPE = "application/octet-stream"
|
| 60 |
+
|
| 61 |
+
# Provide additional MIME type/extension pairs to be recognized.
|
| 62 |
+
# https://en.wikipedia.org/wiki/List_of_archive_formats#Compression_only
|
| 63 |
+
ADDITIONAL_CONTENT_TYPES = MappingProxyType(
|
| 64 |
+
{
|
| 65 |
+
"application/gzip": ".gz",
|
| 66 |
+
"application/x-brotli": ".br",
|
| 67 |
+
"application/x-bzip2": ".bz2",
|
| 68 |
+
"application/x-compress": ".Z",
|
| 69 |
+
"application/x-xz": ".xz",
|
| 70 |
+
}
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class _FileResponseResult(Enum):
|
| 75 |
+
"""The result of the file response."""
|
| 76 |
+
|
| 77 |
+
SEND_FILE = auto() # Ie a regular file to send
|
| 78 |
+
NOT_ACCEPTABLE = auto() # Ie a socket, or non-regular file
|
| 79 |
+
PRE_CONDITION_FAILED = auto() # Ie If-Match or If-None-Match failed
|
| 80 |
+
NOT_MODIFIED = auto() # 304 Not Modified
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# Add custom pairs and clear the encodings map so guess_type ignores them.
|
| 84 |
+
CONTENT_TYPES.encodings_map.clear()
|
| 85 |
+
for content_type, extension in ADDITIONAL_CONTENT_TYPES.items():
|
| 86 |
+
CONTENT_TYPES.add_type(content_type, extension)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
_CLOSE_FUTURES: Set[asyncio.Future[None]] = set()
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class FileResponse(StreamResponse):
|
| 93 |
+
"""A response object can be used to send files."""
|
| 94 |
+
|
| 95 |
+
def __init__(
|
| 96 |
+
self,
|
| 97 |
+
path: PathLike,
|
| 98 |
+
chunk_size: int = 256 * 1024,
|
| 99 |
+
status: int = 200,
|
| 100 |
+
reason: Optional[str] = None,
|
| 101 |
+
headers: Optional[LooseHeaders] = None,
|
| 102 |
+
) -> None:
|
| 103 |
+
super().__init__(status=status, reason=reason, headers=headers)
|
| 104 |
+
|
| 105 |
+
self._path = pathlib.Path(path)
|
| 106 |
+
self._chunk_size = chunk_size
|
| 107 |
+
|
| 108 |
+
def _seek_and_read(self, fobj: IO[Any], offset: int, chunk_size: int) -> bytes:
|
| 109 |
+
fobj.seek(offset)
|
| 110 |
+
return fobj.read(chunk_size) # type: ignore[no-any-return]
|
| 111 |
+
|
| 112 |
+
async def _sendfile_fallback(
|
| 113 |
+
self, writer: AbstractStreamWriter, fobj: IO[Any], offset: int, count: int
|
| 114 |
+
) -> AbstractStreamWriter:
|
| 115 |
+
# To keep memory usage low,fobj is transferred in chunks
|
| 116 |
+
# controlled by the constructor's chunk_size argument.
|
| 117 |
+
|
| 118 |
+
chunk_size = self._chunk_size
|
| 119 |
+
loop = asyncio.get_event_loop()
|
| 120 |
+
chunk = await loop.run_in_executor(
|
| 121 |
+
None, self._seek_and_read, fobj, offset, chunk_size
|
| 122 |
+
)
|
| 123 |
+
while chunk:
|
| 124 |
+
await writer.write(chunk)
|
| 125 |
+
count = count - chunk_size
|
| 126 |
+
if count <= 0:
|
| 127 |
+
break
|
| 128 |
+
chunk = await loop.run_in_executor(None, fobj.read, min(chunk_size, count))
|
| 129 |
+
|
| 130 |
+
await writer.drain()
|
| 131 |
+
return writer
|
| 132 |
+
|
| 133 |
+
async def _sendfile(
|
| 134 |
+
self, request: "BaseRequest", fobj: IO[Any], offset: int, count: int
|
| 135 |
+
) -> AbstractStreamWriter:
|
| 136 |
+
writer = await super().prepare(request)
|
| 137 |
+
assert writer is not None
|
| 138 |
+
|
| 139 |
+
if NOSENDFILE or self.compression:
|
| 140 |
+
return await self._sendfile_fallback(writer, fobj, offset, count)
|
| 141 |
+
|
| 142 |
+
loop = request._loop
|
| 143 |
+
transport = request.transport
|
| 144 |
+
assert transport is not None
|
| 145 |
+
|
| 146 |
+
try:
|
| 147 |
+
await loop.sendfile(transport, fobj, offset, count)
|
| 148 |
+
except NotImplementedError:
|
| 149 |
+
return await self._sendfile_fallback(writer, fobj, offset, count)
|
| 150 |
+
|
| 151 |
+
await super().write_eof()
|
| 152 |
+
return writer
|
| 153 |
+
|
| 154 |
+
@staticmethod
|
| 155 |
+
def _etag_match(etag_value: str, etags: Tuple[ETag, ...], *, weak: bool) -> bool:
|
| 156 |
+
if len(etags) == 1 and etags[0].value == ETAG_ANY:
|
| 157 |
+
return True
|
| 158 |
+
return any(
|
| 159 |
+
etag.value == etag_value for etag in etags if weak or not etag.is_weak
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
async def _not_modified(
|
| 163 |
+
self, request: "BaseRequest", etag_value: str, last_modified: float
|
| 164 |
+
) -> Optional[AbstractStreamWriter]:
|
| 165 |
+
self.set_status(HTTPNotModified.status_code)
|
| 166 |
+
self._length_check = False
|
| 167 |
+
self.etag = etag_value
|
| 168 |
+
self.last_modified = last_modified
|
| 169 |
+
# Delete any Content-Length headers provided by user. HTTP 304
|
| 170 |
+
# should always have empty response body
|
| 171 |
+
return await super().prepare(request)
|
| 172 |
+
|
| 173 |
+
async def _precondition_failed(
|
| 174 |
+
self, request: "BaseRequest"
|
| 175 |
+
) -> Optional[AbstractStreamWriter]:
|
| 176 |
+
self.set_status(HTTPPreconditionFailed.status_code)
|
| 177 |
+
self.content_length = 0
|
| 178 |
+
return await super().prepare(request)
|
| 179 |
+
|
| 180 |
+
def _make_response(
|
| 181 |
+
self, request: "BaseRequest", accept_encoding: str
|
| 182 |
+
) -> Tuple[
|
| 183 |
+
_FileResponseResult, Optional[io.BufferedReader], os.stat_result, Optional[str]
|
| 184 |
+
]:
|
| 185 |
+
"""Return the response result, io object, stat result, and encoding.
|
| 186 |
+
|
| 187 |
+
If an uncompressed file is returned, the encoding is set to
|
| 188 |
+
:py:data:`None`.
|
| 189 |
+
|
| 190 |
+
This method should be called from a thread executor
|
| 191 |
+
since it calls os.stat which may block.
|
| 192 |
+
"""
|
| 193 |
+
file_path, st, file_encoding = self._get_file_path_stat_encoding(
|
| 194 |
+
accept_encoding
|
| 195 |
+
)
|
| 196 |
+
if not file_path:
|
| 197 |
+
return _FileResponseResult.NOT_ACCEPTABLE, None, st, None
|
| 198 |
+
|
| 199 |
+
etag_value = f"{st.st_mtime_ns:x}-{st.st_size:x}"
|
| 200 |
+
|
| 201 |
+
# https://www.rfc-editor.org/rfc/rfc9110#section-13.1.1-2
|
| 202 |
+
if (ifmatch := request.if_match) is not None and not self._etag_match(
|
| 203 |
+
etag_value, ifmatch, weak=False
|
| 204 |
+
):
|
| 205 |
+
return _FileResponseResult.PRE_CONDITION_FAILED, None, st, file_encoding
|
| 206 |
+
|
| 207 |
+
if (
|
| 208 |
+
(unmodsince := request.if_unmodified_since) is not None
|
| 209 |
+
and ifmatch is None
|
| 210 |
+
and st.st_mtime > unmodsince.timestamp()
|
| 211 |
+
):
|
| 212 |
+
return _FileResponseResult.PRE_CONDITION_FAILED, None, st, file_encoding
|
| 213 |
+
|
| 214 |
+
# https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2-2
|
| 215 |
+
if (ifnonematch := request.if_none_match) is not None and self._etag_match(
|
| 216 |
+
etag_value, ifnonematch, weak=True
|
| 217 |
+
):
|
| 218 |
+
return _FileResponseResult.NOT_MODIFIED, None, st, file_encoding
|
| 219 |
+
|
| 220 |
+
if (
|
| 221 |
+
(modsince := request.if_modified_since) is not None
|
| 222 |
+
and ifnonematch is None
|
| 223 |
+
and st.st_mtime <= modsince.timestamp()
|
| 224 |
+
):
|
| 225 |
+
return _FileResponseResult.NOT_MODIFIED, None, st, file_encoding
|
| 226 |
+
|
| 227 |
+
fobj = file_path.open("rb")
|
| 228 |
+
with suppress(OSError):
|
| 229 |
+
# fstat() may not be available on all platforms
|
| 230 |
+
# Once we open the file, we want the fstat() to ensure
|
| 231 |
+
# the file has not changed between the first stat()
|
| 232 |
+
# and the open().
|
| 233 |
+
st = os.stat(fobj.fileno())
|
| 234 |
+
return _FileResponseResult.SEND_FILE, fobj, st, file_encoding
|
| 235 |
+
|
| 236 |
+
def _get_file_path_stat_encoding(
|
| 237 |
+
self, accept_encoding: str
|
| 238 |
+
) -> Tuple[Optional[pathlib.Path], os.stat_result, Optional[str]]:
|
| 239 |
+
file_path = self._path
|
| 240 |
+
for file_extension, file_encoding in ENCODING_EXTENSIONS.items():
|
| 241 |
+
if file_encoding not in accept_encoding:
|
| 242 |
+
continue
|
| 243 |
+
|
| 244 |
+
compressed_path = file_path.with_suffix(file_path.suffix + file_extension)
|
| 245 |
+
with suppress(OSError):
|
| 246 |
+
# Do not follow symlinks and ignore any non-regular files.
|
| 247 |
+
st = compressed_path.lstat()
|
| 248 |
+
if S_ISREG(st.st_mode):
|
| 249 |
+
return compressed_path, st, file_encoding
|
| 250 |
+
|
| 251 |
+
# Fallback to the uncompressed file
|
| 252 |
+
st = file_path.stat()
|
| 253 |
+
return file_path if S_ISREG(st.st_mode) else None, st, None
|
| 254 |
+
|
| 255 |
+
async def prepare(self, request: "BaseRequest") -> Optional[AbstractStreamWriter]:
|
| 256 |
+
loop = asyncio.get_running_loop()
|
| 257 |
+
# Encoding comparisons should be case-insensitive
|
| 258 |
+
# https://www.rfc-editor.org/rfc/rfc9110#section-8.4.1
|
| 259 |
+
accept_encoding = request.headers.get(hdrs.ACCEPT_ENCODING, "").lower()
|
| 260 |
+
try:
|
| 261 |
+
response_result, fobj, st, file_encoding = await loop.run_in_executor(
|
| 262 |
+
None, self._make_response, request, accept_encoding
|
| 263 |
+
)
|
| 264 |
+
except PermissionError:
|
| 265 |
+
self.set_status(HTTPForbidden.status_code)
|
| 266 |
+
return await super().prepare(request)
|
| 267 |
+
except OSError:
|
| 268 |
+
# Most likely to be FileNotFoundError or OSError for circular
|
| 269 |
+
# symlinks in python >= 3.13, so respond with 404.
|
| 270 |
+
self.set_status(HTTPNotFound.status_code)
|
| 271 |
+
return await super().prepare(request)
|
| 272 |
+
|
| 273 |
+
# Forbid special files like sockets, pipes, devices, etc.
|
| 274 |
+
if response_result is _FileResponseResult.NOT_ACCEPTABLE:
|
| 275 |
+
self.set_status(HTTPForbidden.status_code)
|
| 276 |
+
return await super().prepare(request)
|
| 277 |
+
|
| 278 |
+
if response_result is _FileResponseResult.PRE_CONDITION_FAILED:
|
| 279 |
+
return await self._precondition_failed(request)
|
| 280 |
+
|
| 281 |
+
if response_result is _FileResponseResult.NOT_MODIFIED:
|
| 282 |
+
etag_value = f"{st.st_mtime_ns:x}-{st.st_size:x}"
|
| 283 |
+
last_modified = st.st_mtime
|
| 284 |
+
return await self._not_modified(request, etag_value, last_modified)
|
| 285 |
+
|
| 286 |
+
assert fobj is not None
|
| 287 |
+
try:
|
| 288 |
+
return await self._prepare_open_file(request, fobj, st, file_encoding)
|
| 289 |
+
finally:
|
| 290 |
+
# We do not await here because we do not want to wait
|
| 291 |
+
# for the executor to finish before returning the response
|
| 292 |
+
# so the connection can begin servicing another request
|
| 293 |
+
# as soon as possible.
|
| 294 |
+
close_future = loop.run_in_executor(None, fobj.close)
|
| 295 |
+
# Hold a strong reference to the future to prevent it from being
|
| 296 |
+
# garbage collected before it completes.
|
| 297 |
+
_CLOSE_FUTURES.add(close_future)
|
| 298 |
+
close_future.add_done_callback(_CLOSE_FUTURES.remove)
|
| 299 |
+
|
| 300 |
+
async def _prepare_open_file(
|
| 301 |
+
self,
|
| 302 |
+
request: "BaseRequest",
|
| 303 |
+
fobj: io.BufferedReader,
|
| 304 |
+
st: os.stat_result,
|
| 305 |
+
file_encoding: Optional[str],
|
| 306 |
+
) -> Optional[AbstractStreamWriter]:
|
| 307 |
+
status = self._status
|
| 308 |
+
file_size: int = st.st_size
|
| 309 |
+
file_mtime: float = st.st_mtime
|
| 310 |
+
count: int = file_size
|
| 311 |
+
start: Optional[int] = None
|
| 312 |
+
|
| 313 |
+
if (ifrange := request.if_range) is None or file_mtime <= ifrange.timestamp():
|
| 314 |
+
# If-Range header check:
|
| 315 |
+
# condition = cached date >= last modification date
|
| 316 |
+
# return 206 if True else 200.
|
| 317 |
+
# if False:
|
| 318 |
+
# Range header would not be processed, return 200
|
| 319 |
+
# if True but Range header missing
|
| 320 |
+
# return 200
|
| 321 |
+
try:
|
| 322 |
+
rng = request.http_range
|
| 323 |
+
start = rng.start
|
| 324 |
+
end: Optional[int] = rng.stop
|
| 325 |
+
except ValueError:
|
| 326 |
+
# https://tools.ietf.org/html/rfc7233:
|
| 327 |
+
# A server generating a 416 (Range Not Satisfiable) response to
|
| 328 |
+
# a byte-range request SHOULD send a Content-Range header field
|
| 329 |
+
# with an unsatisfied-range value.
|
| 330 |
+
# The complete-length in a 416 response indicates the current
|
| 331 |
+
# length of the selected representation.
|
| 332 |
+
#
|
| 333 |
+
# Will do the same below. Many servers ignore this and do not
|
| 334 |
+
# send a Content-Range header with HTTP 416
|
| 335 |
+
self._headers[hdrs.CONTENT_RANGE] = f"bytes */{file_size}"
|
| 336 |
+
self.set_status(HTTPRequestRangeNotSatisfiable.status_code)
|
| 337 |
+
return await super().prepare(request)
|
| 338 |
+
|
| 339 |
+
# If a range request has been made, convert start, end slice
|
| 340 |
+
# notation into file pointer offset and count
|
| 341 |
+
if start is not None:
|
| 342 |
+
if start < 0 and end is None: # return tail of file
|
| 343 |
+
start += file_size
|
| 344 |
+
if start < 0:
|
| 345 |
+
# if Range:bytes=-1000 in request header but file size
|
| 346 |
+
# is only 200, there would be trouble without this
|
| 347 |
+
start = 0
|
| 348 |
+
count = file_size - start
|
| 349 |
+
else:
|
| 350 |
+
# rfc7233:If the last-byte-pos value is
|
| 351 |
+
# absent, or if the value is greater than or equal to
|
| 352 |
+
# the current length of the representation data,
|
| 353 |
+
# the byte range is interpreted as the remainder
|
| 354 |
+
# of the representation (i.e., the server replaces the
|
| 355 |
+
# value of last-byte-pos with a value that is one less than
|
| 356 |
+
# the current length of the selected representation).
|
| 357 |
+
count = (
|
| 358 |
+
min(end if end is not None else file_size, file_size) - start
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
if start >= file_size:
|
| 362 |
+
# HTTP 416 should be returned in this case.
|
| 363 |
+
#
|
| 364 |
+
# According to https://tools.ietf.org/html/rfc7233:
|
| 365 |
+
# If a valid byte-range-set includes at least one
|
| 366 |
+
# byte-range-spec with a first-byte-pos that is less than
|
| 367 |
+
# the current length of the representation, or at least one
|
| 368 |
+
# suffix-byte-range-spec with a non-zero suffix-length,
|
| 369 |
+
# then the byte-range-set is satisfiable. Otherwise, the
|
| 370 |
+
# byte-range-set is unsatisfiable.
|
| 371 |
+
self._headers[hdrs.CONTENT_RANGE] = f"bytes */{file_size}"
|
| 372 |
+
self.set_status(HTTPRequestRangeNotSatisfiable.status_code)
|
| 373 |
+
return await super().prepare(request)
|
| 374 |
+
|
| 375 |
+
status = HTTPPartialContent.status_code
|
| 376 |
+
# Even though you are sending the whole file, you should still
|
| 377 |
+
# return a HTTP 206 for a Range request.
|
| 378 |
+
self.set_status(status)
|
| 379 |
+
|
| 380 |
+
# If the Content-Type header is not already set, guess it based on the
|
| 381 |
+
# extension of the request path. The encoding returned by guess_type
|
| 382 |
+
# can be ignored since the map was cleared above.
|
| 383 |
+
if hdrs.CONTENT_TYPE not in self._headers:
|
| 384 |
+
if sys.version_info >= (3, 13):
|
| 385 |
+
guesser = CONTENT_TYPES.guess_file_type
|
| 386 |
+
else:
|
| 387 |
+
guesser = CONTENT_TYPES.guess_type
|
| 388 |
+
self.content_type = guesser(self._path)[0] or FALLBACK_CONTENT_TYPE
|
| 389 |
+
|
| 390 |
+
if file_encoding:
|
| 391 |
+
self._headers[hdrs.CONTENT_ENCODING] = file_encoding
|
| 392 |
+
self._headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING
|
| 393 |
+
# Disable compression if we are already sending
|
| 394 |
+
# a compressed file since we don't want to double
|
| 395 |
+
# compress.
|
| 396 |
+
self._compression = False
|
| 397 |
+
|
| 398 |
+
self.etag = f"{st.st_mtime_ns:x}-{st.st_size:x}"
|
| 399 |
+
self.last_modified = file_mtime
|
| 400 |
+
self.content_length = count
|
| 401 |
+
|
| 402 |
+
self._headers[hdrs.ACCEPT_RANGES] = "bytes"
|
| 403 |
+
|
| 404 |
+
if status == HTTPPartialContent.status_code:
|
| 405 |
+
real_start = start
|
| 406 |
+
assert real_start is not None
|
| 407 |
+
self._headers[hdrs.CONTENT_RANGE] = "bytes {}-{}/{}".format(
|
| 408 |
+
real_start, real_start + count - 1, file_size
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
# If we are sending 0 bytes calling sendfile() will throw a ValueError
|
| 412 |
+
if count == 0 or must_be_empty_body(request.method, status):
|
| 413 |
+
return await super().prepare(request)
|
| 414 |
+
|
| 415 |
+
# be aware that start could be None or int=0 here.
|
| 416 |
+
offset = start or 0
|
| 417 |
+
|
| 418 |
+
return await self._sendfile(request, fobj, offset, count)
|
aiohttp/web_log.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import datetime
|
| 2 |
+
import functools
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
import time as time_mod
|
| 7 |
+
from collections import namedtuple
|
| 8 |
+
from typing import Any, Callable, Dict, Iterable, List, Tuple # noqa
|
| 9 |
+
|
| 10 |
+
from .abc import AbstractAccessLogger
|
| 11 |
+
from .web_request import BaseRequest
|
| 12 |
+
from .web_response import StreamResponse
|
| 13 |
+
|
| 14 |
+
KeyMethod = namedtuple("KeyMethod", "key method")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class AccessLogger(AbstractAccessLogger):
|
| 18 |
+
"""Helper object to log access.
|
| 19 |
+
|
| 20 |
+
Usage:
|
| 21 |
+
log = logging.getLogger("spam")
|
| 22 |
+
log_format = "%a %{User-Agent}i"
|
| 23 |
+
access_logger = AccessLogger(log, log_format)
|
| 24 |
+
access_logger.log(request, response, time)
|
| 25 |
+
|
| 26 |
+
Format:
|
| 27 |
+
%% The percent sign
|
| 28 |
+
%a Remote IP-address (IP-address of proxy if using reverse proxy)
|
| 29 |
+
%t Time when the request was started to process
|
| 30 |
+
%P The process ID of the child that serviced the request
|
| 31 |
+
%r First line of request
|
| 32 |
+
%s Response status code
|
| 33 |
+
%b Size of response in bytes, including HTTP headers
|
| 34 |
+
%T Time taken to serve the request, in seconds
|
| 35 |
+
%Tf Time taken to serve the request, in seconds with floating fraction
|
| 36 |
+
in .06f format
|
| 37 |
+
%D Time taken to serve the request, in microseconds
|
| 38 |
+
%{FOO}i request.headers['FOO']
|
| 39 |
+
%{FOO}o response.headers['FOO']
|
| 40 |
+
%{FOO}e os.environ['FOO']
|
| 41 |
+
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
LOG_FORMAT_MAP = {
|
| 45 |
+
"a": "remote_address",
|
| 46 |
+
"t": "request_start_time",
|
| 47 |
+
"P": "process_id",
|
| 48 |
+
"r": "first_request_line",
|
| 49 |
+
"s": "response_status",
|
| 50 |
+
"b": "response_size",
|
| 51 |
+
"T": "request_time",
|
| 52 |
+
"Tf": "request_time_frac",
|
| 53 |
+
"D": "request_time_micro",
|
| 54 |
+
"i": "request_header",
|
| 55 |
+
"o": "response_header",
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
LOG_FORMAT = '%a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"'
|
| 59 |
+
FORMAT_RE = re.compile(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrsbOD]|Tf?)")
|
| 60 |
+
CLEANUP_RE = re.compile(r"(%[^s])")
|
| 61 |
+
_FORMAT_CACHE: Dict[str, Tuple[str, List[KeyMethod]]] = {}
|
| 62 |
+
|
| 63 |
+
def __init__(self, logger: logging.Logger, log_format: str = LOG_FORMAT) -> None:
|
| 64 |
+
"""Initialise the logger.
|
| 65 |
+
|
| 66 |
+
logger is a logger object to be used for logging.
|
| 67 |
+
log_format is a string with apache compatible log format description.
|
| 68 |
+
|
| 69 |
+
"""
|
| 70 |
+
super().__init__(logger, log_format=log_format)
|
| 71 |
+
|
| 72 |
+
_compiled_format = AccessLogger._FORMAT_CACHE.get(log_format)
|
| 73 |
+
if not _compiled_format:
|
| 74 |
+
_compiled_format = self.compile_format(log_format)
|
| 75 |
+
AccessLogger._FORMAT_CACHE[log_format] = _compiled_format
|
| 76 |
+
|
| 77 |
+
self._log_format, self._methods = _compiled_format
|
| 78 |
+
|
| 79 |
+
def compile_format(self, log_format: str) -> Tuple[str, List[KeyMethod]]:
|
| 80 |
+
"""Translate log_format into form usable by modulo formatting
|
| 81 |
+
|
| 82 |
+
All known atoms will be replaced with %s
|
| 83 |
+
Also methods for formatting of those atoms will be added to
|
| 84 |
+
_methods in appropriate order
|
| 85 |
+
|
| 86 |
+
For example we have log_format = "%a %t"
|
| 87 |
+
This format will be translated to "%s %s"
|
| 88 |
+
Also contents of _methods will be
|
| 89 |
+
[self._format_a, self._format_t]
|
| 90 |
+
These method will be called and results will be passed
|
| 91 |
+
to translated string format.
|
| 92 |
+
|
| 93 |
+
Each _format_* method receive 'args' which is list of arguments
|
| 94 |
+
given to self.log
|
| 95 |
+
|
| 96 |
+
Exceptions are _format_e, _format_i and _format_o methods which
|
| 97 |
+
also receive key name (by functools.partial)
|
| 98 |
+
|
| 99 |
+
"""
|
| 100 |
+
# list of (key, method) tuples, we don't use an OrderedDict as users
|
| 101 |
+
# can repeat the same key more than once
|
| 102 |
+
methods = list()
|
| 103 |
+
|
| 104 |
+
for atom in self.FORMAT_RE.findall(log_format):
|
| 105 |
+
if atom[1] == "":
|
| 106 |
+
format_key1 = self.LOG_FORMAT_MAP[atom[0]]
|
| 107 |
+
m = getattr(AccessLogger, "_format_%s" % atom[0])
|
| 108 |
+
key_method = KeyMethod(format_key1, m)
|
| 109 |
+
else:
|
| 110 |
+
format_key2 = (self.LOG_FORMAT_MAP[atom[2]], atom[1])
|
| 111 |
+
m = getattr(AccessLogger, "_format_%s" % atom[2])
|
| 112 |
+
key_method = KeyMethod(format_key2, functools.partial(m, atom[1]))
|
| 113 |
+
|
| 114 |
+
methods.append(key_method)
|
| 115 |
+
|
| 116 |
+
log_format = self.FORMAT_RE.sub(r"%s", log_format)
|
| 117 |
+
log_format = self.CLEANUP_RE.sub(r"%\1", log_format)
|
| 118 |
+
return log_format, methods
|
| 119 |
+
|
| 120 |
+
@staticmethod
|
| 121 |
+
def _format_i(
|
| 122 |
+
key: str, request: BaseRequest, response: StreamResponse, time: float
|
| 123 |
+
) -> str:
|
| 124 |
+
if request is None:
|
| 125 |
+
return "(no headers)"
|
| 126 |
+
|
| 127 |
+
# suboptimal, make istr(key) once
|
| 128 |
+
return request.headers.get(key, "-")
|
| 129 |
+
|
| 130 |
+
@staticmethod
|
| 131 |
+
def _format_o(
|
| 132 |
+
key: str, request: BaseRequest, response: StreamResponse, time: float
|
| 133 |
+
) -> str:
|
| 134 |
+
# suboptimal, make istr(key) once
|
| 135 |
+
return response.headers.get(key, "-")
|
| 136 |
+
|
| 137 |
+
@staticmethod
|
| 138 |
+
def _format_a(request: BaseRequest, response: StreamResponse, time: float) -> str:
|
| 139 |
+
if request is None:
|
| 140 |
+
return "-"
|
| 141 |
+
ip = request.remote
|
| 142 |
+
return ip if ip is not None else "-"
|
| 143 |
+
|
| 144 |
+
@staticmethod
|
| 145 |
+
def _format_t(request: BaseRequest, response: StreamResponse, time: float) -> str:
|
| 146 |
+
tz = datetime.timezone(datetime.timedelta(seconds=-time_mod.timezone))
|
| 147 |
+
now = datetime.datetime.now(tz)
|
| 148 |
+
start_time = now - datetime.timedelta(seconds=time)
|
| 149 |
+
return start_time.strftime("[%d/%b/%Y:%H:%M:%S %z]")
|
| 150 |
+
|
| 151 |
+
@staticmethod
|
| 152 |
+
def _format_P(request: BaseRequest, response: StreamResponse, time: float) -> str:
|
| 153 |
+
return "<%s>" % os.getpid()
|
| 154 |
+
|
| 155 |
+
@staticmethod
|
| 156 |
+
def _format_r(request: BaseRequest, response: StreamResponse, time: float) -> str:
|
| 157 |
+
if request is None:
|
| 158 |
+
return "-"
|
| 159 |
+
return "{} {} HTTP/{}.{}".format(
|
| 160 |
+
request.method,
|
| 161 |
+
request.path_qs,
|
| 162 |
+
request.version.major,
|
| 163 |
+
request.version.minor,
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
@staticmethod
|
| 167 |
+
def _format_s(request: BaseRequest, response: StreamResponse, time: float) -> int:
|
| 168 |
+
return response.status
|
| 169 |
+
|
| 170 |
+
@staticmethod
|
| 171 |
+
def _format_b(request: BaseRequest, response: StreamResponse, time: float) -> int:
|
| 172 |
+
return response.body_length
|
| 173 |
+
|
| 174 |
+
@staticmethod
|
| 175 |
+
def _format_T(request: BaseRequest, response: StreamResponse, time: float) -> str:
|
| 176 |
+
return str(round(time))
|
| 177 |
+
|
| 178 |
+
@staticmethod
|
| 179 |
+
def _format_Tf(request: BaseRequest, response: StreamResponse, time: float) -> str:
|
| 180 |
+
return "%06f" % time
|
| 181 |
+
|
| 182 |
+
@staticmethod
|
| 183 |
+
def _format_D(request: BaseRequest, response: StreamResponse, time: float) -> str:
|
| 184 |
+
return str(round(time * 1000000))
|
| 185 |
+
|
| 186 |
+
def _format_line(
|
| 187 |
+
self, request: BaseRequest, response: StreamResponse, time: float
|
| 188 |
+
) -> Iterable[Tuple[str, Callable[[BaseRequest, StreamResponse, float], str]]]:
|
| 189 |
+
return [(key, method(request, response, time)) for key, method in self._methods]
|
| 190 |
+
|
| 191 |
+
@property
|
| 192 |
+
def enabled(self) -> bool:
|
| 193 |
+
"""Check if logger is enabled."""
|
| 194 |
+
# Avoid formatting the log line if it will not be emitted.
|
| 195 |
+
return self.logger.isEnabledFor(logging.INFO)
|
| 196 |
+
|
| 197 |
+
def log(self, request: BaseRequest, response: StreamResponse, time: float) -> None:
|
| 198 |
+
try:
|
| 199 |
+
fmt_info = self._format_line(request, response, time)
|
| 200 |
+
|
| 201 |
+
values = list()
|
| 202 |
+
extra = dict()
|
| 203 |
+
for key, value in fmt_info:
|
| 204 |
+
values.append(value)
|
| 205 |
+
|
| 206 |
+
if key.__class__ is str:
|
| 207 |
+
extra[key] = value
|
| 208 |
+
else:
|
| 209 |
+
k1, k2 = key # type: ignore[misc]
|
| 210 |
+
dct = extra.get(k1, {}) # type: ignore[var-annotated,has-type]
|
| 211 |
+
dct[k2] = value # type: ignore[index,has-type]
|
| 212 |
+
extra[k1] = dct # type: ignore[has-type,assignment]
|
| 213 |
+
|
| 214 |
+
self.logger.info(self._log_format % tuple(values), extra=extra)
|
| 215 |
+
except Exception:
|
| 216 |
+
self.logger.exception("Error in logging")
|
aiohttp/web_routedef.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import abc
|
| 2 |
+
import os # noqa
|
| 3 |
+
from typing import (
|
| 4 |
+
TYPE_CHECKING,
|
| 5 |
+
Any,
|
| 6 |
+
Callable,
|
| 7 |
+
Dict,
|
| 8 |
+
Iterator,
|
| 9 |
+
List,
|
| 10 |
+
Optional,
|
| 11 |
+
Sequence,
|
| 12 |
+
Type,
|
| 13 |
+
Union,
|
| 14 |
+
overload,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
import attr
|
| 18 |
+
|
| 19 |
+
from . import hdrs
|
| 20 |
+
from .abc import AbstractView
|
| 21 |
+
from .typedefs import Handler, PathLike
|
| 22 |
+
|
| 23 |
+
if TYPE_CHECKING:
|
| 24 |
+
from .web_request import Request
|
| 25 |
+
from .web_response import StreamResponse
|
| 26 |
+
from .web_urldispatcher import AbstractRoute, UrlDispatcher
|
| 27 |
+
else:
|
| 28 |
+
Request = StreamResponse = UrlDispatcher = AbstractRoute = None
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
__all__ = (
|
| 32 |
+
"AbstractRouteDef",
|
| 33 |
+
"RouteDef",
|
| 34 |
+
"StaticDef",
|
| 35 |
+
"RouteTableDef",
|
| 36 |
+
"head",
|
| 37 |
+
"options",
|
| 38 |
+
"get",
|
| 39 |
+
"post",
|
| 40 |
+
"patch",
|
| 41 |
+
"put",
|
| 42 |
+
"delete",
|
| 43 |
+
"route",
|
| 44 |
+
"view",
|
| 45 |
+
"static",
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class AbstractRouteDef(abc.ABC):
|
| 50 |
+
@abc.abstractmethod
|
| 51 |
+
def register(self, router: UrlDispatcher) -> List[AbstractRoute]:
|
| 52 |
+
pass # pragma: no cover
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
_HandlerType = Union[Type[AbstractView], Handler]
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@attr.s(auto_attribs=True, frozen=True, repr=False, slots=True)
|
| 59 |
+
class RouteDef(AbstractRouteDef):
|
| 60 |
+
method: str
|
| 61 |
+
path: str
|
| 62 |
+
handler: _HandlerType
|
| 63 |
+
kwargs: Dict[str, Any]
|
| 64 |
+
|
| 65 |
+
def __repr__(self) -> str:
|
| 66 |
+
info = []
|
| 67 |
+
for name, value in sorted(self.kwargs.items()):
|
| 68 |
+
info.append(f", {name}={value!r}")
|
| 69 |
+
return "<RouteDef {method} {path} -> {handler.__name__!r}{info}>".format(
|
| 70 |
+
method=self.method, path=self.path, handler=self.handler, info="".join(info)
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
def register(self, router: UrlDispatcher) -> List[AbstractRoute]:
|
| 74 |
+
if self.method in hdrs.METH_ALL:
|
| 75 |
+
reg = getattr(router, "add_" + self.method.lower())
|
| 76 |
+
return [reg(self.path, self.handler, **self.kwargs)]
|
| 77 |
+
else:
|
| 78 |
+
return [
|
| 79 |
+
router.add_route(self.method, self.path, self.handler, **self.kwargs)
|
| 80 |
+
]
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
@attr.s(auto_attribs=True, frozen=True, repr=False, slots=True)
|
| 84 |
+
class StaticDef(AbstractRouteDef):
|
| 85 |
+
prefix: str
|
| 86 |
+
path: PathLike
|
| 87 |
+
kwargs: Dict[str, Any]
|
| 88 |
+
|
| 89 |
+
def __repr__(self) -> str:
|
| 90 |
+
info = []
|
| 91 |
+
for name, value in sorted(self.kwargs.items()):
|
| 92 |
+
info.append(f", {name}={value!r}")
|
| 93 |
+
return "<StaticDef {prefix} -> {path}{info}>".format(
|
| 94 |
+
prefix=self.prefix, path=self.path, info="".join(info)
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
def register(self, router: UrlDispatcher) -> List[AbstractRoute]:
|
| 98 |
+
resource = router.add_static(self.prefix, self.path, **self.kwargs)
|
| 99 |
+
routes = resource.get_info().get("routes", {})
|
| 100 |
+
return list(routes.values())
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def route(method: str, path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
|
| 104 |
+
return RouteDef(method, path, handler, kwargs)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def head(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
|
| 108 |
+
return route(hdrs.METH_HEAD, path, handler, **kwargs)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def options(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
|
| 112 |
+
return route(hdrs.METH_OPTIONS, path, handler, **kwargs)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def get(
|
| 116 |
+
path: str,
|
| 117 |
+
handler: _HandlerType,
|
| 118 |
+
*,
|
| 119 |
+
name: Optional[str] = None,
|
| 120 |
+
allow_head: bool = True,
|
| 121 |
+
**kwargs: Any,
|
| 122 |
+
) -> RouteDef:
|
| 123 |
+
return route(
|
| 124 |
+
hdrs.METH_GET, path, handler, name=name, allow_head=allow_head, **kwargs
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def post(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
|
| 129 |
+
return route(hdrs.METH_POST, path, handler, **kwargs)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def put(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
|
| 133 |
+
return route(hdrs.METH_PUT, path, handler, **kwargs)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def patch(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
|
| 137 |
+
return route(hdrs.METH_PATCH, path, handler, **kwargs)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def delete(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
|
| 141 |
+
return route(hdrs.METH_DELETE, path, handler, **kwargs)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def view(path: str, handler: Type[AbstractView], **kwargs: Any) -> RouteDef:
|
| 145 |
+
return route(hdrs.METH_ANY, path, handler, **kwargs)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def static(prefix: str, path: PathLike, **kwargs: Any) -> StaticDef:
|
| 149 |
+
return StaticDef(prefix, path, kwargs)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
_Deco = Callable[[_HandlerType], _HandlerType]
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
class RouteTableDef(Sequence[AbstractRouteDef]):
|
| 156 |
+
"""Route definition table"""
|
| 157 |
+
|
| 158 |
+
def __init__(self) -> None:
|
| 159 |
+
self._items: List[AbstractRouteDef] = []
|
| 160 |
+
|
| 161 |
+
def __repr__(self) -> str:
|
| 162 |
+
return f"<RouteTableDef count={len(self._items)}>"
|
| 163 |
+
|
| 164 |
+
@overload
|
| 165 |
+
def __getitem__(self, index: int) -> AbstractRouteDef: ...
|
| 166 |
+
|
| 167 |
+
@overload
|
| 168 |
+
def __getitem__(self, index: slice) -> List[AbstractRouteDef]: ...
|
| 169 |
+
|
| 170 |
+
def __getitem__(self, index): # type: ignore[no-untyped-def]
|
| 171 |
+
return self._items[index]
|
| 172 |
+
|
| 173 |
+
def __iter__(self) -> Iterator[AbstractRouteDef]:
|
| 174 |
+
return iter(self._items)
|
| 175 |
+
|
| 176 |
+
def __len__(self) -> int:
|
| 177 |
+
return len(self._items)
|
| 178 |
+
|
| 179 |
+
def __contains__(self, item: object) -> bool:
|
| 180 |
+
return item in self._items
|
| 181 |
+
|
| 182 |
+
def route(self, method: str, path: str, **kwargs: Any) -> _Deco:
|
| 183 |
+
def inner(handler: _HandlerType) -> _HandlerType:
|
| 184 |
+
self._items.append(RouteDef(method, path, handler, kwargs))
|
| 185 |
+
return handler
|
| 186 |
+
|
| 187 |
+
return inner
|
| 188 |
+
|
| 189 |
+
def head(self, path: str, **kwargs: Any) -> _Deco:
|
| 190 |
+
return self.route(hdrs.METH_HEAD, path, **kwargs)
|
| 191 |
+
|
| 192 |
+
def get(self, path: str, **kwargs: Any) -> _Deco:
|
| 193 |
+
return self.route(hdrs.METH_GET, path, **kwargs)
|
| 194 |
+
|
| 195 |
+
def post(self, path: str, **kwargs: Any) -> _Deco:
|
| 196 |
+
return self.route(hdrs.METH_POST, path, **kwargs)
|
| 197 |
+
|
| 198 |
+
def put(self, path: str, **kwargs: Any) -> _Deco:
|
| 199 |
+
return self.route(hdrs.METH_PUT, path, **kwargs)
|
| 200 |
+
|
| 201 |
+
def patch(self, path: str, **kwargs: Any) -> _Deco:
|
| 202 |
+
return self.route(hdrs.METH_PATCH, path, **kwargs)
|
| 203 |
+
|
| 204 |
+
def delete(self, path: str, **kwargs: Any) -> _Deco:
|
| 205 |
+
return self.route(hdrs.METH_DELETE, path, **kwargs)
|
| 206 |
+
|
| 207 |
+
def options(self, path: str, **kwargs: Any) -> _Deco:
|
| 208 |
+
return self.route(hdrs.METH_OPTIONS, path, **kwargs)
|
| 209 |
+
|
| 210 |
+
def view(self, path: str, **kwargs: Any) -> _Deco:
|
| 211 |
+
return self.route(hdrs.METH_ANY, path, **kwargs)
|
| 212 |
+
|
| 213 |
+
def static(self, prefix: str, path: PathLike, **kwargs: Any) -> None:
|
| 214 |
+
self._items.append(StaticDef(prefix, path, kwargs))
|
bin/accelerate
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from accelerate.commands.accelerate_cli import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/accelerate-config
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from accelerate.commands.config import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/accelerate-estimate-memory
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from accelerate.commands.estimate import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/accelerate-launch
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from accelerate.commands.launch import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/accelerate-merge-weights
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from accelerate.commands.merge import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/datasets-cli
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from datasets.commands.datasets_cli import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/diffusers-cli
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from diffusers.commands.diffusers_cli import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/f2py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from numpy.f2py.f2py2e import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/get_gprof
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
#
|
| 3 |
+
# Author: Mike McKerns (mmckerns @caltech and @uqfoundation)
|
| 4 |
+
# Copyright (c) 2008-2016 California Institute of Technology.
|
| 5 |
+
# Copyright (c) 2016-2025 The Uncertainty Quantification Foundation.
|
| 6 |
+
# License: 3-clause BSD. The full license text is available at:
|
| 7 |
+
# - https://github.com/uqfoundation/dill/blob/master/LICENSE
|
| 8 |
+
'''
|
| 9 |
+
build profile graph for the given instance
|
| 10 |
+
|
| 11 |
+
running:
|
| 12 |
+
$ get_gprof <args> <instance>
|
| 13 |
+
|
| 14 |
+
executes:
|
| 15 |
+
gprof2dot -f pstats <args> <type>.prof | dot -Tpng -o <type>.call.png
|
| 16 |
+
|
| 17 |
+
where:
|
| 18 |
+
<args> are arguments for gprof2dot, such as "-n 5 -e 5"
|
| 19 |
+
<instance> is code to create the instance to profile
|
| 20 |
+
<type> is the class of the instance (i.e. type(instance))
|
| 21 |
+
|
| 22 |
+
For example:
|
| 23 |
+
$ get_gprof -n 5 -e 1 "import numpy; numpy.array([1,2])"
|
| 24 |
+
|
| 25 |
+
will create 'ndarray.call.png' with the profile graph for numpy.array([1,2]),
|
| 26 |
+
where '-n 5' eliminates nodes below 5% threshold, similarly '-e 1' eliminates
|
| 27 |
+
edges below 1% threshold
|
| 28 |
+
'''
|
| 29 |
+
|
| 30 |
+
if __name__ == "__main__":
|
| 31 |
+
import sys
|
| 32 |
+
if len(sys.argv) < 2:
|
| 33 |
+
print ("Please provide an object instance (e.g. 'import math; math.pi')")
|
| 34 |
+
sys.exit()
|
| 35 |
+
# grab args for gprof2dot
|
| 36 |
+
args = sys.argv[1:-1]
|
| 37 |
+
args = ' '.join(args)
|
| 38 |
+
# last arg builds the object
|
| 39 |
+
obj = sys.argv[-1]
|
| 40 |
+
obj = obj.split(';')
|
| 41 |
+
# multi-line prep for generating an instance
|
| 42 |
+
for line in obj[:-1]:
|
| 43 |
+
exec(line)
|
| 44 |
+
# one-line generation of an instance
|
| 45 |
+
try:
|
| 46 |
+
obj = eval(obj[-1])
|
| 47 |
+
except Exception:
|
| 48 |
+
print ("Error processing object instance")
|
| 49 |
+
sys.exit()
|
| 50 |
+
|
| 51 |
+
# get object 'name'
|
| 52 |
+
objtype = type(obj)
|
| 53 |
+
name = getattr(objtype, '__name__', getattr(objtype, '__class__', objtype))
|
| 54 |
+
|
| 55 |
+
# profile dumping an object
|
| 56 |
+
import dill
|
| 57 |
+
import os
|
| 58 |
+
import cProfile
|
| 59 |
+
#name = os.path.splitext(os.path.basename(__file__))[0]
|
| 60 |
+
cProfile.run("dill.dumps(obj)", filename="%s.prof" % name)
|
| 61 |
+
msg = "gprof2dot -f pstats %s %s.prof | dot -Tpng -o %s.call.png" % (args, name, name)
|
| 62 |
+
try:
|
| 63 |
+
res = os.system(msg)
|
| 64 |
+
except Exception:
|
| 65 |
+
print ("Please verify install of 'gprof2dot' to view profile graphs")
|
| 66 |
+
if res:
|
| 67 |
+
print ("Please verify install of 'gprof2dot' to view profile graphs")
|
| 68 |
+
|
| 69 |
+
# get stats
|
| 70 |
+
f_prof = "%s.prof" % name
|
| 71 |
+
import pstats
|
| 72 |
+
stats = pstats.Stats(f_prof, stream=sys.stdout)
|
| 73 |
+
stats.strip_dirs().sort_stats('cumtime')
|
| 74 |
+
stats.print_stats(20) #XXX: save to file instead of print top 20?
|
| 75 |
+
os.remove(f_prof)
|
bin/get_objgraph
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
#
|
| 3 |
+
# Author: Mike McKerns (mmckerns @caltech and @uqfoundation)
|
| 4 |
+
# Copyright (c) 2008-2016 California Institute of Technology.
|
| 5 |
+
# Copyright (c) 2016-2025 The Uncertainty Quantification Foundation.
|
| 6 |
+
# License: 3-clause BSD. The full license text is available at:
|
| 7 |
+
# - https://github.com/uqfoundation/dill/blob/master/LICENSE
|
| 8 |
+
"""
|
| 9 |
+
display the reference paths for objects in ``dill.types`` or a .pkl file
|
| 10 |
+
|
| 11 |
+
Notes:
|
| 12 |
+
the generated image is useful in showing the pointer references in
|
| 13 |
+
objects that are or can be pickled. Any object in ``dill.objects``
|
| 14 |
+
listed in ``dill.load_types(picklable=True, unpicklable=True)`` works.
|
| 15 |
+
|
| 16 |
+
Examples::
|
| 17 |
+
|
| 18 |
+
$ get_objgraph ArrayType
|
| 19 |
+
Image generated as ArrayType.png
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import dill as pickle
|
| 23 |
+
#pickle.debug.trace(True)
|
| 24 |
+
#import pickle
|
| 25 |
+
|
| 26 |
+
# get all objects for testing
|
| 27 |
+
from dill import load_types
|
| 28 |
+
load_types(pickleable=True,unpickleable=True)
|
| 29 |
+
from dill import objects
|
| 30 |
+
|
| 31 |
+
if __name__ == "__main__":
|
| 32 |
+
import sys
|
| 33 |
+
if len(sys.argv) != 2:
|
| 34 |
+
print ("Please provide exactly one file or type name (e.g. 'IntType')")
|
| 35 |
+
msg = "\n"
|
| 36 |
+
for objtype in list(objects.keys())[:40]:
|
| 37 |
+
msg += objtype + ', '
|
| 38 |
+
print (msg + "...")
|
| 39 |
+
else:
|
| 40 |
+
objtype = str(sys.argv[-1])
|
| 41 |
+
try:
|
| 42 |
+
obj = objects[objtype]
|
| 43 |
+
except KeyError:
|
| 44 |
+
obj = pickle.load(open(objtype,'rb'))
|
| 45 |
+
import os
|
| 46 |
+
objtype = os.path.splitext(objtype)[0]
|
| 47 |
+
try:
|
| 48 |
+
import objgraph
|
| 49 |
+
objgraph.show_refs(obj, filename=objtype+'.png')
|
| 50 |
+
except ImportError:
|
| 51 |
+
print ("Please install 'objgraph' to view object graphs")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# EOF
|
bin/hf
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from huggingface_hub.cli.hf import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/httpx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from httpx import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/isympy
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from isympy import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/markdown-it
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from markdown_it.cli.parse import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/normalizer
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from charset_normalizer.cli import cli_detect
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(cli_detect())
|
bin/proton
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from triton.profiler.proton import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/proton-viewer
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from triton.profiler.viewer import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/pygmentize
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from pygments.cmdline import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/tiny-agents
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from huggingface_hub.inference._mcp.cli import app
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(app())
|
bin/torchfrtrace
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from torch.distributed.flight_recorder.fr_trace import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/torchrun
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from torch.distributed.run import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|
bin/tqdm
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/home/zeus/miniconda3/envs/cloudspace/bin/python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import sys
|
| 4 |
+
from tqdm.cli import main
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
if sys.argv[0].endswith("-script.pyw"):
|
| 7 |
+
sys.argv[0] = sys.argv[0][:-11]
|
| 8 |
+
elif sys.argv[0].endswith(".exe"):
|
| 9 |
+
sys.argv[0] = sys.argv[0][:-4]
|
| 10 |
+
sys.exit(main())
|