Added STS profile persistence and policy parsing/enforcement (including load/save, upgrades, and expiry rescheduling) to the STS module.

Integrated STS capability handling and connection lifecycle hooks (ignore CAP DEL, trigger upgrades, reschedule on disconnect, new server fields).
Initialized and cleaned up STS state during startup/shutdown to persist policies across sessions.
This commit is contained in:
2026-01-25 17:20:53 -07:00
parent 4d6c77704c
commit a0f0c48bc5
6 changed files with 552 additions and 9 deletions

View File

@@ -43,6 +43,7 @@
#include "inbound.h"
#include "server.h"
#include "servlist.h"
#include "sts.h"
#include "text.h"
#include "ctcp.h"
#include "zoitechatc.h"
@@ -1722,6 +1723,25 @@ void
inbound_cap_del (server *serv, char *nick, char *extensions,
const message_tags_data *tags_data)
{
if (extensions)
{
char **tokens = g_strsplit (extensions, " ", 0);
int i;
for (i = 0; tokens[i]; i++)
{
if (!g_strcmp0 (tokens[i], "sts") ||
g_str_has_prefix (tokens[i], "sts="))
{
/* STS cannot be disabled via CAP DEL. */
g_strfreev (tokens);
return;
}
}
g_strfreev (tokens);
}
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPDEL, serv->server_session, nick, extensions,
NULL, NULL, 0, tags_data->timestamp);
@@ -1819,6 +1839,7 @@ inbound_cap_ls (server *serv, char *nick, char *extensions_str,
{
char buffer[500]; /* buffer for requesting capabilities and emitting the signal */
gboolean want_cap = FALSE; /* format the CAP REQ string based on previous capabilities being requested or not */
gboolean sts_upgrade_triggered = FALSE;
char **extensions;
int i;
@@ -1853,6 +1874,15 @@ inbound_cap_ls (server *serv, char *nick, char *extensions_str,
value++;
}
if (!g_strcmp0 (extension, "sts"))
{
if (value)
{
sts_upgrade_triggered |= sts_handle_capability (serv, value);
}
continue;
}
/* if the SASL password is set AND auth mode is set to SASL, request SASL auth */
if (!g_strcmp0 (extension, "sasl") &&
(((serv->loginmethod == LOGIN_SASL
@@ -1888,6 +1918,11 @@ inbound_cap_ls (server *serv, char *nick, char *extensions_str,
g_strfreev (extensions);
if (sts_upgrade_triggered)
{
return;
}
if (want_cap)
{
/* buffer + 9 = emit buffer without "CAP REQ :" */

View File

@@ -54,6 +54,7 @@
#include "proto-irc.h"
#include "servlist.h"
#include "server.h"
#include "sts.h"
#ifdef USE_OPENSSL
#include <openssl/ssl.h> /* SSL_() */
@@ -1034,6 +1035,8 @@ server_disconnect (session * sess, int sendquit, int err)
server_sendquit (sess);
}
sts_reschedule_on_disconnect (serv);
fe_server_event (serv, FE_SE_DISCONNECT, 0);
/* close all sockets & io tags */
@@ -1588,6 +1591,15 @@ server_connect (server *serv, char *hostname, int port, int no_login)
int pid, read_des[2];
session *sess = serv->server_session;
if (!hostname[0])
return;
safe_strcpy (serv->sts_host, hostname, sizeof (serv->sts_host));
if (!sts_apply_policy_for_connection (serv, hostname, &port))
{
return;
}
#ifdef USE_OPENSSL
if (!serv->ctx && serv->use_ssl)
{
@@ -1599,9 +1611,6 @@ server_connect (server *serv, char *hostname, int port, int no_login)
}
#endif
if (!hostname[0])
return;
if (port < 1 || port > 65535)
{
/* use default port for this server type */
@@ -1842,6 +1851,8 @@ server_set_defaults (server *serv)
serv->have_sasl = FALSE;
serv->have_except = FALSE;
serv->have_invite = FALSE;
serv->sts_duration_seen = FALSE;
serv->sts_upgrade_in_progress = FALSE;
}
char *

View File

@@ -17,9 +17,24 @@
*/
#include <glib.h>
#include <time.h>
#include <fcntl.h>
#ifdef WIN32
#include <io.h>
#else
#include <unistd.h>
#endif
#include "zoitechat.h"
#include "cfgfiles.h"
#include "util.h"
#include "text.h"
#include "sts.h"
static GHashTable *sts_profiles = NULL;
static gboolean sts_loaded = FALSE;
static gboolean
sts_parse_bool (const char *value)
{
@@ -34,13 +49,14 @@ sts_parse_bool (const char *value)
}
sts_profile *
sts_profile_new (const char *host, guint16 port, time_t expires_at, gboolean preload)
sts_profile_new (const char *host, guint16 port, time_t expires_at, guint64 duration, gboolean preload)
{
sts_profile *profile = g_new0 (sts_profile, 1);
profile->host = g_strdup (host);
profile->port = port;
profile->expires_at = expires_at;
profile->duration = duration;
profile->preload = preload;
return profile;
@@ -77,6 +93,11 @@ sts_profile_serialize (const sts_profile *profile)
g_string_append_printf (serialized, " %u %" G_GINT64_FORMAT,
profile->port, (gint64) profile->expires_at);
if (profile->duration > 0)
{
g_string_append_printf (serialized, " %" G_GUINT64_FORMAT, profile->duration);
}
if (profile->preload)
{
g_string_append (serialized, " 1");
@@ -92,7 +113,9 @@ sts_profile_deserialize (const char *serialized)
char *host = NULL;
guint16 port = 0;
gint64 expires_at = -1;
guint64 duration = 0;
gboolean preload = FALSE;
gboolean duration_seen = FALSE;
gchar **pairs = NULL;
int i = 0;
@@ -103,7 +126,7 @@ sts_profile_deserialize (const char *serialized)
pairs = g_strsplit_set (serialized, " \t", -1);
{
const char *fields[4] = {0};
const char *fields[5] = {0};
int field_count = 0;
for (i = 0; pairs[i]; i++)
@@ -113,7 +136,7 @@ sts_profile_deserialize (const char *serialized)
continue;
}
if (field_count < 4)
if (field_count < 5)
{
fields[field_count++] = pairs[i];
}
@@ -133,7 +156,21 @@ sts_profile_deserialize (const char *serialized)
if (field_count >= 4)
{
preload = sts_parse_bool (fields[3]);
if (field_count == 4 && sts_parse_bool (fields[3]))
{
preload = TRUE;
}
else
{
duration = g_ascii_strtoull (fields[3], NULL, 10);
duration_seen = TRUE;
}
}
if (field_count >= 5)
{
duration_seen = TRUE;
preload = sts_parse_bool (fields[4]);
}
}
}
@@ -145,8 +182,452 @@ sts_profile_deserialize (const char *serialized)
return NULL;
}
sts_profile *profile = sts_profile_new (host, port, (time_t) expires_at, preload);
if (!duration_seen && duration == 0 && expires_at > 0)
{
time_t now = time (NULL);
if (expires_at > now)
{
duration = (guint64) (expires_at - now);
}
}
sts_profile *profile = sts_profile_new (host, port, (time_t) expires_at, duration, preload);
g_free (host);
g_strfreev (pairs);
return profile;
}
static char *
sts_normalize_host (const char *host)
{
char *normalized;
gsize len;
if (!host || !*host)
{
return NULL;
}
normalized = g_ascii_strdown (host, -1);
g_strstrip (normalized);
len = strlen (normalized);
if (len > 2 && normalized[0] == '[' && normalized[len - 1] == ']')
{
char *trimmed = g_strndup (normalized + 1, len - 2);
g_free (normalized);
normalized = trimmed;
}
return normalized;
}
static void
sts_profiles_ensure (void)
{
if (!sts_profiles)
{
sts_profiles = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
(GDestroyNotify) sts_profile_free);
}
}
static void
sts_profile_store (sts_profile *profile)
{
char *normalized;
if (!profile || !profile->host)
{
sts_profile_free (profile);
return;
}
sts_profiles_ensure ();
normalized = sts_normalize_host (profile->host);
if (!normalized)
{
sts_profile_free (profile);
return;
}
g_hash_table_replace (sts_profiles, normalized, profile);
}
static void
sts_profile_remove (const char *host)
{
char *normalized;
if (!host)
{
return;
}
sts_profiles_ensure ();
normalized = sts_normalize_host (host);
if (!normalized)
{
return;
}
g_hash_table_remove (sts_profiles, normalized);
g_free (normalized);
}
static sts_profile *
sts_profile_lookup (const char *host, time_t now)
{
char *normalized;
sts_profile *profile = NULL;
sts_profiles_ensure ();
normalized = sts_normalize_host (host);
if (!normalized)
{
return NULL;
}
profile = g_hash_table_lookup (sts_profiles, normalized);
if (profile && profile->expires_at > 0 && profile->expires_at <= now)
{
g_hash_table_remove (sts_profiles, normalized);
profile = NULL;
}
g_free (normalized);
return profile;
}
static gboolean
sts_parse_value (const char *value, guint16 *port, guint64 *duration, gboolean *preload,
gboolean *has_port, gboolean *has_duration, gboolean *has_preload)
{
char **tokens;
gsize i;
if (!value || !*value)
{
return FALSE;
}
*has_port = FALSE;
*has_duration = FALSE;
*has_preload = FALSE;
tokens = g_strsplit (value, ",", -1);
for (i = 0; tokens[i]; i++)
{
char *token = g_strstrip (tokens[i]);
char *equals = strchr (token, '=');
char *key = token;
char *val = NULL;
if (!*token)
{
continue;
}
if (equals)
{
*equals = '\0';
val = equals + 1;
}
if (!g_ascii_strcasecmp (key, "port"))
{
gint64 port_value;
if (*has_port || !val)
{
continue;
}
port_value = g_ascii_strtoll (val, NULL, 10);
if (port_value > 0 && port_value <= G_MAXUINT16)
{
*port = (guint16) port_value;
*has_port = TRUE;
}
}
else if (!g_ascii_strcasecmp (key, "duration"))
{
guint64 duration_value;
if (*has_duration || !val)
{
continue;
}
duration_value = g_ascii_strtoull (val, NULL, 10);
*duration = duration_value;
*has_duration = TRUE;
}
else if (!g_ascii_strcasecmp (key, "preload"))
{
if (*has_preload)
{
continue;
}
*preload = TRUE;
*has_preload = TRUE;
}
}
g_strfreev (tokens);
return TRUE;
}
void
sts_init (void)
{
sts_profiles_ensure ();
if (sts_loaded)
{
return;
}
sts_loaded = TRUE;
{
int fh;
char buf[512];
fh = zoitechat_open_file ("sts.conf", O_RDONLY, 0, 0);
if (fh != -1)
{
while (waitline (fh, buf, sizeof buf, FALSE) != -1)
{
if (buf[0] == '#' || buf[0] == '\0')
{
continue;
}
sts_profile *profile = sts_profile_deserialize (buf);
if (!profile)
{
continue;
}
if (profile->expires_at <= time (NULL))
{
sts_profile_free (profile);
continue;
}
if (profile->duration == 0)
{
sts_profile_free (profile);
continue;
}
sts_profile_store (profile);
}
close (fh);
}
}
}
void
sts_save (void)
{
GHashTableIter iter;
gpointer key;
gpointer value;
int fh;
sts_profiles_ensure ();
fh = zoitechat_open_file ("sts.conf", O_TRUNC | O_WRONLY | O_CREAT, 0600, XOF_DOMODE);
if (fh == -1)
{
return;
}
g_hash_table_iter_init (&iter, sts_profiles);
while (g_hash_table_iter_next (&iter, &key, &value))
{
sts_profile *profile = value;
char *serialized;
if (!profile || profile->expires_at <= time (NULL) || profile->duration == 0)
{
continue;
}
serialized = sts_profile_serialize (profile);
if (serialized)
{
write (fh, serialized, strlen (serialized));
write (fh, "\n", 1);
g_free (serialized);
}
}
close (fh);
}
void
sts_cleanup (void)
{
if (!sts_profiles)
{
return;
}
sts_save ();
g_hash_table_destroy (sts_profiles);
sts_profiles = NULL;
sts_loaded = FALSE;
}
gboolean
sts_apply_policy_for_connection (struct server *serv, const char *hostname, int *port)
{
sts_profile *profile;
time_t now;
if (!hostname || !*hostname || !port)
{
return TRUE;
}
sts_init ();
sts_profiles_ensure ();
now = time (NULL);
profile = sts_profile_lookup (hostname, now);
if (!profile)
{
return TRUE;
}
if (profile->port == 0)
{
sts_profile_remove (hostname);
return TRUE;
}
#ifdef USE_OPENSSL
serv->use_ssl = TRUE;
if (profile->port > 0)
{
*port = profile->port;
}
return TRUE;
#else
PrintTextf (serv->server_session,
_("STS policy requires TLS for %s, but TLS is not available.\n"),
hostname);
return FALSE;
#endif
}
gboolean
sts_handle_capability (struct server *serv, const char *value)
{
guint16 port = 0;
guint64 duration = 0;
gboolean preload = FALSE;
gboolean has_port = FALSE;
gboolean has_duration = FALSE;
gboolean has_preload = FALSE;
const char *hostname;
if (!serv || !value)
{
return FALSE;
}
sts_init ();
if (!sts_parse_value (value, &port, &duration, &preload,
&has_port, &has_duration, &has_preload))
{
return FALSE;
}
hostname = serv->sts_host[0] ? serv->sts_host : serv->servername;
if (!hostname || !*hostname)
{
return FALSE;
}
if (!serv->use_ssl)
{
if (!has_port)
{
return FALSE;
}
#ifdef USE_OPENSSL
if (serv->sts_upgrade_in_progress)
{
return TRUE;
}
serv->sts_upgrade_in_progress = TRUE;
serv->use_ssl = TRUE;
{
char host_copy[128];
safe_strcpy (host_copy, hostname, sizeof (host_copy));
serv->disconnect (serv->server_session, FALSE, -1);
serv->connect (serv, host_copy, (int) port, serv->no_login);
}
#else
PrintTextf (serv->server_session,
_("STS upgrade requested for %s, but TLS is not available.\n"),
hostname);
#endif
return TRUE;
}
if (!has_duration)
{
return FALSE;
}
if (duration == 0)
{
sts_profile_remove (hostname);
serv->sts_duration_seen = FALSE;
return FALSE;
}
{
time_t now = time (NULL);
time_t expires_at = now + (time_t) duration;
guint16 effective_port = serv->port > 0 ? (guint16) serv->port : port;
sts_profile *profile;
if (effective_port == 0)
{
return FALSE;
}
profile = sts_profile_new (hostname, effective_port, expires_at, duration,
has_preload ? preload : FALSE);
sts_profile_store (profile);
serv->sts_duration_seen = TRUE;
}
return FALSE;
}
void
sts_reschedule_on_disconnect (struct server *serv)
{
sts_profile *profile;
time_t now;
if (!serv || !serv->sts_duration_seen)
{
return;
}
sts_init ();
now = time (NULL);
profile = sts_profile_lookup (serv->sts_host[0] ? serv->sts_host : serv->servername, now);
if (!profile || profile->duration == 0)
{
return;
}
profile->expires_at = now + (time_t) profile->duration;
}

View File

@@ -24,20 +24,30 @@
G_BEGIN_DECLS
struct server;
typedef struct sts_profile
{
char *host;
guint16 port;
time_t expires_at;
guint64 duration;
gboolean preload;
} sts_profile;
sts_profile *sts_profile_new (const char *host, guint16 port, time_t expires_at, gboolean preload);
sts_profile *sts_profile_new (const char *host, guint16 port, time_t expires_at, guint64 duration, gboolean preload);
void sts_profile_free (sts_profile *profile);
char *sts_profile_serialize (const sts_profile *profile);
sts_profile *sts_profile_deserialize (const char *serialized);
void sts_init (void);
void sts_save (void);
void sts_cleanup (void);
gboolean sts_apply_policy_for_connection (struct server *serv, const char *hostname, int *port);
gboolean sts_handle_capability (struct server *serv, const char *value);
void sts_reschedule_on_disconnect (struct server *serv);
G_END_DECLS
#endif

View File

@@ -49,6 +49,7 @@
#include "notify.h"
#include "server.h"
#include "servlist.h"
#include "sts.h"
#include "outbound.h"
#include "text.h"
#include "url.h"
@@ -1200,6 +1201,7 @@ xchat_init (void)
sound_load ();
notify_load ();
ignore_load ();
sts_init ();
g_snprintf (buf, sizeof (buf),
"NAME %s~%s~\n" "CMD query %%s\n\n"\
@@ -1352,6 +1354,7 @@ zoitechat_exit (void)
sound_save ();
notify_save ();
ignore_save ();
sts_cleanup ();
free_sessions ();
chanopt_save_all (TRUE);
servlist_cleanup ();

View File

@@ -514,6 +514,7 @@ typedef struct server
int joindelay_tag; /* waiting before we send JOIN */
char hostname[128]; /* real ip number */
char servername[128]; /* what the server says is its name */
char sts_host[128];
char password[1024];
char nick[NICKLEN];
char linebuf[8704]; /* RFC says 512 chars including \r\n, IRCv3 message tags add 8191, plus the NUL byte */
@@ -588,6 +589,8 @@ typedef struct server
unsigned int have_sasl:1; /* SASL capability */
unsigned int have_except:1; /* ban exemptions +e */
unsigned int have_invite:1; /* invite exemptions +I */
unsigned int sts_duration_seen:1;
unsigned int sts_upgrade_in_progress:1;
unsigned int have_cert:1; /* have loaded a cert */
unsigned int use_who:1; /* whether to use WHO command to get dcc_ip */
unsigned int sasl_mech; /* mechanism for sasl auth */