/*
 * Daniel Kouril <kouril@ics.muni.cz>
 * http://meta.cesnet.cz/software/heimdal/negotiate.en.html
*/

/* ====================================================================
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2000 The Apache Software Foundation.  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. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Apache" and "Apache Software Foundation" 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 name, without prior written
 *    permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``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 SOFTWARE FOUNDATION 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.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 * Portions of this software are based upon public domain software
 * originally written at the National Center for Supercomputing Applications,
 * University of Illinois, Urbana-Champaign.
 */
#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_protocol.h"
#include "http_log.h"

#include <gssapi.h>

#define KRB5 1

#ifdef KRB5
#include <krb5.h>
#include <kafs.h>
#endif

module MODULE_VAR_EXPORT gssapi_auth_module;

typedef struct {
  gss_ctx_id_t gss_context;
  int authenticated;
  char *id;
} client_entry_t;

typedef struct {
#ifdef KRB5
  /* These directives could be shared with mod_auth_kerb */
  char *krb5_keytab;  
  int   krb5_save_credentials;
  char *krb5_cells;
#endif
  int save_credentials;
  int use_gss_auth;
} gssapi_config_rec;

#ifdef KRB5
static const char *krb5_save_cells(cmd_parms *, gssapi_config_rec *, char *);
#endif

static const command_rec gssapi_cmds[] =
{
#ifdef KRB5
  { "GssKrb5Keytab", ap_set_file_slot,
    (void*)XtOffsetOf(gssapi_config_rec, krb5_keytab),
    OR_AUTHCFG, TAKE1, "Kerberos v5 keytab." },
  { "GssKrb5SaveCredentials", ap_set_flag_slot,
    (void*)XtOffsetOf(gssapi_config_rec, krb5_save_credentials),
    OR_AUTHCFG, FLAG, "Save the v5 credential cache?" },
  { "GssKrb5AFSCells", krb5_save_cells, NULL,
     OR_AUTHCFG, RAW_ARGS, "Create pag and AFS tokens for cells" },
#endif
  { "GssSaveCredentials", ap_set_flag_slot,
    (void*)XtOffsetOf(gssapi_config_rec, save_credentials),
    OR_AUTHCFG, FLAG, "Save the v5 credential cache?" },

  { "GssAuth", ap_set_flag_slot,
     (void*)XtOffsetOf(gssapi_config_rec, use_gss_auth),
     OR_AUTHCFG, FLAG, "Whether to use GSS-API authentication" },

  /* Server Name ?? */

  { NULL, NULL, NULL, 0, 0, NULL}
};

static const char *
get_gss_error(int min_stat, char *prefix)
{
   /* Opsano z heimdalu (appl/test/gss_common.c) */
   OM_uint32 new_stat;
   OM_uint32 msg_ctx = 0;
   gss_buffer_desc status_string;
   OM_uint32 ret;
   static char buf[1024];

   snprintf(buf, sizeof(buf), "%s: ", prefix);
   do {
      ret = gss_display_status (&new_stat,
	                        min_stat,
				GSS_C_MECH_CODE,
				GSS_C_NO_OID,
				&msg_ctx,
				&status_string);
      strncat(buf, (char*)status_string.value, sizeof(buf) - strlen(buf));
   } while (!GSS_ERROR(ret) && msg_ctx != 0);

   return buf; /* nemuze volat vice potomku a prat se o buf ? */
}

static void initialize_module(server_rec *s, pool *p)
{
  /* gss_acquire_creds() ?? */
}

static void* create_gssapi_dir_config(pool *p, char *dir)
{
  gssapi_config_rec *rec;

  rec = (gssapi_config_rec *) ap_pcalloc(p, sizeof(gssapi_config_rec));
  return rec;
}

static int client_entry_get(request_rec *r, client_entry_t **entry)
{
  /* slo by nejak implementovat ukladani napr. do sdilene pameti nebo souboru?
   */
  client_entry_t *e;

  e = malloc(sizeof(client_entry_t));
  if (e == NULL) {
    return -1;
  }
  memset(e, 0, sizeof(client_entry_t));
  *entry = e;

  return 0;
}

static void client_entry_free(client_entry_t *entry)
{
  free(entry);
}

static int client_entry_store(request_rec *r, client_entry_t *entry)
{
  client_entry_free(entry);
  return 0;
}

#ifdef KRB5
static void krb5_cache_cleanup(void *data)
{
   krb5_context context;
   krb5_ccache  cache;
   krb5_error_code problem;
   char *cache_name = (char *) data;

   problem = krb5_init_context(&context);
   if (problem) {
      ap_log_error(APLOG_MARK, APLOG_ERR, NULL, "krb5_init_context failed");
      return;
   }

   problem = krb5_cc_resolve(context, cache_name, &cache);
   if (problem) {
      ap_log_error(APLOG_MARK, APLOG_ERR, NULL, "krb5_cc_resolve failed (%s: %s)",
	           cache_name, krb5_get_err_text(context, problem)); 
      return;
   }

   krb5_cc_destroy(context, cache);
   krb5_free_context(context);
}

static void krb5_dummy_cleanup(void *data) {}

static void krb5_unlog_cleanup(void *data)
{
   if (k_hasafs())
      k_unlog();
}

static const char *
krb5_save_cells(cmd_parms *cmd, gssapi_config_rec *sec, char *arg)
{
   sec->krb5_cells = ap_pstrdup(cmd->pool,arg);
   return NULL;
}
#endif

static void
note_gssapi_auth_failure(request_rec *r, const gssapi_config_rec *conf)
{
   const char *auth_type = NULL;
   const char *auth_name = NULL;

   /* get the type specified in .htaccess */
   auth_type = ap_auth_type(r);

   /* get the user realm specified in .htaccess */
   auth_name = ap_auth_name(r);

   ap_table_set(r->err_headers_out, "WWW-Authenticate", "GSS-Negotiate ");
#ifdef KRB5
   if (auth_type && strncasecmp(auth_type, "KerberosV5", 10) == 0)
      ap_table_add(r->err_headers_out, "WWW-Authenticate",
	           ap_pstrcat(r->pool, "Basic realm=\"", auth_name, "\"", NULL));
#endif
}
			 

static int authenticate_gssapi_user(request_rec *r)
{
  gssapi_config_rec *conf =
       (gssapi_config_rec *) ap_get_module_config(r->per_dir_config,
						  &gssapi_auth_module);
  OM_uint32 major_status, minor_status, minor_status2;
  gss_buffer_desc input_token = GSS_C_EMPTY_BUFFER;
  gss_buffer_desc output_token = GSS_C_EMPTY_BUFFER;
  const char *auth_line = NULL;
  char *auth_param = NULL;
#ifdef KRB5
  extern krb5_keytab gssapi_krb5_keytab;
  krb5_context kcontext = NULL;
#endif
  int ret;
  client_entry_t *client_entry = NULL;
  gss_name_t client_name;
  gss_cred_id_t delegated_cred = GSS_C_NO_CREDENTIAL;
  char *p;

  /* apache neumi obslouzit vice AuthType na jednom adresari. Ale protoze 
   * chceme podporovat obe metody (gss i Basic) je potreba zavest novou volbu 
   * do konfigurace  */
  if (!conf->use_gss_auth)
     return DECLINED;

  /* get what the user sent us in the HTTP header */
  auth_line = ap_table_get (r->headers_in, "Authorization");
  if(!auth_line) {
      note_gssapi_auth_failure(r, conf);
      return HTTP_UNAUTHORIZED;
  }

  if (strncasecmp(ap_getword_white(r->pool, &auth_line), "GSS-Negotiate", 13) != 0)
     return DECLINED;

  ret = client_entry_get(r, &client_entry); 
  if (ret) {
    ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r,
	         "client_entry_get()");
    ret = SERVER_ERROR;
    goto end;
  }

  if (client_entry->authenticated) {
     ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r,
	          "Klient uz autentizovan");
    return OK;
  }

#ifdef KRB5
  krb5_init_context(&kcontext);
  if (conf->krb5_keytab) {
     if (krb5_kt_resolve(kcontext, conf->krb5_keytab, &gssapi_krb5_keytab))
	ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r,
	             "Chyba pri krb5_kt_resolve()");
  }
#endif

  /* ap_getword() posune parametr */
  /* pointer vraceny ap_getword() je alokovan -- mel by se uvolnit ??? */
  auth_param = ap_getword_white(r->pool, &auth_line);
  if (auth_param != NULL) {
    input_token.length = ap_base64decode_len(auth_param);
    input_token.value = malloc(input_token.length);
    if (input_token.value == NULL) {
      ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r,
	           "ENOMEM");
      ret = HTTP_UNAUTHORIZED; /* XXX */
      goto end;
    }
    input_token.length = ap_base64decode(input_token.value, auth_param);
  } else {
     ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r,
	          "zadny auth parametr od klienta");
  }

  major_status = gss_accept_sec_context(&minor_status,
                                        &client_entry->gss_context,
	                                /* &gss_context,*/
					GSS_C_NO_CREDENTIAL,
					&input_token,
					GSS_C_NO_CHANNEL_BINDINGS,
					&client_name,
					NULL,
					&output_token,
					NULL,
					NULL,
					&delegated_cred);
  if (output_token.length) {
     auth_param = malloc(ap_base64encode_len(output_token.length));
     if (auth_param == NULL) {
	ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r,
	             "Gss: not enough memory");
        ret = SERVER_ERROR;
	goto end;
     }
     ap_base64encode(auth_param, output_token.value, output_token.length);
     ap_table_set(r->err_headers_out, "WWW-Authenticate",
	          ap_pstrcat(r->pool, "GSS-Negotiate ", auth_param, NULL));
     free(auth_param);
     gss_release_buffer(&minor_status2, &output_token);
  }

  if (GSS_ERROR(major_status)) {
     ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r,
	           "%s", 
		   get_gss_error(minor_status, "chyba v gss_accept_sec_context()"));
     client_entry_free(client_entry);
     ret = HTTP_UNAUTHORIZED;
     goto end;
  }

  client_entry_store(r, client_entry);
  if (major_status & GSS_S_CONTINUE_NEEDED) {
     ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r,
	          "nepodporujeme slozitejsi mechanimsmy"); 
     ret = HTTP_UNAUTHORIZED;
     goto end;
  }

  major_status = gss_export_name(&minor_status, client_name, &output_token);
  gss_release_name(&minor_status, &client_name); /* take jeste jinam */
  if (GSS_ERROR(major_status)) {
    ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r,
	          "%s",
		  get_gss_error(minor_status, "chyba v gss_export_name()"));
    ret = SERVER_ERROR;
    goto end;
  }

  /* client_entry->authenticated = 1; */
  r->connection->user = strdup(output_token.value);
  p = strchr(r->connection->user, '@');
  if (p != NULL)
     *p = '\0';

  gss_release_buffer(&minor_status, &output_token);

#ifdef KRB5
  if (delegated_cred && delegated_cred->ccache) {
     krb5_error_code problem;
     char *cache_name = NULL;
     krb5_ccache ccache;
     krb5_principal principal;

     ret = SERVER_ERROR;

     if (conf->krb5_cells && k_hasafs()) {
	char *cells = ap_pstrdup(r->pool,conf->krb5_cells),*next;

	k_setpag();
	for (next=strtok(cells,"	"); next; next = strtok(NULL," 	"))
	    krb5_afslog(kcontext,ccache,next,NULL);
	ap_register_cleanup(r->pool,NULL,krb5_unlog_cleanup,krb5_dummy_cleanup);
     }

     problem = krb5_cc_gen_new(kcontext, &krb5_fcc_ops, &ccache);
     if (problem) {
	ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r,
	              "Gss: krb5_cc_gen_new() failed (%s)",
		      krb5_get_err_text(kcontext, problem));
	goto end;
     }

     problem = krb5_cc_get_principal(kcontext, delegated_cred->ccache, 
	                             &principal);
     if (problem) {
	ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r,
	              "Gss: krb5_cc_get_principal() failed (%s)",
		      krb5_get_err_text(kcontext, problem));
	goto end;
     }

     problem = krb5_cc_initialize(kcontext, ccache, principal);
     if (problem) {
	ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r,
	              "Gss: krb5_cc_initialize() failed (%s)",
		      krb5_get_err_text(kcontext, problem));
	goto end;
     }

     problem = krb5_cc_copy_cache(kcontext, delegated_cred->ccache, ccache);
     if (problem) {
	ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, r,
	              "Gss: krb5_cc_copy_cache() failed (%s)",
		      krb5_get_err_text(kcontext, problem));
	goto end;
     }

     cache_name = ap_pstrdup(r->pool, krb5_cc_get_name(kcontext, ccache));
     ap_table_setn(r->subprocess_env, "KRB5CCNAME", cache_name);
     ap_register_cleanup(r->pool, cache_name, krb5_cache_cleanup, krb5_dummy_cleanup);
     
     krb5_cc_close(kcontext, ccache);
  }
#endif
  ret = OK;

end:
  if (delegated_cred)
     gss_release_cred(&minor_status, &delegated_cred);

#ifdef KRB5
  krb5_free_context(kcontext);
#endif

  if (ret == HTTP_UNAUTHORIZED)
     note_gssapi_auth_failure(r, conf);

  return ret;
}

static int gssapi_check_user(request_rec *r)
{
  gssapi_config_rec *conf =
     (gssapi_config_rec *) ap_get_module_config(r->per_dir_config,
						&gssapi_auth_module);
  char *user = r->connection->user;
  const char *auth_line = NULL;
  const char *auth_type = NULL;
  int m = r->method_number;
  int method_restricted = 0;
  register int x;
  const char *t, *w;
  const require_line *require;
  const array_header *required_users;
  char *p, *instance;

  if (!conf->use_gss_auth)
     return DECLINED;

  auth_line = ap_table_get (r->headers_in, "Authorization");
  if(auth_line == NULL)
     return AUTH_REQUIRED;

  auth_type = ap_getword_white(r->pool, &auth_line);

  if (strncasecmp(auth_type, "Basic", 5) == 0)
     return DECLINED;

  if (strncasecmp(auth_type, "GSS-Negotiate", 13) != 0) {
     note_gssapi_auth_failure(r, conf);
     return AUTH_REQUIRED;
  }

  required_users = ap_requires(r);
  if (required_users == NULL)
     return OK;

  require = (require_line *) required_users->elts;

  instance = strdup(user);
  if ((p = strchr(instance, '@')) != NULL)
     *p = '\0';

  for (x = 0; x < required_users->nelts; x++) {
     if (! (require[x].method_mask & (1 << m)))
	continue;

     method_restricted = 1;

     t = require[x].requirement;
     w = ap_getword_white(r->pool, &t);
     if (strcmp(w, "valid-user") == 0) {
	free(instance);
	return OK;
     }


     if (strcmp(w, "user") == 0) {
	while (t[0] != '\0') {
	   w = ap_getword_conf(r->pool, &t);
	   if (strchr(w, '@') == NULL) {
	      if (!strcmp(instance, w)) {
		 free(instance);
		 return OK;
	      }
	   } else
	      if (!strcmp(user, w)) {
		 free(instance);
	         return OK;
	      }
	}
     }
  }

  free(instance);

  if (! method_restricted)
     return OK;

  ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
	"Gssapi: access to %s failed, reason: user %s not allowed access",
	r->uri, user);

  note_gssapi_auth_failure(r, conf);

  return AUTH_REQUIRED;
}

module MODULE_VAR_EXPORT gssapi_auth_module =
{
  STANDARD_MODULE_STUFF, 
  initialize_module,          /* initializer */
  create_gssapi_dir_config,   /* dir config creater */
  NULL,                       /* dir merger --- default is to override */
  NULL,                       /* server config */
  NULL,                       /* merge server config */
  gssapi_cmds,                /* command table */
  NULL,                       /* handlers */
  NULL,                       /* filename translation */
  authenticate_gssapi_user,   /* check_user_id */
  gssapi_check_user,          /* check auth */
  NULL,                       /* check access */
  NULL,                       /* type_checker */
  NULL,                       /* fixups */
  NULL,                       /* logger */
  NULL,                       /* header parser */
  NULL,                       /* child_init */
  NULL,                       /* child_exit */
  NULL                        /* post read-request */
};
