chiark / gitweb /
Initial checkin as found.
authorIan Jackson <ian@chiark.greenend.org.uk>
Tue, 1 Jun 2010 13:22:13 +0000 (14:22 +0100)
committerIan Jackson <ian@chiark.greenend.org.uk>
Tue, 1 Jun 2010 13:22:13 +0000 (14:22 +0100)
18 files changed:
.gitignore [new file with mode: 0644]
GPL [new file with mode: 0644]
Makefile [new file with mode: 0644]
README [new file with mode: 0644]
config.c [new file with mode: 0644]
main.c [new file with mode: 0644]
md5.c [new file with mode: 0644]
md5.h [new file with mode: 0644]
nnrpwrap.c [new file with mode: 0644]
nntp-merge-serv [new file with mode: 0755]
nntp-merge.conf [new file with mode: 0644]
nntp-merge.h [new file with mode: 0644]
nyxpost [new file with mode: 0755]
nyxpostrun.c [new file with mode: 0644]
post.c [new file with mode: 0644]
post.c.orig [new file with mode: 0644]
rfc977.txt [new file with mode: 0644]
sharedsecret.c [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..92ff319
--- /dev/null
@@ -0,0 +1,6 @@
+*~
+*.o
+md5cookie1way
+nnrpwrap
+nntp-merge
+nyxpostrun
diff --git a/GPL b/GPL
new file mode 100644 (file)
index 0000000..a43ea21
--- /dev/null
+++ b/GPL
@@ -0,0 +1,339 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                          675 Mass Ave, Cambridge, MA 02139, USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+\f
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+\f
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+\f
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+\f
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+\f
+       Appendix: How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) 19yy  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) 19yy name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..644accb
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,23 @@
+CFLAGS= -Wall -Wwrite-strings -Wpointer-arith -Wnested-externs \
+       -Wmissing-declarations -Wmissing-prototypes -O3 -g
+# -DDEBUG
+LDFLAGS= -s
+LIBS= -lident
+
+all:           nntp-merge md5cookie1way nnrpwrap nyxpostrun
+
+nntp-merge:    main.o post.o config.o md5.o
+               $(CC) $(LDFLAGS) -o $@ $^ $(LIBS)
+
+md5cookie1way: sharedsecret.o md5.o
+               $(CC) $(LDFLAGS) -o $@ $^ $(LIBS)
+
+nnrpwrap:      nnrpwrap.o md5.o
+               $(CC) $(LDFLAGS) -o $@ $^ $(LIBS)
+
+nyxpostrun:    nyxpostrun.o
+               $(CC) $(LDFLAGS) -o $@ $^ $(LIBS)
+
+main.o post.o config.o:                nntp-merge.h
+main.o:                                md5.h
+md5.o:                         md5.h
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..97866ad
--- /dev/null
+++ b/README
@@ -0,0 +1,38 @@
+This is nntp-merge, a program which acts as an NNTP server and passes
+requests to one or more other NNTP servers, towards which it presents a
+client interface.  Selection of which server to use is based on the group
+being read or posted to.  The merger supports XAUTHINFO GENERIC
+authentication with its own shared-secret MD5 challenge-response protocol;
+other authentication methods could be added in principle.
+
+I wrote it for my own system, chiark.greenend.org.uk, where it has proved
+to be very useful; it may also be useful to you.  If you like it and patch
+it please do send me the patches as I may someday turn this into a real
+free software product with documentation, version numbers, features and
+everything.
+
+There may be some binaries in the tarfile; these are Linux/i386 libc5
+ELF.  There is no configure script and no documentation, but there is
+a Makefile and a sample config file.
+
+If there are any new versions or anything I might put them up on my main
+WWW page (<URL:http://www.chiark.greenend.org.uk/~ijackson/>), or possibly
+at <URL:http://www.chiark.greenend.org.uk/~ian/nntp-merge/>.
+
+nntp-merge is Copyright (C)1995-1997 Ian Jackson.  It is free software; you
+can redistribute it and/or modify it under the terms of the GNU General
+Public License 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 as the file GPL; if not, email Ian Jackson
+<ian@chiark.greenend.org.uk> or write to the Free Software Foundation,
+Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA, or consult
+the Free Software Foundation's WWW page at <URL:http://www.fsf.org/>.
+
+ - Ian Jackson 16.09.1997
diff --git a/config.c b/config.c
new file mode 100644 (file)
index 0000000..87128c9
--- /dev/null
+++ b/config.c
@@ -0,0 +1,360 @@
+/**/
+
+/* Algorithm for finding a group:
+ *  OUT OF DATE !
+ * Start at listentry 0.
+ * For each character,
+ *  keep going through list until one of the following:
+ *   if character in string matches in list entry,
+ *    then jump to that listentry's `p' and go on to the next character
+ *   if we encounter a null character (ie, default, end of this list)
+ *    then we go to the listentry's `p' and use the `gi' member of
+ *    the union as the group info struct
+ * If we run out of characters,
+ *  keep going through the list until we find a default or an if-terminate-here
+ *  use the listentry's `p', and the `gi' member of that entry's union.
+ *
+ * The listentry pointingto's are indices into the array of listentry unions.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <ctype.h>
+#include <string.h>
+#include <limits.h>
+#include <sys/types.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#include "nntp-merge.h"
+
+/* Conceptually we have one array with all the stuff in it.  However,
+ * this is wasteful of space (we want the lookup algorithm and its
+ * data to fit in 1K).  Conceptually the array is one of
+ *  struct {
+ *    char c;
+ *    unsigned short n_to_go_to_if_char_matches_p;
+ *    struct groupinfo *groupinfo_to_use_if_char_does_not_match;
+ *  };
+ * The groupinfo pointer is in the array element corresponding to the
+ * null character at the end of the list.
+ * Conceptually `end of string' is just another character, 1, except
+ * that when you find it in the table you use the groupinfo member rather
+ * then n_to_go member.
+ */
+
+struct tnode {
+  unsigned short lip;
+  struct tlistentry {
+    struct tlistentry *next;
+    char c;
+    struct tnode *p;
+  } *l;
+  struct groupinfo *ifdef, *ifterm;
+};
+
+void *xmalloc(size_t s) {
+  void *r;
+  r= malloc(s);
+  if (!r) die("malloc failed");
+  return r;
+}
+
+char *xstrdup(const char *s) {
+  char *r;
+  r= xmalloc(strlen(s)+1);
+  strcpy(r,s);
+  return r;
+}
+
+struct permission *permissions;
+struct permission *restrictpost;
+struct serverinfo *servers;
+static struct serverinfo **lastserver;
+
+static char *lisc=0;
+static unsigned short *lisp=0;
+static struct groupinfo **lisg=0;
+
+static int line;
+static int nrequired;
+
+static struct tnode *newemptytnode(void) {
+  static struct tnode *r;
+
+  r= xmalloc(sizeof(struct tnode));
+  r->l= 0;
+  r->ifdef= 0;
+  r->ifterm= 0;
+  return r;
+}
+
+static void setmissingdefaults(struct tnode *n, struct groupinfo *gi) {
+  struct tlistentry *tle;
+  if (n->ifdef) gi= n->ifdef; else n->ifdef= gi;
+  for (tle= n->l; tle; tle= tle->next) setmissingdefaults(tle->p,gi);
+}
+  
+static void syntax(const char *m) {
+  printf("400 syntax error in config file, line %d: %s\r\n",line,m);
+  exit(1);
+}
+
+static void countup(struct tnode *n) {
+  struct tlistentry *tle;
+
+  assert(n->ifdef);
+  n->lip= nrequired;
+  for (tle= n->l; tle; tle= tle->next) nrequired++;
+  if (n->ifterm) nrequired++;
+  nrequired++;
+
+  for (tle= n->l; tle; tle= tle->next) countup(tle->p);
+}
+
+static void fillin(struct tnode *n) {
+  struct tlistentry *tle;
+  int i;
+
+  i= n->lip;
+  for (tle= n->l; tle; tle= tle->next) {
+    lisc[i]= tle->c;
+    lisp[i]= tle->p->lip;
+    fillin(tle->p);
+    i++;
+  }
+  if (n->ifterm) {
+    lisc[i]= '\1';
+    lisg[i]= n->ifterm;
+    i++;
+  }
+  lisc[i]= 0;
+  lisg[i]= n->ifdef;
+}
+
+static void posscopy(int *needcopyp, struct groupinfo **gip) {
+  struct groupinfo *gi;
+  if (!*needcopyp) return;
+  gi= xmalloc(sizeof(struct groupinfo));
+  *gi= **gip;
+  *needcopyp= 0;
+  *gip= gi;
+  return;
+}
+
+struct permission *findpermit(const char *name) {
+  struct permission *pi;
+
+  for (pi= permissions; pi && strcmp(name,pi->name); pi= pi->next);
+  return pi;
+}
+
+static struct permission *findmakepermit(const char *name) {
+  struct permission *pi;
+
+  if (!strcmp(name,"*")) return 0;
+  pi= findpermit(name); if (pi) return pi;
+
+  pi= xmalloc(sizeof(struct permission));
+  pi->name= xstrdup(name);
+  pi->authd= 0;
+  pi->next= permissions;
+  permissions= pi;
+  return pi;
+}
+
+static struct serverinfo *findserver(const char *name) {
+  struct serverinfo *search;
+
+  for (search= servers;
+       search && strcmp(search->nickname,name);
+       search= search->next);
+
+  if (!search) syntax("unknown server nickname");
+  return search;
+}
+
+static struct tnode *ttree;
+
+void readconfig(void) {
+  FILE *file;
+  struct groupinfo *gi;
+  int needcopy= 0;
+  struct tnode *tcur;
+  struct tlistentry *tle;
+  char buf[MAX_COMMAND], *p, *q;
+  struct serverinfo *si;
+  int c, i, insearchser;
+  unsigned long portt;
+
+  file= fopen("nntp-merge.conf","r");
+  if (!file) die("unable to open config file");
+
+  gi= xmalloc(sizeof(struct groupinfo));
+  gi->readfrom= 0;
+  gi->postto[0]= 0;
+  gi->offset= 0;
+  gi->restrictto= 0;
+  gi->readonlyto= 0;
+  needcopy= 0;
+
+  ttree= newemptytnode();
+  line= 0;
+  servers= 0;
+  lastserver= &servers;
+  while ((c= getc(file)) != EOF) {
+    line++;
+    if (c == '\n') continue;
+    if (c == '#') {
+      while ((c= getc(file)) != EOF && c != '\n');
+      continue;
+    }
+    if (isspace(c)) {
+      if (!gi->readfrom) syntax("config file has groups before readfrom specs");
+      do { c= getc(file); } while (c != EOF && c != '\n' && isspace(c));
+      if (c == '\n') continue;
+      ungetc(c,file);
+      tcur= ttree;
+      while ((c= getc(file)) != EOF && !isspace(c) && c != '*') {
+        if (c == '\1') syntax("group name in config file has ^A character");
+        for (tle= tcur->l; tle && tle->c != c; tle= tle->next);
+        if (!tle) {
+          tle= xmalloc(sizeof(struct tlistentry));
+          tle->next= tcur->l;
+          tle->c= c;
+          tle->p= newemptytnode();
+          tcur->l= tle;
+        }
+        tcur= tle->p;
+      }
+      if (c == '*') {
+       tcur->ifdef= gi;
+        needcopy= 1;
+        c= getc(file);
+      } else {
+        while (c != EOF && c != '\n' && isspace(c)) c= getc(file);
+        if (c != EOF && (isdigit(c) || c == '-')) {
+          posscopy(&needcopy,&gi);
+          ungetc(c,file);
+          if (fscanf(file,"%d",&gi->offset) != 1) die("fscanf in group offset");
+        } else if (gi->offset) {
+          posscopy(&needcopy,&gi);
+          gi->offset= 0;
+        }
+        tcur->ifterm= gi;
+        needcopy= 1;
+      }
+      while (c != EOF && c != '\n') {
+        c= getc(file);
+        if (!isspace(c)) syntax("stuff after * or space (and offset?) in group");
+      }
+    } else {
+      ungetc(c,file);
+      if (!fgets(buf,sizeof(buf),file)) break;
+      p= strchr(buf,'\n'); if (p) *p= 0;
+      p= strtok(buf," \t");
+      if (!strcmp(p,"read")) {
+        p= strtok(0," \t"); if (!p) syntax("missing server after read");
+        posscopy(&needcopy,&gi);
+        gi->readfrom= findserver(p);
+      } else if (!strcmp(p,"post")) {
+        posscopy(&needcopy,&gi);
+        for (i=0; (p= strtok(0," \t")); i++) {
+          if (i >= sizeof(gi->postto)/sizeof(gi->postto[0])-1)
+            syntax("too many servers after post");
+          gi->postto[i]= findserver(p);
+        }
+        gi->postto[i]= 0;
+      } else if (!strcmp(p,"server") || !strcmp(p,"server-nosearch")) {
+        insearchser= !strcmp(p,"server");
+        p= strtok(0," \t"); if (!p) syntax("missing server name");
+        for (si= servers; si && strcmp(si->nickname,p); si= si->next);
+        if (si) syntax("server nickname redefined");
+        si= xmalloc(sizeof(struct serverinfo));
+        si->next= 0;
+        si->searchthis= insearchser;
+        si->nickname= xstrdup(p);
+        si->send= 0;
+        p= strtok(0," \t"); if (!p) syntax("missing server host:port/command");
+        if (*p == '/') {
+          si->hostname= xstrdup(p);
+          si->port= 0;
+          si->program= 1;
+        } else {
+          q= strchr(p,':'); if (!q || !*q) syntax("missing :port in server");
+          *q++= 0;
+          si->hostname= xstrdup(p);
+          portt= strtoul(q,&q,10);
+          if (portt > USHRT_MAX || *q) syntax("bad :port in server");
+          si->port= portt;
+          si->program= 0;
+        }
+        while ((p= strtok(0," \t"))) {
+          if (strcmp(p,"mode-reader")) syntax("bad option on server");
+          si->send= "MODE READER";
+        }
+        si->rfile= si->wfile= 0;
+        *lastserver= si;
+        lastserver= &si->next;
+      } else if (!strcmp(p,"permit")) {
+        posscopy(&needcopy,&gi);
+        p= strtok(0," \t");
+        if (!p) syntax("missing read-only group after permit");
+        gi->readonlyto= findmakepermit(p);
+        p= strtok(0," \t");
+        if (!p) syntax("missing read-write group after permit");
+        gi->restrictto= findmakepermit(p);
+      } else if (!strcmp(p,"myfqdn")) {
+        posscopy(&needcopy,&gi);
+        p= strtok(0," \t");
+        if (!p) syntax("missing fqdn after myfqdn");
+        free(myfqdn);
+        myfqdn= xstrdup(p);
+      } else if (!strcmp(p,"xref")) {
+        posscopy(&needcopy,&gi);
+        p= strtok(0," \t");
+        if (!p) syntax("missing fqdn after myfqdn");
+        free(myxref);
+        myxref= xstrdup(p);
+      } else if (!strcmp(p,"fetch") ||
+                 !strcmp(p,"believe") ||
+                 !strcmp(p,"minreaddays") ||
+                 !strcmp(p,"maxperuser") ||
+                 !strcmp(p,"extrarc") ||
+                 !strcmp(p,"ignorerc")) {
+        /* These aren't for us, we ignore them. */
+      } else {
+        syntax("unknown thing in file");
+      }
+    }
+  }
+  if (ferror(file)) die("failed to read config file");
+  if (!ttree->ifdef) syntax("missing default newsgroup entry");
+  fclose(file);
+
+  setmissingdefaults(ttree,0);
+  
+  /* Now we count the total number of entries we need in our table. */
+  nrequired= 0;
+  countup(ttree);
+  lisc= xmalloc(nrequired);
+  lisp= xmalloc(nrequired*sizeof(unsigned short));
+  lisg= xmalloc(nrequired*sizeof(struct groupinfo*));
+  fillin(ttree);
+}
+
+struct groupinfo *findgroup(const char *name) {
+  /* Null or ':' terminated */
+  int c, d;
+  unsigned short cnode;
+
+  cnode= 0;
+  while ((c= *name++) && c != ':' && !isspace(c)) {
+    while ((d= lisc[cnode]) && d != c) cnode++;
+    if (d) { cnode= lisp[cnode]; continue; }
+    return lisg[cnode];
+  }
+  while ((d= lisc[cnode]) && d != '\1') cnode++;
+  return lisg[cnode];
+}
diff --git a/main.c b/main.c
new file mode 100644 (file)
index 0000000..cd1314e
--- /dev/null
+++ b/main.c
@@ -0,0 +1,1430 @@
+/**/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+#include <unistd.h>
+#include <netdb.h>
+#include <assert.h>
+#include <time.h>
+#include <errno.h>
+#include <fnmatch.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#include "nntp-merge.h"
+#include "md5.h"
+
+char *myfqdn, *myxref, *lastdoneauth= 0;
+const char *theirfqdn= 0;
+char currentgroupname[MAX_COMMAND+3];
+struct groupinfo *currentgroup;
+struct sockaddr_in peername;
+
+int stripcommand(char *buf) {
+  int n;
+  
+  n= strlen(buf);
+  if (n == 0) return 0;
+  if (buf[n-1] != '\n') return 0;
+  while (n>0 && isspace(buf[n-1])) n--;
+  buf[n]= 0;
+  return n;
+}
+
+void die(const char *msg) {
+  int e;
+  e= errno;
+  printf("400 server failure: %s - %s\r\n",msg,strerror(errno));
+  exit(0);
+}
+
+static int parsehex(char *p, unsigned char *up, int maxlen) {
+  int nused, v, n;
+  
+  nused= 0;
+  while (nused < MAX_SECRET && (n=-1, sscanf(p,"%x%n",&v,&n) >0) && n>0) {
+    p+= n;
+    *up++= v;
+    nused++;
+    if (*p == ':') p++;
+  }
+  return nused;
+}
+
+enum { scf_norecover=001, scf_nogroup };
+
+void closeserver(struct serverinfo *server) {
+  fclose(server->rfile); server->rfile= 0;
+  fclose(server->wfile); server->wfile= 0;
+}
+
+int decoderesponse(char response[MAX_RESPONSE+3], unsigned long *rvp,
+                   struct serverinfo *server) {
+  char temp[MAX_RESPONSE+3];
+  if (!isdigit(response[0]) || !isdigit(response[1]) || !isdigit(response[2]) ||
+      !(isspace(response[3]) || !response[3])) {
+    strcpy(temp,response);
+    sprintf(response,"503 server %.100s produced garbage: %.100s",
+            server->hostname,temp);
+    closeserver(server); return 0;
+  }
+  *rvp= strtol(response,0,16);
+  return 1;
+}
+
+#ifdef DEBUG
+static int realservercommand(struct serverinfo *server,
+                             const char command[], char response[MAX_RESPONSE+3],
+                             int flags);
+#endif
+int servercommand(struct serverinfo *server,
+                  const char command[], char response[MAX_RESPONSE+3],
+                  int flags) {
+#ifdef DEBUG
+  int rcode;
+  fprintf(stderr,"[%03o]>>>%s: %s\n",flags,server->nickname,command);
+  rcode= realservercommand(server,command,response,flags);
+  fprintf(stderr,"[%03x]<<<%s: %s\n",rcode,server->nickname,response);
+  return rcode;
+}
+static int realservercommand(struct serverinfo *server,
+                             const char command[], char response[MAX_RESPONSE+3],
+                             int flags) {
+#endif
+  struct hostent *he;
+  char **alist;
+  struct sockaddr_in sin;
+  pid_t c1;
+  int rpipefds[2], wpipefds[2], rfd, wfd, l, n;
+  unsigned long rv;
+  char temp[MAX_RESPONSE+3], *p;
+  unsigned char message[16+MAX_SECRET], cryptresponse[16];
+  struct MD5Context md5ctx;
+  FILE *file;
+  
+  /* response will be null-terminated and have trailing whitespace
+   * removed including \r\n
+   * (but indeed the extra two spaces used by this routine are for \r\n)
+   */
+  if (server->rfile) {
+    if (fprintf(server->wfile,"%s\r\n",command) == EOF ||
+        fflush(server->wfile) == EOF) goto conn_broken;
+    if (!fgets(response,MAX_RESPONSE+3,server->rfile)) goto conn_broken;
+    if (!stripcommand(response)) goto conn_broken;
+    if (!strncmp(response,"400",3)) goto conn_broken;
+    goto message_sent_response_ok;
+  conn_broken:
+    closeserver(server);
+    if (flags & scf_norecover) {
+      strcpy(response,"205 it had already gone"); return 0x205;
+    }
+  }
+  if (server->program) {
+    if (pipe(rpipefds) || pipe(wpipefds)) die("unable to create pipe");
+    if ((c1= fork()) == -1) die("unable to fork");
+    if (!c1) {
+      close(0); close(1); dup(wpipefds[0]); dup(rpipefds[1]);
+      close(wpipefds[0]); close(wpipefds[1]);
+      close(rpipefds[0]); close(rpipefds[1]);
+      signal(SIGPIPE,SIG_DFL);
+      if (lastdoneauth) setenv("NNTPMERGE_AUTHD_AS",lastdoneauth,1);
+      execl(server->hostname,server->hostname,(char*)0);
+      printf("400 exec %s: %s\r\n",server->hostname,strerror(errno));
+      exit(1);
+    }
+    rfd= rpipefds[0]; close(rpipefds[1]);
+    wfd= wpipefds[1]; close(wpipefds[0]);
+  } else {
+    he= gethostbyname(server->hostname);
+    if (!he) {
+      sprintf(response,"503 unable to find address of %.100s.",server->hostname);
+      return 0x503;
+    }
+    if (he->h_addrtype != AF_INET) {
+      sprintf(response,"503 address of %.100s is of unknown type 0x%x.",
+              server->hostname,he->h_addrtype);
+      return 0x503;
+    }
+    for (alist= he->h_addr_list; *alist; alist++) {
+      rfd= socket(PF_INET,SOCK_STREAM,0);
+      if (rfd == -1) die("unable to create TCP socket");
+      memset(&sin,0,sizeof(sin));
+      sin.sin_family= AF_INET;
+      sin.sin_addr= *(struct in_addr*)*alist;
+      sin.sin_port= htons(server->port);
+      if (!connect(rfd,(struct sockaddr*)&sin,sizeof(sin))) break;
+      sprintf(response,"503 unable to connect to %.100s (%.50s:%u): %.100s.",
+              server->hostname, inet_ntoa(sin.sin_addr), server->port, strerror(errno));
+      close(rfd);
+    }
+    if (!*alist) return 0x503;
+    wfd= dup(rfd);
+    if (wfd < 0) die("failed to dup socket fd");
+  }
+  server->rfile= fdopen(rfd,"r");
+  server->wfile= fdopen(wfd,"w");
+  if (!server->rfile || !server->wfile) die("failed to fdopen");
+  if (setvbuf(server->rfile,0,_IOLBF,0) || setvbuf(server->wfile,0,_IOLBF,0))
+    die("failed to setvbuf linebuf");
+  if (!fgets(response,MAX_RESPONSE+3,server->rfile)) {
+    sprintf(response,"503 failed to read from server %.100s: %.100s.",
+            server->hostname,strerror(errno));
+  }
+  if (!stripcommand(response)) {
+    sprintf(response,"503 server %.100s is spouting null garbage.",server->hostname);
+    closeserver(server); return 0x503;
+  }
+  if (!strncmp(response,"480",3) &&
+      response[3] == ' ' && (p= strchr(response+4,' ')) &&
+      ((*p=0), (parsehex(response+4,message,17) == 16)) &&
+      (file= fopen(SESAMEFILE,"r"))) {
+    do {
+      if (!fgets(temp,sizeof(temp),file)) {
+        if (ferror(file)) {
+          sprintf(response,"503 error reading sesame file: %.100s",strerror(errno));
+        } else {
+          sprintf(response,"503 no sesame for server nicknamed %.100s",server->nickname);
+        }
+        fclose(file); closeserver(server); return 0x503;
+      }
+    } while (!stripcommand(temp) || !*temp || *temp == '#' ||
+             !(p= strchr(temp,' ')) || ((*p++= 0), strcmp(temp,server->nickname)));
+    fclose(file); l= strlen(p);
+    if (l>MAX_SECRET) {
+      sprintf(response,"503 sesame file secret for %.100s is too long",server->nickname);
+      closeserver(server); return 0x503;
+    }
+    memcpy(message+16,p,l);
+
+    MD5Init(&md5ctx);
+    MD5Update(&md5ctx,message,16+l);
+    MD5Final(cryptresponse,&md5ctx);
+
+    fprintf(server->wfile,"PASS ");
+    for (n=0; n<16; n++) {
+      fprintf(server->wfile,"%s%02x", n?":":"", cryptresponse[n]);
+    }
+    fprintf(server->wfile,"\r\n");
+    if (fflush(server->wfile) == EOF || ferror(server->wfile)) {
+      sprintf(response,"503 error sending sesame response to %.100s: %.100s",
+              server->hostname,strerror(errno));
+      closeserver(server); return 0x503;
+    }
+    if (!fgets(response,MAX_RESPONSE+3,server->rfile)) {
+      sprintf(response,"503 error reading from %.100s after sesame: %.100s.",
+              server->hostname,strerror(errno));
+      closeserver(server); return 0x503;
+    }
+  }
+  if (!strncmp(response,"400",3)) goto server_is_unavailable;
+  if (response[0] != '2') {
+    strcpy(temp,response);
+    stripcommand(temp);
+    sprintf(response,"503 server %.100s refuses to talk: %.100s",server->hostname,temp);
+    closeserver(server); return 0x503;
+  }
+  if (server->send) {
+    if (fprintf(server->wfile,"%s\r\n",server->send) == EOF ||
+        fflush(server->wfile) == EOF) {
+      sprintf(response,"503 error sending %.50s to %.100s: %.100s",
+              server->send,server->hostname,strerror(errno));
+      closeserver(server); return 0x503;
+    }
+    if (!fgets(response,MAX_RESPONSE+3,server->rfile)) {
+      sprintf(response,"503 error reading from %.100s after %.50s: %.100s.",
+              server->hostname,server->send,strerror(errno));
+      closeserver(server); return 0x503;
+    }
+  }
+  if (!(flags & scf_nogroup) && currentgroup && currentgroup->readfrom == server) {
+    if (fprintf(server->wfile,"GROUP %s\r\n",currentgroupname) == EOF ||
+        fflush(server->wfile) == EOF) {
+      sprintf(response,"503 error sending GROUP to %.100s: %.100s",
+            server->hostname,strerror(errno));
+      closeserver(server); return 0x503;
+    }
+    if (!fgets(response,MAX_RESPONSE+3,server->rfile)) {
+      sprintf(response,"503 error reading from %.100s after GROUP: %.100s.",
+              server->hostname,strerror(errno));
+      closeserver(server); return 0x503;
+    }
+  }
+  if (fprintf(server->wfile,"%s\r\n",command) == EOF ||
+      fflush(server->wfile) == EOF) {
+    sprintf(response,"503 error sending to %.100s: %.100s",
+            server->hostname,strerror(errno));
+    closeserver(server); return 0x503;
+  }
+  if (!fgets(response,MAX_RESPONSE+3,server->rfile)) {
+    sprintf(response,"503 error reading from %.100s: %.100s.",server->hostname,
+            strerror(errno));
+    closeserver(server); return 0x503;
+  }
+  if (!stripcommand(response)) {
+    sprintf(response,"503 server %.100s is replying with null garbage.",
+            server->hostname);
+    closeserver(server); return 0x503;
+  }
+ message_sent_response_ok:
+  if (!decoderesponse(response,&rv,server)) return 0x503;
+  if (rv != 0x400) return rv;
+ server_is_unavailable:
+  strcpy(temp,response);
+  sprintf(response,"503 server %.100s unavailable: %.200s",
+          server->hostname,temp);
+  closeserver(server); return 0x503;
+}
+
+static int adjust(int original, int offset) {
+  if (offset < 0 && original < -offset) return 0;
+  return original+offset;
+}
+
+void serverdataerr(struct serverinfo *si) {
+  if (ferror(si->rfile)) {
+    printf("\r\n\r\n*** problem detected by nntp-merge:\r\n"
+           "error reading data from server %s: %s\r\n",
+           si->hostname, strerror(errno));
+  } else {
+    printf("\r\n\r\n*** problem detected by nntp-merge:\r\n"
+           "server %s closed connection while sending data\r\n",
+           si->hostname);
+  }
+  printf("closing connection on you, sorry.\r\n");
+  exit(0);
+}
+
+static int noargs(const char *arg) {
+  if (!*arg) return 1;
+  printf("501 no arguments please.\r\n");
+  return 0;
+}
+
+static void cmd_unimplemented(char *arg, const struct cmdinfo *cip) { \
+  printf("500 %s not implemented.\r\n",cip->command); \
+}
+
+static void cmd_quit(char *arg, const struct cmdinfo *cip) {
+  char responsebuf[MAX_RESPONSE+3];
+  struct serverinfo *si;
+
+  if (!noargs(arg)) return;
+  for (si= servers; si; si= si->next) {
+    if (!si->rfile) continue;
+    servercommand(si,"QUIT",responsebuf,scf_norecover);
+    fclose(si->wfile); fclose(si->rfile); /* we're not interested in the response */
+  }
+  printf("205 goodbye.\r\n");
+  exit(0);
+}
+
+static void cmd_date(char *arg, const struct cmdinfo *cip) {
+  char responsebuf[MAX_RESPONSE+3];
+  struct tm *brokendown;
+  char buf[100];
+  int n, rcode;
+  time_t now;
+  struct serverinfo *si;
+
+  if (!noargs(arg)) return;
+  time(&now);
+  brokendown= gmtime(&now); if (!brokendown) die("unable to break down date/time");
+  n= strftime(buf,sizeof(buf)-1,"%Y%m%d%H%M%S",brokendown);
+  if (n <= 0 || n >= sizeof(buf)-1 || strlen(buf) != 14)
+    die("unable to format date/time");
+  for (si= servers; si; si= si->next) {
+    if (!si->searchthis) continue;
+    rcode= servercommand(si,"DATE",responsebuf,0);
+    if (rcode == 0x111 && responsebuf[3]==' ' &&
+        strspn(responsebuf+4,"0123456789")==14 &&
+        responsebuf[18]==0 &&
+        strcmp(responsebuf+4,buf) < 0)
+      strcpy(buf,responsebuf+4);
+  }
+  printf("111 %s\r\n",buf);
+}
+
+static void cmd_noop(char *arg, const struct cmdinfo *cip) {
+  if (!noargs(arg)) return;
+  printf("200 doing that has no effect.\r\n");
+}
+
+static void cmd_slave(char *arg, const struct cmdinfo *cip) {
+  if (!noargs(arg)) return;
+  printf("202 bind me, whip me.\r\n");
+}
+
+static void cmd_ihave(char *arg, const struct cmdinfo *cip) {
+   printf("502 please don't feed me.\r\n");
+}
+
+static void cmd_newnews(char *arg, const struct cmdinfo *cip) {
+   printf("502 get a proper feed, slurping through this merger is silly.\r\n");
+}
+
+static void cmd_mode(char *arg, const struct cmdinfo *cip) {
+  if (!strcasecmp(arg,"READER")) {
+    printf("200 %s that was uncalled-for.\r\n",myfqdn);
+  } else {
+    printf("501 you want me to do *what* ?\r\n");
+  }
+}
+
+static void authrequired(void) {
+  if (lastdoneauth) {
+    printf("480 the sight of %s is but dim.\r\n",lastdoneauth);
+  } else {
+    printf("480 identify yourself !  friend or foe ?\r\n");
+  }
+}
+
+int stillrestricted(struct permission **pip) {
+  if (!*pip) return 0;
+  if (!(*pip)->authd) return 1;
+  *pip= 0; return 0;
+}
+
+static int groupreadable(struct groupinfo *gi) {
+  if (!stillrestricted(&gi->restrictto) ||
+      !stillrestricted(&gi->readonlyto)) return 1;
+  authrequired();
+  return 0;
+}
+
+static void cmd_group(char *arg, const struct cmdinfo *cip) {
+  struct groupinfo *gi;
+  int estcount, first, last, startfrom;
+  char commandbuf[MAX_COMMAND+3];
+  char responsebuf[MAX_RESPONSE+3];
+  int rcode;
+  
+  if (strlen(arg) > MAX_COMMAND-12) {
+    printf("501 too long.\r\n");
+    return;
+  }
+  if (*arg) {
+    gi= findgroup(arg); if (!groupreadable(gi)) return;
+  } else {
+    printf("501 which group, then ?\r\n");
+    return;
+  }
+  
+  sprintf(commandbuf,"%s %s",cip->command,arg);
+  rcode= servercommand(gi->readfrom,commandbuf,responsebuf,scf_nogroup);
+  if ((rcode & 0xf00) != 0x200) {
+    printf("%s\r\n",responsebuf);
+    return;
+  } else if (gi->offset) {
+    startfrom= -1;
+    if (sscanf(responsebuf,"211 %d %d %d %n", &estcount,&first,&last,&startfrom) != 3 ||
+        startfrom == -1) {
+      printf("503 %s said (after GROUP): %s\r\n",gi->readfrom->hostname,responsebuf);
+      closeserver(gi->readfrom); return;
+    }
+    printf("211 %d %d %d %s\r\n",
+           estcount, adjust(first,gi->offset), adjust(last,gi->offset),
+           responsebuf + startfrom);
+  } else {
+    printf("%s\r\n",responsebuf);
+  }
+  currentgroup= gi;
+  strcpy(currentgroupname,arg);
+}
+
+int copydatafile(FILE *from, FILE *to) {
+  /* `to' may be null */
+  int stopstate, c;
+  
+  stopstate= 1; /* 0: middle of line; 1: start of line; 2: after `.' */
+  while ((c= getc(from)) != EOF) {
+    if (to) putc(c,to);
+    if (c == '\r') continue;
+    switch (stopstate) {
+    case 0:
+      if (c == '\n') stopstate= 1;
+      continue;
+    case 1:
+      stopstate= (c == '.') ? 2 : (c == '\n') ? 1 : 0;
+      continue;
+    case 2:
+      if (c == '\n') return 1;
+      stopstate= 0;
+      continue;
+    }
+  }
+  return 0;
+}  
+
+static void copydata(struct serverinfo *from, FILE *to) {
+  if (!copydatafile(from->rfile,to)) serverdataerr(from);
+}
+
+static void cmd_listgroup(char *arg, const struct cmdinfo *cip) {
+  struct groupinfo *gi;
+  char commandbuf[MAX_COMMAND+3];
+  char responsebuf[MAX_RESPONSE+3];
+  long an;
+  char *p;
+  int rcode;
+  
+  if (strlen(arg) > MAX_COMMAND-12) {
+    printf("501 too long.\r\n");
+    return;
+  }
+  if (*arg) {
+    gi= findgroup(arg); if (!groupreadable(gi)) return;
+  } else if (!currentgroup) {
+    printf("412 list of articles in which group ?\r\n");
+    return;
+  } else {
+    gi= currentgroup;
+  }
+  
+  sprintf(commandbuf,"%s %s",cip->command,arg);
+  rcode= servercommand(gi->readfrom,commandbuf,responsebuf,scf_nogroup);
+  if ((rcode & 0xf00) != 0x200) {
+    printf("%s\r\n",responsebuf);
+    return;
+  }
+  if (rcode != 0x211) {
+    printf("503 %s said (after %s): %s\r\n",
+           gi->readfrom->hostname,cip->command,responsebuf);
+    return;
+  }
+  printf("211 article list:\r\n");
+  if (!gi->offset) {
+    copydata(gi->readfrom,stdout);
+  } else {
+    for (;;) {
+      if (!fgets(responsebuf,MAX_RESPONSE+3,gi->readfrom->rfile))
+        serverdataerr(gi->readfrom);
+      if (!strcmp(responsebuf,".\r\n") || !strcmp(responsebuf,".\n"))
+        break;
+      an= strtol(responsebuf,&p,10);
+      printf("%d%s",adjust(an,gi->offset),p);
+    }
+    printf(".\r\n");
+  }
+  currentgroup= gi;
+}
+
+static void pserver(struct serverinfo *si) {
+  printf("  %s= %s:%u %s (%s)\r\n",
+         si->nickname,
+         si->hostname, si->port,
+         si->rfile ? "open" : "closed",
+         si->send ? si->send : "-");
+}
+
+static void cmd_xmergeinfo(char *arg, const struct cmdinfo *cip) {
+  struct groupinfo *gi;
+  int i;
+  
+  if (strlen(arg) > MAX_COMMAND-6) {
+    printf("501 too long.\r\n");
+    return;
+  }
+  gi= findgroup(arg);
+  printf("100 %s\r\n read:\r\n",arg);
+  pserver(gi->readfrom);
+  printf(" post:\r\n");
+  for (i= 0; gi->postto[i]; i++) pserver(gi->postto[i]);
+  printf(" offset %d\r\n"
+         " auth %s / read-auth %s\r\n"
+         ".\r\n",
+         gi->offset,
+         gi->restrictto && gi->restrictto->name ? gi->restrictto->name : "<none>",
+         gi->readonlyto && gi->readonlyto->name ? gi->readonlyto->name : "<none>");
+}
+
+static void cmd_help(char *arg, const struct cmdinfo *cip);
+
+static int articleselectresponse(char responsebuf[MAX_RESPONSE+3], char *typecharp,
+                                 struct serverinfo *si, const struct cmdinfo *cip,
+                                 char modifiedresponse[MAX_RESPONSE+3]) {
+  int startfrom, ran;
+  
+  startfrom= -1;
+  if (sscanf(responsebuf,"22%c %d %n",typecharp,&ran,&startfrom) != 2 ||
+      startfrom == -1 || !strchr("0123",*typecharp)) {
+    printf("503 %s said (after %s): %s\r\n",
+           si->hostname, cip->command, responsebuf);
+    return 0;
+  }
+  ran= (currentgroup && si == currentgroup->readfrom)
+    ? adjust(ran,currentgroup->offset) : 0;
+  sprintf(modifiedresponse,"22%c %d %s",*typecharp,ran,responsebuf+startfrom);
+  return 1;
+}
+
+static void cmd_last_next(char *arg, const struct cmdinfo *cip) {
+  int rcode;
+  char responsebuf[MAX_RESPONSE+3];
+  char modresponse[MAX_RESPONSE+3];
+  char dummy;
+  
+  if (!noargs(arg)) return;
+  if (!currentgroup) {
+    printf("412 use GROUP and ARTICLE first.\r\n");
+    return;
+  }
+  rcode= servercommand(currentgroup->readfrom,cip->command,responsebuf,0);
+  if ((rcode & 0xf00) != 0x200) {
+    printf("%s\r\n",responsebuf); return;
+  }
+  articleselectresponse(responsebuf,&dummy,currentgroup->readfrom,cip,modresponse);
+  printf("%s\r\n",modresponse);
+}
+
+static void processxref(char *linebufplus6, int n, FILE *writeto,
+                        struct serverinfo *gotfrom) {
+  char *space, *cpos, *colon;
+  struct groupinfo *gi;
+  unsigned long an;
+  
+  linebufplus6[n++]= ' ';
+  linebufplus6[n]= 0;
+  cpos= linebufplus6;
+  space= strchr(cpos,' ');
+  if (!space) return;
+  *space++= 0;
+  fprintf(writeto,"%s",myxref);
+  cpos= space;
+  while ((space= strchr(cpos,' '))) {
+    *space++= 0;
+    colon= strrchr(cpos,':');
+    if (colon) {
+      *colon++= 0;
+      gi= findgroup(cpos);
+      if (gi->readfrom != gotfrom) {
+        cpos= space; continue;
+      }
+      fprintf(writeto," %s:",cpos);
+      if (gi->offset && isdigit(*colon)) {
+        an= strtol(colon,&colon,10);
+        fprintf(writeto,"%d", adjust(an,gi->offset));
+      }
+      fputs(colon,writeto);
+    } else {
+      if (*cpos) putc(' ',writeto);
+      fputs(cpos,writeto);
+    }
+    cpos= space;
+  }
+}
+
+static void cmd_article(char *arg, const struct cmdinfo *cip) {
+  struct serverinfo *si;
+  struct groupinfo *gi;
+  char commandbuf[MAX_COMMAND+40+3];
+  char responsebuf[MAX_RESPONSE+3];
+  char modresponse[MAX_RESPONSE+3];
+  char linebuf[MAX_XREFLINE+3];
+  char typechar;
+  unsigned long an;
+  int rcode, n, c, realeinfo;
+  char *p;
+  const char *realcommand;
+  int checkpermission= 0;
+  FILE *file;
+
+  realcommand= cip->command;
+  if (*arg == '<') {
+    checkpermission= 1;
+    switch (cip->einfo) {
+    case 0:  realcommand= "HEAD";       break;
+    case 2:  realcommand= "ARTICLE";    break;
+    }
+    sprintf(commandbuf,"%s %s",realcommand,arg);
+    rcode= 0x423;
+    for (si= servers; si; si= si->next) {
+      if (!si->searchthis) continue;
+      rcode= servercommand(si,commandbuf,responsebuf,0);
+      if ((rcode & 0xf00) == 0x200) break;
+    }
+  } else if (isdigit(*arg) || !*arg) {
+    if (!currentgroup) {
+      printf("412 use GROUP first.\r\n");
+      return;
+    }
+    if (*arg) {
+      an= strtol(arg,&p,10);
+      if (*p) { printf("501 bad article number.\r\n"); return; }
+      if (an < currentgroup->offset)
+        printf("423 offset makes number negative, so no such article.\r\n");
+      sprintf(commandbuf,"%s %lu",cip->command,an - currentgroup->offset);
+    } else {
+      sprintf(commandbuf,"%s",cip->command);
+    }
+    si= currentgroup->readfrom;
+    rcode= servercommand(si,commandbuf,responsebuf,0);
+  } else {
+    printf("501 optional arg must be message-id or article number.\r\n");
+    return;
+  }
+  if ((rcode & 0xff0) != 0x220) {
+    printf("%s\r\n",responsebuf);
+    return;
+  }
+  switch (rcode & 0x00f) {
+  case 0:   realeinfo= 03;  break;
+  case 1:   realeinfo= 01;  break;
+  case 2:   realeinfo= 02;  break;
+  case 3:   realeinfo= 00;  break;
+  default:  realeinfo= -1;
+  }
+  if (realeinfo == (cip->einfo | 01)) {
+    switch (cip->einfo) {
+    case 0:  responsebuf[2]= '3';    break;
+    case 2:  responsebuf[2]= '2';    break;
+    }
+  } else if (realeinfo != cip->einfo) {
+    printf("503 %s gave bad code (in response to %s): %s\r\n",
+           si->hostname,realcommand,responsebuf);
+    closeserver(si);
+    return;
+  }
+    
+  if (!articleselectresponse(responsebuf,&typechar,si,cip,modresponse)) return;
+  if (checkpermission) {
+    file= tmpfile(); if (!file) die("failed to create temp file");
+  } else {
+    printf("%s\r\n",modresponse);
+    if (!realeinfo) return;
+    file= stdout;
+  }
+  if (realeinfo & 01) {
+    for (;;) {
+      if (!fgets(linebuf,MAX_XREFLINE+3,si->rfile)) serverdataerr(si);
+      n= strlen(linebuf);
+      if (n==0) continue;
+      while (n>0 && ((c= linebuf[n-1]) == '\n' || c == '\r')) n--;
+      linebuf[n]= 0;
+      if (!strcmp(linebuf,".")) { fputs(".\r\n",file); break; }
+      if ((cip->einfo & 01) && !strncasecmp(linebuf,"Xref: ",6)) {
+        fprintf(file,"Xref: ");
+        processxref(linebuf+6,n-6,file,si);
+        fputs("\r\n",file);
+        continue;
+      }
+      if (cip->einfo & 01) fprintf(file,"%s\r\n",linebuf);
+      if (checkpermission && !strncasecmp(linebuf,"Newsgroups: ",12)) {
+        p= strtok(linebuf," "); assert(p);
+        while ((p= strtok(0,","))) {
+          gi= findgroup(p);
+          if (!stillrestricted(&gi->restrictto) ||
+              !stillrestricted(&gi->readonlyto)) break;
+        }
+        if (!p) checkpermission= -1; /* Don't return, we must clean up &c */
+      }
+      if (n == 0 && realeinfo == 03) break; /* end of header, go on to body */
+      continue;
+    }
+  }
+  if (realeinfo & 02) {
+    /* process body */
+    copydata(si,file);
+  }
+
+  switch (checkpermission) {
+  case 0:
+    return;
+  case 1:
+    if (ferror(file) && fflush(file)) die("error writing article temp file");
+    printf("%s\r\n",modresponse);
+    if (!cip->einfo) { fclose(file); return; }
+    if (fseek(file,0,SEEK_SET)) die("unable to rewind article temp file");
+    while ((c= getc(file)) != EOF) putchar(c);
+    if (ferror(file)) die("unable to read article temp file");
+    fclose(file);
+    return;
+  case -1:
+    fclose(file);
+    authrequired();
+    return;
+  }
+}
+
+struct listwhatinfo {
+  const char *name;
+  void (*call)(const struct listwhatinfo *lwi, const char *cachename);
+  int mustall;
+  const char *what;
+  int einfo;
+};
+
+static void lwc_bynewsgroup(const struct listwhatinfo *lwi, const char *cachename) {
+  struct serverinfo *si;
+  struct groupinfo *gi;
+  char fnbuf[250];
+  char linebuf[MAX_RESPONSE+3];
+  FILE *file;
+  long an1, an2;
+  char *space1, *space2, *space3;
+
+  for (si= servers; si; si= si->next) {
+    if (!si->searchthis) continue;
+    sprintf(fnbuf,"%.100s:%.100s",si->nickname,cachename);
+    file= fopen(fnbuf,"r");
+    if (!file) {
+      if (lwi->mustall) die("unable to open list file when spouting");
+      continue;
+    }
+    while (fgets(linebuf,MAX_RESPONSE+3,file)) {
+      gi= findgroup(linebuf);
+      if (gi->readfrom != si) continue;
+      if (lwi->einfo && gi->offset &&
+          (space1= strchr(linebuf,' ')) &&
+          ((an1= strtol(space1+1,&space2,10)), *space2 == ' ') &&
+          ((an2= strtol(space2+1,&space3,10)), *space3 == ' ')) {
+        *space1= 0;
+        printf("%s %d %d %s",linebuf,
+               adjust(an1,gi->offset),adjust(an2,gi->offset),space3+1);
+      } else {
+        fputs(linebuf,stdout);
+      }
+    }
+    if (ferror(file)) die("read error on list file");
+    fclose(file);
+  }
+  return;
+}
+
+static void lwc_fixed(const struct listwhatinfo *lwi, const char *cachename) {
+  char *pathname;
+                                                 pathname= 
+  FILE *f= fopen(
+  char *p, tc;
+  struct disdone { struct disdone *next; char *name; } *done, *search, *tmp;
+  struct serverinfo *si;
+  char fnbuf[250];
+  char linebuf[MAX_RESPONSE+3];
+  FILE *file;
+
+  done= 0;
+  for (si= servers; si; si= si->next) {
+    if (!si->searchthis) continue;
+    sprintf(fnbuf,"%.100s:%.100s",si->nickname,cachename);
+    file= fopen(fnbuf,"r");
+    if (!file) continue;
+    while (fgets(linebuf,MAX_RESPONSE+3,file)) {
+      p= linebuf; while (*p && !isspace(*p)) p++;
+      tc= *p; *p= 0;
+      for (search= done;
+           search && strcasecmp(linebuf,search->name);
+           search= search->next);
+      if (search) continue;
+      tmp= xmalloc(sizeof(struct disdone));
+      tmp->name= xstrdup(linebuf);
+      tmp->next= done;
+      done= tmp;
+      *p= tc;
+      fputs(linebuf,stdout);
+    }
+    if (ferror(file)) die("read error on list file");
+    fclose(file);
+  }
+  for (search= done; search; search= tmp) {
+    tmp= search->next; free(search->name); free(search);
+  }
+  return;
+} 
+
+static void lwc_overview(const struct listwhatinfo *lwi, const char *cachename) {
+  fputs("Subject:\r\n"
+       "From:\r\n"
+       "Date:\r\n"
+       "Message-ID:\r\n"
+       "References:\r\n"
+       "Bytes:\r\n"
+       "Lines:\r\n"
+       "Xref:full\r\n", stdout);
+}
+
+static const struct listwhatinfo listwhatinfos[]= {
+  { "",               lwc_bynewsgroup,    1,  "active file",                1   },
+  { "active",         lwc_bynewsgroup,    1,  "active file",                1   },
+  { "active.times",   lwc_bynewsgroup,    1,  "newsgroups' creation info",  0   },
+  { "newsgroups",     lwc_bynewsgroup,    0,  "newsgroup titles",           0   },
+  { "subscriptions",  lwc_bynewsgroup,    0,  "default subscription list",  0   },
+  { "distributions",  lwc_distributions,  0,  "distributions available"         },
+  { "overview.fmt",   lwc_overview,      -1,  "field list"                      },
+  {  0                                                                          }
+};
+
+static int copydatanodot(struct serverinfo *si, FILE *file) {
+  int c;
+  
+  while ((c= getc(si->rfile)) != EOF) {
+    if (c == '.') {
+      c= getc(si->rfile);
+      if (c == '\r')  {
+        c= getc(si->rfile);
+        if (c == '\n')
+          return 1;
+        fputs(".\r",file);
+      } else if (c == '\n') {
+        return 1;
+      } else {
+        putc('.',file);
+      }
+    }
+    while (c != EOF && c != '\r' && c != '\n') {
+      putc(c,file);
+      c= getc(si->rfile);
+    }
+    if (c == EOF) break;
+    putc(c,file);
+  }
+  if (ferror(si->rfile)) {
+    printf("503 error getting data from %s: %s\r\n",si->hostname,strerror(errno));
+  } else {
+    printf("503 connection closed during data transfer from %s\r\n",si->hostname);
+  }
+  return 0;
+}
+
+static void cmd_list(char *arg, const struct cmdinfo *cip) {
+  const struct listwhatinfo *lwi;
+  int rcode, c;
+  char commandbuf[MAX_COMMAND+40+3];
+  char responsebuf[MAX_RESPONSE+3];
+  char ufnbuf[250], nfnbuf[250], fnbuf[250];
+  struct stat stab;
+  FILE *file;
+  struct serverinfo *si;
+  const char *cachename;
+  
+  for (lwi= listwhatinfos; lwi->name && strcasecmp(arg,lwi->name); lwi++);
+  if (!lwi->name) {
+    printf("501 LIST %s not available, sorry.\r\n",arg);
+    return;
+  }
+  if (lwi->mustall >= 0) {
+    cachename= lwi->name; if (!*cachename) cachename= (lwi+1)->name;
+  } else {
+    cachename= 0;
+  }
+  if (lwi->mustall > 0) {
+    sprintf(ufnbuf,"unavailable:%.100s",arg);
+    file= fopen(ufnbuf,"r");
+    if (file) {
+      printf("501 %s not available because not supported by ",arg);
+      while ((c= getc(file)) != EOF) { if (!isspace(c)) putchar(c); }
+      printf(".\r\n");
+      fclose(file);
+      return;
+    }
+  }
+  for (si= servers; lwi->mustall >= 0 && si; si= si->next) {
+    if (!si->searchthis) continue;
+    sprintf(commandbuf,"LIST %.100s",lwi->name);
+    rcode= servercommand(si,commandbuf,responsebuf,0);
+    sprintf(fnbuf,"%.100s:%.100s",si->nickname,cachename);
+    if (rcode == 0x500 || rcode == 0x501) {
+      if (lwi->mustall) {
+        printf("501 just discovered: %s doesn't support it - it says: %s\r\n",
+               si->hostname,responsebuf);
+        sprintf(nfnbuf,"~%s~%ld",ufnbuf,(long)getpid());
+        file= fopen(nfnbuf,"w"); if (!file) die("unable to create tmp unsup list file");
+        if (fprintf(file,"%s\n",si->hostname) == EOF)
+          die("unable to write unsup list file");
+        if (fclose(file)) die("unable to close unsup list file");
+        if (rename(nfnbuf,ufnbuf)) die("unable to install unsup list file");
+        return;
+      } else {
+        if (unlink(fnbuf) && errno != ENOENT) die("unable to remove now unsup list");
+        continue;
+      }
+    }
+    if (rcode == 0x215) {
+      sprintf(nfnbuf,"~%s~%ld",fnbuf,(long)getpid());
+      file= fopen(nfnbuf,"w"); if (!file) die("unable to create tmp list file");
+      if (!copydatanodot(si,file)) {
+        fclose(file); unlink(nfnbuf); return;
+      }
+      if (ferror(file) || fclose(file)) die("unable to write tmp list file");
+      if (rename(nfnbuf,fnbuf)) die("unable to install new list file");
+    } else if (rcode == 0x503) {
+      if (lwi->mustall && stat(fnbuf,&stab)) {
+        printf("501 no can do: need all, but file for %s not accessible: %s\r\n",
+               si->hostname,strerror(errno));
+      }
+    } else {
+      printf("503 %s said (after LIST): %s\r\n",si->hostname,responsebuf);
+      return;
+    }
+  }
+  printf("215 %s follows:\r\n",lwi->what);
+  lwi->call(lwi,cachename);
+  printf(".\r\n");
+}
+
+static int unadjustnumber(char *appendto, char *from, int offset) {
+  char *p;
+  int n;
+  long an;
+
+  an= strtol(from,&p,10);
+  if (!*from || *p) {
+    printf("503 bad article range.\r\n");
+    return 0;
+  }
+  if (an > offset)
+    an-= offset;
+  else
+    an= 0;
+  n= strlen(appendto);
+  sprintf(appendto+n,"%lu",an);
+  return 1;
+}
+
+static int unadjustrange(char *appendto, char *from, int offset) {
+  char *p;
+
+  p= strchr(from,'-');
+  if (p) {
+    *p++= 0;
+    if (!unadjustnumber(appendto,from,offset)) return 0;
+    from= p;
+    strcat(appendto,"-");
+  }
+  return unadjustnumber(appendto,from,offset);
+}
+static void cmd_xover(char *arg, const struct cmdinfo *cipa) {
+  char commandbuf[MAX_COMMAND+40+3];
+  char responsebuf[MAX_RESPONSE+3];
+  char xrefbuf[MAX_XREFLINE+3];
+  char *p;
+  int rcode;
+  struct serverinfo *si;
+  int an, n, fieldn, c;
+  
+  if (!currentgroup) {
+    printf("412 overview of which group ?\r\n");
+    return;
+  }
+  strcpy(commandbuf,"XOVER ");
+  if (*arg && !unadjustrange(commandbuf,arg,currentgroup->offset)) return;
+  si= currentgroup->readfrom;
+  rcode= servercommand(si,commandbuf,responsebuf,0);
+  if ((rcode & 0xf00) != 0x200) {
+    printf("%s\r\n",responsebuf);
+    return;
+  } else if (rcode != 0x224) {
+    printf("503 %s said (after XOVER): %s\r\n",si->hostname,responsebuf);
+    return;
+  }
+  printf("%s\r\n",responsebuf);
+  for (;;) {
+    c= getc(si->rfile);
+    if (c == EOF) serverdataerr(si);
+    if (c == '.') {
+      c= getc(si->rfile); 
+      if (c == '\r') c= getc(si->rfile);
+      if (c == EOF) serverdataerr(si);
+      if (c == '\n') break;
+      /* Oh, well, it's clearly bozoid, so what if we dropped a `.' ... */
+      putchar('.');
+      putchar(c);
+    }
+    ungetc(c,si->rfile);
+    if (fscanf(si->rfile,"%d",&an) != 1) {
+      while ((c= getc(si->rfile)) != EOF && c != '\n');
+      if (c == EOF) serverdataerr(si);
+      continue;
+    }
+    printf("%d",adjust(an,currentgroup->offset));
+    p= xrefbuf; n= 0; fieldn= 0;
+    for (;;) {
+      c= getc(si->rfile);
+      if (c == EOF) serverdataerr(si);
+      if (c == '\r') continue;
+      if (c != '\t' && c != '\n' && n < MAX_XREFLINE) {
+        *p++= c; n++;
+        continue;
+      }
+      if (c == '\t' || c == '\n') fieldn++;
+      *p++= 0;
+      if (fieldn >= 8 && !strncasecmp("Xref: ",xrefbuf,6)) {
+        printf("Xref: ");
+        processxref(xrefbuf+6,strlen(xrefbuf)-6,stdout,si);
+      } else {
+        fputs(xrefbuf,stdout);
+      }
+      if (c == '\n') break;
+      putchar('\t');
+      p= xrefbuf; n= 0;
+    }
+    printf("\r\n");
+  }
+  printf(".\r\n");
+  return;
+}
+
+static void cmd_xhdr(char *arg, const struct cmdinfo *cipa) {
+  char commandbuf[MAX_COMMAND+40+3];
+  char responsebuf[MAX_RESPONSE+3];
+  char linebuf[MAX_XREFLINE+40+3];
+  char *p, *q;
+  int rcode, isxref;
+  struct serverinfo *si;
+  long an;
+  
+  if (!currentgroup) {
+    printf("412 headers in which group ?\r\n");
+    return;
+  }
+  p= strchr(arg,' ');
+  if (!p) {
+    printf("501 need header and range.\r\n");
+    return;
+  }
+  *p++= 0;
+  sprintf(commandbuf,"XHDR %s ",arg);
+  if (!unadjustrange(commandbuf,p,currentgroup->offset)) return;
+  si= currentgroup->readfrom;
+  rcode= servercommand(si,commandbuf,responsebuf,0);
+  if ((rcode & 0xf00) != 0x200) {
+    printf("%s\r\n",responsebuf);
+    return;
+  } else if (rcode != 0x221) {
+    printf("503 %s said (after XHDR): %s\r\n",si->hostname,responsebuf);
+    return;
+  }
+  printf("%s\r\n",responsebuf);
+  isxref= !strcasecmp(arg,"Xref");
+  if (!isxref && !currentgroup->offset) {
+    copydata(si,stdout);
+  } else {
+    for (;;) {
+      if (!fgets(linebuf,MAX_XREFLINE+40,si->rfile)) serverdataerr(si);
+      if (!stripcommand(linebuf)) continue;
+      if (!strcmp(linebuf,".")) break;
+      q= linebuf;
+      if (currentgroup->offset) {
+        an= strtol(linebuf,&q,10);
+        printf("%d",adjust(an,currentgroup->offset));
+      }
+      if (isxref && (p= strchr(q,' ')) && strcmp(p+1,"(none)")) {
+        *p++= 0;
+        printf("%s ",q);
+        processxref(p,strlen(p),stdout,si);
+      } else {
+        fputs(q,stdout);
+      }
+      fputs("\r\n",stdout);
+    }
+    printf(".\r\n");
+  }
+}
+
+static void cmd_newgroups_xgtitle(char *arg, const struct cmdinfo *cip) {
+  char commandbuf[MAX_COMMAND+40+3];
+  char responsebuf[MAX_RESPONSE+3];
+  char linebuf[MAX_XREFLINE+40+3];
+  int rcode, okrcode;
+  struct serverinfo *si, *gsi;
+
+  sprintf(commandbuf,"%s %s",cip->command,arg);
+
+  okrcode= cip->einfo ? 0x282 : 0x231;
+  if (cip->einfo && !strchr(arg,'*') && !strchr(arg,'?')) {
+    if (!*arg) {
+      if (!currentgroup) {
+        printf("412 title of which group ?\r\n");
+        return;
+      }
+      si= currentgroup->readfrom;
+    } else {
+      si= findgroup(arg)->readfrom;
+    }
+    rcode= servercommand(si,commandbuf,responsebuf,0);
+    printf("%s\r\n",responsebuf);
+    if ((rcode & 0xf00) == 0x200) copydata(si,stdout);
+    return;
+  }
+
+  for (si= servers; si; si= si->next) {
+    if (!si->searchthis) continue;
+    si->tempfile= 0;
+  }
+  for (si= servers; si; si= si->next) {
+    if (!si->searchthis) continue;
+    rcode= servercommand(si,commandbuf,responsebuf,0);
+    if (rcode != okrcode) {
+      if ((rcode & 0xf00) == 0x200) closeserver(si);
+      if (cip->einfo) continue;
+      printf("503 NEWGROUPS not available - %s: %s\r\n",si->hostname,responsebuf);
+      goto close_files_and_abort;
+    }
+    si->tempfile= tmpfile();
+    if (!si->tempfile) die("unable to create tempfile");
+    if (!copydatanodot(si,si->tempfile)) goto close_files_and_abort;
+    if (ferror(si->tempfile) || fflush(si->tempfile) || fseek(si->tempfile,0,SEEK_SET))
+      die("unable to write temp file");
+  }
+  printf("%x here you are:\r\n",okrcode);
+  for (si= servers; si; si= si->next) {
+    if (!si->searchthis) continue;
+    if (!si->tempfile) continue;
+    while (fgets(linebuf,MAX_XREFLINE+3,si->tempfile)) {
+      gsi= findgroup(linebuf)->readfrom;
+      if (gsi != si) continue;
+      fputs(linebuf,stdout);
+    }
+    if (ferror(si->tempfile)) die("read error on temp file");
+    fclose(si->tempfile); si->tempfile= 0;
+  }
+  printf(".\r\n");
+  return;
+
+ close_files_and_abort:
+  for (si= servers; si; si= si->next) {
+    if (!si->searchthis) continue;
+    if (si->tempfile) fclose(si->tempfile);
+  }
+  return;
+}
+
+static void cmd_authinfo(char *arg, const struct cmdinfo *cip) {
+  struct authdas { struct authdas *next; char *name; };
+
+  static struct authdas *alreadydone= 0;
+
+  struct authdas *asearch;
+  struct MD5Context md5ctx;
+  char *p, *claim, *here, *what;
+  FILE *file;
+  char buf[500], buf2[MAX_RESPONSE+3];
+  unsigned char message[16+MAX_SECRET], expect[16], reply[16];
+  int n, nused, matching, ifmatch;
+  struct timeval timevab;
+  unsigned long ul;
+  unsigned short us;
+  struct permission *pi;
+
+  if (strcasecmp(strtok(arg," "),"generic") ||
+      strcmp(strtok(0," "),"md5cookie1way")) {
+    printf("501 please use AUTHINFO GENERIC md5cookie1way <claim>.\r\n");
+    return;
+  }
+  claim= strtok(0," ");
+  if (strtok(0," ")) {
+    printf("501 something after claim.\r\n");
+    return;
+  }
+  for (asearch= alreadydone;
+       asearch && strcmp(asearch->name,claim);
+       asearch= asearch->next);
+  if (asearch) {
+    printf("502 you are already user/group %s; need other group for more access.\r\n",
+           claim);
+    return;
+  }
+  file= fopen("md5cookies","r");
+  if (!file) {
+    printf("503 couldn't open md5cookies file: %s\r\n",strerror(errno));
+    return;
+  }
+  matching= 0;
+  for (;;) {
+    if (!fgets(buf,sizeof(buf),file)) {
+      if (ferror(file)) {
+        printf("503 error reading md5cookies file: %s\r\n",strerror(errno));
+      } else {
+        printf("502 who did you say you were ?\r\n");
+      }
+      fclose(file);
+      return;
+    }
+    if (!stripcommand(buf) || *buf == '#' || !*buf) continue;
+    here= strtok(buf," \t");
+    if (!strcmp(buf,"@")) {
+      what= strtok(0," \t");
+      if (!strcmp(what,"allow")) {
+        ifmatch= 1;
+      } else if (!strcmp(what,"refuse")) {
+        ifmatch= 0;
+      } else {
+        continue;
+      }
+      what= strtok(0,"");
+      if (!what) continue;
+      if (fnmatch(what,theirfqdn ? theirfqdn : "_unknown_",0)) continue;
+      matching= ifmatch;
+    } else if (!strcmp(here,claim)) {
+      if (matching) break;
+    }
+  }
+  fclose(file);
+  
+  if (gettimeofday(&timevab,(void*)0)) die("unable to gettimeofday for nonce");
+  memcpy(message,&timevab.tv_sec,4);
+  ul= timevab.tv_usec; memcpy(message+4,&ul,4);
+  memcpy(message+8,&peername.sin_addr,4);
+  memcpy(message+12,&peername.sin_port,2);
+  us= getpid(); memcpy(message+14,&us,2);
+
+  p= strtok(0," \t");
+  if (!p) {
+    printf("502 md5cookies file is missing secret for you.\r\n");
+    return;
+  }
+  nused= parsehex(p,message+16,MAX_SECRET);
+
+  MD5Init(&md5ctx);
+  MD5Update(&md5ctx,message,16+nused);
+  MD5Final(expect,&md5ctx);
+  
+  printf("100 ");
+  for (n=0; n<16; n++) {
+    printf("%s%02x", n?":":"", message[n]);
+  }
+  printf("\r\n");
+  if (ferror(stdout) || fflush(stdout)) die("unable to write auth challenge");
+
+  if (!fgets(buf2,MAX_RESPONSE,stdin)) {
+    if (ferror(stdin)) die("client connection failed during crypto");
+    exit(1);
+  }
+  if (strncasecmp(buf2,"MD5 ",4)) {
+    printf("502 expecting MD5.\r\n");
+    return;
+  }
+  nused= parsehex(buf2+4,reply,17);
+  if (nused != 16) {
+    printf("502 expecting 16 pairs (got %d).\r\n",nused);
+    return;
+  }
+  if (memcmp(reply,expect,16)) {
+    printf("502 pull the other one, it's got bells on.\r\n");
+    return;
+  }
+  asearch= xmalloc(sizeof(struct authdas));
+  asearch->name= xstrdup(claim);
+  asearch->next= alreadydone;
+  alreadydone= asearch;
+  printf("281 ok");
+  while ((p= strtok(0," \t"))) {
+    printf(" %s",p);
+    for (pi= permissions; pi; pi= pi->next) {
+      if (strcmp(pi->name,p)) continue;
+      if (!pi->authd) {
+        putchar('+');
+        pi->authd= 1;
+      }
+    }
+  }
+  lastdoneauth= asearch->name;
+  printf("\r\n");
+}
+
+const struct cmdinfo cmdinfos[]= {
+  { "ARTICLE",      cmd_article,            3  },
+  { "AUTHINFO",     cmd_authinfo               },
+  { "XAUTHINFO",    cmd_authinfo               },
+  { "HEAD",         cmd_article,            1  },
+  { "BODY",         cmd_article,            2  },
+  { "STAT",         cmd_article,            0  },
+  { "GROUP",        cmd_group                  },
+  { "HELP",         cmd_help                   },
+  { "IHAVE",        cmd_ihave                  },
+  { "LAST",         cmd_last_next              },
+  { "LIST",         cmd_list                   },
+  { "NEWGROUPS",    cmd_newgroups_xgtitle,  0  },
+  { "NEWNEWS",      cmd_newnews                },
+  { "NEXT",         cmd_last_next              },
+  { "NOOP",         cmd_noop                   },
+  { "POST",         cmd_post                   },
+  { "QUIT",         cmd_quit                   },
+  { "SLAVE",        cmd_slave                  },
+  { "DATE",         cmd_date                   },
+  { "LISTGROUP",    cmd_listgroup              },
+  { "XLISTGROUP",   cmd_listgroup              },
+  { "MODE",         cmd_mode                   },
+  { "XMODE",        cmd_mode                   },
+  { "XMERGEINFO",   cmd_xmergeinfo             },
+  { "XGTITLE",      cmd_newgroups_xgtitle,  1  },
+  { "XHDR",         cmd_xhdr                   },
+  { "XOVER",        cmd_xover                  },
+
+  { "XPAT",         cmd_unimplemented          },
+  { "XPATH",        cmd_unimplemented          },
+  {  0                                         }
+};
+
+static void cmd_help(char *arg, const struct cmdinfo *cipa) {
+  /* close outstanding channels */
+  const struct cmdinfo *cip;
+  if (!noargs(arg)) return;
+  printf("100  commands are (* = implemented)\r\n");
+  for (cip= cmdinfos; cip->command; cip++) {
+    printf(" %s %s\r\n",
+           cip->call == cmd_unimplemented ? " " : "*",
+           cip->command);
+  }
+  printf(".\r\n");
+}
+
+int main(int argc, char **argv) {
+  char cmdbuf[1000];
+  int n;
+  unsigned int nu;
+  const struct cmdinfo *cip;
+  struct hostent *he;
+
+  cmdbuf[sizeof(cmdbuf)-1]= 0;
+  if (gethostname(cmdbuf,sizeof(cmdbuf)-1)) die("gethostname");
+  if (cmdbuf[sizeof(cmdbuf)-1]) die("gethostname overflow");
+  if (!(he= gethostbyname(cmdbuf))) die("gethostbyname");
+  myfqdn= xstrdup(he->h_name);
+  myxref= xstrdup(he->h_name);
+
+  if (chdir("/var/lib/news/merge")) die("chdir");
+  readconfig();
+
+  setvbuf(stdin,0,_IOLBF,0);
+  setvbuf(stdout,0,_IOFBF,10240);
+  signal(SIGPIPE,SIG_IGN);
+  nu= sizeof(peername);
+  memset(&peername,0,nu);
+  if (getpeername(0,(struct sockaddr*)&peername,&nu)) {
+    theirfqdn= "_direct_";
+  } else {
+    he= gethostbyaddr((void*)&peername.sin_addr,sizeof(peername.sin_addr),AF_INET);
+    if (he && he->h_name) theirfqdn= xstrdup(he->h_name);
+  }
+  
+  printf("200 %s nntp-merge ready (posting might be ok).\r\n",myfqdn);
+
+  for (;;) {
+    if (fflush(stdout)) die("flush stdout");
+    if (ferror(stdout) || ferror(stdin)) die("client connection died");
+    waitpid(-1,0,WNOHANG); /* ignore all children */
+    errno=0; if (!fgets(cmdbuf,sizeof(cmdbuf),stdin)) {
+      if (ferror(stdin)) die("client closed"); else exit(0);
+    }
+    if (!stripcommand(cmdbuf)) continue;
+    for (cip= cmdinfos; cip->command; cip++) {
+      n= strlen(cip->command);
+      if (!strncasecmp(cip->command,cmdbuf,n) &&
+          (!cmdbuf[n] || isspace(cmdbuf[n])))
+        break;
+    }
+    if (cip->command) {
+      while (isspace(cmdbuf[n])) n++;
+      cip->call(cmdbuf+n,cip);
+    } else {
+      printf("500 huh?\r\n");
+    }
+  }
+}
diff --git a/md5.c b/md5.c
new file mode 100644 (file)
index 0000000..46cdbe8
--- /dev/null
+++ b/md5.c
@@ -0,0 +1,236 @@
+/*
+ * This code implements the MD5 message-digest algorithm.
+ * The algorithm is due to Ron Rivest.  This code was
+ * written by Colin Plumb in 1993, no copyright is claimed.
+ * This code is in the public domain; do with it what you wish.
+ *
+ * Equivalent code is available from RSA Data Security, Inc.
+ * This code has been tested against that, and is equivalent,
+ * except that you don't need to include two pages of legalese
+ * with every copy.
+ *
+ * To compute the message digest of a chunk of bytes, declare an
+ * MD5Context structure, pass it to MD5Init, call MD5Update as
+ * needed on buffers full of bytes, and then call MD5Final, which
+ * will fill a supplied 16-byte array with the digest.
+ *
+ * Modified by Ian Jackson in 1995 so as not to use Colin Plumb's
+ * `usuals.h'.  Arranged for byteSwap to be compiled and called
+ * even on little-endian machines, as performance isn't critical
+ * here.
+ */
+
+#include <string.h>            /* for memcpy() */
+#include "md5.h"
+
+/*
+ * Note: this code is harmless but does nothing on little-endian machines.
+ */
+void
+byteSwap(UINT32 *buf, unsigned words)
+{
+       UINT8 *p = (UINT8 *)buf;
+
+       do {
+               *buf++ = (UINT32)((unsigned)p[3] << 8 | p[2]) << 16 |
+                       ((unsigned)p[1] << 8 | p[0]);
+               p += 4;
+       } while (--words);
+}
+
+/*
+ * Start MD5 accumulation.  Set bit count to 0 and buffer to mysterious
+ * initialization constants.
+ */
+void
+MD5Init(struct MD5Context *ctx)
+{
+       ctx->buf[0] = 0x67452301;
+       ctx->buf[1] = 0xefcdab89;
+       ctx->buf[2] = 0x98badcfe;
+       ctx->buf[3] = 0x10325476;
+
+       ctx->bytes[0] = 0;
+       ctx->bytes[1] = 0;
+}
+
+/*
+ * Update context to reflect the concatenation of another buffer full
+ * of bytes.
+ */
+void
+MD5Update(struct MD5Context *ctx, UINT8 const *buf, unsigned len)
+{
+       UINT32 t;
+
+       /* Update byte count */
+
+       t = ctx->bytes[0];
+       if ((ctx->bytes[0] = t + len) < t)
+               ctx->bytes[1]++;        /* Carry from low to high */
+
+       t = 64 - (t & 0x3f);    /* Space available in ctx->in (at least 1) */
+       if (t > len) {
+               memcpy((UINT8 *)ctx->in + 64 - t, buf, len);
+               return;
+       }
+       /* First chunk is an odd size */
+       memcpy((UINT8 *)ctx->in + 64 - t, buf, t);
+       byteSwap(ctx->in, 16);
+       MD5Transform(ctx->buf, ctx->in);
+       buf += t;
+       len -= t;
+
+       /* Process data in 64-byte chunks */
+       while (len >= 64) {
+               memcpy(ctx->in, buf, 64);
+               byteSwap(ctx->in, 16);
+               MD5Transform(ctx->buf, ctx->in);
+               buf += 64;
+               len -= 64;
+       }
+
+       /* Handle any remaining bytes of data. */
+       memcpy(ctx->in, buf, len);
+}
+
+/*
+ * Final wrapup - pad to 64-byte boundary with the bit pattern 
+ * 1 0* (64-bit count of bits processed, MSB-first)
+ */
+void
+MD5Final(UINT8 digest[16], struct MD5Context *ctx)
+{
+       int count = ctx->bytes[0] & 0x3f;       /* Number of bytes in ctx->in */
+       UINT8 *p = (UINT8 *)ctx->in + count;
+
+       /* Set the first char of padding to 0x80.  There is always room. */
+       *p++ = 0x80;
+
+       /* Bytes of padding needed to make 56 bytes (-8..55) */
+       count = 56 - 1 - count;
+
+       if (count < 0) {        /* Padding forces an extra block */
+               memset(p, 0, count + 8);
+               byteSwap(ctx->in, 16);
+               MD5Transform(ctx->buf, ctx->in);
+               p = (UINT8 *)ctx->in;
+               count = 56;
+       }
+       memset(p, 0, count);
+       byteSwap(ctx->in, 14);
+
+       /* Append length in bits and transform */
+       ctx->in[14] = ctx->bytes[0] << 3;
+       ctx->in[15] = ctx->bytes[1] << 3 | ctx->bytes[0] >> 29;
+       MD5Transform(ctx->buf, ctx->in);
+
+       byteSwap(ctx->buf, 4);
+       memcpy(digest, ctx->buf, 16);
+       memset(ctx, 0, sizeof(ctx));    /* In case it's sensitive */
+}
+
+#ifndef ASM_MD5
+
+/* The four core functions - F1 is optimized somewhat */
+
+/* #define F1(x, y, z) (x & y | ~x & z) */
+#define F1(x, y, z) (z ^ (x & (y ^ z)))
+#define F2(x, y, z) F1(z, x, y)
+#define F3(x, y, z) (x ^ y ^ z)
+#define F4(x, y, z) (y ^ (x | ~z))
+
+/* This is the central step in the MD5 algorithm. */
+#define MD5STEP(f,w,x,y,z,in,s) \
+        (w += f(x,y,z) + in, w = (w<<s | w>>(32-s)) + x)
+
+/*
+ * The core of the MD5 algorithm, this alters an existing MD5 hash to
+ * reflect the addition of 16 longwords of new data.  MD5Update blocks
+ * the data and converts bytes into longwords for this routine.
+ */
+void
+MD5Transform(UINT32 buf[4], UINT32 const in[16])
+{
+       register UINT32  a, b, c, d;
+
+       a = buf[0];
+       b = buf[1];
+       c = buf[2];
+       d = buf[3];
+
+       MD5STEP(F1, a, b, c, d, in[0] + 0xd76aa478, 7);
+       MD5STEP(F1, d, a, b, c, in[1] + 0xe8c7b756, 12);
+       MD5STEP(F1, c, d, a, b, in[2] + 0x242070db, 17);
+       MD5STEP(F1, b, c, d, a, in[3] + 0xc1bdceee, 22);
+       MD5STEP(F1, a, b, c, d, in[4] + 0xf57c0faf, 7);
+       MD5STEP(F1, d, a, b, c, in[5] + 0x4787c62a, 12);
+       MD5STEP(F1, c, d, a, b, in[6] + 0xa8304613, 17);
+       MD5STEP(F1, b, c, d, a, in[7] + 0xfd469501, 22);
+       MD5STEP(F1, a, b, c, d, in[8] + 0x698098d8, 7);
+       MD5STEP(F1, d, a, b, c, in[9] + 0x8b44f7af, 12);
+       MD5STEP(F1, c, d, a, b, in[10] + 0xffff5bb1, 17);
+       MD5STEP(F1, b, c, d, a, in[11] + 0x895cd7be, 22);
+       MD5STEP(F1, a, b, c, d, in[12] + 0x6b901122, 7);
+       MD5STEP(F1, d, a, b, c, in[13] + 0xfd987193, 12);
+       MD5STEP(F1, c, d, a, b, in[14] + 0xa679438e, 17);
+       MD5STEP(F1, b, c, d, a, in[15] + 0x49b40821, 22);
+
+       MD5STEP(F2, a, b, c, d, in[1] + 0xf61e2562, 5);
+       MD5STEP(F2, d, a, b, c, in[6] + 0xc040b340, 9);
+       MD5STEP(F2, c, d, a, b, in[11] + 0x265e5a51, 14);
+       MD5STEP(F2, b, c, d, a, in[0] + 0xe9b6c7aa, 20);
+       MD5STEP(F2, a, b, c, d, in[5] + 0xd62f105d, 5);
+       MD5STEP(F2, d, a, b, c, in[10] + 0x02441453, 9);
+       MD5STEP(F2, c, d, a, b, in[15] + 0xd8a1e681, 14);
+       MD5STEP(F2, b, c, d, a, in[4] + 0xe7d3fbc8, 20);
+       MD5STEP(F2, a, b, c, d, in[9] + 0x21e1cde6, 5);
+       MD5STEP(F2, d, a, b, c, in[14] + 0xc33707d6, 9);
+       MD5STEP(F2, c, d, a, b, in[3] + 0xf4d50d87, 14);
+       MD5STEP(F2, b, c, d, a, in[8] + 0x455a14ed, 20);
+       MD5STEP(F2, a, b, c, d, in[13] + 0xa9e3e905, 5);
+       MD5STEP(F2, d, a, b, c, in[2] + 0xfcefa3f8, 9);
+       MD5STEP(F2, c, d, a, b, in[7] + 0x676f02d9, 14);
+       MD5STEP(F2, b, c, d, a, in[12] + 0x8d2a4c8a, 20);
+
+       MD5STEP(F3, a, b, c, d, in[5] + 0xfffa3942, 4);
+       MD5STEP(F3, d, a, b, c, in[8] + 0x8771f681, 11);
+       MD5STEP(F3, c, d, a, b, in[11] + 0x6d9d6122, 16);
+       MD5STEP(F3, b, c, d, a, in[14] + 0xfde5380c, 23);
+       MD5STEP(F3, a, b, c, d, in[1] + 0xa4beea44, 4);
+       MD5STEP(F3, d, a, b, c, in[4] + 0x4bdecfa9, 11);
+       MD5STEP(F3, c, d, a, b, in[7] + 0xf6bb4b60, 16);
+       MD5STEP(F3, b, c, d, a, in[10] + 0xbebfbc70, 23);
+       MD5STEP(F3, a, b, c, d, in[13] + 0x289b7ec6, 4);
+       MD5STEP(F3, d, a, b, c, in[0] + 0xeaa127fa, 11);
+       MD5STEP(F3, c, d, a, b, in[3] + 0xd4ef3085, 16);
+       MD5STEP(F3, b, c, d, a, in[6] + 0x04881d05, 23);
+       MD5STEP(F3, a, b, c, d, in[9] + 0xd9d4d039, 4);
+       MD5STEP(F3, d, a, b, c, in[12] + 0xe6db99e5, 11);
+       MD5STEP(F3, c, d, a, b, in[15] + 0x1fa27cf8, 16);
+       MD5STEP(F3, b, c, d, a, in[2] + 0xc4ac5665, 23);
+
+       MD5STEP(F4, a, b, c, d, in[0] + 0xf4292244, 6);
+       MD5STEP(F4, d, a, b, c, in[7] + 0x432aff97, 10);
+       MD5STEP(F4, c, d, a, b, in[14] + 0xab9423a7, 15);
+       MD5STEP(F4, b, c, d, a, in[5] + 0xfc93a039, 21);
+       MD5STEP(F4, a, b, c, d, in[12] + 0x655b59c3, 6);
+       MD5STEP(F4, d, a, b, c, in[3] + 0x8f0ccc92, 10);
+       MD5STEP(F4, c, d, a, b, in[10] + 0xffeff47d, 15);
+       MD5STEP(F4, b, c, d, a, in[1] + 0x85845dd1, 21);
+       MD5STEP(F4, a, b, c, d, in[8] + 0x6fa87e4f, 6);
+       MD5STEP(F4, d, a, b, c, in[15] + 0xfe2ce6e0, 10);
+       MD5STEP(F4, c, d, a, b, in[6] + 0xa3014314, 15);
+       MD5STEP(F4, b, c, d, a, in[13] + 0x4e0811a1, 21);
+       MD5STEP(F4, a, b, c, d, in[4] + 0xf7537e82, 6);
+       MD5STEP(F4, d, a, b, c, in[11] + 0xbd3af235, 10);
+       MD5STEP(F4, c, d, a, b, in[2] + 0x2ad7d2bb, 15);
+       MD5STEP(F4, b, c, d, a, in[9] + 0xeb86d391, 21);
+
+       buf[0] += a;
+       buf[1] += b;
+       buf[2] += c;
+       buf[3] += d;
+}
+
+#endif
diff --git a/md5.h b/md5.h
new file mode 100644 (file)
index 0000000..245ecc9
--- /dev/null
+++ b/md5.h
@@ -0,0 +1,27 @@
+/*
+ * Header file for Colin Plumb's MD5 implementation.
+ * Modified by Ian Jackson so as not to use Colin Plumb's `usuals.h'.
+ *
+ * This file is in the public domain.
+ */
+
+#ifndef MD5_H
+#define MD5_H
+
+#define UINT8 unsigned char
+#define UINT32 unsigned long
+
+struct MD5Context {
+       UINT32 buf[4];
+       UINT32 bytes[2];
+       UINT32 in[16];
+};
+
+void MD5Init(struct MD5Context *context);
+void MD5Update(struct MD5Context *context, UINT8 const *buf, unsigned len);
+void MD5Final(unsigned char digest[16], struct MD5Context *context);
+void MD5Transform(UINT32 buf[4], UINT32 const in[16]);
+
+void byteSwap(UINT32 *buf, unsigned words);
+
+#endif /* !MD5_H */
diff --git a/nnrpwrap.c b/nnrpwrap.c
new file mode 100644 (file)
index 0000000..8b144bf
--- /dev/null
@@ -0,0 +1,139 @@
+/**/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#include "md5.h"
+
+#define MAX_SECRET 64
+#define NNRPDREAL "/usr/sbin/nnrpd.real"
+
+static void chopspaces(char *buf) {
+  int n;
+  n= strlen(buf);
+  while (n > 0 && isspace(buf[n-1])) n--;
+  buf[n]= 0;
+}
+
+int main(int argc, char **argv) {
+  char *p, *q, **argvs;
+  unsigned char message[16+MAX_SECRET], expect[16];
+  char responsebuf[300];
+  char sesamebuf[MAX_SECRET*2+100];
+  struct sockaddr_in peername;
+  int c, namesize, n, l;
+  struct MD5Context md5ctx;
+  FILE *file;
+  unsigned long ul;
+  unsigned short us;
+  struct timeval timevab;
+
+  argvs= argv;
+  while ((p= *++argvs)) {
+    if (*p++ != '-') break;
+    c= *p++;
+    if (!c) break;
+    do {
+      if (c == 'r') {
+        if (*++argvs) {
+          printf("400 %s\r\n",*argvs); exit(1);
+        }
+        --argvs;
+        break;
+      } else if (c == 's' || c == 'S') {
+        if (!*++argvs) --argvs;
+        break;
+      }
+      c= *p++;
+    } while (c);
+  }
+
+  if (gettimeofday(&timevab,(void*)0)) {
+    printf("400 what year is it: %s\r\n",strerror(errno));
+    exit(1);
+  }
+  memcpy(message,&timevab.tv_sec,4);
+  ul= timevab.tv_usec; memcpy(message+4,&ul,4);
+  namesize= sizeof(peername);
+  if (getsockname(fileno(stdin),(struct sockaddr*)&peername,&namesize) &&
+      namesize == sizeof(peername) &&
+      peername.sin_family == AF_INET) {
+    memcpy(message+8,&peername.sin_addr,4);
+    memcpy(message+12,&peername.sin_port,2);
+  } else {
+    memset(message+8,'x',6);
+  }
+  us= getpid(); memcpy(message+14,&us,2);
+
+  printf("480 ");
+  for (n=0; n<16; n++) {
+    printf("%s%02x", n?":":"", message[n]);
+  }
+  printf("  halt !  who goes there ?\r\n");
+  if (fflush(stdout) || ferror(stdout)) { perror("stdout challenge"); exit(1); }
+  if (!fgets(responsebuf,sizeof(responsebuf),stdin)) {
+    if (ferror(stdin)) perror("stdin response");
+    else fprintf(stderr,"stdin response EOF\n");
+    exit(1);
+  }
+  chopspaces(responsebuf);
+  p= strchr(responsebuf,' ');
+  if (p) *p++= 0;
+  if (!strcasecmp(responsebuf,"QUIT")) {
+    printf("205 please remember your papers next time.\r\n");
+    exit(0);
+  }
+  if (strcasecmp(responsebuf,"PASS")) {
+    printf("500 guards !  guards !\r\n"
+           "400 this client was just leaving.\r\n");
+    exit(1);
+  }
+  if (!p) {
+    printf("501 fail.\r\n"
+           "400 we don't want failures here.\r\n");
+    exit(1);
+  }
+  file= fopen("/etc/news/sesame","r");
+  if (!file) {
+    printf("400 suddenly i can't see anything: %s\r\n",strerror(errno));
+    exit(1);
+  }
+  do {
+    if (!fgets(sesamebuf,sizeof(sesamebuf),file)) {
+      if (ferror(file)) {
+        printf("400 i'm not sure about that: %s\r\n",strerror(errno));
+      } else {
+        printf("400 happiness is mandatory.\r\n");
+      }
+      exit(1);
+    }
+    chopspaces(sesamebuf);
+  } while (!*sesamebuf || *sesamebuf == '#' || !(q= strchr(sesamebuf,' ')));
+  *q++= 0;
+  l= strlen(q);
+  if (l>MAX_SECRET) { printf("400 i feel all bloated.\r\n"); exit(1); }
+  memcpy(message+16,q,l);
+
+  MD5Init(&md5ctx);
+  MD5Update(&md5ctx,message,16+l);
+  MD5Final(expect,&md5ctx);
+
+  for (n=0; n<16; n++) sprintf(sesamebuf+n*3,"%02x:",expect[n]);
+  sesamebuf[47]= 0;
+  if (!strcasecmp(p,sesamebuf)) {
+    execvp(NNRPDREAL,argv);
+    printf("400 o master, i have failed you: %s\r\n",strerror(errno));
+    exit(1);
+  }
+  printf("502 impostor !\r\n"
+         "400 do not darken my door again.\r\n");
+  exit(1);
+}
diff --git a/nntp-merge-serv b/nntp-merge-serv
new file mode 100755 (executable)
index 0000000..7406a1c
--- /dev/null
@@ -0,0 +1,3 @@
+#!/bin/sh
+cd /u/iwj10/nntp-merge
+exec ./nntp-merge chiark.chu.cam.ac.uk 2>>err-log
diff --git a/nntp-merge.conf b/nntp-merge.conf
new file mode 100644 (file)
index 0000000..139bf93
--- /dev/null
@@ -0,0 +1,151 @@
+# Consists of
+# command
+#    group/pattern
+#    group/pattern
+#    ...
+
+# Commands are:
+
+# myfqdn <merge-server-fqdn>
+# xref <xref-name>
+# server <server-name> <host:port>
+
+# permit <group-of-users-read-only> <group-of-users-posting>
+#  *=all, -=none
+# fetch <method-for-fetch> <method-for-fetch> ...
+# read <server-name>
+# post [<server-name>]
+#  missing server-name means posting not allowed
+# minreaddays <n>
+# extrarc <filename> <user>
+
+# when doing ARTICLE <mid> &c, the first server listed is tried first
+myfqdn chiark.chu.cam.ac.uk
+xref nntp-merge.chiark
+server chiark chiark.chu.cam.ac.uk:118
+server lyra nntp-serv.cam.ac.uk:119
+server-nosearch nyxpost /usr/local/src/nntp-merge/nyxpost
+
+minreaddays 21
+maxperuser 250
+ignorerc guest
+
+# Chiark's local groups
+fetch nowhere
+read chiark
+post chiark
+
+# ... private mailing lists
+permit debian -
+       chiark.mail.debian.private
+       chiark.mail.debian.team
+permit camlbg -
+       chiark.mail.cam-lbg
+# ... the rest
+permit - -
+       chiark.test
+permit * *
+       chiark.*
+       local.*
+       control*
+       junk
+
+# Regional hierarchies near Nyx
+fetch nyx
+read chiark
+post nyxpost
+permit * nyxers
+       co.*
+       du.*
+       nyx.*
+permit * posters
+
+# Groups censored at UKnet, and in the `blocked' area at Nyx
+fetch lyra derwent demon
+read chiark
+post chiark
+permit - censor
+       alt.binaries.pictures.erotica
+       alt.binaries.pictures.erotica.*
+       alt.binaries.pictures.tasteless
+permit * posters
+
+# Don't bother with these at sol
+fetch lyra demon
+read chiark
+post chiark
+       demon.*
+
+# Groups likely to have bad propagation
+fetch lyra derwent demon nyx
+read chiark
+post lyra chiark
+       alt.anonymous.messages
+
+# Groups censored at UKnet, but available elsewhere
+fetch lyra derwent demon
+read chiark
+post chiark
+permit - censor
+       alt.drugs
+       alt.evil
+       alt.lycra
+       alt.personals
+       alt.personals.*
+       alt.psychoactives
+       alt.sex
+       alt.sex.*
+       alt.tasteless
+       rec.arts.erotica
+permit * posters
+
+# Groups on chiark anyway
+read chiark
+post chiark
+       alt.fan.linus-torvalds
+       alt.support.divorce
+       alt.support.personality
+       sci.psychology.*
+       alt.med.cfs
+       alt.security.pgp
+       cam.misc
+       comp.lang.perl.announce
+       comp.org.cpsr.announce
+       comp.os.linux.advocacy
+       comp.os.linux.announce
+       comp.os.linux.answers
+       comp.os.linux.development.apps
+       comp.os.linux.development.system
+       comp.os.linux.hardware
+       comp.os.linux.misc
+       comp.os.linux.networking
+       comp.sources.hp48
+       demon.announce
+       demon.dev
+       demon.sales.d
+       demon.security
+       demon.security.keys
+       demon.tickets
+       gnu.utils.bug
+       news.announce.newusers
+       news.software.nntp
+       rec.games.computer.doom.announce
+       sci.crypt
+       sci.crypt.research
+       soc.bi
+       talk.politics.crypto
+       uk.gay-lesbian-bi
+
+# UK and Cambridge hierarchies
+fetch nowhere
+read lyra
+post lyra chiark
+       cam.*
+       ucam.*
+       uk.*
+
+# Default
+fetch nowhere
+read lyra
+post lyra
+       *
diff --git a/nntp-merge.h b/nntp-merge.h
new file mode 100644 (file)
index 0000000..b9a4737
--- /dev/null
@@ -0,0 +1,71 @@
+/**/
+
+#ifndef NNTP_MERGE_H
+#define NNTP_MERGE_H
+
+#define MAX_COMMAND 2048
+#define MAX_RESPONSE 2048
+#define MAX_XREFLINE 5120
+#define MAX_SECRET 64
+#define SESAMEFILE "/etc/news/sesame"
+
+struct permission {
+  struct permission *next;
+  int authd;
+  char *name;
+};
+
+struct serverinfo {
+  struct serverinfo *next; /* only used during setup */
+  int searchthis;
+  int program;
+  const char *nickname;
+  const char *hostname;
+  unsigned short port;
+  const char *send;
+  FILE *rfile, *wfile, *tempfile;
+};
+
+struct groupinfo {
+  struct serverinfo *readfrom;
+  struct serverinfo *postto[10];
+  int offset;
+  struct permission *restrictto, *readonlyto;
+};
+
+struct cmdinfo {
+  const char *command;
+  void (*call)(char *arg, const struct cmdinfo*);
+  int einfo;
+};
+
+void cmd_post(char *arg, const struct cmdinfo *cip);
+
+void die(const char *msg);
+
+void readconfig(void);
+struct groupinfo *findgroup(const char *groupname);
+struct permission *findpermit(const char *name);
+
+void *xmalloc(size_t);
+char *xstrdup(const char *);
+
+void closeserver(struct serverinfo *server);
+int stripcommand(char *buf);
+int decoderesponse(char response[MAX_RESPONSE+3], unsigned long *rvp,
+                   struct serverinfo *server);
+int servercommand(struct serverinfo *server,
+                  const char command[], char response[MAX_RESPONSE+3],
+                  int flags);
+void serverdataerr(struct serverinfo *si);
+int copydatafile(FILE *from, FILE *to);
+int stillrestricted(struct permission **pip);
+
+extern struct permission *permissions, *restrictpost;
+extern struct serverinfo *servers;
+struct sockaddr_in peername;
+
+extern char *myfqdn, *myxref, *lastdoneauth;
+extern const char *theirfqdn;
+
+#endif /* NNTP_MERGE_H */
diff --git a/nyxpost b/nyxpost
new file mode 100755 (executable)
index 0000000..cb06569
--- /dev/null
+++ b/nyxpost
@@ -0,0 +1,45 @@
+#!/usr/bin/perl
+
+defined($la= $ENV{'NNTPMERGE_AUTHD_AS'}) || die "no la";
+$la.= "\@chiark";
+$outnews= "/usr/local/lib/exchange/outnews";
+$route= "nyx";
+
+$|=1;
+print "200 nyxpost\r\n";
+while (<>) {
+    s/\s*\n$//;
+    if (m/^POST$/i) {
+        print "340 mmmm\r\n";
+        $art= '';
+        for (;;) {
+            length($_=<>) || exit(1);
+            last if m/^\.\r?\n$/;
+            s/^\.//; s/\r\n$/\n/; $art.= $_;
+        }
+        defined($c= open(P,"-|")) || die "fork: $!";
+        if (!$c) {
+            open(STDERR,">&STDOUT");
+            defined($c2= open(Q,"|-")) || die "fork: $!";
+            if (!$c2) {
+                exec($outnews,"--sender",$la,$route);
+                die "exec: $!";
+            }
+            print(Q $art) || die "write: $!";
+            close(Q); $? && die "outnews: $?";
+            exit(0);
+        }
+        undef $/; $_= <P>; $/= "\n"; s/\n/ /g;
+        close(P);
+        if ($?) {
+            print "441 $_\r\n";
+        } else {
+            print "240 $_\r\n";
+        }
+    } elsif (m/^QUIT$/i) {
+        print "205 ttfn\r\n";
+        exit(0);
+    } else {
+        print "500 too stupid\r\n";
+    }
+}
diff --git a/nyxpostrun.c b/nyxpostrun.c
new file mode 100644 (file)
index 0000000..b7e0c31
--- /dev/null
@@ -0,0 +1,10 @@
+#include <unistd.h>
+#include <stdio.h>
+int main(int argc, char **argv) {
+  int g;
+  g= getegid();
+  setregid(g,g);
+  execv("/usr/local/lib/news/nyxpost",argv);
+  perror("nyxpostrun: exec");
+  exit(1);
+}
diff --git a/post.c b/post.c
new file mode 100644 (file)
index 0000000..986fb60
--- /dev/null
+++ b/post.c
@@ -0,0 +1,294 @@
+/**/
+
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <ctype.h>
+#include <assert.h>
+#include <sys/types.h>
+#include <netdb.h>
+#include <arpa/inet.h>
+#include <time.h>
+#include <unistd.h>
+#include <netinet/in.h>
+
+#include <ident.h>
+
+#include "nntp-merge.h"
+
+static int output64(FILE *file, unsigned long v, int minn) {
+  static const char *const b64=
+    "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789" "+-";
+  int i= 0;
+  while (minn>0 || v) {
+    putc(b64[v&077],file);
+    v >>= 6;
+    i++; minn--;
+  }
+  return i;
+}
+
+static void eatpost() {
+  if (!copydatafile(stdin,0)) die("failed read from client during POST");
+}
+
+static void badpost(const char *why) {
+  eatpost();
+  printf("441 not acceptable: %s\r\n",why);
+}
+
+static int attemptposting(struct serverinfo *si, FILE *file,
+                          char response[MAX_RESPONSE+3]) {
+  int rcode, c;
+  unsigned long rv;
+
+  if (fseek(file,0,SEEK_SET)) {
+    sprintf(response,"503 rewind article failed: %.100s",strerror(errno));
+    return 0x503;
+  }
+  rcode= servercommand(si,"POST",response,0);
+  if (rcode != 0x340) return rcode;
+  while ((c= getc(file)) != EOF) {
+    if (putc(c,si->wfile) == EOF) {
+      sprintf(response,"503 server write failed: %.100s",strerror(errno));
+      closeserver(si); return 0x503;
+    }
+  }
+  if (fflush(si->wfile) == EOF || ferror(file)) {
+    sprintf(response,"503 read article failed: %.100s",strerror(errno));
+    closeserver(si); return 0x503;
+  }
+  if (!fgets(response,MAX_RESPONSE+3,si->rfile)) {
+    sprintf(response,"503 server read failed: %.100s",strerror(errno));
+    closeserver(si); return 0x503;
+  }
+  if (!stripcommand(response)) {
+    sprintf(response,"503 null garbage in reply to article");
+    closeserver(si); return 0x503;
+  }
+  if (!decoderesponse(response,&rv,si)) return 0x503;
+  return rv;
+}
+
+void cmd_post(char *arg, const struct cmdinfo *cip) {
+  char linebuf[MAX_XREFLINE+3];
+  FILE *file;
+  int n, m, hadmessageid, haddate, hadnewsgroups, moderated, zonediff, i, j, rcode;
+  int zonediffc, hadpermprob, c;
+  struct groupinfo *gi;
+  char *p, *colon, *space, *comma;
+  struct serverinfo *postto[20], *si;
+  char response[MAX_RESPONSE+3];
+  char commandbuf[MAX_COMMAND+3];
+  char buf[1000];
+  IDENT *id;
+  time_t now;
+  struct tm local, gm, *tmtmp;
+
+  file= tmpfile();  if (!file) die("unable to make tmpfile for POST");
+  if (lastdoneauth)
+    fputs("340 go ahead.\r\n",stdout);
+  else
+    fputs("340 make my day, punk.\r\n",stdout);
+  if (ferror(stdout) || fflush(stdout)) die("unable to say go ahead to POST");
+  hadmessageid= haddate= hadnewsgroups= 0; hadpermprob= 0;
+  postto[0]= 0;
+  for (si= servers; si; si= si->next) si->tempfile= 0;
+  for (;;) {
+    errno= 0; if (!fgets(linebuf,MAX_XREFLINE,stdin)) die("error reading data in POST");
+    m= n= strlen(linebuf); if (!n) die("null data line in POST");
+    while (n > 0 && (linebuf[n-1] == '\n' || linebuf[n-1] == '\r')) n--;
+    if (m == n) { badpost("line in POST too long"); fclose(file); return; }
+    if (n==0) break;
+    linebuf[n]= 0;
+    if (n == 1 && linebuf[0] == '.') {
+      printf("441 message must have a body.\r\n"); fclose(file); return;
+    }
+    colon= strchr(linebuf,':');
+    if (!colon) { badpost("header line with no colon"); fclose(file); return; }
+    *colon++= 0;
+    while (isspace(*colon)) colon++;
+    for (p=linebuf; *p; p++)
+      if (isspace(*p)) { badpost("space in header name"); fclose(file); return; }
+    if (!strcasecmp(linebuf,"originator")) continue;
+    if (!strcasecmp(linebuf,"nntp-posting-host")) continue;
+    if (!strcasecmp(linebuf,"path")) continue;
+    fprintf(file,"%s: %s\r\n",linebuf,colon);
+    if (!strcasecmp(linebuf,"message-id")) {
+      if (hadmessageid++) {
+        badpost("two (or more) Message-ID headers"); fclose(file); return;
+      }
+    } else if (!strcasecmp(linebuf,"date")) {
+      if (haddate++) {
+        badpost("two (or more) Date headers"); fclose(file); return;
+      }
+    } else if (!strcasecmp(linebuf,"newsgroups")) {
+      if (hadnewsgroups++) {
+        badpost("two (or more) Newsgroups headers"); fclose(file); return;
+      }
+      p= colon;
+      while (p) {
+        comma= strchr(p,',');
+        if (comma) *comma++= 0;
+        gi= findgroup(p);
+        if (stillrestricted(&gi->restrictto)) {
+          hadpermprob= 1; p= comma; continue;
+        } else if (!gi->postto[0]) {
+          p= comma; continue;
+        }
+        for (i=0;
+             i<(sizeof(gi->postto)/sizeof(gi->postto[0])) &&
+             gi->postto[i];
+             i++) {
+          for (j=0;
+               j<(sizeof(postto)/sizeof(postto[0])) && postto[j] &&
+               postto[j] != gi->postto[i];
+               j++);
+          if (j >= (sizeof(postto)/sizeof(postto[0]))) {
+            badpost("would have to post to too many servers"); fclose(file); return;
+          }
+          if (!postto[j]) {
+            postto[j]= gi->postto[i];
+            if (j+1 < (sizeof(postto)/sizeof(postto[0]))) postto[j+1]= 0;
+          }
+        }
+        /* We don't bother checking for moderation status if we're only
+         * posting to one group, and that group is only on one server.
+         */
+        if (p == colon && !comma && !gi->postto[1]) break;
+       /* Try undocumented nnrp feature, falling back on getting whole active file. */
+       snprintf(commandbuf, sizeof(commandbuf), "LIST ACTIVE %s", p);
+       rcode= servercommand(gi->postto[0],commandbuf,response,0);
+       if (rcode != 0x215) {
+         rcode= servercommand(gi->postto[0],"LIST",response,0);
+         if (rcode != 0x215) {
+           eatpost(); fclose(file);
+           printf("441 couldn't LIST to check moderation status of %s - %s: %s\r\n",
+                  p,gi->postto[0]->hostname,response);
+           return;
+         }
+       }
+        moderated= 0;
+        for (;;) {
+          if (!fgets(response,sizeof(response),gi->postto[0]->rfile))
+            serverdataerr(gi->postto[0]);
+          if (!strcmp(response,".\r\n") || !strcmp(response,".\n")) break;
+          space= strchr(response,' ');
+          if (!space) continue;
+          *space++= 0;
+          if (strcmp(response,p)) continue;
+          space= strchr(space,' ');
+          if (!space) continue;
+          space= strchr(++space,' ');
+          if (!space) continue;
+          ++space;
+          if (*space == 'm') moderated= 1;
+        }
+        if (moderated) {
+          postto[1]= 0;
+          break;
+        }
+        p= comma;
+      }
+    } else {
+      for (;;) {
+        c= getchar();
+        if (c != ' ' && c != '\t') break;
+        for (;;) {
+          putc(c,file);
+          if (c == '\n') break;
+          c= getchar();
+          if (c == EOF) break;
+        }
+      }
+      ungetc(c,stdin);
+    }
+  }
+  /* Right, we've copied the header into tmpfile, and we've read
+   * but not yet copied the blank line.  We must add the Originator
+   * field.
+   */
+  if (!hadnewsgroups) { badpost("missing Newsgroups header"); fclose(file); return; }
+  if (!postto[0]) {
+    if (hadpermprob) {
+      eatpost();
+      if (lastdoneauth) {
+        printf("480 the power of %s is but weak.\r\n",lastdoneauth);
+      } else {
+        printf("480 thou must prove thine strength.\r\n");
+      }
+    } else {
+      badpost("no server(s) for those groups, cannot post.\r\n");
+    }
+    return;
+  }
+  id= ident_lookup(0,30);
+  fprintf(file,"Originator: %s@", id && id->identifier ? id->identifier : "");
+  if (theirfqdn) fprintf(file,"%s ([%s])",
+                         strcmp(theirfqdn,"localhost") ? theirfqdn : myfqdn,
+                         inet_ntoa(peername.sin_addr));
+  else fprintf(file,"[%s]", inet_ntoa(peername.sin_addr));
+  fputs("\r\n",file);
+  /* Forms of Originator line are:
+   *  Originator: <identifier>@<hostname> ([<addr>])
+   *  Originator: <identifier>@[<addr>]
+   *  Originator: @<hostname> ([<addr>])
+   *  Originator: @[addr]
+   * <opsys>, <charset> are empty strings if not available.
+   */
+
+  if (!haddate || !hadmessageid) {
+    time(&now);
+  }
+  if (!haddate) {
+    tmtmp= gmtime(&now); if (!tmtmp) die("can't get Greenwich Mean Time");
+    gm= *tmtmp;
+    tmtmp= localtime(&now); if (!tmtmp) die("can't get local time");
+    local= *tmtmp;
+    zonediff= (local.tm_hour*60 + local.tm_min) - (gm.tm_hour*60 + gm.tm_min);
+    zonediff += 2880;
+    zonediff %= 1440;
+    zonediffc= '+';
+    if (zonediff > 720) { zonediff= 1440-zonediff; zonediffc= '-'; }
+    assert(sprintf(buf,"Date: %%d %%b %%Y %%H:%%M:%%S %c%02d%02d (%%Z)\r\n",
+                   zonediffc,zonediff/60,zonediff%60) < sizeof(buf));
+    if (strftime(response,sizeof(response),buf,&local) == sizeof(response))
+      die("date is too long for buffer");
+    fputs(response,file);
+  }
+  if (!hadmessageid) {
+    unsigned long pid= getpid();
+    fputs("Message-ID: <",file);
+    output64(file,(now&03)|(pid<<2),0);
+    putc('*',file);
+    output64(file,now>>2,5);
+    fprintf(file,"@%s>\r\n",myfqdn);
+    sleep(2);
+  }
+
+  fputs("\r\n",file);
+  if (!copydatafile(stdin,file)) die("failed read from client during POST");
+
+  if (ferror(file) || fflush(file) || fseek(file,0,SEEK_SET))
+    die("error writing tmp posting file");
+
+  rcode= attemptposting(postto[0],file,response);
+  if (rcode != 0x240) {
+    fclose(file);
+    printf("441 POST failed - %s: %s\r\n",postto[0]->hostname,response);
+    return;
+  }
+  printf("240 %s ok",postto[0]->hostname); fflush(stdout);
+  for (i=1; i<sizeof(postto)/sizeof(postto[0]) && postto[i]; i++) {
+    rcode= attemptposting(postto[i],file,response);
+    if (rcode != 0x240) {
+      printf("; %s err %.30s",postto[i]->hostname,response);
+    } else {
+      printf("; %s ok",postto[i]->hostname);
+    }
+    fflush(stdout);
+  }
+  printf(" - posted.\r\n");
+  fclose(file);
+  return;
+}
diff --git a/post.c.orig b/post.c.orig
new file mode 100644 (file)
index 0000000..af2a7ee
--- /dev/null
@@ -0,0 +1,288 @@
+/**/
+
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <ctype.h>
+#include <assert.h>
+#include <sys/types.h>
+#include <netdb.h>
+#include <arpa/inet.h>
+#include <time.h>
+#include <unistd.h>
+#include <netinet/in.h>
+
+#include <ident.h>
+
+#include "nntp-merge.h"
+
+static int output64(FILE *file, unsigned long v, int minn) {
+  static const char *const b64=
+    "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789" "+-";
+  int i= 0;
+  while (minn>0 || v) {
+    putc(b64[v&077],file);
+    v >>= 6;
+    i++; minn--;
+  }
+  return i;
+}
+
+static void eatpost() {
+  if (!copydatafile(stdin,0)) die("failed read from client during POST");
+}
+
+static void badpost(const char *why) {
+  eatpost();
+  printf("441 not acceptable: %s\r\n",why);
+}
+
+static int attemptposting(struct serverinfo *si, FILE *file,
+                          char response[MAX_RESPONSE+3]) {
+  int rcode, c;
+  unsigned long rv;
+
+  if (fseek(file,0,SEEK_SET)) {
+    sprintf(response,"503 rewind article failed: %.100s",strerror(errno));
+    return 0x503;
+  }
+  rcode= servercommand(si,"POST",response,0);
+  if (rcode != 0x340) return rcode;
+  while ((c= getc(file)) != EOF) {
+    if (putc(c,si->wfile) == EOF) {
+      sprintf(response,"503 server write failed: %.100s",strerror(errno));
+      closeserver(si); return 0x503;
+    }
+  }
+  if (fflush(si->wfile) == EOF || ferror(file)) {
+    sprintf(response,"503 read article failed: %.100s",strerror(errno));
+    closeserver(si); return 0x503;
+  }
+  if (!fgets(response,MAX_RESPONSE+3,si->rfile)) {
+    sprintf(response,"503 server read failed: %.100s",strerror(errno));
+    closeserver(si); return 0x503;
+  }
+  if (!stripcommand(response)) {
+    sprintf(response,"503 null garbage in reply to article");
+    closeserver(si); return 0x503;
+  }
+  if (!decoderesponse(response,&rv,si)) return 0x503;
+  return rv;
+}
+
+void cmd_post(char *arg, const struct cmdinfo *cip) {
+  char linebuf[MAX_XREFLINE+3];
+  FILE *file;
+  int n, m, hadmessageid, haddate, hadnewsgroups, moderated, zonediff, i, j, rcode;
+  int zonediffc, hadpermprob, c;
+  struct groupinfo *gi;
+  char *p, *colon, *space, *comma;
+  struct serverinfo *postto[20], *si;
+  char response[MAX_RESPONSE+3];
+  char buf[1000];
+  IDENT *id;
+  time_t now;
+  struct tm local, gm, *tmtmp;
+
+  file= tmpfile();  if (!file) die("unable to make tmpfile for POST");
+  if (lastdoneauth)
+    fputs("340 go ahead.\r\n",stdout);
+  else
+    fputs("340 make my day, punk.\r\n",stdout);
+  if (ferror(stdout) || fflush(stdout)) die("unable to say go ahead to POST");
+  hadmessageid= haddate= hadnewsgroups= 0; hadpermprob= 0;
+  postto[0]= 0;
+  for (si= servers; si; si= si->next) si->tempfile= 0;
+  for (;;) {
+    errno= 0; if (!fgets(linebuf,MAX_XREFLINE,stdin)) die("error reading data in POST");
+    m= n= strlen(linebuf); if (!n) die("null data line in POST");
+    while (n > 0 && (linebuf[n-1] == '\n' || linebuf[n-1] == '\r')) n--;
+    if (m == n) { badpost("line in POST too long"); fclose(file); return; }
+    if (n==0) break;
+    linebuf[n]= 0;
+    if (n == 1 && linebuf[0] == '.') {
+      printf("441 message must have a body.\r\n"); fclose(file); return;
+    }
+    colon= strchr(linebuf,':');
+    if (!colon) { badpost("header line with no colon"); fclose(file); return; }
+    *colon++= 0;
+    while (isspace(*colon)) colon++;
+    for (p=linebuf; *p; p++)
+      if (isspace(*p)) { badpost("space in header name"); fclose(file); return; }
+    if (!strcasecmp(linebuf,"originator")) continue;
+    if (!strcasecmp(linebuf,"nntp-posting-host")) continue;
+    if (!strcasecmp(linebuf,"path")) continue;
+    fprintf(file,"%s: %s\r\n",linebuf,colon);
+    if (!strcasecmp(linebuf,"message-id")) {
+      if (hadmessageid++) {
+        badpost("two (or more) Message-ID headers"); fclose(file); return;
+      }
+    } else if (!strcasecmp(linebuf,"date")) {
+      if (haddate++) {
+        badpost("two (or more) Date headers"); fclose(file); return;
+      }
+    } else if (!strcasecmp(linebuf,"newsgroups")) {
+      if (hadnewsgroups++) {
+        badpost("two (or more) Newsgroups headers"); fclose(file); return;
+      }
+      p= colon;
+      while (p) {
+        comma= strchr(p,',');
+        if (comma) *comma++= 0;
+        gi= findgroup(p);
+        if (stillrestricted(&gi->restrictto)) {
+          hadpermprob= 1; p= comma; continue;
+        } else if (!gi->postto[0]) {
+          p= comma; continue;
+        }
+        for (i=0;
+             i<(sizeof(gi->postto)/sizeof(gi->postto[0])) &&
+             gi->postto[i];
+             i++) {
+          for (j=0;
+               j<(sizeof(postto)/sizeof(postto[0])) && postto[j] &&
+               postto[j] != gi->postto[i];
+               j++);
+          if (j >= (sizeof(postto)/sizeof(postto[0]))) {
+            badpost("would have to post to too many servers"); fclose(file); return;
+          }
+          if (!postto[j]) {
+            postto[j]= gi->postto[i];
+            if (j+1 < (sizeof(postto)/sizeof(postto[0]))) postto[j+1]= 0;
+          }
+        }
+        /* We don't bother checking for moderation status if we're only
+         * posting to one group, and that group is only on one server.
+         */
+        if (p == colon && !comma && !gi->postto[1]) break;
+        rcode= servercommand(gi->postto[0],"LIST",response,0);
+        if (rcode != 0x215) {
+          eatpost(); fclose(file);
+          printf("441 couldn't LIST to check moderation status of %s - %s: %s\r\n",
+                 p,gi->postto[0]->hostname,response);
+          return;
+        }
+        moderated= 0;
+        for (;;) {
+          if (!fgets(response,sizeof(response),gi->postto[0]->rfile))
+            serverdataerr(gi->postto[0]);
+          if (!strcmp(response,".\r\n") || !strcmp(response,".\n")) break;
+          space= strchr(response,' ');
+          if (!space) continue;
+          *space++= 0;
+          if (strcmp(response,p)) continue;
+          space= strchr(space,' ');
+          if (!space) continue;
+          space= strchr(++space,' ');
+          if (!space) continue;
+          ++space;
+          if (*space == 'm') moderated= 1;
+        }
+        if (moderated) {
+          postto[1]= 0;
+          break;
+        }
+        p= comma;
+      }
+    } else {
+      for (;;) {
+        c= getchar();
+        if (c != ' ' && c != '\t') break;
+        for (;;) {
+          putc(c,file);
+          if (c == '\n') break;
+          c= getchar();
+          if (c == EOF) break;
+        }
+      }
+      ungetc(c,stdin);
+    }
+  }
+  /* Right, we've copied the header into tmpfile, and we've read
+   * but not yet copied the blank line.  We must add the Originator
+   * field.
+   */
+  if (!hadnewsgroups) { badpost("missing Newsgroups header"); fclose(file); return; }
+  if (!postto[0]) {
+    if (hadpermprob) {
+      eatpost();
+      if (lastdoneauth) {
+        printf("480 the power of %s is but weak.\r\n",lastdoneauth);
+      } else {
+        printf("480 thou must prove thine strength.\r\n");
+      }
+    } else {
+      badpost("no server(s) for those groups, cannot post.\r\n");
+    }
+    return;
+  }
+  id= ident_lookup(0,30);
+  fprintf(file,"Originator: %s@", id && id->identifier ? id->identifier : "");
+  if (theirfqdn) fprintf(file,"%s ([%s])",
+                         strcmp(theirfqdn,"localhost") ? theirfqdn : myfqdn,
+                         inet_ntoa(peername.sin_addr));
+  else fprintf(file,"[%s]", inet_ntoa(peername.sin_addr));
+  fputs("\r\n",file);
+  /* Forms of Originator line are:
+   *  Originator: <identifier>@<hostname> ([<addr>])
+   *  Originator: <identifier>@[<addr>]
+   *  Originator: @<hostname> ([<addr>])
+   *  Originator: @[addr]
+   * <opsys>, <charset> are empty strings if not available.
+   */
+
+  if (!haddate || !hadmessageid) {
+    time(&now);
+  }
+  if (!haddate) {
+    tmtmp= gmtime(&now); if (!tmtmp) die("can't get Greenwich Mean Time");
+    gm= *tmtmp;
+    tmtmp= localtime(&now); if (!tmtmp) die("can't get local time");
+    local= *tmtmp;
+    zonediff= (local.tm_hour*60 + local.tm_min) - (gm.tm_hour*60 + gm.tm_min);
+    zonediff += 2880;
+    zonediff %= 1440;
+    zonediffc= '+';
+    if (zonediff > 720) { zonediff= 1440-zonediff; zonediffc= '-'; }
+    assert(sprintf(buf,"Date: %%d %%b %%Y %%H:%%M:%%S %c%02d%02d (%%Z)\r\n",
+                   zonediffc,zonediff/60,zonediff%60) < sizeof(buf));
+    if (strftime(response,sizeof(response),buf,&local) == sizeof(response))
+      die("date is too long for buffer");
+    fputs(response,file);
+  }
+  if (!hadmessageid) {
+    unsigned long pid= getpid();
+    fputs("Message-ID: <",file);
+    output64(file,(now&03)|(pid<<2),0);
+    putc('*',file);
+    output64(file,now>>2,5);
+    fprintf(file,"@%s>\r\n",myfqdn);
+    sleep(2);
+  }
+
+  fputs("\r\n",file);
+  if (!copydatafile(stdin,file)) die("failed read from client during POST");
+
+  if (ferror(file) || fflush(file) || fseek(file,0,SEEK_SET))
+    die("error writing tmp posting file");
+
+  rcode= attemptposting(postto[0],file,response);
+  if (rcode != 0x240) {
+    fclose(file);
+    printf("441 POST failed - %s: %s\r\n",postto[0]->hostname,response);
+    return;
+  }
+  printf("240 %s ok",postto[0]->hostname); fflush(stdout);
+  for (i=1; i<sizeof(postto)/sizeof(postto[0]) && postto[i]; i++) {
+    rcode= attemptposting(postto[i],file,response);
+    if (rcode != 0x240) {
+      printf("; %s err %.30s",postto[i]->hostname,response);
+    } else {
+      printf("; %s ok",postto[i]->hostname);
+    }
+    fflush(stdout);
+  }
+  printf(" - posted.\r\n");
+  fclose(file);
+  return;
+}
diff --git a/rfc977.txt b/rfc977.txt
new file mode 100644 (file)
index 0000000..046a270
--- /dev/null
@@ -0,0 +1,1539 @@
+
+
+Network Working Group                      Brian Kantor (U.C. San Diego)
+Request for Comments: 977                   Phil Lapsley (U.C. Berkeley)
+                                                           February 1986
+
+                     Network News Transfer Protocol
+                                    
+                A Proposed Standard for the Stream-Based
+                          Transmission of News
+
+Status of This Memo
+
+   NNTP specifies a protocol for the distribution, inquiry, retrieval,
+   and posting of news articles using a reliable stream-based
+   transmission of news among the ARPA-Internet community.  NNTP is
+   designed so that news articles are stored in a central database
+   allowing a subscriber to select only those items he wishes to read.
+   Indexing, cross-referencing, and expiration of aged messages are also
+   provided. This RFC suggests a proposed protocol for the ARPA-Internet
+   community, and requests discussion and suggestions for improvements.
+   Distribution of this memo is unlimited.
+
+1.  Introduction
+
+   For many years, the ARPA-Internet community has supported the
+   distribution of bulletins, information, and data in a timely fashion
+   to thousands of participants.  We collectively refer to such items of
+   information as "news".  Such news provides for the rapid
+   dissemination of items of interest such as software bug fixes, new
+   product reviews, technical tips, and programming pointers, as well as
+   rapid-fire discussions of matters of concern to the working computer
+   professional. News is very popular among its readers.
+
+   There are popularly two methods of distributing such news: the
+   Internet method of direct mailing, and the USENET news system.
+
+1.1.  Internet Mailing Lists
+
+   The Internet community distributes news by the use of mailing lists.
+   These are lists of subscriber's mailbox addresses and remailing
+   sublists of all intended recipients.  These mailing lists operate by
+   remailing a copy of the information to be distributed to each
+   subscriber on the mailing list.  Such remailing is inefficient when a
+   mailing list grows beyond a dozen or so people, since sending a
+   separate copy to each of the subscribers occupies large quantities of
+   network bandwidth, CPU resources, and significant amounts of disk
+   storage at the destination host.  There is also a significant problem
+   in maintenance of the list itself: as subscribers move from one job
+   to another; as new subscribers join and old ones leave; and as hosts
+   come in and out of service.
+
+
+
+
+Kantor & Lapsley                                                [Page 1]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+1.2.  The USENET News System
+
+   Clearly, a worthwhile reduction of the amount of these resources used
+   can be achieved if articles are stored in a central database on the
+   receiving host instead of in each subscriber's mailbox. The USENET
+   news system provides a method of doing just this.  There is a central
+   repository of the news articles in one place (customarily a spool
+   directory of some sort), and a set of programs that allow a
+   subscriber to select those items he wishes to read.  Indexing,
+   cross-referencing, and expiration of aged messages are also provided.
+
+1.3.  Central Storage of News
+
+   For clusters of hosts connected together by fast local area networks
+   (such as Ethernet), it makes even more sense to consolidate news
+   distribution onto one (or a very few) hosts, and to allow access to
+   these news articles using a server and client model.  Subscribers may
+   then request only the articles they wish to see, without having to
+   wastefully duplicate the storage of a copy of each item on each host.
+
+1.4.  A Central News Server
+
+   A way to achieve these economies is to have a central computer system
+   that can provide news service to the other systems on the local area
+   network.  Such a server would manage the collection of news articles
+   and index files, with each person who desires to read news bulletins
+   doing so over the LAN.  For a large cluster of computer systems, the
+   savings in total disk space is clearly worthwhile.  Also, this allows
+   workstations with limited disk storage space to participate in the
+   news without incoming items consuming oppressive amounts of the
+   workstation's disk storage.
+
+   We have heard rumors of somewhat successful attempts to provide
+   centralized news service using IBIS and other shared or distributed
+   file systems.  While it is possible that such a distributed file
+   system implementation might work well with a group of similar
+   computers running nearly identical operating systems, such a scheme
+   is not general enough to offer service to a wide range of client
+   systems, especially when many diverse operating systems may be in use
+   among a group of clients.  There are few (if any) shared or networked
+   file systems that can offer the generality of service that stream
+   connections using Internet TCP provide, particularly when a wide
+   range of host hardware and operating systems are considered.
+
+   NNTP specifies a protocol for the distribution, inquiry, retrieval,
+   and posting of news articles using a reliable stream (such as TCP)
+   server-client model. NNTP is designed so that news articles need only
+
+
+Kantor & Lapsley                                                [Page 2]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+   be stored on one (presumably central) host, and subscribers on other
+   hosts attached to the LAN may read news articles using stream
+   connections to the news host.
+
+   NNTP is modelled upon the news article specifications in RFC 850,
+   which describes the USENET news system.  However, NNTP makes few
+   demands upon the structure, content, or storage of news articles, and
+   thus we believe it easily can be adapted to other non-USENET news
+   systems.
+
+   Typically, the NNTP server runs as a background process on one host,
+   and would accept connections from other hosts on the LAN.  This works
+   well when there are a number of small computer systems (such as
+   workstations, with only one or at most a few users each), and a large
+   central server.
+
+1.5.  Intermediate News Servers
+
+   For clusters of machines with many users (as might be the case in a
+   university or large industrial environment), an intermediate server
+   might be used.  This intermediate or "slave" server runs on each
+   computer system, and is responsible for mediating news reading
+   requests and performing local caching of recently-retrieved news
+   articles.
+
+   Typically, a client attempting to obtain news service would first
+   attempt to connect to the news service port on the local machine.  If
+   this attempt were unsuccessful, indicating a failed server, an
+   installation might choose to either deny news access, or to permit
+   connection to the central "master" news server.
+
+   For workstations or other small systems, direct connection to the
+   master server would probably be the normal manner of operation.
+
+   This specification does not cover the operation of slave NNTP
+   servers.  We merely suggest that slave servers are a logical addition
+   to NNTP server usage which would enhance operation on large local
+   area networks.
+
+1.6.  News Distribution
+
+   NNTP has commands which provide a straightforward method of
+   exchanging articles between cooperating hosts. Hosts which are well
+   connected on a local area or other fast network and who wish to
+   actually obtain copies of news articles for local storage might well
+   find NNTP to be a more efficient way to distribute news than more
+   traditional transfer methods (such as UUCP).
+
+
+Kantor & Lapsley                                                [Page 3]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+   In the traditional method of distributing news articles, news is
+   propagated from host to host by flooding - that is, each host will
+   send all its new news articles on to each host that it feeds.  These
+   hosts will then in turn send these new articles on to other hosts
+   that they feed.  Clearly, sending articles that a host already has
+   obtained a copy of from another feed (many hosts that receive news
+   are redundantly fed) again is a waste of time and communications
+   resources, but for transport mechanisms that are single-transaction
+   based rather than interactive (such as UUCP in the UNIX-world <1>),
+   distribution time is diminished by sending all articles and having
+   the receiving host simply discard the duplicates.  This is an
+   especially true when communications sessions are limited to once a
+   day.
+
+   Using NNTP, hosts exchanging news articles have an interactive
+   mechanism for deciding which articles are to be transmitted.  A host
+   desiring new news, or which has new news to send, will typically
+   contact one or more of its neighbors using NNTP.  First it will
+   inquire if any new news groups have been created on the serving host
+   by means of the NEWGROUPS command.  If so, and those are appropriate
+   or desired (as established by local site-dependent rules), those new
+   newsgroups can be created.
+
+   The client host will then inquire as to which new articles have
+   arrived in all or some of the newsgroups that it desires to receive,
+   using the NEWNEWS command.  It will receive a list of new articles
+   from the server, and can request transmission of those articles that
+   it desires and does not already have.
+
+   Finally, the client can advise the server of those new articles which
+   the client has recently received.  The server will indicate those
+   articles that it has already obtained copies of, and which articles
+   should be sent to add to its collection.
+
+   In this manner, only those articles which are not duplicates and
+   which are desired are transferred.
+
+
+
+
+
+
+
+
+
+
+
+
+
+Kantor & Lapsley                                                [Page 4]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+2.  The NNTP Specification
+
+2.1.  Overview
+
+   The news server specified by this document uses a stream connection
+   (such as TCP) and SMTP-like commands and responses.  It is designed
+   to accept connections from hosts, and to provide a simple interface
+   to the news database.
+
+   This server is only an interface between programs and the news
+   databases. It does not perform any user interaction or presentation-
+   level functions. These "user-friendly" functions are better left to
+   the client programs, which have a better understanding of the
+   environment in which they are operating.
+
+   When used via Internet TCP, the contact port assigned for this
+   service is 119.
+
+2.2.  Character Codes
+
+   Commands and replies are composed of characters from the ASCII
+   character set.  When the transport service provides an 8-bit byte
+   (octet) transmission channel, each 7-bit character is transmitted
+   right justified in an octet with the high order bit cleared to zero.
+
+2.3.  Commands
+
+   Commands consist of a command word, which in some cases may be
+   followed by a parameter.  Commands with parameters must separate the
+   parameters from each other and from the command by one or more space
+   or tab characters.  Command lines must be complete with all required
+   parameters, and may not contain more than one command.
+
+   Commands and command parameters are not case sensitive. That is, a
+   command or parameter word may be upper case, lower case, or any
+   mixture of upper and lower case.
+
+   Each command line must be terminated by a CR-LF (Carriage Return -
+   Line Feed) pair.
+
+   Command lines shall not exceed 512 characters in length, counting all
+   characters including spaces, separators, punctuation, and the
+   trailing CR-LF (thus there are 510 characters maximum allowed for the
+   command and its parameters).  There is no provision for continuation
+   command lines.
+
+
+
+
+Kantor & Lapsley                                                [Page 5]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+2.4.  Responses
+
+   Responses are of two kinds, textual and status.
+
+2.4.1.  Text Responses
+
+   Text is sent only after a numeric status response line has been sent
+   that indicates that text will follow.  Text is sent as a series of
+   successive lines of textual matter, each terminated with CR-LF pair.
+   A single line containing only a period (.) is sent to indicate the
+   end of the text (i.e., the server will send a CR-LF pair at the end
+   of the last line of text, a period, and another CR-LF pair).
+
+   If the text contained a period as the first character of the text
+   line in the original, that first period is doubled.  Therefore, the
+   client must examine the first character of each line received, and
+   for those beginning with a period, determine either that this is the
+   end of the text or whether to collapse the doubled period to a single
+   one.
+
+   The intention is that text messages will usually be displayed on the
+   user's terminal whereas command/status responses will be interpreted
+   by the client program before any possible display is done.
+
+2.4.2.  Status Responses
+
+   These are status reports from the server and indicate the response to
+   the last command received from the client.
+
+   Status response lines begin with a 3 digit numeric code which is
+   sufficient to distinguish all responses.  Some of these may herald
+   the subsequent transmission of text.
+
+   The first digit of the response broadly indicates the success,
+   failure, or progress of the previous command.
+
+      1xx - Informative message
+      2xx - Command ok
+      3xx - Command ok so far, send the rest of it.
+      4xx - Command was correct, but couldn't be performed for
+            some reason.
+      5xx - Command unimplemented, or incorrect, or a serious
+            program error occurred.
+
+
+
+
+
+
+Kantor & Lapsley                                                [Page 6]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+   The next digit in the code indicates the function response category.
+
+      x0x - Connection, setup, and miscellaneous messages
+      x1x - Newsgroup selection
+      x2x - Article selection
+      x3x - Distribution functions
+      x4x - Posting
+      x8x - Nonstandard (private implementation) extensions
+      x9x - Debugging output
+
+   The exact response codes that should be expected from each command
+   are detailed in the description of that command.  In addition, below
+   is listed a general set of response codes that may be received at any
+   time.
+
+   Certain status responses contain parameters such as numbers and
+   names. The number and type of such parameters is fixed for each
+   response code to simplify interpretation of the response.
+
+   Parameters are separated from the numeric response code and from each
+   other by a single space. All numeric parameters are decimal, and may
+   have leading zeros. All string parameters begin after the separating
+   space, and end before the following separating space or the CR-LF
+   pair at the end of the line. (String parameters may not, therefore,
+   contain spaces.) All text, if any, in the response which is not a
+   parameter of the response must follow and be separated from the last
+   parameter by a space.  Also, note that the text following a response
+   number may vary in different implementations of the server. The
+   3-digit numeric code should be used to determine what response was
+   sent.
+
+   Response codes not specified in this standard may be used for any
+   installation-specific additional commands also not specified. These
+   should be chosen to fit the pattern of x8x specified above.  (Note
+   that debugging is provided for explicitly in the x9x response codes.)
+   The use of unspecified response codes for standard commands is
+   prohibited.
+
+   We have provided a response pattern x9x for debugging.  Since much
+   debugging output may be classed as "informative messages", we would
+   expect, therefore, that responses 190 through 199 would be used for
+   various debugging outputs.  There is no requirement in this
+   specification for debugging output, but if such is provided over the
+   connected stream, it must use these response codes.  If appropriate
+   to a specific implementation, other x9x codes may be used for
+   debugging.  (An example might be to use e.g., 290 to acknowledge a
+   remote debugging request.)
+
+
+Kantor & Lapsley                                                [Page 7]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+2.4.3.  General Responses
+
+   The following is a list of general response codes that may be sent by
+   the NNTP server.  These are not specific to any one command, but may
+   be returned as the result of a connection, a failure, or some unusual
+   condition.
+
+   In general, 1xx codes may be ignored or displayed as desired;  code
+   200 or 201 is sent upon initial connection to the NNTP server
+   depending upon posting permission; code 400 will be sent when the
+   NNTP server discontinues service (by operator request, for example);
+   and 5xx codes indicate that the command could not be performed for
+   some unusual reason.
+
+      100 help text
+      190
+        through
+      199 debug output
+
+      200 server ready - posting allowed
+      201 server ready - no posting allowed
+
+      400 service discontinued
+
+      500 command not recognized
+      501 command syntax error
+      502 access restriction or permission denied
+      503 program fault - command not performed
+
+3.  Command and Response Details
+
+   On the following pages are descriptions of each command recognized by
+   the NNTP server and the responses which will be returned by those
+   commands.
+
+   Each command is shown in upper case for clarity, although case is
+   ignored in the interpretation of commands by the NNTP server.  Any
+   parameters are shown in lower case.  A parameter shown in [square
+   brackets] is optional.  For example, [GMT] indicates that the
+   triglyph GMT may present or omitted.
+
+   Every command described in this section must be implemented by all
+   NNTP servers.
+
+
+
+
+
+
+Kantor & Lapsley                                                [Page 8]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+   There is no prohibition against additional commands being added;
+   however, it is recommended that any such unspecified command begin
+   with the letter "X" to avoid conflict with later revisions of this
+   specification.
+
+   Implementors are reminded that such additional commands may not
+   redefine specified status response codes.  Using additional
+   unspecified responses for standard commands is also prohibited.
+
+3.1.  The ARTICLE, BODY, HEAD, and STAT commands
+
+   There are two forms to the ARTICLE command (and the related BODY,
+   HEAD, and STAT commands), each using a different method of specifying
+   which article is to be retrieved.  When the ARTICLE command is
+   followed by a message-id in angle brackets ("<" and ">"), the first
+   form of the command is used; when a numeric parameter or no parameter
+   is supplied, the second form is invoked.
+
+   The text of the article is returned as a textual response, as
+   described earlier in this document.
+
+   The HEAD and BODY commands are identical to the ARTICLE command
+   except that they respectively return only the header lines or text
+   body of the article.
+
+   The STAT command is similar to the ARTICLE command except that no
+   text is returned.  When selecting by message number within a group,
+   the STAT command serves to set the current article pointer without
+   sending text. The returned acknowledgement response will contain the
+   message-id, which may be of some value.  Using the STAT command to
+   select by message-id is valid but of questionable value, since a
+   selection by message-id does NOT alter the "current article pointer".
+
+3.1.1.  ARTICLE (selection by message-id)
+
+   ARTICLE <message-id>
+
+   Display the header, a blank line, then the body (text) of the
+   specified article.  Message-id is the message id of an article as
+   shown in that article's header.  It is anticipated that the client
+   will obtain the message-id from a list provided by the NEWNEWS
+   command, from references contained within another article, or from
+   the message-id provided in the response to some other commands.
+
+   Please note that the internally-maintained "current article pointer"
+   is NOT ALTERED by this command. This is both to facilitate the
+   presentation of articles that may be referenced within an article
+
+
+Kantor & Lapsley                                                [Page 9]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+   being read, and because of the semantic difficulties of determining
+   the proper sequence and membership of an article which may have been
+   posted to more than one newsgroup.
+
+3.1.2.  ARTICLE (selection by number)
+
+   ARTICLE [nnn]
+
+   Displays the header, a blank line, then the body (text) of the
+   current or specified article.  The optional parameter nnn is the
+
+   numeric id of an article in the current newsgroup and must be chosen
+   from the range of articles provided when the newsgroup was selected.
+   If it is omitted, the current article is assumed.
+
+   The internally-maintained "current article pointer" is set by this
+   command if a valid article number is specified.
+
+   [the following applies to both forms of the article command.] A
+   response indicating the current article number, a message-id string,
+   and that text is to follow will be returned.
+
+   The message-id string returned is an identification string contained
+   within angle brackets ("<" and ">"), which is derived from the header
+   of the article itself.  The Message-ID header line (required by
+   RFC850) from the article must be used to supply this information. If
+   the message-id header line is missing from the article, a single
+   digit "0" (zero) should be supplied within the angle brackets.
+
+   Since the message-id field is unique with each article, it may be
+   used by a news reading program to skip duplicate displays of articles
+   that have been posted more than once, or to more than one newsgroup.
+
+3.1.3.  Responses
+
+   220 n <a> article retrieved - head and body follow
+           (n = article number, <a> = message-id)
+   221 n <a> article retrieved - head follows
+   222 n <a> article retrieved - body follows
+   223 n <a> article retrieved - request text separately
+   412 no newsgroup has been selected
+   420 no current article has been selected
+   423 no such article number in this group
+   430 no such article found
+
+
+
+
+
+Kantor & Lapsley                                               [Page 10]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+3.2.  The GROUP command
+
+3.2.1.  GROUP
+
+   GROUP ggg
+
+   The required parameter ggg is the name of the newsgroup to be
+   selected (e.g. "net.news").  A list of valid newsgroups may be
+   obtained from the LIST command.
+
+   The successful selection response will return the article numbers of
+   the first and last articles in the group, and an estimate of the
+   number of articles on file in the group.  It is not necessary that
+   the estimate be correct, although that is helpful; it must only be
+   equal to or larger than the actual number of articles on file.  (Some
+   implementations will actually count the number of articles on file.
+   Others will just subtract first article number from last to get an
+   estimate.)
+
+   When a valid group is selected by means of this command, the
+   internally maintained "current article pointer" is set to the first
+   article in the group.  If an invalid group is specified, the
+   previously selected group and article remain selected.  If an empty
+   newsgroup is selected, the "current article pointer" is in an
+   indeterminate state and should not be used.
+
+   Note that the name of the newsgroup is not case-dependent.  It must
+   otherwise match a newsgroup obtained from the LIST command or an
+   error will result.
+
+3.2.2.  Responses
+
+   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.)
+   411 no such news group
+
+
+
+
+
+
+
+
+
+
+
+Kantor & Lapsley                                               [Page 11]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+3.3.  The HELP command
+
+3.3.1.  HELP
+
+   HELP
+
+   Provides a short summary of commands that are understood by this
+   implementation of the server. The help text will be presented as a
+   textual response, terminated by a single period on a line by itself.
+
+   3.3.2.  Responses
+
+   100 help text follows
+
+3.4.  The IHAVE command
+
+3.4.1.  IHAVE
+
+   IHAVE <messageid>
+
+   The IHAVE command informs the server that the client has an article
+   whose id is <messageid>.  If the server desires a copy of that
+   article, it will return a response instructing the client to send the
+   entire article.  If the server does not want the article (if, for
+   example, the server already has a copy of it), a response indicating
+   that the article is not wanted will be returned.
+
+   If transmission of the article is requested, the client should send
+   the entire article, including header and body, in the manner
+   specified for text transmission from the server. A response code
+   indicating success or failure of the transferral of the article will
+   be returned.
+
+   This function differs from the POST command in that it is intended
+   for use in transferring already-posted articles between hosts.
+   Normally it will not be used when the client is a personal
+   newsreading program.  In particular, this function will invoke the
+   server's news posting program with the appropriate settings (flags,
+   options, etc) to indicate that the forthcoming article is being
+   forwarded from another host.
+
+   The server may, however, elect not to post or forward the article if
+   after further examination of the article it deems it inappropriate to
+   do so.  The 436 or 437 error codes may be returned as appropriate to
+   the situation.
+
+   Reasons for such subsequent rejection of an article may include such
+
+
+Kantor & Lapsley                                               [Page 12]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+   problems as inappropriate newsgroups or distributions, disk space
+   limitations, article lengths, garbled headers, and the like.  These
+   are typically restrictions enforced by the server host's news
+   software and not necessarily the NNTP server itself.
+
+3.4.2.  Responses
+
+   235 article transferred ok
+   335 send article to be transferred.  End with <CR-LF>.<CR-LF>
+   435 article not wanted - do not send it
+   436 transfer failed - try again later
+   437 article rejected - do not try again
+
+   An implementation note:
+
+   Because some host news posting software may not be able to decide
+   immediately that an article is inappropriate for posting or
+   forwarding, it is acceptable to acknowledge the successful transfer
+   of the article and to later silently discard it.  Thus it is
+   permitted to return the 235 acknowledgement code and later discard
+   the received article.  This is not a fully satisfactory solution to
+   the problem.  Perhaps some implementations will wish to send mail to
+   the author of the article in certain of these cases.
+
+3.5.  The LAST command
+
+3.5.1.  LAST
+
+   LAST
+
+   The internally maintained "current article pointer" is set to the
+   previous article in the current newsgroup.  If already positioned at
+   the first article of the newsgroup, an error message is returned and
+   the current article remains selected.
+
+   The internally-maintained "current article pointer" is set by this
+   command.
+
+   A response indicating the current article number, and a message-id
+   string will be returned.  No text is sent in response to this
+   command.
+
+3.5.2.  Responses
+
+   223 n a article retrieved - request text separately
+           (n = article number, a = unique article id)
+
+
+
+Kantor & Lapsley                                               [Page 13]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+   412 no newsgroup selected
+   420 no current article has been selected
+   422 no previous article in this group
+
+3.6.  The LIST command
+
+3.6.1.  LIST
+
+   LIST
+
+   Returns a list of valid newsgroups and associated information.  Each
+   newsgroup is sent as a line of text in the following format:
+
+      group last first p
+
+   where <group> is the name of the newsgroup, <last> is the number of
+   the last known article currently in that newsgroup, <first> is the
+   number of the first article currently in the newsgroup, and <p> is
+   either 'y' or 'n' indicating whether posting to this newsgroup is
+   allowed ('y') or prohibited ('n').
+
+   The <first> and <last> fields will always be numeric.  They may have
+   leading zeros.  If the <last> field evaluates to less than the
+   <first> field, there are no articles currently on file in the
+   newsgroup.
+
+   Note that posting may still be prohibited to a client even though the
+   LIST command indicates that posting is permitted to a particular
+   newsgroup. See the POST command for an explanation of client
+   prohibitions.  The posting flag exists for each newsgroup because
+   some newsgroups are moderated or are digests, and therefore cannot be
+   posted to; that is, articles posted to them must be mailed to a
+   moderator who will post them for the submitter.  This is independent
+   of the posting permission granted to a client by the NNTP server.
+
+   Please note that an empty list (i.e., the text body returned by this
+   command consists only of the terminating period) is a possible valid
+   response, and indicates that there are currently no valid newsgroups.
+
+3.6.2.  Responses
+
+   215 list of newsgroups follows
+
+
+
+
+
+
+
+Kantor & Lapsley                                               [Page 14]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+3.7.  The NEWGROUPS command
+
+3.7.1.  NEWGROUPS
+
+   NEWGROUPS date time [GMT] [<distributions>]
+
+   A list of newsgroups created since <date and time> will be listed in
+   the same format as the LIST command.
+
+   The date is sent as 6 digits in the format YYMMDD, where YY is the
+   last two digits of the year, MM is the two digits of the month (with
+   leading zero, if appropriate), and DD is the day of the month (with
+   leading zero, if appropriate).  The closest century is assumed as
+   part of the year (i.e., 86 specifies 1986, 30 specifies 2030, 99 is
+   1999, 00 is 2000).
+
+   Time must also be specified.  It must be as 6 digits HHMMSS with HH
+   being hours on the 24-hour clock, MM minutes 00-59, and SS seconds
+   00-59.  The time is assumed to be in the server's timezone unless the
+   token "GMT" appears, in which case both time and date are evaluated
+   at the 0 meridian.
+
+   The optional parameter "distributions" is a list of distribution
+   groups, enclosed in angle brackets.  If specified, the distribution
+   portion of a new newsgroup (e.g, 'net' in 'net.wombat') will be
+   examined for a match with the distribution categories listed, and
+   only those new newsgroups which match will be listed.  If more than
+   one distribution group is to be listed, they must be separated by
+   commas within the angle brackets.
+
+   Please note that an empty list (i.e., the text body returned by this
+   command consists only of the terminating period) is a possible valid
+   response, and indicates that there are currently no new newsgroups.
+
+3.7.2.  Responses
+
+   231 list of new newsgroups follows
+
+
+
+
+
+
+
+
+
+
+
+
+Kantor & Lapsley                                               [Page 15]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+3.8.  The NEWNEWS command
+
+3.8.1.  NEWNEWS
+
+   NEWNEWS newsgroups date time [GMT] [<distribution>]
+
+   A list of message-ids of articles posted or received to the specified
+   newsgroup since "date" will be listed. The format of the listing will
+   be one message-id per line, as though text were being sent.  A single
+   line consisting solely of one period followed by CR-LF will terminate
+   the list.
+
+   Date and time are in the same format as the NEWGROUPS command.
+
+   A newsgroup name containing a "*" (an asterisk) may be specified to
+   broaden the article search to some or all newsgroups.  The asterisk
+   will be extended to match any part of a newsgroup name (e.g.,
+   net.micro* will match net.micro.wombat, net.micro.apple, etc). Thus
+   if only an asterisk is given as the newsgroup name, all newsgroups
+   will be searched for new news.
+
+   (Please note that the asterisk "*" expansion is a general
+   replacement; in particular, the specification of e.g., net.*.unix
+   should be correctly expanded to embrace names such as net.wombat.unix
+   and net.whocares.unix.)
+
+   Conversely, if no asterisk appears in a given newsgroup name, only
+   the specified newsgroup will be searched for new articles. Newsgroup
+   names must be chosen from those returned in the listing of available
+   groups.  Multiple newsgroup names (including a "*") may be specified
+   in this command, separated by a comma.  No comma shall appear after
+   the last newsgroup in the list.  [Implementors are cautioned to keep
+   the 512 character command length limit in mind.]
+
+   The exclamation point ("!") may be used to negate a match. This can
+   be used to selectively omit certain newsgroups from an otherwise
+   larger list.  For example, a newsgroups specification of
+   "net.*,mod.*,!mod.map.*" would specify that all net.<anything> and
+   all mod.<anything> EXCEPT mod.map.<anything> newsgroup names would be
+   matched.  If used, the exclamation point must appear as the first
+   character of the given newsgroup name or pattern.
+
+   The optional parameter "distributions" is a list of distribution
+   groups, enclosed in angle brackets.  If specified, the distribution
+   portion of an article's newsgroup (e.g, 'net' in 'net.wombat') will
+   be examined for a match with the distribution categories listed, and
+   only those articles which have at least one newsgroup belonging to
+
+
+Kantor & Lapsley                                               [Page 16]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+   the list of distributions will be listed.  If more than one
+   distribution group is to be supplied, they must be separated by
+   commas within the angle brackets.
+
+   The use of the IHAVE, NEWNEWS, and NEWGROUPS commands to distribute
+   news is discussed in an earlier part of this document.
+
+   Please note that an empty list (i.e., the text body returned by this
+   command consists only of the terminating period) is a possible valid
+   response, and indicates that there is currently no new news.
+
+3.8.2.  Responses
+
+   230 list of new articles by message-id follows
+
+3.9.  The NEXT command
+
+3.9.1.  NEXT
+
+   NEXT
+
+   The internally maintained "current article pointer" is advanced to
+   the next article in the current newsgroup.  If no more articles
+   remain in the current group, an error message is returned and the
+   current article remains selected.
+
+   The internally-maintained "current article pointer" is set by this
+   command.
+
+   A response indicating the current article number, and the message-id
+   string will be returned.  No text is sent in response to this
+   command.
+
+3.9.2.  Responses
+
+   223 n a article retrieved - request text separately
+           (n = article number, a = unique article id)
+   412 no newsgroup selected
+   420 no current article has been selected
+   421 no next article in this group
+
+
+
+
+
+
+
+
+
+Kantor & Lapsley                                               [Page 17]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+3.10.  The POST command
+
+3.10.1.  POST
+
+   POST
+
+   If posting is allowed, response code 340 is returned to indicate that
+   the article to be posted should be sent. Response code 440 indicates
+   that posting is prohibited for some installation-dependent reason.
+
+   If posting is permitted, the article should be presented in the
+   format specified by RFC850, and should include all required header
+   lines. After the article's header and body have been completely sent
+   by the client to the server, a further response code will be returned
+   to indicate success or failure of the posting attempt.
+
+   The text forming the header and body of the message to be posted
+   should be sent by the client using the conventions for text received
+   from the news server:  A single period (".") on a line indicates the
+   end of the text, with lines starting with a period in the original
+   text having that period doubled during transmission.
+
+   No attempt shall be made by the server to filter characters, fold or
+   limit lines, or otherwise process incoming text.  It is our intent
+   that the server just pass the incoming message to be posted to the
+   server installation's news posting software, which is separate from
+   this specification.  See RFC850 for more details.
+
+   Since most installations will want the client news program to allow
+   the user to prepare his message using some sort of text editor, and
+   transmit it to the server for posting only after it is composed, the
+   client program should take note of the herald message that greeted it
+   when the connection was first established. This message indicates
+   whether postings from that client are permitted or not, and can be
+   used to caution the user that his access is read-only if that is the
+   case. This will prevent the user from wasting a good deal of time
+   composing a message only to find posting of the message was denied.
+   The method and determination of which clients and hosts may post is
+   installation dependent and is not covered by this specification.
+
+3.10.2.  Responses
+
+   240 article posted ok
+   340 send article to be posted. End with <CR-LF>.<CR-LF>
+   440 posting not allowed
+   441 posting failed
+
+
+
+Kantor & Lapsley                                               [Page 18]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+   (for reference, one of the following codes will be sent upon initial
+   connection; the client program should determine whether posting is
+   generally permitted from these:) 200 server ready - posting allowed
+   201 server ready - no posting allowed
+
+3.11.  The QUIT command
+
+3.11.1.  QUIT
+
+   QUIT
+
+   The server process acknowledges the QUIT command and then closes the
+   connection to the client.  This is the preferred method for a client
+   to indicate that it has finished all its transactions with the NNTP
+   server.
+
+   If a client simply disconnects (or the connection times out, or some
+   other fault occurs), the server should gracefully cease its attempts
+   to service the client.
+
+3.11.2.  Responses
+
+   205 closing connection - goodbye!
+
+3.12.  The SLAVE command
+
+3.12.1.  SLAVE
+
+   SLAVE
+
+   Indicates to the server that this client connection is to a slave
+   server, rather than a user.
+
+   This command is intended for use in separating connections to single
+   users from those to subsidiary ("slave") servers.  It may be used to
+   indicate that priority should therefore be given to requests from
+   this client, as it is presumably serving more than one person.  It
+   might also be used to determine which connections to close when
+   system load levels are exceeded, perhaps giving preference to slave
+   servers.  The actual use this command is put to is entirely
+   implementation dependent, and may vary from one host to another.  In
+   NNTP servers which do not give priority to slave servers, this
+   command must nonetheless be recognized and acknowledged.
+
+3.12.2.  Responses
+
+   202 slave status noted
+
+
+Kantor & Lapsley                                               [Page 19]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+4.  Sample Conversations
+
+   These are samples of the conversations that might be expected with
+   the news server in hypothetical sessions.  The notation C: indicates
+   commands sent to the news server from the client program; S: indicate
+   responses received from the server by the client.
+
+4.1.  Example 1 - relative access with NEXT
+
+   S:      (listens at TCP port 119)
+
+   C:      (requests connection on TCP port 119)
+   S:      200 wombatvax news server ready - posting ok
+
+   (client asks for a current newsgroup list)
+   C:      LIST
+   S:      215 list of newsgroups follows
+   S:      net.wombats 00543 00501 y
+   S:      net.unix-wizards 10125 10011 y
+           (more information here)
+   S:      net.idiots 00100 00001 n
+   S:      .
+
+   (client selects a newsgroup)
+   C:      GROUP net.unix-wizards
+   S:      211 104 10011 10125 net.unix-wizards group selected
+           (there are 104 articles on file, from 10011 to 10125)
+
+   (client selects an article to read)
+   C:      STAT 10110
+   S:      223 10110 <23445@sdcsvax.ARPA> article retrieved - statistics
+           only (article 10110 selected, its message-id is
+           <23445@sdcsvax.ARPA>)
+
+   (client examines the header)
+   C:      HEAD
+   S:      221 10110 <23445@sdcsvax.ARPA> article retrieved - head
+           follows (text of the header appears here)
+   S:      .
+
+   (client wants to see the text body of the article)
+   C:      BODY
+   S:      222 10110 <23445@sdcsvax.ARPA> article retrieved - body
+           follows (body text here)
+   S:      .
+
+   (client selects next article in group)
+
+
+Kantor & Lapsley                                               [Page 20]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+   C:      NEXT
+   S:      223 10113 <21495@nudebch.uucp> article retrieved - statistics
+           only (article 10113 was next in group)
+
+   (client finishes session)
+   C:      QUIT
+   S:      205 goodbye.
+
+4.2.  Example 2 - absolute article access with ARTICLE
+
+   S:      (listens at TCP port 119)
+
+   C:      (requests connection on TCP port 119)
+   S:      201 UCB-VAX netnews server ready -- no posting allowed
+
+   C:      GROUP msgs
+   S:      211 103 402 504 msgs Your new group is msgs
+           (there are 103 articles, from 402 to 504)
+
+   C:      ARTICLE 401
+   S:      423 No such article in this newsgroup
+
+   C:      ARTICLE 402
+   S:      220 402 <4105@ucbvax.ARPA> Article retrieved, text follows
+   S:      (article header and body follow)
+   S:      .
+
+   C:      HEAD 403
+   S:      221 403 <3108@mcvax.UUCP> Article retrieved, header follows
+   S:      (article header follows)
+   S:      .
+
+   C:      QUIT
+   S:      205 UCB-VAX news server closing connection.  Goodbye.
+
+4.3.  Example 3 - NEWGROUPS command
+
+   S:      (listens at TCP port 119)
+
+   C:      (requests connection on TCP port 119)
+   S:      200 Imaginary Institute News Server ready (posting ok)
+
+   (client asks for new newsgroups since April 3, 1985)
+   C:      NEWGROUPS 850403 020000
+
+   S:      231 New newsgroups since 03/04/85 02:00:00 follow
+
+
+
+Kantor & Lapsley                                               [Page 21]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+   S:      net.music.gdead
+   S:      net.games.sources
+   S:      .
+
+   C:      GROUP net.music.gdead
+   S:      211 0 1 1 net.music.gdead Newsgroup selected
+           (there are no articles in that newsgroup, and
+           the first and last article numbers should be ignored)
+
+   C:      QUIT
+   S:      205 Imaginary Institute news server ceasing service.  Bye!
+
+4.4.  Example 4 - posting a news article
+
+   S:      (listens at TCP port 119)
+
+   C:      (requests connection on TCP port 119)
+   S:      200 BANZAIVAX news server ready, posting allowed.
+
+   C:      POST
+   S:      340 Continue posting; Period on a line by itself to end
+   C:      (transmits news article in RFC850 format)
+   C:      .
+   S:      240 Article posted successfully.
+
+   C:      QUIT
+   S:      205 BANZAIVAX closing connection.  Goodbye.
+
+4.5.  Example 5 - interruption due to operator request
+
+   S:      (listens at TCP port 119)
+
+   C:      (requests connection on TCP port 119)
+   S:      201 genericvax news server ready, no posting allowed.
+
+           (assume normal conversation for some time, and
+           that a newsgroup has been selected)
+
+   C:      NEXT
+   S:      223 1013 <5734@mcvax.UUCP> Article retrieved; text separate.
+
+   C:      HEAD
+   C:      221 1013 <5734@mcvax.UUCP> Article retrieved; head follows.
+
+   S:      (sends head of article, but halfway through is
+           interrupted by an operator request.  The following
+           then occurs, without client intervention.)
+
+
+Kantor & Lapsley                                               [Page 22]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+   S:      (ends current line with a CR-LF pair)
+   S:      .
+   S:      400 Connection closed by operator.  Goodbye.
+   S:      (closes connection)
+
+4.6.  Example 6 - Using the news server to distribute news between
+      systems.
+
+   S:      (listens at TCP port 119)
+
+   C:      (requests connection on TCP port 119)
+   S:      201 Foobar NNTP server ready (no posting)
+
+   (client asks for new newsgroups since 2 am, May 15, 1985)
+   C:      NEWGROUPS 850515 020000
+   S:      235 New newsgroups since 850515 follow
+   S:      net.fluff
+   S:      net.lint
+   S:      .
+
+   (client asks for new news articles since 2 am, May 15, 1985)
+   C:      NEWNEWS * 850515 020000
+   S:      230 New news since 850515 020000 follows
+   S:      <1772@foo.UUCP>
+   S:      <87623@baz.UUCP>
+   S:      <17872@GOLD.CSNET>
+   S:      .
+
+   (client asks for article <1772@foo.UUCP>)
+   C:      ARTICLE <1772@foo.UUCP>
+   S:      220 <1772@foo.UUCP> All of article follows
+   S:      (sends entire message)
+   S:      .
+
+   (client asks for article <87623@baz.UUCP>
+   C:      ARTICLE <87623@baz.UUCP>
+   S:      220 <87623@baz.UUCP> All of article follows
+   S:      (sends entire message)
+   S:      .
+
+   (client asks for article <17872@GOLD.CSNET>
+   C:      ARTICLE <17872@GOLD.CSNET>
+   S:      220 <17872@GOLD.CSNET> All of article follows
+   S:      (sends entire message)
+   S:      .
+
+
+
+
+Kantor & Lapsley                                               [Page 23]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+   (client offers an article it has received recently)
+   C:      IHAVE <4105@ucbvax.ARPA>
+   S:      435 Already seen that one, where you been?
+
+   (client offers another article)
+   C:      IHAVE <4106@ucbvax.ARPA>
+   S:      335 News to me!  <CRLF.CRLF> to end.
+   C:      (sends article)
+   C:      .
+   S:      235 Article transferred successfully.  Thanks.
+
+   (or)
+
+   S:      436 Transfer failed.
+
+   (client is all through with the session)
+   C:      QUIT
+   S:      205 Foobar NNTP server bids you farewell.
+
+4.7.  Summary of commands and responses.
+
+   The following are the commands recognized and responses returned by
+   the NNTP server.
+
+4.7.1.  Commands
+
+   ARTICLE
+   BODY
+   GROUP
+   HEAD
+   HELP
+   IHAVE
+   LAST
+   LIST
+   NEWGROUPS
+   NEWNEWS
+   NEXT
+   POST
+   QUIT
+   SLAVE
+   STAT
+
+4.7.2.  Responses
+
+   100 help text follows
+   199 debug output
+
+
+
+Kantor & Lapsley                                               [Page 24]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+   200 server ready - posting allowed
+   201 server ready - no posting allowed
+   202 slave status noted
+   205 closing connection - goodbye!
+   211 n f l s group selected
+   215 list of newsgroups follows
+   220 n <a> article retrieved - head and body follow 221 n <a> article
+   retrieved - head follows
+   222 n <a> article retrieved - body follows
+   223 n <a> article retrieved - request text separately 230 list of new
+   articles by message-id follows
+   231 list of new newsgroups follows
+   235 article transferred ok
+   240 article posted ok
+
+   335 send article to be transferred.  End with <CR-LF>.<CR-LF>
+   340 send article to be posted. End with <CR-LF>.<CR-LF>
+
+   400 service discontinued
+   411 no such news group
+   412 no newsgroup has been selected
+   420 no current article has been selected
+   421 no next article in this group
+   422 no previous article in this group
+   423 no such article number in this group
+   430 no such article found
+   435 article not wanted - do not send it
+   436 transfer failed - try again later
+   437 article rejected - do not try again.
+   440 posting not allowed
+   441 posting failed
+
+   500 command not recognized
+   501 command syntax error
+   502 access restriction or permission denied
+   503 program fault - command not performed
+
+4.8.  A Brief Word about the USENET News System
+
+   In the UNIX world, which traditionally has been linked by 1200 baud
+   dial-up telephone lines, the USENET News system has evolved to handle
+   central storage, indexing, retrieval, and distribution of news.  With
+   the exception of its underlying transport mechanism (UUCP), USENET
+   News is an efficient means of providing news and bulletin service to
+   subscribers on UNIX and other hosts worldwide.  The USENET News
+
+
+
+
+Kantor & Lapsley                                               [Page 25]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+   system is discussed in detail in RFC 850.  It runs on most versions
+   of UNIX and on many other operating systems, and is customarily
+   distributed without charge.
+
+   USENET uses a spooling area on the UNIX host to store news articles,
+   one per file. Each article consists of a series of heading text,
+   which contain the sender's identification and organizational
+   affiliation, timestamps, electronic mail reply paths, subject,
+   newsgroup (subject category), and the like.  A complete news article
+   is reproduced in its entirety below.  Please consult RFC 850 for more
+   details.
+
+      Relay-Version: version B 2.10.3 4.3bsd-beta 6/6/85; site
+      sdcsvax.UUCP
+      Posting-Version: version B 2.10.1 6/24/83 SMI; site unitek.uucp
+      Path:sdcsvax!sdcrdcf!hplabs!qantel!ihnp4!alberta!ubc-vision!unitek
+      !honman
+      From: honman@unitek.uucp (Man Wong)
+      Newsgroups: net.unix-wizards
+      Subject: foreground -> background ?
+      Message-ID: <167@unitek.uucp>
+      Date: 25 Sep 85 23:51:52 GMT
+      Date-Received: 29 Sep 85 09:54:48 GMT
+      Reply-To: honman@unitek.UUCP (Hon-Man Wong)
+      Distribution: net.all
+      Organization: Unitek Technologies Corporation
+      Lines: 12
+
+      I have a process (C program) which generates a child and waits for
+      it to return.  What I would like to do is to be able to run the
+      child process interactively for a while before kicking itself into
+      the background so I can return to the parent process (while the
+      child process is RUNNING in the background).  Can it be done?  And
+      if it can, how?
+
+      Please reply by E-mail.  Thanks in advance.
+
+      Hon-Man Wong
+
+
+
+
+
+
+
+
+
+
+
+Kantor & Lapsley                                               [Page 26]
+\f
+
+
+RFC 977                                                    February 1986
+Network News Transfer Protocol
+
+
+5.  References
+
+   [1]  Crocker, D., "Standard for the Format of ARPA Internet Text
+        Messages", RFC-822, Department of Electrical Engineering,
+        University of Delaware, August, 1982.
+
+   [2]  Horton, M., "Standard for Interchange of USENET Messages",
+        RFC-850, USENET Project, June, 1983.
+
+   [3]  Postel, J., "Transmission Control Protocol- DARPA Internet
+        Program Protocol Specification", RFC-793, USC/Information
+        Sciences Institute, September, 1981.
+
+   [4]  Postel, J., "Simple Mail Transfer Protocol", RFC-821,
+        USC/Information Sciences Institute, August, 1982.
+
+6.  Acknowledgements
+
+   The authors wish to express their heartfelt thanks to those many
+   people who contributed to this specification, and especially to Erik
+   Fair and Chuq von Rospach, without whose inspiration this whole thing
+   would not have been necessary.
+
+7.  Notes
+
+   <1> UNIX is a trademark of Bell Laboratories.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Kantor & Lapsley                                               [Page 27]
+\f
diff --git a/sharedsecret.c b/sharedsecret.c
new file mode 100644 (file)
index 0000000..b264749
--- /dev/null
@@ -0,0 +1,101 @@
+/**/
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <unistd.h>
+
+#include "md5.h"
+#define MAX_SECRET 100
+
+static void ohshit(const char *p) {
+  fprintf(stderr,"md5cookie1way: fatal error: %s\n",p); exit(1);
+}
+
+static void ohshite(const char *p) {
+  fprintf(stderr,"md5cookie1way: fatal system error: %s: %s\n",p,strerror(errno));
+  exit(1);
+}
+
+static void try(FILE **fp, const char *fn) {
+  if (*fp) return;
+  *fp= fopen(fn,"r");
+}
+
+static int parsehex(char *p, unsigned char *up, int maxlen) {
+  int nused, v, n;
+  
+  nused= 0;
+  while (nused < maxlen && (n=-1, sscanf(p,"%x%n",&v,&n) >0) && n>0) {
+    p+= n;
+    *up++= v;
+    nused++;
+    if (*p == ':') p++;
+  }
+  return nused;
+}
+
+int main(int argc, char **argv) {
+  char *authfds, *claim, *here, *p;
+  int rfd, wfd, n, nused;
+  char buf[200];
+  unsigned char message[16+MAX_SECRET], reply[16];
+  struct MD5Context md5ctx;
+  FILE *file;
+
+  if (argc != 2) ohshit("bad args");
+  claim= argv[1];
+
+  authfds= getenv("NNTP_AUTH_FDS"); if (!authfds) ohshit("no NNTP_AUTH_FDS");
+  p= strchr(authfds,'.'); if (!p) ohshit("no . in NNTP_AUTH_FDS");
+  rfd= atoi(authfds);
+  wfd= atoi(p+1);
+  dup2(rfd,0); dup2(wfd,1);
+
+  file= 0;
+  if ((p= getenv("HOME"))) {
+    sprintf(buf,"%.180s/News/md5cookies",p); try(&file,buf);
+    sprintf(buf,"%.180s/.newscookies",p); try(&file,buf);
+  }
+  try(&file,"/etc/news/md5cookies.read");
+  try(&file,"/etc/news/md5cookies");
+  if (!file) ohshite("no cookies file");
+
+  for (;;) {
+    errno= 0; if (!(fgets(buf,sizeof(buf),file))) ohshite("no line for claim");
+    if (!*buf || *buf == '\n' || *buf == '#' || *buf == '@') continue;
+    here= strtok(buf," \t\n");
+    if (!here) continue;
+    if (!strcmp(here,claim)) break;
+  }
+
+  p= strtok(0," \t\n");
+  if (!p) ohshit("no cookie on line");
+
+  nused= parsehex(p,message+16,MAX_SECRET);
+
+  errno= 0; if (!fgets(buf,sizeof(buf),stdin)) ohshite("no comm with server");
+  if (strncmp(buf,"100 ",4)) {
+    fprintf(stderr,"\nunable to authenticate - server sent:\n %s",buf);
+    exit(1);
+  }
+
+  if (parsehex(buf+4,message,17) != 16) ohshit("server sent wrong amount of hex");
+
+  MD5Init(&md5ctx);
+  MD5Update(&md5ctx,message,16+nused);
+  MD5Final(reply,&md5ctx);
+
+  printf("MD5 ");
+  for (n=0; n<16; n++) {
+    printf("%s%02x", n?":":"", reply[n]);
+  }
+  printf("\r\n");
+  fflush(stdout);
+
+  errno= 0; if (!fgets(buf,sizeof(buf),stdin)) ohshite("no reply from server");
+  if (strncmp(buf,"281",3)) ohshit("server didn't send 281");
+
+  exit(0);
+}