/*
nntputil.c -- misc nntp-related stuff

Written by Arnt Gulbrandsen <agulbra@troll.no> and copyright 1995
Troll Tech AS, Postboks 6133 Etterstad, 0602 Oslo, Norway, fax +47
22646949.
Modified by Cornelius Krasel <krasel@wpxx02.toxi.uni-wuerzburg.de>
and Randolf Skerka <Randolf.Skerka@gmx.de>.
Copyright of the modifications 1997.
Modified by Kent Robotti <robotti@erols.com>. Copyright of the
modifications 1998.
Modified by Markus Enzenberger <enz@cip.physik.uni-muenchen.de>.
Copyright of the modifications 1998.
Modified by Cornelius Krasel <krasel@wpxx02.toxi.uni-wuerzburg.de>.
Copyright of the modifications 1998, 1999.
Modified by Ralf Wildenhues <ralf.wildenhues@gmx.de>.
Copyright of the modifications 2002.
Modified by Matthias Andree <matthias.andree@gmx.de>.
Copyright of the modifications 2000 - 2003.

See file COPYING for restrictions on the use of this software.
*/

#include "system.h"
#include "leafnode.h"
#include "mysigact.h"

#include <fcntl.h>
#include <sys/uio.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <netinet/in.h>
#ifndef __LCLINT__
#include <arpa/inet.h>
#endif				/* not __LCLINT__ */
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
#include <netdb.h>
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <syslog.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <dirent.h>

#include "ln_log.h"

char last_command[SIZE_lineout + 1];
char lineout[SIZE_lineout + 1];

/*@relnull@*//*@dependent@*/ FILE *nntpin = NULL;
/*@relnull@*//*@dependent@*/ FILE *nntpout = NULL;

int stat_is_evil = 0;
int date_is_evil = 0;

static int xnntpreply(const struct server *, int);

static void authsucc(const struct server *current_server) {
    if (verbose)
	printf("%s: authenticated as %s\n", current_server->name, current_server->username);
    syslog(LOG_INFO, "%s: authenticated as %s", current_server->name, current_server->username);
}

/*
05/26/97 - T. Sweeney - Send a string out, keeping a copy in reserve.
*/
void
putaline(void)
{
    if (debug >= 1) {
	char y, *x = lineout + strcspn(lineout, "\r\n");

	y = *x; *x = '\0';
	syslog(LOG_DEBUG, ">%s", lineout);
	*x = y;
    }
    strcpy(last_command, lineout);	/* RATS: ignore */
    fputs(lineout, nntpout);
    fflush(nntpout);
}

/*
 * Authenticate ourselves at a remote server.
 * Returns TRUE if authentication succeeds, FALSE if it does not.
 * Error will have been logged in case of a FALSE return,
 * no log output if TRUE returned.
 */
int
authenticate(const struct server *current_server)
{
    int reply;

    if (!current_server) {
	ln_log(LNLOG_SERR, LNLOG_CTOP,
		"authenticate: internal error: current_server is NULL, aborting");
	abort();
    }

    if (!current_server->username) {
	ln_log(LNLOG_SERR, LNLOG_CTOP, "error: %s: username needed for authentication",
	       current_server->name);
	return FALSE;
    }

    fprintf(nntpout, "AUTHINFO USER %s\r\n", current_server->username);
    fflush(nntpout);
    if (debugmode)
	syslog(LOG_DEBUG, ">AUTHINFO USER %s", current_server->username);

    reply = xnntpreply(current_server, 0);
    if (reply == 281) {
	authsucc(current_server);
	return TRUE;
    } else if (reply != 381) {
	ln_log(LNLOG_SERR, LNLOG_CSERVER, "error: %s: AUTHINFO USER rejected: %03d",
		current_server->name, reply);
	return FALSE;
    }

    if (!current_server->password) {
	ln_log(LNLOG_SERR, LNLOG_CTOP, "error: %s: password needed for authentication",
	       current_server->name);
	return FALSE;
    }
    /* DO NOT LOG THIS: */
    fprintf(nntpout, "AUTHINFO PASS %s\r\n", current_server->password);
    fflush(nntpout);
    if (debugmode)
	syslog(LOG_DEBUG, ">AUTHINFO PASS <password not shown>");

    reply = xnntpreply(current_server, 0);

    if (reply != 281) {
	ln_log(LNLOG_SERR, LNLOG_CTOP, "error: AUTHINFO PASS failed: %03d", reply);
	return FALSE;
    }
    authsucc(current_server);
    return TRUE;
}

static size_t lllen = 0;
static
				    /*@null@*/
 /*@owned@*/
char *llstr = NULL;

void freelastreply(void)
{
    if (!llstr) return; 
    free(llstr);
    llstr = NULL;
}

/*@dependent@*//*@null@*/ char *
lastreply(void)
{
    return llstr;
}

/**
 * decode an NNTP reply number
 * reads a line from the server and returns an integer
 *
 * 498 is used to mean "protocol error", like smail,
 * and includes timeout and "server disconnect" (EOF)
 * conditions
 *
 * the text returned is stored in lllen/llstr
 * for later retrieval by lastreply()
 *
 * from Tim Sweeney: retry in case of AUTHINFO failure.
 */
static int
xnntpreply(const struct server *current_server,
	/** set this to true to enable authentication after 480 reply */
	int may_auth)
{
    char *response;
    int r = 0;
    int c;

    do {
	response = mgetaline(nntpin);
	if (!response) {
	    if (llstr)
		free(llstr);
	    llstr = NULL;
	    ln_log(LNLOG_SERR, LNLOG_CTOP, "error: NNTP server went away (server disconnect or timeout)");
	    return 498;
	}
	if (debug == 1) syslog(LOG_DEBUG, "<%s", response);

	/* cache line */
	if (strlen(response) > lllen || !llstr) {
	    if (llstr)
		free(llstr);
	    lllen = strlen(response);
	    llstr = critmalloc(lllen + 1, "nntpreply");
	}
	strcpy(llstr, response);	/* RATS: ignore */
	if (strlen(response) > 2
		&& isdigit((unsigned char)response[0])
		&& isdigit((unsigned char)response[1])
		&& isdigit((unsigned char)response[2])
		&& ((response[3] == ' ')
		    || (response[3] == '\0')
		    || (response[3] == '-'))) {
	    int rl;
	    rl = atoi(response);
	    if (r > 0 && r != rl) {
		ln_log(LNLOG_SERR, LNLOG_CTOP, "error: multiline reply with variant error code (%d vs. %d), last line: %s", r, rl, response);
		r = 498;	/* protocol error */
	    } else
		r = rl;
	    c = (response[3] == '-');
	} else {
	    ln_log(LNLOG_SERR, LNLOG_CTOP, "error: syntax error in reply \"%s\"", response);
	    c = 0;
	    r = 498;		/* protocol error */
	}
    } while(c);

    if (r == 480 && may_auth) {	/* need to authenticate */
	char *x = critstrdup(last_command, "xnntpreply");
	x[strcspn(last_command, "\r\n")] = '\0';
	if (debugmode)
	    syslog(LOG_DEBUG, "%s: requested authentication for command \"%s\"",
		    current_server->name, x);
	if (verbose)
	    printf("%s: requested authentication for command \"%s\"\n",
		    current_server->name, x);
	free(x);
	if (authenticate(current_server)) {
	    strcpy(lineout, last_command);
	    putaline();
	    r = xnntpreply(current_server, 0);
	}
    }
    return r;
}

int nntpreply(const struct server *s) {
    return xnntpreply(s, 1);
}

struct versions {
    const char *name;
    int is_evil;
};

/*
 * NewsCache 0.99.17 and previous versions always
 * reply with 223 0 <MID> when asked "STAT <MID>". This
 * is a violation of RFC 977 and breaks posting.
 */
static struct versions stat_versions[] = {
    { "NewsCache 0.99.2 ", 1 },
    { "NewsCache 0.99.2", 0 },
    { "NewsCache 0.99.19", 0 },
    { "NewsCache 0.99.18", 0 },
    { "NewsCache 1.0.", 1 },
    { "NewsCache 1", 0 },
    { "NewsCache", 1 },
    /* reported to be necessary by 
     * Robert Marshall <robert@chezmarshall.freeserve.co.uk>:
     * nc news.cache.ntlworld.com 119
     * 200 ntl NNTP news cache. posting ok (feedback to nntptrial-feedback@ntli.net)
     * quit
     * 205 NNTP Service closing connection - goodbye!
     */
    { "NNTP news cache", 1 },
};
static const int stat_count = sizeof(stat_versions)/sizeof(stat_versions[0]);

static struct versions date_versions[] = {
    { "NewsCache 0.99.22p", 0 },
    { "NewsCache 0.99.2 ", 1 },
    { "NewsCache 0.99.20", 1 },
    { "NewsCache 0.99.21", 1 },
    { "NewsCache 0.99.22", 1 },
    { "NewsCache 0.99.2", 0 },
    { "NewsCache 1.1.10 ", 1 },
    { "NewsCache 1.1.11 ", 1 },
    { "NewsCache 1.1.1 ", 1 },
    { "NewsCache 1.1.0", 1},
    { "NewsCache 1", 0},
    { "NewsCache", 1 }
};
static const int date_count = sizeof(date_versions)/sizeof(date_versions[0]);

static int check_linlist(const char *s, const struct versions *list, int count) {
    int i;

    for (i = 0; i < count; i++) {
	if (strstr(s, list[i].name)) {
	    return list[i].is_evil;
	}
    }
    return 0;
}

#define incopy(a)       (*((struct in_addr *)a))

/*
 * connect to upstream nntp server
 *
 * returns 200 for posting allowed, 201 for read-only;
 * if connection failed, return 0
 */
int
nntpconnect(const struct server *upstream)
{
    static /*@observer@*/ struct servent *sp;
    struct servent sp_def;
#ifdef HAVE_IPV6
    struct addrinfo hints, *ai;
    struct addrinfo *volatile aii;
    char buf[INET6_ADDRSTRLEN+1];
#else
    struct sockaddr_in s_in;
    struct hostent *hp;
    char buf[16];
    volatile int i;
#endif
    int sock, reply, e, ds;
    char *line;

    if (upstream->port == 0) {
	sp = getservbyname("nntp", "tcp");
	if (sp == NULL) {
	    ln_log(LNLOG_SERR, LNLOG_CTOP, "error: unable to find service name nntp/tcp");
	    return FALSE;
	}
    } else {
	sp = &sp_def;
	sp->s_port = htons(upstream->port);
    }

    sprintf(buf, "%hu", ntohs(sp->s_port));

    /* Fetch the ip addresses of the given host. */
#ifdef HAVE_IPV6
    memset((void *)&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags = AI_CANONNAME|AI_ADDRCONFIG;

    e = getaddrinfo(upstream->name, buf, &hints, &ai);
    if (e == 0) {
	for (aii = ai; aii; aii = aii->ai_next) {
	    sock = socket(aii->ai_family, SOCK_STREAM, 0);
	    if (sock < 0) {
		ln_log(LNLOG_SERR, LNLOG_CTOP,
			"error: cannot create inet/stream socket: %m");
		break;
	    }
	    switch(aii->ai_family) {
		case AF_INET6:
		    inet_ntop(aii->ai_family, &((const struct sockaddr_in6 *)aii->ai_addr)->sin6_addr, buf, sizeof(buf));
		    break;
		case AF_INET:
		    inet_ntop(aii->ai_family, &((const struct sockaddr_in *)aii->ai_addr)->sin_addr, buf, sizeof(buf));
		    break;
		default:
		    strcpy(buf, "UNKNOWN");
	    }
#else
    hp = gethostbyname(upstream->name);
    if (hp) {
	/* Try to make connection to each of the addresses in turn. */
	for (i = 0; (int *)(hp->h_addr_list)[i]; i++) {
	    sock = socket(AF_INET, SOCK_STREAM, 0);
	    if (sock < 0) {
		ln_log(LNLOG_SERR, LNLOG_CTOP,
			"error: cannot create inet/stream socket: %m");
		break;
	    }
	    strcpy(buf, inet_ntoa(s_in.sin_addr));
#endif

	    if (sigsetjmp(timeout,1) != 0) {
		ln_log(LNLOG_SWARNING, LNLOG_CTOP,
			"warning: %s: connection to %s timed out",
			upstream->name, buf);
		(void)close(sock);
		continue;
	    }

	    (void)mysigact(SIGALRM, SA_RESETHAND, timer, 0);
	    (void)alarm((unsigned)upstream->timeout);

#ifdef HAVE_IPV6
	    e = connect(sock, aii->ai_addr, aii->ai_addrlen);
	    if (e) {
		ln_log(LNLOG_SWARNING, LNLOG_CTOP,
			"warning: %s: connection to %s failed: %m",
			upstream->name, buf);
	    }
#else
	    memset((void *)&s_in, 0, sizeof(s_in));
	    s_in.sin_family = hp->h_addrtype;
	    s_in.sin_port = sp->s_port;
	    s_in.sin_addr = incopy(hp->h_addr_list[i]);

	    e = connect(sock, (struct sockaddr *)&s_in, sizeof(s_in));
	    if (e)
		ln_log(LNLOG_SWARNING, LNLOG_CTOP,
			"warning: %s: connection to %s failed: %m", 
			upstream->name, inet_ntoa(s_in.sin_addr));
#endif
	    (void)alarm(0U);
	    (void)mysigact(SIGALRM, 0, SIG_DFL, 0);
	    if (e)
		continue;

	    nntpout = fdopen(sock, "w");
	    if (nntpout == NULL) {
		ln_log(LNLOG_SERR, LNLOG_CTOP,
			"error: %s: fdopen(%d, \"w\") failed: %m",
			upstream->name, sock);
		break;
	    }

	    if ((ds = dup(sock)) < 0) {
		ln_log(LNLOG_SERR, LNLOG_CTOP,
			"error: %s: dup(%d) failed returning %d: %m",
			upstream->name, sock, ds);
		break;
	    }

	    nntpin = fdopen(ds, "r");
	    if (nntpin == NULL) {
		ln_log(LNLOG_SERR, LNLOG_CTOP,
			"error: %s: fdopen(%d, \"r\") failed: %m",
			upstream->name, sock);
		break;
	    }

	    reply = nntpreply(upstream);
	    if (reply == 200 || reply == 201) {
		syslog(LOG_INFO, "%s: connected to %s:%hd, reply: %d",
			upstream->name, buf, ntohs(sp->s_port), reply);
		line = lastreply();
		if (line == NULL) {
		    ln_log(LNLOG_SWARNING, LNLOG_CTOP,
			    "warning: %s: server disconnect or timeout before sending the greeting",
			    upstream->name);
		    nntpdisconnect();
		    continue;
		}

		if (strstr(line, "NNTPcache server V2.3")) {
		    /* NNTPcache 2.3.3 is still in widespread use, but it
		     * has Y2k bugs which have only been fixed in a beta
		     * version as of 2001-12-24. This 2.3 version is
		     * unsuitable for any use since 2000-01-01. */
		    static const char msg[] =
			"error: %s: Server greeting \"%s\" hints to "
			"NNTPcache v2.3.x. "
			"This server has severe (Year 2000) bugs which make it "
			"unsuitable for use with leafnode. "
			"Ask the news server administrator to update to "
			"NNTPcache v3.0.x or newer.";
		    ln_log(LNLOG_SERR, LNLOG_CTOP, msg, upstream->name, line);
		    nntpquit();
		    continue;
		}
		stat_is_evil = check_linlist(lastreply(), stat_versions,
			stat_count);
		date_is_evil = check_linlist(lastreply(), date_versions,
			date_count);
		if (stat_is_evil) {
		    syslog(LOG_WARNING, "warning: server \"%s\" greeting "
			    "\"%s\" hints to "
			    "an outdated version with broken "
			    "STAT command handling. Please ask the upstream "
			    "maintainer to update. "
			    "Emulating STAT with HEAD at the expense of bandwidth.",
			    upstream->name, line);
		}
#ifdef HAVE_IPV6
		if (ai)
		    freeaddrinfo(ai);
#endif
		return reply;
	    } else { /* reply not 200 and not 201 */
		char *ll = lastreply();
		ln_log(LNLOG_SERR, LNLOG_CTOP, "error: %s: received bogus greeting (%d): %s",
			upstream->name, reply, ll ? ll : "(nil)");
		nntpquit();
	    }
	} /* end of IP-addresses for loop */
#ifdef HAVE_IPV6
	if (!aii)
#else
	if (!(int *)(hp->h_addr_list)[i])
#endif
	    ln_log(LNLOG_SNOTICE, LNLOG_CTOP,
		    "%s: address list exhausted without establishing connection.",
		    upstream->name);
#ifdef HAVE_IPV6
	if (ai)
	    freeaddrinfo(ai);
#endif
    } else {
	/* gethostbyname or getaddrinfo returned error */
	const char *er;
#ifdef HAVE_IPV6
	er = gai_strerror(e);
#else
	switch(h_errno) {
	    case HOST_NOT_FOUND:
		er = "No such host.";
		break;
	    case NO_DATA:
		er = "Name exists in DNS, but has no associated address (\"A\"-type DNS resource record).";
		break;
	    case NO_RECOVERY:
		er = "Unexpected permanent server failure.";
		break;
	    case TRY_AGAIN:
		er = "Temporary DNS error, please try again later.";
		break;
	    default:
		er = "Unknown h_errno value.";
		break;
	}
#endif
	ln_log(LNLOG_SWARNING, LNLOG_CTOP,
		"warning: %s: cannot resolve host name: %s", upstream->name, er);
    }
    return FALSE;
}				/* end of connect function */

/*
 * disconnect from upstream server
 */
void
nntpdisconnect(void)
{
    if (nntpin) {
	fclose(nntpin);
	nntpin = NULL;
    }
    if (nntpout) {
	fclose(nntpout);
	nntpout = NULL;
    }
}

void
nntpquit(void)
{
    xsnprintf(lineout, SIZE_lineout, "QUIT\r\n");	/* say it, then just exit :) */
    putaline();
    nntpdisconnect();
}

#ifdef MAIN
int verbose=0;
int debug=0;

int main(int argc, char **argv) {
    int i = 1;
    while (i < argc) {
	int stat_is_evil;
	int date_is_evil;

	stat_is_evil = check_linlist(argv[i], stat_versions,
		stat_count);
	date_is_evil = check_linlist(argv[i], date_versions,
		date_count);

	printf("%s: stat_evil=%d, date_evil=%d\n",
		argv[i], stat_is_evil, date_is_evil);

	i++;
    }
    return 0;
}
#endif


syntax highlighted by Code2HTML, v. 0.9.1