From 4a0313d84f7ec84fc3b6368d2da1fb50305391c6 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Fri, 13 Nov 2020 12:31:52 -0500 Subject: [PATCH 01/91] Here's my first pass at it --- docs/specifications/index.rst | 1 + docs/specifications/url.rst | 103 ++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 docs/specifications/url.rst diff --git a/docs/specifications/index.rst b/docs/specifications/index.rst index 7d99934f6..2029c9e5a 100644 --- a/docs/specifications/index.rst +++ b/docs/specifications/index.rst @@ -8,6 +8,7 @@ the data formats used by Tahoe. :maxdepth: 2 outline + url uri file-encoding URI-extension diff --git a/docs/specifications/url.rst b/docs/specifications/url.rst new file mode 100644 index 000000000..369f78392 --- /dev/null +++ b/docs/specifications/url.rst @@ -0,0 +1,103 @@ +URLs +==== + +The goal of this document is to completely specify the construction and use of the URLs by Tahoe-LAFS for service location. +This includes, but is not limited to, the original Foolscap-based URLs. +These are not to be confused with the URI-like capabilities Tahoe-LAFS uses to refer to stored data. +An attempt is also made to outline the rationale for certain choices about these URLs. +The intended audience for this document is Tahoe-LAFS maintainers and other developers interested in interoperating with Tahoe-LAFS or these URLs. + +Background +---------- + +Tahoe-LAFS first used Foolscap_ for network communication. +Foolscap connection setup takes as an input a Foolscap URL or a *fURL*. +A fURL includes three components: + +* the base32-encoded SHA1 hash of the DER form of an x509v3 certificate +* zero or more network addresses +* an object identifier + +A Foolscap client tries to connect to each network address in turn. +If a connection is established then TLS is negotiated. +The server is authenticated by matching its certificate against the hash in the fURL. +A matching certificate serves as proof that the handshaking peer is the correct server. +This serves as the process by which the client authenticates the server. + +The client can then exercise further Foolscap functionality using the fURL's object identifier. +If the object identifier is an unguessable, secret string then it serves as a capability. +This serves as the process by which the server authorizes the client. + +NURLs +----- + +The authentication and authorization properties of fURLs are a good fit for Tahoe-LAFS' requirements. +These are not inherently tied to the Foolscap protocol itself. +In particular they are beneficial to :doc:`http-storage-node-protocol` which uses HTTP instead of Foolscap. +It is conceivable they will also be used with WebSockets at some point as well. + +Continuing to refer to these URLs as fURLs when they are being used for other protocols may cause confusion. +Therefore, +this document coins the name *NURL* for these URLs. +This can be considered to expand to "New URLs" or "Authe*N*ticating URLs" or "Authorizi*N*g URLs" as the reader prefers. + +Syntax +------ + +The EBNF for a NURL is as follows:: + + scheme = "pb://" + + hostname = domain-name | ipv4-address | ipv6-address + net-loc = hostname, [ ":" port ] + net-loc-list = net-loc, [ { ",", net-loc } ] + + swiss-number = segment + + nurl = scheme, hash, "@", net-loc-list, "/", swiss-number + +See https://tools.ietf.org/html/rfc4648#section-5 for the definition of ``urlsafe-base64-string`` +(RFC 4648 provides an ad hoc definition rather than EBNF). +See https://tools.ietf.org/html/rfc3986#section-3.3 for the definition of ``segment``. + +Versions +-------- + +Though all NURLs are syntactically compatible some semantic differences are allowed. +These differences are separated into distinct versions. + +Version 0 +--------- + +A Foolscap fURL is considered the canonical definition of a version 0 NURL. +Notably, +the hash component is defined as the base32-encoded SHA1 hash of the DER form of an x509v3 certificate. +A version 0 NURL is identified by the length of the hash string which is always 32 bytes. + +Version 1 +--------- + +The hash component of a version 1 NURL differs in three ways from the prior version. + +1. The hash function used is SHA3-224 instead of SHA1. + The security of SHA1 `continues to be eroded`_. + Contrariwise SHA3 is currently the most recent addition to the SHA family by NIST. + The 224 bit instance is chosen to keep the output short and because it offers greater collision resistance than SHA1 was thought to offer even at its inception + (prior to security research showing actual collision resistance is lower). +2. The hash is computed over the certificate's SPKI instead of the whole certificate. + This allows certificate re-generation so long as the public key remains the same. + This is useful to allow contact information to be updated or extension of validity period. + Use of an SPKI hash has also been `explored by the web community`_ during its flirtation with using it for HTTPS certificate pinning + (though this is now largely abandoned). +3. The hash is encoded using urlsafe-base64 (without padding) instead of base32. + This provides a more compact representation and minimizes the usability impacts of switching from a 160 bit hash to a 224 bit hash. + +A version 1 NURL is identified by the length of the hash string which is always 38 bytes. + +It is possible for a client to unilaterally upgrade a version 0 NURL to a version 1 NURL. +After establishing and authenticating a connection the client will have received a copy of the server's certificate. +This is sufficient to compute the new hash and rewrite the NURL to upgrade it to version 1. +This provides stronger authentication assurances for future uses but it is not required. + +.. _`continues to be eroded`: https://en.wikipedia.org/wiki/SHA-1#Cryptanalysis_and_validation +.. _`explored by the web community`: https://www.imperialviolet.org/2011/05/04/pinning.html From 983800d593dd473927cee5d51ce795ce11ddcdfb Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Fri, 13 Nov 2020 12:35:03 -0500 Subject: [PATCH 02/91] news fragment --- newsfragments/3503.other | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/3503.other diff --git a/newsfragments/3503.other b/newsfragments/3503.other new file mode 100644 index 000000000..5d0c681b6 --- /dev/null +++ b/newsfragments/3503.other @@ -0,0 +1 @@ +The specification section of the Tahoe-LAFS documentation now includes explicit discussion of the security properties of Foolscap "fURLs" on which it depends. From a40f4ecef3cc763ee8ba40977c44617a0223bb6d Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Fri, 13 Nov 2020 12:56:33 -0500 Subject: [PATCH 03/91] Fix the ebnf and add more references --- docs/specifications/url.rst | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/specifications/url.rst b/docs/specifications/url.rst index 369f78392..c8beddc1c 100644 --- a/docs/specifications/url.rst +++ b/docs/specifications/url.rst @@ -46,19 +46,23 @@ Syntax The EBNF for a NURL is as follows:: + nurl = scheme, hash, "@", net-loc-list, "/", swiss-number + scheme = "pb://" - hostname = domain-name | ipv4-address | ipv6-address - net-loc = hostname, [ ":" port ] + hash = unreserved + net-loc-list = net-loc, [ { ",", net-loc } ] + net-loc = hostname, [ ":" port ] + hostname = domain | IPv4address | IPv6address swiss-number = segment - nurl = scheme, hash, "@", net-loc-list, "/", swiss-number - -See https://tools.ietf.org/html/rfc4648#section-5 for the definition of ``urlsafe-base64-string`` -(RFC 4648 provides an ad hoc definition rather than EBNF). See https://tools.ietf.org/html/rfc3986#section-3.3 for the definition of ``segment``. +See https://tools.ietf.org/html/rfc2396#appendix-A for the definition of ``unreserved``. +See https://tools.ietf.org/html/draft-main-ipaddr-text-rep-02#section-3.1 for the definition of ``IPv4address``. +See https://tools.ietf.org/html/draft-main-ipaddr-text-rep-02#section-3.2 for the definition of ``IPv6address``. +See https://tools.ietf.org/html/rfc1035#section-2.3.1 for the definition of ``domain``. Versions -------- From c9b9f2d0ce4e5a82cbe4c56e25fd7a6e3010edb1 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Fri, 13 Nov 2020 13:57:36 -0500 Subject: [PATCH 04/91] Be explicit with the version Forward compatibility issues seem like a problem with the hash length based solution --- docs/specifications/url.rst | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/specifications/url.rst b/docs/specifications/url.rst index c8beddc1c..8b44e1951 100644 --- a/docs/specifications/url.rst +++ b/docs/specifications/url.rst @@ -46,7 +46,7 @@ Syntax The EBNF for a NURL is as follows:: - nurl = scheme, hash, "@", net-loc-list, "/", swiss-number + nurl = scheme, hash, "@", net-loc-list, "/", swiss-number, [ version1 ] scheme = "pb://" @@ -58,6 +58,8 @@ The EBNF for a NURL is as follows:: swiss-number = segment + version1 = "#v=1" + See https://tools.ietf.org/html/rfc3986#section-3.3 for the definition of ``segment``. See https://tools.ietf.org/html/rfc2396#appendix-A for the definition of ``unreserved``. See https://tools.ietf.org/html/draft-main-ipaddr-text-rep-02#section-3.1 for the definition of ``IPv4address``. @@ -76,7 +78,10 @@ Version 0 A Foolscap fURL is considered the canonical definition of a version 0 NURL. Notably, the hash component is defined as the base32-encoded SHA1 hash of the DER form of an x509v3 certificate. -A version 0 NURL is identified by the length of the hash string which is always 32 bytes. +A version 0 NURL is identified in two ways: + +* Primarily, by the absence of the ``v=1`` fragment. +* Secondarily, by the length of the hash string which is always 32 bytes. Version 1 --------- @@ -96,7 +101,9 @@ The hash component of a version 1 NURL differs in three ways from the prior vers 3. The hash is encoded using urlsafe-base64 (without padding) instead of base32. This provides a more compact representation and minimizes the usability impacts of switching from a 160 bit hash to a 224 bit hash. -A version 1 NURL is identified by the length of the hash string which is always 38 bytes. +A version 1 NURL is identified by the presence of the ``v=1`` fragment. +Though the length of the hash string (38 bytes) could also be used to differentiate it from a version 0 NURL, +there is no guarantee that this will be effective in differentiating it from future versions so this approach should not be used. It is possible for a client to unilaterally upgrade a version 0 NURL to a version 1 NURL. After establishing and authenticating a connection the client will have received a copy of the server's certificate. From b34afb819fab1fe877f1527df870bd79af1a6d69 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Fri, 13 Nov 2020 14:05:22 -0500 Subject: [PATCH 05/91] don't even hint at hash length as a version indicator --- docs/specifications/url.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/specifications/url.rst b/docs/specifications/url.rst index 8b44e1951..e83abb3fb 100644 --- a/docs/specifications/url.rst +++ b/docs/specifications/url.rst @@ -78,10 +78,7 @@ Version 0 A Foolscap fURL is considered the canonical definition of a version 0 NURL. Notably, the hash component is defined as the base32-encoded SHA1 hash of the DER form of an x509v3 certificate. -A version 0 NURL is identified in two ways: - -* Primarily, by the absence of the ``v=1`` fragment. -* Secondarily, by the length of the hash string which is always 32 bytes. +A version 0 NURL is identified by the absence of the ``v=1`` fragment. Version 1 --------- From 59902e037b1575d041cfb71b2ed856b329429a65 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Fri, 13 Nov 2020 14:11:21 -0500 Subject: [PATCH 06/91] some rst improvements --- docs/specifications/url.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/specifications/url.rst b/docs/specifications/url.rst index e83abb3fb..2bb554818 100644 --- a/docs/specifications/url.rst +++ b/docs/specifications/url.rst @@ -33,13 +33,13 @@ NURLs The authentication and authorization properties of fURLs are a good fit for Tahoe-LAFS' requirements. These are not inherently tied to the Foolscap protocol itself. -In particular they are beneficial to :doc:`http-storage-node-protocol` which uses HTTP instead of Foolscap. +In particular they are beneficial to :doc:`../proposed/http-storage-node-protocol` which uses HTTP instead of Foolscap. It is conceivable they will also be used with WebSockets at some point as well. Continuing to refer to these URLs as fURLs when they are being used for other protocols may cause confusion. Therefore, -this document coins the name *NURL* for these URLs. -This can be considered to expand to "New URLs" or "Authe*N*ticating URLs" or "Authorizi*N*g URLs" as the reader prefers. +this document coins the name **NURL** for these URLs. +This can be considered to expand to "**N**\ ew URLs" or "Authe\ **N**\ ticating URLs" or "Authorizi\ **N**\ g URLs" as the reader prefers. Syntax ------ @@ -109,3 +109,4 @@ This provides stronger authentication assurances for future uses but it is not r .. _`continues to be eroded`: https://en.wikipedia.org/wiki/SHA-1#Cryptanalysis_and_validation .. _`explored by the web community`: https://www.imperialviolet.org/2011/05/04/pinning.html +.. _Foolscap: https://github.com/warner/foolscap From 2205e144f97d8fc14923ee29b0a659419e7cf855 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Fri, 13 Nov 2020 21:06:58 -0500 Subject: [PATCH 07/91] Stop pointing folks at [client]introducer.furl in the docs --- docs/configuration.rst | 45 +++++++++++++++++-------------- docs/historical/configuration.rst | 2 +- docs/running.rst | 6 ++--- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index ab4751a04..8583888ca 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -398,13 +398,13 @@ This section controls *when* Tor and I2P are used. The ``[tor]`` and ``[i2p]`` sections (described later) control *how* Tor/I2P connections are managed. -All Tahoe nodes need to make a connection to the Introducer; the ``[client] -introducer.furl`` setting (described below) indicates where the Introducer -lives. Tahoe client nodes must also make connections to storage servers: -these targets are specified in announcements that come from the Introducer. -Both are expressed as FURLs (a Foolscap URL), which include a list of -"connection hints". Each connection hint describes one (of perhaps many) -network endpoints where the service might live. +All Tahoe nodes need to make a connection to the Introducer; the +``private/introducers.yaml`` file (described below) configures where the +Introducer lives. Tahoe client nodes must also make connections to storage +servers: these targets are specified in announcements that come from the +Introducer. Both are expressed as FURLs (a Foolscap URL), which include a +list of "connection hints". Each connection hint describes one (of perhaps +many) network endpoints where the service might live. Connection hints include a type, and look like: @@ -580,6 +580,8 @@ Client Configuration ``introducer.furl = (FURL string, mandatory)`` + DEPRECATED. See :ref:`introducer-definitions`. + This FURL tells the client how to connect to the introducer. Each Tahoe-LAFS grid is defined by an introducer. The introducer's FURL is created by the introducer node and written into its private base @@ -965,29 +967,28 @@ This section describes these other files. with as many people as possible, put the empty string (so that ``private/convergence`` is a zero-length file). -Additional Introducer Definitions -================================= +.. _introducer-definitions: -The ``private/introducers.yaml`` file defines additional Introducers. The -first introducer is defined in ``tahoe.cfg``, in ``[client] -introducer.furl``. To use two or more Introducers, choose a locally-unique -"petname" for each one, then define their FURLs in -``private/introducers.yaml`` like this:: +Introducer Definitions +====================== + +The ``private/introducers.yaml`` file defines Introducers. +Choose a locally-unique "petname" for each one then define their FURLs in ``private/introducers.yaml`` like this:: introducers: petname2: - furl: FURL2 + furl: "FURL2" petname3: - furl: FURL3 + furl: "FURL3" Servers will announce themselves to all configured introducers. Clients will merge the announcements they receive from all introducers. Nothing will re-broadcast an announcement (i.e. telling introducer 2 about something you heard from introducer 1). -If you omit the introducer definitions from both ``tahoe.cfg`` and -``introducers.yaml``, the node will not use an Introducer at all. Such -"introducerless" clients must be configured with static servers (described +If you omit the introducer definitions from ``introducers.yaml``, +the node will not use an Introducer at all. +Such "introducerless" clients must be configured with static servers (described below), or they will not be able to upload and download files. Static Server Definitions @@ -1152,7 +1153,6 @@ a legal one. timeout.disconnect = 1800 [client] - introducer.furl = pb://ok45ssoklj4y7eok5c3xkmj@tcp:tahoe.example:44801/ii3uumo helper.furl = pb://ggti5ssoklj4y7eok5c3xkmj@tcp:helper.tahoe.example:7054/kk8lhr [storage] @@ -1163,6 +1163,11 @@ a legal one. [helper] enabled = True +To be introduced to storage servers, here is a sample ``private/introducers.yaml`` which can be used in conjunction:: + + introducers: + examplegrid: + furl: "pb://ok45ssoklj4y7eok5c3xkmj@tcp:tahoe.example:44801/ii3uumo" Old Configuration Files ======================= diff --git a/docs/historical/configuration.rst b/docs/historical/configuration.rst index 660bc8489..7d9b9fbe4 100644 --- a/docs/historical/configuration.rst +++ b/docs/historical/configuration.rst @@ -20,7 +20,7 @@ Config setting File Comment ``[node]log_gatherer.furl`` ``BASEDIR/log_gatherer.furl`` (one per line) ``[node]timeout.keepalive`` ``BASEDIR/keepalive_timeout`` ``[node]timeout.disconnect`` ``BASEDIR/disconnect_timeout`` -``[client]introducer.furl`` ``BASEDIR/introducer.furl`` + ``BASEDIR/introducer.furl`` ``BASEDIR/private/introducers.yaml`` ``[client]helper.furl`` ``BASEDIR/helper.furl`` ``[client]key_generator.furl`` ``BASEDIR/key_generator.furl`` ``[client]stats_gatherer.furl`` ``BASEDIR/stats_gatherer.furl`` diff --git a/docs/running.rst b/docs/running.rst index 2b43adf75..ef6ba42ed 100644 --- a/docs/running.rst +++ b/docs/running.rst @@ -65,9 +65,9 @@ Running a Client To construct a client node, run “``tahoe create-client``”, which will create ``~/.tahoe`` to be the node's base directory. Acquire the ``introducer.furl`` (see below if you are running your own introducer, or use the one from the -`TestGrid page`_), and paste it after ``introducer.furl =`` in the -``[client]`` section of ``~/.tahoe/tahoe.cfg``. Then use “``tahoe run -~/.tahoe``”. After that, the node should be off and running. The first thing +`TestGrid page`_), and write it to ``~/.tahoe/private/introducers.yaml`` +(see :ref:`introducer-definitions`). Then use “``tahoe run ~/.tahoe``”. +After that, the node should be off and running. The first thing it will do is connect to the introducer and get itself connected to all other nodes on the grid. From bcef851ae04aee3ec0cd15100282516b60a92b5d Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Fri, 13 Nov 2020 21:08:19 -0500 Subject: [PATCH 08/91] news fragment --- newsfragments/3504.configuration | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/3504.configuration diff --git a/newsfragments/3504.configuration b/newsfragments/3504.configuration new file mode 100644 index 000000000..9ff74482c --- /dev/null +++ b/newsfragments/3504.configuration @@ -0,0 +1 @@ +The ``[client]introducer.furl`` configuration item is now deprecated in favor of the ``private/introducers.yaml`` file. \ No newline at end of file From bef5ccd0cab356fc534b16b65b8a6af960fcfea2 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 09:12:14 -0500 Subject: [PATCH 09/91] Move the introducer config reading code into _Config --- src/allmydata/client.py | 45 ++-------------------------- src/allmydata/node.py | 65 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 42 deletions(-) diff --git a/src/allmydata/client.py b/src/allmydata/client.py index a768ba354..8e9f8f9e8 100644 --- a/src/allmydata/client.py +++ b/src/allmydata/client.py @@ -1,7 +1,6 @@ import os, stat, time, weakref from base64 import urlsafe_b64encode from functools import partial -from errno import ENOENT, EPERM # On Python 2 this will be the backported package: from configparser import NoSectionError @@ -464,51 +463,13 @@ def create_introducer_clients(config, main_tub, _introducer_factory=None): # we return this list introducer_clients = [] - introducers_yaml_filename = config.get_private_path("introducers.yaml") - introducers_filepath = FilePath(introducers_yaml_filename) + introducers = config.get_introducer_configuration() - try: - with introducers_filepath.open() as f: - introducers_yaml = yamlutil.safe_load(f) - if introducers_yaml is None: - raise EnvironmentError( - EPERM, - "Can't read '{}'".format(introducers_yaml_filename), - introducers_yaml_filename, - ) - introducers = introducers_yaml.get("introducers", {}) - log.msg( - "found {} introducers in private/introducers.yaml".format( - len(introducers), - ) - ) - except EnvironmentError as e: - if e.errno != ENOENT: - raise - introducers = {} - - if "default" in introducers.keys(): - raise ValueError( - "'default' introducer furl cannot be specified in introducers.yaml;" - " please fix impossible configuration." - ) - - # read furl from tahoe.cfg - tahoe_cfg_introducer_furl = config.get_config("client", "introducer.furl", None) - if tahoe_cfg_introducer_furl == "None": - raise ValueError( - "tahoe.cfg has invalid 'introducer.furl = None':" - " to disable it, use 'introducer.furl ='" - " or omit the key entirely" - ) - if tahoe_cfg_introducer_furl: - introducers[u'default'] = {'furl':tahoe_cfg_introducer_furl} - - for petname, introducer in introducers.items(): + for petname, introducer_furl in introducers.items(): introducer_cache_filepath = FilePath(config.get_private_path("introducer_{}_cache.yaml".format(petname))) ic = _introducer_factory( main_tub, - introducer['furl'].encode("ascii"), + introducer_furl.encode("ascii"), config.nickname, str(allmydata.__full_version__), str(_Client.OLDEST_SUPPORTED_VERSION), diff --git a/src/allmydata/node.py b/src/allmydata/node.py index 9e7143fd4..7dc579a83 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -21,10 +21,13 @@ import types import errno import tempfile from base64 import b32decode, b32encode +from errno import ENOENT, EPERM +from warnings import warn # On Python 2 this will be the backported package. import configparser +from twisted.python.filepath import FilePath from twisted.python import log as twlog from twisted.application import service from twisted.python.failure import Failure @@ -37,6 +40,9 @@ from allmydata.util.assertutil import _assert from allmydata.util.fileutil import abspath_expanduser_unicode from allmydata.util.encodingutil import get_filesystem_encoding, quote_output from allmydata.util import configutil +from allmydata.util.yamlutil import ( + safe_load, +) def _common_valid_config(): return configutil.ValidConfiguration({ @@ -428,6 +434,65 @@ class _Config(object): os.path.join(self._basedir, *args) ) + def get_introducer_configuration(self): + """ + Get configuration for introducers. + + :return {unicode: unicode}: A mapping from introducer petname to the + introducer's fURL. + """ + introducers_yaml_filename = self.get_private_path("introducers.yaml") + introducers_filepath = FilePath(introducers_yaml_filename) + + try: + with introducers_filepath.open() as f: + introducers_yaml = safe_load(f) + if introducers_yaml is None: + raise EnvironmentError( + EPERM, + "Can't read '{}'".format(introducers_yaml_filename), + introducers_yaml_filename, + ) + introducers = { + petname: config["furl"] + for petname, config + in introducers_yaml.get("introducers", {}).items() + } + log.msg( + "found {} introducers in private/introducers.yaml".format( + len(introducers), + ) + ) + except EnvironmentError as e: + if e.errno != ENOENT: + raise + introducers = {} + + if "default" in introducers.keys(): + raise ValueError( + "'default' introducer furl cannot be specified in introducers.yaml;" + " please fix impossible configuration." + ) + + # read furl from tahoe.cfg + tahoe_cfg_introducer_furl = self.get_config("client", "introducer.furl", None) + if tahoe_cfg_introducer_furl == "None": + raise ValueError( + "tahoe.cfg has invalid 'introducer.furl = None':" + " to disable it, use 'introducer.furl ='" + " or omit the key entirely" + ) + if tahoe_cfg_introducer_furl: + warn( + "tahoe.cfg [client]introducer.furl is deprecated; " + "use private/introducers.yaml instead.", + category=DeprecationWarning, + stacklevel=-1, + ) + introducers['default'] = tahoe_cfg_introducer_furl + + return introducers + def create_tub_options(config): """ From e0f69dcfcf26e5faabcb3fca13d69ba3e4567602 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 09:26:07 -0500 Subject: [PATCH 10/91] Get the path manipulation into _Config too --- src/allmydata/client.py | 7 +++---- src/allmydata/node.py | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/allmydata/client.py b/src/allmydata/client.py index 8e9f8f9e8..ed65917ed 100644 --- a/src/allmydata/client.py +++ b/src/allmydata/client.py @@ -465,17 +465,16 @@ def create_introducer_clients(config, main_tub, _introducer_factory=None): introducers = config.get_introducer_configuration() - for petname, introducer_furl in introducers.items(): - introducer_cache_filepath = FilePath(config.get_private_path("introducer_{}_cache.yaml".format(petname))) + for petname, (furl, cache_path) in introducers.items(): ic = _introducer_factory( main_tub, - introducer_furl.encode("ascii"), + furl.encode("ascii"), config.nickname, str(allmydata.__full_version__), str(_Client.OLDEST_SUPPORTED_VERSION), list(node.get_app_versions()), partial(_sequencer, config), - introducer_cache_filepath, + cache_path, ) introducer_clients.append(ic) return introducer_clients diff --git a/src/allmydata/node.py b/src/allmydata/node.py index 7dc579a83..7b8c5d65a 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -438,12 +438,17 @@ class _Config(object): """ Get configuration for introducers. - :return {unicode: unicode}: A mapping from introducer petname to the - introducer's fURL. + :return {unicode: (unicode, FilePath)}: A mapping from introducer + petname to a tuple of the introducer's fURL and local cache path. """ introducers_yaml_filename = self.get_private_path("introducers.yaml") introducers_filepath = FilePath(introducers_yaml_filename) + def get_cache_filepath(petname): + return FilePath( + self.get_private_path("introducer_{}_cache.yaml".format(petname)), + ) + try: with introducers_filepath.open() as f: introducers_yaml = safe_load(f) @@ -491,7 +496,11 @@ class _Config(object): ) introducers['default'] = tahoe_cfg_introducer_furl - return introducers + return { + petname: (furl, get_cache_filepath(petname)) + for (petname, furl) + in introducers.items() + } def create_tub_options(config): From 25666ee49c2b5a6bb238aa5c221013346fabd662 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 09:57:17 -0500 Subject: [PATCH 11/91] Get rid of [client]introducer.furl from test_client --- src/allmydata/test/test_client.py | 57 +++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/src/allmydata/test/test_client.py b/src/allmydata/test/test_client.py index 0f0648a4c..8e6bcd6ec 100644 --- a/src/allmydata/test/test_client.py +++ b/src/allmydata/test/test_client.py @@ -75,13 +75,7 @@ from .matchers import ( SOME_FURL = b"pb://abcde@nowhere/fake" -BASECONFIG = ("[client]\n" - "introducer.furl = \n" - ) - -BASECONFIG_I = ("[client]\n" - "introducer.furl = %s\n" - ) +BASECONFIG = "[client]\n" class Basic(testutil.ReallyEqualMixin, unittest.TestCase): def test_loadable(self): @@ -660,6 +654,22 @@ def flush_but_dont_ignore(res): return d +def write_introducer(basedir, petname, furl): + """ + Overwrite the node's ``introducers.yaml`` with a file containing the given + introducer information. + """ + FilePath(basedir).child(b"private").child(b"introducers.yaml").setContent( + safe_dump({ + "introducers": { + petname: { + "furl": furl, + }, + }, + }), + ) + + class AnonymousStorage(SyncTestCase): """ Tests for behaviors of the client object with respect to the anonymous @@ -672,10 +682,11 @@ class AnonymousStorage(SyncTestCase): """ basedir = self.id() os.makedirs(basedir + b"/private") + write_introducer(basedir, "someintroducer", SOME_FURL) config = client.config_from_string( basedir, "tub.port", - BASECONFIG_I % (SOME_FURL,) + ( + BASECONFIG + ( "[storage]\n" "enabled = true\n" "anonymous = true\n" @@ -703,10 +714,11 @@ class AnonymousStorage(SyncTestCase): """ basedir = self.id() os.makedirs(basedir + b"/private") + write_introducer(basedir, "someintroducer", SOME_FURL) config = client.config_from_string( basedir, "tub.port", - BASECONFIG_I % (SOME_FURL,) + ( + BASECONFIG + ( "[storage]\n" "enabled = true\n" "anonymous = false\n" @@ -743,7 +755,7 @@ class AnonymousStorage(SyncTestCase): enabled_config = client.config_from_string( basedir, "tub.port", - BASECONFIG_I % (SOME_FURL,) + ( + BASECONFIG + ( "[storage]\n" "enabled = true\n" "anonymous = true\n" @@ -767,7 +779,7 @@ class AnonymousStorage(SyncTestCase): disabled_config = client.config_from_string( basedir, "tub.port", - BASECONFIG_I % (SOME_FURL,) + ( + BASECONFIG + ( "[storage]\n" "enabled = true\n" "anonymous = false\n" @@ -953,19 +965,25 @@ class Run(unittest.TestCase, testutil.StallMixin): @defer.inlineCallbacks def test_loadable(self): + """ + A configuration consisting only of an introducer can be turned into a + client node. + """ basedir = "test_client.Run.test_loadable" - os.mkdir(basedir) + os.makedirs(basedir + b"/private") dummy = "pb://wl74cyahejagspqgy4x5ukrvfnevlknt@127.0.0.1:58889/bogus" - fileutil.write(os.path.join(basedir, "tahoe.cfg"), BASECONFIG_I % dummy) + write_introducer(basedir, "someintroducer", dummy) + fileutil.write(os.path.join(basedir, "tahoe.cfg"), BASECONFIG) fileutil.write(os.path.join(basedir, client._Client.EXIT_TRIGGER_FILE), "") yield client.create_client(basedir) @defer.inlineCallbacks def test_reloadable(self): basedir = "test_client.Run.test_reloadable" - os.mkdir(basedir) + os.makedirs(basedir + b"/private") dummy = "pb://wl74cyahejagspqgy4x5ukrvfnevlknt@127.0.0.1:58889/bogus" - fileutil.write(os.path.join(basedir, "tahoe.cfg"), BASECONFIG_I % dummy) + write_introducer(basedir, "someintroducer", dummy) + fileutil.write(os.path.join(basedir, "tahoe.cfg"), BASECONFIG) c1 = yield client.create_client(basedir) c1.setServiceParent(self.sparent) @@ -1129,10 +1147,16 @@ class StorageAnnouncementTests(SyncTestCase): super(StorageAnnouncementTests, self).setUp() self.basedir = self.useFixture(TempDir()).path create_node_dir(self.basedir, u"") + # Write an introducer configuration or we can't observer + # announcements. + write_introducer(self.basedir, "someintroducer", SOME_FURL) def get_config(self, storage_enabled, more_storage="", more_sections=""): return """ +[client] +# Empty + [node] tub.location = tcp:192.0.2.0:1234 @@ -1140,9 +1164,6 @@ tub.location = tcp:192.0.2.0:1234 enabled = {storage_enabled} {more_storage} -[client] -introducer.furl = pb://abcde@nowhere/fake - {more_sections} """.format( storage_enabled=storage_enabled, From b202f81fd1b46fef73c744b05a545c42123381f8 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 10:23:07 -0500 Subject: [PATCH 12/91] move config helper to shared location --- src/allmydata/test/common.py | 20 ++++++++++++++++++++ src/allmydata/test/test_client.py | 17 +---------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/allmydata/test/common.py b/src/allmydata/test/common.py index 1cf1d6428..2bf3ca50d 100644 --- a/src/allmydata/test/common.py +++ b/src/allmydata/test/common.py @@ -32,6 +32,10 @@ import attr import treq +from yaml import ( + safe_dump, +) + from zope.interface import implementer from testtools import ( @@ -100,6 +104,22 @@ EMPTY_CLIENT_CONFIG = config_from_string( ) +def write_introducer(basedir, petname, furl): + """ + Overwrite the node's ``introducers.yaml`` with a file containing the given + introducer information. + """ + FilePath(basedir).child(b"private").child(b"introducers.yaml").setContent( + safe_dump({ + "introducers": { + petname: { + "furl": furl, + }, + }, + }), + ) + + @attr.s class MemoryIntroducerClient(object): """ diff --git a/src/allmydata/test/test_client.py b/src/allmydata/test/test_client.py index 8e6bcd6ec..b76047be0 100644 --- a/src/allmydata/test/test_client.py +++ b/src/allmydata/test/test_client.py @@ -66,6 +66,7 @@ from .common import ( UseTestPlugins, MemoryIntroducerClient, get_published_announcements, + write_introducer, ) from .matchers import ( MatchesSameElements, @@ -654,22 +655,6 @@ def flush_but_dont_ignore(res): return d -def write_introducer(basedir, petname, furl): - """ - Overwrite the node's ``introducers.yaml`` with a file containing the given - introducer information. - """ - FilePath(basedir).child(b"private").child(b"introducers.yaml").setContent( - safe_dump({ - "introducers": { - petname: { - "furl": furl, - }, - }, - }), - ) - - class AnonymousStorage(SyncTestCase): """ Tests for behaviors of the client object with respect to the anonymous From 0664416f6594dea2e6f25d7d57805c0cf63eb3a5 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 10:23:28 -0500 Subject: [PATCH 13/91] Remove [client]introducer.furl from test_introducer --- src/allmydata/node.py | 11 +++++------ src/allmydata/test/test_introducer.py | 17 ++++++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/allmydata/node.py b/src/allmydata/node.py index 7b8c5d65a..87a9579d8 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -473,12 +473,6 @@ class _Config(object): raise introducers = {} - if "default" in introducers.keys(): - raise ValueError( - "'default' introducer furl cannot be specified in introducers.yaml;" - " please fix impossible configuration." - ) - # read furl from tahoe.cfg tahoe_cfg_introducer_furl = self.get_config("client", "introducer.furl", None) if tahoe_cfg_introducer_furl == "None": @@ -494,6 +488,11 @@ class _Config(object): category=DeprecationWarning, stacklevel=-1, ) + if "default" in introducers: + raise ValueError( + "'default' introducer furl cannot be specified in tahoe.cfg and introducers.yaml;" + " please fix impossible configuration." + ) introducers['default'] = tahoe_cfg_introducer_furl return { diff --git a/src/allmydata/test/test_introducer.py b/src/allmydata/test/test_introducer.py index d99e18c4a..4ccabd4e4 100644 --- a/src/allmydata/test/test_introducer.py +++ b/src/allmydata/test/test_introducer.py @@ -40,7 +40,8 @@ from allmydata.util.iputil import ( listenOnUnused, ) import allmydata.test.common_util as testutil -from allmydata.test.common import ( +from .common import ( + write_introducer, SyncTestCase, AsyncTestCase, AsyncBrokenTestCase, @@ -788,8 +789,13 @@ class Announcements(AsyncTestCase): @defer.inlineCallbacks def test_client_cache(self): + """ + Announcements received by an introducer client are written to that + introducer client's cache file. + """ basedir = "introducer/ClientSeqnums/test_client_cache_1" - fileutil.make_dirs(basedir) + fileutil.make_dirs(basedir + b"/private") + write_introducer(basedir, "default", "nope") cache_filepath = FilePath(os.path.join(basedir, "private", "introducer_default_cache.yaml")) @@ -798,8 +804,6 @@ class Announcements(AsyncTestCase): # until the introducer connection is established). To avoid getting # confused by this, disable storage. with open(os.path.join(basedir, "tahoe.cfg"), "w") as f: - f.write("[client]\n") - f.write("introducer.furl = nope\n") f.write("[storage]\n") f.write("enabled = false\n") @@ -880,14 +884,13 @@ class ClientSeqnums(AsyncBrokenTestCase): @defer.inlineCallbacks def test_client(self): basedir = "introducer/ClientSeqnums/test_client" - fileutil.make_dirs(basedir) + fileutil.make_dirs(basedir + b"/private") + write_introducer(basedir, "default", "nope") # if storage is enabled, the Client will publish its storage server # during startup (although the announcement will wait in a queue # until the introducer connection is established). To avoid getting # confused by this, disable storage. f = open(os.path.join(basedir, "tahoe.cfg"), "w") - f.write("[client]\n") - f.write("introducer.furl = nope\n") f.write("[storage]\n") f.write("enabled = false\n") f.close() From fabcc079c51e1048f867301087e8fc495d983b94 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 10:58:01 -0500 Subject: [PATCH 14/91] we're not testing the yaml library --- src/allmydata/test/test_multi_introducers.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/allmydata/test/test_multi_introducers.py b/src/allmydata/test/test_multi_introducers.py index 34e6e5d96..2d057e604 100644 --- a/src/allmydata/test/test_multi_introducers.py +++ b/src/allmydata/test/test_multi_introducers.py @@ -52,18 +52,6 @@ class MultiIntroTests(unittest.TestCase): # assertions self.failUnlessEqual(ic_count, 3) - @defer.inlineCallbacks - def test_introducer_count_commented(self): - """ Ensure that the Client creates same number of introducer clients - as found in "basedir/private/introducers" config file when there is one - commented.""" - self.yaml_path.setContent(INTRODUCERS_CFG_FURLS_COMMENTED) - # get a client and count of introducer_clients - myclient = yield create_client(self.basedir) - ic_count = len(myclient.introducer_clients) - - # assertions - self.failUnlessEqual(ic_count, 2) @defer.inlineCallbacks def test_read_introducer_furl_from_tahoecfg(self): From b181b577e8f1a123a5847411df0ff2dc69878c80 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 11:09:40 -0500 Subject: [PATCH 15/91] Remove [client]introducer.furl from test_multi_introducers (mostly) Leave in this one test to demonstrate the deprecated functionality still works, until we delete it entirely. --- src/allmydata/test/test_multi_introducers.py | 50 +++++++++++++------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/src/allmydata/test/test_multi_introducers.py b/src/allmydata/test/test_multi_introducers.py index 2d057e604..36801c740 100644 --- a/src/allmydata/test/test_multi_introducers.py +++ b/src/allmydata/test/test_multi_introducers.py @@ -24,9 +24,6 @@ class MultiIntroTests(unittest.TestCase): config = {'hide-ip':False, 'listen': 'tcp', 'port': None, 'location': None, 'hostname': 'example.net'} write_node_config(c, config) - fake_furl = "furl1" - c.write("[client]\n") - c.write("introducer.furl = %s\n" % fake_furl) c.write("[storage]\n") c.write("enabled = false\n") c.close() @@ -36,8 +33,10 @@ class MultiIntroTests(unittest.TestCase): @defer.inlineCallbacks def test_introducer_count(self): - """ Ensure that the Client creates same number of introducer clients - as found in "basedir/private/introducers" config file. """ + """ + If there are two introducers configured in ``introducers.yaml`` then + ``Client`` creates two introducer clients. + """ connections = { 'introducers': { u'intro1':{ 'furl': 'furl1' }, @@ -50,13 +49,13 @@ class MultiIntroTests(unittest.TestCase): ic_count = len(myclient.introducer_clients) # assertions - self.failUnlessEqual(ic_count, 3) - + self.failUnlessEqual(ic_count, len(connections["introducers"])) @defer.inlineCallbacks def test_read_introducer_furl_from_tahoecfg(self): - """ Ensure that the Client reads the introducer.furl config item from - the tahoe.cfg file. """ + """ + The deprecated [client]introducer.furl item is still read and respected. + """ # create a custom tahoe.cfg c = open(os.path.join(self.basedir, "tahoe.cfg"), "w") config = {'hide-ip':False, 'listen': 'tcp', @@ -75,20 +74,41 @@ class MultiIntroTests(unittest.TestCase): # assertions self.failUnlessEqual(fake_furl, tahoe_cfg_furl) + self.assertEqual( + list( + warning["message"] + for warning + in self.flushWarnings() + if warning["category"] is DeprecationWarning + ), + ["tahoe.cfg [client]introducer.furl is deprecated; " + "use private/introducers.yaml instead."], + ) @defer.inlineCallbacks def test_reject_default_in_yaml(self): - connections = {'introducers': { - u'default': { 'furl': 'furl1' }, - }} + """ + If an introducer is configured in tahoe.cfg then a "default" introducer in + introducers.yaml is rejected. + """ + connections = { + 'introducers': { + u'default': { 'furl': 'furl1' }, + }, + } self.yaml_path.setContent(yamlutil.safe_dump(connections)) + FilePath(self.basedir).child("tahoe.cfg").setContent( + "[client]\n" + "introducer.furl = furl1\n" + ) + with self.assertRaises(ValueError) as ctx: yield create_client(self.basedir) self.assertEquals( str(ctx.exception), - "'default' introducer furl cannot be specified in introducers.yaml; please " - "fix impossible configuration.", + "'default' introducer furl cannot be specified in tahoe.cfg and introducers.yaml; " + "please fix impossible configuration.", ) SIMPLE_YAML = """ @@ -114,8 +134,6 @@ class NoDefault(unittest.TestCase): config = {'hide-ip':False, 'listen': 'tcp', 'port': None, 'location': None, 'hostname': 'example.net'} write_node_config(c, config) - c.write("[client]\n") - c.write("# introducer.furl =\n") # omit default c.write("[storage]\n") c.write("enabled = false\n") c.close() From b6bebc514a8efe0b1ad2237dca91bc10da434bf2 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 11:22:01 -0500 Subject: [PATCH 16/91] Remove [client]introducer.furl from test_node --- src/allmydata/test/test_node.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/allmydata/test/test_node.py b/src/allmydata/test/test_node.py index 693183ea4..c758262f1 100644 --- a/src/allmydata/test/test_node.py +++ b/src/allmydata/test/test_node.py @@ -605,8 +605,6 @@ class TestMissingPorts(unittest.TestCase): BASE_CONFIG = """ -[client] -introducer.furl = empty [tor] enabled = false [i2p] From 0f4e34c41da6d9ce5461f219a06d71bb5c218714 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 11:32:58 -0500 Subject: [PATCH 17/91] Take [client]introducer.furl out of the UseNode fixture --- src/allmydata/test/common.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/common.py b/src/allmydata/test/common.py index 2bf3ca50d..f07a3e74f 100644 --- a/src/allmydata/test/common.py +++ b/src/allmydata/test/common.py @@ -267,6 +267,11 @@ class UseNode(object): config=format_config_items(self.plugin_config), ) + write_introducer( + self.basedir.asBytesMode().path, + "default", + self.introducer_furl, + ) self.config = config_from_string( self.basedir.asTextMode().path, "tub.port", @@ -275,11 +280,9 @@ class UseNode(object): {node_config} [client] -introducer.furl = {furl} storage.plugins = {storage_plugin} {plugin_config_section} """.format( - furl=self.introducer_furl, storage_plugin=self.storage_plugin, node_config=format_config_items(self.node_config), plugin_config_section=plugin_config_section, From 18e327417c740b4f92a0882c9c3d8d2972e3f4d5 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 11:43:28 -0500 Subject: [PATCH 18/91] Get [client]introducer.furl out of test_system --- src/allmydata/test/test_system.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/allmydata/test/test_system.py b/src/allmydata/test/test_system.py index 21da0a914..14bcb9c47 100644 --- a/src/allmydata/test/test_system.py +++ b/src/allmydata/test/test_system.py @@ -47,6 +47,9 @@ from .web.common import ( from allmydata.test.test_runner import RunBinTahoeMixin from . import common_util as testutil from .common_util import run_cli +from .common import ( + write_introducer, +) LARGE_DATA = """ This is some data to publish to the remote grid.., which needs to be large @@ -806,8 +809,6 @@ class SystemTestMixin(pollmixin.PollMixin, testutil.StallMixin): except1 = set(range(self.numclients)) - {1} feature_matrix = { - # client 1 uses private/introducers.yaml, not tahoe.cfg - ("client", "introducer.furl"): except1, ("client", "nickname"): except1, # client 1 has to auto-assign an address. @@ -833,7 +834,6 @@ class SystemTestMixin(pollmixin.PollMixin, testutil.StallMixin): setnode = partial(setconf, config, which, "node") sethelper = partial(setconf, config, which, "helper") - setclient("introducer.furl", self.introducer_furl) setnode("nickname", u"client %d \N{BLACK SMILING FACE}" % (which,)) if self.stats_gatherer_furl: @@ -850,13 +850,11 @@ class SystemTestMixin(pollmixin.PollMixin, testutil.StallMixin): sethelper("enabled", "True") - if which == 1: - # clients[1] uses private/introducers.yaml, not tahoe.cfg - iyaml = ("introducers:\n" - " petname2:\n" - " furl: %s\n") % self.introducer_furl - iyaml_fn = os.path.join(basedir, "private", "introducers.yaml") - fileutil.write(iyaml_fn, iyaml) + iyaml = ("introducers:\n" + " petname2:\n" + " furl: %s\n") % self.introducer_furl + iyaml_fn = os.path.join(basedir, "private", "introducers.yaml") + fileutil.write(iyaml_fn, iyaml) return _render_config(config) @@ -909,10 +907,15 @@ class SystemTestMixin(pollmixin.PollMixin, testutil.StallMixin): if not os.path.isdir(basedir): fileutil.make_dirs(basedir) config = "[client]\n" - config += "introducer.furl = %s\n" % self.introducer_furl if helper_furl: config += "helper.furl = %s\n" % helper_furl fileutil.write(os.path.join(basedir, 'tahoe.cfg'), config) + os.makedirs(basedir + b"/private") + write_introducer( + basedir, + "default", + self.introducer_furl, + ) c = yield client.create_client(basedir) self.clients.append(c) From 22973e6951af6a320473862534743586b173741e Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 12:36:22 -0500 Subject: [PATCH 19/91] Attempt to make Python 3 happier --- src/allmydata/test/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/common.py b/src/allmydata/test/common.py index f07a3e74f..4475e287a 100644 --- a/src/allmydata/test/common.py +++ b/src/allmydata/test/common.py @@ -116,7 +116,7 @@ def write_introducer(basedir, petname, furl): "furl": furl, }, }, - }), + }).encode("ascii"), ) From 7b2d76c7ecf0db54990dc482ded4c9c6ef88143d Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 16:12:07 -0500 Subject: [PATCH 20/91] Another effort to make this simultaneously Py2/Py3 friendly --- src/allmydata/test/common.py | 2 +- src/allmydata/test/test_storage_client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/common.py b/src/allmydata/test/common.py index 4475e287a..c4f937aad 100644 --- a/src/allmydata/test/common.py +++ b/src/allmydata/test/common.py @@ -243,7 +243,7 @@ class UseNode(object): plugin_config = attr.ib() storage_plugin = attr.ib() basedir = attr.ib() - introducer_furl = attr.ib() + introducer_furl = attr.ib(validator=attr.validators.instance_of(bytes)) node_config = attr.ib(default=attr.Factory(dict)) config = attr.ib(default=None) diff --git a/src/allmydata/test/test_storage_client.py b/src/allmydata/test/test_storage_client.py index fa3a34b15..421c588ff 100644 --- a/src/allmydata/test/test_storage_client.py +++ b/src/allmydata/test/test_storage_client.py @@ -458,7 +458,7 @@ class StoragePluginWebPresence(AsyncTestCase): }, storage_plugin=self.storage_plugin, basedir=self.basedir, - introducer_furl=ensure_text(SOME_FURL), + introducer_furl=SOME_FURL, )) self.node = yield self.node_fixture.create_node() self.webish = self.node.getServiceNamed(WebishServer.name) From 06fe3869ef68d4db7208a6b150f4c327afdb2e2b Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 16:36:51 -0500 Subject: [PATCH 21/91] is pyyaml screwing it up? --- src/allmydata/node.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/allmydata/node.py b/src/allmydata/node.py index 87a9579d8..680e8e1a7 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -463,6 +463,8 @@ class _Config(object): for petname, config in introducers_yaml.get("introducers", {}).items() } + assert all(isinstance(unicode, k) for k in introducers.keys()) + assert all(isinstance(unicode, v) for v in introducers.values()) log.msg( "found {} introducers in private/introducers.yaml".format( len(introducers), From 4963e78f9f7501b3112e9e7f57afb004a11c212d Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 16:46:46 -0500 Subject: [PATCH 22/91] speed up ci-based testing --- .circleci/config.yml | 100 +++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index afa3fafa1..424fc1dd4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,70 +26,70 @@ workflows: # Contexts are managed in the CircleCI web interface: # # https://app.circleci.com/settings/organization/github/tahoe-lafs/contexts - - "debian-9": &DOCKERHUB_CONTEXT - context: "dockerhub-auth" + # - "debian-9": &DOCKERHUB_CONTEXT + # context: "dockerhub-auth" - - "debian-8": - <<: *DOCKERHUB_CONTEXT - requires: - - "debian-9" + # - "debian-8": + # <<: *DOCKERHUB_CONTEXT + # requires: + # - "debian-9" - - "ubuntu-20-04": - <<: *DOCKERHUB_CONTEXT - - "ubuntu-18-04": - <<: *DOCKERHUB_CONTEXT - requires: - - "ubuntu-20-04" - - "ubuntu-16-04": - <<: *DOCKERHUB_CONTEXT - requires: - - "ubuntu-20-04" + # - "ubuntu-20-04": + # <<: *DOCKERHUB_CONTEXT + # - "ubuntu-18-04": + # <<: *DOCKERHUB_CONTEXT + # requires: + # - "ubuntu-20-04" + # - "ubuntu-16-04": + # <<: *DOCKERHUB_CONTEXT + # requires: + # - "ubuntu-20-04" - - "fedora-29": - <<: *DOCKERHUB_CONTEXT - - "fedora-28": - <<: *DOCKERHUB_CONTEXT - requires: - - "fedora-29" + # - "fedora-29": + # <<: *DOCKERHUB_CONTEXT + # - "fedora-28": + # <<: *DOCKERHUB_CONTEXT + # requires: + # - "fedora-29" - - "centos-8": - <<: *DOCKERHUB_CONTEXT + # - "centos-8": + # <<: *DOCKERHUB_CONTEXT - - "nixos-19-09": - <<: *DOCKERHUB_CONTEXT + # - "nixos-19-09": + # <<: *DOCKERHUB_CONTEXT - # Test against PyPy 2.7 - - "pypy27-buster": - <<: *DOCKERHUB_CONTEXT + # # Test against PyPy 2.7 + # - "pypy27-buster": + # <<: *DOCKERHUB_CONTEXT # Just one Python 3.6 configuration while the port is in-progress. - "python36": <<: *DOCKERHUB_CONTEXT # Other assorted tasks and configurations - - "lint": - <<: *DOCKERHUB_CONTEXT - - "pyinstaller": - <<: *DOCKERHUB_CONTEXT - - "deprecations": - <<: *DOCKERHUB_CONTEXT - - "c-locale": - <<: *DOCKERHUB_CONTEXT - # Any locale other than C or UTF-8. - - "another-locale": - <<: *DOCKERHUB_CONTEXT + # - "lint": + # <<: *DOCKERHUB_CONTEXT + # - "pyinstaller": + # <<: *DOCKERHUB_CONTEXT + # - "deprecations": + # <<: *DOCKERHUB_CONTEXT + # - "c-locale": + # <<: *DOCKERHUB_CONTEXT + # # Any locale other than C or UTF-8. + # - "another-locale": + # <<: *DOCKERHUB_CONTEXT - - "integration": - <<: *DOCKERHUB_CONTEXT - requires: - # If the unit test suite doesn't pass, don't bother running the - # integration tests. - - "debian-9" + # - "integration": + # <<: *DOCKERHUB_CONTEXT + # requires: + # # If the unit test suite doesn't pass, don't bother running the + # # integration tests. + # - "debian-9" - # Generate the underlying data for a visualization to aid with Python 3 - # porting. - - "build-porting-depgraph": - <<: *DOCKERHUB_CONTEXT + # # Generate the underlying data for a visualization to aid with Python 3 + # # porting. + # - "build-porting-depgraph": + # <<: *DOCKERHUB_CONTEXT images: # Build the Docker images used by the ci jobs. This makes the ci jobs From c529d271eedf3438739c2d1fdd98fc12949f0e79 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 16:46:54 -0500 Subject: [PATCH 23/91] "unicode" is spelled "str" now --- src/allmydata/node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/allmydata/node.py b/src/allmydata/node.py index 680e8e1a7..83c6d23a9 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -463,8 +463,8 @@ class _Config(object): for petname, config in introducers_yaml.get("introducers", {}).items() } - assert all(isinstance(unicode, k) for k in introducers.keys()) - assert all(isinstance(unicode, v) for v in introducers.values()) + assert all(isinstance(str, k) for k in introducers.keys()) + assert all(isinstance(str, v) for v in introducers.values()) log.msg( "found {} introducers in private/introducers.yaml".format( len(introducers), From fd495124efcf63b7f9629a026a5e05d93cebcca2 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 16:47:52 -0500 Subject: [PATCH 24/91] bleh can't just comment it all out anymore --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 424fc1dd4..0149d3768 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,8 +26,8 @@ workflows: # Contexts are managed in the CircleCI web interface: # # https://app.circleci.com/settings/organization/github/tahoe-lafs/contexts - # - "debian-9": &DOCKERHUB_CONTEXT - # context: "dockerhub-auth" + - "debian-9": &DOCKERHUB_CONTEXT + context: "dockerhub-auth" # - "debian-8": # <<: *DOCKERHUB_CONTEXT From dbb8050a8c2e2439b04ad27e5f6de0ff7501d27b Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 16:51:36 -0500 Subject: [PATCH 25/91] really suffering from not having a local dev env here --- src/allmydata/node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/allmydata/node.py b/src/allmydata/node.py index 83c6d23a9..56b0c39f0 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -463,8 +463,8 @@ class _Config(object): for petname, config in introducers_yaml.get("introducers", {}).items() } - assert all(isinstance(str, k) for k in introducers.keys()) - assert all(isinstance(str, v) for v in introducers.values()) + assert all(isinstance(k, str) for k in introducers.keys()) + assert all(isinstance(v, str) for v in introducers.values()) log.msg( "found {} introducers in private/introducers.yaml".format( len(introducers), From 3ac2e9365fc240be0ec7dbbfddd1902f7acd95b8 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 16:57:45 -0500 Subject: [PATCH 26/91] yea okay that one fails --- src/allmydata/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/node.py b/src/allmydata/node.py index 56b0c39f0..f467f95b9 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -464,7 +464,7 @@ class _Config(object): in introducers_yaml.get("introducers", {}).items() } assert all(isinstance(k, str) for k in introducers.keys()) - assert all(isinstance(v, str) for v in introducers.values()) + assert all(isinstance(v, str) for v in introducers.values()), introducers.values() log.msg( "found {} introducers in private/introducers.yaml".format( len(introducers), From d27c25a26f290dc71be03eba22432dd839f8ae2c Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 18:17:34 -0500 Subject: [PATCH 27/91] make sure we put text into yaml --- src/allmydata/test/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/common.py b/src/allmydata/test/common.py index c4f937aad..8eab155b0 100644 --- a/src/allmydata/test/common.py +++ b/src/allmydata/test/common.py @@ -113,7 +113,7 @@ def write_introducer(basedir, petname, furl): safe_dump({ "introducers": { petname: { - "furl": furl, + "furl": furl.decode("ascii"), }, }, }).encode("ascii"), From 7cf5b04b770713b0f4eb103d368385661b1feb34 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 14 Nov 2020 18:22:41 -0500 Subject: [PATCH 28/91] Put all the CI back --- .circleci/config.yml | 96 ++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0149d3768..afa3fafa1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -29,67 +29,67 @@ workflows: - "debian-9": &DOCKERHUB_CONTEXT context: "dockerhub-auth" - # - "debian-8": - # <<: *DOCKERHUB_CONTEXT - # requires: - # - "debian-9" + - "debian-8": + <<: *DOCKERHUB_CONTEXT + requires: + - "debian-9" - # - "ubuntu-20-04": - # <<: *DOCKERHUB_CONTEXT - # - "ubuntu-18-04": - # <<: *DOCKERHUB_CONTEXT - # requires: - # - "ubuntu-20-04" - # - "ubuntu-16-04": - # <<: *DOCKERHUB_CONTEXT - # requires: - # - "ubuntu-20-04" + - "ubuntu-20-04": + <<: *DOCKERHUB_CONTEXT + - "ubuntu-18-04": + <<: *DOCKERHUB_CONTEXT + requires: + - "ubuntu-20-04" + - "ubuntu-16-04": + <<: *DOCKERHUB_CONTEXT + requires: + - "ubuntu-20-04" - # - "fedora-29": - # <<: *DOCKERHUB_CONTEXT - # - "fedora-28": - # <<: *DOCKERHUB_CONTEXT - # requires: - # - "fedora-29" + - "fedora-29": + <<: *DOCKERHUB_CONTEXT + - "fedora-28": + <<: *DOCKERHUB_CONTEXT + requires: + - "fedora-29" - # - "centos-8": - # <<: *DOCKERHUB_CONTEXT + - "centos-8": + <<: *DOCKERHUB_CONTEXT - # - "nixos-19-09": - # <<: *DOCKERHUB_CONTEXT + - "nixos-19-09": + <<: *DOCKERHUB_CONTEXT - # # Test against PyPy 2.7 - # - "pypy27-buster": - # <<: *DOCKERHUB_CONTEXT + # Test against PyPy 2.7 + - "pypy27-buster": + <<: *DOCKERHUB_CONTEXT # Just one Python 3.6 configuration while the port is in-progress. - "python36": <<: *DOCKERHUB_CONTEXT # Other assorted tasks and configurations - # - "lint": - # <<: *DOCKERHUB_CONTEXT - # - "pyinstaller": - # <<: *DOCKERHUB_CONTEXT - # - "deprecations": - # <<: *DOCKERHUB_CONTEXT - # - "c-locale": - # <<: *DOCKERHUB_CONTEXT - # # Any locale other than C or UTF-8. - # - "another-locale": - # <<: *DOCKERHUB_CONTEXT + - "lint": + <<: *DOCKERHUB_CONTEXT + - "pyinstaller": + <<: *DOCKERHUB_CONTEXT + - "deprecations": + <<: *DOCKERHUB_CONTEXT + - "c-locale": + <<: *DOCKERHUB_CONTEXT + # Any locale other than C or UTF-8. + - "another-locale": + <<: *DOCKERHUB_CONTEXT - # - "integration": - # <<: *DOCKERHUB_CONTEXT - # requires: - # # If the unit test suite doesn't pass, don't bother running the - # # integration tests. - # - "debian-9" + - "integration": + <<: *DOCKERHUB_CONTEXT + requires: + # If the unit test suite doesn't pass, don't bother running the + # integration tests. + - "debian-9" - # # Generate the underlying data for a visualization to aid with Python 3 - # # porting. - # - "build-porting-depgraph": - # <<: *DOCKERHUB_CONTEXT + # Generate the underlying data for a visualization to aid with Python 3 + # porting. + - "build-porting-depgraph": + <<: *DOCKERHUB_CONTEXT images: # Build the Docker images used by the ci jobs. This makes the ci jobs From 10600ef5ec1c138d7d852413f9f79e01350b540a Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 16 Nov 2020 14:59:42 -0500 Subject: [PATCH 29/91] Move write_introducer somewhere it can be used more widely --- src/allmydata/scripts/common.py | 26 +++++++++++++++++++++++--- src/allmydata/test/common.py | 23 +++-------------------- src/allmydata/test/test_client.py | 4 +++- src/allmydata/test/test_introducer.py | 4 +++- src/allmydata/test/test_system.py | 2 +- 5 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/allmydata/scripts/common.py b/src/allmydata/scripts/common.py index 34266ee72..572048f71 100644 --- a/src/allmydata/scripts/common.py +++ b/src/allmydata/scripts/common.py @@ -4,15 +4,19 @@ import os, sys, urllib, textwrap import codecs from os.path import join +from yaml import ( + safe_dump, +) + # Python 2 compatibility from future.utils import PY2 if PY2: from future.builtins import str # noqa: F401 -# On Python 2 this will be the backported package: -from configparser import NoSectionError - from twisted.python import usage +from twisted.python.filepath import FilePath + + from allmydata.util.assertutil import precondition from allmydata.util.encodingutil import unicode_to_url, quote_output, \ @@ -115,6 +119,22 @@ class NoDefaultBasedirOptions(BasedirOptions): DEFAULT_ALIAS = u"tahoe" +def write_introducer(basedir, petname, furl): + """ + Overwrite the node's ``introducers.yaml`` with a file containing the given + introducer information. + """ + FilePath(basedir).child(b"private").child(b"introducers.yaml").setContent( + safe_dump({ + "introducers": { + petname: { + "furl": furl.decode("ascii"), + }, + }, + }).encode("ascii"), + ) + + def get_introducer_furl(nodedir, config): """ :return: the introducer FURL for the given node (no matter if it's diff --git a/src/allmydata/test/common.py b/src/allmydata/test/common.py index 8eab155b0..cbe4d768c 100644 --- a/src/allmydata/test/common.py +++ b/src/allmydata/test/common.py @@ -32,10 +32,6 @@ import attr import treq -from yaml import ( - safe_dump, -) - from zope.interface import implementer from testtools import ( @@ -85,6 +81,9 @@ from allmydata.client import ( config_from_string, create_client_from_config, ) +from allmydata.scripts.common import ( + write_introducer, + ) from ..crypto import ( ed25519, @@ -104,22 +103,6 @@ EMPTY_CLIENT_CONFIG = config_from_string( ) -def write_introducer(basedir, petname, furl): - """ - Overwrite the node's ``introducers.yaml`` with a file containing the given - introducer information. - """ - FilePath(basedir).child(b"private").child(b"introducers.yaml").setContent( - safe_dump({ - "introducers": { - petname: { - "furl": furl.decode("ascii"), - }, - }, - }).encode("ascii"), - ) - - @attr.s class MemoryIntroducerClient(object): """ diff --git a/src/allmydata/test/test_client.py b/src/allmydata/test/test_client.py index b76047be0..7fb82c262 100644 --- a/src/allmydata/test/test_client.py +++ b/src/allmydata/test/test_client.py @@ -58,6 +58,9 @@ from allmydata.util import ( from allmydata.util.fileutil import abspath_expanduser_unicode from allmydata.interfaces import IFilesystemNode, IFileNode, \ IImmutableFileNode, IMutableFileNode, IDirectoryNode +from allmydata.scripts.common import ( + write_introducer, +) from foolscap.api import flushEventualQueue import allmydata.test.common_util as testutil from .common import ( @@ -66,7 +69,6 @@ from .common import ( UseTestPlugins, MemoryIntroducerClient, get_published_announcements, - write_introducer, ) from .matchers import ( MatchesSameElements, diff --git a/src/allmydata/test/test_introducer.py b/src/allmydata/test/test_introducer.py index 4ccabd4e4..f6497eeb4 100644 --- a/src/allmydata/test/test_introducer.py +++ b/src/allmydata/test/test_introducer.py @@ -39,9 +39,11 @@ from allmydata.util import pollmixin, idlib, fileutil, yamlutil from allmydata.util.iputil import ( listenOnUnused, ) +from allmydata.scripts.common import ( + write_introducer, +) import allmydata.test.common_util as testutil from .common import ( - write_introducer, SyncTestCase, AsyncTestCase, AsyncBrokenTestCase, diff --git a/src/allmydata/test/test_system.py b/src/allmydata/test/test_system.py index 14bcb9c47..5a34dcc4d 100644 --- a/src/allmydata/test/test_system.py +++ b/src/allmydata/test/test_system.py @@ -47,7 +47,7 @@ from .web.common import ( from allmydata.test.test_runner import RunBinTahoeMixin from . import common_util as testutil from .common_util import run_cli -from .common import ( +from ..scripts.common import ( write_introducer, ) From 4e84f5e690b997becba221e5d8dbe8dac2bf7075 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 16 Nov 2020 15:00:20 -0500 Subject: [PATCH 30/91] write introducers.yaml instead of [client]introducer.furl in client creation --- src/allmydata/scripts/create_node.py | 29 +++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/allmydata/scripts/create_node.py b/src/allmydata/scripts/create_node.py index 2634e0915..89d81722e 100644 --- a/src/allmydata/scripts/create_node.py +++ b/src/allmydata/scripts/create_node.py @@ -3,13 +3,22 @@ from __future__ import print_function import os import json +from twisted.python.filepath import ( + FilePath, +) from twisted.internet import reactor, defer from twisted.python.usage import UsageError -from allmydata.scripts.common import BasedirOptions, NoDefaultBasedirOptions + +from allmydata.scripts.common import ( + BasedirOptions, + NoDefaultBasedirOptions, + write_introducer, +) from allmydata.scripts.default_nodedir import _default_nodedir from allmydata.util.assertutil import precondition from allmydata.util.encodingutil import listdir_unicode, argv_to_unicode, quote_local_unicode_path, get_io_encoding from allmydata.util import fileutil, i2p_provider, iputil, tor_provider + from wormhole import wormhole @@ -299,12 +308,15 @@ def write_node_config(c, config): def write_client_config(c, config): - # note, config can be a plain dict, it seems -- see - # test_configutil.py in test_create_client_config + introducer = config.get("introducer", None) + if introducer is not None: + write_introducer( + config["basedir"], + "default", + introducer, + ) + c.write("[client]\n") - c.write("# Which services should this client connect to?\n") - introducer = config.get("introducer", None) or "" - c.write("introducer.furl = %s\n" % introducer) c.write("helper.furl =\n") c.write("#stats_gatherer.furl =\n") c.write("\n") @@ -437,8 +449,11 @@ def create_node(config): print("Node created in %s" % quote_local_unicode_path(basedir), file=out) tahoe_cfg = quote_local_unicode_path(os.path.join(basedir, "tahoe.cfg")) + introducers_yaml = quote_local_unicode_path( + os.path.join(basedir, "private", "introducers.yaml"), + ) if not config.get("introducer", ""): - print(" Please set [client]introducer.furl= in %s!" % tahoe_cfg, file=out) + print(" Please add introducers to %s!" % (introducers_yaml,), file=out) print(" The node cannot connect to a grid without it.", file=out) if not config.get("nickname", ""): print(" Please set [node]nickname= in %s" % tahoe_cfg, file=out) From 1946ee502360eaf50d3202f912e38b0acd998d0c Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 16 Nov 2020 15:00:49 -0500 Subject: [PATCH 31/91] note this is for deprecated functionality --- src/allmydata/test/test_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/test_client.py b/src/allmydata/test/test_client.py index 7fb82c262..02423a475 100644 --- a/src/allmydata/test/test_client.py +++ b/src/allmydata/test/test_client.py @@ -786,8 +786,8 @@ class IntroducerClients(unittest.TestCase): def test_invalid_introducer_furl(self): """ - An introducer.furl of 'None' is invalid and causes - create_introducer_clients to fail. + An introducer.furl of 'None' in the deprecated [client]introducer.furl + field is invalid and causes `create_introducer_clients` to fail. """ cfg = ( "[client]\n" From 69b8262f6b196bf83e6767cd92f996bb669ec53d Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 16 Nov 2020 15:01:05 -0500 Subject: [PATCH 32/91] use a different .furl item since introducer.furl will go away --- src/allmydata/test/test_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/test_client.py b/src/allmydata/test/test_client.py index 02423a475..e082a33d1 100644 --- a/src/allmydata/test/test_client.py +++ b/src/allmydata/test/test_client.py @@ -120,14 +120,14 @@ class Basic(testutil.ReallyEqualMixin, unittest.TestCase): def write_config(s): config = ("[client]\n" - "introducer.furl = %s\n" % s) + "helper.furl = %s\n" % s) fileutil.write(os.path.join(basedir, "tahoe.cfg"), config) for s in should_fail: write_config(s) with self.assertRaises(UnescapedHashError) as ctx: yield client.create_client(basedir) - self.assertIn("[client]introducer.furl", str(ctx.exception)) + self.assertIn("[client]helper.furl", str(ctx.exception)) def test_unreadable_config(self): if sys.platform == "win32": From 0fd354396f207d071788a370379273c7fd61292e Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 16 Nov 2020 15:01:21 -0500 Subject: [PATCH 33/91] note this is for deprecated functionality --- src/allmydata/test/test_multi_introducers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/allmydata/test/test_multi_introducers.py b/src/allmydata/test/test_multi_introducers.py index 36801c740..520a5a69a 100644 --- a/src/allmydata/test/test_multi_introducers.py +++ b/src/allmydata/test/test_multi_introducers.py @@ -88,7 +88,8 @@ class MultiIntroTests(unittest.TestCase): @defer.inlineCallbacks def test_reject_default_in_yaml(self): """ - If an introducer is configured in tahoe.cfg then a "default" introducer in + If an introducer is configured in tahoe.cfg with the deprecated + [client]introducer.furl then a "default" introducer in introducers.yaml is rejected. """ connections = { From 302b5cb01fc0c3bf48e90aa53d1dd5c423754b10 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 16 Nov 2020 15:01:34 -0500 Subject: [PATCH 34/91] look for the introducer furl via a more structured interface --- src/allmydata/test/cli/test_invite.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/cli/test_invite.py b/src/allmydata/test/cli/test_invite.py index 0daeb5840..f356e18de 100644 --- a/src/allmydata/test/cli/test_invite.py +++ b/src/allmydata/test/cli/test_invite.py @@ -8,7 +8,9 @@ from twisted.internet import defer from ..common_util import run_cli from ..no_network import GridTestMixin from .common import CLITestMixin - +from ...client import ( + read_config, +) class _FakeWormhole(object): @@ -81,9 +83,19 @@ class Join(GridTestMixin, CLITestMixin, unittest.TestCase): ) self.assertEqual(0, rc) + + config = read_config(node_dir, u"") + self.assertIn( + "pb://foo", + set( + furl + for (furl, cache) + in config.get_introducer_configuration().values() + ), + ) + with open(join(node_dir, 'tahoe.cfg'), 'r') as f: config = f.read() - self.assertIn("pb://foo", config) self.assertIn(u"somethinghopefullyunique", config) @defer.inlineCallbacks From c9f7ce8db5ca883f07ce3fa77c1a4b2922d24850 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 16 Nov 2020 15:01:52 -0500 Subject: [PATCH 35/91] write introducers.yaml instead of [client]introducer.furl --- src/allmydata/test/check_memory.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/allmydata/test/check_memory.py b/src/allmydata/test/check_memory.py index 41cf6e1d7..3ab88b9c6 100644 --- a/src/allmydata/test/check_memory.py +++ b/src/allmydata/test/check_memory.py @@ -21,6 +21,10 @@ from allmydata.util import fileutil, pollmixin from allmydata.util.fileutil import abspath_expanduser_unicode from allmydata.util.encodingutil import get_filesystem_encoding +from allmydata.scripts.common import ( + write_introducer, +) + class StallableHTTPGetterDiscarder(tw_client.HTTPPageGetter, object): full_speed_ahead = False _bytes_so_far = 0 @@ -183,13 +187,13 @@ class SystemFramework(pollmixin.PollMixin): self.nodes = [] for i in range(self.numnodes): nodedir = os.path.join(self.testdir, "node%d" % i) - os.mkdir(nodedir) + os.makedirs(nodedir + b"/private") + write_introducer(nodedir, "default", self.introducer_url) f = open(os.path.join(nodedir, "tahoe.cfg"), "w") f.write("[client]\n" - "introducer.furl = %s\n" "shares.happy = 1\n" "[storage]\n" - % (self.introducer_furl,)) + ) # the only tests for which we want the internal nodes to actually # retain shares are the ones where somebody's going to download # them. @@ -235,16 +239,16 @@ this file are ignored. quiet = StringIO() create_node.create_node({'basedir': clientdir}, out=quiet) log.msg("DONE MAKING CLIENT") + write_introducer(clientdir, "default", self.introducer_furl) # now replace tahoe.cfg # set webport=0 and then ask the node what port it picked. f = open(os.path.join(clientdir, "tahoe.cfg"), "w") f.write("[node]\n" "web.port = tcp:0:interface=127.0.0.1\n" "[client]\n" - "introducer.furl = %s\n" "shares.happy = 1\n" "[storage]\n" - % (self.introducer_furl,)) + ) if self.mode in ("upload-self", "receive"): # accept and store shares, to trigger the memory consumption bugs From 0258bb72954ebbb7c19bb79a9fc927d8382fb1b7 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 16 Nov 2020 15:02:13 -0500 Subject: [PATCH 36/91] note it's deprecated --- src/allmydata/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/node.py b/src/allmydata/node.py index f467f95b9..04ba2f2fe 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -475,7 +475,7 @@ class _Config(object): raise introducers = {} - # read furl from tahoe.cfg + # supported the deprecated [client]introducer.furl item in tahoe.cfg tahoe_cfg_introducer_furl = self.get_config("client", "introducer.furl", None) if tahoe_cfg_introducer_furl == "None": raise ValueError( From 60e0056ad82f0bc43ea4fd0189711a7c184f4245 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 16 Nov 2020 15:02:18 -0500 Subject: [PATCH 37/91] don't guide folks to the deprecated config item --- src/allmydata/node.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/allmydata/node.py b/src/allmydata/node.py index 04ba2f2fe..7eefbbce0 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -480,8 +480,7 @@ class _Config(object): if tahoe_cfg_introducer_furl == "None": raise ValueError( "tahoe.cfg has invalid 'introducer.furl = None':" - " to disable it, use 'introducer.furl ='" - " or omit the key entirely" + " to disable it omit the key entirely" ) if tahoe_cfg_introducer_furl: warn( From 92206b907e35801fee77a41eaf4ce776d752c602 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 16 Nov 2020 15:02:29 -0500 Subject: [PATCH 38/91] write introducers.yaml instead of [client]introducer.furl --- integration/test_tor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/integration/test_tor.py b/integration/test_tor.py index 3d169a88f..8dd05fc3f 100644 --- a/integration/test_tor.py +++ b/integration/test_tor.py @@ -9,6 +9,10 @@ import pytest_twisted import util +from allmydata.test.common import ( + write_introducer, +) + # see "conftest.py" for the fixtures (e.g. "tor_network") # XXX: Integration tests that involve Tor do not run reliably on @@ -89,6 +93,9 @@ def _create_anonymous_node(reactor, name, control_port, request, temp_dir, flog_ ) yield proto.done + + # Which services should this client connect to? + write_introducer(node_dir, "default", introducer_furl) with open(join(node_dir, 'tahoe.cfg'), 'w') as f: f.write(''' [node] @@ -105,15 +112,12 @@ onion = true onion.private_key_file = private/tor_onion.privkey [client] -# Which services should this client connect to? -introducer.furl = %(furl)s shares.needed = 1 shares.happy = 1 shares.total = 2 ''' % { 'name': name, - 'furl': introducer_furl, 'web_port': web_port, 'log_furl': flog_gatherer, 'control_port': control_port, From 5cb1df06c4bff71f1b526b3bb3824f1b80b739c6 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 16 Nov 2020 15:02:51 -0500 Subject: [PATCH 39/91] delegate introducer furl lookup to the config object --- src/allmydata/scripts/common.py | 24 ++++++++++++------------ src/allmydata/scripts/tahoe_invite.py | 3 ++- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/allmydata/scripts/common.py b/src/allmydata/scripts/common.py index 572048f71..6d633b502 100644 --- a/src/allmydata/scripts/common.py +++ b/src/allmydata/scripts/common.py @@ -140,19 +140,19 @@ def get_introducer_furl(nodedir, config): :return: the introducer FURL for the given node (no matter if it's a client-type node or an introducer itself) """ + for petname, (furl, cache) in config.get_introducer_configuration().items(): + return furl + + # We have no configured introducers. Maybe this is running *on* the + # introducer? Let's guess, sure why not. try: - introducer_furl = config.get('client', 'introducer.furl') - except NoSectionError: - # we're not a client; maybe this is running *on* the introducer? - try: - with open(join(nodedir, "private", "introducer.furl"), "r") as f: - introducer_furl = f.read().strip() - except IOError: - raise Exception( - "Can't find introducer FURL in tahoe.cfg nor " - "{}/private/introducer.furl".format(nodedir) - ) - return introducer_furl + with open(join(nodedir, "private", "introducer.furl"), "r") as f: + return f.read().strip() + except IOError: + raise Exception( + "Can't find introducer FURL in tahoe.cfg nor " + "{}/private/introducer.furl".format(nodedir) + ) def get_aliases(nodedir): diff --git a/src/allmydata/scripts/tahoe_invite.py b/src/allmydata/scripts/tahoe_invite.py index cca4216e3..8096fa3c1 100644 --- a/src/allmydata/scripts/tahoe_invite.py +++ b/src/allmydata/scripts/tahoe_invite.py @@ -11,6 +11,7 @@ from wormhole import wormhole from allmydata.util import configutil from allmydata.util.encodingutil import argv_to_abspath from allmydata.scripts.common import get_default_nodedir, get_introducer_furl +from allmydata.node import read_config class InviteOptions(usage.Options): @@ -77,7 +78,7 @@ def invite(options): basedir = argv_to_abspath(options.parent['node-directory']) else: basedir = get_default_nodedir() - config = configutil.get_config(join(basedir, 'tahoe.cfg')) + config = read_config(basedir, u"") out = options.stdout err = options.stderr From 2ee0b1d3c634d05af00b57c542760e3af50ced73 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 16 Nov 2020 15:05:04 -0500 Subject: [PATCH 40/91] flake cleanup --- src/allmydata/scripts/create_node.py | 3 --- src/allmydata/scripts/tahoe_invite.py | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/allmydata/scripts/create_node.py b/src/allmydata/scripts/create_node.py index 89d81722e..30024dfd9 100644 --- a/src/allmydata/scripts/create_node.py +++ b/src/allmydata/scripts/create_node.py @@ -3,9 +3,6 @@ from __future__ import print_function import os import json -from twisted.python.filepath import ( - FilePath, -) from twisted.internet import reactor, defer from twisted.python.usage import UsageError diff --git a/src/allmydata/scripts/tahoe_invite.py b/src/allmydata/scripts/tahoe_invite.py index 8096fa3c1..dbc84d0ea 100644 --- a/src/allmydata/scripts/tahoe_invite.py +++ b/src/allmydata/scripts/tahoe_invite.py @@ -1,14 +1,12 @@ from __future__ import print_function import json -from os.path import join from twisted.python import usage from twisted.internet import defer, reactor from wormhole import wormhole -from allmydata.util import configutil from allmydata.util.encodingutil import argv_to_abspath from allmydata.scripts.common import get_default_nodedir, get_introducer_furl from allmydata.node import read_config From e60c643b5f46d9365dc67e6022c904f10c261b45 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 18 Nov 2020 10:57:38 -0500 Subject: [PATCH 41/91] Make configutil.write_config atomic and also make it take a FilePath --- src/allmydata/test/test_configutil.py | 7 +++++-- src/allmydata/util/configutil.py | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/allmydata/test/test_configutil.py b/src/allmydata/test/test_configutil.py index 9e792dc87..19e5b303d 100644 --- a/src/allmydata/test/test_configutil.py +++ b/src/allmydata/test/test_configutil.py @@ -15,6 +15,9 @@ if PY2: import os.path +from twisted.python.filepath import ( + FilePath, +) from twisted.trial import unittest from allmydata.util import configutil @@ -55,7 +58,7 @@ enabled = false # test that set_config can mutate an existing option configutil.set_config(config, "node", "nickname", "Alice!") - configutil.write_config(tahoe_cfg, config) + configutil.write_config(FilePath(tahoe_cfg), config) config = configutil.get_config(tahoe_cfg) self.failUnlessEqual(config.get("node", "nickname"), "Alice!") @@ -63,7 +66,7 @@ enabled = false # test that set_config can set a new option descriptor = "Twas brillig, and the slithy toves Did gyre and gimble in the wabe" configutil.set_config(config, "node", "descriptor", descriptor) - configutil.write_config(tahoe_cfg, config) + configutil.write_config(FilePath(tahoe_cfg), config) config = configutil.get_config(tahoe_cfg) self.failUnlessEqual(config.get("node", "descriptor"), descriptor) diff --git a/src/allmydata/util/configutil.py b/src/allmydata/util/configutil.py index 4bee7d043..d905a8e00 100644 --- a/src/allmydata/util/configutil.py +++ b/src/allmydata/util/configutil.py @@ -59,8 +59,21 @@ def set_config(config, section, option, value): assert config.get(section, option) == value def write_config(tahoe_cfg, config): - with open(tahoe_cfg, "w") as f: - config.write(f) + """ + Write a configuration to a file. + + :param FilePath tahoe_cfg: The path to which to write the config. + + :param ConfigParser config: The configuration to write. + + :return: ``None`` + """ + tmp = tahoe_cfg.temporarySibling() + # FilePath.open can only open files in binary mode which does not work + # with ConfigParser.write. + with open(tmp.path, "wt") as fp: + config.write(fp) + tmp.moveTo(tahoe_cfg) def validate_config(fname, cfg, valid_config): """ From aedac9d570641ad6a30d1a207f9968e8765eca79 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 18 Nov 2020 11:02:58 -0500 Subject: [PATCH 42/91] news fragment --- newsfragments/3511.minor | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 newsfragments/3511.minor diff --git a/newsfragments/3511.minor b/newsfragments/3511.minor new file mode 100644 index 000000000..e69de29bb From 34714d5f6b691d03269100db1914a85ca6756f2e Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 18 Nov 2020 12:42:31 -0500 Subject: [PATCH 43/91] Add everything and nothing config validation helpers --- src/allmydata/test/test_configutil.py | 101 ++++++++++++++++++++++++++ src/allmydata/util/configutil.py | 26 ++++++- 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/src/allmydata/test/test_configutil.py b/src/allmydata/test/test_configutil.py index 19e5b303d..e6035c512 100644 --- a/src/allmydata/test/test_configutil.py +++ b/src/allmydata/test/test_configutil.py @@ -14,6 +14,17 @@ if PY2: from builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, list, object, range, str, max, min # noqa: F401 import os.path +from configparser import ( + ConfigParser, +) + +from hypothesis import ( + given, +) +from hypothesis.strategies import ( + dictionaries, + text, +) from twisted.python.filepath import ( FilePath, @@ -23,6 +34,51 @@ from twisted.trial import unittest from allmydata.util import configutil +def arbitrary_config_dicts( + min_sections=0, + max_sections=3, + max_items_per_section=3, + max_item_length=8, + max_value_length=8, +): + """ + Build ``dict[str, dict[str, str]]`` instances populated with arbitrary + configurations. + """ + return dictionaries( + text(), + dictionaries( + text(max_size=max_item_length), + text(max_size=max_value_length), + max_size=max_items_per_section, + ), + min_size=min_sections, + max_size=max_sections, + ) + + +def to_configparser(dictconfig): + """ + Take a ``dict[str, dict[str, str]]`` and turn it into the corresponding + populated ``ConfigParser`` instance. + """ + cp = ConfigParser() + for section, items in dictconfig.items(): + cp.add_section(section) + for k, v in items.items(): + cp.set( + section, + k, + # ConfigParser has a feature that everyone knows and loves + # where it will use %-style interpolation to substitute + # values from one part of the config into another part of + # the config. Escape all our `%`s to avoid hitting this + # and complicating things. + v.replace("%", "%%"), + ) + return cp + + class ConfigUtilTests(unittest.TestCase): def setUp(self): super(ConfigUtilTests, self).setUp() @@ -166,3 +222,48 @@ enabled = false config = configutil.get_config(fname) self.assertEqual(config.get("node", "a"), "foo") self.assertEqual(config.get("node", "b"), "bar") + + @given(arbitrary_config_dicts()) + def test_everything_valid(self, cfgdict): + """ + ``validate_config`` returns ``None`` when the validator is + ``ValidConfiguration.everything()``. + """ + cfg = to_configparser(cfgdict) + self.assertIs( + configutil.validate_config( + "", + cfg, + configutil.ValidConfiguration.everything(), + ), + None, + ) + + @given(arbitrary_config_dicts(min_sections=1)) + def test_nothing_valid(self, cfgdict): + """ + ``validate_config`` raises ``UnknownConfigError`` when the validator is + ``ValidConfiguration.nothing()`` for all non-empty configurations. + """ + cfg = to_configparser(cfgdict) + with self.assertRaises(configutil.UnknownConfigError): + configutil.validate_config( + "", + cfg, + configutil.ValidConfiguration.nothing(), + ) + + def test_nothing_empty_valid(self): + """ + ``validate_config`` returns ``None`` when the validator is + ``ValidConfiguration.nothing()`` if the configuration is empty. + """ + cfg = ConfigParser() + self.assertIs( + configutil.validate_config( + "", + cfg, + configutil.ValidConfiguration.nothing(), + ), + None, + ) diff --git a/src/allmydata/util/configutil.py b/src/allmydata/util/configutil.py index d905a8e00..c85f58af3 100644 --- a/src/allmydata/util/configutil.py +++ b/src/allmydata/util/configutil.py @@ -115,10 +115,34 @@ class ValidConfiguration(object): an item name as bytes and returns True if that section, item pair is valid, False otherwise. """ - _static_valid_sections = attr.ib() + _static_valid_sections = attr.ib( + validator=attr.validators.instance_of(dict) + ) _is_valid_section = attr.ib(default=lambda section_name: False) _is_valid_item = attr.ib(default=lambda section_name, item_name: False) + @classmethod + def everything(cls): + """ + Create a validator which considers everything valid. + """ + return cls( + {}, + lambda section_name: True, + lambda section_name, item_name: True, + ) + + @classmethod + def nothing(cls): + """ + Create a validator which considers nothing valid. + """ + return cls( + {}, + lambda section_name: False, + lambda section_name, item_name: False, + ) + def is_valid_section(self, section_name): """ :return: True if the given section name is valid, False otherwise. From 021615bdfffbe56b803e0602f72a62cebb358b36 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 18 Nov 2020 12:44:52 -0500 Subject: [PATCH 44/91] Some further test_configutil improvements --- src/allmydata/test/test_configutil.py | 47 ++++++++++++--------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/src/allmydata/test/test_configutil.py b/src/allmydata/test/test_configutil.py index e6035c512..04631cb55 100644 --- a/src/allmydata/test/test_configutil.py +++ b/src/allmydata/test/test_configutil.py @@ -128,13 +128,15 @@ enabled = false self.failUnlessEqual(config.get("node", "descriptor"), descriptor) def test_config_validation_success(self): - fname = self.create_tahoe_cfg('[node]\nvalid = foo\n') - - config = configutil.get_config(fname) + """ + ``configutil.validate_config`` returns ``None`` when the configuration it + is given has nothing more than the static sections and items defined + by the validator. + """ # should succeed, no exceptions configutil.validate_config( - fname, - config, + "", + to_configparser({"node": {"valid": "foo"}}), self.static_valid_config, ) @@ -144,24 +146,20 @@ enabled = false validation but are matched by the dynamic validation is considered valid. """ - fname = self.create_tahoe_cfg('[node]\nvalid = foo\n') - - config = configutil.get_config(fname) # should succeed, no exceptions configutil.validate_config( - fname, - config, + "", + to_configparser({"node": {"valid": "foo"}}), self.dynamic_valid_config, ) def test_config_validation_invalid_item(self): - fname = self.create_tahoe_cfg('[node]\nvalid = foo\ninvalid = foo\n') - - config = configutil.get_config(fname) + config = to_configparser({"node": {"valid": "foo", "invalid": "foo"}}) e = self.assertRaises( configutil.UnknownConfigError, configutil.validate_config, - fname, config, + "", + config, self.static_valid_config, ) self.assertIn("section [node] contains unknown option 'invalid'", str(e)) @@ -171,13 +169,12 @@ enabled = false A configuration with a section that is matched by neither the static nor dynamic validators is rejected. """ - fname = self.create_tahoe_cfg('[node]\nvalid = foo\n[invalid]\n') - - config = configutil.get_config(fname) + config = to_configparser({"node": {"valid": "foo"}, "invalid": {}}) e = self.assertRaises( configutil.UnknownConfigError, configutil.validate_config, - fname, config, + "", + config, self.static_valid_config, ) self.assertIn("contains unknown section [invalid]", str(e)) @@ -187,13 +184,12 @@ enabled = false A configuration with a section that is matched by neither the static nor dynamic validators is rejected. """ - fname = self.create_tahoe_cfg('[node]\nvalid = foo\n[invalid]\n') - - config = configutil.get_config(fname) + config = to_configparser({"node": {"valid": "foo"}, "invalid": {}}) e = self.assertRaises( configutil.UnknownConfigError, configutil.validate_config, - fname, config, + "", + config, self.dynamic_valid_config, ) self.assertIn("contains unknown section [invalid]", str(e)) @@ -203,13 +199,12 @@ enabled = false A configuration with a section, item pair that is matched by neither the static nor dynamic validators is rejected. """ - fname = self.create_tahoe_cfg('[node]\nvalid = foo\ninvalid = foo\n') - - config = configutil.get_config(fname) + config = to_configparser({"node": {"valid": "foo", "invalid": "foo"}}) e = self.assertRaises( configutil.UnknownConfigError, configutil.validate_config, - fname, config, + "", + config, self.dynamic_valid_config, ) self.assertIn("section [node] contains unknown option 'invalid'", str(e)) From 84647e25b78e7d78e101e7157414a181413aeb98 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 18 Nov 2020 12:59:23 -0500 Subject: [PATCH 45/91] Refine the ConfigParser generator Limit the characters used in the section and item name strategies. ConfigParser doesn't allow all characters in all places. --- src/allmydata/test/test_configutil.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/test_configutil.py b/src/allmydata/test/test_configutil.py index 04631cb55..ea7759fec 100644 --- a/src/allmydata/test/test_configutil.py +++ b/src/allmydata/test/test_configutil.py @@ -17,6 +17,9 @@ import os.path from configparser import ( ConfigParser, ) +from functools import ( + partial, +) from hypothesis import ( given, @@ -24,6 +27,7 @@ from hypothesis import ( from hypothesis.strategies import ( dictionaries, text, + characters, ) from twisted.python.filepath import ( @@ -37,6 +41,7 @@ from allmydata.util import configutil def arbitrary_config_dicts( min_sections=0, max_sections=3, + max_section_name_size=8, max_items_per_section=3, max_item_length=8, max_value_length=8, @@ -45,10 +50,23 @@ def arbitrary_config_dicts( Build ``dict[str, dict[str, str]]`` instances populated with arbitrary configurations. """ + identifier_text = partial( + text, + # Don't allow most control characters or spaces + alphabet=characters( + blacklist_categories=('Cc', 'Cs', 'Zs'), + ), + ) return dictionaries( - text(), + identifier_text( + min_size=1, + max_size=max_section_name_size, + ), dictionaries( - text(max_size=max_item_length), + identifier_text( + min_size=1, + max_size=max_item_length, + ), text(max_size=max_value_length), max_size=max_items_per_section, ), From 53aa434d77d52931ef08a48a15b8ceb34d9665d4 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 18 Nov 2020 13:01:08 -0500 Subject: [PATCH 46/91] Add a helper to make a deep copy of a ConfigParser This will help avoid unintentional side-effects --- src/allmydata/test/test_configutil.py | 13 +++++++++++++ src/allmydata/util/configutil.py | 17 +++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/allmydata/test/test_configutil.py b/src/allmydata/test/test_configutil.py index ea7759fec..1b8fb5029 100644 --- a/src/allmydata/test/test_configutil.py +++ b/src/allmydata/test/test_configutil.py @@ -280,3 +280,16 @@ enabled = false ), None, ) + + @given(arbitrary_config_dicts()) + def test_copy_config(self, cfgdict): + """ + ``copy_config`` creates a new ``ConfigParser`` object containing the same + values as its input. + """ + cfg = to_configparser(cfgdict) + copied = configutil.copy_config(cfg) + # Should be equal + self.assertEqual(cfg, copied) + # But not because they're the same object. + self.assertIsNot(cfg, copied) diff --git a/src/allmydata/util/configutil.py b/src/allmydata/util/configutil.py index c85f58af3..8063ba449 100644 --- a/src/allmydata/util/configutil.py +++ b/src/allmydata/util/configutil.py @@ -173,6 +173,23 @@ class ValidConfiguration(object): ) +def copy_config(old): + """ + Return a brand new ``ConfigParser`` containing the same values as + the given object. + + :param ConfigParser old: The configuration to copy. + + :return ConfigParser: The new object containing the same configuration. + """ + new = ConfigParser() + for section_name in old.sections(): + new.add_section(section_name) + for k, v in old.items(section_name): + new.set(section_name, k, v.replace("%", "%%")) + return new + + def _either(f, g): """ :return: A function which returns True if either f or g returns True. From f21e3189b5ca91d6f59f9eb1988e79b278100a0d Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 18 Nov 2020 13:17:16 -0500 Subject: [PATCH 47/91] Remove some repetition between read_config and config_from_string --- src/allmydata/node.py | 105 +++++++++++++++++--------- src/allmydata/test/cli/test_create.py | 7 +- 2 files changed, 71 insertions(+), 41 deletions(-) diff --git a/src/allmydata/node.py b/src/allmydata/node.py index 9e7143fd4..f320e6e12 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -22,9 +22,14 @@ import errno import tempfile from base64 import b32decode, b32encode +import attr + # On Python 2 this will be the backported package. import configparser +from twisted.python.filepath import ( + FilePath, +) from twisted.python import log as twlog from twisted.application import service from twisted.python.failure import Failure @@ -192,25 +197,27 @@ def read_config(basedir, portnumfile, generated_files=[], _valid_config=None): # canonicalize the portnum file portnumfile = os.path.join(basedir, portnumfile) - # (try to) read the main config file - config_fname = os.path.join(basedir, "tahoe.cfg") + config_path = FilePath(basedir).child("tahoe.cfg") try: - parser = configutil.get_config(config_fname) + config_str = config_path.getContent() except EnvironmentError as e: if e.errno != errno.ENOENT: raise # The file is missing, just create empty ConfigParser. - parser = configutil.get_config_from_string(u"") + config_str = u"" + else: + config_str = config_str.decode("utf-8-sig") - configutil.validate_config(config_fname, parser, _valid_config) - - # make sure we have a private configuration area - fileutil.make_dirs(os.path.join(basedir, "private"), 0o700) - - return _Config(parser, portnumfile, basedir, config_fname) + return config_from_string( + basedir, + portnumfile, + config_str, + _valid_config, + config_path, + ) -def config_from_string(basedir, portnumfile, config_str, _valid_config=None): +def config_from_string(basedir, portnumfile, config_str, _valid_config=None, fpath=None): """ load and validate configuration from in-memory string """ @@ -223,9 +230,19 @@ def config_from_string(basedir, portnumfile, config_str, _valid_config=None): # load configuration from in-memory string parser = configutil.get_config_from_string(config_str) - fname = "" - configutil.validate_config(fname, parser, _valid_config) - return _Config(parser, portnumfile, basedir, fname) + configutil.validate_config( + "" if fpath is None else fpath.path, + parser, + _valid_config, + ) + + return _Config( + parser, + portnumfile, + basedir, + fpath, + _valid_config, + ) def get_app_versions(): @@ -260,6 +277,7 @@ def _error_about_old_config_files(basedir, generated_files): raise e +@attr.s class _Config(object): """ Manages configuration of a Tahoe 'node directory'. @@ -268,30 +286,47 @@ class _Config(object): class; names and funtionality have been kept the same while moving the code. It probably makes sense for several of these APIs to have better names. + + :ivar ConfigParser config: The actual configuration values. + + :ivar str portnum_fname: filename to use for the port-number file (a + relative path inside basedir). + + :ivar str _basedir: path to our "node directory", inside which all + configuration is managed. + + :ivar (FilePath|NoneType) config_path: The path actually used to create + the configparser (might be ``None`` if using in-memory data). + + :ivar ValidConfiguration valid_config_sections: The validator for the + values in this configuration. """ + config = attr.ib(validator=attr.validators.instance_of(configparser.ConfigParser)) + portnum_fname = attr.ib() + _basedir = attr.ib( + converter=lambda basedir: abspath_expanduser_unicode(ensure_text(basedir)), + ) + config_path = attr.ib( + validator=attr.validators.optional( + attr.validators.instance_of(FilePath), + ), + ) + valid_config_sections = attr.ib( + default=configutil.ValidConfiguration.everything(), + validator=attr.validators.instance_of(configutil.ValidConfiguration), + ) - def __init__(self, configparser, portnum_fname, basedir, config_fname): - """ - :param configparser: a ConfigParser instance + @property + def nickname(self): + nickname = self.get_config("node", "nickname", u"") + assert isinstance(nickname, str) + return nickname - :param portnum_fname: filename to use for the port-number file - (a relative path inside basedir) - - :param basedir: path to our "node directory", inside which all - configuration is managed - - :param config_fname: the pathname actually used to create the - configparser (might be 'fake' if using in-memory data) - """ - self.portnum_fname = portnum_fname - self._basedir = abspath_expanduser_unicode(ensure_text(basedir)) - self._config_fname = config_fname - self.config = configparser - self.nickname = self.get_config("node", "nickname", u"") - assert isinstance(self.nickname, str) - - def validate(self, valid_config_sections): - configutil.validate_config(self._config_fname, self.config, valid_config_sections) + @property + def _config_fname(self): + if self.config_path is None: + return "" + return self.config_path.path def write_config_file(self, name, value, mode="w"): """ diff --git a/src/allmydata/test/cli/test_create.py b/src/allmydata/test/cli/test_create.py index f013c0205..aee07a671 100644 --- a/src/allmydata/test/cli/test_create.py +++ b/src/allmydata/test/cli/test_create.py @@ -52,13 +52,8 @@ class Config(unittest.TestCase): create_node.write_node_config(f, opts) create_node.write_client_config(f, opts) - config = configutil.get_config(fname) # should succeed, no exceptions - configutil.validate_config( - fname, - config, - client._valid_config(), - ) + client.read_config(d, "") @defer.inlineCallbacks def test_client(self): From 862d32a90d27b6387affa6f56d7d323ab7829747 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 18 Nov 2020 13:18:21 -0500 Subject: [PATCH 48/91] Add `_Config.set_config` for presistently changing config values --- src/allmydata/node.py | 18 ++++++++ src/allmydata/test/test_node.py | 81 ++++++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/allmydata/node.py b/src/allmydata/node.py index f320e6e12..99062d771 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -371,6 +371,24 @@ class _Config(object): ) return default + def set_config(self, section, option, value): + """ + Set a config options in a section and re-write the tahoe.cfg file + """ + if option.endswith(".furl") and "#" in value: + raise UnescapedHashError(section, option, value) + + copied_config = configutil.copy_config(self.config) + configutil.set_config(copied_config, section, option, value) + configutil.validate_config( + self._config_fname, + copied_config, + self.valid_config_sections, + ) + if self.config_path is not None: + configutil.write_config(self.config_path, copied_config) + self.config = copied_config + def get_config_from_file(self, name, required=False): """Get the (string) contents of a config file, or None if the file did not exist. If required=True, raise an exception rather than diff --git a/src/allmydata/test/test_node.py b/src/allmydata/test/test_node.py index 693183ea4..a4c64a654 100644 --- a/src/allmydata/test/test_node.py +++ b/src/allmydata/test/test_node.py @@ -29,6 +29,9 @@ from hypothesis.strategies import ( from unittest import skipIf +from twisted.python.filepath import ( + FilePath, +) from twisted.trial import unittest from twisted.internet import defer @@ -52,7 +55,11 @@ from allmydata import client from allmydata.util import fileutil, iputil from allmydata.util.namespace import Namespace -from allmydata.util.configutil import UnknownConfigError +from allmydata.util.configutil import ( + ValidConfiguration, + UnknownConfigError, +) + from allmydata.util.i2p_provider import create as create_i2p_provider from allmydata.util.tor_provider import create as create_tor_provider import allmydata.test.common_util as testutil @@ -431,6 +438,78 @@ class TestCase(testutil.SignalMixin, unittest.TestCase): yield client.create_client(basedir) self.failUnless(ns.called) + def test_set_config_unescaped_furl_hash(self): + """ + ``_Config.set_config`` raises ``UnescapedHashError`` if the item being set + is a furl and the value includes ``"#"`` and does not set the value. + """ + basedir = self.mktemp() + new_config = config_from_string(basedir, "", "") + with self.assertRaises(UnescapedHashError): + new_config.set_config("foo", "bar.furl", "value#1") + with self.assertRaises(MissingConfigEntry): + new_config.get_config("foo", "bar.furl") + + def test_set_config_new_section(self): + """ + ``_Config.set_config`` can be called with the name of a section that does + not already exist to create that section and set an item in it. + """ + basedir = self.mktemp() + new_config = config_from_string(basedir, "", "", ValidConfiguration.everything()) + new_config.set_config("foo", "bar", "value1") + self.assertEqual( + new_config.get_config("foo", "bar"), + "value1" + ) + + def test_set_config_replace(self): + """ + ``_Config.set_config`` can be called with a section and item that already + exists to change an existing value to a new one. + """ + basedir = self.mktemp() + new_config = config_from_string(basedir, "", "", ValidConfiguration.everything()) + new_config.set_config("foo", "bar", "value1") + new_config.set_config("foo", "bar", "value2") + self.assertEqual( + new_config.get_config("foo", "bar"), + "value2" + ) + + def test_set_config_write(self): + """ + ``_Config.set_config`` persists the configuration change so it can be + re-loaded later. + """ + # Let our nonsense config through + valid_config = ValidConfiguration.everything() + basedir = FilePath(self.mktemp()) + basedir.makedirs() + cfg = basedir.child(b"tahoe.cfg") + cfg.setContent(b"") + new_config = read_config(basedir.path, "", [], valid_config) + new_config.set_config("foo", "bar", "value1") + loaded_config = read_config(basedir.path, "", [], valid_config) + self.assertEqual( + loaded_config.get_config("foo", "bar"), + "value1", + ) + + def test_set_config_rejects_invalid_config(self): + """ + ``_Config.set_config`` raises ``UnknownConfigError`` if the section or + item is not recognized by the validation object and does not set the + value. + """ + # Make everything invalid. + valid_config = ValidConfiguration.nothing() + new_config = config_from_string(self.mktemp(), "", "", valid_config) + with self.assertRaises(UnknownConfigError): + new_config.set_config("foo", "bar", "baz") + with self.assertRaises(MissingConfigEntry): + new_config.get_config("foo", "bar") + class TestMissingPorts(unittest.TestCase): """ From 88ce823618c9066d374f2d615904ffe550eb8e44 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 18 Nov 2020 18:26:52 -0500 Subject: [PATCH 49/91] Update integration test caller of altered write_config --- integration/util.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/integration/util.py b/integration/util.py index a64bcbf8e..d212a002f 100644 --- a/integration/util.py +++ b/integration/util.py @@ -6,6 +6,9 @@ from os.path import exists, join from six.moves import StringIO from functools import partial +from twisted.python.filepath import ( + FilePath, +) from twisted.internet.defer import Deferred, succeed from twisted.internet.protocol import ProcessProtocol from twisted.internet.error import ProcessExitedAlready, ProcessDone @@ -258,7 +261,7 @@ def _create_node(reactor, request, temp_dir, introducer_furl, flog_gatherer, nam config_path = join(node_dir, 'tahoe.cfg') config = get_config(config_path) set_config(config, 'node', 'log_gatherer.furl', flog_gatherer) - write_config(config_path, config) + write_config(FilePath(config_path), config) created_d.addCallback(created) d = Deferred() From 594f8019d1b20326bcd25846ab3866adca61b782 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 18 Nov 2020 18:29:36 -0500 Subject: [PATCH 50/91] Better support Windows here --- src/allmydata/util/configutil.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/allmydata/util/configutil.py b/src/allmydata/util/configutil.py index 8063ba449..ea64e1704 100644 --- a/src/allmydata/util/configutil.py +++ b/src/allmydata/util/configutil.py @@ -20,6 +20,10 @@ from configparser import ConfigParser import attr +from twisted.python.runtime import ( + platform, +) + class UnknownConfigError(Exception): """ @@ -73,6 +77,10 @@ def write_config(tahoe_cfg, config): # with ConfigParser.write. with open(tmp.path, "wt") as fp: config.write(fp) + # Windows doesn't have atomic overwrite semantics for moveTo. Thus we end + # up slightly less than atomic. + if platform.isWindows(): + tahoe_cfg.remove() tmp.moveTo(tahoe_cfg) def validate_config(fname, cfg, valid_config): From 40d372a2f6633753114a9fd5a1c3e3eb412f44ca Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 19 Nov 2020 11:11:48 -0500 Subject: [PATCH 51/91] Some progress towards passing tests on Python 3. --- src/allmydata/introducer/client.py | 20 ++++++++------- src/allmydata/introducer/common.py | 13 ++++++---- src/allmydata/test/test_introducer.py | 37 ++++++++++++++------------- 3 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/allmydata/introducer/client.py b/src/allmydata/introducer/client.py index 36adae474..b7238d58c 100644 --- a/src/allmydata/introducer/client.py +++ b/src/allmydata/introducer/client.py @@ -1,4 +1,4 @@ -from past.builtins import unicode +from past.builtins import unicode, long import time from zope.interface import implementer @@ -120,6 +120,8 @@ class IntroducerClient(service.Service, Referenceable): } announcements.append(server_params) announcement_cache_yaml = yamlutil.safe_dump(announcements) + if isinstance(announcement_cache_yaml, unicode): + announcement_cache_yaml = announcement_cache_yaml.encode("utf-8") self._cache_filepath.setContent(announcement_cache_yaml) def _got_introducer(self, publisher): @@ -163,7 +165,7 @@ class IntroducerClient(service.Service, Referenceable): self._subscribed_service_names.add(service_name) self._maybe_subscribe() for index,(ann,key_s,when) in self._inbound_announcements.items(): - precondition(isinstance(key_s, str), key_s) + precondition(isinstance(key_s, bytes), key_s) servicename = index[0] if servicename == service_name: eventually(cb, key_s, ann, *args, **kwargs) @@ -239,7 +241,7 @@ class IntroducerClient(service.Service, Referenceable): # this might raise UnknownKeyError or bad-sig error ann, key_s = unsign_from_foolscap(ann_t) # key is "v0-base32abc123" - precondition(isinstance(key_s, str), key_s) + precondition(isinstance(key_s, bytes), key_s) except BadSignature: self.log("bad signature on inbound announcement: %s" % (ann_t,), parent=lp, level=log.WEIRD, umid="ZAU15Q") @@ -249,7 +251,7 @@ class IntroducerClient(service.Service, Referenceable): self._process_announcement(ann, key_s) def _process_announcement(self, ann, key_s): - precondition(isinstance(key_s, str), key_s) + precondition(isinstance(key_s, bytes), key_s) self._debug_counts["inbound_announcement"] += 1 service_name = str(ann["service-name"]) if service_name not in self._subscribed_service_names: @@ -258,7 +260,7 @@ class IntroducerClient(service.Service, Referenceable): self._debug_counts["wrong_service"] += 1 return # for ASCII values, simplejson might give us unicode *or* bytes - if "nickname" in ann and isinstance(ann["nickname"], str): + if "nickname" in ann and isinstance(ann["nickname"], bytes): ann["nickname"] = unicode(ann["nickname"]) nick_s = ann.get("nickname",u"").encode("utf-8") lp2 = self.log(format="announcement for nickname '%(nick)s', service=%(svc)s: %(ann)s", @@ -267,11 +269,11 @@ class IntroducerClient(service.Service, Referenceable): # how do we describe this node in the logs? desc_bits = [] assert key_s - desc_bits.append("serverid=" + key_s[:20]) + desc_bits.append(b"serverid=" + key_s[:20]) if "anonymous-storage-FURL" in ann: tubid_s = get_tubid_string_from_ann(ann) - desc_bits.append("tubid=" + tubid_s[:8]) - description = "/".join(desc_bits) + desc_bits.append(b"tubid=" + tubid_s[:8]) + description = b"/".join(desc_bits) # the index is used to track duplicates index = (service_name, key_s) @@ -321,7 +323,7 @@ class IntroducerClient(service.Service, Referenceable): self._deliver_announcements(key_s, ann) def _deliver_announcements(self, key_s, ann): - precondition(isinstance(key_s, str), key_s) + precondition(isinstance(key_s, bytes), key_s) service_name = str(ann["service-name"]) for (service_name2,cb,args,kwargs) in self._local_subscribers: if service_name2 == service_name: diff --git a/src/allmydata/introducer/common.py b/src/allmydata/introducer/common.py index abc0811f0..7383d507e 100644 --- a/src/allmydata/introducer/common.py +++ b/src/allmydata/introducer/common.py @@ -1,16 +1,19 @@ +from past.builtins import unicode + import re -import json from allmydata.crypto.util import remove_prefix from allmydata.crypto import ed25519 -from allmydata.util import base32, rrefutil +from allmydata.util import base32, rrefutil, jsonbytes as json def get_tubid_string_from_ann(ann): - return get_tubid_string(str(ann.get("anonymous-storage-FURL") - or ann.get("FURL"))) + furl = ann.get("anonymous-storage-FURL") or ann.get("FURL") + if isinstance(furl, unicode): + furl = furl.encode("utf-8") + return get_tubid_string(furl) def get_tubid_string(furl): - m = re.match(r'pb://(\w+)@', furl) + m = re.match(br'pb://(\w+)@', furl) assert m return m.group(1).lower() diff --git a/src/allmydata/test/test_introducer.py b/src/allmydata/test/test_introducer.py index d99e18c4a..c1a17297e 100644 --- a/src/allmydata/test/test_introducer.py +++ b/src/allmydata/test/test_introducer.py @@ -1,3 +1,4 @@ +from six import ensure_binary, ensure_text import os, re, itertools from base64 import b32decode @@ -200,9 +201,9 @@ class Client(AsyncTestCase): def _received(key_s, ann): announcements.append( (key_s, ann) ) ic1.subscribe_to("storage", _received) - furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:36106/gydnp" - furl1a = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:7777/gydnp" - furl2 = "pb://ttwwooyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:36106/ttwwoo" + furl1 = b"pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:36106/gydnp" + furl1a = b"pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:7777/gydnp" + furl2 = b"pb://ttwwooyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:36106/ttwwoo" private_key, public_key = ed25519.create_signing_keypair() public_key_str = ed25519.string_from_verifying_key(public_key) @@ -300,7 +301,7 @@ class Server(AsyncTestCase): "introducer.furl", u"my_nickname", "ver23", "oldest_version", {}, realseq, FilePath(self.mktemp())) - furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:36106/gydnp" + furl1 = b"pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:36106/gydnp" private_key, _ = ed25519.create_signing_keypair() @@ -398,7 +399,7 @@ class Queue(SystemTestMixin, AsyncTestCase): c = IntroducerClient(tub2, ifurl, u"nickname", "version", "oldest", {}, fakeseq, FilePath(self.mktemp())) - furl1 = "pb://onug64tu@127.0.0.1:123/short" # base32("short") + furl1 = b"pb://onug64tu@127.0.0.1:123/short" # base32("short") private_key, _ = ed25519.create_signing_keypair() d = introducer.disownServiceParent() @@ -741,7 +742,7 @@ class ClientInfo(AsyncTestCase): client_v2 = IntroducerClient(tub, introducer_furl, NICKNAME % u"v2", "my_version", "oldest", app_versions, fakeseq, FilePath(self.mktemp())) - #furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum" + #furl1 = b"pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum" #ann_s = make_ann_t(client_v2, furl1, None, 10) #introducer.remote_publish_v2(ann_s, Referenceable()) subscriber = FakeRemoteReference() @@ -764,7 +765,7 @@ class Announcements(AsyncTestCase): client_v2 = IntroducerClient(tub, introducer_furl, u"nick-v2", "my_version", "oldest", app_versions, fakeseq, FilePath(self.mktemp())) - furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum" + furl1 = b"pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum" private_key, public_key = ed25519.create_signing_keypair() public_key_str = remove_prefix(ed25519.string_from_verifying_key(public_key), "pub-") @@ -806,8 +807,8 @@ class Announcements(AsyncTestCase): c = yield create_client(basedir) ic = c.introducer_clients[0] private_key, public_key = ed25519.create_signing_keypair() - public_key_str = remove_prefix(ed25519.string_from_verifying_key(public_key), "pub-") - furl1 = "pb://onug64tu@127.0.0.1:123/short" # base32("short") + public_key_str = remove_prefix(ed25519.string_from_verifying_key(public_key), b"pub-") + furl1 = b"pb://onug64tu@127.0.0.1:123/short" # base32("short") ann_t = make_ann_t(ic, furl1, private_key, 1) ic.got_announcements([ann_t]) @@ -818,12 +819,12 @@ class Announcements(AsyncTestCase): self.failUnlessEqual(len(announcements), 1) self.failUnlessEqual(announcements[0]['key_s'], public_key_str) ann = announcements[0]["ann"] - self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1) + self.failUnlessEqual(ensure_binary(ann["anonymous-storage-FURL"]), furl1) self.failUnlessEqual(ann["seqnum"], 1) # a new announcement that replaces the first should replace the # cached entry, not duplicate it - furl2 = furl1 + "er" + furl2 = furl1 + b"er" ann_t2 = make_ann_t(ic, furl2, private_key, 2) ic.got_announcements([ann_t2]) yield flushEventualQueue() @@ -831,14 +832,14 @@ class Announcements(AsyncTestCase): self.failUnlessEqual(len(announcements), 1) self.failUnlessEqual(announcements[0]['key_s'], public_key_str) ann = announcements[0]["ann"] - self.failUnlessEqual(ann["anonymous-storage-FURL"], furl2) + self.failUnlessEqual(ensure_binary(ann["anonymous-storage-FURL"]), furl2) self.failUnlessEqual(ann["seqnum"], 2) # but a third announcement with a different key should add to the # cache private_key2, public_key2 = ed25519.create_signing_keypair() - public_key_str2 = remove_prefix(ed25519.string_from_verifying_key(public_key2), "pub-") - furl3 = "pb://onug64tu@127.0.0.1:456/short" + public_key_str2 = remove_prefix(ed25519.string_from_verifying_key(public_key2), b"pub-") + furl3 = b"pb://onug64tu@127.0.0.1:456/short" ann_t3 = make_ann_t(ic, furl3, private_key2, 1) ic.got_announcements([ann_t3]) yield flushEventualQueue() @@ -848,7 +849,7 @@ class Announcements(AsyncTestCase): self.failUnlessEqual(set([public_key_str, public_key_str2]), set([a["key_s"] for a in announcements])) self.failUnlessEqual(set([furl2, furl3]), - set([a["ann"]["anonymous-storage-FURL"] + set([ensure_binary(a["ann"]["anonymous-storage-FURL"]) for a in announcements])) # test loading @@ -864,9 +865,9 @@ class Announcements(AsyncTestCase): yield flushEventualQueue() self.failUnless(public_key_str in announcements) - self.failUnlessEqual(announcements[public_key_str]["anonymous-storage-FURL"], + self.failUnlessEqual(ensure_binary(announcements[public_key_str]["anonymous-storage-FURL"]), furl2) - self.failUnlessEqual(announcements[public_key_str2]["anonymous-storage-FURL"], + self.failUnlessEqual(ensure_binary(announcements[public_key_str2]["anonymous-storage-FURL"]), furl3) c2 = yield create_client(basedir) @@ -979,7 +980,7 @@ class DecodeFurl(SyncTestCase): def test_decode(self): # make sure we have a working base64.b32decode. The one in # python2.4.[01] was broken. - furl = 'pb://t5g7egomnnktbpydbuijt6zgtmw4oqi5@127.0.0.1:51857/hfzv36i' + furl = b'pb://t5g7egomnnktbpydbuijt6zgtmw4oqi5@127.0.0.1:51857/hfzv36i' m = re.match(r'pb://(\w+)@', furl) assert m nodeid = b32decode(m.group(1).upper()) From 38dd0d1b709d8f2f9b4ee5737c2b0f0b6eb5d9ad Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 19 Nov 2020 11:12:08 -0500 Subject: [PATCH 52/91] Only run codechecks on changed Python source files --- .pre-commit-config.yaml | 11 ++++++----- tox.ini | 14 +++++++++----- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 76162535a..916b331e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,10 @@ repos: - - repo: local + - repo: "local" hooks: - - id: codechecks - name: codechecks + - id: "codechecks" + name: "codechecks" stages: ["push"] + language: "system" + files: ".py$" entry: "tox -e codechecks" - language: system - pass_filenames: false + pass_filenames: true diff --git a/tox.ini b/tox.ini index 597270e3a..b95476f58 100644 --- a/tox.ini +++ b/tox.ini @@ -95,12 +95,16 @@ setenv = # .decode(getattr(sys.stdout, "encoding", "utf8")) # `TypeError: decode() argument 1 must be string, not None` PYTHONIOENCODING=utf_8 + + # If no positional arguments are given, try to run the checks on the + # entire codebase, including various pieces of supporting code. + DEFAULT_FILES="src integration static misc setup.py" commands = - flake8 src integration static misc setup.py - python misc/coding_tools/check-umids.py src - python misc/coding_tools/check-debugging.py - python misc/coding_tools/find-trailing-spaces.py -r src static misc setup.py - python misc/coding_tools/check-miscaptures.py + flake8 {posargs:{env:DEFAULT_FILES}} + python misc/coding_tools/check-umids.py {posargs:{env:DEFAULT_FILES}} + python misc/coding_tools/check-debugging.py {posargs:{env:DEFAULT_FILES}} + python misc/coding_tools/find-trailing-spaces.py -r {posargs:{env:DEFAULT_FILES}} + python misc/coding_tools/check-miscaptures.py {posargs:{env:DEFAULT_FILES}} # If towncrier.check fails, you forgot to add a towncrier news # fragment explaining the change in this branch. Create one at From c4a67d6b8caf99aa244ec40e976f153fe5f7a2f5 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 19 Nov 2020 11:12:21 -0500 Subject: [PATCH 53/91] news fragment --- newsfragments/3515.minor | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 newsfragments/3515.minor diff --git a/newsfragments/3515.minor b/newsfragments/3515.minor new file mode 100644 index 000000000..e69de29bb From 8029a1befc6feaf675078464e985f42840f49638 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 19 Nov 2020 11:45:32 -0500 Subject: [PATCH 54/91] First passing test on Python 3. --- src/allmydata/client.py | 18 +++++++++++++----- src/allmydata/introducer/client.py | 5 ++++- src/allmydata/test/test_introducer.py | 6 +++--- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/allmydata/client.py b/src/allmydata/client.py index a768ba354..e7d0737c2 100644 --- a/src/allmydata/client.py +++ b/src/allmydata/client.py @@ -1,3 +1,5 @@ +from past.builtins import unicode + import os, stat, time, weakref from base64 import urlsafe_b64encode from functools import partial @@ -728,10 +730,14 @@ class _Client(node.Node, pollmixin.PollMixin): return { 'node.uptime': time.time() - self.started_timestamp } def init_secrets(self): - lease_s = self.config.get_or_create_private_config("secret", _make_secret) + # configs are always unicode + def _unicode_make_secret(): + return unicode(_make_secret(), "ascii") + lease_s = self.config.get_or_create_private_config( + "secret", _unicode_make_secret).encode("utf-8") lease_secret = base32.a2b(lease_s) - convergence_s = self.config.get_or_create_private_config('convergence', - _make_secret) + convergence_s = self.config.get_or_create_private_config( + 'convergence', _unicode_make_secret).encode("utf-8") self.convergence = base32.a2b(convergence_s) self._secret_holder = SecretHolder(lease_secret, self.convergence) @@ -740,9 +746,11 @@ class _Client(node.Node, pollmixin.PollMixin): # existing key def _make_key(): private_key, _ = ed25519.create_signing_keypair() - return ed25519.string_from_signing_key(private_key) + b"\n" + # Config values are always unicode: + return unicode(ed25519.string_from_signing_key(private_key) + b"\n", "utf-8") - private_key_str = self.config.get_or_create_private_config("node.privkey", _make_key) + private_key_str = self.config.get_or_create_private_config( + "node.privkey", _make_key).encode("utf-8") private_key, public_key = ed25519.signing_keypair_from_string(private_key_str) public_key_str = ed25519.string_from_verifying_key(public_key) self.config.write_config_file("node.pubkey", public_key_str + b"\n", "wb") diff --git a/src/allmydata/introducer/client.py b/src/allmydata/introducer/client.py index b7238d58c..170b6883d 100644 --- a/src/allmydata/introducer/client.py +++ b/src/allmydata/introducer/client.py @@ -1,4 +1,5 @@ from past.builtins import unicode, long +from six import ensure_text import time from zope.interface import implementer @@ -114,9 +115,11 @@ class IntroducerClient(service.Service, Referenceable): announcements = [] for _, value in self._inbound_announcements.items(): ann, key_s, time_stamp = value + # On Python 2, bytes are stored as Unicode. To minimize changes, Python + # 3 for now ensures the same is true. server_params = { "ann" : ann, - "key_s" : key_s, + "key_s" : ensure_text(key_s), } announcements.append(server_params) announcement_cache_yaml = yamlutil.safe_dump(announcements) diff --git a/src/allmydata/test/test_introducer.py b/src/allmydata/test/test_introducer.py index c1a17297e..b820d8796 100644 --- a/src/allmydata/test/test_introducer.py +++ b/src/allmydata/test/test_introducer.py @@ -817,7 +817,7 @@ class Announcements(AsyncTestCase): # check the cache for the announcement announcements = self._load_cache(cache_filepath) self.failUnlessEqual(len(announcements), 1) - self.failUnlessEqual(announcements[0]['key_s'], public_key_str) + self.failUnlessEqual(ensure_binary(announcements[0]['key_s']), public_key_str) ann = announcements[0]["ann"] self.failUnlessEqual(ensure_binary(ann["anonymous-storage-FURL"]), furl1) self.failUnlessEqual(ann["seqnum"], 1) @@ -830,7 +830,7 @@ class Announcements(AsyncTestCase): yield flushEventualQueue() announcements = self._load_cache(cache_filepath) self.failUnlessEqual(len(announcements), 1) - self.failUnlessEqual(announcements[0]['key_s'], public_key_str) + self.failUnlessEqual(ensure_binary(announcements[0]['key_s']), public_key_str) ann = announcements[0]["ann"] self.failUnlessEqual(ensure_binary(ann["anonymous-storage-FURL"]), furl2) self.failUnlessEqual(ann["seqnum"], 2) @@ -847,7 +847,7 @@ class Announcements(AsyncTestCase): announcements = self._load_cache(cache_filepath) self.failUnlessEqual(len(announcements), 2) self.failUnlessEqual(set([public_key_str, public_key_str2]), - set([a["key_s"] for a in announcements])) + set([ensure_binary(a["key_s"]) for a in announcements])) self.failUnlessEqual(set([furl2, furl3]), set([ensure_binary(a["ann"]["anonymous-storage-FURL"]) for a in announcements])) From ad893c9aa1f0b7fd7463b889eac105b3ce30db26 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 19 Nov 2020 11:47:57 -0500 Subject: [PATCH 55/91] More passing Python 3 tests. --- src/allmydata/test/test_introducer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/allmydata/test/test_introducer.py b/src/allmydata/test/test_introducer.py index b820d8796..55ea02229 100644 --- a/src/allmydata/test/test_introducer.py +++ b/src/allmydata/test/test_introducer.py @@ -207,7 +207,7 @@ class Client(AsyncTestCase): private_key, public_key = ed25519.create_signing_keypair() public_key_str = ed25519.string_from_verifying_key(public_key) - pubkey_s = remove_prefix(public_key_str, "pub-") + pubkey_s = remove_prefix(public_key_str, b"pub-") # ann1: ic1, furl1 # ann1a: ic1, furl1a (same SturdyRef, different connection hints) @@ -227,7 +227,7 @@ class Client(AsyncTestCase): self.failUnlessEqual(len(announcements), 1) key_s,ann = announcements[0] self.failUnlessEqual(key_s, pubkey_s) - self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1) + self.failUnlessEqual(ensure_binary(ann["anonymous-storage-FURL"]), furl1) self.failUnlessEqual(ann["my-version"], "ver23") d.addCallback(_then1) @@ -261,7 +261,7 @@ class Client(AsyncTestCase): self.failUnlessEqual(len(announcements), 2) key_s,ann = announcements[-1] self.failUnlessEqual(key_s, pubkey_s) - self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1) + self.failUnlessEqual(ensure_binary(ann["anonymous-storage-FURL"]), furl1) self.failUnlessEqual(ann["my-version"], "ver24") d.addCallback(_then3) @@ -273,7 +273,7 @@ class Client(AsyncTestCase): self.failUnlessEqual(len(announcements), 3) key_s,ann = announcements[-1] self.failUnlessEqual(key_s, pubkey_s) - self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1a) + self.failUnlessEqual(ensure_binary(ann["anonymous-storage-FURL"]), furl1a) self.failUnlessEqual(ann["my-version"], "ver23") d.addCallback(_then4) @@ -289,7 +289,7 @@ class Client(AsyncTestCase): self.failUnlessEqual(len(announcements2), 1) key_s,ann = announcements2[-1] self.failUnlessEqual(key_s, pubkey_s) - self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1a) + self.failUnlessEqual(ensure_binary(ann["anonymous-storage-FURL"]), furl1a) self.failUnlessEqual(ann["my-version"], "ver23") d.addCallback(_then5) return d @@ -768,7 +768,7 @@ class Announcements(AsyncTestCase): furl1 = b"pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum" private_key, public_key = ed25519.create_signing_keypair() - public_key_str = remove_prefix(ed25519.string_from_verifying_key(public_key), "pub-") + public_key_str = remove_prefix(ed25519.string_from_verifying_key(public_key), b"pub-") ann_t0 = make_ann_t(client_v2, furl1, private_key, 10) canary0 = Referenceable() @@ -781,7 +781,7 @@ class Announcements(AsyncTestCase): self.failUnlessEqual(a[0].nickname, u"nick-v2") self.failUnlessEqual(a[0].service_name, "storage") self.failUnlessEqual(a[0].version, "my_version") - self.failUnlessEqual(a[0].announcement["anonymous-storage-FURL"], furl1) + self.failUnlessEqual(ensure_binary(a[0].announcement["anonymous-storage-FURL"]), furl1) def _load_cache(self, cache_filepath): with cache_filepath.open() as f: From 2ae03043b7898df771bcf5973bce7dec29bc77d8 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 19 Nov 2020 12:04:02 -0500 Subject: [PATCH 56/91] Another passing Python 3 test. --- src/allmydata/test/test_introducer.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/test_introducer.py b/src/allmydata/test/test_introducer.py index 55ea02229..60dd2dbb6 100644 --- a/src/allmydata/test/test_introducer.py +++ b/src/allmydata/test/test_introducer.py @@ -1,3 +1,4 @@ +from past.builtins import unicode from six import ensure_binary, ensure_text import os, re, itertools @@ -908,7 +909,9 @@ class ClientSeqnums(AsyncBrokenTestCase): self.failUnless("sA" in outbound) self.failUnlessEqual(outbound["sA"]["seqnum"], 1) nonce1 = outbound["sA"]["nonce"] - self.failUnless(isinstance(nonce1, str)) + self.failUnless(isinstance(nonce1, bytes)) + # Make nonce unicode, to match JSON: + outbound["sA"]["nonce"] = unicode(nonce1, "utf-8") self.failUnlessEqual(json.loads(published["sA"][0]), outbound["sA"]) # [1] is the signature, [2] is the pubkey @@ -922,8 +925,11 @@ class ClientSeqnums(AsyncBrokenTestCase): self.failUnless("sA" in outbound) self.failUnlessEqual(outbound["sA"]["seqnum"], 2) nonce2 = outbound["sA"]["nonce"] - self.failUnless(isinstance(nonce2, str)) + self.failUnless(isinstance(nonce2, bytes)) self.failIfEqual(nonce1, nonce2) + # Make nonce unicode, to match JSON: + outbound["sA"]["nonce"] = unicode(nonce2, "utf-8") + outbound["sB"]["nonce"] = unicode(outbound["sB"]["nonce"], "utf-8") self.failUnlessEqual(json.loads(published["sA"][0]), outbound["sA"]) self.failUnlessEqual(json.loads(published["sB"][0]), From bcc509b7a731516be0ead2f79be7e779b9171651 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 19 Nov 2020 14:23:41 -0500 Subject: [PATCH 57/91] Some progress towards passing tests. --- src/allmydata/introducer/client.py | 18 +++++++++------- src/allmydata/introducer/server.py | 7 ++++--- src/allmydata/test/test_introducer.py | 30 +++++++++++++-------------- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/allmydata/introducer/client.py b/src/allmydata/introducer/client.py index 170b6883d..bdbd987e7 100644 --- a/src/allmydata/introducer/client.py +++ b/src/allmydata/introducer/client.py @@ -18,7 +18,7 @@ from allmydata.util.assertutil import precondition class InvalidCacheError(Exception): pass -V2 = "http://allmydata.org/tahoe/protocols/introducer/v2" +V2 = b"http://allmydata.org/tahoe/protocols/introducer/v2" @implementer(RIIntroducerSubscriberClient_v2, IIntroducerClient) class IntroducerClient(service.Service, Referenceable): @@ -28,6 +28,7 @@ class IntroducerClient(service.Service, Referenceable): app_versions, sequencer, cache_filepath): self._tub = tub self.introducer_furl = introducer_furl + assert isinstance(introducer_furl, (bytes, type(None))) assert type(nickname) is unicode self._nickname = nickname @@ -37,11 +38,11 @@ class IntroducerClient(service.Service, Referenceable): self._sequencer = sequencer self._cache_filepath = cache_filepath - self._my_subscriber_info = { "version": 0, - "nickname": self._nickname, - "app-versions": self._app_versions, - "my-version": self._my_version, - "oldest-supported": self._oldest_supported, + self._my_subscriber_info = { b"version": 0, + b"nickname": self._nickname, + b"app-versions": self._app_versions, + b"my-version": self._my_version, + b"oldest-supported": self._oldest_supported, } self._outbound_announcements = {} # not signed @@ -129,9 +130,9 @@ class IntroducerClient(service.Service, Referenceable): def _got_introducer(self, publisher): self.log("connected to introducer, getting versions") - default = { "http://allmydata.org/tahoe/protocols/introducer/v1": + default = { b"http://allmydata.org/tahoe/protocols/introducer/v1": { }, - "application-version": "unknown: no get_version()", + b"application-version": b"unknown: no get_version()", } d = add_version_to_remote_reference(publisher, default) d.addCallback(self._got_versioned_introducer) @@ -144,6 +145,7 @@ class IntroducerClient(service.Service, Referenceable): def _got_versioned_introducer(self, publisher): self.log("got introducer version: %s" % (publisher.version,)) # we require an introducer that speaks at least V2 + assert all(type(V2) == type(v) for v in publisher.version) if V2 not in publisher.version: raise InsufficientVersionError("V2", publisher.version) self._publisher = publisher diff --git a/src/allmydata/introducer/server.py b/src/allmydata/introducer/server.py index 0a933bd01..9c756fef1 100644 --- a/src/allmydata/introducer/server.py +++ b/src/allmydata/introducer/server.py @@ -1,3 +1,4 @@ +from past.builtins import long import time, os.path, textwrap from zope.interface import implementer @@ -122,7 +123,7 @@ class _IntroducerNode(node.Node): from allmydata.webish import IntroducerWebishServer nodeurl_path = self.config.get_config_path(u"node.url") - config_staticdir = self.get_config("node", "web.static", "public_html").decode('utf-8') + config_staticdir = self.get_config("node", "web.static", "public_html") staticdir = self.config.get_config_path(config_staticdir) ws = IntroducerWebishServer(self, webport, nodeurl_path, staticdir) ws.setServiceParent(self) @@ -133,8 +134,8 @@ class IntroducerService(service.MultiService, Referenceable): # v1 is the original protocol, added in 1.0 (but only advertised starting # in 1.3), removed in 1.12. v2 is the new signed protocol, added in 1.10 VERSION = { #"http://allmydata.org/tahoe/protocols/introducer/v1": { }, - "http://allmydata.org/tahoe/protocols/introducer/v2": { }, - "application-version": str(allmydata.__full_version__), + b"http://allmydata.org/tahoe/protocols/introducer/v2": { }, + b"application-version": allmydata.__full_version__.encode("utf-8"), } def __init__(self): diff --git a/src/allmydata/test/test_introducer.py b/src/allmydata/test/test_introducer.py index 60dd2dbb6..f4d01b3d9 100644 --- a/src/allmydata/test/test_introducer.py +++ b/src/allmydata/test/test_introducer.py @@ -102,7 +102,7 @@ class Node(testutil.SignalMixin, testutil.ReallyEqualMixin, AsyncTestCase): q1 = yield create_introducer(basedir) del q1 # new nodes create unguessable furls in private/introducer.furl - ifurl = fileutil.read(private_fn) + ifurl = fileutil.read(private_fn, mode="r") self.failUnless(ifurl) ifurl = ifurl.strip() self.failIf(ifurl.endswith("/introducer"), ifurl) @@ -122,7 +122,7 @@ class Node(testutil.SignalMixin, testutil.ReallyEqualMixin, AsyncTestCase): q2 = yield create_introducer(basedir) del q2 self.failIf(os.path.exists(public_fn)) - ifurl2 = fileutil.read(private_fn) + ifurl2 = fileutil.read(private_fn, mode="r") self.failUnless(ifurl2) self.failUnlessEqual(ifurl2.strip(), guessable) @@ -422,7 +422,7 @@ class Queue(SystemTestMixin, AsyncTestCase): def _done(ign): v = introducer.get_announcements()[0] furl = v.announcement["anonymous-storage-FURL"] - self.failUnlessEqual(furl, furl1) + self.failUnlessEqual(ensure_binary(furl), furl1) d.addCallback(_done) # now let the ack get back @@ -448,7 +448,7 @@ class SystemTest(SystemTestMixin, AsyncTestCase): iff = os.path.join(self.basedir, "introducer.furl") tub = self.central_tub ifurl = self.central_tub.registerReference(introducer, furlFile=iff) - self.introducer_furl = ifurl + self.introducer_furl = ifurl.encode("utf-8") # we have 5 clients who publish themselves as storage servers, and a # sixth which does which not. All 6 clients subscriber to hear about @@ -489,7 +489,7 @@ class SystemTest(SystemTestMixin, AsyncTestCase): subscribing_clients.append(c) expected_announcements[i] += 1 # all expect a 'storage' announcement - node_furl = tub.registerReference(Referenceable()) + node_furl = tub.registerReference(Referenceable()).encode("utf-8") private_key, public_key = ed25519.create_signing_keypair() public_key_str = ed25519.string_from_verifying_key(public_key) privkeys[i] = private_key @@ -506,7 +506,7 @@ class SystemTest(SystemTestMixin, AsyncTestCase): if i == 2: # also publish something that nobody cares about - boring_furl = tub.registerReference(Referenceable()) + boring_furl = tub.registerReference(Referenceable()).encode("utf-8") c.publish("boring", make_ann(boring_furl), private_key) c.setServiceParent(self.parent) @@ -987,10 +987,10 @@ class DecodeFurl(SyncTestCase): # make sure we have a working base64.b32decode. The one in # python2.4.[01] was broken. furl = b'pb://t5g7egomnnktbpydbuijt6zgtmw4oqi5@127.0.0.1:51857/hfzv36i' - m = re.match(r'pb://(\w+)@', furl) + m = re.match(br'pb://(\w+)@', furl) assert m nodeid = b32decode(m.group(1).upper()) - self.failUnlessEqual(nodeid, "\x9fM\xf2\x19\xcckU0\xbf\x03\r\x10\x99\xfb&\x9b-\xc7A\x1d") + self.failUnlessEqual(nodeid, b"\x9fM\xf2\x19\xcckU0\xbf\x03\r\x10\x99\xfb&\x9b-\xc7A\x1d") class Signatures(SyncTestCase): @@ -1002,11 +1002,11 @@ class Signatures(SyncTestCase): (msg, sig, key) = ann_t self.failUnlessEqual(type(msg), type("".encode("utf-8"))) # bytes self.failUnlessEqual(json.loads(msg.decode("utf-8")), ann) - self.failUnless(sig.startswith("v0-")) - self.failUnless(key.startswith("v0-")) + self.failUnless(sig.startswith(b"v0-")) + self.failUnless(key.startswith(b"v0-")) (ann2,key2) = unsign_from_foolscap(ann_t) self.failUnlessEqual(ann2, ann) - self.failUnlessEqual("pub-" + key2, public_key_str) + self.failUnlessEqual(b"pub-" + key2, public_key_str) # not signed self.failUnlessRaises(UnknownKeyError, @@ -1021,16 +1021,16 @@ class Signatures(SyncTestCase): # unrecognized signatures self.failUnlessRaises(UnknownKeyError, - unsign_from_foolscap, (bad_msg, "v999-sig", key)) + unsign_from_foolscap, (bad_msg, b"v999-sig", key)) self.failUnlessRaises(UnknownKeyError, - unsign_from_foolscap, (bad_msg, sig, "v999-key")) + unsign_from_foolscap, (bad_msg, sig, b"v999-key")) def test_unsigned_announcement(self): ed25519.verifying_key_from_string(b"pub-v0-wodst6ly4f7i7akt2nxizsmmy2rlmer6apltl56zctn67wfyu5tq") mock_tub = Mock() ic = IntroducerClient( mock_tub, - u"pb://", + b"pb://", u"fake_nick", "0.0.0", "1.2.3", @@ -1040,7 +1040,7 @@ class Signatures(SyncTestCase): ) self.assertEqual(0, ic._debug_counts["inbound_announcement"]) ic.got_announcements([ - ("message", "v0-aaaaaaa", "v0-wodst6ly4f7i7akt2nxizsmmy2rlmer6apltl56zctn67wfyu5tq") + (b"message", b"v0-aaaaaaa", b"v0-wodst6ly4f7i7akt2nxizsmmy2rlmer6apltl56zctn67wfyu5tq") ]) # we should have rejected this announcement due to a bad signature self.assertEqual(0, ic._debug_counts["inbound_announcement"]) From 0e198e73618795bc3ca93aabaccb50e47f587730 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 20 Nov 2020 11:16:32 -0500 Subject: [PATCH 58/91] Stop hiding Twisted logs! --- src/allmydata/test/common.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/common.py b/src/allmydata/test/common.py index 1cf1d6428..c45b0c4e9 100644 --- a/src/allmydata/test/common.py +++ b/src/allmydata/test/common.py @@ -1190,7 +1190,9 @@ class AsyncTestCase(_TestCaseMixin, TestCase): only fire if the global reactor is running. """ run_tests_with = EliotLoggedRunTest.make_factory( - AsynchronousDeferredRunTest.make_factory(timeout=60.0), + AsynchronousDeferredRunTest.make_factory( + timeout=60.0, suppress_twisted_logging=False, + store_twisted_logs=False), ) @@ -1204,7 +1206,9 @@ class AsyncBrokenTestCase(_TestCaseMixin, TestCase): pass with ``AsyncTestCase``. """ run_tests_with = EliotLoggedRunTest.make_factory( - AsynchronousDeferredRunTestForBrokenTwisted.make_factory(timeout=60.0), + AsynchronousDeferredRunTestForBrokenTwisted.make_factory( + timeout=60.0, suppress_twisted_logging=False, + store_twisted_logs=False), ) From 53a6882f21a1e139039b413176a7370cc58b9561 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 20 Nov 2020 12:02:22 -0500 Subject: [PATCH 59/91] Some progress on Python 3 passing tests, some going backwards. --- src/allmydata/introducer/server.py | 3 +++ src/allmydata/test/test_introducer.py | 8 ++++---- src/allmydata/web/introweb.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/allmydata/introducer/server.py b/src/allmydata/introducer/server.py index 9c756fef1..ef3e3c6d7 100644 --- a/src/allmydata/introducer/server.py +++ b/src/allmydata/introducer/server.py @@ -1,4 +1,5 @@ from past.builtins import long +from six import ensure_str import time, os.path, textwrap from zope.interface import implementer @@ -280,6 +281,7 @@ class IntroducerService(service.MultiService, Referenceable): def remote_subscribe_v2(self, subscriber, service_name, subscriber_info): self.log("introducer: subscription[%s] request at %s" % (service_name, subscriber), umid="U3uzLg") + service_name = ensure_str(service_name) return self.add_subscriber(subscriber, service_name, subscriber_info) def add_subscriber(self, subscriber, service_name, subscriber_info): @@ -303,6 +305,7 @@ class IntroducerService(service.MultiService, Referenceable): subscriber.notifyOnDisconnect(_remove) # now tell them about any announcements they're interested in + assert {type(service_name)} == set(type(k[0]) for k in self._announcements) announcements = set( [ ann_t for idx,(ann_t,canary,ann,when) in self._announcements.items() diff --git a/src/allmydata/test/test_introducer.py b/src/allmydata/test/test_introducer.py index f4d01b3d9..bdae11403 100644 --- a/src/allmydata/test/test_introducer.py +++ b/src/allmydata/test/test_introducer.py @@ -594,7 +594,7 @@ class SystemTest(SystemTestMixin, AsyncTestCase): self.failUnlessEqual(cdc["outbound_message"], expected) # now check the web status, make sure it renders without error ir = introweb.IntroducerRoot(self.parent) - self.parent.nodeid = "NODEID" + self.parent.nodeid = b"NODEID" log.msg("_check1 done") return flattenString(None, ir._create_element()) d.addCallback(_check1) @@ -604,7 +604,7 @@ class SystemTest(SystemTestMixin, AsyncTestCase): self.assertIn(NICKNAME % "0", text) # a v2 client self.assertIn(NICKNAME % "1", text) # another v2 client for i in range(NUM_STORAGE): - self.assertIn(printable_serverids[i], text, + self.assertIn(ensure_text(printable_serverids[i]), text, (i,printable_serverids[i],text)) # make sure there isn't a double-base32ed string too self.assertNotIn(idlib.nodeid_b2a(printable_serverids[i]), text, @@ -644,7 +644,7 @@ class SystemTest(SystemTestMixin, AsyncTestCase): self.create_tub(self.central_portnum) newfurl = self.central_tub.registerReference(self.the_introducer, furlFile=iff) - assert newfurl == self.introducer_furl + assert ensure_binary(newfurl) == self.introducer_furl d.addCallback(_restart_introducer_tub) d.addCallback(_wait_for_connected) @@ -696,7 +696,7 @@ class SystemTest(SystemTestMixin, AsyncTestCase): self.the_introducer = introducer newfurl = self.central_tub.registerReference(self.the_introducer, furlFile=iff) - assert newfurl == self.introducer_furl + assert ensure_binary(newfurl) == self.introducer_furl d.addCallback(_restart_introducer) d.addCallback(_wait_for_connected) diff --git a/src/allmydata/web/introweb.py b/src/allmydata/web/introweb.py index f57a5232a..171c155d7 100644 --- a/src/allmydata/web/introweb.py +++ b/src/allmydata/web/introweb.py @@ -105,7 +105,7 @@ class IntroducerRootElement(Element): if ad.service_name not in services: services[ad.service_name] = 0 services[ad.service_name] += 1 - service_names = services.keys() + service_names = list(services.keys()) service_names.sort() return u", ".join(u"{}: {}".format(service_name, services[service_name]) for service_name in service_names) From 5b87fb4afe60a7c35b0fad7a1b2886cd35d90035 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 20 Nov 2020 14:01:48 -0500 Subject: [PATCH 60/91] All tests pass on Python 2 and 3. --- src/allmydata/introducer/client.py | 3 ++- src/allmydata/introducer/server.py | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/allmydata/introducer/client.py b/src/allmydata/introducer/client.py index bdbd987e7..bfbca7b32 100644 --- a/src/allmydata/introducer/client.py +++ b/src/allmydata/introducer/client.py @@ -27,8 +27,9 @@ class IntroducerClient(service.Service, Referenceable): nickname, my_version, oldest_supported, app_versions, sequencer, cache_filepath): self._tub = tub + if isinstance(introducer_furl, unicode): + introducer_furl = introducer_furl.encode("utf-8") self.introducer_furl = introducer_furl - assert isinstance(introducer_furl, (bytes, type(None))) assert type(nickname) is unicode self._nickname = nickname diff --git a/src/allmydata/introducer/server.py b/src/allmydata/introducer/server.py index ef3e3c6d7..84c940d81 100644 --- a/src/allmydata/introducer/server.py +++ b/src/allmydata/introducer/server.py @@ -1,5 +1,5 @@ from past.builtins import long -from six import ensure_str +from six import ensure_str, ensure_text import time, os.path, textwrap from zope.interface import implementer @@ -9,7 +9,7 @@ from twisted.python.failure import Failure from foolscap.api import Referenceable import allmydata from allmydata import node -from allmydata.util import log, rrefutil +from allmydata.util import log, rrefutil, dictutil from allmydata.util.i2p_provider import create as create_i2p_provider from allmydata.util.tor_provider import create as create_tor_provider from allmydata.introducer.interfaces import \ @@ -282,6 +282,9 @@ class IntroducerService(service.MultiService, Referenceable): self.log("introducer: subscription[%s] request at %s" % (service_name, subscriber), umid="U3uzLg") service_name = ensure_str(service_name) + subscriber_info = dictutil.UnicodeKeyDict({ + ensure_text(k): v for (k, v) in subscriber_info.items() + }) return self.add_subscriber(subscriber, service_name, subscriber_info) def add_subscriber(self, subscriber, service_name, subscriber_info): @@ -305,7 +308,8 @@ class IntroducerService(service.MultiService, Referenceable): subscriber.notifyOnDisconnect(_remove) # now tell them about any announcements they're interested in - assert {type(service_name)} == set(type(k[0]) for k in self._announcements) + assert {type(service_name)} >= set(type(k[0]) for k in self._announcements), ( + service_name, self._announcements.keys()) announcements = set( [ ann_t for idx,(ann_t,canary,ann,when) in self._announcements.items() From 661bc967d2c5d7ce2e94a39745927d4bad12399c Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 20 Nov 2020 14:06:16 -0500 Subject: [PATCH 61/91] Port to Python 3. --- src/allmydata/test/test_introducer.py | 23 +++++++++++++++++------ src/allmydata/util/_python3.py | 1 + 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/allmydata/test/test_introducer.py b/src/allmydata/test/test_introducer.py index bdae11403..58b2db494 100644 --- a/src/allmydata/test/test_introducer.py +++ b/src/allmydata/test/test_introducer.py @@ -1,4 +1,15 @@ -from past.builtins import unicode +""" +Ported to Python 3. +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from future.utils import PY2 +if PY2: + from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401 + from six import ensure_binary, ensure_text import os, re, itertools @@ -171,7 +182,7 @@ def fakeseq(): seqnum_counter = itertools.count(1) def realseq(): - return seqnum_counter.next(), str(os.randint(1,100000)) + return next(seqnum_counter), str(os.randint(1,100000)) def make_ann(furl): ann = { "anonymous-storage-FURL": furl, @@ -583,7 +594,7 @@ class SystemTest(SystemTestMixin, AsyncTestCase): serverid0 = printable_serverids[0] ann = anns[serverid0] nick = ann["nickname"] - self.failUnlessEqual(type(nick), unicode) + self.assertIsInstance(nick, str) self.failUnlessEqual(nick, NICKNAME % "0") for c in publishing_clients: cdc = c._debug_counts @@ -911,7 +922,7 @@ class ClientSeqnums(AsyncBrokenTestCase): nonce1 = outbound["sA"]["nonce"] self.failUnless(isinstance(nonce1, bytes)) # Make nonce unicode, to match JSON: - outbound["sA"]["nonce"] = unicode(nonce1, "utf-8") + outbound["sA"]["nonce"] = str(nonce1, "utf-8") self.failUnlessEqual(json.loads(published["sA"][0]), outbound["sA"]) # [1] is the signature, [2] is the pubkey @@ -928,8 +939,8 @@ class ClientSeqnums(AsyncBrokenTestCase): self.failUnless(isinstance(nonce2, bytes)) self.failIfEqual(nonce1, nonce2) # Make nonce unicode, to match JSON: - outbound["sA"]["nonce"] = unicode(nonce2, "utf-8") - outbound["sB"]["nonce"] = unicode(outbound["sB"]["nonce"], "utf-8") + outbound["sA"]["nonce"] = str(nonce2, "utf-8") + outbound["sB"]["nonce"] = str(outbound["sB"]["nonce"], "utf-8") self.failUnlessEqual(json.loads(published["sA"][0]), outbound["sA"]) self.failUnlessEqual(json.loads(published["sB"][0]), diff --git a/src/allmydata/util/_python3.py b/src/allmydata/util/_python3.py index 7afefceed..e5c1e2d5a 100644 --- a/src/allmydata/util/_python3.py +++ b/src/allmydata/util/_python3.py @@ -137,6 +137,7 @@ PORTED_TEST_MODULES = [ "allmydata.test.test_helper", "allmydata.test.test_humanreadable", "allmydata.test.test_immutable", + "allmydata.test.test_introducer", "allmydata.test.test_iputil", "allmydata.test.test_log", "allmydata.test.test_monitor", From 0d652a3af1d025d7e1228adf9780dcfe50fb1f8a Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 20 Nov 2020 14:06:31 -0500 Subject: [PATCH 62/91] News file. --- newsfragments/3514.minor | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 newsfragments/3514.minor diff --git a/newsfragments/3514.minor b/newsfragments/3514.minor new file mode 100644 index 000000000..e69de29bb From e385cd02a36062030cbb1909d54012e7c8292395 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 25 Nov 2020 18:30:36 -0500 Subject: [PATCH 63/91] Footnote about the zero-or-more thing for fURLs --- docs/specifications/url.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/specifications/url.rst b/docs/specifications/url.rst index 2bb554818..05807a858 100644 --- a/docs/specifications/url.rst +++ b/docs/specifications/url.rst @@ -15,7 +15,7 @@ Foolscap connection setup takes as an input a Foolscap URL or a *fURL*. A fURL includes three components: * the base32-encoded SHA1 hash of the DER form of an x509v3 certificate -* zero or more network addresses +* zero or more network addresses [1]_ * an object identifier A Foolscap client tries to connect to each network address in turn. @@ -110,3 +110,11 @@ This provides stronger authentication assurances for future uses but it is not r .. _`continues to be eroded`: https://en.wikipedia.org/wiki/SHA-1#Cryptanalysis_and_validation .. _`explored by the web community`: https://www.imperialviolet.org/2011/05/04/pinning.html .. _Foolscap: https://github.com/warner/foolscap + +.. [1] ``foolscap.furl.decode_furl`` is taken as the canonical definition of the syntax of a fURL. + The **location hints** part of the fURL, + as it is referred to in Foolscap, + is matched by the regular expression fragment ``([^/]*)``. + Since this matches the empty string, + no network addresses are required to form a fURL. + The supporting code around the regular expression also takes extra steps to allow an empty string to match here. From c4f7643b999c1266f20ca1ec747d42027c44855f Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Nov 2020 07:25:25 -0500 Subject: [PATCH 64/91] introduce "swiss number" and clarify text a bit --- docs/specifications/url.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/specifications/url.rst b/docs/specifications/url.rst index 05807a858..16bf79d9f 100644 --- a/docs/specifications/url.rst +++ b/docs/specifications/url.rst @@ -26,7 +26,10 @@ This serves as the process by which the client authenticates the server. The client can then exercise further Foolscap functionality using the fURL's object identifier. If the object identifier is an unguessable, secret string then it serves as a capability. -This serves as the process by which the server authorizes the client. +This unguessable identifier is sometimes called a `swiss number`_ (or swissnum). +The client's use of the swissnum is what allows the server to authorize the client. + +.. _`swiss number`: http://wiki.erights.org/wiki/Swiss_number NURLs ----- From ee72029bd49f44bde17b0d66d54c89a6de104823 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Nov 2020 07:31:59 -0500 Subject: [PATCH 65/91] warn about what's unauthenticated --- docs/specifications/url.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/specifications/url.rst b/docs/specifications/url.rst index 16bf79d9f..24794ceec 100644 --- a/docs/specifications/url.rst +++ b/docs/specifications/url.rst @@ -98,6 +98,11 @@ The hash component of a version 1 NURL differs in three ways from the prior vers This is useful to allow contact information to be updated or extension of validity period. Use of an SPKI hash has also been `explored by the web community`_ during its flirtation with using it for HTTPS certificate pinning (though this is now largely abandoned). + +.. note:: + *Only* the certificate's keypair is pinned by the SPKI hash. + The freedom to change every other part of the certificate is coupled with the fact that all other parts of the certificate contain arbitrary information set by the private key holder. + 3. The hash is encoded using urlsafe-base64 (without padding) instead of base32. This provides a more compact representation and minimizes the usability impacts of switching from a 160 bit hash to a 224 bit hash. From 02579768bd379a9c453870620a655f3b9e2e1f26 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Nov 2020 09:18:12 -0500 Subject: [PATCH 66/91] Still gonna TLS --- docs/specifications/url.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/specifications/url.rst b/docs/specifications/url.rst index 24794ceec..1f55b7db3 100644 --- a/docs/specifications/url.rst +++ b/docs/specifications/url.rst @@ -44,6 +44,9 @@ Therefore, this document coins the name **NURL** for these URLs. This can be considered to expand to "**N**\ ew URLs" or "Authe\ **N**\ ticating URLs" or "Authorizi\ **N**\ g URLs" as the reader prefers. +The anticipated use for a **NURL** will still be to establish a TLS connection to a peer. +The protocol run over that TLS connection could be Foolscap though it is more likely to be an HTTP-based protocol. + Syntax ------ From ccc6afa14063cf3ff69877850b1761f187d48cf0 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Nov 2020 09:43:05 -0500 Subject: [PATCH 67/91] explicit callback --- docs/specifications/url.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specifications/url.rst b/docs/specifications/url.rst index 1f55b7db3..8829d756f 100644 --- a/docs/specifications/url.rst +++ b/docs/specifications/url.rst @@ -45,7 +45,7 @@ this document coins the name **NURL** for these URLs. This can be considered to expand to "**N**\ ew URLs" or "Authe\ **N**\ ticating URLs" or "Authorizi\ **N**\ g URLs" as the reader prefers. The anticipated use for a **NURL** will still be to establish a TLS connection to a peer. -The protocol run over that TLS connection could be Foolscap though it is more likely to be an HTTP-based protocol. +The protocol run over that TLS connection could be Foolscap though it is more likely to be an HTTP-based protocol (such as GBS). Syntax ------ From 4ee28a8479235d463c427a76336508c05ad0f748 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Nov 2020 10:16:40 -0500 Subject: [PATCH 68/91] Some example NURLs for flavor --- docs/specifications/url.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/specifications/url.rst b/docs/specifications/url.rst index 8829d756f..e6e905bab 100644 --- a/docs/specifications/url.rst +++ b/docs/specifications/url.rst @@ -86,6 +86,12 @@ Notably, the hash component is defined as the base32-encoded SHA1 hash of the DER form of an x509v3 certificate. A version 0 NURL is identified by the absence of the ``v=1`` fragment. +Examples +~~~~~~~~ + +* ``pb://sisi4zenj7cxncgvdog7szg3yxbrnamy@tcp:127.1:34399/xphmwz6lx24rh2nxlinni`` +* ``pb://2uxmzoqqimpdwowxr24q6w5ekmxcymby@localhost:47877/riqhpojvzwxujhna5szkn`` + Version 1 --------- @@ -118,6 +124,12 @@ After establishing and authenticating a connection the client will have received This is sufficient to compute the new hash and rewrite the NURL to upgrade it to version 1. This provides stronger authentication assurances for future uses but it is not required. +Examples +~~~~~~~~ + +* ``pb://1WUX44xKjKdpGLohmFcBNuIRN-8rlv1Iij_7rQ@tcp:127.1:34399/jhjbc3bjbhk#v=1`` +* ``pb://azEu8vlRpnEeYm0DySQDeNY3Z2iJXHC_bsbaAw@localhost:47877/64i4aokv4ej#v=1`` + .. _`continues to be eroded`: https://en.wikipedia.org/wiki/SHA-1#Cryptanalysis_and_validation .. _`explored by the web community`: https://www.imperialviolet.org/2011/05/04/pinning.html .. _Foolscap: https://github.com/warner/foolscap From a5f0be6513936dfcb18c5719decaf06cd87f9765 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Nov 2020 10:16:49 -0500 Subject: [PATCH 69/91] Oops these have schemes most of the time except tcp is implied if the scheme is missing Is this grammar ambiguous? I don't know but I think so. --- docs/specifications/url.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/specifications/url.rst b/docs/specifications/url.rst index e6e905bab..32e755bed 100644 --- a/docs/specifications/url.rst +++ b/docs/specifications/url.rst @@ -59,7 +59,13 @@ The EBNF for a NURL is as follows:: hash = unreserved net-loc-list = net-loc, [ { ",", net-loc } ] - net-loc = hostname, [ ":" port ] + net-loc = tcp-loc | tor-loc | i2p-loc + + tcp-loc = [ "tcp:" ], hostname, [ ":" port ] + tor-loc = "tor:", hostname, [ ":" port ] + i2p-loc = "i2p:", i2p-addr, [ ":" port ] + + i2p-addr = { unreserved }, ".i2p" hostname = domain | IPv4address | IPv6address swiss-number = segment From b2c0d1b7ae67e990fdd8d6e9f5aa0c81dc64e76a Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Nov 2020 10:31:22 -0500 Subject: [PATCH 70/91] Caveat the rest of the certificate fields --- docs/specifications/url.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/specifications/url.rst b/docs/specifications/url.rst index 32e755bed..4aa5c21d6 100644 --- a/docs/specifications/url.rst +++ b/docs/specifications/url.rst @@ -117,6 +117,9 @@ The hash component of a version 1 NURL differs in three ways from the prior vers .. note:: *Only* the certificate's keypair is pinned by the SPKI hash. The freedom to change every other part of the certificate is coupled with the fact that all other parts of the certificate contain arbitrary information set by the private key holder. + It is neither guaranteed nor expected that a certificate-issuing authority has validated this information. + Therefore, + *all* certificate fields should be considered within the context of the relationship identified by the SPKI hash. 3. The hash is encoded using urlsafe-base64 (without padding) instead of base32. This provides a more compact representation and minimizes the usability impacts of switching from a 160 bit hash to a 224 bit hash. From c3ba08c205008c142567d34a869ccb7f9e5fefca Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Nov 2020 10:41:26 -0500 Subject: [PATCH 71/91] open questions --- docs/specifications/url.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/specifications/url.rst b/docs/specifications/url.rst index 4aa5c21d6..31fb05fad 100644 --- a/docs/specifications/url.rst +++ b/docs/specifications/url.rst @@ -150,3 +150,16 @@ Examples Since this matches the empty string, no network addresses are required to form a fURL. The supporting code around the regular expression also takes extra steps to allow an empty string to match here. + +Open Questions +-------------- + +1. Should we make a hard recommendation that all certificate fields are ignored? + The system makes no guarantees about validation of these fields. + Is it just an unnecessary risk to let a user see them? + +2. Should the version specifier be a query-arg-alike or a fragment-alike? + The value is only necessary on the client side which makes it similar to an HTTP URL fragment. + The current Tahoe-LAFS configuration parsing code has special handling of the fragment character (``#``) which makes it unusable. + However, + the configuration parsing code is easily changed. From d81fe54faf1eeea19f41beeebccb43f489f9d33a Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Nov 2020 10:50:36 -0500 Subject: [PATCH 72/91] typo fix --- src/allmydata/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/node.py b/src/allmydata/node.py index 99062d771..93fb7461d 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -373,7 +373,7 @@ class _Config(object): def set_config(self, section, option, value): """ - Set a config options in a section and re-write the tahoe.cfg file + Set a config option in a section and re-write the tahoe.cfg file """ if option.endswith(".furl") and "#" in value: raise UnescapedHashError(section, option, value) From df53fdcf9bd923e7b548d21d5365f3179e8ba8aa Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Nov 2020 10:53:42 -0500 Subject: [PATCH 73/91] add missing docs to new set_config method --- src/allmydata/node.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/allmydata/node.py b/src/allmydata/node.py index 93fb7461d..98d7dbafa 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -374,6 +374,16 @@ class _Config(object): def set_config(self, section, option, value): """ Set a config option in a section and re-write the tahoe.cfg file + + :param str section: The name of the section in which to set the + option. + + :param str option: The name of the option to set. + + :param str value: The value of the option. + + :raise UnescapedHashError: If the option holds a fURL and there is a + ``#`` in the value. """ if option.endswith(".furl") and "#" in value: raise UnescapedHashError(section, option, value) From 3843131acfc8a659a708368f3405ba2835bceb35 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Nov 2020 19:29:52 -0500 Subject: [PATCH 74/91] Can have more than one introducer if you want --- docs/configuration.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 8583888ca..0aab9b395 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -399,8 +399,8 @@ This section controls *when* Tor and I2P are used. The ``[tor]`` and managed. All Tahoe nodes need to make a connection to the Introducer; the -``private/introducers.yaml`` file (described below) configures where the -Introducer lives. Tahoe client nodes must also make connections to storage +``private/introducers.yaml`` file (described below) configures where one or more +Introducers live. Tahoe client nodes must also make connections to storage servers: these targets are specified in announcements that come from the Introducer. Both are expressed as FURLs (a Foolscap URL), which include a list of "connection hints". Each connection hint describes one (of perhaps From a978fcf433816174c521a236361392c37b1f1f48 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Nov 2020 19:35:39 -0500 Subject: [PATCH 75/91] Replace asserts with explicit checks and TypeError --- src/allmydata/node.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/allmydata/node.py b/src/allmydata/node.py index 7eefbbce0..6253aad4b 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -463,11 +463,34 @@ class _Config(object): for petname, config in introducers_yaml.get("introducers", {}).items() } - assert all(isinstance(k, str) for k in introducers.keys()) - assert all(isinstance(v, str) for v in introducers.values()), introducers.values() + non_strs = list( + k + for k + in introducers.keys() + if not isinstance(k, str) + ) + if non_strs: + raise TypeError( + "Introducer petnames {!r} should have been str".format( + non_strs, + ), + ) + non_strs = list( + v + for v + in introducers.values() + if not isinstance(v, str) + ) + if non_strs: + raise TypeError( + "Introducer fURLs {!r} should have been str".format( + non_strs, + ), + ) log.msg( - "found {} introducers in private/introducers.yaml".format( + "found {} introducers in {!r}".format( len(introducers), + introducers_yaml_filename, ) ) except EnvironmentError as e: From 805378ef1177f13c925a8a2dce2b626b2c8c16f6 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Nov 2020 20:53:57 -0500 Subject: [PATCH 76/91] Do more path stuff with FilePath --- src/allmydata/scripts/common.py | 2 +- src/allmydata/scripts/create_node.py | 5 +- src/allmydata/test/check_memory.py | 25 +++++---- src/allmydata/test/common.py | 2 +- src/allmydata/test/test_client.py | 74 ++++++++++++++------------- src/allmydata/test/test_introducer.py | 35 +++++++------ src/allmydata/test/test_system.py | 15 +++--- 7 files changed, 86 insertions(+), 72 deletions(-) diff --git a/src/allmydata/scripts/common.py b/src/allmydata/scripts/common.py index 6d633b502..f38a241dd 100644 --- a/src/allmydata/scripts/common.py +++ b/src/allmydata/scripts/common.py @@ -124,7 +124,7 @@ def write_introducer(basedir, petname, furl): Overwrite the node's ``introducers.yaml`` with a file containing the given introducer information. """ - FilePath(basedir).child(b"private").child(b"introducers.yaml").setContent( + basedir.child(b"private").child(b"introducers.yaml").setContent( safe_dump({ "introducers": { petname: { diff --git a/src/allmydata/scripts/create_node.py b/src/allmydata/scripts/create_node.py index 30024dfd9..a4b2213ed 100644 --- a/src/allmydata/scripts/create_node.py +++ b/src/allmydata/scripts/create_node.py @@ -5,6 +5,9 @@ import json from twisted.internet import reactor, defer from twisted.python.usage import UsageError +from twisted.python.filepath import ( + FilePath, +) from allmydata.scripts.common import ( BasedirOptions, @@ -308,7 +311,7 @@ def write_client_config(c, config): introducer = config.get("introducer", None) if introducer is not None: write_introducer( - config["basedir"], + FilePath(config["basedir"]), "default", introducer, ) diff --git a/src/allmydata/test/check_memory.py b/src/allmydata/test/check_memory.py index 3ab88b9c6..6ec90eeae 100644 --- a/src/allmydata/test/check_memory.py +++ b/src/allmydata/test/check_memory.py @@ -8,6 +8,9 @@ if PY2: from future.builtins import str # noqa: F401 from six.moves import cStringIO as StringIO +from twisted.python.filepath import ( + FilePath, +) from twisted.internet import defer, reactor, protocol, error from twisted.application import service, internet from twisted.web import client as tw_client @@ -184,15 +187,17 @@ class SystemFramework(pollmixin.PollMixin): self.introducer_furl = self.introducer.introducer_url def make_nodes(self): + root = FilePath(self.testdir) self.nodes = [] for i in range(self.numnodes): - nodedir = os.path.join(self.testdir, "node%d" % i) - os.makedirs(nodedir + b"/private") + nodedir = root.child("node%d" % (i,)) + private = nodedir.child("private") + private.makedirs() write_introducer(nodedir, "default", self.introducer_url) - f = open(os.path.join(nodedir, "tahoe.cfg"), "w") - f.write("[client]\n" - "shares.happy = 1\n" - "[storage]\n" + config = ( + "[client]\n" + "shares.happy = 1\n" + "[storage]\n" ) # the only tests for which we want the internal nodes to actually # retain shares are the ones where somebody's going to download @@ -204,13 +209,13 @@ class SystemFramework(pollmixin.PollMixin): # for these tests, we tell the storage servers to pretend to # accept shares, but really just throw them out, since we're # only testing upload and not download. - f.write("debug_discard = true\n") + config += "debug_discard = true\n" if self.mode in ("receive",): # for this mode, the client-under-test gets all the shares, # so our internal nodes can refuse requests - f.write("readonly = true\n") - f.close() - c = client.Client(basedir=nodedir) + config += "readonly = true\n" + nodedir.child("tahoe.cfg").setContent(config) + c = client.Client(basedir=nodedir.path) c.setServiceParent(self) self.nodes.append(c) # the peers will start running, eventually they will connect to each diff --git a/src/allmydata/test/common.py b/src/allmydata/test/common.py index cbe4d768c..9691877fd 100644 --- a/src/allmydata/test/common.py +++ b/src/allmydata/test/common.py @@ -251,7 +251,7 @@ class UseNode(object): ) write_introducer( - self.basedir.asBytesMode().path, + self.basedir, "default", self.introducer_furl, ) diff --git a/src/allmydata/test/test_client.py b/src/allmydata/test/test_client.py index e082a33d1..080716507 100644 --- a/src/allmydata/test/test_client.py +++ b/src/allmydata/test/test_client.py @@ -667,11 +667,11 @@ class AnonymousStorage(SyncTestCase): """ If anonymous storage access is enabled then the client announces it. """ - basedir = self.id() - os.makedirs(basedir + b"/private") + basedir = FilePath(self.id()) + basedir.child("private").makedirs() write_introducer(basedir, "someintroducer", SOME_FURL) config = client.config_from_string( - basedir, + basedir.path, "tub.port", BASECONFIG + ( "[storage]\n" @@ -687,7 +687,7 @@ class AnonymousStorage(SyncTestCase): get_published_announcements(node), MatchesListwise([ matches_storage_announcement( - basedir, + basedir.path, anonymous=True, ), ]), @@ -699,11 +699,11 @@ class AnonymousStorage(SyncTestCase): If anonymous storage access is disabled then the client does not announce it nor does it write a fURL for it to beneath the node directory. """ - basedir = self.id() - os.makedirs(basedir + b"/private") + basedir = FilePath(self.id()) + basedir.child("private").makedirs() write_introducer(basedir, "someintroducer", SOME_FURL) config = client.config_from_string( - basedir, + basedir.path, "tub.port", BASECONFIG + ( "[storage]\n" @@ -719,7 +719,7 @@ class AnonymousStorage(SyncTestCase): get_published_announcements(node), MatchesListwise([ matches_storage_announcement( - basedir, + basedir.path, anonymous=False, ), ]), @@ -737,10 +737,10 @@ class AnonymousStorage(SyncTestCase): possible to reach the anonymous storage server via the originally published fURL. """ - basedir = self.id() - os.makedirs(basedir + b"/private") + basedir = FilePath(self.id()) + basedir.child("private").makedirs() enabled_config = client.config_from_string( - basedir, + basedir.path, "tub.port", BASECONFIG + ( "[storage]\n" @@ -764,7 +764,7 @@ class AnonymousStorage(SyncTestCase): ) disabled_config = client.config_from_string( - basedir, + basedir.path, "tub.port", BASECONFIG + ( "[storage]\n" @@ -956,22 +956,24 @@ class Run(unittest.TestCase, testutil.StallMixin): A configuration consisting only of an introducer can be turned into a client node. """ - basedir = "test_client.Run.test_loadable" - os.makedirs(basedir + b"/private") + basedir = FilePath("test_client.Run.test_loadable") + private = basedir.child("private") + private.makedirs() dummy = "pb://wl74cyahejagspqgy4x5ukrvfnevlknt@127.0.0.1:58889/bogus" write_introducer(basedir, "someintroducer", dummy) - fileutil.write(os.path.join(basedir, "tahoe.cfg"), BASECONFIG) - fileutil.write(os.path.join(basedir, client._Client.EXIT_TRIGGER_FILE), "") - yield client.create_client(basedir) + basedir.child("tahoe.cfg").setContent(BASECONFIG) + basedir.child(client._Client.EXIT_TRIGGER_FILE).touch() + yield client.create_client(basedir.path) @defer.inlineCallbacks def test_reloadable(self): - basedir = "test_client.Run.test_reloadable" - os.makedirs(basedir + b"/private") + basedir = FilePath("test_client.Run.test_reloadable") + private = basedir.child("private") + private.makedirs() dummy = "pb://wl74cyahejagspqgy4x5ukrvfnevlknt@127.0.0.1:58889/bogus" write_introducer(basedir, "someintroducer", dummy) - fileutil.write(os.path.join(basedir, "tahoe.cfg"), BASECONFIG) - c1 = yield client.create_client(basedir) + basedir.child("tahoe.cfg").setContent(BASECONFIG) + c1 = yield client.create_client(basedir.path) c1.setServiceParent(self.sparent) # delay to let the service start up completely. I'm not entirely sure @@ -993,7 +995,7 @@ class Run(unittest.TestCase, testutil.StallMixin): # also change _check_exit_trigger to use it instead of a raw # reactor.stop, also instrument the shutdown event in an # attribute that we can check.) - c2 = yield client.create_client(basedir) + c2 = yield client.create_client(basedir.path) c2.setServiceParent(self.sparent) yield c2.disownServiceParent() @@ -1132,8 +1134,8 @@ class StorageAnnouncementTests(SyncTestCase): """ def setUp(self): super(StorageAnnouncementTests, self).setUp() - self.basedir = self.useFixture(TempDir()).path - create_node_dir(self.basedir, u"") + self.basedir = FilePath(self.useFixture(TempDir()).path) + create_node_dir(self.basedir.path, u"") # Write an introducer configuration or we can't observer # announcements. write_introducer(self.basedir, "someintroducer", SOME_FURL) @@ -1164,7 +1166,7 @@ enabled = {storage_enabled} No storage announcement is published if storage is not enabled. """ config = client.config_from_string( - self.basedir, + self.basedir.path, "tub.port", self.get_config(storage_enabled=False), ) @@ -1186,7 +1188,7 @@ enabled = {storage_enabled} storage is enabled. """ config = client.config_from_string( - self.basedir, + self.basedir.path, "tub.port", self.get_config(storage_enabled=True), ) @@ -1203,7 +1205,7 @@ enabled = {storage_enabled} # Match the following list (of one element) ... MatchesListwise([ # The only element in the list ... - matches_storage_announcement(self.basedir), + matches_storage_announcement(self.basedir.path), ]), )), ) @@ -1218,7 +1220,7 @@ enabled = {storage_enabled} value = u"thing" config = client.config_from_string( - self.basedir, + self.basedir.path, "tub.port", self.get_config( storage_enabled=True, @@ -1238,7 +1240,7 @@ enabled = {storage_enabled} get_published_announcements, MatchesListwise([ matches_storage_announcement( - self.basedir, + self.basedir.path, options=[ matches_dummy_announcement( u"tahoe-lafs-dummy-v1", @@ -1259,7 +1261,7 @@ enabled = {storage_enabled} self.useFixture(UseTestPlugins()) config = client.config_from_string( - self.basedir, + self.basedir.path, "tub.port", self.get_config( storage_enabled=True, @@ -1281,7 +1283,7 @@ enabled = {storage_enabled} get_published_announcements, MatchesListwise([ matches_storage_announcement( - self.basedir, + self.basedir.path, options=[ matches_dummy_announcement( u"tahoe-lafs-dummy-v1", @@ -1307,7 +1309,7 @@ enabled = {storage_enabled} self.useFixture(UseTestPlugins()) config = client.config_from_string( - self.basedir, + self.basedir.path, "tub.port", self.get_config( storage_enabled=True, @@ -1343,7 +1345,7 @@ enabled = {storage_enabled} self.useFixture(UseTestPlugins()) config = client.config_from_string( - self.basedir, + self.basedir.path, "tub.port", self.get_config( storage_enabled=True, @@ -1359,7 +1361,7 @@ enabled = {storage_enabled} get_published_announcements, MatchesListwise([ matches_storage_announcement( - self.basedir, + self.basedir.path, options=[ matches_dummy_announcement( u"tahoe-lafs-dummy-v1", @@ -1381,7 +1383,7 @@ enabled = {storage_enabled} self.useFixture(UseTestPlugins()) config = client.config_from_string( - self.basedir, + self.basedir.path, "tub.port", self.get_config( storage_enabled=True, @@ -1408,7 +1410,7 @@ enabled = {storage_enabled} available on the system. """ config = client.config_from_string( - self.basedir, + self.basedir.path, "tub.port", self.get_config( storage_enabled=True, diff --git a/src/allmydata/test/test_introducer.py b/src/allmydata/test/test_introducer.py index f6497eeb4..1fb456e72 100644 --- a/src/allmydata/test/test_introducer.py +++ b/src/allmydata/test/test_introducer.py @@ -795,21 +795,24 @@ class Announcements(AsyncTestCase): Announcements received by an introducer client are written to that introducer client's cache file. """ - basedir = "introducer/ClientSeqnums/test_client_cache_1" - fileutil.make_dirs(basedir + b"/private") + basedir = FilePath("introducer/ClientSeqnums/test_client_cache_1") + private = basedir.child("private") + private.makedirs() write_introducer(basedir, "default", "nope") - cache_filepath = FilePath(os.path.join(basedir, "private", - "introducer_default_cache.yaml")) + cache_filepath = basedir.descendant([ + "private", + "introducer_default_cache.yaml", + ]) # if storage is enabled, the Client will publish its storage server # during startup (although the announcement will wait in a queue # until the introducer connection is established). To avoid getting # confused by this, disable storage. - with open(os.path.join(basedir, "tahoe.cfg"), "w") as f: + with basedir.child("tahoe.cfg").open("w") as f: f.write("[storage]\n") f.write("enabled = false\n") - c = yield create_client(basedir) + c = yield create_client(basedir.path) ic = c.introducer_clients[0] private_key, public_key = ed25519.create_signing_keypair() public_key_str = remove_prefix(ed25519.string_from_verifying_key(public_key), "pub-") @@ -875,7 +878,7 @@ class Announcements(AsyncTestCase): self.failUnlessEqual(announcements[public_key_str2]["anonymous-storage-FURL"], furl3) - c2 = yield create_client(basedir) + c2 = yield create_client(basedir.path) c2.introducer_clients[0]._load_announcements() yield flushEventualQueue() self.assertEqual(c2.storage_broker.get_all_serverids(), @@ -885,26 +888,24 @@ class ClientSeqnums(AsyncBrokenTestCase): @defer.inlineCallbacks def test_client(self): - basedir = "introducer/ClientSeqnums/test_client" - fileutil.make_dirs(basedir + b"/private") + basedir = FilePath("introducer/ClientSeqnums/test_client") + private = basedir.child("private") + private.makedirs() write_introducer(basedir, "default", "nope") # if storage is enabled, the Client will publish its storage server # during startup (although the announcement will wait in a queue # until the introducer connection is established). To avoid getting # confused by this, disable storage. - f = open(os.path.join(basedir, "tahoe.cfg"), "w") - f.write("[storage]\n") - f.write("enabled = false\n") - f.close() + with basedir.child("tahoe.cfg").open("w") as f: + f.write("[storage]\n") + f.write("enabled = false\n") - c = yield create_client(basedir) + c = yield create_client(basedir.path) ic = c.introducer_clients[0] outbound = ic._outbound_announcements published = ic._published_announcements def read_seqnum(): - f = open(os.path.join(basedir, "announcement-seqnum")) - seqnum = f.read().strip() - f.close() + seqnum = basedir.child("announcement-seqnum").getContent() return int(seqnum) ic.publish("sA", {"key": "value1"}, c._node_private_key) diff --git a/src/allmydata/test/test_system.py b/src/allmydata/test/test_system.py index 5a34dcc4d..7a7fe117b 100644 --- a/src/allmydata/test/test_system.py +++ b/src/allmydata/test/test_system.py @@ -33,6 +33,9 @@ from allmydata.mutable.publish import MutableData from foolscap.api import DeadReferenceError, fireEventually, flushEventualQueue from twisted.python.failure import Failure +from twisted.python.filepath import ( + FilePath, +) from .common import ( TEST_RSA_KEY_SIZE, @@ -903,21 +906,21 @@ class SystemTestMixin(pollmixin.PollMixin, testutil.StallMixin): # usually this node is *not* parented to our self.sparent, so we can # shut it down separately from the rest, to exercise the # connection-lost code - basedir = self.getdir("client%d" % client_num) - if not os.path.isdir(basedir): - fileutil.make_dirs(basedir) + basedir = FilePath(self.getdir("client%d" % client_num)) + basedir.makedirs() config = "[client]\n" if helper_furl: config += "helper.furl = %s\n" % helper_furl - fileutil.write(os.path.join(basedir, 'tahoe.cfg'), config) - os.makedirs(basedir + b"/private") + basedir.child("tahoe.cfg").setContent(config) + private = basedir.child("private") + private.makedirs() write_introducer( basedir, "default", self.introducer_furl, ) - c = yield client.create_client(basedir) + c = yield client.create_client(basedir.path) self.clients.append(c) c.set_default_mutable_keysize(TEST_RSA_KEY_SIZE) self.numclients += 1 From 84088e4f4182f0fdb52b995f7db8f7d8b90ea06b Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Nov 2020 21:18:58 -0500 Subject: [PATCH 77/91] unused import --- src/allmydata/scripts/common.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/allmydata/scripts/common.py b/src/allmydata/scripts/common.py index f38a241dd..b20cca65f 100644 --- a/src/allmydata/scripts/common.py +++ b/src/allmydata/scripts/common.py @@ -14,9 +14,6 @@ if PY2: from future.builtins import str # noqa: F401 from twisted.python import usage -from twisted.python.filepath import FilePath - - from allmydata.util.assertutil import precondition from allmydata.util.encodingutil import unicode_to_url, quote_output, \ From ae5351c2040724b2e523b2706b65000c9653d284 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Nov 2020 21:46:57 -0500 Subject: [PATCH 78/91] Adapt test_tor to write_introducer change --- integration/test_tor.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/integration/test_tor.py b/integration/test_tor.py index 8dd05fc3f..af657f0ad 100644 --- a/integration/test_tor.py +++ b/integration/test_tor.py @@ -9,6 +9,10 @@ import pytest_twisted import util +from twisted.python.filepath import ( + FilePath, +) + from allmydata.test.common import ( write_introducer, ) @@ -70,12 +74,12 @@ def test_onion_service_storage(reactor, request, temp_dir, flog_gatherer, tor_ne @pytest_twisted.inlineCallbacks def _create_anonymous_node(reactor, name, control_port, request, temp_dir, flog_gatherer, tor_network, introducer_furl): - node_dir = join(temp_dir, name) + node_dir = FilePath(temp_dir).child(name) web_port = "tcp:{}:interface=localhost".format(control_port + 2000) if True: - print("creating", node_dir) - mkdir(node_dir) + print("creating", node_dir.path) + node_dir.makedirs() proto = util._DumpOutputProtocol(None) reactor.spawnProcess( proto, @@ -88,7 +92,7 @@ def _create_anonymous_node(reactor, name, control_port, request, temp_dir, flog_ '--hide-ip', '--tor-control-port', 'tcp:localhost:{}'.format(control_port), '--listen', 'tor', - node_dir, + node_dir.path, ) ) yield proto.done @@ -96,7 +100,7 @@ def _create_anonymous_node(reactor, name, control_port, request, temp_dir, flog_ # Which services should this client connect to? write_introducer(node_dir, "default", introducer_furl) - with open(join(node_dir, 'tahoe.cfg'), 'w') as f: + with node_dir.child('tahoe.cfg').open('w') as f: f.write(''' [node] nickname = %(name)s @@ -125,5 +129,5 @@ shares.total = 2 }) print("running") - yield util._run_node(reactor, node_dir, request, None) + yield util._run_node(reactor, node_dir.path, request, None) print("okay, launched") From 4c8fb8d93aa5715856c47bc1d57155b4117b87ad Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Nov 2020 21:48:06 -0500 Subject: [PATCH 79/91] unused import --- integration/test_tor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/integration/test_tor.py b/integration/test_tor.py index af657f0ad..dcbfb1151 100644 --- a/integration/test_tor.py +++ b/integration/test_tor.py @@ -1,7 +1,6 @@ from __future__ import print_function import sys -from os import mkdir from os.path import join import pytest From 263ada9be4b0c8e7f98109ab679251733ad88c9f Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Fri, 27 Nov 2020 16:24:16 -0500 Subject: [PATCH 80/91] Get rid of the spurious quotes in the flake8 command I don't understand tox.ini syntax or quoting rules and I don't see any documentation about it. But what could go wrong with trial and error? --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index b95476f58..5b9a146fe 100644 --- a/tox.ini +++ b/tox.ini @@ -98,9 +98,9 @@ setenv = # If no positional arguments are given, try to run the checks on the # entire codebase, including various pieces of supporting code. - DEFAULT_FILES="src integration static misc setup.py" + DEFAULT_FILES=src integration static misc setup.py commands = - flake8 {posargs:{env:DEFAULT_FILES}} + flake8 -v {posargs:{env:DEFAULT_FILES}} python misc/coding_tools/check-umids.py {posargs:{env:DEFAULT_FILES}} python misc/coding_tools/check-debugging.py {posargs:{env:DEFAULT_FILES}} python misc/coding_tools/find-trailing-spaces.py -r {posargs:{env:DEFAULT_FILES}} From e6a09fa4441a3d354cc0d806516e472b64917489 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Fri, 27 Nov 2020 16:28:23 -0500 Subject: [PATCH 81/91] Don't check check-debugging.py --- misc/coding_tools/check-debugging.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/misc/coding_tools/check-debugging.py b/misc/coding_tools/check-debugging.py index 17eeb30b7..f2ba6528e 100755 --- a/misc/coding_tools/check-debugging.py +++ b/misc/coding_tools/check-debugging.py @@ -11,8 +11,12 @@ umids = {} for starting_point in sys.argv[1:]: for root, dirs, files in os.walk(starting_point): - for fn in [f for f in files if f.endswith(".py")]: - fn = os.path.join(root, fn) + for f in files: + if not f.endswith(".py"): + continue + if f == "check-debugging.py": + continue + fn = os.path.join(root, f) for lineno,line in enumerate(open(fn, "r").readlines()): lineno = lineno+1 mo = re.search(r"\.setDebugging\(True\)", line) From 9f7ae56a82831bd705dd38acc8e0da910aecb4eb Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Mon, 30 Nov 2020 16:24:27 -0500 Subject: [PATCH 82/91] Make the explanation less nonsensical. --- src/allmydata/introducer/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/allmydata/introducer/client.py b/src/allmydata/introducer/client.py index bfbca7b32..5b75a7ab1 100644 --- a/src/allmydata/introducer/client.py +++ b/src/allmydata/introducer/client.py @@ -117,8 +117,9 @@ class IntroducerClient(service.Service, Referenceable): announcements = [] for _, value in self._inbound_announcements.items(): ann, key_s, time_stamp = value - # On Python 2, bytes are stored as Unicode. To minimize changes, Python - # 3 for now ensures the same is true. + # On Python 2, bytes strings are encoded into YAML Unicode strings. + # On Python 3, bytes are encoded as YAML bytes. To minimize + # changes, Python 3 for now ensures the same is true. server_params = { "ann" : ann, "key_s" : ensure_text(key_s), From 413cf75d54a80457de8988944ce28d3cf92528b1 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Mon, 30 Nov 2020 16:25:24 -0500 Subject: [PATCH 83/91] Uses clearer issuperset(). --- src/allmydata/introducer/server.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/allmydata/introducer/server.py b/src/allmydata/introducer/server.py index 84c940d81..e41bff14b 100644 --- a/src/allmydata/introducer/server.py +++ b/src/allmydata/introducer/server.py @@ -308,8 +308,10 @@ class IntroducerService(service.MultiService, Referenceable): subscriber.notifyOnDisconnect(_remove) # now tell them about any announcements they're interested in - assert {type(service_name)} >= set(type(k[0]) for k in self._announcements), ( - service_name, self._announcements.keys()) + assert {type(service_name)}.issuperset( + set(type(k[0]) for k in self._announcements)), ( + service_name, self._announcements.keys() + ) announcements = set( [ ann_t for idx,(ann_t,canary,ann,when) in self._announcements.items() From eaca639b6f49212cc917ce61d4ed205a1cd28bb2 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Mon, 30 Nov 2020 16:28:26 -0500 Subject: [PATCH 84/91] Undo changes that should probably be in a different branch. --- src/allmydata/test/common.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/allmydata/test/common.py b/src/allmydata/test/common.py index c45b0c4e9..1cf1d6428 100644 --- a/src/allmydata/test/common.py +++ b/src/allmydata/test/common.py @@ -1190,9 +1190,7 @@ class AsyncTestCase(_TestCaseMixin, TestCase): only fire if the global reactor is running. """ run_tests_with = EliotLoggedRunTest.make_factory( - AsynchronousDeferredRunTest.make_factory( - timeout=60.0, suppress_twisted_logging=False, - store_twisted_logs=False), + AsynchronousDeferredRunTest.make_factory(timeout=60.0), ) @@ -1206,9 +1204,7 @@ class AsyncBrokenTestCase(_TestCaseMixin, TestCase): pass with ``AsyncTestCase``. """ run_tests_with = EliotLoggedRunTest.make_factory( - AsynchronousDeferredRunTestForBrokenTwisted.make_factory( - timeout=60.0, suppress_twisted_logging=False, - store_twisted_logs=False), + AsynchronousDeferredRunTestForBrokenTwisted.make_factory(timeout=60.0), ) From 8615c1ade8af0882c0b206156388097f6eb84907 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Mon, 30 Nov 2020 16:45:14 -0500 Subject: [PATCH 85/91] Try to fix sorting on Python 3. --- src/allmydata/mutable/publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/mutable/publish.py b/src/allmydata/mutable/publish.py index 17b73685e..8a760c5d3 100644 --- a/src/allmydata/mutable/publish.py +++ b/src/allmydata/mutable/publish.py @@ -914,7 +914,7 @@ class Publish(object): def log_goal(self, goal, message=""): logmsg = [message] - for (shnum, server) in sorted([(s,p) for (p,s) in goal]): + for (shnum, server) in sorted([(s,p) for (p,s) in goal], key=lambda t: (id(t[0]), id(t[1]))): logmsg.append("sh%d to [%s]" % (shnum, server.get_name())) self.log("current goal: %s" % (", ".join(logmsg)), level=log.NOISY) self.log("we are planning to push new seqnum=#%d" % self._new_seqnum, From ff49414ae9c3c58345b8e04c04372ef6012c78e3 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 30 Nov 2020 17:20:23 -0500 Subject: [PATCH 86/91] Use Tor project's new repository signing key Fix for https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3542 --- integration/install-tor.sh | 1440 +++++++++++++++++++----------------- newsfragments/3542.minor | 0 2 files changed, 757 insertions(+), 683 deletions(-) create mode 100644 newsfragments/3542.minor diff --git a/integration/install-tor.sh b/integration/install-tor.sh index 2478389f8..66fa64cb1 100755 --- a/integration/install-tor.sh +++ b/integration/install-tor.sh @@ -25,694 +25,768 @@ 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 +PVg3cta5Ctmn/mM2ehO3Y+XCEtenTZJP8ZtHg3pZEt4PtQaOBtrWxqX1h633KEIa +0a7dASaU4KOZg/SyKoChcSr2pY+jtzDacsZ8q/et+zz2gktdvcDSkJurkPjlORx9 +CcWFhOd7PFP4ZWn0A0AkufMpbLXhlVJCmSykyyG0Don3C9i7sG045303KNy6CA+l +jvcm/EBeeMWvLMdjr51XmkGFjaAs4Lyw0CfKj9uNZdriOtSVtH2kcMmNSvcUln2B +FZTBo2NeRKGJAhwEEAECAAYFAktpE+EACgkQxel8K2OfamZhpg/+P9NPk88rqRnE +uDVDHodlkA5hG0d0Yi5vkV9rw07yjYut474aUd3FjJFqNEoiW+6dFbNy6YqqYPhr +XLtnfJl5LAUJUzMA2aSLtbuX+cq18DCv5ZmU4DW6kZOWi5vX7QkQCTTLP03VlcD3 +Gu6HyofseBMgE4zoEXdmZSZmPnOygakFLzC9w+D1XfK2gcaTKjAJJdW80aY56eUe +zFDKLhOw+YzIK1/ZeeOTS4LeITtTq5J6/hnwHrJdjApX80v2WJzVVoy7lQbxAPsl +JHZdYVFCBy2Tyk7kYdddVxYCcdYr0e8A+GfG/tQJGxvZ3O4nOrezSv0XmlhLZ5rj +Cn8M6fg/NKUXsPtXiac+DQJbr5RwQ5Sc7bnPVsCywqetOeA+xv3L2wi94rg4u97Q +iwqhDW0SE9zZuQL5vaXl/GFpaRXs+mVGATS9h+0lDBQPi21oPkdN/BKKzr//2GCl +5VFb+rkOY65HthCuiIrT8jFGArJIF4nXku/4BPpNrganC89iTsd5+UUNFIlta+WY +kENQ9tC2mwj96BaK0KyRQZP9AAzTo5wG8aouczptpwSH0aECJNy8kd/UR8IAkZkx +jY4+zyfQDlb4aNDsVGvempgjFcNo0rciKrPQl5GyRLQj2azuv46gaGcYzqsobejS +/2jqJLMnkTeExaCryrWuXo/raWBWQLOJAhwEEAECAAYFAkybgq4ACgkQ2HRyfjOa +f6huKQ//Yfey5BJXqZqIt9i6tyw2VqzMtZ1gAqFdEKeuSmz30xty9g6KknIjpeZo ++POb3rQFUKGZ/q4AjWKdD9C5WUvLcXd0RCWeDG7dmD78h35OWwqhc+8FXO1vU0nG +yFdEx89cNiO42M/z+eYeoysgVL3ixbCjJlrN4MHrilqshxH5MvG7JfIfoPwucQyt +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 +EWaMEjPPQbNJ1KrFzAFfIUeFTbBL5kMmJK5aYVUiHWnLZq0SK5OlWGqBihuRLI7O +IoBOjlcoXvFoEgSkgUKpapE6C9VkErW60WCK91sMhaa8CY9pVDPaanMG2o73BfS3 +jGPylm4H2+8jlJ1+l5ietvoyiqOST1iIfOsbi30mxuVJ4JBvKtmapqpBwT6eNvCi +PKsMyjB5oWI5IVbK8MDIaYQM9TL+nyMGhl19GzcUMP8tZRlCifM9b/zmMMt1sgVY +0koF8AZfh3Ho9KLyXqNMUtXAFSQrAcTbN5SmzjlJtl+hz6uhiHH9kAeSX4MFRXX6 +JDfZxyAw72JqJkZaPEAKQCpodkNwNG9b2dedIBsTaD9IoEkryDtR17qV2ePwlCey +muwNnGVVaJ8hLbI7ZATbIaSn7XNvMGM8hX0N/ram5nTvrR2laG1o1ss5oxtg7PfT +rhMyCTrzTcxc8VskAgtbJjoyi4kCHAQQAQIABgUCUHsxcgAKCRBTnENrguQL99ul +EACwc68fdeLrP5U/oRJ38+WkQxO08Doa7dspj1cy763kXGlzp27xvXlc9xWMEkLZ +eFG6az+sI3YvW6Lq1PZYzQRLsjgMKDNDMDMB5xR3g65lRW4KKd0pCs5/CRxs2hDP +k6ulih2E12N/yqwgXTstbEDzXmaBe/66uGdtAnK0GinXdE+5wBMu0y0CFxJygkCm +to/hOeYufOHSLIcVVrzbsLH3meuPX9hUxjN4RIOduwWeHi0IWUCx5IyGPtMNBAfX ++AIVYmX7rWpEH+bqlfaEZYJh5kLDebzCq8gtRteRzvRz0jCX/Zr7SZu1PskjRN+e +MLdkNgsF9TXQy2vFFM5aRoEenHeD5gMla8pBFnbZP+AA4nPrbuKUyVcfdrLIIgQE +KIvxjKetSj3MV1PTuH5YNBw2I3qxrgmrv+YlClmiy/VQXt3///////////////// //////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////// -////////////////////////////////////iQIcBBABAgAGBQJRcGlBAAoJELlv -IwCtEcvuoWwP/ReLzhFKWlc/F35MvNyO1usz+qvs+SrlAtwaNcv3Dd9ih0mw+bH+ -U+PVVgXlk1g0NY9hNNRLxt2mUc+mg9ttN+ha0RkqUYsYjg1Wj9bDuR0a+3DhtuS9 -hhEjWrBBT3UbTcWT5lxKkUgy4Sj+Dh0N78spHo2orUN3qRw3VkHY4hWcxAvlXreu -Ev6J7Ik4uZ+8MMgJFld4oVhMmnWOrMwt10D58URvZsGypI+dK0p2JSue5yfBWkSM -pFsJ8z2cCOBMAPQq9S63mhXZiORrxJS4pzJ87wcYG/H3R1pqF6I/49tWBlyZwiwO -Ys0fFEJc9idF/hSzen/qDDQpvy4gNF48if7SGEtOBu1vEGqWKvNsataNcjYgj4BZ -hDlMHgAxWn0G7VNRVsx1D6nzOzEAlFa/PQgQfCXScJXRV72uKoMk2uuOk8yb2+to -OW5LoS/0UbsnUi77VvknpZPbQPQ5svsGBCU1BQpDeFsQk4IMW5Flv1VVSEtxnfLi -89An4HPMN92+qNUDRM3E/eLkFnrPdiB3yMkjAgDbao5Gh+CTszQ118xkhmRC+pNC -I75AS/X4V1WrcAJUniTbFgBRZr4t2tWfLMgx44XMtVrKraROj7QH4rEODSInBBEW -T2hiJeWm4QS1g5Rfoym4ur02xxqhwXAsCXFGFKZirXDoTMHDds6dI0QXiQIcBBAB -AgAGBQJR+DzBAAoJECIs6MQ2RAKIjU4QAIi24KlFH1hL0d45GsQswFJ3YiokF62j -pXRU2x7/+D+cJUqA4omjaGkSn0Go+J2MG8/bQST/Eioev8/PtHpPVRWyOq1ACUue -DFpvzXAmxEBA25OkdDRWiy2y2CUSwu2n/OJBg6+C3TIRyoqzs2YiXIDr9TDi7NcX -UP2Gd+xDWyEh5zd3xilAZl/SNkW73gen2GnG0WRMjzvJ9SSqYVFGw2L0oaSyX+HI -3ulAybWuYaHtwREcgcKJpRK7VMeICERRzmGQxaUzbBtsWf1lhVUaCjINbKEOOfuq -EqcRGsXl3AJw/qYUaj3CE7hTiUpQ0kcDw7G0NvuYOFqBjTAZVOpr45vbOqCqKp4u -pNh2KLsGcGqzBy+RubsEsbOmIuDImyjFLpGHOZv54mJNLQ+SDbbLcj96EPZ5+gg7 -ip7+e6gGqGhJEOQLWeXejTk2rAX5zgkHutmjqY7qZIe63iXnlq88B66tZct2dYwv -3M9t2X2Mkx3UR1UxQZ8wJjmSYSS49HDfIZh5NIz58QH8AePltBk32yMxSFq0lndG -KEyhE2omMyNYzSt0EcXcsaaiqrphQ9iPJ8fCY29MOkKRQz9S3P/NOZFQrqL3zavf -JX2+npx0umP97xPIowMPn0QZEWkTf2rvcG3s8s5jfUNOsi+ZcPazhjwqV6tX2Ovb -fK49CG3vTdcAiQIcBBABAgAGBQJTChVdAAoJEA7aqUXopP+Xnn8QAITUo4Kkapoc -g1wurgpYjetGyz9pI6PwtV27Q9xWWjLWRnZhlsHhSo1JhvNY9QBIKb2QQU+WGoBR -tWPIxm0AtbhmGBlKscRRMYfKB0U2pFE6HGDh7tWPzSzPWHKb3oobyB2bmgtBNsWG -BxgcoQESC18uZnYJ2ffk6N5BhU7JnN5PD6TeKFokengr+BVkxwB0sVP5Zahc7lXN -nj7mDTeths0ZyxDTzog9AnImKlJR7Qu/uhhhz2mYnobS9tgzvyqRtibmxd7RLwlG -Owf6/jUA3wmYgvN1B17reB1GwylK9eRIem1OPG0t2UV/i6ik9BFMrwruoeTDd1PO -nA7+SDLXC2tUbGwBK4PEKbD/IMe43dKQypaAYXQgWqwl3Lf3t9eCAfW0X/PRUdcS -xA8WWpY6pFqT6Eg1GicSnwarhvSWcs9I3FNo7foBcu3S6+wO6jK6/izOOymznuzG -putQnVCZuVIL3FF+QC95popTUjnTBRF6O1p2o5OfEOAJ2f76c3a/tZuPB5Z0Wfiv -uQXzDDsEC7O7SRkYMvCI8Vs/H6fijsOnKYtSvHRbm52R0zFWHPFCK36/tFaoUVrc -PNbGyFYT2klL177G+e6mJubz8nzPMLlWzP5utUYY8gM4UIaGr/mPudKmT2jFp/25 -oFKpxaiBrROsXVv9+NQ01QYtxT5BueyoiQIcBBABAgAGBQJTgEpqAAoJEPEtmFdD -7iYgp/AP/jMbTr4b6Vqyrbaq/SBAfN2SpcmqOWmQn5tiBRvG/FgV68v55dugKMia -B1Opw3zGgF3l8xzdjFduMFH1iZBgv7kaooguZ7ttV84g6xGVE/9XjOughX4KchIR -rpS8Qqba7Sr46vYrNmGhP6YMh9CmOG5ydg0H6MwXmiC9osKY/G71OyP8/K07ziE+ -iImS+oOsCrnnwkxBEDLkwo3engbJMoCNLK+qpSuatwydtI6Gy7LaZOIBjEuf8I3m -D4wkKU1SE6f6w83pEMzDq6xC2Y6hLCz1kooamJA08WJ01u1npIrje873yY6Qw0Wf -E5Jsp0WR4gdhbScL4S4m2Q9ZLW/2jWFbwz6tNHAfo3//nWZn2II6GDJfgfX7dU9W -6pid+p52bAAughh6hDjbH/eVF9BaWommIVtjjHAkpHWJ8V6vOKpojG+lyrrzi3Ye -Tw44s14BsfxRx/8TcSKzM+1jXNpeT27pIHohjvlRVdJtrw5MYUOqTGpVtH6GhoJS -m62sM196CbB7RkpEH0TojOenzQhsV+e+W4FVrb2QEQQqWB3TkmGLpiRt98FPltZO -7fHJmMSaqe+WOxTxrciP0FoyZJxYQM+NujQUTlMxmAdSw5AHAwhVIHmtreTEZKHp -lx984hSZiiKzdsTUr7/AcndDcdY9KN2/p3Zf11y6nPlYsv4ToDXTiQIcBBABAgAG -BQJTwUz5AAoJEHhUBU7O/hnkcfcP/3+Vv2CLClRCfpgPTLjoG31P425RxTLmx0Hg -H+ULaQI8D1Ymrx6j+UUW5mkFNtx1HkF8ebezH+wi4PrROJ5UNdJ0pW4cgMZCHlPU -2uh1d/THKQnVOaBlqsDIyOAZem1sUtJfkYsOZTnUbbyS4CSYkf9HTfPfQ3TWePS0 -gEhj2zV4r4APMPKrfAfc761CWu33IY0SqYLwDWPQezd50pGJDJYnBWJixArgJQK7 -PCkD6hRslnXPW/Vj8VqBptiNO5yZGAKn8UPUg5LeMeXTU21Qh9KQbQHAGOWxbpUF -Vnon3pVPms7JbrBA5I4U9Q3emBoiFgV+ARLQFcBehTBDOB3NHN+1nKaA0ZG7Q7Hk -pTH1ophpbC9q67NvQvDgihlMXHwZRF+EwKBafo1IsgXwx8k/0Ju+6N+i8PlbnB98 -yTpxgo2rnj+9V5HkfqGGV33s9Aio959mNM6gOrTwVZvL/DzfyyOXvdUl9nIWM6su -azxO6wWRItj9vUqDgq/byJ6X5R9rXyCioKhwe6ztU283hgpNAY7KkvTDyPpK+W8r -u2sfY0IENUtT6w3pHGVVkoqGox5dkcO9fv+Ok0eGOLJnperFcvGdd4V/mGLKH+TS -Ksn3fJPs+9MH8XMKcp6NDbki3aff5vPDfj2+nhwLXG6WT9l9IPmAxeaw5QxmkNDW -v/2zU4RFiQIcBBABAgAGBQJT6qc3AAoJEDov2JR5p8TBfZEP/Rgw5Imwcis+iIVV -IW7r3DyW+A9+9JAI8muShU+Z1zliImIwanwNWn5RbcmY4sohQ9SDmnb8L7wMuCNY -tXR/neys4J2qn2pcHH/TIo629E0aVMRjLBU8Dok543ONx2BSAGuRPyXDciPzn54k -NVXpAW5NI+Z5SniEQRJ40o9YpPQHcjGXnNtUc7pB9r02JsIqk6WX1iSzAl3Ke14U -ySlWb4urmamvomlufVYOtGdxeniN1lgbrJY/BCb2b/ZRr0gupv3EEEp/uU2TLBVu -S7yGbNlf+9gZk8ZVl117HVALCnpQ11QxodQaGH8HuR/QEex4Y802DyzEsT1Fnzm8 -eZcqZB9QO8pjcrOTU3yCiB/7vwpeHymLkogrMirQaxQK6OUnEhYNOuj9Det7cJPw -zeinerHaCNlumxKbB6gm0w1R8tjqHHkzcjp9rEoH1UDf91ugHCxevhR11Cz4ZSwF -bkx6+sUr1HgK4fCktKlkFcP8adDSncz8N2btbLmWYfBSKZK1z6GStIquwTLyLFrm -xXSGrsiLaETXF0S0VI4IuCggggDOn43oxqoISSaMJTPr//vhbL387wwXbK271wC7 -WVAQmslrZhImTzLbxjTvL2L1/NCSRBJXKalRV9HCzZgGaq+7LFwz0PQeciSK+ijq -RrmQKIyxhDVjcXm4OU6LrIFzoAzniQIcBBABAgAGBQJUOeCYAAoJENFZiZ/T2fiy -0AcP/i7qIAAFw6wqYgojDsqA7/YifGh9RGvrxmC4dWdrgLxW7dorUh0uw/JLn0JR -rzKoS6EF3hHrPQasmCPyz9ckZeRZjIhR5mKYtqrWsF3vpaL2VALXsb54KqAR8l4/ -iT083JTm1mvEbMJ4JFVGNrGNVIWYdDgfQOKzD6lZtwRZTEjY5u+sJHS4VRvjAju8 -2vlmEx8hmrcDV2f+9St40pThNR9o1Rcna562NFldsccL7fFL9uM7kmFMGid5JwaR -U4b/iXiSZ6YctNQyfitkOWoHG0aKXvJM39WsJulHKCekSi4z4nNd5hZgMRFG2L4f -zgcm6wNEh081yhsVN4xHxURT1DrMg3Xtd3Zj4wBL5XFHySludRCd/PYPRpvfcCwe -JJ/OTroepfr3DGw/Qo2VnZKe+Hu+4KpZnB5NrYIz5mMcysJMDCXiA9YdwRlF7EsP -/ma8FdYpxyrR61+GRY+ANUP1KMqautJj9qW7HtIbqaZUFAuhmD6uetcCraWD7EF7 -meaFJuozenO9fBzBgcpJiJWKjNElJxpaPXiWPC+dQVvK0jpy12U1UNp38PBs18M9 -w2eOsC70tVhko53rCr1clL5Tdb133jNWo+jyWmKcYFKARziGQ3Q3GTE8ycRwebZe -SgIHYLzm9zHGZQc9crpC0Mfoa09vcbBNyt5NRT6s/nOE4tjTiQIcBBABAgAGBQJU -OiR+AAoJEJo0q5orsokP9bYQALApojYAycnlIEF8GVnt0fbzSYLwGBxWuMMmzdiH -3HHvTxsUBQ2KvcBRrvSC1C8gOhpYdouI+RSXPXb6pkBHWJPFmGaPp0RKqgLMDi+w -K7zZiPESMK8vaYJS9RmLS2KzJMn30QYQ0VZfrJiw+K5ejSgdoFz3pOpcJCNlBmNj -MocA6M+u6O1PDb/OOqSPRqSlzZzu1S5HyDaOK32XXZj20G7ltwn5abtdk9KO8KWR -2b3ZT6GMzxx3L83lBL6hcg7a6NrYQKsXdUP/HEvt6pnVBBKTk6LzjRNAPp79a2w4 -muT0rMAfHdGzRhe77828KTlTllQXFBEKH6m23daQAHEw1ydB/9H+rG+S0ulP1V2I -gza9agB9XASIgRKLjwkDdMzOehf0oKt0U6P6kpytS3M025t9yVA2qUuG6A5DWwwf -uRrY+dxUdbF5ZoQYEJuXDLk7vVuh2ggJcnfGZ3fIHjtCwvdlMRQzDX2adpzoNy7u -xZHz0QBbaSOeFhGs2xE1/lLfpRaWw+ISXdp7b9HjB6dI6bSfYUP940Mi72d62HUH -wbWNkaWe4afeCAWbTuHWCe4jnPvTBF0t6mU4k+lwWo0uYYjJM2W4V0OGSflIdB8s -gOxgmFzlB72TIwwVBeLrCUEvymHqbTUedY4jUSWrL15sfDjxAhGJxZiHe4ydqxz6 -p29jiQIcBBABAgAGBQJUZxhJAAoJEM2XTJo5TWM/ICcQAI5555kQLg4N5+fmq1rN -6AwjrI4lW13IjX3H462PMvVWCHgJV86o+5/ab747czA0xbszW8vr/K0iayA6hfwR -VtuEXfQl0Uh/Taj4+fhI8cfW2+5EX4+lpGrOclVCsHHVfVZ/k8LL7mbjboJhG9xj -SFf6JBtqr+/AFsEIA+MYGFBaiLgL68j3CJuDPvjkwpC/Ofov5FRdPEOWQ9odC0z2 -yvDjAqxOkmDjjZ04FJ5PBnNbo67AOUk25shqHuBzVH94MAP7Hrg7UaFeRQiMgNEl -f8qsTzukyzCFPxJ4yyFpb56dDil8wxsvGcJlOEfT8sAi3YT2J2QoT7KAcES+aYgP -1Q2Uj3gv0MkaLNcDtPsQGksbxXoipq/Wygj2UwOQh9ZVtycjuv0D2LyfPMVTPVG6 -6DYUpaverd5QB2nT2LZLxXhOH0tIqmFUaoIFrvrLn25A0Z5QFy/HbPfWAw7PJvgj -l6AoxT3nKvozt0tUdZ2DfE3h4zjBXDiv3Y014FmhgqwEOgnCn89SR/7SHMJsMKp8 -oyn1mfzsncs+gqcpOhvj2XfPEpnObDLqg98J6eyFGDfhEv1bNEOB4IcFF8YrUNEu -6/rS+l7rNH7vlw7hVWE0D1EnpZ8KYk8qPlOuHDHLMgemECUbR7Ogt63H3jqFfDAh -lKBUY3hG5S7lpWiLzcQccYZciQIcBBABAgAGBQJUa/DbAAoJEFyzYeVS+w0Q16QP -/10IdfE8aurLIfVMURxzr0CWHBwuAGV6mCKAriYRaEEjMWFThYsRtCS/CGtdc9Bx -XU5GwuHFcHFuBCP425I9kxmxh/Rc+w8A/ZZAVU5A4gaSB0hkM5oZdB2QwYmXrECE -Sdt0iHxcz9/zyB1R4q2KryzbbkJNJJzbOrGpxG6vh6Dk4B9rFJeRYc7lVfH3TqiO -HCljlHBdEw9iQDGl6IFuQxUqOJNJK75p+4/f0eK64W1jXI2bGekTAQ3V1mA9xv6P -+SR+NjPg4WQlx6sTyksaxbkzOcchyx8zzm1DNH9wm4NsoZKME4n0sCIB7CdY7oBS -FxJfyRp1JSPrUwdNIX8kSsdgJpM7ORgZkojfWWCqt6unlgRsZmurFYigzZFWBAGR -eHIeHJ54eULpg2QPKnwwWuwYHdEPp/bbuaLcPQcklPOGnnQynBpUvu3Ud/Fr7+4T -MHmOI/e5EUUyKbmK0pJLP36Lp3i28bHUTALF2mrDlx3+oMRjF5iSySC41KikBSBi -pRx0WO3jFzdS6NLVdjNlxG9lpiHCkc7bHz9edMvuAnahK/EbS6hFUEkWQOJtJKc8 -B8hXJmChM2YxtEDVv0GngAAwcHZAvphFeuy9vYf2S5IbIqKMNrKgq4VQ+jTqHHXI -57LkGHDCY2igDHQGo/StbI4s8Ow5btQMdXPnAO4rZ61FiQIcBBABAgAGBQJUcelG -AAoJEJjdu04iyiyDqF0QAJUdUtSUzHV3Vo36pbamTnCtyOqEp2X5L5wCjh+UAw9K -GeZu7Jiiz7ueQqxKQtz0miLnb2i3NeK9EWdoaKrM1+PIym7H40ATaurleKD9sq49 -b859tz5iy6DLh1YPeeeuQV/NbjJyh06SzNkMjke6S34CcpDa1OoczVsI1RufWVMu -q1C94+PZD0yCCVLjMUD53c0AldgsFXdd2oEU/JPd7P9wCYSKV/+9F+wRa7/U77HR -KNHd2FCshmJ1mbhk3BFHTALFn9ld1/mqtjUTArt14wxs5GxsPkr1YsWQ1A8uVUtn -W3rsn0UnP9bFcAfn3/d382HhuyW+HOV4g5JhKVlG/hBbvdL+HV17Y+YksGeQW1sK -Iqmjvr7ArFhCIUYo4+emyDEjQmTfuv8RRO1u4yR0iAZqlkk8/8z53ewE3HEfepwx -uo6el/uuRuXfQOmWfdNENjd9xn5gzIDbwqwvtZExjN2PolbiaSLP/3pI8prtrOYu -W+Mk5o6iucceazwdPyevOhoMuW5gZFffKo9w6TU5SRGcYIhrTJY8C7h2Yumsmir/ -XXpLaadcBp2R4uDEoHb6eGlXqvSYMED/mu1fw2VOuKosddCpf/JkwXHwwB39z+dX -o3HYSofyec1mb3kcAsOUbTkAh86IWN1ymqcNTyytK8AEwOLHQ1f4o0ml7n9Xzf1H -iQIcBBABAgAGBQJUsRPJAAoJEBe/lIwEdhN9Z5MP/3Oo8Oc767lRFi1Oj5FVoHvR -xfZvX3oKrG3jphPlCBgKWK8xR7c5YECNIwnlQ8uCqUgxpFf8/iPV3xVuO1HFwDna -fokTqyNtKz2XgpmyfteV/02e32hsDNGfaDCkqbUC2hkuDfWWZa/g0tWfSCryZaI6 -OkoD8UHSiYeDwVzLQXgGsR08iFP9xiHyQHNtCpy0HHeOutrjiWibADwEMZ6n9/1D -SqTQkxnxBwIHpGqK1M06QQT6ty2Bbm16gru0N6ulMr3Dc516PdOzQzqo0T7c2BzS -4wOydYE7UGEeRzuzA7Q57dVK+P0DLtqhiblJuyxBgMLxKICgEeR6ScjWQpHW19bC -wfmbHIqHeeNCZCirF17KEtPqFCv5k5uzsqPvRv9yVwjo1/LF+k1iFgRez41AvGlN -B+VrzziRK0YvdfS5wtQ1I/a9m2g+oyWPj6c3p57CrqxaSiGa+FOHOxUx+rQk2AdB -8l4xtG3HNuiwjEy75CbKsHwIBRd/9kRrGcilb16/osU/c/jr4QopKU9HKhb0DIcl -pY8B/ZMdYV3uG+oy0aLlld10GJ4SHW0x1uB/rZU5zireTudOb+12qMfF6AyVV/ts -Aq4pELEVFD4INWxgh4EuzDAkJCvt6r7XfmojXTFR3vv9fHCc8vAVwRdbxK1NKn4B -mMUVlSwZwLyy1roeLveCiQIcBBABAgAGBQJW5/QxAAoJEPvqMRCoU3iU3SkP+wRd -T8z3EczONAcvJsu7ZHgh1ggzsmozTciSuaAZRfvFmUyB9h63cKNTS86CIrqHmMZr -tHRu9llkNNiE4Nj8JAAsMPSR4YaKHfHxc3bOH0iWtcPxtIiQEwYs/7oP0/YzFAxc -UmZBDeLvy7aKpFqdPUcEhMTWmscVajjJXv+6G8IZwYGFAFvSkYSimZP102gmgKQh -cfPDqmlqy78Ft+T5MfIha1Q950iZyAM3j46lVWMkBaKPQKq1G3kKaL7Sy3o75y4N -7lgzY5WfYnBYVAU8eUjv408FoFKAYFTsA3RG7P2VROoNefPaLRSgEgZPR6efVux9 -Z3R4zOUQuljvq8r00zMS0t5RVcDp1gCNZQ9xv2QeN/ZDld0U0IbDQRrlT15+l3St -hkXapMMvbSVKEILMgaL+ysl7raMW/Zqv1KN2ByVJsPjWnwWCPnn0fMFWr15ExzfZ -BUNh2rZlQ56jBsJanHF69Th0vI7JNm7/Gd5FRWL8RcXzAL/UbVDuyGaO2JPztQ2d -L1lnHVL5mgOMjs90YpADenNR5XkQxuazTRiQIOXfoZhgPwe99S9vEdYM6UPYZjt8 -uo1bmFEkV0CGjWngJc2ySSurftXPFJ7gzFhDbx70Ga/1lw/4H2RPs9ZiZKKTtiGc -DLhDxSuX5z3MgzzD3CNp7uKJQlTIg4aFeX9JWQvUiQIcBBABCgAGBQJTgEwEAAoJ -EBYg3FrGoH2curQQAKKAZDUbPFSAyRMFlr3TFAYjzPgHz4+tdSgwFGaXjHb1b0Z9 -MJKBkqjoiTOo6ysTOzFeOVuql5tFv5lUR1ocHJHtIX7kARvLrlaAMAVPsG+f9Ft7 -jNg2B0E3uokZHUOCXdvX8O5KNMFjiT8arYbiw1hugAJrQ1KMKIv3EsT5Zf6AnwXI -UN8eI4hUjZrJqmx1jjhKLam3SLuF8YMpAIAFwFb/OutQoRUU7CQzVb6/1B5FCIYd -SWEHv5tT6dguFyUC2pjxIf7Oxz4qntPk4HDJtr4sOBj46cNUsW7Xrr23wpvabCQW -YcGQc4gK12bB9uyleIo52UoDqjqLddbhDDv26GuyJVu1mlJR6oW6EYtRLZLb8cp+ -9p+9vWtLbp4AeyX3NGtY5iyZkGZCj7aks1DvxpNdcdU2u3Qp4IBZyneVvVYaj+UM -y/jrVX6uKYvKUEW6xHsR6g9DIGUFK8dexYdkRHQ52ueT7W9cA6V8jzME8CE8YCtT -Jxw/IQM1mHbcHkrx1iNXz33Of2qBouqMf2vDXyAvd/ilzca+dwOyoSGum2tnpD2M -nCROrfo4eCeAOb4bZ46hEayzr6RNtmtUgnrTmV0iIxDkxSzGXfjWWt11H2W9H5bg -aPG8dEqKcxFLYPOnCLJfvmYn4hhy72MKqdI/4/DlHHa13gBuL+2cm1pTLgltiQIc -BBABCgAGBQJTgLe0AAoJELdhiDtEKL3AEToP/3kV24dJyYCcqzWg2NWLHUACkeXC -GOLmKSoVVV3oFzu1OnZ9KSdhpwU0M+b199GAUM4Q4o6cIeTnqLd/plfWdNDmEtqw -8T7hyGJWAHkf0n4c1nNgE3QFW4ri8zPeWPaJ3+nDms7GpIbYcLjLLNCzSActo68p -vaKrn6EQ5UOub97g500VjWlcS7qfXWlgMcKvLVLUNHBgVSxTyghQDkQwhRl1IZB+ -LSM1p1qHgWYZdeMu7DXzK2m5htscHjcv+BlVxRXCPFf6zh7ZIKnaZoWKiWAjp2zX -y9VntYJ7DpbOmYukH7PWys9b26agMUa+iHylBPlyijC2dvEEu5+myqPBZk60T+On -trTp4PPXpX50TgylbabM0glxoHJBvPtgyOW5QM4UMdn1WAX3ohW+9y55WyMWWPXW -nrQl9sZ79QyKLmoPJE8u7pcOXBpBJ5NvLghR/wRb04DPXjLrRvqE5V+mPpIYFFrG -XD9wXhjWsgMIVC6oxGH55LIS2ZgLto1MJ7HfMEPWG6zkx/NIGss1Xxbd7ZOMvUFi -eY2l7zWWVDs1aAA3ydc6/tA2ekjvbRWjOkIbA2ctmdGqo6CfqiqZsDhoqDs+xY6t -J0IkOek5TRAMGbN3GpO92n3IO5BLpZ8mzoi49uoDNiVlZlDLViWclETtmr9Cfvav -YXui4CtPbIsik4utiQIcBBABCgAGBQJTgSAwAAoJEF1w0uvK0snmuUcP/1kWyfoA -qIt1DFY+Od+vL5HY1IMKG62t9c3TTff7le+QtOG7fvu0IHFZHpsiiYumOvhSDBBo -0Bfy3aDHF13ul+hcTfDzuGdvbDNoma+GO6ccW0ZrFjD3eSVrUnO9nT12sTqrWl5+ -/GywcuH8htfA6pL60GgktympcMbi/lvTtFNW1Dcfo423f9bYdEkN71+P1UfT594b -bGUQclIugeCLHsGK/GIN9tAoBOpa6b98U28cHxs6eoWaTRu1fhAW9MCP4Juj7d4O -vfPA7o9XIRrQzcKFicpmRi0VRe7zB4btbIMie8jhMrUm1mez13PSVB8LB3/bivxt -DgqYBy+B9V2dNQjYE7+aT0g8JVmoXr8WdyfP18wD9orWUowpBBj4R+RgpR/S8QfM -lZJMfHIkAhSAYIwaAcJ4dboaNGAEtKsS7aeH/6LUUGIUuUeTJmFSn0o7v0hD0PUG -d6Z33/v5JR4f5esaZwZd38SSjj7lObtzdgkQL5sCd73gTLhZm511DjNasnlJpROq -KeB9LQCUON463vX6QWLXHtD72gaG4G8SRIUUHjt7vd9UoVUwqoV2N5ZRhoDmg+La -UpnHz1zdbmVZrE6WHBDqxB1J3C09HQbV822EJAW/CRDrN9Y0fhucWN3TFQ6ZG+UX -USWcJg7zxyUYB0tOMNULNvC+XrkiahmtxspWiQIcBBABCgAGBQJTpLA5AAoJEHQ3 -f59qR5Gf128P/iBTk6pvJaqe+17zV3z1G3WVyUtQOdMkVptBuMtHIykZZuBUQmTY -XptQH+t+4da6pMFrxcsqu7JZAvelkz49y0zKt0cYpKivG/87qCAER/x3E24FoMkV -WlrsN5J3STT30SXSZyL+lVEKU5zuqgtK2wjstn2xT4TuhQOZ3CDSWxjWBjbqcl4P -JOnhzSlRJL28kq0Zx8SukxVwTpJarIKSL2dPivy7TZrlSdPO7sdIKnaOPHnekVaF -35/SfTCm/sfnaZcCobQZd4sJij+xgc/HDJJcfsROhRUK9BvlBzcJDCohlz3FnOyK -Xjafmk7nVfcwqRMlhX2rsO0abQQKxxnVzoUGSBf6SpRq3q1g2SRy7ABe2YnnCl+c -q9acwmH7S0tzGGNLwdjEAHUA/1HdVq984kqx2eUiSCJ1vxIuHR36cNQYdyplnxr0 -+bn9Yb/wghF6E++z8xkX6WxKT/oWV/GTqL+jcH2efOOksR8MfjmTkncRsESbi1X/ -xAt6Fn9hv+qJUas7MSkKCkiOhAz7ZRxZMu2Qd9Vh//i6hP7qs8aMNo+/pXlYwJYJ -YGuPo0oU/NhWm7yTM+MDrdBZDZ0EvP+t11R/yMrHk+aFXqEqTaB+Uw/LaaHMWv+Y -DB8mfRUE0jbFipuWoSt55ElemSa18nnRcgBTbFL+U8Nm2IGbDGeqToGniQIcBBAB -CgAGBQJVfZS1AAoJEFuCGoE7lKfEYBsP+gOUOmmHg0c09v/iPkel7JJGcNnipk4z -8xl5nTxXay4nTY6TKtelOhQUBqDHBqdOe8PNWVutXqSDQKyzRPvXJRYgF2i3IUHq -/GtCK2yPaGV7XnYfEvddXmjAlYS9LkHcYH7zp7vLMW/8HgZ0JjeHAfmNF5+Q62rk -DUMVBnSRVlA+1mc3/o1O5p/Kn1Tt47kCkLJUMNyBxXl9BnbqJtFWKzoqgMovr2QE -IZeUQzlJKygexnU4tCP5q5VefVqaVnEHkluXJq9knYK/G3c2Pet/GEDe5Fkukzou -QvcqGaujjvc/pmT7VISkeO4YXvmfctOpggJ9J/ohxg4RgvqaRYdGoFgnNQMEnFLI -xd5+8Sb48mskS59rVwwOllWsbR+6T/ZDW8FYmpNzzuK7Af/JoOcWy7/j0fwOhJa4 -qX5aKgph5S/rE9pvhmhbkgZta5m8GQ9bHInQnbefud5axRtSyx4cG1ZB/mRLFD7+ -kkVfW/KrtdP/7PuuYtIP/nEhs9HnwOmcoRI1WpDGERC6eUc+Dgc5sFD16tvp+2PW -8/EBAWQK55b9jZ4Uws0D/3Tn8BE0CP1lJCZzIzKqbO4+VhWNq0eJgwZWTUNoXQuF -P1gOhJT+yqtxBRBP9YAOg+bO5kdjqS9IinbbYoaMkY8rUmqrF5r5XNob9mJzgF52 -2npjWOx4P+7KiQIcBBIBAgAGBQJUyWhmAAoJEIHFzE+IMpocFMoP/RJWptx2l2qa -aJW1r5p1F1wSYHFgkUPWgS2mNwcgkFgGm0+QhPXiNAw7evt6aTMLMatewzq3i34W -9rIaNj1UNs7VFYEVzYzWrAGlBiMgkmvHpmMmNIoH5sOc6D8pzxagOalvHjHXXabR -Ch6r8C6FX2jpQmwYVT/lF10ARGoQMW59MGFhUcEPfGVTFWgSEj5hgKvLhvDYj3Lq -LreSsiKuVU7yU+K5kMY7q7wT+8jGt5zdoV/99OjbJOo/a7gmIDHGeuJnSuNRRV3D -ltaRyk0N2FQcoB96q53++BdNXwDNTVA3eKVcrjpTXJcxMlpcmDvaF/KlIpctEDIA -50aTNlkLvRLMnPTlFMeoNyURSc38HO5c35chioH8zd+2Cs/QHGyI+JBlTZOOodUB -4alKB6SKHwMrWpy4+JfSxF+DUEW0VQwj/wXEpi+B3HKGYI0QNuzpEGZ1qvaq0Vi7 -SqlcyKbZuvUGBz/RdKeAFiSjmOOQUbm2cebmFQzYNr8KWPt42knV+PQMet92aaNV -WhgPp7Z/OcvpUABQZBPchJvBRr+Qso+uqQvLRvlXGD+rRni1/NZxgnVh1cHN7CiF -IJOlE+bBozJ+xtDx5ZOAlH5qWJ/bm19zQDnufWxocqNv3ek8DuM2iyOmvpbi1REi -4ASbhDjMQDFmRNYx+3bIi80KJEnC2kZViQIcBBMBAgAGBQJWOIXXAAoJEE8/UHhs -QB3OlqIP/3lofZqqiV+uoiTdV91Tjmij9Rioz0kohpQsm/tau6JKXItjG7DaG3XP -L6NPckNGI+twD393Hdb/VkqatbpxLeJUQLoCjV3M02p6zDJHQ5wPiXgC/8HZVdcP -2jlvnrkg4N5dpLJJK4wpZ/KXMsw/SrBj047ZnySIl5qw9ytXrQm58R7FBB/ANjEN -vo9C3LEsaDAKv0TL4vyMpz52TjUfgoz68g31Sl6KKOw1HG+dUB69M7MARSVEgaWU -Om33eM12QQtCTndJQDg+LeYjfvfHbcnMZnniCZR7rHGxAhBzgKQqJU/JizfZ4FDc -BkABhsUQgkSeg3llFVzSU1iofT37A5cbQr0xUShPQwKgkESryuyL059neVsAhDY/ -hFeyWCKtVQ12i3H7cvzRlfYxD8c/mN5TDiC70Cft1pcLU++u/6Ga1kuzA7rkfoUo -crCSjqb9FwLBokWcwbi7SyA8YD5m7W8sPINx7reokK7mvDsbOxpBp/y/yT5ZpTjK -3/MNgESrq2N+Qg9EFC4Srlg8wzovn0zamzb2xDJpLfrV/t2DsFrVf2SWFd/YMjkl -jOLQhbsEpQIdrfS8/hNGgfoUIiko8lqNi50sGQ7kO9kirmjCZaAuOaOi8U0K1C9R -vVGTN3oGrxzRRXeqt2Z3bBqs5Lz5lrCNkerWZYXcItIyZ415i/FsiF4EEBYIAAYF -AlpeZjsACgkQG7icBgI2dEl5UQD+LepkokCazIBkNFnZraHcCESgXDW5f8f+dpOx -ZVo5Z0sA/1FkP70D6Mw5HbRuebIZJ6Ma56I7+Hjg2pVSs+vJ050HiQEcBBABAgAG -BQJPdxJcAAoJEMP2qyU7W7Bccy8IAJSvbu6RkwVtTznNXGtGFXqVsCP/yJMAgU2l -hLMAl6yvUMk9IrRyKZloxxFeBObqQ3urdLQqXeDIJmhrIoxix5Mv2VuSUJ7vj9Gx -Ts+w6vldvPHc4BzJWR8YALTngfyUURMuJXV6BxseXvdq2WhOedSptLgGKFgZAQxG -/LcUzlLES32H1IsEHnhUhbmi8yrrR7sTi2KD5XBUJf6cDeEbwBQQd0MDrr0oPOe7 -wLJSNtIbYj0hXAQug5AezJrh0dWvBqJIZxm9HGTsMc+dnpgWamVvcBMXdXxtKau+ -XxBfr35zVFDylNuULr8hj4ZmtOKKILCT7BCNQ5HpkVTXHru2kwaJARwEEAEIAAYF -Alf7Qx8ACgkQo/9aebCRiCSTowf+Jm7U7n83AR4MriM1ehGg+QfX9kB3jsG1OXgK -RpGPIORqxLAniMFGQKP/pqeg2X530HctqjpV+ALG4Ass/kNn4exu5se2KuThQMKL -K7h7kfqCnrC8ObeCM7X70ny80b2h+749xWZtahpTuQwVrhcAikgPfS2nXSKdubOy -eBH3y0kT2zAoml0MOQsUb6yGycjdnbFrKvfINKfuZvF+z16YOu3eYZ3NO6dErWQ5 -iTecuNe0nnn30D8+nWA5JfCxNDPfc0e85dm6xK6GTPdaQd5hpF14TdYZu5eT34BX -JcmL5hJ6MzM+OFn5CIn2Xa6r6h9AOp5C0o15Qb6SXpUdZrV/34kBHAQQAQgABgUC -WCj2AQAKCRABFQplW72BAiXGCACSHG54fSeKZysDiX7yUnaUeDf2szdvegD+OPSV -JQhcDdhyC/YnipEN4XFpeIkpxUrBXWYyy5B/ymzDQl95O8vI6TnDpUa+bvpkWEAl -BK2DuElRojXfPo35ABu0IetQ9xyR+3IzaepHL7Ekf0n0H9vFTmeyYUc3B1m7RDwn -UJuAlWRt1qQHmOejkzTDBZALeg+BJ5PtnWqCr29+JZB8cwUJ3Ca8YpbiCrXWYHu3 -jlXDDyEhQ73t5OlruOMiYp+opmRySu4rF2d9yJIXnq6uf0WNb6G6JzlVMOqHKvtm -rnwXb9zlFTSXb/NkxNmbYPrTvKmSr09YDC/p9iRkuDSeI/OEiQEzBBABCAAdFiEE -IFnjmbk0Pj2JY1NS8U5YASgDCxkFAlqf+YgACgkQ8U5YASgDCxkWRwf9FHB9FN2G -FXNhPGRrgtSzffos5ccxXGFKuzmNoJzceQNpecWbsWuzCG5gNOKlROgTzRsIV98h -Nq8JWhlViEHq+fOUwt7m7pnPRSmDGIW+yAo4wHGsqgO0Y69viw66Rx4rG+g2ADdj -qFfo5KuS1rQOyeF5MMJKPj8SvPLxWcfjnpdDg7OOnzJtG5FPviSektDc6kMac77I -nF2WstLBykhxpdhtoYQk8uYdKoxDQWMqNDEh2pJkAKELMnHl898uiNTgLqgOQoNA -C6UWVITDvUHqoq+uI12ZW6x2mwVDFWIQnTUsnhEnPIlM/zUHg0BuTmUv5/9x6XvW -fJJkHis8YEBXXYkBUwQTAQIAPQIbAwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAFiEE -o8Tw+XnKoizbqPUS7oy8noht3YkFAltn6jwFCRhLy9EACgkQ7oy8noht3YkhfAf+ -L/XXwlc/4k/sWL3A4Kxe2LejqrrfSGdzo6A9JQTkwuGzb5t2UbynACNpbYxFlbdl -g2zOH2rBx72Yjg4EYSyzPEOmCMvwAO3ekBmreO8UyPV38b3c6mss9JxTenkKokFt -BqsAnUhryykaGlQ8fZs87oXbOtpHZL48DG2TlSiQ2k4j3YjiXnsHlPZpDPfVHrU1 -wlcxciI3SEPQNUxcRwHXkGtAcXK2P4fmRcDSXcgISh43Dg9ikV3yPLlJuxa887/u -Qe2ytHNOCgC9GhGyCOfQV09lr7mKpfJmz2YR0xZ+NGd6n5Tvs5GpKwoc30zo9eOQ -f6TAnQAX6w0NWHhKQEJCFYkCGwQQAQIABgUCUVSNVAAKCRB+fTNcWi1ewX4xD/d0 -R2OHFLo42KJPsIc9Wz3AMO7mfpbCmSXcxoM+Cyd9/GT2qgAt9hgItv3iqg9dj+Ab -jPNUKfpGG4Q4D/x/tb018C3F4U1PLC/PQ2lYX0csvuv3Gp5MuNpCuHS5bW4kLyOp -RZh1JrqniL8K1Mp8cdBhMf6H+ZckQuXShGHwOhGyBMu3X7biXikSvdgQmbDQMtaD -bxuYZ+JGXF0uacPVnlAUwW1F55IIhmUHIV7t+poYo/8M0HJ/lB9y5auamrJT4acs -PWS+fYHAjfGfpSE7T7QWuiIKJ2EmpVa5hpGhzII9ahF0wtHTKkF7d7RYV1p1UUA5 -nu8QFTope8fyERJDZg88ICt+TpXJ7+PJ9THcXgNI+papKy2wKHPfly6B+071BA4n -0UX0tV7zqWk9axoN+nyUL97/k572kLTbxahrBEYXphdNeqqXHa/udWpTYaKwSGYm -IohTSIqBZh7Xa/rhLsx2UfgR5B0WW34E8cTzuiZz//////////////////////// +/////////////////////////////////////////////4kCHAQQAQIABgUCUfg8 +wQAKCRAiLOjENkQCiI1OEACItuCpRR9YS9HeORrELMBSd2IqJBeto6V0VNse//g/ +nCVKgOKJo2hpEp9BqPidjBvP20Ek/xIqHr/Pz7R6T1UVsjqtQAlLngxab81wJsRA +QNuTpHQ0VoststglEsLtp/ziQYOvgt0yEcqKs7NmIlyA6/Uw4uzXF1D9hnfsQ1sh +Iec3d8YpQGZf0jZFu94Hp9hpxtFkTI87yfUkqmFRRsNi9KGksl/hyN7pQMm1rmGh +7cERHIHCiaUSu1THiAhEUc5hkMWlM2wbbFn9ZYVVGgoyDWyhDjn7qhKnERrF5dwC +cP6mFGo9whO4U4lKUNJHA8OxtDb7mDhagY0wGVTqa+Ob2zqgqiqeLqTYdii7BnBq +swcvkbm7BLGzpiLgyJsoxS6Rhzmb+eJiTS0Pkg22y3I/ehD2efoIO4qe/nuoBqho +SRDkC1nl3o05NqwF+c4JB7rZo6mO6mSHut4l55avPAeurWXLdnWML9zPbdl9jJMd +1EdVMUGfMCY5kmEkuPRw3yGYeTSM+fEB/AHj5bQZN9sjMUhatJZ3RihMoRNqJjMj +WM0rdBHF3LGmoqq6YUPYjyfHwmNvTDpCkUM/Utz/zTmRUK6i982r3yV9vp6cdLpj +/e8TyKMDD59EGRFpE39q73Bt7PLOY31DTrIvmXD2s4Y8KlerV9jr23yuPQht703X +AIkBPAQTAQIAJgIbAwYLCQgHAwIEFQIIAwQWAgMBAh4BAheABQJUA0a8BQkUqY9H +AAoJEO6MvJ6Ibd2Jz8cIAKfXu8kXq9b9RqMsK632pt2n1jcuxtGyOYH/fFj64ZIH +N3GqVVQ6TnvOzmnns3iAj+nbkxPEuWLq8MfpW3Aj2aewqOLsowHSI1RwIcBhoacx +t+GPGenmwneM9ABJTRqQ0KTLSqaS5wkUcJJ7r6SgSJ+LMQ4LKHyIOr6OIvJy+Zqy +M4Q6X21vTSvZVeCr5rweE/l+Wc3U5ENMmtWh7RnTGk7SpjjFZP+HHhkQ8OuaZZRh +KOGUBIBlWd05jR4nYrkoRqolRG0gxkRRFTlIhfcr0fruof/YqlC8TqADn2DLhrWr +Y62TOOnfA0djtaNNJ2xh1mGkFaophnedlqwiYIQCDMWIRgQQEQIABgUCUwjQUQAK +CRCEQzF7BlX3gMtqAJwMblJHTT7TRUfMFUTp8ODTbt43awCeM0s5htFIHEGcQtQM +oLtWNrP+wAyIXgQQEQgABgUCU95n0wAKCRBOpRTltBrmqEVnAQChZNcw4xBLHvzh +Zwwde3w4R5B04YQ5IeSw4m5aHIn0IAEAoGR4ZXhPF6tjZg+p4jpX9IF/MerMx6C3 +boAMimHZ0buIYQQwEQgACQUCU95qhgIdAAAKCRBOpRTltBrmqIaeAP92zcglLcFt +fLl3NLu8JlNhkYWr7DNWowJWjhVcFkNkrQEApYO7wwKS1N1ZSp3YfaWdLfDjEwMd +2nEHloRWDaSMr+mJARwEEAECAAYFAlBbsukACgkQLJrFl69P+H9BSQf/Sv1aGS7w +JKz7/Yi54t7hVmwxQuVEpvAy6/m6e/ikLRFInWe1kNiLlOcs5sjUgqQtoAlkpvw3 +5klIwmNtR8jRVZDsvwu0E1U5XIJ0icQEsf4n0N81rYOlwrQuzDNOY0p4a7jpLFAw +MhNwrBreF4ebz3ZF9yquxmWuCoJHE3iA+J/FaMzmGdNVxMpQXUPOjdX1hNH2e1BB +GwbUqpSlqI8qfjEVuYjZTs0u7xaHN9e6DaqwRoI9zcv143yY1FrRJuWFBLCsdogF +xDDUKk2VwLSFw45dmZRTABD8ew0Y7kkwHTmsEcVg8PM6XAVcVOT04+kVZQJ0so2C +d2sL041JreDaDokBHAQQAQIABgUCUtmKKwAKCRBI64stZr6841y+B/92de8LDKj4 +UjfV05o6e0Ln6lIRgxpexbgqyQ7A/odZ9K8B/N9cNNaFZJR4tAAt+E8Xahcyd3qn +0rspvI7cdwl4pslO+DIsdoejuL8g7SBDWCjE9sQLEDLxG2hqUkCrc5mh6MeAXcrK +12LKCq1uMPQzc2P5Prz2C4j0XITBzSGxukxtoC/vj93+h/gGcQUzQIq3L4QE1q8X +F6bqTFpt6i+tJULSZdrFNkcg3zx0BkLAceGCd+BDv++M4BRpWuzkXH/tFpXq/reh +uh3ZSstkvpqZot+q34GMCgGUvsM/U18akYJFYpog25rdYTLTs3eYSqR1ef6BQ4lh +GWDx4ev41YIriQEcBBABAgAGBQJTBnZtAAoJENgv4DzFW8/jPXAH/RObXOYzaU0R +8ludCEhJcWlx3IibYRCQZUcQUUTdiPHEiEVq2vPruujvL9KmK2c5lvK3TGuPm804 +F9MpCBWA6GSM8txmIndPIUuAKoZP/dErMo+A699BbBesTGY0v1pF6eyKPA5cgh6c +OaUXHCCOl5LPiWN664Euwk+IUM8bi3Qx78PopW+E0EJehd3PLkC5XyBIIe6YI9ov +Xe8K0B0DMMWDydgdafTjGCB/nSO/C1qpa7tVwvGLFdh9qhKndb1kbFYBHv957ZhX +QoLFo9D1IAPEzXEr3q9FsNgaVvJNlJj73pjesO6DNfBEXHHr6IbGl/IrmH+Wgo7Z +m4RIYW8DfTiJARwEEQECAAYFAlO+oyIACgkQj6lgRkXLfvdS1wgArBNLxdl9uDp1 +4N7kpYYWDGi0FMgNhyQCLzm6wFZVhZ9L1bwhel8j199rzpTOL96ijAZf4V/ProUj +vs/LJ0Gm0eqLLYqRoloBkSlpmywf+T3wADjT5iT7AdgAjOEdqI34mrjDXE9/kbM5 +K9a8J2WWLtl4P4SaTqiWmQBJBbNBlaL5uIutqX9e2cm+/jufcfpIvAFi/ALCu0AB +C2XnfAKpezotzyyk2TxmpVwemJeBscJgbF+mN4JssQQq/WcgGiQHtIxtZeKjpSVC ++T99v4/oPscOyPt57cP5/QHgv3N87ikzCHwtfOpWXWJmHza9qImDPzxlk3XeMZyb +fve4tO6bSYkBXAQQAQIABgUCU3uwcQAKCRCKcvkT9Qxk2uuTCf4xTAn7tQPaq5wu +6MIjizqrUuYnh/1B4bFW85HUrJ45BxqLZ3a1mk5Kl2hiV6bLoCXH+sOrCrDmdsYB +uheth9lzDTcTljTEZR9v5vYyjDlxkuRvCiZ2/KLmjX9m5sg6NUPOgeQxc3R0JQ6D ++IgevkgTrgN1F+eEHjS+rh4nsJzuRUiUvZnOIH1Vc92IejeOWafg7rAY/AvCYWJL +20YbJ2cxDXa7wGc9SBn8h+7Nvp0+Q4Q95BdW2ux2aRfmBEG2JuC4KPYswZJI9MWK +lzeQEW6aegXpynTtVieG8Ixa+IViqqREk2iaXtfoxVuvilBUcu5w9gNCJF+fHHZj +Uor5qHvZz91/6T0NBlCqZrcjwlONsReSh1Stez8SLEZk1NyYmG56nvCaYSb1FvOv ++nCBjz5JaoyERfgv4LnI+A1hbXqn3YkBnAQQAQIABgUCU3+zcQAKCRBPo46CLk+k +j1MWC/44XL3oiuhfZ/lv+VGFXxLRI7bkN3rZrn1Ed+6MONU5qz9pT9aF4C5H/IgA +mIHWxDaA30zSXAEAGXY3ztXYOcm4/pnox/Wr6sXG83rG5M/L4fqD0PMv7mCbVt6b +sINX5FTrCVUYU7ErsdpCgMRyJ8gKRh/tGsOtbyMZ/3q9E+hyq/cGu8DjhfEjtQZD +hP1Gpq4cyZrTRevl+Q2+5juA4bCyUl00DQLHdCuEEjryq4XWl0Q2CENDhkVV+Wkv +fuIOIVgW11j7+MmMXLzMMyk4MZtzgedJW8aU2/q0mPn313357E9DwMZj9XvB3JCx +4dRjBR67zwYySVvnK8KMWVNPWcleVrY+oj1l9psq+d4pkjtAa/cd1mBfh7h6uKzk +ekj/zWuJV0+HEbKRmmBpc8SWc4QRNUrCBk7vVfGsBLCmiCK9Rij1zgrwihrw/T77 +BcvOcxhZNd3Y9Vs9vavExF0/5IqclwcuJqQO5fRKmMCFi1rwT5ZcWANmJXdaN8H/ +7D1WNXuJAZwEEAEKAAYFAlN4AagACgkQRCkHtYjfxFfaSQwAjmRJHNBnTYQ2Sluy +9KzmgtiVlxl6Maxr2zBQvXv4/mH2Sl2BeFWaM8kiyQzl6XZV5/q8TCkmskW0N8YO +l+l6AhFGuh4PS8UWe050fcxJCB6Z6XUFdvVQ1F1dI3bNcmm5libcMSNFNS7pQF1q +az4fmVniwPx1ezBdAvd4n4l4dipg2bW93iPMiy1JDRc1Um6U/ouW2KnD7l5/PkQK +WLzSx96xvfimDD6DXbW+/7nFhle7foTLSlFOcyeuXCOQCa04XQOJGKZtiVp1Ax3M +v8t1A0t2EzYlTTKZCCCCa9EDReI1m7EJZ7+SJueaW6u6/TuM887l4FFuM+6Bow0I +EC8FJyPdZg/BqnZ3tK4xSm3tF6oxc8IkaQJip9R76hPSWRfzc7ooTbxQrzYVzTZa +/pb6RfL5bTi3Q9D1xCRjPtkZIceMWfPtnymlTIDwdefzTT0wxj1vTSluqMih0LOD +RDrmysDSx9MBfH+zhigweooCCj0wLmOkmT0PjgJvL9TBG5HViQGcBBABCgAGBQJT +eNsQAAoJEPLvL0cGnouP5ewL+wVOickmGd+Dout44YAmPXSzdP1KervaRAWIQLFd +a7XFb2krwGwIpkw7hR9qhAG/CWbF/WRQqWB9M2qQEaHP7LXjPuCQVf9w5UJXzKUB +ft//PRF6IzBOm8g+yHY1MJo3x3PDd2Bym2hnr4iV4teVnoHiutAcKPndpu6idaTk +hguNuKOc1hXqILi3x9WRVi1d2UL8MakyamVz2k2sRktKQEZ4goEYq+8kFeT/T0DH +/bB5N3PEKwpK/v03T4fD8ihMFYwblN7Y+Rx0mrYthCIQYpfAVA6eXjyABv4kRj/l +1G1ir8ar1PnrHiNp2Hv1aipDvfDZnNpicwySOrdyQgpjGao75Ipw1RNcCuS9DWUU +POYYQQfknCeUMgtQDqoJBYiE3wp24QZw3PsszyMk86bQWqGuhdrmA97zwX9f1me2 +BdhwyLPkBJVt/6t2Tp+vx00VmhbQKLbpPIACzqAGw8RtUx1G5bmSjRgAuo6xWOC2 +u9Ncxt33u/zQ7UvC/wQ2FwHHD4kBnAQQAQoABgUCU4DA6QAKCRAq0+1D59sVj5pD +DAC+MneOmun1zAq7WSSZmf+AI3BzYGoYN67lJ8QXTcgDgbqXAtGQvp71G2It9ugd +PEeyQ4T3DxNIYA2uC344hdsVCAnQHO6NMvR5A1qBUldxp1w7GfgV39p1ANzxDNwG +jwwfUQfqk9VEOp4+puut4o2fhyMmkC9RaGzWV5taPyWL1N9+JqfNfsjWFC5qeS9J +OLTvhmk2lLVKnw7uKluiQVzr7yj/gqcsyA2sPfs938cIr96CveTdd3d1IWcRErB7 +2e3zb0PKKvrtXjfAMoZG0vrsA4So0D2Z3Y710bGgLQ1WYDlRw7YM7/XKN2WWIBWx +LNfEjVIuVnpHLCTNdmntLp5oaBsC9TrDwUMDZ5DEro1XHijX3h7x5Ni+XU89ZodS +eQy9uvLwkgjiZIxD4DfCXQNc7I2a7h+M3rvu3LeBIQe3v/KNMDpgL20AyLxUs7/e +qe0zWm3F4sfYu7ywA/mkH1Az3xTWj/I76WlmKPSeJpNEi/fol0PCsTJ3vWdpu1Hk +t4KJAfAEEAECAAYFAlKfzT0ACgkQ/bW4wGfyU4fk7A6fayMhAuOjAsP5s7GebYVz +RI8Aj5Qmp4w7DyJRYpwTzyIVPXzLTpOmpQRp4sChlIA9YM/Ho8jhacvpBKDPuJr3 +p2DhVTUVL+BRRWoTFJyrlbC20ftr3nCOMEW4yHA2u8bKvHwPIUzasqqPtybJ2wdj +Xx7V5W6TpwWnpJFHl6TyqFEsb0b/Ne61Tx7mB8m/0UUjKyu43O0k5p49dFA7FUUl +maZmjGrfdxSN3HbwRXbaOmWYn4q7TRL56BmLWZklxwXCY1nwEXdkC/R0U0s6NNU4 +o07hahbc202SzLX9PaHCEAREVlTz2nVdIXcPUdo3hOIJhE/2mbfKTqB8WRgE5jfX +zdogJBhP7D4pV2DyvE+SKvIXQ1Xp/2SN9hLWwBg+pQwjMpiFX+HVRw+6p7QorR/k +2kryhtc7aUnMtkTuCq1tzzwbdGD7e8O6QPhuhId06GbqKLplqYPap2sVAONE6NHL +zmWaY0nFdzXiICXSk0oTUS9NwmAn0WdCeC1pJi6T5iyopxDNMyIFFTBTDFjxWbeM +o6HRKsbjnhEEayV4bwJ8IaPjhvEUTpDgyV28kCSRgJ8zvNLDD+nms6k39K7c0xji +BgIek47zMp6bgTPAn0Q23hwCMf+FiQHwBBABAgAGBQJS0swMAAoJEKQiudjlJ9vb +tnQOn04QseTRPp6toW3qTzPs2vFToGrZWuhRDFxEUEuR1GGM3UFWvk/a7UnaHsaX +LqZqqKIdqWlCb1EwddFJKiZU+Fq/sRm86VAeK6OQkNwMtbIugW2WC9MPre8D9gVu +dx5ZjYBNjqCnX+yn+33M7/LAa6Tr7GVUqV3aM0ltCmQHABRp1acQWkWLG3IQiA5T +y64hXrCPr/dXLCyFsbUyXccvgTiqlKo5OCh6xC8vLI2OUjckvwoH5yWM3EnEE4Tm +ypGAHk+EP2aVkNflYWMvcRbBAeLVKk8+a6+JyJJnLRKHDTKN6++kyceeTN4fb1Bv +2AN+S+WZLkeTatibeq+78jn3ES2Yl9Jdik7KF7cSx9+Y7EcSoua1DXZzHVO4rPSB +cWeH4yb+3ET6xUeyK4+iZqd/067qTxED6ZDf7vXk/8+GiobRC7ob4Y0IigH7bWWf +xiv6DBuwpcRipVAhMReoOR42UIfL1IWOk9d/lcmHjmTiYvG6XRMcDAu3VHjUKE/j +b/6vcq5hZ9dcBSzPQJ/mR9AtiqnA3Y6RfK1UrbpQ3rJUu4UF61NTi4la0kFAETcf +JS2rTRgBJ+tbL0hPPVC/81ZzjF2mgnvz0CfVxXpQ7un2iLnRKKd7q4kCHAQQAQIA +BgUCUwoVXQAKCRAO2qlF6KT/l55/EACE1KOCpGqaHINcLq4KWI3rRss/aSOj8LVd +u0PcVloy1kZ2YZbB4UqNSYbzWPUASCm9kEFPlhqAUbVjyMZtALW4ZhgZSrHEUTGH +ygdFNqRROhxg4e7Vj80sz1hym96KG8gdm5oLQTbFhgcYHKEBEgtfLmZ2Cdn35Oje +QYVOyZzeTw+k3ihaJHp4K/gVZMcAdLFT+WWoXO5VzZ4+5g03rYbNGcsQ086IPQJy +JipSUe0Lv7oYYc9pmJ6G0vbYM78qkbYm5sXe0S8JRjsH+v41AN8JmILzdQde63gd +RsMpSvXkSHptTjxtLdlFf4uopPQRTK8K7qHkw3dTzpwO/kgy1wtrVGxsASuDxCmw +/yDHuN3SkMqWgGF0IFqsJdy397fXggH1tF/z0VHXEsQPFlqWOqRak+hINRonEp8G +q4b0lnLPSNxTaO36AXLt0uvsDuoyuv4szjsps57sxqbrUJ1QmblSC9xRfkAveaaK +U1I50wURejtadqOTnxDgCdn++nN2v7WbjweWdFn4r7kF8ww7BAuzu0kZGDLwiPFb +Px+n4o7DpymLUrx0W5udkdMxVhzxQit+v7RWqFFa3DzWxshWE9pJS9e+xvnupibm +8/J8zzC5Vsz+brVGGPIDOFCGhq/5j7nSpk9oxaf9uaBSqcWoga0TrF1b/fjUNNUG +LcU+QbnsqIkCHAQQAQIABgUCU4BKagAKCRDxLZhXQ+4mIKfwD/4zG06+G+lasq22 +qv0gQHzdkqXJqjlpkJ+bYgUbxvxYFevL+eXboCjImgdTqcN8xoBd5fMc3YxXbjBR +9YmQYL+5GqKILme7bVfOIOsRlRP/V4zroIV+CnISEa6UvEKm2u0q+Or2KzZhoT+m +DIfQpjhucnYNB+jMF5ogvaLCmPxu9Tsj/PytO84hPoiJkvqDrAq558JMQRAy5MKN +3p4GyTKAjSyvqqUrmrcMnbSOhsuy2mTiAYxLn/CN5g+MJClNUhOn+sPN6RDMw6us +QtmOoSws9ZKKGpiQNPFidNbtZ6SK43vO98mOkMNFnxOSbKdFkeIHYW0nC+EuJtkP +WS1v9o1hW8M+rTRwH6N//51mZ9iCOhgyX4H1+3VPVuqYnfqedmwALoIYeoQ42x/3 +lRfQWlqJpiFbY4xwJKR1ifFerziqaIxvpcq684t2Hk8OOLNeAbH8Ucf/E3EiszPt +Y1zaXk9u6SB6IY75UVXSba8OTGFDqkxqVbR+hoaCUputrDNfegmwe0ZKRB9E6Izn +p80IbFfnvluBVa29kBEEKlgd05Jhi6YkbffBT5bWTu3xyZjEmqnvljsU8a3Ij9Ba +MmScWEDPjbo0FE5TMZgHUsOQBwMIVSB5ra3kxGSh6ZcffOIUmYois3bE1K+/wHJ3 +Q3HWPSjdv6d2X9dcupz5WLL+E6A104kCHAQQAQIABgUCU8FM+QAKCRB4VAVOzv4Z +5HH3D/9/lb9giwpUQn6YD0y46Bt9T+NuUcUy5sdB4B/lC2kCPA9WJq8eo/lFFuZp +BTbcdR5BfHm3sx/sIuD60TieVDXSdKVuHIDGQh5T1NrodXf0xykJ1TmgZarAyMjg +GXptbFLSX5GLDmU51G28kuAkmJH/R03z30N01nj0tIBIY9s1eK+ADzDyq3wH3O+t +Qlrt9yGNEqmC8A1j0Hs3edKRiQyWJwViYsQK4CUCuzwpA+oUbJZ1z1v1Y/FagabY +jTucmRgCp/FD1IOS3jHl01NtUIfSkG0BwBjlsW6VBVZ6J96VT5rOyW6wQOSOFPUN +3pgaIhYFfgES0BXAXoUwQzgdzRzftZymgNGRu0Ox5KUx9aKYaWwvauuzb0Lw4IoZ +TFx8GURfhMCgWn6NSLIF8MfJP9CbvujfovD5W5wffMk6cYKNq54/vVeR5H6hhld9 +7PQIqPefZjTOoDq08FWby/w838sjl73VJfZyFjOrLms8TusFkSLY/b1Kg4Kv28ie +l+Ufa18goqCocHus7VNvN4YKTQGOypL0w8j6SvlvK7trH2NCBDVLU+sN6RxlVZKK +hqMeXZHDvX7/jpNHhjiyZ6XqxXLxnXeFf5hiyh/k0irJ93yT7PvTB/FzCnKejQ25 +It2n3+bzw349vp4cC1xulk/ZfSD5gMXmsOUMZpDQ1r/9s1OERYkCHAQQAQIABgUC +U+qnNwAKCRA6L9iUeafEwX2RD/0YMOSJsHIrPoiFVSFu69w8lvgPfvSQCPJrkoVP +mdc5YiJiMGp8DVp+UW3JmOLKIUPUg5p2/C+8DLgjWLV0f53srOCdqp9qXBx/0yKO +tvRNGlTEYywVPA6JOeNzjcdgUgBrkT8lw3Ij85+eJDVV6QFuTSPmeUp4hEESeNKP +WKT0B3Ixl5zbVHO6Qfa9NibCKpOll9YkswJdynteFMkpVm+Lq5mpr6Jpbn1WDrRn +cXp4jdZYG6yWPwQm9m/2Ua9ILqb9xBBKf7lNkywVbku8hmzZX/vYGZPGVZddex1Q +Cwp6UNdUMaHUGhh/B7kf0BHseGPNNg8sxLE9RZ85vHmXKmQfUDvKY3Kzk1N8gogf ++78KXh8pi5KIKzIq0GsUCujlJxIWDTro/Q3re3CT8M3op3qx2gjZbpsSmweoJtMN +UfLY6hx5M3I6faxKB9VA3/dboBwsXr4UddQs+GUsBW5MevrFK9R4CuHwpLSpZBXD +/GnQ0p3M/Ddm7Wy5lmHwUimStc+hkrSKrsEy8ixa5sV0hq7Ii2hE1xdEtFSOCLgo +IIIAzp+N6MaqCEkmjCUz6//74Wy9/O8MF2ytu9cAu1lQEJrJa2YSJk8y28Y07y9i +9fzQkkQSVympUVfRws2YBmqvuyxcM9D0HnIkivoo6ka5kCiMsYQ1Y3F5uDlOi6yB +c6AM54kCHAQQAQIABgUCVDngmAAKCRDRWYmf09n4stAHD/4u6iAABcOsKmIKIw7K +gO/2InxofURr68ZguHVna4C8Vu3aK1IdLsPyS59CUa8yqEuhBd4R6z0GrJgj8s/X +JGXkWYyIUeZimLaq1rBd76Wi9lQC17G+eCqgEfJeP4k9PNyU5tZrxGzCeCRVRjax +jVSFmHQ4H0Disw+pWbcEWUxI2ObvrCR0uFUb4wI7vNr5ZhMfIZq3A1dn/vUreNKU +4TUfaNUXJ2uetjRZXbHHC+3xS/bjO5JhTBoneScGkVOG/4l4kmemHLTUMn4rZDlq +BxtGil7yTN/VrCbpRygnpEouM+JzXeYWYDERRti+H84HJusDRIdPNcobFTeMR8VE +U9Q6zIN17Xd2Y+MAS+VxR8kpbnUQnfz2D0ab33AsHiSfzk66HqX69wxsP0KNlZ2S +nvh7vuCqWZweTa2CM+ZjHMrCTAwl4gPWHcEZRexLD/5mvBXWKccq0etfhkWPgDVD +9SjKmrrSY/alux7SG6mmVBQLoZg+rnrXAq2lg+xBe5nmhSbqM3pzvXwcwYHKSYiV +iozRJScaWj14ljwvnUFbytI6ctdlNVDad/DwbNfDPcNnjrAu9LVYZKOd6wq9XJS+ +U3W9d94zVqPo8lpinGBSgEc4hkN0NxkxPMnEcHm2XkoCB2C85vcxxmUHPXK6QtDH +6GtPb3GwTcreTUU+rP5zhOLY04kCHAQQAQIABgUCVDokfgAKCRCaNKuaK7KJD/W2 +EACwKaI2AMnJ5SBBfBlZ7dH280mC8BgcVrjDJs3Yh9xx708bFAUNir3AUa70gtQv +IDoaWHaLiPkUlz12+qZAR1iTxZhmj6dESqoCzA4vsCu82YjxEjCvL2mCUvUZi0ti +syTJ99EGENFWX6yYsPiuXo0oHaBc96TqXCQjZQZjYzKHAOjPrujtTw2/zjqkj0ak +pc2c7tUuR8g2jit9l12Y9tBu5bcJ+Wm7XZPSjvClkdm92U+hjM8cdy/N5QS+oXIO +2uja2ECrF3VD/xxL7eqZ1QQSk5Oi840TQD6e/WtsOJrk9KzAHx3Rs0YXu+/NvCk5 +U5ZUFxQRCh+ptt3WkABxMNcnQf/R/qxvktLpT9VdiIM2vWoAfVwEiIESi48JA3TM +znoX9KCrdFOj+pKcrUtzNNubfclQNqlLhugOQ1sMH7ka2PncVHWxeWaEGBCblwy5 +O71bodoICXJ3xmd3yB47QsL3ZTEUMw19mnac6Dcu7sWR89EAW2kjnhYRrNsRNf5S +36UWlsPiEl3ae2/R4wenSOm0n2FD/eNDIu9neth1B8G1jZGlnuGn3ggFm07h1gnu +I5z70wRdLeplOJPpcFqNLmGIyTNluFdDhkn5SHQfLIDsYJhc5Qe9kyMMFQXi6wlB +L8ph6m01HnWOI1Elqy9ebHw48QIRicWYh3uMnasc+qdvY4kCHAQQAQIABgUCVGcY +SQAKCRDNl0yaOU1jPyAnEACOeeeZEC4ODefn5qtazegMI6yOJVtdyI19x+OtjzL1 +Vgh4CVfOqPuf2m++O3MwNMW7M1vL6/ytImsgOoX8EVbbhF30JdFIf02o+Pn4SPHH +1tvuRF+PpaRqznJVQrBx1X1Wf5PCy+5m426CYRvcY0hX+iQbaq/vwBbBCAPjGBhQ +Woi4C+vI9wibgz745MKQvzn6L+RUXTxDlkPaHQtM9srw4wKsTpJg442dOBSeTwZz +W6OuwDlJNubIah7gc1R/eDAD+x64O1GhXkUIjIDRJX/KrE87pMswhT8SeMshaW+e +nQ4pfMMbLxnCZThH0/LAIt2E9idkKE+ygHBEvmmID9UNlI94L9DJGizXA7T7EBpL +G8V6Iqav1soI9lMDkIfWVbcnI7r9A9i8nzzFUz1Ruug2FKWr3q3eUAdp09i2S8V4 +Th9LSKphVGqCBa76y59uQNGeUBcvx2z31gMOzyb4I5egKMU95yr6M7dLVHWdg3xN +4eM4wVw4r92NNeBZoYKsBDoJwp/PUkf+0hzCbDCqfKMp9Zn87J3LPoKnKTob49l3 +zxKZzmwy6oPfCenshRg34RL9WzRDgeCHBRfGK1DRLuv60vpe6zR+75cO4VVhNA9R +J6WfCmJPKj5TrhwxyzIHphAlG0ezoLetx946hXwwIZSgVGN4RuUu5aVoi83EHHGG +XIkCHAQQAQIABgUCVGvw2wAKCRBcs2HlUvsNENekD/9dCHXxPGrqyyH1TFEcc69A +lhwcLgBlepgigK4mEWhBIzFhU4WLEbQkvwhrXXPQcV1ORsLhxXBxbgQj+NuSPZMZ +sYf0XPsPAP2WQFVOQOIGkgdIZDOaGXQdkMGJl6xAhEnbdIh8XM/f88gdUeKtiq8s +225CTSSc2zqxqcRur4eg5OAfaxSXkWHO5VXx906ojhwpY5RwXRMPYkAxpeiBbkMV +KjiTSSu+afuP39HiuuFtY1yNmxnpEwEN1dZgPcb+j/kkfjYz4OFkJcerE8pLGsW5 +MznHIcsfM85tQzR/cJuDbKGSjBOJ9LAiAewnWO6AUhcSX8kadSUj61MHTSF/JErH +YCaTOzkYGZKI31lgqrerp5YEbGZrqxWIoM2RVgQBkXhyHhyeeHlC6YNkDyp8MFrs +GB3RD6f227mi3D0HJJTzhp50MpwaVL7t1Hfxa+/uEzB5jiP3uRFFMim5itKSSz9+ +i6d4tvGx1EwCxdpqw5cd/qDEYxeYkskguNSopAUgYqUcdFjt4xc3UujS1XYzZcRv +ZaYhwpHO2x8/XnTL7gJ2oSvxG0uoRVBJFkDibSSnPAfIVyZgoTNmMbRA1b9Bp4AA +MHB2QL6YRXrsvb2H9kuSGyKijDayoKuFUPo06hx1yOey5BhwwmNooAx0BqP0rWyO +LPDsOW7UDHVz5wDuK2etRYkCHAQQAQIABgUCVHHpRgAKCRCY3btOIsosg6hdEACV +HVLUlMx1d1aN+qW2pk5wrcjqhKdl+S+cAo4flAMPShnmbuyYos+7nkKsSkLc9Joi +529otzXivRFnaGiqzNfjyMpux+NAE2rq5Xig/bKuPW/Ofbc+Ysugy4dWD3nnrkFf +zW4ycodOkszZDI5Hukt+AnKQ2tTqHM1bCNUbn1lTLqtQvePj2Q9MgglS4zFA+d3N +AJXYLBV3XdqBFPyT3ez/cAmEilf/vRfsEWu/1O+x0SjR3dhQrIZidZm4ZNwRR0wC +xZ/ZXdf5qrY1EwK7deMMbORsbD5K9WLFkNQPLlVLZ1t67J9FJz/WxXAH59/3d/Nh +4bslvhzleIOSYSlZRv4QW73S/h1de2PmJLBnkFtbCiKpo76+wKxYQiFGKOPnpsgx +I0Jk37r/EUTtbuMkdIgGapZJPP/M+d3sBNxxH3qcMbqOnpf7rkbl30Dpln3TRDY3 +fcZ+YMyA28KsL7WRMYzdj6JW4mkiz/96SPKa7azmLlvjJOaOornHHms8HT8nrzoa +DLluYGRX3yqPcOk1OUkRnGCIa0yWPAu4dmLprJoq/116S2mnXAadkeLgxKB2+nhp +V6r0mDBA/5rtX8NlTriqLHXQqX/yZMFx8MAd/c/nV6Nx2EqH8nnNZm95HALDlG05 +AIfOiFjdcpqnDU8srSvABMDix0NX+KNJpe5/V839R4kCHAQQAQIABgUCVLETyQAK +CRAXv5SMBHYTfWeTD/9zqPDnO+u5URYtTo+RVaB70cX2b196Cqxt46YT5QgYCliv +MUe3OWBAjSMJ5UPLgqlIMaRX/P4j1d8VbjtRxcA52n6JE6sjbSs9l4KZsn7Xlf9N +nt9obAzRn2gwpKm1AtoZLg31lmWv4NLVn0gq8mWiOjpKA/FB0omHg8Fcy0F4BrEd +PIhT/cYh8kBzbQqctBx3jrra44lomwA8BDGep/f9Q0qk0JMZ8QcCB6RqitTNOkEE ++rctgW5teoK7tDerpTK9w3Odej3Ts0M6qNE+3Ngc0uMDsnWBO1BhHkc7swO0Oe3V +Svj9Ay7aoYm5SbssQYDC8SiAoBHkeknI1kKR1tfWwsH5mxyKh3njQmQoqxdeyhLT +6hQr+ZObs7Kj70b/clcI6NfyxfpNYhYEXs+NQLxpTQfla884kStGL3X0ucLUNSP2 +vZtoPqMlj4+nN6eewq6sWkohmvhThzsVMfq0JNgHQfJeMbRtxzbosIxMu+QmyrB8 +CAUXf/ZEaxnIpW9ev6LFP3P46+EKKSlPRyoW9AyHJaWPAf2THWFd7hvqMtGi5ZXd +dBieEh1tMdbgf62VOc4q3k7nTm/tdqjHxegMlVf7bAKuKRCxFRQ+CDVsYIeBLsww +JCQr7eq+135qI10xUd77/XxwnPLwFcEXW8StTSp+AZjFFZUsGcC8sta6Hi73gokC +HAQQAQoABgUCU4BMBAAKCRAWINxaxqB9nLq0EACigGQ1GzxUgMkTBZa90xQGI8z4 +B8+PrXUoMBRml4x29W9GfTCSgZKo6IkzqOsrEzsxXjlbqpebRb+ZVEdaHByR7SF+ +5AEby65WgDAFT7Bvn/Rbe4zYNgdBN7qJGR1Dgl3b1/DuSjTBY4k/Gq2G4sNYboAC +a0NSjCiL9xLE+WX+gJ8FyFDfHiOIVI2ayapsdY44Si2pt0i7hfGDKQCABcBW/zrr +UKEVFOwkM1W+v9QeRQiGHUlhB7+bU+nYLhclAtqY8SH+zsc+Kp7T5OBwyba+LDgY ++OnDVLFu1669t8Kb2mwkFmHBkHOICtdmwfbspXiKOdlKA6o6i3XW4Qw79uhrsiVb +tZpSUeqFuhGLUS2S2/HKfvafvb1rS26eAHsl9zRrWOYsmZBmQo+2pLNQ78aTXXHV +Nrt0KeCAWcp3lb1WGo/lDMv461V+rimLylBFusR7EeoPQyBlBSvHXsWHZER0Odrn +k+1vXAOlfI8zBPAhPGArUyccPyEDNZh23B5K8dYjV899zn9qgaLqjH9rw18gL3f4 +pc3GvncDsqEhrptrZ6Q9jJwkTq36OHgngDm+G2eOoRGss6+kTbZrVIJ605ldIiMQ +5MUsxl341lrddR9lvR+W4GjxvHRKinMRS2DzpwiyX75mJ+IYcu9jCqnSP+Pw5Rx2 +td4Abi/tnJtaUy4JbYkCHAQQAQoABgUCU4C3tAAKCRC3YYg7RCi9wBE6D/95FduH +ScmAnKs1oNjVix1AApHlwhji5ikqFVVd6Bc7tTp2fSknYacFNDPm9ffRgFDOEOKO +nCHk56i3f6ZX1nTQ5hLasPE+4chiVgB5H9J+HNZzYBN0BVuK4vMz3lj2id/pw5rO +xqSG2HC4yyzQs0gHLaOvKb2iq5+hEOVDrm/e4OdNFY1pXEu6n11pYDHCry1S1DRw +YFUsU8oIUA5EMIUZdSGQfi0jNadah4FmGXXjLuw18ytpuYbbHB43L/gZVcUVwjxX ++s4e2SCp2maFiolgI6ds18vVZ7WCew6WzpmLpB+z1srPW9umoDFGvoh8pQT5coow +tnbxBLufpsqjwWZOtE/jp7a06eDz16V+dE4MpW2mzNIJcaByQbz7YMjluUDOFDHZ +9VgF96IVvvcueVsjFlj11p60JfbGe/UMii5qDyRPLu6XDlwaQSeTby4IUf8EW9OA +z14y60b6hOVfpj6SGBRaxlw/cF4Y1rIDCFQuqMRh+eSyEtmYC7aNTCex3zBD1hus +5MfzSBrLNV8W3e2TjL1BYnmNpe81llQ7NWgAN8nXOv7QNnpI720VozpCGwNnLZnR +qqOgn6oqmbA4aKg7PsWOrSdCJDnpOU0QDBmzdxqTvdp9yDuQS6WfJs6IuPbqAzYl +ZWZQy1YlnJRE7Zq/Qn72r2F7ouArT2yLIpOLrYkCHAQQAQoABgUCU4EgMAAKCRBd +cNLrytLJ5rlHD/9ZFsn6AKiLdQxWPjnfry+R2NSDChutrfXN0033+5XvkLThu377 +tCBxWR6bIomLpjr4UgwQaNAX8t2gxxdd7pfoXE3w87hnb2wzaJmvhjunHFtGaxYw +93kla1JzvZ09drE6q1pefvxssHLh/IbXwOqS+tBoJLcpqXDG4v5b07RTVtQ3H6ON +t3/W2HRJDe9fj9VH0+feG2xlEHJSLoHgix7BivxiDfbQKATqWum/fFNvHB8bOnqF +mk0btX4QFvTAj+Cbo+3eDr3zwO6PVyEa0M3ChYnKZkYtFUXu8weG7WyDInvI4TK1 +JtZns9dz0lQfCwd/24r8bQ4KmAcvgfVdnTUI2BO/mk9IPCVZqF6/Fncnz9fMA/aK +1lKMKQQY+EfkYKUf0vEHzJWSTHxyJAIUgGCMGgHCeHW6GjRgBLSrEu2nh/+i1FBi +FLlHkyZhUp9KO79IQ9D1Bnemd9/7+SUeH+XrGmcGXd/Eko4+5Tm7c3YJEC+bAne9 +4Ey4WZuddQ4zWrJ5SaUTqingfS0AlDjeOt71+kFi1x7Q+9oGhuBvEkSFFB47e73f +VKFVMKqFdjeWUYaA5oPi2lKZx89c3W5lWaxOlhwQ6sQdSdwtPR0G1fNthCQFvwkQ +6zfWNH4bnFjd0xUOmRvlF1ElnCYO88clGAdLTjDVCzbwvl65ImoZrcbKVokCHAQQ +AQoABgUCU6SwOQAKCRB0N3+fakeRn9dvD/4gU5OqbyWqnvte81d89Rt1lclLUDnT +JFabQbjLRyMpGWbgVEJk2F6bUB/rfuHWuqTBa8XLKruyWQL3pZM+PctMyrdHGKSo +rxv/O6ggBEf8dxNuBaDJFVpa7DeSd0k099El0mci/pVRClOc7qoLStsI7LZ9sU+E +7oUDmdwg0lsY1gY26nJeDyTp4c0pUSS9vJKtGcfErpMVcE6SWqyCki9nT4r8u02a +5UnTzu7HSCp2jjx53pFWhd+f0n0wpv7H52mXAqG0GXeLCYo/sYHPxwySXH7EToUV +CvQb5Qc3CQwqIZc9xZzsil42n5pO51X3MKkTJYV9q7DtGm0ECscZ1c6FBkgX+kqU +at6tYNkkcuwAXtmJ5wpfnKvWnMJh+0tLcxhjS8HYxAB1AP9R3VavfOJKsdnlIkgi +db8SLh0d+nDUGHcqZZ8a9Pm5/WG/8IIRehPvs/MZF+lsSk/6Flfxk6i/o3B9nnzj +pLEfDH45k5J3EbBEm4tV/8QLehZ/Yb/qiVGrOzEpCgpIjoQM+2UcWTLtkHfVYf/4 +uoT+6rPGjDaPv6V5WMCWCWBrj6NKFPzYVpu8kzPjA63QWQ2dBLz/rddUf8jKx5Pm +hV6hKk2gflMPy2mhzFr/mAwfJn0VBNI2xYqblqEreeRJXpkmtfJ50XIAU2xS/lPD +ZtiBmwxnqk6Bp4kCHAQSAQIABgUCVMloZgAKCRCBxcxPiDKaHBTKD/0SVqbcdpdq +mmiVta+adRdcEmBxYJFD1oEtpjcHIJBYBptPkIT14jQMO3r7emkzCzGrXsM6t4t+ +FvayGjY9VDbO1RWBFc2M1qwBpQYjIJJrx6ZjJjSKB+bDnOg/Kc8WoDmpbx4x112m +0Qoeq/AuhV9o6UJsGFU/5RddAERqEDFufTBhYVHBD3xlUxVoEhI+YYCry4bw2I9y +6i63krIirlVO8lPiuZDGO6u8E/vIxrec3aFf/fTo2yTqP2u4JiAxxnriZ0rjUUVd +w5bWkcpNDdhUHKAfequd/vgXTV8AzU1QN3ilXK46U1yXMTJaXJg72hfypSKXLRAy +AOdGkzZZC70SzJz05RTHqDclEUnN/BzuXN+XIYqB/M3ftgrP0BxsiPiQZU2TjqHV +AeGpSgekih8DK1qcuPiX0sRfg1BFtFUMI/8FxKYvgdxyhmCNEDbs6RBmdar2qtFY +u0qpXMim2br1Bgc/0XSngBYko5jjkFG5tnHm5hUM2Da/Clj7eNpJ1fj0DHrfdmmj +VVoYD6e2fznL6VAAUGQT3ISbwUa/kLKPrqkLy0b5Vxg/q0Z4tfzWcYJ1YdXBzewo +hSCTpRPmwaMyfsbQ8eWTgJR+alif25tfc0A57n1saHKjb93pPA7jNosjpr6W4tUR +IuAEm4Q4zEAxZkTWMft2yIvNCiRJwtpGVYhGBBMRAgAGBQJSUrSEAAoJECkMEkm9 +2HALgkYAoL9Hez9mLtUeiYsv27TT9fL4mE+RAKCGNS3OO0mBVDAOxcMhRV+lkgG+ +WIheBBARCAAGBQJVYgtfAAoJEH19Eb9inVpnerEBAJ0wIuWRlKqtEtCKOVEboLMD +q/0cBBYfGzu5yTlFjnDZAP4rNy5hiL5mEu5GJqGEY0o9wXNLzJ3bw+kNimI6dy9X +A4kBHAQQAQIABgUCVcQyrgAKCRDHXurc0X7YRErCB/4uDl6B5/rymPi/3AK3LMyJ +bLqZZzErK917s491J+zelFywOoUEWdH+xvUzEOonioTvKkGrQ5Tooy3+7cHojW2q +SauLh+rG+b+73TZJyRSYDD4nwWz3/Wlg21BLinQioaNTgj0pb5Hm70NwQwUcFtvy +JNw/LJ9mfQaxt//OFSF2TRpBMr5MMhs5vd85G5hGHydZw9v0sLRglk5IzkcxNdku +WEG+MpCNBTJs3rkSzCmYSczS1aand3l/3KAwtkau/ru9wtBftrqsbLJZ8Nmv6Ud4 +4nKTF0dsj5hZaLrGbL5oeMfkEuYEZYSXl0CMtIg0wA9OCvk3ZjutMy0+8calRF87 +iQEcBBABAgAGBQJWc8vRAAoJELPrw2Cr+RQDqw4H/2Oj/o3ApVt42KmxOXC5Mcua +aINf3BrTwK0HDzIP+PSmRd3APVVku0Xv89obY/7l4YakI2UTzyvx5hvjRTM5jEOq +m4bd0E1atLo5UIzGtSdgTnPeAbH07beW4UHSG1FCWw35CwYtdyXm9qri9ppWlPKm +Hc91PIwGJTfSoIfWUT6MnCSaPjCed3peTXj4FpW1JeOjDtE3yR8gvmIdIfrI4a8Y +6CGYAUIdVWawNifLahEZjYS2rFcGCssjBSaWR25McL7m8lb/ChpoqpvQry3MaJXo +eOFE7X1zInPda9vDdWR4QFrLDN8JjxzBzwsQcfaA+ypv95SlD3qL6vFpHGHZ4/6J +ARwEEAECAAYFAlZ1TPMACgkQGMawXRQNVOhCaQf/aQZ0xEVW+iBuqXzd65axP3yW +S9dM//h9psP/UKhFzfxCdn3XzmJ92J0sv22DjR8AbbGLP/H9CeZY8nCQnYOHp+GQ +ikGJNjzyd1Zni+Ph67EYfEV2eqRO55GGmiRtUrZaur2pfnbNsvTQtA2rGXen5tLS +sCh4qDNHrM1TlP9MSV0clzoVWRrRNvkODrSDaCdEEDrOqfy0AEFlLmBTqSsduo4c +O46j0ruC0SvflYx+2HN3rVtZzt1wrhaPBPnV6gP7dhKp9XM4erWV40dP14YyDExZ +oKNys7Kq7pnRQMbE3HL6UGa8VPvu9eiELs7kw01pYBtYl1my9ekminj8cygpdYkB +HAQQAQgABgUCVolllwAKCRAjRRsQeqA5QYnjB/9oDZYh20qEpGIZRSmur8M/cGFK +J6IMxBHFIz73PM+hHB3v28aYRW0lXGu8BNGZVxkTuTjd1HlSFMCNpcNfbMmRhEGt +Ep3qGq+cq7zu72lVEiY8tJliq9zyOm+guFzUQ00pvaXuTUFlshvwlRS+GIGn8U2P +/SVRGqSOqCkidp4f06yElt5QifwzvHT8KvxjPgFA5NfQAXE5i/IoepV53XDhECqO +vsORbc0JT8n8/4hT8qHTno8UNbYK5BQjHlby92v7ZFVgI86Li2zb0HgQSmvpU/qR +ibSzg0gEUrWwUR4knTkoKYQwjry2bQ653oNgv0OsnSGEroYOyQ1Q96jOMFKViQEc +BBABCAAGBQJWxLxwAAoJENnYUJL2kkSzPbcH/jl1mYhR4f25pRe1InyR7BJF83YD +hJYIhbBCGqGVenFEy29hco832HkhMUukaos34KZjsWGDFX1IWe6cxOJvBZsDYHua +LCueh5I8/Tmtq+HuebuF0RJtJh7ItJoCrEv7ZyUQmbJ+aHLx2pXSqYUIiWlPvIlG +2/esQlUo7pOub7eEb8U3oKWYgs9HkytMeHSTKiuFJ7mzEyh2fLcgsc2q1XT4Vxuq +ksWxYv8MstTOxrltQ7LyP2QH/BzfqI5yE3UfSSg1sZE2Nh2cIFNWTYVxdx1fBJWG +tTT7l2o99mYwufSLz1UTbGF5PcXeK3sYxN5IJta2FUByaJAWPJonRnojinyJARwE +EAEIAAYFAlf7Qx8ACgkQo/9aebCRiCSTowf+Jm7U7n83AR4MriM1ehGg+QfX9kB3 +jsG1OXgKRpGPIORqxLAniMFGQKP/pqeg2X530HctqjpV+ALG4Ass/kNn4exu5se2 +KuThQMKLK7h7kfqCnrC8ObeCM7X70ny80b2h+749xWZtahpTuQwVrhcAikgPfS2n +XSKdubOyeBH3y0kT2zAoml0MOQsUb6yGycjdnbFrKvfINKfuZvF+z16YOu3eYZ3N +O6dErWQ5iTecuNe0nnn30D8+nWA5JfCxNDPfc0e85dm6xK6GTPdaQd5hpF14TdYZ +u5eT34BXJcmL5hJ6MzM+OFn5CIn2Xa6r6h9AOp5C0o15Qb6SXpUdZrV/34kBHAQQ +AQgABgUCWCj2AQAKCRABFQplW72BAiXGCACSHG54fSeKZysDiX7yUnaUeDf2szdv +egD+OPSVJQhcDdhyC/YnipEN4XFpeIkpxUrBXWYyy5B/ymzDQl95O8vI6TnDpUa+ +bvpkWEAlBK2DuElRojXfPo35ABu0IetQ9xyR+3IzaepHL7Ekf0n0H9vFTmeyYUc3 +B1m7RDwnUJuAlWRt1qQHmOejkzTDBZALeg+BJ5PtnWqCr29+JZB8cwUJ3Ca8Ypbi +CrXWYHu3jlXDDyEhQ73t5OlruOMiYp+opmRySu4rF2d9yJIXnq6uf0WNb6G6JzlV +MOqHKvtmrnwXb9zlFTSXb/NkxNmbYPrTvKmSr09YDC/p9iRkuDSeI/OEiQEcBBAB +CgAGBQJWlDXmAAoJEISlRGJ0Rpv+6/AIAJGPLDwkeCSkBIGwkg5Mtrlc3PNkGsX2 +hb2GP6CUiOeF/UAYU9HcxLv62nK/2qY8o96XY5D/CDOTMmvfr/S2Siyp3u6SVDbE +oj1KX7nTzItfWdk1t/uxfC0+d1zQC0tyJ5O/DHQBDabsZ9REZDqKjhTimilFIWlu +Gov3Hdaa8xkEij9f05REarOBNviaYUxoy9i5Vfo6Uh8jA9XaXw+mS5RIrssa/KlF +fh02wXH5xlExHeepo4g79nFD+lmnE5T9PhfjRnBtogCV3ZBehApS8hJze9JfLnex +7l1DGSPp6ydIyqoWHbk8VYiPMPfHMSlXpaeuprfq8xdBhqMT2a6Fp+KJARwEEgEC +AAYFAlSakYMACgkQlARpDCzjZAx4FAf9GP3vrIvZdZisDqcOoRmKl8iWkY5X3lmx +e5BaQ4qjQ6aUvxsopqLN4ETLTbp8oH9c3sTyshQA0BMtdJFst/ZjhDE9pU90Kel9 +CMbEgq0I5FE5A+348Ovmobe0TUPn2WClwyRGPCe4X0WMEikEHs3Bb1CFzYfbbIe0 +N1M/DqjUvfKv0lc325P7i2DlbDuUoLmNMgHHx6+jFqsxlNCobkq+IrhKLxv27/K3 +13UOzECiPRIbMhHmLHQic9MeJp0bzJiTo1icQVRnim5ZovcpXW2piJQaWqx/TUXG +aRdCjYrJJJZObIi6qnSB7SjdxwJUq6GuTEb/BJElQFnjsxySvTu24YkCGwQQAQIA +BgUCUVSNVAAKCRB+fTNcWi1ewX4xD/d0R2OHFLo42KJPsIc9Wz3AMO7mfpbCmSXc +xoM+Cyd9/GT2qgAt9hgItv3iqg9dj+AbjPNUKfpGG4Q4D/x/tb018C3F4U1PLC/P +Q2lYX0csvuv3Gp5MuNpCuHS5bW4kLyOpRZh1JrqniL8K1Mp8cdBhMf6H+ZckQuXS +hGHwOhGyBMu3X7biXikSvdgQmbDQMtaDbxuYZ+JGXF0uacPVnlAUwW1F55IIhmUH +IV7t+poYo/8M0HJ/lB9y5auamrJT4acsPWS+fYHAjfGfpSE7T7QWuiIKJ2EmpVa5 +hpGhzII9ahF0wtHTKkF7d7RYV1p1UUA5nu8QFTope8fyERJDZg88ICt+TpXJ7+PJ +9THcXgNI+papKy2wKHPfly6B+071BA4n0UX0tV7zqWk9axoN+nyUL97/k572kLTb +xahrBEYXphdNeqqXHa/udWpTYaKwSGYmIohTSIqBZh7Xa/rhLsx2UfgR5B0WW34E +8cTzuiZz//////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////// -////////////////////////////////////////iQIcBBABCAAGBQJX+0LWAAoJ -EAJ4If97HP7GahAQAMxf3Nyab2t+xJlFR+/ZCvqMq5rM8iq67ZK5fLG000RjLiBN -5bd6BglAq03l2DuE3b9hdnosKfU3FCeysivn0af0kxjMaH+W+9JSQJ9E5EjO+RgI -JDkn3n6X/lQjVl3N7R6FeaWY6Ug9paSCtAlVlwCfg/rn2jFIiHQb++44nQFpaX4W -uNzZWoy1SOGg32e624fjsgqB0aH2cmY3oGdMFt8FGuzOfa89JGW8P7mUeZsiQQRx -R4y+L7omQ60rlveKZeEo/ZVfSZUVtzM9wplXpUMbF6/XtUC9dmsVrSZePrsAHnjj -bbk0GBKit2UswC8fKdHVz9YiWKuM4QLEWiucYLkcWcHUFyp1Tk9ZeS3R3yPASC4e -WV72IVGS0mjjolcFwatMfYghQ42+sR+G6duEcJSN7sqrdzYxRny7aYz7GFXv1GCE -iz/CzhepHDROpu9KZv6xetyP4xmaunanzzrd7kM23530jFRK53GJ/4p6XlwYA3jN -sxaGoAADOTIwqolgxtvdrNwEeX0pNpFI85BXSJrvBxKseL4o2NlxxvkyrLPIuuU6 -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 -I4Tesw== -=i53l +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 +keSPmmDrjl8cySCNsMo= +=R0uO -----END PGP PUBLIC KEY BLOCK----- EOF diff --git a/newsfragments/3542.minor b/newsfragments/3542.minor new file mode 100644 index 000000000..e69de29bb From df3ec2a1d48f124f25060f8133ac2264cac11d38 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 30 Nov 2020 17:33:43 -0500 Subject: [PATCH 87/91] Try to trigger image building --- .circleci/config.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index afa3fafa1..a9ce0cc03 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -92,16 +92,16 @@ workflows: <<: *DOCKERHUB_CONTEXT images: - # Build the Docker images used by the ci jobs. This makes the ci jobs - # faster and takes various spurious failures out of the critical path. - triggers: - # Build once a day - - schedule: - cron: "0 0 * * *" - filters: - branches: - only: - - "master" + # # Build the Docker images used by the ci jobs. This makes the ci jobs + # # faster and takes various spurious failures out of the critical path. + # triggers: + # # Build once a day + # - schedule: + # cron: "0 0 * * *" + # filters: + # branches: + # only: + # - "master" jobs: - "build-image-debian-8": From 9ea99726c7d5afbbe91e0c71b4acf6fcca31566d Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 30 Nov 2020 19:18:32 -0500 Subject: [PATCH 88/91] Undo docker image building trigger --- .circleci/config.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a9ce0cc03..afa3fafa1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -92,16 +92,16 @@ workflows: <<: *DOCKERHUB_CONTEXT images: - # # Build the Docker images used by the ci jobs. This makes the ci jobs - # # faster and takes various spurious failures out of the critical path. - # triggers: - # # Build once a day - # - schedule: - # cron: "0 0 * * *" - # filters: - # branches: - # only: - # - "master" + # Build the Docker images used by the ci jobs. This makes the ci jobs + # faster and takes various spurious failures out of the critical path. + triggers: + # Build once a day + - schedule: + cron: "0 0 * * *" + filters: + branches: + only: + - "master" jobs: - "build-image-debian-8": From d50a1151bc5fdf73de4e25c880cd42ccdc1a34b3 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Tue, 1 Dec 2020 09:45:02 -0500 Subject: [PATCH 89/91] verbose is kind of annoying for normal use --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5b9a146fe..c61331885 100644 --- a/tox.ini +++ b/tox.ini @@ -100,7 +100,7 @@ setenv = # entire codebase, including various pieces of supporting code. DEFAULT_FILES=src integration static misc setup.py commands = - flake8 -v {posargs:{env:DEFAULT_FILES}} + flake8 {posargs:{env:DEFAULT_FILES}} python misc/coding_tools/check-umids.py {posargs:{env:DEFAULT_FILES}} python misc/coding_tools/check-debugging.py {posargs:{env:DEFAULT_FILES}} python misc/coding_tools/find-trailing-spaces.py -r {posargs:{env:DEFAULT_FILES}} From 272d6d0aef3989018d41aaad4085824d311143e0 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Tue, 1 Dec 2020 09:52:38 -0500 Subject: [PATCH 90/91] Update developer docs wrt pre-commit --- Makefile | 16 +--------- docs/developer-guide.rst | 67 ++++++---------------------------------- 2 files changed, 11 insertions(+), 72 deletions(-) diff --git a/Makefile b/Makefile index b48e74b0e..f7a357588 100644 --- a/Makefile +++ b/Makefile @@ -13,8 +13,6 @@ MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --no-builtin-rules # Local target variables -VCS_HOOK_SAMPLES=$(wildcard .git/hooks/*.sample) -VCS_HOOKS=$(VCS_HOOK_SAMPLES:%.sample=%) PYTHON=python export PYTHON PYFLAKES=flake8 @@ -31,15 +29,6 @@ TEST_SUITE=allmydata default: @echo "no default target" -.PHONY: install-vcs-hooks -## Install the VCS hooks to run linters on commit and all tests on push -install-vcs-hooks: .git/hooks/pre-commit .git/hooks/pre-push -.PHONY: uninstall-vcs-hooks -## Remove the VCS hooks -uninstall-vcs-hooks: .tox/create-venvs.log - "./$(dir $(<))py36/bin/pre-commit" uninstall || true - "./$(dir $(<))py36/bin/pre-commit" uninstall -t pre-push || true - .PHONY: test ## Run all tests and code reports test: .tox/create-venvs.log @@ -215,7 +204,7 @@ clean: rm -f *.pkg .PHONY: distclean -distclean: clean uninstall-vcs-hooks +distclean: clean rm -rf src/*.egg-info rm -f src/allmydata/_version.py rm -f src/allmydata/_appname.py @@ -261,6 +250,3 @@ src/allmydata/_version.py: .tox/create-venvs.log: tox.ini setup.py tox --notest -p all | tee -a "$(@)" - -$(VCS_HOOKS): .tox/create-venvs.log .pre-commit-config.yaml - "./$(dir $(<))py36/bin/pre-commit" install --hook-type $(@:.git/hooks/%=%) diff --git a/docs/developer-guide.rst b/docs/developer-guide.rst index 2d26e68a4..a44414f8f 100644 --- a/docs/developer-guide.rst +++ b/docs/developer-guide.rst @@ -5,23 +5,17 @@ Developer Guide Pre-commit Checks ----------------- -This project is configured for use with `pre-commit`_ to install `VCS/git hooks`_ which -perform some static code analysis checks and other code checks to catch common errors -before each commit and to run the full self-test suite to find less obvious regressions -before each push to a remote. +This project is configured for use with `pre-commit`_ to install `VCS/git hooks`_ which perform some static code analysis checks and other code checks to catch common errors. +These hooks can be configured to run before commits or pushes For example:: - tahoe-lafs $ make install-vcs-hooks - ... - + ./.tox//py36/bin/pre-commit install --hook-type pre-commit - pre-commit installed at .git/hooks/pre-commit - + ./.tox//py36/bin/pre-commit install --hook-type pre-push + tahoe-lafs $ pre-commit install --hook-type pre-push pre-commit installed at .git/hooks/pre-push - tahoe-lafs $ python -c "import pathlib; pathlib.Path('src/allmydata/tabbed.py').write_text('def foo():\\n\\tpass\\n')" - tahoe-lafs $ git add src/allmydata/tabbed.py + tahoe-lafs $ echo "undefined" > src/allmydata/undefined_name.py + tahoe-lafs $ git add src/allmydata/undefined_name.py tahoe-lafs $ git commit -a -m "Add a file that violates flake8" - ... + tahoe-lafs $ git push codechecks...............................................................Failed - hook id: codechecks - exit code: 1 @@ -30,58 +24,17 @@ For example:: codechecks inst-nodeps: ... codechecks installed: ... codechecks run-test-pre: PYTHONHASHSEED='...' - codechecks run-test: commands[0] | flake8 src static misc setup.py - src/allmydata/tabbed.py:2:1: W191 indentation contains tabs - ERROR: InvocationError for command ./tahoe-lafs/.tox/codechecks/bin/flake8 src static misc setup.py (exited with code 1) + codechecks run-test: commands[0] | flake8 src/allmydata/undefined_name.py + src/allmydata/undefined_name.py:1:1: F821 undefined name 'undefined' + ERROR: InvocationError for command ./tahoe-lafs/.tox/codechecks/bin/flake8 src/allmydata/undefined_name.py (exited with code 1) ___________________________________ summary ____________________________________ ERROR: codechecks: commands failed - ... To uninstall:: - tahoe-lafs $ make uninstall-vcs-hooks - ... - + ./.tox/py36/bin/pre-commit uninstall - pre-commit uninstalled - + ./.tox/py36/bin/pre-commit uninstall -t pre-push + tahoe-lafs $ pre-commit uninstall --hook-type pre-push pre-push uninstalled -Note that running the full self-test suite takes several minutes so expect pushing to -take some time. If you can't or don't want to wait for the hooks in some cases, use the -``--no-verify`` option to ``$ git commit ...`` or ``$ git push ...``. Alternatively, -see the `pre-commit`_ documentation and CLI help output and use the committed -`pre-commit configuration`_ as a starting point to write a local, uncommitted -``../.pre-commit-config.local.yaml`` configuration to use instead. For example:: - - tahoe-lafs $ ./.tox/py36/bin/pre-commit --help - tahoe-lafs $ ./.tox/py36/bin/pre-commit instll --help - tahoe-lafs $ cp "./.pre-commit-config.yaml" "./.pre-commit-config.local.yaml" - tahoe-lafs $ editor "./.pre-commit-config.local.yaml" - ... - tahoe-lafs $ ./.tox/py36/bin/pre-commit install -c "./.pre-commit-config.local.yaml" -t pre-push - pre-commit installed at .git/hooks/pre-push - tahoe-lafs $ git commit -a -m "Add a file that violates flake8" - [3398.pre-commit 29f8f43d2] Add a file that violates flake8 - 1 file changed, 2 insertions(+) - create mode 100644 src/allmydata/tabbed.py - tahoe-lafs $ git push - ... - codechecks...............................................................Failed - - hook id: codechecks - - exit code: 1 - - GLOB sdist-make: ./tahoe-lafs/setup.py - codechecks inst-nodeps: ... - codechecks installed: ... - codechecks run-test-pre: PYTHONHASHSEED='...' - codechecks run-test: commands[0] | flake8 src static misc setup.py - src/allmydata/tabbed.py:2:1: W191 indentation contains tabs - ERROR: InvocationError for command ./tahoe-lafs/.tox/codechecks/bin/flake8 src static misc setup.py (exited with code 1) - ___________________________________ summary ____________________________________ - ERROR: codechecks: commands failed - ... - - error: failed to push some refs to 'github.com:jaraco/tahoe-lafs.git' .. _`pre-commit`: https://pre-commit.com From 587222033d23f5492a80c47deeba5798b1da2955 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 1 Dec 2020 11:58:56 -0500 Subject: [PATCH 91/91] Fix bad merge. --- src/allmydata/introducer/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/allmydata/introducer/client.py b/src/allmydata/introducer/client.py index c143cef8b..f54595221 100644 --- a/src/allmydata/introducer/client.py +++ b/src/allmydata/introducer/client.py @@ -40,6 +40,7 @@ class IntroducerClient(service.Service, Referenceable): self._my_subscriber_info = { b"version": 0, b"nickname": self._nickname, + b"app-versions": [], b"my-version": self._my_version, b"oldest-supported": self._oldest_supported, }