/*
  This file is part of TALER
  (C) 2019--2025 Taler Systems SA

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

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

  You should have received a copy of the GNU General Public License along with
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file sync/sync-httpd.c
 * @brief HTTP serving layer intended to provide basic backup operations
 * @author Christian Grothoff
 */
#include "platform.h"
#include <gnunet/gnunet_util_lib.h>
#include "sync_util.h"
#include "sync-httpd2.h"
#include "sync_database_lib.h"
#include "sync-httpd2_backup.h"
#include "sync-httpd2_config.h"

/**
 * Backlog for listen operation on unix-domain sockets.
 */
#define UNIX_BACKLOG 500


/**
 * Should a "Connection: close" header be added to each HTTP response?
 */
static int SH_sync_connection_close;

/**
 * Upload limit to the service, in megabytes.
 */
unsigned long long int SH_upload_limit_mb;

/**
 * Annual fee for the backup account.
 */
struct TALER_Amount SH_annual_fee;

/**
 * Our Taler backend to process payments.
 */
char *SH_backend_url;

/**
 * Our fulfillment URL.
 */
char *SH_fulfillment_url;

/**
 * Our context for making HTTP requests.
 */
struct GNUNET_CURL_Context *SH_ctx;

/**
 * Reschedule context for #SH_ctx.
 */
static struct GNUNET_CURL_RescheduleContext *rc;

/**
 * Global return code
 */
static int global_ret;

/**
 * Connection handle to the our database
 */
struct SYNC_DatabasePlugin *db;

/**
 * Username and password to use for client authentication
 * (optional).
 */
static char *userpass;

/**
 * Type of the client's TLS certificate (optional).
 */
static char *certtype;

/**
 * File with the client's TLS certificate (optional).
 */
static char *certfile;

/**
 * File with the client's TLS private key (optional).
 */
static char *keyfile;

/**
 * This value goes in the Authorization:-header.
 */
static char *apikey;

/**
 * Passphrase to decrypt client's TLS private key file (optional).
 */
static char *keypass;

/**
 * Amount of insurance.
 */
struct TALER_Amount SH_insurance;


/**
 * Function to respond to GET requests on '/'.
 *
 * @param request the MHD request to handle
 * @param upload_size number of bytes uploaded
 * @return MHD action
 */
static const struct MHD_Action *
respond_root (struct MHD_Request *request,
              uint_fast64_t upload_size)
{
  const char *msg = "Hello, I'm sync. This HTTP server is not for humans.\n";
  struct MHD_Response *resp;

  GNUNET_break_op (0 == upload_size);
  resp = MHD_response_from_buffer_static (
    MHD_HTTP_STATUS_OK,
    strlen (msg),
    msg);
  GNUNET_break (MHD_SC_OK ==
                MHD_response_add_header (resp,
                                         MHD_HTTP_HEADER_CONTENT_TYPE,
                                         "text/plain"));
  return MHD_action_from_response (request,
                                   resp);
}


/**
 * Function to respond to GET requests on unknown URLs.
 *
 * @param request the MHD request to handle
 * @param upload_size number of bytes uploaded
 * @return MHD action
 */
static const struct MHD_Action *
respond_404 (struct MHD_Request *request,
             uint_fast64_t upload_size)
{
  const char *msg = "<html><title>404: not found</title></html>";
  struct MHD_Response *resp;

  GNUNET_break_op (0 == upload_size);
  resp = MHD_response_from_buffer_static (
    MHD_HTTP_STATUS_NOT_FOUND,
    strlen (msg),
    msg);
  GNUNET_break (MHD_SC_OK ==
                MHD_response_add_header (resp,
                                         MHD_HTTP_HEADER_CONTENT_TYPE,
                                         "text/html"));
  return MHD_action_from_response (request,
                                   resp);
}


/**
 * Function to respond to GET requests on '/agpl'.
 *
 * @param request the MHD request to handle
 * @param upload_size number of bytes uploaded
 * @return MHD action
 */
static const struct MHD_Action *
respond_agpl (struct MHD_Request *request,
              uint_fast64_t upload_size)
{
  GNUNET_break_op (0 == upload_size);
  return TALER_MHD2_reply_agpl (request,
                                "https://git.taler.net/sync.git/");
}


/**
 * A client has requested the given url using the given method
 * (#MHD_HTTP_METHOD_GET, #MHD_HTTP_METHOD_PUT,
 * #MHD_HTTP_METHOD_DELETE, #MHD_HTTP_METHOD_POST, etc).  The callback
 * must call MHD callbacks to provide content to give back to the
 * client.
 *
 * @param cls argument given together with the function
 *        pointer when the handler was registered with MHD
 * @param request request the request to handle
 * @param path the requested uri (without arguments after "?")
 * @param method the HTTP method used (#MHD_HTTP_METHOD_GET,
 *        #MHD_HTTP_METHOD_PUT, etc.)
 * @param upload_size the size of the message upload content payload,
 *                    #MHD_SIZE_UNKNOWN for chunked uploads (if the
 *                    final chunk has not been processed yet)
  * @return next action
 */
static const struct MHD_Action *
url_handler (void *cls,
             struct MHD_Request *request,
             const struct MHD_String *path,
             enum MHD_HTTP_Method method,
             uint_fast64_t upload_size)
{
  static struct SH_RequestHandler handlers[] = {
    /* Landing page, tell humans to go away. */
    {
      .url = "/",
      .method = MHD_HTTP_METHOD_GET,
      .handler =  &respond_root
    },
    {
      .url = "/agpl",
      .method = MHD_HTTP_METHOD_GET,
      .handler = &respond_agpl,
    },
    {
      .url = "/config",
      .method = MHD_HTTP_METHOD_GET,
      .handler = &SH_handler_config,
    },
    {
      .url = NULL
    }
  };
  struct SYNC_AccountPublicKeyP account_pub;

  (void) cls;
#if BUG
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Handling %s request for URL '%s'\n",
              MHD_http_method_to_string (method)->cstr,
              path->cstr);
#endif
  if (0 == strncmp (path->cstr,
                    "/backups/",
                    strlen ("/backups/")))
  {
    const char *ac = &path->cstr[strlen ("/backups/")];

    if (GNUNET_OK !=
        GNUNET_CRYPTO_eddsa_public_key_from_string (ac,
                                                    strlen (ac),
                                                    &account_pub.eddsa_pub))
    {
      GNUNET_break_op (0);
      return TALER_MHD2_reply_with_error (request,
                                          MHD_HTTP_STATUS_BAD_REQUEST,
                                          TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                          ac);
    }
    if (MHD_HTTP_METHOD_OPTIONS == method)
    {
      return TALER_MHD2_reply_cors_preflight (request);
    }
    if (MHD_HTTP_METHOD_GET == method)
    {
      return SH_backup_get (request,
                            &account_pub);
    }
    if (MHD_HTTP_METHOD_POST == method)
    {
      return SH_backup_post (request,
                             &account_pub,
                             upload_size);
    }
    // FIXME: return bad method!
  }
  for (unsigned int i = 0; NULL != handlers[i].url; i++)
  {
    struct SH_RequestHandler *rh = &handlers[i];

    if (0 == strcmp (path->cstr,
                     rh->url))
    {
      if (MHD_HTTP_METHOD_OPTIONS == method)
      {
        return TALER_MHD2_reply_cors_preflight (request);
      }
      if (rh->method == method)
      {
        return rh->handler (request,
                            upload_size);
      }
    }
  }
  return respond_404 (request,
                      upload_size);
}


/**
 * Shutdown task. Invoked when the application is being terminated.
 *
 * @param cls NULL
 */
static void
do_shutdown (void *cls)
{
  struct MHD_Daemon *mhd;

  (void) cls;
  SH_resume_all_bc ();
  if (NULL != SH_ctx)
  {
    GNUNET_CURL_fini (SH_ctx);
    SH_ctx = NULL;
  }
  if (NULL != rc)
  {
    GNUNET_CURL_gnunet_rc_destroy (rc);
    rc = NULL;
  }
  mhd = TALER_MHD2_daemon_stop ();
  if (NULL != mhd)
  {
    MHD_daemon_destroy (mhd);
    mhd = NULL;
  }
  if (NULL != db)
  {
    SYNC_DB_plugin_unload (db);
    db = NULL;
  }
}


/**
 * Kick MHD to run now, to be called after MHD_request_resume().
 * Basically, we need to explicitly resume MHD's event loop whenever
 * we made progress serving a request.  This function re-schedules
 * the task processing MHD's activities to run immediately.
 */
// FIXME: replace with direct call...
void
SH_trigger_daemon ()
{
  TALER_MHD2_daemon_trigger ();
}


/**
 * Kick GNUnet Curl scheduler to begin curl interactions.
 */
void
SH_trigger_curl ()
{
  GNUNET_CURL_gnunet_scheduler_reschedule (&rc);
}


/**
 * Main function that will be run by the scheduler.
 *
 * @param cls closure
 * @param args remaining command-line arguments
 * @param cfgfile name of the configuration file used (for saving, can be
 *        NULL!)
 * @param config configuration
 */
static void
run (void *cls,
     char *const *args,
     const char *cfgfile,
     const struct GNUNET_CONFIGURATION_Handle *config)
{
  int fh;
  enum TALER_MHD2_GlobalOptions go;
  uint16_t port;
  struct MHD_Daemon *mhd;


  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Starting sync-httpd\n");
  go = TALER_MHD2_GO_NONE;
  if (SH_sync_connection_close)
    go |= TALER_MHD2_GO_FORCE_CONNECTION_CLOSE;
  TALER_MHD2_setup (go);
  global_ret = EXIT_NOTCONFIGURED;
  GNUNET_SCHEDULER_add_shutdown (&do_shutdown,
                                 NULL);
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_number (config,
                                             "sync",
                                             "UPLOAD_LIMIT_MB",
                                             &SH_upload_limit_mb))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               "sync",
                               "UPLOAD_LIMIT_MB");
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (GNUNET_OK !=
      TALER_config_get_amount (config,
                               "sync",
                               "INSURANCE",
                               &SH_insurance))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               "sync",
                               "INSURANCE");
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (GNUNET_OK !=
      TALER_config_get_amount (config,
                               "sync",
                               "ANNUAL_FEE",
                               &SH_annual_fee))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               "sync",
                               "ANNUAL_FEE");
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (config,
                                             "sync",
                                             "PAYMENT_BACKEND_URL",
                                             &SH_backend_url))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               "sync",
                               "PAYMENT_BACKEND_URL");
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (config,
                                             "sync",
                                             "FULFILLMENT_URL",
                                             &SH_fulfillment_url))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               "sync",
                               "BASE_URL");
    GNUNET_SCHEDULER_shutdown ();
    return;
  }

  /* setup HTTP client event loop */
  SH_ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
                             &rc);
  rc = GNUNET_CURL_gnunet_rc_create (SH_ctx);
  if (NULL != userpass)
    GNUNET_CURL_set_userpass (SH_ctx,
                              userpass);
  if (NULL != keyfile)
    GNUNET_CURL_set_tlscert (SH_ctx,
                             certtype,
                             certfile,
                             keyfile,
                             keypass);
  if (GNUNET_OK ==
      GNUNET_CONFIGURATION_get_value_string (config,
                                             "sync",
                                             "API_KEY",
                                             &apikey))
  {
    char *auth_header;

    GNUNET_asprintf (&auth_header,
                     "%s: %s",
                     MHD_HTTP_HEADER_AUTHORIZATION,
                     apikey);
    if (GNUNET_OK !=
        GNUNET_CURL_append_header (SH_ctx,
                                   auth_header))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Failed to set %s header, trying without\n",
                  MHD_HTTP_HEADER_AUTHORIZATION);
    }
    GNUNET_free (auth_header);
  }

  if (NULL ==
      (db = SYNC_DB_plugin_load (config,
                                 false)))
  {
    global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (GNUNET_OK !=
      db->preflight (db->cls))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Database not setup. Did you run sync-dbinit?\n");
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  fh = TALER_MHD_bind (config,
                       "sync",
                       &port);
  if ( (0 == port) &&
       (-1 == fh) )
  {
    global_ret = EXIT_NO_RESTART;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  mhd = MHD_daemon_create (&url_handler,
                           NULL);
  if (NULL == mhd)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to launch HTTP service, exiting.\n");
    global_ret = EXIT_NO_RESTART;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  GNUNET_assert (MHD_SC_OK ==
                 MHD_DAEMON_SET_OPTIONS (
                   mhd,
                   MHD_D_OPTION_DEFAULT_TIMEOUT (10),
                   (-1 == fh)
                   ? MHD_D_OPTION_BIND_PORT (MHD_AF_AUTO,
                                             port)
                   : MHD_D_OPTION_LISTEN_SOCKET (fh)));
  global_ret = EXIT_SUCCESS;
  TALER_MHD2_daemon_start (mhd);
}


/**
 * The main function of the serve tool
 *
 * @param argc number of arguments from the command line
 * @param argv command line arguments
 * @return 0 ok, 1 on error
 */
int
main (int argc,
      char *const *argv)
{
  struct GNUNET_GETOPT_CommandLineOption options[] = {
    GNUNET_GETOPT_option_string ('A',
                                 "auth",
                                 "USERNAME:PASSWORD",
                                 "use the given USERNAME and PASSWORD for client authentication",
                                 &userpass),
    GNUNET_GETOPT_option_flag ('C',
                               "connection-close",
                               "force HTTP connections to be closed after each request",
                               &SH_sync_connection_close),
    GNUNET_GETOPT_option_string ('k',
                                 "key",
                                 "KEYFILE",
                                 "file with the private TLS key for TLS client authentication",
                                 &keyfile),
    GNUNET_GETOPT_option_string ('p',
                                 "pass",
                                 "KEYFILEPASSPHRASE",
                                 "passphrase needed to decrypt the TLS client private key file",
                                 &keypass),
    GNUNET_GETOPT_option_string ('t',
                                 "type",
                                 "CERTTYPE",
                                 "type of the TLS client certificate, defaults to PEM if not specified",
                                 &certtype),
    GNUNET_GETOPT_OPTION_END
  };
  enum GNUNET_GenericReturnValue ret;

  ret = GNUNET_PROGRAM_run (SYNC_project_data (),
                            argc, argv,
                            "sync-httpd",
                            "sync HTTP interface",
                            options,
                            &run, NULL);
  if (GNUNET_NO == ret)
    return EXIT_SUCCESS;
  if (GNUNET_SYSERR == ret)
    return EXIT_INVALIDARGUMENT;
  return global_ret;
}
