]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser-launcher
Merge pull request #112 from heichblatt/master
[torbrowser-launcher.git] / torbrowser-launcher
1 #!/usr/bin/env python
2 """
3 Tor Browser Launcher
4 https://github.com/micahflee/torbrowser-launcher/
5
6 Copyright (c) 2013-2014 Micah Lee <micah@micahflee.com>
7
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
15 conditions:
16
17 The above copyright notice and this permission notice shall be
18 included in all copies or substantial portions of the Software.
19
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.
28 """
29
30 import os
31 SHARE = os.getenv('TBL_SHARE', '/usr/share')
32 import sys
33 import platform
34
35 import gettext
36 gettext.install('torbrowser-launcher', os.path.join(SHARE, 'torbrowser-launcher/locale'))
37
38 from twisted.internet import gtk2reactor
39 gtk2reactor.install()
40 from twisted.internet import reactor
41
42 import pygtk
43 pygtk.require('2.0')
44 import gtk
45
46 import subprocess, locale, time, pickle, json, tarfile, psutil, hashlib, lzma
47
48 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
49 from twisted.web.http_headers import Headers
50 from twisted.internet.protocol import Protocol
51 from twisted.internet.ssl import ClientContextFactory
52 from twisted.internet.error import DNSLookupError
53
54 import OpenSSL
55
56
57 class TryStableException(Exception):
58     pass
59
60
61 class TryDefaultMirrorException(Exception):
62     pass
63
64
65 class DownloadErrorException(Exception):
66     pass
67
68
69 class VerifyTorProjectCert(ClientContextFactory):
70
71     def __init__(self, torproject_pem):
72         self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
73
74     def getContext(self, host, port):
75         ctx = ClientContextFactory.getContext(self)
76         ctx.set_verify_depth(0)
77         ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
78         return ctx
79
80     def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
81         return cert.digest('sha256') == self.torproject_ca.digest('sha256')
82
83
84 class TBLCommon:
85
86     def __init__(self, tbl_version):
87         print _('Initializing Tor Browser Launcher')
88         self.tbl_version = tbl_version
89
90         # initialize the app
91         self.default_mirror = 'https://www.torproject.org/dist/'
92         self.discover_arch_lang()
93         self.build_paths()
94         self.mkdir(self.paths['data_dir'])
95         self.load_mirrors()
96         self.load_settings()
97         self.mkdir(self.paths['download_dir'])
98         self.mkdir(self.paths['tbb']['dir'])
99         self.init_gnupg()
100
101         # allow buttons to have icons
102         try:
103             gtk_settings = gtk.settings_get_default()
104             gtk_settings.props.gtk_button_images = True
105         except:
106             pass
107
108     # discover the architecture and language
109     def discover_arch_lang(self):
110         # figure out the architecture
111         self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
112
113         # figure out the language
114         available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
115         default_locale = locale.getdefaultlocale()[0]
116         if default_locale is None:
117             self.language = 'en-US'
118         else:
119             self.language = default_locale.replace('_', '-')
120             if self.language not in available_languages:
121                 self.language = self.language.split('-')[0]
122                 if self.language not in available_languages:
123                     for l in available_languages:
124                         if l[0:2] == self.language:
125                             self.language = l
126             # if language isn't available, default to english
127             if self.language not in available_languages:
128                 self.language = 'en-US'
129
130     # build all relevant paths
131     def build_paths(self, tbb_version=None):
132         homedir = os.getenv('HOME')
133         if not homedir:
134             homedir = '/tmp/.torbrowser-'+os.getenv('USER')
135             if not os.path.exists(homedir):
136                 try:
137                     os.mkdir(homedir, 0700)
138                 except:
139                     self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
140         if not os.access(homedir, os.W_OK):
141             self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
142
143         tbb_data = '%s/.torbrowser' % homedir
144
145         if tbb_version:
146             # tarball filename
147             if self.architecture == 'x86_64':
148                 arch = 'linux64'
149             else:
150                 arch = 'linux32'
151             tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
152
153             # tarball
154             self.paths['tarball_url'] = '{0}torbrowser/'+tbb_version+'/'+tarball_filename
155             self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
156             self.paths['tarball_filename'] = tarball_filename
157
158             # sig
159             self.paths['sha256_file'] = tbb_data+'/download/sha256sums.txt'
160             self.paths['sha256_sig_file'] = tbb_data+'/download/sha256sums.txt.asc'
161             self.paths['sha256_url'] = '{0}torbrowser/'+tbb_version+'/sha256sums.txt'
162             self.paths['sha256_sig_url'] = '{0}torbrowser/'+tbb_version+'/sha256sums.txt-mikeperry.asc'
163         else:
164             self.paths = {
165                 'tbl_bin': '/usr/bin/torbrowser-launcher',
166                 'icon_file': os.path.join(SHARE, 'pixmaps/torbrowser80.xpm'),
167                 'torproject_pem': os.path.join(SHARE, 'torbrowser-launcher/torproject.pem'),
168                 'erinn_key': os.path.join(SHARE, 'torbrowser-launcher/erinn.asc'),
169                 'sebastian_key': os.path.join(SHARE, 'torbrowser-launcher/sebastian.asc'),
170                 'alexandre_key': os.path.join(SHARE, 'torbrowser-launcher/alexandre.asc'),
171                 'mike_key': os.path.join(SHARE, 'torbrowser-launcher/mike-2013-09.asc'),
172                 'mirrors_txt': [os.path.join(SHARE, 'torbrowser-launcher/mirrors.txt'),
173                                 '/usr/local/share/torbrowser-launcher/mirrors.txt'],
174                 'modem_sound': os.path.join(SHARE, 'torbrowser-launcher/modem.ogg'),
175                 'data_dir': tbb_data,
176                 'download_dir': tbb_data+'/download',
177                 'gnupg_homedir': tbb_data+'/gnupg_homedir',
178                 'settings_file': tbb_data+'/settings',
179                 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
180                 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
181                 'tbb': {
182                     'dir': tbb_data+'/tbb/'+self.architecture,
183                     'start': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
184                     'versions': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
185                 }
186             }
187
188     # create a directory
189     @staticmethod
190     def mkdir(path):
191         try:
192             if not os.path.exists(path):
193                 os.makedirs(path, 0700)
194                 return True
195         except:
196             print _("Cannot create directory {0}").format(path)
197             return False
198         if not os.access(path, os.W_OK):
199             print _("{0} is not writable").format(path)
200             return False
201         return True
202
203     # if gnupg_homedir isn't set up, set it up
204     def init_gnupg(self):
205         if not os.path.exists(self.paths['gnupg_homedir']):
206             print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
207             self.mkdir(self.paths['gnupg_homedir'])
208         self.import_keys()
209
210     # import gpg keys
211     def import_keys(self):
212         print _('Importing keys')
213         subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['erinn_key'], self.paths['sebastian_key'], self.paths['alexandre_key'], self.paths['mike_key']]).wait()
214
215     # load mirrors
216     def load_mirrors(self):
217         self.mirrors = []
218         for srcfile in self.paths['mirrors_txt']:
219             if os.path.exists(srcfile):
220                 print "Successfully loaded mirrors from %s" % srcfile
221             elif not os.path.exists(srcfile):
222                 print "Warning: can't load mirrors from %s" % srcfile
223                 continue
224             for mirror in open(srcfile, 'r').readlines():
225                 if mirror.strip() not in self.mirrors:
226                     self.mirrors.append(mirror.strip())
227
228     # load settings
229     def load_settings(self):
230         default_settings = {
231             'tbl_version': self.tbl_version,
232             'installed_version': False,
233             'latest_version': '0',
234             'update_over_tor': True,
235             'check_for_updates': False,
236             'modem_sound': False,
237             'last_update_check_timestamp': 0,
238             'mirror': self.default_mirror
239         }
240
241         if os.path.isfile(self.paths['settings_file']):
242             settings = pickle.load(open(self.paths['settings_file']))
243             resave = False
244
245             # settings migrations
246             if settings['tbl_version'] <= '0.1.0':
247                 print '0.1.0 migration'
248                 settings['installed_version'] = settings['installed_version']['stable']
249                 settings['latest_version'] = settings['latest_version']['stable']
250                 resave = True
251
252                 # make new tbb folder
253                 self.mkdir(self.paths['tbb']['dir'])
254                 old_tbb_dir = self.paths['data_dir']+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language
255                 new_tbb_dir = self.paths['tbb']['dir']+'/tor-browser_'+self.language
256                 if os.path.isdir(old_tbb_dir):
257                     os.rename(old_tbb_dir, new_tbb_dir)
258
259             # make sure settings file is up-to-date
260             for setting in default_settings:
261                 if setting not in settings:
262                     settings[setting] = default_settings[setting]
263                     resave = True
264
265             # make sure the version is current
266             if settings['tbl_version'] != self.tbl_version:
267                 settings['tbl_version'] = self.tbl_version
268                 resave = True
269
270             self.settings = settings
271             if resave:
272                 self.save_settings()
273
274         else:
275             self.settings = default_settings
276             self.save_settings()
277
278     # save settings
279     def save_settings(self):
280         pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
281         return True
282
283     # get the process id of a program
284     @staticmethod
285     def get_pid(bin_path, python=False):
286         pid = None
287
288         for p in psutil.process_iter():
289             try:
290                 if p.pid != os.getpid():
291                     exe = None
292                     if python:
293                         if len(p.cmdline) > 1:
294                             if 'python' in p.cmdline[0]:
295                                 exe = p.cmdline[1]
296                     else:
297                         if len(p.cmdline) > 0:
298                             exe = p.cmdline[0]
299
300                     if exe == bin_path:
301                         pid = p.pid
302
303             except:
304                 pass
305
306         return pid
307
308     # bring program's x window to front
309     @staticmethod
310     def bring_window_to_front(pid):
311         # figure out the window id
312         win_id = None
313         p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
314         for line in p.stdout.readlines():
315             line_split = line.split()
316             cur_win_id = line_split[0]
317             cur_win_pid = int(line_split[2])
318             if cur_win_pid == pid:
319                 win_id = cur_win_id
320
321         # bring to front
322         if win_id:
323             subprocess.call(['wmctrl', '-i', '-a', win_id])
324
325
326 class TBLSettings:
327     def __init__(self, common):
328         print _('Starting settings dialog')
329         self.common = common
330
331         # set up the window
332         self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
333         self.window.set_title(_("Tor Browser Launcher Settings"))
334         self.window.set_icon_from_file(self.common.paths['icon_file'])
335         self.window.set_position(gtk.WIN_POS_CENTER)
336         self.window.set_border_width(10)
337         self.window.connect("delete_event", self.delete_event)
338         self.window.connect("destroy", self.destroy)
339
340         # build the rest of the UI
341         self.box = gtk.VBox(False, 10)
342         self.window.add(self.box)
343         self.box.show()
344
345         self.hbox = gtk.HBox(False, 10)
346         self.box.pack_start(self.hbox, True, True, 0)
347         self.hbox.show()
348
349         self.settings_box = gtk.VBox(False, 10)
350         self.hbox.pack_start(self.settings_box, True, True, 0)
351         self.settings_box.show()
352
353         self.labels_box = gtk.VBox(False, 10)
354         self.hbox.pack_start(self.labels_box, True, True, 0)
355         self.labels_box.show()
356
357         # download over tor
358         try:
359             import txsocksx
360             self.txsocks_found = True
361         except ImportError:
362             self.txsocks_found = False
363         self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
364         if self.txsocks_found:
365             self.tor_update_checkbox.set_tooltip_text(_("This option is only available when using a system wide Tor installation."))
366         else:
367             self.tor_update_checkbox.set_tooltip_text(_("This option requires the python-txsocksx package."))
368
369         self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
370         if self.common.settings['update_over_tor'] and self.txsocks_found:
371             self.tor_update_checkbox.set_active(True)
372         else:
373             self.tor_update_checkbox.set_active(False)
374
375         if self.txsocks_found == False:
376             self.tor_update_checkbox.set_sensitive(False)
377
378         self.tor_update_checkbox.show()
379
380         # check for updates
381         self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
382         self.settings_box.pack_start(self.update_checkbox, True, True, 0)
383         if self.common.settings['check_for_updates']:
384             self.update_checkbox.set_active(True)
385         else:
386             self.update_checkbox.set_active(False)
387         self.update_checkbox.show()
388
389         # modem sound
390         self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
391         self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
392
393         try:
394             import pygame
395             if self.common.settings['modem_sound']:
396                 self.modem_checkbox.set_active(True)
397             else:
398                 self.modem_checkbox.set_active(False)
399         except ImportError:
400             self.modem_checkbox.set_active(False)
401             self.modem_checkbox.set_sensitive(False)
402             self.modem_checkbox.set_tooltip_text(_("This option requires python-pygame to be installed"))
403         self.modem_checkbox.show()
404
405         # labels
406         if(self.common.settings['installed_version']):
407             self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version']))
408         else:
409             self.label1 = gtk.Label(_('Not installed'))
410         self.label1.set_line_wrap(True)
411         self.labels_box.pack_start(self.label1, True, True, 0)
412         self.label1.show()
413
414         if(self.common.settings['last_update_check_timestamp'] > 0):
415             self.label1 = gtk.Label(_('Last checked for updates:\n{0}').format(time.strftime("%B %d, %Y %I:%M %P", time.gmtime(self.common.settings['last_update_check_timestamp']))))
416         else:
417             self.label1 = gtk.Label(_('Never checked for updates'))
418         self.label1.set_line_wrap(True)
419         self.labels_box.pack_start(self.label1, True, True, 0)
420         self.label1.show()
421
422         # mirrors
423         self.mirrors_box = gtk.HBox(False, 10)
424         self.box.pack_start(self.mirrors_box, True, True, 0)
425         self.mirrors_box.show()
426
427         self.mirrors_label = gtk.Label(_('Mirror'))
428         self.mirrors_label.set_line_wrap(True)
429         self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
430         self.mirrors_label.show()
431
432         self.mirrors = gtk.combo_box_new_text()
433         for mirror in self.common.mirrors:
434             self.mirrors.append_text(mirror)
435         if self.common.settings['mirror'] in self.common.mirrors:
436             self.mirrors.set_active(self.common.mirrors.index(self.common.settings['mirror']))
437         else:
438             self.mirrors.set_active(0)
439         self.mirrors_box.pack_start(self.mirrors, True, True, 0)
440         self.mirrors.show()
441
442         # button box
443         self.button_box = gtk.HButtonBox()
444         self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
445         self.box.pack_start(self.button_box, True, True, 0)
446         self.button_box.show()
447
448         # save and launch button
449         save_launch_image = gtk.Image()
450         save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
451         self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
452         self.save_launch_button.set_image(save_launch_image)
453         self.save_launch_button.connect("clicked", self.save_launch, None)
454         self.button_box.add(self.save_launch_button)
455         self.save_launch_button.show()
456
457         # save and exit button
458         save_exit_image = gtk.Image()
459         save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
460         self.save_exit_button = gtk.Button(_("Save & Exit"))
461         self.save_exit_button.set_image(save_exit_image)
462         self.save_exit_button.connect("clicked", self.save_exit, None)
463         self.button_box.add(self.save_exit_button)
464         self.save_exit_button.show()
465
466         # cancel button
467         cancel_image = gtk.Image()
468         cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
469         self.cancel_button = gtk.Button(_("Cancel"))
470         self.cancel_button.set_image(cancel_image)
471         self.cancel_button.connect("clicked", self.destroy, None)
472         self.button_box.add(self.cancel_button)
473         self.cancel_button.show()
474
475         # show the window
476         self.window.show()
477
478         # start gtk
479         gtk.main()
480
481     # UI Callback for update over tor/use system tor
482     def on_system_tor_clicked(self, event):
483         if self.txsocks_found:
484             value = self.system_tor_checkbox.get_active()
485         else:
486             value = False
487
488         self.tor_update_checkbox.set_active(value)
489         self.tor_update_checkbox.set_sensitive(value)
490
491     # save and launch
492     def save_launch(self, widget, data=None):
493         self.save()
494         subprocess.Popen([self.common.paths['tbl_bin']])
495         self.destroy(False)
496
497     # save and exit
498     def save_exit(self, widget, data=None):
499         self.save()
500         self.destroy(False)
501
502     # save settings
503     def save(self):
504         # checkbox options
505         self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
506         self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
507         self.common.settings['modem_sound'] = self.modem_checkbox.get_active()
508
509         # figure out the selected mirror
510         self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
511
512         # save them
513         self.common.save_settings()
514
515     # exit
516     def delete_event(self, widget, event, data=None):
517         return False
518
519     def destroy(self, widget, data=None):
520         gtk.main_quit()
521
522
523 class TBLLauncher:
524     def __init__(self, common):
525         print _('Starting launcher dialog')
526         self.common = common
527
528         # init launcher
529         self.set_gui(None, '', [])
530         self.launch_gui = True
531         print "LATEST VERSION", self.common.settings['latest_version']
532         self.common.build_paths(self.common.settings['latest_version'])
533
534         if self.common.settings['update_over_tor']:
535             try:
536                 import txsocksx
537             except ImportError:
538                 md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-txsocksx package is missing, downloads will not happen over tor"))
539                 md.set_position(gtk.WIN_POS_CENTER)
540                 md.run()
541                 md.destroy()
542                 self.common.settings['update_over_tor'] = False
543                 self.common.save_settings()
544
545         # is firefox already running?
546         if self.common.settings['installed_version']:
547             firefox_pid = self.common.get_pid('./Browser/firefox')
548             if firefox_pid:
549                 print _('Firefox are is open, bringing to focus')
550                 # bring firefox to front
551                 self.common.bring_window_to_front(firefox_pid)
552                 return
553
554         # check for updates?
555         check_for_updates = False
556         if self.common.settings['check_for_updates']:
557             check_for_updates = True
558
559         if not check_for_updates:
560             # how long was it since the last update check?
561             # 86400 seconds = 24 hours
562             current_timestamp = int(time.time())
563             if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
564                 check_for_updates = True
565
566         if check_for_updates:
567             # check for update
568             print 'Checking for update'
569             self.set_gui('task', _("Checking for Tor Browser update."),
570                          ['download_update_check',
571                           'attempt_update'])
572         else:
573             # no need to check for update
574             print _('Checked for update within 24 hours, skipping')
575             self.start_launcher()
576
577         if self.launch_gui:
578             # set up the window
579             self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
580             self.window.set_title(_("Tor Browser"))
581             self.window.set_icon_from_file(self.common.paths['icon_file'])
582             self.window.set_position(gtk.WIN_POS_CENTER)
583             self.window.set_border_width(10)
584             self.window.connect("delete_event", self.delete_event)
585             self.window.connect("destroy", self.destroy)
586
587             # build the rest of the UI
588             self.build_ui()
589
590     # download or run TBB
591     def start_launcher(self):
592         # is TBB already installed?
593         latest_version = self.common.settings['latest_version']
594         installed_version = self.common.settings['installed_version']
595
596         # verify installed version for newer versions of TBB (#58)
597         if installed_version >= '3.0':
598             versions_filename = self.common.paths['tbb']['versions']
599             if os.path.exists(versions_filename):
600                 for line in open(versions_filename):
601                     if 'TORBROWSER_VERSION' in line:
602                         installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
603
604         start = self.common.paths['tbb']['start']
605         if os.path.isfile(start) and os.access(start, os.X_OK):
606             if installed_version == latest_version:
607                 print _('Latest version of TBB is installed, launching')
608                 # current version of tbb is installed, launch it
609                 self.run(False)
610                 self.launch_gui = False
611             elif installed_version < latest_version:
612                 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
613                 # there is a tbb upgrade available
614                 self.set_gui('task', _("Your Tor Browser is out of date."),
615                              ['download_sha256',
616                               'download_sha256_sig',
617                               'download_tarball',
618                               'verify',
619                               'extract',
620                               'run'])
621             else:
622                 # for some reason the installed tbb is newer than the current version?
623                 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
624
625         # not installed
626         else:
627             print _('TBB is not installed, attempting to install {0}'.format(latest_version))
628             self.set_gui('task', _("Downloading and installing Tor Browser."),
629                          ['download_sha256',
630                           'download_sha256_sig',
631                           'download_tarball',
632                           'verify',
633                           'extract',
634                           'run'])
635
636     # there are different GUIs that might appear, this sets which one we want
637     def set_gui(self, gui, message, tasks, autostart=True):
638         self.gui = gui
639         self.gui_message = message
640         self.gui_tasks = tasks
641         self.gui_task_i = 0
642         self.gui_autostart = autostart
643
644     # set all gtk variables to False
645     def clear_ui(self):
646         if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
647             self.box.destroy()
648         self.box = False
649
650         self.label = False
651         self.progressbar = False
652         self.button_box = False
653         self.start_button = False
654         self.exit_button = False
655
656     # build the application's UI
657     def build_ui(self):
658         self.clear_ui()
659
660         self.box = gtk.VBox(False, 20)
661         self.window.add(self.box)
662
663         if 'error' in self.gui:
664             # labels
665             self.label = gtk.Label(self.gui_message)
666             self.label.set_line_wrap(True)
667             self.box.pack_start(self.label, True, True, 0)
668             self.label.show()
669
670             # button box
671             self.button_box = gtk.HButtonBox()
672             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
673             self.box.pack_start(self.button_box, True, True, 0)
674             self.button_box.show()
675
676             if self.gui != 'error':
677                 # yes button
678                 yes_image = gtk.Image()
679                 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
680                 self.yes_button = gtk.Button("Yes")
681                 self.yes_button.set_image(yes_image)
682                 if self.gui == 'error_try_stable':
683                     self.yes_button.connect("clicked", self.try_stable, None)
684                 elif self.gui == 'error_try_default_mirror':
685                     self.yes_button.connect("clicked", self.try_default_mirror, None)
686                 elif self.gui == 'error_try_tor':
687                     self.yes_button.connect("clicked", self.try_tor, None)
688                 self.button_box.add(self.yes_button)
689                 self.yes_button.show()
690
691             # exit button
692             exit_image = gtk.Image()
693             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
694             self.exit_button = gtk.Button("Exit")
695             self.exit_button.set_image(exit_image)
696             self.exit_button.connect("clicked", self.destroy, None)
697             self.button_box.add(self.exit_button)
698             self.exit_button.show()
699
700         elif self.gui == 'task':
701             # label
702             self.label = gtk.Label(self.gui_message)
703             self.label.set_line_wrap(True)
704             self.box.pack_start(self.label, True, True, 0)
705             self.label.show()
706
707             # progress bar
708             self.progressbar = gtk.ProgressBar(adjustment=None)
709             self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
710             self.progressbar.set_pulse_step(0.01)
711             self.box.pack_start(self.progressbar, True, True, 0)
712
713             # button box
714             self.button_box = gtk.HButtonBox()
715             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
716             self.box.pack_start(self.button_box, True, True, 0)
717             self.button_box.show()
718
719             # start button
720             start_image = gtk.Image()
721             start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
722             self.start_button = gtk.Button(_("Start"))
723             self.start_button.set_image(start_image)
724             self.start_button.connect("clicked", self.start, None)
725             self.button_box.add(self.start_button)
726             if not self.gui_autostart:
727                 self.start_button.show()
728
729             # exit button
730             exit_image = gtk.Image()
731             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
732             self.exit_button = gtk.Button(_("Exit"))
733             self.exit_button.set_image(exit_image)
734             self.exit_button.connect("clicked", self.destroy, None)
735             self.button_box.add(self.exit_button)
736             self.exit_button.show()
737
738         self.box.show()
739         self.window.show()
740
741         if self.gui_autostart:
742             self.start(None)
743
744     # start button clicked, begin tasks
745     def start(self, widget, data=None):
746         # disable the start button
747         if self.start_button:
748             self.start_button.set_sensitive(False)
749
750         # start running tasks
751         self.run_task()
752
753     # run the next task in the task list
754     def run_task(self):
755         self.refresh_gtk()
756
757         if self.gui_task_i >= len(self.gui_tasks):
758             self.destroy(False)
759             return
760
761         task = self.gui_tasks[self.gui_task_i]
762
763         # get ready for the next task
764         self.gui_task_i += 1
765
766         print _('Running task: {0}'.format(task))
767         if task == 'download_update_check':
768             print _('Downloading'), self.common.paths['update_check_url']
769             self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
770
771         if task == 'attempt_update':
772             print _('Checking to see if update is needed')
773             self.attempt_update()
774
775         elif task == 'download_sha256':
776             print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
777             self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
778
779         elif task == 'download_sha256_sig':
780             print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
781             self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
782
783         elif task == 'download_tarball':
784             print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
785             self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
786
787         elif task == 'verify':
788             print _('Verifying signature')
789             self.verify()
790
791         elif task == 'extract':
792             print _('Extracting'), self.common.paths['tarball_filename']
793             self.extract()
794
795         elif task == 'run':
796             print _('Running'), self.common.paths['tbb']['start']
797             self.run()
798
799         elif task == 'start_over':
800             print _('Starting download over again')
801             self.start_over()
802
803     def response_received(self, response):
804         class FileDownloader(Protocol):
805             def __init__(self, common, file, total, progress, done_cb):
806                 self.file = file
807                 self.total = total
808                 self.so_far = 0
809                 self.progress = progress
810                 self.all_done = done_cb
811
812                 if response.code != 200:
813                     if common.settings['mirror'] != common.default_mirror:
814                         raise TryDefaultMirrorException(_("Download Error: {0} {1}\n\nYou are currently using a non-default mirror:\n{2}\n\nWould you like to switch back to the default?").format(response.code, response.phrase, common.settings['mirror']))
815                     else:
816                         raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
817
818             def dataReceived(self, bytes):
819                 self.file.write(bytes)
820                 self.so_far += len(bytes)
821                 percent = float(self.so_far) / float(self.total)
822                 self.progress.set_fraction(percent)
823                 amount = float(self.so_far)
824                 units = "bytes"
825                 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
826                     if amount > size:
827                         units = unit
828                         amount = amount / float(size)
829                         break
830
831                 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
832
833             def connectionLost(self, reason):
834                 print _('Finished receiving body:'), reason.getErrorMessage()
835                 self.all_done(reason)
836
837         dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
838         response.deliverBody(dl)
839
840     def response_finished(self, msg):
841         if msg.check(ResponseDone):
842             self.file_download.close()
843             delattr(self, 'current_download_path')
844
845             # next task!
846             self.run_task()
847
848         else:
849             print "FINISHED", msg
850             ## FIXME handle errors
851
852     def download_error(self, f):
853         print _("Download error:"), f.value, type(f.value)
854
855         if isinstance(f.value, TryStableException):
856             f.trap(TryStableException)
857             self.set_gui('error_try_stable', str(f.value), [], False)
858
859         elif isinstance(f.value, TryDefaultMirrorException):
860             f.trap(TryDefaultMirrorException)
861             self.set_gui('error_try_default_mirror', str(f.value), [], False)
862
863         elif isinstance(f.value, DownloadErrorException):
864             f.trap(DownloadErrorException)
865             self.set_gui('error', str(f.value), [], False)
866
867         elif isinstance(f.value, DNSLookupError):
868             f.trap(DNSLookupError)
869             if common.settings['mirror'] != common.default_mirror:
870                 self.set_gui('error_try_default_mirror', _("DNS Lookup Error\n\nYou are currently using a non-default mirror:\n{0}\n\nWould you like to switch back to the default?").format(common.settings['mirror']), [], False)
871             else:
872                 self.set_gui('error', str(f.value), [], False)
873
874         elif isinstance(f.value, ResponseFailed):
875             for reason in f.value.reasons:
876                 if isinstance(reason.value, OpenSSL.SSL.Error):
877                     # TODO: add the ability to report attack by posting bug to trac.torproject.org
878                     if not self.common.settings['update_over_tor']:
879                         self.set_gui('error_try_tor', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack. Try the download again using Tor?'), [], False)
880                     else:
881                         self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
882
883         else:
884             self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
885
886         self.build_ui()
887
888     def download(self, name, url, path):
889         # keep track of current download
890         self.current_download_path = path
891
892         # initialize the progress bar
893         mirror_url = url.format(self.common.settings['mirror'])
894         self.progressbar.set_fraction(0)
895         self.progressbar.set_text(_('Downloading {0}').format(name))
896         self.progressbar.show()
897         self.refresh_gtk()
898
899         if self.common.settings['update_over_tor']:
900             print _('Updating over Tor')
901             from twisted.internet.endpoints import TCP4ClientEndpoint
902             from txsocksx.http import SOCKS5Agent
903
904             torEndpoint = TCP4ClientEndpoint(reactor, '127.0.0.1', 9050)
905
906             # default mirror gets certificate pinning, only for requests that use the mirror
907             if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
908                 agent = SOCKS5Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']), proxyEndpoint=torEndpoint)
909             else:
910                 agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
911         else:
912             if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
913                 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
914             else:
915                 agent = Agent(reactor)
916
917         # actually, agent needs to follow redirect
918         agent = RedirectAgent(agent)
919
920         # start the request
921         d = agent.request('GET', mirror_url,
922                           Headers({'User-Agent': ['torbrowser-launcher']}),
923                           None)
924
925         self.file_download = open(path, 'w')
926         d.addCallback(self.response_received).addErrback(self.download_error)
927
928         if not reactor.running:
929             reactor.run()
930
931     def try_default_mirror(self, widget, data=None):
932         # change mirror to default and relaunch TBL
933         self.common.settings['mirror'] = self.common.default_mirror
934         self.common.save_settings()
935         subprocess.Popen([self.common.paths['tbl_bin']])
936         self.destroy(False)
937
938     def try_tor(self, widget, data=None):
939         # set update_over_tor to true and relaunch TBL
940         self.common.settings['update_over_tor'] = True
941         self.common.save_settings()
942         subprocess.Popen([self.common.paths['tbl_bin']])
943         self.destroy(False)
944
945     def attempt_update(self):
946         # load the update check file
947         try:
948             versions = json.load(open(self.common.paths['update_check_file']))
949             latest = None
950
951             # filter linux versions
952             valid = []
953             for version in versions:
954                 if '-Linux' in version:
955                     valid.append(str(version))
956             valid.sort()
957             if len(valid):
958                 latest = valid.pop()
959
960             if latest:
961                 self.common.settings['latest_version'] = latest[:-len('-Linux')]
962                 self.common.settings['last_update_check_timestamp'] = int(time.time())
963                 self.common.settings['check_for_updates'] = False
964                 self.common.save_settings()
965                 self.common.build_paths(self.common.settings['latest_version'])
966                 self.start_launcher()
967
968             else:
969                 # failed to find the latest version
970                 self.set_gui('error', _("Error checking for updates."), [], False)
971
972         except:
973             # not a valid JSON object
974             self.set_gui('error', _("Error checking for updates."), [], False)
975
976         # now start over
977         self.clear_ui()
978         self.build_ui()
979
980     def verify(self):
981         # initialize the progress bar
982         self.progressbar.set_fraction(0)
983         self.progressbar.set_text(_('Verifying Signature'))
984         self.progressbar.show()
985
986         verified = False
987         # check the sha256 file's sig, and also take the sha256 of the tarball and compare
988         p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
989         self.pulse_until_process_exits(p)
990         if p.returncode == 0:
991             # compare with sha256 of the tarball
992             tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
993             for line in open(self.common.paths['sha256_file'], 'r').readlines():
994                 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
995                     verified = True
996
997         if verified:
998             self.run_task()
999         else:
1000             # TODO: add the ability to report attack by posting bug to trac.torproject.org
1001             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)
1002             self.clear_ui()
1003             self.build_ui()
1004
1005             if not reactor.running:
1006                 reactor.run()
1007
1008     def extract(self):
1009         # initialize the progress bar
1010         self.progressbar.set_fraction(0)
1011         self.progressbar.set_text(_('Installing'))
1012         self.progressbar.show()
1013         self.refresh_gtk()
1014
1015         extracted = False
1016         try:
1017             if self.common.paths['tarball_file'][-2:] == 'xz':
1018                 # if tarball is .tar.xz
1019                 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1020                 tf = tarfile.open(fileobj=xz)
1021                 tf.extractall(self.common.paths['tbb']['dir'])
1022                 extracted = True
1023             else:
1024                 # if tarball is .tar.gz
1025                 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1026                     tf = tarfile.open(self.common.paths['tarball_file'])
1027                     tf.extractall(self.common.paths['tbb']['dir'])
1028                     extracted = True
1029         except:
1030             pass
1031
1032         if not extracted:
1033             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1034             self.clear_ui()
1035             self.build_ui()
1036             return
1037
1038         # installation is finished, so save installed_version
1039         self.common.settings['installed_version'] = self.common.settings['latest_version']
1040         self.common.save_settings()
1041
1042         self.run_task()
1043
1044     def run(self, run_next_task=True):
1045         subprocess.Popen([self.common.paths['tbb']['start']])
1046
1047         # play modem sound?
1048         if self.common.settings['modem_sound']:
1049             try:
1050                 import pygame
1051                 pygame.mixer.init()
1052                 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1053                 sound.play()
1054                 time.sleep(10)
1055             except ImportError:
1056                 md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-pygame package is missing, the modem sound is unavailable."))
1057                 md.set_position(gtk.WIN_POS_CENTER)
1058                 md.run()
1059                 md.destroy()
1060
1061
1062         if run_next_task:
1063             self.run_task()
1064
1065     # make the progress bar pulse until process p (a Popen object) finishes
1066     def pulse_until_process_exits(self, p):
1067         while p.poll() is None:
1068             time.sleep(0.01)
1069             self.progressbar.pulse()
1070             self.refresh_gtk()
1071
1072     # start over and download TBB again
1073     def start_over(self):
1074         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1075         self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
1076         self.gui_task_i = 0
1077         self.start(None)
1078
1079     # refresh gtk
1080     def refresh_gtk(self):
1081         while gtk.events_pending():
1082             gtk.main_iteration(False)
1083
1084     # exit
1085     def delete_event(self, widget, event, data=None):
1086         return False
1087
1088     def destroy(self, widget, data=None):
1089         if hasattr(self, 'file_download'):
1090             self.file_download.close()
1091         if hasattr(self, 'current_download_path'):
1092             os.remove(self.current_download_path)
1093             delattr(self, 'current_download_path')
1094         if reactor.running:
1095             reactor.stop()
1096
1097 if __name__ == "__main__":
1098     with open(os.path.join(SHARE, 'torbrowser-launcher/version')) as buf:
1099         tor_browser_launcher_version = buf.read().strip()
1100
1101     print _('Tor Browser Launcher')
1102     print _('By Micah Lee, licensed under GPLv3')
1103     print _('version {0}').format(tor_browser_launcher_version)
1104     print 'https://github.com/micahflee/torbrowser-launcher'
1105
1106     common = TBLCommon(tor_browser_launcher_version)
1107
1108     # is torbrowser-launcher already running?
1109     tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1110     if tbl_pid:
1111         print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1112         common.bring_window_to_front(tbl_pid)
1113         sys.exit()
1114
1115     if '-settings' in sys.argv:
1116         # settings mode
1117         app = TBLSettings(common)
1118
1119     else:
1120         # launcher mode
1121         app = TBLLauncher(common)