]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser-launcher
made #17 work with old versions of psutils as well, like what comes with debian squeeze
[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                 p2 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['sebastian_key']])
283                 # wait for keys to import before moving on
284                 p1.wait()
285                 p2.wait()
286
287     # there are different GUIs that might appear, this sets which one we want
288     def set_gui(self, gui, message, tasks, autostart=True):
289         self.gui = gui
290         self.gui_message = message
291         self.gui_tasks = tasks
292         self.gui_task_i = 0
293         self.gui_autostart = autostart
294
295     # set all gtk variables to False
296     def clear_ui(self):
297         if hasattr(self, 'box'):
298             self.box.destroy()
299         self.box = False
300
301         self.label = False
302         self.progressbar = False
303         self.button_box = False
304         self.start_button = False
305         self.exit_button = False
306
307     # build the application's UI
308     def build_ui(self):
309         self.box = gtk.VBox(False, 20)
310         self.window.add(self.box)
311
312         if self.gui == 'error':
313             # labels
314             self.label = gtk.Label( self.gui_message ) 
315             self.label.set_line_wrap(True)
316             self.box.pack_start(self.label, True, True, 0)
317             self.label.show()
318
319             #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.") 
320             #self.label2.set_line_wrap(True)
321             #self.box.pack_start(self.label2, True, True, 0)
322             #self.label2.show()
323
324             # exit button
325             exit_image = gtk.Image()
326             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
327             self.exit_button = gtk.Button("Exit")
328             self.exit_button.set_image(exit_image)
329             self.exit_button.connect("clicked", self.destroy, None)
330             self.box.add(self.exit_button)
331             self.exit_button.show()
332
333         elif self.gui == 'task':
334             # label
335             self.label = gtk.Label( self.gui_message ) 
336             self.label.set_line_wrap(True)
337             self.box.pack_start(self.label, True, True, 0)
338             self.label.show()
339             
340             # progress bar
341             self.progressbar = gtk.ProgressBar(adjustment=None)
342             self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
343             self.progressbar.set_pulse_step(0.01)
344             self.box.pack_start(self.progressbar, True, True, 0)
345
346             # button box
347             self.button_box = gtk.HButtonBox()
348             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
349             self.box.pack_start(self.button_box, True, True, 0)
350             self.button_box.show()
351
352             # start button
353             start_image = gtk.Image()
354             start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
355             self.start_button = gtk.Button("Start")
356             self.start_button.set_image(start_image)
357             self.start_button.connect("clicked", self.start, None)
358             self.button_box.add(self.start_button)
359             if not self.gui_autostart:
360               self.start_button.show()
361
362             # exit button
363             exit_image = gtk.Image()
364             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
365             self.exit_button = gtk.Button("Exit")
366             self.exit_button.set_image(exit_image)
367             self.exit_button.connect("clicked", self.destroy, None)
368             self.button_box.add(self.exit_button)
369             self.exit_button.show()
370
371         self.box.show()
372         self.window.show()
373
374         if self.gui_autostart:
375             self.start(None)
376
377     # start button clicked, begin tasks
378     def start(self, widget, data=None):
379         # disable the start button
380         if self.start_button:
381             self.start_button.set_sensitive(False)
382
383         # start running tasks
384         self.run_task()
385       
386     # run the next task in the task list
387     def run_task(self):
388         self.refresh_gtk()
389
390         if self.gui_task_i >= len(self.gui_tasks):
391             self.destroy(False)
392             return
393
394         task = self.gui_tasks[self.gui_task_i]
395         
396         # get ready for the next task
397         self.gui_task_i += 1
398
399         if task == 'download_update_check':
400             print _('Downloading'), self.paths['url']['update_check']
401             self.download('update check', self.paths['url']['update_check'], self.paths['file']['update_check'])
402         
403         if task == 'attempt_update':
404             print _('Checking to see if update it needed')
405             self.attempt_update()
406
407         elif task == 'download_tarball':
408             print _('Downloading'), self.paths['url']['tarball']
409             self.download('tarball', self.paths['url']['tarball'], self.paths['file']['tarball'])
410
411         elif task == 'download_tarball_sig':
412             print _('Downloading'), self.paths['url']['tarball_sig']
413             self.download('signature', self.paths['url']['tarball_sig'], self.paths['file']['tarball_sig'])
414
415         elif task == 'verify':
416             print _('Verifying signature')
417             self.verify()
418
419         elif task == 'extract':
420             print _('Extracting'), self.paths['filename']['tarball']
421             self.extract()
422
423         elif task == 'run':
424             print _('Running'), self.paths['file']['start']
425             self.run()
426         
427         elif task == 'start_over':
428             print _('Starting download over again')
429             self.start_over()
430
431     def response_received(self, response):
432         class FileDownloader(Protocol):
433             def __init__(self, file, total, progress, done_cb):
434                 self.file = file
435                 self.total = total
436                 self.so_far = 0
437                 self.progress = progress
438                 self.all_done = done_cb
439
440             def dataReceived(self, bytes):
441                 self.file.write(bytes)
442                 self.so_far += len(bytes)
443                 percent = float(self.so_far) / float(self.total)
444                 self.progress.set_fraction(percent)
445                 amount = float(self.so_far)
446                 units = "bytes"
447                 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
448                     if amount > size:
449                         units = unit
450                         amount = amount / float(size)
451                         break
452
453                 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
454
455             def connectionLost(self, reason):
456                 print _('Finished receiving body:'), reason.getErrorMessage()
457                 self.all_done(reason)
458
459         dl = FileDownloader(self.file_download, response.length, self.progressbar, self.response_finished)
460         response.deliverBody(dl)
461
462     def response_finished(self, msg):
463         if msg.check(ResponseDone):
464             self.file_download.close()
465             # next task!
466             self.run_task()
467
468         else:
469             print "FINISHED", msg
470             ## FIXME handle errors
471
472     def download_error(self, f):
473         print _("Download error"), f
474         self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
475         self.clear_ui()
476         self.build_ui()
477
478     def download(self, name, url, path):
479         # initialize the progress bar
480         self.progressbar.set_fraction(0) 
481         self.progressbar.set_text(_('Downloading {0}').format(name))
482         self.progressbar.show()
483         self.refresh_gtk()
484
485         agent = Agent(reactor, VerifyTorProjectCert(self.paths['file']['torproject_pem']))
486         d = agent.request('GET', url,
487                           Headers({'User-Agent': ['torbrowser-launcher']}),
488                           None)
489
490         self.file_download = open(path, 'w')
491         d.addCallback(self.response_received).addErrback(self.download_error)
492         
493         if not reactor.running:
494             reactor.run()
495
496     def attempt_update(self):
497         # load the update check file
498         try:
499             versions = json.load(open(self.paths['file']['update_check']))
500             latest_version = None
501
502             end = '-Linux'
503             for version in versions:
504                 if str(version).find(end) != -1:
505                     latest_version = str(version)
506
507             if latest_version:
508                 self.settings['latest_version'] = latest_version[:-len(end)]
509                 self.settings['last_update_check_timestamp'] = int(time.time())
510                 self.save_settings()
511                 self.build_paths(self.settings['latest_version'])
512                 self.start_launcher()
513
514             else:
515                 # failed to find the latest version
516                 self.set_gui('error', _("Error checking for updates."), [], False)
517         
518         except:
519             # not a valid JSON object
520             self.set_gui('error', _("Error checking for updates."), [], False)
521
522         # now start over
523         self.clear_ui()
524         self.build_ui()
525
526     def verify(self):
527         # initialize the progress bar
528         self.progressbar.set_fraction(0) 
529         self.progressbar.set_text(_('Verifying Signature'))
530         self.progressbar.show()
531
532         p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--verify', self.paths['file']['tarball_sig']])
533         self.pulse_until_process_exits(p)
534         
535         if p.returncode == 0:
536             self.run_task()
537         else:
538             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)
539             self.clear_ui()
540             self.build_ui()
541
542             if not reactor.running:
543                 reactor.run()
544
545     def extract(self):
546         # initialize the progress bar
547         self.progressbar.set_fraction(0) 
548         self.progressbar.set_text(_('Installing'))
549         self.progressbar.show()
550         self.refresh_gtk()
551
552         # make sure this file is a tarfile
553         if tarfile.is_tarfile(self.paths['file']['tarball']):
554           tf = tarfile.open(self.paths['file']['tarball'])
555           tf.extractall(self.paths['dir']['tbb'])
556         else:
557             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}"), ['start_over'], False)
558             self.clear_ui()
559             self.build_ui()
560
561         # installation is finished, so save installed_version
562         self.settings['installed_version'] = self.settings['latest_version']
563         self.save_settings()
564
565         self.run_task()
566
567     def run(self, run_next_task = True):
568         subprocess.Popen([self.paths['file']['start']])
569         if run_next_task:
570             self.run_task()
571
572     # make the progress bar pulse until process p (a Popen object) finishes
573     def pulse_until_process_exits(self, p):
574         while p.poll() == None:
575             time.sleep(0.01)
576             self.progressbar.pulse()
577             self.refresh_gtk()
578
579     # start over and download TBB again
580     def start_over(self):
581         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
582         self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
583         self.gui_task_i = 0
584         self.start(None)
585
586     # load settings
587     def load_settings(self):
588         if os.path.isfile(self.paths['file']['settings']):
589             self.settings = pickle.load(open(self.paths['file']['settings']))
590             # sanity checks
591             if not 'installed_version' in self.settings:
592                 return False
593             if not 'latest_version' in self.settings:
594                 return False
595             if not 'last_update_check_timestamp' in self.settings:
596                 return False
597         else:
598             self.settings = {
599                 'installed_version': False,
600                 'latest_version': '0',
601                 'last_update_check_timestamp': 0
602             }
603             self.save_settings()
604         return True
605
606     # save settings
607     def save_settings(self):
608         pickle.dump(self.settings, open(self.paths['file']['settings'], 'w'))
609         return True
610     
611     # refresh gtk
612     def refresh_gtk(self):
613         while gtk.events_pending():
614             gtk.main_iteration(False)
615
616     # exit
617     def delete_event(self, widget, event, data=None):
618         return False
619     def destroy(self, widget, data=None):
620         if hasattr(self, 'file_download'):
621             self.file_download.close()
622         if reactor.running:
623             reactor.stop()
624
625 if __name__ == "__main__":
626     tor_browser_launcher_version = '0.1'
627
628     print _('Tor Browser Launcher')
629     print _('version {0}').format(tor_browser_launcher_version)
630     print 'https://github.com/micahflee/torbrowser-launcher'
631
632     app = TorBrowserLauncher()
633