/* forward.c - forward sockets over an encrypted/compressed connection
   This has nothing to do with cryptography.
   Copyright (C) 1998 Paul Sheer

   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, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
   02111-1307, USA.
 */

#include <config.h>
#include "mostincludes.h"
#include "vfs/net-includes.h"
#include <stdarg.h>
#include <signal.h>
#include "mirrordir.h"
#include "diffie/parse.h"
#include "vfs/vfs.h"
#include "vfs/mcfs.h"
#include "vfs/tcputil.h"
#include "cmdlineopt.h"
#include "netrc.h"
#include "sys/socket.h"
#include "zlib/zlib.h"
#include "diffie/diffie-socket.h"
#include "diffie/z-socket.h"
#include "diffie/field.h"
#include "diffie/arc.h"
#include "diffie/compat.h"
#include "mad.h"

/* command line options */

static int daemon_mode = 1;
static char *redirect_to_machine = 0;
static int redirect_to_port = -1;
static int listen_on_port = -1;
static char *secure_mcserv_url = 0;

static int show_help = 0;
int verbose = 0;
static int show_version = 0;
static int allow_netrc = 1;
static int secure = 0;
static int key_size = 512;
static int compress_mode = 0;

static char *cmdline_password = 0;

static char *progname;

void progmess (char *s, char *par)
{
    fprintf (stderr, "%s: %s: %s\n", progname, s, par);
}

void verbmess (char *s, char *par)
{
    if (par)
	fprintf (stdout, "%s: ---verbose--- %s: %s\n", progname, s, par);
    else
	fprintf (stdout, "%s: ---verbose--- %s\n", progname, s);
}

void progmess_strerror (char *s, char *p)
{
#ifdef HAVE_STRERROR
    fprintf (stderr, "%s: %s: %s: %s\n", progname, s, p, strerror (mc_errno));
#else
    fprintf (stderr, "%s: %s: %s: %d\n", progname, s, p, mc_errno);
#endif
}

/* {{{ acceptor */

void redirect (int local_sock, int remote_sock);
int mcfs_open_tcp_link (char *host, char *user, int *port, char *netrcpass, int *version);

/* {{{ signal handling */

#if (RETSIGTYPE==void)
#define handler_return return
#else
#define handler_return return 1
#endif

static int term_fd_close1, term_fd_close2;

static RETSIGTYPE quit_handler (int x)
{
    shutdown (term_fd_close1, 2);
    shutdown (term_fd_close2, 2);
    exit (0);
    handler_return;
}

/* }}} signal handling */

static int do_accept (int _listen_on_port, char *_secure_mcserv_url, char *_redirect_to_machine, int _redirect_to_port)
{
    int sock;
    struct sockaddr_in client_address, server_address;
    struct hostent *hp;
    char hostname[255];
    int yes = 1;
    int client_port;

    if ((sock = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
	progmess_strerror ("error creating socket", "socket()");
	return 1;
    }
    /* Use this to debug: */
    if (setsockopt (sock, SOL_SOCKET, SO_REUSEADDR, (char *) &yes, sizeof (yes)) < 0) {
	progmess_strerror ("error setting socket options", "setsockopt()");
	return 1;
    }
    gethostname (hostname, 255);
    if (verbose)
	verbmess ("hostname", hostname);
    hp = gethostbyname (hostname);
    if (hp == 0) {
	progmess_strerror ("error getting hostname", "gethostbyname()");
	return 1;
    }
    memset (&server_address, 0, sizeof (server_address));
    server_address.sin_family = hp->h_addrtype;
    server_address.sin_addr.s_addr = htonl (INADDR_ANY);
    server_address.sin_port = htons (_listen_on_port);
    if (bind (sock, (struct sockaddr *) &server_address, sizeof (server_address)) < 0) {
	progmess_strerror ("error binding to socket", "bind()");
	return 1;
    }
    listen (sock, 5);
    for (;;) {
	char *host, *pass, *user;
	int port, version, child, local_sock, remote_sock, result;
	unsigned int client_address_len;
	client_address_len = sizeof (client_address);
#ifdef SIGPIPE
	signal (SIGPIPE, SIG_IGN);
#endif
#ifdef SIGCHLD
	signal (SIGCHLD, SIG_IGN);
#endif
	if (verbose)
	    verbmess ("accepting connection", 0);
#undef accept
	local_sock = accept (sock, (struct sockaddr *) &client_address, &client_address_len);
	if (daemon_mode && (child = fork ())) {
	    int status;
	    shutdown (local_sock, 2);
	    waitpid ((pid_t) child, (int *) &status, (int) 0);
	    continue;
	}
	if (daemon_mode && fork ())
	    exit (0);
	if (strncmp (_secure_mcserv_url, "mc://", 5)) {
	    shutdown (local_sock, 2);
	    progmess_strerror ("bad url for remote", _secure_mcserv_url);
	    return 1;
	}
	_secure_mcserv_url += 5;
	free (get_host_and_username (_secure_mcserv_url, &host, &user, &port, 0, 0, &pass));
	if (verbose)
	    verbmess ("host is", host);
	if (verbose)
	    verbmess ("user is", user);
	if (verbose)
	    verbmess ("opening connection to", _secure_mcserv_url);
	if (!(remote_sock = mcfs_open_tcp_link (host, user, &port, pass, &version))) {
	    shutdown (local_sock, 2);
	    progmess_strerror ("error connecting with remote", _secure_mcserv_url);
	    return 1;
	}
	if (verbose) {
	    char mess[256];
	    sprintf (mess, "machine %.100s, port %.100d", _redirect_to_machine, _redirect_to_port);
	    verbmess ("sending redirect command", mess);
	}
	client_port = (int) ntohs (client_address.sin_port);
	rpc_send (remote_sock, RPC_INT, MC_REDIRECT, RPC_STRING, _redirect_to_machine,
		  RPC_INT, _redirect_to_port, RPC_INT, client_port, RPC_END);
	if (verbose)
	    verbmess ("recieving result", 0);
	if (!rpc_get (remote_sock, RPC_INT, &result, RPC_INT, &errno, RPC_END)) {
	    shutdown (remote_sock, 2);
	    shutdown (local_sock, 2);
	    progmess_strerror ("error recieving status from remote", _secure_mcserv_url);
	    return 1;
	}
	if (result) {
	    shutdown (remote_sock, 2);
	    shutdown (local_sock, 2);
	    progmess_strerror ("remote plain text connection failed", _redirect_to_machine);
	    return 1;
	}

	term_fd_close1 = local_sock;
	term_fd_close2 = remote_sock;
#ifdef SIGQUIT
	signal (SIGQUIT, quit_handler);
#endif
	signal (SIGINT, quit_handler);
        signal (SIGTERM, quit_handler);

	if (verbose)
	    verbmess ("redirecting ports", 0);
	ioctl (local_sock, FIONBIO, &yes);
	redirect (local_sock, remote_sock);
	if (verbose)
	    verbmess ("shutting connection", 0);

	shutdown (remote_sock, 2);
	shutdown (local_sock, 2);
	return 0;
    }
}

/* }}} acceptor */

/* {{{ command line processing */

static void usage (int exit_code)
{
    char **q;
    char *p[256] =
    {"",
     "Usage:",
     "    forward [options] <user@machine1:port1> <machine2:port> <listen_port>",
     "",
     "forward listens on port <listen_port>. Any connection made to this",
     "port will be automatically forwarded to <machine2:port> from the",
     "secure-mcserv daemon running on <machine1:port1>. Usually machine1",
     "and machine2 will be the same.",
     "",
     "      --debug                   do not run as a background process",
     "  -h, --help                    print help and exit",
     "  -v, --verbose                 comprehensive messaging",
     "  -V, --version                 print version and exit",
     "",
     "  -p, --password <password>     specify password for <user@machine1>",
     "      --netrc                   consult ~/.netrc (this is the default)",
     "      --no-netrc                don't consult ~/.netrc",
     "",
     "      --secure                  enable strong encryption with key exchanges",
     "  -K, --key-size <bits>         use encryption key size of <n>",
     "",
     "  -z, --gzip                    compress traffic using gzip compression",
     "",
     "Please send comments, suggestions and bug reports to",
     "    mirrordir@mail.obsidian.co.za",
     "", 0};

    if (exit_code) {
	progmess ("forward: error on command-line", "try `forward --help'");
	exit (exit_code);
    }
    for (q = p; *q; q++)
	puts (*q);

    exit (0);
}

static int opt_password_callback (int a, char *s)
{
    cmdline_password = (char *) strdup (s);
    memset (s, 0, strlen (s));
    return 0;
}

static int pars_opts (int argc, char **argv)
{
    int i;
    char **mandatory_args;

    struct prog_options arg_opts[] =
    {
	{' ', "", "", ARG_STRINGS, 0, 0, 0},
	{'h', "", "--help", ARG_ADD, 0, 0, &show_help},
	{'p', "", "--password", ARG_CALLBACK, 0, 0, (void *) opt_password_callback},
	{'v', "", "--verbose", ARG_ADD, 0, 0, &verbose},
	{'V', "", "--version", ARG_SET, 0, 0, &show_version},
	{0, "", "--no-netrc", ARG_CLEAR, 0, 0, &allow_netrc},
	{0, "", "--netrc", ARG_SET, 0, 0, &allow_netrc},
	{0, "", "--secure", ARG_ADD, 0, 0, &secure},
	{'K', "", "--key-size", ARG_INT, 0, 0, &key_size},
	{'z', "", "--gzip", ARG_SET, 0, 0, &compress_mode},
	{0, "", "--debug", ARG_CLEAR, 0, 0, &daemon_mode},
	{0, 0, 0, 0, 0, 0, 0}
    };

    mandatory_args = malloc (argc * sizeof (char *));
    arg_opts[0].strs = mandatory_args;
    memset (mandatory_args, 0, argc * sizeof (char *));

    i = get_cmdline_options (argc, argv, arg_opts);

    if (show_help)
	usage (0);

    if (show_version) {
#ifdef HAVE_BUILTIN_ARC
	fprintf (stdout, "Mirrordir version " VERSION " (International - builtin encryption)\n");
#else
	fprintf (stdout, "Mirrordir version " VERSION " (US - slow script encryption)\n");
#endif
	exit (0);
    }
    if (i) {
	progmess ("error on cmdline near", argv[i]);
	return 1;
    }
    for (i = 0; mandatory_args[i]; i++);
    if (i < 3) {
	progmess ("not enough args", argv[argc - 1]);
	return 1;
    }
    if (i > 3) {
	progmess ("to many args", mandatory_args[i - 1]);
	return 1;
    }
    key_size = field_type (key_size);
    if (!key_size) {
	progmess ("unsupported key-size", "--key-size");
	return 1;
    }
    secure_mcserv_url = malloc (strlen (mandatory_args[0]) + 10);
    strcpy (secure_mcserv_url, "mc://");
    strcat (secure_mcserv_url, mandatory_args[0]);
    {
	char *p;
	redirect_to_machine = (char *) strdup (mandatory_args[1]);
	p = strchr (redirect_to_machine, ':');
	if (!p) {
	    progmess ("error on cmdline near", mandatory_args[1]);
	    return 1;
	}
	*p++ = '\0';
	redirect_to_port = atoi (p);
	if (!redirect_to_port) {
	    progmess ("error on cmdline near", mandatory_args[1]);
	    return 1;
	}
    }
    listen_on_port = atoi (mandatory_args[2]);
    return 0;
}

/* }}} command line processing */

/* {{{ vfs callbacks */

static void message_callback (char *p,...)
{
    char s[1024];
    va_list pa;
    if (!verbose)
	return;
    va_start (pa, p);
    vsprintf (s, p, pa);
    va_end (pa);
    p = s;
    while ((p = strchr (p, '\n')))
	*p = ' ';
    if (strstr (s, "etting file") || strstr (s, "toring file") ||
	strstr (s, "eading FTP directory") || strstr (s, "ot listing")) {
	fprintf (stdout, "%s", s);
	fflush (stdout);
	fprintf (stdout, "\r");
    } else {
	verbmess (s, 0);
    }
}

static char *anonymous_password_callback (char *host)
{
    return NULL;
}

static char *password_callback (char *a, char *b, char *c, char *host, char *user)
{
    char *p;
    /* password explicitly specified on commandline takes precedence */
    if (cmdline_password)
	return cmdline_password;
    /* then have a look at ~/.netrc */
    if (allow_netrc
	&& (p = lookup_netrc (host, user))) {	/*EK */
	if (verbose)
	    verbmess ("found password in ~/.netrc for", host);
	return p;
    }
    progmess ("no password in ~/.netrc file or on command-line", host);
    exit (1);
    return 0;
}

static int gotinterrupt_callback (void)
{
    exit (1);
    return 0;
}

/* }}} vfs callbacks */


/* {{{ main function */

#define SCRIPT_ACCEPT_MIN_SIZE 80

static int warn_callback (char *f, void *x)
{
/* this can't be interactive - we are running as a daemon usually */
#if 0
    char c[20] = "";
    printf (\
    "You are attempting to connect to a host that you appear to have\n" \
     "never connected to before as this user on this machine. If you\n" \
    "know that you have connected to this host in the past then this\n" \
      "could mean that an infiltration is being attempted. Otherwise\n" \
    "you are advised to copy the hosts " PUBLIC_KEY_DIR " file via a\n" \
    "secure channel (stiffy disk) to\n\t%s\nbefore connected to this\n" \
	    "host for the first time. Type y to continue.\n", f);
    read (0, c, 20);
    if (c[0] == 'y' || c[0] == 'Y')
	return 0;
    printf ("Aborting.\n");
    exit (1);
    return 1;			/* prevents warning */
#else
    return 0;
#endif
}

static void check_scripts (void)
{
    struct stat s;
    if (stat (SCRIPT_ACCEPT, &s)) {
	fprintf (stderr, "Cannot locate script " SCRIPT_ACCEPT ": Mirrordir has not been installed properly");
	exit (1);
    }
    if (s.st_size < SCRIPT_ACCEPT_MIN_SIZE) {
	fprintf (stderr, "Forward has detected that " SCRIPT_ACCEPT " is truncated.\n");
	fprintf (stderr, "This means that you have a US version of this software.\n");
	fprintf (stderr, "run\n\tmirrordir --download-scripts\nto download scripts first.\n");
	exit (1);
    }
}

int main (int argc, char *argv[])
{
    char *p;
    int result;

    check_scripts ();
    diffie_add_functions ();

    p = strrchr (argv[0], PATH_SEP);
    progname = (char *) strdup (p ? p + 1 : argv[0]);

    parser_init ();
    if (pars_opts (argc, argv))
	usage (1);
    if (daemon_mode && fork ())
	exit (0);

    z_socket_set_compression (compress_mode);
    if (compress_mode) {
	if (verbose)
	    verbmess ("compressed connection", 0);
    }
    if (secure) {
	if (verbose)
	    verbmess ("encrypted connection", 0);
	arc_socket_set_flags (ARC_SOCKET_FLAGS_NONCRYPTO_OFF | key_size);
	arc_socket_add_warning_callback (warn_callback, 0);
    } else
	arc_socket_set_flags (ARC_SOCKET_FLAGS_CRYPTO_OFF);

    vfs_set_message_callback (message_callback);
    vfs_set_getpasswd_callback (password_callback);
    vfs_set_getanonpasswd_callback (anonymous_password_callback);
    vfs_set_gotinterrupt_callback (gotinterrupt_callback);

    vfs_init ();
    ftpfs_init_passwd ();

    result = do_accept (listen_on_port, secure_mcserv_url, redirect_to_machine, redirect_to_port);

    vfs_shut ();
    parser_shut ();

    mad_finalize (__FILE__, __LINE__);

    return result;
}

/* }}} main function */


