/*
  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_solve.c
 * @brief functions to handle incoming /solve requests
 * @author Christian Grothoff
 */
#include "platform.h"
#include "challenger-httpd.h"
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_db_lib.h>
#include "challenger-httpd_common.h"
#include "challenger-httpd_solve.h"
#include <taler/taler_json_lib.h>
#include <taler/taler_templating_lib.h>
#include <taler/taler_signatures.h>
#include "challenger-database/get_validation.h"
#include "challenger-database/do_solve_challenge.h"


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

/**
 * Context for a /solve operation.
 */
struct SolveContext
{

  /**
   * Nonce of the operation.
   */
  struct CHALLENGER_ValidationNonceP nonce;

  /**
   * HTTP request context.
   */
  struct CH_HandlerContext *hc;

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

  /**
   * OAuth2 client redirection URI for error reporting.
   */
  char *client_redirect_uri;

  /**
   * 0-terminated PIN submitted to us.
   */
  char *pin;

  /**
   * OAuth2 state.
   */
  char *state;

  /**
   * Number of bytes in @a pin, excluding 0-terminator.
   */
  size_t pin_len;

  /**
   * How many address changes are still allowed?
   */
  uint32_t addr_left;

  /**
   * How many authentication attempts are still allowed?
   */
  uint32_t auth_attempts_left;

  /**
   * How many pin transmissions can still be requested?
   */
  uint32_t pin_transmissions_left;

  /**
   * Set to true if the upload is too big.
   */
  bool too_big;
};


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

  if (NULL != bc->pp)
  {
    GNUNET_break_op (MHD_YES ==
                     MHD_destroy_post_processor (bc->pp));
  }
  GNUNET_free (bc->pin);
  GNUNET_free (bc->state);
  GNUNET_free (bc->client_redirect_uri);
  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 SolveContext *`
 * @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 SolveContext *bc = cls;

  (void) filename;
  (void) content_type;
  (void) transfer_encoding;
  (void) off;
  if (bc->too_big)
    return MHD_YES; /* discard the rest, we already have our answer */
  if (0 != strcmp (key,
                   "pin"))
    return MHD_YES;
  if (MHD_POSTDATA_KIND != kind)
    return MHD_YES;
  /* Hard cap independent of Content-Length: TALER_MHD_check_content_length()
     does not reject requests that omit Content-Length (e.g. chunked uploads),
     so without this guard an attacker could stream an unbounded 'pin' value. */
  if (bc->pin_len + size > 1024)
  {
    bc->too_big = true;
    GNUNET_break_op (0);
    /* Returning MHD_NO here would put the post processor into an error
       state and make MHD tear the connection down without a response;
       keep going (discarding the data) so that the handler can reply
       with a proper 413. */
    return MHD_YES;
  }
  bc->pin = GNUNET_realloc (bc->pin,
                            bc->pin_len + size + 1);
  memcpy (bc->pin + bc->pin_len,
          data,
          size);
  bc->pin_len += size;
  bc->pin[bc->pin_len] = '\0';
  return MHD_YES;
}


enum MHD_Result
CH_handler_solve (struct CH_HandlerContext *hc,
                  const char *upload_data,
                  size_t *upload_data_size)
{
  struct SolveContext *bc = hc->ctx;

  if (NULL == bc)
  {
    /* first call, setup internals */
    bc = GNUNET_new (struct SolveContext);
    hc->cc = &cleanup_ctx;
    hc->ctx = bc;
    bc->hc = hc;
    TALER_MHD_check_content_length (hc->connection,
                                    1024);
    if (GNUNET_OK !=
        GNUNET_STRINGS_string_to_data (hc->path,
                                       strlen (hc->path),
                                       &bc->nonce,
                                       sizeof (bc->nonce)))
    {
      GNUNET_break_op (0);
      return TALER_MHD_reply_with_error (
        hc->connection,
        MHD_HTTP_BAD_REQUEST,
        TALER_EC_GENERIC_PARAMETER_MALFORMED,
        "nonce");
    }
    bc->pp = MHD_create_post_processor (hc->connection,
                                        1024,
                                        &post_iter,
                                        bc);
    if (NULL == bc->pp)
    {
      GNUNET_break_op (0);
      return TALER_MHD_reply_with_error (
        hc->connection,
        MHD_HTTP_UNSUPPORTED_MEDIA_TYPE,
        TALER_EC_GENERIC_PARAMETER_MALFORMED,
        MHD_HTTP_HEADER_CONTENT_TYPE);
    }
    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 (bc->too_big)
  {
    GNUNET_break_op (0);
    return TALER_MHD_reply_with_error (
      hc->connection,
      MHD_HTTP_CONTENT_TOO_LARGE,
      TALER_EC_GENERIC_UPLOAD_EXCEEDS_LIMIT,
      NULL);
  }
  if (NULL == bc->pin)
  {
    GNUNET_break_op (0);
    return TALER_MHD_reply_with_error (
      hc->connection,
      MHD_HTTP_BAD_REQUEST,
      TALER_EC_GENERIC_PARAMETER_MISSING,
      "pin");
  }
  {
    unsigned int pin;
    char dummy;
    enum GNUNET_DB_QueryStatus qs;
    bool solved;
    bool exhausted;
    bool no_challenge;

    if (1 != sscanf (bc->pin,
                     "%u%c",
                     &pin,
                     &dummy))
    {
      GNUNET_break_op (0);
      return TALER_MHD_reply_with_error (
        hc->connection,
        MHD_HTTP_BAD_REQUEST,
        TALER_EC_GENERIC_PARAMETER_MALFORMED,
        "pin");
    }

    for (unsigned int r = 0; r<MAX_RETRIES; r++)
    {
      qs = CHALLENGERDB_do_solve_challenge (CH_context,
                                            &bc->nonce,
                                            pin,
                                            &solved,
                                            &exhausted,
                                            &no_challenge,
                                            &bc->state,
                                            &bc->addr_left,
                                            &bc->auth_attempts_left,
                                            &bc->pin_transmissions_left,
                                            &bc->client_redirect_uri);
      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,
          "do_solve_challenge");
      case GNUNET_DB_STATUS_SOFT_ERROR:
        if (r < MAX_RETRIES - 1)
          continue;
        GNUNET_break (0);
        return TALER_MHD_reply_with_error (
          hc->connection,
          MHD_HTTP_INTERNAL_SERVER_ERROR,
          TALER_EC_GENERIC_DB_FETCH_FAILED,
          "do_solve_challenge");
      case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
        return TALER_MHD_reply_with_error (
          hc->connection,
          MHD_HTTP_NOT_FOUND,
          TALER_EC_CHALLENGER_GENERIC_VALIDATION_UNKNOWN,
          NULL);
      case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
        break;
      }
      break;
    }
    if (! solved)
    {
      enum MHD_Result ret;
      json_t *details;
      unsigned int http_status;
      enum TALER_ErrorCode ec;

      /* Only give up if *no* option remains: a /challenge retransmission
         resets auth_attempts_left to 3, so as long as the user may still
         request another PIN transmission they can still succeed. */
      if ( (0 == bc->addr_left) &&
           (0 == bc->auth_attempts_left) &&
           (0 == bc->pin_transmissions_left) )
      {
        /* Terminal: no address change, no retransmission and no guess
           remains.  Reported with the same body as every other /solve
           failure (all counters are zero) so that a client only ever has
           to parse one shape; the error code tells the cases apart. */
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Client exhausted all chances to satisfy challenge\n");
        http_status = MHD_HTTP_TOO_MANY_REQUESTS;
        ec = TALER_EC_CHALLENGER_TOO_MANY_ATTEMPTS;
      }
      /* Distinguish the three ways in which a /solve can fail.  Only the
         last one is actually about the PIN that was submitted; reporting
         the other two as "the PIN code provided is incorrect" misleads
         the user.  Each gets its own error code so that the user agent can
         tell "provide your address first" from "wait or ask for a new PIN"
         from "that PIN was wrong" without parsing the human-readable
         hint. */
      else if (no_challenge)
      {
        /* No PIN was ever transmitted for this validation, so there is
           nothing to check; the user must POST /challenge first. */
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "PIN submitted before any challenge was transmitted\n");
        http_status = MHD_HTTP_CONFLICT;
        ec = TALER_EC_CHALLENGER_NO_CHALLENGE_TRANSMITTED;
      }
      else if (exhausted)
      {
        /* The PIN was not even looked at: no attempts left for it.  The
           user may still request a retransmission (otherwise we would
           have taken the terminal branch above). */
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "No attempts left to check the PIN\n");
        http_status = MHD_HTTP_TOO_MANY_REQUESTS;
        ec = TALER_EC_CHALLENGER_NO_PIN_ATTEMPTS_LEFT;
      }
      else
      {
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Invalid PIN supplied\n");
        http_status = MHD_HTTP_FORBIDDEN;
        ec = TALER_EC_CHALLENGER_INVALID_PIN;
        /* The stored procedure only reports 'exhausted' on the request
           *after* the last attempt was consumed; tell the client already
           on the response that consumed it. */
        exhausted = (0 == bc->auth_attempts_left);
      }
      details = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("type",
                                 "pending"),
        TALER_JSON_pack_ec (ec),
        GNUNET_JSON_pack_uint64 ("addresses_left",
                                 bc->addr_left),
        GNUNET_JSON_pack_uint64 ("pin_transmissions_left",
                                 bc->pin_transmissions_left),
        GNUNET_JSON_pack_uint64 ("auth_attempts_left",
                                 bc->auth_attempts_left),
        GNUNET_JSON_pack_bool ("exhausted",
                               exhausted),
        GNUNET_JSON_pack_bool ("no_challenge",
                               no_challenge)
        );
      ret = TALER_MHD_reply_json (hc->connection,
                                  details,
                                  http_status);
      json_decref (details);
      return ret;
    }
  }

  {
    struct MHD_Response *response;
    char *url;
    unsigned int http_status;
    enum GNUNET_GenericReturnValue ret;

    ret = CH_build_full_redirect_url (&bc->nonce,
                                      hc->connection,
                                      &url);
    if (GNUNET_OK != ret)
      return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
    if (0 == CH_get_output_type (hc->connection))
    {
      {
        const char *ok = "Ok!";

        response = MHD_create_response_from_buffer (strlen (ok),
                                                    (void *) ok,
                                                    MHD_RESPMEM_PERSISTENT);
      }
      if (NULL == response)
      {
        GNUNET_break (0);
        GNUNET_free (url);
        return MHD_NO;
      }
      TALER_MHD_add_global_headers (response,
                                    false);
      GNUNET_break (MHD_YES ==
                    MHD_add_response_header (response,
                                             MHD_HTTP_HEADER_CONTENT_TYPE,
                                             "text/plain"));
      if (MHD_NO ==
          MHD_add_response_header (response,
                                   MHD_HTTP_HEADER_LOCATION,
                                   url))
      {
        GNUNET_break (0);
        MHD_destroy_response (response);
        GNUNET_free (url);
        return MHD_NO;
      }
      http_status = MHD_HTTP_FOUND;
      GNUNET_free (url);
    }
    else
    {
      json_t *args;

      args = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("type",
                                 "completed"),
        GNUNET_JSON_pack_string ("redirect_url",
                                 url)
        );
      GNUNET_free (url);
      response = TALER_MHD_make_json_steal (args);
      http_status = MHD_HTTP_OK;
    }

    {
      enum MHD_Result mret;

      mret = MHD_queue_response (hc->connection,
                                 http_status,
                                 response);
      MHD_destroy_response (response);
      return mret;
    }
  }
}
