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