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