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')
256 def __init__(self, common):
257 print _('Starting launcher dialog')
260 self.set_gui(None, '', [])
261 self.launch_gui = True
263 # if we haven't already hit an error
264 if self.gui != 'error':
266 if self.common.load_settings():
267 self.common.build_paths(self.common.settings['latest_version'])
269 # is vidalia already running and we just need to open a new firefox?
270 if self.common.settings['installed_version']:
271 vidalia_pid = self.common.get_pid('./App/vidalia')
272 firefox_pid = self.common.get_pid(self.common.paths['file']['firefox_bin'])
274 if vidalia_pid and not firefox_pid:
275 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
276 self.common.bring_window_to_front(vidalia_pid)
277 subprocess.Popen([self.common.paths['file']['firefox_bin'], '-no-remote', '-profile', self.common.paths['file']['firefox_profile']])
279 elif vidalia_pid and firefox_pid:
280 print _('Vidalia and Firefox are already open, bringing them to focus')
282 # bring firefox to front, then vidalia
283 self.common.bring_window_to_front(firefox_pid)
284 self.common.bring_window_to_front(vidalia_pid)
287 # how long was it since the last update check?
288 # 86400 seconds = 24 hours
289 current_timestamp = int(time.time())
290 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
292 print 'Checking for update'
293 self.set_gui('task', _("Checking for Tor Browser update."),
294 ['download_update_check',
298 # no need to check for update
299 print _('Checked for update within 24 hours, skipping')
300 self.start_launcher()
303 self.set_gui('error', _("Error loading settings. Delete ~/.torbrowser and try again."), [])
307 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
308 self.window.set_title(_("Tor Browser"))
309 self.window.set_icon_from_file(self.common.paths['file']['icon'])
310 self.window.set_position(gtk.WIN_POS_CENTER)
311 self.window.set_border_width(10)
312 self.window.connect("delete_event", self.delete_event)
313 self.window.connect("destroy", self.destroy)
315 # build the rest of the UI
318 # download or run TBB
319 def start_launcher(self):
320 # is TBB already installed?
321 if os.path.isfile(self.common.paths['file']['start']) and os.access(self.common.paths['file']['start'], os.X_OK):
322 if self.common.settings['installed_version'] == self.common.settings['latest_version']:
323 # current version of tbb is installed, launch it
325 self.launch_gui = False
326 elif self.common.settings['installed_version'] < self.common.settings['latest_version']:
327 # there is a tbb upgrade available
328 self.set_gui('task', _("Your Tor Browser is out of date."),
330 'download_tarball_sig',
335 # for some reason the installed tbb is newer than the current version?
336 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
340 # are the tarball and sig already downloaded?
341 if os.path.isfile(self.common.paths['file']['tarball']) and os.path.isfile(self.common.paths['file']['tarball_sig']):
342 # start the gui with verify
343 self.set_gui('task', _("Installing Tor Browser."),
350 self.set_gui('task', _("Downloading and installing Tor Browser."),
352 'download_tarball_sig',
357 # there are different GUIs that might appear, this sets which one we want
358 def set_gui(self, gui, message, tasks, autostart=True):
360 self.gui_message = message
361 self.gui_tasks = tasks
363 self.gui_autostart = autostart
365 # set all gtk variables to False
367 if hasattr(self, 'box'):
372 self.progressbar = False
373 self.button_box = False
374 self.start_button = False
375 self.exit_button = False
377 # build the application's UI
379 self.box = gtk.VBox(False, 20)
380 self.window.add(self.box)
382 if self.gui == 'error':
384 self.label = gtk.Label( self.gui_message )
385 self.label.set_line_wrap(True)
386 self.box.pack_start(self.label, True, True, 0)
390 exit_image = gtk.Image()
391 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
392 self.exit_button = gtk.Button("Exit")
393 self.exit_button.set_image(exit_image)
394 self.exit_button.connect("clicked", self.destroy, None)
395 self.box.add(self.exit_button)
396 self.exit_button.show()
398 elif self.gui == 'task':
400 self.label = gtk.Label( self.gui_message )
401 self.label.set_line_wrap(True)
402 self.box.pack_start(self.label, True, True, 0)
406 self.progressbar = gtk.ProgressBar(adjustment=None)
407 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
408 self.progressbar.set_pulse_step(0.01)
409 self.box.pack_start(self.progressbar, True, True, 0)
412 self.button_box = gtk.HButtonBox()
413 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
414 self.box.pack_start(self.button_box, True, True, 0)
415 self.button_box.show()
418 start_image = gtk.Image()
419 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
420 self.start_button = gtk.Button("Start")
421 self.start_button.set_image(start_image)
422 self.start_button.connect("clicked", self.start, None)
423 self.button_box.add(self.start_button)
424 if not self.gui_autostart:
425 self.start_button.show()
428 exit_image = gtk.Image()
429 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
430 self.exit_button = gtk.Button("Exit")
431 self.exit_button.set_image(exit_image)
432 self.exit_button.connect("clicked", self.destroy, None)
433 self.button_box.add(self.exit_button)
434 self.exit_button.show()
439 if self.gui_autostart:
442 # start button clicked, begin tasks
443 def start(self, widget, data=None):
444 # disable the start button
445 if self.start_button:
446 self.start_button.set_sensitive(False)
448 # start running tasks
451 # run the next task in the task list
455 if self.gui_task_i >= len(self.gui_tasks):
459 task = self.gui_tasks[self.gui_task_i]
461 # get ready for the next task
464 if task == 'download_update_check':
465 print _('Downloading'), self.common.paths['url']['update_check']
466 self.download('update check', self.common.paths['url']['update_check'], self.common.paths['file']['update_check'])
468 if task == 'attempt_update':
469 print _('Checking to see if update it needed')
470 self.attempt_update()
472 elif task == 'download_tarball':
473 print _('Downloading'), self.common.paths['url']['tarball']
474 self.download('tarball', self.common.paths['url']['tarball'], self.common.paths['file']['tarball'])
476 elif task == 'download_tarball_sig':
477 print _('Downloading'), self.common.paths['url']['tarball_sig']
478 self.download('signature', self.common.paths['url']['tarball_sig'], self.common.paths['file']['tarball_sig'])
480 elif task == 'verify':
481 print _('Verifying signature')
484 elif task == 'extract':
485 print _('Extracting'), self.common.paths['filename']['tarball']
489 print _('Running'), self.common.paths['file']['start']
492 elif task == 'start_over':
493 print _('Starting download over again')
496 def response_received(self, response):
497 class FileDownloader(Protocol):
498 def __init__(self, file, total, progress, done_cb):
502 self.progress = progress
503 self.all_done = done_cb
505 def dataReceived(self, bytes):
506 self.file.write(bytes)
507 self.so_far += len(bytes)
508 percent = float(self.so_far) / float(self.total)
509 self.progress.set_fraction(percent)
510 amount = float(self.so_far)
512 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
515 amount = amount / float(size)
518 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
520 def connectionLost(self, reason):
521 print _('Finished receiving body:'), reason.getErrorMessage()
522 self.all_done(reason)
524 dl = FileDownloader(self.file_download, response.length, self.progressbar, self.response_finished)
525 response.deliverBody(dl)
527 def response_finished(self, msg):
528 if msg.check(ResponseDone):
529 self.file_download.close()
534 print "FINISHED", msg
535 ## FIXME handle errors
537 def download_error(self, f):
538 print _("Download error"), f
539 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
543 def download(self, name, url, path):
544 # initialize the progress bar
545 self.progressbar.set_fraction(0)
546 self.progressbar.set_text(_('Downloading {0}').format(name))
547 self.progressbar.show()
550 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['file']['torproject_pem']))
551 d = agent.request('GET', url,
552 Headers({'User-Agent': ['torbrowser-launcher']}),
555 self.file_download = open(path, 'w')
556 d.addCallback(self.response_received).addErrback(self.download_error)
558 if not reactor.running:
561 def attempt_update(self):
562 # load the update check file
564 versions = json.load(open(self.common.paths['file']['update_check']))
565 latest_version = None
568 for version in versions:
569 if str(version).find(end) != -1:
570 latest_version = str(version)
573 self.common.settings['latest_version'] = latest_version[:-len(end)]
574 self.common.settings['last_update_check_timestamp'] = int(time.time())
575 self.common.save_settings()
576 self.common.build_paths(self.common.settings['latest_version'])
577 self.start_launcher()
580 # failed to find the latest version
581 self.set_gui('error', _("Error checking for updates."), [], False)
584 # not a valid JSON object
585 self.set_gui('error', _("Error checking for updates."), [], False)
592 # initialize the progress bar
593 self.progressbar.set_fraction(0)
594 self.progressbar.set_text(_('Verifying Signature'))
595 self.progressbar.show()
597 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['dir']['gnupg_homedir'], '--verify', self.common.paths['file']['tarball_sig']])
598 self.pulse_until_process_exits(p)
600 if p.returncode == 0:
603 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)
607 if not reactor.running:
611 # initialize the progress bar
612 self.progressbar.set_fraction(0)
613 self.progressbar.set_text(_('Installing'))
614 self.progressbar.show()
617 # make sure this file is a tarfile
618 if tarfile.is_tarfile(self.common.paths['file']['tarball']):
619 tf = tarfile.open(self.common.paths['file']['tarball'])
620 tf.extractall(self.common.paths['dir']['tbb'])
622 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}"), ['start_over'], False)
626 # installation is finished, so save installed_version
627 self.common.settings['installed_version'] = self.common.settings['latest_version']
628 self.common.save_settings()
632 def run(self, run_next_task = True):
633 subprocess.Popen([self.common.paths['file']['start']])
637 # make the progress bar pulse until process p (a Popen object) finishes
638 def pulse_until_process_exits(self, p):
639 while p.poll() == None:
641 self.progressbar.pulse()
644 # start over and download TBB again
645 def start_over(self):
646 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
647 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
652 def refresh_gtk(self):
653 while gtk.events_pending():
654 gtk.main_iteration(False)
657 def delete_event(self, widget, event, data=None):
659 def destroy(self, widget, data=None):
660 if hasattr(self, 'file_download'):
661 self.file_download.close()
665 if __name__ == "__main__":
666 tor_browser_launcher_version = '0.0.1'
668 print _('Tor Browser Launcher')
669 print _('By Micah Lee, licensed under GPLv3')
670 print _('version {0}').format(tor_browser_launcher_version)
671 print 'https://github.com/micahflee/torbrowser-launcher'
675 # is torbrowser-launcher already running?
676 tbl_pid = common.get_pid(common.paths['file']['tbl_bin'], True)
678 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
679 common.bring_window_to_front(tbl_pid)
682 if '-settings' in sys.argv:
684 app = TBLSettings(common)
688 app = TBLLauncher(common)