3 https://github.com/micahflee/torbrowser-launcher/
5 Copyright (c) 2013-2014 Micah Lee <micah@micahflee.com>
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
16 The above copyright notice and this permission notice shall be
17 included in all copies or substantial portions of the Software.
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.
29 import os, subprocess, time, json, tarfile, hashlib, lzma, threading, re, unicodedata
30 from twisted.internet import reactor
31 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
32 from twisted.web.http_headers import Headers
33 from twisted.web.iweb import IPolicyForHTTPS
34 from twisted.internet.protocol import Protocol
35 from twisted.internet.error import DNSLookupError, ConnectionRefusedError
37 import xml.etree.ElementTree as ET
45 class TryStableException(Exception):
48 class TryDefaultMirrorException(Exception):
51 class DownloadErrorException(Exception):
55 def __init__(self, common, url_list):
57 self.url_list = url_list
59 # this is the current version of Tor Browser, which should get updated with every release
60 self.min_version = '5.5.2'
63 self.set_gui(None, '', [])
64 self.launch_gui = True
66 # if Tor Browser is not installed, detect latest version, download, and install
67 if not self.common.settings['installed']:
68 # if downloading over Tor, include txsocksx
69 if self.common.settings['download_over_tor']:
72 print _('Downloading over Tor')
74 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"))
75 md.set_position(gtk.WIN_POS_CENTER)
78 self.common.settings['download_over_tor'] = False
79 self.common.save_settings()
81 # download and install
82 print _("Downloading and installing Tor Browser for the first time.")
83 self.set_gui('task', _("Downloading and installing Tor Browser for the first time."),
84 ['download_version_check',
93 # Tor Browser is already installed, so run
95 self.launch_gui = False
98 # build the rest of the UI
101 def configure_window(self):
102 if not hasattr(self, 'window'):
103 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
104 self.window.set_title(_("Tor Browser"))
105 self.window.set_icon_from_file(self.common.paths['icon_file'])
106 self.window.set_position(gtk.WIN_POS_CENTER)
107 self.window.set_border_width(10)
108 self.window.connect("delete_event", self.delete_event)
109 self.window.connect("destroy", self.destroy)
111 # there are different GUIs that might appear, this sets which one we want
112 def set_gui(self, gui, message, tasks, autostart=True):
114 self.gui_message = message
115 self.gui_tasks = tasks
117 self.gui_autostart = autostart
119 # set all gtk variables to False
121 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
126 self.progressbar = False
127 self.button_box = False
128 self.start_button = False
129 self.exit_button = False
131 # build the application's UI
135 self.box = gtk.VBox(False, 20)
136 self.configure_window()
137 self.window.add(self.box)
139 if 'error' in self.gui:
141 self.label = gtk.Label(self.gui_message)
142 self.label.set_line_wrap(True)
143 self.box.pack_start(self.label, True, True, 0)
147 self.button_box = gtk.HButtonBox()
148 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
149 self.box.pack_start(self.button_box, True, True, 0)
150 self.button_box.show()
152 if self.gui != 'error':
154 yes_image = gtk.Image()
155 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
156 self.yes_button = gtk.Button("Yes")
157 self.yes_button.set_image(yes_image)
158 if self.gui == 'error_try_stable':
159 self.yes_button.connect("clicked", self.try_stable, None)
160 elif self.gui == 'error_try_default_mirror':
161 self.yes_button.connect("clicked", self.try_default_mirror, None)
162 elif self.gui == 'error_try_tor':
163 self.yes_button.connect("clicked", self.try_tor, None)
164 self.button_box.add(self.yes_button)
165 self.yes_button.show()
168 exit_image = gtk.Image()
169 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
170 self.exit_button = gtk.Button("Exit")
171 self.exit_button.set_image(exit_image)
172 self.exit_button.connect("clicked", self.destroy, None)
173 self.button_box.add(self.exit_button)
174 self.exit_button.show()
176 elif self.gui == 'task':
178 self.label = gtk.Label(self.gui_message)
179 self.label.set_line_wrap(True)
180 self.box.pack_start(self.label, True, True, 0)
184 self.progressbar = gtk.ProgressBar(adjustment=None)
185 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
186 self.progressbar.set_pulse_step(0.01)
187 self.box.pack_start(self.progressbar, True, True, 0)
190 self.button_box = gtk.HButtonBox()
191 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
192 self.box.pack_start(self.button_box, True, True, 0)
193 self.button_box.show()
196 start_image = gtk.Image()
197 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
198 self.start_button = gtk.Button(_("Start"))
199 self.start_button.set_image(start_image)
200 self.start_button.connect("clicked", self.start, None)
201 self.button_box.add(self.start_button)
202 if not self.gui_autostart:
203 self.start_button.show()
206 exit_image = gtk.Image()
207 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
208 self.exit_button = gtk.Button(_("Exit"))
209 self.exit_button.set_image(exit_image)
210 self.exit_button.connect("clicked", self.destroy, None)
211 self.button_box.add(self.exit_button)
212 self.exit_button.show()
217 if self.gui_autostart:
220 # start button clicked, begin tasks
221 def start(self, widget, data=None):
222 # disable the start button
223 if self.start_button:
224 self.start_button.set_sensitive(False)
226 # start running tasks
229 # run the next task in the task list
233 if self.gui_task_i >= len(self.gui_tasks):
237 task = self.gui_tasks[self.gui_task_i]
239 # get ready for the next task
242 if task == 'download_version_check':
243 print _('Downloading'), self.common.paths['version_check_url']
244 self.download('version check', self.common.paths['version_check_url'], self.common.paths['version_check_file'])
246 if task == 'set_version':
247 version = self.get_stable_version()
249 self.common.build_paths(self.get_stable_version())
250 print _('Latest version: {}').format(version)
253 self.set_gui('error', _("Error detecting Tor Browser version."), [], False)
257 elif task == 'download_sig':
258 print _('Downloading'), self.common.paths['sig_url'].format(self.common.settings['mirror'])
259 self.download('signature', self.common.paths['sig_url'], self.common.paths['sig_file'])
261 elif task == 'download_tarball':
262 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
263 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
265 elif task == 'verify':
266 print _('Verifying signature')
269 elif task == 'extract':
270 print _('Extracting'), self.common.paths['tarball_filename']
274 print _('Running'), self.common.paths['tbb']['start']
277 elif task == 'start_over':
278 print _('Starting download over again')
281 def response_received(self, response):
282 class FileDownloader(Protocol):
283 def __init__(self, common, file, url, total, progress, done_cb):
287 self.progress = progress
288 self.all_done = done_cb
290 if response.code != 200:
291 if common.settings['mirror'] != common.default_mirror:
292 raise TryDefaultMirrorException(_("Download Error: {0} {1}\n\nYou are currently using a non-default mirror:\n{2}\n\nWould you like to switch back to the default?").format(response.code, response.phrase, common.settings['mirror']))
294 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
296 def dataReceived(self, bytes):
297 self.file.write(bytes)
298 self.so_far += len(bytes)
299 percent = float(self.so_far) / float(self.total)
300 self.progress.set_fraction(percent)
301 amount = float(self.so_far)
303 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
306 amount = amount / float(size)
309 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
311 def connectionLost(self, reason):
312 self.all_done(reason)
314 if hasattr(self, 'current_download_url'):
315 url = self.current_download_url
319 dl = FileDownloader(self.common, self.file_download, url, response.length, self.progressbar, self.response_finished)
320 response.deliverBody(dl)
322 def response_finished(self, msg):
323 if msg.check(ResponseDone):
324 self.file_download.close()
325 delattr(self, 'current_download_path')
326 delattr(self, 'current_download_url')
332 print "FINISHED", msg
333 ## FIXME handle errors
335 def download_error(self, f):
336 print _("Download error:"), f.value, type(f.value)
338 if isinstance(f.value, TryStableException):
339 f.trap(TryStableException)
340 self.set_gui('error_try_stable', str(f.value), [], False)
342 elif isinstance(f.value, TryDefaultMirrorException):
343 f.trap(TryDefaultMirrorException)
344 self.set_gui('error_try_default_mirror', str(f.value), [], False)
346 elif isinstance(f.value, DownloadErrorException):
347 f.trap(DownloadErrorException)
348 self.set_gui('error', str(f.value), [], False)
350 elif isinstance(f.value, DNSLookupError):
351 f.trap(DNSLookupError)
352 if common.settings['mirror'] != common.default_mirror:
353 self.set_gui('error_try_default_mirror', _("DNS Lookup Error\n\nYou are currently using a non-default mirror:\n{0}\n\nWould you like to switch back to the default?").format(common.settings['mirror']), [], False)
355 self.set_gui('error', str(f.value), [], False)
357 elif isinstance(f.value, ResponseFailed):
358 for reason in f.value.reasons:
359 if isinstance(reason.value, OpenSSL.SSL.Error):
360 # TODO: add the ability to report attack by posting bug to trac.torproject.org
361 if not self.common.settings['download_over_tor']:
362 self.set_gui('error_try_tor', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack. Try the download again using Tor?'), [], False)
364 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
366 elif isinstance(f.value, ConnectionRefusedError) and self.common.settings['download_over_tor']:
367 # If we're using Tor, we'll only get this error when we fail to
368 # connect to the SOCKS server. If the connection fails at the
369 # remote end, we'll get txsocksx.errors.ConnectionRefused.
370 addr = self.common.settings['tor_socks_address']
371 self.set_gui('error', _("Error connecting to Tor at {0}").format(addr), [], False)
374 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
378 def download(self, name, url, path):
379 # keep track of current download
380 self.current_download_path = path
381 self.current_download_url = url
383 mirror_url = url.format(self.common.settings['mirror'])
385 # convert mirror_url from unicode to string, if needed (#205)
386 if isinstance(mirror_url, unicode):
387 mirror_url = unicodedata.normalize('NFKD', mirror_url).encode('ascii','ignore')
389 # initialize the progress bar
390 self.progressbar.set_fraction(0)
391 self.progressbar.set_text(_('Downloading {0}').format(name))
392 self.progressbar.show()
395 if self.common.settings['download_over_tor']:
396 from twisted.internet.endpoints import clientFromString
397 from txsocksx.http import SOCKS5Agent
399 torEndpoint = clientFromString(reactor, self.common.settings['tor_socks_address'])
401 # default mirror gets certificate pinning, only for requests that use the mirror
402 agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
404 agent = Agent(reactor)
406 # actually, agent needs to follow redirect
407 agent = RedirectAgent(agent)
410 d = agent.request('GET', mirror_url,
411 Headers({'User-Agent': ['torbrowser-launcher']}),
414 self.file_download = open(path, 'w')
415 d.addCallback(self.response_received).addErrback(self.download_error)
417 if not reactor.running:
420 def try_default_mirror(self, widget, data=None):
421 # change mirror to default and relaunch TBL
422 self.common.settings['mirror'] = self.common.default_mirror
423 self.common.save_settings()
424 subprocess.Popen([self.common.paths['tbl_bin']])
427 def try_tor(self, widget, data=None):
428 # set download_over_tor to true and relaunch TBL
429 self.common.settings['download_over_tor'] = True
430 self.common.save_settings()
431 subprocess.Popen([self.common.paths['tbl_bin']])
434 def get_stable_version(self):
435 tree = ET.parse(self.common.paths['version_check_file'])
436 for up in tree.getroot():
437 if up.tag == 'update' and up.attrib['appVersion']:
438 return str(up.attrib['appVersion'])
442 # initialize the progress bar
443 self.progressbar.set_fraction(0)
444 self.progressbar.set_text(_('Verifying Signature'))
445 self.progressbar.show()
447 # verify the PGP signature
449 FNULL = open(os.devnull, 'w')
450 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sig_file']], stdout=FNULL, stderr=subprocess.STDOUT)
451 self.pulse_until_process_exits(p)
452 if p.returncode == 0:
458 # TODO: add the ability to report attack by posting bug to trac.torproject.org
459 self.set_gui('task', _("SIGNATURE VERIFICATION FAILED!\n\nYou might be under attack, or there might just be a networking problem. Click Start try the download again."), ['start_over'], False)
463 if not reactor.running:
467 # initialize the progress bar
468 self.progressbar.set_fraction(0)
469 self.progressbar.set_text(_('Installing'))
470 self.progressbar.show()
475 if self.common.paths['tarball_file'][-2:] == 'xz':
476 # if tarball is .tar.xz
477 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
478 tf = tarfile.open(fileobj=xz)
479 tf.extractall(self.common.paths['tbb']['dir'])
482 # if tarball is .tar.gz
483 if tarfile.is_tarfile(self.common.paths['tarball_file']):
484 tf = tarfile.open(self.common.paths['tarball_file'])
485 tf.extractall(self.common.paths['tbb']['dir'])
491 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
498 def check_min_version(self):
499 installed_version = None
500 for line in open(self.common.paths['tbb']['versions']).readlines():
501 if line.startswith('TORBROWSER_VERSION='):
502 installed_version = line.split('=')[1].strip()
505 if self.min_version <= installed_version:
510 def run(self, run_next_task=True):
511 # don't run if it isn't at least the minimum version
512 if not self.check_min_version():
513 message = _("The version of Tor Browser you have installed is earlier than it should be, which could be a sign of an attack!")
516 md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _(message))
517 md.set_position(gtk.WIN_POS_CENTER)
524 if self.common.settings['modem_sound']:
525 def play_modem_sound():
529 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
533 md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-pygame package is missing, the modem sound is unavailable."))
534 md.set_position(gtk.WIN_POS_CENTER)
538 t = threading.Thread(target=play_modem_sound)
541 # hide the TBL window (#151)
542 if hasattr(self, 'window'):
544 while gtk.events_pending():
545 gtk.main_iteration_do(True)
548 subprocess.call([self.common.paths['tbb']['start']], cwd=self.common.paths['tbb']['dir_tbb'])
553 # make the progress bar pulse until process p (a Popen object) finishes
554 def pulse_until_process_exits(self, p):
555 while p.poll() is None:
557 self.progressbar.pulse()
560 # start over and download TBB again
561 def start_over(self):
562 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
563 self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
568 def refresh_gtk(self):
569 while gtk.events_pending():
570 gtk.main_iteration(False)
573 def delete_event(self, widget, event, data=None):
576 def destroy(self, widget, data=None):
577 if hasattr(self, 'file_download'):
578 self.file_download.close()
579 if hasattr(self, 'current_download_path'):
580 os.remove(self.current_download_path)
581 delattr(self, 'current_download_path')
582 delattr(self, 'current_download_url')