/*
 * 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: ssh-security.c,v 1.23 2006/08/21 20:17:10 martinea Exp $
 *
 * ssh-security.c - security and transport over ssh or a ssh-like command.
 *
 * XXX still need to check for initial keyword on connect so we can skip
 * over shell garbage and other stuff that ssh might want to spew out.
 */

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

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

/*
 * Magic values for ssh_conn->handle
 */
#define	H_TAKEN	-1		/* ssh_conn->tok was already read */
#define	H_EOF	-2		/* this connection has been shut down */

/*
 * Interface functions
 */
static void	ssh_connect(const char *, char *(*)(char *, void *),
			void (*)(void *, security_handle_t *, security_status_t),
			void *, void *);
static void ssh_accept(const security_driver_t *driver, char *(*conf_fn)(char *, void *),
			int in, int out,
			void (*fn)(security_handle_t *, pkt_t *),
			void *datap);

/*
 * This is our interface to the outside world.
 */
const security_driver_t ssh_security_driver = {
    "SSH",
    ssh_connect,
    ssh_accept,
    sec_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 runssh(struct tcp_conn *, const char *, const char *, const char *,
		  const char *);

/*
 * ssh version of a security handle allocator.  Logically sets
 * up a network "connection".
 */
static void
ssh_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, *client_username=NULL, *ssh_keys=NULL;
    char *client_port = NULL;

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

    auth_debug(1, "ssh_connect: %s\n", hostname);

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

    rh->hostname = g_strdup(hostname);
    rh->rs = tcpma_stream_client(rh, newhandle++);
    if (rh->rc == NULL)
	goto error;
    rh->rc->conf_fn = conf_fn;
    rh->rc->datap = datap;

    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) {
	char *port_str;
	amandad_path    = conf_fn("amandad_path", datap);
	client_username = conf_fn("client_username", datap);
	ssh_keys        = conf_fn("ssh_keys", datap);
	port_str        = conf_fn("client_port", datap);
	if (port_str && strlen(port_str) >= 1) {
	    client_port = port_str;
	}
    }
    if(rh->rc->read == -1) {
	if (runssh(rh->rs->rc, amandad_path, client_username, ssh_keys,
		   client_port) < 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);
}

/* like sec_accept, but first it gets the remote system's hostname */
static void
ssh_accept(
    const security_driver_t *driver,
    char       *(*conf_fn)(char *, void *),
    int		in,
    int		out,
    void	(*fn)(security_handle_t *, pkt_t *),
    void       *datap)
{
    struct sec_handle *rh;
    struct tcp_conn *rc = sec_tcp_conn_get(NULL, "", 0);
    char *ssh_connection, *p;
    char *errmsg = NULL;
    sockaddr_union addr;
    int result;

    /* "Accepting" an SSH connection means that amandad was invoked via sshd, so
     * we should have anSSH_CONNECTION env var.  If not, then this probably isn't
     * a real SSH connection and we should bail out. */
    ssh_connection = getenv("SSH_CONNECTION");
    if (!ssh_connection) {
	errmsg = g_strdup("$SSH_CONNECTION not set - was amandad started by sshd?");
	goto error;
    }

    /* make a local copy, to munge */
    ssh_connection = g_strdup(ssh_connection);

    /* strip off the first component - the ASCII IP address */
    if ((p = strchr(ssh_connection, ' ')) == NULL) {
	errmsg = g_strdup("$SSH_CONNECTION malformed");
	goto error;
    }
    *p = '\0';

    /* ---- everything from here on is just a warning, leaving hostname at "" */

    SU_INIT(&addr, AF_INET);

    /* turn the string address into a sockaddr */
    if ((result = str_to_sockaddr(ssh_connection, &addr)) != 1) {
	if (result == 0) {
	    g_warning("Could not parse peer address %s", ssh_connection);
	} else {
	    g_warning("Parsing peer address %s: %s", ssh_connection, gai_strerror(result));
	}
	goto done;
    }

    /* find the hostname */
    result = getnameinfo((struct sockaddr *)&addr, SS_LEN(&addr),
		 rc->hostname, sizeof(rc->hostname), NULL, 0, 0);
    if (result != 0) {
	g_warning("Could not get hostname for SSH client %s: %s", ssh_connection,
		gai_strerror(result));
	goto done;
    }

    /* and verify it */
    if (check_name_give_sockaddr(rc->hostname,
				 (struct sockaddr *)&addr, &errmsg) < 0) {
	rc->hostname[0] = '\0'; /* null out the bad hostname */
	g_warning("Checking SSH client DNS: %s", errmsg);
	amfree(errmsg);
	goto done;
    }

done:
    g_free(ssh_connection);

    rc->read = in;
    rc->write = out;
    rc->accept_fn = fn;
    rc->driver = driver;
    rc->conf_fn = conf_fn;
    rc->datap = datap;
    sec_tcp_conn_read(rc);
    return;

error:
    if (ssh_connection)
	g_free(ssh_connection);

    /* make up a fake handle for the error */
    rh = g_new0(struct sec_handle, 1);
    security_handleinit(&rh->sech, driver);
    security_seterror((security_handle_t*)rh, "ssh_accept: %s", errmsg);
    amfree(errmsg);
    (*fn)(&rh->sech, NULL);
}

static void
ssh_child_watch_callback(
    pid_t pid,
    gint status,
    gpointer data)
{
    struct tcp_conn *rc = (struct tcp_conn *)data;

    if (pid != rc->pid)
	return;

    g_assert(pid == rc->pid);
    rc->pid = -1 ; /* it's gone now.. */

    if (WIFEXITED(status)) {
	int exitcode = WEXITSTATUS(status);
	g_debug("ssh exited with status %d", exitcode);
    } else if (WIFSIGNALED(status)) {
	int signal = WTERMSIG(status);
	g_debug("ssh died on signal %d", signal);
    }
}

/*
 * Forks a ssh to the host listed in rc->hostname
 * Returns negative on error, with an errmsg in rc->errmsg.
 */
static int
runssh(
    struct tcp_conn *	rc,
    const char *	amandad_path,
    const char *	client_username,
    const char *	ssh_keys,
    const char *        client_port)
{
    int rpipe[2], wpipe[2];
    char *xamandad_path = (char *)amandad_path;
    char *xclient_username = (char *)client_username;
    char *xssh_keys = (char *)ssh_keys;
    char *xclient_port = (char *)client_port;
    GPtrArray *myargs;
    gchar *ssh_options[100] = {SSH_OPTIONS, NULL};
    gchar **ssh_option;
    gchar *cmd;

    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);
    }

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

    myargs = g_ptr_array_sized_new(20);
    g_ptr_array_add(myargs, SSH);
    for (ssh_option = ssh_options; *ssh_option != NULL; ssh_option++) {
	g_ptr_array_add(myargs, *ssh_option);
    }
    g_ptr_array_add(myargs, "-l");
    g_ptr_array_add(myargs, xclient_username);
    if (xclient_port) {
	g_ptr_array_add(myargs, "-p");
	g_ptr_array_add(myargs, xclient_port);
    }
    if (ssh_keys && strlen(ssh_keys) > 1) {
	g_ptr_array_add(myargs, "-i");
	g_ptr_array_add(myargs, xssh_keys);
    }
    g_ptr_array_add(myargs, rc->hostname);
    g_ptr_array_add(myargs, xamandad_path);
    g_ptr_array_add(myargs, "-auth=ssh");
    g_ptr_array_add(myargs, NULL);

    cmd = g_strjoinv(" ", (gchar **)myargs->pdata);
    g_debug("exec: %s", cmd);
    g_free(cmd);

    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:
	dup2(wpipe[0], 0);
	dup2(rpipe[1], 1);
	break;
    default:
	rc->read = rpipe[0];
	aclose(rpipe[1]);
	rc->write = wpipe[1];
	aclose(wpipe[0]);
	rc->child_watch = new_child_watch_source(rc->pid);
	g_source_set_callback(rc->child_watch,
            (GSourceFunc)ssh_child_watch_callback, rc, NULL);
	g_source_attach(rc->child_watch, NULL);
	g_source_unref(rc->child_watch);

	return (0);
    }

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

    safe_fd(-1, 0);

    execvp(SSH, (gchar **)myargs->pdata);

    error("error: couldn't exec %s: %s", SSH, strerror(errno));

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