3 https://github.com/micahflee/torbrowser-launcher/
5 Copyright (c) 2013 Micah Lee
8 Redistribution and use in source and binary forms, with or without
9 modification, are permitted provided that the following conditions
11 1. Redistributions of source code must retain the above copyright
12 notice, this list of conditions and the following disclaimer.
13 2. Redistributions in binary form must reproduce the above copyright
14 notice, this list of conditions and the following disclaimer in the
15 documentation and/or other materials provided with the distribution.
16 3. Neither the name of the University nor the names of its contributors
17 may be used to endorse or promote products derived from this software
18 without specific prior written permission.
20 THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
21 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23 ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
24 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
26 OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
27 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
29 OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
36 gettext.install('torbrowser-launcher', '/usr/share/torbrowser-launcher/locale')
38 from twisted.internet import gtk2reactor
40 from twisted.internet import reactor
46 import os, sys, subprocess, locale, urllib2, gobject, time, pickle, json, tarfile, psutil
48 from twisted.web.client import Agent, ResponseDone
49 from twisted.web.http_headers import Headers
50 from twisted.internet.protocol import Protocol
51 from twisted.internet.ssl import ClientContextFactory
53 from OpenSSL.SSL import Context, VERIFY_PEER, VERIFY_FAIL_IF_NO_PEER_CERT
54 from OpenSSL.crypto import load_certificate, FILETYPE_PEM
56 class VerifyTorProjectCert(ClientContextFactory):
58 def __init__(self, torproject_pem):
59 self.torproject_ca = load_certificate(FILETYPE_PEM, open(torproject_pem, 'r').read())
61 def getContext(self, host, port):
62 ctx = ClientContextFactory.getContext(self)
63 ctx.set_verify_depth(0)
64 ctx.set_verify(VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
67 def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
68 return cert.digest('sha256') == self.torproject_ca.digest('sha256')
71 class TorBrowserLauncher:
74 self.set_gui(None, '', [])
75 self.discover_arch_lang()
77 self.mkdir(self.paths['dir']['download'])
78 self.mkdir(self.paths['dir']['tbb'])
81 # allow buttons to have icons
83 settings = gtk.settings_get_default()
84 settings.props.gtk_button_images = True
88 self.launch_gui = True
90 # if we haven't already hit an error
91 if self.gui != 'error':
93 if self.load_settings():
94 self.build_paths(self.settings['latest_version'])
96 # is tbb already running and we just need to open a new firefox?
97 if self.settings['installed_version']:
100 for p in psutil.process_iter():
104 # old versions of psutil don't have exe
105 if hasattr(p, 'exe'):
107 # need to rely on cmdline instead
109 if len(p.cmdline) > 0:
112 if exe == self.paths['file']['vidalia_bin'] or exe == './App/vidalia':
114 if exe == self.paths['file']['firefox_bin']:
120 if vidalia_pid and not firefox_pid:
121 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
122 subprocess.Popen([self.paths['file']['firefox_bin'], '-no-remote', '-profile', self.paths['file']['firefox_profile']])
124 elif vidalia_pid and firefox_pid:
125 print _('Vidalia and Firefox are already open, bringing them to focus')
127 # figure out the window ids of vidalia and firefox
128 vidalia_win_id = None
129 firefox_win_id = None
130 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
131 for line in p.stdout.readlines():
132 line_split = line.split()
133 win_id = line_split[0]
134 win_pid = int(line_split[2])
135 if win_pid == vidalia_pid:
136 vidalia_win_id = win_id
137 if win_pid == firefox_pid:
138 firefox_win_id = win_id
140 # bring firefox to front, then vidalia
142 subprocess.call(['wmctrl', '-i', '-a', firefox_win_id])
144 subprocess.call(['wmctrl', '-i', '-a', vidalia_win_id])
148 # how long was it since the last update check?
149 # 86400 seconds = 24 hours
150 current_timestamp = int(time.time())
151 if current_timestamp - self.settings['last_update_check_timestamp'] >= 86400:
153 print 'Checking for update'
154 self.set_gui('task', _("Checking for Tor Browser update."),
155 ['download_update_check',
159 # no need to check for update
160 print _('Checked for update within 24 hours, skipping')
161 self.start_launcher()
164 self.set_gui('error', _("Error loading settings. Delete ~/.torbrowser and try again."), [])
168 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
169 self.window.set_title(_("Tor Browser"))
170 self.window.set_icon_from_file(self.paths['file']['icon'])
171 self.window.set_position(gtk.WIN_POS_CENTER)
172 self.window.set_border_width(10)
173 self.window.connect("delete_event", self.delete_event)
174 self.window.connect("destroy", self.destroy)
176 # build the rest of the UI
179 # download or run TBB
180 def start_launcher(self):
181 # is TBB already installed?
182 if os.path.isfile(self.paths['file']['start']) and os.access(self.paths['file']['start'], os.X_OK):
183 if self.settings['installed_version'] == self.settings['latest_version']:
184 # current version of tbb is installed, launch it
186 self.launch_gui = False
187 elif self.settings['installed_version'] < self.settings['latest_version']:
188 # there is a tbb upgrade available
189 self.set_gui('task', _("Your Tor Browser is out of date."),
191 'download_tarball_sig',
196 # for some reason the installed tbb is newer than the current version?
197 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
201 # are the tarball and sig already downloaded?
202 if os.path.isfile(self.paths['file']['tarball']) and os.path.isfile(self.paths['file']['tarball_sig']):
203 # start the gui with verify
204 self.set_gui('task', _("Installing Tor Browser."),
211 self.set_gui('task', _("Downloading and installing Tor Browser."),
213 'download_tarball_sig',
218 # discover the architecture and language
219 def discover_arch_lang(self):
220 # figure out the architecture
221 (sysname, nodename, release, version, machine) = os.uname()
222 self.architecture = machine
224 # figure out the language
225 available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
226 default_locale = locale.getdefaultlocale()[0]
227 if default_locale == None:
228 self.language = 'en-US'
230 self.language = default_locale.replace('_', '-')
231 if self.language not in available_languages:
232 self.language = self.language.split('-')[0]
233 if self.language not in available_languages:
234 for l in available_languages:
235 if l[0:2] == self.language:
237 # if language isn't available, default to english
238 if self.language not in available_languages:
239 self.language = 'en-US'
241 # build all relevant paths
242 def build_paths(self, tbb_version = None):
243 homedir = os.getenv('HOME')
245 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
246 if os.path.exists(homedir) == False:
248 os.mkdir(homedir, 0700)
250 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
251 if not os.access(homedir, os.W_OK):
252 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
254 tbb_data = '%s/.torbrowser' % homedir
257 tarball_filename = 'tor-browser-gnu-linux-'+self.architecture+'-'+tbb_version+'-dev-'+self.language+'.tar.gz'
258 self.paths['file']['tarball'] = tbb_data+'/download/'+tarball_filename
259 self.paths['file']['tarball_sig'] = tbb_data+'/download/'+tarball_filename+'.asc'
260 self.paths['url']['tarball'] = 'https://www.torproject.org/dist/torbrowser/linux/'+tarball_filename
261 self.paths['url']['tarball_sig'] = 'https://www.torproject.org/dist/torbrowser/linux/'+tarball_filename+'.asc'
262 self.paths['filename']['tarball'] = tarball_filename
263 self.paths['filename']['tarball_sig'] = tarball_filename+'.asc'
269 'download': tbb_data+'/download',
270 'tbb': tbb_data+'/tbb/'+self.architecture,
271 'gnupg_homedir': tbb_data+'/gnupg_homedir'
274 'settings': tbb_data+'/settings',
275 'version': tbb_data+'/version',
276 'start': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
277 'vidalia_bin': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
278 'firefox_bin': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
279 'firefox_profile': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
280 'update_check': tbb_data+'/download/RecommendedTBBVersions',
281 'icon': '/usr/share/pixmaps/torbrowser80.xpm',
282 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
283 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
284 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc'
287 'update_check': 'https://check.torproject.org/RecommendedTBBVersions'
293 def mkdir(self, path):
295 if os.path.exists(path) == False:
296 os.makedirs(path, 0700)
299 self.set_gui('error', _("Cannot create directory {0}").format(path), [], False)
301 if not os.access(path, os.W_OK):
302 self.set_gui('error', _("{0} is not writable").format(path), [], False)
306 # if gnupg_homedir isn't set up, set it up
307 def init_gnupg(self):
308 if not os.path.exists(self.paths['dir']['gnupg_homedir']):
309 print _('Creating GnuPG homedir'), self.paths['dir']['gnupg_homedir']
310 if self.mkdir(self.paths['dir']['gnupg_homedir']):
312 print _('Importing keys')
313 p1 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['erinn_key']])
315 p2 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['sebastian_key']])
318 # there are different GUIs that might appear, this sets which one we want
319 def set_gui(self, gui, message, tasks, autostart=True):
321 self.gui_message = message
322 self.gui_tasks = tasks
324 self.gui_autostart = autostart
326 # set all gtk variables to False
328 if hasattr(self, 'box'):
333 self.progressbar = False
334 self.button_box = False
335 self.start_button = False
336 self.exit_button = False
338 # build the application's UI
340 self.box = gtk.VBox(False, 20)
341 self.window.add(self.box)
343 if self.gui == 'error':
345 self.label = gtk.Label( self.gui_message )
346 self.label.set_line_wrap(True)
347 self.box.pack_start(self.label, True, True, 0)
350 #self.label2 = gtk.Label("You can fix the problem by deleting:\n"+self.paths['dir']['data']+"\n\nHowever, you will lose all your bookmarks and other Tor Browser preferences.")
351 #self.label2.set_line_wrap(True)
352 #self.box.pack_start(self.label2, True, True, 0)
356 exit_image = gtk.Image()
357 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
358 self.exit_button = gtk.Button("Exit")
359 self.exit_button.set_image(exit_image)
360 self.exit_button.connect("clicked", self.destroy, None)
361 self.box.add(self.exit_button)
362 self.exit_button.show()
364 elif self.gui == 'task':
366 self.label = gtk.Label( self.gui_message )
367 self.label.set_line_wrap(True)
368 self.box.pack_start(self.label, True, True, 0)
372 self.progressbar = gtk.ProgressBar(adjustment=None)
373 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
374 self.progressbar.set_pulse_step(0.01)
375 self.box.pack_start(self.progressbar, True, True, 0)
378 self.button_box = gtk.HButtonBox()
379 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
380 self.box.pack_start(self.button_box, True, True, 0)
381 self.button_box.show()
384 start_image = gtk.Image()
385 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
386 self.start_button = gtk.Button("Start")
387 self.start_button.set_image(start_image)
388 self.start_button.connect("clicked", self.start, None)
389 self.button_box.add(self.start_button)
390 if not self.gui_autostart:
391 self.start_button.show()
394 exit_image = gtk.Image()
395 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
396 self.exit_button = gtk.Button("Exit")
397 self.exit_button.set_image(exit_image)
398 self.exit_button.connect("clicked", self.destroy, None)
399 self.button_box.add(self.exit_button)
400 self.exit_button.show()
405 if self.gui_autostart:
408 # start button clicked, begin tasks
409 def start(self, widget, data=None):
410 # disable the start button
411 if self.start_button:
412 self.start_button.set_sensitive(False)
414 # start running tasks
417 # run the next task in the task list
421 if self.gui_task_i >= len(self.gui_tasks):
425 task = self.gui_tasks[self.gui_task_i]
427 # get ready for the next task
430 if task == 'download_update_check':
431 print _('Downloading'), self.paths['url']['update_check']
432 self.download('update check', self.paths['url']['update_check'], self.paths['file']['update_check'])
434 if task == 'attempt_update':
435 print _('Checking to see if update it needed')
436 self.attempt_update()
438 elif task == 'download_tarball':
439 print _('Downloading'), self.paths['url']['tarball']
440 self.download('tarball', self.paths['url']['tarball'], self.paths['file']['tarball'])
442 elif task == 'download_tarball_sig':
443 print _('Downloading'), self.paths['url']['tarball_sig']
444 self.download('signature', self.paths['url']['tarball_sig'], self.paths['file']['tarball_sig'])
446 elif task == 'verify':
447 print _('Verifying signature')
450 elif task == 'extract':
451 print _('Extracting'), self.paths['filename']['tarball']
455 print _('Running'), self.paths['file']['start']
458 elif task == 'start_over':
459 print _('Starting download over again')
462 def response_received(self, response):
463 class FileDownloader(Protocol):
464 def __init__(self, file, total, progress, done_cb):
468 self.progress = progress
469 self.all_done = done_cb
471 def dataReceived(self, bytes):
472 self.file.write(bytes)
473 self.so_far += len(bytes)
474 percent = float(self.so_far) / float(self.total)
475 self.progress.set_fraction(percent)
476 amount = float(self.so_far)
478 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
481 amount = amount / float(size)
484 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
486 def connectionLost(self, reason):
487 print _('Finished receiving body:'), reason.getErrorMessage()
488 self.all_done(reason)
490 dl = FileDownloader(self.file_download, response.length, self.progressbar, self.response_finished)
491 response.deliverBody(dl)
493 def response_finished(self, msg):
494 if msg.check(ResponseDone):
495 self.file_download.close()
500 print "FINISHED", msg
501 ## FIXME handle errors
503 def download_error(self, f):
504 print _("Download error"), f
505 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
509 def download(self, name, url, path):
510 # initialize the progress bar
511 self.progressbar.set_fraction(0)
512 self.progressbar.set_text(_('Downloading {0}').format(name))
513 self.progressbar.show()
516 agent = Agent(reactor, VerifyTorProjectCert(self.paths['file']['torproject_pem']))
517 d = agent.request('GET', url,
518 Headers({'User-Agent': ['torbrowser-launcher']}),
521 self.file_download = open(path, 'w')
522 d.addCallback(self.response_received).addErrback(self.download_error)
524 if not reactor.running:
527 def attempt_update(self):
528 # load the update check file
530 versions = json.load(open(self.paths['file']['update_check']))
531 latest_version = None
534 for version in versions:
535 if str(version).find(end) != -1:
536 latest_version = str(version)
539 self.settings['latest_version'] = latest_version[:-len(end)]
540 self.settings['last_update_check_timestamp'] = int(time.time())
542 self.build_paths(self.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()
563 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--verify', self.paths['file']['tarball_sig']])
564 self.pulse_until_process_exits(p)
566 if p.returncode == 0:
569 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)
573 if not reactor.running:
577 # initialize the progress bar
578 self.progressbar.set_fraction(0)
579 self.progressbar.set_text(_('Installing'))
580 self.progressbar.show()
583 # make sure this file is a tarfile
584 if tarfile.is_tarfile(self.paths['file']['tarball']):
585 tf = tarfile.open(self.paths['file']['tarball'])
586 tf.extractall(self.paths['dir']['tbb'])
588 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}"), ['start_over'], False)
592 # installation is finished, so save installed_version
593 self.settings['installed_version'] = self.settings['latest_version']
598 def run(self, run_next_task = True):
599 subprocess.Popen([self.paths['file']['start']])
603 # make the progress bar pulse until process p (a Popen object) finishes
604 def pulse_until_process_exits(self, p):
605 while p.poll() == None:
607 self.progressbar.pulse()
610 # start over and download TBB again
611 def start_over(self):
612 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
613 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
618 def load_settings(self):
619 if os.path.isfile(self.paths['file']['settings']):
620 self.settings = pickle.load(open(self.paths['file']['settings']))
622 if not 'installed_version' in self.settings:
624 if not 'latest_version' in self.settings:
626 if not 'last_update_check_timestamp' in self.settings:
630 'installed_version': False,
631 'latest_version': '0',
632 'last_update_check_timestamp': 0
638 def save_settings(self):
639 pickle.dump(self.settings, open(self.paths['file']['settings'], 'w'))
643 def refresh_gtk(self):
644 while gtk.events_pending():
645 gtk.main_iteration(False)
648 def delete_event(self, widget, event, data=None):
650 def destroy(self, widget, data=None):
651 if hasattr(self, 'file_download'):
652 self.file_download.close()
656 if __name__ == "__main__":
657 tor_browser_launcher_version = '0.0.1'
659 print _('Tor Browser Launcher')
660 print _('By Micah Lee, licensed under GPLv3')
661 print _('version {0}').format(tor_browser_launcher_version)
662 print 'https://github.com/micahflee/torbrowser-launcher'
664 app = TorBrowserLauncher()