From a0f0c48bc5253c732884367bc8fcb477433ac0c2 Mon Sep 17 00:00:00 2001 From: deepend Date: Sun, 25 Jan 2026 17:20:53 -0700 Subject: [PATCH] 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. --- src/common/inbound.c | 35 +++ src/common/server.c | 17 +- src/common/sts.c | 491 ++++++++++++++++++++++++++++++++++++++++- src/common/sts.h | 12 +- src/common/zoitechat.c | 3 + src/common/zoitechat.h | 3 + 6 files changed, 552 insertions(+), 9 deletions(-) diff --git a/src/common/inbound.c b/src/common/inbound.c index 3abb9d45..fc1fa3cc 100644 --- a/src/common/inbound.c +++ b/src/common/inbound.c @@ -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 :" */ diff --git a/src/common/server.c b/src/common/server.c index f916771b..aa5b8ff4 100644 --- a/src/common/server.c +++ b/src/common/server.c @@ -54,6 +54,7 @@ #include "proto-irc.h" #include "servlist.h" #include "server.h" +#include "sts.h" #ifdef USE_OPENSSL #include /* 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 * diff --git a/src/common/sts.c b/src/common/sts.c index d529945d..9d57a905 100644 --- a/src/common/sts.c +++ b/src/common/sts.c @@ -17,9 +17,24 @@ */ #include +#include +#include +#ifdef WIN32 +#include +#else +#include +#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; +} diff --git a/src/common/sts.h b/src/common/sts.h index 2e60bae3..50c00e65 100644 --- a/src/common/sts.h +++ b/src/common/sts.h @@ -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 diff --git a/src/common/zoitechat.c b/src/common/zoitechat.c index 255d1a23..d1b962b8 100644 --- a/src/common/zoitechat.c +++ b/src/common/zoitechat.c @@ -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 (); diff --git a/src/common/zoitechat.h b/src/common/zoitechat.h index 30727521..810394d6 100644 --- a/src/common/zoitechat.h +++ b/src/common/zoitechat.h @@ -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 */