1import copy
2import logging
3import os
4from threading import RLock
5
6import socks
7import stem.process
8from stem import (
9    ControllerError,
10    InvalidArguments,
11    InvalidRequest,
12    OperationFailed,
13    ProtocolError,
14    SocketClosed,
15    SocketError,
16    UnsatisfiableRequest,
17)
18from stem.connection import IncorrectSocketType
19from stem.control import Controller, Listener
20
21from sbws import settings
22from sbws.globals import (
23    TORRC_OPTIONS_CAN_FAIL,
24    TORRC_RUNTIME_OPTIONS,
25    TORRC_STARTING_POINT,
26    fail_hard,
27)
28
29log = logging.getLogger(__name__)
30stream_building_lock = RLock()
31
32
33def attach_stream_to_circuit_listener(controller, circ_id):
34    """Returns a function that should be given to add_event_listener(). It
35    looks for newly created streams and attaches them to the given circ_id"""
36
37    def closure_stream_event_listener(st):
38        if st.status == "NEW" and st.purpose == "USER":
39            log.debug(
40                "Attaching stream %s to circ %s %s",
41                st.id,
42                circ_id,
43                circuit_str(controller, circ_id),
44            )
45            try:
46                controller.attach_stream(st.id, circ_id)
47            # So far we never saw this error.
48            except (
49                UnsatisfiableRequest,
50                InvalidRequest,
51                OperationFailed,
52            ) as e:
53                log.debug(
54                    "Error attaching stream %s to circ %s: %s",
55                    st.id,
56                    circ_id,
57                    e,
58                )
59        else:
60            pass
61
62    return closure_stream_event_listener
63
64
65def add_event_listener(controller, func, event):
66    try:
67        controller.add_event_listener(func, event)
68    except ProtocolError as e:
69        log.exception("Exception trying to add event listener %s", e)
70
71
72def remove_event_listener(controller, func):
73    try:
74        controller.remove_event_listener(func)
75    except SocketClosed as e:
76        if not settings.end_event.is_set():
77            log.debug(e)
78        else:
79            log.exception(e)
80    except ProtocolError as e:
81        log.exception("Exception trying to remove event %s", e)
82
83
84def init_controller(conf):
85    c = None
86    # If the external control port is set, use it to initialize the controller.
87    control_port = conf["tor"]["external_control_port"]
88    if control_port:
89        control_port = int(control_port)
90        # If it can not connect, the program will exit here
91        c = _init_controller_port(control_port)
92    # There is no configuration for external control socket, therefore do not
93    # attempt to connect to the control socket.
94    return c
95
96
97def is_bootstrapped(c):
98    try:
99        line = c.get_info("status/bootstrap-phase")
100    except (ControllerError, InvalidArguments, ProtocolError) as e:
101        log.exception("Error trying to check bootstrap phase %s", e)
102        return False
103    state, _, progress, *_ = line.split()
104    progress = int(progress.split("=")[1])
105    if state == "NOTICE" and progress == 100:
106        return True
107    log.debug("Not bootstrapped. state={} progress={}".format(state, progress))
108    return False
109
110
111def _init_controller_port(port):
112    assert isinstance(port, int)
113    try:
114        c = Controller.from_port(port=port)
115        c.authenticate()
116    except (IncorrectSocketType, SocketError):
117        fail_hard("Unable to connect to control port %s.", port)
118    # TODO: Allow for auth via more than just CookieAuthentication
119    log.info("Connected to tor via port %s", port)
120    return c
121
122
123def _init_controller_socket(socket):
124    assert isinstance(socket, str)
125    try:
126        c = Controller.from_socket_file(path=socket)
127        c.authenticate()
128    except (IncorrectSocketType, SocketError):
129        log.debug("Error initting controller socket: socket error.")
130        return None
131    except Exception as e:
132        log.exception("Error initting controller socket: %s", e)
133        return None
134    # TODO: Allow for auth via more than just CookieAuthentication
135    log.info("Connected to tor via socket %s", socket)
136    return c
137
138
139def parse_user_torrc_config(torrc, torrc_text):
140    """Parse the user configuration torrc text call `extra_lines`
141    to a dictionary suitable to use with stem and return a new torrc
142    dictionary that merges that dictionary with the existing torrc.
143    Example::
144
145        [tor]
146        extra_lines =
147            Log debug file /tmp/tor-debug.log
148            NumCPUs 1
149    """
150    torrc_dict = torrc.copy()
151    for line in torrc_text.split("\n"):
152        # Remove leading and trailing whitespace, if any
153        line = line.strip()
154        # Ignore blank lines
155        if len(line) < 1:
156            continue
157        # Some torrc options are only a key, some are a key value pair.
158        kv = line.split(None, 1)
159        if len(kv) > 1:
160            key, value = kv
161        else:
162            key = kv[0]
163            value = None
164        # It's really easy to add to the torrc if the key doesn't exist
165        if key not in torrc:
166            torrc_dict.update({key: value})
167        # But if it does, we have to make a list of values. For example, say
168        # the user wants to add a SocksPort and we already have
169        # 'SocksPort auto' in the torrc. We'll go from
170        #     torrc['SocksPort'] == 'auto'
171        # to
172        #     torrc['SocksPort'] == ['auto', '9050']
173        else:
174            existing_val = torrc[key]
175            if isinstance(existing_val, str):
176                torrc_dict.update({key: [existing_val, value]})
177            else:
178                assert isinstance(existing_val, list)
179                existing_val.append(value)
180                torrc_dict.update({key: existing_val})
181        log.debug(
182            'Adding "%s %s" to torrc with which we are launching Tor',
183            key,
184            value,
185        )
186    return torrc_dict
187
188
189def set_torrc_starting_point(controller):
190    """Set the torrc starting point options."""
191    for k, v in TORRC_STARTING_POINT.items():
192        try:
193            controller.set_conf(k, v)
194        except (ControllerError, InvalidRequest, InvalidArguments) as e:
195            log.exception("Error setting option %s, %s: %s", k, v, e)
196            exit(1)
197
198
199def set_torrc_runtime_options(controller):
200    """Set torrc options at runtime."""
201    try:
202        controller.set_options(TORRC_RUNTIME_OPTIONS)
203    # Only the first option that fails will be logged here.
204    # Just log stem's exceptions.
205    except (ControllerError, InvalidRequest, InvalidArguments) as e:
206        log.exception(e)
207        exit(1)
208
209
210def set_torrc_options_can_fail(controller):
211    """Set options that can fail, at runtime.
212
213    They can be set at launch, but since the may fail because they are not
214    supported in some Tor versions, it's easier to try one by one at runtime
215    and ignore the ones that fail.
216    """
217    for k, v in TORRC_OPTIONS_CAN_FAIL.items():
218        try:
219            controller.set_conf(k, v)
220        except (InvalidArguments, InvalidRequest) as error:
221            log.debug(
222                "Ignoring option not supported by this Tor version. %s", error
223            )
224        except ControllerError as e:
225            log.exception(e)
226            exit(1)
227
228
229def launch_tor(conf):
230    os.makedirs(conf.getpath("tor", "datadir"), mode=0o700, exist_ok=True)
231    os.makedirs(conf.getpath("tor", "log"), exist_ok=True)
232    os.makedirs(conf.getpath("tor", "run_dpath"), mode=0o700, exist_ok=True)
233    # Bare minimum things, more or less
234    torrc = copy.deepcopy(TORRC_STARTING_POINT)
235    # Very important and/or common settings that we don't know until runtime
236    # The rest of the settings are in globals.py
237    torrc.update(
238        {
239            "DataDirectory": conf.getpath("tor", "datadir"),
240            "PidFile": conf.getpath("tor", "pid"),
241            "ControlSocket": conf.getpath("tor", "control_socket"),
242            "Log": [
243                "NOTICE file {}".format(
244                    os.path.join(conf.getpath("tor", "log"), "notice.log")
245                ),
246            ],
247            "CircuitBuildTimeout": conf["general"]["circuit_timeout"],
248        }
249    )
250
251    torrc = parse_user_torrc_config(torrc, conf["tor"]["extra_lines"])
252    # Finally launch Tor
253    try:
254        # If there is already a tor process running with the same control
255        # socket, this will exit here.
256        stem.process.launch_tor_with_config(
257            torrc, init_msg_handler=log.debug, take_ownership=True
258        )
259    except Exception as e:
260        fail_hard("Error trying to launch tor: %s", e)
261    log.info("Started own tor.")
262    # And return a controller to it
263    cont = _init_controller_socket(conf.getpath("tor", "control_socket"))
264    # In the case it was not possible to connect to own tor socket.
265    if not cont:
266        fail_hard("Could not connect to own tor control socket.")
267    return cont
268
269
270def launch_or_connect_to_tor(conf):
271    cont = init_controller(conf)
272    if not cont:
273        cont = launch_tor(conf)
274    else:
275        if not is_torrc_starting_point_set(cont):
276            set_torrc_starting_point(cont)
277    # Set options that can fail at runtime
278    set_torrc_options_can_fail(cont)
279    # Set runtime options
280    set_torrc_runtime_options(cont)
281    log.info("Started or connected to Tor %s.", cont.get_version())
282    return cont
283
284
285def get_socks_info(controller):
286    """Returns the first SocksPort Tor is configured to listen on, in the form
287    of an (address, port) tuple"""
288    try:
289        socks_ports = controller.get_listeners(Listener.SOCKS)
290        return socks_ports[0]
291    except SocketClosed as e:
292        if not settings.end_event.is_set():
293            log.debug(e)
294    # This might need to return the exception if this happen in other cases
295    # than when stopping the scanner.
296    except ControllerError as e:
297        log.debug(e)
298
299
300def only_relays_with_bandwidth(controller, relays, min_bw=None, max_bw=None):
301    """
302    Given a list of relays, only return those that optionally have above
303    **min_bw** and optionally have below **max_bw**, inclusively. If neither
304    min_bw nor max_bw are given, essentially just returns the input list of
305    relays.
306    """
307    assert min_bw is None or min_bw >= 0
308    assert max_bw is None or max_bw >= 0
309    ret = []
310    for relay in relays:
311        assert hasattr(relay, "consensus_bandwidth")
312        if min_bw is not None and relay.consensus_bandwidth < min_bw:
313            continue
314        if max_bw is not None and relay.consensus_bandwidth > max_bw:
315            continue
316        ret.append(relay)
317    return ret
318
319
320def circuit_str(controller, circ_id):
321    assert isinstance(circ_id, str)
322    int(circ_id)
323    try:
324        circ = controller.get_circuit(circ_id)
325    except ValueError as e:
326        log.warning(
327            "Circuit %s no longer seems to exist so can't return "
328            "a valid circuit string for it: %s",
329            circ_id,
330            e,
331        )
332        return None
333    # exceptions raised when stopping the scanner
334    except (ControllerError, SocketClosed, socks.GeneralProxyError) as e:
335        log.debug(e)
336        return None
337    return (
338        "["
339        + " -> ".join(["{} ({})".format(n, fp[0:8]) for fp, n in circ.path])
340        + "]"
341    )
342
343
344def is_torrc_starting_point_set(tor_controller):
345    """Verify that the tor controller has the correct configuration.
346
347    When connecting to a tor controller that has not been launched by sbws,
348    it should have been configured to work with sbws.
349
350    """
351    bad_options = False
352    torrc = TORRC_STARTING_POINT
353    for k, v in torrc.items():
354        value_set = tor_controller.get_conf(k)
355        if v != value_set:
356            log.exception(
357                "Incorrectly configured %s, should be %s, is %s",
358                k,
359                v,
360                value_set,
361            )
362            bad_options = True
363    if not bad_options:
364        log.info("Tor is correctly configured to work with sbws.")
365    return bad_options
366