4 https://github.com/micahflee/torbrowser-launcher/
6 Copyright (c) 2013 Micah Lee <micahflee@riseup.net>
8 Permission is hereby granted, free of charge, to any person
9 obtaining a copy of this software and associated documentation
10 files (the "Software"), to deal in the Software without
11 restriction, including without limitation the rights to use,
12 copy, modify, merge, publish, distribute, sublicense, and/or sell
13 copies of the Software, and to permit persons to whom the
14 Software is furnished to do so, subject to the following
17 The above copyright notice and this permission notice shall be
18 included in all copies or substantial portions of the Software.
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27 OTHER DEALINGS IN THE SOFTWARE.
31 gettext.install('torbrowser-launcher', '/usr/share/torbrowser-launcher/locale')
33 from twisted.internet import gtk2reactor
35 from twisted.internet import reactor
41 import os, sys, subprocess, locale, urllib2, gobject, time, pickle, json, tarfile, psutil
43 from twisted.web.client import Agent, ResponseDone
44 from twisted.web.http_headers import Headers
45 from twisted.internet.protocol import Protocol
46 from twisted.internet.ssl import ClientContextFactory
48 from OpenSSL.SSL import Context, VERIFY_PEER, VERIFY_FAIL_IF_NO_PEER_CERT
49 from OpenSSL.crypto import load_certificate, FILETYPE_PEM
51 class VerifyTorProjectCert(ClientContextFactory):
53 def __init__(self, torproject_pem):
54 self.torproject_ca = load_certificate(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(VERIFY_PEER | 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')
68 print _('Initializing Tor Browser Launcher')
71 self.discover_arch_lang()
73 self.mkdir(self.paths['dir']['download'])
74 self.mkdir(self.paths['dir']['tbb'])
77 # allow buttons to have icons
79 settings = gtk.settings_get_default()
80 settings.props.gtk_button_images = True
84 # discover the architecture and language
85 def discover_arch_lang(self):
86 # figure out the architecture
87 (sysname, nodename, release, version, machine) = os.uname()
88 self.architecture = machine
90 # figure out the language
91 available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
92 default_locale = locale.getdefaultlocale()[0]
93 if default_locale == None:
94 self.language = 'en-US'
96 self.language = default_locale.replace('_', '-')
97 if self.language not in available_languages:
98 self.language = self.language.split('-')[0]
99 if self.language not in available_languages:
100 for l in available_languages:
101 if l[0:2] == self.language:
103 # if language isn't available, default to english
104 if self.language not in available_languages:
105 self.language = 'en-US'
107 # build all relevant paths
108 def build_paths(self, tbb_version = None):
109 homedir = os.getenv('HOME')
111 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
112 if os.path.exists(homedir) == False:
114 os.mkdir(homedir, 0700)
116 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
117 if not os.access(homedir, os.W_OK):
118 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
120 tbb_data = '%s/.torbrowser' % homedir
123 tarball_filename = 'tor-browser-gnu-linux-'+self.architecture+'-'+tbb_version+'-dev-'+self.language+'.tar.gz'
124 self.paths['file']['tarball'] = tbb_data+'/download/'+tarball_filename
125 self.paths['file']['tarball_sig'] = tbb_data+'/download/'+tarball_filename+'.asc'
126 self.paths['url']['tarball'] = 'https://www.torproject.org/dist/torbrowser/linux/'+tarball_filename
127 self.paths['url']['tarball_sig'] = 'https://www.torproject.org/dist/torbrowser/linux/'+tarball_filename+'.asc'
128 self.paths['filename']['tarball'] = tarball_filename
129 self.paths['filename']['tarball_sig'] = tarball_filename+'.asc'
135 'download': tbb_data+'/download',
136 'tbb': tbb_data+'/tbb/'+self.architecture,
137 'gnupg_homedir': tbb_data+'/gnupg_homedir'
140 'tbl_bin': '/usr/bin/torbrowser-launcher',
141 'settings': tbb_data+'/settings',
142 'version': tbb_data+'/version',
143 'start': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
144 'vidalia_bin': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
145 'firefox_bin': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
146 'firefox_profile': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
147 'update_check': tbb_data+'/download/RecommendedTBBVersions',
148 'icon': '/usr/share/pixmaps/torbrowser80.xpm',
149 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
150 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
151 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc'
154 'update_check': 'https://check.torproject.org/RecommendedTBBVersions'
160 def mkdir(self, path):
162 if os.path.exists(path) == False:
163 os.makedirs(path, 0700)
166 self.set_gui('error', _("Cannot create directory {0}").format(path), [], False)
168 if not os.access(path, os.W_OK):
169 self.set_gui('error', _("{0} is not writable").format(path), [], False)
173 # if gnupg_homedir isn't set up, set it up
174 def init_gnupg(self):
175 if not os.path.exists(self.paths['dir']['gnupg_homedir']):
176 print _('Creating GnuPG homedir'), self.paths['dir']['gnupg_homedir']
177 if self.mkdir(self.paths['dir']['gnupg_homedir']):
179 print _('Importing keys')
180 p1 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['erinn_key']])
182 p2 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['sebastian_key']])
186 def load_settings(self):
187 if os.path.isfile(self.paths['file']['settings']):
188 self.settings = pickle.load(open(self.paths['file']['settings']))
190 if not 'installed_version' in self.settings:
192 if not 'latest_version' in self.settings:
194 if not 'last_update_check_timestamp' in self.settings:
198 'installed_version': False,
199 'latest_version': '0',
200 'last_update_check_timestamp': 0
206 def save_settings(self):
207 pickle.dump(self.settings, open(self.paths['file']['settings'], 'w'))
210 # get the process id of a program
211 def get_pid(self, bin_path, python = False):
214 for p in psutil.process_iter():
216 if p.pid != os.getpid():
219 if len(p.cmdline) > 1:
220 if 'python' in p.cmdline[0]:
223 if len(p.cmdline) > 0:
234 # bring program's x window to front
235 def bring_window_to_front(self, pid):
236 # figure out the window id
238 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
239 for line in p.stdout.readlines():
240 line_split = line.split()
241 cur_win_id = line_split[0]
242 cur_win_pid = int(line_split[2])
243 if cur_win_pid == pid:
248 subprocess.call(['wmctrl', '-i', '-a', win_id])
251 def __init__(self, common):
252 print _('Starting settings dialog')
254 self.common.load_settings()
257 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
258 self.window.set_title(_("Tor Browser Launcher Settings"))
259 self.window.set_icon_from_file(self.common.paths['file']['icon'])
260 self.window.set_position(gtk.WIN_POS_CENTER)
261 self.window.set_border_width(10)
262 self.window.connect("delete_event", self.delete_event)
263 self.window.connect("destroy", self.destroy)
265 # build the rest of the UI
266 self.box = gtk.VBox(False, 20)
267 self.window.add(self.box)
270 if(self.common.settings['installed_version']):
271 self.label1 = gtk.Label(_('Installed version: {0}').format(self.common.settings['installed_version']))
273 self.label1 = gtk.Label(_('Tor Browser Bundle not installed'))
274 self.label1.set_line_wrap(True)
275 self.box.pack_start(self.label1, True, True, 0)
278 if(self.common.settings['last_update_check_timestamp'] > 0):
279 self.label1 = gtk.Label(_('Last checked for updates: {0}').format(time.strftime("%B %d, %Y %I:%M %P", time.gmtime(self.common.settings['last_update_check_timestamp']))))
281 self.label1 = gtk.Label(_('Never checked for updates'))
282 self.label1.set_line_wrap(True)
283 self.box.pack_start(self.label1, True, True, 0)
287 self.button_box = gtk.HButtonBox()
288 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
289 self.box.pack_start(self.button_box, True, True, 0)
290 self.button_box.show()
293 # save and launch button
294 save_launch_image = gtk.Image()
295 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
296 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
297 self.save_launch_button.set_image(save_launch_image)
298 self.save_launch_button.connect("clicked", self.save_launch, None)
299 self.button_box.add(self.save_launch_button)
300 self.save_launch_button.show()
302 # save and exit button
303 save_exit_image = gtk.Image()
304 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
305 self.save_exit_button = gtk.Button(_("Save & Exit"))
306 self.save_exit_button.set_image(save_exit_image)
307 self.save_exit_button.connect("clicked", self.save_exit, None)
308 self.button_box.add(self.save_exit_button)
309 self.save_exit_button.show()
312 cancel_image = gtk.Image()
313 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
314 self.cancel_button = gtk.Button(_("Cancel"))
315 self.cancel_button.set_image(cancel_image)
316 self.cancel_button.connect("clicked", self.destroy, None)
317 self.button_box.add(self.cancel_button)
318 self.cancel_button.show()
328 def save_launch(self, widget, data=None):
330 p = subprocess.Popen([self.common.paths['file']['tbl_bin']])
334 def save_exit(self, widget, data=None):
343 def delete_event(self, widget, event, data=None):
345 def destroy(self, widget, data=None):
350 def __init__(self, common):
351 print _('Starting launcher dialog')
354 self.set_gui(None, '', [])
355 self.launch_gui = True
357 # if we haven't already hit an error
358 if self.gui != 'error':
360 if self.common.load_settings():
361 self.common.build_paths(self.common.settings['latest_version'])
363 # is vidalia already running and we just need to open a new firefox?
364 if self.common.settings['installed_version']:
365 vidalia_pid = self.common.get_pid('./App/vidalia')
366 firefox_pid = self.common.get_pid(self.common.paths['file']['firefox_bin'])
368 if vidalia_pid and not firefox_pid:
369 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
370 self.common.bring_window_to_front(vidalia_pid)
371 subprocess.Popen([self.common.paths['file']['firefox_bin'], '-no-remote', '-profile', self.common.paths['file']['firefox_profile']])
373 elif vidalia_pid and firefox_pid:
374 print _('Vidalia and Firefox are already open, bringing them to focus')
376 # bring firefox to front, then vidalia
377 self.common.bring_window_to_front(firefox_pid)
378 self.common.bring_window_to_front(vidalia_pid)
381 # how long was it since the last update check?
382 # 86400 seconds = 24 hours
383 current_timestamp = int(time.time())
384 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
386 print 'Checking for update'
387 self.set_gui('task', _("Checking for Tor Browser update."),
388 ['download_update_check',
392 # no need to check for update
393 print _('Checked for update within 24 hours, skipping')
394 self.start_launcher()
397 self.set_gui('error', _("Error loading settings. Delete ~/.torbrowser and try again."), [])
401 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
402 self.window.set_title(_("Tor Browser"))
403 self.window.set_icon_from_file(self.common.paths['file']['icon'])
404 self.window.set_position(gtk.WIN_POS_CENTER)
405 self.window.set_border_width(10)
406 self.window.connect("delete_event", self.delete_event)
407 self.window.connect("destroy", self.destroy)
409 # build the rest of the UI
412 # download or run TBB
413 def start_launcher(self):
414 # is TBB already installed?
415 if os.path.isfile(self.common.paths['file']['start']) and os.access(self.common.paths['file']['start'], os.X_OK):
416 if self.common.settings['installed_version'] == self.common.settings['latest_version']:
417 # current version of tbb is installed, launch it
419 self.launch_gui = False
420 elif self.common.settings['installed_version'] < self.common.settings['latest_version']:
421 # there is a tbb upgrade available
422 self.set_gui('task', _("Your Tor Browser is out of date."),
424 'download_tarball_sig',
429 # for some reason the installed tbb is newer than the current version?
430 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
434 # are the tarball and sig already downloaded?
435 if os.path.isfile(self.common.paths['file']['tarball']) and os.path.isfile(self.common.paths['file']['tarball_sig']):
436 # start the gui with verify
437 self.set_gui('task', _("Installing Tor Browser."),
444 self.set_gui('task', _("Downloading and installing Tor Browser."),
446 'download_tarball_sig',
451 # there are different GUIs that might appear, this sets which one we want
452 def set_gui(self, gui, message, tasks, autostart=True):
454 self.gui_message = message
455 self.gui_tasks = tasks
457 self.gui_autostart = autostart
459 # set all gtk variables to False
461 if hasattr(self, 'box'):
466 self.progressbar = False
467 self.button_box = False
468 self.start_button = False
469 self.exit_button = False
471 # build the application's UI
473 self.box = gtk.VBox(False, 20)
474 self.window.add(self.box)
476 if self.gui == 'error':
478 self.label = gtk.Label( self.gui_message )
479 self.label.set_line_wrap(True)
480 self.box.pack_start(self.label, True, True, 0)
484 exit_image = gtk.Image()
485 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
486 self.exit_button = gtk.Button("Exit")
487 self.exit_button.set_image(exit_image)
488 self.exit_button.connect("clicked", self.destroy, None)
489 self.box.add(self.exit_button)
490 self.exit_button.show()
492 elif self.gui == 'task':
494 self.label = gtk.Label( self.gui_message )
495 self.label.set_line_wrap(True)
496 self.box.pack_start(self.label, True, True, 0)
500 self.progressbar = gtk.ProgressBar(adjustment=None)
501 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
502 self.progressbar.set_pulse_step(0.01)
503 self.box.pack_start(self.progressbar, True, True, 0)
506 self.button_box = gtk.HButtonBox()
507 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
508 self.box.pack_start(self.button_box, True, True, 0)
509 self.button_box.show()
512 start_image = gtk.Image()
513 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
514 self.start_button = gtk.Button(_("Start"))
515 self.start_button.set_image(start_image)
516 self.start_button.connect("clicked", self.start, None)
517 self.button_box.add(self.start_button)
518 if not self.gui_autostart:
519 self.start_button.show()
522 exit_image = gtk.Image()
523 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
524 self.exit_button = gtk.Button(_("Exit"))
525 self.exit_button.set_image(exit_image)
526 self.exit_button.connect("clicked", self.destroy, None)
527 self.button_box.add(self.exit_button)
528 self.exit_button.show()
533 if self.gui_autostart:
536 # start button clicked, begin tasks
537 def start(self, widget, data=None):
538 # disable the start button
539 if self.start_button:
540 self.start_button.set_sensitive(False)
542 # start running tasks
545 # run the next task in the task list
549 if self.gui_task_i >= len(self.gui_tasks):
553 task = self.gui_tasks[self.gui_task_i]
555 # get ready for the next task
558 if task == 'download_update_check':
559 print _('Downloading'), self.common.paths['url']['update_check']
560 self.download('update check', self.common.paths['url']['update_check'], self.common.paths['file']['update_check'])
562 if task == 'attempt_update':
563 print _('Checking to see if update it needed')
564 self.attempt_update()
566 elif task == 'download_tarball':
567 print _('Downloading'), self.common.paths['url']['tarball']
568 self.download('tarball', self.common.paths['url']['tarball'], self.common.paths['file']['tarball'])
570 elif task == 'download_tarball_sig':
571 print _('Downloading'), self.common.paths['url']['tarball_sig']
572 self.download('signature', self.common.paths['url']['tarball_sig'], self.common.paths['file']['tarball_sig'])
574 elif task == 'verify':
575 print _('Verifying signature')
578 elif task == 'extract':
579 print _('Extracting'), self.common.paths['filename']['tarball']
583 print _('Running'), self.common.paths['file']['start']
586 elif task == 'start_over':
587 print _('Starting download over again')
590 def response_received(self, response):
591 class FileDownloader(Protocol):
592 def __init__(self, file, total, progress, done_cb):
596 self.progress = progress
597 self.all_done = done_cb
599 def dataReceived(self, bytes):
600 self.file.write(bytes)
601 self.so_far += len(bytes)
602 percent = float(self.so_far) / float(self.total)
603 self.progress.set_fraction(percent)
604 amount = float(self.so_far)
606 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
609 amount = amount / float(size)
612 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
614 def connectionLost(self, reason):
615 print _('Finished receiving body:'), reason.getErrorMessage()
616 self.all_done(reason)
618 dl = FileDownloader(self.file_download, response.length, self.progressbar, self.response_finished)
619 response.deliverBody(dl)
621 def response_finished(self, msg):
622 if msg.check(ResponseDone):
623 self.file_download.close()
628 print "FINISHED", msg
629 ## FIXME handle errors
631 def download_error(self, f):
632 print _("Download error"), f
633 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
637 def download(self, name, url, path):
638 # initialize the progress bar
639 self.progressbar.set_fraction(0)
640 self.progressbar.set_text(_('Downloading {0}').format(name))
641 self.progressbar.show()
644 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['file']['torproject_pem']))
645 d = agent.request('GET', url,
646 Headers({'User-Agent': ['torbrowser-launcher']}),
649 self.file_download = open(path, 'w')
650 d.addCallback(self.response_received).addErrback(self.download_error)
652 if not reactor.running:
655 def attempt_update(self):
656 # load the update check file
658 versions = json.load(open(self.common.paths['file']['update_check']))
659 latest_version = None
662 for version in versions:
663 if str(version).find(end) != -1:
664 latest_version = str(version)
667 self.common.settings['latest_version'] = latest_version[:-len(end)]
668 self.common.settings['last_update_check_timestamp'] = int(time.time())
669 self.common.save_settings()
670 self.common.build_paths(self.common.settings['latest_version'])
671 self.start_launcher()
674 # failed to find the latest version
675 self.set_gui('error', _("Error checking for updates."), [], False)
678 # not a valid JSON object
679 self.set_gui('error', _("Error checking for updates."), [], False)
686 # initialize the progress bar
687 self.progressbar.set_fraction(0)
688 self.progressbar.set_text(_('Verifying Signature'))
689 self.progressbar.show()
691 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['dir']['gnupg_homedir'], '--verify', self.common.paths['file']['tarball_sig']])
692 self.pulse_until_process_exits(p)
694 if p.returncode == 0:
697 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)
701 if not reactor.running:
705 # initialize the progress bar
706 self.progressbar.set_fraction(0)
707 self.progressbar.set_text(_('Installing'))
708 self.progressbar.show()
711 # make sure this file is a tarfile
712 if tarfile.is_tarfile(self.common.paths['file']['tarball']):
713 tf = tarfile.open(self.common.paths['file']['tarball'])
714 tf.extractall(self.common.paths['dir']['tbb'])
716 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}"), ['start_over'], False)
720 # installation is finished, so save installed_version
721 self.common.settings['installed_version'] = self.common.settings['latest_version']
722 self.common.save_settings()
726 def run(self, run_next_task = True):
727 subprocess.Popen([self.common.paths['file']['start']])
731 # make the progress bar pulse until process p (a Popen object) finishes
732 def pulse_until_process_exits(self, p):
733 while p.poll() == None:
735 self.progressbar.pulse()
738 # start over and download TBB again
739 def start_over(self):
740 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
741 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
746 def refresh_gtk(self):
747 while gtk.events_pending():
748 gtk.main_iteration(False)
751 def delete_event(self, widget, event, data=None):
753 def destroy(self, widget, data=None):
754 if hasattr(self, 'file_download'):
755 self.file_download.close()
759 if __name__ == "__main__":
760 tor_browser_launcher_version = '0.0.1'
762 print _('Tor Browser Launcher')
763 print _('By Micah Lee, licensed under GPLv3')
764 print _('version {0}').format(tor_browser_launcher_version)
765 print 'https://github.com/micahflee/torbrowser-launcher'
769 # is torbrowser-launcher already running?
770 tbl_pid = common.get_pid(common.paths['file']['tbl_bin'], True)
772 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
773 common.bring_window_to_front(tbl_pid)
776 if '-settings' in sys.argv:
778 app = TBLSettings(common)
782 app = TBLLauncher(common)