diff --git a/configure.ac b/configure.ac
@@ -7,7 +7,7 @@ dnl src/windows/unrealinst.iss
dnl doc/Config.header
dnl src/version.c.SH
-AC_INIT([unrealircd], [5.0.9], [https://bugs.unrealircd.org/], [], [https://unrealircd.org/])
+AC_INIT([unrealircd], [5.2.0.1], [https://bugs.unrealircd.org/], [], [https://unrealircd.org/])
AC_CONFIG_SRCDIR([src/ircd.c])
AC_CONFIG_HEADER([include/setup.h])
AC_CONFIG_AUX_DIR([autoconf])
@@ -30,17 +30,17 @@ UNREAL_VERSION_GENERATION=["5"]
AC_DEFINE_UNQUOTED([UNREAL_VERSION_GENERATION], [$UNREAL_VERSION_GENERATION], [Generation version number (e.g.: X for X.Y.Z)])
# Major version number (e.g.: Y in X.Y.Z)
-UNREAL_VERSION_MAJOR=["0"]
+UNREAL_VERSION_MAJOR=["2"]
AC_DEFINE_UNQUOTED([UNREAL_VERSION_MAJOR], [$UNREAL_VERSION_MAJOR], [Major version number (e.g.: Y for X.Y.Z)])
# Minor version number (e.g.: Z in X.Y.Z)
-UNREAL_VERSION_MINOR=["9"]
+UNREAL_VERSION_MINOR=["0"]
AC_DEFINE_UNQUOTED([UNREAL_VERSION_MINOR], [$UNREAL_VERSION_MINOR], [Minor version number (e.g.: Z for X.Y.Z)])
# The version suffix such as a beta marker or release candidate
# marker. (e.g.: -rcX for unrealircd-3.2.9-rcX). This macro is a
# string instead of an integer because it contains arbitrary data.
-UNREAL_VERSION_SUFFIX=[""]
+UNREAL_VERSION_SUFFIX=[".1"]
AC_DEFINE_UNQUOTED([UNREAL_VERSION_SUFFIX], ["$UNREAL_VERSION_SUFFIX"], [Version suffix such as a beta marker or release candidate marker. (e.g.: -rcX for unrealircd-3.2.9-rcX)])
AC_PATH_PROG(RM,rm)
@@ -83,6 +83,18 @@ AC_CHECK_LIB(descrypt, crypt,
[AC_DEFINE([HAVE_CRYPT], [], [Define if you have crypt])
IRCDLIBS="$IRCDLIBS-lcrypt "])])
+dnl Check for big-endian system, even though these hardly exist anymore...
+AS_CASE([$host_cpu],
+ [i?86|amd64|x86_64],
+ [ac_cv_c_bigendian=no]
+)
+AC_C_BIGENDIAN(
+ AC_DEFINE(NATIVE_BIG_ENDIAN, 1, [machine is bigendian]),
+ AC_DEFINE(NATIVE_LITTLE_ENDIAN, 1, [machine is littleendian]),
+ AC_MSG_ERROR([unknown endianness]),
+ AC_MSG_ERROR([universal endianness is not supported - compile separately and use lipo(1)])
+)
+
dnl HARDENING START
dnl This is taken from https://github.com/kmcallister/autoharden
dnl With some very small modifications (to remove C++ checking for instance)
@@ -499,6 +511,7 @@ AC_ARG_WITH(operoverride-verify, [AS_HELP_STRING([--with-operoverride-verify], [
[AC_DEFINE([OPEROVERRIDE_VERIFY], [], [Define if you want opers to have to use /invite to join +s/+p channels])])])
AC_ARG_WITH(system-pcre2, [AS_HELP_STRING([--without-system-pcre2], [Use the system pcre2 package instead of bundled, discovered using pkg-config])], [], [with_system_pcre2=yes])
AC_ARG_WITH(system-argon2, [AS_HELP_STRING([--without-system-argon2], [Use bundled version instead of system argon2 library. Normally autodetected via pkg-config])], [], [with_system_argon2=yes])
+AC_ARG_WITH(system-sodium, [AS_HELP_STRING([--without-system-sodium], [Use bundled version instead of system sodium library. Normally autodetected via pkg-config])], [], [with_system_sodium=yes])
AC_ARG_WITH(system-cares, [AS_HELP_STRING([--without-system-cares], [Use bundled version instead of system c-ares. Normally autodetected via pkg-config.])], [], [with_system_cares=yes])
CHECK_SSL
CHECK_SSL_CTX_SET1_CURVES_LIST
@@ -625,6 +638,55 @@ AC_SUBST(ARGON2_LIBS)
cd $cur_dir
])
+dnl Use system sodium when available, unless --without-system-sodium
+has_system_sodium="no"
+AS_IF([test "x$with_system_sodium" = "xyes"],[
+PKG_CHECK_MODULES([SODIUM], [libsodium >= 1.0.16],[has_system_sodium=yes
+AS_IF([test "x$PRIVATELIBDIR" != "x"], [rm -f "$PRIVATELIBDIR/"libsodium*])],[has_system_sodium=no])])
+
+AS_IF([test "$has_system_sodium" = "no"],[
+dnl REMEMBER TO CHANGE WITH A NEW SODIUM RELEASE!
+sodium_version="1.0.18"
+AC_MSG_RESULT(extracting sodium library)
+cur_dir=`pwd`
+cd extras
+dnl remove old sodium directory to force a recompile...
+dnl and remove its installation prefix just to clean things up.
+rm -rf sodium-$sodium_version sodium
+if test "x$ac_cv_path_GUNZIP" = "x" ; then
+ tar xfz libsodium.tar.gz
+else
+ cp libsodium.tar.gz libsodium.tar.gz.bak
+ gunzip -f libsodium.tar.gz
+ cp libsodium.tar.gz.bak libsodium.tar.gz
+ tar xf libsodium.tar
+fi
+AC_MSG_RESULT(compiling sodium library)
+cd libsodium-$sodium_version
+save_cflags="$CFLAGS"
+CFLAGS="$orig_cflags"
+export CFLAGS
+./configure --prefix=$cur_dir/extras/sodium --libdir=$PRIVATELIBDIR --enable-shared --disable-static --enable-opt || exit 1
+CFLAGS="$save_cflags"
+AC_MSG_RESULT(compiling sodium resolver library)
+$ac_cv_prog_MAKER || exit 1
+AC_MSG_RESULT(installing sodium resolver library)
+$ac_cv_prog_MAKER install || exit 1
+SODIUM_CFLAGS="-I$cur_dir/extras/sodium/include"
+AC_SUBST(SODIUM_CFLAGS)
+SODIUM_LIBS=
+dnl See c-ares's compilation section for more info on this hack.
+dnl ensure that we're linking against the bundled version
+dnl (we only reach this code if linking against the bundled version is desired).
+AS_IF([test -n "$ac_cv_path_PKGCONFIG"],
+ [SODIUM_LIBS="`$ac_cv_path_PKGCONFIG --libs libsodium.pc`"])
+dnl For when pkg-config isn't available
+AS_IF([test -z "$SODIUM_LIBS"],
+ [SODIUM_LIBS="-L$PRIVATELIBDIR -lsodium"])
+AC_SUBST(SODIUM_LIBS)
+cd $cur_dir
+])
+
dnl Use system c-ares when available, unless --without-system-cares.
has_system_cares="no"
AS_IF([test "x$with_system_cares" = "xyes"],[
@@ -654,7 +716,7 @@ cd c-ares-$cares_version
save_cflags="$CFLAGS"
CFLAGS="$orig_cflags"
export CFLAGS
-./configure --prefix=$cur_dir/extras/c-ares --libdir=$PRIVATELIBDIR --enable-shared || exit 1
+./configure --prefix=$cur_dir/extras/c-ares --libdir=$PRIVATELIBDIR --enable-shared --disable-tests || exit 1
CFLAGS="$save_cflags"
AC_MSG_RESULT(compiling c-ares resolver library)
$ac_cv_prog_MAKER || exit 1
diff --git a/doc/Config.header b/doc/Config.header
@@ -7,7 +7,7 @@
\___/|_| |_|_| \___|\__,_|_|\___/\_| \_| \____/\__,_|
Configuration Program
- for UnrealIRCd 5.0.9
+ for UnrealIRCd 5.2.0.1
This program will help you to compile your IRC server, and ask you
questions regarding the compile-time settings of it during the process.
@@ -22,7 +22,7 @@ https://www.unrealircd.org/docs/UnrealIRCd_5_documentation
The full release notes are available in doc/RELEASE-NOTES.md
For easier viewing, check out the latest online release notes at:
-https://github.com/unrealircd/unrealircd/blob/unreal50/doc/RELEASE-NOTES.md
+https://github.com/unrealircd/unrealircd/blob/unreal52/doc/RELEASE-NOTES.md
UnrealIRCd 5 is compatible with the following services:
* anope with the "unreal4" protocol module - version 2.0.7 or higher required!
diff --git a/doc/RELEASE-NOTES.md b/doc/RELEASE-NOTES.md
@@ -1,7 +1,176 @@
-UnrealIRCd 5.0.9 Release Notes
-===============================
+UnrealIRCd 5.2.0.1 Release Notes
+=================================
+
+About 5.2.0.1
+--------------
+5.2.0.1 fixes an issue with spamfilter that was present in 5.2.0.
+In channels spamfilters were processed for type ```p``` instead of ```c```.
+Existing 5.2.0 users on *NIX can upgrade without restart by running
+```./unrealircd hot-patch wrongspamfilter520```
+
+UnrealIRCd 5.2.0 is out!
+-------------------------
+
+This is UnrealIRCd 5.2.0, a release with lots of new features.
+The two main new features are: an improved and more flexible anti-flood block
+and channel history which can now be stored encrypted on disk and allows
+clients to fetch hundreds/thousands of lines.
+
+Upgrading and the 5.0.x series
+-------------------------------
+UnrealIRCd 5.2.0 is the direct successor to 5.0.9/5.0.9.1.
+There will be [no further 5.0.x releases](https://www.unrealircd.org/docs/FAQ#About_the_new_5.2.x_series),
+in particular there will be no 5.0.10.
+
+Only four bugs that affect a limited number of people/networks were fixed.
+UnrealIRCd 5.2.0 is mostly a feature release.
+Admins wishing to take a conservative approach don't need to rush an
+upgrade from 5.0.x to 5.2.0, they can wait for a 5.2.1 or 5.2.2 release.
+
+If you are upgrading from 5.0.9(.1) to 5.2.0 then feel free to try the new
+```./unrealircd upgrade``` command.
+
+The only configuration change is in the set::anti-flood block (as explained
+further down under *Enhancements*). When starting UnrealIRCd will give you
+clear instructions if anything needs to be changed (and what).
+This process is really minor, the server will usually tell you to just
+delete a few old lines from the configuration file.
-This release comes with several nice feature enhancements. There are no major bug fixes.
+Enhancements
+-------------
+* The set::anti-flood block has been redone so you can have different limits
+ for *unknown-users* and *known-users*.
+ * As a reminder, by default, *known-users* are users who are identified
+ to services OR are on an IP that has been connected for over 2 hours
+ in the past X days. The exact definition of "known-users" is in the
+ [security-group block](https://www.unrealircd.org/docs/Security-group_block).
+ * See [here](https://www.unrealircd.org/docs/Anti-flood_settings)
+ for more information on the layout of the new set::anti-flood block.
+ * All violations of target-flood, nick-flood, join-flood, away-flood,
+ invite-flood, knock-flood, max-concurrent-conversations are now
+ reported to opers with the snomask ```f``` (flood).
+* Add support for database encryption. The way this works
+ is that you define an encryption password in a
+ [secret { } block](https://www.unrealircd.org/docs/Secret_block).
+ Then from the various modules you can refer to this secret
+ block, from
+ [set::reputation::db-secret](https://www.unrealircd.org/docs/Set_block#set::reputation),
+ [set::tkldb::db-secret](https://www.unrealircd.org/docs/Set_block#set::tkldb)
+ and [set::channeldb::db-secret](https://www.unrealircd.org/docs/Set_block#set::channeldb).
+ This way you can encrypt the reputation, TKL and channel
+ database for increased privacy.
+* Add optional support for
+ [persistent channel history](https://www.unrealircd.org/docs/Set_block#Persistent_channel_history):
+ * This stores channel history on disk for channels that have
+ both ```+H``` and ```+P``` set.
+ * If you enable this then we ALWAYS require you to set an
+ encryption password, as we do not allow storing of
+ channel history in plain text.
+ * If you enable the option, then the history is stored in
+ ```data/history/``` in individual .db files. No channel
+ names are visible in the filenames for optimal privacy.
+ * See [Persistent channel history](https://www.unrealircd.org/docs/Set_block#Persistent_channel_history)
+ on how to enable this. By default it is off.
+* Add support for IRCv3
+ [draft/chathistory](https://ircv3.net/specs/extensions/chathistory).
+* The maximums for channel mode ```+H``` have been raised and are now
+ different for ```+r``` (registered) and ```-r``` channels. For unregistered
+ channels the limit is now 200 lines / 31 days. For registered channels
+ the limit is 5000 lines / 31 days. The old limit for both was 200 lines / 7 days.
+ These maximums can be changed in the now slightly different
+ [set::history::channel::max-storage-per-channel](https://www.unrealircd.org/docs/Set_block#set::history)
+ block.
+* Add c-ares and libsodium version output to boot screen and /VERSION.
+* WHOX now supports displaying the
+ [reputation score](https://www.unrealircd.org/docs/Reputation_score).
+ If you are an IRCOp then you can use e.g. ```WHO * %cuhsnfmdaRr```.
+* Add ability to [spamfilter](https://www.unrealircd.org/docs/Spamfilter)
+ message tags via the new ```T``` target. Right now it would be unusual
+ to use this, but some day when we have more
+ [message tags](https://www.unrealircd.org/docs/Message_tags) it
+ may come in handy.
+* Support [```+draft/reply```](https://ircv3.net/specs/client-tags/reply) IRCv3
+ client tag. Can be used by bots (and others) to indicate to what message
+ people are replying to. This module, reply-tag, is loaded by default.
+* Send [```draft/bot```](https://ircv3.net/specs/extensions/bot-mode) IRCv3
+ message tag if the user has mode ```+B``` set.
+* [Websockets](https://www.unrealircd.org/docs/WebSocket_support):
+ add support for clients to negotiate an explicit type via
+ ```Sec-WebSocket-Protocol```, instead of only the default type from
+ [listen::websocket::type](https://www.unrealircd.org/docs/WebSocket_support#2._Enable_websocket_on_the_port).
+ This is based on an IRCv3 websocket draft specification.
+ Note that UnrealIRCd refuses type text if your configuration allows
+ non-UTF8 characters in channel or nick names because it would lead
+ to security and compatibility issues.
+* [set::restrict-commands](https://www.unrealircd.org/docs/Set_block#set::restrict-commands):
+ new option *exempt-tls* which allows SSL/TLS users to bypass a restriction.
+
+Fixes
+------
+* Server squiting the wrong side. Often harmless, but when (re)connecting
+ rapidly to multiple servers with autoconnect this could cause the
+ network to fall apart.
+* Forbid using [extended server bans](https://www.unrealircd.org/docs/Extended_server_bans)
+ in ZLINE/GZLINE since they won't work there.
+* Extended server ban ```~a:accname``` was not working for shun, and only
+ partially working for kline/gline.
+* More accurate /ELINE error message.
+
+Changed
+--------
+* Channel mode ```+H``` always showed time in minutes (```m```) until now.
+ From now on it will show it in minutes (```m```), hours (```h```) or
+ days (```d```) depending on the actual value. Eg ```+H 50:7d```.
+* If you ran ```./unrealircd stop``` we used to wait only 1 second.
+ From now on we will wait up to 10 seconds max. This gives UnrealIRCd
+ plenty of time to write database files.
+* If you have zero [log blocks](https://www.unrealircd.org/docs/Log_block)
+ then we already automatically logged errors to ```ircd.log```.
+ From now on we will log everything (not only errors) to that file.
+
+Removed
+--------
+* Version check for curl and openssl as nowadays they have ABI guarantees.
+
+Module coders / Developers
+---------------------------
+* New UnrealDB API and disk format, see
+ https://www.unrealircd.org/docs/Dev:UnrealDB
+* We now use libsodium for file encryption routines as well
+ as some helpers to lock/clear passwords in memory.
+* Updated ```HOOKTYPE_LOCAL_NICKCHANGE``` and
+ ```HOOKTYPE_REMOTE_NICKCHANGE``` to include an
+ ```MessageTag *mtags``` argument in the middle.
+ You can use ```#if UNREAL_VERSION_TIME>=202115``` to detect this.
+* Updated channel mode ```conv_param``` function to
+ include a ```Channel *channel``` argument at the end.
+ You can use ```#if UNREAL_VERSION_TIME>=202120``` to detect this.
+* New: ```ModuleSetOptions(modinfo->handle, MOD_OPT_UNLOAD_PRIORITY, priority);```.
+ This can be used for modules to indicate they wish to be unloaded
+ before or after others. It is used by for example the channel
+ and history modules so they can save their databases before
+ channel mode modules or other modules get unloaded.
+* New CAP [```draft/chathistory```](https://ircv3.net/specs/extensions/chathistory).
+ If a client REQ's this CAP then UnrealIRCd won't send history on-join as
+ it assumes the client will fetch it when they feel the need for it.
+* New informative CAP:
+ [```unrealircd.org/history-backend```](https://www.unrealircd.org/history-backend)
+
+Reminder: UnrealIRCd 4 is no longer supported
+----------------------------------------------
+
+UnrealIRCd 4.x is [no longer supported](https://www.unrealircd.org/docs/UnrealIRCd_4_EOL).
+Admins must [upgrade to UnrealIRCd 5](https://www.unrealircd.org/docs/Upgrading_from_4.x).
+
+UnrealIRCd 5.0.9.1
+-------------------
+The only change between 5.0.9 and 5.0.9.1 is:
+* Build improvements on *NIX (faster compiling and lower memory requirements)
+* Windows version is unchanged and still 5.0.9
+
+UnrealIRCd 5.0.9
+-----------------
+The 5.0.9 release comes with several nice feature enhancements. There are no major bug fixes.
Enhancements:
* Changes to the "Client connecting" notice on IRC (for IRCOps):
@@ -38,7 +207,7 @@ Fixes:
missing.
Changes:
-* Add doc/KEYS which contains the public key(s) used to sign UnrealIRCd releases
+* Add ```doc/KEYS``` which contains the public key(s) used to sign UnrealIRCd releases
* The options set::anti-flood::unknown-flood-* have been renamed and
integrated in a new block called
[set::anti-flood::handshake-data-flood](https://www.unrealircd.org/docs/Set_block#set::anti-flood::handshake-data-flood).
@@ -49,18 +218,6 @@ That is, when in "auto" mode, which is like for 99% of the users.
Note that the system may still limit the actual number of connections
to a lower value, epending on the value of ```ulimit -n -H```.
-Reminder: UnrealIRCd 4 is no longer supported
-----------------------------------------------
-
-UnrealIRCd 4.x is [no longer supported](https://www.unrealircd.org/docs/UnrealIRCd_4_EOL).
-Admins must upgrade to UnrealIRCd 5.
-
-Upgrading from 4.x to 5.x?
-Then check out the *UnrealIRCd 5* release notes [further down](#unrealircd-5).
-Or, at the very least, check out
-[Upgrading from 4.x](https://www.unrealircd.org/docs/Upgrading_from_4.x).
-
-
UnrealIRCd 5.0.8
-----------------
diff --git a/doc/conf/modules.conf b/doc/conf/modules.conf
@@ -162,6 +162,8 @@ loadmodule "extbans/securitygroup"; /* +b ~G */
loadmodule "account-notify";
loadmodule "account-tag";
loadmodule "batch";
+loadmodule "bot-tag";
+loadmodule "chathistory";
loadmodule "clienttagdeny";
loadmodule "echo-message";
loadmodule "labeled-response";
@@ -169,6 +171,7 @@ loadmodule "link-security";
loadmodule "message-ids";
loadmodule "message-tags";
loadmodule "plaintext-policy";
+loadmodule "reply-tag";
loadmodule "server-time";
loadmodule "sts";
loadmodule "typing-indicator";
@@ -186,7 +189,7 @@ loadmodule "history_backend_mem";
#loadmodule "history_backend_null";
loadmodule "ident_lookup";
loadmodule "jointhrottle";
-#loadmodule "targetfloodprot";
+loadmodule "targetfloodprot";
loadmodule "tkldb";
loadmodule "tls_antidos";
loadmodule "userhost-tag";
diff --git a/doc/conf/opers.conf b/doc/conf/opers.conf
@@ -5,7 +5,7 @@ oper acidvegas {
operclass netadmin;
require-modes z;
maxlogins 1;
- vhost super.nets;
+ vhost most.dangerous.motherfuck;
swhois "1,1 1,5 1,1 ";
swhois "1,1 1,5 1,7 1,5 1,7 1,5 1,1 ";
swhois "1,1 1,5 1,7 1,5 1,7 1,5 1,7 1,5 1,1 0 1 ";
diff --git a/doc/conf/unrealircd.remote.conf b/doc/conf/unrealircd.remote.conf
@@ -26,6 +26,10 @@ listen { ip *; port 6667; options { clientsonly; } }
listen { ip *; port 6697; options { clientsonly; tls; } }
listen { ip *; port REDACTED; options { serversonly; tls; } }
+deny channel { channel "#pumpcoin"; reason "This channel has moved to #exchange"; redirect "#exchange"; }
+deny channel { channel "#dev"; reason "This channel has moved to #superbowl"; redirect "#superbowl"; }
+deny channel { channel "#help"; reason "This channel has moved to #superbowl"; redirect "#superbowl"; }
+
link irc.supernets.org {
incoming { mask REDACTED; }
outgoing {
@@ -106,7 +110,7 @@ set {
private-notice { connect-delay 3600; exempt-identified yes; exempt-reputation-score 100; }
}
#auto-join "#superbowl";
- oper-auto-join "#help";
+ oper-auto-join "#superbowl";
static-quit "EMO-QUIT";
static-part "EMO-PART";
who-limit 100;
@@ -141,28 +145,43 @@ set {
oper-message "Network operators must connect using an up-to-date SSL/TLS protocol or cipher";
}
anti-flood {
- away-flood 3:300;
- connect-flood 3:300;
- invite-flood 3:300;
- join-flood 3:300;
- knock-flood 3:300;
- nick-flood 3:300;
- max-concurrent-conversations {
- users 5;
- new-user-every 60s;
+ everyone {
+ connect-flood 3:300;
+ handshake-data-flood {
+ amount 4k;
+ ban-action gzline;
+ ban-time 1h;
+ }
+ target-flood {
+ channel-notice 15:5;
+ channel-privmsg 45:5;
+ channel-tagmsg 15:5;
+ private-notice 10:5;
+ private-privmsg 30:5;
+ private-tagmsg 10:5;
+ }
+ }
+ known-users {
+ away-flood 3:300;
+ invite-flood 3:300;
+ join-flood 3:300;
+ knock-flood 3:300;
+ nick-flood 3:300;
+ max-concurrent-conversations {
+ users 5;
+ new-user-every 60s;
+ }
}
- #target-flood {
- # channel-privmsg 45:5;
- # channel-notice 15:5;
- # channel-tagmsg 15:5;
- # private-privmsg 30:5;
- # private-notice 10:5;
- # private-tagmsg 10:5;
- #}
- handshake-data-flood {
- amount 4k;
- ban-action gzline;
- ban-time 1h;
+ unknown-users {
+ away-flood 3:300;
+ invite-flood 3:300;
+ join-flood 3:300;
+ knock-flood 3:300;
+ nick-flood 3:300;
+ max-concurrent-conversations {
+ users 3;
+ new-user-every 60s;
+ }
}
}
default-bantime 30d;
@@ -183,19 +202,22 @@ set {
unauthorized "8,4 E N T E R T H E V O I D ";
}
antimixedutf8 {
- score 10;
+ score 8;
ban-action block;
ban-reason "8,4 E N T E R T H E V O I D ";
}
connthrottle {
- known-users { minimum-reputation-score 24; sasl-bypass yes; }
- new-users { local-throttle 20:60; global-throttle 30:60; }
- disabled-when { reputation-gathering 1w; start-delay 3m; }
+ known-users { minimum-reputation-score 100; sasl-bypass yes; }
+ new-users { local-throttle 20:60; global-throttle 30:60; }
+ disabled-when { reputation-gathering 1w; start-delay 3m; }
}
history {
channel {
- playback-on-join { lines 100; time 1d; }
- max-storage-per-channel { lines 100; time 1d; }
+ playback-on-join { lines 100; time 1d; }
+ max-storage-per-channel {
+ registered { lines 100; time 1d; }
+ unregistered { lines 50; time 12h; }
+ }
}
}
hide-idle-time { policy usermode; }
@@ -206,4 +228,11 @@ hideserver {
disable-links yes;
map-deny-message "Denied";
links-deny-message "Denied";
+}
+
+security-group known-users {
+ identified yes;
+ webirc no;
+ tls no;
+ reputation-score 100;
}
\ No newline at end of file
diff --git a/extras/build-tests/nix/run-tests b/extras/build-tests/nix/run-tests
@@ -40,6 +40,14 @@ else
./run -services atheme || exit 1
fi
+# Database writing/reading tests
+## unencrypted:
+./run -services none -boot tests/db/writing/* || exit 1
+./run -services none -keepdbs -boot tests/db/reading/* || exit 1
+## encrypted:
+./run -services none -include db_crypted.conf -boot tests/db/writing/* || exit 1
+./run -services none -include db_crypted.conf -keepdbs -boot tests/db/reading/* || exit 1
+
# Do cipherscan test at the end
# Has problems on non-Linux-64-bit, so we skip there:
if [ "$FREEBSD" = 0 -a "$HOSTNAME" != "ub18-ia32" ]; then
diff --git a/extras/build-tests/windows/build.bat b/extras/build-tests/windows/build.bat
@@ -78,11 +78,26 @@ if %ERRORLEVEL% NEQ 0 EXIT /B 1
cd unrealircd-tests
dir
+rem All tests except db:
"C:\Program Files\Git\bin\bash.exe" ./runwin
if %ERRORLEVEL% NEQ 0 EXIT /B 1
+rem Test unencrypted db's:
+"C:\Program Files\Git\bin\bash.exe" ./runwin -boot tests/db/writing/*
+if %ERRORLEVEL% NEQ 0 EXIT /B 1
+"C:\Program Files\Git\bin\bash.exe" ./runwin -keepdbs -boot tests/db/reading/*
+if %ERRORLEVEL% NEQ 0 EXIT /B 1
+
+rem Test encrypted db's:
+"C:\Program Files\Git\bin\bash.exe" ./runwin -include db_crypted.conf -boot tests/db/writing/*
+if %ERRORLEVEL% NEQ 0 EXIT /B 1
+"C:\Program Files\Git\bin\bash.exe" ./runwin -include db_crypted.conf -keepdbs -boot tests/db/reading/*
+if %ERRORLEVEL% NEQ 0 EXIT /B 1
+
goto end
+
+
:installerfailed
type setup.log
echo INSTALLATION FAILED
diff --git a/extras/build-tests/windows/compilecmd/vs2019.bat b/extras/build-tests/windows/compilecmd/vs2019.bat
@@ -15,4 +15,7 @@ PCRE2_LIB_DIR="c:\projects\unrealircd-5-libs\pcre2\lib" ^
PCRE2LIB="pcre2-8.lib" ^
ARGON2_LIB_DIR="c:\projects\unrealircd-5-libs\argon2\vs2015\build" ^
ARGON2_INC_DIR="c:\projects\unrealircd-5-libs\argon2\include" ^
-ARGON2LIB="Argon2RefDll.lib" %*
+ARGON2LIB="Argon2RefDll.lib" ^
+SODIUM_LIB_DIR="c:\projects\unrealircd-5-libs\libsodium\bin\x64\Release\v142\dynamic" ^
+SODIUM_INC_DIR="c:\projects\unrealircd-5-libs\libsodium\src\libsodium\include" ^
+SODIUMLIB="libsodium.lib" %*
diff --git a/extras/curlinstall b/extras/curlinstall
@@ -90,7 +90,7 @@ cd "$OUTD" || exit 1
echo "Building and installing libcurl"
CPPFLAGS="-I$ARESPATH/include" ./configure --prefix=$UNREALDIR/extras/curl --libdir=$LIBDIR --enable-shared \
- --disable-thread --enable-ares=$ARESPATH --disable-ipv6
+ --enable-ares=$ARESPATH --with-openssl
cp -R $ARESPATH/lib ares
make && make install
diff --git a/extras/doxygen/Doxyfile b/extras/doxygen/Doxyfile
@@ -38,7 +38,7 @@ PROJECT_NAME = "UnrealIRCd"
# could be handy for archiving the generated documentation or if some version
# control system is used.
-PROJECT_NUMBER = 5.0.9
+PROJECT_NUMBER = 5.2.0.1
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
diff --git a/extras/libsodium.tar.gz b/extras/libsodium.tar.gz
Binary files differ.
diff --git a/include/dynconf.h b/include/dynconf.h
@@ -1,6 +1,7 @@
/************************************************************************
* Unreal Internet Relay Chat Daemon, include/dynconf.h
- * Copyright (C) 1999 Carsten Munk
+ * Copyright (C) 1999-2003 Carsten Munk
+ * Copyright (C) 2003-2021 Bram Matthys
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,6 +23,15 @@
#define DYNCONF_H
+typedef struct FloodSettings FloodSettings;
+
+struct FloodSettings {
+ FloodSettings *prev, *next;
+ char *name;
+ int limit[MAXFLOODOPTIONS];
+ long period[MAXFLOODOPTIONS];
+};
+
typedef struct NetworkConfiguration NetworkConfiguration;
struct NetworkConfiguration {
unsigned x_inah:1;
@@ -118,16 +128,7 @@ struct Configuration {
int handshake_data_flood_ban_action;
struct ChMode modes_on_join;
int level_on_join;
- unsigned char away_count;
- long away_period;
- unsigned char nick_count;
- long nick_period;
- unsigned char invite_count;
- long invite_period;
- unsigned char knock_count;
- long knock_period;
- unsigned char max_concurrent_conversations_users;
- unsigned char max_concurrent_conversations_new_user_every;
+ FloodSettings *floodsettings;
int ident_connect_timeout;
int ident_read_timeout;
long default_bantime;
@@ -234,15 +235,6 @@ extern MODVAR int ipv6_disabled;
#define MODES_ON_JOIN iConf.modes_on_join.mode
#define LEVEL_ON_JOIN iConf.level_on_join
-#define AWAY_PERIOD iConf.away_period
-#define AWAY_COUNT iConf.away_count
-#define NICK_PERIOD iConf.nick_period
-#define NICK_COUNT iConf.nick_count
-#define KNOCK_PERIOD iConf.knock_period
-#define KNOCK_COUNT iConf.knock_count
-#define INVITE_PERIOD iConf.invite_period
-#define INVITE_COUNT iConf.invite_count
-
#define IDENT_CONNECT_TIMEOUT iConf.ident_connect_timeout
#define IDENT_READ_TIMEOUT iConf.ident_read_timeout
@@ -325,17 +317,8 @@ struct SetCheck {
unsigned has_restrict_channelmodes:1;
unsigned has_restrict_extendedbans:1;
unsigned has_channel_command_prefix:1;
- unsigned has_anti_flood_handshake_data_flood_amount:1;
- unsigned has_anti_flood_handshake_data_flood_ban_action:1;
- unsigned has_anti_flood_handshake_data_flood_ban_time:1;
unsigned has_modes_on_join:1;
unsigned has_level_on_join:1;
- unsigned has_anti_flood_away_count:1;
- unsigned has_anti_flood_away_period:1;
- unsigned has_anti_flood_nick_flood:1;
- unsigned has_anti_flood_connect_flood:1;
- unsigned has_anti_flood_invite_flood:1;
- unsigned has_anti_flood_knock_flood:1;
unsigned has_ident_connect_timeout:1;
unsigned has_ident_read_timeout:1;
unsigned has_default_bantime:1;
diff --git a/include/h.h b/include/h.h
@@ -285,6 +285,7 @@ extern char *myctime(time_t);
extern char *short_date(time_t, char *buf);
extern char *long_date(time_t);
extern void exit_client(Client *client, MessageTag *recv_mtags, char *comment);
+extern void exit_client_ex(Client *client, Client *origin, MessageTag *recv_mtags, char *comment);
extern void initstats(), tstats(Client *, char *);
extern char *check_string(char *);
extern char *make_nick_user_host(char *, char *, char *);
@@ -312,7 +313,7 @@ extern int find_str_match_link(Link *, char *);
extern void free_str_list(Link *);
extern Link *make_link();
extern Ban *make_ban();
-extern ClientUser *make_user(Client *);
+extern User *make_user(Client *);
extern Server *make_server();
extern Client *make_client(Client *, Client *);
extern Member *find_channel_link(Member *, Channel *);
@@ -447,6 +448,7 @@ extern void Auth_FreeAuthConfig(AuthConfig *as);
extern int Auth_Check(Client *cptr, AuthConfig *as, char *para);
extern char *Auth_Hash(int type, char *para);
extern int Auth_CheckError(ConfigEntry *ce);
+extern int Auth_AutoDetectHashType(char *hash);
extern void make_cloakedhost(Client *client, char *curr, char *buf, size_t buflen);
extern int channel_canjoin(Client *client, char *name);
@@ -545,9 +547,31 @@ extern void *safe_alloc(size_t size);
*/
#define raw_strldup(str, max) our_strldup(str, max)
+extern void *safe_alloc_sensitive(size_t size);
+
+/** Free previously allocate memory pointer - this is the sensitive version which
+ * may ONLY be called on allocations returned by safe_alloc_sensitive() / safe_strdup_sensitive().
+ * This will set the memory to all zeroes before actually deallocating.
+ * It also sets the pointer to NULL, since that would otherwise be common to forget.
+ * @note If you call this function on normally allocated memory (non-sensitive) then we will crash.
+ */
+#define safe_free_sensitive(x) do { if (x) sodium_free(x); x = NULL; } while(0)
+
+/** Free previous memory (if any) and then save a duplicate of the specified string -
+ * This is the 'sensitive' version which should only be used for HIGHLY sensitive data,
+ * as it wastes about 8000 bytes even if you only duplicate a string of 32 bytes (this is by design).
+ * @param dst The current pointer and the pointer where a new copy of the string will be stored.
+ * @param str The string you want to copy
+ */
+#define safe_strdup_sensitive(dst,str) do { if (dst) sodium_free(dst); if (!(str)) dst = NULL; else dst = our_strdup_sensitive(str); } while(0)
+
+/** Safely destroy a string in memory (but do not free!) */
+#define destroy_string(str) sodium_memzero(str, strlen(str))
+
/** @} */
extern char *our_strdup(const char *str);
extern char *our_strldup(const char *str, size_t max);
+extern char *our_strdup_sensitive(const char *str);
extern long config_checkval(char *value, unsigned short flags);
extern void config_status(FORMAT_STRING(const char *format), ...) __attribute__((format(printf,1,2)));
@@ -701,7 +725,8 @@ extern MODVAR void (*tkl_stats)(Client *cptr, int type, char *para, int *cnt);
extern MODVAR void (*tkl_sync)(Client *client);
extern MODVAR void (*cmd_tkl)(Client *client, MessageTag *recv_mtags, int parc, char *parv[]);
extern MODVAR int (*place_host_ban)(Client *client, BanAction action, char *reason, long duration);
-extern MODVAR int (*match_spamfilter)(Client *client, char *str_in, int type, char *target, int flags, TKL **rettk);
+extern MODVAR int (*match_spamfilter)(Client *client, char *str_in, int type, char *cmd, char *target, int flags, TKL **rettk);
+extern MODVAR int (*match_spamfilter_mtags)(Client *client, MessageTag *mtags, char *cmd);
extern MODVAR int (*join_viruschan)(Client *client, TKL *tk, int type);
extern MODVAR unsigned char *(*StripColors)(unsigned char *text);
extern MODVAR const char *(*StripControlCodes)(unsigned char *text);
@@ -787,6 +812,7 @@ extern char *clean_ban_mask(char *, int, Client *);
extern int find_invex(Channel *channel, Client *client);
extern void DoMD5(char *mdout, const char *src, unsigned long n);
extern char *md5hash(char *dst, const char *src, unsigned long n);
+extern char *sha256hash(char *dst, const char *src, unsigned long n);
extern MODVAR TKL *tklines[TKLISTLEN];
extern MODVAR TKL *tklines_ip_hash[TKLIPHASHLEN1][TKLIPHASHLEN2];
extern char *cmdname_by_spamftarget(int target);
@@ -944,8 +970,12 @@ extern void free_message_tags(MessageTag *m);
extern time_t server_time_to_unix_time(const char *tbuf);
extern int history_set_limit(char *object, int max_lines, long max_t);
extern int history_add(char *object, MessageTag *mtags, char *line);
-extern int history_request(Client *acptr, char *object, HistoryFilter *filter);
+extern HistoryResult *history_request(char *object, HistoryFilter *filter);
extern int history_destroy(char *object);
+extern int can_receive_history(Client *client);
+extern void history_send_result(Client *client, HistoryResult *r);
+extern void free_history_result(HistoryResult *r);
+extern void free_history_filter(HistoryFilter *f);
extern void special_delayed_unloading(void);
extern int write_int64(FILE *fd, uint64_t t);
extern int write_int32(FILE *fd, uint32_t t);
@@ -1004,3 +1034,41 @@ extern NameValuePrioList *find_nvplist(NameValuePrioList *list, char *name);
extern void free_nvplist(NameValuePrioList *lst);
extern char *get_connect_extinfo(Client *client);
extern char *unreal_strftime(char *str);
+extern void strtolower_safe(char *dst, char *src, int size);
+extern int running_interactively(void);
+extern void skip_whitespace(char **p);
+extern void read_until(char **p, char *stopchars);
+/* src/unrealdb.c start */
+extern UnrealDB *unrealdb_open(const char *filename, UnrealDBMode mode, char *secret_block);
+extern int unrealdb_close(UnrealDB *c);
+extern char *unrealdb_test_db(const char *filename, char *secret_block);
+extern int unrealdb_write_int64(UnrealDB *c, uint64_t t);
+extern int unrealdb_write_int32(UnrealDB *c, uint32_t t);
+extern int unrealdb_write_int16(UnrealDB *c, uint16_t t);
+extern int unrealdb_write_str(UnrealDB *c, char *x);
+extern int unrealdb_write_char(UnrealDB *c, char t);
+extern int unrealdb_read_int64(UnrealDB *c, uint64_t *t);
+extern int unrealdb_read_int32(UnrealDB *c, uint32_t *t);
+extern int unrealdb_read_int16(UnrealDB *c, uint16_t *t);
+extern int unrealdb_read_str(UnrealDB *c, char **x);
+extern int unrealdb_read_char(UnrealDB *c, char *t);
+extern char *unrealdb_test_secret(char *name);
+extern UnrealDBConfig *unrealdb_copy_config(UnrealDBConfig *src);
+extern UnrealDBConfig *unrealdb_get_config(UnrealDB *db);
+extern void unrealdb_free_config(UnrealDBConfig *c);
+extern UnrealDBError unrealdb_get_error_code(void);
+extern char *unrealdb_get_error_string(void);
+/* src/unrealdb.c end */
+/* secret { } related stuff */
+extern Secret *find_secret(char *secret_name);
+extern void free_secret_cache(SecretCache *c);
+extern void free_secret(Secret *s);
+extern Secret *secrets;
+/* end */
+extern int check_password_strength(char *pass, int min_length, int strict, char **err);
+extern int valid_secret_password(char *pass, char **err);
+extern int flood_limit_exceeded(Client *client, FloodOption opt);
+extern FloodSettings *find_floodsettings_block(const char *name);
+extern FloodSettings *get_floodsettings_for_user(Client *client, FloodOption opt);
+extern MODVAR char *floodoption_names[];
+extern void flood_limit_exceeded_log(Client *client, char *floodname);
diff --git a/include/modules.h b/include/modules.h
@@ -241,14 +241,15 @@ typedef struct {
*
* This function pointer is NULL (unused) for modes without parameters.
* @param para The input parameter.
- * @param client The client that the mode request came from (can be NULL)
+ * @param client The client that the mode request came from (can be NULL!)
+ * @param channel The channel that the mode request came from (can be NULL!)
* @returns pointer to output string (temporary storage)
* @note The 'client' field will be NULL if for example called for set::modes-on-join.
- * @note You should probably not use 'client' in most cases.
+ * @note You should probably not use 'client' or 'channel' in most cases.
* In particular you MUST NOT SEND ERRORS to the client.
* This should be done in is_ok() and not in conv_param().
*/
- char *(*conv_param)(char *para, Client *client);
+ char *(*conv_param)(char *para, Client *client, Channel *channel);
/** Free and remove parameter from list.
* This function pointer is NULL (unused) for modes without parameters.
@@ -309,7 +310,7 @@ typedef struct {
int (*is_ok)(Client *,Channel *, char mode, char *para, int, int);
void * (*put_param)(void *, char *);
char * (*get_param)(void *);
- char * (*conv_param)(char *, Client *);
+ char * (*conv_param)(char *, Client *, Channel *);
void (*free_param)(void *);
void * (*dup_struct)(void *);
int (*sjoin_check)(Channel *, void *, void *);
@@ -486,11 +487,43 @@ typedef struct {
/** @} */
+/** Filter for history: the command / type of the request */
+typedef enum HistoryFilterCommand {
+ HFC_SIMPLE=1, /**< Simple history request for lines / unixtime */
+ HFC_BEFORE=2, /**< CHATHISTORY BEFORE */
+ HFC_AFTER=3, /**< CHATHISTORY AFTER */
+ HFC_LATEST=4, /**< CHATHISTORY LATEST */
+ HFC_AROUND=5, /**< CHATHISTORY AROUND */
+ HFC_BETWEEN=6 /**< CHATHISTORY BETWEEN */
+} HistoryFilterCommand;
+
/** Filter for history get requests */
typedef struct HistoryFilter HistoryFilter;
struct HistoryFilter {
- int last_lines;
- int last_seconds;
+ HistoryFilterCommand cmd; /**< Filter command, one of HistoryFilterCommand */
+ int last_lines; /**< Used by HFC_SIMPLE */
+ int last_seconds; /**< Used by HFC_SIMPLE */
+ char *timestamp_a; /**< First parameter of HFC_* (either this or msgid_a) */
+ char *msgid_a; /**< First parameter of HFC_* (either this or timestamp_a) */
+ char *timestamp_b; /**< Second parameter of HFC_BETWEEN (either this or msgid_b) */
+ char *msgid_b; /**< Second parameter of HFC_BETWEEN (either this or timestamp_b) */
+ int limit; /**< Maximum number of lines to return */
+};
+
+/** History log lines, used by HistoryResult among others */
+typedef struct HistoryLogLine HistoryLogLine;
+struct HistoryLogLine {
+ HistoryLogLine *prev, *next;
+ time_t t;
+ MessageTag *mtags;
+ char line[1];
+};
+
+typedef struct HistoryResult HistoryResult;
+struct HistoryResult {
+ char *object; /**< Name of the history object, eg '#test' */
+ HistoryLogLine *log; /**< The resulting log lines */
+ HistoryLogLine *log_tail; /**< Last entry in the log lines */
};
/** History Backend */
@@ -500,7 +533,7 @@ struct HistoryBackend {
char *name; /**< The name of the history backend (eg: "mem") */
int (*history_set_limit)(char *object, int max_lines, long max_time); /**< Impose a limit on a history object */
int (*history_add)(char *object, MessageTag *mtags, char *line); /**< Add to history */
- int (*history_request)(Client *acptr, char *object, HistoryFilter *filter); /**< Request history */
+ HistoryResult *(*history_request)(char *object, HistoryFilter *filter); /**< Request history */
int (*history_destroy)(char *object); /**< Destroy history of this object completely */
Module *owner; /**< Module introducing this */
char unloaded; /**< Internal flag to indicate module is being unloaded */
@@ -513,7 +546,7 @@ typedef struct {
char *name;
int (*history_set_limit)(char *object, int max_lines, long max_time);
int (*history_add)(char *object, MessageTag *mtags, char *line);
- int (*history_request)(Client *acptr, char *object, HistoryFilter *filter);
+ HistoryResult *(*history_request)(char *object, HistoryFilter *filter);
int (*history_destroy)(char *object);
} HistoryBackendInfo;
@@ -614,11 +647,12 @@ typedef struct ModuleObject {
extern unsigned int ModuleGetError(Module *module);
extern const char *ModuleGetErrorStr(Module *module);
extern unsigned int ModuleGetOptions(Module *module);
-extern unsigned int ModuleSetOptions(Module *module, unsigned int options, int action);
+extern void ModuleSetOptions(Module *module, unsigned int options, int action);
struct Module
{
struct Module *prev, *next;
+ int priority;
ModuleHeader *header; /* The module's header */
#ifdef _WIN32
HMODULE dll; /* Return value of LoadLibrary */
@@ -644,6 +678,7 @@ struct Module
#define MOD_OPT_OFFICIAL 0x0002 /* Official module, do not set "tainted" */
#define MOD_OPT_PERM_RELOADABLE 0x0004 /* Module is semi-permanent: it can be re-loaded but not un-loaded */
#define MOD_OPT_GLOBAL 0x0008 /* Module is required to be loaded globally (i.e. across the entire network) */
+#define MOD_OPT_UNLOAD_PRIORITY 0x1000 /* Module wants a higher or lower unload priority */
#define MOD_Dep(name, container,module) {#name, (vFP *) &container, module}
/** Event structs */
@@ -1223,17 +1258,19 @@ int hooktype_server_quit(Client *client, MessageTag *mtags);
/** Called when a local user changes the nick name (function prototype for HOOKTYPE_LOCAL_NICKCHANGE).
* @param client The client
+ * @param mtags Message tags associated with the event
* @param newnick The new nick name
* @return The return value is ignored (use return 0)
*/
-int hooktype_local_nickchange(Client *client, char *newnick);
+int hooktype_local_nickchange(Client *client, MessageTag *mtags, char *newnick);
/** Called when a remote user changes the nick name (function prototype for HOOKTYPE_REMOTE_NICKCHANGE).
* @param client The client
+ * @param mtags Message tags associated with the event
* @param newnick The new nick name
* @return The return value is ignored (use return 0)
*/
-int hooktype_remote_nickchange(Client *client, char *newnick);
+int hooktype_remote_nickchange(Client *client, MessageTag *mtags, char *newnick);
/** Called when a user wants to join a channel, may the user join? (function prototype for HOOKTYPE_CAN_JOIN).
* @param client The client
@@ -2222,8 +2259,9 @@ enum EfunctionType {
EFUNC_TKL_SYNCH,
EFUNC_CMD_TKL,
EFUNC_PLACE_HOST_BAN,
- EFUNC_DOSPAMFILTER,
- EFUNC_DOSPAMFILTER_VIRUSCHAN,
+ EFUNC_MATCH_SPAMFILTER,
+ EFUNC_MATCH_SPAMFILTER_MTAGS,
+ EFUNC_JOIN_VIRUSCHAN,
EFUNC_FIND_TKLINE_MATCH_ZAP_EX,
EFUNC_SEND_LIST,
EFUNC_STRIPCOLORS,
@@ -2308,6 +2346,7 @@ enum EfunctionType {
#define CONFIG_REQUIRE 9
#define CONFIG_LISTEN 10
#define CONFIG_LISTEN_OPTIONS 11
+#define CONFIG_SET_HISTORY_CHANNEL 12
#define MOD_HEADER Mod_Header
#define MOD_TEST() DLLFUNC int Mod_Test(ModuleInfo *modinfo)
diff --git a/include/setup.h.in b/include/setup.h.in
@@ -142,6 +142,12 @@
/* Define the location of the modules */
#undef MODULESDIR
+/* machine is bigendian */
+#undef NATIVE_BIG_ENDIAN
+
+/* machine is littleendian */
+#undef NATIVE_LITTLE_ENDIAN
+
/* Set to the nickname history length you want */
#undef NICKNAMEHISTORYLENGTH
@@ -226,3 +232,15 @@
/* Define if you have libcurl installed to get remote includes and MOTD
support */
#undef USE_LIBCURL
+
+/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most
+ significant byte first (like Motorola and SPARC, unlike Intel). */
+#if defined AC_APPLE_UNIVERSAL_BUILD
+# if defined __BIG_ENDIAN__
+# define WORDS_BIGENDIAN 1
+# endif
+#else
+# ifndef WORDS_BIGENDIAN
+# undef WORDS_BIGENDIAN
+# endif
+#endif
diff --git a/include/struct.h b/include/struct.h
@@ -109,6 +109,7 @@ typedef struct ConfigItem_blacklist_module ConfigItem_blacklist_module;
typedef struct ConfigItem_help ConfigItem_help;
typedef struct ConfigItem_offchans ConfigItem_offchans;
typedef struct SecurityGroup SecurityGroup;
+typedef struct Secret Secret;
typedef struct ListStruct ListStruct;
typedef struct ListStructPrio ListStructPrio;
@@ -120,7 +121,7 @@ typedef struct Watch Watch;
typedef struct Client Client;
typedef struct LocalClient LocalClient;
typedef struct Channel Channel;
-typedef struct User ClientUser;
+typedef struct User User;
typedef struct Server Server;
typedef struct Link Link;
typedef struct Ban Ban;
@@ -211,7 +212,7 @@ typedef OperPermission (*OperClassEntryEvalCallback)(OperClassACLEntryVar* varia
#define LOG_CHGCMDS 0x0100
#define LOG_OVERRIDE 0x0200
#define LOG_SPAMFILTER 0x0400
-#define LOG_DBG 0x0800 /* fixme */
+#define LOG_FLOOD 0x0800
/*
** 'offsetof' is defined in ANSI-C. The following definition
@@ -240,6 +241,14 @@ typedef OperPermission (*OperClassEntryEvalCallback)(OperClassACLEntryVar* varia
*/
#define SIPHASH_KEY_LENGTH 16
+/** The length of a standard 'msgid' tag (note that special
+ * msgid tags will be longer).
+ * The 22 alphanumeric characters provide slightly more
+ * than 128 bits of randomness (62^22 > 2^128).
+ * See mtag_add_or_inherit_msgid() for more information.
+ */
+#define MSGIDLEN 22
+
/** This specifies the current client status or the client type - see @link ClientStatus @endlink in particular.
* You may think "server" or "client" are the only choices here, but there are many more
* such as states where the user is in the middle of an SSL/TLS handshake.
@@ -661,6 +670,7 @@ struct ListStructPrio {
CHECK_PRIO_LIST_ENTRY(list) \
CHECK_PRIO_LIST_ENTRY(item) \
CHECK_NULL_LIST_ITEM(item) \
+ item->priority = prio; \
add_ListItemPrio((ListStructPrio *)item, (ListStructPrio **)&list, prio); \
} while(0)
@@ -754,6 +764,7 @@ struct LoopStruct {
unsigned do_bancheck_spamf_user : 1; /* perform 'user' spamfilter bancheck */
unsigned do_bancheck_spamf_away : 1; /* perform 'away' spamfilter bancheck */
unsigned ircd_rehashing : 1;
+ unsigned ircd_terminating : 1;
unsigned tainted : 1;
Client *rehash_save_cptr, *rehash_save_client;
int rehash_save_sig;
@@ -862,6 +873,97 @@ typedef void (*CmdFunc)(Client *client, MessageTag *mtags, int parc, char *parv[
typedef void (*AliasCmdFunc)(Client *client, MessageTag *mtags, int parc, char *parv[], char *cmd);
typedef void (*OverrideCmdFunc)(CommandOverride *ovr, Client *client, MessageTag *mtags, int parc, char *parv[]);
+#include <sodium.h>
+
+/* This is the 'chunk size', the size of encryption blocks.
+ * We choose 4K here since that is a decent amount as of 2021 and
+ * more would not benefit performance anyway.
+ * Note that you cannot change this value easily afterwards
+ * (you cannot read files with a different chunk size).
+ */
+#define UNREALDB_CRYPT_FILE_CHUNK_SIZE 4096
+
+/** The salt length. Don't change. */
+#define UNREALDB_SALT_LEN 16
+
+/** Database modes of operation (read or write)
+ * @ingroup UnrealDBFunctions
+ */
+typedef enum UnrealDBMode {
+ UNREALDB_MODE_READ = 0,
+ UNREALDB_MODE_WRITE = 1
+} UnrealDBMode;
+
+typedef enum UnrealDBCipher {
+ UNREALDB_CIPHER_XCHACHA20 = 0x0001
+} UnrealDBCipher;
+
+typedef enum UnrealDBKDF {
+ UNREALDB_KDF_ARGON2ID = 0x0001
+} UnrealDBKDF;
+
+/** Database configuration for a particular file */
+typedef struct UnrealDBConfig {
+ uint16_t kdf; /**< Key derivation function (always 0x01) */
+ uint16_t t_cost; /**< Time cost (number of rounds) */
+ uint16_t m_cost; /**< Memory cost (in number of bitshifts, eg 15 means 1<<15=32M) */
+ uint16_t p_cost; /**< Parallel cost (number of concurrent threads) */
+ uint16_t saltlen; /**< Length of the salt (normally UNREALDB_SALT_LEN) */
+ char *salt; /**< Salt */
+ uint16_t cipher; /**< Encryption cipher (always 0x01) */
+ uint16_t keylen; /**< Key length */
+ char *key; /**< The key used for encryption/decryption */
+} UnrealDBConfig;
+
+/** Error codes returned by @ref UnrealDBFunctions
+ * @ingroup UnrealDBFunctions
+ */
+typedef enum UnrealDBError {
+ UNREALDB_ERROR_SUCCESS = 0, /**< Success, not an error */
+ UNREALDB_ERROR_FILENOTFOUND = 1, /**< File does not exist */
+ UNREALDB_ERROR_CRYPTED = 2, /**< File is crypted but no password provided */
+ UNREALDB_ERROR_NOTCRYPTED = 3, /**< File is not crypted and a password was provided */
+ UNREALDB_ERROR_HEADER = 4, /**< Header is corrupt, invalid or unknown format */
+ UNREALDB_ERROR_SECRET = 5, /**< Invalid secret { } block provided - either does not exist or does not meet requirements */
+ UNREALDB_ERROR_PASSWORD = 6, /**< Invalid password provided */
+ UNREALDB_ERROR_IO = 7, /**< I/O error */
+ UNREALDB_ERROR_API = 8, /**< API call violation, eg requesting to write on a file opened for reading */
+ UNREALDB_ERROR_INTERNAL = 9, /**< Internal error, eg crypto routine returned something unexpected */
+} UnrealDBError;
+
+/** Database handle
+ * This is returned by unrealdb_open() and used by all other @ref UnrealDBFunctions
+ * @ingroup UnrealDBFunctions
+ */
+typedef struct UnrealDB {
+ FILE *fd; /**< File descriptor */
+ UnrealDBMode mode; /**< UNREALDB_MODE_READ / UNREALDB_MODE_WRITE */
+ int crypted; /**< Are we doing any encryption or just plaintext? */
+ uint64_t creationtime; /**< When this file was created/updates */
+ crypto_secretstream_xchacha20poly1305_state st; /**< Internal state for crypto engine */
+ char buf[UNREALDB_CRYPT_FILE_CHUNK_SIZE]; /**< Buffer used for reading/writing */
+ int buflen; /**< Length of current data in buffer */
+ UnrealDBError error_code; /**< Last error code. Whenever this happens we will set this, never overwrite, and block further I/O */
+ char *error_string; /**< Error string upon failure */
+ UnrealDBConfig *config; /**< Config */
+} UnrealDB;
+
+/** Used for speeding up reading/writing of DBs (so we don't have to run argon2 repeatedly) */
+typedef struct SecretCache SecretCache;
+struct SecretCache {
+ SecretCache *prev, *next;
+ UnrealDBConfig *config;
+ time_t cache_hit;
+};
+
+/** Used for storing secret { } blocks */
+struct Secret {
+ Secret *prev, *next;
+ char *name;
+ char *password;
+ SecretCache *cache;
+};
+
/* tkl:
* TKL_KILL|TKL_GLOBAL = Global K-Line (GLINE)
@@ -898,12 +1000,13 @@ typedef void (*OverrideCmdFunc)(CommandOverride *ovr, Client *client, MessageTag
#define SPAMF_USERMSG 0x0002 /* p */
#define SPAMF_USERNOTICE 0x0004 /* n */
#define SPAMF_CHANNOTICE 0x0008 /* N */
-#define SPAMF_PART 0x0010 /* P */
-#define SPAMF_QUIT 0x0020 /* q */
-#define SPAMF_DCC 0x0040 /* d */
-#define SPAMF_USER 0x0080 /* u */
-#define SPAMF_AWAY 0x0100 /* a */
-#define SPAMF_TOPIC 0x0200 /* t */
+#define SPAMF_PART 0x0010 /* P */
+#define SPAMF_QUIT 0x0020 /* q */
+#define SPAMF_DCC 0x0040 /* d */
+#define SPAMF_USER 0x0080 /* u */
+#define SPAMF_AWAY 0x0100 /* a */
+#define SPAMF_TOPIC 0x0200 /* t */
+#define SPAMF_MTAG 0x0400 /* m */
/* Other flags only for function calls: */
#define SPAMFLAG_NOWARN 0x0001
@@ -1107,6 +1210,25 @@ extern void unload_all_unused_moddata(void);
#define TLSFLAG_NOSTARTTLS 0x8
#define TLSFLAG_DISABLECLIENTCERT 0x10
+/** Flood counters for local clients */
+typedef struct FloodCounter {
+ int count;
+ long t;
+} FloodCounter;
+
+/** This is the list of different flood counters that we keep for local clients. */
+/* IMPORTANT: If you change this, update floodoption_names[] in src/user.c too !!!!!!!!!!!! */
+typedef enum FloodOption {
+ FLD_NICK = 0, /**< nick-flood */
+ FLD_JOIN = 1, /**< join-flood */
+ FLD_AWAY = 2, /**< away-flood */
+ FLD_INVITE = 3, /**< invite-flood */
+ FLD_KNOCK = 4, /**< knock-flood */
+ FLD_CONVERSATIONS = 5, /**< max-concurrent-conversations */
+} FloodOption;
+#define MAXFLOODOPTIONS 10
+
+
/** This shows the Client struct (any client), the User struct (a user), Server (a server) that are commonly accessed both in the core and by 3rd party coders.
* @defgroup CommonStructs Common structs
* @{
@@ -1119,7 +1241,7 @@ struct Client {
struct list_head lclient_node; /**< For local client list (lclient_list) */
struct list_head special_node; /**< For special lists (server || unknown || oper) */
LocalClient *local; /**< Additional information regarding locally connected clients */
- ClientUser *user; /**< Additional information, if this client is a user */
+ User *user; /**< Additional information, if this client is a user */
Server *serv; /**< Additional information, if this is a server */
ClientStatus status; /**< Client status, one of CLIENT_STATUS_* */
struct list_head client_hash; /**< For name hash table (clientTable) */
@@ -1186,6 +1308,7 @@ struct LocalClient {
struct hostent *hostp; /**< Host record for this client (used by DNS code) */
char sockhost[HOSTLEN + 1]; /**< Hostname from the socket */
u_short port; /**< Remote TCP port of client */
+ FloodCounter flood[MAXFLOODOPTIONS];
};
/** User information (persons, not servers), you use client->user to access these (see also @link Client @endlink).
@@ -1208,11 +1331,9 @@ struct User {
char *operlogin; /**< Which oper { } block was used to oper up, otherwise NULL - used by oper::maxlogins */
struct {
time_t nick_t; /**< For set::anti-flood::nick-flood: time */
- time_t away_t; /**< For set::anti-flood::away-flood: time */
time_t knock_t; /**< For set::anti-flood::knock-flood: time */
time_t invite_t; /**< For set::anti-flood::invite-flood: time */
unsigned char nick_c; /**< For set::anti-flood::nick-flood: counter */
- unsigned char away_c; /**< For set::anti-flood::away-flood: counter */
unsigned char knock_c; /**< For set::anti-flood::knock-flood: counter */
unsigned char invite_c; /**< For set::anti-flood::invite-flood: counter */
} flood; /**< Anti-flood counters */
diff --git a/include/sys.h b/include/sys.h
@@ -224,4 +224,15 @@ extern char OSName[256];
# endif
#endif
+#ifdef NATIVE_BIG_ENDIAN
+ #if defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__)
+ #include <sys/endian.h>
+ #define bswap_64 bswap64
+ #define bswap_32 bswap32
+ #define bswap_16 bswap16
+ #else
+ #include <byteswap.h>
+ #endif
+#endif
+
#endif /* __sys_include__ */
diff --git a/include/unrealircd.h b/include/unrealircd.h
@@ -26,6 +26,7 @@
#else
#include <sys/resource.h>
#include <utime.h>
+ #include <dirent.h>
#endif
#include <fcntl.h>
#include <signal.h>
@@ -35,3 +36,4 @@
#ifdef USE_LIBCURL
#include <curl/curl.h>
#endif
+#include <argon2.h>
diff --git a/include/version.h b/include/version.h
@@ -54,7 +54,7 @@
* Can be useful if the above 3 versionids are insufficient for you (eg: you want to support CVS).
* This is updated automatically on the CVS server every Monday. so don't touch it.
*/
-#define UNREAL_VERSION_TIME 202001
+#define UNREAL_VERSION_TIME 202120
#define UnrealProtocol 5002
#define PATCH1 macro_to_str(UNREAL_VERSION_GENERATION)
diff --git a/include/windows/setup.h b/include/windows/setup.h
@@ -60,13 +60,13 @@
#define UNREAL_VERSION_GENERATION 5
/* Major version number (e.g.: 2 for Unreal3.2*) */
-#define UNREAL_VERSION_MAJOR 0
+#define UNREAL_VERSION_MAJOR 2
/* Minor version number (e.g.: 1 for Unreal3.2.1) */
-#define UNREAL_VERSION_MINOR 9
+#define UNREAL_VERSION_MINOR 0
/* Version suffix such as a beta marker or release candidate marker. (e.g.:
-rcX for unrealircd-3.2.9-rcX) */
-#define UNREAL_VERSION_SUFFIX ""
+#define UNREAL_VERSION_SUFFIX ".1"
#endif
diff --git a/src/Makefile.in b/src/Makefile.in
@@ -31,7 +31,7 @@ OBJS=dns.o auth.o channel.o crule.o dbuf.o \
api-moddata.o api-extban.o api-isupport.o api-command.o \
api-clicap.o api-messagetag.o api-history-backend.o api-efunctions.o \
api-event.o \
- crypt_blowfish.o updconf.o crashreport.o modulemanager.o \
+ crypt_blowfish.o unrealdb.o updconf.o crashreport.o modulemanager.o \
utf8.o \
openssl_hostname_validation.o $(URL)
@@ -224,6 +224,9 @@ api-efunctions.o: api-efunctions.c $(INCLUDES)
crypt_blowfish.o: crypt_blowfish.c $(INCLUDES)
$(CC) $(CFLAGS) $(BINCFLAGS) -c crypt_blowfish.c
+unrealdb.o: unrealdb.c $(INCLUDES)
+ $(CC) $(CFLAGS) $(BINCFLAGS) -c unrealdb.c
+
updconf.o: updconf.c $(INCLUDES)
$(CC) $(CFLAGS) $(BINCFLAGS) -c updconf.c
diff --git a/src/aliases.c b/src/aliases.c
@@ -66,7 +66,7 @@ void cmd_alias(Client *client, MessageTag *mtags, int parc, char *parv[], char *
{
if (SERVICES_NAME && (acptr = find_person(alias->nick, NULL)))
{
- if (alias->spamfilter && match_spamfilter(client, parv[1], SPAMF_USERMSG, alias->nick, 0, NULL))
+ if (alias->spamfilter && match_spamfilter(client, parv[1], SPAMF_USERMSG, cmd, alias->nick, 0, NULL))
return;
sendto_one(acptr, NULL, ":%s PRIVMSG %s@%s :%s", client->name,
alias->nick, SERVICES_NAME, parv[1]);
@@ -78,7 +78,7 @@ void cmd_alias(Client *client, MessageTag *mtags, int parc, char *parv[], char *
{
if (STATS_SERVER && (acptr = find_person(alias->nick, NULL)))
{
- if (alias->spamfilter && match_spamfilter(client, parv[1], SPAMF_USERMSG, alias->nick, 0, NULL))
+ if (alias->spamfilter && match_spamfilter(client, parv[1], SPAMF_USERMSG, cmd, alias->nick, 0, NULL))
return;
sendto_one(acptr, NULL, ":%s PRIVMSG %s@%s :%s", client->name,
alias->nick, STATS_SERVER, parv[1]);
@@ -90,7 +90,7 @@ void cmd_alias(Client *client, MessageTag *mtags, int parc, char *parv[], char *
{
if ((acptr = find_person(alias->nick, NULL)))
{
- if (alias->spamfilter && match_spamfilter(client, parv[1], SPAMF_USERMSG, alias->nick, 0, NULL))
+ if (alias->spamfilter && match_spamfilter(client, parv[1], SPAMF_USERMSG, cmd, alias->nick, 0, NULL))
return;
if (MyUser(acptr))
sendto_one(acptr, NULL, ":%s!%s@%s PRIVMSG %s :%s", client->name,
@@ -112,7 +112,7 @@ void cmd_alias(Client *client, MessageTag *mtags, int parc, char *parv[], char *
char *errmsg = NULL;
if (can_send_to_channel(client, channel, &msg, &errmsg, 0))
{
- if (alias->spamfilter && match_spamfilter(client, parv[1], SPAMF_CHANMSG, channel->chname, 0, NULL))
+ if (alias->spamfilter && match_spamfilter(client, parv[1], SPAMF_CHANMSG, cmd, channel->chname, 0, NULL))
return;
new_message(client, NULL, &mtags);
sendto_channel(channel, client, client->direction,
@@ -203,7 +203,7 @@ void cmd_alias(Client *client, MessageTag *mtags, int parc, char *parv[], char *
{
if (SERVICES_NAME && (acptr = find_person(format->nick, NULL)))
{
- if (alias->spamfilter && match_spamfilter(client, output, SPAMF_USERMSG, format->nick, 0, NULL))
+ if (alias->spamfilter && match_spamfilter(client, output, SPAMF_USERMSG, cmd, format->nick, 0, NULL))
return;
sendto_one(acptr, NULL, ":%s PRIVMSG %s@%s :%s", client->name,
format->nick, SERVICES_NAME, output);
@@ -214,7 +214,7 @@ void cmd_alias(Client *client, MessageTag *mtags, int parc, char *parv[], char *
{
if (STATS_SERVER && (acptr = find_person(format->nick, NULL)))
{
- if (alias->spamfilter && match_spamfilter(client, output, SPAMF_USERMSG, format->nick, 0, NULL))
+ if (alias->spamfilter && match_spamfilter(client, output, SPAMF_USERMSG, cmd, format->nick, 0, NULL))
return;
sendto_one(acptr, NULL, ":%s PRIVMSG %s@%s :%s", client->name,
format->nick, STATS_SERVER, output);
@@ -225,7 +225,7 @@ void cmd_alias(Client *client, MessageTag *mtags, int parc, char *parv[], char *
{
if ((acptr = find_person(format->nick, NULL)))
{
- if (alias->spamfilter && match_spamfilter(client, output, SPAMF_USERMSG, format->nick, 0, NULL))
+ if (alias->spamfilter && match_spamfilter(client, output, SPAMF_USERMSG, cmd, format->nick, 0, NULL))
return;
if (MyUser(acptr))
sendto_one(acptr, NULL, ":%s!%s@%s PRIVMSG %s :%s", client->name,
@@ -247,7 +247,7 @@ void cmd_alias(Client *client, MessageTag *mtags, int parc, char *parv[], char *
char *errmsg = NULL;
if (!can_send_to_channel(client, channel, &msg, &errmsg, 0))
{
- if (alias->spamfilter && match_spamfilter(client, output, SPAMF_CHANMSG, channel->chname, 0, NULL))
+ if (alias->spamfilter && match_spamfilter(client, output, SPAMF_CHANMSG, cmd, channel->chname, 0, NULL))
return;
new_message(client, NULL, &mtags);
sendto_channel(channel, client, client->direction,
diff --git a/src/api-efunctions.c b/src/api-efunctions.c
@@ -67,7 +67,8 @@ void (*tkl_stats)(Client *client, int type, char *para, int *cnt);
void (*tkl_sync)(Client *client);
void (*cmd_tkl)(Client *client, MessageTag *mtags, int parc, char *parv[]);
int (*place_host_ban)(Client *client, BanAction action, char *reason, long duration);
-int (*match_spamfilter)(Client *client, char *str_in, int type, char *target, int flags, TKL **rettk);
+int (*match_spamfilter)(Client *client, char *str_in, int type, char *cmd, char *target, int flags, TKL **rettk);
+int (*match_spamfilter_mtags)(Client *client, MessageTag *mtags, char *cmd);
int (*join_viruschan)(Client *client, TKL *tk, int type);
unsigned char *(*StripColors)(unsigned char *text);
const char *(*StripControlCodes)(unsigned char *text);
@@ -306,8 +307,9 @@ void efunctions_init(void)
efunc_init_function(EFUNC_TKL_SYNCH, tkl_sync, NULL);
efunc_init_function(EFUNC_CMD_TKL, cmd_tkl, NULL);
efunc_init_function(EFUNC_PLACE_HOST_BAN, place_host_ban, NULL);
- efunc_init_function(EFUNC_DOSPAMFILTER, match_spamfilter, NULL);
- efunc_init_function(EFUNC_DOSPAMFILTER_VIRUSCHAN, join_viruschan, NULL);
+ efunc_init_function(EFUNC_MATCH_SPAMFILTER, match_spamfilter, NULL);
+ efunc_init_function(EFUNC_MATCH_SPAMFILTER_MTAGS, match_spamfilter_mtags, NULL);
+ efunc_init_function(EFUNC_JOIN_VIRUSCHAN, join_viruschan, NULL);
efunc_init_function(EFUNC_STRIPCOLORS, StripColors, NULL);
efunc_init_function(EFUNC_STRIPCONTROLCODES, StripControlCodes, NULL);
efunc_init_function(EFUNC_SPAMFILTER_BUILD_USER_STRING, spamfilter_build_user_string, NULL);
diff --git a/src/api-event.c b/src/api-event.c
@@ -28,6 +28,7 @@ ID_Copyright("(C) Carsten Munk 2001");
MODVAR Event *events = NULL;
extern EVENT(unrealdns_removeoldrecords);
+extern EVENT(unrealdb_expire_secret_cache);
/** Add an event, a function that will run at regular intervals.
* @param module Module that this event belongs to
@@ -241,4 +242,5 @@ void SetupEvents(void)
EventAdd(NULL, "handshake_timeout", handshake_timeout, NULL, 1000, 0);
EventAdd(NULL, "try_connections", try_connections, NULL, 2000, 0);
EventAdd(NULL, "tls_check_expiry", tls_check_expiry, NULL, (86400/2)*1000, 0);
+ EventAdd(NULL, "unrealdb_expire_secret_cache", unrealdb_expire_secret_cache, NULL, 61000, 0);
}
diff --git a/src/api-history-backend.c b/src/api-history-backend.c
@@ -166,14 +166,21 @@ int history_add(char *object, MessageTag *mtags, char *line)
return 1;
}
-int history_request(Client *client, char *object, HistoryFilter *filter)
+HistoryResult *history_request(char *object, HistoryFilter *filter)
{
- HistoryBackend *hb;
+ HistoryBackend *hb = historybackends;
+ HistoryResult *r;
+ HistoryLogLine *l;
- for (hb = historybackends; hb; hb=hb->next)
- hb->history_request(client, object, filter);
+ if (!hb)
+ return 0; /* no history backend loaded */
- return 1;
+ /* Right now we return whenever the first backend has a result. */
+ for (hb = historybackends; hb; hb = hb->next)
+ if ((r = hb->history_request(object, filter)))
+ return r;
+
+ return NULL;
}
int history_destroy(char *object)
@@ -195,3 +202,83 @@ int history_set_limit(char *object, int max_lines, long max_t)
return 1;
}
+
+/** Free a HistoryResult object that was returned from request_result() earlier */
+void free_history_result(HistoryResult *r)
+{
+ HistoryLogLine *l, *l_next;
+ for (l = r->log; l; l = l_next)
+ {
+ l_next = l->next;
+ free_message_tags(l->mtags);
+ safe_free(l);
+ }
+ safe_free(r->object);
+ safe_free(r);
+}
+
+/** Returns 1 if the client can receive channel history, 0 if not.
+ * @param client The client to check.
+ * @note It is recommend to call this function BEFORE trying to
+ * retrieve channel history via history_request(),
+ * as to not waste useless resources.
+ */
+int can_receive_history(Client *client)
+{
+ if (HasCapability(client, "server-time"))
+ return 1;
+ return 0;
+}
+
+static void history_send_result_line(Client *client, HistoryLogLine *l, char *batchid)
+{
+ if (BadPtr(batchid))
+ {
+ sendto_one(client, l->mtags, "%s", l->line);
+ } else {
+ MessageTag *m = safe_alloc(sizeof(MessageTag));
+ m->name = "batch";
+ m->value = batchid;
+ AddListItem(m, l->mtags);
+ sendto_one(client, l->mtags, "%s", l->line);
+ DelListItem(m, l->mtags);
+ safe_free(m);
+ }
+}
+
+/** Send the result of a history_request() to the client.
+ * @param client The client to send to.
+ * @param r The history result retrieved via history_request().
+ */
+void history_send_result(Client *client, HistoryResult *r)
+{
+ char batch[BATCHLEN+1];
+ HistoryLogLine *l;
+
+ if (!can_receive_history(client))
+ return;
+
+ batch[0] = '\0';
+ if (HasCapability(client, "batch"))
+ {
+ /* Start a new batch */
+ generate_batch_id(batch);
+ sendto_one(client, NULL, ":%s BATCH +%s chathistory %s", me.name, batch, r->object);
+ }
+
+ for (l = r->log; l; l = l->next)
+ history_send_result_line(client, l, batch);
+
+ /* End of batch */
+ if (*batch)
+ sendto_one(client, NULL, ":%s BATCH -%s", me.name, batch);
+}
+
+void free_history_filter(HistoryFilter *f)
+{
+ safe_free(f->timestamp_a);
+ safe_free(f->msgid_a);
+ safe_free(f->timestamp_b);
+ safe_free(f->msgid_b);
+ safe_free(f);
+}
diff --git a/src/auth.c b/src/auth.c
@@ -20,7 +20,6 @@
#include "unrealircd.h"
#include "crypt_blowfish.h"
-#include <argon2.h>
typedef struct AuthTypeList AuthTypeList;
struct AuthTypeList {
diff --git a/src/conf.c b/src/conf.c
@@ -71,6 +71,7 @@ static int _conf_help (ConfigFile *conf, ConfigEntry *ce);
static int _conf_offchans (ConfigFile *conf, ConfigEntry *ce);
static int _conf_sni (ConfigFile *conf, ConfigEntry *ce);
static int _conf_security_group (ConfigFile *conf, ConfigEntry *ce);
+static int _conf_secret (ConfigFile *conf, ConfigEntry *ce);
/*
* Validation commands
@@ -104,6 +105,7 @@ static int _test_help (ConfigFile *conf, ConfigEntry *ce);
static int _test_offchans (ConfigFile *conf, ConfigEntry *ce);
static int _test_sni (ConfigFile *conf, ConfigEntry *ce);
static int _test_security_group (ConfigFile *conf, ConfigEntry *ce);
+static int _test_secret (ConfigFile *conf, ConfigEntry *ce);
/* This MUST be alphabetized */
static ConfigCommand _ConfigCommands[] = {
@@ -128,6 +130,7 @@ static ConfigCommand _ConfigCommands[] = {
{ "oper", _conf_oper, _test_oper },
{ "operclass", _conf_operclass, _test_operclass },
{ "require", _conf_require, _test_require },
+ { "secret", _conf_secret, _test_secret },
{ "security-group", _conf_security_group, _test_security_group },
{ "set", _conf_set, _test_set },
{ "sni", _conf_sni, _test_sni },
@@ -160,6 +163,7 @@ static NameValue _LogFlags[] = {
{ LOG_CHGCMDS, "chg-commands" },
{ LOG_CLIENT, "connects" },
{ LOG_ERROR, "errors" },
+ { LOG_FLOOD, "flood" },
{ LOG_KILL, "kills" },
{ LOG_KLINE, "kline" },
{ LOG_OPER, "oper" },
@@ -258,6 +262,7 @@ ConfigItem_blacklist_module *conf_blacklist_module = NULL;
ConfigItem_help *conf_help = NULL;
ConfigItem_offchans *conf_offchans = NULL;
SecurityGroup *securitygroups = NULL;
+Secret *secrets = NULL;
MODVAR Configuration iConf;
MODVAR Configuration tempiConf;
@@ -358,6 +363,128 @@ char *x;
return 1;
}
+/** Find an anti-flood settings block by name.
+ * @param name The name of the set::anti-flood block
+ * @returns The FloodSettings block if found, or NULL if not found.
+ */
+FloodSettings *find_floodsettings_block_ex(Configuration *conf, const char *name)
+{
+ FloodSettings *f;
+
+ for (f = conf->floodsettings; f; f = f->next)
+ if (!strcmp(f->name, name))
+ return f;
+
+ return NULL;
+}
+
+/** Find an anti-flood settings block by name.
+ * @param name The name of the set::anti-flood block
+ * @returns The FloodSettings block if found, or NULL if not found.
+ */
+FloodSettings *find_floodsettings_block(const char *name)
+{
+ return find_floodsettings_block_ex(&iConf, name);
+}
+
+/** Check if 'name' is in the array 'list'.
+ * @param name The name to check
+ * @param list The char *list[] with the list of valid names.
+ * @returns 1 if found, 0 if not
+ * @note The array in list must end in a NULL element!
+ */
+int text_in_array(const char *name, const char *list[])
+{
+ int i;
+
+ for (i=0; list[i]; i++)
+ if (!strcmp(name, list[i]))
+ return 1;
+
+ return 0; /* Not found */
+}
+
+int flood_option_is_old(const char *name)
+{
+ const char *opts[] =
+ {
+ "max-concurrent-conversations",
+ "unknown-flood-amount",
+ "unknown-flood-bantime",
+ "handshake-data-flood",
+ "away-count",
+ "away-period",
+ "away-flood",
+ "nick-flood",
+ "join-flood",
+ "invite-flood",
+ "knock-flood",
+ "connect-flood",
+ "target-flood",
+ NULL
+ };
+
+ return text_in_array(name, opts);
+}
+
+int flood_option_is_for_everyone(const char *name)
+{
+ const char *opts[] =
+ {
+ "connect-flood",
+ "handshake-data-flood",
+ "unknown-flood",
+ "target-flood",
+ NULL
+ };
+
+ return text_in_array(name, opts);
+}
+
+
+/** Parses a value like '5:60s' into a flood setting that we can store.
+ * @param str The string to parse (eg: '5:60s')
+ * @param settings The FloodSettings block to store the result in
+ * @param opt The option (eg: FLD_AWAY)
+ * @returns 1 if OK, 0 for parse error.
+ */
+int config_parse_flood_generic(const char *str, Configuration *conf, char *blockname, FloodOption opt)
+{
+ char buf[64], *p;
+ FloodSettings *settings = find_floodsettings_block_ex(conf, blockname);
+
+ /* Create a new anti-flood block if it doesn't exist */
+ if (!settings)
+ {
+ settings = safe_alloc(sizeof(FloodSettings));
+ safe_strdup(settings->name, blockname);
+ AddListItem(settings, conf->floodsettings);
+ }
+
+ if (!strcmp(str, "unlimited") || !strcmp(str, "max"))
+ {
+ settings->limit[opt] = -1;
+ settings->period[opt] = 0;
+ return 1;
+ }
+
+ /* Work on a copy so we don't destroy 'str' */
+ strlcpy(buf, str, sizeof(buf));
+
+ p = strchr(buf, ':');
+
+ /* 'blah', ':blah', '1:' */
+ if (!p || (p == buf) || (*(p+1) == '\0'))
+ return 0;
+
+ *p++ = '\0';
+
+ settings->limit[opt] = atoi(buf);
+ settings->period[opt] = config_checkval(p, CFG_TIME);
+
+ return 1;
+}
+
long config_checkval(char *orig, unsigned short flags) {
char *value = raw_strdup(orig);
char *text;
@@ -536,7 +663,7 @@ void conf_channelmodes(char *modes, struct ChMode *store, int warn)
{
if (!param)
break;
- param = Channelmode_Table[i].conv_param(param, NULL);
+ param = Channelmode_Table[i].conv_param(param, NULL, NULL);
if (!param)
break; /* invalid parameter fmt, do not set mode. */
store->extparams[i] = raw_strdup(param);
@@ -1625,17 +1752,9 @@ void config_setdefaultsettings(Configuration *i)
{
char tmp[512];
- i->handshake_data_flood_amount = 4096;
- i->handshake_data_flood_ban_action = BAN_ACT_ZLINE;
- i->handshake_data_flood_ban_time = 600;
safe_strdup(i->oper_snomask, SNO_DEFOPER);
i->ident_read_timeout = 7;
i->ident_connect_timeout = 3;
- i->nick_count = 3; i->nick_period = 60; /* NICK flood protection: max 3 per 60s */
- i->away_count = 4; i->away_period = 120; /* AWAY flood protection: max 4 per 120s */
- i->invite_count = 4; i->invite_period = 60; /* INVITE flood protection: max 4 per 60s */
- i->knock_count = 4; i->knock_period = 120; /* KNOCK protection: max 4 per 120s */
- i->throttle_count = 3; i->throttle_period = 60; /* throttle protection: max 3 per 60s */
i->ban_version_tkl_time = 86400; /* 1d */
i->spamfilter_ban_time = 86400; /* 1d */
safe_strdup(i->spamfilter_ban_reason, "Spam/advertising");
@@ -1674,6 +1793,28 @@ void config_setdefaultsettings(Configuration *i)
i->handshake_delay = -1;
i->broadcast_channel_messages = BROADCAST_CHANNEL_MESSAGES_AUTO;
+ /* Flood options */
+ /* - everyone */
+ i->throttle_count = 3; i->throttle_period = 60; /* throttle protection: max 3 per 60s */
+ i->handshake_data_flood_amount = 4096;
+ i->handshake_data_flood_ban_action = BAN_ACT_ZLINE;
+ i->handshake_data_flood_ban_time = 600;
+ // (targetflood is in the targetflood module)
+ /* - known-users */
+ config_parse_flood_generic("3:60", i, "known-users", FLD_NICK); /* NICK flood protection: max 3 per 60s */
+ config_parse_flood_generic("3:90", i, "known-users", FLD_JOIN); /* JOIN flood protection: max 3 per 90s */
+ config_parse_flood_generic("4:120", i, "known-users", FLD_AWAY); /* AWAY flood protection: max 4 per 120s */
+ config_parse_flood_generic("4:60", i, "known-users", FLD_INVITE); /* INVITE flood protection: max 4 per 60s */
+ config_parse_flood_generic("4:120", i, "known-users", FLD_KNOCK); /* KNOCK protection: max 4 per 120s */
+ config_parse_flood_generic("10:15", i, "known-users", FLD_CONVERSATIONS); /* 10 users, new user every 15s */
+ /* - unknown-users */
+ config_parse_flood_generic("2:60", i, "unknown-users", FLD_NICK); /* NICK flood protection: max 2 per 60s */
+ config_parse_flood_generic("2:90", i, "unknown-users", FLD_JOIN); /* JOIN flood protection: max 2 per 90s */
+ config_parse_flood_generic("4:120", i, "unknown-users", FLD_AWAY); /* AWAY flood protection: max 4 per 120s */
+ config_parse_flood_generic("2:60", i, "unknown-users", FLD_INVITE); /* INVITE flood protection: max 2 per 60s */
+ config_parse_flood_generic("2:120", i, "unknown-users", FLD_KNOCK); /* KNOCK protection: max 2 per 120s */
+ config_parse_flood_generic("4:15", i, "unknown-users", FLD_CONVERSATIONS); /* 4 users, new user every 15s */
+
/* SSL/TLS options */
i->tls_options = safe_alloc(sizeof(TLSOptions));
snprintf(tmp, sizeof(tmp), "%s/tls/server.cert.pem", CONFDIR);
@@ -1713,9 +1854,6 @@ void config_setdefaultsettings(Configuration *i)
i->ban_setter = SETTER_NICK;
i->ban_setter_sync = 1;
- i->max_concurrent_conversations_users = 10;
- i->max_concurrent_conversations_new_user_every = 15;
-
i->allowed_channelchars = ALLOWED_CHANNELCHARS_UTF8;
i->automatic_ban_target = BAN_TARGET_IP;
@@ -1730,11 +1868,11 @@ static void make_default_logblock(void)
{
ConfigItem_log *ca = safe_alloc(sizeof(ConfigItem_log));
- config_status("No log { } block found -- using default: errors will be logged to 'ircd.log'");
+ config_status("No log { } block found -- logging everything to 'ircd.log'");
safe_strdup(ca->file, "ircd.log");
convert_to_absolute_path(&ca->file, LOGDIR);
- ca->flags |= LOG_ERROR;
+ ca->flags |= LOG_CHGCMDS|LOG_CLIENT|LOG_ERROR|LOG_KILL|LOG_KLINE|LOG_OPER|LOG_OVERRIDE|LOG_SACMDS|LOG_SERVER|LOG_SPAMFILTER|LOG_TKL;
ca->logfd = -1;
AddListItem(ca, conf_log);
}
@@ -2705,8 +2843,15 @@ int config_run()
config_status("Running %s", cfptr->cf_filename);
for (ce = cfptr->cf_entries; ce; ce = ce->ce_next)
{
- if (!strcmp(ce->ce_varname, "set") || !strcmp(ce->ce_varname, "class"))
- continue; // already processed
+ /* These are already processed above (set, class)
+ * or via config_test() (secret).
+ */
+ if (!strcmp(ce->ce_varname, "set") ||
+ !strcmp(ce->ce_varname, "class") ||
+ !strcmp(ce->ce_varname, "secret"))
+ {
+ continue;
+ }
if ((cc = config_binary_search(ce->ce_varname))) {
if ((cc->conffunc) && (cc->conffunc(cfptr, ce) < 0))
@@ -2793,6 +2938,17 @@ int config_test()
{
if (config_verbose > 1)
config_status("Testing %s", cfptr->cf_filename);
+ /* First test and run the secret { } blocks */
+ for (ce = cfptr->cf_entries; ce; ce = ce->ce_next)
+ {
+ if (!strcmp(ce->ce_varname, "secret"))
+ {
+ int n = _test_secret(cfptr, ce);
+ errors += n;
+ if (n == 0)
+ _conf_secret(cfptr, ce);
+ }
+ }
/* First test the set { } block */
for (ce = cfptr->cf_entries; ce; ce = ce->ce_next)
{
@@ -2803,8 +2959,11 @@ int config_test()
for (ce = cfptr->cf_entries; ce; ce = ce->ce_next)
{
/* These are already processed, so skip them here.. */
- if (!strcmp(ce->ce_varname, "set"))
+ if (!strcmp(ce->ce_varname, "secret") ||
+ !strcmp(ce->ce_varname, "set"))
+ {
continue;
+ }
if ((cc = config_binary_search(ce->ce_varname))) {
if (cc->testfunc)
errors += (cc->testfunc(cfptr, ce));
@@ -2850,8 +3009,8 @@ int config_test()
if (strchr(ce->ce_varname, ':'))
{
config_error("You cannot use :: in a directive, you have to write them out. "
- "For example 'set::anti-flood::nick-flood 3:60' needs to be written as: "
- "set { anti-flood { nick-flood 3:60; } }");
+ "For example 'set::auto-join #something' needs to be written as: "
+ "set { auto-join \"#something\"; }");
config_error("See also https://www.unrealircd.org/docs/Set_block#Syntax_used_in_this_documentation");
}
}
@@ -3164,16 +3323,14 @@ char *pretty_time_val(long timeval)
buf[0] = 0;
if (timeval/86400)
- snprintf(buf, sizeof(buf), "%ld day%s ", timeval/86400, timeval/86400 != 1 ? "s" : "");
+ snprintf(buf, sizeof(buf), "%ldd", timeval/86400);
if ((timeval/3600) % 24)
- snprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), "%ld hour%s ", (timeval/3600)%24, (timeval/3600)%24 != 1 ? "s" : "");
+ snprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), "%ldh", (timeval/3600)%24);
if ((timeval/60)%60)
- snprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), "%ld minute%s ", (timeval/60)%60, (timeval/60)%60 != 1 ? "s" : "");
+ snprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), "%ldm", (timeval/60)%60);
if ((timeval%60))
- snprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), "%ld second%s", timeval%60, timeval%60 != 1 ? "s" : "");
- /* Strip space at the end (if any) */
- if (*buf && (buf[strlen(buf)-1] == ' '))
- buf[strlen(buf)-1] = '\0';
+ snprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), "%lds", timeval%60);
+
return buf;
}
@@ -7311,7 +7468,7 @@ void conf_tlsblock(ConfigFile *conf, ConfigEntry *cep, TLSOptions *tlsoptions)
int _conf_set(ConfigFile *conf, ConfigEntry *ce)
{
- ConfigEntry *cep, *cepp, *ceppp;
+ ConfigEntry *cep, *cepp, *ceppp, *cep4;
Hook *h;
for (cep = ce->ce_entries; cep; cep = cep->ce_next)
@@ -7506,85 +7663,75 @@ int _conf_set(ConfigFile *conf, ConfigEntry *ce)
else if (!strcmp(cep->ce_varname, "anti-flood")) {
for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next)
{
- if (!strcmp(cepp->ce_varname, "handshake-data-flood"))
+ for (ceppp = cepp->ce_entries; ceppp; ceppp = ceppp->ce_next)
{
- for (ceppp = cepp->ce_entries; ceppp; ceppp = ceppp->ce_next)
+ if (!strcmp(ceppp->ce_varname, "handshake-data-flood"))
{
- if (!strcmp(ceppp->ce_varname, "amount"))
- tempiConf.handshake_data_flood_amount = config_checkval(ceppp->ce_vardata, CFG_SIZE);
- else if (!strcmp(ceppp->ce_varname, "ban-time"))
- tempiConf.handshake_data_flood_ban_time = config_checkval(ceppp->ce_vardata, CFG_TIME);
- else if (!strcmp(ceppp->ce_varname, "ban-action"))
- tempiConf.handshake_data_flood_ban_action = banact_stringtoval(ceppp->ce_vardata);
+ for (cep4 = ceppp->ce_entries; cep4; cep4 = cep4->ce_next)
+ {
+ if (!strcmp(cep4->ce_varname, "amount"))
+ tempiConf.handshake_data_flood_amount = config_checkval(cep4->ce_vardata, CFG_SIZE);
+ else if (!strcmp(cep4->ce_varname, "ban-time"))
+ tempiConf.handshake_data_flood_ban_time = config_checkval(cep4->ce_vardata, CFG_TIME);
+ else if (!strcmp(cep4->ce_varname, "ban-action"))
+ tempiConf.handshake_data_flood_ban_action = banact_stringtoval(cep4->ce_vardata);
+ }
}
- }
- else if (!strcmp(cepp->ce_varname, "away-count"))
- tempiConf.away_count = atol(cepp->ce_vardata);
- else if (!strcmp(cepp->ce_varname, "away-period"))
- tempiConf.away_period = config_checkval(cepp->ce_vardata, CFG_TIME);
- else if (!strcmp(cepp->ce_varname, "away-flood"))
- {
- int cnt, period;
- config_parse_flood(cepp->ce_vardata, &cnt, &period);
- tempiConf.away_count = cnt;
- tempiConf.away_period = period;
- }
- else if (!strcmp(cepp->ce_varname, "nick-flood"))
- {
- int cnt, period;
- config_parse_flood(cepp->ce_vardata, &cnt, &period);
- tempiConf.nick_count = cnt;
- tempiConf.nick_period = period;
- }
- else if (!strcmp(cepp->ce_varname, "away-flood"))
- {
- int cnt, period;
- config_parse_flood(cepp->ce_vardata, &cnt, &period);
- tempiConf.away_count = cnt;
- tempiConf.away_period = period;
- }
- else if (!strcmp(cepp->ce_varname, "invite-flood"))
- {
- int cnt, period;
- config_parse_flood(cepp->ce_vardata, &cnt, &period);
- tempiConf.invite_count = cnt;
- tempiConf.invite_period = period;
- }
- else if (!strcmp(cepp->ce_varname, "knock-flood"))
- {
- int cnt, period;
- config_parse_flood(cepp->ce_vardata, &cnt, &period);
- tempiConf.knock_count = cnt;
- tempiConf.knock_period = period;
- }
- else if (!strcmp(cepp->ce_varname, "connect-flood"))
- {
- int cnt, period;
- config_parse_flood(cepp->ce_vardata, &cnt, &period);
- tempiConf.throttle_count = cnt;
- tempiConf.throttle_period = period;
- }
- if (!strcmp(cepp->ce_varname, "max-concurrent-conversations"))
- {
- for (ceppp = cepp->ce_entries; ceppp; ceppp = ceppp->ce_next)
+ else if (!strcmp(ceppp->ce_varname, "away-flood"))
{
- if (!strcmp(ceppp->ce_varname, "users"))
- {
- tempiConf.max_concurrent_conversations_users = atoi(ceppp->ce_vardata);
- } else
- if (!strcmp(ceppp->ce_varname, "new-user-every"))
+ config_parse_flood_generic(ceppp->ce_vardata, &tempiConf, cepp->ce_varname, FLD_AWAY);
+ }
+ else if (!strcmp(ceppp->ce_varname, "nick-flood"))
+ {
+ config_parse_flood_generic(ceppp->ce_vardata, &tempiConf, cepp->ce_varname, FLD_NICK);
+ }
+ else if (!strcmp(ceppp->ce_varname, "join-flood"))
+ {
+ config_parse_flood_generic(ceppp->ce_vardata, &tempiConf, cepp->ce_varname, FLD_JOIN);
+ }
+ else if (!strcmp(ceppp->ce_varname, "invite-flood"))
+ {
+ config_parse_flood_generic(ceppp->ce_vardata, &tempiConf, cepp->ce_varname, FLD_INVITE);
+ }
+ else if (!strcmp(ceppp->ce_varname, "knock-flood"))
+ {
+ config_parse_flood_generic(ceppp->ce_vardata, &tempiConf, cepp->ce_varname, FLD_KNOCK);
+ }
+ else if (!strcmp(ceppp->ce_varname, "connect-flood"))
+ {
+ int cnt, period;
+ config_parse_flood(ceppp->ce_vardata, &cnt, &period);
+ tempiConf.throttle_count = cnt;
+ tempiConf.throttle_period = period;
+ }
+ if (!strcmp(ceppp->ce_varname, "max-concurrent-conversations"))
+ {
+ /* We use a hack here to make it fit our storage format */
+ char buf[64];
+ int users=0;
+ long every=0;
+ for (cep4 = ceppp->ce_entries; cep4; cep4 = cep4->ce_next)
{
- tempiConf.max_concurrent_conversations_new_user_every = config_checkval(ceppp->ce_vardata, CFG_TIME);
+ if (!strcmp(cep4->ce_varname, "users"))
+ {
+ users = atoi(cep4->ce_vardata);
+ } else
+ if (!strcmp(cep4->ce_varname, "new-user-every"))
+ {
+ every = config_checkval(cep4->ce_vardata, CFG_TIME);
+ }
}
+ snprintf(buf, sizeof(buf), "%d:%ld", users, every);
+ config_parse_flood_generic(buf, &tempiConf, cepp->ce_varname, FLD_CONVERSATIONS);
}
- }
- else
- {
- for (h = Hooks[HOOKTYPE_CONFIGRUN]; h; h = h->next)
+ else
{
- int value = (*(h->func.intfunc))(conf,cepp,CONFIG_SET_ANTI_FLOOD);
- if (value == 1)
- break;
+ for (h = Hooks[HOOKTYPE_CONFIGRUN]; h; h = h->next)
+ {
+ int value = (*(h->func.intfunc))(conf,ceppp,CONFIG_SET_ANTI_FLOOD);
+ if (value == 1)
+ break;
+ }
}
}
}
@@ -7876,7 +8023,7 @@ int _conf_set(ConfigFile *conf, ConfigEntry *ce)
int _test_set(ConfigFile *conf, ConfigEntry *ce)
{
- ConfigEntry *cep, *cepp, *ceppp;
+ ConfigEntry *cep, *cepp, *ceppp, *cep4;
int tempi;
int errors = 0;
Hook *h;
@@ -8338,243 +8485,306 @@ int _test_set(ConfigFile *conf, ConfigEntry *ce)
}
else if (!strcmp(cep->ce_varname, "anti-flood"))
{
+ int anti_flood_old = 0;
+ int anti_flood_old_and_default = 0;
+
for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next)
{
- if (!strcmp(cepp->ce_varname, "max-concurrent-conversations"))
+ /* Test for old options: */
+ if (flood_option_is_old(cepp->ce_varname))
{
- for (ceppp = cepp->ce_entries; ceppp; ceppp = ceppp->ce_next)
+ /* Special code if the user is using 100% of the defaults */
+ if (cepp->ce_vardata &&
+ ((!strcmp(cepp->ce_varname, "nick-flood") && !strcmp(cepp->ce_vardata, "3:60")) ||
+ (!strcmp(cepp->ce_varname, "connect-flood") && cepp->ce_vardata && !strcmp(cepp->ce_vardata, "3:60")) ||
+ (!strcmp(cepp->ce_varname, "away-flood") && cepp->ce_vardata && !strcmp(cepp->ce_vardata, "4:120"))))
{
- CheckNull(ceppp);
- if (!strcmp(ceppp->ce_varname, "users"))
- {
- int v = atoi(ceppp->ce_vardata);
- if ((v < 1) || (v > MAXCCUSERS))
- {
- config_error("%s:%i: set::anti-flood::max-concurrent-conversations::users: "
- "value should be between 1 and %d",
- ceppp->ce_fileptr->cf_filename, ceppp->ce_varlinenum, MAXCCUSERS);
- errors++;
- }
- } else
- if (!strcmp(ceppp->ce_varname, "new-user-every"))
- {
- long v = config_checkval(ceppp->ce_vardata, CFG_TIME);
- if ((v < 1) || (v > 120))
- {
- config_error("%s:%i: set::anti-flood::max-concurrent-conversations::new-user-every: "
- "value should be between 1 and 120 seconds",
- ceppp->ce_fileptr->cf_filename, ceppp->ce_varlinenum);
- errors++;
- }
- } else
- {
- config_error_unknownopt(ceppp->ce_fileptr->cf_filename,
- ceppp->ce_varlinenum, "set::anti-flood",
- ceppp->ce_varname);
- errors++;
- }
+ anti_flood_old_and_default = 1;
+ } else
+ {
+ anti_flood_old = 1;
}
- continue; /* required here, due to checknull directly below */
- }
- else if (!strcmp(cepp->ce_varname, "unknown-flood-amount") ||
- !strcmp(cepp->ce_varname, "unknown-flood-bantime"))
- {
- config_error("%s:%i: set::anti-flood::%s: this setting has been moved. "
- "See https://www.unrealircd.org/docs/Set_block#set::anti-flood::handshake-data-flood",
- cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum, cepp->ce_varname);
- errors++;
continue;
}
- else if (!strcmp(cepp->ce_varname, "handshake-data-flood"))
+
+ for (ceppp = cepp->ce_entries; ceppp; ceppp = ceppp->ce_next)
{
- for (ceppp = cepp->ce_entries; ceppp; ceppp = ceppp->ce_next)
+ int everyone = !strcmp(cepp->ce_varname, "everyone") ? 1 : 0;
+ int for_everyone = flood_option_is_for_everyone(ceppp->ce_varname);
+
+ if (everyone && !for_everyone)
+ {
+ config_error("%s:%i: %s cannot be in the set::anti-flood::everyone block. "
+ "You can put it in 'known-users' or 'unknown-users' instead.",
+ ceppp->ce_fileptr->cf_filename, ceppp->ce_varlinenum,
+ ceppp->ce_varname);
+ errors++;
+ continue;
+ } else
+ if (!everyone && for_everyone)
{
- if (!strcmp(ceppp->ce_varname, "amount"))
+ config_error("%s:%i: %s must be in the set::anti-flood::everyone block, not anywhere else.",
+ ceppp->ce_fileptr->cf_filename, ceppp->ce_varlinenum,
+ ceppp->ce_varname);
+ errors++;
+ continue;
+ }
+
+ /* Now comes the actual config check for each element... */
+ if (!strcmp(ceppp->ce_varname, "max-concurrent-conversations"))
+ {
+ for (cep4 = ceppp->ce_entries; cep4; cep4 = cep4->ce_next)
{
- long v;
- CheckNull(ceppp);
- CheckDuplicate(ceppp, anti_flood_handshake_data_flood_amount, "anti-flood::handshake-data-flood::amount");
- v = config_checkval(ceppp->ce_vardata, CFG_SIZE);
- if (v < 1024)
+ CheckNull(cep4);
+ if (!strcmp(cep4->ce_varname, "users"))
{
- config_error("%s:%i: set::anti-flood::handshake-data-flood::amount must be at least 1024 bytes",
- ceppp->ce_fileptr->cf_filename, ceppp->ce_varlinenum);
- errors++;
- }
- } else
- if (!strcmp(ceppp->ce_varname, "ban-action"))
- {
- CheckNull(ceppp);
- CheckDuplicate(ceppp, anti_flood_handshake_data_flood_ban_action, "anti-flood::handshake-data-flood::ban-action");
- if (!banact_stringtoval(ceppp->ce_vardata))
+ int v = atoi(cep4->ce_vardata);
+ if ((v < 1) || (v > MAXCCUSERS))
+ {
+ config_error("%s:%i: set::anti-flood::max-concurrent-conversations::users: "
+ "value should be between 1 and %d",
+ cep4->ce_fileptr->cf_filename, cep4->ce_varlinenum, MAXCCUSERS);
+ errors++;
+ }
+ } else
+ if (!strcmp(cep4->ce_varname, "new-user-every"))
{
- config_error("%s:%i: set::anti-flood::handshake-data-flood::ban-action has unknown action type '%s'",
- ceppp->ce_fileptr->cf_filename, ceppp->ce_varlinenum,
- ceppp->ce_vardata);
+ long v = config_checkval(cep4->ce_vardata, CFG_TIME);
+ if ((v < 1) || (v > 120))
+ {
+ config_error("%s:%i: set::anti-flood::max-concurrent-conversations::new-user-every: "
+ "value should be between 1 and 120 seconds",
+ cep4->ce_fileptr->cf_filename, cep4->ce_varlinenum);
+ errors++;
+ }
+ } else
+ {
+ config_error_unknownopt(cep4->ce_fileptr->cf_filename,
+ cep4->ce_varlinenum, "set::anti-flood",
+ cep4->ce_varname);
errors++;
}
- } else
- if (!strcmp(ceppp->ce_varname, "ban-time"))
- {
- CheckNull(ceppp);
- CheckDuplicate(ceppp, anti_flood_handshake_data_flood_ban_time, "anti-flood::handshake-data-flood::ban-time");
- } else
- {
- config_error_unknownopt(ceppp->ce_fileptr->cf_filename,
- ceppp->ce_varlinenum, "set::anti-flood::handshake-data-flood",
- ceppp->ce_varname);
- errors++;
}
+ continue; /* required here, due to checknull directly below */
}
- }
- else if (!strcmp(cepp->ce_varname, "away-count"))
- {
- int temp = atol(cepp->ce_vardata);
- CheckNull(cepp);
- CheckDuplicate(cepp, anti_flood_away_count, "anti-flood::away-count");
- if (temp < 1 || temp > 255)
+ else if (!strcmp(ceppp->ce_varname, "unknown-flood-amount") ||
+ !strcmp(ceppp->ce_varname, "unknown-flood-bantime"))
{
- config_error("%s:%i: set::anti-flood::away-count must be between 1 and 255",
- cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum);
+ config_error("%s:%i: set::anti-flood::%s: this setting has been moved. "
+ "See https://www.unrealircd.org/docs/Anti-flood_settings#handshake-data-flood",
+ ceppp->ce_fileptr->cf_filename, ceppp->ce_varlinenum, ceppp->ce_varname);
errors++;
+ continue;
}
- }
- else if (!strcmp(cepp->ce_varname, "away-period"))
- {
- CheckNull(cepp);
- int temp = config_checkval(cepp->ce_vardata, CFG_TIME);
- CheckDuplicate(cepp, anti_flood_away_period, "anti-flood::away-period");
- if (temp < 10)
+ else if (!strcmp(ceppp->ce_varname, "handshake-data-flood"))
{
- config_error("%s:%i: set::anti-flood::away-period must be greater than 9",
- cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum);
- errors++;
+ for (cep4 = ceppp->ce_entries; cep4; cep4 = cep4->ce_next)
+ {
+ if (!strcmp(cep4->ce_varname, "amount"))
+ {
+ long v;
+ CheckNull(cep4);
+ v = config_checkval(cep4->ce_vardata, CFG_SIZE);
+ if (v < 1024)
+ {
+ config_error("%s:%i: set::anti-flood::handshake-data-flood::amount must be at least 1024 bytes",
+ cep4->ce_fileptr->cf_filename, cep4->ce_varlinenum);
+ errors++;
+ }
+ } else
+ if (!strcmp(cep4->ce_varname, "ban-action"))
+ {
+ CheckNull(cep4);
+ if (!banact_stringtoval(cep4->ce_vardata))
+ {
+ config_error("%s:%i: set::anti-flood::handshake-data-flood::ban-action has unknown action type '%s'",
+ cep4->ce_fileptr->cf_filename, cep4->ce_varlinenum,
+ cep4->ce_vardata);
+ errors++;
+ }
+ } else
+ if (!strcmp(cep4->ce_varname, "ban-time"))
+ {
+ CheckNull(cep4);
+ } else
+ {
+ config_error_unknownopt(cep4->ce_fileptr->cf_filename,
+ cep4->ce_varlinenum, "set::anti-flood::handshake-data-flood",
+ cep4->ce_varname);
+ errors++;
+ }
+ }
}
- }
- else if (!strcmp(cepp->ce_varname, "away-flood"))
- {
- int cnt, period;
- CheckNull(cepp);
- if (settings.has_anti_flood_away_period)
+ else if (!strcmp(ceppp->ce_varname, "away-count"))
{
- config_warn("%s:%d: set::anti-flood::away-flood overrides set::anti-flood::away-period",
- cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum);
- continue;
+ int temp = atol(ceppp->ce_vardata);
+ CheckNull(ceppp);
+ if (temp < 1 || temp > 255)
+ {
+ config_error("%s:%i: set::anti-flood::away-count must be between 1 and 255",
+ ceppp->ce_fileptr->cf_filename, ceppp->ce_varlinenum);
+ errors++;
+ }
}
- if (settings.has_anti_flood_away_count)
+ else if (!strcmp(ceppp->ce_varname, "away-period"))
{
- config_warn("%s:%d: set::anti-flood::away-flood overrides set::anti-flood::away-count",
- cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum);
- continue;
+ CheckNull(ceppp);
+ int temp = config_checkval(ceppp->ce_vardata, CFG_TIME);
+ if (temp < 10)
+ {
+ config_error("%s:%i: set::anti-flood::away-period must be greater than 9",
+ ceppp->ce_fileptr->cf_filename, ceppp->ce_varlinenum);
+ errors++;
+ }
}
- settings.has_anti_flood_away_period = 1;
- settings.has_anti_flood_away_count = 1;
- if (!config_parse_flood(cepp->ce_vardata, &cnt, &period) ||
- (cnt < 1) || (cnt > 255) || (period < 10))
+ else if (!strcmp(ceppp->ce_varname, "away-flood"))
{
- config_error("%s:%i: set::anti-flood::away-flood error. Syntax is '<count>:<period>' (eg 5:60), "
- "count should be 1-255, period should be greater than 9",
- cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum);
- errors++;
+ int cnt, period;
+ CheckNull(ceppp);
+ if (!config_parse_flood(ceppp->ce_vardata, &cnt, &period) ||
+ (cnt < 1) || (cnt > 255) || (period < 10))
+ {
+ config_error("%s:%i: set::anti-flood::away-flood error. Syntax is '<count>:<period>' (eg 5:60), "
+ "count should be 1-255, period should be greater than 9",
+ ceppp->ce_fileptr->cf_filename, ceppp->ce_varlinenum);
+ errors++;
+ }
}
- }
- else if (!strcmp(cepp->ce_varname, "nick-flood"))
- {
- int cnt, period;
- CheckNull(cepp);
- CheckDuplicate(cepp, anti_flood_nick_flood, "anti-flood::nick-flood");
- if (!config_parse_flood(cepp->ce_vardata, &cnt, &period) ||
- (cnt < 1) || (cnt > 255) || (period < 5))
+ else if (!strcmp(ceppp->ce_varname, "nick-flood"))
{
- config_error("%s:%i: set::anti-flood::nick-flood error. Syntax is '<count>:<period>' (eg 5:60), "
- "count should be 1-255, period should be greater than 4",
- cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum);
- errors++;
+ int cnt, period;
+ CheckNull(ceppp);
+ if (!config_parse_flood(ceppp->ce_vardata, &cnt, &period) ||
+ (cnt < 1) || (cnt > 255) || (period < 5))
+ {
+ config_error("%s:%i: set::anti-flood::nick-flood error. Syntax is '<count>:<period>' (eg 5:60), "
+ "count should be 1-255, period should be greater than 4",
+ ceppp->ce_fileptr->cf_filename, ceppp->ce_varlinenum);
+ errors++;
+ }
}
- }
- else if (!strcmp(cepp->ce_varname, "invite-flood"))
- {
- int cnt, period;
- CheckNull(cepp);
- CheckDuplicate(cepp, anti_flood_invite_flood, "anti-flood::invite-flood");
- if (!config_parse_flood(cepp->ce_vardata, &cnt, &period) ||
- (cnt < 1) || (cnt > 255) || (period < 5))
+ else if (!strcmp(ceppp->ce_varname, "join-flood"))
{
- config_error("%s:%i: set::anti-flood::invite-flood error. Syntax is '<count>:<period>' (eg 5:60), "
- "count should be 1-255, period should be greater than 4",
- cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum);
- errors++;
+ int cnt, period;
+ CheckNull(ceppp);
+
+ if (!config_parse_flood(ceppp->ce_vardata, &cnt, &period) ||
+ (cnt < 1) || (cnt > 255) || (period < 5))
+ {
+ config_error("%s:%i: join-flood error. Syntax is '<count>:<period>' (eg 5:60), "
+ "count should be 1-255, period should be greater than 4",
+ ceppp->ce_fileptr->cf_filename, ceppp->ce_varlinenum);
+ errors++;
+ }
}
- }
- else if (!strcmp(cepp->ce_varname, "knock-flood"))
- {
- int cnt, period;
- CheckNull(cepp);
- CheckDuplicate(cepp, anti_flood_knock_flood, "anti-flood::knock-flood");
- if (!config_parse_flood(cepp->ce_vardata, &cnt, &period) ||
- (cnt < 1) || (cnt > 255) || (period < 5))
+ else if (!strcmp(ceppp->ce_varname, "invite-flood"))
{
- config_error("%s:%i: set::anti-flood::knock-flood error. Syntax is '<count>:<period>' (eg 5:60), "
- "count should be 1-255, period should be greater than 4",
- cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum);
- errors++;
+ int cnt, period;
+ CheckNull(ceppp);
+ if (!config_parse_flood(ceppp->ce_vardata, &cnt, &period) ||
+ (cnt < 1) || (cnt > 255) || (period < 5))
+ {
+ config_error("%s:%i: set::anti-flood::invite-flood error. Syntax is '<count>:<period>' (eg 5:60), "
+ "count should be 1-255, period should be greater than 4",
+ ceppp->ce_fileptr->cf_filename, ceppp->ce_varlinenum);
+ errors++;
+ }
}
- }
- else if (!strcmp(cepp->ce_varname, "connect-flood"))
- {
- int cnt, period;
- CheckNull(cepp);
- CheckDuplicate(cepp, anti_flood_connect_flood, "anti-flood::connect-flood");
- if (!config_parse_flood(cepp->ce_vardata, &cnt, &period) ||
- (cnt < 1) || (cnt > 255) || (period < 1) || (period > 3600))
+ else if (!strcmp(ceppp->ce_varname, "knock-flood"))
{
- config_error("%s:%i: set::anti-flood::connect-flood: Syntax is '<count>:<period>' (eg 5:60), "
- "count should be 1-255, period should be 1-3600",
- cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum);
- errors++;
+ int cnt, period;
+ CheckNull(ceppp);
+ if (!config_parse_flood(ceppp->ce_vardata, &cnt, &period) ||
+ (cnt < 1) || (cnt > 255) || (period < 5))
+ {
+ config_error("%s:%i: set::anti-flood::knock-flood error. Syntax is '<count>:<period>' (eg 5:60), "
+ "count should be 1-255, period should be greater than 4",
+ ceppp->ce_fileptr->cf_filename, ceppp->ce_varlinenum);
+ errors++;
+ }
}
- }
- else
- {
- /* hmm.. I don't like this method. but I just quickly copied it from CONFIG_ALLOW for now... */
- int used = 0;
- Hook *h;
- for (h = Hooks[HOOKTYPE_CONFIGTEST]; h; h = h->next)
+ else if (!strcmp(ceppp->ce_varname, "connect-flood"))
{
- int value, errs = 0;
- if (h->owner && !(h->owner->flags & MODFLAG_TESTING)
- && !(h->owner->options & MOD_OPT_PERM))
+ int cnt, period;
+ CheckNull(ceppp);
+ if (strcmp(cepp->ce_varname, "everyone"))
+ {
+ config_error("%s:%i: connect-flood must be in the set::anti-flood::everyone block, not anywhere else.",
+ ceppp->ce_fileptr->cf_filename, ceppp->ce_varlinenum);
+ errors++;
continue;
- value = (*(h->func.intfunc))(conf,cepp,CONFIG_SET_ANTI_FLOOD,&errs);
- if (value == 2)
- used = 1;
- if (value == 1)
+ }
+ if (!config_parse_flood(ceppp->ce_vardata, &cnt, &period) ||
+ (cnt < 1) || (cnt > 255) || (period < 1) || (period > 3600))
{
- used = 1;
- break;
+ config_error("%s:%i: set::anti-flood::connect-flood: Syntax is '<count>:<period>' (eg 5:60), "
+ "count should be 1-255, period should be 1-3600",
+ ceppp->ce_fileptr->cf_filename, ceppp->ce_varlinenum);
+ errors++;
}
- if (value == -1)
+ }
+ else
+ {
+ /* hmm.. I don't like this method. but I just quickly copied it from CONFIG_ALLOW for now... */
+ int used = 0;
+ Hook *h;
+ for (h = Hooks[HOOKTYPE_CONFIGTEST]; h; h = h->next)
{
- used = 1;
- errors += errs;
- break;
+ int value, errs = 0;
+ if (h->owner && !(h->owner->flags & MODFLAG_TESTING)
+ && !(h->owner->options & MOD_OPT_PERM))
+ continue;
+ value = (*(h->func.intfunc))(conf,ceppp,CONFIG_SET_ANTI_FLOOD,&errs);
+ if (value == 2)
+ used = 1;
+ if (value == 1)
+ {
+ used = 1;
+ break;
+ }
+ if (value == -1)
+ {
+ used = 1;
+ errors += errs;
+ break;
+ }
+ if (value == -2)
+ {
+ used = 1;
+ errors += errs;
+ }
}
- if (value == -2)
+ if (!used)
{
- used = 1;
- errors += errs;
+ config_error_unknownopt(ceppp->ce_fileptr->cf_filename,
+ ceppp->ce_varlinenum, "set::anti-flood",
+ ceppp->ce_varname);
+ errors++;
}
+ continue;
}
- if (!used)
- {
- config_error_unknownopt(cepp->ce_fileptr->cf_filename,
- cepp->ce_varlinenum, "set::anti-flood",
- cepp->ce_varname);
- errors++;
- }
- continue;
}
}
+ /* Now the warnings: */
+ if (anti_flood_old == 1)
+ {
+ config_warn("%s:%d: the set::anti-flood block has been reorganized to be more flexible. "
+ "Your custom anti-flood settings have NOT been read.",
+ cep->ce_fileptr->cf_filename, cep->ce_varlinenum);
+ config_warn("See https://www.unrealircd.org/docs/Anti-flood_settings for the new block style,");
+ config_warn("OR: simply remove all the anti-flood options from the conf to get rid of this "
+ "warning and use the built-in defaults.");
+ } else
+ if (anti_flood_old_and_default == 1)
+ {
+ config_warn("%s:%d: the set::anti-flood block has been reorganized to be more flexible.",
+ cep->ce_fileptr->cf_filename, cep->ce_varlinenum);
+ config_warn("To fix this warning, delete the anti-flood block from your configuration file "
+ "(file %s around line %d), this will make UnrealIRCd use the built-in defaults.",
+ cep->ce_fileptr->cf_filename, cep->ce_varlinenum);
+ config_warn("If you want to learn more about the new functionality you can visit "
+ "https://www.unrealircd.org/docs/Anti-flood_settings");
+ }
}
else if (!strcmp(cep->ce_varname, "options")) {
for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next) {
@@ -10151,6 +10361,10 @@ int _test_security_group(ConfigFile *conf, ConfigEntry *ce)
{
CheckNull(cep);
} else
+ if (!strcmp(cep->ce_varname, "tls"))
+ {
+ CheckNull(cep);
+ } else
if (!strcmp(cep->ce_varname, "reputation-score"))
{
int v;
@@ -10185,6 +10399,8 @@ int _conf_security_group(ConfigFile *conf, ConfigEntry *ce)
s->webirc = config_checkval(cep->ce_vardata, CFG_YESNO);
else if (!strcmp(cep->ce_varname, "identified"))
s->identified = config_checkval(cep->ce_vardata, CFG_YESNO);
+ else if (!strcmp(cep->ce_varname, "tls"))
+ s->tls = config_checkval(cep->ce_vardata, CFG_YESNO);
else if (!strcmp(cep->ce_varname, "reputation-score"))
s->reputation_score = atoi(cep->ce_vardata);
else if (!strcmp(cep->ce_varname, "priority"))
@@ -10197,6 +10413,306 @@ int _conf_security_group(ConfigFile *conf, ConfigEntry *ce)
return 1;
}
+Secret *find_secret(char *secret_name)
+{
+ Secret *s;
+ for (s = secrets; s; s = s->next)
+ {
+ if (!strcasecmp(s->name, secret_name))
+ return s;
+ }
+ return NULL;
+}
+
+void free_secret_cache(SecretCache *c)
+{
+ unrealdb_free_config(c->config);
+ safe_free(c);
+}
+
+void free_secret(Secret *s)
+{
+ SecretCache *c, *c_next;
+ for (c = s->cache; c; c = c_next)
+ {
+ c_next = c->next;
+ DelListItem(c, s->cache);
+ free_secret_cache(c);
+ }
+ safe_free(s->name);
+ safe_free_sensitive(s->password);
+ safe_free(s);
+}
+
+char *_conf_secret_read_password_file(char *fname)
+{
+ char *pwd, *err;
+ int fd, n;
+
+#ifndef _WIN32
+ fd = open(fname, O_RDONLY);
+#else
+ fd = open(fname, _O_RDONLY|_O_BINARY);
+#endif
+ if (fd < 0)
+ {
+ /* This should not happen, as we tested for file exists earlier.. */
+ config_error("Could not open file '%s': %s", fname, strerror(errno));
+ return NULL;
+ }
+
+ pwd = safe_alloc_sensitive(512);
+ n = read(fd, pwd, 511);
+ if (n <= 0)
+ {
+ close(fd);
+ config_error("Could not read from file '%s': %s", fname, strerror(errno));
+ safe_free_sensitive(pwd);
+ return NULL;
+ }
+ close(fd);
+ stripcrlf(pwd);
+ sodium_stackzero(1024);
+ if (!valid_secret_password(pwd, &err))
+ {
+ config_error("Key from file '%s' does not meet password complexity requirements: %s", fname, err);
+ safe_free_sensitive(pwd);
+ return NULL;
+ }
+ return pwd;
+}
+
+char *_conf_secret_read_prompt(char *blockname)
+{
+ char *pwd, *pwd_prompt;
+ char buf[256];
+
+#ifdef _WIN32
+ /* FIXME: add windows support? should be possible in GUI no? */
+ return NULL;
+#else
+ snprintf(buf, sizeof(buf), "Enter password for secret '%s': ", blockname);
+ pwd_prompt = getpass(buf);
+ if (pwd_prompt)
+ {
+ pwd = safe_alloc_sensitive(512);
+ strlcpy(pwd, pwd_prompt, 512);
+ memset(pwd_prompt, 0, strlen(pwd_prompt)); // zero password out
+ sodium_stackzero(1024);
+ return pwd;
+ }
+ return NULL;
+#endif
+}
+
+int _test_secret(ConfigFile *conf, ConfigEntry *ce)
+{
+ int errors = 0;
+ int has_password = 0, has_password_file = 0, has_password_prompt = 0;
+ ConfigEntry *cep;
+ char *err;
+ Secret *existing;
+
+ if (!ce->ce_vardata)
+ {
+ config_error("%s:%i: secret block needs a name, eg: secret xyz {",
+ ce->ce_fileptr->cf_filename, ce->ce_varlinenum);
+ errors++;
+ return errors; /* need to return here since we dereference ce->ce_vardata later.. */
+ } else {
+ if (!security_group_valid_name(ce->ce_vardata))
+ {
+ config_error("%s:%i: secret block name '%s' contains invalid characters or is too long. "
+ "Only letters, numbers, underscore and hyphen are allowed.",
+ ce->ce_fileptr->cf_filename, ce->ce_varlinenum, ce->ce_vardata);
+ errors++;
+ }
+ }
+
+ existing = find_secret(ce->ce_vardata);
+
+ for (cep = ce->ce_entries; cep; cep = cep->ce_next)
+ {
+ if (!strcmp(cep->ce_varname, "password"))
+ {
+ int n;
+ has_password = 1;
+ CheckNull(cep);
+ if (cep->ce_entries ||
+ (((n = Auth_AutoDetectHashType(cep->ce_vardata))) && ((n == AUTHTYPE_BCRYPT) || (n == AUTHTYPE_ARGON2))))
+ {
+ config_error("%s:%d: you cannot use hashed passwords here, see "
+ "https://www.unrealircd.org/docs/Secret_block#secret-plaintext",
+ cep->ce_fileptr->cf_filename, cep->ce_varlinenum);
+ errors++;
+ continue;
+ }
+ if (!valid_secret_password(cep->ce_vardata, &err))
+ {
+ config_error("%s:%d: secret::password does not meet password complexity requirements: %s",
+ cep->ce_fileptr->cf_filename, cep->ce_varlinenum, err);
+ errors++;
+ }
+ } else
+ if (!strcmp(cep->ce_varname, "password-file"))
+ {
+ char *str;
+ has_password_file = 1;
+ CheckNull(cep);
+ convert_to_absolute_path(&cep->ce_vardata, CONFDIR);
+ if (!file_exists(cep->ce_vardata) && existing && existing->password)
+ {
+ /* Silently ignore the case where a secret block already
+ * has the password read and now the file is no longer available.
+ * This so secret::password-file can be used only to boot
+ * and then the media (eg: USB stick) can be pulled.
+ */
+ } else
+ {
+ str = _conf_secret_read_password_file(cep->ce_vardata);
+ if (!str)
+ {
+ config_error("%s:%d: secret::password-file: error reading password from file, see error from above.",
+ cep->ce_fileptr->cf_filename, cep->ce_varlinenum);
+ errors++;
+ }
+ safe_free_sensitive(str);
+ }
+ } else
+ if (!strcmp(cep->ce_varname, "password-prompt"))
+ {
+#ifdef _WIN32
+ config_error("%s:%d: secret::password-prompt is not implemented in Windows at the moment, sorry!",
+ cep->ce_fileptr->cf_filename, cep->ce_varlinenum);
+ config_error("Choose a different method to enter passwords or use *NIX");
+ errors++;
+ return errors;
+#endif
+ has_password_prompt = 1;
+ if (loop.ircd_booted && !find_secret(ce->ce_vardata))
+ {
+ config_error("%s:%d: you cannot add a new secret { } block that uses password-prompt and then /REHASH. "
+ "With 'password-prompt' you can only add such a password on boot.",
+ cep->ce_fileptr->cf_filename, cep->ce_varlinenum);
+ config_error("Either use a different method to enter passwords or restart the IRCd on the console.");
+ errors++;
+ }
+ if (!loop.ircd_booted && !running_interactively())
+ {
+ config_error("ERROR: IRCd is not running interactively, but via a cron job or something similar.");
+ config_error("%s:%d: unable to prompt for password since IRCd is not started in a terminal",
+ cep->ce_fileptr->cf_filename, cep->ce_varlinenum);
+ config_error("Either use a different method to enter passwords or start the IRCd in a terminal/SSH/..");
+ }
+ } else
+ if (!strcmp(cep->ce_varname, "password-url"))
+ {
+ config_error("%s:%d: secret::password-url is not supported yet in this UnrealIRCd version.",
+ cep->ce_fileptr->cf_filename, cep->ce_varlinenum);
+ errors++;
+ } else
+ {
+ config_error_unknown(cep->ce_fileptr->cf_filename, cep->ce_varlinenum,
+ "secret", cep->ce_varname);
+ errors++;
+ continue;
+ }
+ if (cep->ce_entries)
+ {
+ config_error("%s:%d: secret::%s does not support sub-options (%s)",
+ cep->ce_fileptr->cf_filename, cep->ce_varlinenum,
+ cep->ce_varname, cep->ce_entries->ce_varname);
+ errors++;
+ }
+ }
+
+ if (!has_password && !has_password_file && !has_password_prompt)
+ {
+ config_error("%s:%d: secret { } block must contain 1 of: password OR password-file OR password-prompt",
+ ce->ce_fileptr->cf_filename, ce->ce_varlinenum);
+ errors++;
+ }
+
+ return errors;
+}
+
+/* NOTE: contrary to all other _conf* stuff, this one actually runs during config_test,
+ * so during the early CONFIG TEST stage rather than CONFIG RUN.
+ * This so all secret { } block configuration is available already during TEST/POSTTEST
+ * stage for modules, so they can check if the password is correct or not.
+ */
+int _conf_secret(ConfigFile *conf, ConfigEntry *ce)
+{
+ ConfigEntry *cep;
+ Secret *s;
+ Secret *existing = find_secret(ce->ce_vardata);
+
+ s = safe_alloc(sizeof(Secret));
+ safe_strdup(s->name, ce->ce_vardata);
+
+ for (cep = ce->ce_entries; cep; cep = cep->ce_next)
+ {
+ if (!strcmp(cep->ce_varname, "password"))
+ {
+ safe_strdup_sensitive(s->password, cep->ce_vardata);
+ destroy_string(cep->ce_vardata); /* destroy the original */
+ } else
+ if (!strcmp(cep->ce_varname, "password-file"))
+ {
+ if (!file_exists(cep->ce_vardata) && existing && existing->password)
+ {
+ /* Silently ignore the case where a secret block already
+ * has the password read and now the file is no longer available.
+ * This so secret::password-file can be used only to boot
+ * and then the media (eg: USB stick) can be pulled.
+ */
+ } else
+ {
+ s->password = _conf_secret_read_password_file(cep->ce_vardata);
+ }
+ } else
+ if (!strcmp(cep->ce_varname, "password-prompt"))
+ {
+ if (!loop.ircd_booted && running_interactively())
+ {
+ s->password = _conf_secret_read_prompt(ce->ce_vardata);
+ if (!s->password || !valid_secret_password(s->password, NULL))
+ {
+ config_error("Invalid password entered on console (does not meet complexity requirements)");
+ /* This cannot be the correct password, so exit */
+ exit(-1);
+ }
+ }
+ }
+ }
+
+ /* This may happen if we run twice, due to destroy_string() earlier: */
+ if (BadPtr(s->password))
+ {
+ free_secret(s);
+ return 1;
+ }
+
+ /* If there is an existing secret { } block with this name in memory
+ * and it has a different password, then free that secret block
+ */
+ if (existing)
+ {
+ if (!strcmp(s->password, existing->password))
+ {
+ free_secret(s);
+ return 1;
+ }
+ /* passwords differ, so free the old existing one,
+ * including purging the cache for it.
+ */
+ DelListItem(existing, secrets);
+ free_secret(existing);
+ }
+ AddListItem(s, secrets);
+ return 1;
+}
+
#ifdef USE_LIBCURL
static void conf_download_complete(const char *url, const char *file, const char *errorbuf, int cached, void *inc_key)
{
diff --git a/src/conf_preprocessor.c b/src/conf_preprocessor.c
@@ -12,16 +12,6 @@ extern ConfigFile *conf;
NameValueList *config_defines = NULL; /**< List of @defines, only valid during configuration reading */
-void skip_whitespace(char **p)
-{
- for (; **p == ' ' || **p == '\t'; *p = *p + 1);
-}
-
-void read_until(char **p, char *stopchars)
-{
- for (; **p && !strchr(stopchars, **p); *p = *p + 1);
-}
-
static inline int ValidVarCharacter(char x)
{
if (isupper(x) || isdigit(x) || strchr("_", x))
diff --git a/src/crashreport.c b/src/crashreport.c
@@ -4,9 +4,7 @@
*/
#include "unrealircd.h"
-#ifndef _WIN32
-#include <dirent.h>
-#else
+#ifdef _WIN32
extern void StartUnrealAgain(void);
#endif
#include "version.h"
@@ -527,24 +525,6 @@ char *generate_crash_report(char *coredump, int *thirdpartymods)
return reportfname;
}
-int running_interactive(void)
-{
-#ifndef _WIN32
- char *s;
-
- if (!isatty(0))
- return 0;
-
- s = getenv("TERM");
- if (!s || !strcasecmp(s, "dumb") || !strcasecmp(s, "none"))
- return 0;
-
- return 1;
-#else
- return IsService ? 0 : 1;
-#endif
-}
-
#define REPORT_NEVER -1
#define REPORT_ASK 0
#define REPORT_AUTO 1
@@ -728,13 +708,41 @@ void report_crash_not_sent(char *fname)
" (if you do, please set the option 'View Status' at the end of the bug report page to 'private'!!)\n", fname);
}
+/** This checks if there are indications that 3rd party modules are
+ * loaded. This is used to provide a small warning to the user that
+ * the crash may be likely due to that.
+ */
+int check_third_party_mods_present(void)
+{
+#ifndef _WIN32
+ struct dirent *dir;
+ DIR *fd = opendir(TMPDIR);
+
+ if (!fd)
+ return 0;
+
+ /* We search for files like tmp/FC5C3116.third.somename.so */
+ while ((dir = readdir(fd)))
+ {
+ char *fname = dir->d_name;
+ if (strstr(fname, ".third.") && strstr(fname, ".so"))
+ {
+ closedir(fd);
+ return 1;
+ }
+ }
+ closedir(fd);
+#endif
+ return 0;
+}
+
void report_crash(void)
{
char *coredump, *fname;
int thirdpartymods = 0;
int crashed_secs_ago;
- if (!running_interactive() && (report_pref != REPORT_AUTO))
+ if (!running_interactively() && (report_pref != REPORT_AUTO))
exit(0); /* don't bother if we run through cron or something similar */
coredump = find_best_coredump();
@@ -750,6 +758,8 @@ void report_crash(void)
if (!fname)
return;
+ if (thirdpartymods == 0)
+ thirdpartymods = check_third_party_mods_present();
#ifndef _WIN32
printf("The IRCd has been started now (and is running), but it did crash %d seconds ago.\n", crashed_secs_ago);
printf("Crash report generated in: %s\n\n", fname);
@@ -761,10 +771,10 @@ void report_crash(void)
"by someone other than the UnrealIRCd team). If you installed new 3rd party\n"
"module(s) in the past few weeks we suggest to unload these modules and see if\n"
"the crash issue dissapears. If so, that module is probably to blame.\n"
- "If you keep crashing without 3rd party modules then please do report it to\n"
- "the UnrealIRCd team.\n"
- "The reason we ask you to do this is because more than 95%% of the crash issues\n"
- "reported nowadays are caused by 3rd party modules and not by an UnrealIRCd bug.\n"
+ "If you keep crashing without any 3rd party modules loaded then please do report\n"
+ "it to the UnrealIRCd team.\n"
+ "The reason we ask you to do this is because MORE THAN 95%% OF ALL CRASH ISSUES\n"
+ "ARE CAUSED BY 3RD PARTY MODULES and not by an UnrealIRCd bug.\n"
"\n");
}
@@ -778,7 +788,9 @@ void report_crash(void)
char answerbuf[64], *answer;
printf("Shall I send a crash report to the UnrealIRCd developers?\n");
if (!thirdpartymods)
- printf("Crash reports help us greatly with fixing bugs that affect you and others\n");
+ printf("Crash reports help us greatly with fixing bugs that affect you and others\n");
+ else
+ printf("NOTE: If the crash is caused by a 3rd party module then UnrealIRCd devs can't fix that.\n");
printf("\n");
do
@@ -805,7 +817,7 @@ void report_crash(void)
return;
}
- if (running_interactive())
+ if (running_interactively())
{
char buf[8192], *line;
diff --git a/src/hash.c b/src/hash.c
@@ -465,7 +465,7 @@ Client *hash_find_nickatserver(const char *str, Client *def)
if (serv)
*serv++ = '\0';
- client = find_client(nick, NULL);
+ client = find_person(nick, NULL);
if (!client)
return NULL; /* client not found */
diff --git a/src/ircd.c b/src/ircd.c
@@ -19,6 +19,7 @@
*/
#include "unrealircd.h"
+#include <ares.h>
#ifdef __FreeBSD__
char *malloc_options = "h" MALLOC_FLAGS_EXTRA;
@@ -60,6 +61,7 @@ static void open_debugfile(), setup_signals();
extern void init_glines(void);
extern void tkl_init(void);
extern void process_clients(void);
+extern void unrealdb_test(void);
#ifndef _WIN32
MODVAR char **myargv;
@@ -80,6 +82,7 @@ void s_die()
Client *client;
if (!IsService)
{
+ loop.ircd_terminating = 1;
unload_all_modules();
list_for_each_entry(client, &lclient_list, lclient_node)
@@ -94,6 +97,7 @@ void s_die()
ControlService(hService, SERVICE_CONTROL_STOP, &status);
}
#else
+ loop.ircd_terminating = 1;
unload_all_modules();
unlink(conf_files ? conf_files->pid_file : IRCD_PIDFILE);
exit(0);
@@ -366,7 +370,7 @@ int match_tkls(Client *client)
if (loop.do_bancheck_spamf_away && IsUser(client) &&
client->user->away != NULL &&
- match_spamfilter(client, client->user->away, SPAMF_AWAY, NULL, SPAMFLAG_NOWARN, NULL))
+ match_spamfilter(client, client->user->away, SPAMF_AWAY, "AWAY", NULL, SPAMFLAG_NOWARN, NULL))
{
return 1;
}
@@ -573,84 +577,6 @@ char buf[1024];
#endif
}
-/** Ugly version checker that ensures ssl/curl runtime libraries match the
- * version we compiled for.
- */
-static void do_version_check()
-{
- const char *compiledfor, *runtime;
- int error = 0;
- char *p;
-
- /* OPENSSL:
- * Nowadays (since openssl 1.0.0) they retain binary compatibility
- * when the first two version numbers are the same: eg 1.0.0 and 1.0.2
- */
- compiledfor = OPENSSL_VERSION_TEXT;
- runtime = SSLeay_version(SSLEAY_VERSION);
- p = strchr(compiledfor, '.');
- if (p)
- {
- p = strchr(p+1, '.');
- if (p)
- {
- int versionlen = p - compiledfor + 1;
-
- if (strncasecmp(compiledfor, runtime, versionlen))
- {
- version_check_logerror("OpenSSL version mismatch: compiled for '%s', library is '%s'",
- compiledfor, runtime);
- error=1;
- }
- }
- }
-
-
-#ifdef USE_LIBCURL
- /* Perhaps someone should tell them to do this a bit more easy ;)
- * problem is runtime output is like: 'libcurl/7.11.1 c-ares/1.2.0'
- * while header output is like: '7.11.1'.
- */
- {
- char buf[128], *p;
-
- runtime = curl_version();
- compiledfor = LIBCURL_VERSION;
- if (!strncmp(runtime, "libcurl/", 8))
- {
- strlcpy(buf, runtime+8, sizeof(buf));
- p = strchr(buf, ' ');
- if (p)
- {
- *p = '\0';
- if (strcmp(compiledfor, buf))
- {
- version_check_logerror("Curl version mismatch: compiled for '%s', library is '%s'",
- compiledfor, buf);
- error = 1;
- }
- }
- }
- }
-#endif
-
- if (error)
- {
-#ifndef _WIN32
- version_check_logerror("Header<->library mismatches can make UnrealIRCd *CRASH*! "
- "Make sure you don't have multiple versions of openssl installed (eg: "
- "one in /usr and one in /usr/local). And, if you recently upgraded them, "
- "be sure to recompile UnrealIRCd.");
-#else
- version_check_logerror("Header<->library mismatches can make UnrealIRCd *CRASH*! "
- "This should never happen with official Windows builds... unless "
- "you overwrote any .dll files with newer/older ones or something.");
- win_error();
-#endif
- tainted = 1;
- }
-}
-
extern void applymeblock(void);
extern MODVAR Event *events;
@@ -927,6 +853,11 @@ int InitUnrealIRCd(int argc, char *argv[])
safe_strdup(configfile, CONFIGFILE);
init_random(); /* needs to be done very early!! */
+ if (sodium_init() < 0)
+ {
+ fprintf(stderr, "Failed to initialize sodium library -- error accessing random device?\n");
+ exit(-1);
+ }
memset(&botmotd, '\0', sizeof(MOTDFile));
memset(&rules, '\0', sizeof(MOTDFile));
@@ -1070,9 +1001,10 @@ int InitUnrealIRCd(int argc, char *argv[])
exit(0);
}
#endif
-#if 0
+#if 1
case 'S':
- charsys_dump_table(p ? p : "*");
+ //charsys_dump_table(p ? p : "*");
+ unrealdb_test();
exit(0);
#endif
#ifndef _WIN32
@@ -1165,8 +1097,6 @@ int InitUnrealIRCd(int argc, char *argv[])
}
}
- do_version_check();
-
#if !defined(_WIN32)
#ifndef _WIN32
mkdir(TMPDIR, S_IRUSR|S_IWUSR|S_IXUSR); /* Create the tmp dir, if it doesn't exist */
@@ -1215,11 +1145,13 @@ int InitUnrealIRCd(int argc, char *argv[])
fprintf(stderr, "UnrealIRCd is brought to you by Bram Matthys (Syzop), Gottem and i\n\n");
fprintf(stderr, "Using the following libraries:\n");
- fprintf(stderr, "* %s\n", pcre2_version());
fprintf(stderr, "* %s\n", SSLeay_version(SSLEAY_VERSION));
+ fprintf(stderr, "* libsodium %s\n", sodium_version_string());
#ifdef USE_LIBCURL
fprintf(stderr, "* %s\n", curl_version());
#endif
+ fprintf(stderr, "* c-ares %s\n", ares_version(NULL));
+ fprintf(stderr, "* %s\n", pcre2_version());
#endif
check_user_limit();
#ifndef _WIN32
diff --git a/src/list.c b/src/list.c
@@ -76,7 +76,7 @@ void initlists(void)
client_pool = mp_pool_new(sizeof(Client), 512 * 1024);
local_client_pool = mp_pool_new(sizeof(LocalClient), 512 * 1024);
- user_pool = mp_pool_new(sizeof(ClientUser), 512 * 1024);
+ user_pool = mp_pool_new(sizeof(User), 512 * 1024);
link_pool = mp_pool_new(sizeof(Link), 512 * 1024);
}
@@ -184,30 +184,20 @@ void free_client(Client *client)
** 'make_user' add's an User information block to a client
** if it was not previously allocated.
*/
-ClientUser *make_user(Client *client)
+User *make_user(Client *client)
{
- ClientUser *user;
+ User *user;
user = client->user;
if (!user)
{
user = mp_pool_get(user_pool);
- memset(user, 0, sizeof(ClientUser));
+ memset(user, 0, sizeof(User));
#ifdef DEBUGMODE
users.inuse++;
#endif
- user->swhois = NULL;
- user->away = NULL;
- user->flood.away_t = 0;
- user->flood.away_c = 0;
- user->joined = 0;
- user->channel = NULL;
- user->invited = NULL;
- user->server = NULL;
strlcpy(user->svid, "0", sizeof(user->svid));
- user->whowas = NULL;
- user->snomask = 0;
if (client->ip)
{
/* initially set client->user->realhost to IP */
@@ -450,6 +440,8 @@ void del_ListItem(ListStruct *item, ListStruct **list)
item->next->prev = item->prev;
if (*list == item)
*list = item->next; /* new head */
+ /* And update 'item', prev/next should point nowhere anymore */
+ item->prev = item->next = NULL;
}
/** Add item to list with a 'priority'.
diff --git a/src/mempool.c b/src/mempool.c
@@ -506,11 +506,6 @@ mp_pool_new(size_t item_size, size_t chunk_capacity)
pool->next = mp_allocated_pools;
mp_allocated_pools = pool;
- ircd_log(LOG_DBG, "Capacity is %lu, item size is %lu, alloc size is %lu",
- (unsigned long)pool->new_chunk_capacity,
- (unsigned long)pool->item_alloc_size,
- (unsigned long)(pool->new_chunk_capacity*pool->item_alloc_size));
-
return pool;
}
@@ -701,19 +696,12 @@ mp_pool_log_status(mp_pool_t *pool)
for (chunk = pool->empty_chunks; chunk; chunk = chunk->next)
bytes_allocated += chunk->mem_size;
- ircd_log(LOG_DBG, "%llu bytes in %d empty chunks",
- bytes_allocated, pool->n_empty_chunks);
for (chunk = pool->used_chunks; chunk; chunk = chunk->next) {
++n_used;
bu += chunk->n_allocated * pool->item_alloc_size;
ba += chunk->mem_size;
-
- ircd_log(LOG_DBG, " used chunk: %d items allocated",
- chunk->n_allocated);
}
- ircd_log(LOG_DBG, "%llu/%llu bytes in %d partially full chunks",
- bu, ba, n_used);
bytes_used += bu;
bytes_allocated += ba;
bu = ba = 0;
@@ -724,22 +712,7 @@ mp_pool_log_status(mp_pool_t *pool)
ba += chunk->mem_size;
}
- ircd_log(LOG_DBG, "%llu/%llu bytes in %d full chunks",
- bu, ba, n_full);
bytes_used += bu;
bytes_allocated += ba;
-
- ircd_log(LOG_DBG, "Total: %llu/%llu bytes allocated "
- "for cell pools are full.",
- bytes_used, bytes_allocated);
-
-#ifdef MEMPOOL_STATS
- ircd_log(LOG_DBG, "%llu cell allocations ever; "
- "%llu chunk allocations ever; "
- "%llu chunk frees ever.",
- (long long)pool->total_items_allocated,
- (long long)pool->total_chunks_allocated,
- (long long)pool->total_chunks_freed);
-#endif
}
#endif
diff --git a/src/misc.c b/src/misc.c
@@ -81,16 +81,17 @@ typedef struct {
} SpamfilterTargetTable;
SpamfilterTargetTable spamfiltertargettable[] = {
- { SPAMF_CHANMSG, 'c', "channel", "PRIVMSG" },
- { SPAMF_USERMSG, 'p', "private", "PRIVMSG" },
+ { SPAMF_CHANMSG, 'c', "channel", "PRIVMSG" },
+ { SPAMF_USERMSG, 'p', "private", "PRIVMSG" },
{ SPAMF_USERNOTICE, 'n', "private-notice", "NOTICE" },
{ SPAMF_CHANNOTICE, 'N', "channel-notice", "NOTICE" },
- { SPAMF_PART, 'P', "part", "PART" },
- { SPAMF_QUIT, 'q', "quit", "QUIT" },
- { SPAMF_DCC, 'd', "dcc", "PRIVMSG" },
- { SPAMF_USER, 'u', "user", "NICK" },
- { SPAMF_AWAY, 'a', "away", "AWAY" },
- { SPAMF_TOPIC, 't', "topic", "TOPIC" },
+ { SPAMF_PART, 'P', "part", "PART" },
+ { SPAMF_QUIT, 'q', "quit", "QUIT" },
+ { SPAMF_DCC, 'd', "dcc", "PRIVMSG" },
+ { SPAMF_USER, 'u', "user", "NICK" },
+ { SPAMF_AWAY, 'a', "away", "AWAY" },
+ { SPAMF_TOPIC, 't', "topic", "TOPIC" },
+ { SPAMF_MTAG, 'T', "message-tag", "message-tag" },
{ 0, 0, 0, 0 }
};
@@ -583,7 +584,7 @@ static void recurse_send_quits(Client *cptr, Client *client, Client *from, Clien
recurse_send_quits(cptr, acptr, from, to, mtags, comment, splitstr);
}
- if (cptr == client && to != from)
+ if (cptr == client && to != from && !(to->direction && (to->direction == from)))
sendto_one(to, mtags, "SQUIT %s :%s", client->name, comment);
}
@@ -700,6 +701,16 @@ static void exit_one_client(Client *client, MessageTag *mtags_i, const char *com
*/
void exit_client(Client *client, MessageTag *recv_mtags, char *comment)
{
+ exit_client_ex(client, client->direction, recv_mtags, comment);
+}
+
+/** Exit this IRC client, and all the dependents (users, servers) if this is a server.
+ * @param client The client to exit.
+ * @param recv_mtags Message tags to use as a base (if any).
+ * @param comment The (s)quit message
+ */
+void exit_client_ex(Client *client, Client *origin, MessageTag *recv_mtags, char *comment)
+{
long long on_for;
ConfigItem_listen *listen_conf;
MessageTag *mtags_generated = NULL;
@@ -812,7 +823,7 @@ void exit_client(Client *client, MessageTag *recv_mtags, char *comment)
else
ircsnprintf(splitstr, sizeof splitstr, "%s %s", client->srvptr->name, client->name);
- remove_dependents(client, client->direction, recv_mtags, comment, splitstr);
+ remove_dependents(client, origin, recv_mtags, comment, splitstr);
RunHook2(HOOKTYPE_SERVER_QUIT, client, recv_mtags);
}
@@ -1187,6 +1198,7 @@ char *our_strcasestr(char *haystack, char *needle)
* @param tag A tag used internally and for server-to-server traffic,
* not visible to end-users.
* @param priority Priority - for ordering multiple swhois entries
+ * (lower number = further up in the swhoises list in WHOIS)
* @param swhois The actual special whois title (string) you want to add to the user
* @param from Who added this entry
* @param skip Which server(-side) to skip broadcasting this entry to.
@@ -1785,6 +1797,23 @@ int read_str(FILE *fd, char **x)
return 1;
}
+/** Convert binary 'data' of size 'len' to a hexadecimal string 'str'.
+ * The caller is responsible to ensure that 'str' is sufficiently large.
+ */
+void binarytohex(void *data, size_t len, char *str)
+{
+ const char hexchars[16] = "0123456789abcdef";
+ char *datastr = (char *)data;
+ int i, n = 0;
+
+ for (i=0; i<len; i++)
+ {
+ str[n++] = hexchars[(datastr[i] >> 4) & 0xF];
+ str[n++] = hexchars[datastr[i] & 0xF];
+ }
+ str[n] = '\0';
+}
+
/** Generates an MD5 checksum.
* @param mdout[out] Buffer to store result in, the result will be 16 bytes in binary
* (not ascii printable!).
@@ -1809,31 +1838,27 @@ void DoMD5(char *mdout, const char *src, unsigned long n)
char *md5hash(char *dst, const char *src, unsigned long n)
{
char tmp[16];
- SHA256_CTX hash;
DoMD5(tmp, src, n);
- sprintf(dst, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
- tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8],
- tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14], tmp[15]);
-
+ binarytohex(tmp, sizeof(tmp), dst);
return dst;
}
-/** Convert binary 'data' of size 'len' to a hexadecimal string 'str'.
- * The caller is responsible to ensure that 'str' is sufficiently large.
+/** Generates a SHA256 checksum - ASCII printable string (0011223344..etc..).
+ * @param dst[out] Buffer to store result in, which needs to be 65 bytes minimum.
+ * @param src[in] The input data used to generate the checksum.
+ * @param n[in] Length of data.
*/
-void binarytohex(void *data, size_t len, char *str)
+char *sha256hash(char *dst, const char *src, unsigned long n)
{
- const char hexchars[16] = "0123456789abcdef";
- char *datastr = (char *)data;
- int i, n = 0;
+ SHA256_CTX hash;
+ char binaryhash[SHA256_DIGEST_LENGTH];
- for (i=0; i<len; i++)
- {
- str[n++] = hexchars[(datastr[i] >> 4) & 0xF];
- str[n++] = hexchars[datastr[i] & 0xF];
- }
- str[n] = '\0';
+ SHA256_Init(&hash);
+ SHA256_Update(&hash, src, n);
+ SHA256_Final(binaryhash, &hash);
+ binarytohex(binaryhash, sizeof(binaryhash), dst);
+ return dst;
}
/** Calculate the SHA256 checksum of a file */
@@ -1973,3 +1998,101 @@ char *sendtype_to_cmd(SendType sendtype)
return "TAGMSG";
return NULL;
}
+
+/** Check password strength.
+ * @param pass The password to check
+ * @param min_length The minimum length of the password
+ * @param strict Whether to require UPPER+lower+digits
+ * @returns 1 if good, 0 if not.
+ */
+int check_password_strength(char *pass, int min_length, int strict, char **err)
+{
+ char has_lowercase=0, has_uppercase=0, has_digit=0;
+ char *p;
+ static char buf[256];
+
+ if (err)
+ *err = NULL;
+
+ if (strlen(pass) < min_length)
+ {
+ if (err)
+ {
+ snprintf(buf, sizeof(buf), "Password must be at least %d characters", min_length);
+ *err = buf;
+ }
+ return 0;
+ }
+
+ for (p=pass; *p; p++)
+ {
+ if (islower(*p))
+ has_lowercase = 1;
+ else if (isupper(*p))
+ has_uppercase = 1;
+ else if (isdigit(*p))
+ has_digit = 1;
+ }
+
+ if (strict)
+ {
+ if (!has_lowercase)
+ {
+ if (err)
+ *err = "Password must contain at least 1 lowercase character";
+ return 0;
+ } else
+ if (!has_uppercase)
+ {
+ if (err)
+ *err = "Password must contain at least 1 UPPERcase character";
+ return 0;
+ } else
+ if (!has_digit)
+ {
+ if (err)
+ *err = "Password must contain at least 1 digit (number)";
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
+int valid_secret_password(char *pass, char **err)
+{
+ return check_password_strength(pass, 10, 1, err);
+}
+
+int running_interactively(void)
+{
+#ifndef _WIN32
+ char *s;
+
+ if (!isatty(0))
+ return 0;
+
+ s = getenv("TERM");
+ if (!s || !strcasecmp(s, "dumb") || !strcasecmp(s, "none"))
+ return 0;
+
+ return 1;
+#else
+ return IsService ? 0 : 1;
+#endif
+}
+
+/** Skip whitespace (if any) */
+void skip_whitespace(char **p)
+{
+ for (; **p == ' ' || **p == '\t'; *p = *p + 1);
+}
+
+/** Keep reading '*p' until we hit any of the 'stopchars'.
+ * Actually behaves like strstr() but then hit the end
+ * of the string (\0) i guess?
+ */
+void read_until(char **p, char *stopchars)
+{
+ for (; **p && !strchr(stopchars, **p); *p = *p + 1);
+}
diff --git a/src/modulemanager.c b/src/modulemanager.c
@@ -6,7 +6,6 @@
#include "unrealircd.h"
#ifndef _WIN32
-#include <dirent.h>
#define MODULEMANAGER_CONNECT_TIMEOUT 7
#define MODULEMANAGER_READ_TIMEOUT 20
diff --git a/src/modules.c b/src/modules.c
@@ -30,9 +30,6 @@
#else
#include <dlfcn.h>
#endif
-#ifndef _WIN32
-#include <dirent.h>
-#endif
#ifndef RTLD_NOW
#define RTLD_NOW RTLD_LAZY
#endif
@@ -444,7 +441,7 @@ char *Module_Create(char *path_)
}
}
mod->flags = MODFLAG_TESTING;
- AddListItem(mod, Modules);
+ AddListItemPrio(mod, Modules, 0);
return NULL;
}
else
@@ -1187,6 +1184,9 @@ void unload_all_modules(void)
int (*Mod_Unload)();
for (m = Modules; m; m = m->next)
{
+#ifdef DEBUGMODE
+ ircd_log(LOG_ERROR, "Unloading %s...", m->header->name);
+#endif
irc_dlsym(m->dll, "Mod_Unload", Mod_Unload);
if (Mod_Unload)
(*Mod_Unload)(&m->modinfo);
@@ -1194,15 +1194,21 @@ void unload_all_modules(void)
}
}
-unsigned int ModuleSetOptions(Module *module, unsigned int options, int action)
+void ModuleSetOptions(Module *module, unsigned int options, int action)
{
unsigned int oldopts = module->options;
- if (action)
- module->options |= options;
- else
- module->options &= ~options;
- return oldopts;
+ if (options == MOD_OPT_UNLOAD_PRIORITY)
+ {
+ DelListItem(module, Modules);
+ AddListItemPrio(module, Modules, action);
+ } else {
+ /* Simple bit flag(s) */
+ if (action)
+ module->options |= options;
+ else
+ module->options &= ~options;
+ }
}
unsigned int ModuleGetOptions(Module *module)
diff --git a/src/modules/Makefile.in b/src/modules/Makefile.in
@@ -71,8 +71,9 @@ R_MODULES= \
account-tag.so labeled-response.so link-security.so \
message-ids.so plaintext-policy.so server-time.so sts.so \
echo-message.so userip-tag.so userhost-tag.so \
- typing-indicator.so \
- ident_lookup.so history.so \
+ bot-tag.so \
+ reply-tag.so typing-indicator.so \
+ ident_lookup.so history.so chathistory.so \
targetfloodprot.so clienttagdeny.so
MODULES=cloak.so $(R_MODULES)
@@ -612,6 +613,14 @@ userhost-tag.so: userhost-tag.c $(INCLUDES)
$(CC) $(CFLAGS) $(MODULEFLAGS) -DDYNAMIC_LINKING \
-o userhost-tag.so userhost-tag.c
+bot-tag.so: bot-tag.c $(INCLUDES)
+ $(CC) $(CFLAGS) $(MODULEFLAGS) -DDYNAMIC_LINKING \
+ -o bot-tag.so bot-tag.c
+
+reply-tag.so: reply-tag.c $(INCLUDES)
+ $(CC) $(CFLAGS) $(MODULEFLAGS) -DDYNAMIC_LINKING \
+ -o reply-tag.so reply-tag.c
+
typing-indicator.so: typing-indicator.c $(INCLUDES)
$(CC) $(CFLAGS) $(MODULEFLAGS) -DDYNAMIC_LINKING \
-o typing-indicator.so typing-indicator.c
@@ -632,6 +641,10 @@ history.so: history.c $(INCLUDES)
$(CC) $(CFLAGS) $(MODULEFLAGS) -DDYNAMIC_LINKING \
-o history.so history.c
+chathistory.so: chathistory.c $(INCLUDES)
+ $(CC) $(CFLAGS) $(MODULEFLAGS) -DDYNAMIC_LINKING \
+ -o chathistory.so chathistory.c
+
targetfloodprot.so: targetfloodprot.c $(INCLUDES)
$(CC) $(CFLAGS) $(MODULEFLAGS) -DDYNAMIC_LINKING \
-o targetfloodprot.so targetfloodprot.c
diff --git a/src/modules/antirandom.c b/src/modules/antirandom.c
@@ -858,20 +858,6 @@ static int internal_getscore(char *str)
return score;
}
-void strtolower_safe(char *dst, char *src, int size)
-{
- if (!size)
- return; /* size of 0 is unworkable */
- size--; /* for \0 */
-
- for (; *src && size; src++)
- {
- *dst++ = tolower(*src);
- size--;
- }
- *dst = '\0';
-}
-
/** Returns "spam score".
* @note a user is expected, do not call for anything else (eg: servers)
*/
diff --git a/src/modules/authprompt.c b/src/modules/authprompt.c
@@ -451,7 +451,7 @@ int authprompt_sasl_continuation(Client *client, char *buf)
sendto_one(agent, NULL, ":%s SASL %s %s C %s",
me.id, AGENT_SID(agent), client->id, SEUSER(client)->authmsg);
}
- SEUSER(client)->authmsg = NULL;
+ safe_free(SEUSER(client)->authmsg);
}
return 1; /* inhibit displaying of message */
}
diff --git a/src/modules/away.c b/src/modules/away.c
@@ -86,24 +86,16 @@ CMD_FUNC(cmd_away)
}
/* Check spamfilters */
- if (MyUser(client) && match_spamfilter(client, new_reason, SPAMF_AWAY, NULL, 0, NULL))
+ if (MyUser(client) && match_spamfilter(client, new_reason, SPAMF_AWAY, "AWAY", NULL, 0, NULL))
return;
- /* Check set::anti-flood::away-flood */
- if (MyUser(client) && AWAY_PERIOD && !ValidatePermissionsForPath("immune:away-flood",client,NULL,NULL,NULL))
+ /* Check away-flood */
+ if (MyUser(client) &&
+ !ValidatePermissionsForPath("immune:away-flood",client,NULL,NULL,NULL) &&
+ flood_limit_exceeded(client, FLD_AWAY))
{
- if ((client->user->flood.away_t + AWAY_PERIOD) <= timeofday)
- {
- client->user->flood.away_c = 0;
- client->user->flood.away_t = timeofday;
- }
- if (client->user->flood.away_c <= AWAY_COUNT)
- client->user->flood.away_c++;
- if (client->user->flood.away_c > AWAY_COUNT)
- {
- sendnumeric(client, ERR_TOOMANYAWAY);
- return;
- }
+ sendnumeric(client, ERR_TOOMANYAWAY);
+ return;
}
/* Obey set::away-length */
diff --git a/src/modules/bot-tag.c b/src/modules/bot-tag.c
@@ -0,0 +1,90 @@
+/*
+ * IRC - Internet Relay Chat, src/modules/bot-tag.c
+ * (C) 2021 Syzop & The UnrealIRCd Team
+ *
+ * See file AUTHORS in IRC package for additional names of
+ * the programmers.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 1, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
+/* This implements the message tag that is mentioned in
+ * https://ircv3.net/specs/extensions/bot-mode
+ * The B mode and 005 is in the modules/usermodes/bot module.
+ */
+
+#include "unrealircd.h"
+
+ModuleHeader MOD_HEADER
+ = {
+ "bot-tag",
+ "5.0",
+ "bot message tag",
+ "UnrealIRCd Team",
+ "unrealircd-5",
+ };
+
+int bottag_mtag_is_ok(Client *client, char *name, char *value);
+void mtag_add_bottag(Client *client, MessageTag *recv_mtags, MessageTag **mtag_list, char *signature);
+
+MOD_INIT()
+{
+ MessageTagHandlerInfo mtag;
+
+ MARK_AS_OFFICIAL_MODULE(modinfo);
+
+ memset(&mtag, 0, sizeof(mtag));
+ mtag.name = "draft/bot";
+ mtag.is_ok = bottag_mtag_is_ok;
+ mtag.flags = MTAG_HANDLER_FLAGS_NO_CAP_NEEDED;
+ MessageTagHandlerAdd(modinfo->handle, &mtag);
+
+ HookAddVoid(modinfo->handle, HOOKTYPE_NEW_MESSAGE, 0, mtag_add_bottag);
+
+ return MOD_SUCCESS;
+}
+
+MOD_LOAD()
+{
+ return MOD_SUCCESS;
+}
+
+MOD_UNLOAD()
+{
+ return MOD_SUCCESS;
+}
+
+/** This function verifies if the client sending the mtag is permitted to do so.
+ */
+int bottag_mtag_is_ok(Client *client, char *name, char *value)
+{
+ if (IsServer(client) && (value == NULL))
+ return 1; /* OK */
+
+ return 0;
+}
+
+void mtag_add_bottag(Client *client, MessageTag *recv_mtags, MessageTag **mtag_list, char *signature)
+{
+ MessageTag *m;
+
+ if (IsUser(client) && has_user_mode(client, 'B'))
+ {
+ MessageTag *m = safe_alloc(sizeof(MessageTag));
+ safe_strdup(m->name, "draft/bot");
+ m->value = NULL;
+ AddListItem(m, *mtag_list);
+ }
+}
diff --git a/src/modules/chanmodes/floodprot.c b/src/modules/chanmodes/floodprot.c
@@ -28,13 +28,13 @@ ModuleHeader MOD_HEADER
"unrealircd-5",
};
-#define FLD_CTCP 0 /* c */
-#define FLD_JOIN 1 /* j */
-#define FLD_KNOCK 2 /* k */
-#define FLD_MSG 3 /* m */
-#define FLD_NICK 4 /* n */
-#define FLD_TEXT 5 /* t */
-#define FLD_REPEAT 6 /* r */
+#define CHFLD_CTCP 0 /* c */
+#define CHFLD_JOIN 1 /* j */
+#define CHFLD_KNOCK 2 /* k */
+#define CHFLD_MSG 3 /* m */
+#define CHFLD_NICK 4 /* n */
+#define CHFLD_TEXT 5 /* t */
+#define CHFLD_REPEAT 6 /* r */
#define NUMFLD 7 /* 7 flood types */
@@ -58,13 +58,13 @@ typedef struct FloodType {
* IMPORTANT: MUST be in alphabetic order!!
*/
FloodType floodtypes[] = {
- { 'c', FLD_CTCP, "CTCPflood", 'C', "mM", 0, },
- { 'j', FLD_JOIN, "joinflood", 'i', "R", 0, },
- { 'k', FLD_KNOCK, "knockflood", 'K', "", 0, },
- { 'm', FLD_MSG, "msg/noticeflood", 'm', "M", 0, },
- { 'n', FLD_NICK, "nickflood", 'N', "", 0, },
- { 't', FLD_TEXT, "msg/noticeflood", '\0', "bd", 1, },
- { 'r', FLD_REPEAT, "repeating", '\0', "bd", 1, },
+ { 'c', CHFLD_CTCP, "CTCPflood", 'C', "mM", 0, },
+ { 'j', CHFLD_JOIN, "joinflood", 'i', "R", 0, },
+ { 'k', CHFLD_KNOCK, "knockflood", 'K', "", 0, },
+ { 'm', CHFLD_MSG, "msg/noticeflood", 'm', "M", 0, },
+ { 'n', CHFLD_NICK, "nickflood", 'N', "", 0, },
+ { 't', CHFLD_TEXT, "msg/noticeflood", '\0', "bd", 1, },
+ { 'r', CHFLD_REPEAT, "repeating", '\0', "bd", 1, },
};
#define MODEF_DEFAULT_UNSETTIME cfg.modef_default_unsettime
@@ -132,7 +132,7 @@ uint64_t gen_floodprot_msghash(char *text);
int cmodef_is_ok(Client *client, Channel *channel, char mode, char *para, int type, int what);
void *cmodef_put_param(void *r_in, char *param);
char *cmodef_get_param(void *r_in);
-char *cmodef_conv_param(char *param_in, Client *client);
+char *cmodef_conv_param(char *param_in, Client *client, Channel *channel);
void cmodef_free_param(void *r);
void *cmodef_dup_struct(void *r_in);
int cmodef_sjoin_check(Channel *channel, void *ourx, void *theirx);
@@ -142,7 +142,7 @@ int cmodef_channel_destroy(Channel *channel, int *should_destroy);
int floodprot_can_send_to_channel(Client *client, Channel *channel, Membership *lp, char **msg, char **errmsg, SendType sendtype);
int floodprot_post_chanmsg(Client *client, Channel *channel, int sendflags, int prefix, char *target, MessageTag *mtags, char *text, SendType sendtype);
int floodprot_knock(Client *client, Channel *channel, MessageTag *mtags, char *comment);
-int floodprot_nickchange(Client *client, char *oldnick);
+int floodprot_nickchange(Client *client, MessageTag *mtags, char *oldnick);
int floodprot_chanmode_del(Channel *channel, int m);
void memberflood_free(ModData *md);
int floodprot_stats(Client *client, char *flag);
@@ -596,7 +596,7 @@ char *cmodef_get_param(void *r_in)
/** Convert parameter to something proper.
* NOTE: client may be NULL if called for e.g. set::modes-on-join
*/
-char *cmodef_conv_param(char *param_in, Client *client)
+char *cmodef_conv_param(char *param_in, Client *client, Channel *channel)
{
static char retbuf[256];
char param[256];
@@ -753,7 +753,7 @@ int floodprot_join(Client *client, Channel *channel, MessageTag *mtags, char *pa
(client->srvptr->serv->boottime && (TStime() - client->srvptr->serv->boottime >= MODEF_BOOT_DELAY)) &&
!IsULine(client))
{
- do_floodprot(channel, client, FLD_JOIN);
+ do_floodprot(channel, client, CHFLD_JOIN);
}
return 0;
}
@@ -842,7 +842,7 @@ int floodprot_can_send_to_channel(Client *client, Channel *channel, Membership *
chp = (ChannelFloodProtection *)GETPARASTRUCT(channel, 'f');
- if (!chp || !(chp->limit[FLD_TEXT] || chp->limit[FLD_REPEAT]))
+ if (!chp || !(chp->limit[CHFLD_TEXT] || chp->limit[CHFLD_REPEAT]))
return HOOK_CONTINUE;
if (moddata_membership(mb, mdflood).ptr == NULL)
@@ -859,7 +859,7 @@ int floodprot_can_send_to_channel(Client *client, Channel *channel, Membership *
memberflood->firstmsg = TStime();
memberflood->nmsg = 1;
memberflood->nmsg_repeat = 1;
- if (chp->limit[FLD_REPEAT])
+ if (chp->limit[CHFLD_REPEAT])
{
memberflood->lastmsg = gen_floodprot_msghash(*msg);
memberflood->prevmsg = 0;
@@ -868,7 +868,7 @@ int floodprot_can_send_to_channel(Client *client, Channel *channel, Membership *
}
/* Anti-repeat ('r') */
- if (chp->limit[FLD_REPEAT])
+ if (chp->limit[CHFLD_REPEAT])
{
msghash = gen_floodprot_msghash(*msg);
if (memberflood->lastmsg)
@@ -876,7 +876,7 @@ int floodprot_can_send_to_channel(Client *client, Channel *channel, Membership *
if ((memberflood->lastmsg == msghash) || (memberflood->prevmsg == msghash))
{
memberflood->nmsg_repeat++;
- if (memberflood->nmsg_repeat > chp->limit[FLD_REPEAT])
+ if (memberflood->nmsg_repeat > chp->limit[CHFLD_REPEAT])
is_flooding_repeat = 1;
}
memberflood->prevmsg = memberflood->lastmsg;
@@ -884,11 +884,11 @@ int floodprot_can_send_to_channel(Client *client, Channel *channel, Membership *
memberflood->lastmsg = msghash;
}
- if (chp->limit[FLD_TEXT])
+ if (chp->limit[CHFLD_TEXT])
{
/* increase msgs */
memberflood->nmsg++;
- if (memberflood->nmsg > chp->limit[FLD_TEXT])
+ if (memberflood->nmsg > chp->limit[CHFLD_TEXT])
is_flooding_text = 1;
}
@@ -903,11 +903,11 @@ int floodprot_can_send_to_channel(Client *client, Channel *channel, Membership *
if (is_flooding_repeat)
{
snprintf(errbuf, sizeof(errbuf), "Flooding (Your last message is too similar to previous ones)");
- flood_type = FLD_REPEAT;
+ flood_type = CHFLD_REPEAT;
} else
{
- snprintf(errbuf, sizeof(errbuf), "Flooding (Limit is %i lines per %i seconds)", chp->limit[FLD_TEXT], chp->per);
- flood_type = FLD_TEXT;
+ snprintf(errbuf, sizeof(errbuf), "Flooding (Limit is %i lines per %i seconds)", chp->limit[CHFLD_TEXT], chp->per);
+ flood_type = CHFLD_TEXT;
}
if (chp->action[flood_type] == 'd')
@@ -952,10 +952,10 @@ int floodprot_post_chanmsg(Client *client, Channel *channel, int sendflags, int
/* HINT: don't be so stupid to reorder the items in the if's below.. you'll break things -- Syzop. */
- do_floodprot(channel, client, FLD_MSG);
+ do_floodprot(channel, client, CHFLD_MSG);
if ((text[0] == '\001') && strncmp(text+1, "ACTION ", 7))
- do_floodprot(channel, client, FLD_CTCP);
+ do_floodprot(channel, client, CHFLD_CTCP);
return 0;
}
@@ -963,11 +963,11 @@ int floodprot_post_chanmsg(Client *client, Channel *channel, int sendflags, int
int floodprot_knock(Client *client, Channel *channel, MessageTag *mtags, char *comment)
{
if (IsFloodLimit(channel) && !IsULine(client))
- do_floodprot(channel, client, FLD_KNOCK);
+ do_floodprot(channel, client, CHFLD_KNOCK);
return 0;
}
-int floodprot_nickchange(Client *client, char *oldnick)
+int floodprot_nickchange(Client *client, MessageTag *mtags, char *oldnick)
{
Membership *mp;
@@ -980,7 +980,7 @@ int floodprot_nickchange(Client *client, char *oldnick)
if (channel && IsFloodLimit(channel) &&
!(mp->flags & (CHFL_CHANOP|CHFL_VOICE|CHFL_CHANOWNER|CHFL_HALFOP|CHFL_CHANADMIN)))
{
- do_floodprot(channel, client, FLD_NICK);
+ do_floodprot(channel, client, CHFLD_NICK);
}
}
return 0;
@@ -1001,27 +1001,27 @@ int floodprot_chanmode_del(Channel *channel, int modechar)
switch(modechar)
{
case 'C':
- chp->counter[FLD_CTCP] = 0;
+ chp->counter[CHFLD_CTCP] = 0;
break;
case 'N':
- chp->counter[FLD_NICK] = 0;
+ chp->counter[CHFLD_NICK] = 0;
break;
case 'm':
- chp->counter[FLD_MSG] = 0;
- chp->counter[FLD_CTCP] = 0;
+ chp->counter[CHFLD_MSG] = 0;
+ chp->counter[CHFLD_CTCP] = 0;
break;
case 'K':
- chp->counter[FLD_KNOCK] = 0;
+ chp->counter[CHFLD_KNOCK] = 0;
break;
case 'i':
- chp->counter[FLD_JOIN] = 0;
+ chp->counter[CHFLD_JOIN] = 0;
break;
case 'M':
- chp->counter[FLD_MSG] = 0;
- chp->counter[FLD_CTCP] = 0;
+ chp->counter[CHFLD_MSG] = 0;
+ chp->counter[CHFLD_CTCP] = 0;
break;
case 'R':
- chp->counter[FLD_JOIN] = 0;
+ chp->counter[CHFLD_JOIN] = 0;
break;
default:
break;
diff --git a/src/modules/chanmodes/history.c b/src/modules/chanmodes/history.c
@@ -19,10 +19,12 @@ struct ConfigHistoryExt {
int lines; /**< number of lines */
long time; /**< seconds */
};
-struct {
+typedef struct cfgstruct cfgstruct;
+struct cfgstruct {
ConfigHistoryExt playback_on_join; /**< Maximum number of lines & time to playback on-join */
- ConfigHistoryExt max_storage_per_channel; /**< Maximum number of lines & time to record */
-} cfg;
+ ConfigHistoryExt max_storage_per_channel_registered; /**< Maximum number of lines & time to record for +r channels*/
+ ConfigHistoryExt max_storage_per_channel_unregistered; /**< Maximum number of lines & time to record for -r channels */
+};
typedef struct HistoryChanMode HistoryChanMode;
struct HistoryChanMode {
@@ -30,29 +32,38 @@ struct HistoryChanMode {
unsigned long max_time; /**< Maximum number of time (in seconds) to record */
};
+/* Global variables */
Cmode_t EXTMODE_HISTORY = 0L;
+static cfgstruct cfg;
+static cfgstruct test;
+
#define HistoryEnabled(channel) (channel->mode.extmode & EXTMODE_HISTORY)
/* Forward declarations */
-static void init_config(void);
+static void init_config(cfgstruct *cfg);
int history_config_test(ConfigFile *, ConfigEntry *, int, int *);
+int history_config_posttest(int *);
int history_config_run(ConfigFile *, ConfigEntry *, int);
int history_chanmode_change(Client *client, Channel *channel, MessageTag *mtags, char *modebuf, char *parabuf, time_t sendts, int samode);
static int compare_history_modes(HistoryChanMode *a, HistoryChanMode *b);
int history_chanmode_is_ok(Client *client, Channel *channel, char mode, char *para, int type, int what);
void *history_chanmode_put_param(void *r_in, char *param);
char *history_chanmode_get_param(void *r_in);
-char *history_chanmode_conv_param(char *param, Client *client);
+char *history_chanmode_conv_param(char *param, Client *client, Channel *channel);
void history_chanmode_free_param(void *r);
void *history_chanmode_dup_struct(void *r_in);
int history_chanmode_sjoin_check(Channel *channel, void *ourx, void *theirx);
int history_channel_destroy(Channel *channel, int *should_destroy);
int history_chanmsg(Client *client, Channel *channel, int sendflags, int prefix, char *target, MessageTag *mtags, char *text, SendType sendtype);
int history_join(Client *client, Channel *channel, MessageTag *mtags, char *parv[]);
+CMD_OVERRIDE_FUNC(override_mode);
MOD_TEST()
{
+ init_config(&test);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, history_config_test);
+ HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, history_config_posttest);
+
return MOD_SUCCESS;
}
@@ -75,7 +86,7 @@ MOD_INIT()
creq.sjoin_check = history_chanmode_sjoin_check;
CmodeAdd(modinfo->handle, creq, &EXTMODE_HISTORY);
- init_config();
+ init_config(&cfg);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, history_config_run);
HookAdd(modinfo->handle, HOOKTYPE_LOCAL_CHANMODE, 0, history_chanmode_change);
@@ -88,6 +99,11 @@ MOD_INIT()
MOD_LOAD()
{
+ CommandOverrideAdd(modinfo->handle, "MODE", override_mode);
+ CommandOverrideAdd(modinfo->handle, "SVSMODE", override_mode);
+ CommandOverrideAdd(modinfo->handle, "SVS2MODE", override_mode);
+ CommandOverrideAdd(modinfo->handle, "SAMODE", override_mode);
+ CommandOverrideAdd(modinfo->handle, "SJOIN", override_mode);
return MOD_SUCCESS;
}
@@ -96,14 +112,16 @@ MOD_UNLOAD()
return MOD_SUCCESS;
}
-static void init_config(void)
+static void init_config(cfgstruct *cfg)
{
/* Set default values */
- memset(&cfg, 0, sizeof(cfg));
- cfg.playback_on_join.lines = 15;
- cfg.playback_on_join.time = 86400;
- cfg.max_storage_per_channel.lines = 200;
- cfg.max_storage_per_channel.time = 86400*7;
+ memset(cfg, 0, sizeof(cfgstruct));
+ cfg->playback_on_join.lines = 15;
+ cfg->playback_on_join.time = 86400;
+ cfg->max_storage_per_channel_unregistered.lines = 200;
+ cfg->max_storage_per_channel_unregistered.time = 86400*31;
+ cfg->max_storage_per_channel_registered.lines = 5000;
+ cfg->max_storage_per_channel_registered.time = 86400*31;
}
#define CheckNull(x) if ((!(x)->ce_vardata) || (!(*((x)->ce_vardata)))) { config_error("%s:%i: missing parameter", (x)->ce_fileptr->cf_filename, (x)->ce_varlinenum); errors++; continue; }
@@ -111,9 +129,9 @@ static void init_config(void)
int history_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
int errors = 0;
- ConfigEntry *cep, *cepp, *cep4;
- int on_join_lines=0, maximum_storage_lines=0;
- long on_join_time=0L, maximum_storage_time=0L;
+ ConfigEntry *cep, *cepp, *cep4, *cep5;
+ int on_join_lines=0, maximum_storage_lines_registered=0, maximum_storage_lines_unregistered=0;
+ long on_join_time=0L, maximum_storage_time_registered=0L, maximum_storage_time_unregistered=0L;
/* We only care about set::history */
if ((type != CONFIG_SET) || strcmp(ce->ce_varname, "history"))
@@ -134,29 +152,29 @@ int history_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
int v;
CheckNull(cep4);
v = atoi(cep4->ce_vardata);
- if ((v < 1) || (v > 1000000000))
+ if ((v < 0) || (v > 1000))
{
- config_error("%s:%i: set::history::channel::playback-on-join::lines must be between 1 and 1000. "
+ config_error("%s:%i: set::history::channel::playback-on-join::lines must be between 0 and 1000. "
"Recommended values are 10-50. Got: %d.",
cep4->ce_fileptr->cf_filename, cep4->ce_varlinenum, v);
errors++;
continue;
}
- on_join_lines = v;
+ test.playback_on_join.lines = v;
} else
if (!strcmp(cep4->ce_varname, "time"))
{
long v;
CheckNull(cep4);
v = config_checkval(cep4->ce_vardata, CFG_TIME);
- if (v < 1)
+ if (v < 0)
{
- config_error("%s:%i: set::history::channel::playback-on-join::time must be a positive number.",
+ config_error("%s:%i: set::history::channel::playback-on-join::time must be zero or more.",
cep4->ce_fileptr->cf_filename, cep4->ce_varlinenum);
errors++;
continue;
}
- on_join_time = v;
+ test.playback_on_join.time = v;
} else
{
config_error_unknown(cep4->ce_fileptr->cf_filename,
@@ -169,45 +187,127 @@ int history_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
for (cep4 = cepp->ce_entries; cep4; cep4 = cep4->ce_next)
{
- if (!strcmp(cep4->ce_varname, "lines"))
+ if (!strcmp(cep4->ce_varname, "registered"))
{
- int v;
- CheckNull(cep4);
- v = atoi(cep4->ce_vardata);
- if (v < 1)
+ for (cep5 = cep4->ce_entries; cep5; cep5 = cep5->ce_next)
{
- config_error("%s:%i: set::history::channel::max-storage-per-channel::lines must be a positive number.",
- cep4->ce_fileptr->cf_filename, cep4->ce_varlinenum);
- errors++;
- continue;
+ if (!strcmp(cep5->ce_varname, "lines"))
+ {
+ int v;
+ CheckNull(cep5);
+ v = atoi(cep5->ce_vardata);
+ if (v < 1)
+ {
+ config_error("%s:%i: set::history::channel::max-storage-per-channel::registered::lines must be a positive number.",
+ cep5->ce_fileptr->cf_filename, cep5->ce_varlinenum);
+ errors++;
+ continue;
+ }
+ test.max_storage_per_channel_registered.lines = v;
+ } else
+ if (!strcmp(cep5->ce_varname, "time"))
+ {
+ long v;
+ CheckNull(cep5);
+ v = config_checkval(cep5->ce_vardata, CFG_TIME);
+ if (v < 1)
+ {
+ config_error("%s:%i: set::history::channel::max-storage-per-channel::registered::time must be a positive number.",
+ cep5->ce_fileptr->cf_filename, cep5->ce_varlinenum);
+ errors++;
+ continue;
+ }
+ test.max_storage_per_channel_registered.time = v;
+ } else
+ {
+ config_error_unknown(cep5->ce_fileptr->cf_filename,
+ cep5->ce_varlinenum, "set::history::channel::max-storage-per-channel::registered", cep5->ce_varname);
+ errors++;
+ }
}
- maximum_storage_lines = v;
} else
- if (!strcmp(cep4->ce_varname, "time"))
+ if (!strcmp(cep4->ce_varname, "unregistered"))
{
- long v;
- CheckNull(cep4);
- v = config_checkval(cep4->ce_vardata, CFG_TIME);
- if (v < 1)
+ for (cep5 = cep4->ce_entries; cep5; cep5 = cep5->ce_next)
{
- config_error("%s:%i: set::history::channel::max-storage-per-channel::time must be a positive number.",
- cep4->ce_fileptr->cf_filename, cep4->ce_varlinenum);
- errors++;
- continue;
+ if (!strcmp(cep5->ce_varname, "lines"))
+ {
+ int v;
+ CheckNull(cep5);
+ v = atoi(cep5->ce_vardata);
+ if (v < 1)
+ {
+ config_error("%s:%i: set::history::channel::max-storage-per-channel::unregistered::lines must be a positive number.",
+ cep5->ce_fileptr->cf_filename, cep5->ce_varlinenum);
+ errors++;
+ continue;
+ }
+ test.max_storage_per_channel_unregistered.lines = v;
+ } else
+ if (!strcmp(cep5->ce_varname, "time"))
+ {
+ long v;
+ CheckNull(cep5);
+ v = config_checkval(cep5->ce_vardata, CFG_TIME);
+ if (v < 1)
+ {
+ config_error("%s:%i: set::history::channel::max-storage-per-channel::unregistered::time must be a positive number.",
+ cep5->ce_fileptr->cf_filename, cep5->ce_varlinenum);
+ errors++;
+ continue;
+ }
+ test.max_storage_per_channel_unregistered.time = v;
+ } else
+ {
+ config_error_unknown(cep5->ce_fileptr->cf_filename,
+ cep5->ce_varlinenum, "set::history::channel::max-storage-per-channel::unregistered", cep5->ce_varname);
+ errors++;
+ }
}
- maximum_storage_time = v;
} else
{
- config_error_unknown(cep4->ce_fileptr->cf_filename,
- cep4->ce_varlinenum, "set::history::channel::max-storage-per-channel", cep4->ce_varname);
+ config_error_unknown(cep->ce_fileptr->cf_filename,
+ cep->ce_varlinenum, "set::history::max-storage-per-channel", cep->ce_varname);
errors++;
}
}
} else
{
- config_error_unknown(cepp->ce_fileptr->cf_filename,
- cepp->ce_varlinenum, "set::history::channel", cepp->ce_varname);
- errors++;
+ /* hmm.. I don't like this method. but I just quickly copied it from CONFIG_ALLOW for now... */
+ int used = 0;
+ Hook *h;
+ for (h = Hooks[HOOKTYPE_CONFIGTEST]; h; h = h->next)
+ {
+ int value, errs = 0;
+ if (h->owner && !(h->owner->flags & MODFLAG_TESTING)
+ && !(h->owner->options & MOD_OPT_PERM))
+ continue;
+ value = (*(h->func.intfunc))(cf, cepp, CONFIG_SET_HISTORY_CHANNEL, &errs);
+ if (value == 2)
+ used = 1;
+ if (value == 1)
+ {
+ used = 1;
+ break;
+ }
+ if (value == -1)
+ {
+ used = 1;
+ errors += errs;
+ break;
+ }
+ if (value == -2)
+ {
+ used = 1;
+ errors += errs;
+ }
+ }
+ if (!used)
+ {
+ config_error_unknown(cepp->ce_fileptr->cf_filename,
+ cepp->ce_varlinenum, "set::history::channel", cepp->ce_varname);
+ errors++;
+ }
}
}
} else {
@@ -217,23 +317,25 @@ int history_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
}
}
- if ((on_join_time && maximum_storage_time) && (on_join_time > maximum_storage_time))
- {
- config_error("set::history::channel::playback-on-join::time cannot be higher than set::history::channel::max-storage-per-channel::time. Either set the playback-on-join::time lower or the maximum::time higher.");
- errors++;
- }
- if ((on_join_lines && maximum_storage_lines) && (on_join_lines > maximum_storage_lines))
- {
- config_error("set::history::channel::playback-on-join::lines cannot be higher than set::history::channel::max-storage-per-channel::lines. Either set the playback-on-join::lines lower or the maximum::lines higher.");
- errors++;
- }
+ *errs = errors;
+ return errors ? -1 : 1;
+}
+
+int history_config_posttest(int *errs)
+{
+ int errors = 0;
+
+ /* We could check here for on join lines / on join time being bigger than max storage but..
+ * not really important.
+ */
+
*errs = errors;
return errors ? -1 : 1;
}
int history_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
{
- ConfigEntry *cep, *cepp, *cep4;
+ ConfigEntry *cep, *cepp, *cep4, *cep5;
if ((type != CONFIG_SET) || strcmp(ce->ce_varname, "history"))
return 0;
@@ -262,15 +364,44 @@ int history_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
{
for (cep4 = cepp->ce_entries; cep4; cep4 = cep4->ce_next)
{
- if (!strcmp(cep4->ce_varname, "lines"))
+ if (!strcmp(cep4->ce_varname, "registered"))
{
- cfg.max_storage_per_channel.lines = atoi(cep4->ce_vardata);
+ for (cep5 = cep4->ce_entries; cep5; cep5 = cep5->ce_next)
+ {
+ if (!strcmp(cep5->ce_varname, "lines"))
+ {
+ cfg.max_storage_per_channel_registered.lines = atoi(cep5->ce_vardata);
+ } else
+ if (!strcmp(cep5->ce_varname, "time"))
+ {
+ cfg.max_storage_per_channel_registered.time = config_checkval(cep5->ce_vardata, CFG_TIME);
+ }
+ }
} else
- if (!strcmp(cep4->ce_varname, "time"))
+ if (!strcmp(cep4->ce_varname, "unregistered"))
{
- cfg.max_storage_per_channel.time = config_checkval(cep4->ce_vardata, CFG_TIME);
+ for (cep5 = cep4->ce_entries; cep5; cep5 = cep5->ce_next)
+ {
+ if (!strcmp(cep5->ce_varname, "lines"))
+ {
+ cfg.max_storage_per_channel_unregistered.lines = atoi(cep5->ce_vardata);
+ } else
+ if (!strcmp(cep5->ce_varname, "time"))
+ {
+ cfg.max_storage_per_channel_unregistered.time = config_checkval(cep5->ce_vardata, CFG_TIME);
+ }
+ }
}
}
+ } else
+ {
+ Hook *h;
+ for (h = Hooks[HOOKTYPE_CONFIGRUN]; h; h = h->next)
+ {
+ int value = (*(h->func.intfunc))(cf, cepp, CONFIG_SET_HISTORY_CHANNEL);
+ if (value == 1)
+ break;
+ }
}
}
}
@@ -284,7 +415,7 @@ int history_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
* @param lines: The number of lines (the X in +H X:Y)
* @param t: The time value (the Y in +H X:Y)
*/
-int history_parse_chanmode(char *param, int *lines, long *t)
+int history_parse_chanmode(Channel *channel, char *param, int *lines, long *t)
{
char buf[64], *p, *q;
char contains_non_digit = 0;
@@ -327,12 +458,20 @@ int history_parse_chanmode(char *param, int *lines, long *t)
return 0;
/* Check imposed configuration limits... */
- if (*lines > cfg.max_storage_per_channel.lines)
- *lines = cfg.max_storage_per_channel.lines;
+ if (!channel || has_channel_mode(channel, 'r'))
+ {
+ if (*lines > cfg.max_storage_per_channel_registered.lines)
+ *lines = cfg.max_storage_per_channel_registered.lines;
- if (*t > cfg.max_storage_per_channel.time)
- *t = cfg.max_storage_per_channel.time;
+ if (*t > cfg.max_storage_per_channel_registered.time)
+ *t = cfg.max_storage_per_channel_registered.time;
+ } else {
+ if (*lines > cfg.max_storage_per_channel_unregistered.lines)
+ *lines = cfg.max_storage_per_channel_unregistered.lines;
+ if (*t > cfg.max_storage_per_channel_unregistered.time)
+ *t = cfg.max_storage_per_channel_unregistered.time;
+ }
return 1;
}
@@ -355,7 +494,7 @@ int history_chanmode_is_ok(Client *client, Channel *channel, char mode, char *pa
int lines = 0;
long t = 0L;
- if (!history_parse_chanmode(param, &lines, &t))
+ if (!history_parse_chanmode(channel, param, &lines, &t))
{
sendnumeric(client, ERR_CANNOTCHANGECHANMODE, 'H', "Invalid syntax for MODE +H. Use +H lines:period. The period must be in minutes (eg: 10) or a time value (eg: 1h).");
return EX_DENY;
@@ -369,19 +508,37 @@ int history_chanmode_is_ok(Client *client, Channel *channel, char mode, char *pa
return EX_DENY;
}
+static void history_chanmode_helper(char *buf, size_t bufsize, int lines, long t)
+{
+ if ((t % 86400) == 0)
+ {
+ /* Can be represented in full days, eg "1d" */
+ snprintf(buf, bufsize, "%d:%ldd", lines, t / 86400);
+ } else
+ if ((t % 3600) == 0)
+ {
+ /* Can be represented in hours, eg "8h" */
+ snprintf(buf, bufsize, "%d:%ldh", lines, t / 3600);
+ } else
+ {
+ /* Otherwise, stick to minutes */
+ snprintf(buf, bufsize, "%d:%ldm", lines, t / 60);
+ }
+}
+
/** Convert channel parameter to something proper.
* NOTE: client may be NULL if called for e.g. set::modes-playback-on-join
*/
-char *history_chanmode_conv_param(char *param, Client *client)
+char *history_chanmode_conv_param(char *param, Client *client, Channel *channel)
{
static char buf[64];
int lines = 0;
long t = 0L;
- if (!history_parse_chanmode(param, &lines, &t))
+ if (!history_parse_chanmode(channel, param, &lines, &t))
return NULL;
- snprintf(buf, sizeof(buf), "%d:%ldm", lines, t / 60);
+ history_chanmode_helper(buf, sizeof(buf), lines, t);
return buf;
}
@@ -392,7 +549,7 @@ void *history_chanmode_put_param(void *mode_in, char *param)
int lines = 0;
long t = 0L;
- if (!history_parse_chanmode(param, &lines, &t))
+ if (!history_parse_chanmode(NULL, param, &lines, &t))
return NULL;
if (!h)
@@ -416,13 +573,7 @@ char *history_chanmode_get_param(void *h_in)
if (!h_in)
return NULL;
- /* For now we convert the time to minutes for displaying purposes
- * and show it as eg 5:10m.
- * In a later release we can have a go at converting to '1h', '1d'
- * and such, but not before most people run 5.0.2+ as otherwise you
- * get desyncs in channel history retention times.
- */
- snprintf(buf, sizeof(buf), "%d:%ldm", h->max_lines, h->max_time / 60);
+ history_chanmode_helper(buf, sizeof(buf), h->max_lines, h->max_time);
return buf;
}
@@ -531,17 +682,117 @@ int history_chanmsg(Client *client, Channel *channel, int sendflags, int prefix,
int history_join(Client *client, Channel *channel, MessageTag *mtags, char *parv[])
{
- if (!HistoryEnabled(channel))
+ /* Only for +H channels */
+ if (!HistoryEnabled(channel) || !cfg.playback_on_join.lines || !cfg.playback_on_join.time)
+ return 0;
+
+ /* No history-on-join for clients that implement CHATHISTORY,
+ * they will pull history themselves if they need it.
+ */
+ if (HasCapability(client, "draft/chathistory") || HasCapability(client, "chathistory"))
return 0;
- if (MyUser(client))
+ if (MyUser(client) && can_receive_history(client))
{
HistoryFilter filter;
+ HistoryResult *r;
memset(&filter, 0, sizeof(filter));
+ filter.cmd = HFC_SIMPLE;
filter.last_lines = cfg.playback_on_join.lines;
filter.last_seconds = cfg.playback_on_join.time;
- history_request(client, channel->chname, &filter);
+ r = history_request(channel->chname, &filter);
+ if (r)
+ {
+ history_send_result(client, r);
+ free_history_result(r);
+ }
}
return 0;
}
+
+/** Check if a channel went from +r to -r and adjust +H if needed.
+ * This does not only override "MODE" but also "SAMODE", "SJOIN" and more.
+ */
+CMD_OVERRIDE_FUNC(override_mode)
+{
+ Channel *channel;
+ int had_r = 0;
+
+ /* We only bother checking for this corner case if the -r
+ * comes from a server directly linked to us, this normally
+ * means: we are the server that services are linked to.
+ */
+ if ((IsServer(client) && client->local) ||
+ (IsUser(client) && client->srvptr && client->srvptr->local))
+ {
+ /* Now check if the channel is currently +r */
+ if ((parc >= 2) && !BadPtr(parv[1]) && ((channel = find_channel(parv[1], NULL))) &&
+ has_channel_mode(channel, 'r'))
+ {
+ had_r = 1;
+ }
+ }
+ CallCommandOverride(ovr, client, recv_mtags, parc, parv);
+
+ /* If..
+ * - channel was +r
+ * - re-lookup the channel and check that it still
+ * exists (as it may have been destroyed)
+ * - and is now -r
+ * - and has +H set
+ * then...
+ */
+ if (had_r &&
+ ((channel = find_channel(parv[1], NULL))) &&
+ !has_channel_mode(channel, 'r') &&
+ HistoryEnabled(channel))
+ {
+ /* Check if limit is higher than allowed for unregistered channels */
+ HistoryChanMode *settings = (HistoryChanMode *)GETPARASTRUCT(channel, 'H');
+ int changed = 0;
+
+ if (!settings)
+ return; /* Weird */
+
+ if (settings->max_lines > cfg.max_storage_per_channel_unregistered.lines)
+ {
+ settings->max_lines = cfg.max_storage_per_channel_unregistered.lines;
+ changed = 1;
+ }
+
+ if (settings->max_time > cfg.max_storage_per_channel_unregistered.time)
+ {
+ settings->max_time = cfg.max_storage_per_channel_unregistered.time;
+ changed = 1;
+ }
+
+ if (changed)
+ {
+ MessageTag *mtags = NULL;
+ char *params = history_chanmode_get_param(settings);
+
+ if (!params)
+ return; /* Weird */
+
+ strlcpy(modebuf, "+H", sizeof(modebuf));
+ strlcpy(parabuf, params, sizeof(modebuf));
+
+ new_message(&me, NULL, &mtags);
+
+ sendto_channel(channel, &me, &me, 0, 0, SEND_LOCAL, mtags,
+ ":%s MODE %s %s %s",
+ me.name, channel->chname, modebuf, parabuf);
+ sendto_server(NULL, 0, 0, mtags, ":%s MODE %s %s %s %lld",
+ me.id, channel->chname, modebuf, parabuf,
+ (long long)channel->creationtime);
+
+ /* Activate this hook just like cmd_mode.c */
+ RunHook7(HOOKTYPE_REMOTE_CHANMODE, &me, channel, mtags, modebuf, parabuf, 0, 0);
+
+ free_message_tags(mtags);
+
+ *modebuf = *parabuf = '\0';
+ }
+ }
+}
diff --git a/src/modules/chanmodes/link.c b/src/modules/chanmodes/link.c
@@ -48,7 +48,7 @@ typedef enum {
int cmodeL_is_ok(Client *client, Channel *channel, char mode, char *para, int type, int what);
void *cmodeL_put_param(void *r_in, char *param);
char *cmodeL_get_param(void *r_in);
-char *cmodeL_conv_param(char *param_in, Client *client);
+char *cmodeL_conv_param(char *param_in, Client *client, Channel *channel);
void cmodeL_free_param(void *r);
void *cmodeL_dup_struct(void *r_in);
int cmodeL_sjoin_check(Channel *channel, void *ourx, void *theirx);
@@ -171,7 +171,7 @@ char *cmodeL_get_param(void *r_in)
/** Convert parameter to something proper.
* NOTE: client may be NULL
*/
-char *cmodeL_conv_param(char *param, Client *client)
+char *cmodeL_conv_param(char *param, Client *client, Channel *channel)
{
char *p;
diff --git a/src/modules/channeldb.c b/src/modules/channeldb.c
@@ -35,14 +35,14 @@ ModuleHeader MOD_HEADER = {
do { \
sendto_realops_and_log("[channeldb] Error writing to temporary database file " \
"'%s': %s (DATABASE NOT SAVED)", \
- fname, strerror(errno)); \
+ fname, unrealdb_get_error_string()); \
} while(0)
#define W_SAFE(x) \
do { \
if (!(x)) { \
WARN_WRITE_ERROR(tmpfname); \
- fclose(fd); \
+ unrealdb_close(db); \
return 0; \
} \
} while(0)
@@ -55,43 +55,53 @@ ModuleHeader MOD_HEADER = {
} \
} while(0)
+/* Structs */
+struct cfgstruct {
+ char *database;
+ char *db_secret;
+};
+
/* Forward declarations */
void channeldb_moddata_free(ModData *md);
-void setcfg(void);
-void freecfg(void);
-int channeldb_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
-int channeldb_configrun(ConfigFile *cf, ConfigEntry *ce, int type);
+void setcfg(struct cfgstruct *cfg);
+void freecfg(struct cfgstruct *cfg);
+int channeldb_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
+int channeldb_config_posttest(int *errs);
+int channeldb_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
EVENT(write_channeldb_evt);
int write_channeldb(void);
-int write_channel_entry(FILE *fd, const char *tmpfname, Channel *channel);
+int write_channel_entry(UnrealDB *db, const char *tmpfname, Channel *channel);
int read_channeldb(void);
static void set_channel_mode(Channel *channel, char *modes, char *parameters);
/* Global variables */
static uint32_t channeldb_version = CHANNELDB_VERSION;
-struct cfgstruct {
- char *database;
-};
static struct cfgstruct cfg;
+static struct cfgstruct test;
static long channeldb_next_event = 0;
MOD_TEST()
{
memset(&cfg, 0, sizeof(cfg));
- HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, channeldb_configtest);
+ memset(&test, 0, sizeof(test));
+ setcfg(&test);
+ HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, channeldb_config_test);
+ HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, channeldb_config_posttest);
return MOD_SUCCESS;
}
MOD_INIT()
{
MARK_AS_OFFICIAL_MODULE(modinfo);
+ /* We must unload early, when all channel modes and such are still in place: */
+ ModuleSetOptions(modinfo->handle, MOD_OPT_UNLOAD_PRIORITY, -99999999);
LoadPersistentLong(modinfo, channeldb_next_event);
- setcfg();
+ setcfg(&cfg);
- HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, channeldb_configrun);
+ HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, channeldb_config_run);
return MOD_SUCCESS;
}
@@ -122,7 +132,10 @@ MOD_LOAD()
MOD_UNLOAD()
{
- freecfg();
+ if (loop.ircd_terminating)
+ write_channeldb();
+ freecfg(&test);
+ freecfg(&cfg);
SavePersistentLong(modinfo, channeldb_next_event);
return MOD_SUCCESS;
}
@@ -133,19 +146,20 @@ void channeldb_moddata_free(ModData *md)
md->i = 0;
}
-void setcfg(void)
+void setcfg(struct cfgstruct *cfg)
{
// Default: data/channel.db
- safe_strdup(cfg.database, "channel.db");
- convert_to_absolute_path(&cfg.database, PERMDATADIR);
+ safe_strdup(cfg->database, "channel.db");
+ convert_to_absolute_path(&cfg->database, PERMDATADIR);
}
-void freecfg(void)
+void freecfg(struct cfgstruct *cfg)
{
- safe_free(cfg.database);
+ safe_free(cfg->database);
+ safe_free(cfg->db_secret);
}
-int channeldb_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
+int channeldb_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
int errors = 0;
ConfigEntry *cep;
@@ -159,16 +173,45 @@ int channeldb_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
for (cep = ce->ce_entries; cep; cep = cep->ce_next)
{
- if (!cep->ce_vardata) {
+ if (!cep->ce_vardata)
+ {
config_error("%s:%i: blank set::channeldb::%s without value", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname);
errors++;
- continue;
- }
- if (!strcmp(cep->ce_varname, "database")) {
+ } else
+ if (!strcmp(cep->ce_varname, "database"))
+ {
convert_to_absolute_path(&cep->ce_vardata, PERMDATADIR);
- continue;
+ safe_strdup(test.database, cep->ce_vardata);
+ } else
+ if (!strcmp(cep->ce_varname, "db-secret"))
+ {
+ char *err;
+ if ((err = unrealdb_test_secret(cep->ce_vardata)))
+ {
+ config_error("%s:%i: set::channeldb::db-secret: %s", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, err);
+ errors++;
+ continue;
+ }
+ safe_strdup(test.db_secret, cep->ce_vardata);
+ } else
+ {
+ config_error("%s:%i: unknown directive set::channeldb::%s", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname);
+ errors++;
}
- config_error("%s:%i: unknown directive set::channeldb::%s", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname);
+ }
+
+ *errs = errors;
+ return errors ? -1 : 1;
+}
+
+int channeldb_config_posttest(int *errs)
+{
+ int errors = 0;
+ char *errstr;
+
+ if (test.database && ((errstr = unrealdb_test_db(test.database, test.db_secret))))
+ {
+ config_error("[channeldb] %s", errstr);
errors++;
}
@@ -176,7 +219,7 @@ int channeldb_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
return errors ? -1 : 1;
}
-int channeldb_configrun(ConfigFile *cf, ConfigEntry *ce, int type)
+int channeldb_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
{
ConfigEntry *cep;
@@ -191,6 +234,8 @@ int channeldb_configrun(ConfigFile *cf, ConfigEntry *ce, int type)
{
if (!strcmp(cep->ce_varname, "database"))
safe_strdup(cfg.database, cep->ce_vardata);
+ else if (!strcmp(cep->ce_varname, "db-secret"))
+ safe_strdup(cfg.db_secret, cep->ce_vardata);
}
return 1;
}
@@ -206,7 +251,7 @@ EVENT(write_channeldb_evt)
int write_channeldb(void)
{
char tmpfname[512];
- FILE *fd;
+ UnrealDB *db;
Channel *channel;
int cnt = 0;
#ifdef BENCHMARK
@@ -217,33 +262,33 @@ int write_channeldb(void)
// Write to a tempfile first, then rename it if everything succeeded
snprintf(tmpfname, sizeof(tmpfname), "%s.%x.tmp", cfg.database, getrandom32());
- fd = fopen(tmpfname, "wb");
- if (!fd)
+ db = unrealdb_open(tmpfname, UNREALDB_MODE_WRITE, cfg.db_secret);
+ if (!db)
{
WARN_WRITE_ERROR(tmpfname);
return 0;
}
- W_SAFE(write_data(fd, &channeldb_version, sizeof(channeldb_version)));
+ W_SAFE(unrealdb_write_int32(db, channeldb_version));
/* First, count +P channels and write the count to the database */
for (channel = channels; channel; channel=channel->nextch)
if (has_channel_mode(channel, 'P'))
cnt++;
- W_SAFE(write_int64(fd, cnt));
+ W_SAFE(unrealdb_write_int64(db, cnt));
for (channel = channels; channel; channel=channel->nextch)
{
/* We only care about +P (persistent) channels */
if (has_channel_mode(channel, 'P'))
{
- if (!write_channel_entry(fd, tmpfname, channel))
+ if (!write_channel_entry(db, tmpfname, channel))
return 0;
}
}
// Everything seems to have gone well, attempt to close and rename the tempfile
- if (fclose(fd) != 0)
+ if (!unrealdb_close(db))
{
WARN_WRITE_ERROR(tmpfname);
return 0;
@@ -266,7 +311,7 @@ int write_channeldb(void)
return 1;
}
-int write_listmode(FILE *fd, const char *tmpfname, Ban *lst)
+int write_listmode(UnrealDB *db, const char *tmpfname, Ban *lst)
{
Ban *l;
int cnt = 0;
@@ -274,50 +319,50 @@ int write_listmode(FILE *fd, const char *tmpfname, Ban *lst)
/* First count and write the list count */
for (l = lst; l; l = l->next)
cnt++;
- W_SAFE(write_int32(fd, cnt));
+ W_SAFE(unrealdb_write_int32(db, cnt));
for (l = lst; l; l = l->next)
{
/* The entry, setby, seton */
- W_SAFE(write_str(fd, l->banstr));
- W_SAFE(write_str(fd, l->who));
- W_SAFE(write_int64(fd, l->when));
+ W_SAFE(unrealdb_write_str(db, l->banstr));
+ W_SAFE(unrealdb_write_str(db, l->who));
+ W_SAFE(unrealdb_write_int64(db, l->when));
}
return 1;
}
-int write_channel_entry(FILE *fd, const char *tmpfname, Channel *channel)
+int write_channel_entry(UnrealDB *db, const char *tmpfname, Channel *channel)
{
- W_SAFE(write_int32(fd, MAGIC_CHANNEL_START));
+ W_SAFE(unrealdb_write_int32(db, MAGIC_CHANNEL_START));
/* Channel name */
- W_SAFE(write_str(fd, channel->chname));
+ W_SAFE(unrealdb_write_str(db, channel->chname));
/* Channel creation time */
- W_SAFE(write_int64(fd, channel->creationtime));
+ W_SAFE(unrealdb_write_int64(db, channel->creationtime));
/* Topic (topic, setby, seton) */
- W_SAFE(write_str(fd, channel->topic));
- W_SAFE(write_str(fd, channel->topic_nick));
- W_SAFE(write_int64(fd, channel->topic_time));
+ W_SAFE(unrealdb_write_str(db, channel->topic));
+ W_SAFE(unrealdb_write_str(db, channel->topic_nick));
+ W_SAFE(unrealdb_write_int64(db, channel->topic_time));
/* Basic channel modes (eg: +sntkl key 55) */
channel_modes(&me, modebuf, parabuf, sizeof(modebuf), sizeof(parabuf), channel);
- W_SAFE(write_str(fd, modebuf));
- W_SAFE(write_str(fd, parabuf));
+ W_SAFE(unrealdb_write_str(db, modebuf));
+ W_SAFE(unrealdb_write_str(db, parabuf));
/* Mode lock */
- W_SAFE(write_str(fd, channel->mode_lock));
+ W_SAFE(unrealdb_write_str(db, channel->mode_lock));
/* List modes (bans, exempts, invex) */
- if (!write_listmode(fd, tmpfname, channel->banlist))
+ if (!write_listmode(db, tmpfname, channel->banlist))
return 0;
- if (!write_listmode(fd, tmpfname, channel->exlist))
+ if (!write_listmode(db, tmpfname, channel->exlist))
return 0;
- if (!write_listmode(fd, tmpfname, channel->invexlist))
+ if (!write_listmode(db, tmpfname, channel->invexlist))
return 0;
- W_SAFE(write_int32(fd, MAGIC_CHANNEL_END));
+ W_SAFE(unrealdb_write_int32(db, MAGIC_CHANNEL_END));
return 1;
}
#define R_SAFE(x) \
do { \
if (!(x)) { \
- config_warn("[channeldb] Read error from database file '%s' (possible corruption): %s", cfg.database, strerror(errno)); \
+ config_warn("[channeldb] Read error from database file '%s' (possible corruption): %s", cfg.database, unrealdb_get_error_string()); \
if (e) \
{ \
safe_free(e->banstr); \
@@ -328,21 +373,21 @@ int write_channel_entry(FILE *fd, const char *tmpfname, Channel *channel)
} \
} while(0)
-int read_listmode(FILE *fd, Ban **lst)
+int read_listmode(UnrealDB *db, Ban **lst)
{
uint32_t total;
uint64_t when;
int i;
Ban *e = NULL;
- R_SAFE(read_data(fd, &total, sizeof(total)));
+ R_SAFE(unrealdb_read_int32(db, &total));
for (i = 0; i < total; i++)
{
e = safe_alloc(sizeof(Ban));
- R_SAFE(read_str(fd, &e->banstr));
- R_SAFE(read_str(fd, &e->who));
- R_SAFE(read_data(fd, &when, sizeof(when)));
+ R_SAFE(unrealdb_read_str(db, &e->banstr));
+ R_SAFE(unrealdb_read_str(db, &e->who));
+ R_SAFE(unrealdb_read_int64(db, &when));
e->when = when;
e->next = *lst;
*lst = e;
@@ -366,8 +411,8 @@ int read_listmode(FILE *fd, Ban **lst)
#define R_SAFE(x) \
do { \
if (!(x)) { \
- config_warn("[channeldb] Read error from database file '%s' (possible corruption): %s", cfg.database, strerror(errno)); \
- fclose(fd); \
+ config_warn("[channeldb] Read error from database file '%s' (possible corruption): %s", cfg.database, unrealdb_get_error_string()); \
+ unrealdb_close(db); \
FreeChannelEntry(); \
return 0; \
} \
@@ -375,7 +420,7 @@ int read_listmode(FILE *fd, Ban **lst)
int read_channeldb(void)
{
- FILE *fd;
+ UnrealDB *db;
uint32_t version;
int added = 0;
int i;
@@ -397,29 +442,41 @@ int read_channeldb(void)
gettimeofday(&tv_alpha, NULL);
#endif
- fd = fopen(cfg.database, "rb");
- if (!fd)
+ db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, cfg.db_secret);
+ if (!db)
{
- if (errno == ENOENT)
+ if (unrealdb_get_error_code() == UNREALDB_ERROR_FILENOTFOUND)
{
/* Database does not exist. Could be first boot */
config_warn("[channeldb] No database present at '%s', will start a new one", cfg.database);
return 1;
- } else {
- config_warn("[channeldb] Unable to open the database file '%s' for reading: %s", cfg.database, strerror(errno));
+ } else
+ if (unrealdb_get_error_code() == UNREALDB_ERROR_NOTCRYPTED)
+ {
+ /* Re-open as unencrypted */
+ db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, NULL);
+ if (!db)
+ {
+ /* This should actually never happen, unless some weird I/O error */
+ config_warn("[channeldb] Unable to open the database file '%s': %s", cfg.database, unrealdb_get_error_string());
+ return 0;
+ }
+ } else
+ {
+ config_warn("[channeldb] Unable to open the database file '%s' for reading: %s", cfg.database, unrealdb_get_error_string());
return 0;
}
}
- R_SAFE(read_data(fd, &version, sizeof(version)));
+ R_SAFE(unrealdb_read_int32(db, &version));
if (version > channeldb_version)
{
config_warn("[channeldb] Database '%s' has a wrong version: expected it to be <= %u but got %u instead", cfg.database, channeldb_version, version);
- fclose(fd);
+ unrealdb_close(db);
return 0;
}
- R_SAFE(read_data(fd, &count, sizeof(count)));
+ R_SAFE(unrealdb_read_int64(db, &count));
for (i=1; i <= count; i++)
{
@@ -434,20 +491,20 @@ int read_channeldb(void)
mode_lock = NULL;
Channel *channel;
- R_SAFE(read_data(fd, &magic, sizeof(magic)));
+ R_SAFE(unrealdb_read_int32(db, &magic));
if (magic != MAGIC_CHANNEL_START)
{
config_error("[channeldb] Corrupt database (%s) - channel magic start is 0x%x. Further reading aborted.", cfg.database, magic);
break;
}
- R_SAFE(read_str(fd, &chname));
- R_SAFE(read_data(fd, &creationtime, sizeof(creationtime)));
- R_SAFE(read_str(fd, &topic));
- R_SAFE(read_str(fd, &topic_nick));
- R_SAFE(read_data(fd, &topic_time, sizeof(topic_time)));
- R_SAFE(read_str(fd, &modes1));
- R_SAFE(read_str(fd, &modes2));
- R_SAFE(read_str(fd, &mode_lock));
+ R_SAFE(unrealdb_read_str(db, &chname));
+ R_SAFE(unrealdb_read_int64(db, &creationtime));
+ R_SAFE(unrealdb_read_str(db, &topic));
+ R_SAFE(unrealdb_read_str(db, &topic_nick));
+ R_SAFE(unrealdb_read_int64(db, &topic_time));
+ R_SAFE(unrealdb_read_str(db, &modes1));
+ R_SAFE(unrealdb_read_str(db, &modes2));
+ R_SAFE(unrealdb_read_str(db, &mode_lock));
/* If we got this far, we can create/initialize the channel with the above */
channel = get_channel(&me, chname, CREATE);
channel->creationtime = creationtime;
@@ -456,10 +513,10 @@ int read_channeldb(void)
channel->topic_time = topic_time;
safe_strdup(channel->mode_lock, mode_lock);
set_channel_mode(channel, modes1, modes2);
- R_SAFE(read_listmode(fd, &channel->banlist));
- R_SAFE(read_listmode(fd, &channel->exlist));
- R_SAFE(read_listmode(fd, &channel->invexlist));
- R_SAFE(read_data(fd, &magic, sizeof(magic)));
+ R_SAFE(read_listmode(db, &channel->banlist));
+ R_SAFE(read_listmode(db, &channel->exlist));
+ R_SAFE(read_listmode(db, &channel->invexlist));
+ R_SAFE(unrealdb_read_int32(db, &magic));
FreeChannelEntry();
added++;
if (magic != MAGIC_CHANNEL_END)
@@ -469,7 +526,7 @@ int read_channeldb(void)
}
}
- fclose(fd);
+ unrealdb_close(db);
if (added)
sendto_realops_and_log("[channeldb] Added %d persistent channels (+P)", added);
diff --git a/src/modules/chathistory.c b/src/modules/chathistory.c
@@ -0,0 +1,294 @@
+/* src/modules/chathistory.c - IRCv3 CHATHISTORY command.
+ * (C) Copyright 2021 Bram Matthys (Syzop) and the UnrealIRCd team
+ * License: GPLv2
+ *
+ * This implements the "CHATHISTORY" command, the CAP and 005 token.
+ * https://ircv3.net/specs/extensions/chathistory
+ */
+#include "unrealircd.h"
+
+ModuleHeader MOD_HEADER
+= {
+ "chathistory",
+ "1.0",
+ "IRCv3 CHATHISTORY command",
+ "UnrealIRCd Team",
+ "unrealircd-5",
+};
+
+/* Forward declarations */
+CMD_FUNC(cmd_chathistory);
+
+/* Global variables */
+long CAP_CHATHISTORY = 0L;
+
+/* TODO: consider moving to config file */
+#define CHATHISTORY_LIMIT 50
+
+MOD_INIT()
+{
+ ClientCapabilityInfo c;
+
+ MARK_AS_OFFICIAL_MODULE(modinfo);
+ CommandAdd(modinfo->handle, "CHATHISTORY", cmd_chathistory, MAXPARA, CMD_USER);
+
+ memset(&c, 0, sizeof(c));
+ c.name = "draft/chathistory";
+ ClientCapabilityAdd(modinfo->handle, &c, &CAP_CHATHISTORY);
+ return MOD_SUCCESS;
+}
+
+MOD_LOAD()
+{
+ ISupportSetFmt(modinfo->handle, "CHATHISTORY", "%d", CHATHISTORY_LIMIT);
+ return MOD_SUCCESS;
+}
+
+MOD_UNLOAD()
+{
+ return MOD_SUCCESS;
+}
+
+int chathistory_token(char *str, char *token, char **store)
+{
+ char *p = strchr(str, '=');
+ if (!p)
+ return 0;
+ *p = '\0'; // frag
+ if (!strcmp(str, token))
+ {
+ *p = '='; // restore
+ *store = strdup(p + 1); // can be \0
+ return 1;
+ }
+ *p = '='; // restore
+ return 0;
+}
+
+static int chathistory_targets_send_line(Client *client, HistoryResult *r, char *batchid)
+{
+ MessageTag *mtags = NULL;
+ MessageTag *m;
+ char *ts;
+
+ if (!r->log || !((m = find_mtag(r->log->mtags, "time"))) || !m->value)
+ return 0;
+ ts = m->value;
+
+ if (!BadPtr(batchid))
+ {
+ mtags = safe_alloc(sizeof(MessageTag));
+ mtags->name = strdup("batch");
+ mtags->value = strdup(batchid);
+ }
+
+ sendto_one(client, mtags, ":%s CHATHISTORY TARGETS %s %s",
+ me.name, r->object, ts);
+
+ if (mtags)
+ free_message_tags(mtags);
+
+ return 1;
+}
+
+void chathistory_targets(Client *client, HistoryFilter *filter, int limit)
+{
+ Membership *mp;
+ HistoryResult *r;
+ char batch[BATCHLEN+1];
+ int sent = 0;
+
+ batch[0] = '\0';
+ if (HasCapability(client, "batch"))
+ {
+ /* Start a new batch */
+ generate_batch_id(batch);
+ sendto_one(client, NULL, ":%s BATCH +%s draft/chathistory-targets", me.name, batch);
+ }
+
+ filter->cmd = HFC_BEFORE;
+ if (strcmp(filter->timestamp_a, filter->timestamp_b) < 0)
+ {
+ /* Swap if needed */
+ char *swap = filter->timestamp_a;
+ filter->timestamp_a = filter->timestamp_b;
+ filter->timestamp_b = swap;
+ }
+ filter->limit = 1;
+
+ for (mp = client->user->channel; mp; mp = mp->next)
+ {
+ Channel *channel = mp->channel;
+ r = history_request(channel->chname, filter);
+ if (r->log && chathistory_targets_send_line(client, r, batch))
+ {
+ if (++sent >= limit)
+ break; /* We are done */
+ }
+ free_history_result(r);
+ r = NULL;
+ }
+
+ /* End of batch */
+ if (*batch)
+ sendto_one(client, NULL, ":%s BATCH -%s", me.name, batch);
+}
+
+CMD_FUNC(cmd_chathistory)
+{
+ HistoryFilter *filter = NULL;
+ HistoryResult *r = NULL;
+ Channel *channel;
+
+ memset(&filter, 0, sizeof(filter));
+
+ /* This command is only for local users */
+ if (!MyUser(client))
+ return;
+
+ if ((parc < 5) || BadPtr(parv[4]))
+ {
+ sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS :Insufficient parameters", me.name);
+ return;
+ }
+
+ if (!HasCapability(client, "server-time"))
+ {
+ sendnotice(client, "Your IRC client does not support the 'server-time' capability");
+ sendnotice(client, "https://ircv3.net/specs/extensions/server-time");
+ sendnotice(client, "History request refused.");
+ return;
+ }
+
+ if (!strcmp(parv[1], "TARGETS"))
+ {
+ Membership *mp;
+ int limit;
+
+ filter = safe_alloc(sizeof(HistoryFilter));
+ /* Below this point, instead of 'return', use 'goto end' */
+
+ if (!chathistory_token(parv[2], "timestamp", &filter->timestamp_a))
+ {
+ sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx",
+ me.name, parv[1], parv[3]);
+ goto end;
+ }
+ if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_b))
+ {
+ sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx",
+ me.name, parv[1], parv[4]);
+ goto end;
+ }
+ limit = atoi(parv[4]);
+ chathistory_targets(client, filter, limit);
+ goto end;
+ }
+
+ channel = find_channel(parv[2], NULL);
+ if (!channel || !IsMember(client, channel) || !has_channel_mode(channel, 'H'))
+ {
+ sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_TARGET %s %s :Messages could not be retrieved",
+ me.name, parv[1], parv[2]);
+ return;
+ }
+
+ filter = safe_alloc(sizeof(HistoryFilter));
+ /* Below this point, instead of 'return', use 'goto end', which takes care of the freeing of 'filter' and 'history' */
+
+ if (!strcmp(parv[1], "BEFORE"))
+ {
+ filter->cmd = HFC_BEFORE;
+ if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) &&
+ !chathistory_token(parv[3], "msgid", &filter->msgid_a))
+ {
+ sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx",
+ me.name, parv[1], parv[3]);
+ goto end;
+ }
+ filter->limit = atoi(parv[4]);
+ } else
+ if (!strcmp(parv[1], "AFTER"))
+ {
+ filter->cmd = HFC_AFTER;
+ if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) &&
+ !chathistory_token(parv[3], "msgid", &filter->msgid_a))
+ {
+ sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx",
+ me.name, parv[1], parv[3]);
+ goto end;
+ }
+ filter->limit = atoi(parv[4]);
+ } else
+ if (!strcmp(parv[1], "LATEST"))
+ {
+ filter->cmd = HFC_LATEST;
+ if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) &&
+ !chathistory_token(parv[3], "msgid", &filter->msgid_a) &&
+ strcmp(parv[3], "*"))
+ {
+ sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx or *",
+ me.name, parv[1], parv[3]);
+ goto end;
+ }
+ filter->limit = atoi(parv[4]);
+ } else
+ if (!strcmp(parv[1], "AROUND"))
+ {
+ filter->cmd = HFC_AROUND;
+ if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) &&
+ !chathistory_token(parv[3], "msgid", &filter->msgid_a))
+ {
+ sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx",
+ me.name, parv[1], parv[3]);
+ goto end;
+ }
+ filter->limit = atoi(parv[4]);
+ } else
+ if (!strcmp(parv[1], "BETWEEN"))
+ {
+ filter->cmd = HFC_BETWEEN;
+ if (BadPtr(parv[5]))
+ {
+ sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s :Insufficient parameters", parv[1], me.name);
+ goto end;
+ }
+ if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) &&
+ !chathistory_token(parv[3], "msgid", &filter->msgid_a))
+ {
+ sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx",
+ me.name, parv[1], parv[3]);
+ goto end;
+ }
+ if (!chathistory_token(parv[4], "timestamp", &filter->timestamp_b) &&
+ !chathistory_token(parv[4], "msgid", &filter->msgid_b))
+ {
+ sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx",
+ me.name, parv[1], parv[4]);
+ goto end;
+ }
+ filter->limit = atoi(parv[5]);
+ } else {
+ sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s :Invalid subcommand", me.name, parv[1]);
+ goto end;
+ }
+
+ if (filter->limit <= 0)
+ {
+ sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %d :Specified limit is =<0",
+ me.name, parv[1], filter->limit);
+ goto end;
+ }
+
+ if (filter->limit > CHATHISTORY_LIMIT)
+ filter->limit = CHATHISTORY_LIMIT;
+
+ if ((r = history_request(channel->chname, filter)))
+ history_send_result(client, r);
+
+end:
+ if (filter)
+ free_history_filter(filter);
+ if (r)
+ free_history_result(r);
+}
diff --git a/src/modules/dccdeny.c b/src/modules/dccdeny.c
@@ -639,7 +639,7 @@ static int can_dcc(Client *client, char *target, Client *targetcli, char *filena
return 0;
}
- if (match_spamfilter(client, filename, SPAMF_DCC, target, 0, NULL))
+ if (match_spamfilter(client, filename, SPAMF_DCC, "PRIVMSG", target, 0, NULL))
return 0;
if ((fl = dcc_isforbidden(client, filename)))
diff --git a/src/modules/history.c b/src/modules/history.c
@@ -72,6 +72,7 @@ void history_usage(Client *client)
CMD_FUNC(cmd_history)
{
HistoryFilter filter;
+ HistoryResult *r;
Channel *channel;
int lines = HISTORY_LINES_DEFAULT;
@@ -115,12 +116,18 @@ CMD_FUNC(cmd_history)
if (!HasCapability(client, "server-time"))
{
sendnotice(client, "Your IRC client does not support the 'server-time' capability");
- sendnotice(client, "https://ircv3.net/specs/extensions/server-time-3.2.html");
+ sendnotice(client, "https://ircv3.net/specs/extensions/server-time");
sendnotice(client, "History request refused.");
return;
}
memset(&filter, 0, sizeof(filter));
+ filter.cmd = HFC_SIMPLE;
filter.last_lines = lines;
- history_request(client, channel->chname, &filter);
+
+ if ((r = history_request(channel->chname, &filter)))
+ {
+ history_send_result(client, r);
+ free_history_result(r);
+ }
}
diff --git a/src/modules/history_backend_mem.c b/src/modules/history_backend_mem.c
@@ -1,5 +1,5 @@
/* src/modules/history_backend_mem.c - History Backend: memory
- * (C) Copyright 2019 Bram Matthys (Syzop) and the UnrealIRCd team
+ * (C) Copyright 2019-2021 Bram Matthys (Syzop) and the UnrealIRCd team
* License: GPLv2
*/
#include "unrealircd.h"
@@ -34,19 +34,34 @@ ModuleHeader MOD_HEADER
* HISTORY_MAX_OFF_SECS: how many seconds may the history be 'off',
* that is: how much may we store the history longer than required.
* The other 2 macros are calculated based on that target.
+ *
+ * Update April 2021: these values are now also used for saving the
+ * history if the persistent option is enabled. Therefore changed the
+ * values to spread it even more out: from 16/128 to 60/300 so
+ * in case of persistent it will save every 5 minutes.
*/
-#define HISTORY_SPREAD 16
-#define HISTORY_MAX_OFF_SECS 128
+#if 0 //was: DEBUGMODE
+#define HISTORY_CLEAN_PER_LOOP HISTORY_BACKEND_MEM_HASH_TABLE_SIZE
+#define HISTORY_TIMER_EVERY 5
+#else
+#define HISTORY_SPREAD 60
+#define HISTORY_MAX_OFF_SECS 300
#define HISTORY_CLEAN_PER_LOOP (HISTORY_BACKEND_MEM_HASH_TABLE_SIZE/HISTORY_SPREAD)
#define HISTORY_TIMER_EVERY (HISTORY_MAX_OFF_SECS/HISTORY_SPREAD)
+#endif
-/* Definitions (structs, etc.) */
-typedef struct HistoryLogLine HistoryLogLine;
-struct HistoryLogLine {
- HistoryLogLine *prev, *next;
- time_t t;
- MessageTag *mtags;
- char line[1];
+/* Some magic numbers used in the database format */
+#define HISTORYDB_MAGIC_FILE_START 0xFEFEFEFE
+#define HISTORYDB_MAGIC_FILE_END 0xEFEFEFEF
+#define HISTORYDB_MAGIC_ENTRY_START 0xFFFFFFFF
+#define HISTORYDB_MAGIC_ENTRY_END 0xEEEEEEEE
+
+/* Definitions (structs, etc.) -- all for persistent history */
+struct cfgstruct {
+ int persist;
+ char *directory;
+ char *masterdb; /* Autogenerated for convenience, not a real config item */
+ char *db_secret;
};
typedef struct HistoryLogObject HistoryLogObject;
@@ -58,30 +73,86 @@ struct HistoryLogObject {
time_t oldest_t; /**< Oldest time in log */
int max_lines; /**< Maximum number of lines permitted */
long max_time; /**< Maximum number of seconds to retain history */
+ int dirty; /**< Dirty flag, used for disk writing */
char name[OBJECTLEN+1];
};
/* Global variables */
-static char siphashkey_history_backend_mem[SIPHASH_KEY_LENGTH];
-HistoryLogObject *history_hash_table[HISTORY_BACKEND_MEM_HASH_TABLE_SIZE];
+struct cfgstruct cfg;
+struct cfgstruct test;
+static char *siphashkey_history_backend_mem = NULL;
+HistoryLogObject **history_hash_table;
+static long already_loaded = 0;
+static char *hbm_prehash = NULL;
+static char *hbm_posthash = NULL;
/* Forward declarations */
+int hbm_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
+int hbm_config_posttest(int *errs);
+int hbm_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
+int hbm_rehash(void);
+int hbm_rehash_complete(void);
+static void setcfg(struct cfgstruct *cfg);
+static void freecfg(struct cfgstruct *cfg);
+static void hbm_init_hashes(ModuleInfo *m);
+static void init_history_storage(ModuleInfo *modinfo);
+int hbm_modechar_del(Channel *channel, int modechar);
int hbm_history_add(char *object, MessageTag *mtags, char *line);
int hbm_history_cleanup(HistoryLogObject *h);
-int hbm_history_request(Client *client, char *object, HistoryFilter *filter);
+HistoryResult *hbm_history_request(char *object, HistoryFilter *filter);
int hbm_history_destroy(char *object);
int hbm_history_set_limit(char *object, int max_lines, long max_time);
EVENT(history_mem_clean);
+EVENT(history_mem_init);
+static int hbm_read_masterdb(void);
+static void hbm_read_dbs(void);
+static int hbm_read_db(char *fname);
+static int hbm_write_masterdb(void);
+static int hbm_write_db(HistoryLogObject *h);
+static void hbm_delete_db(HistoryLogObject *h);
+static void hbm_flush(void);
+void hbm_generic_free(ModData *m);
+void hbm_free_all_history(ModData *m);
+
+MOD_TEST()
+{
+ hbm_init_hashes(modinfo);
+ memset(&cfg, 0, sizeof(cfg));
+ memset(&test, 0, sizeof(test));
+ setcfg(&test);
+ HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, hbm_config_test);
+ HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, hbm_config_posttest);
+
+ return MOD_SUCCESS;
+}
MOD_INIT()
{
HistoryBackendInfo hbi;
MARK_AS_OFFICIAL_MODULE(modinfo);
- ModuleSetOptions(modinfo->handle, MOD_OPT_PERM, 1);
+ /* We must unload early, when all channel modes and such are still in place: */
+ ModuleSetOptions(modinfo->handle, MOD_OPT_UNLOAD_PRIORITY, -99999999);
+
+ setcfg(&cfg);
- memset(&history_hash_table, 0, sizeof(history_hash_table));
- siphash_generate_key(siphashkey_history_backend_mem);
+ LoadPersistentLong(modinfo, already_loaded);
+ LoadPersistentPointer(modinfo, siphashkey_history_backend_mem, hbm_generic_free);
+ LoadPersistentPointer(modinfo, history_hash_table, hbm_free_all_history);
+ if (history_hash_table == NULL)
+ history_hash_table = safe_alloc(sizeof(HistoryLogObject *) * HISTORY_BACKEND_MEM_HASH_TABLE_SIZE);
+ /* hbm_prehash & hbm_posthash already loaded in MOD_TEST through hbm_init_hashes() */
+
+ HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, hbm_config_run);
+ HookAdd(modinfo->handle, HOOKTYPE_MODECHAR_DEL, 0, hbm_modechar_del);
+ HookAdd(modinfo->handle, HOOKTYPE_REHASH, 0, hbm_rehash);
+ HookAdd(modinfo->handle, HOOKTYPE_REHASH_COMPLETE, 0, hbm_rehash_complete);
+
+ if (siphashkey_history_backend_mem == NULL)
+ {
+ siphashkey_history_backend_mem = safe_alloc(SIPHASH_KEY_LENGTH);
+ siphash_generate_key(siphashkey_history_backend_mem);
+ }
memset(&hbi, 0, sizeof(hbi));
hbi.name = "mem";
@@ -97,15 +168,264 @@ MOD_INIT()
MOD_LOAD()
{
+ /* Need to save these here already (after conf reading these are set),
+ * as on next round the module reads it in TEST which happens before
+ * the saving in MOD_UNLOAD:
+ */
+ SavePersistentPointer(modinfo, hbm_prehash);
+ SavePersistentPointer(modinfo, hbm_posthash);
+
+ EventAdd(modinfo->handle, "history_mem_init", history_mem_init, NULL, 1, 1);
EventAdd(modinfo->handle, "history_mem_clean", history_mem_clean, NULL, HISTORY_TIMER_EVERY*1000, 0);
+ init_history_storage(modinfo);
return MOD_SUCCESS;
}
+/* Read the .db if 'persist' mode is enabled.
+ * Normally this would be in MOD_LOAD, but the load order always
+ * must be: channeldb first, this module second, and since we
+ * cannot influence the load order we do this silly trick
+ * with a one-time 1msec event.
+ */
+EVENT(history_mem_init)
+{
+ if (!already_loaded)
+ {
+ /* Initial boot / load of the module... */
+ already_loaded = 1;
+ if (cfg.persist)
+ hbm_read_dbs();
+ }
+}
+
MOD_UNLOAD()
{
+ if (loop.ircd_terminating)
+ hbm_flush();
+ freecfg(&test);
+ freecfg(&cfg);
+ SavePersistentPointer(modinfo, hbm_prehash);
+ SavePersistentPointer(modinfo, hbm_posthash);
+ SavePersistentPointer(modinfo, history_hash_table);
+ SavePersistentPointer(modinfo, siphashkey_history_backend_mem);
+ SavePersistentLong(modinfo, already_loaded);
return MOD_SUCCESS;
}
+/** Set cfg->masterdb based on cfg->directory, for convenience */
+static void hbm_set_masterdb_filename(struct cfgstruct *cfg)
+{
+ char buf[512];
+
+ safe_free(cfg->masterdb);
+ if (cfg->directory)
+ {
+ snprintf(buf, sizeof(buf), "%s/master.db", cfg->directory);
+ safe_strdup(cfg->masterdb, buf);
+ }
+}
+
+/** Default configuration for set::history::channel */
+static void setcfg(struct cfgstruct *cfg)
+{
+ safe_strdup(cfg->directory, "history");
+ convert_to_absolute_path(&cfg->directory, PERMDATADIR);
+ hbm_set_masterdb_filename(cfg);
+}
+
+static void freecfg(struct cfgstruct *cfg)
+{
+ safe_free(cfg->directory);
+ safe_free(cfg->db_secret);
+}
+
+static void hbm_init_hashes(ModuleInfo *modinfo)
+{
+ char buf[256];
+
+ LoadPersistentPointer(modinfo, hbm_prehash, hbm_generic_free);
+ LoadPersistentPointer(modinfo, hbm_posthash, hbm_generic_free);
+
+ if (!hbm_prehash)
+ {
+ gen_random_alnum(buf, 128);
+ safe_strdup(hbm_prehash, buf);
+ }
+
+ if (!hbm_posthash)
+ {
+ gen_random_alnum(buf, 128);
+ safe_strdup(hbm_posthash, buf);
+ }
+}
+
+/** Test the set::history::channel configuration */
+int hbm_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
+{
+ int errors = 0;
+
+ if ((type != CONFIG_SET_HISTORY_CHANNEL) || !ce || !ce->ce_varname)
+ return 0;
+
+ if (!strcmp(ce->ce_varname, "persist"))
+ {
+ if (!ce->ce_vardata)
+ {
+ config_error("%s:%i: missing parameter",
+ ce->ce_fileptr->cf_filename, ce->ce_varlinenum);
+ errors++;
+ } else {
+ test.persist = config_checkval(ce->ce_vardata, CFG_YESNO);
+ }
+ } else
+ if (!strcmp(ce->ce_varname, "db-secret"))
+ {
+ char *err;
+ if ((err = unrealdb_test_secret(ce->ce_vardata)))
+ {
+ config_error("%s:%i: set::history::channel::db-secret: %s", ce->ce_fileptr->cf_filename, ce->ce_varlinenum, err);
+ errors++;
+ }
+ safe_strdup(test.db_secret, ce->ce_vardata);
+ } else
+ if (!strcmp(ce->ce_varname, "directory")) // or "path" ?
+ {
+ if (!ce->ce_vardata)
+ {
+ config_error("%s:%i: missing parameter",
+ ce->ce_fileptr->cf_filename, ce->ce_varlinenum);
+ errors++;
+ } else
+ {
+ safe_strdup(test.directory, ce->ce_vardata);
+ hbm_set_masterdb_filename(&test);
+ }
+ } else
+ {
+ return 0; /* unknown option to us, let another module handle it */
+ }
+
+ *errs = errors;
+ return errors ? -1 : 1;
+}
+
+/** Post-configuration test on set::history::channel */
+int hbm_config_posttest(int *errs)
+{
+ int errors = 0;
+
+ if (test.db_secret && !test.persist)
+ {
+ config_error("set::history::channel::db-secret is set but set::history::channel::persist is disabled, this makes no sense. "
+ "Either use 'persist yes' or comment out / delete 'db-secret'.");
+ errors++;
+ } else
+ if (!test.db_secret && test.persist)
+ {
+ config_error("set::history::channel::db-secret needs to be set."); // TODO: REFER TO FAQ OR OTHER ENTRY!!!!
+ errors++;
+ } else
+ if (test.db_secret && test.persist)
+ {
+ /* Configuration is good, now check if the password is correct
+ * (if we can check at all, that is)...
+ */
+ char *errstr = NULL;
+ if (test.masterdb && ((errstr = unrealdb_test_db(test.masterdb, test.db_secret))))
+ {
+ config_error("[history] %s", errstr);
+ errors++;
+ goto hbm_config_posttest_end;
+ }
+
+ /* Ensure directory exists and is writable */
+#ifdef _WIN32
+ (void)mkdir(test.directory); /* (errors ignored) */
+#else
+ (void)mkdir(test.directory, S_IRUSR|S_IWUSR|S_IXUSR); /* (errors ignored) */
+#endif
+ if (!file_exists(test.directory))
+ {
+ config_error("[history] Directory %s does not exist and could not be created",
+ test.directory);
+ errors++;
+ } else
+ {
+ /* Only do this if directory actually exists, hence in the 'else' block */
+ if (!hbm_read_masterdb())
+ errors++;
+ }
+ }
+
+hbm_config_posttest_end:
+ freecfg(&test);
+ setcfg(&test);
+ *errs = errors;
+ return errors ? -1 : 1;
+}
+
+/** Configure ourselves based on the set::history::channel settings */
+int hbm_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
+{
+ if ((type != CONFIG_SET_HISTORY_CHANNEL) || !ce || !ce->ce_varname)
+ return 0;
+
+ if (!strcmp(ce->ce_varname, "persist"))
+ {
+ cfg.persist = config_checkval(ce->ce_vardata, CFG_YESNO);
+ } else
+ if (!strcmp(ce->ce_varname, "directory")) // or "path" ?
+ {
+ safe_strdup(cfg.directory, ce->ce_vardata);
+ convert_to_absolute_path(&cfg.directory, PERMDATADIR);
+ hbm_set_masterdb_filename(&cfg);
+ } else
+ if (!strcmp(ce->ce_varname, "db-secret"))
+ {
+ safe_strdup(cfg.db_secret, ce->ce_vardata);
+ } else
+ {
+ return 0; /* unknown option to us, let another module handle it */
+ }
+
+ return 1; /* handled by us */
+}
+
+int hbm_rehash(void)
+{
+ freecfg(&cfg);
+ setcfg(&cfg);
+ return 0;
+}
+
+int hbm_rehash_complete(void)
+{
+ return 0;
+}
+
+char *history_storage_capability_parameter(Client *client)
+{
+ static char buf[128];
+
+ if (cfg.persist)
+ strlcpy(buf, "memory,disk=encrypted", sizeof(buf));
+ else
+ strlcpy(buf, "memory", sizeof(buf));
+
+ return buf;
+}
+
+static void init_history_storage(ModuleInfo *modinfo)
+{
+ ClientCapabilityInfo cap;
+
+ memset(&cap, 0, sizeof(cap));
+ cap.name = "unrealircd.org/history-storage";
+ cap.flags = CLICAP_FLAGS_ADVERTISE_ONLY;
+ cap.parameter = history_storage_capability_parameter;
+ ClientCapabilityAdd(modinfo->handle, &cap, NULL);
+}
+
uint64_t hbm_hash(char *object)
{
return siphash_nocase(object, siphashkey_history_backend_mem) % HISTORY_BACKEND_MEM_HASH_TABLE_SIZE;
@@ -143,12 +463,38 @@ HistoryLogObject *hbm_find_or_add_object(char *object)
void hbm_delete_object_hlo(HistoryLogObject *h)
{
- int hashv = hbm_hash(h->name);
+ int hashv;
+ if (cfg.persist)
+ hbm_delete_db(h);
+
+ hashv = hbm_hash(h->name);
DelListItem(h, history_hash_table[hashv]);
safe_free(h);
}
+int hbm_modechar_del(Channel *channel, int modechar)
+{
+ HistoryLogObject *h;
+
+ if (!cfg.persist)
+ return 0;
+
+ if ((modechar == 'P') && ((h = hbm_find_object(channel->chname))))
+ {
+ /* Channel went from +P to -P and also has channel history: delete the history file */
+ hbm_delete_db(h);
+
+ h->dirty = 1;
+ /* The reason for marking the entry as 'dirty' is that someone may later
+ * set the channel +P again. If we would not set the h->dirty=1 then this
+ * would mean the history log would not get rewritten until someone speaks.
+ */
+ }
+
+ return 0;
+}
+
void hbm_duplicate_mtags(HistoryLogLine *l, MessageTag *m)
{
MessageTag *n;
@@ -207,6 +553,7 @@ void hbm_history_add_line(HistoryLogObject *h, MessageTag *mtags, char *line)
/* no tail, no head */
h->head = h->tail = l;
}
+ h->dirty = 1;
h->num_lines++;
if ((l->t < h->oldest_t) || (h->oldest_t == 0))
h->oldest_t = l->t;
@@ -233,6 +580,7 @@ void hbm_history_del_line(HistoryLogObject *h, HistoryLogLine *l)
free_message_tags(l->mtags);
safe_free(l);
+ h->dirty = 1;
h->num_lines--;
/* IMPORTANT: updating h->oldest_t takes place at the caller
@@ -263,54 +611,188 @@ int hbm_history_add(char *object, MessageTag *mtags, char *line)
return 0;
}
-int can_receive_history(Client *client)
+HistoryLogLine *duplicate_log_line(HistoryLogLine *l)
{
- if (HasCapability(client, "server-time"))
- return 1;
- return 0;
+ HistoryLogLine *n = safe_alloc(sizeof(HistoryLogLine) + strlen(l->line));
+ strcpy(n->line, l->line); /* safe, see memory allocation above ^ */
+ hbm_duplicate_mtags(n, l->mtags);
+ return n;
}
-void hbm_send_line(Client *client, HistoryLogLine *l, char *batchid)
+/** Quickly append a new line 'n' to result 'r' */
+static void hbm_result_append_line(HistoryResult *r, HistoryLogLine *n)
{
- if (can_receive_history(client))
+ if (!r->log)
+ {
+ /* First item */
+ r->log = r->log_tail = n;
+ } else
{
- if (BadPtr(batchid))
+ /* Quick append to tail */
+ r->log_tail->next = n;
+ n->prev = r->log_tail;
+ r->log_tail = n; /* we are the new tail */
+ }
+}
+
+/** Quickly prepend a new line 'n' to result 'r' */
+static void hbm_result_prepend_line(HistoryResult *r, HistoryLogLine *n)
+{
+ if (!r->log)
+ r->log_tail = n;
+ AddListItem(n, r->log);
+}
+
+/** Put lines in HistoryResult that are after a certain msgid or
+ * timestamp (excluding said msgid/timestamp).
+ * @param r The history result set that we will use
+ * @param h The history log object
+ * @param filter The filter that applies
+ * @returns Number of lines written, note that this could be zero,
+ * which is a perfectly valid result.
+ */
+static int hbm_return_after(HistoryResult *r, HistoryLogObject *h, HistoryFilter *filter)
+{
+ HistoryLogLine *l, *n;
+ int written = 0;
+ int started = 0;
+ MessageTag *m;
+
+ for (l = h->head; l; l = l->next)
+ {
+ /* Not started yet? Check if this is the starting point... */
+ if (!started)
{
- sendto_one(client, l->mtags, "%s", l->line);
- } else {
- MessageTag *m = safe_alloc(sizeof(MessageTag));
- m->name = "batch";
- m->value = batchid;
- AddListItem(m, l->mtags);
- sendto_one(client, l->mtags, "%s", l->line);
- DelListItem(m, l->mtags);
- safe_free(m);
+ if (filter->timestamp_a && ((m = find_mtag(l->mtags, "time"))) && (strcmp(m->value, filter->timestamp_a) > 0))
+ {
+ started = 1;
+ } else
+ if (filter->msgid_a && ((m = find_mtag(l->mtags, "msgid"))) && !strcmp(m->value, filter->msgid_a))
+ {
+ started = 1;
+ continue;
+ }
+ }
+ if (started)
+ {
+ /* Check if we need to stop */
+ if (filter->timestamp_b && ((m = find_mtag(l->mtags, "time"))) && (strcmp(m->value, filter->timestamp_b) >= 0))
+ {
+ break;
+ } else
+ if (filter->msgid_b && ((m = find_mtag(l->mtags, "msgid"))) && !strcmp(m->value, filter->msgid_b))
+ {
+ break;
+ }
+
+ /* Add line to the return buffer */
+ n = duplicate_log_line(l);
+ hbm_result_append_line(r, n);
+ if (++written >= filter->limit)
+ break;
}
- } else {
- /* without server-time, log playback is a bit annoying, so skip it? */
}
+
+ return written;
}
-int hbm_history_request(Client *client, char *object, HistoryFilter *filter)
+/** Put lines in HistoryResult that before after a certain msgid or
+ * timestamp (excluding said msgid/timestamp).
+ * @param r The history result set that we will use
+ * @param h The history log object
+ * @param filter The filter that applies
+ * @returns Number of lines written, note that this could be zero,
+ * which is a perfectly valid result.
+ */
+static int hbm_return_before(HistoryResult *r, HistoryLogObject *h, HistoryFilter *filter)
{
- HistoryLogObject *h = hbm_find_object(object);
- HistoryLogLine *l;
- char batch[BATCHLEN+1];
- long redline; /* Imaginary timestamp. Before the red line, history is too old. */
- int lines_sendable = 0, lines_to_skip = 0, cnt = 0;
+ HistoryLogLine *l, *n;
+ int written = 0;
+ int started = 0;
+ MessageTag *m;
- if (!h || !can_receive_history(client))
- return 0;
+ for (l = h->tail; l; l = l->prev)
+ {
+ /* Not started yet? Check if this is the starting point... */
+ if (!started)
+ {
+ if (filter->timestamp_a && ((m = find_mtag(l->mtags, "time"))) && (strcmp(m->value, filter->timestamp_a) < 0))
+ {
+ started = 1;
+ } else
+ if (filter->msgid_a && ((m = find_mtag(l->mtags, "msgid"))) && !strcmp(m->value, filter->msgid_a))
+ {
+ started = 1;
+ continue;
+ }
+ }
+ if (started)
+ {
+ /* Check if we need to stop */
+ if (filter->timestamp_b && ((m = find_mtag(l->mtags, "time"))) && (strcmp(m->value, filter->timestamp_b) < 0))
+ {
+ break;
+ } else
+ if (filter->msgid_b && ((m = find_mtag(l->mtags, "msgid"))) && !strcmp(m->value, filter->msgid_b))
+ {
+ break;
+ }
- batch[0] = '\0';
+ /* Add line to the return buffer */
+ n = duplicate_log_line(l);
+ hbm_result_prepend_line(r, n);
+ if (++written >= filter->limit)
+ break;
+ }
+ }
+
+ return written;
+}
+
+/** Put lines in HistoryResult that are 'latest'
+ * @param r The history result set that we will use
+ * @param h The history log object
+ * @param filter The filter that applies
+ * @returns Number of lines written, note that this could be zero,
+ * which is a perfectly valid result.
+ */
+static int hbm_return_latest(HistoryResult *r, HistoryLogObject *h, HistoryFilter *filter)
+{
+ HistoryLogLine *l, *n;
+ int written = 0;
+ MessageTag *m;
- if (HasCapability(client, "batch"))
+ for (l = h->tail; l; l = l->prev)
{
- /* Start a new batch */
- generate_batch_id(batch);
- sendto_one(client, NULL, ":%s BATCH +%s chathistory %s", me.name, batch, object);
+ if (filter->timestamp_a && ((m = find_mtag(l->mtags, "time"))) && (strcmp(m->value, filter->timestamp_a) <= 0))
+ break; /* Stop now */
+ else
+ if (filter->msgid_a && ((m = find_mtag(l->mtags, "msgid"))) && !strcmp(m->value, filter->msgid_a))
+ break; /* Stop now */
+
+ n = duplicate_log_line(l);
+ hbm_result_prepend_line(r, n);
+ if (++written >= filter->limit)
+ break;
}
+ return written;
+}
+
+/** Put lines in HistoryResult based on a 'simple' request, that is: maximum lines or time
+ * @param r The history result set that we will use
+ * @param h The history log object
+ * @param filter The filter that applies
+ * @returns Number of lines written, note that this could be zero,
+ * which is a perfectly valid result.
+ */
+static int hbm_return_simple(HistoryResult *r, HistoryLogObject *h, HistoryFilter *filter)
+{
+ HistoryLogLine *l;
+ int lines_sendable = 0, lines_to_skip = 0, cnt = 0;
+ long redline;
+ int written = 0;
+
/* Decide on red line, under this the history is too old.
* Filter can be more strict than history object (but not the other way around):
*/
@@ -336,13 +818,190 @@ int hbm_history_request(Client *client, char *object, HistoryFilter *filter)
* taken into account in hbm_history_add.
*/
if (l->t >= redline && (++cnt > lines_to_skip))
- hbm_send_line(client, l, batch);
+ {
+ /* Add to result */
+ HistoryLogLine *n = duplicate_log_line(l);
+ hbm_result_append_line(r, n);
+ written++;
+ }
}
- /* End of batch */
- if (*batch)
- sendto_one(client, NULL, ":%s BATCH -%s", me.name, batch);
- return 1;
+ return written;
+}
+
+/** Put lines in HistoryResult that are 'around' a certain point.
+ * @param r The history result set that we will use
+ * @param h The history log object
+ * @param filter The filter that applies
+ * @returns Number of lines written, note that this could be zero,
+ * which is a perfectly valid result.
+ */
+static int hbm_return_around(HistoryResult *r, HistoryLogObject *h, HistoryFilter *filter)
+{
+ int n = 0;
+ int orig_limit = filter->limit;
+
+ /* First request 50% above the search term */
+ if (filter->limit > 1)
+ filter->limit = filter->limit / 2;
+ n = hbm_return_before(r, h, filter);
+ /* Then the remainder (50% or more) below the search term.
+ *
+ * Ok, well, unless the original limit was 1 and we already
+ * sent 1 line, then we may not send anything anymore..
+ */
+ filter->limit = orig_limit - n;
+ if (filter->limit > 0)
+ n += hbm_return_after(r, h, filter);
+
+ return n;
+}
+
+/** Figure out the direction (forwards or backwards) for CHATHISTORY BETWEEN request
+ * @param h The history log object
+ * @param filter The filter that applies
+ * @returns 0 for backward searching, 1 for forward searching, -1 for invalid / not found
+ */
+static int hbm_return_between_figure_out_direction(HistoryLogObject *h, HistoryFilter *filter)
+{
+ HistoryLogLine *l;
+ int found_a = 0;
+ int found_b = 0;
+ MessageTag *m;
+
+ /* Two timestamps? Then we can easily tell the direction. */
+ if (filter->timestamp_a && filter->timestamp_b)
+ return (strcmp(filter->timestamp_a, filter->timestamp_b) <= 0) ? 1 : 0;
+
+ for (l = h->head; l; l = l->next)
+ {
+ if (!found_a)
+ {
+ if (filter->timestamp_a && ((m = find_mtag(l->mtags, "time"))) && (strcmp(m->value, filter->timestamp_a) >= 0))
+ {
+ found_a = 1;
+ } else
+ if (filter->msgid_a && ((m = find_mtag(l->mtags, "msgid"))) && !strcmp(m->value, filter->msgid_a))
+ {
+ found_a = 1;
+ }
+ if (found_a)
+ {
+ if (found_b)
+ {
+ /* B was found before A? Then the result is: backwards */
+ return 0;
+ }
+ if (filter->timestamp_b && (m = find_mtag(l->mtags, "time")) && m->value)
+ {
+ /* We can already resolve the direction now: */
+ char *timestamp_a = m->value;
+ return (strcmp(timestamp_a, filter->timestamp_b) <= 0) ? 1 : 0;
+ }
+ }
+ }
+ if (!found_b)
+ {
+ if (filter->timestamp_b && ((m = find_mtag(l->mtags, "time"))) && (strcmp(m->value, filter->timestamp_b) >= 0))
+ {
+ found_b = 1;
+ } else
+ if (filter->msgid_b && ((m = find_mtag(l->mtags, "msgid"))) && !strcmp(m->value, filter->msgid_b))
+ {
+ found_b = 1;
+ }
+ if (found_b)
+ {
+ if (found_a)
+ {
+ /* A was found before B? Then the result is: forwards */
+ return 1;
+ }
+ if (filter->timestamp_a && (m = find_mtag(l->mtags, "time")) && m->value)
+ {
+ /* We can already resolve the direction now: */
+ char *timestamp_b = m->value;
+ return (strcmp(filter->timestamp_a, timestamp_b) <= 0) ? 1 : 0;
+ }
+ }
+ }
+ }
+
+ /* Neither points were found OR
+ * one of the point is a msgid that could not be found.
+ */
+ return -1; /* Result: invalid */
+}
+
+/** Put lines in HistoryResult that are 'between' two points.
+ * @param r The history result set that we will use
+ * @param h The history log object
+ * @param filter The filter that applies
+ * @returns Number of lines written, note that this could be zero,
+ * which is a perfectly valid result.
+ */
+static int hbm_return_between(HistoryResult *r, HistoryLogObject *h, HistoryFilter *filter)
+{
+ int direction;
+
+ direction = hbm_return_between_figure_out_direction(h, filter);
+
+ if (direction == 1)
+ return hbm_return_after(r, h, filter);
+ else if (direction == 0)
+ return hbm_return_before(r, h, filter);
+ /* else direction is -1 which means not found / invalid */
+
+ return 0;
+}
+
+HistoryResult *hbm_history_request(char *object, HistoryFilter *filter)
+{
+ HistoryResult *r;
+ HistoryLogObject *h = hbm_find_object(object);
+ HistoryLogLine *l;
+ int lines_sendable = 0, lines_to_skip = 0, cnt = 0;
+ long redline;
+
+ if (!h)
+ return NULL; /* nothing found */
+
+ /* Check if we need to remove some history entries due to 'time'.
+ * No need to worry about 'count' as that is being taken care off
+ * by hbm_history_add().
+ */
+ if (h->oldest_t < TStime() - h->max_time)
+ hbm_history_cleanup(h);
+
+ r = safe_alloc(sizeof(HistoryResult));
+ safe_strdup(r->object, object);
+
+ switch(filter->cmd)
+ {
+ case HFC_BEFORE:
+ hbm_return_before(r, h, filter);
+ break;
+ case HFC_AFTER:
+ hbm_return_after(r, h, filter);
+ break;
+ case HFC_LATEST:
+ hbm_return_latest(r, h, filter);
+ break;
+ case HFC_AROUND:
+ hbm_return_around(r, h, filter);
+ break;
+ case HFC_BETWEEN:
+ hbm_return_between(r, h, filter);
+ break;
+ case HFC_SIMPLE:
+ hbm_return_simple(r, h, filter);
+ break;
+ default:
+ // unhandled
+ break;
+ }
+
+ return r;
}
/** Clean up expired entries */
@@ -425,6 +1084,367 @@ int hbm_history_set_limit(char *object, int max_lines, long max_time)
return 1;
}
+/** Read the master.db file, this is done at the INIT stage so we can still
+ * reject the configuration / boot attempt.
+ *
+ * IMPORTANT: Because we run at INIT you must use test.xyz values and not cfg.xyz!
+ */
+static int hbm_read_masterdb(void)
+{
+ UnrealDB *db;
+ uint32_t mdb_version;
+ char *prehash = NULL;
+ char *posthash = NULL;
+
+ db = unrealdb_open(test.masterdb, UNREALDB_MODE_READ, test.db_secret);
+
+ if (!db)
+ {
+ if (unrealdb_get_error_code() == UNREALDB_ERROR_FILENOTFOUND)
+ {
+ /* Database does not exist. Could be first boot */
+ config_warn("[history] No database present at '%s', will start a new one", test.masterdb);
+ // TODO: maybe check for condition where 'master.db' does not exist but
+ // there are other .db files.
+ if (!hbm_write_masterdb())
+ return 0; /* fatal error */
+ return 1;
+ } else
+ {
+ config_warn("[history] Unable to open the database file '%s' for reading: %s", test.masterdb, unrealdb_get_error_string());
+ return 0;
+ }
+ }
+
+ /* Master db has an easy format:
+ * 64 bits: version number
+ * string: pre hash
+ * string: post hash
+ */
+ if (!unrealdb_read_int32(db, &mdb_version) ||
+ !unrealdb_read_str(db, &prehash) ||
+ !unrealdb_read_str(db, &posthash))
+ {
+ safe_free(prehash);
+ safe_free(posthash);
+ config_error("[history] Read error from database file '%s': %s",
+ test.masterdb, unrealdb_get_error_string());
+ unrealdb_close(db);
+ return 0;
+ }
+ unrealdb_close(db);
+
+ if (!prehash || !posthash)
+ {
+ config_error("[history] Read error from database file '%s': unexpected values encountered",
+ test.masterdb);
+ return 0;
+ }
+
+ /* Now, safely switch over.. */
+ safe_free(hbm_prehash);
+ safe_free(hbm_posthash);
+ hbm_prehash = prehash;
+ hbm_posthash = posthash;
+
+ return 1;
+}
+
+/** Write the master.db file. Only call this if it does not exist yet! */
+static int hbm_write_masterdb(void)
+{
+ UnrealDB *db;
+ uint32_t mdb_version;
+
+ if (!test.db_secret)
+ abort();
+
+ db = unrealdb_open(test.masterdb, UNREALDB_MODE_WRITE, test.db_secret);
+ if (!db)
+ {
+ config_error("[history] Unable to write to '%s': %s",
+ test.masterdb, unrealdb_get_error_string());
+ return 0;
+ }
+
+ if (!hbm_prehash || !hbm_posthash)
+ abort(); /* impossible */
+
+ mdb_version = 5000;
+ if (!unrealdb_write_int32(db, mdb_version) ||
+ !unrealdb_write_str(db, hbm_prehash) ||
+ !unrealdb_write_str(db, hbm_posthash))
+ {
+ config_error("[history] Unable to write to '%s': %s",
+ test.masterdb, unrealdb_get_error_string());
+ return 0;
+ }
+ unrealdb_close(db);
+ return 1;
+}
+
+/** Read all database files (except master.db, which is already loaded) */
+static void hbm_read_dbs(void)
+{
+ char buf[512];
+#ifndef _WIN32
+ struct dirent *dir;
+ DIR *fd = opendir(cfg.directory);
+
+ if (!fd)
+ return;
+
+ while ((dir = readdir(fd)))
+ {
+ char *fname = dir->d_name;
+#else
+ /* Windows */
+ WIN32_FIND_DATA hData;
+ HANDLE hFile;
+ char xbuf[512];
+
+ snprintf(xbuf, sizeof(xbuf), "%s/*.db", cfg.directory);
+
+ hFile = FindFirstFile(xbuf, &hData);
+ if (hFile == INVALID_HANDLE_VALUE)
+ return;
+
+ do
+ {
+ char *fname = hData.cFileName;
+#endif
+
+ /* Common section for both *NIX and Windows */
+
+ snprintf(buf, sizeof(buf), "%s/%s", cfg.directory, fname);
+ if (filename_has_suffix(fname, ".db") && strcmp(fname, "master.db"))
+ {
+ if (!hbm_read_db(buf))
+ {
+ /* On error, we move the file to the 'bad' subdirectory,
+ * eg data/history/bad/xyz.db
+ */
+ char buf2[512];
+ snprintf(buf2, sizeof(buf2), "%s/bad", cfg.directory);
+#ifdef _WIN32
+ (void)mkdir(buf2); /* (errors ignored) */
+#else
+ (void)mkdir(buf2, S_IRUSR|S_IWUSR|S_IXUSR); /* (errors ignored) */
+#endif
+ snprintf(buf2, sizeof(buf2), "%s/bad/%s", cfg.directory, fname);
+ unlink(buf2);
+ (void)rename(buf, buf2);
+ }
+ }
+
+ /* End of common section */
+#ifndef _WIN32
+ }
+ closedir(fd);
+#else
+ } while (FindNextFile(hFile, &hData));
+ FindClose(hFile);
+#endif
+}
+
+#define RESET_VALUES_LOOP() do { \
+ safe_free(mtag_name); \
+ safe_free(mtag_value); \
+ safe_free(line); \
+ free_message_tags(mtags); \
+ mtags = NULL; \
+ magic = 0; \
+ line_ts = 0; \
+ } while(0)
+
+#define R_SAFE_CLEANUP() do { \
+ unrealdb_close(db); \
+ RESET_VALUES_LOOP(); \
+ safe_free(prehash); \
+ safe_free(posthash); \
+ safe_free(object); \
+ } while(0)
+#define R_SAFE(x) \
+ do { \
+ if (!(x)) { \
+ config_warn("[history] Read error from database file '%s' (possible corruption): %s", fname, unrealdb_get_error_string()); \
+ R_SAFE_CLEANUP(); \
+ return 0; \
+ } \
+ } while(0)
+
+
+/** Read a channel history db file */
+static int hbm_read_db(char *fname)
+{
+ UnrealDB *db = NULL;
+ // header
+ uint32_t magic = 0;
+ uint32_t version = 0;
+ char *prehash = NULL;
+ char *posthash = NULL;
+ char *object = NULL;
+ uint64_t max_lines = 0;
+ uint64_t max_time = 0;
+ // then, for each entry:
+ // (magic)
+ uint64_t line_ts;
+ char *mtag_name = NULL;
+ char *mtag_value = NULL;
+ MessageTag *mtags = NULL, *m;
+ char *line = NULL;
+ HistoryLogObject *h;
+
+ db = unrealdb_open(fname, UNREALDB_MODE_READ, cfg.db_secret);
+ if (!db)
+ {
+ config_warn("[history] Unable to open the database file '%s' for reading: %s", fname, unrealdb_get_error_string());
+ return 0;
+ }
+
+ R_SAFE(unrealdb_read_int32(db, &magic));
+ if (magic != HISTORYDB_MAGIC_FILE_START)
+ {
+ config_warn("[history] Database '%s' has wrong magic value, possibly corrupt (0x%lx), expected HISTORYDB_MAGIC_FILE_START.",
+ fname, (long)magic);
+ unrealdb_close(db);
+ return 0;
+ }
+
+ /* Now do a version check */
+ R_SAFE(unrealdb_read_int32(db, &version));
+ if (version < 4999)
+ {
+ config_warn("[history] Database '%s' uses an unsupported - possibly old - format (%ld).", fname, (long)version);
+ unrealdb_close(db);
+ return 0;
+ }
+ if (version > 5000)
+ {
+ config_warn("[history] Database '%s' has version %lu while we only support %lu. Did you just downgrade UnrealIRCd? Sorry this is not suported",
+ fname, (unsigned long)version, (unsigned long)5000);
+ unrealdb_close(db);
+ return 0;
+ }
+
+ R_SAFE(unrealdb_read_str(db, &prehash));
+ R_SAFE(unrealdb_read_str(db, &posthash));
+
+ if (!prehash || !posthash || strcmp(prehash, hbm_prehash) || strcmp(posthash, hbm_posthash))
+ {
+ config_warn("[history] Database '%s' does not belong to our 'master.db'. Are you mixing old with new .db files perhaps? This is not supported. File ignored.",
+ fname);
+ R_SAFE_CLEANUP();
+ return 0;
+ }
+
+ R_SAFE(unrealdb_read_str(db, &object));
+ R_SAFE(unrealdb_read_int64(db, &max_lines));
+ R_SAFE(unrealdb_read_int64(db, &max_time));
+ h = hbm_find_object(object);
+ if (!h)
+ {
+ config_warn("Channel %s does not have +H set, deleting history", object);
+ R_SAFE_CLEANUP();
+ unlink(fname);
+ return 1; /* No problem */
+ }
+
+ while(1)
+ {
+ RESET_VALUES_LOOP();
+ R_SAFE(unrealdb_read_int32(db, &magic));
+ if (magic == HISTORYDB_MAGIC_FILE_END)
+ break; /* We're done, end gracefully */
+ if (magic != HISTORYDB_MAGIC_ENTRY_START)
+ {
+ config_warn("[history] Read error from database file '%s': wrong magic value in entry (0x%lx), expected HISTORYDB_MAGIC_ENTRY_START",
+ fname, (long)magic);
+ R_SAFE_CLEANUP();
+ return 0;
+ }
+
+ R_SAFE(unrealdb_read_int64(db, &line_ts));
+ while(1)
+ {
+ R_SAFE(unrealdb_read_str(db, &mtag_name));
+ R_SAFE(unrealdb_read_str(db, &mtag_value));
+ if (!mtag_name && !mtag_value)
+ break; /* We're done reading mtags for this particular line */
+ m = safe_alloc(sizeof(MessageTag));
+ safe_strdup(m->name, mtag_name);
+ safe_strdup(m->value, mtag_value);
+ AppendListItem(m, mtags);
+ safe_free(mtag_name);
+ safe_free(mtag_value);
+ }
+ R_SAFE(unrealdb_read_str(db, &line));
+ R_SAFE(unrealdb_read_int32(db, &magic));
+ if (magic != HISTORYDB_MAGIC_ENTRY_END)
+ {
+ config_warn("[history] Read error from database file '%s': wrong magic value in entry (0x%lx), expected HISTORYDB_MAGIC_ENTRY_END",
+ fname, (long)magic);
+ R_SAFE_CLEANUP();
+ return 0;
+ }
+ hbm_history_add(object, mtags, line);
+ }
+
+ /* Prevent directly rewriting the channel, now that we have just read it.
+ * This could cause things not to fire in case of corner issues like
+ * hot-loading but that should be acceptable. The alternative is that
+ * all log files are written again with identical contents for no reason,
+ * which is a waste of resources.
+ */
+ h->dirty = 0;
+
+ R_SAFE_CLEANUP();
+ return 1;
+}
+
+/** Flush all dirty logs to disk on UnrealIRCd stop */
+static void hbm_flush(void)
+{
+ int hashnum;
+ HistoryLogObject *h;
+
+ if (!cfg.persist)
+ return; /* nothing to flush anyway */
+
+ for (hashnum = 0; hashnum < HISTORY_BACKEND_MEM_HASH_TABLE_SIZE; hashnum++)
+ {
+ for (h = history_hash_table[hashnum]; h; h = h->next)
+ {
+ hbm_history_cleanup(h);
+ if (cfg.persist && h->dirty)
+ hbm_write_db(h);
+ }
+ }
+}
+
+/** Free all history.
+ * This is only called when the module is unloaded for good, so
+ * when UnrealIRCd is terminating or someone comments the module out
+ * and/or switches history backends.
+ */
+void hbm_free_all_history(ModData *m)
+{
+ int hashnum;
+ HistoryLogObject *h, *h_next;
+
+ for (hashnum = 0; hashnum < HISTORY_BACKEND_MEM_HASH_TABLE_SIZE; hashnum++)
+ {
+ for (h = history_hash_table[hashnum]; h; h = h_next)
+ {
+ h_next = h->next;
+ hbm_history_destroy(h->name);
+ }
+ }
+
+ /* And free the hash table pointer */
+ safe_free(m->ptr);
+}
+
/** Periodically clean the history.
* Instead of doing all channels in 1 go, we do a limited number
* of channels each call, hence the 'static int' and the do { } while
@@ -441,8 +1461,12 @@ EVENT(history_mem_clean)
do
{
- for (h = history_hash_table[hashnum++]; h; h = h->next)
+ for (h = history_hash_table[hashnum]; h; h = h->next)
+ {
hbm_history_cleanup(h);
+ if (cfg.persist && h->dirty)
+ hbm_write_db(h);
+ }
hashnum++;
@@ -450,3 +1474,133 @@ EVENT(history_mem_clean)
hashnum = 0;
} while(loopcnt++ < HISTORY_CLEAN_PER_LOOP);
}
+
+char *hbm_history_filename(HistoryLogObject *h)
+{
+ static char fname[512];
+ char oname[OBJECTLEN+1];
+ char hashdata[512];
+ char hash[128];
+
+ if (!hbm_prehash || !hbm_posthash)
+ abort(); /* impossible */
+
+ strtolower_safe(oname, h->name, sizeof(oname));
+ snprintf(hashdata, sizeof(hashdata), "%s %s %s", hbm_prehash, oname, hbm_posthash);
+ sha256hash(hash, hashdata, strlen(hashdata));
+
+ snprintf(fname, sizeof(fname), "%s/%s.db", cfg.directory, hash);
+ return fname;
+}
+
+#define WARN_WRITE_ERROR(fname) \
+ do { \
+ sendto_realops_and_log("[history] Error writing to temporary database file " \
+ "'%s': %s (DATABASE NOT SAVED)", \
+ fname, unrealdb_get_error_string()); \
+ } while(0)
+
+#define W_SAFE(x) \
+ do { \
+ if (!(x)) { \
+ WARN_WRITE_ERROR(tmpfname); \
+ unrealdb_close(db); \
+ return 0; \
+ } \
+ } while(0)
+
+
+// FIXME: the code below will cause massive floods on disk or I/O errors if hundreds of
+// channel logs fail to write... fun.
+static int hbm_write_db(HistoryLogObject *h)
+{
+ UnrealDB *db;
+ char *realfname;
+ char tmpfname[512];
+ HistoryLogLine *l;
+ MessageTag *m;
+ Channel *channel;
+
+ if (!cfg.db_secret)
+ abort();
+
+ channel = find_channel(h->name, NULL);
+ if (!channel || !has_channel_mode(channel, 'P'))
+ return 1; /* Don't save this channel, pretend success */
+
+ realfname = hbm_history_filename(h);
+ snprintf(tmpfname, sizeof(tmpfname), "%s.tmp", realfname);
+
+ db = unrealdb_open(tmpfname, UNREALDB_MODE_WRITE, cfg.db_secret);
+ if (!db)
+ {
+ WARN_WRITE_ERROR(tmpfname);
+ return 0;
+ }
+
+ W_SAFE(unrealdb_write_int32(db, HISTORYDB_MAGIC_FILE_START));
+ W_SAFE(unrealdb_write_int32(db, 5000)); /* VERSION */
+ W_SAFE(unrealdb_write_str(db, hbm_prehash));
+ W_SAFE(unrealdb_write_str(db, hbm_posthash));
+ W_SAFE(unrealdb_write_str(db, h->name));
+
+ W_SAFE(unrealdb_write_int64(db, h->max_lines));
+ W_SAFE(unrealdb_write_int64(db, h->max_time));
+
+ for (l = h->head; l; l = l->next)
+ {
+ W_SAFE(unrealdb_write_int32(db, HISTORYDB_MAGIC_ENTRY_START));
+ W_SAFE(unrealdb_write_int64(db, l->t));
+ for (m = l->mtags; m; m = m->next)
+ {
+ W_SAFE(unrealdb_write_str(db, m->name));
+ W_SAFE(unrealdb_write_str(db, m->value)); /* can be NULL */
+ }
+ W_SAFE(unrealdb_write_str(db, NULL));
+ W_SAFE(unrealdb_write_str(db, NULL));
+ W_SAFE(unrealdb_write_str(db, l->line));
+ W_SAFE(unrealdb_write_int32(db, HISTORYDB_MAGIC_ENTRY_END));
+ }
+ W_SAFE(unrealdb_write_int32(db, HISTORYDB_MAGIC_FILE_END));
+
+ if (!unrealdb_close(db))
+ {
+ WARN_WRITE_ERROR(tmpfname);
+ return 0;
+ }
+
+#ifdef _WIN32
+ /* The rename operation cannot be atomic on Windows as it will cause a "file exists" error */
+ unlink(realfname);
+#endif
+ if (rename(tmpfname, realfname) < 0)
+ {
+ sendto_realops_and_log("[history] Error renaming '%s' to '%s': %s (HISTORY NOT SAVED)",
+ tmpfname, realfname, strerror(errno));
+ return 0;
+ }
+
+ /* Now that everything was successful, clear the dirty flag */
+ h->dirty = 0;
+ return 1;
+}
+
+static void hbm_delete_db(HistoryLogObject *h)
+{
+ UnrealDB *db;
+ char *fname;
+ if (!cfg.persist || !hbm_prehash || !hbm_posthash)
+ {
+#ifdef DEBUGMODE
+ abort(); /* we should not be called, so debug this */
+#endif
+ return;
+ }
+ fname = hbm_history_filename(h);
+ unlink(fname);
+}
+
+void hbm_generic_free(ModData *m)
+{
+ safe_free(m->ptr);
+}
diff --git a/src/modules/history_backend_null.c b/src/modules/history_backend_null.c
@@ -22,7 +22,7 @@ ModuleHeader MOD_HEADER
/* Forward declarations */
int hbn_history_set_limit(char *object, int max_lines, long max_time);
int hbn_history_add(char *object, MessageTag *mtags, char *line);
-int hbn_history_request(Client *client, char *object, HistoryFilter *filter);
+HistoryResult *hbn_history_request(char *object, HistoryFilter *filter);
int hbn_history_destroy(char *object);
MOD_INIT()
@@ -30,7 +30,6 @@ MOD_INIT()
HistoryBackendInfo hbi;
MARK_AS_OFFICIAL_MODULE(modinfo);
- ModuleSetOptions(modinfo->handle, MOD_OPT_PERM, 1);
memset(&hbi, 0, sizeof(hbi));
hbi.name = "mem";
@@ -59,9 +58,9 @@ int hbn_history_add(char *object, MessageTag *mtags, char *line)
return 1;
}
-int hbn_history_request(Client *client, char *object, HistoryFilter *filter)
+HistoryResult *hbn_history_request(char *object, HistoryFilter *filter)
{
- return 0;
+ return NULL;
}
int hbn_history_set_limit(char *object, int max_lines, long max_time)
diff --git a/src/modules/ident_lookup.c b/src/modules/ident_lookup.c
@@ -190,16 +190,6 @@ static void ident_lookup_receive(int fd, int revents, void *userdata)
return;
}
-void skip_whitespace(char **p)
-{
- for (; **p == ' ' || **p == '\t'; *p = *p + 1);
-}
-
-void read_until(char **p, char *stopchars)
-{
- for (; **p && !strchr(stopchars, **p); *p = *p + 1);
-}
-
static char *ident_lookup_parse(Client *client, char *buf)
{
/* <port> , <port> : USERID : <OSTYPE>: <username>
diff --git a/src/modules/invite.c b/src/modules/invite.c
@@ -166,25 +166,16 @@ CMD_FUNC(cmd_invite)
return;
}
- if (MyConnect(client))
+ if (MyUser(client))
{
if (target_limit_exceeded(client, target, target->name))
return;
- if (!ValidatePermissionsForPath("immune:invite-flood",client,NULL,NULL,NULL))
+ if (!ValidatePermissionsForPath("immune:invite-flood",client,NULL,NULL,NULL) &&
+ flood_limit_exceeded(client, FLD_INVITE))
{
- if ((client->user->flood.invite_t + INVITE_PERIOD) <= timeofday)
- {
- client->user->flood.invite_c = 0;
- client->user->flood.invite_t = timeofday;
- }
- if (client->user->flood.invite_c <= INVITE_COUNT)
- client->user->flood.invite_c++;
- if (client->user->flood.invite_c > INVITE_COUNT)
- {
- sendnumeric(client, RPL_TRYAGAIN, "INVITE");
- return;
- }
+ sendnumeric(client, RPL_TRYAGAIN, "INVITE");
+ return;
}
if (!override)
diff --git a/src/modules/jointhrottle.c b/src/modules/jointhrottle.c
@@ -23,10 +23,6 @@
#include "unrealircd.h"
-/* Default settings for set::anti-flood::join-flood block: */
-#define JOINTHROTTLE_DEFAULT_COUNT 3
-#define JOINTHROTTLE_DEFAULT_TIME 90
-
ModuleHeader MOD_HEADER
= {
"jointhrottle",
@@ -40,11 +36,6 @@ ModuleInfo *ModInfo = NULL;
ModDataInfo *jointhrottle_md; /* Module Data structure which we acquire */
-struct {
- unsigned short num;
- unsigned short t;
-} cfg;
-
typedef struct JoinFlood JoinFlood;
struct JoinFlood {
@@ -55,8 +46,6 @@ struct JoinFlood {
};
/* Forward declarations */
-int jointhrottle_config_test(ConfigFile *, ConfigEntry *, int, int *);
-int jointhrottle_config_run(ConfigFile *, ConfigEntry *, int);
void jointhrottle_md_free(ModData *m);
int jointhrottle_can_join(Client *client, Channel *channel, char *key, char *parv[]);
int jointhrottle_local_join(Client *client, Channel *channel, MessageTag *mtags, char *parv[]);
@@ -67,7 +56,6 @@ JoinFlood *jointhrottle_addentry(Client *client, Channel *channel);
MOD_TEST()
{
- HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, jointhrottle_config_test);
return MOD_SUCCESS;
}
@@ -89,12 +77,9 @@ MOD_INIT()
if (!jointhrottle_md)
abort();
- HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, jointhrottle_config_run);
HookAdd(modinfo->handle, HOOKTYPE_CAN_JOIN, 0, jointhrottle_can_join);
HookAdd(modinfo->handle, HOOKTYPE_LOCAL_JOIN, 0, jointhrottle_local_join);
- cfg.t = JOINTHROTTLE_DEFAULT_TIME;
- cfg.num = JOINTHROTTLE_DEFAULT_COUNT;
return MOD_SUCCESS;
}
@@ -109,53 +94,10 @@ MOD_UNLOAD()
return MOD_FAILED;
}
-int jointhrottle_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
-{
- int errors = 0;
- int cnt=0, period=0;
-
- if (type != CONFIG_SET_ANTI_FLOOD)
- return 0;
-
- if (strcmp(ce->ce_varname, "join-flood"))
- return 0; /* otherwise not interested */
-
- if (!ce->ce_vardata || !config_parse_flood(ce->ce_vardata, &cnt, &period) ||
- (cnt < 1) || (cnt > 255) || (period < 5))
- {
- config_error("%s:%i: set::anti-flood::join-flood. Syntax is '<count>:<period>' (eg 3:90), "
- "count should be 1-255, period should be greater than 4",
- ce->ce_fileptr->cf_filename, ce->ce_varlinenum);
- errors++;
- }
-
- *errs = errors;
- return errors ? -1 : 1;
-}
-
-int jointhrottle_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
-{
- int cnt=0, period=0;
-
- if (type != CONFIG_SET_ANTI_FLOOD)
- return 0;
-
- if (strcmp(ce->ce_varname, "join-flood"))
- return 0; /* otherwise not interested */
-
- config_parse_flood(ce->ce_vardata, &cnt, &period);
-
- cfg.t = period;
- cfg.num = cnt;
-
- return 0;
-}
-
static int isjthrottled(Client *client, Channel *channel)
{
JoinFlood *e;
- int num = cfg.num;
- int t = cfg.t;
+ FloodSettings *settings = get_floodsettings_for_user(client, FLD_JOIN);
if (!MyUser(client))
return 0;
@@ -171,7 +113,8 @@ static int isjthrottled(Client *client, Channel *channel)
/* Ok... now the actual check:
* if ([timer valid] && [one more join would exceed num])
*/
- if (((TStime() - e->firstjoin) < t) && (e->numjoins == num))
+ if (((TStime() - e->firstjoin) < settings->period[FLD_JOIN]) &&
+ (e->numjoins >= settings->limit[FLD_JOIN]))
return 1; /* Throttled */
return 0;
@@ -196,7 +139,7 @@ static void jointhrottle_increase_usercounter(Client *client, Channel *channel)
e->firstjoin = TStime();
e->numjoins = 1;
} else
- if ((TStime() - e->firstjoin) < cfg.t) /* still valid? */
+ if ((TStime() - e->firstjoin) < iConf.floodsettings->period[FLD_JOIN]) /* still valid? */
{
e->numjoins++;
} else {
@@ -266,11 +209,12 @@ EVENT(jointhrottle_cleanup_structs)
{
jf_next = jf->next;
- if (jf->firstjoin + cfg.t > TStime())
+ if (jf->firstjoin + iConf.floodsettings->period[FLD_JOIN] > TStime())
continue; /* still valid entry */
#ifdef DEBUGMODE
- ircd_log(LOG_ERROR, "jointhrottle_cleanup_structs(): freeing %s/%s (%ld[%ld], %d)",
- client->name, jf->chname, jf->firstjoin, (long)(TStime() - jf->firstjoin), cfg.t);
+ ircd_log(LOG_ERROR, "jointhrottle_cleanup_structs(): freeing %s/%s (%ld[%ld], %ld)",
+ client->name, jf->chname, jf->firstjoin, (long)(TStime() - jf->firstjoin),
+ iConf.floodsettings->period[FLD_JOIN]);
#endif
if (moddata_local_client(client, jointhrottle_md).ptr == jf)
{
diff --git a/src/modules/knock.c b/src/modules/knock.c
@@ -132,21 +132,12 @@ CMD_FUNC(cmd_knock)
if (i == HOOK_DENY)
return;
- if (MyUser(client) && !ValidatePermissionsForPath("immune:knock-flood",client,NULL,NULL,NULL))
+ if (MyUser(client) &&
+ !ValidatePermissionsForPath("immune:knock-flood",client,NULL,NULL,NULL) &&
+ flood_limit_exceeded(client, FLD_KNOCK))
{
- if ((client->user->flood.knock_t + KNOCK_PERIOD) <= timeofday)
- {
- client->user->flood.knock_c = 0;
- client->user->flood.knock_t = timeofday;
- }
- if (client->user->flood.knock_c <= KNOCK_COUNT)
- client->user->flood.knock_c++;
- if (client->user->flood.knock_c > KNOCK_COUNT)
- {
- sendnumeric(client, ERR_CANNOTKNOCK, parv[1],
- "You are KNOCK flooding");
- return;
- }
+ sendnumeric(client, ERR_CANNOTKNOCK, parv[1], "You are KNOCK flooding");
+ return;
}
new_message(&me, NULL, &mtags);
diff --git a/src/modules/message-ids.c b/src/modules/message-ids.c
@@ -31,14 +31,6 @@ ModuleHeader MOD_HEADER
"unrealircd-5",
};
-/** The length of a standard 'msgid' tag (note that special
- * msgid tags will be longer).
- * The 22 alphanumeric characters provide slightly more
- * than 128 bits of randomness (62^22 > 2^128).
- * See mtag_add_or_inherit_msgid() for more information.
- */
-#define MSGIDLEN 22
-
/* Variables */
long CAP_ACCOUNT_TAG = 0L;
diff --git a/src/modules/message.c b/src/modules/message.c
@@ -117,8 +117,14 @@ int can_send_to_user(Client *client, Client *target, char **msgtext, char **errm
}
// Possible FIXME: make match_spamfilter also use errmsg, or via a wrapper? or use same numeric?
- if (MyUser(client) && match_spamfilter(client, *msgtext, (sendtype == SEND_TYPE_NOTICE ? SPAMF_USERNOTICE : SPAMF_USERMSG), target->name, 0, NULL))
- return 0;
+ if (MyUser(client))
+ {
+ int spamtype = (sendtype == SEND_TYPE_NOTICE ? SPAMF_USERNOTICE : SPAMF_USERMSG);
+ char *cmd = sendtype_to_cmd(sendtype);
+
+ if (match_spamfilter(client, *msgtext, spamtype, cmd, target->name, 0, NULL))
+ return 0;
+ }
n = HOOK_CONTINUE;
for (h = Hooks[HOOKTYPE_CAN_SEND_TO_USER]; h; h = h->next)
@@ -379,8 +385,13 @@ void cmd_message(Client *client, MessageTag *recv_mtags, int parc, char *parv[],
if ((*parv[2] == '\001') && strncmp(&parv[2][1], "ACTION ", 7))
sendflags |= SKIP_CTCP;
- if (MyUser(client) && match_spamfilter(client, text, (sendtype == SEND_TYPE_NOTICE ? SPAMF_CHANNOTICE : SPAMF_CHANMSG), channel->chname, 0, NULL))
- return;
+ if (MyUser(client))
+ {
+ int spamtype = (sendtype == SEND_TYPE_NOTICE ? SPAMF_CHANNOTICE : SPAMF_CHANMSG);
+
+ if (match_spamfilter(client, text, spamtype, cmd, channel->chname, 0, NULL))
+ return;
+ }
new_message(client, recv_mtags, &mtags);
diff --git a/src/modules/mode.c b/src/modules/mode.c
@@ -1293,7 +1293,7 @@ int do_extmode_char(Channel *channel, Cmode *handler, char *param, u_int what,
if (handler->is_ok(client, channel, mode, param, EXCHK_PARAM, what) == FALSE)
return paracnt; /* rejected by is_ok */
- morphed = handler->conv_param(param, client);
+ morphed = handler->conv_param(param, client, channel);
if (!morphed)
return paracnt; /* rejected by conv_param */
@@ -1303,12 +1303,12 @@ int do_extmode_char(Channel *channel, Cmode *handler, char *param, u_int what,
char *now, *requested;
char flag = handler->flag;
now = cm_getparameter(channel, flag);
- requested = handler->conv_param(param, client);
+ requested = handler->conv_param(param, client, channel);
if (now && requested && !strcmp(now, requested))
return paracnt; /* ignore... */
}
ircsnprintf(pvar[*pcount], MODEBUFLEN + 3, "+%c%s",
- handler->flag, handler->conv_param(param, client));
+ handler->flag, handler->conv_param(param, client, channel));
(*pcount)++;
param = morphed; /* set param to converted parameter. */
}
diff --git a/src/modules/nick.c b/src/modules/nick.c
@@ -214,11 +214,10 @@ CMD_FUNC(cmd_nick_remote)
sendto_snomask(SNO_FNICKCHANGE, "*** %s (%s@%s) has changed their nickname to %s",
client->name, client->user->username, client->user->realhost, nick);
- RunHook2(HOOKTYPE_REMOTE_NICKCHANGE, client, nick);
-
+ new_message(client, recv_mtags, &mtags);
+ RunHook3(HOOKTYPE_REMOTE_NICKCHANGE, client, mtags, nick);
client->lastnick = lastnick ? lastnick : TStime();
add_history(client, 1);
- new_message(client, recv_mtags, &mtags);
sendto_server(client, 0, 0, mtags, ":%s NICK %s %lld",
client->id, nick, (long long)client->lastnick);
sendto_local_common_channels(client, client, 0, mtags, ":%s NICK :%s", client->name, nick);
@@ -275,19 +274,6 @@ CMD_FUNC(cmd_nick_local)
return;
}
- /* set::anti-flood::nick-flood */
- if (client->user && !ValidatePermissionsForPath("immune:nick-flood",client,NULL,NULL,NULL))
- {
- if ((client->user->flood.nick_c >= NICK_COUNT) &&
- (TStime() - client->user->flood.nick_t < NICK_PERIOD))
- {
- /* Throttle... */
- sendnumeric(client, ERR_NCHANGETOOFAST, nick,
- (int)(NICK_PERIOD - (TStime() - client->user->flood.nick_t)));
- return;
- }
- }
-
/* Check for collisions / in use */
if (!strcasecmp("ircd", nick) || !strcasecmp("irc", nick))
{
@@ -299,7 +285,7 @@ CMD_FUNC(cmd_nick_local)
{
/* Local client changing nick: check spamfilter */
spamfilter_build_user_string(spamfilter_user, nick, client);
- if (match_spamfilter(client, spamfilter_user, SPAMF_USER, NULL, 0, NULL))
+ if (match_spamfilter(client, spamfilter_user, SPAMF_USER, "NICK", NULL, 0, NULL))
return;
}
@@ -322,6 +308,16 @@ CMD_FUNC(cmd_nick_local)
/* fallthrough for ircops that have sufficient privileges */
}
+ /* set::anti-flood::nick-flood */
+ if (client->user &&
+ !ValidatePermissionsForPath("immune:nick-flood",client,NULL,NULL,NULL) &&
+ flood_limit_exceeded(client, FLD_NICK))
+ {
+ /* Throttle... */
+ sendnumeric(client, ERR_NCHANGETOOFAST, nick);
+ return;
+ }
+
if (!ValidatePermissionsForPath("immune:nick-flood",client,NULL,NULL,NULL))
cptr->local->since += 3; /* Nick-flood prot. -Donwulff */
@@ -440,20 +436,13 @@ CMD_FUNC(cmd_nick_local)
}
}
- if (TStime() - client->user->flood.nick_t >= NICK_PERIOD)
- {
- client->user->flood.nick_t = TStime();
- client->user->flood.nick_c = 1;
- } else
- client->user->flood.nick_c++;
-
sendto_snomask(SNO_NICKCHANGE, "*** %s (%s@%s) has changed their nickname to %s",
client->name, client->user->username, client->user->realhost, nick);
- RunHook2(HOOKTYPE_LOCAL_NICKCHANGE, client, nick);
+ new_message(client, recv_mtags, &mtags);
+ RunHook3(HOOKTYPE_LOCAL_NICKCHANGE, client, mtags, nick);
client->lastnick = TStime();
add_history(client, 1);
- new_message(client, recv_mtags, &mtags);
sendto_server(client, 0, 0, mtags, ":%s NICK %s %lld",
client->id, nick, (long long)client->lastnick);
sendto_local_common_channels(client, client, 0, mtags, ":%s NICK :%s", client->name, nick);
@@ -681,7 +670,12 @@ nickkill2done:
return;
if (client->user->svid[0] != '0')
+ {
user_account_login(recv_mtags, client);
+ /* no need to check for kill upon user_account_login() here
+ * since that can only happen for local users.
+ */
+ }
RunHook(HOOKTYPE_REMOTE_CONNECT, client);
@@ -721,7 +715,7 @@ int _register_user(Client *client, char *nick, char *username, char *umode, char
userbad[USERLEN * 2 + 1], *ubad = userbad, noident = 0;
int i, xx;
Hook *h;
- ClientUser *user = client->user;
+ User *user = client->user;
char *tkllayer[9] = {
me.name, /*0 server.name */
"+", /*1 +|- */
@@ -879,7 +873,7 @@ int _register_user(Client *client, char *nick, char *username, char *umode, char
find_shun(client);
spamfilter_build_user_string(spamfilter_user, client->name, client);
- if (match_spamfilter(client, spamfilter_user, SPAMF_USER, NULL, 0, &savetkl))
+ if (match_spamfilter(client, spamfilter_user, SPAMF_USER, NULL, NULL, 0, &savetkl))
{
if (savetkl && ((savetkl->ptr.spamfilter->action == BAN_ACT_VIRUSCHAN) ||
(savetkl->ptr.spamfilter->action == BAN_ACT_SOFT_VIRUSCHAN)))
diff --git a/src/modules/part.c b/src/modules/part.c
@@ -89,7 +89,7 @@ CMD_FUNC(cmd_part)
}
if (commentx)
{
- if (match_spamfilter(client, commentx, SPAMF_PART, parv[1], 0, NULL))
+ if (match_spamfilter(client, commentx, SPAMF_PART, "PART", parv[1], 0, NULL))
commentx = NULL;
if (IsDead(client))
return;
diff --git a/src/modules/quit.c b/src/modules/quit.c
@@ -82,7 +82,7 @@ CMD_FUNC(cmd_quit)
return;
}
- if (match_spamfilter(client, comment, SPAMF_QUIT, NULL, 0, NULL))
+ if (match_spamfilter(client, comment, SPAMF_QUIT, "QUIT", NULL, 0, NULL))
{
comment = client->name;
if (IsDead(client))
diff --git a/src/modules/reply-tag.c b/src/modules/reply-tag.c
@@ -0,0 +1,116 @@
+/*
+ * IRC - Internet Relay Chat, src/modules/reply-tag.c
+ * (C) 2021 Syzop & The UnrealIRCd Team
+ *
+ * See file AUTHORS in IRC package for additional names of
+ * the programmers.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 1, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
+/* This implements https://ircv3.net/specs/client-tags/reply */
+
+#include "unrealircd.h"
+
+ModuleHeader MOD_HEADER
+ = {
+ "reply-tag",
+ "5.0",
+ "+reply client tag",
+ "UnrealIRCd Team",
+ "unrealircd-5",
+ };
+
+int replytag_mtag_is_ok(Client *client, char *name, char *value);
+void mtag_add_replytag(Client *client, MessageTag *recv_mtags, MessageTag **mtag_list, char *signature);
+
+MOD_INIT()
+{
+ MessageTagHandlerInfo mtag;
+
+ MARK_AS_OFFICIAL_MODULE(modinfo);
+
+#if 0
+ memset(&mtag, 0, sizeof(mtag));
+ mtag.name = "+reply";
+ mtag.is_ok = replytag_mtag_is_ok;
+ mtag.flags = MTAG_HANDLER_FLAGS_NO_CAP_NEEDED;
+ MessageTagHandlerAdd(modinfo->handle, &mtag);
+#endif
+
+ memset(&mtag, 0, sizeof(mtag));
+ mtag.name = "+draft/reply";
+ mtag.is_ok = replytag_mtag_is_ok;
+ mtag.flags = MTAG_HANDLER_FLAGS_NO_CAP_NEEDED;
+ MessageTagHandlerAdd(modinfo->handle, &mtag);
+
+ HookAddVoid(modinfo->handle, HOOKTYPE_NEW_MESSAGE, 0, mtag_add_replytag);
+
+ return MOD_SUCCESS;
+}
+
+MOD_LOAD()
+{
+ return MOD_SUCCESS;
+}
+
+MOD_UNLOAD()
+{
+ return MOD_SUCCESS;
+}
+
+/** This function verifies if the client sending the mtag is permitted to do so.
+ */
+int replytag_mtag_is_ok(Client *client, char *name, char *value)
+{
+ char *p;
+
+ /* Require a non-empty parameter */
+ if (BadPtr(value))
+ return 0;
+
+ /* All our PRIVMSG/NOTICE msgid's are of this size: */
+ if (strlen(value) != MSGIDLEN)
+ return 0;
+
+ for (p = value; *p; p++)
+ if (!isalnum(*p))
+ return 0; /* non-alphanumeric */
+
+ return 1; /* OK */
+}
+
+void mtag_add_replytag(Client *client, MessageTag *recv_mtags, MessageTag **mtag_list, char *signature)
+{
+ MessageTag *m;
+
+ if (IsUser(client))
+ {
+#if 0
+ m = find_mtag(recv_mtags, "+reply");
+ if (m)
+ {
+ m = duplicate_mtag(m);
+ AddListItem(m, *mtag_list);
+ }
+#endif
+ m = find_mtag(recv_mtags, "+draft/reply");
+ if (m)
+ {
+ m = duplicate_mtag(m);
+ AddListItem(m, *mtag_list);
+ }
+ }
+}
diff --git a/src/modules/reputation.c b/src/modules/reputation.c
@@ -19,9 +19,12 @@
#define REPUTATION_VERSION "1.2"
+/* Change to #define to benchmark. Note that this will add random
+ * reputation entries so should never be used on production servers!!!
+ */
+#undef BENCHMARK
#undef TEST
-#undef BENCHMARK
/* Benchmark results (2GHz Xeon Skylake, compiled with -O2, Linux):
* 10k random IP's with various expire times:
* - load db: 23 ms
@@ -76,12 +79,30 @@ ModuleHeader MOD_HEADER
#define Reputation(client) moddata_client(client, reputation_md).l
+#define WARN_WRITE_ERROR(fname) \
+ do { \
+ sendto_realops_and_log("[reputation] Error writing to temporary database file " \
+ "'%s': %s (DATABASE NOT SAVED)", \
+ fname, unrealdb_get_error_string()); \
+ } while(0)
+
+#define W_SAFE(x) \
+ do { \
+ if (!(x)) { \
+ WARN_WRITE_ERROR(tmpfname); \
+ unrealdb_close(db); \
+ return 0; \
+ } \
+ } while(0)
+
+
/* Definitions (structs, etc.) */
struct cfgstruct {
int expire_score[MAXEXPIRES];
long expire_time[MAXEXPIRES];
char *database;
+ char *db_secret;
};
typedef struct ReputationEntry ReputationEntry;
@@ -97,6 +118,7 @@ struct ReputationEntry {
/* Global variables */
static struct cfgstruct cfg; /**< Current configuration */
+static struct cfgstruct test; /**< Testing configuration (not active yet) */
long reputation_starttime = 0;
long reputation_writtentime = 0;
@@ -111,7 +133,8 @@ ModDataInfo *reputation_md; /* Module Data structure which we acquire */
void reputation_md_free(ModData *m);
char *reputation_md_serialize(ModData *m);
void reputation_md_unserialize(char *str, ModData *m);
-void config_setdefaults(void);
+void reputation_config_setdefaults(struct cfgstruct *cfg);
+void reputation_free_config(struct cfgstruct *cfg);
CMD_FUNC(reputation_cmd);
CMD_FUNC(reputationunperm);
int reputation_whois(Client *client, Client *target);
@@ -122,18 +145,21 @@ int reputation_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
int reputation_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
int reputation_config_posttest(int *errs);
static uint64_t hash_reputation_entry(char *ip);
+ReputationEntry *find_reputation_entry(char *ip);
void add_reputation_entry(ReputationEntry *e);
EVENT(delete_old_records);
EVENT(add_scores);
-EVENT(save_db_evt);
-void load_db(void);
-void save_db(void);
+EVENT(reputation_save_db_evt);
+int reputation_load_db(void);
+int reputation_save_db(void);
int reputation_starttime_callback(void);
MOD_TEST()
{
memcpy(&ModInf, modinfo, modinfo->size);
memset(&cfg, 0, sizeof(cfg));
+ memset(&test, 0, sizeof(cfg));
+ reputation_config_setdefaults(&test);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, reputation_config_test);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, reputation_config_posttest);
CallbackAddEx(modinfo->handle, CALLBACKTYPE_REPUTATION_STARTTIME, reputation_starttime_callback);
@@ -146,6 +172,7 @@ MOD_INIT()
MARK_AS_OFFICIAL_MODULE(modinfo);
ModuleSetOptions(modinfo->handle, MOD_OPT_PERM, 1);
+
memset(&ReputationHashTable, 0, sizeof(ReputationHashTable));
siphash_generate_key(siphashkey_reputation);
@@ -160,7 +187,7 @@ MOD_INIT()
if (!reputation_md)
abort();
- config_setdefaults();
+ reputation_config_setdefaults(&cfg);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, reputation_config_run);
HookAdd(modinfo->handle, HOOKTYPE_WHOIS, 0, reputation_whois);
HookAdd(modinfo->handle, HOOKTYPE_HANDSHAKE, 0, reputation_set_on_connect);
@@ -172,43 +199,79 @@ MOD_INIT()
return MOD_SUCCESS;
}
+#ifdef BENCHMARK
+void reputation_benchmark(int entries)
+{
+ char ip[64];
+ int i;
+ ReputationEntry *e;
+
+ srand(1234); // fixed seed
+
+ for (i = 0; i < entries; i++)
+ {
+ ReputationEntry *e = safe_alloc(sizeof(ReputationEntry) + 64);
+ snprintf(e->ip, 63, "%d.%d.%d.%d", rand()%255, rand()%255, rand()%255, rand()%255);
+ e->score = rand()%255 + 1;
+ e->last_seen = TStime();
+ if (find_reputation_entry(e->ip))
+ {
+ safe_free(e);
+ continue;
+ }
+ add_reputation_entry(e);
+ }
+}
+#endif
MOD_LOAD()
{
- load_db();
+ reputation_load_db();
if (reputation_starttime == 0)
reputation_starttime = TStime();
EventAdd(ModInf.handle, "delete_old_records", delete_old_records, NULL, DELETE_OLD_EVERY*1000, 0);
EventAdd(ModInf.handle, "add_scores", add_scores, NULL, BUMP_SCORE_EVERY*1000, 0);
- EventAdd(ModInf.handle, "save_db", save_db_evt, NULL, SAVE_DB_EVERY*1000, 0);
+ EventAdd(ModInf.handle, "reputation_save_db", reputation_save_db_evt, NULL, SAVE_DB_EVERY*1000, 0);
+#ifdef BENCHMARK
+ reputation_benchmark(10000);
+#endif
return MOD_SUCCESS;
}
MOD_UNLOAD()
{
- save_db();
+ if (loop.ircd_terminating)
+ reputation_save_db();
+ reputation_free_config(&test);
+ reputation_free_config(&cfg);
return MOD_SUCCESS;
}
-void config_setdefaults(void)
+void reputation_config_setdefaults(struct cfgstruct *cfg)
{
/* data/reputation.db */
- safe_strdup(cfg.database, "reputation.db");
- convert_to_absolute_path(&cfg.database, PERMDATADIR);
+ safe_strdup(cfg->database, "reputation.db");
+ convert_to_absolute_path(&cfg->database, PERMDATADIR);
/* EXPIRES the following entries if the IP does appear for some time: */
/* <=2 points after 1 hour */
- cfg.expire_score[0] = 2;
+ cfg->expire_score[0] = 2;
#ifndef TEST
- cfg.expire_time[0] = 3600;
+ cfg->expire_time[0] = 3600;
#else
- cfg.expire_time[0] = 36;
+ cfg->expire_time[0] = 36;
#endif
/* <=6 points after 7 days */
- cfg.expire_score[1] = 6;
- cfg.expire_time[1] = 86400*7;
+ cfg->expire_score[1] = 6;
+ cfg->expire_time[1] = 86400*7;
/* ANY result that has not been seen for 30 days */
- cfg.expire_score[2] = -1;
- cfg.expire_time[2] = 86400*30;
+ cfg->expire_score[2] = -1;
+ cfg->expire_time[2] = 86400*30;
+}
+
+void reputation_free_config(struct cfgstruct *cfg)
+{
+ safe_free(cfg->database);
+ safe_free(cfg->db_secret);
}
int reputation_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
@@ -218,11 +281,11 @@ int reputation_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
if (type != CONFIG_SET)
return 0;
-
+
/* We are only interrested in set::reputation.. */
if (!ce || strcmp(ce->ce_varname, "reputation"))
return 0;
-
+
for (cep = ce->ce_entries; cep; cep = cep->ce_next)
{
if (!cep->ce_vardata)
@@ -235,6 +298,18 @@ int reputation_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
if (!strcmp(cep->ce_varname, "database"))
{
convert_to_absolute_path(&cep->ce_vardata, PERMDATADIR);
+ safe_strdup(test.database, cep->ce_vardata);
+ } else
+ if (!strcmp(cep->ce_varname, "db-secret"))
+ {
+ char *err;
+ if ((err = unrealdb_test_secret(cep->ce_vardata)))
+ {
+ config_error("%s:%i: set::channeldb::db-secret: %s", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, err);
+ errors++;
+ continue;
+ }
+ safe_strdup(test.db_secret, cep->ce_vardata);
} else
{
config_error("%s:%i: unknown directive set::reputation::%s",
@@ -243,7 +318,7 @@ int reputation_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
continue;
}
}
-
+
*errs = errors;
return errors ? -1 : 1;
}
@@ -254,16 +329,20 @@ int reputation_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
if (type != CONFIG_SET)
return 0;
-
+
/* We are only interrested in set::reputation.. */
if (!ce || strcmp(ce->ce_varname, "reputation"))
return 0;
-
+
for (cep = ce->ce_entries; cep; cep = cep->ce_next)
{
if (!strcmp(cep->ce_varname, "database"))
{
safe_strdup(cfg.database, cep->ce_vardata);
+ } else
+ if (!strcmp(cep->ce_varname, "db-secret"))
+ {
+ safe_strdup(cfg.db_secret, cep->ce_vardata);
}
}
return 1;
@@ -272,13 +351,20 @@ int reputation_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
int reputation_config_posttest(int *errs)
{
int errors = 0;
+ char *errstr;
+
+ if (test.database && ((errstr = unrealdb_test_db(test.database, test.db_secret))))
+ {
+ config_error("[reputation] %s", errstr);
+ errors++;
+ }
*errs = errors;
return errors ? -1 : 1;
}
/** Parse database header and set variables appropriately */
-int parse_db_header(char *buf)
+int parse_db_header_old(char *buf)
{
char *header=NULL, *version=NULL, *starttime=NULL, *writtentime=NULL;
char *p=NULL;
@@ -308,7 +394,7 @@ int parse_db_header(char *buf)
return 1;
}
-void load_db(void)
+void reputation_load_db_old(void)
{
FILE *fd;
char buf[512], *p;
@@ -324,7 +410,7 @@ void load_db(void)
config_warn("WARNING: Could not open/read database '%s': %s", cfg.database, strerror(ERRNO));
return;
}
-
+
memset(buf, 0, sizeof(buf));
if (fgets(buf, 512, fd) == NULL)
{
@@ -332,7 +418,7 @@ void load_db(void)
fclose(fd);
return;
}
-
+
/* Header contains: REPDB <version> <starttime> <writtentime>
* Where:
* REPDB: Literally the string "REPDB".
@@ -341,7 +427,7 @@ void load_db(void)
* in other words: when this module was first loaded, ever.
* <writtentime> Time that the database was last written.
*/
- if (!parse_db_header(buf))
+ if (!parse_db_header_old(buf))
{
config_error("WARNING: Cannot load database %s. Error reading header. "
"Database corrupt? Or are you downgrading from a newer "
@@ -355,7 +441,7 @@ void load_db(void)
{
char *ip = NULL, *score = NULL, *last_seen = NULL;
ReputationEntry *e;
-
+
stripcrlf(buf);
/* Format: <ip> <score> <last seen> */
ip = strtoken(&p, buf, " ");
@@ -367,12 +453,12 @@ void load_db(void)
last_seen = strtoken(&p, NULL, " ");
if (!last_seen)
continue;
-
+
e = safe_alloc(sizeof(ReputationEntry)+strlen(ip));
strcpy(e->ip, ip); /* safe, see alloc above */
e->score = atoi(score);
e->last_seen = atol(last_seen);
-
+
add_reputation_entry(e);
}
fclose(fd);
@@ -384,7 +470,130 @@ void load_db(void)
#endif
}
-void save_db(void)
+#define R_SAFE(x) \
+ do { \
+ if (!(x)) { \
+ config_warn("[reputation] Read error from database file '%s' (possible corruption): %s", cfg.database, unrealdb_get_error_string()); \
+ unrealdb_close(db); \
+ safe_free(ip); \
+ return 0; \
+ } \
+ } while(0)
+
+int reputation_load_db_new(UnrealDB *db)
+{
+ uint64_t l_db_version = 0;
+ uint64_t l_starttime = 0;
+ uint64_t l_writtentime = 0;
+ uint64_t count = 0;
+ uint64_t i;
+ char *ip = NULL;
+ uint16_t score;
+ uint64_t last_seen;
+ ReputationEntry *e;
+#ifdef BENCHMARK
+ struct timeval tv_alpha, tv_beta;
+
+ gettimeofday(&tv_alpha, NULL);
+#endif
+
+ R_SAFE(unrealdb_read_int64(db, &l_db_version)); /* reputation db version */
+ if (l_db_version > 2)
+ {
+ config_error("[reputation] Reputation DB is of a newer version (%ld) than supported by us (%ld). "
+ "Did you perhaps downgrade your UnrealIRCd?",
+ (long)l_db_version, (long)2);
+ unrealdb_close(db);
+ return 0;
+ }
+ R_SAFE(unrealdb_read_int64(db, &l_starttime)); /* starttime of data gathering */
+ R_SAFE(unrealdb_read_int64(db, &l_writtentime)); /* current time */
+ R_SAFE(unrealdb_read_int64(db, &count)); /* number of entries */
+
+ reputation_starttime = l_starttime;
+ reputation_writtentime = l_writtentime;
+
+ for (i=0; i < count; i++)
+ {
+ R_SAFE(unrealdb_read_str(db, &ip));
+ R_SAFE(unrealdb_read_int16(db, &score));
+ R_SAFE(unrealdb_read_int64(db, &last_seen));
+
+ e = safe_alloc(sizeof(ReputationEntry)+strlen(ip));
+ strcpy(e->ip, ip); /* safe, see alloc above */
+ e->score = score;
+ e->last_seen = last_seen;
+ add_reputation_entry(e);
+ safe_free(ip);
+ }
+ unrealdb_close(db);
+#ifdef BENCHMARK
+ gettimeofday(&tv_beta, NULL);
+ ircd_log(LOG_ERROR, "Reputation benchmark: LOAD DB: %lld microseconds",
+ (long long)(((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
+#endif
+ return 1;
+}
+
+/** Load the reputation DB.
+ * Strategy is:
+ * 1) Check for the old header "REPDB 1", if so then call reputation_load_db_old().
+ * 2) Otherwise, open with unrealdb routine
+ * 3) If that fails due to a password provided but the file is unrealdb without password
+ * then fallback to open without a password (so users can easily upgrade to encrypted)
+ */
+int reputation_load_db(void)
+{
+ FILE *fd;
+ UnrealDB *db;
+ char buf[512];
+
+ fd = fopen(cfg.database, "r");
+ if (!fd)
+ {
+ /* Database does not exist. Could be first boot */
+ config_warn("[reputation] No database present at '%s', will start a new one", cfg.database);
+ return 1;
+ }
+
+ *buf = '\0';
+ if (fgets(buf, sizeof(buf), fd) == NULL)
+ {
+ fclose(fd);
+ config_warn("[reputation] Database at '%s' is 0 bytes", cfg.database);
+ return 1;
+ }
+ fclose(fd);
+ if (!strncmp(buf, "REPDB 1 ", 8))
+ {
+ reputation_load_db_old();
+ return 1; /* not so good to always pretend succes */
+ }
+
+ /* Otherwise, it is an unrealdb, crypted or not */
+ db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, cfg.db_secret);
+ if (!db)
+ {
+ if (unrealdb_get_error_code() == UNREALDB_ERROR_FILENOTFOUND)
+ {
+ /* Database does not exist. Could be first boot */
+ config_warn("[reputation] No database present at '%s', will start a new one", cfg.database);
+ return 1;
+ } else
+ if (unrealdb_get_error_code() == UNREALDB_ERROR_NOTCRYPTED)
+ {
+ db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, NULL);
+ }
+ if (!db)
+ {
+ config_error("[reputation] Unable to open the database file '%s' for reading: %s", cfg.database, unrealdb_get_error_string());
+ return 0;
+ }
+ }
+ return reputation_load_db_new(db);
+}
+
+int reputation_save_db_old(void)
{
FILE *fd;
char tmpfname[512];
@@ -395,19 +604,15 @@ void save_db(void)
gettimeofday(&tv_alpha, NULL);
#endif
-
-#ifdef TEST
- sendto_realops("REPUTATION IS RUNNING IN TEST MODE. SAVING DB'S...");
-#endif
/* We write to a temporary file. Only to rename it later if everything was ok */
snprintf(tmpfname, sizeof(tmpfname), "%s.%x.tmp", cfg.database, getrandom32());
-
+
fd = fopen(tmpfname, "w");
if (!fd)
{
config_error("ERROR: Could not open/write database '%s': %s -- DATABASE *NOT* SAVED!!!", tmpfname, strerror(ERRNO));
- return;
+ return 0;
}
if (fprintf(fd, "REPDB 1 %lld %lld\n", (long long)reputation_starttime, (long long)TStime()) < 0)
@@ -422,7 +627,7 @@ void save_db(void)
write_fail:
config_error("ERROR writing to '%s': %s -- DATABASE *NOT* SAVED!!!", tmpfname, strerror(ERRNO));
fclose(fd);
- return;
+ return 0;
}
}
}
@@ -430,9 +635,9 @@ write_fail:
if (fclose(fd) < 0)
{
config_error("ERROR writing to '%s': %s -- DATABASE *NOT* SAVED!!!", tmpfname, strerror(ERRNO));
- return;
+ return 0;
}
-
+
/* Everything went fine. We rename our temporary file to the existing
* DB file (will overwrite), which is more or less an atomic operation.
*/
@@ -444,7 +649,7 @@ write_fail:
{
config_error("ERROR renaming '%s' to '%s': %s -- DATABASE *NOT* SAVED!!!",
tmpfname, cfg.database, strerror(ERRNO));
- return;
+ return 0;
}
reputation_writtentime = TStime();
@@ -455,7 +660,91 @@ write_fail:
(long long)(((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
#endif
- return;
+ return 1;
+}
+
+int reputation_save_db(void)
+{
+ UnrealDB *db;
+ char tmpfname[512];
+ int i;
+ uint64_t count;
+ ReputationEntry *e;
+#ifdef BENCHMARK
+ struct timeval tv_alpha, tv_beta;
+
+ gettimeofday(&tv_alpha, NULL);
+#endif
+
+#ifdef TEST
+ sendto_realops("REPUTATION IS RUNNING IN TEST MODE. SAVING DB'S...");
+#endif
+
+ /* Comment this out after one or more releases (means you cannot downgrade to <=5.0.9.1 anymore) */
+ if (cfg.db_secret == NULL)
+ return reputation_save_db_old();
+
+ /* We write to a temporary file. Only to rename it later if everything was ok */
+ snprintf(tmpfname, sizeof(tmpfname), "%s.%x.tmp", cfg.database, getrandom32());
+
+ db = unrealdb_open(tmpfname, UNREALDB_MODE_WRITE, cfg.db_secret);
+ if (!db)
+ {
+ WARN_WRITE_ERROR(tmpfname);
+ return 0;
+ }
+
+ /* Write header */
+ W_SAFE(unrealdb_write_int64(db, 2)); /* reputation db version */
+ W_SAFE(unrealdb_write_int64(db, reputation_starttime)); /* starttime of data gathering */
+ W_SAFE(unrealdb_write_int64(db, TStime())); /* current time */
+
+ /* Count entries */
+ count = 0;
+ for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
+ for (e = ReputationHashTable[i]; e; e = e->next)
+ count++;
+ W_SAFE(unrealdb_write_int64(db, count)); /* Number of DB entries */
+
+ /* Now write the actual individual entries: */
+ for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
+ {
+ for (e = ReputationHashTable[i]; e; e = e->next)
+ {
+ W_SAFE(unrealdb_write_str(db, e->ip));
+ W_SAFE(unrealdb_write_int16(db, e->score));
+ W_SAFE(unrealdb_write_int64(db, e->last_seen));
+ }
+ }
+
+ if (!unrealdb_close(db))
+ {
+ WARN_WRITE_ERROR(tmpfname);
+ return 0;
+ }
+
+ /* Everything went fine. We rename our temporary file to the existing
+ * DB file (will overwrite), which is more or less an atomic operation.
+ */
+#ifdef _WIN32
+ /* The rename operation cannot be atomic on Windows as it will cause a "file exists" error */
+ unlink(cfg.database);
+#endif
+ if (rename(tmpfname, cfg.database) < 0)
+ {
+ config_error("ERROR renaming '%s' to '%s': %s -- DATABASE *NOT* SAVED!!!",
+ tmpfname, cfg.database, strerror(ERRNO));
+ return 0;
+ }
+
+ reputation_writtentime = TStime();
+
+#ifdef BENCHMARK
+ gettimeofday(&tv_beta, NULL);
+ ircd_log(LOG_ERROR, "Reputation benchmark: SAVE DB: %lld microseconds",
+ (long long)(((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
+#endif
+ return 1;
}
static uint64_t hash_reputation_entry(char *ip)
@@ -548,7 +837,7 @@ EVENT(add_scores)
*/
#define MARKER_UNREGISTERED_USER (marker)
#define MARKER_REGISTERED_USER (marker+1)
-
+
list_for_each_entry(client, &client_list, client_node)
{
if (!IsUser(client))
@@ -625,13 +914,13 @@ EVENT(delete_old_records)
gettimeofday(&tv_alpha, NULL);
#endif
-
+
for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
{
for (e = ReputationHashTable[i]; e; e = e_next)
{
e_next = e->next;
-
+
if (is_reputation_expired(e))
{
#ifdef DEBUGMODE
@@ -651,9 +940,9 @@ EVENT(delete_old_records)
#endif
}
-EVENT(save_db_evt)
+EVENT(reputation_save_db_evt)
{
- save_db();
+ reputation_save_db();
}
CMD_FUNC(reputationunperm)
@@ -801,7 +1090,7 @@ CMD_FUNC(reputation_user_cmd)
sendnumeric(client, ERR_NOPRIVILEGES);
return;
}
-
+
if ((parc < 2) || BadPtr(parv[1]))
{
sendnotice(client, "Reputation module statistics:");
@@ -825,7 +1114,7 @@ CMD_FUNC(reputation_user_cmd)
sendnotice(client, "/REPUTATION <NN List users with reputation score below value NN");
return;
}
-
+
if (strchr(parv[1], '.') || strchr(parv[1], ':'))
{
ip = parv[1];
@@ -871,7 +1160,7 @@ CMD_FUNC(reputation_user_cmd)
return;
}
}
-
+
e = find_reputation_entry(ip);
if (!e)
{
@@ -927,7 +1216,7 @@ CMD_FUNC(reputation_server_cmd)
sendnumeric(client, ERR_NEEDMOREPARAMS, "REPUTATION");
return;
}
-
+
ip = parv[1];
if (parv[2][0] == '*')
@@ -993,7 +1282,7 @@ CMD_FUNC(reputation_cmd)
{
if (MyUser(client))
reputation_user_cmd(client, recv_mtags, parc, parv);
- else if (IsServer(client))
+ else if (IsServer(client) || IsMe(client))
reputation_server_cmd(client, recv_mtags, parc, parv);
}
@@ -1003,7 +1292,7 @@ int reputation_whois(Client *client, Client *target)
if (!IsOper(client))
return 0; /* only opers can see this.. */
-
+
if (reputation > 0)
{
sendto_one(client, NULL, ":%s %d %s %s :is using an IP with a reputation score of %d",
diff --git a/src/modules/restrict-commands.c b/src/modules/restrict-commands.c
@@ -36,6 +36,7 @@ struct RestrictedCommand {
int exempt_identified;
int exempt_reputation_score;
int exempt_webirc;
+ int exempt_tls;
};
typedef struct {
@@ -179,9 +180,9 @@ int rcmd_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
if (!strcmp(cep2->ce_varname, "connect-delay"))
{
long v = config_checkval(cep2->ce_vardata, CFG_TIME);
- if ((v < 10) || (v > 3600))
+ if ((v < 1) || (v > 3600))
{
- config_error("%s:%i: set::restrict-commands::%s::connect-delay should be in range 10-3600", cep2->ce_fileptr->cf_filename, cep2->ce_varlinenum, cep->ce_varname);
+ config_error("%s:%i: set::restrict-commands::%s::connect-delay should be in range 1-3600", cep2->ce_fileptr->cf_filename, cep2->ce_varlinenum, cep->ce_varname);
errors++;
}
continue;
@@ -189,10 +190,13 @@ int rcmd_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
if (!strcmp(cep2->ce_varname, "exempt-identified"))
continue;
-
+
if (!strcmp(cep2->ce_varname, "exempt-webirc"))
continue;
-
+
+ if (!strcmp(cep2->ce_varname, "exempt-tls"))
+ continue;
+
if (!strcmp(cep2->ce_varname, "exempt-reputation-score"))
{
int v = atoi(cep2->ce_vardata);
@@ -279,6 +283,12 @@ int rcmd_configrun(ConfigFile *cf, ConfigEntry *ce, int type)
continue;
}
+ if (!strcmp(cep2->ce_varname, "exempt-tls"))
+ {
+ rcmd->exempt_tls = config_checkval(cep2->ce_vardata, CFG_YESNO);
+ continue;
+ }
+
if (!strcmp(cep2->ce_varname, "exempt-reputation-score"))
{
rcmd->exempt_reputation_score = atoi(cep2->ce_vardata);
@@ -299,6 +309,8 @@ int rcmd_canbypass(Client *client, RestrictedCommand *rcmd)
return 1;
if (rcmd->exempt_webirc && moddata_client_get(client, "webirc"))
return 1;
+ if (rcmd->exempt_tls && IsSecureConnect(client))
+ return 1;
if (rcmd->exempt_reputation_score > 0 && (GetReputation(client) >= rcmd->exempt_reputation_score))
return 1;
if (rcmd->connect_delay && client->local && (TStime() - client->local->firsttime >= rcmd->connect_delay))
diff --git a/src/modules/sajoin.c b/src/modules/sajoin.c
@@ -275,24 +275,5 @@ CMD_FUNC(cmd_sajoin)
strlcat(jbuf, ",", sizeof jbuf);
strlcat(jbuf, name, sizeof jbuf);
}
-
- if (did_anything)
- {
- if (!sjmode)
- {
- //sendnotice(target, "*** You were forced to join %s", jbuf);
- sendto_umode_global(UMODE_OPER, "%s used SAJOIN to make %s join %s", client->name, target->name, jbuf);
- /* Logging function added by XeRXeS */
- ircd_log(LOG_SACMDS,"SAJOIN: %s used SAJOIN to make %s join %s",
- client->name, target->name, jbuf);
- }
- else
- {
- //sendnotice(target, "*** You were forced to join %s with '%c'", jbuf, sjmode);
- sendto_umode_global(UMODE_OPER, "%s used SAJOIN to make %s join %c%s", client->name, target->name, sjmode, jbuf);
- ircd_log(LOG_SACMDS,"SAJOIN: %s used SAJOIN to make %s join %c%s",
- client->name, target->name, sjmode, jbuf);
- }
- }
}
}
diff --git a/src/modules/sapart.c b/src/modules/sapart.c
@@ -159,22 +159,6 @@ CMD_FUNC(cmd_sapart)
parv[0] = target->name; // nick
parv[1] = parv[2]; // chan
parv[2] = comment ? commentx : NULL; // comment
- if (comment)
- {
- //sendnotice(target, "*** You were forced to part %s (%s)", parv[1], commentx);
- sendto_umode_global(UMODE_OPER, "%s used SAPART to make %s part %s (%s)",
- client->name, target->name, parv[1], comment);
- ircd_log(LOG_SACMDS,"SAPART: %s used SAPART to make %s part %s (%s)",
- client->name, target->name, parv[1], comment);
- }
- else
- {
- //sendnotice(target, "*** You were forced to part %s", parv[1]);
- sendto_umode_global(UMODE_OPER, "%s used SAPART to make %s part %s",
- client->name, target->name, parv[1]);
- ircd_log(LOG_SACMDS,"SAPART: %s used SAPART to make %s part %s",
- client->name, target->name, parv[1]);
- }
do_cmd(target, NULL, "PART", comment ? 3 : 2, parv);
/* target may be killed now due to the part reason @ spamfilter */
}
diff --git a/src/modules/sasl.c b/src/modules/sasl.c
@@ -118,6 +118,8 @@ CMD_FUNC(cmd_svslogin)
strlcpy(target->user->svid, parv[3], sizeof(target->user->svid));
user_account_login(recv_mtags, target);
+ if (MyConnect(target) && IsDead(target))
+ return; /* was killed due to *LINE on ~a probably */
} else {
/* It is perfectly normal for target to be NULL as this
* happens during registration phase (pre-connect).
diff --git a/src/modules/server.c b/src/modules/server.c
@@ -179,6 +179,7 @@ void _send_protoctl_servers(Client *client, int response)
{
char buf[512];
Client *acptr;
+ int sendit = 1;
sendto_one(client, NULL, "PROTOCTL EAUTH=%s,%d,%s%s,%s",
me.name, UnrealProtocol, serveropts, extraflags ? extraflags : "", version);
@@ -188,15 +189,24 @@ void _send_protoctl_servers(Client *client, int response)
list_for_each_entry(acptr, &global_server_list, client_node)
{
snprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), "%s,", acptr->id);
+ sendit = 1;
if (strlen(buf) > sizeof(buf)-12)
- break; /* prevent overflow/cutoff if you have a network with more than 90 servers or something. */
+ {
+ if (buf[strlen(buf)-1] == ',')
+ buf[strlen(buf)-1] = '\0';
+ sendto_one(client, NULL, "%s", buf);
+ /* We use the asterisk here too for continuation lines */
+ ircsnprintf(buf, sizeof(buf), "PROTOCTL SERVERS=*");
+ sendit = 0;
+ }
}
/* Remove final comma (if any) */
if (buf[strlen(buf)-1] == ',')
buf[strlen(buf)-1] = '\0';
- sendto_one(client, NULL, "%s", buf);
+ if (sendit)
+ sendto_one(client, NULL, "%s", buf);
}
void _send_server_message(Client *client)
diff --git a/src/modules/setname.c b/src/modules/setname.c
@@ -89,7 +89,7 @@ CMD_FUNC(cmd_setname)
/* set the new name before we check, but don't send to servers unless it is ok */
strcpy(client->info, parv[1]);
spamfilter_build_user_string(spamfilter_user, client->name, client);
- if (match_spamfilter(client, spamfilter_user, SPAMF_USER, NULL, 0, NULL))
+ if (match_spamfilter(client, spamfilter_user, SPAMF_USER, "SETNAME", NULL, 0, NULL))
{
/* Was rejected by spamfilter, restore the realname */
strcpy(client->info, tmpinfo);
diff --git a/src/modules/squit.c b/src/modules/squit.c
@@ -144,15 +144,6 @@ CMD_FUNC(cmd_squit)
sendto_umode_global(UMODE_OPER, "Received SQUIT %s from %s (%s)",
target->name, get_client_name(client, FALSE), comment);
}
- if (IsOper(client))
- {
- /*
- * It was manually /squit'ed by a human being(we hope),
- * there is a very good chance they don't want us to
- * reconnect right away. -Cabal95
- */
- SetSQuit(target);
- }
- exit_client(target, recv_mtags, comment);
+ exit_client_ex(target, client->direction, recv_mtags, comment);
}
diff --git a/src/modules/stats.c b/src/modules/stats.c
@@ -376,8 +376,14 @@ CMD_FUNC(cmd_stats)
else
stat->func(client, NULL);
- /* Modules can append data: */
- RunHook2(HOOKTYPE_STATS, client, flags);
+ /* Modules can append data:
+ * ('STATS S' already has special code for this that
+ * maintains certain ordering, so not included here)
+ */
+ if (stat->flag != 'S')
+ {
+ RunHook2(HOOKTYPE_STATS, client, flags);
+ }
sendnumeric(client, RPL_ENDOFSTATS, stat->flag);
@@ -772,9 +778,33 @@ int stats_officialchannels(Client *client, char *para)
#define SafePrint(x) ((x) ? (x) : "")
+/** Helper for stats_set() */
+static void stats_set_anti_flood(Client *client, FloodSettings *f)
+{
+ int i;
+
+ for (i=0; floodoption_names[i]; i++)
+ {
+ if (i == FLD_CONVERSATIONS)
+ {
+ sendtxtnumeric(client, "anti-flood::%s::%s: %d users, new user every %s",
+ f->name, floodoption_names[i],
+ (int)f->limit[i], pretty_time_val(f->period[i]));
+ }
+ else
+ {
+ sendtxtnumeric(client, "anti-flood::%s::%s: %d per %s",
+ f->name, floodoption_names[i],
+ (int)f->limit[i], pretty_time_val(f->period[i]));
+ }
+ }
+}
+
int stats_set(Client *client, char *para)
{
char *uhallow;
+ SecurityGroup *s;
+ FloodSettings *f;
if (!ValidatePermissionsForPath("server:info:stats",client,NULL,NULL,NULL))
{
@@ -875,9 +905,17 @@ int stats_set(Client *client, char *para)
sendtxtnumeric(client, "anti-flood::handshake-data-flood::amount: %ld bytes", iConf.handshake_data_flood_amount);
sendtxtnumeric(client, "anti-flood::handshake-data-flood::ban-action: %s", banact_valtostring(iConf.handshake_data_flood_ban_action));
sendtxtnumeric(client, "anti-flood::handshake-data-flood::ban-time: %s", pretty_time_val(iConf.handshake_data_flood_ban_time));
- if (AWAY_PERIOD)
- sendtxtnumeric(client, "anti-flood::away-flood: %d per %s", AWAY_COUNT, pretty_time_val(AWAY_PERIOD));
- sendtxtnumeric(client, "anti-flood::nick-flood: %d per %s", NICK_COUNT, pretty_time_val(NICK_PERIOD));
+
+ /* set::anti-flood */
+ for (s = securitygroups; s; s = s->next)
+ if ((f = find_floodsettings_block(s->name)))
+ stats_set_anti_flood(client, f);
+ f = find_floodsettings_block("unknown-users");
+ stats_set_anti_flood(client, f);
+
+ //if (AWAY_PERIOD)
+ // sendtxtnumeric(client, "anti-flood::away-flood: %d per %s", AWAY_COUNT, pretty_time_val(AWAY_PERIOD));
+ //sendtxtnumeric(client, "anti-flood::nick-flood: %d per %s", NICK_COUNT, pretty_time_val(NICK_PERIOD));
sendtxtnumeric(client, "handshake-timeout: %s", pretty_time_val(iConf.handshake_timeout));
sendtxtnumeric(client, "sasl-timeout: %s", pretty_time_val(iConf.sasl_timeout));
sendtxtnumeric(client, "ident::connect-timeout: %s", pretty_time_val(IDENT_CONNECT_TIMEOUT));
@@ -895,6 +933,10 @@ int stats_set(Client *client, char *para)
sendtxtnumeric(client, "outdated-tls-policy::oper: %s", policy_valtostr(iConf.outdated_tls_policy_oper));
sendtxtnumeric(client, "outdated-tls-policy::server: %s", policy_valtostr(iConf.outdated_tls_policy_server));
RunHook2(HOOKTYPE_STATS, client, "S");
+#ifndef _WIN32
+ sendtxtnumeric(client, "This server can handle %d concurrent sockets (%d clients + %d reserve)",
+ maxclients+CLIENTS_RESERVE, maxclients, CLIENTS_RESERVE);
+#endif
return 1;
}
diff --git a/src/modules/svsmode.c b/src/modules/svsmode.c
@@ -408,6 +408,8 @@ void do_svsmode(Client *client, MessageTag *recv_mtags, int parc, char *parv[],
{
strlcpy(target->user->svid, parv[3], sizeof(target->user->svid));
user_account_login(recv_mtags, target);
+ if (MyConnect(target) && IsDead(target))
+ return; /* was killed due to *LINE on ~a probably */
}
else
{
diff --git a/src/modules/svsnick.c b/src/modules/svsnick.c
@@ -95,6 +95,7 @@ CMD_FUNC(cmd_svsnick)
/* no 'recv_mtags' here, we do not inherit from SVSNICK but generate a new NICK event */
new_message(acptr, NULL, &mtags);
+ RunHook3(HOOKTYPE_LOCAL_NICKCHANGE, acptr, mtags, parv[2]);
sendto_local_common_channels(acptr, acptr, 0, mtags, ":%s NICK :%s", acptr->name, parv[2]);
sendto_one(acptr, mtags, ":%s NICK :%s", acptr->name, parv[2]);
sendto_server(NULL, 0, 0, mtags, ":%s NICK %s :%ld", acptr->id, parv[2], atol(parv[3]));
@@ -107,7 +108,6 @@ CMD_FUNC(cmd_svsnick)
sendto_snomask(SNO_NICKCHANGE,
"*** %s (%s@%s) has been forced to change their nickname to %s",
acptr->name, acptr->user->username, acptr->user->realhost, parv[2]);
- RunHook2(HOOKTYPE_LOCAL_NICKCHANGE, acptr, parv[2]);
strlcpy(acptr->name, parv[2], sizeof acptr->name);
add_to_client_hash_table(parv[2], acptr);
diff --git a/src/modules/targetfloodprot.c b/src/modules/targetfloodprot.c
@@ -234,8 +234,8 @@ int targetfloodprot_can_send_to_channel(Client *client, Channel *channel, Member
if (!MyUser(client))
return HOOK_CONTINUE;
- /* IRCOps and U-Lines override */
- if (IsULine(client) || (IsOper(client) && ValidatePermissionsForPath("immune:target-flood",client,NULL,channel,NULL)))
+ /* U-Lines, servers and IRCOps override */
+ if (IsULine(client) || !IsUser(client) || (IsOper(client) && ValidatePermissionsForPath("immune:target-flood",client,NULL,channel,NULL)))
return HOOK_CONTINUE;
what = sendtypetowhat(sendtype);
@@ -259,6 +259,7 @@ int targetfloodprot_can_send_to_channel(Client *client, Channel *channel, Member
if (flood->cnt[what] >= channelcfg->cnt[what])
{
/* Flood detected */
+ flood_limit_exceeded_log(client, "target-flood-channel");
snprintf(errbuf, sizeof(errbuf), "Channel is being flooded. Message not delivered.");
*errmsg = errbuf;
return HOOK_DENY;
@@ -280,8 +281,8 @@ int targetfloodprot_can_send_to_user(Client *client, Client *target, char **text
if (!MyUser(target))
return HOOK_CONTINUE;
- /* IRCOps and U-Lines override */
- if (IsULine(client) || (IsOper(client) && ValidatePermissionsForPath("immune:target-flood",client,target,NULL,NULL)))
+ /* U-Lines, servers and IRCOps override */
+ if (IsULine(client) || !IsUser(client) || (IsOper(client) && ValidatePermissionsForPath("immune:target-flood",client,target,NULL,NULL)))
return HOOK_CONTINUE;
what = sendtypetowhat(sendtype);
@@ -305,6 +306,7 @@ int targetfloodprot_can_send_to_user(Client *client, Client *target, char **text
if (flood->cnt[what] >= privatecfg->cnt[what])
{
/* Flood detected */
+ flood_limit_exceeded_log(client, "target-flood-user");
snprintf(errbuf, sizeof(errbuf), "User is being flooded. Message not delivered.");
*errmsg = errbuf;
return HOOK_DENY;
diff --git a/src/modules/tkl.c b/src/modules/tkl.c
@@ -82,7 +82,9 @@ void _tkl_stats(Client *client, int type, char *para, int *cnt);
void _tkl_sync(Client *client);
CMD_FUNC(_cmd_tkl);
int _place_host_ban(Client *client, BanAction action, char *reason, long duration);
-int _match_spamfilter(Client *client, char *str_in, int type, char *target, int flags, TKL **rettk);
+int _match_spamfilter(Client *client, char *str_in, int type, char *cmd, char *target, int flags, TKL **rettk);
+int _match_spamfilter_mtags(Client *client, MessageTag *mtags, char *cmd);
+int check_mtag_spamfilters_present(void);
int _join_viruschan(Client *client, TKL *tk, int type);
void _spamfilter_build_user_string(char *buf, char *nick, Client *client);
int _match_user(char *rmask, Client *client, int options);
@@ -109,6 +111,7 @@ struct TKLTypeTable
char *log_name; /**< Used for logging and server notices */
unsigned tkltype:1; /**< Is a type available in cmd_tkl() and friends */
unsigned exceptiontype:1; /**< Is a type available for exceptions */
+ unsigned needip:1; /**< When using this exempt option, only IP addresses are permitted (processed before DNS/ident lookups etc) */
};
/** This table which defines all TKL types and TKL exception types.
@@ -117,35 +120,35 @@ struct TKLTypeTable
*
* IMPORTANT IF YOU ARE ADDING A NEW TYPE TO THIS TABLE:
* - also update eline_syntax()
- * - also check if eline_type_requires_ip() needs to be updated
* - update help.conf (HELPOP ELINE)
* - more?
*/
TKLTypeTable tkl_types[] = {
- /* <config name> <letter> <TKL_xxx type> <logging name> <tkl option?> <exempt option?> */
- { "gline", 'G', TKL_KILL | TKL_GLOBAL, "G-Line", 1, 1 },
- { "kline", 'k', TKL_KILL, "K-Line", 1, 1 },
- { "gzline", 'Z', TKL_ZAP | TKL_GLOBAL, "Global Z-Line", 1, 1 },
- { "zline", 'z', TKL_ZAP, "Z-Line", 1, 1 },
- { "spamfilter", 'F', TKL_SPAMF | TKL_GLOBAL, "Spamfilter", 1, 1 },
- { "qline", 'Q', TKL_NAME | TKL_GLOBAL, "Q-Line", 1, 1 },
- { "except", 'E', TKL_EXCEPTION | TKL_GLOBAL, "Exception", 1, 0 },
- { "shun", 's', TKL_SHUN | TKL_GLOBAL, "Shun", 1, 1 },
- { "local-qline", 'q', TKL_NAME, "Local Q-Line", 1, 0 },
- { "local-spamfilter", 'e', TKL_EXCEPTION, "Local Exception", 1, 0 },
- { "local-exception", 'f', TKL_SPAMF, "Local Spamfilter", 1, 0 },
- { "blacklist", 'b', TKL_BLACKLIST, "Blacklist", 0, 1 },
- { "connect-flood", 'c', TKL_CONNECT_FLOOD, "Connect flood", 0, 1 },
- { "maxperip", 'm', TKL_MAXPERIP, "Max-per-IP", 0, 1 },
- { "handshake-data-flood", 'd', TKL_HANDSHAKE_DATA_FLOOD, "Handshake data flood", 0, 1 },
- { "antirandom", 'r', TKL_ANTIRANDOM, "Antirandom", 0, 1 },
- { "antimixedutf8", '8', TKL_ANTIMIXEDUTF8, "Antimixedutf8", 0, 1 },
- { "ban-version", 'v', TKL_BAN_VERSION, "Ban Version", 0, 1 },
- { NULL, '\0', 0, NULL, 0, 0 },
+ /* <config name> <letter> <TKL_xxx type> <logging name> <tkl option?> <exempt option?> <need ip address?> */
+ { "gline", 'G', TKL_KILL | TKL_GLOBAL, "G-Line", 1, 1, 0 },
+ { "kline", 'k', TKL_KILL, "K-Line", 1, 1, 0 },
+ { "gzline", 'Z', TKL_ZAP | TKL_GLOBAL, "Global Z-Line", 1, 1, 1 },
+ { "zline", 'z', TKL_ZAP, "Z-Line", 1, 1, 1 },
+ { "spamfilter", 'F', TKL_SPAMF | TKL_GLOBAL, "Spamfilter", 1, 1, 0 },
+ { "qline", 'Q', TKL_NAME | TKL_GLOBAL, "Q-Line", 1, 1, 0 },
+ { "except", 'E', TKL_EXCEPTION | TKL_GLOBAL, "Exception", 1, 0, 0 },
+ { "shun", 's', TKL_SHUN | TKL_GLOBAL, "Shun", 1, 1, 0 },
+ { "local-qline", 'q', TKL_NAME, "Local Q-Line", 1, 0, 0 },
+ { "local-spamfilter", 'e', TKL_EXCEPTION, "Local Exception", 1, 0, 0 },
+ { "local-exception", 'f', TKL_SPAMF, "Local Spamfilter", 1, 0, 0 },
+ { "blacklist", 'b', TKL_BLACKLIST, "Blacklist", 0, 1, 1 },
+ { "connect-flood", 'c', TKL_CONNECT_FLOOD, "Connect flood", 0, 1, 1 },
+ { "maxperip", 'm', TKL_MAXPERIP, "Max-per-IP", 0, 1, 0 },
+ { "handshake-data-flood", 'd', TKL_HANDSHAKE_DATA_FLOOD, "Handshake data flood", 0, 1, 1 },
+ { "antirandom", 'r', TKL_ANTIRANDOM, "Antirandom", 0, 1, 0 },
+ { "antimixedutf8", '8', TKL_ANTIMIXEDUTF8, "Antimixedutf8", 0, 1, 0 },
+ { "ban-version", 'v', TKL_BAN_VERSION, "Ban Version", 0, 1, 0 },
+ { NULL, '\0', 0, NULL, 0, 0, 0 },
};
#define ALL_VALID_EXCEPTION_TYPES "kline, gline, zline, gzline, spamfilter, shun, qline, blacklist, connect-flood, handshake-data-flood, antirandom, antimixedutf8, ban-version"
int max_stats_matches = 1000;
+int mtag_spamfilters_present = 0; /**< Are any spamfilters with type SPAMF_MTAG present? */
MOD_TEST()
{
@@ -178,8 +181,9 @@ MOD_TEST()
EfunctionAddVoid(modinfo->handle, EFUNC_TKL_SYNCH, _tkl_sync);
EfunctionAddVoid(modinfo->handle, EFUNC_CMD_TKL, _cmd_tkl);
EfunctionAdd(modinfo->handle, EFUNC_PLACE_HOST_BAN, _place_host_ban);
- EfunctionAdd(modinfo->handle, EFUNC_DOSPAMFILTER, _match_spamfilter);
- EfunctionAdd(modinfo->handle, EFUNC_DOSPAMFILTER_VIRUSCHAN, _join_viruschan);
+ EfunctionAdd(modinfo->handle, EFUNC_MATCH_SPAMFILTER, _match_spamfilter);
+ EfunctionAdd(modinfo->handle, EFUNC_MATCH_SPAMFILTER_MTAGS, _match_spamfilter_mtags);
+ EfunctionAdd(modinfo->handle, EFUNC_JOIN_VIRUSCHAN, _join_viruschan);
EfunctionAddVoid(modinfo->handle, EFUNC_SPAMFILTER_BUILD_USER_STRING, _spamfilter_build_user_string);
EfunctionAdd(modinfo->handle, EFUNC_MATCH_USER, _match_user);
EfunctionAdd(modinfo->handle, EFUNC_TKL_IP_HASH, _tkl_ip_hash);
@@ -213,6 +217,7 @@ MOD_INIT()
MOD_LOAD()
{
+ check_mtag_spamfilters_present();
EventAdd(modinfo->handle, "tklexpire", tkl_check_expire, NULL, 5000, 0);
return MOD_SUCCESS;
}
@@ -1363,6 +1368,15 @@ void cmd_tkl_line(Client *client, int parc, char *parv[], char *type)
mask[3] = '\0';
usermask = mask; /* eg ~S: */
hostmask = mask2buf;
+
+ if (((*type == 'z') || (*type == 'Z')))
+ {
+ sendnotice(client, "ERROR: (g)zlines must be placed at *@\037IPMASK\037. "
+ "Extended server bans don't work here because (g)zlines are processed"
+ "BEFORE dns and ident lookups are done and before reading any client data. "
+ "If you want to use extended server bans then use a KLINE/GLINE instead.");
+ return;
+ }
} else {
/* Delete: allow any attempt */
strlcpy(mask2buf, mask+3, sizeof(mask2buf));
@@ -1543,14 +1557,14 @@ void eline_syntax(Client *client)
* exception to be placed on *@ip rather than
* user@host or *@host. For eg zlines.
*/
-int eline_type_requires_ip(char *bantypes)
+TKLTypeTable *eline_type_requires_ip(char *bantypes)
{
- if (strchr(bantypes, 'z') || strchr(bantypes, 'Z') ||
- strchr(bantypes, 'c') ||
- strchr(bantypes, 'b') ||
- strchr(bantypes, 'd'))
- return 1;
- return 0;
+ int i;
+
+ for (i=0; tkl_types[i].config_name; i++)
+ if (tkl_types[i].needip && strchr(bantypes, tkl_types[i].letter))
+ return &tkl_types[i];
+ return NULL;
}
/** Checks a string to see if it contains invalid ban exception types */
@@ -1590,6 +1604,7 @@ CMD_FUNC(cmd_eline)
"-", /*9 reason */
NULL
};
+ TKLTypeTable *t;
if (IsServer(client))
return;
@@ -1673,11 +1688,11 @@ CMD_FUNC(cmd_eline)
mask[3] = '\0';
usermask = mask; /* eg ~S: */
hostmask = mask2buf;
- if (eline_type_requires_ip(bantypes))
+ if ((t = eline_type_requires_ip(bantypes)))
{
- sendnotice(client, "ERROR: Ban exceptions with type z/Z/c/b do not work on extended server bans. "
- "This is because checking (g)zlines, connect-flood and blacklists is done BEFORE "
- "extended bans can be checked.");
+ sendnotice(client, "ERROR: Ban exception with type '%c' does not work on extended server bans. "
+ "This is because checking for %s takes places BEFORE "
+ "extended bans can be checked.", t->letter, t->log_name);
return;
}
} else {
@@ -1724,25 +1739,31 @@ CMD_FUNC(cmd_eline)
sendnotice(client, "[error] For technical reasons you cannot start the host with a ':', sorry");
return;
}
- if (add && eline_type_requires_ip(bantypes))
+ if (add && ((t = eline_type_requires_ip(bantypes))))
{
/* Trying to exempt a user from a (G)ZLINE,
* make sure the user isn't specifying a host then.
*/
if (strcmp(usermask, "*"))
{
- sendnotice(client, "ERROR: Ban exceptions with type z/Z/c/b need to be placed at \037*\037@ipmask, not \037user\037@ipmask. "
- "This is because checking (g)zlines, connect-flood and blacklists is done BEFORE any dns and ident lookups.");
+ sendnotice(client, "ERROR: Ban exception with type '%c' need to be placed at \037*\037@ipmask, not \037user\037@ipmask. "
+ "This is because checking %s takes places (possibly) BEFORE any dns and ident lookups.",
+ t->letter,
+ t->log_name);
return;
}
for (p=hostmask; *p; p++)
+ {
if (isalpha(*p) && !isxdigit(*p))
{
- sendnotice(client, "ERROR: Ban exceptions with type z/Z/c/b need to be placed at *@\037ipmask\037, not *@\037hostmask\037. "
- "(so for example *@192.168.* is ok, but *@*.aol.com is not). "
- "This is because checking (g)zlines, connect-flood and blacklists is done BEFORE any dns and ident lookups.");
+ sendnotice(client, "ERROR: Ban exception with type '%c' needs to be placed at *@\037ipmask\037, not *@\037hostmask\037. "
+ "(so for example *@192.168.* is OK, but *@*.aol.com is not). "
+ "This is because checking %s takes places (possibly) BEFORE any dns and ident lookups.",
+ t->letter,
+ t->log_name);
return;
}
+ }
}
}
else
@@ -2367,6 +2388,9 @@ TKL *_tkl_add_spamfilter(int type, unsigned short target, BanAction action, Matc
index = tkl_hash(tkl_typetochar(type));
AddListItem(tkl, tklines[index]);
+ if (target & SPAMF_MTAG)
+ mtag_spamfilters_present = 1;
+
return tkl;
}
@@ -2637,6 +2661,7 @@ void _tkl_del_line(TKL *tkl)
/* Finally, free the entry */
free_tkl(tkl);
+ check_mtag_spamfilters_present();
}
/** Add some default ban exceptions - for localhost */
@@ -3052,7 +3077,7 @@ int _find_shun(Client *client)
if (!(tkl->type & TKL_SHUN))
continue;
- snprintf(uhost, sizeof(uhost), "%s@%s", tkl->ptr.serverban->usermask, tkl->ptr.serverban->hostmask);
+ tkl_uhost(tkl, uhost, sizeof(uhost), NO_SOFT_PREFIX);
if (match_user(uhost, client, MATCH_CHECK_REAL))
{
@@ -3114,7 +3139,7 @@ int _find_spamfilter_user(Client *client, int flags)
return 0;
spamfilter_build_user_string(spamfilter_user, client->name, client);
- return match_spamfilter(client, spamfilter_user, SPAMF_USER, NULL, flags, NULL);
+ return match_spamfilter(client, spamfilter_user, SPAMF_USER, NULL, NULL, flags, NULL);
}
/** Check a spamfilter against all local users and print a message.
@@ -4654,6 +4679,7 @@ int _join_viruschan(Client *client, TKL *tkl, int type)
/** match_spamfilter: executes the spamfilter on the input string.
* @param str The text (eg msg text, notice text, part text, quit text, etc
* @param target The spamfilter target (SPAMF_*)
+ * @param cmd The command (eg: "PRIVMSG")
* @param destination The destination as a text string (eg: "somenick", can be NULL.. eg for away)
* @param flags Any flags (SPAMFLAG_*)
* @param rettkl Pointer to an aTKLline struct, _used for special circumstances only_
@@ -4661,7 +4687,7 @@ int _join_viruschan(Client *client, TKL *tkl, int type)
* 1 if spamfilter matched and it should be blocked (or client exited), 0 if not matched.
* In case of 1, be sure to check IsDead(client)..
*/
-int _match_spamfilter(Client *client, char *str_in, int target, char *destination, int flags, TKL **rettkl)
+int _match_spamfilter(Client *client, char *str_in, int target, char *cmd, char *destination, int flags, TKL **rettkl)
{
TKL *tkl;
TKL *winner_tkl = NULL;
@@ -4676,6 +4702,9 @@ int _match_spamfilter(Client *client, char *str_in, int target, char *destinatio
if (rettkl)
*rettkl = NULL; /* initialize to NULL */
+ if (!cmd)
+ cmd = cmdname_by_spamftarget(target);
+
if (target == SPAMF_USER)
str = str_in;
else
@@ -4755,7 +4784,7 @@ int _match_spamfilter(Client *client, char *str_in, int target, char *destinatio
ircsnprintf(buf, sizeof(buf), "[Spamfilter] %s!%s@%s matches filter '%s': [%s%s: '%s'] [%s]",
client->name, client->user->username, client->user->realhost,
tkl->ptr.spamfilter->match->str,
- cmdname_by_spamftarget(target), destinationbuf, str,
+ cmd, destinationbuf, str,
unreal_decodespace(tkl->ptr.spamfilter->tkl_reason));
sendto_snomask_global(SNO_SPAMF, "%s", buf);
@@ -4807,6 +4836,12 @@ int _match_spamfilter(Client *client, char *str_in, int target, char *destinatio
me.name, client->name, destination, reason);
break;
}
+ case SPAMF_MTAG:
+ {
+ sendnumericfmt(client, ERR_CANNOTDOCOMMAND, "%s :Command blocked: %s",
+ cmd, reason);
+ break;
+ }
case SPAMF_DCC:
{
char errmsg[512];
@@ -4837,7 +4872,7 @@ int _match_spamfilter(Client *client, char *str_in, int target, char *destinatio
if ((tkl->ptr.spamfilter->action == BAN_ACT_WARN) || (tkl->ptr.spamfilter->action == BAN_ACT_SOFT_WARN))
{
if ((target != SPAMF_USER) && (target != SPAMF_QUIT))
- sendnumeric(client, RPL_SPAMCMDFWD, cmdname_by_spamftarget(target), reason);
+ sendnumeric(client, RPL_SPAMCMDFWD, cmd, reason);
return 0;
} else
if ((tkl->ptr.spamfilter->action == BAN_ACT_DCCBLOCK) || (tkl->ptr.spamfilter->action == BAN_ACT_SOFT_DCCBLOCK))
@@ -4875,6 +4910,60 @@ int _match_spamfilter(Client *client, char *str_in, int target, char *destinatio
return 0; /* NOTREACHED */
}
+/** Check message-tag spamfilters.
+ * @param client The client
+ * @param mtags Message tags sent by client
+ * @param cmd Command to be executed (can be NULL)
+ * @retval Return 1 to stop processing the command (ignore it) or 0 to allow/continue as normal
+ */
+int _match_spamfilter_mtags(Client *client, MessageTag *mtags, char *cmd)
+{
+ MessageTag *m;
+ char buf[4096];
+ char *str;
+
+ /* This is a shortcut: if there are no spamfilters present
+ * on message tags then we can return immediately.
+ * Saves a lot of CPU and it is quite likely too!
+ */
+ if (mtag_spamfilters_present == 0)
+ return 0;
+
+ for (m = mtags; m; m = m->next)
+ {
+ if (m->value)
+ {
+ snprintf(buf, sizeof(buf), "%s=%s", m->name, m->value);
+ str = buf;
+ } else {
+ str = m->name;
+ }
+ if (match_spamfilter(client, str, SPAMF_MTAG, cmd, NULL, 0, NULL))
+ return 1;
+ }
+ return 0;
+}
+
+/** Updates 'mtag_spamfilters_present' based on if any spamfilters
+ * are present with the SPAMF_MTAG target.
+ */
+int check_mtag_spamfilters_present(void)
+{
+ TKL *tkl;
+
+ for (tkl = tklines[tkl_hash('F')]; tkl; tkl = tkl->next)
+ {
+ if (tkl->ptr.spamfilter->target & SPAMF_MTAG)
+ {
+ mtag_spamfilters_present = 1;
+ return 1;
+ }
+ }
+
+ mtag_spamfilters_present = 0;
+ return 0;
+}
+
/** CIDR function to compare the first 'mask' bits.
* @author Taken from atheme
* @returns 1 if equal, 0 if not.
diff --git a/src/modules/tkldb.c b/src/modules/tkldb.c
@@ -61,14 +61,14 @@ ModuleHeader MOD_HEADER = {
do { \
sendto_realops_and_log("[tkldb] Error writing to temporary database file " \
"'%s': %s (DATABASE NOT SAVED)", \
- fname, strerror(errno)); \
+ fname, unrealdb_get_error_string()); \
} while(0)
#define R_SAFE(x) \
do { \
if (!(x)) { \
- config_warn("[tkldb] Read error from database file '%s' (possible corruption): %s", cfg.database, strerror(errno)); \
- fclose(fd); \
+ config_warn("[tkldb] Read error from database file '%s' (possible corruption): %s", cfg.database, unrealdb_get_error_string()); \
+ unrealdb_close(db); \
FreeTKLRead(); \
return 0; \
} \
@@ -78,7 +78,7 @@ ModuleHeader MOD_HEADER = {
do { \
if (!(x)) { \
WARN_WRITE_ERROR(tmpfname); \
- fclose(fd); \
+ unrealdb_close(db); \
return 0; \
} \
} while(0)
@@ -91,41 +91,56 @@ ModuleHeader MOD_HEADER = {
} \
} while(0)
+/* Structs */
+struct cfgstruct {
+ char *database;
+ char *db_secret;
+};
+
/* Forward declarations */
void tkldb_moddata_free(ModData *md);
-void setcfg(void);
-void freecfg(void);
-int tkldb_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
-int tkldb_configrun(ConfigFile *cf, ConfigEntry *ce, int type);
+void setcfg(struct cfgstruct *cfg);
+void freecfg(struct cfgstruct *cfg);
+int tkldb_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
+int tkldb_config_posttest(int *errs);
+int tkldb_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
EVENT(write_tkldb_evt);
int write_tkldb(void);
-int write_tkline(FILE *fd, const char *tmpfname, TKL *tkl);
+int write_tkline(UnrealDB *db, const char *tmpfname, TKL *tkl);
int read_tkldb(void);
/* Globals variables */
const uint32_t tkldb_version = TKLDB_VERSION;
-struct cfgstruct {
- char *database;
-};
static struct cfgstruct cfg;
+static struct cfgstruct test;
static long tkldb_next_event = 0;
MOD_TEST()
{
memset(&cfg, 0, sizeof(cfg));
- HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, tkldb_configtest);
+ memset(&test, 0, sizeof(test));
+ setcfg(&test);
+ HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, tkldb_config_test);
+ HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, tkldb_config_posttest);
return MOD_SUCCESS;
}
MOD_INIT()
{
MARK_AS_OFFICIAL_MODULE(modinfo);
+ ModuleSetOptions(modinfo->handle, MOD_OPT_UNLOAD_PRIORITY, -9999);
LoadPersistentLong(modinfo, tkldb_next_event);
- setcfg();
+ setcfg(&cfg);
+ HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, tkldb_config_run);
+ return MOD_SUCCESS;
+}
+
+MOD_LOAD()
+{
if (!tkldb_next_event)
{
/* If this is the first time that our module is loaded, then
@@ -142,25 +157,16 @@ MOD_INIT()
}
tkldb_next_event = TStime() + TKLDB_SAVE_EVERY + TKLDB_SAVE_EVERY_DELTA;
}
- HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, tkldb_configrun);
- return MOD_SUCCESS;
-}
-
-MOD_LOAD()
-{
EventAdd(modinfo->handle, "tkldb_write_tkldb", write_tkldb_evt, NULL, 1000, 0);
- if (ModuleGetError(modinfo->handle) != MODERR_NOERROR)
- {
- config_error("A critical error occurred when loading module %s: %s", MOD_HEADER.name, ModuleGetErrorStr(modinfo->handle));
- return MOD_FAILED;
- }
return MOD_SUCCESS;
}
MOD_UNLOAD()
{
- write_tkldb();
- freecfg();
+ if (loop.ircd_terminating)
+ write_tkldb();
+ freecfg(&test);
+ freecfg(&cfg);
SavePersistentLong(modinfo, tkldb_next_event);
return MOD_SUCCESS;
}
@@ -171,19 +177,20 @@ void tkldb_moddata_free(ModData *md)
md->i = 0;
}
-void setcfg(void)
+void setcfg(struct cfgstruct *cfg)
{
// Default: data/tkl.db
- safe_strdup(cfg.database, "tkl.db");
- convert_to_absolute_path(&cfg.database, PERMDATADIR);
+ safe_strdup(cfg->database, "tkl.db");
+ convert_to_absolute_path(&cfg->database, PERMDATADIR);
}
-void freecfg(void)
+void freecfg(struct cfgstruct *cfg)
{
- safe_free(cfg.database);
+ safe_free(cfg->database);
+ safe_free(cfg->db_secret);
}
-int tkldb_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
+int tkldb_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
int errors = 0;
ConfigEntry *cep;
@@ -197,16 +204,45 @@ int tkldb_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
for (cep = ce->ce_entries; cep; cep = cep->ce_next)
{
- if (!cep->ce_vardata) {
+ if (!cep->ce_vardata)
+ {
config_error("%s:%i: blank set::tkldb::%s without value", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname);
errors++;
- continue;
- }
- if (!strcmp(cep->ce_varname, "database")) {
+ } else
+ if (!strcmp(cep->ce_varname, "database"))
+ {
convert_to_absolute_path(&cep->ce_vardata, PERMDATADIR);
- continue;
+ safe_strdup(test.database, cep->ce_vardata);
+ } else
+ if (!strcmp(cep->ce_varname, "db-secret"))
+ {
+ char *err;
+ if ((err = unrealdb_test_secret(cep->ce_vardata)))
+ {
+ config_error("%s:%i: set::tkldb::db-secret: %s", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, err);
+ errors++;
+ continue;
+ }
+ safe_strdup(test.db_secret, cep->ce_vardata);
+ } else
+ {
+ config_error("%s:%i: unknown directive set::tkldb::%s", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname);
+ errors++;
}
- config_error("%s:%i: unknown directive set::tkldb::%s", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname);
+ }
+
+ *errs = errors;
+ return errors ? -1 : 1;
+}
+
+int tkldb_config_posttest(int *errs)
+{
+ int errors = 0;
+ char *errstr;
+
+ if (test.database && ((errstr = unrealdb_test_db(test.database, test.db_secret))))
+ {
+ config_error("[tkldb] %s", errstr);
errors++;
}
@@ -214,7 +250,7 @@ int tkldb_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
return errors ? -1 : 1;
}
-int tkldb_configrun(ConfigFile *cf, ConfigEntry *ce, int type)
+int tkldb_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
{
ConfigEntry *cep;
@@ -229,6 +265,8 @@ int tkldb_configrun(ConfigFile *cf, ConfigEntry *ce, int type)
{
if (!strcmp(cep->ce_varname, "database"))
safe_strdup(cfg.database, cep->ce_vardata);
+ else if (!strcmp(cep->ce_varname, "db-secret"))
+ safe_strdup(cfg.db_secret, cep->ce_vardata);
}
return 1;
}
@@ -244,7 +282,7 @@ EVENT(write_tkldb_evt)
int write_tkldb(void)
{
char tmpfname[512];
- FILE *fd;
+ UnrealDB *db;
uint64_t tklcount;
int index, index2;
TKL *tkl;
@@ -256,15 +294,15 @@ int write_tkldb(void)
// Write to a tempfile first, then rename it if everything succeeded
snprintf(tmpfname, sizeof(tmpfname), "%s.%x.tmp", cfg.database, getrandom32());
- fd = fopen(tmpfname, "wb");
- if (!fd)
+ db = unrealdb_open(tmpfname, UNREALDB_MODE_WRITE, cfg.db_secret);
+ if (!db)
{
WARN_WRITE_ERROR(tmpfname);
return 0;
}
- W_SAFE(write_int32(fd, TKLDB_MAGIC));
- W_SAFE(write_data(fd, &tkldb_version, sizeof(tkldb_version)));
+ W_SAFE(unrealdb_write_int32(db, TKLDB_MAGIC));
+ W_SAFE(unrealdb_write_int32(db, tkldb_version));
// Count the *-Lines
tklcount = 0;
@@ -292,7 +330,7 @@ int write_tkldb(void)
tklcount++;
}
}
- W_SAFE(write_data(fd, &tklcount, sizeof(tklcount)));
+ W_SAFE(unrealdb_write_int64(db, tklcount));
// Now write the actual *-Lines, first the ones in the hash table
for (index = 0; index < TKLIPHASHLEN1; index++)
@@ -303,7 +341,7 @@ int write_tkldb(void)
{
if (tkl->flags & TKL_FLAG_CONFIG)
continue; /* config entry */
- if (!write_tkline(fd, tmpfname, tkl)) // write_tkline() closes the fd on errors itself
+ if (!write_tkline(db, tmpfname, tkl)) // write_tkline() closes the db on errors itself
return 0;
}
}
@@ -315,13 +353,13 @@ int write_tkldb(void)
{
if (tkl->flags & TKL_FLAG_CONFIG)
continue; /* config entry */
- if (!write_tkline(fd, tmpfname, tkl))
+ if (!write_tkline(db, tmpfname, tkl))
return 0;
}
}
// Everything seems to have gone well, attempt to close and rename the tempfile
- if (fclose(fd) != 0)
+ if (!unrealdb_close(db))
{
WARN_WRITE_ERROR(tmpfname);
return 0;
@@ -344,18 +382,18 @@ int write_tkldb(void)
}
/** Write a TKL entry */
-int write_tkline(FILE *fd, const char *tmpfname, TKL *tkl)
+int write_tkline(UnrealDB *db, const char *tmpfname, TKL *tkl)
{
char tkltype;
char buf[256];
/* First, write the common attributes */
tkltype = tkl_typetochar(tkl->type);
- W_SAFE(write_data(fd, &tkltype, sizeof(tkltype))); // TKL char
+ W_SAFE(unrealdb_write_char(db, tkltype)); // TKL char
- W_SAFE(write_str(fd, tkl->set_by));
- W_SAFE(write_int64(fd, tkl->set_at));
- W_SAFE(write_int64(fd, tkl->expire_at));
+ W_SAFE(unrealdb_write_str(db, tkl->set_by));
+ W_SAFE(unrealdb_write_int64(db, tkl->set_at));
+ W_SAFE(unrealdb_write_int64(db, tkl->expire_at));
if (TKLIsServerBan(tkl))
{
@@ -365,9 +403,9 @@ int write_tkline(FILE *fd, const char *tmpfname, TKL *tkl)
snprintf(buf, sizeof(buf), "%%%s", tkl->ptr.serverban->usermask);
usermask = buf;
}
- W_SAFE(write_str(fd, usermask));
- W_SAFE(write_str(fd, tkl->ptr.serverban->hostmask));
- W_SAFE(write_str(fd, tkl->ptr.serverban->reason));
+ W_SAFE(unrealdb_write_str(db, usermask));
+ W_SAFE(unrealdb_write_str(db, tkl->ptr.serverban->hostmask));
+ W_SAFE(unrealdb_write_str(db, tkl->ptr.serverban->reason));
} else
if (TKLIsBanException(tkl))
{
@@ -377,17 +415,17 @@ int write_tkline(FILE *fd, const char *tmpfname, TKL *tkl)
snprintf(buf, sizeof(buf), "%%%s", tkl->ptr.banexception->usermask);
usermask = buf;
}
- W_SAFE(write_str(fd, usermask));
- W_SAFE(write_str(fd, tkl->ptr.banexception->hostmask));
- W_SAFE(write_str(fd, tkl->ptr.banexception->bantypes));
- W_SAFE(write_str(fd, tkl->ptr.banexception->reason));
+ W_SAFE(unrealdb_write_str(db, usermask));
+ W_SAFE(unrealdb_write_str(db, tkl->ptr.banexception->hostmask));
+ W_SAFE(unrealdb_write_str(db, tkl->ptr.banexception->bantypes));
+ W_SAFE(unrealdb_write_str(db, tkl->ptr.banexception->reason));
} else
if (TKLIsNameBan(tkl))
{
char *hold = tkl->ptr.nameban->hold ? "H" : "*";
- W_SAFE(write_str(fd, hold));
- W_SAFE(write_str(fd, tkl->ptr.nameban->name));
- W_SAFE(write_str(fd, tkl->ptr.nameban->reason));
+ W_SAFE(unrealdb_write_str(db, hold));
+ W_SAFE(unrealdb_write_str(db, tkl->ptr.nameban->name));
+ W_SAFE(unrealdb_write_str(db, tkl->ptr.nameban->reason));
} else
if (TKLIsSpamfilter(tkl))
{
@@ -395,12 +433,12 @@ int write_tkline(FILE *fd, const char *tmpfname, TKL *tkl)
char *target = spamfilter_target_inttostring(tkl->ptr.spamfilter->target);
char action = banact_valtochar(tkl->ptr.spamfilter->action);
- W_SAFE(write_str(fd, match_type));
- W_SAFE(write_str(fd, tkl->ptr.spamfilter->match->str));
- W_SAFE(write_str(fd, target));
- W_SAFE(write_data(fd, &action, sizeof(action)));
- W_SAFE(write_str(fd, tkl->ptr.spamfilter->tkl_reason));
- W_SAFE(write_int64(fd, tkl->ptr.spamfilter->tkl_duration));
+ W_SAFE(unrealdb_write_str(db, match_type));
+ W_SAFE(unrealdb_write_str(db, tkl->ptr.spamfilter->match->str));
+ W_SAFE(unrealdb_write_str(db, target));
+ W_SAFE(unrealdb_write_char(db, action));
+ W_SAFE(unrealdb_write_str(db, tkl->ptr.spamfilter->tkl_reason));
+ W_SAFE(unrealdb_write_int64(db, tkl->ptr.spamfilter->tkl_duration));
}
return 1;
@@ -409,7 +447,7 @@ int write_tkline(FILE *fd, const char *tmpfname, TKL *tkl)
/** Read all entries from the TKL db */
int read_tkldb(void)
{
- FILE *fd;
+ UnrealDB *db;
TKL *tkl = NULL;
uint32_t magic = 0;
uint32_t version;
@@ -426,49 +464,61 @@ int read_tkldb(void)
gettimeofday(&tv_alpha, NULL);
#endif
- fd = fopen(cfg.database, "rb");
- if (!fd)
+ db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, cfg.db_secret);
+ if (!db)
{
- if (errno == ENOENT)
+ if (unrealdb_get_error_code() == UNREALDB_ERROR_FILENOTFOUND)
{
/* Database does not exist. Could be first boot */
config_warn("[tkldb] No database present at '%s', will start a new one", cfg.database);
return 1;
- } else {
- config_warn("[tkldb] Unable to open database file '%s' for reading: %s", cfg.database, strerror(errno));
+ } else
+ if (unrealdb_get_error_code() == UNREALDB_ERROR_NOTCRYPTED)
+ {
+ /* Re-open as unencrypted */
+ db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, NULL);
+ if (!db)
+ {
+ /* This should actually never happen, unless some weird I/O error */
+ config_warn("[tkldb] Unable to open the database file '%s': %s", cfg.database, unrealdb_get_error_string());
+ return 0;
+ }
+ } else
+ {
+ config_warn("[tkldb] Unable to open the database file '%s' for reading: %s", cfg.database, unrealdb_get_error_string());
return 0;
}
}
/* The database starts with a "magic value" - unless it's some old version or corrupt */
- R_SAFE(read_data(fd, &magic, sizeof(magic)));
+ R_SAFE(unrealdb_read_int32(db, &magic));
if (magic != TKLDB_MAGIC)
{
config_warn("[tkldb] Database '%s' uses an old and unsupported format OR is corrupt", cfg.database);
config_status("If you are upgrading from UnrealIRCd 4 (or 5.0.0-alpha1) then we suggest you to "
"delete the existing database. Just keep at least 1 server linked during the upgrade "
"process to preserve your global *LINES and Spamfilters.");
- fclose(fd);
+ unrealdb_close(db);
return 0;
}
/* Now do a version check */
- R_SAFE(read_data(fd, &version, sizeof(version)));
+ R_SAFE(unrealdb_read_int32(db, &version));
if (version < 4999)
{
config_warn("[tkldb] Database '%s' uses an unsupport - possibly old - format (%ld).", cfg.database, (long)version);
- fclose(fd);
+ unrealdb_close(db);
return 0;
}
if (version > tkldb_version)
{
config_warn("[tkldb] Database '%s' has version %lu while we only support %lu. Did you just downgrade UnrealIRCd? Sorry this is not suported",
cfg.database, (unsigned long)tkldb_version, (unsigned long)version);
- fclose(fd);
+ unrealdb_close(db);
return 0;
}
- R_SAFE(read_data(fd, &tklcount, sizeof(tklcount)));
+ R_SAFE(unrealdb_read_int64(db, &tklcount));
for (cnt = 0; cnt < tklcount; cnt++)
{
@@ -477,7 +527,7 @@ int read_tkldb(void)
tkl = safe_alloc(sizeof(TKL));
/* First, fetch the TKL type.. */
- R_SAFE(read_data(fd, &c, sizeof(c)));
+ R_SAFE(unrealdb_read_char(db, &c));
tkl->type = tkl_chartotype(c);
if (!tkl->type)
{
@@ -493,10 +543,10 @@ int read_tkldb(void)
}
/* Read the common types (same for all TKLs) */
- R_SAFE(read_str(fd, &tkl->set_by));
- R_SAFE(read_int64(fd, &v));
+ R_SAFE(unrealdb_read_str(db, &tkl->set_by));
+ R_SAFE(unrealdb_read_int64(db, &v));
tkl->set_at = v;
- R_SAFE(read_int64(fd, &v));
+ R_SAFE(unrealdb_read_int64(db, &v));
tkl->expire_at = v;
/* Save some CPU... if it's already expired then don't bother adding */
@@ -513,7 +563,7 @@ int read_tkldb(void)
/* Usermask - but taking into account that the
* %-prefix means a soft ban.
*/
- R_SAFE(read_str(fd, &str));
+ R_SAFE(unrealdb_read_str(db, &str));
if (*str == '%')
{
softban = 1;
@@ -524,8 +574,8 @@ int read_tkldb(void)
safe_free(str);
/* And the other 2 fields.. */
- R_SAFE(read_str(fd, &tkl->ptr.serverban->hostmask));
- R_SAFE(read_str(fd, &tkl->ptr.serverban->reason));
+ R_SAFE(unrealdb_read_str(db, &tkl->ptr.serverban->hostmask));
+ R_SAFE(unrealdb_read_str(db, &tkl->ptr.serverban->reason));
if (find_tkl_serverban(tkl->type, tkl->ptr.serverban->usermask,
tkl->ptr.serverban->hostmask, softban))
@@ -551,7 +601,7 @@ int read_tkldb(void)
/* Usermask - but taking into account that the
* %-prefix means a soft ban.
*/
- R_SAFE(read_str(fd, &str));
+ R_SAFE(unrealdb_read_str(db, &str));
if (*str == '%')
{
softban = 1;
@@ -562,9 +612,9 @@ int read_tkldb(void)
safe_free(str);
/* And the other 3 fields.. */
- R_SAFE(read_str(fd, &tkl->ptr.banexception->hostmask));
- R_SAFE(read_str(fd, &tkl->ptr.banexception->bantypes));
- R_SAFE(read_str(fd, &tkl->ptr.banexception->reason));
+ R_SAFE(unrealdb_read_str(db, &tkl->ptr.banexception->hostmask));
+ R_SAFE(unrealdb_read_str(db, &tkl->ptr.banexception->bantypes));
+ R_SAFE(unrealdb_read_str(db, &tkl->ptr.banexception->reason));
if (find_tkl_banexception(tkl->type, tkl->ptr.banexception->usermask,
tkl->ptr.banexception->hostmask, softban))
@@ -587,12 +637,12 @@ int read_tkldb(void)
{
tkl->ptr.nameban = safe_alloc(sizeof(NameBan));
- R_SAFE(read_str(fd, &str));
+ R_SAFE(unrealdb_read_str(db, &str));
if (*str == 'H')
tkl->ptr.nameban->hold = 1;
safe_free(str);
- R_SAFE(read_str(fd, &tkl->ptr.nameban->name));
- R_SAFE(read_str(fd, &tkl->ptr.nameban->reason));
+ R_SAFE(unrealdb_read_str(db, &tkl->ptr.nameban->name));
+ R_SAFE(unrealdb_read_str(db, &tkl->ptr.nameban->reason));
if (find_tkl_nameban(tkl->type, tkl->ptr.nameban->name,
tkl->ptr.nameban->hold))
@@ -617,7 +667,7 @@ int read_tkldb(void)
tkl->ptr.spamfilter = safe_alloc(sizeof(Spamfilter));
/* Match method */
- R_SAFE(read_str(fd, &str));
+ R_SAFE(unrealdb_read_str(db, &str));
match_method = unreal_match_method_strtoval(str);
if (!match_method)
{
@@ -627,7 +677,7 @@ int read_tkldb(void)
safe_free(str);
/* Match string (eg: regex) */
- R_SAFE(read_str(fd, &str));
+ R_SAFE(unrealdb_read_str(db, &str));
tkl->ptr.spamfilter->match = unreal_create_match(match_method, str, &err);
if (!tkl->ptr.spamfilter->match)
{
@@ -637,7 +687,7 @@ int read_tkldb(void)
safe_free(str);
/* Target (eg: cpn) */
- R_SAFE(read_str(fd, &str));
+ R_SAFE(unrealdb_read_str(db, &str));
tkl->ptr.spamfilter->target = spamfilter_gettargets(str, NULL);
if (!tkl->ptr.spamfilter->target)
{
@@ -648,7 +698,7 @@ int read_tkldb(void)
safe_free(str);
/* Action */
- R_SAFE(read_data(fd, &c, sizeof(c)));
+ R_SAFE(unrealdb_read_char(db, &c));
tkl->ptr.spamfilter->action = banact_chartoval(c);
if (!tkl->ptr.spamfilter->action)
{
@@ -657,8 +707,8 @@ int read_tkldb(void)
do_not_add = 1;
}
- R_SAFE(read_str(fd, &tkl->ptr.spamfilter->tkl_reason));
- R_SAFE(read_int64(fd, &v));
+ R_SAFE(unrealdb_read_str(db, &tkl->ptr.spamfilter->tkl_reason));
+ R_SAFE(unrealdb_read_int64(db, &v));
tkl->ptr.spamfilter->tkl_duration = v;
if (find_tkl_spamfilter(tkl->type, tkl->ptr.spamfilter->match->str,
@@ -695,10 +745,7 @@ int read_tkldb(void)
FreeTKLRead();
}
- /* If everything went fine, then reading a single byte should cause an EOF error */
- if (fread(&c, 1, 1, fd) == 1)
- config_warn("[tkldb] Database invalid. Extra data found at end of DB file.");
- fclose(fd);
+ unrealdb_close(db);
if (added_cnt)
sendto_realops_and_log("[tkldb] Re-added %d *-Lines", added_cnt);
diff --git a/src/modules/topic.c b/src/modules/topic.c
@@ -242,7 +242,7 @@ CMD_FUNC(cmd_topic)
Hook *tmphook;
int n;
- if (match_spamfilter(client, topic, SPAMF_TOPIC, channel->chname, 0, NULL))
+ if (match_spamfilter(client, topic, SPAMF_TOPIC, "TOPIC", channel->chname, 0, NULL))
return;
for (tmphook = Hooks[HOOKTYPE_PRE_LOCAL_TOPIC]; tmphook; tmphook = tmphook->next) {
diff --git a/src/modules/websocket.c b/src/modules/websocket.c
@@ -26,6 +26,11 @@ ModuleHeader MOD_HEADER
#define WEBSOCKET_SEND_BUFFER_SIZE 16384
#endif
+typedef enum WebSocketType {
+ WEBSOCKET_TYPE_BINARY = 1,
+ WEBSOCKET_TYPE_TEXT = 2
+} WebSocketType;
+
typedef struct WebSocketUser WebSocketUser;
struct WebSocketUser {
char get; /**< GET initiated */
@@ -33,14 +38,14 @@ struct WebSocketUser {
char *handshake_key; /**< Handshake key (used during handshake) */
char *lefttoparse; /**< Leftover buffer to parse */
int lefttoparselen; /**< Length of lefttoparse buffer */
+ WebSocketType type; /**< WEBSOCKET_TYPE_BINARY or WEBSOCKET_TYPE_TEXT */
+ char *sec_websocket_protocol; /**< Only valid during parsing of the request, after that it is NULL again */
};
-#define WEBSOCKET_TYPE_BINARY 0x1
-#define WEBSOCKET_TYPE_TEXT 0x2
-
#define WSU(client) ((WebSocketUser *)moddata_client(client, websocket_md).ptr)
-#define WEBSOCKET_TYPE(client) ((client->local && client->local->listener) ? client->local->listener->websocket_options : 0)
+#define WEBSOCKET_PORT(client) ((client->local && client->local->listener) ? client->local->listener->websocket_options : 0)
+#define WEBSOCKET_TYPE(client) (WSU(client)->type)
#define WEBSOCKET_MAGIC_KEY "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" /* see RFC6455 */
@@ -59,7 +64,7 @@ int websocket_packet_in(Client *client, char *readbuf, int *length);
void websocket_mdata_free(ModData *m);
int websocket_handle_packet(Client *client, char *readbuf, int length);
int websocket_handle_handshake(Client *client, char *readbuf, int *length);
-int websocket_complete_handshake(Client *client);
+int websocket_handshake_send_response(Client *client);
int websocket_handle_packet_ping(Client *client, char *buf, int len);
int websocket_handle_packet_pong(Client *client, char *buf, int len);
int websocket_create_packet(int opcode, char **buf, int *len);
@@ -67,6 +72,7 @@ int websocket_send_pong(Client *client, char *buf, int len);
/* Global variables */
ModDataInfo *websocket_md;
+static int ws_text_mode_available = 1;
MOD_TEST()
{
@@ -98,6 +104,8 @@ MOD_INIT()
MOD_LOAD()
{
+ if (non_utf8_nick_chars_in_use || (iConf.allowed_channelchars == ALLOWED_CHANNELCHARS_ANY))
+ ws_text_mode_available = 0;
return MOD_SUCCESS;
}
@@ -302,11 +310,12 @@ int websocket_handle_websocket(Client *client, char *readbuf2, int length2)
*/
int websocket_packet_in(Client *client, char *readbuf, int *length)
{
- if ((client->local->receiveM == 0) && WEBSOCKET_TYPE(client) && !WSU(client) && (*length > 8) && !strncmp(readbuf, "GET ", 4))
+ if ((client->local->receiveM == 0) && WEBSOCKET_PORT(client) && !WSU(client) && (*length > 8) && !strncmp(readbuf, "GET ", 4))
{
/* Allocate a new WebSocketUser struct for this session */
moddata_client(client, websocket_md).ptr = safe_alloc(sizeof(WebSocketUser));
WSU(client)->get = 1;
+ WSU(client)->type = client->local->listener->websocket_options; /* the default, unless the client chooses otherwise */
}
if (!WSU(client))
@@ -452,6 +461,58 @@ int websocket_handshake_helper(char *buffer, int len, char **key, char **value,
return 0;
}
+/** Finally, validate the websocket request (handshake) and proceed or reject. */
+int websocket_handshake_valid(Client *client)
+{
+ if (!WSU(client)->handshake_key)
+ {
+ if (is_module_loaded("webredir"))
+ {
+ char *parx[2] = { NULL, NULL };
+ do_cmd(client, NULL, "GET", 1, parx);
+ }
+ dead_socket(client, "Invalid WebSocket request");
+ return 0;
+ }
+ if (WSU(client)->sec_websocket_protocol)
+ {
+ char *p = NULL, *name;
+ int negotiated = 0;
+
+ for (name = strtoken(&p, WSU(client)->sec_websocket_protocol, ",");
+ name;
+ name = strtoken(&p, NULL, ","))
+ {
+ skip_whitespace(&name);
+ if (!strcmp(name, "binary.ircv3.net"))
+ {
+ negotiated = WEBSOCKET_TYPE_BINARY;
+ break; /* First hit wins */
+ } else
+ if (!strcmp(name, "text.ircv3.net") && ws_text_mode_available)
+ {
+ negotiated = WEBSOCKET_TYPE_TEXT;
+ break; /* First hit wins */
+ }
+ }
+ if (negotiated == WEBSOCKET_TYPE_BINARY)
+ {
+ WSU(client)->type = WEBSOCKET_TYPE_BINARY;
+ safe_strdup(WSU(client)->sec_websocket_protocol, "binary.ircv3.net");
+ } else
+ if (negotiated == WEBSOCKET_TYPE_TEXT)
+ {
+ WSU(client)->type = WEBSOCKET_TYPE_TEXT;
+ safe_strdup(WSU(client)->sec_websocket_protocol, "text.ircv3.net");
+ } else
+ {
+ /* Negotiation failed, fallback to the default (don't set it here) */
+ safe_free(WSU(client)->sec_websocket_protocol);
+ }
+ }
+ return 1;
+}
+
/** Handle client GET WebSocket handshake.
* Yes, I'm going to assume that the header fits in one packet and one packet only.
*/
@@ -500,22 +561,19 @@ int websocket_handle_handshake(Client *client, char *readbuf, int *length)
return -1;
}
safe_strdup(WSU(client)->handshake_key, value);
+ } else
+ if (!strcasecmp(key, "Sec-WebSocket-Protocol"))
+ {
+ /* Save it here, will be processed later */
+ safe_strdup(WSU(client)->sec_websocket_protocol, value);
}
}
if (end_of_request)
{
- if (!WSU(client)->handshake_key)
- {
- if (is_module_loaded("webredir"))
- {
- char *parx[2] = { NULL, NULL };
- do_cmd(client, NULL, "GET", 1, parx);
- }
- dead_socket(client, "Invalid WebSocket request");
+ if (!websocket_handshake_valid(client))
return -1;
- }
- websocket_complete_handshake(client);
+ websocket_handshake_send_response(client);
return 0;
}
@@ -528,7 +586,7 @@ int websocket_handle_handshake(Client *client, char *readbuf, int *length)
}
/** Complete the handshake by sending the appropriate HTTP 101 response etc. */
-int websocket_complete_handshake(Client *client)
+int websocket_handshake_send_response(Client *client)
{
char buf[512], hashbuf[64];
SHA_CTX hash;
@@ -547,10 +605,21 @@ int websocket_complete_handshake(Client *client)
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
- "Sec-WebSocket-Accept: %s\r\n"
- "\r\n",
+ "Sec-WebSocket-Accept: %s\r\n",
hashbuf);
+ if (WSU(client)->sec_websocket_protocol)
+ {
+ /* using strlen() is safe here since above buffer will not
+ * cause it to be >=512 and thus we won't get into negatives.
+ */
+ snprintf(buf+strlen(buf), sizeof(buf)-strlen(buf),
+ "Sec-WebSocket-Protocol: %s\r\n",
+ WSU(client)->sec_websocket_protocol);
+ }
+
+ strlcat(buf, "\r\n", sizeof(buf));
+
/* Caution: we bypass sendQ flood checking by doing it this way.
* Risk is minimal, though, as we only permit limited text only
* once per session.
diff --git a/src/modules/whois.c b/src/modules/whois.c
@@ -103,15 +103,8 @@ CMD_FUNC(cmd_whois)
if (wilds)
continue;
- if ((target = find_client(nick, NULL)))
+ if ((target = find_person(nick, NULL)))
{
- if (IsServer(target))
- continue;
- /*
- * I'm always last :-) and target->next == NULL!!
- */
- if (IsMe(target))
- break;
/*
* 'Rules' established for sending a WHOIS reply:
* - only send replies about common or public channels
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |