/*
 * Amanda, The Advanced Maryland Automatic Network Disk Archiver
 * Copyright (c) 1999 University of Maryland
 * All Rights Reserved.
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation, and that the name of U.M. not be used in advertising or
 * publicity pertaining to distribution of the software without specific,
 * written prior permission.  U.M. makes no representations about the
 * suitability of this software for any purpose.  It is provided "as is"
 * without express or implied warranty.
 *
 * U.M. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL U.M.
 * BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * Authors: the Amanda Development Team.  Its members are listed in a
 * file named AUTHORS, in the root directory of this distribution.
 */

/*
 * $Id: local-security.c 6512 2007-05-24 17:00:24Z ian $
 *
 * local-security.c - security and transport over local or a local-like command.
 *
 * XXX still need to check for initial keyword on connect so we can skip
 * over shell garbage and other stuff that local might want to spew out.
 */

#include "amanda.h"
#include "match.h"
#include "amutil.h"
#include "event.h"
#include "packet.h"
#include "security.h"
#include "security-util.h"
#include "stream.h"

/*
 * Number of seconds amandad has to start up
 */
#define CONNECT_TIMEOUT 20

/*
 * Interface functions
 */
static void local_connect(const char *, char *(*)(char *, void *),
			void (*)(void *, security_handle_t *, security_status_t),
			void *, void *);

static char *local_get_authenticated_peer_name_hostname(security_handle_t *hdl);

static gboolean is_local(const char *);

/*
 * This is our interface to the outside world.
 */
const security_driver_t local_security_driver = {
    "LOCAL",
    local_connect,
    sec_accept,
    local_get_authenticated_peer_name_hostname,
    sec_close,
    stream_sendpkt,
    stream_recvpkt,
    stream_recvpkt_cancel,
    tcpma_stream_server,
    tcpma_stream_accept,
    tcpma_stream_client,
    tcpma_stream_close,
    tcpma_stream_close_async,
    sec_stream_auth,
    sec_stream_id,
    tcpm_stream_write,
    tcpm_stream_write_async,
    tcpm_stream_read,
    tcpm_stream_read_sync,
    tcpm_stream_read_to_shm_ring,
    tcpm_stream_read_cancel,
    tcpm_stream_pause,
    tcpm_stream_resume,
    tcpm_close_connection,
    NULL,
    NULL,
    generic_data_write,
    generic_data_write_non_blocking,
    generic_data_read
};

static int newhandle = 1;

/*
 * Local functions
 */
static int runlocal(struct tcp_conn *, const char *, const char *);


/*
 * local version of a security handle allocator.  Logically sets
 * up a network "connection".
 */
static void
local_connect(
    const char *	hostname,
    char *		(*conf_fn)(char *, void *),
    void		(*fn)(void *, security_handle_t *, security_status_t),
    void *		arg,
    void *		datap)
{
    struct sec_handle *rh;
    char *amandad_path=NULL;
    char *client_username=NULL;

    assert(fn != NULL);
    assert(hostname != NULL);

    auth_debug(1, _("local: local_connect: %s\n"), hostname);

    rh = g_new0(struct sec_handle, 1);
    security_handleinit(&rh->sech, &local_security_driver);
    rh->dle_hostname = g_strdup(hostname);
    rh->hostname = NULL;
    rh->rs = NULL;
    rh->ev_timeout = NULL;
    rh->rc = NULL;

    if (!is_local(hostname)) {
	security_seterror(&rh->sech,
	    _("%s: is not local"), hostname);
	(*fn)(arg, &rh->sech, S_ERROR);
	return;
    }
    rh->hostname = g_strdup(hostname);
    rh->rs = tcpma_stream_client(rh, newhandle++);
    if (rh->rc == NULL)
	goto error;

    if (rh->rs == NULL)
	goto error;

    amfree(rh->hostname);
    rh->hostname = g_strdup(rh->rs->rc->hostname);

    /*
     * We need to open a new connection.
     *
     * XXX need to eventually limit number of outgoing connections here.
     */
    if(conf_fn) {
	amandad_path    = conf_fn("amandad_path", datap);
	client_username = conf_fn("client_username", datap);
    }
    if(rh->rc->read == -1) {
	if (runlocal(rh->rs->rc, amandad_path, client_username) < 0) {
	    security_seterror(&rh->sech, _("can't connect to %s: %s"),
			      hostname, rh->rs->rc->errmsg);
	    goto error;
	}
	rh->rc->refcnt++;
    }

    /*
     * The socket will be opened async so hosts that are down won't
     * block everything.  We need to register a write event
     * so we will know when the socket comes alive.
     *
     * Overload rh->rs->ev_read to provide a write event handle.
     * We also register a timeout.
     */
    g_mutex_lock(security_mutex);
    rh->fn.connect = fn;
    rh->arg = arg;
    rh->rs->rc->ev_write = event_create((event_id_t)rh->rs->rc->write, EV_WRITEFD,
	sec_connect_callback, rh);
    rh->ev_timeout = event_create((event_id_t)CONNECT_TIMEOUT, EV_TIME,
	sec_connect_timeout, rh);
    event_activate(rh->rs->rc->ev_write);
    event_activate(rh->ev_timeout);
    g_mutex_unlock(security_mutex);

    return;

error:
    (*fn)(arg, &rh->sech, S_ERROR);
    amfree(rh->hostname);
}

/*
 * Forks a local to the host listed in rc->hostname
 * Returns negative on error, with an errmsg in rc->errmsg.
 */
static int
runlocal(
    struct tcp_conn *	rc,
    const char *	amandad_path,
    const char *	client_username G_GNUC_UNUSED)
{
    int rpipe[2], wpipe[2];
    char *xamandad_path = (char *)amandad_path;

#ifndef SINGLE_USERID
    struct passwd *pwd = NULL;
    uid_t uid = 0;
    gid_t gid = 0;

    if (getuid() == 0) {
	if (client_username && strlen(client_username) > 1) {
	    pwd = getpwnam(client_username);
            if (!pwd) {
		dbprintf("User '%s' doesn't exist\n", client_username);
	    } else {
		uid = pwd->pw_uid;
		gid = pwd->pw_gid;
	    }
	}
	if (!pwd) {
	    uid = get_client_uid();
	    gid = get_client_gid();
	}
    }
#endif

    memset(rpipe, -1, sizeof(rpipe));
    memset(wpipe, -1, sizeof(wpipe));
    if (pipe(rpipe) < 0 || pipe(wpipe) < 0) {
	g_free(rc->errmsg);
	rc->errmsg = g_strdup_printf(_("pipe: %s"), strerror(errno));
	return (-1);
    }

    switch (rc->pid = fork()) {
    case -1:
	g_free(rc->errmsg);
	rc->errmsg = g_strdup_printf(_("fork: %s"), strerror(errno));
	aclose(rpipe[0]);
	aclose(rpipe[1]);
	aclose(wpipe[0]);
	aclose(wpipe[1]);
	return (-1);
    case 0:
	aclose(wpipe[1]);
	aclose(rpipe[0]);
	close(0);
	close(1);
	dup2(wpipe[0], 0);
	dup2(rpipe[1], 1);
	aclose(wpipe[0]);
	aclose(rpipe[1]);
	break;
    default:
	rc->read = rpipe[0];
	aclose(rpipe[1]);
	rc->write = wpipe[1];
	aclose(wpipe[0]);
	return (0);
    }

    /* drop root privs for good */
    set_root_privs(-1);

    if(!xamandad_path || strlen(xamandad_path) <= 1)
	xamandad_path = g_strjoin(NULL, amlibexecdir, "/", "amandad", NULL);

#ifndef SINGLE_USERID
    if (client_username && *client_username != '\0') {
	initgroups(client_username, gid);
    } else {
	initgroups(CLIENT_LOGIN, gid);
    }
    if (gid != 0) {
	if (setregid(gid, gid) == -1) {
	    error("Can't setregid(%d,%d): %s", gid, gid, strerror(errno));
	}
    }
    if (uid != 0) {
	if (setreuid(uid, uid) == -1) {
	    error("Can't setreuid(%d,%d): %s", uid, uid, strerror(errno));
	}
    }
#endif

    safe_fd(-1, 0);

    execlp(xamandad_path, xamandad_path,
	   "-auth=local", (char *)NULL);
    error(_("error: couldn't exec %s: %s"), xamandad_path, strerror(errno));

    /* should never go here, shut up compiler warning */
    return(-1);
}

static char *
local_get_authenticated_peer_name_hostname(
    security_handle_t *hdl G_GNUC_UNUSED)
{
    char *server_hostname;
    server_hostname = malloc(1024);
    if (gethostname(server_hostname, 1024) == 0) {
	server_hostname[1023] = '\0';
	return server_hostname;
    }
    amfree(server_hostname);
    return g_strdup("localhost");

}

/*
 * Recognize whether hostname refers to this host, regardless of how it is
 * spelled, capitalized, abbreviated or fully qualified, etc.
 *
 * If there exists any address for hostname to which a socket can be bound,
 * return TRUE.
 */
static gboolean
is_local(const char *hostname)
{
    struct addrinfo *addrs;
    struct addrinfo *a;
    int rslt;
    int s;
    gboolean verdict = FALSE;

    rslt = resolve_hostname(hostname, SOCK_STREAM, &addrs, NULL);
    if (0 != rslt) {
        dbprintf("Unresolved hostname %s assumed nonlocal: %s\n",
	         hostname, gai_strerror(rslt));
        return verdict;
    }
    /* Beyond this point, addrs must be freed */

    /* Invariant: s is not an open socket */
    for ( a = addrs ; NULL != a ; a = a->ai_next ) {
        s = socket(a->ai_family, a->ai_socktype, a->ai_protocol);
	if ( -1 == s )
	    continue;
	rslt = bind(s, a->ai_addr, a->ai_addrlen);
	if ( 0 == rslt ) {
	    close(s);
	    verdict = TRUE;
	    break;
	}
	if ( EADDRNOTAVAIL != errno ) {
	    dbprintf("strange bind() result %s\n", strerror(errno));
	}
	close(s);
    }
    /* s is not open here */

    freeaddrinfo(addrs);
    return verdict;
}
