/* 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.43 2005/01/07 20:59:04 ken3 Exp $ */ /* * TODO: * * - add sender and PGP verification code for control messages * - figure out what to do with control messages when proxying */ #include #ifdef HAVE_UNISTD_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 __attribute__((unused))) { 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_capabilities = 0; int allowanonymous = 0; int singleinstance = 1; /* attempt single instance store */ struct stagemsg *stage = NULL; /* Bitmasks for NNTP modes */ enum { MODE_READ = (1<<0), MODE_FEED = (1<<1) }; static unsigned nntp_capa = MODE_READ | MODE_FEED; /* general-purpose */ 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_capabilities(char *keyword); 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, const char *empty_resp, 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 __attribute__((unused)), 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; } /* 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_reset_stdio(); 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; did_capabilities = 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); 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_FEED; 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 __attribute__((unused)), char **argv __attribute__((unused)), char **envp __attribute__((unused))) { socklen_t salen; char localip[60], remoteip[60]; char hbuf[NI_MAXHOST]; int niflags; 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)); strlcat(nntp_clienthost, " ", sizeof(nntp_clienthost)); nntp_clienthost[sizeof(nntp_clienthost)-30] = '\0'; } else { nntp_clienthost[0] = '\0'; } niflags = NI_NUMERICHOST; #ifdef NI_WITHSCOPEID if (((struct sockaddr *)&nntp_remoteaddr)->sa_family == AF_INET6) niflags |= NI_WITHSCOPEID; #endif if (getnameinfo((struct sockaddr *)&nntp_remoteaddr, salen, hbuf, sizeof(hbuf), NULL, 0, niflags) != 0) strlcpy(hbuf, "unknown", sizeof(hbuf)); 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("nntp", 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 ] [-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); } if (stage) append_removestage(stage); 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 && strcmp(err, PROT_EOF_STRING)) { 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); } /* Ihave/Takethis only allowed for feeders */ if (!(nntp_capa & MODE_FEED) && strchr("IT", cmd.s[0])) goto noperm; /* Body/Date/Group/Newgroups/Newnews/Next/Over/Post/Xhdr/Xover/Xpat only allowed for readers */ if (!(nntp_capa & MODE_READ) && strchr("BDGNOPX", cmd.s[0])) goto noperm; /* Only Authinfo/Capabilities/Check/Head/Help/Ihave/List Active/ Mode/Quit/Starttls/Stat/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, "Capabilities")) { arg1.len = 0; if (c == ' ') { c = getword(nntp_in, &arg1); /* keyword (optional) */ if (c == EOF) goto missingargs; } if (c == '\r') c = prot_getc(nntp_in); if (c != '\n') goto extraargs; cmd_capabilities(arg1.s); } else if (!(nntp_capa & MODE_FEED)) goto noperm; else 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)) goto nomsgid; if (!nntp_group || strcmp(mboxname, nntp_group->name)) { if ((r = open_group(mboxname, 1, ret, NULL))) goto nomsgid; *msgid = str; } /* else, within the current group, so treat as by uid */ } 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 == ULONG_MAX || *p) return -1; /* convert timestr to ulong */ t = strtoul(timestr, &p, 10); if (t == ULONG_MAX || *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; } static void cmd_capabilities(char *keyword __attribute__((unused))) { const char *mechlist; unsigned mechcount = 0; prot_printf(nntp_out, "101 Capability list follows:\r\n"); prot_printf(nntp_out, "VERSION 2\r\n"); prot_printf(nntp_out, "IMPLEMENTATION Cyrus NNTP%s server %s\r\n", config_mupdate_server ? " Murder" : "", CYRUS_VERSION); /* add STARTTLS */ if (tls_enabled() && !nntp_starttls_done && !nntp_authstate) prot_printf(nntp_out, "STARTTLS\r\n"); /* check for SASL mechs */ sasl_listmech(nntp_saslconn, NULL, "SASL ", " ", "\r\n", &mechlist, NULL, &mechcount); /* add the AUTHINFO variants */ if (!nntp_authstate) { prot_printf(nntp_out, "AUTHINFO%s%s\r\n", (nntp_starttls_done || config_getswitch(IMAPOPT_ALLOWPLAINTEXT)) ? " USER" : "", mechcount ? " SASL" : ""); } /* add the SASL mechs */ if (mechcount) prot_printf(nntp_out, "%s", mechlist); /* add the reader capabilities/extensions */ if ((nntp_capa & MODE_READ) && (nntp_userid || allowanonymous)) { prot_printf(nntp_out, "READER POST LISTGROUP\r\n"); prot_printf(nntp_out, "HDR\r\n"); prot_printf(nntp_out, "OVER\r\n"); prot_printf(nntp_out, "XPAT\r\n"); } /* add the feeder capabilities/extensions */ if (nntp_capa & MODE_FEED) { prot_printf(nntp_out, "IHAVE\r\n"); prot_printf(nntp_out, "STREAMING\r\n"); } /* add the LIST variants */ prot_printf(nntp_out, "LIST ACTIVE%s\r\n", ((nntp_capa & MODE_READ) && (nntp_userid || allowanonymous)) ? " HEADERS NEWSGROUPS OVERVIEW.FMT" : ""); prot_printf(nntp_out, ".\r\n"); did_capabilities = 1; } /* * 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) { 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) { syslog(LOG_NOTICE, "badlogin: %s plaintext %s %s", nntp_clienthost, nntp_userid, sasl_errdetail(nntp_saslconn)); sleep(3); 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" : "", "User logged in"); 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) { int code; const char *errorstring = NULL; switch (r) { case IMAP_SASL_CANCEL: prot_printf(nntp_out, "481 Client canceled authentication\r\n"); break; case IMAP_SASL_PROTERR: errorstring = prot_error(nntp_in); prot_printf(nntp_out, "482 Error reading client response: %s\r\n", errorstring ? errorstring : ""); break; default: /* failed authentication */ switch (sasl_result) { case SASL_NOMECH: case SASL_TOOWEAK: code = 501; break; case SASL_ENCRYPT: code = 483; break; case SASL_BADPROT: code = 482; break; default: code = 481; } 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, "%d %s\r\n", code, errorstring); } else { prot_printf(nntp_out, "%d Error authenticating\r\n", code); } } 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, "481 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, "283 %s\r\n", success_data); free(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"); if ((nntp_capa & MODE_READ) && (nntp_userid || allowanonymous)) { prot_printf(nntp_out, "\tARTICLE [ message-id | number ]\r\n" "\t\tRetrieve entirety of the specified article.\r\n"); } if (!nntp_authstate) { if (!nntp_userid) { prot_printf(nntp_out, "\tAUTHINFO SASL mechanism [initial-response]\r\n" "\t\tPerform an authentication exchange using the specified\r\n" "\t\tSASL mechanism.\r\n"); prot_printf(nntp_out, "\tAUTHINFO USER username\r\n" "\t\tPresent username for authentication.\r\n"); } prot_printf(nntp_out, "\tAUTHINFO PASS password\r\n" "\t\tPresent clear-text password for authentication.\r\n"); } if ((nntp_capa & MODE_READ) && (nntp_userid || allowanonymous)) { prot_printf(nntp_out, "\tBODY [ message-id | number ]\r\n" "\t\tRetrieve body of the specified article.\r\n"); } prot_printf(nntp_out, "\tCAPABILITIES\r\n" "\t\tList the current server capabilities.\r\n"); if (nntp_capa & MODE_FEED) { prot_printf(nntp_out, "\tCHECK message-id\r\n" "\t\tCheck if the server wants the specified article.\r\n"); } if ((nntp_capa & MODE_READ) && (nntp_userid || allowanonymous)) { prot_printf(nntp_out, "\tDATE\r\n" "\t\tRequest the current server UTC date and time.\r\n"); prot_printf(nntp_out, "\tGROUP group\r\n" "\t\tSelect a newsgroup for article retrieval.\r\n"); prot_printf(nntp_out, "\tHDR header [ message-id | range ]\r\n" "\t\tRetrieve the specified header/metadata from the\r\n" "\t\tspecified article(s).\r\n"); } prot_printf(nntp_out, "\tHEAD [ message-id | number ]\r\n" "\t\tRetrieve the headers of the specified article.\r\n"); prot_printf(nntp_out, "\tHELP\r\n" "\t\tRequest command summary (this text).\r\n"); if (nntp_capa & MODE_FEED) { prot_printf(nntp_out, "\tIHAVE message-id\r\n" "\t\tPresent/transfer the specified article to the server.\r\n"); } if ((nntp_capa & MODE_READ) && (nntp_userid || allowanonymous)) { prot_printf(nntp_out, "\tLAST\r\n" "\t\tSelect the previous article.\r\n"); } prot_printf(nntp_out, "\tLIST [ ACTIVE wildmat ]\r\n" "\t\tList the (subset of) valid newsgroups.\r\n"); if ((nntp_capa & MODE_READ) && (nntp_userid || allowanonymous)) { prot_printf(nntp_out, "\tLIST HEADERS [ MSGID | RANGE ]\r\n" "\t\tList the headers and metadata items available via HDR.\r\n"); prot_printf(nntp_out, "\tLIST NEWSGROUPS [wildmat]\r\n" "\t\tList the descriptions of the specified newsgroups.\r\n"); prot_printf(nntp_out, "\tLIST OVERVIEW.FMT\r\n" "\t\tList the headers and metadata items available via OVER.\r\n"); } if ((nntp_capa & MODE_READ) && (nntp_userid || allowanonymous)) { prot_printf(nntp_out, "\tLISTGROUP [group]\r\n" "\t\tList the article numbers in the specified newsgroup.\r\n"); if (config_getswitch(IMAPOPT_ALLOWNEWNEWS)) prot_printf(nntp_out, "\tNEWNEWS wildmat date time [GMT]\r\n" "\t\tList the newly arrived articles in the specified newsgroup(s)\r\n" "\t\tsince the specified date and time.\r\n"); prot_printf(nntp_out, "\tNEXT\r\n" "\t\tSelect the next article.\r\n"); prot_printf(nntp_out, "\tOVER [ message-id | range ]\r\n" "\t\tRetrieve the overview information for the specified article(s).\r\n"); prot_printf(nntp_out, "\tPOST\r\n" "\t\tPost an article to the server.\r\n"); } prot_printf(nntp_out, "\tQUIT\r\n" "\t\tTerminate the session.\r\n"); if (tls_enabled() && !nntp_starttls_done && !nntp_authstate) { prot_printf(nntp_out, "\tSTARTTLS\r\n" "\t\tStart a TLS negotiation.\r\n"); } prot_printf(nntp_out, "\tSTAT [ message-id | number ]\r\n" "\t\tCheck if the specified article exists.\r\n"); if (nntp_capa & MODE_FEED) { prot_printf(nntp_out, "\tTAKETHIS message-id\r\n" "\t\tTransfer the specified article to the server.\r\n"); } if ((nntp_capa & MODE_READ) && (nntp_userid || allowanonymous)) { prot_printf(nntp_out, "\tXPAT header message-id|range wildmat\r\n" "\t\tList the specified article(s) in which the contents\r\n" "\t\tof the specified header/metadata matches the wildmat.\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 __attribute__((unused)), 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; struct enum_rock erock; if (!arg2) arg2 = "*"; erock.cmd = "ACTIVE"; erock.wild = xstrdup(arg2); /* make a copy before we munge it */ lrock.proc = do_active; lrock.wild = split_wildmats(arg2); /* split the list of wildmats */ /* 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 (!(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 && strcmp(arg2, "msgid") && strcmp(arg2, "range")) { 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; struct enum_rock erock; if (!arg2) arg2 = "*"; erock.cmd = "NEWSGROUPS"; erock.wild = xstrdup(arg2); /* make a copy before we munge it */ lrock.proc = do_newsgroups; lrock.wild = split_wildmats(arg2); /* split the list of wildmats */ /* 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_capabilities) { /* 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_FEED) { 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 __attribute__((unused))) { prot_printf(nntp_out, "503 Can't determine NEWGROUPS at this time\r\n"); #if 0 prot_printf(nntp_out, "231 List of new newsgroups follows:\r\n"); /* Do search of annotations here. */ prot_printf(nntp_out, ".\r\n"); #endif } 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 */ 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->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->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; char *rcpt = NULL; size_t n; for (p = groups;; p += n) { /* skip whitespace */ 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 *)); } /* find end of group name */ n = strcspn(p, ", \t"); rcpt = xrealloc(rcpt, strlen(newsprefix) + n + 1); if (!rcpt) return -1; /* construct the mailbox name */ 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; } } 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, **groups; int r, i; time_t now = time(NULL); static int post_count = 0; FILE *stagef = NULL; const char *skipheaders[] = { "Path", /* need to prepend our servername */ "Xref", /* need to remove (generated on the fly) */ "Reply-To", /* need to add "post" email addresses */ NULL }; int addlen; /* fill the cache */ r = spool_fill_hdrcache(nntp_in, f, m->hdrcache, skipheaders); 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 */ addlen = strlen(config_servername) + 1; if ((body = spool_getheader(m->hdrcache, "path")) != NULL) { /* prepend to the cached path */ addlen += strlen(body[0]); body[0] = xrealloc((char *) body[0], addlen + 1); memmove((char *) body[0] + strlen(config_servername) + 1, body[0], strlen(body[0]) + 1); /* +1 for \0 */ strcpy((char *) body[0], config_servername); *((char *) body[0] + strlen(config_servername)) = '!'; m->path = xstrdup(body[0]); } else { /* no path, create one */ addlen += nntp_userid ? strlen(nntp_userid) : strlen("anonymous"); m->path = xmalloc(addlen + 1); sprintf(m->path, "%s!%s", config_servername, nntp_userid ? nntp_userid : "anonymous"); spool_cache_header(xstrdup("Path"), xstrdup(m->path), m->hdrcache); } fprintf(f, "Path: %s\r\n", m->path); /* 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, "", p, (int) now, post_count++, config_servername); fprintf(f, "Message-ID: %s\r\n", m->id); spool_cache_header(xstrdup("Message-ID"), xstrdup(m->id), m->hdrcache); } /* get date */ if ((body = spool_getheader(m->hdrcache, "date")) == NULL) { /* no date, create one */ char datestr[80]; rfc822date_gen(datestr, sizeof(datestr), now); fprintf(f, "Date: %s\r\n", datestr); spool_cache_header(xstrdup("Date"), xstrdup(datestr), m->hdrcache); } /* 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 ((groups = spool_getheader(m->hdrcache, "newsgroups")) != NULL) { /* parse newsgroups and create recipients */ r = parse_groups(groups[0], m); if (!r && !m->rcpt_num) { r = IMAP_MAILBOX_NONEXISTENT; /* no newsgroups that we serve */ } if (!r) { const char *newspostuser = config_getstring(IMAPOPT_NEWSPOSTUSER); /* get reply-to */ body = spool_getheader(m->hdrcache, "reply-to"); /* add Reply-To: header */ if (body || newspostuser) { const char **postto, *p; char *replyto, *r, *fold = NULL, *sep = ""; size_t n; if (newspostuser) { /* add "post" email addresses based on newsgroup */ /* determine which groups header to use */ postto = spool_getheader(m->hdrcache, "followup-to"); if (!postto) postto = groups; /* count the number of groups */ for (n = 0, p = postto[0]; p; n++) { p = strchr(p, ','); if (p) p++; } /* estimate size of post addresses */ addlen = strlen(postto[0]) + n * (strlen(newspostuser) + 3); if (body) { /* append to the cached header */ addlen += strlen(body[0]); body[0] = xrealloc((char *) body[0], addlen + 1); replyto = (char *) body[0]; fold = replyto + strlen(replyto) + 1; sep = ", "; } else { /* create a new header body */ replyto = xzmalloc(addlen + 1); } r = replyto + strlen(replyto); for (p = postto[0];; p += n) { /* skip whitespace */ while (p && *p && (isspace((int) *p) || *p == ',')) p++; if (!p || !*p) break; /* find end of group name */ n = strcspn(p, ", \t"); /* add the post address */ r += sprintf(r, "%s%s+%.*s", sep, newspostuser, n, p); sep = ", "; } if (!body) { /* add the new header to the cache */ spool_cache_header(xstrdup("Reply-To"), replyto, m->hdrcache); } } else { /* no newspostuser, use original replyto */ replyto = (char *) body[0]; } /* add the header to the file */ fprintf(f, "Reply-To: "); r = replyto; if (fold) { fprintf(f, "%.*s\r\n\t", fold - r, r); r = fold; } fprintf(f, "%s\r\n", r); } } } 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, &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(stage); stage = NULL; 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 (stage) { r = append_fromstage(&as, 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 __attribute__((unused)), 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; } /* strip any post addresses from a header body. * returns 1 if a nonpost address was found, 0 otherwise. */ static int strip_post_addresses(char *body) { const char *newspostuser = config_getstring(IMAPOPT_NEWSPOSTUSER); char *p, *end; size_t postlen, n; int nonpost = 0; if (!newspostuser) return 1; /* we didn't add this header, so leave it */ postlen = strlen(newspostuser); for (p = body;; p += n) { end = p; /* skip whitespace */ while (p && *p && (isspace((int) *p) || *p == ',')) p++; if (!p || !*p) break; /* find end of address */ n = strcspn(p, ", \t\r\n"); if ((n > postlen + 1) && /* +1 for '+' */ !strncmp(p, newspostuser, postlen) && p[postlen] == '+') { /* found a post address. since we always add the post * addresses to the end of the header, truncate it right here. */ strcpy(end, "\r\n"); break; } nonpost = 1; } return nonpost; } 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]; int body = 0, skip; /* 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 (!body && buf[0] == '\r' && buf[1] == '\n') { /* blank line between header and body */ body = 1; } skip = 0; if (!body) { if (!strncasecmp(buf, "Reply-To:", 9)) { /* strip any post addresses, skip if becomes empty */ if (!strip_post_addresses(buf+9)) skip = 1; } } if (!skip && buf[0] == '.') prot_putc('.', pout); do { if (!skip) 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; } else if (!strncasecmp(buf, "Reply-To:", 9)) { /* strip any post addresses, skip if becomes empty */ if (!strip_post_addresses(buf+9)) skip = 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 = strchr(cur_peer, '\t'))) *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() */ if (stage) append_removestage(stage); stage = NULL; } 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); }