pissircd/src/json.c

616 lines
20 KiB
C

/************************************************************************
* UnralIRCd JSON functions, src/json.c
* (C) 2021-.. Bram Matthys (Syzop) and the UnrealIRCd Team
* License: GPLv2 or later
*/
#include "unrealircd.h"
/** @file
* @brief JSON functions - used for logging and RPC.
*/
/** Are we currently in the logging code? */
int log_json_filter = 0;
/** Calculate expansion of a JSON string thanks to double escaping.
* orig => JSON => IRC
* " => \" => \\"
* \ => \\ => \\\\
*/
int json_dump_string_length(const char *s)
{
int len = 0;
for (; *s; s++)
{
if (*s == '\\')
len += 4;
else if (*s == '"')
len += 3;
else
len++;
}
return len;
}
/** Convert a regular string value to a JSON string.
* In UnrealIRCd, this must be used instead of json_string()
* as we may use non-UTF8 sequences. Also, this takes care
* of using json_null() if the string was NULL, which is
* usually what we want as well.
* @param s Input string
* @returns a json string value or json null value.
*/
json_t *json_string_unreal(const char *s)
{
char buf1[512], buf2[512];
char *verified_s;
const char *stripped;
if (s == NULL)
return json_null();
if (log_json_filter)
{
stripped = StripControlCodesEx(s, buf1, sizeof(buf1), UNRL_STRIP_LOW_ASCII|UNRL_STRIP_KEEP_LF);
verified_s = unrl_utf8_make_valid(buf1, buf2, sizeof(buf2), 0);
} else {
verified_s = unrl_utf8_make_valid(s, buf2, sizeof(buf2), 0);
}
return json_string(verified_s);
}
const char *json_object_get_string(json_t *j, const char *name)
{
json_t *v = json_object_get(j, name);
return v ? json_string_value(v) : NULL;
}
/** Get integer value of a JSON object.
* @param j The JSON object that should contain a 'name' item
* @param name The item to search for
* @param default_value The value to return when the JSON object 'name' is not found
* or not an integer.
* @returns The integer value, or default_value if the object does not exist or is not an integer.
*/
int json_object_get_integer(json_t *j, const char *name, int default_value)
{
json_t *v = json_object_get(j, name);
if (!v || !json_is_integer(v))
return default_value;
return json_integer_value(v);
}
int json_object_get_boolean(json_t *j, const char *name, int default_value)
{
json_t *v = json_object_get(j, name);
if (!v)
return default_value;
if (json_is_true(v))
return 1;
return 0;
}
#define json_string __BAD___DO__NOT__USE__JSON__STRING__PLZ
const char *json_get_value(json_t *t)
{
static char buf[32];
if (json_is_string(t))
return json_string_value(t);
if (json_is_integer(t))
{
snprintf(buf, sizeof(buf), "%lld", (long long)json_integer_value(t));
return buf;
}
if (json_is_boolean(t))
{
if (json_is_true(t))
return "true";
return "false";
}
if (json_is_array(t))
return "<array>";
return NULL;
}
json_t *json_timestamp(time_t v)
{
const char *ts = timestamp_iso8601(v);
if (ts)
return json_string_unreal(ts);
return json_null();
}
const char *timestamp_iso8601_now(void)
{
struct timeval t;
struct tm *tm;
time_t sec;
static char buf[64];
gettimeofday(&t, NULL);
sec = t.tv_sec;
tm = gmtime(&sec);
snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
tm->tm_year + 1900,
tm->tm_mon + 1,
tm->tm_mday,
tm->tm_hour,
tm->tm_min,
tm->tm_sec,
(int)(t.tv_usec / 1000));
return buf;
}
const char *timestamp_iso8601(time_t v)
{
struct tm *tm;
static char buf[64];
if (v == 0)
return NULL;
tm = gmtime(&v);
if (tm == NULL)
return NULL;
snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
tm->tm_year + 1900,
tm->tm_mon + 1,
tm->tm_mday,
tm->tm_hour,
tm->tm_min,
tm->tm_sec,
0);
return buf;
}
void json_expand_client_security_groups(json_t *parent, Client *client)
{
SecurityGroup *s;
json_t *child = json_array();
json_object_set_new(parent, "security-groups", child);
/* We put known-users or unknown-users at the beginning.
* The latter is special and doesn't actually exist
* in the linked list, hence the special code here,
* and again later in the for loop to skip it.
*/
if (user_allowed_by_security_group_name(client, "known-users"))
json_array_append_new(child, json_string_unreal("known-users"));
else
json_array_append_new(child, json_string_unreal("unknown-users"));
for (s = securitygroups; s; s = s->next)
if (strcmp(s->name, "known-users") && user_allowed_by_security_group(client, s))
json_array_append_new(child, json_string_unreal(s->name));
}
/* detail=0: only name, id
* detail=1: only name, id, hostname, ip, details, geoip
* detail=2: everything, except 'channels'
* detail=3: everything, with 'channels' being a max 384 character string (meant for JSON logging only)
* detail=4: everything, with 'channels' object (full).
*/
void json_expand_client(json_t *j, const char *key, Client *client, int detail)
{
char buf[BUFSIZE+1];
json_t *child;
json_t *user = NULL;
time_t ts;
if (key)
{
child = json_object();
json_object_set_new(j, key, child);
} else {
child = j;
}
/* First the information that is available for ALL client types: */
json_object_set_new(child, "name", json_string_unreal(client->name));
json_object_set_new(child, "id", json_string_unreal(client->id));
if (detail == 0)
return;
/* hostname is available for all, it just depends a bit on whether it is DNS or IP */
if (client->user && *client->user->realhost)
json_object_set_new(child, "hostname", json_string_unreal(client->user->realhost));
else if (client->local && *client->local->sockhost)
json_object_set_new(child, "hostname", json_string_unreal(client->local->sockhost));
else
json_object_set_new(child, "hostname", json_string_unreal(GetIP(client)));
/* same for ip, is there for all (well, some services pseudo-users may not have one) */
json_object_set_new(child, "ip", json_string_unreal(client->ip));
/* client.details is always available: it is nick!user@host, nick@host, server@host
* server@ip, or just server.
*/
if (client->user)
{
if (IsUser(client) || !MyConnect(client))
{
/* Post-handshake, after register_user(), it is easy: */
snprintf(buf, sizeof(buf), "%s!%s@%s", client->name, client->user->username, client->user->realhost);
} else
{
/* In the handshake, more possibilities (ident could still be ongoing)
* and more speculative (a later class block may want to ignore ident,
* but we don't know that, so we assume that is not the case).
*/
const char *ident;
char temp[USERLEN+1];
if (IDENT_CHECK)
{
if (IsIdentSuccess(client))
{
/* ident succeeded means: use the identd and no ~ prefix */
ident = client->ident;
} else {
/* ident check failed means ~ prefix */
snprintf(temp, sizeof(temp), "~%s", client->user->username);
ident = temp;
}
} else {
/* no ident check means no ~ prefix */
ident = client->user->username;
}
snprintf(buf, sizeof(buf), "%s!%s@%s", client->name, ident, client->user->realhost);
}
json_object_set_new(child, "details", json_string_unreal(buf));
} else if (client->ip) {
if (*client->name)
snprintf(buf, sizeof(buf), "%s@%s", client->name, client->ip);
else
snprintf(buf, sizeof(buf), "[%s]", client->ip);
json_object_set_new(child, "details", json_string_unreal(buf));
} else {
json_object_set_new(child, "details", json_string_unreal(client->name));
}
if (detail < 2)
{
RunHook(HOOKTYPE_JSON_EXPAND_CLIENT, client, detail, child);
return;
}
if (client->local && client->local->listener)
json_object_set_new(child, "server_port", json_integer(client->local->listener->port));
if (client->local && client->local->port)
json_object_set_new(child, "client_port", json_integer(client->local->port));
if ((ts = get_creationtime(client)))
json_object_set_new(child, "connected_since", json_timestamp(ts));
if (client->local && client->local->idle_since)
json_object_set_new(child, "idle_since", json_timestamp(client->local->idle_since));
if (client->user)
{
char buf[512];
const char *str;
/* client.user */
user = json_object();
json_object_set_new(child, "user", user);
json_object_set_new(user, "username", json_string_unreal(client->user->username));
if (!BadPtr(client->info))
json_object_set_new(user, "realname", json_string_unreal(client->info));
if (has_user_mode(client, 'x') && client->user->virthost && strcmp(client->user->virthost, client->user->realhost))
json_object_set_new(user, "vhost", json_string_unreal(client->user->virthost));
if (*client->user->cloakedhost)
json_object_set_new(user, "cloakedhost", json_string_unreal(client->user->cloakedhost));
if (client->uplink)
json_object_set_new(user, "servername", json_string_unreal(client->uplink->name));
if (IsLoggedIn(client))
json_object_set_new(user, "account", json_string_unreal(client->user->account));
json_object_set_new(user, "reputation", json_integer(GetReputation(client)));
json_expand_client_security_groups(user, client);
/* user modes and snomasks */
get_usermode_string_r(client, buf, sizeof(buf));
json_object_set_new(user, "modes", json_string_unreal(buf+1));
if (client->user->snomask)
json_object_set_new(user, "snomasks", json_string_unreal(client->user->snomask));
/* if oper then we can possibly expand a bit more */
str = get_operlogin(client);
if (str)
json_object_set_new(user, "operlogin", json_string_unreal(str));
str = get_operclass(client);
if (str)
json_object_set_new(user, "operclass", json_string_unreal(str));
/* For detail>2 we will include the channels.
* Even if the user is on 0 channels we include "channels":[]
* so it is clear that the user is on 0 channels and it is
* not because of low detail level that channels are skipped.
*/
if (detail > 2)
{
Membership *m;
int cnt = 0;
int len = 0;
json_t *channels = json_array();
json_object_set_new(user, "channels", channels);
if (detail == 3)
{
/* Short format, mainly for JSON logging */
for (m = client->user->channel; m; m = m->next)
{
len += json_dump_string_length(m->channel->name);
if (len > 384)
{
/* Truncated */
json_array_append_new(channels, json_string_unreal("..."));
break;
}
json_array_append_new(channels, json_string_unreal(m->channel->name));
}
} else {
/* Long format for JSON-RPC */
for (m = client->user->channel; m; m = m->next)
{
json_t *e = json_object();
json_object_set_new(e, "name", json_string_unreal(m->channel->name));
if (*m->member_modes)
json_object_set_new(e, "level", json_string_unreal(m->member_modes));
json_array_append_new(channels, e);
}
}
}
RunHook(HOOKTYPE_JSON_EXPAND_CLIENT_USER, client, detail, child, user);
} else
if (IsMe(client))
{
json_t *server = json_object();
json_t *features;
/* client.server */
json_object_set_new(child, "server", server);
if (!BadPtr(client->info))
json_object_set_new(server, "info", json_string_unreal(client->info));
json_object_set_new(server, "num_users", json_integer(client->server->users));
json_object_set_new(server, "boot_time", json_timestamp(client->server->boottime));
/* client.server.features */
features = json_object();
json_object_set_new(server, "features", features);
if (!BadPtr(client->server->features.software))
{
char buf[256];
snprintf(buf, sizeof(buf), "UnrealIRCd-%s", buildid);
json_object_set_new(features, "software", json_string_unreal(buf));
}
json_object_set_new(features, "protocol", json_integer(UnrealProtocol));
if (!BadPtr(client->server->features.usermodes))
json_object_set_new(features, "usermodes", json_string_unreal(umodestring));
/* client.server.features.chanmodes (array) */
{
int i;
char buf[512];
json_t *chanmodes = json_array();
json_object_set_new(features, "chanmodes", chanmodes);
/* first one is special - wait.. is this still the case? lol. */
snprintf(buf, sizeof(buf), "%s%s", CHPAR1, EXPAR1);
json_array_append_new(chanmodes, json_string_unreal(buf));
for (i=1; i < 4; i++)
json_array_append_new(chanmodes, json_string_unreal(extchmstr[i]));
}
if (!BadPtr(client->server->features.nickchars))
json_object_set_new(features, "nick_character_sets", json_string_unreal(charsys_get_current_languages()));
RunHook(HOOKTYPE_JSON_EXPAND_CLIENT_SERVER, client, detail, child, server);
} else
if (IsServer(client) && client->server)
{
/* client.server */
/* Whenever a server is expanded, which is rare,
* we should probably expand as much as info as possible:
*/
json_t *server = json_object();
json_t *features;
/* client.server */
json_object_set_new(child, "server", server);
if (!BadPtr(client->info))
json_object_set_new(server, "info", json_string_unreal(client->info));
if (client->uplink)
json_object_set_new(server, "uplink", json_string_unreal(client->uplink->name));
json_object_set_new(server, "num_users", json_integer(client->server->users));
json_object_set_new(server, "boot_time", json_timestamp(client->server->boottime));
json_object_set_new(server, "synced", json_boolean(client->server->flags.synced));
json_object_set_new(server, "ulined", json_boolean(IsULine(client)));
/* client.server.features */
features = json_object();
json_object_set_new(server, "features", features);
if (!BadPtr(client->server->features.software))
json_object_set_new(features, "software", json_string_unreal(client->server->features.software));
json_object_set_new(features, "protocol", json_integer(client->server->features.protocol));
if (!BadPtr(client->server->features.usermodes))
json_object_set_new(features, "usermodes", json_string_unreal(client->server->features.usermodes));
if (!BadPtr(client->server->features.chanmodes[0]))
{
/* client.server.features.chanmodes (array) */
int i;
json_t *chanmodes = json_array();
json_object_set_new(features, "chanmodes", chanmodes);
for (i=0; i < 4; i++)
json_array_append_new(chanmodes, json_string_unreal(client->server->features.chanmodes[i]));
}
if (!BadPtr(client->server->features.nickchars))
json_object_set_new(features, "nick_character_sets", json_string_unreal(client->server->features.nickchars));
RunHook(HOOKTYPE_JSON_EXPAND_CLIENT_SERVER, client, detail, child, server);
}
RunHook(HOOKTYPE_JSON_EXPAND_CLIENT, client, detail, child);
}
void json_expand_channel_ban(json_t *child, const char *banlist_name, Ban *banlist)
{
Ban *ban;
json_t *list, *e;
list = json_array();
json_object_set_new(child, banlist_name, list);
for (ban = banlist; ban; ban = ban->next)
{
e = json_object();
json_array_append_new(list, e);
json_object_set_new(e, "name", json_string_unreal(ban->banstr));
json_object_set_new(e, "set_by", json_string_unreal(ban->who));
json_object_set_new(e, "set_at", json_timestamp(ban->when));
}
}
/* detail=1 adds bans, ban_exemptions and invite_exceptions
* detail=2 adds members
* detail=3+ makes the members more detailed
*/
void json_expand_channel(json_t *j, const char *key, Channel *channel, int detail)
{
char mode1[512], mode2[512], modes[512];
json_t *child;
if (key)
{
child = json_object();
json_object_set_new(j, key, child);
} else {
child = j;
}
json_object_set_new(child, "name", json_string_unreal(channel->name));
if (detail == 0)
return;
json_object_set_new(child, "creation_time", json_timestamp(channel->creationtime));
json_object_set_new(child, "num_users", json_integer(channel->users));
if (channel->topic)
{
json_object_set_new(child, "topic", json_string_unreal(channel->topic));
json_object_set_new(child, "topic_set_by", json_string_unreal(channel->topic_nick));
json_object_set_new(child, "topic_set_at", json_timestamp(channel->topic_time));
}
/* Add "mode" too */
channel_modes(NULL, mode1, mode2, sizeof(mode1), sizeof(mode2), channel, 0);
if (*mode2)
{
snprintf(modes, sizeof(modes), "%s %s", mode1+1, mode2);
json_object_set_new(child, "modes", json_string_unreal(modes));
} else {
json_object_set_new(child, "modes", json_string_unreal(mode1+1));
}
if (detail > 1)
{
json_expand_channel_ban(child, "bans", channel->banlist);
json_expand_channel_ban(child, "ban_exemptions", channel->exlist);
json_expand_channel_ban(child, "invite_exceptions", channel->invexlist);
}
if (detail >= 3)
{
Member *u;
json_t *list = json_array();
json_object_set_new(child, "members", list);
for (u = channel->members; u; u = u->next)
{
json_t *e = json_object();
if (*u->member_modes)
json_object_set_new(e, "level", json_string_unreal(u->member_modes));
json_expand_client(e, NULL, u->client, detail-3);
json_array_append_new(list, e);
}
}
// Possibly later: If detail is set to 1 then expand more...
RunHook(HOOKTYPE_JSON_EXPAND_CHANNEL, channel, detail, child);
}
void json_expand_tkl(json_t *root, const char *key, TKL *tkl, int detail)
{
char buf[BUFSIZE];
json_t *j;
if (key)
{
j = json_object();
json_object_set_new(root, key, j);
} else {
j = root;
}
json_object_set_new(j, "type", json_string_unreal(tkl_type_config_string(tkl))); // Eg 'kline'
json_object_set_new(j, "type_string", json_string_unreal(tkl_type_string(tkl))); // Eg 'Soft K-Line'
json_object_set_new(j, "set_by", json_string_unreal(tkl->set_by));
json_object_set_new(j, "set_at", json_timestamp(tkl->set_at));
json_object_set_new(j, "expire_at", json_timestamp(tkl->expire_at));
*buf = '\0';
short_date(tkl->set_at, buf);
strlcat(buf, " GMT", sizeof(buf));
json_object_set_new(j, "set_at_string", json_string_unreal(buf));
if (tkl->expire_at <= 0)
{
json_object_set_new(j, "expire_at_string", json_string_unreal("Never"));
json_object_set_new(j, "duration_string", json_string_unreal("permanent"));
} else {
*buf = '\0';
short_date(tkl->expire_at, buf);
strlcat(buf, " GMT", sizeof(buf));
json_object_set_new(j, "expire_at_string", json_string_unreal(buf));
json_object_set_new(j, "duration_string", json_string_unreal(pretty_time_val_r(buf, sizeof(buf), tkl->expire_at - tkl->set_at)));
}
json_object_set_new(j, "set_at_delta", json_integer(TStime() - tkl->set_at));
if (tkl->flags & TKL_FLAG_CONFIG)
json_object_set_new(j, "set_in_config", json_boolean(1));
if (TKLIsServerBan(tkl))
{
json_object_set_new(j, "name", json_string_unreal(tkl_uhost(tkl, buf, sizeof(buf), 0)));
json_object_set_new(j, "reason", json_string_unreal(tkl->ptr.serverban->reason));
} else
if (TKLIsNameBan(tkl))
{
json_object_set_new(j, "name", json_string_unreal(tkl->ptr.nameban->name));
json_object_set_new(j, "reason", json_string_unreal(tkl->ptr.nameban->reason));
} else
if (TKLIsBanException(tkl))
{
json_object_set_new(j, "name", json_string_unreal(tkl_uhost(tkl, buf, sizeof(buf), 0)));
json_object_set_new(j, "reason", json_string_unreal(tkl->ptr.banexception->reason));
json_object_set_new(j, "exception_types", json_string_unreal(tkl->ptr.banexception->bantypes));
} else
if (TKLIsSpamfilter(tkl))
{
if (tkl->ptr.spamfilter->match->str)
{
json_object_set_new(j, "name", json_string_unreal(tkl->ptr.spamfilter->match->str));
json_object_set_new(j, "match_type", json_string_unreal(unreal_match_method_valtostr(tkl->ptr.spamfilter->match->type)));
}
if (tkl->ptr.spamfilter->prettyrule)
json_object_set_new(j, "rule", json_string_unreal(tkl->ptr.spamfilter->prettyrule));
json_object_set_new(j, "ban_action", json_string_unreal(ban_actions_to_string(tkl->ptr.spamfilter->action)));
json_object_set_new(j, "ban_duration", json_integer(tkl->ptr.spamfilter->tkl_duration));
json_object_set_new(j, "ban_duration_string", json_string_unreal(pretty_time_val_r(buf, sizeof(buf), tkl->ptr.spamfilter->tkl_duration)));
json_object_set_new(j, "spamfilter_targets", json_string_unreal(spamfilter_target_inttostring(tkl->ptr.spamfilter->target)));
json_object_set_new(j, "reason", json_string_unreal(unreal_decodespace(tkl->ptr.spamfilter->tkl_reason)));
json_object_set_new(j, "hits", json_integer(tkl->ptr.spamfilter->hits));
json_object_set_new(j, "hits_except", json_integer(tkl->ptr.spamfilter->hits_except));
}
}