/*
      This file is part of GNUnet
      Copyright (C) 2008--2013 GNUnet e.V.

      GNUnet 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 of the License,
      or (at your option) any later version.

      GNUnet 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 this program.  If not, see <http://www.gnu.org/licenses/>.

     SPDX-License-Identifier: AGPL3.0-or-later
 */


/**
 * @file testbed/gnunet-daemon-testbed-blacklist.c
 * @brief daemon to restrict incoming connections from other peers at the
 *          transport layer of a peer
 * @author Sree Harsha Totakura <sreeharsha@totakura.in>
 */

#include "platform.h"
#include "gnunet_util_lib.h"
#include "gnunet_transport_service.h"
#include "gnunet_transport_manipulation_service.h"
#include "gnunet_ats_service.h"
#include "gnunet_testing_lib.h"
#include <sqlite3.h>

/**
 * Logging shorthand
 */
#define LOG(type, ...)                           \
  GNUNET_log (type, __VA_ARGS__)

/**
 * Debug logging shorthand
 */
#define DEBUG(...)                              \
  LOG (GNUNET_ERROR_TYPE_DEBUG, __VA_ARGS__)

/**
 * Log an error message at log-level 'level' that indicates
 * a failure of the command 'cmd' on file 'filename'
 * with the message given by strerror(errno).
 */
#define LOG_SQLITE(db, msg, level, cmd)                                 \
  do {                                                                  \
    GNUNET_log_from (level, "sqlite", _ ( \
                       "`%s' failed at %s:%d with error: %s\n"), \
                     cmd, __FILE__, __LINE__, sqlite3_errmsg (db));  \
    if (msg != NULL)                                                    \
    GNUNET_asprintf (msg, _ ("`%s' failed at %s:%u with error: %s"), cmd, \
                     __FILE__, __LINE__, sqlite3_errmsg (db));     \
  } while (0)


/**
 * The map to store the peer identities to allow/deny
 */
static struct GNUNET_CONTAINER_MultiPeerMap *map;

/**
 * The database connection
 */
static struct sqlite3 *db;

/**
 * The blacklist handle we obtain from transport when we register ourselves for
 * access control
 */
static struct GNUNET_TRANSPORT_Blacklist *bh;

/**
 * The hostkeys file
 */
struct GNUNET_DISK_FileHandle *hostkeys_fd;

/**
 * The hostkeys map
 */
static struct GNUNET_DISK_MapHandle *hostkeys_map;

/**
 * The hostkeys data
 */
static void *hostkeys_data;

/**
 * Handle to the transport service.  This is used for setting link metrics
 */
static struct GNUNET_TRANSPORT_ManipulationHandle *transport;

/**
 * The number of hostkeys in the hostkeys array
 */
static unsigned int num_hostkeys;


/**
 * @ingroup hashmap
 * Iterator over hash map entries.
 *
 * @param cls closure
 * @param key current key code
 * @param value value in the hash map
 * @return #GNUNET_YES if we should continue to
 *         iterate,
 *         #GNUNET_NO if not.
 */
static int
iterator (void *cls, const struct GNUNET_PeerIdentity *key, void *value)
{
  GNUNET_assert (GNUNET_YES == GNUNET_CONTAINER_multipeermap_remove (map, key,
                                                                     value));
  return GNUNET_YES;
}


/**
 * Cleaup and destroy the map
 */
static void
cleanup_map ()
{
  if (NULL != map)
  {
    GNUNET_assert (GNUNET_SYSERR != GNUNET_CONTAINER_multipeermap_iterate (map,
                                                                           &
                                                                           iterator,
                                                                           NULL));
    GNUNET_CONTAINER_multipeermap_destroy (map);
    map = NULL;
  }
}


/**
 * Function that decides if a connection is acceptable or not.
 *
 * @param cls closure
 * @param pid peer to approve or disapproave
 * @return GNUNET_OK if the connection is allowed, GNUNET_SYSERR if not
 */
static int
check_access (void *cls, const struct GNUNET_PeerIdentity *pid)
{
  int contains;

  GNUNET_assert (NULL != map);
  contains = GNUNET_CONTAINER_multipeermap_contains (map, pid);
  if (GNUNET_YES == contains)
  {
    DEBUG ("Permitting `%s'\n", GNUNET_i2s (pid));
    return GNUNET_OK;
  }
  DEBUG ("Not permitting `%s'\n", GNUNET_i2s (pid));
  return GNUNET_SYSERR;
}


static int
get_identity (unsigned int offset,
              struct GNUNET_PeerIdentity *id)
{
  struct GNUNET_CRYPTO_EddsaPrivateKey private_key;

  if (offset >= num_hostkeys)
    return GNUNET_SYSERR;
  GNUNET_memcpy (&private_key,
                 hostkeys_data + (offset * GNUNET_TESTING_HOSTKEYFILESIZE),
                 GNUNET_TESTING_HOSTKEYFILESIZE);
  GNUNET_CRYPTO_eddsa_key_get_public (&private_key,
                                      &id->public_key);
  return GNUNET_OK;
}


/**
 * Whilelist entry
 */
struct WhiteListRow
{
  /**
   * Next ptr
   */
  struct WhiteListRow *next;

  /**
   * The offset where to find the hostkey for the peer
   */
  unsigned int id;

  /**
   * Latency to be assigned to the link
   */
  int latency;
};


/**
 * Function to load keys
 */
static int
load_keys (const struct GNUNET_CONFIGURATION_Handle *c)
{
  char *data_dir;
  char *idfile;
  uint64_t fsize;

  data_dir = NULL;
  idfile = NULL;
  fsize = 0;
  data_dir = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR);
  GNUNET_asprintf (&idfile, "%s/testing_hostkeys.ecc", data_dir);
  GNUNET_free (data_dir);
  data_dir = NULL;
  if (GNUNET_OK !=
      GNUNET_DISK_file_size (idfile, &fsize, GNUNET_YES, GNUNET_YES))
  {
    GNUNET_free (idfile);
    return GNUNET_SYSERR;
  }
  if (0 != (fsize % GNUNET_TESTING_HOSTKEYFILESIZE))
  {
    LOG (GNUNET_ERROR_TYPE_ERROR,
         _ ("Incorrect hostkey file format: %s\n"), idfile);
    GNUNET_free (idfile);
    return GNUNET_SYSERR;
  }
  hostkeys_fd = GNUNET_DISK_file_open (idfile, GNUNET_DISK_OPEN_READ,
                                       GNUNET_DISK_PERM_NONE);
  if (NULL == hostkeys_fd)
  {
    GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, "open", idfile);
    GNUNET_free (idfile);
    return GNUNET_SYSERR;
  }
  GNUNET_free (idfile);
  idfile = NULL;
  hostkeys_data = GNUNET_DISK_file_map (hostkeys_fd,
                                        &hostkeys_map,
                                        GNUNET_DISK_MAP_TYPE_READ,
                                        fsize);
  if (NULL == hostkeys_data)
  {
    GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, "mmap");
    return GNUNET_SYSERR;
  }
  num_hostkeys = fsize / GNUNET_TESTING_HOSTKEYFILESIZE;
  return GNUNET_OK;
}


/**
 * Function to unload keys
 */
static void
unload_keys ()
{
  if (NULL != hostkeys_map)
  {
    GNUNET_assert (NULL != hostkeys_data);
    GNUNET_DISK_file_unmap (hostkeys_map);
    hostkeys_map = NULL;
    hostkeys_data = NULL;
  }
  if (NULL != hostkeys_fd)
  {
    GNUNET_DISK_file_close (hostkeys_fd);
    hostkeys_fd = NULL;
  }
}


/**
 * Shutdown task to cleanup our resources and exit.
 *
 * @param cls NULL
 */
static void
do_shutdown (void *cls)
{
  if (NULL != transport)
  {
    GNUNET_TRANSPORT_manipulation_disconnect (transport);
    transport = NULL;
  }
  cleanup_map ();
  unload_keys ();
  if (NULL != bh)
    GNUNET_TRANSPORT_blacklist_cancel (bh);
}


/**
 * Function to read whitelist rows from the database
 *
 * @param db the database connection
 * @param pid the identity of this peer
 * @param wl_rows where to store the retrieved whitelist rows
 * @return GNUNET_SYSERR upon error OR the number of rows retrieved
 */
static int
db_read_whitelist (struct sqlite3 *db, int pid, struct WhiteListRow **wl_rows)
{
  static const char *query_wl =
    "SELECT oid, latency FROM whitelist WHERE (id == ?);";
  struct sqlite3_stmt *stmt_wl;
  struct WhiteListRow *lr;
  int nrows;
  int ret;

  if (SQLITE_OK != (ret = sqlite3_prepare_v2 (db, query_wl, -1, &stmt_wl,
                                              NULL)))
  {
    LOG_SQLITE (db, NULL, GNUNET_ERROR_TYPE_ERROR, "sqlite3_prepare_v2");
    return GNUNET_SYSERR;
  }
  if (SQLITE_OK != (ret = sqlite3_bind_int (stmt_wl, 1, pid)))
  {
    LOG_SQLITE (db, NULL, GNUNET_ERROR_TYPE_ERROR, "sqlite3_bind_int");
    sqlite3_finalize (stmt_wl);
    return GNUNET_SYSERR;
  }
  nrows = 0;
  do
  {
    ret = sqlite3_step (stmt_wl);
    if (SQLITE_ROW != ret)
      break;
    nrows++;
    lr = GNUNET_new (struct WhiteListRow);
    lr->id = sqlite3_column_int (stmt_wl, 0);
    lr->latency = sqlite3_column_int (stmt_wl, 1);
    lr->next = *wl_rows;
    *wl_rows = lr;
  }
  while (1);
  sqlite3_finalize (stmt_wl);
  return nrows;
}


/**
 * Main function that will be run.
 *
 * @param cls closure
 * @param args remaining command-line arguments
 * @param cfgfile name of the configuration file used (for saving, can be NULL!)
 * @param c configuration
 */
static void
run (void *cls, char *const *args, const char *cfgfile,
     const struct GNUNET_CONFIGURATION_Handle *c)
{
  char *dbfile;
  struct WhiteListRow *wl_head;
  struct WhiteListRow *wl_entry;
  struct GNUNET_PeerIdentity identity;
  struct GNUNET_ATS_Properties prop;
  struct GNUNET_TIME_Relative delay;
  unsigned long long pid;
  unsigned int nrows;
  int ret;

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_number (c, "TESTBED",
                                             "PEERID", &pid))
  {
    GNUNET_break (0);
    return;
  }
  if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_filename (c,
                                                            "TESTBED-UNDERLAY",
                                                            "DBFILE",
                                                            &dbfile))
  {
    GNUNET_break (0);
    return;
  }
  if (SQLITE_OK != (ret = sqlite3_open_v2 (dbfile, &db, SQLITE_OPEN_READONLY,
                                           NULL)))
  {
    if (NULL != db)
    {
      LOG_SQLITE (db, NULL, GNUNET_ERROR_TYPE_ERROR, "sqlite_open_v2");
      GNUNET_break (SQLITE_OK == sqlite3_close (db));
    }
    else
      LOG (GNUNET_ERROR_TYPE_ERROR, "Cannot open sqlite file %s\n", dbfile);
    GNUNET_free (dbfile);
    return;
  }
  DEBUG ("Opened database %s\n", dbfile);
  GNUNET_free (dbfile);
  dbfile = NULL;
  wl_head = NULL;
  if (GNUNET_OK != load_keys (c))
    goto close_db;

  transport = GNUNET_TRANSPORT_manipulation_connect (c);
  if (NULL == transport)
  {
    GNUNET_break (0);
    return;
  }
  /* read and process whitelist */
  nrows = 0;
  wl_head = NULL;
  nrows = db_read_whitelist (db, pid, &wl_head);
  if ((GNUNET_SYSERR == nrows) || (0 == nrows))
  {
    GNUNET_TRANSPORT_manipulation_disconnect (transport);
    goto close_db;
  }
  map = GNUNET_CONTAINER_multipeermap_create (nrows, GNUNET_NO);
  while (NULL != (wl_entry = wl_head))
  {
    wl_head = wl_entry->next;
    delay.rel_value_us = wl_entry->latency;
    memset (&prop, 0, sizeof(prop));
    GNUNET_assert (GNUNET_OK == get_identity (wl_entry->id, &identity));
    GNUNET_break (GNUNET_OK ==
                  GNUNET_CONTAINER_multipeermap_put (map, &identity, &identity,
                                                     GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_FAST));
    DEBUG ("Setting %u ms latency to peer `%s'\n",
           wl_entry->latency,
           GNUNET_i2s (&identity));
    GNUNET_TRANSPORT_manipulation_set (transport,
                                       &identity,
                                       &prop,
                                       delay,
                                       delay);
    GNUNET_free (wl_entry);
  }
  bh = GNUNET_TRANSPORT_blacklist (c, &check_access, NULL);
  GNUNET_SCHEDULER_add_shutdown (&do_shutdown, NULL);

close_db:
  GNUNET_break (SQLITE_OK == sqlite3_close (db));
}


/**
 * The main function.
 *
 * @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)
{
  static const struct GNUNET_GETOPT_CommandLineOption options[] = {
    GNUNET_GETOPT_OPTION_END
  };
  int ret;

  if (GNUNET_OK != GNUNET_STRINGS_get_utf8_args (argc, argv, &argc, &argv))
    return 2;
#ifdef SQLITE_CONFIG_MMAP_SIZE
  (void) sqlite3_config (SQLITE_CONFIG_MMAP_SIZE, 512000, 256000000);
#endif
  ret =
    (GNUNET_OK ==
     GNUNET_PROGRAM_run (argc, argv, "testbed-underlay",
                         _
                         (
                           "Daemon to restrict underlay network in testbed deployments"),
                         options, &run, NULL)) ? 0 : 1;
  GNUNET_free_nz ((void *) argv);
  return ret;
}
