#!/bin/bash /usr/scripts/python.sh
#-*- coding: utf-8 -*-
# ----------------------------------------------------------------------------
# Copyright (c) 2010, Matteo Bertozzi <theo.bertozzi@gmail.com>
# Copyright (c) 2014, Valentin Samir <valentin.samir@crans.org>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#    * Redistributions of source code must retain the above copyright
#      notice, this list of conditions and the following disclaimer.
#    * Redistributions in binary form must reproduce the above copyright
#      notice, this list of conditions and the following disclaimer in the
#      documentation and/or other materials provided with the distribution.
#    * Neither the name of the Matteo Bertozzi nor the
#      names of its contributors may be used to endorse or promote products
#      derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY Matteo Bertozzi ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL Matteo Bertozzi BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------
# How to mount the File-System:
#   python ldapcertfs mnt [ldap_filter]
#
# How to umount the File-System?
#   fusermount -u mnt

import os
import sys
import ssl
import stat
import ldap
import time
import fuse
import errno
import getpass

from OpenSSL import crypto

import lc_ldap.shortcuts
import lc_ldap.attributs
import gestion.secrets_new as secrets

# Specify what Fuse API use: 0.2
fuse.fuse_python_api = (0, 2)


CACHE = {}
CACHE_TIMEOUT = 600
DEBUG = False

def set_to_cache(keys, value, now=None, null=False):
    global CACHE
    if now is None:
        now = time.time()
    if not isinstance(keys, list):
        keys = [keys]
    for key in keys:
        if key or null:
            CACHE[key] = (now, value)

def get_from_cache(key, now=None, expire=CACHE_TIMEOUT):
    global CACHE
    if now is None:
        now = time.time()
    if key in CACHE:
        (ts, v) = CACHE[key]
        if now - ts < expire or expire < 0:
            return v
        else:
            raise ValueError("Expired")
    else:
        raise ValueError("Not Found")

class Pass:
    pass

class Item(object):
    """
    An Item is an Object on Disk, it can be a Directory, File, Symlink, ...
    """
    def __init__(self, mode, uid, gid):
        # ----------------------------------- Metadata --
        self.atime = time.time()   # time of last acces
        self.mtime = self.atime    # time of last modification
        self.ctime = self.atime    # time of last status change

        self.dev  = 0        # device ID (if special file)
        self.mode = mode     # protection and file-type
        self.uid  = uid      # user ID of owner
        self.gid  = gid      # group ID of owner

        # ------------------------ Extended Attributes --
        self.xattr = {}

        # --------------------------------------- Data --
        if stat.S_ISDIR(mode):
            self.data = set()
        else:
            self.data = ''

    def read(self, offset, length):
        return self.data[offset:offset+length]

    def write(self, offset, data):
        length = len(data)
        self.data = self.data[:offset] + data + self.data[offset+length:]
        return length

    def truncate(self, length):
        if len(self.data) > length:
            self.data = self.data[:length]
        else:
            self.data += '\x00'# (length - len(self.data))

def zstat(stat):
    stat.st_mode  = 0
    stat.st_ino   = 0
    stat.st_dev   = 0
    stat.st_nlink = 2
    stat.st_uid   = 0
    stat.st_gid   = 0
    stat.st_size  = 0
    stat.st_atime = 0
    stat.st_mtime = 0
    stat.st_ctime = 0
    return stat

class LdapCertFS(fuse.Fuse):
    def __init__(self, *args, **kwargs):
        fuse.Fuse.__init__(self, *args, **kwargs)

        self.uid = os.getuid()
        self.gid = os.getgid()

        self.basedn = 'ou=data,dc=crans,dc=org'
        self.conn = lc_ldap.shortcuts.lc_ldap_readonly()
        self._storage = {}
        self.path_to_dn_alias = {}
        self.dn_to_path_alias = {}

    def _func_cache(self, func, *args, **kwargs):
        rec = kwargs.pop('rec', False)
        kwargs_l = ["%s=%s" % kv for kv in kwargs.items()]
        kwargs_l.sort()
        serial = "%s(%s,%s)" % (func.__name__, ", ".join(args), ", ".join(kwargs_l))
        try:
            return get_from_cache(serial)
        except ValueError:
            try:
                objects = func(*args, **kwargs)
                set_to_cache(serial, objects)
                return objects
            except ldap.SERVER_DOWN:
                try:
                    self.conn = lc_ldap.shortcuts.lc_ldap_readonly()
                    if not rec:
                        logger.warning("[ldapcertfs] ldap down, retrying")
                        kwargs['rec'] = rec
                        self.func_cache(func, *args, **kwargs)
                except Exception as e:
                    logger.error("[ldapcertfs] uncaught exception %r" % e)
                # Si le serveur est down on essaye de fournir un ancienne valeur du cache
                try:
                    return get_from_cache(serial, expire=-1)
                except ValueError:
                    logger.critical("[ldapcertfs] fail to return a valid result, I will probably crash next to this")
                    return []

    def search_cache(self, *args, **kwargs):
        return self._func_cache(self.conn.search, *args, **kwargs)

    def is_singlevalue(self, attr):
        try:
            return getattr(lc_ldap.attributs, attr).singlevalue
        except AttributeError:
            return False

    def dn_to_path(self, dn):
        if dn == self.basedn:
            return '/'
        if '/' in dn:
            raise ValueError('/ in dn is invalid')
        else:
            if not dn.endswith(self.basedn):
                raise ValueError("dn %s outside of %s" % (dn, self.basedn))
            else:
                dn = dn[0:-len(self.basedn)-1]
            dn = dn.split(',')
            dn.reverse()
            return "/" + "/".join(dn)

    def path_to_dn(self, path):
        path = path[1:]
        if ',' in path:
            raise ValueError(', in path is invalid')
        path = path.split('/')
        path.reverse()
        attr = None
        index = None
        if path and not '=' in path[0]:
            try:
                index=int(path[0])
                attr=path[1]
                path=path[2:]
            except ValueError:
                attr=path[0]
                del path[0]
        vdn=",".join(path)
        path.reverse()
        if vdn:
            dn = "%s,%s" % (vdn, self.basedn)
        else:
            dn = self.basedn
        return (dn, attr, index)

    def make(self, path):
        try:
            get_from_cache(path)
        except ValueError:
            try:
                (dn, attr, index) = self.path_to_dn(path)
                if attr:
                     self.make_attr(dn, attr, index)
                else:
                    self.make_dn(dn)
            except ldap.INVALID_DN_SYNTAX:
                DEBUG and open('/tmp/lc_ldap_fs.log', 'a+').write('INVALID_DN_SYNTAX: %s\n' % path)
            except KeyError:
                DEBUG and open('/tmp/lc_ldap_fs.log', 'a+').write('KeyError: %s\n' % path)
            set_to_cache(path, True)

    def make_attr(self, dn, attr, index):
        DEBUG and open('/tmp/lc_ldap_fs.log', 'a+').write('dn=%s | attr=%s\n' % (dn, attr))
        attr_data = self.search_cache(dn=dn, scope=0, sizelimit=-1)[0].attrs[attr]
        path = self.dn_to_path(dn)
        apath = os.path.join(path, attr)
        if self.is_singlevalue(attr):
            self._storage[apath]=Item(0400 | stat.S_IFREG, self.uid, self.gid)
            self._storage[apath].data = str(attr_data[0]) + "\n"
            self._add_to_parent_dir(apath)
        else:
            if index is None:
                self._storage[apath]=Item(0500 | stat.S_IFDIR, self.uid, self.gid)
                self._add_to_parent_dir(apath)
                for i in range(0, len(attr_data)):
                    ipath=os.path.join(apath, str(i))
                    if not ipath in self._storage:
                        self._storage[ipath] = Item(0400 | stat.S_IFREG, self.uid, self.gid)
                        self._storage[ipath].data = str(attr_data[i]) + "\n"
                    self._add_to_parent_dir(ipath)
            else:
                ipath = os.path.join(apath, str(index))
                self._add_to_parent_dir(ipath)
                self._storage[ipath]=Item(0400 | stat.S_IFREG, self.uid, self.gid)
                self._storage[ipath].data = str(attr_data[index]) + "\n"

    def make_dn(self, dn):
        DEBUG and open('/tmp/lc_ldap_fs.log', 'a+').write('dn=%s\n' % dn)
        path = self.dn_to_path(dn)
        objects = self.search_cache(dn=dn, scope=1, sizelimit=-1)
        try:
            current_obj = self.search_cache(dn=dn, scope=0, sizelimit=-1)[0]
            spath = os.path.join(os.path.dirname(path), str(current_obj))
            self._storage[spath] = Item(0644 | stat.S_IFLNK, self.uid, self.gid)
            self._storage[spath].data = os.path.basename(path)
            self._add_to_parent_dir(spath)
            attrs = current_obj.attrs
        except ldap.NO_SUCH_OBJECT:
            attrs = {}
        DEBUG and open('/tmp/lc_ldap_fs.log', 'a+').write('path=%s\n' % path)
        self._storage[path] = Item(0500 | stat.S_IFDIR, self.uid, self.gid)
        self._add_to_parent_dir(path)
        for obj in objects:
            opath = self.dn_to_path(obj.dn)
            if not opath in self._storage:
                self._storage[opath]=Item(0555 | stat.S_IFDIR, self.uid, self.gid)
            self._add_to_parent_dir(opath)
            spath = os.path.join(os.path.dirname(opath), str(obj))
            self._storage[spath] = Item(0644 | stat.S_IFLNK, self.uid, self.gid)
            self._storage[spath].data = os.path.basename(opath)
            self._add_to_parent_dir(spath)
            
        for attr in attrs:
            apath = os.path.join(path, attr)
            if not apath in self._storage:
                if not self.is_singlevalue(attr):
                    self._storage[apath]=Item(0555 | stat.S_IFDIR, self.uid, self.gid)
                else:
                    self._storage[apath]=Item(0400 | stat.S_IFREG, self.uid, self.gid)
            self._add_to_parent_dir(apath)
            

    # --- Metadata -----------------------------------------------------------
    def getattr(self, path):
        try:
            self.make(path)
            if not path in self._storage:
                return -errno.ENOENT

            # Lookup Item and fill the stat struct
            item = self._storage[path]
            st = zstat(fuse.Stat())
            st.st_mode  = item.mode
            st.st_uid   = item.uid
            st.st_gid   = item.gid
            st.st_dev   = item.dev
            st.st_atime = item.atime
            st.st_mtime = item.mtime
            st.st_ctime = item.ctime
            st.st_size  = len(item.data)
            return st
        except Exception as e:
            DEBUG and open('/tmp/lc_ldap_fs.log', 'a+').write('%r\n' % e)
            return -errno.ENOENT

    def chmod(self, path, mode):
        return -errno.EPERM

    def chown(self, path, uid, gid):
        return -errno.EPERM

    def utime(self, path, times):
        item = self._storage[path]
        item.ctime = item.mtime = times[0]

    # --- Namespace ----------------------------------------------------------
    def unlink(self, path):
        return -errno.EPERM

    def rename(self, oldpath, newpath):
        return -errno.EPERM

    # --- Links --------------------------------------------------------------
    def symlink(self, path, newpath):
        return -errno.EPERM

    def readlink(self, path):
        return self._storage[path].data

    # --- Extra Attributes ---------------------------------------------------
    def setxattr(self, path, name, value, flags):
        return -errno.EPERM

    def getxattr(self, path, name, size):
        value = self._storage[path].xattr.get(name, '')
        if size == 0:   # We are asked for size of the value
            return len(value)
        return value

    def listxattr(self, path, size):
        attrs = self._storage[path].xattr.keys()
        if size == 0:
            return len(attrs) + len(''.join(attrs))
        return attrs

    def removexattr(self, path, name):
        return -errno.EPERM

    # --- Files --------------------------------------------------------------
    def mknod(self, path, mode, dev):
        return -errno.EPERM

    def create(self, path, flags, mode):
        return -errno.EPERM

    def truncate(self, path, len):
        return -errno.EPERM

    def read(self, path, size, offset):
        return self._storage[path].read(offset, size)

    def write(self, path, buf, offset):
        return -errno.EPERM

    # --- Directories --------------------------------------------------------
    def mkdir(self, path, mode):
        return -errno.EPERM

    def rmdir(self, path):
        return -errno.EPERM

    def readdir(self, path, offset):
        dir_items = self._storage[path].data
        for item in dir_items:
            yield fuse.Direntry(item)

    def _add_to_parent_dir(self, path):
        parent_path = os.path.dirname(path)
        filename = os.path.basename(path)
        if parent_path in self._storage and filename:
            DEBUG and open('/tmp/lc_ldap_fs.log', 'a+').write('add %s to parent %s\n' % (filename, parent_path))
            self._storage[parent_path].data.add(filename)
        else:
            DEBUG and open('/tmp/lc_ldap_fs.log', 'a+').write('add %s to parent %s but parent is unknown\n' % (filename, parent_path))

    def _remove_from_parent_dir(self, path):
        parent_path = os.path.dirname(path)
        filename = os.path.basename(path)
        self._storage[parent_path].data.remove(filename)

def main(usage):
    # Vérification que le point de montage est bien un dossier
    end_option = False
    for item in sys.argv[1:]:
        if end_option or not item.startswith('-'):
            if not os.path.isdir(item):
                raise EnvironmentError("%s is not a dir" % item)
            break
        if item == '--':
            end_option=True

    # Instanciation du FS
    server = LdapCertFS(version="%prog " + fuse.__version__,
                     usage=usage,
                     dash_s_do='setsingle')

    server.parse(errex=1)
    server.main()

if __name__ == '__main__':
    usage="""
LdapCertFS - Ldap Certificate File System
    Les obtions spécifiques sont :
      *  --decrypt : pour déchiffrer les clefs privées (un prompt demande le
           mot de passe si nécessaire.
      *  --nopkey : exclure les clefs privées lors de la construction du FS.
      *  --ldap-filter filtre : selectionner les machines à utiliser pour
           construire le FS avec un filtre ldap. Nécéssite les droits root.

    Si --ldap-filter n'est pas spécifier :
       * Si le programme est appelé par root, on utilises les machines
         correspondants aux ips des interfaces de la machine physique.
       * Sinon, on utilise les machines de l'utilisateur dans la base
         de donnée.

""" + fuse.Fuse.fusage
    # On force à fornir au moint un paramètre (il faut au moins un point de montage)
    if len(sys.argv)<2:
        sys.stderr.write("%s\n" % usage.replace('%prog', sys.argv[0]))
        sys.exit(1)
    # On appel main et on affiche les exceptions EnvironmentError
    try:
        main(usage)
    except (EnvironmentError, fuse.FuseError) as e:
        sys.stderr.write("Error: %s\n" % e)
        sys.exit(1)

