Skip to content

Commit

Permalink
Now with the right httplib.py
Browse files Browse the repository at this point in the history
  • Loading branch information
darjus committed Dec 11, 2015
1 parent a153cd8 commit 44778c4
Showing 1 changed file with 121 additions and 46 deletions.
167 changes: 121 additions & 46 deletions lib-python/2.7/httplib.py
Expand Up @@ -68,6 +68,7 @@

from array import array
import os
import re
import socket
from sys import py3kwarning
from urlparse import urlsplit
Expand Down Expand Up @@ -218,6 +219,38 @@
# maximum amount of headers accepted
_MAXHEADERS = 100

# Header name/value ABNF (http://tools.ietf.org/html/rfc7230#section-3.2)
#
# VCHAR = %x21-7E
# obs-text = %x80-FF
# header-field = field-name ":" OWS field-value OWS
# field-name = token
# field-value = *( field-content / obs-fold )
# field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
# field-vchar = VCHAR / obs-text
#
# obs-fold = CRLF 1*( SP / HTAB )
# ; obsolete line folding
# ; see Section 3.2.4

# token = 1*tchar
#
# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
# / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
# / DIGIT / ALPHA
# ; any VCHAR, except delimiters
#
# VCHAR defined in http://tools.ietf.org/html/rfc5234#appendix-B.1

# the patterns for both name and value are more leniant than RFC
# definitions to allow for backwards compatibility
_is_legal_header_name = re.compile(r'\A[^:\s][^:\r\n]*\Z').match
_is_illegal_header_value = re.compile(r'\n(?![ \t])|\r(?![ \t\n])').search

# We always set the Content-Length header for these methods because some
# servers will otherwise respond with a 411
_METHODS_EXPECTING_BODY = {'PATCH', 'POST', 'PUT'}


class HTTPMessage(mimetools.Message):

Expand Down Expand Up @@ -313,6 +346,11 @@ def readheaders(self):
hlist.append(line)
self.addheader(headerseen, line[len(headerseen)+1:].strip())
continue
elif headerseen is not None:
# An empty header name. These aren't allowed in HTTP, but it's
# probably a benign mistake. Don't add the header, just keep
# going.
continue
else:
# It's not a header line; throw it back and stop here.
if not self.dict:
Expand Down Expand Up @@ -522,9 +560,10 @@ def _check_close(self):
return True

def close(self):
if self.fp:
self.fp.close()
fp = self.fp
if fp:
self.fp = None
fp.close()

def isclosed(self):
# NOTE: it is possible that we will not ever call self.close(). This
Expand Down Expand Up @@ -723,7 +762,7 @@ def set_tunnel(self, host, port=None, headers=None):
endpoint passed to set_tunnel. This is done by sending a HTTP CONNECT
request to the proxy server when the connection is established.
This method must be called before the HTML connection has been
This method must be called before the HTTP connection has been
established.
The headers argument should be a mapping of extra HTTP headers
Expand All @@ -733,8 +772,7 @@ def set_tunnel(self, host, port=None, headers=None):
if self.sock:
raise RuntimeError("Can't setup tunnel for established connection.")

self._tunnel_host = host
self._tunnel_port = port
self._tunnel_host, self._tunnel_port = self._get_hostport(host, port)
if headers:
self._tunnel_headers = headers
else:
Expand Down Expand Up @@ -763,15 +801,20 @@ def set_debuglevel(self, level):
self.debuglevel = level

def _tunnel(self):
(host, port) = self._get_hostport(self._tunnel_host, self._tunnel_port)
self.send("CONNECT %s:%d HTTP/1.0\r\n" % (host, port))
self.send("CONNECT %s:%d HTTP/1.0\r\n" % (self._tunnel_host,
self._tunnel_port))
for header, value in self._tunnel_headers.iteritems():
self.send("%s: %s\r\n" % (header, value))
self.send("\r\n")
response = self.response_class(self.sock, strict = self.strict,
method = self._method)
(version, code, message) = response._read_status()

if version == "HTTP/0.9":
# HTTP/0.9 doesn't support the CONNECT verb, so if httplib has
# concluded HTTP/0.9 is being used something has gone wrong.
self.close()
raise socket.error("Invalid response from tunnel request")
if code != 200:
self.close()
raise socket.error("Tunnel connection failed: %d %s" % (code,
Expand All @@ -797,13 +840,17 @@ def connect(self):

def close(self):
"""Close the connection to the HTTP server."""
if self.sock:
self.sock.close() # close it manually... there may be other refs
self.sock = None
if self.__response:
self.__response.close()
self.__response = None
self.__state = _CS_IDLE
try:
sock = self.sock
if sock:
self.sock = None
sock.close() # close it manually... there may be other refs
finally:
response = self.__response
if response:
self.__response = None
response.close()

def send(self, data):
"""Send `data' to the server."""
Expand Down Expand Up @@ -978,7 +1025,16 @@ def putheader(self, header, *values):
if self.__state != _CS_REQ_STARTED:
raise CannotSendHeader()

hdr = '%s: %s' % (header, '\r\n\t'.join([str(v) for v in values]))
header = '%s' % header
if not _is_legal_header_name(header):
raise ValueError('Invalid header name %r' % (header,))

values = [str(v) for v in values]
for one_value in values:
if _is_illegal_header_value(one_value):
raise ValueError('Invalid header value %r' % (one_value,))

hdr = '%s: %s' % (header, '\r\n\t'.join(values))
self._output(hdr)

def endheaders(self, message_body=None):
Expand All @@ -1000,19 +1056,25 @@ def request(self, method, url, body=None, headers={}):
"""Send a complete request to the server."""
self._send_request(method, url, body, headers)

def _set_content_length(self, body):
# Set the content-length based on the body.
def _set_content_length(self, body, method):
# Set the content-length based on the body. If the body is "empty", we
# set Content-Length: 0 for methods that expect a body (RFC 7230,
# Section 3.3.2). If the body is set for other methods, we set the
# header provided we can figure out what the length is.
thelen = None
try:
thelen = str(len(body))
except TypeError, te:
# If this is a file-like object, try to
# fstat its file descriptor
if body is None and method.upper() in _METHODS_EXPECTING_BODY:
thelen = '0'
elif body is not None:
try:
thelen = str(os.fstat(body.fileno()).st_size)
except (AttributeError, OSError):
# Don't send a length if this failed
if self.debuglevel > 0: print "Cannot stat!!"
thelen = str(len(body))
except (TypeError, AttributeError):
# If this is a file-like object, try to
# fstat its file descriptor
try:
thelen = str(os.fstat(body.fileno()).st_size)
except (AttributeError, OSError):
# Don't send a length if this failed
if self.debuglevel > 0: print "Cannot stat!!"

if thelen is not None:
self.putheader('Content-Length', thelen)
Expand All @@ -1028,8 +1090,8 @@ def _send_request(self, method, url, body, headers):

self.putrequest(method, url, **skips)

if body is not None and 'content-length' not in header_names:
self._set_content_length(body)
if 'content-length' not in header_names:
self._set_content_length(body, method)
for hdr, value in headers.iteritems():
self.putheader(hdr, value)
self.endheaders(body)
Expand Down Expand Up @@ -1070,18 +1132,22 @@ def getresponse(self, buffering=False):
kwds["buffering"] = True;
response = self.response_class(*args, **kwds)

response.begin()
assert response.will_close != _UNKNOWN
self.__state = _CS_IDLE
try:
response.begin()
assert response.will_close != _UNKNOWN
self.__state = _CS_IDLE

if response.will_close:
# this effectively passes the connection to the response
self.close()
else:
# remember this, so we can tell when it is complete
self.__response = response
if response.will_close:
# this effectively passes the connection to the response
self.close()
else:
# remember this, so we can tell when it is complete
self.__response = response

return response
return response
except:
response.close()
raise


class HTTP:
Expand Down Expand Up @@ -1125,7 +1191,7 @@ def connect(self, host=None, port=None):
"Accept arguments to set the host/port, since the superclass doesn't."

if host is not None:
self._conn._set_hostport(host, port)
(self._conn.host, self._conn.port) = self._conn._get_hostport(host, port)
self._conn.connect()

def getfile(self):
Expand Down Expand Up @@ -1187,21 +1253,29 @@ class HTTPSConnection(HTTPConnection):

def __init__(self, host, port=None, key_file=None, cert_file=None,
strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
source_address=None):
source_address=None, context=None):
HTTPConnection.__init__(self, host, port, strict, timeout,
source_address)
self.key_file = key_file
self.cert_file = cert_file
if context is None:
context = ssl._create_default_https_context()
if key_file or cert_file:
context.load_cert_chain(cert_file, key_file)
self._context = context

def connect(self):
"Connect to a host on a given (SSL) port."

sock = self._create_connection((self.host, self.port),
self.timeout, self.source_address)
HTTPConnection.connect(self)

if self._tunnel_host:
self.sock = sock
self._tunnel()
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file)
server_hostname = self._tunnel_host
else:
server_hostname = self.host

self.sock = self._context.wrap_socket(self.sock,
server_hostname=server_hostname)

__all__.append("HTTPSConnection")

Expand All @@ -1216,14 +1290,15 @@ class HTTPS(HTTP):
_connection_class = HTTPSConnection

def __init__(self, host='', port=None, key_file=None, cert_file=None,
strict=None):
strict=None, context=None):
# provide a default host, pass the X509 cert info

# urf. compensate for bad input.
if port == 0:
port = None
self._setup(self._connection_class(host, port, key_file,
cert_file, strict))
cert_file, strict,
context=context))

# we never actually use these for anything, but we keep them
# here for compatibility with post-1.5.2 CVS.
Expand Down

0 comments on commit 44778c4

Please sign in to comment.