stem.py 9.66 KB
Newer Older
1
2
import socks

3
from stem.control import (Controller, Listener)
4
from stem import (SocketError, InvalidRequest, UnsatisfiableRequest,
5
                  OperationFailed, ControllerError, InvalidArguments,
6
                  ProtocolError, SocketClosed)
7
from stem.connection import IncorrectSocketType
Matt Traudt's avatar
Matt Traudt committed
8
import stem.process
9
from configparser import ConfigParser
10
from threading import RLock
11
import copy
12
import logging
Matt Traudt's avatar
Matt Traudt committed
13
import os
14
from sbws.globals import fail_hard
15
from sbws.globals import TORRC_STARTING_POINT, TORRC_RUNTIME_OPTIONS
16
17

log = logging.getLogger(__name__)
18
19
stream_building_lock = RLock()

20

21
def attach_stream_to_circuit_listener(controller, circ_id):
Matt Traudt's avatar
Matt Traudt committed
22
23
    ''' 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
24

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


def add_event_listener(controller, func, event):
43
44
    try:
        controller.add_event_listener(func, event)
45
    except ProtocolError as e:
46
        log.exception("Exception trying to add event listener %s", e)
Matt Traudt's avatar
Matt Traudt committed
47
48


49
def remove_event_listener(controller, func):
50
51
    try:
        controller.remove_event_listener(func)
52
    except ProtocolError as e:
53
        log.exception("Exception trying to remove event %s", e)
Matt Traudt's avatar
Matt Traudt committed
54
55


56
def init_controller(port=None, path=None, set_custom_stream_settings=True):
57
58
59
    # 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
60
61
62
63
64
65
66
67
68
69
    # 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:
70
            return None, 'Unable to reach tor on control port'
71
72
73
    else:
        c = _init_controller_socket(path)
        if not c:
74
            return None, 'Unable to reach tor on control socket'
75
76
    assert c is not None
    if set_custom_stream_settings:
77
78
79
80
81
82
        # These options are also set in launch_tor.
        # In a future refactor they could be set in the case they are not
        # already in the running instance. This way the controller_port
        # could also be used.
        set_torrc_options_can_fail(c)
        set_torrc_runtime_options(c)
83
84
85
    return c, ''


86
def is_bootstrapped(c):
87
88
    try:
        line = c.get_info('status/bootstrap-phase')
89
    except (ControllerError, InvalidArguments, ProtocolError) as e:
90
        log.exception("Error trying to check bootstrap phase %s", e)
91
92
93
94
95
96
97
98
99
        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
100
101
102
103
def _init_controller_port(port):
    assert isinstance(port, int)
    try:
        c = Controller.from_port(port=port)
104
105
        c.authenticate()
    except (IncorrectSocketType, SocketError):
Matt Traudt's avatar
Matt Traudt committed
106
107
108
109
110
111
112
113
114
        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)
115
116
        c.authenticate()
    except (IncorrectSocketType, SocketError):
117
118
119
120
        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
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 parse_user_torrc_config(torrc, torrc_text):
    """Parse the user configuration torrc text call `extra_lines`
128
129
    to a dictionary suitable to use with stem and return a new torrc
    dictionary that merges that dictionary with the existing torrc.
130
131
132
133
134
135
    Example:
        [tor]
        extra_lines =
            Log debug file /tmp/tor-debug.log
            NumCPUs 1
    """
136
    torrc_dict = torrc.copy()
137
    for line in torrc_text.split('\n'):
138
        # Remove leading and trailing whitespace, if any
139
        line = line.strip()
140
        # Ignore blank lines
141
142
        if len(line) < 1:
            continue
143
        # Some torrc options are only a key, some are a key value pair.
144
        kv = line.split(None, 1)
145
146
147
148
149
        if len(kv) > 1:
            key, value = kv
        else:
            key = kv[0]
            value = None
150
        # It's really easy to add to the torrc if the key doesn't exist
Matt Traudt's avatar
Matt Traudt committed
151
        if key not in torrc:
152
            torrc_dict.update({key: value})
153
154
155
        # 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
156
        #     torrc['SocksPort'] == 'auto'
157
        # to
Matt Traudt's avatar
Matt Traudt committed
158
        #     torrc['SocksPort'] == ['auto', '9050']
159
        else:
Matt Traudt's avatar
Matt Traudt committed
160
161
            existing_val = torrc[key]
            if isinstance(existing_val, str):
162
                torrc_dict.update({key: [existing_val, value]})
163
            else:
Matt Traudt's avatar
Matt Traudt committed
164
165
                assert isinstance(existing_val, list)
                existing_val.append(value)
166
                torrc_dict.update({key: existing_val})
167
168
        log.debug('Adding "%s %s" to torrc with which we are launching Tor',
                  key, value)
169
    return torrc_dict
170
171


172
173
174
175
def set_torrc_runtime_options(controller):
    """Set torrc options at runtime."""
    try:
        controller.set_options(TORRC_RUNTIME_OPTIONS)
176
177
178
179
    # Only the first option that fails will be logged here.
    # Just log stem's exceptions.
    except (ControllerError, InvalidRequest, InvalidArguments) as e:
        log.exception(e)
180
181
        exit(1)

182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def launch_tor(conf):
    assert isinstance(conf, ConfigParser)
    os.makedirs(conf.getpath('tor', 'datadir'), mode=0o700, exist_ok=True)
    os.makedirs(conf.getpath('tor', 'log'), exist_ok=True)
    os.makedirs(conf.getpath('tor', 'run_dpath'), mode=0o700, exist_ok=True)
    # Bare minimum things, more or less
    torrc = copy.deepcopy(TORRC_STARTING_POINT)
    # Very important and/or common settings that we don't know until runtime
    torrc.update({
        'DataDirectory': conf.getpath('tor', 'datadir'),
        'PidFile': conf.getpath('tor', 'pid'),
        'ControlSocket': conf.getpath('tor', 'control_socket'),
        'Log': [
            'NOTICE file {}'.format(os.path.join(conf.getpath('tor', 'log'),
                                                 'notice.log')),
        ],
        # 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'],
    })

    torrc = parse_user_torrc_config(torrc, conf['tor']['extra_lines'])
206
    # Finally launch Tor
207
208
209
210
211
    try:
        stem.process.launch_tor_with_config(
            torrc, init_msg_handler=log.debug, take_ownership=True)
    except Exception as e:
        fail_hard('Error trying to launch tor: %s', e)
212
    # And return a controller to it
213
    cont = _init_controller_socket(conf.getpath('tor', 'control_socket'))
214
215
216

    set_torrc_runtime_options(cont)

217
    log.info('Started and connected to Tor %s via %s', cont.get_version(),
218
             conf.getpath('tor', 'control_socket'))
219
    return cont
220
221
222
223
224


def get_socks_info(controller):
    ''' Returns the first SocksPort Tor is configured to listen on, in the form
    of an (address, port) tuple '''
225
226
227
    try:
        socks_ports = controller.get_listeners(Listener.SOCKS)
        return socks_ports[0]
228
    except ControllerError as e:
229
230
        log.exception("Exception trying to get socks info: %e.", e)
        exit(1)
231
232


233
234
235
236
237
238
239
240
241
242
243
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:
juga's avatar
juga committed
244
245
        assert hasattr(relay, 'consensus_bandwidth')
        if min_bw is not None and relay.consensus_bandwidth < min_bw:
246
            continue
juga's avatar
juga committed
247
        if max_bw is not None and relay.consensus_bandwidth > max_bw:
248
249
250
            continue
        ret.append(relay)
    return ret
251
252
253
254
255


def circuit_str(controller, circ_id):
    assert isinstance(circ_id, str)
    int(circ_id)
256
257
258
259
260
261
    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
262
263
264
    # exceptions raised when stopping the scanner
    except (ControllerError, SocketClosed, socks.GeneralProxyError) as e:
        log.debug(e)
265
        return None
266
267
268
    return '[' +\
        ' -> '.join(['{} ({})'.format(n, fp[0:8]) for fp, n in circ.path]) +\
        ']'