diff --git a/.gitmodules b/.gitmodules index 5d67bf0..b9f6fd6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "pacparser"] path = pacparser url = https://github.com/pacparser/pacparser.git +[submodule "tinyproxy"] + path = tinyproxy + url = https://github.com/pac4cli/tinyproxy.git diff --git a/pac4cli/__main__.py b/pac4cli/__main__.py index 5c5a889..80e08fc 100755 --- a/pac4cli/__main__.py +++ b/pac4cli/__main__.py @@ -2,17 +2,17 @@ logger = logging.getLogger('pac4cli') from argparse import ArgumentParser +from os import path +import os +import tempfile +import signal +import subprocess from twisted.internet import reactor -from twisted.web import proxy -from twisted.web.http import HTTPFactory from twisted.web.client import Agent, readBody from twisted.internet.defer import inlineCallbacks -import pacparser - from .wpad import WPAD, install_network_state_changed_callback -from .pac4cli import WPADProxyRequest from . import servicemanager @@ -26,18 +26,45 @@ parser.add_argument("-F", "--force-proxy", type=str, metavar="PROXY STRING") parser.add_argument("--loglevel", type=str, default="info", metavar="LEVEL") parser.add_argument("--systemd", action='store_true') +parser.add_argument("--runtimedir", type=str, default=tempfile.mkdtemp()) -args= parser.parse_args() - -@inlineCallbacks -def start_server(interface, port, reactor): - factory = HTTPFactory() - factory.protocol = proxy.Proxy - factory.protocol.requestFactory = WPADProxyRequest +args = parser.parse_args() - yield reactor.listenTCP(port, factory, interface=interface) +server_process = None - servicemanager.notify_ready(); +def start_server(interface, port, reactor): + write_pac_file(None) + tinyproxy_conf_path = path.join(args.runtimedir, "tinyproxy.conf") + with open(tinyproxy_conf_path, "w") as config: + config.write(""" + Listen {interface} + Port {port} + MaxClients 1 + StartServers 1 + PacUpstream "{pac_filename}" + """.format( + interface=interface, + port=port, + pac_filename=path.join(args.runtimedir, "wpad.dat"))) + global server_process + server_process = subprocess.Popen(["/home/tkluck/src/pac4cli/tinyproxy/src/tinyproxy", + "-d", "-c", tinyproxy_conf_path]) + # TODO: make tinyproxy send this signal + servicemanager.notify_ready() + +def write_pac_file(script): + if script is None: + script = b""" + function FindProxyForURL(url, host) { + return "DIRECT"; + } + """ + pac_file_path = path.join(args.runtimedir, "wpad.dat") + with open(pac_file_path, "wb") as pac_file: + pac_file.write(script) + os.sync() + if server_process is not None: + server_process.send_signal(signal.SIGHUP) @inlineCallbacks def get_possible_configuration_locations(): @@ -56,9 +83,7 @@ def updateWPAD(signum=None, stackframe=None): logger.info("Updating WPAD configuration...") wpad_urls = yield get_possible_configuration_locations() - # use DIRECT temporarily; who knows what state the below gets pacparser - # in - WPADProxyRequest.force_direct = 'DIRECT' + write_pac_file(None) for wpad_url in wpad_urls: logger.info("Trying %s...", wpad_url) try: @@ -68,25 +93,22 @@ def updateWPAD(signum=None, stackframe=None): response = yield agent.request(b'GET', wpad_url.encode('ascii')) body = yield readBody(response) logger.info("...found. Parsing configuration...") - pacparser.parse_pac_string(body.decode('ascii')) + write_pac_file(body) logger.info("Updated configuration") - WPADProxyRequest.force_direct = None break except Exception as e: - logger.info("...didn't work") + logger.info("...didn't work", exc_info=True) pass else: logger.info("None of the tried urls seem to have worked; falling back to direct") - WPADProxyRequest.force_direct = 'DIRECT' @inlineCallbacks def main(args): try: - pacparser.init() - WPADProxyRequest.force_direct = 'DIRECT' # direct, until we have a configuration if args.force_proxy: - WPADProxyRequest.force_proxy = args.force_proxy + # TODO: pass to tinyproxy configuration + pass try: yield install_network_state_changed_callback(reactor, updateWPAD) @@ -119,4 +141,9 @@ def main(args): log_handler.setFormatter(logging.Formatter(fmt="%(levelname)s [%(process)d]: %(name)s: %(message)s")) main(args) reactor.run() - logger.info("Shutdown") + logger.info("Shutdown...") + if server_process is not None: + logger.info("Terminating server process...") + server_process.terminate() + server_process.wait() + logger.info("Shutdown complete") diff --git a/pac4cli/pac4cli.py b/pac4cli/pac4cli.py deleted file mode 100644 index 212c091..0000000 --- a/pac4cli/pac4cli.py +++ /dev/null @@ -1,162 +0,0 @@ -import logging -logger = logging.getLogger('pac4cli') - -import re - -from twisted.web import proxy -from twisted.python.compat import urllib_parse -import pacparser - -from . import portforward - -def split_host_port(destination): - ''' - >>> split_host_port('host') - ('host', None) - >>> split_host_port('host.com') - ('host.com', None) - >>> split_host_port('host0') - ('host0', None) - >>> split_host_port('host0:80') - ('host0', 80) - >>> split_host_port('127.0.0.1') - ('127.0.0.1', None) - >>> split_host_port('127.0.0.1:80') - ('127.0.0.1', 80) - >>> split_host_port('[0abc:1def::1234]') - ('[0abc:1def::1234]', None) - >>> split_host_port('[0abc:1def::1234]:443') - ('[0abc:1def::1234]', 443) - ''' - components = destination.rsplit(':', maxsplit=1) - if len(components) == 1: - return (destination, None) - elif all(c.isdigit() for c in components[1]): - return components[0], int(components[1]) - else: - return (destination, None) - -class WPADProxyRequest(proxy.ProxyRequest): - - force_proxy = None - force_direct = None - - proxy_suggestion_parser = re.compile( r'(DIRECT$|PROXY) (.*)' ) - - def process(self): - method = self.method.decode('ascii') - uri = self.uri.decode('ascii') - if method == 'CONNECT': - host, port = split_host_port(uri) - port = int(port) - else: - parsed = urllib_parse.urlparse(self.uri) - decoded = parsed[1].decode('ascii') - host, port = split_host_port(decoded) - if port is None: - port = 80 - rest = urllib_parse.urlunparse((b'', b'') + parsed[2:]) - if not rest: - rest = rest + b'/' - - headers = self.getAllHeaders().copy() - self.content.seek(0, 0) - s = self.content.read() - - proxy_suggestion = self.force_proxy or self.force_direct or pacparser.find_proxy('http://{}'.format(host)) - - proxy_suggestions = proxy_suggestion.split(";") - parsed_proxy_suggestion = self.proxy_suggestion_parser.match(proxy_suggestions[0]) - - if parsed_proxy_suggestion: - connect_method, destination = parsed_proxy_suggestion.groups() - if connect_method == 'PROXY': - proxy_host, proxy_port = destination.split(":") - proxy_port = int(proxy_port) - if method != 'CONNECT': - clientFactory = proxy.ProxyClientFactory( - self.method, - self.uri, - self.clientproto, - headers, - s, - self, - ) - logger.info('%s %s; forwarding request to %s:%s', method, uri, proxy_host, proxy_port) - else: - self.transport.unregisterProducer() - self.transport.pauseProducing() - rawConnectionProtocol = portforward.Proxy() - rawConnectionProtocol.transport = self.transport - self.transport.protocol = rawConnectionProtocol - - clientFactory = CONNECTProtocolForwardFactory(host, port) - clientFactory.setServer(rawConnectionProtocol) - - logger.info('%s %s; establishing tunnel through %s:%s', method, uri, proxy_host, proxy_port) - - self.reactor.connectTCP(proxy_host, proxy_port, clientFactory) - return - else: - # can this be anything else? Let's fall back to the DIRECT - # codepath. - pass - if method != 'CONNECT': - if b'host' not in headers: - headers[b'host'] = host.encode('ascii') - - clientFactory = proxy.ProxyClientFactory( - self.method, - rest, - self.clientproto, - headers, - s, - self, - ) - logger.info('%s %s; forwarding request', method, uri) - self.reactor.connectTCP(host, port, clientFactory) - else: - # hack/trick to move responsibility for this connection - # away from a HTTP protocol class hierarchy and to a - # port forward hierarchy - self.transport.unregisterProducer() - self.transport.pauseProducing() - rawConnectionProtocol = portforward.Proxy() - rawConnectionProtocol.transport = self.transport - self.transport.protocol = rawConnectionProtocol - - clientFactory = portforward.ProxyClientFactory() - clientFactory.setServer(rawConnectionProtocol) - clientFactory.protocol = CONNECTProtocolClient - # we don't do connectSSL, as the handshake is taken - # care of by the client, and we only forward it - logger.info('%s %s; establishing tunnel to %s:%s', method, uri, host, port) - self.reactor.connectTCP(host, port, - clientFactory) - - -class CONNECTProtocolClient(portforward.ProxyClient): - def connectionMade(self): - self.peer.transport.write(b"HTTP/1.1 200 OK\r\n\r\n") - portforward.ProxyClient.connectionMade(self) - - -class CONNECTProtocolForward(portforward.ProxyClient): - def connectionMade(self): - self.transport.write( - "CONNECT {}:{} HTTP/1.1\r\nhost: {}\r\n\r\n".format( - self.factory.host, - self.factory.port, - self.factory.host, - ).encode('ascii') - ) - portforward.ProxyClient.connectionMade(self) - -class CONNECTProtocolForwardFactory(portforward.ProxyClientFactory): - protocol = CONNECTProtocolForward - def __init__(self, host, port): - portforward.ProxyClientFactory.__init__(self) - self.host = host - self.port = port - - diff --git a/pac4cli/portforward.py b/pac4cli/portforward.py deleted file mode 100644 index a3c3954..0000000 --- a/pac4cli/portforward.py +++ /dev/null @@ -1,99 +0,0 @@ - -# Copyright (c) Twisted Matrix Laboratories. -# See LICENSE for details. - -""" -A simple port forwarder. -""" - -# Twisted imports -from twisted.internet import protocol -from twisted.python import log - -class Proxy(protocol.Protocol): - noisy = True - - peer = None - - def setPeer(self, peer): - self.peer = peer - - - def connectionLost(self, reason): - if self.peer is not None: - self.peer.transport.loseConnection() - self.peer = None - elif self.noisy: - log.msg("Unable to connect to peer: %s" % (reason,)) - - - def dataReceived(self, data): - self.peer.transport.write(data) - - - -class ProxyClient(Proxy): - def connectionMade(self): - self.peer.setPeer(self) - - # Wire this and the peer transport together to enable - # flow control (this stops connections from filling - # this proxy memory when one side produces data at a - # higher rate than the other can consume). - self.transport.registerProducer(self.peer.transport, True) - self.peer.transport.registerProducer(self.transport, True) - - # We're connected, everybody can read to their hearts content. - self.peer.transport.resumeProducing() - - - -class ProxyClientFactory(protocol.ClientFactory): - - protocol = ProxyClient - - def setServer(self, server): - self.server = server - - - def buildProtocol(self, *args, **kw): - prot = protocol.ClientFactory.buildProtocol(self, *args, **kw) - prot.setPeer(self.server) - return prot - - - def clientConnectionFailed(self, connector, reason): - self.server.transport.loseConnection() - - - -class ProxyServer(Proxy): - - clientProtocolFactory = ProxyClientFactory - reactor = None - - def connectionMade(self): - # Don't read anything from the connecting client until we have - # somewhere to send it to. - self.transport.pauseProducing() - - client = self.clientProtocolFactory() - client.setServer(self) - - if self.reactor is None: - from twisted.internet import reactor - self.reactor = reactor - self.reactor.connectTCP(self.factory.host, self.factory.port, client) - - - -class ProxyFactory(protocol.Factory): - """ - Factory for port forwarder. - """ - - protocol = ProxyServer - - def __init__(self, host, port): - self.host = host - self.port = port diff --git a/test/runtests.py b/test/runtests.py index dfcb0c8..ae7c623 100644 --- a/test/runtests.py +++ b/test/runtests.py @@ -69,7 +69,8 @@ def test_direct_proxy(self): curl("http://www.booking.com") self.assertTrue(True) finally: - proxy.proc.kill() + proxy.proc.terminate() + proxy.proc.wait() def test_proxied_proxy(self): proxy1 = pac4cli["-F", "DIRECT", "-p", "23128"] & BG @@ -81,8 +82,10 @@ def test_proxied_proxy(self): curl("http://www.booking.com") self.assertTrue(True) finally: - proxy2.proc.kill() - proxy1.proc.kill() + proxy2.proc.terminate() + proxy2.proc.wait() + proxy1.proc.terminate() + proxy1.proc.wait() def test_proxy_from_dhcp_wpad(self): # set up mock dbus with dhcp settings @@ -148,10 +151,14 @@ def test_proxy_from_dhcp_wpad(self): "Hello from fake proxy no 2!", ) finally: - proxy_to_test.proc.kill() - fake_proxy_2.proc.kill() - fake_proxy_1.proc.kill() - static_server.proc.kill() + proxy_to_test.proc.terminate() + proxy_to_test.proc.wait() + fake_proxy_2.proc.terminate() + fake_proxy_2.proc.wait() + fake_proxy_1.proc.terminate() + fake_proxy_1.proc.wait() + static_server.proc.terminate() + static_server.proc.wait() if __name__ == '__main__': diff --git a/tinyproxy b/tinyproxy new file mode 160000 index 0000000..c2b82f3 --- /dev/null +++ b/tinyproxy @@ -0,0 +1 @@ +Subproject commit c2b82f3918f9d74d298e12bd02c5dc23d5e253b0