#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netdb.h>		/* for gethostbyname */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
extern int errno;

#define VERSION "2.5.1"

#ifndef INADDR_NONE
#define INADDR_NONE 0xffffffff	/* -1 return */
#endif /* INADDR_NONE */

#define KEEPALIVE_TIMEOUT 900	/* 15 min. */

int tty;
int timeout_sec = KEEPALIVE_TIMEOUT;
int timeout_flag;
int crlf_flag = 0;

void timeout(int sig)
{
    timeout_flag++;
}

int xread(int fd, char* buff, int n)
{
    if(timeout_sec > 0)
    {
	timeout_flag = 0;
	alarm(timeout_sec);
	timeout_flag = 0;
	if((n = read(fd, buff, n)) < 0)
	{
	    if(timeout_flag && errno == EINTR)
	    {
		write(2, "TIMEOUT\n", 8);
		exit(0);
	    }
	    else
		return n;
	}
	alarm(0);
    }
    else
	n = read(fd, buff, n);
    return n;
}

#ifdef KEEPALIVE_TIMEOUT
#define read(fd, buff, n) xread(fd, buff, n)
#endif /* KEEPALIVE_TIMEOUT */

#define SWAP(a, b, tmp) tmp=a; a=b; b=tmp
void swap_endian(char* x, int size)
{
    char tmp;

    switch(size)
    {
      case 2:
	SWAP(x[0], x[1], tmp);
	break;
      case 4:
	SWAP(x[0], x[3], tmp);
	SWAP(x[1], x[2], tmp);
	break;
    }
}

/* Return internet IP address in network byte order */
unsigned int host_ip_address(const char* address)
{
    unsigned int addr;
    struct hostent *hp;

    if((addr = inet_addr(address)) != INADDR_NONE)
	return addr;

    if((hp = gethostbyname(address)) == NULL)
    {
	fprintf(stderr, "Unknown host name: %s\n", address);
	exit(1);
    }
    memcpy(&addr, hp->h_addr, hp->h_length);
    return addr;
}

int bind_sock(int sfd, unsigned short port, unsigned int addr)
{
    struct sockaddr_in in;

    memset(&in, 0, sizeof(in));
    in.sin_port        = htons(port);
    in.sin_family      = AF_INET;
    in.sin_addr.s_addr = addr;

#ifdef SO_REUSEADDR
    {
	int on=1;
	setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, (caddr_t)&on, sizeof(on));
    }
#endif /* SO_REUSEADDR */
    return bind(sfd, (struct sockaddr *)&in, sizeof(in));
}

int connect_to_server(const char* hostname,
		      unsigned short tcp_port, unsigned short local_port)
{
    int fd, len;
    struct sockaddr_in in;
    unsigned int addr;

    if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
	perror("socket");
	exit(1);
    }

    memset(&in, 0, sizeof(in));
    in.sin_family = AF_INET;
    in.sin_port   = htons(tcp_port);
    addr = host_ip_address(hostname);
    memcpy(&in.sin_addr, &addr, 4);

    if(local_port > 0)
	if(bind_sock(fd, local_port, 0) < 0)
	{
	    perror("bind");
	    exit(1);
	}


    if(tty)
    {
	struct hostent *hp;
	hp = gethostbyaddr((unsigned char *)&in.sin_addr, 4, AF_INET);
	if(hp != NULL)
	    fprintf(stderr, "Try to connect to %s (addr=%s, port=%d)\n",
		    hp->h_name,
		    inet_ntoa(in.sin_addr),
		    tcp_port);
	else
	    fprintf(stderr, "Try to connect to %s (port=%d)\n",
		    inet_ntoa(in.sin_addr), tcp_port);
    }

    alarm(2 * 60);
    if(connect(fd, (struct sockaddr *)&in, sizeof(in)) < 0)
    {
	perror("connect");
	exit(1);
    }
    alarm(0);

    len = sizeof(in);
    if(getsockname(fd, (struct sockaddr *)&in, &len) < 0)
	perror("getsockname");
    else if(tty)
	fprintf(stderr, "Local Port: %d\n", ntohs(in.sin_port));

    return fd;
}

int open_server_socket(unsigned short port)
{
    int sfd;

    if((sfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
	perror("sokcet");
	exit(1);
    }

    if(bind_sock(sfd, port, INADDR_ANY) < 0)
    {
	perror("bind");
	exit(1);
    }

    /* Set it up to wait for connections. */
    if(listen(sfd, SOMAXCONN) < 0)
    {
	perror("listen");
	close(sfd);
	exit(1);
    }

    return sfd;
}

int wait_connection(int fd)
{
    int cnt, n;
    fd_set rmask;
    char buff[BUFSIZ];

    FD_ZERO(&rmask);
    FD_SET(fd, &rmask);
    FD_SET(0,  &rmask);

  select_proc:
    cnt = select(fd + 1, &rmask, NULL, NULL, NULL);
    if(cnt < 0)
    {
	perror("select");
	exit(1);
    }
    if(cnt == 0)
    {
	sleep(1);
	goto select_proc;
    }
    if(FD_ISSET(0, &rmask))
    {
	n = read(0, buff, BUFSIZ);
	if(n < 0)
	{
	    perror("read");
	    return -1;
	}
	if(n == 0)
	    return 0;
	write(1, buff, n);
    }
    if(FD_ISSET(fd, &rmask))
	return 1;
    sleep(1);
    goto select_proc;
}

unsigned short service_port(const char* s)
{
    unsigned short port;
    if('0' <= *s && *s <= '9')
	port = atoi(s);
    else
    {
	struct servent* sp;
	sp = getservbyname(s, "tcp");

	if(sp == NULL)
	{
	    fprintf(stderr, "Unknown service `%s'\n", s);
	    exit(1);
	}
	port = ntohs(sp->s_port);
    }
    return port;
}

int xwrite(int fd, char *buff, int size)
{
    int ret;

    if(crlf_flag == 0 || size == 0)
    {
	return write(fd, buff, size);
    }

    ret = 0;
    while(size > 0)
    {
	char *lfpt, backch;
	int n;

	if((lfpt = strchr(buff, '\n')) == NULL)
	{
	    ret += write(fd, buff, size);
	    return ret;
	}

	n = lfpt - buff;
	backch = lfpt[1];
	lfpt[0] = '\r';
	lfpt[1] = '\n';
	write(fd, buff, n + 2);
	n++;
	buff += n;
	size -= n;
	buff[0] = backch;
    }
    return size;
}

char* progname;
void usage(void)
{
    fprintf(stderr, "Usage: %s [opts] hostname {tcp_port|service} [{tcp_port|service}]\n", progname);
    fprintf(stderr, "       %s [opts] -pasv {tcp_port|service}\n", progname);
    fprintf(stderr, "Opts:\n");
    fprintf(stderr, "-h                    help\n");
    fprintf(stderr, "-timeout <timeout>    time out (default is 900)\n");
    fprintf(stderr, "                      '-timeout -' means no time out\n");
    fprintf(stderr, "-V                    print version.\n");
    fprintf(stderr, "-bufsiz <buffer-size> buffer size (default is %d)\n",
	    BUFSIZ);
    fprintf(stderr, "-crlf                 map <lf> to <cr><lf>\n");
}

int main(int argc, char** argv)
{
    int fd = -1, sfd = -1;
    int pasv_flag = 0;
    char* buff = NULL;
    int buff_size = 0;
    unsigned short tcp_port = 0;
    unsigned short local_port = 0;
    char* hostname = NULL;
    fd_set rmask;
    int cnt;
    int n, i;

    progname = argv[0];

    tty = isatty(1) && isatty(0);

    /* parse args */
    for(i = 1; i < argc; i++)
    {
	if(!strcmp(argv[i], "-pasv"))
	{
	    pasv_flag = 1;
	}
	else if(!strcmp(argv[i], "-timeout"))
	{
	    if(++i == argc) { usage(); return 0; }
	    if(argv[i][0] == '-')
		timeout_sec = 0;
	    else
		timeout_sec = atoi(argv[i]);
	}
	else if(!strcmp(argv[i], "-bufsiz"))
	{
	    if(++i == argc) { usage(); return 0; }
	    buff_size = atoi(argv[i]);
	}
	else if(!strcmp(argv[i], "-h"))
	{
	    usage();
	    return 0;
	}
	else if(!strcmp(argv[i], "-crlf"))
	{
	    crlf_flag = 1;
	}
	else if(!strcmp(argv[i], "-V"))
	{
	    fprintf(stderr, "%s Version %s\n", progname, VERSION);
	    return 0;
	}
	else if(argv[i][0] == '-')
	{
	    usage();
	    return 1;
	}
	else
	    break;
    }
    if(pasv_flag)
    {
	if(i == argc)
	{
	    usage();
	    return 1;
	}
	tcp_port = service_port(argv[i]);
    }
    else
    {
	if(i + 2 != argc && i + 3 != argc)
	{
	    usage();
	    return 1;
	}
	hostname = argv[i];
	tcp_port = service_port(argv[i + 1]);
	if(i + 3 == argc)
	    local_port = service_port(argv[i + 2]);
    }

    if(buff_size <= 0)
	buff_size = BUFSIZ;
    if((buff = (char *)malloc(buff_size + 1)) == NULL)
    {
	perror("malloc");
	return 1;
    }

    signal(SIGALRM, timeout);
    signal(SIGPIPE, SIG_IGN);

    if(pasv_flag)		/* passive open */
	sfd = open_server_socket(tcp_port);
    else			/* active open */
	fd = connect_to_server(hostname, tcp_port, local_port);

    do
    {
	if(pasv_flag)
	{
	    struct sockaddr_in client_data;
	    int len;

	    if(wait_connection(sfd) != 1)
		break;

	    len = sizeof(client_data);
	    if((fd = accept(sfd, (struct sockaddr *)&client_data, &len)) < 0)
	    {
		perror("accest");
		break;
	    }

	    if(tty)
	    {
		struct hostent *hp;
		struct in_addr addr;

		memcpy(&addr, &client_data.sin_addr, 4);
		hp = gethostbyaddr((unsigned char *)&addr, 4, AF_INET);
		fprintf(stderr, "Client %s (Port=%u) is ready!\n",
			(hp && hp->h_name ? hp->h_name : inet_ntoa(addr)),
			client_data.sin_port);
	    }
	}

	while(1)
	{
	    FD_ZERO(&rmask);
	    FD_SET(fd, &rmask);
	    FD_SET(0,  &rmask);

	    cnt = select(fd + 1, &rmask, NULL, NULL, NULL);
	    if(cnt < 0)
	    {
		perror("select");
		return 1;
	    }

	    if(FD_ISSET(0, &rmask))
	    {
		n = read(0, buff, buff_size);
		if(n < 0)
		{
		    perror("read");
		    close(fd);
		    return 1;
		}
		if(n == 0)
		{
		    shutdown(fd, 1);
		    while((n = read(fd, buff, buff_size)) > 0)
			write(1, buff, n);
		    if(n < 0)
			perror("read");
		    shutdown(fd, 2);
		    close(fd);
		    break;
		}
		xwrite(fd, buff, n);
		cnt--;
	    }

	    if(cnt > 0 && FD_ISSET(fd, &rmask))
	    {
		n = read(fd, buff, buff_size);
		if(n < 0)
		{
		    perror("read");
		    close(fd);
		    return 1;
		}
		if(n == 0)
		    break;
		write(1, buff, n);
	    }
	}
	if(tty)
	    fprintf(stderr, "Connection is closed.\n");
	if(pasv_flag)
	    shutdown(fd, 2);
	close(fd);
    } while(pasv_flag);

    if(pasv_flag)
	shutdown(sfd, 2);
    return 0;
}
