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