def parse_keywords(lines, multiline_values=True, key_hints=None):
    """
    Utility method to parse name=value pairs (GETINFO etc). Takes a
    string with newline-separated lines and expects at most one = sign
    per line. Accumulates multi-line values.

    :param multiline_values:
        The default is True which allows for multi-line values until a
        line with the next = sign on it. So: '''Foo=bar\nBar'''
        produces one key, 'Foo', with value 'bar\nBar' -- set to
        False, there would be two keys: 'Foo' with value 'bar' and
        'Bar' with value DEFAULT_VALUE.
    """

    rtn = {}
    key = None
    value = ''
    # FIXME could use some refactoring to reduce code duplication!
    for line in lines.split('\n'):
        if line.strip() == 'OK':
            continue

        sp = line.split('=', 1)
        found_key = ('=' in line and ' ' not in sp[0])
        if found_key and key_hints and sp[0] not in key_hints:
            found_key = False
        if found_key:
            if key:
                if key in rtn:
                    if isinstance(rtn[key], list):
                        rtn[key].append(unquote(value))
                    else:
                        rtn[key] = [rtn[key], unquote(value)]
                else:
                    rtn[key] = unquote(value)
            (key, value) = line.split('=', 1)

        else:
            if key is None:
                rtn[line.strip()] = DEFAULT_VALUE

            elif multiline_values is False:
                rtn[key] = value
                rtn[line.strip()] = DEFAULT_VALUE
                key = None
                value = ''

            else:
                value = value + '\n' + line
    if key:
        if key in rtn:
            if isinstance(rtn[key], list):
                rtn[key].append(unquote(value))
            else:
                rtn[key] = [rtn[key], unquote(value)]
        else:
            rtn[key] = unquote(value)
    return rtn DEFAULT_VALUE = 'DEFAULT' class TorProtocolError(RuntimeError):
    """
    Happens on 500-level responses in the protocol, almost certainly
    in an errback chain.

    :ivar code: the actual error code
    :ivar text: other text from the protocol
    """

    def __init__(self, code, text):
        self.code = code
        self.text = text
        super(TorProtocolError, self).__init__(text)

    def __str__(self):
        return str(self.code) + ' ' + self.text class ITorControlProtocol(Interface):
    """
    This defines the API to the TorController object.

    This is the usual entry-point to this library, and you shouldn't
    need to call methods outside this interface.
    """

    def get_info(info):
        """
        :return: a Deferred which will callback with the info keys you
           asked for. For values ones, see control-spec.
        """

    def get_conf(*args):
        """
        Returns one or many configuration values via Deferred. See
        control-spec for valid keys. The value will be a dictionary.
        """

    def signal(signal_name):
        """
        Issues a signal to Tor. See control-spec or .valid_signals for
        which ones are available and their return values.
        """

    def build_circuit(routers):
        """
        Builds a circuit consisting of exactly the routers specified,
        in order.  This issues a series of EXTENDCIRCUIT calls to Tor;
        the deferred returned from this is for the final
        EXTEND. FIXME: should return the Circuit instance, but
        currently returns final extend message 'EXTEND 1234' for
        example.
        """

    def close_circuit(circuit):
        """
        Asks Tor to close the circuit. Note that the Circuit instance
        is only removed as a result of the next CIRC CLOSED event. The
        Deferred returned from this method callbacks when the
        CLOSECIRCUIT command has successfully executed, not when the
        circuit is actually gone.

        If you wish to know when this circuit is actually gone, add an
        ICircuitListener and wait for circuit_closed()
        """

    def add_event_listener(evt, callback):
        """
        Add a listener to an Event object. This may be called multiple
        times for the same event. Every time the event happens, the
        callback method will be called. The callback has one argument
        (a string, the contents of the event, minus the '650' and the
        name of the event)

        FIXME: should have an interface for the callback.
        """ def find_keywords(args, key_filter=lambda x: not x.startswith("$")):
    """
    This splits up strings like name=value, foo=bar into a dict. Does NOT deal
    with quotes in value (e.g. key="value with space" will not work

    By default, note that it takes OUT any key which starts with $ (i.e. a
    single dollar sign) since for many use-cases the way Tor encodes nodes
    with "$hash=name" looks like a keyword argument (but it isn't). If you
    don't want this, override the "key_filter" argument to this method.

    :param args: a list of strings, each with one key=value pair

    :return:
        a dict of key->value (both strings) of all name=value type
        keywords found in args.
    """
    filtered = [x for x in args if '=' in x and key_filter(x.split('=')[0])]
    return dict(x.split('=', 1) for x in filtered) class IOnionClient(IOnionService):
    """
    A single client from a 'parent' IAuthenticatedOnionClients. We do
    this because hidden services can have different URLs and/or
    auth_tokens on a per-client basis. So, the only way to access
    *anything* from an authenticated onion service is to list the
    cleints -- which gives you one IOnionClient per client.

    Note that this inherits from :class:`txtorcon.IOnionService` and
    adds only those attributes required for authentication. For
    'stealth' authentication, the hostnames of each client will be
    unique; for 'basic' authentication the hostname is the same. The
    auth_tokens are always unique -- these are given to clients to
    include using the Tor option `HidServAuth`
    """
    auth_token = Attribute('Some secret bytes')
    name = Attribute('str')  # XXX required? probably.
    parent = Attribute('the IAuthenticatedOnionClients instance who owns me')
    # from the IOnionService base interface, inherits:
    #    hostname
    #    private_key
    #    ports class FilesystemOnionService(object):
    """
    An Onion service whose keys are stored on disk.
    """

    @staticmethod
    @defer.inlineCallbacks
    def create(reactor, config, hsdir, ports,
               version=3,
               group_readable=False,
               progress=None,
               await_all_uploads=None):
        """
        returns a new FilesystemOnionService after adding it to the
        provided config and ensuring at least one of its descriptors
        is uploaded.

        :param config: a :class:`txtorcon.TorConfig` instance

        :param ports: a list of ports to make available; any of these
            can be 2-tuples of (remote, local) if you want to expose a
            particular port locally (otherwise, an available one is
            chosen)

        :param hsdir: the directory in which to store private keys

        :param version: 2 or 3, which kind of service to create

        :param group_readable: if True, the Tor option
            `HiddenServiceDirGroupReadable` is set to 1 for this service

        :param progress: a callable taking (percent, tag, description)
            that is called periodically to report progress.

        :param await_all_uploads: if True, the Deferred only fires
            after ALL descriptor uploads have completed (otherwise, it
            fires when at least one has completed).

        See also :meth:`txtorcon.Tor.create_onion_service` (which
        ultimately calls this).
        """

        # if hsdir is relative, it's "least surprising" (IMO) to make
        # it into a absolute path here -- otherwise, it's relative to
        # whatever Tor's cwd is.
        hsdir = _canonical_hsdir(hsdir)
        processed_ports = yield _validate_ports(reactor, ports)

        fhs = FilesystemOnionService(config, hsdir, processed_ports, version=version, group_readable=group_readable)
        config.HiddenServices.append(fhs)
        # we .save() down below, after setting HS_DESC listener

        # XXX I *hate* this version checking crap. Can we discover a
        # different way if this Tor supports proper HS_DESC stuff? I
        # think part of the problem here is that "some" Tors have
        # HS_DESC event, but it's not .. sufficient?
        uploaded = [None]
        if not version_at_least(config.tor_protocol.version, 0, 2, 7, 2):
            if progress:
                progress(
                    102, "wait_desctiptor",
                    "Adding an onion service to Tor requires at least version"
                )
                progress(
                    103, "wait_desctiptor",
                    "0.2.7.2 so that HS_DESC events work properly and we can"
                )
                progress(
                    104, "wait_desctiptor",
                    "detect our desctiptor being uploaded."
                )
                progress(
                    105, "wait_desctiptor",
                    "Your version is '{}'".format(config.tor_protocol.version),
                )
                progress(
                    106, "wait_desctiptor",
                    "So, we'll just declare it done right now..."
                )
                uploaded[0] = defer.succeed(None)
        else:
            # XXX actually, there's some versions of Tor when v3
            # filesystem services could be added but they didn't send
            # HS_DESC updates -- did any of these actually get
            # released?!
            uploaded[0] = _await_descriptor_upload(config.tor_protocol, fhs, progress, await_all_uploads)

        yield config.save()
        yield uploaded[0]
        defer.returnValue(fhs)

    def __init__(self, config, thedir, ports, version=3, group_readable=0):
        """
        Do not instantiate directly; use
        :func:`txtorcon.onion.FilesystemOnionService.create`
        """
        self._config = config
        self._dir = os.path.realpath(thedir)

        _validate_ports_low_level(ports)
        from .torconfig import _ListWrapper
        self._ports = _ListWrapper(
            ports,
            functools.partial(config.mark_unsaved, 'HiddenServices'),
        )
        self._version = version
        self._group_readable = group_readable
        self._hostname = None
        self._private_key = None

    @property
    def hostname(self):
        if self._hostname is None:
            try:
                with open(os.path.join(self._dir, 'hostname'), 'r') as f:
                    self._hostname = f.read().strip()
            except IOError:
                # not clear under what circumstances this happens
                # (i.e. we can create a new onion, but somehow not
                # read the hostname file) but ... safety?
                self._hostname = None
        return self._hostname

    @property
    def private_key(self):
        # XXX there's also a file called 'hs_ed25519_public_key' but I
        # think we can just ignore that? .. or do we need a v3-only
        # accessor for .public_key() as well?
        if self._private_key is None:
            if self.version == 2:
                try:
                    with open(os.path.join(self._dir, 'private_key'), 'r') as f:
                        self._private_key = f.read().strip()
                except IOError:
                    # not clear under what circumstances this happens
                    # (i.e. we can create a new onion, but somehow not
                    # read the private_key file) but ... safety?
                    self._private_key = None
            elif self.version == 3:
                # XXX see tor bug #20699 -- would be Really Nice to
                # not have to deal with binary data here (well, more
                # for ADD_ONION, but still)
                try:
                    with open(os.path.join(self._dir, 'hs_ed25519_secret_key'), 'rb') as f:
                        self._private_key = f.read().strip()
                except IOError:
                    # not clear under what circumstances this happens
                    # (i.e. we can create a new onion, but somehow not
                    # read the private key file) but ... safety?
                    self._private_key = None
            else:
                raise RuntimeError(
                    "Don't know how to load private_key for version={} "
                    "Onion service".format(self.version)
                )
        return self._private_key

    @property
    def ports(self):
        return self._ports

    @ports.setter
    def ports(self, ports):
        # XXX FIXME need to update Tor's notion of config and/or
        # reject this request after we *have* updated Tor..."or
        # something"
        from .torconfig import _ListWrapper
        self._ports = _ListWrapper(
            ports,
            functools.partial(self._config.mark_unsaved, 'HiddenServices'),
        )
        self._config.mark_unsaved('HiddenServices')

    @property
    def directory(self):
        return self._dir

    @directory.setter
    def directory(self, d):
        self._dir = d
        self._config.mark_unsaved('HiddenServices')

    @property
    def dir(self):
        # deprecated
        return self.directory

    @dir.setter
    def dir(self, d):
        # deprecated
        self.directory = d

    @property
    def group_readable(self):
        return self._group_readable

    @property
    def version(self):
        return self._version

    @version.setter
    def version(self, v):
        self._version = v
        self._config.mark_unsaved('HiddenServices')

    def config_attributes(self):
        """
        Helper method used by TorConfig when generating a torrc file and
        SETCONF commands
        """

        rtn = [('HiddenServiceDir', str(self._dir))]
        if self._config._supports['HiddenServiceDirGroupReadable'] \
           and self.group_readable:
            rtn.append(('HiddenServiceDirGroupReadable', str(1)))
        for x in self.ports:
            rtn.append(('HiddenServicePort', str(x)))
        if self.version:
            rtn.append(('HiddenServiceVersion', str(self.version)))
        return rtn class FilesystemAuthenticatedOnionService(object):
    """
    An Onion service whose keys are stored on disk by Tor and which
    does authentication.
    """

    @staticmethod
    @defer.inlineCallbacks
    def create(reactor, config, hsdir, ports,
               auth=None,
               version=3,
               group_readable=False,
               progress=None,
               await_all_uploads=None):
        """
        returns a new FilesystemAuthenticatedOnionService after adding it
        to the provided config and ensureing at least one of its
        descriptors is uploaded.

        :param config: a :class:`txtorcon.TorConfig` instance

        :param ports: a list of ports to make available; any of these
            can be 2-tuples of (remote, local) if you want to expose a
            particular port locally (otherwise, an available one is
            chosen)

        :param auth: an instance of :class:`txtorcon.AuthBasic` or
            :class:`txtorcon.AuthStealth`

        :param version: 2 or 3, which kind of service to create

        :param group_readable: if True, the Tor option
            `HiddenServiceDirGroupReadable` is set to 1 for this service

        :param progress: a callable taking (percent, tag, description)
            that is called periodically to report progress.

        :param await_all_uploads: if True, the Deferred only fires
            after ALL descriptor uploads have completed (otherwise, it
            fires when at least one has completed).
        """
        # if hsdir is relative, it's "least surprising" (IMO) to make
        # it into a relative path here -- otherwise, it's relative to
        # whatever Tor's cwd is. Issue similar warning to Tor?
        hsdir = _canonical_hsdir(hsdir)
        processed_ports = yield _validate_ports(reactor, ports)

        fhs = FilesystemAuthenticatedOnionService(
            config, hsdir, processed_ports, auth,
            version=version,
            group_readable=group_readable,
        )
        config.HiddenServices.append(fhs)

        def translate_progress(pct, tag, description):
            # XXX fixme actually translate..
            if progress:
                progress(pct, tag, description)

        # most of this code same as non-authenticated version; can we share?
        # we .save() down below, after setting HS_DESC listener
        uploaded = [None]
        if not version_at_least(config.tor_protocol.version, 0, 2, 7, 2):
            translate_progress(
                102, "wait_desctiptor",
                "Adding an onion service to Tor requires at least version"
            )
            translate_progress(
                103, "wait_desctiptor",
                "0.2.7.2 so that HS_DESC events work properly and we can"
            )
            translate_progress(
                104, "wait_desctiptor",
                "detect our desctiptor being uploaded."
            )
            translate_progress(
                105, "wait_desctiptor",
                "Your version is '{}'".format(config.tor_protocol.version),
            )
            translate_progress(
                106, "wait_desctiptor",
                "So, we'll just declare it done right now..."
            )
            uploaded[0] = defer.succeed(None)
        else:
            # XXX actually, there's some versions of Tor when v3
            # filesystem services could be added but they didn't send
            # HS_DESC updates -- did any of these actually get
            # released?!
            uploaded[0] = _await_descriptor_upload(config.tor_protocol, fhs, progress, await_all_uploads)

        yield config.save()
        yield uploaded[0]
        defer.returnValue(fhs)

    def __init__(self, config, thedir, ports, auth, version=3, group_readable=0):
        # XXX do we need version here? probably...
        self._config = config
        self._dir = thedir
        self._ports = ports
        assert auth is not None, "Must provide an auth= instance"
        if not isinstance(auth, (AuthBasic, AuthStealth)):
            raise ValueError("auth= must be one of AuthBasic or AuthStealth")
        self._auth = auth
        # dict: name -> IAuthenticatedOnionClient
        self._clients = None
        self._expected_clients = auth.client_names()
        self._version = version
        self._group_readable = group_readable
        self._client_keys = None

    @property
    def hidden_service_directory(self):
        return self._dir

    @property
    def group_readable(self):
        return self._group_readable

    @property
    def ports(self):
        return self._ports

    @property
    def version(self):
        return self._version

    # basically everything in HiddenService, except the only API we
    # provide is "clients" because there's a separate .onion hostname
    # and authentication token per client.

    def get_permanent_id(self):
        """
        IAuthenticatedOnionClients API
        """
        with open(os.path.join(self._dir, "private_key"), "rb") as f:
            private_key = serialization.load_pem_private_key(
                f.read(),
                password=None,
                backend=default_backend(),
            )
        return _compute_permanent_id(private_key)

    def client_names(self):
        """
        IAuthenticatedOnionClients API
        """
        if self._clients is None:
            self._parse_hostname()
        return self._clients.keys()

    def get_client(self, name):
        """
        IAuthenticatedOnionClients API
        """
        if self._clients is None:
            self._parse_hostname()
        try:
            return self._clients[name]
        except KeyError:
            raise KeyError("No such client '{}'".format(name))

    def add_client(self, name, hostname, ports, token):
        if self._clients is None:
            self._parse_hostname()
        client = FilesystemAuthenticatedOnionServiceClient(
            parent=self,
            name=name,
            hostname=hostname,
            ports=ports, token=token,
        )
        self._clients[client.name] = client
        self._config.HiddenServices.append(client)

    def _private_key(self, name):
        if self._client_keys is None:
            self._parse_client_keys()
        return self._client_keys[name]

    def _parse_client_keys(self):
        try:
            with open(os.path.join(self._dir, 'client_keys'), 'r') as f:
                keys = _parse_client_keys(f)
        except IOError:
            keys = []
        self._client_keys = {}
        for auth in keys:
            self._client_keys[auth.name] = auth

    def _parse_hostname(self):
        clients = {}
        try:
            with open(os.path.join(self._dir, 'hostname')) as f:
                for idx, line in enumerate(f.readlines()):
                    # lines are like: hex.onion hex # client: name
                    m = re.match("(.*) (.*) # client: (.*)", line)
                    hostname, cookie, name = m.groups()
                    # -> for auth'd services we end up with multiple
                    # -> HiddenService instances now (because different
                    # -> hostnames)
                    clients[name] = FilesystemAuthenticatedOnionServiceClient(
                        self, name, hostname,
                        ports=self._ports,
                        token=cookie,
                    )
        except IOError:
            self._clients = dict()
            return

        self._clients = clients
        if self._expected_clients:
            for expected in self._expected_clients:
                if expected not in self._clients:
                    raise RuntimeError(
                        "Didn't find expected client '{}'".format(expected)
                    )

    def config_attributes(self):
        """
        Helper method used by TorConfig when generating a torrc file and
        SETCONF commands
        """

        rtn = [('HiddenServiceDir', str(self._dir))]
        if self._config._supports['HiddenServiceDirGroupReadable'] \
           and self.group_readable:
            rtn.append(('HiddenServiceDirGroupReadable', str(1)))
        for port in self.ports:
            rtn.append(('HiddenServicePort', str(port)))
        if self._version:
            rtn.append(('HiddenServiceVersion', str(self._version)))
        if self._clients:
            rtn.append((
                'HiddenServiceAuthorizeClient',
                "{} {}".format(self._auth.auth_type, ','.join(self.client_names()))
            ))
        else:
            rtn.append((
                'HiddenServiceAuthorizeClient',
                "{} {}".format(self._auth.auth_type, ','.join(self._expected_clients))
            ))
        return rtn DISCARD = object() class AuthStealth(_AuthCommon):
    """
    Authentication details for 'stealth' auth.
    """
    auth_type = 'stealth'
    # note that _AuthCommon.__init__ takes 'clients' class AuthBasic(_AuthCommon):
    """
    Authentication details for 'basic' auth.
    """
    auth_type = 'basic'
    # note that _AuthCommon.__init__ takes 'clients' class EphemeralOnionService(object):
    """
    An Onion service whose keys live in memory and are not persisted
    by Tor.

    It is up to the application developer to retrieve and store the
    private key if this service is ever to be brought online again.
    """

    @staticmethod
    @defer.inlineCallbacks
    def create(reactor, config, ports,
               detach=False,
               private_key=None,  # or DISCARD
               version=None,
               progress=None,
               await_all_uploads=None,
               single_hop=False):
        """
        returns a new EphemeralOnionService after adding it to the
        provided config and ensuring at least one of its descriptors
        is uploaded.

        :param config: a :class:`txtorcon.TorConfig` instance

        :param ports: a list of ports to make available; any of these
            can be 2-tuples of (remote, local) if you want to expose a
            particular port locally (otherwise, an available one is
            chosen)

        :param private_key: None, `DISCARD`, or a private key blob

        :param detach: if True, tell Tor to NOT associate this control
            connection with the lifetime of the created service

        :param version: 2 or 3, which kind of service to create

        :param progress: a callable taking (percent, tag, description)
            that is called periodically to report progress.

        :param await_all_uploads: if True, the Deferred only fires
            after ALL descriptor uploads have completed (otherwise, it
            fires when at least one has completed).

        :param single_hop: if True, pass the `NonAnonymous` flag. Note
            that Tor options `HiddenServiceSingleHopMode`,
            `HiddenServiceNonAnonymousMode` must be set to `1` and there
            must be no `SOCKSPort` configured for this to actually work.

        See also :meth:`txtorcon.Tor.create_onion_service` (which
        ultimately calls this).
        """
        version = 2 if version is None else version
        assert version in (2, 3)

        processed_ports = yield _validate_ports(reactor, ports)

        onion = EphemeralOnionService(
            config, processed_ports,
            hostname=None,
            private_key=private_key,
            detach=detach,
            version=version,
            await_all_uploads=await_all_uploads,
            single_hop=single_hop,
        )

        yield _add_ephemeral_service(config, onion, progress, version, None, await_all_uploads)

        defer.returnValue(onion)

    def __init__(self, config, ports, hostname=None, private_key=None, version=3,
                 detach=False, await_all_uploads=None, single_hop=None, **kwarg):
        """
        Users should create instances of this class by using the async
        method :meth:`txtorcon.EphemeralOnionService.create`
        """

        # prior to 17.0.0, this took an argument called "ver" instead
        # of "version". So, we will silently upgrade that.
        if "ver" in kwarg:
            version = int(kwarg.pop("ver"))
        # any other kwargs are illegal
        if len(kwarg):
            raise ValueError(
                "Unknown kwargs: {}".format(", ".join(kwarg.keys()))
            )

        _validate_ports_low_level(ports)

        self._config = config
        self._ports = ports
        self._hostname = hostname
        self._private_key = private_key
        self._version = version
        self._detach = detach
        self._single_hop = single_hop

    # not putting an "add_to_tor" method here; that class is now
    # deprecated and you add one of these by using .create()

    @defer.inlineCallbacks
    def remove(self):
        """
        Issues a DEL_ONION call to our tor, removing this service.
        """
        cmd = 'DEL_ONION {}'.format(self._hostname[:-len('.onion')])
        res = yield self._config.tor_protocol.queue_command(cmd)
        if res.strip() != "OK":
            raise RuntimeError("Failed to remove service")

    @property
    def ports(self):
        return set(self._ports)

    @property
    def version(self):
        return self._version

    @property
    def hostname(self):
        return self._hostname

    @property
    def private_key(self):
        return self._private_key @defer.inlineCallbacks
def _await_descriptor_upload(tor_protocol, onion, progress, await_all_uploads):
    """
    Internal helper.

    :param tor_protocol: ITorControlProtocol instance

    :param onion: IOnionService instance

    :param progress: a progess callback, or None

    :returns: a Deferred that fires once we've detected at least one
        descriptor upload for the service (as detected by listening for
        HS_DESC events)
    """
    # For v3 services, Tor attempts to upload to 16 services; we'll
    # assume that for now but also cap it (we want to show some
    # progress for "attempting uploads" but we need to decide how
    # much) .. so we leave 50% of the "progress" for attempts, and the
    # other 50% for "are we done" (which is either "one thing
    # uploaded" or "all the things uploaded")
    attempted_uploads = set()
    confirmed_uploads = set()
    failed_uploads = set()
    uploaded = defer.Deferred()
    await_all = False if await_all_uploads is None else await_all_uploads

    def translate_progress(tag, description):
        if progress:
            done = len(confirmed_uploads) + len(failed_uploads)
            done_endpoint = float(len(attempted_uploads)) if await_all else 1.0
            done_pct = 0 if not attempted_uploads else float(done) / done_endpoint
            started_pct = float(min(16, len(attempted_uploads))) / 16.0
            try:
                progress(
                    (done_pct * 50.0) + (started_pct * 50.0),
                    tag,
                    description,
                )
            except Exception:
                log.err()

    def hostname_matches(hostname):
        if IAuthenticatedOnionClients.providedBy(onion):
            return hostname[:-6] == onion.get_permanent_id()
        else:
            # provides IOnionService
            return onion.hostname == hostname

    def hs_desc(evt):
        """
        From control-spec:
        "650" SP "HS_DESC" SP Action SP HSAddress SP AuthType SP HsDir
        [SP DescriptorID] [SP "REASON=" Reason] [SP "REPLICA=" Replica]
        """
        args = evt.split()
        subtype = args[0]
        if subtype == 'UPLOAD':
            if hostname_matches('{}.onion'.format(args[1])):
                attempted_uploads.add(args[3])
                translate_progress(
                    "wait_descriptor",
                    "Upload to {} started".format(args[3])
                )

        elif subtype == 'UPLOADED':
            # we only need ONE successful upload to happen for the
            # HS to be reachable.
            # unused? addr = args[1]

            # XXX FIXME I think tor is sending the onion-address
            # properly with these now, so we can use those
            # (i.e. instead of matching to "attempted_uploads")
            if args[3] in attempted_uploads:
                confirmed_uploads.add(args[3])
                log.msg("Uploaded '{}' to '{}'".format(args[1], args[3]))
                translate_progress(
                    "wait_descriptor",
                    "Successful upload to {}".format(args[3])
                )
                if not uploaded.called:
                    if await_all:
                        if (len(failed_uploads) + len(confirmed_uploads)) == len(attempted_uploads):
                            uploaded.callback(onion)
                    else:
                        uploaded.callback(onion)

        elif subtype == 'FAILED':
            if hostname_matches('{}.onion'.format(args[1])):
                failed_uploads.add(args[3])
                translate_progress(
                    "wait_descriptor",
                    "Failed upload to {}".format(args[3])
                )
                if failed_uploads == attempted_uploads:
                    msg = "Failed to upload '{}' to: {}".format(
                        args[1],
                        ', '.join(failed_uploads),
                    )
                    uploaded.errback(RuntimeError(msg))

    # the first 'yield' should be the add_event_listener so that a
    # caller can do "d = _await_descriptor_upload()", then add the
    # service.
    yield tor_protocol.add_event_listener('HS_DESC', hs_desc)
    yield uploaded
    yield tor_protocol.remove_event_listener('HS_DESC', hs_desc)
    # ensure we show "100%" at the end
    if progress:
        if await_all_uploads:
            msg = "Completed descriptor uploads"
        else:
            msg = "At least one descriptor uploaded"
        try:
            progress(100.0, "wait_descriptor", msg)
        except Exception:
            log.err() def _parse_client_keys(self):
    try:
        with open(os.path.join(self._dir, 'client_keys'), 'r') as f:
            keys = _parse_client_keys(f)
    except IOError:
        keys = []
    self._client_keys = {}
    for auth in keys:
        self._client_keys[auth.name] = auth class _Version(object):
    """
    Replacement for incremental.Version until
    https://github.com/meejah/txtorcon/issues/233 and/or
    https://github.com/hawkowl/incremental/issues/31 is fixed.
    """
    # as of latest incremental, it should only access .package and
    # .short() via the getVersionString() method that Twisted's
    # deprecated() uses...

    def __init__(self, package, major, minor, patch):
        self.package = package
        self.major = major
        self.minor = minor
        self.patch = patch

    def short(self):
        return '{}.{}.{}'.format(self.major, self.minor, self.patch)
import os
import re
import six
import functools
import warnings
from io import StringIO
from warnings import warn
    from collections.abc import OrderedDict
    from collections import OrderedDict
from twisted.python import log
from twisted.python.compat import nativeString
from twisted.python.deprecate import deprecated
from twisted.internet import defer
from twisted.internet.endpoints import TCP4ClientEndpoint, UNIXClientEndpoint
from txtorcon.torcontrolprotocol import parse_keywords, DEFAULT_VALUE
from txtorcon.torcontrolprotocol import TorProtocolError
from txtorcon.interface import ITorControlProtocol
from txtorcon.util import find_keywords
from .onion import IOnionClient, FilesystemOnionService, FilesystemAuthenticatedOnionService
from .onion import DISCARD
from .onion import AuthStealth, AuthBasic
from .onion import EphemeralOnionService
from .onion import _await_descriptor_upload
from .onion import _parse_client_keys
from .util import _Version
    from .controller import launch
        return name

    @defer.inlineCallbacks
    def _get_defaults(self):
        try:
            defaults_raw = yield self.protocol.get_info_raw("config/defaults")
            defaults = {}
            for line in defaults_raw.split('\n')[1:]:
                k, v = line.split(' ', 1)
                if k in defaults:
                    if isinstance(defaults[k], list):
                        defaults[k].append(v)
                    else:
                        defaults[k] = [defaults[k], v]
                else:
                    defaults[k] = v
        except TorProtocolError:
            # must be a version of Tor without config/defaults
            defaults = dict()
        defer.returnValue(defaults)

    @defer.inlineCallbacks
    def _do_setup(self, data):
        defaults = self.__dict__['_defaults'] = yield self._get_defaults()

        for line in data.split('\n'):
            if line == "config/names=":
                continue

            (name, value) = line.split()
            if name in self._supports:
                self._supports[name] = True

            if name == 'HiddenServiceOptions':
                # set up the "special-case" hidden service stuff
                servicelines = yield self.protocol.get_conf_raw(
                    'HiddenServiceOptions')
                self._setup_hidden_services(servicelines)
                continue

            # there's a whole bunch of FooPortLines (where "Foo" is
            # "Socks", "Control", etc) and some have defaults, some
            # don't but they all have FooPortLines, FooPort, and
            # __FooPort definitions so we only "do stuff" for the
            # "FooPortLines"
            if name.endswith('PortLines'):
                rn = self._find_real_name(name[:-5])
                self.parsers[rn] = String()  # not Port() because options etc
                self.list_parsers.add(rn)
                v = yield self.protocol.get_conf(name[:-5])
                v = v[name[:-5]]

                initial = []
                if v == DEFAULT_VALUE or v == 'auto':
                    try:
                        initial = defaults[name[:-5]]
                    except KeyError:
                        default_key = '__{}'.format(name[:-5])
                        default = yield self.protocol.get_conf_single(default_key)
                        if not default:
                            initial = []
                        else:
                            initial = [default]
                else:
                    initial = [self.parsers[rn].parse(v)]
                self.config[rn] = _ListWrapper(
                    initial, functools.partial(self.mark_unsaved, rn))

            # XXX for Virtual check that it's one of the *Ports things
            # (because if not it should be an error)
            if value in ('Dependant', 'Dependent', 'Virtual'):
                continue

            # there's a thing called "Boolean+Auto" which is -1 for
            # auto, 0 for false and 1 for true. could be nicer if it
            # was called AutoBoolean or something, but...
            value = value.replace('+', '_')

            inst = None
            # FIXME: put parser classes in dict instead?
            for cls in config_types:
                if cls.__name__ == value:
                    inst = cls()
            if not inst:
                raise RuntimeError("Don't have a parser for: " + value)
            v = yield self.protocol.get_conf(name)
            v = v[name]

            rn = self._find_real_name(name)
            self.parsers[rn] = inst
            if is_list_config_type(inst.__class__):
                self.list_parsers.add(rn)
                parsed = self.parsers[rn].parse(v)
                if parsed == [DEFAULT_VALUE]:
                    parsed = defaults.get(rn, [])
                self.config[rn] = _ListWrapper(
                    parsed, functools.partial(self.mark_unsaved, rn))

            else:
                if v == '' or v == DEFAULT_VALUE:
                    parsed = self.parsers[rn].parse(defaults.get(rn, DEFAULT_VALUE))
                else:
                    parsed = self.parsers[rn].parse(v)
                self.config[rn] = parsed

        # get any ephemeral services we own, or detached services.
        # these are *not* _ListWrappers because we don't care if they
        # change, nothing in Tor's config exists for these (probably
        # begging the question: why are we putting them in here at all
        # then...?)
        try:
            ephemeral = yield self.protocol.get_info('onions/current')
        except Exception:
            self.config['EphemeralOnionServices'] = []
        else:
            onions = []
            for line in ephemeral['onions/current'].split('\n'):
                onion = line.strip()
                if onion:
                    onions.append(

                        EphemeralOnionService(