/*
 *  Kerberized NNTP connection forwarder, server side.
 *
 *  $Id: knntpd.c,v 1.1 1999/12/12 08:36:55 kolya Exp $
 *
 *  kolya@MIT.EDU, 11 December 1999
 */

#include "krb5.h"
#include "com_err.h"

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <syslog.h>
#include <fcntl.h>

#include "knntp.h"

extern krb5_deltat krb5_clockskew;
krb5_context context;
krb5_auth_context auth_context = NULL;

void xmit_error(char *msg, int sock) {
    write(sock, msg, strlen(msg));

    krb5_auth_con_free(context, auth_context);
    krb5_free_context(context);
    exit(1);
}

void usage(char *name) {
    fprintf(stderr, "usage: %s [-p port] [-s service] [-S keytab]\n",
	    name);
}	

int main(int argc, char *argv[])
{
    krb5_ticket * ticket;
    krb5_address peeraddr;
    struct sockaddr_in server_sin;
    struct sockaddr_in peername;
    int namelen = sizeof(peername);
    int listen_sock = 0;
    int sock = -1;			/* incoming connection fd */
    int port = KNNTP_PORT;
    krb5_error_code retval;
    krb5_principal server, client;
    char repbuf[BUFSIZ];
    char read_buf[BUFSIZ];
    char *cname;
    char *service = KNNTP_SERVICE;
    extern int opterr, optind;
    extern char * optarg;
    int ch;
    int on = 1;
    krb5_keytab keytab = NULL;	/* Allow specification on command line */
    char *progname = *argv;
    int authorized = 0;
    fd_set all_fds;

    /* open a log connection */
    openlog("knntpd", 0, LOG_DAEMON);

    /*
     * Parse command line arguments
     *  
     */
    opterr = 0;
    while ((ch = getopt(argc, argv, "p:S:s:")) != EOF)
    switch (ch) {
    case 'p':
	port = atoi(optarg);
	break;
    case 's':
	service = optarg;
	break;
    case 'S':
	if (retval = krb5_kt_resolve(context, optarg, &keytab)) {
	    com_err(progname, retval,
		    "while resolving keytab file %s", optarg);
	    exit(2);
	}
	break;

    case '?':
    default:
	usage(progname);
	exit(1);
	break;
    }

    argc -= optind;
    argv += optind;

    listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(listen_sock < 0) {
	perror("creating socket");
	exit(1);
    }
    setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on));

    server_sin.sin_port = htons(port);
    server_sin.sin_family = AF_INET;
    server_sin.sin_addr.s_addr = htonl(INADDR_ANY);

    if(bind(listen_sock, (struct sockaddr *)&server_sin, sizeof(server_sin))) {
	perror("binding to socket");
	exit(1);
    }

    if(listen(listen_sock, 5) < 0) {
	perror("listening on socket");
	exit(1);
    }

    namelen = sizeof(peername);
    while((sock = accept(listen_sock, (struct sockaddr *)&peername, &namelen)) >= 0) {
	if(!fork())
	    break;
	else
	    close(sock);
    }

    if(sock < 0) {
	perror("accepting connection");
	exit(1);
    }

    retval = krb5_init_context(&context);
    if (retval) {
	    com_err(argv[0], retval, "while initializing krb5");
	    exit(1);
    }

    if (retval = krb5_sname_to_principal(context, NULL, service, 
					 KRB5_NT_SRV_HST, &server)) {
	syslog(LOG_ERR, "while generating service name (%s): %s",
	       service, error_message(retval));
	exit(1);
    }
    
    /*
     * To verify authenticity, we need to know the address of the
     * client.
     */
    peeraddr.addrtype = peername.sin_family;
    peeraddr.length = sizeof(peername.sin_addr);
    peeraddr.contents = (krb5_octet *)&peername.sin_addr;

    if (retval = krb5_recvauth(context, &auth_context, (krb5_pointer)&sock,
			       KNNTP_VERSION, server, 
			       0,	/* no flags */
			       keytab,	/* default keytab is NULL */
			       &ticket)) {
	syslog(LOG_ERR, "recvauth failed--%s", error_message(retval));
	exit(1);
    }

    /* Get client name */
    if (retval = krb5_unparse_name(context, ticket->enc_part2->client, &cname)){
	syslog(LOG_ERR, "unparse failed: %s", error_message(retval));
	sprintf(repbuf, "502 krb5_unparse_name() error\n");
    } else {
	char *at_ptr;

	at_ptr = strchr(cname, '@');
	if(!at_ptr) {
	    sprintf(repbuf, "502 Principal missing @ character\n");
	} else {
	    if(strcmp(at_ptr+1, ALLOW_REALM)) {
		snprintf(repbuf, sizeof(repbuf),
			 "502 Realm `%s' not authorized\n", at_ptr+1);
	    } else {
		authorized = 1;
	    }
	}

	free(cname);
    }

    if(!authorized) {
	xmit_error(repbuf, sock);
    } else {
	/* The user is authorized to access the news server; connect */
	struct hostent *hp;
	struct sockaddr_in sin;
	int news_sock;

	news_sock = socket(AF_INET, SOCK_STREAM, 0);
	if(news_sock < 0) {
	    xmit_error("502 Error creating socket\n", sock);
	}

	memset((char *)&sin, 0, sizeof(sin));
	sin.sin_family = AF_INET;
	sin.sin_port = htons(NNTP_PORT);

	hp = gethostbyname(NNTP_SERVER);
	if(!hp) {
	    xmit_error("502 Unable to resolve news server hostname\n", sock);
	}
	memcpy((char *)&sin.sin_addr, (char *)hp->h_addr, sizeof(hp->h_addr));

	if(connect(news_sock, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
	    snprintf(repbuf, sizeof(repbuf),
		     "502 Error connecting to news server: %s\n",
		     strerror(errno));
	    xmit_error(repbuf, sock);
	}

	fcntl(sock, F_SETFL, O_NONBLOCK);
	fcntl(news_sock, F_SETFL, O_NONBLOCK);

	FD_ZERO(&all_fds);
	FD_SET(sock, &all_fds);
	FD_SET(news_sock, &all_fds);

	while(1) {
	    fd_set rfds = all_fds;
	    int maxFds = MAX(sock, news_sock) + 1;

	    retval = select(maxFds, &rfds, NULL, NULL, NULL);
	    if(retval < 0) {
		snprintf(repbuf, sizeof(repbuf),
			 "%%m: select error: %d", retval);
		syslog(LOG_ERR, repbuf);
		exit(1);
	    }

	    if(FD_ISSET(news_sock, &rfds)) {
		retval = read(news_sock, read_buf, sizeof(read_buf));
		if(retval <= 0) {
		    if(errno != EAGAIN)
			exit(1);
		} else {
		    write(sock, read_buf, retval);
		}
	    }

	    if(FD_ISSET(sock, &rfds)) {
		retval = read(sock, &read_buf, sizeof(read_buf));
		if(retval <= 0) {
		    exit(1);
		}

		/* XXX assuming write() sends everything at once */
		write(news_sock, read_buf, retval);
	    }
	}
    }

    krb5_auth_con_free(context, auth_context);
    krb5_free_context(context);
    exit(0);
}

