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')
66 class TorBrowserLauncher:
69 self.set_gui(None, '', [])
70 self.discover_arch_lang()
72 self.mkdir(self.paths['dir']['download'])
73 self.mkdir(self.paths['dir']['tbb'])
76 # allow buttons to have icons
78 settings = gtk.settings_get_default()
79 settings.props.gtk_button_images = True
83 self.launch_gui = True
85 # if we haven't already hit an error
86 if self.gui != 'error':
88 if self.load_settings():
89 self.build_paths(self.settings['latest_version'])
91 # is tbb already running and we just need to open a new firefox?
92 if self.settings['installed_version']:
95 for p in psutil.process_iter():
99 # old versions of psutil don't have exe
100 if hasattr(p, 'exe'):
102 # need to rely on cmdline instead
104 if len(p.cmdline) > 0:
107 if exe == self.paths['file']['vidalia_bin'] or exe == './App/vidalia':
109 if exe == self.paths['file']['firefox_bin']:
115 if vidalia_pid and not firefox_pid:
116 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
117 subprocess.Popen([self.paths['file']['firefox_bin'], '-no-remote', '-profile', self.paths['file']['firefox_profile']])
119 elif vidalia_pid and firefox_pid:
120 print _('Vidalia and Firefox are already open, bringing them to focus')
122 # figure out the window ids of vidalia and firefox
123 vidalia_win_id = None
124 firefox_win_id = None
125 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
126 for line in p.stdout.readlines():
127 line_split = line.split()
128 win_id = line_split[0]
129 win_pid = int(line_split[2])
130 if win_pid == vidalia_pid:
131 vidalia_win_id = win_id
132 if win_pid == firefox_pid:
133 firefox_win_id = win_id
135 # bring firefox to front, then vidalia
137 subprocess.call(['wmctrl', '-i', '-a', firefox_win_id])
139 subprocess.call(['wmctrl', '-i', '-a', vidalia_win_id])
143 # how long was it since the last update check?
144 # 86400 seconds = 24 hours
145 current_timestamp = int(time.time())
146 if current_timestamp - self.settings['last_update_check_timestamp'] >= 86400:
148 print 'Checking for update'
149 self.set_gui('task', _("Checking for Tor Browser update."),
150 ['download_update_check',
154 # no need to check for update
155 print _('Checked for update within 24 hours, skipping')
156 self.start_launcher()
159 self.set_gui('error', _("Error loading settings. Delete ~/.torbrowser and try again."), [])
163 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
164 self.window.set_title(_("Tor Browser"))
165 self.window.set_icon_from_file(self.paths['file']['icon'])
166 self.window.set_position(gtk.WIN_POS_CENTER)
167 self.window.set_border_width(10)
168 self.window.connect("delete_event", self.delete_event)
169 self.window.connect("destroy", self.destroy)
171 # build the rest of the UI
174 # download or run TBB
175 def start_launcher(self):
176 # is TBB already installed?
177 if os.path.isfile(self.paths['file']['start']) and os.access(self.paths['file']['start'], os.X_OK):
178 if self.settings['installed_version'] == self.settings['latest_version']:
179 # current version of tbb is installed, launch it
181 self.launch_gui = False
182 elif self.settings['installed_version'] < self.settings['latest_version']:
183 # there is a tbb upgrade available
184 self.set_gui('task', _("Your Tor Browser is out of date."),
186 'download_tarball_sig',
191 # for some reason the installed tbb is newer than the current version?
192 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
196 # are the tarball and sig already downloaded?
197 if os.path.isfile(self.paths['file']['tarball']) and os.path.isfile(self.paths['file']['tarball_sig']):
198 # start the gui with verify
199 self.set_gui('task', _("Installing Tor Browser."),
206 self.set_gui('task', _("Downloading and installing Tor Browser."),
208 'download_tarball_sig',
213 # discover the architecture and language
214 def discover_arch_lang(self):
215 # figure out the architecture
216 (sysname, nodename, release, version, machine) = os.uname()
217 self.architecture = machine
219 # figure out the language
220 available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
221 default_locale = locale.getdefaultlocale()[0]
222 if default_locale == None:
223 self.language = 'en-US'
225 self.language = default_locale.replace('_', '-')
226 if self.language not in available_languages:
227 self.language = self.language.split('-')[0]
228 if self.language not in available_languages:
229 for l in available_languages:
230 if l[0:2] == self.language:
232 # if language isn't available, default to english
233 if self.language not in available_languages:
234 self.language = 'en-US'
236 # build all relevant paths
237 def build_paths(self, tbb_version = None):
238 homedir = os.getenv('HOME')
240 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
241 if os.path.exists(homedir) == False:
243 os.mkdir(homedir, 0700)
245 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
246 if not os.access(homedir, os.W_OK):
247 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
249 tbb_data = '%s/.torbrowser' % homedir
252 tarball_filename = 'tor-browser-gnu-linux-'+self.architecture+'-'+tbb_version+'-dev-'+self.language+'.tar.gz'
253 self.paths['file']['tarball'] = tbb_data+'/download/'+tarball_filename
254 self.paths['file']['tarball_sig'] = tbb_data+'/download/'+tarball_filename+'.asc'
255 self.paths['url']['tarball'] = 'https://www.torproject.org/dist/torbrowser/linux/'+tarball_filename
256 self.paths['url']['tarball_sig'] = 'https://www.torproject.org/dist/torbrowser/linux/'+tarball_filename+'.asc'
257 self.paths['filename']['tarball'] = tarball_filename
258 self.paths['filename']['tarball_sig'] = tarball_filename+'.asc'
264 'download': tbb_data+'/download',
265 'tbb': tbb_data+'/tbb/'+self.architecture,
266 'gnupg_homedir': tbb_data+'/gnupg_homedir'
269 'settings': tbb_data+'/settings',
270 'version': tbb_data+'/version',
271 'start': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
272 'vidalia_bin': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
273 'firefox_bin': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
274 'firefox_profile': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
275 'update_check': tbb_data+'/download/RecommendedTBBVersions',
276 'icon': '/usr/share/pixmaps/torbrowser80.xpm',
277 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
278 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
279 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc'
282 'update_check': 'https://check.torproject.org/RecommendedTBBVersions'
288 def mkdir(self, path):
290 if os.path.exists(path) == False:
291 os.makedirs(path, 0700)
294 self.set_gui('error', _("Cannot create directory {0}").format(path), [], False)
296 if not os.access(path, os.W_OK):
297 self.set_gui('error', _("{0} is not writable").format(path), [], False)
301 # if gnupg_homedir isn't set up, set it up
302 def init_gnupg(self):
303 if not os.path.exists(self.paths['dir']['gnupg_homedir']):
304 print _('Creating GnuPG homedir'), self.paths['dir']['gnupg_homedir']
305 if self.mkdir(self.paths['dir']['gnupg_homedir']):
307 print _('Importing keys')
308 p1 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['erinn_key']])
310 p2 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['sebastian_key']])
313 # there are different GUIs that might appear, this sets which one we want
314 def set_gui(self, gui, message, tasks, autostart=True):
316 self.gui_message = message
317 self.gui_tasks = tasks
319 self.gui_autostart = autostart
321 # set all gtk variables to False
323 if hasattr(self, 'box'):
328 self.progressbar = False
329 self.button_box = False
330 self.start_button = False
331 self.exit_button = False
333 # build the application's UI
335 self.box = gtk.VBox(False, 20)
336 self.window.add(self.box)
338 if self.gui == 'error':
340 self.label = gtk.Label( self.gui_message )
341 self.label.set_line_wrap(True)
342 self.box.pack_start(self.label, True, True, 0)
345 #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.")
346 #self.label2.set_line_wrap(True)
347 #self.box.pack_start(self.label2, True, True, 0)
351 exit_image = gtk.Image()
352 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
353 self.exit_button = gtk.Button("Exit")
354 self.exit_button.set_image(exit_image)
355 self.exit_button.connect("clicked", self.destroy, None)
356 self.box.add(self.exit_button)
357 self.exit_button.show()
359 elif self.gui == 'task':
361 self.label = gtk.Label( self.gui_message )
362 self.label.set_line_wrap(True)
363 self.box.pack_start(self.label, True, True, 0)
367 self.progressbar = gtk.ProgressBar(adjustment=None)
368 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
369 self.progressbar.set_pulse_step(0.01)
370 self.box.pack_start(self.progressbar, True, True, 0)
373 self.button_box = gtk.HButtonBox()
374 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
375 self.box.pack_start(self.button_box, True, True, 0)
376 self.button_box.show()
379 start_image = gtk.Image()
380 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
381 self.start_button = gtk.Button("Start")
382 self.start_button.set_image(start_image)
383 self.start_button.connect("clicked", self.start, None)
384 self.button_box.add(self.start_button)
385 if not self.gui_autostart:
386 self.start_button.show()
389 exit_image = gtk.Image()
390 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
391 self.exit_button = gtk.Button("Exit")
392 self.exit_button.set_image(exit_image)
393 self.exit_button.connect("clicked", self.destroy, None)
394 self.button_box.add(self.exit_button)
395 self.exit_button.show()
400 if self.gui_autostart:
403 # start button clicked, begin tasks
404 def start(self, widget, data=None):
405 # disable the start button
406 if self.start_button:
407 self.start_button.set_sensitive(False)
409 # start running tasks
412 # run the next task in the task list
416 if self.gui_task_i >= len(self.gui_tasks):
420 task = self.gui_tasks[self.gui_task_i]
422 # get ready for the next task
425 if task == 'download_update_check':
426 print _('Downloading'), self.paths['url']['update_check']
427 self.download('update check', self.paths['url']['update_check'], self.paths['file']['update_check'])
429 if task == 'attempt_update':
430 print _('Checking to see if update it needed')
431 self.attempt_update()
433 elif task == 'download_tarball':
434 print _('Downloading'), self.paths['url']['tarball']
435 self.download('tarball', self.paths['url']['tarball'], self.paths['file']['tarball'])
437 elif task == 'download_tarball_sig':
438 print _('Downloading'), self.paths['url']['tarball_sig']
439 self.download('signature', self.paths['url']['tarball_sig'], self.paths['file']['tarball_sig'])
441 elif task == 'verify':
442 print _('Verifying signature')
445 elif task == 'extract':
446 print _('Extracting'), self.paths['filename']['tarball']
450 print _('Running'), self.paths['file']['start']
453 elif task == 'start_over':
454 print _('Starting download over again')
457 def response_received(self, response):
458 class FileDownloader(Protocol):
459 def __init__(self, file, total, progress, done_cb):
463 self.progress = progress
464 self.all_done = done_cb
466 def dataReceived(self, bytes):
467 self.file.write(bytes)
468 self.so_far += len(bytes)
469 percent = float(self.so_far) / float(self.total)
470 self.progress.set_fraction(percent)
471 amount = float(self.so_far)
473 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
476 amount = amount / float(size)
479 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
481 def connectionLost(self, reason):
482 print _('Finished receiving body:'), reason.getErrorMessage()
483 self.all_done(reason)
485 dl = FileDownloader(self.file_download, response.length, self.progressbar, self.response_finished)
486 response.deliverBody(dl)
488 def response_finished(self, msg):
489 if msg.check(ResponseDone):
490 self.file_download.close()
495 print "FINISHED", msg
496 ## FIXME handle errors
498 def download_error(self, f):
499 print _("Download error"), f
500 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
504 def download(self, name, url, path):
505 # initialize the progress bar
506 self.progressbar.set_fraction(0)
507 self.progressbar.set_text(_('Downloading {0}').format(name))
508 self.progressbar.show()
511 agent = Agent(reactor, VerifyTorProjectCert(self.paths['file']['torproject_pem']))
512 d = agent.request('GET', url,
513 Headers({'User-Agent': ['torbrowser-launcher']}),
516 self.file_download = open(path, 'w')
517 d.addCallback(self.response_received).addErrback(self.download_error)
519 if not reactor.running:
522 def attempt_update(self):
523 # load the update check file
525 versions = json.load(open(self.paths['file']['update_check']))
526 latest_version = None
529 for version in versions:
530 if str(version).find(end) != -1:
531 latest_version = str(version)
534 self.settings['latest_version'] = latest_version[:-len(end)]
535 self.settings['last_update_check_timestamp'] = int(time.time())
537 self.build_paths(self.settings['latest_version'])
538 self.start_launcher()
541 # failed to find the latest version
542 self.set_gui('error', _("Error checking for updates."), [], False)
545 # not a valid JSON object
546 self.set_gui('error', _("Error checking for updates."), [], False)
553 # initialize the progress bar
554 self.progressbar.set_fraction(0)
555 self.progressbar.set_text(_('Verifying Signature'))
556 self.progressbar.show()
558 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--verify', self.paths['file']['tarball_sig']])
559 self.pulse_until_process_exits(p)
561 if p.returncode == 0:
564 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)
568 if not reactor.running:
572 # initialize the progress bar
573 self.progressbar.set_fraction(0)
574 self.progressbar.set_text(_('Installing'))
575 self.progressbar.show()
578 # make sure this file is a tarfile
579 if tarfile.is_tarfile(self.paths['file']['tarball']):
580 tf = tarfile.open(self.paths['file']['tarball'])
581 tf.extractall(self.paths['dir']['tbb'])
583 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}"), ['start_over'], False)
587 # installation is finished, so save installed_version
588 self.settings['installed_version'] = self.settings['latest_version']
593 def run(self, run_next_task = True):
594 subprocess.Popen([self.paths['file']['start']])
598 # make the progress bar pulse until process p (a Popen object) finishes
599 def pulse_until_process_exits(self, p):
600 while p.poll() == None:
602 self.progressbar.pulse()
605 # start over and download TBB again
606 def start_over(self):
607 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
608 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
613 def load_settings(self):
614 if os.path.isfile(self.paths['file']['settings']):
615 self.settings = pickle.load(open(self.paths['file']['settings']))
617 if not 'installed_version' in self.settings:
619 if not 'latest_version' in self.settings:
621 if not 'last_update_check_timestamp' in self.settings:
625 'installed_version': False,
626 'latest_version': '0',
627 'last_update_check_timestamp': 0
633 def save_settings(self):
634 pickle.dump(self.settings, open(self.paths['file']['settings'], 'w'))
638 def refresh_gtk(self):
639 while gtk.events_pending():
640 gtk.main_iteration(False)
643 def delete_event(self, widget, event, data=None):
645 def destroy(self, widget, data=None):
646 if hasattr(self, 'file_download'):
647 self.file_download.close()
651 if __name__ == "__main__":
652 tor_browser_launcher_version = '0.0.1'
654 print _('Tor Browser Launcher')
655 print _('By Micah Lee, licensed under GPLv3')
656 print _('version {0}').format(tor_browser_launcher_version)
657 print 'https://github.com/micahflee/torbrowser-launcher'
659 app = TorBrowserLauncher()