stem.py 8.95 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 configparser import ConfigParser
6
from threading import RLock
7
import copy
8
import logging
Matt Traudt's avatar
Matt Traudt committed
9
import os
10
from sbws.globals import fail_hard
11
from sbws.globals import TORRC_STARTING_POINT
12
13

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

16

17
def attach_stream_to_circuit_listener(controller, circ_id):
Matt Traudt's avatar
Matt Traudt committed
18
19
    ''' 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
20

Matt Traudt's avatar
Matt Traudt committed
21
22
    def closure_stream_event_listener(st):
        if st.status == 'NEW' and st.purpose == 'USER':
23
24
            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
25
26
27
            try:
                controller.attach_stream(st.id, circ_id)
            except (UnsatisfiableRequest, InvalidRequest) as e:
28
29
                log.warning('Couldn\'t attach stream to circ %s: %s',
                            circ_id, e)
30
31
            except Exception as e:
                log.exception("Exception trying to get ns %s", e)
Matt Traudt's avatar
Matt Traudt committed
32
33
34
35
36
37
        else:
            pass
    return closure_stream_event_listener


def add_event_listener(controller, func, event):
38
39
40
41
    try:
        controller.add_event_listener(func, event)
    except Exception as e:
        log.exception("Exception trying to add event listener %s", e)
Matt Traudt's avatar
Matt Traudt committed
42
43


44
def remove_event_listener(controller, func):
45
46
47
48
    try:
        controller.remove_event_listener(func)
    except Exception as e:
        log.exception("Exception trying to remove event %s", e)
Matt Traudt's avatar
Matt Traudt committed
49
50


51
def init_controller(port=None, path=None, set_custom_stream_settings=True):
52
53
54
    # 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
55
56
57
58
59
60
61
62
63
64
    # 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:
65
            return None, 'Unable to reach tor on control port'
66
67
68
    else:
        c = _init_controller_socket(path)
        if not c:
69
            return None, 'Unable to reach tor on control socket'
70
71
72
73
    assert c is not None
    if set_custom_stream_settings:
        c.set_conf('__DisablePredictedCircuits', '1')
        c.set_conf('__LeaveStreamsUnattached', '1')
74
75
76
    return c, ''


77
def is_bootstrapped(c):
78
79
80
81
    try:
        line = c.get_info('status/bootstrap-phase')
    except Exception as e:
        log.exception("Exception bootstrapping %s", e)
82
83
84
85
86
87
88
89
90
        return False
    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
91
92
93
94
def _init_controller_port(port):
    assert isinstance(port, int)
    try:
        c = Controller.from_port(port=port)
95
96
        c.authenticate()
    except (IncorrectSocketType, SocketError):
Matt Traudt's avatar
Matt Traudt committed
97
98
99
100
101
102
103
104
105
        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)
106
107
        c.authenticate()
    except (IncorrectSocketType, SocketError):
108
109
110
111
        log.debug("Error initting controller socket: socket error.")
        return None
    except Exception as e:
        log.exception("Error initting controller socket: %s", e)
Matt Traudt's avatar
Matt Traudt committed
112
113
114
        return None
    # TODO: Allow for auth via more than just CookieAuthentication
    return c
Matt Traudt's avatar
Matt Traudt committed
115
116
117
118


def launch_tor(conf):
    assert isinstance(conf, ConfigParser)
119
    os.makedirs(conf['tor']['datadir'], mode=0o700, exist_ok=True)
Matt Traudt's avatar
Matt Traudt committed
120
    # Bare minimum things, more or less
Matt Traudt's avatar
Matt Traudt committed
121
    torrc = copy.deepcopy(TORRC_STARTING_POINT)
122
    # Very important and/or common settings that we don't know until runtime
Matt Traudt's avatar
Matt Traudt committed
123
    torrc.update({
124
125
126
        '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
127
        'Log': [
128
            'NOTICE file {}'.format(conf['tor']['log']),
Matt Traudt's avatar
Matt Traudt committed
129
        ],
130
131
132
133
134
        # 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
135
    })
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
    # 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
151
    for line in conf['tor']['extra_lines'].split('\n'):
152
        # Remove leading and trailing whitespace, if any
153
        line = line.strip()
154
        # Ignore blank lines
155
156
        if len(line) < 1:
            continue
157
158
        # The way stem handles configuring Tor with a dictionary is the first
        # word is a key and the remaining words are the value.
159
160
        kv = line.split(None, 1)
        if len(kv) < 2:
161
162
            fail_hard('All torrc lines must have 2 or more words. "%s" has '
                      'fewer', line)
163
        key, value = kv
164
165
        log.info('Adding "%s %s" to torrc with which we are launching Tor',
                 key, value)
166
        # It's really easy to add to the torrc if the key doesn't exist
Matt Traudt's avatar
Matt Traudt committed
167
168
        if key not in torrc:
            torrc.update({key: value})
169
170
171
        # 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
172
        #     torrc['SocksPort'] == 'auto'
173
        # to
Matt Traudt's avatar
Matt Traudt committed
174
        #     torrc['SocksPort'] == ['auto', '9050']
175
        else:
Matt Traudt's avatar
Matt Traudt committed
176
177
178
            existing_val = torrc[key]
            if isinstance(existing_val, str):
                torrc.update({key: [existing_val, value]})
179
            else:
Matt Traudt's avatar
Matt Traudt committed
180
181
182
                assert isinstance(existing_val, list)
                existing_val.append(value)
                torrc.update({key: existing_val})
183
    # Finally launch Tor
184
    stem.process.launch_tor_with_config(
Matt Traudt's avatar
Matt Traudt committed
185
        torrc, init_msg_handler=log.debug, take_ownership=True)
186
    # And return a controller to it
187
    cont = _init_controller_socket(conf['tor']['control_socket'])
188
189
190
    # Because we build things by hand and can't set these before Tor bootstraps
    cont.set_conf('__DisablePredictedCircuits', '1')
    cont.set_conf('__LeaveStreamsUnattached', '1')
191
192
193
194
195
196
    try:
        log.info('Started and connected to Tor %s via %s', cont.get_version(),
                 conf['tor']['control_socket'])
        return cont
    except Exception as e:
        log.exception("Exception trying to launch tor %s", e)
197
198
199
200
201


def get_socks_info(controller):
    ''' Returns the first SocksPort Tor is configured to listen on, in the form
    of an (address, port) tuple '''
202
203
204
205
206
207
    try:
        socks_ports = controller.get_listeners(Listener.SOCKS)
        return socks_ports[0]
    except Exception as e:
        log.exception("Exception trying to get socks info: %e.", e)
        exit(1)
208
209


210
211
212
213
214
215
216
217
218
219
220
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 min_bw is None or min_bw >= 0
    assert max_bw is None or max_bw >= 0
    ret = []
    for relay in relays:
221
        assert hasattr(relay, 'bandwidth')
222
223
224
225
226
227
        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
228
229
230
231
232


def circuit_str(controller, circ_id):
    assert isinstance(circ_id, str)
    int(circ_id)
233
234
235
236
237
238
    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
239
240
241
    except Exception as e:
        log.exception("Exception trying to get circuit string %s", e)
        return None
242
243
244
    return '[' +\
        ' -> '.join(['{} ({})'.format(n, fp[0:8]) for fp, n in circ.path]) +\
        ']'