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