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