blob: 1edee85707013d9b822f42e1e575875869aa8b57 [file] [log] [blame]
# Copyright 2008 Google Inc. All Rights Reserved.
# Code originally from Ian Bicking
# (http://code.google.com/p/appengine-monkey/).
# Contributed by Ian and subsequently modified here at Google.
"""Copyright 2008 Python Software Foundation, Ian Bicking, and Google."""
import cStringIO
import inspect
import mimetools
HTTP_PORT = 80
HTTPS_PORT = 443
_UNKNOWN = 'UNKNOWN'
# status codes
# informational
CONTINUE = 100
SWITCHING_PROTOCOLS = 101
PROCESSING = 102
# successful
OK = 200
CREATED = 201
ACCEPTED = 202
NON_AUTHORITATIVE_INFORMATION = 203
NO_CONTENT = 204
RESET_CONTENT = 205
PARTIAL_CONTENT = 206
MULTI_STATUS = 207
IM_USED = 226
# redirection
MULTIPLE_CHOICES = 300
MOVED_PERMANENTLY = 301
FOUND = 302
SEE_OTHER = 303
NOT_MODIFIED = 304
USE_PROXY = 305
TEMPORARY_REDIRECT = 307
# client error
BAD_REQUEST = 400
UNAUTHORIZED = 401
PAYMENT_REQUIRED = 402
FORBIDDEN = 403
NOT_FOUND = 404
METHOD_NOT_ALLOWED = 405
NOT_ACCEPTABLE = 406
PROXY_AUTHENTICATION_REQUIRED = 407
REQUEST_TIMEOUT = 408
CONFLICT = 409
GONE = 410
LENGTH_REQUIRED = 411
PRECONDITION_FAILED = 412
REQUEST_ENTITY_TOO_LARGE = 413
REQUEST_URI_TOO_LONG = 414
UNSUPPORTED_MEDIA_TYPE = 415
REQUESTED_RANGE_NOT_SATISFIABLE = 416
EXPECTATION_FAILED = 417
UNPROCESSABLE_ENTITY = 422
LOCKED = 423
FAILED_DEPENDENCY = 424
UPGRADE_REQUIRED = 426
# server error
INTERNAL_SERVER_ERROR = 500
NOT_IMPLEMENTED = 501
BAD_GATEWAY = 502
SERVICE_UNAVAILABLE = 503
GATEWAY_TIMEOUT = 504
HTTP_VERSION_NOT_SUPPORTED = 505
INSUFFICIENT_STORAGE = 507
NOT_EXTENDED = 510
# Mapping status codes to official W3C names
responses = {
100: 'Continue',
101: 'Switching Protocols',
200: 'OK',
201: 'Created',
202: 'Accepted',
203: 'Non-Authoritative Information',
204: 'No Content',
205: 'Reset Content',
206: 'Partial Content',
300: 'Multiple Choices',
301: 'Moved Permanently',
302: 'Found',
303: 'See Other',
304: 'Not Modified',
305: 'Use Proxy',
306: '(Unused)',
307: 'Temporary Redirect',
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Timeout',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Request Entity Too Large',
414: 'Request-URI Too Long',
415: 'Unsupported Media Type',
416: 'Requested Range Not Satisfiable',
417: 'Expectation Failed',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
505: 'HTTP Version Not Supported',
}
# maximal amount of data to read at one time in _safe_read
MAXAMOUNT = 1048576
# maximal line length when calling readline().
_MAXLINE = 65536
# Can't get this symbol from socket since importing socket causes an import
# cycle though:
# google.net.proto.ProtocolBuffer imports...
# httplib imports ...
# socket imports ...
# remote_socket_service_pb imports ProtocolBuffer
_GLOBAL_DEFAULT_TIMEOUT = object()
_IMPLEMENTATION = 'gae'
class HTTPMessage(mimetools.Message):
# App Engine Note: This class has been copied almost unchanged from
# Python 2.7.2
def addheader(self, key, value):
"""Add header for field key handling repeats."""
prev = self.dict.get(key)
if prev is None:
self.dict[key] = value
else:
combined = ", ".join((prev, value))
self.dict[key] = combined
def addcontinue(self, key, more):
"""Add more field data from a continuation line."""
prev = self.dict[key]
self.dict[key] = prev + "\n " + more
def readheaders(self):
"""Read header lines.
Read header lines up to the entirely blank line that terminates them.
The (normally blank) line that ends the headers is skipped, but not
included in the returned list. If a non-header line ends the headers,
(which is an error), an attempt is made to backspace over it; it is
never included in the returned list.
The variable self.status is set to the empty string if all went well,
otherwise it is an error message. The variable self.headers is a
completely uninterpreted list of lines contained in the header (so
printing them will reproduce the header exactly as it appears in the
file).
If multiple header fields with the same name occur, they are combined
according to the rules in RFC 2616 sec 4.2:
Appending each subsequent field-value to the first, each separated
by a comma. The order in which header fields with the same field-name
are received is significant to the interpretation of the combined
field value.
"""
# XXX The implementation overrides the readheaders() method of
# rfc822.Message. The base class design isn't amenable to
# customized behavior here so the method here is a copy of the
# base class code with a few small changes.
self.dict = {}
self.unixfrom = ''
self.headers = hlist = []
self.status = ''
headerseen = ""
firstline = 1
startofline = unread = tell = None
if hasattr(self.fp, 'unread'):
unread = self.fp.unread
elif self.seekable:
tell = self.fp.tell
while True:
if tell:
try:
startofline = tell()
except IOError:
startofline = tell = None
self.seekable = 0
line = self.fp.readline(_MAXLINE + 1)
if len(line) > _MAXLINE:
raise LineTooLong("header line")
if not line:
self.status = 'EOF in headers'
break
# Skip unix From name time lines
if firstline and line.startswith('From '):
self.unixfrom = self.unixfrom + line
continue
firstline = 0
if headerseen and line[0] in ' \t':
# XXX Not sure if continuation lines are handled properly
# for http and/or for repeating headers
# It's a continuation line.
hlist.append(line)
self.addcontinue(headerseen, line.strip())
continue
elif self.iscomment(line):
# It's a comment. Ignore it.
continue
elif self.islast(line):
# Note! No pushback here! The delimiter line gets eaten.
break
headerseen = self.isheader(line)
if headerseen:
# It's a legal header line, save it.
hlist.append(line)
self.addheader(headerseen, line[len(headerseen)+1:].strip())
continue
else:
# It's not a header line; throw it back and stop here.
if not self.dict:
self.status = 'No headers'
else:
self.status = 'Non-header line where header expected'
# Try to undo the read.
if unread:
unread(line)
elif tell:
self.fp.seek(startofline)
else:
self.status = self.status + '; bad seek'
break
class HTTPResponse:
# App Engine Note: The public interface is identical to the interface provided
# in Python 2.7 except __init__ takes a
# google.appengine.api.urlfetch.Response instance rather than a socket.
def __init__(self,
fetch_response, # App Engine Note: fetch_response was "sock".
debuglevel=0,
strict=0,
method=None,
buffering=False):
self._fetch_response = fetch_response
self.fp = cStringIO.StringIO(fetch_response.content) # For the HTTP class.
self.debuglevel = debuglevel
self.strict = strict
self._method = method
self.msg = None
# from the Status-Line of the response
self.version = _UNKNOWN # HTTP-Version
self.status = _UNKNOWN # Status-Code
self.reason = _UNKNOWN # Reason-Phrase
self.chunked = _UNKNOWN # is "chunked" being used?
self.chunk_left = _UNKNOWN # bytes left to read in current chunk
self.length = _UNKNOWN # number of bytes left in response
self.will_close = _UNKNOWN # conn will close at end of response
def begin(self):
if self.msg is not None:
# we've already started reading the response
return
self.msg = self._fetch_response.header_msg
self.version = 11 # We can't get the real HTTP version so make one up.
self.status = self._fetch_response.status_code
self.reason = responses.get(self._fetch_response.status_code, 'Unknown')
# The following are implementation details and should not be read by
# clients - but set them to reasonable values just in case.
self.chunked = 0
self.chunk_left = None
self.length = None
self.will_close = 1
def close(self):
if self.fp:
self.fp.close()
self.fp = None
def isclosed(self):
return self.fp is None
def read(self, amt=None):
if self.fp is None:
return ''
if self._method == 'HEAD':
self.close()
return ''
if amt is None:
return self.fp.read()
else:
return self.fp.read(amt)
def fileno(self):
raise NotImplementedError('fileno is not supported')
def getheader(self, name, default=None):
if self.msg is None:
raise ResponseNotReady()
return self.msg.getheader(name, default)
def getheaders(self):
"""Return list of (header, value) tuples."""
if self.msg is None:
raise ResponseNotReady()
return self.msg.items()
class HTTPConnection:
# App Engine Note: The public interface is identical to the interface provided
# in Python 2.7.2 but the implementation uses
# google.appengine.api.urlfetch. Some methods are no-ops and set_tunnel
# raises NotImplementedError.
_protocol = 'http' # passed to urlfetch.
_http_vsn = 11
_http_vsn_str = 'HTTP/1.1'
response_class = HTTPResponse
default_port = HTTP_PORT
auto_open = 1
debuglevel = 0
strict = 0
_allow_truncated = True
_follow_redirects = False
def __init__(self, host, port=None, strict=None,
timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None):
# net.proto.ProcotolBuffer relies on httplib so importing urlfetch at the
# module level causes a failure on prod. That means the import needs to be
# lazy.
from google.appengine.api import urlfetch
self._fetch = urlfetch.fetch
self._method_map = {
'GET': urlfetch.GET,
'POST': urlfetch.POST,
'HEAD': urlfetch.HEAD,
'PUT': urlfetch.PUT,
'DELETE': urlfetch.DELETE,
'PATCH': urlfetch.PATCH,
}
self.host = host
self.port = port
# With urllib2 in Python 2.6, an object can be passed here.
# The default is set to socket.GLOBAL_DEFAULT_TIMEOUT which is an object.
# We only accept float, int or long values, otherwise it can be
# silently ignored.
if not isinstance(timeout, (float, int, long)):
timeout = None
self.timeout = timeout
# Both 'strict' and 'source_address' are ignored.
self._method = self._url = None
self._body = ''
self.headers = []
def set_tunnel(self, host, port=None, headers=None):
""" Sets up the host and the port for the HTTP CONNECT Tunnelling.
The headers argument should be a mapping of extra HTTP headers
to send with the CONNECT request.
App Engine Note: This method is not supported.
"""
raise NotImplementedError('HTTP CONNECT Tunnelling is not supported')
def set_debuglevel(self, level):
pass
def connect(self):
"""Connect to the host and port specified in __init__.
App Engine Note: This method is a no-op.
"""
def close(self):
"""Close the connection to the HTTP server.
App Engine Note: This method is a no-op.
"""
def send(self, data):
"""Send `data' to the server."""
self._body += data
def putrequest(self, method, url, skip_host=0, skip_accept_encoding=0):
"""Send a request to the server.
`method' specifies an HTTP request method, e.g. 'GET'.
`url' specifies the object being requested, e.g. '/index.html'.
`skip_host' if True does not add automatically a 'Host:' header
`skip_accept_encoding' if True does not add automatically an
'Accept-Encoding:' header
App Engine Note: `skip_host' and `skip_accept_encoding' are not honored by
the urlfetch service.
"""
self._method = method
self._url = url
def putheader(self, header, *values):
"""Send a request header line to the server.
For example: h.putheader('Accept', 'text/html')
"""
hdr = '\r\n\t'.join([str(v) for v in values])
self.headers.append((header, hdr))
def endheaders(self, message_body=None):
"""Indicate that the last header line has been sent to the server.
This method sends the request to the server. The optional
message_body argument can be used to pass message body
associated with the request. The message body will be sent in
the same packet as the message headers if possible. The
message_body should be a string.
"""
if message_body is not None:
self.send(message_body)
def request(self, method, url, body=None, headers=None):
"""Send a complete request to the server."""
self._method = method
self._url = url
try: # 'body' can be a file.
self._body = body.read()
except AttributeError:
self._body = body
if headers is None:
headers = []
elif hasattr(headers, 'items'):
headers = headers.items()
self.headers = headers
@staticmethod
def _getargspec(callable_object):
assert callable(callable_object)
try:
# Methods and lambdas.
return inspect.getargspec(callable_object)
except TypeError:
# Class instances with __call__.
return inspect.getargspec(callable_object.__call__)
def getresponse(self, buffering=False):
"""Get the response from the server.
App Engine Note: buffering is ignored.
"""
# net.proto.ProcotolBuffer relies on httplib so importing urlfetch at the
# module level causes a failure on prod. That means the import needs to be
# lazy.
from google.appengine.api import urlfetch
import socket # Cannot be done at global scope due to circular import.
if self.port and self.port != self.default_port:
host = '%s:%s' % (self.host, self.port)
else:
host = self.host
if not self._url.startswith(self._protocol):
url = '%s://%s%s' % (self._protocol, host, self._url)
else:
url = self._url
headers = dict(self.headers)
if self.timeout in [_GLOBAL_DEFAULT_TIMEOUT,
socket._GLOBAL_DEFAULT_TIMEOUT]:
deadline = socket.getdefaulttimeout()
else:
deadline = self.timeout
try:
method = self._method_map[self._method.upper()]
except KeyError:
raise ValueError('%r is an unrecognized HTTP method' % self._method)
try:
# The Python Standard Library doesn't validate certificates so don't
# validate them here either. But some libraries (httplib2, possibly
# others) use an alternate technique where the fetch function does not
# have a validate_certificate argument so only provide it when supported.
argspec = self._getargspec(self._fetch)
extra_kwargs = (
{'validate_certificate': False}
if argspec.keywords or 'validate_certificate' in argspec.args
else {})
fetch_response = self._fetch(url,
self._body,
method, headers,
self._allow_truncated,
self._follow_redirects,
deadline,
**extra_kwargs)
except urlfetch.InvalidURLError, e:
raise InvalidURL(str(e))
except (urlfetch.ResponseTooLargeError, urlfetch.DeadlineExceededError), e:
raise HTTPException(str(e))
except urlfetch.SSLCertificateError, e:
# Should be ssl.SSLError but the ssl module isn't available.
# Continue to support this exception for versions of _fetch that do not
# support validate_certificates. Also, in production App Engine defers
# specific semantics so leaving this in just in case.
raise HTTPException(str(e))
except urlfetch.DownloadError, e:
# One of the following occured: UNSPECIFIED_ERROR, FETCH_ERROR
raise socket.error(
'An error occured while connecting to the server: %s' % e)
response = self.response_class(fetch_response, method=method)
response.begin()
self.close()
return response
class HTTPSConnection(HTTPConnection):
"This class allows communication via SSL."
# App Engine Note: The public interface is identical to the interface provided
# in Python 2.7.2 but the implementation does not support key and
# certificate files.
_protocol = 'https' # passed to urlfetch.
default_port = HTTPS_PORT
def __init__(self, host, port=None, key_file=None, cert_file=None,
strict=False, timeout=_GLOBAL_DEFAULT_TIMEOUT,
source_address=None):
if key_file is not None or cert_file is not None:
raise NotImplementedError(
'key_file and cert_file arguments are not implemented')
HTTPConnection.__init__(self, host, port, strict, timeout, source_address)
class HTTP:
"Compatibility class with httplib.py from 1.5."
# App Engine Note: The public interface is identical to the interface provided
# in Python 2.7.
_http_vsn = 10
_http_vsn_str = 'HTTP/1.0'
debuglevel = 0
_connection_class = HTTPConnection
def __init__(self, host='', port=None, strict=None):
"Provide a default host, since the superclass requires one."
# some joker passed 0 explicitly, meaning default port
if port == 0:
port = None
# Note that we may pass an empty string as the host; this will throw
# an error when we attempt to connect. Presumably, the client code
# will call connect before then, with a proper host.
self._setup(self._connection_class(host, port, strict))
def _setup(self, conn):
self._conn = conn
# set up delegation to flesh out interface
self.send = conn.send
self.putrequest = conn.putrequest
self.endheaders = conn.endheaders
self.set_debuglevel = conn.set_debuglevel
conn._http_vsn = self._http_vsn
conn._http_vsn_str = self._http_vsn_str
self.file = None
def connect(self, host=None, port=None):
"Accept arguments to set the host/port, since the superclass doesn't."
self.__init__(host, port)
def getfile(self):
"Provide a getfile, since the superclass' does not use this concept."
return self.file
def putheader(self, header, *values):
"The superclass allows only one value argument."
self._conn.putheader(header, '\r\n\t'.join([str(v) for v in values]))
def getreply(self, buffering=False):
"""Compat definition since superclass does not define it.
Returns a tuple consisting of:
- server status code (e.g. '200' if all goes well)
- server "reason" corresponding to status code
- any RFC822 headers in the response from the server
"""
response = self._conn.getresponse()
self.headers = response.msg
self.file = response.fp
return response.status, response.reason, response.msg
def close(self):
self._conn.close()
# note that self.file == response.fp, which gets closed by the
# superclass. just clear the object ref here.
### hmm. messy. if status==-1, then self.file is owned by us.
### well... we aren't explicitly closing, but losing this ref will
### do it
self.file = None
# Copy from Python's httplib implementation.
class HTTPS(HTTP):
"""Compatibility with 1.5 httplib interface
Python 1.5.2 did not have an HTTPS class, but it defined an
interface for sending http requests that is also useful for
https.
"""
# App Engine Note: The public interface is identical to the interface provided
# in Python 2.7 except that key and certificate files are not supported.
_connection_class = HTTPSConnection
def __init__(self, host='', port=None, key_file=None, cert_file=None,
strict=None):
if key_file is not None or cert_file is not None:
raise NotImplementedError(
'key_file and cert_file arguments are not implemented')
# 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))
# we never actually use these for anything, but we keep them
# here for compatibility with post-1.5.2 CVS.
self.key_file = key_file
self.cert_file = cert_file
class HTTPException(Exception):
# App Engine Note: This class has been copied unchanged from Python 2.7.2
# Subclasses that define an __init__ must call Exception.__init__
# or define self.args. Otherwise, str() will fail.
pass
class NotConnected(HTTPException):
# App Engine Note: This class has been copied unchanged from Python 2.7.2
pass
class InvalidURL(HTTPException):
# App Engine Note: This class has been copied unchanged from Python 2.7.2
pass
class UnknownProtocol(HTTPException):
# App Engine Note: This class has been copied unchanged from Python 2.7.2
def __init__(self, version):
self.args = version,
self.version = version
class UnknownTransferEncoding(HTTPException):
# App Engine Note: This class has been copied unchanged from Python 2.7.2
pass
class UnimplementedFileMode(HTTPException):
# App Engine Note: This class has been copied unchanged from Python 2.7.2
pass
class IncompleteRead(HTTPException):
# App Engine Note: This class has been copied unchanged from Python 2.7.2
def __init__(self, partial, expected=None):
self.args = partial,
self.partial = partial
self.expected = expected
def __repr__(self):
if self.expected is not None:
e = ', %i more expected' % self.expected
else:
e = ''
return 'IncompleteRead(%i bytes read%s)' % (len(self.partial), e)
def __str__(self):
return repr(self)
class ImproperConnectionState(HTTPException):
# App Engine Note: This class has been copied unchanged from Python 2.7.2
pass
class CannotSendRequest(ImproperConnectionState):
# App Engine Note: This class has been copied unchanged from Python 2.7.2
pass
class CannotSendHeader(ImproperConnectionState):
# App Engine Note: This class has been copied unchanged from Python 2.7.2
pass
class ResponseNotReady(ImproperConnectionState):
# App Engine Note: This class has been copied unchanged from Python 2.7.2
pass
class BadStatusLine(HTTPException):
# App Engine Note: This class has been copied unchanged from Python 2.7.2
def __init__(self, line):
if not line:
line = repr(line)
self.args = line,
self.line = line
class LineTooLong(HTTPException):
# App Engine Note: This class has been copied unchanged from Python 2.7.2
def __init__(self, line_type):
HTTPException.__init__(self, "got more than %d bytes when reading %s"
% (_MAXLINE, line_type))
# for backwards compatibility
error = HTTPException
class LineAndFileWrapper:
"""A limited file-like object for HTTP/0.9 responses."""
# App Engine Note: This class has been copied unchanged from Python 2.7.2
# The status-line parsing code calls readline(), which normally
# get the HTTP status line. For a 0.9 response, however, this is
# actually the first line of the body! Clients need to get a
# readable file object that contains that line.
def __init__(self, line, file):
self._line = line
self._file = file
self._line_consumed = 0
self._line_offset = 0
self._line_left = len(line)
def __getattr__(self, attr):
return getattr(self._file, attr)
def _done(self):
# called when the last byte is read from the line. After the
# call, all read methods are delegated to the underlying file
# object.
self._line_consumed = 1
self.read = self._file.read
self.readline = self._file.readline
self.readlines = self._file.readlines
def read(self, amt=None):
if self._line_consumed:
return self._file.read(amt)
assert self._line_left
if amt is None or amt > self._line_left:
s = self._line[self._line_offset:]
self._done()
if amt is None:
return s + self._file.read()
else:
return s + self._file.read(amt - len(s))
else:
assert amt <= self._line_left
i = self._line_offset
j = i + amt
s = self._line[i:j]
self._line_offset = j
self._line_left -= amt
if self._line_left == 0:
self._done()
return s
def readline(self):
if self._line_consumed:
return self._file.readline()
assert self._line_left
s = self._line[self._line_offset:]
self._done()
return s
def readlines(self, size=None):
if self._line_consumed:
return self._file.readlines(size)
assert self._line_left
L = [self._line[self._line_offset:]]
self._done()
if size is None:
return L + self._file.readlines()
else:
return L + self._file.readlines(size)