diff options
author | SVN-Git Migration <python-modules-team@lists.alioth.debian.org> | 2015-10-08 13:19:35 -0700 |
---|---|---|
committer | SVN-Git Migration <python-modules-team@lists.alioth.debian.org> | 2015-10-08 13:19:35 -0700 |
commit | 52980ebd0a4eb75acf055a2256e095772c1fa7c6 (patch) | |
tree | f339a7e66934caa7948c15e8c5bd3ea04ee72e5a /dummyserver | |
parent | 92b84b67f7b187b81dacbf1ae46d59a1d0b5b125 (diff) | |
download | python-urllib3-52980ebd0a4eb75acf055a2256e095772c1fa7c6.tar python-urllib3-52980ebd0a4eb75acf055a2256e095772c1fa7c6.tar.gz |
Imported Upstream version 1.7.1
Diffstat (limited to 'dummyserver')
-rw-r--r-- | dummyserver/handlers.py | 57 | ||||
-rwxr-xr-x | dummyserver/proxy.py | 137 | ||||
-rwxr-xr-x | dummyserver/server.py | 50 | ||||
-rw-r--r-- | dummyserver/testcase.py | 86 |
4 files changed, 286 insertions, 44 deletions
diff --git a/dummyserver/handlers.py b/dummyserver/handlers.py index ab48b53..bc51f31 100644 --- a/dummyserver/handlers.py +++ b/dummyserver/handlers.py @@ -87,7 +87,7 @@ class TestingApp(WSGIHandler): if request.method != method: return Response("Wrong method: %s != %s" % - (method, request.method), status='400') + (method, request.method), status='400 Bad Request') return Response() def upload(self, request): @@ -100,17 +100,18 @@ class TestingApp(WSGIHandler): if len(files_) != 1: return Response("Expected 1 file for '%s', not %d" %(param, len(files_)), - status='400') + status='400 Bad Request') file_ = files_[0] data = file_['body'] if int(size) != len(data): return Response("Wrong size: %d != %d" % - (size, len(data)), status='400') + (size, len(data)), status='400 Bad Request') if filename != file_['filename']: return Response("Wrong filename: %s != %s" % - (filename, file_.filename), status='400') + (filename, file_.filename), + status='400 Bad Request') return Response() @@ -118,7 +119,7 @@ class TestingApp(WSGIHandler): "Perform a redirect to ``target``" target = request.params.get('target', '/') headers = [('Location', target)] - return Response(status='303', headers=headers) + return Response(status='303 See Other', headers=headers) def keepalive(self, request): if request.params.get('close', b'0') == b'1': @@ -169,3 +170,49 @@ class TestingApp(WSGIHandler): def shutdown(self, request): sys.exit() + + +# RFC2231-aware replacement of internal tornado function +def _parse_header(line): + r"""Parse a Content-type like header. + + Return the main content-type and a dictionary of options. + + >>> d = _parse_header("CD: fd; foo=\"bar\"; file*=utf-8''T%C3%A4st")[1] + >>> d['file'] == 'T\u00e4st' + True + >>> d['foo'] + 'bar' + """ + import tornado.httputil + import email.utils + from urllib3.packages import six + if not six.PY3: + line = line.encode('utf-8') + parts = tornado.httputil._parseparam(';' + line) + key = next(parts) + # decode_params treats first argument special, but we already stripped key + params = [('Dummy', 'value')] + for p in parts: + i = p.find('=') + if i >= 0: + name = p[:i].strip().lower() + value = p[i + 1:].strip() + params.append((name, value)) + params = email.utils.decode_params(params) + params.pop(0) # get rid of the dummy again + pdict = {} + for name, value in params: + print(repr(value)) + value = email.utils.collapse_rfc2231_value(value) + if len(value) >= 2 and value[0] == '"' and value[-1] == '"': + value = value[1:-1] + pdict[name] = value + return key, pdict + +# TODO: make the following conditional as soon as we know a version +# which does not require this fix. +# See https://github.com/facebook/tornado/issues/868 +if True: + import tornado.httputil + tornado.httputil._parse_header = _parse_header diff --git a/dummyserver/proxy.py b/dummyserver/proxy.py new file mode 100755 index 0000000..aca92a7 --- /dev/null +++ b/dummyserver/proxy.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# +# Simple asynchronous HTTP proxy with tunnelling (CONNECT). +# +# GET/POST proxying based on +# http://groups.google.com/group/python-tornado/msg/7bea08e7a049cf26 +# +# Copyright (C) 2012 Senko Rasic <senko.rasic@dobarkod.hr> +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import sys +import socket + +import tornado.httpserver +import tornado.ioloop +import tornado.iostream +import tornado.web +import tornado.httpclient + +__all__ = ['ProxyHandler', 'run_proxy'] + + +class ProxyHandler(tornado.web.RequestHandler): + SUPPORTED_METHODS = ['GET', 'POST', 'CONNECT'] + + @tornado.web.asynchronous + def get(self): + + def handle_response(response): + if response.error and not isinstance(response.error, + tornado.httpclient.HTTPError): + self.set_status(500) + self.write('Internal server error:\n' + str(response.error)) + self.finish() + else: + self.set_status(response.code) + for header in ('Date', 'Cache-Control', 'Server', + 'Content-Type', 'Location'): + v = response.headers.get(header) + if v: + self.set_header(header, v) + if response.body: + self.write(response.body) + self.finish() + + req = tornado.httpclient.HTTPRequest(url=self.request.uri, + method=self.request.method, body=self.request.body, + headers=self.request.headers, follow_redirects=False, + allow_nonstandard_methods=True) + + client = tornado.httpclient.AsyncHTTPClient() + try: + client.fetch(req, handle_response) + except tornado.httpclient.HTTPError as e: + if hasattr(e, 'response') and e.response: + self.handle_response(e.response) + else: + self.set_status(500) + self.write('Internal server error:\n' + str(e)) + self.finish() + + @tornado.web.asynchronous + def post(self): + return self.get() + + @tornado.web.asynchronous + def connect(self): + host, port = self.request.uri.split(':') + client = self.request.connection.stream + + def read_from_client(data): + upstream.write(data) + + def read_from_upstream(data): + client.write(data) + + def client_close(data=None): + if upstream.closed(): + return + if data: + upstream.write(data) + upstream.close() + + def upstream_close(data=None): + if client.closed(): + return + if data: + client.write(data) + client.close() + + def start_tunnel(): + client.read_until_close(client_close, read_from_client) + upstream.read_until_close(upstream_close, read_from_upstream) + client.write(b'HTTP/1.0 200 Connection established\r\n\r\n') + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + upstream = tornado.iostream.IOStream(s) + upstream.connect((host, int(port)), start_tunnel) + + +def run_proxy(port, start_ioloop=True): + """ + Run proxy on the specified port. If start_ioloop is True (default), + the tornado IOLoop will be started immediately. + """ + app = tornado.web.Application([ + (r'.*', ProxyHandler), + ]) + app.listen(port) + ioloop = tornado.ioloop.IOLoop.instance() + if start_ioloop: + ioloop.start() + +if __name__ == '__main__': + port = 8888 + if len(sys.argv) > 1: + port = int(sys.argv[1]) + + print ("Starting HTTP proxy on port %d" % port) + run_proxy(port) diff --git a/dummyserver/server.py b/dummyserver/server.py index 9031664..f4f98a4 100755 --- a/dummyserver/server.py +++ b/dummyserver/server.py @@ -11,11 +11,14 @@ import sys import threading import socket +from tornado import netutil import tornado.wsgi import tornado.httpserver import tornado.ioloop +import tornado.web from dummyserver.handlers import TestingApp +from dummyserver.proxy import ProxyHandler log = logging.getLogger(__name__) @@ -36,28 +39,29 @@ class SocketServerThread(threading.Thread): """ :param socket_handler: Callable which receives a socket argument for one request. - :param ready_lock: Lock which gets released when the socket handler is + :param ready_event: Event which gets set when the socket handler is ready to receive requests. """ def __init__(self, socket_handler, host='localhost', port=8081, - ready_lock=None): + ready_event=None): threading.Thread.__init__(self) self.socket_handler = socket_handler self.host = host - self.port = port - self.ready_lock = ready_lock + self.ready_event = ready_event def _start_server(self): sock = socket.socket() - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind((self.host, self.port)) + if sys.platform != 'win32': + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind((self.host, 0)) + self.port = sock.getsockname()[1] # Once listen() returns, the server socket is ready sock.listen(1) - if self.ready_lock: - self.ready_lock.release() + if self.ready_event: + self.ready_event.set() self.socket_handler(sock) sock.close() @@ -67,34 +71,44 @@ class SocketServerThread(threading.Thread): class TornadoServerThread(threading.Thread): - def __init__(self, host='localhost', port=8081, scheme='http', certs=None): + app = tornado.wsgi.WSGIContainer(TestingApp()) + + def __init__(self, host='localhost', scheme='http', certs=None, + ready_event=None): threading.Thread.__init__(self) self.host = host - self.port = port self.scheme = scheme self.certs = certs + self.ready_event = ready_event def _start_server(self): - container = tornado.wsgi.WSGIContainer(TestingApp()) - if self.scheme == 'https': - http_server = tornado.httpserver.HTTPServer(container, + http_server = tornado.httpserver.HTTPServer(self.app, ssl_options=self.certs) else: - http_server = tornado.httpserver.HTTPServer(container) + http_server = tornado.httpserver.HTTPServer(self.app) - http_server.listen(self.port, address=self.host) + family = socket.AF_INET6 if ':' in self.host else socket.AF_INET + sock, = netutil.bind_sockets(None, address=self.host, family=family) + self.port = sock.getsockname()[1] + http_server.add_sockets([sock]) return http_server def run(self): - self.server = self._start_server() self.ioloop = tornado.ioloop.IOLoop.instance() + self.server = self._start_server() + if self.ready_event: + self.ready_event.set() self.ioloop.start() def stop(self): - self.server.stop() - self.ioloop.stop() + self.ioloop.add_callback(self.server.stop) + self.ioloop.add_callback(self.ioloop.stop) + + +class ProxyServerThread(TornadoServerThread): + app = tornado.web.Application([(r'.*', ProxyHandler)]) if __name__ == '__main__': diff --git a/dummyserver/testcase.py b/dummyserver/testcase.py index 73b8f2f..a2a1da1 100644 --- a/dummyserver/testcase.py +++ b/dummyserver/testcase.py @@ -1,14 +1,15 @@ import unittest - -from threading import Lock +import socket +import threading +from nose.plugins.skip import SkipTest from dummyserver.server import ( TornadoServerThread, SocketServerThread, DEFAULT_CERTS, + ProxyServerThread, ) - -# TODO: Change ports to auto-allocated? +has_ipv6 = hasattr(socket, 'has_ipv6') class SocketDummyServerTestCase(unittest.TestCase): @@ -18,19 +19,16 @@ class SocketDummyServerTestCase(unittest.TestCase): """ scheme = 'http' host = 'localhost' - port = 18080 @classmethod def _start_server(cls, socket_handler): - ready_lock = Lock() - ready_lock.acquire() + ready_event = threading.Event() cls.server_thread = SocketServerThread(socket_handler=socket_handler, - ready_lock=ready_lock, - host=cls.host, port=cls.port) + ready_event=ready_event, + host=cls.host) cls.server_thread.start() - - # Lock gets released by thread above - ready_lock.acquire() + ready_event.wait() + cls.port = cls.server_thread.port @classmethod def tearDownClass(cls): @@ -41,20 +39,19 @@ class SocketDummyServerTestCase(unittest.TestCase): class HTTPDummyServerTestCase(unittest.TestCase): scheme = 'http' host = 'localhost' - host_alt = '127.0.0.1' # Some tests need two hosts - port = 18081 + host_alt = '127.0.0.1' # Some tests need two hosts certs = DEFAULT_CERTS @classmethod def _start_server(cls): - cls.server_thread = TornadoServerThread(host=cls.host, port=cls.port, + ready_event = threading.Event() + cls.server_thread = TornadoServerThread(host=cls.host, scheme=cls.scheme, - certs=cls.certs) + certs=cls.certs, + ready_event=ready_event) cls.server_thread.start() - - # TODO: Loop-check here instead - import time - time.sleep(0.1) + ready_event.wait() + cls.port = cls.server_thread.port @classmethod def _stop_server(cls): @@ -73,5 +70,52 @@ class HTTPDummyServerTestCase(unittest.TestCase): class HTTPSDummyServerTestCase(HTTPDummyServerTestCase): scheme = 'https' host = 'localhost' - port = 18082 certs = DEFAULT_CERTS + + +class HTTPDummyProxyTestCase(unittest.TestCase): + + http_host = 'localhost' + http_host_alt = '127.0.0.1' + + https_host = 'localhost' + https_host_alt = '127.0.0.1' + https_certs = DEFAULT_CERTS + + proxy_host = 'localhost' + proxy_host_alt = '127.0.0.1' + + @classmethod + def setUpClass(cls): + cls.http_thread = TornadoServerThread(host=cls.http_host, + scheme='http') + cls.http_thread._start_server() + cls.http_port = cls.http_thread.port + + cls.https_thread = TornadoServerThread( + host=cls.https_host, scheme='https', certs=cls.https_certs) + cls.https_thread._start_server() + cls.https_port = cls.https_thread.port + + ready_event = threading.Event() + cls.proxy_thread = ProxyServerThread( + host=cls.proxy_host, ready_event=ready_event) + cls.proxy_thread.start() + ready_event.wait() + cls.proxy_port = cls.proxy_thread.port + + @classmethod + def tearDownClass(cls): + cls.proxy_thread.stop() + cls.proxy_thread.join() + + +class IPv6HTTPDummyServerTestCase(HTTPDummyServerTestCase): + host = '::1' + + @classmethod + def setUpClass(cls): + if not has_ipv6: + raise SkipTest('IPv6 not available') + else: + super(IPv6HTTPDummyServerTestCase, cls).setUpClass() |