diff --git a/README.md b/README.md index 17110a4d..8aab1a0e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Proxy.Py](ProxyPy.png)](https://github.com/abhinavsingh/proxy.py) -[![alt text](https://travis-ci.org/abhinavsingh/proxy.py.svg?branch=develop "Build Status")](https://travis-ci.org/abhinavsingh/proxy.py/) [![Coverage Status](https://coveralls.io/repos/github/abhinavsingh/proxy.py/badge.svg?branch=develop)](https://coveralls.io/github/abhinavsingh/proxy.py?branch=develop) +[![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![alt text](https://travis-ci.org/abhinavsingh/proxy.py.svg?branch=develop "Build Status")](https://travis-ci.org/abhinavsingh/proxy.py/) [![Coverage Status](https://coveralls.io/repos/github/abhinavsingh/proxy.py/badge.svg?branch=develop)](https://coveralls.io/github/abhinavsingh/proxy.py?branch=develop) Features -------- @@ -35,7 +35,8 @@ usage: proxy.py [-h] [--backlog BACKLOG] [--basic-auth BASIC_AUTH] [--client-recvbuf-size CLIENT_RECVBUF_SIZE] [--hostname HOSTNAME] [--ipv4] [--enable-http-proxy] [--enable-web-server] [--log-level LOG_LEVEL] - [--log-format LOG_FORMAT] [--num-workers NUM_WORKERS] + [--log-file LOG_FILE] [--log-format LOG_FORMAT] + [--num-workers NUM_WORKERS] [--open-file-limit OPEN_FILE_LIMIT] [--pac-file PAC_FILE] [--pac-file-url-path PAC_FILE_URL_PATH] [--plugins PLUGINS] [--port PORT] [--server-recvbuf-size SERVER_RECVBUF_SIZE] @@ -51,7 +52,7 @@ optional arguments: Default: No authentication. Specify colon separated user:password to enable basic authentication. --client-recvbuf-size CLIENT_RECVBUF_SIZE - Default: 8 KB. Maximum amount of data received from + Default: 1 MB. Maximum amount of data received from the client in a single recv() operation. Bump this value for faster uploads at the expense of increased RAM. @@ -67,6 +68,7 @@ optional arguments: CRITICAL. Both upper and lowercase values are allowed.You may also simply use the leading character e.g. --log-level d + --log-file LOG_FILE Default: sys.stdout. Log file destination. --log-format LOG_FORMAT Log format for Python logger. --num-workers NUM_WORKERS @@ -81,7 +83,7 @@ optional arguments: --plugins PLUGINS Comma separated plugins --port PORT Default: 8899. Server port. --server-recvbuf-size SERVER_RECVBUF_SIZE - Default: 8 KB. Maximum amount of data received from + Default: 1 MB. Maximum amount of data received from the server in a single recv() operation. Bump this value for faster downloads at the expense of increased RAM. diff --git a/proxy.py b/proxy.py index c095f98c..485f4442 100755 --- a/proxy.py +++ b/proxy.py @@ -60,6 +60,7 @@ DEFAULT_NUM_WORKERS = 0 DEFAULT_PLUGINS = {} DEFAULT_VERSION = False DEFAULT_LOG_FORMAT = '%(asctime)s - %(levelname)s - pid:%(process)d - %(funcName)s:%(lineno)d - %(message)s' +DEFAULT_LOG_FILE = None # Set to True if under test UNDER_TEST = False @@ -1124,7 +1125,7 @@ def init_parser() -> argparse.ArgumentParser: help='Default: No authentication. Specify colon separated user:password ' 'to enable basic authentication.') parser.add_argument('--client-recvbuf-size', type=int, default=DEFAULT_CLIENT_RECVBUF_SIZE, - help='Default: 8 KB. Maximum amount of data received from the ' + help='Default: 1 MB. Maximum amount of data received from the ' 'client in a single recv() operation. Bump this ' 'value for faster uploads at the expense of ' 'increased RAM.') @@ -1141,6 +1142,8 @@ def init_parser() -> argparse.ArgumentParser: help='Valid options: DEBUG, INFO (default), WARNING, ERROR, CRITICAL. ' 'Both upper and lowercase values are allowed.' 'You may also simply use the leading character e.g. --log-level d') + parser.add_argument('--log-file', type=str, default=DEFAULT_LOG_FILE, + help='Default: sys.stdout. Log file destination.') parser.add_argument('--log-format', type=str, default=DEFAULT_LOG_FORMAT, help='Log format for Python logger.') parser.add_argument('--num-workers', type=int, default=DEFAULT_NUM_WORKERS, @@ -1157,7 +1160,7 @@ def init_parser() -> argparse.ArgumentParser: parser.add_argument('--port', type=int, default=DEFAULT_PORT, help='Default: 8899. Server port.') parser.add_argument('--server-recvbuf-size', type=int, default=DEFAULT_SERVER_RECVBUF_SIZE, - help='Default: 8 KB. Maximum amount of data received from the ' + help='Default: 1 MB. Maximum amount of data received from the ' 'server in a single recv() operation. Bump this ' 'value for faster downloads at the expense of ' 'increased RAM.') @@ -1186,14 +1189,17 @@ def main(args): sys.exit(0) try: - logging.basicConfig(level=getattr( + log_level = getattr( logging, {'D': 'DEBUG', 'I': 'INFO', 'W': 'WARNING', 'E': 'ERROR', - 'C': 'CRITICAL'}[args.log_level.upper()[0]]), - format=args.log_format) + 'C': 'CRITICAL'}[args.log_level.upper()[0]]) + if args.log_file: + logging.basicConfig(filename=args.log_file, filemode='a', level=log_level, format=args.log_format) + else: + logging.basicConfig(level=log_level, format=args.log_format) set_open_file_limit(args.open_file_limit) diff --git a/tests.py b/tests.py index 1a79ffde..ba77439c 100644 --- a/tests.py +++ b/tests.py @@ -15,6 +15,8 @@ import os import socket import time import unittest +import errno +import proxy from contextlib import closing from http.server import HTTPServer, BaseHTTPRequestHandler from threading import Thread @@ -23,8 +25,6 @@ from unittest import mock if os.name != 'nt': import resource -import proxy - logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s') @@ -36,6 +36,58 @@ def get_available_port(): return port +class TestTcpConnection(unittest.TestCase): + + def testHandlesIOError(self): + self.conn = proxy.TcpConnection(proxy.TcpConnection.types.CLIENT) + _conn = mock.MagicMock() + _conn.recv.side_effect = IOError() + self.conn.conn = _conn + with mock.patch('proxy.logger') as mock_logger: + self.conn.recv() + mock_logger.exception.assert_called() + logging.info(mock_logger.exception.call_args[0][0].startswith('Exception while receiving from connection')) + + def testHandlesConnReset(self): + self.conn = proxy.TcpConnection(proxy.TcpConnection.types.CLIENT) + _conn = mock.MagicMock() + e = IOError() + e.errno = errno.ECONNRESET + _conn.recv.side_effect = e + self.conn.conn = _conn + with mock.patch('proxy.logger') as mock_logger: + self.conn.recv() + mock_logger.exception.assert_not_called() + mock_logger.debug.assert_called() + self.assertEqual(mock_logger.debug.call_args[0][0], '%r' % e) + + def testClosesIfNotClosed(self): + self.conn = proxy.TcpConnection(proxy.TcpConnection.types.CLIENT) + _conn = mock.MagicMock() + self.conn.conn = _conn + self.conn.close() + _conn.close.assert_called() + self.assertTrue(self.conn.closed) + + def testNoOpIfAlreadyClosed(self): + self.conn = proxy.TcpConnection(proxy.TcpConnection.types.CLIENT) + _conn = mock.MagicMock() + self.conn.conn = _conn + self.conn.closed = True + self.conn.close() + _conn.close.assert_not_called() + self.assertTrue(self.conn.closed) + + @mock.patch('socket.create_connection') + def testTcpServerClosesConnOnGC(self, mock_create_connection): + conn = mock.MagicMock() + mock_create_connection.return_value = conn + self.conn = proxy.TcpServerConnection(proxy.DEFAULT_IPV4_HOSTNAME, proxy.DEFAULT_PORT) + self.conn.connect() + del self.conn + conn.close.assert_called() + + @unittest.skipIf(os.getenv('TESTING_ON_TRAVIS', 0), 'Opening sockets not allowed on Travis') class TestTcpServer(unittest.TestCase): ipv4_port = None @@ -774,6 +826,34 @@ class TestWorker(unittest.TestCase): self.assertTrue(mock_http_proxy.called) +class TestHttpRequestRejected(unittest.TestCase): + + def setUp(self): + self.request = proxy.HttpParser(proxy.HttpParser.types.REQUEST_PARSER) + + def test_empty_response(self): + e = proxy.HttpRequestRejected() + self.assertEqual(e.response(self.request), None) + + def test_status_code_response(self): + e = proxy.HttpRequestRejected(status_code=b'200 OK') + self.assertEqual(e.response(self.request), proxy.CRLF.join([ + b'HTTP/1.1 200 OK', + proxy.PROXY_AGENT_HEADER, + proxy.CRLF + ])) + + def test_body_response(self): + e = proxy.HttpRequestRejected(status_code=b'404 NOT FOUND', body=b'Nothing here') + self.assertEqual(e.response(self.request), proxy.CRLF.join([ + b'HTTP/1.1 404 NOT FOUND', + proxy.PROXY_AGENT_HEADER, + b'Content-Length: 12', + proxy.CRLF, + b'Nothing here' + ])) + + class TestMain(unittest.TestCase): @mock.patch('proxy.HttpProtocolConfig') @@ -809,7 +889,7 @@ class TestMain(unittest.TestCase): mock_set_open_file_limit.assert_not_called() mock_config.assert_not_called() - @unittest.skipIf(True, 'For some reason this test passes when running in Intellij but fails via CLI :(') + @unittest.skipIf(False, 'For some reason this test passes when running with Intellij but fails via CLI :(') @mock.patch('builtins.print') @mock.patch('proxy.HttpProtocolConfig') @mock.patch('proxy.set_open_file_limit') @@ -821,7 +901,7 @@ class TestMain(unittest.TestCase): with self.assertRaises(SystemExit): proxy.main([]) mock_version.assert_called() - logging.info(mock_print.call_args.startswith('DEPRECATION')) + self.assertTrue(mock_print.call_args.startswith('DEPRECATION')) mock_multicore_dispatcher.assert_not_called() mock_set_open_file_limit.assert_not_called() mock_config.assert_not_called() @@ -878,34 +958,6 @@ class TestMain(unittest.TestCase): mock_set_rlimit.assert_not_called() -class TestHttpRequestRejected(unittest.TestCase): - - def setUp(self): - self.request = proxy.HttpParser(proxy.HttpParser.types.REQUEST_PARSER) - - def test_empty_response(self): - e = proxy.HttpRequestRejected() - self.assertEqual(e.response(self.request), None) - - def test_status_code_response(self): - e = proxy.HttpRequestRejected(status_code=b'200 OK') - self.assertEqual(e.response(self.request), proxy.CRLF.join([ - b'HTTP/1.1 200 OK', - proxy.PROXY_AGENT_HEADER, - proxy.CRLF - ])) - - def test_body_response(self): - e = proxy.HttpRequestRejected(status_code=b'404 NOT FOUND', body=b'Nothing here') - self.assertEqual(e.response(self.request), proxy.CRLF.join([ - b'HTTP/1.1 404 NOT FOUND', - proxy.PROXY_AGENT_HEADER, - b'Content-Length: 12', - proxy.CRLF, - b'Nothing here' - ])) - - if __name__ == '__main__': proxy.UNDER_TEST = True unittest.main()