Revised patch for merging nnrpperlauth and readers.conf

Erik Klavon erik at eriq.org
Tue Jan 22 06:15:18 UTC 2002



Greetings

I have been making changes to nnrpd in order to combine the
functionality of the perl authentication hooks and readers.conf. This
has been achived through the addition of two new parameters for auth
groups. The diff of these modifications (revised version with
documentation and examples) are at the end of this message.

perl_auth allows the use of perl to authenticate a user. It works in
the same manner as auth:

perl_auth: "/path/to/script/auth1.pl"

It is a special type of auth: the only difference is that it calls the
script given as argument using the perl hook rather then an external
program. Multiple/mixed use of auth: and perl_auth: is permitted
within any auth group. Each method is tried in order as they appear in
the auth group. Initialization of the perl code is delayed so that it
is only loaded when needed (when the nnrpd-perl auth method is reached
or the perl filter is used or perl_access is reached as described
below).

The file given as argument to perl_auth should contain the same
procedures as before. The hash global hash %attributes remains the
same, except for the removal of the "type" entry which is no longer
needed in this modification. The return array now only contains two
elements, the first of which is the NNTP return code. The second is an
error string which is passed to the client if the error code indicates
that the authentication attempt has failed. This allows a specific
error message to be generated by the perl script in place of
"Authentication failed".

To dynamically generate an access group for an auth group using a perl
script, use the parameter:

perl_access: "/path/to/access.pl"

where access.pl contains a function named "access" described below.
If an auth group is successful and contains a perl_access parameter,
then the argument perl script will be used to create an access
group. This group will then be used to determine the access rights of
the client, overriding any access groups in readers.conf. If a
sucessful auth group does not contain the perl_access parameter, then
readers.conf access groups are used to determine the client's rights.

A global hash %attributes exists where `$attributes{hostname}' will
contain the hostname (or the IP address if it doesn't resolve) of the
client machine and `$attributes{ipaddress}' will contain its IP
address (as a string). `$attributes{interface}' will contain the
interface. If a username is available, `$attributes{username}' will
contain the provided username and domain (in username at domain format).

access() returns a hash, containing the desiered access parameters and
values. Here is a trival example:

sub access {
 %return_hash = (
     "read" => "*",
     "post" => "local.*",
     "virtualhost" => "true",
#     ...
 );
 return %return_hash;
}

This functionality should provide all of the existing capabilities of
the perl hook, in combination with the flexability of readers.conf and
the use of other authentication and resolving programs.

The perl_access concept implemented above is due to Jeffrey
M. Vinocur.

---------------------------------------------------

This file should be: inn/samples/nnrpd_access.pl.in

---------------------------------------------------

#! /usr/bin/perl
# fixscript will replace this line with require innshellvars.pl

##
##  Sample code for the nnrpd Perl access hooks.

##  This file is loaded when a perl_access: parameter is reached in
##  readers.conf.  If it defines a sub named access, which will be
##  called during processing of a perl_access: parameter. Attributes
##  about the connection are passed to the program in the %attributes
##  global variable.  It should return a hash containing
##  parameter-value pairs for the access group. If there is a problem,
##  nnrpd will die and syslog the exact error.

##  The default behavior of the following code is to look for nnrp.access
##  in INN's configuration file directory and to attempt to implement about
##  the same host-based access control as the previous nnrp.access code in
##  earlier versions of INN.  This may be useful for backward compatibility.

##  This file cannot be run as a standalone script, although it would be
##  worthwhile to add some code so that it could so that one could test the
##  results of various authentication and connection queries from the
##  command line.  The #! line at the top is just so that fixscript will
##  work.

# This function is called when perl_access: is reached in readers.conf.
# For details on all the information passed to it, see
# ~news/doc/hook-perl.
sub access {
   &loadnnrp($inn::newsetc . '/nnrp.access');
   return &checkhost($attributes{hostname}, $attributes{ipaddress});
}

# Called at startup, this loads the nnrp.access file and converts it into a
# convenient internal format for later queries.
sub loadnnrp {
    my $file = shift;
    my ($block, $perm, $user, $pass);

    open (ACCESS, $file) or die "Could not open $file: $!\n";
    local $_;
    while (<ACCESS>) {
        my %tmp;

        chomp;
        s/\#.*//;
        ($block, $perm, $user, $pass, $tmp{groups}) = split /:/;
        next unless (defined $tmp{groups});

        # We don't support username/password entries, so be safe.
        next if ($user || $pass);

        # Change the wildmat pattern to a regex (this isn't thorough, as
        # some ranges won't be converted properly, but it should be good
        # enough for this purpose).
        if ($block !~ m%^(?:\d+\.){3}\d+/\d+$%) {
            $block =~ s/\./\\./g;
            $block =~ s/\?/./g;
            $block =~ s/\*/.*/g;
        }
        $tmp{block} = $block;

        $tmp{canread} = ($perm =~ /r/i);
        $tmp{canpost} = ($perm =~ /p/i);

        unshift(@hosts, { %tmp });
    }
    close ACCESS;
}

# Given the hostname and IP address of a connecting host, use our @hosts
# array constructed from nnrp.access and see what permissions that host has.
sub checkhost {
    my ($host, $ip) = @_;
    my %return_hash;
    my $key;
    for $key (@hosts) {
        my ($read, $post) = ($key->{canread}, $key->{canpost});

        # First check for CIDR-style blocks.
        if ($key->{block} =~ m%^(\d+\.\d+\.\d+\.\d+)/(\d+)$%) {
            my $block = unpack('N', pack('C4', split(/\./, $1)));
            my $mask = (0xffffffff << (32 - $2)) & 0xffffffff;
            $block = $block & $mask;
            my $packedip = unpack('N', pack('C4', split(/\./, $ip)));
            if (($packedip & $mask) == $block) {
                if ($read) {
                    $return_hash{"read"} = $key->{groups};
                }
                if ($post) {
                    $return_hash{"post"} = $key->{groups};
                }
                return %return_hash;
            }
        }

        if ($ip =~ /^$key->{block}$/) {
            if ($read) {
                $return_hash{"read"} = $key->{groups};
            }
            if ($post) {
                $return_hash{"post"} = $key->{groups};
            }
            return %return_hash;
        }

        if ($host =~ /^$key->{block}$/) {
            if ($read) {
                $return_hash{"read"} = $key->{groups};
            }
            if ($post) {
                $return_hash{"post"} = $key->{groups};
            }
            return %return_hash;
        }
    }

    # If we fell through to here, nothing matched, so we should deny
    # permissions.
    return %return_hash;
}

---------------------------------------------

What follows is a diff for all modifications.

---------------------------------------------

diff -r -C3 inn/MANIFEST inn_patch/MANIFEST
*** inn/MANIFEST	Wed Jan 16 06:16:39 2002
--- inn_patch/MANIFEST	Thu Jan 17 16:32:17 2002
***************
*** 509,514 ****
--- 508,514 ----
  samples/newsfeeds.in                  innd feed configuration
  samples/nnrpd.py                      Python hooks for nnrpd
  samples/nnrpd.track                   Reader tracking configuration
+ samples/nnrpd_access.pl.in            Sample nnrpd Perl access hooks
  samples/nnrpd_auth.pl.in              Sample nnrpd Perl authorization hooks
  samples/nnrpd_auth.py.in              Sample nnrpd Python authorization hooks
  samples/nntpsend.ctl                  Outgoing nntpsend feed configuration

diff -r -C3 inn/doc/pod/hook-perl.pod inn_patch/doc/pod/hook-perl.pod
*** inn/doc/pod/hook-perl.pod	Sun Mar 26 00:13:21 2000
--- inn_patch/doc/pod/hook-perl.pod	Thu Jan 17 16:57:39 2002
***************
*** 273,284 ****
  
  =head1 The nnrpd Posting Filter
  
! When nnrpd starts, it first loads the file _PATH_PERL_FILTER_NNRPD
! (defined in F<include/paths.h>, by default F<filter_nnrpd.pl>).  This file
! must be located in the directory specified by pathfilter in F<inn.conf>
! (F</usr/local/news/bin/filter> by default).  The default directory for
! filter code can be specified at configure time by giving the flag
! B<--with-filter-dir> to configure.
  
  If F<filter_nnrpd.pl> loads successfully and defines the Perl function
  filter_post(), Perl filtering is turned on.  Otherwise, it's turned off.
--- 273,285 ----
  
  =head1 The nnrpd Posting Filter
  
! Whenever perl support is needed in nnrpd, it first loads the file
! _PATH_PERL_FILTER_NNRPD (defined in F<include/paths.h>, by default
! F<filter_nnrpd.pl>).  This file must be located in the directory
! specified by pathfilter in F<inn.conf> (F</usr/local/news/bin/filter>
! by default).  The default directory for filter code can be specified
! at configure time by giving the flag B<--with-filter-dir> to
! configure.
  
  If F<filter_nnrpd.pl> loads successfully and defines the Perl function
  filter_post(), Perl filtering is turned on.  Otherwise, it's turned off.
***************
*** 339,401 ****
  
  =head1 Perl Authentication Support for nnrpd
  
! The functionality described in this section is likely to be merged into
! the new F<readers.conf> method of specifying reader authentication,
! probably as an authentication type of Perl.  The details are likely to be
! substantially the same, but there may be some minor changes.  The
! following documentation describes the current method.
  
! If nnrpperlauth in F<inn.conf> is set to true, nnrpd will authenticate
! readers by calling a Perl function rather than reading F<readers.conf> and
! using the normal authentication mechanism.  If it is set, nnrpd loads
! _PATH_PERL_AUTH (defined in F<include/paths.h>, by default
! F<nnrpd_auth.pl>).  This file must be located in the directory specified
! by pathfilter in F<inn.conf> (F</usr/local/news/bin/filter> by default).
! The default directory for filter code can be specified at configure time
! by giving the flag B<--with-filter-dir> to configure.
! 
! If a Perl function auth_init() is defined by that file, it is called
  immediately after the file is loaded.  It takes no arguments and returns
  nothing.
  
! Provided nnrpperlauth is true, the file loads without errors, auth_init()
! (if present) runs without fatal errors, and a Perl function authenticate()
! is defined, authenticate() will be called during the processing of a
! connection, authentication request, or a disconnect.  authenticate() takes
  no arguments, but it has access to a global hash %attributes which
! contains information about the connection as follows: C<$attributes{type}>
! will contain either "connect", indicating a new connection is in progress,
! or "authenticate", indicating that a client has sent an AUTHINFO command.
! C<$attributes{hostname}> will contain the hostname (or the IP address if
! it doesn't resolve) of the client machine and C<$attributes{ipaddress}>
! will contain its IP address (as a string).  If type was "authenticate",
  C<$attributes{username}> will contain the provided username and
  C<$attributes{password}> the password.
  
! authenticate() should return a five-element array.  The first element is
! the NNTP response code to return to the client, the second element is a
! boolean value indicating whether the client is allowed to read, the third
! element is a boolean value indicating whether the client is allowed to
! post, the fourth element is a wildmat(3) expression that says what groups
! the client is allowed to read, and the fifth element is the maximum bytes
! per second a client is permitted to use for retrieving articles.
  
! If type is connect, the NNTP response code should probably be chosen from
! one of the following values:  200 (reading and posting allowd), 201 (no
! posting allowed), 480 (authentication required), or 502 (permission
! denied).  If the code returned is 502, nnrpd will print a permission
! refused message, drop the connection, and exit.
  
- If the type is authentication, the NNTP response code should probably be
- either 281 (authentication successful) or 502 (authentication
- unsuccessful).  If the code returned is anything other than 281, nnrpd
- will print an authentication error message and drop the connection and
- exit.
- 
  If authenticate() dies (either due to a Perl error or due to calling die),
! or if it returns anything other than the four-element array described
  above, an internal error will be reported to the client, the exact error
  will be logged to syslog, and nnrpd will drop the connection and exit.
  
  =head1 Notes on Writing Embedded Perl
  
--- 340,437 ----
  
  =head1 Perl Authentication Support for nnrpd
  
! Support for authentication via perl is provided in nnrpd by the
! inclusion of a perl_auth: parameter in a F<readers.conf> auth
! group. perl_auth: works exactly like the auth: parameter in
! F<readers.conf>, the only difference is that it calls the script given
! as argument using the perl hook rather then treating it as an external
! program.
  
! If the processing of readers.conf requires that a perl_auth: statement
! be used for authentication, perl is loaded (if it has yet to be) and
! the file given as argument to the perl_auth: parameter is loaded as
! well. If a Perl function auth_init() is defined by that file, it is called
  immediately after the file is loaded.  It takes no arguments and returns
  nothing.
  
! Provided the file loads without errors, auth_init() (if present) runs
! without fatal errors, and a Perl function authenticate() is defined,
! authenticate() will then be called. authenticate() takes
  no arguments, but it has access to a global hash %attributes which
! contains information about the connection as follows:
! C<$attributes{hostname}> will contain the hostname (or the IP address
! if it doesn't resolve) of the client machine, C<$attributes{ipaddress}>
! will contain its IP address (as a string), C<$attributes{interface}>
! contains the interface the client connected on, 
  C<$attributes{username}> will contain the provided username and
  C<$attributes{password}> the password.
  
! authenticate() should return a two-element array.  The first element is
! the NNTP response code to return to the client, the second element is an
! error string which is passed to the client if the response code indicates
! that the authentication attempt has failed.
  
! The NNTP response code should probably be either 281 (authentication
! successful) or 502 (authentication unsuccessful).  If the code
! returned is anything other than 281, nnrpd will print an
! authentication error message and drop the connection and exit.
  
  If authenticate() dies (either due to a Perl error or due to calling die),
! or if it returns anything other than the two-element array described
  above, an internal error will be reported to the client, the exact error
  will be logged to syslog, and nnrpd will drop the connection and exit.
+ 
+ =head1 Dynamic Generation of Access Groups
+ 
+ A perl script may be used to dynamically generate an access group
+ which is then used to determine the access rights of the client. This
+ occurs whenever the perl_access: is specified in an auth group which
+ has successfully matched the client. 
+ 
+ When a perl_access: parameter is encountered, perl is loaded (if it
+ has yet to be) and the file given as argument is loaded as
+ well. Provided the file loads without errors, and a Perl function
+ access() is defined, access() will then be called. access() takes
+ no arguments, but it has access to a global hash %attributes which
+ contains information about the connection as follows:
+ C<$attributes{hostname}> will contain the hostname (or the IP address
+ if it doesn't resolve) of the client machine, C<$attributes{ipaddress}>
+ will contain its IP address (as a string), C<$attributes{interface}>
+ contains the interface the client connected on. If it is available,  
+ C<$attributes{username}> will contain the provided username and domain
+ (in username at domain form).
+ 
+ access() returns a hash, containing the desired  access parameters and
+ values. Here is a trivial example:
+ 
+      sub access {
+       %return_hash = (
+           "read" => "*",
+           "post" => "local.*",
+           "virtualhost" => "true",
+      #     ...
+       );
+       return %return_hash;
+      }
+ 
+ Note that both the keys and values are quoted strings. These values
+ are to be returned to a C program and must be quoted strings. For
+ values containing one or more spaces, it is not necessary to include
+ extra quotes inside the string.
+ 
+ While you may include the users: parameter in a dynamically generated
+ access group, some care should be taken (unless your pattern is just
+ * which is equivalent to leaving the parameter out). The group created
+ with the values returned from the perl script is the only one
+ considered when nnrpd attempts to find an access group matching the
+ connection. If a users: parameter is included and it doesn't match the
+ connection, then the client will be denied access since there are no
+ other access groups which could match the connection.
+ 
+ If access() dies (either due to a Perl error or due to calling die),
+ or if it returns anything other than a hash as described
+ above, an internal error will be reported to the client, the exact error
+ will be logged to syslog, and nnrpd will drop the connection and exit.
  
  =head1 Notes on Writing Embedded Perl
  
diff -r -C3 inn/doc/pod/inn.conf.pod inn_patch/doc/pod/inn.conf.pod
*** inn/doc/pod/inn.conf.pod	Tue Sep 25 00:29:13 2001
--- inn_patch/doc/pod/inn.conf.pod	Thu Jan 17 16:29:07 2002
***************
*** 494,506 ****
  number of "article is missing" errors seen by the client.  This is a
  boolean value and the default is true.
  
- =item I<nnrpperlauth>
- 
- Whether to use the Perl hook in nnrpd(8) to authenticate readers.  If this
- is enabled, normal readers.conf(5) authentication will not be used, and
- instead the Perl hook will be called to authenticate connections.  This is
- a boolean value and the default is false.
- 
  =item I<nnrppythonauth>
  
  Whether to use the Python hook in nnrpd(8) to authenticate readers.  If
--- 494,499 ----
diff -r -C3 inn/doc/pod/readers.conf.pod inn_patch/doc/pod/readers.conf.pod
*** inn/doc/pod/readers.conf.pod	Sat Jun 16 07:14:22 2001
--- inn_patch/doc/pod/readers.conf.pod	Thu Jan 17 16:26:25 2002
***************
*** 90,99 ****
  
  If the user later authenticates via the AUTHINFO USER/PASS commands, the
  provided username and password is passed to <auth-program>, the value of
! the auth: parameter (if present).  If this succeeds and returns a
! different identity than the one assigned at the time of the connection, it
! is matched against the available access groups again and the actions the
! user is authorized to do may change.
  
  When matching auth groups, the last auth group in the file that matches a
  given connection or username/password combination is used.
--- 90,99 ----
  
  If the user later authenticates via the AUTHINFO USER/PASS commands, the
  provided username and password is passed to <auth-program>, the value of
! the auth: or perl_auth: parameter (if present).  If this succeeds and
! returns a different identity than the one assigned at the time of the
! connection, it is matched against the available access groups again
! and the actions the user is authorized to do may change.
  
  When matching auth groups, the last auth group in the file that matches a
  given connection or username/password combination is used.
***************
*** 124,130 ****
  various inn.conf(5) parameters for just that group of users.
  
  Just like with auth groups, when matching access groups the last matching
! one in the file is used to determine the user's permissions.
  
  There is one additional special case to be aware of.  When forming
  particularly complex authentication and authorization rules, it is
--- 124,135 ----
  various inn.conf(5) parameters for just that group of users.
  
  Just like with auth groups, when matching access groups the last matching
! one in the file is used to determine the user's permissions. There is
! an exception to this rule: if the auth group which matched the client
! contains the perl_access: parameter then the perl script given as
! argument is used to dynamically generate an access group. This new
! access group is then used to determine the access rights of the
! client.
  
  There is one additional special case to be aware of.  When forming
  particularly complex authentication and authorization rules, it is
***************
*** 218,223 ****
--- 223,242 ----
  auth: parameters; they will be tried in order and the results of the first
  successful one will be used.
  
+ =item B<perl_auth:>
+ 
+ A path to a perl script for authentication. perl_auth: works exactly
+ like auth:, the only difference is that it calls the script given as
+ argument using the perl hook rather then an external
+ program. Multiple/mixed use of auth: and perl_auth: is permitted
+ within any auth group. Each method is tried in order as they appear in
+ the auth group. perl_auth: has some advantages over auth: in that it
+ provides the authentication program with additional information about
+ the client and the ability to return an error string. This parameter
+ is only valid if INN is compiled with Perl support (B<--with-perl>
+ passed to configure). More information may be found in the file
+ F<READEME.hook-perl>.
+ 
  =item B<default:>
  
  The default username for connections matching this auth group.  This is
***************
*** 248,253 ****
--- 267,285 ----
  it is encrypted using SSL.  This parameter is only valid if INN is
  compiled with SSL support (B<--with-openssl> passed to configure).
  
+ =item B<perl_access:>
+ 
+ A path to a perl script for dynamically generating an access group. If
+ an auth group is successful and contains a perl_access parameter, then
+ the argument perl script will be used to create an access group. This
+ group will then be used to determine the access rights of the client,
+ overriding any access groups in readers.conf. If a sucessful auth
+ group does not contain the perl_access parameter, then readers.conf
+ access groups are used to determine the client's rights. This
+ parameter is only valid if INN is compiled with Perl support
+ (B<--with-perl> passed to configure). More information may be found in
+ the file F<README.hook-perl>.
+ 
  =back
  
  =head1 ACCESS GROUP PARAMETERS
***************
*** 432,446 ****
  
  =item *
  
! When the user authenticates, all auth groups with auth: lines are then
! checked from the bottom up and the first one that returns a valid user is
! kept as the default auth group.
  
  =item *
  
  Regardless of how an auth group is established, as soon as one is, the
  user permissions are granted by scanning the access groups from bottom up
! and finding the first match.
  
  =back
  
--- 464,480 ----
  
  =item *
  
! When the user authenticates, all auth groups with auth: or perl_auth:
! lines are then checked from the bottom up and the first one that
! returns a valid user is kept as the default auth group.
  
  =item *
  
  Regardless of how an auth group is established, as soon as one is, the
  user permissions are granted by scanning the access groups from bottom up
! and finding the first match. Unless the established auth group
! contains the perl_access: parameter, in which case the dyanmically
! generated access group returned by the perl script is used.
  
  =back
  
diff -r -C3 inn/include/conffile.h inn_patch/include/conffile.h
*** inn/include/conffile.h	Fri Nov 19 00:53:12 1999
--- inn_patch/include/conffile.h	Tue Jan 15 21:56:15 2002
***************
*** 12,17 ****
--- 12,19 ----
      char *buf;
      unsigned int sbuf;
      int lineno;
+     int array_len;
+     char **array;
      char *filename;
  } CONFFILE;

diff -r -C3 inn/lib/conffile.c inn_patch/lib/conffile.c
*** inn/lib/conffile.c	Wed Oct  4 17:36:51 2000
--- inn_patch/lib/conffile.c	Mon Jan 21 16:45:08 2002
***************
*** 9,14 ****
--- 9,36 ----
  #include "libinn.h"
  #include "macros.h"
  
+ int getline(CONFFILE *F, char *buffer, int length) {
+   if (F->f) {
+     fgets(buffer, length, F->f);
+   } else if (F->array) {
+     strncpy(buffer, F->array[F->lineno], length);
+   }
+   F->lineno++;
+   if (strlen (F->buf) == F->sbuf) {
+     return 1; /* Line too long */
+   } else {
+     return 0;
+   }
+ }
+ 
+ int cfeof(CONFFILE *F) {
+   if (F->f) {
+     return feof(F->f);
+   } else if (F->array) {
+     return (F->lineno == F->array_len);
+   }
+ }
+ 
  static char *CONFgetword(CONFFILE *F)
  {
    register char *p;
***************
*** 19,32 ****
  
    if (!F) return (NULL);	/* No conf file */
    if (!F->buf || !F->buf[0]) {
!     if (feof (F->f)) return (NULL);
      if (!F->buf) {
        F->sbuf = BIG_BUFFER;
        F->buf = NEW(char, F->sbuf);
      }
!     fgets(F->buf, F->sbuf, F->f);
!     F->lineno++;
!     if (strlen (F->buf) == F->sbuf)
        return (NULL); /* Line too long */
    }
    do {
--- 41,52 ----
  
    if (!F) return (NULL);	/* No conf file */
    if (!F->buf || !F->buf[0]) {
!     if (cfeof (F)) return (NULL);
      if (!F->buf) {
        F->sbuf = BIG_BUFFER;
        F->buf = NEW(char, F->sbuf);
      }
!     if (getline(F, F->buf, F->sbuf) != 0)
        return (NULL); /* Line too long */
    }
    do {
***************
*** 39,54 ****
       }
       for (p = F->buf; *p == ' ' || *p == '\t' ; p++);
       flag = TRUE;
!      if (*p == '\0' && !feof (F->f)) {
         flag = FALSE;
!        fgets(F->buf, F->sbuf, F->f);
!        F->lineno++;
!        if (strlen (F->buf) == F->sbuf)
           return (NULL); /* Line too long */
         continue;
       }
       break;
!   } while (!feof (F->f) || !flag);
  
    if (*p == '"') { /* double quoted string ? */
      p++;
--- 59,72 ----
       }
       for (p = F->buf; *p == ' ' || *p == '\t' ; p++);
       flag = TRUE;
!      if (*p == '\0' && !cfeof(F)) {
         flag = FALSE;
!        if (getline(F, F->buf, F->sbuf))
           return (NULL); /* Line too long */
         continue;
       }
       break;
!   } while (!cfeof(F) || !flag);
  
    if (*p == '"') { /* double quoted string ? */
      p++;
***************
*** 57,65 ****
               *t != '\0'; t++);
        if (*t == '\0') {
          *t++ = '\n';
!         fgets(t, F->sbuf - strlen (F->buf), F->f);
! 	F->lineno++;
!         if (strlen (F->buf) == F->sbuf)
            return (NULL); /* Line too long */
          if ((s = strchr(t, '\n')) != NULL)
            *s = '\0';
--- 75,81 ----
               *t != '\0'; t++);
        if (*t == '\0') {
          *t++ = '\n';
!         if (getline(F, t, F->sbuf - strlen(F->buf)))
            return (NULL); /* Line too long */
          if ((s = strchr(t, '\n')) != NULL)
            *s = '\0';
***************
*** 66,72 ****
        }
        else 
          break;
!     } while (!feof (F->f));
      *t++ = '\0';
    }
    else {
--- 82,88 ----
        }
        else 
          break;
!     } while (!cfeof(F));
      *t++ = '\0';
    }
    else {
***************
*** 74,80 ****
      if (*t != '\0')
        *t++ = '\0';
    }
!   if (*p == '\0' && feof (F->f)) return (NULL);
    word = COPY (p);
    for (p = F->buf; *t != '\0'; t++)
      *p++ = *t;
--- 90,96 ----
      if (*t != '\0')
        *t++ = '\0';
    }
!   if (*p == '\0' && cfeof(F)) return (NULL);
    word = COPY (p);
    for (p = F->buf; *t != '\0'; t++)
      *p++ = *t;
***************
*** 101,106 ****
--- 117,123 ----
    ret->sbuf = 0;
    ret->lineno = 0;
    ret->f = f;
+   ret->array = NULL;
    return(ret);
  }
  
diff -r -C3 inn/nnrpd/commands.c inn_patch/nnrpd/commands.c
*** inn/nnrpd/commands.c	Fri Jun 29 14:42:23 2001
--- inn_patch/nnrpd/commands.c	Mon Jan 14 15:51:57 2002
***************
*** 223,228 ****
--- 223,229 ----
      static char	User[30];
      static char	Password[30];
      char	accesslist[BIG_BUFFER];
+     char        errorstr[BIG_BUFFER];
      int         code;
  
      if (caseEQ(av[1], "generic")) {
***************
*** 285,316 ****
  	    Password[sizeof Password - 1] = 0;
  	}
  
- #ifdef DO_PERL
- 	if (innconf->nnrpperlauth) {
- 	    code = perlAuthenticate(ClientHost, ClientIp, ServerHost, User, Password, accesslist);
- 	    if (code == NNTP_AUTH_OK_VAL) {
- 		PERMspecified = NGgetlist(&PERMreadlist, accesslist);
- 		PERMpostlist = PERMreadlist;
- 		syslog(L_NOTICE, "%s user %s", ClientHost, User);
- 		if (LLOGenable) {
- 			fprintf(locallog, "%s user (%s):%s\n", ClientHost, Username, User);
- 			fflush(locallog);
- 		}
- 		Reply("%d Ok\r\n", NNTP_AUTH_OK_VAL);
- 		/* save these values in case you need them later */
- 		strcpy(PERMuser, User);
- 		strcpy(PERMpass, Password);
- 		PERMneedauth = FALSE;
- 		PERMauthorized = TRUE;
- 		return;
- 	    } else {
- 		syslog(L_NOTICE, "%s bad_auth", ClientHost);
- 		Reply("%d Authentication error\r\n", NNTP_ACCESS_VAL);
- 		ExitWithStats(1, FALSE);
- 	    }
- 	} else {
- #endif /* DO_PERL */
- 
  #ifdef DO_PYTHON
  	    if (innconf->nnrppythonauth) {
  	        if ((code = PY_authenticate(ClientHost, ClientIp, ServerHost, User, Password, accesslist)) < 0) {
--- 286,291 ----
***************
*** 351,357 ****
  		PERMauthorized = TRUE;
  		return;
  	    }
! 	    PERMlogin(User, Password);
  	    PERMgetpermissions();
  	    if (!PERMneedauth) {
  		syslog(L_NOTICE, "%s user %s", ClientHost, User);
--- 326,335 ----
  		PERMauthorized = TRUE;
  		return;
  	    }
! 
!             errorstr[0] = '\0';
! 
!             PERMlogin(User, Password, errorstr);
  	    PERMgetpermissions();
  	    if (!PERMneedauth) {
  		syslog(L_NOTICE, "%s user %s", ClientHost, User);
***************
*** 367,378 ****
  #ifdef	DO_PYTHON
  	}
  #endif	/* DO_PYTHON */
- #ifdef DO_PERL
- 	}
- #endif /* DO_PERL */
  
  	syslog(L_NOTICE, "%s bad_auth", ClientHost);
! 	Reply("%d Authentication error\r\n", NNTP_ACCESS_VAL);
  	ExitWithStats(1, FALSE);
      }
  
--- 345,358 ----
  #ifdef	DO_PYTHON
  	}
  #endif	/* DO_PYTHON */
  
  	syslog(L_NOTICE, "%s bad_auth", ClientHost);
!         if (errorstr[0] != '\0') {
!             syslog(L_NOTICE, "%s perl error str: %s", ClientHost, errorstr);
!             Reply("%d %s\r\n", NNTP_ACCESS_VAL, errorstr);
!         } else {
!             Reply("%d Authentication error\r\n", NNTP_ACCESS_VAL);
!         }
  	ExitWithStats(1, FALSE);
      }
  
diff -r -C3 inn/nnrpd/nnrpd.c inn_patch/nnrpd/nnrpd.c
*** inn/nnrpd/nnrpd.c	Tue Sep 25 00:32:15 2001
--- inn_patch/nnrpd/nnrpd.c	Sun Jan 13 19:13:17 2002
***************
*** 97,102 ****
--- 97,106 ----
  static char 	*HostErrorStr;
  bool GetHostByAddr = TRUE;      /* formerly DO_NNRP_GETHOSTBYADDR */
  
+ #ifdef DO_PERL
+ bool   PerlLoaded = FALSE;
+ #endif DO_PERL
+ 
  extern void	CMDauthinfo();
  extern void	CMDdate();
  extern void	CMDfetch();
***************
*** 504,526 ****
      LogName[sizeof(LogName) - 1] = '\0';
  
      syslog(L_NOTICE, "%s connect", ClientHost);
- #ifdef DO_PERL
-     if (innconf->nnrpperlauth) {
- 	if ((code = perlConnect(ClientHost, ClientIp, ServerHost, accesslist)) == 502) {
- 	    syslog(L_NOTICE, "%s no_access", ClientHost);
- 	    Printf("%d You are not in my access file. Goodbye.\r\n",
- 		   NNTP_ACCESS_VAL);
- 	    ExitWithStats(1, TRUE);
- 	}
- 	PERMspecified = NGgetlist(&PERMreadlist, accesslist);
- 	PERMpostlist = PERMreadlist;
- 	if (!authconf)
- 	    authconf = NEW(ACCESSGROUP, 1);
- 	PERMaccessconf = authconf;
- 	SetDefaultAccess(PERMaccessconf);
-     } else {
- #endif	/* DO_PERL */
- 
  #ifdef DO_PYTHON
      if (innconf->nnrppythonauth) {
          if ((code = PY_authenticate(ClientHost, ClientIp, ServerHost, NULL, NULL, accesslist)) < 0) {
--- 508,513 ----
***************
*** 546,554 ****
  #ifdef DO_PYTHON
      }
  #endif /* DO_PYTHON */
- #ifdef DO_PERL
-     }
- #endif /* DO_PERL */
  }
  
  
--- 533,538 ----
***************
*** 658,682 ****
  
  static void SetupDaemon(void) {
      bool                val;
-     char *path;
      time_t statinterval;
  
- #if defined(DO_PERL)
-     /* Load the Perl code */
-     path = concatpath(innconf->pathfilter, _PATH_PERL_FILTER_NNRPD);
-     PERLsetup(NULL, path, "filter_post");
-     free(path);
-     if (innconf->nnrpperlauth) {
-         path = concatpath(innconf->pathfilter, _PATH_PERL_AUTH);
- 	PERLsetup(NULL, path, "authenticate");
-         free(path);
- 	PerlFilter(TRUE);
- 	perlAuthInit();
-     } else {
- 	PerlFilter(TRUE);
-     }
- #endif /* defined(DO_PERL) */
- 
  #ifdef	DO_PYTHON
      /* Load the Python code */
      if (innconf->nnrppythonauth) {
--- 642,649 ----
diff -r -C3 inn/nnrpd/nnrpd.h inn_patch/nnrpd/nnrpd.h
*** inn/nnrpd/nnrpd.h	Sun Jul 29 07:36:36 2001
--- inn_patch/nnrpd/nnrpd.h	Tue Jan 15 21:33:19 2002
***************
*** 210,217 ****
  #endif
  
  char *HandleHeaders(char *article);
! int perlConnect(char *ClientHost, char *ClientIP, char *ServerHost, char *accesslist);
! int perlAuthenticate(char *ClientHost, char *ClientIP, char *ServerHost, char *user, char *passwd, char *accesslist);
  bool ARTinstorebytoken(TOKEN token);
  
  #ifdef	DO_PYTHON
--- 210,217 ----
  #endif
  
  char *HandleHeaders(char *article);
! char **perlAccess(char *ClientHost, char *ClientIP, char *ServerHost, char *user);
! int perlAuthenticate(char *ClientHost, char *ClientIP, char *ServerHost, char *user, char *passwd, char *accesslist, char *errorstring);
  bool ARTinstorebytoken(TOKEN token);
  
  #ifdef	DO_PYTHON
diff -r -C3 inn/nnrpd/perl.c inn_patch/nnrpd/perl.c
*** inn/nnrpd/perl.c	Wed Oct  4 17:53:26 2000
--- inn_patch/nnrpd/perl.c	Fri Jan 18 16:08:54 2002
***************
*** 41,46 ****
--- 41,49 ----
  extern bool HeadersModified;
  static int HeaderLen;
  
+ extern bool PerlLoaded;
+ void loadPerl(void);
+ 
  /* #define DEBUG_MODIFY only if you want to see verbose outout */
  #ifdef DEBUG_MODIFY
  static FILE *flog;
***************
*** 62,67 ****
--- 65,74 ----
     SV            *modswitch;
     int            OtherSize;
  
+    if(!PerlLoaded) {
+        loadPerl();
+    }
+ 
     if (!PerlFilterActive)
         return NULL; /* not really necessary */
  
***************
*** 186,248 ****
     return NULL;
  }
  
! int perlConnect(char *ClientHost, char *ClientIP, char *ServerHost, char *accesslist) {
!     dSP;
!     HV              *attribs;
!     int             rc;
!     SV              *sv;
!     char            *p;
!     int             code;
!     
!     if (!PerlFilterActive)
! 	return NNTP_ACCESS_VAL;
  
!     ENTER;
!     SAVETMPS;
!     attribs = perl_get_hv("attributes", TRUE);
!     hv_store(attribs, "type", 4, newSVpv("connect", 0), 0);
!     hv_store(attribs, "hostname", 8, newSVpv(ClientHost, 0), 0);
!     hv_store(attribs, "interface", 9, newSVpv(ServerHost, 0), 0);
!     hv_store(attribs, "ipaddress", 9, newSVpv(ClientIP, 0), 0);
!     
!     PUSHMARK(SP);
!     rc = perl_call_pv("authenticate", G_EVAL|G_ARRAY);
  
!     SPAGAIN;
  
!     if (rc == 0 ) { /* Error occured, same as checking $@ */
! 	syslog(L_ERROR, "Perl function authenticate died: %s",
! 	       SvPV(ERRSV, PL_na));
! 	Reply("%d Internal Error (1).  Goodbye\r\n", NNTP_ACCESS_VAL);
! 	ExitWithStats(1, TRUE);
!     }
  
!     if (rc != 5) {
! 	syslog(L_ERROR, "Perl function authenticate returned wrong number of results: %d", rc);
! 	Reply("%d Internal Error (2).  Goodbye\r\n", NNTP_ACCESS_VAL);
! 	ExitWithStats(1, TRUE);
!     }
  
!     MaxBytesPerSecond = POPi;
!     p = POPp;
!     strcpy(accesslist, p); 
!     sv = POPs; PERMcanpost = SvTRUE(sv);
!     sv = POPs; PERMcanread = SvTRUE(sv); 
!     code = POPi;
  
!     if ((code == NNTP_POSTOK_VAL) || (code == NNTP_NOPOSTOK_VAL))
! 	code = PERMcanpost ? NNTP_POSTOK_VAL : NNTP_NOPOSTOK_VAL;
  
!     if (code == NNTP_AUTH_NEEDED_VAL) 
! 	PERMneedauth = TRUE;
  
!     hv_undef(attribs);
  
!     PUTBACK;
!     FREETMPS;
!     LEAVE;
!     
!     return code;
  }
  
  int perlAuthInit(void) {
--- 193,293 ----
     return NULL;
  }
  
! void loadPerl(void) {
!     char *path;
  
!     path = concatpath(innconf->pathfilter, _PATH_PERL_FILTER_NNRPD);
!     PERLsetup(NULL, path, "filter_post");
!     free(path);
!     PerlFilter(TRUE);
!     PerlLoaded = TRUE;
! }
  
! char *itoa(int n) {
!   int i = 0;
!   char *s = "";
!   char *tmp;
  
!   do {
!     tmp = calloc(2, sizeof(char));
!     tmp[0] = n % 10 + '0';
!     tmp[1] = '\0';
!     s = strcat(tmp, s);
!   } while ((n /= 10) > 0);
  
!   return s;
! }
  
! char **perlAccess(char *ClientHost, char *ClientIP, char *ServerHost, char *user) {
!   dSP;
!   HV              *attribs;
!   SV              *sv;
!   int             rc, code, i;
!   char            *p, *key, *val, **access_array;
  
!   if (!PerlFilterActive)
!        return 0;
  
!   ENTER;
!   SAVETMPS;
!   
!   attribs = perl_get_hv("attributes", TRUE);
!   hv_store(attribs, "hostname", 8, newSVpv(ClientHost, 0), 0);
!   hv_store(attribs, "ipaddress", 9, newSVpv(ClientIP, 0), 0);
!   hv_store(attribs, "interface", 9, newSVpv(ServerHost, 0), 0);
!   hv_store(attribs, "username", 8, newSVpv(user, 0), 0);
  
!   PUSHMARK(SP);
  
!   if (perl_get_cv("access", 0) != NULL)
!        rc = perl_call_pv("access", G_EVAL|G_ARRAY);
! 
!   SPAGAIN;
! 
!   if (rc == 0 ) { /* Error occured, same as checking $@ */
!       syslog(L_ERROR, "Perl function access died: %s",
!              SvPV(ERRSV, PL_na));
!       Reply("%d Internal Error (1).  Goodbye\r\n", NNTP_ACCESS_VAL);
!       ExitWithStats(1, TRUE);
!   }
! 
!   if ((rc % 2) != 0) {
!     syslog(L_ERROR, "Perl function access returned an odd number of arguments: %i", rc);
!     Reply("%d Internal Error (2).  Goodbye\r\n", NNTP_ACCESS_VAL);
!     ExitWithStats(1, TRUE);
!   }
!   
!   i = (rc / 2) + 1;
!   access_array = calloc(i, sizeof(char *));
!   i--;
!   p = itoa(i);
!   access_array[0] = COPY(p);
!   free(p);
!   
!   i = 0;
!   
!   for (i = (rc / 2); i >= 1; i--) {
!     sv = POPs;
!     p = SvPV_nolen(sv);
!     val = COPY(p);
!     sv = POPs;
!     p = SvPV_nolen(sv);
!     key = COPY(p);
!   
!     key = strcat(key, ": \"");
!     key = strcat(key, val);
!     key = strcat(key, "\"\n");
!     access_array[i] = COPY(key);
!         
!     free(key);
!     free(val);
!   }
! 
!   PUTBACK;
!   FREETMPS;
!   LEAVE;
! 
!   return access_array;
  }
  
  int perlAuthInit(void) {
***************
*** 279,285 ****
      
  }
  
! int perlAuthenticate(char *ClientHost, char *ClientIP, char *ServerHost, char *user, char *passwd, char *accesslist) {
      dSP;
      HV              *attribs;
      int             rc;
--- 324,330 ----
      
  }
  
! int perlAuthenticate(char *ClientHost, char *ClientIP, char *ServerHost, char *user, char *passwd, char *accesslist, char *errorstring) {
      dSP;
      HV              *attribs;
      int             rc;
***************
*** 293,299 ****
      ENTER;
      SAVETMPS;
      attribs = perl_get_hv("attributes", TRUE);
-     hv_store(attribs, "type", 4, newSVpv("authenticate", 0), 0);
      hv_store(attribs, "hostname", 8, newSVpv(ClientHost, 0), 0);
      hv_store(attribs, "ipaddress", 9, newSVpv(ClientIP, 0), 0);
      hv_store(attribs, "interface", 9, newSVpv(ServerHost, 0), 0);
--- 338,343 ----
***************
*** 312,328 ****
  	ExitWithStats(1, FALSE);
      }
  
!     if (rc != 5) {
  	syslog(L_ERROR, "Perl function authenticate returned wrong number of results: %d", rc);
  	Reply("%d Internal Error (2).  Goodbye\r\n", NNTP_ACCESS_VAL);
  	ExitWithStats(1, FALSE);
      }
  
-     MaxBytesPerSecond = POPi;
      p = POPp;
!     strcpy(accesslist, p); 
!     sv = POPs; PERMcanpost = SvTRUE(sv);
!     sv = POPs; PERMcanread = SvTRUE(sv); 
      code = POPi;
  
      if ((code == NNTP_POSTOK_VAL) || (code == NNTP_NOPOSTOK_VAL))
--- 356,369 ----
  	ExitWithStats(1, FALSE);
      }
  
!     if (rc != 2) {
  	syslog(L_ERROR, "Perl function authenticate returned wrong number of results: %d", rc);
  	Reply("%d Internal Error (2).  Goodbye\r\n", NNTP_ACCESS_VAL);
  	ExitWithStats(1, FALSE);
      }
  
      p = POPp;
!     strcpy(errorstring, p);
      code = POPi;
  
      if ((code == NNTP_POSTOK_VAL) || (code == NNTP_NOPOSTOK_VAL))
diff -r -C3 inn/nnrpd/perm.c inn_patch/nnrpd/perm.c
*** inn/nnrpd/perm.c	Thu Oct 18 20:56:53 2001
--- inn_patch/nnrpd/perm.c	Fri Jan 18 16:04:57 2002
***************
*** 29,34 ****
--- 29,35 ----
  typedef struct _METHOD {
      char *name;
      char *program;
+     int  type;          /* for seperating perl_auth from auth */
      char *users;	/* only used for auth_methods, not for res_methods. */
      char **extra_headers;
      char **extra_logs;
***************
*** 46,51 ****
--- 47,53 ----
      char *default_user;
      char *default_domain;
      char *localaddress;
+     char *perl_access;
  } AUTHGROUP;
  
  typedef struct _GROUP {
***************
*** 58,64 ****
  /* function declarations */
  static void PERMreadfile(char *filename);
  static void authdecl_parse(AUTHGROUP*, CONFFILE*, CONFTOKEN*);
! static void accessdecl_parse(ACCESSGROUP*, CONFFILE*, CONFTOKEN*);
  static void method_parse(METHOD*, CONFFILE*, CONFTOKEN*, int);
  
  static void add_authgroup(AUTHGROUP*);
--- 60,66 ----
  /* function declarations */
  static void PERMreadfile(char *filename);
  static void authdecl_parse(AUTHGROUP*, CONFFILE*, CONFTOKEN*);
! static void accessdecl_parse(ACCESSGROUP *curaccess, CONFFILE *f, CONFTOKEN *tok);
  static void method_parse(METHOD*, CONFFILE*, CONFTOKEN*, int);
  
  static void add_authgroup(AUTHGROUP*);
***************
*** 76,84 ****
  static bool MatchHost(char*, char*, char*);
  static int MatchUser(char*, char*);
  static char *ResolveUser(AUTHGROUP*);
! static char *AuthenticateUser(AUTHGROUP*, char*, char*);
  
  static void GrowArray(void***, void*);
  
  /* global variables */
  static AUTHGROUP **auth_realms;
--- 78,87 ----
  static bool MatchHost(char*, char*, char*);
  static int MatchUser(char*, char*);
  static char *ResolveUser(AUTHGROUP*);
! static char *AuthenticateUser(AUTHGROUP*, char*, char*, char*);
  
  static void GrowArray(void***, void*);
+ static void PERMarraytoaccess(ACCESSGROUP *access, char *name, char **array, int array_len);
  
  /* global variables */
  static AUTHGROUP **auth_realms;
***************
*** 88,93 ****
--- 91,99 ----
  static char	*ConfigBit;
  static int	ConfigBitsize;
  
+ extern int LLOGenable;
+ extern bool PerlLoaded;
+ 
  #define PERMlbrace		1
  #define PERMrbrace		2
  #define PERMgroup		3
***************
*** 143,153 ****
  #define PERMlocaladdress	53
  #define PERMrejectwith		54
  #define PERMmaxbytespersecond	55
  #ifdef HAVE_SSL
! #define PERMrequire_ssl		56
! #define PERMMAX			57
  #else
! #define PERMMAX			56
  #endif
  
  #define TEST_CONFIG(a, b) \
--- 149,161 ----
  #define PERMlocaladdress	53
  #define PERMrejectwith		54
  #define PERMmaxbytespersecond	55
+ #define PERMperl_auth           56
+ #define PERMperl_access         57
  #ifdef HAVE_SSL
! #define PERMrequire_ssl		58
! #define PERMMAX			59
  #else
! #define PERMMAX			58
  #endif
  
  #define TEST_CONFIG(a, b) \
***************
*** 228,233 ****
--- 236,243 ----
    { PERMlocaladdress, "localaddress:" },
    { PERMrejectwith, "reject_with:" },
    { PERMmaxbytespersecond, "max_rate:" },
+   { PERMperl_auth, "perl_auth:" },
+   { PERMperl_access, "perl_access:" },
  #ifdef HAVE_SSL
    { PERMrequire_ssl, "require_ssl:" },
  #endif
***************
*** 280,285 ****
--- 290,297 ----
  	      (void*) COPY(orig->extra_logs[i]));
      }
  
+     ret->type = orig->type;
+ 
      return(ret);
  }
  
***************
*** 363,368 ****
--- 375,385 ----
      else
  	ret->localaddress = 0;
  
+     if (orig->perl_access)
+         ret->perl_access = COPY(orig->perl_access);
+     else
+         ret->perl_access = 0;
+ 
      return(ret);
  }
  
***************
*** 496,501 ****
--- 513,520 ----
  	DISPOSE(del->default_domain);
      if (del->localaddress)
  	DISPOSE(del->localaddress);
+     if (del->perl_access)
+         DISPOSE(del->perl_access);
      DISPOSE(del);
  }
  
***************
*** 669,682 ****
  	}
  	break;
        case PERMauth:
        case PERMauthprog:
  	m = NEW(METHOD, 1);
  	memset(m, 0, sizeof(METHOD));
  	memset(ConfigBit, '\0', ConfigBitsize);
  	GrowArray((void***) &curauth->auth_methods, (void*) m);
! 	if (oldtype == PERMauthprog)
  	    m->program = COPY(tok->name);
! 	else {
  	    m->name = COPY(tok->name);
  	    tok = CONFgettoken(PERMtoks, f);
  
--- 688,710 ----
  	}
  	break;
        case PERMauth:
+       case PERMperl_auth:
        case PERMauthprog:
  	m = NEW(METHOD, 1);
  	memset(m, 0, sizeof(METHOD));
  	memset(ConfigBit, '\0', ConfigBitsize);
  	GrowArray((void***) &curauth->auth_methods, (void*) m);
! 	if (oldtype == PERMauthprog) {
!             m->type = PERMauthprog;
  	    m->program = COPY(tok->name);
! 	} else if (oldtype == PERMperl_auth) {
! #ifdef DO_PERL
!             m->type = PERMperl_auth;
! 	    m->program = COPY(tok->name);
! #else
!             ReportError(f, "perl_auth can not be used in readers.conf: inn not compiled with perl support enabled.");
! #endif
!         } else {
  	    m->name = COPY(tok->name);
  	    tok = CONFgettoken(PERMtoks, f);
  
***************
*** 695,702 ****
  		ReportError(f, "Unexpected EOF.");
  	    }
  	}
- 
  	break;
        case PERMlocaladdress:
  	curauth->localaddress = COPY(tok->name);
  	CompressList(curauth->localaddress);
--- 723,736 ----
  		ReportError(f, "Unexpected EOF.");
  	    }
  	}
  	break;
+       case PERMperl_access:
+ #ifdef DO_PERL
+         curauth->perl_access = COPY(tok->name);
+ #else
+         ReportError(f, "perl_access can not be used in readers.conf: inn not compiled with perl support enabled.");
+ #endif
+         break;
        case PERMlocaladdress:
  	curauth->localaddress = COPY(tok->name);
  	CompressList(curauth->localaddress);
***************
*** 703,709 ****
  	SET_CONFIG(PERMlocaladdress);
  	break;
        default:
! 	ReportError(f, "Unexpected token.");
  	break;
      }
  }
--- 737,744 ----
  	SET_CONFIG(PERMlocaladdress);
  	break;
        default:
!         p = strcat("Unexpected token: ", tok->name);
! 	ReportError(f, p);
  	break;
      }
  }
***************
*** 712,718 ****
  {
      int oldtype, boolval;
      bool bit;
!     char buff[SMBUF], *oldname;
  
      oldtype = tok->type;
      oldname = tok->name;
--- 747,753 ----
  {
      int oldtype, boolval;
      bool bit;
!     char buff[SMBUF], *oldname, *p;
  
      oldtype = tok->type;
      oldname = tok->name;
***************
*** 945,955 ****
  	SET_CONFIG(oldtype);
  	break;
        default:
! 	ReportError(f, "Unexpected token.");
  	break;
      }
  }
  
  static void PERMreadfile(char *filename)
  {
      CONFCHAIN	*cf	    = NULL,
--- 980,1019 ----
  	SET_CONFIG(oldtype);
  	break;
        default:
!         p = strcat("Unexpected token: ", tok->name);
! 	ReportError(f, p);
  	break;
      }
  }
  
+ static void PERMarraytoaccess(ACCESSGROUP *access, char *name, char **array, int array_len) {
+     CONFTOKEN	*tok	    = NULL;
+     CONFFILE    *file;
+     char        *str;
+     int i;
+ 
+     file	= NEW(CONFFILE, 1);
+     memset(file, 0, sizeof(CONFFILE));
+     file->array = array;
+     file->array_len = array_len;
+  
+     memset(ConfigBit, '\0', ConfigBitsize);
+ 
+     SetDefaultAccess(access);
+     str = COPY(name);
+     access->name = str;
+ 
+     for (i = 0; i <= array_len; i++) {
+       tok = CONFgettoken(PERMtoks, file);
+ 
+       if (tok != NULL) {
+         accessdecl_parse(access, file, tok);
+       }
+     }
+     DISPOSE(file);
+     return;       
+ }
+ 
  static void PERMreadfile(char *filename)
  {
      CONFCHAIN	*cf	    = NULL,
***************
*** 963,968 ****
--- 1027,1033 ----
      int		oldtype;
      char	*str	    = NULL;
      char        *path       = NULL;
+     char        *p;
  
      if(filename != NULL) {
  	syslog(L_TRACE, "Reading access from %s", 
***************
*** 1188,1194 ****
  		accessdecl_parse(curgroup->access, cf->f, tok);
  		break;
  	      default:
! 		ReportError(cf->f, "Unexpected token.");
  		break;
  	    }
  	} else if (inwhat == 1) {
--- 1253,1260 ----
  		accessdecl_parse(curgroup->access, cf->f, tok);
  		break;
  	      default:
!                 p = strcat("Unexpected token: ", tok->name);
!                 ReportError(cf->f, p);
  		break;
  	    }
  	} else if (inwhat == 1) {
***************
*** 1334,1340 ****
      }
  }
  
! void PERMlogin(char *uname, char *pass)
  {
      int i   = 0;
      char *runame;
--- 1400,1406 ----
      }
  }
  
! void PERMlogin(char *uname, char *pass, char *errorstr)
  {
      int i   = 0;
      char *runame;
***************
*** 1361,1367 ****
      runame  = NULL;
  
      while (runame == NULL && i--)
! 	runame = AuthenticateUser(auth_realms[i], uname, pass);
      if (runame) {
  	strcpy(PERMuser, runame);
  	uname = strchr(PERMuser, '@');
--- 1427,1433 ----
      runame  = NULL;
  
      while (runame == NULL && i--)
! 	runame = AuthenticateUser(auth_realms[i], uname, pass, errorstr);
      if (runame) {
  	strcpy(PERMuser, runame);
  	uname = strchr(PERMuser, '@');
***************
*** 1403,1408 ****
--- 1469,1479 ----
      char *cp, **list;
      char *user[2];
      static ACCESSGROUP *noaccessconf;
+     char **access_array;
+     char *p, *uname;
+     char *cpp, *perl_path;
+     char **args;
+     int array_len = 0;
  
      if (ConfigBit == NULL) {
  	if (PERMMAX % 8 == 0)
***************
*** 1420,1453 ****
  	PERMaccessconf = noaccessconf;
  	SetDefaultAccess(PERMaccessconf);
  	return;
!     }
!     for (i = 0; access_realms[i]; i++)
! 	;
!     user[0] = PERMuser;
!     user[1] = 0;
!     while (i--) {
! 	if ((!success_auth->key && !access_realms[i]->key) ||
! 	  (access_realms[i]->key && success_auth->key &&
! 	   strcmp(access_realms[i]->key, success_auth->key) == 0)) {
! 	    if (!access_realms[i]->users)
! 		break;
! 	    else if (!*PERMuser)
! 		continue;
! 	    cp = COPY(access_realms[i]->users);
! 	    list = 0;
! 	    NGgetlist(&list, cp);
! 	    if (PERMmatch(list, user)) {
! 		syslog(L_TRACE, "%s match_user %s %s", ClientHost,
! 		  PERMuser, access_realms[i]->users);
! 		DISPOSE(cp);
! 		DISPOSE(list);
! 		break;
! 	    } else
! 		syslog(L_TRACE, "%s no_match_user %s %s", ClientHost,
! 		  PERMuser, access_realms[i]->users);
! 	    DISPOSE(cp);
! 	    DISPOSE(list);
  	}
      }
      if (i >= 0) {
  	/* found the right access group */
--- 1491,1562 ----
  	PERMaccessconf = noaccessconf;
  	SetDefaultAccess(PERMaccessconf);
  	return;
! #ifdef DO_PERL
!     } else if (success_auth->perl_access != NULL) {
!       i = 0;
!       cpp = COPY(success_auth->perl_access);
!       args = 0;
!       Argify(cpp, &args);
!       perl_path = concat(args[0], (char *) 0);
!       if ((perl_path != NULL) && (strlen(perl_path) > 0)) {
!         if(!PerlLoaded) {
!           loadPerl();
!         }
!         PERLsetup(NULL, perl_path, "access");
!         free(perl_path);
! 
!         uname = COPY(PERMuser);
!         DISPOSE(args);        
!         
!         args = perlAccess(ClientHost, ClientIp, ServerHost, uname);
!         p = args[0];
!         array_len = atoi(p);
!         access_array = args;
!         access_array++;
! 
!         DISPOSE(uname);
! 
!         access_realms[0] = NEW(ACCESSGROUP, 1);
!         memset(access_realms[0], 0, sizeof(ACCESSGROUP));
! 
!         PERMarraytoaccess(access_realms[0], "perl-dyanmic", access_array, array_len);
!       } else {
!         syslog(L_ERROR, "No script specified in auth method.\n");
!         Reply("%d NNTP server unavailable. Try later.\r\n", NNTP_TEMPERR_VAL);
!         ExitWithStats(1, TRUE);
!       }
!       DISPOSE(cpp);
!       DISPOSE(args);
! #endif /* DO_PERL */
!     } else {
!       for (i = 0; access_realms[i]; i++)
!         ;
!       user[0] = PERMuser;
!       user[1] = 0;
!       while (i--) {
!         if ((!success_auth->key && !access_realms[i]->key) ||
!             (access_realms[i]->key && success_auth->key &&
!              strcmp(access_realms[i]->key, success_auth->key) == 0)) {
!           if (!access_realms[i]->users)
!             break;
!           else if (!*PERMuser)
!             continue;
!           cp = COPY(access_realms[i]->users);
!           list = 0;
!           NGgetlist(&list, cp);
!           if (PERMmatch(list, user)) {
!             syslog(L_TRACE, "%s match_user %s %s", ClientHost,
!                    PERMuser, access_realms[i]->users);
!             DISPOSE(cp);
!             DISPOSE(list);
!             break;
!           } else
!             syslog(L_TRACE, "%s no_match_user %s %s", ClientHost,
!                    PERMuser, access_realms[i]->users);
!           DISPOSE(cp);
!           DISPOSE(list);
  	}
+       }
      }
      if (i >= 0) {
  	/* found the right access group */
***************
*** 1920,1927 ****
--- 2029,2039 ----
      char *arg0;
      char *resdir;
      char *tmp;
+     char *perl_path;
      EXECSTUFF *foo;
      int done	    = 0;
+     int code;
+     char accesslist[BIG_BUFFER];
      char buf[BIG_BUFFER];
  
      if (!auth->res_methods)
***************
*** 1976,1982 ****
  }
  
  /* execute a series of authenticators to get the remote username */
! static char *AuthenticateUser(AUTHGROUP *auth, char *username, char *password)
  {
      int i, j;
      char *cp;
--- 2088,2094 ----
  }
  
  /* execute a series of authenticators to get the remote username */
! static char *AuthenticateUser(AUTHGROUP *auth, char *username, char *password, char *errorstr)
  {
      int i, j;
      char *cp;
***************
*** 1984,1991 ****
--- 2096,2106 ----
      char *arg0;
      char *resdir;
      char *tmp;
+     char *perl_path;
      EXECSTUFF *foo;
      int done	    = 0;
+     int code;
+     char accesslist[BIG_BUFFER];
      char buf[BIG_BUFFER];
  
      if (!auth->auth_methods)
***************
*** 1997,2002 ****
--- 2112,2150 ----
  
      ubuf[0] = '\0';
      for (i = 0; auth->auth_methods[i]; i++) {
+ #ifdef DO_PERL
+       if (auth->auth_methods[i]->type == PERMperl_auth) {
+             cp = COPY(auth->auth_methods[i]->program);
+             args = 0;
+             Argify(cp, &args);
+             perl_path = concat(args[0], (char *) 0);
+             if ((perl_path != NULL) && (strlen(perl_path) > 0)) {
+                 if(!PerlLoaded) {
+                     loadPerl();
+                 }
+                 PERLsetup(NULL, perl_path, "authenticate");
+                 free(perl_path);
+                 perlAuthInit();
+           
+                 code = perlAuthenticate(ClientHost, ClientIp, ServerHost, username, password, accesslist, errorstr);
+                 if (code == NNTP_AUTH_OK_VAL) {
+                     syslog(L_NOTICE, "%s user %s", ClientHost, username);
+                     if (LLOGenable) {
+                         fprintf(locallog, "%s user %s\n", ClientHost, username);
+                         fflush(locallog);
+                     }
+               
+                     /* save these values in case you need them later */
+                     strcpy(ubuf, username);
+                     break;
+                 } else {
+                     syslog(L_NOTICE, "%s bad_auth", ClientHost);
+                 }            
+             } else {
+               syslog(L_ERROR, "No script specified in auth method.\n");
+             }
+       } else if (auth->auth_methods[i]->type == PERMauthprog) {
+ #endif	/* DO_PERL */    
  	if (auth->auth_methods[i]->users &&
  	  !MatchUser(auth->auth_methods[i]->users, username))
  	    continue;
***************
*** 2038,2043 ****
--- 2186,2194 ----
  	if (done)
  	    /* this authenticator succeeded */
  	    break;
+ #ifdef DO_PERL
+       }
+ #endif /* DO_PERL */
      }
      DISPOSE(resdir);
      if (ubuf[0])
diff -r -C3 inn/samples/Makefile inn_patch/samples/Makefile
*** inn/samples/Makefile	Sun May 13 20:59:08 2001
--- inn_patch/samples/Makefile	Mon Jan 21 13:40:08 2002
***************
*** 9,15 ****
  
  include ../Makefile.global
  
! ALL           = nnrpd_auth.pl
  
  EXTRA         = inn.conf innreport.conf newsfeeds sasl.conf
  
--- 9,15 ----
  
  include ../Makefile.global
  
! ALL           = nnrpd_auth.pl nnrpd_access.pl
  
  EXTRA         = inn.conf innreport.conf newsfeeds sasl.conf
  
***************
*** 29,31 ****
--- 29,32 ----
  FIX             = $(FIXSCRIPT)
  
  nnrpd_auth.pl:	nnrpd_auth.pl.in $(FIX)	; $(FIX) nnrpd_auth.pl.in
+ nnrpd_access.pl:	nnrpd_access.pl.in $(FIX) ; $(FIX) nnrpd_access.pl.in
diff -r -C3 inn/samples/nnrpd_auth.pl.in inn_patch/samples/nnrpd_auth.pl.in
*** inn/samples/nnrpd_auth.pl.in	Mon Jan 15 05:35:58 2001
--- inn_patch/samples/nnrpd_auth.pl.in	Thu Jan 17 16:13:47 2002
***************
*** 1,36 ****
  #! /usr/bin/perl
  # fixscript will replace this line with require innshellvars.pl
  
- ##  $Id: nnrpd_auth.pl.in,v 1.4 2001/01/15 13:35:58 rra Exp $
  ##
  ##  Sample code for the nnrpd Perl authentication hooks.
  ##
! ##  This file is loaded when nnrpd starts up.  If it defines a sub named
! ##  authenticate, that function will be called during processing of a
! ##  connection, an authentication request, or a disconnection.  Attributes
! ##  about the connection are passed to the program in the %attributes global
! ##  variable.  It should return an array with five elements:
  ##
! ##  1) NNTP response code.  Should be one of the codes from %connectcodes
! ##     or %authcodes below to not risk violating the protocol.
! ##  2) Whether reading is allowed.  Should be a boolean value.
! ##  3) Whether posting is allowed.  Should be a boolean value.
! ##  4) A wildmat expression giving what newsgroups to provide access to.
! ##  5) Maximum bytes per second a client is permitted to use for retrieving
! ##     articles (0 indicates no limit).
! ##
! ##  All five are required.  If there is a problem, nnrpd will die and syslog
! ##  the exact error.
  
! ##  The default behavior of the following code is to look for nnrp.access
! ##  in INN's configuration file directory and to attempt to implement about
! ##  the same host-based access control as the previous nnrp.access code in
! ##  earlier versions of INN.  This may be useful for backward compatibility.
! ##
! ##  Usernames and passwords are not implemented.  There is code below to use
! ##  a user database based on CDB_File, but it is not enabled by default and
! ##  is here just as an example of what this code can do.  By default, this
! ##  code just always returns permission denied for authentication requests.
  
  ##  This file cannot be run as a standalone script, although it would be
  ##  worthwhile to add some code so that it could so that one could test the
--- 1,24 ----
  #! /usr/bin/perl
  # fixscript will replace this line with require innshellvars.pl
  
  ##
  ##  Sample code for the nnrpd Perl authentication hooks.
  ##
! ##  This file is loaded when a perl_auth: parameter is reached in
! ##  readers.conf.  If it defines a sub named authenticate, that
! ##  function will be called during processing of a perl_auth:
! ##  parameter. Attributes about the connection are passed to the
! ##  program in the %attributes global variable.  It should return an
! ##  array with two elements:
  ##
! ##  1) NNTP response code.  Should be one of the codes from %authcodes
! ##  below to not risk violating the protocol.  
! ##  2) An error string to be passed to the client.
! ##  Both elements are required.  If there is a problem, nnrpd will die
! ##  and syslog the exact error.
  
! ##  The code below uses a user database based on CDB_File. It is
! ##  provided here as an example of an authentication script.
  
  ##  This file cannot be run as a standalone script, although it would be
  ##  worthwhile to add some code so that it could so that one could test the
***************
*** 39,190 ****
  ##  work.
  
  use strict;
! use vars qw(%attributes %authcodes %connectcodes @hosts %users);
  
- # The following codes are mandated by the NNTP protocol in RFC 977 except
- # for authneeded, which is a widely implemented de facto standard.
- %connectcodes = ('read/post'  => 200,
-                  'read'       => 201,
-                  'authneeded' => 480,
-                  'permdenied' => 502);
- 
  # These codes are a widely implemented de facto standard.
  %authcodes = ('allowed' => 281, 'denied' => 502);
  
! 
! # This is called by nnrpd when it first starts up.  This sub should perform
! # any initialization work that the authentication stuff needs.
  sub auth_init {
!     &loadnnrp($inn::newsetc . '/nnrp.access');
! 
!     #require CDB_File;
!     #tie (%users, 'CDB_File', $inn::pathdb . '/users.cdb')
!     #    or warn "Could not open $inn::pathdb/users.cdb for users: $!\n";
  }
  
! # This function is called for all connections, disconnections, and
! # authentication requests.  For details on all the information passed to it,
! # see ~news/doc/hook-perl.
  sub authenticate {
!     if ($attributes{type} eq 'connect') {
!         return &checkhost($attributes{hostname}, $attributes{ipaddress});
!     } elsif ($attributes{type} eq 'authenticate') {
!         return ($connectcodes{permdenied}, undef, undef, undef, undef);
!         #return &checkuser();
!     } elsif ($attributes{type} eq 'disconnect') {
!         # Do nothing.
!     } else {
!         # This should never be reached.
!         return ($connectcodes{permdenied}, undef, undef, undef, undef);
!     }
  }
  
! 
! # Called at startup, this loads the nnrp.access file and converts it into a
! # convenient internal format for later queries.
! sub loadnnrp {
!     my $file = shift;
!     my ($block, $perm, $user, $pass);
! 
!     open (ACCESS, $file) or die "Could not open $file: $!\n";
!     local $_;
!     while (<ACCESS>) {
!         my %tmp;
! 
!         chomp;
!         s/\#.*//;
!         ($block, $perm, $user, $pass, $tmp{groups}) = split /:/;
!         next unless (defined $tmp{groups});
! 
!         # We don't support username/password entries, so be safe.
!         next if ($user || $pass);
! 
!         # Change the wildmat pattern to a regex (this isn't thorough, as
!         # some ranges won't be converted properly, but it should be good
!         # enough for this purpose).
!         if ($block !~ m%^(?:\d+\.){3}\d+/\d+$%) {
!             $block =~ s/\./\\./g;
!             $block =~ s/\?/./g;
!             $block =~ s/\*/.*/g;
!         }
!         $tmp{block} = $block;
! 
!         $tmp{canread} = ($perm =~ /r/i);
!         $tmp{canpost} = ($perm =~ /p/i);
! 
!         unshift(@hosts, { %tmp });
!     }
!     close ACCESS;
! }
! 
! # Given two boolean values saying whether one can read and whether one can
! # post, convert this to a connection code and return the appropriate one.
! # If the user can neither read nor post, return permdenied.  If you use the
! # checkuser code or some other code to verify authentication requests, this
! # should be changed to return authneeded instead.
! sub permtocode {
!     my ($read, $post) = @_;
! 
!     return $connectcodes{'read/post'} if $post;
!     return $connectcodes{'read'} if $read;
! 
!     #return $connectcodes{'authneeded'};
!     return $connectcodes{'permdenied'};
! }
! 
! # Given the hostname and IP address of a connecting host, use our @hosts
! # array constructed from nnrp.access and see what permissions that host has.
! sub checkhost {
!     my ($host, $ip) = @_;
! 
!     my $key;
!     for $key (@hosts) {
!         my ($read, $post) = ($key->{canread}, $key->{canpost});
!         my $code = permtocode($read, $post);
! 
!         # First check for CIDR-style blocks.
!         if ($key->{block} =~ m%^(\d+\.\d+\.\d+\.\d+)/(\d+)$%) {
!             my $block = unpack('N', pack('C4', split(/\./, $1)));
!             my $mask = (0xffffffff << (32 - $2)) & 0xffffffff;
!             $block = $block & $mask;
!             my $packedip = unpack('N', pack('C4', split(/\./, $ip)));
!             if (($packedip & $mask) == $block) {
!                 return ($code, $read, $post, $key->{groups}, undef);
!             }
!         }
! 
!         if ($ip =~ /^$key->{block}$/) {
!             return ($code, $read, $post, $key->{groups}, undef);
!         }
! 
!         if ($host =~ /^$key->{block}$/) {
!             return ($code, $read, $post, $key->{groups}, undef);
!         }
!     }
! 
!     # If we fell through to here, nothing matched, so we should deny
!     # permissions.
!     return ($connectcodes{permdenied}, undef, undef, undef, undef);
! }
! 
! # This function isn't called by default; it assumes that there's a database
! # tied as %users that contains, keyed by users, a tab-separated list of the
! # password (in crypt format), whether they can post, a wildmat matching what
! # newsgroups they have access to, and the number of bytes per second they're
! # allowed to use.
  sub checkuser {
      my $user = $attributes{'username'};
      my $pass = $attributes{'password'};
  
!     return ($authcodes{denied}, undef, undef, undef, undef)
          unless defined $users{$user};
  
      my ($password, $post, $speed, $subscription) = split(/\t/, $users{$user});
!     return ($authcodes{denied}, undef, undef, undef, undef)
          if (crypt($pass, $password) ne $password);
  
!     $post = lc $post;
!     $post = ($post eq 'y') ? 1 : 0;
!     $subscription ||= '*';
!     return ($authcodes{allowed}, 1, $post, $subscription, $speed);
  }
--- 27,68 ----
  ##  work.
  
  use strict;
! use vars qw(%attributes %authcodes %users);
  
  # These codes are a widely implemented de facto standard.
  %authcodes = ('allowed' => 281, 'denied' => 502);
  
! # This sub should perform any initialization work that the
! # authentication stuff needs.
  sub auth_init {
!     require CDB_File;
!     tie (%users, 'CDB_File', $inn::pathdb . '/users.cdb')
!         or warn "Could not open $inn::pathdb/users.cdb for users: $!\n";
  }
  
! # This function is called for authentication requests.  For details on
! # all the information passed to it, see ~news/doc/hook-perl.
  sub authenticate {
!     return &checkuser();
  }
  
! # This function assumes that there's a database tied as %users that
! # contains, keyed by users, a tab-separated list of the password (in
! # crypt format), whether they can post, a wildmat matching what
! # newsgroups they have access to, and the number of bytes per second
! # they're allowed to use. This section of the code only accesses the
! # username and password fields. See the file nnrpd_access.pl for
! # access rights based on the other fields.
  sub checkuser {
      my $user = $attributes{'username'};
      my $pass = $attributes{'password'};
  
!     return ($authcodes{denied}, "No username given.")
          unless defined $users{$user};
  
      my ($password, $post, $speed, $subscription) = split(/\t/, $users{$user});
!     return ($authcodes{denied}, "Incorrect password.")
          if (crypt($pass, $password) ne $password);
  
!     return ($authcodes{allowed}, "");
  }
diff -r -C3 inn/site/Makefile inn_patch/site/Makefile
*** inn/site/Makefile	Sun May 13 21:12:05 2001
--- inn_patch/site/Makefile	Fri Jan 18 16:36:23 2002
***************
*** 59,65 ****
  	motd.news storage.conf cycbuff.conf buffindexed.conf \
  	innfeed.conf startup_innd.pl filter_innd.pl filter_nnrpd.pl \
  	filter_innd.py INN.py \
! 	startup.tcl filter.tcl nnrpd_auth.pl news2mail.cf readers.conf \
  	radius.conf nnrpd_auth.py ovdb.conf sasl.conf active.minimal \
  	newsgroups.minimal subscriptions
  
--- 59,66 ----
  	motd.news storage.conf cycbuff.conf buffindexed.conf \
  	innfeed.conf startup_innd.pl filter_innd.pl filter_nnrpd.pl \
  	filter_innd.py INN.py \
! 	startup.tcl filter.tcl nnrpd_auth.pl nnrpd_access.pl \
!         news2mail.cf readers.conf \
  	radius.conf nnrpd_auth.py ovdb.conf sasl.conf active.minimal \
  	newsgroups.minimal subscriptions
  
***************
*** 181,186 ****
--- 182,188 ----
  $D$(PATH_TCL_STARTUP): startup.tcl	; $(COPY_RPRI) $? $@
  $D$(PATH_TCL_FILTER): filter.tcl	; $(COPY_RPRI) $? $@
  $D$(PATH_NNRPAUTH): nnrpd_auth.pl       ; $(COPY_RPRI) $? $@
+ $D$(PATH_NNRPACCESS): nnrpd_access.pl     ; $(COPY_RPRI) $? $@
  $D$(PATH_NNRPYAUTH): nnrpd_auth.py      ; $(COPY_RPRI) $? $@
  $D$(PATH_ACTSYNC_CFG): actsync.cfg	; $(COPY_RPRI) $? $@
  $D$(PATH_ACTSYNC_IGN): actsync.ign	; $(COPY_RPRI) $? $@
***************
*** 208,213 ****
--- 210,216 ----
  expire.ctl:	../samples/expire.ctl		; $(COPY) $? $@
  filter.tcl:	../samples/filter.tcl		; $(COPY) $? $@
  nnrpd_auth.pl:  ../samples/nnrpd_auth.pl	; $(COPY) $? $@
+ nnrpd_access.pl:  ../samples/nnrpd_access.pl	; $(COPY) $? $@
  nnrpd_auth.py:  ../samples/nnrpd_auth.py	; $(COPY) $? $@
  filter_innd.pl:	../samples/filter_innd.pl	; $(COPY) $? $@
  filter_nnrpd.pl: ../samples/filter_nnrpd.pl	; $(COPY) $? $@


-- 
erik         | "It is idle to think that, by means of words, | Maurice
  kl at von     | any real communication can ever pass | Maeterlinck
    eriq.org | from one [human] to another." | Silence


More information about the inn-patches mailing list