1# -*- coding: utf-8 -*-
2
3from __future__ import absolute_import
4from __future__ import print_function
5from __future__ import with_statement
6
7import os
8import re
9import six
10import functools
11import warnings
12from io import StringIO
13from warnings import warn
14try:
15    from collections.abc import OrderedDict
16except ImportError:
17    from collections import OrderedDict
18
19from twisted.python import log
20from twisted.python.compat import nativeString
21from twisted.python.deprecate import deprecated
22from twisted.internet import defer
23from twisted.internet.endpoints import TCP4ClientEndpoint, UNIXClientEndpoint
24
25from txtorcon.torcontrolprotocol import parse_keywords, DEFAULT_VALUE
26from txtorcon.torcontrolprotocol import TorProtocolError
27from txtorcon.interface import ITorControlProtocol
28from txtorcon.util import find_keywords
29from .onion import IOnionClient, FilesystemOnionService, FilesystemAuthenticatedOnionService
30from .onion import DISCARD
31from .onion import AuthStealth, AuthBasic
32from .onion import EphemeralOnionService
33from .onion import _await_descriptor_upload
34from .onion import _parse_client_keys
35from .util import _Version
36
37
38@defer.inlineCallbacks
39@deprecated(_Version("txtorcon", 0, 18, 0))
40def launch_tor(config, reactor,
41               tor_binary=None,
42               progress_updates=None,
43               connection_creator=None,
44               timeout=None,
45               kill_on_stderr=True,
46               stdout=None, stderr=None):
47    """
48    Deprecated; use launch() instead.
49
50    See also controller.py
51    """
52    from .controller import launch
53    # XXX FIXME are we dealing with options in the config "properly"
54    # as far as translating semantics from the old launch_tor to
55    # launch()? DataDirectory, User, ControlPort, ...?
56    tor = yield launch(
57        reactor,
58        stdout=stdout,
59        stderr=stderr,
60        progress_updates=progress_updates,
61        tor_binary=tor_binary,
62        connection_creator=connection_creator,
63        timeout=timeout,
64        kill_on_stderr=kill_on_stderr,
65        _tor_config=config,
66    )
67    defer.returnValue(tor.process)
68
69
70class TorConfigType(object):
71    """
72    Base class for all configuration types, which function as parsers
73    and un-parsers.
74    """
75
76    def parse(self, s):
77        """
78        Given the string s, this should return a parsed representation
79        of it.
80        """
81        return s
82
83    def validate(self, s, instance, name):
84        """
85        If s is not a valid type for this object, an exception should
86        be thrown. The validated object should be returned.
87        """
88        return s
89
90
91class Boolean(TorConfigType):
92    "Boolean values are stored as 0 or 1."
93    def parse(self, s):
94        if int(s):
95            return True
96        return False
97
98    def validate(self, s, instance, name):
99        if s:
100            return 1
101        return 0
102
103
104class Boolean_Auto(TorConfigType):
105    """
106    weird class-name, but see the parser for these which is *mostly*
107    just the classname <==> string from Tor, except for something
108    called Boolean+Auto which is replace()d to be Boolean_Auto
109    """
110
111    def parse(self, s):
112        if s == 'auto' or int(s) < 0:
113            return -1
114        if int(s):
115            return 1
116        return 0
117
118    def validate(self, s, instance, name):
119        # FIXME: Is 'auto' an allowed value? (currently not)
120        s = int(s)
121        if s < 0:
122            return 'auto'
123        elif s:
124            return 1
125        else:
126            return 0
127
128
129class Integer(TorConfigType):
130    def parse(self, s):
131        return int(s)
132
133    def validate(self, s, instance, name):
134        return int(s)
135
136
137class SignedInteger(Integer):
138    pass
139
140
141class Port(Integer):
142    pass
143
144
145class TimeInterval(Integer):
146    pass
147
148
149# not actually used?
150class TimeMsecInterval(TorConfigType):
151    pass
152
153
154class DataSize(Integer):
155    pass
156
157
158class Float(TorConfigType):
159    def parse(self, s):
160        return float(s)
161
162
163# unused also?
164class Time(TorConfigType):
165    pass
166
167
168class CommaList(TorConfigType):
169    def parse(self, s):
170        return [x.strip() for x in s.split(',')]
171
172
173# FIXME: in latest master; what is it?
174# Tor source says "A list of strings, separated by commas and optional
175# whitespace, representing intervals in seconds, with optional units"
176class TimeIntervalCommaList(CommaList):
177    pass
178
179
180# FIXME: is this really a comma-list?
181class RouterList(CommaList):
182    pass
183
184
185class String(TorConfigType):
186    pass
187
188
189class Filename(String):
190    pass
191
192
193class LineList(TorConfigType):
194    def parse(self, s):
195        if isinstance(s, list):
196            return [str(x).strip() for x in s]
197        return [x.strip() for x in s.split('\n')]
198
199    def validate(self, obj, instance, name):
200        if not isinstance(obj, list):
201            raise ValueError("Not valid for %s: %s" % (self.__class__, obj))
202        return _ListWrapper(
203            obj, functools.partial(instance.mark_unsaved, name))
204
205
206config_types = [Boolean, Boolean_Auto, LineList, Integer, SignedInteger, Port,
207                TimeInterval, TimeMsecInterval,
208                DataSize, Float, Time, CommaList, String, LineList, Filename,
209                RouterList, TimeIntervalCommaList]
210
211
212def is_list_config_type(klass):
213    return 'List' in klass.__name__ or klass.__name__ in ['HiddenServices']
214
215
216def _wrapture(orig):
217    """
218    Returns a new method that wraps orig (the original method) with
219    something that first calls on_modify from the
220    instance. _ListWrapper uses this to wrap all methods that modify
221    the list.
222    """
223
224#    @functools.wraps(orig)
225    def foo(*args):
226        obj = args[0]
227        obj.on_modify()
228        return orig(*args)
229    return foo
230
231
232class _ListWrapper(list):
233    """
234    Do some voodoo to wrap lists so that if you do anything to modify
235    it, we mark the config as needing saving.
236
237    FIXME: really worth it to preserve attribute-style access? seems
238    to be okay from an exterior API perspective....
239    """
240
241    def __init__(self, thelist, on_modify_cb):
242        list.__init__(self, thelist)
243        self.on_modify = on_modify_cb
244
245    __setitem__ = _wrapture(list.__setitem__)
246    append = _wrapture(list.append)
247    extend = _wrapture(list.extend)
248    insert = _wrapture(list.insert)
249    remove = _wrapture(list.remove)
250    pop = _wrapture(list.pop)
251
252    def __repr__(self):
253        return '_ListWrapper' + super(_ListWrapper, self).__repr__()
254
255
256if six.PY2:
257    setattr(_ListWrapper, '__setslice__', _wrapture(list.__setslice__))
258
259
260class HiddenService(object):
261    """
262    Because hidden service configuration is handled specially by Tor,
263    we wrap the config in this class. This corresponds to the
264    HiddenServiceDir, HiddenServicePort, HiddenServiceVersion and
265    HiddenServiceAuthorizeClient lines from the config. If you want
266    multiple HiddenServicePort lines, simply append more strings to
267    the ports member.
268
269    To create an additional hidden service, append a new instance of
270    this class to the config (ignore the conf argument)::
271
272        state.hiddenservices.append(HiddenService('/path/to/dir', ['80 127.0.0.1:1234']))
273    """
274
275    def __init__(self, config, thedir, ports,
276                 auth=[], ver=2, group_readable=0):
277        """
278        config is the TorConfig to which this will belong, thedir
279        corresponds to 'HiddenServiceDir' and will ultimately contain
280        a 'hostname' and 'private_key' file, ports is a list of lines
281        corresponding to HiddenServicePort (like '80 127.0.0.1:1234'
282        to advertise a hidden service at port 80 and redirect it
283        internally on 127.0.0.1:1234). auth corresponds to the
284        HiddenServiceAuthenticateClient lines and can be either a
285        string or a list of strings (like 'basic client0,client1' or
286        'stealth client5,client6') and ver corresponds to
287        HiddenServiceVersion and is always 2 right now.
288
289        XXX FIXME can we avoid having to pass the config object
290        somehow? Like provide a factory-function on TorConfig for
291        users instead?
292        """
293
294        self.conf = config
295        self.dir = thedir
296        self.version = ver
297        self.group_readable = group_readable
298
299        # lazy-loaded if the @properties are accessed
300        self._private_key = None
301        self._clients = None
302        self._hostname = None
303        self._client_keys = None
304
305        # HiddenServiceAuthorizeClient is a list
306        # in case people are passing '' for the auth
307        if not auth:
308            auth = []
309        elif not isinstance(auth, list):
310            auth = [auth]
311        self.authorize_client = _ListWrapper(
312            auth, functools.partial(
313                self.conf.mark_unsaved, 'HiddenServices'
314            )
315        )
316
317        # there are three magic attributes, "hostname" and
318        # "private_key" are gotten from the dir if they're still None
319        # when accessed. "client_keys" parses out any client
320        # authorizations. Note that after a SETCONF has returned '250
321        # OK' it seems from tor code that the keys will always have
322        # been created on disk by that point
323
324        if not isinstance(ports, list):
325            ports = [ports]
326        self.ports = _ListWrapper(ports, functools.partial(
327            self.conf.mark_unsaved, 'HiddenServices'))
328
329    def __setattr__(self, name, value):
330        """
331        We override the default behavior so that we can mark
332        HiddenServices as unsaved in our TorConfig object if anything
333        is changed.
334        """
335        watched_params = ['dir', 'version', 'authorize_client', 'ports']
336        if name in watched_params and self.conf:
337            self.conf.mark_unsaved('HiddenServices')
338        if isinstance(value, list):
339            value = _ListWrapper(value, functools.partial(
340                self.conf.mark_unsaved, 'HiddenServices'))
341        self.__dict__[name] = value
342
343    @property
344    def private_key(self):
345        if self._private_key is None:
346            with open(os.path.join(self.dir, 'private_key')) as f:
347                self._private_key = f.read().strip()
348        return self._private_key
349
350    @property
351    def clients(self):
352        if self._clients is None:
353            self._clients = []
354            try:
355                with open(os.path.join(self.dir, 'hostname')) as f:
356                    for line in f.readlines():
357                        args = line.split()
358                        # XXX should be a dict?
359                        if len(args) > 1:
360                            # tag, onion-uri?
361                            self._clients.append((args[0], args[1]))
362                        else:
363                            self._clients.append(('default', args[0]))
364            except IOError:
365                pass
366        return self._clients
367
368    @property
369    def hostname(self):
370        if self._hostname is None:
371            with open(os.path.join(self.dir, 'hostname')) as f:
372                data = f.read().strip()
373            host = None
374            for line in data.split('\n'):
375                h = line.split(' ')[0]
376                if host is None:
377                    host = h
378                elif h != host:
379                    raise RuntimeError(
380                        ".hostname accessed on stealth-auth'd hidden-service "
381                        "with multiple onion addresses."
382                    )
383            self._hostname = h
384        return self._hostname
385
386    @property
387    def client_keys(self):
388        if self._client_keys is None:
389            fname = os.path.join(self.dir, 'client_keys')
390            self._client_keys = []
391            if os.path.exists(fname):
392                with open(fname) as f:
393                    self._client_keys = _parse_client_keys(f)
394        return self._client_keys
395
396    def config_attributes(self):
397        """
398        Helper method used by TorConfig when generating a torrc file.
399        """
400
401        rtn = [('HiddenServiceDir', str(self.dir))]
402        if self.conf._supports['HiddenServiceDirGroupReadable'] \
403           and self.group_readable:
404            rtn.append(('HiddenServiceDirGroupReadable', str(1)))
405        for port in self.ports:
406            rtn.append(('HiddenServicePort', str(port)))
407        if self.version:
408            rtn.append(('HiddenServiceVersion', str(self.version)))
409        for authline in self.authorize_client:
410            rtn.append(('HiddenServiceAuthorizeClient', str(authline)))
411        return rtn
412
413
414def _is_valid_keyblob(key_blob_or_type):
415    try:
416        key_blob_or_type = nativeString(key_blob_or_type)
417    except (UnicodeError, TypeError):
418        return False
419    else:
420        return re.match(r'[^ :]+:[^ :]+$', key_blob_or_type)
421
422
423# we can't use @deprecated here because then you can't use the
424# resulting class in isinstance() things and the like, because Twisted
425# makes it into a function instead :( so we @deprecate __init__ for now
426# @deprecated(_Version("txtorcon", 18, 0, 0))
427class EphemeralHiddenService(object):
428    '''
429    Deprecated as of 18.0.0. Please instead use :class:`txtorcon.EphemeralOnionService`
430
431    This uses the ephemeral hidden-service APIs (in comparison to
432    torrc or SETCONF). This means your hidden-service private-key is
433    never in a file. It also means that when the process exits, that
434    HS goes away. See documentation for ADD_ONION in torspec:
435    https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n1295
436    '''
437
438    @deprecated(_Version("txtorcon", 18, 0, 0))
439    def __init__(self, ports, key_blob_or_type='NEW:BEST', auth=[], ver=2):
440        # deprecated; use Tor.create_onion_service
441        warn(
442            'EphemeralHiddenService is deprecated; use EphemeralOnionService instead',
443            DeprecationWarning,
444        )
445        if _is_valid_keyblob(key_blob_or_type):
446            self._key_blob = nativeString(key_blob_or_type)
447        else:
448            raise ValueError(
449                'key_blob_or_type must be a string in the formats '
450                '"NEW:<ALGORITHM>" or "<ALGORITHM>:<KEY>"')
451        if isinstance(ports, (six.text_type, str)):
452            ports = [ports]
453        self._ports = [x.replace(' ', ',') for x in ports]
454        self._keyblob = key_blob_or_type
455        self.auth = auth  # FIXME ununsed
456        # FIXME nicer than assert, plz
457        self.version = ver
458        self.hostname = None
459
460    @defer.inlineCallbacks
461    def add_to_tor(self, protocol):
462        '''
463        Returns a Deferred which fires with 'self' after at least one
464        descriptor has been uploaded. Errback if no descriptor upload
465        succeeds.
466        '''
467
468        upload_d = _await_descriptor_upload(protocol, self, progress=None, await_all_uploads=False)
469
470        # _add_ephemeral_service takes a TorConfig but we don't have
471        # that here ..  and also we're just keeping this for
472        # backwards-compatability anyway so instead of trying to
473        # re-use that helper I'm leaving this original code here. So
474        # this is what it supports and that's that:
475        ports = ' '.join(map(lambda x: 'Port=' + x.strip(), self._ports))
476        cmd = 'ADD_ONION %s %s' % (self._key_blob, ports)
477        ans = yield protocol.queue_command(cmd)
478        ans = find_keywords(ans.split('\n'))
479        self.hostname = ans['ServiceID'] + '.onion'
480        if self._key_blob.startswith('NEW:'):
481            self.private_key = ans['PrivateKey']
482        else:
483            self.private_key = self._key_blob
484
485        log.msg('Created hidden-service at', self.hostname)
486
487        log.msg("Created '{}', waiting for descriptor uploads.".format(self.hostname))
488        yield upload_d
489
490    @defer.inlineCallbacks
491    def remove_from_tor(self, protocol):
492        '''
493        Returns a Deferred which fires with None
494        '''
495        r = yield protocol.queue_command('DEL_ONION %s' % self.hostname[:-6])
496        if r.strip() != 'OK':
497            raise RuntimeError('Failed to remove hidden service: "%s".' % r)
498
499
500def _endpoint_from_socksport_line(reactor, socks_config):
501    """
502    Internal helper.
503
504    Returns an IStreamClientEndpoint for the given config, which is of
505    the same format expected by the SOCKSPort option in Tor.
506    """
507    if socks_config.startswith('unix:'):
508        # XXX wait, can SOCKSPort lines with "unix:/path" still
509        # include options afterwards? What about if the path has a
510        # space in it?
511        return UNIXClientEndpoint(reactor, socks_config[5:])
512
513    # options like KeepAliveIsolateSOCKSAuth can be appended
514    # to a SocksPort line...
515    if ' ' in socks_config:
516        socks_config = socks_config.split()[0]
517    if ':' in socks_config:
518        host, port = socks_config.split(':', 1)
519        port = int(port)
520    else:
521        host = '127.0.0.1'
522        port = int(socks_config)
523    return TCP4ClientEndpoint(reactor, host, port)
524
525
526class TorConfig(object):
527    """This class abstracts out Tor's config, and can be used both to
528    create torrc files from nothing and track live configuration of a Tor
529    instance.
530
531    Also, it gives easy access to all the configuration options
532    present. This is initialized at "bootstrap" time, providing
533    attribute-based access thereafter. Note that after you set some
534    number of items, you need to do a save() before these are sent to
535    Tor (and then they will be done as one SETCONF).
536
537    You may also use this class to construct a configuration from
538    scratch (e.g. to give to :func:`txtorcon.launch_tor`). In this
539    case, values are reflected right away. (If we're not bootstrapped
540    to a Tor, this is the mode).
541
542    Note that you do not need to call save() if you're just using
543    TorConfig to create a .torrc file or for input to launch_tor().
544
545    This class also listens for CONF_CHANGED events to update the
546    cached data in the event other controllers (etc) changed it.
547
548    There is a lot of magic attribute stuff going on in here (which
549    might be a bad idea, overall) but the *intent* is that you can
550    just set Tor options and it will all Just Work. For config items
551    that take multiple values, set that to a list. For example::
552
553        conf = TorConfig(...)
554        conf.SOCKSPort = [9050, 1337]
555        conf.HiddenServices.append(HiddenService(...))
556
557    (Incoming objects, like lists, are intercepted and wrapped).
558
559    FIXME: when is CONF_CHANGED introduced in Tor? Can we do anything
560    like it for prior versions?
561
562    FIXME:
563
564        - HiddenServiceOptions is special: GETCONF on it returns
565          several (well, two) values. Besides adding the two keys 'by
566          hand' do we need to do anything special? Can't we just depend
567          on users doing 'conf.hiddenservicedir = foo' AND
568          'conf.hiddenserviceport = bar' before a save() ?
569
570        - once I determine a value is default, is there any way to
571          actually get what this value is?
572
573    """
574
575    @staticmethod
576    @defer.inlineCallbacks
577    def from_protocol(proto):
578        """
579        This creates and returns a ready-to-go TorConfig instance from the
580        given protocol, which should be an instance of
581        TorControlProtocol.
582        """
583        cfg = TorConfig(control=proto)
584        yield cfg.post_bootstrap
585        defer.returnValue(cfg)
586
587    def __init__(self, control=None):
588        self.config = {}
589        '''Current configuration, by keys.'''
590
591        if control is None:
592            self._protocol = None
593            self.__dict__['_accept_all_'] = None
594
595        else:
596            self._protocol = ITorControlProtocol(control)
597
598        self.unsaved = OrderedDict()
599        '''Configuration that has been changed since last save().'''
600
601        self.parsers = {}
602        '''Instances of the parser classes, subclasses of TorConfigType'''
603
604        self.list_parsers = set(['hiddenservices', 'ephemeralonionservices'])
605        '''All the names (keys from .parsers) that are a List of something.'''
606
607        # during bootstrapping we decide whether we support the
608        # following features. A thing goes in here if TorConfig
609        # behaves differently depending upon whether it shows up in
610        # "GETINFO config/names"
611        self._supports = dict(
612            HiddenServiceDirGroupReadable=False
613        )
614        self._defaults = dict()
615
616        self.post_bootstrap = defer.Deferred()
617        if self.protocol:
618            if self.protocol.post_bootstrap:
619                self.protocol.post_bootstrap.addCallback(
620                    self.bootstrap).addErrback(self.post_bootstrap.errback)
621            else:
622                self.bootstrap()
623
624        else:
625            self.do_post_bootstrap(self)
626
627        self.__dict__['_setup_'] = None
628
629    def socks_endpoint(self, reactor, port=None):
630        """
631        Returns a TorSocksEndpoint configured to use an already-configured
632        SOCKSPort from the Tor we're connected to. By default, this
633        will be the very first SOCKSPort.
634
635        :param port: a str, the first part of the SOCKSPort line (that
636            is, a port like "9151" or a Unix socket config like
637            "unix:/path". You may also specify a port as an int.
638
639        If you need to use a particular port that may or may not
640        already be configured, see the async method
641        :meth:`txtorcon.TorConfig.create_socks_endpoint`
642        """
643
644        if len(self.SocksPort) == 0:
645            raise RuntimeError(
646                "No SOCKS ports configured"
647            )
648
649        socks_config = None
650        if port is None:
651            socks_config = self.SocksPort[0]
652        else:
653            port = str(port)  # in case e.g. an int passed in
654            if ' ' in port:
655                raise ValueError(
656                    "Can't specify options; use create_socks_endpoint instead"
657                )
658
659            for idx, port_config in enumerate(self.SocksPort):
660                # "SOCKSPort" is a gnarly beast that can have a bunch
661                # of options appended, so we have to split off the
662                # first thing which *should* be the port (or can be a
663                # string like 'unix:')
664                if port_config.split()[0] == port:
665                    socks_config = port_config
666                    break
667        if socks_config is None:
668            raise RuntimeError(
669                "No SOCKSPort configured for port {}".format(port)
670            )
671
672        return _endpoint_from_socksport_line(reactor, socks_config)
673
674    @defer.inlineCallbacks
675    def create_socks_endpoint(self, reactor, socks_config):
676        """
677        Creates a new TorSocksEndpoint instance given a valid
678        configuration line for ``SocksPort``; if this configuration
679        isn't already in the underlying tor, we add it. Note that this
680        method may call :meth:`txtorcon.TorConfig.save()` on this instance.
681
682        Note that calling this with `socks_config=None` is equivalent
683        to calling `.socks_endpoint` (which is not async).
684
685        XXX socks_config should be .. i dunno, but there's fucking
686        options and craziness, e.g. default Tor Browser Bundle is:
687        ['9150 IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth',
688        '9155']
689
690        XXX maybe we should say "socks_port" as the 3rd arg, insist
691        it's an int, and then allow/support all the other options
692        (e.g. via kwargs)
693
694        XXX we could avoid the "maybe call .save()" thing; worth it?
695        (actually, no we can't or the Tor won't have it config'd)
696        """
697
698        yield self.post_bootstrap
699
700        if socks_config is None:
701            if len(self.SocksPort) == 0:
702                raise RuntimeError(
703                    "socks_port is None and Tor has no SocksPorts configured"
704                )
705            socks_config = self.SocksPort[0]
706        else:
707            if not any([socks_config in port for port in self.SocksPort]):
708                # need to configure Tor
709                self.SocksPort.append(socks_config)
710                try:
711                    yield self.save()
712                except TorProtocolError as e:
713                    extra = ''
714                    if socks_config.startswith('unix:'):
715                        # XXX so why don't we check this for the
716                        # caller, earlier on?
717                        extra = '\nNote Tor has specific ownership/permissions ' +\
718                                'requirements for unix sockets and parent dir.'
719                    raise RuntimeError(
720                        "While configuring SOCKSPort to '{}', error from"
721                        " Tor: {}{}".format(
722                            socks_config, e, extra
723                        )
724                    )
725
726        defer.returnValue(
727            _endpoint_from_socksport_line(reactor, socks_config)
728        )
729
730    # FIXME should re-name this to "tor_protocol" to be consistent
731    # with other things? Or rename the other things?
732    """
733    read-only access to TorControlProtocol. Call attach_protocol() to
734    set it, which can only be done if we don't already have a
735    protocol.
736    """
737    def _get_protocol(self):
738        return self.__dict__['_protocol']
739    protocol = property(_get_protocol)
740    tor_protocol = property(_get_protocol)
741
742    def attach_protocol(self, proto):
743        """
744        returns a Deferred that fires once we've set this object up to
745        track the protocol. Fails if we already have a protocol.
746        """
747        if self._protocol is not None:
748            raise RuntimeError("Already have a protocol.")
749        # make sure we have nothing in self.unsaved
750        self.save()
751        self.__dict__['_protocol'] = proto
752
753        # FIXME some of this is duplicated from ctor
754        del self.__dict__['_accept_all_']
755        self.__dict__['post_bootstrap'] = defer.Deferred()
756        if proto.post_bootstrap:
757            proto.post_bootstrap.addCallback(self.bootstrap)
758        return self.__dict__['post_bootstrap']
759
760    def __setattr__(self, name, value):
761        """
762        we override this so that we can provide direct attribute
763        access to our config items, and move them into self.unsaved
764        when they've been changed. hiddenservices have to be special
765        unfortunately. the _setup_ thing is so that we can set up the
766        attributes we need in the constructor without uusing __dict__
767        all over the place.
768        """
769
770        # appease flake8's hatred of lambda :/
771        def has_setup_attr(o):
772            return '_setup_' in o.__dict__
773
774        def has_accept_all_attr(o):
775            return '_accept_all_' in o.__dict__
776
777        def is_hidden_services(s):
778            return s.lower() == "hiddenservices"
779
780        if has_setup_attr(self):
781            name = self._find_real_name(name)
782            if not has_accept_all_attr(self) and not is_hidden_services(name):
783                value = self.parsers[name].validate(value, self, name)
784            if isinstance(value, list):
785                value = _ListWrapper(
786                    value, functools.partial(self.mark_unsaved, name))
787
788            name = self._find_real_name(name)
789            self.unsaved[name] = value
790
791        else:
792            super(TorConfig, self).__setattr__(name, value)
793
794    def _maybe_create_listwrapper(self, rn):
795        if rn.lower() in self.list_parsers and rn not in self.config:
796            self.config[rn] = _ListWrapper([], functools.partial(
797                self.mark_unsaved, rn))
798
799    def __getattr__(self, name):
800        """
801        on purpose, we don't return self.unsaved if the key is in there
802        because I want the config to represent the running Tor not
803        ``things which might get into the running Tor if save() were
804        to be called''
805        """
806        rn = self._find_real_name(name)
807        if '_accept_all_' in self.__dict__ and rn in self.unsaved:
808            return self.unsaved[rn]
809        self._maybe_create_listwrapper(rn)
810        v = self.config[rn]
811        if v == DEFAULT_VALUE:
812            v = self.__dict__['_defaults'].get(rn, DEFAULT_VALUE)
813        return v
814
815    def __contains__(self, item):
816        if item in self.unsaved and '_accept_all_' in self.__dict__:
817            return True
818        return item in self.config
819
820    def __iter__(self):
821        '''
822        FIXME needs proper iterator tests in test_torconfig too
823        '''
824        for x in self.config.__iter__():
825            yield x
826        for x in self.__dict__['unsaved'].__iter__():
827            yield x
828
829    def get_type(self, name):
830        """
831        return the type of a config key.
832
833        :param: name the key
834
835        FIXME can we do something more-clever than this for client
836        code to determine what sort of thing a key is?
837        """
838
839        # XXX FIXME uhm...how to do all the different types of hidden-services?
840        if name.lower() == 'hiddenservices':
841            return FilesystemOnionService
842        return type(self.parsers[name])
843
844    def _conf_changed(self, arg):
845        """
846        internal callback. from control-spec:
847
848        4.1.18. Configuration changed
849
850          The syntax is:
851             StartReplyLine *(MidReplyLine) EndReplyLine
852
853             StartReplyLine = "650-CONF_CHANGED" CRLF
854             MidReplyLine = "650-" KEYWORD ["=" VALUE] CRLF
855             EndReplyLine = "650 OK"
856
857          Tor configuration options have changed (such as via a SETCONF or
858          RELOAD signal). KEYWORD and VALUE specify the configuration option
859          that was changed.  Undefined configuration options contain only the
860          KEYWORD.
861        """
862
863        conf = parse_keywords(arg, multiline_values=False)
864        for (k, v) in conf.items():
865            # v will be txtorcon.DEFAULT_VALUE already from
866            # parse_keywords if it was unspecified
867            real_name = self._find_real_name(k)
868            if real_name in self.parsers:
869                v = self.parsers[real_name].parse(v)
870            self.config[real_name] = v
871
872    def bootstrap(self, arg=None):
873        '''
874        This only takes args so it can be used as a callback. Don't
875        pass an arg, it is ignored.
876        '''
877        try:
878            d = self.protocol.add_event_listener(
879                'CONF_CHANGED', self._conf_changed)
880        except RuntimeError:
881            # for Tor versions which don't understand CONF_CHANGED
882            # there's nothing we can really do.
883            log.msg(
884                "Can't listen for CONF_CHANGED event; won't stay up-to-date "
885                "with other clients.")
886            d = defer.succeed(None)
887        d.addCallback(lambda _: self.protocol.get_info_raw("config/names"))
888        d.addCallback(self._do_setup)
889        d.addCallback(self.do_post_bootstrap)
890        d.addErrback(self.do_post_errback)
891
892    def do_post_errback(self, f):
893        self.post_bootstrap.errback(f)
894        return None
895
896    def do_post_bootstrap(self, arg):
897        if not self.post_bootstrap.called:
898            self.post_bootstrap.callback(self)
899        return self
900
901    def needs_save(self):
902        return len(self.unsaved) > 0
903
904    def mark_unsaved(self, name):
905        name = self._find_real_name(name)
906        if name in self.config and name not in self.unsaved:
907            self.unsaved[name] = self.config[self._find_real_name(name)]
908
909    def save(self):
910        """
911        Save any outstanding items. This returns a Deferred which will
912        errback if Tor was unhappy with anything, or callback with
913        this TorConfig object on success.
914        """
915
916        if not self.needs_save():
917            return defer.succeed(self)
918
919        args = []
920        directories = []
921        for (key, value) in self.unsaved.items():
922            if key == 'HiddenServices':
923                self.config['HiddenServices'] = value
924                # using a list here because at least one unit-test
925                # cares about order -- and conceivably order *could*
926                # matter here, to Tor...
927                services = list()
928                # authenticated services get flattened into the HiddenServices list...
929                for hs in value:
930                    if IOnionClient.providedBy(hs):
931                        parent = IOnionClient(hs).parent
932                        if parent not in services:
933                            services.append(parent)
934                    elif isinstance(hs, (EphemeralOnionService, EphemeralHiddenService)):
935                        raise ValueError(
936                            "Only filesystem based Onion services may be added"
937                            " via TorConfig.hiddenservices; ephemeral services"
938                            " must be created with 'create_onion_service'."
939                        )
940                    else:
941                        if hs not in services:
942                            services.append(hs)
943
944                for hs in services:
945                    for (k, v) in hs.config_attributes():
946                        if k == 'HiddenServiceDir':
947                            if v not in directories:
948                                directories.append(v)
949                                args.append(k)
950                                args.append(v)
951                            else:
952                                raise RuntimeError("Trying to add hidden service with same HiddenServiceDir: %s" % v)
953                        else:
954                            args.append(k)
955                            args.append(v)
956                continue
957
958            if isinstance(value, list):
959                for x in value:
960                    # FIXME XXX
961                    if x is not DEFAULT_VALUE:
962                        args.append(key)
963                        args.append(str(x))
964
965            else:
966                args.append(key)
967                args.append(value)
968
969            # FIXME in future we should wait for CONF_CHANGED and
970            # update then, right?
971            real_name = self._find_real_name(key)
972            if not isinstance(value, list) and real_name in self.parsers:
973                value = self.parsers[real_name].parse(value)
974            self.config[real_name] = value
975
976        # FIXME might want to re-think this, but currently there's no
977        # way to put things into a config and get them out again
978        # nicely...unless you just don't assign a protocol
979        if self.protocol:
980            d = self.protocol.set_conf(*args)
981            d.addCallback(self._save_completed)
982            return d
983
984        else:
985            self._save_completed()
986            return defer.succeed(self)
987
988    def _save_completed(self, *args):
989        '''internal callback'''
990        self.__dict__['unsaved'] = {}
991        return self
992
993    def _find_real_name(self, name):
994        keys = list(self.__dict__['parsers'].keys()) + list(self.__dict__['config'].keys())
995        for x in keys:
996            if x.lower() == name.lower():
997                return x
998        return name
999
1000    @defer.inlineCallbacks
1001    def _get_defaults(self):
1002        try:
1003            defaults_raw = yield self.protocol.get_info_raw("config/defaults")
1004            defaults = {}
1005            for line in defaults_raw.split('\n')[1:]:
1006                k, v = line.split(' ', 1)
1007                if k in defaults:
1008                    if isinstance(defaults[k], list):
1009                        defaults[k].append(v)
1010                    else:
1011                        defaults[k] = [defaults[k], v]
1012                else:
1013                    defaults[k] = v
1014        except TorProtocolError:
1015            # must be a version of Tor without config/defaults
1016            defaults = dict()
1017        defer.returnValue(defaults)
1018
1019    @defer.inlineCallbacks
1020    def _do_setup(self, data):
1021        defaults = self.__dict__['_defaults'] = yield self._get_defaults()
1022
1023        for line in data.split('\n'):
1024            if line == "config/names=":
1025                continue
1026
1027            (name, value) = line.split()
1028            if name in self._supports:
1029                self._supports[name] = True
1030
1031            if name == 'HiddenServiceOptions':
1032                # set up the "special-case" hidden service stuff
1033                servicelines = yield self.protocol.get_conf_raw(
1034                    'HiddenServiceOptions')
1035                self._setup_hidden_services(servicelines)
1036                continue
1037
1038            # there's a whole bunch of FooPortLines (where "Foo" is
1039            # "Socks", "Control", etc) and some have defaults, some
1040            # don't but they all have FooPortLines, FooPort, and
1041            # __FooPort definitions so we only "do stuff" for the
1042            # "FooPortLines"
1043            if name.endswith('PortLines'):
1044                rn = self._find_real_name(name[:-5])
1045                self.parsers[rn] = String()  # not Port() because options etc
1046                self.list_parsers.add(rn)
1047                v = yield self.protocol.get_conf(name[:-5])
1048                v = v[name[:-5]]
1049
1050                initial = []
1051                if v == DEFAULT_VALUE or v == 'auto':
1052                    try:
1053                        initial = defaults[name[:-5]]
1054                    except KeyError:
1055                        default_key = '__{}'.format(name[:-5])
1056                        default = yield self.protocol.get_conf_single(default_key)
1057                        if not default:
1058                            initial = []
1059                        else:
1060                            initial = [default]
1061                else:
1062                    initial = [self.parsers[rn].parse(v)]
1063                self.config[rn] = _ListWrapper(
1064                    initial, functools.partial(self.mark_unsaved, rn))
1065
1066            # XXX for Virtual check that it's one of the *Ports things
1067            # (because if not it should be an error)
1068            if value in ('Dependant', 'Dependent', 'Virtual'):
1069                continue
1070
1071            # there's a thing called "Boolean+Auto" which is -1 for
1072            # auto, 0 for false and 1 for true. could be nicer if it
1073            # was called AutoBoolean or something, but...
1074            value = value.replace('+', '_')
1075
1076            inst = None
1077            # FIXME: put parser classes in dict instead?
1078            for cls in config_types:
1079                if cls.__name__ == value:
1080                    inst = cls()
1081            if not inst:
1082                raise RuntimeError("Don't have a parser for: " + value)
1083            v = yield self.protocol.get_conf(name)
1084            v = v[name]
1085
1086            rn = self._find_real_name(name)
1087            self.parsers[rn] = inst
1088            if is_list_config_type(inst.__class__):
1089                self.list_parsers.add(rn)
1090                parsed = self.parsers[rn].parse(v)
1091                if parsed == [DEFAULT_VALUE]:
1092                    parsed = defaults.get(rn, [])
1093                self.config[rn] = _ListWrapper(
1094                    parsed, functools.partial(self.mark_unsaved, rn))
1095
1096            else:
1097                if v == '' or v == DEFAULT_VALUE:
1098                    parsed = self.parsers[rn].parse(defaults.get(rn, DEFAULT_VALUE))
1099                else:
1100                    parsed = self.parsers[rn].parse(v)
1101                self.config[rn] = parsed
1102
1103        # get any ephemeral services we own, or detached services.
1104        # these are *not* _ListWrappers because we don't care if they
1105        # change, nothing in Tor's config exists for these (probably
1106        # begging the question: why are we putting them in here at all
1107        # then...?)
1108        try:
1109            ephemeral = yield self.protocol.get_info('onions/current')
1110        except Exception:
1111            self.config['EphemeralOnionServices'] = []
1112        else:
1113            onions = []
1114            for line in ephemeral['onions/current'].split('\n'):
1115                onion = line.strip()
1116                if onion:
1117                    onions.append(
1118                        EphemeralOnionService(
1119                            self,
1120                            ports=[],  # no way to discover ports=
1121                            hostname=onion,
1122                            private_key=DISCARD,  # we don't know it, anyway
1123                            version=2,
1124                            detach=False,
1125                        )
1126                    )
1127            self.config['EphemeralOnionServices'] = onions
1128
1129        try:
1130            detached = yield self.protocol.get_info('onions/detached')
1131        except Exception:
1132            self.config['DetachedOnionServices'] = []
1133        else:
1134            onions = []
1135            for line in detached['onions/detached'].split('\n'):
1136                onion = line.strip()
1137                if onion:
1138                    onions.append(
1139                        EphemeralOnionService(
1140                            self,
1141                            ports=[],  # no way to discover original ports=
1142                            hostname=onion,
1143                            detach=True,
1144                            private_key=DISCARD,
1145                        )
1146                    )
1147            self.config['DetachedOnionServices'] = onions
1148        defer.returnValue(self)
1149
1150    def _setup_hidden_services(self, servicelines):
1151
1152        def maybe_add_hidden_service():
1153            if directory is not None:
1154                if directory not in directories:
1155                    directories.append(directory)
1156                    if not auth:
1157                        service = FilesystemOnionService(
1158                            self, directory, ports, ver, group_read
1159                        )
1160                        hs.append(service)
1161                    else:
1162                        auth_type, clients = auth.split(' ', 1)
1163                        clients = clients.split(',')
1164                        if auth_type == 'basic':
1165                            auth0 = AuthBasic(clients)
1166                        elif auth_type == 'stealth':
1167                            auth0 = AuthStealth(clients)
1168                        else:
1169                            raise ValueError(
1170                                "Unknown auth type '{}'".format(auth_type)
1171                            )
1172                        parent_service = FilesystemAuthenticatedOnionService(
1173                            self, directory, ports, auth0, ver, group_read
1174                        )
1175                        for client_name in parent_service.client_names():
1176                            hs.append(parent_service.get_client(client_name))
1177                else:
1178                    raise RuntimeError("Trying to add hidden service with same HiddenServiceDir: %s" % directory)
1179
1180        hs = []
1181        directory = None
1182        directories = []
1183        ports = []
1184        ver = None
1185        group_read = None
1186        auth = None
1187        for line in servicelines.split('\n'):
1188            if not len(line.strip()):
1189                continue
1190
1191            if line == 'HiddenServiceOptions':
1192                continue
1193            k, v = line.split('=')
1194            if k == 'HiddenServiceDir':
1195                maybe_add_hidden_service()
1196                directory = v
1197                _directory = directory
1198                directory = os.path.abspath(directory)
1199                if directory != _directory:
1200                    warnings.warn(
1201                        "Directory path: %s changed to absolute path: %s" % (_directory, directory),
1202                        RuntimeWarning
1203                    )
1204                ports = []
1205                ver = None
1206                auth = None
1207                group_read = 0
1208
1209            elif k == 'HiddenServicePort':
1210                ports.append(v)
1211
1212            elif k == 'HiddenServiceVersion':
1213                ver = int(v)
1214
1215            elif k == 'HiddenServiceAuthorizeClient':
1216                if auth is not None:
1217                    # definitely error, or keep going?
1218                    raise ValueError("Multiple HiddenServiceAuthorizeClient lines for one service")
1219                auth = v
1220
1221            elif k == 'HiddenServiceDirGroupReadable':
1222                group_read = int(v)
1223
1224            else:
1225                raise RuntimeError("Can't parse HiddenServiceOptions: " + k)
1226
1227        maybe_add_hidden_service()
1228
1229        name = 'HiddenServices'
1230        self.config[name] = _ListWrapper(
1231            hs, functools.partial(self.mark_unsaved, name))
1232
1233    def config_args(self):
1234        '''
1235        Returns an iterator of 2-tuples (config_name, value), one for each
1236        configuration option in this config. This is more-or-less an
1237        internal method, but see, e.g., launch_tor()'s implementation
1238        if you think you need to use this for something.
1239
1240        See :meth:`txtorcon.TorConfig.create_torrc` which returns a
1241        string which is also a valid ``torrc`` file
1242        '''
1243
1244        everything = dict()
1245        everything.update(self.config)
1246        everything.update(self.unsaved)
1247
1248        for (k, v) in list(everything.items()):
1249            if type(v) is _ListWrapper:
1250                if k.lower() == 'hiddenservices':
1251                    for x in v:
1252                        for (kk, vv) in x.config_attributes():
1253                            yield (str(kk), str(vv))
1254
1255                else:
1256                    # FIXME actually, is this right? don't we want ALL
1257                    # the values in one string?!
1258                    for x in v:
1259                        yield (str(k), str(x))
1260
1261            else:
1262                yield (str(k), str(v))
1263
1264    def create_torrc(self):
1265        rtn = StringIO()
1266
1267        for (k, v) in self.config_args():
1268            rtn.write(u'%s %s\n' % (k, v))
1269
1270        return rtn.getvalue()
1271