proxy.py/tests/http/test_protocol_handler.py

355 lines
14 KiB
Python

# -*- coding: utf-8 -*-
"""
proxy.py
~~~~~~~~
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
Network monitoring, controls & Application development, testing, debugging.
:copyright: (c) 2013-present by Abhinav Singh and contributors.
:license: BSD, see LICENSE for more details.
"""
import unittest
import selectors
import base64
from typing import cast
from unittest import mock
from proxy.common.version import __version__
from proxy.common.flags import Flags
from proxy.common.utils import bytes_
from proxy.common.constants import CRLF
from proxy.core.connection import TcpClientConnection
from proxy.http.parser import HttpParser
from proxy.http.proxy import HttpProxyPlugin
from proxy.http.parser import httpParserStates, httpParserTypes
from proxy.http.exception import ProxyAuthenticationFailed, ProxyConnectionFailed
from proxy.http.handler import HttpProtocolHandler
class TestHttpProtocolHandler(unittest.TestCase):
@mock.patch('selectors.DefaultSelector')
@mock.patch('socket.fromfd')
def setUp(self,
mock_fromfd: mock.Mock,
mock_selector: mock.Mock) -> None:
self.fileno = 10
self._addr = ('127.0.0.1', 54382)
self._conn = mock_fromfd.return_value
self.http_server_port = 65535
self.flags = Flags()
self.flags.plugins = Flags.load_plugins([
b'proxy.http.proxy.HttpProxyPlugin',
b'proxy.http.server.HttpWebServerPlugin',
])
self.mock_selector = mock_selector
self.protocol_handler = HttpProtocolHandler(
TcpClientConnection(self._conn, self._addr), flags=self.flags)
self.protocol_handler.initialize()
@mock.patch('proxy.http.proxy.server.TcpServerConnection')
def test_http_get(self, mock_server_connection: mock.Mock) -> None:
server = mock_server_connection.return_value
server.connect.return_value = True
server.buffer_size.return_value = 0
self.mock_selector_for_client_read_read_server_write(
self.mock_selector, server)
# Send request line
assert self.http_server_port is not None
self._conn.recv.return_value = (b'GET http://localhost:%d HTTP/1.1' %
self.http_server_port) + CRLF
self.protocol_handler.run_once()
self.assertEqual(
self.protocol_handler.request.state,
httpParserStates.LINE_RCVD)
self.assertNotEqual(
self.protocol_handler.request.state,
httpParserStates.COMPLETE)
# Send headers and blank line, thus completing HTTP request
assert self.http_server_port is not None
self._conn.recv.return_value = CRLF.join([
b'User-Agent: proxy.py/%s' % bytes_(__version__),
b'Host: localhost:%d' % self.http_server_port,
b'Accept: */*',
b'Proxy-Connection: Keep-Alive',
CRLF
])
self.assert_data_queued(mock_server_connection, server)
self.protocol_handler.run_once()
server.flush.assert_called_once()
def assert_tunnel_response(
self, mock_server_connection: mock.Mock, server: mock.Mock) -> None:
self.protocol_handler.run_once()
self.assertTrue(
cast(HttpProxyPlugin, self.protocol_handler.plugins['HttpProxyPlugin']).server is not None)
self.assertEqual(
self.protocol_handler.client.buffer[0],
HttpProxyPlugin.PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT)
mock_server_connection.assert_called_once()
server.connect.assert_called_once()
server.queue.assert_not_called()
server.closed = False
parser = HttpParser(httpParserTypes.RESPONSE_PARSER)
parser.parse(self.protocol_handler.client.buffer[0].tobytes())
self.assertEqual(parser.state, httpParserStates.COMPLETE)
assert parser.code is not None
self.assertEqual(int(parser.code), 200)
@mock.patch('proxy.http.proxy.server.TcpServerConnection')
def test_http_tunnel(self, mock_server_connection: mock.Mock) -> None:
server = mock_server_connection.return_value
server.connect.return_value = True
def has_buffer() -> bool:
return cast(bool, server.queue.called)
server.has_buffer.side_effect = has_buffer
self.mock_selector.return_value.select.side_effect = [
[(selectors.SelectorKey(
fileobj=self._conn,
fd=self._conn.fileno,
events=selectors.EVENT_READ,
data=None), selectors.EVENT_READ), ],
[(selectors.SelectorKey(
fileobj=self._conn,
fd=self._conn.fileno,
events=0,
data=None), selectors.EVENT_WRITE), ],
[(selectors.SelectorKey(
fileobj=self._conn,
fd=self._conn.fileno,
events=selectors.EVENT_READ,
data=None), selectors.EVENT_READ), ],
[(selectors.SelectorKey(
fileobj=server.connection,
fd=server.connection.fileno,
events=0,
data=None), selectors.EVENT_WRITE), ],
]
assert self.http_server_port is not None
self._conn.recv.return_value = CRLF.join([
b'CONNECT localhost:%d HTTP/1.1' % self.http_server_port,
b'Host: localhost:%d' % self.http_server_port,
b'User-Agent: proxy.py/%s' % bytes_(__version__),
b'Proxy-Connection: Keep-Alive',
CRLF
])
self.assert_tunnel_response(mock_server_connection, server)
# Dispatch tunnel established response to client
self.protocol_handler.run_once()
self.assert_data_queued_to_server(server)
self.protocol_handler.run_once()
self.assertEqual(server.queue.call_count, 1)
server.flush.assert_called_once()
def test_proxy_connection_failed(self) -> None:
self.mock_selector_for_client_read(self.mock_selector)
self._conn.recv.return_value = CRLF.join([
b'GET http://unknown.domain HTTP/1.1',
b'Host: unknown.domain',
CRLF
])
self.protocol_handler.run_once()
self.assertEqual(
self.protocol_handler.client.buffer[0],
ProxyConnectionFailed.RESPONSE_PKT)
@mock.patch('selectors.DefaultSelector')
@mock.patch('socket.fromfd')
def test_proxy_authentication_failed(
self,
mock_fromfd: mock.Mock,
mock_selector: mock.Mock) -> None:
self._conn = mock_fromfd.return_value
self.mock_selector_for_client_read(mock_selector)
flags = Flags(
auth_code=b'Basic %s' %
base64.b64encode(b'user:pass'))
flags.plugins = Flags.load_plugins([
b'proxy.http.proxy.HttpProxyPlugin',
b'proxy.http.server.HttpWebServerPlugin',
])
self.protocol_handler = HttpProtocolHandler(
TcpClientConnection(self._conn, self._addr), flags=flags)
self.protocol_handler.initialize()
self._conn.recv.return_value = CRLF.join([
b'GET http://abhinavsingh.com HTTP/1.1',
b'Host: abhinavsingh.com',
CRLF
])
self.protocol_handler.run_once()
self.assertEqual(
self.protocol_handler.client.buffer[0],
ProxyAuthenticationFailed.RESPONSE_PKT)
@mock.patch('selectors.DefaultSelector')
@mock.patch('socket.fromfd')
@mock.patch('proxy.http.proxy.server.TcpServerConnection')
def test_authenticated_proxy_http_get(
self, mock_server_connection: mock.Mock,
mock_fromfd: mock.Mock,
mock_selector: mock.Mock) -> None:
self._conn = mock_fromfd.return_value
self.mock_selector_for_client_read(mock_selector)
server = mock_server_connection.return_value
server.connect.return_value = True
server.buffer_size.return_value = 0
flags = Flags(
auth_code=b'Basic %s' %
base64.b64encode(b'user:pass'))
flags.plugins = Flags.load_plugins([
b'proxy.http.proxy.HttpProxyPlugin',
b'proxy.http.server.HttpWebServerPlugin',
])
self.protocol_handler = HttpProtocolHandler(
TcpClientConnection(self._conn, self._addr), flags=flags)
self.protocol_handler.initialize()
assert self.http_server_port is not None
self._conn.recv.return_value = b'GET http://localhost:%d HTTP/1.1' % self.http_server_port
self.protocol_handler.run_once()
self.assertEqual(
self.protocol_handler.request.state,
httpParserStates.INITIALIZED)
self._conn.recv.return_value = CRLF
self.protocol_handler.run_once()
self.assertEqual(
self.protocol_handler.request.state,
httpParserStates.LINE_RCVD)
assert self.http_server_port is not None
self._conn.recv.return_value = CRLF.join([
b'User-Agent: proxy.py/%s' % bytes_(__version__),
b'Host: localhost:%d' % self.http_server_port,
b'Accept: */*',
b'Proxy-Connection: Keep-Alive',
b'Proxy-Authorization: Basic dXNlcjpwYXNz',
CRLF
])
self.assert_data_queued(mock_server_connection, server)
@mock.patch('selectors.DefaultSelector')
@mock.patch('socket.fromfd')
@mock.patch('proxy.http.proxy.server.TcpServerConnection')
def test_authenticated_proxy_http_tunnel(
self, mock_server_connection: mock.Mock,
mock_fromfd: mock.Mock,
mock_selector: mock.Mock) -> None:
server = mock_server_connection.return_value
server.connect.return_value = True
server.buffer_size.return_value = 0
self._conn = mock_fromfd.return_value
self.mock_selector_for_client_read_read_server_write(
mock_selector, server)
flags = Flags(
auth_code=b'Basic %s' %
base64.b64encode(b'user:pass'))
flags.plugins = Flags.load_plugins([
b'proxy.http.proxy.HttpProxyPlugin',
b'proxy.http.server.HttpWebServerPlugin'
])
self.protocol_handler = HttpProtocolHandler(
TcpClientConnection(self._conn, self._addr), flags=flags)
self.protocol_handler.initialize()
assert self.http_server_port is not None
self._conn.recv.return_value = CRLF.join([
b'CONNECT localhost:%d HTTP/1.1' % self.http_server_port,
b'Host: localhost:%d' % self.http_server_port,
b'User-Agent: proxy.py/%s' % bytes_(__version__),
b'Proxy-Connection: Keep-Alive',
b'Proxy-Authorization: Basic dXNlcjpwYXNz',
CRLF
])
self.assert_tunnel_response(mock_server_connection, server)
self.protocol_handler.client.flush()
self.assert_data_queued_to_server(server)
self.protocol_handler.run_once()
server.flush.assert_called_once()
def mock_selector_for_client_read_read_server_write(
self, mock_selector: mock.Mock, server: mock.Mock) -> None:
mock_selector.return_value.select.side_effect = [
[(selectors.SelectorKey(
fileobj=self._conn,
fd=self._conn.fileno,
events=selectors.EVENT_READ,
data=None), selectors.EVENT_READ), ],
[(selectors.SelectorKey(
fileobj=self._conn,
fd=self._conn.fileno,
events=0,
data=None), selectors.EVENT_READ), ],
[(selectors.SelectorKey(
fileobj=server.connection,
fd=server.connection.fileno,
events=0,
data=None), selectors.EVENT_WRITE), ],
]
def assert_data_queued(
self, mock_server_connection: mock.Mock, server: mock.Mock) -> None:
self.protocol_handler.run_once()
self.assertEqual(
self.protocol_handler.request.state,
httpParserStates.COMPLETE)
mock_server_connection.assert_called_once()
server.connect.assert_called_once()
server.closed = False
assert self.http_server_port is not None
pkt = CRLF.join([
b'GET / HTTP/1.1',
b'User-Agent: proxy.py/%s' % bytes_(__version__),
b'Host: localhost:%d' % self.http_server_port,
b'Accept: */*',
b'Via: 1.1 proxy.py v%s' % bytes_(__version__),
CRLF
])
server.queue.assert_called_once_with(pkt)
server.buffer_size.return_value = len(pkt)
def assert_data_queued_to_server(self, server: mock.Mock) -> None:
assert self.http_server_port is not None
self.assertEqual(
self._conn.send.call_args[0][0],
HttpProxyPlugin.PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT)
pkt = CRLF.join([
b'GET / HTTP/1.1',
b'Host: localhost:%d' % self.http_server_port,
b'User-Agent: proxy.py/%s' % bytes_(__version__),
CRLF
])
self._conn.recv.return_value = pkt
self.protocol_handler.run_once()
server.queue.assert_called_once_with(pkt)
server.buffer_size.return_value = len(pkt)
server.flush.assert_not_called()
def mock_selector_for_client_read(self, mock_selector: mock.Mock) -> None:
mock_selector.return_value.select.return_value = [(
selectors.SelectorKey(
fileobj=self._conn,
fd=self._conn.fileno,
events=selectors.EVENT_READ,
data=None), selectors.EVENT_READ), ]