NEWSD -- ERCO'S SIMPLE NNTP NEWS SERVER --------------------------------------- *** THIS FILE FOR VIEWING PURPOSES ONLY *** The following is a concatenation of all the source files for the Newsd NNTP News Server version 1.00. For actual compilable source, please download the gzipped tar file, which has all the source files broken out: http://3dsite.com/people/erco/unixtools/newsd.1.10.tar.gz Please report bugs to Greg Ercolano, erco@3dsite.com CONTENTS: everything.H -- misc headers Article.H -- article class header Group.H -- newsgroup class header Article.C -- article class methods Group.C -- newsgroup methods newsd.C -- news server daemon main program README -- compile info etc LICENSE -- GPL license --------------------------------------------------------------------------------- __ __ __ ___ __ |_ \ / |_ |__| |__| | |__| | |\ | | _ |__| |__ \/ |__ | \ | | | | | | \| |__| o | | --------------------------------------------------------------------------------- #ifndef EVERYTHING_H #define EVERYTHING_H // // everything.H // // erco@3dsite.com 1.00 01/02/03 // Copyright Greg Ercolano 2003. All rights reserved. // #include #include #include #include #include #include // waitpid #include // flock() #include #include #include #include #include #include #include #include #include #include #include #define LINE_LEN 4096 #define GROUP_MAX 1024 #define FIELD_MAX 1024 #define SPOOL_DIR "/var/spool/news" #define SENDMAIL "/usr/sbin/sendmail" extern int G_debug; #ifdef BSD typedef unsigned long ulong; typedef unsigned int uint; #define SIGCLD SIGCHLD #endif /*BSD*/ #endif /*EVERYTHING_H*/ --------------------------------------------------------------------------------- __ __ ___ __ __ |__| |__| | | | | |_ |__| | | | \ | | |__ |__ |__ o | | --------------------------------------------------------------------------------- #ifndef ARTICLE_H #define ARTICLE_H #include "everything.H" // // Article.H -- Manage newsgroup articles // 1.00 erco@3dsite.com 12/16/02 // Copyright Greg Ercolano 2003. All rights reserved. // class Article { char group[GROUP_MAX]; // group name char filename[LINE_LEN]; // full path to the article ulong number; // article number int valid; // article is valid (0=invalid) // commonly accessed header info char from[FIELD_MAX]; // From: field char date[FIELD_MAX]; // Date: field char messageid[FIELD_MAX]; // Message-ID: field char subject[FIELD_MAX]; // Subject: field char references[FIELD_MAX]; // References: field char xref[FIELD_MAX]; // Xref: field int lines; // Lines: field string errmsg; // error message void _Copy(const Article& o) { strcpy(group, o.group); strcpy(filename, o.filename); number = o.number; valid = o.valid; strcpy(from, o.from); strcpy(date, o.date); strcpy(messageid, o.messageid); strcpy(subject, o.subject); strcpy(references, o.references); strcpy(xref, o.xref); lines = o.lines; errmsg = o.errmsg; } public: Article() { group[0] = filename[0] = 0; from[0] = messageid[0] = 0; date[0] = references[0] = 0; subject[0] = xref[0] = 0; lines = 0; number = 0; valid = 0; } ~Article() { } Article(const Article& o) { _Copy(o); } Article& operator=(const Article& o) { _Copy(o); return(*this); } int IsValid() { return(valid); } const char *Group() { return(group); } const char *Filename() { return(filename); } const char *From() { return(from); } const char *Date() { return(date); } const char *MessageID() { return(messageid); } const char *Subject() { return(subject); } const char *Xref() { return(xref); } const char *References() { return(references); } int Lines() { return(lines); } ulong Number() { return(number); } const char *Errmsg() { return(errmsg.c_str()); } int Load(const char *group, ulong num); // load info for group/article int Load(ulong num); // load info for article int SendArticle(int fd, int head=1, int body=1); // send article to remote via fd int SendHead(int fd); // send article head only int SendBody(int fd); // send article body only void Overview(string &reply, const char *overview[] ); }; #endif /*ARTICLE_H*/ --------------------------------------------------------------------------------- __ __ __ __ | _ |__| | | | | |__| |__| |__| | \ |__| |__| | o | | --------------------------------------------------------------------------------- #ifndef GROUP_H #define GROUP_H #include "everything.H" // // Group.H -- Manage newsgroup groups // 1.00 erco@3dsite.com 12/16/02 // Copyright Greg Ercolano 2003. All rights reserved. // class Group { // ".info" FILE DATA ulong start, end, total; // start/end/total articles // ".config" FILE DATA string desc; // group description string creator; // group creator email string name; // group name string ccpost; // email address to cc all postings to int postok, // allow posting to this group postlimit; // posting line limit time_t ctime; // creation time int valid; // is group valid? 0=invalid string errmsg; // error message // GENERAL LOCKING // Write lock whenever posting or saving info file. // Read lock before accessing info file. // int WriteLock(); int ReadLock(); void Unlock(int fd); int WriteString(FILE *fp, const char *buf); int BuildInfo(int dolock = 1); int LoadInfo(int dolock = 1); int SaveInfo(int dolock = 1); int LoadConfig(int dolock = 1); int SaveConfig(); void ReorderHeader(const char*overview[], vector& head); void _Copy(const Group&o) { start = o.start; end = o.end; total = o.total; desc = o.desc; creator = o.creator; name = o.name; ccpost = o.ccpost; postok = o.postok; postlimit = o.postlimit; ctime = o.ctime; valid = o.valid; } public: Group() { start = end = total = 0; desc = ""; creator = ""; name = ""; ccpost = "-"; postok = 0; postlimit = 0; ctime = 0; valid = 0; } ~Group() { } Group(const Group&o) { _Copy(o); } Group& operator=(const Group&o) { _Copy(o); return(*this); } // ACCESSORS ulong Start() { return(start); } ulong End() { return(end); } ulong Total() { return(total); } int PostOK() { return(postok); } int PostLimit() { return(postlimit); } long Ctime() { return(ctime); } int IsValid() { return(valid); } int IsCCPost() { return(ccpost == "-" ? 0 : 1); } const char *Description() { return(desc.c_str()); } const char *Creator() { return(creator.c_str()); } const char *Name() { return(name.c_str()); } const char *CCPost() { return(ccpost.c_str()); } const char *Errmsg() { return(errmsg.c_str()); } void Start(ulong val) { start = val; } void End(ulong val) { end = val; } void Total(ulong val) { total = val; } void Name(const char*val) { name = val; } int Load(const char *group, int dolock = 1); int WriteInfo(int fd); int FindArticleByMessageID(const char *find_messageid, ulong &articlenum); int Post(const char*overview[], vector &head, vector &body); const char *Dirname(); int NewGroup(); const char *NewsHostname() { return("news.3dsite.com"); // I YAM HERE } const char *RemoteHostIPAddrStr() { return("???.???.???.???"); // I YAM HERE } }; #endif /*GROUP_H*/ --------------------------------------------------------------------------------- __ __ ___ __ __ __ |__| |__| | | | | |_ | | | | \ | | |__ |__ |__ o |__ --------------------------------------------------------------------------------- #include "everything.H" #include "Article.H" // // Article.C -- Manage newsgroup articles // 1.00 erco@3dsite.com 12/16/02 // Copyright Greg Ercolano 2003. All rights reserved. // // RETURN ASCII VERSION OF AN UNSIGNED LONG static const char *ultoa(ulong num) { static char s[80]; sprintf(s, "%lu", num); return(s); } int Article::Load(const char *group, ulong num) { number = num; // ZERO OUT FIELDS valid = 0; // assume invalid until successful filename[0] = 0; messageid[0] = 0; subject[0] = 0; from[0] = 0; date[0] = 0; references[0] = 0; xref[0] = 0; lines = 0; if ( strlen(group) >= GROUP_MAX ) { errmsg = "Group name too long"; return(-1); } if ( this->group != group ) strcpy(this->group, group); sprintf(filename, "%s/%s/%lu", SPOOL_DIR, group, number); { char *ss; while ( ( ss = strchr(filename, '.') ) != NULL ) { *ss = '/'; } } FILE *fp = fopen(filename, "r"); if ( fp == NULL ) { errmsg = "article "; errmsg.append(ultoa(number)); errmsg.append(" no longer exists: '"); errmsg.append(filename); errmsg.append("': "); errmsg.append(strerror(errno)); return(-1); } // LOAD KEY/VALUE PAIRS char s[LINE_LEN]; while ( fgets(s, sizeof(s)-1, fp) != NULL ) { // END OF HEADER? if ( s[0] == '\n' ) { break; } char *key = s, *val = 0; for ( char *ss = s; *ss; ss++ ) { if ( *ss == '\n' ) { *ss = 0; break; } else if ( *ss == ' ' && val == 0 ) { *ss = 0; val = ++ss; } } // fprintf(stderr, "KEY='%s' VAL='%s'\n", key, val); if ( val ) { // TRUNCATE IF TOO LONG if ( strlen(val) >= FIELD_MAX ) { val[FIELD_MAX-1] = 0; } // HEADER NAMES ARE CASE INSENSITIVE: INTERNET DRAFT (Son of RFC1036) if ( strcasecmp(key, "Subject:" ) == 0 ) { strcpy(subject, val); } else if ( strcasecmp(key, "From:" ) == 0 ) { strcpy(from, val); } else if ( strcasecmp(key, "Date:" ) == 0 ) { strcpy(date, val); } else if ( strcasecmp(key, "Xref:" ) == 0 ) { strcpy(xref, val); } else if ( strcasecmp(key, "Message-ID:") == 0 ) { strcpy(messageid, val); } else if ( strcasecmp(key, "References:") == 0 ) { strcpy(references, val); } else if ( strcasecmp(key, "Lines:") == 0 ) { lines = atoi(val); } } } fclose(fp); if ( messageid == "" ) { errmsg = "No 'Message-ID' field"; return(-1); } if ( from == "" ) { errmsg = "No 'From' field"; return(-1); } valid = 1; return(0); } int Article::Load(ulong num) { if ( group[0] == 0 ) { errmsg = "No group selected"; return(-1); } return(Load(group, num)); } // SEND ARTICLE TO REMOTE VIA FD // head: 1=send header // body: 1=send body // If both head and body are 1, separator (blank line) // is also sent. // int Article::SendArticle(int fd, int head, int body) { FILE *fp = fopen(filename, "r"); if ( fp == NULL ) { errmsg = "article "; errmsg.append(ultoa(number)); errmsg.append(" no longer exists: "); errmsg.append(strerror(errno)); return(-1); } char s[LINE_LEN]; enum Mode { MODE_HEAD, MODE_SEP, MODE_BODY }; Mode mode = MODE_HEAD; while ( fgets(s, LINE_LEN-2, fp) ) { if ( mode == MODE_SEP ) { // MOVED OFF SEPARATOR INTO BODY mode = MODE_BODY; } else if ( s[0] == '\n' && mode == MODE_HEAD ) { // END OF HEADER, AND NOT SENDING BODY? DONE mode = MODE_SEP; // end of header if ( body == 0 ) { break; } // not sending body? done } // LINE TOO LONG? -- TRUNCATE s[LINE_LEN-4] = '\n'; s[LINE_LEN-3] = 0; char *ss = strchr(s, '\n'); if ( ss ) { // TERMINATE WITH CRLF *ss = '\r'; ss++; *ss = '\n'; ss++; *ss = 0; } if ( ( mode == MODE_HEAD && head ) || ( mode == MODE_BODY && body ) || ( mode == MODE_SEP && head && body ) ) { write(fd, s, strlen(s)); if ( G_debug ) { cerr << "SEND: " << s; } } } fclose(fp); return(0); } int Article::SendHead(int fd) { return(SendArticle(fd, 1, 0)); } int Article::SendBody(int fd) { return(SendArticle(fd, 0, 1)); } void Article::Overview(string &reply, const char *overview[] ) { reply = ultoa(Number()); for ( int r=0; overview[r]; r++ ) { // HEADER NAMES ARE CASE INSENSITIVE: INTERNET DRAFT (Son of RFC1036) if ( strcasecmp(overview[r], "Subject:" ) == 0 ) { reply += "\t"; reply += subject; } else if ( strcasecmp(overview[r], "From:" ) == 0 ) { reply += "\t"; reply += from; } else if ( strcasecmp(overview[r], "Date:" ) == 0 ) { reply += "\t"; reply += date; } else if ( strcasecmp(overview[r], "Message-ID:") == 0 ) { reply += "\t"; reply += messageid; } else if ( strcasecmp(overview[r], "References:") == 0 ) { reply += "\t"; reply += references; } else if ( strcasecmp(overview[r], "Lines:" ) == 0 ) { reply += "\t"; if ( lines > 0 ) { reply += ultoa(lines); } } else if ( strcmp(overview[r], "Bytes:" ) == 0 ) { // WE DONT KEEP TRACK OF BYTES -- LEAVE FIELD EMPTY reply += "\t"; } else if ( strcmp(overview[r], "Xref:full") == 0 ) { reply += "\t"; if ( xref[0] != 0 ) { reply += "Xref: "; reply += xref; } } } reply += '\0'; if ( G_debug ) { int count = -1; cerr << endl << "----OVERVIEW: FIELD #" << count <<" (ARTICLE#): '"; for ( const char *ss = reply.c_str(); *ss; ss++ ) { if ( *ss == '\t' ) { count++; cerr << "'" << endl << " FIELD #" << count <<" (" << overview[count] << "): '"; } else { cerr << *ss; } } cerr << "'" << endl << "---" << endl; } } --------------------------------------------------------------------------------- __ __ __ __ __ | _ |__| | | | | |__| | |__| | \ |__| |__| | o |__ --------------------------------------------------------------------------------- #include "everything.H" #include "Group.H" // // Group.C -- Manage newsgroup groups // 1.00 erco@3dsite.com 12/16/02 // Copyright Greg Ercolano 2003. All rights reserved. // // RETURN ASCII VERSION OF AN UNSIGNED LONG static const char *ultoa(ulong num) { static char s[80]; sprintf(s, "%lu", num); return(s); } const char *Group::Dirname() { static string dirname; // CONVERT GROUP NAME TO A DIRECTORY NAME // eg. rush.general -> /var/spool/news/rush/general // dirname = SPOOL_DIR; dirname += "/"; dirname += name; for ( int t=0; dirname[t]; t++ ) if ( dirname[t] == '.' ) dirname[t] = '/'; return(dirname.c_str()); } // SAVE OUT GROUP'S ".info" FILE int Group::SaveInfo(int dolock) { string path = Dirname(); path += "/.info"; // WRITE OUT INFO FILE int ilock = -1; if ( dolock ) { ilock = WriteLock(); } { FILE *fp = fopen(path.c_str(), "w"); if ( fp == NULL ) { errmsg = path; errmsg += ": "; errmsg += strerror(errno); if ( G_debug ) cerr << "Group::SaveInfo(): " << errmsg << endl; if ( dolock ) Unlock(ilock); return(-1); } WriteString(fp, "start "); WriteString(fp, ultoa(start)); WriteString(fp, "\n"); WriteString(fp, "end "); WriteString(fp, ultoa(end)); WriteString(fp, "\n"); WriteString(fp, "total "); WriteString(fp, ultoa(total)); WriteString(fp, "\n"); fflush(fp); fsync(fileno(fp)); fclose(fp); } if ( dolock ) { Unlock(ilock); } return(0); } // BUILD GROUP INFO FROM ACTUAL ARTICLES ON DISK // Do this if info file doesn't already exist // int Group::BuildInfo(int dolock) { // I YAM HERE // // *** FAKE IT FOR NOW *** // What it should really do: // 1) Walk the file system to determine start/end values. // 2) Use group dir's owner to determine creator // 3) Make description 'none' // int wlock = -1; int ret = 0; if ( dolock ) { wlock = WriteLock(); } { start = 1; end = 1; total = 0; ret = SaveInfo(0); } if ( dolock ) { Unlock(wlock); } return(ret); } // LOAD GROUP INFO // If none exists, create a .info file. // int Group::LoadInfo(int dolock) { // LOAD INFO FILE string path = Dirname(); path += "/.info"; FILE *fp = fopen(path.c_str(), "r"); if ( fp == NULL ) { // NO INFO FILE? BUILD ONE if ( errno == ENOENT ) { return(BuildInfo(dolock)); } errmsg = path; errmsg += ": "; errmsg += strerror(errno); if ( G_debug ) cerr << "Group::LoadInfo(): " << errmsg << endl; return(-1); } int ilock = -1; if ( dolock ) { ilock = ReadLock(); } { char buf[LINE_LEN]; char foo[256]; while ( fgets(buf, LINE_LEN-1, fp) ) { // REMOVE TRAILING \n if ( strlen(buf) > 1 ) buf[strlen(buf)-1] = 0; // SKIP BLANK LINES AND COMMENTS if ( buf[0] == '#' || buf[0] == '\n' ) continue; if ( strncmp(buf, "description ", strlen("description ")) == 0 ) { desc = buf + strlen("description "); continue; } if ( sscanf(buf, "creator %255s", foo) == 1 ) { creator = foo; continue; } if ( sscanf(buf, "start %lu", &start) == 1 ) { continue; } if ( sscanf(buf, "end %lu", &end ) == 1 ) { continue; } if ( sscanf(buf, "total %lu", &total) == 1 ) { continue; } if ( sscanf(buf, "postok %d", &postok) == 1 ) { continue; } if ( sscanf(buf, "postlimit %d", &postlimit) == 1 ) { continue; } if ( sscanf(buf, "ccpost %255s", foo) == 1 ) { ccpost = foo; continue; } } fclose(fp); } if ( dolock ) { Unlock(ilock); } return(0); } // LOAD GROUP'S ".config" FILE int Group::LoadConfig(int dolock) { // LOAD CONFIG FILE string path = Dirname(); path += "/.config"; FILE *fp = fopen(path.c_str(), "r"); if ( fp == NULL ) { errmsg = path; errmsg += ": "; errmsg += strerror(errno); if ( G_debug ) cerr << "Group::LoadConfig(): " << errmsg << endl; return(-1); } int ilock = -1; if ( dolock ) { ilock = ReadLock(); } { char buf[LINE_LEN]; char foo[256]; while ( fgets(buf, LINE_LEN-1, fp) ) { // REMOVE TRAILING \n if ( strlen(buf) > 1 ) buf[strlen(buf)-1] = 0; // SKIP BLANK LINES AND COMMENTS if ( buf[0] == '#' || buf[0] == '\n' ) continue; if ( strncmp(buf, "description ", strlen("description ")) == 0 ) { desc = buf + strlen("description "); continue; } if ( sscanf(buf, "creator %255s", foo) == 1 ) { creator = foo; continue; } if ( sscanf(buf, "postok %d", &postok) == 1 ) { continue; } if ( sscanf(buf, "postlimit %d", &postlimit) == 1 ) { continue; } if ( sscanf(buf, "ccpost %255s", foo) == 1 ) { ccpost = foo; continue; } } fclose(fp); } if ( dolock ) { Unlock(ilock); } return(0); } // SAVE OUT GROUP'S ".config" FILE // This should only be done by manually invoking 'newsd -newgroup'. // int Group::SaveConfig() { string path = Dirname(); path += "/.config"; // WRITE OUT CONFIG FILE int ilock = WriteLock(); { FILE *fp = fopen(path.c_str(), "w"); if ( fp == NULL ) { errmsg = path; errmsg += ": "; errmsg += strerror(errno); Unlock(ilock); return(-1); } WriteString(fp, "description "); WriteString(fp, desc.c_str()); WriteString(fp, "\n"); WriteString(fp, "creator "); WriteString(fp, creator.c_str()); WriteString(fp, "\n"); WriteString(fp, "postok "); WriteString(fp, ultoa((ulong)postok)); WriteString(fp, "\n"); WriteString(fp, "postlimit "); WriteString(fp, ultoa((ulong)postlimit)); WriteString(fp, "\n"); WriteString(fp, "ccpost "); WriteString(fp, ccpost.c_str()); WriteString(fp, "\n"); fflush(fp); fsync(fileno(fp)); fclose(fp); } Unlock(ilock); return(0); } // LOAD INFO FOR GROUP // Set dolock=0 if lock has already been made // to prevent deadlocks. // int Group::Load(const char *group_name, int dolock) { valid = 0; // assume failed until success if ( strlen(group_name) >= GROUP_MAX ) { errmsg = "Group name too long"; return(-1); } struct stat sbuf; if ( stat(Dirname(), &sbuf) < 0 ) { errmsg = "invalid group name '"; errmsg += group_name; errmsg += "': "; errmsg += strerror(errno); if ( G_debug ) cerr << "Group::Load(): " << errmsg << endl; return(-1); } // GET CREATION TIME FROM DIR'S DATESTAMP ctime = sbuf.st_ctime; // LOAD INFO FILE // Builds one if it doesn't exist // name = group_name; if ( LoadInfo(dolock) < 0 ) { return(-1); } // LOAD CONFIG FILE if ( LoadConfig(dolock) < 0 ) { return(-1); } // GROUP IS NOW VALID valid = 1; return(0); } // WRITE NULL TERMINATED STRING TO FD int Group::WriteString(FILE *fp, const char *buf) { size_t len = strlen(buf); if ( fwrite(buf, 1, len, fp) != len) { errmsg = "write error"; if ( G_debug ) cerr << "Group::WriteString(): " << errmsg << endl; return(-1); } return(0); } // FIND ARTICLE NUMBER GIVEN A MESSAGEID // int Group::FindArticleByMessageID(const char *find_messageid, ulong &articlenum) int Group::FindArticleByMessageID(const char *, ulong &) { // TBD errmsg = "Find article by Message-ID not yet implemented"; if ( G_debug ) cerr << "Group::FindArticleByMessageID(): " << errmsg << endl; return(-1); } // READ LOCK int Group::ReadLock() { string lockpath = Dirname(); lockpath += "/.lock"; int fd = open(lockpath.c_str(), O_CREAT|O_WRONLY, 0644); if ( fd < 0 ) { errmsg = lockpath; errmsg += ": "; errmsg += strerror(errno); if ( G_debug ) cerr << "Group::ReadLock(): " << errmsg << endl; return(-1); } if ( flock(fd, LOCK_SH) < 0 ) { errmsg = "flock("; errmsg += lockpath; errmsg += ", SHARED): "; errmsg += strerror(errno); if ( G_debug ) cerr << "Group::ReadLock(): " << errmsg << endl; return(-1); } return(fd); } // WRITE LOCK int Group::WriteLock() { string lockpath = Dirname(); lockpath += "/.lock"; int fd = open(lockpath.c_str(), O_CREAT|O_WRONLY, 0644); if ( fd < 0 ) { errmsg = lockpath; errmsg += ": "; errmsg += strerror(errno); if ( G_debug ) cerr << "Group::WriteLock(): " << errmsg << endl; return(-1); } if ( flock(fd, LOCK_EX) < 0 ) { errmsg = "flock("; errmsg += lockpath; errmsg += ", EXCLUSIVE): "; errmsg += strerror(errno); if ( G_debug ) cerr << "Group::WriteLock(): " << errmsg << endl; return(-1); } return(fd); } // RELEASE POSTING LOCK void Group::Unlock(int fd) { if ( fd > 0 ) { flock(fd, LOCK_UN); close(fd); } } // REORDER AN ARTICLE'S HEADER // Why is this here? Because POST is a group method, // so we need it here. // void Group::ReorderHeader(const char*overview[], vector& head) { // REFORMAT HEADER IN OVERVIEW FORMAT vector newhead; // SORT OVERVIEW HEADERS TO TOP for ( int t=0; overview[t]; t++ ) { for ( uint r=0; rHHMM") // eg: Sun, 27 Mar 83 20:39:37 -0800 // Sun, 27 Mar 83 20:39:37 +0200 // static char s[80]; char gmtoff_sign = (tm->tm_gmtoff<0) ? '-' : '+'; long gmtoff_abs = (tm->tm_gmtoff<0) ? -(tm->tm_gmtoff) : (tm->tm_gmtoff); int gmtoff_hour = gmtoff_abs / 3600; // secs -> hours int gmtoff_min = (gmtoff_abs / 60) % 60; // secs -> mins sprintf(s, "%.3s, %02d %.3s %d %02d:%02d:%02d %c%02d%02d", (const char*)wday[tm->tm_wday], (int)tm->tm_mday, (const char*)mon[tm->tm_mon], (int)tm->tm_year + 1900, // 4 digit date instead of 2 digit (mozilla) (int)tm->tm_hour, (int)tm->tm_min, (int)tm->tm_sec, (char)gmtoff_sign, (int)gmtoff_hour, (int)gmtoff_min); return(s); } // POST ARTICLE int Group::Post(const char*overview[], vector &head, vector &body) { // DETERMINE GROUP BEING POSTED TO int found = 0; char postgroup[80]; for ( uint t=0; !found && t", (ulong)msgnum, (const char*)postgroup, (const char*)NewsHostname()); head.push_back(misc); sprintf(misc, "Lines: %u", body.size()); head.push_back(misc); ReorderHeader(overview, head); // WRITE HEADER for ( uint t=0; t= 'a' && groupname[t] <= 'z' ) || // alphalower ( groupname[t] >= 'A' && groupname[t] <= 'Z' ) || // alphaupper ( groupname[t] >= '0' && groupname[t] <= '9' ) || // numeric groupname[t] == '.' ) // dot { continue; } else { errmsg = "illegal chars in groupname"; return(-1); } string dirname; dirname = SPOOL_DIR; dirname.append("/"); dirname.append(groupname); struct stat sbuf; if ( stat(dirname.c_str(), &sbuf) == -1 ) { errmsg = "group does not exist"; return(-1); } return(0); } // FIND GROUP, UPDATE START/END/TOTAL INFO int Server::NewGroup(const char *the_group) { if ( ValidGroup(the_group) < 0 ) { return(-1); } if ( group.Load(the_group) < 0 ) { errmsg = group.Errmsg(); return(-1); } return(0); } // REFORMAT HEADER OF ARTICLE // Add Lines:, Path:, etc. // int ReformatArticle(string &msg, vector&head, vector&body) { // REQUIRED // -------- // From: // Date: Wdy, DD Mon YY HH:MM:SS TIMEZONE (or: Wdy Mon DD HH:MM:SS YYYY) // Newsgroups: news.group[,news.group] -- ignore invalid groups // Subject: xx yy zz // Message-ID: // Path: ,, // // OPTIONAL // -------- // Control: // Distribution: // Organization: // References: // Lines: <#lines in body> // Xref: [newsgroup:article#] // // CONTROL MESSAGES // ---------------- // 1) Subject: cmsg // // 2) Control: // // 3) Newsgroups: all.all.ctl // Subject: // char *ss = (char*)msg.c_str(), *start = ss; int headflag = 1; // PARSE OUT HEADERS AND BODY while ( 1 ) { if ( *ss == 0 ) { break; // parsed entire msg } if ( *ss == '\r' ) // end of line? { *ss = 0; // terminate if ( strcmp(start, ".") != 0 ) { if ( headflag ) { head.push_back(start); } // append else { body.push_back(start); } } *ss = '\r'; ++ss; if ( *ss == '\n' ) { ++ss; } // skip over CRLF start = ss; // save next line start // CHECK FOR BLANK LINE ENDING HEADER if ( headflag ) { if ( *ss == '\r' ) // next line blank? { headflag = 0; // disable header mode ss++; // skip over \r if ( *ss == '\n' ) { ss++; } // and \n if any start = ss; // start of body } } continue; } ++ss; } return(0); } void AllGroups(vector& groupnames) { FILE *fp; if ( ( fp = popen("( cd " SPOOL_DIR "; find . -type d)", "r") ) == NULL ) { return; } char groupname[LINE_LEN]; while ( fgets(groupname, sizeof(groupname)-1, fp) ) { if ( strlen(groupname) > 0 ) { groupname[strlen(groupname)-1] = 0; } if ( strlen(groupname) <= 2 ) { continue; } if ( groupname[0]=='.' && groupname[1]=='/' ) { strcpy(groupname, groupname+2); } // "./rush/general" -> "rush/general" // ONLY LIST GROUPS THAT HAVE .config FILES string configfile = SPOOL_DIR; configfile += "/"; configfile += groupname; configfile += "/.config"; struct stat buf; if ( stat(configfile.c_str(), &buf) < 0 ) { continue; } for ( char *ss = groupname; *ss; ss++ ) if ( *ss == '/' ) *ss = '.'; groupnames.push_back(groupname); } pclose(fp); } // HANDLE COMMANDS FROM REMOTE int Server::CommandLoop() { char s[LINE_LEN+1], cmd[LINE_LEN+1], arg1[LINE_LEN+1], arg2[LINE_LEN+1], reply[LINE_LEN]; Send("200 newsd news server ready - posting ok"); while ( 1 ) { int total = 0; char *ss = s; int found = 0; // READ A CRLF-TERMINATED (OR LF-TERMINATED) LINE for ( ; read(msgsock, ss, 1) == 1; ss++, total++ ) { if ( *ss == '\n' || total >= (LINE_LEN-2) ) { *ss = 0; found = 1; break; } if ( *ss == '\r' ) { read(msgsock, ss, 1); // skip \n *ss = 0; found = 1; break; } } if ( ! found ) { break; } fprintf(stderr, "GOT: %s\n", s); arg1[0] = arg2[0] = 0; if ( sscanf(s, "%s%s%s", cmd, arg1, arg2) < 1 ) { continue; } ISIT("CHECK") // TRANSPORT EXTENSION -- RFC 2980 { Send("400 not accepting articles - we are not a news feed"); continue; } ISIT("TAKETHIS") // TRANSPORT EXTENSION -- RFC 2980 { Send("400 not accepting articles - we are not a news feed"); continue; } ISIT("MODE") // TRANSPORT EXTENSION -- RFC 2980 { if ( strcasecmp(arg1, "stream") == 0 ) { Send("500 Streaming not implemented on this server"); continue; } // NEWS READER EXTENSION -- RFC 2980 if ( strcasecmp(arg1, "reader") == 0 ) { // Send("201 erco's newsd server ready (no posting)"); Send("200 erco's newsd server ready (posting ok)"); continue; } Send("500 What?"); // inn/nnrp/commands.c:CMDmode() - erco continue; } ISIT("LIST") { if ( strcasecmp(arg1, "EXTENSIONS") == 0 ) // INTERNET DRAFT (S.Barber) { Send("202 Extensions supported:\r\n" "LISTGROUP\r\n" "MODE\r\n" "XREPLIC\r\n" "XOVER\r\n" "."); continue; } if ( strcasecmp(arg1, "ACTIVE") == 0 || // NEWS READER EXTENSION -- RFC 2980 arg1[0] == 0 ) // RFC 977 { // "LIST ACTIVE rush.*" if ( strcasecmp(cmd, "LIST") == 0 && strcasecmp(arg1, "ACTIVE") == 0 && arg2[0] != 0 ) { Send("501 LIST ACTIVE : wildmats not supported"); // TBD continue; } // "LIST" or "LIST ACTIVE" Send("215 list of newsgroups follows"); vector groupnames; AllGroups(groupnames); for ( uint t=0; t groupnames; AllGroups(groupnames); for ( uint t=0; t groupnames; AllGroups(groupnames); for ( uint t=0; t group.End() ) { sarticle = group.End(); } if ( earticle < group.Start() ) { earticle = group.Start(); } if ( earticle > group.End() ) { earticle = group.End(); } if ( sarticle > earticle ) { sarticle = earticle; } Send("224 overview follows"); for ( ulong t=sarticle; t<=earticle; t++ ) { // LOAD EACH ARTICLE Article a; if ( a.Load(group.Name(), t) < 0 ) { cerr << " DEBUG: ERROR: " << a.Errmsg() << endl; continue; } string reply; a.Overview(reply, overview); Send(reply.c_str()); } Send("."); continue; } ISIT("GROUP") // RFC 977 { if ( arg1[0] == 0 ) { Send("501 syntax error; expected 'GROUP '"); continue; } Group restore = group; if ( group.Load(arg1) < 0 ) { sprintf(reply, "411 No such newsgroup: %s", (const char*)group.Errmsg()); Send(reply); group = restore; continue; } // UPDATE CURRENT ARTICLE article.Load(group.Name(), group.Start()); // 211 n f l s group selected // (n = estimated number of articles in group, // f = first article number in the group, // l = last article number in the group, // s = name of the group.) // sprintf(reply, "211 %lu %lu %lu %s group selected", (ulong)group.Total(), (ulong)group.Start(), (ulong)group.End(), (const char*)group.Name()); Send(reply); continue; } ISIT("HELP") // RFC 977 { Send("100 help text follows"); Send("CHECK\r\n" "TAKETHIS\r\n" "MODE\r\n" "LIST\r\n" "LISTGROUP\r\n" "XREPLIC\r\n" "XOVER\r\n" "GROUP\r\n" "HELP\r\n" "NEWGROUPS\r\n" "NEXT\r\n" "HEAD\r\n" "BODY\r\n" "ARTICLE\r\n" "STAT\r\n" "POST\r\n" "QUIT\r\n" "."); continue; } ISIT("NEWGROUPS") // RFC 977 { // NEWGROUPS [GMT] [] if ( strlen(arg1) != 6 || strlen(arg2) != 6 ) { Send("501 Bad or missing date/time arguments"); continue; } int year, mon, day, hour, min, sec; if ( sscanf(arg1, "%2d%2d%2d", &year, &mon, &day) != 3 || sscanf(arg2, "%2d%2d%2d", &hour, &min, &sec) != 3 ) { Send("501 Bad date/time argument"); continue; } // TBD // 1) TRANSLATE YY -> YYYY // 2) TRANSLATE TIMES INTO A time() VALUE // 3) COMPARE TIME TO GROUP'S CTIME // vector groupnames; AllGroups(groupnames); Send("231 list of new newsgroups follows"); for ( uint t=0; t checktime ) { Send(tgroup.Name()); } } Send("."); continue; ********/ } ISIT("NEWNEWS") // RFC 977 { Send("501 Command not implemented on server"); // TBD continue; } ISIT("NEXT") // RFC 977 { Article restore = article; if ( ! group.IsValid() ) { Send("412 no newsgroup selected"); continue; } if ( ! article.IsValid() ) { Send("420 no article has been selected"); continue; } ulong next = article.Number() + 1; if ( next < group.Start() || next > group.End() ) { Send("421 no next article in this group"); continue; } if ( article.Load(group.Name(), next) < 0 ) { sprintf(reply, "421 error retrieving article %lu: %s", (ulong)next, (const char*)article.Errmsg()); Send(reply); article = restore; continue; } sprintf(reply, "223 %lu %s article retrieved - request text separately", (ulong)next, (const char*)article.MessageID()); Send(reply); continue; } if ( strcasecmp(cmd, "HEAD") == 0 || // RFC 977 strcasecmp(cmd, "BODY") == 0 || // RFC 977 strcasecmp(cmd, "ARTICLE") == 0 || // RFC 977 strcasecmp(cmd, "STAT") == 0 ) // RFC 977 { Article restore = article; ulong the_article; char restoreflag = 0; if ( ! group.IsValid() ) { Send("412 Not currently in newsgroup"); continue; } if ( arg1[0] == '<' ) // "HEAD <123@foo.com>" { if ( group.FindArticleByMessageID(arg1, the_article) < 0 ) { Send("430 no such article found"); continue; } restoreflag = 1; // RFC 977: do not affect current article } else if ( isdigit(arg1[0]) ) // "HEAD 12" { if ( sscanf(arg1, "%lu", &the_article) != 1 ) { Send("501 bad article number"); continue; } restoreflag = 0; // RFC 977: affect current article if valid } else if ( arg1[0] == 0 ) // "HEAD" { the_article = article.Number(); restoreflag = 1; } else // all else is junk { Send("501 bad argument"); continue; } if ( article.Load(group.Name(), the_article) < 0 ) { sprintf(reply, "430 no such article: %s", (const char*)article.Errmsg()); Send(reply); continue; } // HANDLE VARIATIONS OF COMMAND if ( strcasecmp(cmd, "ARTICLE") == 0 ) { sprintf(reply, "220 %lu %s article retrieved - head and body follow", (ulong)the_article, (const char*)article.MessageID()); Send(reply); article.SendArticle(msgsock); Send("."); } else if ( strcasecmp(cmd, "HEAD") == 0 ) { sprintf(reply, "221 %lu %s article retrieved - head follows", (ulong)the_article, (const char*)article.MessageID()); Send(reply); article.SendHead(msgsock); Send("."); } else if ( strcasecmp(cmd, "BODY") == 0 ) { sprintf(reply, "222 %lu %s article retrieved - body follows", (ulong)the_article, (const char*)article.MessageID()); Send(reply); article.SendBody(msgsock); Send("."); } else if ( strcasecmp(cmd, "STAT") == 0 ) { sprintf(reply, "223 %lu %s article retrieved - request text separately", (ulong)the_article, (const char*)article.MessageID()); Send(reply); } if ( restoreflag ) { article = restore; } continue; } ISIT("POST") // RFC 977 { Send("340 Continue posting; Period on a line by itself to end"); // KEEP TRACK OF POSTING LENGTH // If too long, stop loading posting. // int linechars = 0, linecount = 0, toolong = 0; // COLLECT POSTING FROM CLIENT int eom = 0; char c; string msg; while (read(msgsock, &c, 1) == 1 ) { // KEEP TRACK OF #LINES // Lines longer than 80 chars count as multiple lines. // If posting too long, stop accumulating message in ram, // but keep reading until they've sent the terminating "." // ++linechars; if ( linechars > 80 || c == '\n' ) { linechars = 0; linecount++; } if ( group.PostLimit() > 0 && linecount > group.PostLimit() ) { toolong = 1; continue; } msg += c; // PARSE FOR END OF MESSAGE // Cute little state machine to maintain end of message // parse over buffer boundaries. The states: // // 0 - not yet at eom // 1 - \n before the dot // 2 - the dot // 3 - \r (or \n) after the dot // if ( eom == 0 && ( c == '\n' ) ) { eom = 1; continue; } if ( eom == 1 && ( c == '.' ) ) { eom = 2; continue; } if ( eom == 2 && ( c == '\r' || c == '\n' ) ) { eom = 3; break; } if ( eom != 0 ) { eom = 0; } } // POSTING TOO LONG? FAIL if ( toolong ) { sprintf(reply, "411 Not Posted: article exceeds sanity line limit of %d.", (int)group.PostLimit()); Send(reply); continue; } // BLESS ARTICLE -- VERIFY HEADER INTEGRITY, ADD NEEDED HEADERS vector header; vector body; if ( ReformatArticle(msg, header, body) < 0 ) { sprintf(reply, "441 %s", (const char*)Errmsg()); Send(reply); continue; } // POST ARTICLE // Don't affect 'current group' or 'current article'. // Group tgroup; if ( tgroup.Post(overview, header, body) < 0 ) { sprintf(reply, "441 %s", (const char*)tgroup.Errmsg()); Send(reply); continue; } Send("240 Article posted successfully."); // CC MESSAGE TO MAIL ADDRESS? if ( tgroup.IsCCPost() ) { string from = "Anonymous", subject = "-"; for ( uint t=0; tport = port; if ((sock = socket (AF_INET,SOCK_STREAM,0)) < 0) { errmsg = "socket(): "; errmsg += strerror(errno); return(-1); } // Allow reuse of address to avoid "bind(): address already in use" { int on = 1; if ( setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(int)) < 0 ) { errmsg = "setsockopt(SO_REUSEADDR): "; errmsg += strerror(errno); return(-1); } } sin.sin_family = AF_INET; sin.sin_addr.s_addr = INADDR_ANY; sin.sin_port = htons(port); while (bind (sock, (struct sockaddr*)&sin,sizeof(sin)) < 0) { perror("binding stream socket"); sleep(5); continue; } if ( listen(sock,5) < 0 ) { errmsg = "listen(sock,5): "; errmsg += strerror(errno); return(-1); } return(0); } // ACCEPT CONNECTIONS FROM REMOTE int Server::Accept() { fprintf(stderr, "Listening for connect requests on port %d\n", (int)port); size_t length = sizeof(sin); msgsock = accept(sock, (struct sockaddr*)&sin, &length); if ( msgsock < 0) { errmsg = "accept(): "; errmsg += strerror(errno); return(-1); } fprintf(stderr, "Connection from host %s, port %u\n", (const char*)inet_ntoa(sin.sin_addr), (int)ntohs(sin.sin_port)); return(0); } void HelpAndExit() { cerr << "simplenews - a news daemon\n" "\n" " -p -- use a port other than default (119)\n" " -newgroup -- create a new group, with interactive prompts.\n" << endl; exit(1); } void sigcld_handler(int) { int junk; // LIMIT LOOP for( int t=0; t<5 && waitpid(-1, &junk, WNOHANG) != -1; t++ ) { } } // RETURN THE UID/GID FOR A GIVEN USERNAME int Name2UidGid(const char *name, uid_t &uid, gid_t &gid) { struct passwd *pw = getpwnam(name); if (pw == NULL ) return(-1); uid = pw->pw_uid; gid = pw->pw_gid; endpwent(); return(0); } // RUN AS A PARTICULAR USER void RunAs(const char *name) { uid_t uid; gid_t gid; int err = 0; if ( Name2UidGid(name, uid, gid) < 0 ) { cerr << "ERROR: can't lookup uid/gid for user '" << name << "'" << endl; } else { if ( setgid(gid) < 0 ) { perror("setgid()"); err = 1; } if ( setuid(uid) < 0 ) { perror("setuid()"); err = 1; } if ( seteuid(uid) < 0 ) { perror("seteuid()"); err = 1; } } if ( err ) { cerr << "daemon exiting; can't become user '" << name << "'" << endl; exit(1); } } int main(int argc, const char *argv[]) { int port = 119; // IGNORE CHILD AND PIPE INTERRUPTS signal(SIGCLD, sigcld_handler); signal(SIGPIPE, SIG_IGN); for ( int t=1; t