free a small static string on exit.
[tor.git] / contrib / exitlist
blobaabf739f623e7b4622862b9f9bf0fcd2b95da54e
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.1.0.x and earlier):
13 python exitlist 18.244.0.188:80 < ~/.tor/cached-directory
15 example usage (Tor 0.1.1.10-alpha and later):
17 cat ~/.tor/cached-routers* | python exitlist 18.244.0.188:80
19 If you're using Tor 0.1.1.18-rc or later, you should look at
20 the "FetchUselessDescriptors" config option in the man page.
22 Note that this script won't give you a perfect list of IP addresses
23 that might connect to you using Tor, since some Tor servers might exit
24 from other addresses than the one they publish.
26 """
29 # Change this to True if you want more verbose output. By default, we
30 # only print the IPs of the servers that accept any the listed
31 # addresses, one per line.
33 VERBOSE = False
36 # Change this to True if you want to reverse the output, and list the
37 # servers that accept *none* of the listed addresses.
39 INVERSE = False
42 # Change this list to contain all of the target services you are interested
43 # in. It must contain one entry per line, each consisting of an IPv4 address,
44 # a colon, and a port number. This default is only used if we don't learn
45 # about any addresses from the command-line.
47 ADDRESSES_OF_INTEREST = """
48 1.2.3.4:80
49 """
53 # YOU DO NOT NEED TO EDIT AFTER THIS POINT.
56 import sys
57 import re
58 import getopt
59 import socket
60 import struct
61 import time
63 assert sys.version_info >= (2,2)
66 def maskIP(ip,mask):
67 return "".join([chr(ord(a) & ord(b)) for a,b in zip(ip,mask)])
69 def maskFromLong(lng):
70 return struct.pack("!L", lng)
72 def maskByBits(n):
73 return maskFromLong(0xffffffffl ^ ((1L<<(32-n))-1))
75 class Pattern:
76 """
77 >>> import socket
78 >>> ip1 = socket.inet_aton("192.169.64.11")
79 >>> ip2 = socket.inet_aton("192.168.64.11")
80 >>> ip3 = socket.inet_aton("18.244.0.188")
82 >>> print Pattern.parse("18.244.0.188")
83 18.244.0.188/255.255.255.255:1-65535
84 >>> print Pattern.parse("18.244.0.188/16:*")
85 18.244.0.0/255.255.0.0:1-65535
86 >>> print Pattern.parse("18.244.0.188/2.2.2.2:80")
87 2.0.0.0/2.2.2.2:80-80
88 >>> print Pattern.parse("192.168.0.1/255.255.00.0:22-25")
89 192.168.0.0/255.255.0.0:22-25
90 >>> p1 = Pattern.parse("192.168.0.1/255.255.00.0:22-25")
91 >>> import socket
92 >>> p1.appliesTo(ip1, 22)
93 False
94 >>> p1.appliesTo(ip2, 22)
95 True
96 >>> p1.appliesTo(ip2, 25)
97 True
98 >>> p1.appliesTo(ip2, 26)
99 False
101 def __init__(self, ip, mask, portMin, portMax):
102 self.ip = maskIP(ip,mask)
103 self.mask = mask
104 self.portMin = portMin
105 self.portMax = portMax
107 def __str__(self):
108 return "%s/%s:%s-%s"%(socket.inet_ntoa(self.ip),
109 socket.inet_ntoa(self.mask),
110 self.portMin,
111 self.portMax)
113 def parse(s):
114 if ":" in s:
115 addrspec, portspec = s.split(":",1)
116 else:
117 addrspec, portspec = s, "*"
119 if addrspec == '*':
120 ip,mask = "\x00\x00\x00\x00","\x00\x00\x00\x00"
121 elif '/' not in addrspec:
122 ip = socket.inet_aton(addrspec)
123 mask = "\xff\xff\xff\xff"
124 else:
125 ip,mask = addrspec.split("/",1)
126 ip = socket.inet_aton(ip)
127 if "." in mask:
128 mask = socket.inet_aton(mask)
129 else:
130 mask = maskByBits(int(mask))
132 if portspec == '*':
133 portMin = 1
134 portMax = 65535
135 elif '-' not in portspec:
136 portMin = portMax = int(portspec)
137 else:
138 portMin, portMax = map(int,portspec.split("-",1))
140 return Pattern(ip,mask,portMin,portMax)
142 parse = staticmethod(parse)
144 def appliesTo(self, ip, port):
145 return ((maskIP(ip,self.mask) == self.ip) and
146 (self.portMin <= port <= self.portMax))
148 class Policy:
150 >>> import socket
151 >>> ip1 = socket.inet_aton("192.169.64.11")
152 >>> ip2 = socket.inet_aton("192.168.64.11")
153 >>> ip3 = socket.inet_aton("18.244.0.188")
155 >>> pol = Policy.parseLines(["reject *:80","accept 18.244.0.188:*"])
156 >>> print str(pol).strip()
157 reject 0.0.0.0/0.0.0.0:80-80
158 accept 18.244.0.188/255.255.255.255:1-65535
159 >>> pol.accepts(ip1,80)
160 False
161 >>> pol.accepts(ip3,80)
162 False
163 >>> pol.accepts(ip3,81)
164 True
167 def __init__(self, lst):
168 self.lst = lst
170 def parseLines(lines):
171 r = []
172 for item in lines:
173 a,p=item.split(" ",1)
174 if a == 'accept':
175 a = True
176 elif a == 'reject':
177 a = False
178 else:
179 raise ValueError("Unrecognized action %r",a)
180 p = Pattern.parse(p)
181 r.append((p,a))
182 return Policy(r)
184 parseLines = staticmethod(parseLines)
186 def __str__(self):
187 r = []
188 for pat, accept in self.lst:
189 rule = accept and "accept" or "reject"
190 r.append("%s %s\n"%(rule,pat))
191 return "".join(r)
193 def accepts(self, ip, port):
194 for pattern,accept in self.lst:
195 if pattern.appliesTo(ip,port):
196 return accept
197 return True
199 class Server:
200 def __init__(self, name, ip, policy, published, fingerprint):
201 self.name = name
202 self.ip = ip
203 self.policy = policy
204 self.published = published
205 self.fingerprint = fingerprint
207 def uniq_sort(lst):
208 d = {}
209 for item in lst: d[item] = 1
210 lst = d.keys()
211 lst.sort()
212 return lst
214 def run():
215 global VERBOSE
216 global INVERSE
217 global ADDRESSES_OF_INTEREST
219 if len(sys.argv) > 1:
220 try:
221 opts, pargs = getopt.getopt(sys.argv[1:], "vx")
222 except getopt.GetoptError, e:
223 print """
224 usage: cat ~/.tor/cached-routers* | %s [-v] [-x] [host:port [host:port [...]]]
225 -v verbose output
226 -x invert results
227 """ % sys.argv[0]
228 sys.exit(0)
230 for o, a in opts:
231 if o == "-v":
232 VERBOSE = True
233 if o == "-x":
234 INVERSE = True
235 if len(pargs):
236 ADDRESSES_OF_INTEREST = "\n".join(pargs)
238 servers = []
239 policy = []
240 name = ip = None
241 published = 0
242 fp = ""
243 for line in sys.stdin.xreadlines():
244 if line.startswith('router '):
245 if name:
246 servers.append(Server(name, ip, Policy.parseLines(policy),
247 published, fp))
248 _, name, ip, rest = line.split(" ", 3)
249 policy = []
250 published = 0
251 fp = ""
252 elif line.startswith('fingerprint') or \
253 line.startswith('opt fingerprint'):
254 elts = line.strip().split()
255 if elts[0] == 'opt': del elts[0]
256 assert elts[0] == 'fingerprint'
257 del elts[0]
258 fp = "".join(elts)
259 elif line.startswith('accept ') or line.startswith('reject '):
260 policy.append(line.strip())
261 elif line.startswith('published '):
262 date = time.strptime(line[len('published '):].strip(),
263 "%Y-%m-%d %H:%M:%S")
264 published = time.mktime(date)
266 if name:
267 servers.append(Server(name, ip, Policy.parseLines(policy), published,
268 fp))
270 targets = []
271 for line in ADDRESSES_OF_INTEREST.split("\n"):
272 line = line.strip()
273 if not line: continue
274 p = Pattern.parse(line)
275 targets.append((p.ip, p.portMin))
277 # remove all but the latest server of each IP/Nickname pair.
278 latest = {}
279 for s in servers:
280 if (not latest.has_key((s.fingerprint))
281 or s.published > latest[(s.fingerprint)]):
282 latest[s.fingerprint] = s
283 servers = latest.values()
285 accepters, rejecters = {}, {}
286 for s in servers:
287 for ip,port in targets:
288 if s.policy.accepts(ip,port):
289 accepters[s.ip] = s
290 break
291 else:
292 rejecters[s.ip] = s
294 # If any server at IP foo accepts, the IP does not reject.
295 for k in accepters.keys():
296 if rejecters.has_key(k):
297 del rejecters[k]
299 if INVERSE:
300 printlist = rejecters.values()
301 else:
302 printlist = accepters.values()
304 ents = []
305 if VERBOSE:
306 ents = uniq_sort([ "%s\t%s"%(s.ip,s.name) for s in printlist ])
307 else:
308 ents = uniq_sort([ s.ip for s in printlist ])
309 for e in ents:
310 print e
312 def _test():
313 import doctest, exitparse
314 return doctest.testmod(exitparse)
315 #_test()
317 run()