Merge branch 'refs/heads/develop' into redesign
This commit is contained in:
+8
-5
@@ -1,6 +1,7 @@
|
||||
# axel.py
|
||||
#
|
||||
# Copyright (C) 2010 Adrian Cristea adrian dot cristea at gmail dotcom
|
||||
# Edits by Ruud Burger
|
||||
#
|
||||
# Based on an idea by Peter Thatcher, found on
|
||||
# http://www.valuedlessons.com/2008/04/events-in-python.html
|
||||
@@ -11,12 +12,14 @@
|
||||
# Source: http://pypi.python.org/pypi/axel
|
||||
# Docs: http://packages.python.org/axel
|
||||
|
||||
from couchpotato.core.helpers.variable import natsortKey
|
||||
import Queue
|
||||
from Queue import Empty, Queue
|
||||
import hashlib
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from couchpotato.core.helpers.variable import natsortKey
|
||||
|
||||
|
||||
class Event(object):
|
||||
"""
|
||||
Event object inspired by C# events. Handlers can be registered and
|
||||
@@ -140,7 +143,7 @@ class Event(object):
|
||||
|
||||
def fire(self, *args, **kwargs):
|
||||
""" Stores all registered handlers in a queue for processing """
|
||||
self.queue = Queue.Queue()
|
||||
self.queue = Queue()
|
||||
result = {}
|
||||
|
||||
if self.handlers:
|
||||
@@ -239,9 +242,9 @@ class Event(object):
|
||||
order_lock.release()
|
||||
|
||||
if self.queue.empty():
|
||||
raise Queue.Empty
|
||||
raise Empty
|
||||
|
||||
except Queue.Empty:
|
||||
except Empty:
|
||||
break
|
||||
|
||||
def _extract(self, queue_item):
|
||||
|
||||
@@ -36,17 +36,17 @@ usage:
|
||||
The other HTTP methods are supported - see `requests.api`. Full documentation
|
||||
is at <http://python-requests.org>.
|
||||
|
||||
:copyright: (c) 2013 by Kenneth Reitz.
|
||||
:copyright: (c) 2014 by Kenneth Reitz.
|
||||
:license: Apache 2.0, see LICENSE for more details.
|
||||
|
||||
"""
|
||||
|
||||
__title__ = 'requests'
|
||||
__version__ = '2.1.0'
|
||||
__build__ = 0x020100
|
||||
__version__ = '2.3.0'
|
||||
__build__ = 0x020300
|
||||
__author__ = 'Kenneth Reitz'
|
||||
__license__ = 'Apache 2.0'
|
||||
__copyright__ = 'Copyright 2013 Kenneth Reitz'
|
||||
__copyright__ = 'Copyright 2014 Kenneth Reitz'
|
||||
|
||||
# Attempt to enable urllib3's SNI support, if possible
|
||||
try:
|
||||
|
||||
+21
-23
@@ -16,7 +16,7 @@ from .packages.urllib3.response import HTTPResponse
|
||||
from .packages.urllib3.util import Timeout as TimeoutSauce
|
||||
from .compat import urlparse, basestring, urldefrag, unquote
|
||||
from .utils import (DEFAULT_CA_BUNDLE_PATH, get_encoding_from_headers,
|
||||
except_on_missing_scheme, get_auth_from_url)
|
||||
prepend_scheme_if_needed, get_auth_from_url)
|
||||
from .structures import CaseInsensitiveDict
|
||||
from .packages.urllib3.exceptions import MaxRetryError
|
||||
from .packages.urllib3.exceptions import TimeoutError
|
||||
@@ -55,14 +55,16 @@ class HTTPAdapter(BaseAdapter):
|
||||
|
||||
:param pool_connections: The number of urllib3 connection pools to cache.
|
||||
:param pool_maxsize: The maximum number of connections to save in the pool.
|
||||
:param max_retries: The maximum number of retries each connection should attempt.
|
||||
:param int max_retries: The maximum number of retries each connection
|
||||
should attempt. Note, this applies only to failed connections and
|
||||
timeouts, never to requests where the server returns a response.
|
||||
:param pool_block: Whether the connection pool should block for connections.
|
||||
|
||||
Usage::
|
||||
|
||||
>>> import requests
|
||||
>>> s = requests.Session()
|
||||
>>> a = requests.adapters.HTTPAdapter()
|
||||
>>> a = requests.adapters.HTTPAdapter(max_retries=3)
|
||||
>>> s.mount('http://', a)
|
||||
"""
|
||||
__attrs__ = ['max_retries', 'config', '_pool_connections', '_pool_maxsize',
|
||||
@@ -88,6 +90,11 @@ class HTTPAdapter(BaseAdapter):
|
||||
self.__attrs__)
|
||||
|
||||
def __setstate__(self, state):
|
||||
# Can't handle by adding 'proxy_manager' to self.__attrs__ because
|
||||
# because self.poolmanager uses a lambda function, which isn't pickleable.
|
||||
self.proxy_manager = {}
|
||||
self.config = {}
|
||||
|
||||
for attr, value in state.items():
|
||||
setattr(self, attr, value)
|
||||
|
||||
@@ -196,13 +203,16 @@ class HTTPAdapter(BaseAdapter):
|
||||
proxy = proxies.get(urlparse(url.lower()).scheme)
|
||||
|
||||
if proxy:
|
||||
except_on_missing_scheme(proxy)
|
||||
proxy = prepend_scheme_if_needed(proxy, 'http')
|
||||
proxy_headers = self.proxy_headers(proxy)
|
||||
|
||||
if not proxy in self.proxy_manager:
|
||||
self.proxy_manager[proxy] = proxy_from_url(
|
||||
proxy,
|
||||
proxy_headers=proxy_headers)
|
||||
proxy_headers=proxy_headers,
|
||||
num_pools=self._pool_connections,
|
||||
maxsize=self._pool_maxsize,
|
||||
block=self._pool_block)
|
||||
|
||||
conn = self.proxy_manager[proxy].connection_from_url(url)
|
||||
else:
|
||||
@@ -276,10 +286,6 @@ class HTTPAdapter(BaseAdapter):
|
||||
username, password = get_auth_from_url(proxy)
|
||||
|
||||
if username and password:
|
||||
# Proxy auth usernames and passwords will be urlencoded, we need
|
||||
# to decode them.
|
||||
username = unquote(username)
|
||||
password = unquote(password)
|
||||
headers['Proxy-Authorization'] = _basic_auth_str(username,
|
||||
password)
|
||||
|
||||
@@ -304,10 +310,7 @@ class HTTPAdapter(BaseAdapter):
|
||||
|
||||
chunked = not (request.body is None or 'Content-Length' in request.headers)
|
||||
|
||||
if stream:
|
||||
timeout = TimeoutSauce(connect=timeout)
|
||||
else:
|
||||
timeout = TimeoutSauce(connect=timeout, read=timeout)
|
||||
timeout = TimeoutSauce(connect=timeout, read=timeout)
|
||||
|
||||
try:
|
||||
if not chunked:
|
||||
@@ -366,25 +369,20 @@ class HTTPAdapter(BaseAdapter):
|
||||
conn._put_conn(low_conn)
|
||||
|
||||
except socket.error as sockerr:
|
||||
raise ConnectionError(sockerr)
|
||||
raise ConnectionError(sockerr, request=request)
|
||||
|
||||
except MaxRetryError as e:
|
||||
raise ConnectionError(e)
|
||||
raise ConnectionError(e, request=request)
|
||||
|
||||
except _ProxyError as e:
|
||||
raise ProxyError(e)
|
||||
|
||||
except (_SSLError, _HTTPError) as e:
|
||||
if isinstance(e, _SSLError):
|
||||
raise SSLError(e)
|
||||
raise SSLError(e, request=request)
|
||||
elif isinstance(e, TimeoutError):
|
||||
raise Timeout(e)
|
||||
raise Timeout(e, request=request)
|
||||
else:
|
||||
raise
|
||||
|
||||
r = self.build_response(request, resp)
|
||||
|
||||
if not stream:
|
||||
r.content
|
||||
|
||||
return r
|
||||
return self.build_response(request, resp)
|
||||
|
||||
@@ -26,7 +26,7 @@ def request(method, url, **kwargs):
|
||||
:param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
|
||||
:param files: (optional) Dictionary of 'name': file-like-objects (or {'name': ('filename', fileobj)}) for multipart encoding upload.
|
||||
:param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
|
||||
:param timeout: (optional) Float describing the timeout of the request.
|
||||
:param timeout: (optional) Float describing the timeout of the request in seconds.
|
||||
:param allow_redirects: (optional) Boolean. Set to True if POST/PUT/DELETE redirect following is allowed.
|
||||
:param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
|
||||
:param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided.
|
||||
|
||||
@@ -11,7 +11,6 @@ import os
|
||||
import re
|
||||
import time
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from base64 import b64encode
|
||||
|
||||
@@ -19,8 +18,6 @@ from .compat import urlparse, str
|
||||
from .cookies import extract_cookies_to_jar
|
||||
from .utils import parse_dict_header
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
|
||||
CONTENT_TYPE_MULTI_PART = 'multipart/form-data'
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
pythoncompat
|
||||
"""
|
||||
|
||||
from .packages import charade as chardet
|
||||
from .packages import chardet
|
||||
|
||||
import sys
|
||||
|
||||
@@ -75,7 +75,9 @@ is_solaris = ('solar==' in str(sys.platform).lower()) # Complete guess.
|
||||
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
except (ImportError, SyntaxError):
|
||||
# simplejson does not support Python 3.2, it thows a SyntaxError
|
||||
# because of u'...' Unicode literals.
|
||||
import json
|
||||
|
||||
# ---------
|
||||
|
||||
+37
-28
@@ -198,30 +198,39 @@ class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping):
|
||||
self.set_cookie(c)
|
||||
return c
|
||||
|
||||
def iterkeys(self):
|
||||
"""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()."""
|
||||
keys = []
|
||||
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()."""
|
||||
for cookie in iter(self):
|
||||
keys.append(cookie.name)
|
||||
return keys
|
||||
yield cookie.value
|
||||
|
||||
def values(self):
|
||||
"""Dict-like values() that returns a list of values of cookies from the jar.
|
||||
See keys() and items()."""
|
||||
values = []
|
||||
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()."""
|
||||
for cookie in iter(self):
|
||||
values.append(cookie.value)
|
||||
return values
|
||||
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."""
|
||||
items = []
|
||||
for cookie in iter(self):
|
||||
items.append((cookie.name, cookie.value))
|
||||
return items
|
||||
return list(self.iteritems())
|
||||
|
||||
def list_domains(self):
|
||||
"""Utility method to list all the domains in the jar."""
|
||||
@@ -378,29 +387,29 @@ def create_cookie(name, value, **kwargs):
|
||||
|
||||
def morsel_to_cookie(morsel):
|
||||
"""Convert a Morsel object into a Cookie containing the one k/v pair."""
|
||||
|
||||
expires = None
|
||||
if morsel["max-age"]:
|
||||
expires = time.time() + morsel["max-age"]
|
||||
if morsel['max-age']:
|
||||
expires = time.time() + morsel['max-age']
|
||||
elif morsel['expires']:
|
||||
expires = morsel['expires']
|
||||
if type(expires) == type(""):
|
||||
time_template = "%a, %d-%b-%Y %H:%M:%S GMT"
|
||||
expires = time.mktime(time.strptime(expires, time_template))
|
||||
c = create_cookie(
|
||||
name=morsel.key,
|
||||
value=morsel.value,
|
||||
version=morsel['version'] or 0,
|
||||
port=None,
|
||||
domain=morsel['domain'],
|
||||
path=morsel['path'],
|
||||
secure=bool(morsel['secure']),
|
||||
expires=expires,
|
||||
discard=False,
|
||||
time_template = '%a, %d-%b-%Y %H:%M:%S GMT'
|
||||
expires = time.mktime(
|
||||
time.strptime(morsel['expires'], time_template)) - time.timezone
|
||||
return create_cookie(
|
||||
comment=morsel['comment'],
|
||||
comment_url=bool(morsel['comment']),
|
||||
discard=False,
|
||||
domain=morsel['domain'],
|
||||
expires=expires,
|
||||
name=morsel.key,
|
||||
path=morsel['path'],
|
||||
port=None,
|
||||
rest={'HttpOnly': morsel['httponly']},
|
||||
rfc2109=False,)
|
||||
return c
|
||||
rfc2109=False,
|
||||
secure=bool(morsel['secure']),
|
||||
value=morsel.value,
|
||||
version=morsel['version'] or 0,
|
||||
)
|
||||
|
||||
|
||||
def cookiejar_from_dict(cookie_dict, cookiejar=None, overwrite=True):
|
||||
|
||||
@@ -7,21 +7,29 @@ requests.exceptions
|
||||
This module contains the set of Requests' exceptions.
|
||||
|
||||
"""
|
||||
from .packages.urllib3.exceptions import HTTPError as BaseHTTPError
|
||||
|
||||
|
||||
class RequestException(IOError):
|
||||
"""There was an ambiguous exception that occurred while handling your
|
||||
request."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initialize RequestException with `request` and `response` objects.
|
||||
"""
|
||||
response = kwargs.pop('response', None)
|
||||
self.response = response
|
||||
self.request = kwargs.pop('request', None)
|
||||
if (response is not None and not self.request and
|
||||
hasattr(response, 'request')):
|
||||
self.request = self.response.request
|
||||
super(RequestException, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class HTTPError(RequestException):
|
||||
"""An HTTP error occurred."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" Initializes HTTPError with optional `response` object. """
|
||||
self.response = kwargs.pop('response', None)
|
||||
super(HTTPError, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class ConnectionError(RequestException):
|
||||
"""A Connection error occurred."""
|
||||
@@ -61,3 +69,7 @@ class InvalidURL(RequestException, ValueError):
|
||||
|
||||
class ChunkedEncodingError(RequestException):
|
||||
"""The server declared chunked encoding but sent an invalid chunk."""
|
||||
|
||||
|
||||
class ContentDecodingError(RequestException, BaseHTTPError):
|
||||
"""Failed to decode response content"""
|
||||
|
||||
+63
-29
@@ -8,7 +8,6 @@ This module contains the primary objects that power Requests.
|
||||
"""
|
||||
|
||||
import collections
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
from io import BytesIO, UnsupportedOperation
|
||||
@@ -20,9 +19,10 @@ from .cookies import cookiejar_from_dict, get_cookie_header
|
||||
from .packages.urllib3.fields import RequestField
|
||||
from .packages.urllib3.filepost import encode_multipart_formdata
|
||||
from .packages.urllib3.util import parse_url
|
||||
from .packages.urllib3.exceptions import DecodeError
|
||||
from .exceptions import (
|
||||
HTTPError, RequestException, MissingSchema, InvalidURL,
|
||||
ChunkedEncodingError)
|
||||
ChunkedEncodingError, ContentDecodingError)
|
||||
from .utils import (
|
||||
guess_filename, get_auth_from_url, requote_uri,
|
||||
stream_decode_response_unicode, to_key_val_list, parse_header_links,
|
||||
@@ -30,12 +30,20 @@ from .utils import (
|
||||
from .compat import (
|
||||
cookielib, urlunparse, urlsplit, urlencode, str, bytes, StringIO,
|
||||
is_py2, chardet, json, builtin_str, basestring, IncompleteRead)
|
||||
from .status_codes import codes
|
||||
|
||||
#: The set of HTTP status codes that indicate an automatically
|
||||
#: processable redirect.
|
||||
REDIRECT_STATI = (
|
||||
codes.moved, # 301
|
||||
codes.found, # 302
|
||||
codes.other, # 303
|
||||
codes.temporary_moved, # 307
|
||||
)
|
||||
DEFAULT_REDIRECT_LIMIT = 30
|
||||
CONTENT_CHUNK_SIZE = 10 * 1024
|
||||
ITER_CHUNK_SIZE = 512
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RequestEncodingMixin(object):
|
||||
@property
|
||||
@@ -301,8 +309,8 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
p = PreparedRequest()
|
||||
p.method = self.method
|
||||
p.url = self.url
|
||||
p.headers = self.headers.copy()
|
||||
p._cookies = self._cookies.copy()
|
||||
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.body = self.body
|
||||
p.hooks = self.hooks
|
||||
return p
|
||||
@@ -400,9 +408,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
|
||||
is_stream = all([
|
||||
hasattr(data, '__iter__'),
|
||||
not isinstance(data, basestring),
|
||||
not isinstance(data, list),
|
||||
not isinstance(data, dict)
|
||||
not isinstance(data, (basestring, list, tuple, dict))
|
||||
])
|
||||
|
||||
try:
|
||||
@@ -427,7 +433,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
else:
|
||||
if data:
|
||||
body = self._encode_params(data)
|
||||
if isinstance(data, str) or isinstance(data, builtin_str) or hasattr(data, 'read'):
|
||||
if isinstance(data, basestring) or hasattr(data, 'read'):
|
||||
content_type = None
|
||||
else:
|
||||
content_type = 'application/x-www-form-urlencoded'
|
||||
@@ -516,7 +522,7 @@ class Response(object):
|
||||
self._content = False
|
||||
self._content_consumed = False
|
||||
|
||||
#: Integer Code of responded HTTP Status.
|
||||
#: Integer Code of responded HTTP Status, e.g. 404 or 200.
|
||||
self.status_code = None
|
||||
|
||||
#: Case-insensitive Dictionary of Response Headers.
|
||||
@@ -525,7 +531,7 @@ class Response(object):
|
||||
self.headers = CaseInsensitiveDict()
|
||||
|
||||
#: File-like object representation of response (for advanced usage).
|
||||
#: Requires that ``stream=True` on the request.
|
||||
#: Use of ``raw`` requires that ``stream=True`` be set on the request.
|
||||
# This requirement does not apply for use internally to Requests.
|
||||
self.raw = None
|
||||
|
||||
@@ -540,6 +546,7 @@ class Response(object):
|
||||
#: up here. The list is sorted from the oldest to the most recent request.
|
||||
self.history = []
|
||||
|
||||
#: Textual reason of responded HTTP Status, e.g. "Not Found" or "OK".
|
||||
self.reason = None
|
||||
|
||||
#: A CookieJar of Cookies the server sent back.
|
||||
@@ -549,6 +556,10 @@ class Response(object):
|
||||
#: and the arrival of the response (as a timedelta)
|
||||
self.elapsed = datetime.timedelta(0)
|
||||
|
||||
#: The :class:`PreparedRequest <PreparedRequest>` object to which this
|
||||
#: is a response.
|
||||
self.request = None
|
||||
|
||||
def __getstate__(self):
|
||||
# Consume everything; accessing the content attribute makes
|
||||
# sure the content has been fully read.
|
||||
@@ -566,6 +577,7 @@ class Response(object):
|
||||
|
||||
# pickled objects do not have .raw
|
||||
setattr(self, '_content_consumed', True)
|
||||
setattr(self, 'raw', None)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Response [%s]>' % (self.status_code)
|
||||
@@ -590,10 +602,16 @@ class Response(object):
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_redirect(self):
|
||||
"""True if this Response is a well-formed HTTP redirect that could have
|
||||
been processed automatically (by :meth:`Session.resolve_redirects`).
|
||||
"""
|
||||
return ('location' in self.headers and self.status_code in REDIRECT_STATI)
|
||||
|
||||
@property
|
||||
def apparent_encoding(self):
|
||||
"""The apparent encoding, provided by the lovely Charade library
|
||||
(Thanks, Ian!)."""
|
||||
"""The apparent encoding, provided by the chardet library"""
|
||||
return chardet.detect(self.content)['encoding']
|
||||
|
||||
def iter_content(self, chunk_size=1, decode_unicode=False):
|
||||
@@ -602,20 +620,20 @@ class Response(object):
|
||||
large responses. The chunk size is the number of bytes it should
|
||||
read into memory. This is not necessarily the length of each item
|
||||
returned as decoding can take place.
|
||||
"""
|
||||
if self._content_consumed:
|
||||
# simulate reading small chunks of the content
|
||||
return iter_slices(self._content, chunk_size)
|
||||
|
||||
If decode_unicode is True, content will be decoded using the best
|
||||
available encoding based on the response.
|
||||
"""
|
||||
def generate():
|
||||
try:
|
||||
# Special case for urllib3.
|
||||
try:
|
||||
for chunk in self.raw.stream(chunk_size,
|
||||
decode_content=True):
|
||||
for chunk in self.raw.stream(chunk_size, decode_content=True):
|
||||
yield chunk
|
||||
except IncompleteRead as e:
|
||||
raise ChunkedEncodingError(e)
|
||||
except DecodeError as e:
|
||||
raise ContentDecodingError(e)
|
||||
except AttributeError:
|
||||
# Standard file-like object.
|
||||
while True:
|
||||
@@ -626,12 +644,17 @@ class Response(object):
|
||||
|
||||
self._content_consumed = True
|
||||
|
||||
gen = generate()
|
||||
# simulate reading small chunks of the content
|
||||
reused_chunks = iter_slices(self._content, chunk_size)
|
||||
|
||||
stream_chunks = generate()
|
||||
|
||||
chunks = reused_chunks if self._content_consumed else stream_chunks
|
||||
|
||||
if decode_unicode:
|
||||
gen = stream_decode_response_unicode(gen, self)
|
||||
chunks = stream_decode_response_unicode(chunks, self)
|
||||
|
||||
return gen
|
||||
return chunks
|
||||
|
||||
def iter_lines(self, chunk_size=ITER_CHUNK_SIZE, decode_unicode=None):
|
||||
"""Iterates over the response data, one line at a time. When
|
||||
@@ -641,8 +664,7 @@ class Response(object):
|
||||
|
||||
pending = None
|
||||
|
||||
for chunk in self.iter_content(chunk_size=chunk_size,
|
||||
decode_unicode=decode_unicode):
|
||||
for chunk in self.iter_content(chunk_size=chunk_size, decode_unicode=decode_unicode):
|
||||
|
||||
if pending is not None:
|
||||
chunk = pending + chunk
|
||||
@@ -688,7 +710,12 @@ class Response(object):
|
||||
"""Content of the response, in unicode.
|
||||
|
||||
If Response.encoding is None, encoding will be guessed using
|
||||
``charade``.
|
||||
``chardet``.
|
||||
|
||||
The encoding of the response content is determined based solely on HTTP
|
||||
headers, following RFC 2616 to the letter. If you can take advantage of
|
||||
non-HTTP knowledge to make a better guess at the encoding, you should
|
||||
set ``r.encoding`` appropriately before accessing this property.
|
||||
"""
|
||||
|
||||
# Try charset from content-type
|
||||
@@ -729,7 +756,14 @@ class Response(object):
|
||||
# a best guess).
|
||||
encoding = guess_json_utf(self.content)
|
||||
if encoding is not None:
|
||||
return json.loads(self.content.decode(encoding), **kwargs)
|
||||
try:
|
||||
return json.loads(self.content.decode(encoding), **kwargs)
|
||||
except UnicodeDecodeError:
|
||||
# Wrong UTF codec detected; usually because it's not UTF-8
|
||||
# but some other 8-bit codec. This is an RFC violation,
|
||||
# and the server didn't bother to tell us what codec *was*
|
||||
# used.
|
||||
pass
|
||||
return json.loads(self.text, **kwargs)
|
||||
|
||||
@property
|
||||
@@ -765,8 +799,8 @@ class Response(object):
|
||||
raise HTTPError(http_error_msg, response=self)
|
||||
|
||||
def close(self):
|
||||
"""Closes the underlying file descriptor and releases the connection
|
||||
back to the pool.
|
||||
"""Releases the connection back to the pool. Once this method has been
|
||||
called the underlying ``raw`` object must not be accessed again.
|
||||
|
||||
*Note: Should not normally need to be called explicitly.*
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
If you are planning to submit a pull request to requests with any changes in
|
||||
this library do not go any further. These are independent libraries which we
|
||||
vendor into requests. Any changes necessary to these libraries must be made in
|
||||
them and submitted as separate pull requests to those libraries.
|
||||
|
||||
urllib3 pull requests go here: https://github.com/shazow/urllib3
|
||||
|
||||
chardet pull requests go here: https://github.com/chardet/chardet
|
||||
@@ -1,7 +0,0 @@
|
||||
'''
|
||||
support ';python -m charade <file1> [file2] ...' package execution syntax (2.7+)
|
||||
'''
|
||||
|
||||
from charade import charade_cli
|
||||
|
||||
charade_cli()
|
||||
+1
-35
@@ -15,7 +15,7 @@
|
||||
# 02110-1301 USA
|
||||
######################### END LICENSE BLOCK #########################
|
||||
|
||||
__version__ = "1.0.3"
|
||||
__version__ = "2.2.1"
|
||||
from sys import version_info
|
||||
|
||||
|
||||
@@ -30,37 +30,3 @@ def detect(aBuf):
|
||||
u.feed(aBuf)
|
||||
u.close()
|
||||
return u.result
|
||||
|
||||
def _description_of(path):
|
||||
"""Return a string describing the probable encoding of a file."""
|
||||
from charade.universaldetector import UniversalDetector
|
||||
|
||||
u = UniversalDetector()
|
||||
for line in open(path, 'rb'):
|
||||
u.feed(line)
|
||||
u.close()
|
||||
result = u.result
|
||||
if result['encoding']:
|
||||
return '%s: %s with confidence %s' % (path,
|
||||
result['encoding'],
|
||||
result['confidence'])
|
||||
else:
|
||||
return '%s: no result' % path
|
||||
|
||||
|
||||
def charade_cli():
|
||||
"""
|
||||
Script which takes one or more file paths and reports on their detected
|
||||
encodings
|
||||
|
||||
Example::
|
||||
|
||||
% chardetect.py somefile someotherfile
|
||||
somefile: windows-1252 with confidence 0.5
|
||||
someotherfile: ascii with confidence 1.0
|
||||
|
||||
"""
|
||||
from sys import argv
|
||||
for path in argv[1:]:
|
||||
print(_description_of(path))
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Script which takes one or more file paths and reports on their detected
|
||||
encodings
|
||||
|
||||
Example::
|
||||
|
||||
% chardetect somefile someotherfile
|
||||
somefile: windows-1252 with confidence 0.5
|
||||
someotherfile: ascii with confidence 1.0
|
||||
|
||||
If no paths are provided, it takes its input from stdin.
|
||||
|
||||
"""
|
||||
from io import open
|
||||
from sys import argv, stdin
|
||||
|
||||
from chardet.universaldetector import UniversalDetector
|
||||
|
||||
|
||||
def description_of(file, name='stdin'):
|
||||
"""Return a string describing the probable encoding of a file."""
|
||||
u = UniversalDetector()
|
||||
for line in file:
|
||||
u.feed(line)
|
||||
u.close()
|
||||
result = u.result
|
||||
if result['encoding']:
|
||||
return '%s: %s with confidence %s' % (name,
|
||||
result['encoding'],
|
||||
result['confidence'])
|
||||
else:
|
||||
return '%s: no result' % name
|
||||
|
||||
|
||||
def main():
|
||||
if len(argv) <= 1:
|
||||
print(description_of(stdin))
|
||||
else:
|
||||
for path in argv[1:]:
|
||||
with open(path, 'rb') as f:
|
||||
print(description_of(f, path))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -169,7 +169,7 @@ class JapaneseContextAnalysis:
|
||||
def get_confidence(self):
|
||||
# This is just one way to calculate confidence. It works well for me.
|
||||
if self._mTotalRel > MINIMUM_DATA_THRESHOLD:
|
||||
return float(self._mTotalRel - self._mRelSample[0]) / self._mTotalRel
|
||||
return (self._mTotalRel - self._mRelSample[0]) / self._mTotalRel
|
||||
else:
|
||||
return DONT_KNOW
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ class Latin1Prober(CharSetProber):
|
||||
if total < 0.01:
|
||||
confidence = 0.0
|
||||
else:
|
||||
confidence = ((float(self._mFreqCounter[3]) / total)
|
||||
confidence = ((self._mFreqCounter[3] / total)
|
||||
- (self._mFreqCounter[1] * 20.0 / total))
|
||||
if confidence < 0.0:
|
||||
confidence = 0.0
|
||||
+10
-8
@@ -74,10 +74,12 @@ class UniversalDetector:
|
||||
if aBuf[:3] == codecs.BOM:
|
||||
# EF BB BF UTF-8 with BOM
|
||||
self.result = {'encoding': "UTF-8", 'confidence': 1.0}
|
||||
elif aBuf[:4] in (codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE):
|
||||
elif aBuf[:4] == codecs.BOM_UTF32_LE:
|
||||
# FF FE 00 00 UTF-32, little-endian BOM
|
||||
self.result = {'encoding': "UTF-32LE", 'confidence': 1.0}
|
||||
elif aBuf[:4] == codecs.BOM_UTF32_BE:
|
||||
# 00 00 FE FF UTF-32, big-endian BOM
|
||||
self.result = {'encoding': "UTF-32", 'confidence': 1.0}
|
||||
self.result = {'encoding': "UTF-32BE", 'confidence': 1.0}
|
||||
elif aBuf[:4] == b'\xFE\xFF\x00\x00':
|
||||
# FE FF 00 00 UCS-4, unusual octet order BOM (3412)
|
||||
self.result = {
|
||||
@@ -90,10 +92,12 @@ class UniversalDetector:
|
||||
'encoding': "X-ISO-10646-UCS-4-2143",
|
||||
'confidence': 1.0
|
||||
}
|
||||
elif aBuf[:2] == codecs.BOM_LE or aBuf[:2] == codecs.BOM_BE:
|
||||
elif aBuf[:2] == codecs.BOM_LE:
|
||||
# FF FE UTF-16, little endian BOM
|
||||
self.result = {'encoding': "UTF-16LE", 'confidence': 1.0}
|
||||
elif aBuf[:2] == codecs.BOM_BE:
|
||||
# FE FF UTF-16, big endian BOM
|
||||
self.result = {'encoding': "UTF-16", 'confidence': 1.0}
|
||||
self.result = {'encoding': "UTF-16BE", 'confidence': 1.0}
|
||||
|
||||
self._mGotData = True
|
||||
if self.result['encoding'] and (self.result['confidence'] > 0.0):
|
||||
@@ -113,10 +117,8 @@ class UniversalDetector:
|
||||
if not self._mEscCharSetProber:
|
||||
self._mEscCharSetProber = EscCharSetProber()
|
||||
if self._mEscCharSetProber.feed(aBuf) == constants.eFoundIt:
|
||||
self.result = {
|
||||
'encoding': self._mEscCharSetProber.get_charset_name(),
|
||||
'confidence': self._mEscCharSetProber.get_confidence()
|
||||
}
|
||||
self.result = {'encoding': self._mEscCharSetProber.get_charset_name(),
|
||||
'confidence': self._mEscCharSetProber.get_confidence()}
|
||||
self.done = True
|
||||
elif self._mInputState == eHighbyte:
|
||||
if not self._mCharSetProbers:
|
||||
@@ -4,7 +4,7 @@
|
||||
# This module is part of urllib3 and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
from collections import MutableMapping
|
||||
from collections import Mapping, MutableMapping
|
||||
try:
|
||||
from threading import RLock
|
||||
except ImportError: # Platform-specific: No threads available
|
||||
@@ -20,9 +20,10 @@ try: # Python 2.7+
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from .packages.ordered_dict import OrderedDict
|
||||
from .packages.six import itervalues
|
||||
|
||||
|
||||
__all__ = ['RecentlyUsedContainer']
|
||||
__all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict']
|
||||
|
||||
|
||||
_Null = object()
|
||||
@@ -101,3 +102,104 @@ class RecentlyUsedContainer(MutableMapping):
|
||||
def keys(self):
|
||||
with self.lock:
|
||||
return self._container.keys()
|
||||
|
||||
|
||||
class HTTPHeaderDict(MutableMapping):
|
||||
"""
|
||||
:param headers:
|
||||
An iterable of field-value pairs. Must not contain multiple field names
|
||||
when compared case-insensitively.
|
||||
|
||||
:param kwargs:
|
||||
Additional field-value pairs to pass in to ``dict.update``.
|
||||
|
||||
A ``dict`` like container for storing HTTP Headers.
|
||||
|
||||
Field names are stored and compared case-insensitively in compliance with
|
||||
RFC 2616. Iteration provides the first case-sensitive key seen for each
|
||||
case-insensitive pair.
|
||||
|
||||
Using ``__setitem__`` syntax overwrites fields that compare equal
|
||||
case-insensitively in order to maintain ``dict``'s api. For fields that
|
||||
compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add``
|
||||
in a loop.
|
||||
|
||||
If multiple fields that are equal case-insensitively are passed to the
|
||||
constructor or ``.update``, the behavior is undefined and some will be
|
||||
lost.
|
||||
|
||||
>>> headers = HTTPHeaderDict()
|
||||
>>> headers.add('Set-Cookie', 'foo=bar')
|
||||
>>> headers.add('set-cookie', 'baz=quxx')
|
||||
>>> headers['content-length'] = '7'
|
||||
>>> headers['SET-cookie']
|
||||
'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)
|
||||
|
||||
def add(self, key, value):
|
||||
"""Adds a (name, value) pair, doesn't overwrite the value if it already
|
||||
exists.
|
||||
|
||||
>>> headers = HTTPHeaderDict(foo='bar')
|
||||
>>> headers.add('Foo', 'baz')
|
||||
>>> headers['foo']
|
||||
'bar, baz'
|
||||
"""
|
||||
self._data.setdefault(key.lower(), []).append((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 []
|
||||
|
||||
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]
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r)' % (self.__class__.__name__, dict(self.items()))
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
# This module is part of urllib3 and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
import sys
|
||||
import socket
|
||||
from socket import timeout as SocketTimeout
|
||||
|
||||
try: # Python 3
|
||||
from http.client import HTTPConnection, HTTPException
|
||||
from http.client import HTTPConnection as _HTTPConnection, HTTPException
|
||||
except ImportError:
|
||||
from httplib import HTTPConnection, HTTPException
|
||||
from httplib import HTTPConnection as _HTTPConnection, HTTPException
|
||||
|
||||
class DummyConnection(object):
|
||||
"Used to detect a failed ConnectionCls import."
|
||||
@@ -24,9 +25,9 @@ try: # Compiled with SSL?
|
||||
pass
|
||||
|
||||
try: # Python 3
|
||||
from http.client import HTTPSConnection
|
||||
from http.client import HTTPSConnection as _HTTPSConnection
|
||||
except ImportError:
|
||||
from httplib import HTTPSConnection
|
||||
from httplib import HTTPSConnection as _HTTPSConnection
|
||||
|
||||
import ssl
|
||||
BaseSSLError = ssl.SSLError
|
||||
@@ -38,6 +39,7 @@ from .exceptions import (
|
||||
ConnectTimeoutError,
|
||||
)
|
||||
from .packages.ssl_match_hostname import match_hostname
|
||||
from .packages import six
|
||||
from .util import (
|
||||
assert_fingerprint,
|
||||
resolve_cert_reqs,
|
||||
@@ -45,6 +47,88 @@ from .util import (
|
||||
ssl_wrap_socket,
|
||||
)
|
||||
|
||||
|
||||
port_by_scheme = {
|
||||
'http': 80,
|
||||
'https': 443,
|
||||
}
|
||||
|
||||
|
||||
class HTTPConnection(_HTTPConnection, object):
|
||||
"""
|
||||
Based on httplib.HTTPConnection but provides an extra constructor
|
||||
backwards-compatibility layer between older and newer Pythons.
|
||||
"""
|
||||
|
||||
default_port = port_by_scheme['http']
|
||||
|
||||
# By default, disable Nagle's Algorithm.
|
||||
tcp_nodelay = 1
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
if six.PY3: # Python 3
|
||||
kw.pop('strict', None)
|
||||
if sys.version_info < (2, 7): # Python 2.6 and older
|
||||
kw.pop('source_address', None)
|
||||
|
||||
# Pre-set source_address in case we have an older Python like 2.6.
|
||||
self.source_address = kw.get('source_address')
|
||||
|
||||
# Superclass also sets self.source_address in Python 2.7+.
|
||||
_HTTPConnection.__init__(self, *args, **kw)
|
||||
|
||||
def _new_conn(self):
|
||||
""" Establish a socket connection and set nodelay settings on it.
|
||||
|
||||
:return: a new socket connection
|
||||
"""
|
||||
extra_args = []
|
||||
if self.source_address: # Python 2.7+
|
||||
extra_args.append(self.source_address)
|
||||
|
||||
conn = socket.create_connection(
|
||||
(self.host, self.port), self.timeout, *extra_args)
|
||||
conn.setsockopt(
|
||||
socket.IPPROTO_TCP, socket.TCP_NODELAY, self.tcp_nodelay)
|
||||
|
||||
return conn
|
||||
|
||||
def _prepare_conn(self, conn):
|
||||
self.sock = conn
|
||||
# the _tunnel_host attribute was added in python 2.6.3 (via
|
||||
# http://hg.python.org/cpython/rev/0f57b30a152f) so pythons 2.6(0-2) do
|
||||
# not have them.
|
||||
if getattr(self, '_tunnel_host', None):
|
||||
# TODO: Fix tunnel so it doesn't depend on self.sock state.
|
||||
self._tunnel()
|
||||
|
||||
def connect(self):
|
||||
conn = self._new_conn()
|
||||
self._prepare_conn(conn)
|
||||
|
||||
|
||||
class HTTPSConnection(HTTPConnection):
|
||||
default_port = port_by_scheme['https']
|
||||
|
||||
def __init__(self, host, port=None, key_file=None, cert_file=None,
|
||||
strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, **kw):
|
||||
|
||||
HTTPConnection.__init__(self, host, port, strict=strict,
|
||||
timeout=timeout, **kw)
|
||||
|
||||
self.key_file = key_file
|
||||
self.cert_file = cert_file
|
||||
|
||||
# Required property for Google AppEngine 1.9.0 which otherwise causes
|
||||
# HTTPS requests to go out as HTTP. (See Issue #356)
|
||||
self._protocol = 'https'
|
||||
|
||||
def connect(self):
|
||||
conn = self._new_conn()
|
||||
self._prepare_conn(conn)
|
||||
self.sock = ssl.wrap_socket(conn, self.key_file, self.cert_file)
|
||||
|
||||
|
||||
class VerifiedHTTPSConnection(HTTPSConnection):
|
||||
"""
|
||||
Based on httplib.HTTPSConnection but wraps the socket with
|
||||
@@ -53,6 +137,7 @@ class VerifiedHTTPSConnection(HTTPSConnection):
|
||||
cert_reqs = None
|
||||
ca_certs = None
|
||||
ssl_version = None
|
||||
conn_kw = {}
|
||||
|
||||
def set_cert(self, key_file=None, cert_file=None,
|
||||
cert_reqs=None, ca_certs=None,
|
||||
@@ -67,31 +152,41 @@ class VerifiedHTTPSConnection(HTTPSConnection):
|
||||
|
||||
def connect(self):
|
||||
# Add certificate verification
|
||||
|
||||
try:
|
||||
sock = socket.create_connection(
|
||||
address=(self.host, self.port),
|
||||
timeout=self.timeout,
|
||||
)
|
||||
address=(self.host, self.port), timeout=self.timeout,
|
||||
**self.conn_kw)
|
||||
except SocketTimeout:
|
||||
raise ConnectTimeoutError(
|
||||
self, "Connection to %s timed out. (connect timeout=%s)" %
|
||||
(self.host, self.timeout))
|
||||
raise ConnectTimeoutError(
|
||||
self, "Connection to %s timed out. (connect timeout=%s)" %
|
||||
(self.host, self.timeout))
|
||||
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY,
|
||||
self.tcp_nodelay)
|
||||
|
||||
resolved_cert_reqs = resolve_cert_reqs(self.cert_reqs)
|
||||
resolved_ssl_version = resolve_ssl_version(self.ssl_version)
|
||||
|
||||
if self._tunnel_host:
|
||||
hostname = self.host
|
||||
if getattr(self, '_tunnel_host', None):
|
||||
# _tunnel_host was added in Python 2.6.3
|
||||
# (See: http://hg.python.org/cpython/rev/0f57b30a152f)
|
||||
|
||||
self.sock = sock
|
||||
# Calls self._set_hostport(), so self.host is
|
||||
# self._tunnel_host below.
|
||||
self._tunnel()
|
||||
|
||||
# Override the host with the one we're requesting data from.
|
||||
hostname = self._tunnel_host
|
||||
|
||||
# Wrap socket using verification with the root certs in
|
||||
# trusted_root_certs
|
||||
self.sock = ssl_wrap_socket(sock, self.key_file, self.cert_file,
|
||||
cert_reqs=resolved_cert_reqs,
|
||||
ca_certs=self.ca_certs,
|
||||
server_hostname=self.host,
|
||||
server_hostname=hostname,
|
||||
ssl_version=resolved_ssl_version)
|
||||
|
||||
if resolved_cert_reqs != ssl.CERT_NONE:
|
||||
@@ -100,8 +195,10 @@ class VerifiedHTTPSConnection(HTTPSConnection):
|
||||
self.assert_fingerprint)
|
||||
elif self.assert_hostname is not False:
|
||||
match_hostname(self.sock.getpeercert(),
|
||||
self.assert_hostname or self.host)
|
||||
self.assert_hostname or hostname)
|
||||
|
||||
|
||||
if ssl:
|
||||
# Make a copy for testing.
|
||||
UnverifiedHTTPSConnection = HTTPSConnection
|
||||
HTTPSConnection = VerifiedHTTPSConnection
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# This module is part of urllib3 and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
import sys
|
||||
import errno
|
||||
import logging
|
||||
|
||||
@@ -19,9 +20,11 @@ except ImportError:
|
||||
|
||||
from .exceptions import (
|
||||
ClosedPoolError,
|
||||
ConnectionError,
|
||||
ConnectTimeoutError,
|
||||
EmptyPoolError,
|
||||
HostChangedError,
|
||||
LocationParseError,
|
||||
MaxRetryError,
|
||||
SSLError,
|
||||
TimeoutError,
|
||||
@@ -31,6 +34,7 @@ from .exceptions import (
|
||||
from .packages.ssl_match_hostname import CertificateError
|
||||
from .packages import six
|
||||
from .connection import (
|
||||
port_by_scheme,
|
||||
DummyConnection,
|
||||
HTTPConnection, HTTPSConnection, VerifiedHTTPSConnection,
|
||||
HTTPException, BaseSSLError,
|
||||
@@ -38,7 +42,6 @@ from .connection import (
|
||||
from .request import RequestMethods
|
||||
from .response import HTTPResponse
|
||||
from .util import (
|
||||
assert_fingerprint,
|
||||
get_host,
|
||||
is_connection_dropped,
|
||||
Timeout,
|
||||
@@ -51,12 +54,6 @@ log = logging.getLogger(__name__)
|
||||
|
||||
_Default = object()
|
||||
|
||||
port_by_scheme = {
|
||||
'http': 80,
|
||||
'https': 443,
|
||||
}
|
||||
|
||||
|
||||
## Pool objects
|
||||
|
||||
class ConnectionPool(object):
|
||||
@@ -69,6 +66,9 @@ class ConnectionPool(object):
|
||||
QueueCls = LifoQueue
|
||||
|
||||
def __init__(self, host, port=None):
|
||||
if host is None:
|
||||
raise LocationParseError(host)
|
||||
|
||||
# httplib doesn't like it when we include brackets in ipv6 addresses
|
||||
host = host.strip('[]')
|
||||
|
||||
@@ -140,7 +140,7 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
|
||||
def __init__(self, host, port=None, strict=False,
|
||||
timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, block=False,
|
||||
headers=None, _proxy=None, _proxy_headers=None):
|
||||
headers=None, _proxy=None, _proxy_headers=None, **conn_kw):
|
||||
ConnectionPool.__init__(self, host, port)
|
||||
RequestMethods.__init__(self, headers)
|
||||
|
||||
@@ -167,21 +167,26 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
self.num_connections = 0
|
||||
self.num_requests = 0
|
||||
|
||||
if sys.version_info < (2, 7): # Python 2.6 and older
|
||||
conn_kw.pop('source_address', None)
|
||||
self.conn_kw = conn_kw
|
||||
|
||||
def _new_conn(self):
|
||||
"""
|
||||
Return a fresh :class:`httplib.HTTPConnection`.
|
||||
Return a fresh :class:`HTTPConnection`.
|
||||
"""
|
||||
self.num_connections += 1
|
||||
log.info("Starting new HTTP connection (%d): %s" %
|
||||
(self.num_connections, self.host))
|
||||
|
||||
extra_params = {}
|
||||
if not six.PY3: # Python 2
|
||||
extra_params['strict'] = self.strict
|
||||
|
||||
return self.ConnectionCls(host=self.host, port=self.port,
|
||||
conn = self.ConnectionCls(host=self.host, port=self.port,
|
||||
timeout=self.timeout.connect_timeout,
|
||||
**extra_params)
|
||||
strict=self.strict, **self.conn_kw)
|
||||
if self.proxy is not None:
|
||||
# Enable Nagle's algorithm for proxies, to avoid packet
|
||||
# fragmentation.
|
||||
conn.tcp_nodelay = 0
|
||||
return conn
|
||||
|
||||
def _get_conn(self, timeout=None):
|
||||
"""
|
||||
@@ -238,8 +243,9 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
pass
|
||||
except Full:
|
||||
# This should never happen if self.block == True
|
||||
log.warning("HttpConnectionPool is full, discarding connection: %s"
|
||||
% self.host)
|
||||
log.warning(
|
||||
"Connection pool is full, discarding connection: %s" %
|
||||
self.host)
|
||||
|
||||
# Connection never got put back into the pool, close it.
|
||||
if conn:
|
||||
@@ -260,7 +266,7 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
def _make_request(self, conn, method, url, timeout=_Default,
|
||||
**httplib_request_kw):
|
||||
"""
|
||||
Perform a request on a given httplib connection object taken from our
|
||||
Perform a request on a given urllib connection object taken from our
|
||||
pool.
|
||||
|
||||
:param conn:
|
||||
@@ -371,9 +377,11 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
# TODO: Add optional support for socket.gethostbyname checking.
|
||||
scheme, host, port = get_host(url)
|
||||
|
||||
# Use explicit default port for comparison when none is given
|
||||
if self.port and not port:
|
||||
# Use explicit default port for comparison when none is given.
|
||||
port = port_by_scheme.get(scheme)
|
||||
elif not self.port and port == port_by_scheme.get(scheme):
|
||||
port = None
|
||||
|
||||
return (scheme, host, port) == (self.scheme, self.host, self.port)
|
||||
|
||||
@@ -412,10 +420,13 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
|
||||
:param retries:
|
||||
Number of retries to allow before raising a MaxRetryError exception.
|
||||
If `False`, then retries are disabled and any exception is raised
|
||||
immediately.
|
||||
|
||||
:param redirect:
|
||||
If True, automatically handle redirects (status codes 301, 302,
|
||||
303, 307, 308). Each redirect counts as a retry.
|
||||
303, 307, 308). Each redirect counts as a retry. Disabling retries
|
||||
will disable redirect, too.
|
||||
|
||||
:param assert_same_host:
|
||||
If ``True``, will make sure that the host of the pool requests is
|
||||
@@ -449,7 +460,7 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
if headers is None:
|
||||
headers = self.headers
|
||||
|
||||
if retries < 0:
|
||||
if retries < 0 and retries is not False:
|
||||
raise MaxRetryError(self, url)
|
||||
|
||||
if release_conn is None:
|
||||
@@ -468,6 +479,10 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
headers = headers.copy()
|
||||
headers.update(self.proxy_headers)
|
||||
|
||||
# Must keep the exception bound to a separate variable or else Python 3
|
||||
# complains about UnboundLocalError.
|
||||
err = None
|
||||
|
||||
try:
|
||||
# Request a connection from the queue
|
||||
conn = self._get_conn(timeout=pool_timeout)
|
||||
@@ -495,38 +510,41 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
# ``response.read()``)
|
||||
|
||||
except Empty:
|
||||
# Timed out by queue
|
||||
# Timed out by queue.
|
||||
raise EmptyPoolError(self, "No pool connections are available.")
|
||||
|
||||
except BaseSSLError as e:
|
||||
except (BaseSSLError, CertificateError) as e:
|
||||
# Release connection unconditionally because there is no way to
|
||||
# close it externally in case of exception.
|
||||
release_conn = True
|
||||
raise SSLError(e)
|
||||
|
||||
except CertificateError as e:
|
||||
# Name mismatch
|
||||
raise SSLError(e)
|
||||
except (TimeoutError, HTTPException, SocketError) as e:
|
||||
if conn:
|
||||
# Discard the connection for these exceptions. It will be
|
||||
# be replaced during the next _get_conn() call.
|
||||
conn.close()
|
||||
conn = None
|
||||
|
||||
except TimeoutError as e:
|
||||
# Connection broken, discard.
|
||||
conn = None
|
||||
# Save the error off for retry logic.
|
||||
err = e
|
||||
if not retries:
|
||||
if isinstance(e, TimeoutError):
|
||||
# TimeoutError is exempt from MaxRetryError-wrapping.
|
||||
# FIXME: ... Not sure why. Add a reason here.
|
||||
raise
|
||||
|
||||
if retries == 0:
|
||||
raise
|
||||
# Wrap unexpected exceptions with the most appropriate
|
||||
# module-level exception and re-raise.
|
||||
if isinstance(e, SocketError) and self.proxy:
|
||||
raise ProxyError('Cannot connect to proxy.', e)
|
||||
|
||||
except (HTTPException, SocketError) as e:
|
||||
if isinstance(e, SocketError) and self.proxy is not None:
|
||||
raise ProxyError('Cannot connect to proxy. '
|
||||
'Socket error: %s.' % e)
|
||||
if retries is False:
|
||||
raise ConnectionError('Connection failed.', e)
|
||||
|
||||
# Connection broken, discard. It will be replaced next _get_conn().
|
||||
conn = None
|
||||
# This is necessary so we can access e below
|
||||
err = e
|
||||
|
||||
if retries == 0:
|
||||
raise MaxRetryError(self, url, e)
|
||||
|
||||
# Keep track of the error for the retry warning.
|
||||
err = e
|
||||
|
||||
finally:
|
||||
if release_conn:
|
||||
# Put the connection back to be reused. If the connection is
|
||||
@@ -536,8 +554,8 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
|
||||
if not conn:
|
||||
# Try again
|
||||
log.warn("Retrying (%d attempts remain) after connection "
|
||||
"broken by '%r': %s" % (retries, err, url))
|
||||
log.warning("Retrying (%d attempts remain) after connection "
|
||||
"broken by '%r': %s" % (retries, err, url))
|
||||
return self.urlopen(method, url, body, headers, retries - 1,
|
||||
redirect, assert_same_host,
|
||||
timeout=timeout, pool_timeout=pool_timeout,
|
||||
@@ -545,7 +563,7 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
|
||||
# Handle redirect?
|
||||
redirect_location = redirect and response.get_redirect_location()
|
||||
if redirect_location:
|
||||
if redirect_location and retries is not False:
|
||||
if response.status == 303:
|
||||
method = 'GET'
|
||||
log.info("Redirecting %s -> %s" % (url, redirect_location))
|
||||
@@ -563,7 +581,7 @@ class HTTPSConnectionPool(HTTPConnectionPool):
|
||||
|
||||
When Python is compiled with the :mod:`ssl` module, then
|
||||
:class:`.VerifiedHTTPSConnection` is used, which *can* verify certificates,
|
||||
instead of :class:`httplib.HTTPSConnection`.
|
||||
instead of :class:`.HTTPSConnection`.
|
||||
|
||||
:class:`.VerifiedHTTPSConnection` uses one of ``assert_fingerprint``,
|
||||
``assert_hostname`` and ``host`` in this order to verify connections.
|
||||
@@ -584,10 +602,14 @@ class HTTPSConnectionPool(HTTPConnectionPool):
|
||||
_proxy=None, _proxy_headers=None,
|
||||
key_file=None, cert_file=None, cert_reqs=None,
|
||||
ca_certs=None, ssl_version=None,
|
||||
assert_hostname=None, assert_fingerprint=None):
|
||||
assert_hostname=None, assert_fingerprint=None,
|
||||
**conn_kw):
|
||||
|
||||
if sys.version_info < (2, 7): # Python 2.6 or older
|
||||
conn_kw.pop('source_address', None)
|
||||
|
||||
HTTPConnectionPool.__init__(self, host, port, strict, timeout, maxsize,
|
||||
block, headers, _proxy, _proxy_headers)
|
||||
block, headers, _proxy, _proxy_headers, **conn_kw)
|
||||
self.key_file = key_file
|
||||
self.cert_file = cert_file
|
||||
self.cert_reqs = cert_reqs
|
||||
@@ -595,6 +617,7 @@ class HTTPSConnectionPool(HTTPConnectionPool):
|
||||
self.ssl_version = ssl_version
|
||||
self.assert_hostname = assert_hostname
|
||||
self.assert_fingerprint = assert_fingerprint
|
||||
self.conn_kw = conn_kw
|
||||
|
||||
def _prepare_conn(self, conn):
|
||||
"""
|
||||
@@ -610,6 +633,7 @@ class HTTPSConnectionPool(HTTPConnectionPool):
|
||||
assert_hostname=self.assert_hostname,
|
||||
assert_fingerprint=self.assert_fingerprint)
|
||||
conn.ssl_version = self.ssl_version
|
||||
conn.conn_kw = self.conn_kw
|
||||
|
||||
if self.proxy is not None:
|
||||
# Python 2.7+
|
||||
@@ -646,10 +670,15 @@ class HTTPSConnectionPool(HTTPConnectionPool):
|
||||
extra_params = {}
|
||||
if not six.PY3: # Python 2
|
||||
extra_params['strict'] = self.strict
|
||||
extra_params.update(self.conn_kw)
|
||||
|
||||
conn = self.ConnectionCls(host=actual_host, port=actual_port,
|
||||
timeout=self.timeout.connect_timeout,
|
||||
**extra_params)
|
||||
if self.proxy is not None:
|
||||
# Enable Nagle's algorithm for proxies, to avoid packet
|
||||
# fragmentation.
|
||||
conn.tcp_nodelay = 0
|
||||
|
||||
return self._prepare_conn(conn)
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
'''SSL with SNI-support for Python 2.
|
||||
'''SSL with SNI_-support for Python 2. Follow these instructions if you would
|
||||
like to verify SSL certificates in Python 2. Note, the default libraries do
|
||||
*not* do certificate checking; you need to do additional work to validate
|
||||
certificates yourself.
|
||||
|
||||
This needs the following packages installed:
|
||||
|
||||
@@ -6,9 +9,15 @@ This needs the following packages installed:
|
||||
* ndg-httpsclient (tested with 0.3.2)
|
||||
* pyasn1 (tested with 0.1.6)
|
||||
|
||||
To activate it call :func:`~urllib3.contrib.pyopenssl.inject_into_urllib3`.
|
||||
This can be done in a ``sitecustomize`` module, or at any other time before
|
||||
your application begins using ``urllib3``, like this::
|
||||
You can install them with the following command:
|
||||
|
||||
pip install pyopenssl ndg-httpsclient pyasn1
|
||||
|
||||
To activate certificate checking, call
|
||||
:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code
|
||||
before you begin making HTTP requests. This can be done in a ``sitecustomize``
|
||||
module, or at any other time before your application begins using ``urllib3``,
|
||||
like this::
|
||||
|
||||
try:
|
||||
import urllib3.contrib.pyopenssl
|
||||
@@ -18,13 +27,31 @@ your application begins using ``urllib3``, like this::
|
||||
|
||||
Now you can use :mod:`urllib3` as you normally would, and it will support SNI
|
||||
when the required modules are installed.
|
||||
|
||||
Activating this module also has the positive side effect of disabling SSL/TLS
|
||||
encryption in Python 2 (see `CRIME attack`_).
|
||||
|
||||
If you want to configure the default list of supported cipher suites, you can
|
||||
set the ``urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST`` variable.
|
||||
|
||||
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)
|
||||
|
||||
'''
|
||||
|
||||
from ndg.httpsclient.ssl_peer_verification import SUBJ_ALT_NAME_SUPPORT
|
||||
from ndg.httpsclient.subj_alt_name import SubjectAltName
|
||||
from ndg.httpsclient.subj_alt_name import SubjectAltName as BaseSubjectAltName
|
||||
import OpenSSL.SSL
|
||||
from pyasn1.codec.der import decoder as der_decoder
|
||||
from socket import _fileobject
|
||||
from pyasn1.type import univ, constraint
|
||||
from socket import _fileobject, timeout
|
||||
import ssl
|
||||
import select
|
||||
from cStringIO import StringIO
|
||||
@@ -50,6 +77,23 @@ _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"
|
||||
|
||||
|
||||
orig_util_HAS_SNI = util.HAS_SNI
|
||||
orig_connection_ssl_wrap_socket = connection.ssl_wrap_socket
|
||||
@@ -69,6 +113,17 @@ def extract_from_urllib3():
|
||||
util.HAS_SNI = orig_util_HAS_SNI
|
||||
|
||||
|
||||
### Note: This is a slightly bug-fixed version of same from ndg-httpsclient.
|
||||
class SubjectAltName(BaseSubjectAltName):
|
||||
'''ASN.1 implementation for subjectAltNames support'''
|
||||
|
||||
# There is no limit to how many SAN certificates a certificate may have,
|
||||
# however this needs to have some limit so we'll set an arbitrarily high
|
||||
# limit.
|
||||
sizeSpec = univ.SequenceOf.sizeSpec + \
|
||||
constraint.ValueSizeConstraint(1, 1024)
|
||||
|
||||
|
||||
### Note: This is a slightly bug-fixed version of same from ndg-httpsclient.
|
||||
def get_subj_alt_name(peer_cert):
|
||||
# Search through extensions
|
||||
@@ -102,6 +157,13 @@ def get_subj_alt_name(peer_cert):
|
||||
|
||||
class fileobject(_fileobject):
|
||||
|
||||
def _wait_for_sock(self):
|
||||
rd, wd, ed = select.select([self._sock], [], [],
|
||||
self._sock.gettimeout())
|
||||
if not rd:
|
||||
raise timeout()
|
||||
|
||||
|
||||
def read(self, size=-1):
|
||||
# Use max, disallow tiny reads in a loop as they are very inefficient.
|
||||
# We never leave read() with any leftover data from a new recv() call
|
||||
@@ -119,6 +181,7 @@ class fileobject(_fileobject):
|
||||
try:
|
||||
data = self._sock.recv(rbufsize)
|
||||
except OpenSSL.SSL.WantReadError:
|
||||
self._wait_for_sock()
|
||||
continue
|
||||
if not data:
|
||||
break
|
||||
@@ -146,6 +209,7 @@ class fileobject(_fileobject):
|
||||
try:
|
||||
data = self._sock.recv(left)
|
||||
except OpenSSL.SSL.WantReadError:
|
||||
self._wait_for_sock()
|
||||
continue
|
||||
if not data:
|
||||
break
|
||||
@@ -197,6 +261,7 @@ class fileobject(_fileobject):
|
||||
break
|
||||
buffers.append(data)
|
||||
except OpenSSL.SSL.WantReadError:
|
||||
self._wait_for_sock()
|
||||
continue
|
||||
break
|
||||
return "".join(buffers)
|
||||
@@ -207,6 +272,7 @@ class fileobject(_fileobject):
|
||||
try:
|
||||
data = self._sock.recv(self._rbufsize)
|
||||
except OpenSSL.SSL.WantReadError:
|
||||
self._wait_for_sock()
|
||||
continue
|
||||
if not data:
|
||||
break
|
||||
@@ -234,7 +300,8 @@ class fileobject(_fileobject):
|
||||
try:
|
||||
data = self._sock.recv(self._rbufsize)
|
||||
except OpenSSL.SSL.WantReadError:
|
||||
continue
|
||||
self._wait_for_sock()
|
||||
continue
|
||||
if not data:
|
||||
break
|
||||
left = size - buf_len
|
||||
@@ -329,6 +396,15 @@ def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
|
||||
ctx.load_verify_locations(ca_certs, None)
|
||||
except OpenSSL.SSL.Error as e:
|
||||
raise ssl.SSLError('bad ca_certs: %r' % ca_certs, e)
|
||||
else:
|
||||
ctx.set_default_verify_paths()
|
||||
|
||||
# Disable TLS compression to migitate CRIME attack (issue #309)
|
||||
OP_NO_COMPRESSION = 0x20000
|
||||
ctx.set_options(OP_NO_COMPRESSION)
|
||||
|
||||
# Set list of supported ciphersuites.
|
||||
ctx.set_cipher_list(DEFAULT_SSL_CIPHER_LIST)
|
||||
|
||||
cnx = OpenSSL.SSL.Connection(ctx, sock)
|
||||
cnx.set_tlsext_host_name(server_hostname)
|
||||
|
||||
@@ -44,6 +44,11 @@ class ProxyError(HTTPError):
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionError(HTTPError):
|
||||
"Raised when a normal connection fails."
|
||||
pass
|
||||
|
||||
|
||||
class DecodeError(HTTPError):
|
||||
"Raised when automatic decoding based on Content-Type fails."
|
||||
pass
|
||||
|
||||
@@ -15,7 +15,7 @@ def guess_content_type(filename, default='application/octet-stream'):
|
||||
Guess the "Content-Type" of a file.
|
||||
|
||||
:param filename:
|
||||
The filename to guess the "Content-Type" of using :mod:`mimetimes`.
|
||||
The filename to guess the "Content-Type" of using :mod:`mimetypes`.
|
||||
:param default:
|
||||
If no "Content-Type" can be guessed, default to `default`.
|
||||
"""
|
||||
|
||||
@@ -46,16 +46,15 @@ def iter_field_objects(fields):
|
||||
|
||||
def iter_fields(fields):
|
||||
"""
|
||||
.. deprecated:: 1.6
|
||||
|
||||
Iterate over fields.
|
||||
|
||||
.. deprecated ::
|
||||
|
||||
The addition of `~urllib3.fields.RequestField` makes this function
|
||||
obsolete. Instead, use :func:`iter_field_objects`, which returns
|
||||
`~urllib3.fields.RequestField` objects, instead.
|
||||
The addition of :class:`~urllib3.fields.RequestField` makes this function
|
||||
obsolete. Instead, use :func:`iter_field_objects`, which returns
|
||||
:class:`~urllib3.fields.RequestField` objects.
|
||||
|
||||
Supports list of (k, v) tuples and dicts.
|
||||
|
||||
"""
|
||||
if isinstance(fields, dict):
|
||||
return ((k, v) for k, v in six.iteritems(fields))
|
||||
|
||||
@@ -7,7 +7,7 @@ except ImportError:
|
||||
from backports.ssl_match_hostname import CertificateError, match_hostname
|
||||
except ImportError:
|
||||
# Our vendored copy
|
||||
from _implementation import CertificateError, match_hostname
|
||||
from ._implementation import CertificateError, match_hostname
|
||||
|
||||
# Not needed, but documenting what we provide.
|
||||
__all__ = ('CertificateError', 'match_hostname')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# urllib3/poolmanager.py
|
||||
# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt)
|
||||
# Copyright 2008-2014 Andrey Petrov and contributors (see CONTRIBUTORS.txt)
|
||||
#
|
||||
# This module is part of urllib3 and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -176,7 +176,7 @@ class ProxyManager(PoolManager):
|
||||
Behaves just like :class:`PoolManager`, but sends all requests through
|
||||
the defined proxy, using the CONNECT method for HTTPS URLs.
|
||||
|
||||
:param poxy_url:
|
||||
:param proxy_url:
|
||||
The URL of the proxy to be used.
|
||||
|
||||
:param proxy_headers:
|
||||
|
||||
@@ -45,7 +45,6 @@ class RequestMethods(object):
|
||||
"""
|
||||
|
||||
_encode_url_methods = set(['DELETE', 'GET', 'HEAD', 'OPTIONS'])
|
||||
_encode_body_methods = set(['PATCH', 'POST', 'PUT', 'TRACE'])
|
||||
|
||||
def __init__(self, headers=None):
|
||||
self.headers = headers or {}
|
||||
|
||||
@@ -9,6 +9,7 @@ import logging
|
||||
import zlib
|
||||
import io
|
||||
|
||||
from ._collections import HTTPHeaderDict
|
||||
from .exceptions import DecodeError
|
||||
from .packages.six import string_types as basestring, binary_type
|
||||
from .util import is_fp_closed
|
||||
@@ -79,7 +80,10 @@ class HTTPResponse(io.IOBase):
|
||||
def __init__(self, body='', headers=None, status=0, version=0, reason=None,
|
||||
strict=0, preload_content=True, decode_content=True,
|
||||
original_response=None, pool=None, connection=None):
|
||||
self.headers = headers or {}
|
||||
|
||||
self.headers = HTTPHeaderDict()
|
||||
if headers:
|
||||
self.headers.update(headers)
|
||||
self.status = status
|
||||
self.version = version
|
||||
self.reason = reason
|
||||
@@ -249,17 +253,9 @@ class HTTPResponse(io.IOBase):
|
||||
with ``original_response=r``.
|
||||
"""
|
||||
|
||||
# Normalize headers between different versions of Python
|
||||
headers = {}
|
||||
headers = HTTPHeaderDict()
|
||||
for k, v in r.getheaders():
|
||||
# Python 3: Header keys are returned capitalised
|
||||
k = k.lower()
|
||||
|
||||
has_value = headers.get(k)
|
||||
if has_value: # Python 3: Repeating header keys are unmerged.
|
||||
v = ', '.join([has_value, v])
|
||||
|
||||
headers[k] = v
|
||||
headers.add(k, v)
|
||||
|
||||
# HTTPResponse objects in Python 3 don't have a .strict attribute
|
||||
strict = getattr(r, 'strict', 0)
|
||||
|
||||
@@ -1,643 +0,0 @@
|
||||
# urllib3/util.py
|
||||
# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt)
|
||||
#
|
||||
# This module is part of urllib3 and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
|
||||
from base64 import b64encode
|
||||
from binascii import hexlify, unhexlify
|
||||
from collections import namedtuple
|
||||
from hashlib import md5, sha1
|
||||
from socket import error as SocketError, _GLOBAL_DEFAULT_TIMEOUT
|
||||
import time
|
||||
|
||||
try:
|
||||
from select import poll, POLLIN
|
||||
except ImportError: # `poll` doesn't exist on OSX and other platforms
|
||||
poll = False
|
||||
try:
|
||||
from select import select
|
||||
except ImportError: # `select` doesn't exist on AppEngine.
|
||||
select = False
|
||||
|
||||
try: # Test for SSL features
|
||||
SSLContext = None
|
||||
HAS_SNI = False
|
||||
|
||||
import ssl
|
||||
from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23
|
||||
from ssl import SSLContext # Modern SSL?
|
||||
from ssl import HAS_SNI # Has SNI?
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from .packages import six
|
||||
from .exceptions import LocationParseError, SSLError, TimeoutStateError
|
||||
|
||||
|
||||
_Default = object()
|
||||
# The default timeout to use for socket connections. This is the attribute used
|
||||
# by httplib to define the default timeout
|
||||
|
||||
|
||||
def current_time():
|
||||
"""
|
||||
Retrieve the current time, this function is mocked out in unit testing.
|
||||
"""
|
||||
return time.time()
|
||||
|
||||
|
||||
class Timeout(object):
|
||||
"""
|
||||
Utility object for storing timeout values.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
timeout = urllib3.util.Timeout(connect=2.0, read=7.0)
|
||||
pool = HTTPConnectionPool('www.google.com', 80, timeout=timeout)
|
||||
pool.request(...) # Etc, etc
|
||||
|
||||
:param connect:
|
||||
The maximum amount of time to wait for a connection attempt to a server
|
||||
to succeed. Omitting the parameter will default the connect timeout to
|
||||
the system default, probably `the global default timeout in socket.py
|
||||
<http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_.
|
||||
None will set an infinite timeout for connection attempts.
|
||||
|
||||
:type connect: integer, float, or None
|
||||
|
||||
:param read:
|
||||
The maximum amount of time to wait between consecutive
|
||||
read operations for a response from the server. Omitting
|
||||
the parameter will default the read timeout to the system
|
||||
default, probably `the global default timeout in socket.py
|
||||
<http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_.
|
||||
None will set an infinite timeout.
|
||||
|
||||
:type read: integer, float, or None
|
||||
|
||||
:param total:
|
||||
This combines the connect and read timeouts into one; the read timeout
|
||||
will be set to the time leftover from the connect attempt. In the
|
||||
event that both a connect timeout and a total are specified, or a read
|
||||
timeout and a total are specified, the shorter timeout will be applied.
|
||||
|
||||
Defaults to None.
|
||||
|
||||
:type total: integer, float, or None
|
||||
|
||||
.. note::
|
||||
|
||||
Many factors can affect the total amount of time for urllib3 to return
|
||||
an HTTP response. Specifically, Python's DNS resolver does not obey the
|
||||
timeout specified on the socket. Other factors that can affect total
|
||||
request time include high CPU load, high swap, the program running at a
|
||||
low priority level, or other behaviors. The observed running time for
|
||||
urllib3 to return a response may be greater than the value passed to
|
||||
`total`.
|
||||
|
||||
In addition, the read and total timeouts only measure the time between
|
||||
read operations on the socket connecting the client and the server,
|
||||
not the total amount of time for the request to return a complete
|
||||
response. For most requests, the timeout is raised because the server
|
||||
has not sent the first byte in the specified time. This is not always
|
||||
the case; if a server streams one byte every fifteen seconds, a timeout
|
||||
of 20 seconds will not ever trigger, even though the request will
|
||||
take several minutes to complete.
|
||||
|
||||
If your goal is to cut off any request after a set amount of wall clock
|
||||
time, consider having a second "watcher" thread to cut off a slow
|
||||
request.
|
||||
"""
|
||||
|
||||
#: A sentinel object representing the default timeout value
|
||||
DEFAULT_TIMEOUT = _GLOBAL_DEFAULT_TIMEOUT
|
||||
|
||||
def __init__(self, total=None, connect=_Default, read=_Default):
|
||||
self._connect = self._validate_timeout(connect, 'connect')
|
||||
self._read = self._validate_timeout(read, 'read')
|
||||
self.total = self._validate_timeout(total, 'total')
|
||||
self._start_connect = None
|
||||
|
||||
def __str__(self):
|
||||
return '%s(connect=%r, read=%r, total=%r)' % (
|
||||
type(self).__name__, self._connect, self._read, self.total)
|
||||
|
||||
|
||||
@classmethod
|
||||
def _validate_timeout(cls, value, name):
|
||||
""" Check that a timeout attribute is valid
|
||||
|
||||
:param value: The timeout value to validate
|
||||
:param name: The name of the timeout attribute to validate. This is used
|
||||
for clear error messages
|
||||
:return: the value
|
||||
:raises ValueError: if the type is not an integer or a float, or if it
|
||||
is a numeric value less than zero
|
||||
"""
|
||||
if value is _Default:
|
||||
return cls.DEFAULT_TIMEOUT
|
||||
|
||||
if value is None or value is cls.DEFAULT_TIMEOUT:
|
||||
return value
|
||||
|
||||
try:
|
||||
float(value)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError("Timeout value %s was %s, but it must be an "
|
||||
"int or float." % (name, value))
|
||||
|
||||
try:
|
||||
if value < 0:
|
||||
raise ValueError("Attempted to set %s timeout to %s, but the "
|
||||
"timeout cannot be set to a value less "
|
||||
"than 0." % (name, value))
|
||||
except TypeError: # Python 3
|
||||
raise ValueError("Timeout value %s was %s, but it must be an "
|
||||
"int or float." % (name, value))
|
||||
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def from_float(cls, timeout):
|
||||
""" Create a new Timeout from a legacy timeout value.
|
||||
|
||||
The timeout value used by httplib.py sets the same timeout on the
|
||||
connect(), and recv() socket requests. This creates a :class:`Timeout`
|
||||
object that sets the individual timeouts to the ``timeout`` value passed
|
||||
to this function.
|
||||
|
||||
:param timeout: The legacy timeout value
|
||||
:type timeout: integer, float, sentinel default object, or None
|
||||
:return: a Timeout object
|
||||
:rtype: :class:`Timeout`
|
||||
"""
|
||||
return Timeout(read=timeout, connect=timeout)
|
||||
|
||||
def clone(self):
|
||||
""" Create a copy of the timeout object
|
||||
|
||||
Timeout properties are stored per-pool but each request needs a fresh
|
||||
Timeout object to ensure each one has its own start/stop configured.
|
||||
|
||||
:return: a copy of the timeout object
|
||||
:rtype: :class:`Timeout`
|
||||
"""
|
||||
# We can't use copy.deepcopy because that will also create a new object
|
||||
# for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to
|
||||
# detect the user default.
|
||||
return Timeout(connect=self._connect, read=self._read,
|
||||
total=self.total)
|
||||
|
||||
def start_connect(self):
|
||||
""" Start the timeout clock, used during a connect() attempt
|
||||
|
||||
:raises urllib3.exceptions.TimeoutStateError: if you attempt
|
||||
to start a timer that has been started already.
|
||||
"""
|
||||
if self._start_connect is not None:
|
||||
raise TimeoutStateError("Timeout timer has already been started.")
|
||||
self._start_connect = current_time()
|
||||
return self._start_connect
|
||||
|
||||
def get_connect_duration(self):
|
||||
""" Gets the time elapsed since the call to :meth:`start_connect`.
|
||||
|
||||
:return: the elapsed time
|
||||
:rtype: float
|
||||
:raises urllib3.exceptions.TimeoutStateError: if you attempt
|
||||
to get duration for a timer that hasn't been started.
|
||||
"""
|
||||
if self._start_connect is None:
|
||||
raise TimeoutStateError("Can't get connect duration for timer "
|
||||
"that has not started.")
|
||||
return current_time() - self._start_connect
|
||||
|
||||
@property
|
||||
def connect_timeout(self):
|
||||
""" Get the value to use when setting a connection timeout.
|
||||
|
||||
This will be a positive float or integer, the value None
|
||||
(never timeout), or the default system timeout.
|
||||
|
||||
:return: the connect timeout
|
||||
:rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None
|
||||
"""
|
||||
if self.total is None:
|
||||
return self._connect
|
||||
|
||||
if self._connect is None or self._connect is self.DEFAULT_TIMEOUT:
|
||||
return self.total
|
||||
|
||||
return min(self._connect, self.total)
|
||||
|
||||
@property
|
||||
def read_timeout(self):
|
||||
""" Get the value for the read timeout.
|
||||
|
||||
This assumes some time has elapsed in the connection timeout and
|
||||
computes the read timeout appropriately.
|
||||
|
||||
If self.total is set, the read timeout is dependent on the amount of
|
||||
time taken by the connect timeout. If the connection time has not been
|
||||
established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be
|
||||
raised.
|
||||
|
||||
:return: the value to use for the read timeout
|
||||
:rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None
|
||||
:raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect`
|
||||
has not yet been called on this object.
|
||||
"""
|
||||
if (self.total is not None and
|
||||
self.total is not self.DEFAULT_TIMEOUT and
|
||||
self._read is not None and
|
||||
self._read is not self.DEFAULT_TIMEOUT):
|
||||
# in case the connect timeout has not yet been established.
|
||||
if self._start_connect is None:
|
||||
return self._read
|
||||
return max(0, min(self.total - self.get_connect_duration(),
|
||||
self._read))
|
||||
elif self.total is not None and self.total is not self.DEFAULT_TIMEOUT:
|
||||
return max(0, self.total - self.get_connect_duration())
|
||||
else:
|
||||
return self._read
|
||||
|
||||
|
||||
class Url(namedtuple('Url', ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment'])):
|
||||
"""
|
||||
Datastructure for representing an HTTP URL. Used as a return value for
|
||||
:func:`parse_url`.
|
||||
"""
|
||||
slots = ()
|
||||
|
||||
def __new__(cls, scheme=None, auth=None, host=None, port=None, path=None, query=None, fragment=None):
|
||||
return super(Url, cls).__new__(cls, scheme, auth, host, port, path, query, fragment)
|
||||
|
||||
@property
|
||||
def hostname(self):
|
||||
"""For backwards-compatibility with urlparse. We're nice like that."""
|
||||
return self.host
|
||||
|
||||
@property
|
||||
def request_uri(self):
|
||||
"""Absolute path including the query string."""
|
||||
uri = self.path or '/'
|
||||
|
||||
if self.query is not None:
|
||||
uri += '?' + self.query
|
||||
|
||||
return uri
|
||||
|
||||
@property
|
||||
def netloc(self):
|
||||
"""Network location including host and port"""
|
||||
if self.port:
|
||||
return '%s:%d' % (self.host, self.port)
|
||||
return self.host
|
||||
|
||||
|
||||
def split_first(s, delims):
|
||||
"""
|
||||
Given a string and an iterable of delimiters, split on the first found
|
||||
delimiter. Return two split parts and the matched delimiter.
|
||||
|
||||
If not found, then the first part is the full input string.
|
||||
|
||||
Example: ::
|
||||
|
||||
>>> split_first('foo/bar?baz', '?/=')
|
||||
('foo', 'bar?baz', '/')
|
||||
>>> split_first('foo/bar?baz', '123')
|
||||
('foo/bar?baz', '', None)
|
||||
|
||||
Scales linearly with number of delims. Not ideal for large number of delims.
|
||||
"""
|
||||
min_idx = None
|
||||
min_delim = None
|
||||
for d in delims:
|
||||
idx = s.find(d)
|
||||
if idx < 0:
|
||||
continue
|
||||
|
||||
if min_idx is None or idx < min_idx:
|
||||
min_idx = idx
|
||||
min_delim = d
|
||||
|
||||
if min_idx is None or min_idx < 0:
|
||||
return s, '', None
|
||||
|
||||
return s[:min_idx], s[min_idx+1:], min_delim
|
||||
|
||||
|
||||
def parse_url(url):
|
||||
"""
|
||||
Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is
|
||||
performed to parse incomplete urls. Fields not provided will be None.
|
||||
|
||||
Partly backwards-compatible with :mod:`urlparse`.
|
||||
|
||||
Example: ::
|
||||
|
||||
>>> parse_url('http://google.com/mail/')
|
||||
Url(scheme='http', host='google.com', port=None, path='/', ...)
|
||||
>>> parse_url('google.com:80')
|
||||
Url(scheme=None, host='google.com', port=80, path=None, ...)
|
||||
>>> parse_url('/foo?bar')
|
||||
Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...)
|
||||
"""
|
||||
|
||||
# While this code has overlap with stdlib's urlparse, it is much
|
||||
# simplified for our needs and less annoying.
|
||||
# Additionally, this implementations does silly things to be optimal
|
||||
# on CPython.
|
||||
|
||||
scheme = None
|
||||
auth = None
|
||||
host = None
|
||||
port = None
|
||||
path = None
|
||||
fragment = None
|
||||
query = None
|
||||
|
||||
# Scheme
|
||||
if '://' in url:
|
||||
scheme, url = url.split('://', 1)
|
||||
|
||||
# Find the earliest Authority Terminator
|
||||
# (http://tools.ietf.org/html/rfc3986#section-3.2)
|
||||
url, path_, delim = split_first(url, ['/', '?', '#'])
|
||||
|
||||
if delim:
|
||||
# Reassemble the path
|
||||
path = delim + path_
|
||||
|
||||
# Auth
|
||||
if '@' in url:
|
||||
# Last '@' denotes end of auth part
|
||||
auth, url = url.rsplit('@', 1)
|
||||
|
||||
# IPv6
|
||||
if url and url[0] == '[':
|
||||
host, url = url.split(']', 1)
|
||||
host += ']'
|
||||
|
||||
# Port
|
||||
if ':' in url:
|
||||
_host, port = url.split(':', 1)
|
||||
|
||||
if not host:
|
||||
host = _host
|
||||
|
||||
if port:
|
||||
# If given, ports must be integers.
|
||||
if not port.isdigit():
|
||||
raise LocationParseError("Failed to parse: %s" % url)
|
||||
port = int(port)
|
||||
else:
|
||||
# Blank ports are cool, too. (rfc3986#section-3.2.3)
|
||||
port = None
|
||||
|
||||
elif not host and url:
|
||||
host = url
|
||||
|
||||
if not path:
|
||||
return Url(scheme, auth, host, port, path, query, fragment)
|
||||
|
||||
# Fragment
|
||||
if '#' in path:
|
||||
path, fragment = path.split('#', 1)
|
||||
|
||||
# Query
|
||||
if '?' in path:
|
||||
path, query = path.split('?', 1)
|
||||
|
||||
return Url(scheme, auth, host, port, path, query, fragment)
|
||||
|
||||
|
||||
def get_host(url):
|
||||
"""
|
||||
Deprecated. Use :func:`.parse_url` instead.
|
||||
"""
|
||||
p = parse_url(url)
|
||||
return p.scheme or 'http', p.hostname, p.port
|
||||
|
||||
|
||||
def make_headers(keep_alive=None, accept_encoding=None, user_agent=None,
|
||||
basic_auth=None, proxy_basic_auth=None):
|
||||
"""
|
||||
Shortcuts for generating request headers.
|
||||
|
||||
:param keep_alive:
|
||||
If ``True``, adds 'connection: keep-alive' header.
|
||||
|
||||
:param accept_encoding:
|
||||
Can be a boolean, list, or string.
|
||||
``True`` translates to 'gzip,deflate'.
|
||||
List will get joined by comma.
|
||||
String will be used as provided.
|
||||
|
||||
:param user_agent:
|
||||
String representing the user-agent you want, such as
|
||||
"python-urllib3/0.6"
|
||||
|
||||
:param basic_auth:
|
||||
Colon-separated username:password string for 'authorization: basic ...'
|
||||
auth header.
|
||||
|
||||
:param proxy_basic_auth:
|
||||
Colon-separated username:password string for 'proxy-authorization: basic ...'
|
||||
auth header.
|
||||
|
||||
Example: ::
|
||||
|
||||
>>> make_headers(keep_alive=True, user_agent="Batman/1.0")
|
||||
{'connection': 'keep-alive', 'user-agent': 'Batman/1.0'}
|
||||
>>> make_headers(accept_encoding=True)
|
||||
{'accept-encoding': 'gzip,deflate'}
|
||||
"""
|
||||
headers = {}
|
||||
if accept_encoding:
|
||||
if isinstance(accept_encoding, str):
|
||||
pass
|
||||
elif isinstance(accept_encoding, list):
|
||||
accept_encoding = ','.join(accept_encoding)
|
||||
else:
|
||||
accept_encoding = 'gzip,deflate'
|
||||
headers['accept-encoding'] = accept_encoding
|
||||
|
||||
if user_agent:
|
||||
headers['user-agent'] = user_agent
|
||||
|
||||
if keep_alive:
|
||||
headers['connection'] = 'keep-alive'
|
||||
|
||||
if basic_auth:
|
||||
headers['authorization'] = 'Basic ' + \
|
||||
b64encode(six.b(basic_auth)).decode('utf-8')
|
||||
|
||||
if proxy_basic_auth:
|
||||
headers['proxy-authorization'] = 'Basic ' + \
|
||||
b64encode(six.b(proxy_basic_auth)).decode('utf-8')
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
def is_connection_dropped(conn): # Platform-specific
|
||||
"""
|
||||
Returns True if the connection is dropped and should be closed.
|
||||
|
||||
:param conn:
|
||||
:class:`httplib.HTTPConnection` object.
|
||||
|
||||
Note: For platforms like AppEngine, this will always return ``False`` to
|
||||
let the platform handle connection recycling transparently for us.
|
||||
"""
|
||||
sock = getattr(conn, 'sock', False)
|
||||
if not sock: # Platform-specific: AppEngine
|
||||
return False
|
||||
|
||||
if not poll:
|
||||
if not select: # Platform-specific: AppEngine
|
||||
return False
|
||||
|
||||
try:
|
||||
return select([sock], [], [], 0.0)[0]
|
||||
except SocketError:
|
||||
return True
|
||||
|
||||
# This version is better on platforms that support it.
|
||||
p = poll()
|
||||
p.register(sock, POLLIN)
|
||||
for (fno, ev) in p.poll(0.0):
|
||||
if fno == sock.fileno():
|
||||
# Either data is buffered (bad), or the connection is dropped.
|
||||
return True
|
||||
|
||||
|
||||
def resolve_cert_reqs(candidate):
|
||||
"""
|
||||
Resolves the argument to a numeric constant, which can be passed to
|
||||
the wrap_socket function/method from the ssl module.
|
||||
Defaults to :data:`ssl.CERT_NONE`.
|
||||
If given a string it is assumed to be the name of the constant in the
|
||||
:mod:`ssl` module or its abbrevation.
|
||||
(So you can specify `REQUIRED` instead of `CERT_REQUIRED`.
|
||||
If it's neither `None` nor a string we assume it is already the numeric
|
||||
constant which can directly be passed to wrap_socket.
|
||||
"""
|
||||
if candidate is None:
|
||||
return CERT_NONE
|
||||
|
||||
if isinstance(candidate, str):
|
||||
res = getattr(ssl, candidate, None)
|
||||
if res is None:
|
||||
res = getattr(ssl, 'CERT_' + candidate)
|
||||
return res
|
||||
|
||||
return candidate
|
||||
|
||||
|
||||
def resolve_ssl_version(candidate):
|
||||
"""
|
||||
like resolve_cert_reqs
|
||||
"""
|
||||
if candidate is None:
|
||||
return PROTOCOL_SSLv23
|
||||
|
||||
if isinstance(candidate, str):
|
||||
res = getattr(ssl, candidate, None)
|
||||
if res is None:
|
||||
res = getattr(ssl, 'PROTOCOL_' + candidate)
|
||||
return res
|
||||
|
||||
return candidate
|
||||
|
||||
|
||||
def assert_fingerprint(cert, fingerprint):
|
||||
"""
|
||||
Checks if given fingerprint matches the supplied certificate.
|
||||
|
||||
:param cert:
|
||||
Certificate as bytes object.
|
||||
:param fingerprint:
|
||||
Fingerprint as string of hexdigits, can be interspersed by colons.
|
||||
"""
|
||||
|
||||
# Maps the length of a digest to a possible hash function producing
|
||||
# this digest.
|
||||
hashfunc_map = {
|
||||
16: md5,
|
||||
20: sha1
|
||||
}
|
||||
|
||||
fingerprint = fingerprint.replace(':', '').lower()
|
||||
|
||||
digest_length, rest = divmod(len(fingerprint), 2)
|
||||
|
||||
if rest or digest_length not in hashfunc_map:
|
||||
raise SSLError('Fingerprint is of invalid length.')
|
||||
|
||||
# We need encode() here for py32; works on py2 and p33.
|
||||
fingerprint_bytes = unhexlify(fingerprint.encode())
|
||||
|
||||
hashfunc = hashfunc_map[digest_length]
|
||||
|
||||
cert_digest = hashfunc(cert).digest()
|
||||
|
||||
if not cert_digest == fingerprint_bytes:
|
||||
raise SSLError('Fingerprints did not match. Expected "{0}", got "{1}".'
|
||||
.format(hexlify(fingerprint_bytes),
|
||||
hexlify(cert_digest)))
|
||||
|
||||
def is_fp_closed(obj):
|
||||
"""
|
||||
Checks whether a given file-like object is closed.
|
||||
|
||||
:param obj:
|
||||
The file-like object to check.
|
||||
"""
|
||||
if hasattr(obj, 'fp'):
|
||||
# Object is a container for another file-like object that gets released
|
||||
# on exhaustion (e.g. HTTPResponse)
|
||||
return obj.fp is None
|
||||
|
||||
return obj.closed
|
||||
|
||||
|
||||
if SSLContext is not None: # Python 3.2+
|
||||
def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
|
||||
ca_certs=None, server_hostname=None,
|
||||
ssl_version=None):
|
||||
"""
|
||||
All arguments except `server_hostname` have the same meaning as for
|
||||
:func:`ssl.wrap_socket`
|
||||
|
||||
:param server_hostname:
|
||||
Hostname of the expected certificate
|
||||
"""
|
||||
context = SSLContext(ssl_version)
|
||||
context.verify_mode = cert_reqs
|
||||
if ca_certs:
|
||||
try:
|
||||
context.load_verify_locations(ca_certs)
|
||||
# Py32 raises IOError
|
||||
# Py33 raises FileNotFoundError
|
||||
except Exception as e: # Reraise as SSLError
|
||||
raise SSLError(e)
|
||||
if certfile:
|
||||
# FIXME: This block needs a test.
|
||||
context.load_cert_chain(certfile, keyfile)
|
||||
if HAS_SNI: # Platform-specific: OpenSSL with enabled SNI
|
||||
return context.wrap_socket(sock, server_hostname=server_hostname)
|
||||
return context.wrap_socket(sock)
|
||||
|
||||
else: # Python 3.1 and earlier
|
||||
def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
|
||||
ca_certs=None, server_hostname=None,
|
||||
ssl_version=None):
|
||||
return wrap_socket(sock, keyfile=keyfile, certfile=certfile,
|
||||
ca_certs=ca_certs, cert_reqs=cert_reqs,
|
||||
ssl_version=ssl_version)
|
||||
@@ -0,0 +1,27 @@
|
||||
# urllib3/util/__init__.py
|
||||
# Copyright 2008-2014 Andrey Petrov and contributors (see CONTRIBUTORS.txt)
|
||||
#
|
||||
# This module is part of urllib3 and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
from .connection import is_connection_dropped
|
||||
from .request import make_headers
|
||||
from .response import is_fp_closed
|
||||
from .ssl_ import (
|
||||
SSLContext,
|
||||
HAS_SNI,
|
||||
assert_fingerprint,
|
||||
resolve_cert_reqs,
|
||||
resolve_ssl_version,
|
||||
ssl_wrap_socket,
|
||||
)
|
||||
from .timeout import (
|
||||
current_time,
|
||||
Timeout,
|
||||
)
|
||||
from .url import (
|
||||
get_host,
|
||||
parse_url,
|
||||
split_first,
|
||||
Url,
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
from socket import error as SocketError
|
||||
try:
|
||||
from select import poll, POLLIN
|
||||
except ImportError: # `poll` doesn't exist on OSX and other platforms
|
||||
poll = False
|
||||
try:
|
||||
from select import select
|
||||
except ImportError: # `select` doesn't exist on AppEngine.
|
||||
select = False
|
||||
|
||||
def is_connection_dropped(conn): # Platform-specific
|
||||
"""
|
||||
Returns True if the connection is dropped and should be closed.
|
||||
|
||||
:param conn:
|
||||
:class:`httplib.HTTPConnection` object.
|
||||
|
||||
Note: For platforms like AppEngine, this will always return ``False`` to
|
||||
let the platform handle connection recycling transparently for us.
|
||||
"""
|
||||
sock = getattr(conn, 'sock', False)
|
||||
if sock is False: # Platform-specific: AppEngine
|
||||
return False
|
||||
if sock is None: # Connection already closed (such as by httplib).
|
||||
return False
|
||||
|
||||
if not poll:
|
||||
if not select: # Platform-specific: AppEngine
|
||||
return False
|
||||
|
||||
try:
|
||||
return select([sock], [], [], 0.0)[0]
|
||||
except SocketError:
|
||||
return True
|
||||
|
||||
# This version is better on platforms that support it.
|
||||
p = poll()
|
||||
p.register(sock, POLLIN)
|
||||
for (fno, ev) in p.poll(0.0):
|
||||
if fno == sock.fileno():
|
||||
# Either data is buffered (bad), or the connection is dropped.
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
from base64 import b64encode
|
||||
|
||||
from ..packages import six
|
||||
|
||||
|
||||
ACCEPT_ENCODING = 'gzip,deflate'
|
||||
|
||||
|
||||
def make_headers(keep_alive=None, accept_encoding=None, user_agent=None,
|
||||
basic_auth=None, proxy_basic_auth=None):
|
||||
"""
|
||||
Shortcuts for generating request headers.
|
||||
|
||||
:param keep_alive:
|
||||
If ``True``, adds 'connection: keep-alive' header.
|
||||
|
||||
:param accept_encoding:
|
||||
Can be a boolean, list, or string.
|
||||
``True`` translates to 'gzip,deflate'.
|
||||
List will get joined by comma.
|
||||
String will be used as provided.
|
||||
|
||||
:param user_agent:
|
||||
String representing the user-agent you want, such as
|
||||
"python-urllib3/0.6"
|
||||
|
||||
:param basic_auth:
|
||||
Colon-separated username:password string for 'authorization: basic ...'
|
||||
auth header.
|
||||
|
||||
:param proxy_basic_auth:
|
||||
Colon-separated username:password string for 'proxy-authorization: basic ...'
|
||||
auth header.
|
||||
|
||||
Example: ::
|
||||
|
||||
>>> make_headers(keep_alive=True, user_agent="Batman/1.0")
|
||||
{'connection': 'keep-alive', 'user-agent': 'Batman/1.0'}
|
||||
>>> make_headers(accept_encoding=True)
|
||||
{'accept-encoding': 'gzip,deflate'}
|
||||
"""
|
||||
headers = {}
|
||||
if accept_encoding:
|
||||
if isinstance(accept_encoding, str):
|
||||
pass
|
||||
elif isinstance(accept_encoding, list):
|
||||
accept_encoding = ','.join(accept_encoding)
|
||||
else:
|
||||
accept_encoding = ACCEPT_ENCODING
|
||||
headers['accept-encoding'] = accept_encoding
|
||||
|
||||
if user_agent:
|
||||
headers['user-agent'] = user_agent
|
||||
|
||||
if keep_alive:
|
||||
headers['connection'] = 'keep-alive'
|
||||
|
||||
if basic_auth:
|
||||
headers['authorization'] = 'Basic ' + \
|
||||
b64encode(six.b(basic_auth)).decode('utf-8')
|
||||
|
||||
if proxy_basic_auth:
|
||||
headers['proxy-authorization'] = 'Basic ' + \
|
||||
b64encode(six.b(proxy_basic_auth)).decode('utf-8')
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
def is_fp_closed(obj):
|
||||
"""
|
||||
Checks whether a given file-like object is closed.
|
||||
|
||||
:param obj:
|
||||
The file-like object to check.
|
||||
"""
|
||||
if hasattr(obj, 'fp'):
|
||||
# Object is a container for another file-like object that gets released
|
||||
# on exhaustion (e.g. HTTPResponse)
|
||||
return obj.fp is None
|
||||
|
||||
return obj.closed
|
||||
@@ -0,0 +1,133 @@
|
||||
from binascii import hexlify, unhexlify
|
||||
from hashlib import md5, sha1
|
||||
|
||||
from ..exceptions import SSLError
|
||||
|
||||
|
||||
try: # Test for SSL features
|
||||
SSLContext = None
|
||||
HAS_SNI = False
|
||||
|
||||
import ssl
|
||||
from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23
|
||||
from ssl import SSLContext # Modern SSL?
|
||||
from ssl import HAS_SNI # Has SNI?
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def assert_fingerprint(cert, fingerprint):
|
||||
"""
|
||||
Checks if given fingerprint matches the supplied certificate.
|
||||
|
||||
:param cert:
|
||||
Certificate as bytes object.
|
||||
:param fingerprint:
|
||||
Fingerprint as string of hexdigits, can be interspersed by colons.
|
||||
"""
|
||||
|
||||
# Maps the length of a digest to a possible hash function producing
|
||||
# this digest.
|
||||
hashfunc_map = {
|
||||
16: md5,
|
||||
20: sha1
|
||||
}
|
||||
|
||||
fingerprint = fingerprint.replace(':', '').lower()
|
||||
|
||||
digest_length, rest = divmod(len(fingerprint), 2)
|
||||
|
||||
if rest or digest_length not in hashfunc_map:
|
||||
raise SSLError('Fingerprint is of invalid length.')
|
||||
|
||||
# We need encode() here for py32; works on py2 and p33.
|
||||
fingerprint_bytes = unhexlify(fingerprint.encode())
|
||||
|
||||
hashfunc = hashfunc_map[digest_length]
|
||||
|
||||
cert_digest = hashfunc(cert).digest()
|
||||
|
||||
if not cert_digest == fingerprint_bytes:
|
||||
raise SSLError('Fingerprints did not match. Expected "{0}", got "{1}".'
|
||||
.format(hexlify(fingerprint_bytes),
|
||||
hexlify(cert_digest)))
|
||||
|
||||
|
||||
def resolve_cert_reqs(candidate):
|
||||
"""
|
||||
Resolves the argument to a numeric constant, which can be passed to
|
||||
the wrap_socket function/method from the ssl module.
|
||||
Defaults to :data:`ssl.CERT_NONE`.
|
||||
If given a string it is assumed to be the name of the constant in the
|
||||
:mod:`ssl` module or its abbrevation.
|
||||
(So you can specify `REQUIRED` instead of `CERT_REQUIRED`.
|
||||
If it's neither `None` nor a string we assume it is already the numeric
|
||||
constant which can directly be passed to wrap_socket.
|
||||
"""
|
||||
if candidate is None:
|
||||
return CERT_NONE
|
||||
|
||||
if isinstance(candidate, str):
|
||||
res = getattr(ssl, candidate, None)
|
||||
if res is None:
|
||||
res = getattr(ssl, 'CERT_' + candidate)
|
||||
return res
|
||||
|
||||
return candidate
|
||||
|
||||
|
||||
def resolve_ssl_version(candidate):
|
||||
"""
|
||||
like resolve_cert_reqs
|
||||
"""
|
||||
if candidate is None:
|
||||
return PROTOCOL_SSLv23
|
||||
|
||||
if isinstance(candidate, str):
|
||||
res = getattr(ssl, candidate, None)
|
||||
if res is None:
|
||||
res = getattr(ssl, 'PROTOCOL_' + candidate)
|
||||
return res
|
||||
|
||||
return candidate
|
||||
|
||||
|
||||
if SSLContext is not None: # Python 3.2+
|
||||
def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
|
||||
ca_certs=None, server_hostname=None,
|
||||
ssl_version=None):
|
||||
"""
|
||||
All arguments except `server_hostname` have the same meaning as for
|
||||
:func:`ssl.wrap_socket`
|
||||
|
||||
:param server_hostname:
|
||||
Hostname of the expected certificate
|
||||
"""
|
||||
context = SSLContext(ssl_version)
|
||||
context.verify_mode = cert_reqs
|
||||
|
||||
# Disable TLS compression to migitate CRIME attack (issue #309)
|
||||
OP_NO_COMPRESSION = 0x20000
|
||||
context.options |= OP_NO_COMPRESSION
|
||||
|
||||
if ca_certs:
|
||||
try:
|
||||
context.load_verify_locations(ca_certs)
|
||||
# Py32 raises IOError
|
||||
# Py33 raises FileNotFoundError
|
||||
except Exception as e: # Reraise as SSLError
|
||||
raise SSLError(e)
|
||||
if certfile:
|
||||
# FIXME: This block needs a test.
|
||||
context.load_cert_chain(certfile, keyfile)
|
||||
if HAS_SNI: # Platform-specific: OpenSSL with enabled SNI
|
||||
return context.wrap_socket(sock, server_hostname=server_hostname)
|
||||
return context.wrap_socket(sock)
|
||||
|
||||
else: # Python 3.1 and earlier
|
||||
def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
|
||||
ca_certs=None, server_hostname=None,
|
||||
ssl_version=None):
|
||||
return wrap_socket(sock, keyfile=keyfile, certfile=certfile,
|
||||
ca_certs=ca_certs, cert_reqs=cert_reqs,
|
||||
ssl_version=ssl_version)
|
||||
@@ -0,0 +1,234 @@
|
||||
from socket import _GLOBAL_DEFAULT_TIMEOUT
|
||||
import time
|
||||
|
||||
from ..exceptions import TimeoutStateError
|
||||
|
||||
|
||||
def current_time():
|
||||
"""
|
||||
Retrieve the current time, this function is mocked out in unit testing.
|
||||
"""
|
||||
return time.time()
|
||||
|
||||
|
||||
_Default = object()
|
||||
# The default timeout to use for socket connections. This is the attribute used
|
||||
# by httplib to define the default timeout
|
||||
|
||||
|
||||
class Timeout(object):
|
||||
"""
|
||||
Utility object for storing timeout values.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
timeout = urllib3.util.Timeout(connect=2.0, read=7.0)
|
||||
pool = HTTPConnectionPool('www.google.com', 80, timeout=timeout)
|
||||
pool.request(...) # Etc, etc
|
||||
|
||||
:param connect:
|
||||
The maximum amount of time to wait for a connection attempt to a server
|
||||
to succeed. Omitting the parameter will default the connect timeout to
|
||||
the system default, probably `the global default timeout in socket.py
|
||||
<http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_.
|
||||
None will set an infinite timeout for connection attempts.
|
||||
|
||||
:type connect: integer, float, or None
|
||||
|
||||
:param read:
|
||||
The maximum amount of time to wait between consecutive
|
||||
read operations for a response from the server. Omitting
|
||||
the parameter will default the read timeout to the system
|
||||
default, probably `the global default timeout in socket.py
|
||||
<http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_.
|
||||
None will set an infinite timeout.
|
||||
|
||||
:type read: integer, float, or None
|
||||
|
||||
:param total:
|
||||
This combines the connect and read timeouts into one; the read timeout
|
||||
will be set to the time leftover from the connect attempt. In the
|
||||
event that both a connect timeout and a total are specified, or a read
|
||||
timeout and a total are specified, the shorter timeout will be applied.
|
||||
|
||||
Defaults to None.
|
||||
|
||||
:type total: integer, float, or None
|
||||
|
||||
.. note::
|
||||
|
||||
Many factors can affect the total amount of time for urllib3 to return
|
||||
an HTTP response. Specifically, Python's DNS resolver does not obey the
|
||||
timeout specified on the socket. Other factors that can affect total
|
||||
request time include high CPU load, high swap, the program running at a
|
||||
low priority level, or other behaviors. The observed running time for
|
||||
urllib3 to return a response may be greater than the value passed to
|
||||
`total`.
|
||||
|
||||
In addition, the read and total timeouts only measure the time between
|
||||
read operations on the socket connecting the client and the server,
|
||||
not the total amount of time for the request to return a complete
|
||||
response. For most requests, the timeout is raised because the server
|
||||
has not sent the first byte in the specified time. This is not always
|
||||
the case; if a server streams one byte every fifteen seconds, a timeout
|
||||
of 20 seconds will not ever trigger, even though the request will
|
||||
take several minutes to complete.
|
||||
|
||||
If your goal is to cut off any request after a set amount of wall clock
|
||||
time, consider having a second "watcher" thread to cut off a slow
|
||||
request.
|
||||
"""
|
||||
|
||||
#: A sentinel object representing the default timeout value
|
||||
DEFAULT_TIMEOUT = _GLOBAL_DEFAULT_TIMEOUT
|
||||
|
||||
def __init__(self, total=None, connect=_Default, read=_Default):
|
||||
self._connect = self._validate_timeout(connect, 'connect')
|
||||
self._read = self._validate_timeout(read, 'read')
|
||||
self.total = self._validate_timeout(total, 'total')
|
||||
self._start_connect = None
|
||||
|
||||
def __str__(self):
|
||||
return '%s(connect=%r, read=%r, total=%r)' % (
|
||||
type(self).__name__, self._connect, self._read, self.total)
|
||||
|
||||
|
||||
@classmethod
|
||||
def _validate_timeout(cls, value, name):
|
||||
""" Check that a timeout attribute is valid
|
||||
|
||||
:param value: The timeout value to validate
|
||||
:param name: The name of the timeout attribute to validate. This is used
|
||||
for clear error messages
|
||||
:return: the value
|
||||
:raises ValueError: if the type is not an integer or a float, or if it
|
||||
is a numeric value less than zero
|
||||
"""
|
||||
if value is _Default:
|
||||
return cls.DEFAULT_TIMEOUT
|
||||
|
||||
if value is None or value is cls.DEFAULT_TIMEOUT:
|
||||
return value
|
||||
|
||||
try:
|
||||
float(value)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError("Timeout value %s was %s, but it must be an "
|
||||
"int or float." % (name, value))
|
||||
|
||||
try:
|
||||
if value < 0:
|
||||
raise ValueError("Attempted to set %s timeout to %s, but the "
|
||||
"timeout cannot be set to a value less "
|
||||
"than 0." % (name, value))
|
||||
except TypeError: # Python 3
|
||||
raise ValueError("Timeout value %s was %s, but it must be an "
|
||||
"int or float." % (name, value))
|
||||
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def from_float(cls, timeout):
|
||||
""" Create a new Timeout from a legacy timeout value.
|
||||
|
||||
The timeout value used by httplib.py sets the same timeout on the
|
||||
connect(), and recv() socket requests. This creates a :class:`Timeout`
|
||||
object that sets the individual timeouts to the ``timeout`` value passed
|
||||
to this function.
|
||||
|
||||
:param timeout: The legacy timeout value
|
||||
:type timeout: integer, float, sentinel default object, or None
|
||||
:return: a Timeout object
|
||||
:rtype: :class:`Timeout`
|
||||
"""
|
||||
return Timeout(read=timeout, connect=timeout)
|
||||
|
||||
def clone(self):
|
||||
""" Create a copy of the timeout object
|
||||
|
||||
Timeout properties are stored per-pool but each request needs a fresh
|
||||
Timeout object to ensure each one has its own start/stop configured.
|
||||
|
||||
:return: a copy of the timeout object
|
||||
:rtype: :class:`Timeout`
|
||||
"""
|
||||
# We can't use copy.deepcopy because that will also create a new object
|
||||
# for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to
|
||||
# detect the user default.
|
||||
return Timeout(connect=self._connect, read=self._read,
|
||||
total=self.total)
|
||||
|
||||
def start_connect(self):
|
||||
""" Start the timeout clock, used during a connect() attempt
|
||||
|
||||
:raises urllib3.exceptions.TimeoutStateError: if you attempt
|
||||
to start a timer that has been started already.
|
||||
"""
|
||||
if self._start_connect is not None:
|
||||
raise TimeoutStateError("Timeout timer has already been started.")
|
||||
self._start_connect = current_time()
|
||||
return self._start_connect
|
||||
|
||||
def get_connect_duration(self):
|
||||
""" Gets the time elapsed since the call to :meth:`start_connect`.
|
||||
|
||||
:return: the elapsed time
|
||||
:rtype: float
|
||||
:raises urllib3.exceptions.TimeoutStateError: if you attempt
|
||||
to get duration for a timer that hasn't been started.
|
||||
"""
|
||||
if self._start_connect is None:
|
||||
raise TimeoutStateError("Can't get connect duration for timer "
|
||||
"that has not started.")
|
||||
return current_time() - self._start_connect
|
||||
|
||||
@property
|
||||
def connect_timeout(self):
|
||||
""" Get the value to use when setting a connection timeout.
|
||||
|
||||
This will be a positive float or integer, the value None
|
||||
(never timeout), or the default system timeout.
|
||||
|
||||
:return: the connect timeout
|
||||
:rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None
|
||||
"""
|
||||
if self.total is None:
|
||||
return self._connect
|
||||
|
||||
if self._connect is None or self._connect is self.DEFAULT_TIMEOUT:
|
||||
return self.total
|
||||
|
||||
return min(self._connect, self.total)
|
||||
|
||||
@property
|
||||
def read_timeout(self):
|
||||
""" Get the value for the read timeout.
|
||||
|
||||
This assumes some time has elapsed in the connection timeout and
|
||||
computes the read timeout appropriately.
|
||||
|
||||
If self.total is set, the read timeout is dependent on the amount of
|
||||
time taken by the connect timeout. If the connection time has not been
|
||||
established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be
|
||||
raised.
|
||||
|
||||
:return: the value to use for the read timeout
|
||||
:rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None
|
||||
:raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect`
|
||||
has not yet been called on this object.
|
||||
"""
|
||||
if (self.total is not None and
|
||||
self.total is not self.DEFAULT_TIMEOUT and
|
||||
self._read is not None and
|
||||
self._read is not self.DEFAULT_TIMEOUT):
|
||||
# in case the connect timeout has not yet been established.
|
||||
if self._start_connect is None:
|
||||
return self._read
|
||||
return max(0, min(self.total - self.get_connect_duration(),
|
||||
self._read))
|
||||
elif self.total is not None and self.total is not self.DEFAULT_TIMEOUT:
|
||||
return max(0, self.total - self.get_connect_duration())
|
||||
else:
|
||||
return self._read
|
||||
@@ -0,0 +1,162 @@
|
||||
from collections import namedtuple
|
||||
|
||||
from ..exceptions import LocationParseError
|
||||
|
||||
|
||||
class Url(namedtuple('Url', ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment'])):
|
||||
"""
|
||||
Datastructure for representing an HTTP URL. Used as a return value for
|
||||
:func:`parse_url`.
|
||||
"""
|
||||
slots = ()
|
||||
|
||||
def __new__(cls, scheme=None, auth=None, host=None, port=None, path=None, query=None, fragment=None):
|
||||
return super(Url, cls).__new__(cls, scheme, auth, host, port, path, query, fragment)
|
||||
|
||||
@property
|
||||
def hostname(self):
|
||||
"""For backwards-compatibility with urlparse. We're nice like that."""
|
||||
return self.host
|
||||
|
||||
@property
|
||||
def request_uri(self):
|
||||
"""Absolute path including the query string."""
|
||||
uri = self.path or '/'
|
||||
|
||||
if self.query is not None:
|
||||
uri += '?' + self.query
|
||||
|
||||
return uri
|
||||
|
||||
@property
|
||||
def netloc(self):
|
||||
"""Network location including host and port"""
|
||||
if self.port:
|
||||
return '%s:%d' % (self.host, self.port)
|
||||
return self.host
|
||||
|
||||
|
||||
def split_first(s, delims):
|
||||
"""
|
||||
Given a string and an iterable of delimiters, split on the first found
|
||||
delimiter. Return two split parts and the matched delimiter.
|
||||
|
||||
If not found, then the first part is the full input string.
|
||||
|
||||
Example: ::
|
||||
|
||||
>>> split_first('foo/bar?baz', '?/=')
|
||||
('foo', 'bar?baz', '/')
|
||||
>>> split_first('foo/bar?baz', '123')
|
||||
('foo/bar?baz', '', None)
|
||||
|
||||
Scales linearly with number of delims. Not ideal for large number of delims.
|
||||
"""
|
||||
min_idx = None
|
||||
min_delim = None
|
||||
for d in delims:
|
||||
idx = s.find(d)
|
||||
if idx < 0:
|
||||
continue
|
||||
|
||||
if min_idx is None or idx < min_idx:
|
||||
min_idx = idx
|
||||
min_delim = d
|
||||
|
||||
if min_idx is None or min_idx < 0:
|
||||
return s, '', None
|
||||
|
||||
return s[:min_idx], s[min_idx+1:], min_delim
|
||||
|
||||
|
||||
def parse_url(url):
|
||||
"""
|
||||
Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is
|
||||
performed to parse incomplete urls. Fields not provided will be None.
|
||||
|
||||
Partly backwards-compatible with :mod:`urlparse`.
|
||||
|
||||
Example: ::
|
||||
|
||||
>>> parse_url('http://google.com/mail/')
|
||||
Url(scheme='http', host='google.com', port=None, path='/', ...)
|
||||
>>> parse_url('google.com:80')
|
||||
Url(scheme=None, host='google.com', port=80, path=None, ...)
|
||||
>>> parse_url('/foo?bar')
|
||||
Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...)
|
||||
"""
|
||||
|
||||
# While this code has overlap with stdlib's urlparse, it is much
|
||||
# simplified for our needs and less annoying.
|
||||
# Additionally, this implementations does silly things to be optimal
|
||||
# on CPython.
|
||||
|
||||
scheme = None
|
||||
auth = None
|
||||
host = None
|
||||
port = None
|
||||
path = None
|
||||
fragment = None
|
||||
query = None
|
||||
|
||||
# Scheme
|
||||
if '://' in url:
|
||||
scheme, url = url.split('://', 1)
|
||||
|
||||
# Find the earliest Authority Terminator
|
||||
# (http://tools.ietf.org/html/rfc3986#section-3.2)
|
||||
url, path_, delim = split_first(url, ['/', '?', '#'])
|
||||
|
||||
if delim:
|
||||
# Reassemble the path
|
||||
path = delim + path_
|
||||
|
||||
# Auth
|
||||
if '@' in url:
|
||||
# Last '@' denotes end of auth part
|
||||
auth, url = url.rsplit('@', 1)
|
||||
|
||||
# IPv6
|
||||
if url and url[0] == '[':
|
||||
host, url = url.split(']', 1)
|
||||
host += ']'
|
||||
|
||||
# Port
|
||||
if ':' in url:
|
||||
_host, port = url.split(':', 1)
|
||||
|
||||
if not host:
|
||||
host = _host
|
||||
|
||||
if port:
|
||||
# If given, ports must be integers.
|
||||
if not port.isdigit():
|
||||
raise LocationParseError(url)
|
||||
port = int(port)
|
||||
else:
|
||||
# Blank ports are cool, too. (rfc3986#section-3.2.3)
|
||||
port = None
|
||||
|
||||
elif not host and url:
|
||||
host = url
|
||||
|
||||
if not path:
|
||||
return Url(scheme, auth, host, port, path, query, fragment)
|
||||
|
||||
# Fragment
|
||||
if '#' in path:
|
||||
path, fragment = path.split('#', 1)
|
||||
|
||||
# Query
|
||||
if '?' in path:
|
||||
path, query = path.split('?', 1)
|
||||
|
||||
return Url(scheme, auth, host, port, path, query, fragment)
|
||||
|
||||
|
||||
def get_host(url):
|
||||
"""
|
||||
Deprecated. Use :func:`.parse_url` instead.
|
||||
"""
|
||||
p = parse_url(url)
|
||||
return p.scheme or 'http', p.hostname, p.port
|
||||
+111
-26
@@ -12,27 +12,28 @@ import os
|
||||
from collections import Mapping
|
||||
from datetime import datetime
|
||||
|
||||
from .auth import _basic_auth_str
|
||||
from .compat import cookielib, OrderedDict, urljoin, urlparse, builtin_str
|
||||
from .cookies import (
|
||||
cookiejar_from_dict, extract_cookies_to_jar, RequestsCookieJar, merge_cookies)
|
||||
from .models import Request, PreparedRequest
|
||||
from .models import Request, PreparedRequest, DEFAULT_REDIRECT_LIMIT
|
||||
from .hooks import default_hooks, dispatch_hook
|
||||
from .utils import to_key_val_list, default_headers
|
||||
from .exceptions import TooManyRedirects, InvalidSchema
|
||||
from .utils import to_key_val_list, default_headers, to_native_string
|
||||
from .exceptions import (
|
||||
TooManyRedirects, InvalidSchema, ChunkedEncodingError, ContentDecodingError)
|
||||
from .structures import CaseInsensitiveDict
|
||||
|
||||
from .adapters import HTTPAdapter
|
||||
|
||||
from .utils import requote_uri, get_environ_proxies, get_netrc_auth
|
||||
from .utils import (
|
||||
requote_uri, get_environ_proxies, get_netrc_auth, should_bypass_proxies,
|
||||
get_auth_from_url
|
||||
)
|
||||
|
||||
from .status_codes import codes
|
||||
REDIRECT_STATI = (
|
||||
codes.moved, # 301
|
||||
codes.found, # 302
|
||||
codes.other, # 303
|
||||
codes.temporary_moved, # 307
|
||||
)
|
||||
DEFAULT_REDIRECT_LIMIT = 30
|
||||
|
||||
# formerly defined here, reexposed here for backward compatibility
|
||||
from .models import REDIRECT_STATI
|
||||
|
||||
|
||||
def merge_setting(request_setting, session_setting, dict_class=OrderedDict):
|
||||
@@ -63,6 +64,8 @@ def merge_setting(request_setting, session_setting, dict_class=OrderedDict):
|
||||
if v is None:
|
||||
del merged_setting[k]
|
||||
|
||||
merged_setting = dict((k, v) for (k, v) in merged_setting.items() if v is not None)
|
||||
|
||||
return merged_setting
|
||||
|
||||
|
||||
@@ -89,11 +92,13 @@ class SessionRedirectMixin(object):
|
||||
|
||||
i = 0
|
||||
|
||||
# ((resp.status_code is codes.see_other))
|
||||
while ('location' in resp.headers and resp.status_code in REDIRECT_STATI):
|
||||
while resp.is_redirect:
|
||||
prepared_request = req.copy()
|
||||
|
||||
resp.content # Consume socket so it can be released
|
||||
try:
|
||||
resp.content # Consume socket so it can be released
|
||||
except (ChunkedEncodingError, ContentDecodingError, RuntimeError):
|
||||
resp.raw.read(decode_content=False)
|
||||
|
||||
if i >= self.max_redirects:
|
||||
raise TooManyRedirects('Exceeded %s redirects.' % self.max_redirects)
|
||||
@@ -121,7 +126,7 @@ class SessionRedirectMixin(object):
|
||||
else:
|
||||
url = requote_uri(url)
|
||||
|
||||
prepared_request.url = url
|
||||
prepared_request.url = to_native_string(url)
|
||||
|
||||
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4
|
||||
if (resp.status_code == codes.see_other and
|
||||
@@ -153,12 +158,19 @@ class SessionRedirectMixin(object):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
extract_cookies_to_jar(prepared_request._cookies,
|
||||
prepared_request, resp.raw)
|
||||
extract_cookies_to_jar(prepared_request._cookies, prepared_request, resp.raw)
|
||||
prepared_request._cookies.update(self.cookies)
|
||||
prepared_request.prepare_cookies(prepared_request._cookies)
|
||||
|
||||
# Rebuild auth and proxy information.
|
||||
proxies = self.rebuild_proxies(prepared_request, proxies)
|
||||
self.rebuild_auth(prepared_request, resp)
|
||||
|
||||
# Override the original request.
|
||||
req = prepared_request
|
||||
|
||||
resp = self.send(
|
||||
prepared_request,
|
||||
req,
|
||||
stream=stream,
|
||||
timeout=timeout,
|
||||
verify=verify,
|
||||
@@ -172,6 +184,68 @@ class SessionRedirectMixin(object):
|
||||
i += 1
|
||||
yield resp
|
||||
|
||||
def rebuild_auth(self, prepared_request, response):
|
||||
"""
|
||||
When being redirected we may want to strip authentication from the
|
||||
request to avoid leaking credentials. This method intelligently removes
|
||||
and reapplies authentication where possible to avoid credential loss.
|
||||
"""
|
||||
headers = prepared_request.headers
|
||||
url = prepared_request.url
|
||||
|
||||
if 'Authorization' in headers:
|
||||
# If we get redirected to a new host, we should strip out any
|
||||
# authentication headers.
|
||||
original_parsed = urlparse(response.request.url)
|
||||
redirect_parsed = urlparse(url)
|
||||
|
||||
if (original_parsed.hostname != redirect_parsed.hostname):
|
||||
del headers['Authorization']
|
||||
|
||||
# .netrc might have more auth for us on our new host.
|
||||
new_auth = get_netrc_auth(url) if self.trust_env else None
|
||||
if new_auth is not None:
|
||||
prepared_request.prepare_auth(new_auth)
|
||||
|
||||
return
|
||||
|
||||
def rebuild_proxies(self, prepared_request, proxies):
|
||||
"""
|
||||
This method re-evaluates the proxy configuration by considering the
|
||||
environment variables. If we are redirected to a URL covered by
|
||||
NO_PROXY, we strip the proxy configuration. Otherwise, we set missing
|
||||
proxy keys for this URL (in case they were stripped by a previous
|
||||
redirect).
|
||||
|
||||
This method also replaces the Proxy-Authorization header where
|
||||
necessary.
|
||||
"""
|
||||
headers = prepared_request.headers
|
||||
url = prepared_request.url
|
||||
scheme = urlparse(url).scheme
|
||||
new_proxies = proxies.copy() if proxies is not None else {}
|
||||
|
||||
if self.trust_env and not should_bypass_proxies(url):
|
||||
environ_proxies = get_environ_proxies(url)
|
||||
|
||||
proxy = environ_proxies.get(scheme)
|
||||
|
||||
if proxy:
|
||||
new_proxies.setdefault(scheme, environ_proxies[scheme])
|
||||
|
||||
if 'Proxy-Authorization' in headers:
|
||||
del headers['Proxy-Authorization']
|
||||
|
||||
try:
|
||||
username, password = get_auth_from_url(new_proxies[scheme])
|
||||
except KeyError:
|
||||
username, password = None, None
|
||||
|
||||
if username and password:
|
||||
headers['Proxy-Authorization'] = _basic_auth_str(username, password)
|
||||
|
||||
return new_proxies
|
||||
|
||||
|
||||
class Session(SessionRedirectMixin):
|
||||
"""A Requests session.
|
||||
@@ -255,7 +329,7 @@ class Session(SessionRedirectMixin):
|
||||
:class:`Session`.
|
||||
|
||||
:param request: :class:`Request` instance to prepare with this
|
||||
session's settings.
|
||||
session's settings.
|
||||
"""
|
||||
cookies = request.cookies or {}
|
||||
|
||||
@@ -319,7 +393,7 @@ class Session(SessionRedirectMixin):
|
||||
:param auth: (optional) Auth tuple or callable to enable
|
||||
Basic/Digest/Custom HTTP Auth.
|
||||
:param timeout: (optional) Float describing the timeout of the
|
||||
request.
|
||||
request in seconds.
|
||||
:param allow_redirects: (optional) Boolean. Set to True by default.
|
||||
:param proxies: (optional) Dictionary mapping protocol to the URL of
|
||||
the proxy.
|
||||
@@ -466,8 +540,7 @@ class Session(SessionRedirectMixin):
|
||||
if not isinstance(request, PreparedRequest):
|
||||
raise ValueError('You can only send PreparedRequests.')
|
||||
|
||||
# Set up variables needed for resolve_redirects and dispatching of
|
||||
# hooks
|
||||
# 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')
|
||||
@@ -481,8 +554,10 @@ class Session(SessionRedirectMixin):
|
||||
|
||||
# Start time (approximately) of the request
|
||||
start = datetime.utcnow()
|
||||
|
||||
# Send the request
|
||||
r = adapter.send(request, **kwargs)
|
||||
|
||||
# Total elapsed time of the request (approximately)
|
||||
r.elapsed = datetime.utcnow() - start
|
||||
|
||||
@@ -491,15 +566,20 @@ class Session(SessionRedirectMixin):
|
||||
|
||||
# Persist cookies
|
||||
if r.history:
|
||||
|
||||
# If the hooks create history then we want those cookies too
|
||||
for resp in r.history:
|
||||
extract_cookies_to_jar(self.cookies, resp.request, resp.raw)
|
||||
|
||||
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,
|
||||
stream=stream,
|
||||
timeout=timeout,
|
||||
verify=verify,
|
||||
cert=cert,
|
||||
proxies=proxies)
|
||||
|
||||
# Resolve redirects if allowed.
|
||||
history = [resp for resp in gen] if allow_redirects else []
|
||||
@@ -510,7 +590,10 @@ class Session(SessionRedirectMixin):
|
||||
history.insert(0, r)
|
||||
# Get the last request made
|
||||
r = history.pop()
|
||||
r.history = tuple(history)
|
||||
r.history = history
|
||||
|
||||
if not stream:
|
||||
r.content
|
||||
|
||||
return r
|
||||
|
||||
@@ -533,8 +616,10 @@ class Session(SessionRedirectMixin):
|
||||
"""Registers a connection adapter to a prefix.
|
||||
|
||||
Adapters are sorted in descending order by key length."""
|
||||
|
||||
self.adapters[prefix] = adapter
|
||||
keys_to_move = [k for k in self.adapters if len(k) < len(prefix)]
|
||||
|
||||
for key in keys_to_move:
|
||||
self.adapters[key] = self.adapters.pop(key)
|
||||
|
||||
|
||||
@@ -8,30 +8,7 @@ Data structures that power Requests.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import collections
|
||||
from itertools import islice
|
||||
|
||||
|
||||
class IteratorProxy(object):
|
||||
"""docstring for IteratorProxy"""
|
||||
def __init__(self, i):
|
||||
self.i = i
|
||||
# self.i = chain.from_iterable(i)
|
||||
|
||||
def __iter__(self):
|
||||
return self.i
|
||||
|
||||
def __len__(self):
|
||||
if hasattr(self.i, '__len__'):
|
||||
return len(self.i)
|
||||
if hasattr(self.i, 'len'):
|
||||
return self.i.len
|
||||
if hasattr(self.i, 'fileno'):
|
||||
return os.fstat(self.i.fileno()).st_size
|
||||
|
||||
def read(self, n):
|
||||
return "".join(islice(self.i, None, n))
|
||||
|
||||
|
||||
class CaseInsensitiveDict(collections.MutableMapping):
|
||||
@@ -106,8 +83,7 @@ class CaseInsensitiveDict(collections.MutableMapping):
|
||||
return CaseInsensitiveDict(self._store.values())
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r)' % (self.__class__.__name__, dict(self.items()))
|
||||
|
||||
return str(dict(self.items()))
|
||||
|
||||
class LookupDict(dict):
|
||||
"""Dictionary lookup object."""
|
||||
|
||||
+61
-31
@@ -24,10 +24,10 @@ 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)
|
||||
builtin_str, getproxies, proxy_bypass, urlunparse)
|
||||
from .cookies import RequestsCookieJar, cookiejar_from_dict
|
||||
from .structures import CaseInsensitiveDict
|
||||
from .exceptions import MissingSchema, InvalidURL
|
||||
from .exceptions import InvalidURL
|
||||
|
||||
_hush_pyflakes = (RequestsCookieJar,)
|
||||
|
||||
@@ -61,25 +61,34 @@ def super_len(o):
|
||||
return os.fstat(fileno).st_size
|
||||
|
||||
if hasattr(o, 'getvalue'):
|
||||
# e.g. BytesIO, cStringIO.StringI
|
||||
# e.g. BytesIO, cStringIO.StringIO
|
||||
return len(o.getvalue())
|
||||
|
||||
|
||||
def get_netrc_auth(url):
|
||||
"""Returns the Requests tuple auth for a given url from netrc."""
|
||||
|
||||
try:
|
||||
from netrc import netrc, NetrcParseError
|
||||
|
||||
locations = (os.path.expanduser('~/{0}'.format(f)) for f in NETRC_FILES)
|
||||
netrc_path = None
|
||||
|
||||
for loc in locations:
|
||||
if os.path.exists(loc) and not netrc_path:
|
||||
for f in NETRC_FILES:
|
||||
try:
|
||||
loc = os.path.expanduser('~/{0}'.format(f))
|
||||
except KeyError:
|
||||
# os.path.expanduser can fail when $HOME is undefined and
|
||||
# getpwuid fails. See http://bugs.python.org/issue20164 &
|
||||
# https://github.com/kennethreitz/requests/issues/1846
|
||||
return
|
||||
|
||||
if os.path.exists(loc):
|
||||
netrc_path = loc
|
||||
break
|
||||
|
||||
# Abort early if there isn't one.
|
||||
if netrc_path is None:
|
||||
return netrc_path
|
||||
return
|
||||
|
||||
ri = urlparse(url)
|
||||
|
||||
@@ -457,9 +466,10 @@ def is_valid_cidr(string_network):
|
||||
return True
|
||||
|
||||
|
||||
def get_environ_proxies(url):
|
||||
"""Return a dict of environment proxies."""
|
||||
|
||||
def should_bypass_proxies(url):
|
||||
"""
|
||||
Returns whether we should bypass proxies or not.
|
||||
"""
|
||||
get_proxy = lambda k: os.environ.get(k) or os.environ.get(k.upper())
|
||||
|
||||
# First check whether no_proxy is defined. If it is, check that the URL
|
||||
@@ -477,23 +487,36 @@ def get_environ_proxies(url):
|
||||
for proxy_ip in no_proxy:
|
||||
if is_valid_cidr(proxy_ip):
|
||||
if address_in_network(ip, proxy_ip):
|
||||
return {}
|
||||
return True
|
||||
else:
|
||||
for host in no_proxy:
|
||||
if netloc.endswith(host) or netloc.split(':')[0].endswith(host):
|
||||
# The URL does match something in no_proxy, so we don't want
|
||||
# to apply the proxies on this URL.
|
||||
return {}
|
||||
return True
|
||||
|
||||
# If the system proxy settings indicate that this URL should be bypassed,
|
||||
# don't proxy.
|
||||
if proxy_bypass(netloc):
|
||||
return {}
|
||||
# The proxy_bypass function is incredibly buggy on OS X in early versions
|
||||
# of Python 2.6, so allow this call to fail. Only catch the specific
|
||||
# exceptions we've seen, though: this call failing in other ways can reveal
|
||||
# legitimate problems.
|
||||
try:
|
||||
bypass = proxy_bypass(netloc)
|
||||
except (TypeError, socket.gaierror):
|
||||
bypass = False
|
||||
|
||||
# If we get here, we either didn't have no_proxy set or we're not going
|
||||
# anywhere that no_proxy applies to, and the system settings don't require
|
||||
# bypassing the proxy for the current URL.
|
||||
return getproxies()
|
||||
if bypass:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_environ_proxies(url):
|
||||
"""Return a dict of environment proxies."""
|
||||
if should_bypass_proxies(url):
|
||||
return {}
|
||||
else:
|
||||
return getproxies()
|
||||
|
||||
|
||||
def default_user_agent(name="python-requests"):
|
||||
@@ -530,7 +553,7 @@ def default_user_agent(name="python-requests"):
|
||||
def default_headers():
|
||||
return CaseInsensitiveDict({
|
||||
'User-Agent': default_user_agent(),
|
||||
'Accept-Encoding': ', '.join(('gzip', 'deflate', 'compress')),
|
||||
'Accept-Encoding': ', '.join(('gzip', 'deflate')),
|
||||
'Accept': '*/*'
|
||||
})
|
||||
|
||||
@@ -604,24 +627,31 @@ def guess_json_utf(data):
|
||||
return None
|
||||
|
||||
|
||||
def except_on_missing_scheme(url):
|
||||
"""Given a URL, raise a MissingSchema exception if the scheme is missing.
|
||||
"""
|
||||
scheme, netloc, path, params, query, fragment = urlparse(url)
|
||||
def prepend_scheme_if_needed(url, new_scheme):
|
||||
'''Given a URL that may or may not have a scheme, prepend the given scheme.
|
||||
Does not replace a present scheme with the one provided as an argument.'''
|
||||
scheme, netloc, path, params, query, fragment = urlparse(url, new_scheme)
|
||||
|
||||
if not scheme:
|
||||
raise MissingSchema('Proxy URLs must have explicit schemes.')
|
||||
# urlparse is a finicky beast, and sometimes decides that there isn't a
|
||||
# netloc present. Assume that it's being over-cautious, and switch netloc
|
||||
# and path if urlparse decided there was no netloc.
|
||||
if not netloc:
|
||||
netloc, path = path, netloc
|
||||
|
||||
return urlunparse((scheme, netloc, path, params, query, fragment))
|
||||
|
||||
|
||||
def get_auth_from_url(url):
|
||||
"""Given a url with authentication components, extract them into a tuple of
|
||||
username,password."""
|
||||
if url:
|
||||
url = unquote(url)
|
||||
parsed = urlparse(url)
|
||||
return (parsed.username, parsed.password)
|
||||
else:
|
||||
return ('', '')
|
||||
parsed = urlparse(url)
|
||||
|
||||
try:
|
||||
auth = (unquote(parsed.username), unquote(parsed.password))
|
||||
except (AttributeError, TypeError):
|
||||
auth = ('', '')
|
||||
|
||||
return auth
|
||||
|
||||
|
||||
def to_native_string(string, encoding='ascii'):
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
* text=auto
|
||||
@@ -1,4 +0,0 @@
|
||||
*.pyc
|
||||
*.pyd
|
||||
benchtree
|
||||
build
|
||||
@@ -1,373 +0,0 @@
|
||||
// scandir C speedups
|
||||
//
|
||||
// TODO: this is a work in progress!
|
||||
//
|
||||
// There's a fair bit of PY_MAJOR_VERSION boilerplate to support both Python 2
|
||||
// and Python 3 -- the structure of this is taken from here:
|
||||
// http://docs.python.org/3.3/howto/cporting.html
|
||||
|
||||
#include <Python.h>
|
||||
#include <structseq.h>
|
||||
|
||||
#ifdef MS_WINDOWS
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
#define INITERROR return NULL
|
||||
#define FROM_LONG PyLong_FromLong
|
||||
#define FROM_STRING PyUnicode_FromStringAndSize
|
||||
#else
|
||||
#define INITERROR return
|
||||
#define FROM_LONG PyInt_FromLong
|
||||
#define FROM_STRING PyString_FromStringAndSize
|
||||
#endif
|
||||
|
||||
#ifdef MS_WINDOWS
|
||||
|
||||
static PyObject *
|
||||
win32_error_unicode(char* function, Py_UNICODE* filename)
|
||||
{
|
||||
errno = GetLastError();
|
||||
if (filename)
|
||||
return PyErr_SetFromWindowsErrWithUnicodeFilename(errno, filename);
|
||||
else
|
||||
return PyErr_SetFromWindowsErr(errno);
|
||||
}
|
||||
|
||||
/* Below, we *know* that ugo+r is 0444 */
|
||||
#if _S_IREAD != 0400
|
||||
#error Unsupported C library
|
||||
#endif
|
||||
static int
|
||||
attributes_to_mode(DWORD attr)
|
||||
{
|
||||
int m = 0;
|
||||
if (attr & FILE_ATTRIBUTE_DIRECTORY)
|
||||
m |= _S_IFDIR | 0111; /* IFEXEC for user,group,other */
|
||||
else
|
||||
m |= _S_IFREG;
|
||||
if (attr & FILE_ATTRIBUTE_READONLY)
|
||||
m |= 0444;
|
||||
else
|
||||
m |= 0666;
|
||||
if (attr & FILE_ATTRIBUTE_REPARSE_POINT)
|
||||
m |= 0120000; // S_IFLNK
|
||||
return m;
|
||||
}
|
||||
|
||||
double
|
||||
filetime_to_time(FILETIME *filetime)
|
||||
{
|
||||
const double SECONDS_BETWEEN_EPOCHS = 11644473600.0;
|
||||
|
||||
unsigned long long total = (unsigned long long)filetime->dwHighDateTime << 32 |
|
||||
(unsigned long long)filetime->dwLowDateTime;
|
||||
return (double)total / 10000000.0 - SECONDS_BETWEEN_EPOCHS;
|
||||
}
|
||||
|
||||
static PyTypeObject StatResultType;
|
||||
|
||||
static PyObject *
|
||||
find_data_to_statresult(WIN32_FIND_DATAW *data)
|
||||
{
|
||||
PY_LONG_LONG size;
|
||||
PyObject *v = PyStructSequence_New(&StatResultType);
|
||||
if (v == NULL)
|
||||
return NULL;
|
||||
|
||||
size = (PY_LONG_LONG)data->nFileSizeHigh << 32 |
|
||||
(PY_LONG_LONG)data->nFileSizeLow;
|
||||
|
||||
PyStructSequence_SET_ITEM(v, 0, FROM_LONG(attributes_to_mode(data->dwFileAttributes)));
|
||||
PyStructSequence_SET_ITEM(v, 1, FROM_LONG(0));
|
||||
PyStructSequence_SET_ITEM(v, 2, FROM_LONG(0));
|
||||
PyStructSequence_SET_ITEM(v, 3, FROM_LONG(0));
|
||||
PyStructSequence_SET_ITEM(v, 4, FROM_LONG(0));
|
||||
PyStructSequence_SET_ITEM(v, 5, FROM_LONG(0));
|
||||
PyStructSequence_SET_ITEM(v, 6, PyLong_FromLongLong((PY_LONG_LONG)size));
|
||||
PyStructSequence_SET_ITEM(v, 7, PyFloat_FromDouble(filetime_to_time(&data->ftLastAccessTime)));
|
||||
PyStructSequence_SET_ITEM(v, 8, PyFloat_FromDouble(filetime_to_time(&data->ftLastWriteTime)));
|
||||
PyStructSequence_SET_ITEM(v, 9, PyFloat_FromDouble(filetime_to_time(&data->ftCreationTime)));
|
||||
|
||||
if (PyErr_Occurred()) {
|
||||
Py_DECREF(v);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
static PyStructSequence_Field stat_result_fields[] = {
|
||||
{"st_mode", "protection bits"},
|
||||
{"st_ino", "inode"},
|
||||
{"st_dev", "device"},
|
||||
{"st_nlink", "number of hard links"},
|
||||
{"st_uid", "user ID of owner"},
|
||||
{"st_gid", "group ID of owner"},
|
||||
{"st_size", "total size, in bytes"},
|
||||
{"st_atime", "time of last access"},
|
||||
{"st_mtime", "time of last modification"},
|
||||
{"st_ctime", "time of last change"},
|
||||
{0}
|
||||
};
|
||||
|
||||
static PyStructSequence_Desc stat_result_desc = {
|
||||
"stat_result", /* name */
|
||||
NULL, /* doc */
|
||||
stat_result_fields,
|
||||
10
|
||||
};
|
||||
|
||||
static PyObject *
|
||||
scandir_helper(PyObject *self, PyObject *args)
|
||||
{
|
||||
PyObject *d, *v;
|
||||
HANDLE hFindFile;
|
||||
BOOL result;
|
||||
WIN32_FIND_DATAW wFileData;
|
||||
Py_UNICODE *wnamebuf;
|
||||
Py_ssize_t len;
|
||||
PyObject *po;
|
||||
PyObject *name_stat;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "U:scandir_helper", &po))
|
||||
return NULL;
|
||||
|
||||
/* Overallocate for \\*.*\0 */
|
||||
len = PyUnicode_GET_SIZE(po);
|
||||
wnamebuf = malloc((len + 5) * sizeof(wchar_t));
|
||||
if (!wnamebuf) {
|
||||
PyErr_NoMemory();
|
||||
return NULL;
|
||||
}
|
||||
wcscpy(wnamebuf, PyUnicode_AS_UNICODE(po));
|
||||
if (len > 0) {
|
||||
Py_UNICODE wch = wnamebuf[len-1];
|
||||
if (wch != L'/' && wch != L'\\' && wch != L':')
|
||||
wnamebuf[len++] = L'\\';
|
||||
wcscpy(wnamebuf + len, L"*.*");
|
||||
}
|
||||
if ((d = PyList_New(0)) == NULL) {
|
||||
free(wnamebuf);
|
||||
return NULL;
|
||||
}
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
hFindFile = FindFirstFileW(wnamebuf, &wFileData);
|
||||
Py_END_ALLOW_THREADS
|
||||
if (hFindFile == INVALID_HANDLE_VALUE) {
|
||||
int error = GetLastError();
|
||||
if (error == ERROR_FILE_NOT_FOUND) {
|
||||
free(wnamebuf);
|
||||
return d;
|
||||
}
|
||||
Py_DECREF(d);
|
||||
win32_error_unicode("FindFirstFileW", wnamebuf);
|
||||
free(wnamebuf);
|
||||
return NULL;
|
||||
}
|
||||
do {
|
||||
/* Skip over . and .. */
|
||||
if (wcscmp(wFileData.cFileName, L".") != 0 &&
|
||||
wcscmp(wFileData.cFileName, L"..") != 0) {
|
||||
v = PyUnicode_FromUnicode(wFileData.cFileName, wcslen(wFileData.cFileName));
|
||||
if (v == NULL) {
|
||||
Py_DECREF(d);
|
||||
d = NULL;
|
||||
break;
|
||||
}
|
||||
name_stat = Py_BuildValue("ON", v, find_data_to_statresult(&wFileData));
|
||||
if (name_stat == NULL) {
|
||||
Py_DECREF(v);
|
||||
Py_DECREF(d);
|
||||
d = NULL;
|
||||
break;
|
||||
}
|
||||
if (PyList_Append(d, name_stat) != 0) {
|
||||
Py_DECREF(v);
|
||||
Py_DECREF(d);
|
||||
Py_DECREF(name_stat);
|
||||
d = NULL;
|
||||
break;
|
||||
}
|
||||
Py_DECREF(name_stat);
|
||||
Py_DECREF(v);
|
||||
}
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
result = FindNextFileW(hFindFile, &wFileData);
|
||||
Py_END_ALLOW_THREADS
|
||||
/* FindNextFile sets error to ERROR_NO_MORE_FILES if
|
||||
it got to the end of the directory. */
|
||||
if (!result && GetLastError() != ERROR_NO_MORE_FILES) {
|
||||
Py_DECREF(d);
|
||||
win32_error_unicode("FindNextFileW", wnamebuf);
|
||||
FindClose(hFindFile);
|
||||
free(wnamebuf);
|
||||
return NULL;
|
||||
}
|
||||
} while (result == TRUE);
|
||||
|
||||
if (FindClose(hFindFile) == FALSE) {
|
||||
Py_DECREF(d);
|
||||
win32_error_unicode("FindClose", wnamebuf);
|
||||
free(wnamebuf);
|
||||
return NULL;
|
||||
}
|
||||
free(wnamebuf);
|
||||
return d;
|
||||
}
|
||||
|
||||
#else // Linux / OS X
|
||||
|
||||
#include <dirent.h>
|
||||
#define NAMLEN(dirent) strlen((dirent)->d_name)
|
||||
|
||||
static PyObject *
|
||||
posix_error_with_allocated_filename(char* name)
|
||||
{
|
||||
PyObject *rc = PyErr_SetFromErrnoWithFilename(PyExc_OSError, name);
|
||||
PyMem_Free(name);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
scandir_helper(PyObject *self, PyObject *args)
|
||||
{
|
||||
char *name = NULL;
|
||||
PyObject *d, *v, *name_type;
|
||||
DIR *dirp;
|
||||
struct dirent *ep;
|
||||
int arg_is_unicode = 1;
|
||||
|
||||
errno = 0;
|
||||
if (!PyArg_ParseTuple(args, "U:scandir_helper", &v)) {
|
||||
arg_is_unicode = 0;
|
||||
PyErr_Clear();
|
||||
}
|
||||
if (!PyArg_ParseTuple(args, "et:scandir_helper", Py_FileSystemDefaultEncoding, &name))
|
||||
return NULL;
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
dirp = opendir(name);
|
||||
Py_END_ALLOW_THREADS
|
||||
if (dirp == NULL) {
|
||||
return posix_error_with_allocated_filename(name);
|
||||
}
|
||||
if ((d = PyList_New(0)) == NULL) {
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
closedir(dirp);
|
||||
Py_END_ALLOW_THREADS
|
||||
PyMem_Free(name);
|
||||
return NULL;
|
||||
}
|
||||
for (;;) {
|
||||
errno = 0;
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
ep = readdir(dirp);
|
||||
Py_END_ALLOW_THREADS
|
||||
if (ep == NULL) {
|
||||
if (errno == 0) {
|
||||
break;
|
||||
} else {
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
closedir(dirp);
|
||||
Py_END_ALLOW_THREADS
|
||||
Py_DECREF(d);
|
||||
return posix_error_with_allocated_filename(name);
|
||||
}
|
||||
}
|
||||
if (ep->d_name[0] == '.' &&
|
||||
(NAMLEN(ep) == 1 ||
|
||||
(ep->d_name[1] == '.' && NAMLEN(ep) == 2)))
|
||||
continue;
|
||||
v = FROM_STRING(ep->d_name, NAMLEN(ep));
|
||||
if (v == NULL) {
|
||||
Py_DECREF(d);
|
||||
d = NULL;
|
||||
break;
|
||||
}
|
||||
if (arg_is_unicode) {
|
||||
PyObject *w;
|
||||
|
||||
w = PyUnicode_FromEncodedObject(v,
|
||||
Py_FileSystemDefaultEncoding,
|
||||
"strict");
|
||||
if (w != NULL) {
|
||||
Py_DECREF(v);
|
||||
v = w;
|
||||
}
|
||||
else {
|
||||
/* fall back to the original byte string, as
|
||||
discussed in patch #683592 */
|
||||
PyErr_Clear();
|
||||
}
|
||||
}
|
||||
name_type = Py_BuildValue("ON", v, FROM_LONG(ep->d_type));
|
||||
if (name_type == NULL) {
|
||||
Py_DECREF(v);
|
||||
Py_DECREF(d);
|
||||
d = NULL;
|
||||
break;
|
||||
}
|
||||
if (PyList_Append(d, name_type) != 0) {
|
||||
Py_DECREF(v);
|
||||
Py_DECREF(d);
|
||||
Py_DECREF(name_type);
|
||||
d = NULL;
|
||||
break;
|
||||
}
|
||||
Py_DECREF(name_type);
|
||||
Py_DECREF(v);
|
||||
}
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
closedir(dirp);
|
||||
Py_END_ALLOW_THREADS
|
||||
PyMem_Free(name);
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
static PyMethodDef scandir_methods[] = {
|
||||
{"scandir_helper", (PyCFunction)scandir_helper, METH_VARARGS, NULL},
|
||||
{NULL, NULL},
|
||||
};
|
||||
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
static struct PyModuleDef moduledef = {
|
||||
PyModuleDef_HEAD_INIT,
|
||||
"_scandir",
|
||||
NULL,
|
||||
0,
|
||||
scandir_methods,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
};
|
||||
#endif
|
||||
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
PyObject *
|
||||
PyInit__scandir(void)
|
||||
{
|
||||
PyObject *module = PyModule_Create(&moduledef);
|
||||
#else
|
||||
void
|
||||
init_scandir(void)
|
||||
{
|
||||
PyObject *module = Py_InitModule("_scandir", scandir_methods);
|
||||
#endif
|
||||
if (module == NULL) {
|
||||
INITERROR;
|
||||
}
|
||||
|
||||
#ifdef MS_WINDOWS
|
||||
stat_result_desc.name = "scandir.stat_result";
|
||||
PyStructSequence_InitType(&StatResultType, &stat_result_desc);
|
||||
#endif
|
||||
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
return module;
|
||||
#endif
|
||||
}
|
||||
@@ -1,456 +0,0 @@
|
||||
"""scandir, a better directory iterator that exposes all file info OS provides
|
||||
|
||||
scandir is a generator version of os.listdir() that returns an iterator over
|
||||
files in a directory, and also exposes the extra information most OSes provide
|
||||
while iterating files in a directory.
|
||||
|
||||
See README.md or https://github.com/benhoyt/scandir for rationale and docs.
|
||||
|
||||
scandir is released under the new BSD 3-clause license. See LICENSE.txt for
|
||||
the full license text.
|
||||
"""
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
|
||||
__version__ = '0.3'
|
||||
__all__ = ['scandir', 'walk']
|
||||
|
||||
# Shortcuts to these functions for speed and ease
|
||||
join = os.path.join
|
||||
lstat = os.lstat
|
||||
|
||||
S_IFDIR = stat.S_IFDIR
|
||||
S_IFREG = stat.S_IFREG
|
||||
S_IFLNK = stat.S_IFLNK
|
||||
|
||||
# 'unicode' isn't defined on Python 3
|
||||
try:
|
||||
unicode
|
||||
except NameError:
|
||||
unicode = str
|
||||
|
||||
_scandir = None
|
||||
|
||||
|
||||
class GenericDirEntry(object):
|
||||
__slots__ = ('name', '_lstat', '_path')
|
||||
|
||||
def __init__(self, path, name):
|
||||
self._path = path
|
||||
self.name = name
|
||||
self._lstat = None
|
||||
|
||||
def lstat(self):
|
||||
if self._lstat is None:
|
||||
self._lstat = lstat(join(self._path, self.name))
|
||||
return self._lstat
|
||||
|
||||
def is_dir(self):
|
||||
try:
|
||||
self.lstat()
|
||||
except OSError:
|
||||
return False
|
||||
return self._lstat.st_mode & 0o170000 == S_IFDIR
|
||||
|
||||
def is_file(self):
|
||||
try:
|
||||
self.lstat()
|
||||
except OSError:
|
||||
return False
|
||||
return self._lstat.st_mode & 0o170000 == S_IFREG
|
||||
|
||||
def is_symlink(self):
|
||||
try:
|
||||
self.lstat()
|
||||
except OSError:
|
||||
return False
|
||||
return self._lstat.st_mode & 0o170000 == S_IFLNK
|
||||
|
||||
def __str__(self):
|
||||
return '<{0}: {1!r}>'.format(self.__class__.__name__, self.name)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
if sys.platform == 'win32':
|
||||
from ctypes import wintypes
|
||||
|
||||
# Various constants from windows.h
|
||||
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
|
||||
ERROR_FILE_NOT_FOUND = 2
|
||||
ERROR_NO_MORE_FILES = 18
|
||||
FILE_ATTRIBUTE_READONLY = 1
|
||||
FILE_ATTRIBUTE_DIRECTORY = 16
|
||||
FILE_ATTRIBUTE_REPARSE_POINT = 1024
|
||||
|
||||
# Numer of seconds between 1601-01-01 and 1970-01-01
|
||||
SECONDS_BETWEEN_EPOCHS = 11644473600
|
||||
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
|
||||
# ctypes wrappers for (wide string versions of) FindFirstFile,
|
||||
# FindNextFile, and FindClose
|
||||
FindFirstFile = kernel32.FindFirstFileW
|
||||
FindFirstFile.argtypes = [
|
||||
wintypes.LPCWSTR,
|
||||
ctypes.POINTER(wintypes.WIN32_FIND_DATAW),
|
||||
]
|
||||
FindFirstFile.restype = wintypes.HANDLE
|
||||
|
||||
FindNextFile = kernel32.FindNextFileW
|
||||
FindNextFile.argtypes = [
|
||||
wintypes.HANDLE,
|
||||
ctypes.POINTER(wintypes.WIN32_FIND_DATAW),
|
||||
]
|
||||
FindNextFile.restype = wintypes.BOOL
|
||||
|
||||
FindClose = kernel32.FindClose
|
||||
FindClose.argtypes = [wintypes.HANDLE]
|
||||
FindClose.restype = wintypes.BOOL
|
||||
|
||||
def filetime_to_time(filetime):
|
||||
"""Convert Win32 FILETIME to time since Unix epoch in seconds."""
|
||||
total = filetime.dwHighDateTime << 32 | filetime.dwLowDateTime
|
||||
return total / 10000000 - SECONDS_BETWEEN_EPOCHS
|
||||
|
||||
def find_data_to_stat(data):
|
||||
"""Convert Win32 FIND_DATA struct to stat_result."""
|
||||
# First convert Win32 dwFileAttributes to st_mode
|
||||
attributes = data.dwFileAttributes
|
||||
st_mode = 0
|
||||
if attributes & FILE_ATTRIBUTE_DIRECTORY:
|
||||
st_mode |= S_IFDIR | 0o111
|
||||
else:
|
||||
st_mode |= S_IFREG
|
||||
if attributes & FILE_ATTRIBUTE_READONLY:
|
||||
st_mode |= 0o444
|
||||
else:
|
||||
st_mode |= 0o666
|
||||
if attributes & FILE_ATTRIBUTE_REPARSE_POINT:
|
||||
st_mode |= S_IFLNK
|
||||
|
||||
st_size = data.nFileSizeHigh << 32 | data.nFileSizeLow
|
||||
st_atime = filetime_to_time(data.ftLastAccessTime)
|
||||
st_mtime = filetime_to_time(data.ftLastWriteTime)
|
||||
st_ctime = filetime_to_time(data.ftCreationTime)
|
||||
|
||||
# Some fields set to zero per CPython's posixmodule.c: st_ino, st_dev,
|
||||
# st_nlink, st_uid, st_gid
|
||||
return os.stat_result((st_mode, 0, 0, 0, 0, 0, st_size, st_atime,
|
||||
st_mtime, st_ctime))
|
||||
|
||||
class Win32DirEntry(object):
|
||||
__slots__ = ('name', '_lstat', '_find_data')
|
||||
|
||||
def __init__(self, name, find_data):
|
||||
self.name = name
|
||||
self._lstat = None
|
||||
self._find_data = find_data
|
||||
|
||||
def lstat(self):
|
||||
if self._lstat is None:
|
||||
# Lazily convert to stat object, because it's slow, and often
|
||||
# we only need is_dir() etc
|
||||
self._lstat = find_data_to_stat(self._find_data)
|
||||
return self._lstat
|
||||
|
||||
def is_dir(self):
|
||||
return (self._find_data.dwFileAttributes &
|
||||
FILE_ATTRIBUTE_DIRECTORY != 0)
|
||||
|
||||
def is_file(self):
|
||||
return (self._find_data.dwFileAttributes &
|
||||
FILE_ATTRIBUTE_DIRECTORY == 0)
|
||||
|
||||
def is_symlink(self):
|
||||
return (self._find_data.dwFileAttributes &
|
||||
FILE_ATTRIBUTE_REPARSE_POINT != 0)
|
||||
|
||||
def __str__(self):
|
||||
return '<{0}: {1!r}>'.format(self.__class__.__name__, self.name)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def win_error(error, filename):
|
||||
exc = WindowsError(error, ctypes.FormatError(error))
|
||||
exc.filename = filename
|
||||
return exc
|
||||
|
||||
def scandir(path='.', windows_wildcard='*.*'):
|
||||
"""Like os.listdir(), but yield DirEntry objects instead of returning
|
||||
a list of names.
|
||||
"""
|
||||
# Call FindFirstFile and handle errors
|
||||
data = wintypes.WIN32_FIND_DATAW()
|
||||
data_p = ctypes.byref(data)
|
||||
filename = join(path, windows_wildcard)
|
||||
handle = FindFirstFile(filename, data_p)
|
||||
if handle == INVALID_HANDLE_VALUE:
|
||||
error = ctypes.GetLastError()
|
||||
if error == ERROR_FILE_NOT_FOUND:
|
||||
# No files, don't yield anything
|
||||
return
|
||||
raise win_error(error, path)
|
||||
|
||||
# Call FindNextFile in a loop, stopping when no more files
|
||||
try:
|
||||
while True:
|
||||
# Skip '.' and '..' (current and parent directory), but
|
||||
# otherwise yield (filename, stat_result) tuple
|
||||
name = data.cFileName
|
||||
if name not in ('.', '..'):
|
||||
yield Win32DirEntry(name, data)
|
||||
|
||||
data = wintypes.WIN32_FIND_DATAW()
|
||||
data_p = ctypes.byref(data)
|
||||
success = FindNextFile(handle, data_p)
|
||||
if not success:
|
||||
error = ctypes.GetLastError()
|
||||
if error == ERROR_NO_MORE_FILES:
|
||||
break
|
||||
raise win_error(error, path)
|
||||
finally:
|
||||
if not FindClose(handle):
|
||||
raise win_error(ctypes.GetLastError(), path)
|
||||
|
||||
try:
|
||||
import _scandir
|
||||
|
||||
scandir_helper = _scandir.scandir_helper
|
||||
|
||||
class Win32DirEntry(object):
|
||||
__slots__ = ('name', '_lstat')
|
||||
|
||||
def __init__(self, name, lstat):
|
||||
self.name = name
|
||||
self._lstat = lstat
|
||||
|
||||
def lstat(self):
|
||||
return self._lstat
|
||||
|
||||
def is_dir(self):
|
||||
return self._lstat.st_mode & 0o170000 == S_IFDIR
|
||||
|
||||
def is_file(self):
|
||||
return self._lstat.st_mode & 0o170000 == S_IFREG
|
||||
|
||||
def is_symlink(self):
|
||||
return self._lstat.st_mode & 0o170000 == S_IFLNK
|
||||
|
||||
def __str__(self):
|
||||
return '<{0}: {1!r}>'.format(self.__class__.__name__, self.name)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def scandir(path='.'):
|
||||
for name, stat in scandir_helper(unicode(path)):
|
||||
yield Win32DirEntry(name, stat)
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# Linux, OS X, and BSD implementation
|
||||
elif sys.platform.startswith(('linux', 'darwin')) or 'bsd' in sys.platform:
|
||||
import ctypes.util
|
||||
|
||||
DIR_p = ctypes.c_void_p
|
||||
|
||||
# Rather annoying how the dirent struct is slightly different on each
|
||||
# platform. The only fields we care about are d_name and d_type.
|
||||
class Dirent(ctypes.Structure):
|
||||
if sys.platform.startswith('linux'):
|
||||
_fields_ = (
|
||||
('d_ino', ctypes.c_ulong),
|
||||
('d_off', ctypes.c_long),
|
||||
('d_reclen', ctypes.c_ushort),
|
||||
('d_type', ctypes.c_byte),
|
||||
('d_name', ctypes.c_char * 256),
|
||||
)
|
||||
else:
|
||||
_fields_ = (
|
||||
('d_ino', ctypes.c_uint32), # must be uint32, not ulong
|
||||
('d_reclen', ctypes.c_ushort),
|
||||
('d_type', ctypes.c_byte),
|
||||
('d_namlen', ctypes.c_byte),
|
||||
('d_name', ctypes.c_char * 256),
|
||||
)
|
||||
|
||||
DT_UNKNOWN = 0
|
||||
DT_DIR = 4
|
||||
DT_REG = 8
|
||||
DT_LNK = 10
|
||||
|
||||
Dirent_p = ctypes.POINTER(Dirent)
|
||||
Dirent_pp = ctypes.POINTER(Dirent_p)
|
||||
|
||||
libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)
|
||||
opendir = libc.opendir
|
||||
opendir.argtypes = [ctypes.c_char_p]
|
||||
opendir.restype = DIR_p
|
||||
|
||||
readdir_r = libc.readdir_r
|
||||
readdir_r.argtypes = [DIR_p, Dirent_p, Dirent_pp]
|
||||
readdir_r.restype = ctypes.c_int
|
||||
|
||||
closedir = libc.closedir
|
||||
closedir.argtypes = [DIR_p]
|
||||
closedir.restype = ctypes.c_int
|
||||
|
||||
file_system_encoding = sys.getfilesystemencoding()
|
||||
|
||||
class PosixDirEntry(object):
|
||||
__slots__ = ('name', '_d_type', '_lstat', '_path')
|
||||
|
||||
def __init__(self, path, name, d_type):
|
||||
self._path = path
|
||||
self.name = name
|
||||
self._d_type = d_type
|
||||
self._lstat = None
|
||||
|
||||
def lstat(self):
|
||||
if self._lstat is None:
|
||||
self._lstat = lstat(join(self._path, self.name))
|
||||
return self._lstat
|
||||
|
||||
# Ridiculous duplication between these is* functions -- helps a little
|
||||
# bit with os.walk() performance compared to calling another function.
|
||||
def is_dir(self):
|
||||
d_type = self._d_type
|
||||
if d_type != DT_UNKNOWN:
|
||||
return d_type == DT_DIR
|
||||
try:
|
||||
self.lstat()
|
||||
except OSError:
|
||||
return False
|
||||
return self._lstat.st_mode & 0o170000 == S_IFDIR
|
||||
|
||||
def is_file(self):
|
||||
d_type = self._d_type
|
||||
if d_type != DT_UNKNOWN:
|
||||
return d_type == DT_REG
|
||||
try:
|
||||
self.lstat()
|
||||
except OSError:
|
||||
return False
|
||||
return self._lstat.st_mode & 0o170000 == S_IFREG
|
||||
|
||||
def is_symlink(self):
|
||||
d_type = self._d_type
|
||||
if d_type != DT_UNKNOWN:
|
||||
return d_type == DT_LNK
|
||||
try:
|
||||
self.lstat()
|
||||
except OSError:
|
||||
return False
|
||||
return self._lstat.st_mode & 0o170000 == S_IFLNK
|
||||
|
||||
def __str__(self):
|
||||
return '<{0}: {1!r}>'.format(self.__class__.__name__, self.name)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def posix_error(filename):
|
||||
errno = ctypes.get_errno()
|
||||
exc = OSError(errno, os.strerror(errno))
|
||||
exc.filename = filename
|
||||
return exc
|
||||
|
||||
def scandir(path='.'):
|
||||
"""Like os.listdir(), but yield DirEntry objects instead of returning
|
||||
a list of names.
|
||||
"""
|
||||
dir_p = opendir(path.encode(file_system_encoding))
|
||||
if not dir_p:
|
||||
raise posix_error(path)
|
||||
try:
|
||||
result = Dirent_p()
|
||||
while True:
|
||||
entry = Dirent()
|
||||
if readdir_r(dir_p, entry, result):
|
||||
raise posix_error(path)
|
||||
if not result:
|
||||
break
|
||||
name = entry.d_name.decode(file_system_encoding)
|
||||
if name not in ('.', '..'):
|
||||
yield PosixDirEntry(path, name, entry.d_type)
|
||||
finally:
|
||||
if closedir(dir_p):
|
||||
raise posix_error(path)
|
||||
|
||||
try:
|
||||
import _scandir
|
||||
|
||||
scandir_helper = _scandir.scandir_helper
|
||||
|
||||
def scandir(path='.'):
|
||||
for name, d_type in scandir_helper(unicode(path)):
|
||||
yield PosixDirEntry(path, name, d_type)
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# Some other system -- no d_type or stat information
|
||||
else:
|
||||
def scandir(path='.'):
|
||||
"""Like os.listdir(), but yield DirEntry objects instead of returning
|
||||
a list of names.
|
||||
"""
|
||||
for name in os.listdir(path):
|
||||
yield GenericDirEntry(path, name)
|
||||
|
||||
|
||||
def walk(top, topdown=True, onerror=None, followlinks=False):
|
||||
"""Like os.walk(), but faster, as it uses scandir() internally."""
|
||||
# Determine which are files and which are directories
|
||||
dirs = []
|
||||
nondirs = []
|
||||
try:
|
||||
for entry in scandir(top):
|
||||
if entry.is_dir():
|
||||
dirs.append(entry)
|
||||
else:
|
||||
nondirs.append(entry)
|
||||
except OSError as error:
|
||||
if onerror is not None:
|
||||
onerror(error)
|
||||
return
|
||||
|
||||
# Yield before recursion if going top down
|
||||
if topdown:
|
||||
# Need to do some fancy footwork here as caller is allowed to modify
|
||||
# dir_names, and we really want them to modify dirs (list of DirEntry
|
||||
# objects) instead. Keep a mapping of entries keyed by name.
|
||||
dir_names = []
|
||||
entries_by_name = {}
|
||||
for entry in dirs:
|
||||
dir_names.append(entry.name)
|
||||
entries_by_name[entry.name] = entry
|
||||
|
||||
yield top, dir_names, [e.name for e in nondirs]
|
||||
|
||||
dirs = []
|
||||
for dir_name in dir_names:
|
||||
entry = entries_by_name.get(dir_name)
|
||||
if entry is None:
|
||||
# Only happens when caller creates a new directory and adds it
|
||||
# to dir_names
|
||||
entry = GenericDirEntry(top, dir_name)
|
||||
dirs.append(entry)
|
||||
|
||||
# Recurse into sub-directories, following symbolic links if "followlinks"
|
||||
for entry in dirs:
|
||||
if followlinks or not entry.is_symlink():
|
||||
new_path = join(top, entry.name)
|
||||
for x in walk(new_path, topdown, onerror, followlinks):
|
||||
yield x
|
||||
|
||||
# Yield before recursion if going bottom up
|
||||
if not topdown:
|
||||
yield top, [e.name for e in dirs], [e.name for e in nondirs]
|
||||
@@ -25,5 +25,5 @@ from __future__ import absolute_import, division, print_function, with_statement
|
||||
# is zero for an official release, positive for a development branch,
|
||||
# or negative for a release candidate or beta (after the base version
|
||||
# number has been incremented)
|
||||
version = "3.2"
|
||||
version_info = (3, 2, 0, 0)
|
||||
version = "3.3.dev1"
|
||||
version_info = (3, 3, 0, -100)
|
||||
|
||||
+69
-25
@@ -34,15 +34,29 @@ See the individual service classes below for complete documentation.
|
||||
|
||||
Example usage for Google OpenID::
|
||||
|
||||
class GoogleLoginHandler(tornado.web.RequestHandler,
|
||||
tornado.auth.GoogleMixin):
|
||||
class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
|
||||
tornado.auth.GoogleOAuth2Mixin):
|
||||
@tornado.gen.coroutine
|
||||
def get(self):
|
||||
if self.get_argument("openid.mode", None):
|
||||
user = yield self.get_authenticated_user()
|
||||
# Save the user with e.g. set_secure_cookie()
|
||||
if self.get_argument('code', False):
|
||||
user = yield self.get_authenticated_user(
|
||||
redirect_uri='http://your.site.com/auth/google',
|
||||
code=self.get_argument('code'))
|
||||
# Save the user with e.g. set_secure_cookie
|
||||
else:
|
||||
yield self.authenticate_redirect()
|
||||
yield self.authorize_redirect(
|
||||
redirect_uri='http://your.site.com/auth/google',
|
||||
client_id=self.settings['google_oauth']['key'],
|
||||
scope=['profile', 'email'],
|
||||
response_type='code',
|
||||
extra_params={'approval_prompt': 'auto'})
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
All of the callback interfaces in this module are now guaranteed
|
||||
to run their callback with an argument of ``None`` on error.
|
||||
Previously some functions would do this while others would simply
|
||||
terminate the request on their own. This change also ensures that
|
||||
errors are more consistently reported through the ``Future`` interfaces.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function, with_statement
|
||||
@@ -61,6 +75,7 @@ from tornado import httpclient
|
||||
from tornado import escape
|
||||
from tornado.httputil import url_concat
|
||||
from tornado.log import gen_log
|
||||
from tornado.stack_context import ExceptionStackContext
|
||||
from tornado.util import bytes_type, u, unicode_type, ArgReplacer
|
||||
|
||||
try:
|
||||
@@ -73,6 +88,11 @@ try:
|
||||
except ImportError:
|
||||
import urllib as urllib_parse # py2
|
||||
|
||||
try:
|
||||
long # py2
|
||||
except NameError:
|
||||
long = int # py3
|
||||
|
||||
|
||||
class AuthError(Exception):
|
||||
pass
|
||||
@@ -103,7 +123,14 @@ def _auth_return_future(f):
|
||||
if callback is not None:
|
||||
future.add_done_callback(
|
||||
functools.partial(_auth_future_to_callback, callback))
|
||||
f(*args, **kwargs)
|
||||
def handle_exception(typ, value, tb):
|
||||
if future.done():
|
||||
return False
|
||||
else:
|
||||
future.set_exc_info((typ, value, tb))
|
||||
return True
|
||||
with ExceptionStackContext(handle_exception):
|
||||
f(*args, **kwargs)
|
||||
return future
|
||||
return wrapper
|
||||
|
||||
@@ -161,7 +188,7 @@ class OpenIdMixin(object):
|
||||
url = self._OPENID_ENDPOINT
|
||||
if http_client is None:
|
||||
http_client = self.get_auth_http_client()
|
||||
http_client.fetch(url, self.async_callback(
|
||||
http_client.fetch(url, functools.partial(
|
||||
self._on_authentication_verified, callback),
|
||||
method="POST", body=urllib_parse.urlencode(args))
|
||||
|
||||
@@ -333,7 +360,7 @@ class OAuthMixin(object):
|
||||
http_client.fetch(
|
||||
self._oauth_request_token_url(callback_uri=callback_uri,
|
||||
extra_params=extra_params),
|
||||
self.async_callback(
|
||||
functools.partial(
|
||||
self._on_request_token,
|
||||
self._OAUTH_AUTHORIZE_URL,
|
||||
callback_uri,
|
||||
@@ -341,7 +368,7 @@ class OAuthMixin(object):
|
||||
else:
|
||||
http_client.fetch(
|
||||
self._oauth_request_token_url(),
|
||||
self.async_callback(
|
||||
functools.partial(
|
||||
self._on_request_token, self._OAUTH_AUTHORIZE_URL,
|
||||
callback_uri,
|
||||
callback))
|
||||
@@ -378,7 +405,7 @@ class OAuthMixin(object):
|
||||
if http_client is None:
|
||||
http_client = self.get_auth_http_client()
|
||||
http_client.fetch(self._oauth_access_token_url(token),
|
||||
self.async_callback(self._on_access_token, callback))
|
||||
functools.partial(self._on_access_token, callback))
|
||||
|
||||
def _oauth_request_token_url(self, callback_uri=None, extra_params=None):
|
||||
consumer_token = self._oauth_consumer_token()
|
||||
@@ -455,7 +482,7 @@ class OAuthMixin(object):
|
||||
|
||||
access_token = _oauth_parse_response(response.body)
|
||||
self._oauth_get_user_future(access_token).add_done_callback(
|
||||
self.async_callback(self._on_oauth_get_user, access_token, future))
|
||||
functools.partial(self._on_oauth_get_user, access_token, future))
|
||||
|
||||
def _oauth_consumer_token(self):
|
||||
"""Subclasses must override this to return their OAuth consumer keys.
|
||||
@@ -640,7 +667,7 @@ class TwitterMixin(OAuthMixin):
|
||||
"""
|
||||
http = self.get_auth_http_client()
|
||||
http.fetch(self._oauth_request_token_url(callback_uri=callback_uri),
|
||||
self.async_callback(
|
||||
functools.partial(
|
||||
self._on_request_token, self._OAUTH_AUTHENTICATE_URL,
|
||||
None, callback))
|
||||
|
||||
@@ -698,7 +725,7 @@ class TwitterMixin(OAuthMixin):
|
||||
if args:
|
||||
url += "?" + urllib_parse.urlencode(args)
|
||||
http = self.get_auth_http_client()
|
||||
http_callback = self.async_callback(self._on_twitter_request, callback)
|
||||
http_callback = functools.partial(self._on_twitter_request, callback)
|
||||
if post_args is not None:
|
||||
http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args),
|
||||
callback=http_callback)
|
||||
@@ -815,7 +842,7 @@ class FriendFeedMixin(OAuthMixin):
|
||||
args.update(oauth)
|
||||
if args:
|
||||
url += "?" + urllib_parse.urlencode(args)
|
||||
callback = self.async_callback(self._on_friendfeed_request, callback)
|
||||
callback = functools.partial(self._on_friendfeed_request, callback)
|
||||
http = self.get_auth_http_client()
|
||||
if post_args is not None:
|
||||
http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args),
|
||||
@@ -856,6 +883,10 @@ class FriendFeedMixin(OAuthMixin):
|
||||
class GoogleMixin(OpenIdMixin, OAuthMixin):
|
||||
"""Google Open ID / OAuth authentication.
|
||||
|
||||
*Deprecated:* New applications should use `GoogleOAuth2Mixin`
|
||||
below instead of this class. As of May 19, 2014, Google has stopped
|
||||
supporting registration-free authentication.
|
||||
|
||||
No application registration is necessary to use Google for
|
||||
authentication or to access Google resources on behalf of a user.
|
||||
|
||||
@@ -926,7 +957,7 @@ class GoogleMixin(OpenIdMixin, OAuthMixin):
|
||||
http = self.get_auth_http_client()
|
||||
token = dict(key=token, secret="")
|
||||
http.fetch(self._oauth_access_token_url(token),
|
||||
self.async_callback(self._on_access_token, callback))
|
||||
functools.partial(self._on_access_token, callback))
|
||||
else:
|
||||
chain_future(OpenIdMixin.get_authenticated_user(self),
|
||||
callback)
|
||||
@@ -945,6 +976,19 @@ class GoogleMixin(OpenIdMixin, OAuthMixin):
|
||||
class GoogleOAuth2Mixin(OAuth2Mixin):
|
||||
"""Google authentication using OAuth2.
|
||||
|
||||
In order to use, register your application with Google and copy the
|
||||
relevant parameters to your application settings.
|
||||
|
||||
* Go to the Google Dev Console at http://console.developers.google.com
|
||||
* Select a project, or create a new one.
|
||||
* In the sidebar on the left, select APIs & Auth.
|
||||
* In the list of APIs, find the Google+ API service and set it to ON.
|
||||
* In the sidebar on the left, select Credentials.
|
||||
* In the OAuth section of the page, select Create New Client ID.
|
||||
* Set the Redirect URI to point to your auth handler
|
||||
* Copy the "Client secret" and "Client ID" to the application settings as
|
||||
{"google_oauth": {"key": CLIENT_ID, "secret": CLIENT_SECRET}}
|
||||
|
||||
.. versionadded:: 3.2
|
||||
"""
|
||||
_OAUTH_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/auth"
|
||||
@@ -958,7 +1002,7 @@ class GoogleOAuth2Mixin(OAuth2Mixin):
|
||||
|
||||
Example usage::
|
||||
|
||||
class GoogleOAuth2LoginHandler(LoginHandler,
|
||||
class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
|
||||
tornado.auth.GoogleOAuth2Mixin):
|
||||
@tornado.gen.coroutine
|
||||
def get(self):
|
||||
@@ -985,7 +1029,7 @@ class GoogleOAuth2Mixin(OAuth2Mixin):
|
||||
})
|
||||
|
||||
http.fetch(self._OAUTH_ACCESS_TOKEN_URL,
|
||||
self.async_callback(self._on_access_token, callback),
|
||||
functools.partial(self._on_access_token, callback),
|
||||
method="POST", headers={'Content-Type': 'application/x-www-form-urlencoded'}, body=body)
|
||||
|
||||
def _on_access_token(self, future, response):
|
||||
@@ -1026,7 +1070,7 @@ class FacebookMixin(object):
|
||||
@tornado.web.asynchronous
|
||||
def get(self):
|
||||
if self.get_argument("session", None):
|
||||
self.get_authenticated_user(self.async_callback(self._on_auth))
|
||||
self.get_authenticated_user(self._on_auth)
|
||||
return
|
||||
yield self.authenticate_redirect()
|
||||
|
||||
@@ -1112,7 +1156,7 @@ class FacebookMixin(object):
|
||||
session = escape.json_decode(self.get_argument("session"))
|
||||
self.facebook_request(
|
||||
method="facebook.users.getInfo",
|
||||
callback=self.async_callback(
|
||||
callback=functools.partial(
|
||||
self._on_get_user_info, callback, session),
|
||||
session_key=session["session_key"],
|
||||
uids=session["uid"],
|
||||
@@ -1138,7 +1182,7 @@ class FacebookMixin(object):
|
||||
def get(self):
|
||||
self.facebook_request(
|
||||
method="stream.get",
|
||||
callback=self.async_callback(self._on_stream),
|
||||
callback=self._on_stream,
|
||||
session_key=self.current_user["session_key"])
|
||||
|
||||
def _on_stream(self, stream):
|
||||
@@ -1162,7 +1206,7 @@ class FacebookMixin(object):
|
||||
url = "http://api.facebook.com/restserver.php?" + \
|
||||
urllib_parse.urlencode(args)
|
||||
http = self.get_auth_http_client()
|
||||
http.fetch(url, callback=self.async_callback(
|
||||
http.fetch(url, callback=functools.partial(
|
||||
self._parse_response, callback))
|
||||
|
||||
def _on_get_user_info(self, callback, session, users):
|
||||
@@ -1260,7 +1304,7 @@ class FacebookGraphMixin(OAuth2Mixin):
|
||||
fields.update(extra_fields)
|
||||
|
||||
http.fetch(self._oauth_request_token_url(**args),
|
||||
self.async_callback(self._on_access_token, redirect_uri, client_id,
|
||||
functools.partial(self._on_access_token, redirect_uri, client_id,
|
||||
client_secret, callback, fields))
|
||||
|
||||
def _on_access_token(self, redirect_uri, client_id, client_secret,
|
||||
@@ -1277,7 +1321,7 @@ class FacebookGraphMixin(OAuth2Mixin):
|
||||
|
||||
self.facebook_request(
|
||||
path="/me",
|
||||
callback=self.async_callback(
|
||||
callback=functools.partial(
|
||||
self._on_get_user_info, future, session, fields),
|
||||
access_token=session["access_token"],
|
||||
fields=",".join(fields)
|
||||
@@ -1344,7 +1388,7 @@ class FacebookGraphMixin(OAuth2Mixin):
|
||||
|
||||
if all_args:
|
||||
url += "?" + urllib_parse.urlencode(all_args)
|
||||
callback = self.async_callback(self._on_facebook_request, callback)
|
||||
callback = functools.partial(self._on_facebook_request, callback)
|
||||
http = self.get_auth_http_client()
|
||||
if post_args is not None:
|
||||
http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args),
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""xAutomatically restart the server when a source file is modified.
|
||||
"""Automatically restart the server when a source file is modified.
|
||||
|
||||
Most applications should not access this module directly. Instead,
|
||||
pass the keyword argument ``autoreload=True`` to the
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+95
-34
@@ -40,52 +40,132 @@ class ReturnValueIgnoredError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class _DummyFuture(object):
|
||||
class Future(object):
|
||||
"""Placeholder for an asynchronous result.
|
||||
|
||||
A ``Future`` encapsulates the result of an asynchronous
|
||||
operation. In synchronous applications ``Futures`` are used
|
||||
to wait for the result from a thread or process pool; in
|
||||
Tornado they are normally used with `.IOLoop.add_future` or by
|
||||
yielding them in a `.gen.coroutine`.
|
||||
|
||||
`tornado.concurrent.Future` is similar to
|
||||
`concurrent.futures.Future`, but not thread-safe (and therefore
|
||||
faster for use with single-threaded event loops).
|
||||
|
||||
In addition to ``exception`` and ``set_exception``, methods ``exc_info``
|
||||
and ``set_exc_info`` are supported to capture tracebacks in Python 2.
|
||||
The traceback is automatically available in Python 3, but in the
|
||||
Python 2 futures backport this information is discarded.
|
||||
This functionality was previously available in a separate class
|
||||
``TracebackFuture``, which is now a deprecated alias for this class.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
`tornado.concurrent.Future` is always a thread-unsafe ``Future``
|
||||
with support for the ``exc_info`` methods. Previously it would
|
||||
be an alias for the thread-safe `concurrent.futures.Future`
|
||||
if that package was available and fall back to the thread-unsafe
|
||||
implementation if it was not.
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
self._done = False
|
||||
self._result = None
|
||||
self._exception = None
|
||||
self._exc_info = None
|
||||
self._callbacks = []
|
||||
|
||||
def cancel(self):
|
||||
"""Cancel the operation, if possible.
|
||||
|
||||
Tornado ``Futures`` do not support cancellation, so this method always
|
||||
returns False.
|
||||
"""
|
||||
return False
|
||||
|
||||
def cancelled(self):
|
||||
"""Returns True if the operation has been cancelled.
|
||||
|
||||
Tornado ``Futures`` do not support cancellation, so this method
|
||||
always returns False.
|
||||
"""
|
||||
return False
|
||||
|
||||
def running(self):
|
||||
"""Returns True if this operation is currently running."""
|
||||
return not self._done
|
||||
|
||||
def done(self):
|
||||
"""Returns True if the future has finished running."""
|
||||
return self._done
|
||||
|
||||
def result(self, timeout=None):
|
||||
self._check_done()
|
||||
if self._exception:
|
||||
"""If the operation succeeded, return its result. If it failed,
|
||||
re-raise its exception.
|
||||
"""
|
||||
if self._result is not None:
|
||||
return self._result
|
||||
if self._exc_info is not None:
|
||||
raise_exc_info(self._exc_info)
|
||||
elif self._exception is not None:
|
||||
raise self._exception
|
||||
self._check_done()
|
||||
return self._result
|
||||
|
||||
def exception(self, timeout=None):
|
||||
self._check_done()
|
||||
if self._exception:
|
||||
"""If the operation raised an exception, return the `Exception`
|
||||
object. Otherwise returns None.
|
||||
"""
|
||||
if self._exception is not None:
|
||||
return self._exception
|
||||
else:
|
||||
self._check_done()
|
||||
return None
|
||||
|
||||
def add_done_callback(self, fn):
|
||||
"""Attaches the given callback to the `Future`.
|
||||
|
||||
It will be invoked with the `Future` as its argument when the Future
|
||||
has finished running and its result is available. In Tornado
|
||||
consider using `.IOLoop.add_future` instead of calling
|
||||
`add_done_callback` directly.
|
||||
"""
|
||||
if self._done:
|
||||
fn(self)
|
||||
else:
|
||||
self._callbacks.append(fn)
|
||||
|
||||
def set_result(self, result):
|
||||
"""Sets the result of a ``Future``.
|
||||
|
||||
It is undefined to call any of the ``set`` methods more than once
|
||||
on the same object.
|
||||
"""
|
||||
self._result = result
|
||||
self._set_done()
|
||||
|
||||
def set_exception(self, exception):
|
||||
"""Sets the exception of a ``Future.``"""
|
||||
self._exception = exception
|
||||
self._set_done()
|
||||
|
||||
def exc_info(self):
|
||||
"""Returns a tuple in the same format as `sys.exc_info` or None.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
"""
|
||||
return self._exc_info
|
||||
|
||||
def set_exc_info(self, exc_info):
|
||||
"""Sets the exception information of a ``Future.``
|
||||
|
||||
Preserves tracebacks on Python 2.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
"""
|
||||
self._exc_info = exc_info
|
||||
self.set_exception(exc_info[1])
|
||||
|
||||
def _check_done(self):
|
||||
if not self._done:
|
||||
raise Exception("DummyFuture does not support blocking for results")
|
||||
@@ -97,38 +177,16 @@ class _DummyFuture(object):
|
||||
cb(self)
|
||||
self._callbacks = None
|
||||
|
||||
TracebackFuture = Future
|
||||
|
||||
if futures is None:
|
||||
Future = _DummyFuture
|
||||
FUTURES = Future
|
||||
else:
|
||||
Future = futures.Future
|
||||
FUTURES = (futures.Future, Future)
|
||||
|
||||
|
||||
class TracebackFuture(Future):
|
||||
"""Subclass of `Future` which can store a traceback with
|
||||
exceptions.
|
||||
|
||||
The traceback is automatically available in Python 3, but in the
|
||||
Python 2 futures backport this information is discarded.
|
||||
"""
|
||||
def __init__(self):
|
||||
super(TracebackFuture, self).__init__()
|
||||
self.__exc_info = None
|
||||
|
||||
def exc_info(self):
|
||||
return self.__exc_info
|
||||
|
||||
def set_exc_info(self, exc_info):
|
||||
"""Traceback-aware replacement for
|
||||
`~concurrent.futures.Future.set_exception`.
|
||||
"""
|
||||
self.__exc_info = exc_info
|
||||
self.set_exception(exc_info[1])
|
||||
|
||||
def result(self, timeout=None):
|
||||
if self.__exc_info is not None:
|
||||
raise_exc_info(self.__exc_info)
|
||||
else:
|
||||
return super(TracebackFuture, self).result(timeout=timeout)
|
||||
def is_future(x):
|
||||
return isinstance(x, FUTURES)
|
||||
|
||||
|
||||
class DummyExecutor(object):
|
||||
@@ -254,10 +312,13 @@ def return_future(f):
|
||||
def chain_future(a, b):
|
||||
"""Chain two futures together so that when one completes, so does the other.
|
||||
|
||||
The result (success or failure) of ``a`` will be copied to ``b``.
|
||||
The result (success or failure) of ``a`` will be copied to ``b``, unless
|
||||
``b`` has already been completed or cancelled by the time ``a`` finishes.
|
||||
"""
|
||||
def copy(future):
|
||||
assert future is a
|
||||
if b.done():
|
||||
return
|
||||
if (isinstance(a, TracebackFuture) and isinstance(b, TracebackFuture)
|
||||
and a.exc_info() is not None):
|
||||
b.set_exc_info(a.exc_info())
|
||||
|
||||
@@ -268,6 +268,7 @@ class CurlAsyncHTTPClient(AsyncHTTPClient):
|
||||
info["callback"](HTTPResponse(
|
||||
request=info["request"], code=code, headers=info["headers"],
|
||||
buffer=buffer, effective_url=effective_url, error=error,
|
||||
reason=info['headers'].get("X-Http-Reason", None),
|
||||
request_time=time.time() - info["curl_start_time"],
|
||||
time_info=time_info))
|
||||
except Exception:
|
||||
@@ -470,7 +471,11 @@ def _curl_header_callback(headers, header_line):
|
||||
header_line = header_line.strip()
|
||||
if header_line.startswith("HTTP/"):
|
||||
headers.clear()
|
||||
return
|
||||
try:
|
||||
(__, __, reason) = httputil.parse_response_start_line(header_line)
|
||||
header_line = "X-Http-Reason: %s" % reason
|
||||
except httputil.HTTPInputException:
|
||||
return
|
||||
if not header_line:
|
||||
return
|
||||
headers.parse_line(header_line)
|
||||
|
||||
@@ -75,7 +75,7 @@ def xhtml_unescape(value):
|
||||
|
||||
|
||||
# The fact that json_encode wraps json.dumps is an implementation detail.
|
||||
# Please see https://github.com/facebook/tornado/pull/706
|
||||
# Please see https://github.com/tornadoweb/tornado/pull/706
|
||||
# before sending a pull request that adds **kwargs to this function.
|
||||
def json_encode(value):
|
||||
"""JSON-encodes the given Python object."""
|
||||
|
||||
+328
-142
@@ -87,9 +87,9 @@ import itertools
|
||||
import sys
|
||||
import types
|
||||
|
||||
from tornado.concurrent import Future, TracebackFuture
|
||||
from tornado.concurrent import Future, TracebackFuture, is_future, chain_future
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.stack_context import ExceptionStackContext, wrap
|
||||
from tornado import stack_context
|
||||
|
||||
|
||||
class KeyReuseError(Exception):
|
||||
@@ -112,6 +112,10 @@ class ReturnValueIgnoredError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TimeoutError(Exception):
|
||||
"""Exception raised by ``with_timeout``."""
|
||||
|
||||
|
||||
def engine(func):
|
||||
"""Callback-oriented decorator for asynchronous generators.
|
||||
|
||||
@@ -129,45 +133,20 @@ def engine(func):
|
||||
`~tornado.web.RequestHandler` :ref:`HTTP verb methods <verbs>`,
|
||||
which use ``self.finish()`` in place of a callback argument.
|
||||
"""
|
||||
func = _make_coroutine_wrapper(func, replace_callback=False)
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
runner = None
|
||||
|
||||
def handle_exception(typ, value, tb):
|
||||
# if the function throws an exception before its first "yield"
|
||||
# (or is not a generator at all), the Runner won't exist yet.
|
||||
# However, in that case we haven't reached anything asynchronous
|
||||
# yet, so we can just let the exception propagate.
|
||||
if runner is not None:
|
||||
return runner.handle_exception(typ, value, tb)
|
||||
return False
|
||||
with ExceptionStackContext(handle_exception) as deactivate:
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
except (Return, StopIteration) as e:
|
||||
result = getattr(e, 'value', None)
|
||||
else:
|
||||
if isinstance(result, types.GeneratorType):
|
||||
def final_callback(value):
|
||||
if value is not None:
|
||||
raise ReturnValueIgnoredError(
|
||||
"@gen.engine functions cannot return values: "
|
||||
"%r" % (value,))
|
||||
assert value is None
|
||||
deactivate()
|
||||
runner = Runner(result, final_callback)
|
||||
runner.run()
|
||||
return
|
||||
if result is not None:
|
||||
future = func(*args, **kwargs)
|
||||
def final_callback(future):
|
||||
if future.result() is not None:
|
||||
raise ReturnValueIgnoredError(
|
||||
"@gen.engine functions cannot return values: %r" %
|
||||
(result,))
|
||||
deactivate()
|
||||
# no yield, so we're done
|
||||
(future.result(),))
|
||||
future.add_done_callback(final_callback)
|
||||
return wrapper
|
||||
|
||||
|
||||
def coroutine(func):
|
||||
def coroutine(func, replace_callback=True):
|
||||
"""Decorator for asynchronous generators.
|
||||
|
||||
Any generator that yields objects from this module must be wrapped
|
||||
@@ -191,43 +170,56 @@ def coroutine(func):
|
||||
From the caller's perspective, ``@gen.coroutine`` is similar to
|
||||
the combination of ``@return_future`` and ``@gen.engine``.
|
||||
"""
|
||||
return _make_coroutine_wrapper(func, replace_callback=True)
|
||||
|
||||
|
||||
def _make_coroutine_wrapper(func, replace_callback):
|
||||
"""The inner workings of ``@gen.coroutine`` and ``@gen.engine``.
|
||||
|
||||
The two decorators differ in their treatment of the ``callback``
|
||||
argument, so we cannot simply implement ``@engine`` in terms of
|
||||
``@coroutine``.
|
||||
"""
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
runner = None
|
||||
future = TracebackFuture()
|
||||
|
||||
if 'callback' in kwargs:
|
||||
if replace_callback and 'callback' in kwargs:
|
||||
callback = kwargs.pop('callback')
|
||||
IOLoop.current().add_future(
|
||||
future, lambda future: callback(future.result()))
|
||||
|
||||
def handle_exception(typ, value, tb):
|
||||
try:
|
||||
if runner is not None and runner.handle_exception(typ, value, tb):
|
||||
return True
|
||||
except Exception:
|
||||
typ, value, tb = sys.exc_info()
|
||||
future.set_exc_info((typ, value, tb))
|
||||
return True
|
||||
with ExceptionStackContext(handle_exception) as deactivate:
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
except (Return, StopIteration) as e:
|
||||
result = getattr(e, 'value', None)
|
||||
except Exception:
|
||||
deactivate()
|
||||
future.set_exc_info(sys.exc_info())
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
except (Return, StopIteration) as e:
|
||||
result = getattr(e, 'value', None)
|
||||
except Exception:
|
||||
future.set_exc_info(sys.exc_info())
|
||||
return future
|
||||
else:
|
||||
if isinstance(result, types.GeneratorType):
|
||||
# Inline the first iteration of Runner.run. This lets us
|
||||
# avoid the cost of creating a Runner when the coroutine
|
||||
# never actually yields, which in turn allows us to
|
||||
# use "optional" coroutines in critical path code without
|
||||
# performance penalty for the synchronous case.
|
||||
try:
|
||||
orig_stack_contexts = stack_context._state.contexts
|
||||
yielded = next(result)
|
||||
if stack_context._state.contexts is not orig_stack_contexts:
|
||||
yielded = TracebackFuture()
|
||||
yielded.set_exception(
|
||||
stack_context.StackContextInconsistentError(
|
||||
'stack_context inconsistency (probably caused '
|
||||
'by yield within a "with StackContext" block)'))
|
||||
except (StopIteration, Return) as e:
|
||||
future.set_result(getattr(e, 'value', None))
|
||||
except Exception:
|
||||
future.set_exc_info(sys.exc_info())
|
||||
else:
|
||||
Runner(result, future, yielded)
|
||||
return future
|
||||
else:
|
||||
if isinstance(result, types.GeneratorType):
|
||||
def final_callback(value):
|
||||
deactivate()
|
||||
future.set_result(value)
|
||||
runner = Runner(result, final_callback)
|
||||
runner.run()
|
||||
return future
|
||||
deactivate()
|
||||
future.set_result(result)
|
||||
future.set_result(result)
|
||||
return future
|
||||
return wrapper
|
||||
|
||||
@@ -348,7 +340,7 @@ class WaitAll(YieldPoint):
|
||||
return [self.runner.pop_result(key) for key in self.keys]
|
||||
|
||||
|
||||
class Task(YieldPoint):
|
||||
def Task(func, *args, **kwargs):
|
||||
"""Runs a single asynchronous operation.
|
||||
|
||||
Takes a function (and optional additional arguments) and runs it with
|
||||
@@ -362,25 +354,25 @@ class Task(YieldPoint):
|
||||
|
||||
func(args, callback=(yield gen.Callback(key)))
|
||||
result = yield gen.Wait(key)
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
``gen.Task`` is now a function that returns a `.Future`, instead of
|
||||
a subclass of `YieldPoint`. It still behaves the same way when
|
||||
yielded.
|
||||
"""
|
||||
def __init__(self, func, *args, **kwargs):
|
||||
assert "callback" not in kwargs
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.func = func
|
||||
|
||||
def start(self, runner):
|
||||
self.runner = runner
|
||||
self.key = object()
|
||||
runner.register_callback(self.key)
|
||||
self.kwargs["callback"] = runner.result_callback(self.key)
|
||||
self.func(*self.args, **self.kwargs)
|
||||
|
||||
def is_ready(self):
|
||||
return self.runner.is_ready(self.key)
|
||||
|
||||
def get_result(self):
|
||||
return self.runner.pop_result(self.key)
|
||||
future = Future()
|
||||
def handle_exception(typ, value, tb):
|
||||
if future.done():
|
||||
return False
|
||||
future.set_exc_info((typ, value, tb))
|
||||
return True
|
||||
def set_result(result):
|
||||
if future.done():
|
||||
return
|
||||
future.set_result(result)
|
||||
with stack_context.ExceptionStackContext(handle_exception):
|
||||
func(*args, callback=_argument_adapter(set_result), **kwargs)
|
||||
return future
|
||||
|
||||
|
||||
class YieldFuture(YieldPoint):
|
||||
@@ -414,10 +406,14 @@ class YieldFuture(YieldPoint):
|
||||
class Multi(YieldPoint):
|
||||
"""Runs multiple asynchronous operations in parallel.
|
||||
|
||||
Takes a list of ``Tasks`` or other ``YieldPoints`` and returns a list of
|
||||
Takes a list of ``YieldPoints`` or ``Futures`` and returns a list of
|
||||
their responses. It is not necessary to call `Multi` explicitly,
|
||||
since the engine will do so automatically when the generator yields
|
||||
a list of ``YieldPoints``.
|
||||
a list of ``YieldPoints`` or a mixture of ``YieldPoints`` and ``Futures``.
|
||||
|
||||
Instead of a list, the argument may also be a dictionary whose values are
|
||||
Futures, in which case a parallel dictionary is returned mapping the same
|
||||
keys to their results.
|
||||
"""
|
||||
def __init__(self, children):
|
||||
self.keys = None
|
||||
@@ -426,7 +422,7 @@ class Multi(YieldPoint):
|
||||
children = children.values()
|
||||
self.children = []
|
||||
for i in children:
|
||||
if isinstance(i, Future):
|
||||
if is_future(i):
|
||||
i = YieldFuture(i)
|
||||
self.children.append(i)
|
||||
assert all(isinstance(i, YieldPoint) for i in self.children)
|
||||
@@ -450,18 +446,127 @@ class Multi(YieldPoint):
|
||||
return list(result)
|
||||
|
||||
|
||||
class _NullYieldPoint(YieldPoint):
|
||||
def start(self, runner):
|
||||
pass
|
||||
def multi_future(children):
|
||||
"""Wait for multiple asynchronous futures in parallel.
|
||||
|
||||
def is_ready(self):
|
||||
return True
|
||||
Takes a list of ``Futures`` (but *not* other ``YieldPoints``) and returns
|
||||
a new Future that resolves when all the other Futures are done.
|
||||
If all the ``Futures`` succeeded, the returned Future's result is a list
|
||||
of their results. If any failed, the returned Future raises the exception
|
||||
of the first one to fail.
|
||||
|
||||
def get_result(self):
|
||||
return None
|
||||
Instead of a list, the argument may also be a dictionary whose values are
|
||||
Futures, in which case a parallel dictionary is returned mapping the same
|
||||
keys to their results.
|
||||
|
||||
It is not necessary to call `multi_future` explcitly, since the engine will
|
||||
do so automatically when the generator yields a list of `Futures`.
|
||||
This function is faster than the `Multi` `YieldPoint` because it does not
|
||||
require the creation of a stack context.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
"""
|
||||
if isinstance(children, dict):
|
||||
keys = list(children.keys())
|
||||
children = children.values()
|
||||
else:
|
||||
keys = None
|
||||
assert all(is_future(i) for i in children)
|
||||
unfinished_children = set(children)
|
||||
|
||||
future = Future()
|
||||
if not children:
|
||||
future.set_result({} if keys is not None else [])
|
||||
def callback(f):
|
||||
unfinished_children.remove(f)
|
||||
if not unfinished_children:
|
||||
try:
|
||||
result_list = [i.result() for i in children]
|
||||
except Exception:
|
||||
future.set_exc_info(sys.exc_info())
|
||||
else:
|
||||
if keys is not None:
|
||||
future.set_result(dict(zip(keys, result_list)))
|
||||
else:
|
||||
future.set_result(result_list)
|
||||
for f in children:
|
||||
f.add_done_callback(callback)
|
||||
return future
|
||||
|
||||
|
||||
_null_yield_point = _NullYieldPoint()
|
||||
def maybe_future(x):
|
||||
"""Converts ``x`` into a `.Future`.
|
||||
|
||||
If ``x`` is already a `.Future`, it is simply returned; otherwise
|
||||
it is wrapped in a new `.Future`. This is suitable for use as
|
||||
``result = yield gen.maybe_future(f())`` when you don't know whether
|
||||
``f()`` returns a `.Future` or not.
|
||||
"""
|
||||
if is_future(x):
|
||||
return x
|
||||
else:
|
||||
fut = Future()
|
||||
fut.set_result(x)
|
||||
return fut
|
||||
|
||||
|
||||
def with_timeout(timeout, future, io_loop=None):
|
||||
"""Wraps a `.Future` in a timeout.
|
||||
|
||||
Raises `TimeoutError` if the input future does not complete before
|
||||
``timeout``, which may be specified in any form allowed by
|
||||
`.IOLoop.add_timeout` (i.e. a `datetime.timedelta` or an absolute time
|
||||
relative to `.IOLoop.time`)
|
||||
|
||||
Currently only supports Futures, not other `YieldPoint` classes.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
"""
|
||||
# TODO: allow yield points in addition to futures?
|
||||
# Tricky to do with stack_context semantics.
|
||||
#
|
||||
# It's tempting to optimize this by cancelling the input future on timeout
|
||||
# instead of creating a new one, but A) we can't know if we are the only
|
||||
# one waiting on the input future, so cancelling it might disrupt other
|
||||
# callers and B) concurrent futures can only be cancelled while they are
|
||||
# in the queue, so cancellation cannot reliably bound our waiting time.
|
||||
result = Future()
|
||||
chain_future(future, result)
|
||||
if io_loop is None:
|
||||
io_loop = IOLoop.current()
|
||||
timeout_handle = io_loop.add_timeout(
|
||||
timeout,
|
||||
lambda: result.set_exception(TimeoutError("Timeout")))
|
||||
if isinstance(future, Future):
|
||||
# We know this future will resolve on the IOLoop, so we don't
|
||||
# need the extra thread-safety of IOLoop.add_future (and we also
|
||||
# don't care about StackContext here.
|
||||
future.add_done_callback(
|
||||
lambda future: io_loop.remove_timeout(timeout_handle))
|
||||
else:
|
||||
# concurrent.futures.Futures may resolve on any thread, so we
|
||||
# need to route them back to the IOLoop.
|
||||
io_loop.add_future(
|
||||
future, lambda future: io_loop.remove_timeout(timeout_handle))
|
||||
return result
|
||||
|
||||
|
||||
_null_future = Future()
|
||||
_null_future.set_result(None)
|
||||
|
||||
moment = Future()
|
||||
moment.__doc__ = \
|
||||
"""A special object which may be yielded to allow the IOLoop to run for
|
||||
one iteration.
|
||||
|
||||
This is not needed in normal use but it can be helpful in long-running
|
||||
coroutines that are likely to yield Futures that are ready instantly.
|
||||
|
||||
Usage: ``yield gen.moment``
|
||||
|
||||
.. versionadded:: 3.3
|
||||
"""
|
||||
moment.set_result(None)
|
||||
|
||||
|
||||
class Runner(object):
|
||||
@@ -469,35 +574,55 @@ class Runner(object):
|
||||
|
||||
Maintains information about pending callbacks and their results.
|
||||
|
||||
``final_callback`` is run after the generator exits.
|
||||
The results of the generator are stored in ``result_future`` (a
|
||||
`.TracebackFuture`)
|
||||
"""
|
||||
def __init__(self, gen, final_callback):
|
||||
def __init__(self, gen, result_future, first_yielded):
|
||||
self.gen = gen
|
||||
self.final_callback = final_callback
|
||||
self.yield_point = _null_yield_point
|
||||
self.pending_callbacks = set()
|
||||
self.results = {}
|
||||
self.result_future = result_future
|
||||
self.future = _null_future
|
||||
self.yield_point = None
|
||||
self.pending_callbacks = None
|
||||
self.results = None
|
||||
self.running = False
|
||||
self.finished = False
|
||||
self.exc_info = None
|
||||
self.had_exception = False
|
||||
self.io_loop = IOLoop.current()
|
||||
# For efficiency, we do not create a stack context until we
|
||||
# reach a YieldPoint (stack contexts are required for the historical
|
||||
# semantics of YieldPoints, but not for Futures). When we have
|
||||
# done so, this field will be set and must be called at the end
|
||||
# of the coroutine.
|
||||
self.stack_context_deactivate = None
|
||||
if self.handle_yield(first_yielded):
|
||||
self.run()
|
||||
|
||||
def register_callback(self, key):
|
||||
"""Adds ``key`` to the list of callbacks."""
|
||||
if self.pending_callbacks is None:
|
||||
# Lazily initialize the old-style YieldPoint data structures.
|
||||
self.pending_callbacks = set()
|
||||
self.results = {}
|
||||
if key in self.pending_callbacks:
|
||||
raise KeyReuseError("key %r is already pending" % (key,))
|
||||
self.pending_callbacks.add(key)
|
||||
|
||||
def is_ready(self, key):
|
||||
"""Returns true if a result is available for ``key``."""
|
||||
if key not in self.pending_callbacks:
|
||||
if self.pending_callbacks is None or key not in self.pending_callbacks:
|
||||
raise UnknownKeyError("key %r is not pending" % (key,))
|
||||
return key in self.results
|
||||
|
||||
def set_result(self, key, result):
|
||||
"""Sets the result for ``key`` and attempts to resume the generator."""
|
||||
self.results[key] = result
|
||||
self.run()
|
||||
if self.yield_point is not None and self.yield_point.is_ready():
|
||||
try:
|
||||
self.future.set_result(self.yield_point.get_result())
|
||||
except:
|
||||
self.future.set_exc_info(sys.exc_info())
|
||||
self.yield_point = None
|
||||
self.run()
|
||||
|
||||
def pop_result(self, key):
|
||||
"""Returns the result for ``key`` and unregisters it."""
|
||||
@@ -513,25 +638,27 @@ class Runner(object):
|
||||
try:
|
||||
self.running = True
|
||||
while True:
|
||||
if self.exc_info is None:
|
||||
try:
|
||||
if not self.yield_point.is_ready():
|
||||
return
|
||||
next = self.yield_point.get_result()
|
||||
self.yield_point = None
|
||||
except Exception:
|
||||
self.exc_info = sys.exc_info()
|
||||
future = self.future
|
||||
if not future.done():
|
||||
return
|
||||
self.future = None
|
||||
try:
|
||||
if self.exc_info is not None:
|
||||
orig_stack_contexts = stack_context._state.contexts
|
||||
try:
|
||||
value = future.result()
|
||||
except Exception:
|
||||
self.had_exception = True
|
||||
exc_info = self.exc_info
|
||||
self.exc_info = None
|
||||
yielded = self.gen.throw(*exc_info)
|
||||
yielded = self.gen.throw(*sys.exc_info())
|
||||
else:
|
||||
yielded = self.gen.send(next)
|
||||
yielded = self.gen.send(value)
|
||||
if stack_context._state.contexts is not orig_stack_contexts:
|
||||
self.gen.throw(
|
||||
stack_context.StackContextInconsistentError(
|
||||
'stack_context inconsistency (probably caused '
|
||||
'by yield within a "with StackContext" block)'))
|
||||
except (StopIteration, Return) as e:
|
||||
self.finished = True
|
||||
self.yield_point = _null_yield_point
|
||||
self.future = _null_future
|
||||
if self.pending_callbacks and not self.had_exception:
|
||||
# If we ran cleanly without waiting on all callbacks
|
||||
# raise an error (really more of a warning). If we
|
||||
@@ -540,46 +667,105 @@ class Runner(object):
|
||||
raise LeakedCallbackError(
|
||||
"finished without waiting for callbacks %r" %
|
||||
self.pending_callbacks)
|
||||
self.final_callback(getattr(e, 'value', None))
|
||||
self.final_callback = None
|
||||
self.result_future.set_result(getattr(e, 'value', None))
|
||||
self.result_future = None
|
||||
self._deactivate_stack_context()
|
||||
return
|
||||
except Exception:
|
||||
self.finished = True
|
||||
self.yield_point = _null_yield_point
|
||||
raise
|
||||
if isinstance(yielded, (list, dict)):
|
||||
yielded = Multi(yielded)
|
||||
elif isinstance(yielded, Future):
|
||||
yielded = YieldFuture(yielded)
|
||||
if isinstance(yielded, YieldPoint):
|
||||
self.yield_point = yielded
|
||||
try:
|
||||
self.yield_point.start(self)
|
||||
except Exception:
|
||||
self.exc_info = sys.exc_info()
|
||||
else:
|
||||
self.exc_info = (BadYieldError(
|
||||
"yielded unknown object %r" % (yielded,)),)
|
||||
self.future = _null_future
|
||||
self.result_future.set_exc_info(sys.exc_info())
|
||||
self.result_future = None
|
||||
self._deactivate_stack_context()
|
||||
return
|
||||
if not self.handle_yield(yielded):
|
||||
return
|
||||
finally:
|
||||
self.running = False
|
||||
|
||||
def result_callback(self, key):
|
||||
def inner(*args, **kwargs):
|
||||
if kwargs or len(args) > 1:
|
||||
result = Arguments(args, kwargs)
|
||||
elif args:
|
||||
result = args[0]
|
||||
def handle_yield(self, yielded):
|
||||
if isinstance(yielded, list):
|
||||
if all(is_future(f) for f in yielded):
|
||||
yielded = multi_future(yielded)
|
||||
else:
|
||||
result = None
|
||||
self.set_result(key, result)
|
||||
return wrap(inner)
|
||||
yielded = Multi(yielded)
|
||||
elif isinstance(yielded, dict):
|
||||
if all(is_future(f) for f in yielded.values()):
|
||||
yielded = multi_future(yielded)
|
||||
else:
|
||||
yielded = Multi(yielded)
|
||||
|
||||
if isinstance(yielded, YieldPoint):
|
||||
self.future = TracebackFuture()
|
||||
def start_yield_point():
|
||||
try:
|
||||
yielded.start(self)
|
||||
if yielded.is_ready():
|
||||
self.future.set_result(
|
||||
yielded.get_result())
|
||||
else:
|
||||
self.yield_point = yielded
|
||||
except Exception:
|
||||
self.future = TracebackFuture()
|
||||
self.future.set_exc_info(sys.exc_info())
|
||||
if self.stack_context_deactivate is None:
|
||||
# Start a stack context if this is the first
|
||||
# YieldPoint we've seen.
|
||||
with stack_context.ExceptionStackContext(
|
||||
self.handle_exception) as deactivate:
|
||||
self.stack_context_deactivate = deactivate
|
||||
def cb():
|
||||
start_yield_point()
|
||||
self.run()
|
||||
self.io_loop.add_callback(cb)
|
||||
return False
|
||||
else:
|
||||
start_yield_point()
|
||||
elif is_future(yielded):
|
||||
self.future = yielded
|
||||
if not self.future.done() or self.future is moment:
|
||||
self.io_loop.add_future(
|
||||
self.future, lambda f: self.run())
|
||||
return False
|
||||
else:
|
||||
self.future = TracebackFuture()
|
||||
self.future.set_exception(BadYieldError(
|
||||
"yielded unknown object %r" % (yielded,)))
|
||||
return True
|
||||
|
||||
def result_callback(self, key):
|
||||
return stack_context.wrap(_argument_adapter(
|
||||
functools.partial(self.set_result, key)))
|
||||
|
||||
def handle_exception(self, typ, value, tb):
|
||||
if not self.running and not self.finished:
|
||||
self.exc_info = (typ, value, tb)
|
||||
self.future = TracebackFuture()
|
||||
self.future.set_exc_info((typ, value, tb))
|
||||
self.run()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _deactivate_stack_context(self):
|
||||
if self.stack_context_deactivate is not None:
|
||||
self.stack_context_deactivate()
|
||||
self.stack_context_deactivate = None
|
||||
|
||||
Arguments = collections.namedtuple('Arguments', ['args', 'kwargs'])
|
||||
|
||||
|
||||
def _argument_adapter(callback):
|
||||
"""Returns a function that when invoked runs ``callback`` with one arg.
|
||||
|
||||
If the function returned by this function is called with exactly
|
||||
one argument, that argument is passed to ``callback``. Otherwise
|
||||
the args tuple and kwargs dict are wrapped in an `Arguments` object.
|
||||
"""
|
||||
def wrapper(*args, **kwargs):
|
||||
if kwargs or len(args) > 1:
|
||||
callback(Arguments(args, kwargs))
|
||||
elif args:
|
||||
callback(args[0])
|
||||
else:
|
||||
callback(None)
|
||||
return wrapper
|
||||
|
||||
@@ -0,0 +1,650 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2014 Facebook
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Client and server implementations of HTTP/1.x.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function, with_statement
|
||||
|
||||
from tornado.concurrent import Future
|
||||
from tornado.escape import native_str, utf8
|
||||
from tornado import gen
|
||||
from tornado import httputil
|
||||
from tornado import iostream
|
||||
from tornado.log import gen_log, app_log
|
||||
from tornado import stack_context
|
||||
from tornado.util import GzipDecompressor
|
||||
|
||||
|
||||
class _QuietException(Exception):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
class _ExceptionLoggingContext(object):
|
||||
"""Used with the ``with`` statement when calling delegate methods to
|
||||
log any exceptions with the given logger. Any exceptions caught are
|
||||
converted to _QuietException
|
||||
"""
|
||||
def __init__(self, logger):
|
||||
self.logger = logger
|
||||
|
||||
def __enter__(self):
|
||||
pass
|
||||
|
||||
def __exit__(self, typ, value, tb):
|
||||
if value is not None:
|
||||
self.logger.error("Uncaught exception", exc_info=(typ, value, tb))
|
||||
raise _QuietException
|
||||
|
||||
class HTTP1ConnectionParameters(object):
|
||||
"""Parameters for `.HTTP1Connection` and `.HTTP1ServerConnection`.
|
||||
"""
|
||||
def __init__(self, no_keep_alive=False, chunk_size=None,
|
||||
max_header_size=None, header_timeout=None, max_body_size=None,
|
||||
body_timeout=None, use_gzip=False):
|
||||
"""
|
||||
:arg bool no_keep_alive: If true, always close the connection after
|
||||
one request.
|
||||
:arg int chunk_size: how much data to read into memory at once
|
||||
:arg int max_header_size: maximum amount of data for HTTP headers
|
||||
:arg float header_timeout: how long to wait for all headers (seconds)
|
||||
:arg int max_body_size: maximum amount of data for body
|
||||
:arg float body_timeout: how long to wait while reading body (seconds)
|
||||
:arg bool use_gzip: if true, decode incoming ``Content-Encoding: gzip``
|
||||
"""
|
||||
self.no_keep_alive = no_keep_alive
|
||||
self.chunk_size = chunk_size or 65536
|
||||
self.max_header_size = max_header_size or 65536
|
||||
self.header_timeout = header_timeout
|
||||
self.max_body_size = max_body_size
|
||||
self.body_timeout = body_timeout
|
||||
self.use_gzip = use_gzip
|
||||
|
||||
|
||||
class HTTP1Connection(httputil.HTTPConnection):
|
||||
"""Implements the HTTP/1.x protocol.
|
||||
|
||||
This class can be on its own for clients, or via `HTTP1ServerConnection`
|
||||
for servers.
|
||||
"""
|
||||
def __init__(self, stream, is_client, params=None, context=None):
|
||||
"""
|
||||
:arg stream: an `.IOStream`
|
||||
:arg bool is_client: client or server
|
||||
:arg params: a `.HTTP1ConnectionParameters` instance or ``None``
|
||||
:arg context: an opaque application-defined object that can be accessed
|
||||
as ``connection.context``.
|
||||
"""
|
||||
self.is_client = is_client
|
||||
self.stream = stream
|
||||
if params is None:
|
||||
params = HTTP1ConnectionParameters()
|
||||
self.params = params
|
||||
self.context = context
|
||||
self.no_keep_alive = params.no_keep_alive
|
||||
# The body limits can be altered by the delegate, so save them
|
||||
# here instead of just referencing self.params later.
|
||||
self._max_body_size = (self.params.max_body_size or
|
||||
self.stream.max_buffer_size)
|
||||
self._body_timeout = self.params.body_timeout
|
||||
# _write_finished is set to True when finish() has been called,
|
||||
# i.e. there will be no more data sent. Data may still be in the
|
||||
# stream's write buffer.
|
||||
self._write_finished = False
|
||||
# True when we have read the entire incoming body.
|
||||
self._read_finished = False
|
||||
# _finish_future resolves when all data has been written and flushed
|
||||
# to the IOStream.
|
||||
self._finish_future = Future()
|
||||
# If true, the connection should be closed after this request
|
||||
# (after the response has been written in the server side,
|
||||
# and after it has been read in the client)
|
||||
self._disconnect_on_finish = False
|
||||
self._clear_callbacks()
|
||||
# Save the start lines after we read or write them; they
|
||||
# affect later processing (e.g. 304 responses and HEAD methods
|
||||
# have content-length but no bodies)
|
||||
self._request_start_line = None
|
||||
self._response_start_line = None
|
||||
self._request_headers = None
|
||||
# True if we are writing output with chunked encoding.
|
||||
self._chunking_output = None
|
||||
# While reading a body with a content-length, this is the
|
||||
# amount left to read.
|
||||
self._expected_content_remaining = None
|
||||
# A Future for our outgoing writes, returned by IOStream.write.
|
||||
self._pending_write = None
|
||||
|
||||
def read_response(self, delegate):
|
||||
"""Read a single HTTP response.
|
||||
|
||||
Typical client-mode usage is to write a request using `write_headers`,
|
||||
`write`, and `finish`, and then call ``read_response``.
|
||||
|
||||
:arg delegate: a `.HTTPMessageDelegate`
|
||||
|
||||
Returns a `.Future` that resolves to None after the full response has
|
||||
been read.
|
||||
"""
|
||||
if self.params.use_gzip:
|
||||
delegate = _GzipMessageDelegate(delegate, self.params.chunk_size)
|
||||
return self._read_message(delegate)
|
||||
|
||||
@gen.coroutine
|
||||
def _read_message(self, delegate):
|
||||
need_delegate_close = False
|
||||
try:
|
||||
header_future = self.stream.read_until_regex(
|
||||
b"\r?\n\r?\n",
|
||||
max_bytes=self.params.max_header_size)
|
||||
if self.params.header_timeout is None:
|
||||
header_data = yield header_future
|
||||
else:
|
||||
try:
|
||||
header_data = yield gen.with_timeout(
|
||||
self.stream.io_loop.time() + self.params.header_timeout,
|
||||
header_future,
|
||||
io_loop=self.stream.io_loop)
|
||||
except gen.TimeoutError:
|
||||
self.close()
|
||||
raise gen.Return(False)
|
||||
start_line, headers = self._parse_headers(header_data)
|
||||
if self.is_client:
|
||||
start_line = httputil.parse_response_start_line(start_line)
|
||||
self._response_start_line = start_line
|
||||
else:
|
||||
start_line = httputil.parse_request_start_line(start_line)
|
||||
self._request_start_line = start_line
|
||||
self._request_headers = headers
|
||||
|
||||
self._disconnect_on_finish = not self._can_keep_alive(
|
||||
start_line, headers)
|
||||
need_delegate_close = True
|
||||
with _ExceptionLoggingContext(app_log):
|
||||
header_future = delegate.headers_received(start_line, headers)
|
||||
if header_future is not None:
|
||||
yield header_future
|
||||
if self.stream is None:
|
||||
# We've been detached.
|
||||
need_delegate_close = False
|
||||
raise gen.Return(False)
|
||||
skip_body = False
|
||||
if self.is_client:
|
||||
if (self._request_start_line is not None and
|
||||
self._request_start_line.method == 'HEAD'):
|
||||
skip_body = True
|
||||
code = start_line.code
|
||||
if code == 304:
|
||||
skip_body = True
|
||||
if code >= 100 and code < 200:
|
||||
# TODO: client delegates will get headers_received twice
|
||||
# in the case of a 100-continue. Document or change?
|
||||
yield self._read_message(delegate)
|
||||
else:
|
||||
if (headers.get("Expect") == "100-continue" and
|
||||
not self._write_finished):
|
||||
self.stream.write(b"HTTP/1.1 100 (Continue)\r\n\r\n")
|
||||
if not skip_body:
|
||||
body_future = self._read_body(headers, delegate)
|
||||
if body_future is not None:
|
||||
if self._body_timeout is None:
|
||||
yield body_future
|
||||
else:
|
||||
try:
|
||||
yield gen.with_timeout(
|
||||
self.stream.io_loop.time() + self._body_timeout,
|
||||
body_future, self.stream.io_loop)
|
||||
except gen.TimeoutError:
|
||||
gen_log.info("Timeout reading body from %s",
|
||||
self.context)
|
||||
self.stream.close()
|
||||
raise gen.Return(False)
|
||||
self._read_finished = True
|
||||
if not self._write_finished or self.is_client:
|
||||
need_delegate_close = False
|
||||
with _ExceptionLoggingContext(app_log):
|
||||
delegate.finish()
|
||||
# If we're waiting for the application to produce an asynchronous
|
||||
# response, and we're not detached, register a close callback
|
||||
# on the stream (we didn't need one while we were reading)
|
||||
if (not self._finish_future.done() and
|
||||
self.stream is not None and
|
||||
not self.stream.closed()):
|
||||
self.stream.set_close_callback(self._on_connection_close)
|
||||
yield self._finish_future
|
||||
if self.is_client and self._disconnect_on_finish:
|
||||
self.close()
|
||||
if self.stream is None:
|
||||
raise gen.Return(False)
|
||||
except httputil.HTTPInputException as e:
|
||||
gen_log.info("Malformed HTTP message from %s: %s",
|
||||
self.context, e)
|
||||
self.close()
|
||||
raise gen.Return(False)
|
||||
finally:
|
||||
if need_delegate_close:
|
||||
with _ExceptionLoggingContext(app_log):
|
||||
delegate.on_connection_close()
|
||||
self._clear_callbacks()
|
||||
raise gen.Return(True)
|
||||
|
||||
def _clear_callbacks(self):
|
||||
"""Clears the callback attributes.
|
||||
|
||||
This allows the request handler to be garbage collected more
|
||||
quickly in CPython by breaking up reference cycles.
|
||||
"""
|
||||
self._write_callback = None
|
||||
self._write_future = None
|
||||
self._close_callback = None
|
||||
if self.stream is not None:
|
||||
self.stream.set_close_callback(None)
|
||||
|
||||
def set_close_callback(self, callback):
|
||||
"""Sets a callback that will be run when the connection is closed.
|
||||
|
||||
.. deprecated:: 3.3
|
||||
Use `.HTTPMessageDelegate.on_connection_close` instead.
|
||||
"""
|
||||
self._close_callback = stack_context.wrap(callback)
|
||||
|
||||
def _on_connection_close(self):
|
||||
# Note that this callback is only registered on the IOStream
|
||||
# when we have finished reading the request and are waiting for
|
||||
# the application to produce its response.
|
||||
if self._close_callback is not None:
|
||||
callback = self._close_callback
|
||||
self._close_callback = None
|
||||
callback()
|
||||
if not self._finish_future.done():
|
||||
self._finish_future.set_result(None)
|
||||
self._clear_callbacks()
|
||||
|
||||
def close(self):
|
||||
if self.stream is not None:
|
||||
self.stream.close()
|
||||
self._clear_callbacks()
|
||||
if not self._finish_future.done():
|
||||
self._finish_future.set_result(None)
|
||||
|
||||
def detach(self):
|
||||
"""Take control of the underlying stream.
|
||||
|
||||
Returns the underlying `.IOStream` object and stops all further
|
||||
HTTP processing. May only be called during
|
||||
`.HTTPMessageDelegate.headers_received`. Intended for implementing
|
||||
protocols like websockets that tunnel over an HTTP handshake.
|
||||
"""
|
||||
self._clear_callbacks()
|
||||
stream = self.stream
|
||||
self.stream = None
|
||||
return stream
|
||||
|
||||
def set_body_timeout(self, timeout):
|
||||
"""Sets the body timeout for a single request.
|
||||
|
||||
Overrides the value from `.HTTP1ConnectionParameters`.
|
||||
"""
|
||||
self._body_timeout = timeout
|
||||
|
||||
def set_max_body_size(self, max_body_size):
|
||||
"""Sets the body size limit for a single request.
|
||||
|
||||
Overrides the value from `.HTTP1ConnectionParameters`.
|
||||
"""
|
||||
self._max_body_size = max_body_size
|
||||
|
||||
def write_headers(self, start_line, headers, chunk=None, callback=None):
|
||||
"""Implements `.HTTPConnection.write_headers`."""
|
||||
if self.is_client:
|
||||
self._request_start_line = start_line
|
||||
# Client requests with a non-empty body must have either a
|
||||
# Content-Length or a Transfer-Encoding.
|
||||
self._chunking_output = (
|
||||
start_line.method in ('POST', 'PUT', 'PATCH') and
|
||||
'Content-Length' not in headers and
|
||||
'Transfer-Encoding' not in headers)
|
||||
else:
|
||||
self._response_start_line = start_line
|
||||
self._chunking_output = (
|
||||
# TODO: should this use
|
||||
# self._request_start_line.version or
|
||||
# start_line.version?
|
||||
self._request_start_line.version == 'HTTP/1.1' and
|
||||
# 304 responses have no body (not even a zero-length body), and so
|
||||
# should not have either Content-Length or Transfer-Encoding.
|
||||
# headers.
|
||||
start_line.code != 304 and
|
||||
# No need to chunk the output if a Content-Length is specified.
|
||||
'Content-Length' not in headers and
|
||||
# Applications are discouraged from touching Transfer-Encoding,
|
||||
# but if they do, leave it alone.
|
||||
'Transfer-Encoding' not in headers)
|
||||
# If a 1.0 client asked for keep-alive, add the header.
|
||||
if (self._request_start_line.version == 'HTTP/1.0' and
|
||||
(self._request_headers.get('Connection', '').lower()
|
||||
== 'keep-alive')):
|
||||
headers['Connection'] = 'Keep-Alive'
|
||||
if self._chunking_output:
|
||||
headers['Transfer-Encoding'] = 'chunked'
|
||||
if (not self.is_client and
|
||||
(self._request_start_line.method == 'HEAD' or
|
||||
start_line.code == 304)):
|
||||
self._expected_content_remaining = 0
|
||||
elif 'Content-Length' in headers:
|
||||
self._expected_content_remaining = int(headers['Content-Length'])
|
||||
else:
|
||||
self._expected_content_remaining = None
|
||||
lines = [utf8("%s %s %s" % start_line)]
|
||||
lines.extend([utf8(n) + b": " + utf8(v) for n, v in headers.get_all()])
|
||||
for line in lines:
|
||||
if b'\n' in line:
|
||||
raise ValueError('Newline in header: ' + repr(line))
|
||||
future = None
|
||||
if self.stream.closed():
|
||||
future = self._write_future = Future()
|
||||
future.set_exception(iostream.StreamClosedError())
|
||||
else:
|
||||
if callback is not None:
|
||||
self._write_callback = stack_context.wrap(callback)
|
||||
else:
|
||||
future = self._write_future = Future()
|
||||
data = b"\r\n".join(lines) + b"\r\n\r\n"
|
||||
if chunk:
|
||||
data += self._format_chunk(chunk)
|
||||
self._pending_write = self.stream.write(data)
|
||||
self._pending_write.add_done_callback(self._on_write_complete)
|
||||
return future
|
||||
|
||||
def _format_chunk(self, chunk):
|
||||
if self._expected_content_remaining is not None:
|
||||
self._expected_content_remaining -= len(chunk)
|
||||
if self._expected_content_remaining < 0:
|
||||
# Close the stream now to stop further framing errors.
|
||||
self.stream.close()
|
||||
raise httputil.HTTPOutputException(
|
||||
"Tried to write more data than Content-Length")
|
||||
if self._chunking_output and chunk:
|
||||
# Don't write out empty chunks because that means END-OF-STREAM
|
||||
# with chunked encoding
|
||||
return utf8("%x" % len(chunk)) + b"\r\n" + chunk + b"\r\n"
|
||||
else:
|
||||
return chunk
|
||||
|
||||
def write(self, chunk, callback=None):
|
||||
"""Implements `.HTTPConnection.write`.
|
||||
|
||||
For backwards compatibility is is allowed but deprecated to
|
||||
skip `write_headers` and instead call `write()` with a
|
||||
pre-encoded header block.
|
||||
"""
|
||||
future = None
|
||||
if self.stream.closed():
|
||||
future = self._write_future = Future()
|
||||
self._write_future.set_exception(iostream.StreamClosedError())
|
||||
else:
|
||||
if callback is not None:
|
||||
self._write_callback = stack_context.wrap(callback)
|
||||
else:
|
||||
future = self._write_future = Future()
|
||||
self._pending_write = self.stream.write(self._format_chunk(chunk))
|
||||
self._pending_write.add_done_callback(self._on_write_complete)
|
||||
return future
|
||||
|
||||
def finish(self):
|
||||
"""Implements `.HTTPConnection.finish`."""
|
||||
if (self._expected_content_remaining is not None and
|
||||
self._expected_content_remaining != 0 and
|
||||
not self.stream.closed()):
|
||||
self.stream.close()
|
||||
raise httputil.HTTPOutputException(
|
||||
"Tried to write %d bytes less than Content-Length" %
|
||||
self._expected_content_remaining)
|
||||
if self._chunking_output:
|
||||
if not self.stream.closed():
|
||||
self._pending_write = self.stream.write(b"0\r\n\r\n")
|
||||
self._pending_write.add_done_callback(self._on_write_complete)
|
||||
self._write_finished = True
|
||||
# If the app finished the request while we're still reading,
|
||||
# divert any remaining data away from the delegate and
|
||||
# close the connection when we're done sending our response.
|
||||
# Closing the connection is the only way to avoid reading the
|
||||
# whole input body.
|
||||
if not self._read_finished:
|
||||
self._disconnect_on_finish = True
|
||||
# No more data is coming, so instruct TCP to send any remaining
|
||||
# data immediately instead of waiting for a full packet or ack.
|
||||
self.stream.set_nodelay(True)
|
||||
if self._pending_write is None:
|
||||
self._finish_request(None)
|
||||
else:
|
||||
self._pending_write.add_done_callback(self._finish_request)
|
||||
|
||||
def _on_write_complete(self, future):
|
||||
if self._write_callback is not None:
|
||||
callback = self._write_callback
|
||||
self._write_callback = None
|
||||
self.stream.io_loop.add_callback(callback)
|
||||
if self._write_future is not None:
|
||||
future = self._write_future
|
||||
self._write_future = None
|
||||
future.set_result(None)
|
||||
|
||||
def _can_keep_alive(self, start_line, headers):
|
||||
if self.params.no_keep_alive:
|
||||
return False
|
||||
connection_header = headers.get("Connection")
|
||||
if connection_header is not None:
|
||||
connection_header = connection_header.lower()
|
||||
if start_line.version == "HTTP/1.1":
|
||||
return connection_header != "close"
|
||||
elif ("Content-Length" in headers
|
||||
or start_line.method in ("HEAD", "GET")):
|
||||
return connection_header == "keep-alive"
|
||||
return False
|
||||
|
||||
def _finish_request(self, future):
|
||||
self._clear_callbacks()
|
||||
if not self.is_client and self._disconnect_on_finish:
|
||||
self.close()
|
||||
return
|
||||
# Turn Nagle's algorithm back on, leaving the stream in its
|
||||
# default state for the next request.
|
||||
self.stream.set_nodelay(False)
|
||||
if not self._finish_future.done():
|
||||
self._finish_future.set_result(None)
|
||||
|
||||
def _parse_headers(self, data):
|
||||
data = native_str(data.decode('latin1'))
|
||||
eol = data.find("\r\n")
|
||||
start_line = data[:eol]
|
||||
try:
|
||||
headers = httputil.HTTPHeaders.parse(data[eol:])
|
||||
except ValueError:
|
||||
# probably form split() if there was no ':' in the line
|
||||
raise httputil.HTTPInputException("Malformed HTTP headers: %r" %
|
||||
data[eol:100])
|
||||
return start_line, headers
|
||||
|
||||
def _read_body(self, headers, delegate):
|
||||
content_length = headers.get("Content-Length")
|
||||
if content_length:
|
||||
content_length = int(content_length)
|
||||
if content_length > self._max_body_size:
|
||||
raise httputil.HTTPInputException("Content-Length too long")
|
||||
return self._read_fixed_body(content_length, delegate)
|
||||
if headers.get("Transfer-Encoding") == "chunked":
|
||||
return self._read_chunked_body(delegate)
|
||||
if self.is_client:
|
||||
return self._read_body_until_close(delegate)
|
||||
return None
|
||||
|
||||
@gen.coroutine
|
||||
def _read_fixed_body(self, content_length, delegate):
|
||||
while content_length > 0:
|
||||
body = yield self.stream.read_bytes(
|
||||
min(self.params.chunk_size, content_length), partial=True)
|
||||
content_length -= len(body)
|
||||
if not self._write_finished or self.is_client:
|
||||
with _ExceptionLoggingContext(app_log):
|
||||
yield gen.maybe_future(delegate.data_received(body))
|
||||
|
||||
@gen.coroutine
|
||||
def _read_chunked_body(self, delegate):
|
||||
# TODO: "chunk extensions" http://tools.ietf.org/html/rfc2616#section-3.6.1
|
||||
total_size = 0
|
||||
while True:
|
||||
chunk_len = yield self.stream.read_until(b"\r\n", max_bytes=64)
|
||||
chunk_len = int(chunk_len.strip(), 16)
|
||||
if chunk_len == 0:
|
||||
return
|
||||
total_size += chunk_len
|
||||
if total_size > self._max_body_size:
|
||||
raise httputil.HTTPInputException("chunked body too large")
|
||||
bytes_to_read = chunk_len
|
||||
while bytes_to_read:
|
||||
chunk = yield self.stream.read_bytes(
|
||||
min(bytes_to_read, self.params.chunk_size), partial=True)
|
||||
bytes_to_read -= len(chunk)
|
||||
if not self._write_finished or self.is_client:
|
||||
with _ExceptionLoggingContext(app_log):
|
||||
yield gen.maybe_future(delegate.data_received(chunk))
|
||||
# chunk ends with \r\n
|
||||
crlf = yield self.stream.read_bytes(2)
|
||||
assert crlf == b"\r\n"
|
||||
|
||||
@gen.coroutine
|
||||
def _read_body_until_close(self, delegate):
|
||||
body = yield self.stream.read_until_close()
|
||||
if not self._write_finished or self.is_client:
|
||||
with _ExceptionLoggingContext(app_log):
|
||||
delegate.data_received(body)
|
||||
|
||||
|
||||
class _GzipMessageDelegate(httputil.HTTPMessageDelegate):
|
||||
"""Wraps an `HTTPMessageDelegate` to decode ``Content-Encoding: gzip``.
|
||||
"""
|
||||
def __init__(self, delegate, chunk_size):
|
||||
self._delegate = delegate
|
||||
self._chunk_size = chunk_size
|
||||
self._decompressor = None
|
||||
|
||||
def headers_received(self, start_line, headers):
|
||||
if headers.get("Content-Encoding") == "gzip":
|
||||
self._decompressor = GzipDecompressor()
|
||||
# Downstream delegates will only see uncompressed data,
|
||||
# so rename the content-encoding header.
|
||||
# (but note that curl_httpclient doesn't do this).
|
||||
headers.add("X-Consumed-Content-Encoding",
|
||||
headers["Content-Encoding"])
|
||||
del headers["Content-Encoding"]
|
||||
return self._delegate.headers_received(start_line, headers)
|
||||
|
||||
@gen.coroutine
|
||||
def data_received(self, chunk):
|
||||
if self._decompressor:
|
||||
compressed_data = chunk
|
||||
while compressed_data:
|
||||
decompressed = self._decompressor.decompress(
|
||||
compressed_data, self._chunk_size)
|
||||
if decompressed:
|
||||
yield gen.maybe_future(
|
||||
self._delegate.data_received(decompressed))
|
||||
compressed_data = self._decompressor.unconsumed_tail
|
||||
else:
|
||||
yield gen.maybe_future(self._delegate.data_received(chunk))
|
||||
|
||||
def finish(self):
|
||||
if self._decompressor is not None:
|
||||
tail = self._decompressor.flush()
|
||||
if tail:
|
||||
# I believe the tail will always be empty (i.e.
|
||||
# decompress will return all it can). The purpose
|
||||
# of the flush call is to detect errors such
|
||||
# as truncated input. But in case it ever returns
|
||||
# anything, treat it as an extra chunk
|
||||
self._delegate.data_received(tail)
|
||||
return self._delegate.finish()
|
||||
|
||||
|
||||
class HTTP1ServerConnection(object):
|
||||
"""An HTTP/1.x server."""
|
||||
def __init__(self, stream, params=None, context=None):
|
||||
"""
|
||||
:arg stream: an `.IOStream`
|
||||
:arg params: a `.HTTP1ConnectionParameters` or None
|
||||
:arg context: an opaque application-defined object that is accessible
|
||||
as ``connection.context``
|
||||
"""
|
||||
self.stream = stream
|
||||
if params is None:
|
||||
params = HTTP1ConnectionParameters()
|
||||
self.params = params
|
||||
self.context = context
|
||||
self._serving_future = None
|
||||
|
||||
@gen.coroutine
|
||||
def close(self):
|
||||
"""Closes the connection.
|
||||
|
||||
Returns a `.Future` that resolves after the serving loop has exited.
|
||||
"""
|
||||
self.stream.close()
|
||||
# Block until the serving loop is done, but ignore any exceptions
|
||||
# (start_serving is already responsible for logging them).
|
||||
try:
|
||||
yield self._serving_future
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def start_serving(self, delegate):
|
||||
"""Starts serving requests on this connection.
|
||||
|
||||
:arg delegate: a `.HTTPServerConnectionDelegate`
|
||||
"""
|
||||
assert isinstance(delegate, httputil.HTTPServerConnectionDelegate)
|
||||
self._serving_future = self._server_request_loop(delegate)
|
||||
# Register the future on the IOLoop so its errors get logged.
|
||||
self.stream.io_loop.add_future(self._serving_future,
|
||||
lambda f: f.result())
|
||||
|
||||
@gen.coroutine
|
||||
def _server_request_loop(self, delegate):
|
||||
try:
|
||||
while True:
|
||||
conn = HTTP1Connection(self.stream, False,
|
||||
self.params, self.context)
|
||||
request_delegate = delegate.start_request(self, conn)
|
||||
try:
|
||||
ret = yield conn.read_response(request_delegate)
|
||||
except (iostream.StreamClosedError,
|
||||
iostream.UnsatisfiableReadError):
|
||||
return
|
||||
except _QuietException:
|
||||
# This exception was already logged.
|
||||
conn.close()
|
||||
return
|
||||
except Exception:
|
||||
gen_log.error("Uncaught exception", exc_info=True)
|
||||
conn.close()
|
||||
return
|
||||
if not ret:
|
||||
return
|
||||
yield gen.moment
|
||||
finally:
|
||||
delegate.on_close(self)
|
||||
@@ -25,6 +25,11 @@ to switch to ``curl_httpclient`` for reasons such as the following:
|
||||
Note that if you are using ``curl_httpclient``, it is highly recommended that
|
||||
you use a recent version of ``libcurl`` and ``pycurl``. Currently the minimum
|
||||
supported version is 7.18.2, and the recommended version is 7.21.1 or newer.
|
||||
It is highly recommended that your ``libcurl`` installation is built with
|
||||
asynchronous DNS resolver (threaded or c-ares), otherwise you may encounter
|
||||
various problems with request timeouts (for more information, see
|
||||
http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTCONNECTTIMEOUTMS
|
||||
and comments in curl_httpclient.py).
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function, with_statement
|
||||
@@ -34,7 +39,7 @@ import time
|
||||
import weakref
|
||||
|
||||
from tornado.concurrent import TracebackFuture
|
||||
from tornado.escape import utf8
|
||||
from tornado.escape import utf8, native_str
|
||||
from tornado import httputil, stack_context
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.util import Configurable
|
||||
@@ -166,7 +171,7 @@ class AsyncHTTPClient(Configurable):
|
||||
kwargs: ``HTTPRequest(request, **kwargs)``
|
||||
|
||||
This method returns a `.Future` whose result is an
|
||||
`HTTPResponse`. The ``Future`` wil raise an `HTTPError` if
|
||||
`HTTPResponse`. The ``Future`` will raise an `HTTPError` if
|
||||
the request returned a non-200 response code.
|
||||
|
||||
If a ``callback`` is given, it will be invoked with the `HTTPResponse`.
|
||||
@@ -259,14 +264,27 @@ class HTTPRequest(object):
|
||||
proxy_password=None, allow_nonstandard_methods=None,
|
||||
validate_cert=None, ca_certs=None,
|
||||
allow_ipv6=None,
|
||||
client_key=None, client_cert=None):
|
||||
client_key=None, client_cert=None, body_producer=None,
|
||||
expect_100_continue=False):
|
||||
r"""All parameters except ``url`` are optional.
|
||||
|
||||
:arg string url: URL to fetch
|
||||
:arg string method: HTTP method, e.g. "GET" or "POST"
|
||||
:arg headers: Additional HTTP headers to pass on the request
|
||||
:arg body: HTTP body to pass on the request
|
||||
:type headers: `~tornado.httputil.HTTPHeaders` or `dict`
|
||||
:arg body: HTTP request body as a string (byte or unicode; if unicode
|
||||
the utf-8 encoding will be used)
|
||||
:arg body_producer: Callable used for lazy/asynchronous request bodies.
|
||||
It is called with one argument, a ``write`` function, and should
|
||||
return a `.Future`. It should call the write function with new
|
||||
data as it becomes available. The write function returns a
|
||||
`.Future` which can be used for flow control.
|
||||
Only one of ``body`` and ``body_producer`` may
|
||||
be specified. ``body_producer`` is not supported on
|
||||
``curl_httpclient``. When using ``body_producer`` it is recommended
|
||||
to pass a ``Content-Length`` in the headers as otherwise chunked
|
||||
encoding will be used, and many servers do not support chunked
|
||||
encoding on requests. New in Tornado 3.3
|
||||
:arg string auth_username: Username for HTTP authentication
|
||||
:arg string auth_password: Password for HTTP authentication
|
||||
:arg string auth_mode: Authentication mode; default is "basic".
|
||||
@@ -319,6 +337,11 @@ class HTTPRequest(object):
|
||||
note below when used with ``curl_httpclient``.
|
||||
:arg string client_cert: Filename for client SSL certificate, if any.
|
||||
See note below when used with ``curl_httpclient``.
|
||||
:arg bool expect_100_continue: If true, send the
|
||||
``Expect: 100-continue`` header and wait for a continue response
|
||||
before sending the request body. Only supported with
|
||||
simple_httpclient.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -334,6 +357,9 @@ class HTTPRequest(object):
|
||||
|
||||
.. versionadded:: 3.1
|
||||
The ``auth_mode`` argument.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
The ``body_producer`` and ``expect_100_continue`` arguments.
|
||||
"""
|
||||
# Note that some of these attributes go through property setters
|
||||
# defined below.
|
||||
@@ -348,6 +374,7 @@ class HTTPRequest(object):
|
||||
self.url = url
|
||||
self.method = method
|
||||
self.body = body
|
||||
self.body_producer = body_producer
|
||||
self.auth_username = auth_username
|
||||
self.auth_password = auth_password
|
||||
self.auth_mode = auth_mode
|
||||
@@ -367,6 +394,7 @@ class HTTPRequest(object):
|
||||
self.allow_ipv6 = allow_ipv6
|
||||
self.client_key = client_key
|
||||
self.client_cert = client_cert
|
||||
self.expect_100_continue = expect_100_continue
|
||||
self.start_time = time.time()
|
||||
|
||||
@property
|
||||
@@ -388,6 +416,14 @@ class HTTPRequest(object):
|
||||
def body(self, value):
|
||||
self._body = utf8(value)
|
||||
|
||||
@property
|
||||
def body_producer(self):
|
||||
return self._body_producer
|
||||
|
||||
@body_producer.setter
|
||||
def body_producer(self, value):
|
||||
self._body_producer = stack_context.wrap(value)
|
||||
|
||||
@property
|
||||
def streaming_callback(self):
|
||||
return self._streaming_callback
|
||||
@@ -423,8 +459,6 @@ class HTTPResponse(object):
|
||||
* code: numeric HTTP status code, e.g. 200 or 404
|
||||
|
||||
* reason: human-readable reason phrase describing the status code
|
||||
(with curl_httpclient, this is a default value rather than the
|
||||
server's actual response)
|
||||
|
||||
* headers: `tornado.httputil.HTTPHeaders` object
|
||||
|
||||
@@ -466,7 +500,8 @@ class HTTPResponse(object):
|
||||
self.effective_url = effective_url
|
||||
if error is None:
|
||||
if self.code < 200 or self.code >= 300:
|
||||
self.error = HTTPError(self.code, response=self)
|
||||
self.error = HTTPError(self.code, message=self.reason,
|
||||
response=self)
|
||||
else:
|
||||
self.error = None
|
||||
else:
|
||||
@@ -556,7 +591,7 @@ def main():
|
||||
if options.print_headers:
|
||||
print(response.headers)
|
||||
if options.print_body:
|
||||
print(response.body)
|
||||
print(native_str(response.body))
|
||||
client.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+154
-407
@@ -20,70 +20,55 @@ Typical applications have little direct interaction with the `HTTPServer`
|
||||
class except to start a server at the beginning of the process
|
||||
(and even that is often done indirectly via `tornado.web.Application.listen`).
|
||||
|
||||
This module also defines the `HTTPRequest` class which is exposed via
|
||||
`tornado.web.RequestHandler.request`.
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The ``HTTPRequest`` class that used to live in this module has been moved
|
||||
to `tornado.httputil.HTTPServerRequest`. The old name remains as an alias.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function, with_statement
|
||||
|
||||
import socket
|
||||
import ssl
|
||||
import time
|
||||
import copy
|
||||
|
||||
from tornado.escape import native_str, parse_qs_bytes
|
||||
from tornado.escape import native_str
|
||||
from tornado.http1connection import HTTP1ServerConnection, HTTP1ConnectionParameters
|
||||
from tornado import gen
|
||||
from tornado import httputil
|
||||
from tornado import iostream
|
||||
from tornado.log import gen_log
|
||||
from tornado import netutil
|
||||
from tornado.tcpserver import TCPServer
|
||||
from tornado import stack_context
|
||||
from tornado.util import bytes_type
|
||||
|
||||
try:
|
||||
import Cookie # py2
|
||||
except ImportError:
|
||||
import http.cookies as Cookie # py3
|
||||
|
||||
|
||||
class HTTPServer(TCPServer):
|
||||
class HTTPServer(TCPServer, httputil.HTTPServerConnectionDelegate):
|
||||
r"""A non-blocking, single-threaded HTTP server.
|
||||
|
||||
A server is defined by a request callback that takes an HTTPRequest
|
||||
instance as an argument and writes a valid HTTP response with
|
||||
`HTTPRequest.write`. `HTTPRequest.finish` finishes the request (but does
|
||||
not necessarily close the connection in the case of HTTP/1.1 keep-alive
|
||||
requests). A simple example server that echoes back the URI you
|
||||
requested::
|
||||
A server is defined by either a request callback that takes a
|
||||
`.HTTPServerRequest` as an argument or a `.HTTPServerConnectionDelegate`
|
||||
instance.
|
||||
|
||||
A simple example server that echoes back the URI you requested::
|
||||
|
||||
import tornado.httpserver
|
||||
import tornado.ioloop
|
||||
|
||||
def handle_request(request):
|
||||
message = "You requested %s\n" % request.uri
|
||||
request.write("HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s" % (
|
||||
len(message), message))
|
||||
request.finish()
|
||||
request.connection.write_headers(
|
||||
httputil.ResponseStartLine('HTTP/1.1', 200, 'OK'),
|
||||
{"Content-Length": str(len(message))})
|
||||
request.connection.write(message)
|
||||
request.connection.finish()
|
||||
|
||||
http_server = tornado.httpserver.HTTPServer(handle_request)
|
||||
http_server.listen(8888)
|
||||
tornado.ioloop.IOLoop.instance().start()
|
||||
|
||||
`HTTPServer` is a very basic connection handler. It parses the request
|
||||
headers and body, but the request callback is responsible for producing
|
||||
the response exactly as it will appear on the wire. This affords
|
||||
maximum flexibility for applications to implement whatever parts
|
||||
of HTTP responses are required.
|
||||
Applications should use the methods of `.HTTPConnection` to write
|
||||
their response.
|
||||
|
||||
`HTTPServer` supports keep-alive connections by default
|
||||
(automatically for HTTP/1.1, or for HTTP/1.0 when the client
|
||||
requests ``Connection: keep-alive``). This means that the request
|
||||
callback must generate a properly-framed response, using either
|
||||
the ``Content-Length`` header or ``Transfer-Encoding: chunked``.
|
||||
Applications that are unable to frame their responses properly
|
||||
should instead return a ``Connection: close`` header in each
|
||||
response and pass ``no_keep_alive=True`` to the `HTTPServer`
|
||||
constructor.
|
||||
requests ``Connection: keep-alive``).
|
||||
|
||||
If ``xheaders`` is ``True``, we support the
|
||||
``X-Real-Ip``/``X-Forwarded-For`` and
|
||||
@@ -143,407 +128,169 @@ class HTTPServer(TCPServer):
|
||||
servers if you want to create your listening sockets in some
|
||||
way other than `tornado.netutil.bind_sockets`.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
Added ``gzip``, ``chunk_size``, ``max_header_size``,
|
||||
``idle_connection_timeout``, ``body_timeout``, ``max_body_size``
|
||||
arguments. Added support for `.HTTPServerConnectionDelegate`
|
||||
instances as ``request_callback``.
|
||||
"""
|
||||
def __init__(self, request_callback, no_keep_alive=False, io_loop=None,
|
||||
xheaders=False, ssl_options=None, protocol=None, **kwargs):
|
||||
xheaders=False, ssl_options=None, protocol=None, gzip=False,
|
||||
chunk_size=None, max_header_size=None,
|
||||
idle_connection_timeout=None, body_timeout=None,
|
||||
max_body_size=None, max_buffer_size=None):
|
||||
self.request_callback = request_callback
|
||||
self.no_keep_alive = no_keep_alive
|
||||
self.xheaders = xheaders
|
||||
self.protocol = protocol
|
||||
self.conn_params = HTTP1ConnectionParameters(
|
||||
use_gzip=gzip,
|
||||
chunk_size=chunk_size,
|
||||
max_header_size=max_header_size,
|
||||
header_timeout=idle_connection_timeout or 3600,
|
||||
max_body_size=max_body_size,
|
||||
body_timeout=body_timeout)
|
||||
TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options,
|
||||
**kwargs)
|
||||
max_buffer_size=max_buffer_size,
|
||||
read_chunk_size=chunk_size)
|
||||
self._connections = set()
|
||||
|
||||
@gen.coroutine
|
||||
def close_all_connections(self):
|
||||
while self._connections:
|
||||
# Peek at an arbitrary element of the set
|
||||
conn = next(iter(self._connections))
|
||||
yield conn.close()
|
||||
|
||||
def handle_stream(self, stream, address):
|
||||
HTTPConnection(stream, address, self.request_callback,
|
||||
self.no_keep_alive, self.xheaders, self.protocol)
|
||||
context = _HTTPRequestContext(stream, address,
|
||||
self.protocol)
|
||||
conn = HTTP1ServerConnection(
|
||||
stream, self.conn_params, context)
|
||||
self._connections.add(conn)
|
||||
conn.start_serving(self)
|
||||
|
||||
def start_request(self, server_conn, request_conn):
|
||||
return _ServerRequestAdapter(self, request_conn)
|
||||
|
||||
def on_close(self, server_conn):
|
||||
self._connections.remove(server_conn)
|
||||
|
||||
|
||||
class _BadRequestException(Exception):
|
||||
"""Exception class for malformed HTTP requests."""
|
||||
pass
|
||||
|
||||
|
||||
class HTTPConnection(object):
|
||||
"""Handles a connection to an HTTP client, executing HTTP requests.
|
||||
|
||||
We parse HTTP headers and bodies, and execute the request callback
|
||||
until the HTTP conection is closed.
|
||||
"""
|
||||
def __init__(self, stream, address, request_callback, no_keep_alive=False,
|
||||
xheaders=False, protocol=None):
|
||||
self.stream = stream
|
||||
class _HTTPRequestContext(object):
|
||||
def __init__(self, stream, address, protocol):
|
||||
self.address = address
|
||||
self.protocol = protocol
|
||||
# Save the socket's address family now so we know how to
|
||||
# interpret self.address even after the stream is closed
|
||||
# and its socket attribute replaced with None.
|
||||
self.address_family = stream.socket.family
|
||||
self.request_callback = request_callback
|
||||
self.no_keep_alive = no_keep_alive
|
||||
self.xheaders = xheaders
|
||||
self.protocol = protocol
|
||||
self._clear_request_state()
|
||||
# Save stack context here, outside of any request. This keeps
|
||||
# contexts from one request from leaking into the next.
|
||||
self._header_callback = stack_context.wrap(self._on_headers)
|
||||
self.stream.set_close_callback(self._on_connection_close)
|
||||
self.stream.read_until(b"\r\n\r\n", self._header_callback)
|
||||
|
||||
def _clear_request_state(self):
|
||||
"""Clears the per-request state.
|
||||
|
||||
This is run in between requests to allow the previous handler
|
||||
to be garbage collected (and prevent spurious close callbacks),
|
||||
and when the connection is closed (to break up cycles and
|
||||
facilitate garbage collection in cpython).
|
||||
"""
|
||||
self._request = None
|
||||
self._request_finished = False
|
||||
self._write_callback = None
|
||||
self._close_callback = None
|
||||
|
||||
def set_close_callback(self, callback):
|
||||
"""Sets a callback that will be run when the connection is closed.
|
||||
|
||||
Use this instead of accessing
|
||||
`HTTPConnection.stream.set_close_callback
|
||||
<.BaseIOStream.set_close_callback>` directly (which was the
|
||||
recommended approach prior to Tornado 3.0).
|
||||
"""
|
||||
self._close_callback = stack_context.wrap(callback)
|
||||
|
||||
def _on_connection_close(self):
|
||||
if self._close_callback is not None:
|
||||
callback = self._close_callback
|
||||
self._close_callback = None
|
||||
callback()
|
||||
# Delete any unfinished callbacks to break up reference cycles.
|
||||
self._header_callback = None
|
||||
self._clear_request_state()
|
||||
|
||||
def close(self):
|
||||
self.stream.close()
|
||||
# Remove this reference to self, which would otherwise cause a
|
||||
# cycle and delay garbage collection of this connection.
|
||||
self._header_callback = None
|
||||
self._clear_request_state()
|
||||
|
||||
def write(self, chunk, callback=None):
|
||||
"""Writes a chunk of output to the stream."""
|
||||
if not self.stream.closed():
|
||||
self._write_callback = stack_context.wrap(callback)
|
||||
self.stream.write(chunk, self._on_write_complete)
|
||||
|
||||
def finish(self):
|
||||
"""Finishes the request."""
|
||||
self._request_finished = True
|
||||
# No more data is coming, so instruct TCP to send any remaining
|
||||
# data immediately instead of waiting for a full packet or ack.
|
||||
self.stream.set_nodelay(True)
|
||||
if not self.stream.writing():
|
||||
self._finish_request()
|
||||
|
||||
def _on_write_complete(self):
|
||||
if self._write_callback is not None:
|
||||
callback = self._write_callback
|
||||
self._write_callback = None
|
||||
callback()
|
||||
# _on_write_complete is enqueued on the IOLoop whenever the
|
||||
# IOStream's write buffer becomes empty, but it's possible for
|
||||
# another callback that runs on the IOLoop before it to
|
||||
# simultaneously write more data and finish the request. If
|
||||
# there is still data in the IOStream, a future
|
||||
# _on_write_complete will be responsible for calling
|
||||
# _finish_request.
|
||||
if self._request_finished and not self.stream.writing():
|
||||
self._finish_request()
|
||||
|
||||
def _finish_request(self):
|
||||
if self.no_keep_alive or self._request is None:
|
||||
disconnect = True
|
||||
if stream.socket is not None:
|
||||
self.address_family = stream.socket.family
|
||||
else:
|
||||
connection_header = self._request.headers.get("Connection")
|
||||
if connection_header is not None:
|
||||
connection_header = connection_header.lower()
|
||||
if self._request.supports_http_1_1():
|
||||
disconnect = connection_header == "close"
|
||||
elif ("Content-Length" in self._request.headers
|
||||
or self._request.method in ("HEAD", "GET")):
|
||||
disconnect = connection_header != "keep-alive"
|
||||
else:
|
||||
disconnect = True
|
||||
self._clear_request_state()
|
||||
if disconnect:
|
||||
self.close()
|
||||
return
|
||||
try:
|
||||
# Use a try/except instead of checking stream.closed()
|
||||
# directly, because in some cases the stream doesn't discover
|
||||
# that it's closed until you try to read from it.
|
||||
self.stream.read_until(b"\r\n\r\n", self._header_callback)
|
||||
|
||||
# Turn Nagle's algorithm back on, leaving the stream in its
|
||||
# default state for the next request.
|
||||
self.stream.set_nodelay(False)
|
||||
except iostream.StreamClosedError:
|
||||
self.close()
|
||||
|
||||
def _on_headers(self, data):
|
||||
try:
|
||||
data = native_str(data.decode('latin1'))
|
||||
eol = data.find("\r\n")
|
||||
start_line = data[:eol]
|
||||
try:
|
||||
method, uri, version = start_line.split(" ")
|
||||
except ValueError:
|
||||
raise _BadRequestException("Malformed HTTP request line")
|
||||
if not version.startswith("HTTP/"):
|
||||
raise _BadRequestException("Malformed HTTP version in HTTP Request-Line")
|
||||
try:
|
||||
headers = httputil.HTTPHeaders.parse(data[eol:])
|
||||
except ValueError:
|
||||
# Probably from split() if there was no ':' in the line
|
||||
raise _BadRequestException("Malformed HTTP headers")
|
||||
|
||||
# HTTPRequest wants an IP, not a full socket address
|
||||
if self.address_family in (socket.AF_INET, socket.AF_INET6):
|
||||
remote_ip = self.address[0]
|
||||
else:
|
||||
# Unix (or other) socket; fake the remote address
|
||||
remote_ip = '0.0.0.0'
|
||||
|
||||
self._request = HTTPRequest(
|
||||
connection=self, method=method, uri=uri, version=version,
|
||||
headers=headers, remote_ip=remote_ip, protocol=self.protocol)
|
||||
|
||||
content_length = headers.get("Content-Length")
|
||||
if content_length:
|
||||
content_length = int(content_length)
|
||||
if content_length > self.stream.max_buffer_size:
|
||||
raise _BadRequestException("Content-Length too long")
|
||||
if headers.get("Expect") == "100-continue":
|
||||
self.stream.write(b"HTTP/1.1 100 (Continue)\r\n\r\n")
|
||||
self.stream.read_bytes(content_length, self._on_request_body)
|
||||
return
|
||||
|
||||
self.request_callback(self._request)
|
||||
except _BadRequestException as e:
|
||||
gen_log.info("Malformed HTTP request from %r: %s",
|
||||
self.address, e)
|
||||
self.close()
|
||||
return
|
||||
|
||||
def _on_request_body(self, data):
|
||||
self._request.body = data
|
||||
if self._request.method in ("POST", "PATCH", "PUT"):
|
||||
httputil.parse_body_arguments(
|
||||
self._request.headers.get("Content-Type", ""), data,
|
||||
self._request.body_arguments, self._request.files)
|
||||
|
||||
for k, v in self._request.body_arguments.items():
|
||||
self._request.arguments.setdefault(k, []).extend(v)
|
||||
self.request_callback(self._request)
|
||||
|
||||
|
||||
class HTTPRequest(object):
|
||||
"""A single HTTP request.
|
||||
|
||||
All attributes are type `str` unless otherwise noted.
|
||||
|
||||
.. attribute:: method
|
||||
|
||||
HTTP request method, e.g. "GET" or "POST"
|
||||
|
||||
.. attribute:: uri
|
||||
|
||||
The requested uri.
|
||||
|
||||
.. attribute:: path
|
||||
|
||||
The path portion of `uri`
|
||||
|
||||
.. attribute:: query
|
||||
|
||||
The query portion of `uri`
|
||||
|
||||
.. attribute:: version
|
||||
|
||||
HTTP version specified in request, e.g. "HTTP/1.1"
|
||||
|
||||
.. attribute:: headers
|
||||
|
||||
`.HTTPHeaders` dictionary-like object for request headers. Acts like
|
||||
a case-insensitive dictionary with additional methods for repeated
|
||||
headers.
|
||||
|
||||
.. attribute:: body
|
||||
|
||||
Request body, if present, as a byte string.
|
||||
|
||||
.. attribute:: remote_ip
|
||||
|
||||
Client's IP address as a string. If ``HTTPServer.xheaders`` is set,
|
||||
will pass along the real IP address provided by a load balancer
|
||||
in the ``X-Real-Ip`` or ``X-Forwarded-For`` header.
|
||||
|
||||
.. versionchanged:: 3.1
|
||||
The list format of ``X-Forwarded-For`` is now supported.
|
||||
|
||||
.. attribute:: protocol
|
||||
|
||||
The protocol used, either "http" or "https". If ``HTTPServer.xheaders``
|
||||
is set, will pass along the protocol used by a load balancer if
|
||||
reported via an ``X-Scheme`` header.
|
||||
|
||||
.. attribute:: host
|
||||
|
||||
The requested hostname, usually taken from the ``Host`` header.
|
||||
|
||||
.. attribute:: arguments
|
||||
|
||||
GET/POST arguments are available in the arguments property, which
|
||||
maps arguments names to lists of values (to support multiple values
|
||||
for individual names). Names are of type `str`, while arguments
|
||||
are byte strings. Note that this is different from
|
||||
`.RequestHandler.get_argument`, which returns argument values as
|
||||
unicode strings.
|
||||
|
||||
.. attribute:: query_arguments
|
||||
|
||||
Same format as ``arguments``, but contains only arguments extracted
|
||||
from the query string.
|
||||
|
||||
.. versionadded:: 3.2
|
||||
|
||||
.. attribute:: body_arguments
|
||||
|
||||
Same format as ``arguments``, but contains only arguments extracted
|
||||
from the request body.
|
||||
|
||||
.. versionadded:: 3.2
|
||||
|
||||
.. attribute:: files
|
||||
|
||||
File uploads are available in the files property, which maps file
|
||||
names to lists of `.HTTPFile`.
|
||||
|
||||
.. attribute:: connection
|
||||
|
||||
An HTTP request is attached to a single HTTP connection, which can
|
||||
be accessed through the "connection" attribute. Since connections
|
||||
are typically kept open in HTTP/1.1, multiple requests can be handled
|
||||
sequentially on a single connection.
|
||||
"""
|
||||
def __init__(self, method, uri, version="HTTP/1.0", headers=None,
|
||||
body=None, remote_ip=None, protocol=None, host=None,
|
||||
files=None, connection=None):
|
||||
self.method = method
|
||||
self.uri = uri
|
||||
self.version = version
|
||||
self.headers = headers or httputil.HTTPHeaders()
|
||||
self.body = body or ""
|
||||
|
||||
# set remote IP and protocol
|
||||
self.remote_ip = remote_ip
|
||||
self.address_family = None
|
||||
# In HTTPServerRequest we want an IP, not a full socket address.
|
||||
if (self.address_family in (socket.AF_INET, socket.AF_INET6) and
|
||||
address is not None):
|
||||
self.remote_ip = address[0]
|
||||
else:
|
||||
# Unix (or other) socket; fake the remote address.
|
||||
self.remote_ip = '0.0.0.0'
|
||||
if protocol:
|
||||
self.protocol = protocol
|
||||
elif connection and isinstance(connection.stream,
|
||||
iostream.SSLIOStream):
|
||||
elif isinstance(stream, iostream.SSLIOStream):
|
||||
self.protocol = "https"
|
||||
else:
|
||||
self.protocol = "http"
|
||||
self._orig_remote_ip = self.remote_ip
|
||||
self._orig_protocol = self.protocol
|
||||
|
||||
# xheaders can override the defaults
|
||||
if connection and connection.xheaders:
|
||||
# Squid uses X-Forwarded-For, others use X-Real-Ip
|
||||
ip = self.headers.get("X-Forwarded-For", self.remote_ip)
|
||||
ip = ip.split(',')[-1].strip()
|
||||
ip = self.headers.get(
|
||||
"X-Real-Ip", ip)
|
||||
if netutil.is_valid_ip(ip):
|
||||
self.remote_ip = ip
|
||||
# AWS uses X-Forwarded-Proto
|
||||
proto = self.headers.get(
|
||||
"X-Scheme", self.headers.get("X-Forwarded-Proto", self.protocol))
|
||||
if proto in ("http", "https"):
|
||||
self.protocol = proto
|
||||
def __str__(self):
|
||||
if self.address_family in (socket.AF_INET, socket.AF_INET6):
|
||||
return self.remote_ip
|
||||
elif isinstance(self.address, bytes):
|
||||
# Python 3 with the -bb option warns about str(bytes),
|
||||
# so convert it explicitly.
|
||||
# Unix socket addresses are str on mac but bytes on linux.
|
||||
return native_str(self.address)
|
||||
else:
|
||||
return str(self.address)
|
||||
|
||||
self.host = host or self.headers.get("Host") or "127.0.0.1"
|
||||
self.files = files or {}
|
||||
def _apply_xheaders(self, headers):
|
||||
"""Rewrite the ``remote_ip`` and ``protocol`` fields."""
|
||||
# Squid uses X-Forwarded-For, others use X-Real-Ip
|
||||
ip = headers.get("X-Forwarded-For", self.remote_ip)
|
||||
ip = ip.split(',')[-1].strip()
|
||||
ip = headers.get("X-Real-Ip", ip)
|
||||
if netutil.is_valid_ip(ip):
|
||||
self.remote_ip = ip
|
||||
# AWS uses X-Forwarded-Proto
|
||||
proto_header = headers.get(
|
||||
"X-Scheme", headers.get("X-Forwarded-Proto",
|
||||
self.protocol))
|
||||
if proto_header in ("http", "https"):
|
||||
self.protocol = proto_header
|
||||
|
||||
def _unapply_xheaders(self):
|
||||
"""Undo changes from `_apply_xheaders`.
|
||||
|
||||
Xheaders are per-request so they should not leak to the next
|
||||
request on the same connection.
|
||||
"""
|
||||
self.remote_ip = self._orig_remote_ip
|
||||
self.protocol = self._orig_protocol
|
||||
|
||||
|
||||
class _ServerRequestAdapter(httputil.HTTPMessageDelegate):
|
||||
"""Adapts the `HTTPMessageDelegate` interface to the interface expected
|
||||
by our clients.
|
||||
"""
|
||||
def __init__(self, server, connection):
|
||||
self.server = server
|
||||
self.connection = connection
|
||||
self._start_time = time.time()
|
||||
self._finish_time = None
|
||||
self.request = None
|
||||
if isinstance(server.request_callback,
|
||||
httputil.HTTPServerConnectionDelegate):
|
||||
self.delegate = server.request_callback.start_request(connection)
|
||||
self._chunks = None
|
||||
else:
|
||||
self.delegate = None
|
||||
self._chunks = []
|
||||
|
||||
self.path, sep, self.query = uri.partition('?')
|
||||
self.arguments = parse_qs_bytes(self.query, keep_blank_values=True)
|
||||
self.query_arguments = copy.deepcopy(self.arguments)
|
||||
self.body_arguments = {}
|
||||
def headers_received(self, start_line, headers):
|
||||
if self.server.xheaders:
|
||||
self.connection.context._apply_xheaders(headers)
|
||||
if self.delegate is None:
|
||||
self.request = httputil.HTTPServerRequest(
|
||||
connection=self.connection, start_line=start_line,
|
||||
headers=headers)
|
||||
else:
|
||||
return self.delegate.headers_received(start_line, headers)
|
||||
|
||||
def supports_http_1_1(self):
|
||||
"""Returns True if this request supports HTTP/1.1 semantics"""
|
||||
return self.version == "HTTP/1.1"
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
"""A dictionary of Cookie.Morsel objects."""
|
||||
if not hasattr(self, "_cookies"):
|
||||
self._cookies = Cookie.SimpleCookie()
|
||||
if "Cookie" in self.headers:
|
||||
try:
|
||||
self._cookies.load(
|
||||
native_str(self.headers["Cookie"]))
|
||||
except Exception:
|
||||
self._cookies = {}
|
||||
return self._cookies
|
||||
|
||||
def write(self, chunk, callback=None):
|
||||
"""Writes the given chunk to the response stream."""
|
||||
assert isinstance(chunk, bytes_type)
|
||||
self.connection.write(chunk, callback=callback)
|
||||
def data_received(self, chunk):
|
||||
if self.delegate is None:
|
||||
self._chunks.append(chunk)
|
||||
else:
|
||||
return self.delegate.data_received(chunk)
|
||||
|
||||
def finish(self):
|
||||
"""Finishes this HTTP request on the open connection."""
|
||||
self.connection.finish()
|
||||
self._finish_time = time.time()
|
||||
|
||||
def full_url(self):
|
||||
"""Reconstructs the full URL for this request."""
|
||||
return self.protocol + "://" + self.host + self.uri
|
||||
|
||||
def request_time(self):
|
||||
"""Returns the amount of time it took for this request to execute."""
|
||||
if self._finish_time is None:
|
||||
return time.time() - self._start_time
|
||||
if self.delegate is None:
|
||||
self.request.body = b''.join(self._chunks)
|
||||
self.request._parse_body()
|
||||
self.server.request_callback(self.request)
|
||||
else:
|
||||
return self._finish_time - self._start_time
|
||||
self.delegate.finish()
|
||||
self._cleanup()
|
||||
|
||||
def get_ssl_certificate(self, binary_form=False):
|
||||
"""Returns the client's SSL certificate, if any.
|
||||
def on_connection_close(self):
|
||||
if self.delegate is None:
|
||||
self._chunks = None
|
||||
else:
|
||||
self.delegate.on_connection_close()
|
||||
self._cleanup()
|
||||
|
||||
To use client certificates, the HTTPServer must have been constructed
|
||||
with cert_reqs set in ssl_options, e.g.::
|
||||
def _cleanup(self):
|
||||
if self.server.xheaders:
|
||||
self.connection.context._unapply_xheaders()
|
||||
|
||||
server = HTTPServer(app,
|
||||
ssl_options=dict(
|
||||
certfile="foo.crt",
|
||||
keyfile="foo.key",
|
||||
cert_reqs=ssl.CERT_REQUIRED,
|
||||
ca_certs="cacert.crt"))
|
||||
|
||||
By default, the return value is a dictionary (or None, if no
|
||||
client certificate is present). If ``binary_form`` is true, a
|
||||
DER-encoded form of the certificate is returned instead. See
|
||||
SSLSocket.getpeercert() in the standard library for more
|
||||
details.
|
||||
http://docs.python.org/library/ssl.html#sslsocket-objects
|
||||
"""
|
||||
try:
|
||||
return self.connection.stream.socket.getpeercert(
|
||||
binary_form=binary_form)
|
||||
except ssl.SSLError:
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
attrs = ("protocol", "host", "method", "uri", "version", "remote_ip")
|
||||
args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs])
|
||||
return "%s(%s, headers=%s)" % (
|
||||
self.__class__.__name__, args, dict(self.headers))
|
||||
HTTPRequest = httputil.HTTPServerRequest
|
||||
|
||||
+398
-3
@@ -14,20 +14,31 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""HTTP utility code shared by clients and servers."""
|
||||
"""HTTP utility code shared by clients and servers.
|
||||
|
||||
This module also defines the `HTTPServerRequest` class which is exposed
|
||||
via `tornado.web.RequestHandler.request`.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function, with_statement
|
||||
|
||||
import calendar
|
||||
import collections
|
||||
import copy
|
||||
import datetime
|
||||
import email.utils
|
||||
import numbers
|
||||
import re
|
||||
import time
|
||||
|
||||
from tornado.escape import native_str, parse_qs_bytes, utf8
|
||||
from tornado.log import gen_log
|
||||
from tornado.util import ObjectDict
|
||||
from tornado.util import ObjectDict, bytes_type
|
||||
|
||||
try:
|
||||
import Cookie # py2
|
||||
except ImportError:
|
||||
import http.cookies as Cookie # py3
|
||||
|
||||
try:
|
||||
from httplib import responses # py2
|
||||
@@ -43,6 +54,13 @@ try:
|
||||
except ImportError:
|
||||
from urllib.parse import urlencode # py3
|
||||
|
||||
try:
|
||||
from ssl import SSLError
|
||||
except ImportError:
|
||||
# ssl is unavailable on app engine.
|
||||
class SSLError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class _NormalizedHeaderCache(dict):
|
||||
"""Dynamic cached mapping of header names to Http-Header-Case.
|
||||
@@ -212,6 +230,337 @@ class HTTPHeaders(dict):
|
||||
return HTTPHeaders(self)
|
||||
|
||||
|
||||
class HTTPServerRequest(object):
|
||||
"""A single HTTP request.
|
||||
|
||||
All attributes are type `str` unless otherwise noted.
|
||||
|
||||
.. attribute:: method
|
||||
|
||||
HTTP request method, e.g. "GET" or "POST"
|
||||
|
||||
.. attribute:: uri
|
||||
|
||||
The requested uri.
|
||||
|
||||
.. attribute:: path
|
||||
|
||||
The path portion of `uri`
|
||||
|
||||
.. attribute:: query
|
||||
|
||||
The query portion of `uri`
|
||||
|
||||
.. attribute:: version
|
||||
|
||||
HTTP version specified in request, e.g. "HTTP/1.1"
|
||||
|
||||
.. attribute:: headers
|
||||
|
||||
`.HTTPHeaders` dictionary-like object for request headers. Acts like
|
||||
a case-insensitive dictionary with additional methods for repeated
|
||||
headers.
|
||||
|
||||
.. attribute:: body
|
||||
|
||||
Request body, if present, as a byte string.
|
||||
|
||||
.. attribute:: remote_ip
|
||||
|
||||
Client's IP address as a string. If ``HTTPServer.xheaders`` is set,
|
||||
will pass along the real IP address provided by a load balancer
|
||||
in the ``X-Real-Ip`` or ``X-Forwarded-For`` header.
|
||||
|
||||
.. versionchanged:: 3.1
|
||||
The list format of ``X-Forwarded-For`` is now supported.
|
||||
|
||||
.. attribute:: protocol
|
||||
|
||||
The protocol used, either "http" or "https". If ``HTTPServer.xheaders``
|
||||
is set, will pass along the protocol used by a load balancer if
|
||||
reported via an ``X-Scheme`` header.
|
||||
|
||||
.. attribute:: host
|
||||
|
||||
The requested hostname, usually taken from the ``Host`` header.
|
||||
|
||||
.. attribute:: arguments
|
||||
|
||||
GET/POST arguments are available in the arguments property, which
|
||||
maps arguments names to lists of values (to support multiple values
|
||||
for individual names). Names are of type `str`, while arguments
|
||||
are byte strings. Note that this is different from
|
||||
`.RequestHandler.get_argument`, which returns argument values as
|
||||
unicode strings.
|
||||
|
||||
.. attribute:: query_arguments
|
||||
|
||||
Same format as ``arguments``, but contains only arguments extracted
|
||||
from the query string.
|
||||
|
||||
.. versionadded:: 3.2
|
||||
|
||||
.. attribute:: body_arguments
|
||||
|
||||
Same format as ``arguments``, but contains only arguments extracted
|
||||
from the request body.
|
||||
|
||||
.. versionadded:: 3.2
|
||||
|
||||
.. attribute:: files
|
||||
|
||||
File uploads are available in the files property, which maps file
|
||||
names to lists of `.HTTPFile`.
|
||||
|
||||
.. attribute:: connection
|
||||
|
||||
An HTTP request is attached to a single HTTP connection, which can
|
||||
be accessed through the "connection" attribute. Since connections
|
||||
are typically kept open in HTTP/1.1, multiple requests can be handled
|
||||
sequentially on a single connection.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
Moved from ``tornado.httpserver.HTTPRequest``.
|
||||
"""
|
||||
def __init__(self, method=None, uri=None, version="HTTP/1.0", headers=None,
|
||||
body=None, host=None, files=None, connection=None,
|
||||
start_line=None):
|
||||
if start_line is not None:
|
||||
method, uri, version = start_line
|
||||
self.method = method
|
||||
self.uri = uri
|
||||
self.version = version
|
||||
self.headers = headers or HTTPHeaders()
|
||||
self.body = body or ""
|
||||
|
||||
# set remote IP and protocol
|
||||
context = getattr(connection, 'context', None)
|
||||
self.remote_ip = getattr(context, 'remote_ip')
|
||||
self.protocol = getattr(context, 'protocol', "http")
|
||||
|
||||
self.host = host or self.headers.get("Host") or "127.0.0.1"
|
||||
self.files = files or {}
|
||||
self.connection = connection
|
||||
self._start_time = time.time()
|
||||
self._finish_time = None
|
||||
|
||||
self.path, sep, self.query = uri.partition('?')
|
||||
self.arguments = parse_qs_bytes(self.query, keep_blank_values=True)
|
||||
self.query_arguments = copy.deepcopy(self.arguments)
|
||||
self.body_arguments = {}
|
||||
|
||||
def supports_http_1_1(self):
|
||||
"""Returns True if this request supports HTTP/1.1 semantics.
|
||||
|
||||
.. deprecated:: 3.3
|
||||
Applications are less likely to need this information with the
|
||||
introduction of `.HTTPConnection`. If you still need it, access
|
||||
the ``version`` attribute directly.
|
||||
"""
|
||||
return self.version == "HTTP/1.1"
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
"""A dictionary of Cookie.Morsel objects."""
|
||||
if not hasattr(self, "_cookies"):
|
||||
self._cookies = Cookie.SimpleCookie()
|
||||
if "Cookie" in self.headers:
|
||||
try:
|
||||
self._cookies.load(
|
||||
native_str(self.headers["Cookie"]))
|
||||
except Exception:
|
||||
self._cookies = {}
|
||||
return self._cookies
|
||||
|
||||
def write(self, chunk, callback=None):
|
||||
"""Writes the given chunk to the response stream.
|
||||
|
||||
.. deprecated:: 3.3
|
||||
Use ``request.connection`` and the `.HTTPConnection` methods
|
||||
to write the response.
|
||||
"""
|
||||
assert isinstance(chunk, bytes_type)
|
||||
self.connection.write(chunk, callback=callback)
|
||||
|
||||
def finish(self):
|
||||
"""Finishes this HTTP request on the open connection.
|
||||
|
||||
.. deprecated:: 3.3
|
||||
Use ``request.connection`` and the `.HTTPConnection` methods
|
||||
to write the response.
|
||||
"""
|
||||
self.connection.finish()
|
||||
self._finish_time = time.time()
|
||||
|
||||
def full_url(self):
|
||||
"""Reconstructs the full URL for this request."""
|
||||
return self.protocol + "://" + self.host + self.uri
|
||||
|
||||
def request_time(self):
|
||||
"""Returns the amount of time it took for this request to execute."""
|
||||
if self._finish_time is None:
|
||||
return time.time() - self._start_time
|
||||
else:
|
||||
return self._finish_time - self._start_time
|
||||
|
||||
def get_ssl_certificate(self, binary_form=False):
|
||||
"""Returns the client's SSL certificate, if any.
|
||||
|
||||
To use client certificates, the HTTPServer must have been constructed
|
||||
with cert_reqs set in ssl_options, e.g.::
|
||||
|
||||
server = HTTPServer(app,
|
||||
ssl_options=dict(
|
||||
certfile="foo.crt",
|
||||
keyfile="foo.key",
|
||||
cert_reqs=ssl.CERT_REQUIRED,
|
||||
ca_certs="cacert.crt"))
|
||||
|
||||
By default, the return value is a dictionary (or None, if no
|
||||
client certificate is present). If ``binary_form`` is true, a
|
||||
DER-encoded form of the certificate is returned instead. See
|
||||
SSLSocket.getpeercert() in the standard library for more
|
||||
details.
|
||||
http://docs.python.org/library/ssl.html#sslsocket-objects
|
||||
"""
|
||||
try:
|
||||
return self.connection.stream.socket.getpeercert(
|
||||
binary_form=binary_form)
|
||||
except SSLError:
|
||||
return None
|
||||
|
||||
def _parse_body(self):
|
||||
parse_body_arguments(
|
||||
self.headers.get("Content-Type", ""), self.body,
|
||||
self.body_arguments, self.files,
|
||||
self.headers)
|
||||
|
||||
for k, v in self.body_arguments.items():
|
||||
self.arguments.setdefault(k, []).extend(v)
|
||||
|
||||
def __repr__(self):
|
||||
attrs = ("protocol", "host", "method", "uri", "version", "remote_ip")
|
||||
args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs])
|
||||
return "%s(%s, headers=%s)" % (
|
||||
self.__class__.__name__, args, dict(self.headers))
|
||||
|
||||
|
||||
class HTTPInputException(Exception):
|
||||
"""Exception class for malformed HTTP requests or responses
|
||||
from remote sources.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class HTTPOutputException(Exception):
|
||||
"""Exception class for errors in HTTP output.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class HTTPServerConnectionDelegate(object):
|
||||
"""Implement this interface to handle requests from `.HTTPServer`.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
"""
|
||||
def start_request(self, server_conn, request_conn):
|
||||
"""This method is called by the server when a new request has started.
|
||||
|
||||
:arg server_conn: is an opaque object representing the long-lived
|
||||
(e.g. tcp-level) connection.
|
||||
:arg request_conn: is a `.HTTPConnection` object for a single
|
||||
request/response exchange.
|
||||
|
||||
This method should return a `.HTTPMessageDelegate`.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def on_close(self, server_conn):
|
||||
"""This method is called when a connection has been closed.
|
||||
|
||||
:arg server_conn: is a server connection that has previously been
|
||||
passed to ``start_request``.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class HTTPMessageDelegate(object):
|
||||
"""Implement this interface to handle an HTTP request or response.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
"""
|
||||
def headers_received(self, start_line, headers):
|
||||
"""Called when the HTTP headers have been received and parsed.
|
||||
|
||||
:arg start_line: a `.RequestStartLine` or `.ResponseStartLine`
|
||||
depending on whether this is a client or server message.
|
||||
:arg headers: a `.HTTPHeaders` instance.
|
||||
|
||||
Some `.HTTPConnection` methods can only be called during
|
||||
``headers_received``.
|
||||
|
||||
May return a `.Future`; if it does the body will not be read
|
||||
until it is done.
|
||||
"""
|
||||
pass
|
||||
|
||||
def data_received(self, chunk):
|
||||
"""Called when a chunk of data has been received.
|
||||
|
||||
May return a `.Future` for flow control.
|
||||
"""
|
||||
pass
|
||||
|
||||
def finish(self):
|
||||
"""Called after the last chunk of data has been received."""
|
||||
pass
|
||||
|
||||
def on_connection_close(self):
|
||||
"""Called if the connection is closed without finishing the request.
|
||||
|
||||
If ``headers_received`` is called, either ``finish`` or
|
||||
``on_connection_close`` will be called, but not both.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class HTTPConnection(object):
|
||||
"""Applications use this interface to write their responses.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
"""
|
||||
def write_headers(self, start_line, headers, chunk=None, callback=None):
|
||||
"""Write an HTTP header block.
|
||||
|
||||
:arg start_line: a `.RequestStartLine` or `.ResponseStartLine`.
|
||||
:arg headers: a `.HTTPHeaders` instance.
|
||||
:arg chunk: the first (optional) chunk of data. This is an optimization
|
||||
so that small responses can be written in the same call as their
|
||||
headers.
|
||||
:arg callback: a callback to be run when the write is complete.
|
||||
|
||||
Returns a `.Future` if no callback is given.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def write(self, chunk, callback=None):
|
||||
"""Writes a chunk of body data.
|
||||
|
||||
The callback will be run when the write is complete. If no callback
|
||||
is given, returns a Future.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def finish(self):
|
||||
"""Indicates that the last body data has been written.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def url_concat(url, args):
|
||||
"""Concatenate url and argument dictionary regardless of whether
|
||||
url has existing query parameters.
|
||||
@@ -310,7 +659,7 @@ def _int_or_none(val):
|
||||
return int(val)
|
||||
|
||||
|
||||
def parse_body_arguments(content_type, body, arguments, files):
|
||||
def parse_body_arguments(content_type, body, arguments, files, headers=None):
|
||||
"""Parses a form request body.
|
||||
|
||||
Supports ``application/x-www-form-urlencoded`` and
|
||||
@@ -319,6 +668,10 @@ def parse_body_arguments(content_type, body, arguments, files):
|
||||
and ``files`` parameters are dictionaries that will be updated
|
||||
with the parsed contents.
|
||||
"""
|
||||
if headers and 'Content-Encoding' in headers:
|
||||
gen_log.warning("Unsupported Content-Encoding: %s",
|
||||
headers['Content-Encoding'])
|
||||
return
|
||||
if content_type.startswith("application/x-www-form-urlencoded"):
|
||||
try:
|
||||
uri_arguments = parse_qs_bytes(native_str(body), keep_blank_values=True)
|
||||
@@ -405,6 +758,48 @@ def format_timestamp(ts):
|
||||
raise TypeError("unknown timestamp type: %r" % ts)
|
||||
return email.utils.formatdate(ts, usegmt=True)
|
||||
|
||||
|
||||
RequestStartLine = collections.namedtuple(
|
||||
'RequestStartLine', ['method', 'path', 'version'])
|
||||
|
||||
|
||||
def parse_request_start_line(line):
|
||||
"""Returns a (method, path, version) tuple for an HTTP 1.x request line.
|
||||
|
||||
The response is a `collections.namedtuple`.
|
||||
|
||||
>>> parse_request_start_line("GET /foo HTTP/1.1")
|
||||
RequestStartLine(method='GET', path='/foo', version='HTTP/1.1')
|
||||
"""
|
||||
try:
|
||||
method, path, version = line.split(" ")
|
||||
except ValueError:
|
||||
raise HTTPInputException("Malformed HTTP request line")
|
||||
if not version.startswith("HTTP/"):
|
||||
raise HTTPInputException(
|
||||
"Malformed HTTP version in HTTP Request-Line: %r" % version)
|
||||
return RequestStartLine(method, path, version)
|
||||
|
||||
|
||||
ResponseStartLine = collections.namedtuple(
|
||||
'ResponseStartLine', ['version', 'code', 'reason'])
|
||||
|
||||
|
||||
def parse_response_start_line(line):
|
||||
"""Returns a (version, code, reason) tuple for an HTTP 1.x response line.
|
||||
|
||||
The response is a `collections.namedtuple`.
|
||||
|
||||
>>> parse_response_start_line("HTTP/1.1 200 OK")
|
||||
ResponseStartLine(version='HTTP/1.1', code=200, reason='OK')
|
||||
"""
|
||||
line = native_str(line)
|
||||
match = re.match("(HTTP/1.[01]) ([0-9]+) ([^\r]*)", line)
|
||||
if not match:
|
||||
raise HTTPInputException("Error parsing response start line")
|
||||
return ResponseStartLine(match.group(1), int(match.group(2)),
|
||||
match.group(3))
|
||||
|
||||
# _parseparam and _parse_header are copied and modified from python2.7's cgi.py
|
||||
# The original 2.7 version of this code did not correctly support some
|
||||
# combinations of semicolons and double quotes.
|
||||
|
||||
+105
-28
@@ -32,6 +32,7 @@ import datetime
|
||||
import errno
|
||||
import functools
|
||||
import heapq
|
||||
import itertools
|
||||
import logging
|
||||
import numbers
|
||||
import os
|
||||
@@ -41,10 +42,11 @@ import threading
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from tornado.concurrent import Future, TracebackFuture
|
||||
from tornado.concurrent import TracebackFuture, is_future
|
||||
from tornado.log import app_log, gen_log
|
||||
from tornado import stack_context
|
||||
from tornado.util import Configurable
|
||||
from tornado.util import errno_from_exception
|
||||
|
||||
try:
|
||||
import signal
|
||||
@@ -156,6 +158,15 @@ class IOLoop(Configurable):
|
||||
assert not IOLoop.initialized()
|
||||
IOLoop._instance = self
|
||||
|
||||
@staticmethod
|
||||
def clear_instance():
|
||||
"""Clear the global `IOLoop` instance.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
"""
|
||||
if hasattr(IOLoop, "_instance"):
|
||||
del IOLoop._instance
|
||||
|
||||
@staticmethod
|
||||
def current():
|
||||
"""Returns the current thread's `IOLoop`.
|
||||
@@ -244,21 +255,40 @@ class IOLoop(Configurable):
|
||||
raise NotImplementedError()
|
||||
|
||||
def add_handler(self, fd, handler, events):
|
||||
"""Registers the given handler to receive the given events for fd.
|
||||
"""Registers the given handler to receive the given events for ``fd``.
|
||||
|
||||
The ``fd`` argument may either be an integer file descriptor or
|
||||
a file-like object with a ``fileno()`` method (and optionally a
|
||||
``close()`` method, which may be called when the `IOLoop` is shut
|
||||
down).
|
||||
|
||||
The ``events`` argument is a bitwise or of the constants
|
||||
``IOLoop.READ``, ``IOLoop.WRITE``, and ``IOLoop.ERROR``.
|
||||
|
||||
When an event occurs, ``handler(fd, events)`` will be run.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
Added the ability to pass file-like objects in addition to
|
||||
raw file descriptors.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def update_handler(self, fd, events):
|
||||
"""Changes the events we listen for fd."""
|
||||
"""Changes the events we listen for ``fd``.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
Added the ability to pass file-like objects in addition to
|
||||
raw file descriptors.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def remove_handler(self, fd):
|
||||
"""Stop listening for events on fd."""
|
||||
"""Stop listening for events on ``fd``.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
Added the ability to pass file-like objects in addition to
|
||||
raw file descriptors.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_blocking_signal_threshold(self, seconds, action):
|
||||
@@ -372,7 +402,7 @@ class IOLoop(Configurable):
|
||||
future_cell[0] = TracebackFuture()
|
||||
future_cell[0].set_exc_info(sys.exc_info())
|
||||
else:
|
||||
if isinstance(result, Future):
|
||||
if is_future(result):
|
||||
future_cell[0] = result
|
||||
else:
|
||||
future_cell[0] = TracebackFuture()
|
||||
@@ -463,7 +493,7 @@ class IOLoop(Configurable):
|
||||
The callback is invoked with one argument, the
|
||||
`.Future`.
|
||||
"""
|
||||
assert isinstance(future, Future)
|
||||
assert is_future(future)
|
||||
callback = stack_context.wrap(callback)
|
||||
future.add_done_callback(
|
||||
lambda future: self.add_callback(callback, future))
|
||||
@@ -490,6 +520,47 @@ class IOLoop(Configurable):
|
||||
"""
|
||||
app_log.error("Exception in callback %r", callback, exc_info=True)
|
||||
|
||||
def split_fd(self, fd):
|
||||
"""Returns an (fd, obj) pair from an ``fd`` parameter.
|
||||
|
||||
We accept both raw file descriptors and file-like objects as
|
||||
input to `add_handler` and related methods. When a file-like
|
||||
object is passed, we must retain the object itself so we can
|
||||
close it correctly when the `IOLoop` shuts down, but the
|
||||
poller interfaces favor file descriptors (they will accept
|
||||
file-like objects and call ``fileno()`` for you, but they
|
||||
always return the descriptor itself).
|
||||
|
||||
This method is provided for use by `IOLoop` subclasses and should
|
||||
not generally be used by application code.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
"""
|
||||
try:
|
||||
return fd.fileno(), fd
|
||||
except AttributeError:
|
||||
return fd, fd
|
||||
|
||||
def close_fd(self, fd):
|
||||
"""Utility method to close an ``fd``.
|
||||
|
||||
If ``fd`` is a file-like object, we close it directly; otherwise
|
||||
we use `os.close`.
|
||||
|
||||
This method is provided for use by `IOLoop` subclasses (in
|
||||
implementations of ``IOLoop.close(all_fds=True)`` and should
|
||||
not generally be used by application code.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
fd.close()
|
||||
except AttributeError:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
class PollIOLoop(IOLoop):
|
||||
"""Base class for IOLoops built around a select-like function.
|
||||
@@ -515,7 +586,8 @@ class PollIOLoop(IOLoop):
|
||||
self._closing = False
|
||||
self._thread_ident = None
|
||||
self._blocking_signal_threshold = None
|
||||
|
||||
self._timeout_counter = itertools.count()
|
||||
|
||||
# Create a pipe that we send bogus data to when we want to wake
|
||||
# the I/O loop when it is idle
|
||||
self._waker = Waker()
|
||||
@@ -528,26 +600,24 @@ class PollIOLoop(IOLoop):
|
||||
self._closing = True
|
||||
self.remove_handler(self._waker.fileno())
|
||||
if all_fds:
|
||||
for fd in self._handlers.keys():
|
||||
try:
|
||||
close_method = getattr(fd, 'close', None)
|
||||
if close_method is not None:
|
||||
close_method()
|
||||
else:
|
||||
os.close(fd)
|
||||
except Exception:
|
||||
gen_log.debug("error closing fd %s", fd, exc_info=True)
|
||||
for fd, handler in self._handlers.values():
|
||||
self.close_fd(fd)
|
||||
self._waker.close()
|
||||
self._impl.close()
|
||||
self._callbacks = None
|
||||
self._timeouts = None
|
||||
|
||||
def add_handler(self, fd, handler, events):
|
||||
self._handlers[fd] = stack_context.wrap(handler)
|
||||
fd, obj = self.split_fd(fd)
|
||||
self._handlers[fd] = (obj, stack_context.wrap(handler))
|
||||
self._impl.register(fd, events | self.ERROR)
|
||||
|
||||
def update_handler(self, fd, events):
|
||||
fd, obj = self.split_fd(fd)
|
||||
self._impl.modify(fd, events | self.ERROR)
|
||||
|
||||
def remove_handler(self, fd):
|
||||
fd, obj = self.split_fd(fd)
|
||||
self._handlers.pop(fd, None)
|
||||
self._events.pop(fd, None)
|
||||
try:
|
||||
@@ -566,6 +636,8 @@ class PollIOLoop(IOLoop):
|
||||
action if action is not None else signal.SIG_DFL)
|
||||
|
||||
def start(self):
|
||||
if self._running:
|
||||
raise RuntimeError("IOLoop is already running")
|
||||
self._setup_logging()
|
||||
if self._stopped:
|
||||
self._stopped = False
|
||||
@@ -666,9 +738,7 @@ class PollIOLoop(IOLoop):
|
||||
# two ways EINTR might be signaled:
|
||||
# * e.errno == errno.EINTR
|
||||
# * e.args is like (errno.EINTR, 'Interrupted system call')
|
||||
if (getattr(e, 'errno', None) == errno.EINTR or
|
||||
(isinstance(getattr(e, 'args', None), tuple) and
|
||||
len(e.args) == 2 and e.args[0] == errno.EINTR)):
|
||||
if errno_from_exception(e) == errno.EINTR:
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
@@ -685,15 +755,17 @@ class PollIOLoop(IOLoop):
|
||||
while self._events:
|
||||
fd, events = self._events.popitem()
|
||||
try:
|
||||
self._handlers[fd](fd, events)
|
||||
fd_obj, handler_func = self._handlers[fd]
|
||||
handler_func(fd_obj, events)
|
||||
except (OSError, IOError) as e:
|
||||
if e.args[0] == errno.EPIPE:
|
||||
if errno_from_exception(e) == errno.EPIPE:
|
||||
# Happens when the client closes the connection
|
||||
pass
|
||||
else:
|
||||
self.handle_callback_exception(self._handlers.get(fd))
|
||||
except Exception:
|
||||
self.handle_callback_exception(self._handlers.get(fd))
|
||||
fd_obj = handler_func = None
|
||||
|
||||
finally:
|
||||
# reset the stopped flag so another start/stop pair can be issued
|
||||
@@ -765,16 +837,21 @@ class _Timeout(object):
|
||||
"""An IOLoop timeout, a UNIX timestamp and a callback"""
|
||||
|
||||
# Reduce memory overhead when there are lots of pending callbacks
|
||||
__slots__ = ['deadline', 'callback']
|
||||
__slots__ = ['deadline', 'callback', 'tiebreaker']
|
||||
|
||||
def __init__(self, deadline, callback, io_loop):
|
||||
if isinstance(deadline, numbers.Real):
|
||||
self.deadline = deadline
|
||||
elif isinstance(deadline, datetime.timedelta):
|
||||
self.deadline = io_loop.time() + _Timeout.timedelta_to_seconds(deadline)
|
||||
now = io_loop.time()
|
||||
try:
|
||||
self.deadline = now + deadline.total_seconds()
|
||||
except AttributeError: # py2.6
|
||||
self.deadline = now + _Timeout.timedelta_to_seconds(deadline)
|
||||
else:
|
||||
raise TypeError("Unsupported deadline %r" % deadline)
|
||||
self.callback = callback
|
||||
self.tiebreaker = next(io_loop._timeout_counter)
|
||||
|
||||
@staticmethod
|
||||
def timedelta_to_seconds(td):
|
||||
@@ -786,12 +863,12 @@ class _Timeout(object):
|
||||
# in python2.5, and __lt__ in 2.6+ (sort() and most other comparisons
|
||||
# use __lt__).
|
||||
def __lt__(self, other):
|
||||
return ((self.deadline, id(self)) <
|
||||
(other.deadline, id(other)))
|
||||
return ((self.deadline, self.tiebreaker) <
|
||||
(other.deadline, other.tiebreaker))
|
||||
|
||||
def __le__(self, other):
|
||||
return ((self.deadline, id(self)) <=
|
||||
(other.deadline, id(other)))
|
||||
return ((self.deadline, self.tiebreaker) <=
|
||||
(other.deadline, other.tiebreaker))
|
||||
|
||||
|
||||
class PeriodicCallback(object):
|
||||
|
||||
+477
-147
@@ -31,21 +31,27 @@ import errno
|
||||
import numbers
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
import re
|
||||
|
||||
from tornado.concurrent import TracebackFuture
|
||||
from tornado import ioloop
|
||||
from tornado.log import gen_log, app_log
|
||||
from tornado.netutil import ssl_wrap_socket, ssl_match_hostname, SSLCertificateError
|
||||
from tornado import stack_context
|
||||
from tornado.util import bytes_type
|
||||
from tornado.util import bytes_type, errno_from_exception
|
||||
|
||||
try:
|
||||
from tornado.platform.posix import _set_nonblocking
|
||||
except ImportError:
|
||||
_set_nonblocking = None
|
||||
|
||||
try:
|
||||
import ssl
|
||||
except ImportError:
|
||||
# ssl is not available on Google App Engine
|
||||
ssl = None
|
||||
|
||||
# These errnos indicate that a non-blocking operation must be retried
|
||||
# at a later time. On most platforms they're the same value, but on
|
||||
# some they differ.
|
||||
@@ -66,12 +72,31 @@ class StreamClosedError(IOError):
|
||||
pass
|
||||
|
||||
|
||||
class UnsatisfiableReadError(Exception):
|
||||
"""Exception raised when a read cannot be satisfied.
|
||||
|
||||
Raised by ``read_until`` and ``read_until_regex`` with a ``max_bytes``
|
||||
argument.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class StreamBufferFullError(Exception):
|
||||
"""Exception raised by `IOStream` methods when the buffer is full.
|
||||
"""
|
||||
|
||||
|
||||
class BaseIOStream(object):
|
||||
"""A utility class to write to and read from a non-blocking file or socket.
|
||||
|
||||
We support a non-blocking ``write()`` and a family of ``read_*()`` methods.
|
||||
All of the methods take callbacks (since writing and reading are
|
||||
non-blocking and asynchronous).
|
||||
All of the methods take an optional ``callback`` argument and return a
|
||||
`.Future` only if no callback is given. When the operation completes,
|
||||
the callback will be run or the `.Future` will resolve with the data
|
||||
read (or ``None`` for ``write()``). All outstanding ``Futures`` will
|
||||
resolve with a `StreamClosedError` when the stream is closed; users
|
||||
of the callback interface will be notified via
|
||||
`.BaseIOStream.set_close_callback` instead.
|
||||
|
||||
When a stream is closed due to an error, the IOStream's ``error``
|
||||
attribute contains the exception object.
|
||||
@@ -80,24 +105,48 @@ class BaseIOStream(object):
|
||||
`read_from_fd`, and optionally `get_fd_error`.
|
||||
"""
|
||||
def __init__(self, io_loop=None, max_buffer_size=None,
|
||||
read_chunk_size=4096):
|
||||
read_chunk_size=None, max_write_buffer_size=None):
|
||||
"""`BaseIOStream` constructor.
|
||||
|
||||
:arg io_loop: The `.IOLoop` to use; defaults to `.IOLoop.current`.
|
||||
:arg max_buffer_size: Maximum amount of incoming data to buffer;
|
||||
defaults to 100MB.
|
||||
:arg read_chunk_size: Amount of data to read at one time from the
|
||||
underlying transport; defaults to 64KB.
|
||||
:arg max_write_buffer_size: Amount of outgoing data to buffer;
|
||||
defaults to unlimited.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
Add the ``max_write_buffer_size`` parameter. Changed default
|
||||
``read_chunk_size`` to 64KB.
|
||||
"""
|
||||
self.io_loop = io_loop or ioloop.IOLoop.current()
|
||||
self.max_buffer_size = max_buffer_size or 104857600
|
||||
self.read_chunk_size = read_chunk_size
|
||||
# A chunk size that is too close to max_buffer_size can cause
|
||||
# spurious failures.
|
||||
self.read_chunk_size = min(read_chunk_size or 65536,
|
||||
self.max_buffer_size // 2)
|
||||
self.max_write_buffer_size = max_write_buffer_size
|
||||
self.error = None
|
||||
self._read_buffer = collections.deque()
|
||||
self._write_buffer = collections.deque()
|
||||
self._read_buffer_size = 0
|
||||
self._write_buffer_size = 0
|
||||
self._write_buffer_frozen = False
|
||||
self._read_delimiter = None
|
||||
self._read_regex = None
|
||||
self._read_max_bytes = None
|
||||
self._read_bytes = None
|
||||
self._read_partial = False
|
||||
self._read_until_close = False
|
||||
self._read_callback = None
|
||||
self._read_future = None
|
||||
self._streaming_callback = None
|
||||
self._write_callback = None
|
||||
self._write_future = None
|
||||
self._close_callback = None
|
||||
self._connect_callback = None
|
||||
self._connect_future = None
|
||||
self._connecting = False
|
||||
self._state = None
|
||||
self._pending_callbacks = 0
|
||||
@@ -142,98 +191,162 @@ class BaseIOStream(object):
|
||||
"""
|
||||
return None
|
||||
|
||||
def read_until_regex(self, regex, callback):
|
||||
"""Run ``callback`` when we read the given regex pattern.
|
||||
def read_until_regex(self, regex, callback=None, max_bytes=None):
|
||||
"""Asynchronously read until we have matched the given regex.
|
||||
|
||||
The callback will get the data read (including the data that
|
||||
matched the regex and anything that came before it) as an argument.
|
||||
The result includes the data that matches the regex and anything
|
||||
that came before it. If a callback is given, it will be run
|
||||
with the data as an argument; if not, this method returns a
|
||||
`.Future`.
|
||||
|
||||
If ``max_bytes`` is not None, the connection will be closed
|
||||
if more than ``max_bytes`` bytes have been read and the regex is
|
||||
not satisfied.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
Added the ``max_bytes`` argument. The ``callback`` argument is
|
||||
now optional and a `.Future` will be returned if it is omitted.
|
||||
"""
|
||||
self._set_read_callback(callback)
|
||||
future = self._set_read_callback(callback)
|
||||
self._read_regex = re.compile(regex)
|
||||
self._try_inline_read()
|
||||
self._read_max_bytes = max_bytes
|
||||
try:
|
||||
self._try_inline_read()
|
||||
except UnsatisfiableReadError as e:
|
||||
# Handle this the same way as in _handle_events.
|
||||
gen_log.info("Unsatisfiable read, closing connection: %s" % e)
|
||||
self.close(exc_info=True)
|
||||
return future
|
||||
return future
|
||||
|
||||
def read_until(self, delimiter, callback):
|
||||
"""Run ``callback`` when we read the given delimiter.
|
||||
def read_until(self, delimiter, callback=None, max_bytes=None):
|
||||
"""Asynchronously read until we have found the given delimiter.
|
||||
|
||||
The callback will get the data read (including the delimiter)
|
||||
as an argument.
|
||||
The result includes all the data read including the delimiter.
|
||||
If a callback is given, it will be run with the data as an argument;
|
||||
if not, this method returns a `.Future`.
|
||||
|
||||
If ``max_bytes`` is not None, the connection will be closed
|
||||
if more than ``max_bytes`` bytes have been read and the delimiter
|
||||
is not found.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
Added the ``max_bytes`` argument. The ``callback`` argument is
|
||||
now optional and a `.Future` will be returned if it is omitted.
|
||||
"""
|
||||
self._set_read_callback(callback)
|
||||
future = self._set_read_callback(callback)
|
||||
self._read_delimiter = delimiter
|
||||
self._try_inline_read()
|
||||
self._read_max_bytes = max_bytes
|
||||
try:
|
||||
self._try_inline_read()
|
||||
except UnsatisfiableReadError as e:
|
||||
# Handle this the same way as in _handle_events.
|
||||
gen_log.info("Unsatisfiable read, closing connection: %s" % e)
|
||||
self.close(exc_info=True)
|
||||
return future
|
||||
return future
|
||||
|
||||
def read_bytes(self, num_bytes, callback, streaming_callback=None):
|
||||
"""Run callback when we read the given number of bytes.
|
||||
def read_bytes(self, num_bytes, callback=None, streaming_callback=None,
|
||||
partial=False):
|
||||
"""Asynchronously read a number of bytes.
|
||||
|
||||
If a ``streaming_callback`` is given, it will be called with chunks
|
||||
of data as they become available, and the argument to the final
|
||||
``callback`` will be empty. Otherwise, the ``callback`` gets
|
||||
the data as an argument.
|
||||
of data as they become available, and the final result will be empty.
|
||||
Otherwise, the result is all the data that was read.
|
||||
If a callback is given, it will be run with the data as an argument;
|
||||
if not, this method returns a `.Future`.
|
||||
|
||||
If ``partial`` is true, the callback is run as soon as we have
|
||||
any bytes to return (but never more than ``num_bytes``)
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
Added the ``partial`` argument. The callback argument is now
|
||||
optional and a `.Future` will be returned if it is omitted.
|
||||
"""
|
||||
self._set_read_callback(callback)
|
||||
future = self._set_read_callback(callback)
|
||||
assert isinstance(num_bytes, numbers.Integral)
|
||||
self._read_bytes = num_bytes
|
||||
self._read_partial = partial
|
||||
self._streaming_callback = stack_context.wrap(streaming_callback)
|
||||
self._try_inline_read()
|
||||
return future
|
||||
|
||||
def read_until_close(self, callback, streaming_callback=None):
|
||||
"""Reads all data from the socket until it is closed.
|
||||
def read_until_close(self, callback=None, streaming_callback=None):
|
||||
"""Asynchronously reads all data from the socket until it is closed.
|
||||
|
||||
If a ``streaming_callback`` is given, it will be called with chunks
|
||||
of data as they become available, and the argument to the final
|
||||
``callback`` will be empty. Otherwise, the ``callback`` gets the
|
||||
data as an argument.
|
||||
of data as they become available, and the final result will be empty.
|
||||
Otherwise, the result is all the data that was read.
|
||||
If a callback is given, it will be run with the data as an argument;
|
||||
if not, this method returns a `.Future`.
|
||||
|
||||
Subject to ``max_buffer_size`` limit from `IOStream` constructor if
|
||||
a ``streaming_callback`` is not used.
|
||||
.. versionchanged:: 3.3
|
||||
The callback argument is now optional and a `.Future` will
|
||||
be returned if it is omitted.
|
||||
"""
|
||||
self._set_read_callback(callback)
|
||||
future = self._set_read_callback(callback)
|
||||
self._streaming_callback = stack_context.wrap(streaming_callback)
|
||||
if self.closed():
|
||||
if self._streaming_callback is not None:
|
||||
self._run_callback(self._streaming_callback,
|
||||
self._consume(self._read_buffer_size))
|
||||
self._run_callback(self._read_callback,
|
||||
self._consume(self._read_buffer_size))
|
||||
self._streaming_callback = None
|
||||
self._read_callback = None
|
||||
return
|
||||
self._run_read_callback(self._read_buffer_size, True)
|
||||
self._run_read_callback(self._read_buffer_size, False)
|
||||
return future
|
||||
self._read_until_close = True
|
||||
self._streaming_callback = stack_context.wrap(streaming_callback)
|
||||
self._try_inline_read()
|
||||
return future
|
||||
|
||||
def write(self, data, callback=None):
|
||||
"""Write the given data to this stream.
|
||||
"""Asynchronously write the given data to this stream.
|
||||
|
||||
If ``callback`` is given, we call it when all of the buffered write
|
||||
data has been successfully written to the stream. If there was
|
||||
previously buffered write data and an old write callback, that
|
||||
callback is simply overwritten with this new callback.
|
||||
|
||||
If no ``callback`` is given, this method returns a `.Future` that
|
||||
resolves (with a result of ``None``) when the write has been
|
||||
completed. If `write` is called again before that `.Future` has
|
||||
resolved, the previous future will be orphaned and will never resolve.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
Now returns a `.Future` if no callback is given.
|
||||
"""
|
||||
assert isinstance(data, bytes_type)
|
||||
self._check_closed()
|
||||
# We use bool(_write_buffer) as a proxy for write_buffer_size>0,
|
||||
# so never put empty strings in the buffer.
|
||||
if data:
|
||||
if (self.max_write_buffer_size is not None and
|
||||
self._write_buffer_size + len(data) > self.max_write_buffer_size):
|
||||
raise StreamBufferFullError("Reached maximum read buffer size")
|
||||
# Break up large contiguous strings before inserting them in the
|
||||
# write buffer, so we don't have to recopy the entire thing
|
||||
# as we slice off pieces to send to the socket.
|
||||
WRITE_BUFFER_CHUNK_SIZE = 128 * 1024
|
||||
if len(data) > WRITE_BUFFER_CHUNK_SIZE:
|
||||
for i in range(0, len(data), WRITE_BUFFER_CHUNK_SIZE):
|
||||
self._write_buffer.append(data[i:i + WRITE_BUFFER_CHUNK_SIZE])
|
||||
else:
|
||||
self._write_buffer.append(data)
|
||||
self._write_callback = stack_context.wrap(callback)
|
||||
for i in range(0, len(data), WRITE_BUFFER_CHUNK_SIZE):
|
||||
self._write_buffer.append(data[i:i + WRITE_BUFFER_CHUNK_SIZE])
|
||||
self._write_buffer_size += len(data)
|
||||
if callback is not None:
|
||||
self._write_callback = stack_context.wrap(callback)
|
||||
future = None
|
||||
else:
|
||||
future = self._write_future = TracebackFuture()
|
||||
if not self._connecting:
|
||||
self._handle_write()
|
||||
if self._write_buffer:
|
||||
self._add_io_state(self.io_loop.WRITE)
|
||||
self._maybe_add_error_listener()
|
||||
return future
|
||||
|
||||
def set_close_callback(self, callback):
|
||||
"""Call the given callback when the stream is closed."""
|
||||
"""Call the given callback when the stream is closed.
|
||||
|
||||
This is not necessary for applications that use the `.Future`
|
||||
interface; all outstanding ``Futures`` will resolve with a
|
||||
`StreamClosedError` when the stream is closed.
|
||||
"""
|
||||
self._close_callback = stack_context.wrap(callback)
|
||||
self._maybe_add_error_listener()
|
||||
|
||||
def close(self, exc_info=False):
|
||||
"""Close this stream.
|
||||
@@ -251,13 +364,9 @@ class BaseIOStream(object):
|
||||
if self._read_until_close:
|
||||
if (self._streaming_callback is not None and
|
||||
self._read_buffer_size):
|
||||
self._run_callback(self._streaming_callback,
|
||||
self._consume(self._read_buffer_size))
|
||||
callback = self._read_callback
|
||||
self._read_callback = None
|
||||
self._run_read_callback(self._read_buffer_size, True)
|
||||
self._read_until_close = False
|
||||
self._run_callback(callback,
|
||||
self._consume(self._read_buffer_size))
|
||||
self._run_read_callback(self._read_buffer_size, False)
|
||||
if self._state is not None:
|
||||
self.io_loop.remove_handler(self.fileno())
|
||||
self._state = None
|
||||
@@ -269,6 +378,25 @@ class BaseIOStream(object):
|
||||
# If there are pending callbacks, don't run the close callback
|
||||
# until they're done (see _maybe_add_error_handler)
|
||||
if self.closed() and self._pending_callbacks == 0:
|
||||
futures = []
|
||||
if self._read_future is not None:
|
||||
futures.append(self._read_future)
|
||||
self._read_future = None
|
||||
if self._write_future is not None:
|
||||
futures.append(self._write_future)
|
||||
self._write_future = None
|
||||
if self._connect_future is not None:
|
||||
futures.append(self._connect_future)
|
||||
self._connect_future = None
|
||||
for future in futures:
|
||||
if (isinstance(self.error, (socket.error, IOError)) and
|
||||
errno_from_exception(self.error) in _ERRNO_CONNRESET):
|
||||
# Treat connection resets as closed connections so
|
||||
# clients only have to catch one kind of exception
|
||||
# to avoid logging.
|
||||
future.set_exception(StreamClosedError())
|
||||
else:
|
||||
future.set_exception(self.error or StreamClosedError())
|
||||
if self._close_callback is not None:
|
||||
cb = self._close_callback
|
||||
self._close_callback = None
|
||||
@@ -282,7 +410,7 @@ class BaseIOStream(object):
|
||||
|
||||
def reading(self):
|
||||
"""Returns true if we are currently reading from the stream."""
|
||||
return self._read_callback is not None
|
||||
return self._read_callback is not None or self._read_future is not None
|
||||
|
||||
def writing(self):
|
||||
"""Returns true if we are currently writing to the stream."""
|
||||
@@ -309,16 +437,22 @@ class BaseIOStream(object):
|
||||
|
||||
def _handle_events(self, fd, events):
|
||||
if self.closed():
|
||||
gen_log.warning("Got events for closed stream %d", fd)
|
||||
gen_log.warning("Got events for closed stream %s", fd)
|
||||
return
|
||||
try:
|
||||
if self._connecting:
|
||||
# Most IOLoops will report a write failed connect
|
||||
# with the WRITE event, but SelectIOLoop reports a
|
||||
# READ as well so we must check for connecting before
|
||||
# either.
|
||||
self._handle_connect()
|
||||
if self.closed():
|
||||
return
|
||||
if events & self.io_loop.READ:
|
||||
self._handle_read()
|
||||
if self.closed():
|
||||
return
|
||||
if events & self.io_loop.WRITE:
|
||||
if self._connecting:
|
||||
self._handle_connect()
|
||||
self._handle_write()
|
||||
if self.closed():
|
||||
return
|
||||
@@ -334,13 +468,20 @@ class BaseIOStream(object):
|
||||
state |= self.io_loop.READ
|
||||
if self.writing():
|
||||
state |= self.io_loop.WRITE
|
||||
if state == self.io_loop.ERROR:
|
||||
if state == self.io_loop.ERROR and self._read_buffer_size == 0:
|
||||
# If the connection is idle, listen for reads too so
|
||||
# we can tell if the connection is closed. If there is
|
||||
# data in the read buffer we won't run the close callback
|
||||
# yet anyway, so we don't need to listen in this case.
|
||||
state |= self.io_loop.READ
|
||||
if state != self._state:
|
||||
assert self._state is not None, \
|
||||
"shouldn't happen: _handle_events without self._state"
|
||||
self._state = state
|
||||
self.io_loop.update_handler(self.fileno(), self._state)
|
||||
except UnsatisfiableReadError as e:
|
||||
gen_log.info("Unsatisfiable read, closing connection: %s" % e)
|
||||
self.close(exc_info=True)
|
||||
except Exception:
|
||||
gen_log.error("Uncaught exception, closing connection.",
|
||||
exc_info=True)
|
||||
@@ -381,42 +522,108 @@ class BaseIOStream(object):
|
||||
self._pending_callbacks += 1
|
||||
self.io_loop.add_callback(wrapper)
|
||||
|
||||
def _read_to_buffer_loop(self):
|
||||
# This method is called from _handle_read and _try_inline_read.
|
||||
try:
|
||||
if self._read_bytes is not None:
|
||||
target_bytes = self._read_bytes
|
||||
elif self._read_max_bytes is not None:
|
||||
target_bytes = self._read_max_bytes
|
||||
elif self.reading():
|
||||
# For read_until without max_bytes, or
|
||||
# read_until_close, read as much as we can before
|
||||
# scanning for the delimiter.
|
||||
target_bytes = None
|
||||
else:
|
||||
target_bytes = 0
|
||||
next_find_pos = 0
|
||||
# Pretend to have a pending callback so that an EOF in
|
||||
# _read_to_buffer doesn't trigger an immediate close
|
||||
# callback. At the end of this method we'll either
|
||||
# estabilsh a real pending callback via
|
||||
# _read_from_buffer or run the close callback.
|
||||
#
|
||||
# We need two try statements here so that
|
||||
# pending_callbacks is decremented before the `except`
|
||||
# clause below (which calls `close` and does need to
|
||||
# trigger the callback)
|
||||
self._pending_callbacks += 1
|
||||
while not self.closed():
|
||||
# Read from the socket until we get EWOULDBLOCK or equivalent.
|
||||
# SSL sockets do some internal buffering, and if the data is
|
||||
# sitting in the SSL object's buffer select() and friends
|
||||
# can't see it; the only way to find out if it's there is to
|
||||
# try to read it.
|
||||
if self._read_to_buffer() == 0:
|
||||
break
|
||||
|
||||
self._run_streaming_callback()
|
||||
|
||||
# If we've read all the bytes we can use, break out of
|
||||
# this loop. We can't just call read_from_buffer here
|
||||
# because of subtle interactions with the
|
||||
# pending_callback and error_listener mechanisms.
|
||||
#
|
||||
# If we've reached target_bytes, we know we're done.
|
||||
if (target_bytes is not None and
|
||||
self._read_buffer_size >= target_bytes):
|
||||
break
|
||||
|
||||
# Otherwise, we need to call the more expensive find_read_pos.
|
||||
# It's inefficient to do this on every read, so instead
|
||||
# do it on the first read and whenever the read buffer
|
||||
# size has doubled.
|
||||
if self._read_buffer_size >= next_find_pos:
|
||||
pos = self._find_read_pos()
|
||||
if pos is not None:
|
||||
return pos
|
||||
next_find_pos = self._read_buffer_size * 2
|
||||
return self._find_read_pos()
|
||||
finally:
|
||||
self._pending_callbacks -= 1
|
||||
|
||||
def _handle_read(self):
|
||||
try:
|
||||
try:
|
||||
# Pretend to have a pending callback so that an EOF in
|
||||
# _read_to_buffer doesn't trigger an immediate close
|
||||
# callback. At the end of this method we'll either
|
||||
# estabilsh a real pending callback via
|
||||
# _read_from_buffer or run the close callback.
|
||||
#
|
||||
# We need two try statements here so that
|
||||
# pending_callbacks is decremented before the `except`
|
||||
# clause below (which calls `close` and does need to
|
||||
# trigger the callback)
|
||||
self._pending_callbacks += 1
|
||||
while not self.closed():
|
||||
# Read from the socket until we get EWOULDBLOCK or equivalent.
|
||||
# SSL sockets do some internal buffering, and if the data is
|
||||
# sitting in the SSL object's buffer select() and friends
|
||||
# can't see it; the only way to find out if it's there is to
|
||||
# try to read it.
|
||||
if self._read_to_buffer() == 0:
|
||||
break
|
||||
finally:
|
||||
self._pending_callbacks -= 1
|
||||
pos = self._read_to_buffer_loop()
|
||||
except UnsatisfiableReadError:
|
||||
raise
|
||||
except Exception:
|
||||
gen_log.warning("error on read", exc_info=True)
|
||||
self.close(exc_info=True)
|
||||
return
|
||||
if self._read_from_buffer():
|
||||
if pos is not None:
|
||||
self._read_from_buffer(pos)
|
||||
return
|
||||
else:
|
||||
self._maybe_run_close_callback()
|
||||
|
||||
def _set_read_callback(self, callback):
|
||||
assert not self._read_callback, "Already reading"
|
||||
self._read_callback = stack_context.wrap(callback)
|
||||
assert self._read_callback is None, "Already reading"
|
||||
assert self._read_future is None, "Already reading"
|
||||
if callback is not None:
|
||||
self._read_callback = stack_context.wrap(callback)
|
||||
else:
|
||||
self._read_future = TracebackFuture()
|
||||
return self._read_future
|
||||
|
||||
def _run_read_callback(self, size, streaming):
|
||||
if streaming:
|
||||
callback = self._streaming_callback
|
||||
else:
|
||||
callback = self._read_callback
|
||||
self._read_callback = self._streaming_callback = None
|
||||
if self._read_future is not None:
|
||||
assert callback is None
|
||||
future = self._read_future
|
||||
self._read_future = None
|
||||
future.set_result(self._consume(size))
|
||||
if callback is not None:
|
||||
assert self._read_future is None
|
||||
self._run_callback(callback, self._consume(size))
|
||||
else:
|
||||
# If we scheduled a callback, we will add the error listener
|
||||
# afterwards. If we didn't, we have to do it now.
|
||||
self._maybe_add_error_listener()
|
||||
|
||||
def _try_inline_read(self):
|
||||
"""Attempt to complete the current read operation from buffered data.
|
||||
@@ -426,18 +633,14 @@ class BaseIOStream(object):
|
||||
listening for reads on the socket.
|
||||
"""
|
||||
# See if we've already got the data from a previous read
|
||||
if self._read_from_buffer():
|
||||
self._run_streaming_callback()
|
||||
pos = self._find_read_pos()
|
||||
if pos is not None:
|
||||
self._read_from_buffer(pos)
|
||||
return
|
||||
self._check_closed()
|
||||
try:
|
||||
try:
|
||||
# See comments in _handle_read about incrementing _pending_callbacks
|
||||
self._pending_callbacks += 1
|
||||
while not self.closed():
|
||||
if self._read_to_buffer() == 0:
|
||||
break
|
||||
finally:
|
||||
self._pending_callbacks -= 1
|
||||
pos = self._read_to_buffer_loop()
|
||||
except Exception:
|
||||
# If there was an in _read_to_buffer, we called close() already,
|
||||
# but couldn't run the close callback because of _pending_callbacks.
|
||||
@@ -445,9 +648,15 @@ class BaseIOStream(object):
|
||||
# applicable.
|
||||
self._maybe_run_close_callback()
|
||||
raise
|
||||
if self._read_from_buffer():
|
||||
if pos is not None:
|
||||
self._read_from_buffer(pos)
|
||||
return
|
||||
self._maybe_add_error_listener()
|
||||
# We couldn't satisfy the read inline, so either close the stream
|
||||
# or listen for new data.
|
||||
if self.closed():
|
||||
self._maybe_run_close_callback()
|
||||
else:
|
||||
self._add_io_state(ioloop.IOLoop.READ)
|
||||
|
||||
def _read_to_buffer(self):
|
||||
"""Reads from the socket and appends the result to the read buffer.
|
||||
@@ -472,32 +681,42 @@ class BaseIOStream(object):
|
||||
return 0
|
||||
self._read_buffer.append(chunk)
|
||||
self._read_buffer_size += len(chunk)
|
||||
if self._read_buffer_size >= self.max_buffer_size:
|
||||
if self._read_buffer_size > self.max_buffer_size:
|
||||
gen_log.error("Reached maximum read buffer size")
|
||||
self.close()
|
||||
raise IOError("Reached maximum read buffer size")
|
||||
raise StreamBufferFullError("Reached maximum read buffer size")
|
||||
return len(chunk)
|
||||
|
||||
def _read_from_buffer(self):
|
||||
"""Attempts to complete the currently-pending read from the buffer.
|
||||
|
||||
Returns True if the read was completed.
|
||||
"""
|
||||
def _run_streaming_callback(self):
|
||||
if self._streaming_callback is not None and self._read_buffer_size:
|
||||
bytes_to_consume = self._read_buffer_size
|
||||
if self._read_bytes is not None:
|
||||
bytes_to_consume = min(self._read_bytes, bytes_to_consume)
|
||||
self._read_bytes -= bytes_to_consume
|
||||
self._run_callback(self._streaming_callback,
|
||||
self._consume(bytes_to_consume))
|
||||
if self._read_bytes is not None and self._read_buffer_size >= self._read_bytes:
|
||||
num_bytes = self._read_bytes
|
||||
callback = self._read_callback
|
||||
self._read_callback = None
|
||||
self._streaming_callback = None
|
||||
self._read_bytes = None
|
||||
self._run_callback(callback, self._consume(num_bytes))
|
||||
return True
|
||||
self._run_read_callback(bytes_to_consume, True)
|
||||
|
||||
def _read_from_buffer(self, pos):
|
||||
"""Attempts to complete the currently-pending read from the buffer.
|
||||
|
||||
The argument is either a position in the read buffer or None,
|
||||
as returned by _find_read_pos.
|
||||
"""
|
||||
self._read_bytes = self._read_delimiter = self._read_regex = None
|
||||
self._read_partial = False
|
||||
self._run_read_callback(pos, False)
|
||||
|
||||
def _find_read_pos(self):
|
||||
"""Attempts to find a position in the read buffer that satisfies
|
||||
the currently-pending read.
|
||||
|
||||
Returns a position in the buffer if the current read can be satisfied,
|
||||
or None if it cannot.
|
||||
"""
|
||||
if (self._read_bytes is not None and
|
||||
(self._read_buffer_size >= self._read_bytes or
|
||||
(self._read_partial and self._read_buffer_size > 0))):
|
||||
num_bytes = min(self._read_bytes, self._read_buffer_size)
|
||||
return num_bytes
|
||||
elif self._read_delimiter is not None:
|
||||
# Multi-byte delimiters (e.g. '\r\n') may straddle two
|
||||
# chunks in the read buffer, so we can't easily find them
|
||||
@@ -506,37 +725,40 @@ class BaseIOStream(object):
|
||||
# length) tend to be "line" oriented, the delimiter is likely
|
||||
# to be in the first few chunks. Merge the buffer gradually
|
||||
# since large merges are relatively expensive and get undone in
|
||||
# consume().
|
||||
# _consume().
|
||||
if self._read_buffer:
|
||||
while True:
|
||||
loc = self._read_buffer[0].find(self._read_delimiter)
|
||||
if loc != -1:
|
||||
callback = self._read_callback
|
||||
delimiter_len = len(self._read_delimiter)
|
||||
self._read_callback = None
|
||||
self._streaming_callback = None
|
||||
self._read_delimiter = None
|
||||
self._run_callback(callback,
|
||||
self._consume(loc + delimiter_len))
|
||||
return True
|
||||
self._check_max_bytes(self._read_delimiter,
|
||||
loc + delimiter_len)
|
||||
return loc + delimiter_len
|
||||
if len(self._read_buffer) == 1:
|
||||
break
|
||||
_double_prefix(self._read_buffer)
|
||||
self._check_max_bytes(self._read_delimiter,
|
||||
len(self._read_buffer[0]))
|
||||
elif self._read_regex is not None:
|
||||
if self._read_buffer:
|
||||
while True:
|
||||
m = self._read_regex.search(self._read_buffer[0])
|
||||
if m is not None:
|
||||
callback = self._read_callback
|
||||
self._read_callback = None
|
||||
self._streaming_callback = None
|
||||
self._read_regex = None
|
||||
self._run_callback(callback, self._consume(m.end()))
|
||||
return True
|
||||
self._check_max_bytes(self._read_regex, m.end())
|
||||
return m.end()
|
||||
if len(self._read_buffer) == 1:
|
||||
break
|
||||
_double_prefix(self._read_buffer)
|
||||
return False
|
||||
self._check_max_bytes(self._read_regex,
|
||||
len(self._read_buffer[0]))
|
||||
return None
|
||||
|
||||
def _check_max_bytes(self, delimiter, size):
|
||||
if (self._read_max_bytes is not None and
|
||||
size > self._read_max_bytes):
|
||||
raise UnsatisfiableReadError(
|
||||
"delimiter %r not found within %d bytes" % (
|
||||
delimiter, self._read_max_bytes))
|
||||
|
||||
def _handle_write(self):
|
||||
while self._write_buffer:
|
||||
@@ -563,6 +785,7 @@ class BaseIOStream(object):
|
||||
self._write_buffer_frozen = False
|
||||
_merge_prefix(self._write_buffer, num_bytes)
|
||||
self._write_buffer.popleft()
|
||||
self._write_buffer_size -= num_bytes
|
||||
except (socket.error, IOError, OSError) as e:
|
||||
if e.args[0] in _ERRNO_WOULDBLOCK:
|
||||
self._write_buffer_frozen = True
|
||||
@@ -572,14 +795,19 @@ class BaseIOStream(object):
|
||||
# Broken pipe errors are usually caused by connection
|
||||
# reset, and its better to not log EPIPE errors to
|
||||
# minimize log spam
|
||||
gen_log.warning("Write error on %d: %s",
|
||||
gen_log.warning("Write error on %s: %s",
|
||||
self.fileno(), e)
|
||||
self.close(exc_info=True)
|
||||
return
|
||||
if not self._write_buffer and self._write_callback:
|
||||
callback = self._write_callback
|
||||
self._write_callback = None
|
||||
self._run_callback(callback)
|
||||
if not self._write_buffer:
|
||||
if self._write_callback:
|
||||
callback = self._write_callback
|
||||
self._write_callback = None
|
||||
self._run_callback(callback)
|
||||
if self._write_future:
|
||||
future = self._write_future
|
||||
self._write_future = None
|
||||
future.set_result(None)
|
||||
|
||||
def _consume(self, loc):
|
||||
if loc == 0:
|
||||
@@ -593,10 +821,19 @@ class BaseIOStream(object):
|
||||
raise StreamClosedError("Stream is closed")
|
||||
|
||||
def _maybe_add_error_listener(self):
|
||||
if self._state is None and self._pending_callbacks == 0:
|
||||
# This method is part of an optimization: to detect a connection that
|
||||
# is closed when we're not actively reading or writing, we must listen
|
||||
# for read events. However, it is inefficient to do this when the
|
||||
# connection is first established because we are going to read or write
|
||||
# immediately anyway. Instead, we insert checks at various times to
|
||||
# see if the connection is idle and add the read listener then.
|
||||
if self._pending_callbacks != 0:
|
||||
return
|
||||
if self._state is None or self._state == ioloop.IOLoop.ERROR:
|
||||
if self.closed():
|
||||
self._maybe_run_close_callback()
|
||||
else:
|
||||
elif (self._read_buffer_size == 0 and
|
||||
self._close_callback is not None):
|
||||
self._add_io_state(ioloop.IOLoop.READ)
|
||||
|
||||
def _add_io_state(self, state):
|
||||
@@ -680,7 +917,7 @@ class IOStream(BaseIOStream):
|
||||
super(IOStream, self).__init__(*args, **kwargs)
|
||||
|
||||
def fileno(self):
|
||||
return self.socket.fileno()
|
||||
return self.socket
|
||||
|
||||
def close_fd(self):
|
||||
self.socket.close()
|
||||
@@ -714,7 +951,9 @@ class IOStream(BaseIOStream):
|
||||
not previously connected. The address parameter is in the
|
||||
same format as for `socket.connect <socket.socket.connect>`,
|
||||
i.e. a ``(host, port)`` tuple. If ``callback`` is specified,
|
||||
it will be called when the connection is completed.
|
||||
it will be called with no arguments when the connection is
|
||||
completed; if not this method returns a `.Future` (whose result
|
||||
after a successful connection will be the stream itself).
|
||||
|
||||
If specified, the ``server_hostname`` parameter will be used
|
||||
in SSL connections for certificate validation (if requested in
|
||||
@@ -726,6 +965,9 @@ class IOStream(BaseIOStream):
|
||||
which case the data will be written as soon as the connection
|
||||
is ready. Calling `IOStream` read methods before the socket is
|
||||
connected works on some platforms but is non-portable.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
If no callback is given, returns a `.Future`.
|
||||
"""
|
||||
self._connecting = True
|
||||
try:
|
||||
@@ -738,14 +980,83 @@ class IOStream(BaseIOStream):
|
||||
# returned immediately when attempting to connect to
|
||||
# localhost, so handle them the same way as an error
|
||||
# reported later in _handle_connect.
|
||||
if (e.args[0] != errno.EINPROGRESS and
|
||||
e.args[0] not in _ERRNO_WOULDBLOCK):
|
||||
gen_log.warning("Connect error on fd %d: %s",
|
||||
if (errno_from_exception(e) != errno.EINPROGRESS and
|
||||
errno_from_exception(e) not in _ERRNO_WOULDBLOCK):
|
||||
gen_log.warning("Connect error on fd %s: %s",
|
||||
self.socket.fileno(), e)
|
||||
self.close(exc_info=True)
|
||||
return
|
||||
self._connect_callback = stack_context.wrap(callback)
|
||||
if callback is not None:
|
||||
self._connect_callback = stack_context.wrap(callback)
|
||||
future = None
|
||||
else:
|
||||
future = self._connect_future = TracebackFuture()
|
||||
self._add_io_state(self.io_loop.WRITE)
|
||||
return future
|
||||
|
||||
def start_tls(self, server_side, ssl_options=None, server_hostname=None):
|
||||
"""Convert this `IOStream` to an `SSLIOStream`.
|
||||
|
||||
This enables protocols that begin in clear-text mode and
|
||||
switch to SSL after some initial negotiation (such as the
|
||||
``STARTTLS`` extension to SMTP and IMAP).
|
||||
|
||||
This method cannot be used if there are outstanding reads
|
||||
or writes on the stream, or if there is any data in the
|
||||
IOStream's buffer (data in the operating system's socket
|
||||
buffer is allowed). This means it must generally be used
|
||||
immediately after reading or writing the last clear-text
|
||||
data. It can also be used immediately after connecting,
|
||||
before any reads or writes.
|
||||
|
||||
The ``ssl_options`` argument may be either a dictionary
|
||||
of options or an `ssl.SSLContext`. If a ``server_hostname``
|
||||
is given, it will be used for certificate verification
|
||||
(as configured in the ``ssl_options``).
|
||||
|
||||
This method returns a `.Future` whose result is the new
|
||||
`SSLIOStream`. After this method has been called,
|
||||
any other operation on the original stream is undefined.
|
||||
|
||||
If a close callback is defined on this stream, it will be
|
||||
transferred to the new stream.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
"""
|
||||
if (self._read_callback or self._read_future or
|
||||
self._write_callback or self._write_future or
|
||||
self._connect_callback or self._connect_future or
|
||||
self._pending_callbacks or self._closed or
|
||||
self._read_buffer or self._write_buffer):
|
||||
raise ValueError("IOStream is not idle; cannot convert to SSL")
|
||||
if ssl_options is None:
|
||||
ssl_options = {}
|
||||
|
||||
socket = self.socket
|
||||
self.io_loop.remove_handler(socket)
|
||||
self.socket = None
|
||||
socket = ssl_wrap_socket(socket, ssl_options, server_side=server_side,
|
||||
do_handshake_on_connect=False)
|
||||
orig_close_callback = self._close_callback
|
||||
self._close_callback = None
|
||||
|
||||
future = TracebackFuture()
|
||||
ssl_stream = SSLIOStream(socket, ssl_options=ssl_options,
|
||||
io_loop=self.io_loop)
|
||||
# Wrap the original close callback so we can fail our Future as well.
|
||||
# If we had an "unwrap" counterpart to this method we would need
|
||||
# to restore the original callback after our Future resolves
|
||||
# so that repeated wrap/unwrap calls don't build up layers.
|
||||
def close_callback():
|
||||
if not future.done():
|
||||
future.set_exception(ssl_stream.error or StreamClosedError())
|
||||
if orig_close_callback is not None:
|
||||
orig_close_callback()
|
||||
ssl_stream.set_close_callback(close_callback)
|
||||
ssl_stream._ssl_connect_callback = lambda: future.set_result(ssl_stream)
|
||||
ssl_stream.max_buffer_size = self.max_buffer_size
|
||||
ssl_stream.read_chunk_size = self.read_chunk_size
|
||||
return future
|
||||
|
||||
def _handle_connect(self):
|
||||
err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
|
||||
@@ -755,14 +1066,19 @@ class IOStream(BaseIOStream):
|
||||
# an error state before the socket becomes writable, so
|
||||
# in that case a connection failure would be handled by the
|
||||
# error path in _handle_events instead of here.
|
||||
gen_log.warning("Connect error on fd %d: %s",
|
||||
self.socket.fileno(), errno.errorcode[err])
|
||||
if self._connect_future is None:
|
||||
gen_log.warning("Connect error on fd %s: %s",
|
||||
self.socket.fileno(), errno.errorcode[err])
|
||||
self.close()
|
||||
return
|
||||
if self._connect_callback is not None:
|
||||
callback = self._connect_callback
|
||||
self._connect_callback = None
|
||||
self._run_callback(callback)
|
||||
if self._connect_future is not None:
|
||||
future = self._connect_future
|
||||
self._connect_future = None
|
||||
future.set_result(self)
|
||||
self._connecting = False
|
||||
|
||||
def set_nodelay(self, value):
|
||||
@@ -841,7 +1157,7 @@ class SSLIOStream(IOStream):
|
||||
peer = self.socket.getpeername()
|
||||
except Exception:
|
||||
peer = '(not connected)'
|
||||
gen_log.warning("SSL Error on %d %s: %s",
|
||||
gen_log.warning("SSL Error on %s %s: %s",
|
||||
self.socket.fileno(), peer, err)
|
||||
return self.close(exc_info=True)
|
||||
raise
|
||||
@@ -907,19 +1223,33 @@ class SSLIOStream(IOStream):
|
||||
# has completed.
|
||||
self._ssl_connect_callback = stack_context.wrap(callback)
|
||||
self._server_hostname = server_hostname
|
||||
super(SSLIOStream, self).connect(address, callback=None)
|
||||
# Note: Since we don't pass our callback argument along to
|
||||
# super.connect(), this will always return a Future.
|
||||
# This is harmless, but a bit less efficient than it could be.
|
||||
return super(SSLIOStream, self).connect(address, callback=None)
|
||||
|
||||
def _handle_connect(self):
|
||||
# Call the superclass method to check for errors.
|
||||
super(SSLIOStream, self)._handle_connect()
|
||||
if self.closed():
|
||||
return
|
||||
# When the connection is complete, wrap the socket for SSL
|
||||
# traffic. Note that we do this by overriding _handle_connect
|
||||
# instead of by passing a callback to super().connect because
|
||||
# user callbacks are enqueued asynchronously on the IOLoop,
|
||||
# but since _handle_events calls _handle_connect immediately
|
||||
# followed by _handle_write we need this to be synchronous.
|
||||
#
|
||||
# The IOLoop will get confused if we swap out self.socket while the
|
||||
# fd is registered, so remove it now and re-register after
|
||||
# wrap_socket().
|
||||
self.io_loop.remove_handler(self.socket)
|
||||
old_state = self._state
|
||||
self._state = None
|
||||
self.socket = ssl_wrap_socket(self.socket, self._ssl_options,
|
||||
server_hostname=self._server_hostname,
|
||||
do_handshake_on_connect=False)
|
||||
super(SSLIOStream, self)._handle_connect()
|
||||
self._add_io_state(old_state)
|
||||
|
||||
def read_from_fd(self):
|
||||
if self._ssl_accepting:
|
||||
@@ -978,9 +1308,9 @@ class PipeIOStream(BaseIOStream):
|
||||
try:
|
||||
chunk = os.read(self.fd, self.read_chunk_size)
|
||||
except (IOError, OSError) as e:
|
||||
if e.args[0] in _ERRNO_WOULDBLOCK:
|
||||
if errno_from_exception(e) in _ERRNO_WOULDBLOCK:
|
||||
return None
|
||||
elif e.args[0] == errno.EBADF:
|
||||
elif errno_from_exception(e) == errno.EBADF:
|
||||
# If the writing half of a pipe is closed, select will
|
||||
# report it as readable but reads will fail with EBADF.
|
||||
self.close(exc_info=True)
|
||||
|
||||
+5
-5
@@ -83,10 +83,10 @@ class LogFormatter(logging.Formatter):
|
||||
DEFAULT_FORMAT = '%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s'
|
||||
DEFAULT_DATE_FORMAT = '%y%m%d %H:%M:%S'
|
||||
DEFAULT_COLORS = {
|
||||
logging.DEBUG: 4, # Blue
|
||||
logging.INFO: 2, # Green
|
||||
logging.WARNING: 3, # Yellow
|
||||
logging.ERROR: 1, # Red
|
||||
logging.DEBUG: 4, # Blue
|
||||
logging.INFO: 2, # Green
|
||||
logging.WARNING: 3, # Yellow
|
||||
logging.ERROR: 1, # Red
|
||||
}
|
||||
|
||||
def __init__(self, color=True, fmt=DEFAULT_FORMAT,
|
||||
@@ -184,7 +184,7 @@ def enable_pretty_logging(options=None, logger=None):
|
||||
"""
|
||||
if options is None:
|
||||
from tornado.options import options
|
||||
if options.logging == 'none':
|
||||
if options.logging is None or options.logging.lower() == 'none':
|
||||
return
|
||||
if logger is None:
|
||||
logger = logging.getLogger()
|
||||
|
||||
+43
-8
@@ -20,18 +20,26 @@ from __future__ import absolute_import, division, print_function, with_statement
|
||||
|
||||
import errno
|
||||
import os
|
||||
import platform
|
||||
import socket
|
||||
import ssl
|
||||
import stat
|
||||
|
||||
from tornado.concurrent import dummy_executor, run_on_executor
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.platform.auto import set_close_exec
|
||||
from tornado.util import u, Configurable
|
||||
from tornado.util import u, Configurable, errno_from_exception
|
||||
|
||||
try:
|
||||
import ssl
|
||||
except ImportError:
|
||||
# ssl is not available on Google App Engine
|
||||
ssl = None
|
||||
|
||||
if hasattr(ssl, 'match_hostname') and hasattr(ssl, 'CertificateError'): # python 3.2+
|
||||
ssl_match_hostname = ssl.match_hostname
|
||||
SSLCertificateError = ssl.CertificateError
|
||||
elif ssl is None:
|
||||
ssl_match_hostname = SSLCertificateError = None
|
||||
else:
|
||||
import backports.ssl_match_hostname
|
||||
ssl_match_hostname = backports.ssl_match_hostname.match_hostname
|
||||
@@ -44,6 +52,11 @@ else:
|
||||
# thread now.
|
||||
u('foo').encode('idna')
|
||||
|
||||
# These errnos indicate that a non-blocking operation must be retried
|
||||
# at a later time. On most platforms they're the same value, but on
|
||||
# some they differ.
|
||||
_ERRNO_WOULDBLOCK = (errno.EWOULDBLOCK, errno.EAGAIN)
|
||||
|
||||
|
||||
def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags=None):
|
||||
"""Creates listening sockets bound to the given port and address.
|
||||
@@ -77,13 +90,23 @@ def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags
|
||||
family = socket.AF_INET
|
||||
if flags is None:
|
||||
flags = socket.AI_PASSIVE
|
||||
bound_port = None
|
||||
for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM,
|
||||
0, flags)):
|
||||
af, socktype, proto, canonname, sockaddr = res
|
||||
if (platform.system() == 'Darwin' and address == 'localhost' and
|
||||
af == socket.AF_INET6 and sockaddr[3] != 0):
|
||||
# Mac OS X includes a link-local address fe80::1%lo0 in the
|
||||
# getaddrinfo results for 'localhost'. However, the firewall
|
||||
# doesn't understand that this is a local address and will
|
||||
# prompt for access (often repeatedly, due to an apparent
|
||||
# bug in its ability to remember granting access to an
|
||||
# application). Skip these addresses.
|
||||
continue
|
||||
try:
|
||||
sock = socket.socket(af, socktype, proto)
|
||||
except socket.error as e:
|
||||
if e.args[0] == errno.EAFNOSUPPORT:
|
||||
if errno_from_exception(e) == errno.EAFNOSUPPORT:
|
||||
continue
|
||||
raise
|
||||
set_close_exec(sock.fileno())
|
||||
@@ -100,8 +123,16 @@ def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags
|
||||
# Python 2.x on windows doesn't have IPPROTO_IPV6.
|
||||
if hasattr(socket, "IPPROTO_IPV6"):
|
||||
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
||||
|
||||
# automatic port allocation with port=None
|
||||
# should bind on the same port on IPv4 and IPv6
|
||||
host, requested_port = sockaddr[:2]
|
||||
if requested_port == 0 and bound_port is not None:
|
||||
sockaddr = tuple([host, bound_port] + list(sockaddr[2:]))
|
||||
|
||||
sock.setblocking(0)
|
||||
sock.bind(sockaddr)
|
||||
bound_port = sock.getsockname()[1]
|
||||
sock.listen(backlog)
|
||||
sockets.append(sock)
|
||||
return sockets
|
||||
@@ -124,7 +155,7 @@ if hasattr(socket, 'AF_UNIX'):
|
||||
try:
|
||||
st = os.stat(file)
|
||||
except OSError as err:
|
||||
if err.errno != errno.ENOENT:
|
||||
if errno_from_exception(err) != errno.ENOENT:
|
||||
raise
|
||||
else:
|
||||
if stat.S_ISSOCK(st.st_mode):
|
||||
@@ -154,18 +185,18 @@ def add_accept_handler(sock, callback, io_loop=None):
|
||||
try:
|
||||
connection, address = sock.accept()
|
||||
except socket.error as e:
|
||||
# EWOULDBLOCK and EAGAIN indicate we have accepted every
|
||||
# _ERRNO_WOULDBLOCK indicate we have accepted every
|
||||
# connection that is available.
|
||||
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
|
||||
if errno_from_exception(e) in _ERRNO_WOULDBLOCK:
|
||||
return
|
||||
# ECONNABORTED indicates that there was a connection
|
||||
# but it was closed while still in the accept queue.
|
||||
# (observed on FreeBSD).
|
||||
if e.args[0] == errno.ECONNABORTED:
|
||||
if errno_from_exception(e) == errno.ECONNABORTED:
|
||||
continue
|
||||
raise
|
||||
callback(connection, address)
|
||||
io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ)
|
||||
io_loop.add_handler(sock, accept_handler, IOLoop.READ)
|
||||
|
||||
|
||||
def is_valid_ip(ip):
|
||||
@@ -381,6 +412,10 @@ def ssl_options_to_context(ssl_options):
|
||||
context.load_verify_locations(ssl_options['ca_certs'])
|
||||
if 'ciphers' in ssl_options:
|
||||
context.set_ciphers(ssl_options['ciphers'])
|
||||
if hasattr(ssl, 'OP_NO_COMPRESSION'):
|
||||
# Disable TLS compression to avoid CRIME and related attacks.
|
||||
# This constant wasn't added until python 3.3.
|
||||
context.options |= ssl.OP_NO_COMPRESSION
|
||||
return context
|
||||
|
||||
|
||||
|
||||
+16
-2
@@ -56,6 +56,18 @@ We support `datetimes <datetime.datetime>`, `timedeltas
|
||||
the top-level functions in this module (`define`, `parse_command_line`, etc)
|
||||
simply call methods on it. You may create additional `OptionParser`
|
||||
instances to define isolated sets of options, such as for subcommands.
|
||||
|
||||
.. note::
|
||||
|
||||
By default, several options are defined that will configure the
|
||||
standard `logging` module when `parse_command_line` or `parse_config_file`
|
||||
are called. If you want Tornado to leave the logging configuration
|
||||
alone so you can manage it yourself, either pass ``--logging=none``
|
||||
on the command line or do the following to disable it in code::
|
||||
|
||||
from tornado.options import options, parse_command_line
|
||||
options.logging = None
|
||||
parse_command_line()
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function, with_statement
|
||||
@@ -360,6 +372,8 @@ class _Mockable(object):
|
||||
|
||||
|
||||
class _Option(object):
|
||||
UNSET = object()
|
||||
|
||||
def __init__(self, name, default=None, type=basestring_type, help=None,
|
||||
metavar=None, multiple=False, file_name=None, group_name=None,
|
||||
callback=None):
|
||||
@@ -374,10 +388,10 @@ class _Option(object):
|
||||
self.group_name = group_name
|
||||
self.callback = callback
|
||||
self.default = default
|
||||
self._value = None
|
||||
self._value = _Option.UNSET
|
||||
|
||||
def value(self):
|
||||
return self.default if self._value is None else self._value
|
||||
return self.default if self._value is _Option.UNSET else self._value
|
||||
|
||||
def parse(self, value):
|
||||
_parse = {
|
||||
|
||||
@@ -10,21 +10,31 @@ unfinished callbacks on the event loop that fail when it resumes)
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function, with_statement
|
||||
import asyncio
|
||||
import datetime
|
||||
import functools
|
||||
import os
|
||||
|
||||
from tornado.ioloop import IOLoop
|
||||
# _Timeout is used for its timedelta_to_seconds method for py26 compatibility.
|
||||
from tornado.ioloop import IOLoop, _Timeout
|
||||
from tornado import stack_context
|
||||
|
||||
try:
|
||||
# Import the real asyncio module for py33+ first. Older versions of the
|
||||
# trollius backport also use this name.
|
||||
import asyncio
|
||||
except ImportError as e:
|
||||
# Asyncio itself isn't available; see if trollius is (backport to py26+).
|
||||
try:
|
||||
import trollius as asyncio
|
||||
except ImportError:
|
||||
# Re-raise the original asyncio error, not the trollius one.
|
||||
raise e
|
||||
|
||||
class BaseAsyncIOLoop(IOLoop):
|
||||
def initialize(self, asyncio_loop, close_loop=False):
|
||||
self.asyncio_loop = asyncio_loop
|
||||
self.close_loop = close_loop
|
||||
self.asyncio_loop.call_soon(self.make_current)
|
||||
# Maps fd to handler function (as in IOLoop.add_handler)
|
||||
# Maps fd to (fileobj, handler function) pair (as in IOLoop.add_handler)
|
||||
self.handlers = {}
|
||||
# Set of fds listening for reads/writes
|
||||
self.readers = set()
|
||||
@@ -34,19 +44,18 @@ class BaseAsyncIOLoop(IOLoop):
|
||||
def close(self, all_fds=False):
|
||||
self.closing = True
|
||||
for fd in list(self.handlers):
|
||||
fileobj, handler_func = self.handlers[fd]
|
||||
self.remove_handler(fd)
|
||||
if all_fds:
|
||||
try:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
self.close_fd(fileobj)
|
||||
if self.close_loop:
|
||||
self.asyncio_loop.close()
|
||||
|
||||
def add_handler(self, fd, handler, events):
|
||||
fd, fileobj = self.split_fd(fd)
|
||||
if fd in self.handlers:
|
||||
raise ValueError("fd %d added twice" % fd)
|
||||
self.handlers[fd] = stack_context.wrap(handler)
|
||||
raise ValueError("fd %s added twice" % fd)
|
||||
self.handlers[fd] = (fileobj, stack_context.wrap(handler))
|
||||
if events & IOLoop.READ:
|
||||
self.asyncio_loop.add_reader(
|
||||
fd, self._handle_events, fd, IOLoop.READ)
|
||||
@@ -57,6 +66,7 @@ class BaseAsyncIOLoop(IOLoop):
|
||||
self.writers.add(fd)
|
||||
|
||||
def update_handler(self, fd, events):
|
||||
fd, fileobj = self.split_fd(fd)
|
||||
if events & IOLoop.READ:
|
||||
if fd not in self.readers:
|
||||
self.asyncio_loop.add_reader(
|
||||
@@ -77,6 +87,7 @@ class BaseAsyncIOLoop(IOLoop):
|
||||
self.writers.remove(fd)
|
||||
|
||||
def remove_handler(self, fd):
|
||||
fd, fileobj = self.split_fd(fd)
|
||||
if fd not in self.handlers:
|
||||
return
|
||||
if fd in self.readers:
|
||||
@@ -88,7 +99,8 @@ class BaseAsyncIOLoop(IOLoop):
|
||||
del self.handlers[fd]
|
||||
|
||||
def _handle_events(self, fd, events):
|
||||
self.handlers[fd](fd, events)
|
||||
fileobj, handler_func = self.handlers[fd]
|
||||
handler_func(fileobj, events)
|
||||
|
||||
def start(self):
|
||||
self._setup_logging()
|
||||
@@ -107,7 +119,7 @@ class BaseAsyncIOLoop(IOLoop):
|
||||
if isinstance(deadline, (int, float)):
|
||||
delay = max(deadline - self.time(), 0)
|
||||
elif isinstance(deadline, datetime.timedelta):
|
||||
delay = deadline.total_seconds()
|
||||
delay = _Timeout.timedelta_to_seconds(deadline)
|
||||
else:
|
||||
raise TypeError("Unsupported deadline %r", deadline)
|
||||
return self.asyncio_loop.call_later(delay, self._run_callback,
|
||||
|
||||
@@ -30,6 +30,10 @@ import os
|
||||
if os.name == 'nt':
|
||||
from tornado.platform.common import Waker
|
||||
from tornado.platform.windows import set_close_exec
|
||||
elif 'APPENGINE_RUNTIME' in os.environ:
|
||||
from tornado.platform.common import Waker
|
||||
def set_close_exec(fd):
|
||||
pass
|
||||
else:
|
||||
from tornado.platform.posix import set_close_exec, Waker
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ class Waker(interface.Waker):
|
||||
and Jython.
|
||||
"""
|
||||
def __init__(self):
|
||||
# Based on Zope async.py: http://svn.zope.org/zc.ngi/trunk/src/zc/ngi/async.py
|
||||
# Based on Zope select_trigger.py:
|
||||
# https://github.com/zopefoundation/Zope/blob/master/src/ZServer/medusa/thread/select_trigger.py
|
||||
|
||||
self.writer = socket.socket()
|
||||
# Disable buffering -- pulling the trigger sends 1 byte,
|
||||
|
||||
@@ -37,7 +37,7 @@ class _KQueue(object):
|
||||
|
||||
def register(self, fd, events):
|
||||
if fd in self._active:
|
||||
raise IOError("fd %d already registered" % fd)
|
||||
raise IOError("fd %s already registered" % fd)
|
||||
self._control(fd, events, select.KQ_EV_ADD)
|
||||
self._active[fd] = events
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class _Select(object):
|
||||
|
||||
def register(self, fd, events):
|
||||
if fd in self.read_fds or fd in self.write_fds or fd in self.error_fds:
|
||||
raise IOError("fd %d already registered" % fd)
|
||||
raise IOError("fd %s already registered" % fd)
|
||||
if events & IOLoop.READ:
|
||||
self.read_fds.add(fd)
|
||||
if events & IOLoop.WRITE:
|
||||
|
||||
@@ -91,6 +91,11 @@ from tornado.netutil import Resolver
|
||||
from tornado.stack_context import NullContext, wrap
|
||||
from tornado.ioloop import IOLoop
|
||||
|
||||
try:
|
||||
long # py2
|
||||
except NameError:
|
||||
long = int # py3
|
||||
|
||||
|
||||
@implementer(IDelayedCall)
|
||||
class TornadoDelayedCall(object):
|
||||
@@ -365,8 +370,9 @@ def install(io_loop=None):
|
||||
|
||||
@implementer(IReadDescriptor, IWriteDescriptor)
|
||||
class _FD(object):
|
||||
def __init__(self, fd, handler):
|
||||
def __init__(self, fd, fileobj, handler):
|
||||
self.fd = fd
|
||||
self.fileobj = fileobj
|
||||
self.handler = handler
|
||||
self.reading = False
|
||||
self.writing = False
|
||||
@@ -377,15 +383,15 @@ class _FD(object):
|
||||
|
||||
def doRead(self):
|
||||
if not self.lost:
|
||||
self.handler(self.fd, tornado.ioloop.IOLoop.READ)
|
||||
self.handler(self.fileobj, tornado.ioloop.IOLoop.READ)
|
||||
|
||||
def doWrite(self):
|
||||
if not self.lost:
|
||||
self.handler(self.fd, tornado.ioloop.IOLoop.WRITE)
|
||||
self.handler(self.fileobj, tornado.ioloop.IOLoop.WRITE)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
if not self.lost:
|
||||
self.handler(self.fd, tornado.ioloop.IOLoop.ERROR)
|
||||
self.handler(self.fileobj, tornado.ioloop.IOLoop.ERROR)
|
||||
self.lost = True
|
||||
|
||||
def logPrefix(self):
|
||||
@@ -412,14 +418,19 @@ class TwistedIOLoop(tornado.ioloop.IOLoop):
|
||||
self.reactor.callWhenRunning(self.make_current)
|
||||
|
||||
def close(self, all_fds=False):
|
||||
fds = self.fds
|
||||
self.reactor.removeAll()
|
||||
for c in self.reactor.getDelayedCalls():
|
||||
c.cancel()
|
||||
if all_fds:
|
||||
for fd in fds.values():
|
||||
self.close_fd(fd.fileobj)
|
||||
|
||||
def add_handler(self, fd, handler, events):
|
||||
if fd in self.fds:
|
||||
raise ValueError('fd %d added twice' % fd)
|
||||
self.fds[fd] = _FD(fd, wrap(handler))
|
||||
raise ValueError('fd %s added twice' % fd)
|
||||
fd, fileobj = self.split_fd(fd)
|
||||
self.fds[fd] = _FD(fd, fileobj, wrap(handler))
|
||||
if events & tornado.ioloop.IOLoop.READ:
|
||||
self.fds[fd].reading = True
|
||||
self.reactor.addReader(self.fds[fd])
|
||||
@@ -428,6 +439,7 @@ class TwistedIOLoop(tornado.ioloop.IOLoop):
|
||||
self.reactor.addWriter(self.fds[fd])
|
||||
|
||||
def update_handler(self, fd, events):
|
||||
fd, fileobj = self.split_fd(fd)
|
||||
if events & tornado.ioloop.IOLoop.READ:
|
||||
if not self.fds[fd].reading:
|
||||
self.fds[fd].reading = True
|
||||
@@ -446,6 +458,7 @@ class TwistedIOLoop(tornado.ioloop.IOLoop):
|
||||
self.reactor.removeWriter(self.fds[fd])
|
||||
|
||||
def remove_handler(self, fd):
|
||||
fd, fileobj = self.split_fd(fd)
|
||||
if fd not in self.fds:
|
||||
return
|
||||
self.fds[fd].lost = True
|
||||
|
||||
+11
-3
@@ -21,7 +21,6 @@ the server into multiple processes and managing subprocesses.
|
||||
from __future__ import absolute_import, division, print_function, with_statement
|
||||
|
||||
import errno
|
||||
import multiprocessing
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
@@ -35,6 +34,13 @@ from tornado.iostream import PipeIOStream
|
||||
from tornado.log import gen_log
|
||||
from tornado.platform.auto import set_close_exec
|
||||
from tornado import stack_context
|
||||
from tornado.util import errno_from_exception
|
||||
|
||||
try:
|
||||
import multiprocessing
|
||||
except ImportError:
|
||||
# Multiprocessing is not availble on Google App Engine.
|
||||
multiprocessing = None
|
||||
|
||||
try:
|
||||
long # py2
|
||||
@@ -44,6 +50,8 @@ except NameError:
|
||||
|
||||
def cpu_count():
|
||||
"""Returns the number of processors on this machine."""
|
||||
if multiprocessing is None:
|
||||
return 1
|
||||
try:
|
||||
return multiprocessing.cpu_count()
|
||||
except NotImplementedError:
|
||||
@@ -136,7 +144,7 @@ def fork_processes(num_processes, max_restarts=100):
|
||||
try:
|
||||
pid, status = os.wait()
|
||||
except OSError as e:
|
||||
if e.errno == errno.EINTR:
|
||||
if errno_from_exception(e) == errno.EINTR:
|
||||
continue
|
||||
raise
|
||||
if pid not in children:
|
||||
@@ -283,7 +291,7 @@ class Subprocess(object):
|
||||
try:
|
||||
ret_pid, status = os.waitpid(pid, os.WNOHANG)
|
||||
except OSError as e:
|
||||
if e.args[0] == errno.ECHILD:
|
||||
if errno_from_exception(e) == errno.ECHILD:
|
||||
return
|
||||
if ret_pid == 0:
|
||||
return
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user