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
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):
67 print _('Starting launcher dialog')
71 self.set_gui(None, '', [])
72 self.launch_gui = True
73 print "LATEST VERSION", self.common.settings['latest_version']
74 self.common.build_paths(self.common.settings['latest_version'])
76 if self.common.settings['update_over_tor']:
80 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"))
81 md.set_position(gtk.WIN_POS_CENTER)
84 self.common.settings['update_over_tor'] = False
85 self.common.save_settings()
87 # is firefox already running?
88 if self.common.settings['installed_version']:
89 firefox_pid = self.common.get_pid('./Browser/firefox')
91 print _('Firefox is open, bringing to focus')
92 # bring firefox to front
93 self.common.bring_window_to_front(firefox_pid)
97 check_for_updates = False
98 if self.common.settings['check_for_updates']:
99 check_for_updates = True
101 if not check_for_updates:
102 # how long was it since the last update check?
103 # 86400 seconds = 24 hours
104 current_timestamp = int(time.time())
105 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
106 check_for_updates = True
108 if check_for_updates:
110 print 'Checking for update'
111 self.set_gui('task', _("Checking for Tor Browser update."),
112 ['download_update_check',
115 # no need to check for update
116 print _('Checked for update within 24 hours, skipping')
117 self.start_launcher()
121 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
122 self.window.set_title(_("Tor Browser"))
123 self.window.set_icon_from_file(self.common.paths['icon_file'])
124 self.window.set_position(gtk.WIN_POS_CENTER)
125 self.window.set_border_width(10)
126 self.window.connect("delete_event", self.delete_event)
127 self.window.connect("destroy", self.destroy)
129 # build the rest of the UI
132 # download or run TBB
133 def start_launcher(self):
134 # is TBB already installed?
135 latest_version = self.common.settings['latest_version']
136 installed_version = self.common.settings['installed_version']
138 # verify installed version for newer versions of TBB (#58)
139 if installed_version >= '3.0':
140 versions_filename = self.common.paths['tbb']['versions']
141 if os.path.exists(versions_filename):
142 for line in open(versions_filename):
143 if 'TORBROWSER_VERSION' in line:
144 installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
146 start = self.common.paths['tbb']['start']
147 if os.path.isfile(start) and os.access(start, os.X_OK):
148 if installed_version == latest_version:
149 print _('Latest version of TBB is installed, launching')
150 # current version of tbb is installed, launch it
152 self.launch_gui = False
153 elif installed_version < latest_version:
154 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
155 # there is a tbb upgrade available
156 self.set_gui('task', _("Your Tor Browser is out of date."),
158 'download_sha256_sig',
164 # for some reason the installed tbb is newer than the current version?
165 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
169 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
170 self.set_gui('task', _("Downloading and installing Tor Browser."),
172 'download_sha256_sig',
178 # there are different GUIs that might appear, this sets which one we want
179 def set_gui(self, gui, message, tasks, autostart=True):
181 self.gui_message = message
182 self.gui_tasks = tasks
184 self.gui_autostart = autostart
186 # set all gtk variables to False
188 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
193 self.progressbar = False
194 self.button_box = False
195 self.start_button = False
196 self.exit_button = False
198 # build the application's UI
202 self.box = gtk.VBox(False, 20)
203 self.window.add(self.box)
205 if 'error' in self.gui:
207 self.label = gtk.Label(self.gui_message)
208 self.label.set_line_wrap(True)
209 self.box.pack_start(self.label, True, True, 0)
213 self.button_box = gtk.HButtonBox()
214 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
215 self.box.pack_start(self.button_box, True, True, 0)
216 self.button_box.show()
218 if self.gui != 'error':
220 yes_image = gtk.Image()
221 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
222 self.yes_button = gtk.Button("Yes")
223 self.yes_button.set_image(yes_image)
224 if self.gui == 'error_try_stable':
225 self.yes_button.connect("clicked", self.try_stable, None)
226 elif self.gui == 'error_try_default_mirror':
227 self.yes_button.connect("clicked", self.try_default_mirror, None)
228 elif self.gui == 'error_try_tor':
229 self.yes_button.connect("clicked", self.try_tor, None)
230 self.button_box.add(self.yes_button)
231 self.yes_button.show()
234 exit_image = gtk.Image()
235 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
236 self.exit_button = gtk.Button("Exit")
237 self.exit_button.set_image(exit_image)
238 self.exit_button.connect("clicked", self.destroy, None)
239 self.button_box.add(self.exit_button)
240 self.exit_button.show()
242 elif self.gui == 'task':
244 self.label = gtk.Label(self.gui_message)
245 self.label.set_line_wrap(True)
246 self.box.pack_start(self.label, True, True, 0)
250 self.progressbar = gtk.ProgressBar(adjustment=None)
251 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
252 self.progressbar.set_pulse_step(0.01)
253 self.box.pack_start(self.progressbar, True, True, 0)
256 self.button_box = gtk.HButtonBox()
257 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
258 self.box.pack_start(self.button_box, True, True, 0)
259 self.button_box.show()
262 start_image = gtk.Image()
263 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
264 self.start_button = gtk.Button(_("Start"))
265 self.start_button.set_image(start_image)
266 self.start_button.connect("clicked", self.start, None)
267 self.button_box.add(self.start_button)
268 if not self.gui_autostart:
269 self.start_button.show()
272 exit_image = gtk.Image()
273 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
274 self.exit_button = gtk.Button(_("Exit"))
275 self.exit_button.set_image(exit_image)
276 self.exit_button.connect("clicked", self.destroy, None)
277 self.button_box.add(self.exit_button)
278 self.exit_button.show()
283 if self.gui_autostart:
286 # start button clicked, begin tasks
287 def start(self, widget, data=None):
288 # disable the start button
289 if self.start_button:
290 self.start_button.set_sensitive(False)
292 # start running tasks
295 # run the next task in the task list
299 if self.gui_task_i >= len(self.gui_tasks):
303 task = self.gui_tasks[self.gui_task_i]
305 # get ready for the next task
308 print _('Running task: {0}'.format(task))
309 if task == 'download_update_check':
310 print _('Downloading'), self.common.paths['update_check_url']
311 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
313 if task == 'attempt_update':
314 print _('Checking to see if update is needed')
315 self.attempt_update()
317 elif task == 'download_sha256':
318 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
319 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
321 elif task == 'download_sha256_sig':
322 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
323 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
325 elif task == 'download_tarball':
326 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
327 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
329 elif task == 'verify':
330 print _('Verifying signature')
333 elif task == 'extract':
334 print _('Extracting'), self.common.paths['tarball_filename']
338 print _('Running'), self.common.paths['tbb']['start']
341 elif task == 'start_over':
342 print _('Starting download over again')
345 def response_received(self, response):
346 class FileDownloader(Protocol):
347 def __init__(self, common, file, url, total, progress, done_cb):
351 self.progress = progress
352 self.all_done = done_cb
354 if response.code != 200:
355 if common.settings['mirror'] != common.default_mirror:
356 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']))
358 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
360 def dataReceived(self, bytes):
361 self.file.write(bytes)
362 self.so_far += len(bytes)
363 percent = float(self.so_far) / float(self.total)
364 self.progress.set_fraction(percent)
365 amount = float(self.so_far)
367 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
370 amount = amount / float(size)
373 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
375 def connectionLost(self, reason):
376 print _('Finished receiving body:'), reason.getErrorMessage()
377 self.all_done(reason)
379 if hasattr(self, 'current_download_url'):
380 url = self.current_download_url
384 dl = FileDownloader(self.common, self.file_download, url, response.length, self.progressbar, self.response_finished)
385 response.deliverBody(dl)
387 def response_finished(self, msg):
388 if msg.check(ResponseDone):
389 self.file_download.close()
390 delattr(self, 'current_download_path')
391 delattr(self, 'current_download_url')
397 print "FINISHED", msg
398 ## FIXME handle errors
400 def download_error(self, f):
401 print _("Download error:"), f.value, type(f.value)
403 if isinstance(f.value, TryStableException):
404 f.trap(TryStableException)
405 self.set_gui('error_try_stable', str(f.value), [], False)
407 elif isinstance(f.value, TryDefaultMirrorException):
408 f.trap(TryDefaultMirrorException)
409 self.set_gui('error_try_default_mirror', str(f.value), [], False)
411 elif isinstance(f.value, DownloadErrorException):
412 f.trap(DownloadErrorException)
413 self.set_gui('error', str(f.value), [], False)
415 elif isinstance(f.value, DNSLookupError):
416 f.trap(DNSLookupError)
417 if common.settings['mirror'] != common.default_mirror:
418 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)
420 self.set_gui('error', str(f.value), [], False)
422 elif isinstance(f.value, ResponseFailed):
423 for reason in f.value.reasons:
424 if isinstance(reason.value, OpenSSL.SSL.Error):
425 # TODO: add the ability to report attack by posting bug to trac.torproject.org
426 if not self.common.settings['update_over_tor']:
427 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)
429 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
432 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
436 def download(self, name, url, path):
437 # keep track of current download
438 self.current_download_path = path
439 self.current_download_url = url
441 # initialize the progress bar
442 mirror_url = url.format(self.common.settings['mirror'])
443 self.progressbar.set_fraction(0)
444 self.progressbar.set_text(_('Downloading {0}').format(name))
445 self.progressbar.show()
448 if self.common.settings['update_over_tor']:
449 print _('Updating over Tor')
450 from twisted.internet.endpoints import TCP4ClientEndpoint
451 from txsocksx.http import SOCKS5Agent
453 torEndpoint = TCP4ClientEndpoint(reactor, '127.0.0.1', 9050)
455 # default mirror gets certificate pinning, only for requests that use the mirror
456 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
457 agent = SOCKS5Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']), proxyEndpoint=torEndpoint)
459 agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
461 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
462 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
464 agent = Agent(reactor)
466 # actually, agent needs to follow redirect
467 agent = RedirectAgent(agent)
470 d = agent.request('GET', mirror_url,
471 Headers({'User-Agent': ['torbrowser-launcher']}),
474 self.file_download = open(path, 'w')
475 d.addCallback(self.response_received).addErrback(self.download_error)
477 if not reactor.running:
480 def try_default_mirror(self, widget, data=None):
481 # change mirror to default and relaunch TBL
482 self.common.settings['mirror'] = self.common.default_mirror
483 self.common.save_settings()
484 subprocess.Popen([self.common.paths['tbl_bin']])
487 def try_tor(self, widget, data=None):
488 # set update_over_tor to true and relaunch TBL
489 self.common.settings['update_over_tor'] = True
490 self.common.save_settings()
491 subprocess.Popen([self.common.paths['tbl_bin']])
494 def attempt_update(self):
495 # load the update check file
497 versions = json.load(open(self.common.paths['update_check_file']))
500 # filter linux versions
502 for version in versions:
503 if '-Linux' in version:
504 valid.append(str(version))
511 # remove alphas/betas
512 for version in valid:
513 if '-alpha-' not in version and '-beta-' not in version:
514 stable.append(version)
516 latest = stable.pop()
521 self.common.settings['latest_version'] = latest[:-len('-Linux')]
522 self.common.settings['last_update_check_timestamp'] = int(time.time())
523 self.common.settings['check_for_updates'] = False
524 self.common.save_settings()
525 self.common.build_paths(self.common.settings['latest_version'])
526 self.start_launcher()
529 # failed to find the latest version
530 self.set_gui('error', _("Error checking for updates."), [], False)
533 # not a valid JSON object
534 self.set_gui('error', _("Error checking for updates."), [], False)
541 # initialize the progress bar
542 self.progressbar.set_fraction(0)
543 self.progressbar.set_text(_('Verifying Signature'))
544 self.progressbar.show()
547 # check the sha256 file's sig, and also take the sha256 of the tarball and compare
548 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
549 self.pulse_until_process_exits(p)
550 if p.returncode == 0:
551 # compare with sha256 of the tarball
552 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
553 for line in open(self.common.paths['sha256_file'], 'r').readlines():
554 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
560 # TODO: add the ability to report attack by posting bug to trac.torproject.org
561 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)
565 if not reactor.running:
569 # initialize the progress bar
570 self.progressbar.set_fraction(0)
571 self.progressbar.set_text(_('Installing'))
572 self.progressbar.show()
577 if self.common.paths['tarball_file'][-2:] == 'xz':
578 # if tarball is .tar.xz
579 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
580 tf = tarfile.open(fileobj=xz)
581 tf.extractall(self.common.paths['tbb']['dir'])
584 # if tarball is .tar.gz
585 if tarfile.is_tarfile(self.common.paths['tarball_file']):
586 tf = tarfile.open(self.common.paths['tarball_file'])
587 tf.extractall(self.common.paths['tbb']['dir'])
593 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 # installation is finished, so save installed_version
599 self.common.settings['installed_version'] = self.common.settings['latest_version']
600 self.common.save_settings()
604 def run(self, run_next_task=True):
605 devnull = open('/dev/null', 'w')
606 subprocess.Popen([self.common.paths['tbb']['start']], stdout=devnull, stderr=devnull)
609 if self.common.settings['modem_sound']:
613 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
617 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."))
618 md.set_position(gtk.WIN_POS_CENTER)
625 # make the progress bar pulse until process p (a Popen object) finishes
626 def pulse_until_process_exits(self, p):
627 while p.poll() is None:
629 self.progressbar.pulse()
632 # start over and download TBB again
633 def start_over(self):
634 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
635 self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
640 def refresh_gtk(self):
641 while gtk.events_pending():
642 gtk.main_iteration(False)
645 def delete_event(self, widget, event, data=None):
648 def destroy(self, widget, data=None):
649 if hasattr(self, 'file_download'):
650 self.file_download.close()
651 if hasattr(self, 'current_download_path'):
652 os.remove(self.current_download_path)
653 delattr(self, 'current_download_path')
654 delattr(self, 'current_download_url')