/*
  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_challenge.c
 * @brief functions to handle incoming /challenge requests
 * @author Christian Grothoff
 */
#include "platform.h"
#include "challenger-httpd.h"
#include <regex.h>
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_pq_lib.h>
#include "challenger-httpd_challenge.h"
#include <taler/taler_json_lib.h>
#include <taler/taler_mhd_lib.h>
#include <taler/taler_templating_lib.h>
#include <taler/taler_signatures.h>
#include "challenger-database/do_challenge_address.h"
#include "challenger-database/get_validation_address.h"
#include "challenger-httpd_common.h"

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

/**
 * Set to 1 to dump addresses into the log.
 */
#define DEBUG 0

/**
 * Context for a /challenge operation.
 */
struct ChallengeContext
{

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

  /**
   * Kept in a DLL.
   */
  struct ChallengeContext *next;

  /**
   * Kept in a DLL.
   */
  struct ChallengeContext *prev;

  /**
   * Our handler context.
   */
  struct CH_HandlerContext *hc;

  /**
   * Handle to the helper process.
   */
  struct GNUNET_Process *child;

  /**
   * Handle to wait for @e child
   */
  struct GNUNET_ChildWaitHandle *cwh;

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

  /**
   * Where we store the collected address data.
   */
  json_t *address;

  /**
   * Last key during POST processing.
   */
  char *last_key;

  /**
   * Name of the first uploaded field that could not be represented
   * in JSON (because the key or the value was not valid UTF-8), or
   * NULL if all fields were fine.
   */
  char *bad_key;

  /**
   * Uploaded data during POST processing.
   */
  char *data;

  /**
   * Where to redirect the client on errors?
   */
  char *client_redirect_uri;

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

  /**
   * Buffer used by #TALER_MHD_parse_post_json(). Or NULL.
   */
  void *jbuffer;

  /**
   * When did we transmit last?
   */
  struct GNUNET_TIME_Absolute last_tx_time;

  /**
   * Exit code from helper.
   */
  unsigned long int exit_code;

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

  /**
   * Our tan.
   */
  uint32_t tan;

  /**
   * How many attempts does the user have left on the current PIN?
   */
  uint32_t pin_attempts_left;

  /**
   * How many times may a PIN still be transmitted for this validation?
   */
  uint32_t pin_transmissions_left;

  /**
   * How did the helper die?
   */
  enum GNUNET_OS_ProcessStatusType pst;

  /**
   * Connection status. #GNUNET_OK to continue
   * normally, #GNUNET_NO if an error was already
   * returned, #GNUNET_SYSERR if we failed to
   * return an error and should just return #MHD_NO.
   */
  enum GNUNET_GenericReturnValue status;

  /**
   * #GNUNET_YES if we are suspended in #bc_head,
   * #GNUNET_NO if operating normally,
   * #GNUNET_SYSERR if resumed by shutdown (end with #MHD_NO)
   */
  enum GNUNET_GenericReturnValue suspended;

  /**
   * True if the provided address was refused, usually because
   * the user tried too many different addresses already.
   */
  bool address_refused;

  /**
   * Should we retransmit the PIN?
   */
  bool retransmit;

  /**
   * Is the challenge already solved?
   */
  bool solved;

  /**
   * Did we do the DB interaction?
   */
  bool db_finished;

  /**
   * True if we handed a new PIN to the transmission helper and it still
   * needs to be confirmed in the database once the helper succeeded.
   */
  bool pin_pending;

  /**
   * Is the upload in JSON?
   */
  bool is_json;

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


/**
 * Head of suspended challenger contexts.
 */
struct ChallengeContext *bc_head;

/**
 * Tail of suspended challenger contexts.
 */
struct ChallengeContext *bc_tail;


void
CH_wakeup_challenge_on_shutdown ()
{
  struct ChallengeContext *bc;

  while (NULL != (bc = bc_head))
  {
    GNUNET_CONTAINER_DLL_remove (bc_head,
                                 bc_tail,
                                 bc);
    MHD_resume_connection (bc->hc->connection);
    bc->suspended = GNUNET_SYSERR;
  }
}


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

  if (NULL != bc->pp)
  {
    GNUNET_break_op (MHD_YES ==
                     MHD_destroy_post_processor (bc->pp));
  }
  if (NULL != bc->cwh)
  {
    GNUNET_wait_child_cancel (bc->cwh);
    bc->cwh = NULL;
  }
  if (NULL != bc->child)
  {
    GNUNET_break (GNUNET_OK ==
                  GNUNET_process_kill (bc->child,
                                       SIGKILL));
    GNUNET_break (GNUNET_OK ==
                  GNUNET_process_wait (bc->child,
                                       true,
                                       NULL,
                                       NULL));
    GNUNET_process_destroy (bc->child);
    bc->child = NULL;
  }
  TALER_MHD_parse_post_cleanup_callback (bc->jbuffer);
  json_decref (bc->address);
  GNUNET_free (bc->data);
  GNUNET_free (bc->state);
  GNUNET_free (bc->last_key);
  GNUNET_free (bc->bad_key);
  GNUNET_free (bc->client_redirect_uri);
  GNUNET_free (bc);
}


/**
 * Generate error reply.
 *
 * @param bc our context
 * @param http_status HTTP status to return
 * @param ec error code to return
 * @param hint human-readable hint to give
 */
static enum MHD_Result
reply_error (struct ChallengeContext *bc,
             unsigned int http_status,
             enum TALER_ErrorCode ec,
             const char *hint)
{
  struct CH_HandlerContext *hc = bc->hc;

  return TALER_MHD_reply_with_error (
    hc->connection,
    http_status,
    ec,
    hint);
}


/**
 * Function called when our PIN transmission helper has terminated.
 *
 * @param cls our `struct ChallengeContext *`
 * @param type type of the process
 * @param exit_code status code of the process
 */
static void
child_done_cb (void *cls,
               enum GNUNET_OS_ProcessStatusType type,
               long unsigned int exit_code)
{
  struct ChallengeContext *bc = cls;
  struct GNUNET_AsyncScopeSave old_scope;

  GNUNET_async_scope_enter (&bc->hc->async_scope_id,
                            &old_scope);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Child done with exit code %d/%llu\n",
              (int) type,
              (unsigned long long) exit_code);
  GNUNET_process_destroy (bc->child);
  bc->child = NULL;
  bc->cwh = NULL;
  bc->pst = type;
  bc->exit_code = exit_code;
  bc->suspended = GNUNET_NO;
  MHD_resume_connection (bc->hc->connection);
  GNUNET_CONTAINER_DLL_remove (bc_head,
                               bc_tail,
                               bc);
  CH_trigger_daemon ();
  GNUNET_async_scope_restore (&old_scope);
}


/**
 * Transmit the TAN to the given address.
 *
 * @param[in,out] bc context to submit TAN for
 */
static void
send_tan (struct ChallengeContext *bc)
{
  struct GNUNET_DISK_PipeHandle *p;
  struct GNUNET_DISK_FileHandle *pipe_stdin;
  void *msg;
  size_t msg_len;

  p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW);
  if (NULL == p)
  {
    enum MHD_Result mres;

    GNUNET_break (0);
    mres = TALER_MHD_reply_with_error (
      bc->hc->connection,
      MHD_HTTP_BAD_GATEWAY,
      TALER_EC_CHALLENGER_HELPER_EXEC_FAILED,
      "pipe");
    bc->status = (MHD_YES == mres)
      ? GNUNET_NO
      : GNUNET_SYSERR;
    return;
  }
  {
    char *address;

    address = json_dumps (bc->address,
                          JSON_COMPACT);
#if DEBUG
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Running auth command `%s' on address `%s'\n",
                CH_auth_command,
                address);
#endif
    bc->child = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR);
    GNUNET_assert (GNUNET_OK ==
                   GNUNET_process_set_options (
                     bc->child,
                     GNUNET_process_option_inherit_rpipe (p,
                                                          STDIN_FILENO)));
    if (GNUNET_OK !=
        GNUNET_process_run_command_va (bc->child,
                                       CH_auth_command,
                                       CH_auth_command,
                                       address,
                                       NULL))
    {
      GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
                           "exec");
      GNUNET_process_destroy (bc->child);
      bc->child = NULL;
    }
    free (address);
  }
  if (NULL == bc->child)
  {
    enum MHD_Result mres;

    GNUNET_break (0);
    GNUNET_break (GNUNET_OK ==
                  GNUNET_DISK_pipe_close (p));
    mres = TALER_MHD_reply_with_error (
      bc->hc->connection,
      MHD_HTTP_BAD_GATEWAY,
      TALER_EC_CHALLENGER_HELPER_EXEC_FAILED,
      "exec");
    bc->status = (MHD_YES == mres)
      ? GNUNET_NO
      : GNUNET_SYSERR;
    return;
  }
  pipe_stdin = GNUNET_DISK_pipe_detach_end (p,
                                            GNUNET_DISK_PIPE_END_WRITE);
  GNUNET_assert (NULL != pipe_stdin);
  GNUNET_break (GNUNET_OK ==
                GNUNET_DISK_pipe_close (p));
  if (0 != CH_message_template_len)
  {
    json_t *root;
    int mret;

    root = GNUNET_JSON_PACK (
      GNUNET_JSON_pack_object_incref ("address",
                                      bc->address),
      GNUNET_JSON_pack_int64 ("pin",
                              bc->tan));
    mret = TALER_TEMPLATING_fill2 (CH_message_template,
                                   CH_message_template_len,
                                   root,
                                   &msg,
                                   &msg_len);
    json_decref (root);
    if (0 != mret)
    {
      enum MHD_Result mres;

      GNUNET_break (0);
      mres = TALER_MHD_reply_with_error (
        bc->hc->connection,
        MHD_HTTP_INTERNAL_SERVER_ERROR,
        TALER_EC_GENERIC_FAILED_TO_EXPAND_TEMPLATE,
        NULL);
      GNUNET_DISK_file_close (pipe_stdin);
      bc->status = (MHD_YES == mres)
          ? GNUNET_NO
          : GNUNET_SYSERR;
      return;
    }
  }
  else
  {
    char *cmsg;

    GNUNET_asprintf (&cmsg,
                     "PIN: %u",
                     (unsigned int) bc->tan);
    msg_len = strlen (cmsg);
    msg = cmsg;
  }
  {
    const char *off = msg;
    size_t left = msg_len;

    while (0 != left)
    {
      ssize_t ret;

      ret = GNUNET_DISK_file_write (pipe_stdin,
                                    off,
                                    left);
      if (ret <= 0)
      {
        enum MHD_Result mres;

        GNUNET_break (0);
        mres = TALER_MHD_reply_with_error (
          bc->hc->connection,
          MHD_HTTP_BAD_GATEWAY,
          TALER_EC_CHALLENGER_HELPER_EXEC_FAILED,
          "write");
        GNUNET_DISK_file_close (pipe_stdin);
        GNUNET_free (msg);
        bc->status = (MHD_YES == mres)
          ? GNUNET_NO
          : GNUNET_SYSERR;
        return;
      }
      off += (size_t) ret;
      left -= (size_t) ret;
    }
    GNUNET_DISK_file_close (pipe_stdin);
  }
  GNUNET_free (msg);
  bc->cwh = GNUNET_wait_child (bc->child,
                               &child_done_cb,
                               bc);
  /* The PIN is now in the hands of the helper; if it terminates
     successfully, the main handler promotes it in the database. */
  bc->pin_pending = true;
  MHD_suspend_connection (bc->hc->connection);
  bc->suspended = GNUNET_YES;
  GNUNET_CONTAINER_DLL_insert (bc_head,
                               bc_tail,
                               bc);
}


/**
 * Store the value accumulated in @a bc->data under @a bc->last_key in
 * the address object of @a bc, and free both.  If the key or the value
 * is not valid UTF-8 (and thus not representable in JSON), the field is
 * dropped and its name is remembered in @a bc->bad_key so that the main
 * handler can fail the request with a 400.
 *
 * Both strings are raw bytes from the client, so failure here must
 * never be fatal to the process.
 *
 * @param[in,out] bc context to update
 */
static void
flush_field (struct ChallengeContext *bc)
{
  json_t *jkey;
  json_t *jval;

  /* json_string() returns NULL if the input is not valid UTF-8. */
  jkey = json_string (bc->last_key);
  jval = json_string (NULL != bc->data
                      ? bc->data
                      : "");
  if ( (NULL == jkey) ||
       (NULL == jval) )
  {
    GNUNET_break_op (0);
    if (NULL == bc->bad_key)
      bc->bad_key = GNUNET_strdup (
        (NULL != jkey)
        ? bc->last_key
        : "<field name is not valid UTF-8>");
  }
  else
  {
    GNUNET_assert (0 ==
                   json_object_set (bc->address,
                                    bc->last_key,
                                    jval));
  }
  json_decref (jkey);
  json_decref (jval);
  GNUNET_free (bc->last_key);
  GNUNET_free (bc->data);
  bc->data_len = 0;
}


/**
 * 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 ChallengeContext *`
 * @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 ChallengeContext *bc = cls;

  (void) filename;
  (void) content_type;
  (void) transfer_encoding;
  (void) off;
  if (MHD_POSTDATA_KIND != kind)
    return MHD_YES;
  if (bc->too_big)
    return MHD_YES; /* discard the rest, we already have our answer */
  if ( (NULL != bc->last_key) &&
       (0 != strcmp (key,
                     bc->last_key)) )
  {
    flush_field (bc);
  }
  if (NULL == bc->last_key)
  {
    bc->last_key = GNUNET_strdup (key);
  }
  /* 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 field value. */
  if (bc->data_len + size > 1024)
  {
    GNUNET_break_op (0);
    bc->too_big = true;
    /* 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->data = GNUNET_realloc (bc->data,
                             bc->data_len + size + 1);
  memcpy (bc->data + bc->data_len,
          data,
          size);
  bc->data_len += size;
  bc->data[bc->data_len] = '\0';
  return MHD_YES;
}


/**
 * Check if the given address satisfies our restrictions.
 *
 * @param address address data provided by the client
 * @param[out] ec set to the error code to report if the check failed;
 *        #TALER_EC_CHALLENGER_ADDRESS_RESTRICTION_VIOLATED if the value
 *        supplied by the user is at fault, and
 *        #TALER_EC_CHALLENGER_ADDRESS_RESTRICTION_MALFORMED if our own
 *        configuration is at fault
 * @return NULL on success, otherwise the key that failed
 */
static const char *
check_restrictions (const json_t *address,
                    enum TALER_ErrorCode *ec)
{
  const char *key;
  const json_t *val;

  *ec = TALER_EC_CHALLENGER_ADDRESS_RESTRICTION_VIOLATED;
  json_object_foreach ((json_t *) CH_restrictions, key, val)
  {
    const char *str = json_string_value (
      json_object_get (address,
                       key));
    const char *regex = json_string_value (
      json_object_get (
        val,
        "regex"));
    regex_t re;

    if (NULL == str)
      return key; /* missing is not allowed */
    if (NULL == regex)
    {
      /* regex must be specified */
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Regex missing for `%s'\n",
                  key);
      *ec = TALER_EC_CHALLENGER_ADDRESS_RESTRICTION_MALFORMED;
      return key;
    }
    if (0 != regcomp (&re,
                      regex,
                      REG_EXTENDED))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Invalid regex `%s' address restriction specified for `%s'\n",
                  regex,
                  key);
      /* Fail closed: a restriction that cannot be compiled must not be
         treated as "no restriction". */
      *ec = TALER_EC_CHALLENGER_ADDRESS_RESTRICTION_MALFORMED;
      return key;
    }
    {
      regmatch_t m;

      /* Anchor the match: the pattern must match the ENTIRE value, not just a
         substring, otherwise e.g. "[0-9]{4}" would accept any value that
         merely contains four digits. */
      if ( (0 != regexec (&re,
                          str,
                          1,
                          &m,
                          0)) ||
           (0 != m.rm_so) ||
           ('\0' != str[m.rm_eo]) )
      {
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Client input `%s' rejected as it does not match address restriction `%s' specified for `%s'\n",
                    str,
                    regex,
                    key);
        regfree (&re);
        return key;
      }
    }
    regfree (&re);
  }
  return NULL;
}


/**
 * Check if @a a1 and @a a2 are equal, ignoring the
 * "read_only" field.
 *
 * @param a1 addr to compare
 * @param a2 addr to compare
 * @return true if both are equal
 */
static bool
addr_equal (const json_t *a1,
            const json_t *a2)
{
  bool ret;
  json_t *j1 = json_deep_copy (a1);
  json_t *j2 = json_deep_copy (a2);

  GNUNET_assert (NULL != j1);
  GNUNET_assert (NULL != j2);
  json_object_del (j1,
                   "read_only");
  json_object_del (j2,
                   "read_only");
  ret = (1 == json_equal (j1, j2));
  json_decref (j1);
  json_decref (j2);
  return ret;
}


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

  if (NULL == bc)
  {
    /* first call, setup internals */
    bc = GNUNET_new (struct ChallengeContext);
    bc->status = GNUNET_OK;
    bc->hc = hc;
    hc->cc = &cleanup_ctx;
    hc->ctx = bc;
    bc->pst = GNUNET_OS_PROCESS_UNKNOWN;
    bc->tan
      = GNUNET_CRYPTO_random_u32 (100000000);
    if (GNUNET_OK !=
        GNUNET_STRINGS_string_to_data (hc->path,
                                       strlen (hc->path),
                                       &bc->nonce,
                                       sizeof (bc->nonce)))
    {
      GNUNET_break_op (0);
      /* The nonce is present but unusable; that is a malformed request,
         not a missing parameter and not an unknown validation.  /authorize
         and /solve answer the same way. */
      return TALER_MHD_reply_with_error (
        hc->connection,
        MHD_HTTP_BAD_REQUEST,
        TALER_EC_GENERIC_PARAMETER_MALFORMED,
        "nonce");
    }
    TALER_MHD_check_content_length (hc->connection,
                                    1024);
    {
      const char *ct;

      ct = MHD_lookup_connection_value (hc->connection,
                                        MHD_HEADER_KIND,
                                        MHD_HTTP_HEADER_CONTENT_TYPE);
      bc->is_json = ( (NULL != ct) &&
                      (0 == strcasecmp (ct,
                                        "application/json")) );
      if (! bc->is_json)
        bc->address = json_object ();
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Processing /challenge upload with %s encoding...\n",
                  ct);
    }
    if (! bc->is_json)
    {
      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);
      }
    }
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Awaiting /challenge upload (%u)...\n",
                (unsigned int) *upload_data_size);
    return MHD_YES;
  }
  GNUNET_assert (GNUNET_YES != bc->suspended);
  if (GNUNET_SYSERR == bc->suspended)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "/challenge ends in shutdown\n");
    return MHD_NO;
  }
  /* Handle case where helper process failed */
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Checking child process status: %d / %d (%s)\n",
              (int) bc->pst,
              (int) bc->exit_code,
              NULL == bc->child ? "dead" : "running");
  if ( (GNUNET_OS_PROCESS_UNKNOWN != bc->pst) &&
       (NULL == bc->child) &&
       ( (GNUNET_OS_PROCESS_EXITED != bc->pst) ||
         (0 != bc->exit_code) ) )
  {
    char es[32];

    GNUNET_break (0);
    GNUNET_snprintf (es,
                     sizeof (es),
                     "%u/%d",
                     (unsigned int) bc->exit_code,
                     bc->pst);
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Helper failed with status %d/%d\n",
                (int) bc->pst,
                (int) bc->exit_code);
    return TALER_MHD_reply_with_error (
      hc->connection,
      MHD_HTTP_BAD_GATEWAY,
      TALER_EC_CHALLENGER_HELPER_EXEC_FAILED,
      es);
  }
  /* handle upload */
  if (bc->is_json)
  {
    if (NULL == bc->address)
    {
      enum GNUNET_GenericReturnValue res;

      res = TALER_MHD_parse_post_json (hc->connection,
                                       &bc->jbuffer,
                                       upload_data,
                                       upload_data_size,
                                       &bc->address);
      if (GNUNET_SYSERR != res)
        return MHD_YES;
      GNUNET_break (0);
      return MHD_NO;
    }
    else
    {
      GNUNET_break (0 == *upload_data_size);
    }
  }
  else 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;
    GNUNET_break (0);
    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->last_key)
  {
    flush_field (bc);
  }
  if (NULL != bc->bad_key)
  {
    return reply_error (bc,
                        MHD_HTTP_BAD_REQUEST,
                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
                        bc->bad_key);
  }
  if (! json_is_object (bc->address))
  {
    /* The address is echoed back with GNUNET_JSON_pack_object_incref() and
       (if a message template is configured) expanded by send_tan(), both of
       which assert on anything that is not an object; refuse it here
       instead.  Only reachable through an "application/json" upload, as the
       form encodings always build an object. */
    GNUNET_break_op (0);
    return reply_error (bc,
                        MHD_HTTP_BAD_REQUEST,
                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
                        "address");
  }
#if DEBUG
  {
    char *address;

    address = json_dumps (bc->address,
                          JSON_COMPACT);
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Submitted address is `%s'\n",
                address);
    free (address);
  }
#endif
  {
    const char *bad_field;
    enum TALER_ErrorCode ec;

    bad_field = check_restrictions (bc->address,
                                    &ec);
    if (NULL != bad_field)
    {
      bool misconfigured
        = (TALER_EC_CHALLENGER_ADDRESS_RESTRICTION_MALFORMED == ec);

      /* A restriction we cannot evaluate is our fault, not the user's, and
         must not be reported as a bad request. */
      if (misconfigured)
        GNUNET_break (0);
      else
        GNUNET_break_op (0);
      return reply_error (bc,
                          misconfigured
                          ? MHD_HTTP_INTERNAL_SERVER_ERROR
                          : MHD_HTTP_BAD_REQUEST,
                          ec,
                          bad_field);
    }
  }
  if (! bc->db_finished)
  {
    for (unsigned int r = 0; r < MAX_RETRIES; r++)
    {
      enum GNUNET_DB_QueryStatus qs;
      json_t *old_address;
      json_t *ro;

      GNUNET_assert (NULL == bc->client_redirect_uri);
      qs = CHALLENGERDB_get_validation_address (CH_context,
                                                &bc->nonce,
                                                &old_address);
      switch (qs)
      {
      case GNUNET_DB_STATUS_HARD_ERROR:
        GNUNET_break (0);
        return reply_error (bc,
                            MHD_HTTP_INTERNAL_SERVER_ERROR,
                            TALER_EC_GENERIC_DB_FETCH_FAILED,
                            "get_validation_address");
      case GNUNET_DB_STATUS_SOFT_ERROR:
        if (r < MAX_RETRIES - 1)
          continue;
        GNUNET_break (0);
        return reply_error (bc,
                            MHD_HTTP_INTERNAL_SERVER_ERROR,
                            TALER_EC_GENERIC_DB_FETCH_FAILED,
                            "get_validation_address");
      case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
        GNUNET_break_op (0);
        return reply_error (bc,
                            MHD_HTTP_NOT_FOUND,
                            TALER_EC_CHALLENGER_GENERIC_VALIDATION_UNKNOWN,
                            NULL);
      case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
        break;
      }
      ro = json_object_get (old_address,
                            "read_only");
      if ( (NULL != ro) &&
           (json_boolean_value (ro)) &&
           (! addr_equal (old_address,
                          bc->address)) )
      {
        GNUNET_break_op (0);
        json_decref (old_address);
        return reply_error (bc,
                            MHD_HTTP_FORBIDDEN,
                            TALER_EC_CHALLENGER_CLIENT_FORBIDDEN_READ_ONLY,
                            NULL);
      }
      if (NULL != ro)
        GNUNET_assert (0 ==
                       json_object_set (bc->address,
                                        "read_only",
                                        ro));
      json_decref (old_address);

      qs = CHALLENGERDB_do_challenge_address (CH_context,
                                              &bc->nonce,
                                              bc->address,
                                              CH_pin_retransmission_frequency,
                                              &bc->tan,
                                              &bc->state,
                                              &bc->last_tx_time,
                                              &bc->pin_attempts_left,
                                              &bc->pin_transmissions_left,
                                              &bc->retransmit,
                                              &bc->client_redirect_uri,
                                              &bc->address_refused,
                                              &bc->solved);
      switch (qs)
      {
      case GNUNET_DB_STATUS_HARD_ERROR:
        GNUNET_break (0);
        return reply_error (bc,
                            MHD_HTTP_INTERNAL_SERVER_ERROR,
                            TALER_EC_GENERIC_DB_STORE_FAILED,
                            "do_challenge_address");
      case GNUNET_DB_STATUS_SOFT_ERROR:
        if (r < MAX_RETRIES - 1)
          continue;
        GNUNET_break (0);
        return reply_error (bc,
                            MHD_HTTP_INTERNAL_SERVER_ERROR,
                            TALER_EC_GENERIC_DB_STORE_FAILED,
                            "do_challenge_address");
      case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
        GNUNET_break_op (0);
        return reply_error (bc,
                            MHD_HTTP_NOT_FOUND,
                            TALER_EC_CHALLENGER_GENERIC_VALIDATION_UNKNOWN,
                            NULL);
      case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
        break;
      }
      break;
    }
    bc->db_finished = true;
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Database interaction done (challenge %s)\n",
                bc->solved ? "solved" : "unsolved");
    if (bc->solved)
    {
      enum GNUNET_GenericReturnValue ret;
      char *url;

      ret = CH_build_full_redirect_url (&bc->nonce,
                                        hc->connection,
                                        &url);
      if (GNUNET_OK != ret)
        return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
      ret = TALER_MHD_REPLY_JSON_PACK (
        hc->connection,
        MHD_HTTP_OK,
        GNUNET_JSON_pack_string ("type",
                                 "completed"),
        GNUNET_JSON_pack_string ("redirect_url",
                                 url)
        );
      GNUNET_free (url);
      return ret;
    }
    if (bc->address_refused)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Address changes exhausted address change limit for this process\n");
      return reply_error (bc,
                          MHD_HTTP_TOO_MANY_REQUESTS,
                          TALER_EC_CHALLENGER_TOO_MANY_ADDRESS_CHANGES,
                          "client exceeded authorization attempts limit (too many addresses attempted)");

    }
    if ( (! bc->retransmit) &&
         (0 == bc->pin_transmissions_left) )
    {
      /* Note: only the *transmission* budget being exhausted is terminal.
         If merely the cooldown blocks the transmission, we fall through and
         report "transmitted":false with the retransmission_time. */
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "PIN transmission limit exhausted for this validation\n");
      return reply_error (bc,
                          MHD_HTTP_TOO_MANY_REQUESTS,
                          TALER_EC_CHALLENGER_TOO_MANY_PIN_TRANSMISSIONS,
                          "client exceeded authorization attempts limit (too many PINs)");
    }

    if (bc->retransmit)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Transmitting PIN\n");
      /* (Re)transmit PIN/TAN */
      send_tan (bc);
      if (GNUNET_YES == bc->suspended)
      {
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Suspending request after PIN transmission\n");
        return MHD_YES;
      }
      /* Did we already try to generate a response? */
      if (GNUNET_OK != bc->status)
        return (GNUNET_NO == bc->status)
          ? MHD_YES
          : MHD_NO;
    }
  }

  if (bc->pin_pending)
  {
    /* The helper terminated successfully (checked above), so the PIN really
       went out and only now becomes the PIN we accept.  Had the helper
       failed, we returned 502 above and the previous PIN (if any) remains
       valid. */
    bc->pin_pending = false;
    for (unsigned int r = 0; r<MAX_RETRIES; r++)
    {
      enum GNUNET_DB_QueryStatus qs;

      qs = CHALLENGERDB_do_challenge_address_confirm_pin (
        CH_context,
        &bc->nonce,
        &bc->pin_attempts_left);
      switch (qs)
      {
      case GNUNET_DB_STATUS_HARD_ERROR:
        GNUNET_break (0);
        return reply_error (bc,
                            MHD_HTTP_INTERNAL_SERVER_ERROR,
                            TALER_EC_GENERIC_DB_STORE_FAILED,
                            "do_challenge_address_confirm_pin");
      case GNUNET_DB_STATUS_SOFT_ERROR:
        if (r < MAX_RETRIES - 1)
          continue;
        GNUNET_break (0);
        return reply_error (bc,
                            MHD_HTTP_INTERNAL_SERVER_ERROR,
                            TALER_EC_GENERIC_DB_STORE_FAILED,
                            "do_challenge_address_confirm_pin");
      case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
        /* Validation was solved or removed while we were transmitting;
           there is nothing to promote, but the PIN did go out. */
        GNUNET_break_op (0);
        break;
      case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
        break;
      }
      break;
    }
  }

  {
    json_t *args;
    struct MHD_Response *resp;
    unsigned int http_status;
    enum MHD_Result res;

    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Returning success from challenge with %u attempts left\n",
                (unsigned int) bc->pin_attempts_left);
    args = GNUNET_JSON_PACK (
      GNUNET_JSON_pack_uint64 ("attempts_left",
                               bc->pin_attempts_left),
      GNUNET_JSON_pack_string ("type",
                               "created"),
      GNUNET_JSON_pack_object_incref ("address",
                                      bc->address),
      GNUNET_JSON_pack_bool ("transmitted",
                             bc->retransmit),
      GNUNET_JSON_pack_timestamp ("retransmission_time",
                                  GNUNET_TIME_absolute_to_timestamp (
                                    GNUNET_TIME_absolute_add (
                                      bc->last_tx_time,
                                      CH_pin_retransmission_frequency)))
      );
    http_status = MHD_HTTP_OK;
    resp = TALER_MHD_make_json_steal (args);
    GNUNET_break (MHD_YES ==
                  MHD_add_response_header (resp,
                                           MHD_HTTP_HEADER_CACHE_CONTROL,
                                           "no-store,no-cache"));
    res = MHD_queue_response (hc->connection,
                              http_status,
                              resp);
    MHD_destroy_response (resp);
    return res;
  }
}
