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',
152 'alexandre_key': '/usr/share/torbrowser-launcher/alexandre.asc'
155 'update_check': 'https://check.torproject.org/RecommendedTBBVersions'
161 def mkdir(self, path):
163 if os.path.exists(path) == False:
164 os.makedirs(path, 0700)
167 self.set_gui('error', _("Cannot create directory {0}").format(path), [], False)
169 if not os.access(path, os.W_OK):
170 self.set_gui('error', _("{0} is not writable").format(path), [], False)
174 # if gnupg_homedir isn't set up, set it up
175 def init_gnupg(self):
176 if not os.path.exists(self.paths['dir']['gnupg_homedir']):
177 print _('Creating GnuPG homedir'), self.paths['dir']['gnupg_homedir']
178 if self.mkdir(self.paths['dir']['gnupg_homedir']):
180 print _('Importing keys')
181 p1 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['erinn_key']])
183 p2 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['sebastian_key']])
185 p3 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['alexandre_key']])
189 def load_settings(self):
190 if os.path.isfile(self.paths['file']['settings']):
191 self.settings = pickle.load(open(self.paths['file']['settings']))
193 if not 'installed_version' in self.settings:
195 if not 'latest_version' in self.settings:
197 if not 'last_update_check_timestamp' in self.settings:
201 'installed_version': False,
202 'latest_version': '0',
203 'last_update_check_timestamp': 0
209 def save_settings(self):
210 pickle.dump(self.settings, open(self.paths['file']['settings'], 'w'))
213 # get the process id of a program
214 def get_pid(self, bin_path, python = False):
217 for p in psutil.process_iter():
219 if p.pid != os.getpid():
222 if len(p.cmdline) > 1:
223 if 'python' in p.cmdline[0]:
226 if len(p.cmdline) > 0:
237 # bring program's x window to front
238 def bring_window_to_front(self, pid):
239 # figure out the window id
241 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
242 for line in p.stdout.readlines():
243 line_split = line.split()
244 cur_win_id = line_split[0]
245 cur_win_pid = int(line_split[2])
246 if cur_win_pid == pid:
251 subprocess.call(['wmctrl', '-i', '-a', win_id])
254 def __init__(self, common):
255 print _('Starting settings dialog')
257 self.common.load_settings()
260 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
261 self.window.set_title(_("Tor Browser Launcher Settings"))
262 self.window.set_icon_from_file(self.common.paths['file']['icon'])
263 self.window.set_position(gtk.WIN_POS_CENTER)
264 self.window.set_border_width(10)
265 self.window.connect("delete_event", self.delete_event)
266 self.window.connect("destroy", self.destroy)
268 # build the rest of the UI
269 self.box = gtk.VBox(False, 20)
270 self.window.add(self.box)
273 if(self.common.settings['installed_version']):
274 self.label1 = gtk.Label(_('Installed version: {0}').format(self.common.settings['installed_version']))
276 self.label1 = gtk.Label(_('Tor Browser Bundle not installed'))
277 self.label1.set_line_wrap(True)
278 self.box.pack_start(self.label1, True, True, 0)
281 if(self.common.settings['last_update_check_timestamp'] > 0):
282 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']))))
284 self.label1 = gtk.Label(_('Never checked for updates'))
285 self.label1.set_line_wrap(True)
286 self.box.pack_start(self.label1, True, True, 0)
290 self.button_box = gtk.HButtonBox()
291 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
292 self.box.pack_start(self.button_box, True, True, 0)
293 self.button_box.show()
296 # save and launch button
297 save_launch_image = gtk.Image()
298 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
299 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
300 self.save_launch_button.set_image(save_launch_image)
301 self.save_launch_button.connect("clicked", self.save_launch, None)
302 self.button_box.add(self.save_launch_button)
303 self.save_launch_button.show()
305 # save and exit button
306 save_exit_image = gtk.Image()
307 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
308 self.save_exit_button = gtk.Button(_("Save & Exit"))
309 self.save_exit_button.set_image(save_exit_image)
310 self.save_exit_button.connect("clicked", self.save_exit, None)
311 self.button_box.add(self.save_exit_button)
312 self.save_exit_button.show()
315 cancel_image = gtk.Image()
316 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
317 self.cancel_button = gtk.Button(_("Cancel"))
318 self.cancel_button.set_image(cancel_image)
319 self.cancel_button.connect("clicked", self.destroy, None)
320 self.button_box.add(self.cancel_button)
321 self.cancel_button.show()
331 def save_launch(self, widget, data=None):
333 p = subprocess.Popen([self.common.paths['file']['tbl_bin']])
337 def save_exit(self, widget, data=None):
346 def delete_event(self, widget, event, data=None):
348 def destroy(self, widget, data=None):
353 def __init__(self, common):
354 print _('Starting launcher dialog')
357 self.set_gui(None, '', [])
358 self.launch_gui = True
360 # if we haven't already hit an error
361 if self.gui != 'error':
363 if self.common.load_settings():
364 self.common.build_paths(self.common.settings['latest_version'])
366 # is vidalia already running and we just need to open a new firefox?
367 if self.common.settings['installed_version']:
368 vidalia_pid = self.common.get_pid('./App/vidalia')
369 firefox_pid = self.common.get_pid(self.common.paths['file']['firefox_bin'])
371 if vidalia_pid and not firefox_pid:
372 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
373 self.common.bring_window_to_front(vidalia_pid)
374 subprocess.Popen([self.common.paths['file']['firefox_bin'], '-no-remote', '-profile', self.common.paths['file']['firefox_profile']])
376 elif vidalia_pid and firefox_pid:
377 print _('Vidalia and Firefox are already open, bringing them to focus')
379 # bring firefox to front, then vidalia
380 self.common.bring_window_to_front(firefox_pid)
381 self.common.bring_window_to_front(vidalia_pid)
384 # how long was it since the last update check?
385 # 86400 seconds = 24 hours
386 current_timestamp = int(time.time())
387 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
389 print 'Checking for update'
390 self.set_gui('task', _("Checking for Tor Browser update."),
391 ['download_update_check',
395 # no need to check for update
396 print _('Checked for update within 24 hours, skipping')
397 self.start_launcher()
400 self.set_gui('error', _("Error loading settings. Delete ~/.torbrowser and try again."), [])
404 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
405 self.window.set_title(_("Tor Browser"))
406 self.window.set_icon_from_file(self.common.paths['file']['icon'])
407 self.window.set_position(gtk.WIN_POS_CENTER)
408 self.window.set_border_width(10)
409 self.window.connect("delete_event", self.delete_event)
410 self.window.connect("destroy", self.destroy)
412 # build the rest of the UI
415 # download or run TBB
416 def start_launcher(self):
417 # is TBB already installed?
418 if os.path.isfile(self.common.paths['file']['start']) and os.access(self.common.paths['file']['start'], os.X_OK):
419 if self.common.settings['installed_version'] == self.common.settings['latest_version']:
420 # current version of tbb is installed, launch it
422 self.launch_gui = False
423 elif self.common.settings['installed_version'] < self.common.settings['latest_version']:
424 # there is a tbb upgrade available
425 self.set_gui('task', _("Your Tor Browser is out of date."),
427 'download_tarball_sig',
432 # for some reason the installed tbb is newer than the current version?
433 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
437 # are the tarball and sig already downloaded?
438 if os.path.isfile(self.common.paths['file']['tarball']) and os.path.isfile(self.common.paths['file']['tarball_sig']):
439 # start the gui with verify
440 self.set_gui('task', _("Installing Tor Browser."),
447 self.set_gui('task', _("Downloading and installing Tor Browser."),
449 'download_tarball_sig',
454 # there are different GUIs that might appear, this sets which one we want
455 def set_gui(self, gui, message, tasks, autostart=True):
457 self.gui_message = message
458 self.gui_tasks = tasks
460 self.gui_autostart = autostart
462 # set all gtk variables to False
464 if hasattr(self, 'box'):
469 self.progressbar = False
470 self.button_box = False
471 self.start_button = False
472 self.exit_button = False
474 # build the application's UI
476 self.box = gtk.VBox(False, 20)
477 self.window.add(self.box)
479 if self.gui == 'error':
481 self.label = gtk.Label( self.gui_message )
482 self.label.set_line_wrap(True)
483 self.box.pack_start(self.label, True, True, 0)
487 exit_image = gtk.Image()
488 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
489 self.exit_button = gtk.Button("Exit")
490 self.exit_button.set_image(exit_image)
491 self.exit_button.connect("clicked", self.destroy, None)
492 self.box.add(self.exit_button)
493 self.exit_button.show()
495 elif self.gui == 'task':
497 self.label = gtk.Label( self.gui_message )
498 self.label.set_line_wrap(True)
499 self.box.pack_start(self.label, True, True, 0)
503 self.progressbar = gtk.ProgressBar(adjustment=None)
504 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
505 self.progressbar.set_pulse_step(0.01)
506 self.box.pack_start(self.progressbar, True, True, 0)
509 self.button_box = gtk.HButtonBox()
510 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
511 self.box.pack_start(self.button_box, True, True, 0)
512 self.button_box.show()
515 start_image = gtk.Image()
516 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
517 self.start_button = gtk.Button(_("Start"))
518 self.start_button.set_image(start_image)
519 self.start_button.connect("clicked", self.start, None)
520 self.button_box.add(self.start_button)
521 if not self.gui_autostart:
522 self.start_button.show()
525 exit_image = gtk.Image()
526 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
527 self.exit_button = gtk.Button(_("Exit"))
528 self.exit_button.set_image(exit_image)
529 self.exit_button.connect("clicked", self.destroy, None)
530 self.button_box.add(self.exit_button)
531 self.exit_button.show()
536 if self.gui_autostart:
539 # start button clicked, begin tasks
540 def start(self, widget, data=None):
541 # disable the start button
542 if self.start_button:
543 self.start_button.set_sensitive(False)
545 # start running tasks
548 # run the next task in the task list
552 if self.gui_task_i >= len(self.gui_tasks):
556 task = self.gui_tasks[self.gui_task_i]
558 # get ready for the next task
561 if task == 'download_update_check':
562 print _('Downloading'), self.common.paths['url']['update_check']
563 self.download('update check', self.common.paths['url']['update_check'], self.common.paths['file']['update_check'])
565 if task == 'attempt_update':
566 print _('Checking to see if update it needed')
567 self.attempt_update()
569 elif task == 'download_tarball':
570 print _('Downloading'), self.common.paths['url']['tarball']
571 self.download('tarball', self.common.paths['url']['tarball'], self.common.paths['file']['tarball'])
573 elif task == 'download_tarball_sig':
574 print _('Downloading'), self.common.paths['url']['tarball_sig']
575 self.download('signature', self.common.paths['url']['tarball_sig'], self.common.paths['file']['tarball_sig'])
577 elif task == 'verify':
578 print _('Verifying signature')
581 elif task == 'extract':
582 print _('Extracting'), self.common.paths['filename']['tarball']
586 print _('Running'), self.common.paths['file']['start']
589 elif task == 'start_over':
590 print _('Starting download over again')
593 def response_received(self, response):
594 class FileDownloader(Protocol):
595 def __init__(self, file, total, progress, done_cb):
599 self.progress = progress
600 self.all_done = done_cb
602 def dataReceived(self, bytes):
603 self.file.write(bytes)
604 self.so_far += len(bytes)
605 percent = float(self.so_far) / float(self.total)
606 self.progress.set_fraction(percent)
607 amount = float(self.so_far)
609 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
612 amount = amount / float(size)
615 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
617 def connectionLost(self, reason):
618 print _('Finished receiving body:'), reason.getErrorMessage()
619 self.all_done(reason)
621 dl = FileDownloader(self.file_download, response.length, self.progressbar, self.response_finished)
622 response.deliverBody(dl)
624 def response_finished(self, msg):
625 if msg.check(ResponseDone):
626 self.file_download.close()
631 print "FINISHED", msg
632 ## FIXME handle errors
634 def download_error(self, f):
635 print _("Download error"), f
636 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
640 def download(self, name, url, path):
641 # initialize the progress bar
642 self.progressbar.set_fraction(0)
643 self.progressbar.set_text(_('Downloading {0}').format(name))
644 self.progressbar.show()
647 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['file']['torproject_pem']))
648 d = agent.request('GET', url,
649 Headers({'User-Agent': ['torbrowser-launcher']}),
652 self.file_download = open(path, 'w')
653 d.addCallback(self.response_received).addErrback(self.download_error)
655 if not reactor.running:
658 def attempt_update(self):
659 # load the update check file
661 versions = json.load(open(self.common.paths['file']['update_check']))
662 latest_version = None
665 for version in versions:
666 if str(version).find(end) != -1:
667 latest_version = str(version)
670 self.common.settings['latest_version'] = latest_version[:-len(end)]
671 self.common.settings['last_update_check_timestamp'] = int(time.time())
672 self.common.save_settings()
673 self.common.build_paths(self.common.settings['latest_version'])
674 self.start_launcher()
677 # failed to find the latest version
678 self.set_gui('error', _("Error checking for updates."), [], False)
681 # not a valid JSON object
682 self.set_gui('error', _("Error checking for updates."), [], False)
689 # initialize the progress bar
690 self.progressbar.set_fraction(0)
691 self.progressbar.set_text(_('Verifying Signature'))
692 self.progressbar.show()
694 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['dir']['gnupg_homedir'], '--verify', self.common.paths['file']['tarball_sig']])
695 self.pulse_until_process_exits(p)
697 if p.returncode == 0:
700 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)
704 if not reactor.running:
708 # initialize the progress bar
709 self.progressbar.set_fraction(0)
710 self.progressbar.set_text(_('Installing'))
711 self.progressbar.show()
714 # make sure this file is a tarfile
715 if tarfile.is_tarfile(self.common.paths['file']['tarball']):
716 tf = tarfile.open(self.common.paths['file']['tarball'])
717 tf.extractall(self.common.paths['dir']['tbb'])
719 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}"), ['start_over'], False)
723 # installation is finished, so save installed_version
724 self.common.settings['installed_version'] = self.common.settings['latest_version']
725 self.common.save_settings()
729 def run(self, run_next_task = True):
730 subprocess.Popen([self.common.paths['file']['start']])
734 # make the progress bar pulse until process p (a Popen object) finishes
735 def pulse_until_process_exits(self, p):
736 while p.poll() == None:
738 self.progressbar.pulse()
741 # start over and download TBB again
742 def start_over(self):
743 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
744 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
749 def refresh_gtk(self):
750 while gtk.events_pending():
751 gtk.main_iteration(False)
754 def delete_event(self, widget, event, data=None):
756 def destroy(self, widget, data=None):
757 if hasattr(self, 'file_download'):
758 self.file_download.close()
762 if __name__ == "__main__":
763 tor_browser_launcher_version = '0.0.1'
765 print _('Tor Browser Launcher')
766 print _('By Micah Lee, licensed under GPLv3')
767 print _('version {0}').format(tor_browser_launcher_version)
768 print 'https://github.com/micahflee/torbrowser-launcher'
772 # is torbrowser-launcher already running?
773 tbl_pid = common.get_pid(common.paths['file']['tbl_bin'], True)
775 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
776 common.bring_window_to_front(tbl_pid)
779 if '-settings' in sys.argv:
781 app = TBLSettings(common)
785 app = TBLLauncher(common)