/* auth.c:  ra_serf authentication handling
 *
 * ====================================================================
 * Copyright (c) 2007 CollabNet.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://subversion.tigris.org/license-1.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 *
 * This software consists of voluntary contributions made by many
 * individuals.  For exact contribution history, see the revision
 * history and logs, available at http://subversion.tigris.org/.
 * ====================================================================
 */

/*** Includes. ***/

#include <serf.h>
#include <apr_base64.h>

#include "ra_serf.h"
#include "win32_auth_sspi.h"
#include "svn_private_config.h"

/*** Forward declarations. ***/

static svn_error_t *
handle_basic_auth(svn_ra_serf__session_t *session,
                  svn_ra_serf__connection_t *conn,
                  serf_request_t *request,
                  serf_bucket_t *response,
                  char *auth_hdr,
                  char *auth_attr,
                  apr_pool_t *pool);

static svn_error_t *
init_basic_connection(svn_ra_serf__session_t *session,
                      svn_ra_serf__connection_t *conn,
                      apr_pool_t *pool);

static svn_error_t *
setup_request_basic_auth(svn_ra_serf__connection_t *conn,
                         serf_bucket_t *hdrs_bkt);

static svn_error_t *
handle_proxy_basic_auth(svn_ra_serf__session_t *session,
                        svn_ra_serf__connection_t *conn,
                        serf_request_t *request,
                        serf_bucket_t *response,
                        char *auth_hdr,
                        char *auth_attr,
                        apr_pool_t *pool);

static svn_error_t *
init_proxy_basic_connection(svn_ra_serf__session_t *session,
                            svn_ra_serf__connection_t *conn,
                            apr_pool_t *pool);

static svn_error_t *
setup_request_proxy_basic_auth(svn_ra_serf__connection_t *conn,
                               serf_bucket_t *hdrs_bkt);

/*** Global variables. ***/
static const svn_ra_serf__auth_protocol_t serf_auth_protocols[] = {
  {
    401,
    "Basic",
    init_basic_connection,
    handle_basic_auth,
    setup_request_basic_auth,
  },
  {
    407,
    "Basic",
    init_proxy_basic_connection,
    handle_proxy_basic_auth,
    setup_request_proxy_basic_auth,
  },
#ifdef SVN_RA_SERF_SSPI_ENABLED
  {
    401,
    "NTLM",
    init_sspi_connection,
    handle_sspi_auth,
    setup_request_sspi_auth,
  },
#endif /* SVN_RA_SERF_SSPI_ENABLED */

  /* ADD NEW AUTHENTICATION IMPLEMENTATIONS HERE (as they're written) */

  /* sentinel */
  { 0 }
};

/*** Code. ***/

/**
 * base64 encode the authentication data and build an authentication
 * header in this format:
 * [PROTOCOL] [BASE64 AUTH DATA]
 */
void
svn_ra_serf__encode_auth_header(const char * protocol, char **header,
                                const char * data, apr_size_t data_len,
                                apr_pool_t *pool)
{
  apr_size_t encoded_len, proto_len;
  char * ptr;

  encoded_len = apr_base64_encode_len(data_len);
  proto_len = strlen(protocol);

  *header = apr_palloc(pool, encoded_len + proto_len + 1);
  ptr = *header;

  apr_cpystrn(ptr, protocol, proto_len + 1);
  ptr += proto_len;
  *ptr++ = ' ';

  apr_base64_encode(ptr, data, data_len);
}


svn_error_t *
svn_ra_serf__handle_auth(int code,
                         svn_ra_serf__session_t *session,
                         svn_ra_serf__connection_t *conn,
                         serf_request_t *request,
                         serf_bucket_t *response,
                         apr_pool_t *pool)
{
  serf_bucket_t *hdrs;
  const svn_ra_serf__auth_protocol_t *prot;
  char *auth_name, *auth_attr, *auth_hdr, *header, *header_attr;
  svn_error_t *cached_err;

  hdrs = serf_bucket_response_get_headers(response);
  if (code == 401)
    auth_hdr = (char*)serf_bucket_headers_get(hdrs, "WWW-Authenticate");
  else if (code == 407)
    auth_hdr = (char*)serf_bucket_headers_get(hdrs, "Proxy-Authenticate");

  if (!auth_hdr)
    {
      if (session->auth_protocol)
        return svn_error_createf(SVN_ERR_AUTHN_FAILED, NULL,
                                 "%s Authentication failed",
                                 session->auth_protocol->auth_name);
      else
        return svn_error_create(SVN_ERR_AUTHN_FAILED, NULL, NULL);
    }

  /* If multiple *-Authenticate headers are found, serf will combine them into
     one header, with the values separated by a comma. */
  header = apr_strtok(auth_hdr, ",", &header_attr);

  while (header)
    {
      svn_boolean_t proto_found = FALSE;
      auth_name = apr_strtok(header, " ", &auth_attr);

      cached_err = SVN_NO_ERROR;

      /* Find the matching authentication handler.
         Note that we don't reuse the auth protocol stored in the session,
         as that may have changed. (ex. fallback from ntlm to basic.) */
      for (prot = serf_auth_protocols; prot->code != 0; ++prot)
        {
          if (code == prot->code && strcmp(auth_name, prot->auth_name) == 0)
            {
              svn_serf__auth_handler_func_t handler = prot->handle_func;
              svn_error_t *err = NULL;

              /* If this is the first time we use this protocol in this session,
                 make sure to initialize the authentication part of the session
                 first. */
              if (code == 401 && session->auth_protocol != prot)
                {
                  err = prot->init_conn_func(session, conn, session->pool);
                  if (err == SVN_NO_ERROR)
                    session->auth_protocol = prot;
                  else
                    session->auth_protocol = NULL;
                }
             else if (code == 407 && session->proxy_auth_protocol != prot)
                {
                  err = prot->init_conn_func(session, conn, session->pool);
                  if (err == SVN_NO_ERROR)
                    session->proxy_auth_protocol = prot;
                  else
                    session->proxy_auth_protocol = NULL;
                }

              if (err == SVN_NO_ERROR)
                {
                  proto_found = TRUE;
                  err = handler(session, conn, request, response,
                                header, auth_attr, session->pool);
                }
              if (err)
                {
                  /* If authentication fails, cache the error for now. Try the
                     next available scheme. If there's none raise the error. */
                  proto_found = FALSE;
                  prot = NULL;
                  if (cached_err)
                    svn_error_clear(cached_err);
                  cached_err = err;
                }

              break;
            }
        }
      if (proto_found)
        break;

      /* Try the next Authentication header. */
      header = apr_strtok(NULL, ",", &header_attr);
    }

  SVN_ERR(cached_err);

  if (!prot || prot->auth_name == NULL)
    {
      /* Support more authentication mechanisms. */
      return svn_error_createf(SVN_ERR_AUTHN_FAILED, NULL,
                               "%s authentication not supported.\n"
                               "Authentication failed", auth_name);
    }

  return SVN_NO_ERROR;
}

static svn_error_t *
handle_basic_auth(svn_ra_serf__session_t *session,
                  svn_ra_serf__connection_t *conn,
                  serf_request_t *request,
                  serf_bucket_t *response,
                  char *auth_hdr,
                  char *auth_attr,
                  apr_pool_t *pool)
{
  void *creds;
  char *last, *realm_name;
  svn_auth_cred_simple_t *simple_creds;
  const char *tmp;
  apr_size_t tmp_len;
  apr_port_t port;
  int i;

  if (!session->realm)
    {
      char *attr;

      attr = apr_strtok(auth_attr, "=", &last);
      if (strcmp(attr, "realm") == 0)
        {
          realm_name = apr_strtok(NULL, "=", &last);
          if (realm_name[0] == '\"')
            {
              apr_size_t realm_len;

              realm_len = strlen(realm_name);
              if (realm_name[realm_len - 1] == '\"')
                {
                  realm_name[realm_len - 1] = '\0';
                  realm_name++;
                }
            }
        }
      else
        {
          return svn_error_create
            (SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
             _("Missing 'realm' attribute in Authorization header."));
        }
      if (!realm_name)
        {
          return svn_error_create
            (SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
             _("Missing 'realm' attribute in Authorization header."));
        }

      if (session->repos_url.port_str)
        {
          port = session->repos_url.port;
        }
      else
        {
          port = apr_uri_port_of_scheme(session->repos_url.scheme);
        }

      session->realm = apr_psprintf(session->pool, "<%s://%s:%d> %s",
                                    session->repos_url.scheme,
                                    session->repos_url.hostname,
                                    port,
                                    realm_name);
    }

  if (!session->auth_state)
    {
      SVN_ERR(svn_auth_first_credentials(&creds,
                                         &session->auth_state,
                                         SVN_AUTH_CRED_SIMPLE,
                                         session->realm,
                                         session->wc_callbacks->auth_baton,
                                         session->pool));
    }
  else
    {
      SVN_ERR(svn_auth_next_credentials(&creds,
                                        session->auth_state,
                                        session->pool));
    }

  session->auth_attempts++;

  if (!creds || session->auth_attempts > 4)
    {
      /* No more credentials. */
      return svn_error_create(SVN_ERR_AUTHN_FAILED, NULL,
                "No more credentials or we tried too many times.\n"
                "Authentication failed");
    }

  simple_creds = creds;

  tmp = apr_pstrcat(session->pool,
                    simple_creds->username, ":", simple_creds->password, NULL);
  tmp_len = strlen(tmp);

  svn_ra_serf__encode_auth_header(session->auth_protocol->auth_name,
                                  &session->auth_value, tmp, tmp_len, pool);
  session->auth_header = "Authorization";

  /* FIXME Come up with a cleaner way of changing the connection auth. */
  for (i = 0; i < session->num_conns; i++)
    {
      session->conns[i]->auth_header = session->auth_header;
      session->conns[i]->auth_value = session->auth_value;
    }

  return SVN_NO_ERROR;
}

static svn_error_t *
init_basic_connection(svn_ra_serf__session_t *session,
                      svn_ra_serf__connection_t *conn,
                      apr_pool_t *pool)
{
  conn->auth_header = session->auth_header;
  conn->auth_value = session->auth_value;

  return SVN_NO_ERROR;
}

static svn_error_t *
setup_request_basic_auth(svn_ra_serf__connection_t *conn,
                         serf_bucket_t *hdrs_bkt)
{
  /* Take the default authentication header for this connection, if any. */
  if (conn->auth_header && conn->auth_value)
    {
      serf_bucket_headers_setn(hdrs_bkt, conn->auth_header, conn->auth_value);
    }

  return SVN_NO_ERROR;
}

static svn_error_t *
handle_proxy_basic_auth(svn_ra_serf__session_t *session,
                        svn_ra_serf__connection_t *conn,
                        serf_request_t *request,
                        serf_bucket_t *response,
                        char *auth_hdr,
                        char *auth_attr,
                        apr_pool_t *pool)
{
  const char *tmp;
  apr_size_t tmp_len;
  int i;

  tmp = apr_pstrcat(session->pool,
                    session->proxy_username, ":", 
                    session->proxy_password, NULL);
  tmp_len = strlen(tmp);

  session->proxy_auth_attempts++;

  if (session->proxy_auth_attempts > 1)
    {
      /* No more credentials. */
      return svn_error_create(SVN_ERR_AUTHN_FAILED, NULL,
                "Proxy authentication failed");
    }

  svn_ra_serf__encode_auth_header(session->proxy_auth_protocol->auth_name, 
                                  &session->proxy_auth_value,
                                  tmp, tmp_len, pool);
  session->proxy_auth_header = "Proxy-Authorization";

  /* FIXME Come up with a cleaner way of changing the connection auth. */
  for (i = 0; i < session->num_conns; i++)
    {
      session->conns[i]->proxy_auth_header = session->proxy_auth_header;
      session->conns[i]->proxy_auth_value = session->proxy_auth_value;
    }

  return SVN_NO_ERROR;
}

static svn_error_t *
init_proxy_basic_connection(svn_ra_serf__session_t *session,
                            svn_ra_serf__connection_t *conn,
                            apr_pool_t *pool)
{
  conn->proxy_auth_header = session->proxy_auth_header;
  conn->proxy_auth_value = session->proxy_auth_value;

  return SVN_NO_ERROR;
}

static svn_error_t *
setup_request_proxy_basic_auth(svn_ra_serf__connection_t *conn,
                               serf_bucket_t *hdrs_bkt)
{
  /* Take the default authentication header for this connection, if any. */
  if (conn->proxy_auth_header && conn->proxy_auth_value)
    {
      serf_bucket_headers_setn(hdrs_bkt, conn->proxy_auth_header, 
                               conn->proxy_auth_value);
    }

  return SVN_NO_ERROR;
}
