]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser-launcher
made gpg keys get imported one after the other, fixes #20
[torbrowser-launcher.git] / torbrowser-launcher
1 #!/usr/bin/env python
2
3 import gettext
4 gettext.install('torbrowser-launcher', '/usr/share/torbrowser-launcher/locale')
5
6 from twisted.internet import gtk2reactor
7 gtk2reactor.install()
8 from twisted.internet import reactor
9
10 import pygtk
11 pygtk.require('2.0')
12 import gtk
13
14 import os, sys, subprocess, locale, urllib2, gobject, time, pickle, json, tarfile, psutil
15
16 from twisted.web.client import Agent, ResponseDone
17 from twisted.web.http_headers import Headers
18 from twisted.internet.protocol import Protocol
19 from twisted.internet.ssl import ClientContextFactory
20
21 from OpenSSL.SSL import Context, VERIFY_PEER, VERIFY_FAIL_IF_NO_PEER_CERT
22 from OpenSSL.crypto import load_certificate, FILETYPE_PEM
23
24 class VerifyTorProjectCert(ClientContextFactory):
25
26     def __init__(self, torproject_pem):
27         self.torproject_ca = load_certificate(FILETYPE_PEM, open(torproject_pem, 'r').read())
28
29     def getContext(self, host, port):
30         ctx = ClientContextFactory.getContext(self)
31         ctx.set_verify_depth(0)
32         ctx.set_verify(VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
33         return ctx
34
35     def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
36         return cert.digest('sha256') == self.torproject_ca.digest('sha256')
37
38
39 class TorBrowserLauncher:
40     def __init__(self):
41         # initialize the app
42         self.set_gui(None, '', [])
43         self.discover_arch_lang()
44         self.build_paths()
45         self.mkdir(self.paths['dir']['download'])
46         self.mkdir(self.paths['dir']['tbb'])
47         self.init_gnupg()
48
49         # allow buttons to have icons
50         try:
51             settings = gtk.settings_get_default()
52             settings.props.gtk_button_images = True
53         except:
54             pass
55
56         self.launch_gui = True
57
58         # if we haven't already hit an error
59         if self.gui != 'error':
60             # load settings
61             if self.load_settings():
62                 self.build_paths(self.settings['latest_version'])
63
64                 # is tbb already running and we just need to open a new firefox?
65                 if self.settings['installed_version']:
66                     vidalia_pid = None
67                     firefox_pid = None
68                     for p in psutil.process_iter():
69                         try:
70                             exe = None
71
72                             # old versions of psutil don't have exe
73                             if hasattr(p, 'exe'):
74                                 exe = p.exe
75                             # need to rely on cmdline instead
76                             else:
77                                 if len(p.cmdline) > 0:
78                                     exe = p.cmdline[0]
79                             
80                             if exe == self.paths['file']['vidalia_bin'] or exe == './App/vidalia':
81                                 vidalia_pid = p.pid
82                             if exe == self.paths['file']['firefox_bin']:
83                                 firefox_pid = p.pid
84
85                         except:
86                             pass
87
88                     if vidalia_pid and not firefox_pid:
89                         print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
90                         subprocess.Popen([self.paths['file']['firefox_bin'], '-no-remote', '-profile', self.paths['file']['firefox_profile']])
91                         return
92                     elif vidalia_pid and firefox_pid:
93                         print _('Vidalia and Firefox are already open, bringing them to focus')
94
95                         # figure out the window ids of vidalia and firefox
96                         vidalia_win_id = None
97                         firefox_win_id = None
98                         p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
99                         for line in p.stdout.readlines():
100                             line_split = line.split()
101                             win_id = line_split[0]
102                             win_pid = int(line_split[2])
103                             if win_pid == vidalia_pid:
104                                 vidalia_win_id = win_id
105                             if win_pid == firefox_pid:
106                                 firefox_win_id = win_id
107
108                         # bring firefox to front, then vidalia
109                         if firefox_win_id:
110                             subprocess.call(['wmctrl', '-i', '-a', firefox_win_id])
111                         if vidalia_win_id:
112                             subprocess.call(['wmctrl', '-i', '-a', vidalia_win_id])
113
114                         return
115
116                 # how long was it since the last update check?
117                 # 86400 seconds = 24 hours
118                 current_timestamp = int(time.time())
119                 if current_timestamp - self.settings['last_update_check_timestamp'] >= 86400:
120                     # check for update
121                     print 'Checking for update'
122                     self.set_gui('task', _("Checking for Tor Browser update."), 
123                         ['download_update_check', 
124                          'attempt_update'])
125
126                 else:
127                     # no need to check for update
128                     print _('Checked for update within 24 hours, skipping')
129                     self.start_launcher()
130
131             else:
132                 self.set_gui('error', _("Error loading settings. Delete ~/.torbrowser and try again."), [])
133
134         if self.launch_gui:
135             # set up the window
136             self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
137             self.window.set_title(_("Tor Browser"))
138             self.window.set_icon_from_file(self.paths['file']['icon'])
139             self.window.set_position(gtk.WIN_POS_CENTER)
140             self.window.set_border_width(10)
141             self.window.connect("delete_event", self.delete_event)
142             self.window.connect("destroy", self.destroy)
143
144             # build the rest of the UI
145             self.build_ui()
146
147     # download or run TBB
148     def start_launcher(self):
149       # is TBB already installed?
150       if os.path.isfile(self.paths['file']['start']) and os.access(self.paths['file']['start'], os.X_OK):
151         if self.settings['installed_version'] == self.settings['latest_version']:
152           # current version of tbb is installed, launch it
153           self.run(False)
154           self.launch_gui = False
155         elif self.settings['installed_version'] < self.settings['latest_version']:
156           # there is a tbb upgrade available
157           self.set_gui('task', _("Your Tor Browser is out of date."), 
158             ['download_tarball', 
159              'download_tarball_sig', 
160              'verify', 
161              'extract', 
162              'run'])
163         else:
164           # for some reason the installed tbb is newer than the current version?
165           self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
166
167       # not installed
168       else:
169           # are the tarball and sig already downloaded?
170           if os.path.isfile(self.paths['file']['tarball']) and os.path.isfile(self.paths['file']['tarball_sig']):
171               # start the gui with verify
172               self.set_gui('task', _("Installing Tor Browser."), 
173                   ['verify', 
174                    'extract', 
175                    'run'])
176
177           # first run
178           else:
179               self.set_gui('task', _("Downloading and installing Tor Browser."), 
180                   ['download_tarball', 
181                    'download_tarball_sig', 
182                    'verify', 
183                    'extract', 
184                    'run'])
185    
186     # discover the architecture and language
187     def discover_arch_lang(self):
188         # figure out the architecture
189         (sysname, nodename, release, version, machine) = os.uname()
190         self.architecture = machine
191
192         # figure out the language
193         available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
194         default_locale = locale.getdefaultlocale()[0]
195         if default_locale == None:
196             self.language = 'en-US'
197         else:
198             self.language = default_locale.replace('_', '-')
199             if self.language not in available_languages:
200                 self.language = self.language.split('-')[0]
201                 if self.language not in available_languages:
202                     for l in available_languages:
203                         if l[0:2] == self.language:
204                             self.language = l
205             # if language isn't available, default to english
206             if self.language not in available_languages:
207                 self.language = 'en-US'
208
209     # build all relevant paths
210     def build_paths(self, tbb_version = None):
211         homedir = os.getenv('HOME')
212         if not homedir:
213             homedir = '/tmp/.torbrowser-'+os.getenv('USER')
214             if os.path.exists(homedir) == False:
215                 try:
216                     os.mkdir(homedir, 0700)
217                 except:
218                     self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
219         if not os.access(homedir, os.W_OK):
220             self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
221
222         tbb_data = '%s/.torbrowser' % homedir
223
224         if tbb_version:
225             tarball_filename = 'tor-browser-gnu-linux-'+self.architecture+'-'+tbb_version+'-dev-'+self.language+'.tar.gz'
226             self.paths['file']['tarball'] = tbb_data+'/download/'+tarball_filename
227             self.paths['file']['tarball_sig'] = tbb_data+'/download/'+tarball_filename+'.asc'
228             self.paths['url']['tarball'] = 'https://www.torproject.org/dist/torbrowser/linux/'+tarball_filename
229             self.paths['url']['tarball_sig'] = 'https://www.torproject.org/dist/torbrowser/linux/'+tarball_filename+'.asc'
230             self.paths['filename']['tarball'] = tarball_filename
231             self.paths['filename']['tarball_sig'] = tarball_filename+'.asc'
232
233         else:
234             self.paths = {
235                 'dir': {
236                     'data': tbb_data,
237                     'download': tbb_data+'/download',
238                     'tbb': tbb_data+'/tbb/'+self.architecture,
239                     'gnupg_homedir': tbb_data+'/gnupg_homedir'
240                 },
241                 'file': {
242                     'settings': tbb_data+'/settings',
243                     'version': tbb_data+'/version',
244                     'start': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
245                     'vidalia_bin': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
246                     'firefox_bin': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
247                     'firefox_profile': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
248                     'update_check': tbb_data+'/download/RecommendedTBBVersions',
249                     'icon': '/usr/share/pixmaps/torbrowser80.xpm',
250                     'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
251                     'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
252                     'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc'
253                 },
254                 'url': {
255                     'update_check': 'https://check.torproject.org/RecommendedTBBVersions'
256                 },
257                 'filename': {}
258             }
259
260     # create a directory
261     def mkdir(self, path):
262         try:
263             if os.path.exists(path) == False:
264                 os.makedirs(path, 0700)
265                 return True
266         except:
267             self.set_gui('error', _("Cannot create directory {0}").format(path), [], False)
268             return False
269         if not os.access(path, os.W_OK):
270             self.set_gui('error', _("{0} is not writable").format(path), [], False)
271             return False
272         return True
273
274     # if gnupg_homedir isn't set up, set it up
275     def init_gnupg(self):
276         if not os.path.exists(self.paths['dir']['gnupg_homedir']):
277             print _('Creating GnuPG homedir'), self.paths['dir']['gnupg_homedir']
278             if self.mkdir(self.paths['dir']['gnupg_homedir']):
279                 # import keys
280                 print _('Importing keys')
281                 p1 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['erinn_key']])
282                 p1.wait()
283                 p2 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['sebastian_key']])
284                 p2.wait()
285
286     # there are different GUIs that might appear, this sets which one we want
287     def set_gui(self, gui, message, tasks, autostart=True):
288         self.gui = gui
289         self.gui_message = message
290         self.gui_tasks = tasks
291         self.gui_task_i = 0
292         self.gui_autostart = autostart
293
294     # set all gtk variables to False
295     def clear_ui(self):
296         if hasattr(self, 'box'):
297             self.box.destroy()
298         self.box = False
299
300         self.label = False
301         self.progressbar = False
302         self.button_box = False
303         self.start_button = False
304         self.exit_button = False
305
306     # build the application's UI
307     def build_ui(self):
308         self.box = gtk.VBox(False, 20)
309         self.window.add(self.box)
310
311         if self.gui == 'error':
312             # labels
313             self.label = gtk.Label( self.gui_message ) 
314             self.label.set_line_wrap(True)
315             self.box.pack_start(self.label, True, True, 0)
316             self.label.show()
317
318             #self.label2 = gtk.Label("You can fix the problem by deleting:\n"+self.paths['dir']['data']+"\n\nHowever, you will lose all your bookmarks and other Tor Browser preferences.") 
319             #self.label2.set_line_wrap(True)
320             #self.box.pack_start(self.label2, True, True, 0)
321             #self.label2.show()
322
323             # exit button
324             exit_image = gtk.Image()
325             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
326             self.exit_button = gtk.Button("Exit")
327             self.exit_button.set_image(exit_image)
328             self.exit_button.connect("clicked", self.destroy, None)
329             self.box.add(self.exit_button)
330             self.exit_button.show()
331
332         elif self.gui == 'task':
333             # label
334             self.label = gtk.Label( self.gui_message ) 
335             self.label.set_line_wrap(True)
336             self.box.pack_start(self.label, True, True, 0)
337             self.label.show()
338             
339             # progress bar
340             self.progressbar = gtk.ProgressBar(adjustment=None)
341             self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
342             self.progressbar.set_pulse_step(0.01)
343             self.box.pack_start(self.progressbar, True, True, 0)
344
345             # button box
346             self.button_box = gtk.HButtonBox()
347             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
348             self.box.pack_start(self.button_box, True, True, 0)
349             self.button_box.show()
350
351             # start button
352             start_image = gtk.Image()
353             start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
354             self.start_button = gtk.Button("Start")
355             self.start_button.set_image(start_image)
356             self.start_button.connect("clicked", self.start, None)
357             self.button_box.add(self.start_button)
358             if not self.gui_autostart:
359               self.start_button.show()
360
361             # exit button
362             exit_image = gtk.Image()
363             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
364             self.exit_button = gtk.Button("Exit")
365             self.exit_button.set_image(exit_image)
366             self.exit_button.connect("clicked", self.destroy, None)
367             self.button_box.add(self.exit_button)
368             self.exit_button.show()
369
370         self.box.show()
371         self.window.show()
372
373         if self.gui_autostart:
374             self.start(None)
375
376     # start button clicked, begin tasks
377     def start(self, widget, data=None):
378         # disable the start button
379         if self.start_button:
380             self.start_button.set_sensitive(False)
381
382         # start running tasks
383         self.run_task()
384       
385     # run the next task in the task list
386     def run_task(self):
387         self.refresh_gtk()
388
389         if self.gui_task_i >= len(self.gui_tasks):
390             self.destroy(False)
391             return
392
393         task = self.gui_tasks[self.gui_task_i]
394         
395         # get ready for the next task
396         self.gui_task_i += 1
397
398         if task == 'download_update_check':
399             print _('Downloading'), self.paths['url']['update_check']
400             self.download('update check', self.paths['url']['update_check'], self.paths['file']['update_check'])
401         
402         if task == 'attempt_update':
403             print _('Checking to see if update it needed')
404             self.attempt_update()
405
406         elif task == 'download_tarball':
407             print _('Downloading'), self.paths['url']['tarball']
408             self.download('tarball', self.paths['url']['tarball'], self.paths['file']['tarball'])
409
410         elif task == 'download_tarball_sig':
411             print _('Downloading'), self.paths['url']['tarball_sig']
412             self.download('signature', self.paths['url']['tarball_sig'], self.paths['file']['tarball_sig'])
413
414         elif task == 'verify':
415             print _('Verifying signature')
416             self.verify()
417
418         elif task == 'extract':
419             print _('Extracting'), self.paths['filename']['tarball']
420             self.extract()
421
422         elif task == 'run':
423             print _('Running'), self.paths['file']['start']
424             self.run()
425         
426         elif task == 'start_over':
427             print _('Starting download over again')
428             self.start_over()
429
430     def response_received(self, response):
431         class FileDownloader(Protocol):
432             def __init__(self, file, total, progress, done_cb):
433                 self.file = file
434                 self.total = total
435                 self.so_far = 0
436                 self.progress = progress
437                 self.all_done = done_cb
438
439             def dataReceived(self, bytes):
440                 self.file.write(bytes)
441                 self.so_far += len(bytes)
442                 percent = float(self.so_far) / float(self.total)
443                 self.progress.set_fraction(percent)
444                 amount = float(self.so_far)
445                 units = "bytes"
446                 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
447                     if amount > size:
448                         units = unit
449                         amount = amount / float(size)
450                         break
451
452                 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
453
454             def connectionLost(self, reason):
455                 print _('Finished receiving body:'), reason.getErrorMessage()
456                 self.all_done(reason)
457
458         dl = FileDownloader(self.file_download, response.length, self.progressbar, self.response_finished)
459         response.deliverBody(dl)
460
461     def response_finished(self, msg):
462         if msg.check(ResponseDone):
463             self.file_download.close()
464             # next task!
465             self.run_task()
466
467         else:
468             print "FINISHED", msg
469             ## FIXME handle errors
470
471     def download_error(self, f):
472         print _("Download error"), f
473         self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
474         self.clear_ui()
475         self.build_ui()
476
477     def download(self, name, url, path):
478         # initialize the progress bar
479         self.progressbar.set_fraction(0) 
480         self.progressbar.set_text(_('Downloading {0}').format(name))
481         self.progressbar.show()
482         self.refresh_gtk()
483
484         agent = Agent(reactor, VerifyTorProjectCert(self.paths['file']['torproject_pem']))
485         d = agent.request('GET', url,
486                           Headers({'User-Agent': ['torbrowser-launcher']}),
487                           None)
488
489         self.file_download = open(path, 'w')
490         d.addCallback(self.response_received).addErrback(self.download_error)
491         
492         if not reactor.running:
493             reactor.run()
494
495     def attempt_update(self):
496         # load the update check file
497         try:
498             versions = json.load(open(self.paths['file']['update_check']))
499             latest_version = None
500
501             end = '-Linux'
502             for version in versions:
503                 if str(version).find(end) != -1:
504                     latest_version = str(version)
505
506             if latest_version:
507                 self.settings['latest_version'] = latest_version[:-len(end)]
508                 self.settings['last_update_check_timestamp'] = int(time.time())
509                 self.save_settings()
510                 self.build_paths(self.settings['latest_version'])
511                 self.start_launcher()
512
513             else:
514                 # failed to find the latest version
515                 self.set_gui('error', _("Error checking for updates."), [], False)
516         
517         except:
518             # not a valid JSON object
519             self.set_gui('error', _("Error checking for updates."), [], False)
520
521         # now start over
522         self.clear_ui()
523         self.build_ui()
524
525     def verify(self):
526         # initialize the progress bar
527         self.progressbar.set_fraction(0) 
528         self.progressbar.set_text(_('Verifying Signature'))
529         self.progressbar.show()
530
531         p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--verify', self.paths['file']['tarball_sig']])
532         self.pulse_until_process_exits(p)
533         
534         if p.returncode == 0:
535             self.run_task()
536         else:
537             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)
538             self.clear_ui()
539             self.build_ui()
540
541             if not reactor.running:
542                 reactor.run()
543
544     def extract(self):
545         # initialize the progress bar
546         self.progressbar.set_fraction(0) 
547         self.progressbar.set_text(_('Installing'))
548         self.progressbar.show()
549         self.refresh_gtk()
550
551         # make sure this file is a tarfile
552         if tarfile.is_tarfile(self.paths['file']['tarball']):
553           tf = tarfile.open(self.paths['file']['tarball'])
554           tf.extractall(self.paths['dir']['tbb'])
555         else:
556             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}"), ['start_over'], False)
557             self.clear_ui()
558             self.build_ui()
559
560         # installation is finished, so save installed_version
561         self.settings['installed_version'] = self.settings['latest_version']
562         self.save_settings()
563
564         self.run_task()
565
566     def run(self, run_next_task = True):
567         subprocess.Popen([self.paths['file']['start']])
568         if run_next_task:
569             self.run_task()
570
571     # make the progress bar pulse until process p (a Popen object) finishes
572     def pulse_until_process_exits(self, p):
573         while p.poll() == None:
574             time.sleep(0.01)
575             self.progressbar.pulse()
576             self.refresh_gtk()
577
578     # start over and download TBB again
579     def start_over(self):
580         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
581         self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
582         self.gui_task_i = 0
583         self.start(None)
584
585     # load settings
586     def load_settings(self):
587         if os.path.isfile(self.paths['file']['settings']):
588             self.settings = pickle.load(open(self.paths['file']['settings']))
589             # sanity checks
590             if not 'installed_version' in self.settings:
591                 return False
592             if not 'latest_version' in self.settings:
593                 return False
594             if not 'last_update_check_timestamp' in self.settings:
595                 return False
596         else:
597             self.settings = {
598                 'installed_version': False,
599                 'latest_version': '0',
600                 'last_update_check_timestamp': 0
601             }
602             self.save_settings()
603         return True
604
605     # save settings
606     def save_settings(self):
607         pickle.dump(self.settings, open(self.paths['file']['settings'], 'w'))
608         return True
609     
610     # refresh gtk
611     def refresh_gtk(self):
612         while gtk.events_pending():
613             gtk.main_iteration(False)
614
615     # exit
616     def delete_event(self, widget, event, data=None):
617         return False
618     def destroy(self, widget, data=None):
619         if hasattr(self, 'file_download'):
620             self.file_download.close()
621         if reactor.running:
622             reactor.stop()
623
624 if __name__ == "__main__":
625     tor_browser_launcher_version = '0.1-alpha'
626
627     print _('Tor Browser Launcher')
628     print _('By Micah Lee, licensed under GPLv3')
629     print _('version {0}').format(tor_browser_launcher_version)
630     print 'https://github.com/micahflee/torbrowser-launcher'
631
632     app = TorBrowserLauncher()
633