X-Git-Url: https://code.citadel.org/?a=blobdiff_plain;f=citadel%2Fmodules%2Fnntp%2Fserv_nntp.c;h=14f75db6ce32328f38133dec8e5a044648707270;hb=e329db30593524cc2d8851a4500bac41f2340354;hp=6a75c622a5cd0e9c5b7845e7327b7f940171a9da;hpb=09347a0495a9390ce5c015ab25192f7446b189c6;p=citadel.git diff --git a/citadel/modules/nntp/serv_nntp.c b/citadel/modules/nntp/serv_nntp.c index 6a75c622a..14f75db6c 100644 --- a/citadel/modules/nntp/serv_nntp.c +++ b/citadel/modules/nntp/serv_nntp.c @@ -1,7 +1,7 @@ // -// NNTP server module FIXME THIS IS NOT FINISHED +// NNTP server module (RFC 3977) // -// Copyright (c) 2014 by the citadel.org team +// Copyright (c) 2014-2018 by the citadel.org team // // This program is open source software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 3. @@ -74,6 +74,7 @@ extern long timezone; int is_valid_newsgroup_name(char *name) { char *ptr = name; int has_a_letter = 0; + int num_dots = 0; if (!ptr) return(0); if (!strncasecmp(name, "ctdl.", 5)) return(0); @@ -84,6 +85,10 @@ int is_valid_newsgroup_name(char *name) { has_a_letter = 1; } + if (ptr[0] == '.') { + ++num_dots; + } + if ( (isalnum(ptr[0])) || (ptr[0] == '.') || (ptr[0] == '+') @@ -95,11 +100,10 @@ int is_valid_newsgroup_name(char *name) { return(0); } } - return(has_a_letter); + return( (has_a_letter) && (num_dots >= 1) ) ; } - // // Convert a Citadel room name to a valid newsgroup name // @@ -124,7 +128,7 @@ void room_to_newsgroup(char *target, char *source, size_t target_size) { || (ch == '.') || (ch == '-') ) { - target[len++] = ch; + target[len++] = tolower(ch); target[len] = 0; } else { @@ -185,9 +189,9 @@ void nntp_greeting(void) strcpy(CC->cs_clientname, "NNTP session"); CC->cs_flags |= CS_STEALTH; - /* CC->session_specific_data = malloc(sizeof(citnntp)); - memset(NNTP, 0, sizeof(citnntp)); - */ + CC->session_specific_data = malloc(sizeof(citnntp)); + citnntp *nntpstate = (citnntp *) CC->session_specific_data; + memset(nntpstate, 0, sizeof(citnntp)); if (CC->nologin==1) { cprintf("451 Too many connections are already open; please try again later.\r\n"); @@ -195,10 +199,8 @@ void nntp_greeting(void) return; } - // Note: the FQDN *must* appear as the first thing after the 220 code. - // Some clients (including citmail.c) depend on it being there. - // - cprintf("200 %s NNTP Citadel server is not finished yet\r\n", config.c_fqdn); + // Display the standard greeting + cprintf("200 %s NNTP Citadel server is not finished yet\r\n", CtdlGetConfigStr("c_fqdn")); } @@ -214,7 +216,6 @@ void nntps_greeting(void) { } - // // implements the STARTTLS command // @@ -237,11 +238,12 @@ void nntp_starttls(void) void nntp_capabilities(void) { cprintf("101 Capability list:\r\n"); - cprintf("IMPLEMENTATION Citadel v%d.%02d\r\n", (REV_LEVEL/100), (REV_LEVEL%100)); + cprintf("IMPLEMENTATION Citadel %d\r\n", REV_LEVEL); cprintf("VERSION 2\r\n"); cprintf("READER\r\n"); cprintf("MODE-READER\r\n"); cprintf("LIST ACTIVE NEWSGROUPS\r\n"); + cprintf("OVER\r\n"); #ifdef HAVE_OPENSSL cprintf("STARTTLS\r\n"); #endif @@ -271,19 +273,18 @@ void nntp_cleanup(void) } - // // Implements the AUTHINFO USER command (RFC 4643) // void nntp_authinfo_user(const char *username) { - int a = CtdlLoginExistingUser(NULL, username); + int a = CtdlLoginExistingUser(username); switch (a) { case login_already_logged_in: cprintf("482 Already logged in\r\n"); return; case login_too_many_users: - cprintf("481 Too many users are already online (maximum is %d)\r\n", config.c_maxsessions); + cprintf("481 Too many users are already online (maximum is %d)\r\n", CtdlGetConfigInt("c_maxsessions")); return; case login_ok: cprintf("381 Password required for %s\r\n", CC->curr_user); @@ -323,7 +324,6 @@ void nntp_authinfo_pass(const char *buf) } - // // Implements the AUTHINFO extension (RFC 4643) in USER/PASS mode // @@ -365,20 +365,6 @@ struct nntp_msglist nntp_fetch_msglist(struct ctdlroom *qrbuf) { } - -// -// Various output formats for the LIST commands -// -enum { - NNTP_LIST_ACTIVE, - NNTP_LIST_ACTIVE_TIMES, - NNTP_LIST_DISTRIB_PATS, - NNTP_LIST_HEADERS, - NNTP_LIST_NEWSGROUPS, - NNTP_LIST_OVERVIEW_FMT -}; - - // // Output a room name (newsgroup name) in formats required for LIST and NEWGROUPS command // @@ -419,7 +405,6 @@ void output_roomname_in_list_format(struct ctdlroom *qrbuf, int which_format, ch } - // // Called once per room by nntp_newgroups() to qualify and possibly output a single room // @@ -450,13 +435,8 @@ void nntp_newgroups_backend(struct ctdlroom *qrbuf, void *data) // Implements the NEWGROUPS command // void nntp_newgroups(const char *cmd) { - /* - * HACK: this works because the 5XX series error codes from citadel - * protocol will also be considered error codes by an NNTP client - */ if (CtdlAccessCheck(ac_logged_in_or_guest)) return; - char stringy_date[16]; char stringy_time[16]; char stringy_gmt[16]; @@ -513,10 +493,6 @@ void nntp_list_backend(struct ctdlroom *qrbuf, void *data) // Implements the LIST commands // void nntp_list(const char *cmd) { - // - // HACK: this works because the 5XX series error codes from citadel - // protocol will also be considered error codes by an NNTP client - // if (CtdlAccessCheck(ac_logged_in_or_guest)) return; char list_format[64]; @@ -539,11 +515,30 @@ void nntp_list(const char *cmd) { else if (!strcasecmp(list_format, "NEWSGROUPS")) { nld.list_format = NNTP_LIST_NEWSGROUPS; } + else if (!strcasecmp(list_format, "OVERVIEW.FMT")) { + nld.list_format = NNTP_LIST_OVERVIEW_FMT; + } else { cprintf("501 syntax error , unsupported list format\r\n"); return; } + // OVERVIEW.FMT delivers a completely different type of data than all of the + // other LIST commands. It's a stupid place to put it. But that's how it's + // written into RFC3977, so we have to handle it here. + if (nld.list_format == NNTP_LIST_OVERVIEW_FMT) { + cprintf("215 Order of fields in overview database.\r\n"); + cprintf("Subject:\r\n"); + cprintf("From:\r\n"); + cprintf("Date:\r\n"); + cprintf("Message-ID:\r\n"); + cprintf("References:\r\n"); + cprintf("Bytes:\r\n"); + cprintf("Lines:\r\n"); + cprintf(".\r\n"); + return; + } + cprintf("215 list of newsgroups follows\r\n"); CtdlGetUser(&CC->user, CC->curr_user); CtdlForEachRoom(nntp_list_backend, &nld); @@ -561,6 +556,25 @@ void nntp_help(void) { } +// +// Implement DATE command. +// +void nntp_date(void) { + time_t now; + struct tm nowLocal; + struct tm nowUtc; + char tsFromUtc[32]; + + now = time(NULL); + localtime_r(&now, &nowLocal); + gmtime_r(&now, &nowUtc); + + strftime(tsFromUtc, sizeof(tsFromUtc), "%Y%m%d%H%M%S", &nowUtc); + + cprintf("111 %s\r\n", tsFromUtc); +} + + // // back end for the LISTGROUP command , called for each message number // @@ -580,12 +594,9 @@ void nntp_listgroup_backend(long msgnum, void *userdata) { // Implements the GROUP and LISTGROUP commands // void nntp_group(const char *cmd) { - // - // HACK: this works because the 5XX series error codes from citadel - // protocol will also be considered error codes by an NNTP client - // if (CtdlAccessCheck(ac_logged_in_or_guest)) return; + citnntp *nntpstate = (citnntp *) CC->session_specific_data; char verb[16]; char requested_group[1024]; char message_range[256]; @@ -653,8 +664,9 @@ void nntp_group(const char *cmd) { CtdlUserGoto(NULL, 0, 0, &msgs, &new, &oldest, &newest); cprintf("211 %d %ld %ld %s\r\n", msgs, oldest, newest, requested_group); - // If this is a GROUP command, we can stop here. + // If this is a GROUP command, set the "current article number" to zero, and then stop here. if (!strcasecmp(verb, "GROUP")) { + nntpstate->current_article_number = oldest; return; } @@ -674,7 +686,8 @@ void nntp_mode(const char *cmd) { extract_token(which_mode, cmd, 1, ' ', sizeof which_mode); if (!strcasecmp(which_mode, "reader")) { - cprintf("201 Reader mode FIXME implement posting and change to 200\r\n"); + // FIXME implement posting and change to 200 + cprintf("201 Reader mode activated\r\n"); } else { cprintf("501 unknown mode\r\n"); @@ -682,14 +695,23 @@ void nntp_mode(const char *cmd) { } - // // Implements the ARTICLE, HEAD, BODY, and STAT commands. // (These commands all accept the same parameters; they differ only in how they output the retrieved message.) // void nntp_article(const char *cmd) { + if (CtdlAccessCheck(ac_logged_in_or_guest)) return; + + citnntp *nntpstate = (citnntp *) CC->session_specific_data; + char which_command[16]; + int acmd = 0; + char requested_article[256]; + long requested_msgnum = 0; + char *lb, *rb = NULL; + int must_change_currently_selected_article = 0; // We're going to store one of these values in the variable 'acmd' so that + // we can quickly check later which version of the output we want. enum { ARTICLE, HEAD, @@ -697,8 +719,6 @@ void nntp_article(const char *cmd) { STAT }; - char which_command[16]; - int acmd = 0; extract_token(which_command, cmd, 0, ' ', sizeof which_command); if (!strcasecmp(which_command, "article")) { @@ -725,36 +745,39 @@ void nntp_article(const char *cmd) { else if (acmd == STAT) headers_only = HEADERS_FAST; // now figure out what the client is asking for. - char requested_article[256]; - long requested_msgnum = 0; - char *lb, *rb = NULL; extract_token(requested_article, cmd, 1, ' ', sizeof requested_article); lb = strchr(requested_article, '<'); rb = strchr(requested_article, '>'); requested_msgnum = atol(requested_article); - // If no article number or message-id is specified, the client wants the "next" article. - // We don't know how to do that yet. + // If no article number or message-id is specified, the client wants the "currently selected article" if (IsEmptyStr(requested_article)) { - cprintf("500 FIXME I don't know how to fetch next yet.\r\n"); - return; + if (nntpstate->current_article_number < 1) { + cprintf("420 No current article selected\r\n"); + return; + } + requested_msgnum = nntpstate->current_article_number; + must_change_currently_selected_article = 1; + // got it -- now fall through and keep going } // If the requested article is numeric, it maps directly to a message number. Good. else if (requested_msgnum > 0) { + must_change_currently_selected_article = 1; // good -- fall through and keep going } // If the requested article has angle brackets, the client wants a specific message-id. // We don't know how to do that yet. else if ( (lb != NULL) && (rb != NULL) && (lb < rb) ) { - cprintf("500 FIXME I don't know how to fetch by message-id yet.\r\n"); + must_change_currently_selected_article = 0; + cprintf("500 I don't know how to fetch by message-id yet.\r\n"); // FIXME return; } // Anything else is noncompliant gobbledygook and should die in a car fire. - // Also, the weasel who is spreading untrue rumors about me at work should die in a slow and painful car fire. else { + must_change_currently_selected_article = 0; cprintf("500 syntax error\r\n"); return; } @@ -783,6 +806,13 @@ void nntp_article(const char *cmd) { return; } + // RFC3977 6.2.1.2 specifes conditions under which the "currently selected article" + // MUST or MUST NOT be set to the message we just referenced. + if (must_change_currently_selected_article) { + nntpstate->current_article_number = requested_msgnum; + } + + // Now give the client what it asked for. if (acmd == ARTICLE) { cprintf("220 %ld <%s>\r\n", requested_msgnum, fetched_message_id); } @@ -805,6 +835,222 @@ void nntp_article(const char *cmd) { } +// +// Utility function for nntp_last_next() that turns a msgnum into a message ID. +// The memory for the returned string is pwnz0red by the caller. +// +char *message_id_from_msgnum(long msgnum) { + + char *fetched_message_id = NULL; + CC->redirect_buffer = NewStrBufPlain(NULL, SIZ); + CtdlOutputMsg(msgnum, + MT_RFC822, // output in RFC822 format ... sort of + HEADERS_FAST, // headers, body, or both? + 0, // don't do Citadel protocol responses + 1, // CRLF newlines + NULL, // teh whole thing, not just a section + 0, // no flags yet ... maybe new ones for Path: etc ? + NULL, + NULL, + &fetched_message_id // extract the message ID from the message as we go... + ); + StrBuf *msgtext = CC->redirect_buffer; + CC->redirect_buffer = NULL; + + FreeStrBuf(&msgtext); + return(fetched_message_id); +} + + +// +// The LAST and NEXT commands are so similar that they are handled by a single function. +// +void nntp_last_next(const char *cmd) { + if (CtdlAccessCheck(ac_logged_in_or_guest)) return; + + citnntp *nntpstate = (citnntp *) CC->session_specific_data; + char which_command[16]; + int acmd = 0; + + // We're going to store one of these values in the variable 'acmd' so that + // we can quickly check later which version of the output we want. + enum { + NNTP_LAST, + NNTP_NEXT + }; + + extract_token(which_command, cmd, 0, ' ', sizeof which_command); + + if (!strcasecmp(which_command, "last")) { + acmd = NNTP_LAST; + } + else if (!strcasecmp(which_command, "next")) { + acmd = NNTP_NEXT; + } + else { + cprintf("500 I'm afraid I can't do that.\r\n"); + return; + } + + // ok, here we go ... fetch the msglist so we can figure out our place in the universe + struct nntp_msglist nm; + int i = 0; + long selected_msgnum = 0; + char *message_id = NULL; + + nm = nntp_fetch_msglist(&CC->room); + if ((nm.num_msgs < 0) || (nm.msgnums == NULL)) { + cprintf("500 something bad happened\r\n"); + return; + } + + if ( (acmd == NNTP_LAST) && (nm.num_msgs == 0) ) { + cprintf("422 no previous article in this group\r\n"); // nothing here + } + + else if ( (acmd == NNTP_LAST) && (nntpstate->current_article_number <= nm.msgnums[0]) ) { + cprintf("422 no previous article in this group\r\n"); // already at the beginning + } + + else if (acmd == NNTP_LAST) { + for (i=0; ((i= nntpstate->current_article_number) && (i > 0) ) { + selected_msgnum = nm.msgnums[i-1]; + } + } + if (selected_msgnum > 0) { + nntpstate->current_article_number = selected_msgnum; + message_id = message_id_from_msgnum(nntpstate->current_article_number); + cprintf("223 %ld <%s>\r\n", nntpstate->current_article_number, message_id); + if (message_id) free(message_id); + } + else { + cprintf("422 no previous article in this group\r\n"); + } + } + + else if ( (acmd == NNTP_NEXT) && (nm.num_msgs == 0) ) { + cprintf("421 no next article in this group\r\n"); // nothing here + } + + else if ( (acmd == NNTP_NEXT) && (nntpstate->current_article_number >= nm.msgnums[nm.num_msgs-1]) ) { + cprintf("421 no next article in this group\r\n"); // already at the end + } + + else if (acmd == NNTP_NEXT) { + for (i=0; ((i nntpstate->current_article_number) { + selected_msgnum = nm.msgnums[i]; + } + } + if (selected_msgnum > 0) { + nntpstate->current_article_number = selected_msgnum; + message_id = message_id_from_msgnum(nntpstate->current_article_number); + cprintf("223 %ld <%s>\r\n", nntpstate->current_article_number, message_id); + if (message_id) free(message_id); + } + else { + cprintf("421 no next article in this group\r\n"); + } + } + + // should never get here + else { + cprintf("500 internal error\r\n"); + } + + if (nm.msgnums != NULL) { + free(nm.msgnums); + } + +} + + +// +// back end for the XOVER command , called for each message number +// +void nntp_xover_backend(long msgnum, void *userdata) { + + struct listgroup_range *lr = (struct listgroup_range *)userdata; + + // check range if supplied + if (msgnum < lr->lo) return; + if ((lr->hi != 0) && (msgnum > lr->hi)) return; + + struct CtdlMessage *msg = CtdlFetchMessage(msgnum, 0, 1); + if (msg == NULL) { + return; + } + + // Teh RFC says we need: + // ------------------------- + // Subject header content + // From header content + // Date header content + // Message-ID header content + // References header content + // :bytes metadata item + // :lines metadata item + + time_t msgtime = atol(msg->cm_fields[eTimestamp]); + char strtimebuf[26]; + ctime_r(&msgtime, strtimebuf); + + // here we go -- print the line o'data + cprintf("%ld\t%s\t%s <%s>\t%s\t%s\t%s\t100\t10\r\n", + msgnum, + msg->cm_fields[eMsgSubject], + msg->cm_fields[eAuthor], + msg->cm_fields[erFc822Addr], + strtimebuf, + msg->cm_fields[emessageId], + msg->cm_fields[eWeferences] + ); + + CM_Free(msg); +} + + +// +// +// XOVER is used by some clients, even if we don't offer it +// +void nntp_xover(const char *cmd) { + if (CtdlAccessCheck(ac_logged_in_or_guest)) return; + + citnntp *nntpstate = (citnntp *) CC->session_specific_data; + char range[256]; + struct listgroup_range lr; + + extract_token(range, cmd, 1, ' ', sizeof range); + lr.lo = atol(range); + if (lr.lo <= 0) { + lr.lo = nntpstate->current_article_number; + lr.hi = nntpstate->current_article_number; + } + else { + char *dash = strchr(range, '-'); + if (dash != NULL) { + ++dash; + lr.hi = atol(dash); + if (lr.hi == 0) { + lr.hi = LONG_MAX; + } + if (lr.hi < lr.lo) { + lr.hi = lr.lo; + } + } + else { + lr.hi = lr.lo; + } + } + + cprintf("224 Overview information follows\r\n"); + CtdlForEachMessage(MSGS_ALL, 0L, NULL, NULL, NULL, nntp_xover_backend, &lr); + cprintf(".\r\n"); +} + + // // Main command loop for NNTP server sessions. // @@ -833,6 +1079,10 @@ void nntp_command_loop(void) nntp_help(); } + else if (!strcasecmp(cmdname, "date")) { + nntp_date(); + } + else if (!strcasecmp(cmdname, "capabilities")) { nntp_capabilities(); } @@ -870,10 +1120,27 @@ void nntp_command_loop(void) || (!strcasecmp(cmdname, "head")) || (!strcasecmp(cmdname, "body")) || (!strcasecmp(cmdname, "stat")) - ) { + ) + { nntp_article(ChrPtr(Cmd)); } + else if ( + (!strcasecmp(cmdname, "last")) + || (!strcasecmp(cmdname, "next")) + ) + { + nntp_last_next(ChrPtr(Cmd)); + } + + else if ( + (!strcasecmp(cmdname, "xover")) + || (!strcasecmp(cmdname, "over")) + ) + { + nntp_xover(ChrPtr(Cmd)); + } + else { cprintf("500 I'm afraid I can't do that.\r\n"); } @@ -897,15 +1164,21 @@ void nntp_cleanup_function(void) if (CC->h_command_function != nntp_command_loop) return; syslog(LOG_DEBUG, "Performing NNTP cleanup hook\n"); + citnntp *nntpstate = (citnntp *) CC->session_specific_data; + if (nntpstate != NULL) { + free(nntpstate); + nntpstate = NULL; + } } const char *CitadelServiceNNTP="NNTP"; +const char *CitadelServiceNNTPS="NNTPS"; CTDL_MODULE_INIT(nntp) { if (!threading) { - CtdlRegisterServiceHook(119, // FIXME config.c_nntp_port, + CtdlRegisterServiceHook(CtdlGetConfigInt("c_nntp_port"), NULL, nntp_greeting, nntp_command_loop, @@ -913,12 +1186,12 @@ CTDL_MODULE_INIT(nntp) CitadelServiceNNTP); #ifdef HAVE_OPENSSL - CtdlRegisterServiceHook(563, // FIXME config.c_nntps_port, + CtdlRegisterServiceHook(CtdlGetConfigInt("c_nntps_port"), NULL, nntps_greeting, nntp_command_loop, NULL, - CitadelServiceNNTP); + CitadelServiceNNTPS); #endif CtdlRegisterCleanupHook(nntp_cleanup);