newsd.C 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. // newsd -- A simple news server
  2. //
  3. // Copyright 2003-2009 Michael Sweet
  4. // Copyright 2002 Greg Ercolano
  5. //
  6. // This program is free software; you can redistribute it and/or modify
  7. // it under the terms of the GNU General Public Licensse as published by
  8. // the Free Software Foundation; either version 2 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // This program is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU General Public License
  17. // along with this program; if not, write to the Free Software
  18. // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  19. //
  20. // This program was designed based on the RFCs:
  21. //
  22. // rfc1036.txt
  23. // rfc2980.txt
  24. // rfc3977.txt
  25. //
  26. // TODO:
  27. // Should probably reorganize both NNTP headers and mail headers
  28. // so when browsing messages, headers don't jump around
  29. //
  30. // Cleanup the whole mail gateway posting/mail vs regular posting/mail;
  31. // lots of redundant code.
  32. //
  33. // IPv6 support!
  34. //
  35. // Convenience macro...
  36. #define ISHEAD(a) (strncasecmp(header[t].c_str(), (a), strlen(a))==0)
  37. #include "Server.H"
  38. // Global configuration data...
  39. Configuration G_conf;
  40. // Number of child processes...
  41. static unsigned G_numclients = 0;
  42. // Overview data headers...
  43. static const char *overview[] =
  44. {
  45. "Subject:",
  46. "From:",
  47. "Date:",
  48. "Message-ID:",
  49. "References:",
  50. "Bytes:",
  51. "Lines:",
  52. "Xref:full",
  53. "Reply-To:",
  54. NULL
  55. };
  56. // HANDLE SIGCHLD
  57. // Daemon reaps child processes to keep count.
  58. //
  59. void sigcld_handler(int)
  60. {
  61. // REAPER
  62. // for() limits #children reaped at a time mainly to prevent
  63. // possible infinite loop.
  64. //
  65. pid_t pid; // Process ID
  66. int status; // Exit status
  67. for (int t = 0; t < 100 && (pid = waitpid(-1, &status, WNOHANG)) > 0; t ++)
  68. {
  69. if ( pid > 0 && G_numclients > 0 )
  70. G_numclients --;
  71. }
  72. }
  73. void HelpAndExit()
  74. {
  75. fputs("newsd - a simple news daemon (V " VERSION ")\n"
  76. " See LICENSE file packaged with newsd for license/copyright info.\n"
  77. "\n"
  78. "Usage:\n"
  79. " newsd [-c configfile] [-d] [-f] -- start server\n"
  80. " newsd -mailgateway <group> -- used in /etc/aliases\n"
  81. " newsd -newgroup -- used to create new groups\n"
  82. " newsd -rotate -- force log rotation\n",
  83. stderr);
  84. exit(1);
  85. }
  86. // RUN AS A PARTICULAR USER
  87. int RunAs()
  88. {
  89. int err = 0;
  90. if (chdir("/")) { perror("newsd: chdir(\"/\")"); err = 1; }
  91. if (G_conf.BadUser()) err = 1;
  92. else if (!geteuid())
  93. {
  94. gid_t gid = G_conf.GID();
  95. setgroups(1, &gid);
  96. if ( setgid(gid) < 0 ) { perror("newsd: setgid()"); err = 1; }
  97. if ( setuid(G_conf.UID()) < 0 ) { perror("newsd: setuid()"); err = 1; }
  98. }
  99. return(err);
  100. }
  101. // CREATE DEAD LETTER FILE, CHOWN ACCORDINGLY
  102. int CreateDeadLetter()
  103. {
  104. string filename = G_conf.SpoolDir();
  105. filename += "/.deadletters";
  106. fprintf(stderr, "(Creating %s)\n", filename.c_str());
  107. FILE *fp = fopen(filename.c_str(), "a");
  108. if ( fp == NULL ) { perror(filename.c_str()); return(-1); }
  109. fchmod(fileno(fp), 0600);
  110. fchown(fileno(fp), G_conf.UID(), G_conf.GID());
  111. fclose(fp);
  112. return(0);
  113. }
  114. // WRITE A MESSAGE (header+body) TO DEAD LETTER FILE
  115. void DeadLetter(const char *errmsg, vector<string>&head, vector<string>&body)
  116. {
  117. string filename = G_conf.SpoolDir();
  118. filename += "/.deadletters";
  119. FILE *fp = fopen(filename.c_str(), "a");
  120. fchmod(fileno(fp), 0600);
  121. fchown(fileno(fp), G_conf.UID(), G_conf.GID());
  122. if ( fp == NULL )
  123. { perror(filename.c_str()); return; }
  124. for ( uint t=0; t<head.size(); t++ )
  125. fprintf(fp, "%s\n", head[t].c_str());
  126. fprintf(fp, "\n");
  127. for ( uint t=0; t<body.size(); t++ )
  128. fprintf(fp, "%s\n", body[t].c_str());
  129. fprintf(fp, "\n");
  130. fclose(fp);
  131. }
  132. // HANDLE GATEWAYING MAIL INTO THE NEWSGROUP
  133. // Reads email message from stdin.
  134. //
  135. int MailGateway(const char *groupname)
  136. {
  137. Group group;
  138. if ( group.Load(groupname) < 0 )
  139. {
  140. fprintf(stderr, "newsd: Unknown group \"%s\": %s\n", groupname,
  141. group.Errmsg());
  142. return(1);
  143. }
  144. // COLLECT EMAIL FROM STDIN
  145. int linechars = 0,
  146. linecount = 0,
  147. toolong = 0;
  148. int eom = 0;
  149. char c;
  150. string msg;
  151. // CONVERT EMAIL HEADER -> NEWSGROUP HEADER
  152. {
  153. // Newsgroups:
  154. msg = "Newsgroups: ";
  155. msg += groupname;
  156. msg += "\r\n";
  157. // X-News-Gateway:
  158. // I pulled this outta my ass; need some way to indicate
  159. // msg passed through a mail -> news gateway.
  160. //
  161. msg += "X-Mail-To-News-Gateway: via newsd ";
  162. msg += VERSION;
  163. msg += "\r\n";
  164. }
  165. while (read(0, &c, 1) == 1 )
  166. {
  167. // KEEP TRACK OF #LINES
  168. // Lines longer than 80 chars count as multiple lines.
  169. // If posting too long, stop accumulating message in ram,
  170. // but keep reading until they've sent the terminating "."
  171. //
  172. ++linechars;
  173. if ( linechars > 80 || c == '\n' )
  174. { linechars = 0; linecount++; }
  175. if ( group.PostLimit() > 0 && linecount > group.PostLimit() )
  176. { toolong = 1; continue; }
  177. if ( c == '\n' ) { msg += '\r'; } // SMTP "\n" -> NNTP "\r\n"
  178. msg += c;
  179. // PARSE FOR END OF MESSAGE
  180. // Cute little state machine to maintain end of message
  181. // parse over buffer boundaries. The states:
  182. //
  183. // 0 - not yet at eom
  184. // 1 - \n before the dot
  185. // 2 - the dot
  186. // 3 - \r (or \n) after the dot
  187. //
  188. if (c == '\n' || c == '\r')
  189. {
  190. if (eom == 0)
  191. eom = 1;
  192. else if (eom == 2)
  193. {
  194. eom = 3;
  195. break;
  196. }
  197. }
  198. else if ( eom == 1 && ( c == '.' ) )
  199. eom = 2;
  200. else if ( eom != 0 )
  201. eom = 0;
  202. }
  203. // POSTING TOO LONG? FAIL
  204. if ( toolong )
  205. {
  206. fprintf(stderr, "newsd: Article not posted to %s: longer than %d lines.\n",
  207. groupname, group.PostLimit());
  208. return(1);
  209. }
  210. // BLESS ARTICLE -- VERIFY HEADER INTEGRITY, ADD NEEDED HEADERS
  211. vector<string> header;
  212. vector<string> body;
  213. if ( group.ParseArticle(msg, header, body) < 0 )
  214. {
  215. fprintf(stderr, "newsd: Article not posted to %s: %s.\n",
  216. groupname, group.Errmsg());
  217. return(1);
  218. }
  219. // UPDATE 'Path:'
  220. group.UpdatePath(header);
  221. // CHECK FOR LOOPS, MASSAGE HEADERS
  222. for ( uint t=0; t<header.size(); t++ )
  223. {
  224. // CONVERT RFC822 "From .." -> "X-Original-From: .."
  225. if ( ISHEAD("From ") )
  226. {
  227. string newfrom = header[t];
  228. newfrom.replace(0, strlen("From "), "X-Original-From: ");
  229. header[t] = newfrom;
  230. }
  231. // SHORT CIRCUIT LOOP DELIVERY
  232. // Example: "X-Loop: outOfSpace"
  233. //
  234. else if ( ISHEAD("X-Newsd-Loop:") || ISHEAD("X-Loop:") )
  235. {
  236. string errmsg = "newsd: -mailgateway ";
  237. errmsg += groupname;
  238. errmsg += ": NOT POSTED: '";
  239. errmsg += header[t];
  240. errmsg += "' mail loop detected: message dropped to "
  241. SPOOL_DIR "/.deadletters";
  242. DeadLetter(errmsg.c_str(), header, body);
  243. fprintf(stderr, "%s\n",
  244. (const char*)errmsg.c_str());
  245. return(1);
  246. }
  247. }
  248. // POST ARTICLE
  249. // Don't affect 'current group' or 'current article'.
  250. //
  251. if ( group.Post(overview, header, body, "localhost", true) < 0 )
  252. {
  253. fprintf(stderr, "newsd: Article not posted to %s: %s.\n",
  254. groupname, group.Errmsg());
  255. return(1);
  256. }
  257. // CC MESSAGE TO MAIL ADDRESS?
  258. if ( group.IsCCPost() )
  259. {
  260. string from = "Anonymous",
  261. subject = "-";
  262. // HANDLE PRESERVING FIELDS FROM NNTP POSTING -> SMTP
  263. string preserve;
  264. int pflag = 0;
  265. for ( unsigned t=0; t<header.size(); t++ )
  266. {
  267. c = header[t].c_str()[0];
  268. // CONTINUATION OF HEADER LINE?
  269. if ( c == ' ' || c == 9 )
  270. {
  271. // CONTINUATION OF PREVIOUS PRESERVED HEADER LINE?
  272. if ( pflag )
  273. { preserve += header[t]; preserve += "\n"; }
  274. continue;
  275. }
  276. // ZERO OUT PRESERVE -- NO MORE CONTINUATIONS
  277. pflag = 0;
  278. // CHECK FOR PRESERVE FIELDS
  279. if ( ISHEAD("From: ") || // must
  280. ISHEAD("Subject: ") || // must
  281. ISHEAD("References: ") || // needed to preserve threading
  282. ISHEAD("Xref: ") || // ?
  283. ISHEAD("Path: ") || // RFC 1036 2.1.6 (STR #15)
  284. ISHEAD("Content-Type: ") || // mime related
  285. ISHEAD("MIME-Version: ") || // mime related
  286. ISHEAD("Message-ID: ") ) // needed to preserve threading
  287. {
  288. pflag = 1;
  289. preserve += header[t];
  290. preserve += "\n";
  291. }
  292. }
  293. G_conf.LogMessage(L_DEBUG, "popen(%s,\"w\")..",
  294. (const char*)G_conf.SendMail());
  295. FILE *fp = popen(G_conf.SendMail(), "w");
  296. if ( ! fp )
  297. {
  298. G_conf.LogMessage(L_ERROR,
  299. "mailgateway: ccpost popen() can't execute '%s': %s",
  300. (const char*)G_conf.SendMail(),
  301. (const char*)strerror(errno));
  302. }
  303. else
  304. {
  305. fprintf(fp, "To: %s\n", (const char*)group.VoidEmail());
  306. fprintf(fp, "Bcc: %s\n", (const char*)group.CCPost());
  307. fprintf(fp, "%s", preserve.c_str());
  308. // Reply-To: Needed for mail gateway
  309. if ( group.IsReplyTo() )
  310. fprintf(fp, "Reply-To: %s\n", (const char*)group.ReplyTo());
  311. // Errors-To: advised so admin hears about problems, in addition
  312. // to the real person who sent the message.
  313. //
  314. fprintf(fp, "Errors-To: %s\n", (const char*)group.Creator());
  315. fprintf(fp, "\n");
  316. fprintf(fp, "[posted to %s]\n\n", (const char*)group.Name());
  317. for (unsigned t = 0; t < body.size(); t ++ )
  318. fprintf(fp, "%s\n", body[t].c_str());
  319. if (pclose(fp) < 0)
  320. G_conf.LogMessage(L_ERROR,
  321. "mailgateway: ccpost pclose() failed for '%s': %s",
  322. (const char*)G_conf.SendMail(),
  323. (const char*)strerror(errno));
  324. }
  325. }
  326. return(0);
  327. }
  328. int main(int argc, const char *argv[])
  329. {
  330. // HANDLE SIGCHLD, IGNORE SIGPIPE + SIGALRM
  331. signal(SIGCHLD, sigcld_handler);
  332. signal(SIGPIPE, SIG_IGN);
  333. signal(SIGALRM, SIG_IGN);
  334. umask(022); // enforce rw-r--r-- perms
  335. Server server;
  336. const char *conffile = CONFIG_FILE;
  337. const char *mailgateway = NULL;
  338. int newgroup = 0;
  339. int dodebug = 0,
  340. dofork = 1,
  341. dorotate = 0;
  342. // Scan command-line...
  343. for (int t = 1; t < argc; t ++)
  344. {
  345. if (!strcmp(argv[t], "-c"))
  346. {
  347. if (++t >= argc)
  348. {
  349. fputs("newsd: Expected filename after \"-c\"!\n", stderr);
  350. HelpAndExit();
  351. }
  352. conffile = argv[t];
  353. }
  354. else if (!strcmp(argv[t], "-d"))
  355. { dodebug = 1; dofork = 0; }
  356. else if (!strcmp(argv[t], "-f"))
  357. { dofork = 0; }
  358. else if (!strncmp(argv[t], "-h", 2))
  359. { HelpAndExit(); }
  360. else if (!strcmp(argv[t], "-mailgateway"))
  361. {
  362. if (++t >= argc)
  363. {
  364. fputs("newsd: Expected groupname after \"-mailgateway\"!\n", stderr);
  365. HelpAndExit();
  366. }
  367. mailgateway = argv[t];
  368. dofork = 0;
  369. }
  370. else if (!strcmp(argv[t], "-newgroup"))
  371. { newgroup = 1; dofork = 0; }
  372. else if (!strcmp(argv[t], "-rotate"))
  373. { dorotate = 1; dofork = 0; }
  374. else
  375. { fprintf(stderr, "newsd: Unknown argument '%s'\n", argv[t]); HelpAndExit(); }
  376. }
  377. // Load global config data...
  378. G_conf.Load(conffile);
  379. if (dodebug)
  380. {
  381. G_conf.LogLevel(L_DEBUG);
  382. G_conf.ErrorLog("stderr");
  383. }
  384. // Do stuff...
  385. if (dorotate)
  386. {
  387. G_conf.InitLog(); // open log (it isn't yet)
  388. G_conf.LogLock(); // lock while rotating
  389. G_conf.Rotate(true); // force rotation
  390. G_conf.LogUnlock();
  391. exit(0);
  392. }
  393. else if (mailgateway)
  394. {
  395. if (RunAs()) return(1);
  396. return(MailGateway(mailgateway));
  397. }
  398. else if (newgroup)
  399. {
  400. if (RunAs()) return(1);
  401. // CREATE DEAD LETTER FILE, IF NONE
  402. if ( CreateDeadLetter() < 0 )
  403. { fprintf(stderr, "news: CreateDeadLetter(): failed\n"); return(1); }
  404. Group tmp;
  405. return(tmp.NewGroup());
  406. }
  407. // Start logging...
  408. G_conf.InitLog();
  409. // Log server information...
  410. G_conf.LogMessage(L_INFO, "-- newsd started --");
  411. G_conf.LogMessage(L_INFO, "-- start config summary --");
  412. G_conf.LogSelf(L_INFO);
  413. G_conf.LogMessage(L_INFO, "-- end config summary --");
  414. if (server.Listen() < 0)
  415. {
  416. G_conf.LogMessage(L_ERROR, "Unable to listen for connections: %s",
  417. server.Errmsg());
  418. return(1);
  419. }
  420. // RUN AS THE USER 'NEWS'
  421. // Now that we've opened the reserved port,
  422. // we no longer need to be root.
  423. //
  424. if (RunAs())
  425. return(1);
  426. // Fork into the background...
  427. if (dofork)
  428. {
  429. pid_t pid = fork();
  430. switch ( pid )
  431. {
  432. case -1: // ERROR
  433. G_conf.LogMessage(L_ERROR, "daemonize fork(): %s (exiting)",
  434. strerror(errno));
  435. return(1);
  436. case 0: // CHILD
  437. // "Daemonize" our application so it is no longer connected to
  438. // the current terminal session...
  439. close(0);
  440. open("/dev/null", O_RDONLY);
  441. close(1);
  442. open("/dev/null", O_WRONLY);
  443. close(2);
  444. open("/dev/null", O_WRONLY);
  445. if ( setsid() < 0 )
  446. G_conf.LogMessage(L_ERROR, "setsid() failed (ignored): %s",
  447. strerror(errno));
  448. break;
  449. default: // PARENT
  450. return(0);
  451. }
  452. }
  453. // ACCEPT NEW CONNECTIONS LOOP
  454. for (;;)
  455. {
  456. if (server.Accept() < 0)
  457. {
  458. G_conf.LogMessage(L_ERROR, "Unable to accept new connection: %s",
  459. server.Errmsg());
  460. sleep(10);
  461. continue;
  462. }
  463. // TOO MANY CHILDREN?
  464. if (G_conf.MaxClients() != 0 && G_numclients >= G_conf.MaxClients())
  465. {
  466. server.Send("400 Server has too many connections open -- try again later");
  467. close(server.MsgSock());
  468. continue;
  469. }
  470. // FORK A CHILD TO HANDLE CONNECTION
  471. // News readers can keep a connection open for the entire
  472. // duration of the news reading session.
  473. //
  474. pid_t pid;
  475. while ((pid = fork()) == -1)
  476. {
  477. G_conf.LogMessage(L_ERROR, "Unable to fork handler process: %s",
  478. strerror(errno));
  479. sleep(10);
  480. }
  481. G_numclients ++;
  482. switch (pid)
  483. {
  484. case 0 : // CHILD
  485. G_conf.ErrorLog(G_conf.ErrorLog());
  486. server.CommandLoop(overview);
  487. exit(0);
  488. default : // PARENT
  489. close(server.MsgSock());
  490. break;
  491. }
  492. }
  493. //NOTREACHED
  494. }