/*
This file is part of GNUnet.
Copyright (C) 2012, 2013, 2014, 2019, 2022 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 .
SPDX-License-Identifier: AGPL3.0-or-later
*/
/**
* @file gnunet-namestore-dbtool.c
* @brief command line tool to manipulate the database backends for the namestore
* @author Martin Schanzenbach
*
*/
#include "platform.h"
#include
#include
#define MAX_RECORDS_PER_NAME 50
/**
* Maximum length of a zonefile line
*/
#define MAX_ZONEFILE_LINE_LEN 4096
/**
* FIXME: Soft limit this?
*/
#define MAX_ZONEFILE_RECORD_DATA_LEN 2048
/**
* The record data under a single label. Reused.
* Hard limit.
*/
static struct GNUNET_GNSRECORD_Data rd[MAX_RECORDS_PER_NAME];
/**
* Current record $TTL to use
*/
static struct GNUNET_TIME_Relative ttl;
/**
* Current origin
*/
static char origin[GNUNET_DNSPARSER_MAX_NAME_LENGTH];
/**
* Number of records for currently parsed set
*/
static unsigned int rd_count = 0;
/**
* Return code
*/
static int ret = 0;
/**
* Name of the ego
*/
static char *ego_name = NULL;
/**
* Currently read line or NULL on EOF
*/
static char *res;
/**
* Statistics, how many published record sets
*/
static unsigned int published_sets = 0;
/**
* Statistics, how many records published in aggregate
*/
static unsigned int published_records = 0;
/**
* Handle to identity lookup.
*/
static struct GNUNET_IDENTITY_EgoLookup *el;
/**
* Private key for the our zone.
*/
static struct GNUNET_IDENTITY_PrivateKey zone_pkey;
/**
* Queue entry for the 'add' operation.
*/
static struct GNUNET_NAMESTORE_QueueEntry *ns_qe;
/**
* Handle to the namestore.
*/
static struct GNUNET_NAMESTORE_Handle *ns;
/**
* Origin create operations
*/
static struct GNUNET_IDENTITY_Operation *id_op;
/**
* Handle to IDENTITY
*/
static struct GNUNET_IDENTITY_Handle *id;
/**
* Current configurataion
*/
static const struct GNUNET_CONFIGURATION_Handle *cfg;
/**
* Scheduled parse task
*/
static struct GNUNET_SCHEDULER_Task *parse_task;
/**
* The current state of the parser
*/
static int state;
enum ZonefileImportState
{
/* Uninitialized */
ZS_READY,
/* The initial state */
ZS_ORIGIN_SET,
/* The $ORIGIN has changed */
ZS_ORIGIN_CHANGED,
/* The record name/label has changed */
ZS_NAME_CHANGED
};
/**
* Task run on shutdown. Cleans up everything.
*
* @param cls unused
*/
static void
do_shutdown (void *cls)
{
(void) cls;
if (NULL != ego_name)
GNUNET_free (ego_name);
if (NULL != el)
{
GNUNET_IDENTITY_ego_lookup_cancel (el);
el = NULL;
}
if (NULL != ns_qe)
GNUNET_NAMESTORE_cancel (ns_qe);
if (NULL != id_op)
GNUNET_IDENTITY_cancel (id_op);
if (NULL != ns)
GNUNET_NAMESTORE_disconnect (ns);
if (NULL != id)
GNUNET_IDENTITY_disconnect (id);
for (int i = 0; i < rd_count; i++)
{
void *rd_ptr = (void*) rd[i].data;
GNUNET_free (rd_ptr);
}
if (NULL != parse_task)
GNUNET_SCHEDULER_cancel (parse_task);
}
static void
tx_end (void *cls, enum GNUNET_ErrorCode ec)
{
ns_qe = NULL;
if (GNUNET_EC_NONE != ec)
{
fprintf (stderr,
_ ("Ego `%s' not known to identity service\n"),
ego_name);
GNUNET_SCHEDULER_shutdown ();
ret = -1;
}
GNUNET_SCHEDULER_shutdown ();
}
static void
parse (void *cls);
static char*
trim (char *line)
{
char *ltrimmed = line;
int ltrimmed_len;
int quoted = 0;
// Trim all whitespace to the left
while (*ltrimmed == ' ')
ltrimmed++;
ltrimmed_len = strlen (ltrimmed);
// Find the first occurence of an unqoted ';', which is our comment
for (int i = 0; i < ltrimmed_len; i++)
{
if (ltrimmed[i] == '"')
quoted = ! quoted;
if ((ltrimmed[i] != ';') || quoted)
continue;
ltrimmed[i] = '\0';
}
ltrimmed_len = strlen (ltrimmed);
// Remove trailing whitespace
for (int i = ltrimmed_len; i > 0; i--)
{
if (ltrimmed[i - 1] != ' ')
break;
ltrimmed[i - 1] = '\0';
}
ltrimmed_len = strlen (ltrimmed);
if (ltrimmed[ltrimmed_len - 1] == '\n')
ltrimmed[ltrimmed_len - 1] = ' ';
return ltrimmed;
}
static char*
next_token (char *token)
{
char *next = token;
while (*next == ' ')
next++;
return next;
}
static int
parse_ttl (char *token, struct GNUNET_TIME_Relative *ttl)
{
char *next;
unsigned int ttl_tmp;
next = strchr (token, ';');
if (NULL != next)
next[0] = '\0';
next = strchr (token, ' ');
if (NULL != next)
next[0] = '\0';
if (1 != sscanf (token, "%u", &ttl_tmp))
{
fprintf (stderr, "Unable to parse TTL `%s'\n", token);
return GNUNET_SYSERR;
}
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "TTL is: %u\n", ttl_tmp);
ttl->rel_value_us = ttl_tmp * 1000 * 1000;
return GNUNET_OK;
}
static int
parse_origin (char *token, char *origin)
{
char *next;
next = strchr (token, ';');
if (NULL != next)
next[0] = '\0';
next = strchr (token, ' ');
if (NULL != next)
next[0] = '\0';
strcpy (origin, token);
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Origin is: %s\n", origin);
return GNUNET_OK;
}
static void
origin_create_cb (void *cls, const struct GNUNET_IDENTITY_PrivateKey *pk,
enum GNUNET_ErrorCode ec)
{
id_op = NULL;
if (GNUNET_EC_NONE != ec)
{
fprintf (stderr, "Error: %s\n", GNUNET_ErrorCode_get_hint (ec));
ret = 1;
GNUNET_SCHEDULER_shutdown ();
return;
}
state = ZS_ORIGIN_SET;
zone_pkey = *pk;
parse_task = GNUNET_SCHEDULER_add_now (&parse, NULL);
}
static void
origin_lookup_cb (void *cls, struct GNUNET_IDENTITY_Ego *ego)
{
el = NULL;
if (NULL == ego)
{
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"$ORIGIN %s does not exist, creating...\n", ego_name);
id_op = GNUNET_IDENTITY_create (id, ego_name, NULL,
GNUNET_IDENTITY_TYPE_ECDSA, // FIXME make configurable
origin_create_cb,
NULL);
return;
}
state = ZS_ORIGIN_SET;
zone_pkey = *GNUNET_IDENTITY_ego_get_private_key (ego);
parse_task = GNUNET_SCHEDULER_add_now (&parse, NULL);
}
static void
add_continuation (void *cls, enum GNUNET_ErrorCode ec)
{
ns_qe = NULL;
if (GNUNET_EC_NONE != ec)
{
fprintf (stderr,
_ ("Failed to store records...\n"));
GNUNET_SCHEDULER_shutdown ();
ret = -1;
}
if (ZS_ORIGIN_CHANGED == state)
{
if (NULL != ego_name)
GNUNET_free (ego_name);
ego_name = GNUNET_strdup (origin);
if (ego_name[strlen (ego_name) - 1] == '.')
ego_name[strlen (ego_name) - 1] = '\0';
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Changing origin to %s\n", ego_name);
el = GNUNET_IDENTITY_ego_lookup (cfg, ego_name,
&origin_lookup_cb, NULL);
return;
}
parse_task = GNUNET_SCHEDULER_add_now (&parse, NULL);
}
/**
* Main function that will be run.
*
* TODO:
* - We must assume that names are not repeated later in the zonefile because
* our _store APIs are replacing. No sure if that is common in zonefiles.
* - We must only actually store a record set when the name to store changes or
* the end of the file is reached.
* that way we can group them and add (see above).
* - We need to hope our string formats are compatible, but seems ok.
*
* @param cls closure
* @param args remaining command-line arguments
* @param cfgfile name of the configuration file used (for saving, can be NULL!)
* @param cfg configuration
*/
static void
parse (void *cls)
{
char buf[MAX_ZONEFILE_LINE_LEN];
char payload[MAX_ZONEFILE_RECORD_DATA_LEN];
char *next;
char *token;
char *payload_pos;
static char lastname[GNUNET_DNSPARSER_MAX_LABEL_LENGTH];
char newname[GNUNET_DNSPARSER_MAX_LABEL_LENGTH];
void *data;
size_t data_size;
int ttl_line = 0;
int type;
int bracket_unclosed = 0;
int quoted = 0;
parse_task = NULL;
/* use filename provided as 1st argument (stdin by default) */
int ln = 0;
while ((res = fgets (buf, sizeof(buf), stdin))) /* read each line of input */
{
ln++;
ttl_line = 0;
token = trim (buf);
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Trimmed line (bracket %s): `%s'\n",
(bracket_unclosed > 0) ? "unclosed" : "closed",
token);
if ((0 == strlen (token)) ||
((1 == strlen (token)) && (' ' == *token)))
continue; // I guess we can safely ignore blank lines
if (bracket_unclosed == 0)
{
/* Payload is already parsed */
payload_pos = payload;
/* Find space */
next = strchr (token, ' ');
if (NULL == next)
{
fprintf (stderr, "Error at line %u: %s\n", ln, token);
ret = 1;
GNUNET_SCHEDULER_shutdown ();
return;
}
next[0] = '\0';
next++;
if (0 == (strcmp (token, "$ORIGIN")))
{
state = ZS_ORIGIN_CHANGED;
token = next_token (next);
}
else if (0 == (strcmp (token, "$TTL")))
{
ttl_line = 1;
token = next_token (next);
}
else
{
if (0 == strcmp (token, "IN")) // Inherit name from before
{
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Old name: %s\n", lastname);
strcpy (newname, lastname);
token[strlen (token)] = ' ';
}
else if (token[strlen (token) - 1] != '.') // no fqdn
{
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "New name: %s\n", token);
if (GNUNET_DNSPARSER_MAX_LABEL_LENGTH < strlen (token))
{
fprintf (stderr,
_ ("Name `%s' is too long\n"),
token);
ret = 1;
GNUNET_SCHEDULER_shutdown ();
return;
}
strcpy (newname, token);
token = next_token (next);
}
else if (0 == strcmp (token, origin))
{
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "New name: @\n");
strcpy (newname, "@");
token = next_token (next);
}
else
{
if (strlen (token) < strlen (origin))
{
fprintf (stderr, "Wrong origin: %s (expected %s)\n", token, origin);
break; // FIXME error?
}
if (0 != strcmp (token + (strlen (token) - strlen (origin)), origin))
{
fprintf (stderr, "Wrong origin: %s (expected %s)\n", token, origin);
break;
}
token[strlen (token) - strlen (origin) - 1] = '\0';
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "New name: %s\n", token);
if (GNUNET_DNSPARSER_MAX_LABEL_LENGTH < strlen (token))
{
fprintf (stderr,
_ ("Name `%s' is too long\n"),
token);
ret = 1;
GNUNET_SCHEDULER_shutdown ();
return;
}
strcpy (newname, token);
token = next_token (next);
}
if (0 != strcmp (newname, lastname) &&
(0 < rd_count))
{
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Name changed %s->%s, storing record set of %u elements\n",
lastname, newname,
rd_count);
state = ZS_NAME_CHANGED;
}
else {
strcpy (lastname, newname);
}
}
if (ttl_line)
{
if (GNUNET_SYSERR == parse_ttl (token, &ttl))
{
fprintf (stderr, _ ("Failed to parse $TTL\n"));
ret = 1;
GNUNET_SCHEDULER_shutdown ();
return;
}
continue;
}
if (ZS_ORIGIN_CHANGED == state)
{
if (GNUNET_SYSERR == parse_origin (token, origin))
{
fprintf (stderr, _ ("Failed to parse $ORIGIN from %s\n"), token);
ret = 1;
GNUNET_SCHEDULER_shutdown ();
return;
}
break;
}
if (ZS_READY == state)
{
fprintf (stderr,
_ (
"You must provide $ORIGIN in your zonefile or via arguments (--zone)!\n"));
ret = 1;
GNUNET_SCHEDULER_shutdown ();
return;
}
// This is a record, let's go
if (MAX_RECORDS_PER_NAME == rd_count)
{
fprintf (stderr,
_ ("Only %u records per unique name supported.\n"),
MAX_RECORDS_PER_NAME);
ret = 1;
GNUNET_SCHEDULER_shutdown ();
return;
}
rd[rd_count].flags = GNUNET_GNSRECORD_RF_RELATIVE_EXPIRATION;
rd[rd_count].expiration_time = ttl.rel_value_us;
next = strchr (token, ' ');
if (NULL == next)
{
fprintf (stderr, "Error, last token: %s\n", token);
ret = 1;
GNUNET_SCHEDULER_shutdown ();
break;
}
next[0] = '\0';
next++;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "class is: %s\n", token);
while (*next == ' ')
next++;
token = next;
next = strchr (token, ' ');
if (NULL == next)
{
fprintf (stderr, "Error\n");
break;
}
next[0] = '\0';
next++;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "type is: %s\n", token);
type = GNUNET_GNSRECORD_typename_to_number (token);
rd[rd_count].record_type = type;
while (*next == ' ')
next++;
token = next;
}
for (int i = 0; i < strlen (token); i++)
{
if (token[i] == '"')
quoted = ! quoted;
if ((token[i] == '(') && ! quoted)
bracket_unclosed++;
if ((token[i] == ')') && ! quoted)
bracket_unclosed--;
}
memcpy (payload_pos, token, strlen (token));
payload_pos += strlen (token);
if (bracket_unclosed > 0)
{
*payload_pos = ' ';
payload_pos++;
continue;
}
*payload_pos = '\0';
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "data is: %s\n\n", payload);
if (GNUNET_OK !=
GNUNET_GNSRECORD_string_to_value (type, payload,
&data,
&data_size))
{
fprintf (stderr,
_ ("Data `%s' invalid\n"),
payload);
ret = 1;
GNUNET_SCHEDULER_shutdown ();
return;
}
rd[rd_count].data = data;
rd[rd_count].data_size = data_size;
if (ZS_NAME_CHANGED == state)
break;
rd_count++;
}
if (rd_count > 0)
{
ns_qe = GNUNET_NAMESTORE_records_store (ns,
&zone_pkey,
lastname,
rd_count,
rd,
&add_continuation,
NULL);
published_sets++;
published_records += rd_count;
for (int i = 0; i < rd_count; i++)
{
data = (void*) rd[i].data;
GNUNET_free (data);
}
if (ZS_NAME_CHANGED == state)
{
rd[0] = rd[rd_count]; // recover last rd parsed.
rd_count = 1;
strcpy (lastname, newname);
state = ZS_ORIGIN_SET;
}
else
rd_count = 0;
return;
}
if (ZS_ORIGIN_CHANGED == state)
{
if (NULL != ego_name)
GNUNET_free (ego_name);
ego_name = GNUNET_strdup (origin);
if (ego_name[strlen (ego_name) - 1] == '.')
ego_name[strlen (ego_name) - 1] = '\0';
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Changing origin to %s\n", ego_name);
el = GNUNET_IDENTITY_ego_lookup (cfg, ego_name,
&origin_lookup_cb, NULL);
return;
}
printf ("Published %u records sets with total %u records\n",
published_sets, published_records);
ns_qe = GNUNET_NAMESTORE_transaction_commit (ns,
&tx_end,
NULL);
}
static void
tx_start (void *cls, enum GNUNET_ErrorCode ec)
{
ns_qe = NULL;
if (GNUNET_EC_NONE != ec)
{
fprintf (stderr,
_ ("Ego `%s' not known to identity service\n"),
ego_name);
GNUNET_SCHEDULER_shutdown ();
ret = -1;
return;
}
parse_task = GNUNET_SCHEDULER_add_now (&parse, NULL);
}
static void
identity_cb (void *cls, struct GNUNET_IDENTITY_Ego *ego)
{
el = NULL;
if (NULL == ego)
{
if (NULL != ego_name)
{
fprintf (stderr,
_ ("Ego `%s' not known to identity service\n"),
ego_name);
}
GNUNET_SCHEDULER_shutdown ();
ret = -1;
return;
}
zone_pkey = *GNUNET_IDENTITY_ego_get_private_key (ego);
sprintf (origin, "%s.", ego_name);
state = ZS_ORIGIN_SET;
ns_qe = GNUNET_NAMESTORE_transaction_begin (ns,
&tx_start,
NULL);
}
static void
run (void *cls,
char *const *args,
const char *cfgfile,
const struct GNUNET_CONFIGURATION_Handle *_cfg)
{
cfg = _cfg;
ns = GNUNET_NAMESTORE_connect (cfg);
GNUNET_SCHEDULER_add_shutdown (&do_shutdown, (void *) cfg);
if (NULL == ns)
{
fprintf (stderr,
_ ("Failed to connect to NAMESTORE\n"));
return;
}
id = GNUNET_IDENTITY_connect (cfg, NULL, NULL);
if (NULL == id)
{
fprintf (stderr,
_ ("Failed to connect to IDENTITY\n"));
return;
}
if (NULL != ego_name)
el = GNUNET_IDENTITY_ego_lookup (cfg, ego_name, &identity_cb, (void *) cfg);
else
parse_task = GNUNET_SCHEDULER_add_now (&parse, NULL);
state = ZS_READY;
}
/**
* The main function for gnunet-namestore-dbtool.
*
* @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 ('z',
"zone",
"EGO",
gettext_noop (
"name of the ego controlling the zone"),
&ego_name),
GNUNET_GETOPT_OPTION_END
};
int lret;
if (GNUNET_OK != GNUNET_STRINGS_get_utf8_args (argc, argv, &argc, &argv))
return 2;
GNUNET_log_setup ("gnunet-namestore-dbtool", "WARNING", NULL);
if (GNUNET_OK !=
(lret = GNUNET_PROGRAM_run (argc,
argv,
"gnunet-namestore-zonefile",
_ (
"GNUnet namestore database manipulation tool"),
options,
&run,
NULL)))
{
GNUNET_free_nz ((void *) argv);
return lret;
}
GNUNET_free_nz ((void *) argv);
return ret;
}