/*
 * tcp.c - TCP module
 *
 * This file is part of zsh, the Z shell.
 *
 * Copyright (c) 1998-2001 Peter Stephenson
 * All rights reserved.
 *
 * Permission is hereby granted, without written agreement and without
 * license or royalty fees, to use, copy, modify, and distribute this
 * software and to distribute modified versions of this software for any
 * purpose, provided that the above copyright notice and the following
 * two paragraphs appear in all copies of this software.
 *
 * In no event shall Peter Stephenson or the Zsh Development
 * Group be liable to any party for direct, indirect, special, incidental,
 * or consequential damages arising out of the use of this software and
 * its documentation, even if Peter Stephenson, and the Zsh
 * Development Group have been advised of the possibility of such damage.
 *
 * Peter Stephenson and the Zsh Development Group specifically
 * disclaim any warranties, including, but not limited to, the implied
 * warranties of merchantability and fitness for a particular purpose.  The
 * software provided hereunder is on an "as is" basis, and Peter Stephenson
 * and the Zsh Development Group have no obligation to provide maintenance,
 * support, updates, enhancements, or modifications.
 *
 */

/*
 * We need to include the zsh headers later to avoid clashes with
 * the definitions on some systems, however we need the configuration
 * file to decide whether we can include netinet/in_systm.h, which
 * doesn't exist on cygwin.
 */
#include "tcp.h"
#include "tcp.mdh"

/*
 * We use poll() in preference to select because some subset of manuals says
 * that's the thing to do, plus it's a bit less fiddly.  I don't actually
 * have access to a system with poll but not select, however, though
 * both bits of the code have been tested on a machine with both.
 */
#ifdef HAVE_POLL_H
# include <poll.h>
#endif
#if defined(HAVE_POLL) && !defined(POLLIN) && !defined(POLLNORM)
# undef HAVE_POLL
#endif

#ifdef USE_LOCAL_H_ERRNO
int h_errno;
#endif

/* We use the RFC 2553 interfaces.  If the functions don't exist in the
 * library, simulate them. */

#ifndef INET_ADDRSTRLEN
# define INET_ADDRSTRLEN 16
#endif

#ifndef INET6_ADDRSTRLEN
# define INET6_ADDRSTRLEN 46
#endif

/**/
#ifndef HAVE_INET_NTOP

/**/
mod_export char const *
zsh_inet_ntop(int af, void const *cp, char *buf, size_t len)
{       
    if (af != AF_INET) {
	errno = EAFNOSUPPORT;
	return NULL;
    } 
    if (len < INET_ADDRSTRLEN) {
	errno = ENOSPC;
	return NULL;
    }
    strcpy(buf, inet_ntoa(*(struct in_addr *)cp));
    return buf;
}

/**/
#else /* !HAVE_INET_NTOP */

/**/
# define zsh_inet_ntop inet_ntop

/**/
#endif /* !HAVE_INET_NTOP */

/**/
#ifndef HAVE_INET_ATON

# ifndef INADDR_NONE
#  define INADDR_NONE 0xffffffffUL
# endif

/**/
mod_export int zsh_inet_aton(char const *src, struct in_addr *dst)
{
    return (dst->s_addr = inet_addr(src)) != INADDR_NONE;
}

/**/
#else /* !HAVE_INET_ATON */

/**/
# define zsh_inet_aton inet_aton

/**/
#endif /* !HAVE_INET_ATON */

/**/
#ifndef HAVE_INET_PTON

/**/
mod_export int
zsh_inet_pton(int af, char const *src, void *dst)
{
    if (af != AF_INET) {
	errno = EAFNOSUPPORT;
	return -1;
    }
    return !!zsh_inet_aton(src, dst);
}

#else /* !HAVE_INET_PTON */

# define zsh_inet_pton inet_pton

/**/
#endif /* !HAVE_INET_PTON */

/**/
#ifndef HAVE_GETIPNODEBYNAME

/**/
# ifndef HAVE_GETHOSTBYNAME2

/**/
mod_export struct hostent *
zsh_gethostbyname2(char const *name, int af)
{
    if (af != AF_INET) {
	h_errno = NO_RECOVERY;
	return NULL;
    }
    return gethostbyname(name);
}

/**/
#else /* !HAVE_GETHOSTBYNAME2 */

/**/
# define zsh_gethostbyname2 gethostbyname2

/**/
# endif /* !HAVE_GETHOSTBYNAME2 */

/* note: this is not a complete implementation.  If ignores the flags,
   and does not provide the memory allocation of the standard interface.
   Each returned structure will overwrite the previous one. */

/**/
mod_export struct hostent *
zsh_getipnodebyname(char const *name, int af, UNUSED(int flags), int *errorp)
{
    static struct hostent ahe;
    static char nbuf[16];
    static char *addrlist[] = { nbuf, NULL };
# ifdef SUPPORT_IPV6
    static char pbuf[INET6_ADDRSTRLEN];
# else
    static char pbuf[INET_ADDRSTRLEN];
# endif
    struct hostent *he;
    if (zsh_inet_pton(af, name, nbuf) == 1) {
	zsh_inet_ntop(af, nbuf, pbuf, sizeof(pbuf));
	ahe.h_name = pbuf;
	ahe.h_aliases = addrlist+1;
	ahe.h_addrtype = af;
	ahe.h_length = (af == AF_INET) ? 4 : 16;
	ahe.h_addr_list = addrlist;
	return &ahe;
    }
    he = zsh_gethostbyname2(name, af);
    if (!he)
	*errorp = h_errno;
    return he;
}

/**/
mod_export void
freehostent(UNUSED(struct hostent *ptr))
{
}

/**/
#else /* !HAVE_GETIPNODEBYNAME */

/**/
# define zsh_getipnodebyname getipnodebyname

/**/
#endif /* !HAVE_GETIPNODEBYNAME */

LinkList ztcp_sessions;

/* "allocate" a tcp_session */
static Tcp_session
zts_alloc(int ztflags)
{
    Tcp_session sess;

    sess = (Tcp_session)zshcalloc(sizeof(struct tcp_session));
    if (!sess) return NULL;
    sess->fd=-1;
    sess->flags=ztflags;

    zinsertlinknode(ztcp_sessions, lastnode(ztcp_sessions), (void *)sess);

    return sess;
}

/**/
mod_export Tcp_session
tcp_socket(int domain, int type, int protocol, int ztflags)
{
    Tcp_session sess;

    sess = zts_alloc(ztflags);
    if (!sess) return NULL;

    sess->fd = socket(domain, type, protocol);
    return sess;
}

static int
ztcp_free_session(Tcp_session sess)
{
    zfree(sess, sizeof(struct tcp_session));

    return 0;
}

static int
zts_delete(Tcp_session sess)
{
    LinkNode node;

    node = linknodebydatum(ztcp_sessions, (void *)sess);

    if (!node)
    {
	return 1;
    }

    ztcp_free_session(getdata(node));
    remnode(ztcp_sessions, node);

    return 0;
}

static Tcp_session
zts_byfd(int fd)
{
    LinkNode node;
    
    for (node = firstnode(ztcp_sessions); node; incnode(node))
	if (((Tcp_session)getdata(node))->fd == fd)
	    return (Tcp_session)getdata(node);
    
    return NULL;
}

static void
tcp_cleanup(void)
{
    LinkNode node, next;

    for (node = firstnode(ztcp_sessions); node; node = next) {
	next = node->next;
	tcp_close((Tcp_session)getdata(node));
    }
}

/**/
mod_export int
tcp_close(Tcp_session sess)
{
    int err;
    
    if (sess)
    {  
	if (sess->fd != -1)
	{
	    err = close(sess->fd);
	    if (err)
		zwarn("connection close failed: %e", errno);
	}
	zts_delete(sess);
	return 0;
    }

    return -1;
}

/**/
mod_export int
tcp_connect(Tcp_session sess, char *addrp, struct hostent *zhost, int d_port)
{
    int salen;
#ifdef SUPPORT_IPV6
    if (zhost->h_addrtype==AF_INET6) {
	memcpy(&(sess->peer.in6.sin6_addr), addrp, zhost->h_length);
	sess->peer.in6.sin6_port = d_port;
	sess->peer.in6.sin6_flowinfo = 0;
# ifdef HAVE_STRUCT_SOCKADDR_IN6_SIN6_SCOPE_ID
	sess->peer.in6.sin6_scope_id = 0;
# endif
	sess->peer.in6.sin6_family = zhost->h_addrtype;
	salen = sizeof(struct sockaddr_in6);
    } else
#endif /* SUPPORT_IPV6 */
    {
	memcpy(&(sess->peer.in.sin_addr), addrp, zhost->h_length);
	sess->peer.in.sin_port = d_port;
	sess->peer.in.sin_family = zhost->h_addrtype;
	salen = sizeof(struct sockaddr_in);
    }

    return connect(sess->fd, (struct sockaddr *)&(sess->peer), salen);
}

static int
bin_ztcp(char *nam, char **args, Options ops, UNUSED(int func))
{
    int herrno, err=1, destport, force=0, verbose=0, test=0, targetfd=0;
    ZSOCKLEN_T  len;
    char **addrp, *desthost, *localname, *remotename;
    struct hostent *zthost = NULL, *ztpeer = NULL;
    struct servent *srv;
    Tcp_session sess = NULL;

    if (OPT_ISSET(ops,'f'))
	force = 1;

    if (OPT_ISSET(ops,'v'))
	verbose = 1;

    if (OPT_ISSET(ops,'t'))
        test = 1;

    if (OPT_ISSET(ops,'d')) {
	targetfd = atoi(OPT_ARG(ops,'d'));
	if (!targetfd) {
	    zwarnnam(nam, "%s is an invalid argument to -d", OPT_ARG(ops,'d'));
	    return 1;
	}
    }


    if (OPT_ISSET(ops,'c')) {
	if (!args[0]) {
	    tcp_cleanup();
	}
	else {
	    targetfd = atoi(args[0]);
	    sess = zts_byfd(targetfd);
	    if(!targetfd) {
		zwarnnam(nam, "%s is an invalid argument to -c", args[0]);
		return 1;
	    }

	    if (sess)
	    {
		if ((sess->flags & ZTCP_ZFTP) && !force)
		{
		    zwarnnam(nam, "use -f to force closure of a zftp control connection");
		    return 1;
		}
		tcp_close(sess);
		return 0;
	    }
	    else
	    {
		zwarnnam(nam, "fd %s not found in tcp table", args[0]);
		return 1;
	    }
	}
    }
    else if (OPT_ISSET(ops,'l')) {
	int lport = 0;

	if (!args[0]) {
	    zwarnnam(nam, "-l requires an argument");
	    return 1;
	}

	srv = getservbyname(args[0], "tcp");
	if (srv)
	    lport = srv->s_port;
	else
	    lport = htons(atoi(args[0]));
	if (!lport) { zwarnnam(nam, "bad service name or port number");
	return 1;
	}
	sess = tcp_socket(PF_INET, SOCK_STREAM, 0, ZTCP_LISTEN);

	if (!sess) {
	    zwarnnam(nam, "unable to allocate a TCP session slot");
	    return 1;
	}
#ifdef SO_OOBINLINE
	len = 1;
	setsockopt(sess->fd, SOL_SOCKET, SO_OOBINLINE, (char *)&len, sizeof(len));
#endif
	if (!zsh_inet_aton("0.0.0.0", &(sess->sock.in.sin_addr)))
	{
	    zwarnnam(nam, "bad address: %s", "0.0.0.0");
	    return 1;
	}

	sess->sock.in.sin_family = AF_INET;
	sess->sock.in.sin_port = lport;


	if (bind(sess->fd, (struct sockaddr *)&sess->sock.in, sizeof(struct sockaddr_in)))
	{
	    char buf[DIGBUFSIZE];
	    convbase(buf, (zlong)ntohs(lport), 10);
	    zwarnnam(nam, "could not bind to port %s: %e", buf, errno);
	    tcp_close(sess);
	    return 1;
	}

	if (listen(sess->fd, 1))
	{
	    zwarnnam(nam, "could not listen on socket: %e", errno);
	    tcp_close(sess);
	    return 1;
	}

	if (targetfd) {
	    redup(sess->fd,targetfd);
	    sess->fd = targetfd;
	}
	else {
	    /* move the fd since no one will want to read from it */
	    sess->fd = movefd(sess->fd);
	}

	setiparam("REPLY", sess->fd);

	if (verbose)
	    printf("%d listener is on fd %d\n", ntohs(sess->sock.in.sin_port), sess->fd);

	return 0;

    }
    else if (OPT_ISSET(ops,'a'))
    {
	int lfd, rfd;

	if (!args[0]) {
	    zwarnnam(nam, "-a requires an argument");
	    return 1;
	}

	lfd = atoi(args[0]);

	if (!lfd) {
	    zwarnnam(nam, "invalid numerical argument");
	    return 1;
	}

	sess = zts_byfd(lfd);
	if (!sess) {
	    zwarnnam(nam, "fd %s is not registered as a tcp connection", args[0]);
	    return 1;
	}

	if (!(sess->flags & ZTCP_LISTEN))
	{
	    zwarnnam(nam, "tcp connection not a listener");
	    return 1;
	}

	if (test) {
#if defined(HAVE_POLL) || defined(HAVE_SELECT)
# ifdef HAVE_POLL
	    struct pollfd pfd;
	    int ret;

	    pfd.fd = lfd;
	    pfd.events = POLLIN;
	    if ((ret = poll(&pfd, 1, 0)) == 0) return 1;
	    else if (ret == -1)
	    {
		zwarnnam(nam, "poll error: %e", errno);
		return 1;
	    }
# else
	    fd_set rfds;
	    struct timeval tv;
	    int ret;
	    
	    FD_ZERO(&rfds);
	    FD_SET(lfd, &rfds);
	    tv.tv_sec = 0;
	    tv.tv_usec = 0;
	    
	    if ((ret = select(lfd+1, &rfds, NULL, NULL, &tv))) return 1;
	    else if (ret == -1)
	    {
		zwarnnam(nam, "select error: %e", errno);
		return 1;
	    }
	    
# endif
	    
#else
	    zwarnnam(nam, "not currently supported");
	    return 1;
#endif
	}
	sess = zts_alloc(ZTCP_INBOUND);

	len = sizeof(sess->peer.in);
	if ((rfd = accept(lfd, (struct sockaddr *)&sess->peer.in, &len)) == -1)
	{
	    zwarnnam(nam, "could not accept connection: %e", errno);
	    tcp_close(sess);
	    return 1;
	}

	if (targetfd) {
	    redup(rfd, targetfd);
	    sess->fd = targetfd;
	}
	else {
	    sess->fd = rfd;
	}

	setiparam("REPLY", sess->fd);

	if (verbose)
	    printf("%d is on fd %d\n", ntohs(sess->peer.in.sin_port), sess->fd);
    }
    else
    {
	if (!args[0]) {
	    LinkNode node;
	    for(node = firstnode(ztcp_sessions); node; incnode(node))
	    {
		sess = (Tcp_session)getdata(node);

		if (sess->fd != -1)
		{
		    zthost = gethostbyaddr((const void *)&(sess->sock.in.sin_addr), sizeof(sess->sock.in.sin_addr), AF_INET);
		    if (zthost)
			localname = zthost->h_name;
		    else
			localname = ztrdup(inet_ntoa(sess->sock.in.sin_addr));
		    ztpeer = gethostbyaddr((const void *)&(sess->peer.in.sin_addr), sizeof(sess->peer.in.sin_addr), AF_INET);
		    if (ztpeer)
			remotename = ztpeer->h_name;
		    else
			remotename = ztrdup(inet_ntoa(sess->peer.in.sin_addr));
		    if (OPT_ISSET(ops,'L')) {
			int schar;
			if (sess->flags & ZTCP_ZFTP)
			    schar = 'Z';
			else if (sess->flags & ZTCP_LISTEN)
			    schar = 'L';
			else if (sess->flags & ZTCP_INBOUND)
			    schar = 'I';
			else
			    schar = 'O';
			printf("%d %c %s %d %s %d\n",
			       sess->fd, schar,
			       localname, ntohs(sess->sock.in.sin_port),
			       remotename, ntohs(sess->peer.in.sin_port));
		    } else {
			printf("%s:%d %s %s:%d is on fd %d%s\n",
			       localname, ntohs(sess->sock.in.sin_port),
			       ((sess->flags & ZTCP_LISTEN) ? "-<" :
				((sess->flags & ZTCP_INBOUND) ? "<-" : "->")),
			       remotename, ntohs(sess->peer.in.sin_port),
			       sess->fd,
			       (sess->flags & ZTCP_ZFTP) ? " ZFTP" : "");
		    }
		}
	    }
	    return 0;
	}
	else if (!args[1]) {
	    destport = htons(23);
	}
	else {

	    srv = getservbyname(args[1],"tcp");
	    if (srv)
		destport = srv->s_port;
	    else
		destport = htons(atoi(args[1]));
	}
	
	desthost = ztrdup(args[0]);
	
	zthost = zsh_getipnodebyname(desthost, AF_INET, 0, &herrno);
	if (!zthost || errflag) {
	    zwarnnam(nam, "host resolution failure: %s", desthost);
	    return 1;
	}
	
	sess = tcp_socket(PF_INET, SOCK_STREAM, 0, 0);

	if (!sess) {
	    zwarnnam(nam, "unable to allocate a TCP session slot");
	    return 1;
	}

#ifdef SO_OOBINLINE
	len = 1;
	setsockopt(sess->fd, SOL_SOCKET, SO_OOBINLINE, (char *)&len, sizeof(len));
#endif

	if (sess->fd < 0) {
	    zwarnnam(nam, "socket creation failed: %e", errno);
	    zsfree(desthost);
	    zts_delete(sess);
	    return 1;
	}
	
	for (addrp = zthost->h_addr_list; err && *addrp; addrp++) {
	    if (zthost->h_length != 4)
		zwarnnam(nam, "address length mismatch");
	    do {
		err = tcp_connect(sess, *addrp, zthost, destport);
	    } while (err && errno == EINTR && !errflag);
	}
	
	if (err) {
	    zwarnnam(nam, "connection failed: %e", errno);
	    tcp_close(sess);
	    zsfree(desthost);
	    return 1;
	}
	else
	{
	    if (targetfd) {
		redup(sess->fd, targetfd);
		sess->fd = targetfd;
	    }

	    setiparam("REPLY", sess->fd);

	    if (verbose)
		printf("%s:%d is now on fd %d\n",
			desthost, destport, sess->fd);
	}
	
	zsfree(desthost);
    }

    return 0;
}

static struct builtin bintab[] = {
    BUILTIN("ztcp", 0, bin_ztcp, 0, 3, 0, "acd:flLtv", NULL),
};

static struct features module_features = {
    bintab, sizeof(bintab)/sizeof(*bintab),
    NULL, 0,
    NULL, 0,
    NULL, 0,
    0
};

/* The load/unload routines required by the zsh library interface */

/**/
int
setup_(UNUSED(Module m))
{
    return 0;
}

/**/
int
features_(Module m, char ***features)
{
    *features = featuresarray(m, &module_features);
    return 0;
}

/**/
int
enables_(Module m, int **enables)
{
    return handlefeatures(m, &module_features, enables);
}

/**/
int
boot_(Module m)
{
    ztcp_sessions = znewlinklist();
    return 0;
}


/**/
int
cleanup_(Module m)
{
    tcp_cleanup();
    freelinklist(ztcp_sessions, (FreeFunc) ztcp_free_session);
    return setfeatureenables(m, &module_features, NULL);
}

/**/
int
finish_(UNUSED(Module m))
{
    return 0;
}
