/*
  This file is part of Anastasis
  Copyright (C) 2019 Anastasis SARL

  Anastasis 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.

  Anastasis 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
  Anastasis; see the file COPYING.GPL.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file anastasis_authorization_plugin_file.c
 * @brief authorization plugin file based for testing
 * @author Dominik Meister
 */
#include "platform.h"
#include "anastasis_authorization_plugin.h"
#include <taler/taler_mhd_lib.h>
#include <gnunet/gnunet_db_lib.h>
#include "anastasis_database_lib.h"

/**
 * How many retries do we allow per code?
 */
#define INITIAL_RETRY_COUNTER 3


/**
 * Context of the plugin, shared by all challenges it serves.
 */
struct FileContext
{
  /**
   * Configuration we are using.
   */
  const struct ANASTASIS_AuthorizationContext *ac;

  /**
   * Directory challenge files are written to.  Always ends in '/'.
   */
  char *directory;
};


/**
 * Check that @a data names a file this plugin is allowed to write.
 *
 * The "address" of this method is a file name, and it arrives with the
 * truth, i.e. it is chosen by whoever uploaded the truth and must be
 * treated as hostile: without this check any client could make us write
 * to an arbitrary path.  Accepted are paths inside @a ctx->directory that
 * cannot climb back out of it again.
 *
 * @param ctx our plugin context
 * @param data the file name to check, not necessarily 0-terminated
 * @param data_length number of bytes in @a data
 * @return true if @a data may be written to
 */
static bool
filename_ok (const struct FileContext *ctx,
             const char *data,
             size_t data_length)
{
  size_t dlen = strlen (ctx->directory);
  const char *rest;
  bool ok;
  char *fn;

  if (0 == data_length)
    return false;
  /* An embedded 0 would make GNUNET_strndup() keep a different (shorter)
     name than the one checked here, so validate and start would disagree. */
  if (NULL != memchr (data,
                      '\0',
                      data_length))
    return false;
  if (data_length <= dlen)
    return false;
  if (0 != strncmp (data,
                    ctx->directory,
                    dlen))
    return false;
  if ('/' == data[data_length - 1])
    return false;
  fn = GNUNET_strndup (data,
                       data_length);
  /* ctx->directory ends in '/', so a leading ".." shows up as "/.." here */
  rest = fn + dlen - 1;
  {
    size_t rlen = strlen (rest);

    ok = ( (NULL == strstr (rest,
                            "/../")) &&
           ( (rlen < 3) ||
             (0 != strcmp (rest + rlen - 3,
                           "/..")) ) );
  }
  GNUNET_free (fn);
  return ok;
}


/**
 * Saves the state of a authorization process
 */
struct ANASTASIS_AUTHORIZATION_State
{
  /**
   * UUID of the challenge which is authorised
   */
  struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid;

  /**
   * Code which is sent to the user (here saved into a file)
   */
  uint64_t code;

  /**
   * holds the truth information
   */
  char *filename;

  /**
   * closure
   */
  void *cls;
};


/**
 * Validate @a data is a well-formed input into the challenge method,
 * i.e. @a data is a well-formed phone number for sending an SMS, or
 * a well-formed e-mail address for sending an e-mail. Not expected to
 * check that the phone number or e-mail account actually exists.
 *
 * To be possibly used before issuing a 402 payment required to the client.
 *
 * @param cls closure with a `const struct FileContext *`
 * @param connection HTTP client request (for queuing response)
 * @param truth_mime mime type of @e data
 * @param data input to validate (i.e. is it a valid phone number, etc.)
 * @param data_length number of bytes in @a data
 * @return #GNUNET_OK if @a data is valid,
 *         #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection
 *         #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection
 */
static enum GNUNET_GenericReturnValue
file_validate (void *cls,
               struct MHD_Connection *connection,
               const char *truth_mime,
               const char *data,
               size_t data_length)
{
  const struct FileContext *ctx = cls;

  if (NULL == data)
    return GNUNET_SYSERR;
  /* Screen exactly the bytes file_start() will use as the file name.  The
     old check ran over the Crockford base32 *encoding* of the truth, whose
     alphabet contains neither ' ' nor '/', so it could never reject
     anything, while the name actually opened went unchecked. */
  if (! filename_ok (ctx,
                     data,
                     data_length))
  {
    /* Invalid input is #GNUNET_NO with a reply queued; #GNUNET_SYSERR is
       reserved for "invalid, and we could not even answer". */
    if (MHD_NO ==
        TALER_MHD_reply_with_error (connection,
                                    MHD_HTTP_CONFLICT,
                                    TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                    "filename"))
      return GNUNET_SYSERR;
    return GNUNET_NO;
  }
  return GNUNET_OK;
}


/**
 * Begin issuing authentication challenge to user based on @a data.
 * I.e. start to send SMS or e-mail or launch video identification.
 *
 * @param cls closure with a `const struct FileContext *`
 * @param trigger function to call when we made progress
 * @param trigger_cls closure for @a trigger
 * @param truth_uuid Identifier of the challenge, to be (if possible) included in the
 *             interaction with the user
 * @param code secret code that the user has to provide back to satisfy the challenge in
 *             the main anastasis protocol
 * @param data input to validate (i.e. is it a valid phone number, etc.)
 * @param data_length number of bytes in @a data
 * @return state to track progress on the authorization operation, NULL on failure
 */
static struct ANASTASIS_AUTHORIZATION_State *
file_start (void *cls,
            GNUNET_SCHEDULER_TaskCallback trigger,
            void *trigger_cls,
            const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
            uint64_t code,
            const void *data,
            size_t data_length)
{
  struct ANASTASIS_AUTHORIZATION_State *as;
  enum GNUNET_DB_QueryStatus qs;
  const struct FileContext *ctx = cls;

  /* @e validate is optional as far as the plugin API is concerned, so the
     screen has to be repeated here: this is the function whose result is
     actually opened for writing. */
  if (! filename_ok (ctx,
                     data,
                     data_length))
  {
    GNUNET_break_op (0);
    return NULL;
  }
  /* If the user can show this challenge code, this
     plugin is already happy (no additional
     requirements), so mark this challenge as
     already satisfied from the start. */
  qs = ANASTASIS_DB_update_to_challenge_code_satisfied (
    truth_uuid,
    code);
  if (qs <= 0)
  {
    GNUNET_break (0);
    return NULL;
  }
  as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State);
  as->cls = cls;
  as->truth_uuid = *truth_uuid;
  as->code = code;
  as->filename = GNUNET_strndup (data,
                                 data_length);
  return as;
}


/**
 * Begin issuing authentication challenge to user based on @a data.
 * I.e. start to send SMS or e-mail or launch video identification.
 *
 * @param as authorization state
 * @param connection HTTP client request (for queuing response, such as redirection to video portal)
 * @return state of the request
 */
static enum ANASTASIS_AUTHORIZATION_ChallengeResult
file_challenge (struct ANASTASIS_AUTHORIZATION_State *as,
                struct MHD_Connection *connection)
{
  const char *mime;
  const char *lang;

  mime = MHD_lookup_connection_value (connection,
                                      MHD_HEADER_KIND,
                                      MHD_HTTP_HEADER_ACCEPT);
  if (NULL == mime)
    mime = "text/plain";
  lang = MHD_lookup_connection_value (connection,
                                      MHD_HEADER_KIND,
                                      MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
  if (NULL == lang)
    lang = "en";
  {
    FILE *f;
    int fd;

    /* The challenge code is a secret, and the default directory sits in a
       world-writable /tmp: refuse to follow a symlink someone else planted
       there, and do not let anyone else read the code back. */
    fd = open (as->filename,
               O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW,
               S_IRUSR | S_IWUSR);
    f = (-1 == fd)
        ? NULL
        : fdopen (fd, "w");
    if (NULL == f)
    {
      struct MHD_Response *resp;
      enum MHD_Result mres;

      if (-1 != fd)
        GNUNET_break (0 == close (fd));
      GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
                                "open",
                                as->filename);
      resp = TALER_MHD_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                                   "open");
      mres = MHD_queue_response (connection,
                                 MHD_HTTP_INTERNAL_SERVER_ERROR,
                                 resp);
      MHD_destroy_response (resp);
      if (MHD_YES != mres)
        return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED;
      return ANASTASIS_AUTHORIZATION_CRES_FAILED;
    }

    /* print challenge code to file */
    if (0 >= fprintf (f,
                      "%lu",
                      as->code))
    {
      struct MHD_Response *resp;
      enum MHD_Result mres;

      GNUNET_break (0 == fclose (f));
      resp = TALER_MHD_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                                   "write");
      mres = MHD_queue_response (connection,
                                 MHD_HTTP_INTERNAL_SERVER_ERROR,
                                 resp);
      MHD_destroy_response (resp);
      if (MHD_YES != mres)
        return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED;
      return ANASTASIS_AUTHORIZATION_CRES_FAILED;
    }
    GNUNET_break (0 == fclose (f));
  }

  /* Build HTTP response */
  {
    struct MHD_Response *resp;

    if (0.0 < TALER_pattern_matches (mime,
                                     "application/json"))
    {
      resp = TALER_MHD_MAKE_JSON_PACK (
        GNUNET_JSON_pack_string ("challenge_type",
                                 "FILE_WRITTEN"),
        GNUNET_JSON_pack_string ("filename",
                                 as->filename));
    }
    else
    {
      size_t response_size;
      char *response;

      response_size = GNUNET_asprintf (&response,
                                       _ ("Challenge written to file"));
      resp = MHD_create_response_from_buffer (response_size,
                                              response,
                                              MHD_RESPMEM_MUST_COPY);
      GNUNET_free (response);
      TALER_MHD_add_global_headers (resp,
                                    false);
      GNUNET_break (MHD_YES ==
                    MHD_add_response_header (resp,
                                             MHD_HTTP_HEADER_CONTENT_TYPE,
                                             "text/plain"));
    }

    {
      enum MHD_Result mres;

      mres = MHD_queue_response (connection,
                                 MHD_HTTP_OK,
                                 resp);
      MHD_destroy_response (resp);
      if (MHD_YES != mres)
        return ANASTASIS_AUTHORIZATION_CRES_SUCCESS_REPLY_FAILED;
      return ANASTASIS_AUTHORIZATION_CRES_SUCCESS;
    }
  }
}


/**
 * Free internal state associated with @a as.
 *
 * @param as state to clean up
 */
static void
file_cleanup (struct ANASTASIS_AUTHORIZATION_State *as)
{
  GNUNET_free (as->filename);
  GNUNET_free (as);
}


/**
 * Initialize File based authorization plugin
 *
 * @param cls a configuration instance
 * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin`
 */
void *
libanastasis_plugin_authorization_file_init (void *cls);

/* declaration to fix compiler warning */
void *
libanastasis_plugin_authorization_file_init (void *cls)
{
  const struct ANASTASIS_AuthorizationContext *ac = cls;
  struct ANASTASIS_AuthorizationPlugin *plugin;
  struct FileContext *ctx;
  char *dir;

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_filename (ac->cfg,
                                               "authorization-file",
                                               "DIRECTORY",
                                               &dir))
  {
    const char *tmpdir = getenv ("TMPDIR");

    if (NULL == tmpdir)
      tmpdir = "/tmp";
    GNUNET_asprintf (&dir,
                     "%s/anastasis-file-challenges",
                     tmpdir);
  }
  ctx = GNUNET_new (struct FileContext);
  ctx->ac = ac;
  /* The default lands in a shared /tmp under a predictable name, so a local
     user can pre-create it and read every challenge code that follows.  That
     is tolerable only because this plugin exists for testing; an operator who
     points DIRECTORY somewhere private gets a private directory. */
  /* filename_ok() relies on the trailing '/' to tell "inside the directory"
     from "a sibling whose name merely starts the same way". */
  GNUNET_asprintf (&ctx->directory,
                   "%s/",
                   dir);
  GNUNET_free (dir);
  if (GNUNET_OK !=
      GNUNET_DISK_directory_create (ctx->directory))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to create directory `%s' for file challenges\n",
                ctx->directory);
    GNUNET_free (ctx->directory);
    GNUNET_free (ctx);
    return NULL;
  }
  plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin);
  plugin->cls = ctx;
  plugin->retry_counter = INITIAL_RETRY_COUNTER;
  plugin->code_validity_period = GNUNET_TIME_UNIT_MINUTES;
  plugin->code_rotation_period = GNUNET_TIME_UNIT_MINUTES;
  plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_MINUTES;
  plugin->validate = &file_validate;
  plugin->start = &file_start;
  plugin->challenge = &file_challenge;
  plugin->cleanup = &file_cleanup;
  return plugin;
}


/**
 * Unload authorization plugin
 *
 * @param cls a `struct ANASTASIS_AuthorizationPlugin`
 * @return NULL (always)
 */
void *
libanastasis_plugin_authorization_file_done (void *cls);

/* declaration to fix compiler warning */
void *
libanastasis_plugin_authorization_file_done (void *cls)
{
  struct ANASTASIS_AuthorizationPlugin *plugin = cls;
  struct FileContext *ctx = plugin->cls;

  GNUNET_free (ctx->directory);
  GNUNET_free (ctx);
  GNUNET_free (plugin);
  return NULL;
}
