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

@@ -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;
}