#!/usr/bin/env python
"""
Tor Browser Launcher
https://github.com/micahflee/torbrowser-launcher/

Copyright (c) 2013-2014 Micah Lee <micah@micahflee.com>

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
"""

import os
SHARE = os.getenv('TBL_SHARE', '/usr/share')
import sys
import platform

import gettext
gettext.install('torbrowser-launcher', os.path.join(SHARE, 'torbrowser-launcher/locale'))

from twisted.internet import gtk2reactor
gtk2reactor.install()
from twisted.internet import reactor

import pygtk
pygtk.require('2.0')
import gtk

import subprocess, locale, time, pickle, json, tarfile, psutil, hashlib, lzma

from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
from twisted.web.http_headers import Headers
from twisted.internet.protocol import Protocol
from twisted.internet.ssl import ClientContextFactory
from twisted.internet.error import DNSLookupError

import OpenSSL


class TryStableException(Exception):
    pass


class TryDefaultMirrorException(Exception):
    pass


class DownloadErrorException(Exception):
    pass


class VerifyTorProjectCert(ClientContextFactory):

    def __init__(self, torproject_pem):
        self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())

    def getContext(self, host, port):
        ctx = ClientContextFactory.getContext(self)
        ctx.set_verify_depth(0)
        ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
        return ctx

    def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
        return cert.digest('sha256') == self.torproject_ca.digest('sha256')


class TBLCommon:

    def __init__(self, tbl_version):
        print _('Initializing Tor Browser Launcher')
        self.tbl_version = tbl_version

        # initialize the app
        self.default_mirror = 'https://www.torproject.org/dist/'
        self.discover_arch_lang()
        self.build_paths()
        for d in self.paths['dirs']:
            self.mkdir(self.paths['dirs'][d])
        self.load_mirrors()
        self.load_settings()
        self.mkdir(self.paths['download_dir'])
        self.mkdir(self.paths['tbb']['dir'])
        self.init_gnupg()

        # allow buttons to have icons
        try:
            gtk_settings = gtk.settings_get_default()
            gtk_settings.props.gtk_button_images = True
        except:
            pass

    # discover the architecture and language
    def discover_arch_lang(self):
        # figure out the architecture
        self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'

        # figure out the language
        available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
        default_locale = locale.getdefaultlocale()[0]
        if default_locale is None:
            self.language = 'en-US'
        else:
            self.language = default_locale.replace('_', '-')
            if self.language not in available_languages:
                self.language = self.language.split('-')[0]
                if self.language not in available_languages:
                    for l in available_languages:
                        if l[0:2] == self.language:
                            self.language = l
            # if language isn't available, default to english
            if self.language not in available_languages:
                self.language = 'en-US'

    # build all relevant paths
    def build_paths(self, tbb_version=None):
        homedir = os.getenv('HOME')
        if not homedir:
            homedir = '/tmp/.torbrowser-'+os.getenv('USER')
            if not os.path.exists(homedir):
                try:
                    os.mkdir(homedir, 0700)
                except:
                    self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
        if not os.access(homedir, os.W_OK):
            self.set_gui('error', _("{0} is not writable").format(homedir), [], False)

        tbb_config = '{0}/.config/torbrowser'.format(homedir)
        tbb_cache = '{0}/.cache/torbrowser'.format(homedir)
        tbb_local = '{0}/.local/share/torbrowser'.format(homedir)
        old_tbb_data = '{0}/.torbrowser'.format(homedir)

        if tbb_version:
            # tarball filename
            if self.architecture == 'x86_64':
                arch = 'linux64'
            else:
                arch = 'linux32'
            tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'

            # tarball
            self.paths['tarball_url'] = '{0}torbrowser/'+tbb_version+'/'+tarball_filename
            self.paths['tarball_file'] = tbb_cache+'/download/'+tarball_filename
            self.paths['tarball_filename'] = tarball_filename

            # sig
            self.paths['sha256_file'] = tbb_cache+'/download/sha256sums.txt'
            self.paths['sha256_sig_file'] = tbb_cache+'/download/sha256sums.txt.asc'
            self.paths['sha256_url'] = '{0}torbrowser/'+tbb_version+'/sha256sums.txt'
            self.paths['sha256_sig_url'] = '{0}torbrowser/'+tbb_version+'/sha256sums.txt-mikeperry.asc'
        else:
            self.paths = {
                'dirs': {
                    'config': tbb_config,
                    'cache': tbb_cache,
                    'local': tbb_local,
                },
                'old_data_dir': old_tbb_data,
                'tbl_bin': '/usr/bin/torbrowser-launcher',
                'icon_file': os.path.join(SHARE, 'pixmaps/torbrowser80.xpm'),
                'torproject_pem': os.path.join(SHARE, 'torbrowser-launcher/torproject.pem'),
                'mike_key': os.path.join(SHARE, 'torbrowser-launcher/mike-2013-09.asc'),
                'mirrors_txt': [os.path.join(SHARE, 'torbrowser-launcher/mirrors.txt'),
                                '/usr/local/share/torbrowser-launcher/mirrors.txt'],
                'modem_sound': os.path.join(SHARE, 'torbrowser-launcher/modem.ogg'),
                'download_dir': tbb_cache+'/download',
                'gnupg_homedir': tbb_local+'/gnupg_homedir',
                'settings_file': tbb_config+'/settings',
                'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
                'update_check_file': tbb_cache+'/download/RecommendedTBBVersions',
                'tbb': {
                    'dir': tbb_local+'/tbb/'+self.architecture,
                    'start': tbb_local+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
                    'versions': tbb_local+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
                },
            }

    # create a directory
    @staticmethod
    def mkdir(path):
        try:
            if not os.path.exists(path):
                os.makedirs(path, 0700)
                return True
        except:
            print _("Cannot create directory {0}").format(path)
            return False
        if not os.access(path, os.W_OK):
            print _("{0} is not writable").format(path)
            return False
        return True

    # if gnupg_homedir isn't set up, set it up
    def init_gnupg(self):
        if not os.path.exists(self.paths['gnupg_homedir']):
            print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
            self.mkdir(self.paths['gnupg_homedir'])
        self.import_keys()

    # import gpg keys
    def import_keys(self):
        print _('Importing keys')
        subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['mike_key']]).wait()

    # load mirrors
    def load_mirrors(self):
        self.mirrors = []
        for srcfile in self.paths['mirrors_txt']:
            if os.path.exists(srcfile):
                print "Successfully loaded mirrors from %s" % srcfile
            elif not os.path.exists(srcfile):
                print "Warning: can't load mirrors from %s" % srcfile
                continue
            for mirror in open(srcfile, 'r').readlines():
                if mirror.strip() not in self.mirrors:
                    self.mirrors.append(mirror.strip())

    # load settings
    def load_settings(self):
        default_settings = {
            'tbl_version': self.tbl_version,
            'installed_version': False,
            'latest_version': '0',
            'update_over_tor': True,
            'check_for_updates': False,
            'modem_sound': False,
            'last_update_check_timestamp': 0,
            'mirror': self.default_mirror
        }

        if os.path.isfile(self.paths['settings_file']):
            settings = pickle.load(open(self.paths['settings_file']))
            resave = False

            # settings migrations
            if settings['tbl_version'] <= '0.1.0':
                print '0.1.0 migration'
                settings['installed_version'] = settings['installed_version']['stable']
                settings['latest_version'] = settings['latest_version']['stable']
                resave = True

                # make new tbb folder
                self.mkdir(self.paths['tbb']['dir'])
                old_tbb_dir = self.paths['old_data_dir']+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language
                new_tbb_dir = self.paths['tbb']['dir']+'/tor-browser_'+self.language
                if os.path.isdir(old_tbb_dir):
                    os.rename(old_tbb_dir, new_tbb_dir)

            # make sure settings file is up-to-date
            for setting in default_settings:
                if setting not in settings:
                    settings[setting] = default_settings[setting]
                    resave = True

            # make sure the version is current
            if settings['tbl_version'] != self.tbl_version:
                settings['tbl_version'] = self.tbl_version
                resave = True

            self.settings = settings
            if resave:
                self.save_settings()

        else:
            self.settings = default_settings
            self.save_settings()

    # save settings
    def save_settings(self):
        pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
        return True

    # get the process id of a program
    @staticmethod
    def get_pid(bin_path, python=False):
        pid = None

        for p in psutil.process_iter():
            try:
                if p.pid != os.getpid():
                    exe = None
                    if python:
                        if len(p.cmdline) > 1:
                            if 'python' in p.cmdline[0]:
                                exe = p.cmdline[1]
                    else:
                        if len(p.cmdline) > 0:
                            exe = p.cmdline[0]

                    if exe == bin_path:
                        pid = p.pid

            except:
                pass

        return pid

    # bring program's x window to front
    @staticmethod
    def bring_window_to_front(pid):
        # figure out the window id
        win_id = None
        p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
        for line in p.stdout.readlines():
            line_split = line.split()
            cur_win_id = line_split[0]
            cur_win_pid = int(line_split[2])
            if cur_win_pid == pid:
                win_id = cur_win_id

        # bring to front
        if win_id:
            subprocess.call(['wmctrl', '-i', '-a', win_id])


class TBLSettings:
    def __init__(self, common):
        print _('Starting settings dialog')
        self.common = common

        # set up the window
        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.window.set_title(_("Tor Browser Launcher Settings"))
        self.window.set_icon_from_file(self.common.paths['icon_file'])
        self.window.set_position(gtk.WIN_POS_CENTER)
        self.window.set_border_width(10)
        self.window.connect("delete_event", self.delete_event)
        self.window.connect("destroy", self.destroy)

        # build the rest of the UI
        self.box = gtk.VBox(False, 10)
        self.window.add(self.box)
        self.box.show()

        self.hbox = gtk.HBox(False, 10)
        self.box.pack_start(self.hbox, True, True, 0)
        self.hbox.show()

        self.settings_box = gtk.VBox(False, 10)
        self.hbox.pack_start(self.settings_box, True, True, 0)
        self.settings_box.show()

        self.labels_box = gtk.VBox(False, 10)
        self.hbox.pack_start(self.labels_box, True, True, 0)
        self.labels_box.show()

        # download over tor
        try:
            import txsocksx
            self.txsocks_found = True
        except ImportError:
            self.txsocks_found = False
        self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
        if self.txsocks_found:
            self.tor_update_checkbox.set_tooltip_text(_("This option is only available when using a system wide Tor installation."))
        else:
            self.tor_update_checkbox.set_tooltip_text(_("This option requires the python-txsocksx package."))

        self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
        if self.common.settings['update_over_tor'] and self.txsocks_found:
            self.tor_update_checkbox.set_active(True)
        else:
            self.tor_update_checkbox.set_active(False)

        if self.txsocks_found == False:
            self.tor_update_checkbox.set_sensitive(False)

        self.tor_update_checkbox.show()

        # check for updates
        self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
        self.settings_box.pack_start(self.update_checkbox, True, True, 0)
        if self.common.settings['check_for_updates']:
            self.update_checkbox.set_active(True)
        else:
            self.update_checkbox.set_active(False)
        self.update_checkbox.show()

        # modem sound
        self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
        self.settings_box.pack_start(self.modem_checkbox, True, True, 0)

        try:
            import pygame
            if self.common.settings['modem_sound']:
                self.modem_checkbox.set_active(True)
            else:
                self.modem_checkbox.set_active(False)
        except ImportError:
            self.modem_checkbox.set_active(False)
            self.modem_checkbox.set_sensitive(False)
            self.modem_checkbox.set_tooltip_text(_("This option requires python-pygame to be installed"))
        self.modem_checkbox.show()

        # labels
        if(self.common.settings['installed_version']):
            self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version']))
        else:
            self.label1 = gtk.Label(_('Not installed'))
        self.label1.set_line_wrap(True)
        self.labels_box.pack_start(self.label1, True, True, 0)
        self.label1.show()

        if(self.common.settings['last_update_check_timestamp'] > 0):
            self.label1 = gtk.Label(_('Last checked for updates:\n{0}').format(time.strftime("%B %d, %Y %I:%M %P", time.gmtime(self.common.settings['last_update_check_timestamp']))))
        else:
            self.label1 = gtk.Label(_('Never checked for updates'))
        self.label1.set_line_wrap(True)
        self.labels_box.pack_start(self.label1, True, True, 0)
        self.label1.show()

        # mirrors
        self.mirrors_box = gtk.HBox(False, 10)
        self.box.pack_start(self.mirrors_box, True, True, 0)
        self.mirrors_box.show()

        self.mirrors_label = gtk.Label(_('Mirror'))
        self.mirrors_label.set_line_wrap(True)
        self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
        self.mirrors_label.show()

        self.mirrors = gtk.combo_box_new_text()
        for mirror in self.common.mirrors:
            self.mirrors.append_text(mirror)
        if self.common.settings['mirror'] in self.common.mirrors:
            self.mirrors.set_active(self.common.mirrors.index(self.common.settings['mirror']))
        else:
            self.mirrors.set_active(0)
        self.mirrors_box.pack_start(self.mirrors, True, True, 0)
        self.mirrors.show()

        # button box
        self.button_box = gtk.HButtonBox()
        self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
        self.box.pack_start(self.button_box, True, True, 0)
        self.button_box.show()

        # save and launch button
        save_launch_image = gtk.Image()
        save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
        self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
        self.save_launch_button.set_image(save_launch_image)
        self.save_launch_button.connect("clicked", self.save_launch, None)
        self.button_box.add(self.save_launch_button)
        self.save_launch_button.show()

        # save and exit button
        save_exit_image = gtk.Image()
        save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
        self.save_exit_button = gtk.Button(_("Save & Exit"))
        self.save_exit_button.set_image(save_exit_image)
        self.save_exit_button.connect("clicked", self.save_exit, None)
        self.button_box.add(self.save_exit_button)
        self.save_exit_button.show()

        # cancel button
        cancel_image = gtk.Image()
        cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
        self.cancel_button = gtk.Button(_("Cancel"))
        self.cancel_button.set_image(cancel_image)
        self.cancel_button.connect("clicked", self.destroy, None)
        self.button_box.add(self.cancel_button)
        self.cancel_button.show()

        # show the window
        self.window.show()

        # start gtk
        gtk.main()

    # UI Callback for update over tor/use system tor
    def on_system_tor_clicked(self, event):
        if self.txsocks_found:
            value = self.system_tor_checkbox.get_active()
        else:
            value = False

        self.tor_update_checkbox.set_active(value)
        self.tor_update_checkbox.set_sensitive(value)

    # save and launch
    def save_launch(self, widget, data=None):
        self.save()
        subprocess.Popen([self.common.paths['tbl_bin']])
        self.destroy(False)

    # save and exit
    def save_exit(self, widget, data=None):
        self.save()
        self.destroy(False)

    # save settings
    def save(self):
        # checkbox options
        self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
        self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
        self.common.settings['modem_sound'] = self.modem_checkbox.get_active()

        # figure out the selected mirror
        self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]

        # save them
        self.common.save_settings()

    # exit
    def delete_event(self, widget, event, data=None):
        return False

    def destroy(self, widget, data=None):
        gtk.main_quit()


class TBLLauncher:
    def __init__(self, common):
        print _('Starting launcher dialog')
        self.common = common

        # init launcher
        self.set_gui(None, '', [])
        self.launch_gui = True
        print "LATEST VERSION", self.common.settings['latest_version']
        self.common.build_paths(self.common.settings['latest_version'])

        if self.common.settings['update_over_tor']:
            try:
                import txsocksx
            except ImportError:
                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"))
                md.set_position(gtk.WIN_POS_CENTER)
                md.run()
                md.destroy()
                self.common.settings['update_over_tor'] = False
                self.common.save_settings()

        # is firefox already running?
        if self.common.settings['installed_version']:
            firefox_pid = self.common.get_pid('./Browser/firefox')
            if firefox_pid:
                print _('Firefox are is open, bringing to focus')
                # bring firefox to front
                self.common.bring_window_to_front(firefox_pid)
                return

        # check for updates?
        check_for_updates = False
        if self.common.settings['check_for_updates']:
            check_for_updates = True

        if not check_for_updates:
            # how long was it since the last update check?
            # 86400 seconds = 24 hours
            current_timestamp = int(time.time())
            if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
                check_for_updates = True

        if check_for_updates:
            # check for update
            print 'Checking for update'
            self.set_gui('task', _("Checking for Tor Browser update."),
                         ['download_update_check',
                          'attempt_update'])
        else:
            # no need to check for update
            print _('Checked for update within 24 hours, skipping')
            self.start_launcher()

        if self.launch_gui:
            # set up the window
            self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
            self.window.set_title(_("Tor Browser"))
            self.window.set_icon_from_file(self.common.paths['icon_file'])
            self.window.set_position(gtk.WIN_POS_CENTER)
            self.window.set_border_width(10)
            self.window.connect("delete_event", self.delete_event)
            self.window.connect("destroy", self.destroy)

            # build the rest of the UI
            self.build_ui()

    # download or run TBB
    def start_launcher(self):
        # is TBB already installed?
        latest_version = self.common.settings['latest_version']
        installed_version = self.common.settings['installed_version']

        # verify installed version for newer versions of TBB (#58)
        if installed_version >= '3.0':
            versions_filename = self.common.paths['tbb']['versions']
            if os.path.exists(versions_filename):
                for line in open(versions_filename):
                    if 'TORBROWSER_VERSION' in line:
                        installed_version = line.lstrip('TORBROWSER_VERSION=').strip()

        start = self.common.paths['tbb']['start']
        if os.path.isfile(start) and os.access(start, os.X_OK):
            if installed_version == latest_version:
                print _('Latest version of TBB is installed, launching')
                # current version of tbb is installed, launch it
                self.run(False)
                self.launch_gui = False
            elif installed_version < latest_version:
                print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
                # there is a tbb upgrade available
                self.set_gui('task', _("Your Tor Browser is out of date."),
                             ['download_sha256',
                              'download_sha256_sig',
                              'download_tarball',
                              'verify',
                              'extract',
                              'run'])
            else:
                # for some reason the installed tbb is newer than the current version?
                self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])

        # not installed
        else:
            print _('TBB is not installed, attempting to install {0}'.format(latest_version))
            self.set_gui('task', _("Downloading and installing Tor Browser."),
                         ['download_sha256',
                          'download_sha256_sig',
                          'download_tarball',
                          'verify',
                          'extract',
                          'run'])

    # there are different GUIs that might appear, this sets which one we want
    def set_gui(self, gui, message, tasks, autostart=True):
        self.gui = gui
        self.gui_message = message
        self.gui_tasks = tasks
        self.gui_task_i = 0
        self.gui_autostart = autostart

    # set all gtk variables to False
    def clear_ui(self):
        if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
            self.box.destroy()
        self.box = False

        self.label = False
        self.progressbar = False
        self.button_box = False
        self.start_button = False
        self.exit_button = False

    # build the application's UI
    def build_ui(self):
        self.clear_ui()

        self.box = gtk.VBox(False, 20)
        self.window.add(self.box)

        if 'error' in self.gui:
            # labels
            self.label = gtk.Label(self.gui_message)
            self.label.set_line_wrap(True)
            self.box.pack_start(self.label, True, True, 0)
            self.label.show()

            # button box
            self.button_box = gtk.HButtonBox()
            self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
            self.box.pack_start(self.button_box, True, True, 0)
            self.button_box.show()

            if self.gui != 'error':
                # yes button
                yes_image = gtk.Image()
                yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
                self.yes_button = gtk.Button("Yes")
                self.yes_button.set_image(yes_image)
                if self.gui == 'error_try_stable':
                    self.yes_button.connect("clicked", self.try_stable, None)
                elif self.gui == 'error_try_default_mirror':
                    self.yes_button.connect("clicked", self.try_default_mirror, None)
                elif self.gui == 'error_try_tor':
                    self.yes_button.connect("clicked", self.try_tor, None)
                self.button_box.add(self.yes_button)
                self.yes_button.show()

            # exit button
            exit_image = gtk.Image()
            exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
            self.exit_button = gtk.Button("Exit")
            self.exit_button.set_image(exit_image)
            self.exit_button.connect("clicked", self.destroy, None)
            self.button_box.add(self.exit_button)
            self.exit_button.show()

        elif self.gui == 'task':
            # label
            self.label = gtk.Label(self.gui_message)
            self.label.set_line_wrap(True)
            self.box.pack_start(self.label, True, True, 0)
            self.label.show()

            # progress bar
            self.progressbar = gtk.ProgressBar(adjustment=None)
            self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
            self.progressbar.set_pulse_step(0.01)
            self.box.pack_start(self.progressbar, True, True, 0)

            # button box
            self.button_box = gtk.HButtonBox()
            self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
            self.box.pack_start(self.button_box, True, True, 0)
            self.button_box.show()

            # start button
            start_image = gtk.Image()
            start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
            self.start_button = gtk.Button(_("Start"))
            self.start_button.set_image(start_image)
            self.start_button.connect("clicked", self.start, None)
            self.button_box.add(self.start_button)
            if not self.gui_autostart:
                self.start_button.show()

            # exit button
            exit_image = gtk.Image()
            exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
            self.exit_button = gtk.Button(_("Exit"))
            self.exit_button.set_image(exit_image)
            self.exit_button.connect("clicked", self.destroy, None)
            self.button_box.add(self.exit_button)
            self.exit_button.show()

        self.box.show()
        self.window.show()

        if self.gui_autostart:
            self.start(None)

    # start button clicked, begin tasks
    def start(self, widget, data=None):
        # disable the start button
        if self.start_button:
            self.start_button.set_sensitive(False)

        # start running tasks
        self.run_task()

    # run the next task in the task list
    def run_task(self):
        self.refresh_gtk()

        if self.gui_task_i >= len(self.gui_tasks):
            self.destroy(False)
            return

        task = self.gui_tasks[self.gui_task_i]

        # get ready for the next task
        self.gui_task_i += 1

        print _('Running task: {0}'.format(task))
        if task == 'download_update_check':
            print _('Downloading'), self.common.paths['update_check_url']
            self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])

        if task == 'attempt_update':
            print _('Checking to see if update is needed')
            self.attempt_update()

        elif task == 'download_sha256':
            print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
            self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])

        elif task == 'download_sha256_sig':
            print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
            self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])

        elif task == 'download_tarball':
            print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
            self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])

        elif task == 'verify':
            print _('Verifying signature')
            self.verify()

        elif task == 'extract':
            print _('Extracting'), self.common.paths['tarball_filename']
            self.extract()

        elif task == 'run':
            print _('Running'), self.common.paths['tbb']['start']
            self.run()

        elif task == 'start_over':
            print _('Starting download over again')
            self.start_over()

    def response_received(self, response):
        class FileDownloader(Protocol):
            def __init__(self, common, file, total, progress, done_cb):
                self.file = file
                self.total = total
                self.so_far = 0
                self.progress = progress
                self.all_done = done_cb

                if response.code != 200:
                    if common.settings['mirror'] != common.default_mirror:
                        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']))
                    else:
                        raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))

            def dataReceived(self, bytes):
                self.file.write(bytes)
                self.so_far += len(bytes)
                percent = float(self.so_far) / float(self.total)
                self.progress.set_fraction(percent)
                amount = float(self.so_far)
                units = "bytes"
                for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
                    if amount > size:
                        units = unit
                        amount = amount / float(size)
                        break

                self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))

            def connectionLost(self, reason):
                print _('Finished receiving body:'), reason.getErrorMessage()
                self.all_done(reason)

        dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
        response.deliverBody(dl)

    def response_finished(self, msg):
        if msg.check(ResponseDone):
            self.file_download.close()
            delattr(self, 'current_download_path')

            # next task!
            self.run_task()

        else:
            print "FINISHED", msg
            ## FIXME handle errors

    def download_error(self, f):
        print _("Download error:"), f.value, type(f.value)

        if isinstance(f.value, TryStableException):
            f.trap(TryStableException)
            self.set_gui('error_try_stable', str(f.value), [], False)

        elif isinstance(f.value, TryDefaultMirrorException):
            f.trap(TryDefaultMirrorException)
            self.set_gui('error_try_default_mirror', str(f.value), [], False)

        elif isinstance(f.value, DownloadErrorException):
            f.trap(DownloadErrorException)
            self.set_gui('error', str(f.value), [], False)

        elif isinstance(f.value, DNSLookupError):
            f.trap(DNSLookupError)
            if common.settings['mirror'] != common.default_mirror:
                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)
            else:
                self.set_gui('error', str(f.value), [], False)

        elif isinstance(f.value, ResponseFailed):
            for reason in f.value.reasons:
                if isinstance(reason.value, OpenSSL.SSL.Error):
                    # TODO: add the ability to report attack by posting bug to trac.torproject.org
                    if not self.common.settings['update_over_tor']:
                        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)
                    else:
                        self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)

        else:
            self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)

        self.build_ui()

    def download(self, name, url, path):
        # keep track of current download
        self.current_download_path = path

        # initialize the progress bar
        mirror_url = url.format(self.common.settings['mirror'])
        self.progressbar.set_fraction(0)
        self.progressbar.set_text(_('Downloading {0}').format(name))
        self.progressbar.show()
        self.refresh_gtk()

        if self.common.settings['update_over_tor']:
            print _('Updating over Tor')
            from twisted.internet.endpoints import TCP4ClientEndpoint
            from txsocksx.http import SOCKS5Agent

            torEndpoint = TCP4ClientEndpoint(reactor, '127.0.0.1', 9050)

            # default mirror gets certificate pinning, only for requests that use the mirror
            if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
                agent = SOCKS5Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']), proxyEndpoint=torEndpoint)
            else:
                agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
        else:
            if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
                agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
            else:
                agent = Agent(reactor)

        # actually, agent needs to follow redirect
        agent = RedirectAgent(agent)

        # start the request
        d = agent.request('GET', mirror_url,
                          Headers({'User-Agent': ['torbrowser-launcher']}),
                          None)

        self.file_download = open(path, 'w')
        d.addCallback(self.response_received).addErrback(self.download_error)

        if not reactor.running:
            reactor.run()

    def try_default_mirror(self, widget, data=None):
        # change mirror to default and relaunch TBL
        self.common.settings['mirror'] = self.common.default_mirror
        self.common.save_settings()
        subprocess.Popen([self.common.paths['tbl_bin']])
        self.destroy(False)

    def try_tor(self, widget, data=None):
        # set update_over_tor to true and relaunch TBL
        self.common.settings['update_over_tor'] = True
        self.common.save_settings()
        subprocess.Popen([self.common.paths['tbl_bin']])
        self.destroy(False)

    def attempt_update(self):
        # load the update check file
        try:
            versions = json.load(open(self.common.paths['update_check_file']))
            latest = None

            # filter linux versions
            valid = []
            for version in versions:
                if '-Linux' in version:
                    valid.append(str(version))
            valid.sort()
            if len(valid):
                latest = valid.pop()

            if latest:
                self.common.settings['latest_version'] = latest[:-len('-Linux')]
                self.common.settings['last_update_check_timestamp'] = int(time.time())
                self.common.settings['check_for_updates'] = False
                self.common.save_settings()
                self.common.build_paths(self.common.settings['latest_version'])
                self.start_launcher()

            else:
                # failed to find the latest version
                self.set_gui('error', _("Error checking for updates."), [], False)

        except:
            # not a valid JSON object
            self.set_gui('error', _("Error checking for updates."), [], False)

        # now start over
        self.clear_ui()
        self.build_ui()

    def verify(self):
        # initialize the progress bar
        self.progressbar.set_fraction(0)
        self.progressbar.set_text(_('Verifying Signature'))
        self.progressbar.show()

        verified = False
        # check the sha256 file's sig, and also take the sha256 of the tarball and compare
        p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
        self.pulse_until_process_exits(p)
        if p.returncode == 0:
            # compare with sha256 of the tarball
            tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
            for line in open(self.common.paths['sha256_file'], 'r').readlines():
                if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
                    verified = True

        if verified:
            self.run_task()
        else:
            # TODO: add the ability to report attack by posting bug to trac.torproject.org
            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)
            self.clear_ui()
            self.build_ui()

            if not reactor.running:
                reactor.run()

    def extract(self):
        # initialize the progress bar
        self.progressbar.set_fraction(0)
        self.progressbar.set_text(_('Installing'))
        self.progressbar.show()
        self.refresh_gtk()

        extracted = False
        try:
            if self.common.paths['tarball_file'][-2:] == 'xz':
                # if tarball is .tar.xz
                xz = lzma.LZMAFile(self.common.paths['tarball_file'])
                tf = tarfile.open(fileobj=xz)
                tf.extractall(self.common.paths['tbb']['dir'])
                extracted = True
            else:
                # if tarball is .tar.gz
                if tarfile.is_tarfile(self.common.paths['tarball_file']):
                    tf = tarfile.open(self.common.paths['tarball_file'])
                    tf.extractall(self.common.paths['tbb']['dir'])
                    extracted = True
        except:
            pass

        if not extracted:
            self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
            self.clear_ui()
            self.build_ui()
            return

        # installation is finished, so save installed_version
        self.common.settings['installed_version'] = self.common.settings['latest_version']
        self.common.save_settings()

        self.run_task()

    def run(self, run_next_task=True):
        devnull = open('/dev/null', 'w')
        subprocess.Popen([self.common.paths['tbb']['start']], stdout=devnull, stderr=devnull)

        # play modem sound?
        if self.common.settings['modem_sound']:
            try:
                import pygame
                pygame.mixer.init()
                sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
                sound.play()
                time.sleep(10)
            except ImportError:
                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."))
                md.set_position(gtk.WIN_POS_CENTER)
                md.run()
                md.destroy()

        if run_next_task:
            self.run_task()

    # make the progress bar pulse until process p (a Popen object) finishes
    def pulse_until_process_exits(self, p):
        while p.poll() is None:
            time.sleep(0.01)
            self.progressbar.pulse()
            self.refresh_gtk()

    # start over and download TBB again
    def start_over(self):
        self.label.set_text(_("Downloading Tor Browser Bundle over again."))
        self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
        self.gui_task_i = 0
        self.start(None)

    # refresh gtk
    def refresh_gtk(self):
        while gtk.events_pending():
            gtk.main_iteration(False)

    # exit
    def delete_event(self, widget, event, data=None):
        return False

    def destroy(self, widget, data=None):
        if hasattr(self, 'file_download'):
            self.file_download.close()
        if hasattr(self, 'current_download_path'):
            os.remove(self.current_download_path)
            delattr(self, 'current_download_path')
        if reactor.running:
            reactor.stop()

if __name__ == "__main__":
    with open(os.path.join(SHARE, 'torbrowser-launcher/version')) as buf:
        tor_browser_launcher_version = buf.read().strip()

    print _('Tor Browser Launcher')
    print _('By Micah Lee, licensed under GPLv3')
    print _('version {0}').format(tor_browser_launcher_version)
    print 'https://github.com/micahflee/torbrowser-launcher'

    common = TBLCommon(tor_browser_launcher_version)

    # is torbrowser-launcher already running?
    tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
    if tbl_pid:
        print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
        common.bring_window_to_front(tbl_pid)
        sys.exit()

    if '-settings' in sys.argv:
        # settings mode
        app = TBLSettings(common)

    else:
        # launcher mode
        app = TBLLauncher(common)
