# -*- coding: utf-8 -*-
# vim:fenc=utf-8

# This file is part of the  X2Go Project - https://www.x2go.org
# Copyright (C) 2012-2020 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
#
# X2Go Session Broker is free software; you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# X2Go Session Broker 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program; if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

import asyncore
import os
import pam
import socket

from pwd import getpwnam
from grp import getgrnam

# X2Go Session Broker modules
import x2gobroker.defaults
from x2gobroker.loggers import logger_broker


def authenticate(username, password, service="x2gobroker"):
    """\
    Attempt PAM authentication proxied through X2Go Broker's Auth
    Service.

    The X2Go Broker Auth Service runs with root privileges. For PAM
    authentication mechanisms like the ``pam_unix.so`` PAM module, the
    login process requires root privileges (as, staying with the example
    of ``pam_unix.so``, the ``/etc/shadow`` file, where those passwords
    are stored, is only accessible by the root superuser).

    As the X2Go Session Broker runs with reduced system privileges, it
    has to delegate the actual PAM authentication process to the X2Go
    Broker Auth Service.

    For this, X2Go Session Broker needs to connect to the Auth Service's
    authentication socket (see the ``X2GOBROKER_AUTHSERVICE_SOCKET``
    variable in :mod:`x2gobroker.defaults`) and send the string
    ``<username>\\r<password>\\r<service>\\n`` to the socket (where service
    is the name of the PAM service file to use.

    :param username: username to use during authentication
    :type username: ``str``
    :param password: password to use during authentication
    :type password: ``str``

    :returns: Authentication success or failure
    :rtype: ``bool``

    """

    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    logger_broker.debug('authservice.authenticate(): connecting to authentication service socket {socket}'.format(socket=x2gobroker.defaults.X2GOBROKER_AUTHSERVICE_SOCKET))
    s.connect(x2gobroker.defaults.X2GOBROKER_AUTHSERVICE_SOCKET)
    # FIXME: somehow logging output disappears after we have connected to the socket file...
    logger_broker.debug('authservice.authenticate(): sending username={username}, password=<hidden>, service={service} to authentication service'.format(username=username, service=service))
    s.send('{username}\r{password}\r{service}\n'.format(username=username, password=password, service=service).encode())
    result = s.recv(1024).decode()
    s.close()
    if result.startswith('ok'):
        logger_broker.info('authservice.authenticate(): authentication against PAM service »{service}« succeeded for user »{username}«'.format(username=username, service=service))
        return True
    logger_broker.info('authservice.authenticate(): authentication against service »{service}« failed for user »{username}«'.format(username=username, service=service))
    return False

class AuthClient(asyncore.dispatcher_with_send):
    """\
    Handle incoming PAM credential verification request and send a
    response back through the socket.

    :param sock: open socket connection
    :type sock: ``<obj>``
    :param logger: logger instance to report log messages to
    :type logger: ``obj``

    """
    def __init__(self, sock, logger=None):
        self.logger = logger
        asyncore.dispatcher_with_send.__init__(self, sock)
        self._buf = ''

    def handle_read(self):
        """\
        Handle the incoming request after :func:`AuthService.accept()`
        and respond accordingly.

        The requests are expected line by line, the fields are split by "\\r"::

          <user>\\r<password>\\r<pam-service>\\n

        The reponse is sent back over the open socket connection.
        Possibly answers are either::

          ok\\n

        or...

          fail\\n

        """
        data = self._buf + self.recv(1024).decode()
        if not data:
            self.close()
            return
        reqs, data = data.rsplit('\n', 1)
        self._buf = data
        for req in reqs.split('\n'):
            try:
                user, passwd, service = req.split('\r')
            except:
                self.send('bad\n')
                self.logger.warning('bad authentication data received')
            else:
                opam = pam
                if hasattr(pam, "pam"):
                    opam = pam.pam()
                if opam.authenticate(user, passwd, service):
                    self.send('ok\n'.encode())
                    self.logger.info('successful authentication for \'{user}\' with password \'<hidden>\' against PAM service \'{service}\''.format(user=user, service=service))
                else:
                    self.send('fail\n'.encode())
                    self.logger.info('authentication failure for \'{user}\' with password \'<hidden>\' against PAM service \'{service}\''.format(user=user, service=service))

    def handle_close(self):
        """\
        Close the connected :class:``AuthClient`` connection.

        """
        self.close()


class AuthService(asyncore.dispatcher_with_send):
    """\
    Provide an :mod:`asyncore` based authentication socket handler where
    client can send credential checking requests to.

    Access to the sockt is limited by file permissions to given owner and
    group.

    :param socketfile: file name path of the to be created Unix domain
        socket file. The directory in the give path must exist.
    :type socketfile: ``str``
    :param owner: chown the socket file to this owner
    :type owner: ``str``
    :param group: chgrp the socket file to this group
    :type group: ``str``
    :param permissions: octal representation of the file permissions (handed over as string)
    :type permissions: ``str``
    :param logger: logger instance to report log messages to
    :type logger: ``<obj>``

    """
    def __init__(self, socketfile, owner='root', group_owner='root', permissions='0o660', logger=None):
        self.logger = logger
        asyncore.dispatcher_with_send.__init__(self)
        self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self.set_reuse_addr()
        self.bind(socketfile)
        os.chown(socketfile, getpwnam(owner).pw_uid, getgrnam(group_owner).gr_gid)
        os.chmod(socketfile, int(permissions, 8))
        self.listen(1)

    def handle_accept(self):
        """\
        Handle accepted connection requests.

        """
        conn, _ = self.accept()
        AuthClient(conn, logger=self.logger)
