Make filename lines in proposals match actual filenames. Accept 135.
[tor.git] / contrib / exitlist
blobba682b5c0638c0ac960c6a8e20bb8f8d0f6808c0
1 #!/usr/bin/python
2 # Copyright 2005-2006 Nick Mathewson
3 # See the LICENSE file in the Tor distribution for licensing information.
5 # Requires Python 2.2 or later.
7 """
8 exitlist -- Given a Tor directory on stdin, lists the Tor servers
9 that accept connections to given addreses.
11 example usage (Tor 0.2.0.7-alpha and earlier):
13 cat ~/.tor/cached-routers* | python exitlist 18.244.0.188:80
15 example usage (Tor 0.2.0.8-alpha and later):
17 cat ~/.tor/cached-descriptors* | python exitlist 18.244.0.188:80
19 You should look at the "FetchUselessDescriptors" config option in the
20 man page. For 0.2.0.13-alpha and later, also look at the
21 "FetchDirInfoEarly" config option.
23 Note that this script won't give you a perfect list of IP addresses
24 that might connect to you using Tor, since some Tor servers might exit
25 from other addresses than the one they publish. See
26 https://check.torproject.org/ for an alternative (more
27 accurate!) approach.
29 """
32 # Change this to True if you want more verbose output. By default, we
33 # only print the IPs of the servers that accept any the listed
34 # addresses, one per line.
36 VERBOSE = False
39 # Change this to True if you want to reverse the output, and list the
40 # servers that accept *none* of the listed addresses.
42 INVERSE = False
45 # Change this list to contain all of the target services you are interested
46 # in. It must contain one entry per line, each consisting of an IPv4 address,
47 # a colon, and a port number. This default is only used if we don't learn
48 # about any addresses from the command-line.
50 ADDRESSES_OF_INTEREST = """
51 1.2.3.4:80
52 """
56 # YOU DO NOT NEED TO EDIT AFTER THIS POINT.
59 import sys
60 import re
61 import getopt
62 import socket
63 import struct
64 import time
66 assert sys.version_info >= (2,2)
69 def maskIP(ip,mask):
70 return "".join([chr(ord(a) & ord(b)) for a,b in zip(ip,mask)])
72 def maskFromLong(lng):
73 return struct.pack("!L", lng)
75 def maskByBits(n):
76 return maskFromLong(0xffffffffl ^ ((1L<<(32-n))-1))
78 class Pattern:
79 """
80 >>> import socket
81 >>> ip1 = socket.inet_aton("192.169.64.11")
82 >>> ip2 = socket.inet_aton("192.168.64.11")
83 >>> ip3 = socket.inet_aton("18.244.0.188")
85 >>> print Pattern.parse("18.244.0.188")
86 18.244.0.188/255.255.255.255:1-65535
87 >>> print Pattern.parse("18.244.0.188/16:*")
88 18.244.0.0/255.255.0.0:1-65535
89 >>> print Pattern.parse("18.244.0.188/2.2.2.2:80")
90 2.0.0.0/2.2.2.2:80-80
91 >>> print Pattern.parse("192.168.0.1/255.255.00.0:22-25")
92 192.168.0.0/255.255.0.0:22-25
93 >>> p1 = Pattern.parse("192.168.0.1/255.255.00.0:22-25")
94 >>> import socket
95 >>> p1.appliesTo(ip1, 22)
96 False
97 >>> p1.appliesTo(ip2, 22)
98 True
99 >>> p1.appliesTo(ip2, 25)
100 True
101 >>> p1.appliesTo(ip2, 26)
102 False
104 def __init__(self, ip, mask, portMin, portMax):
105 self.ip = maskIP(ip,mask)
106 self.mask = mask
107 self.portMin = portMin
108 self.portMax = portMax
110 def __str__(self):
111 return "%s/%s:%s-%s"%(socket.inet_ntoa(self.ip),
112 socket.inet_ntoa(self.mask),
113 self.portMin,
114 self.portMax)
116 def parse(s):
117 if ":" in s:
118 addrspec, portspec = s.split(":",1)
119 else:
120 addrspec, portspec = s, "*"
122 if addrspec == '*':
123 ip,mask = "\x00\x00\x00\x00","\x00\x00\x00\x00"
124 elif '/' not in addrspec:
125 ip = socket.inet_aton(addrspec)
126 mask = "\xff\xff\xff\xff"
127 else:
128 ip,mask = addrspec.split("/",1)
129 ip = socket.inet_aton(ip)
130 if "." in mask:
131 mask = socket.inet_aton(mask)
132 else:
133 mask = maskByBits(int(mask))
135 if portspec == '*':
136 portMin = 1
137 portMax = 65535
138 elif '-' not in portspec:
139 portMin = portMax = int(portspec)
140 else:
141 portMin, portMax = map(int,portspec.split("-",1))
143 return Pattern(ip,mask,portMin,portMax)
145 parse = staticmethod(parse)
147 def appliesTo(self, ip, port):
148 return ((maskIP(ip,self.mask) == self.ip) and
149 (self.portMin <= port <= self.portMax))
151 class Policy:
153 >>> import socket
154 >>> ip1 = socket.inet_aton("192.169.64.11")
155 >>> ip2 = socket.inet_aton("192.168.64.11")
156 >>> ip3 = socket.inet_aton("18.244.0.188")
158 >>> pol = Policy.parseLines(["reject *:80","accept 18.244.0.188:*"])
159 >>> print str(pol).strip()
160 reject 0.0.0.0/0.0.0.0:80-80
161 accept 18.244.0.188/255.255.255.255:1-65535
162 >>> pol.accepts(ip1,80)
163 False
164 >>> pol.accepts(ip3,80)
165 False
166 >>> pol.accepts(ip3,81)
167 True
170 def __init__(self, lst):
171 self.lst = lst
173 def parseLines(lines):
174 r = []
175 for item in lines:
176 a,p=item.split(" ",1)
177 if a == 'accept':
178 a = True
179 elif a == 'reject':
180 a = False
181 else:
182 raise ValueError("Unrecognized action %r",a)
183 p = Pattern.parse(p)
184 r.append((p,a))
185 return Policy(r)
187 parseLines = staticmethod(parseLines)
189 def __str__(self):
190 r = []
191 for pat, accept in self.lst:
192 rule = accept and "accept" or "reject"
193 r.append("%s %s\n"%(rule,pat))
194 return "".join(r)
196 def accepts(self, ip, port):
197 for pattern,accept in self.lst:
198 if pattern.appliesTo(ip,port):
199 return accept
200 return True
202 class Server:
203 def __init__(self, name, ip, policy, published, fingerprint):
204 self.name = name
205 self.ip = ip
206 self.policy = policy
207 self.published = published
208 self.fingerprint = fingerprint
210 def uniq_sort(lst):
211 d = {}
212 for item in lst: d[item] = 1
213 lst = d.keys()
214 lst.sort()
215 return lst
217 def run():
218 global VERBOSE
219 global INVERSE
220 global ADDRESSES_OF_INTEREST
222 if len(sys.argv) > 1:
223 try:
224 opts, pargs = getopt.getopt(sys.argv[1:], "vx")
225 except getopt.GetoptError, e:
226 print """
227 usage: cat ~/.tor/cached-routers* | %s [-v] [-x] [host:port [host:port [...]]]
228 -v verbose output
229 -x invert results
230 """ % sys.argv[0]
231 sys.exit(0)
233 for o, a in opts:
234 if o == "-v":
235 VERBOSE = True
236 if o == "-x":
237 INVERSE = True
238 if len(pargs):
239 ADDRESSES_OF_INTEREST = "\n".join(pargs)
241 servers = []
242 policy = []
243 name = ip = None
244 published = 0
245 fp = ""
246 for line in sys.stdin.xreadlines():
247 if line.startswith('router '):
248 if name:
249 servers.append(Server(name, ip, Policy.parseLines(policy),
250 published, fp))
251 _, name, ip, rest = line.split(" ", 3)
252 policy = []
253 published = 0
254 fp = ""
255 elif line.startswith('fingerprint') or \
256 line.startswith('opt fingerprint'):
257 elts = line.strip().split()
258 if elts[0] == 'opt': del elts[0]
259 assert elts[0] == 'fingerprint'
260 del elts[0]
261 fp = "".join(elts)
262 elif line.startswith('accept ') or line.startswith('reject '):
263 policy.append(line.strip())
264 elif line.startswith('published '):
265 date = time.strptime(line[len('published '):].strip(),
266 "%Y-%m-%d %H:%M:%S")
267 published = time.mktime(date)
269 if name:
270 servers.append(Server(name, ip, Policy.parseLines(policy), published,
271 fp))
273 targets = []
274 for line in ADDRESSES_OF_INTEREST.split("\n"):
275 line = line.strip()
276 if not line: continue
277 p = Pattern.parse(line)
278 targets.append((p.ip, p.portMin))
280 # remove all but the latest server of each IP/Nickname pair.
281 latest = {}
282 for s in servers:
283 if (not latest.has_key((s.fingerprint))
284 or s.published > latest[(s.fingerprint)]):
285 latest[s.fingerprint] = s
286 servers = latest.values()
288 accepters, rejecters = {}, {}
289 for s in servers:
290 for ip,port in targets:
291 if s.policy.accepts(ip,port):
292 accepters[s.ip] = s
293 break
294 else:
295 rejecters[s.ip] = s
297 # If any server at IP foo accepts, the IP does not reject.
298 for k in accepters.keys():
299 if rejecters.has_key(k):
300 del rejecters[k]
302 if INVERSE:
303 printlist = rejecters.values()
304 else:
305 printlist = accepters.values()
307 ents = []
308 if VERBOSE:
309 ents = uniq_sort([ "%s\t%s"%(s.ip,s.name) for s in printlist ])
310 else:
311 ents = uniq_sort([ s.ip for s in printlist ])
312 for e in ents:
313 print e
315 def _test():
316 import doctest, exitparse
317 return doctest.testmod(exitparse)
318 #_test()
320 run()