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.
        """ class TorProcessProtocol(protocol.ProcessProtocol):

    def __init__(self, connection_creator, progress_updates=None, config=None,
                 ireactortime=None, timeout=None, kill_on_stderr=True,
                 stdout=None, stderr=None):
        """
        This will read the output from a Tor process and attempt a
        connection to its control port when it sees any 'Bootstrapped'
        message on stdout. You probably don't need to use this
        directly except as the return value from the
        :func:`txtorcon.launch_tor` method. tor_protocol contains a
        valid :class:`txtorcon.TorControlProtocol` instance by that
        point.

        connection_creator is a callable that should return a Deferred
        that callbacks with a :class:`txtorcon.TorControlProtocol`;
        see :func:`txtorcon.launch_tor` for the default one which is a
        functools.partial that will call
        ``connect(TorProtocolFactory())`` on an appropriate
        :api:`twisted.internet.endpoints.TCP4ClientEndpoint`

        :param connection_creator: A no-parameter callable which
            returns a Deferred which promises a
            :api:`twisted.internet.interfaces.IStreamClientEndpoint
            <IStreamClientEndpoint>`. If this is None, we do NOT
            attempt to connect to the underlying Tor process.

        :param progress_updates: A callback which received progress
            updates with three args: percent, tag, summary

        :param config: a TorConfig object to connect to the
            TorControlProtocl from the launched tor (should it succeed)

        :param ireactortime:
            An object implementing IReactorTime (i.e. a reactor) which
            needs to be supplied if you pass a timeout.

        :param timeout:
            An int representing the timeout in seconds. If we are
            unable to reach 100% by this time we will consider the
            setting up of Tor to have failed. Must supply ireactortime
            if you supply this.

        :param kill_on_stderr:
            When True, kill subprocess if we receive anything on stderr

        :param stdout:
            Anything subprocess writes to stdout is sent to .write() on this

        :param stderr:
            Anything subprocess writes to stderr is sent to .write() on this

        :ivar tor_protocol: The TorControlProtocol instance connected
            to the Tor this
            :api:`twisted.internet.protocol.ProcessProtocol
            <ProcessProtocol>`` is speaking to. Will be valid after
            the Deferred returned from
            :meth:`TorProcessProtocol.when_connected` is triggered.
        """

        self.config = config
        self.tor_protocol = None
        self.progress_updates = progress_updates

        # XXX if connection_creator is not None .. is connected_cb
        # tied to connection_creator...?
        if connection_creator:
            self.connection_creator = connection_creator
        else:
            self.connection_creator = None
        # use SingleObserver
        self._connected_listeners = []  # list of Deferred (None when we're connected)

        self.attempted_connect = False
        self.to_delete = []
        self.kill_on_stderr = kill_on_stderr
        self.stderr = stderr
        self.stdout = stdout
        self.collected_stdout = StringIO()

        self._setup_complete = False
        self._did_timeout = False
        self._timeout_delayed_call = None
        self._on_exit = []  # Deferred's we owe a call/errback to when we exit
        if timeout:
            if not ireactortime:
                raise RuntimeError(
                    'Must supply an IReactorTime object when supplying a '
                    'timeout')
            ireactortime = IReactorTime(ireactortime)
            self._timeout_delayed_call = ireactortime.callLater(
                timeout, self._timeout_expired)

    def when_connected(self):
        if self._connected_listeners is None:
            return succeed(self)
        d = Deferred()
        self._connected_listeners.append(d)
        return d

    def _maybe_notify_connected(self, arg):
        """
        Internal helper.

        .callback or .errback on all Deferreds we've returned from
        `when_connected`
        """
        if self._connected_listeners is None:
            return
        for d in self._connected_listeners:
            # Twisted will turn this into an errback if "arg" is a
            # Failure
            d.callback(arg)
        self._connected_listeners = None

    def quit(self):
        """
        This will terminate (with SIGTERM) the underlying Tor process.

        :returns: a Deferred that callback()'s (with None) when the
            process has actually exited.
        """

        try:
            self.transport.signalProcess('TERM')
            d = Deferred()
            self._on_exit.append(d)

        except error.ProcessExitedAlready:
            self.transport.loseConnection()
            d = succeed(None)
        except Exception:
            d = fail()
        return d

    def _signal_on_exit(self, reason):
        to_notify = self._on_exit
        self._on_exit = []
        for d in to_notify:
            d.callback(None)

    def outReceived(self, data):
        """
        :api:`twisted.internet.protocol.ProcessProtocol <ProcessProtocol>` API
        """

        if self.stdout:
            self.stdout.write(data.decode('ascii'))

        # minor hack: we can't try this in connectionMade because
        # that's when the process first starts up so Tor hasn't
        # opened any ports properly yet. So, we presume that after
        # its first output we're good-to-go. If this fails, we'll
        # reset and try again at the next output (see this class'
        # tor_connection_failed)
        txtorlog.msg(data)
        if not self.attempted_connect and self.connection_creator \
                and b'Opening Control listener' in data:
            self.attempted_connect = True
            # hmmm, we don't "do" anything with this Deferred?
            # (should it be connected to the when_connected
            # Deferreds?)
            d = self.connection_creator()
            d.addCallback(self._tor_connected)
            d.addErrback(self._tor_connection_failed)
# XXX 'should' be able to improve the error-handling by directly tying
# this Deferred into the notifications -- BUT we might try again, so
# we need to know "have we given up -- had an error" and only in that
# case send to the connected things. I think?
#            d.addCallback(self._maybe_notify_connected)

    def _timeout_expired(self):
        """
        A timeout was supplied during setup, and the time has run out.
        """
        self._did_timeout = True
        try:
            self.transport.signalProcess('TERM')
        except error.ProcessExitedAlready:
            # XXX why don't we just always do this?
            self.transport.loseConnection()

        fail = Failure(RuntimeError("timeout while launching Tor"))
        self._maybe_notify_connected(fail)

    def errReceived(self, data):
        """
        :api:`twisted.internet.protocol.ProcessProtocol <ProcessProtocol>` API
        """

        if self.stderr:
            self.stderr.write(data)

        if self.kill_on_stderr:
            self.transport.loseConnection()
            raise RuntimeError(
                "Received stderr output from slave Tor process: " + data.decode('utf8')
            )

    def cleanup(self):
        """
        Clean up my temporary files.
        """

        all([delete_file_or_tree(f) for f in self.to_delete])
        self.to_delete = []

    def processExited(self, reason):
        self._signal_on_exit(reason)

    def processEnded(self, status):
        """
        :api:`twisted.internet.protocol.ProcessProtocol <ProcessProtocol>` API
        """
        self.cleanup()

        if status.value.exitCode is None:
            if self._did_timeout:
                err = RuntimeError("Timeout waiting for Tor launch.")
            else:
                err = RuntimeError(
                    "Tor was killed (%s)." % status.value.signal)
        else:
            err = RuntimeError(
                "Tor exited with error-code %d" % status.value.exitCode)

        # hmmm, this log() should probably go away...not always an
        # error (e.g. .quit()
        log.err(err)
        self._maybe_notify_connected(Failure(err))

    def progress(self, percent, tag, summary):
        """
        Can be overridden or monkey-patched if you want to get
        progress updates yourself.
        """

        if self.progress_updates:
            self.progress_updates(percent, tag, summary)

    # the below are all callbacks

    def _tor_connection_failed(self, failure):
        # FIXME more robust error-handling please, like a timeout so
        # we don't just wait forever after 100% bootstrapped (that
        # is, we're ignoring these errors, but shouldn't do so after
        # we'll stop trying)
        # XXX also, should check if the failure is e.g. a syntax error
        # or an actually connection failure

        # okay, so this is a little trickier than I thought at first:
        # we *can* just relay this back to the
        # connection_creator()-returned Deferred, *but* we don't know
        # if this is "the last" error and we're going to try again
        # (and thus e.g. should fail all the when_connected()
        # Deferreds) or not.
        log.err(failure)
        self.attempted_connect = False
        return None

    def _status_client(self, arg):
        args = shlex.split(arg)
        if args[1] != 'BOOTSTRAP':
            return

        kw = find_keywords(args)
        prog = int(kw['PROGRESS'])
        tag = kw['TAG']
        summary = kw['SUMMARY']
        self.progress(prog, tag, summary)

        if prog == 100:
            if self._timeout_delayed_call:
                self._timeout_delayed_call.cancel()
                self._timeout_delayed_call = None
            self._maybe_notify_connected(self)

    @inlineCallbacks
    def _tor_connected(self, proto):
        txtorlog.msg("tor_connected %s" % proto)

        self.tor_protocol = proto
        self.tor_protocol.is_owned = self.transport.pid

        yield self.tor_protocol.post_bootstrap
        txtorlog.msg("Protocol is bootstrapped")
        yield self.tor_protocol.add_event_listener('STATUS_CLIENT', self._status_client)
        yield self.tor_protocol.queue_command('TAKEOWNERSHIP')
        yield self.tor_protocol.queue_command('RESETCONF __OwningControllerProcess')
        if self.config is not None and self.config.protocol is None:
            yield self.config.attach_protocol(proto)
        returnValue(self)  # XXX or "proto"? class TorConfig(object):
    """This class abstracts out Tor's config, and can be used both to
    create torrc files from nothing and track live configuration of a Tor
    instance.

    Also, it gives easy access to all the configuration options
    present. This is initialized at "bootstrap" time, providing
    attribute-based access thereafter. Note that after you set some
    number of items, you need to do a save() before these are sent to
    Tor (and then they will be done as one SETCONF).

    You may also use this class to construct a configuration from
    scratch (e.g. to give to :func:`txtorcon.launch_tor`). In this
    case, values are reflected right away. (If we're not bootstrapped
    to a Tor, this is the mode).

    Note that you do not need to call save() if you're just using
    TorConfig to create a .torrc file or for input to launch_tor().

    This class also listens for CONF_CHANGED events to update the
    cached data in the event other controllers (etc) changed it.

    There is a lot of magic attribute stuff going on in here (which
    might be a bad idea, overall) but the *intent* is that you can
    just set Tor options and it will all Just Work. For config items
    that take multiple values, set that to a list. For example::

        conf = TorConfig(...)
        conf.SOCKSPort = [9050, 1337]
        conf.HiddenServices.append(HiddenService(...))

    (Incoming objects, like lists, are intercepted and wrapped).

    FIXME: when is CONF_CHANGED introduced in Tor? Can we do anything
    like it for prior versions?

    FIXME:

        - HiddenServiceOptions is special: GETCONF on it returns
          several (well, two) values. Besides adding the two keys 'by
          hand' do we need to do anything special? Can't we just depend
          on users doing 'conf.hiddenservicedir = foo' AND
          'conf.hiddenserviceport = bar' before a save() ?

        - once I determine a value is default, is there any way to
          actually get what this value is?

    """

    @staticmethod
    @defer.inlineCallbacks
    def from_protocol(proto):
        """
        This creates and returns a ready-to-go TorConfig instance from the
        given protocol, which should be an instance of
        TorControlProtocol.
        """
        cfg = TorConfig(control=proto)
        yield cfg.post_bootstrap
        defer.returnValue(cfg)

    def __init__(self, control=None):
        self.config = {}
        '''Current configuration, by keys.'''

        if control is None:
            self._protocol = None
            self.__dict__['_accept_all_'] = None

        else:
            self._protocol = ITorControlProtocol(control)

        self.unsaved = OrderedDict()
        '''Configuration that has been changed since last save().'''

        self.parsers = {}
        '''Instances of the parser classes, subclasses of TorConfigType'''

        self.list_parsers = set(['hiddenservices', 'ephemeralonionservices'])
        '''All the names (keys from .parsers) that are a List of something.'''

        # during bootstrapping we decide whether we support the
        # following features. A thing goes in here if TorConfig
        # behaves differently depending upon whether it shows up in
        # "GETINFO config/names"
        self._supports = dict(
            HiddenServiceDirGroupReadable=False
        )
        self._defaults = dict()

        self.post_bootstrap = defer.Deferred()
        if self.protocol:
            if self.protocol.post_bootstrap:
                self.protocol.post_bootstrap.addCallback(
                    self.bootstrap).addErrback(self.post_bootstrap.errback)
            else:
                self.bootstrap()

        else:
            self.do_post_bootstrap(self)

        self.__dict__['_setup_'] = None

    def socks_endpoint(self, reactor, port=None):
        """
        Returns a TorSocksEndpoint configured to use an already-configured
        SOCKSPort from the Tor we're connected to. By default, this
        will be the very first SOCKSPort.

        :param port: a str, the first part of the SOCKSPort line (that
            is, a port like "9151" or a Unix socket config like
            "unix:/path". You may also specify a port as an int.

        If you need to use a particular port that may or may not
        already be configured, see the async method
        :meth:`txtorcon.TorConfig.create_socks_endpoint`
        """

        if len(self.SocksPort) == 0:
            raise RuntimeError(
                "No SOCKS ports configured"
            )

        socks_config = None
        if port is None:
            socks_config = self.SocksPort[0]
        else:
            port = str(port)  # in case e.g. an int passed in
            if ' ' in port:
                raise ValueError(
                    "Can't specify options; use create_socks_endpoint instead"
                )

            for idx, port_config in enumerate(self.SocksPort):
                # "SOCKSPort" is a gnarly beast that can have a bunch
                # of options appended, so we have to split off the
                # first thing which *should* be the port (or can be a
                # string like 'unix:')
                if port_config.split()[0] == port:
                    socks_config = port_config
                    break
        if socks_config is None:
            raise RuntimeError(
                "No SOCKSPort configured for port {}".format(port)
            )

        return _endpoint_from_socksport_line(reactor, socks_config)

    @defer.inlineCallbacks
    def create_socks_endpoint(self, reactor, socks_config):
        """
        Creates a new TorSocksEndpoint instance given a valid
        configuration line for ``SocksPort``; if this configuration
        isn't already in the underlying tor, we add it. Note that this
        method may call :meth:`txtorcon.TorConfig.save()` on this instance.

        Note that calling this with `socks_config=None` is equivalent
        to calling `.socks_endpoint` (which is not async).

        XXX socks_config should be .. i dunno, but there's fucking
        options and craziness, e.g. default Tor Browser Bundle is:
        ['9150 IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth',
        '9155']

        XXX maybe we should say "socks_port" as the 3rd arg, insist
        it's an int, and then allow/support all the other options
        (e.g. via kwargs)

        XXX we could avoid the "maybe call .save()" thing; worth it?
        (actually, no we can't or the Tor won't have it config'd)
        """

        yield self.post_bootstrap

        if socks_config is None:
            if len(self.SocksPort) == 0:
                raise RuntimeError(
                    "socks_port is None and Tor has no SocksPorts configured"
                )
            socks_config = self.SocksPort[0]
        else:
            if not any([socks_config in port for port in self.SocksPort]):
                # need to configure Tor
                self.SocksPort.append(socks_config)
                try:
                    yield self.save()
                except TorProtocolError as e:
                    extra = ''
                    if socks_config.startswith('unix:'):
                        # XXX so why don't we check this for the
                        # caller, earlier on?
                        extra = '\nNote Tor has specific ownership/permissions ' +\
                                'requirements for unix sockets and parent dir.'
                    raise RuntimeError(
                        "While configuring SOCKSPort to '{}', error from"
                        " Tor: {}{}".format(
                            socks_config, e, extra
                        )
                    )

        defer.returnValue(
            _endpoint_from_socksport_line(reactor, socks_config)
        )

    # FIXME should re-name this to "tor_protocol" to be consistent
    # with other things? Or rename the other things?
    """
    read-only access to TorControlProtocol. Call attach_protocol() to
    set it, which can only be done if we don't already have a
    protocol.
    """
    def _get_protocol(self):
        return self.__dict__['_protocol']
    protocol = property(_get_protocol)
    tor_protocol = property(_get_protocol)

    def attach_protocol(self, proto):
        """
        returns a Deferred that fires once we've set this object up to
        track the protocol. Fails if we already have a protocol.
        """
        if self._protocol is not None:
            raise RuntimeError("Already have a protocol.")
        # make sure we have nothing in self.unsaved
        self.save()
        self.__dict__['_protocol'] = proto

        # FIXME some of this is duplicated from ctor
        del self.__dict__['_accept_all_']
        self.__dict__['post_bootstrap'] = defer.Deferred()
        if proto.post_bootstrap:
            proto.post_bootstrap.addCallback(self.bootstrap)
        return self.__dict__['post_bootstrap']

    def __setattr__(self, name, value):
        """
        we override this so that we can provide direct attribute
        access to our config items, and move them into self.unsaved
        when they've been changed. hiddenservices have to be special
        unfortunately. the _setup_ thing is so that we can set up the
        attributes we need in the constructor without uusing __dict__
        all over the place.
        """

        # appease flake8's hatred of lambda :/
        def has_setup_attr(o):
            return '_setup_' in o.__dict__

        def has_accept_all_attr(o):
            return '_accept_all_' in o.__dict__

        def is_hidden_services(s):
            return s.lower() == "hiddenservices"

        if has_setup_attr(self):
            name = self._find_real_name(name)
            if not has_accept_all_attr(self) and not is_hidden_services(name):
                value = self.parsers[name].validate(value, self, name)
            if isinstance(value, list):
                value = _ListWrapper(
                    value, functools.partial(self.mark_unsaved, name))

            name = self._find_real_name(name)
            self.unsaved[name] = value

        else:
            super(TorConfig, self).__setattr__(name, value)

    def _maybe_create_listwrapper(self, rn):
        if rn.lower() in self.list_parsers and rn not in self.config:
            self.config[rn] = _ListWrapper([], functools.partial(
                self.mark_unsaved, rn))

    def __getattr__(self, name):
        """
        on purpose, we don't return self.unsaved if the key is in there
        because I want the config to represent the running Tor not
        ``things which might get into the running Tor if save() were
        to be called''
        """
        rn = self._find_real_name(name)
        if '_accept_all_' in self.__dict__ and rn in self.unsaved:
            return self.unsaved[rn]
        self._maybe_create_listwrapper(rn)
        v = self.config[rn]
        if v == DEFAULT_VALUE:
            v = self.__dict__['_defaults'].get(rn, DEFAULT_VALUE)
        return v

    def __contains__(self, item):
        if item in self.unsaved and '_accept_all_' in self.__dict__:
            return True
        return item in self.config

    def __iter__(self):
        '''
        FIXME needs proper iterator tests in test_torconfig too
        '''
        for x in self.config.__iter__():
            yield x
        for x in self.__dict__['unsaved'].__iter__():
            yield x

    def get_type(self, name):
        """
        return the type of a config key.

        :param: name the key

        FIXME can we do something more-clever than this for client
        code to determine what sort of thing a key is?
        """

        # XXX FIXME uhm...how to do all the different types of hidden-services?
        if name.lower() == 'hiddenservices':
            return FilesystemOnionService
        return type(self.parsers[name])

    def _conf_changed(self, arg):
        """
        internal callback. from control-spec:

        4.1.18. Configuration changed

          The syntax is:
             StartReplyLine *(MidReplyLine) EndReplyLine

             StartReplyLine = "650-CONF_CHANGED" CRLF
             MidReplyLine = "650-" KEYWORD ["=" VALUE] CRLF
             EndReplyLine = "650 OK"

          Tor configuration options have changed (such as via a SETCONF or
          RELOAD signal). KEYWORD and VALUE specify the configuration option
          that was changed.  Undefined configuration options contain only the
          KEYWORD.
        """

        conf = parse_keywords(arg, multiline_values=False)
        for (k, v) in conf.items():
            # v will be txtorcon.DEFAULT_VALUE already from
            # parse_keywords if it was unspecified
            real_name = self._find_real_name(k)
            if real_name in self.parsers:
                v = self.parsers[real_name].parse(v)
            self.config[real_name] = v

    def bootstrap(self, arg=None):
        '''
        This only takes args so it can be used as a callback. Don't
        pass an arg, it is ignored.
        '''
        try:
            d = self.protocol.add_event_listener(
                'CONF_CHANGED', self._conf_changed)
        except RuntimeError:
            # for Tor versions which don't understand CONF_CHANGED
            # there's nothing we can really do.
            log.msg(
                "Can't listen for CONF_CHANGED event; won't stay up-to-date "
                "with other clients.")
            d = defer.succeed(None)
        d.addCallback(lambda _: self.protocol.get_info_raw("config/names"))
        d.addCallback(self._do_setup)
        d.addCallback(self.do_post_bootstrap)
        d.addErrback(self.do_post_errback)

    def do_post_errback(self, f):
        self.post_bootstrap.errback(f)
        return None

    def do_post_bootstrap(self, arg):
        if not self.post_bootstrap.called:
            self.post_bootstrap.callback(self)
        return self

    def needs_save(self):
        return len(self.unsaved) > 0

    def mark_unsaved(self, name):
        name = self._find_real_name(name)
        if name in self.config and name not in self.unsaved:
            self.unsaved[name] = self.config[self._find_real_name(name)]

    def save(self):
        """
        Save any outstanding items. This returns a Deferred which will
        errback if Tor was unhappy with anything, or callback with
        this TorConfig object on success.
        """

        if not self.needs_save():
            return defer.succeed(self)

        args = []
        directories = []
        for (key, value) in self.unsaved.items():
            if key == 'HiddenServices':
                self.config['HiddenServices'] = value
                # using a list here because at least one unit-test
                # cares about order -- and conceivably order *could*
                # matter here, to Tor...
                services = list()
                # authenticated services get flattened into the HiddenServices list...
                for hs in value:
                    if IOnionClient.providedBy(hs):
                        parent = IOnionClient(hs).parent
                        if parent not in services:
                            services.append(parent)
                    elif isinstance(hs, (EphemeralOnionService, EphemeralHiddenService)):
                        raise ValueError(
                            "Only filesystem based Onion services may be added"
                            " via TorConfig.hiddenservices; ephemeral services"
                            " must be created with 'create_onion_service'."
                        )
                    else:
                        if hs not in services:
                            services.append(hs)

                for hs in services:
                    for (k, v) in hs.config_attributes():
                        if k == 'HiddenServiceDir':
                            if v not in directories:
                                directories.append(v)
                                args.append(k)
                                args.append(v)
                            else:
                                raise RuntimeError("Trying to add hidden service with same HiddenServiceDir: %s" % v)
                        else:
                            args.append(k)
                            args.append(v)
                continue

            if isinstance(value, list):
                for x in value:
                    # FIXME XXX
                    if x is not DEFAULT_VALUE:
                        args.append(key)
                        args.append(str(x))

            else:
                args.append(key)
                args.append(value)

            # FIXME in future we should wait for CONF_CHANGED and
            # update then, right?
            real_name = self._find_real_name(key)
            if not isinstance(value, list) and real_name in self.parsers:
                value = self.parsers[real_name].parse(value)
            self.config[real_name] = value

        # FIXME might want to re-think this, but currently there's no
        # way to put things into a config and get them out again
        # nicely...unless you just don't assign a protocol
        if self.protocol:
            d = self.protocol.set_conf(*args)
            d.addCallback(self._save_completed)
            return d

        else:
            self._save_completed()
            return defer.succeed(self)

    def _save_completed(self, *args):
        '''internal callback'''
        self.__dict__['unsaved'] = {}
        return self

    def _find_real_name(self, name):
        keys = list(self.__dict__['parsers'].keys()) + list(self.__dict__['config'].keys())
        for x in keys:
            if x.lower() == name.lower():
                return x
        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(
                            self,
                            ports=[],  # no way to discover ports=
                            hostname=onion,
                            private_key=DISCARD,  # we don't know it, anyway
                            version=2,
                            detach=False,
                        )
                    )
            self.config['EphemeralOnionServices'] = onions

        try:
            detached = yield self.protocol.get_info('onions/detached')
        except Exception:
            self.config['DetachedOnionServices'] = []
        else:
            onions = []
            for line in detached['onions/detached'].split('\n'):
                onion = line.strip()
                if onion:
                    onions.append(
                        EphemeralOnionService(
                            self,
                            ports=[],  # no way to discover original ports=
                            hostname=onion,
                            detach=True,
                            private_key=DISCARD,
                        )
                    )
            self.config['DetachedOnionServices'] = onions
        defer.returnValue(self)

    def _setup_hidden_services(self, servicelines):

        def maybe_add_hidden_service():
            if directory is not None:
                if directory not in directories:
                    directories.append(directory)
                    if not auth:
                        service = FilesystemOnionService(
                            self, directory, ports, ver, group_read
                        )
                        hs.append(service)
                    else:
                        auth_type, clients = auth.split(' ', 1)
                        clients = clients.split(',')
                        if auth_type == 'basic':
                            auth0 = AuthBasic(clients)
                        elif auth_type == 'stealth':
                            auth0 = AuthStealth(clients)
                        else:
                            raise ValueError(
                                "Unknown auth type '{}'".format(auth_type)
                            )
                        parent_service = FilesystemAuthenticatedOnionService(
                            self, directory, ports, auth0, ver, group_read
                        )
                        for client_name in parent_service.client_names():
                            hs.append(parent_service.get_client(client_name))
                else:
                    raise RuntimeError("Trying to add hidden service with same HiddenServiceDir: %s" % directory)

        hs = []
        directory = None
        directories = []
        ports = []
        ver = None
        group_read = None
        auth = None
        for line in servicelines.split('\n'):
            if not len(line.strip()):
                continue

            if line == 'HiddenServiceOptions':
                continue
            k, v = line.split('=')
            if k == 'HiddenServiceDir':
                maybe_add_hidden_service()
                directory = v
                _directory = directory
                directory = os.path.abspath(directory)
                if directory != _directory:
                    warnings.warn(
                        "Directory path: %s changed to absolute path: %s" % (_directory, directory),
                        RuntimeWarning
                    )
                ports = []
                ver = None
                auth = None
                group_read = 0

            elif k == 'HiddenServicePort':
                ports.append(v)

            elif k == 'HiddenServiceVersion':
                ver = int(v)

            elif k == 'HiddenServiceAuthorizeClient':
                if auth is not None:
                    # definitely error, or keep going?
                    raise ValueError("Multiple HiddenServiceAuthorizeClient lines for one service")
                auth = v

            elif k == 'HiddenServiceDirGroupReadable':
                group_read = int(v)

            else:
                raise RuntimeError("Can't parse HiddenServiceOptions: " + k)

        maybe_add_hidden_service()

        name = 'HiddenServices'
        self.config[name] = _ListWrapper(
            hs, functools.partial(self.mark_unsaved, name))

    def config_args(self):
        '''
        Returns an iterator of 2-tuples (config_name, value), one for each
        configuration option in this config. This is more-or-less an
        internal method, but see, e.g., launch_tor()'s implementation
        if you think you need to use this for something.

        See :meth:`txtorcon.TorConfig.create_torrc` which returns a
        string which is also a valid ``torrc`` file
        '''

        everything = dict()
        everything.update(self.config)
        everything.update(self.unsaved)

        for (k, v) in list(everything.items()):
            if type(v) is _ListWrapper:
                if k.lower() == 'hiddenservices':
                    for x in v:
                        for (kk, vv) in x.config_attributes():
                            yield (str(kk), str(vv))

                else:
                    # FIXME actually, is this right? don't we want ALL
                    # the values in one string?!
                    for x in v:
                        yield (str(k), str(x))

            else:
                yield (str(k), str(v))

    def create_torrc(self):
        rtn = StringIO()

        for (k, v) in self.config_args():
            rtn.write(u'%s %s\n' % (k, v))

        return rtn.getvalue() DEFAULT_VALUE = 'DEFAULT' class HiddenService(object):
    """
    Because hidden service configuration is handled specially by Tor,
    we wrap the config in this class. This corresponds to the
    HiddenServiceDir, HiddenServicePort, HiddenServiceVersion and
    HiddenServiceAuthorizeClient lines from the config. If you want
    multiple HiddenServicePort lines, simply append more strings to
    the ports member.

    To create an additional hidden service, append a new instance of
    this class to the config (ignore the conf argument)::

        state.hiddenservices.append(HiddenService('/path/to/dir', ['80 127.0.0.1:1234']))
    """

    def __init__(self, config, thedir, ports,
                 auth=[], ver=2, group_readable=0):
        """
        config is the TorConfig to which this will belong, thedir
        corresponds to 'HiddenServiceDir' and will ultimately contain
        a 'hostname' and 'private_key' file, ports is a list of lines
        corresponding to HiddenServicePort (like '80 127.0.0.1:1234'
        to advertise a hidden service at port 80 and redirect it
        internally on 127.0.0.1:1234). auth corresponds to the
        HiddenServiceAuthenticateClient lines and can be either a
        string or a list of strings (like 'basic client0,client1' or
        'stealth client5,client6') and ver corresponds to
        HiddenServiceVersion and is always 2 right now.

        XXX FIXME can we avoid having to pass the config object
        somehow? Like provide a factory-function on TorConfig for
        users instead?
        """

        self.conf = config
        self.dir = thedir
        self.version = ver
        self.group_readable = group_readable

        # lazy-loaded if the @properties are accessed
        self._private_key = None
        self._clients = None
        self._hostname = None
        self._client_keys = None

        # HiddenServiceAuthorizeClient is a list
        # in case people are passing '' for the auth
        if not auth:
            auth = []
        elif not isinstance(auth, list):
            auth = [auth]
        self.authorize_client = _ListWrapper(
            auth, functools.partial(
                self.conf.mark_unsaved, 'HiddenServices'
            )
        )

        # there are three magic attributes, "hostname" and
        # "private_key" are gotten from the dir if they're still None
        # when accessed. "client_keys" parses out any client
        # authorizations. Note that after a SETCONF has returned '250
        # OK' it seems from tor code that the keys will always have
        # been created on disk by that point

        if not isinstance(ports, list):
            ports = [ports]
        self.ports = _ListWrapper(ports, functools.partial(
            self.conf.mark_unsaved, 'HiddenServices'))

    def __setattr__(self, name, value):
        """
        We override the default behavior so that we can mark
        HiddenServices as unsaved in our TorConfig object if anything
        is changed.
        """
        watched_params = ['dir', 'version', 'authorize_client', 'ports']
        if name in watched_params and self.conf:
            self.conf.mark_unsaved('HiddenServices')
        if isinstance(value, list):
            value = _ListWrapper(value, functools.partial(
                self.conf.mark_unsaved, 'HiddenServices'))
        self.__dict__[name] = value

    @property
    def private_key(self):
        if self._private_key is None:
            with open(os.path.join(self.dir, 'private_key')) as f:
                self._private_key = f.read().strip()
        return self._private_key

    @property
    def clients(self):
        if self._clients is None:
            self._clients = []
            try:
                with open(os.path.join(self.dir, 'hostname')) as f:
                    for line in f.readlines():
                        args = line.split()
                        # XXX should be a dict?
                        if len(args) > 1:
                            # tag, onion-uri?
                            self._clients.append((args[0], args[1]))
                        else:
                            self._clients.append(('default', args[0]))
            except IOError:
                pass
        return self._clients

    @property
    def hostname(self):
        if self._hostname is None:
            with open(os.path.join(self.dir, 'hostname')) as f:
                data = f.read().strip()
            host = None
            for line in data.split('\n'):
                h = line.split(' ')[0]
                if host is None:
                    host = h
                elif h != host:
                    raise RuntimeError(
                        ".hostname accessed on stealth-auth'd hidden-service "
                        "with multiple onion addresses."
                    )
            self._hostname = h
        return self._hostname

    @property
    def client_keys(self):
        if self._client_keys is None:
            fname = os.path.join(self.dir, 'client_keys')
            self._client_keys = []
            if os.path.exists(fname):
                with open(fname) as f:
                    self._client_keys = _parse_client_keys(f)
        return self._client_keys

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

        rtn = [('HiddenServiceDir', str(self.dir))]
        if self.conf._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)))
        for authline in self.authorize_client:
            rtn.append(('HiddenServiceAuthorizeClient', str(authline)))
        return rtn @inlineCallbacks
def launch(reactor,
           progress_updates=None,
           control_port=None,
           data_directory=None,
           socks_port=None,
           non_anonymous_mode=None,
           stdout=None,
           stderr=None,
           timeout=None,
           tor_binary=None,
           user=None,  # XXX like the config['User'] special-casing from before
           # 'users' probably never need these:
           connection_creator=None,
           kill_on_stderr=True,
           _tor_config=None,  # a TorConfig instance, mostly for tests
           ):
    """
    launches a new Tor process, and returns a Deferred that fires with
    a new :class:`txtorcon.Tor` instance. From this instance, you can
    create or get any "interesting" instances you need: the
    :class:`txtorcon.TorConfig` instance, create endpoints, create
    :class:`txtorcon.TorState` instance(s), etc.

    Note that there is NO way to pass in a config; we only expost a
    couple of basic Tor options. If you need anything beyond these,
    you can access the ``TorConfig`` instance (via ``.config``)
    and make any changes there, reflecting them in tor with
    ``.config.save()``.

    You can igore all the options and safe defaults will be
    provided. However, **it is recommended to pass data_directory**
    especially if you will be starting up Tor frequently, as it saves
    a bunch of time (and bandwidth for the directory
    authorities). "Safe defaults" means:

      - a tempdir for a ``DataDirectory`` is used (respecting ``TMP``)
        and is deleted when this tor is shut down (you therefore
        *probably* want to supply the ``data_directory=`` kwarg);
      - a random, currently-unused local TCP port is used as the
        ``SocksPort`` (specify ``socks_port=`` if you want your
        own). If you want no SOCKS listener at all, pass
        ``socks_port=0``
      - we set ``__OwningControllerProcess`` and call
        ``TAKEOWNERSHIP`` so that if our control connection goes away,
        tor shuts down (see `control-spec
        <https://gitweb.torproject.org/torspec.git/blob/HEAD:/control-spec.txt>`_
        3.23).
      - the launched Tor will use ``COOKIE`` authentication.

    :param reactor: a Twisted IReactorCore implementation (usually
        twisted.internet.reactor)

    :param progress_updates: a callback which gets progress updates; gets 3
         args: percent, tag, summary (FIXME make an interface for this).

    :param data_directory: set as the ``DataDirectory`` option to Tor,
        this is where tor keeps its state information (cached relays,
        etc); starting with an already-populated state directory is a lot
        faster. If ``None`` (the default), we create a tempdir for this
        **and delete it on exit**. It is recommended you pass something here.

    :param non_anonymous_mode: sets the Tor options
        `HiddenServiceSingleHopMode` and
        `HiddenServiceNonAnonymousMode` to 1 and un-sets any
        `SOCKSPort` config, thus putting this Tor client into
        "non-anonymous mode" which allows starting so-called Single
        Onion services -- which use single-hop circuits to rendezvous
        points. See WARNINGs in Tor manual! Also you need Tor
        `0.3.4.1` or later (e.g. any `0.3.5.*` or newer) for this to
        work properly.

    :param stdout: a file-like object to which we write anything that
        Tor prints on stdout (just needs to support write()).

    :param stderr: a file-like object to which we write anything that
        Tor prints on stderr (just needs .write()). Note that we kill
        Tor off by default if anything appears on stderr; pass
        "kill_on_stderr=False" if you don't want this behavior.

    :param tor_binary: path to the Tor binary to run. If None (the
        default), we try to find the tor binary.

    :param kill_on_stderr:
        When True (the default), if Tor prints anything on stderr we
        kill off the process, close the TorControlProtocol and raise
        an exception.

    :param connection_creator: is mostly available to ease testing, so
        you probably don't want to supply this. If supplied, it is a
        callable that should return a Deferred that delivers an
        :api:`twisted.internet.interfaces.IProtocol <IProtocol>` or
        ConnectError.
        See :api:`twisted.internet.interfaces.IStreamClientEndpoint`.connect
        Note that this parameter is ignored if config.ControlPort == 0

    :return: a Deferred which callbacks with :class:`txtorcon.Tor`
        instance, from which you can retrieve the TorControlProtocol
        instance via the ``.protocol`` property.

    HACKS:

     1. It's hard to know when Tor has both (completely!) written its
        authentication cookie file AND is listening on the control
        port. It seems that waiting for the first 'bootstrap' message on
        stdout is sufficient. Seems fragile...and doesn't work 100% of
        the time, so FIXME look at Tor source.



    XXX this "User" thing was, IIRC, a feature for root-using scripts
    (!!) that were going to launch tor, but where tor would drop to a
    different user. Do we still want to support this? Probably
    relevant to Docker (where everything is root! yay!)

    ``User``: if this exists, we attempt to set ownership of the tempdir
    to this user (but only if our effective UID is 0).
    """

    # We have a slight problem with the approach: we need to pass a
    # few minimum values to a torrc file so that Tor will start up
    # enough that we may connect to it. Ideally, we'd be able to
    # start a Tor up which doesn't really do anything except provide
    # "AUTHENTICATE" and "GETINFO config/names" so we can do our
    # config validation.

    if not IReactorCore.providedBy(reactor):
        raise ValueError(
            "'reactor' argument must provide IReactorCore"
            " (got '{}': {})".format(
                type(reactor).__class__.__name__,
                repr(reactor)
            )
        )

    if tor_binary is None:
        tor_binary = find_tor_binary()
    if tor_binary is None:
        # We fail right here instead of waiting for the reactor to start
        raise TorNotFound('Tor binary could not be found')

    # make sure we got things that have write() for stderr, stdout
    # kwargs (XXX is there a "better" way to check for file-like
    # object? do we use anything besides 'write()'?)
    for arg in [stderr, stdout]:
        if arg and not getattr(arg, "write", None):
            raise RuntimeError(
                'File-like object needed for stdout or stderr args.'
            )

    config = _tor_config or TorConfig()
    if data_directory is not None:
        user_set_data_directory = True
        config.DataDirectory = data_directory
        try:
            os.mkdir(data_directory, 0o0700)
        except OSError:
            pass
    else:
        user_set_data_directory = False
        data_directory = tempfile.mkdtemp(prefix='tortmp')
        config.DataDirectory = data_directory
        # note: we also set up the ProcessProtocol to delete this when
        # Tor exits, this is "just in case" fallback:
        reactor.addSystemEventTrigger(
            'before', 'shutdown',
            functools.partial(delete_file_or_tree, data_directory)
        )

    # things that used launch_tor() had to set ControlPort and/or
    # SocksPort on the config to pass them, so we honour that here.
    if control_port is None and _tor_config is not None:
        try:
            control_port = config.ControlPort
        except KeyError:
            control_port = None

    if socks_port is None and _tor_config is not None:
        try:
            socks_port = config.SocksPort
        except KeyError:
            socks_port = None

    if non_anonymous_mode:
        if socks_port is not None:
            raise ValueError(
                "Cannot use SOCKS options with non_anonymous_mode=True"
            )
        config.HiddenServiceNonAnonymousMode = 1
        config.HiddenServiceSingleHopMode = 1
        config.SOCKSPort = 0
    else:
        if socks_port is None:
            socks_port = yield available_tcp_port(reactor)
        config.SOCKSPort = socks_port

    try:
        our_user = user or config.User
    except KeyError:
        pass
    else:
        # if we're root, make sure the directory is owned by the User
        # that Tor is configured to drop to
        if sys.platform in ('linux', 'linux2', 'darwin') and os.geteuid() == 0:
            os.chown(data_directory, pwd.getpwnam(our_user).pw_uid, -1)

    # user can pass in a control port, or we set one up here
    if control_port is None:
        # on posix-y systems, we can use a unix-socket
        if sys.platform in ('linux', 'linux2', 'darwin'):
            # note: tor will not accept a relative path for ControlPort
            control_port = 'unix:{}'.format(
                os.path.join(os.path.realpath(data_directory), 'control.socket')
            )
        else:
            control_port = yield available_tcp_port(reactor)
    else:
        if str(control_port).startswith('unix:'):
            control_path = control_port.lstrip('unix:')
            containing_dir = dirname(control_path)
            if not exists(containing_dir):
                raise ValueError(
                    "The directory containing '{}' must exist".format(
                        containing_dir
                    )
                )
            # Tor will be sad if the directory isn't 0700
            mode = (0o0777 & os.stat(containing_dir).st_mode)
            if mode & ~(0o0700):
                raise ValueError(
                    "The directory containing a unix control-socket ('{}') "
                    "must only be readable by the user".format(containing_dir)
                )
    config.ControlPort = control_port

    config.CookieAuthentication = 1
    config.__OwningControllerProcess = os.getpid()
    if connection_creator is None:
        if str(control_port).startswith('unix:'):
            connection_creator = functools.partial(
                UNIXClientEndpoint(reactor, control_port[5:]).connect,
                TorProtocolFactory()
            )
        else:
            connection_creator = functools.partial(
                TCP4ClientEndpoint(reactor, 'localhost', control_port).connect,
                TorProtocolFactory()
            )
    # not an "else" on purpose; if we passed in "control_port=0" *and*
    # a custom connection creator, we should still set this to None so
    # it's never called (since we can't connect with ControlPort=0)
    if control_port == 0:
        connection_creator = None

    # NOTE well, that if we don't pass "-f" then Tor will merrily load
    # its default torrc, and apply our options over top... :/ should
    # file a bug probably? --no-defaults or something maybe? (does
    # --defaults-torrc - or something work?)
    config_args = ['-f', '/dev/null/non-existant-on-purpose', '--ignore-missing-torrc']

    # ...now add all our config options on the command-line. This
    # avoids writing a temporary torrc.
    for (k, v) in config.config_args():
        config_args.append(k)
        config_args.append(v)

    process_protocol = TorProcessProtocol(
        connection_creator,
        progress_updates,
        config, reactor,
        timeout,
        kill_on_stderr,
        stdout,
        stderr,
    )
    if control_port == 0:
        connected_cb = succeed(None)
    else:
        connected_cb = process_protocol.when_connected()

    # we set both to_delete and the shutdown events because this
    # process might be shut down way before the reactor, but if the
    # reactor bombs out without the subprocess getting closed cleanly,
    # we'll want the system shutdown events triggered so the temporary
    # files get cleaned up either way

    # we don't want to delete the user's directories, just temporary
    # ones this method created.
    if not user_set_data_directory:
        process_protocol.to_delete = [data_directory]
        reactor.addSystemEventTrigger(
            'before', 'shutdown',
            functools.partial(delete_file_or_tree, data_directory)
        )

    log.msg('Spawning tor process with DataDirectory', data_directory)
    args = [tor_binary] + config_args
    transport = reactor.spawnProcess(
        process_protocol,
        tor_binary,
        args=args,
        env={'HOME': data_directory},
        path=data_directory if os.path.exists(data_directory) else None,  # XXX error if it doesn't exist?
    )
    transport.closeStdin()
    proto = yield connected_cb
    # note "proto" here is a TorProcessProtocol

    # we might need to attach this protocol to the TorConfig
    if config.protocol is None and proto is not None and proto.tor_protocol is not None:
        # proto is None in the ControlPort=0 case
        yield config.attach_protocol(proto.tor_protocol)
        # note that attach_protocol waits for the protocol to be
        # boostrapped if necessary

    returnValue(
        Tor(
            reactor,
            config.protocol,
            _tor_config=config,
            _process_proto=process_protocol,
            _non_anonymous=True if non_anonymous_mode else False,
        )
    ) class TorNotFound(RuntimeError):
    """
    Raised by launch_tor() in case the tor binary was unspecified and could
    not be found by consulting the shell.
    """ class CommaList(TorConfigType):
    def parse(self, s):
        return [x.strip() for x in s.split(',')] @defer.inlineCallbacks
@deprecated(_Version("txtorcon", 0, 18, 0))
def launch_tor(config, reactor,
               tor_binary=None,
               progress_updates=None,
               connection_creator=None,
               timeout=None,
               kill_on_stderr=True,
               stdout=None, stderr=None):
    """
    Deprecated; use launch() instead.

    See also controller.py
    """
    from .controller import launch
    # XXX FIXME are we dealing with options in the config "properly"
    # as far as translating semantics from the old launch_tor to
    # launch()? DataDirectory, User, ControlPort, ...?
    tor = yield launch(
        reactor,
        stdout=stdout,
        stderr=stderr,
        progress_updates=progress_updates,
        tor_binary=tor_binary,
        connection_creator=connection_creator,
        timeout=timeout,
        kill_on_stderr=kill_on_stderr,
        _tor_config=config,
    )
    defer.returnValue(tor.process) class EphemeralHiddenService(object):
    '''
    Deprecated as of 18.0.0. Please instead use :class:`txtorcon.EphemeralOnionService`

    This uses the ephemeral hidden-service APIs (in comparison to
    torrc or SETCONF). This means your hidden-service private-key is
    never in a file. It also means that when the process exits, that
    HS goes away. See documentation for ADD_ONION in torspec:
    https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n1295
    '''

    @deprecated(_Version("txtorcon", 18, 0, 0))
    def __init__(self, ports, key_blob_or_type='NEW:BEST', auth=[], ver=2):
        # deprecated; use Tor.create_onion_service
        warn(
            'EphemeralHiddenService is deprecated; use EphemeralOnionService instead',
            DeprecationWarning,
        )
        if _is_valid_keyblob(key_blob_or_type):
            self._key_blob = nativeString(key_blob_or_type)
        else:
            raise ValueError(
                'key_blob_or_type must be a string in the formats '
                '"NEW:<ALGORITHM>" or "<ALGORITHM>:<KEY>"')
        if isinstance(ports, (six.text_type, str)):
            ports = [ports]
        self._ports = [x.replace(' ', ',') for x in ports]
        self._keyblob = key_blob_or_type
        self.auth = auth  # FIXME ununsed
        # FIXME nicer than assert, plz
        self.version = ver
        self.hostname = None

    @defer.inlineCallbacks
    def add_to_tor(self, protocol):
        '''
        Returns a Deferred which fires with 'self' after at least one
        descriptor has been uploaded. Errback if no descriptor upload
        succeeds.
        '''

        upload_d = _await_descriptor_upload(protocol, self, progress=None, await_all_uploads=False)

        # _add_ephemeral_service takes a TorConfig but we don't have
        # that here ..  and also we're just keeping this for
        # backwards-compatability anyway so instead of trying to
        # re-use that helper I'm leaving this original code here. So
        # this is what it supports and that's that:
        ports = ' '.join(map(lambda x: 'Port=' + x.strip(), self._ports))
        cmd = 'ADD_ONION %s %s' % (self._key_blob, ports)
        ans = yield protocol.queue_command(cmd)
        ans = find_keywords(ans.split('\n'))
        self.hostname = ans['ServiceID'] + '.onion'
        if self._key_blob.startswith('NEW:'):
            self.private_key = ans['PrivateKey']
        else:
            self.private_key = self._key_blob

        log.msg('Created hidden-service at', self.hostname)

        log.msg("Created '{}', waiting for descriptor uploads.".format(self.hostname))
        yield upload_d

    @defer.inlineCallbacks
    def remove_from_tor(self, protocol):
        '''
        Returns a Deferred which fires with None
        '''
        r = yield protocol.queue_command('DEL_ONION %s' % self.hostname[:-6])
        if r.strip() != 'OK':
            raise RuntimeError('Failed to remove hidden service: "%s".' % r) 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 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 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 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 class AuthBasic(_AuthCommon):
    """
    Authentication details for 'basic' auth.
    """
    auth_type = 'basic'
    # note that _AuthCommon.__init__ takes 'clients' class FakeControlProtocol(object):
    """
    This is a little weird, but in most tests the answer at the top of
    the list is sent back immediately in an already-called
    Deferred. However, if the answer list is empty at the time of the
    call, instead the returned Deferred is added to the pending list
    and answer_pending() may be called to have the next Deferred
    fire. (see test_accept_all_postbootstrap for an example).

    It is done this way in case we need to have some other code run
    between the get_conf (or whatever) and the callback -- if the
    Deferred is already-fired when get_conf runs, there's a Very Good
    Chance (always?) that the callback just runs right away.
    """

    def __init__(self, answers):
        self.answers = answers
        self.pending = []
        self.post_bootstrap = defer.succeed(self)
        self.on_disconnect = defer.Deferred()
        self.sets = []
        self.events = {}  #: event type -> callback
        self.pending_events = {}  #: event type -> list
        self.is_owned = -1
        self.version = "0.2.8.0"
        # XXX can we get rud of 'commands' and just use pending?
        self.commands = []

    def queue_command(self, cmd):
        if len(self.answers) == 0:
            d = defer.Deferred()
            self.pending.append(d)
            self.commands.append((cmd, d))
            return d

        d = defer.succeed(self.answers[0])
        self.commands.append((cmd, d))
        self.answers = self.answers[1:]
        return d

    def event_happened(self, event_type, *args):
        '''
        Use this in your tests to send 650 events when an event-listener
        is added.  XXX Also if we've *already* added one? Do that if
        there's a use-case for it
        '''
        if event_type in self.events:
            self.events[event_type](*args)
        elif event_type in self.pending_events:
            self.pending_events[event_type].append(args)
        else:
            self.pending_events[event_type] = [args]

    def answer_pending(self, answer):
        d = self.pending[0]
        self.pending = self.pending[1:]
        d.callback(answer)

    def get_info(self, info):
        a = self.answers.pop()
        return defer.succeed(a)

    def get_info_raw(self, info):
        if len(self.answers) == 0:
            d = defer.Deferred()
            self.pending.append(d)
            return d

        d = defer.succeed(self.answers[0])
        self.answers = self.answers[1:]
        return d

    @defer.inlineCallbacks
    def get_info_incremental(self, info, cb):
        text = yield self.get_info_raw(info)
        for line in text.split('\r\n'):
            cb(line)
        defer.returnValue('')  # FIXME uh....what's up at torstate.py:350?

    def get_conf(self, info):
        if len(self.answers) == 0:
            d = defer.Deferred()
            self.pending.append(d)
            return d

        d = defer.succeed(self.answers[0])
        self.answers = self.answers[1:]
        return d

    get_conf_raw = get_conf  # up to test author ensure the answer is a raw string
    get_conf_single = get_conf

    def set_conf(self, *args):
        for i in range(0, len(args), 2):
            self.sets.append((args[i], args[i + 1]))
        return defer.succeed('')

    def add_event_listener(self, nm, cb):
        self.events[nm] = cb
        if nm in self.pending_events:
            for event in self.pending_events[nm]:
                cb(*event)
        return defer.succeed(None)

    def remove_event_listener(self, nm, cb):
        del self.events[nm]
import os
import shutil
import tempfile
import functools
import warnings
from six import StringIO
from mock import Mock, patch
from os.path import join
from zope.interface import implementer, directlyProvides
from twisted.trial import unittest
from twisted.test import proto_helpers
from twisted.internet import defer
from twisted.internet.interfaces import IReactorCore
from twisted.python.failure import Failure
from txtorcon import TorProtocolError
from txtorcon import ITorControlProtocol
from txtorcon import TorProcessProtocol
from txtorcon import TorConfig
from txtorcon import DEFAULT_VALUE
from txtorcon import HiddenService
from txtorcon import launch
from txtorcon import TorNotFound
from txtorcon.torconfig import CommaList
from txtorcon.torconfig import launch_tor
from txtorcon.torconfig import EphemeralHiddenService
from txtorcon.onion import _parse_client_keys
from txtorcon.onion import FilesystemOnionService
from txtorcon.onion import EphemeralOnionService
from txtorcon.onion import FilesystemAuthenticatedOnionService
from txtorcon.onion import AuthBasic
from txtorcon.testutil import FakeControlProtocol
        self.protocol.answers.append('''config/names=
HiddenServiceOptions Virtual
HiddenServiceVersion Dependant
HiddenServiceDirGroupReadable Dependant
HiddenServiceAuthorizeClient Dependant''')
        self.protocol.answers.append('')  # defaults

    @defer.inlineCallbacks
    def test_config_client_keys(self):
        self.protocol.answers.append('')  # no hiddenservices by default
        conf = TorConfig(self.protocol)
        yield conf.post_bootstrap
        self.assertTrue(conf.post_bootstrap.called)
        fakedir = self.mktemp()
        os.mkdir(fakedir)
        with open(join(fakedir, 'hostname'), 'w') as f:
            for name in ['alice', 'bob']:
                f.write('hostname_{name} cookie_{name} # client: {name}\n'.format(name=name))
        with open(join(fakedir, 'client_keys'), 'w') as f:
            for name in ['alice', 'bob']:
                f.write('''client-name {name}
descriptor-cookie CVAQuIn8iSsYo4KXW2Ljvw==
client-key
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDgE+4otmhhRInd2IyZemfPGYjFiYEGCCnaVWQ7JpZv25atvxuj
RB+S1PnSST6V2brF83vwaRuR4QWtFHz8v8VmUPDXcVEGoGQ+oQUrnwgGLTseBYJE
n9uY8snE7755mrsddbMorLjg2JhmoTFmGUqga1YsypqZ39H5K0qsfRw2kwIDAQAB
AoGAFoSqOFksYCn9GNg8OOg+KmfMgN1yo+KKIjDWo8Ma9x7AI7HC20NrUNwqRuGp
cnGw/VecqupFJQHSCjS24sd61L9D0OZ7NHOWsBhpUlQ5tlM3xxCz7PQmSsISro/h
IFAsAzCdhA7ies6our9m0vO93WsZTPE32hEmHCS476SvyBECQQD0OitF1gRhJ550
rAkLnZwIQlVzNH1iK10AesgivBbAw2ywkeolplUp5l0cPrckrtCZW7FDZi6wgHhw
hq2q/jetAkEA6uEczfRTYJ762pZxk/VQ17GP4T/0LBITCm4PIDQ13etuqoPe+AI7
aOcAayecbmWA9CgJdmHE77g5zDcddxrvPwJBAKG2vp6AZuf59uckXtztILsrSS9+
ayOMuQkvQ8QULTC4dgi4pYUGjU+wNKkWMei9RGy3lTmeuH2wo49G7knSCQUCQQC+
1iiRLfKQjptC/vlJPghxN9OvMEczh3vw/XtMrx8VMDS6VmrTFv0uPoIYjhaLm+0q
c1080jMwkn5jbmSCYWABAkB28YVDOpAbxRqb/7u4kYFoohWb4YpY9uBkqA9FMwsg
DnkEGTrOUFZ7CbDp+SM18BjmFXI2n0bFJEznXFhH+Awz
-----END RSA PRIVATE KEY-----
'''.format(name=name))

        hs = FilesystemAuthenticatedOnionService(
            conf, fakedir, ['1 127.0.0.1:12345'],
            auth=AuthBasic(['alice']),
        )
        hs._private_key('alice')

    @defer.inlineCallbacks
    def test_config_client_auth_service(self):
        self.protocol.answers.append('')  # no hiddenservices by default
        conf = TorConfig(self.protocol)
        yield conf.post_bootstrap
        self.assertTrue(conf.post_bootstrap.called)
        fakedir = self.mktemp()
        os.mkdir(fakedir)
        with open(join(fakedir, 'hostname'), 'w') as f:
            for name in ['alice', 'bob']:
                f.write('hostname_{name} cookie_{name} # client: {name}\n'.format(name=name))

        # create a client-auth'd onion service, but only "add" one of
        # its newly created clients
        hs = FilesystemAuthenticatedOnionService(
            conf, fakedir, ['1 127.0.0.1:12345'],
            auth=AuthBasic(['alice', 'bob']),
            group_readable=True,
        )
        hs.add_client(
            name="carol",
            hostname="hostname_carol",
            ports=['1 127.0.0.1:12345'],
            token="cookie_carol",
        )
        yield conf.save()

    @defer.inlineCallbacks
    def test_options_hidden(self):
        self.protocol.answers.append(
            'HiddenServiceDir=/fake/path\nHiddenServicePort=80 '
            '127.0.0.1:1234\nHiddenServiceDirGroupReadable=1\n'
        )

        conf = TorConfig(self.protocol)
        yield conf.post_bootstrap
        self.assertTrue(conf.post_bootstrap.called)
        self.assertTrue('HiddenServiceOptions' not in conf.config)
        self.assertTrue('HiddenServices' in conf.config)
        self.assertEqual(len(conf.HiddenServices), 1)

        self.assertTrue(not conf.needs_save())
        conf.hiddenservices.append(
            HiddenService(conf, '/some/dir', '80 127.0.0.1:2345', 'auth', 2, True)
        )
        conf.hiddenservices[0].ports.append('443 127.0.0.1:443')
        self.assertTrue(conf.needs_save())
        conf.save()

        self.assertEqual(len(self.protocol.sets), 9)
        self.assertEqual(self.protocol.sets[0], ('HiddenServiceDir', '/fake/path'))
        self.assertEqual(self.protocol.sets[1], ('HiddenServiceDirGroupReadable', '1'))
        self.assertEqual(self.protocol.sets[2], ('HiddenServicePort', '80 127.0.0.1:1234'))
        self.assertEqual(self.protocol.sets[3], ('HiddenServicePort', '443 127.0.0.1:443'))
        self.assertEqual(self.protocol.sets[4], ('HiddenServiceDir', '/some/dir'))
        self.assertEqual(self.protocol.sets[5], ('HiddenServiceDirGroupReadable', '1'))
        self.assertEqual(self.protocol.sets[6], ('HiddenServicePort', '80 127.0.0.1:2345'))
        self.assertEqual(self.protocol.sets[7], ('HiddenServiceVersion', '2'))
        self.assertEqual(self.protocol.sets[8], ('HiddenServiceAuthorizeClient', 'auth'))

    def test_save_no_protocol(self):
        conf = TorConfig()
        conf.HiddenServices = [HiddenService(conf, '/fake/path', ['80 127.0.0.1:1234'])]
        conf.save()

    def test_two_hidden_services_before_save(self):
        conf = TorConfig()
        conf.HiddenServices = [HiddenService(conf, '/fake/path', ['80 127.0.0.1:1234'])]
        conf.HiddenServices.append(HiddenService(conf, '/fake/path/two', ['1234 127.0.0.1:1234']))
        conf.save()
        self.assertEqual(2, len(conf.HiddenServices))

    def test_hs_ephemeral_wrong_list(self):
        conf = TorConfig()

        conf.HiddenServices.append(EphemeralOnionService(conf, []))