Page MenuHomePhorge

nntpd.c
No OneTemporary

Authored By
Unknown
Size
95 KB
Referenced Files
None
Subscribers
None
/* nntpd.c -- NNTP server
*
* Copyright (c) 1998-2003 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any other legal
* details, please contact
* Office of Technology Transfer
* Carnegie Mellon University
* 5000 Forbes Avenue
* Pittsburgh, PA 15213-3890
* (412) 268-4387, fax: (412) 268-7395
* tech-transfer@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* $Id: nntpd.c,v 1.12 2004/01/22 21:17:09 ken3 Exp $
*/
/*
* TODO:
*
* - add sender and PGP verification code for control messages
* - figure out what to do with control messages when proxying
*/
#include <config.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/param.h>
#include <syslog.h>
#include <com_err.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sasl/sasl.h>
#include <sasl/saslutil.h>
#include "acl.h"
#include "annotate.h"
#include "append.h"
#include "auth.h"
#include "backend.h"
#include "duplicate.h"
#include "exitcodes.h"
#include "global.h"
#include "hash.h"
#include "imap_err.h"
#include "index.h"
#include "iptostring.h"
#include "mailbox.h"
#include "map.h"
#include "mboxlist.h"
#include "mkgmtime.h"
#include "mupdate-client.h"
#include "nntp_err.h"
#include "prot.h"
#include "retry.h"
#include "rfc822date.h"
#include "smtpclient.h"
#include "spool.h"
#include "telemetry.h"
#include "tls.h"
#include "util.h"
#include "version.h"
#include "wildmat.h"
#include "xmalloc.h"
extern int optind;
extern char *optarg;
extern int opterr;
/* Stuff to make index.c link */
int imapd_exists;
struct protstream *imapd_out = NULL;
struct auth_state *imapd_authstate = NULL;
char *imapd_userid = NULL;
void printastring(const char *s)
{
fatal("not implemented", EC_SOFTWARE);
}
/* end stuff to make index.c link */
/* PROXY STUFF */
/* we want a list of our outgoing connections here and which one we're
currently piping */
#define IDLE_TIMEOUT (5 * 60)
/* the current server most commands go to */
struct backend *backend_current = NULL;
/* our cached connections */
struct backend **backend_cached = NULL;
#ifdef HAVE_SSL
static SSL *tls_conn;
#endif /* HAVE_SSL */
sasl_conn_t *nntp_saslconn; /* the sasl connection context */
char newsprefix[100] = "";
char *nntp_userid = 0, *newsmaster;
struct auth_state *nntp_authstate = 0, *newsmaster_authstate;
static struct mailbox *nntp_group = 0;
struct sockaddr_storage nntp_localaddr, nntp_remoteaddr;
int nntp_haveaddr = 0;
char nntp_clienthost[NI_MAXHOST*2+1] = "[local]";
struct protstream *nntp_out = NULL;
struct protstream *nntp_in = NULL;
static int nntp_logfd = -1;
unsigned nntp_exists = 0;
unsigned nntp_current = 0;
unsigned did_extensions = 0;
int allowanonymous = 0;
int singleinstance = 1; /* attempt single instance store */
/* Bitmasks for NNTP modes */
enum {
MODE_READ = (1<<0),
MODE_FEED = (1<<1),
MODE_STREAM = (1<<2)
};
static unsigned nntp_capa = MODE_READ | MODE_FEED | MODE_STREAM;
static int nntps = 0;
int nntp_starttls_done = 0;
static struct mailbox mboxstruct;
/* the sasl proxy policy context */
static struct proxy_context nntp_proxyctx = {
0, 1, &nntp_authstate, NULL, NULL
};
/* for config.c */
const int config_need_data = CONFIG_NEED_PARTITION_DATA;
/*
* values for article parts
* these correspond to the last digit of the response code
*/
enum {
ARTICLE_ALL = 0,
ARTICLE_HEAD = 1,
ARTICLE_BODY = 2,
ARTICLE_STAT = 3
};
/* values for post modes */
enum {
POST_POST = 0,
POST_IHAVE = 1,
POST_CHECK = 2,
POST_TAKETHIS = 3
};
/* response codes for each stage of posting */
struct {
int ok, cont, no, fail;
} post_codes[] = { { 240, 340, 440, 441 },
{ 235, 335, 435, 436 },
{ -1, 238, 438, -1 },
{ 239, -1, -1, 439 } };
struct wildmat {
char *pat;
int not;
};
static struct wildmat *split_wildmats(char *str);
static void free_wildmats(struct wildmat *wild);
static void cmdloop(void);
static int open_group(char *name, int has_prefix,
struct backend **ret, int *postable);
static int parserange(char *str, unsigned long *uid, unsigned long *last,
char **msgid, struct backend **be);
static time_t parse_datetime(char *datestr, char *timestr, char *gmt);
static void cmd_article(int part, char *msgid, unsigned long uid);
static void cmd_authinfo_user(char *user);
static void cmd_authinfo_pass(char *pass);
static void cmd_authinfo_sasl(char *cmd, char *mech, char *resp);
static void cmd_hdr(char *cmd, char *hdr, char *pat, char *msgid,
unsigned long uid, unsigned long last);
static void cmd_help(void);
static void cmd_list(char *arg1, char *arg2);
static void cmd_mode(char *arg);
static void cmd_newgroups(time_t tstamp);
static void cmd_newnews(char *wild, time_t tstamp);
static void cmd_over(char *msgid, unsigned long uid, unsigned long last);
static void cmd_post(char *msgid, int mode);
static void cmd_starttls(int nntps);
void usage(void);
void shut_down(int code) __attribute__ ((noreturn));
extern void setproctitle_init(int argc, char **argv, char **envp);
extern int proc_register(const char *progname, const char *clienthost,
const char *userid, const char *mailbox);
extern void proc_cleanup(void);
extern int saslserver(sasl_conn_t *conn, const char *mech,
const char *init_resp, const char *resp_prefix,
const char *continuation,
struct protstream *pin, struct protstream *pout,
int *sasl_result, char **success_data);
static struct
{
char *ipremoteport;
char *iplocalport;
sasl_ssf_t ssf;
char *authid;
} saslprops = {NULL,NULL,0,NULL};
static struct sasl_callback mysasl_cb[] = {
{ SASL_CB_GETOPT, &mysasl_config, NULL },
{ SASL_CB_PROXY_POLICY, &mysasl_proxy_policy, (void*) &nntp_proxyctx },
{ SASL_CB_CANON_USER, &mysasl_canon_user, NULL },
{ SASL_CB_LIST_END, NULL, NULL }
};
/* proxy support functions */
void proxyd_downserver(struct backend *s)
{
if (!s || !s->timeout) {
/* already disconnected */
return;
}
/* need to logout of server */
backend_disconnect(s, &protocol[PROTOCOL_NNTP]);
if(s == backend_current) backend_current = NULL;
/* remove the timeout */
prot_removewaitevent(nntp_in, s->timeout);
s->timeout = NULL;
}
struct prot_waitevent *backend_timeout(struct protstream *s,
struct prot_waitevent *ev, void *rock)
{
struct backend *be = (struct backend *) rock;
if (be != backend_current) {
/* server is not our current server, and idle too long.
* down the backend server (removes the event as a side-effect)
*/
proxyd_downserver(be);
return NULL;
}
else {
/* it will timeout in IDLE_TIMEOUT seconds from now */
ev->mark = time(NULL) + IDLE_TIMEOUT;
return ev;
}
}
/* return the connection to the server */
struct backend *proxyd_findserver(const char *server)
{
int i = 0;
struct backend *ret = NULL;
while (backend_cached && backend_cached[i]) {
if (!strcmp(server, backend_cached[i]->hostname)) {
/* xxx do we want to ping/noop the server here? */
ret = backend_cached[i];
break;
}
i++;
}
if (!ret || !ret->timeout) {
/* need to (re)establish connection to server or create one */
ret = backend_connect(ret, server, &protocol[PROTOCOL_NNTP],
nntp_userid ? nntp_userid : "anonymous", NULL);
if(!ret) return NULL;
/* set the id */
if (!ret->context) {
ret->context = xmalloc(sizeof(unsigned));
*((unsigned *) ret->context) = i;
}
/* add the timeout */
ret->timeout = prot_addwaitevent(nntp_in, time(NULL) + IDLE_TIMEOUT,
backend_timeout, ret);
}
ret->timeout->mark = time(NULL) + IDLE_TIMEOUT;
/* insert server in list of cached connections */
if (!backend_cached[i]) {
backend_cached = (struct backend **)
xrealloc(backend_cached, (i + 2) * sizeof(struct backend *));
backend_cached[i] = ret;
backend_cached[i + 1] = NULL;
}
return ret;
}
static void kick_mupdate(void)
{
char buf[2048];
struct sockaddr_un srvaddr;
int s, r;
int len;
s = socket(AF_UNIX, SOCK_STREAM, 0);
if (s == -1) {
syslog(LOG_ERR, "socket: %m");
return;
}
strlcpy(buf, config_dir, sizeof(buf));
strlcat(buf, FNAME_MUPDATE_TARGET_SOCK, sizeof(buf));
memset((char *)&srvaddr, 0, sizeof(srvaddr));
srvaddr.sun_family = AF_UNIX;
strcpy(srvaddr.sun_path, buf);
len = sizeof(srvaddr.sun_family) + strlen(srvaddr.sun_path) + 1;
r = connect(s, (struct sockaddr *)&srvaddr, len);
if (r == -1) {
syslog(LOG_ERR, "kick_mupdate: can't connect to target: %m");
close(s);
return;
}
r = read(s, buf, sizeof(buf));
if (r <= 0) {
syslog(LOG_ERR, "kick_mupdate: can't read from target: %m");
close(s);
return;
}
/* if we got here, it's been kicked */
close(s);
return;
}
/* proxy mboxlist_lookup; on misses, it asks the listener for this
machine to make a roundtrip to the master mailbox server to make
sure it's up to date */
static int mlookup(const char *name, char **server, char **aclp, void *tid)
{
int r, type;
if(server) *server = NULL;
r = mboxlist_detail(name, &type, NULL, server, aclp, tid);
if (r == IMAP_MAILBOX_NONEXISTENT && config_mupdate_server) {
kick_mupdate();
r = mboxlist_detail(name, &type, NULL, server, aclp, tid);
}
if (type & MBTYPE_REMOTE) {
/* xxx hide the fact that we are storing partitions */
if(server && *server) {
char *c;
c = strchr(*server, '!');
if(c) *c = '\0';
}
}
else if (server)
*server = NULL;
return r;
}
static int read_response(struct backend *s, int force_notfatal, char **result)
{
static char buf[2048];
s->timeout->mark = time(NULL) + IDLE_TIMEOUT;
if (!prot_fgets(buf, sizeof(buf), s->in)) {
/* uh oh */
if (s == backend_current && !force_notfatal)
fatal("Lost connection to selected backend", EC_UNAVAILABLE);
proxyd_downserver(s);
return IMAP_SERVER_UNAVAILABLE;
}
*result = buf;
return 0;
}
static int pipe_to_end_of_response(struct backend *s, int force_notfatal)
{
char buf[2048];
s->timeout->mark = time(NULL) + IDLE_TIMEOUT;
do {
if (!prot_fgets(buf, sizeof(buf), s->in)) {
/* uh oh */
if (s == backend_current && !force_notfatal)
fatal("Lost connection to selected backend", EC_UNAVAILABLE);
proxyd_downserver(s);
return IMAP_SERVER_UNAVAILABLE;
}
prot_printf(nntp_out, "%s", buf);
} while (strcmp(buf, ".\r\n"));
return 0;
}
/* end proxy support functions */
static void nntp_reset(void)
{
int i;
proc_cleanup();
/* close local mailbox */
if (nntp_group) {
mailbox_close(nntp_group);
nntp_group = 0;
}
/* close backend connections */
i = 0;
while (backend_cached && backend_cached[i]) {
proxyd_downserver(backend_cached[i]);
free(backend_cached[i]->context);
free(backend_cached[i]);
i++;
}
if (backend_cached) free(backend_cached);
backend_cached = NULL;
backend_current = NULL;
if (nntp_in) {
prot_NONBLOCK(nntp_in);
prot_fill(nntp_in);
prot_free(nntp_in);
}
if (nntp_out) {
prot_flush(nntp_out);
prot_free(nntp_out);
}
nntp_in = nntp_out = NULL;
#ifdef HAVE_SSL
if (tls_conn) {
tls_reset_servertls(&tls_conn);
tls_conn = NULL;
}
#endif
cyrus_close_sock(0);
cyrus_close_sock(1);
cyrus_close_sock(2);
strcpy(nntp_clienthost, "[local]");
if (nntp_logfd != -1) {
close(nntp_logfd);
nntp_logfd = -1;
}
if (nntp_userid != NULL) {
free(nntp_userid);
nntp_userid = NULL;
}
if (nntp_authstate) {
auth_freestate(nntp_authstate);
nntp_authstate = NULL;
}
if (nntp_saslconn) {
sasl_dispose(&nntp_saslconn);
nntp_saslconn = NULL;
}
nntp_starttls_done = 0;
if(saslprops.iplocalport) {
free(saslprops.iplocalport);
saslprops.iplocalport = NULL;
}
if(saslprops.ipremoteport) {
free(saslprops.ipremoteport);
saslprops.ipremoteport = NULL;
}
if(saslprops.authid) {
free(saslprops.authid);
saslprops.authid = NULL;
}
saslprops.ssf = 0;
nntp_exists = 0;
nntp_current = 0;
}
/*
* run once when process is forked;
* MUST NOT exit directly; must return with non-zero error code
*/
int service_init(int argc __attribute__((unused)),
char **argv __attribute__((unused)),
char **envp __attribute__((unused)))
{
int opt;
const char *prefix;
initialize_nntp_error_table();
if (geteuid() == 0) fatal("must run as the Cyrus user", EC_USAGE);
setproctitle_init(argc, argv, envp);
/* set signal handlers */
signals_set_shutdown(&shut_down);
signals_add_handlers();
signal(SIGPIPE, SIG_IGN);
/* load the SASL plugins */
global_sasl_init(1, 1, mysasl_cb);
if ((prefix = config_getstring(IMAPOPT_NEWSPREFIX)))
snprintf(newsprefix, sizeof(newsprefix), "%s.", prefix);
/* initialize duplicate delivery database */
if (duplicate_init(NULL, 0) != 0) {
syslog(LOG_ERR,
"unable to init duplicate delivery database\n");
fatal("unable to init duplicate delivery database", EC_SOFTWARE);
}
/* open the mboxlist, we'll need it for real work */
mboxlist_init(0);
mboxlist_open(NULL);
/* open the quota db, we'll need it for expunge */
quotadb_init(0);
quotadb_open(NULL);
while ((opt = getopt(argc, argv, "srf")) != EOF) {
switch(opt) {
case 's': /* nntps (do starttls right away) */
nntps = 1;
if (!tls_enabled()) {
syslog(LOG_ERR, "nntps: required OpenSSL options not present");
fatal("nntps: required OpenSSL options not present",
EC_CONFIG);
}
break;
case 'r': /* enter reader-only mode */
nntp_capa &= MODE_READ;
break;
case 'f': /* enter feeder-only mode */
nntp_capa &= ~MODE_READ;
break;
default:
usage();
}
}
/* Initialize the annotatemore extention */
annotatemore_init(0, NULL, NULL);
annotatemore_open(NULL);
newsmaster = (char *) config_getstring(IMAPOPT_NEWSMASTER);
newsmaster_authstate = auth_newstate(newsmaster);
singleinstance = config_getswitch(IMAPOPT_SINGLEINSTANCESTORE);
return 0;
}
/*
* run for each accepted connection
*/
int service_main(int argc, char **argv, char **envp)
{
socklen_t salen;
char localip[60], remoteip[60];
char hbuf[NI_MAXHOST];
int timeout;
sasl_security_properties_t *secprops=NULL;
char unavail[1024];
signals_poll();
nntp_in = prot_new(0, 0);
nntp_out = prot_new(1, 1);
/* Find out name of client host */
salen = sizeof(nntp_remoteaddr);
if (getpeername(0, (struct sockaddr *)&nntp_remoteaddr, &salen) == 0 &&
(nntp_remoteaddr.ss_family == AF_INET ||
nntp_remoteaddr.ss_family == AF_INET6)) {
if (getnameinfo((struct sockaddr *)&nntp_remoteaddr, salen,
hbuf, sizeof(hbuf), NULL, 0, NI_NAMEREQD) == 0) {
strncpy(nntp_clienthost, hbuf, sizeof(hbuf));
nntp_clienthost[sizeof(nntp_clienthost)-30] = '\0';
} else {
nntp_clienthost[0] = '\0';
}
getnameinfo((struct sockaddr *)&nntp_remoteaddr, salen, hbuf,
sizeof(hbuf), NULL, 0, NI_NUMERICHOST | NI_WITHSCOPEID);
strlcat(nntp_clienthost, "[", sizeof(nntp_clienthost));
strlcat(nntp_clienthost, hbuf, sizeof(nntp_clienthost));
strlcat(nntp_clienthost, "]", sizeof(nntp_clienthost));
salen = sizeof(nntp_localaddr);
if (getsockname(0, (struct sockaddr *)&nntp_localaddr, &salen) == 0) {
nntp_haveaddr = 1;
}
}
/* other params should be filled in */
if (sasl_server_new("news", config_servername, NULL, NULL, NULL,
NULL, SASL_SUCCESS_DATA, &nntp_saslconn) != SASL_OK)
fatal("SASL failed initializing: sasl_server_new()",EC_TEMPFAIL);
/* will always return something valid */
secprops = mysasl_secprops(SASL_SEC_NOPLAINTEXT);
sasl_setprop(nntp_saslconn, SASL_SEC_PROPS, secprops);
if(iptostring((struct sockaddr *)&nntp_localaddr, salen,
localip, 60) == 0) {
sasl_setprop(nntp_saslconn, SASL_IPLOCALPORT, localip);
saslprops.iplocalport = xstrdup(localip);
}
if(iptostring((struct sockaddr *)&nntp_remoteaddr, salen,
remoteip, 60) == 0) {
sasl_setprop(nntp_saslconn, SASL_IPREMOTEPORT, remoteip);
saslprops.ipremoteport = xstrdup(remoteip);
}
proc_register("nntpd", nntp_clienthost, NULL, NULL);
/* Set inactivity timer */
timeout = config_getint(IMAPOPT_TIMEOUT);
if (timeout < 3) timeout = 3;
prot_settimeout(nntp_in, timeout*60);
prot_setflushonread(nntp_in, nntp_out);
if (config_mupdate_server) {
/* setup the cache */
backend_cached = xmalloc(sizeof(struct backend *));
backend_cached[0] = NULL;
}
/* we were connected on nntps port so we should do
TLS negotiation immediatly */
if (nntps == 1) cmd_starttls(1);
if (shutdown_file(unavail, sizeof(unavail))) {
prot_printf(nntp_out,
"400 %s Cyrus NNTP%s %s server unavailable, %s\r\n",
config_servername, config_mupdate_server ? " Murder" : "",
CYRUS_VERSION, unavail);
shut_down(0);
}
prot_printf(nntp_out,
"%u %s Cyrus NNTP%s %s server ready, posting %s\r\n",
(nntp_capa & MODE_READ) ? 200 : 201,
config_servername, config_mupdate_server ? " Murder" : "",
CYRUS_VERSION,
(nntp_capa & MODE_READ) ? "allowed" : "prohibited");
cmdloop();
/* QUIT executed */
/* cleanup */
nntp_reset();
return 0;
}
/* Called by service API to shut down the service */
void service_abort(int error)
{
shut_down(error);
}
void usage(void)
{
prot_printf(nntp_out, "503 usage: nntpd [-C <alt_config>] [-s]\r\n");
prot_flush(nntp_out);
exit(EC_USAGE);
}
/*
* Cleanly shut down and exit
*/
void shut_down(int code)
{
int i;
proc_cleanup();
/* close local mailbox */
if (nntp_group) {
mailbox_close(nntp_group);
}
/* close backend connections */
i = 0;
while (backend_cached && backend_cached[i]) {
proxyd_downserver(backend_cached[i]);
free(backend_cached[i]->context);
free(backend_cached[i]);
i++;
}
if (backend_cached) free(backend_cached);
duplicate_done();
mboxlist_close();
mboxlist_done();
quotadb_close();
quotadb_done();
annotatemore_close();
annotatemore_done();
if (nntp_in) {
prot_NONBLOCK(nntp_in);
prot_fill(nntp_in);
prot_free(nntp_in);
}
if (nntp_out) {
prot_flush(nntp_out);
prot_free(nntp_out);
}
#ifdef HAVE_SSL
tls_shutdown_serverengine();
#endif
cyrus_done();
exit(code);
}
void fatal(const char* s, int code)
{
static int recurse_code = 0;
if (recurse_code) {
/* We were called recursively. Just give up */
proc_cleanup();
exit(recurse_code);
}
recurse_code = code;
if (nntp_out) {
prot_printf(nntp_out, "205 Fatal error: %s\r\n", s);
prot_flush(nntp_out);
}
syslog(LOG_ERR, "Fatal error: %s", s);
shut_down(code);
}
/* Reset the given sasl_conn_t to a sane state */
static int reset_saslconn(sasl_conn_t **conn)
{
int ret;
sasl_security_properties_t *secprops = NULL;
sasl_dispose(conn);
/* do initialization typical of service_main */
ret = sasl_server_new("news", config_servername,
NULL, NULL, NULL,
NULL, SASL_SUCCESS_DATA, conn);
if(ret != SASL_OK) return ret;
if(saslprops.ipremoteport)
ret = sasl_setprop(*conn, SASL_IPREMOTEPORT,
saslprops.ipremoteport);
if(ret != SASL_OK) return ret;
if(saslprops.iplocalport)
ret = sasl_setprop(*conn, SASL_IPLOCALPORT,
saslprops.iplocalport);
if(ret != SASL_OK) return ret;
secprops = mysasl_secprops(SASL_SEC_NOPLAINTEXT);
ret = sasl_setprop(*conn, SASL_SEC_PROPS, secprops);
if(ret != SASL_OK) return ret;
/* end of service_main initialization excepting SSF */
/* If we have TLS/SSL info, set it */
if(saslprops.ssf) {
ret = sasl_setprop(*conn, SASL_SSF_EXTERNAL, &saslprops.ssf);
}
if(ret != SASL_OK) return ret;
if(saslprops.authid) {
ret = sasl_setprop(*conn, SASL_AUTH_EXTERNAL, saslprops.authid);
if(ret != SASL_OK) return ret;
}
/* End TLS/SSL Info */
return SASL_OK;
}
/*
* Top-level command loop parsing
*/
static void cmdloop(void)
{
int c, r = 0, mode;
static struct buf cmd, arg1, arg2, arg3, arg4;
char *p, *result, buf[1024];
const char *err;
unsigned long uid;
struct backend *be;
allowanonymous = config_getswitch(IMAPOPT_ALLOWANONYMOUSLOGIN);
for (;;) {
signals_poll();
/* Parse command name */
c = getword(nntp_in, &cmd);
if (c == EOF) {
if ((err = prot_error(nntp_in)) != NULL) {
syslog(LOG_WARNING, "%s, closing connection", err);
prot_printf(nntp_out, "400 %s\r\n", err);
}
return;
}
if (shutdown_file(buf, sizeof(buf))) {
prot_printf(nntp_out, "400 %s\r\n", buf);
shut_down(0);
}
if (!cmd.s[0]) {
prot_printf(nntp_out, "501 Empty command\r\n");
eatline(nntp_in, c);
continue;
}
if (islower((unsigned char) cmd.s[0]))
cmd.s[0] = toupper((unsigned char) cmd.s[0]);
for (p = &cmd.s[1]; *p; p++) {
if (isupper((unsigned char) *p)) *p = tolower((unsigned char) *p);
}
/* Check/Takethis only allowed for streamers */
if (!(nntp_capa & MODE_STREAM) && strchr("CT", cmd.s[0])) goto noperm;
/* Ihave only allowed for feeders */
if (!(nntp_capa & MODE_FEED) && cmd.s[0] == 'I') goto noperm;
/* Body/Date/Group/Newgroups/Newnews/Next/Over/Post/Xhdr/Xover
only allowed for readers */
if (!(nntp_capa & MODE_READ) &&
strchr("BDGNOPX", cmd.s[0])) goto noperm;
/* Only Authinfo/Check/Head/Help/Ihave/List [ Active | Extensions ]/
Mode/Quit/Stat/Starttls/Takethis allowed when not logged in */
if (!nntp_userid && !allowanonymous &&
!strchr("ACHILMQST", cmd.s[0])) goto nologin;
switch (cmd.s[0]) {
case 'A':
if (!strcmp(cmd.s, "Authinfo")) {
arg2.len = arg3.len = 0;
if (c != ' ') goto missingargs;
c = getword(nntp_in, &arg1); /* subcommand */
if (c == EOF) goto missingargs;
lcase(arg1.s);
if (strcmp(arg1.s, "generic") && c != ' ') {
/* arg2 is required for all subcommands except generic */
goto missingargs;
}
if (c == ' ') {
c = getword(nntp_in, &arg2); /* argument/sasl mech */
if (c == EOF) goto missingargs;
}
if (!strcmp(arg1.s, "sasl") && c == ' ') {
c = getword(nntp_in, &arg3); /* init response (optional) */
if (c == EOF) goto missingargs;
}
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
if (!strcmp(arg1.s, "user"))
cmd_authinfo_user(arg2.s);
else if (!strcmp(arg1.s, "pass"))
cmd_authinfo_pass(arg2.s);
else if (!strcmp(arg1.s, "sasl") || !strcmp(arg1.s, "generic"))
cmd_authinfo_sasl(arg1.s, arg2.len ? arg2.s : NULL,
arg3.len ? arg3.s : NULL);
else
prot_printf(nntp_out,
"501 Unrecognized AUTHINFO command\r\n");
}
else if (!(nntp_capa & MODE_READ)) goto noperm;
else if (!nntp_userid && !allowanonymous) goto nologin;
else if (!strcmp(cmd.s, "Article")) {
char curgroup[MAX_MAILBOX_NAME+1], *msgid;
mode = ARTICLE_ALL;
article:
if (arg1.s) *arg1.s = 0;
if (c == ' ') {
c = getword(nntp_in, &arg1); /* number/msgid (optional) */
if (c == EOF) goto missingargs;
}
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
/* in case a msgid makes us switch groups */
strcpy(curgroup, nntp_group ? nntp_group->name : "");
if (parserange(arg1.s, &uid, NULL, &msgid, &be) != -1) {
if (be) {
if (arg1.s && *arg1.s)
prot_printf(be->out, "%s %s\r\n", cmd.s, arg1.s);
else
prot_printf(be->out, "%s\r\n", cmd.s);
r = read_response(be, 0, &result);
if (r) goto noopengroup;
prot_printf(nntp_out, "%s", result);
if (!strncmp(result, "22", 2) &&
mode != ARTICLE_STAT) {
pipe_to_end_of_response(be, 0);
}
}
else
cmd_article(mode, msgid, uid);
}
/* return to previously selected group */
if (*curgroup && nntp_group &&
strcmp(curgroup, nntp_group->name)) {
open_group(curgroup, 1, NULL, NULL);
}
}
else goto badcmd;
break;
case 'B':
if (!strcmp(cmd.s, "Body")) {
mode = ARTICLE_BODY;
goto article;
}
else goto badcmd;
break;
case 'C':
if (!strcmp(cmd.s, "Check")) {
mode = POST_CHECK;
goto ihave;
}
else goto badcmd;
break;
case 'D':
if (!strcmp(cmd.s, "Date")) {
time_t now = time(NULL);
struct tm *my_tm = gmtime(&now);
char buf[15];
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
strftime(buf, sizeof(buf), "%Y%m%d%H%M%S", my_tm);
prot_printf(nntp_out, "111 %s\r\n", buf);
}
else goto badcmd;
break;
case 'G':
if (!strcmp(cmd.s, "Group")) {
if (c != ' ') goto missingargs;
c = getword(nntp_in, &arg1); /* group */
if (c == EOF) goto missingargs;
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
r = open_group(arg1.s, 0, &backend_current, NULL);
if (r) goto nogroup;
else if (backend_current) {
prot_printf(backend_current->out, "GROUP %s\r\n", arg1.s);
r = read_response(backend_current, 0, &result);
if (r) goto nogroup;
prot_printf(nntp_out, "%s", result);
}
else {
nntp_exists = nntp_group->exists;
nntp_current = nntp_exists > 0;
prot_printf(nntp_out, "211 %u %lu %lu %s\r\n",
nntp_exists,
nntp_exists ? index_getuid(1) :
nntp_group->last_uid+1,
nntp_exists ? index_getuid(nntp_exists) :
nntp_group->last_uid,
arg1.s);
}
}
else goto badcmd;
break;
case 'H':
if (!strcmp(cmd.s, "Head")) {
mode = ARTICLE_HEAD;
goto article;
}
else if (!strcmp(cmd.s, "Help")) {
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
cmd_help();
}
else if (!(nntp_capa & MODE_READ)) goto noperm;
else if (!nntp_userid && !allowanonymous) goto nologin;
else if (!strcmp(cmd.s, "Hdr")) {
char curgroup[MAX_MAILBOX_NAME+1], *msgid;
unsigned long last;
hdr:
if (arg2.s) *arg2.s = 0;
if (c != ' ') goto missingargs;
c = getword(nntp_in, &arg1); /* header */
if (c == EOF) goto missingargs;
if (c == ' ') {
c = getword(nntp_in, &arg2); /* range (optional) */
if (c == EOF) goto missingargs;
}
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
/* in case a msgid makes us switch groups */
strcpy(curgroup, nntp_group ? nntp_group->name : "");
if (parserange(arg2.s, &uid, &last, &msgid, &be) != -1) {
if (be) {
if (arg2.s && *arg2.s)
prot_printf(be->out, "%s %s %s\r\n",
cmd.s, arg1.s, arg2.s);
else
prot_printf(be->out, "%s %s\r\n", cmd.s, arg1.s);
r = read_response(be, 0, &result);
if (r) goto noopengroup;
prot_printf(nntp_out, "%s", result);
if (!strncmp(result, "22", 2)) { /* 221 or 225 */
pipe_to_end_of_response(be, 0);
}
}
else
cmd_hdr(cmd.s, arg1.s, NULL, msgid, uid, last);
}
/* return to previously selected group */
if (*curgroup && nntp_group &&
strcmp(curgroup, nntp_group->name)) {
open_group(curgroup, 1, NULL, NULL);
}
}
else goto badcmd;
break;
case 'I':
if (!strcmp(cmd.s, "Ihave")) {
mode = POST_IHAVE;
ihave:
if (c != ' ') goto missingargs;
c = getword(nntp_in, &arg1); /* msgid */
if (c == EOF) goto missingargs;
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
cmd_post(arg1.s, mode);
}
else goto badcmd;
break;
case 'L':
if (!strcmp(cmd.s, "List")) {
arg1.len = arg2.len = 0;
if (c == ' ') {
c = getword(nntp_in, &arg1); /* subcommand (optional) */
if (c == EOF) goto missingargs;
if (c == ' ') {
c = getword(nntp_in, &arg2); /* argument (optional) */
if (c == EOF) goto missingargs;
}
}
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
cmd_list(arg1.len ? arg1.s : NULL, arg2.len ? arg2.s : NULL);
}
else if (!(nntp_capa & MODE_READ)) goto noperm;
else if (!nntp_userid && !allowanonymous) goto nologin;
else if (!strcmp(cmd.s, "Last")) {
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
if (backend_current) {
prot_printf(backend_current->out, "LAST\r\n");
r = read_response(backend_current, 0, &result);
if (r) goto noopengroup;
prot_printf(nntp_out, "%s", result);
}
else if (!nntp_group) goto noopengroup;
else if (!nntp_current) goto nocurrent;
else if (nntp_current == 1) {
prot_printf(nntp_out,
"422 No previous article in this group\r\n");
}
else {
char *msgid = index_get_msgid(nntp_group, --nntp_current);
prot_printf(nntp_out, "223 %u %s\r\n",
index_getuid(nntp_current),
msgid ? msgid : "<0>");
if (msgid) free(msgid);
}
}
else if (!strcmp(cmd.s, "Listgroup")) {
arg1.len = 0;
if (c == ' ') {
c = getword(nntp_in, &arg1); /* group (optional) */
if (c == EOF) goto missingargs;
}
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
if (arg1.len) {
r = open_group(arg1.s, 0, &backend_current, NULL);
if (r) goto nogroup;
if (nntp_group) {
nntp_exists = nntp_group->exists;
nntp_current = nntp_exists > 0;
}
}
if (backend_current) {
if (arg1.len)
prot_printf(backend_current->out, "LISTGROUP %s\r\n",
arg1.s);
else
prot_printf(backend_current->out, "LISTGROUP\r\n");
r = read_response(backend_current, 0, &result);
if (r) goto noopengroup;
prot_printf(nntp_out, "%s", result);
if (!strncmp(result, "211", 3)) {
pipe_to_end_of_response(backend_current, 0);
}
}
else if (!nntp_group) goto noopengroup;
else {
int i;
nntp_exists = nntp_group->exists;
nntp_current = nntp_exists > 0;
prot_printf(nntp_out, "211 %u %lu %lu %s\r\n",
nntp_exists,
nntp_exists ? index_getuid(1) :
nntp_group->last_uid+1,
nntp_exists ? index_getuid(nntp_exists) :
nntp_group->last_uid,
nntp_group->name + strlen(newsprefix));
for (i = 1; i <= nntp_exists; i++)
prot_printf(nntp_out, "%u\r\n", index_getuid(i));
prot_printf(nntp_out, ".\r\n");
}
}
else goto badcmd;
break;
case 'M':
if (!strcmp(cmd.s, "Mode")) {
if (c != ' ') goto missingargs;
c = getword(nntp_in, &arg1); /* mode */
if (c == EOF) goto missingargs;
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
cmd_mode(arg1.s);
}
else goto badcmd;
break;
case 'N':
if (!strcmp(cmd.s, "Newgroups")) {
time_t tstamp;
arg3.len = 0;
if (c != ' ') goto missingargs;
c = getword(nntp_in, &arg1); /* date */
if (c != ' ') goto missingargs;
c = getword(nntp_in, &arg2); /* time */
if (c == EOF) goto missingargs;
if (c == ' ') {
c = getword(nntp_in, &arg3); /* "GMT" (optional) */
if (c == EOF) goto missingargs;
}
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
if ((tstamp = parse_datetime(arg1.s, arg2.s,
arg3.len ? arg3.s : NULL)) < 0)
goto baddatetime;
cmd_newgroups(tstamp);
}
else if (!strcmp(cmd.s, "Newnews")) {
time_t tstamp;
if (!config_getswitch(IMAPOPT_ALLOWNEWNEWS))
goto cmddisabled;
arg4.len = 0;
if (c != ' ') goto missingargs;
c = getword(nntp_in, &arg1); /* wildmat */
if (c != ' ') goto missingargs;
c = getword(nntp_in, &arg2); /* date */
if (c != ' ') goto missingargs;
c = getword(nntp_in, &arg3); /* time */
if (c == EOF) goto missingargs;
if (c == ' ') {
c = getword(nntp_in, &arg4); /* "GMT" (optional) */
if (c == EOF) goto missingargs;
}
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
if ((tstamp = parse_datetime(arg2.s, arg3.s,
arg4.len ? arg4.s : NULL)) < 0)
goto baddatetime;
cmd_newnews(arg1.s, tstamp);
}
else if (!strcmp(cmd.s, "Next")) {
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
if (backend_current) {
prot_printf(backend_current->out, "NEXT\r\n");
r = read_response(backend_current, 0, &result);
if (r) goto noopengroup;
prot_printf(nntp_out, "%s", result);
}
else if (!nntp_group) goto noopengroup;
else if (!nntp_current) goto nocurrent;
else if (nntp_current == nntp_exists) {
prot_printf(nntp_out,
"421 No next article in this group\r\n");
}
else {
char *msgid = index_get_msgid(nntp_group, ++nntp_current);
prot_printf(nntp_out, "223 %u %s\r\n",
index_getuid(nntp_current),
msgid ? msgid : "<0>");
if (msgid) free(msgid);
}
}
else goto badcmd;
break;
case 'O':
if (!strcmp(cmd.s, "Over")) {
char curgroup[MAX_MAILBOX_NAME+1], *msgid;
unsigned long last;
over:
if (arg1.s) *arg1.s = 0;
if (c == ' ') {
c = getword(nntp_in, &arg1); /* range/msgid (optional) */
if (c == EOF) goto missingargs;
}
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
/* in case a msgid makes us switch groups */
strcpy(curgroup, nntp_group ? nntp_group->name : "");
msgid = NULL;
if (parserange(arg1.s, &uid, &last,
/* XOVER doesn't accept message-id */
(cmd.s[0] == 'X' ? NULL : &msgid), &be) != -1) {
if (be) {
if (arg1.s && *arg1.s)
prot_printf(be->out, "%s %s\r\n", cmd.s, arg1.s);
else
prot_printf(be->out, "%s\r\n", cmd.s);
r = read_response(be, 0, &result);
if (r) goto noopengroup;
prot_printf(nntp_out, "%s", result);
if (!strncmp(result, "224", 3)) {
pipe_to_end_of_response(be, 0);
}
}
else
cmd_over(msgid, uid, last);
}
/* return to previously selected group */
if (*curgroup && nntp_group &&
strcmp(curgroup, nntp_group->name)) {
open_group(curgroup, 1, NULL, NULL);
}
}
else goto badcmd;
break;
case 'P':
if (!strcmp(cmd.s, "Post")) {
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
cmd_post(NULL, POST_POST);
}
else goto badcmd;
break;
case 'Q':
if (!strcmp(cmd.s, "Quit")) {
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
prot_printf(nntp_out, "205 Connection closing\r\n");
return;
}
else goto badcmd;
break;
case 'S':
if (!strcmp(cmd.s, "Starttls") && tls_enabled()) {
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
cmd_starttls(0);
}
else if (!strcmp(cmd.s, "Stat")) {
mode = ARTICLE_STAT;
goto article;
}
else if (!nntp_userid && !allowanonymous) goto nologin;
else if (!strcmp(cmd.s, "Slave")) {
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
prot_printf(nntp_out, "202 Slave status noted\r\n");
}
else goto badcmd;
break;
case 'T':
if (!strcmp(cmd.s, "Takethis")) {
mode = POST_TAKETHIS;
goto ihave;
}
else goto badcmd;
break;
case 'X':
if (!strcmp(cmd.s, "Xhdr")) {
goto hdr;
}
else if (!strcmp(cmd.s, "Xover")) {
goto over;
}
else if (!strcmp(cmd.s, "Xpat")) {
char curgroup[MAX_MAILBOX_NAME+1], *msgid;
unsigned long last;
if (c != ' ') goto missingargs;
c = getword(nntp_in, &arg1); /* header */
if (c != ' ') goto missingargs;
/* gobble extra whitespace (hack for Mozilla) */
while ((c = prot_getc(nntp_in)) == ' ');
prot_ungetc(c, nntp_in);
c = getword(nntp_in, &arg2); /* range */
if (c != ' ') goto missingargs;
c = getword(nntp_in, &arg3); /* wildmat */
if (c == EOF) goto missingargs;
/* XXX per RFC 2980, we can have multiple patterns */
if (c == '\r') c = prot_getc(nntp_in);
if (c != '\n') goto extraargs;
/* in case a msgid makes us switch groups */
strcpy(curgroup, nntp_group ? nntp_group->name : "");
if (parserange(arg2.s, &uid, &last, &msgid, &be) != -1) {
if (be) {
prot_printf(be->out, "%s %s %s %s\r\n",
cmd.s, arg1.s, arg2.s, arg3.s);
r = read_response(be, 0, &result);
if (r) goto noopengroup;
prot_printf(nntp_out, "%s", result);
if (!strncmp(result, "221", 3)) {
pipe_to_end_of_response(be, 0);
}
}
else
cmd_hdr(cmd.s, arg1.s, arg3.s, msgid, uid, last);
}
/* return to previously selected group */
if (*curgroup && nntp_group &&
strcmp(curgroup, nntp_group->name)) {
open_group(curgroup, 1, NULL, NULL);
}
}
else goto badcmd;
break;
default:
badcmd:
prot_printf(nntp_out, "500 Unrecognized command\r\n");
eatline(nntp_in, c);
}
continue;
noperm:
prot_printf(nntp_out, "502 Permission denied\r\n");
eatline(nntp_in, c);
continue;
nologin:
prot_printf(nntp_out, "480 Authentication required\r\n");
eatline(nntp_in, c);
continue;
cmddisabled:
prot_printf(nntp_out, "503 \"%s\" disabled\r\n", cmd.s);
eatline(nntp_in, c);
continue;
extraargs:
prot_printf(nntp_out, "501 Unexpected extra argument\r\n");
eatline(nntp_in, c);
continue;
missingargs:
prot_printf(nntp_out, "501 Missing argument\r\n");
eatline(nntp_in, c);
continue;
baddatetime:
prot_printf(nntp_out, "501 Bad date/time\r\n");
continue;
nogroup:
prot_printf(nntp_out, "411 No such newsgroup (%s)\r\n",
error_message(r));
continue;
noopengroup:
prot_printf(nntp_out, "412 No newsgroup selected\r\n");
continue;
nocurrent:
prot_printf(nntp_out, "420 Current article number is invalid\r\n");
continue;
}
}
struct findrock {
const char *mailbox;
unsigned long uid;
};
/*
* duplicate_find() callback function to fetch a message by msgid
*/
static int find_cb(const char *msgid __attribute__((unused)),
const char *mailbox,
time_t mark __attribute__((unused)),
unsigned long uid, void *rock)
{
struct findrock *frock = (struct findrock *) rock;
/* make sure its a message in a mailbox that we're serving via NNTP */
if (!strncmp(mailbox, "user.", 5) ||
strncmp(mailbox, newsprefix, strlen(newsprefix))) return 0;
frock->mailbox = mailbox;
frock->uid = uid;
return CYRUSDB_DONE;
}
static int find_msgid(char *msgid, char **mailbox, unsigned long *uid)
{
struct findrock frock = { NULL, 0 };
duplicate_find(msgid, &find_cb, &frock);
if (!frock.mailbox) return 0;
if (mailbox) {
if (!frock.mailbox[0]) return 0;
*mailbox = (char *) frock.mailbox;
}
if (uid) {
if (!frock.uid) return 0;
*uid = frock.uid;
}
return 1;
}
static int parsenum(char *str, char **rem)
{
char *p = str;
int result = 0;
while (*p && isdigit((int) *p)) {
result = result * 10 + *p++ - '0';
if (result < 0) {
/* xxx overflow */
}
}
if (rem) {
*rem = p;
return (*p && p == str ? -1 : result);
}
return (*p ? -1 : result);
}
static int parserange(char *str, unsigned long *uid, unsigned long *last,
char **msgid, struct backend **ret)
{
char *p = NULL, *mboxname;
int r = 0;
*uid = 0;
if (last) *last = 0;
if (msgid) *msgid = NULL;
if (ret) *ret = NULL;
if (!str || !*str) {
/* argument, use current article */
if (backend_current) {
if (ret) *ret = backend_current;
}
else if (!nntp_group) goto noopengroup;
else if (!nntp_current) goto nocurrent;
else {
*uid = index_getuid(nntp_current);
if (last) *last = *uid;
}
}
else if (*str == '<') {
/* message-id, find server and/or mailbox */
if (!msgid) goto badrange;
if (!find_msgid(str, &mboxname, uid) ||
(r = open_group(mboxname, 1, ret, NULL)))
goto nomsgid;
*msgid = str;
}
else if (backend_current)
*ret = backend_current;
else if (!nntp_group) goto noopengroup;
else if (!nntp_exists) goto noarticle;
else if ((*uid = parsenum(str, &p)) <= 0) goto badrange;
else if (p && *p) {
/* extra stuff, check for range */
if (!last || (*p != '-')) goto badrange;
if (*++p)
*last = parsenum(p, NULL);
else
*last = index_getuid(nntp_exists);
if (*last <= 0 || *last < *uid) goto badrange;
}
if (last && !*last) *last = *uid;
return 0;
noopengroup:
prot_printf(nntp_out, "412 No newsgroup selected\r\n");
return -1;
nocurrent:
prot_printf(nntp_out, "420 Current article number is invalid\r\n");
return -1;
noarticle:
prot_printf(nntp_out, "423 No such article in this newsgroup\r\n");
return -1;
nomsgid:
prot_printf(nntp_out, "430 No article found with that message-id");
if (r) prot_printf(nntp_out, " (%s)", error_message(r));
prot_printf(nntp_out, "\r\n");
return -1;
badrange:
prot_printf(nntp_out, "501 Bad message-id or range\r\n");
return -1;
}
static const int numdays[] = {
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
};
#define isleap(year) (!((year) % 4) && (((year) % 100) || !((year) % 400)))
/*
* Parse a date/time specification per draft-ietf-nntpext-base.
*/
static time_t parse_datetime(char *datestr, char *timestr, char *gmt)
{
int datelen = strlen(datestr), leapday;
unsigned long d, t;
char *p;
struct tm tm;
/* check format of strings */
if ((datelen != 6 && datelen != 8) ||
strlen(timestr) != 6 || (gmt && strcasecmp(gmt, "GMT")))
return -1;
/* convert datestr to ulong */
d = strtoul(datestr, &p, 10);
if (d < 0 || *p) return -1;
/* convert timestr to ulong */
t = strtoul(timestr, &p, 10);
if (t < 0 || *p) return -1;
/* populate the time struct */
tm.tm_year = d / 10000;
d %= 10000;
tm.tm_mon = d / 100 - 1;
tm.tm_mday = d % 100;
tm.tm_hour = t / 10000;
t %= 10000;
tm.tm_min = t / 100;
tm.tm_sec = t % 100;
/* massage the year to years since 1900 */
if (tm.tm_year > 99) tm.tm_year -= 1900;
else {
/*
* guess century
* if year > current year, use previous century
* otherwise, use current century
*/
time_t now = time(NULL);
struct tm *current;
int century;
current = gmt ? gmtime(&now) : localtime(&now);
century = current->tm_year / 100;
if (tm.tm_year > current->tm_year % 100) century--;
tm.tm_year += century * 100;
}
/* sanity check the date/time (including leap day and leap second) */
leapday = tm.tm_mon == 1 && isleap(tm.tm_year + 1900);
if (tm.tm_year < 70 || tm.tm_mon < 0 || tm.tm_mon > 11 ||
tm.tm_mday < 1 || tm.tm_mday > (numdays[tm.tm_mon] + leapday) ||
tm.tm_hour > 23 || tm.tm_min > 59 || tm.tm_sec > 60)
return -1;
return (gmt ? mkgmtime(&tm) : mktime(&tm));
}
static int open_group(char *name, int has_prefix, struct backend **ret,
int *postable /* used for LIST ACTIVE only */)
{
char mailboxname[MAX_MAILBOX_NAME+1];
int r = 0;
char *acl, *newserver;
struct backend *backend_next = NULL;
/* close local group */
if (nntp_group) {
mailbox_close(nntp_group);
nntp_group = 0;
}
if (!has_prefix) {
snprintf(mailboxname, sizeof(mailboxname), "%s%s", newsprefix, name);
name = mailboxname;
}
if (!r) r = mlookup(name, &newserver, &acl, NULL);
if (!r && acl) {
int myrights = cyrus_acl_myrights(nntp_authstate, acl);
if (postable) *postable = myrights & ACL_POST;
if (!postable && /* allow limited 'r' for LIST ACTIVE */
!(myrights & ACL_READ)) {
r = (myrights & ACL_LOOKUP) ?
IMAP_PERMISSION_DENIED : IMAP_MAILBOX_NONEXISTENT;
}
}
if (r) return r;
if (newserver) {
/* remote group */
backend_next = proxyd_findserver(newserver);
if (!backend_next) return IMAP_SERVER_UNAVAILABLE;
*ret = backend_next;
}
else {
/* local group */
int doclose = 0;
r = mailbox_open_header(name, nntp_authstate, &mboxstruct);
if (!r) {
doclose = 1;
r = mailbox_open_index(&mboxstruct);
}
if (r) {
if (doclose) mailbox_close(&mboxstruct);
return r;
}
nntp_group = &mboxstruct;
index_operatemailbox(nntp_group);
if (ret) *ret = NULL;
}
syslog(LOG_DEBUG, "open: user %s opened %s",
nntp_userid ? nntp_userid : "anonymous", name);
return 0;
}
/*
* duplicate_find() callback function to build Xref content
*/
struct xref_rock {
char *buf;
size_t size;
};
static int xref_cb(const char *msgid __attribute__((unused)),
const char *mailbox,
time_t mark __attribute__((unused)),
unsigned long uid, void *rock)
{
struct xref_rock *xrock = (struct xref_rock *) rock;
size_t len = strlen(xrock->buf);
/* make sure its a message in a mailbox that we're serving via NNTP */
if (*mailbox && !strncmp(mailbox, newsprefix, strlen(newsprefix)) &&
strncmp(mailbox, "user.", 5)) {
snprintf(xrock->buf + len, xrock->size - len,
" %s:%lu", mailbox + strlen(newsprefix), uid);
}
return 0;
}
/*
* Build an Xref header. We have to do this on the fly because there is
* no way to store it in the article at delivery time.
*/
static void build_xref(char *msgid, char *buf, size_t size, int body_only)
{
struct xref_rock xrock = { buf, size };
snprintf(buf, size, "%s%s", body_only ? "" : "Xref: ", config_servername);
duplicate_find(msgid, &xref_cb, &xrock);
}
static void cmd_article(int part, char *msgid, unsigned long uid)
{
int msgno, by_msgid = (msgid != NULL);
char fname[MAX_MAILBOX_PATH+1];
FILE *msgfile;
msgno = index_finduid(uid);
if (!msgno || index_getuid(msgno) != uid) {
prot_printf(nntp_out, "423 No such article in this newsgroup\r\n");
return;
}
strlcpy(fname, nntp_group->path, sizeof(fname));
strlcat(fname, "/", sizeof(fname));
mailbox_message_get_fname(nntp_group, uid, fname + strlen(fname),
sizeof(fname) - strlen(fname));
msgfile = fopen(fname, "r");
if (!msgfile) {
prot_printf(nntp_out, "502 Could not read message file\r\n");
return;
}
if (!by_msgid) msgid = index_get_msgid(nntp_group, msgno);
prot_printf(nntp_out, "%u %lu %s\r\n",
220 + part, by_msgid ? 0 : uid, msgid ? msgid : "<0>");
if (part != ARTICLE_STAT) {
char buf[4096];
int body = 0;
int output = (part != ARTICLE_BODY);
while (fgets(buf, sizeof(buf), msgfile)) {
if (!body && buf[0] == '\r' && buf[1] == '\n') {
/* blank line between header and body */
body = 1;
if (output) {
/* add the Xref header */
char xref[8192];
build_xref(msgid, xref, sizeof(xref), 0);
prot_printf(nntp_out, "%s\r\n", xref);
}
if (part == ARTICLE_HEAD) {
/* we're done */
break;
}
else if (part == ARTICLE_BODY) {
/* start outputing text */
output = 1;
continue;
}
}
if (output) {
if (buf[0] == '.') prot_putc('.', nntp_out);
do {
prot_printf(nntp_out, "%s", buf);
} while (buf[strlen(buf)-1] != '\n' &&
fgets(buf, sizeof(buf), msgfile));
}
}
/* Protect against messages not ending in CRLF */
if (buf[strlen(buf)-1] != '\n') prot_printf(nntp_out, "\r\n");
prot_printf(nntp_out, ".\r\n");
}
if (!by_msgid) free(msgid);
fclose(msgfile);
}
static void cmd_authinfo_user(char *user)
{
char *p;
if (nntp_authstate) {
prot_printf(nntp_out, "502 Already authenticated\r\n");
return;
}
/* possibly disallow USER */
if (!(nntp_starttls_done || config_getswitch(IMAPOPT_ALLOWPLAINTEXT))) {
prot_printf(nntp_out,
"483 AUTHINFO USER command only available under a layer\r\n");
return;
}
if (nntp_userid) {
prot_printf(nntp_out, "502 Must give AUTHINFO PASS command\r\n");
return;
}
if (!(p = canonify_userid(user, NULL, NULL))) {
prot_printf(nntp_out, "502 Invalid user\r\n");
syslog(LOG_NOTICE,
"badlogin: %s plaintext %s invalid user",
nntp_clienthost, beautify_string(user));
}
else {
nntp_userid = xstrdup(p);
prot_printf(nntp_out, "381 Give AUTHINFO PASS command\r\n");
}
}
static void cmd_authinfo_pass(char *pass)
{
char *reply = 0;
if (nntp_authstate) {
prot_printf(nntp_out, "502 Already authenticated\r\n");
return;
}
if (!nntp_userid) {
prot_printf(nntp_out, "482 Must give AUTHINFO USER command first\r\n");
return;
}
if (!strcmp(nntp_userid, "anonymous")) {
if (allowanonymous) {
pass = beautify_string(pass);
if (strlen(pass) > 500) pass[500] = '\0';
syslog(LOG_NOTICE, "login: %s anonymous %s",
nntp_clienthost, pass);
}
else {
syslog(LOG_NOTICE, "badlogin: %s anonymous login refused",
nntp_clienthost);
prot_printf(nntp_out, "502 Invalid login\r\n");
return;
}
}
else if (sasl_checkpass(nntp_saslconn,
nntp_userid,
strlen(nntp_userid),
pass,
strlen(pass))!=SASL_OK) {
if (reply) {
syslog(LOG_NOTICE, "badlogin: %s plaintext %s %s",
nntp_clienthost, nntp_userid, reply);
}
prot_printf(nntp_out, "502 Invalid login\r\n");
free(nntp_userid);
nntp_userid = 0;
return;
}
else {
syslog(LOG_NOTICE, "login: %s %s plaintext%s %s", nntp_clienthost,
nntp_userid, nntp_starttls_done ? "+TLS" : "",
reply ? reply : "");
prot_printf(nntp_out, "281 User logged in\r\n");
nntp_authstate = auth_newstate(nntp_userid);
/* Create telemetry log */
nntp_logfd = telemetry_log(nntp_userid, nntp_in, nntp_out, 0);
}
}
static void cmd_authinfo_sasl(char *cmd, char *mech, char *resp)
{
int r, sasl_result;
char *success_data;
const int *ssfp;
char *ssfmsg = NULL;
const char *canon_user;
if (nntp_userid) {
prot_printf(nntp_out, "502 Already authenticated\r\n");
return;
}
if (cmd[0] == 'g') {
/* if client didn't specify any mech we give them the list */
if (!mech) {
const char *sasllist;
unsigned int mechnum;
prot_printf(nntp_out, "281 List of mechanisms follows\r\n");
/* CRLF separated, dot terminated */
if (sasl_listmech(nntp_saslconn, NULL,
"", "\r\n", "\r\n",
&sasllist,
NULL, &mechnum) == SASL_OK) {
if (mechnum > 0) {
prot_printf(nntp_out, "%s", sasllist);
}
}
prot_printf(nntp_out, ".\r\n");
return;
}
r = saslserver(nntp_saslconn, mech, resp, "AUTHINFO GENERIC ", "381 ",
nntp_in, nntp_out, &sasl_result, &success_data);
}
else
r = saslserver(nntp_saslconn, mech, resp, "", "383 ",
nntp_in, nntp_out, &sasl_result, &success_data);
if (r) {
const char *errorstring = NULL;
switch (r) {
case IMAP_SASL_CANCEL:
prot_printf(nntp_out,
"484 Client canceled authentication\r\n");
break;
case IMAP_SASL_PROTERR:
errorstring = prot_error(nntp_in);
prot_printf(nntp_out,
"502 Error reading client response: %s\r\n",
errorstring ? errorstring : "");
break;
default:
/* failed authentication */
errorstring = sasl_errstring(sasl_result, NULL, NULL);
syslog(LOG_NOTICE, "badlogin: %s %s [%s]",
nntp_clienthost, mech, sasl_errdetail(nntp_saslconn));
sleep(3);
if (errorstring) {
prot_printf(nntp_out, "502 %s\r\n", errorstring);
} else {
prot_printf(nntp_out, "502 Error authenticating\r\n");
}
}
reset_saslconn(&nntp_saslconn);
return;
}
/* successful authentication */
/* get the userid from SASL --- already canonicalized from
* mysasl_proxy_policy()
*/
sasl_result = sasl_getprop(nntp_saslconn, SASL_USERNAME,
(const void **) &canon_user);
nntp_userid = xstrdup(canon_user);
if (sasl_result != SASL_OK) {
prot_printf(nntp_out, "482 weird SASL error %d SASL_USERNAME\r\n",
sasl_result);
syslog(LOG_ERR, "weird SASL error %d getting SASL_USERNAME",
sasl_result);
reset_saslconn(&nntp_saslconn);
return;
}
proc_register("nntpd", nntp_clienthost, nntp_userid, (char *)0);
syslog(LOG_NOTICE, "login: %s %s %s%s %s", nntp_clienthost, nntp_userid,
mech, nntp_starttls_done ? "+TLS" : "", "User logged in");
sasl_getprop(nntp_saslconn, SASL_SSF, (const void **) &ssfp);
/* really, we should be doing a sasl_getprop on SASL_SSF_EXTERNAL,
but the current libsasl doesn't allow that. */
if (nntp_starttls_done) {
switch(*ssfp) {
case 0: ssfmsg = "tls protection"; break;
case 1: ssfmsg = "tls plus integrity protection"; break;
default: ssfmsg = "tls plus privacy protection"; break;
}
} else {
switch(*ssfp) {
case 0: ssfmsg = "no protection"; break;
case 1: ssfmsg = "integrity protection"; break;
default: ssfmsg = "privacy protection"; break;
}
}
if (success_data)
prot_printf(nntp_out, "282 %s\r\n", success_data);
else
prot_printf(nntp_out, "281 Success (%s)\r\n", ssfmsg);
prot_setsasl(nntp_in, nntp_saslconn);
prot_setsasl(nntp_out, nntp_saslconn);
/* Create telemetry log */
nntp_logfd = telemetry_log(nntp_userid, nntp_in, nntp_out, 0);
}
static void cmd_hdr(char *cmd, char *hdr, char *pat, char *msgid,
unsigned long uid, unsigned long last)
{
lcase(hdr);
prot_printf(nntp_out, "%u Headers follow:\r\n", cmd[0] == 'X' ? 221 : 225);
for (; uid <= last; uid++) {
char *body;
int msgno = index_finduid(uid);
int by_msgid = (msgid != NULL);
if (!msgno || index_getuid(msgno) != uid) continue;
/* see if we're looking for metadata */
if (hdr[0] == ':') {
if (!strcasecmp(":size", hdr)) {
char xref[8192];
unsigned long size = index_getsize(nntp_group, msgno);
if (!by_msgid) msgid = index_get_msgid(nntp_group, msgno);
build_xref(msgid, xref, sizeof(xref), 0);
if (!by_msgid) free(msgid);
prot_printf(nntp_out, "%lu %lu\r\n", by_msgid ? 0 : uid,
size + strlen(xref) + 2); /* +2 for \r\n */
}
else if (!strcasecmp(":lines", hdr))
prot_printf(nntp_out, "%lu %lu\r\n", by_msgid ? 0 : uid,
index_getlines(nntp_group, msgno));
else
prot_printf(nntp_out, "%lu \r\n", by_msgid ? 0 : uid);
}
else if (!strcmp(hdr, "xref") && !pat /* [X]HDR only */) {
char xref[8192];
if (!by_msgid) msgid = index_get_msgid(nntp_group, msgno);
build_xref(msgid, xref, sizeof(xref), 1);
if (!by_msgid) free(msgid);
prot_printf(nntp_out, "%lu %s\r\n", by_msgid ? 0 : uid, xref);
}
else if ((body = index_getheader(nntp_group, msgno, hdr)) &&
(!pat || /* [X]HDR */
wildmat(body, pat))) { /* XPAT with match */
prot_printf(nntp_out, "%lu %s\r\n", by_msgid ? 0 : uid, body);
}
}
prot_printf(nntp_out, ".\r\n");
}
static void cmd_help(void)
{
prot_printf(nntp_out, "100 Supported commands:\r\n");
/* auth/security commands */
if (!nntp_authstate)
prot_printf(nntp_out, "\tAUTHINFO USER | PASS | SASL\r\n");
if (tls_enabled() && !nntp_starttls_done)
prot_printf(nntp_out, "\tSTARTTLS\r\n");
/* reader-only commands */
if (nntp_capa & MODE_READ) {
prot_printf(nntp_out, "\tARTICLE\r\n");
prot_printf(nntp_out, "\tBODY\r\n");
prot_printf(nntp_out, "\tDATE\r\n");
prot_printf(nntp_out, "\tGROUP\r\n");
prot_printf(nntp_out, "\tHDR | XHDR\r\n");
prot_printf(nntp_out, "\tLAST\r\n");
prot_printf(nntp_out, "\tLISTGROUP\r\n");
if (config_getswitch(IMAPOPT_ALLOWNEWNEWS))
prot_printf(nntp_out, "\tNEWNEWS\r\n");
prot_printf(nntp_out, "\tNEXT\r\n");
prot_printf(nntp_out, "\tOVER | XOVER\r\n");
prot_printf(nntp_out, "\tPOST\r\n");
prot_printf(nntp_out, "\tSLAVE\r\n");
}
/* feeder-only commands */
if (nntp_capa & MODE_FEED) prot_printf(nntp_out, "\tIHAVE\r\n");
/* streaming-only commands */
if (nntp_capa & MODE_STREAM) {
prot_printf(nntp_out, "\tCHECK\r\n");
prot_printf(nntp_out, "\tTAKETHIS\r\n");
}
/* generic commands */
prot_printf(nntp_out, "\tHEAD\r\n");
prot_printf(nntp_out, "\tHELP\r\n");
prot_printf(nntp_out, "\tLIST [ ACTIVE | EXTENSIONS");
if (nntp_capa & MODE_READ)
prot_printf(nntp_out, " | HEADERS | NEWSGROUPS | OVERVIEW.FMT");
prot_printf(nntp_out, " ]\r\n");
prot_printf(nntp_out, "\tMODE");
if (nntp_capa & MODE_READ) prot_printf(nntp_out, " READER");
if (nntp_capa & MODE_STREAM) {
prot_printf(nntp_out, "%s STREAM",
(nntp_capa & MODE_READ) ? " |" : "");
}
prot_printf(nntp_out, "\r\n");
prot_printf(nntp_out, "\tSTAT\r\n");
prot_printf(nntp_out, "\tQUIT\r\n");
prot_printf(nntp_out, ".\r\n");
}
struct list_rock {
int (*proc)();
struct wildmat *wild;
struct hash_table server_table;
};
/*
* mboxlist_findall() callback function to LIST
*/
int list_cb(char *name, int matchlen, int maycreate __attribute__((unused)),
void *rock)
{
static char lastname[MAX_MAILBOX_NAME+1];
struct list_rock *lrock = (struct list_rock *) rock;
struct wildmat *wild;
/* We have to reset the initial state.
* Handle it as a dirty hack.
*/
if (!name) {
lastname[0] = '\0';
return 0;
}
/* skip personal mailboxes */
if ((!strncasecmp(name, "INBOX", 5) && (!name[5] || name[5] == '.')) ||
!strncmp(name, "user.", 5))
return 0;
/* don't repeat */
if (matchlen == strlen(lastname) &&
!strncmp(name, lastname, matchlen)) return 0;
strncpy(lastname, name, matchlen);
lastname[matchlen] = '\0';
/* see if the mailbox matches one of our wildmats */
wild = lrock->wild;
while (wild->pat && wildmat(name, wild->pat) != 1) wild++;
/* if we don't have a match, or its a negative match, skip it */
if (!wild->pat || wild->not) return 0;
return lrock->proc(name, lrock);
}
struct enum_rock {
const char *cmd;
char *wild;
};
/*
* hash_enumerate() callback function to LIST (proxy)
*/
void list_proxy(char *server, void *data, void *rock)
{
struct enum_rock *erock = (struct enum_rock *) rock;
struct backend *be;
int r;
char *result;
be = proxyd_findserver(server);
if (!be) return;
prot_printf(be->out, "LIST %s %s\r\n", erock->cmd, erock->wild);
r = read_response(be, 0, &result);
if (!r && !strncmp(result, "215 ", 4)) {
while (!(r = read_response(be, 0, &result)) && result[0] != '.') {
prot_printf(nntp_out, "%s", result);
}
}
}
/*
* perform LIST ACTIVE (backend) or create a server hash table (proxy)
*/
int do_active(char *name, void *rock)
{
struct list_rock *lrock = (struct list_rock *) rock;
int r, postable;
struct backend *be;
/* open the group */
r = open_group(name, 1, &be, &postable);
if (r) {
/* can't open group, skip it */
}
else if (be) {
if (!hash_lookup(be->hostname, &lrock->server_table)) {
/* add this server to our table */
hash_insert(be->hostname, (void *)0xDEADBEEF, &lrock->server_table);
}
}
else {
prot_printf(nntp_out, "%s %lu %lu %c\r\n", name+strlen(newsprefix),
nntp_group->exists ? index_getuid(nntp_group->exists) :
nntp_group->last_uid,
nntp_group->exists ? index_getuid(1) :
nntp_group->last_uid+1,
postable ? 'y' : 'n');
mailbox_close(nntp_group);
nntp_group = 0;
}
return 0;
}
/*
* perform LIST NEWSGROUPS (backend) or create a server hash table (proxy)
*/
int do_newsgroups(char *name, void *rock)
{
struct list_rock *lrock = (struct list_rock *) rock;
char *acl, *server;
int r;
r = mlookup(name, &server, &acl, NULL);
if (r || !acl || !(cyrus_acl_myrights(nntp_authstate, acl) && ACL_LOOKUP))
return 0;
if (server) {
/* remote group */
if (!hash_lookup(server, &lrock->server_table)) {
/* add this server to our table */
hash_insert(server, (void *)0xDEADBEEF, &lrock->server_table);
}
}
else {
/* local group */
return CYRUSDB_DONE;
}
return 0;
}
/*
* annotatemore_findall() callback function to LIST NEWSGROUPS
*/
int newsgroups_cb(const char *mailbox,
const char *entry __attribute__((unused)),
const char *userid,
struct annotation_data *attrib, void *rock)
{
struct wildmat *wild = (struct wildmat *) rock;
/* skip personal mailboxes */
if ((!strncasecmp(mailbox, "INBOX", 5) &&
(!mailbox[5] || mailbox[5] == '.')) ||
!strncmp(mailbox, "user.", 5))
return 0;
/* see if the mailbox matches one of our wildmats */
while (wild->pat && wildmat(mailbox, wild->pat) != 1) wild++;
/* if we don't have a match, or its a negative match, skip it */
if (!wild->pat || wild->not) return 0;
/* we only care about shared /comment */
if (userid[0]) return 0;
prot_printf(nntp_out, "%s\t%s\r\n", mailbox+strlen(newsprefix),
attrib->value);
return 0;
}
static void cmd_list(char *arg1, char *arg2)
{
if (!arg1)
arg1 = "active";
else
lcase(arg1);
if (!strcmp(arg1, "active")) {
char pattern[MAX_MAILBOX_NAME+1];
struct list_rock lrock = { &do_active };
struct enum_rock erock = { "ACTIVE" };
if (!arg2) arg2 = "*";
/* make a copy before we munge it */
erock.wild = xstrdup(arg2);
/* split the list of wildmats */
lrock.wild = split_wildmats(arg2);
/* xxx better way to determine a size for this table? */
construct_hash_table(&lrock.server_table, 10, 1);
prot_printf(nntp_out, "215 List of newsgroups follows:\r\n");
strcpy(pattern, newsprefix);
strcat(pattern, "*");
list_cb(NULL, 0, 0, NULL);
mboxlist_findall(NULL, pattern, 0, nntp_userid, nntp_authstate,
list_cb, &lrock);
/* proxy to the backends */
hash_enumerate(&lrock.server_table, list_proxy, &erock);
prot_printf(nntp_out, ".\r\n");
/* free the hash table */
free_hash_table(&lrock.server_table, NULL);
/* free the wildmats */
free_wildmats(lrock.wild);
free(erock.wild);
if (nntp_group) {
mailbox_close(nntp_group);
nntp_group = 0;
}
}
else if (!strcmp(arg1, "extensions")) {
unsigned mechcount = 0;
const char *mechlist;
if (arg2) {
prot_printf(nntp_out, "501 Unexpected extra argument\r\n");
return;
}
prot_printf(nntp_out, "202 Extension list follows:\r\n");
if (!nntp_authstate) {
/* check for SASL mechs */
sasl_listmech(nntp_saslconn, NULL, "SASL ", " ", "\r\n",
&mechlist, NULL, &mechcount);
if (mechcount || nntp_starttls_done ||
config_getswitch(IMAPOPT_ALLOWPLAINTEXT)) {
prot_printf(nntp_out, "AUTHINFO%s\r\n",
(nntp_starttls_done ||
config_getswitch(IMAPOPT_ALLOWPLAINTEXT)) ?
" USER" : "");
/* add the SASL mechs */
if (mechcount)
prot_write(nntp_out, mechlist, strlen(mechlist));
}
}
if ((nntp_capa & MODE_READ) &&
(nntp_userid || allowanonymous)) {
prot_printf(nntp_out, "HDR ALL\r\n");
prot_printf(nntp_out, "LISTGROUP\r\n");
prot_printf(nntp_out, "OVER\r\n");
}
if (tls_enabled() && !nntp_starttls_done && !nntp_authstate)
prot_printf(nntp_out, "STARTTLS\r\n");
if (nntp_capa & MODE_STREAM) {
prot_printf(nntp_out, "STREAMING\r\n");
}
prot_printf(nntp_out, ".\r\n");
did_extensions = 1;
}
else if (!(nntp_capa & MODE_READ)) {
prot_printf(nntp_out, "502 Permission denied\r\n");
return;
}
else if (!nntp_userid && !allowanonymous) {
prot_printf(nntp_out, "480 Authentication required\r\n");
return;
}
else if (!strcmp(arg1, "headers")) {
if (arg2) {
prot_printf(nntp_out, "501 Unexpected extra argument\r\n");
return;
}
prot_printf(nntp_out, "215 Header and metadata list follows:\r\n");
prot_printf(nntp_out, ":\r\n"); /* all headers */
prot_printf(nntp_out, ":bytes\r\n");
prot_printf(nntp_out, ":lines\r\n");
prot_printf(nntp_out, ".\r\n");
}
else if (!strcmp(arg1, "newsgroups")) {
char pattern[MAX_MAILBOX_NAME+1];
struct list_rock lrock = { &do_newsgroups };
struct enum_rock erock = { "NEWSGROUPS" };
if (!arg2) arg2 = "*";
/* make a copy before we munge it */
erock.wild = xstrdup(arg2);
/* split the list of wildmats */
lrock.wild = split_wildmats(arg2);
/* xxx better way to determine a size for this table? */
construct_hash_table(&lrock.server_table, 10, 1);
prot_printf(nntp_out, "215 List of newsgroups follows:\r\n");
strcpy(pattern, newsprefix);
strcat(pattern, "*");
list_cb(NULL, 0, 0, NULL);
mboxlist_findall(NULL, pattern, 0, nntp_userid, nntp_authstate,
list_cb, &lrock);
/* proxy to the backends */
hash_enumerate(&lrock.server_table, list_proxy, &erock);
strcpy(pattern, newsprefix);
strcat(pattern, "*");
annotatemore_findall(pattern, "/comment",
newsgroups_cb, lrock.wild, NULL);
prot_printf(nntp_out, ".\r\n");
/* free the hash table */
free_hash_table(&lrock.server_table, NULL);
/* free the wildmats */
free_wildmats(lrock.wild);
free(erock.wild);
}
else if (!strcmp(arg1, "overview.fmt")) {
if (arg2) {
prot_printf(nntp_out, "501 Unexpected extra argument\r\n");
return;
}
prot_printf(nntp_out, "215 Order of overview fields follows:\r\n");
prot_printf(nntp_out, "Subject:\r\n");
prot_printf(nntp_out, "From:\r\n");
prot_printf(nntp_out, "Date:\r\n");
prot_printf(nntp_out, "Message-ID:\r\n");
prot_printf(nntp_out, "References:\r\n");
if (did_extensions) {
/* new OVER format */
prot_printf(nntp_out, ":bytes\r\n");
prot_printf(nntp_out, ":lines\r\n");
} else {
/* old XOVER format */
prot_printf(nntp_out, "Bytes:\r\n");
prot_printf(nntp_out, "Lines:\r\n");
}
prot_printf(nntp_out, "Xref:full\r\n");
prot_printf(nntp_out, ".\r\n");
}
else if (!strcmp(arg1, "active.times") || !strcmp(arg1, "distributions") ||
!strcmp(arg1, "distrib.pats")) {
prot_printf(nntp_out, "503 Unsupported LIST command\r\n");
}
else {
prot_printf(nntp_out, "501 Unrecognized LIST command\r\n");
}
prot_flush(nntp_out);
}
static void cmd_mode(char *arg)
{
lcase(arg);
if (!strcmp(arg, "reader")) {
prot_printf(nntp_out,
"%u %s Cyrus NNTP%s %s server ready, posting %s\r\n",
(nntp_capa & MODE_READ) ? 200 : 201,
config_servername, config_mupdate_server ? " Murder" : "",
CYRUS_VERSION,
(nntp_capa & MODE_READ) ? "allowed" : "prohibited");
}
else if (!strcmp(arg, "stream")) {
if (nntp_capa & MODE_STREAM) {
prot_printf(nntp_out, "203 Streaming allowed\r\n");
}
else {
prot_printf(nntp_out, "502 Streaming prohibited\r\n");
}
}
else {
prot_printf(nntp_out, "501 Unrecognized MODE\r\n");
}
prot_flush(nntp_out);
}
static void cmd_newgroups(time_t tstamp)
{
prot_printf(nntp_out, "231 List of new newsgroups follows:\r\n");
/* Do search of annotations here. */
prot_printf(nntp_out, ".\r\n");
}
struct newrock {
time_t tstamp;
struct wildmat *wild;
};
/*
* duplicate_find() callback function to list NEWNEWS
*/
static int newnews_cb(const char *msgid, const char *rcpt, time_t mark,
unsigned long uid, void *rock)
{
static char lastid[1024];
struct newrock *nrock = (struct newrock *) rock;
/* We have to reset the initial state.
* Handle it as a dirty hack.
*/
if (!msgid) {
lastid[0] = '\0';
return 0;
}
/* Make sure we don't return duplicate msgids,
* the message is newer than the tstamp, and
* the message isn't in a personal mailbox.
*/
if (strcmp(msgid, lastid) && mark >= nrock->tstamp &&
uid && rcpt[0] && strncmp(rcpt, "user.", 5)) {
struct wildmat *wild = nrock->wild;
strlcpy(lastid, msgid, sizeof(lastid));
/* see if the mailbox matches one of our wildmats */
while (wild->pat && wildmat(rcpt, wild->pat) != 1) wild++;
/* we have a match, and its not a negative match */
if (wild->pat && !wild->not)
prot_printf(nntp_out, "%s\r\n", msgid);
}
return 0;
}
static void cmd_newnews(char *wild, time_t tstamp)
{
struct newrock nrock;
nrock.tstamp = tstamp;
nrock.wild = split_wildmats(wild);
prot_printf(nntp_out, "230 List of new articles follows:\r\n");
newnews_cb(NULL, NULL, 0, 0, NULL);
duplicate_find("", &newnews_cb, &nrock);
prot_printf(nntp_out, ".\r\n");
free_wildmats(nrock.wild);
}
static void cmd_over(char *msgid, unsigned long uid, unsigned long last)
{
int msgno;
struct nntp_overview *over;
int found = 0;
for (; uid <= last; uid++) {
msgno = index_finduid(uid);
if (!msgno || index_getuid(msgno) != uid) continue;
if ((over = index_overview(nntp_group, msgno))) {
char xref[8192];
build_xref(over->msgid, xref, sizeof(xref), 0);
if (!found++)
prot_printf(nntp_out, "224 Overview information follows:\r\n");
prot_printf(nntp_out, "%lu\t%s\t%s\t%s\t%s\t%s\t%lu\t%lu\t%s\r\n",
msgid ? 0 : over->uid,
over->subj ? over->subj : "",
over->from ? over->from : "",
over->date ? over->date : "",
over->msgid ? over->msgid : "",
over->ref ? over->ref : "",
over->bytes + strlen(xref) + 2, /* +2 for \r\n */
over->lines, xref);
}
}
if (found)
prot_printf(nntp_out, ".\r\n");
else
prot_printf(nntp_out, "420 No articles selected\r\n");
}
#define RCPT_GROW 30
typedef struct message_data message_data_t;
struct message_data {
struct protstream *data; /* message in temp file */
FILE *f; /* FILE * corresponding */
struct stagemsg *stage; /* staging location for single instance
store */
char *id; /* message id */
char *path; /* path */
char *control; /* control message */
unsigned long size; /* size of message in bytes */
char **rcpt; /* mailboxes to post message */
int rcpt_num; /* number of groups */
hdrcache_t hdrcache;
};
/* returns non-zero on failure */
int msg_new(message_data_t **m)
{
message_data_t *ret = (message_data_t *) xmalloc(sizeof(message_data_t));
ret->data = NULL;
ret->f = NULL;
ret->stage = NULL;
ret->id = NULL;
ret->path = NULL;
ret->control = NULL;
ret->size = 0;
ret->rcpt = NULL;
ret->rcpt_num = 0;
ret->hdrcache = spool_new_hdrcache();
*m = ret;
return 0;
}
void msg_free(message_data_t *m)
{
int i;
if (m->data) {
prot_free(m->data);
}
if (m->f) {
fclose(m->f);
}
if (m->stage) {
append_removestage(m->stage);
}
if (m->id) {
free(m->id);
}
if (m->path) {
free(m->path);
}
if (m->control) {
free(m->control);
}
if (m->rcpt) {
for (i = 0; i < m->rcpt_num; i++) {
free(m->rcpt[i]);
}
free(m->rcpt);
}
spool_free_hdrcache(m->hdrcache);
free(m);
}
static int parse_groups(const char *groups, message_data_t *msg)
{
const char *p = groups;
char *rcpt = NULL;
size_t n;
for (;;) {
while (p && *p && (isspace((int) *p) || *p == ',')) p++;
if (!p || !*p) return 0;
if (!(msg->rcpt_num % RCPT_GROW)) { /* time to alloc more */
msg->rcpt = (char **)
xrealloc(msg->rcpt, (msg->rcpt_num + RCPT_GROW + 1) *
sizeof(char *));
}
n = strcspn(p, ", \t");
rcpt = xrealloc(rcpt, strlen(newsprefix) + n + 1);
if (!rcpt) return -1;
sprintf(rcpt, "%s%.*s", newsprefix, n, p);
/* Only add mailboxes that exist */
if (!mlookup(rcpt, NULL, NULL, NULL)) {
msg->rcpt[msg->rcpt_num] = rcpt;
msg->rcpt_num++;
msg->rcpt[msg->rcpt_num] = rcpt = NULL;
}
p += n;
}
return NNTP_FAIL_NEWSGROUPS;
}
/*
* file in the message structure 'm' from 'pin', assuming a dot-stuffed
* stream a la nntp.
*
* returns 0 on success, imap error code on failure
*/
static int savemsg(message_data_t *m, FILE *f)
{
struct stat sbuf;
const char **body;
int r, i;
time_t now = time(NULL);
static int post_count = 0;
FILE *stagef = NULL;
/* fill the cache */
r = spool_fill_hdrcache(nntp_in, f, m->hdrcache);
if (r) {
/* got a bad header */
/* flush the remaining output */
spool_copy_msg(nntp_in, NULL);
return r;
}
/* now, using our header cache, fill in the data that we want */
/* get path */
if ((body = spool_getheader(m->hdrcache, "path")) != NULL) {
m->path = xstrdup(body[0]);
} else {
m->path = NULL; /* no path-id */
fprintf(f, "Path: %s!%s\r\n",
config_servername, nntp_userid ? nntp_userid : "anonymous");
}
/* get message-id */
if ((body = spool_getheader(m->hdrcache, "message-id")) != NULL) {
m->id = xstrdup(body[0]);
} else {
/* no message-id, create one */
pid_t p = getpid();
m->id = xmalloc(40 + strlen(config_servername));
sprintf(m->id, "<cmu-nntpd-%d-%d-%d@%s>", p, (int) now,
post_count++, config_servername);
fprintf(f, "Message-ID: %s\r\n", m->id);
}
/* get date */
if ((body = spool_getheader(m->hdrcache, "date")) == NULL) {
/* date, create one */
char datestr[80];
rfc822date_gen(datestr, sizeof(datestr), now);
fprintf(f, "Date: %s\r\n", datestr);
}
/* get control */
if ((body = spool_getheader(m->hdrcache, "control")) != NULL) {
int len;
m->control = xstrdup(body[0]);
/* create a recipient for the appropriate pseudo newsgroup */
m->rcpt_num = 1;
m->rcpt = (char **) xmalloc(sizeof(char *));
len = strcspn(m->control, " \t\r\n");
m->rcpt[0] = xmalloc(strlen(newsprefix) + 8 + len + 1);
sprintf(m->rcpt[0], "%scontrol.%.*s", newsprefix, len, m->control);
} else {
m->control = NULL; /* no control */
/* get newsgroups */
if ((body = spool_getheader(m->hdrcache, "newsgroups")) != NULL) {
/* parse newsgroups and create recipients */
r = parse_groups(body[0], m);
if (!r && !m->rcpt_num) {
r = IMAP_MAILBOX_NONEXISTENT; /* no newsgroups that we serve */
}
if (!r) {
const char *newspostuser;
if ((newspostuser = config_getstring(IMAPOPT_NEWSPOSTUSER))) {
char buf[1024] = "";
const char *sep = "";
int n;
/* build a To: header */
for (n = 0; n < m->rcpt_num; n++) {
snprintf(buf+strlen(buf), sizeof(buf)-strlen(buf),
"%s%s+%s@%s", sep, newspostuser,
m->rcpt[n]+strlen(newsprefix),
config_servername);
sep = ", ";
}
/* add To: header */
fprintf(f, "To: %s\r\n", buf);
}
}
} else {
r = NNTP_NO_NEWSGROUPS; /* no newsgroups header */
}
if (r) {
/* error getting newsgroups */
/* flush the remaining output */
spool_copy_msg(nntp_in, NULL);
return r;
}
}
fflush(f);
if (ferror(f)) {
return IMAP_IOERROR;
}
if (fstat(fileno(f), &sbuf) == -1) {
return IMAP_IOERROR;
}
/* spool to the stage of one of the recipients */
for (i = 0; !stagef && (i < m->rcpt_num); i++) {
stagef = append_newstage(m->rcpt[i], now, 0, &m->stage);
}
if (stagef) {
const char *base = 0;
unsigned long size = 0;
int n;
/* copy the header from our tmpfile to the stage */
map_refresh(fileno(f), 1, &base, &size, sbuf.st_size, "tmp", 0);
n = retry_write(fileno(stagef), base, size);
map_free(&base, &size);
if (n == -1) {
/* close and remove the stage */
fclose(stagef);
append_removestage(m->stage);
return IMAP_IOERROR;
}
else {
/* close the tmpfile and use the stage */
fclose(f);
f = stagef;
}
}
/* else this is probably a remote group, so use the tmpfile */
r = spool_copy_msg(nntp_in, f);
if (r) return r;
fflush(f);
if (ferror(f)) {
return IMAP_IOERROR;
}
if (fstat(fileno(f), &sbuf) == -1) {
return IMAP_IOERROR;
}
m->size = sbuf.st_size;
m->f = f;
m->data = prot_new(fileno(f), 0);
return 0;
}
static int deliver(message_data_t *msg)
{
int n, r, myrights;
char *rcpt = NULL, *local_rcpt = NULL, *server, *acl;
time_t now = time(NULL);
unsigned long uid, backend_mask = 0;
/* check ACLs of all mailboxes */
for (n = 0; n < msg->rcpt_num; n++) {
rcpt = msg->rcpt[n];
/* look it up */
r = mlookup(rcpt, &server, &acl, NULL);
if (r) return IMAP_MAILBOX_NONEXISTENT;
if (!(acl && (myrights = cyrus_acl_myrights(nntp_authstate, acl)) &&
(myrights & ACL_POST)))
return IMAP_PERMISSION_DENIED;
if (server) {
/* remote group */
struct backend *be = NULL;
unsigned id;
char buf[4096];
be = proxyd_findserver(server);
if (!be) return IMAP_SERVER_UNAVAILABLE;
/* check if we've already sent to this backend
* XXX this only works for <= 32 backends
*/
if ((id = *((unsigned *) be->context)) < 32) {
if (backend_mask & (1 << id)) continue;
backend_mask |= (1 << id);
}
/* tell the backend about our new article */
prot_printf(be->out, "IHAVE %s\r\n", msg->id);
prot_flush(be->out);
if (!prot_fgets(buf, sizeof(buf), be->in) ||
strncmp("335", buf, 3)) {
syslog(LOG_NOTICE, "backend doesn't want article %s", msg->id);
continue;
}
/* send the article */
rewind(msg->f);
while (fgets(buf, sizeof(buf), msg->f)) {
if (buf[0] == '.') prot_putc('.', be->out);
do {
prot_printf(be->out, "%s", buf);
} while (buf[strlen(buf)-1] != '\n' &&
fgets(buf, sizeof(buf), msg->f));
}
/* Protect against messages not ending in CRLF */
if (buf[strlen(buf)-1] != '\n') prot_printf(be->out, "\r\n");
prot_printf(be->out, ".\r\n");
if (!prot_fgets(buf, sizeof(buf), be->in) ||
strncmp("235", buf, 3)) {
syslog(LOG_WARNING, "article %s transfer to backend failed",
msg->id);
return NNTP_FAIL_TRANSFER;
}
}
else {
/* local group */
struct appendstate as;
if (msg->id &&
duplicate_check(msg->id, strlen(msg->id), rcpt, strlen(rcpt))) {
/* duplicate message */
duplicate_log(msg->id, rcpt, "nntp delivery");
continue;
}
r = append_setup(&as, rcpt, MAILBOX_FORMAT_NORMAL,
nntp_userid, nntp_authstate, ACL_POST, 0);
if (!r) {
prot_rewind(msg->data);
if (msg->stage) {
r = append_fromstage(&as, msg->stage, now,
(const char **) NULL, 0, !singleinstance);
} else {
/* XXX should never get here */
r = append_fromstream(&as, msg->data, msg->size, now,
(const char **) NULL, 0);
}
if (!r) append_commit(&as, 0, NULL, &uid, NULL);
else append_abort(&as);
}
if (!r && msg->id)
duplicate_mark(msg->id, strlen(msg->id), rcpt, strlen(rcpt),
now, uid);
if (r) return r;
local_rcpt = rcpt;
}
}
return 0;
}
static int newgroup(message_data_t *msg)
{
int r;
char *group;
char mailboxname[MAX_MAILBOX_NAME+1];
/* isolate newsgroup */
group = msg->control + 8; /* skip "newgroup" */
while (isspace((int) *group)) group++;
snprintf(mailboxname, sizeof(mailboxname), "%s%.*s",
newsprefix, (int) strcspn(group, " \t\r\n"), group);
r = mboxlist_createmailbox(mailboxname, 0, NULL, 0,
newsmaster, newsmaster_authstate, 0, 0, 0);
/* XXX check body of message for useful MIME parts */
return r;
}
static int rmgroup(message_data_t *msg)
{
int r;
char *group;
char mailboxname[MAX_MAILBOX_NAME+1];
/* isolate newsgroup */
group = msg->control + 7; /* skip "rmgroup" */
while (isspace((int) *group)) group++;
snprintf(mailboxname, sizeof(mailboxname), "%s%.*s",
newsprefix, (int) strcspn(group, " \t\r\n"), group);
/* XXX should we delete right away, or wait until empty? */
r = mboxlist_deletemailbox(mailboxname, 0,
newsmaster, newsmaster_authstate, 1, 0, 0);
return r;
}
static int mvgroup(message_data_t *msg)
{
int r, len;
char *group;
char oldmailboxname[MAX_MAILBOX_NAME+1];
char newmailboxname[MAX_MAILBOX_NAME+1];
/* isolate old newsgroup */
group = msg->control + 7; /* skip "mvgroup" */
while (isspace((int) *group)) group++;
len = (int) strcspn(group, " \t\r\n");
snprintf(oldmailboxname, sizeof(oldmailboxname), "%s%.*s",
newsprefix, len, group);
/* isolate new newsgroup */
group += len; /* skip old newsgroup */
while (isspace((int) *group)) group++;
len = (int) strcspn(group, " \t\r\n");
snprintf(newmailboxname, sizeof(newmailboxname), "%s%.*s",
newsprefix, len, group);
r = mboxlist_renamemailbox(oldmailboxname, newmailboxname, NULL, 0,
newsmaster, newsmaster_authstate);
/* XXX check body of message for useful MIME parts */
return r;
}
/*
* mailbox_exchange() callback function to delete cancelled articles
*/
static int expunge_cancelled(struct mailbox *mailbox, void *rock, char *index)
{
int uid = ntohl(*((bit32 *)(index+OFFSET_UID)));
/* only expunge the UID that we obtained from the msgid */
return (uid == *((unsigned long *) rock));
}
/*
* duplicate_find() callback function to cancel articles
*/
static int cancel_cb(const char *msgid __attribute__((unused)),
const char *mailbox,
time_t mark __attribute__((unused)),
unsigned long uid,
void *rock)
{
/* make sure its a message in a mailbox that we're serving via NNTP */
if (*mailbox && !strncmp(mailbox, newsprefix, strlen(newsprefix)) &&
strncmp(mailbox, "user.", 5)) {
struct mailbox mbox;
int r, doclose = 0;
r = mailbox_open_header(mailbox, 0, &mbox);
if (!r &&
!(cyrus_acl_myrights(newsmaster_authstate, mbox.acl) & ACL_DELETE))
r = IMAP_PERMISSION_DENIED;
if (!r) {
doclose = 1;
if (mbox.header_fd != -1)
mailbox_lock_header(&mbox);
mbox.header_lock_count = 1;
r = mailbox_open_index(&mbox);
}
if (!r) {
mailbox_lock_index(&mbox);
mbox.index_lock_count = 1;
mailbox_expunge(&mbox, 0, expunge_cancelled, &uid);
}
if (doclose) mailbox_close(&mbox);
/* if we failed, pass the return code back in the rock */
if (r) *((int *) rock) = r;
}
return 0;
}
static int cancel(message_data_t *msg)
{
int r = 0;
char *msgid, *p;
time_t now = time(NULL);
/* isolate msgid */
msgid = strchr(msg->control, '<');
p = strrchr(msgid, '>') + 1;
*p = '\0';
/* find and expunge the message from all mailboxes */
duplicate_find(msgid, &cancel_cb, &r);
/* store msgid of cancelled message for IHAVE/CHECK/TAKETHIS
* (in case we haven't received the message yet)
*/
duplicate_mark(msgid, strlen(msgid), "", 0, 0, now);
return r;
}
static void feedpeer(char *peer, message_data_t *msg)
{
char *user, *pass, *host, *port, *wild, *path, *s;
int oldform = 0;
struct wildmat *wmat = NULL, *w;
int len, err, n, feed = 1;
struct addrinfo hints, *res, *res0;
int sock = -1;
struct protstream *pin, *pout;
char buf[4096];
/* parse the peer */
user = pass = host = port = wild = NULL;
if (wild = strrchr(peer, '/'))
*wild++ = '\0';
else if ((wild = strrchr(peer, ':')) &&
strcspn(wild, "!*?,.") != strlen(wild)) {
*wild++ = '\0';
host = peer;
oldform = 1;
}
if (!oldform) {
if (host = strchr(peer, '@')) {
*host++ = '\0';
user = peer;
if (pass = strchr(user, ':')) *pass++ = '\0';
}
else
host = peer;
if (port = strchr(host, ':')) *port++ = '\0';
}
/* check path to see if this message came through our peer */
len = strlen(host);
path = msg->path;
while (path && (s = strchr(path, '!'))) {
if ((s - path) == len && !strncmp(path, host, len)) {
return;
}
path = s + 1;
}
/* check newsgroups against wildmat to see if we should feed it */
if (wild && *wild) {
wmat = split_wildmats(wild);
feed = 0;
for (n = 0; n < msg->rcpt_num; n++) {
/* see if the newsgroup matches one of our wildmats */
w = wmat;
while (w->pat &&
wildmat(msg->rcpt[n], w->pat) != 1) {
w++;
}
if (w->pat) {
/* we have a match, check to see what kind of match */
if (!w->not) {
/* positive match, ok to feed, keep checking */
feed = 1;
}
else if (w->not < 0) {
/* absolute negative match, do not feed */
feed = 0;
break;
}
else {
/* negative match, keep checking */
}
}
else {
/* no match, keep checking */
}
}
free_wildmats(wmat);
}
if (!feed) return;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = 0;
if (!port || !*port) port = "119";
if ((err = getaddrinfo(host, port, &hints, &res0)) != 0) {
syslog(LOG_ERR, "getaddrinfo(%s, %s) failed: %m", host, port);
return;
}
for (res = res0; res; res = res->ai_next) {
if ((sock = socket(res->ai_family, res->ai_socktype,
res->ai_protocol)) < 0)
continue;
if (connect(sock, res->ai_addr, res->ai_addrlen) >= 0)
break;
close(sock);
sock = -1;
}
freeaddrinfo(res0);
if(sock < 0) {
syslog(LOG_ERR, "connect(%s:%s) failed: %m", host, port);
return;
}
pin = prot_new(sock, 0);
pout = prot_new(sock, 1);
prot_setflushonread(pin, pout);
/* read the initial greeting */
if (!prot_fgets(buf, sizeof(buf), pin) || strncmp("200", buf, 3)) {
syslog(LOG_ERR, "peer doesn't allow posting");
goto quit;
}
if (user) {
/* change to reader mode - not always necessary, so ignore result */
prot_printf(pout, "MODE READER\r\n");
prot_fgets(buf, sizeof(buf), pin);
if (*user) {
/* authenticate to peer */
/* XXX this should be modified to support SASL and STARTTLS */
prot_printf(pout, "AUTHINFO USER %s\r\n", user);
if (!prot_fgets(buf, sizeof(buf), pin)) {
syslog(LOG_ERR, "AUTHINFO USER terminated abnormally");
goto quit;
}
else if (!strncmp("381", buf, 3)) {
/* password required */
if (!pass) {
syslog(LOG_ERR, "need password for AUTHINFO PASS");
goto quit;
}
prot_printf(pout, "AUTHINFO PASS %s\r\n", pass);
if (!prot_fgets(buf, sizeof(buf), pin)) {
syslog(LOG_ERR, "AUTHINFO PASS terminated abnormally");
goto quit;
}
}
if (strncmp("281", buf, 3)) {
/* auth failed */
syslog(LOG_ERR, "authentication failed");
goto quit;
}
}
/* tell the peer we want to post */
prot_printf(pout, "POST\r\n");
prot_flush(pout);
if (!prot_fgets(buf, sizeof(buf), pin) || strncmp("340", buf, 3)) {
syslog(LOG_ERR, "peer doesn't allow posting");
goto quit;
}
}
else {
/* tell the peer about our new article */
prot_printf(pout, "IHAVE %s\r\n", msg->id);
prot_flush(pout);
if (!prot_fgets(buf, sizeof(buf), pin) || strncmp("335", buf, 3)) {
syslog(LOG_ERR, "peer doesn't want article %s", msg->id);
goto quit;
}
}
/* send the article */
rewind(msg->f);
while (fgets(buf, sizeof(buf), msg->f)) {
if (buf[0] == '.') prot_putc('.', pout);
do {
prot_printf(pout, "%s", buf);
} while (buf[strlen(buf)-1] != '\n' &&
fgets(buf, sizeof(buf), msg->f));
}
/* Protect against messages not ending in CRLF */
if (buf[strlen(buf)-1] != '\n') prot_printf(pout, "\r\n");
prot_printf(pout, ".\r\n");
if (!prot_fgets(buf, sizeof(buf), pin) || strncmp("2", buf, 1)) {
syslog(LOG_ERR, "article %s transfer to peer failed", msg->id);
}
quit:
prot_printf(pout, "QUIT\r\n");
prot_flush(pout);
prot_fgets(buf, sizeof(buf), pin);
/* Flush the incoming buffer */
prot_NONBLOCK(pin);
prot_fill(pin);
/* close/free socket & prot layer */
close(sock);
prot_free(pin);
prot_free(pout);
return;
}
void printstring(const char *s __attribute__((unused)))
{
/* needed to link against annotate.o */
fatal("printstring() executed, but its not used for nntpd!",
EC_SOFTWARE);
}
#define ALLOC_SIZE 10
static void news2mail(message_data_t *msg)
{
struct annotation_data attrib;
int n, i, r;
FILE *sm;
static const char **smbuf = NULL;
static int allocsize = 0;
int sm_stat;
pid_t sm_pid;
char buf[4096], to[1024] = "";
if (!smbuf) {
allocsize += ALLOC_SIZE;
smbuf = xzmalloc(allocsize * sizeof(const char *));
smbuf[0] = "sendmail";
smbuf[1] = "-i"; /* ignore dots */
smbuf[2] = "-f";
smbuf[3] = "<>";
smbuf[4] = "--";
}
for (i = 5, n = 0; n < msg->rcpt_num; n++) {
/* see if we want to send this to a mailing list */
r = annotatemore_lookup(msg->rcpt[n],
"/vendor/cmu/cyrus-imapd/news2mail", "",
&attrib);
if (r) continue;
/* add the email address to our argv[] and to our To: header */
if (attrib.value) {
if (i >= allocsize - 1) {
allocsize += ALLOC_SIZE;
smbuf = xrealloc(smbuf, allocsize * sizeof(const char *));
}
smbuf[i++] = xstrdup(attrib.value);
smbuf[i] = NULL;
if (to[0]) strlcat(to, ", ", sizeof(to));
strlcat(to, attrib.value, sizeof(to));
}
}
/* send the message */
if (i > 5) {
sm_pid = open_sendmail(smbuf, &sm);
if (!sm)
syslog(LOG_ERR, "news2mail: could not spawn sendmail process");
else {
int body = 0, skip, found_to = 0;
rewind(msg->f);
while (fgets(buf, sizeof(buf), msg->f)) {
if (!body && buf[0] == '\r' && buf[1] == '\n') {
/* blank line between header and body */
body = 1;
/* insert a To: header if the message doesn't have one */
if (!found_to) fprintf(sm, "To: %s\r\n", to);
}
skip = 0;
if (!body) {
/* munge various news-specific headers */
if (!strncasecmp(buf, "Newsgroups:", 11)) {
/* rename Newsgroups: to X-Newsgroups: */
fprintf(sm, "X-");
} else if (!strncasecmp(buf, "Xref:", 5) ||
!strncasecmp(buf, "Path:", 5) ||
!strncasecmp(buf, "NNTP-Posting-", 13)) {
/* skip these (for now) */
skip = 1;
} else if (!strncasecmp(buf, "To:", 3)) {
/* insert our mailing list RCPTs first, and then
fold the header to accomodate the original RCPTs */
fprintf(sm, "To: %s,\r\n", to);
/* overwrite the original "To:" with spaces */
memset(buf, ' ', 3);
found_to = 1;
}
}
do {
if (!skip) fprintf(sm, "%s", buf);
} while (buf[strlen(buf)-1] != '\n' &&
fgets(buf, sizeof(buf), msg->f));
}
/* Protect against messages not ending in CRLF */
if (buf[strlen(buf)-1] != '\n') fprintf(sm, "\r\n");
fclose(sm);
while (waitpid(sm_pid, &sm_stat, 0) < 0);
if (sm_stat) /* sendmail exit value */
syslog(LOG_ERR, "news2mail failed: %s",
sendmail_errstr(sm_stat));
}
/* free the RCPTs */
for (i = 5; smbuf[i]; i++) {
free((char *) smbuf[i]);
smbuf[i] = NULL;
}
}
return;
}
static void cmd_post(char *msgid, int mode)
{
FILE *f = NULL;
message_data_t *msg;
int r = 0;
/* check if we want this article */
if (msgid && find_msgid(msgid, NULL, NULL)) {
/* already have it */
r = NNTP_DONT_SEND;
}
if (mode != POST_TAKETHIS) {
if (r) {
prot_printf(nntp_out, "%u %s Do not send article\r\n",
post_codes[mode].no, msgid ? msgid : "");
return;
}
else {
prot_printf(nntp_out, "%u %s Send article\r\n",
post_codes[mode].cont, msgid ? msgid : "");
if (mode == POST_CHECK) return;
}
}
/* get a spool file (if needed) */
if (!r) {
f = tmpfile();
if (!f) r = IMAP_IOERROR;
}
if (f) {
msg_new(&msg);
/* spool the article */
r = savemsg(msg, f);
/* deliver the article */
if (!r) r = deliver(msg);
if (!r) {
prot_printf(nntp_out, "%u %s Article received ok\r\n",
post_codes[mode].ok, msg->id ? msg->id : "");
/* process control messages */
if (msg->control && !config_mupdate_server) {
int r1 = 0;
/* XXX check PGP signature */
if (!strncmp(msg->control, "newgroup", 8))
r1 = newgroup(msg);
else if (!strncmp(msg->control, "rmgroup", 7))
r1 = rmgroup(msg);
else if (!strncmp(msg->control, "mvgroup", 7))
r1 = mvgroup(msg);
else if (!strncmp(msg->control, "cancel", 6))
r1 = cancel(msg);
else
r1 = NNTP_UNKNOWN_CONTROLMSG;
if (r1)
syslog(LOG_WARNING, "control message '%s' failed: %s",
msg->control, error_message(r1));
else {
syslog(LOG_INFO, "control message '%s' succeeded",
msg->control);
}
}
if (msg->id) {
const char *peers = config_getstring(IMAPOPT_NEWSPEER);
/* send the article upstream */
if (peers) {
char *tmpbuf, *cur_peer, *next_peer;
/* make a working copy of the peers */
cur_peer = tmpbuf = xstrdup(peers);
while (cur_peer) {
/* eat any leading whitespace */
while (isspace(*cur_peer)) cur_peer++;
/* find end of peer */
if (next_peer = strchr(cur_peer, ' '))
*next_peer++ = '\0';
/* feed the article to this peer */
feedpeer(cur_peer, msg);
/* move to next peer */
cur_peer = next_peer;
}
free(tmpbuf);
}
/* gateway news to mail */
news2mail(msg);
}
}
msg_free(msg); /* does fclose() */
}
else {
/* flush the article from the stream */
spool_copy_msg(nntp_in, NULL);
}
if (r) {
prot_printf(nntp_out, "%u %s Failed receiving article (%s)\r\n",
post_codes[mode].fail, msgid ? msgid : "",
error_message(r));
}
prot_flush(nntp_out);
}
#ifdef HAVE_SSL
static void cmd_starttls(int nntps)
{
int result;
int *layerp;
sasl_ssf_t ssf;
char *auth_id;
if (nntp_starttls_done == 1) {
prot_printf(nntp_out, "502 %s\r\n",
"Already successfully executed STARTTLS");
return;
}
/* SASL and openssl have different ideas about whether ssf is signed */
layerp = (int *) &ssf;
result=tls_init_serverengine("nntp",
5, /* depth to verify */
!nntps, /* can client auth? */
!nntps); /* TLS only? */
if (result == -1) {
syslog(LOG_ERR, "[nntpd] error initializing TLS");
if (nntps == 0)
prot_printf(nntp_out, "580 %s\r\n", "Error initializing TLS");
else
fatal("tls_init() failed",EC_TEMPFAIL);
return;
}
if (nntps == 0)
{
prot_printf(nntp_out, "382 %s\r\n", "Begin TLS negotiation now");
/* must flush our buffers before starting tls */
prot_flush(nntp_out);
}
result=tls_start_servertls(0, /* read */
1, /* write */
layerp,
&auth_id,
&tls_conn);
/* if error */
if (result==-1) {
if (nntps == 0) {
prot_printf(nntp_out, "580 Starttls failed\r\n");
syslog(LOG_NOTICE, "[nntpd] STARTTLS failed: %s", nntp_clienthost);
} else {
syslog(LOG_NOTICE, "nntps failed: %s", nntp_clienthost);
fatal("tls_start_servertls() failed", EC_TEMPFAIL);
}
return;
}
/* tell SASL about the negotiated layer */
result = sasl_setprop(nntp_saslconn, SASL_SSF_EXTERNAL, &ssf);
if (result != SASL_OK) {
fatal("sasl_setprop() failed: cmd_starttls()", EC_TEMPFAIL);
}
saslprops.ssf = ssf;
result = sasl_setprop(nntp_saslconn, SASL_AUTH_EXTERNAL, auth_id);
if (result != SASL_OK) {
fatal("sasl_setprop() failed: cmd_starttls()", EC_TEMPFAIL);
}
if(saslprops.authid) {
free(saslprops.authid);
saslprops.authid = NULL;
}
if(auth_id)
saslprops.authid = xstrdup(auth_id);
/* tell the prot layer about our new layers */
prot_settls(nntp_in, tls_conn);
prot_settls(nntp_out, tls_conn);
nntp_starttls_done = 1;
}
#else
static void cmd_starttls(int nntps __attribute__((unused)))
{
fatal("cmd_starttls() called, but no OpenSSL", EC_SOFTWARE);
}
#endif /* HAVE_SSL */
static struct wildmat *split_wildmats(char *str)
{
const char *prefix;
char pattern[MAX_MAILBOX_NAME+1] = "", *p, *c;
struct wildmat *wild = NULL;
int n = 0;
if ((prefix = config_getstring(IMAPOPT_NEWSPREFIX)))
snprintf(pattern, sizeof(pattern), "%s.", prefix);
p = pattern + strlen(pattern);
/*
* split the list of wildmats
*
* we split them right to left because this is the order in which
* we want to test them (per draft-ietf-nntpext-base 5.2)
*/
do {
if ((c = strrchr(str, ',')))
*c++ = '\0';
else
c = str;
if (!(n % 10)) /* alloc some more */
wild = xrealloc(wild, (n + 11) * sizeof(struct wildmat));
if (*c == '!') wild[n].not = 1; /* not */
else if (*c == '@') wild[n].not = -1; /* absolute not (feeding) */
else wild[n].not = 0;
strcpy(p, wild[n].not ? c + 1 : c);
wild[n++].pat = xstrdup(pattern);
} while (c != str);
wild[n].pat = NULL;
return wild;
}
static void free_wildmats(struct wildmat *wild)
{
struct wildmat *w = wild;
while (w->pat) {
free(w->pat);
w++;
}
free(wild);
}

File Metadata

Mime Type
text/x-c
Expires
Sat, Apr 4, 1:46 AM (1 w, 4 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18822006
Default Alt Text
nntpd.c (95 KB)

Event Timeline