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
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.ssl import CertificateOptions
36 from twisted.internet._sslverify import ClientTLSOptions
37 from twisted.internet.error import DNSLookupError
38 from zope.interface import implementer
46 class TryStableException(Exception):
49 class TryDefaultMirrorException(Exception):
52 class DownloadErrorException(Exception):
55 class TorProjectCertificateOptions(CertificateOptions):
56 def __init__(self, torproject_pem):
57 CertificateOptions.__init__(self)
58 self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
60 def getContext(self, host, port):
61 ctx = CertificateOptions.getContext(self)
62 ctx.set_verify_depth(0)
63 ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
66 def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
67 return cert.digest('sha256') == self.torproject_ca.digest('sha256')
69 @implementer(IPolicyForHTTPS)
70 class TorProjectPolicyForHTTPS:
71 def __init__(self, torproject_pem):
72 self.torproject_pem = torproject_pem
74 def creatorForNetloc(self, hostname, port):
75 certificateOptions = TorProjectCertificateOptions(self.torproject_pem)
76 return ClientTLSOptions(hostname.decode('utf-8'),
77 certificateOptions.getContext(hostname, port))
80 def __init__(self, common, url_list):
82 self.url_list = url_list
85 self.set_gui(None, '', [])
86 self.launch_gui = True
87 self.common.build_paths(self.common.settings['latest_version'])
89 if self.common.settings['update_over_tor']:
92 print _('Updating over Tor')
94 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"))
95 md.set_position(gtk.WIN_POS_CENTER)
98 self.common.settings['update_over_tor'] = False
99 self.common.save_settings()
101 # is firefox already running?
102 if self.common.settings['installed_version']:
103 firefox_pid = self.common.get_pid('./Browser/firefox')
105 print _('Firefox is open, bringing to focus')
106 # bring firefox to front
107 self.common.bring_window_to_front(firefox_pid)
111 check_for_updates = False
112 if self.common.settings['check_for_updates']:
113 check_for_updates = True
115 if not check_for_updates:
116 # how long was it since the last update check?
117 # 86400 seconds = 24 hours
118 current_timestamp = int(time.time())
119 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
120 check_for_updates = True
122 if check_for_updates:
124 print 'Checking for update'
125 self.set_gui('task', _("Checking for Tor Browser update."),
126 ['download_update_check',
129 # no need to check for update
130 print _('Checked for update within 24 hours, skipping')
131 self.start_launcher()
135 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
136 self.window.set_title(_("Tor Browser"))
137 self.window.set_icon_from_file(self.common.paths['icon_file'])
138 self.window.set_position(gtk.WIN_POS_CENTER)
139 self.window.set_border_width(10)
140 self.window.connect("delete_event", self.delete_event)
141 self.window.connect("destroy", self.destroy)
143 # build the rest of the UI
146 # download or run TBB
147 def start_launcher(self):
148 # is TBB already installed?
149 latest_version = self.common.settings['latest_version']
150 installed_version = self.common.settings['installed_version']
152 # verify installed version for newer versions of TBB (#58)
153 if installed_version >= '3.0':
154 versions_filename = self.common.paths['tbb']['versions']
155 if os.path.exists(versions_filename):
156 for line in open(versions_filename):
157 if 'TORBROWSER_VERSION' in line:
158 installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
160 start = self.common.paths['tbb']['start']
161 if os.path.isfile(start) and os.access(start, os.X_OK):
162 if installed_version == latest_version:
163 print _('Latest version of TBB is installed, launching')
164 # current version of tbb is installed, launch it
166 self.launch_gui = False
167 elif installed_version < latest_version:
168 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
169 # there is a tbb upgrade available
170 self.set_gui('task', _("Your Tor Browser is out of date. Upgrading from {0} to {1}.".format(installed_version, latest_version)),
172 'download_sha256_sig',
178 # for some reason the installed tbb is newer than the current version?
179 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
183 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
184 self.set_gui('task', _("Downloading and installing Tor Browser for the first time."),
186 'download_sha256_sig',
192 # there are different GUIs that might appear, this sets which one we want
193 def set_gui(self, gui, message, tasks, autostart=True):
195 self.gui_message = message
196 self.gui_tasks = tasks
198 self.gui_autostart = autostart
200 # set all gtk variables to False
202 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
207 self.progressbar = False
208 self.button_box = False
209 self.start_button = False
210 self.exit_button = False
212 # build the application's UI
216 self.box = gtk.VBox(False, 20)
217 self.window.add(self.box)
219 if 'error' in self.gui:
221 self.label = gtk.Label(self.gui_message)
222 self.label.set_line_wrap(True)
223 self.box.pack_start(self.label, True, True, 0)
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()
232 if self.gui != 'error':
234 yes_image = gtk.Image()
235 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
236 self.yes_button = gtk.Button("Yes")
237 self.yes_button.set_image(yes_image)
238 if self.gui == 'error_try_stable':
239 self.yes_button.connect("clicked", self.try_stable, None)
240 elif self.gui == 'error_try_default_mirror':
241 self.yes_button.connect("clicked", self.try_default_mirror, None)
242 elif self.gui == 'error_try_tor':
243 self.yes_button.connect("clicked", self.try_tor, None)
244 self.button_box.add(self.yes_button)
245 self.yes_button.show()
248 exit_image = gtk.Image()
249 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
250 self.exit_button = gtk.Button("Exit")
251 self.exit_button.set_image(exit_image)
252 self.exit_button.connect("clicked", self.destroy, None)
253 self.button_box.add(self.exit_button)
254 self.exit_button.show()
256 elif self.gui == 'task':
258 self.label = gtk.Label(self.gui_message)
259 self.label.set_line_wrap(True)
260 self.box.pack_start(self.label, True, True, 0)
264 self.progressbar = gtk.ProgressBar(adjustment=None)
265 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
266 self.progressbar.set_pulse_step(0.01)
267 self.box.pack_start(self.progressbar, True, True, 0)
270 self.button_box = gtk.HButtonBox()
271 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
272 self.box.pack_start(self.button_box, True, True, 0)
273 self.button_box.show()
276 start_image = gtk.Image()
277 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
278 self.start_button = gtk.Button(_("Start"))
279 self.start_button.set_image(start_image)
280 self.start_button.connect("clicked", self.start, None)
281 self.button_box.add(self.start_button)
282 if not self.gui_autostart:
283 self.start_button.show()
286 exit_image = gtk.Image()
287 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
288 self.exit_button = gtk.Button(_("Exit"))
289 self.exit_button.set_image(exit_image)
290 self.exit_button.connect("clicked", self.destroy, None)
291 self.button_box.add(self.exit_button)
292 self.exit_button.show()
297 if self.gui_autostart:
300 # start button clicked, begin tasks
301 def start(self, widget, data=None):
302 # disable the start button
303 if self.start_button:
304 self.start_button.set_sensitive(False)
306 # start running tasks
309 # run the next task in the task list
313 if self.gui_task_i >= len(self.gui_tasks):
317 task = self.gui_tasks[self.gui_task_i]
319 # get ready for the next task
322 if task == 'download_update_check':
323 print _('Downloading'), self.common.paths['update_check_url']
324 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
326 if task == 'attempt_update':
327 print _('Checking to see if update is needed')
328 self.attempt_update()
330 elif task == 'download_sha256':
331 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
332 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
334 elif task == 'download_sha256_sig':
335 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
336 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
338 elif task == 'download_tarball':
339 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
340 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
342 elif task == 'verify':
343 print _('Verifying signature')
346 elif task == 'extract':
347 print _('Extracting'), self.common.paths['tarball_filename']
351 print _('Running'), self.common.paths['tbb']['start']
354 elif task == 'start_over':
355 print _('Starting download over again')
358 def response_received(self, response):
359 class FileDownloader(Protocol):
360 def __init__(self, common, file, url, total, progress, done_cb):
364 self.progress = progress
365 self.all_done = done_cb
367 if response.code != 200:
368 if common.settings['mirror'] != common.default_mirror:
369 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']))
371 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
373 def dataReceived(self, bytes):
374 self.file.write(bytes)
375 self.so_far += len(bytes)
376 percent = float(self.so_far) / float(self.total)
377 self.progress.set_fraction(percent)
378 amount = float(self.so_far)
380 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
383 amount = amount / float(size)
386 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
388 def connectionLost(self, reason):
389 self.all_done(reason)
391 if hasattr(self, 'current_download_url'):
392 url = self.current_download_url
396 dl = FileDownloader(self.common, self.file_download, url, response.length, self.progressbar, self.response_finished)
397 response.deliverBody(dl)
399 def response_finished(self, msg):
400 if msg.check(ResponseDone):
401 self.file_download.close()
402 delattr(self, 'current_download_path')
403 delattr(self, 'current_download_url')
409 print "FINISHED", msg
410 ## FIXME handle errors
412 def download_error(self, f):
413 print _("Download error:"), f.value, type(f.value)
415 if isinstance(f.value, TryStableException):
416 f.trap(TryStableException)
417 self.set_gui('error_try_stable', str(f.value), [], False)
419 elif isinstance(f.value, TryDefaultMirrorException):
420 f.trap(TryDefaultMirrorException)
421 self.set_gui('error_try_default_mirror', str(f.value), [], False)
423 elif isinstance(f.value, DownloadErrorException):
424 f.trap(DownloadErrorException)
425 self.set_gui('error', str(f.value), [], False)
427 elif isinstance(f.value, DNSLookupError):
428 f.trap(DNSLookupError)
429 if common.settings['mirror'] != common.default_mirror:
430 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)
432 self.set_gui('error', str(f.value), [], False)
434 elif isinstance(f.value, ResponseFailed):
435 for reason in f.value.reasons:
436 if isinstance(reason.value, OpenSSL.SSL.Error):
437 # TODO: add the ability to report attack by posting bug to trac.torproject.org
438 if not self.common.settings['update_over_tor']:
439 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)
441 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
444 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
448 def download(self, name, url, path):
449 # keep track of current download
450 self.current_download_path = path
451 self.current_download_url = url
453 # initialize the progress bar
454 mirror_url = url.format(self.common.settings['mirror'])
455 self.progressbar.set_fraction(0)
456 self.progressbar.set_text(_('Downloading {0}').format(name))
457 self.progressbar.show()
460 if self.common.settings['update_over_tor']:
461 from twisted.internet.endpoints import TCP4ClientEndpoint
462 from txsocksx.http import SOCKS5Agent
464 torEndpoint = TCP4ClientEndpoint(reactor, '127.0.0.1', 9050)
466 # default mirror gets certificate pinning, only for requests that use the mirror
467 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
468 agent = SOCKS5Agent(reactor, TorProjectPolicyForHTTPS(self.common.paths['torproject_pem']), proxyEndpoint=torEndpoint)
470 agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
472 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
473 agent = Agent(reactor, TorProjectPolicyForHTTPS(self.common.paths['torproject_pem']))
475 agent = Agent(reactor)
477 # actually, agent needs to follow redirect
478 agent = RedirectAgent(agent)
481 d = agent.request('GET', mirror_url,
482 Headers({'User-Agent': ['torbrowser-launcher']}),
485 self.file_download = open(path, 'w')
486 d.addCallback(self.response_received).addErrback(self.download_error)
488 if not reactor.running:
491 def try_default_mirror(self, widget, data=None):
492 # change mirror to default and relaunch TBL
493 self.common.settings['mirror'] = self.common.default_mirror
494 self.common.save_settings()
495 subprocess.Popen([self.common.paths['tbl_bin']])
498 def try_tor(self, widget, data=None):
499 # set update_over_tor to true and relaunch TBL
500 self.common.settings['update_over_tor'] = True
501 self.common.save_settings()
502 subprocess.Popen([self.common.paths['tbl_bin']])
505 def attempt_update(self):
506 # load the update check file
508 versions = json.load(open(self.common.paths['update_check_file']))
511 # filter linux versions
513 for version in versions:
514 if '-Linux' in version:
515 valid.append(str(version))
520 if len(versions) == 1:
521 latest = versions.pop()
524 # remove alphas/betas
525 for version in versions:
526 if not re.search(r'a\d-Linux', version) and not re.search(r'b\d-Linux', version):
527 stable.append(version)
529 latest = stable.pop()
531 latest = versions.pop()
535 if latest.endswith('-Linux'):
536 latest = latest.rstrip('-Linux')
538 self.common.settings['latest_version'] = latest
539 self.common.settings['last_update_check_timestamp'] = int(time.time())
540 self.common.settings['check_for_updates'] = False
541 self.common.save_settings()
542 self.common.build_paths(self.common.settings['latest_version'])
543 self.start_launcher()
546 # failed to find the latest version
547 self.set_gui('error', _("Error checking for updates."), [], False)
550 # not a valid JSON object
551 self.set_gui('error', _("Error checking for updates."), [], False)
558 # initialize the progress bar
559 self.progressbar.set_fraction(0)
560 self.progressbar.set_text(_('Verifying Signature'))
561 self.progressbar.show()
564 # check the sha256 file's sig, and also take the sha256 of the tarball and compare
565 FNULL = open(os.devnull, 'w')
566 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']], stdout=FNULL, stderr=subprocess.STDOUT)
567 self.pulse_until_process_exits(p)
568 if p.returncode == 0:
569 # compare with sha256 of the tarball
570 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
571 for line in open(self.common.paths['sha256_file'], 'r').readlines():
572 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
578 # TODO: add the ability to report attack by posting bug to trac.torproject.org
579 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)
583 if not reactor.running:
587 # initialize the progress bar
588 self.progressbar.set_fraction(0)
589 self.progressbar.set_text(_('Installing'))
590 self.progressbar.show()
595 if self.common.paths['tarball_file'][-2:] == 'xz':
596 # if tarball is .tar.xz
597 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
598 tf = tarfile.open(fileobj=xz)
599 tf.extractall(self.common.paths['tbb']['dir'])
602 # if tarball is .tar.gz
603 if tarfile.is_tarfile(self.common.paths['tarball_file']):
604 tf = tarfile.open(self.common.paths['tarball_file'])
605 tf.extractall(self.common.paths['tbb']['dir'])
611 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
616 # installation is finished, so save installed_version
617 self.common.settings['installed_version'] = self.common.settings['latest_version']
618 self.common.save_settings()
622 def run(self, run_next_task=True):
624 if self.common.settings['modem_sound']:
625 def play_modem_sound():
629 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
633 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."))
634 md.set_position(gtk.WIN_POS_CENTER)
638 t = threading.Thread(target=play_modem_sound)
641 # hide the TBL window (#151)
642 if hasattr(self, 'window'):
644 while gtk.events_pending():
645 gtk.main_iteration_do(True)
648 subprocess.call([self.common.paths['tbb']['start']], cwd=self.common.paths['tbb']['dir_tbb'])
653 # make the progress bar pulse until process p (a Popen object) finishes
654 def pulse_until_process_exits(self, p):
655 while p.poll() is None:
657 self.progressbar.pulse()
660 # start over and download TBB again
661 def start_over(self):
662 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
663 self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
668 def refresh_gtk(self):
669 while gtk.events_pending():
670 gtk.main_iteration(False)
673 def delete_event(self, widget, event, data=None):
676 def destroy(self, widget, data=None):
677 if hasattr(self, 'file_download'):
678 self.file_download.close()
679 if hasattr(self, 'current_download_path'):
680 os.remove(self.current_download_path)
681 delattr(self, 'current_download_path')
682 delattr(self, 'current_download_url')