/* GNU Mailutils -- a suite of utilities for electronic mail
   Copyright (C) 2003, 2004, 2005, 2007, 2008, 2009, 2010 Free Software
   Foundation, Inc.

   GNU Mailutils is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3, or (at your option)
   any later version.

   GNU Mailutils is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with GNU Mailutils.  If not, see <http://www.gnu.org/licenses/>. */

#include "imap4d.h"
#include <gsasl.h>
#include <mailutils/gsasl.h>
#ifdef USE_SQL
# include <mailutils/sql.h>
#endif

static Gsasl *ctx;   
static Gsasl_session *sess_ctx; 

static void auth_gsasl_capa_init (int disable);

static void
finish_session (void)
{
  gsasl_finish (sess_ctx);
}

static int
restore_and_return (struct imap4d_auth *ap, mu_stream_t *str, int resp)
{
  mu_stream_unref (str[0]);
  mu_stream_unref (str[1]);
  ap->response = resp;
  return imap4d_auth_resp;
}

static enum imap4d_auth_result
auth_gsasl (struct imap4d_auth *ap)
{
  char *input_str = NULL;
  size_t input_size = 0;
  size_t input_len;
  char *output;
  int rc;
  
  rc = gsasl_server_start (ctx, ap->auth_type, &sess_ctx);
  if (rc != GSASL_OK)
    {
      mu_diag_output (MU_DIAG_NOTICE, _("SASL gsasl_server_start: %s"),
 	              gsasl_strerror (rc));
      return imap4d_auth_fail;
    }

  gsasl_callback_hook_set (ctx, &ap->username);

  output = NULL;
  while ((rc = gsasl_step64 (sess_ctx, input_str, &output))
	   == GSASL_NEEDS_MORE)
    {
      io_sendf ("+ %s\n", output);
      io_getline (&input_str, &input_size, &input_len);
    }
  
  if (rc != GSASL_OK)
    {
      mu_diag_output (MU_DIAG_NOTICE, _("GSASL error: %s"),
		      gsasl_strerror (rc));
      free (input_str);
      free (output);
      ap->response = RESP_NO;
      return imap4d_auth_resp;
    }

  /* Some SASL mechanisms output additional data when GSASL_OK is
     returned, and clients must respond with an empty response. */
  if (output[0])
    {
      io_sendf ("+ %s\n", output);
      io_getline (&input_str, &input_size, &input_len);
      if (input_len != 0)
	{
	  mu_diag_output (MU_DIAG_NOTICE, _("non-empty client response"));
          free (input_str);
          free (output);
	  ap->response = RESP_NO;
	  return imap4d_auth_resp;
	}
    }

  free (input_str);
  free (output);

  if (ap->username == NULL)
    {
      mu_diag_output (MU_DIAG_NOTICE, _("GSASL %s: cannot get username"),
		      ap->auth_type);
      ap->response = RESP_NO;
      return imap4d_auth_resp;
    }

  auth_gsasl_capa_init (1);
  if (sess_ctx)
    {
      mu_stream_t stream[2], newstream[2];

      rc = mu_stream_ioctl (iostream, MU_IOCTL_SUBSTREAM, MU_IOCTL_OP_GET, 
                            stream);
      if (rc)
	{
	  mu_error (_("%s failed: %s"), "MU_IOCTL_GET_STREAM",
		    mu_stream_strerror (iostream, rc));
	  ap->response = RESP_NO;
	  return imap4d_auth_resp;
	}
      rc = gsasl_encoder_stream (&newstream[0], stream[0], sess_ctx,
				 MU_STREAM_READ);
      if (rc)
	{
	  mu_error (_("%s failed: %s"), "gsasl_encoder_stream",
		    mu_strerror (rc));
	  return restore_and_return (ap, stream, RESP_NO);
	}

      rc = gsasl_decoder_stream (&newstream[1], stream[1], sess_ctx,
				 MU_STREAM_WRITE);
      if (rc)
	{
	  mu_error (_("%s failed: %s"), "gsasl_decoder_stream",
		    mu_strerror (rc));
	  mu_stream_destroy (&newstream[0]);
	  return restore_and_return (ap, stream, RESP_NO);
	}
      
      if (ap->username)
	{
	  if (imap4d_session_setup (ap->username))
	    {
	      mu_stream_destroy (&newstream[0]);
	      mu_stream_destroy (&newstream[1]);
	      return restore_and_return (ap, stream, RESP_NO);
	    }
	}

      /* FIXME: This is not reflected in the transcript. */
      io_stream_completion_response (stream[1], ap->command, RESP_OK,
				     "%s authentication successful",
				     ap->auth_type);
      mu_stream_flush (stream[1]);

      mu_stream_unref (stream[0]);
      mu_stream_unref (stream[1]);
      
      rc = mu_stream_ioctl (iostream, MU_IOCTL_SUBSTREAM, 
                            MU_IOCTL_OP_SET, newstream);
      if (rc)
	{
	  mu_error (_("%s failed when it should not: %s"),
		    "MU_IOCTL_SET_STREAM",
		    mu_stream_strerror (iostream, rc));
	  abort ();
	}

      mu_stream_unref (newstream[0]);
      mu_stream_unref (newstream[1]);
      
      util_atexit (finish_session);
      return imap4d_auth_ok;
    }
  
  ap->response = RESP_OK;
  return imap4d_auth_resp;
}

static void
auth_gsasl_capa_init (int disable)
{
  int rc;
  char *listmech;
  struct mu_wordsplit ws;
  
  rc =  gsasl_server_mechlist (ctx, &listmech);
  if (rc != GSASL_OK)
    return;

  ws.ws_delim = " ";
  if (mu_wordsplit (listmech, &ws,
		    MU_WRDSF_DELIM|MU_WRDSF_SQUEEZE_DELIMS|
		    MU_WRDSF_NOVAR|MU_WRDSF_NOCMD))
    {
      mu_error (_("cannot split line `%s': %s"), listmech,
		mu_wordsplit_strerror (&ws));
    }
  else
    {
      size_t i;

      for (i = 0; i < ws.ws_wordc; i++)
	{
	  if (disable)
	    auth_remove (ws.ws_wordv[i]);
	  else
	    {
	      auth_add (ws.ws_wordv[i], auth_gsasl);
	      ws.ws_wordv[i] = NULL;
	    }
	}
      mu_wordsplit_free (&ws);
    }
  free (listmech);
}

#define IMAP_GSSAPI_SERVICE "imap"

static int
retrieve_password (Gsasl *ctx, Gsasl_session *sctx)
{
  char **username = gsasl_callback_hook_get (ctx);
  const char *authid = gsasl_property_get (sctx, GSASL_AUTHID);
  
  if (username && *username == 0)
    *username = strdup (authid);

  if (mu_gsasl_module_data.cram_md5_pwd
      && access (mu_gsasl_module_data.cram_md5_pwd, R_OK) == 0)
    {
      char *key;
      int rc = gsasl_simple_getpass (mu_gsasl_module_data.cram_md5_pwd,
				     authid, &key);
      if (rc == GSASL_OK)
	{
	  gsasl_property_set (sctx, GSASL_PASSWORD, key);
	  free (key);
	  return rc;
	}
    }
  
#ifdef USE_SQL
  if (mu_sql_module_config.password_type == password_plaintext)
    {
      char *passwd;
      int status = mu_sql_getpass (*username, &passwd);
      if (status == 0)
	{
	  gsasl_property_set (sctx, GSASL_PASSWORD, passwd);
	  free (passwd);
	  return GSASL_OK;
	}
    }
#endif
  
  return GSASL_AUTHENTICATION_ERROR; 
}

static int
cb_validate (Gsasl *ctx, Gsasl_session *sctx)
{
  int rc;
  struct mu_auth_data *auth;
  char **username = gsasl_callback_hook_get (ctx);
  const char *authid = gsasl_property_get (sctx, GSASL_AUTHID);
  const char *pass = gsasl_property_get (sctx, GSASL_PASSWORD);

  if (!authid)
    return GSASL_NO_AUTHID;
  if (!pass)
    return GSASL_NO_PASSWORD;
  
  *username = strdup (authid);
  
  auth = mu_get_auth_by_name (*username);

  if (auth == NULL)
    return GSASL_AUTHENTICATION_ERROR;

  rc = mu_authenticate (auth, pass);
  mu_auth_data_free (auth);
  
  return rc == 0 ? GSASL_OK : GSASL_AUTHENTICATION_ERROR;
}
  
static int
callback (Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop)
{
    int rc = GSASL_OK;

    switch (prop) {
    case GSASL_PASSWORD:
      rc = retrieve_password (ctx, sctx);
      break;
      
    case GSASL_SERVICE:
      gsasl_property_set (sctx, prop,
			  mu_gsasl_module_data.service ?
			    mu_gsasl_module_data.service :IMAP_GSSAPI_SERVICE);
      break;
      
    case GSASL_REALM:
      gsasl_property_set (sctx, prop,
			  mu_gsasl_module_data.realm ?
			    mu_gsasl_module_data.realm : util_localname ());
      break;
      
    case GSASL_HOSTNAME:
      gsasl_property_set (sctx, prop,
			  mu_gsasl_module_data.hostname ?
			    mu_gsasl_module_data.hostname : util_localname ());
      break;
      
#if 0
    FIXME:
    case GSASL_VALIDATE_EXTERNAL:
    case GSASL_VALIDATE_SECURID:
#endif
      
    case GSASL_VALIDATE_SIMPLE:
      rc = cb_validate (ctx, sctx);
      break;
      
    case GSASL_VALIDATE_ANONYMOUS:
      if (mu_gsasl_module_data.anon_user)
	{
	  char **username = gsasl_callback_hook_get (ctx);
	  mu_diag_output (MU_DIAG_INFO, _("anonymous user %s logged in"),
			  gsasl_property_get (sctx, GSASL_ANONYMOUS_TOKEN));
	  *username = strdup (mu_gsasl_module_data.anon_user);
	}
      else
	{
	  mu_diag_output (MU_DIAG_ERR,
			  _("attempt to log in as anonymous user denied"));
	}
      break;
      
    case GSASL_VALIDATE_GSSAPI:
      {
	char **username = gsasl_callback_hook_get (ctx);
	*username = strdup (gsasl_property_get(sctx, GSASL_AUTHZID));
	break;
      }
      
    default:
	rc = GSASL_NO_CALLBACK;
	mu_error (_("unsupported callback property %d"), prop);
	break;
    }

    return rc;
}

void
auth_gsasl_init ()
{
  int rc;

  rc = gsasl_init (&ctx);
  if (rc != GSASL_OK)
    {
      mu_diag_output (MU_DIAG_NOTICE, _("cannot initialize libgsasl: %s"),
	      gsasl_strerror (rc));
    }

  gsasl_callback_set (ctx, callback);
  
  auth_gsasl_capa_init (0);
}