Parse bridge blocking info from SQL database.
[tor-bridgedb.git] / bridgedb / filters.py
blob3ea072347173af36aab9d272c34e21f27bd07fe1
1 # -*- coding: utf-8 ; test-case-name: bridgedb.test.test_filters ; -*-
2 #_____________________________________________________________________________
4 # This file is part of BridgeDB, a Tor bridge distribution system.
6 # :authors: Nick Mathewson <nickm@torproject.org>
7 # Isis Lovecruft 0xA3ADB67A2CDB8B35 <isis@torproject.org>
8 # please also see AUTHORS file
9 # :copyright: (c) 2007-2017, The Tor Project, Inc.
10 # (c) 2013-2017, Isis Lovecruft
11 # :license: see LICENSE for licensing information
12 #_____________________________________________________________________________
14 """Functions for filtering :class:`Bridges <bridgedb.bridges.Bridge>`."""
16 import binascii
17 import logging
19 from ipaddr import IPv4Address
20 from ipaddr import IPv6Address
22 from bridgedb.parse.addr import isIPv
25 _cache = {}
28 def bySubring(hmac, assigned, total):
29 """Create a filter function which filters for only the bridges which fall
30 into the same **assigned** subhashring (based on the results of an **hmac**
31 function).
33 :type hmac: callable
34 :param hmac: An HMAC function, i.e. as returned from
35 :func:`bridgedb.crypto.getHMACFunc`.
36 :param int assigned: The subring number that we wish to draw bridges from.
37 For example, if a user is assigned to subring 2of3 based on their IP
38 address, then this function should only return bridges which would
39 also be assigned to subring 2of3.
40 :param int total: The total number of subrings.
41 :rtype: callable
42 :returns: A filter function for :class:`Bridges <bridgedb.bridges.Bridge>`.
43 """
44 logging.debug(("Creating a filter for assigning bridges to subhashring "
45 "%s-of-%s...") % (assigned, total))
47 name = "-".join([binascii.hexlify(str(hmac("")[:8]).encode('utf-8')).decode('utf-8'),
48 str(assigned), "of", str(total)])
49 try:
50 return _cache[name]
51 except KeyError:
52 def _bySubring(bridge):
53 position = int(hmac(bridge.identity)[:8], 16)
54 which = (position % total) + 1
55 return True if which == assigned else False
56 # The `description` attribute must contain an `=`, or else
57 # dumpAssignments() will not work correctly.
58 setattr(_bySubring, "description", "ring=%d" % assigned)
59 _bySubring.__name__ = ("bySubring%sof%s" % (assigned, total))
60 _bySubring.name = name
61 _cache[name] = _bySubring
62 return _bySubring
64 def byFilters(filtres):
65 """Returns a filter which filters by multiple **filtres**.
67 :param list filtres: A list (or other iterable) of callables which some
68 :class:`Bridges <bridgedb.bridges.Bridge>` should be filtered
69 according to.
70 :rtype: callable
71 :returns: A filter function for :class:`Bridges <bridgedb.bridges.Bridge>`.
72 """
73 name = []
74 for filtre in filtres:
75 name.extend(filtre.name.split(" "))
76 name = " ".join(set(name))
78 try:
79 return _cache[name]
80 except KeyError:
81 def _byFilters(bridge):
82 results = [f(bridge) for f in filtres]
83 if False in results:
84 return False
85 return True
86 setattr(_byFilters, "description",
87 " ".join([getattr(f, "description", "") for f in filtres]))
88 _byFilters.name = name
89 _cache[name] = _byFilters
90 return _byFilters
92 def byIPv(ipVersion=None):
93 """Return ``True`` if at least one of the **bridge**'s addresses has the
94 specified **ipVersion**.
96 :param int ipVersion: Either ``4`` or ``6``.
97 """
98 if not ipVersion in (4, 6):
99 ipVersion = 4
101 name = "ipv%d" % ipVersion
102 try:
103 return _cache[name]
104 except KeyError:
105 def _byIPv(bridge):
106 """Determine if the **bridge** has an IPv{0} address.
108 :type bridge: :class:`bridgedb.bridges.Bridge`
109 :param bridge: A bridge to filter.
110 :rtype: bool
111 :returns: ``True`` if the **bridge** has an address with the
112 correct IP version; ``False`` otherwise.
114 if isIPv(ipVersion, bridge.address):
115 return True
116 else:
117 for address, port, version in bridge.allVanillaAddresses:
118 if version == ipVersion or isIPv(ipVersion, address):
119 return True
120 return False
121 setattr(_byIPv, "description", "ip=%d" % ipVersion)
122 _byIPv.__name__ = "byIPv%d()" % ipVersion
123 _byIPv.__doc__ = _byIPv.__doc__.format(ipVersion)
124 _byIPv.name = name
125 _cache[name] = _byIPv
126 return _byIPv
128 byIPv4 = byIPv(4)
129 byIPv6 = byIPv(6)
131 def byProbingResistance(methodname=None, ipVersion=None):
132 """Return ``True`` if the bridge can be given out safely without
133 jeopardizing a probing-resistant transport that runs on the same bridge.
135 :param str methodname: A Pluggable Transport
136 :data:`~bridgedb.bridges.PluggableTransport.methodname`.
137 :param int ipVersion: Either ``4`` or ``6``. The IP version that the
138 ``Bridge``'s ``PluggableTransport``
139 :attr:`address <bridgedb.bridges.PluggableTransport.address>` should
140 have.
141 :rtype: callable
142 :returns: A filter function for :class:`Bridges <bridgedb.bridges.Bridge>`.
145 if ipVersion not in (4, 6):
146 ipVersion = 4
148 methodname = "vanilla" if methodname is None else methodname.lower()
149 name = "by-probing-resistance-%s ipv%d" % (methodname, ipVersion)
151 try:
152 return _cache[name]
153 except KeyError:
154 def _byProbingResistance(bridge):
155 # If we're dealing with a vanilla bridge, make sure that the bridge
156 # has the correct IP version.
157 if methodname == "vanilla":
158 validVersion = byIPv(ipVersion)
159 if not validVersion(bridge):
160 return False
162 if bridge.hasProbingResistantPT():
163 return methodname in ('scramblesuit', 'obfs4')
164 return True
166 setattr(_byProbingResistance, "description", "probing_resistance")
167 _byProbingResistance.__name__ = "byProbingResistance(%s,%s)" % (methodname, ipVersion)
168 _byProbingResistance.name = name
169 _cache[name] = _byProbingResistance
170 return _byProbingResistance
172 def byTransport(methodname=None, ipVersion=None):
173 """Returns a filter function for a :class:`~bridgedb.bridges.Bridge`.
175 The returned filter function should be called on a
176 :class:`~bridgedb.bridges.Bridge`. It returns ``True`` if the
177 :class:`~bridgedb.bridges.Bridge` has a
178 :class:`~bridgedb.bridges.PluggableTransport` such that:
180 1. The :data:`methodname <bridgedb.bridges.PluggableTransport.methodname>`
181 matches **methodname**, and,
183 2. The :attr:`bridgedb.bridges.PluggableTransport.address.version`
184 equals the **ipVersion**.
186 :param str methodname: A Pluggable Transport
187 :data:`~bridgedb.bridges.PluggableTransport.methodname`.
188 :param int ipVersion: Either ``4`` or ``6``. The IP version that the
189 ``Bridge``'s ``PluggableTransport``
190 :attr:`address <bridgedb.bridges.PluggableTransport.address>` should
191 have.
192 :rtype: callable
193 :returns: A filter function for :class:`Bridges <bridgedb.bridges.Bridge>`.
195 if not ipVersion in (4, 6):
196 ipVersion = 4
197 if not methodname:
198 return byIPv(ipVersion)
200 methodname = methodname.lower()
201 name = "transport-%s ipv%d" % (methodname, ipVersion)
203 try:
204 return _cache[name]
205 except KeyError:
206 def _byTransport(bridge):
207 for transport in bridge.transports:
208 if transport.methodname == methodname:
209 if transport.address.version == ipVersion:
210 return True
211 return False
212 setattr(_byTransport, "description", "transport=%s" % methodname)
213 _byTransport.__name__ = "byTransport(%s,%s)" % (methodname, ipVersion)
214 _byTransport.name = name
215 _cache[name] = _byTransport
216 return _byTransport
218 def byNotBlockedIn(countryCode=None, methodname=None, ipVersion=4):
219 """Returns a filter function for :class:`Bridges <bridgedb.bridges.Bridge>`.
221 If a Pluggable Transport **methodname** was not specified, the returned
222 filter function returns ``True`` if any of the ``Bridge``'s addresses or
223 :class:`~bridgedb.bridges.PluggableTransport` addresses aren't blocked in
224 **countryCode**. See :meth:`~bridgedb.bridges.Bridge.isBlockedIn`.
226 Otherwise, if a Pluggable Transport **methodname** was specified, it
227 returns ``True`` if the :class:`~bridgedb.bridges.Bridge` has a
228 :class:`~bridgedb.bridges.PluggableTransport` such that:
230 1. The :data:`methodname <bridgedb.bridges.PluggableTransport.methodname>`
231 matches **methodname**,
233 2. The :attr:`bridgedb.bridges.PluggableTransport.address.version`
234 equals the **ipVersion**, and,
236 3. The :class:`~bridgedb.bridges.PluggableTransport`.
237 :attr:`address <bridgedb.bridges.PluggableTransport.address>` isn't
238 known to be blocked in **countryCode**.
240 :type countryCode: str or ``None``
241 :param countryCode: A two-letter country code which the filtered
242 :class:`PluggableTransports <bridgedb.bridges.PluggableTransport>`
243 should not be blocked in.
244 :param str methodname: A Pluggable Transport
245 :data:`methodname <bridgedb.bridges.PluggableTransport.methodname>`.
246 :param int ipVersion: Either ``4`` or ``6``. The IP version that the
247 ``PluggableTransports``'s addresses should have.
248 :rtype: callable
249 :returns: A filter function for :class:`Bridges <bridgedb.bridges.Bridge>`.
251 if not ipVersion in (4, 6):
252 ipVersion = 4
253 if not countryCode:
254 return byTransport(methodname, ipVersion)
256 methodname = methodname.lower() if methodname else methodname
257 countryCode = countryCode.lower()
259 name = []
260 if methodname:
261 name.append("transport-%s" % methodname)
262 name.append("ipv%d" % ipVersion)
263 name.append("not-blocked-in-%s" % countryCode)
264 name = " ".join(name)
266 try:
267 return _cache[name]
268 except KeyError:
269 def _byNotBlockedIn(bridge):
270 if not methodname:
271 return not bridge.isBlockedIn(countryCode)
272 elif methodname == "vanilla":
273 validVersion = byIPv(ipVersion)
274 if validVersion(bridge):
275 if not bridge.addressIsBlockedIn(countryCode,
276 bridge.address,
277 bridge.orPort):
278 return True
279 else:
280 # Since bridge.transportIsBlockedIn() will return True if the
281 # bridge has that type of transport AND that transport is
282 # blocked, we can "fail fast" here by doing this faster check
283 # before iterating over all the transports testing for the
284 # other conditions.
285 if bridge.transportIsBlockedIn(countryCode, methodname):
286 return False
287 else:
288 for transport in bridge.transports:
289 if transport.methodname == methodname:
290 if transport.address.version == ipVersion:
291 return True
292 return False
293 setattr(_byNotBlockedIn, "description", "unblocked=%s" % countryCode)
294 _byNotBlockedIn.__name__ = ("byTransportNotBlockedIn(%s,%s,%s)"
295 % (methodname, countryCode, ipVersion))
296 _byNotBlockedIn.name = name
297 _cache[name] = _byNotBlockedIn
298 return _byNotBlockedIn