Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3880cf1255 | ||
|
|
ae7c28eb8e | ||
|
|
3e3fc7683c | ||
|
|
3d339ed7e6 | ||
|
|
cfb9e66b04 | ||
|
|
5fcd843eb2 | ||
|
|
1f0e5775f2 | ||
|
|
4cfc5bcd96 | ||
|
|
b906c04a6f | ||
|
|
35045e851a | ||
|
|
16eeee1436 | ||
|
|
576e18a796 | ||
|
|
d0e64d32ba | ||
|
|
a0d9e0bc57 | ||
|
|
5a799dd0f5 | ||
|
|
8cf5e67041 | ||
|
|
a0632d8df0 | ||
|
|
5478dd6271 | ||
|
|
599f834351 | ||
|
|
f02d0f9e79 | ||
|
|
f89b5a5d90 | ||
|
|
0cd9f5855e | ||
|
|
ccc03b93ef | ||
|
|
b0c008c593 | ||
|
|
a57c87a459 | ||
|
|
e3c7b43fc9 | ||
|
|
956ec8321a | ||
|
|
0581f2c7d2 | ||
|
|
1ff9a05181 | ||
|
|
36093e69c7 | ||
|
|
dacf69a16d | ||
|
|
1b5bfe681b | ||
|
|
18e678c1f8 | ||
|
|
1b1866934b | ||
|
|
d294de38c4 | ||
|
|
898465756a | ||
|
|
0ad075c430 | ||
|
|
460caf71a4 | ||
|
|
452ba32edf | ||
|
|
587eb193dd | ||
|
|
1145fe64a5 | ||
|
|
ce234d53a5 | ||
|
|
e95e173b8c | ||
|
|
b2ce5be9d0 | ||
|
|
a0f5776f35 | ||
|
|
1b5317ea08 | ||
|
|
8dda20f8e3 | ||
|
|
54791d1970 | ||
|
|
b7e3370762 | ||
|
|
bedd2be0a7 | ||
|
|
b974cd3741 | ||
|
|
86f9b1616a | ||
|
|
dfa0a2925f | ||
|
|
81d2d5535a | ||
|
|
cd3f7eb388 | ||
|
|
3711e04f37 | ||
|
|
8c6d82a244 | ||
|
|
3385c78999 | ||
|
|
6e2f1eb402 | ||
|
|
fa338da717 | ||
|
|
80a3e87bf3 | ||
|
|
bbbd9de6fd | ||
|
|
e776bb2656 | ||
|
|
f7c85685a8 | ||
|
|
8a2fd7ce6d | ||
|
|
76f7ce8a24 | ||
|
|
573d460aa7 | ||
|
|
3bd8afbff2 | ||
|
|
1acc4992d4 | ||
|
|
bc746e158c | ||
|
|
c4bd6ea150 | ||
|
|
9adaae6785 | ||
|
|
a0790f37b7 | ||
|
|
14b653ab3c | ||
|
|
4497a26c37 | ||
|
|
62200cb07e | ||
|
|
c53aa08cce | ||
|
|
bcfcd9f980 | ||
|
|
ec700a9a13 | ||
|
|
e7ac8ea0c1 | ||
|
|
39e49e5fb9 | ||
|
|
61f4faae70 | ||
|
|
1887f32b17 | ||
|
|
326f77d57c | ||
|
|
11b12c3e07 | ||
|
|
2669f0c0b1 | ||
|
|
9d8a57f2a2 | ||
|
|
6ea3f2ada8 | ||
|
|
461b740db6 | ||
|
|
628474d626 | ||
|
|
de674fc82c | ||
|
|
b58318163c | ||
|
|
d8be2473d1 | ||
|
|
58e513579a | ||
|
|
0b6d2e451b | ||
|
|
6f3c91b834 | ||
|
|
ed73d9e45d | ||
|
|
5a22d2060f | ||
|
|
344b51327c | ||
|
|
62df872871 | ||
|
|
a5a314e8ee | ||
|
|
b86330d1f1 | ||
|
|
69d9594338 | ||
|
|
291b6b24b7 | ||
|
|
426493f388 | ||
|
|
80aee7a225 | ||
|
|
ced6550a25 | ||
|
|
c7beb703bf | ||
|
|
53f33eb473 | ||
|
|
69b7c45c2e | ||
|
|
e644857877 | ||
|
|
3a5e2b2f3d | ||
|
|
02f852cc41 | ||
|
|
ff56e431ad | ||
|
|
b137472936 | ||
|
|
383b20b023 | ||
|
|
3314eb9a62 | ||
|
|
cf3a7f4945 | ||
|
|
fb1971778f | ||
|
|
2f7f6a57f3 | ||
|
|
74d85d0a2f | ||
|
|
eec44dbbb1 | ||
|
|
677bbe30d7 | ||
|
|
f37fe551bb | ||
|
|
751b35c346 | ||
|
|
d2d576b6b1 | ||
|
|
a57eacf080 | ||
|
|
b176ccd481 | ||
|
|
176b365287 | ||
|
|
09658b80c0 | ||
|
|
ac34e3b346 | ||
|
|
5549f50258 | ||
|
|
15597c27cd | ||
|
|
673bd6afce | ||
|
|
7612015424 | ||
|
|
892f2560c1 | ||
|
|
2d1ffad80b | ||
|
|
101425ebfc | ||
|
|
f4ec3d2309 | ||
|
|
da82fb4b1d | ||
|
|
d615405519 |
13
AUTHORS.rst
13
AUTHORS.rst
@@ -1,14 +1,10 @@
|
||||
Requests is written and maintained by Kenneth Reitz and
|
||||
various contributors:
|
||||
|
||||
Development Lead
|
||||
````````````````
|
||||
|
||||
- Kenneth Reitz <me@kennethreitz.org> `@kennethreitz <https://github.com/kennethreitz>`_
|
||||
|
||||
Core Contributors
|
||||
`````````````````
|
||||
Keepers of the Three Crystals
|
||||
`````````````````````````````
|
||||
|
||||
- Kenneth Reitz <me@kennethreitz.org> `@kennethreitz <https://github.com/kennethreitz>`_, Keeper of the Master Crystal.
|
||||
- Cory Benfield <cory@lukasa.co.uk> `@lukasa <https://github.com/lukasa>`_
|
||||
- Ian Cordasco <graffatcolmingov@gmail.com> `@sigmavirus24 <https://github.com/sigmavirus24>`_
|
||||
|
||||
@@ -159,3 +155,6 @@ Patches and Suggestions
|
||||
- Syed Suhail Ahmed <ssuhail.ahmed93@gmail.com> (`@syedsuhail <https://github.com/syedsuhail>`_)
|
||||
- Scott Sadler (`@ssadler <https://github.com/ssadler>`_)
|
||||
- Arthur Darcet (`@arthurdarcet <https://github.com/arthurdarcet>`_)
|
||||
- Ulrich Petri (`@ulope <https://github.com/ulope>`_)
|
||||
- Muhammad Yasoob Ullah Khalid <yasoob.khld@gmail.com> (`@yasoob <https://github.com/yasoob>`_)
|
||||
- Paul van der Linden (`@pvanderlinden <https://github.com/pvanderlinden>`_)
|
||||
|
||||
109
HISTORY.rst
109
HISTORY.rst
@@ -3,6 +3,113 @@
|
||||
Release History
|
||||
---------------
|
||||
|
||||
2.6.1 (2015-04-22)
|
||||
++++++++++++++++++
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Remove VendorAlias import machinery introduced in v2.5.2.
|
||||
|
||||
- Simplify the PreparedRequest.prepare API: We no longer require the user to
|
||||
pass an empty list to the hooks keyword argument. (c.f. #2552)
|
||||
|
||||
- Resolve redirects now receives and forwards all of the original arguments to
|
||||
the adapter. (#2503)
|
||||
|
||||
- Handle UnicodeDecodeErrors when trying to deal with a unicode URL that
|
||||
cannot be encoded in ASCII. (#2540)
|
||||
|
||||
- Populate the parsed path of the URI field when performing Digest
|
||||
Authentication. (#2426)
|
||||
|
||||
- Copy a PreparedRequest's CookieJar more reliably when it is not an instance
|
||||
of RequestsCookieJar. (#2527)
|
||||
|
||||
2.6.0 (2015-03-14)
|
||||
++++++++++++++++++
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- CVE-2015-2296: Fix handling of cookies on redirect. Previously a cookie
|
||||
without a host value set would use the hostname for the redirected URL
|
||||
exposing requests users to session fixation attacks and potentially cookie
|
||||
stealing. This was disclosed privately by Matthew Daley of
|
||||
`BugFuzz <https://bugfuzz.com>`_. This affects all versions of requests from
|
||||
v2.1.0 to v2.5.3 (inclusive on both ends).
|
||||
|
||||
- Fix error when requests is an ``install_requires`` dependency and ``python
|
||||
setup.py test`` is run. (#2462)
|
||||
|
||||
- Fix error when urllib3 is unbundled and requests continues to use the
|
||||
vendored import location.
|
||||
|
||||
- Include fixes to ``urllib3``'s header handling.
|
||||
|
||||
- Requests' handling of unvendored dependencies is now more restrictive.
|
||||
|
||||
**Features and Improvements**
|
||||
|
||||
- Support bytearrays when passed as parameters in the ``files`` argument.
|
||||
(#2468)
|
||||
|
||||
- Avoid data duplication when creating a request with ``str``, ``bytes``, or
|
||||
``bytearray`` input to the ``files`` argument.
|
||||
|
||||
2.5.3 (2015-02-24)
|
||||
++++++++++++++++++
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Revert changes to our vendored certificate bundle. For more context see
|
||||
(#2455, #2456, and http://bugs.python.org/issue23476)
|
||||
|
||||
2.5.2 (2015-02-23)
|
||||
++++++++++++++++++
|
||||
|
||||
**Features and Improvements**
|
||||
|
||||
- Add sha256 fingerprint support. (`shazow/urllib3#540`_)
|
||||
|
||||
- Improve the performance of headers. (`shazow/urllib3#544`_)
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Copy pip's import machinery. When downstream redistributors remove
|
||||
requests.packages.urllib3 the import machinery will continue to let those
|
||||
same symbols work. Example usage in requests' documentation and 3rd-party
|
||||
libraries relying on the vendored copies of urllib3 will work without having
|
||||
to fallback to the system urllib3.
|
||||
|
||||
- Attempt to quote parts of the URL on redirect if unquoting and then quoting
|
||||
fails. (#2356)
|
||||
|
||||
- Fix filename type check for multipart form-data uploads. (#2411)
|
||||
|
||||
- Properly handle the case where a server issuing digest authentication
|
||||
challenges provides both auth and auth-int qop-values. (#2408)
|
||||
|
||||
- Fix a socket leak. (`shazow/urllib3#549`_)
|
||||
|
||||
- Fix multiple ``Set-Cookie`` headers properly. (`shazow/urllib3#534`_)
|
||||
|
||||
- Disable the built-in hostname verification. (`shazow/urllib3#526`_)
|
||||
|
||||
- Fix the behaviour of decoding an exhausted stream. (`shazow/urllib3#535`_)
|
||||
|
||||
**Security**
|
||||
|
||||
- Pulled in an updated ``cacert.pem``.
|
||||
|
||||
- Drop RC4 from the default cipher list. (`shazow/urllib3#551`_)
|
||||
|
||||
.. _shazow/urllib3#551: https://github.com/shazow/urllib3/pull/551
|
||||
.. _shazow/urllib3#549: https://github.com/shazow/urllib3/pull/549
|
||||
.. _shazow/urllib3#544: https://github.com/shazow/urllib3/pull/544
|
||||
.. _shazow/urllib3#540: https://github.com/shazow/urllib3/pull/540
|
||||
.. _shazow/urllib3#535: https://github.com/shazow/urllib3/pull/535
|
||||
.. _shazow/urllib3#534: https://github.com/shazow/urllib3/pull/534
|
||||
.. _shazow/urllib3#526: https://github.com/shazow/urllib3/pull/526
|
||||
|
||||
2.5.1 (2014-12-23)
|
||||
++++++++++++++++++
|
||||
|
||||
@@ -103,7 +210,7 @@ Release History
|
||||
- Support for connect timeouts! Timeout now accepts a tuple (connect, read) which is used to set individual connect and read timeouts.
|
||||
- Allow copying of PreparedRequests without headers/cookies.
|
||||
- Updated bundled urllib3 version.
|
||||
- Refactored settings loading from environment — new `Session.merge_environment_settings`.
|
||||
- Refactored settings loading from environment -- new `Session.merge_environment_settings`.
|
||||
- Handle socket errors in iter_content.
|
||||
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright 2014 Kenneth Reitz
|
||||
Copyright 2015 Kenneth Reitz
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
10
README.rst
10
README.rst
@@ -1,11 +1,13 @@
|
||||
Requests: HTTP for Humans
|
||||
=========================
|
||||
|
||||
.. image:: https://badge.fury.io/py/requests.png
|
||||
:target: http://badge.fury.io/py/requests
|
||||
.. image:: https://img.shields.io/pypi/v/requests.svg
|
||||
:target: https://pypi.python.org/pypi/requests
|
||||
|
||||
.. image:: https://img.shields.io/pypi/dm/requests.svg
|
||||
:target: https://pypi.python.org/pypi/requests
|
||||
|
||||
|
||||
.. image:: https://pypip.in/d/requests/badge.png
|
||||
:target: https://crate.io/packages/requests/
|
||||
|
||||
|
||||
Requests is an Apache2 Licensed HTTP library, written in Python, for human
|
||||
|
||||
16
docs/_templates/sidebarintro.html
vendored
16
docs/_templates/sidebarintro.html
vendored
@@ -15,15 +15,9 @@
|
||||
</p>
|
||||
|
||||
|
||||
<h3>Donate</h3>
|
||||
<p>
|
||||
If you love Requests, consider supporting the author <a href="https://gratipay.com/kennethreitz/">on Gratipay</a>:
|
||||
</p>
|
||||
<p>
|
||||
<iframe style="border: 0; margin: 0; padding: 0;"
|
||||
src="https://www.gratipay.com/kennethreitz/widget.html"
|
||||
width="80pt" height="20pt"></iframe>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://gumroad.com/l/RRZc" class="gumroad-button">Buy Requests Pro</a>
|
||||
</p>
|
||||
|
||||
|
||||
<h3>Get Updates</h3>
|
||||
@@ -46,6 +40,10 @@
|
||||
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="http://docs.python-requests.org/en/latest/community/recommended/">Recomended Packages and Extensions</a></li>
|
||||
|
||||
<p></p>
|
||||
|
||||
<li><a href="http://github.com/kennethreitz/requests">Requests @ GitHub</a></li>
|
||||
<li><a href="http://pypi.python.org/pypi/requests">Requests @ PyPI</a></li>
|
||||
<li><a href="http://github.com/kennethreitz/requests/issues">Issue Tracker</a></li>
|
||||
|
||||
13
docs/_templates/sidebarlogo.html
vendored
13
docs/_templates/sidebarlogo.html
vendored
@@ -14,16 +14,9 @@
|
||||
development release.
|
||||
</p>
|
||||
|
||||
|
||||
<h3>Donate</h3>
|
||||
<p>
|
||||
If you love Requests, consider supporting the author <a href="https://www.gratipay.com/kennethreitz/">on Gittip</a>:
|
||||
</p>
|
||||
<p>
|
||||
<iframe style="border: 0; margin: 0; padding: 0;"
|
||||
src="https://www.gratipay.com/kennethreitz/widget.html"
|
||||
width="80pt" height="20pt"></iframe>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://gumroad.com/l/RRZc" class="gumroad-button">Buy Requests Pro</a>
|
||||
</p>
|
||||
|
||||
<h3>Get Updates</h3>
|
||||
<p>Receive updates on new releases and upcoming projects.</p>
|
||||
|
||||
13
docs/_themes/kr/layout.html
vendored
13
docs/_themes/kr/layout.html
vendored
@@ -15,6 +15,9 @@
|
||||
<a href="https://github.com/kennethreitz/requests" class="github">
|
||||
<img style="position: absolute; top: 0; right: 0; border: 0;" src="http://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub" class="github"/>
|
||||
</a>
|
||||
|
||||
<script type="text/javascript" src="https://gumroad.com/js/gumroad.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
/* <![CDATA[ */
|
||||
(function() {
|
||||
@@ -68,5 +71,15 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
window._pa = window._pa || {};
|
||||
_pa.productId = "requests-docs";
|
||||
var pa = document.createElement('script'); pa.type = 'text/javascript'; pa.async = true;
|
||||
pa.src = ('https:' == document.location.protocol ? 'https:' : 'http:') + "//tag.perfectaudience.com/serve/5226171f87bc6890da0000a0.js";
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(pa, s);
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
{%- endblock %}
|
||||
|
||||
@@ -88,6 +88,12 @@ Cookies
|
||||
.. autofunction:: requests.utils.cookiejar_from_dict
|
||||
.. autofunction:: requests.utils.add_dict_to_cookiejar
|
||||
|
||||
.. autoclass:: requests.cookies.RequestsCookieJar
|
||||
:inherited-members:
|
||||
|
||||
.. autoclass:: requests.cookies.CookieConflictError
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Encodings
|
||||
~~~~~~~~~
|
||||
|
||||
@@ -30,3 +30,4 @@ Articles & Talks
|
||||
- `Issac Kelly's 'Consuming Web APIs' talk <http://issackelly.github.com/Consuming-Web-APIs-with-Python-Talk/slides/slides.html>`_
|
||||
- `Blog post about Requests via Yum <http://arunsag.wordpress.com/2011/08/17/new-package-python-requests-http-for-humans/>`_
|
||||
- `Russian blog post introducing Requests <http://habrahabr.ru/blogs/python/126262/>`_
|
||||
- `Sending JSON in Requests <http://www.coglib.com/~icordasc/blog/2014/11/sending-json-in-requests.html>`_
|
||||
|
||||
57
docs/community/recommended.rst
Normal file
57
docs/community/recommended.rst
Normal file
@@ -0,0 +1,57 @@
|
||||
.. _recommended:
|
||||
|
||||
Recommended Packages and Extensions
|
||||
===================================
|
||||
|
||||
Requests has a great variety of powerful and useful third-party extensions.
|
||||
This page provides an overview of some of the best of them.
|
||||
|
||||
Certifi CA Bundle
|
||||
-----------------
|
||||
|
||||
`Certifi`_ is a carefully curated collection of Root Certificates for
|
||||
validating the trustworthiness of SSL certificates while verifying the
|
||||
identity of TLS hosts. It has been extracted from the Requests project.
|
||||
|
||||
.. _Certifi: http://certifi.io/en/latest/
|
||||
|
||||
CacheControl
|
||||
------------
|
||||
|
||||
`CacheControl`_ is an extension that adds a full HTTP cache to Requests. This
|
||||
makes your web requests substantially more efficient, and should be used
|
||||
whenever you're making a lot of web requests.
|
||||
|
||||
.. _CacheControl: https://cachecontrol.readthedocs.org/en/latest/
|
||||
|
||||
Requests-Toolbelt
|
||||
-----------------
|
||||
|
||||
`Requests-Toolbelt`_ is a collection of utilities that some users of Requests may desire,
|
||||
but do not belong in Requests proper. This library is actively maintained
|
||||
by members of the Requests core team, and reflects the functionality most
|
||||
requested by users within the community.
|
||||
|
||||
.. _Requests-Toolbelt: http://toolbelt.readthedocs.org/en/latest/index.html
|
||||
|
||||
Requests-OAuthlib
|
||||
-----------------
|
||||
|
||||
`requests-oauthlib`_ makes it possible to do the OAuth dance from Requests
|
||||
automatically. This is useful for the large number of websites that use OAuth
|
||||
to provide authentication. It also provides a lot of tweaks that handle ways
|
||||
that specific OAuth providers differ from the standard specifications.
|
||||
|
||||
.. _requests-oauthlib: https://requests-oauthlib.readthedocs.org/en/latest/
|
||||
|
||||
|
||||
Betamax
|
||||
-------
|
||||
|
||||
`Betamax`_ records your HTTP interactions so the NSA does not have to.
|
||||
A VCR imitation designed only for Python-Requests.
|
||||
|
||||
.. _betamax: https://github.com/sigmavirus24/betamax
|
||||
|
||||
|
||||
|
||||
@@ -5,12 +5,21 @@ Support
|
||||
|
||||
If you have questions or issues about Requests, there are several options:
|
||||
|
||||
StackOverflow
|
||||
-------------
|
||||
|
||||
If your question does not contain sensitive (possibly proprietary)
|
||||
information or can be properly anonymized, please ask a question on
|
||||
`StackOverflow <https://stackoverflow.com/questions/tagged/python-requests>`_
|
||||
and use the tag ``python-requests``.
|
||||
|
||||
Send a Tweet
|
||||
------------
|
||||
|
||||
If your question is less than 140 characters, feel free to send a tweet to
|
||||
`@kennethreitz <http://twitter.com/kennethreitz>`_.
|
||||
|
||||
`@kennethreitz <https://twitter.com/kennethreitz>`_,
|
||||
`@sigmavirus24 <https://twitter.com/sigmavirus24>`_, or
|
||||
`@lukasaoz <https://twitter.com/lukasaoz>`_.
|
||||
|
||||
File an Issue
|
||||
-------------
|
||||
@@ -34,4 +43,9 @@ IRC
|
||||
The official Freenode channel for Requests is
|
||||
`#python-requests <irc://irc.freenode.net/python-requests>`_
|
||||
|
||||
I'm also available as **kennethreitz** on Freenode.
|
||||
The core developers of requests are on IRC throughout the day.
|
||||
You can find them in ``#python-requests`` as:
|
||||
|
||||
- kennethreitz
|
||||
- lukasa
|
||||
- sigmavirus24
|
||||
|
||||
105
docs/community/vulnerabilities.rst
Normal file
105
docs/community/vulnerabilities.rst
Normal file
@@ -0,0 +1,105 @@
|
||||
Vulnerability Disclosure
|
||||
========================
|
||||
|
||||
If you think you have found a potential security vulnerability in requests,
|
||||
please email `sigmavirus24 <mailto:graffatcolmingov@gmail.com>`_ and
|
||||
`Lukasa <mailto:cory@lukasa.co.uk>`_ directly. **Do not file a public issue.**
|
||||
|
||||
Our PGP Key fingerprints are:
|
||||
|
||||
- 0161 BB7E B208 B5E0 4FDC 9F81 D9DA 0A04 9113 F853 (@sigmavirus24)
|
||||
|
||||
- 90DC AE40 FEA7 4B14 9B70 662D F25F 2144 EEC1 373D (@lukasa)
|
||||
|
||||
If English is not your first language, please try to describe the problem and
|
||||
its impact to the best of your ability. For greater detail, please use your
|
||||
native language and we will try our best to translate it using online services.
|
||||
|
||||
Please also include the code you used to find the problem and the shortest
|
||||
amount of code necessary to reproduce it.
|
||||
|
||||
Please do not disclose this to anyone else. We will retrieve a CVE identifier
|
||||
if necessary and give you full credit under whatever name or alias you provide.
|
||||
We will only request an identifier when we have a fix and can publish it in a
|
||||
release.
|
||||
|
||||
We will respect your privacy and will only publicize your involvement if you
|
||||
grant us permission.
|
||||
|
||||
Process
|
||||
-------
|
||||
|
||||
This following information discusses the process the requests project follows
|
||||
in response to vulnerability disclosures. If you are disclosing a
|
||||
vulnerability, this section of the documentation lets you know how we will
|
||||
respond to your disclosure.
|
||||
|
||||
Timeline
|
||||
~~~~~~~~
|
||||
|
||||
When you report an issue, one of the project members will respond to you within
|
||||
two days *at the outside*. In most cases responses will be faster, usually
|
||||
within 12 hours. This initial response will at the very least confirm receipt
|
||||
of the report.
|
||||
|
||||
If we were able to rapidly reproduce the issue, the initial response will also
|
||||
contain confirmation of the issue. If we are not, we will often ask for more
|
||||
information about the reproduction scenario.
|
||||
|
||||
Our goal is to have a fix for any vulnerability released within two weeks of
|
||||
the initial disclosure. This may potentially involve shipping an interim
|
||||
release that simply disables function while a more mature fix can be prepared,
|
||||
but will in the vast majority of cases mean shipping a complete release as soon
|
||||
as possible.
|
||||
|
||||
Throughout the fix process we will keep you up to speed with how the fix is
|
||||
progressing. Once the fix is prepared, we will notify you that we believe we
|
||||
have a fix. Often we will ask you to confirm the fix resolves the problem in
|
||||
your environment, especially if we are not confident of our reproduction
|
||||
scenario.
|
||||
|
||||
At this point, we will prepare for the release. We will obtain a CVE number
|
||||
if one is required, providing you with full credit for the discovery. We will
|
||||
also decide on a planned release date, and let you know when it is. This
|
||||
release date will *always* be on a weekday.
|
||||
|
||||
At this point we will reach out to our major downstream packagers to notify
|
||||
them of an impending security-related patch so they can make arrangements. In
|
||||
addition, these packagers will be provided with the intended patch ahead of
|
||||
time, to ensure that they are able to promptly release their downstream
|
||||
packages. Currently the list of people we actively contact *ahead of a public
|
||||
release* is:
|
||||
|
||||
- Ralph Bean, Red Hat (@ralphbean)
|
||||
- Daniele Tricoli, Debian (@eriol)
|
||||
|
||||
We will notify these individuals at least a week ahead of our planned release
|
||||
date to ensure that they have sufficient time to prepare. If you believe you
|
||||
should be on this list, please let one of the maintainers know at one of the
|
||||
email addresses at the top of this article.
|
||||
|
||||
On release day, we will push the patch to our public repository, along with an
|
||||
updated changelog that describes the issue and credits you. We will then issue
|
||||
a PyPI release containing the patch.
|
||||
|
||||
At this point, we will publicise the release. This will involve mails to
|
||||
mailing lists, Tweets, and all other communication mechanisms available to the
|
||||
core team.
|
||||
|
||||
We will also explicitly mention which commits contain the fix to make it easier
|
||||
for other distributors and users to easily patch their own versions of requests
|
||||
if upgrading is not an option.
|
||||
|
||||
Previous CVEs
|
||||
-------------
|
||||
|
||||
- Fixed in 2.6.0
|
||||
|
||||
- `CVE 2015-2296 <http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=2015-2296>`_,
|
||||
reported by Matthew Daley of `BugFuzz <https://bugfuzz.com/>`_.
|
||||
|
||||
- Fixed in 2.3.0
|
||||
|
||||
- `CVE 2014-1829 <http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=2014-1829>`_
|
||||
|
||||
- `CVE 2014-1830 <http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=2014-1830>`_
|
||||
@@ -46,7 +46,7 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Requests'
|
||||
copyright = u'2014. A <a href="http://kennethreitz.com/pages/open-projects.html">Kenneth Reitz</a> Project'
|
||||
copyright = u'2015. A <a href="http://kennethreitz.com/pages/open-projects.html">Kenneth Reitz</a> Project'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
|
||||
@@ -48,7 +48,7 @@ Testimonials
|
||||
Her Majesty's Government, Amazon, Google, Twilio, Runscope, Mozilla, Heroku,
|
||||
PayPal, NPR, Obama for America, Transifex, Native Instruments, The Washington
|
||||
Post, Twitter, SoundCloud, Kippt, Readability, Sony, and Federal US Institutions that prefer to be unnamed
|
||||
use Requests internally. It has been downloaded over 23,000,000 times from PyPI.
|
||||
use Requests internally. It has been downloaded over 40,000,000 times from PyPI.
|
||||
|
||||
**Armin Ronacher**
|
||||
Requests is the perfect example how beautiful an API can be with the
|
||||
@@ -114,8 +114,10 @@ Requests ecosystem and community.
|
||||
:maxdepth: 1
|
||||
|
||||
community/faq
|
||||
community/recommended
|
||||
community/out-there.rst
|
||||
community/support
|
||||
community/vulnerabilities
|
||||
community/updates
|
||||
|
||||
API Documentation
|
||||
|
||||
@@ -399,6 +399,19 @@ set ``stream`` to ``True`` and iterate over the response with
|
||||
if line:
|
||||
print(json.loads(line))
|
||||
|
||||
.. warning::
|
||||
|
||||
:class:`~requests.Response.iter_lines()` is not reentrant safe.
|
||||
Calling this method multiple times causes some of the received data
|
||||
being lost. In case you need to call it from multiple places, use
|
||||
the resulting iterator object instead::
|
||||
|
||||
lines = r.iter_lines()
|
||||
# Save the first line for later or just skip it
|
||||
first_line = next(lines)
|
||||
for line in lines:
|
||||
print(line)
|
||||
|
||||
.. _proxies:
|
||||
|
||||
Proxies
|
||||
|
||||
@@ -73,6 +73,13 @@ You can see that the URL has been correctly encoded by printing the URL::
|
||||
Note that any dictionary key whose value is ``None`` will not be added to the
|
||||
URL's query string.
|
||||
|
||||
In order to pass a list of items as a value you must mark the key as
|
||||
referring to a list like string by appending ``[]`` to the key::
|
||||
|
||||
>>> payload = {'key1': 'value1', 'key2[]': ['value2', 'value3']}
|
||||
>>> r = requests.get("http://httpbin.org/get", params=payload)
|
||||
>>> print(r.url)
|
||||
http://httpbin.org/get?key1=value1&key2%5B%5D=value2&key2%5B%5D=value3
|
||||
|
||||
Response Content
|
||||
----------------
|
||||
@@ -179,10 +186,18 @@ For example, we didn't specify our content-type in the previous example::
|
||||
|
||||
>>> import json
|
||||
>>> url = 'https://api.github.com/some/endpoint'
|
||||
>>> payload = {'some': 'data'}
|
||||
>>> headers = {'content-type': 'application/json'}
|
||||
>>> headers = {'user-agent': 'my-app/0.0.1'}
|
||||
|
||||
>>> r = requests.post(url, data=json.dumps(payload), headers=headers)
|
||||
>>> r = requests.get(url, headers=headers)
|
||||
|
||||
Note: Custom headers are given less precedence than more specific sources of information. For instance:
|
||||
|
||||
* Authorization headers will be overridden if credentials are passed via the ``auth`` parameter or are specified in a ``.netrc`` accessible in the environment.
|
||||
* Authorization headers will be removed if you get redirected off-host.
|
||||
* Proxy-Authorization headers will be overridden by proxy credentials provided in the URL.
|
||||
* Content-Length headers will be overridden when we can determine the length of the content.
|
||||
|
||||
Furthermore, Requests does not change its behavior at all based on which custom headers are specified. The headers are simply passed on into the final request.
|
||||
|
||||
|
||||
More complicated POST requests
|
||||
@@ -268,7 +283,7 @@ In the event you are posting a very large file as a ``multipart/form-data``
|
||||
request, you may want to stream the request. By default, ``requests`` does not
|
||||
support this, but there is a separate package which does -
|
||||
``requests-toolbelt``. You should read `the toolbelt's documentation
|
||||
<https://toolbelt.rtfd.org>`_ for more details about how to use it.
|
||||
<https://toolbelt.readthedocs.org>`_ for more details about how to use it.
|
||||
|
||||
For sending multiple files in one request refer to the :ref:`advanced <advanced>`
|
||||
section.
|
||||
@@ -340,6 +355,15 @@ So, we can access the headers using any capitalization we want::
|
||||
>>> r.headers.get('content-type')
|
||||
'application/json'
|
||||
|
||||
It is also special in that the server could have sent the same header multiple
|
||||
times with different values, but requests combines them so they can be
|
||||
represented in the dictionary within a single mapping, as per
|
||||
`RFC 7230 <http://tools.ietf.org/html/rfc7230#section-3.2>`_:
|
||||
|
||||
> A recipient MAY combine multiple header fields with the same field name
|
||||
> into one "field-name: field-value" pair, without changing the semantics
|
||||
> of the message, by appending each subsequent field value to the combined
|
||||
> field value in order, separated by a comma.
|
||||
|
||||
Cookies
|
||||
-------
|
||||
|
||||
@@ -36,17 +36,17 @@ usage:
|
||||
The other HTTP methods are supported - see `requests.api`. Full documentation
|
||||
is at <http://python-requests.org>.
|
||||
|
||||
:copyright: (c) 2014 by Kenneth Reitz.
|
||||
:copyright: (c) 2015 by Kenneth Reitz.
|
||||
:license: Apache 2.0, see LICENSE for more details.
|
||||
|
||||
"""
|
||||
|
||||
__title__ = 'requests'
|
||||
__version__ = '2.5.1'
|
||||
__build__ = 0x020501
|
||||
__version__ = '2.6.1'
|
||||
__build__ = 0x020601
|
||||
__author__ = 'Kenneth Reitz'
|
||||
__license__ = 'Apache 2.0'
|
||||
__copyright__ = 'Copyright 2014 Kenneth Reitz'
|
||||
__copyright__ = 'Copyright 2015 Kenneth Reitz'
|
||||
|
||||
# Attempt to enable urllib3's SNI support, if possible
|
||||
try:
|
||||
|
||||
@@ -11,10 +11,10 @@ and maintain connections.
|
||||
import socket
|
||||
|
||||
from .models import Response
|
||||
from .packages.urllib3 import Retry
|
||||
from .packages.urllib3.poolmanager import PoolManager, proxy_from_url
|
||||
from .packages.urllib3.response import HTTPResponse
|
||||
from .packages.urllib3.util import Timeout as TimeoutSauce
|
||||
from .packages.urllib3.util.retry import Retry
|
||||
from .compat import urlparse, basestring
|
||||
from .utils import (DEFAULT_CA_BUNDLE_PATH, get_encoding_from_headers,
|
||||
prepend_scheme_if_needed, get_auth_from_url, urldefragauth)
|
||||
|
||||
@@ -16,7 +16,6 @@ from . import sessions
|
||||
|
||||
def request(method, url, **kwargs):
|
||||
"""Constructs and sends a :class:`Request <Request>`.
|
||||
Returns :class:`Response <Response>` object.
|
||||
|
||||
:param method: method for the new :class:`Request` object.
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
@@ -37,6 +36,8 @@ def request(method, url, **kwargs):
|
||||
:param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided.
|
||||
:param stream: (optional) if ``False``, the response content will be immediately downloaded.
|
||||
:param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
|
||||
Usage::
|
||||
|
||||
@@ -55,10 +56,12 @@ def request(method, url, **kwargs):
|
||||
|
||||
|
||||
def get(url, **kwargs):
|
||||
"""Sends a GET request. Returns :class:`Response` object.
|
||||
"""Sends a GET request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
kwargs.setdefault('allow_redirects', True)
|
||||
@@ -66,10 +69,12 @@ def get(url, **kwargs):
|
||||
|
||||
|
||||
def options(url, **kwargs):
|
||||
"""Sends a OPTIONS request. Returns :class:`Response` object.
|
||||
"""Sends a OPTIONS request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
kwargs.setdefault('allow_redirects', True)
|
||||
@@ -77,10 +82,12 @@ def options(url, **kwargs):
|
||||
|
||||
|
||||
def head(url, **kwargs):
|
||||
"""Sends a HEAD request. Returns :class:`Response` object.
|
||||
"""Sends a HEAD request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
kwargs.setdefault('allow_redirects', False)
|
||||
@@ -88,44 +95,52 @@ def head(url, **kwargs):
|
||||
|
||||
|
||||
def post(url, data=None, json=None, **kwargs):
|
||||
"""Sends a POST request. Returns :class:`Response` object.
|
||||
"""Sends a POST request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
|
||||
:param json: (optional) json data to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return request('post', url, data=data, json=json, **kwargs)
|
||||
|
||||
|
||||
def put(url, data=None, **kwargs):
|
||||
"""Sends a PUT request. Returns :class:`Response` object.
|
||||
"""Sends a PUT request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return request('put', url, data=data, **kwargs)
|
||||
|
||||
|
||||
def patch(url, data=None, **kwargs):
|
||||
"""Sends a PATCH request. Returns :class:`Response` object.
|
||||
"""Sends a PATCH request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return request('patch', url, data=data, **kwargs)
|
||||
|
||||
|
||||
def delete(url, **kwargs):
|
||||
"""Sends a DELETE request. Returns :class:`Response` object.
|
||||
"""Sends a DELETE request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return request('delete', url, **kwargs)
|
||||
|
||||
@@ -103,7 +103,8 @@ class HTTPDigestAuth(AuthBase):
|
||||
# XXX not implemented yet
|
||||
entdig = None
|
||||
p_parsed = urlparse(url)
|
||||
path = p_parsed.path
|
||||
#: path is request-uri defined in RFC 2616 which should not be empty
|
||||
path = p_parsed.path or "/"
|
||||
if p_parsed.query:
|
||||
path += '?' + p_parsed.query
|
||||
|
||||
@@ -124,13 +125,15 @@ class HTTPDigestAuth(AuthBase):
|
||||
s += os.urandom(8)
|
||||
|
||||
cnonce = (hashlib.sha1(s).hexdigest()[:16])
|
||||
noncebit = "%s:%s:%s:%s:%s" % (nonce, ncvalue, cnonce, qop, HA2)
|
||||
if _algorithm == 'MD5-SESS':
|
||||
HA1 = hash_utf8('%s:%s:%s' % (HA1, nonce, cnonce))
|
||||
|
||||
if qop is None:
|
||||
respdig = KD(HA1, "%s:%s" % (nonce, HA2))
|
||||
elif qop == 'auth' or 'auth' in qop.split(','):
|
||||
noncebit = "%s:%s:%s:%s:%s" % (
|
||||
nonce, ncvalue, cnonce, 'auth', HA2
|
||||
)
|
||||
respdig = KD(HA1, noncebit)
|
||||
else:
|
||||
# XXX handle auth-int.
|
||||
|
||||
@@ -21,58 +21,6 @@ is_py2 = (_ver[0] == 2)
|
||||
#: Python 3.x?
|
||||
is_py3 = (_ver[0] == 3)
|
||||
|
||||
#: Python 3.0.x
|
||||
is_py30 = (is_py3 and _ver[1] == 0)
|
||||
|
||||
#: Python 3.1.x
|
||||
is_py31 = (is_py3 and _ver[1] == 1)
|
||||
|
||||
#: Python 3.2.x
|
||||
is_py32 = (is_py3 and _ver[1] == 2)
|
||||
|
||||
#: Python 3.3.x
|
||||
is_py33 = (is_py3 and _ver[1] == 3)
|
||||
|
||||
#: Python 3.4.x
|
||||
is_py34 = (is_py3 and _ver[1] == 4)
|
||||
|
||||
#: Python 2.7.x
|
||||
is_py27 = (is_py2 and _ver[1] == 7)
|
||||
|
||||
#: Python 2.6.x
|
||||
is_py26 = (is_py2 and _ver[1] == 6)
|
||||
|
||||
#: Python 2.5.x
|
||||
is_py25 = (is_py2 and _ver[1] == 5)
|
||||
|
||||
#: Python 2.4.x
|
||||
is_py24 = (is_py2 and _ver[1] == 4) # I'm assuming this is not by choice.
|
||||
|
||||
|
||||
# ---------
|
||||
# Platforms
|
||||
# ---------
|
||||
|
||||
|
||||
# Syntax sugar.
|
||||
_ver = sys.version.lower()
|
||||
|
||||
is_pypy = ('pypy' in _ver)
|
||||
is_jython = ('jython' in _ver)
|
||||
is_ironpython = ('iron' in _ver)
|
||||
|
||||
# Assume CPython, if nothing else.
|
||||
is_cpython = not any((is_pypy, is_jython, is_ironpython))
|
||||
|
||||
# Windows-based system.
|
||||
is_windows = 'win32' in str(sys.platform).lower()
|
||||
|
||||
# Standard Linux 2+ system.
|
||||
is_linux = ('linux' in str(sys.platform).lower())
|
||||
is_osx = ('darwin' in str(sys.platform).lower())
|
||||
is_hpux = ('hpux' in str(sys.platform).lower()) # Complete guess.
|
||||
is_solaris = ('solar==' in str(sys.platform).lower()) # Complete guess.
|
||||
|
||||
try:
|
||||
import simplejson as json
|
||||
except (ImportError, SyntaxError):
|
||||
@@ -99,7 +47,6 @@ if is_py2:
|
||||
basestring = basestring
|
||||
numeric_types = (int, long, float)
|
||||
|
||||
|
||||
elif is_py3:
|
||||
from urllib.parse import urlparse, urlunparse, urljoin, urlsplit, urlencode, quote, unquote, quote_plus, unquote_plus, urldefrag
|
||||
from urllib.request import parse_http_list, getproxies, proxy_bypass
|
||||
|
||||
@@ -6,6 +6,7 @@ Compatibility code to be able to use `cookielib.CookieJar` with requests.
|
||||
requests.utils imports from here, so be careful with imports.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import time
|
||||
import collections
|
||||
from .compat import cookielib, urlparse, urlunparse, Morsel
|
||||
@@ -157,26 +158,28 @@ class CookieConflictError(RuntimeError):
|
||||
|
||||
|
||||
class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping):
|
||||
"""Compatibility class; is a cookielib.CookieJar, but exposes a dict interface.
|
||||
"""Compatibility class; is a cookielib.CookieJar, but exposes a dict
|
||||
interface.
|
||||
|
||||
This is the CookieJar we create by default for requests and sessions that
|
||||
don't specify one, since some clients may expect response.cookies and
|
||||
session.cookies to support dict operations.
|
||||
|
||||
Don't use the dict interface internally; it's just for compatibility with
|
||||
with external client code. All `requests` code should work out of the box
|
||||
with externally provided instances of CookieJar, e.g., LWPCookieJar and
|
||||
FileCookieJar.
|
||||
|
||||
Caution: dictionary operations that are normally O(1) may be O(n).
|
||||
Requests does not use the dict interface internally; it's just for
|
||||
compatibility with external client code. All requests code should work
|
||||
out of the box with externally provided instances of ``CookieJar``, e.g.
|
||||
``LWPCookieJar`` and ``FileCookieJar``.
|
||||
|
||||
Unlike a regular CookieJar, this class is pickleable.
|
||||
"""
|
||||
|
||||
.. warning:: dictionary operations that are normally O(1) may be O(n).
|
||||
"""
|
||||
def get(self, name, default=None, domain=None, path=None):
|
||||
"""Dict-like get() that also supports optional domain and path args in
|
||||
order to resolve naming collisions from using one cookie jar over
|
||||
multiple domains. Caution: operation is O(n), not O(1)."""
|
||||
multiple domains.
|
||||
|
||||
.. warning:: operation is O(n), not O(1)."""
|
||||
try:
|
||||
return self._find_no_duplicates(name, domain, path)
|
||||
except KeyError:
|
||||
@@ -199,37 +202,38 @@ class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping):
|
||||
return c
|
||||
|
||||
def iterkeys(self):
|
||||
"""Dict-like iterkeys() that returns an iterator of names of cookies from the jar.
|
||||
See itervalues() and iteritems()."""
|
||||
"""Dict-like iterkeys() that returns an iterator of names of cookies
|
||||
from the jar. See itervalues() and iteritems()."""
|
||||
for cookie in iter(self):
|
||||
yield cookie.name
|
||||
|
||||
def keys(self):
|
||||
"""Dict-like keys() that returns a list of names of cookies from the jar.
|
||||
See values() and items()."""
|
||||
"""Dict-like keys() that returns a list of names of cookies from the
|
||||
jar. See values() and items()."""
|
||||
return list(self.iterkeys())
|
||||
|
||||
def itervalues(self):
|
||||
"""Dict-like itervalues() that returns an iterator of values of cookies from the jar.
|
||||
See iterkeys() and iteritems()."""
|
||||
"""Dict-like itervalues() that returns an iterator of values of cookies
|
||||
from the jar. See iterkeys() and iteritems()."""
|
||||
for cookie in iter(self):
|
||||
yield cookie.value
|
||||
|
||||
def values(self):
|
||||
"""Dict-like values() that returns a list of values of cookies from the jar.
|
||||
See keys() and items()."""
|
||||
"""Dict-like values() that returns a list of values of cookies from the
|
||||
jar. See keys() and items()."""
|
||||
return list(self.itervalues())
|
||||
|
||||
def iteritems(self):
|
||||
"""Dict-like iteritems() that returns an iterator of name-value tuples from the jar.
|
||||
See iterkeys() and itervalues()."""
|
||||
"""Dict-like iteritems() that returns an iterator of name-value tuples
|
||||
from the jar. See iterkeys() and itervalues()."""
|
||||
for cookie in iter(self):
|
||||
yield cookie.name, cookie.value
|
||||
|
||||
def items(self):
|
||||
"""Dict-like items() that returns a list of name-value tuples from the jar.
|
||||
See keys() and values(). Allows client-code to call "dict(RequestsCookieJar)
|
||||
and get a vanilla python dict of key value pairs."""
|
||||
"""Dict-like items() that returns a list of name-value tuples from the
|
||||
jar. See keys() and values(). Allows client-code to call
|
||||
``dict(RequestsCookieJar)`` and get a vanilla python dict of key value
|
||||
pairs."""
|
||||
return list(self.iteritems())
|
||||
|
||||
def list_domains(self):
|
||||
@@ -259,8 +263,9 @@ class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping):
|
||||
return False # there is only one domain in jar
|
||||
|
||||
def get_dict(self, domain=None, path=None):
|
||||
"""Takes as an argument an optional domain and path and returns a plain old
|
||||
Python dict of name-value pairs of cookies that meet the requirements."""
|
||||
"""Takes as an argument an optional domain and path and returns a plain
|
||||
old Python dict of name-value pairs of cookies that meet the
|
||||
requirements."""
|
||||
dictionary = {}
|
||||
for cookie in iter(self):
|
||||
if (domain is None or cookie.domain == domain) and (path is None
|
||||
@@ -269,21 +274,24 @@ class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping):
|
||||
return dictionary
|
||||
|
||||
def __getitem__(self, name):
|
||||
"""Dict-like __getitem__() for compatibility with client code. Throws exception
|
||||
if there are more than one cookie with name. In that case, use the more
|
||||
explicit get() method instead. Caution: operation is O(n), not O(1)."""
|
||||
"""Dict-like __getitem__() for compatibility with client code. Throws
|
||||
exception if there are more than one cookie with name. In that case,
|
||||
use the more explicit get() method instead.
|
||||
|
||||
.. warning:: operation is O(n), not O(1)."""
|
||||
|
||||
return self._find_no_duplicates(name)
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
"""Dict-like __setitem__ for compatibility with client code. Throws exception
|
||||
if there is already a cookie of that name in the jar. In that case, use the more
|
||||
explicit set() method instead."""
|
||||
"""Dict-like __setitem__ for compatibility with client code. Throws
|
||||
exception if there is already a cookie of that name in the jar. In that
|
||||
case, use the more explicit set() method instead."""
|
||||
|
||||
self.set(name, value)
|
||||
|
||||
def __delitem__(self, name):
|
||||
"""Deletes a cookie given a name. Wraps cookielib.CookieJar's remove_cookie_by_name()."""
|
||||
"""Deletes a cookie given a name. Wraps ``cookielib.CookieJar``'s
|
||||
``remove_cookie_by_name()``."""
|
||||
remove_cookie_by_name(self, name)
|
||||
|
||||
def set_cookie(self, cookie, *args, **kwargs):
|
||||
@@ -295,15 +303,16 @@ class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping):
|
||||
"""Updates this jar with cookies from another CookieJar or dict-like"""
|
||||
if isinstance(other, cookielib.CookieJar):
|
||||
for cookie in other:
|
||||
self.set_cookie(cookie)
|
||||
self.set_cookie(copy.copy(cookie))
|
||||
else:
|
||||
super(RequestsCookieJar, self).update(other)
|
||||
|
||||
def _find(self, name, domain=None, path=None):
|
||||
"""Requests uses this method internally to get cookie values. Takes as args name
|
||||
and optional domain and path. Returns a cookie.value. If there are conflicting cookies,
|
||||
_find arbitrarily chooses one. See _find_no_duplicates if you want an exception thrown
|
||||
if there are conflicting cookies."""
|
||||
"""Requests uses this method internally to get cookie values. Takes as
|
||||
args name and optional domain and path. Returns a cookie.value. If
|
||||
there are conflicting cookies, _find arbitrarily chooses one. See
|
||||
_find_no_duplicates if you want an exception thrown if there are
|
||||
conflicting cookies."""
|
||||
for cookie in iter(self):
|
||||
if cookie.name == name:
|
||||
if domain is None or cookie.domain == domain:
|
||||
@@ -313,10 +322,11 @@ class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping):
|
||||
raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path))
|
||||
|
||||
def _find_no_duplicates(self, name, domain=None, path=None):
|
||||
"""__get_item__ and get call _find_no_duplicates -- never used in Requests internally.
|
||||
Takes as args name and optional domain and path. Returns a cookie.value.
|
||||
Throws KeyError if cookie is not found and CookieConflictError if there are
|
||||
multiple cookies that match name and optionally domain and path."""
|
||||
"""Both ``__get_item__`` and ``get`` call this function: it's never
|
||||
used elsewhere in Requests. Takes as args name and optional domain and
|
||||
path. Returns a cookie.value. Throws KeyError if cookie is not found
|
||||
and CookieConflictError if there are multiple cookies that match name
|
||||
and optionally domain and path."""
|
||||
toReturn = None
|
||||
for cookie in iter(self):
|
||||
if cookie.name == name:
|
||||
@@ -350,6 +360,21 @@ class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping):
|
||||
return new_cj
|
||||
|
||||
|
||||
def _copy_cookie_jar(jar):
|
||||
if jar is None:
|
||||
return None
|
||||
|
||||
if hasattr(jar, 'copy'):
|
||||
# We're dealing with an instane of RequestsCookieJar
|
||||
return jar.copy()
|
||||
# We're dealing with a generic CookieJar instance
|
||||
new_jar = copy.copy(jar)
|
||||
new_jar.clear()
|
||||
for cookie in jar:
|
||||
new_jar.set_cookie(copy.copy(cookie))
|
||||
return new_jar
|
||||
|
||||
|
||||
def create_cookie(name, value, **kwargs):
|
||||
"""Make a cookie from underspecified parameters.
|
||||
|
||||
@@ -440,7 +465,7 @@ def merge_cookies(cookiejar, cookies):
|
||||
"""
|
||||
if not isinstance(cookiejar, cookielib.CookieJar):
|
||||
raise ValueError('You can only merge into CookieJar')
|
||||
|
||||
|
||||
if isinstance(cookies, dict):
|
||||
cookiejar = cookiejar_from_dict(
|
||||
cookies, cookiejar=cookiejar, overwrite=False)
|
||||
|
||||
@@ -15,7 +15,7 @@ from .hooks import default_hooks
|
||||
from .structures import CaseInsensitiveDict
|
||||
|
||||
from .auth import HTTPBasicAuth
|
||||
from .cookies import cookiejar_from_dict, get_cookie_header
|
||||
from .cookies import cookiejar_from_dict, get_cookie_header, _copy_cookie_jar
|
||||
from .packages.urllib3.fields import RequestField
|
||||
from .packages.urllib3.filepost import encode_multipart_formdata
|
||||
from .packages.urllib3.util import parse_url
|
||||
@@ -143,12 +143,13 @@ class RequestEncodingMixin(object):
|
||||
else:
|
||||
fn = guess_filename(v) or k
|
||||
fp = v
|
||||
if isinstance(fp, str):
|
||||
fp = StringIO(fp)
|
||||
if isinstance(fp, bytes):
|
||||
fp = BytesIO(fp)
|
||||
|
||||
rf = RequestField(name=k, data=fp.read(),
|
||||
if isinstance(fp, (str, bytes, bytearray)):
|
||||
fdata = fp
|
||||
else:
|
||||
fdata = fp.read()
|
||||
|
||||
rf = RequestField(name=k, data=fdata,
|
||||
filename=fn, headers=fh)
|
||||
rf.make_multipart(content_type=ft)
|
||||
new_fields.append(rf)
|
||||
@@ -319,7 +320,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
p.method = self.method
|
||||
p.url = self.url
|
||||
p.headers = self.headers.copy() if self.headers is not None else None
|
||||
p._cookies = self._cookies.copy() if self._cookies is not None else None
|
||||
p._cookies = _copy_cookie_jar(self._cookies)
|
||||
p.body = self.body
|
||||
p.hooks = self.hooks
|
||||
return p
|
||||
@@ -357,7 +358,8 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
|
||||
if not scheme:
|
||||
raise MissingSchema("Invalid URL {0!r}: No schema supplied. "
|
||||
"Perhaps you meant http://{0}?".format(url))
|
||||
"Perhaps you meant http://{0}?".format(
|
||||
to_native_string(url, 'utf8')))
|
||||
|
||||
if not host:
|
||||
raise InvalidURL("Invalid URL %r: No host supplied" % url)
|
||||
@@ -500,7 +502,15 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
self.prepare_content_length(self.body)
|
||||
|
||||
def prepare_cookies(self, cookies):
|
||||
"""Prepares the given HTTP cookie data."""
|
||||
"""Prepares the given HTTP cookie data.
|
||||
|
||||
This function eventually generates a ``Cookie`` header from the
|
||||
given cookies using cookielib. Due to cookielib's design, the header
|
||||
will not be regenerated if it already exists, meaning this function
|
||||
can only be called once for the life of the
|
||||
:class:`PreparedRequest <PreparedRequest>` object. Any subsequent calls
|
||||
to ``prepare_cookies`` will have no actual effect, unless the "Cookie"
|
||||
header is removed beforehand."""
|
||||
|
||||
if isinstance(cookies, cookielib.CookieJar):
|
||||
self._cookies = cookies
|
||||
@@ -513,6 +523,10 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
|
||||
def prepare_hooks(self, hooks):
|
||||
"""Prepares the given hooks."""
|
||||
# hooks can be passed as None to the prepare method and to this
|
||||
# method. To prevent iterating over None, simply use an empty list
|
||||
# if hooks is False-y
|
||||
hooks = hooks or []
|
||||
for event in hooks:
|
||||
self.register_hook(event, hooks[event])
|
||||
|
||||
@@ -572,7 +586,11 @@ class Response(object):
|
||||
self.cookies = cookiejar_from_dict({})
|
||||
|
||||
#: The amount of time elapsed between sending the request
|
||||
#: and the arrival of the response (as a timedelta)
|
||||
#: and the arrival of the response (as a timedelta).
|
||||
#: This property specifically measures the time taken between sending
|
||||
#: the first byte of the request and finishing parsing the headers. It
|
||||
#: is therefore unaffected by consuming the response content or the
|
||||
#: value of the ``stream`` keyword argument.
|
||||
self.elapsed = datetime.timedelta(0)
|
||||
|
||||
#: The :class:`PreparedRequest <PreparedRequest>` object to which this
|
||||
@@ -688,6 +706,8 @@ class Response(object):
|
||||
"""Iterates over the response data, one line at a time. When
|
||||
stream=True is set on the request, this avoids reading the
|
||||
content at once into memory for large responses.
|
||||
|
||||
.. note:: This method is not reentrant safe.
|
||||
"""
|
||||
|
||||
pending = None
|
||||
|
||||
@@ -4,7 +4,7 @@ urllib3 - Thread-safe connection pooling and re-using.
|
||||
|
||||
__author__ = 'Andrey Petrov (andrey.petrov@shazow.net)'
|
||||
__license__ = 'MIT'
|
||||
__version__ = 'dev'
|
||||
__version__ = '1.10.3'
|
||||
|
||||
|
||||
from .connectionpool import (
|
||||
@@ -55,9 +55,11 @@ def add_stderr_logger(level=logging.DEBUG):
|
||||
del NullHandler
|
||||
|
||||
|
||||
# Set security warning to only go off once by default.
|
||||
import warnings
|
||||
# SecurityWarning's always go off by default.
|
||||
warnings.simplefilter('always', exceptions.SecurityWarning)
|
||||
# InsecurePlatformWarning's don't vary between requests, so we keep it default.
|
||||
warnings.simplefilter('default', exceptions.InsecurePlatformWarning)
|
||||
|
||||
def disable_warnings(category=exceptions.HTTPWarning):
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from collections import Mapping, MutableMapping
|
||||
try:
|
||||
from threading import RLock
|
||||
except ImportError: # Platform-specific: No threads available
|
||||
except ImportError: # Platform-specific: No threads available
|
||||
class RLock:
|
||||
def __enter__(self):
|
||||
pass
|
||||
@@ -10,11 +10,11 @@ except ImportError: # Platform-specific: No threads available
|
||||
pass
|
||||
|
||||
|
||||
try: # Python 2.7+
|
||||
try: # Python 2.7+
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from .packages.ordered_dict import OrderedDict
|
||||
from .packages.six import iterkeys, itervalues
|
||||
from .packages.six import iterkeys, itervalues, PY3
|
||||
|
||||
|
||||
__all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict']
|
||||
@@ -97,7 +97,14 @@ class RecentlyUsedContainer(MutableMapping):
|
||||
return list(iterkeys(self._container))
|
||||
|
||||
|
||||
class HTTPHeaderDict(MutableMapping):
|
||||
_dict_setitem = dict.__setitem__
|
||||
_dict_getitem = dict.__getitem__
|
||||
_dict_delitem = dict.__delitem__
|
||||
_dict_contains = dict.__contains__
|
||||
_dict_setdefault = dict.setdefault
|
||||
|
||||
|
||||
class HTTPHeaderDict(dict):
|
||||
"""
|
||||
:param headers:
|
||||
An iterable of field-value pairs. Must not contain multiple field names
|
||||
@@ -129,25 +136,75 @@ class HTTPHeaderDict(MutableMapping):
|
||||
'foo=bar, baz=quxx'
|
||||
>>> headers['Content-Length']
|
||||
'7'
|
||||
|
||||
If you want to access the raw headers with their original casing
|
||||
for debugging purposes you can access the private ``._data`` attribute
|
||||
which is a normal python ``dict`` that maps the case-insensitive key to a
|
||||
list of tuples stored as (case-sensitive-original-name, value). Using the
|
||||
structure from above as our example:
|
||||
|
||||
>>> headers._data
|
||||
{'set-cookie': [('Set-Cookie', 'foo=bar'), ('set-cookie', 'baz=quxx')],
|
||||
'content-length': [('content-length', '7')]}
|
||||
"""
|
||||
|
||||
def __init__(self, headers=None, **kwargs):
|
||||
self._data = {}
|
||||
if headers is None:
|
||||
headers = {}
|
||||
self.update(headers, **kwargs)
|
||||
dict.__init__(self)
|
||||
if headers is not None:
|
||||
if isinstance(headers, HTTPHeaderDict):
|
||||
self._copy_from(headers)
|
||||
else:
|
||||
self.extend(headers)
|
||||
if kwargs:
|
||||
self.extend(kwargs)
|
||||
|
||||
def add(self, key, value):
|
||||
def __setitem__(self, key, val):
|
||||
return _dict_setitem(self, key.lower(), (key, val))
|
||||
|
||||
def __getitem__(self, key):
|
||||
val = _dict_getitem(self, key.lower())
|
||||
return ', '.join(val[1:])
|
||||
|
||||
def __delitem__(self, key):
|
||||
return _dict_delitem(self, key.lower())
|
||||
|
||||
def __contains__(self, key):
|
||||
return _dict_contains(self, key.lower())
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Mapping) and not hasattr(other, 'keys'):
|
||||
return False
|
||||
if not isinstance(other, type(self)):
|
||||
other = type(self)(other)
|
||||
return dict((k1, self[k1]) for k1 in self) == dict((k2, other[k2]) for k2 in other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
values = MutableMapping.values
|
||||
get = MutableMapping.get
|
||||
update = MutableMapping.update
|
||||
|
||||
if not PY3: # Python 2
|
||||
iterkeys = MutableMapping.iterkeys
|
||||
itervalues = MutableMapping.itervalues
|
||||
|
||||
__marker = object()
|
||||
|
||||
def pop(self, key, default=__marker):
|
||||
'''D.pop(k[,d]) -> v, remove specified key and return the corresponding value.
|
||||
If key is not found, d is returned if given, otherwise KeyError is raised.
|
||||
'''
|
||||
# Using the MutableMapping function directly fails due to the private marker.
|
||||
# Using ordinary dict.pop would expose the internal structures.
|
||||
# So let's reinvent the wheel.
|
||||
try:
|
||||
value = self[key]
|
||||
except KeyError:
|
||||
if default is self.__marker:
|
||||
raise
|
||||
return default
|
||||
else:
|
||||
del self[key]
|
||||
return value
|
||||
|
||||
def discard(self, key):
|
||||
try:
|
||||
del self[key]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def add(self, key, val):
|
||||
"""Adds a (name, value) pair, doesn't overwrite the value if it already
|
||||
exists.
|
||||
|
||||
@@ -156,43 +213,111 @@ class HTTPHeaderDict(MutableMapping):
|
||||
>>> headers['foo']
|
||||
'bar, baz'
|
||||
"""
|
||||
self._data.setdefault(key.lower(), []).append((key, value))
|
||||
key_lower = key.lower()
|
||||
new_vals = key, val
|
||||
# Keep the common case aka no item present as fast as possible
|
||||
vals = _dict_setdefault(self, key_lower, new_vals)
|
||||
if new_vals is not vals:
|
||||
# new_vals was not inserted, as there was a previous one
|
||||
if isinstance(vals, list):
|
||||
# If already several items got inserted, we have a list
|
||||
vals.append(val)
|
||||
else:
|
||||
# vals should be a tuple then, i.e. only one item so far
|
||||
# Need to convert the tuple to list for further extension
|
||||
_dict_setitem(self, key_lower, [vals[0], vals[1], val])
|
||||
|
||||
def extend(self, *args, **kwargs):
|
||||
"""Generic import function for any type of header-like object.
|
||||
Adapted version of MutableMapping.update in order to insert items
|
||||
with self.add instead of self.__setitem__
|
||||
"""
|
||||
if len(args) > 1:
|
||||
raise TypeError("extend() takes at most 1 positional "
|
||||
"arguments ({} given)".format(len(args)))
|
||||
other = args[0] if len(args) >= 1 else ()
|
||||
|
||||
if isinstance(other, HTTPHeaderDict):
|
||||
for key, val in other.iteritems():
|
||||
self.add(key, val)
|
||||
elif isinstance(other, Mapping):
|
||||
for key in other:
|
||||
self.add(key, other[key])
|
||||
elif hasattr(other, "keys"):
|
||||
for key in other.keys():
|
||||
self.add(key, other[key])
|
||||
else:
|
||||
for key, value in other:
|
||||
self.add(key, value)
|
||||
|
||||
for key, value in kwargs.items():
|
||||
self.add(key, value)
|
||||
|
||||
def getlist(self, key):
|
||||
"""Returns a list of all the values for the named field. Returns an
|
||||
empty list if the key doesn't exist."""
|
||||
return self[key].split(', ') if key in self else []
|
||||
try:
|
||||
vals = _dict_getitem(self, key.lower())
|
||||
except KeyError:
|
||||
return []
|
||||
else:
|
||||
if isinstance(vals, tuple):
|
||||
return [vals[1]]
|
||||
else:
|
||||
return vals[1:]
|
||||
|
||||
def copy(self):
|
||||
h = HTTPHeaderDict()
|
||||
for key in self._data:
|
||||
for rawkey, value in self._data[key]:
|
||||
h.add(rawkey, value)
|
||||
return h
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Mapping):
|
||||
return False
|
||||
other = HTTPHeaderDict(other)
|
||||
return dict((k1, self[k1]) for k1 in self._data) == \
|
||||
dict((k2, other[k2]) for k2 in other._data)
|
||||
|
||||
def __getitem__(self, key):
|
||||
values = self._data[key.lower()]
|
||||
return ', '.join(value[1] for value in values)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._data[key.lower()] = [(key, value)]
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self._data[key.lower()]
|
||||
|
||||
def __len__(self):
|
||||
return len(self._data)
|
||||
|
||||
def __iter__(self):
|
||||
for headers in itervalues(self._data):
|
||||
yield headers[0][0]
|
||||
# Backwards compatibility for httplib
|
||||
getheaders = getlist
|
||||
getallmatchingheaders = getlist
|
||||
iget = getlist
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r)' % (self.__class__.__name__, dict(self.items()))
|
||||
return "%s(%s)" % (type(self).__name__, dict(self.itermerged()))
|
||||
|
||||
def _copy_from(self, other):
|
||||
for key in other:
|
||||
val = _dict_getitem(other, key)
|
||||
if isinstance(val, list):
|
||||
# Don't need to convert tuples
|
||||
val = list(val)
|
||||
_dict_setitem(self, key, val)
|
||||
|
||||
def copy(self):
|
||||
clone = type(self)()
|
||||
clone._copy_from(self)
|
||||
return clone
|
||||
|
||||
def iteritems(self):
|
||||
"""Iterate over all header lines, including duplicate ones."""
|
||||
for key in self:
|
||||
vals = _dict_getitem(self, key)
|
||||
for val in vals[1:]:
|
||||
yield vals[0], val
|
||||
|
||||
def itermerged(self):
|
||||
"""Iterate over all headers, merging duplicate ones together."""
|
||||
for key in self:
|
||||
val = _dict_getitem(self, key)
|
||||
yield val[0], ', '.join(val[1:])
|
||||
|
||||
def items(self):
|
||||
return list(self.iteritems())
|
||||
|
||||
@classmethod
|
||||
def from_httplib(cls, message): # Python 2
|
||||
"""Read headers from a Python 2 httplib message object."""
|
||||
# python2.7 does not expose a proper API for exporting multiheaders
|
||||
# efficiently. This function re-reads raw lines from the message
|
||||
# object and extracts the multiheaders properly.
|
||||
headers = []
|
||||
|
||||
for line in message.headers:
|
||||
if line.startswith((' ', '\t')):
|
||||
key, value = headers[-1]
|
||||
headers[-1] = (key, value + '\r\n' + line.rstrip())
|
||||
continue
|
||||
|
||||
key, value = line.split(':', 1)
|
||||
headers.append((key, value.strip()))
|
||||
|
||||
return cls(headers)
|
||||
|
||||
@@ -260,3 +260,5 @@ if ssl:
|
||||
# Make a copy for testing.
|
||||
UnverifiedHTTPSConnection = HTTPSConnection
|
||||
HTTPSConnection = VerifiedHTTPSConnection
|
||||
else:
|
||||
HTTPSConnection = DummyConnection
|
||||
|
||||
@@ -72,6 +72,21 @@ class ConnectionPool(object):
|
||||
return '%s(host=%r, port=%r)' % (type(self).__name__,
|
||||
self.host, self.port)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
# Return False to re-raise any potential exceptions
|
||||
return False
|
||||
|
||||
def close():
|
||||
"""
|
||||
Close all pooled connections and disable the pool.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252
|
||||
_blocking_errnos = set([errno.EAGAIN, errno.EWOULDBLOCK])
|
||||
|
||||
@@ -266,6 +281,10 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
"""
|
||||
pass
|
||||
|
||||
def _prepare_proxy(self, conn):
|
||||
# Nothing to do for HTTP connections.
|
||||
pass
|
||||
|
||||
def _get_timeout(self, timeout):
|
||||
""" Helper that always returns a :class:`urllib3.util.Timeout` """
|
||||
if timeout is _Default:
|
||||
@@ -349,7 +368,7 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
|
||||
# Receive the response from the server
|
||||
try:
|
||||
try: # Python 2.7+, use buffering of HTTP responses
|
||||
try: # Python 2.7, use buffering of HTTP responses
|
||||
httplib_response = conn.getresponse(buffering=True)
|
||||
except TypeError: # Python 2.6 and older
|
||||
httplib_response = conn.getresponse()
|
||||
@@ -510,11 +529,18 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
|
||||
try:
|
||||
# Request a connection from the queue.
|
||||
timeout_obj = self._get_timeout(timeout)
|
||||
conn = self._get_conn(timeout=pool_timeout)
|
||||
|
||||
conn.timeout = timeout_obj.connect_timeout
|
||||
|
||||
is_new_proxy_conn = self.proxy is not None and not getattr(conn, 'sock', None)
|
||||
if is_new_proxy_conn:
|
||||
self._prepare_proxy(conn)
|
||||
|
||||
# Make the request on the httplib connection object.
|
||||
httplib_response = self._make_request(conn, method, url,
|
||||
timeout=timeout,
|
||||
timeout=timeout_obj,
|
||||
body=body, headers=headers)
|
||||
|
||||
# If we're going to release the connection in ``finally:``, then
|
||||
@@ -547,6 +573,14 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
conn = None
|
||||
raise SSLError(e)
|
||||
|
||||
except SSLError:
|
||||
# Treat SSLError separately from BaseSSLError to preserve
|
||||
# traceback.
|
||||
if conn:
|
||||
conn.close()
|
||||
conn = None
|
||||
raise
|
||||
|
||||
except (TimeoutError, HTTPException, SocketError, ConnectionError) as e:
|
||||
if conn:
|
||||
# Discard the connection for these exceptions. It will be
|
||||
@@ -554,14 +588,13 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
conn.close()
|
||||
conn = None
|
||||
|
||||
stacktrace = sys.exc_info()[2]
|
||||
if isinstance(e, SocketError) and self.proxy:
|
||||
e = ProxyError('Cannot connect to proxy.', e)
|
||||
elif isinstance(e, (SocketError, HTTPException)):
|
||||
e = ProtocolError('Connection aborted.', e)
|
||||
|
||||
retries = retries.increment(method, url, error=e,
|
||||
_pool=self, _stacktrace=stacktrace)
|
||||
retries = retries.increment(method, url, error=e, _pool=self,
|
||||
_stacktrace=sys.exc_info()[2])
|
||||
retries.sleep()
|
||||
|
||||
# Keep track of the error for the retry warning.
|
||||
@@ -673,24 +706,26 @@ class HTTPSConnectionPool(HTTPConnectionPool):
|
||||
assert_fingerprint=self.assert_fingerprint)
|
||||
conn.ssl_version = self.ssl_version
|
||||
|
||||
if self.proxy is not None:
|
||||
# Python 2.7+
|
||||
try:
|
||||
set_tunnel = conn.set_tunnel
|
||||
except AttributeError: # Platform-specific: Python 2.6
|
||||
set_tunnel = conn._set_tunnel
|
||||
|
||||
if sys.version_info <= (2, 6, 4) and not self.proxy_headers: # Python 2.6.4 and older
|
||||
set_tunnel(self.host, self.port)
|
||||
else:
|
||||
set_tunnel(self.host, self.port, self.proxy_headers)
|
||||
|
||||
# Establish tunnel connection early, because otherwise httplib
|
||||
# would improperly set Host: header to proxy's IP:port.
|
||||
conn.connect()
|
||||
|
||||
return conn
|
||||
|
||||
def _prepare_proxy(self, conn):
|
||||
"""
|
||||
Establish tunnel connection early, because otherwise httplib
|
||||
would improperly set Host: header to proxy's IP:port.
|
||||
"""
|
||||
# Python 2.7+
|
||||
try:
|
||||
set_tunnel = conn.set_tunnel
|
||||
except AttributeError: # Platform-specific: Python 2.6
|
||||
set_tunnel = conn._set_tunnel
|
||||
|
||||
if sys.version_info <= (2, 6, 4) and not self.proxy_headers: # Python 2.6.4 and older
|
||||
set_tunnel(self.host, self.port)
|
||||
else:
|
||||
set_tunnel(self.host, self.port, self.proxy_headers)
|
||||
|
||||
conn.connect()
|
||||
|
||||
def _new_conn(self):
|
||||
"""
|
||||
Return a fresh :class:`httplib.HTTPSConnection`.
|
||||
@@ -700,7 +735,6 @@ class HTTPSConnectionPool(HTTPConnectionPool):
|
||||
% (self.num_connections, self.host))
|
||||
|
||||
if not self.ConnectionCls or self.ConnectionCls is DummyConnection:
|
||||
# Platform-specific: Python without ssl
|
||||
raise SSLError("Can't connect to HTTPS URL because the SSL "
|
||||
"module is not available.")
|
||||
|
||||
|
||||
@@ -38,8 +38,6 @@ Module Variables
|
||||
----------------
|
||||
|
||||
:var DEFAULT_SSL_CIPHER_LIST: The list of supported SSL/TLS cipher suites.
|
||||
Default: ``ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:
|
||||
ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS``
|
||||
|
||||
.. _sni: https://en.wikipedia.org/wiki/Server_Name_Indication
|
||||
.. _crime attack: https://en.wikipedia.org/wiki/CRIME_(security_exploit)
|
||||
@@ -85,22 +83,7 @@ _openssl_verify = {
|
||||
+ OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
|
||||
}
|
||||
|
||||
# A secure default.
|
||||
# Sources for more information on TLS ciphers:
|
||||
#
|
||||
# - https://wiki.mozilla.org/Security/Server_Side_TLS
|
||||
# - https://www.ssllabs.com/projects/best-practices/index.html
|
||||
# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
|
||||
#
|
||||
# The general intent is:
|
||||
# - Prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE),
|
||||
# - prefer ECDHE over DHE for better performance,
|
||||
# - prefer any AES-GCM over any AES-CBC for better performance and security,
|
||||
# - use 3DES as fallback which is secure but slow,
|
||||
# - disable NULL authentication, MD5 MACs and DSS for security reasons.
|
||||
DEFAULT_SSL_CIPHER_LIST = "ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:" + \
|
||||
"ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:" + \
|
||||
"!aNULL:!MD5:!DSS"
|
||||
DEFAULT_SSL_CIPHER_LIST = util.ssl_.DEFAULT_CIPHERS
|
||||
|
||||
|
||||
orig_util_HAS_SNI = util.HAS_SNI
|
||||
@@ -191,6 +174,11 @@ class WrappedSocket(object):
|
||||
return b''
|
||||
else:
|
||||
raise
|
||||
except OpenSSL.SSL.ZeroReturnError as e:
|
||||
if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:
|
||||
return b''
|
||||
else:
|
||||
raise
|
||||
except OpenSSL.SSL.WantReadError:
|
||||
rd, wd, ed = select.select(
|
||||
[self.socket], [], [], self.socket.gettimeout())
|
||||
@@ -294,7 +282,9 @@ def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
|
||||
try:
|
||||
cnx.do_handshake()
|
||||
except OpenSSL.SSL.WantReadError:
|
||||
select.select([sock], [], [])
|
||||
rd, _, _ = select.select([sock], [], [], sock.gettimeout())
|
||||
if not rd:
|
||||
raise timeout('select timed out')
|
||||
continue
|
||||
except OpenSSL.SSL.Error as e:
|
||||
raise ssl.SSLError('bad handshake', e)
|
||||
|
||||
@@ -157,3 +157,13 @@ class InsecureRequestWarning(SecurityWarning):
|
||||
class SystemTimeWarning(SecurityWarning):
|
||||
"Warned when system time is suspected to be wrong"
|
||||
pass
|
||||
|
||||
|
||||
class InsecurePlatformWarning(SecurityWarning):
|
||||
"Warned when certain SSL configuration is not available on a platform."
|
||||
pass
|
||||
|
||||
|
||||
class ResponseNotChunked(ProtocolError, ValueError):
|
||||
"Response needs to be chunked in order to read it as chunks."
|
||||
pass
|
||||
|
||||
@@ -8,7 +8,7 @@ except ImportError:
|
||||
from ._collections import RecentlyUsedContainer
|
||||
from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool
|
||||
from .connectionpool import port_by_scheme
|
||||
from .exceptions import LocationValueError
|
||||
from .exceptions import LocationValueError, MaxRetryError
|
||||
from .request import RequestMethods
|
||||
from .util.url import parse_url
|
||||
from .util.retry import Retry
|
||||
@@ -64,6 +64,14 @@ class PoolManager(RequestMethods):
|
||||
self.pools = RecentlyUsedContainer(num_pools,
|
||||
dispose_func=lambda p: p.close())
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.clear()
|
||||
# Return False to re-raise any potential exceptions
|
||||
return False
|
||||
|
||||
def _new_pool(self, scheme, host, port):
|
||||
"""
|
||||
Create a new :class:`ConnectionPool` based on host, port and scheme.
|
||||
@@ -167,7 +175,14 @@ class PoolManager(RequestMethods):
|
||||
if not isinstance(retries, Retry):
|
||||
retries = Retry.from_int(retries, redirect=redirect)
|
||||
|
||||
kw['retries'] = retries.increment(method, redirect_location)
|
||||
try:
|
||||
retries = retries.increment(method, url, response=response, _pool=conn)
|
||||
except MaxRetryError:
|
||||
if retries.raise_on_redirect:
|
||||
raise
|
||||
return response
|
||||
|
||||
kw['retries'] = retries
|
||||
kw['redirect'] = redirect
|
||||
|
||||
log.info("Redirecting %s -> %s" % (url, redirect_location))
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
try:
|
||||
import http.client as httplib
|
||||
except ImportError:
|
||||
import httplib
|
||||
import zlib
|
||||
import io
|
||||
from socket import timeout as SocketTimeout
|
||||
|
||||
from ._collections import HTTPHeaderDict
|
||||
from .exceptions import ProtocolError, DecodeError, ReadTimeoutError
|
||||
from .packages.six import string_types as basestring, binary_type
|
||||
from .exceptions import (
|
||||
ProtocolError, DecodeError, ReadTimeoutError, ResponseNotChunked
|
||||
)
|
||||
from .packages.six import string_types as basestring, binary_type, PY3
|
||||
from .connection import HTTPException, BaseSSLError
|
||||
from .util.response import is_fp_closed
|
||||
|
||||
|
||||
|
||||
class DeflateDecoder(object):
|
||||
|
||||
def __init__(self):
|
||||
@@ -21,6 +26,9 @@ class DeflateDecoder(object):
|
||||
return getattr(self._obj, name)
|
||||
|
||||
def decompress(self, data):
|
||||
if not data:
|
||||
return data
|
||||
|
||||
if not self._first_try:
|
||||
return self._obj.decompress(data)
|
||||
|
||||
@@ -36,9 +44,23 @@ class DeflateDecoder(object):
|
||||
self._data = None
|
||||
|
||||
|
||||
class GzipDecoder(object):
|
||||
|
||||
def __init__(self):
|
||||
self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._obj, name)
|
||||
|
||||
def decompress(self, data):
|
||||
if not data:
|
||||
return data
|
||||
return self._obj.decompress(data)
|
||||
|
||||
|
||||
def _get_decoder(mode):
|
||||
if mode == 'gzip':
|
||||
return zlib.decompressobj(16 + zlib.MAX_WBITS)
|
||||
return GzipDecoder()
|
||||
|
||||
return DeflateDecoder()
|
||||
|
||||
@@ -76,9 +98,10 @@ class HTTPResponse(io.IOBase):
|
||||
strict=0, preload_content=True, decode_content=True,
|
||||
original_response=None, pool=None, connection=None):
|
||||
|
||||
self.headers = HTTPHeaderDict()
|
||||
if headers:
|
||||
self.headers.update(headers)
|
||||
if isinstance(headers, HTTPHeaderDict):
|
||||
self.headers = headers
|
||||
else:
|
||||
self.headers = HTTPHeaderDict(headers)
|
||||
self.status = status
|
||||
self.version = version
|
||||
self.reason = reason
|
||||
@@ -100,8 +123,17 @@ class HTTPResponse(io.IOBase):
|
||||
if hasattr(body, 'read'):
|
||||
self._fp = body
|
||||
|
||||
if preload_content and not self._body:
|
||||
self._body = self.read(decode_content=decode_content)
|
||||
# Are we using the chunked-style of transfer encoding?
|
||||
self.chunked = False
|
||||
self.chunk_left = None
|
||||
tr_enc = self.headers.get('transfer-encoding', '')
|
||||
if tr_enc.lower() == "chunked":
|
||||
self.chunked = True
|
||||
|
||||
# We certainly don't want to preload content when the response is chunked.
|
||||
if not self.chunked:
|
||||
if preload_content and not self._body:
|
||||
self._body = self.read(decode_content=decode_content)
|
||||
|
||||
def get_redirect_location(self):
|
||||
"""
|
||||
@@ -202,7 +234,7 @@ class HTTPResponse(io.IOBase):
|
||||
|
||||
except BaseSSLError as e:
|
||||
# FIXME: Is there a better way to differentiate between SSLErrors?
|
||||
if not 'read operation timed out' in str(e): # Defensive:
|
||||
if 'read operation timed out' not in str(e): # Defensive:
|
||||
# This shouldn't happen but just in case we're missing an edge
|
||||
# case, let's avoid swallowing SSL errors.
|
||||
raise
|
||||
@@ -252,11 +284,15 @@ class HTTPResponse(io.IOBase):
|
||||
If True, will attempt to decode the body based on the
|
||||
'content-encoding' header.
|
||||
"""
|
||||
while not is_fp_closed(self._fp):
|
||||
data = self.read(amt=amt, decode_content=decode_content)
|
||||
if self.chunked:
|
||||
for line in self.read_chunked(amt):
|
||||
yield line
|
||||
else:
|
||||
while not is_fp_closed(self._fp):
|
||||
data = self.read(amt=amt, decode_content=decode_content)
|
||||
|
||||
if data:
|
||||
yield data
|
||||
if data:
|
||||
yield data
|
||||
|
||||
@classmethod
|
||||
def from_httplib(ResponseCls, r, **response_kw):
|
||||
@@ -267,14 +303,16 @@ class HTTPResponse(io.IOBase):
|
||||
Remaining parameters are passed to the HTTPResponse constructor, along
|
||||
with ``original_response=r``.
|
||||
"""
|
||||
|
||||
headers = HTTPHeaderDict()
|
||||
for k, v in r.getheaders():
|
||||
headers.add(k, v)
|
||||
headers = r.msg
|
||||
if not isinstance(headers, HTTPHeaderDict):
|
||||
if PY3: # Python 3
|
||||
headers = HTTPHeaderDict(headers.items())
|
||||
else: # Python 2
|
||||
headers = HTTPHeaderDict.from_httplib(headers)
|
||||
|
||||
# HTTPResponse objects in Python 3 don't have a .strict attribute
|
||||
strict = getattr(r, 'strict', 0)
|
||||
return ResponseCls(body=r,
|
||||
resp = ResponseCls(body=r,
|
||||
headers=headers,
|
||||
status=r.status,
|
||||
version=r.version,
|
||||
@@ -282,6 +320,7 @@ class HTTPResponse(io.IOBase):
|
||||
strict=strict,
|
||||
original_response=r,
|
||||
**response_kw)
|
||||
return resp
|
||||
|
||||
# Backwards-compatibility methods for httplib.HTTPResponse
|
||||
def getheaders(self):
|
||||
@@ -331,3 +370,59 @@ class HTTPResponse(io.IOBase):
|
||||
else:
|
||||
b[:len(temp)] = temp
|
||||
return len(temp)
|
||||
|
||||
def read_chunked(self, amt=None):
|
||||
# FIXME: Rewrite this method and make it a class with
|
||||
# a better structured logic.
|
||||
if not self.chunked:
|
||||
raise ResponseNotChunked("Response is not chunked. "
|
||||
"Header 'transfer-encoding: chunked' is missing.")
|
||||
while True:
|
||||
# First, we'll figure out length of a chunk and then
|
||||
# we'll try to read it from socket.
|
||||
if self.chunk_left is None:
|
||||
line = self._fp.fp.readline()
|
||||
line = line.decode()
|
||||
# See RFC 7230: Chunked Transfer Coding.
|
||||
i = line.find(';')
|
||||
if i >= 0:
|
||||
line = line[:i] # Strip chunk-extensions.
|
||||
try:
|
||||
self.chunk_left = int(line, 16)
|
||||
except ValueError:
|
||||
# Invalid chunked protocol response, abort.
|
||||
self.close()
|
||||
raise httplib.IncompleteRead(''.join(line))
|
||||
if self.chunk_left == 0:
|
||||
break
|
||||
if amt is None:
|
||||
chunk = self._fp._safe_read(self.chunk_left)
|
||||
yield chunk
|
||||
self._fp._safe_read(2) # Toss the CRLF at the end of the chunk.
|
||||
self.chunk_left = None
|
||||
elif amt < self.chunk_left:
|
||||
value = self._fp._safe_read(amt)
|
||||
self.chunk_left = self.chunk_left - amt
|
||||
yield value
|
||||
elif amt == self.chunk_left:
|
||||
value = self._fp._safe_read(amt)
|
||||
self._fp._safe_read(2) # Toss the CRLF at the end of the chunk.
|
||||
self.chunk_left = None
|
||||
yield value
|
||||
else: # amt > self.chunk_left
|
||||
yield self._fp._safe_read(self.chunk_left)
|
||||
self._fp._safe_read(2) # Toss the CRLF at the end of the chunk.
|
||||
self.chunk_left = None
|
||||
|
||||
# Chunk content ends with \r\n: discard it.
|
||||
while True:
|
||||
line = self._fp.fp.readline()
|
||||
if not line:
|
||||
# Some sites may not end with '\r\n'.
|
||||
break
|
||||
if line == b'\r\n':
|
||||
break
|
||||
|
||||
# We read everything; close the "file".
|
||||
self.release_conn()
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ def create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
||||
err = _
|
||||
if sock is not None:
|
||||
sock.close()
|
||||
sock = None
|
||||
|
||||
if err is not None:
|
||||
raise err
|
||||
|
||||
@@ -190,7 +190,7 @@ class Retry(object):
|
||||
return isinstance(err, (ReadTimeoutError, ProtocolError))
|
||||
|
||||
def is_forced_retry(self, method, status_code):
|
||||
""" Is this method/response retryable? (Based on method/codes whitelists)
|
||||
""" Is this method/status code retryable? (Based on method/codes whitelists)
|
||||
"""
|
||||
if self.method_whitelist and method.upper() not in self.method_whitelist:
|
||||
return False
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from binascii import hexlify, unhexlify
|
||||
from hashlib import md5, sha1
|
||||
from hashlib import md5, sha1, sha256
|
||||
|
||||
from ..exceptions import SSLError
|
||||
from ..exceptions import SSLError, InsecurePlatformWarning
|
||||
|
||||
|
||||
SSLContext = None
|
||||
@@ -9,9 +9,10 @@ HAS_SNI = False
|
||||
create_default_context = None
|
||||
|
||||
import errno
|
||||
import ssl
|
||||
import warnings
|
||||
|
||||
try: # Test for SSL features
|
||||
import ssl
|
||||
from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23
|
||||
from ssl import HAS_SNI # Has SNI?
|
||||
except ImportError:
|
||||
@@ -24,14 +25,24 @@ except ImportError:
|
||||
OP_NO_SSLv2, OP_NO_SSLv3 = 0x1000000, 0x2000000
|
||||
OP_NO_COMPRESSION = 0x20000
|
||||
|
||||
try:
|
||||
from ssl import _DEFAULT_CIPHERS
|
||||
except ImportError:
|
||||
_DEFAULT_CIPHERS = (
|
||||
'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:'
|
||||
'DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:ECDH+RC4:'
|
||||
'DH+RC4:RSA+RC4:!aNULL:!eNULL:!MD5'
|
||||
)
|
||||
# A secure default.
|
||||
# Sources for more information on TLS ciphers:
|
||||
#
|
||||
# - https://wiki.mozilla.org/Security/Server_Side_TLS
|
||||
# - https://www.ssllabs.com/projects/best-practices/index.html
|
||||
# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
|
||||
#
|
||||
# The general intent is:
|
||||
# - Prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE),
|
||||
# - prefer ECDHE over DHE for better performance,
|
||||
# - prefer any AES-GCM over any AES-CBC for better performance and security,
|
||||
# - use 3DES as fallback which is secure but slow,
|
||||
# - disable NULL authentication, MD5 MACs and DSS for security reasons.
|
||||
DEFAULT_CIPHERS = (
|
||||
'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:'
|
||||
'DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:!aNULL:'
|
||||
'!eNULL:!MD5'
|
||||
)
|
||||
|
||||
try:
|
||||
from ssl import SSLContext # Modern SSL?
|
||||
@@ -39,7 +50,8 @@ except ImportError:
|
||||
import sys
|
||||
|
||||
class SSLContext(object): # Platform-specific: Python 2 & 3.1
|
||||
supports_set_ciphers = sys.version_info >= (2, 7)
|
||||
supports_set_ciphers = ((2, 7) <= sys.version_info < (3,) or
|
||||
(3, 2) <= sys.version_info)
|
||||
|
||||
def __init__(self, protocol_version):
|
||||
self.protocol = protocol_version
|
||||
@@ -69,6 +81,14 @@ except ImportError:
|
||||
self.ciphers = cipher_suite
|
||||
|
||||
def wrap_socket(self, socket, server_hostname=None):
|
||||
warnings.warn(
|
||||
'A true SSLContext object is not available. This prevents '
|
||||
'urllib3 from configuring SSL appropriately and may cause '
|
||||
'certain SSL connections to fail. For more information, see '
|
||||
'https://urllib3.readthedocs.org/en/latest/security.html'
|
||||
'#insecureplatformwarning.',
|
||||
InsecurePlatformWarning
|
||||
)
|
||||
kwargs = {
|
||||
'keyfile': self.keyfile,
|
||||
'certfile': self.certfile,
|
||||
@@ -96,7 +116,8 @@ def assert_fingerprint(cert, fingerprint):
|
||||
# this digest.
|
||||
hashfunc_map = {
|
||||
16: md5,
|
||||
20: sha1
|
||||
20: sha1,
|
||||
32: sha256,
|
||||
}
|
||||
|
||||
fingerprint = fingerprint.replace(':', '').lower()
|
||||
@@ -157,7 +178,7 @@ def resolve_ssl_version(candidate):
|
||||
return candidate
|
||||
|
||||
|
||||
def create_urllib3_context(ssl_version=None, cert_reqs=ssl.CERT_REQUIRED,
|
||||
def create_urllib3_context(ssl_version=None, cert_reqs=None,
|
||||
options=None, ciphers=None):
|
||||
"""All arguments have the same meaning as ``ssl_wrap_socket``.
|
||||
|
||||
@@ -194,6 +215,9 @@ def create_urllib3_context(ssl_version=None, cert_reqs=ssl.CERT_REQUIRED,
|
||||
"""
|
||||
context = SSLContext(ssl_version or ssl.PROTOCOL_SSLv23)
|
||||
|
||||
# Setting the default here, as we may have no ssl module on import
|
||||
cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs
|
||||
|
||||
if options is None:
|
||||
options = 0
|
||||
# SSLv2 is easily broken and is considered harmful and dangerous
|
||||
@@ -207,11 +231,13 @@ def create_urllib3_context(ssl_version=None, cert_reqs=ssl.CERT_REQUIRED,
|
||||
context.options |= options
|
||||
|
||||
if getattr(context, 'supports_set_ciphers', True): # Platform-specific: Python 2.6
|
||||
context.set_ciphers(ciphers or _DEFAULT_CIPHERS)
|
||||
context.set_ciphers(ciphers or DEFAULT_CIPHERS)
|
||||
|
||||
context.verify_mode = cert_reqs
|
||||
if getattr(context, 'check_hostname', None) is not None: # Platform-specific: Python 3.2
|
||||
context.check_hostname = (context.verify_mode == ssl.CERT_REQUIRED)
|
||||
# We do our own verification, including fingerprints and alternative
|
||||
# hostnames. So disable it here
|
||||
context.check_hostname = False
|
||||
return context
|
||||
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict):
|
||||
|
||||
class SessionRedirectMixin(object):
|
||||
def resolve_redirects(self, resp, req, stream=False, timeout=None,
|
||||
verify=True, cert=None, proxies=None):
|
||||
verify=True, cert=None, proxies=None, **adapter_kwargs):
|
||||
"""Receives a Response. Returns a generator of Responses."""
|
||||
|
||||
i = 0
|
||||
@@ -171,7 +171,10 @@ class SessionRedirectMixin(object):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
extract_cookies_to_jar(prepared_request._cookies, prepared_request, resp.raw)
|
||||
# Extract any cookies sent on the response to the cookiejar
|
||||
# in the new request. Because we've mutated our copied prepared
|
||||
# request, use the old one that we haven't yet touched.
|
||||
extract_cookies_to_jar(prepared_request._cookies, req, resp.raw)
|
||||
prepared_request._cookies.update(self.cookies)
|
||||
prepared_request.prepare_cookies(prepared_request._cookies)
|
||||
|
||||
@@ -190,6 +193,7 @@ class SessionRedirectMixin(object):
|
||||
cert=cert,
|
||||
proxies=proxies,
|
||||
allow_redirects=False,
|
||||
**adapter_kwargs
|
||||
)
|
||||
|
||||
extract_cookies_to_jar(self.cookies, prepared_request, resp.raw)
|
||||
@@ -557,10 +561,6 @@ class Session(SessionRedirectMixin):
|
||||
# Set up variables needed for resolve_redirects and dispatching of hooks
|
||||
allow_redirects = kwargs.pop('allow_redirects', True)
|
||||
stream = kwargs.get('stream')
|
||||
timeout = kwargs.get('timeout')
|
||||
verify = kwargs.get('verify')
|
||||
cert = kwargs.get('cert')
|
||||
proxies = kwargs.get('proxies')
|
||||
hooks = request.hooks
|
||||
|
||||
# Get the appropriate adapter to use
|
||||
@@ -588,12 +588,7 @@ class Session(SessionRedirectMixin):
|
||||
extract_cookies_to_jar(self.cookies, request, r.raw)
|
||||
|
||||
# Redirect resolving generator.
|
||||
gen = self.resolve_redirects(r, request,
|
||||
stream=stream,
|
||||
timeout=timeout,
|
||||
verify=verify,
|
||||
cert=cert,
|
||||
proxies=proxies)
|
||||
gen = self.resolve_redirects(r, request, **kwargs)
|
||||
|
||||
# Resolve redirects if allowed.
|
||||
history = [resp for resp in gen] if allow_redirects else []
|
||||
|
||||
@@ -25,7 +25,8 @@ from . import __version__
|
||||
from . import certs
|
||||
from .compat import parse_http_list as _parse_list_header
|
||||
from .compat import (quote, urlparse, bytes, str, OrderedDict, unquote, is_py2,
|
||||
builtin_str, getproxies, proxy_bypass, urlunparse)
|
||||
builtin_str, getproxies, proxy_bypass, urlunparse,
|
||||
basestring)
|
||||
from .cookies import RequestsCookieJar, cookiejar_from_dict
|
||||
from .structures import CaseInsensitiveDict
|
||||
from .exceptions import InvalidURL
|
||||
@@ -115,7 +116,8 @@ def get_netrc_auth(url):
|
||||
def guess_filename(obj):
|
||||
"""Tries to guess the filename of the given object."""
|
||||
name = getattr(obj, 'name', None)
|
||||
if name and isinstance(name, builtin_str) and name[0] != '<' and name[-1] != '>':
|
||||
if (name and isinstance(name, basestring) and name[0] != '<' and
|
||||
name[-1] != '>'):
|
||||
return os.path.basename(name)
|
||||
|
||||
|
||||
@@ -418,10 +420,18 @@ def requote_uri(uri):
|
||||
This function passes the given URI through an unquote/quote cycle to
|
||||
ensure that it is fully and consistently quoted.
|
||||
"""
|
||||
# Unquote only the unreserved characters
|
||||
# Then quote only illegal characters (do not quote reserved, unreserved,
|
||||
# or '%')
|
||||
return quote(unquote_unreserved(uri), safe="!#$%&'()*+,/:;=?@[]~")
|
||||
safe_with_percent = "!#$%&'()*+,/:;=?@[]~"
|
||||
safe_without_percent = "!#$&'()*+,/:;=?@[]~"
|
||||
try:
|
||||
# Unquote only the unreserved characters
|
||||
# Then quote only illegal characters (do not quote reserved,
|
||||
# unreserved, or '%')
|
||||
return quote(unquote_unreserved(uri), safe=safe_with_percent)
|
||||
except InvalidURL:
|
||||
# We couldn't unquote the given URI, so let's try quoting it, but
|
||||
# there may be unquoted '%'s in the URI. We need to make sure they're
|
||||
# properly quoted so they do not cause issues elsewhere.
|
||||
return quote(uri, safe=safe_without_percent)
|
||||
|
||||
|
||||
def address_in_network(ip, net):
|
||||
|
||||
14
setup.py
14
setup.py
@@ -1,10 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import requests
|
||||
|
||||
from codecs import open
|
||||
|
||||
try:
|
||||
@@ -29,6 +28,14 @@ packages = [
|
||||
|
||||
requires = []
|
||||
|
||||
version = ''
|
||||
with open('requests/__init__.py', 'r') as fd:
|
||||
version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]',
|
||||
fd.read(), re.MULTILINE).group(1)
|
||||
|
||||
if not version:
|
||||
raise RuntimeError('Cannot find version information')
|
||||
|
||||
with open('README.rst', 'r', 'utf-8') as f:
|
||||
readme = f.read()
|
||||
with open('HISTORY.rst', 'r', 'utf-8') as f:
|
||||
@@ -36,7 +43,7 @@ with open('HISTORY.rst', 'r', 'utf-8') as f:
|
||||
|
||||
setup(
|
||||
name='requests',
|
||||
version=requests.__version__,
|
||||
version=version,
|
||||
description='Python HTTP for Humans.',
|
||||
long_description=readme + '\n\n' + history,
|
||||
author='Kenneth Reitz',
|
||||
@@ -60,7 +67,6 @@ setup(
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4'
|
||||
|
||||
),
|
||||
extras_require={
|
||||
'security': ['pyOpenSSL', 'ndg-httpsclient', 'pyasn1'],
|
||||
|
||||
@@ -301,13 +301,15 @@ class RequestsTestCase(unittest.TestCase):
|
||||
r = s.get(url)
|
||||
assert r.status_code == 200
|
||||
|
||||
def test_connection_error(self):
|
||||
def test_connection_error_invalid_domain(self):
|
||||
"""Connecting to an unknown domain should raise a ConnectionError"""
|
||||
with pytest.raises(ConnectionError):
|
||||
requests.get("http://fooobarbangbazbing.httpbin.org")
|
||||
requests.get("http://doesnotexist.google.com")
|
||||
|
||||
def test_connection_error_invalid_port(self):
|
||||
"""Connecting to an invalid port should raise a ConnectionError"""
|
||||
with pytest.raises(ConnectionError):
|
||||
requests.get("http://httpbin.org:1")
|
||||
requests.get("http://httpbin.org:1", timeout=1)
|
||||
|
||||
def test_LocationParseError(self):
|
||||
"""Inputing a URL that cannot be parsed should raise an InvalidURL error"""
|
||||
@@ -933,6 +935,19 @@ class RequestsTestCase(unittest.TestCase):
|
||||
|
||||
assert 'multipart/form-data' in p.headers['Content-Type']
|
||||
|
||||
def test_can_send_bytes_bytearray_objects_with_files(self):
|
||||
# Test bytes:
|
||||
data = {'a': 'this is a string'}
|
||||
files = {'b': b'foo'}
|
||||
r = requests.Request('POST', httpbin('post'), data=data, files=files)
|
||||
p = r.prepare()
|
||||
assert 'multipart/form-data' in p.headers['Content-Type']
|
||||
# Test bytearrays:
|
||||
files = {'b': bytearray(b'foo')}
|
||||
r = requests.Request('POST', httpbin('post'), data=data, files=files)
|
||||
p = r.prepare()
|
||||
assert 'multipart/form-data' in p.headers['Content-Type']
|
||||
|
||||
def test_can_send_file_object_with_non_string_filename(self):
|
||||
f = io.BytesIO()
|
||||
f.name = 2
|
||||
@@ -1037,6 +1052,23 @@ class RequestsTestCase(unittest.TestCase):
|
||||
assert 'application/json' in r.request.headers['Content-Type']
|
||||
assert {'life': 42} == r.json()['json']
|
||||
|
||||
def test_response_iter_lines(self):
|
||||
r = requests.get(httpbin('stream/4'), stream=True)
|
||||
assert r.status_code == 200
|
||||
|
||||
it = r.iter_lines()
|
||||
next(it)
|
||||
assert len(list(it)) == 3
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_response_iter_lines_reentrant(self):
|
||||
"""Response.iter_lines() is not reentrant safe"""
|
||||
r = requests.get(httpbin('stream/4'), stream=True)
|
||||
assert r.status_code == 200
|
||||
|
||||
next(r.iter_lines())
|
||||
assert len(list(r.iter_lines())) == 3
|
||||
|
||||
|
||||
class TestContentEncodingDetection(unittest.TestCase):
|
||||
|
||||
@@ -1265,6 +1297,32 @@ class UtilsTestCase(unittest.TestCase):
|
||||
'http://localhost.localdomain:5000/v1.0/') == {}
|
||||
assert get_environ_proxies('http://www.requests.com/') != {}
|
||||
|
||||
def test_guess_filename_when_int(self):
|
||||
from requests.utils import guess_filename
|
||||
assert None is guess_filename(1)
|
||||
|
||||
def test_guess_filename_when_filename_is_an_int(self):
|
||||
from requests.utils import guess_filename
|
||||
fake = type('Fake', (object,), {'name': 1})()
|
||||
assert None is guess_filename(fake)
|
||||
|
||||
def test_guess_filename_with_file_like_obj(self):
|
||||
from requests.utils import guess_filename
|
||||
from requests import compat
|
||||
fake = type('Fake', (object,), {'name': b'value'})()
|
||||
guessed_name = guess_filename(fake)
|
||||
assert b'value' == guessed_name
|
||||
assert isinstance(guessed_name, compat.bytes)
|
||||
|
||||
def test_guess_filename_with_unicode_name(self):
|
||||
from requests.utils import guess_filename
|
||||
from requests import compat
|
||||
filename = b'value'.decode('utf-8')
|
||||
fake = type('Fake', (object,), {'name': filename})()
|
||||
guessed_name = guess_filename(fake)
|
||||
assert filename == guessed_name
|
||||
assert isinstance(guessed_name, compat.str)
|
||||
|
||||
def test_is_ipv4_address(self):
|
||||
from requests.utils import is_ipv4_address
|
||||
assert is_ipv4_address('8.8.8.8')
|
||||
@@ -1301,6 +1359,22 @@ class UtilsTestCase(unittest.TestCase):
|
||||
assert username == percent_encoding_test_chars
|
||||
assert password == percent_encoding_test_chars
|
||||
|
||||
def test_requote_uri_with_unquoted_percents(self):
|
||||
"""Ensure we handle unquoted percent signs in redirects.
|
||||
|
||||
See: https://github.com/kennethreitz/requests/issues/2356
|
||||
"""
|
||||
from requests.utils import requote_uri
|
||||
bad_uri = 'http://example.com/fiz?buz=%ppicture'
|
||||
quoted = 'http://example.com/fiz?buz=%25ppicture'
|
||||
assert quoted == requote_uri(bad_uri)
|
||||
|
||||
def test_requote_uri_properly_requotes(self):
|
||||
"""Ensure requoting doesn't break expectations."""
|
||||
from requests.utils import requote_uri
|
||||
quoted = 'http://example.com/fiz?buz=%25ppicture'
|
||||
assert quoted == requote_uri(quoted)
|
||||
|
||||
|
||||
class TestMorselToCookieExpires(unittest.TestCase):
|
||||
|
||||
@@ -1539,7 +1613,6 @@ def test_prepare_unicode_url():
|
||||
p.prepare(
|
||||
method='GET',
|
||||
url=u('http://www.example.com/üniçø∂é'),
|
||||
hooks=[]
|
||||
)
|
||||
assert_copy(p, p.copy())
|
||||
|
||||
@@ -1554,5 +1627,12 @@ def test_urllib3_retries():
|
||||
with pytest.raises(RetryError):
|
||||
s.get(httpbin('status/500'))
|
||||
|
||||
def test_vendor_aliases():
|
||||
from requests.packages import urllib3
|
||||
from requests.packages import chardet
|
||||
|
||||
with pytest.raises(ImportError):
|
||||
from requests.packages import webbrowser
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user