commit eeb5b7c9d72a4758f8c5dc70b67c8233548c91bf Author: Isis Lovecruft isis@torproject.org Date: Sun Sep 7 00:04:17 2014 +0000
Deprecate b.Bridges.PluggableTransport for b.bridges.PluggableTransport.
This replaces the legacy `bridgedb.Bridges.PluggableTransport` class, which had several issues, including:
- `assert` statements without catching errors - Cyclically referential data structures which contain __del__() methods. (Which means that these structures cannot be garbage collected!! see https://stackoverflow.com/a/10962484)
* ADD new `bridgedb.bridges` module for replacing old code in `bridgedb.Bridges`.
* MOVE legacy implementation `bridgedb.Bridges.PluggableTransport` to `bridgedb.test.deprecated` module for automated regression testing.
* ADD new implementation, `bridgedb.bridges.PluggableTransport`, which is mostly backward-compatible with the old implementation, except that instead of taking an entire `bridgedb.Bridges.Bridge` class as its first argument, it takes just the uppercased, hexadecimal fingerprint of that bridge. This was done because the legacy `Bridge` and `PluggableTransport` classes posed a serialization problem due to infinitely self-referencing eachother, i.e. the `bridgedb.Bridges.Bridge.transports` attribute was a list of `bridgedb.Bridges.PluggableTransport`s, which in turn referenced their `bridgedb.Bridges.Bridge`, which referenced a list of bridgedb.Bridges.PluggableTransport`s... turtles all the way down, and they never get garbage collected, they just end up in Python's `gc.garbage` list for "the programmer to manually deal with".
* FIXES part of #12505 https://bugs.torproject.org/12505 --- lib/bridgedb/Bridges.py | 104 ------------- lib/bridgedb/Main.py | 23 ++- lib/bridgedb/bridges.py | 290 +++++++++++++++++++++++++++++++++++++ lib/bridgedb/test/deprecated.py | 112 ++++++++++++++ lib/bridgedb/test/test_Bridges.py | 7 +- lib/bridgedb/test/test_Tests.py | 2 + 6 files changed, 424 insertions(+), 114 deletions(-)
diff --git a/lib/bridgedb/Bridges.py b/lib/bridgedb/Bridges.py index fa718c0..dead9f2 100644 --- a/lib/bridgedb/Bridges.py +++ b/lib/bridgedb/Bridges.py @@ -451,110 +451,6 @@ re_ipv6 = re.compile("[([a-fA-F0-9:]+)]:(.*$)") re_ipv4 = re.compile("((?:\d{1,3}.?){4}):(.*$)")
-class PluggableTransport(object): - """A PT with reference to the parent bridge on which it is running.""" - - def __init__(self, bridge, methodname, address, port, argdict=None): - """Create a ``PluggableTransport`` describing a PT running on a bridge. - - Pluggable transports are described within a bridge's ``@type - bridge-extrainfo`` descriptor, see the ``Specifications: Client - behavior`` section and the ``TOR_PT_SERVER_TRANSPORT_OPTIONS`` - description in pt-spec.txt_ for additional specification. - - :type bridge: :class:`Bridge` - :param bridge: The parent bridge running this pluggable transport - instance, i.e. the main ORPort bridge whose - ``@type bridge-server-descriptor`` contains a hash digest for a - ``@type bridge-extrainfo-document``, the latter of which contains - the parameter of this pluggable transport in its ``transport`` - line. - - :param str methodname: The canonical "name" for this pluggable - transport, i.e. the one which would be specified in a torrc - file. For example, ``"obfs2"``, ``"obfs3"``, ``"scramblesuit"`` - would all be pluggable transport method names. - - :param str address: The IP address of the transport. Currently (as of - 20 March 2014), there are no known, widely-deployed pluggable - transports which support IPv6. Ergo, this is very likely going to - be an IPv4 address. - - :param int port: A integer specifying the port which this pluggable - transport is listening on. (This should likely be whatever port the - bridge specified in its ``ServerTransportPlugin`` torrc line, - unless the pluggable transport is running in "managed" mode.) - - :param dict argdict: Some PTs can take additional arguments, which - must be distributed to the client out-of-band. These are present - in the ``@type bridge-extrainfo-document``, in the ``transport`` - line like so:: - - METHOD SP ADDR ":" PORT SP [K=V[,K=V[,K=V[…]]]] - - where K is the **argdict** key, and V is the value. For example, - in the case of ``scramblesuit``, for which the client must supply - a shared secret to the ``scramblesuit`` instance running on the - bridge, the **argdict** would be something like:: - - {'password': 'NEQGQYLUMUQGK5TFOJ4XI2DJNZTS4LRO'} - - .. _pt-spec.txt: - https://gitweb.torproject.org/torspec.git/blob/HEAD:/pt-spec.txt - """ - #XXX: assert are disabled with python -O - assert isinstance(bridge, Bridge) - assert type(address) in (ipaddr.IPv4Address, ipaddr.IPv6Address) - assert type(port) is int - assert (0 < port < 65536) - assert type(methodname) is str - - self.bridge = bridge - self.address = address - self.port = port - self.methodname = methodname - if type(argdict) is dict: - self.argdict = argdict - else: self.argdict = {} - - def getTransportLine(self, includeFingerprint=False, bridgePrefix=False): - """Get a torrc line for this pluggable transport. - - This method does not return lines which are prefixed with the word - 'bridge', as they would be in a torrc file. Instead, lines returned - look like this: - - obfs3 245.102.100.252:23619 59ca743e89b508e16b8c7c6d2290efdfd14eea98 - - :param bool includeFingerprints: If ``True``, include the digest of - this bridges public identity key in the torrc line. - :param bool bridgePrefix: If ``True``, add ``'Bridge '`` to the - beginning of each returned line (suitable for pasting directly - into a torrc file). - :rtype: str - :returns: A configuration line for adding this pluggable transport - into a torrc file. - """ - sections = [] - - if bridgePrefix: - sections.append('Bridge') - - if isinstance(self.address, ipaddr.IPv6Address): - host = "%s [%s]:%d" % (self.methodname, self.address, self.port) - else: - host = "%s %s:%d" % (self.methodname, self.address, self.port) - sections.append(host) - - if includeFingerprint: - sections.append(self.bridge.fingerprint) - - args = " ".join(["%s=%s" % (k, v) for k, v in self.argdict.items()]) - sections.append(args) - - line = ' '.join(sections) - return line - def parseExtraInfoFile(f): """ parses lines in Bridges extra-info documents. diff --git a/lib/bridgedb/Main.py b/lib/bridgedb/Main.py index ce3edc5..e1963f5 100644 --- a/lib/bridgedb/Main.py +++ b/lib/bridgedb/Main.py @@ -27,6 +27,9 @@ from bridgedb import proxy from bridgedb import safelog from bridgedb import schedule from bridgedb import util +from bridgedb.bridges import InvalidPluggableTransportIP +from bridgedb.bridges import MalformedPluggableTransport +from bridgedb.bridges import PluggableTransport from bridgedb.configure import loadConfig from bridgedb.configure import Conf from bridgedb.parse import options @@ -128,13 +131,19 @@ def load(state, splitter, clear=False): if bridges[ID].running: logging.info("Adding %s transport to running bridge" % method_name) - bridgePT = Bridges.PluggableTransport( - bridges[ID], method_name, address, port, argdict) - bridges[ID].transports.append(bridgePT) - if not bridgePT in bridges[ID].transports: - logging.critical( - "Added a transport, but it disappeared!", - "\tTransport: %r" % bridgePT) + try: + bridgePT = PluggableTransport(Bridges.toHex(ID), + method_name, address, + port, argdict) + except (InvalidPluggableTransportIP, + MalformedPluggableTransport) as error: + logging.warn(error) + else: + bridges[ID].transports.append(bridgePT) + if not bridgePT in bridges[ID].transports: + logging.critical( + "Added a transport, but it disappeared!", + "\tTransport: %r" % bridgePT) except KeyError as error: logging.error("Could not find bridge with fingerprint '%s'." % Bridges.toHex(ID)) diff --git a/lib/bridgedb/bridges.py b/lib/bridgedb/bridges.py new file mode 100644 index 0000000..65d4d02 --- /dev/null +++ b/lib/bridgedb/bridges.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 ; test-case-name: bridgedb.test.test_bridges -*- +# +# This file is part of BridgeDB, a Tor bridge distribution system. +# +# :authors: please see the AUTHORS file for attributions +# :copyright: (c) 2013-2014, Isis Lovecruft +# (c) 2007-2014, The Tor Project, Inc. +# :license: see LICENSE for licensing information + +"""Classes for manipulating and storing Bridges and their attributes.""" + + +import ipaddr +import logging +import os + +from bridgedb.parse.addr import isValidIP +from bridgedb.parse.fingerprint import isValidFingerprint + + +class MalformedBridgeInfo(ValueError): + """Raised when some information about a bridge appears malformed.""" + + +class MalformedPluggableTransport(MalformedBridgeInfo): + """Raised when information used to initialise a :class:`PluggableTransport` + appears malformed. + """ + +class InvalidPluggableTransportIP(MalformedBridgeInfo): + """Raised when a :class:`PluggableTransport` has an invalid address.""" + + +class PluggableTransport(object): + """A single instance of a Pluggable Transport (PT) offered by a + :class:`Bridge`. + + Pluggable transports are described within a bridge's + ``@type bridge-extrainfo`` descriptor, see the + ``Specifications: Client behavior`` section and the + ``TOR_PT_SERVER_TRANSPORT_OPTIONS`` description in pt-spec.txt_ for + additional specification. + + .. _pt-spec.txt: + https://gitweb.torproject.org/torspec.git/blob/HEAD:/pt-spec.txt + + :type fingerprint: str + :ivar fingerprint: The uppercased, hexadecimal fingerprint of the identity + key of the parent bridge running this pluggable transport instance, + i.e. the main ORPort bridge whose ``@type bridge-server-descriptor`` + contains a hash digest for a ``@type bridge-extrainfo-document``, the + latter of which contains the parameter of this pluggable transport in + its ``transport`` line. + + :type methodname: str + :ivar methodname: The canonical "name" for this pluggable transport, + i.e. the one which would be specified in a torrc file. For example, + ``"obfs2"``, ``"obfs3"``, ``"scramblesuit"`` would all be pluggable + transport method names. + + :type address: ``ipaddr.IPv4Address`` or ``ipaddr.IPv6Address`` + :ivar address: The IP address of the transport. Currently (as of 20 March + 2014), there are no known, widely-deployed pluggable transports which + support IPv6. Ergo, this is very likely going to be an IPv4 address. + + :type port: int + :ivar port: A integer specifying the port which this pluggable transport + is listening on. (This should likely be whatever port the bridge + specified in its ``ServerTransportPlugin`` torrc line, unless the + pluggable transport is running in "managed" mode.) + + :type arguments: dict + :ivar arguments: Some PTs can take additional arguments, which must be + distributed to the client out-of-band. These are present in the + ``@type bridge-extrainfo-document``, in the ``transport`` line like + so:: + + METHOD SP ADDR ":" PORT SP [K=V[,K=V[,K=V[…]]]] + + where K is the key in **arguments**, and V is the value. For example, + in the case of ``scramblesuit``, for which the client must supply a + shared secret to the ``scramblesuit`` instance running on the bridge, + the **arguments** would be something like:: + + {'password': 'NEQGQYLUMUQGK5TFOJ4XI2DJNZTS4LRO'} + """ + + def __init__(self, fingerprint=None, methodname=None, + address=None, port=None, arguments=None): + """Create a ``PluggableTransport`` describing a PT running on a bridge. + + :param str fingerprint: The uppercased, hexadecimal fingerprint of the + identity key of the parent bridge running this pluggable transport. + :param str methodname: The canonical "name" for this pluggable + transport. See :data:`methodname`. + :param str address: The IP address of the transport. See + :data:`address`. + :param int port: A integer specifying the port which this pluggable + transport is listening on. + :param dict arguments: Any additional arguments which the PT takes, + which must be distributed to the client out-of-band. See + :data:`arguments`. + """ + self.fingerprint = fingerprint + self.address = address + self.port = port + self.methodname = methodname + self.arguments = arguments + + # Because we can intitialise this class with the __init__() + # parameters, or use the ``updateFromStemTransport()`` method, we'll + # only use the ``_runChecks()`` method now if we were initialised with + # parameters: + if (self.fingerprint or self.address or self.port or + self.methodname or self.arguments): + self._runChecks() + + def _parseArgumentsIntoDict(self, argumentList): + """Convert a list of Pluggable Transport arguments into a dictionary + suitable for :data:`arguments`. + + :param list argumentList: A list of Pluggable Transport + arguments. There might be multiple, comma-separated ``K=V`` + Pluggable Transport arguments in a single item in the + **argumentList**, or each item might be its own ``K=V``; we don't + care and we should be able to parse it either way. + :rtype: dict + :returns: A dictionary of all the ``K=V`` Pluggable Transport + arguments. + """ + argDict = {} + + # PT argumentss are comma-separated in the extrainfo + # descriptors. While there *shouldn't* be anything after them that was + # separated by a space (and hence would wind up being in a different + # item in `arguments`), if there was we'll join it to the rest of the + # PT arguments with a comma so that they are parsed as if they were PT + # arguments as well: + allArguments = ','.join(argumentList) + + for arg in allArguments.split(','): + try: + key, value = arg.split('=') + except ValueError: + logging.warn(" Couldn't parse K=V from PT arg: %r" % arg) + else: + logging.debug(" Parsed PT Argument: %s: %s" % (key, value)) + argDict[key] = value + + return argDict + + def _runChecks(self): + """Validate that we were initialised with acceptable parameters. + + We currently check that: + + 1. The :data:`fingerprint` is valid, according to + :func:`~bridgedb.parse.fingerprint.isValidFingerprint`. + + 2. The :data:`address` is valid, according to + :func:`~bridgedb.parse.addr.isValidIP`. + + 3. The :data:`port` is an integer, and that it is between the values + of ``0`` and ``65535`` (inclusive). + + 4. The :data:`arguments` is a dictionary. + + :raises MalformedPluggableTransport: if any of the above checks fails. + """ + if not isValidFingerprint(self.fingerprint): + raise MalformedPluggableTransport( + ("Cannot create PluggableTransport with bad Bridge " + "fingerprint: %r.") % self.fingerprint) + + valid = isValidIP(self.address) + if not valid: + raise InvalidPluggableTransportIP( + ("Cannot create PluggableTransport with address '%s'. " + "type(address)=%s.") % (self.address, type(self.address))) + self.address = ipaddr.IPAddress(self.address) + + try: + # Coerce the port to be an integer: + self.port = int(self.port) + except TypeError: + raise MalformedPluggableTransport( + ("Cannot create PluggableTransport with port type: %s.") + % type(self.port)) + else: + if not (0 <= self.port <= 65535): + raise MalformedPluggableTransport( + ("Cannot create PluggableTransport with out-of-range port:" + " %r.") % self.port) + + if not isinstance(self.arguments, dict): + raise MalformedPluggableTransport( + ("Cannot create PluggableTransport with arguments type: %s") + % type(self.arguments)) + + def getTransportLine(self, includeFingerprint=True, bridgePrefix=False): + """Get a Bridge Line for this :class:`PluggableTransport`. + + .. glossary:: + + Bridge Line + A "Bridge Line" is how BridgeDB refers to lines in a ``torrc`` + file which should begin with the word ``"Bridge"``, and it is how + a client tells their Tor process that they would like to use a + particular bridge. + + .. note:: If **bridgePrefix** is ``False``, this method does not + return lines which are prefixed with the word 'bridge', as they + would be in a torrc file. Instead, lines returned look like this:: + + obfs3 245.102.100.252:23619 59ca743e89b508e16b8c7c6d2290efdfd14eea98 + + This was made configurable to fix Vidalia being a brain-damaged + piece of shit (#5851_). TorLaucher replaced Vidalia soon after, + and TorLauncher is intelligent enough to understand + :term:`Bridge Line`s regardless of whether or not they are prefixed + with the word "Bridge". + + .. _#5851: https://bugs.torproject.org/5851 + + :param bool includeFingerprints: If ``True``, include the digest of + this bridges public identity key in the torrc line. + :param bool bridgePrefix: If ``True``, add ``'Bridge '`` to the + beginning of each returned line (suitable for pasting directly + into a ``torrc`` file). + :rtype: str + :returns: A configuration line for adding this Pluggable Transport + into a ``torrc`` file. + """ + sections = [] + + if bridgePrefix: + sections.append('Bridge') + + if self.address.version == 6: + # If the address was IPv6, put brackets around it: + host = '%s [%s]:%d' % (self.methodname, self.address, self.port) + else: + host = '%s %s:%d' % (self.methodname, self.address, self.port) + sections.append(host) + + if includeFingerprint: + sections.append(self.fingerprint) + + for key, value in self.arguments.items(): + sections.append('%s=%s' % (key, value)) + + line = ' '.join(sections) + + return line + + def updateFromStemTransport(self, fingerprint, methodname, kitchenSink): + """Update this :class:`PluggableTransport` from the data structure + which Stem uses. + + Stem's + :api:`stem.descriptor.extrainfo_descriptor.BridgeExtraInfoDescriptor` + parses extrainfo ``transport`` lines into a dictionary with the + following structure:: + + {u'obfs2': (u'34.230.223.87', 37339, []), + u'obfs3': (u'34.230.223.87', 37338, []), + u'obfs4': (u'34.230.223.87', 37341, [ + (u'iat-mode=0,' + u'node-id=2a79f14120945873482b7823caabe2fcde848722,' + u'public-key=0a5b046d07f6f971b7776de682f57c5b9cdc8fa060db7ef59de82e721c8098f4')]), + u'scramblesuit': (u'34.230.223.87', 37340, [ + u'password=ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'])} + + This method will initialise this class from the dictionary key + (**methodname**) and its tuple of values (**kitchenSink**). + + :param str fingerprint: The uppercased, hexadecimal fingerprint of the + identity key of the parent bridge running this pluggable transport. + :param str methodname: The :data:`methodname` of this Pluggable + Transport. + :param tuple kitchenSink: Everything else that was on the + ``transport`` line in the bridge's extrainfo descriptor, which + Stem puts into the 3-tuples shown in the example above. + """ + self.fingerprint = fingerprint + self.methodname = methodname + self.address = kitchenSink[0] + self.port = kitchenSink[1] + self.arguments = self._parseArgumentsIntoDict(kitchenSink[2]) + self._runChecks() diff --git a/lib/bridgedb/test/deprecated.py b/lib/bridgedb/test/deprecated.py index a972a52..40cfe0a 100644 --- a/lib/bridgedb/test/deprecated.py +++ b/lib/bridgedb/test/deprecated.py @@ -123,6 +123,118 @@ class ParseORAddressError(Exception):
@deprecate.deprecated( + Version('bridgedb', 0, 2, 4), + replacement='bridgedb.bridges.PluggableTransport') +class PluggableTransport(object): + """A PT with reference to the parent bridge on which it is running. + + Deprecated :class:`bridgedb.Bridges.PluggableTransport`, replaced in + bridgedb-0.2.4, by :class:`bridgedb.bridges.PluggableTransport`. + """ + + def __init__(self, bridge, methodname, address, port, argdict=None): + """Create a ``PluggableTransport`` describing a PT running on a bridge. + + Pluggable transports are described within a bridge's ``@type + bridge-extrainfo`` descriptor, see the ``Specifications: Client + behavior`` section and the ``TOR_PT_SERVER_TRANSPORT_OPTIONS`` + description in pt-spec.txt_ for additional specification. + + :type bridge: :class:`Bridge` + :param bridge: The parent bridge running this pluggable transport + instance, i.e. the main ORPort bridge whose + ``@type bridge-server-descriptor`` contains a hash digest for a + ``@type bridge-extrainfo-document``, the latter of which contains + the parameter of this pluggable transport in its ``transport`` + line. + + :param str methodname: The canonical "name" for this pluggable + transport, i.e. the one which would be specified in a torrc + file. For example, ``"obfs2"``, ``"obfs3"``, ``"scramblesuit"`` + would all be pluggable transport method names. + + :param str address: The IP address of the transport. Currently (as of + 20 March 2014), there are no known, widely-deployed pluggable + transports which support IPv6. Ergo, this is very likely going to + be an IPv4 address. + + :param int port: A integer specifying the port which this pluggable + transport is listening on. (This should likely be whatever port the + bridge specified in its ``ServerTransportPlugin`` torrc line, + unless the pluggable transport is running in "managed" mode.) + + :param dict argdict: Some PTs can take additional arguments, which + must be distributed to the client out-of-band. These are present + in the ``@type bridge-extrainfo-document``, in the ``transport`` + line like so:: + + METHOD SP ADDR ":" PORT SP [K=V[,K=V[,K=V[…]]]] + + where K is the **argdict** key, and V is the value. For example, + in the case of ``scramblesuit``, for which the client must supply + a shared secret to the ``scramblesuit`` instance running on the + bridge, the **argdict** would be something like:: + + {'password': 'NEQGQYLUMUQGK5TFOJ4XI2DJNZTS4LRO'} + + .. _pt-spec.txt: + https://gitweb.torproject.org/torspec.git/blob/HEAD:/pt-spec.txt + """ + #XXX: assert are disabled with python -O + assert isinstance(bridge, Bridge) + assert type(address) in (ipaddr.IPv4Address, ipaddr.IPv6Address) + assert type(port) is int + assert (0 < port < 65536) + assert type(methodname) is str + + self.bridge = bridge + self.address = address + self.port = port + self.methodname = methodname + if type(argdict) is dict: + self.argdict = argdict + else: self.argdict = {} + + def getTransportLine(self, includeFingerprint=False, bridgePrefix=False): + """Get a torrc line for this pluggable transport. + + This method does not return lines which are prefixed with the word + 'bridge', as they would be in a torrc file. Instead, lines returned + look like this: + + obfs3 245.102.100.252:23619 59ca743e89b508e16b8c7c6d2290efdfd14eea98 + + :param bool includeFingerprints: If ``True``, include the digest of + this bridges public identity key in the torrc line. + :param bool bridgePrefix: If ``True``, add ``'Bridge '`` to the + beginning of each returned line (suitable for pasting directly + into a torrc file). + :rtype: str + :returns: A configuration line for adding this pluggable transport + into a torrc file. + """ + sections = [] + + if bridgePrefix: + sections.append('Bridge') + + if isinstance(self.address, ipaddr.IPv6Address): + host = "%s [%s]:%d" % (self.methodname, self.address, self.port) + else: + host = "%s %s:%d" % (self.methodname, self.address, self.port) + sections.append(host) + + if includeFingerprint: + sections.append(self.bridge.fingerprint) + + args = " ".join(["%s=%s" % (k, v) for k, v in self.argdict.items()]) + sections.append(args) + + line = ' '.join(sections) + return line + + +@deprecate.deprecated( Version('bridgedb', 0, 0, 1), replacement='bridgedb.parse.addr.PortList') class PortList: diff --git a/lib/bridgedb/test/test_Bridges.py b/lib/bridgedb/test/test_Bridges.py index 93d6c10..2c7d09b 100644 --- a/lib/bridgedb/test/test_Bridges.py +++ b/lib/bridgedb/test/test_Bridges.py @@ -15,6 +15,7 @@ from binascii import a2b_hex from twisted.trial import unittest
from bridgedb import Bridges +from bridgedb.bridges import PluggableTransport from bridgedb.parse.addr import PortList
import hashlib @@ -125,9 +126,9 @@ class BridgeClassTest(unittest.TestCase): id_digest=self.id_digest, or_addresses=self.or_addresses) ptArgs = {'password': 'NEQGQYLUMUQGK5TFOJ4XI2DJNZTS4LRO'} - pt = Bridges.PluggableTransport(bridge, 'scramblesuit', - ipaddr.IPAddress('42.42.42.42'), 4242, - ptArgs) + pt = PluggableTransport(bridge.fingerprint, 'scramblesuit', + ipaddr.IPAddress('42.42.42.42'), 4242, + ptArgs) bridge.transports.append(pt) bridgeLine = bridge.getConfigLine(includeFingerprint=True, transport='scramblesuit') diff --git a/lib/bridgedb/test/test_Tests.py b/lib/bridgedb/test/test_Tests.py index 816cd40..e57f4a1 100644 --- a/lib/bridgedb/test/test_Tests.py +++ b/lib/bridgedb/test/test_Tests.py @@ -94,6 +94,8 @@ def monkeypatchTests(): patcher.addPatch(Tests.bridgedb.Bridges, 'fromHex', binascii.a2b_hex) patcher.addPatch(Tests.bridgedb.Bridges, 'is_valid_fingerprint', deprecated.is_valid_fingerprint) + patcher.addPatch(Tests.bridgedb.Bridges, 'PluggableTransport', + deprecated.PluggableTransport) return patcher