stem.py 9.06 KB
Newer Older
1
from stem.control import (Controller, Listener)
Matt Traudt's avatar
Matt Traudt committed
2
from stem import (SocketError, InvalidRequest, UnsatisfiableRequest)
3
from stem.connection import IncorrectSocketType
Matt Traudt's avatar
Matt Traudt committed
4
import stem.process
5
from stem.descriptor.router_status_entry import RouterStatusEntryV3
6
from configparser import ConfigParser
7
from threading import RLock
8
import copy
9
import logging
Matt Traudt's avatar
Matt Traudt committed
10
import os
11
from sbws.globals import fail_hard
12
from sbws.globals import TORRC_STARTING_POINT
13
14

log = logging.getLogger(__name__)
15
16
stream_building_lock = RLock()

17

18
19
20
21
22
23
24
25
26
27
28
def fp_or_nick_to_relay(controller, fp_nick):
    ''' Takes a string that could be either a relay's fingerprint or nickname.
    Return the relay's descriptor if found. Otherwise return None.

    Note that if a nickname is given and multiple relays have that nickname,
    only one of them will be returned. '''
    assert isinstance(fp_nick, str)
    assert is_controller_okay(controller)
    return controller.get_network_status(fp_nick, default=None)


29
def attach_stream_to_circuit_listener(controller, circ_id):
Matt Traudt's avatar
Matt Traudt committed
30
31
    ''' Returns a function that should be given to add_event_listener(). It
    looks for newly created streams and attaches them to the given circ_id '''
Matt Traudt's avatar
Matt Traudt committed
32
    assert is_controller_okay(controller)
Matt Traudt's avatar
Matt Traudt committed
33

Matt Traudt's avatar
Matt Traudt committed
34
35
    def closure_stream_event_listener(st):
        if st.status == 'NEW' and st.purpose == 'USER':
36
37
            log.debug('Attaching stream %s to circ %s %s', st.id, circ_id,
                      circuit_str(controller, circ_id))
Matt Traudt's avatar
Matt Traudt committed
38
39
40
            try:
                controller.attach_stream(st.id, circ_id)
            except (UnsatisfiableRequest, InvalidRequest) as e:
41
42
                log.warning('Couldn\'t attach stream to circ %s: %s',
                            circ_id, e)
Matt Traudt's avatar
Matt Traudt committed
43
44
45
46
47
48
49
50
51
52
        else:
            pass
    return closure_stream_event_listener


def add_event_listener(controller, func, event):
    assert is_controller_okay(controller)
    controller.add_event_listener(func, event)


53
def remove_event_listener(controller, func):
Matt Traudt's avatar
Matt Traudt committed
54
    if not is_controller_okay(controller):
55
        log.warning('Controller not okay so not trying to remove event')
Matt Traudt's avatar
Matt Traudt committed
56
57
58
59
        return
    controller.remove_event_listener(func)


60
def init_controller(port=None, path=None, set_custom_stream_settings=True):
61
62
63
    # NOTE: we do not currently support a control port even though the rest of
    # this function will pretend like port could be set.
    assert port is None
64
65
66
67
68
69
70
71
72
73
    # make sure only one is set
    assert port is not None or path is not None
    assert not (port is not None and path is not None)
    # and for the one that is set, make sure it is likely valid
    assert port is None or isinstance(port, int)
    assert path is None or isinstance(path, str)
    c = None
    if port:
        c = _init_controller_port(port)
        if not c:
74
            return None, 'Unable to reach tor on control port'
75
76
77
    else:
        c = _init_controller_socket(path)
        if not c:
78
            return None, 'Unable to reach tor on control socket'
79
80
81
82
    assert c is not None
    if set_custom_stream_settings:
        c.set_conf('__DisablePredictedCircuits', '1')
        c.set_conf('__LeaveStreamsUnattached', '1')
83
84
85
    return c, ''


86
87
88
89
90
91
92
93
94
95
96
97
def is_bootstrapped(c):
    if not is_controller_okay(c):
        return False
    line = c.get_info('status/bootstrap-phase')
    state, _, progress, *_ = line.split()
    progress = int(progress.split('=')[1])
    if state == 'NOTICE' and progress == 100:
        return True
    log.debug('Not bootstrapped. state={} progress={}'.format(state, progress))
    return False


Matt Traudt's avatar
Matt Traudt committed
98
99
100
101
102
103
104
105
106
107
def is_controller_okay(c):
    if not c:
        return False
    return c.is_alive() and c.is_authenticated()


def _init_controller_port(port):
    assert isinstance(port, int)
    try:
        c = Controller.from_port(port=port)
108
109
        c.authenticate()
    except (IncorrectSocketType, SocketError):
Matt Traudt's avatar
Matt Traudt committed
110
111
112
113
114
115
116
117
118
        return None
    # TODO: Allow for auth via more than just CookieAuthentication
    return c


def _init_controller_socket(socket):
    assert isinstance(socket, str)
    try:
        c = Controller.from_socket_file(path=socket)
119
120
        c.authenticate()
    except (IncorrectSocketType, SocketError):
Matt Traudt's avatar
Matt Traudt committed
121
122
123
        return None
    # TODO: Allow for auth via more than just CookieAuthentication
    return c
Matt Traudt's avatar
Matt Traudt committed
124
125
126
127


def launch_tor(conf):
    assert isinstance(conf, ConfigParser)
128
    os.makedirs(conf['tor']['datadir'], mode=0o700, exist_ok=True)
Matt Traudt's avatar
Matt Traudt committed
129
    # Bare minimum things, more or less
Matt Traudt's avatar
Matt Traudt committed
130
    torrc = copy.deepcopy(TORRC_STARTING_POINT)
131
    # Very important and/or common settings that we don't know until runtime
Matt Traudt's avatar
Matt Traudt committed
132
    torrc.update({
133
134
135
        'DataDirectory': conf['tor']['datadir'],
        'PidFile': os.path.join(conf['tor']['datadir'], 'tor.pid'),
        'ControlSocket': conf['tor']['control_socket'],
Matt Traudt's avatar
Matt Traudt committed
136
        'Log': [
137
            'NOTICE file {}'.format(conf['tor']['log']),
Matt Traudt's avatar
Matt Traudt committed
138
        ],
139
140
141
142
143
        # Things needed to make circuits fail a little faster. We get the
        # circuit_timeout as a string instead of an int on purpose: stem only
        # accepts strings.
        'LearnCircuitBuildTimeout': '0',
        'CircuitBuildTimeout': conf['general']['circuit_timeout'],
Matt Traudt's avatar
Matt Traudt committed
144
    })
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
    # This block of code reads additional torrc lines from the user's
    # config.ini so they can add arbitrary additional options.
    #
    # The user can't replace our options, only add to them. For example,
    # there's no way to remove 'SocksPort auto' (if it is still in
    # TORRC_STARTING_POINT). If you add a SocksPort in your config.ini, you'll
    # open two socks ports.
    #
    # As an example, maybe the user hates their HDD and wants to fill it with
    # debug logs, and wants to tell Tor to use only 1 CPU core.
    #
    #     [tor]
    #     extra_lines =
    #         Log debug file /tmp/tor-debug.log
    #         NumCPUs 1
160
    for line in conf['tor']['extra_lines'].split('\n'):
161
        # Remove leading and trailing whitespace, if any
162
        line = line.strip()
163
        # Ignore blank lines
164
165
        if len(line) < 1:
            continue
166
167
        # The way stem handles configuring Tor with a dictionary is the first
        # word is a key and the remaining words are the value.
168
169
        kv = line.split(None, 1)
        if len(kv) < 2:
170
171
            fail_hard('All torrc lines must have 2 or more words. "%s" has '
                      'fewer', line)
172
        key, value = kv
173
174
        log.info('Adding "%s %s" to torrc with which we are launching Tor',
                 key, value)
175
        # It's really easy to add to the torrc if the key doesn't exist
Matt Traudt's avatar
Matt Traudt committed
176
177
        if key not in torrc:
            torrc.update({key: value})
178
179
180
        # But if it does, we have to make a list of values. For example, say
        # the user wants to add a SocksPort and we already have
        # 'SocksPort auto' in the torrc. We'll go from
Matt Traudt's avatar
Matt Traudt committed
181
        #     torrc['SocksPort'] == 'auto'
182
        # to
Matt Traudt's avatar
Matt Traudt committed
183
        #     torrc['SocksPort'] == ['auto', '9050']
184
        else:
Matt Traudt's avatar
Matt Traudt committed
185
186
187
            existing_val = torrc[key]
            if isinstance(existing_val, str):
                torrc.update({key: [existing_val, value]})
188
            else:
Matt Traudt's avatar
Matt Traudt committed
189
190
191
                assert isinstance(existing_val, list)
                existing_val.append(value)
                torrc.update({key: existing_val})
192
    # Finally launch Tor
193
    stem.process.launch_tor_with_config(
Matt Traudt's avatar
Matt Traudt committed
194
        torrc, init_msg_handler=log.debug, take_ownership=True)
195
    # And return a controller to it
196
    cont = _init_controller_socket(conf['tor']['control_socket'])
197
    assert is_controller_okay(cont)
198
199
200
    # Because we build things by hand and can't set these before Tor bootstraps
    cont.set_conf('__DisablePredictedCircuits', '1')
    cont.set_conf('__LeaveStreamsUnattached', '1')
201
    log.info('Started and connected to Tor %s via %s', cont.get_version(),
202
             conf['tor']['control_socket'])
203
    return cont
204
205
206
207
208
209
210
211


def get_socks_info(controller):
    ''' Returns the first SocksPort Tor is configured to listen on, in the form
    of an (address, port) tuple '''
    assert is_controller_okay(controller)
    socks_ports = controller.get_listeners(Listener.SOCKS)
    return socks_ports[0]
212
213


214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
def only_relays_with_bandwidth(controller, relays, min_bw=None, max_bw=None):
    '''
    Given a list of relays, only return those that optionally have above
    **min_bw** and optionally have below **max_bw**, inclusively. If neither
    min_bw nor max_bw are given, essentially just returns the input list of
    relays.
    '''
    assert is_controller_okay(controller)
    assert min_bw is None or min_bw >= 0
    assert max_bw is None or max_bw >= 0
    ret = []
    for relay in relays:
        assert isinstance(relay, RouterStatusEntryV3)
        if min_bw is not None and relay.bandwidth < min_bw:
            continue
        if max_bw is not None and relay.bandwidth > max_bw:
            continue
        ret.append(relay)
    return ret
233
234
235
236
237
238


def circuit_str(controller, circ_id):
    assert is_controller_okay(controller)
    assert isinstance(circ_id, str)
    int(circ_id)
239
240
241
242
243
244
    try:
        circ = controller.get_circuit(circ_id)
    except ValueError as e:
        log.warning('Circuit %s no longer seems to exist so can\'t return '
                    'a valid circuit string for it: %s', circ_id, e)
        return None
245
246
247
    return '[' +\
        ' -> '.join(['{} ({})'.format(n, fp[0:8]) for fp, n in circ.path]) +\
        ']'