From 08fc106918ed717f1324bce9b06ff060ff2162dd Mon Sep 17 00:00:00 2001 From: MrIron Date: Mon, 16 Mar 2026 15:12:29 +0100 Subject: [PATCH 1/4] mod.cservice: Respect rfc1459 casemapping --- ChangeLog | 4 +++ doc/cservice.sql | 2 ++ doc/cservice.update.sql | 9 ++++++ libgnuworld/misc.cc | 16 +++++++++- libgnuworld/misc.h | 8 +++++ mod.cservice/constants.h | 2 +- mod.cservice/cservice.cc | 4 +-- mod.cservice/cservice.h | 6 ++-- mod.cservice/sqlChannel.cc | 63 ++++++++++++++++++++------------------ mod.cservice/sqlChannel.h | 8 ++++- 10 files changed, 84 insertions(+), 38 deletions(-) diff --git a/ChangeLog b/ChangeLog index f35434c1..caa44275 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,9 @@ // $Id: ChangeLog,v 1.173 2010/04/10 18:56:06 danielaustin Exp $ // +2026-03-16 MrIron + * gnuworld/mod.cservice: + - Respect RFC1459 casemapping. + 2026-01-09 MrIron * Major update: TLS support (based on Compy's initial implementation), SASL/SCRAM authentication, fingerprint features, and expanded user/channel modes. * configure.ac: diff --git a/doc/cservice.sql b/doc/cservice.sql index 28b93fbb..3fe39d54 100644 --- a/doc/cservice.sql +++ b/doc/cservice.sql @@ -122,6 +122,7 @@ CREATE INDEX help_language_id_idx ON help (language_id); CREATE TABLE channels ( id SERIAL, name TEXT NOT NULL UNIQUE, + canon_name TEXT NOT NULL UNIQUE, flags INT4 NOT NULL DEFAULT '0', -- 0x0000 0001 - No Purge -- 0x0000 0002 - Special Channel @@ -188,6 +189,7 @@ CREATE TABLE channels ( -- A channel is inactive if the manager hasn't logged in for 21 days CREATE UNIQUE INDEX channels_name_idx ON channels(LOWER(name)); +CREATE UNIQUE INDEX channels_canon_name_idx ON channels(canon_name); -- Table for bans; channel_id references the channel entry this ban belongs to. CREATE TABLE bans ( diff --git a/doc/cservice.update.sql b/doc/cservice.update.sql index d50b8562..db051cc4 100644 --- a/doc/cservice.update.sql +++ b/doc/cservice.update.sql @@ -1,6 +1,15 @@ -- Timestamped updates for mod.cservice -- Apply appropriate updates when upgrading to a new version of cservice +-- 2026-03-16: MrIron +-- Added canonicalized channel name column +ALTER TABLE channels ADD COLUMN canon_name TEXT UNIQUE; +UPDATE channels SET canon_name = lower( + replace(replace(replace(name, '[', '{'), ']', '}'), '\\', '|') +); +ALTER TABLE channels ALTER COLUMN canon_name SET NOT NULL; +CREATE UNIQUE INDEX channels_canon_name_idx ON channels(canon_name); + -- 2025-04-01: Empus -- Added ident column to user_sec_history table -- Added deleted column to user_sec_history table diff --git a/libgnuworld/misc.cc b/libgnuworld/misc.cc index dcf29063..436f130d 100644 --- a/libgnuworld/misc.cc +++ b/libgnuworld/misc.cc @@ -45,7 +45,7 @@ namespace gnuworld { /** * Converts a character to its RFC1459 lowercase equivalent. * In addition to standard ASCII A-Z -> a-z, RFC1459 defines: - * [ -> {, ] -> }, \\ -> |, ^ -> ~ + * [ -> {, ] -> }, \\ -> | * @param c The character to convert. * @return The RFC1459-lowercased character. */ @@ -67,6 +67,20 @@ unsigned char rfc1459_tolower(unsigned char c) { } } +/** + * Converts a string to its RFC1459 lowercase equivalent. + * Applies rfc1459_tolower to each character. + * @param s The string to convert. + * @return The RFC1459-lowercased string. + */ +std::string rfc1459_tolower(const std::string& s) { + std::string result(s); + for (size_t i = 0; i < result.size(); ++i) { + result[i] = rfc1459_tolower(static_cast(result[i])); + } + return result; +} + /** * Compares two strings using RFC1459 case-insensitive rules. * Returns -1 if a < b, 1 if a > b, 0 if equal (RFC1459-insensitive). diff --git a/libgnuworld/misc.h b/libgnuworld/misc.h index 26940dcf..f222fd82 100644 --- a/libgnuworld/misc.h +++ b/libgnuworld/misc.h @@ -47,6 +47,14 @@ using std::string; */ unsigned char rfc1459_tolower(unsigned char c); +/** + * Converts a string to its RFC1459 lowercase equivalent. + * Applies rfc1459_tolower to each character. + * @param s The string to convert. + * @return The RFC1459-lowercased string. + */ +std::string rfc1459_tolower(const std::string& s); + /** * Compares two strings using RFC1459 case-insensitive rules. * Returns -1 if a < b, 1 if a > b, 0 if equal (RFC1459-insensitive). diff --git a/mod.cservice/constants.h b/mod.cservice/constants.h index 67a3aa24..a2643fdb 100644 --- a/mod.cservice/constants.h +++ b/mod.cservice/constants.h @@ -36,7 +36,7 @@ namespace sql { * articles of data. */ const std::string channel_fields = - "id,name,flags,mass_deop_pro,flood_pro,url,channels.description,comment,keywords,registered_ts," + "id,name,canon_name,flags,mass_deop_pro,flood_pro,url,channels.description,comment,keywords,registered_ts," "channel_ts,channel_mode,userflags,channels.last_updated,limit_offset,limit_period,limit_grace," "limit_max,max_bans,no_take,welcome,limit_joinmax,limit_joinsecs,limit_joinperiod," "limit_joinmode"; diff --git a/mod.cservice/cservice.cc b/mod.cservice/cservice.cc index 890bcf7c..50b6657e 100644 --- a/mod.cservice/cservice.cc +++ b/mod.cservice/cservice.cc @@ -3569,7 +3569,7 @@ bool cservice::sqlRegisterChannel(iClient* theClient, sqlUser* mngrUsr, const st } else newChan->commit(); - sqlChannelCache.insert(cservice::sqlChannelHashType::value_type(newChan->getName(), newChan)); + sqlChannelCache.insert(cservice::sqlChannelHashType::value_type(newChan->getCanonicalName(), newChan)); sqlChannelIDCache.insert(cservice::sqlChannelIDHashType::value_type(newChan->getID(), newChan)); // First delete previous levels @@ -6671,7 +6671,7 @@ void cservice::preloadChannelCache() { newChan->setAllMembers(i); newChan->setLastUsed(currentTime()); - sqlChannelCache.insert(sqlChannelHashType::value_type(newChan->getName(), newChan)); + sqlChannelCache.insert(sqlChannelHashType::value_type(newChan->getCanonicalName(), newChan)); sqlChannelIDCache.insert(sqlChannelIDHashType::value_type(newChan->getID(), newChan)); } // for() diff --git a/mod.cservice/cservice.h b/mod.cservice/cservice.h index ca3afacb..875ecd14 100644 --- a/mod.cservice/cservice.h +++ b/mod.cservice/cservice.h @@ -224,7 +224,7 @@ class cservice : public xClient { typedef map sqlUserHashType; // Channel hash, Key is channelname. - typedef map sqlChannelHashType; + typedef map sqlChannelHashType; typedef map sqlChannelIDHashType; // Accesslevel cache, key is pair(userid, chanid). @@ -622,7 +622,7 @@ class cservice : public xClient { /* Remove a sqlChannel* from cache. */ inline void removeChannelCache(sqlChannel* theChan) { if (theChan) { - sqlChannelCache.erase(theChan->getName()); + sqlChannelCache.erase(theChan->getCanonicalName()); sqlChannelIDCache.erase(theChan->getID()); } } @@ -804,7 +804,7 @@ class cservice : public xClient { typedef vector ValidUserDataListType; /* List of channels in 'pending' registration state. */ - typedef map pendingChannelListType; + typedef map pendingChannelListType; pendingChannelListType pendingChannelList; struct AppData { diff --git a/mod.cservice/sqlChannel.cc b/mod.cservice/sqlChannel.cc index 7bb5c3f9..247fe247 100644 --- a/mod.cservice/sqlChannel.cc +++ b/mod.cservice/sqlChannel.cc @@ -102,7 +102,7 @@ const int sqlChannel::EV_SUSPEND = 19; const int sqlChannel::EV_UNSUSPEND = 20; sqlChannel::sqlChannel(cservice* _bot) - : id(0), name(), flags(0), mass_deop_pro(3), flood_pro(0), msg_period(0), notice_period(0), + : id(0), name(), canon_name(), flags(0), mass_deop_pro(3), flood_pro(0), msg_period(0), notice_period(0), ctcp_period(0), flood_period(0), repeat_count(0), floodlevel(FLOODPRO_KICK), man_floodlevel(FLOODPRO_KICK), url(), description(), comment(), keywords(), welcome(), registered_ts(0), channel_ts(0), channel_mode(), userflags(0), last_topic(0), inChan(false), @@ -132,7 +132,7 @@ bool sqlChannel::loadData(const string& channelName) { #ifdef THERETURN_ENABLED queryString << "LEFT JOIN channels_w cw on channels.id = cw.channel_id "; #endif - queryString << "WHERE lower(channels.name) = '" << escapeSQLChars(string_lower(channelName)) + queryString << "WHERE lower(channels.canon_name) = '" << escapeSQLChars(rfc1459_tolower(channelName)) << "'"; if (SQLDb->Exec(queryString, true)) @@ -197,32 +197,33 @@ void sqlChannel::setAllMembers(int row) { id = atoi(SQLDb->GetValue(row, 0).c_str()); name = SQLDb->GetValue(row, 1); - flags = atoi(SQLDb->GetValue(row, 2).c_str()); - mass_deop_pro = atoi(SQLDb->GetValue(row, 3).c_str()); - flood_pro = atoi(SQLDb->GetValue(row, 4).c_str()); - url = SQLDb->GetValue(row, 5); - description = SQLDb->GetValue(row, 6); - comment = SQLDb->GetValue(row, 7); - keywords = SQLDb->GetValue(row, 8); - registered_ts = atoi(SQLDb->GetValue(row, 9).c_str()); - channel_ts = atoi(SQLDb->GetValue(row, 10).c_str()); - channel_mode = SQLDb->GetValue(row, 11); - userflags = atoi(SQLDb->GetValue(row, 12)); - last_updated = atoi(SQLDb->GetValue(row, 13)); - limit_offset = atoi(SQLDb->GetValue(row, 14)); - limit_period = atoi(SQLDb->GetValue(row, 15)); - limit_grace = atoi(SQLDb->GetValue(row, 16)); - limit_max = atoi(SQLDb->GetValue(row, 17)); - max_bans = atoi(SQLDb->GetValue(row, 18)); - no_take = atoi(SQLDb->GetValue(row, 19)); - welcome = SQLDb->GetValue(row, 20); - limit_joinmax = atoi(SQLDb->GetValue(row, 21)); - limit_joinsecs = atoi(SQLDb->GetValue(row, 22)); - limit_joinperiod = atoi(SQLDb->GetValue(row, 23)); - limit_joinmode = SQLDb->GetValue(row, 24); + canon_name = SQLDb->GetValue(row, 2); + flags = atoi(SQLDb->GetValue(row, 3).c_str()); + mass_deop_pro = atoi(SQLDb->GetValue(row, 4).c_str()); + flood_pro = atoi(SQLDb->GetValue(row, 5).c_str()); + url = SQLDb->GetValue(row, 6); + description = SQLDb->GetValue(row, 7); + comment = SQLDb->GetValue(row, 8); + keywords = SQLDb->GetValue(row, 9); + registered_ts = atoi(SQLDb->GetValue(row, 10).c_str()); + channel_ts = atoi(SQLDb->GetValue(row, 11).c_str()); + channel_mode = SQLDb->GetValue(row, 12); + userflags = atoi(SQLDb->GetValue(row, 13)); + last_updated = atoi(SQLDb->GetValue(row, 14)); + limit_offset = atoi(SQLDb->GetValue(row, 15)); + limit_period = atoi(SQLDb->GetValue(row, 16)); + limit_grace = atoi(SQLDb->GetValue(row, 17)); + limit_max = atoi(SQLDb->GetValue(row, 18)); + max_bans = atoi(SQLDb->GetValue(row, 19)); + no_take = atoi(SQLDb->GetValue(row, 20)); + welcome = SQLDb->GetValue(row, 21); + limit_joinmax = atoi(SQLDb->GetValue(row, 22)); + limit_joinsecs = atoi(SQLDb->GetValue(row, 23)); + limit_joinperiod = atoi(SQLDb->GetValue(row, 24)); + limit_joinmode = SQLDb->GetValue(row, 25); #ifdef THERETURN_ENABLED - hasw = atoi(SQLDb->GetValue(row, 25)); // This must always be the last column. - w_ts = atoi(SQLDb->GetValue(row, 26)); + hasw = atoi(SQLDb->GetValue(row, 26)); // This must always be the last column. + w_ts = atoi(SQLDb->GetValue(row, 27)); #endif setAllFlood(); @@ -260,7 +261,9 @@ bool sqlChannel::commit() { static const char* queryCondition = "WHERE id = "; stringstream queryString; - queryString << queryHeader << "SET flags = " << flags << ", " + queryString << queryHeader << "SET " + << "canon_name = '" << escapeSQLChars(canon_name) << "', " + << "flags = " << flags << ", " << "mass_deop_pro = " << mass_deop_pro << ", " << "flood_pro = " << flood_pro << ", " << "url = '" << escapeSQLChars(url) << "', " @@ -294,11 +297,11 @@ bool sqlChannel::commit() { } bool sqlChannel::insertRecord() { - static const char* queryHeader = "INSERT INTO channels (name, flags, registered_ts, " + static const char* queryHeader = "INSERT INTO channels (name, canon_name, flags, registered_ts, " "channel_ts, channel_mode, last_updated, no_take) VALUES ("; stringstream queryString; - queryString << queryHeader << "'" << escapeSQLChars(name) << "', " << flags << ", " + queryString << queryHeader << "'" << escapeSQLChars(name) << "', '" << escapeSQLChars(canon_name) << "', " << flags << ", " << registered_ts << ", " << channel_ts << ", '" << escapeSQLChars(channel_mode) << "', " << "date_part('epoch', CURRENT_TIMESTAMP)::int," << no_take << ")" << ends; diff --git a/mod.cservice/sqlChannel.h b/mod.cservice/sqlChannel.h index 403cdddd..8c6a03cd 100644 --- a/mod.cservice/sqlChannel.h +++ b/mod.cservice/sqlChannel.h @@ -179,6 +179,8 @@ class sqlChannel { inline const string& getName() const { return name; } + inline const string& getCanonicalName() const { return canon_name; } + inline const flagType& getFlags() const { return flags; } #ifdef THERETURN_ENABLED @@ -296,7 +298,10 @@ class sqlChannel { inline void setID(const unsigned int& _id) { id = _id; } - inline void setName(const string& _name) { name = _name; } + inline void setName(const string& _name) { + name = _name; + canon_name = rfc1459_tolower(_name); + } inline void setFlag(const flagType& whichFlag) { flags |= whichFlag; } @@ -468,6 +473,7 @@ class sqlChannel { protected: unsigned int id; string name; + string canon_name; flagType flags; unsigned short mass_deop_pro; unsigned int flood_pro; From 8b33ef4d3b9883ea1bc672e8d5e5b4287aec4cab Mon Sep 17 00:00:00 2001 From: MrIron Date: Mon, 16 Mar 2026 15:34:55 +0100 Subject: [PATCH 2/4] Adjustments to rfc casemapping --- doc/cservice.update.sql | 2 +- libgnuworld/misc.cc | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/cservice.update.sql b/doc/cservice.update.sql index db051cc4..526c645e 100644 --- a/doc/cservice.update.sql +++ b/doc/cservice.update.sql @@ -5,7 +5,7 @@ -- Added canonicalized channel name column ALTER TABLE channels ADD COLUMN canon_name TEXT UNIQUE; UPDATE channels SET canon_name = lower( - replace(replace(replace(name, '[', '{'), ']', '}'), '\\', '|') + replace(replace(replace(replace(name, '[', '{'), ']', '}'), '\\', '|'), '^', '~') ); ALTER TABLE channels ALTER COLUMN canon_name SET NOT NULL; CREATE UNIQUE INDEX channels_canon_name_idx ON channels(canon_name); diff --git a/libgnuworld/misc.cc b/libgnuworld/misc.cc index 436f130d..cba37382 100644 --- a/libgnuworld/misc.cc +++ b/libgnuworld/misc.cc @@ -45,7 +45,7 @@ namespace gnuworld { /** * Converts a character to its RFC1459 lowercase equivalent. * In addition to standard ASCII A-Z -> a-z, RFC1459 defines: - * [ -> {, ] -> }, \\ -> | + * [ -> {, ] -> }, \ -> |, ^ -> ~ * @param c The character to convert. * @return The RFC1459-lowercased character. */ @@ -62,6 +62,9 @@ unsigned char rfc1459_tolower(unsigned char c) { return '}'; case '\\': return '|'; + case '^': + return '~'; + default: return c; } From bc6f88c124cf92dcabb4c0eb0efd71ed149b7a3e Mon Sep 17 00:00:00 2001 From: MrIron Date: Mon, 16 Mar 2026 22:58:39 +0100 Subject: [PATCH 3/4] Minor fix --- doc/cservice.update.sql | 2 +- libgnuworld/misc.cc | 6 +++--- libgnuworld/misc.h | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/cservice.update.sql b/doc/cservice.update.sql index 526c645e..95f8a3e1 100644 --- a/doc/cservice.update.sql +++ b/doc/cservice.update.sql @@ -5,7 +5,7 @@ -- Added canonicalized channel name column ALTER TABLE channels ADD COLUMN canon_name TEXT UNIQUE; UPDATE channels SET canon_name = lower( - replace(replace(replace(replace(name, '[', '{'), ']', '}'), '\\', '|'), '^', '~') + replace(replace(replace(replace(name, '[', '{'), ']', '}'), '\\', '|'), '~', '^') ); ALTER TABLE channels ALTER COLUMN canon_name SET NOT NULL; CREATE UNIQUE INDEX channels_canon_name_idx ON channels(canon_name); diff --git a/libgnuworld/misc.cc b/libgnuworld/misc.cc index cba37382..d19dacf6 100644 --- a/libgnuworld/misc.cc +++ b/libgnuworld/misc.cc @@ -45,7 +45,7 @@ namespace gnuworld { /** * Converts a character to its RFC1459 lowercase equivalent. * In addition to standard ASCII A-Z -> a-z, RFC1459 defines: - * [ -> {, ] -> }, \ -> |, ^ -> ~ + * [ -> {, ] -> }, \ -> |, ~ -> ^ * @param c The character to convert. * @return The RFC1459-lowercased character. */ @@ -62,8 +62,8 @@ unsigned char rfc1459_tolower(unsigned char c) { return '}'; case '\\': return '|'; - case '^': - return '~'; + case '~': + return '^'; default: return c; diff --git a/libgnuworld/misc.h b/libgnuworld/misc.h index f222fd82..6375f4e0 100644 --- a/libgnuworld/misc.h +++ b/libgnuworld/misc.h @@ -41,7 +41,7 @@ using std::string; /** * Converts a character to its RFC1459 lowercase equivalent. * In addition to standard ASCII A-Z -> a-z, RFC1459 defines: - * [ -> {, ] -> }, \\ -> |, ^ -> ~ + * [ -> {, ] -> }, \\ -> |, ~ -> ^ * @param c The character to convert. * @return The RFC1459-lowercased character. */ From a45eb36580cd3d204d59f5ca3788f2c9eca5d052 Mon Sep 17 00:00:00 2001 From: MrIron Date: Mon, 23 Mar 2026 13:28:33 +0100 Subject: [PATCH 4/4] Use sql migration tool --- doc/cservice.update.sql | 9 --------- mod.cservice/migrations/001_canon_names.sql | 8 ++++++++ 2 files changed, 8 insertions(+), 9 deletions(-) create mode 100644 mod.cservice/migrations/001_canon_names.sql diff --git a/doc/cservice.update.sql b/doc/cservice.update.sql index 95f8a3e1..d50b8562 100644 --- a/doc/cservice.update.sql +++ b/doc/cservice.update.sql @@ -1,15 +1,6 @@ -- Timestamped updates for mod.cservice -- Apply appropriate updates when upgrading to a new version of cservice --- 2026-03-16: MrIron --- Added canonicalized channel name column -ALTER TABLE channels ADD COLUMN canon_name TEXT UNIQUE; -UPDATE channels SET canon_name = lower( - replace(replace(replace(replace(name, '[', '{'), ']', '}'), '\\', '|'), '~', '^') -); -ALTER TABLE channels ALTER COLUMN canon_name SET NOT NULL; -CREATE UNIQUE INDEX channels_canon_name_idx ON channels(canon_name); - -- 2025-04-01: Empus -- Added ident column to user_sec_history table -- Added deleted column to user_sec_history table diff --git a/mod.cservice/migrations/001_canon_names.sql b/mod.cservice/migrations/001_canon_names.sql new file mode 100644 index 00000000..5ef821e0 --- /dev/null +++ b/mod.cservice/migrations/001_canon_names.sql @@ -0,0 +1,8 @@ +-- 2026-03-16: MrIron +-- Added canonicalized channel name column +ALTER TABLE channels ADD COLUMN canon_name TEXT UNIQUE; +UPDATE channels SET canon_name = lower( + replace(replace(replace(replace(name, '[', '{'), ']', '}'), '\\', '|'), '~', '^') +); +ALTER TABLE channels ALTER COLUMN canon_name SET NOT NULL; +CREATE UNIQUE INDEX channels_canon_name_idx ON channels(canon_name);