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