123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558 |
- // newsd -- A simple news server
- //
- // Copyright 2003-2009 Michael Sweet
- // Copyright 2002 Greg Ercolano
- //
- // This program is free software; you can redistribute it and/or modify
- // it under the terms of the GNU General Public Licensse as published by
- // the Free Software Foundation; either version 2 of the License, or
- // (at your option) any later version.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU General Public License for more details.
- //
- // You should have received a copy of the GNU General Public License
- // along with this program; if not, write to the Free Software
- // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
- //
- // This program was designed based on the RFCs:
- //
- // rfc1036.txt
- // rfc2980.txt
- // rfc3977.txt
- //
- // TODO:
- // Should probably reorganize both NNTP headers and mail headers
- // so when browsing messages, headers don't jump around
- //
- // Cleanup the whole mail gateway posting/mail vs regular posting/mail;
- // lots of redundant code.
- //
- // IPv6 support!
- //
- // Convenience macro...
- #define ISHEAD(a) (strncasecmp(header[t].c_str(), (a), strlen(a))==0)
- #include "Server.H"
- // Global configuration data...
- Configuration G_conf;
- // Number of child processes...
- static unsigned G_numclients = 0;
- // Overview data headers...
- static const char *overview[] =
- {
- "Subject:",
- "From:",
- "Date:",
- "Message-ID:",
- "References:",
- "Bytes:",
- "Lines:",
- "Xref:full",
- "Reply-To:",
- NULL
- };
- // HANDLE SIGCHLD
- // Daemon reaps child processes to keep count.
- //
- void sigcld_handler(int)
- {
- // REAPER
- // for() limits #children reaped at a time mainly to prevent
- // possible infinite loop.
- //
- pid_t pid; // Process ID
- int status; // Exit status
- for (int t = 0; t < 100 && (pid = waitpid(-1, &status, WNOHANG)) > 0; t ++)
- {
- if ( pid > 0 && G_numclients > 0 )
- G_numclients --;
- }
- }
- void HelpAndExit()
- {
- fputs("newsd - a simple news daemon (V " VERSION ")\n"
- " See LICENSE file packaged with newsd for license/copyright info.\n"
- "\n"
- "Usage:\n"
- " newsd [-c configfile] [-d] [-f] -- start server\n"
- " newsd -mailgateway <group> -- used in /etc/aliases\n"
- " newsd -newgroup -- used to create new groups\n"
- " newsd -rotate -- force log rotation\n",
- stderr);
- exit(1);
- }
- // RUN AS A PARTICULAR USER
- int RunAs()
- {
- int err = 0;
- if (chdir("/")) { perror("newsd: chdir(\"/\")"); err = 1; }
- if (G_conf.BadUser()) err = 1;
- else if (!geteuid())
- {
- gid_t gid = G_conf.GID();
- setgroups(1, &gid);
- if ( setgid(gid) < 0 ) { perror("newsd: setgid()"); err = 1; }
- if ( setuid(G_conf.UID()) < 0 ) { perror("newsd: setuid()"); err = 1; }
- }
- return(err);
- }
- // CREATE DEAD LETTER FILE, CHOWN ACCORDINGLY
- int CreateDeadLetter()
- {
- string filename = G_conf.SpoolDir();
- filename += "/.deadletters";
- fprintf(stderr, "(Creating %s)\n", filename.c_str());
- FILE *fp = fopen(filename.c_str(), "a");
- if ( fp == NULL ) { perror(filename.c_str()); return(-1); }
- fchmod(fileno(fp), 0600);
- fchown(fileno(fp), G_conf.UID(), G_conf.GID());
- fclose(fp);
- return(0);
- }
- // WRITE A MESSAGE (header+body) TO DEAD LETTER FILE
- void DeadLetter(const char *errmsg, vector<string>&head, vector<string>&body)
- {
- string filename = G_conf.SpoolDir();
- filename += "/.deadletters";
- FILE *fp = fopen(filename.c_str(), "a");
- fchmod(fileno(fp), 0600);
- fchown(fileno(fp), G_conf.UID(), G_conf.GID());
- if ( fp == NULL )
- { perror(filename.c_str()); return; }
- for ( uint t=0; t<head.size(); t++ )
- fprintf(fp, "%s\n", head[t].c_str());
- fprintf(fp, "\n");
- for ( uint t=0; t<body.size(); t++ )
- fprintf(fp, "%s\n", body[t].c_str());
- fprintf(fp, "\n");
- fclose(fp);
- }
- // HANDLE GATEWAYING MAIL INTO THE NEWSGROUP
- // Reads email message from stdin.
- //
- int MailGateway(const char *groupname)
- {
- Group group;
- if ( group.Load(groupname) < 0 )
- {
- fprintf(stderr, "newsd: Unknown group \"%s\": %s\n", groupname,
- group.Errmsg());
- return(1);
- }
- // COLLECT EMAIL FROM STDIN
- int linechars = 0,
- linecount = 0,
- toolong = 0;
- int eom = 0;
- char c;
- string msg;
- // CONVERT EMAIL HEADER -> NEWSGROUP HEADER
- {
- // Newsgroups:
- msg = "Newsgroups: ";
- msg += groupname;
- msg += "\r\n";
- // X-News-Gateway:
- // I pulled this outta my ass; need some way to indicate
- // msg passed through a mail -> news gateway.
- //
- msg += "X-Mail-To-News-Gateway: via newsd ";
- msg += VERSION;
- msg += "\r\n";
- }
- while (read(0, &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; }
- if ( c == '\n' ) { msg += '\r'; } // SMTP "\n" -> NNTP "\r\n"
- 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 (c == '\n' || c == '\r')
- {
- if (eom == 0)
- eom = 1;
- else if (eom == 2)
- {
- eom = 3;
- break;
- }
- }
- else if ( eom == 1 && ( c == '.' ) )
- eom = 2;
- else if ( eom != 0 )
- eom = 0;
- }
- // POSTING TOO LONG? FAIL
- if ( toolong )
- {
- fprintf(stderr, "newsd: Article not posted to %s: longer than %d lines.\n",
- groupname, group.PostLimit());
- return(1);
- }
- // BLESS ARTICLE -- VERIFY HEADER INTEGRITY, ADD NEEDED HEADERS
- vector<string> header;
- vector<string> body;
- if ( group.ParseArticle(msg, header, body) < 0 )
- {
- fprintf(stderr, "newsd: Article not posted to %s: %s.\n",
- groupname, group.Errmsg());
- return(1);
- }
- // UPDATE 'Path:'
- group.UpdatePath(header);
- // CHECK FOR LOOPS, MASSAGE HEADERS
- for ( uint t=0; t<header.size(); t++ )
- {
- // CONVERT RFC822 "From .." -> "X-Original-From: .."
- if ( ISHEAD("From ") )
- {
- string newfrom = header[t];
- newfrom.replace(0, strlen("From "), "X-Original-From: ");
- header[t] = newfrom;
- }
- // SHORT CIRCUIT LOOP DELIVERY
- // Example: "X-Loop: outOfSpace"
- //
- else if ( ISHEAD("X-Newsd-Loop:") || ISHEAD("X-Loop:") )
- {
- string errmsg = "newsd: -mailgateway ";
- errmsg += groupname;
- errmsg += ": NOT POSTED: '";
- errmsg += header[t];
- errmsg += "' mail loop detected: message dropped to "
- SPOOL_DIR "/.deadletters";
- DeadLetter(errmsg.c_str(), header, body);
- fprintf(stderr, "%s\n",
- (const char*)errmsg.c_str());
- return(1);
- }
- }
- // POST ARTICLE
- // Don't affect 'current group' or 'current article'.
- //
- if ( group.Post(overview, header, body, "localhost", true) < 0 )
- {
- fprintf(stderr, "newsd: Article not posted to %s: %s.\n",
- groupname, group.Errmsg());
- return(1);
- }
- // CC MESSAGE TO MAIL ADDRESS?
- if ( group.IsCCPost() )
- {
- string from = "Anonymous",
- subject = "-";
- // HANDLE PRESERVING FIELDS FROM NNTP POSTING -> SMTP
- string preserve;
- int pflag = 0;
- for ( unsigned t=0; t<header.size(); t++ )
- {
- c = header[t].c_str()[0];
- // CONTINUATION OF HEADER LINE?
- if ( c == ' ' || c == 9 )
- {
- // CONTINUATION OF PREVIOUS PRESERVED HEADER LINE?
- if ( pflag )
- { preserve += header[t]; preserve += "\n"; }
- continue;
- }
- // ZERO OUT PRESERVE -- NO MORE CONTINUATIONS
- pflag = 0;
- // CHECK FOR PRESERVE FIELDS
- if ( ISHEAD("From: ") || // must
- ISHEAD("Subject: ") || // must
- ISHEAD("References: ") || // needed to preserve threading
- ISHEAD("Xref: ") || // ?
- ISHEAD("Path: ") || // RFC 1036 2.1.6 (STR #15)
- ISHEAD("Content-Type: ") || // mime related
- ISHEAD("MIME-Version: ") || // mime related
- ISHEAD("Message-ID: ") ) // needed to preserve threading
- {
- pflag = 1;
- preserve += header[t];
- preserve += "\n";
- }
- }
- G_conf.LogMessage(L_DEBUG, "popen(%s,\"w\")..",
- (const char*)G_conf.SendMail());
- FILE *fp = popen(G_conf.SendMail(), "w");
- if ( ! fp )
- {
- G_conf.LogMessage(L_ERROR,
- "mailgateway: ccpost popen() can't execute '%s': %s",
- (const char*)G_conf.SendMail(),
- (const char*)strerror(errno));
- }
- else
- {
- fprintf(fp, "To: %s\n", (const char*)group.VoidEmail());
- fprintf(fp, "Bcc: %s\n", (const char*)group.CCPost());
- fprintf(fp, "%s", preserve.c_str());
- // Reply-To: Needed for mail gateway
- if ( group.IsReplyTo() )
- fprintf(fp, "Reply-To: %s\n", (const char*)group.ReplyTo());
- // Errors-To: advised so admin hears about problems, in addition
- // to the real person who sent the message.
- //
- fprintf(fp, "Errors-To: %s\n", (const char*)group.Creator());
- fprintf(fp, "\n");
- fprintf(fp, "[posted to %s]\n\n", (const char*)group.Name());
- for (unsigned t = 0; t < body.size(); t ++ )
- fprintf(fp, "%s\n", body[t].c_str());
- if (pclose(fp) < 0)
- G_conf.LogMessage(L_ERROR,
- "mailgateway: ccpost pclose() failed for '%s': %s",
- (const char*)G_conf.SendMail(),
- (const char*)strerror(errno));
- }
- }
- return(0);
- }
- int main(int argc, const char *argv[])
- {
- // HANDLE SIGCHLD, IGNORE SIGPIPE + SIGALRM
- signal(SIGCHLD, sigcld_handler);
- signal(SIGPIPE, SIG_IGN);
- signal(SIGALRM, SIG_IGN);
- umask(022); // enforce rw-r--r-- perms
- Server server;
- const char *conffile = CONFIG_FILE;
- const char *mailgateway = NULL;
- int newgroup = 0;
- int dodebug = 0,
- dofork = 1,
- dorotate = 0;
- // Scan command-line...
- for (int t = 1; t < argc; t ++)
- {
- if (!strcmp(argv[t], "-c"))
- {
- if (++t >= argc)
- {
- fputs("newsd: Expected filename after \"-c\"!\n", stderr);
- HelpAndExit();
- }
- conffile = argv[t];
- }
- else if (!strcmp(argv[t], "-d"))
- { dodebug = 1; dofork = 0; }
- else if (!strcmp(argv[t], "-f"))
- { dofork = 0; }
- else if (!strncmp(argv[t], "-h", 2))
- { HelpAndExit(); }
- else if (!strcmp(argv[t], "-mailgateway"))
- {
- if (++t >= argc)
- {
- fputs("newsd: Expected groupname after \"-mailgateway\"!\n", stderr);
- HelpAndExit();
- }
- mailgateway = argv[t];
- dofork = 0;
- }
- else if (!strcmp(argv[t], "-newgroup"))
- { newgroup = 1; dofork = 0; }
- else if (!strcmp(argv[t], "-rotate"))
- { dorotate = 1; dofork = 0; }
- else
- { fprintf(stderr, "newsd: Unknown argument '%s'\n", argv[t]); HelpAndExit(); }
- }
- // Load global config data...
- G_conf.Load(conffile);
- if (dodebug)
- {
- G_conf.LogLevel(L_DEBUG);
- G_conf.ErrorLog("stderr");
- }
- // Do stuff...
- if (dorotate)
- {
- G_conf.InitLog(); // open log (it isn't yet)
- G_conf.LogLock(); // lock while rotating
- G_conf.Rotate(true); // force rotation
- G_conf.LogUnlock();
- exit(0);
- }
- else if (mailgateway)
- {
- if (RunAs()) return(1);
- return(MailGateway(mailgateway));
- }
- else if (newgroup)
- {
- if (RunAs()) return(1);
- // CREATE DEAD LETTER FILE, IF NONE
- if ( CreateDeadLetter() < 0 )
- { fprintf(stderr, "news: CreateDeadLetter(): failed\n"); return(1); }
- Group tmp;
- return(tmp.NewGroup());
- }
- // Start logging...
- G_conf.InitLog();
- // Log server information...
- G_conf.LogMessage(L_INFO, "-- newsd started --");
- G_conf.LogMessage(L_INFO, "-- start config summary --");
- G_conf.LogSelf(L_INFO);
- G_conf.LogMessage(L_INFO, "-- end config summary --");
- if (server.Listen() < 0)
- {
- G_conf.LogMessage(L_ERROR, "Unable to listen for connections: %s",
- server.Errmsg());
- return(1);
- }
- // RUN AS THE USER 'NEWS'
- // Now that we've opened the reserved port,
- // we no longer need to be root.
- //
- if (RunAs())
- return(1);
- // Fork into the background...
- if (dofork)
- {
- pid_t pid = fork();
- switch ( pid )
- {
- case -1: // ERROR
- G_conf.LogMessage(L_ERROR, "daemonize fork(): %s (exiting)",
- strerror(errno));
- return(1);
- case 0: // CHILD
- // "Daemonize" our application so it is no longer connected to
- // the current terminal session...
- close(0);
- open("/dev/null", O_RDONLY);
- close(1);
- open("/dev/null", O_WRONLY);
- close(2);
- open("/dev/null", O_WRONLY);
- if ( setsid() < 0 )
- G_conf.LogMessage(L_ERROR, "setsid() failed (ignored): %s",
- strerror(errno));
- break;
- default: // PARENT
- return(0);
- }
- }
- // ACCEPT NEW CONNECTIONS LOOP
- for (;;)
- {
- if (server.Accept() < 0)
- {
- G_conf.LogMessage(L_ERROR, "Unable to accept new connection: %s",
- server.Errmsg());
- sleep(10);
- continue;
- }
- // TOO MANY CHILDREN?
- if (G_conf.MaxClients() != 0 && G_numclients >= G_conf.MaxClients())
- {
- server.Send("400 Server has too many connections open -- try again later");
- close(server.MsgSock());
- continue;
- }
- // FORK A CHILD TO HANDLE CONNECTION
- // News readers can keep a connection open for the entire
- // duration of the news reading session.
- //
- pid_t pid;
- while ((pid = fork()) == -1)
- {
- G_conf.LogMessage(L_ERROR, "Unable to fork handler process: %s",
- strerror(errno));
- sleep(10);
- }
- G_numclients ++;
- switch (pid)
- {
- case 0 : // CHILD
- G_conf.ErrorLog(G_conf.ErrorLog());
- server.CommandLoop(overview);
- exit(0);
- default : // PARENT
- close(server.MsgSock());
- break;
- }
- }
- //NOTREACHED
- }
|