/*
 *   stunnel       TLS offloading and load-balancing proxy
 *   Copyright (C) 1998-2015 Michal Trojnara <Michal.Trojnara@mirt.net>
 *
 *   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 2 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>.
 *
 *   Linking stunnel statically or dynamically with other modules is making
 *   a combined work based on stunnel. Thus, the terms and conditions of
 *   the GNU General Public License cover the whole combination.
 *
 *   In addition, as a special exception, the copyright holder of stunnel
 *   gives you permission to combine stunnel with free software programs or
 *   libraries that are released under the GNU LGPL and with code included
 *   in the standard release of OpenSSL under the OpenSSL License (or
 *   modified versions of such code, with unchanged license). You may copy
 *   and distribute such a system following the terms of the GNU GPL for
 *   stunnel and the licenses of the other code concerned.
 *
 *   Note that people who make modified versions of stunnel are not obligated
 *   to grant this special exception for their modified versions; it is their
 *   choice whether to do so. The GNU General Public License gives permission
 *   to release a modified version without this exception; this exception
 *   also makes it possible to release a modified version which carries
 *   forward this exception.
 */

#include "common.h"
#include "prototypes.h"

#ifdef USE_LIBWRAP

#include <tcpd.h>

#if defined(USE_PTHREAD) && !defined(__CYGWIN__)
/* http://wiki.osdev.org/Cygwin_Issues#Passing_file_descriptors */
#define USE_LIBWRAP_POOL
#endif /* USE_PTHREAD && !__CYGWIN__ */

NOEXPORT int check(char *, int);

int allow_severity=LOG_NOTICE, deny_severity=LOG_WARNING;

#ifdef USE_LIBWRAP_POOL
#define SERVNAME_LEN 256

NOEXPORT ssize_t read_fd(int, void *, size_t, int *);
NOEXPORT ssize_t write_fd(int, void *, size_t, int);

unsigned num_processes=0;
static int *ipc_socket, *busy;
#endif /* USE_LIBWRAP_POOL */

#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
#endif /* __GNUC__ */
int libwrap_init() {
#ifdef USE_LIBWRAP_POOL
    unsigned i, j;
    int rfd, result;
    char servname[SERVNAME_LEN];
    static int initialized=0;
    SERVICE_OPTIONS *opt;

    if(initialized) /* during startup or previous configuration file reload */
        return 0;
    for(opt=service_options.next; opt; opt=opt->next)
        if(opt->option.libwrap) /* libwrap is enabled for this service */
            break;
    if(!opt) /* disabled for all sections or inetd mode (no sections) */
        return 0;

    num_processes=LIBWRAP_CLIENTS;
    ipc_socket=str_alloc(2*num_processes*sizeof(int));
    busy=str_alloc(num_processes*sizeof(int));
    for(i=0; i<num_processes; ++i) { /* spawn a child */
        if(s_socketpair(AF_UNIX, SOCK_STREAM, 0, ipc_socket+2*i, 0, "libwrap_init"))
            return 1;
        switch(fork()) {
        case -1:    /* error */
            ioerror("fork");
            return 1;
        case  0:    /* child */
            tls_alloc(NULL, ui_tls, "libwrap");
            drop_privileges(0); /* libwrap processes are not chrooted */
            close(0); /* stdin */
            close(1); /* stdout */
            if(!global_options.option.log_stderr) /* for logging in read_fd */
                close(2); /* stderr */
            for(j=0; j<=i; ++j) /* close parent-side sockets created so far */
                close(ipc_socket[2*j]);
            while(1) { /* main libwrap child loop */
                if(read_fd(ipc_socket[2*i+1], servname, SERVNAME_LEN, &rfd)<=0)
                    _exit(0);
                result=check(servname, rfd);
                write(ipc_socket[2*i+1], (uint8_t *)&result, sizeof result);
                if(rfd>=0)
                    close(rfd);
            }
        default:    /* parent */
            close(ipc_socket[2*i+1]); /* child-side socket */
        }
    }
    initialized=1;
#endif /* USE_LIBWRAP_POOL */
    return 0;
}
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif /* __GNUC__ */

void libwrap_auth(CLI *c, char *accepted_address) {
    int result=0; /* deny by default */
#ifdef USE_LIBWRAP_POOL
    static volatile unsigned num_busy=0, roundrobin=0;
    unsigned my_process;
    int retval;
    static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
    static pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
#endif /* USE_LIBWRAP_POOL */

    if(!c->opt->option.libwrap) /* libwrap is disabled for this service */
        return; /* allow connection */
#ifdef HAVE_STRUCT_SOCKADDR_UN
    if(c->peer_addr.sa.sa_family==AF_UNIX) {
        s_log(LOG_INFO, "Libwrap is not supported on Unix sockets");
        return;
    }
#endif
#ifdef USE_LIBWRAP_POOL
    if(num_processes) {
        s_log(LOG_DEBUG, "Waiting for a libwrap process");

        retval=pthread_mutex_lock(&mutex);
        if(retval) {
            errno=retval;
            ioerror("pthread_mutex_lock");
            longjmp(c->err, 1);
        }
        while(num_busy==num_processes) { /* all child processes are busy */
            retval=pthread_cond_wait(&cond, &mutex);
            if(retval) {
                errno=retval;
                ioerror("pthread_cond_wait");
                longjmp(c->err, 1);
            }
        }
        while(busy[roundrobin]) /* find a free child process */
            roundrobin=(roundrobin+1)%num_processes;
        my_process=roundrobin; /* the process allocated by this thread */
        ++num_busy; /* the child process has been allocated */
        busy[my_process]=1; /* mark the child process as busy */
        retval=pthread_mutex_unlock(&mutex);
        if(retval) {
            errno=retval;
            ioerror("pthread_mutex_unlock");
            longjmp(c->err, 1);
        }

        s_log(LOG_DEBUG, "Acquired libwrap process #%d", my_process);
        write_fd(ipc_socket[2*my_process], c->opt->servname,
            strlen(c->opt->servname)+1, c->local_rfd.fd);
        s_read(c, ipc_socket[2*my_process],
            (uint8_t *)&result, sizeof result);
        s_log(LOG_DEBUG, "Releasing libwrap process #%d", my_process);

        retval=pthread_mutex_lock(&mutex);
        if(retval) {
            errno=retval;
            ioerror("pthread_mutex_lock");
            longjmp(c->err, 1);
        }
        busy[my_process]=0; /* mark the child process as free */
        --num_busy; /* the child process has been released */
        retval=pthread_cond_signal(&cond); /* signal a waiting thread */
        if(retval) {
            errno=retval;
            ioerror("pthread_cond_signal");
            longjmp(c->err, 1);
        }
        retval=pthread_mutex_unlock(&mutex);
        if(retval) {
            errno=retval;
            ioerror("pthread_mutex_unlock");
            longjmp(c->err, 1);
        }

        s_log(LOG_DEBUG, "Released libwrap process #%d", my_process);
    } else
#endif /* USE_LIBWRAP_POOL */
    { /* use original, synchronous libwrap calls */
        enter_critical_section(CRIT_LIBWRAP);
        result=check(c->opt->servname, c->local_rfd.fd);
        leave_critical_section(CRIT_LIBWRAP);
    }
    if(!result) {
        s_log(LOG_WARNING, "Service [%s] REFUSED by libwrap from %s",
            c->opt->servname, accepted_address);
        s_log(LOG_DEBUG, "See hosts_access(5) manual for details");
        longjmp(c->err, 1);
    }
    s_log(LOG_DEBUG, "Service [%s] permitted by libwrap from %s",
        c->opt->servname, accepted_address);
}

NOEXPORT int check(char *name, int fd) {
    struct request_info request;

    request_init(&request, RQ_DAEMON, name, RQ_FILE, fd, 0);
    fromhost(&request);
    return hosts_access(&request);
}

#ifdef USE_LIBWRAP_POOL

NOEXPORT ssize_t read_fd(SOCKET fd, void *ptr, size_t nbytes, SOCKET *recvfd) {
    struct msghdr msg;
    struct iovec iov[1];
    ssize_t n;

#ifdef HAVE_MSGHDR_MSG_CONTROL
    union {
        struct cmsghdr cm;
        char control[CMSG_SPACE(sizeof(int))];
    } control_un;
    struct cmsghdr *cmptr;

    msg.msg_control=control_un.control;
    msg.msg_controllen=sizeof control_un.control;
#else
    int newfd;

    msg.msg_accrights=(caddr_t)&newfd;
    msg.msg_accrightslen=sizeof(int);
#endif

    msg.msg_name=NULL;
    msg.msg_namelen=0;

    iov[0].iov_base=ptr;
    iov[0].iov_len=nbytes;
    msg.msg_iov=iov;
    msg.msg_iovlen=1;

    *recvfd=INVALID_SOCKET; /* descriptor was not passed */
    n=recvmsg(fd, &msg, 0);
    if(n<=0)
        return n;

#ifdef HAVE_MSGHDR_MSG_CONTROL
    cmptr=CMSG_FIRSTHDR(&msg);
    if(!cmptr || cmptr->cmsg_len!=CMSG_LEN(sizeof(int)))
        return n;
    if(cmptr->cmsg_level!=SOL_SOCKET) {
        s_log(LOG_ERR, "control level != SOL_SOCKET");
        return -1;
    }
    if(cmptr->cmsg_type!=SCM_RIGHTS) {
        s_log(LOG_ERR, "control type != SCM_RIGHTS");
        return -1;
    }
    memcpy(recvfd, CMSG_DATA(cmptr), sizeof(int));
#else
    if(msg.msg_accrightslen==sizeof(int))
        *recvfd=newfd;
#endif

    return n;
}

NOEXPORT ssize_t write_fd(int fd, void *ptr, size_t nbytes, int sendfd) {
    struct msghdr msg;
    struct iovec iov[1];

#ifdef HAVE_MSGHDR_MSG_CONTROL
    union {
        struct cmsghdr cm;
        char control[CMSG_SPACE(sizeof(int))];
    } control_un;
    struct cmsghdr *cmptr;

    msg.msg_control=control_un.control;
    msg.msg_controllen=sizeof control_un.control;

    cmptr=CMSG_FIRSTHDR(&msg);
    cmptr->cmsg_len=CMSG_LEN(sizeof(int));
    cmptr->cmsg_level=SOL_SOCKET;
    cmptr->cmsg_type=SCM_RIGHTS;
    memcpy(CMSG_DATA(cmptr), &sendfd, sizeof(int));
#else
    msg.msg_accrights=(caddr_t)&sendfd;
    msg.msg_accrightslen=sizeof(int);
#endif

    msg.msg_name=NULL;
    msg.msg_namelen=0;

    iov[0].iov_base=ptr;
    iov[0].iov_len=nbytes;
    msg.msg_iov=iov;
    msg.msg_iovlen=1;

    return sendmsg(fd, &msg, 0);
}

#endif /* USE_LIBWRAP_POOL */

#endif /* USE_LIBWRAP */

/* end of libwrap.c */
