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