/*=============================================================================
 * Copyright (c) 1998-1999 The Apache Group.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. All advertising materials mentioning features or use of this
 *    software must display the following acknowledgment:
 *    "This product includes software developed by the Apache Group
 *    for use in the Apache HTTP server project (http://www.apache.org/)."
 *
 * 4. The names "Apache Server" and "Apache Group" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For written permission, please contact
 *    apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * 6. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by the Apache Group
 *    for use in the Apache HTTP server project (http://www.apache.org/)."
 *
 * THIS SOFTWARE IS PROVIDED BY THE APACHE GROUP ``AS IS'' AND ANY
 * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE APACHE GROUP OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *=============================================================================
 *
 * NAME
 *	EAccess - Extended Access control module
 *
 * AUTHOR
 *	Patrick Asty <pasty@micronet.fr>
 *
 * VERSION
 *	$Revision: 2.3.2.9 $
 *
 * DOCUMENTATION
 *	See doc/index.html...
 *
 * CHANGELOG
 *	$Log: mod_eaccess.c,v $
 *	Revision 2.3.2.9  2000/02/15 07:36:00  pasty
 *	No more unescape '+' => ' '.
 *
 *	Revision 2.3.2.8  2000/02/15 06:08:51  pasty
 *	eaccess_unescape: enfin, '\r' suivi de '\n' devient "\n",
 *	cela facilite largement le traitement des body.
 *
 *	Revision 2.3.2.7  2000/02/07 13:42:34  pasty
 *	on ne re-compile l'ER que si on ne l'a pas conservée...
 *
 *	Revision 2.3.2.6  2000/02/07 13:21:39  pasty
 *	EAccessOptim => conf->optim = atoi (a1); ...
 *
 *	Revision 2.3.2.5  2000/02/06 10:51:26  pasty
 *	EAccessOptim 0|1: code, tests, ...
 *	plus de ap_destroy_request_body mais à la place read_length := 0 pour
 *	tromper ap_die (qui appelle ap_destroy_request_body)...
 *
 *	Revision 2.3.2.4  2000/02/01 14:08:47  pasty
 *	ident EAccess/big-body
 *
 *	Revision 2.3.2.3  2000/02/01 13:09:36  pasty
 *	destroy body if denied
 *
 *	Revision 2.3.2.2  2000/01/25 14:35:45  pasty
 *	no more CR-LF problem... (in fact just ignore CR-LF problem...)
 *	EACCESS_BODY_HACK (see doc)
 *
 *	Revision 2.3.1.10  2000/01/21 10:33:51  pasty
 *	grrrrr: \r & \n on the end of body...
 *
 *	Revision 2.3.1.9  2000/01/21 07:02:57  pasty
 *	Check on URL buffer overflow
 *
 *	Revision 2.3.1.8  2000/01/20 07:21:45  pasty
 *	Add deny=nnn facility
 *	(change auth_ttl => action_arg, auth_opt => action_opt)
 *
 *	Revision 2.3.1.7  2000/01/19 22:26:19  pasty
 *	Error message or regcmp error.
 *	No more use ap_reg* because compiled RE are persistent for the process life
 *	(so no need to free, so no need to use pool...).
 *
 *	Revision 2.3.1.6  2000/01/18 06:31:04  pasty
 *	Use unescape (method + escape (uri) + "?" + args + "|" + body);
 *	Using unparsed_uri was in fact a bad idea...
 *
 *	Revision 2.3.1.5  2000/01/09 08:21:09  pasty
 *	Add |log facility
 *
 *	Revision 2.3.1.4  1999/12/15 09:31:19  pasty
 *	never dbm_open with O_APPEND; use O_WRONLY.
 *
 *	Revision 2.3.1.3  1999/11/19 10:05:07  pasty
 *	File locking and file locking and file locking and ...
 *
 *	Revision 2.3.1.2  1999/11/13 06:14:18  pasty
 *	improvements in file locking
 *
 *	Revision 2.3.1.1  1999/11/08 08:38:18  pasty
 *	eaccess_log lines was truncated
 *
 *	Revision 2.3  1999/10/28 13:14:30  pasty
 *	2.2.1.12 -> 2.3
 *
 *	Revision 2.2.1.12  1999/10/28 04:58:01  pasty
 *	unused code (Ghislaine Labouret)
 *
 *	Revision 2.2.1.11  1999/10/28 04:48:30  pasty
 *	file locking on auth cache
 *	checks for returned values for dbm_... (Ghislaine Labouret)
 *
 *	Revision 2.2.1.10  1999/10/27 13:50:37  pasty
 *	return (int) instead of return (time_t)
 *
 *	Revision 2.2.1.9  1999/10/27 12:12:08  pasty
 *	unescape (URI?QUERY_STRING|BODY) to use simple RE
 *
 *	Revision 2.2.1.8  1999/10/26 12:57:17  pasty
 *	difftime for TTL (Ghislaine Labouret)
 *
 *	Revision 2.2.1.7  1999/10/26 12:37:30  pasty
 *	better checks to get basic auth. (Ghislaine Labouret)
 *
 *	Revision 2.2.1.6  1999/10/26 11:55:26  pasty
 *	no more warning with -Wall
 *
 *	Revision 2.2.1.5  1999/10/26 09:13:22  pasty
 *	"EAccessRule warning": loop must continue with "continue" (not break)
 *
 *	Revision 2.2.1.4  1999/10/26 08:51:03  pasty
 *	check TTL value for auth/... (Ghislaine Labouret)
 *
 *	Revision 2.2.1.3  1999/10/26 04:20:57  pasty
 *	"EAccessRule warning" correction: loop must continue (Ghislaine Labouret)
 *
 *	Revision 2.2.1.2  1999/08/30 12:34:19  pasty
 *	"EAccessEnable off" correction (Ghislaine Labouret)
 *
 *	Revision 2.2.1.1  1999/08/30 12:14:24  pasty
 *	ifndef then define TRUE and FALSE (Ghislaine Labouret)
 *
 *	Revision 2.2  1999/06/26 04:31:08  pasty
 *	auth/securid OK
 *
 *	Revision 2.1  1999/06/24 06:20:37  pasty
 *	2.0.1.4 -> 2.1
 *
 *	Revision 2.0.1.4  1999/06/17 09:42:36  pasty
 *	Fixed: no cache creation if EAccessEnable = off
 *
 *	Revision 2.0.1.3  1999/06/17 09:37:08  pasty
 *	Fixed glibc 2.1 ndbm.h inclusion problems.
 *
 *	Revision 2.0.1.2  1999/06/15 19:40:53  pasty
 *	+ clean
 *
 *	Revision 2.0.1.1  1999/06/09 21:46:24  pasty
 *	doc
 *
 *	Revision 2.0.1.0  1999/06/09 14:14:42  pasty
 *	auth/basic done.
 *
 *	Revision 2.0  1999/06/07 19:31:22  pasty
 *	add permit/deny/warning on EAccessRule, auth not implemented
 *
 *	Revision 1.6  1999/06/05 06:30:08  pasty
 *	Don't create logfile if loglevel == 0.
 *	Correction: all methods may have arguments and body.
 *
 *	Revision 1.5  1999/06/02 05:19:07  pasty
 *	Add logging
 *
 *	Revision 1.4  1999/05/30 14:05:17  pasty
 *	add blah blah
 *
 *	Revision 1.3  1999/05/29 07:45:44  pasty
 *	POST (and PUT) method done
 *
 *	Revision 1.2  1999/05/29 05:15:55  pasty
 *	GET method OK.
 *	todo: POST method...
 *
 *	Revision 1.1  1999/05/28 04:25:43  pasty
 *	Initial revision
 *
 *=============================================================================
 */
static char const rcsid [] = "$Id: mod_eaccess.c,v 2.3.2.9 2000/02/15 07:36:00 pasty Exp $";
#if defined (EACCESS_BODY_HACK)
static char const rcsdi [] = "$Id: "
                             "mod_eaccess.c: EAccess/big-body $";
#endif

#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "http_log.h"
#include "util_script.h"
#include "http_main.h"
#include "http_request.h"
#include "http_core.h"
#include "util_md5.h"
#if defined (__GLIBC__)						&&	\
    defined (__GLIBC_MINOR__)					&&	\
    __GLIBC__ >= 2						&&	\
    __GLIBC_MINOR__ >= 1
#include <db1/ndbm.h>
#else
#include <ndbm.h>
#endif

#ifndef FALSE
#define FALSE	0
#define TRUE	!FALSE
#endif

/*
 * File locking (from mod_rewrite.h)
 */
#if defined(USE_FCNTL_SERIALIZED_ACCEPT)
#define USE_FCNTL 1
#include <fcntl.h>
#endif
#if defined(USE_FLOCK_SERIALIZED_ACCEPT)
#define USE_FLOCK 1
#include <sys/file.h>
#endif
#if !defined(USE_FCNTL) && !defined(USE_FLOCK)
#define USE_FLOCK 1
#if !defined(MPE) && !defined(WIN32) && !defined(__TANDEM)
#include <sys/file.h>
#endif
#ifndef LOCK_UN
#undef USE_FLOCK
#define USE_FCNTL 1
#include <fcntl.h>
#endif
#endif
#ifdef AIX
#undef USE_FLOCK
#define USE_FCNTL 1
#include <fcntl.h>
#endif
#ifdef WIN32
#undef USE_FCNTL
#define USE_LOCKING
#include <sys/locking.h>
#endif

#if !defined(USE_FCNTL) && !defined(USE_FLOCK) && !defined(USE_LOCKING)
#define USE_FCNTL 1
#endif

/*
 * Définitions pour la compilation du module: Configure utilise le texte situé
 * entre -START et -END.
 *
 * MODULE-DEFINITION-START
 * Name: eaccess_module
 * ConfigStart
    . ./helpers/find-dbm-lib
 * ConfigEnd
 * MODULE-DEFINITION-END
 */

/*
 * Pré-déclaration du module pour les cross-references
 */
module eaccess_module;

/*
 * La config (globale) du module
 */
#define EACCESS_DISABLED	1<<0
#define EACCESS_ENABLED		1<<1

/*
 * Les actions possibles dans une règle
 */
typedef enum
{
  EACCESS_DENY,				/* deny				*/
  EACCESS_PERMIT,			/* permit			*/
  EACCESS_WARNING,			/* warning			*/
  EACCESS_AUTH_BASIC,			/* auth/basic[=n]		*/
  EACCESS_AUTH_SECURID			/* auth/securid[=n]		*/
} eaccess_action;

/*
 * Les actions en chaine de caractères
 */
#define EACCESS_DENY_STR		"deny"
#define EACCESS_DENY_LEN		4	/* len ("deny")		*/
#define EACCESS_PERMIT_STR		"permit"
#define EACCESS_WARNING_STR		"warning"
#define EACCESS_AUTH_BASIC_STR		"auth/basic"
#define EACCESS_AUTH_BASIC_LEN		10	/* len ("auth/basic")	*/
#define EACCESS_AUTH_SECURID_STR	"auth/securid"
#define EACCESS_AUTH_SECURID_LEN	12	/* len ("auth/securid")	*/

/*
 * Une règle
 */
typedef struct eaccess_rulentry
{
  char			*pattern;	/* l'er en clair (sans le '!'	*/
  					/* éventuel			*/
  regex_t		regexp;		/* l'ER compilée		*/
  int			revert;		/* revert-match ('!')		*/
  eaccess_action	action;		/* l'action à entreprendre si	*/
  					/* l'ER match l'URL		*/
  int			action_arg;	/* l'argument de l'action, cad:	*/
  					/* - le TTL de l'auth (pour les	*/
					/*   actions auth/...=)		*/
  					/* - le code de retour (pour	*/
					/*   les actions deny=)		*/
  char			*action_opt;	/* les options des règles	*/
  					/* auth/...			*/
} eaccess_rulentry;

/*
 * La config du module (la config dynamique n'est pas implémentée...)
 */
typedef struct eaccess_cfg
{
  int			state;		/* eaccess activé ou non	*/
  array_header		*rules;		/* tableau d'eaccess_rulentry	*/
  char			*logfile;	/* le fichier de log		*/
  int			logfd;		/* le file descriptor du log	*/
  int			loglevel;	/* la verbosité des logs	*/
  char			*cachefile;	/* le fichier de cache des auth	*/
  char			*cachefname;	/* le nom complet du cache auth	*/
  int			optim;		/* la niveau d'optimisation	*/
} eaccess_cfg;


/*
 * mode_t pour les open()
 */
#ifdef WIN32
# define EACCESS_OPEN_MODE	(_S_IREAD | _S_IWRITE)
#else
# define EACCESS_OPEN_MODE	(S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#endif

/*
 * Les niveaux d'optimisation
 */
#define	EACCESS_KEEP_STR_RE	optim > -1
#define	EACCESS_KEEP_BIN_RE	optim >  0

/*
 ************************************************************************
 * File locking and log time (cf mod_rewrite.c)
 ************************************************************************
 */

#ifdef USE_FCNTL
static struct flock	lock_it;
static struct flock	unlock_it;
#endif

static void fd_lock (request_rec *r, int fd)
{
  int	rc;
#ifdef USE_LOCKING
  off_t	offset;
#endif

#if defined(USE_FCNTL)
  lock_it.l_whence = SEEK_SET;		/* from current point		*/
  lock_it.l_start  = 0;			/* -"-				*/
  lock_it.l_len    = 0;			/* until end of file		*/
  lock_it.l_type   = F_WRLCK;		/* set exclusive/write lock	*/
  lock_it.l_pid    = 0;			/* pid not actually interesting	*/

  while ((rc = fcntl (fd, F_SETLKW, &lock_it) < 0) && errno == EINTR)
  {
    continue;
  }
#elif defined(USE_FLOCK)
  while ((rc = flock (fd, LOCK_EX)) < 0 && errno == EINTR)
  {
    continue;
  }
#elif defined(USE_LOCKING)
  /* Store offset, lock the first byte always, and restore offset */
  offset = lseek (fd, 0, SEEK_CUR);
  lseek (fd, 0, SEEK_SET);
  rc = _locking (fd, _LK_LOCK, 1);
  lseek (fd, offset, SEEK_SET);
#else
#error -Dxxx error: use USE_FCNTL, USE_FLOC or USE_LOCKING
#endif

  if (rc < 0)
  {
    ap_log_rerror (APLOG_MARK, APLOG_ERR, r,
		   "EAccess: failed to lock file descriptor");
  }
  return;
}

static void fd_unlock (request_rec *r, int fd)
{
  int	rc;
#ifdef USE_LOCKING
  off_t	offset;
#endif

#if defined(USE_FCNTL)
  unlock_it.l_whence = SEEK_SET;	/* from current point		*/
  unlock_it.l_start  = 0;		/* -"-				*/
  unlock_it.l_len    = 0;		/* until end of file		*/
  unlock_it.l_type   = F_UNLCK;		/* unlock			*/
  unlock_it.l_pid    = 0;		/* pid not actually interesting */

  rc = fcntl (fd, F_SETLKW, &unlock_it);
#elif defined(USE_FLOCK)
  rc = flock (fd, LOCK_UN);
#elif defined(USE_LOCKING)
  /* Store offset, unlock the first byte always, and restore offset */
  offset = lseek (fd, 0, SEEK_CUR);
  lseek (fd, 0, SEEK_SET);
  rc = _locking (fd, _LK_UNLOCK, 1);
  lseek (fd, offset, SEEK_SET);
#else
#error -Dxxx error: use USE_FCNTL, USE_FLOC or USE_LOCKING
#endif

  if (rc < 0)
  {
    ap_log_rerror (APLOG_MARK, APLOG_ERR, r,
		   "EAccess: failed to unlock file descriptor");
  }
}

static char *current_logtime(request_rec *r)
{
    int timz;
    struct tm *t;
    char tstr[80];
    char sign;

    t = ap_get_gmtoff(&timz);
    sign = (timz < 0 ? '-' : '+');
    if (timz < 0) {
        timz = -timz;
    }

    strftime(tstr, 80, "[%d/%b/%Y:%H:%M:%S ", t);
    ap_snprintf(tstr + strlen(tstr), 80-strlen(tstr), "%c%.2d%.2d]",
                sign, timz/60, timz%60);
    return ap_pstrdup(r->pool, tstr);
}

/*
 ************************************************************************
 * Convertir un entier (en base 10) et met à jour endptr:
 * - NULL si pas d'erreur
 * - != NULL si erreur
 ************************************************************************
 */
static int eaccess_atoi (const char *nptr, char **endptr)
{
  long int	result;

  /*
   * strtol n'aime pas traduire une chaine vide (core dump...)
   * De plus, si la chaine à traduire ne contient que <fin de chaine> (\0),
   * on ne va pas plus loin.
   */
  if ((nptr == NULL) || (*nptr == '\0'))
  {
    *endptr = "NULL";
    return -1;
  }

  /*
   * On traduit, ce qui met à jour endptr à la valeur du dernier caractère
   * analysé OK:
   * - s'il n'y a pas d'erreur, c'est le '\0' de la <fin de chaine> de nptr
   *   (puisqu'on a déjà vérifié que *nptr != '\0');
   * - s'il y a une erreur, c'est le pointeur vers le caractère posant problème;
   *
   * De plus, s'il y a dépassement de capacité, errno == ERANGE.
   */
  result = strtol (nptr, endptr, 10);

  /*
   * Le seul cas OK est donc (**endptr == '\0')
   */
  if (errno == ERANGE)
  {
    *endptr = "ERANGE";
    return -1;
  }
  if (**endptr == '\0')
  {
    *endptr = NULL;
    return (int) result;
  }
  return -1;

}

/*
 ************************************************************************
 * Convertion hexa -> char
 *
 * Code extrait de util.c
 ************************************************************************
 */
static char x2c(const char *what)
{
    register char digit;

#ifndef CHARSET_EBCDIC
    digit = ((what[0] >= 'A') ? ((what[0] & 0xdf) - 'A') + 10 : (what[0] - '0'));
    digit *= 16;
    digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A') + 10 : (what[1] - '0'));
#else /*CHARSET_EBCDIC*/
    char xstr[5];
    xstr[0]='0';
    xstr[1]='x';
    xstr[2]=what[0];
    xstr[3]=what[1];
    xstr[4]='\0';
    digit = os_toebcdic[0xFF & strtol(xstr, NULL, 16)];
#endif /*CHARSET_EBCDIC*/
    return (digit);
}

/*
 ************************************************************************
 * Convertir les %hh ; exceptions: \0, \a, \b, \n, \v, \f, \r:
 * - %00 ('\0') => "\0"
 * - %07 ('\a') => "\a"
 * - %08 ('\b') => "\b"
 * - %0A ('\n') => "\n"
 * - %0B ('\v') => "\v"
 * - %0C ('\f') => "\f"
 * - %0D ('\r') => "\r"
 * Note:
 * - %09 ('\t') n'est pas remplacer par "\t" car facilement "matchable",
 * - %5C ('\\') n'est pas remplacer par "\\" pour la même raison.
 *
 * Puis, convertir les ('\r' suivi de '\n') en "\n".
 *
 * Code extrait de util.c
 ************************************************************************
 */
void eaccess_unescape (char *string)
{
    register int x, y;

    for (x = 0, y = 0; string[y]; ++x, ++y) {
	if (string[y] != '%')
	    string[x] = string[y];
	else {
	    if (!ap_isxdigit(string[y + 1]) || !ap_isxdigit(string[y + 2])) {
		string[x] = '%';
	    }
	    else {
		string[x] = x2c(&string[y + 1]);
		y += 2;
		if (string[x] == '\0') {
		    string[x++] = '\\'; string[x] = '0';
		}
		else if (string[x] == '\a') {
		    string[x++] = '\\'; string[x] = 'a';
		}
		else if (string[x] == '\b') {
		    string[x++] = '\\'; string[x] = 'b';
		}
		else if (string[x] == '\n') {
		    string[x++] = '\\'; string[x] = 'n';
		}
		else if (string[x] == '\v') {
		    string[x++] = '\\'; string[x] = 'v';
		}
		else if (string[x] == '\f') {
		    string[x++] = '\\'; string[x] = 'f';
		}
		else if (string[x] == '\r') {
		    string[x++] = '\\'; string[x] = 'r';
		}
	    }
	}
	if (string[y] == '\r' && string[y+1] == '\n') {
	    string[x++] = '\\'; string[x] = 'n';
	    y += 1;
	}
    }
    string[x] = '\0';
}

/*
 ************************************************************************
 * Logger un message d'un niveau
 ************************************************************************
 */
static void eaccess_log (request_rec *r, int level, const char *text, ...)
{
  /*
   * La config actuelle
   */
  eaccess_cfg	*conf =
    (eaccess_cfg *) ap_get_module_config (r->server->module_config,
                                          &eaccess_module);
  /*
   * La liste du text, ... passé
   */
  va_list	ap;

  /*
   * Les "REMOTE" variables utilisées pour le log
   */
  char		*ruser;
  const char	*rhost;

  /*
   * Les différentes parties de la ligne à logger:
   * str1: "rhost rident ruser time", soit approximativement 64 caractères
   *       (arrondi à 128 ...)
   * str2: l'interprétation de texte, ...
   *       contient souvent (voir les appels à cette fonction):
   *       RE#nnn xxx access to 'GET url?args|data'
   *       'GET ...' est limité par Apache à DEFAULT_LIMIT_REQUEST_LINE
   *       caractères (cf read_request_line, http_protocol.c, cette limite
   *       inclue le 'HTTP/x.y' présent en fin de ligne);
   *       "RE#nnn xxx access to" fait toujours 24 caractères.
   */
  char		str1 [128];
  char		str2 [24 + DEFAULT_LIMIT_REQUEST_LINE];

  /*
   * On récupère la liste texte, ... passée en arguments
   */
  va_start (ap, text);

  /*
   * - si on n'a pas de file descripteur,
   * - ou si le message à logger est d'un niveau suppérieur à celui demandé par
   *   la directive LogLevel,
   * alors ce n'est pas la peine d'aller plus loin.
   */
  if ((conf->logfd < 0) || (level > conf->loglevel))
  {
    return;
  }

  /*
   * REMOTE_USER dispo?
   */
  if (r->connection->user == NULL)
  {
    ruser = "-";
  }
  else if (strlen (r->connection->user) != 0)
  {
    ruser = r->connection->user;
  }
  else
  {
    ruser = "\"\"";
  }

  /*
   * REMOTE_HOST
   */
  rhost = ap_get_remote_host (r->connection, r->server->module_config,
                              REMOTE_NOLOOKUP);
  if (rhost == NULL)
  {
    rhost = "UNKNOWN-HOST";
  }

  /*
   * Les 2 parties de la ligne à logger
   */
  ap_snprintf (str1, sizeof (str1), "%s %s %s %s ",
	       rhost,
	       (
		 r->connection->remote_logname != NULL
		 ? r->connection->remote_logname
		 : "-"
	       ),
	       ruser,
	       current_logtime (r));
  ap_vsnprintf (str2, sizeof (str2), text, ap);

  /*
   * On écrit
   */
  fd_lock (r, conf->logfd);
  write (conf->logfd, str1, strlen (str1));
  write (conf->logfd, str2, strlen (str2));
  write (conf->logfd, "\n", strlen ("\n"));
  fd_unlock (r, conf->logfd);

  /*
   * On libère la liste et c'est fini
   */
  va_end (ap);
  return;
}

/*
 ************************************************************************
 * Stockage en cache des auth.
 ************************************************************************
 */
DBM *eaccess_auth_open (request_rec *r, char *fname, int flags)
{
  DBM		*cachefd;

  cachefd = dbm_open (fname, flags, EACCESS_OPEN_MODE);
  if (cachefd != NULL)
  {
    fd_lock (r, dbm_dirfno (cachefd));
  }
  else
  {
    ap_log_rerror (APLOG_MARK, APLOG_ERR, r,
                   "EAccess: could not open EAccessCache file '%s'", fname);
  }
  return cachefd;
}

void eaccess_auth_close (request_rec *r, DBM *fdesc)
{
  fd_unlock (r, dbm_dirfno (fdesc));
  dbm_close (fdesc);
}

time_t eaccess_auth_get (request_rec *r,
                         char *cachefname, const char *auth)
{
  datum		key;
  datum		val;
  time_t	res;
  DBM		*cachefd;
  AP_MD5_CTX	context;
  
  if ((cachefd = eaccess_auth_open (r, cachefname, O_RDONLY)) == NULL)
  {
    ap_log_rerror (APLOG_MARK, APLOG_ERR, r,
                   "EAccess: could not open EAccessCache file '%s'",
		   cachefname);
    return (time_t) 0;
  }

  ap_MD5Init (&context);
  ap_MD5Update (&context, auth, strlen (auth));

  key.dptr  = ap_md5contextTo64 (r->pool, &context);
  key.dsize = strlen (key.dptr);
  val       = dbm_fetch (cachefd, key);
  if (val.dptr != NULL)
  {
    memcpy (&res, val.dptr, sizeof (time_t));
    eaccess_log (r, 2, "DB-GET: '%s' is found: time_t = %ld", auth, res);
    eaccess_auth_close (r, cachefd);
    return res;
  }
  eaccess_log (r, 2, "DB-GET: '%s' is NOT found", auth);
  eaccess_auth_close (r, cachefd);
  return (time_t) 0;
}

void eaccess_auth_put (request_rec *r,
                       char *cachefname, const char *auth, time_t *t)
{
  datum		key;
  datum		val;
  DBM		*cachefd;
  AP_MD5_CTX	context;
  
  if ((cachefd =
         eaccess_auth_open (r, cachefname, O_WRONLY)) == NULL)
  {
    ap_log_rerror (APLOG_MARK, APLOG_ERR, r,
                   "EAccess: could not open EAccessCache file '%s'",
		   cachefname);
    return;
  }

  ap_MD5Init (&context);
  ap_MD5Update (&context, auth, strlen (auth));

  key.dptr  = ap_md5contextTo64 (r->pool, &context);
  key.dsize = strlen (key.dptr);
  val.dptr  = (void *) t;
  val.dsize = sizeof (time_t);
  if (dbm_store (cachefd, key, val, DBM_REPLACE) != 0)
  {
    ap_log_rerror (APLOG_MARK, APLOG_ERR, r,
                   "EAccess: store error for EAccessCache file (dbm err=%i)'",
		   dbm_error (cachefd));
  }
  eaccess_log (r, 2, "DB-PUT: '%s' is stored", auth);
  eaccess_auth_close (r, cachefd);
}

void eaccess_auth_del (request_rec *r,
                       char *cachefname, const char *auth)
{
  datum		key;
  DBM		*cachefd;
  AP_MD5_CTX	context;
  
  if ((cachefd = eaccess_auth_open (r, cachefname, O_WRONLY)) == NULL)
  {
    ap_log_rerror (APLOG_MARK, APLOG_ERR, r,
                   "EAccess: could not open EAccessCache file '%s'",
		   cachefname);
    return;
  }

  ap_MD5Init (&context);
  ap_MD5Update (&context, auth, strlen (auth));
  key.dptr  = ap_md5contextTo64 (r->pool, &context);
  key.dsize = strlen (key.dptr);
  if (dbm_delete (cachefd, key) != 0)
  {
    ap_log_rerror (APLOG_MARK, APLOG_ERR, r,
                   "EAccess: delete error for EAccessCache file (dbm err=%i)'",
		   dbm_error (cachefd));
  }
  eaccess_log (r, 2, "AUTH-DB: '%s' is deleted", auth);
  eaccess_auth_close (r, cachefd);
}

/*
 ************************************************************************
 * Initialisation du module (ouverture du log et de log des auth).
 ************************************************************************
 */
static void eaccess_init (server_rec *s, pool *p)
{
  /*
   * flags pour open()
   */
  int		flags_log   = (O_WRONLY | O_APPEND | O_CREAT);
  /*
   * La config actuelle
   */
  eaccess_cfg	*conf =
    (eaccess_cfg *) ap_get_module_config (s->module_config, &eaccess_module);
  /*
   * Pour réinitialiser le cache
   */
  DBM		*cachefd;

  /*
   * Si EAccessEnable n'est à on, on n'a pas grand chose à faire...
   */
  if (conf->state == EACCESS_DISABLED)
  {
    return;
  }

  /*
   * Si l'utilisateur n'a pas utilisé de directive EAccessAuthCache, on utilise
   * la valeur par défaut pour le fichier de cache
   */
  if (conf->cachefile == NULL)
  {
    conf->cachefile = "logs/eaccess_auth";
  }

  /*
   * On demande à compléter par "Server Root" le nom du fichier si celui-ci
   * ne commence pas par un "/".
   */
  conf->cachefname = ap_server_root_relative (p, conf->cachefile);

  /*
   * On réinitialise le cache (on n'utilise pas eaccess_auth_open et
   * eaccess_auth_close car on n'est pas dans une requête et donc on n'a pas
   * de (request_rec *r).
   * De plus, c'est inutile ici de locker le fichier.
   */
  if ((cachefd = dbm_open (conf->cachefname, O_WRONLY | O_CREAT | O_TRUNC,
			   EACCESS_OPEN_MODE)) == NULL)
  {
    ap_log_error (APLOG_MARK, APLOG_ERR, s,
                  "EAccess: could not create EAccessCache file '%s'",
		  conf->cachefname);
    exit (1);
  }
  dbm_close (cachefd);

  /*
   * Si EAccessLogLevel est à 0, on n'a rien à faire...
   */
  if (conf->loglevel == 0)
  {
    return;
  }

  /*
   * Si l'utilisateur n'a pas utilisé de directive EAccessLog, on utilise la
   * valeur par défaut pour le fichier de log
   */
  if (conf->logfile == NULL)
  {
    conf->logfile = "logs/eaccess_log";
  }

  /*
   * S'agt-il d'un "vrai fichier" ou d'un "pipe"?
   */
  if (*conf->logfile == '|')
  {
    /*
     * C'est un "pipe"
     */
    piped_log	*pl;

    /*
     * On l'ouvre
     */
    pl = ap_open_piped_log (p, conf->logfile + 1);
    if (pl == NULL)
    {
      ap_log_error (APLOG_MARK, APLOG_ERR, s,
		    "EAccess: could not open EAccessLog command '%s'",
		    conf->logfile + 1);
      exit (1);
    }
    conf->logfd = ap_piped_log_write_fd (pl);
    if (conf->logfd < 0)
    {
      ap_log_error (APLOG_MARK, APLOG_ERR, s,
		    "EAccess: could not write EAccessLog command '%s'",
		    conf->logfile + 1);
      exit (1);
    }
  }
  else
  {
    /*
     * C'est un "vrai" fichier
     */
    char	*logfname;

    /*
     * On demande à compléter par "Server Root" le nom du fichier si celui-ci
     * ne commence pas par un "/".
     */
    logfname = ap_server_root_relative (p, conf->logfile);

    /*
     * On ouvre...
     */
    conf->logfd = ap_popenf (p, logfname, flags_log, EACCESS_OPEN_MODE);
    if (conf->logfd < 0)
    {
      ap_log_error (APLOG_MARK, APLOG_ERR, s,
		    "EAccess: could not open EAccessLog file '%s'", logfname);
      exit (1);
    }
  }

  /*
   * Et tout va bien
   */
  return;
}

/*
 ************************************************************************
 * Récupérer l'"auth" dans une header et la supprimer
 ************************************************************************
 */
const char *eaccess_get_auth_basic (request_rec *r)
{
  /*
   * Rappelons le format d'une auth/basic dans un header HTTP de requête:
   *   Authorization: Basic toto:titi
   */
  const char	*auth  = NULL;
  char		*value = NULL;

  auth = ap_table_get (r->headers_in, "Authorization");
  if (auth)
  {
    value = strstr (auth, "Basic ");
    if (value)
    {
      auth += strlen ("Basic ");
    }
  }
  return (auth);
}

void eaccess_unset_auth_basic (request_rec *r)
{
  ap_table_unset (r->headers_in, "Authorization");
}

const char *eaccess_get_auth_securid (request_rec *r)
{
  /*
   * Rappelons le format d'un cookie dans un header HTTP de requête:
   *   Cookie: NAME1=OPAQUE_STRING1; NAME2=OPAQUE_STRING2 ...
   *
   * Pour SecurID, cela donne:
   *   Cookie: AceHandle=...; webid2=...
   */
  const char	*auth   = NULL;
  const char	*cookie = NULL;
  char		*value  = NULL;

  cookie = ap_table_get (r->headers_in, "Cookie");
  if (cookie)
  {
    value = strstr (cookie, "AceHandle=");
    if (value)
    {
      char	*end;

      value += strlen ("AceHandle=");
      auth   = ap_pstrdup (r->pool, value);
      end    = strchr (auth, ';');
      if (end)
      {
        *end = '\0';
      }
    }
  }
  return (auth);
}

void eaccess_unset_auth_securid (request_rec *r)
{
  /*
   * Un peu violent...
   */
  ap_table_unset (r->headers_in, "Cookie");
}

/*
 ************************************************************************
 * Code du vérificateur d'URL
 ************************************************************************
 */
static int eaccess_check (request_rec *r)
{
  /*
   * La config actuelle
   */
  eaccess_cfg		*conf =
    (eaccess_cfg *) ap_get_module_config (r->server->module_config,
                                          &eaccess_module);
  /*
   * == 1 si "Pattern" à matché l'URL (ou si "!Pattern" n'a pas matché l'URL)
   */
  int			matched;
  /*
   * Une à une, les ER de la liste des règles de la config
   */
  eaccess_rulentry	*entries = (eaccess_rulentry *) conf->rules->elts;
  /*
   * Pour parcourir les ER
   */
  int			i;
  /*
   * l'"URL" à vérifier et ses data éventuelles (ou son body pour les POST/PUT).
   */
  char			url [HUGE_STRING_LEN];
  /*
   * 2 paramètres inutilisés pour regexec()
   */
  size_t		dummy_nmatch;
  regmatch_t		dummy_pmatch [1];

  /*
   * Si EAccessEnable n'est pas ON, on s'arrête là...
   */
  if (conf->state == EACCESS_DISABLED)
  {
    return OK;
  }

  /*
   * On "calcule" "<METHOD> <URI>"
   * Note:
   * - r->uri ne contient que l'uri (décodée et optimisée pour les ..), il
   *   faudrait y ajouter éventuellement r->args (les fragments (#xxx dans les
   *   URL) ne servent qu'au butineur, et apache ne s'en sert jamais: par
   *   mesure de précaution on peut les supprimer).
   * - r->unparsed_uri contient le 2ème mot de la requête, c'est à dire l'URI
   *   complète (avec ?args#fragment), non décodée, non optimisée (/xx/../yy).
   *
   * On opte donc pour:
   *   r->parsed_uri.fragment := NULL;
   *   URL := r->method + r->uri + decode ("?" + r->args [ + "|" + datas ]);
   */

  /*
   * Suppression éventuelle des fragments: s'il y en a, ceux-ci ont été
   * alloués (cf util_uri.c, le seul endroit où Apache utilise les fragments)
   * dans le pool de la requête par ap_pstrndup.
   */
  if (r->parsed_uri.fragment)
  {
    r->parsed_uri.fragment [0] = '\0';
  }

  /*
   * On calcule method + uri. Comme plus tard on décodera "URL", on va ici
   * encoder uri, pour avoir uri == décode (encode (uri)).
   * Cette méthode a l'air d'être utilisée dans Apache, alors pourquoi pas?
   * De plus, elle permet de n'utiliser qu'*1* chaîne str [HUGE_STRING_LEN].
   */
  {
    int		to_write,
    		written;
    char	*euri;

    euri     = ap_escape_uri (r->pool, r->uri);
    to_write = strlen (r->method) + 1 + strlen (euri);
    written  = ap_snprintf (url, sizeof (url), "%s %s", r->method, euri);
    if (written != to_write)
    {
      ap_log_rerror (APLOG_MARK, APLOG_NOERRNO | APLOG_ERR, r,
		   "EAccess: truncated URL (uri): wanted %d, got %d",
		   to_write, written);
    }
  }

  /*
   * Ajout des éventuels arguments (QUERY_STRING)
   */
  if (r->args)
  {
    int		to_write,
    		written;

    to_write = strlen (url) + 1 + strlen (r->args);
    written  = ap_snprintf (url, sizeof (url), "%s?%s", url, r->args);
    if (written != to_write)
    {
      ap_log_rerror (APLOG_MARK, APLOG_NOERRNO | APLOG_ERR, r,
		   "EAccess: truncated URL (args): wanted %d, got %d",
		   to_write, written);
    }
  }

  /*
   * Si un body est présent, on en ajoute le contenu "|<DATA>"
   *
   * Pour cela, 2 possibilités:
   *
   * 1/ on appelle ap_blookc() pour lire (sans le consommer) le prochain
   * caractère du body.
   * ap_blookc() a pour effet de remplir le buffer inptr si ce n'est pas déjà
   * fait mais on doit vérifier qu'il y a quelque chose à lire sinon ap_blookc()
   * attend que le butineur fournisse des données...
   * ap_blookc retourne 1 si tout va bien.
   *
   * Le problème de cette méthode est que le buffer n'est rempli que des données
   * provenant du 1er "read" sur la socket. On peut généralement contrôler la
   * taille de ce buffer (setsockopt(SO_RCVBUF); sinon la taille par défaut est
   * contrôler par une variable système: net.core.rmem_default sous Linux,
   * tcp_recv_hiwat sous Solaris).
   * Cependant, certain système "optimise" les read sur les sockets: dès que des
   * données sont disponibles, le read rend la met pour les mettre à
   * disposition. Ce n'est pas trop le cas de Linux, mais par contre Solaris
   * "optimise" parfois "trop". On peut donc n'avoir dans ce buffer inptr que
   * très (trop) peu de caractères.
   *
   * 2/ on "consomme" le body en appelant ap_*_client_block().
   * Il faut d'abord indiquer de quelle façon on doit lire les datas par
   * ap_setup_client_block() (généralement, on utilise la méthode
   * REQUEST_CHUNKED_ERROR qui signifie que si le butineur n'a pas fourni de
   * "Content-length", on retourne une erreur).
   * Puis on vérifie qu'on ne va pas risquer de rester bloqué sur la lecture en
   * appelant ap_should_client_block().
   * On peut alors boucler sur ap_get_client_block() pour lire *toutes* les
   * datas.
   *
   * Le problème avec cette méthode est qu'on ne peut consommer le body qu'une
   * fois. Donc si ce module le fait, les autres n'en disposeront plus: cela
   * poserait un sérieux problèmes à mod_cgi et mod_proxy...
   *
   * On va donc consommer le body et le stoker dans un fichier temporaire.
   * On notera dans la table "notes" de la requête le nom de ce fichier dans la
   * variable "EAccess-body-fn" et son file-descriptor dans "EAccess-body-fd".
   * On doit également "patcher" ap_should_client_block (qui va regarder
   * d'abord si la table "notes" contient la variable "EAccess-body-fn").
   * Enfin, on "patche" également ap_get_client_block qui retourne le contenu
   * du fichier temporaire puis le supprimer si "EAccess-body-fn" existe.
   *
   * La méthode utilisée par defaut est la première. On peut définir lors de la
   * compilation EACCESS_BODY_HACK pour utiliser la 2ème méthode. On doit
   * alors appliquer au code d'Apache le patch mod_eaccess.patch.
   */

# if !defined (EACCESS_BODY_HACK)	// {
  /*
   * Méthode 1: on lit sans consommer ce qu'on peut du body.
   */
  {
    /*
     * Pour voir si un body est présent, on en lira 1 caractère
     */
    char	c;

    if ((r->connection->client->incnt > 0) &&
      (ap_blookc (&c, r->connection->client) == 1))
    {
      int	len;
      /*
       * Ok, un body est diponible, on en ajoute le début à l'"url" à vérifier,
       * sous la forme: "...|début du body".
       *
       * Pour infos, url fait HUGE_STRING_LEN caractères de long (actuellement
       * 8192 dans httpd.h) et inptr DEFAULT_BUFSIZE (4096 dans buff.c).
       */
      strncat (url, "|", sizeof (url));
      len = strlen (url);
      {
	int	to_write,
		written;
	
	to_write = len + 1 + r->connection->client->incnt;
	/*
	 * On n'utilise pas snprintf car le body n'est pas forcément terminé par
	 * un '\0'.
	 */
	if (to_write > sizeof (url))
	{
	  written = sizeof (url);
	  memcpy ((void*) (url + len),
		  (const void*) r->connection->client->inptr,
		  sizeof (url) - len);
	  len = sizeof (url) - 1;
	}
	else
	{
	  written = to_write;
	  memcpy ((void*) (url + len),
		  (const void*) r->connection->client->inptr,
		  r->connection->client->incnt);
	  len = len + r->connection->client->incnt;
	}
	if (written != to_write)
	{
	  ap_log_rerror (APLOG_MARK, APLOG_NOERRNO | APLOG_ERR, r,
		       "EAccess: truncated URL (body): wanted %d, got %d",
		       to_write, written);
	}
      }

      /*
       * Et on met fin à la chaine
       */
      url [len] = '\0';
    }
  }
# else	// }{
  /*
   * Méthode 2: on consomme le body en le sauvegardant dans un fichier
   * temporaire.
   */
  {
    /*
     * Code de retour des différentes fonctions ap_*.
     */
    int		rc;

    /*
     * On veut que le client fournisse un champ "Content-length". Sinon, on
     * sort en erreur HTTP 411.
     */
    if ((rc = ap_setup_client_block (r, REQUEST_CHUNKED_ERROR)))
    {
      return rc;
    }

    /*
     * On vérifie qu'on ne va pas rester bloquer sur la lecture du body
     */
    if (ap_should_client_block (r))
    {
      int	len;
      char	tmpfilename []		/* le nom du fichier temporaire	*/
      		= "/tmp/eaccess_body_XXXXXX";
      int	tmpfile			/* son file descriptor		*/
      		= mkstemp (tmpfilename);
      char	buff [1024];		/* le buffer pour les read	*/
      int	nread,			/* ce qu'on mis dans le buffer	*/
      		to_write,		/* ce qu'on devrait écrire	*/
		written;		/* ce qu'on a réellement écrit	*/
      int	truncated = 0;		/* si url a été tronquée 1x	*/
      /*
       * Ok, un body est diponible, on en ajoute le début à l'"url" à vérifier,
       * sous la forme: "...|début du body".
       */
      strncat (url, "|", sizeof (url));
      len = strlen (url);

      /*
       * On vérifie qu'on a bien ouvert le fichier temporaire.
       */
      if (tmpfile != -1)
      {
	/*
	 * Ok, on va noter le nom et le file-descripto dans la table
	 * "à-tout-faire" inter-modules.
	 * On utilise les champ "EAccess-body-fn" (pour le filename) et
	 * "EAccess-body-fd" (pour le file-descriptor) (que personne d'autre
	 * que nous ne devrait s'amuser à positionner...).
	 * Afin d'indiquer à ap_get_client_block s'il doit se comporter
	 * "normalement" ou lire le contenu du fichier temporaire, on ne
	 * positionne "EAccess-body-fn" qu'après avoir tout lu et on se sert de
	 * la présence de cette variable dans les patchs de ap_*_client_block.
	 */
	ap_table_setn (r->notes, "EAccess-body-fd", (char *) tmpfile);
        /*
	 * On peut consommer le body et le sauvegarder
	 */
	while ((nread = ap_get_client_block (r, buff, sizeof (buff))) > 0)
	{
	  /*
	   * On sauvegarde dans le fichier temporaire
	   */
	  written = write (tmpfile, buff, nread);	/* XXXX vérif... */

	  /*
	   * On écrit ce qu'on vient de lire dans URL; on doit écrire:
	   * <la longueur actuelle de url> + <le nombre de caractères qu'on
	   * vient de lire> + 1 (pour terminer url par '\0').
	   */
	  to_write = len + nread + 1;
	  /*
	   * Comme pour la 1ère méthode, on n'utilise pas snprint.
	   */
	  if (to_write > sizeof (url))
	  {
	    written = sizeof (url);
	    memcpy ((void*) (url + len),
		    (const void*) buff,
		    sizeof (url) - len);
	    len = sizeof (url) - 1;
	  }
	  else
	  {
	    written = to_write;
	    memcpy ((void*) (url + len),
		    (const void*) buff,
		    nread);
	    len = len + nread;
	  }
	  /*
	   * On met fin à la chaîne de caractères
	   */
	  url [len] = '\0';
	  /*
	   * Si on n'a pas assez écrit et que c'est la 1ère fois.
	   */
	  if (!truncated && written != to_write)
	  {
	    truncated = 1;
	    ap_log_rerror (APLOG_MARK, APLOG_NOERRNO | APLOG_ERR, r,
			 "EAccess: truncated URL (body): wanted %d, got %d",
			 to_write, written);
	  }
	}

	/*
	 * On fait croire aux autres qu'on n'a rien lu (et en particulier à
	 * ap_discard_request_body, qui sera appelé par ap_die en cas
	 * d'interdiction par EAccess; donc ce n'est pas à nous de détruire le
	 * body...).
	 */
	r->read_length = 0;

	/*
	 * On peut "rembobiner" le fichier temporaire
	 */
	lseek (tmpfile, 0, SEEK_SET);

	/*
	 * Et noter le nom du fichier temporaire pour qu'ap_get_client_block
	 * modifie son comportement.
	 * tmpfilename étant une variable locale, on la duplique.
	 */
	ap_table_setn (r->notes, "EAccess-body-fn",
	               ap_pstrdup (r->pool, tmpfilename));
      }
      else
      {
        /*
	 * Impossible d'ouvrir le fichier temporaire, tant pis pour le body...
	 */
	ap_log_rerror (APLOG_MARK, APLOG_ERR, r,
		       "EAccess: cannot open tmp file (%s)'", tmpfilename);
      }
    }
  }
# endif	// }

  /*
   * Enfin, pour ne pas compliquer les expressions régulières, on convertit les
   * caractères "%encodés", ainsi que les '+'.
   */
  eaccess_unescape (url);

  /*
   * On parcourt les ER
   */
  for (i = 0; i < conf->rules->nelts; i++)
  {
    /*
     * Si on n'est pas en niveau d'optimisation 1, on doit recompiler l'ER.
     */
    if (!conf->EACCESS_KEEP_BIN_RE)
    {
      regcomp (&(entries[i].regexp), entries[i].pattern,
	       REG_EXTENDED | REG_NOSUB);
    }

    /*
     * On regarde si "<METHOD> <URI>" vérifie l'ER
     */
    matched = 0;
    if (regexec (&(entries[i].regexp), url, dummy_nmatch, dummy_pmatch, 0) == 0)
    {
      matched = 1;
    }

    /*
     * Si on n'est pas en niveau d'optimisation 1, on ne conserve pas la compil
     */
    if (!conf->EACCESS_KEEP_BIN_RE)
    {
      regfree (&(entries[i].regexp));
    }

    /*
     * Si on est en revert-match, on inverse le résultat
     */
    if (entries[i].revert)
    {
      /*
       * On est en "revert": on inverse le résultat,
       * on log que !RE a / n'a pas matchée.
       */
      matched = (matched) ? 0 : 1;
      eaccess_log (r, 2, "RE '!%s' %s URL '%s'", entries[i].pattern,
		   (matched) ? "matches" : "does not match", url);
    }
    else
    {
      /*
       * On log que RE a / n'a pas matchée.
       */
      eaccess_log (r, 2, "RE '%s' %s URL '%s'", entries[i].pattern,
		   (matched) ? "matches" : "does not match", url);
    }

    /*
     * Si l'ER matche, on dispatche suivant l'action à entreprendre
     */
    if (matched)
    {
      switch (entries[i].action)
      {
        case EACCESS_PERMIT:
	{
	  eaccess_log (r, 1, "RE #%03d grants access to '%s'", i + 1, url);
	  return OK;
	}
        case EACCESS_DENY:
	{
	  eaccess_log (r, 1, "RE #%03d denies access to '%s'", i + 1, url);
	  return entries[i].action_arg;
	}
        case EACCESS_WARNING:
	{
	  eaccess_log (r, 1, "RE #%03d *** WARNING! *** '%s'", i + 1, url);
	  /*
	   * Pas de return, car on doit continuer à analyser les autres règles.
	   * (mais un break pour arrêter les case...)
	   */
	  continue;
	}
        case EACCESS_AUTH_BASIC:
        case EACCESS_AUTH_SECURID:
	{
	  /*
	   * L'éventuelle authentification dans le header, et l'éventuelle
	   * première fois qu'elle a été utilisée (en char* car dans une table
	   * on ne peut mettre que des char*; on convertira en time_t...)
	   */
	  const char	*auth;			/* user:pass en base64	*/
	  time_t	first_time;
	  const char	*auth_type;		/* le type de l'auth	*/
	  					/* pour les logs	*/
	  const char	*auth_opt;		/* le type de l'option	*/
	  					/* pour les logs	*/

	  if (entries[i].action == EACCESS_AUTH_BASIC)
	  {
	    auth_type = "basic";
	    auth_opt  = "realm";
	  }
	  else if (entries[i].action == EACCESS_AUTH_SECURID)
	  {
	    auth_type = "securid";
	    auth_opt  = "redirect";
	  }
	  else
	  {
	    auth_type = "<INTERNAL ERROR>";
	    auth_opt  = "<INTERNAL ERROR>";
	  }

	  eaccess_log (r, 2, "RE #%03d auth/%s: %s='%s', TTL=%d",
	               i + 1, auth_type, auth_opt,
		       entries[i].action_opt, entries[i].action_arg);

	  /*
	   * Y a-t-il dans le Header une authentification?
	   */
	  if (((entries[i].action == EACCESS_AUTH_BASIC) &&
	       (auth = eaccess_get_auth_basic (r))) ||
	      ((entries[i].action == EACCESS_AUTH_SECURID) &&
	       (auth = eaccess_get_auth_securid (r))))
	  {
	    /*
	     * Il y a une authentification dans le Header HTTP.
	     */
	    eaccess_log (r, 2, "RE #%03d auth/%s: Authorization='%s'",
			 i + 1, auth_type, auth);
	    /*
	     * Cette authentification est-elle déjà présente dans la table des
	     * auth de cette ER?
	     */
	    first_time = eaccess_auth_get (r, conf->cachefname, auth);
	    if (first_time)
	    {
	      /*
	       * Cette auth est déjà connue, on va regarder si elle a expiré.
	       * Donc avant tout, on regarde si TTL == 0 (=> pas d'expiration)
	       */
	      if (entries[i].action_arg)
	      {
		/*
		 * Ok, on s'intéresse à l'expiration de cette authentification:
		 * le time_t de maintenant et le delta entre les 2 temps
		 */
		time_t	this_time;
		int	diff_time;

		/*
		 * La temps maintenant et la première fois => delta
		 */
		time (&this_time);
		diff_time = (int) difftime (this_time, first_time);
		eaccess_log (r, 2,
			     "RE #%03d auth/%s: Authorization already used: "
			     "first time=%ld => delta = %d seconds",
			     i + 1, auth_type, first_time, diff_time);

		/*
		 * Expirée ou non?
		 */
		if (diff_time > entries[i].action_arg)
		{
		  /*
		   * L'authentification présente dans le header est donc
		   * expirée. Si la règle avait précisé une option (cad un
		   * realm pour auth/basic, une redirection pour auth/securid),
		   * on dit 401 (auth/basic) ou 302 (auth/securid), sinon, on
		   * supprime l'auth dans le header et on continue la boucle...
		   */
		  if (entries[i].action_opt)
		  {
		    if (entries[i].action == EACCESS_AUTH_BASIC)
		    {
		      /*
		       * auth/basic:
		       *
		       * Ok, la règle précise un realm: on supprime cette auth
		       * de la table et on retourne 401 au butineur...
		       */
		      eaccess_log (r, 2,
				   "RE #%03d auth/basic: Realm set to '%s' => "
				   "err 401 returned",
				   i + 1, entries[i].action_opt);
		      eaccess_auth_del (r, conf->cachefname, auth);
		      ap_table_setn (r->err_headers_out, "WWW-Authenticate",
				     ap_pstrcat (r->pool, "Basic realm=\"",
				     ap_escape_quotes (r->pool,
						       entries[i].action_opt),
				     "\"", NULL));
		      eaccess_log (r, 1, "RE #%03d AUTH too old for '%s'",
				   i + 1, url);
		      r->proxyreq = 0;
		      return AUTH_REQUIRED;
		    }
		    else if (entries[i].action == EACCESS_AUTH_SECURID)
		    {
		      /*
		       * auth/securid:
		       *
		       * Ok, la règle précise un realm: on supprime cette auth
		       * de la table, on écrit "Location: <la redirection>" dans
		       * le header et on retourne 302 au butineur...
		       */
		      eaccess_log (r, 2,
				   "RE #%03d auth/securid: "
				   "Option set to '%s' => "
				   "redirected",
				   i + 1, entries[i].action_opt);
		      eaccess_auth_del (r, conf->cachefname, auth);
		      ap_table_setn (r->headers_out, "Location",
				     ap_pstrdup (r->pool,
				                 entries[i].action_opt));
		      eaccess_log (r, 1, "RE #%03d AUTH too old for '%s'",
				   i + 1, url);
		      /*r->proxyreq = 0;*/
		      return REDIRECT;
		    }
		    else
		    {
		      /*
		       * ... erreur dans le code ...
		       */
		      ap_log_rerror (APLOG_MARK, APLOG_NOERRNO | APLOG_ERR, r,
				   "EAccess: internal error: auth/???");
		      return SERVER_ERROR;
		    }
		  }
		  else
		  {
		    /*
		     * La règle ne précise pas d'option: on supprime cette auth
		     * du Header HTTP et de la table et on continue...
		     */
		    eaccess_log (r, 2,
				 "RE #%03d auth/%s: %s unset => auth cancelled",
				 i + 1, auth_type, auth_opt);
		    eaccess_auth_del (r, conf->cachefname, auth);
		    if (entries[i].action == EACCESS_AUTH_BASIC)
		    {
		      eaccess_unset_auth_basic (r);
		    }
		    else if (entries[i].action == EACCESS_AUTH_SECURID)
		    {
		      eaccess_unset_auth_securid (r);
		    }
		    else
		    {
		      ap_log_rerror (APLOG_MARK, APLOG_NOERRNO | APLOG_ERR, r,
				   "EAccess: internal error: auth/???");
		      return SERVER_ERROR;
		    }
		    eaccess_log (r, 1, "RE #%03d AUTH removed for '%s'",
		                 i + 1, url);
		  }
		}
		else
		{
		  /*
		   * L'authentification présente dans le header n'est pas
		   * expirée.
		   */
		  eaccess_log (r, 1, "RE #%03d AUTH not expired '%s'",
			       i + 1, url);
		}
	      }
	      else
	      {
	        /*
		 * TTL == 0 => pas d'expiration
		 */
		eaccess_log (r, 1, "RE #%03d AUTH unTTLed for '%s'",
			     i + 1, url);
	      }
	    }
	    else
	    {
	      /*
	       * L'auth présente dans le Header HTTP n'est pas déjà connue.
	       * On mémorise cette première fois et on continue la boucle
	       * des vérifications d'accès
	       */
	      time (&first_time);
	      eaccess_log (r, 2, "RE #%03d auth/%s: first time this "
			   "Authorization is used (%s/%ld)", i + 1, auth_type,
			   auth, first_time);
	      eaccess_auth_put (r, conf->cachefname, auth, &first_time);
	      eaccess_log (r, 1, "RE #%03d AUTH starting on '%s'",
			   i + 1, url);
	    }
	  }
	  else
	  {
	    eaccess_log (r, 2, "RE #%03d auth/%s: NO Authorization",
	                 i + 1, auth_type);
	    /*
	     * Il n'y a pas d'authentification dans le Header HTTP.
	     * Si l'option dans la règle avait précisé un realm pour auth/basic
	     * (ou une redirection pour auth/securid), on dit 401 (ou 302 pour
	     * securid), sinon on continue la boucle...
	     */
	    if (entries[i].action_opt)
	    {
	      if (entries[i].action == EACCESS_AUTH_BASIC)
	      {
		/*
		 * auth/basic:
		 *
		 * Ok, la règle précise un realm: on retourne 401 au butineur...
		 */
		eaccess_log (r, 2,
			     "RE #%03d auth/basic: Realm set to '%s' => "
			     "err 401 returned",
			     i + 1, entries[i].action_opt);
		ap_table_setn (r->err_headers_out, "WWW-Authenticate",
			       ap_pstrcat (r->pool, "Basic realm=\"",
			       ap_escape_quotes (r->pool,
						 entries[i].action_opt),
			       "\"", NULL));
		eaccess_log (r, 1, "RE #%03d AUTH err 401 for '%s'",
			     i + 1, url);
		r->proxyreq = 0;
		return AUTH_REQUIRED;
	      }
	      else if (entries[i].action == EACCESS_AUTH_SECURID)
	      {
		/*
		 * auth/securid:
		 *
		 * Ok, la règle précise une redirection: on retourne 302...
		 */
		eaccess_log (r, 2,
			     "RE #%03d auth/securid: "
			     "Redirect set to '%s' => "
			     "err 302 returned",
			     i + 1, entries[i].action_opt);
		ap_table_setn (r->headers_out, "Location",
			       ap_pstrdup (r->pool, entries[i].action_opt));
		eaccess_log (r, 1, "RE #%03d AUTH err 302 for '%s'",
			     i + 1, url);
		/*r->proxyreq = 0;*/
		return REDIRECT;
	      }
	      else
	      {
		/*
		 * ... erreur dans le code ...
		 */
		ap_log_rerror (APLOG_MARK, APLOG_NOERRNO | APLOG_ERR, r,
			     "EAccess: internal error: auth/???");
		return SERVER_ERROR;
	      }
	    }
	    else
	    {
	      /*
	       * La règle ne précise pas de realm et pas d'authentification
	       * présente dans le Header HTTP: on continue...
	       */
	      eaccess_log (r, 1, "RE #%03d AUTH not needed  '%s'",
			   i + 1, url);
	    }
	  }
	}
      }
    }
  }

  /*
   * On est arrivé à la dernière ER sans succès, c'est perdu...
   */
  eaccess_log (r, 1, "default denies access to '%s'", url);
  return FORBIDDEN;
}

/*
 ************************************************************************
 * Traitement des commandes de config
 ************************************************************************
 */

/*
 * EAccessEnable
 */
static const char *eaccess_cfg_enable (cmd_parms *cmd, void *dummy, int a1)
{
  /*
   * La config actuelle
   */
  eaccess_cfg *conf =
    (eaccess_cfg *) ap_get_module_config (cmd->server->module_config,
                                          &eaccess_module);
  /*
   * qu'on actualise
   */
  conf->state = (a1 ? EACCESS_ENABLED : EACCESS_DISABLED);

  return NULL;
}

/*
 * EAccessRule
 */
static const char *eaccess_cfg_rule (cmd_parms *cmd, void *dummy, char *a1,
                                     char *a2, char *a3)
{
  /*
   * La config actuelle
   */
  eaccess_cfg *conf =
    (eaccess_cfg *) ap_get_module_config (cmd->server->module_config,
                                          &eaccess_module);

  /*
   * La RE à compilée (a2 ou a2+1 en cas d'utilisation du !)
   */
  char			*regex;

  /*
   * Le code d'erreur lors de la compilation de la RE
   */
  int			reret;

  /*
   * La nouvelle RE compilée
   */
  eaccess_rulentry	*new;

  /*
   * On ajoute une entrée à la liste de rules
   */
  new = ap_push_array (conf->rules);

  /*
   * Dispatch de l'action
   */
  if      (strcasecmp (a1, EACCESS_PERMIT_STR) == 0)
  {
    new->action = EACCESS_PERMIT;
  }
  else if (strncasecmp (a1, EACCESS_DENY_STR, EACCESS_DENY_LEN) == 0)
  {
    new->action = EACCESS_DENY;
    /*
     * Le code de retour est FORBIDDEN par défaut
     */
    new->action_arg = FORBIDDEN;
    /*
     * Si a1 a 1 longueur > à deny, il y a donc 1 '=nnn" derrière
     */
    if (strlen (a1) > EACCESS_DENY_LEN)
    {
      if (a1 [EACCESS_DENY_LEN] == '=')
      {
	char	*atoierr;

	new->action_arg = eaccess_atoi (a1 + EACCESS_DENY_LEN + 1, &atoierr);

	/*
	 * Si l'erreur n'est pas NULL, c'est une erreur...
	 */
	if (atoierr != NULL)
	{
	  return ap_pstrcat (cmd->pool, "EAccess: invalid response code (",
	                     atoierr, ") in action '", a1, "'\n", NULL);
	}
	/*
	 * De plus, pour ne pas risquer un "internal error", on vérifie la
	 * validité du code (cf http_core.c, fonction set_error_document).
	 */
	if (new->action_arg != HTTP_INTERNAL_SERVER_ERROR &&
	    ap_index_of_response (new->action_arg) ==
	    ap_index_of_response (HTTP_INTERNAL_SERVER_ERROR))
	{
	  return ap_pstrcat (cmd->pool, "EAccess: unsupported response code in "
	                     "action '", a1, "'\n", NULL);
	}
      }
      else
      {
	return ap_pstrcat (cmd->pool, "EAccess: unknown option in action '",
	                   a1, "'\n", NULL);
      }
    }
  }
  else if (strcasecmp (a1, EACCESS_WARNING_STR) == 0)
  {
    new->action = EACCESS_WARNING;
  }
  else if (strncasecmp (a1, EACCESS_AUTH_BASIC_STR,
                        EACCESS_AUTH_BASIC_LEN) == 0)
  {
    new->action   = EACCESS_AUTH_BASIC;
    /*
     * Tant qu'on n'a pas lu l'éventuelle option "realm", on le laisse à null
     */
    new->action_opt = NULL;
    /*
     * Tant qu'on n'a pas lu l'éventuel TTL, on le laisse à 0
     */
    new->action_arg = 0;
    /*
     * Si a1 a 1 longueur > à auth/basic, il y a donc 1 '=nnn" derrière
     */
    if (strlen (a1) > EACCESS_AUTH_BASIC_LEN)
    {
      if (a1 [EACCESS_AUTH_BASIC_LEN] == '=')
      {
	char	*atoierr;

	new->action_arg = eaccess_atoi (a1 + EACCESS_AUTH_BASIC_LEN + 1,
					&atoierr);

	/*
	 * Si l'erreur n'est pas NULL, c'est une erreur...
	 */
	if (atoierr != NULL)
	{
	  return ap_pstrcat (cmd->pool, "EAccess: invalid TTL (", atoierr,
	                     ") in action '", a1, "'\n", NULL);
	}
	/*
	 * De plus, un TTL négatif ne veut pas dire grand chose...
	 */
	if (new->action_arg < 0)
	{
	  return ap_pstrcat (cmd->pool, "EAccess: negative TTL in action '",
			     a1, "'\n", NULL);
	}
      }
      else
      {
	return ap_pstrcat (cmd->pool, "EAccess: unknown option in action '",
	                   a1, "'\n", NULL);
      }
    }
    /*
     * a3, facultatif, positionne le "realm" pour auth/basic
     */
    if (a3)
    {
      new->action_opt = ap_pstrdup (cmd->pool, a3);
    }
  }
  else if (strncasecmp (a1, EACCESS_AUTH_SECURID_STR,
                        EACCESS_AUTH_SECURID_LEN) == 0)
  {
    /*
     * code <=> à EACCESS_AUTH_BASIC
     */
    new->action     = EACCESS_AUTH_SECURID;
    new->action_opt = NULL;
    new->action_arg = 0;
    if (strlen (a1) > EACCESS_AUTH_SECURID_LEN)
    {
      if (a1 [EACCESS_AUTH_SECURID_LEN] == '=')
      {
	char	*atoierr;

	new->action_arg = eaccess_atoi (a1 + EACCESS_AUTH_SECURID_LEN + 1,
					&atoierr);

	/*
	 * Si l'erreur n'est pas NULL, c'est une erreur...
	 */
	if (atoierr != NULL)
	{
	  return ap_pstrcat (cmd->pool, "EAccess: invalid TTL (", atoierr,
	                     ") in action '", a1, "'\n", NULL);
	}
	/*
	 * De plus, un TTL négatif ne veut pas dire grand chose...
	 */
	if (new->action_arg < 0)
	{
	  return ap_pstrcat (cmd->pool, "EAccess: negative TTL in action '",
			     a1, "'\n", NULL);
	}
      }
      else
      {
	return ap_pstrcat (cmd->pool, "EAccess: unknown option in action '",
	                   a1, "'\n", NULL);
      }
    }
    /*
     * a3, facultatif, positionne le "redirect" pour auth/securid
     */
    if (a3)
    {
      new->action_opt = ap_pstrdup (cmd->pool, a3);
    }

  }
  else
  {
    return ap_pstrcat (cmd->pool, "EAccess: unknown action '", a1, "'\n", NULL);
  }

  /*
   * Si l'ER commence par un !:
   *   - on note qu'on est en revert-match et on saute le !,
   * sinon:
   *   - on note qu'on n'est pas en revert-match
   */
  if (a2 [0] == '!')
  {
    new->revert = TRUE;
    regex = a2 + 1;
  }
  else
  {
    new->revert = FALSE;
    regex = a2;
  }

  /*
   * On compile l'ER avec les options:
   *	- Use POSIX Extended Regular Expression syntax
   *	- Support for substring addressing of matches is not required
   */
  reret = regcomp (&(new->regexp), regex, REG_EXTENDED | REG_NOSUB);
  if (reret != 0)
  {
    char errline [1024];

    reret = regerror (reret, &(new->regexp), errline, sizeof (errline));
    return ap_pstrcat (cmd->pool,
                       "EAccess: cannot compile regular expression '",
		       regex, "': ", errline, ".\n", NULL);
  }

  /*
   * On note l'ER en clair (sans le ! éventuel)
   */
  new->pattern = ap_pstrdup (cmd->pool, regex);

  /*
   * Si on n'est pas en optimisation niveau 1, on ne garde pas l'ER compilée
   */
  if (!conf->EACCESS_KEEP_BIN_RE)
  {
    regfree (&(new->regexp));
  }

  /*
   * Et c'est tout
   */
  return NULL;
}

/*
 * EAccessLog
 */
static const char *eaccess_cfg_log (cmd_parms *cmd, void *dummy, char *a1)
{
  /*
   * La config actuelle
   */
  eaccess_cfg *conf =
    (eaccess_cfg *) ap_get_module_config (cmd->server->module_config,
                                          &eaccess_module);

  /*
   * On note le nom du fichier
   */
  conf->logfile = a1;

  /*
   * Et c'est tout, on ouvrira le fichier dans le init du module
   */
  return NULL;
}

/*
 * EAccessLogLevel
 */
static const char *eaccess_cfg_loglevel (cmd_parms *cmd, void *dummy, char *a1)
{
  /*
   * La config actuelle
   */
  eaccess_cfg	*conf =
    (eaccess_cfg *) ap_get_module_config (cmd->server->module_config,
                                          &eaccess_module);

  /*
   * On note le niveau de verbosité
   */
  conf->loglevel = atoi (a1);

  /*
   * Et c'est tout
   */
  return NULL;
}

/*
 * EAccessCache
 */
static const char *eaccess_cfg_cache (cmd_parms *cmd, void *dummy, char *a1)
{
  /*
   * La config actuelle
   */
  eaccess_cfg *conf =
    (eaccess_cfg *) ap_get_module_config (cmd->server->module_config,
                                          &eaccess_module);

  /*
   * On note le nom du fichier
   */
  conf->cachefile = a1;

  /*
   * Et c'est tout, on complètera le nom du fichier dans le init du module
   */
  return NULL;
}

/*
 * EAccessOptim
 */
static const char *eaccess_cfg_optim (cmd_parms *cmd, void *dummy, char *a1)
{
  /*
   * La config actuelle
   */
  eaccess_cfg	*conf =
    (eaccess_cfg *) ap_get_module_config (cmd->server->module_config,
                                          &eaccess_module);

  /*
   * On note le niveau d'optimisation
   */
  conf->optim = atoi (a1);

  /*
   * Et c'est tout
   */
  return NULL;
}

/*
 * Liste
 */
command_rec eaccess_cmds [] =
{
  { "EAccessEnable",	eaccess_cfg_enable,	NULL, RSRC_CONF, FLAG,
    "On or Off to enable or disable (default) Extended Access control" },
  { "EAccessRule",	eaccess_cfg_rule,	NULL, RSRC_CONF, TAKE23,
    "a action and a [!]URL-applied regexp-pattern (! for revert-match) "
    "and optional options (see docs)" },
  { "EAccessLog",	eaccess_cfg_log,	NULL, RSRC_CONF, TAKE1,
    "the filename of the Extended Access control logfile" },
  { "EAccessLogLevel",	eaccess_cfg_loglevel,	NULL, RSRC_CONF, TAKE1,
    "the level of the Extended Access control logfile verbosity" },
  { "EAccessCache",	eaccess_cfg_cache,	NULL, RSRC_CONF, TAKE1,
    "the filename of the Extended Access control cachefile" },
  { "EAccessOptim",	eaccess_cfg_optim,	NULL, RSRC_CONF, TAKE1,
    "the optimization level of the Extended Access control" },
  {NULL}
};

/*
 ************************************************************************
 * Création de config
 ************************************************************************
 */
static void *eaccess_create_srv_config (pool *p, server_rec *s)
{
  eaccess_cfg	*conf;

  /*
   * On crée une nouvelle config
   */
  conf = (eaccess_cfg *) ap_pcalloc (p, sizeof (eaccess_cfg));

  /*
   * On initialise les valeurs par défaut
   */
  conf->state    = EACCESS_DISABLED;
  conf->rules    = ap_make_array (p, 2, sizeof (eaccess_rulentry));
  conf->logfile  = NULL;	/* l'init du module affectera 1 valeur	*/
  conf->logfd    = -1;
  conf->loglevel = 0;
  conf->optim    = 1;

  /*
   * Et on retourne cette config
   */
  return (void *) conf;
}

/*
 ************************************************************************
 * Définition du module
 ************************************************************************
 */
module MODULE_VAR_EXPORT eaccess_module =
{
  STANDARD_MODULE_STUFF,
  eaccess_init,			/* initializer */
  NULL,				/* create per-directory config structure */
  NULL,				/* merge per-directory config structures */
  eaccess_create_srv_config,	/* create per-server config structure */
  NULL,				/* merge per-server config structures */
  eaccess_cmds,			/* command table */
  NULL,				/* handlers */
  NULL,				/* translate_handler */
  NULL,				/* check_user_id */
  NULL,				/* check auth */
  eaccess_check,		/* check access */
  NULL,				/* type_checker */
  NULL,				/* pre-run fixups */
  NULL,				/* logger */
  NULL,				/* header parser */
  NULL,				/* child_init */
  NULL,				/* child_exit */
  NULL				/* post read-request */
};
