Bump version number to 0.10.1.
[tor-bridgedb.git] / scripts / get-tor-exits
blobc250dbe0e0747d4d0a91bef789a7764da252e854
1 #!/usr/bin/env python3
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
17 import os.path
18 import socket
19 import sys
20 import io
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"
48                       % backup)
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.
55     """
56     ip = s = None
57     try:
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"
67                 % error)
68     except Exception as error:
69         log.err("get-tor-exits: Unhandled Exception: %s\n%s\n"
70                 % (error.message, error))
71     finally:
72         if s is not None:
73             s.close()
74     return ip.compressed
76 def handle(failure):
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))
91     return finished
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.
112         """
113         self.finished = finished
114         self.remaining = 1024 * 1024 * 2
115         self.fh = file
117     def dataReceived(self, bytes):
118         """Write a portion of the download with ``bytes`` size to disk."""
119         if self.remaining:
120             display = bytes[:self.remaining]
121             self.fh.write(display.decode("utf-8"))
122             self.fh.flush()
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):
134     fh = filename
135     if filename:
136         if (not isinstance(filename, io.TextIOBase)) and (filename is not sys.stdout):
137             fh = open(filename, 'w')
139     if not address:
140         address = getSelfIPAddress()
142     check  = "https://check.torproject.org/torbulkexitlist"
143     params = []
145     params.append('ip=%s' % address)
146     if port is not None:
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)
156     d.addErrback(handle)
157     d.addCallbacks(log.msg, log.err)
159     if not reactor.running:
160         d.addCallback(lambda ignored: reactor.stop())
161         reactor.run()
163     if filename:
164         if (not isinstance(filename, io.TextIOBase)) and (filename is not sys.stdout):
165             fh.flush()
166             fh.close()
169 if __name__ == "__main__":
170     try:
171         options = GetTorExitsOptions()
172         options.parseOptions()
173     except usage.UsageError as error:
174         log.err(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'"
182                 % filename)
183         backupFile(filename)
185     main(filename, options['address'], options['port'])