/*
 * Copyright 2000, International Business Machines Corporation and others.
 * All Rights Reserved.
 * 
 * This software has been released under the terms of the IBM Public
 * License.  For details, see the LICENSE file in the top-level source
 * directory or online at http://www.openafs.org/dl/license10.html
 */

/* Apache plugin for AFS authentication - should be archived to libapacheafs.a
 * contains calls to functions in apache_afs_client.o - and is the intermediary
 * between the module that plugs into apache's source code and the 
 * apache_afs_client. Shares global variables with the module and the client.
 */

/*
 */

#include "apache_api.h"

#define afslog(level,str) if (level <= afsDebugLevel) (afsLogError str)
#define afsassert(str) if(!(str)) { fprintf(stderr, "afs module: assertion failed:%s\t%d\n",__FILE__,__LINE__) ; return SERVER_ERROR; }

#define AFS_AUTHTYPE                        "AFS"
#define AFS_DFS_AUTHTYPE                    "AFS-DFS"
#define ERRSTRLEN                           1024


#include <sys/types.h>
#include <sys/stat.h>


/* Global vars */

u_long afsDebugLevel;
const char module_name[] = "AFS Authentication Module";

typedef struct {
  char defaultCell[64];
  u_long cacheExpiration;
} apache_afs_glob;

static apache_afs_glob *afs_str;

/* Global file descriptors for pipes */
int readPipe, writePipe;

#ifdef AIX
/* Global temp lock file descriptor */
int tempfd;

/*
 * Create a temporary file and unlink it using the file descriptor for locking
 * as a means of synchronization for providing exclusive access to the pipe
 * for communicating with the weblog process
 */
static int create_temp_file()
{
  char tmpFileName[L_tmpnam];
  int lockfd=0;

  tmpnam(tmpFileName);
  unlink(tmpFileName);

  lockfd = open(tmpFileName, O_RDWR | O_CREAT);
  if (lockfd < 0) {
    perror("afs_plugin:Error creating temp file:");
    return lockfd;
  }
  unlink(tmpFileName); 
  return lockfd;
}
#endif

/*
 * Initialization: start up the weblog process. Open the pipes and pass their
 * file descriptors to the weblog process
 */
void afs_plugin_init(int tokenExpiration, char *weblogPath,char *error_fname, char *pf,
		     char *cell,char *dir,int exp,char *loc,int shutdown)
{
  int childpid;
  int pipe1[2], pipe2[2];
  char weblogarg1[32]; 
  char weblogarg2[32]; 
  char weblogarg3[32]; 
  char weblogarg4[32];
  
  FILE *fp;            /* for pid_fname */
  char *afs_weblog_pidfile;
  char *httpd_pid_fname=(char *) malloc(strlen(pf)+1);
  if (httpd_pid_fname == NULL) {
    fprintf(stderr, "%s: malloc failed - out of memory while allocating space for httpd_pid_fname\n", module_name);
    exit(-1);
  }
  strcpy(httpd_pid_fname, pf);
  afs_weblog_pidfile=(char *) malloc(strlen(httpd_pid_fname)+5);
  if (httpd_pid_fname == NULL) {
    fprintf(stderr, "%s: malloc failed - out of memory while allocating space for afs_weblog_pidfile\n", module_name);
    exit(-1);
  }
  sprintf(afs_weblog_pidfile,"%s.afs", httpd_pid_fname);

  if(do_setpag()){
    fprintf(stderr, "%s:Failed to set pag Error:%d\n",
	    module_name,errno);
    exit(-1);
  }

  afs_str = (apache_afs_glob *) malloc(sizeof(apache_afs_glob));
  if (afs_str == NULL) {
    fprintf(stderr, "%s:malloc failed for afs_str\n",module_name);
    exit(-1);
  }
  
  if (cell) 
    strcpy(afs_str->defaultCell, cell);
  else {
    fprintf(stderr, "%s: NULL argument in cell\n",module_name);
    exit(-1);
  }
  afs_str->cacheExpiration = exp;

  afslog(5, ("Default Cell:%s\nCache Expiration:%d\nDebugLevel:%d", 
    	     afs_str->defaultCell, afs_str->cacheExpiration,afsDebugLevel));

#ifdef AIX
  /* Get a temp file fd for locking */
  tempfd=create_temp_file();
  if (tempfd < 0) {
    fprintf(stderr, "%s: Error creating temp file", module_name);
    exit(-1);
  }
#endif

  if (pipe(pipe1) < 0 || pipe(pipe2) < 0) {
    fprintf(stderr, "%s: Error creating pipes - %s",module_name, strerror(errno));
    exit(-1);
  }
  if ((childpid = fork()) < 0) {
    fprintf(stderr,"%s: Error forking - %s",module_name, strerror(errno));
    exit(-1);
  }
  else if (childpid > 0) {      /* parent */
    close(pipe1[0]);
    close(pipe2[1]);
    readPipe = pipe2[0];
    writePipe = pipe1[1];
  }
  else {                        /* child */
    close(pipe1[1]);
    close(pipe2[0]);
    fp=fopen(afs_weblog_pidfile, "w");
    if (fp == NULL) {
      perror("fopen");
      fprintf(stderr, "%s: Error opening pidfile:%s - %s\n", 
	      module_name, afs_weblog_pidfile, strerror(errno));
      close(pipe1[0]);
      close(pipe2[1]);
      exit(-1);
    }
    fprintf(fp,"%ld\n", (long)getpid());
    fclose(fp);
    free(afs_weblog_pidfile);
    sprintf(weblogarg1, "%d", pipe1[0]);
    sprintf(weblogarg2, "%d", pipe2[1]);
    sprintf(weblogarg3, "%d", afs_str->cacheExpiration);
    sprintf(weblogarg4, "%d", tokenExpiration);
    sleep(5);
    execlp(weblogPath,"weblog_starter", weblogPath, error_fname, 
	   weblogarg1,weblogarg2,weblogarg3,weblogarg4,
	   (char *)0);
    fprintf(stderr, "%s: Error executing %s - %s\n", 
	    module_name, weblogPath, strerror(errno));
    perror("execlp");
    close(pipe1[0]);
    close(pipe2[1]);
    fclose(fp);
    
    /* exit by sending a SIGTERM to the httpd process (how to get the pid?) 
     * since at this point the pid file is outdated and only if we sleep for 
     * a while to allow httpd_main to put it's pid in the pidfile can we 
     * attempt to send it a SIGTERM for graceful shuttdown)
     * so that everything is brought down - if you want to bring everything 
     * down!! Else continue with httpd without AFS authentication.
     */
/*#ifdef SHUTDOWN_IF_AFS_FAILS in afs_module.c */
    if (shutdown) {
#define KILL_TIME_WAIT    1
#define MAX_KILL_ATTEMPTS 3
      int attempts=0;
      fp=fopen(httpd_pid_fname,"r");
      fscanf(fp,"%d", &childpid);
      fclose(fp);
    killagain:
      sleep(KILL_TIME_WAIT); 
      if (kill(childpid,SIGTERM) == -1) {
	if ((errno == ESRCH) && (attempts < MAX_KILL_ATTEMPTS)) {
	  attempts++;
	  fprintf(stderr, "%s:SIGTERM to process:%d FAILED attempt:%d\nRetrying "
		  " for %d more attempts every %d seconds\n",
		  module_name,childpid,attempts, 
		  (MAX_KILL_ATTEMPTS-attempts), KILL_TIME_WAIT);
	  goto killagain;
	}
      }
      else {
	fprintf(stderr, "%s:Shutdown complete ...\n", module_name);
      }
      if (attempts >= MAX_KILL_ATTEMPTS) {
	fprintf(stderr,
		"%s:httpd is still running-AFS authentication will fail "
		"because weblog startup failed\n",module_name);
      }
      exit(0);
    }
    else {
      fprintf(stderr, "%s:httpd running - AFS Authentication will not work! "
	      "Weblog startup failure", module_name);
      exit(-1);
    }
  }
}

/*
 * Returns HTTP error codes based on the result of a stat error
 */
static int sort_stat_error(request_rec *r)
{
  int status = 0;
  switch(errno) {
  case ENOENT:
    status = HTTP_NOT_FOUND;
    break;
    
  case EACCES:
    status = FORBIDDEN;
    break;
    
  case ENOLINK:
      status = HTTP_NOT_FOUND;
    break;
    
  case ENODEV:
    status = HTTP_NOT_FOUND;
    break;
    
  default:
    {
      char error[ERRSTRLEN];
      sprintf(error,"%s: stat error: %s",module_name, strerror(errno));
      status = SERVER_ERROR;
      LOG_REASON(error,r->uri, r);
      break;
    }
  }
  return status;
}

/*
 * recursively stats the path to see where we're going wrong and 
 * chops off the path_info part of it - 
 * Returns OK or an HTTP status code
 * Called if we get a ENOTDIR from the first stab at statting the 
 * entire path - so we assume that we have some PATH_INFO and try to
 * chop it off the end and return the path itself
 * Side effects on request_rec
 - sets the filename field
 - sets the path_info field
 */
static int remove_path_info(request_rec *r, char *path, struct stat *buf)
{
  char *cp;
  char *end;
  char *last_cp = NULL;
  int rc = 0;
  
  afsassert(r);
  afsassert(path);
  afsassert(buf);
  
  end = &path[strlen(path)];
  
  /* Advance over trailing slashes ... NOT part of filename */
  for (cp = end; cp > path && cp[-1] == '/'; --cp)
    continue;
  
  while (cp > path) {
    /* See if the pathname ending here exists... */
    *cp = '\0';
    errno = 0;
    rc = stat(path, buf);
    if (cp != end)
      *cp = '/';
    
    if (!rc) {
      if (S_ISDIR(buf->st_mode) && last_cp) {
    	buf->st_mode = 0;   /* No such file... */
    	cp = last_cp;
          }
      r->path_info = pstrdup(r->pool, cp);
      *cp = '\0';
      return OK;
    }
    
    else if (errno == ENOENT || errno == ENOTDIR) {
      last_cp = cp;
      while (--cp > path && *cp != '/')
    	continue;
      while (cp > path && cp[-1] == '/')
    	--cp;
    }
    else if (errno != EACCES) {
      /* 
       * this would be really bad since we checked the entire path
       * earlier and got ENOTDIR instead of EACCES - so why are we getting
       * it now? Anyway, we ought to return FORBIDDEN
       */
      return HTTP_FORBIDDEN;
    }
  }
  r->filename = pstrdup(r->pool, path);
  return OK;
}
    
/*
 * Checks to see if actual access to the URL is permitted or not
 * stats the URI first, if failure returns FORBIDDEN, if allowed then
 * checks to see if it is a file, dir or LINK (TEST), and accordingly does more
 */
static int can_access(request_rec *r)
{
  int rc;
  char *doc_root = (char *)DOCUMENT_ROOT(r);
  struct stat buf;
  char path[MAX_STRING_LEN];

  afsassert(r->uri);
  afsassert(doc_root);

  if (r->filename) {
    afslog(10, ("%s: Found r->filename:%s", module_name, r->filename));
    sprintf(path,"%s",r->filename);
  }
  else {
    afslog(10, ("%s: Composing path from doc_root:%s and r->uri:%s",
		module_name, doc_root, r->uri));
    sprintf(path,"%s/%s",doc_root,r->uri);
    afslog(10, ("%s: Path:%s", module_name, path));
  } 
  rc = stat(path, &buf);
  if (rc == -1) {
    afslog(2, ("%s: pid:%d\tpath:%s\tstat error:%s", 
	       module_name, getpid(), path, strerror(errno)));
    
    /* 
     * if the requested method is an HTTP PUT and the file does not
     * exist then well, we'll get a stat error but we shouldn't return
     * an error - we should try and open the file in append mode and then
     * see if we get a permission denied error
     */
    if ((strncmp(r->method,"PUT",3) == 0) && (errno == ENOENT)) {
      FILE *f=fopen(path,"a");
      if (f == NULL) {
	if (errno == EACCES) {
	  afslog(2, ("%s: Either AFS acls or other permissions forbid write"
		     " access to file %s for user %s", module_name, path, 
		     r->connection->user ? r->connection->user : "UNKNOWN"));
	  return FORBIDDEN;
	}
	else {
	  log_reason("afs_module: Error checking file for PUT method",r->uri,r);
	  return SERVER_ERROR;
	}
      }
    }
    else if (errno == ENOTDIR) {
      /* 
       * maybe the special case of CGI PATH_INFO to be translated to 
       * PATH_TRANSLATED - check each component of this path
       * and stat it to see what portion of the url is actually 
       * the path and discard the rest for our purposes.
       */
      rc = remove_path_info(r, path, &buf);
      afslog(10,("%s:remove_path_info returned %d path:%s", 
		 module_name, rc, path));
      if (rc) 
    	return rc;      
    }
    else {
      return sort_stat_error(r);
    }
  }
  /* 
   * If we get here then we have something - either a file or a directory
   */
  else {
    if (S_IFREG == (buf.st_mode & S_IFMT)) {
      /* regular file */
      FILE *f;
      char permissions[] = { 'r', '\0', '\0', '\0' }; /* room for +, etc... */
      
      if ((strncmp(r->method, "PUT",3) == 0)) {
	strcpy(permissions,"a");
      }
      if(!(f = fopen(path,permissions))) {
	if (errno == EACCES) {
	  afslog(2, ("%s: Either AFS acls or other permissions"
		     " forbid access to file %s for user %s", module_name, path, 
		     r->connection->user ? r->connection->user : "UNKNOWN"));
	  return FORBIDDEN;
	}
	else {
	  char error[ERRSTRLEN];
	  sprintf(error,"%s: Error checking file %s for permissions:%s",
		  module_name, path, strerror(errno));
	  log_reason(error, r->uri, r);
	  return SERVER_ERROR;
	}
      }
      fclose(f);
      return OK;
    }
    if (S_IFDIR == (buf.st_mode&S_IFMT)) {
      /* path is a directory */

      if (r->uri[strlen(r->uri)-1] != '/'){
	/* if we don't have a trailing slash, return REDIRECT */
	char *ifile;
	if (r->args != NULL) {
	  ifile = PSTRCAT (r->pool, escape_uri(r->pool, r->uri),
			   "/", "?", r->args, NULL);
	}
	else {
	  ifile = PSTRCAT (r->pool, escape_uri(r->pool, r->uri),
			   "/", NULL);
	}
	TABLE_SET (r->headers_out, "Location",ifile);
	return REDIRECT;
      }
      else {
	DIR *d;
	if(!(d = opendir(path))) {
	  if (errno == EACCES) {
	    afslog(2, ("%s: Error accessing dir %s - %s",
		       module_name, path, strerror(errno)));
	    return FORBIDDEN;
	  }
	  else {
	    char error[ERRSTRLEN];
	    sprintf(error,"%s: opendir failed with Error:%s",
		    module_name, strerror(errno));
	    log_reason(error,r,r->uri);
	    return SERVER_ERROR;
	  }
	}
	closedir(d);
	return OK;
      }
    }
  }
}


/*
 * Logs requests which led to a FORBIDDEN return code provided a token
 * existed. Added feature (SetAFSAccessLog) in beta2
 */
static int log_Access_Error(request_rec *r)
{
  if(FIND_LINKED_MODULE("afs_module.c") != NULL) {
    char err_msg[1024];
    int rc = 0;
    int len = 0;
    extern int logfd;

    if (r->connection->user)
      sprintf(err_msg, "[%s] AFS ACL's deny permission to " 
	      "user: %s for URI:%s\n",GET_TIME(), r->connection->user, r->uri);
    else 
      sprintf(err_msg,"[%s] AFS ACL's deny permission to user - for URI:%s\n", 
	      GET_TIME(), r->uri);
    
    len=strlen(err_msg);
    rc=write(logfd, err_msg, len);
    
    if(rc != len){
      afslog(0, ("%s: Error logging message:%s - %s",
		 module_name, err_msg, strerror(errno)));
      return -1;
    }
    return rc;
  }
}

/*
 * The interface - hook to obtain an AFS token if needed based on the
 * result of a stat (returned by can_access) or an open file
 */
int afs_auth_internal(request_rec *r, char *cell) 
{
  afsassert(r);
  afsassert(r->uri);
  afsassert(cell);
  if(FIND_LINKED_MODULE("afs_module.c") != NULL) {
    int rc, status;      
    char *type;  
    static int haveToken = 1; /* assume that we always have a token */
    extern int logAccessErrors;
    
  /*
   * Get afs_authtype directive value for that directory or location
   */
    type = (char *) get_afsauthtype(r);
 
    /* 
     * UserDir (tilde) support
     */
#ifndef APACHE_1_3
    if (FIND_LINKED_MODULE("mod_userdir.c") != NULL) {
      rc=translate_userdir(r);
      if ((rc != OK) && (rc != DECLINED)) {
	LOG_REASON("afs_module: Failure while translating userdir", r->uri,r);
	return rc;
      }
    }
#endif

    afslog(20, ("%s: pid:%d r->uri:%s", module_name, getpid(), r->uri));
    if (type) 
      afslog(20, ("%s: AFSAuthType: %s", module_name, type));
    else 
      afslog(20, ("%s: AFSAuthType NULL", module_name));
    
    /* if AuthType is not AFS, then unlog any existing tokens and DECLINE */
    if (type == NULL) {
      if (haveToken)
	unlog();
      return DECLINED;
    }

    if((strcasecmp(type, AFS_AUTHTYPE)) && (strcasecmp(type,AFS_DFS_AUTHTYPE))) {
      if (haveToken) 
	unlog();
      afslog(10, ("%s: Error unknown AFSAuthType:%s returning DECLINED",
		  module_name, type));
      return DECLINED;
    }
    
    if (cell) 
      status=authenticateUser(r, cell, afs_str->cacheExpiration, type);
    else 
      status=authenticateUser(r, afs_str->defaultCell, afs_str->cacheExpiration, type);
     
    if (status != OK ) {
      afslog(10, ("%s: Returning status %d", module_name, status));
      return status;
    }
    
    /* can we access this URL? */
    rc = can_access(r);
    
    if (rc == OK) {
      return DECLINED;
    }
    
    if (rc == REDIRECT) {
      return REDIRECT;
    }
    
    if (rc == FORBIDDEN) {
      rc = forbToAuthReqd(r);
      if (rc == FORBIDDEN) {
	if (logAccessErrors) {
	  log_Access_Error(r);
	}
      }
      return rc;
    }
    return DECLINED;
  }
}
