circuitbuilder.py 6.71 KB
Newer Older
1
from stem import CircuitExtensionFailed, InvalidRequest, ProtocolError, Timeout
2
from stem import InvalidArguments
3
import random
4
from .relaylist import Relay
5
6
7
import logging

log = logging.getLogger(__name__)
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26


class PathLengthException(Exception):
    def __init__(self, message=None, errors=None):
        if message is not None:
            super().__init__(message)
        else:
            super().__init__()
        self.errors = errors


def valid_circuit_length(path):
    assert isinstance(path, int) or isinstance(path, list)
    if isinstance(path, int):
        return path > 0 and path <= 8
    return len(path) > 0 and len(path) <= 8


class CircuitBuilder:
Matt Traudt's avatar
Matt Traudt committed
27
28
    ''' The CircuitBuilder interface.

29
30
    Subclasses must implement their own build_circuit() function.
    Subclasses probably shouldn't implement their own get_circuit_path().
Matt Traudt's avatar
Matt Traudt committed
31
32
33
34
35
36
37
38
39
40
    Subclasses may keep additional state if they'd find it helpful.

    The primary way to use a CircuitBuilder of any type is to simply create it
    and then call cb.build_circuit(...) with any options that your
    CircuitBuilder type needs.

    It might be good practice to close circuits as you find you no longer need
    them, but CircuitBuilder will keep track of existing circuits and close
    them when it is deleted.
    '''
41
42
    def __init__(self, args, conf, controller, relay_list,
                 close_circuits_on_exit=True):
Matt Traudt's avatar
Matt Traudt committed
43
        self.controller = controller
44
        self.rng = random.SystemRandom()
45
        self.relay_list = relay_list
46
47
        self.built_circuits = set()
        self.close_circuits_on_exit = close_circuits_on_exit
48
        self.circuit_timeout = conf.getint('general', 'circuit_timeout')
49

50
51
52
    @property
    def relays(self):
        return self.relay_list.relays
53
54
55

    def build_circuit(self, *a, **kw):
        ''' Implementations of this method should build the circuit and return
Matt Traudt's avatar
Matt Traudt committed
56
        its (str) ID. If it cannot be built, it should return None. '''
57
58
        raise NotImplementedError()

Matt Traudt's avatar
Matt Traudt committed
59
60
    def get_circuit_path(self, circ_id):
        c = self.controller
61
62
63
64
65
66
67
        try:
            circ = c.get_circuit(circ_id, default=None)
        except Exception as e:
            log.exception("Exception trying to get circuit: %s.", e)
        else:
            return [relay[0] for relay in circ.path]
        return None
Matt Traudt's avatar
Matt Traudt committed
68

69
70
    def close_circuit(self, circ_id):
        c = self.controller
71
72
        try:
            c.get_circuit(circ_id, default=None)
73
74
75
76
            try:
                c.close_circuit(circ_id)
            except InvalidArguments:
                pass
77
            self.built_circuits.discard(circ_id)
78
79
        except Exception as e:
            log.exception("Error trying to get circuit to close it: %s.", e)
80
81
82
83
84

    def _build_circuit_impl(self, path):
        if not valid_circuit_length(path):
            raise PathLengthException()
        c = self.controller
85
        timeout = self.circuit_timeout
86
87
        fp_path = '[' + ' -> '.join([p[0:8] for p in path]) + ']'
        log.debug('Building %s', fp_path)
Matt Traudt's avatar
Matt Traudt committed
88
89
        for _ in range(0, 3):
            try:
90
91
                circ_id = c.new_circuit(
                    path, await_build=True, timeout=timeout)
Matt Traudt's avatar
Matt Traudt committed
92
            except (InvalidRequest, CircuitExtensionFailed,
93
                    ProtocolError, Timeout) as e:
94
                log.warning(e)
Matt Traudt's avatar
Matt Traudt committed
95
                continue
96
97
98
            except Exception as e:
                log.exception("Exception trying to build circuit: %s.", e)
                continue
99
            else:
100
                return circ_id
Matt Traudt's avatar
Matt Traudt committed
101
        return None
102
103

    def __del__(self):
Matt Traudt's avatar
Matt Traudt committed
104
        c = self.controller
105
106
107
        if not self.close_circuits_on_exit:
            return
        for circ_id in self.built_circuits:
108
109
            try:
                c.get_circuit(circ_id, default=None)
110
111
112
113
                try:
                    c.close_circuit(circ_id)
                except InvalidArguments:
                    pass
114
115
116
            except Exception as e:
                log.exception("Exception trying to get circuit to delete: %s",
                              e)
117
118
119
120
121
122
123
124
125
126
        self.built_circuits.clear()


class GapsCircuitBuilder(CircuitBuilder):
    ''' The build_circuit member function takes a list. Falsey values in the
    list will be replaced with relays chosen uniformally at random; Truthy
    values will be assumed to be relays. '''
    def __init__(self, *a, **kw):
        super().__init__(*a, **kw)

127
    def _normalize_path(self, path):
Matt Traudt's avatar
Matt Traudt committed
128
129
130
131
132
133
134
        ''' Change fingerprints/nicks to relay descriptor and change Falsey
        values to None. Return the new path, or None if error '''
        new_path = []
        for fp in path:
            if not fp:
                new_path.append(None)
                continue
135
136
137
            relay = Relay(fp, self.controller)
            if not relay.fingerprint:
                log.debug('Tor seems to no longer think %s is a relay', fp)
Matt Traudt's avatar
Matt Traudt committed
138
139
140
                return None
            new_path.append(relay)
        return new_path
141

142
143
144
145
146
147
148
149
150
151
    def _random_sample_relays(self, number, blacklist):
        ''' Get <number> random relays from self.relays that are not in the
        blacklist. Return None if it cannot be done because too many are
        blacklisted. Otherwise return a list of relays. '''
        all_fps = [r.fingerprint for r in self.relays]
        black_fps = [r.fingerprint for r in blacklist]
        if len(black_fps) + number > len(all_fps):
            return None
        chosen_fps = []
        while len(chosen_fps) < number:
152
            choice = self.rng.choice(all_fps)
153
154
155
156
            if choice in black_fps:
                continue
            chosen_fps.append(choice)
            black_fps.append(choice)
157
        return [Relay(fp, self.controller) for fp in chosen_fps]
158

159
160
161
162
    def build_circuit(self, path):
        ''' <path> is a list of relays and Falsey values. Relays can be
        specified by fingerprint or nickname, and fingerprint is highly
        recommended. Falsey values (like None) will be replaced with relays
163
164
        chosen uniformally at random. A relay will not be in a circuit twice.
        '''
165
        if not valid_circuit_length(path):
166
            raise PathLengthException()
167
        path = self._normalize_path(path)
Matt Traudt's avatar
Matt Traudt committed
168
169
        if path is None:
            return None
170
        num_missing = len(['foo' for r in path if not r])
171
172
173
        insert_relays = self._random_sample_relays(
            num_missing, [r for r in path if r is not None])
        if insert_relays is None:
174
            path = ','.join([r.nickname if r else str(None) for r in path])
175
176
177
            log.warning(
                'Problem building a circuit to satisfy %s with available '
                'relays in the network', path)
178
179
            return None
        assert len(insert_relays) == num_missing
180
181
        path = [r.fingerprint if r else insert_relays.pop().fingerprint
                for r in path]
182
        return self._build_circuit_impl(path)