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 # is torbrowser-launcher already running?
86 tbl_pid = self.get_pid(self.paths['file']['tbl_bin'], True)
88 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
89 self.bring_window_to_front(tbl_pid)
92 # if we haven't already hit an error
93 if self.gui != 'error':
95 if self.load_settings():
96 self.build_paths(self.settings['latest_version'])
98 # is vidalia already running and we just need to open a new firefox?
99 if self.settings['installed_version']:
100 vidalia_pid = self.get_pid('./App/vidalia')
101 firefox_pid = self.get_pid(self.paths['file']['firefox_bin'])
103 if vidalia_pid and not firefox_pid:
104 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
105 self.bring_window_to_front(vidalia_pid)
106 subprocess.Popen([self.paths['file']['firefox_bin'], '-no-remote', '-profile', self.paths['file']['firefox_profile']])
108 elif vidalia_pid and firefox_pid:
109 print _('Vidalia and Firefox are already open, bringing them to focus')
111 # bring firefox to front, then vidalia
112 self.bring_window_to_front(firefox_pid)
113 self.bring_window_to_front(vidalia_pid)
116 # how long was it since the last update check?
117 # 86400 seconds = 24 hours
118 current_timestamp = int(time.time())
119 if current_timestamp - self.settings['last_update_check_timestamp'] >= 86400:
121 print 'Checking for update'
122 self.set_gui('task', _("Checking for Tor Browser update."),
123 ['download_update_check',
127 # no need to check for update
128 print _('Checked for update within 24 hours, skipping')
129 self.start_launcher()
132 self.set_gui('error', _("Error loading settings. Delete ~/.torbrowser and try again."), [])
136 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
137 self.window.set_title(_("Tor Browser"))
138 self.window.set_icon_from_file(self.paths['file']['icon'])
139 self.window.set_position(gtk.WIN_POS_CENTER)
140 self.window.set_border_width(10)
141 self.window.connect("delete_event", self.delete_event)
142 self.window.connect("destroy", self.destroy)
144 # build the rest of the UI
147 # download or run TBB
148 def start_launcher(self):
149 # is TBB already installed?
150 if os.path.isfile(self.paths['file']['start']) and os.access(self.paths['file']['start'], os.X_OK):
151 if self.settings['installed_version'] == self.settings['latest_version']:
152 # current version of tbb is installed, launch it
154 self.launch_gui = False
155 elif self.settings['installed_version'] < self.settings['latest_version']:
156 # there is a tbb upgrade available
157 self.set_gui('task', _("Your Tor Browser is out of date."),
159 'download_tarball_sig',
164 # for some reason the installed tbb is newer than the current version?
165 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
169 # are the tarball and sig already downloaded?
170 if os.path.isfile(self.paths['file']['tarball']) and os.path.isfile(self.paths['file']['tarball_sig']):
171 # start the gui with verify
172 self.set_gui('task', _("Installing Tor Browser."),
179 self.set_gui('task', _("Downloading and installing Tor Browser."),
181 'download_tarball_sig',
186 # discover the architecture and language
187 def discover_arch_lang(self):
188 # figure out the architecture
189 (sysname, nodename, release, version, machine) = os.uname()
190 self.architecture = machine
192 # figure out the language
193 available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
194 default_locale = locale.getdefaultlocale()[0]
195 if default_locale == None:
196 self.language = 'en-US'
198 self.language = default_locale.replace('_', '-')
199 if self.language not in available_languages:
200 self.language = self.language.split('-')[0]
201 if self.language not in available_languages:
202 for l in available_languages:
203 if l[0:2] == self.language:
205 # if language isn't available, default to english
206 if self.language not in available_languages:
207 self.language = 'en-US'
209 # build all relevant paths
210 def build_paths(self, tbb_version = None):
211 homedir = os.getenv('HOME')
213 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
214 if os.path.exists(homedir) == False:
216 os.mkdir(homedir, 0700)
218 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
219 if not os.access(homedir, os.W_OK):
220 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
222 tbb_data = '%s/.torbrowser' % homedir
225 tarball_filename = 'tor-browser-gnu-linux-'+self.architecture+'-'+tbb_version+'-dev-'+self.language+'.tar.gz'
226 self.paths['file']['tarball'] = tbb_data+'/download/'+tarball_filename
227 self.paths['file']['tarball_sig'] = tbb_data+'/download/'+tarball_filename+'.asc'
228 self.paths['url']['tarball'] = 'https://www.torproject.org/dist/torbrowser/linux/'+tarball_filename
229 self.paths['url']['tarball_sig'] = 'https://www.torproject.org/dist/torbrowser/linux/'+tarball_filename+'.asc'
230 self.paths['filename']['tarball'] = tarball_filename
231 self.paths['filename']['tarball_sig'] = tarball_filename+'.asc'
237 'download': tbb_data+'/download',
238 'tbb': tbb_data+'/tbb/'+self.architecture,
239 'gnupg_homedir': tbb_data+'/gnupg_homedir'
242 'tbl_bin': '/usr/bin/torbrowser-launcher',
243 'settings': tbb_data+'/settings',
244 'version': tbb_data+'/version',
245 'start': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
246 'vidalia_bin': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
247 'firefox_bin': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
248 'firefox_profile': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
249 'update_check': tbb_data+'/download/RecommendedTBBVersions',
250 'icon': '/usr/share/pixmaps/torbrowser80.xpm',
251 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
252 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
253 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc'
256 'update_check': 'https://check.torproject.org/RecommendedTBBVersions'
262 def mkdir(self, path):
264 if os.path.exists(path) == False:
265 os.makedirs(path, 0700)
268 self.set_gui('error', _("Cannot create directory {0}").format(path), [], False)
270 if not os.access(path, os.W_OK):
271 self.set_gui('error', _("{0} is not writable").format(path), [], False)
275 # if gnupg_homedir isn't set up, set it up
276 def init_gnupg(self):
277 if not os.path.exists(self.paths['dir']['gnupg_homedir']):
278 print _('Creating GnuPG homedir'), self.paths['dir']['gnupg_homedir']
279 if self.mkdir(self.paths['dir']['gnupg_homedir']):
281 print _('Importing keys')
282 p1 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['erinn_key']])
284 p2 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['sebastian_key']])
287 # there are different GUIs that might appear, this sets which one we want
288 def set_gui(self, gui, message, tasks, autostart=True):
290 self.gui_message = message
291 self.gui_tasks = tasks
293 self.gui_autostart = autostart
295 # set all gtk variables to False
297 if hasattr(self, 'box'):
302 self.progressbar = False
303 self.button_box = False
304 self.start_button = False
305 self.exit_button = False
307 # build the application's UI
309 self.box = gtk.VBox(False, 20)
310 self.window.add(self.box)
312 if self.gui == 'error':
314 self.label = gtk.Label( self.gui_message )
315 self.label.set_line_wrap(True)
316 self.box.pack_start(self.label, True, True, 0)
319 #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.")
320 #self.label2.set_line_wrap(True)
321 #self.box.pack_start(self.label2, True, True, 0)
325 exit_image = gtk.Image()
326 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
327 self.exit_button = gtk.Button("Exit")
328 self.exit_button.set_image(exit_image)
329 self.exit_button.connect("clicked", self.destroy, None)
330 self.box.add(self.exit_button)
331 self.exit_button.show()
333 elif self.gui == 'task':
335 self.label = gtk.Label( self.gui_message )
336 self.label.set_line_wrap(True)
337 self.box.pack_start(self.label, True, True, 0)
341 self.progressbar = gtk.ProgressBar(adjustment=None)
342 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
343 self.progressbar.set_pulse_step(0.01)
344 self.box.pack_start(self.progressbar, True, True, 0)
347 self.button_box = gtk.HButtonBox()
348 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
349 self.box.pack_start(self.button_box, True, True, 0)
350 self.button_box.show()
353 start_image = gtk.Image()
354 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
355 self.start_button = gtk.Button("Start")
356 self.start_button.set_image(start_image)
357 self.start_button.connect("clicked", self.start, None)
358 self.button_box.add(self.start_button)
359 if not self.gui_autostart:
360 self.start_button.show()
363 exit_image = gtk.Image()
364 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
365 self.exit_button = gtk.Button("Exit")
366 self.exit_button.set_image(exit_image)
367 self.exit_button.connect("clicked", self.destroy, None)
368 self.button_box.add(self.exit_button)
369 self.exit_button.show()
374 if self.gui_autostart:
377 # start button clicked, begin tasks
378 def start(self, widget, data=None):
379 # disable the start button
380 if self.start_button:
381 self.start_button.set_sensitive(False)
383 # start running tasks
386 # run the next task in the task list
390 if self.gui_task_i >= len(self.gui_tasks):
394 task = self.gui_tasks[self.gui_task_i]
396 # get ready for the next task
399 if task == 'download_update_check':
400 print _('Downloading'), self.paths['url']['update_check']
401 self.download('update check', self.paths['url']['update_check'], self.paths['file']['update_check'])
403 if task == 'attempt_update':
404 print _('Checking to see if update it needed')
405 self.attempt_update()
407 elif task == 'download_tarball':
408 print _('Downloading'), self.paths['url']['tarball']
409 self.download('tarball', self.paths['url']['tarball'], self.paths['file']['tarball'])
411 elif task == 'download_tarball_sig':
412 print _('Downloading'), self.paths['url']['tarball_sig']
413 self.download('signature', self.paths['url']['tarball_sig'], self.paths['file']['tarball_sig'])
415 elif task == 'verify':
416 print _('Verifying signature')
419 elif task == 'extract':
420 print _('Extracting'), self.paths['filename']['tarball']
424 print _('Running'), self.paths['file']['start']
427 elif task == 'start_over':
428 print _('Starting download over again')
431 def response_received(self, response):
432 class FileDownloader(Protocol):
433 def __init__(self, file, total, progress, done_cb):
437 self.progress = progress
438 self.all_done = done_cb
440 def dataReceived(self, bytes):
441 self.file.write(bytes)
442 self.so_far += len(bytes)
443 percent = float(self.so_far) / float(self.total)
444 self.progress.set_fraction(percent)
445 amount = float(self.so_far)
447 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
450 amount = amount / float(size)
453 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
455 def connectionLost(self, reason):
456 print _('Finished receiving body:'), reason.getErrorMessage()
457 self.all_done(reason)
459 dl = FileDownloader(self.file_download, response.length, self.progressbar, self.response_finished)
460 response.deliverBody(dl)
462 def response_finished(self, msg):
463 if msg.check(ResponseDone):
464 self.file_download.close()
469 print "FINISHED", msg
470 ## FIXME handle errors
472 def download_error(self, f):
473 print _("Download error"), f
474 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
478 def download(self, name, url, path):
479 # initialize the progress bar
480 self.progressbar.set_fraction(0)
481 self.progressbar.set_text(_('Downloading {0}').format(name))
482 self.progressbar.show()
485 agent = Agent(reactor, VerifyTorProjectCert(self.paths['file']['torproject_pem']))
486 d = agent.request('GET', url,
487 Headers({'User-Agent': ['torbrowser-launcher']}),
490 self.file_download = open(path, 'w')
491 d.addCallback(self.response_received).addErrback(self.download_error)
493 if not reactor.running:
496 def attempt_update(self):
497 # load the update check file
499 versions = json.load(open(self.paths['file']['update_check']))
500 latest_version = None
503 for version in versions:
504 if str(version).find(end) != -1:
505 latest_version = str(version)
508 self.settings['latest_version'] = latest_version[:-len(end)]
509 self.settings['last_update_check_timestamp'] = int(time.time())
511 self.build_paths(self.settings['latest_version'])
512 self.start_launcher()
515 # failed to find the latest version
516 self.set_gui('error', _("Error checking for updates."), [], False)
519 # not a valid JSON object
520 self.set_gui('error', _("Error checking for updates."), [], False)
527 # initialize the progress bar
528 self.progressbar.set_fraction(0)
529 self.progressbar.set_text(_('Verifying Signature'))
530 self.progressbar.show()
532 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--verify', self.paths['file']['tarball_sig']])
533 self.pulse_until_process_exits(p)
535 if p.returncode == 0:
538 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)
542 if not reactor.running:
546 # initialize the progress bar
547 self.progressbar.set_fraction(0)
548 self.progressbar.set_text(_('Installing'))
549 self.progressbar.show()
552 # make sure this file is a tarfile
553 if tarfile.is_tarfile(self.paths['file']['tarball']):
554 tf = tarfile.open(self.paths['file']['tarball'])
555 tf.extractall(self.paths['dir']['tbb'])
557 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}"), ['start_over'], False)
561 # installation is finished, so save installed_version
562 self.settings['installed_version'] = self.settings['latest_version']
567 def run(self, run_next_task = True):
568 subprocess.Popen([self.paths['file']['start']])
572 # make the progress bar pulse until process p (a Popen object) finishes
573 def pulse_until_process_exits(self, p):
574 while p.poll() == None:
576 self.progressbar.pulse()
579 # start over and download TBB again
580 def start_over(self):
581 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
582 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
587 def load_settings(self):
588 if os.path.isfile(self.paths['file']['settings']):
589 self.settings = pickle.load(open(self.paths['file']['settings']))
591 if not 'installed_version' in self.settings:
593 if not 'latest_version' in self.settings:
595 if not 'last_update_check_timestamp' in self.settings:
599 'installed_version': False,
600 'latest_version': '0',
601 'last_update_check_timestamp': 0
607 def save_settings(self):
608 pickle.dump(self.settings, open(self.paths['file']['settings'], 'w'))
612 def refresh_gtk(self):
613 while gtk.events_pending():
614 gtk.main_iteration(False)
616 # get the process id of a program
617 def get_pid(self, bin_path, python = False):
620 for p in psutil.process_iter():
622 if p.pid != os.getpid():
625 if len(p.cmdline) > 1:
626 if 'python' in p.cmdline[0]:
629 if len(p.cmdline) > 0:
640 # bring program's x window to front
641 def bring_window_to_front(self, pid):
642 # figure out the window id
644 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
645 for line in p.stdout.readlines():
646 line_split = line.split()
647 cur_win_id = line_split[0]
648 cur_win_pid = int(line_split[2])
649 if cur_win_pid == pid:
654 subprocess.call(['wmctrl', '-i', '-a', win_id])
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'
673 app = TorBrowserLauncher()