summaryrefslogtreecommitdiff
path: root/src/leap/platform_init
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/platform_init')
-rw-r--r--src/leap/platform_init/__init__.py28
-rw-r--r--src/leap/platform_init/initializers.py383
-rw-r--r--src/leap/platform_init/locks.py312
3 files changed, 723 insertions, 0 deletions
diff --git a/src/leap/platform_init/__init__.py b/src/leap/platform_init/__init__.py
new file mode 100644
index 00000000..2a262a30
--- /dev/null
+++ b/src/leap/platform_init/__init__.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# __init__.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+System constants
+"""
+import platform
+
+_system = platform.system()
+
+IS_WIN = True if _system == "Windows" else False
+IS_MAC = True if _system == "Darwin" else False
+IS_LINUX = True if _system == "Linux" else False
+IS_UNIX = IS_MAC or IS_LINUX
diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py
new file mode 100644
index 00000000..5345f11a
--- /dev/null
+++ b/src/leap/platform_init/initializers.py
@@ -0,0 +1,383 @@
+# -*- coding: utf-8 -*-
+# initializers.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Platform dependant initializing code
+"""
+
+import logging
+import os
+import platform
+import stat
+import subprocess
+import tempfile
+
+from PySide import QtGui
+
+from leap.config.leapsettings import LeapSettings
+from leap.services.eip import vpnlaunchers
+from leap.util import first
+
+logger = logging.getLogger(__name__)
+
+# NOTE we could use a deferToThread here, but should
+# be aware of this bug: http://www.themacaque.com/?p=1067
+
+__all__ = ["init_platform"]
+
+_system = platform.system()
+
+
+def init_platform():
+ """
+ Returns the right initializer for the platform we are running in, or
+ None if no proper initializer is found
+ """
+ initializer = None
+ try:
+ initializer = globals()[_system + "Initializer"]
+ except:
+ pass
+ if initializer:
+ logger.debug("Running initializer for %s" % (platform.system(),))
+ initializer()
+ else:
+ logger.debug("Initializer not found for %s" % (platform.system(),))
+
+
+#
+# common utils
+#
+
+NOTFOUND_MSG = ("Tried to install %s, but %s "
+ "not found inside this bundle.")
+BADEXEC_MSG = ("Tried to install %s, but %s "
+ "failed to %s.")
+
+UPDOWN_NOTFOUND_MSG = NOTFOUND_MSG % (
+ "updown scripts", "those were")
+UPDOWN_BADEXEC_MSG = BADEXEC_MSG % (
+ "updown scripts", "they", "be copied")
+
+
+def get_missing_updown_dialog():
+ """
+ Creates a dialog for notifying of missing updown scripts.
+ Returns that dialog.
+
+ :rtype: QtGui.QMessageBox instance
+ """
+ WE_NEED_POWERS = ("To better protect your privacy, "
+ "LEAP needs administrative privileges "
+ "to install helper files. "
+ "Do you want to proceed?")
+ msg = QtGui.QMessageBox()
+ msg.setWindowTitle(msg.tr("Missing up/down scripts"))
+ msg.setText(msg.tr(WE_NEED_POWERS))
+ # but maybe the user really deserve to know more
+ #msg.setInformativeText(msg.tr(BECAUSE))
+ msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)
+ msg.addButton("No, don't ask again", QtGui.QMessageBox.RejectRole)
+ msg.setDefaultButton(QtGui.QMessageBox.Yes)
+ return msg
+
+
+def check_missing():
+ """
+ Checks for the need of installing missing scripts, and
+ raises a dialog to ask user for permission to do it.
+ """
+ config = LeapSettings()
+ alert_missing = config.get_alert_missing_scripts()
+
+ launcher = vpnlaunchers.get_platform_launcher()
+ missing_scripts = launcher.missing_updown_scripts
+ missing_other = launcher.missing_other_files
+
+ if alert_missing and (missing_scripts() or missing_other()):
+ msg = get_missing_updown_dialog()
+ ret = msg.exec_()
+
+ if ret == QtGui.QMessageBox.Yes:
+ install_missing_fun = globals().get(
+ "_%s_install_missing_scripts" % (_system.lower(),),
+ None)
+ if not install_missing_fun:
+ logger.warning(
+ "Installer not found for platform %s." % (_system,))
+ return
+ install_missing_fun(
+ # XXX maybe move constants to fun
+ UPDOWN_BADEXEC_MSG,
+ UPDOWN_NOTFOUND_MSG)
+
+ elif ret == QtGui.QMessageBox.No:
+ logger.debug("Not installing missing scripts, "
+ "user decided to ignore our warning.")
+
+ elif ret == QtGui.QMessageBox.Rejected:
+ logger.debug(
+ "Setting alert_missing_scripts to False, we will not "
+ "ask again")
+ config.set_alert_missing_scripts(False)
+#
+# windows initializers
+#
+
+
+def _windows_has_tap_device():
+ """
+ Loops over the windows registry trying to find if the tap0901 tap driver
+ has been installed on this machine.
+ """
+ import _winreg as reg
+
+ adapter_key = 'SYSTEM\CurrentControlSet\Control\Class' \
+ '\{4D36E972-E325-11CE-BFC1-08002BE10318}'
+ with reg.OpenKey(reg.HKEY_LOCAL_MACHINE, adapter_key) as adapters:
+ try:
+ for i in xrange(10000):
+ key_name = reg.EnumKey(adapters, i)
+ with reg.OpenKey(adapters, key_name) as adapter:
+ try:
+ component_id = reg.QueryValueEx(adapter,
+ 'ComponentId')[0]
+ if component_id.startswith("tap0901"):
+ return True
+ except WindowsError:
+ pass
+ except WindowsError:
+ pass
+ return False
+
+
+def WindowsInitializer():
+ """
+ Raises a dialog in case that the windows tap driver has not been found
+ in the registry, asking the user for permission to install the driver
+ """
+ if not _windows_has_tap_device():
+ msg = QtGui.QMessageBox()
+ msg.setWindowTitle(msg.tr("TAP Driver"))
+ msg.setText(msg.tr("LEAPClient needs to install the necessary drivers "
+ "for Encrypted Internet to work. Would you like to "
+ "proceed?"))
+ msg.setInformativeText(msg.tr("Encrypted Internet uses VPN, which "
+ "needs a TAP device installed and none "
+ "has been found. This will ask for "
+ "administrative privileges."))
+ msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)
+ msg.setDefaultButton(QtGui.QMessageBox.Yes)
+ ret = msg.exec_()
+
+ if ret == QtGui.QMessageBox.Yes:
+ # XXX should do this only if executed inside bundle.
+ # Let's assume it's the only way it's gonna be executed under win
+ # by now.
+ driver_path = os.path.join(os.getcwd(),
+ "apps",
+ "eip",
+ "tap_driver")
+ dev_installer = os.path.join(driver_path,
+ "devcon.exe")
+ if os.path.isfile(dev_installer) and \
+ stat.S_IXUSR & os.stat(dev_installer)[stat.ST_MODE] != 0:
+ inf_path = os.path.join(driver_path,
+ "OemWin2k.inf")
+ cmd = [dev_installer, "install", inf_path, "tap0901"]
+ ret = subprocess.call(cmd, stdout=subprocess.PIPE, shell=True)
+ else:
+ logger.error("Tried to install TAP driver, but the installer "
+ "is not found or not executable")
+
+#
+# Darwin initializer functions
+#
+
+
+def _darwin_has_tun_kext():
+ """
+ Returns True only if we found a directory under the system kext folder
+ containing a kext named tun.kext, AND we found a startup item named 'tun'
+ """
+ # XXX we should be smarter here and use kextstats output.
+
+ has_kext = os.path.isdir("/System/Library/Extensions/tun.kext")
+ has_startup = os.path.isdir("/System/Library/StartupItems/tun")
+ has_tun_and_startup = has_kext and has_startup
+ logger.debug(
+ 'platform initializer check: has tun_and_startup = %s' %
+ (has_tun_and_startup,))
+ return has_tun_and_startup
+
+
+def _darwin_install_missing_scripts(badexec, notfound):
+ """
+ Tries to install the missing up/down scripts.
+
+ :param badexec: error for notifying execution error during command.
+ :type badexec: str
+ :param notfound: error for notifying missing path.
+ :type notfound: str
+ """
+ # We expect to execute this from some way of bundle, since
+ # the up/down scripts should be put in place by the installer.
+ installer_path = os.path.join(
+ os.getcwd(),
+ "..",
+ "Resources",
+ "openvpn")
+ launcher = vpnlaunchers.DarwinVPNLauncher
+
+ # TODO should change osascript by use of the proper
+ # os authorization api.
+ if os.path.isdir(installer_path):
+ fd, tempscript = tempfile.mkstemp(prefix="leap_installer-")
+ try:
+ cmd = launcher.OSASCRIPT_BIN
+ scriptlines = launcher.cmd_for_missing_scripts(installer_path)
+ with os.fdopen(fd, 'w') as f:
+ f.write(scriptlines)
+ st = os.stat(tempscript)
+ os.chmod(tempscript, st.st_mode | stat.S_IEXEC | stat.S_IXUSR |
+ stat.S_IXGRP | stat.S_IXOTH)
+
+ osascript = launcher.OSX_ASADMIN % ("/bin/sh %s" % (tempscript,),)
+ cmdline = ["%s -e '%s'" % (cmd, osascript)]
+ ret = subprocess.call(
+ cmdline, stdout=subprocess.PIPE,
+ shell=True)
+ assert([ret]) # happy flakes
+ except Exception as exc:
+ logger.error(badexec)
+ logger.error("Error was: %r" % (exc,))
+ finally:
+ try:
+ os.remove(tempscript)
+ except OSError as exc:
+ logger.error("%r" % (exc,))
+ else:
+ logger.error(notfound)
+ logger.debug('path searched: %s' % (installer_path,))
+
+
+def DarwinInitializer():
+ """
+ Raises a dialog in case that the osx tuntap driver has not been found
+ in the registry, asking the user for permission to install the driver
+ """
+ # XXX split this function into several
+
+ TUNTAP_NOTFOUND_MSG = NOTFOUND_MSG % (
+ "tuntaposx kext", "the installer")
+ TUNTAP_BADEXEC_MSG = BADEXEC_MSG % (
+ "tuntaposx kext", "the installer", "be launched")
+
+ # TODO DRY this with other cases, and
+ # factor out to _should_install() function.
+ # Leave the dialog as a more generic thing.
+
+ if not _darwin_has_tun_kext():
+ msg = QtGui.QMessageBox()
+ msg.setWindowTitle(msg.tr("TUN Driver"))
+ msg.setText(msg.tr("LEAPClient needs to install the necessary drivers "
+ "for Encrypted Internet to work. Would you like to "
+ "proceed?"))
+ msg.setInformativeText(msg.tr("Encrypted Internet uses VPN, which "
+ "needs a kernel extension for a TUN "
+ "device installed, and none "
+ "has been found. This will ask for "
+ "administrative privileges."))
+ msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)
+ msg.setDefaultButton(QtGui.QMessageBox.Yes)
+ ret = msg.exec_()
+
+ if ret == QtGui.QMessageBox.Yes:
+ installer_path = os.path.join(
+ os.getcwd(),
+ "..",
+ "Resources",
+ "tuntap-installer.app")
+ if os.path.isdir(installer_path):
+ cmd = ["open %s" % (installer_path,)]
+ try:
+ ret = subprocess.call(
+ cmd, stdout=subprocess.PIPE,
+ shell=True)
+ except:
+ logger.error(TUNTAP_BADEXEC_MSG)
+ else:
+ logger.error(TUNTAP_NOTFOUND_MSG)
+
+ # Second check, for missing scripts.
+ check_missing()
+
+
+#
+# Linux initializers
+#
+
+def _linux_install_missing_scripts(badexec, notfound):
+ """
+ Tries to install the missing up/down scripts.
+
+ :param badexec: error for notifying execution error during command.
+ :type badexec: str
+ :param notfound: error for notifying missing path.
+ :type notfound: str
+ """
+ installer_path = os.path.join(
+ os.getcwd(),
+ "apps", "eip", "files")
+ launcher = vpnlaunchers.LinuxVPNLauncher
+
+ # XXX refactor with darwin, same block.
+
+ if os.path.isdir(installer_path):
+ fd, tempscript = tempfile.mkstemp(prefix="leap_installer-")
+ try:
+ pkexec = first(launcher.maybe_pkexec())
+ scriptlines = launcher.cmd_for_missing_scripts(installer_path)
+ with os.fdopen(fd, 'w') as f:
+ f.write(scriptlines)
+ st = os.stat(tempscript)
+ os.chmod(tempscript, st.st_mode | stat.S_IEXEC | stat.S_IXUSR |
+ stat.S_IXGRP | stat.S_IXOTH)
+ cmdline = ["%s %s" % (pkexec, tempscript)]
+ ret = subprocess.call(
+ cmdline, stdout=subprocess.PIPE,
+ shell=True)
+ assert([ret]) # happy flakes
+ except Exception as exc:
+ logger.error(badexec)
+ logger.error("Error was: %r" % (exc,))
+ finally:
+ try:
+ os.remove(tempscript)
+ except OSError as exc:
+ logger.error("%r" % (exc,))
+ else:
+ logger.error(notfound)
+ logger.debug('path searched: %s' % (installer_path,))
+
+
+def LinuxInitializer():
+ """
+ Raises a dialog in case that either updown scripts or policykit file
+ are missing or they have incorrect permissions.
+ """
+ check_missing()
diff --git a/src/leap/platform_init/locks.py b/src/leap/platform_init/locks.py
new file mode 100644
index 00000000..c40c31d0
--- /dev/null
+++ b/src/leap/platform_init/locks.py
@@ -0,0 +1,312 @@
+# -*- coding: utf-8 -*-
+# locks.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Utilities for handling multi-platform file locking mechanisms
+"""
+import logging
+import errno
+import os
+import platform
+
+from leap.common.events import signal as signal_event
+from leap.common.events import events_pb2 as proto
+from leap import platform_init
+
+if platform_init.IS_UNIX:
+ from fcntl import flock, LOCK_EX, LOCK_NB
+else:
+ import glob
+ import shutil
+
+ from tempfile import gettempdir
+
+logger = logging.getLogger(__name__)
+
+if platform_init.IS_UNIX:
+
+ class UnixLock(object):
+ """
+ Uses flock to get an exclusive lock over a file.
+ See man 2 flock
+ """
+
+ def __init__(self, path):
+ """
+ iniializes t he UnixLock with the path of the
+ desired lockfile
+ """
+
+ self._fd = None
+ self.path = path
+
+ def get_lock(self):
+ """
+ Tries to get a lock, and writes the running pid there if successful
+ """
+ gotit, pid = self._get_lock_and_pid()
+ return gotit
+
+ def get_pid(self):
+ """
+ Returns the pid of the locking process
+ """
+ gotit, pid = self._get_lock_and_pid()
+ return pid
+
+ def _get_lock(self):
+ """
+ Tries to get a lock, returning True if successful
+
+ :rtype: bool
+ """
+ self._fd = os.open(self.path, os.O_CREAT | os.O_RDWR)
+
+ try:
+ flock(self._fd, LOCK_EX | LOCK_NB)
+ except IOError as exc:
+ # could not get the lock
+ #import ipdb; ipdb.set_trace()
+
+ if exc.args[0] in (errno.EDEADLK, errno.EAGAIN):
+ # errno 11 or 35
+ # Resource temporarily unavailable
+ return False
+ else:
+ raise
+ return True
+
+ @property
+ def locked_by_us(self):
+ """
+ Returns True if the pid in the pidfile
+ is ours.
+
+ :rtype: bool
+ """
+ gotit, pid = self._get_lock_and_pid()
+ return pid == os.getpid()
+
+ def _get_lock_and_pid(self):
+ """
+ Tries to get a lock over the file.
+ Returns (locked, pid) tuple.
+
+ :rtype: tuple
+ """
+
+ if self._get_lock():
+ self._write_to_pidfile()
+ return True, None
+
+ return False, self._read_from_pidfile()
+
+ def _read_from_pidfile(self):
+ """
+ Tries to read pid from the pidfile,
+ returns False if no content found.
+ """
+
+ pidfile = os.read(
+ self._fd, 16)
+ if not pidfile:
+ return False
+
+ try:
+ return int(pidfile.strip())
+ except Exception as exc:
+ exc.args += (pidfile, self.lock_file)
+ raise
+
+ def _write_to_pidfile(self):
+ """
+ Writes the pid of the running process
+ to the pidfile
+ """
+ fd = self._fd
+ os.ftruncate(fd, 0)
+ os.write(fd, '%d\n' % os.getpid())
+ os.fsync(fd)
+
+
+if platform_init.IS_WIN:
+
+ class WindowsLock(object):
+ """
+ Creates a lock based on the atomic nature of mkdir on Windows
+ system calls.
+ """
+ LOCKBASE = os.path.join(gettempdir(), "leap-client-lock")
+
+ def __init__(self):
+ """
+ Initializes the lock.
+ Sets the lock name to basename plus the process pid.
+ """
+ self._fd = None
+ pid = os.getpid()
+ self.name = "%s-%s" % (self.LOCKBASE, pid)
+ self.pid = pid
+
+ def get_lock(self):
+ """
+ Tries to get a lock, and writes the running pid there if successful
+ """
+ gotit = self._get_lock()
+ return gotit
+
+ def _get_lock(self):
+ """
+ Tries to write to a file with the current pid as part of the name
+ """
+ try:
+ self._fd = os.makedirs(self.name)
+ except OSError as exc:
+ # could not create the dir
+ if exc.args[0] == 183:
+ logger.debug('cannot create dir')
+ # cannot create dir with existing name
+ return False
+ else:
+ raise
+ return self._is_one_pidfile()[0]
+
+ def _is_one_pidfile(self):
+ """
+ Returns True, pid if there is only one pidfile with the expected
+ base path
+
+ :rtype: tuple
+ """
+ pidfiles = glob.glob(self.LOCKBASE + '-*')
+ if len(pidfiles) == 1:
+ pid = pidfiles[0].split('-')[-1]
+ return True, int(pid)
+ else:
+ return False, None
+
+ def get_pid(self):
+ """
+ Returns the pid of the locking process
+
+ :rtype: int
+ """
+ # XXX assert there is only one?
+ _, pid = self._is_one_pidfile()
+ return pid
+
+ def release_lock(self):
+ """
+ Releases the pidfile dir for this process, by removing it.
+ """
+ try:
+ shutil.rmtree(self.name)
+ return True
+ except WindowsError as exc:
+ if exc.errno in (errno.EPIPE, errno.ENOENT,
+ errno.ESRCH, errno.EACCES):
+ logger.warning(
+ 'exception while trying to remove the lockfile dir')
+ logger.warning('errno %s: %s' % (exc.errno, exc.args[1]))
+ # path does not exist
+ return False
+ else:
+ logger.debug('errno = %s' % (exc.errno,))
+ # we did not foresee this error, better add it explicitely
+ raise
+
+ @property
+ def locked_by_us(self):
+ """
+ Returns True if the pid in the pidfile
+ is ours.
+
+ :rtype: bool
+ """
+ _, pid = self._is_one_pidfile()
+ return pid == self.pid
+
+ def write_port(self, port):
+ """
+ Writes the port for windows control to the pidfile folder
+ Returns True if successful.
+
+ :rtype: bool
+ """
+ if not self.locked_by_us:
+ logger.warning("Tried to write control port to a "
+ "non-unique pidfile folder")
+ return False
+ port_file = os.path.join(self.name, "port")
+ with open(port_file, 'w') as f:
+ f.write("%s" % port)
+ return True
+
+ def get_control_port(self):
+ """
+ Reads control port of the main instance from the port file
+ in the pidfile dir
+
+ :rtype: int
+ """
+ pid = self.get_pid()
+ port_file = os.path.join(self.LOCKBASE + "-%s" % pid, "port")
+ port = None
+ try:
+ with open(port_file) as f:
+ port_str = f.read()
+ port = int(port_str.strip())
+ except IOError as exc:
+ if exc.errno == errno.ENOENT:
+ logger.error("Tried to read port from non-existent file")
+ else:
+ # we did not know explicitely about this error
+ raise
+ return port
+
+
+def we_are_the_one_and_only():
+ """
+ Returns True if we are the only instance running, False otherwise.
+ If we came later, send a raise signal to the main instance of the
+ application
+
+ :rtype: bool
+ """
+ _sys = platform.system()
+
+ if _sys in ("Linux", "Darwin"):
+ locker = UnixLock('/tmp/leap-client.lock')
+ locker.get_lock()
+ we_are_the_one = locker.locked_by_us
+ if not we_are_the_one:
+ signal_event(proto.RAISE_WINDOW)
+ return we_are_the_one
+
+ elif _sys == "Windows":
+ locker = WindowsLock()
+ locker.get_lock()
+ we_are_the_one = locker.locked_by_us
+ if not we_are_the_one:
+ locker.release_lock()
+ signal_event(proto.RAISE_WINDOW)
+ return we_are_the_one
+
+ else:
+ logger.warning("Multi-instance checker "
+ "not implemented for %s" % (_sys))
+ # lies, lies, lies...
+ return True