]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser_launcher/launcher.py
Add icons to settings buttons
[torbrowser-launcher.git] / torbrowser_launcher / launcher.py
1 """
2 Tor Browser Launcher
3 https://github.com/micahflee/torbrowser-launcher/
4
5 Copyright (c) 2013-2017 Micah Lee <micah@micahflee.com>
6
7 Permission is hereby granted, free of charge, to any person
8 obtaining a copy of this software and associated documentation
9 files (the "Software"), to deal in the Software without
10 restriction, including without limitation the rights to use,
11 copy, modify, merge, publish, distribute, sublicense, and/or sell
12 copies of the Software, and to permit persons to whom the
13 Software is furnished to do so, subject to the following
14 conditions:
15
16 The above copyright notice and this permission notice shall be
17 included in all copies or substantial portions of the Software.
18
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 OTHER DEALINGS IN THE SOFTWARE.
27 """
28
29 from __future__ import print_function
30
31 import os
32 import subprocess
33 import time
34 import json
35 import tarfile
36 import hashlib
37 import lzma
38 import threading
39 import re
40 import unicodedata
41
42 from twisted.internet import reactor
43 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
44 from twisted.web.http_headers import Headers
45 from twisted.internet.protocol import Protocol
46 from twisted.internet.error import DNSLookupError, ConnectionRefusedError
47
48 try:
49     import gpg
50     gpgme_support = True
51 except ImportError:
52     gpgme_support = False
53
54 import xml.etree.ElementTree as ET
55
56 import OpenSSL
57
58 import pygtk
59 pygtk.require('2.0')
60 import gtk
61
62
63 class TryStableException(Exception):
64     pass
65
66
67 class TryDefaultMirrorException(Exception):
68     pass
69
70
71 class TryForcingEnglishException(Exception):
72     pass
73
74
75 class DownloadErrorException(Exception):
76     pass
77
78
79 class Launcher:
80     def __init__(self, common, app, url_list):
81         self.common = common
82         self.app = app
83         
84         self.url_list = url_list
85         self.force_redownload = False
86
87         # this is the current version of Tor Browser, which should get updated with every release
88         self.min_version = '6.0.2'
89
90         # init launcher
91         self.set_gui(None, '', [])
92         self.launch_gui = True
93
94         # if Tor Browser is not installed, detect latest version, download, and install
95         if not self.common.settings['installed'] or not self.check_min_version():
96             # if downloading over Tor, include txsocksx
97             if self.common.settings['download_over_tor']:
98                 try:
99                     import txsocksx
100                     print(_('Downloading over Tor'))
101                 except ImportError:
102                     md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-txsocksx package is missing, downloads will not happen over tor"))
103                     md.set_position(gtk.WIN_POS_CENTER)
104                     md.run()
105                     md.destroy()
106                     self.common.settings['download_over_tor'] = False
107                     self.common.save_settings()
108
109             # different message if downloading for the first time, or because your installed version is too low
110             download_message = ""
111             if not self.common.settings['installed']:
112                 download_message = _("Downloading and installing Tor Browser for the first time.")
113             elif not self.check_min_version():
114                 download_message = _("Your version of Tor Browser is out-of-date. Downloading and installing the newest version.")
115
116             # download and install
117             print(download_message)
118             self.set_gui('task', download_message,
119                          ['download_version_check',
120                           'set_version',
121                           'download_sig',
122                           'download_tarball',
123                           'verify',
124                           'extract',
125                           'run'])
126
127         else:
128             # Tor Browser is already installed, so run
129             self.run(False)
130             self.launch_gui = False
131
132         if self.launch_gui:
133             # build the rest of the UI
134             self.build_ui()
135
136     def configure_window(self):
137         if not hasattr(self, 'window'):
138             self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
139             self.window.set_title(_("Tor Browser"))
140             self.window.set_icon_from_file(self.common.paths['icon_file'])
141             self.window.set_position(gtk.WIN_POS_CENTER)
142             self.window.set_border_width(10)
143             self.window.connect("delete_event", self.delete_event)
144             self.window.connect("destroy", self.destroy)
145
146     # there are different GUIs that might appear, this sets which one we want
147     def set_gui(self, gui, message, tasks, autostart=True):
148         self.gui = gui
149         self.gui_message = message
150         self.gui_tasks = tasks
151         self.gui_task_i = 0
152         self.gui_autostart = autostart
153
154     # set all gtk variables to False
155     def clear_ui(self):
156         if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
157             self.box.destroy()
158         self.box = False
159
160         self.label = False
161         self.progressbar = False
162         self.button_box = False
163         self.start_button = False
164         self.exit_button = False
165
166     # build the application's UI
167     def build_ui(self):
168         self.clear_ui()
169
170         self.box = gtk.VBox(False, 20)
171         self.configure_window()
172         self.window.add(self.box)
173
174         if 'error' in self.gui:
175             # labels
176             self.label = gtk.Label(self.gui_message)
177             self.label.set_line_wrap(True)
178             self.box.pack_start(self.label, True, True, 0)
179             self.label.show()
180
181             # button box
182             self.button_box = gtk.HButtonBox()
183             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
184             self.box.pack_start(self.button_box, True, True, 0)
185             self.button_box.show()
186
187             if self.gui != 'error':
188                 # yes button
189                 yes_image = gtk.Image()
190                 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
191                 self.yes_button = gtk.Button("Yes")
192                 self.yes_button.set_image(yes_image)
193                 if self.gui == 'error_try_stable':
194                     self.yes_button.connect("clicked", self.try_stable, None)
195                 elif self.gui == 'error_try_default_mirror':
196                     self.yes_button.connect("clicked", self.try_default_mirror, None)
197                 elif self.gui == 'error_try_forcing_english':
198                     self.yes_button.connect("clicked", self.try_forcing_english, None)
199                 elif self.gui == 'error_try_tor':
200                     self.yes_button.connect("clicked", self.try_tor, None)
201                 self.button_box.add(self.yes_button)
202                 self.yes_button.show()
203
204             # exit button
205             exit_image = gtk.Image()
206             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
207             self.exit_button = gtk.Button("Exit")
208             self.exit_button.set_image(exit_image)
209             self.exit_button.connect("clicked", self.destroy, None)
210             self.button_box.add(self.exit_button)
211             self.exit_button.show()
212
213         elif self.gui == 'task':
214             # label
215             self.label = gtk.Label(self.gui_message)
216             self.label.set_line_wrap(True)
217             self.box.pack_start(self.label, True, True, 0)
218             self.label.show()
219
220             # progress bar
221             self.progressbar = gtk.ProgressBar(adjustment=None)
222             self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
223             self.progressbar.set_pulse_step(0.01)
224             self.box.pack_start(self.progressbar, True, True, 0)
225
226             # button box
227             self.button_box = gtk.HButtonBox()
228             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
229             self.box.pack_start(self.button_box, True, True, 0)
230             self.button_box.show()
231
232             # start button
233             start_image = gtk.Image()
234             start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
235             self.start_button = gtk.Button(_("Start"))
236             self.start_button.set_image(start_image)
237             self.start_button.connect("clicked", self.start, None)
238             self.button_box.add(self.start_button)
239             if not self.gui_autostart:
240                 self.start_button.show()
241
242             # exit button
243             exit_image = gtk.Image()
244             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
245             self.exit_button = gtk.Button(_("Cancel"))
246             self.exit_button.set_image(exit_image)
247             self.exit_button.connect("clicked", self.destroy, None)
248             self.button_box.add(self.exit_button)
249             self.exit_button.show()
250
251         self.box.show()
252         self.window.show()
253
254         if self.gui_autostart:
255             self.start(None)
256
257     # start button clicked, begin tasks
258     def start(self, widget, data=None):
259         # disable the start button
260         if self.start_button:
261             self.start_button.set_sensitive(False)
262
263         # start running tasks
264         self.run_task()
265
266     # run the next task in the task list
267     def run_task(self):
268         self.refresh_gtk()
269
270         if self.gui_task_i >= len(self.gui_tasks):
271             self.destroy(False)
272             return
273
274         task = self.gui_tasks[self.gui_task_i]
275
276         # get ready for the next task
277         self.gui_task_i += 1
278
279         if task == 'download_version_check':
280             print(_('Downloading'), self.common.paths['version_check_url'])
281             self.download('version check', self.common.paths['version_check_url'], self.common.paths['version_check_file'])
282
283         if task == 'set_version':
284             version = self.get_stable_version()
285             if version:
286                 self.common.build_paths(self.get_stable_version())
287                 print(_('Latest version: {}').format(version))
288                 self.run_task()
289             else:
290                 self.set_gui('error', _("Error detecting Tor Browser version."), [], False)
291                 self.clear_ui()
292                 self.build_ui()
293
294         elif task == 'download_sig':
295             print(_('Downloading'), self.common.paths['sig_url'].format(self.common.settings['mirror']))
296             self.download('signature', self.common.paths['sig_url'], self.common.paths['sig_file'])
297
298         elif task == 'download_tarball':
299             print(_('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror']))
300             if not self.force_redownload and os.path.exists(self.common.paths['tarball_file']):
301                 self.run_task()
302             else:
303                 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
304
305         elif task == 'verify':
306             print(_('Verifying Signature'))
307             self.verify()
308
309         elif task == 'extract':
310             print(_('Extracting'), self.common.paths['tarball_filename'])
311             self.extract()
312
313         elif task == 'run':
314             print(_('Running'), self.common.paths['tbb']['start'])
315             self.run()
316
317         elif task == 'start_over':
318             print(_('Starting download over again'))
319             self.start_over()
320
321     def response_received(self, response):
322         class FileDownloader(Protocol):
323             def __init__(self, common, file, url, total, progress, done_cb):
324                 self.file = file
325                 self.total = total
326                 self.so_far = 0
327                 self.progress = progress
328                 self.all_done = done_cb
329
330                 if response.code != 200:
331                     if common.settings['mirror'] != common.default_mirror:
332                         raise TryDefaultMirrorException(
333                             (_("Download Error:") + " {0} {1}\n\n" + _("You are currently using a non-default mirror")
334                              + ":\n{2}\n\n" + _("Would you like to switch back to the default?")).format(
335                                 response.code, response.phrase, common.settings['mirror']
336                             )
337                         )
338                     elif common.language != 'en-US' and not common.settings['force_en-US']:
339                         raise TryForcingEnglishException(
340                             (_("Download Error:") + " {0} {1}\n\n"
341                              + _("Would you like to try the English version of Tor Browser instead?")).format(
342                                 response.code, response.phrase
343                             )
344                         )
345                     else:
346                         raise DownloadErrorException(
347                             (_("Download Error:") + " {0} {1}").format(response.code, response.phrase)
348                         )
349
350             def dataReceived(self, bytes):
351                 self.file.write(bytes)
352                 self.so_far += len(bytes)
353                 percent = float(self.so_far) / float(self.total)
354                 self.progress.set_fraction(percent)
355                 amount = float(self.so_far)
356                 units = "bytes"
357                 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
358                     if amount > size:
359                         units = unit
360                         amount /= float(size)
361                         break
362
363                 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
364
365             def connectionLost(self, reason):
366                 self.all_done(reason)
367
368         if hasattr(self, 'current_download_url'):
369             url = self.current_download_url
370         else:
371             url = None
372
373         dl = FileDownloader(
374             self.common, self.file_download, url, response.length, self.progressbar, self.response_finished
375         )
376         response.deliverBody(dl)
377
378     def response_finished(self, msg):
379         if msg.check(ResponseDone):
380             self.file_download.close()
381             delattr(self, 'current_download_path')
382             delattr(self, 'current_download_url')
383
384             # next task!
385             self.run_task()
386
387         else:
388             print("FINISHED", msg)
389             ## FIXME handle errors
390
391     def download_error(self, f):
392         print(_("Download Error:"), f.value, type(f.value))
393
394         if isinstance(f.value, TryStableException):
395             f.trap(TryStableException)
396             self.set_gui('error_try_stable', str(f.value), [], False)
397
398         elif isinstance(f.value, TryDefaultMirrorException):
399             f.trap(TryDefaultMirrorException)
400             self.set_gui('error_try_default_mirror', str(f.value), [], False)
401
402         elif isinstance(f.value, TryForcingEnglishException):
403             f.trap(TryForcingEnglishException)
404             self.set_gui('error_try_forcing_english', str(f.value), [], False)
405
406         elif isinstance(f.value, DownloadErrorException):
407             f.trap(DownloadErrorException)
408             self.set_gui('error', str(f.value), [], False)
409
410         elif isinstance(f.value, DNSLookupError):
411             f.trap(DNSLookupError)
412             if common.settings['mirror'] != common.default_mirror:
413                 self.set_gui('error_try_default_mirror', (_("DNS Lookup Error") + "\n\n" +
414                                                           _("You are currently using a non-default mirror")
415                                                           + ":\n{0}\n\n"
416                                                           + _("Would you like to switch back to the default?")
417                                                           ).format(common.settings['mirror']), [], False)
418             else:
419                 self.set_gui('error', str(f.value), [], False)
420
421         elif isinstance(f.value, ResponseFailed):
422             for reason in f.value.reasons:
423                 if isinstance(reason.value, OpenSSL.SSL.Error):
424                     # TODO: add the ability to report attack by posting bug to trac.torproject.org
425                     if not self.common.settings['download_over_tor']:
426                         self.set_gui('error_try_tor',
427                                      _('The SSL certificate served by https://www.torproject.org is invalid! You may '
428                                        'be under attack.') + " " + _('Try the download again using Tor?'), [], False)
429                     else:
430                         self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! '
431                                                 'You may be under attack.'), [], False)
432
433         elif isinstance(f.value, ConnectionRefusedError) and self.common.settings['download_over_tor']:
434             # If we're using Tor, we'll only get this error when we fail to
435             # connect to the SOCKS server.  If the connection fails at the
436             # remote end, we'll get txsocksx.errors.ConnectionRefused.
437             addr = self.common.settings['tor_socks_address']
438             self.set_gui('error', _("Error connecting to Tor at {0}").format(addr), [], False)
439
440         else:
441             self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
442
443         self.build_ui()
444
445     def download(self, name, url, path):
446         # keep track of current download
447         self.current_download_path = path
448         self.current_download_url = url
449
450         mirror_url = url.format(self.common.settings['mirror'])
451
452         # convert mirror_url from unicode to string, if needed (#205)
453         if isinstance(mirror_url, unicode):
454             mirror_url = unicodedata.normalize('NFKD', mirror_url).encode('ascii', 'ignore')
455
456         # initialize the progress bar
457         self.progressbar.set_fraction(0)
458         self.progressbar.set_text(_('Downloading') + ' {0}'.format(name))
459         self.progressbar.show()
460         self.refresh_gtk()
461
462         if self.common.settings['download_over_tor']:
463             from twisted.internet.endpoints import clientFromString
464             from txsocksx.http import SOCKS5Agent
465
466             torendpoint = clientFromString(reactor, self.common.settings['tor_socks_address'])
467
468             # default mirror gets certificate pinning, only for requests that use the mirror
469             agent = SOCKS5Agent(reactor, proxyEndpoint=torendpoint)
470         else:
471             agent = Agent(reactor)
472
473         # actually, agent needs to follow redirect
474         agent = RedirectAgent(agent)
475
476         # start the request
477         d = agent.request('GET', mirror_url,
478                           Headers({'User-Agent': ['torbrowser-launcher']}),
479                           None)
480
481         self.file_download = open(path, 'w')
482         d.addCallback(self.response_received).addErrback(self.download_error)
483
484         if not reactor.running:
485             reactor.run()
486
487     def try_default_mirror(self, widget, data=None):
488         # change mirror to default and relaunch TBL
489         self.common.settings['mirror'] = self.common.default_mirror
490         self.common.save_settings()
491         subprocess.Popen([self.common.paths['tbl_bin']])
492         self.destroy(False)
493
494     def try_forcing_english(self, widget, data=None):
495         # change force english to true and relaunch TBL
496         self.common.settings['force_en-US'] = True
497         self.common.save_settings()
498         subprocess.Popen([self.common.paths['tbl_bin']])
499         self.destroy(False)
500
501     def try_tor(self, widget, data=None):
502         # set download_over_tor to true and relaunch TBL
503         self.common.settings['download_over_tor'] = True
504         self.common.save_settings()
505         subprocess.Popen([self.common.paths['tbl_bin']])
506         self.destroy(False)
507
508     def get_stable_version(self):
509         tree = ET.parse(self.common.paths['version_check_file'])
510         for up in tree.getroot():
511             if up.tag == 'update' and up.attrib['appVersion']:
512                 version = str(up.attrib['appVersion'])
513
514                 # make sure the version does not contain directory traversal attempts
515                 # e.g. "5.5.3", "6.0a", "6.0a-hardened" are valid but "../../../../.." is invalid
516                 if not re.match(r'^[a-z0-9\.\-]+$', version):
517                     return None
518
519                 return version
520         return None
521
522     def verify(self):
523         self.progressbar.set_fraction(0)
524         self.progressbar.set_text(_('Verifying Signature'))
525         self.progressbar.show()
526
527         def gui_raise_sigerror(self, sigerror='MissingErr'):
528             """
529             :type sigerror: str
530             """
531             sigerror = 'SIGNATURE VERIFICATION FAILED!\n\nError Code: {0}\n\nYou might be under attack, there might' \
532                        ' be a network\nproblem, or you may be missing a recently added\nTor Browser verification key.' \
533                        '\nClick Start to refresh the keyring and try again. If the message persists report the above' \
534                        ' error code here:\nhttps://github.com/micahflee/torbrowser-launcher/issues'.format(sigerror)
535
536             self.set_gui('task', sigerror, ['start_over'], False)
537             self.clear_ui()
538             self.build_ui()
539
540         if gpgme_support:
541             with gpg.Context() as c:
542                 c.set_engine_info(gpg.constants.protocol.OpenPGP, home_dir=self.common.paths['gnupg_homedir'])
543
544                 sig = gpg.Data(file=self.common.paths['sig_file'])
545                 signed = gpg.Data(file=self.common.paths['tarball_file'])
546
547                 try:
548                     c.verify(signature=sig, signed_data=signed)
549                 except gpg.errors.BadSignatures as e:
550                     result = str(e).split(": ")
551                     if result[1] == 'Bad signature':
552                         gui_raise_sigerror(self, str(e))
553                     elif result[1] == 'No public key':
554                         self.common.refresh_keyring(result[0])
555                         gui_raise_sigerror(self, str(e))
556                 else:
557                     self.run_task()
558         else:
559             FNULL = open(os.devnull, 'w')
560             p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify',
561                                   self.common.paths['sig_file'], self.common.paths['tarball_file']], stdout=FNULL,
562                                  stderr=subprocess.STDOUT)
563             self.pulse_until_process_exits(p)
564             if p.returncode == 0:
565                 self.run_task()
566             else:
567                 self.common.refresh_keyring()
568                 gui_raise_sigerror(self, 'GENERIC_VERIFY_FAIL')
569                 if not reactor.running:
570                     reactor.run()
571
572     def extract(self):
573         # initialize the progress bar
574         self.progressbar.set_fraction(0)
575         self.progressbar.set_text(_('Installing'))
576         self.progressbar.show()
577         self.refresh_gtk()
578
579         extracted = False
580         try:
581             if self.common.paths['tarball_file'][-2:] == 'xz':
582                 # if tarball is .tar.xz
583                 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
584                 tf = tarfile.open(fileobj=xz)
585                 tf.extractall(self.common.paths['tbb']['dir'])
586                 extracted = True
587             else:
588                 # if tarball is .tar.gz
589                 if tarfile.is_tarfile(self.common.paths['tarball_file']):
590                     tf = tarfile.open(self.common.paths['tarball_file'])
591                     tf.extractall(self.common.paths['tbb']['dir'])
592                     extracted = True
593         except:
594             pass
595
596         if not extracted:
597             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
598             self.clear_ui()
599             self.build_ui()
600             return
601
602         self.run_task()
603
604     def check_min_version(self):
605         installed_version = None
606         for line in open(self.common.paths['tbb']['changelog']).readlines():
607             if line.startswith('Tor Browser '):
608                 installed_version = line.split()[2]
609                 break
610
611         if self.min_version <= installed_version:
612             return True
613
614         return False
615
616     def run(self, run_next_task=True):
617         # don't run if it isn't at least the minimum version
618         if not self.check_min_version():
619             message = _("The version of Tor Browser you have installed is earlier than it should be, which could be a "
620                         "sign of an attack!")
621             print(message)
622
623             md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _(message))
624             md.set_position(gtk.WIN_POS_CENTER)
625             md.run()
626             md.destroy()
627
628             return
629
630         # hide the TBL window (#151)
631         if hasattr(self, 'window'):
632             self.window.hide()
633             while gtk.events_pending():
634                 gtk.main_iteration_do(True)
635
636         # run Tor Browser
637         subprocess.call([self.common.paths['tbb']['start']], cwd=self.common.paths['tbb']['dir_tbb'])
638
639         if run_next_task:
640             self.run_task()
641
642     # make the progress bar pulse until process p (a Popen object) finishes
643     def pulse_until_process_exits(self, p):
644         while p.poll() is None:
645             time.sleep(0.01)
646             self.progressbar.pulse()
647             self.refresh_gtk()
648
649     # start over and download TBB again
650     def start_over(self):
651         self.force_redownload = True  # Overwrite any existing file
652         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
653         self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
654         self.gui_task_i = 0
655         self.start(None)
656
657     # refresh gtk
658     def refresh_gtk(self):
659         while gtk.events_pending():
660             gtk.main_iteration(False)
661
662     # exit
663     def delete_event(self, widget, event, data=None):
664         return False
665
666     def destroy(self, widget, data=None):
667         if hasattr(self, 'file_download'):
668             self.file_download.close()
669         if hasattr(self, 'current_download_path'):
670             os.remove(self.current_download_path)
671             delattr(self, 'current_download_path')
672             delattr(self, 'current_download_url')
673         if reactor.running:
674             reactor.stop()