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