/*
  This file is part of Challenger
  Copyright (C) 2023, 2024 Taler Systems SA

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

  Challenger 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 Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public License along with
  Challenger; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file challenger-httpd_common.c
 * @brief common helper functions
 * @author Christian Grothoff
 */
#include "platform.h"
#include "challenger-httpd_common.h"
#include <gnunet/gnunet_pq_lib.h>
#include "challenger-database/get_validation.h"


/**
 * Prefix required for all Bearer tokens.
 */
#define RFC_8959_PREFIX "secret-token:"

/**
 * Maximum number of retries for the database interaction.
 */
#define MAX_RETRIES 3


// FIXME: enums would be nicer...
int
CH_get_output_type (struct MHD_Connection *connection)
{
  const char *mime;
  double q_html;
  double q_json;

  mime = MHD_lookup_connection_value (connection,
                                      MHD_HEADER_KIND,
                                      MHD_HTTP_HEADER_ACCEPT);
  if (NULL == mime)
    return 1; /* default to JSON */
  q_html = TALER_pattern_matches (mime,
                                  "text/html");
  q_json = TALER_pattern_matches (mime,
                                  "application/json");
  return (q_html > q_json) ? 0 : 1;
}


const char *
CH_get_client_secret (struct MHD_Connection *connection,
                      enum TALER_ErrorCode *ec)
{
  const char *bearer = "Bearer ";
  const char *auth;
  const char *tok;
  enum TALER_ErrorCode ignored;

  if (NULL == ec)
    ec = &ignored;
  auth = MHD_lookup_connection_value (connection,
                                      MHD_HEADER_KIND,
                                      MHD_HTTP_HEADER_AUTHORIZATION);
  if (NULL == auth)
  {
    *ec = TALER_EC_GENERIC_PARAMETER_MISSING;
    return NULL;
  }
  /* RFC 7235: the auth-scheme token ("Bearer") is case-insensitive. */
  if (0 !=
      strncasecmp (auth,
                   bearer,
                   strlen (bearer)))
  {
    *ec = TALER_EC_GENERIC_PARAMETER_MALFORMED;
    return NULL;
  }
  tok = auth + strlen (bearer);
  while (' ' == *tok)
    tok++;
  if (0 !=
      strncasecmp (tok,
                   RFC_8959_PREFIX,
                   strlen (RFC_8959_PREFIX)))
  {
    *ec = TALER_EC_GENERIC_PARAMETER_MALFORMED;
    return NULL;
  }
  *ec = TALER_EC_NONE;
  return tok;
}


char *
CH_compute_code (const struct CHALLENGER_ValidationNonceP *nonce,
                 const char *client_secret,
                 const char *client_scope,
                 const json_t *address,
                 const char *client_redirect_uri)
{
  char *code;
  char *ns;
  char *hs;
  struct GNUNET_ShortHashCode h;
  char *astr;

  astr = json_dumps (address,
                     JSON_COMPACT);
  GNUNET_assert (NULL != astr);
  GNUNET_assert (GNUNET_YES ==
                 GNUNET_CRYPTO_hkdf_gnunet (
                   &h,
                   sizeof (h),
                   nonce,
                   sizeof (*nonce),
                   client_secret,
                   strlen (client_secret),
                   GNUNET_CRYPTO_kdf_arg_string (astr),
                   GNUNET_CRYPTO_kdf_arg_string (client_redirect_uri),
                   GNUNET_CRYPTO_kdf_arg_string (NULL != client_scope
                                                 ? client_scope
                                                 : "")));
  free (astr);
  ns = GNUNET_STRINGS_data_to_string_alloc (nonce,
                                            sizeof (*nonce));
  hs = GNUNET_STRINGS_data_to_string_alloc (&h,
                                            sizeof (h));
  GNUNET_asprintf (&code,
                   "%s-%s",
                   ns,
                   hs);
  GNUNET_free (ns);
  GNUNET_free (hs);
  return code;
}


enum GNUNET_GenericReturnValue
CH_code_to_nonce (const char *code,
                  struct CHALLENGER_ValidationNonceP *nonce)
{
  const char *dash = strchr (code, '-');

  if (NULL == dash)
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      GNUNET_STRINGS_string_to_data (code,
                                     (size_t) (dash - code),
                                     nonce,
                                     sizeof (*nonce)))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Return a copy of @a hint that is safe to embed in the quoted-string of
 * an HTTP header: RFC 9110 quoted-strings may not contain a bare '"' or
 * '\\', and error code hints are free-form text that sometimes does.
 * Rather than emitting quoted-pairs (which sloppy clients mis-parse), the
 * offending characters are substituted.
 *
 * @param hint text to sanitize
 * @return newly allocated sanitized string
 */
static char *
quote_safe (const char *hint)
{
  char *out = GNUNET_strdup (hint);

  for (char *p = out; '\0' != *p; p++)
    if ( ('"' == *p) ||
         ('\\' == *p) )
      *p = '\'';
  return out;
}


enum MHD_Result
CH_reply_with_oauth_error (
  struct MHD_Connection *connection,
  unsigned int http_status,
  const char *oauth_error,
  enum TALER_ErrorCode ec,
  const char *detail)
{
  struct MHD_Response *resp;
  enum MHD_Result mret;

  resp = TALER_MHD_make_json_steal (
    GNUNET_JSON_PACK (
      TALER_MHD_PACK_EC (ec),
      GNUNET_JSON_pack_allow_null (
        GNUNET_JSON_pack_string ("error",
                                 oauth_error)),
      GNUNET_JSON_pack_allow_null (
        GNUNET_JSON_pack_string ("detail",
                                 detail))));
  if (MHD_HTTP_UNAUTHORIZED == http_status)
  {
    char *www_auth;

    /* RFC 6750 section 3: the challenge carries a realm, and SHOULD carry
       an error_description; but when the request contained no
       authentication credentials at all it must NOT carry an error code,
       which the caller signals by passing NULL for @a oauth_error. */
    if (NULL == oauth_error)
    {
      GNUNET_asprintf (&www_auth,
                       "Bearer realm=\"%s\"",
                       CH_OAUTH_REALM);
    }
    else
    {
      char *desc = quote_safe (TALER_ErrorCode_get_hint (ec));

      GNUNET_asprintf (&www_auth,
                       "Bearer realm=\"%s\", error=\"%s\", error_description=\"%s\"",
                       CH_OAUTH_REALM,
                       oauth_error,
                       desc);
      GNUNET_free (desc);
    }
    GNUNET_break (MHD_YES ==
                  MHD_add_response_header (
                    resp,
                    "WWW-Authenticate",
                    www_auth));
    GNUNET_free (www_auth);
  }
  mret = MHD_queue_response (connection,
                             http_status,
                             resp);
  MHD_destroy_response (resp);
  return mret;
}


enum GNUNET_GenericReturnValue
CH_build_full_redirect_url (
  const struct CHALLENGER_ValidationNonceP *nonce,
  struct MHD_Connection *connection,
  char **url)
{
  char *client_secret;
  json_t *address;
  char *client_scope;
  char *client_state;
  char *client_redirect_uri;
  enum GNUNET_DB_QueryStatus qs;
  enum MHD_Result ret;

  /* The session is SERIALIZABLE, so a serialization failure is a routine
     outcome here; this runs on the success path of /solve and must not
     turn into a 500 for a user who already passed the challenge. */
  for (unsigned int r = 0; r<MAX_RETRIES; r++)
  {
    qs = CHALLENGERDB_get_validation (CH_context,
                                      nonce,
                                      &client_secret,
                                      &address,
                                      &client_scope,
                                      &client_state,
                                      &client_redirect_uri);
    switch (qs)
    {
    case GNUNET_DB_STATUS_HARD_ERROR:
      GNUNET_break (0);
      ret = TALER_MHD_reply_with_error (
        connection,
        MHD_HTTP_INTERNAL_SERVER_ERROR,
        TALER_EC_GENERIC_DB_FETCH_FAILED,
        "get_validation");
      return (MHD_NO == ret) ? GNUNET_SYSERR : GNUNET_NO;
    case GNUNET_DB_STATUS_SOFT_ERROR:
      if (r < MAX_RETRIES - 1)
        continue;
      GNUNET_break (0);
      ret = TALER_MHD_reply_with_error (
        connection,
        MHD_HTTP_INTERNAL_SERVER_ERROR,
        TALER_EC_GENERIC_DB_FETCH_FAILED,
        "get_validation");
      return (MHD_NO == ret) ? GNUNET_SYSERR : GNUNET_NO;
    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      /* Reachable when a concurrent /token consumed the validation
         between /solve and the construction of the redirect URL; that is
         a client-side race, not a bug of ours. */
      GNUNET_break_op (0);
      ret = TALER_MHD_reply_with_error (
        connection,
        MHD_HTTP_NOT_FOUND,
        TALER_EC_CHALLENGER_GENERIC_VALIDATION_UNKNOWN,
        NULL);
      return (MHD_NO == ret) ? GNUNET_SYSERR : GNUNET_NO;
    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
      break;
    }
    break;
  } /* for MAX_RETRIES */
  {
    char *code;
    char sep;

    /* RFC 6749 3.1.2 permits redirect URIs with a query component;
       use '&' as separator if the URI already contains a '?'. */
    sep = (NULL == strchr (client_redirect_uri, '?')) ? '?' : '&';
    code = CH_compute_code (nonce,
                            client_secret,
                            client_scope,
                            address,
                            client_redirect_uri);
    if (NULL == client_state)
    {
      GNUNET_asprintf (url,
                       "%s%ccode=%s",
                       client_redirect_uri,
                       sep,
                       code);
    }
    else
    {
      char *url_encoded;

      url_encoded = TALER_urlencode (client_state);
      GNUNET_asprintf (url,
                       "%s%ccode=%s&state=%s",
                       client_redirect_uri,
                       sep,
                       code,
                       url_encoded);
      GNUNET_free (url_encoded);
    }
    GNUNET_free (code);
  }
  json_decref (address);
  GNUNET_free (client_scope);
  GNUNET_free (client_secret);
  GNUNET_free (client_redirect_uri);
  GNUNET_free (client_state);
  return GNUNET_OK;
}
