]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser_launcher/launcher.py
2c95352f9e212be222f8cf91663dfc079271d04f
[torbrowser-launcher.git] / torbrowser_launcher / launcher.py
1 """
2 Tor Browser Launcher
3 https://github.com/micahflee/torbrowser-launcher/
4
5 Copyright (c) 2013-2014 Micah Lee <micah@micahflee.com>
6
7 Permission is hereby granted, free of charge, to any person
8 obtaining a copy of this software and associated documentation
9 files (the "Software"), to deal in the Software without
10 restriction, including without limitation the rights to use,
11 copy, modify, merge, publish, distribute, sublicense, and/or sell
12 copies of the Software, and to permit persons to whom the
13 Software is furnished to do so, subject to the following
14 conditions:
15
16 The above copyright notice and this permission notice shall be
17 included in all copies or substantial portions of the Software.
18
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 OTHER DEALINGS IN THE SOFTWARE.
27 """
28
29 import os, subprocess, time, json, tarfile, hashlib, lzma, threading, re, unicodedata
30 from twisted.internet import reactor
31 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
32 from twisted.web.http_headers import Headers
33 from twisted.web.iweb import IPolicyForHTTPS
34 from twisted.internet.protocol import Protocol
35 from twisted.internet.error import DNSLookupError, ConnectionRefusedError
36
37 import xml.etree.ElementTree as ET
38
39 import OpenSSL
40
41 import pygtk
42 pygtk.require('2.0')
43 import gtk
44
45 class TryStableException(Exception):
46     pass
47
48 class TryDefaultMirrorException(Exception):
49     pass
50
51 class DownloadErrorException(Exception):
52     pass
53
54 class Launcher:
55     def __init__(self, common, url_list):
56         self.common = common
57         self.url_list = url_list
58
59         # this is the current version of Tor Browser, which should get updated with every release
60         self.min_version = '5.5.2'
61
62         # init launcher
63         self.set_gui(None, '', [])
64         self.launch_gui = True
65
66         # if Tor Browser is not installed, detect latest version, download, and install
67         if not self.common.settings['installed']:
68             # if downloading over Tor, include txsocksx
69             if self.common.settings['download_over_tor']:
70                 try:
71                     import txsocksx
72                     print _('Downloading over Tor')
73                 except ImportError:
74                     md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-txsocksx package is missing, downloads will not happen over tor"))
75                     md.set_position(gtk.WIN_POS_CENTER)
76                     md.run()
77                     md.destroy()
78                     self.common.settings['download_over_tor'] = False
79                     self.common.save_settings()
80
81             # download and install
82             print _("Downloading and installing Tor Browser for the first time.")
83             self.set_gui('task', _("Downloading and installing Tor Browser for the first time."),
84                          ['download_version_check',
85                           'set_version',
86                           'download_sig',
87                           'download_tarball',
88                           'verify',
89                           'extract',
90                           'run'])
91
92         else:
93             # Tor Browser is already installed, so run
94             self.run(False)
95             self.launch_gui = False
96
97         if self.launch_gui:
98             # build the rest of the UI
99             self.build_ui()
100
101     def configure_window(self):
102         if not hasattr(self, 'window'):
103             self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
104             self.window.set_title(_("Tor Browser"))
105             self.window.set_icon_from_file(self.common.paths['icon_file'])
106             self.window.set_position(gtk.WIN_POS_CENTER)
107             self.window.set_border_width(10)
108             self.window.connect("delete_event", self.delete_event)
109             self.window.connect("destroy", self.destroy)
110
111     # there are different GUIs that might appear, this sets which one we want
112     def set_gui(self, gui, message, tasks, autostart=True):
113         self.gui = gui
114         self.gui_message = message
115         self.gui_tasks = tasks
116         self.gui_task_i = 0
117         self.gui_autostart = autostart
118
119     # set all gtk variables to False
120     def clear_ui(self):
121         if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
122             self.box.destroy()
123         self.box = False
124
125         self.label = False
126         self.progressbar = False
127         self.button_box = False
128         self.start_button = False
129         self.exit_button = False
130
131     # build the application's UI
132     def build_ui(self):
133         self.clear_ui()
134
135         self.box = gtk.VBox(False, 20)
136         self.configure_window()
137         self.window.add(self.box)
138
139         if 'error' in self.gui:
140             # labels
141             self.label = gtk.Label(self.gui_message)
142             self.label.set_line_wrap(True)
143             self.box.pack_start(self.label, True, True, 0)
144             self.label.show()
145
146             # button box
147             self.button_box = gtk.HButtonBox()
148             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
149             self.box.pack_start(self.button_box, True, True, 0)
150             self.button_box.show()
151
152             if self.gui != 'error':
153                 # yes button
154                 yes_image = gtk.Image()
155                 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
156                 self.yes_button = gtk.Button("Yes")
157                 self.yes_button.set_image(yes_image)
158                 if self.gui == 'error_try_stable':
159                     self.yes_button.connect("clicked", self.try_stable, None)
160                 elif self.gui == 'error_try_default_mirror':
161                     self.yes_button.connect("clicked", self.try_default_mirror, None)
162                 elif self.gui == 'error_try_tor':
163                     self.yes_button.connect("clicked", self.try_tor, None)
164                 self.button_box.add(self.yes_button)
165                 self.yes_button.show()
166
167             # exit button
168             exit_image = gtk.Image()
169             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
170             self.exit_button = gtk.Button("Exit")
171             self.exit_button.set_image(exit_image)
172             self.exit_button.connect("clicked", self.destroy, None)
173             self.button_box.add(self.exit_button)
174             self.exit_button.show()
175
176         elif self.gui == 'task':
177             # label
178             self.label = gtk.Label(self.gui_message)
179             self.label.set_line_wrap(True)
180             self.box.pack_start(self.label, True, True, 0)
181             self.label.show()
182
183             # progress bar
184             self.progressbar = gtk.ProgressBar(adjustment=None)
185             self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
186             self.progressbar.set_pulse_step(0.01)
187             self.box.pack_start(self.progressbar, True, True, 0)
188
189             # button box
190             self.button_box = gtk.HButtonBox()
191             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
192             self.box.pack_start(self.button_box, True, True, 0)
193             self.button_box.show()
194
195             # start button
196             start_image = gtk.Image()
197             start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
198             self.start_button = gtk.Button(_("Start"))
199             self.start_button.set_image(start_image)
200             self.start_button.connect("clicked", self.start, None)
201             self.button_box.add(self.start_button)
202             if not self.gui_autostart:
203                 self.start_button.show()
204
205             # exit button
206             exit_image = gtk.Image()
207             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
208             self.exit_button = gtk.Button(_("Exit"))
209             self.exit_button.set_image(exit_image)
210             self.exit_button.connect("clicked", self.destroy, None)
211             self.button_box.add(self.exit_button)
212             self.exit_button.show()
213
214         self.box.show()
215         self.window.show()
216
217         if self.gui_autostart:
218             self.start(None)
219
220     # start button clicked, begin tasks
221     def start(self, widget, data=None):
222         # disable the start button
223         if self.start_button:
224             self.start_button.set_sensitive(False)
225
226         # start running tasks
227         self.run_task()
228
229     # run the next task in the task list
230     def run_task(self):
231         self.refresh_gtk()
232
233         if self.gui_task_i >= len(self.gui_tasks):
234             self.destroy(False)
235             return
236
237         task = self.gui_tasks[self.gui_task_i]
238
239         # get ready for the next task
240         self.gui_task_i += 1
241
242         if task == 'download_version_check':
243             print _('Downloading'), self.common.paths['version_check_url']
244             self.download('version check', self.common.paths['version_check_url'], self.common.paths['version_check_file'])
245
246         if task == 'set_version':
247             version = self.get_stable_version()
248             if version:
249                 self.common.build_paths(self.get_stable_version())
250                 print _('Latest version: {}').format(version)
251                 self.run_task()
252             else:
253                 self.set_gui('error', _("Error detecting Tor Browser version."), [], False)
254                 self.clear_ui()
255                 self.build_ui()
256
257         elif task == 'download_sig':
258             print _('Downloading'), self.common.paths['sig_url'].format(self.common.settings['mirror'])
259             self.download('signature', self.common.paths['sig_url'], self.common.paths['sig_file'])
260
261         elif task == 'download_tarball':
262             print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
263             self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
264
265         elif task == 'verify':
266             print _('Verifying signature')
267             self.verify()
268
269         elif task == 'extract':
270             print _('Extracting'), self.common.paths['tarball_filename']
271             self.extract()
272
273         elif task == 'run':
274             print _('Running'), self.common.paths['tbb']['start']
275             self.run()
276
277         elif task == 'start_over':
278             print _('Starting download over again')
279             self.start_over()
280
281     def response_received(self, response):
282         class FileDownloader(Protocol):
283             def __init__(self, common, file, url, total, progress, done_cb):
284                 self.file = file
285                 self.total = total
286                 self.so_far = 0
287                 self.progress = progress
288                 self.all_done = done_cb
289
290                 if response.code != 200:
291                     if common.settings['mirror'] != common.default_mirror:
292                         raise TryDefaultMirrorException(_("Download Error: {0} {1}\n\nYou are currently using a non-default mirror:\n{2}\n\nWould you like to switch back to the default?").format(response.code, response.phrase, common.settings['mirror']))
293                     else:
294                         raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
295
296             def dataReceived(self, bytes):
297                 self.file.write(bytes)
298                 self.so_far += len(bytes)
299                 percent = float(self.so_far) / float(self.total)
300                 self.progress.set_fraction(percent)
301                 amount = float(self.so_far)
302                 units = "bytes"
303                 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
304                     if amount > size:
305                         units = unit
306                         amount = amount / float(size)
307                         break
308
309                 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
310
311             def connectionLost(self, reason):
312                 self.all_done(reason)
313
314         if hasattr(self, 'current_download_url'):
315             url = self.current_download_url
316         else:
317             url = None
318
319         dl = FileDownloader(self.common, self.file_download, url, response.length, self.progressbar, self.response_finished)
320         response.deliverBody(dl)
321
322     def response_finished(self, msg):
323         if msg.check(ResponseDone):
324             self.file_download.close()
325             delattr(self, 'current_download_path')
326             delattr(self, 'current_download_url')
327
328             # next task!
329             self.run_task()
330
331         else:
332             print "FINISHED", msg
333             ## FIXME handle errors
334
335     def download_error(self, f):
336         print _("Download error:"), f.value, type(f.value)
337
338         if isinstance(f.value, TryStableException):
339             f.trap(TryStableException)
340             self.set_gui('error_try_stable', str(f.value), [], False)
341
342         elif isinstance(f.value, TryDefaultMirrorException):
343             f.trap(TryDefaultMirrorException)
344             self.set_gui('error_try_default_mirror', str(f.value), [], False)
345
346         elif isinstance(f.value, DownloadErrorException):
347             f.trap(DownloadErrorException)
348             self.set_gui('error', str(f.value), [], False)
349
350         elif isinstance(f.value, DNSLookupError):
351             f.trap(DNSLookupError)
352             if common.settings['mirror'] != common.default_mirror:
353                 self.set_gui('error_try_default_mirror', _("DNS Lookup Error\n\nYou are currently using a non-default mirror:\n{0}\n\nWould you like to switch back to the default?").format(common.settings['mirror']), [], False)
354             else:
355                 self.set_gui('error', str(f.value), [], False)
356
357         elif isinstance(f.value, ResponseFailed):
358             for reason in f.value.reasons:
359                 if isinstance(reason.value, OpenSSL.SSL.Error):
360                     # TODO: add the ability to report attack by posting bug to trac.torproject.org
361                     if not self.common.settings['download_over_tor']:
362                         self.set_gui('error_try_tor', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack. Try the download again using Tor?'), [], False)
363                     else:
364                         self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
365
366         elif isinstance(f.value, ConnectionRefusedError) and self.common.settings['download_over_tor']:
367             # If we're using Tor, we'll only get this error when we fail to
368             # connect to the SOCKS server.  If the connection fails at the
369             # remote end, we'll get txsocksx.errors.ConnectionRefused.
370             addr = self.common.settings['tor_socks_address']
371             self.set_gui('error', _("Error connecting to Tor at {0}").format(addr), [], False)
372
373         else:
374             self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
375
376         self.build_ui()
377
378     def download(self, name, url, path):
379         # keep track of current download
380         self.current_download_path = path
381         self.current_download_url = url
382
383         mirror_url = url.format(self.common.settings['mirror'])
384
385         # convert mirror_url from unicode to string, if needed (#205)
386         if isinstance(mirror_url, unicode):
387             mirror_url = unicodedata.normalize('NFKD', mirror_url).encode('ascii','ignore')
388
389         # initialize the progress bar
390         self.progressbar.set_fraction(0)
391         self.progressbar.set_text(_('Downloading {0}').format(name))
392         self.progressbar.show()
393         self.refresh_gtk()
394
395         if self.common.settings['download_over_tor']:
396             from twisted.internet.endpoints import clientFromString
397             from txsocksx.http import SOCKS5Agent
398
399             torEndpoint = clientFromString(reactor, self.common.settings['tor_socks_address'])
400
401             # default mirror gets certificate pinning, only for requests that use the mirror
402             agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
403         else:
404             agent = Agent(reactor)
405
406         # actually, agent needs to follow redirect
407         agent = RedirectAgent(agent)
408
409         # start the request
410         d = agent.request('GET', mirror_url,
411                           Headers({'User-Agent': ['torbrowser-launcher']}),
412                           None)
413
414         self.file_download = open(path, 'w')
415         d.addCallback(self.response_received).addErrback(self.download_error)
416
417         if not reactor.running:
418             reactor.run()
419
420     def try_default_mirror(self, widget, data=None):
421         # change mirror to default and relaunch TBL
422         self.common.settings['mirror'] = self.common.default_mirror
423         self.common.save_settings()
424         subprocess.Popen([self.common.paths['tbl_bin']])
425         self.destroy(False)
426
427     def try_tor(self, widget, data=None):
428         # set download_over_tor to true and relaunch TBL
429         self.common.settings['download_over_tor'] = True
430         self.common.save_settings()
431         subprocess.Popen([self.common.paths['tbl_bin']])
432         self.destroy(False)
433
434     def get_stable_version(self):
435         tree = ET.parse(self.common.paths['version_check_file'])
436         for up in tree.getroot():
437             if up.tag == 'update' and up.attrib['appVersion']:
438                 return str(up.attrib['appVersion'])
439         return None
440
441     def verify(self):
442         # initialize the progress bar
443         self.progressbar.set_fraction(0)
444         self.progressbar.set_text(_('Verifying Signature'))
445         self.progressbar.show()
446
447         # verify the PGP signature
448         verified = False
449         FNULL = open(os.devnull, 'w')
450         p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sig_file']], stdout=FNULL, stderr=subprocess.STDOUT)
451         self.pulse_until_process_exits(p)
452         if p.returncode == 0:
453             verified = True
454
455         if verified:
456             self.run_task()
457         else:
458             # TODO: add the ability to report attack by posting bug to trac.torproject.org
459             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)
460             self.clear_ui()
461             self.build_ui()
462
463             if not reactor.running:
464                 reactor.run()
465
466     def extract(self):
467         # initialize the progress bar
468         self.progressbar.set_fraction(0)
469         self.progressbar.set_text(_('Installing'))
470         self.progressbar.show()
471         self.refresh_gtk()
472
473         extracted = False
474         try:
475             if self.common.paths['tarball_file'][-2:] == 'xz':
476                 # if tarball is .tar.xz
477                 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
478                 tf = tarfile.open(fileobj=xz)
479                 tf.extractall(self.common.paths['tbb']['dir'])
480                 extracted = True
481             else:
482                 # if tarball is .tar.gz
483                 if tarfile.is_tarfile(self.common.paths['tarball_file']):
484                     tf = tarfile.open(self.common.paths['tarball_file'])
485                     tf.extractall(self.common.paths['tbb']['dir'])
486                     extracted = True
487         except:
488             pass
489
490         if not extracted:
491             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
492             self.clear_ui()
493             self.build_ui()
494             return
495
496         self.run_task()
497
498     def check_min_version(self):
499         installed_version = None
500         for line in open(self.common.paths['tbb']['versions']).readlines():
501             if line.startswith('TORBROWSER_VERSION='):
502                 installed_version = line.split('=')[1].strip()
503                 break
504
505         if self.min_version <= installed_version:
506             return True
507
508         return False
509
510     def run(self, run_next_task=True):
511         # don't run if it isn't at least the minimum version
512         if not self.check_min_version():
513             message =  _("The version of Tor Browser you have installed is earlier than it should be, which could be a sign of an attack!")
514             print message
515
516             md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _(message))
517             md.set_position(gtk.WIN_POS_CENTER)
518             md.run()
519             md.destroy()
520
521             return
522
523         # play modem sound?
524         if self.common.settings['modem_sound']:
525             def play_modem_sound():
526                 try:
527                     import pygame
528                     pygame.mixer.init()
529                     sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
530                     sound.play()
531                     time.sleep(10)
532                 except ImportError:
533                     md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-pygame package is missing, the modem sound is unavailable."))
534                     md.set_position(gtk.WIN_POS_CENTER)
535                     md.run()
536                     md.destroy()
537
538             t = threading.Thread(target=play_modem_sound)
539             t.start()
540
541         # hide the TBL window (#151)
542         if hasattr(self, 'window'):
543             self.window.hide()
544             while gtk.events_pending():
545                 gtk.main_iteration_do(True)
546
547         # run Tor Browser
548         subprocess.call([self.common.paths['tbb']['start']], cwd=self.common.paths['tbb']['dir_tbb'])
549
550         if run_next_task:
551             self.run_task()
552
553     # make the progress bar pulse until process p (a Popen object) finishes
554     def pulse_until_process_exits(self, p):
555         while p.poll() is None:
556             time.sleep(0.01)
557             self.progressbar.pulse()
558             self.refresh_gtk()
559
560     # start over and download TBB again
561     def start_over(self):
562         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
563         self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
564         self.gui_task_i = 0
565         self.start(None)
566
567     # refresh gtk
568     def refresh_gtk(self):
569         while gtk.events_pending():
570             gtk.main_iteration(False)
571
572     # exit
573     def delete_event(self, widget, event, data=None):
574         return False
575
576     def destroy(self, widget, data=None):
577         if hasattr(self, 'file_download'):
578             self.file_download.close()
579         if hasattr(self, 'current_download_path'):
580             os.remove(self.current_download_path)
581             delattr(self, 'current_download_path')
582             delattr(self, 'current_download_url')
583         if reactor.running:
584             reactor.stop()