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