2 # -*- coding: utf-8 -*-
4 # This file is part of BridgeDB, a Tor bridge distribution system.
6 # :authors: Isis Lovecruft 0xA3ADB67A2CDB8B35 <isis@torproject.org>
7 # please also see AUTHORS file
8 # :copyright: (c) 2013 Isis Lovecruft
9 # (c) 2007-2013, The Tor Project, Inc.
10 # (c) 2007-2013, all entities within the AUTHORS file
11 # :license: 3-clause BSD, see included LICENSE for information
13 """get-tor-exits -- Download the current list of Tor exit relays."""
15 from __future__ import print_function
22 from ipaddress import IPv4Address
24 from OpenSSL import SSL
26 from twisted.python import log
27 from twisted.python import usage
28 from twisted.names import client as dnsclient
29 from twisted.names import error as dnserror
30 from twisted.web import client
31 from twisted.internet import defer
32 from twisted.internet import protocol
33 from twisted.internet import reactor
34 from twisted.internet import ssl
35 from twisted.internet.error import ConnectionRefusedError
36 from twisted.internet.error import DNSLookupError
37 from twisted.internet.error import TimeoutError
40 log.startLogging(sys.stderr, setStdout=False)
43 def backupFile(filename):
44 """Move our old exit list file so that we don't append to it."""
45 if os.path.isfile(filename):
46 backup = filename + '.bak'
47 log.msg("get-tor-exits: Moving old exit list file to %s"
49 os.renames(filename, backup)
51 def getSelfIPAddress():
52 """Get external IP address, to ask check which relays can exit to here.
54 TODO: This is blocking. Make it use Twisted.
58 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
59 s.connect(('bridges.torproject.org', 443))
60 name = s.getsockname()[0]
61 ip = IPv4Address(name)
62 if ip.is_link_local or ip.is_private or ip.is_reserved:
63 name = s.getpeername()[0]
64 ip = IPv4Address(name)
65 except ValueError as error:
66 log.err("get-tor-exits: A socket gave us something that wasn't an IP: %s"
68 except Exception as error:
69 log.err("get-tor-exits: Unhandled Exception: %s\n%s\n"
70 % (error.message, error))
77 """Handle a **failure**."""
78 if failure.type == ConnectionRefusedError:
79 log.msg("get-tor-exits: Could not download exitlist; connection was refused.")
80 elif failure.type == DNSLookupError:
81 log.msg("get-tor-exits: Could not download exitlist; domain resolution failed.")
82 elif failure.type == TimeoutError:
83 log.msg("get-tor-exits: Could not download exitlist; connection timed out.")
85 failure.trap(ConnectionRefusedError, DNSLookupError, TimeoutError)
87 def writeToFile(response, filename):
88 log.msg("get-tor-exits: Downloading list of Tor exit relays.")
89 finished = defer.Deferred()
90 response.deliverBody(FileWriter(finished, filename))
94 class GetTorExitsOptions(usage.Options):
95 """Options for this script"""
96 optFlags = [['stdout', 's', "Write results to stdout instead of file"]]
97 optParameters = [['file', 'f', 'exit-list', "File to write results to"],
98 ['address', 'a', '1.1.1.1', "Only exits which can reach this address"],
99 ['port', 'p', 443, "Only exits which can reach this port"]]
102 class FileWriter(protocol.Protocol):
103 """Read a downloaded file incrementally and write to file."""
104 def __init__(self, finished, file):
105 """Create a FileWriter.
107 .. warning:: We currently only handle the first 2MB of a file. Files
108 over 2MB will be truncated prematurely.
110 :param finished: A :class:`~twisted.internet.defer.Deferred` which
111 will fire when another portion of the download is complete.
113 self.finished = finished
114 self.remaining = 1024 * 1024 * 2
117 def dataReceived(self, bytes):
118 """Write a portion of the download with ``bytes`` size to disk."""
120 display = bytes[:self.remaining]
121 self.fh.write(display.decode("utf-8"))
123 self.remaining -= len(display)
125 def connectionLost(self, reason):
126 """Called when the download is complete."""
127 log.msg('get-tor-exits: Finished receiving exit list: %s'
128 % reason.getErrorMessage())
129 self.finished.callback(None)
132 def main(filename=None, address=None, port=None):
136 if (not isinstance(filename, io.TextIOBase)) and (filename is not sys.stdout):
137 fh = open(filename, 'w')
140 address = getSelfIPAddress()
142 check = "https://check.torproject.org/torbulkexitlist"
145 params.append('ip=%s' % address)
147 params.append("port=%s" % port)
149 check += '?' + '&'.join(params)
151 log.msg("get-tor-exits: Requesting %s..." % check)
153 agent = client.Agent(reactor)
154 d = agent.request(b"GET", check.encode("utf-8"))
155 d.addCallback(writeToFile, fh)
157 d.addCallbacks(log.msg, log.err)
159 if not reactor.running:
160 d.addCallback(lambda ignored: reactor.stop())
164 if (not isinstance(filename, io.TextIOBase)) and (filename is not sys.stdout):
169 if __name__ == "__main__":
171 options = GetTorExitsOptions()
172 options.parseOptions()
173 except usage.UsageError as error:
175 raise SystemExit(options.getUsage())
177 if options['stdout']:
178 filename = sys.stdout
179 elif options['file']:
180 filename = options['file']
181 log.msg("get-tor-exits: Saving Tor exit relay list to file: '%s'"
185 main(filename, options['address'], options['port'])