/*
  This file is part of Challenger
  Copyright (C) 2023 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_token.c
 * @brief functions to handle incoming /token requests
 * @author Christian Grothoff
 */
#include "platform.h"
#include "challenger-httpd.h"
#include <gnunet/gnunet_util_lib.h>
#include "challenger-httpd_token.h"
#include "challenger-httpd_common.h"
#include <taler/taler_json_lib.h>
#include <taler/taler_signatures.h>
#include "challenger_cm_enums.h"


/**
 * Context for a /token operation.
 */
struct TokenContext
{

  /**
   * Nonce of the validation process the request is about.
   */
  struct CHALLENGER_ValidationNonceP nonce;

  /**
   * Handle for processing uploaded data.
   */
  struct MHD_PostProcessor *pp;

  /**
   * Uploaded 'client_id' field from POST data.
   */
  char *client_id;

  /**
   * Uploaded 'client_id' field from POST data.
   */
  char *redirect_uri;

  /**
   * Uploaded 'client_secret' field from POST data.
   */
  char *client_secret;

  /**
   * Uploaded 'code' field from POST data.
   */
  char *code;

  /**
   * Uploaded 'grant_type' field from POST data.
   */
  char *grant_type;

  /**
   * Uploaded 'code_verifier' field from POST data.
   */
  char *code_verifier;
};


/**
 * Function called to clean up a backup context.
 *
 * @param cls a `struct TokenContext`
 */
static void
cleanup_ctx (void *cls)
{
  struct TokenContext *bc = cls;

  if (NULL != bc->pp)
  {
    GNUNET_break_op (MHD_YES ==
                     MHD_destroy_post_processor (bc->pp));
  }
  GNUNET_free (bc->client_id);
  GNUNET_free (bc->redirect_uri);
  GNUNET_free (bc->client_secret);
  GNUNET_free (bc->code);
  GNUNET_free (bc->grant_type);
  GNUNET_free (bc->code_verifier);
  GNUNET_free (bc);
}


/**
 * Iterator over key-value pairs where the value may be made available
 * in increments and/or may not be zero-terminated.  Used for
 * processing POST data.
 *
 * @param cls a `struct TokenContext *`
 * @param kind type of the value, always #MHD_POSTDATA_KIND when called from MHD
 * @param key 0-terminated key for the value
 * @param filename name of the uploaded file, NULL if not known
 * @param content_type mime-type of the data, NULL if not known
 * @param transfer_encoding encoding of the data, NULL if not known
 * @param data pointer to @a size bytes of data at the
 *              specified offset
 * @param off offset of data in the overall value
 * @param size number of bytes in @a data available
 * @return #MHD_YES to continue iterating,
 *         #MHD_NO to abort the iteration
 */
static enum MHD_Result
post_iter (void *cls,
           enum MHD_ValueKind kind,
           const char *key,
           const char *filename,
           const char *content_type,
           const char *transfer_encoding,
           const char *data,
           uint64_t off,
           size_t size)
{
  struct TokenContext *bc = cls;
  struct Map
  {
    const char *name;
    char **ptr;
  } map[] = {
    {
      .name = "client_id",
      .ptr = &bc->client_id
    },
    {
      .name = "redirect_uri",
      .ptr = &bc->redirect_uri
    },
    {
      .name = "client_secret",
      .ptr = &bc->client_secret
    },
    {
      .name = "code",
      .ptr = &bc->code
    },
    {
      .name = "grant_type",
      .ptr = &bc->grant_type
    },
    {
      .name = "code_verifier",
      .ptr = &bc->code_verifier
    },
    {
      .name = NULL,
      .ptr = NULL
    },
  };
  char **ptr = NULL;
  size_t slen;

  (void) kind;
  (void) filename;
  (void) content_type;
  (void) transfer_encoding;
  (void) off;
  for (unsigned int i = 0; NULL != map[i].name; i++)
    if (0 == strcmp (key,
                     map[i].name))
      ptr = map[i].ptr;
  if (NULL == ptr)
    return MHD_YES; /* ignore */
  if (NULL == *ptr)
    slen = 0;
  else
    slen = strlen (*ptr);
  if (NULL == *ptr)
    *ptr = GNUNET_malloc (size + 1);
  else
    *ptr = GNUNET_realloc (*ptr,
                           slen + size + 1);
  memcpy (&(*ptr)[slen],
          data,
          size);
  return MHD_YES;
}


MHD_RESULT
CH_handler_token (struct CH_HandlerContext *hc,
                  const char *upload_data,
                  size_t *upload_data_size)
{
  struct TokenContext *bc = hc->ctx;

  if (NULL == bc)
  {
    /* first call, setup internals */
    bc = GNUNET_new (struct TokenContext);
    hc->cc = &cleanup_ctx;
    hc->ctx = bc;
    bc->pp = MHD_create_post_processor (hc->connection,
                                        2 * 1024,
                                        &post_iter,
                                        bc);
    TALER_MHD_check_content_length (hc->connection,
                                    2 * 1024);
    return MHD_YES;
  }
  /* handle upload */
  if (0 != *upload_data_size)
  {
    enum MHD_Result res;

    res = MHD_post_process (bc->pp,
                            upload_data,
                            *upload_data_size);
    *upload_data_size = 0;
    if (MHD_YES == res)
      return MHD_YES;
    return MHD_NO;
  }
  if ( (NULL == bc->grant_type) ||
       (0 != strcmp (bc->grant_type,
                     "authorization_code")) )
  {
    GNUNET_break_op (0);
    return CH_reply_with_oauth_error (
      hc->connection,
      MHD_HTTP_BAD_REQUEST,
      "unsupported_grant_type",
      TALER_EC_GENERIC_PARAMETER_MALFORMED,
      "authorization_code");
  }

  if (NULL == bc->code)
  {
    GNUNET_break_op (0);
    return CH_reply_with_oauth_error (
      hc->connection,
      MHD_HTTP_BAD_REQUEST,
      "invalid_request",
      TALER_EC_GENERIC_PARAMETER_MISSING,
      "code");
  }
  if (NULL == bc->client_secret)
  {
    GNUNET_break_op (0);
    return CH_reply_with_oauth_error (
      hc->connection,
      MHD_HTTP_BAD_REQUEST,
      "invalid_client",
      TALER_EC_GENERIC_PARAMETER_MISSING,
      "client_secret");
  }
  if (NULL == bc->client_id)
  {
    GNUNET_break_op (0);
    return CH_reply_with_oauth_error (
      hc->connection,
      MHD_HTTP_BAD_REQUEST,
      "invalid_client",
      TALER_EC_GENERIC_PARAMETER_MISSING,
      "client_id");
  }
  if (NULL == bc->redirect_uri)
  {
    GNUNET_break_op (0);
    return CH_reply_with_oauth_error (
      hc->connection,
      MHD_HTTP_BAD_REQUEST,
      "invalid_request",
      TALER_EC_GENERIC_PARAMETER_MISSING,
      "redirect_uri");
  }

  /* Check this client is authorized to access the service */
  {
    enum GNUNET_DB_QueryStatus qs;
    char *client_url = NULL;
    unsigned long long client_id;
    char dummy;

    if (1 != sscanf (bc->client_id,
                     "%llu%c",
                     &client_id,
                     &dummy))
    {
      GNUNET_break_op (0);
      return CH_reply_with_oauth_error (
        hc->connection,
        MHD_HTTP_BAD_REQUEST,
        "invalid_client",
        TALER_EC_GENERIC_PARAMETER_MALFORMED,
        "client_id");
    }

    qs = CH_db->client_check (CH_db->cls,
                              client_id,
                              bc->client_secret,
                              0, /* do not increment */
                              &client_url);
    switch (qs)
    {
    case GNUNET_DB_STATUS_HARD_ERROR:
      GNUNET_break (0);
      return TALER_MHD_reply_with_error (
        hc->connection,
        MHD_HTTP_INTERNAL_SERVER_ERROR,
        TALER_EC_GENERIC_DB_FETCH_FAILED,
        "client_check");
    case GNUNET_DB_STATUS_SOFT_ERROR:
      GNUNET_break (0);
      return MHD_NO;
    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      GNUNET_break_op (0);
      return CH_reply_with_oauth_error (
        hc->connection,
        MHD_HTTP_UNAUTHORIZED,
        "invalid_client",
        TALER_EC_CHALLENGER_GENERIC_CLIENT_UNKNOWN,
        NULL);
    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
      break;
    }
    if ( (NULL != client_url) &&
         (0 != strcmp (client_url,
                       bc->redirect_uri)) )
    {
      GNUNET_break_op (0);
      return CH_reply_with_oauth_error (
        hc->connection,
        MHD_HTTP_UNAUTHORIZED,
        "invalid_client",
        TALER_EC_CHALLENGER_GENERIC_CLIENT_FORBIDDEN_BAD_REDIRECT_URI,
        NULL);
    }
    GNUNET_free (client_url);
  }

  if (GNUNET_OK !=
      CH_code_to_nonce (bc->code,
                        &bc->nonce))
  {
    GNUNET_break_op (0);
    return CH_reply_with_oauth_error (
      hc->connection,
      MHD_HTTP_UNAUTHORIZED,
      "invalid_grant",
      TALER_EC_CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE,
      NULL);
  }

  /* Check code is valid */
  {
    char *client_secret;
    json_t *address;
    char *client_scope;
    char *client_state;
    char *client_redirect_uri;
    char *code_challenge;
    uint32_t code_challenge_method;
    enum GNUNET_DB_QueryStatus qs;
    char *code;
    enum CHALLENGER_CM code_challenge_method_enum;

    qs = CH_db->validation_get_pkce (CH_db->cls,
                                     &bc->nonce,
                                     &client_secret,
                                     &address,
                                     &client_scope,
                                     &client_state,
                                     &client_redirect_uri,
                                     &code_challenge,
                                     &code_challenge_method);
    switch (qs)
    {
    case GNUNET_DB_STATUS_HARD_ERROR:
      GNUNET_break (0);
      return TALER_MHD_reply_with_error (
        hc->connection,
        MHD_HTTP_INTERNAL_SERVER_ERROR,
        TALER_EC_GENERIC_DB_FETCH_FAILED,
        "validation_get_pkce");
    case GNUNET_DB_STATUS_SOFT_ERROR:
      GNUNET_break (0);
      return MHD_NO;
    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      GNUNET_break_op (0);
      return CH_reply_with_oauth_error (
        hc->connection,
        MHD_HTTP_UNAUTHORIZED,
        "invalid_grant",
        TALER_EC_CHALLENGER_GENERIC_VALIDATION_UNKNOWN,
        "validation_get_pkce");
    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
      break;
    }

    code_challenge_method_enum = CHALLENGER_cm_from_int (
      code_challenge_method);
    if (CHALLENGER_CM_UNKNOWN == code_challenge_method_enum)
    {
      GNUNET_break (0);
      json_decref (address);
      GNUNET_free (client_scope);
      GNUNET_free (client_secret);
      GNUNET_free (client_redirect_uri);
      GNUNET_free (client_state);
      GNUNET_free (code_challenge);
      return TALER_MHD_reply_with_error (
        hc->connection,
        MHD_HTTP_INTERNAL_SERVER_ERROR,
        TALER_EC_GENERIC_PARAMETER_MALFORMED,
        "Invalid code_challenge_method");
    }

    /* Verify the code_challenge if present*/
    if (NULL != code_challenge)
    {
      if (NULL == bc->code_verifier)
      {
        GNUNET_break_op (0);
        json_decref (address);
        GNUNET_free (client_scope);
        GNUNET_free (client_secret);
        GNUNET_free (client_redirect_uri);
        GNUNET_free (client_state);
        GNUNET_free (code_challenge);
        return CH_reply_with_oauth_error (
          hc->connection,
          MHD_HTTP_UNAUTHORIZED,
          "invalid_grant",
          TALER_EC_GENERIC_PARAMETER_MISSING,
          "code_verifier is missing");
      }

      switch (code_challenge_method_enum)
      {
      case CHALLENGER_CM_S256:
        {
          gcry_md_hd_t hd;
          unsigned char hash[32];
          char *encoded_hash = NULL;
          size_t encoded_len;
          const void *md;

          if (GPG_ERR_NO_ERROR !=
              gcry_md_open (&hd,
                            GCRY_MD_SHA256,
                            0))
          {
            GNUNET_break (0);
            json_decref (address);
            GNUNET_free (client_scope);
            GNUNET_free (client_secret);
            GNUNET_free (client_redirect_uri);
            GNUNET_free (client_state);
            GNUNET_free (code_challenge);
            return CH_reply_with_oauth_error (
              hc->connection,
              MHD_HTTP_INTERNAL_SERVER_ERROR,
              "server_error",
              TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
              "Failed to initialize SHA256 hash function");
          }
          gcry_md_write (hd,
                         bc->code_verifier,
                         strlen (bc->code_verifier));
          md = gcry_md_read (hd,
                             0);
          GNUNET_assert (NULL != md);
          memcpy (hash,
                  md,
                  sizeof (hash));
          gcry_md_close (hd);

          encoded_len
            = GNUNET_STRINGS_base64url_encode (hash,
                                               sizeof (hash),
                                               &encoded_hash);

          if ( (0 == encoded_len) ||
               (NULL == encoded_hash) )
          {
            GNUNET_break (0);
            json_decref (address);
            GNUNET_free (client_scope);
            GNUNET_free (client_secret);
            GNUNET_free (client_redirect_uri);
            GNUNET_free (client_state);
            GNUNET_free (code_challenge);
            GNUNET_free (encoded_hash);
            return CH_reply_with_oauth_error (
              hc->connection,
              MHD_HTTP_INTERNAL_SERVER_ERROR,
              "server_error",
              TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
              "Failed to encode hash to Base64 URL");
          }

          if (0 != strcmp (encoded_hash,
                           code_challenge))
          {
            GNUNET_break_op (0);
            json_decref (address);
            GNUNET_free (client_scope);
            GNUNET_free (client_secret);
            GNUNET_free (client_redirect_uri);
            GNUNET_free (client_state);
            GNUNET_free (code_challenge);
            GNUNET_free (encoded_hash);
            return CH_reply_with_oauth_error (
              hc->connection,
              MHD_HTTP_UNAUTHORIZED,
              "invalid_grant",
              TALER_EC_CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE,
              "code_verifier does not match code_challenge (SHA256)");
          }
          GNUNET_free (encoded_hash);
        }
        break;
      case CHALLENGER_CM_PLAIN:
        {
          if (0 != strcmp (bc->code_verifier,
                           code_challenge))
          {
            GNUNET_break_op (0);
            json_decref (address);
            GNUNET_free (client_scope);
            GNUNET_free (client_secret);
            GNUNET_free (client_redirect_uri);
            GNUNET_free (client_state);
            GNUNET_free (code_challenge);
            return CH_reply_with_oauth_error (
              hc->connection,
              MHD_HTTP_UNAUTHORIZED,
              "invalid_grant",
              TALER_EC_CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE,
              "code_verifier does not match code_challenge (PLAIN)");
          }
        }
        break;
      case CHALLENGER_CM_UNKNOWN:
      case CHALLENGER_CM_EMPTY:
        GNUNET_break (0);
        json_decref (address);
        GNUNET_free (client_scope);
        GNUNET_free (client_secret);
        GNUNET_free (client_redirect_uri);
        GNUNET_free (client_state);
        GNUNET_free (code_challenge);
        return CH_reply_with_oauth_error (
          hc->connection,
          MHD_HTTP_INTERNAL_SERVER_ERROR,
          "server_error",
          TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
          "Database has empty or unknown challenge mode but with code_challenge");
      }
    }

    if (NULL == address)
    {
      GNUNET_break_op (0);
      GNUNET_free (client_scope);
      GNUNET_free (client_secret);
      GNUNET_free (client_redirect_uri);
      GNUNET_free (client_state);
      GNUNET_free (code_challenge);
      return CH_reply_with_oauth_error (
        hc->connection,
        MHD_HTTP_CONFLICT,
        "invalid_request",
        TALER_EC_CHALLENGER_MISSING_ADDRESS,
        "code");
    }
    code = CH_compute_code (&bc->nonce,
                            client_secret,
                            client_scope,
                            address,
                            client_redirect_uri);
    json_decref (address);
    GNUNET_free (client_scope);
    GNUNET_free (client_secret);
    GNUNET_free (client_redirect_uri);
    GNUNET_free (client_state);
    GNUNET_free (code_challenge);
    if (0 != strcmp (code,
                     bc->code))
    {
      GNUNET_break_op (0);
      GNUNET_free (code);
      return CH_reply_with_oauth_error (
        hc->connection,
        MHD_HTTP_UNAUTHORIZED,
        "invalid_grant",
        TALER_EC_CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE,
        "code");
    }
    GNUNET_free (code);
  }

  {
    struct CHALLENGER_AccessTokenP token;
    enum GNUNET_DB_QueryStatus qs;
    /* FIXME: do not hard-code 1h? */
    struct GNUNET_TIME_Relative token_expiration
      = GNUNET_TIME_UNIT_HOURS;

    GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE,
                                &token,
                                sizeof (token));
    qs = CH_db->token_add_token (CH_db->cls,
                                 &bc->nonce,
                                 &token,
                                 token_expiration,
                                 CH_validation_expiration);
    switch (qs)
    {
    case GNUNET_DB_STATUS_HARD_ERROR:
      GNUNET_break (0);
      return TALER_MHD_reply_with_error (
        hc->connection,
        MHD_HTTP_INTERNAL_SERVER_ERROR,
        TALER_EC_GENERIC_DB_STORE_FAILED,
        "token_add_token");
    case GNUNET_DB_STATUS_SOFT_ERROR:
      GNUNET_break (0);
      return MHD_NO;
    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      GNUNET_break (0);
      return CH_reply_with_oauth_error (
        hc->connection,
        MHD_HTTP_UNAUTHORIZED,
        "invalid_grant",
        TALER_EC_CHALLENGER_GRANT_UNKNOWN,
        "token_add_token");
    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
      break;
    }

    return TALER_MHD_REPLY_JSON_PACK (
      hc->connection,
      MHD_HTTP_OK,
      GNUNET_JSON_pack_data_auto ("access_token",
                                  &token),
      GNUNET_JSON_pack_string ("token_type",
                               "Bearer"),
      GNUNET_JSON_pack_uint64 ("expires_in",
                               token_expiration.rel_value_us
                               / GNUNET_TIME_UNIT_SECONDS.rel_value_us));
  }
}
