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
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.internet.protocol import Protocol
34 from twisted.internet.ssl import ClientContextFactory
35 from twisted.internet.error import DNSLookupError
43 class TryStableException(Exception):
46 class TryDefaultMirrorException(Exception):
49 class DownloadErrorException(Exception):
52 class VerifyTorProjectCert(ClientContextFactory):
53 def __init__(self, torproject_pem):
54 self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
56 def getContext(self, host, port):
57 ctx = ClientContextFactory.getContext(self)
58 ctx.set_verify_depth(0)
59 ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
62 def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
63 return cert.digest('sha256') == self.torproject_ca.digest('sha256')
66 def __init__(self, common, url_list):
67 print _('Starting launcher dialog')
69 self.url_list = url_list
72 self.set_gui(None, '', [])
73 self.launch_gui = True
74 print "LATEST VERSION", self.common.settings['latest_version']
75 self.common.build_paths(self.common.settings['latest_version'])
77 if self.common.settings['update_over_tor']:
81 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"))
82 md.set_position(gtk.WIN_POS_CENTER)
85 self.common.settings['update_over_tor'] = False
86 self.common.save_settings()
88 # is firefox already running?
89 if self.common.settings['installed_version']:
90 firefox_pid = self.common.get_pid('./Browser/firefox')
92 print _('Firefox is open, bringing to focus')
93 # bring firefox to front
94 self.common.bring_window_to_front(firefox_pid)
98 check_for_updates = False
99 if self.common.settings['check_for_updates']:
100 check_for_updates = True
102 if not check_for_updates:
103 # how long was it since the last update check?
104 # 86400 seconds = 24 hours
105 current_timestamp = int(time.time())
106 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
107 check_for_updates = True
109 if check_for_updates:
111 print 'Checking for update'
112 self.set_gui('task', _("Checking for Tor Browser update."),
113 ['download_update_check',
116 # no need to check for update
117 print _('Checked for update within 24 hours, skipping')
118 self.start_launcher()
122 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
123 self.window.set_title(_("Tor Browser"))
124 self.window.set_icon_from_file(self.common.paths['icon_file'])
125 self.window.set_position(gtk.WIN_POS_CENTER)
126 self.window.set_border_width(10)
127 self.window.connect("delete_event", self.delete_event)
128 self.window.connect("destroy", self.destroy)
130 # build the rest of the UI
133 # download or run TBB
134 def start_launcher(self):
135 # is TBB already installed?
136 latest_version = self.common.settings['latest_version']
137 installed_version = self.common.settings['installed_version']
139 # verify installed version for newer versions of TBB (#58)
140 if installed_version >= '3.0':
141 versions_filename = self.common.paths['tbb']['versions']
142 if os.path.exists(versions_filename):
143 for line in open(versions_filename):
144 if 'TORBROWSER_VERSION' in line:
145 installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
147 start = self.common.paths['tbb']['start']
148 if os.path.isfile(start) and os.access(start, os.X_OK):
149 if installed_version == latest_version:
150 print _('Latest version of TBB is installed, launching')
151 # current version of tbb is installed, launch it
153 self.launch_gui = False
154 elif installed_version < latest_version:
155 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
156 # there is a tbb upgrade available
157 self.set_gui('task', _("Your Tor Browser is out of date. Upgrading from {0} to {1}.".format(installed_version, latest_version)),
159 'download_sha256_sig',
165 # for some reason the installed tbb is newer than the current version?
166 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
170 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
171 self.set_gui('task', _("Downloading and installing Tor Browser for the first time."),
173 'download_sha256_sig',
179 # there are different GUIs that might appear, this sets which one we want
180 def set_gui(self, gui, message, tasks, autostart=True):
182 self.gui_message = message
183 self.gui_tasks = tasks
185 self.gui_autostart = autostart
187 # set all gtk variables to False
189 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
194 self.progressbar = False
195 self.button_box = False
196 self.start_button = False
197 self.exit_button = False
199 # build the application's UI
203 self.box = gtk.VBox(False, 20)
204 self.window.add(self.box)
206 if 'error' in self.gui:
208 self.label = gtk.Label(self.gui_message)
209 self.label.set_line_wrap(True)
210 self.box.pack_start(self.label, True, True, 0)
214 self.button_box = gtk.HButtonBox()
215 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
216 self.box.pack_start(self.button_box, True, True, 0)
217 self.button_box.show()
219 if self.gui != 'error':
221 yes_image = gtk.Image()
222 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
223 self.yes_button = gtk.Button("Yes")
224 self.yes_button.set_image(yes_image)
225 if self.gui == 'error_try_stable':
226 self.yes_button.connect("clicked", self.try_stable, None)
227 elif self.gui == 'error_try_default_mirror':
228 self.yes_button.connect("clicked", self.try_default_mirror, None)
229 elif self.gui == 'error_try_tor':
230 self.yes_button.connect("clicked", self.try_tor, None)
231 self.button_box.add(self.yes_button)
232 self.yes_button.show()
235 exit_image = gtk.Image()
236 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
237 self.exit_button = gtk.Button("Exit")
238 self.exit_button.set_image(exit_image)
239 self.exit_button.connect("clicked", self.destroy, None)
240 self.button_box.add(self.exit_button)
241 self.exit_button.show()
243 elif self.gui == 'task':
245 self.label = gtk.Label(self.gui_message)
246 self.label.set_line_wrap(True)
247 self.box.pack_start(self.label, True, True, 0)
251 self.progressbar = gtk.ProgressBar(adjustment=None)
252 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
253 self.progressbar.set_pulse_step(0.01)
254 self.box.pack_start(self.progressbar, True, True, 0)
257 self.button_box = gtk.HButtonBox()
258 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
259 self.box.pack_start(self.button_box, True, True, 0)
260 self.button_box.show()
263 start_image = gtk.Image()
264 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
265 self.start_button = gtk.Button(_("Start"))
266 self.start_button.set_image(start_image)
267 self.start_button.connect("clicked", self.start, None)
268 self.button_box.add(self.start_button)
269 if not self.gui_autostart:
270 self.start_button.show()
273 exit_image = gtk.Image()
274 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
275 self.exit_button = gtk.Button(_("Exit"))
276 self.exit_button.set_image(exit_image)
277 self.exit_button.connect("clicked", self.destroy, None)
278 self.button_box.add(self.exit_button)
279 self.exit_button.show()
284 if self.gui_autostart:
287 # start button clicked, begin tasks
288 def start(self, widget, data=None):
289 # disable the start button
290 if self.start_button:
291 self.start_button.set_sensitive(False)
293 # start running tasks
296 # run the next task in the task list
300 if self.gui_task_i >= len(self.gui_tasks):
304 task = self.gui_tasks[self.gui_task_i]
306 # get ready for the next task
309 print _('Running task: {0}'.format(task))
310 if task == 'download_update_check':
311 print _('Downloading'), self.common.paths['update_check_url']
312 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
314 if task == 'attempt_update':
315 print _('Checking to see if update is needed')
316 self.attempt_update()
318 elif task == 'download_sha256':
319 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
320 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
322 elif task == 'download_sha256_sig':
323 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
324 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
326 elif task == 'download_tarball':
327 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
328 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
330 elif task == 'verify':
331 print _('Verifying signature')
334 elif task == 'extract':
335 print _('Extracting'), self.common.paths['tarball_filename']
339 print _('Running'), self.common.paths['tbb']['start']
342 elif task == 'start_over':
343 print _('Starting download over again')
346 def response_received(self, response):
347 class FileDownloader(Protocol):
348 def __init__(self, common, file, url, total, progress, done_cb):
352 self.progress = progress
353 self.all_done = done_cb
355 if response.code != 200:
356 if common.settings['mirror'] != common.default_mirror:
357 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']))
359 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
361 def dataReceived(self, bytes):
362 self.file.write(bytes)
363 self.so_far += len(bytes)
364 percent = float(self.so_far) / float(self.total)
365 self.progress.set_fraction(percent)
366 amount = float(self.so_far)
368 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
371 amount = amount / float(size)
374 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
376 def connectionLost(self, reason):
377 print _('Finished receiving body:'), reason.getErrorMessage()
378 self.all_done(reason)
380 if hasattr(self, 'current_download_url'):
381 url = self.current_download_url
385 dl = FileDownloader(self.common, self.file_download, url, response.length, self.progressbar, self.response_finished)
386 response.deliverBody(dl)
388 def response_finished(self, msg):
389 if msg.check(ResponseDone):
390 self.file_download.close()
391 delattr(self, 'current_download_path')
392 delattr(self, 'current_download_url')
398 print "FINISHED", msg
399 ## FIXME handle errors
401 def download_error(self, f):
402 print _("Download error:"), f.value, type(f.value)
404 if isinstance(f.value, TryStableException):
405 f.trap(TryStableException)
406 self.set_gui('error_try_stable', str(f.value), [], False)
408 elif isinstance(f.value, TryDefaultMirrorException):
409 f.trap(TryDefaultMirrorException)
410 self.set_gui('error_try_default_mirror', str(f.value), [], False)
412 elif isinstance(f.value, DownloadErrorException):
413 f.trap(DownloadErrorException)
414 self.set_gui('error', str(f.value), [], False)
416 elif isinstance(f.value, DNSLookupError):
417 f.trap(DNSLookupError)
418 if common.settings['mirror'] != common.default_mirror:
419 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)
421 self.set_gui('error', str(f.value), [], False)
423 elif isinstance(f.value, ResponseFailed):
424 for reason in f.value.reasons:
425 if isinstance(reason.value, OpenSSL.SSL.Error):
426 # TODO: add the ability to report attack by posting bug to trac.torproject.org
427 if not self.common.settings['update_over_tor']:
428 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)
430 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
433 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
437 def download(self, name, url, path):
438 # keep track of current download
439 self.current_download_path = path
440 self.current_download_url = url
442 # initialize the progress bar
443 mirror_url = url.format(self.common.settings['mirror'])
444 self.progressbar.set_fraction(0)
445 self.progressbar.set_text(_('Downloading {0}').format(name))
446 self.progressbar.show()
449 if self.common.settings['update_over_tor']:
450 print _('Updating over Tor')
451 from twisted.internet.endpoints import TCP4ClientEndpoint
452 from txsocksx.http import SOCKS5Agent
454 torEndpoint = TCP4ClientEndpoint(reactor, '127.0.0.1', 9050)
456 # default mirror gets certificate pinning, only for requests that use the mirror
457 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
458 agent = SOCKS5Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']), proxyEndpoint=torEndpoint)
460 agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
462 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
463 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
465 agent = Agent(reactor)
467 # actually, agent needs to follow redirect
468 agent = RedirectAgent(agent)
471 d = agent.request('GET', mirror_url,
472 Headers({'User-Agent': ['torbrowser-launcher']}),
475 self.file_download = open(path, 'w')
476 d.addCallback(self.response_received).addErrback(self.download_error)
478 if not reactor.running:
481 def try_default_mirror(self, widget, data=None):
482 # change mirror to default and relaunch TBL
483 self.common.settings['mirror'] = self.common.default_mirror
484 self.common.save_settings()
485 subprocess.Popen([self.common.paths['tbl_bin']])
488 def try_tor(self, widget, data=None):
489 # set update_over_tor to true and relaunch TBL
490 self.common.settings['update_over_tor'] = True
491 self.common.save_settings()
492 subprocess.Popen([self.common.paths['tbl_bin']])
495 def attempt_update(self):
496 # load the update check file
498 versions = json.load(open(self.common.paths['update_check_file']))
501 # filter linux versions
503 for version in versions:
504 if '-Linux' in version:
505 valid.append(str(version))
510 if len(versions) == 1:
511 latest = versions.pop()
514 # remove alphas/betas
515 for version in versions:
516 if '-alpha-' not in version and '-beta-' not in version:
517 stable.append(version)
519 latest = stable.pop()
521 latest = versions.pop()
525 if latest.endswith('-Linux'):
526 latest = latest.rstrip('-Linux')
528 self.common.settings['latest_version'] = latest
529 self.common.settings['last_update_check_timestamp'] = int(time.time())
530 self.common.settings['check_for_updates'] = False
531 self.common.save_settings()
532 self.common.build_paths(self.common.settings['latest_version'])
533 self.start_launcher()
536 # failed to find the latest version
537 self.set_gui('error', _("Error checking for updates."), [], False)
540 # not a valid JSON object
541 self.set_gui('error', _("Error checking for updates."), [], False)
548 # initialize the progress bar
549 self.progressbar.set_fraction(0)
550 self.progressbar.set_text(_('Verifying Signature'))
551 self.progressbar.show()
554 # check the sha256 file's sig, and also take the sha256 of the tarball and compare
555 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
556 self.pulse_until_process_exits(p)
557 if p.returncode == 0:
558 # compare with sha256 of the tarball
559 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
560 for line in open(self.common.paths['sha256_file'], 'r').readlines():
561 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
567 # TODO: add the ability to report attack by posting bug to trac.torproject.org
568 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)
572 if not reactor.running:
576 # initialize the progress bar
577 self.progressbar.set_fraction(0)
578 self.progressbar.set_text(_('Installing'))
579 self.progressbar.show()
584 if self.common.paths['tarball_file'][-2:] == 'xz':
585 # if tarball is .tar.xz
586 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
587 tf = tarfile.open(fileobj=xz)
588 tf.extractall(self.common.paths['tbb']['dir'])
591 # if tarball is .tar.gz
592 if tarfile.is_tarfile(self.common.paths['tarball_file']):
593 tf = tarfile.open(self.common.paths['tarball_file'])
594 tf.extractall(self.common.paths['tbb']['dir'])
600 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
605 # installation is finished, so save installed_version
606 self.common.settings['installed_version'] = self.common.settings['latest_version']
607 self.common.save_settings()
611 def run(self, run_next_task=True):
613 if self.common.settings['modem_sound']:
614 def play_modem_sound():
618 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
622 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."))
623 md.set_position(gtk.WIN_POS_CENTER)
627 t = threading.Thread(target=play_modem_sound)
630 # hide the TBL window (#151)
631 if hasattr(self, 'window'):
633 while gtk.events_pending():
634 gtk.main_iteration_do(True)
637 if len(self.url_list) == 0:
638 subprocess.call([self.common.paths['tbb']['start']])
640 subprocess.call([self.common.paths['tbb']['start'], '-allow-remote'] + self.url_list)
645 # make the progress bar pulse until process p (a Popen object) finishes
646 def pulse_until_process_exits(self, p):
647 while p.poll() is None:
649 self.progressbar.pulse()
652 # start over and download TBB again
653 def start_over(self):
654 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
655 self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
660 def refresh_gtk(self):
661 while gtk.events_pending():
662 gtk.main_iteration(False)
665 def delete_event(self, widget, event, data=None):
668 def destroy(self, widget, data=None):
669 if hasattr(self, 'file_download'):
670 self.file_download.close()
671 if hasattr(self, 'current_download_path'):
672 os.remove(self.current_download_path)
673 delattr(self, 'current_download_path')
674 delattr(self, 'current_download_url')