diff --git a/.dockerignore b/.dockerignore index 38a675d9..1052ab9d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,4 @@ **/.git **/.sw? +**/*.o +**/*.a diff --git a/Dockerfile b/Dockerfile index 217a24e8..39660dba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,14 +4,14 @@ ENV GID 1234 ENV UID 1234 RUN DEBIAN_FRONTEND=noninteractive RUNLEVEL=1 apt-get update -RUN DEBIAN_FRONTEND=noninteractive RUNLEVEL=1 apt-get -y install build-essential libssl-dev autoconf automake flex libpcre3-dev byacc gawk git vim procps net-tools iputils-ping bind9-host +RUN DEBIAN_FRONTEND=noninteractive RUNLEVEL=1 apt-get -y install build-essential libssl-dev autoconf automake flex libpcre3-dev byacc gawk git vim procps net-tools iputils-ping bind9-host liblmdb-dev libzstd-dev libcmocka-dev valgrind #libgeoip-dev libmaxminddb-dev # Perl dependencies for iauthd.pl (commented out - using TypeScript version) #RUN DEBIAN_FRONTEND=noninteractive apt-get -y install libpoe-perl libpoe-component-client-dns-perl libterm-readkey-perl libfile-slurp-perl libtime-duration-perl # Node.js for iauthd-ts -RUN DEBIAN_FRONTEND=noninteractive apt-get -y install nodejs npm +RUN DEBIAN_FRONTEND=noninteractive apt-get -y install nodejs npm RUN mkdir -p /home/nefarious/nefarious2 RUN mkdir -p /home/nefarious/ircd @@ -20,6 +20,9 @@ COPY . /home/nefarious/nefarious2 RUN groupadd -g ${GID} nefarious RUN useradd -u ${UID} -g ${GID} nefarious +# Create LMDB directories for chathistory and metadata storage +# Create cores directory for valgrind logs +RUN mkdir -p /home/nefarious/ircd/history /home/nefarious/ircd/metadata /home/nefarious/ircd/cores RUN chown -R nefarious:nefarious /home/nefarious USER nefarious @@ -31,8 +34,12 @@ WORKDIR /home/nefarious/nefarious2 # I cant get the maxminddb library to compile in at all in debian 12, give up on geoip for now # --with-geoip=/usr --with-mmdb=/usr \ -RUN ./configure --libdir=/home/nefarious/ircd --enable-debug --with-maxcon=4096 +# Enable LMDB for chathistory and zstd for compression +RUN ./configure --libdir=/home/nefarious/ircd --enable-debug --with-maxcon=4096 --with-lmdb=/usr --with-zstd=/usr RUN make +# Run unit tests during build (they require the built object files) +RUN make test +# make install runs an interactive SSL generator - pre-create pem to skip, then remove so entrypoint generates fresh one RUN touch /home/nefarious/ircd/ircd.pem && make install && rm /home/nefarious/ircd/ircd.pem # Build iauthd-ts @@ -53,8 +60,7 @@ RUN ln -sf /dev/stdout /home/nefarious/ircd/ircd.log USER root #Clean up build RUN rm -rf /home/nefarious/nefarious2 -RUN apt-get remove -y build-essential && apt-get autoremove -y -RUN apt-get clean +RUN apt-get remove -y build-essential && apt-get autoremove -y && apt-get clean USER nefarious @@ -71,9 +77,11 @@ COPY tools/docker/base.conf-dist /home/nefarious/ircd/base.conf-dist COPY tools/docker/ircd.conf /home/nefarious/ircd/ircd.conf COPY tools/docker/linesync.conf /home/nefarious/ircd/linesync.conf +# Run entrypoint (volume permissions fixed by init container in docker-compose) ENTRYPOINT ["/home/nefarious/dockerentrypoint.sh"] -CMD ["/home/nefarious/bin/ircd", "-n", "-x", "5", "-f", "ircd-docker.conf"] +# Run with Valgrind for memory testing (logs to cores mount for easy access) +CMD ["valgrind", "--leak-check=full", "--show-leak-kinds=all", "--track-origins=yes", "--log-file=/home/nefarious/ircd/cores/valgrind.log", "/home/nefarious/bin/ircd", "-n", "-x", "5", "-f", "ircd-docker.conf"] diff --git a/Makefile.in b/Makefile.in index c91f2770..bfb62b2b 100644 --- a/Makefile.in +++ b/Makefile.in @@ -33,14 +33,14 @@ IRCD_MAKEFILES = Makefile ircd/Makefile ircd/test/Makefile all: build -.PHONY: server build depend install config update diff patch export update +.PHONY: server build depend install config update diff patch export update test # Some versions of make give a warning when this is empty: .SUFFIXES: .dummy build: ${IRCD_MAKEFILES} @for i in ${SUBDIRS}; do \ echo "Building $$i..."; \ - cd $$i; ${MAKE} build; cd ..; \ + cd $$i && ${MAKE} build || exit 1; cd ..; \ done config: @@ -98,20 +98,24 @@ maintainer-clean: root-distclean ${IRCD_MAKEFILES} depend: ${IRCD_MAKEFILES} @for i in ${SUBDIRS}; do \ echo "Making dependencies in $$i..."; \ - cd $$i; ${MAKE} depend; cd ..; \ + cd $$i && ${MAKE} depend || exit 1; cd ..; \ done +test: ${IRCD_MAKEFILES} + @echo "Running unit tests..." + cd ircd/test && ${MAKE} test + install: ${IRCD_MAKEFILES} test -d ${prefix} || mkdir ${prefix} @for i in ${SUBDIRS}; do \ echo "Installing $$i..."; \ - cd $$i; ${MAKE} install; cd ..; \ + cd $$i && ${MAKE} install || exit 1; cd ..; \ done uninstall: ${IRCD_MAKEFILES} @for i in ${SUBDIRS}; do \ echo "Uninstalling $$i..."; \ - cd $$i; ${MAKE} uninstall; cd ..; \ + cd $$i && ${MAKE} uninstall || exit 1; cd ..; \ done ${srcdir}/aclocal.m4: acinclude.m4 diff --git a/config.h.in b/config.h.in index 6c4b04ce..d7598734 100644 --- a/config.h.in +++ b/config.h.in @@ -33,27 +33,24 @@ /* Define to 1 if you have the header file. */ #undef HAVE_CRYPT_H -/* Define to 1 if you have the `getrusage' function. */ +/* Define to 1 if you have the 'getrusage' function. */ #undef HAVE_GETRUSAGE /* Define to 1 if you have the header file. */ #undef HAVE_INTTYPES_H -/* Define to 1 if you have the `kqueue' function. */ +/* Define to 1 if you have the 'kqueue' function. */ #undef HAVE_KQUEUE -/* Define to 1 if you have the `nsl' library (-lnsl). */ +/* Define to 1 if you have the 'nsl' library (-lnsl). */ #undef HAVE_LIBNSL -/* Define to 1 if you have the `resolv' library (-lresolv). */ +/* Define to 1 if you have the 'resolv' library (-lresolv). */ #undef HAVE_LIBRESOLV -/* Define to 1 if you have the `socket' library (-lsocket). */ +/* Define to 1 if you have the 'socket' library (-lsocket). */ #undef HAVE_LIBSOCKET -/* Define to 1 if you have the header file. */ -#undef HAVE_MEMORY_H - /* Define to 1 if you have the header file. */ #undef HAVE_POLL_H @@ -61,12 +58,15 @@ signal. */ #undef HAVE_RESTARTABLE_SYSCALLS -/* Define to 1 if you have the `setrlimit' function. */ +/* Define to 1 if you have the 'setrlimit' function. */ #undef HAVE_SETRLIMIT /* Define to 1 if you have the header file. */ #undef HAVE_STDINT_H +/* Define to 1 if you have the header file. */ +#undef HAVE_STDIO_H + /* Define to 1 if you have the header file. */ #undef HAVE_STDLIB_H @@ -97,13 +97,16 @@ /* Define to 1 if you have the header file. */ #undef HAVE_SYS_STAT_H +/* Define to 1 if you have the header file. */ +#undef HAVE_SYS_TIME_H + /* Define to 1 if you have the header file. */ #undef HAVE_SYS_TYPES_H /* Define to 1 if you have that is POSIX.1 compatible. */ #undef HAVE_SYS_WAIT_H -/* Define to 1 if you have the `times' function. */ +/* Define to 1 if you have the 'times' function. */ #undef HAVE_TIMES /* Define to 1 if you have the header file. */ @@ -169,22 +172,22 @@ /* Define if you have POSIX signals. */ #undef POSIX_SIGNALS -/* The size of `int', as computed by sizeof. */ +/* The size of 'int', as computed by sizeof. */ #undef SIZEOF_INT -/* The size of `int64_t', as computed by sizeof. */ +/* The size of 'int64_t', as computed by sizeof. */ #undef SIZEOF_INT64_T -/* The size of `long', as computed by sizeof. */ +/* The size of 'long', as computed by sizeof. */ #undef SIZEOF_LONG -/* The size of `long long', as computed by sizeof. */ +/* The size of 'long long', as computed by sizeof. */ #undef SIZEOF_LONG_LONG -/* The size of `short', as computed by sizeof. */ +/* The size of 'short', as computed by sizeof. */ #undef SIZEOF_SHORT -/* The size of `void *', as computed by sizeof. */ +/* The size of 'void *', as computed by sizeof. */ #undef SIZEOF_VOID_P /* Path to executable for restarts */ @@ -196,16 +199,19 @@ /* Path name used as a base for the ssl lib files. */ #undef SSL_LIBS_PATH -/* Define to 1 if you have the ANSI C header files. */ +/* Define to 1 if all of the C89 standard headers exist (not just the ones + required in a freestanding environment). This macro is provided for + backward compatibility; new code need not use it. */ #undef STDC_HEADERS /* Define if you have (unreliable) SysV signals. */ #undef SYSV_UNRELIABLE_SIGNALS -/* Define to 1 if you can safely include both and . */ +/* Define to 1 if you can safely include both and . This + macro is obsolete. */ #undef TIME_WITH_SYS_TIME -/* Define to 1 if your declares `struct tm'. */ +/* Define to 1 if your declares 'struct tm'. */ #undef TM_IN_SYS_TIME /* Define to enable the /dev/poll engine */ @@ -223,6 +229,9 @@ /* Define to enable the kqueue engine */ #undef USE_KQUEUE +/* Define if you are using LMDB for chathistory */ +#undef USE_LMDB + /* Define if you are using MaxMindDB */ #undef USE_MMDB @@ -232,6 +241,9 @@ /* Define if you are using OpenSSL */ #undef USE_SSL +/* Define if you are using zstd compression */ +#undef USE_ZSTD + /* 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 @@ -244,36 +256,36 @@ # endif #endif -/* Define to 1 if `lex' declares `yytext' as a `char *' by default, not a - `char[]'. */ +/* Define to 1 if 'lex' declares 'yytext' as a 'char *' by default, not a + 'char[]'. */ #undef YYTEXT_POINTER -/* Define to `int' if doesn't define. */ +/* Define as 'int' if doesn't define. */ #undef gid_t -/* Define to `short' if does not define. */ +/* Define to 'short' if does not define. */ #undef int16_t -/* Define to `long' if does not define. */ +/* Define to 'long' if does not define. */ #undef int32_t -/* Define to `long long' if does not define. */ +/* Define to 'long long' if does not define. */ #undef int64_t -/* Define to `unsigned int' if does not define. */ +/* Define as 'unsigned int' if doesn't define. */ #undef size_t /* type to use in place of socklen_t if not defined */ #undef socklen_t -/* Define to `int' if doesn't define. */ +/* Define as 'int' if doesn't define. */ #undef uid_t -/* Define to `unsigned short' if does not define. */ +/* Define to 'unsigned short' if does not define. */ #undef uint16_t -/* Define to `unsigned long' if does not define. */ +/* Define to 'unsigned long' if does not define. */ #undef uint32_t -/* Define to `unsigned long long' if does not define. */ +/* Define to 'unsigned long long' if does not define. */ #undef uint64_t diff --git a/configure b/configure index 3b6da6a8..2d4ce5e7 100755 --- a/configure +++ b/configure @@ -1,9 +1,10 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.69. +# Generated by GNU Autoconf 2.72. # # -# Copyright (C) 1992-1996, 1998-2012 Free Software Foundation, Inc. +# Copyright (C) 1992-1996, 1998-2017, 2020-2023 Free Software Foundation, +# Inc. # # # This configure script is free software; the Free Software Foundation @@ -14,63 +15,65 @@ # Be more Bourne compatible DUALCASE=1; export DUALCASE # for MKS sh -if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then : +if test ${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1 +then : emulate sh NULLCMD=: # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which # is contrary to our usage. Disable this feature. alias -g '${1+"$@"}'='"$@"' setopt NO_GLOB_SUBST -else - case `(set -o) 2>/dev/null` in #( +else case e in #( + e) case `(set -o) 2>/dev/null` in #( *posix*) : set -o posix ;; #( *) : ;; +esac ;; esac fi + +# Reset variables that may have inherited troublesome values from +# the environment. + +# IFS needs to be set, to space, tab, and newline, in precisely that order. +# (If _AS_PATH_WALK were called with IFS unset, it would have the +# side effect of setting IFS to empty, thus disabling word splitting.) +# Quoting is to prevent editors from complaining about space-tab. as_nl=' ' export as_nl -# Printing a long string crashes Solaris 7 /usr/bin/printf. -as_echo='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\' -as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo -as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo$as_echo -# Prefer a ksh shell builtin over an external printf program on Solaris, -# but without wasting forks for bash or zsh. -if test -z "$BASH_VERSION$ZSH_VERSION" \ - && (test "X`print -r -- $as_echo`" = "X$as_echo") 2>/dev/null; then - as_echo='print -r --' - as_echo_n='print -rn --' -elif (test "X`printf %s $as_echo`" = "X$as_echo") 2>/dev/null; then - as_echo='printf %s\n' - as_echo_n='printf %s' -else - if test "X`(/usr/ucb/echo -n -n $as_echo) 2>/dev/null`" = "X-n $as_echo"; then - as_echo_body='eval /usr/ucb/echo -n "$1$as_nl"' - as_echo_n='/usr/ucb/echo -n' - else - as_echo_body='eval expr "X$1" : "X\\(.*\\)"' - as_echo_n_body='eval - arg=$1; - case $arg in #( - *"$as_nl"*) - expr "X$arg" : "X\\(.*\\)$as_nl"; - arg=`expr "X$arg" : ".*$as_nl\\(.*\\)"`;; - esac; - expr "X$arg" : "X\\(.*\\)" | tr -d "$as_nl" - ' - export as_echo_n_body - as_echo_n='sh -c $as_echo_n_body as_echo' - fi - export as_echo_body - as_echo='sh -c $as_echo_body as_echo' -fi +IFS=" "" $as_nl" + +PS1='$ ' +PS2='> ' +PS4='+ ' + +# Ensure predictable behavior from utilities with locale-dependent output. +LC_ALL=C +export LC_ALL +LANGUAGE=C +export LANGUAGE + +# We cannot yet rely on "unset" to work, but we need these variables +# to be unset--not just set to an empty or harmless value--now, to +# avoid bugs in old shells (e.g. pre-3.0 UWIN ksh). This construct +# also avoids known problems related to "unset" and subshell syntax +# in other old shells (e.g. bash 2.01 and pdksh 5.2.14). +for as_var in BASH_ENV ENV MAIL MAILPATH CDPATH +do eval test \${$as_var+y} \ + && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || : +done + +# Ensure that fds 0, 1, and 2 are open. +if (exec 3>&0) 2>/dev/null; then :; else exec 0&1) 2>/dev/null; then :; else exec 1>/dev/null; fi +if (exec 3>&2) ; then :; else exec 2>/dev/null; fi # The user is always right. -if test "${PATH_SEPARATOR+set}" != set; then +if ${PATH_SEPARATOR+false} :; then PATH_SEPARATOR=: (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && { (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 || @@ -79,13 +82,6 @@ if test "${PATH_SEPARATOR+set}" != set; then fi -# IFS -# We need space, tab and new line, in precisely that order. Quoting is -# there to prevent editors from complaining about space-tab. -# (If _AS_PATH_WALK were called with IFS unset, it would disable word -# splitting by setting IFS to empty value.) -IFS=" "" $as_nl" - # Find who we are. Look in the path if we contain no directory separator. as_myself= case $0 in #(( @@ -94,43 +90,27 @@ case $0 in #(( for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. - test -r "$as_dir/$0" && as_myself=$as_dir/$0 && break + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + test -r "$as_dir$0" && as_myself=$as_dir$0 && break done IFS=$as_save_IFS ;; esac -# We did not find ourselves, most probably we were run as `sh COMMAND' +# We did not find ourselves, most probably we were run as 'sh COMMAND' # in which case we are not to be found in the path. if test "x$as_myself" = x; then as_myself=$0 fi if test ! -f "$as_myself"; then - $as_echo "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 + printf "%s\n" "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 exit 1 fi -# Unset variables that we do not need and which cause bugs (e.g. in -# pre-3.0 UWIN ksh). But do not cause bugs in bash 2.01; the "|| exit 1" -# suppresses any "Segmentation fault" message there. '((' could -# trigger a bug in pdksh 5.2.14. -for as_var in BASH_ENV ENV MAIL MAILPATH -do eval test x\${$as_var+set} = xset \ - && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || : -done -PS1='$ ' -PS2='> ' -PS4='+ ' - -# NLS nuisances. -LC_ALL=C -export LC_ALL -LANGUAGE=C -export LANGUAGE - -# CDPATH. -(unset CDPATH) >/dev/null 2>&1 && unset CDPATH # Use a proper internal environment variable to ensure we don't fall # into an infinite loop, continuously re-executing ourselves. @@ -151,26 +131,28 @@ case $- in # (((( esac exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} # Admittedly, this is quite paranoid, since all the known shells bail -# out after a failed `exec'. -$as_echo "$0: could not re-execute with $CONFIG_SHELL" >&2 -as_fn_exit 255 +# out after a failed 'exec'. +printf "%s\n" "$0: could not re-execute with $CONFIG_SHELL" >&2 +exit 255 fi # We don't want this to propagate to other subprocesses. { _as_can_reexec=; unset _as_can_reexec;} if test "x$CONFIG_SHELL" = x; then - as_bourne_compatible="if test -n \"\${ZSH_VERSION+set}\" && (emulate sh) >/dev/null 2>&1; then : + as_bourne_compatible="if test \${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1 +then : emulate sh NULLCMD=: # Pre-4.2 versions of Zsh do word splitting on \${1+\"\$@\"}, which # is contrary to our usage. Disable this feature. alias -g '\${1+\"\$@\"}'='\"\$@\"' setopt NO_GLOB_SUBST -else - case \`(set -o) 2>/dev/null\` in #( +else case e in #( + e) case \`(set -o) 2>/dev/null\` in #( *posix*) : set -o posix ;; #( *) : ;; +esac ;; esac fi " @@ -185,42 +167,55 @@ as_fn_success || { exitcode=1; echo as_fn_success failed.; } as_fn_failure && { exitcode=1; echo as_fn_failure succeeded.; } as_fn_ret_success || { exitcode=1; echo as_fn_ret_success failed.; } as_fn_ret_failure && { exitcode=1; echo as_fn_ret_failure succeeded.; } -if ( set x; as_fn_ret_success y && test x = \"\$1\" ); then : +if ( set x; as_fn_ret_success y && test x = \"\$1\" ) +then : -else - exitcode=1; echo positional parameters were not saved. +else case e in #( + e) exitcode=1; echo positional parameters were not saved. ;; +esac fi test x\$exitcode = x0 || exit 1 +blah=\$(echo \$(echo blah)) +test x\"\$blah\" = xblah || exit 1 test -x / || exit 1" as_suggested=" as_lineno_1=";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested" as_lineno_1a=\$LINENO as_lineno_2=";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested" as_lineno_2a=\$LINENO eval 'test \"x\$as_lineno_1'\$as_run'\" != \"x\$as_lineno_2'\$as_run'\" && test \"x\`expr \$as_lineno_1'\$as_run' + 1\`\" = \"x\$as_lineno_2'\$as_run'\"' || exit 1 test \$(( 1 + 1 )) = 2 || exit 1" - if (eval "$as_required") 2>/dev/null; then : + if (eval "$as_required") 2>/dev/null +then : as_have_required=yes -else - as_have_required=no +else case e in #( + e) as_have_required=no ;; +esac fi - if test x$as_have_required = xyes && (eval "$as_suggested") 2>/dev/null; then : + if test x$as_have_required = xyes && (eval "$as_suggested") 2>/dev/null +then : -else - as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +else case e in #( + e) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR as_found=false for as_dir in /bin$PATH_SEPARATOR/usr/bin$PATH_SEPARATOR$PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac as_found=: case $as_dir in #( /*) for as_base in sh bash ksh sh5; do # Try only shells that exist, to save several forks. - as_shell=$as_dir/$as_base + as_shell=$as_dir$as_base if { test -f "$as_shell" || test -f "$as_shell.exe"; } && - { $as_echo "$as_bourne_compatible""$as_required" | as_run=a "$as_shell"; } 2>/dev/null; then : + as_run=a "$as_shell" -c "$as_bourne_compatible""$as_required" 2>/dev/null +then : CONFIG_SHELL=$as_shell as_have_required=yes - if { $as_echo "$as_bourne_compatible""$as_suggested" | as_run=a "$as_shell"; } 2>/dev/null; then : + if as_run=a "$as_shell" -c "$as_bourne_compatible""$as_suggested" 2>/dev/null +then : break 2 fi fi @@ -228,14 +223,22 @@ fi esac as_found=false done -$as_found || { if { test -f "$SHELL" || test -f "$SHELL.exe"; } && - { $as_echo "$as_bourne_compatible""$as_required" | as_run=a "$SHELL"; } 2>/dev/null; then : - CONFIG_SHELL=$SHELL as_have_required=yes -fi; } IFS=$as_save_IFS +if $as_found +then : + +else case e in #( + e) if { test -f "$SHELL" || test -f "$SHELL.exe"; } && + as_run=a "$SHELL" -c "$as_bourne_compatible""$as_required" 2>/dev/null +then : + CONFIG_SHELL=$SHELL as_have_required=yes +fi ;; +esac +fi - if test "x$CONFIG_SHELL" != x; then : + if test "x$CONFIG_SHELL" != x +then : export CONFIG_SHELL # We cannot yet assume a decent shell, so we have to provide a # neutralization value for shells without unset; and this also @@ -252,25 +255,27 @@ case $- in # (((( esac exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} # Admittedly, this is quite paranoid, since all the known shells bail -# out after a failed `exec'. -$as_echo "$0: could not re-execute with $CONFIG_SHELL" >&2 +# out after a failed 'exec'. +printf "%s\n" "$0: could not re-execute with $CONFIG_SHELL" >&2 exit 255 fi - if test x$as_have_required = xno; then : - $as_echo "$0: This script requires a shell more modern than all" - $as_echo "$0: the shells that I found on your system." - if test x${ZSH_VERSION+set} = xset ; then - $as_echo "$0: In particular, zsh $ZSH_VERSION has bugs and should" - $as_echo "$0: be upgraded to zsh 4.3.4 or later." + if test x$as_have_required = xno +then : + printf "%s\n" "$0: This script requires a shell more modern than all" + printf "%s\n" "$0: the shells that I found on your system." + if test ${ZSH_VERSION+y} ; then + printf "%s\n" "$0: In particular, zsh $ZSH_VERSION has bugs and should" + printf "%s\n" "$0: be upgraded to zsh 4.3.4 or later." else - $as_echo "$0: Please tell bug-autoconf@gnu.org about your system, + printf "%s\n" "$0: Please tell bug-autoconf@gnu.org about your system, $0: including any error possibly output before this $0: message. Then install a modern shell, or manually run $0: the script under such a shell if you do have one." fi exit 1 -fi +fi ;; +esac fi fi SHELL=${CONFIG_SHELL-/bin/sh} @@ -291,6 +296,7 @@ as_fn_unset () } as_unset=as_fn_unset + # as_fn_set_status STATUS # ----------------------- # Set $? to STATUS, without forking. @@ -322,7 +328,7 @@ as_fn_mkdir_p () as_dirs= while :; do case $as_dir in #( - *\'*) as_qdir=`$as_echo "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( + *\'*) as_qdir=`printf "%s\n" "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( *) as_qdir=$as_dir;; esac as_dirs="'$as_qdir' $as_dirs" @@ -331,7 +337,7 @@ $as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$as_dir" : 'X\(//\)[^/]' \| \ X"$as_dir" : 'X\(//\)$' \| \ X"$as_dir" : 'X\(/\)' \| . 2>/dev/null || -$as_echo X"$as_dir" | +printf "%s\n" X"$as_dir" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q @@ -370,16 +376,18 @@ as_fn_executable_p () # advantage of any shell optimizations that allow amortized linear growth over # repeated appends, instead of the typical quadratic growth present in naive # implementations. -if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null; then : +if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null +then : eval 'as_fn_append () { eval $1+=\$2 }' -else - as_fn_append () +else case e in #( + e) as_fn_append () { eval $1=\$$1\$2 - } + } ;; +esac fi # as_fn_append # as_fn_arith ARG... @@ -387,16 +395,18 @@ fi # as_fn_append # Perform arithmetic evaluation on the ARGs, and store the result in the # global $as_val. Take advantage of shells that can avoid forks. The arguments # must be portable across $(()) and expr. -if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null; then : +if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null +then : eval 'as_fn_arith () { as_val=$(( $* )) }' -else - as_fn_arith () +else case e in #( + e) as_fn_arith () { as_val=`expr "$@" || test $? -eq 1` - } + } ;; +esac fi # as_fn_arith @@ -410,9 +420,9 @@ as_fn_error () as_status=$1; test $as_status -eq 0 && as_status=1 if test "$4"; then as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - $as_echo "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 fi - $as_echo "$as_me: error: $2" >&2 + printf "%s\n" "$as_me: error: $2" >&2 as_fn_exit $as_status } # as_fn_error @@ -439,7 +449,7 @@ as_me=`$as_basename -- "$0" || $as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \ X"$0" : 'X\(//\)$' \| \ X"$0" : 'X\(/\)' \| . 2>/dev/null || -$as_echo X/"$0" | +printf "%s\n" X/"$0" | sed '/^.*\/\([^/][^/]*\)\/*$/{ s//\1/ q @@ -472,6 +482,8 @@ as_cr_alnum=$as_cr_Letters$as_cr_digits /[$]LINENO/= ' <$as_myself | sed ' + t clear + :clear s/[$]LINENO.*/&-/ t lineno b @@ -483,7 +495,7 @@ as_cr_alnum=$as_cr_Letters$as_cr_digits s/-\n.*// ' >$as_me.lineno && chmod +x "$as_me.lineno" || - { $as_echo "$as_me: error: cannot create $as_me.lineno; rerun with a POSIX shell" >&2; as_fn_exit 1; } + { printf "%s\n" "$as_me: error: cannot create $as_me.lineno; rerun with a POSIX shell" >&2; as_fn_exit 1; } # If we had to re-execute with $CONFIG_SHELL, we're ensured to have # already done that, so ensure we don't try to do so again and fall @@ -497,6 +509,10 @@ as_cr_alnum=$as_cr_Letters$as_cr_digits exit } + +# Determine whether it's possible to make 'echo' print without a newline. +# These variables are no longer used directly by Autoconf, but are AC_SUBSTed +# for compatibility with existing Makefiles. ECHO_C= ECHO_N= ECHO_T= case `echo -n x` in #((((( -n*) @@ -510,6 +526,12 @@ case `echo -n x` in #((((( ECHO_N='-n';; esac +# For backward compatibility with old third-party macros, we provide +# the shell variables $as_echo and $as_echo_n. New code should use +# AS_ECHO(["message"]) and AS_ECHO_N(["message"]), respectively. +as_echo='printf %s\n' +as_echo_n='printf %s' + rm -f conf$$ conf$$.exe conf$$.file if test -d conf$$.dir; then rm -f conf$$.dir/conf$$.file @@ -521,9 +543,9 @@ if (echo >conf$$.file) 2>/dev/null; then if ln -s conf$$.file conf$$ 2>/dev/null; then as_ln_s='ln -s' # ... but there are two gotchas: - # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail. - # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable. - # In both cases, we have to default to `cp -pR'. + # 1) On MSYS, both 'ln -s file dir' and 'ln file dir' fail. + # 2) DJGPP < 2.04 has no symlinks; 'ln -s' creates a wrapper executable. + # In both cases, we have to default to 'cp -pR'. ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe || as_ln_s='cp -pR' elif ln conf$$.file conf$$ 2>/dev/null; then @@ -548,10 +570,12 @@ as_test_x='test -x' as_executable_p=as_fn_executable_p # Sed expression to map a string onto a valid CPP name. -as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'" +as_sed_cpp="y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g" +as_tr_cpp="eval sed '$as_sed_cpp'" # deprecated # Sed expression to map a string onto a valid variable name. -as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'" +as_sed_sh="y%*+%pp%;s%[^_$as_cr_alnum]%_%g" +as_tr_sh="eval sed '$as_sed_sh'" # deprecated test -n "$DJDIR" || exec 7<&0 /dev/null && - as_fn_error $? "invalid feature name: $ac_useropt" + as_fn_error $? "invalid feature name: '$ac_useropt'" ac_useropt_orig=$ac_useropt - ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` + ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "enable_$ac_useropt" @@ -881,9 +907,9 @@ do ac_useropt=`expr "x$ac_option" : 'x-*enable-\([^=]*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && - as_fn_error $? "invalid feature name: $ac_useropt" + as_fn_error $? "invalid feature name: '$ac_useropt'" ac_useropt_orig=$ac_useropt - ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` + ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "enable_$ac_useropt" @@ -1036,6 +1062,15 @@ do | -silent | --silent | --silen | --sile | --sil) silent=yes ;; + -runstatedir | --runstatedir | --runstatedi | --runstated \ + | --runstate | --runstat | --runsta | --runst | --runs \ + | --run | --ru | --r) + ac_prev=runstatedir ;; + -runstatedir=* | --runstatedir=* | --runstatedi=* | --runstated=* \ + | --runstate=* | --runstat=* | --runsta=* | --runst=* | --runs=* \ + | --run=* | --ru=* | --r=*) + runstatedir=$ac_optarg ;; + -sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb) ac_prev=sbindir ;; -sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \ @@ -1085,9 +1120,9 @@ do ac_useropt=`expr "x$ac_option" : 'x-*with-\([^=]*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && - as_fn_error $? "invalid package name: $ac_useropt" + as_fn_error $? "invalid package name: '$ac_useropt'" ac_useropt_orig=$ac_useropt - ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` + ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "with_$ac_useropt" @@ -1101,9 +1136,9 @@ do ac_useropt=`expr "x$ac_option" : 'x-*without-\(.*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && - as_fn_error $? "invalid package name: $ac_useropt" + as_fn_error $? "invalid package name: '$ac_useropt'" ac_useropt_orig=$ac_useropt - ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` + ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "with_$ac_useropt" @@ -1131,8 +1166,8 @@ do | --x-librar=* | --x-libra=* | --x-libr=* | --x-lib=* | --x-li=* | --x-l=*) x_libraries=$ac_optarg ;; - -*) as_fn_error $? "unrecognized option: \`$ac_option' -Try \`$0 --help' for more information" + -*) as_fn_error $? "unrecognized option: '$ac_option' +Try '$0 --help' for more information" ;; *=*) @@ -1140,16 +1175,16 @@ Try \`$0 --help' for more information" # Reject names that are not valid shell variable names. case $ac_envvar in #( '' | [0-9]* | *[!_$as_cr_alnum]* ) - as_fn_error $? "invalid variable name: \`$ac_envvar'" ;; + as_fn_error $? "invalid variable name: '$ac_envvar'" ;; esac eval $ac_envvar=\$ac_optarg export $ac_envvar ;; *) # FIXME: should be removed in autoconf 3.0. - $as_echo "$as_me: WARNING: you should use --build, --host, --target" >&2 + printf "%s\n" "$as_me: WARNING: you should use --build, --host, --target" >&2 expr "x$ac_option" : ".*[^-._$as_cr_alnum]" >/dev/null && - $as_echo "$as_me: WARNING: invalid host type: $ac_option" >&2 + printf "%s\n" "$as_me: WARNING: invalid host type: $ac_option" >&2 : "${build_alias=$ac_option} ${host_alias=$ac_option} ${target_alias=$ac_option}" ;; @@ -1165,7 +1200,7 @@ if test -n "$ac_unrecognized_opts"; then case $enable_option_checking in no) ;; fatal) as_fn_error $? "unrecognized options: $ac_unrecognized_opts" ;; - *) $as_echo "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2 ;; + *) printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2 ;; esac fi @@ -1173,7 +1208,7 @@ fi for ac_var in exec_prefix prefix bindir sbindir libexecdir datarootdir \ datadir sysconfdir sharedstatedir localstatedir includedir \ oldincludedir docdir infodir htmldir dvidir pdfdir psdir \ - libdir localedir mandir + libdir localedir mandir runstatedir do eval ac_val=\$$ac_var # Remove trailing slashes. @@ -1190,7 +1225,7 @@ do as_fn_error $? "expected an absolute directory name for --$ac_var: $ac_val" done -# There might be people who depend on the old broken behavior: `$host' +# There might be people who depend on the old broken behavior: '$host' # used to hold the argument of --host etc. # FIXME: To remove some day. build=$build_alias @@ -1229,7 +1264,7 @@ $as_expr X"$as_myself" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$as_myself" : 'X\(//\)[^/]' \| \ X"$as_myself" : 'X\(//\)$' \| \ X"$as_myself" : 'X\(/\)' \| . 2>/dev/null || -$as_echo X"$as_myself" | +printf "%s\n" X"$as_myself" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q @@ -1258,7 +1293,7 @@ if test ! -r "$srcdir/$ac_unique_file"; then test "$ac_srcdir_defaulted" = yes && srcdir="$ac_confdir or .." as_fn_error $? "cannot find sources ($ac_unique_file) in $srcdir" fi -ac_msg="sources are in $srcdir, but \`cd $srcdir' does not work" +ac_msg="sources are in $srcdir, but 'cd $srcdir' does not work" ac_abs_confdir=`( cd "$srcdir" && test -r "./$ac_unique_file" || as_fn_error $? "$ac_msg" pwd)` @@ -1286,7 +1321,7 @@ if test "$ac_init_help" = "long"; then # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF -\`configure' configures this package to adapt to many kinds of systems. +'configure' configures this package to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1300,11 +1335,11 @@ Configuration: --help=short display options specific to this package --help=recursive display the short help of all the included packages -V, --version display version information and exit - -q, --quiet, --silent do not print \`checking ...' messages + -q, --quiet, --silent do not print 'checking ...' messages --cache-file=FILE cache test results in FILE [disabled] - -C, --config-cache alias for \`--cache-file=config.cache' + -C, --config-cache alias for '--cache-file=config.cache' -n, --no-create do not create output files - --srcdir=DIR find the sources in DIR [configure dir or \`..'] + --srcdir=DIR find the sources in DIR [configure dir or '..'] Installation directories: --prefix=PREFIX install architecture-independent files in PREFIX @@ -1312,10 +1347,10 @@ Installation directories: --exec-prefix=EPREFIX install architecture-dependent files in EPREFIX [PREFIX] -By default, \`make install' will install all the files in -\`$ac_default_prefix/bin', \`$ac_default_prefix/lib' etc. You can specify -an installation prefix other than \`$ac_default_prefix' using \`--prefix', -for instance \`--prefix=\$HOME'. +By default, 'make install' will install all the files in +'$ac_default_prefix/bin', '$ac_default_prefix/lib' etc. You can specify +an installation prefix other than '$ac_default_prefix' using '--prefix', +for instance '--prefix=\$HOME'. For better control, use the options below. @@ -1326,6 +1361,7 @@ Fine tuning of the installation directories: --sysconfdir=DIR read-only single-machine data [PREFIX/etc] --sharedstatedir=DIR modifiable architecture-independent data [PREFIX/com] --localstatedir=DIR modifiable single-machine data [PREFIX/var] + --runstatedir=DIR modifiable per-process data [LOCALSTATEDIR/run] --libdir=DIR object code libraries [EPREFIX/lib] --includedir=DIR C header files [PREFIX/include] --oldincludedir=DIR C header files for non-gcc [/usr/include] @@ -1371,6 +1407,8 @@ Optional Features: --disable-ssl Disable Secure Sockets Layer support --disable-geoip Disable GeoIP support --disable-mmdb Disable MaxMindDB support + --disable-lmdb Disable LMDB/chathistory support + --disable-zstd Disable zstd compression support Optional Packages: --with-PACKAGE[=ARG] use PACKAGE [ARG=yes] @@ -1407,6 +1445,18 @@ Optional Packages: /usr/local/include) --with-mmdb-libs=dir Specify location of MaxMindDB libs (default: /usr/local/lib) + --with-lmdb=dir Specify the installation prefix of LMDB (default: + /usr) + --with-lmdb-includes=dir + Specify location of LMDB header files (default: + /usr/include) + --with-lmdb-libs=dir Specify location of LMDB libs (default: /usr/lib) + --with-zstd=dir Specify the installation prefix of zstd (default: + /usr) + --with-zstd-includes=dir + Specify location of zstd header files (default: + /usr/include) + --with-zstd-libs=dir Specify location of zstd libs (default: /usr/lib) --with-maxcon=maxcon Maximum number of connections server will accept Some influential environment variables: @@ -1417,15 +1467,14 @@ Some influential environment variables: LIBS libraries to pass to the linker, e.g. -l CPPFLAGS (Objective) C/C++ preprocessor flags, e.g. -I if you have headers in a nonstandard directory - CPP C preprocessor - YACC The `Yet Another Compiler Compiler' implementation to use. - Defaults to the first program found out of: `bison -y', `byacc', - `yacc'. + YACC The 'Yet Another Compiler Compiler' implementation to use. + Defaults to the first program found out of: 'bison -y', 'byacc', + 'yacc'. YFLAGS The list of arguments that will be passed by default to $YACC. This script will default YFLAGS to the empty string to avoid a - default value of `-d' given by some make applications. + default value of '-d' given by some make applications. -Use these variables to override the choices made by `configure' or to help +Use these variables to override the choices made by 'configure' or to help it to find libraries and programs with nonstandard names/locations. Report bugs to the package provider. @@ -1444,9 +1493,9 @@ if test "$ac_init_help" = "recursive"; then case "$ac_dir" in .) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; *) - ac_dir_suffix=/`$as_echo "$ac_dir" | sed 's|^\.[\\/]||'` + ac_dir_suffix=/`printf "%s\n" "$ac_dir" | sed 's|^\.[\\/]||'` # A ".." for each directory in $ac_dir_suffix. - ac_top_builddir_sub=`$as_echo "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` + ac_top_builddir_sub=`printf "%s\n" "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` case $ac_top_builddir_sub in "") ac_top_builddir_sub=. ac_top_build_prefix= ;; *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; @@ -1474,7 +1523,8 @@ esac ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix cd "$ac_dir" || { ac_status=$?; continue; } - # Check for guested configure. + # Check for configure.gnu first; this name is used for a wrapper for + # Metaconfig's "Configure" on case-insensitive file systems. if test -f "$ac_srcdir/configure.gnu"; then echo && $SHELL "$ac_srcdir/configure.gnu" --help=recursive @@ -1482,7 +1532,7 @@ ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix echo && $SHELL "$ac_srcdir/configure" --help=recursive else - $as_echo "$as_me: WARNING: no configuration information is in $ac_dir" >&2 + printf "%s\n" "$as_me: WARNING: no configuration information is in $ac_dir" >&2 fi || ac_status=$? cd "$ac_pwd" || { ac_status=$?; break; } done @@ -1492,9 +1542,9 @@ test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF configure -generated by GNU Autoconf 2.69 +generated by GNU Autoconf 2.72 -Copyright (C) 2012 Free Software Foundation, Inc. +Copyright (C) 2023 Free Software Foundation, Inc. This configure script is free software; the Free Software Foundation gives unlimited permission to copy, distribute and modify it. _ACEOF @@ -1511,14 +1561,14 @@ fi ac_fn_c_try_compile () { as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - rm -f conftest.$ac_objext + rm -f conftest.$ac_objext conftest.beam if { { ac_try="$ac_compile" case "(($ac_try" in *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -$as_echo "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_compile") 2>conftest.err ac_status=$? if test -s conftest.err; then @@ -1526,17 +1576,19 @@ $as_echo "$ac_try_echo"; } >&5 cat conftest.er1 >&5 mv -f conftest.er1 conftest.err fi - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } && { test -z "$ac_c_werror_flag" || test ! -s conftest.err - } && test -s conftest.$ac_objext; then : + } && test -s conftest.$ac_objext +then : ac_retval=0 -else - $as_echo "$as_me: failed program was:" >&5 +else case e in #( + e) printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 - ac_retval=1 + ac_retval=1 ;; +esac fi eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno as_fn_set_status $ac_retval @@ -1549,14 +1601,14 @@ fi ac_fn_c_try_link () { as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - rm -f conftest.$ac_objext conftest$ac_exeext + rm -f conftest.$ac_objext conftest.beam conftest$ac_exeext if { { ac_try="$ac_link" case "(($ac_try" in *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -$as_echo "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_link") 2>conftest.err ac_status=$? if test -s conftest.err; then @@ -1564,20 +1616,22 @@ $as_echo "$ac_try_echo"; } >&5 cat conftest.er1 >&5 mv -f conftest.er1 conftest.err fi - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } && { test -z "$ac_c_werror_flag" || test ! -s conftest.err } && test -s conftest$ac_exeext && { test "$cross_compiling" = yes || test -x conftest$ac_exeext - }; then : + } +then : ac_retval=0 -else - $as_echo "$as_me: failed program was:" >&5 +else case e in #( + e) printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 - ac_retval=1 + ac_retval=1 ;; +esac fi # Delete the IPA/IPO (Inter Procedural Analysis/Optimization) information # created by the PGI compiler (conftest_ipa8_conftest.oo), as it would @@ -1595,28 +1649,22 @@ fi ac_fn_c_check_func () { as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 -$as_echo_n "checking for $2... " >&6; } -if eval \${$3+:} false; then : - $as_echo_n "(cached) " >&6 -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 +printf %s "checking for $2... " >&6; } +if eval test \${$3+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Define $2 to an innocuous variant, in case declares $2. For example, HP-UX 11i declares gettimeofday. */ #define $2 innocuous_$2 /* System header to define __stub macros and hopefully few prototypes, - which can conflict with char $2 (); below. - Prefer to if __STDC__ is defined, since - exists even on freestanding compilers. */ - -#ifdef __STDC__ -# include -#else -# include -#endif + which can conflict with char $2 (void); below. */ +#include #undef $2 /* Override any GCC internal prototype to avoid an error. @@ -1625,7 +1673,7 @@ else #ifdef __cplusplus extern "C" #endif -char $2 (); +char $2 (void); /* The GNU C library defines this for functions which it implements to always fail with ENOSYS. Some functions are actually named something starting with __ and the normal name is an alias. */ @@ -1634,69 +1682,70 @@ choke me #endif int -main () +main (void) { return $2 (); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : eval "$3=yes" -else - eval "$3=no" +else case e in #( + e) eval "$3=no" ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ - conftest$ac_exeext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext ;; +esac fi eval ac_res=\$$3 - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 -$as_echo "$ac_res" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +printf "%s\n" "$ac_res" >&6; } eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno } # ac_fn_c_check_func -# ac_fn_c_try_cpp LINENO -# ---------------------- -# Try to preprocess conftest.$ac_ext, and return whether this succeeded. -ac_fn_c_try_cpp () +# ac_fn_c_check_header_compile LINENO HEADER VAR INCLUDES +# ------------------------------------------------------- +# Tests whether HEADER exists and can be compiled using the include files in +# INCLUDES, setting the cache variable VAR accordingly. +ac_fn_c_check_header_compile () { as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - if { { ac_try="$ac_cpp conftest.$ac_ext" -case "(($ac_try" in - *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; - *) ac_try_echo=$ac_try;; + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 +printf %s "checking for $2... " >&6; } +if eval test \${$3+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +$4 +#include <$2> +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + eval "$3=yes" +else case e in #( + e) eval "$3=no" ;; esac -eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -$as_echo "$ac_try_echo"; } >&5 - (eval "$ac_cpp conftest.$ac_ext") 2>conftest.err - ac_status=$? - if test -s conftest.err; then - grep -v '^ *+' conftest.err >conftest.er1 - cat conftest.er1 >&5 - mv -f conftest.er1 conftest.err - fi - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; } > conftest.i && { - test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" || - test ! -s conftest.err - }; then : - ac_retval=0 -else - $as_echo "$as_me: failed program was:" >&5 -sed 's/^/| /' conftest.$ac_ext >&5 - - ac_retval=1 fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac +fi +eval ac_res=\$$3 + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +printf "%s\n" "$ac_res" >&6; } eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno - as_fn_set_status $ac_retval -} # ac_fn_c_try_cpp +} # ac_fn_c_check_header_compile # ac_fn_c_try_run LINENO # ---------------------- -# Try to link conftest.$ac_ext, and return whether this succeeded. Assumes -# that executables *can* be run. +# Try to run conftest.$ac_ext, and return whether this succeeded. Assumes that +# executables *can* be run. ac_fn_c_try_run () { as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack @@ -1706,28 +1755,30 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -$as_echo "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_link") 2>&5 ac_status=$? - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } && { ac_try='./conftest$ac_exeext' { { case "(($ac_try" in *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -$as_echo "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_try") 2>&5 ac_status=$? - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; }; }; then : + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; } +then : ac_retval=0 -else - $as_echo "$as_me: program exited with status $ac_status" >&5 - $as_echo "$as_me: failed program was:" >&5 +else case e in #( + e) printf "%s\n" "$as_me: program exited with status $ac_status" >&5 + printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 - ac_retval=$ac_status + ac_retval=$ac_status ;; +esac fi rm -rf conftest.dSYM conftest_ipa8_conftest.oo eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno @@ -1735,124 +1786,6 @@ fi } # ac_fn_c_try_run -# ac_fn_c_check_header_mongrel LINENO HEADER VAR INCLUDES -# ------------------------------------------------------- -# Tests whether HEADER exists, giving a warning if it cannot be compiled using -# the include files in INCLUDES and setting the cache variable VAR -# accordingly. -ac_fn_c_check_header_mongrel () -{ - as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - if eval \${$3+:} false; then : - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 -$as_echo_n "checking for $2... " >&6; } -if eval \${$3+:} false; then : - $as_echo_n "(cached) " >&6 -fi -eval ac_res=\$$3 - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 -$as_echo "$ac_res" >&6; } -else - # Is the header compilable? -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking $2 usability" >&5 -$as_echo_n "checking $2 usability... " >&6; } -cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -$4 -#include <$2> -_ACEOF -if ac_fn_c_try_compile "$LINENO"; then : - ac_header_compiler=yes -else - ac_header_compiler=no -fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_header_compiler" >&5 -$as_echo "$ac_header_compiler" >&6; } - -# Is the header present? -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking $2 presence" >&5 -$as_echo_n "checking $2 presence... " >&6; } -cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#include <$2> -_ACEOF -if ac_fn_c_try_cpp "$LINENO"; then : - ac_header_preproc=yes -else - ac_header_preproc=no -fi -rm -f conftest.err conftest.i conftest.$ac_ext -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_header_preproc" >&5 -$as_echo "$ac_header_preproc" >&6; } - -# So? What about this header? -case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in #(( - yes:no: ) - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $2: accepted by the compiler, rejected by the preprocessor!" >&5 -$as_echo "$as_me: WARNING: $2: accepted by the compiler, rejected by the preprocessor!" >&2;} - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $2: proceeding with the compiler's result" >&5 -$as_echo "$as_me: WARNING: $2: proceeding with the compiler's result" >&2;} - ;; - no:yes:* ) - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $2: present but cannot be compiled" >&5 -$as_echo "$as_me: WARNING: $2: present but cannot be compiled" >&2;} - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $2: check for missing prerequisite headers?" >&5 -$as_echo "$as_me: WARNING: $2: check for missing prerequisite headers?" >&2;} - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $2: see the Autoconf documentation" >&5 -$as_echo "$as_me: WARNING: $2: see the Autoconf documentation" >&2;} - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $2: section \"Present But Cannot Be Compiled\"" >&5 -$as_echo "$as_me: WARNING: $2: section \"Present But Cannot Be Compiled\"" >&2;} - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $2: proceeding with the compiler's result" >&5 -$as_echo "$as_me: WARNING: $2: proceeding with the compiler's result" >&2;} - ;; -esac - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 -$as_echo_n "checking for $2... " >&6; } -if eval \${$3+:} false; then : - $as_echo_n "(cached) " >&6 -else - eval "$3=\$ac_header_compiler" -fi -eval ac_res=\$$3 - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 -$as_echo "$ac_res" >&6; } -fi - eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno - -} # ac_fn_c_check_header_mongrel - -# ac_fn_c_check_header_compile LINENO HEADER VAR INCLUDES -# ------------------------------------------------------- -# Tests whether HEADER exists and can be compiled using the include files in -# INCLUDES, setting the cache variable VAR accordingly. -ac_fn_c_check_header_compile () -{ - as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 -$as_echo_n "checking for $2... " >&6; } -if eval \${$3+:} false; then : - $as_echo_n "(cached) " >&6 -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -$4 -#include <$2> -_ACEOF -if ac_fn_c_try_compile "$LINENO"; then : - eval "$3=yes" -else - eval "$3=no" -fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext -fi -eval ac_res=\$$3 - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 -$as_echo "$ac_res" >&6; } - eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno - -} # ac_fn_c_check_header_compile - # ac_fn_c_check_type LINENO TYPE VAR INCLUDES # ------------------------------------------- # Tests whether TYPE exists after having included INCLUDES, setting cache @@ -1860,17 +1793,18 @@ $as_echo "$ac_res" >&6; } ac_fn_c_check_type () { as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 -$as_echo_n "checking for $2... " >&6; } -if eval \${$3+:} false; then : - $as_echo_n "(cached) " >&6 -else - eval "$3=no" + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 +printf %s "checking for $2... " >&6; } +if eval test \${$3+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) eval "$3=no" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $4 int -main () +main (void) { if (sizeof ($2)) return 0; @@ -1878,12 +1812,13 @@ if (sizeof ($2)) return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $4 int -main () +main (void) { if (sizeof (($2))) return 0; @@ -1891,18 +1826,21 @@ if (sizeof (($2))) return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : -else - eval "$3=yes" +else case e in #( + e) eval "$3=yes" ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi eval ac_res=\$$3 - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 -$as_echo "$ac_res" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +printf "%s\n" "$ac_res" >&6; } eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno } # ac_fn_c_check_type @@ -1921,7 +1859,7 @@ cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $4 int -main () +main (void) { static int test_array [1 - 2 * !(($2) >= 0)]; test_array [0] = 0; @@ -1931,14 +1869,15 @@ return test_array [0]; return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : ac_lo=0 ac_mid=0 while :; do cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $4 int -main () +main (void) { static int test_array [1 - 2 * !(($2) <= $ac_mid)]; test_array [0] = 0; @@ -1948,24 +1887,26 @@ return test_array [0]; return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : ac_hi=$ac_mid; break -else - as_fn_arith $ac_mid + 1 && ac_lo=$as_val +else case e in #( + e) as_fn_arith $ac_mid + 1 && ac_lo=$as_val if test $ac_lo -le $ac_mid; then ac_lo= ac_hi= break fi - as_fn_arith 2 '*' $ac_mid + 1 && ac_mid=$as_val + as_fn_arith 2 '*' $ac_mid + 1 && ac_mid=$as_val ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext done -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $4 int -main () +main (void) { static int test_array [1 - 2 * !(($2) < 0)]; test_array [0] = 0; @@ -1975,14 +1916,15 @@ return test_array [0]; return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : ac_hi=-1 ac_mid=-1 while :; do cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $4 int -main () +main (void) { static int test_array [1 - 2 * !(($2) >= $ac_mid)]; test_array [0] = 0; @@ -1992,24 +1934,28 @@ return test_array [0]; return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : ac_lo=$ac_mid; break -else - as_fn_arith '(' $ac_mid ')' - 1 && ac_hi=$as_val +else case e in #( + e) as_fn_arith '(' $ac_mid ')' - 1 && ac_hi=$as_val if test $ac_mid -le $ac_hi; then ac_lo= ac_hi= break fi - as_fn_arith 2 '*' $ac_mid && ac_mid=$as_val + as_fn_arith 2 '*' $ac_mid && ac_mid=$as_val ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext done -else - ac_lo= ac_hi= +else case e in #( + e) ac_lo= ac_hi= ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext # Binary search between lo and hi bounds. while test "x$ac_lo" != "x$ac_hi"; do as_fn_arith '(' $ac_hi - $ac_lo ')' / 2 + $ac_lo && ac_mid=$as_val @@ -2017,7 +1963,7 @@ while test "x$ac_lo" != "x$ac_hi"; do /* end confdefs.h. */ $4 int -main () +main (void) { static int test_array [1 - 2 * !(($2) <= $ac_mid)]; test_array [0] = 0; @@ -2027,12 +1973,14 @@ return test_array [0]; return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : ac_hi=$ac_mid -else - as_fn_arith '(' $ac_mid ')' + 1 && ac_lo=$as_val +else case e in #( + e) as_fn_arith '(' $ac_mid ')' + 1 && ac_lo=$as_val ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext done case $ac_lo in #(( ?*) eval "$3=\$ac_lo"; ac_retval=0 ;; @@ -2042,12 +1990,12 @@ esac cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $4 -static long int longval () { return $2; } -static unsigned long int ulongval () { return $2; } +static long int longval (void) { return $2; } +static unsigned long int ulongval (void) { return $2; } #include #include int -main () +main (void) { FILE *f = fopen ("conftest.val", "w"); @@ -2075,10 +2023,12 @@ main () return 0; } _ACEOF -if ac_fn_c_try_run "$LINENO"; then : +if ac_fn_c_try_run "$LINENO" +then : echo >>conftest.val; read $3 config.log <<_ACEOF This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. It was created by $as_me, which was -generated by GNU Autoconf 2.69. Invocation command line was +generated by GNU Autoconf 2.72. Invocation command line was - $ $0 $@ + $ $0$ac_configure_args_raw _ACEOF exec 5>>config.log @@ -2129,8 +2099,12 @@ as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. - $as_echo "PATH: $as_dir" + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + printf "%s\n" "PATH: $as_dir" done IFS=$as_save_IFS @@ -2165,7 +2139,7 @@ do | -silent | --silent | --silen | --sile | --sil) continue ;; *\'*) - ac_arg=`$as_echo "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; + ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; esac case $ac_pass in 1) as_fn_append ac_configure_args0 " '$ac_arg'" ;; @@ -2200,11 +2174,13 @@ done # WARNING: Use '\'' to represent an apostrophe within the trap. # WARNING: Do not start the trap code with a newline, due to a FreeBSD 4.0 bug. trap 'exit_status=$? + # Sanitize IFS. + IFS=" "" $as_nl" # Save into config.log some information that might help in debugging. { echo - $as_echo "## ---------------- ## + printf "%s\n" "## ---------------- ## ## Cache variables. ## ## ---------------- ##" echo @@ -2215,8 +2191,8 @@ trap 'exit_status=$? case $ac_val in #( *${as_nl}*) case $ac_var in #( - *_cv_*) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 -$as_echo "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; + *_cv_*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 +printf "%s\n" "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; esac case $ac_var in #( _ | IFS | as_nl) ;; #( @@ -2240,7 +2216,7 @@ $as_echo "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; ) echo - $as_echo "## ----------------- ## + printf "%s\n" "## ----------------- ## ## Output variables. ## ## ----------------- ##" echo @@ -2248,14 +2224,14 @@ $as_echo "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; do eval ac_val=\$$ac_var case $ac_val in - *\'\''*) ac_val=`$as_echo "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; + *\'\''*) ac_val=`printf "%s\n" "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; esac - $as_echo "$ac_var='\''$ac_val'\''" + printf "%s\n" "$ac_var='\''$ac_val'\''" done | sort echo if test -n "$ac_subst_files"; then - $as_echo "## ------------------- ## + printf "%s\n" "## ------------------- ## ## File substitutions. ## ## ------------------- ##" echo @@ -2263,15 +2239,15 @@ $as_echo "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; do eval ac_val=\$$ac_var case $ac_val in - *\'\''*) ac_val=`$as_echo "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; + *\'\''*) ac_val=`printf "%s\n" "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; esac - $as_echo "$ac_var='\''$ac_val'\''" + printf "%s\n" "$ac_var='\''$ac_val'\''" done | sort echo fi if test -s confdefs.h; then - $as_echo "## ----------- ## + printf "%s\n" "## ----------- ## ## confdefs.h. ## ## ----------- ##" echo @@ -2279,8 +2255,8 @@ $as_echo "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; echo fi test "$ac_signal" != 0 && - $as_echo "$as_me: caught signal $ac_signal" - $as_echo "$as_me: exit $exit_status" + printf "%s\n" "$as_me: caught signal $ac_signal" + printf "%s\n" "$as_me: exit $exit_status" } >&5 rm -f core *.core core.conftest.* && rm -f -r conftest* confdefs* conf$$* $ac_clean_files && @@ -2294,65 +2270,50 @@ ac_signal=0 # confdefs.h avoids OS command line length limits that DEFS can exceed. rm -f -r conftest* confdefs.h -$as_echo "/* confdefs.h */" > confdefs.h +printf "%s\n" "/* confdefs.h */" > confdefs.h # Predefined preprocessor variables. -cat >>confdefs.h <<_ACEOF -#define PACKAGE_NAME "$PACKAGE_NAME" -_ACEOF +printf "%s\n" "#define PACKAGE_NAME \"$PACKAGE_NAME\"" >>confdefs.h -cat >>confdefs.h <<_ACEOF -#define PACKAGE_TARNAME "$PACKAGE_TARNAME" -_ACEOF +printf "%s\n" "#define PACKAGE_TARNAME \"$PACKAGE_TARNAME\"" >>confdefs.h -cat >>confdefs.h <<_ACEOF -#define PACKAGE_VERSION "$PACKAGE_VERSION" -_ACEOF +printf "%s\n" "#define PACKAGE_VERSION \"$PACKAGE_VERSION\"" >>confdefs.h -cat >>confdefs.h <<_ACEOF -#define PACKAGE_STRING "$PACKAGE_STRING" -_ACEOF +printf "%s\n" "#define PACKAGE_STRING \"$PACKAGE_STRING\"" >>confdefs.h -cat >>confdefs.h <<_ACEOF -#define PACKAGE_BUGREPORT "$PACKAGE_BUGREPORT" -_ACEOF +printf "%s\n" "#define PACKAGE_BUGREPORT \"$PACKAGE_BUGREPORT\"" >>confdefs.h -cat >>confdefs.h <<_ACEOF -#define PACKAGE_URL "$PACKAGE_URL" -_ACEOF +printf "%s\n" "#define PACKAGE_URL \"$PACKAGE_URL\"" >>confdefs.h # Let the site file select an alternate cache file if it wants to. # Prefer an explicitly selected file to automatically selected ones. -ac_site_file1=NONE -ac_site_file2=NONE if test -n "$CONFIG_SITE"; then - # We do not want a PATH search for config.site. - case $CONFIG_SITE in #(( - -*) ac_site_file1=./$CONFIG_SITE;; - */*) ac_site_file1=$CONFIG_SITE;; - *) ac_site_file1=./$CONFIG_SITE;; - esac + ac_site_files="$CONFIG_SITE" elif test "x$prefix" != xNONE; then - ac_site_file1=$prefix/share/config.site - ac_site_file2=$prefix/etc/config.site + ac_site_files="$prefix/share/config.site $prefix/etc/config.site" else - ac_site_file1=$ac_default_prefix/share/config.site - ac_site_file2=$ac_default_prefix/etc/config.site + ac_site_files="$ac_default_prefix/share/config.site $ac_default_prefix/etc/config.site" fi -for ac_site_file in "$ac_site_file1" "$ac_site_file2" + +for ac_site_file in $ac_site_files do - test "x$ac_site_file" = xNONE && continue - if test /dev/null != "$ac_site_file" && test -r "$ac_site_file"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: loading site script $ac_site_file" >&5 -$as_echo "$as_me: loading site script $ac_site_file" >&6;} + case $ac_site_file in #( + */*) : + ;; #( + *) : + ac_site_file=./$ac_site_file ;; +esac + if test -f "$ac_site_file" && test -r "$ac_site_file"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: loading site script $ac_site_file" >&5 +printf "%s\n" "$as_me: loading site script $ac_site_file" >&6;} sed 's/^/| /' "$ac_site_file" >&5 . "$ac_site_file" \ - || { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} + || { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "failed to load site script $ac_site_file -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } fi done @@ -2360,61 +2321,495 @@ if test -r "$cache_file"; then # Some versions of bash will fail to source /dev/null (special files # actually), so we avoid doing that. DJGPP emulates it as a regular file. if test /dev/null != "$cache_file" && test -f "$cache_file"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: loading cache $cache_file" >&5 -$as_echo "$as_me: loading cache $cache_file" >&6;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: loading cache $cache_file" >&5 +printf "%s\n" "$as_me: loading cache $cache_file" >&6;} case $cache_file in [\\/]* | ?:[\\/]* ) . "$cache_file";; *) . "./$cache_file";; esac fi else - { $as_echo "$as_me:${as_lineno-$LINENO}: creating cache $cache_file" >&5 -$as_echo "$as_me: creating cache $cache_file" >&6;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating cache $cache_file" >&5 +printf "%s\n" "$as_me: creating cache $cache_file" >&6;} >$cache_file fi -# Check that the precious variables saved in the cache have kept the same -# value. -ac_cache_corrupted=false -for ac_var in $ac_precious_vars; do - eval ac_old_set=\$ac_cv_env_${ac_var}_set - eval ac_new_set=\$ac_env_${ac_var}_set - eval ac_old_val=\$ac_cv_env_${ac_var}_value - eval ac_new_val=\$ac_env_${ac_var}_value - case $ac_old_set,$ac_new_set in - set,) - { $as_echo "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&5 -$as_echo "$as_me: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&2;} - ac_cache_corrupted=: ;; - ,set) - { $as_echo "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was not set in the previous run" >&5 -$as_echo "$as_me: error: \`$ac_var' was not set in the previous run" >&2;} - ac_cache_corrupted=: ;; - ,);; - *) - if test "x$ac_old_val" != "x$ac_new_val"; then - # differences in whitespace do not lead to failure. - ac_old_val_w=`echo x $ac_old_val` - ac_new_val_w=`echo x $ac_new_val` - if test "$ac_old_val_w" != "$ac_new_val_w"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' has changed since the previous run:" >&5 -$as_echo "$as_me: error: \`$ac_var' has changed since the previous run:" >&2;} - ac_cache_corrupted=: - else - { $as_echo "$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&5 -$as_echo "$as_me: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&2;} - eval $ac_var=\$ac_old_val - fi - { $as_echo "$as_me:${as_lineno-$LINENO}: former value: \`$ac_old_val'" >&5 -$as_echo "$as_me: former value: \`$ac_old_val'" >&2;} - { $as_echo "$as_me:${as_lineno-$LINENO}: current value: \`$ac_new_val'" >&5 -$as_echo "$as_me: current value: \`$ac_new_val'" >&2;} - fi;; - esac - # Pass precious variables to config.status. - if test "$ac_new_set" = set; then +# Test code for whether the C compiler supports C89 (global declarations) +ac_c_conftest_c89_globals=' +/* Does the compiler advertise C89 conformance? + Do not test the value of __STDC__, because some compilers set it to 0 + while being otherwise adequately conformant. */ +#if !defined __STDC__ +# error "Compiler does not advertise C89 conformance" +#endif + +#include +#include +struct stat; +/* Most of the following tests are stolen from RCS 5.7 src/conf.sh. */ +struct buf { int x; }; +struct buf * (*rcsopen) (struct buf *, struct stat *, int); +static char *e (char **p, int i) +{ + return p[i]; +} +static char *f (char * (*g) (char **, int), char **p, ...) +{ + char *s; + va_list v; + va_start (v,p); + s = g (p, va_arg (v,int)); + va_end (v); + return s; +} + +/* C89 style stringification. */ +#define noexpand_stringify(a) #a +const char *stringified = noexpand_stringify(arbitrary+token=sequence); + +/* C89 style token pasting. Exercises some of the corner cases that + e.g. old MSVC gets wrong, but not very hard. */ +#define noexpand_concat(a,b) a##b +#define expand_concat(a,b) noexpand_concat(a,b) +extern int vA; +extern int vbee; +#define aye A +#define bee B +int *pvA = &expand_concat(v,aye); +int *pvbee = &noexpand_concat(v,bee); + +/* OSF 4.0 Compaq cc is some sort of almost-ANSI by default. It has + function prototypes and stuff, but not \xHH hex character constants. + These do not provoke an error unfortunately, instead are silently treated + as an "x". The following induces an error, until -std is added to get + proper ANSI mode. Curiously \x00 != x always comes out true, for an + array size at least. It is necessary to write \x00 == 0 to get something + that is true only with -std. */ +int osf4_cc_array ['\''\x00'\'' == 0 ? 1 : -1]; + +/* IBM C 6 for AIX is almost-ANSI by default, but it replaces macro parameters + inside strings and character constants. */ +#define FOO(x) '\''x'\'' +int xlc6_cc_array[FOO(a) == '\''x'\'' ? 1 : -1]; + +int test (int i, double x); +struct s1 {int (*f) (int a);}; +struct s2 {int (*f) (double a);}; +int pairnames (int, char **, int *(*)(struct buf *, struct stat *, int), + int, int);' + +# Test code for whether the C compiler supports C89 (body of main). +ac_c_conftest_c89_main=' +ok |= (argc == 0 || f (e, argv, 0) != argv[0] || f (e, argv, 1) != argv[1]); +' + +# Test code for whether the C compiler supports C99 (global declarations) +ac_c_conftest_c99_globals=' +/* Does the compiler advertise C99 conformance? */ +#if !defined __STDC_VERSION__ || __STDC_VERSION__ < 199901L +# error "Compiler does not advertise C99 conformance" +#endif + +// See if C++-style comments work. + +#include +extern int puts (const char *); +extern int printf (const char *, ...); +extern int dprintf (int, const char *, ...); +extern void *malloc (size_t); +extern void free (void *); + +// Check varargs macros. These examples are taken from C99 6.10.3.5. +// dprintf is used instead of fprintf to avoid needing to declare +// FILE and stderr. +#define debug(...) dprintf (2, __VA_ARGS__) +#define showlist(...) puts (#__VA_ARGS__) +#define report(test,...) ((test) ? puts (#test) : printf (__VA_ARGS__)) +static void +test_varargs_macros (void) +{ + int x = 1234; + int y = 5678; + debug ("Flag"); + debug ("X = %d\n", x); + showlist (The first, second, and third items.); + report (x>y, "x is %d but y is %d", x, y); +} + +// Check long long types. +#define BIG64 18446744073709551615ull +#define BIG32 4294967295ul +#define BIG_OK (BIG64 / BIG32 == 4294967297ull && BIG64 % BIG32 == 0) +#if !BIG_OK + #error "your preprocessor is broken" +#endif +#if BIG_OK +#else + #error "your preprocessor is broken" +#endif +static long long int bignum = -9223372036854775807LL; +static unsigned long long int ubignum = BIG64; + +struct incomplete_array +{ + int datasize; + double data[]; +}; + +struct named_init { + int number; + const wchar_t *name; + double average; +}; + +typedef const char *ccp; + +static inline int +test_restrict (ccp restrict text) +{ + // Iterate through items via the restricted pointer. + // Also check for declarations in for loops. + for (unsigned int i = 0; *(text+i) != '\''\0'\''; ++i) + continue; + return 0; +} + +// Check varargs and va_copy. +static bool +test_varargs (const char *format, ...) +{ + va_list args; + va_start (args, format); + va_list args_copy; + va_copy (args_copy, args); + + const char *str = ""; + int number = 0; + float fnumber = 0; + + while (*format) + { + switch (*format++) + { + case '\''s'\'': // string + str = va_arg (args_copy, const char *); + break; + case '\''d'\'': // int + number = va_arg (args_copy, int); + break; + case '\''f'\'': // float + fnumber = va_arg (args_copy, double); + break; + default: + break; + } + } + va_end (args_copy); + va_end (args); + + return *str && number && fnumber; +} +' + +# Test code for whether the C compiler supports C99 (body of main). +ac_c_conftest_c99_main=' + // Check bool. + _Bool success = false; + success |= (argc != 0); + + // Check restrict. + if (test_restrict ("String literal") == 0) + success = true; + char *restrict newvar = "Another string"; + + // Check varargs. + success &= test_varargs ("s, d'\'' f .", "string", 65, 34.234); + test_varargs_macros (); + + // Check flexible array members. + struct incomplete_array *ia = + malloc (sizeof (struct incomplete_array) + (sizeof (double) * 10)); + ia->datasize = 10; + for (int i = 0; i < ia->datasize; ++i) + ia->data[i] = i * 1.234; + // Work around memory leak warnings. + free (ia); + + // Check named initializers. + struct named_init ni = { + .number = 34, + .name = L"Test wide string", + .average = 543.34343, + }; + + ni.number = 58; + + int dynamic_array[ni.number]; + dynamic_array[0] = argv[0][0]; + dynamic_array[ni.number - 1] = 543; + + // work around unused variable warnings + ok |= (!success || bignum == 0LL || ubignum == 0uLL || newvar[0] == '\''x'\'' + || dynamic_array[ni.number - 1] != 543); +' + +# Test code for whether the C compiler supports C11 (global declarations) +ac_c_conftest_c11_globals=' +/* Does the compiler advertise C11 conformance? */ +#if !defined __STDC_VERSION__ || __STDC_VERSION__ < 201112L +# error "Compiler does not advertise C11 conformance" +#endif + +// Check _Alignas. +char _Alignas (double) aligned_as_double; +char _Alignas (0) no_special_alignment; +extern char aligned_as_int; +char _Alignas (0) _Alignas (int) aligned_as_int; + +// Check _Alignof. +enum +{ + int_alignment = _Alignof (int), + int_array_alignment = _Alignof (int[100]), + char_alignment = _Alignof (char) +}; +_Static_assert (0 < -_Alignof (int), "_Alignof is signed"); + +// Check _Noreturn. +int _Noreturn does_not_return (void) { for (;;) continue; } + +// Check _Static_assert. +struct test_static_assert +{ + int x; + _Static_assert (sizeof (int) <= sizeof (long int), + "_Static_assert does not work in struct"); + long int y; +}; + +// Check UTF-8 literals. +#define u8 syntax error! +char const utf8_literal[] = u8"happens to be ASCII" "another string"; + +// Check duplicate typedefs. +typedef long *long_ptr; +typedef long int *long_ptr; +typedef long_ptr long_ptr; + +// Anonymous structures and unions -- taken from C11 6.7.2.1 Example 1. +struct anonymous +{ + union { + struct { int i; int j; }; + struct { int k; long int l; } w; + }; + int m; +} v1; +' + +# Test code for whether the C compiler supports C11 (body of main). +ac_c_conftest_c11_main=' + _Static_assert ((offsetof (struct anonymous, i) + == offsetof (struct anonymous, w.k)), + "Anonymous union alignment botch"); + v1.i = 2; + v1.w.k = 5; + ok |= v1.i != 5; +' + +# Test code for whether the C compiler supports C11 (complete). +ac_c_conftest_c11_program="${ac_c_conftest_c89_globals} +${ac_c_conftest_c99_globals} +${ac_c_conftest_c11_globals} + +int +main (int argc, char **argv) +{ + int ok = 0; + ${ac_c_conftest_c89_main} + ${ac_c_conftest_c99_main} + ${ac_c_conftest_c11_main} + return ok; +} +" + +# Test code for whether the C compiler supports C99 (complete). +ac_c_conftest_c99_program="${ac_c_conftest_c89_globals} +${ac_c_conftest_c99_globals} + +int +main (int argc, char **argv) +{ + int ok = 0; + ${ac_c_conftest_c89_main} + ${ac_c_conftest_c99_main} + return ok; +} +" + +# Test code for whether the C compiler supports C89 (complete). +ac_c_conftest_c89_program="${ac_c_conftest_c89_globals} + +int +main (int argc, char **argv) +{ + int ok = 0; + ${ac_c_conftest_c89_main} + return ok; +} +" + +as_fn_append ac_header_c_list " stdio.h stdio_h HAVE_STDIO_H" +as_fn_append ac_header_c_list " stdlib.h stdlib_h HAVE_STDLIB_H" +as_fn_append ac_header_c_list " string.h string_h HAVE_STRING_H" +as_fn_append ac_header_c_list " inttypes.h inttypes_h HAVE_INTTYPES_H" +as_fn_append ac_header_c_list " stdint.h stdint_h HAVE_STDINT_H" +as_fn_append ac_header_c_list " strings.h strings_h HAVE_STRINGS_H" +as_fn_append ac_header_c_list " sys/stat.h sys_stat_h HAVE_SYS_STAT_H" +as_fn_append ac_header_c_list " sys/types.h sys_types_h HAVE_SYS_TYPES_H" +as_fn_append ac_header_c_list " unistd.h unistd_h HAVE_UNISTD_H" +as_fn_append ac_header_c_list " sys/time.h sys_time_h HAVE_SYS_TIME_H" + +# Auxiliary files required by this configure script. +ac_aux_files="install-sh config.guess config.sub" + +# Locations in which to look for auxiliary files. +ac_aux_dir_candidates="${srcdir}${PATH_SEPARATOR}${srcdir}/..${PATH_SEPARATOR}${srcdir}/../.." + +# Search for a directory containing all of the required auxiliary files, +# $ac_aux_files, from the $PATH-style list $ac_aux_dir_candidates. +# If we don't find one directory that contains all the files we need, +# we report the set of missing files from the *first* directory in +# $ac_aux_dir_candidates and give up. +ac_missing_aux_files="" +ac_first_candidate=: +printf "%s\n" "$as_me:${as_lineno-$LINENO}: looking for aux files: $ac_aux_files" >&5 +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +as_found=false +for as_dir in $ac_aux_dir_candidates +do + IFS=$as_save_IFS + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + as_found=: + + printf "%s\n" "$as_me:${as_lineno-$LINENO}: trying $as_dir" >&5 + ac_aux_dir_found=yes + ac_install_sh= + for ac_aux in $ac_aux_files + do + # As a special case, if "install-sh" is required, that requirement + # can be satisfied by any of "install-sh", "install.sh", or "shtool", + # and $ac_install_sh is set appropriately for whichever one is found. + if test x"$ac_aux" = x"install-sh" + then + if test -f "${as_dir}install-sh"; then + printf "%s\n" "$as_me:${as_lineno-$LINENO}: ${as_dir}install-sh found" >&5 + ac_install_sh="${as_dir}install-sh -c" + elif test -f "${as_dir}install.sh"; then + printf "%s\n" "$as_me:${as_lineno-$LINENO}: ${as_dir}install.sh found" >&5 + ac_install_sh="${as_dir}install.sh -c" + elif test -f "${as_dir}shtool"; then + printf "%s\n" "$as_me:${as_lineno-$LINENO}: ${as_dir}shtool found" >&5 + ac_install_sh="${as_dir}shtool install -c" + else + ac_aux_dir_found=no + if $ac_first_candidate; then + ac_missing_aux_files="${ac_missing_aux_files} install-sh" + else + break + fi + fi + else + if test -f "${as_dir}${ac_aux}"; then + printf "%s\n" "$as_me:${as_lineno-$LINENO}: ${as_dir}${ac_aux} found" >&5 + else + ac_aux_dir_found=no + if $ac_first_candidate; then + ac_missing_aux_files="${ac_missing_aux_files} ${ac_aux}" + else + break + fi + fi + fi + done + if test "$ac_aux_dir_found" = yes; then + ac_aux_dir="$as_dir" + break + fi + ac_first_candidate=false + + as_found=false +done +IFS=$as_save_IFS +if $as_found +then : + +else case e in #( + e) as_fn_error $? "cannot find required auxiliary files:$ac_missing_aux_files" "$LINENO" 5 ;; +esac +fi + + +# These three variables are undocumented and unsupported, +# and are intended to be withdrawn in a future Autoconf release. +# They can cause serious problems if a builder's source tree is in a directory +# whose full name contains unusual characters. +if test -f "${ac_aux_dir}config.guess"; then + ac_config_guess="$SHELL ${ac_aux_dir}config.guess" +fi +if test -f "${ac_aux_dir}config.sub"; then + ac_config_sub="$SHELL ${ac_aux_dir}config.sub" +fi +if test -f "$ac_aux_dir/configure"; then + ac_configure="$SHELL ${ac_aux_dir}configure" +fi + +# Check that the precious variables saved in the cache have kept the same +# value. +ac_cache_corrupted=false +for ac_var in $ac_precious_vars; do + eval ac_old_set=\$ac_cv_env_${ac_var}_set + eval ac_new_set=\$ac_env_${ac_var}_set + eval ac_old_val=\$ac_cv_env_${ac_var}_value + eval ac_new_val=\$ac_env_${ac_var}_value + case $ac_old_set,$ac_new_set in + set,) + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: '$ac_var' was set to '$ac_old_val' in the previous run" >&5 +printf "%s\n" "$as_me: error: '$ac_var' was set to '$ac_old_val' in the previous run" >&2;} + ac_cache_corrupted=: ;; + ,set) + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: '$ac_var' was not set in the previous run" >&5 +printf "%s\n" "$as_me: error: '$ac_var' was not set in the previous run" >&2;} + ac_cache_corrupted=: ;; + ,);; + *) + if test "x$ac_old_val" != "x$ac_new_val"; then + # differences in whitespace do not lead to failure. + ac_old_val_w=`echo x $ac_old_val` + ac_new_val_w=`echo x $ac_new_val` + if test "$ac_old_val_w" != "$ac_new_val_w"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: '$ac_var' has changed since the previous run:" >&5 +printf "%s\n" "$as_me: error: '$ac_var' has changed since the previous run:" >&2;} + ac_cache_corrupted=: + else + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in '$ac_var' since the previous run:" >&5 +printf "%s\n" "$as_me: warning: ignoring whitespace changes in '$ac_var' since the previous run:" >&2;} + eval $ac_var=\$ac_old_val + fi + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: former value: '$ac_old_val'" >&5 +printf "%s\n" "$as_me: former value: '$ac_old_val'" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: current value: '$ac_new_val'" >&5 +printf "%s\n" "$as_me: current value: '$ac_new_val'" >&2;} + fi;; + esac + # Pass precious variables to config.status. + if test "$ac_new_set" = set; then case $ac_new_val in - *\'*) ac_arg=$ac_var=`$as_echo "$ac_new_val" | sed "s/'/'\\\\\\\\''/g"` ;; + *\'*) ac_arg=$ac_var=`printf "%s\n" "$ac_new_val" | sed "s/'/'\\\\\\\\''/g"` ;; *) ac_arg=$ac_var=$ac_new_val ;; esac case " $ac_configure_args " in @@ -2424,11 +2819,12 @@ $as_echo "$as_me: current value: \`$ac_new_val'" >&2;} fi done if $ac_cache_corrupted; then - { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} - { $as_echo "$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build" >&5 -$as_echo "$as_me: error: changes in the environment can compromise the build" >&2;} - as_fn_error $? "run \`make distclean' and/or \`rm $cache_file' and start over" "$LINENO" 5 + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build" >&5 +printf "%s\n" "$as_me: error: changes in the environment can compromise the build" >&2;} + as_fn_error $? "run '${MAKE-make} distclean' and/or 'rm $cache_file' + and start over" "$LINENO" 5 fi ## -------------------- ## ## Main body of script. ## @@ -2443,19 +2839,21 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for installation prefix" >&5 -$as_echo_n "checking for installation prefix... " >&6; } -if ${unet_cv_prefix+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_prefix=$HOME +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for installation prefix" >&5 +printf %s "checking for installation prefix... " >&6; } +if test ${unet_cv_prefix+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_prefix=$HOME ;; +esac fi if test x"$prefix" != xNONE; then unet_cv_prefix=$prefix fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_prefix" >&5 -$as_echo "$unet_cv_prefix" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_prefix" >&5 +printf "%s\n" "$unet_cv_prefix" >&6; } ac_default_prefix=$unet_cv_prefix ac_config_headers="$ac_config_headers config.h" @@ -2463,55 +2861,31 @@ ac_config_headers="$ac_config_headers config.h" -ac_aux_dir= -for ac_dir in "$srcdir" "$srcdir/.." "$srcdir/../.."; do - if test -f "$ac_dir/install-sh"; then - ac_aux_dir=$ac_dir - ac_install_sh="$ac_aux_dir/install-sh -c" - break - elif test -f "$ac_dir/install.sh"; then - ac_aux_dir=$ac_dir - ac_install_sh="$ac_aux_dir/install.sh -c" - break - elif test -f "$ac_dir/shtool"; then - ac_aux_dir=$ac_dir - ac_install_sh="$ac_aux_dir/shtool install -c" - break - fi -done -if test -z "$ac_aux_dir"; then - as_fn_error $? "cannot find install-sh, install.sh, or shtool in \"$srcdir\" \"$srcdir/..\" \"$srcdir/../..\"" "$LINENO" 5 -fi -# These three variables are undocumented and unsupported, -# and are intended to be withdrawn in a future Autoconf release. -# They can cause serious problems if a builder's source tree is in a directory -# whose full name contains unusual characters. -ac_config_guess="$SHELL $ac_aux_dir/config.guess" # Please don't use this var. -ac_config_sub="$SHELL $ac_aux_dir/config.sub" # Please don't use this var. -ac_configure="$SHELL $ac_aux_dir/configure" # Please don't use this var. -# Make sure we can run config.sub. -$SHELL "$ac_aux_dir/config.sub" sun4 >/dev/null 2>&1 || - as_fn_error $? "cannot run $SHELL $ac_aux_dir/config.sub" "$LINENO" 5 + # Make sure we can run config.sub. +$SHELL "${ac_aux_dir}config.sub" sun4 >/dev/null 2>&1 || + as_fn_error $? "cannot run $SHELL ${ac_aux_dir}config.sub" "$LINENO" 5 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking build system type" >&5 -$as_echo_n "checking build system type... " >&6; } -if ${ac_cv_build+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_build_alias=$build_alias +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking build system type" >&5 +printf %s "checking build system type... " >&6; } +if test ${ac_cv_build+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_build_alias=$build_alias test "x$ac_build_alias" = x && - ac_build_alias=`$SHELL "$ac_aux_dir/config.guess"` + ac_build_alias=`$SHELL "${ac_aux_dir}config.guess"` test "x$ac_build_alias" = x && as_fn_error $? "cannot guess build type; you must specify one" "$LINENO" 5 -ac_cv_build=`$SHELL "$ac_aux_dir/config.sub" $ac_build_alias` || - as_fn_error $? "$SHELL $ac_aux_dir/config.sub $ac_build_alias failed" "$LINENO" 5 - +ac_cv_build=`$SHELL "${ac_aux_dir}config.sub" $ac_build_alias` || + as_fn_error $? "$SHELL ${ac_aux_dir}config.sub $ac_build_alias failed" "$LINENO" 5 + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_build" >&5 -$as_echo "$ac_cv_build" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_build" >&5 +printf "%s\n" "$ac_cv_build" >&6; } case $ac_cv_build in *-*-*) ;; *) as_fn_error $? "invalid value of canonical build" "$LINENO" 5;; @@ -2530,21 +2904,23 @@ IFS=$ac_save_IFS case $build_os in *\ *) build_os=`echo "$build_os" | sed 's/ /-/g'`;; esac -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking host system type" >&5 -$as_echo_n "checking host system type... " >&6; } -if ${ac_cv_host+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test "x$host_alias" = x; then +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking host system type" >&5 +printf %s "checking host system type... " >&6; } +if test ${ac_cv_host+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test "x$host_alias" = x; then ac_cv_host=$ac_cv_build else - ac_cv_host=`$SHELL "$ac_aux_dir/config.sub" $host_alias` || - as_fn_error $? "$SHELL $ac_aux_dir/config.sub $host_alias failed" "$LINENO" 5 + ac_cv_host=`$SHELL "${ac_aux_dir}config.sub" $host_alias` || + as_fn_error $? "$SHELL ${ac_aux_dir}config.sub $host_alias failed" "$LINENO" 5 fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_host" >&5 -$as_echo "$ac_cv_host" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_host" >&5 +printf "%s\n" "$ac_cv_host" >&6; } case $ac_cv_host in *-*-*) ;; *) as_fn_error $? "invalid value of canonical host" "$LINENO" 5;; @@ -2564,6 +2940,15 @@ case $host_os in *\ *) host_os=`echo "$host_os" | sed 's/ /-/g'`;; esac + + + + + + + + + ac_ext=c ac_cpp='$CPP $CPPFLAGS' ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' @@ -2572,38 +2957,44 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu if test -n "$ac_tool_prefix"; then # Extract the first word of "${ac_tool_prefix}gcc", so it can be a program name with args. set dummy ${ac_tool_prefix}gcc; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_prog_CC+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test -n "$CC"; then +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_CC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_CC="${ac_tool_prefix}gcc" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -$as_echo "$CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf "%s\n" "$CC" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -2612,38 +3003,44 @@ if test -z "$ac_cv_prog_CC"; then ac_ct_CC=$CC # Extract the first word of "gcc", so it can be a program name with args. set dummy gcc; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_prog_ac_ct_CC+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test -n "$ac_ct_CC"; then +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_ac_ct_CC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$ac_ct_CC"; then ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_ac_ct_CC="gcc" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS -fi +fi ;; +esac fi ac_ct_CC=$ac_cv_prog_ac_ct_CC if test -n "$ac_ct_CC"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 -$as_echo "$ac_ct_CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 +printf "%s\n" "$ac_ct_CC" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi if test "x$ac_ct_CC" = x; then @@ -2651,8 +3048,8 @@ fi else case $cross_compiling:$ac_tool_warned in yes:) -{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 -$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} ac_tool_warned=yes ;; esac CC=$ac_ct_CC @@ -2665,38 +3062,44 @@ if test -z "$CC"; then if test -n "$ac_tool_prefix"; then # Extract the first word of "${ac_tool_prefix}cc", so it can be a program name with args. set dummy ${ac_tool_prefix}cc; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_prog_CC+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test -n "$CC"; then +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_CC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_CC="${ac_tool_prefix}cc" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -$as_echo "$CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf "%s\n" "$CC" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -2705,180 +3108,304 @@ fi if test -z "$CC"; then # Extract the first word of "cc", so it can be a program name with args. set dummy cc; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_prog_CC+:} false; then : - $as_echo_n "(cached) " >&6 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_CC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$CC"; then + ac_cv_prog_CC="$CC" # Let the user override the test. else - if test -n "$CC"; then + ac_prog_rejected=no +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then + if test "$as_dir$ac_word$ac_exec_ext" = "/usr/ucb/cc"; then + ac_prog_rejected=yes + continue + fi + ac_cv_prog_CC="cc" + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +if test $ac_prog_rejected = yes; then + # We found a bogon in the path, so make sure we never use it. + set dummy $ac_cv_prog_CC + shift + if test $# != 0; then + # We chose a different compiler from the bogus one. + # However, it has the same basename, so the bogon will be chosen + # first if we set CC to just the basename; use the full file name. + shift + ac_cv_prog_CC="$as_dir$ac_word${1+' '}$@" + fi +fi +fi ;; +esac +fi +CC=$ac_cv_prog_CC +if test -n "$CC"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf "%s\n" "$CC" >&6; } +else + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } +fi + + +fi +if test -z "$CC"; then + if test -n "$ac_tool_prefix"; then + for ac_prog in cl.exe + do + # Extract the first word of "$ac_tool_prefix$ac_prog", so it can be a program name with args. +set dummy $ac_tool_prefix$ac_prog; ac_word=$2 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_CC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else - ac_prog_rejected=no as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then + ac_cv_prog_CC="$ac_tool_prefix$ac_prog" + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi ;; +esac +fi +CC=$ac_cv_prog_CC +if test -n "$CC"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf "%s\n" "$CC" >&6; } +else + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } +fi + + + test -n "$CC" && break + done +fi +if test -z "$CC"; then + ac_ct_CC=$CC + for ac_prog in cl.exe +do + # Extract the first word of "$ac_prog", so it can be a program name with args. +set dummy $ac_prog; ac_word=$2 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_ac_ct_CC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$ac_ct_CC"; then + ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then - if test "$as_dir/$ac_word$ac_exec_ext" = "/usr/ucb/cc"; then - ac_prog_rejected=yes - continue - fi - ac_cv_prog_CC="cc" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_CC="$ac_prog" + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS -if test $ac_prog_rejected = yes; then - # We found a bogon in the path, so make sure we never use it. - set dummy $ac_cv_prog_CC - shift - if test $# != 0; then - # We chose a different compiler from the bogus one. - # However, it has the same basename, so the bogon will be chosen - # first if we set CC to just the basename; use the full file name. - shift - ac_cv_prog_CC="$as_dir/$ac_word${1+' '}$@" - fi -fi -fi +fi ;; +esac fi -CC=$ac_cv_prog_CC -if test -n "$CC"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -$as_echo "$CC" >&6; } +ac_ct_CC=$ac_cv_prog_ac_ct_CC +if test -n "$ac_ct_CC"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 +printf "%s\n" "$ac_ct_CC" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi + test -n "$ac_ct_CC" && break +done + + if test "x$ac_ct_CC" = x; then + CC="" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + CC=$ac_ct_CC + fi +fi + fi if test -z "$CC"; then if test -n "$ac_tool_prefix"; then - for ac_prog in cl.exe - do - # Extract the first word of "$ac_tool_prefix$ac_prog", so it can be a program name with args. -set dummy $ac_tool_prefix$ac_prog; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_prog_CC+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test -n "$CC"; then + # Extract the first word of "${ac_tool_prefix}clang", so it can be a program name with args. +set dummy ${ac_tool_prefix}clang; ac_word=$2 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_CC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then - ac_cv_prog_CC="$ac_tool_prefix$ac_prog" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then + ac_cv_prog_CC="${ac_tool_prefix}clang" + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -$as_echo "$CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf "%s\n" "$CC" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi - test -n "$CC" && break - done fi -if test -z "$CC"; then +if test -z "$ac_cv_prog_CC"; then ac_ct_CC=$CC - for ac_prog in cl.exe -do - # Extract the first word of "$ac_prog", so it can be a program name with args. -set dummy $ac_prog; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_prog_ac_ct_CC+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test -n "$ac_ct_CC"; then + # Extract the first word of "clang", so it can be a program name with args. +set dummy clang; ac_word=$2 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_ac_ct_CC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$ac_ct_CC"; then ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then - ac_cv_prog_ac_ct_CC="$ac_prog" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_CC="clang" + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS -fi +fi ;; +esac fi ac_ct_CC=$ac_cv_prog_ac_ct_CC if test -n "$ac_ct_CC"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 -$as_echo "$ac_ct_CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 +printf "%s\n" "$ac_ct_CC" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi - - test -n "$ac_ct_CC" && break -done - if test "x$ac_ct_CC" = x; then CC="" else case $cross_compiling:$ac_tool_warned in yes:) -{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 -$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} ac_tool_warned=yes ;; esac CC=$ac_ct_CC fi +else + CC="$ac_cv_prog_CC" fi fi -test -z "$CC" && { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +test -z "$CC" && { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "no acceptable C compiler found in \$PATH -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } # Provide some information about the compiler. -$as_echo "$as_me:${as_lineno-$LINENO}: checking for C compiler version" >&5 +printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for C compiler version" >&5 set X $ac_compile ac_compiler=$2 -for ac_option in --version -v -V -qversion; do +for ac_option in --version -v -V -qversion -version; do { { ac_try="$ac_compiler $ac_option >&5" case "(($ac_try" in *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -$as_echo "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_compiler $ac_option >&5") 2>conftest.err ac_status=$? if test -s conftest.err; then @@ -2888,7 +3415,7 @@ $as_echo "$ac_try_echo"; } >&5 cat conftest.er1 >&5 fi rm -f conftest.er1 conftest.err - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } done @@ -2896,7 +3423,7 @@ cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ int -main () +main (void) { ; @@ -2908,9 +3435,9 @@ ac_clean_files="$ac_clean_files a.out a.out.dSYM a.exe b.out" # Try to create an executable without -o first, disregard a.out. # It will help us diagnose broken compilers, and finding out an intuition # of exeext. -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether the C compiler works" >&5 -$as_echo_n "checking whether the C compiler works... " >&6; } -ac_link_default=`$as_echo "$ac_link" | sed 's/ -o *conftest[^ ]*//'` +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether the C compiler works" >&5 +printf %s "checking whether the C compiler works... " >&6; } +ac_link_default=`printf "%s\n" "$ac_link" | sed 's/ -o *conftest[^ ]*//'` # The possible output files: ac_files="a.out conftest.exe conftest a.exe a_out.exe b.out conftest.*" @@ -2931,13 +3458,14 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -$as_echo "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_link_default") 2>&5 ac_status=$? - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; }; then : - # Autoconf-2.13 could set the ac_cv_exeext variable to `no'. -# So ignore a value of `no', otherwise this would lead to `EXEEXT = no' + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } +then : + # Autoconf-2.13 could set the ac_cv_exeext variable to 'no'. +# So ignore a value of 'no', otherwise this would lead to 'EXEEXT = no' # in a Makefile. We should not override ac_cv_exeext if it was cached, # so that the user can short-circuit this test for compilers unknown to # Autoconf. @@ -2952,12 +3480,12 @@ do # certainly right. break;; *.* ) - if test "${ac_cv_exeext+set}" = set && test "$ac_cv_exeext" != no; + if test ${ac_cv_exeext+y} && test "$ac_cv_exeext" != no; then :; else ac_cv_exeext=`expr "$ac_file" : '[^.]*\(\..*\)'` fi # We set ac_cv_exeext here because the later test for it is not - # safe: cross compilers may not add the suffix if given an `-o' + # safe: cross compilers may not add the suffix if given an '-o' # argument, so we may need to know it at that point already. # Even if this section looks crufty: it has the advantage of # actually working. @@ -2968,48 +3496,52 @@ do done test "$ac_cv_exeext" = no && ac_cv_exeext= -else - ac_file='' +else case e in #( + e) ac_file='' ;; +esac fi -if test -z "$ac_file"; then : - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } -$as_echo "$as_me: failed program was:" >&5 +if test -z "$ac_file" +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } +printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 -{ { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "C compiler cannot create executables -See \`config.log' for more details" "$LINENO" 5; } -else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -$as_echo "yes" >&6; } +See 'config.log' for more details" "$LINENO" 5; } +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for C compiler default output file name" >&5 -$as_echo_n "checking for C compiler default output file name... " >&6; } -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_file" >&5 -$as_echo "$ac_file" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for C compiler default output file name" >&5 +printf %s "checking for C compiler default output file name... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_file" >&5 +printf "%s\n" "$ac_file" >&6; } ac_exeext=$ac_cv_exeext rm -f -r a.out a.out.dSYM a.exe conftest$ac_cv_exeext b.out ac_clean_files=$ac_clean_files_save -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for suffix of executables" >&5 -$as_echo_n "checking for suffix of executables... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for suffix of executables" >&5 +printf %s "checking for suffix of executables... " >&6; } if { { ac_try="$ac_link" case "(($ac_try" in *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -$as_echo "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_link") 2>&5 ac_status=$? - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; }; then : - # If both `conftest.exe' and `conftest' are `present' (well, observable) -# catch `conftest.exe'. For instance with Cygwin, `ls conftest' will -# work properly (i.e., refer to `conftest.exe'), while it won't with -# `rm'. + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } +then : + # If both 'conftest.exe' and 'conftest' are 'present' (well, observable) +# catch 'conftest.exe'. For instance with Cygwin, 'ls conftest' will +# work properly (i.e., refer to 'conftest.exe'), while it won't with +# 'rm'. for ac_file in conftest.exe conftest conftest.*; do test -f "$ac_file" || continue case $ac_file in @@ -3019,15 +3551,16 @@ for ac_file in conftest.exe conftest conftest.*; do * ) break;; esac done -else - { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +else case e in #( + e) { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot compute suffix of executables: cannot compile and link -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } ;; +esac fi rm -f conftest conftest$ac_cv_exeext -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_exeext" >&5 -$as_echo "$ac_cv_exeext" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_exeext" >&5 +printf "%s\n" "$ac_cv_exeext" >&6; } rm -f conftest.$ac_ext EXEEXT=$ac_cv_exeext @@ -3036,9 +3569,11 @@ cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include int -main () +main (void) { FILE *f = fopen ("conftest.out", "w"); + if (!f) + return 1; return ferror (f) || fclose (f) != 0; ; @@ -3048,8 +3583,8 @@ _ACEOF ac_clean_files="$ac_clean_files conftest.out" # Check that the compiler produces executables we can run. If not, either # the compiler is broken, or we cross compile. -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether we are cross compiling" >&5 -$as_echo_n "checking whether we are cross compiling... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether we are cross compiling" >&5 +printf %s "checking whether we are cross compiling... " >&6; } if test "$cross_compiling" != yes; then { { ac_try="$ac_link" case "(($ac_try" in @@ -3057,10 +3592,10 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -$as_echo "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_link") 2>&5 ac_status=$? - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } if { ac_try='./conftest$ac_cv_exeext' { { case "(($ac_try" in @@ -3068,39 +3603,41 @@ $as_echo "$ac_try_echo"; } >&5 *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -$as_echo "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_try") 2>&5 ac_status=$? - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; }; }; then cross_compiling=no else if test "$cross_compiling" = maybe; then cross_compiling=yes else - { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} -as_fn_error $? "cannot run C compiled programs. -If you meant to cross compile, use \`--host'. -See \`config.log' for more details" "$LINENO" 5; } + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} +as_fn_error 77 "cannot run C compiled programs. +If you meant to cross compile, use '--host'. +See 'config.log' for more details" "$LINENO" 5; } fi fi fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $cross_compiling" >&5 -$as_echo "$cross_compiling" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $cross_compiling" >&5 +printf "%s\n" "$cross_compiling" >&6; } -rm -f conftest.$ac_ext conftest$ac_cv_exeext conftest.out +rm -f conftest.$ac_ext conftest$ac_cv_exeext \ + conftest.o conftest.obj conftest.out ac_clean_files=$ac_clean_files_save -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for suffix of object files" >&5 -$as_echo_n "checking for suffix of object files... " >&6; } -if ${ac_cv_objext+:} false; then : - $as_echo_n "(cached) " >&6 -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for suffix of object files" >&5 +printf %s "checking for suffix of object files... " >&6; } +if test ${ac_cv_objext+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ int -main () +main (void) { ; @@ -3114,11 +3651,12 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -$as_echo "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_compile") 2>&5 ac_status=$? - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; }; then : + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } +then : for ac_file in conftest.o conftest.obj conftest.*; do test -f "$ac_file" || continue; case $ac_file in @@ -3127,31 +3665,34 @@ $as_echo "$ac_try_echo"; } >&5 break;; esac done -else - $as_echo "$as_me: failed program was:" >&5 +else case e in #( + e) printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 -{ { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot compute suffix of object files: cannot compile -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } ;; +esac fi -rm -f conftest.$ac_cv_objext conftest.$ac_ext +rm -f conftest.$ac_cv_objext conftest.$ac_ext ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_objext" >&5 -$as_echo "$ac_cv_objext" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_objext" >&5 +printf "%s\n" "$ac_cv_objext" >&6; } OBJEXT=$ac_cv_objext ac_objext=$OBJEXT -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether we are using the GNU C compiler" >&5 -$as_echo_n "checking whether we are using the GNU C compiler... " >&6; } -if ${ac_cv_c_compiler_gnu+:} false; then : - $as_echo_n "(cached) " >&6 -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether the compiler supports GNU C" >&5 +printf %s "checking whether the compiler supports GNU C... " >&6; } +if test ${ac_cv_c_compiler_gnu+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ int -main () +main (void) { #ifndef __GNUC__ choke me @@ -3161,30 +3702,36 @@ main () return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : ac_compiler_gnu=yes -else - ac_compiler_gnu=no +else case e in #( + e) ac_compiler_gnu=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ac_cv_c_compiler_gnu=$ac_compiler_gnu - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_compiler_gnu" >&5 -$as_echo "$ac_cv_c_compiler_gnu" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_compiler_gnu" >&5 +printf "%s\n" "$ac_cv_c_compiler_gnu" >&6; } +ac_compiler_gnu=$ac_cv_c_compiler_gnu + if test $ac_compiler_gnu = yes; then GCC=yes else GCC= fi -ac_test_CFLAGS=${CFLAGS+set} +ac_test_CFLAGS=${CFLAGS+y} ac_save_CFLAGS=$CFLAGS -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether $CC accepts -g" >&5 -$as_echo_n "checking whether $CC accepts -g... " >&6; } -if ${ac_cv_prog_cc_g+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_save_c_werror_flag=$ac_c_werror_flag +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $CC accepts -g" >&5 +printf %s "checking whether $CC accepts -g... " >&6; } +if test ${ac_cv_prog_cc_g+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_save_c_werror_flag=$ac_c_werror_flag ac_c_werror_flag=yes ac_cv_prog_cc_g=no CFLAGS="-g" @@ -3192,57 +3739,63 @@ else /* end confdefs.h. */ int -main () +main (void) { ; return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : ac_cv_prog_cc_g=yes -else - CFLAGS="" +else case e in #( + e) CFLAGS="" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ int -main () +main (void) { ; return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : -else - ac_c_werror_flag=$ac_save_c_werror_flag +else case e in #( + e) ac_c_werror_flag=$ac_save_c_werror_flag CFLAGS="-g" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ int -main () +main (void) { ; return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : ac_cv_prog_cc_g=yes fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext - ac_c_werror_flag=$ac_save_c_werror_flag +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ac_c_werror_flag=$ac_save_c_werror_flag ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_g" >&5 -$as_echo "$ac_cv_prog_cc_g" >&6; } -if test "$ac_test_CFLAGS" = set; then +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_g" >&5 +printf "%s\n" "$ac_cv_prog_cc_g" >&6; } +if test $ac_test_CFLAGS; then CFLAGS=$ac_save_CFLAGS elif test $ac_cv_prog_cc_g = yes; then if test "$GCC" = yes; then @@ -3257,94 +3810,153 @@ else CFLAGS= fi fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $CC option to accept ISO C89" >&5 -$as_echo_n "checking for $CC option to accept ISO C89... " >&6; } -if ${ac_cv_prog_cc_c89+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_cv_prog_cc_c89=no +ac_prog_cc_stdc=no +if test x$ac_prog_cc_stdc = xno +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C11 features" >&5 +printf %s "checking for $CC option to enable C11 features... " >&6; } +if test ${ac_cv_prog_cc_c11+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_cv_prog_cc_c11=no ac_save_CC=$CC cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ -#include -#include -struct stat; -/* Most of the following tests are stolen from RCS 5.7's src/conf.sh. */ -struct buf { int x; }; -FILE * (*rcsopen) (struct buf *, struct stat *, int); -static char *e (p, i) - char **p; - int i; -{ - return p[i]; -} -static char *f (char * (*g) (char **, int), char **p, ...) -{ - char *s; - va_list v; - va_start (v,p); - s = g (p, va_arg (v,int)); - va_end (v); - return s; -} - -/* OSF 4.0 Compaq cc is some sort of almost-ANSI by default. It has - function prototypes and stuff, but not '\xHH' hex character constants. - These don't provoke an error unfortunately, instead are silently treated - as 'x'. The following induces an error, until -std is added to get - proper ANSI mode. Curiously '\x00'!='x' always comes out true, for an - array size at least. It's necessary to write '\x00'==0 to get something - that's true only with -std. */ -int osf4_cc_array ['\x00' == 0 ? 1 : -1]; +$ac_c_conftest_c11_program +_ACEOF +for ac_arg in '' -std=gnu11 +do + CC="$ac_save_CC $ac_arg" + if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_prog_cc_c11=$ac_arg +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam + test "x$ac_cv_prog_cc_c11" != "xno" && break +done +rm -f conftest.$ac_ext +CC=$ac_save_CC ;; +esac +fi -/* IBM C 6 for AIX is almost-ANSI by default, but it replaces macro parameters - inside strings and character constants. */ -#define FOO(x) 'x' -int xlc6_cc_array[FOO(a) == 'x' ? 1 : -1]; +if test "x$ac_cv_prog_cc_c11" = xno +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +printf "%s\n" "unsupported" >&6; } +else case e in #( + e) if test "x$ac_cv_prog_cc_c11" = x +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +printf "%s\n" "none needed" >&6; } +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c11" >&5 +printf "%s\n" "$ac_cv_prog_cc_c11" >&6; } + CC="$CC $ac_cv_prog_cc_c11" ;; +esac +fi + ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c11 + ac_prog_cc_stdc=c11 ;; +esac +fi +fi +if test x$ac_prog_cc_stdc = xno +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C99 features" >&5 +printf %s "checking for $CC option to enable C99 features... " >&6; } +if test ${ac_cv_prog_cc_c99+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_cv_prog_cc_c99=no +ac_save_CC=$CC +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +$ac_c_conftest_c99_program +_ACEOF +for ac_arg in '' -std=gnu99 -std=c99 -c99 -qlanglvl=extc1x -qlanglvl=extc99 -AC99 -D_STDC_C99= +do + CC="$ac_save_CC $ac_arg" + if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_prog_cc_c99=$ac_arg +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam + test "x$ac_cv_prog_cc_c99" != "xno" && break +done +rm -f conftest.$ac_ext +CC=$ac_save_CC ;; +esac +fi -int test (int i, double x); -struct s1 {int (*f) (int a);}; -struct s2 {int (*f) (double a);}; -int pairnames (int, char **, FILE *(*)(struct buf *, struct stat *, int), int, int); -int argc; -char **argv; -int -main () -{ -return f (e, argv, 0) != argv[0] || f (e, argv, 1) != argv[1]; - ; - return 0; -} +if test "x$ac_cv_prog_cc_c99" = xno +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +printf "%s\n" "unsupported" >&6; } +else case e in #( + e) if test "x$ac_cv_prog_cc_c99" = x +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +printf "%s\n" "none needed" >&6; } +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99" >&5 +printf "%s\n" "$ac_cv_prog_cc_c99" >&6; } + CC="$CC $ac_cv_prog_cc_c99" ;; +esac +fi + ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c99 + ac_prog_cc_stdc=c99 ;; +esac +fi +fi +if test x$ac_prog_cc_stdc = xno +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C89 features" >&5 +printf %s "checking for $CC option to enable C89 features... " >&6; } +if test ${ac_cv_prog_cc_c89+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_cv_prog_cc_c89=no +ac_save_CC=$CC +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +$ac_c_conftest_c89_program _ACEOF -for ac_arg in '' -qlanglvl=extc89 -qlanglvl=ansi -std \ - -Ae "-Aa -D_HPUX_SOURCE" "-Xc -D__EXTENSIONS__" +for ac_arg in '' -qlanglvl=extc89 -qlanglvl=ansi -std -Ae "-Aa -D_HPUX_SOURCE" "-Xc -D__EXTENSIONS__" do CC="$ac_save_CC $ac_arg" - if ac_fn_c_try_compile "$LINENO"; then : + if ac_fn_c_try_compile "$LINENO" +then : ac_cv_prog_cc_c89=$ac_arg fi -rm -f core conftest.err conftest.$ac_objext +rm -f core conftest.err conftest.$ac_objext conftest.beam test "x$ac_cv_prog_cc_c89" != "xno" && break done rm -f conftest.$ac_ext -CC=$ac_save_CC +CC=$ac_save_CC ;; +esac +fi +if test "x$ac_cv_prog_cc_c89" = xno +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +printf "%s\n" "unsupported" >&6; } +else case e in #( + e) if test "x$ac_cv_prog_cc_c89" = x +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +printf "%s\n" "none needed" >&6; } +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89" >&5 +printf "%s\n" "$ac_cv_prog_cc_c89" >&6; } + CC="$CC $ac_cv_prog_cc_c89" ;; +esac fi -# AC_CACHE_VAL -case "x$ac_cv_prog_cc_c89" in - x) - { $as_echo "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 -$as_echo "none needed" >&6; } ;; - xno) - { $as_echo "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 -$as_echo "unsupported" >&6; } ;; - *) - CC="$CC $ac_cv_prog_cc_c89" - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89" >&5 -$as_echo "$ac_cv_prog_cc_c89" >&6; } ;; + ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c89 + ac_prog_cc_stdc=c89 ;; esac -if test "x$ac_cv_prog_cc_c89" != xno; then : - +fi fi ac_ext=c @@ -3362,38 +3974,44 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu if test -n "$ac_tool_prefix"; then # Extract the first word of "${ac_tool_prefix}gcc", so it can be a program name with args. set dummy ${ac_tool_prefix}gcc; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_prog_CC+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test -n "$CC"; then +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_CC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_CC="${ac_tool_prefix}gcc" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -$as_echo "$CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf "%s\n" "$CC" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -3402,38 +4020,44 @@ if test -z "$ac_cv_prog_CC"; then ac_ct_CC=$CC # Extract the first word of "gcc", so it can be a program name with args. set dummy gcc; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_prog_ac_ct_CC+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test -n "$ac_ct_CC"; then +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_ac_ct_CC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$ac_ct_CC"; then ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_ac_ct_CC="gcc" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS -fi +fi ;; +esac fi ac_ct_CC=$ac_cv_prog_ac_ct_CC if test -n "$ac_ct_CC"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 -$as_echo "$ac_ct_CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 +printf "%s\n" "$ac_ct_CC" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi if test "x$ac_ct_CC" = x; then @@ -3441,8 +4065,8 @@ fi else case $cross_compiling:$ac_tool_warned in yes:) -{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 -$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} ac_tool_warned=yes ;; esac CC=$ac_ct_CC @@ -3455,38 +4079,44 @@ if test -z "$CC"; then if test -n "$ac_tool_prefix"; then # Extract the first word of "${ac_tool_prefix}cc", so it can be a program name with args. set dummy ${ac_tool_prefix}cc; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_prog_CC+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test -n "$CC"; then +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_CC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_CC="${ac_tool_prefix}cc" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -$as_echo "$CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf "%s\n" "$CC" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -3495,12 +4125,13 @@ fi if test -z "$CC"; then # Extract the first word of "cc", so it can be a program name with args. set dummy cc; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_prog_CC+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test -n "$CC"; then +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_CC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else ac_prog_rejected=no @@ -3508,15 +4139,19 @@ as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then - if test "$as_dir/$ac_word$ac_exec_ext" = "/usr/ucb/cc"; then + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then + if test "$as_dir$ac_word$ac_exec_ext" = "/usr/ucb/cc"; then ac_prog_rejected=yes continue fi ac_cv_prog_CC="cc" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -3532,18 +4167,19 @@ if test $ac_prog_rejected = yes; then # However, it has the same basename, so the bogon will be chosen # first if we set CC to just the basename; use the full file name. shift - ac_cv_prog_CC="$as_dir/$ac_word${1+' '}$@" + ac_cv_prog_CC="$as_dir$ac_word${1+' '}$@" fi fi -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -$as_echo "$CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf "%s\n" "$CC" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -3554,38 +4190,44 @@ if test -z "$CC"; then do # Extract the first word of "$ac_tool_prefix$ac_prog", so it can be a program name with args. set dummy $ac_tool_prefix$ac_prog; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_prog_CC+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test -n "$CC"; then +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_CC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$CC"; then ac_cv_prog_CC="$CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_CC="$ac_tool_prefix$ac_prog" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS -fi +fi ;; +esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -$as_echo "$CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf "%s\n" "$CC" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -3598,38 +4240,44 @@ if test -z "$CC"; then do # Extract the first word of "$ac_prog", so it can be a program name with args. set dummy $ac_prog; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_prog_ac_ct_CC+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test -n "$ac_ct_CC"; then +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_ac_ct_CC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$ac_ct_CC"; then ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_ac_ct_CC="$ac_prog" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS -fi +fi ;; +esac fi ac_ct_CC=$ac_cv_prog_ac_ct_CC if test -n "$ac_ct_CC"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 -$as_echo "$ac_ct_CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 +printf "%s\n" "$ac_ct_CC" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -3641,34 +4289,140 @@ done else case $cross_compiling:$ac_tool_warned in yes:) -{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 -$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + CC=$ac_ct_CC + fi +fi + +fi +if test -z "$CC"; then + if test -n "$ac_tool_prefix"; then + # Extract the first word of "${ac_tool_prefix}clang", so it can be a program name with args. +set dummy ${ac_tool_prefix}clang; ac_word=$2 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_CC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$CC"; then + ac_cv_prog_CC="$CC" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then + ac_cv_prog_CC="${ac_tool_prefix}clang" + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi ;; +esac +fi +CC=$ac_cv_prog_CC +if test -n "$CC"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf "%s\n" "$CC" >&6; } +else + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } +fi + + +fi +if test -z "$ac_cv_prog_CC"; then + ac_ct_CC=$CC + # Extract the first word of "clang", so it can be a program name with args. +set dummy clang; ac_word=$2 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_ac_ct_CC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$ac_ct_CC"; then + ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_CC="clang" + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi ;; +esac +fi +ac_ct_CC=$ac_cv_prog_ac_ct_CC +if test -n "$ac_ct_CC"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 +printf "%s\n" "$ac_ct_CC" >&6; } +else + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } +fi + + if test "x$ac_ct_CC" = x; then + CC="" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} ac_tool_warned=yes ;; esac CC=$ac_ct_CC fi +else + CC="$ac_cv_prog_CC" fi fi -test -z "$CC" && { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +test -z "$CC" && { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "no acceptable C compiler found in \$PATH -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } # Provide some information about the compiler. -$as_echo "$as_me:${as_lineno-$LINENO}: checking for C compiler version" >&5 +printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for C compiler version" >&5 set X $ac_compile ac_compiler=$2 -for ac_option in --version -v -V -qversion; do +for ac_option in --version -v -V -qversion -version; do { { ac_try="$ac_compiler $ac_option >&5" case "(($ac_try" in *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -$as_echo "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_compiler $ac_option >&5") 2>conftest.err ac_status=$? if test -s conftest.err; then @@ -3678,20 +4432,21 @@ $as_echo "$ac_try_echo"; } >&5 cat conftest.er1 >&5 fi rm -f conftest.er1 conftest.err - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } done -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether we are using the GNU C compiler" >&5 -$as_echo_n "checking whether we are using the GNU C compiler... " >&6; } -if ${ac_cv_c_compiler_gnu+:} false; then : - $as_echo_n "(cached) " >&6 -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether the compiler supports GNU C" >&5 +printf %s "checking whether the compiler supports GNU C... " >&6; } +if test ${ac_cv_c_compiler_gnu+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ int -main () +main (void) { #ifndef __GNUC__ choke me @@ -3701,30 +4456,36 @@ main () return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : ac_compiler_gnu=yes -else - ac_compiler_gnu=no +else case e in #( + e) ac_compiler_gnu=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ac_cv_c_compiler_gnu=$ac_compiler_gnu - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_compiler_gnu" >&5 -$as_echo "$ac_cv_c_compiler_gnu" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_compiler_gnu" >&5 +printf "%s\n" "$ac_cv_c_compiler_gnu" >&6; } +ac_compiler_gnu=$ac_cv_c_compiler_gnu + if test $ac_compiler_gnu = yes; then GCC=yes else GCC= fi -ac_test_CFLAGS=${CFLAGS+set} +ac_test_CFLAGS=${CFLAGS+y} ac_save_CFLAGS=$CFLAGS -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether $CC accepts -g" >&5 -$as_echo_n "checking whether $CC accepts -g... " >&6; } -if ${ac_cv_prog_cc_g+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_save_c_werror_flag=$ac_c_werror_flag +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $CC accepts -g" >&5 +printf %s "checking whether $CC accepts -g... " >&6; } +if test ${ac_cv_prog_cc_g+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_save_c_werror_flag=$ac_c_werror_flag ac_c_werror_flag=yes ac_cv_prog_cc_g=no CFLAGS="-g" @@ -3732,57 +4493,63 @@ else /* end confdefs.h. */ int -main () +main (void) { ; return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : ac_cv_prog_cc_g=yes -else - CFLAGS="" +else case e in #( + e) CFLAGS="" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ int -main () +main (void) { ; return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : -else - ac_c_werror_flag=$ac_save_c_werror_flag +else case e in #( + e) ac_c_werror_flag=$ac_save_c_werror_flag CFLAGS="-g" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ int -main () +main (void) { ; return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : ac_cv_prog_cc_g=yes fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext - ac_c_werror_flag=$ac_save_c_werror_flag +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ac_c_werror_flag=$ac_save_c_werror_flag ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_g" >&5 -$as_echo "$ac_cv_prog_cc_g" >&6; } -if test "$ac_test_CFLAGS" = set; then +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_g" >&5 +printf "%s\n" "$ac_cv_prog_cc_g" >&6; } +if test $ac_test_CFLAGS; then CFLAGS=$ac_save_CFLAGS elif test $ac_cv_prog_cc_g = yes; then if test "$GCC" = yes; then @@ -3797,342 +4564,237 @@ else CFLAGS= fi fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $CC option to accept ISO C89" >&5 -$as_echo_n "checking for $CC option to accept ISO C89... " >&6; } -if ${ac_cv_prog_cc_c89+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_cv_prog_cc_c89=no +ac_prog_cc_stdc=no +if test x$ac_prog_cc_stdc = xno +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C11 features" >&5 +printf %s "checking for $CC option to enable C11 features... " >&6; } +if test ${ac_cv_prog_cc_c11+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_cv_prog_cc_c11=no ac_save_CC=$CC cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ -#include -#include -struct stat; -/* Most of the following tests are stolen from RCS 5.7's src/conf.sh. */ -struct buf { int x; }; -FILE * (*rcsopen) (struct buf *, struct stat *, int); -static char *e (p, i) - char **p; - int i; -{ - return p[i]; -} -static char *f (char * (*g) (char **, int), char **p, ...) -{ - char *s; - va_list v; - va_start (v,p); - s = g (p, va_arg (v,int)); - va_end (v); - return s; -} - -/* OSF 4.0 Compaq cc is some sort of almost-ANSI by default. It has - function prototypes and stuff, but not '\xHH' hex character constants. - These don't provoke an error unfortunately, instead are silently treated - as 'x'. The following induces an error, until -std is added to get - proper ANSI mode. Curiously '\x00'!='x' always comes out true, for an - array size at least. It's necessary to write '\x00'==0 to get something - that's true only with -std. */ -int osf4_cc_array ['\x00' == 0 ? 1 : -1]; - -/* IBM C 6 for AIX is almost-ANSI by default, but it replaces macro parameters - inside strings and character constants. */ -#define FOO(x) 'x' -int xlc6_cc_array[FOO(a) == 'x' ? 1 : -1]; - -int test (int i, double x); -struct s1 {int (*f) (int a);}; -struct s2 {int (*f) (double a);}; -int pairnames (int, char **, FILE *(*)(struct buf *, struct stat *, int), int, int); -int argc; -char **argv; -int -main () -{ -return f (e, argv, 0) != argv[0] || f (e, argv, 1) != argv[1]; - ; - return 0; -} +$ac_c_conftest_c11_program _ACEOF -for ac_arg in '' -qlanglvl=extc89 -qlanglvl=ansi -std \ - -Ae "-Aa -D_HPUX_SOURCE" "-Xc -D__EXTENSIONS__" +for ac_arg in '' -std=gnu11 do CC="$ac_save_CC $ac_arg" - if ac_fn_c_try_compile "$LINENO"; then : - ac_cv_prog_cc_c89=$ac_arg + if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_prog_cc_c11=$ac_arg fi -rm -f core conftest.err conftest.$ac_objext - test "x$ac_cv_prog_cc_c89" != "xno" && break +rm -f core conftest.err conftest.$ac_objext conftest.beam + test "x$ac_cv_prog_cc_c11" != "xno" && break done rm -f conftest.$ac_ext -CC=$ac_save_CC +CC=$ac_save_CC ;; +esac +fi +if test "x$ac_cv_prog_cc_c11" = xno +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +printf "%s\n" "unsupported" >&6; } +else case e in #( + e) if test "x$ac_cv_prog_cc_c11" = x +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +printf "%s\n" "none needed" >&6; } +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c11" >&5 +printf "%s\n" "$ac_cv_prog_cc_c11" >&6; } + CC="$CC $ac_cv_prog_cc_c11" ;; +esac fi -# AC_CACHE_VAL -case "x$ac_cv_prog_cc_c89" in - x) - { $as_echo "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 -$as_echo "none needed" >&6; } ;; - xno) - { $as_echo "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 -$as_echo "unsupported" >&6; } ;; - *) - CC="$CC $ac_cv_prog_cc_c89" - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89" >&5 -$as_echo "$ac_cv_prog_cc_c89" >&6; } ;; + ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c11 + ac_prog_cc_stdc=c11 ;; esac -if test "x$ac_cv_prog_cc_c89" != xno; then : - fi - -ac_ext=c -ac_cpp='$CPP $CPPFLAGS' -ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' -ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' -ac_compiler_gnu=$ac_cv_c_compiler_gnu - - -am_cv_prog_cc_stdc=$ac_cv_prog_cc_stdc - - - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $CC option to accept ISO C99" >&5 -$as_echo_n "checking for $CC option to accept ISO C99... " >&6; } -if ${ac_cv_prog_cc_c99+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_cv_prog_cc_c99=no +fi +if test x$ac_prog_cc_stdc = xno +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C99 features" >&5 +printf %s "checking for $CC option to enable C99 features... " >&6; } +if test ${ac_cv_prog_cc_c99+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_cv_prog_cc_c99=no ac_save_CC=$CC cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ -#include -#include -#include -#include -#include - -// Check varargs macros. These examples are taken from C99 6.10.3.5. -#define debug(...) fprintf (stderr, __VA_ARGS__) -#define showlist(...) puts (#__VA_ARGS__) -#define report(test,...) ((test) ? puts (#test) : printf (__VA_ARGS__)) -static void -test_varargs_macros (void) -{ - int x = 1234; - int y = 5678; - debug ("Flag"); - debug ("X = %d\n", x); - showlist (The first, second, and third items.); - report (x>y, "x is %d but y is %d", x, y); -} - -// Check long long types. -#define BIG64 18446744073709551615ull -#define BIG32 4294967295ul -#define BIG_OK (BIG64 / BIG32 == 4294967297ull && BIG64 % BIG32 == 0) -#if !BIG_OK - your preprocessor is broken; -#endif -#if BIG_OK -#else - your preprocessor is broken; -#endif -static long long int bignum = -9223372036854775807LL; -static unsigned long long int ubignum = BIG64; - -struct incomplete_array -{ - int datasize; - double data[]; -}; - -struct named_init { - int number; - const wchar_t *name; - double average; -}; - -typedef const char *ccp; - -static inline int -test_restrict (ccp restrict text) -{ - // See if C++-style comments work. - // Iterate through items via the restricted pointer. - // Also check for declarations in for loops. - for (unsigned int i = 0; *(text+i) != '\0'; ++i) - continue; - return 0; -} - -// Check varargs and va_copy. -static void -test_varargs (const char *format, ...) -{ - va_list args; - va_start (args, format); - va_list args_copy; - va_copy (args_copy, args); - - const char *str; - int number; - float fnumber; - - while (*format) - { - switch (*format++) - { - case 's': // string - str = va_arg (args_copy, const char *); - break; - case 'd': // int - number = va_arg (args_copy, int); - break; - case 'f': // float - fnumber = va_arg (args_copy, double); - break; - default: - break; - } - } - va_end (args_copy); - va_end (args); -} - -int -main () -{ - - // Check bool. - _Bool success = false; - - // Check restrict. - if (test_restrict ("String literal") == 0) - success = true; - char *restrict newvar = "Another string"; - - // Check varargs. - test_varargs ("s, d' f .", "string", 65, 34.234); - test_varargs_macros (); - - // Check flexible array members. - struct incomplete_array *ia = - malloc (sizeof (struct incomplete_array) + (sizeof (double) * 10)); - ia->datasize = 10; - for (int i = 0; i < ia->datasize; ++i) - ia->data[i] = i * 1.234; - - // Check named initializers. - struct named_init ni = { - .number = 34, - .name = L"Test wide string", - .average = 543.34343, - }; - - ni.number = 58; - - int dynamic_array[ni.number]; - dynamic_array[ni.number - 1] = 543; - - // work around unused variable warnings - return (!success || bignum == 0LL || ubignum == 0uLL || newvar[0] == 'x' - || dynamic_array[ni.number - 1] != 543); - - ; - return 0; -} +$ac_c_conftest_c99_program _ACEOF -for ac_arg in '' -std=gnu99 -std=c99 -c99 -AC99 -D_STDC_C99= -qlanglvl=extc99 +for ac_arg in '' -std=gnu99 -std=c99 -c99 -qlanglvl=extc1x -qlanglvl=extc99 -AC99 -D_STDC_C99= do CC="$ac_save_CC $ac_arg" - if ac_fn_c_try_compile "$LINENO"; then : + if ac_fn_c_try_compile "$LINENO" +then : ac_cv_prog_cc_c99=$ac_arg fi -rm -f core conftest.err conftest.$ac_objext +rm -f core conftest.err conftest.$ac_objext conftest.beam test "x$ac_cv_prog_cc_c99" != "xno" && break done rm -f conftest.$ac_ext -CC=$ac_save_CC +CC=$ac_save_CC ;; +esac +fi +if test "x$ac_cv_prog_cc_c99" = xno +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +printf "%s\n" "unsupported" >&6; } +else case e in #( + e) if test "x$ac_cv_prog_cc_c99" = x +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +printf "%s\n" "none needed" >&6; } +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99" >&5 +printf "%s\n" "$ac_cv_prog_cc_c99" >&6; } + CC="$CC $ac_cv_prog_cc_c99" ;; +esac fi -# AC_CACHE_VAL -case "x$ac_cv_prog_cc_c99" in - x) - { $as_echo "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 -$as_echo "none needed" >&6; } ;; - xno) - { $as_echo "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 -$as_echo "unsupported" >&6; } ;; - *) - CC="$CC $ac_cv_prog_cc_c99" - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99" >&5 -$as_echo "$ac_cv_prog_cc_c99" >&6; } ;; + ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c99 + ac_prog_cc_stdc=c99 ;; +esac +fi +fi +if test x$ac_prog_cc_stdc = xno +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C89 features" >&5 +printf %s "checking for $CC option to enable C89 features... " >&6; } +if test ${ac_cv_prog_cc_c89+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_cv_prog_cc_c89=no +ac_save_CC=$CC +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +$ac_c_conftest_c89_program +_ACEOF +for ac_arg in '' -qlanglvl=extc89 -qlanglvl=ansi -std -Ae "-Aa -D_HPUX_SOURCE" "-Xc -D__EXTENSIONS__" +do + CC="$ac_save_CC $ac_arg" + if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_prog_cc_c89=$ac_arg +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam + test "x$ac_cv_prog_cc_c89" != "xno" && break +done +rm -f conftest.$ac_ext +CC=$ac_save_CC ;; esac -if test "x$ac_cv_prog_cc_c99" != xno; then : +fi +if test "x$ac_cv_prog_cc_c89" = xno +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +printf "%s\n" "unsupported" >&6; } +else case e in #( + e) if test "x$ac_cv_prog_cc_c89" = x +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +printf "%s\n" "none needed" >&6; } +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89" >&5 +printf "%s\n" "$ac_cv_prog_cc_c89" >&6; } + CC="$CC $ac_cv_prog_cc_c89" ;; +esac +fi + ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c89 + ac_prog_cc_stdc=c89 ;; +esac +fi fi +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + +am_cv_prog_cc_stdc=$ac_cv_prog_cc_stdc -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for library containing crypt" >&5 -$as_echo_n "checking for library containing crypt... " >&6; } -if ${ac_cv_search_crypt+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_func_search_save_LIBS=$LIBS + + + +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for library containing crypt" >&5 +printf %s "checking for library containing crypt... " >&6; } +if test ${ac_cv_search_crypt+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_func_search_save_LIBS=$LIBS cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ #ifdef __cplusplus extern "C" #endif -char crypt (); +char crypt (void); int -main () +main (void) { return crypt (); ; return 0; } _ACEOF -for ac_lib in '' descrypt crypt; do +for ac_lib in '' descrypt crypt +do if test -z "$ac_lib"; then ac_res="none required" else ac_res=-l$ac_lib LIBS="-l$ac_lib $ac_func_search_save_LIBS" fi - if ac_fn_c_try_link "$LINENO"; then : + if ac_fn_c_try_link "$LINENO" +then : ac_cv_search_crypt=$ac_res fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext - if ${ac_cv_search_crypt+:} false; then : + if test ${ac_cv_search_crypt+y} +then : break fi done -if ${ac_cv_search_crypt+:} false; then : +if test ${ac_cv_search_crypt+y} +then : -else - ac_cv_search_crypt=no +else case e in #( + e) ac_cv_search_crypt=no ;; +esac fi rm conftest.$ac_ext -LIBS=$ac_func_search_save_LIBS +LIBS=$ac_func_search_save_LIBS ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_crypt" >&5 -$as_echo "$ac_cv_search_crypt" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_crypt" >&5 +printf "%s\n" "$ac_cv_search_crypt" >&6; } ac_res=$ac_cv_search_crypt -if test "$ac_res" != no; then : +if test "$ac_res" != no +then : test "$ac_res" = "none required" || LIBS="$ac_res $LIBS" -else - as_fn_error $? "Unable to find library containing crypt()" "$LINENO" 5 +else case e in #( + e) as_fn_error $? "Unable to find library containing crypt()" "$LINENO" 5 ;; +esac fi @@ -4140,460 +4802,491 @@ fi # Most operating systems have gethostbyname() in the default searched # libraries (i.e. libc): ac_fn_c_check_func "$LINENO" "gethostbyname" "ac_cv_func_gethostbyname" -if test "x$ac_cv_func_gethostbyname" = xyes; then : - -else - # Some OSes (eg. Solaris) place it in libnsl: - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for gethostbyname in -lnsl" >&5 -$as_echo_n "checking for gethostbyname in -lnsl... " >&6; } -if ${ac_cv_lib_nsl_gethostbyname+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS +if test "x$ac_cv_func_gethostbyname" = xyes +then : + +else case e in #( + e) # Some OSes (eg. Solaris) place it in libnsl: + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for gethostbyname in -lnsl" >&5 +printf %s "checking for gethostbyname in -lnsl... " >&6; } +if test ${ac_cv_lib_nsl_gethostbyname+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lnsl $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ #ifdef __cplusplus extern "C" #endif -char gethostbyname (); +char gethostbyname (void); int -main () +main (void) { return gethostbyname (); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : ac_cv_lib_nsl_gethostbyname=yes -else - ac_cv_lib_nsl_gethostbyname=no +else case e in #( + e) ac_cv_lib_nsl_gethostbyname=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_nsl_gethostbyname" >&5 -$as_echo "$ac_cv_lib_nsl_gethostbyname" >&6; } -if test "x$ac_cv_lib_nsl_gethostbyname" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_LIBNSL 1 -_ACEOF +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_nsl_gethostbyname" >&5 +printf "%s\n" "$ac_cv_lib_nsl_gethostbyname" >&6; } +if test "x$ac_cv_lib_nsl_gethostbyname" = xyes +then : + printf "%s\n" "#define HAVE_LIBNSL 1" >>confdefs.h LIBS="-lnsl $LIBS" -else - # Some strange OSes (SINIX) have it in libsocket: - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for gethostbyname in -lsocket" >&5 -$as_echo_n "checking for gethostbyname in -lsocket... " >&6; } -if ${ac_cv_lib_socket_gethostbyname+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS +else case e in #( + e) # Some strange OSes (SINIX) have it in libsocket: + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for gethostbyname in -lsocket" >&5 +printf %s "checking for gethostbyname in -lsocket... " >&6; } +if test ${ac_cv_lib_socket_gethostbyname+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lsocket $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ #ifdef __cplusplus extern "C" #endif -char gethostbyname (); +char gethostbyname (void); int -main () +main (void) { return gethostbyname (); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : ac_cv_lib_socket_gethostbyname=yes -else - ac_cv_lib_socket_gethostbyname=no +else case e in #( + e) ac_cv_lib_socket_gethostbyname=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_socket_gethostbyname" >&5 -$as_echo "$ac_cv_lib_socket_gethostbyname" >&6; } -if test "x$ac_cv_lib_socket_gethostbyname" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_LIBSOCKET 1 -_ACEOF +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_socket_gethostbyname" >&5 +printf "%s\n" "$ac_cv_lib_socket_gethostbyname" >&6; } +if test "x$ac_cv_lib_socket_gethostbyname" = xyes +then : + printf "%s\n" "#define HAVE_LIBSOCKET 1" >>confdefs.h LIBS="-lsocket $LIBS" -else - # Unfortunately libsocket sometimes depends on libnsl. +else case e in #( + e) # Unfortunately libsocket sometimes depends on libnsl. # AC_CHECK_LIB's API is essentially broken so the following # ugliness is necessary: - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for gethostbyname in -lsocket" >&5 -$as_echo_n "checking for gethostbyname in -lsocket... " >&6; } -if ${ac_cv_lib_socket_gethostbyname+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for gethostbyname in -lsocket" >&5 +printf %s "checking for gethostbyname in -lsocket... " >&6; } +if test ${ac_cv_lib_socket_gethostbyname+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lsocket -lnsl $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ #ifdef __cplusplus extern "C" #endif -char gethostbyname (); +char gethostbyname (void); int -main () +main (void) { return gethostbyname (); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : ac_cv_lib_socket_gethostbyname=yes -else - ac_cv_lib_socket_gethostbyname=no +else case e in #( + e) ac_cv_lib_socket_gethostbyname=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_socket_gethostbyname" >&5 -$as_echo "$ac_cv_lib_socket_gethostbyname" >&6; } -if test "x$ac_cv_lib_socket_gethostbyname" = xyes; then : +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_socket_gethostbyname" >&5 +printf "%s\n" "$ac_cv_lib_socket_gethostbyname" >&6; } +if test "x$ac_cv_lib_socket_gethostbyname" = xyes +then : LIBS="-lsocket -lnsl $LIBS" -else - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for gethostbyname in -lresolv" >&5 -$as_echo_n "checking for gethostbyname in -lresolv... " >&6; } -if ${ac_cv_lib_resolv_gethostbyname+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for gethostbyname in -lresolv" >&5 +printf %s "checking for gethostbyname in -lresolv... " >&6; } +if test ${ac_cv_lib_resolv_gethostbyname+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lresolv $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ #ifdef __cplusplus extern "C" #endif -char gethostbyname (); +char gethostbyname (void); int -main () +main (void) { return gethostbyname (); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : ac_cv_lib_resolv_gethostbyname=yes -else - ac_cv_lib_resolv_gethostbyname=no +else case e in #( + e) ac_cv_lib_resolv_gethostbyname=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_resolv_gethostbyname" >&5 -$as_echo "$ac_cv_lib_resolv_gethostbyname" >&6; } -if test "x$ac_cv_lib_resolv_gethostbyname" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_LIBRESOLV 1 -_ACEOF +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_resolv_gethostbyname" >&5 +printf "%s\n" "$ac_cv_lib_resolv_gethostbyname" >&6; } +if test "x$ac_cv_lib_resolv_gethostbyname" = xyes +then : + printf "%s\n" "#define HAVE_LIBRESOLV 1" >>confdefs.h LIBS="-lresolv $LIBS" fi - + ;; +esac fi - + ;; +esac fi - + ;; +esac fi - + ;; +esac fi ac_fn_c_check_func "$LINENO" "socket" "ac_cv_func_socket" -if test "x$ac_cv_func_socket" = xyes; then : - -else - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for socket in -lsocket" >&5 -$as_echo_n "checking for socket in -lsocket... " >&6; } -if ${ac_cv_lib_socket_socket+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS +if test "x$ac_cv_func_socket" = xyes +then : + +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for socket in -lsocket" >&5 +printf %s "checking for socket in -lsocket... " >&6; } +if test ${ac_cv_lib_socket_socket+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lsocket $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ #ifdef __cplusplus extern "C" #endif -char socket (); +char socket (void); int -main () +main (void) { return socket (); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : ac_cv_lib_socket_socket=yes -else - ac_cv_lib_socket_socket=no +else case e in #( + e) ac_cv_lib_socket_socket=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_socket_socket" >&5 -$as_echo "$ac_cv_lib_socket_socket" >&6; } -if test "x$ac_cv_lib_socket_socket" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_LIBSOCKET 1 -_ACEOF +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_socket_socket" >&5 +printf "%s\n" "$ac_cv_lib_socket_socket" >&6; } +if test "x$ac_cv_lib_socket_socket" = xyes +then : + printf "%s\n" "#define HAVE_LIBSOCKET 1" >>confdefs.h LIBS="-lsocket $LIBS" -else - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for socket in -lsocket" >&5 -$as_echo_n "checking for socket in -lsocket... " >&6; } -if ${ac_cv_lib_socket_socket+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for socket in -lsocket" >&5 +printf %s "checking for socket in -lsocket... " >&6; } +if test ${ac_cv_lib_socket_socket+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lsocket -lnsl $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ #ifdef __cplusplus extern "C" #endif -char socket (); +char socket (void); int -main () +main (void) { return socket (); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : ac_cv_lib_socket_socket=yes -else - ac_cv_lib_socket_socket=no +else case e in #( + e) ac_cv_lib_socket_socket=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_socket_socket" >&5 -$as_echo "$ac_cv_lib_socket_socket" >&6; } -if test "x$ac_cv_lib_socket_socket" = xyes; then : +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_socket_socket" >&5 +printf "%s\n" "$ac_cv_lib_socket_socket" >&6; } +if test "x$ac_cv_lib_socket_socket" = xyes +then : LIBS="-lsocket -lnsl $LIBS" fi - + ;; +esac fi - + ;; +esac fi -ac_ext=c -ac_cpp='$CPP $CPPFLAGS' -ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' -ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' -ac_compiler_gnu=$ac_cv_c_compiler_gnu -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking how to run the C preprocessor" >&5 -$as_echo_n "checking how to run the C preprocessor... " >&6; } -# On Suns, sometimes $CPP names a directory. -if test -n "$CPP" && test -d "$CPP"; then - CPP= -fi -if test -z "$CPP"; then - if ${ac_cv_prog_CPP+:} false; then : - $as_echo_n "(cached) " >&6 -else - # Double quotes because CPP needs to be expanded - for CPP in "$CC -E" "$CC -E -traditional-cpp" "/lib/cpp" - do - ac_preproc_ok=false -for ac_c_preproc_warn_flag in '' yes +ac_header= ac_cache= +for ac_item in $ac_header_c_list do - # Use a header file that comes with gcc, so configuring glibc - # with a fresh cross-compiler works. - # Prefer to if __STDC__ is defined, since - # exists even on freestanding compilers. - # On the NeXT, cc -E runs the code through the compiler's parser, - # not just through cpp. "Syntax error" is here to catch this case. - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#ifdef __STDC__ -# include -#else -# include -#endif - Syntax error -_ACEOF -if ac_fn_c_try_cpp "$LINENO"; then : + if test $ac_cache; then + ac_fn_c_check_header_compile "$LINENO" $ac_header ac_cv_header_$ac_cache "$ac_includes_default" + if eval test \"x\$ac_cv_header_$ac_cache\" = xyes; then + printf "%s\n" "#define $ac_item 1" >> confdefs.h + fi + ac_header= ac_cache= + elif test $ac_header; then + ac_cache=$ac_item + else + ac_header=$ac_item + fi +done -else - # Broken: fails on valid input. -continue -fi -rm -f conftest.err conftest.i conftest.$ac_ext - # OK, works on sane cases. Now check whether nonexistent headers - # can be detected and how. - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#include -_ACEOF -if ac_fn_c_try_cpp "$LINENO"; then : - # Broken: success on invalid input. -continue -else - # Passes both tests. -ac_preproc_ok=: -break -fi -rm -f conftest.err conftest.i conftest.$ac_ext -done -# Because of `break', _AC_PREPROC_IFELSE's cleaning code was skipped. -rm -f conftest.i conftest.err conftest.$ac_ext -if $ac_preproc_ok; then : - break -fi - done - ac_cv_prog_CPP=$CPP + + + + +if test $ac_cv_header_stdlib_h = yes && test $ac_cv_header_string_h = yes +then : + +printf "%s\n" "#define STDC_HEADERS 1" >>confdefs.h fi - CPP=$ac_cv_prog_CPP -else - ac_cv_prog_CPP=$CPP -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $CPP" >&5 -$as_echo "$CPP" >&6; } -ac_preproc_ok=false -for ac_c_preproc_warn_flag in '' yes -do - # Use a header file that comes with gcc, so configuring glibc - # with a fresh cross-compiler works. - # Prefer to if __STDC__ is defined, since - # exists even on freestanding compilers. - # On the NeXT, cc -E runs the code through the compiler's parser, - # not just through cpp. "Syntax error" is here to catch this case. - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#ifdef __STDC__ -# include -#else -# include -#endif - Syntax error -_ACEOF -if ac_fn_c_try_cpp "$LINENO"; then : +ac_fn_c_check_header_compile "$LINENO" "pthread.h" "ac_cv_header_pthread_h" "$ac_includes_default" +if test "x$ac_cv_header_pthread_h" = xyes +then : + printf "%s\n" "#define HAVE_PTHREAD_H 1" >>confdefs.h -else - # Broken: fails on valid input. -continue fi -rm -f conftest.err conftest.i conftest.$ac_ext - # OK, works on sane cases. Now check whether nonexistent headers - # can be detected and how. - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for library containing pthread_create" >&5 +printf %s "checking for library containing pthread_create... " >&6; } +if test ${ac_cv_search_pthread_create+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_func_search_save_LIBS=$LIBS +cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ -#include + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char pthread_create (void); +int +main (void) +{ +return pthread_create (); + ; + return 0; +} _ACEOF -if ac_fn_c_try_cpp "$LINENO"; then : - # Broken: success on invalid input. -continue -else - # Passes both tests. -ac_preproc_ok=: -break +for ac_lib in '' pthread +do + if test -z "$ac_lib"; then + ac_res="none required" + else + ac_res=-l$ac_lib + LIBS="-l$ac_lib $ac_func_search_save_LIBS" + fi + if ac_fn_c_try_link "$LINENO" +then : + ac_cv_search_pthread_create=$ac_res +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext + if test ${ac_cv_search_pthread_create+y} +then : + break fi -rm -f conftest.err conftest.i conftest.$ac_ext - done -# Because of `break', _AC_PREPROC_IFELSE's cleaning code was skipped. -rm -f conftest.i conftest.err conftest.$ac_ext -if $ac_preproc_ok; then : +if test ${ac_cv_search_pthread_create+y} +then : -else - { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} -as_fn_error $? "C preprocessor \"$CPP\" fails sanity check -See \`config.log' for more details" "$LINENO" 5; } +else case e in #( + e) ac_cv_search_pthread_create=no ;; +esac +fi +rm conftest.$ac_ext +LIBS=$ac_func_search_save_LIBS ;; +esac fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_pthread_create" >&5 +printf "%s\n" "$ac_cv_search_pthread_create" >&6; } +ac_res=$ac_cv_search_pthread_create +if test "$ac_res" != no +then : + test "$ac_res" = "none required" || LIBS="$ac_res $LIBS" -ac_ext=c -ac_cpp='$CPP $CPPFLAGS' -ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' -ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' -ac_compiler_gnu=$ac_cv_c_compiler_gnu +printf "%s\n" "#define HAVE_PTHREAD 1" >>confdefs.h -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for grep that handles long lines and -e" >&5 -$as_echo_n "checking for grep that handles long lines and -e... " >&6; } -if ${ac_cv_path_GREP+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test -z "$GREP"; then + +else case e in #( + e) + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: pthreads not found; async password verification disabled" >&5 +printf "%s\n" "$as_me: WARNING: pthreads not found; async password verification disabled" >&2;} + ;; +esac +fi + + +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for grep that handles long lines and -e" >&5 +printf %s "checking for grep that handles long lines and -e... " >&6; } +if test ${ac_cv_path_GREP+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -z "$GREP"; then ac_path_GREP_found=false # Loop through the user's path and test for each of PROGNAME-LIST as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH$PATH_SEPARATOR/usr/xpg4/bin do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. - for ac_prog in grep ggrep; do + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + for ac_prog in grep ggrep + do for ac_exec_ext in '' $ac_executable_extensions; do - ac_path_GREP="$as_dir/$ac_prog$ac_exec_ext" + ac_path_GREP="$as_dir$ac_prog$ac_exec_ext" as_fn_executable_p "$ac_path_GREP" || continue # Check for GNU ac_path_GREP and select it if it is found. # Check for GNU $ac_path_GREP -case `"$ac_path_GREP" --version 2>&1` in +case `"$ac_path_GREP" --version 2>&1` in #( *GNU*) ac_cv_path_GREP="$ac_path_GREP" ac_path_GREP_found=:;; +#( *) ac_count=0 - $as_echo_n 0123456789 >"conftest.in" + printf %s 0123456789 >"conftest.in" while : do cat "conftest.in" "conftest.in" >"conftest.tmp" mv "conftest.tmp" "conftest.in" cp "conftest.in" "conftest.nl" - $as_echo 'GREP' >> "conftest.nl" + printf "%s\n" 'GREP' >> "conftest.nl" "$ac_path_GREP" -e 'GREP$' -e '-(cannot match)-' < "conftest.nl" >"conftest.out" 2>/dev/null || break diff "conftest.out" "conftest.nl" >/dev/null 2>&1 || break as_fn_arith $ac_count + 1 && ac_count=$as_val @@ -4619,19 +5312,24 @@ IFS=$as_save_IFS else ac_cv_path_GREP=$GREP fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_GREP" >&5 -$as_echo "$ac_cv_path_GREP" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_GREP" >&5 +printf "%s\n" "$ac_cv_path_GREP" >&6; } GREP="$ac_cv_path_GREP" -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for egrep" >&5 -$as_echo_n "checking for egrep... " >&6; } -if ${ac_cv_path_EGREP+:} false; then : - $as_echo_n "(cached) " >&6 -else - if echo a | $GREP -E '(a|b)' >/dev/null 2>&1 +# Autoupdate added the next two lines to ensure that your configure +# script's behavior did not change. They are probably safe to remove. + +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for egrep" >&5 +printf %s "checking for egrep... " >&6; } +if test ${ac_cv_path_EGREP+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if echo a | $GREP -E '(a|b)' >/dev/null 2>&1 then ac_cv_path_EGREP="$GREP -E" else if test -z "$EGREP"; then @@ -4641,25 +5339,31 @@ else for as_dir in $PATH$PATH_SEPARATOR/usr/xpg4/bin do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. - for ac_prog in egrep; do + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + for ac_prog in egrep + do for ac_exec_ext in '' $ac_executable_extensions; do - ac_path_EGREP="$as_dir/$ac_prog$ac_exec_ext" + ac_path_EGREP="$as_dir$ac_prog$ac_exec_ext" as_fn_executable_p "$ac_path_EGREP" || continue # Check for GNU ac_path_EGREP and select it if it is found. # Check for GNU $ac_path_EGREP -case `"$ac_path_EGREP" --version 2>&1` in +case `"$ac_path_EGREP" --version 2>&1` in #( *GNU*) ac_cv_path_EGREP="$ac_path_EGREP" ac_path_EGREP_found=:;; +#( *) ac_count=0 - $as_echo_n 0123456789 >"conftest.in" + printf %s 0123456789 >"conftest.in" while : do cat "conftest.in" "conftest.in" >"conftest.tmp" mv "conftest.tmp" "conftest.in" cp "conftest.in" "conftest.nl" - $as_echo 'EGREP' >> "conftest.nl" + printf "%s\n" 'EGREP' >> "conftest.nl" "$ac_path_EGREP" 'EGREP$' < "conftest.nl" >"conftest.out" 2>/dev/null || break diff "conftest.out" "conftest.nl" >/dev/null 2>&1 || break as_fn_arith $ac_count + 1 && ac_count=$as_val @@ -4686,162 +5390,86 @@ else ac_cv_path_EGREP=$EGREP fi - fi + fi ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_EGREP" >&5 -$as_echo "$ac_cv_path_EGREP" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_EGREP" >&5 +printf "%s\n" "$ac_cv_path_EGREP" >&6; } EGREP="$ac_cv_path_EGREP" + EGREP_TRADITIONAL=$EGREP + ac_cv_path_EGREP_TRADITIONAL=$EGREP -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for ANSI C header files" >&5 -$as_echo_n "checking for ANSI C header files... " >&6; } -if ${ac_cv_header_stdc+:} false; then : - $as_echo_n "(cached) " >&6 -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#include -#include -#include -#include -int -main () -{ +ac_fn_c_check_header_compile "$LINENO" "crypt.h" "ac_cv_header_crypt_h" "$ac_includes_default" +if test "x$ac_cv_header_crypt_h" = xyes +then : + printf "%s\n" "#define HAVE_CRYPT_H 1" >>confdefs.h - ; - return 0; -} -_ACEOF -if ac_fn_c_try_compile "$LINENO"; then : - ac_cv_header_stdc=yes -else - ac_cv_header_stdc=no fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext - -if test $ac_cv_header_stdc = yes; then - # SunOS 4.x string.h does not declare mem*, contrary to ANSI. - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#include - -_ACEOF -if (eval "$ac_cpp conftest.$ac_ext") 2>&5 | - $EGREP "memchr" >/dev/null 2>&1; then : +ac_fn_c_check_header_compile "$LINENO" "poll.h" "ac_cv_header_poll_h" "$ac_includes_default" +if test "x$ac_cv_header_poll_h" = xyes +then : + printf "%s\n" "#define HAVE_POLL_H 1" >>confdefs.h -else - ac_cv_header_stdc=no fi -rm -f conftest* +ac_fn_c_check_header_compile "$LINENO" "inttypes.h" "ac_cv_header_inttypes_h" "$ac_includes_default" +if test "x$ac_cv_header_inttypes_h" = xyes +then : + printf "%s\n" "#define HAVE_INTTYPES_H 1" >>confdefs.h fi +ac_fn_c_check_header_compile "$LINENO" "stdint.h" "ac_cv_header_stdint_h" "$ac_includes_default" +if test "x$ac_cv_header_stdint_h" = xyes +then : + printf "%s\n" "#define HAVE_STDINT_H 1" >>confdefs.h -if test $ac_cv_header_stdc = yes; then - # ISC 2.0.2 stdlib.h does not declare free, contrary to ANSI. - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#include - -_ACEOF -if (eval "$ac_cpp conftest.$ac_ext") 2>&5 | - $EGREP "free" >/dev/null 2>&1; then : - -else - ac_cv_header_stdc=no fi -rm -f conftest* +ac_fn_c_check_header_compile "$LINENO" "sys/devpoll.h" "ac_cv_header_sys_devpoll_h" "$ac_includes_default" +if test "x$ac_cv_header_sys_devpoll_h" = xyes +then : + printf "%s\n" "#define HAVE_SYS_DEVPOLL_H 1" >>confdefs.h fi +ac_fn_c_check_header_compile "$LINENO" "sys/epoll.h" "ac_cv_header_sys_epoll_h" "$ac_includes_default" +if test "x$ac_cv_header_sys_epoll_h" = xyes +then : + printf "%s\n" "#define HAVE_SYS_EPOLL_H 1" >>confdefs.h -if test $ac_cv_header_stdc = yes; then - # /bin/cc in Irix-4.0.5 gets non-ANSI ctype macros unless using -ansi. - if test "$cross_compiling" = yes; then : - : -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#include -#include -#if ((' ' & 0x0FF) == 0x020) -# define ISLOWER(c) ('a' <= (c) && (c) <= 'z') -# define TOUPPER(c) (ISLOWER(c) ? 'A' + ((c) - 'a') : (c)) -#else -# define ISLOWER(c) \ - (('a' <= (c) && (c) <= 'i') \ - || ('j' <= (c) && (c) <= 'r') \ - || ('s' <= (c) && (c) <= 'z')) -# define TOUPPER(c) (ISLOWER(c) ? ((c) | 0x40) : (c)) -#endif - -#define XOR(e, f) (((e) && !(f)) || (!(e) && (f))) -int -main () -{ - int i; - for (i = 0; i < 256; i++) - if (XOR (islower (i), ISLOWER (i)) - || toupper (i) != TOUPPER (i)) - return 2; - return 0; -} -_ACEOF -if ac_fn_c_try_run "$LINENO"; then : - -else - ac_cv_header_stdc=no -fi -rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext fi +ac_fn_c_check_header_compile "$LINENO" "sys/event.h" "ac_cv_header_sys_event_h" "$ac_includes_default" +if test "x$ac_cv_header_sys_event_h" = xyes +then : + printf "%s\n" "#define HAVE_SYS_EVENT_H 1" >>confdefs.h fi -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_header_stdc" >&5 -$as_echo "$ac_cv_header_stdc" >&6; } -if test $ac_cv_header_stdc = yes; then - -$as_echo "#define STDC_HEADERS 1" >>confdefs.h +ac_fn_c_check_header_compile "$LINENO" "sys/param.h" "ac_cv_header_sys_param_h" "$ac_includes_default" +if test "x$ac_cv_header_sys_param_h" = xyes +then : + printf "%s\n" "#define HAVE_SYS_PARAM_H 1" >>confdefs.h fi - -# On IRIX 5.3, sys/types and inttypes.h are conflicting. -for ac_header in sys/types.h sys/stat.h stdlib.h string.h memory.h strings.h \ - inttypes.h stdint.h unistd.h -do : - as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh` -ac_fn_c_check_header_compile "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default -" -if eval test \"x\$"$as_ac_Header"\" = x"yes"; then : - cat >>confdefs.h <<_ACEOF -#define `$as_echo "HAVE_$ac_header" | $as_tr_cpp` 1 -_ACEOF +ac_fn_c_check_header_compile "$LINENO" "sys/resource.h" "ac_cv_header_sys_resource_h" "$ac_includes_default" +if test "x$ac_cv_header_sys_resource_h" = xyes +then : + printf "%s\n" "#define HAVE_SYS_RESOURCE_H 1" >>confdefs.h fi - -done - - -for ac_header in crypt.h poll.h inttypes.h stdint.h sys/devpoll.h sys/epoll.h sys/event.h sys/param.h sys/resource.h sys/socket.h -do : - as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh` -ac_fn_c_check_header_mongrel "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default" -if eval test \"x\$"$as_ac_Header"\" = x"yes"; then : - cat >>confdefs.h <<_ACEOF -#define `$as_echo "HAVE_$ac_header" | $as_tr_cpp` 1 -_ACEOF +ac_fn_c_check_header_compile "$LINENO" "sys/socket.h" "ac_cv_header_sys_socket_h" "$ac_includes_default" +if test "x$ac_cv_header_sys_socket_h" = xyes +then : + printf "%s\n" "#define HAVE_SYS_SOCKET_H 1" >>confdefs.h fi -done - - { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether byte ordering is bigendian" >&5 -$as_echo_n "checking whether byte ordering is bigendian... " >&6; } -if ${ac_cv_c_bigendian+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_cv_c_bigendian=unknown + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether byte ordering is bigendian" >&5 +printf %s "checking whether byte ordering is bigendian... " >&6; } +if test ${ac_cv_c_bigendian+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_cv_c_bigendian=unknown # See if we're dealing with a universal compiler. cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -4851,7 +5479,8 @@ else typedef int dummy; _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : # Check for potential -arch flags. It is not universal unless # there are at least two -arch flags with different values. @@ -4875,7 +5504,7 @@ if ac_fn_c_try_compile "$LINENO"; then : fi done fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext if test $ac_cv_c_bigendian = unknown; then # See if sys/param.h defines the BYTE_ORDER macro. cat confdefs.h - <<_ACEOF >conftest.$ac_ext @@ -4884,10 +5513,10 @@ rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext #include int -main () +main (void) { -#if ! (defined BYTE_ORDER && defined BIG_ENDIAN \ - && defined LITTLE_ENDIAN && BYTE_ORDER && BIG_ENDIAN \ +#if ! (defined BYTE_ORDER && defined BIG_ENDIAN \\ + && defined LITTLE_ENDIAN && BYTE_ORDER && BIG_ENDIAN \\ && LITTLE_ENDIAN) bogus endian macros #endif @@ -4896,7 +5525,8 @@ main () return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : # It does; now see whether it defined to BIG_ENDIAN or not. cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -4904,7 +5534,7 @@ if ac_fn_c_try_compile "$LINENO"; then : #include int -main () +main (void) { #if BYTE_ORDER != BIG_ENDIAN not big endian @@ -4914,14 +5544,16 @@ main () return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : ac_cv_c_bigendian=yes -else - ac_cv_c_bigendian=no +else case e in #( + e) ac_cv_c_bigendian=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext fi if test $ac_cv_c_bigendian = unknown; then # See if defines _LITTLE_ENDIAN or _BIG_ENDIAN (e.g., Solaris). @@ -4930,7 +5562,7 @@ rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext #include int -main () +main (void) { #if ! (defined _LITTLE_ENDIAN || defined _BIG_ENDIAN) bogus endian macros @@ -4940,14 +5572,15 @@ main () return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : # It does; now see whether it defined to _BIG_ENDIAN or not. cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include int -main () +main (void) { #ifndef _BIG_ENDIAN not big endian @@ -4957,50 +5590,55 @@ main () return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : ac_cv_c_bigendian=yes -else - ac_cv_c_bigendian=no +else case e in #( + e) ac_cv_c_bigendian=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext fi if test $ac_cv_c_bigendian = unknown; then # Compile a test program. - if test "$cross_compiling" = yes; then : + if test "$cross_compiling" = yes +then : # Try to guess by grepping values from an object file. cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ -short int ascii_mm[] = +unsigned short int ascii_mm[] = { 0x4249, 0x4765, 0x6E44, 0x6961, 0x6E53, 0x7953, 0 }; - short int ascii_ii[] = + unsigned short int ascii_ii[] = { 0x694C, 0x5454, 0x656C, 0x6E45, 0x6944, 0x6E61, 0 }; int use_ascii (int i) { return ascii_mm[i] + ascii_ii[i]; } - short int ebcdic_ii[] = + unsigned short int ebcdic_ii[] = { 0x89D3, 0xE3E3, 0x8593, 0x95C5, 0x89C4, 0x9581, 0 }; - short int ebcdic_mm[] = + unsigned short int ebcdic_mm[] = { 0xC2C9, 0xC785, 0x95C4, 0x8981, 0x95E2, 0xA8E2, 0 }; int use_ebcdic (int i) { return ebcdic_mm[i] + ebcdic_ii[i]; } - extern int foo; - -int -main () -{ -return use_ascii (foo) == use_ebcdic (foo); - ; - return 0; -} + int + main (int argc, char **argv) + { + /* Intimidate the compiler so that it does not + optimize the arrays away. */ + char *p = argv[0]; + ascii_mm[1] = *p++; ebcdic_mm[1] = *p++; + ascii_ii[1] = *p++; ebcdic_ii[1] = *p++; + return use_ascii (argc) == use_ebcdic (*p); + } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : - if grep BIGenDianSyS conftest.$ac_objext >/dev/null; then +if ac_fn_c_try_link "$LINENO" +then : + if grep BIGenDianSyS conftest$ac_exeext >/dev/null; then ac_cv_c_bigendian=yes fi - if grep LiTTleEnDian conftest.$ac_objext >/dev/null ; then + if grep LiTTleEnDian conftest$ac_exeext >/dev/null ; then if test "$ac_cv_c_bigendian" = unknown; then ac_cv_c_bigendian=no else @@ -5009,13 +5647,14 @@ if ac_fn_c_try_compile "$LINENO"; then : fi fi fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $ac_includes_default int -main () +main (void) { /* Are we little or big endian? From Harbison&Steele. */ @@ -5031,28 +5670,32 @@ main () return 0; } _ACEOF -if ac_fn_c_try_run "$LINENO"; then : +if ac_fn_c_try_run "$LINENO" +then : ac_cv_c_bigendian=no -else - ac_cv_c_bigendian=yes +else case e in #( + e) ac_cv_c_bigendian=yes ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - fi + fi ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_bigendian" >&5 -$as_echo "$ac_cv_c_bigendian" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_bigendian" >&5 +printf "%s\n" "$ac_cv_c_bigendian" >&6; } case $ac_cv_c_bigendian in #( yes) - $as_echo "#define WORDS_BIGENDIAN 1" >>confdefs.h + printf "%s\n" "#define WORDS_BIGENDIAN 1" >>confdefs.h ;; #( no) ;; #( universal) -$as_echo "#define AC_APPLE_UNIVERSAL_BUILD 1" >>confdefs.h +printf "%s\n" "#define AC_APPLE_UNIVERSAL_BUILD 1" >>confdefs.h ;; #( *) @@ -5061,63 +5704,39 @@ $as_echo "#define AC_APPLE_UNIVERSAL_BUILD 1" >>confdefs.h esac ac_fn_c_check_type "$LINENO" "size_t" "ac_cv_type_size_t" "$ac_includes_default" -if test "x$ac_cv_type_size_t" = xyes; then : - -else - -cat >>confdefs.h <<_ACEOF -#define size_t unsigned int -_ACEOF +if test "x$ac_cv_type_size_t" = xyes +then : +else case e in #( + e) +printf "%s\n" "#define size_t unsigned int" >>confdefs.h + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether time.h and sys/time.h may both be included" >&5 -$as_echo_n "checking whether time.h and sys/time.h may both be included... " >&6; } -if ${ac_cv_header_time+:} false; then : - $as_echo_n "(cached) " >&6 -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#include -#include -#include -int -main () -{ -if ((struct tm *) 0) -return 0; - ; - return 0; -} -_ACEOF -if ac_fn_c_try_compile "$LINENO"; then : - ac_cv_header_time=yes -else - ac_cv_header_time=no -fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_header_time" >&5 -$as_echo "$ac_cv_header_time" >&6; } -if test $ac_cv_header_time = yes; then -$as_echo "#define TIME_WITH_SYS_TIME 1" >>confdefs.h +# Obsolete code to be removed. +if test $ac_cv_header_sys_time_h = yes; then + +printf "%s\n" "#define TIME_WITH_SYS_TIME 1" >>confdefs.h fi +# End of obsolete code. -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether struct tm is in sys/time.h or time.h" >&5 -$as_echo_n "checking whether struct tm is in sys/time.h or time.h... " >&6; } -if ${ac_cv_struct_tm+:} false; then : - $as_echo_n "(cached) " >&6 -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether struct tm is in sys/time.h or time.h" >&5 +printf %s "checking whether struct tm is in sys/time.h or time.h... " >&6; } +if test ${ac_cv_struct_tm+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include #include int -main () +main (void) { struct tm tm; int *p = &tm.tm_sec; @@ -5126,293 +5745,300 @@ struct tm tm; return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : ac_cv_struct_tm=time.h -else - ac_cv_struct_tm=sys/time.h +else case e in #( + e) ac_cv_struct_tm=sys/time.h ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_struct_tm" >&5 -$as_echo "$ac_cv_struct_tm" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_struct_tm" >&5 +printf "%s\n" "$ac_cv_struct_tm" >&6; } if test $ac_cv_struct_tm = sys/time.h; then -$as_echo "#define TM_IN_SYS_TIME 1" >>confdefs.h +printf "%s\n" "#define TM_IN_SYS_TIME 1" >>confdefs.h fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for uid_t in sys/types.h" >&5 -$as_echo_n "checking for uid_t in sys/types.h... " >&6; } -if ${ac_cv_type_uid_t+:} false; then : - $as_echo_n "(cached) " >&6 -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#include - -_ACEOF -if (eval "$ac_cpp conftest.$ac_ext") 2>&5 | - $EGREP "uid_t" >/dev/null 2>&1; then : - ac_cv_type_uid_t=yes -else - ac_cv_type_uid_t=no -fi -rm -f conftest* +ac_fn_c_check_type "$LINENO" "uid_t" "ac_cv_type_uid_t" "$ac_includes_default" +if test "x$ac_cv_type_uid_t" = xyes +then : +else case e in #( + e) +printf "%s\n" "#define uid_t int" >>confdefs.h + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_type_uid_t" >&5 -$as_echo "$ac_cv_type_uid_t" >&6; } -if test $ac_cv_type_uid_t = no; then -$as_echo "#define uid_t int" >>confdefs.h - - -$as_echo "#define gid_t int" >>confdefs.h +ac_fn_c_check_type "$LINENO" "gid_t" "ac_cv_type_gid_t" "$ac_includes_default" +if test "x$ac_cv_type_gid_t" = xyes +then : +else case e in #( + e) +printf "%s\n" "#define gid_t int" >>confdefs.h + ;; +esac fi # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking size of short" >&5 -$as_echo_n "checking size of short... " >&6; } -if ${ac_cv_sizeof_short+:} false; then : - $as_echo_n "(cached) " >&6 -else - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (short))" "ac_cv_sizeof_short" "$ac_includes_default"; then : - -else - if test "$ac_cv_type_short" = yes; then - { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of short" >&5 +printf %s "checking size of short... " >&6; } +if test ${ac_cv_sizeof_short+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (short))" "ac_cv_sizeof_short" "$ac_includes_default" +then : + +else case e in #( + e) if test "$ac_cv_type_short" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (short) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_short=0 - fi + fi ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_short" >&5 -$as_echo "$ac_cv_sizeof_short" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_short" >&5 +printf "%s\n" "$ac_cv_sizeof_short" >&6; } -cat >>confdefs.h <<_ACEOF -#define SIZEOF_SHORT $ac_cv_sizeof_short -_ACEOF +printf "%s\n" "#define SIZEOF_SHORT $ac_cv_sizeof_short" >>confdefs.h # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking size of int" >&5 -$as_echo_n "checking size of int... " >&6; } -if ${ac_cv_sizeof_int+:} false; then : - $as_echo_n "(cached) " >&6 -else - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (int))" "ac_cv_sizeof_int" "$ac_includes_default"; then : - -else - if test "$ac_cv_type_int" = yes; then - { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of int" >&5 +printf %s "checking size of int... " >&6; } +if test ${ac_cv_sizeof_int+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (int))" "ac_cv_sizeof_int" "$ac_includes_default" +then : + +else case e in #( + e) if test "$ac_cv_type_int" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (int) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_int=0 - fi + fi ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_int" >&5 -$as_echo "$ac_cv_sizeof_int" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_int" >&5 +printf "%s\n" "$ac_cv_sizeof_int" >&6; } -cat >>confdefs.h <<_ACEOF -#define SIZEOF_INT $ac_cv_sizeof_int -_ACEOF +printf "%s\n" "#define SIZEOF_INT $ac_cv_sizeof_int" >>confdefs.h # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking size of long" >&5 -$as_echo_n "checking size of long... " >&6; } -if ${ac_cv_sizeof_long+:} false; then : - $as_echo_n "(cached) " >&6 -else - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (long))" "ac_cv_sizeof_long" "$ac_includes_default"; then : - -else - if test "$ac_cv_type_long" = yes; then - { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of long" >&5 +printf %s "checking size of long... " >&6; } +if test ${ac_cv_sizeof_long+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (long))" "ac_cv_sizeof_long" "$ac_includes_default" +then : + +else case e in #( + e) if test "$ac_cv_type_long" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (long) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_long=0 - fi + fi ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_long" >&5 -$as_echo "$ac_cv_sizeof_long" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_long" >&5 +printf "%s\n" "$ac_cv_sizeof_long" >&6; } -cat >>confdefs.h <<_ACEOF -#define SIZEOF_LONG $ac_cv_sizeof_long -_ACEOF +printf "%s\n" "#define SIZEOF_LONG $ac_cv_sizeof_long" >>confdefs.h # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking size of void *" >&5 -$as_echo_n "checking size of void *... " >&6; } -if ${ac_cv_sizeof_void_p+:} false; then : - $as_echo_n "(cached) " >&6 -else - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (void *))" "ac_cv_sizeof_void_p" "$ac_includes_default"; then : - -else - if test "$ac_cv_type_void_p" = yes; then - { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of void *" >&5 +printf %s "checking size of void *... " >&6; } +if test ${ac_cv_sizeof_void_p+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (void *))" "ac_cv_sizeof_void_p" "$ac_includes_default" +then : + +else case e in #( + e) if test "$ac_cv_type_void_p" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (void *) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_void_p=0 - fi + fi ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_void_p" >&5 -$as_echo "$ac_cv_sizeof_void_p" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_void_p" >&5 +printf "%s\n" "$ac_cv_sizeof_void_p" >&6; } -cat >>confdefs.h <<_ACEOF -#define SIZEOF_VOID_P $ac_cv_sizeof_void_p -_ACEOF +printf "%s\n" "#define SIZEOF_VOID_P $ac_cv_sizeof_void_p" >>confdefs.h # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking size of int64_t" >&5 -$as_echo_n "checking size of int64_t... " >&6; } -if ${ac_cv_sizeof_int64_t+:} false; then : - $as_echo_n "(cached) " >&6 -else - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (int64_t))" "ac_cv_sizeof_int64_t" "$ac_includes_default"; then : - -else - if test "$ac_cv_type_int64_t" = yes; then - { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of int64_t" >&5 +printf %s "checking size of int64_t... " >&6; } +if test ${ac_cv_sizeof_int64_t+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (int64_t))" "ac_cv_sizeof_int64_t" "$ac_includes_default" +then : + +else case e in #( + e) if test "$ac_cv_type_int64_t" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (int64_t) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_int64_t=0 - fi + fi ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_int64_t" >&5 -$as_echo "$ac_cv_sizeof_int64_t" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_int64_t" >&5 +printf "%s\n" "$ac_cv_sizeof_int64_t" >&6; } -cat >>confdefs.h <<_ACEOF -#define SIZEOF_INT64_T $ac_cv_sizeof_int64_t -_ACEOF +printf "%s\n" "#define SIZEOF_INT64_T $ac_cv_sizeof_int64_t" >>confdefs.h # The cast to long int works around a bug in the HP C Compiler # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects -# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'. # This bug is HP SR number 8606223364. -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking size of long long" >&5 -$as_echo_n "checking size of long long... " >&6; } -if ${ac_cv_sizeof_long_long+:} false; then : - $as_echo_n "(cached) " >&6 -else - if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (long long))" "ac_cv_sizeof_long_long" "$ac_includes_default"; then : - -else - if test "$ac_cv_type_long_long" = yes; then - { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking size of long long" >&5 +printf %s "checking size of long long... " >&6; } +if test ${ac_cv_sizeof_long_long+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (long long))" "ac_cv_sizeof_long_long" "$ac_includes_default" +then : + +else case e in #( + e) if test "$ac_cv_type_long_long" = yes; then + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot compute sizeof (long long) -See \`config.log' for more details" "$LINENO" 5; } +See 'config.log' for more details" "$LINENO" 5; } else ac_cv_sizeof_long_long=0 - fi + fi ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_long_long" >&5 -$as_echo "$ac_cv_sizeof_long_long" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_long_long" >&5 +printf "%s\n" "$ac_cv_sizeof_long_long" >&6; } -cat >>confdefs.h <<_ACEOF -#define SIZEOF_LONG_LONG $ac_cv_sizeof_long_long -_ACEOF +printf "%s\n" "#define SIZEOF_LONG_LONG $ac_cv_sizeof_long_long" >>confdefs.h if test "$ac_cv_sizeof_int" = 2 ; then ac_fn_c_check_type "$LINENO" "int16_t" "ac_cv_type_int16_t" "$ac_includes_default" -if test "x$ac_cv_type_int16_t" = xyes; then : - -else - -cat >>confdefs.h <<_ACEOF -#define int16_t int -_ACEOF +if test "x$ac_cv_type_int16_t" = xyes +then : +else case e in #( + e) +printf "%s\n" "#define int16_t int" >>confdefs.h + ;; +esac fi ac_fn_c_check_type "$LINENO" "uint16_t" "ac_cv_type_uint16_t" "$ac_includes_default" -if test "x$ac_cv_type_uint16_t" = xyes; then : - -else - -cat >>confdefs.h <<_ACEOF -#define uint16_t unsigned int -_ACEOF +if test "x$ac_cv_type_uint16_t" = xyes +then : +else case e in #( + e) +printf "%s\n" "#define uint16_t unsigned int" >>confdefs.h + ;; +esac fi elif test "$ac_cv_sizeof_short" = 2 ; then ac_fn_c_check_type "$LINENO" "int16_t" "ac_cv_type_int16_t" "$ac_includes_default" -if test "x$ac_cv_type_int16_t" = xyes; then : - -else - -cat >>confdefs.h <<_ACEOF -#define int16_t short -_ACEOF +if test "x$ac_cv_type_int16_t" = xyes +then : +else case e in #( + e) +printf "%s\n" "#define int16_t short" >>confdefs.h + ;; +esac fi ac_fn_c_check_type "$LINENO" "uint16_t" "ac_cv_type_uint16_t" "$ac_includes_default" -if test "x$ac_cv_type_uint16_t" = xyes; then : - -else - -cat >>confdefs.h <<_ACEOF -#define uint16_t unsigned short -_ACEOF +if test "x$ac_cv_type_uint16_t" = xyes +then : +else case e in #( + e) +printf "%s\n" "#define uint16_t unsigned short" >>confdefs.h + ;; +esac fi else @@ -5420,71 +6046,71 @@ else fi if test "$ac_cv_sizeof_int" = 4 ; then ac_fn_c_check_type "$LINENO" "int32_t" "ac_cv_type_int32_t" "$ac_includes_default" -if test "x$ac_cv_type_int32_t" = xyes; then : - -else - -cat >>confdefs.h <<_ACEOF -#define int32_t int -_ACEOF +if test "x$ac_cv_type_int32_t" = xyes +then : +else case e in #( + e) +printf "%s\n" "#define int32_t int" >>confdefs.h + ;; +esac fi ac_fn_c_check_type "$LINENO" "uint32_t" "ac_cv_type_uint32_t" "$ac_includes_default" -if test "x$ac_cv_type_uint32_t" = xyes; then : - -else - -cat >>confdefs.h <<_ACEOF -#define uint32_t unsigned int -_ACEOF +if test "x$ac_cv_type_uint32_t" = xyes +then : +else case e in #( + e) +printf "%s\n" "#define uint32_t unsigned int" >>confdefs.h + ;; +esac fi elif test "$ac_cv_sizeof_short" = 4 ; then ac_fn_c_check_type "$LINENO" "int32_t" "ac_cv_type_int32_t" "$ac_includes_default" -if test "x$ac_cv_type_int32_t" = xyes; then : - -else - -cat >>confdefs.h <<_ACEOF -#define int32_t short -_ACEOF +if test "x$ac_cv_type_int32_t" = xyes +then : +else case e in #( + e) +printf "%s\n" "#define int32_t short" >>confdefs.h + ;; +esac fi ac_fn_c_check_type "$LINENO" "uint32_t" "ac_cv_type_uint32_t" "$ac_includes_default" -if test "x$ac_cv_type_uint32_t" = xyes; then : - -else - -cat >>confdefs.h <<_ACEOF -#define uint32_t unsigned short -_ACEOF +if test "x$ac_cv_type_uint32_t" = xyes +then : +else case e in #( + e) +printf "%s\n" "#define uint32_t unsigned short" >>confdefs.h + ;; +esac fi elif test "$ac_cv_sizeof_long" = 4 ; then ac_fn_c_check_type "$LINENO" "int32_t" "ac_cv_type_int32_t" "$ac_includes_default" -if test "x$ac_cv_type_int32_t" = xyes; then : - -else - -cat >>confdefs.h <<_ACEOF -#define int32_t long -_ACEOF +if test "x$ac_cv_type_int32_t" = xyes +then : +else case e in #( + e) +printf "%s\n" "#define int32_t long" >>confdefs.h + ;; +esac fi ac_fn_c_check_type "$LINENO" "uint32_t" "ac_cv_type_uint32_t" "$ac_includes_default" -if test "x$ac_cv_type_uint32_t" = xyes; then : - -else - -cat >>confdefs.h <<_ACEOF -#define uint32_t unsigned long -_ACEOF +if test "x$ac_cv_type_uint32_t" = xyes +then : +else case e in #( + e) +printf "%s\n" "#define uint32_t unsigned long" >>confdefs.h + ;; +esac fi else @@ -5492,36 +6118,38 @@ else fi if test "$ac_cv_sizeof_int64_t" = 8 ; then ac_fn_c_check_type "$LINENO" "int64_t" "ac_cv_type_int64_t" "$ac_includes_default" -if test "x$ac_cv_type_int64_t" = xyes; then : +if test "x$ac_cv_type_int64_t" = xyes +then : fi ac_fn_c_check_type "$LINENO" "uint64_t" "ac_cv_type_uint64_t" "$ac_includes_default" -if test "x$ac_cv_type_uint64_t" = xyes; then : +if test "x$ac_cv_type_uint64_t" = xyes +then : fi elif test "$ac_cv_sizeof_long_long" = 8 ; then ac_fn_c_check_type "$LINENO" "int64_t" "ac_cv_type_int64_t" "$ac_includes_default" -if test "x$ac_cv_type_int64_t" = xyes; then : - -else - -cat >>confdefs.h <<_ACEOF -#define int64_t long long -_ACEOF +if test "x$ac_cv_type_int64_t" = xyes +then : +else case e in #( + e) +printf "%s\n" "#define int64_t long long" >>confdefs.h + ;; +esac fi ac_fn_c_check_type "$LINENO" "uint64_t" "ac_cv_type_uint64_t" "$ac_includes_default" -if test "x$ac_cv_type_uint64_t" = xyes; then : - -else - -cat >>confdefs.h <<_ACEOF -#define uint64_t unsigned long long -_ACEOF +if test "x$ac_cv_type_uint64_t" = xyes +then : +else case e in #( + e) +printf "%s\n" "#define uint64_t unsigned long long" >>confdefs.h + ;; +esac fi else @@ -5530,26 +6158,30 @@ fi ac_fn_c_check_type "$LINENO" "struct sockaddr_in6" "ac_cv_type_struct_sockaddr_in6" "#include #include " -if test "x$ac_cv_type_struct_sockaddr_in6" = xyes; then : +if test "x$ac_cv_type_struct_sockaddr_in6" = xyes +then : unet_have_sockaddr_in6="yes" -else - unet_have_sockaddr_in6="no" +else case e in #( + e) unet_have_sockaddr_in6="no" ;; +esac fi ac_fn_c_check_type "$LINENO" "socklen_t" "ac_cv_type_socklen_t" "#include #include " -if test "x$ac_cv_type_socklen_t" = xyes; then : - -else - - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for socklen_t equivalent" >&5 -$as_echo_n "checking for socklen_t equivalent... " >&6; } - if ${curl_cv_socklen_t_equiv+:} false; then : - $as_echo_n "(cached) " >&6 -else - +if test "x$ac_cv_type_socklen_t" = xyes +then : + +else case e in #( + e) + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for socklen_t equivalent" >&5 +printf %s "checking for socklen_t equivalent... " >&6; } + if test ${curl_cv_socklen_t_equiv+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) curl_cv_socklen_t_equiv= for arg2 in "struct sockaddr" void ; do for t in int size_t unsigned long "unsigned long" ; do @@ -5559,7 +6191,7 @@ else #include int getpeername (int $arg2 *, $t *); int -main () +main (void) { $t len; getpeername(0, 0, &len); @@ -5567,45 +6199,60 @@ $t len; return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : curl_cv_socklen_t_equiv="$t" break fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext done done + ;; +esac +fi + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $curl_cv_socklen_t_equiv" >&5 +printf "%s\n" "$curl_cv_socklen_t_equiv" >&6; } + +printf "%s\n" "#define socklen_t $curl_cv_socklen_t_equiv" >>confdefs.h + ;; +esac fi - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $curl_cv_socklen_t_equiv" >&5 -$as_echo "$curl_cv_socklen_t_equiv" >&6; } -cat >>confdefs.h <<_ACEOF -#define socklen_t $curl_cv_socklen_t_equiv -_ACEOF +ac_fn_c_check_func "$LINENO" "kqueue" "ac_cv_func_kqueue" +if test "x$ac_cv_func_kqueue" = xyes +then : + printf "%s\n" "#define HAVE_KQUEUE 1" >>confdefs.h fi +ac_fn_c_check_func "$LINENO" "setrlimit" "ac_cv_func_setrlimit" +if test "x$ac_cv_func_setrlimit" = xyes +then : + printf "%s\n" "#define HAVE_SETRLIMIT 1" >>confdefs.h +fi +ac_fn_c_check_func "$LINENO" "getrusage" "ac_cv_func_getrusage" +if test "x$ac_cv_func_getrusage" = xyes +then : + printf "%s\n" "#define HAVE_GETRUSAGE 1" >>confdefs.h -for ac_func in kqueue setrlimit getrusage times -do : - as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh` -ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var" -if eval test \"x\$"$as_ac_var"\" = x"yes"; then : - cat >>confdefs.h <<_ACEOF -#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1 -_ACEOF +fi +ac_fn_c_check_func "$LINENO" "times" "ac_cv_func_times" +if test "x$ac_cv_func_times" = xyes +then : + printf "%s\n" "#define HAVE_TIMES 1" >>confdefs.h fi -done -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for sys/wait.h that is POSIX.1 compatible" >&5 -$as_echo_n "checking for sys/wait.h that is POSIX.1 compatible... " >&6; } -if ${ac_cv_header_sys_wait_h+:} false; then : - $as_echo_n "(cached) " >&6 -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for sys/wait.h that is POSIX.1 compatible" >&5 +printf %s "checking for sys/wait.h that is POSIX.1 compatible... " >&6; } +if test ${ac_cv_header_sys_wait_h+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include #include @@ -5617,7 +6264,7 @@ else #endif int -main () +main (void) { int s; wait (&s); @@ -5626,33 +6273,38 @@ main () return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : ac_cv_header_sys_wait_h=yes -else - ac_cv_header_sys_wait_h=no +else case e in #( + e) ac_cv_header_sys_wait_h=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_header_sys_wait_h" >&5 -$as_echo "$ac_cv_header_sys_wait_h" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_header_sys_wait_h" >&5 +printf "%s\n" "$ac_cv_header_sys_wait_h" >&6; } if test $ac_cv_header_sys_wait_h = yes; then -$as_echo "#define HAVE_SYS_WAIT_H 1" >>confdefs.h +printf "%s\n" "#define HAVE_SYS_WAIT_H 1" >>confdefs.h fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for restartable system calls" >&5 -$as_echo_n "checking for restartable system calls... " >&6; } -if ${ac_cv_sys_restartable_syscalls+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test "$cross_compiling" = yes; then : - { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for restartable system calls" >&5 +printf %s "checking for restartable system calls... " >&6; } +if test ${ac_cv_sys_restartable_syscalls+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test "$cross_compiling" = yes +then : + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot run test program while cross compiling -See \`config.log' for more details" "$LINENO" 5; } -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +See 'config.log' for more details" "$LINENO" 5; } +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Exit 0 (true) if wait returns something other than -1, i.e. the pid of the child, which means that wait was restarted @@ -5667,13 +6319,12 @@ $ac_includes_default /* Some platforms explicitly require an extern "C" signal handler when using C++. */ #ifdef __cplusplus -extern "C" void ucatch (int dummy) { } -#else -void ucatch (dummy) int dummy; { } +extern "C" #endif +void ucatch (int dummy) { } int -main () +main (void) { int i = fork (), status; @@ -5694,80 +6345,91 @@ main () return status == -1; } _ACEOF -if ac_fn_c_try_run "$LINENO"; then : +if ac_fn_c_try_run "$LINENO" +then : ac_cv_sys_restartable_syscalls=yes -else - ac_cv_sys_restartable_syscalls=no +else case e in #( + e) ac_cv_sys_restartable_syscalls=no ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sys_restartable_syscalls" >&5 -$as_echo "$ac_cv_sys_restartable_syscalls" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sys_restartable_syscalls" >&5 +printf "%s\n" "$ac_cv_sys_restartable_syscalls" >&6; } if test $ac_cv_sys_restartable_syscalls = yes; then -$as_echo "#define HAVE_RESTARTABLE_SYSCALLS 1" >>confdefs.h +printf "%s\n" "#define HAVE_RESTARTABLE_SYSCALLS 1" >>confdefs.h fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for donuts" >&5 -$as_echo_n "checking for donuts... " >&6; } -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -$as_echo "yes" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for donuts" >&5 +printf %s "checking for donuts... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } for ac_prog in gawk mawk nawk awk do # Extract the first word of "$ac_prog", so it can be a program name with args. set dummy $ac_prog; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_prog_AWK+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test -n "$AWK"; then +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_AWK+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$AWK"; then ac_cv_prog_AWK="$AWK" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_AWK="$ac_prog" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS -fi +fi ;; +esac fi AWK=$ac_cv_prog_AWK if test -n "$AWK"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $AWK" >&5 -$as_echo "$AWK" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $AWK" >&5 +printf "%s\n" "$AWK" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi test -n "$AWK" && break done -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${MAKE-make} sets \$(MAKE)" >&5 -$as_echo_n "checking whether ${MAKE-make} sets \$(MAKE)... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether ${MAKE-make} sets \$(MAKE)" >&5 +printf %s "checking whether ${MAKE-make} sets \$(MAKE)... " >&6; } set x ${MAKE-make} -ac_make=`$as_echo "$2" | sed 's/+/p/g; s/[^a-zA-Z0-9_]/_/g'` -if eval \${ac_cv_prog_make_${ac_make}_set+:} false; then : - $as_echo_n "(cached) " >&6 -else - cat >conftest.make <<\_ACEOF +ac_make=`printf "%s\n" "$2" | sed 's/+/p/g; s/[^a-zA-Z0-9_]/_/g'` +if eval test \${ac_cv_prog_make_${ac_make}_set+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat >conftest.make <<\_ACEOF SHELL = /bin/sh all: @echo '@@@%%%=$(MAKE)=@@@%%%' @@ -5779,19 +6441,21 @@ case `${MAKE-make} -f conftest.make 2>/dev/null` in *) eval ac_cv_prog_make_${ac_make}_set=no;; esac -rm -f conftest.make +rm -f conftest.make ;; +esac fi if eval test \$ac_cv_prog_make_${ac_make}_set = yes; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -$as_echo "yes" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } SET_MAKE= else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } SET_MAKE="MAKE=${MAKE-make}" fi -# Find a good install program. We prefer a C program (faster), + + # Find a good install program. We prefer a C program (faster), # so one script is as good as another. But avoid the broken or # incompatible versions: # SysV /etc/install, /usr/sbin/install @@ -5805,20 +6469,25 @@ fi # OS/2's system install, which has a completely different semantic # ./install, which can be erroneously created by make from ./install.sh. # Reject install programs that cannot install multiple files. -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for a BSD-compatible install" >&5 -$as_echo_n "checking for a BSD-compatible install... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for a BSD-compatible install" >&5 +printf %s "checking for a BSD-compatible install... " >&6; } if test -z "$INSTALL"; then -if ${ac_cv_path_install+:} false; then : - $as_echo_n "(cached) " >&6 -else - as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +if test ${ac_cv_path_install+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. - # Account for people who put trailing slashes in PATH elements. -case $as_dir/ in #(( - ./ | .// | /[cC]/* | \ + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + # Account for fact that we put trailing slashes in our PATH walk. +case $as_dir in #(( + ./ | /[cC]/* | \ /etc/* | /usr/sbin/* | /usr/etc/* | /sbin/* | /usr/afsws/bin/* | \ ?:[\\/]os2[\\/]install[\\/]* | ?:[\\/]OS2[\\/]INSTALL[\\/]* | \ /usr/ucb/* ) ;; @@ -5828,13 +6497,13 @@ case $as_dir/ in #(( # by default. for ac_prog in ginstall scoinst install; do for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_prog$ac_exec_ext"; then + if as_fn_executable_p "$as_dir$ac_prog$ac_exec_ext"; then if test $ac_prog = install && - grep dspmsg "$as_dir/$ac_prog$ac_exec_ext" >/dev/null 2>&1; then + grep dspmsg "$as_dir$ac_prog$ac_exec_ext" >/dev/null 2>&1; then # AIX install. It has an incompatible calling convention. : elif test $ac_prog = install && - grep pwplus "$as_dir/$ac_prog$ac_exec_ext" >/dev/null 2>&1; then + grep pwplus "$as_dir$ac_prog$ac_exec_ext" >/dev/null 2>&1; then # program-specific install script used by HP pwplus--don't use. : else @@ -5842,12 +6511,12 @@ case $as_dir/ in #(( echo one > conftest.one echo two > conftest.two mkdir conftest.dir - if "$as_dir/$ac_prog$ac_exec_ext" -c conftest.one conftest.two "`pwd`/conftest.dir" && + if "$as_dir$ac_prog$ac_exec_ext" -c conftest.one conftest.two "`pwd`/conftest.dir/" && test -s conftest.one && test -s conftest.two && test -s conftest.dir/conftest.one && test -s conftest.dir/conftest.two then - ac_cv_path_install="$as_dir/$ac_prog$ac_exec_ext -c" + ac_cv_path_install="$as_dir$ac_prog$ac_exec_ext -c" break 3 fi fi @@ -5861,9 +6530,10 @@ esac IFS=$as_save_IFS rm -rf conftest.one conftest.two conftest.dir - + ;; +esac fi - if test "${ac_cv_path_install+set}" = set; then + if test ${ac_cv_path_install+y}; then INSTALL=$ac_cv_path_install else # As a last resort, use the slow shell script. Don't cache a @@ -5873,8 +6543,8 @@ fi INSTALL=$ac_install_sh fi fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $INSTALL" >&5 -$as_echo "$INSTALL" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $INSTALL" >&5 +printf "%s\n" "$INSTALL" >&6; } # Use test -z because SunOS4 sh mishandles braces in ${var-val}. # It thinks the first close brace ends the variable substitution. @@ -5884,27 +6554,28 @@ test -z "$INSTALL_SCRIPT" && INSTALL_SCRIPT='${INSTALL}' test -z "$INSTALL_DATA" && INSTALL_DATA='${INSTALL} -m 644' -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ln -s works" >&5 -$as_echo_n "checking whether ln -s works... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether ln -s works" >&5 +printf %s "checking whether ln -s works... " >&6; } LN_S=$as_ln_s if test "$LN_S" = "ln -s"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -$as_echo "yes" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no, using $LN_S" >&5 -$as_echo "no, using $LN_S" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no, using $LN_S" >&5 +printf "%s\n" "no, using $LN_S" >&6; } fi for ac_prog in rm do # Extract the first word of "$ac_prog", so it can be a program name with args. set dummy $ac_prog; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_path_RMPROG+:} false; then : - $as_echo_n "(cached) " >&6 -else - case $RMPROG in +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_path_RMPROG+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) case $RMPROG in [\\/]* | ?:[\\/]*) ac_cv_path_RMPROG="$RMPROG" # Let the user override the test with a path. ;; @@ -5913,11 +6584,15 @@ else for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then - ac_cv_path_RMPROG="$as_dir/$ac_word$ac_exec_ext" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then + ac_cv_path_RMPROG="$as_dir$ac_word$ac_exec_ext" + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -5925,15 +6600,16 @@ done IFS=$as_save_IFS ;; +esac ;; esac fi RMPROG=$ac_cv_path_RMPROG if test -n "$RMPROG"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $RMPROG" >&5 -$as_echo "$RMPROG" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $RMPROG" >&5 +printf "%s\n" "$RMPROG" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -5945,12 +6621,13 @@ for ac_prog in sh do # Extract the first word of "$ac_prog", so it can be a program name with args. set dummy $ac_prog; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_path_SHPROG+:} false; then : - $as_echo_n "(cached) " >&6 -else - case $SHPROG in +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_path_SHPROG+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) case $SHPROG in [\\/]* | ?:[\\/]*) ac_cv_path_SHPROG="$SHPROG" # Let the user override the test with a path. ;; @@ -5959,11 +6636,15 @@ else for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then - ac_cv_path_SHPROG="$as_dir/$ac_word$ac_exec_ext" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then + ac_cv_path_SHPROG="$as_dir$ac_word$ac_exec_ext" + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -5971,15 +6652,16 @@ done IFS=$as_save_IFS ;; +esac ;; esac fi SHPROG=$ac_cv_path_SHPROG if test -n "$SHPROG"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $SHPROG" >&5 -$as_echo "$SHPROG" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $SHPROG" >&5 +printf "%s\n" "$SHPROG" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -5992,38 +6674,44 @@ for ac_prog in flex lex do # Extract the first word of "$ac_prog", so it can be a program name with args. set dummy $ac_prog; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_prog_LEX+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test -n "$LEX"; then +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_LEX+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$LEX"; then ac_cv_prog_LEX="$LEX" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_LEX="$ac_prog" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS -fi +fi ;; +esac fi LEX=$ac_cv_prog_LEX if test -n "$LEX"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $LEX" >&5 -$as_echo "$LEX" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $LEX" >&5 +printf "%s\n" "$LEX" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -6031,15 +6719,26 @@ fi done test -n "$LEX" || LEX=":" -if test "x$LEX" != "x:"; then - cat >conftest.l <<_ACEOF + if test "x$LEX" != "x:"; then + cat >conftest.l <<_ACEOF +%{ +#ifdef __cplusplus +extern "C" +#endif +int yywrap(void); +%} %% a { ECHO; } b { REJECT; } c { yymore (); } d { yyless (1); } e { /* IRIX 6.5 flex 2.5.4 underquotes its yyless argument. */ - yyless ((input () != 0)); } +#ifdef __cplusplus + yyless ((yyinput () != 0)); +#else + yyless ((input () != 0)); +#endif + } f { unput (yytext[0]); } . { BEGIN INITIAL; } %% @@ -6047,101 +6746,218 @@ f { unput (yytext[0]); } extern char *yytext; #endif int +yywrap (void) +{ + return 1; +} +int main (void) { - return ! yylex () + ! yywrap (); + return ! yylex (); } _ACEOF +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for lex output file root" >&5 +printf %s "checking for lex output file root... " >&6; } +if test ${ac_cv_prog_lex_root+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) +ac_cv_prog_lex_root=unknown { { ac_try="$LEX conftest.l" case "(($ac_try" in *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -$as_echo "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$LEX conftest.l") 2>&5 ac_status=$? - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; } -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking lex output file root" >&5 -$as_echo_n "checking lex output file root... " >&6; } -if ${ac_cv_prog_lex_root+:} false; then : - $as_echo_n "(cached) " >&6 -else - + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } && if test -f lex.yy.c; then ac_cv_prog_lex_root=lex.yy elif test -f lexyy.c; then ac_cv_prog_lex_root=lexyy -else - as_fn_error $? "cannot find output from $LEX; giving up" "$LINENO" 5 +fi ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_lex_root" >&5 +printf "%s\n" "$ac_cv_prog_lex_root" >&6; } +if test "$ac_cv_prog_lex_root" = unknown +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cannot find output from $LEX; giving up on $LEX" >&5 +printf "%s\n" "$as_me: WARNING: cannot find output from $LEX; giving up on $LEX" >&2;} + LEX=: LEXLIB= +fi +LEX_OUTPUT_ROOT=$ac_cv_prog_lex_root + +if test ${LEXLIB+y} +then : + +else case e in #( + e) + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for lex library" >&5 +printf %s "checking for lex library... " >&6; } +if test ${ac_cv_lib_lex+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) + ac_save_LIBS="$LIBS" + ac_found=false + for ac_cv_lib_lex in 'none needed' -lfl -ll 'not found'; do + case $ac_cv_lib_lex in #( + 'none needed') : + ;; #( + 'not found') : + break ;; #( + *) : + LIBS="$ac_cv_lib_lex $ac_save_LIBS" ;; #( + *) : + ;; +esac + + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +`cat $LEX_OUTPUT_ROOT.c` +_ACEOF +if ac_fn_c_try_link "$LINENO" +then : + ac_found=: +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext + if $ac_found; then + break + fi + done + LIBS="$ac_save_LIBS" + ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_lex" >&5 +printf "%s\n" "$ac_cv_lib_lex" >&6; } + if test "$ac_cv_lib_lex" = 'not found' +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: required lex library not found; giving up on $LEX" >&5 +printf "%s\n" "$as_me: WARNING: required lex library not found; giving up on $LEX" >&2;} + LEX=: LEXLIB= +elif test "$ac_cv_lib_lex" = 'none needed' +then : + LEXLIB='' +else case e in #( + e) LEXLIB=$ac_cv_lib_lex ;; +esac +fi + ac_save_LIBS="$LIBS" + LIBS= + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for library containing yywrap" >&5 +printf %s "checking for library containing yywrap... " >&6; } +if test ${ac_cv_search_yywrap+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_func_search_save_LIBS=$LIBS +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char yywrap (void); +int +main (void) +{ +return yywrap (); + ; + return 0; +} +_ACEOF +for ac_lib in '' fl l +do + if test -z "$ac_lib"; then + ac_res="none required" + else + ac_res=-l$ac_lib + LIBS="-l$ac_lib $ac_func_search_save_LIBS" + fi + if ac_fn_c_try_link "$LINENO" +then : + ac_cv_search_yywrap=$ac_res fi +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext + if test ${ac_cv_search_yywrap+y} +then : + break fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_lex_root" >&5 -$as_echo "$ac_cv_prog_lex_root" >&6; } -LEX_OUTPUT_ROOT=$ac_cv_prog_lex_root - -if test -z "${LEXLIB+set}"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: checking lex library" >&5 -$as_echo_n "checking lex library... " >&6; } -if ${ac_cv_lib_lex+:} false; then : - $as_echo_n "(cached) " >&6 -else +done +if test ${ac_cv_search_yywrap+y} +then : - ac_save_LIBS=$LIBS - ac_cv_lib_lex='none needed' - for ac_lib in '' -lfl -ll; do - LIBS="$ac_lib $ac_save_LIBS" - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -`cat $LEX_OUTPUT_ROOT.c` -_ACEOF -if ac_fn_c_try_link "$LINENO"; then : - ac_cv_lib_lex=$ac_lib +else case e in #( + e) ac_cv_search_yywrap=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ - conftest$ac_exeext conftest.$ac_ext - test "$ac_cv_lib_lex" != 'none needed' && break - done - LIBS=$ac_save_LIBS - +rm conftest.$ac_ext +LIBS=$ac_func_search_save_LIBS ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_yywrap" >&5 +printf "%s\n" "$ac_cv_search_yywrap" >&6; } +ac_res=$ac_cv_search_yywrap +if test "$ac_res" != no +then : + test "$ac_res" = "none required" || LIBS="$ac_res $LIBS" + LEXLIB="$LIBS" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_lex" >&5 -$as_echo "$ac_cv_lib_lex" >&6; } - test "$ac_cv_lib_lex" != 'none needed' && LEXLIB=$ac_cv_lib_lex + + LIBS="$ac_save_LIBS" ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether yytext is a pointer" >&5 -$as_echo_n "checking whether yytext is a pointer... " >&6; } -if ${ac_cv_prog_lex_yytext_pointer+:} false; then : - $as_echo_n "(cached) " >&6 -else - # POSIX says lex can declare yytext either as a pointer or an array; the +if test "$LEX" != : +then : + +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether yytext is a pointer" >&5 +printf %s "checking whether yytext is a pointer... " >&6; } +if test ${ac_cv_prog_lex_yytext_pointer+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) # POSIX says lex can declare yytext either as a pointer or an array; the # default is implementation-dependent. Figure out which it is, since # not all implementations provide the %pointer and %array declarations. ac_cv_prog_lex_yytext_pointer=no -ac_save_LIBS=$LIBS -LIBS="$LEXLIB $ac_save_LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #define YYTEXT_POINTER 1 `cat $LEX_OUTPUT_ROOT.c` _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : ac_cv_prog_lex_yytext_pointer=yes fi -rm -f core conftest.err conftest.$ac_objext \ - conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_save_LIBS - +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_lex_yytext_pointer" >&5 -$as_echo "$ac_cv_prog_lex_yytext_pointer" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_lex_yytext_pointer" >&5 +printf "%s\n" "$ac_cv_prog_lex_yytext_pointer" >&6; } if test $ac_cv_prog_lex_yytext_pointer = yes; then -$as_echo "#define YYTEXT_POINTER 1" >>confdefs.h +printf "%s\n" "#define YYTEXT_POINTER 1" >>confdefs.h + +fi fi rm -f conftest.l $LEX_OUTPUT_ROOT.c @@ -6159,38 +6975,44 @@ for ac_prog in 'bison -y' byacc do # Extract the first word of "$ac_prog", so it can be a program name with args. set dummy $ac_prog; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_prog_YACC+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test -n "$YACC"; then +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +printf %s "checking for $ac_word... " >&6; } +if test ${ac_cv_prog_YACC+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test -n "$YACC"; then ac_cv_prog_YACC="$YACC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_YACC="$ac_prog" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS -fi +fi ;; +esac fi YACC=$ac_cv_prog_YACC if test -n "$YACC"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $YACC" >&5 -$as_echo "$YACC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $YACC" >&5 +printf "%s\n" "$YACC" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -6203,22 +7025,24 @@ if test "$YACC" = ":" ; then elif echo "" | $YACC -V -v --version > /dev/null 2>&1 ; then : else - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $YACC may not work as yacc." >&5 -$as_echo "$as_me: WARNING: $YACC may not work as yacc." >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $YACC may not work as yacc." >&5 +printf "%s\n" "$as_me: WARNING: $YACC may not work as yacc." >&2;} fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for posix non-blocking" >&5 -$as_echo_n "checking for posix non-blocking... " >&6; } -if ${unet_cv_sys_nonblocking_posix+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test "$cross_compiling" = yes; then : - { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for posix non-blocking" >&5 +printf %s "checking for posix non-blocking... " >&6; } +if test ${unet_cv_sys_nonblocking_posix+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test "$cross_compiling" = yes +then : + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot run test program while cross compiling -See \`config.log' for more details" "$LINENO" 5; } -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +See 'config.log' for more details" "$LINENO" 5; } +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include #include @@ -6244,35 +7068,41 @@ int main(void) exit(1); } _ACEOF -if ac_fn_c_try_run "$LINENO"; then : +if ac_fn_c_try_run "$LINENO" +then : unet_cv_sys_nonblocking_posix=yes -else - unet_cv_sys_nonblocking_posix=no +else case e in #( + e) unet_cv_sys_nonblocking_posix=no ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_sys_nonblocking_posix" >&5 -$as_echo "$unet_cv_sys_nonblocking_posix" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_sys_nonblocking_posix" >&5 +printf "%s\n" "$unet_cv_sys_nonblocking_posix" >&6; } if test $unet_cv_sys_nonblocking_posix = yes; then -$as_echo "#define NBLOCK_POSIX /**/" >>confdefs.h +printf "%s\n" "#define NBLOCK_POSIX /**/" >>confdefs.h else -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for bsd non-blocking" >&5 -$as_echo_n "checking for bsd non-blocking... " >&6; } -if ${unet_cv_sys_nonblocking_bsd+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test "$cross_compiling" = yes; then : - { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for bsd non-blocking" >&5 +printf %s "checking for bsd non-blocking... " >&6; } +if test ${unet_cv_sys_nonblocking_bsd+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test "$cross_compiling" = yes +then : + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot run test program while cross compiling -See \`config.log' for more details" "$LINENO" 5; } -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +See 'config.log' for more details" "$LINENO" 5; } +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include #include @@ -6298,70 +7128,80 @@ int main(void) exit(1); } _ACEOF -if ac_fn_c_try_run "$LINENO"; then : +if ac_fn_c_try_run "$LINENO" +then : unet_cv_sys_nonblocking_bsd=yes -else - unet_cv_sys_nonblocking_bsd=no +else case e in #( + e) unet_cv_sys_nonblocking_bsd=no ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_sys_nonblocking_bsd" >&5 -$as_echo "$unet_cv_sys_nonblocking_bsd" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_sys_nonblocking_bsd" >&5 +printf "%s\n" "$unet_cv_sys_nonblocking_bsd" >&6; } if test $unet_cv_sys_nonblocking_bsd = yes; then -$as_echo "#define NBLOCK_BSD /**/" >>confdefs.h +printf "%s\n" "#define NBLOCK_BSD /**/" >>confdefs.h else -$as_echo "#define NBLOCK_SYSV /**/" >>confdefs.h +printf "%s\n" "#define NBLOCK_SYSV /**/" >>confdefs.h fi fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for posix signals" >&5 -$as_echo_n "checking for posix signals... " >&6; } -if ${unet_cv_sys_signal_posix+:} false; then : - $as_echo_n "(cached) " >&6 -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for posix signals" >&5 +printf %s "checking for posix signals... " >&6; } +if test ${unet_cv_sys_signal_posix+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include int -main () +main (void) { sigaction(SIGTERM, (struct sigaction *)0L, (struct sigaction *)0L) ; return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : +if ac_fn_c_try_compile "$LINENO" +then : unet_cv_sys_signal_posix=yes -else - unet_cv_sys_signal_posix=no +else case e in #( + e) unet_cv_sys_signal_posix=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_sys_signal_posix" >&5 -$as_echo "$unet_cv_sys_signal_posix" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_sys_signal_posix" >&5 +printf "%s\n" "$unet_cv_sys_signal_posix" >&6; } if test $unet_cv_sys_signal_posix = yes; then -$as_echo "#define POSIX_SIGNALS /**/" >>confdefs.h +printf "%s\n" "#define POSIX_SIGNALS /**/" >>confdefs.h else -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for bsd reliable signals" >&5 -$as_echo_n "checking for bsd reliable signals... " >&6; } -if ${unet_cv_sys_signal_bsd+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test "$cross_compiling" = yes; then : - { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for bsd reliable signals" >&5 +printf %s "checking for bsd reliable signals... " >&6; } +if test ${unet_cv_sys_signal_bsd+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) if test "$cross_compiling" = yes +then : + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot run test program while cross compiling -See \`config.log' for more details" "$LINENO" 5; } -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +See 'config.log' for more details" "$LINENO" 5; } +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include int calls = 0; @@ -6379,60 +7219,64 @@ int main(void) exit (0); } _ACEOF -if ac_fn_c_try_run "$LINENO"; then : +if ac_fn_c_try_run "$LINENO" +then : unet_cv_sys_signal_bsd=yes -else - unet_cv_sys_signal_bsd=no +else case e in #( + e) unet_cv_sys_signal_bsd=no ;; +esac fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_sys_signal_bsd" >&5 -$as_echo "$unet_cv_sys_signal_bsd" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_sys_signal_bsd" >&5 +printf "%s\n" "$unet_cv_sys_signal_bsd" >&6; } if test $unet_cv_sys_signal_bsd = yes; then -$as_echo "#define BSD_RELIABLE_SIGNALS /**/" >>confdefs.h +printf "%s\n" "#define BSD_RELIABLE_SIGNALS /**/" >>confdefs.h else -$as_echo "#define SYSV_UNRELIABLE_SIGNALS /**/" >>confdefs.h +printf "%s\n" "#define SYSV_UNRELIABLE_SIGNALS /**/" >>confdefs.h fi fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for OS-dependent information" >&5 -$as_echo_n "checking for OS-dependent information... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for OS-dependent information" >&5 +printf %s "checking for OS-dependent information... " >&6; } case "$host" in *-linux*) - { $as_echo "$as_me:${as_lineno-$LINENO}: result: Linux ($host) found." >&5 -$as_echo "Linux ($host) found." >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Linux ($host) found." >&5 +printf "%s\n" "Linux ($host) found." >&6; } unet_poll_syscall=yes ;; *-solaris*) - { $as_echo "$as_me:${as_lineno-$LINENO}: result: Solaris ($host) found." >&5 -$as_echo "Solaris ($host) found." >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Solaris ($host) found." >&5 +printf "%s\n" "Solaris ($host) found." >&6; } if test x"$ac_cv_header_poll_h" = xyes; then unet_poll_syscall=yes else unet_poll_syscall=no fi -$as_echo "#define IRCU_SOLARIS 1" >>confdefs.h +printf "%s\n" "#define IRCU_SOLARIS 1" >>confdefs.h ;; *-sunos*) - { $as_echo "$as_me:${as_lineno-$LINENO}: result: Solaris ($host) found." >&5 -$as_echo "Solaris ($host) found." >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Solaris ($host) found." >&5 +printf "%s\n" "Solaris ($host) found." >&6; } unet_poll_syscall=no ;; *-openbsd*) - { $as_echo "$as_me:${as_lineno-$LINENO}: result: OpenBSD ($host) found." >&5 -$as_echo "OpenBSD ($host) found." >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: OpenBSD ($host) found." >&5 +printf "%s\n" "OpenBSD ($host) found." >&6; } if test x"$ac_cv_header_poll_h" = xyes; then unet_poll_syscall=yes else @@ -6441,8 +7285,8 @@ $as_echo "OpenBSD ($host) found." >&6; } ;; *-*bsd*) - { $as_echo "$as_me:${as_lineno-$LINENO}: result: Generic BSD ($host) found." >&5 -$as_echo "Generic BSD ($host) found." >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Generic BSD ($host) found." >&5 +printf "%s\n" "Generic BSD ($host) found." >&6; } if test x"$ac_cv_header_poll_h" = xyes; then unet_poll_syscall=yes else @@ -6451,32 +7295,36 @@ $as_echo "Generic BSD ($host) found." >&6; } ;; *-darwin*) - { $as_echo "$as_me:${as_lineno-$LINENO}: result: Darwin (Mac OS X) ($host) found." >&5 -$as_echo "Darwin (Mac OS X) ($host) found." >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Darwin (Mac OS X) ($host) found." >&5 +printf "%s\n" "Darwin (Mac OS X) ($host) found." >&6; } unet_poll_syscall=no ;; *) - { $as_echo "$as_me:${as_lineno-$LINENO}: result: Unknown system type $host found." >&5 -$as_echo "Unknown system type $host found." >&6; } - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: Unknown OS type; using generic routines." >&5 -$as_echo "$as_me: WARNING: Unknown OS type; using generic routines." >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Unknown system type $host found." >&5 +printf "%s\n" "Unknown system type $host found." >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: Unknown OS type; using generic routines." >&5 +printf "%s\n" "$as_me: WARNING: Unknown OS type; using generic routines." >&2;} unet_poll_syscall=no ;; esac -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable use of poll()" >&5 -$as_echo_n "checking whether to enable use of poll()... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable use of poll()" >&5 +printf %s "checking whether to enable use of poll()... " >&6; } # Check whether --enable-poll was given. -if test "${enable_poll+set}" = set; then : +if test ${enable_poll+y} +then : enableval=$enable_poll; unet_cv_enable_poll=$enable_poll -else - if ${unet_cv_enable_poll+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_enable_poll=$unet_poll_syscall +else case e in #( + e) if test ${unet_cv_enable_poll+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_enable_poll=$unet_poll_syscall ;; +esac fi - + ;; +esac fi @@ -6485,12 +7333,12 @@ if test x"$ac_cv_header_poll_h" != xyes; then unet_cv_enable_poll=no fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_poll" >&5 -$as_echo "$unet_cv_enable_poll" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_poll" >&5 +printf "%s\n" "$unet_cv_enable_poll" >&6; } if test x"$unet_cv_enable_poll" = xyes; then -$as_echo "#define USE_POLL 1" >>confdefs.h +printf "%s\n" "#define USE_POLL 1" >>confdefs.h ENGINE_C=engine_poll.c else @@ -6498,46 +7346,54 @@ else fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable debug mode" >&5 -$as_echo_n "checking whether to enable debug mode... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable debug mode" >&5 +printf %s "checking whether to enable debug mode... " >&6; } # Check whether --enable-debug was given. -if test "${enable_debug+set}" = set; then : +if test ${enable_debug+y} +then : enableval=$enable_debug; unet_cv_enable_debug=$enable_debug -else - if ${unet_cv_enable_debug+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_enable_debug=no +else case e in #( + e) if test ${unet_cv_enable_debug+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_enable_debug=no ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_debug" >&5 -$as_echo "$unet_cv_enable_debug" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_debug" >&5 +printf "%s\n" "$unet_cv_enable_debug" >&6; } if test x"$unet_cv_enable_debug" = xyes; then -$as_echo "#define DEBUGMODE 1" >>confdefs.h +printf "%s\n" "#define DEBUGMODE 1" >>confdefs.h fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable leak detection" >&5 -$as_echo_n "checking whether to enable leak detection... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable leak detection" >&5 +printf %s "checking whether to enable leak detection... " >&6; } # Check whether --with-leak-detect was given. -if test "${with_leak_detect+set}" = set; then : +if test ${with_leak_detect+y} +then : withval=$with_leak_detect; unet_cv_with_leak_detect=$with_leak_detect -else - if ${unet_cv_with_leak_detect+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_with_leak_detect=no +else case e in #( + e) if test ${unet_cv_with_leak_detect+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_with_leak_detect=no ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_leak_detect" >&5 -$as_echo "$unet_cv_enable_leak_detect" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_leak_detect" >&5 +printf "%s\n" "$unet_cv_enable_leak_detect" >&6; } if test x"$unet_cv_with_leak_detect" != xno; then LIBS="-lgc $LIBS" @@ -6549,149 +7405,177 @@ fi # Check whether --with-ipv6 was given. -if test "${with_ipv6+set}" = set; then : +if test ${with_ipv6+y} +then : withval=$with_ipv6; ac_cv_use_ipv6=$withval -else - ac_cv_use_ipv6=$unet_have_sockaddr_in6 +else case e in #( + e) ac_cv_use_ipv6=$unet_have_sockaddr_in6 ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to use IPv6" >&5 -$as_echo_n "checking whether to use IPv6... " >&6; } -if ${ac_cv_use_ipv6+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_cv_use_ipv6=no +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to use IPv6" >&5 +printf %s "checking whether to use IPv6... " >&6; } +if test ${ac_cv_use_ipv6+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_cv_use_ipv6=no ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_use_ipv6" >&5 -$as_echo "$ac_cv_use_ipv6" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_use_ipv6" >&5 +printf "%s\n" "$ac_cv_use_ipv6" >&6; } if test x"$ac_cv_use_ipv6" != "xno" ; then -$as_echo "#define IPV6 1" >>confdefs.h +printf "%s\n" "#define IPV6 1" >>confdefs.h fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable asserts" >&5 -$as_echo_n "checking whether to enable asserts... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable asserts" >&5 +printf %s "checking whether to enable asserts... " >&6; } # Check whether --enable-asserts was given. -if test "${enable_asserts+set}" = set; then : +if test ${enable_asserts+y} +then : enableval=$enable_asserts; unet_cv_enable_asserts=$enable_asserts -else - if ${unet_cv_enable_asserts+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_enable_asserts=yes +else case e in #( + e) if test ${unet_cv_enable_asserts+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_enable_asserts=yes ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_asserts" >&5 -$as_echo "$unet_cv_enable_asserts" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_asserts" >&5 +printf "%s\n" "$unet_cv_enable_asserts" >&6; } if test x"$unet_cv_enable_asserts" = xno; then -$as_echo "#define NDEBUG 1" >>confdefs.h +printf "%s\n" "#define NDEBUG 1" >>confdefs.h fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable profiling support (gprof)" >&5 -$as_echo_n "checking whether to enable profiling support (gprof)... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable profiling support (gprof)" >&5 +printf %s "checking whether to enable profiling support (gprof)... " >&6; } # Check whether --enable-profile was given. -if test "${enable_profile+set}" = set; then : +if test ${enable_profile+y} +then : enableval=$enable_profile; unet_cv_enable_profile=$enable_profile -else - if ${unet_cv_enable_profile+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_enable_profile=no +else case e in #( + e) if test ${unet_cv_enable_profile+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_enable_profile=no ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_profile" >&5 -$as_echo "$unet_cv_enable_profile" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_profile" >&5 +printf "%s\n" "$unet_cv_enable_profile" >&6; } if test x"$unet_cv_enable_profile" = xyes; then CFLAGS="-pg $CFLAGS" LDFLAGS="-pg $LDFLAGS" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable pedantic compiler warnings" >&5 -$as_echo_n "checking whether to enable pedantic compiler warnings... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable pedantic compiler warnings" >&5 +printf %s "checking whether to enable pedantic compiler warnings... " >&6; } # Check whether --enable-pedantic was given. -if test "${enable_pedantic+set}" = set; then : +if test ${enable_pedantic+y} +then : enableval=$enable_pedantic; unet_cv_enable_pedantic=$enable_pedantic -else - if ${unet_cv_enable_pedantic+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_enable_pedantic=no +else case e in #( + e) if test ${unet_cv_enable_pedantic+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_enable_pedantic=no ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_pedantic" >&5 -$as_echo "$unet_cv_enable_pedantic" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_pedantic" >&5 +printf "%s\n" "$unet_cv_enable_pedantic" >&6; } if test x"$unet_cv_enable_pedantic" = xyes; then CFLAGS="-pedantic $CFLAGS" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable compiler warnings" >&5 -$as_echo_n "checking whether to enable compiler warnings... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable compiler warnings" >&5 +printf %s "checking whether to enable compiler warnings... " >&6; } # Check whether --enable-warnings was given. -if test "${enable_warnings+set}" = set; then : +if test ${enable_warnings+y} +then : enableval=$enable_warnings; unet_cv_enable_warnings=$enable_warnings -else - if ${unet_cv_enable_warnings+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_enable_warnings=no +else case e in #( + e) if test ${unet_cv_enable_warnings+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_enable_warnings=no ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_warnings" >&5 -$as_echo "$unet_cv_enable_warnings" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_warnings" >&5 +printf "%s\n" "$unet_cv_enable_warnings" >&6; } if test x"$unet_cv_enable_warnings" = xyes; then CFLAGS="-Wall $CFLAGS" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable inlining for a few critical functions" >&5 -$as_echo_n "checking whether to enable inlining for a few critical functions... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable inlining for a few critical functions" >&5 +printf %s "checking whether to enable inlining for a few critical functions... " >&6; } # Check whether --enable-inlines was given. -if test "${enable_inlines+set}" = set; then : +if test ${enable_inlines+y} +then : enableval=$enable_inlines; unet_cv_enable_inlines=$enable_inlines -else - if ${unet_cv_enable_inlines+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_enable_inlines=yes +else case e in #( + e) if test ${unet_cv_enable_inlines+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_enable_inlines=yes ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_inlines" >&5 -$as_echo "$unet_cv_enable_inlines" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_inlines" >&5 +printf "%s\n" "$unet_cv_enable_inlines" >&6; } if test x"$unet_cv_enable_inlines" = xyes; then -$as_echo "#define FORCEINLINE 1" >>confdefs.h +printf "%s\n" "#define FORCEINLINE 1" >>confdefs.h fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable the /dev/poll event engine" >&5 -$as_echo_n "checking whether to enable the /dev/poll event engine... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable the /dev/poll event engine" >&5 +printf %s "checking whether to enable the /dev/poll event engine... " >&6; } # Check whether --enable-devpoll was given. -if test "${enable_devpoll+set}" = set; then : +if test ${enable_devpoll+y} +then : enableval=$enable_devpoll; unet_cv_enable_devpoll=$enable_devpoll -else - if ${unet_cv_enable_devpoll+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_enable_devpoll=yes +else case e in #( + e) if test ${unet_cv_enable_devpoll+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_enable_devpoll=yes ;; +esac fi - + ;; +esac fi @@ -6699,28 +7583,32 @@ if test x"$ac_cv_header_sys_devpoll_h" = xno; then unet_cv_enable_devpoll=no fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_devpoll" >&5 -$as_echo "$unet_cv_enable_devpoll" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_devpoll" >&5 +printf "%s\n" "$unet_cv_enable_devpoll" >&6; } if test x"$unet_cv_enable_devpoll" != xno; then -$as_echo "#define USE_DEVPOLL 1" >>confdefs.h +printf "%s\n" "#define USE_DEVPOLL 1" >>confdefs.h ENGINE_C="engine_devpoll.c $ENGINE_C" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable the kqueue event engine" >&5 -$as_echo_n "checking whether to enable the kqueue event engine... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable the kqueue event engine" >&5 +printf %s "checking whether to enable the kqueue event engine... " >&6; } # Check whether --enable-kqueue was given. -if test "${enable_kqueue+set}" = set; then : +if test ${enable_kqueue+y} +then : enableval=$enable_kqueue; unet_cv_enable_kqueue=$enable_kqueue -else - if ${unet_cv_enable_kqueue+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_enable_kqueue=yes +else case e in #( + e) if test ${unet_cv_enable_kqueue+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_enable_kqueue=yes ;; +esac fi - + ;; +esac fi @@ -6728,28 +7616,32 @@ if test x"$ac_cv_header_sys_event_h" = xno -o x"$ac_cv_func_kqueue" = xno; then unet_cv_enable_kqueue=no fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_kqueue" >&5 -$as_echo "$unet_cv_enable_kqueue" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_kqueue" >&5 +printf "%s\n" "$unet_cv_enable_kqueue" >&6; } if test x"$unet_cv_enable_kqueue" != xno; then -$as_echo "#define USE_KQUEUE 1" >>confdefs.h +printf "%s\n" "#define USE_KQUEUE 1" >>confdefs.h ENGINE_C="engine_kqueue.c $ENGINE_C" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable the epoll event engine" >&5 -$as_echo_n "checking whether to enable the epoll event engine... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable the epoll event engine" >&5 +printf %s "checking whether to enable the epoll event engine... " >&6; } # Check whether --enable-epoll was given. -if test "${enable_epoll+set}" = set; then : +if test ${enable_epoll+y} +then : enableval=$enable_epoll; unet_cv_enable_epoll=$enable_epoll -else - if ${unet_cv_enable_epoll+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_enable_epoll=yes +else case e in #( + e) if test ${unet_cv_enable_epoll+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_enable_epoll=yes ;; +esac fi - + ;; +esac fi @@ -6757,120 +7649,134 @@ if test x"$ac_cv_header_sys_epoll_h" = xno -o x"$ac_cv_func_epoll" = xno; then unet_cv_enable_epoll=no fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_epoll" >&5 -$as_echo "$unet_cv_enable_epoll" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_epoll" >&5 +printf "%s\n" "$unet_cv_enable_epoll" >&6; } if test x"$unet_cv_enable_epoll" != xno; then - { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether epoll functions are properly defined" >&5 -$as_echo_n "checking whether epoll functions are properly defined... " >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether epoll functions are properly defined" >&5 +printf %s "checking whether epoll functions are properly defined... " >&6; } cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include int -main () +main (void) { epoll_create(10); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : - { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -$as_echo "yes" >&6; } -else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } - -$as_echo "#define EPOLL_NEED_BODY 1" >>confdefs.h +if ac_fn_c_try_link "$LINENO" +then : + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } +printf "%s\n" "#define EPOLL_NEED_BODY 1" >>confdefs.h + ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -$as_echo "#define USE_EPOLL 1" >>confdefs.h +printf "%s\n" "#define USE_EPOLL 1" >>confdefs.h ENGINE_C="engine_epoll.c $ENGINE_C" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for va_copy" >&5 -$as_echo_n "checking for va_copy... " >&6; } -if ${unet_cv_c_va_copy+:} false; then : - $as_echo_n "(cached) " >&6 -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for va_copy" >&5 +printf %s "checking for va_copy... " >&6; } +if test ${unet_cv_c_va_copy+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include int -main () +main (void) { va_list ap1, ap2; va_copy(ap1, ap2); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : unet_cv_c_va_copy="yes" -else - unet_cv_c_va_copy="no" - +else case e in #( + e) unet_cv_c_va_copy="no" + ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ - conftest$ac_exeext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_c_va_copy" >&5 -$as_echo "$unet_cv_c_va_copy" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_c_va_copy" >&5 +printf "%s\n" "$unet_cv_c_va_copy" >&6; } if test "$unet_cv_c_va_copy" = "yes" ; then -$as_echo "#define HAVE_VA_COPY 1" >>confdefs.h +printf "%s\n" "#define HAVE_VA_COPY 1" >>confdefs.h fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for __va_copy" >&5 -$as_echo_n "checking for __va_copy... " >&6; } -if ${unet_cv_c___va_copy+:} false; then : - $as_echo_n "(cached) " >&6 -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for __va_copy" >&5 +printf %s "checking for __va_copy... " >&6; } +if test ${unet_cv_c___va_copy+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ #include int -main () +main (void) { va_list ap1, ap2; __va_copy(ap1, ap2); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : unet_cv_c___va_copy="yes" -else - unet_cv_c___va_copy="no" - +else case e in #( + e) unet_cv_c___va_copy="no" + ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ - conftest$ac_exeext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_c___va_copy" >&5 -$as_echo "$unet_cv_c___va_copy" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_c___va_copy" >&5 +printf "%s\n" "$unet_cv_c___va_copy" >&6; } if test "$unet_cv_c___va_copy" = "yes" ; then -$as_echo "#define HAVE___VA_COPY 1" >>confdefs.h +printf "%s\n" "#define HAVE___VA_COPY 1" >>confdefs.h fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking what name to give the symlink" >&5 -$as_echo_n "checking what name to give the symlink... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking what name to give the symlink" >&5 +printf %s "checking what name to give the symlink... " >&6; } # Check whether --with-symlink was given. -if test "${with_symlink+set}" = set; then : +if test ${with_symlink+y} +then : withval=$with_symlink; unet_cv_with_symlink=$with_symlink -else - if ${unet_cv_with_symlink+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_with_symlink="ircd" +else case e in #( + e) if test ${unet_cv_with_symlink+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_with_symlink="ircd" ;; +esac fi - + ;; +esac fi @@ -6878,8 +7784,8 @@ if test x"$unet_cv_with_symlink" = xyes; then unet_cv_with_symlink="ircd" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_symlink" >&5 -$as_echo "$unet_cv_with_symlink" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_symlink" >&5 +printf "%s\n" "$unet_cv_with_symlink" >&6; } if test x"$unet_cv_with_symlink" = xno; then INSTALL_RULE=install-no-symlink @@ -6891,19 +7797,23 @@ fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking what permissions to set on the installed binary" >&5 -$as_echo_n "checking what permissions to set on the installed binary... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking what permissions to set on the installed binary" >&5 +printf %s "checking what permissions to set on the installed binary... " >&6; } # Check whether --with-mode was given. -if test "${with_mode+set}" = set; then : +if test ${with_mode+y} +then : withval=$with_mode; unet_cv_with_mode=$with_mode -else - if ${unet_cv_with_mode+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_with_mode=711 +else case e in #( + e) if test ${unet_cv_with_mode+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_with_mode=711 ;; +esac fi - + ;; +esac fi @@ -6911,26 +7821,30 @@ if test x"$unet_cv_with_mode" = xyes -o x"$unet_cv_with_mode" = xno; then unet_cv_with_mode=711 fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_mode" >&5 -$as_echo "$unet_cv_with_mode" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_mode" >&5 +printf "%s\n" "$unet_cv_with_mode" >&6; } IRCDMODE=$unet_cv_with_mode unet_uid=`id | sed -e 's/.*uid=[0-9]*(//' -e 's/).*//' 2> /dev/null` -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking which user should own the installed binary" >&5 -$as_echo_n "checking which user should own the installed binary... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking which user should own the installed binary" >&5 +printf %s "checking which user should own the installed binary... " >&6; } # Check whether --with-owner was given. -if test "${with_owner+set}" = set; then : +if test ${with_owner+y} +then : withval=$with_owner; unet_cv_with_owner=$with_owner -else - if ${unet_cv_with_owner+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_with_owner=$unet_uid +else case e in #( + e) if test ${unet_cv_with_owner+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_with_owner=$unet_uid ;; +esac fi - + ;; +esac fi @@ -6938,26 +7852,30 @@ if test x"$unet_cv_with_owner" = xyes -o x"$unet_cv_with_owner" = xno; then unet_cv_with_owner=$unet_uid fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_owner" >&5 -$as_echo "$unet_cv_with_owner" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_owner" >&5 +printf "%s\n" "$unet_cv_with_owner" >&6; } IRCDOWN=$unet_cv_with_owner unet_gid=`id | sed -e 's/.*gid=[0-9]*(//' -e 's/).*//' 2> /dev/null` -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking which group should own the installed binary" >&5 -$as_echo_n "checking which group should own the installed binary... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking which group should own the installed binary" >&5 +printf %s "checking which group should own the installed binary... " >&6; } # Check whether --with-group was given. -if test "${with_group+set}" = set; then : +if test ${with_group+y} +then : withval=$with_group; unet_cv_with_group=$with_group -else - if ${unet_cv_with_group+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_with_group=$unet_gid +else case e in #( + e) if test ${unet_cv_with_group+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_with_group=$unet_gid ;; +esac fi - + ;; +esac fi @@ -6965,8 +7883,8 @@ if test x"$unet_cv_with_group" = xyes -o x"$unet_cv_with_group" = xno; then unet_cv_with_group=$unet_gid fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_group" >&5 -$as_echo "$unet_cv_with_group" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_group" >&5 +printf "%s\n" "$unet_cv_with_group" >&6; } IRCDGRP=$unet_cv_with_group @@ -6978,19 +7896,23 @@ if test -f /etc/resolv.conf; then unet_domain=`awk '/^search/ { print $2; exit }' /etc/resolv.conf` fi fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for site domain name" >&5 -$as_echo_n "checking for site domain name... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for site domain name" >&5 +printf %s "checking for site domain name... " >&6; } # Check whether --with-domain was given. -if test "${with_domain+set}" = set; then : +if test ${with_domain+y} +then : withval=$with_domain; unet_cv_with_domain=$with_domain -else - if ${unet_cv_with_domain+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_with_domain=$unet_domain +else case e in #( + e) if test ${unet_cv_with_domain+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_with_domain=$unet_domain ;; +esac fi - + ;; +esac fi @@ -7001,28 +7923,30 @@ if test x"$unet_cv_with_domain" = xno; then as_fn_error $? "Unable to determine server DNS domain; use --with-domain to set it" "$LINENO" 5 fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_domain" >&5 -$as_echo "$unet_cv_with_domain" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_domain" >&5 +printf "%s\n" "$unet_cv_with_domain" >&6; } -cat >>confdefs.h <<_ACEOF -#define DOMAINNAME "*$unet_cv_with_domain" -_ACEOF +printf "%s\n" "#define DOMAINNAME \"*$unet_cv_with_domain\"" >>confdefs.h -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking if chroot operation is desired" >&5 -$as_echo_n "checking if chroot operation is desired... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if chroot operation is desired" >&5 +printf %s "checking if chroot operation is desired... " >&6; } # Check whether --with-chroot was given. -if test "${with_chroot+set}" = set; then : +if test ${with_chroot+y} +then : withval=$with_chroot; unet_cv_with_chroot=$with_chroot -else - if ${unet_cv_with_chroot+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_with_chroot=no +else case e in #( + e) if test ${unet_cv_with_chroot+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_with_chroot=no ;; +esac fi - + ;; +esac fi @@ -7033,8 +7957,8 @@ fi # Ensure there are no trailing /'s to mess us up unet_cv_with_chroot=`echo "$unet_cv_with_chroot" | sed 's%/*$%%'` -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_chroot" >&5 -$as_echo "$unet_cv_with_chroot" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_chroot" >&5 +printf "%s\n" "$unet_cv_with_chroot" >&6; } # Deal with the annoying value "NONE" here unet_save_prefix=$prefix @@ -7059,44 +7983,46 @@ unet_libdir=`eval echo "$libdir"` prefix=$unet_save_prefix exec_prefix=$unet_save_exec_prefix -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking where the binary will be for /restart" >&5 -$as_echo_n "checking where the binary will be for /restart... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking where the binary will be for /restart" >&5 +printf %s "checking where the binary will be for /restart... " >&6; } if test x"$unet_cv_with_symlink" = xno; then unet_spath="$unet_bindir/ircd" else unet_spath="$unet_bindir/$unet_cv_with_symlink" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_spath" >&5 -$as_echo "$unet_spath" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_spath" >&5 +printf "%s\n" "$unet_spath" >&6; } if test x"$unet_cv_with_chroot" != xno; then if echo "$unet_spath" | grep "^$unet_cv_with_chroot" > /dev/null 2>&1; then unet_spath=`echo "$unet_spath" | sed "s%^$unet_cv_with_chroot%%"` else - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: Binary $unet_spath not relative to root directory $unet_cv_with_chroot; restarts will probably fail" >&5 -$as_echo "$as_me: WARNING: Binary $unet_spath not relative to root directory $unet_cv_with_chroot; restarts will probably fail" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: Binary $unet_spath not relative to root directory $unet_cv_with_chroot; restarts will probably fail" >&5 +printf "%s\n" "$as_me: WARNING: Binary $unet_spath not relative to root directory $unet_cv_with_chroot; restarts will probably fail" >&2;} fi fi -cat >>confdefs.h <<_ACEOF -#define SPATH "$unet_spath" -_ACEOF +printf "%s\n" "#define SPATH \"$unet_spath\"" >>confdefs.h -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking what the data directory should be" >&5 -$as_echo_n "checking what the data directory should be... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking what the data directory should be" >&5 +printf %s "checking what the data directory should be... " >&6; } # Check whether --with-dpath was given. -if test "${with_dpath+set}" = set; then : +if test ${with_dpath+y} +then : withval=$with_dpath; unet_cv_with_dpath=$with_dpath -else - if ${unet_cv_with_dpath+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_with_dpath=$unet_libdir +else case e in #( + e) if test ${unet_cv_with_dpath+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_with_dpath=$unet_libdir ;; +esac fi - + ;; +esac fi @@ -7107,8 +8033,8 @@ fi # Ensure there are no trailing /'s to mess us up unet_cv_with_dpath=`echo "$unet_cv_with_dpath" | sed 's%/*$%%'` -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_dpath" >&5 -$as_echo "$unet_cv_with_dpath" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_dpath" >&5 +printf "%s\n" "$unet_cv_with_dpath" >&6; } if test x"$unet_cv_with_chroot" != xno; then if echo "$unet_cv_with_dpath" | grep "^$unet_cv_with_chroot" > /dev/null 2>&1; then @@ -7121,27 +8047,29 @@ else fi -cat >>confdefs.h <<_ACEOF -#define DPATH "$unet_dpath" -_ACEOF +printf "%s\n" "#define DPATH \"$unet_dpath\"" >>confdefs.h DPATH=$unet_cv_with_dpath -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking where the default configuration file resides" >&5 -$as_echo_n "checking where the default configuration file resides... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking where the default configuration file resides" >&5 +printf %s "checking where the default configuration file resides... " >&6; } # Check whether --with-cpath was given. -if test "${with_cpath+set}" = set; then : +if test ${with_cpath+y} +then : withval=$with_cpath; unet_cv_with_cpath=$with_cpath -else - if ${unet_cv_with_cpath+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_with_cpath="ircd.conf" +else case e in #( + e) if test ${unet_cv_with_cpath+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_with_cpath="ircd.conf" ;; +esac fi - + ;; +esac fi @@ -7149,8 +8077,8 @@ if test x"$unet_cv_with_cpath" = xyes -o x"$unet_cv_with_cpath" = xno; then unet_cv_with_cpath="ircd.conf" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_cpath" >&5 -$as_echo "$unet_cv_with_cpath" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_cpath" >&5 +printf "%s\n" "$unet_cv_with_cpath" >&6; } if echo "$unet_cv_with_cpath" | grep '^/' > /dev/null 2>&1; then # Absolute path; check against chroot stuff @@ -7168,24 +8096,26 @@ else fi -cat >>confdefs.h <<_ACEOF -#define CPATH "$unet_cpath" -_ACEOF +printf "%s\n" "#define CPATH \"$unet_cpath\"" >>confdefs.h -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking where to put the debugging log if debugging enabled" >&5 -$as_echo_n "checking where to put the debugging log if debugging enabled... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking where to put the debugging log if debugging enabled" >&5 +printf %s "checking where to put the debugging log if debugging enabled... " >&6; } # Check whether --with-lpath was given. -if test "${with_lpath+set}" = set; then : +if test ${with_lpath+y} +then : withval=$with_lpath; unet_cv_with_lpath=$with_lpath -else - if ${unet_cv_with_lpath+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_with_lpath="ircd.log" +else case e in #( + e) if test ${unet_cv_with_lpath+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_with_lpath="ircd.log" ;; +esac fi - + ;; +esac fi @@ -7193,8 +8123,8 @@ if test x"$unet_cv_with_lpath" = xyes -o x"$unet_cv_with_lpath" = xno; then unet_cv_with_lpath="ircd.log" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_lpath" >&5 -$as_echo "$unet_cv_with_lpath" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_lpath" >&5 +printf "%s\n" "$unet_cv_with_lpath" >&6; } if echo "$unet_cv_with_lpath" | grep '^/' > /dev/null 2>&1; then # Absolute path; check against chroot stuff @@ -7202,8 +8132,8 @@ if echo "$unet_cv_with_lpath" | grep '^/' > /dev/null 2>&1; then if echo "$unet_cv_with_lpath" | grep "^$unet_cv_with_chroot" > /dev/null 2>&1; then unet_lpath=`echo "$unet_cv_with_lpath" | sed "s%^$unet_cv_with_chroot%%"` else - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: Log file $unet_cv_with_lpath not relative to root directory $unet_cv_with_chroot; using default ircd.log instead" >&5 -$as_echo "$as_me: WARNING: Log file $unet_cv_with_lpath not relative to root directory $unet_cv_with_chroot; using default ircd.log instead" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: Log file $unet_cv_with_lpath not relative to root directory $unet_cv_with_chroot; using default ircd.log instead" >&5 +printf "%s\n" "$as_me: WARNING: Log file $unet_cv_with_lpath not relative to root directory $unet_cv_with_chroot; using default ircd.log instead" >&2;} unet_cv_with_lpath="ircd.log" unet_lpath="ircd.log" fi @@ -7215,80 +8145,82 @@ else fi -cat >>confdefs.h <<_ACEOF -#define LPATH "$unet_lpath" -_ACEOF +printf "%s\n" "#define LPATH \"$unet_lpath\"" >>confdefs.h -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for /dev/null" >&5 -$as_echo_n "checking for /dev/null... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for /dev/null" >&5 +printf %s "checking for /dev/null... " >&6; } if test -c /dev/null ; then -$as_echo "#define PATH_DEVNULL \"/dev/null\"" >>confdefs.h +printf "%s\n" "#define PATH_DEVNULL \"/dev/null\"" >>confdefs.h - { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -$as_echo "yes" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } else -$as_echo "#define PATH_DEVNULL \"devnull.log\"" >>confdefs.h +printf "%s\n" "#define PATH_DEVNULL \"devnull.log\"" >>confdefs.h - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no - using devnull.log" >&5 -$as_echo "no - using devnull.log" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no - using devnull.log" >&5 +printf "%s\n" "no - using devnull.log" >&6; } fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable OpenSSL support" >&5 -$as_echo_n "checking whether to enable OpenSSL support... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable OpenSSL support" >&5 +printf %s "checking whether to enable OpenSSL support... " >&6; } # Check whether --enable-ssl was given. -if test "${enable_ssl+set}" = set; then : +if test ${enable_ssl+y} +then : enableval=$enable_ssl; unet_cv_enable_ssl=$enable_ssl -else - if ${unet_cv_enable_ssl+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_enable_ssl=yes +else case e in #( + e) if test ${unet_cv_enable_ssl+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_enable_ssl=yes ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_ssl" >&5 -$as_echo "$unet_cv_enable_ssl" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_ssl" >&5 +printf "%s\n" "$unet_cv_enable_ssl" >&6; } if test x"$unet_cv_enable_ssl" = xyes; then - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for OpenSSL includes" >&5 -$as_echo_n "checking for OpenSSL includes... " >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for OpenSSL includes" >&5 +printf %s "checking for OpenSSL includes... " >&6; } # Check whether --with-openssl-includes was given. -if test "${with_openssl_includes+set}" = set; then : +if test ${with_openssl_includes+y} +then : withval=$with_openssl_includes; base_ssl_inc=$withval -else - base_ssl_inc=/usr/include +else case e in #( + e) base_ssl_inc=/usr/include ;; +esac fi unet_cv_with_openssl_inc_prefix=$base_ssl_inc - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_openssl_inc_prefix" >&5 -$as_echo "$unet_cv_with_openssl_inc_prefix" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_openssl_inc_prefix" >&5 +printf "%s\n" "$unet_cv_with_openssl_inc_prefix" >&6; } -cat >>confdefs.h <<_ACEOF -#define SSL_INCLUDES_PATH "$base_ssl_inc" -_ACEOF +printf "%s\n" "#define SSL_INCLUDES_PATH \"$base_ssl_inc\"" >>confdefs.h - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for OpenSSL libraries" >&5 -$as_echo_n "checking for OpenSSL libraries... " >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for OpenSSL libraries" >&5 +printf %s "checking for OpenSSL libraries... " >&6; } # Check whether --with-openssl-libs was given. -if test "${with_openssl_libs+set}" = set; then : +if test ${with_openssl_libs+y} +then : withval=$with_openssl_libs; unet_cv_with_openssl_prefix=$withval -else - unet_cv_with_openssl_prefix=/usr/lib +else case e in #( + e) unet_cv_with_openssl_prefix=/usr/lib ;; +esac fi - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_openssl_prefix" >&5 -$as_echo "$unet_cv_with_openssl_prefix" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_openssl_prefix" >&5 +printf "%s\n" "$unet_cv_with_openssl_prefix" >&6; } -cat >>confdefs.h <<_ACEOF -#define SSL_LIBS_PATH "$unet_cv_with_openssl_prefix" -_ACEOF +printf "%s\n" "#define SSL_LIBS_PATH \"$unet_cv_with_openssl_prefix\"" >>confdefs.h save_CFLAGS=$CFLAGS @@ -7297,127 +8229,152 @@ _ACEOF CFLAGS="-I$unet_cv_with_openssl_inc_prefix -lcrypto" LIBS="-Wl,-rpath=$unet_cv_with_openssl_prefix -L$unet_cv_with_openssl_prefix -lssl -lcrypto" - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for SSL_read in -lssl" >&5 -$as_echo_n "checking for SSL_read in -lssl... " >&6; } -if ${ac_cv_lib_ssl_SSL_read+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for SSL_read in -lssl" >&5 +printf %s "checking for SSL_read in -lssl... " >&6; } +if test ${ac_cv_lib_ssl_SSL_read+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lssl $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ #ifdef __cplusplus extern "C" #endif -char SSL_read (); +char SSL_read (void); int -main () +main (void) { return SSL_read (); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : ac_cv_lib_ssl_SSL_read=yes -else - ac_cv_lib_ssl_SSL_read=no +else case e in #( + e) ac_cv_lib_ssl_SSL_read=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_ssl_SSL_read" >&5 -$as_echo "$ac_cv_lib_ssl_SSL_read" >&6; } -if test "x$ac_cv_lib_ssl_SSL_read" = xyes; then : - - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for X509_new in -lcrypto" >&5 -$as_echo_n "checking for X509_new in -lcrypto... " >&6; } -if ${ac_cv_lib_crypto_X509_new+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_ssl_SSL_read" >&5 +printf "%s\n" "$ac_cv_lib_ssl_SSL_read" >&6; } +if test "x$ac_cv_lib_ssl_SSL_read" = xyes +then : + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for X509_new in -lcrypto" >&5 +printf %s "checking for X509_new in -lcrypto... " >&6; } +if test ${ac_cv_lib_crypto_X509_new+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lcrypto $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ #ifdef __cplusplus extern "C" #endif -char X509_new (); +char X509_new (void); int -main () +main (void) { return X509_new (); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : ac_cv_lib_crypto_X509_new=yes -else - ac_cv_lib_crypto_X509_new=no +else case e in #( + e) ac_cv_lib_crypto_X509_new=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_crypto_X509_new" >&5 -$as_echo "$ac_cv_lib_crypto_X509_new" >&6; } -if test "x$ac_cv_lib_crypto_X509_new" = xyes; then : - - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for EVP_sha256 in -lcrypto" >&5 -$as_echo_n "checking for EVP_sha256 in -lcrypto... " >&6; } -if ${ac_cv_lib_crypto_EVP_sha256+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_crypto_X509_new" >&5 +printf "%s\n" "$ac_cv_lib_crypto_X509_new" >&6; } +if test "x$ac_cv_lib_crypto_X509_new" = xyes +then : + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for EVP_sha256 in -lcrypto" >&5 +printf %s "checking for EVP_sha256 in -lcrypto... " >&6; } +if test ${ac_cv_lib_crypto_EVP_sha256+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lcrypto $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ #ifdef __cplusplus extern "C" #endif -char EVP_sha256 (); +char EVP_sha256 (void); int -main () +main (void) { return EVP_sha256 (); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : ac_cv_lib_crypto_EVP_sha256=yes -else - ac_cv_lib_crypto_EVP_sha256=no +else case e in #( + e) ac_cv_lib_crypto_EVP_sha256=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_crypto_EVP_sha256" >&5 -$as_echo "$ac_cv_lib_crypto_EVP_sha256" >&6; } -if test "x$ac_cv_lib_crypto_EVP_sha256" = xyes; then : +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_crypto_EVP_sha256" >&5 +printf "%s\n" "$ac_cv_lib_crypto_EVP_sha256" >&6; } +if test "x$ac_cv_lib_crypto_EVP_sha256" = xyes +then : - for ac_header in $base_ssl_inc/openssl/ssl.h $base_ssl_inc/openssl/err.h + for ac_header in $base_ssl_inc/openssl/ssl.h $base_ssl_inc/openssl/err.h do : - as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh` -ac_fn_c_check_header_mongrel "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default" -if eval test \"x\$"$as_ac_Header"\" = x"yes"; then : + as_ac_Header=`printf "%s\n" "ac_cv_header_$ac_header" | sed "$as_sed_sh"` +ac_fn_c_check_header_compile "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default" +if eval test \"x\$"$as_ac_Header"\" = x"yes" +then : cat >>confdefs.h <<_ACEOF -#define `$as_echo "HAVE_$ac_header" | $as_tr_cpp` 1 +#define `printf "%s\n" "HAVE_$ac_header" | sed "$as_sed_cpp"` 1 _ACEOF enable_ssl="yes"; @@ -7427,7 +8384,6 @@ fi done - fi @@ -7442,7 +8398,7 @@ fi if test "x$enable_ssl" = xyes; then -$as_echo "#define USE_SSL /**/" >>confdefs.h +printf "%s\n" "#define USE_SSL /**/" >>confdefs.h LIBS="$LIBS -Wl,-rpath=$unet_cv_with_openssl_prefix -L$unet_cv_with_openssl_prefix $OPENSSL_LDFLAGS" CFLAGS="$CFLAGS -I$unet_cv_with_openssl_inc_prefix" @@ -7451,69 +8407,75 @@ $as_echo "#define USE_SSL /**/" >>confdefs.h fi fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable GeoIP support" >&5 -$as_echo_n "checking whether to enable GeoIP support... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable GeoIP support" >&5 +printf %s "checking whether to enable GeoIP support... " >&6; } # Check whether --enable-geoip was given. -if test "${enable_geoip+set}" = set; then : +if test ${enable_geoip+y} +then : enableval=$enable_geoip; unet_cv_enable_geoip=$enable_geoip -else - if ${unet_cv_enable_geoip+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_enable_geoip=yes +else case e in #( + e) if test ${unet_cv_enable_geoip+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_enable_geoip=yes ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_geoip" >&5 -$as_echo "$unet_cv_enable_geoip" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_geoip" >&5 +printf "%s\n" "$unet_cv_enable_geoip" >&6; } if test x"$unet_cv_enable_geoip" = xyes; then # Check whether --with-geoip was given. -if test "${with_geoip+set}" = set; then : +if test ${with_geoip+y} +then : withval=$with_geoip; base_geoip_prefix=$withval -else - base_geoip_prefix=/usr/local +else case e in #( + e) base_geoip_prefix=/usr/local ;; +esac fi - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for GeoIP includes" >&5 -$as_echo_n "checking for GeoIP includes... " >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GeoIP includes" >&5 +printf %s "checking for GeoIP includes... " >&6; } # Check whether --with-geoip-includes was given. -if test "${with_geoip_includes+set}" = set; then : +if test ${with_geoip_includes+y} +then : withval=$with_geoip_includes; base_geoip_inc=$withval -else - base_geoip_inc=$base_geoip_prefix/include +else case e in #( + e) base_geoip_inc=$base_geoip_prefix/include ;; +esac fi unet_cv_with_geoip_inc_prefix=$base_geoip_inc - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_geoip_inc_prefix" >&5 -$as_echo "$unet_cv_with_geoip_inc_prefix" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_geoip_inc_prefix" >&5 +printf "%s\n" "$unet_cv_with_geoip_inc_prefix" >&6; } -cat >>confdefs.h <<_ACEOF -#define GEOIP_INCLUDES_PATH "$base_geoip_inc" -_ACEOF +printf "%s\n" "#define GEOIP_INCLUDES_PATH \"$base_geoip_inc\"" >>confdefs.h - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for GeoIP libraries" >&5 -$as_echo_n "checking for GeoIP libraries... " >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GeoIP libraries" >&5 +printf %s "checking for GeoIP libraries... " >&6; } # Check whether --with-geoip-libs was given. -if test "${with_geoip_libs+set}" = set; then : +if test ${with_geoip_libs+y} +then : withval=$with_geoip_libs; base_geoip_lib=$withval -else - base_geoip_lib=$base_geoip_prefix/lib +else case e in #( + e) base_geoip_lib=$base_geoip_prefix/lib ;; +esac fi unet_cv_with_geoip_prefix=$base_geoip_lib - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_geoip_prefix" >&5 -$as_echo "$unet_cv_with_geoip_prefix" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_geoip_prefix" >&5 +printf "%s\n" "$unet_cv_with_geoip_prefix" >&6; } -cat >>confdefs.h <<_ACEOF -#define GEOIP_LIBS_PATH "$unet_cv_with_geoip_prefix" -_ACEOF +printf "%s\n" "#define GEOIP_LIBS_PATH \"$unet_cv_with_geoip_prefix\"" >>confdefs.h save_CFLAGS=$CFLAGS @@ -7522,90 +8484,107 @@ _ACEOF CFLAGS="-I$unet_cv_with_geoip_inc_prefix" LIBS="-L$unet_cv_with_geoip_prefix" - for ac_header in $unet_cv_with_geoip_inc_prefix/GeoIP.h + for ac_header in $unet_cv_with_geoip_inc_prefix/GeoIP.h do : - as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh` -ac_fn_c_check_header_mongrel "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default" -if eval test \"x\$"$as_ac_Header"\" = x"yes"; then : + as_ac_Header=`printf "%s\n" "ac_cv_header_$ac_header" | sed "$as_sed_sh"` +ac_fn_c_check_header_compile "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default" +if eval test \"x\$"$as_ac_Header"\" = x"yes" +then : cat >>confdefs.h <<_ACEOF -#define `$as_echo "HAVE_$ac_header" | $as_tr_cpp` 1 +#define `printf "%s\n" "HAVE_$ac_header" | sed "$as_sed_cpp"` 1 _ACEOF - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for GeoIP_id_by_addr_gl in -lGeoIP" >&5 -$as_echo_n "checking for GeoIP_id_by_addr_gl in -lGeoIP... " >&6; } -if ${ac_cv_lib_GeoIP_GeoIP_id_by_addr_gl+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GeoIP_id_by_addr_gl in -lGeoIP" >&5 +printf %s "checking for GeoIP_id_by_addr_gl in -lGeoIP... " >&6; } +if test ${ac_cv_lib_GeoIP_GeoIP_id_by_addr_gl+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lGeoIP $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ #ifdef __cplusplus extern "C" #endif -char GeoIP_id_by_addr_gl (); +char GeoIP_id_by_addr_gl (void); int -main () +main (void) { return GeoIP_id_by_addr_gl (); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : ac_cv_lib_GeoIP_GeoIP_id_by_addr_gl=yes -else - ac_cv_lib_GeoIP_GeoIP_id_by_addr_gl=no +else case e in #( + e) ac_cv_lib_GeoIP_GeoIP_id_by_addr_gl=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_GeoIP_GeoIP_id_by_addr_gl" >&5 -$as_echo "$ac_cv_lib_GeoIP_GeoIP_id_by_addr_gl" >&6; } -if test "x$ac_cv_lib_GeoIP_GeoIP_id_by_addr_gl" = xyes; then : - - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for GeoIP_id_by_addr_v6_gl in -lGeoIP" >&5 -$as_echo_n "checking for GeoIP_id_by_addr_v6_gl in -lGeoIP... " >&6; } -if ${ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6_gl+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_GeoIP_GeoIP_id_by_addr_gl" >&5 +printf "%s\n" "$ac_cv_lib_GeoIP_GeoIP_id_by_addr_gl" >&6; } +if test "x$ac_cv_lib_GeoIP_GeoIP_id_by_addr_gl" = xyes +then : + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GeoIP_id_by_addr_v6_gl in -lGeoIP" >&5 +printf %s "checking for GeoIP_id_by_addr_v6_gl in -lGeoIP... " >&6; } +if test ${ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6_gl+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lGeoIP $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ #ifdef __cplusplus extern "C" #endif -char GeoIP_id_by_addr_v6_gl (); +char GeoIP_id_by_addr_v6_gl (void); int -main () +main (void) { return GeoIP_id_by_addr_v6_gl (); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6_gl=yes -else - ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6_gl=no +else case e in #( + e) ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6_gl=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6_gl" >&5 -$as_echo "$ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6_gl" >&6; } -if test "x$ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6_gl" = xyes; then : +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6_gl" >&5 +printf "%s\n" "$ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6_gl" >&6; } +if test "x$ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6_gl" = xyes +then : enable_geoip="yes"; enable_geoip_gl="yes"; @@ -7614,83 +8593,99 @@ if test "x$ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6_gl" = xyes; then : fi -else - - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for GeoIP_id_by_addr in -lGeoIP" >&5 -$as_echo_n "checking for GeoIP_id_by_addr in -lGeoIP... " >&6; } -if ${ac_cv_lib_GeoIP_GeoIP_id_by_addr+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS +else case e in #( + e) + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GeoIP_id_by_addr in -lGeoIP" >&5 +printf %s "checking for GeoIP_id_by_addr in -lGeoIP... " >&6; } +if test ${ac_cv_lib_GeoIP_GeoIP_id_by_addr+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lGeoIP $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ #ifdef __cplusplus extern "C" #endif -char GeoIP_id_by_addr (); +char GeoIP_id_by_addr (void); int -main () +main (void) { return GeoIP_id_by_addr (); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : ac_cv_lib_GeoIP_GeoIP_id_by_addr=yes -else - ac_cv_lib_GeoIP_GeoIP_id_by_addr=no +else case e in #( + e) ac_cv_lib_GeoIP_GeoIP_id_by_addr=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_GeoIP_GeoIP_id_by_addr" >&5 -$as_echo "$ac_cv_lib_GeoIP_GeoIP_id_by_addr" >&6; } -if test "x$ac_cv_lib_GeoIP_GeoIP_id_by_addr" = xyes; then : - - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for GeoIP_id_by_addr_v6 in -lGeoIP" >&5 -$as_echo_n "checking for GeoIP_id_by_addr_v6 in -lGeoIP... " >&6; } -if ${ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_GeoIP_GeoIP_id_by_addr" >&5 +printf "%s\n" "$ac_cv_lib_GeoIP_GeoIP_id_by_addr" >&6; } +if test "x$ac_cv_lib_GeoIP_GeoIP_id_by_addr" = xyes +then : + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GeoIP_id_by_addr_v6 in -lGeoIP" >&5 +printf %s "checking for GeoIP_id_by_addr_v6 in -lGeoIP... " >&6; } +if test ${ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lGeoIP $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ #ifdef __cplusplus extern "C" #endif -char GeoIP_id_by_addr_v6 (); +char GeoIP_id_by_addr_v6 (void); int -main () +main (void) { return GeoIP_id_by_addr_v6 (); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6=yes -else - ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6=no +else case e in #( + e) ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6" >&5 -$as_echo "$ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6" >&6; } -if test "x$ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6" = xyes; then : +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6" >&5 +printf "%s\n" "$ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6" >&6; } +if test "x$ac_cv_lib_GeoIP_GeoIP_id_by_addr_v6" = xyes +then : enable_geoip="yes"; GEOIP_LDFLAGS="-lGeoIP" @@ -7700,7 +8695,8 @@ fi fi - + ;; +esac fi @@ -7708,90 +8704,95 @@ fi done - LIBS=$save_LIBS CFLAGS=$save_CFLAGS if test "x$enable_geoip" = xyes; then -$as_echo "#define USE_GEOIP /**/" >>confdefs.h +printf "%s\n" "#define USE_GEOIP /**/" >>confdefs.h if test "x$enable_geoip_gl" = xyes; then -$as_echo "#define USE_GEOIP_GL /**/" >>confdefs.h +printf "%s\n" "#define USE_GEOIP_GL /**/" >>confdefs.h fi LIBS="$LIBS -L$unet_cv_with_geoip_prefix $GEOIP_LDFLAGS" CFLAGS="$CFLAGS -I$unet_cv_with_geoip_inc_prefix" else - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: Unable to find GeoIP, GeoIP features will not work without libGeoIP. Disabling GeoIP support." >&5 -$as_echo "$as_me: WARNING: Unable to find GeoIP, GeoIP features will not work without libGeoIP. Disabling GeoIP support." >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: Unable to find GeoIP, GeoIP features will not work without libGeoIP. Disabling GeoIP support." >&5 +printf "%s\n" "$as_me: WARNING: Unable to find GeoIP, GeoIP features will not work without libGeoIP. Disabling GeoIP support." >&2;} fi fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable MaxMindDB support" >&5 -$as_echo_n "checking whether to enable MaxMindDB support... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable MaxMindDB support" >&5 +printf %s "checking whether to enable MaxMindDB support... " >&6; } # Check whether --enable-mmdb was given. -if test "${enable_mmdb+set}" = set; then : +if test ${enable_mmdb+y} +then : enableval=$enable_mmdb; unet_cv_enable_mmdb=$enable_mmdb -else - if ${unet_cv_enable_mmdb+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_enable_mmdb=yes +else case e in #( + e) if test ${unet_cv_enable_mmdb+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_enable_mmdb=yes ;; +esac fi - + ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_mmdb" >&5 -$as_echo "$unet_cv_enable_mmdb" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_mmdb" >&5 +printf "%s\n" "$unet_cv_enable_mmdb" >&6; } if test x"$unet_cv_enable_mmdb" = xyes; then # Check whether --with-mmdb was given. -if test "${with_mmdb+set}" = set; then : +if test ${with_mmdb+y} +then : withval=$with_mmdb; base_mmdb_prefix=$withval -else - base_mmdb_prefix=/usr/local +else case e in #( + e) base_mmdb_prefix=/usr/local ;; +esac fi - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for MaxMindDB includes" >&5 -$as_echo_n "checking for MaxMindDB includes... " >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for MaxMindDB includes" >&5 +printf %s "checking for MaxMindDB includes... " >&6; } # Check whether --with-mmdb-includes was given. -if test "${with_mmdb_includes+set}" = set; then : +if test ${with_mmdb_includes+y} +then : withval=$with_mmdb_includes; base_mmdb_inc=$withval -else - base_mmdb_inc=$base_mmdb_prefix/include +else case e in #( + e) base_mmdb_inc=$base_mmdb_prefix/include ;; +esac fi unet_cv_with_mmdb_inc_prefix=$base_mmdb_inc - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_mmdb_inc_prefix" >&5 -$as_echo "$unet_cv_with_mmdb_inc_prefix" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_mmdb_inc_prefix" >&5 +printf "%s\n" "$unet_cv_with_mmdb_inc_prefix" >&6; } -cat >>confdefs.h <<_ACEOF -#define MMDB_INCLUDES_PATH "$base_mmdb_inc" -_ACEOF +printf "%s\n" "#define MMDB_INCLUDES_PATH \"$base_mmdb_inc\"" >>confdefs.h - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for MaxMindDB libraries" >&5 -$as_echo_n "checking for MaxMindDB libraries... " >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for MaxMindDB libraries" >&5 +printf %s "checking for MaxMindDB libraries... " >&6; } # Check whether --with-mmdb-libs was given. -if test "${with_mmdb_libs+set}" = set; then : +if test ${with_mmdb_libs+y} +then : withval=$with_mmdb_libs; base_mmdb_lib=$withval -else - base_mmdb_lib=$base_mmdb_prefix/lib +else case e in #( + e) base_mmdb_lib=$base_mmdb_prefix/lib ;; +esac fi unet_cv_with_mmdb_prefix=$base_mmdb_lib - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_mmdb_prefix" >&5 -$as_echo "$unet_cv_with_mmdb_prefix" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_mmdb_prefix" >&5 +printf "%s\n" "$unet_cv_with_mmdb_prefix" >&6; } -cat >>confdefs.h <<_ACEOF -#define MMDB_LIBS_PATH "$unet_cv_with_mmdb_prefix" -_ACEOF +printf "%s\n" "#define MMDB_LIBS_PATH \"$unet_cv_with_mmdb_prefix\"" >>confdefs.h save_CFLAGS=$CFLAGS @@ -7800,90 +8801,107 @@ _ACEOF CFLAGS="-I$unet_cv_with_mmdb_inc_prefix" LIBS="-L$unet_cv_with_mmdb_prefix" - for ac_header in $unet_cv_with_mmdb_inc_prefix/maxminddb.h + for ac_header in $unet_cv_with_mmdb_inc_prefix/maxminddb.h do : - as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh` -ac_fn_c_check_header_mongrel "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default" -if eval test \"x\$"$as_ac_Header"\" = x"yes"; then : + as_ac_Header=`printf "%s\n" "ac_cv_header_$ac_header" | sed "$as_sed_sh"` +ac_fn_c_check_header_compile "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default" +if eval test \"x\$"$as_ac_Header"\" = x"yes" +then : cat >>confdefs.h <<_ACEOF -#define `$as_echo "HAVE_$ac_header" | $as_tr_cpp` 1 +#define `printf "%s\n" "HAVE_$ac_header" | sed "$as_sed_cpp"` 1 _ACEOF - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for MMDB_lookup_string in -lmaxminddb" >&5 -$as_echo_n "checking for MMDB_lookup_string in -lmaxminddb... " >&6; } -if ${ac_cv_lib_maxminddb_MMDB_lookup_string+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for MMDB_lookup_string in -lmaxminddb" >&5 +printf %s "checking for MMDB_lookup_string in -lmaxminddb... " >&6; } +if test ${ac_cv_lib_maxminddb_MMDB_lookup_string+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lmaxminddb $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ #ifdef __cplusplus extern "C" #endif -char MMDB_lookup_string (); +char MMDB_lookup_string (void); int -main () +main (void) { return MMDB_lookup_string (); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : ac_cv_lib_maxminddb_MMDB_lookup_string=yes -else - ac_cv_lib_maxminddb_MMDB_lookup_string=no +else case e in #( + e) ac_cv_lib_maxminddb_MMDB_lookup_string=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_maxminddb_MMDB_lookup_string" >&5 -$as_echo "$ac_cv_lib_maxminddb_MMDB_lookup_string" >&6; } -if test "x$ac_cv_lib_maxminddb_MMDB_lookup_string" = xyes; then : - - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for MMDB_get_value in -lmaxminddb" >&5 -$as_echo_n "checking for MMDB_get_value in -lmaxminddb... " >&6; } -if ${ac_cv_lib_maxminddb_MMDB_get_value+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_maxminddb_MMDB_lookup_string" >&5 +printf "%s\n" "$ac_cv_lib_maxminddb_MMDB_lookup_string" >&6; } +if test "x$ac_cv_lib_maxminddb_MMDB_lookup_string" = xyes +then : + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for MMDB_get_value in -lmaxminddb" >&5 +printf %s "checking for MMDB_get_value in -lmaxminddb... " >&6; } +if test ${ac_cv_lib_maxminddb_MMDB_get_value+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS LIBS="-lmaxminddb $LIBS" cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ /* Override any GCC internal prototype to avoid an error. Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ #ifdef __cplusplus extern "C" #endif -char MMDB_get_value (); +char MMDB_get_value (void); int -main () +main (void) { return MMDB_get_value (); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : +if ac_fn_c_try_link "$LINENO" +then : ac_cv_lib_maxminddb_MMDB_get_value=yes -else - ac_cv_lib_maxminddb_MMDB_get_value=no +else case e in #( + e) ac_cv_lib_maxminddb_MMDB_get_value=no ;; +esac fi -rm -f core conftest.err conftest.$ac_objext \ +rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS +LIBS=$ac_check_lib_save_LIBS ;; +esac fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_maxminddb_MMDB_get_value" >&5 -$as_echo "$ac_cv_lib_maxminddb_MMDB_get_value" >&6; } -if test "x$ac_cv_lib_maxminddb_MMDB_get_value" = xyes; then : +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_maxminddb_MMDB_get_value" >&5 +printf "%s\n" "$ac_cv_lib_maxminddb_MMDB_get_value" >&6; } +if test "x$ac_cv_lib_maxminddb_MMDB_get_value" = xyes +then : enable_mmdb="yes"; MMDB_LDFLAGS="-lmaxminddb" @@ -7898,19 +8916,318 @@ fi done - LIBS=$save_LIBS CFLAGS=$save_CFLAGS if test "x$enable_mmdb" = xyes; then -$as_echo "#define USE_MMDB /**/" >>confdefs.h +printf "%s\n" "#define USE_MMDB /**/" >>confdefs.h LIBS="$LIBS -L$unet_cv_with_mmdb_prefix $MMDB_LDFLAGS" CFLAGS="$CFLAGS -I$unet_cv_with_mmdb_inc_prefix" else - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: Unable to find MaxMindDB, MaxMindDB features will not work without libmaxminddb. Disabling MaxMindDB support." >&5 -$as_echo "$as_me: WARNING: Unable to find MaxMindDB, MaxMindDB features will not work without libmaxminddb. Disabling MaxMindDB support." >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: Unable to find MaxMindDB, MaxMindDB features will not work without libmaxminddb. Disabling MaxMindDB support." >&5 +printf "%s\n" "$as_me: WARNING: Unable to find MaxMindDB, MaxMindDB features will not work without libmaxminddb. Disabling MaxMindDB support." >&2;} + fi +fi + +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable LMDB/chathistory support" >&5 +printf %s "checking whether to enable LMDB/chathistory support... " >&6; } +# Check whether --enable-lmdb was given. +if test ${enable_lmdb+y} +then : + enableval=$enable_lmdb; unet_cv_enable_lmdb=$enable_lmdb +else case e in #( + e) if test ${unet_cv_enable_lmdb+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_enable_lmdb=yes ;; +esac +fi + ;; +esac +fi + +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_lmdb" >&5 +printf "%s\n" "$unet_cv_enable_lmdb" >&6; } + +if test x"$unet_cv_enable_lmdb" = xyes; then + +# Check whether --with-lmdb was given. +if test ${with_lmdb+y} +then : + withval=$with_lmdb; base_lmdb_prefix=$withval +else case e in #( + e) base_lmdb_prefix=/usr ;; +esac +fi + + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for LMDB includes" >&5 +printf %s "checking for LMDB includes... " >&6; } + +# Check whether --with-lmdb-includes was given. +if test ${with_lmdb_includes+y} +then : + withval=$with_lmdb_includes; unet_cv_with_lmdb_inc_prefix=$withval +else case e in #( + e) unet_cv_with_lmdb_inc_prefix=$base_lmdb_prefix/include ;; +esac +fi + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_lmdb_inc_prefix" >&5 +printf "%s\n" "$unet_cv_with_lmdb_inc_prefix" >&6; } + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for LMDB libraries" >&5 +printf %s "checking for LMDB libraries... " >&6; } + +# Check whether --with-lmdb-libs was given. +if test ${with_lmdb_libs+y} +then : + withval=$with_lmdb_libs; unet_cv_with_lmdb_prefix=$withval +else case e in #( + e) unet_cv_with_lmdb_prefix=$base_lmdb_prefix/lib ;; +esac +fi + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_lmdb_prefix" >&5 +printf "%s\n" "$unet_cv_with_lmdb_prefix" >&6; } + + save_CFLAGS=$CFLAGS + save_LIBS=$LIBS + + CFLAGS="-I$unet_cv_with_lmdb_inc_prefix" + LIBS="-L$unet_cv_with_lmdb_prefix -llmdb" + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for mdb_env_create in -llmdb" >&5 +printf %s "checking for mdb_env_create in -llmdb... " >&6; } +if test ${ac_cv_lib_lmdb_mdb_env_create+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS +LIBS="-llmdb $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char mdb_env_create (void); +int +main (void) +{ +return mdb_env_create (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO" +then : + ac_cv_lib_lmdb_mdb_env_create=yes +else case e in #( + e) ac_cv_lib_lmdb_mdb_env_create=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_lmdb_mdb_env_create" >&5 +printf "%s\n" "$ac_cv_lib_lmdb_mdb_env_create" >&6; } +if test "x$ac_cv_lib_lmdb_mdb_env_create" = xyes +then : + + for ac_header in $unet_cv_with_lmdb_inc_prefix/lmdb.h +do : + as_ac_Header=`printf "%s\n" "ac_cv_header_$ac_header" | sed "$as_sed_sh"` +ac_fn_c_check_header_compile "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default" +if eval test \"x\$"$as_ac_Header"\" = x"yes" +then : + cat >>confdefs.h <<_ACEOF +#define `printf "%s\n" "HAVE_$ac_header" | sed "$as_sed_cpp"` 1 +_ACEOF + + enable_lmdb="yes"; + LMDB_LDFLAGS="-llmdb" + +fi + +done + +fi + + + LIBS=$save_LIBS + CFLAGS=$save_CFLAGS + + if test "x$enable_lmdb" = xyes; then + +printf "%s\n" "#define USE_LMDB /**/" >>confdefs.h + + LIBS="$LIBS -L$unet_cv_with_lmdb_prefix $LMDB_LDFLAGS" + CFLAGS="$CFLAGS -I$unet_cv_with_lmdb_inc_prefix" + else + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: Unable to find LMDB, chathistory features will not work without liblmdb. Disabling LMDB support." >&5 +printf "%s\n" "$as_me: WARNING: Unable to find LMDB, chathistory features will not work without liblmdb. Disabling LMDB support." >&2;} + fi +fi + +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable zstd compression support" >&5 +printf %s "checking whether to enable zstd compression support... " >&6; } +# Check whether --enable-zstd was given. +if test ${enable_zstd+y} +then : + enableval=$enable_zstd; unet_cv_enable_zstd=$enable_zstd +else case e in #( + e) if test ${unet_cv_enable_zstd+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_enable_zstd=yes ;; +esac +fi + ;; +esac +fi + +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_enable_zstd" >&5 +printf "%s\n" "$unet_cv_enable_zstd" >&6; } + +if test x"$unet_cv_enable_zstd" = xyes; then + +# Check whether --with-zstd was given. +if test ${with_zstd+y} +then : + withval=$with_zstd; base_zstd_prefix=$withval +else case e in #( + e) base_zstd_prefix=/usr ;; +esac +fi + + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for zstd includes" >&5 +printf %s "checking for zstd includes... " >&6; } + +# Check whether --with-zstd-includes was given. +if test ${with_zstd_includes+y} +then : + withval=$with_zstd_includes; unet_cv_with_zstd_inc_prefix=$withval +else case e in #( + e) unet_cv_with_zstd_inc_prefix=$base_zstd_prefix/include ;; +esac +fi + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_zstd_inc_prefix" >&5 +printf "%s\n" "$unet_cv_with_zstd_inc_prefix" >&6; } + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for zstd libraries" >&5 +printf %s "checking for zstd libraries... " >&6; } + +# Check whether --with-zstd-libs was given. +if test ${with_zstd_libs+y} +then : + withval=$with_zstd_libs; unet_cv_with_zstd_prefix=$withval +else case e in #( + e) unet_cv_with_zstd_prefix=$base_zstd_prefix/lib ;; +esac +fi + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_zstd_prefix" >&5 +printf "%s\n" "$unet_cv_with_zstd_prefix" >&6; } + + save_CFLAGS=$CFLAGS + save_LIBS=$LIBS + + CFLAGS="-I$unet_cv_with_zstd_inc_prefix" + LIBS="-L$unet_cv_with_zstd_prefix -lzstd" + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for ZSTD_compress in -lzstd" >&5 +printf %s "checking for ZSTD_compress in -lzstd... " >&6; } +if test ${ac_cv_lib_zstd_ZSTD_compress+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_check_lib_save_LIBS=$LIBS +LIBS="-lzstd $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. + The 'extern "C"' is for builds by C++ compilers; + although this is not generally supported in C code supporting it here + has little cost and some practical benefit (sr 110532). */ +#ifdef __cplusplus +extern "C" +#endif +char ZSTD_compress (void); +int +main (void) +{ +return ZSTD_compress (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO" +then : + ac_cv_lib_zstd_ZSTD_compress=yes +else case e in #( + e) ac_cv_lib_zstd_ZSTD_compress=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_zstd_ZSTD_compress" >&5 +printf "%s\n" "$ac_cv_lib_zstd_ZSTD_compress" >&6; } +if test "x$ac_cv_lib_zstd_ZSTD_compress" = xyes +then : + + for ac_header in $unet_cv_with_zstd_inc_prefix/zstd.h +do : + as_ac_Header=`printf "%s\n" "ac_cv_header_$ac_header" | sed "$as_sed_sh"` +ac_fn_c_check_header_compile "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default" +if eval test \"x\$"$as_ac_Header"\" = x"yes" +then : + cat >>confdefs.h <<_ACEOF +#define `printf "%s\n" "HAVE_$ac_header" | sed "$as_sed_cpp"` 1 +_ACEOF + + enable_zstd="yes"; + ZSTD_LDFLAGS="-lzstd" + +fi + +done + +fi + + + LIBS=$save_LIBS + CFLAGS=$save_CFLAGS + + if test "x$enable_zstd" = xyes; then + +printf "%s\n" "#define USE_ZSTD /**/" >>confdefs.h + + LIBS="$LIBS -L$unet_cv_with_zstd_prefix $ZSTD_LDFLAGS" + CFLAGS="$CFLAGS -I$unet_cv_with_zstd_inc_prefix" + else + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: Unable to find zstd, compression features will not be available. Disabling zstd support." >&5 +printf "%s\n" "$as_me: WARNING: Unable to find zstd, compression features will not be available. Disabling zstd support." >&2;} fi fi @@ -7919,19 +9236,23 @@ if test x"$unet_maxcon" = xunlimited; then unet_maxcon=1024 fi unet_maxcon=`expr $unet_maxcon - 4` -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking max connections" >&5 -$as_echo_n "checking max connections... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking max connections" >&5 +printf %s "checking max connections... " >&6; } # Check whether --with-maxcon was given. -if test "${with_maxcon+set}" = set; then : +if test ${with_maxcon+y} +then : withval=$with_maxcon; unet_cv_with_maxcon=$with_maxcon -else - if ${unet_cv_with_maxcon+:} false; then : - $as_echo_n "(cached) " >&6 -else - unet_cv_with_maxcon=$unet_maxcon +else case e in #( + e) if test ${unet_cv_with_maxcon+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) unet_cv_with_maxcon=$unet_maxcon ;; +esac fi - + ;; +esac fi @@ -7944,13 +9265,11 @@ elif test "$unet_cv_with_maxcon" -lt 32; then as_fn_error $? "Maximum connections (--with-maxcon) must be at least 32." "$LINENO" 5 fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_maxcon" >&5 -$as_echo "$unet_cv_with_maxcon" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $unet_cv_with_maxcon" >&5 +printf "%s\n" "$unet_cv_with_maxcon" >&6; } -cat >>confdefs.h <<_ACEOF -#define MAXCONNECTIONS $unet_cv_with_maxcon -_ACEOF +printf "%s\n" "#define MAXCONNECTIONS $unet_cv_with_maxcon" >>confdefs.h ac_config_files="$ac_config_files Makefile ircd/Makefile ircd/test/Makefile" @@ -7967,8 +9286,8 @@ cat >confcache <<\_ACEOF # config.status only pays attention to the cache file if you give it # the --recheck option to rerun configure. # -# `ac_cv_env_foo' variables (set or unset) will be overridden when -# loading this file, other *unset* `ac_cv_foo' will be assigned the +# 'ac_cv_env_foo' variables (set or unset) will be overridden when +# loading this file, other *unset* 'ac_cv_foo' will be assigned the # following values. _ACEOF @@ -7984,8 +9303,8 @@ _ACEOF case $ac_val in #( *${as_nl}*) case $ac_var in #( - *_cv_*) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 -$as_echo "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; + *_cv_*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 +printf "%s\n" "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; esac case $ac_var in #( _ | IFS | as_nl) ;; #( @@ -7998,14 +9317,14 @@ $as_echo "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; (set) 2>&1 | case $as_nl`(ac_space=' '; set) 2>&1` in #( *${as_nl}ac_space=\ *) - # `set' does not quote correctly, so add quotes: double-quote + # 'set' does not quote correctly, so add quotes: double-quote # substitution turns \\\\ into \\, and sed turns \\ into \. sed -n \ "s/'/'\\\\''/g; s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\\2'/p" ;; #( *) - # `set' quotes correctly as required by POSIX, so do not add quotes. + # 'set' quotes correctly as required by POSIX, so do not add quotes. sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p" ;; esac | @@ -8015,15 +9334,15 @@ $as_echo "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; /^ac_cv_env_/b end t clear :clear - s/^\([^=]*\)=\(.*[{}].*\)$/test "${\1+set}" = set || &/ + s/^\([^=]*\)=\(.*[{}].*\)$/test ${\1+y} || &/ t end s/^\([^=]*\)=\(.*\)$/\1=${\1=\2}/ :end' >>confcache if diff "$cache_file" confcache >/dev/null 2>&1; then :; else if test -w "$cache_file"; then if test "x$cache_file" != "x/dev/null"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: updating cache $cache_file" >&5 -$as_echo "$as_me: updating cache $cache_file" >&6;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: updating cache $cache_file" >&5 +printf "%s\n" "$as_me: updating cache $cache_file" >&6;} if test ! -f "$cache_file" || test -h "$cache_file"; then cat confcache >"$cache_file" else @@ -8037,8 +9356,8 @@ $as_echo "$as_me: updating cache $cache_file" >&6;} fi fi else - { $as_echo "$as_me:${as_lineno-$LINENO}: not updating unwritable cache $cache_file" >&5 -$as_echo "$as_me: not updating unwritable cache $cache_file" >&6;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: not updating unwritable cache $cache_file" >&5 +printf "%s\n" "$as_me: not updating unwritable cache $cache_file" >&6;} fi fi rm -f confcache @@ -8055,7 +9374,7 @@ U= for ac_i in : $LIBOBJS; do test "x$ac_i" = x: && continue # 1. Remove the extension, and $U if already installed. ac_script='s/\$U\././;s/\.o$//;s/\.obj$//' - ac_i=`$as_echo "$ac_i" | sed "$ac_script"` + ac_i=`printf "%s\n" "$ac_i" | sed "$ac_script"` # 2. Prepend LIBOBJDIR. When used with automake>=1.10 LIBOBJDIR # will be set to the directory where LIBOBJS objects are built. as_fn_append ac_libobjs " \${LIBOBJDIR}$ac_i\$U.$ac_objext" @@ -8072,8 +9391,8 @@ LTLIBOBJS=$ac_ltlibobjs ac_write_fail=0 ac_clean_files_save=$ac_clean_files ac_clean_files="$ac_clean_files $CONFIG_STATUS" -{ $as_echo "$as_me:${as_lineno-$LINENO}: creating $CONFIG_STATUS" >&5 -$as_echo "$as_me: creating $CONFIG_STATUS" >&6;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating $CONFIG_STATUS" >&5 +printf "%s\n" "$as_me: creating $CONFIG_STATUS" >&6;} as_write_fail=0 cat >$CONFIG_STATUS <<_ASEOF || as_write_fail=1 #! $SHELL @@ -8096,63 +9415,65 @@ cat >>$CONFIG_STATUS <<\_ASEOF || as_write_fail=1 # Be more Bourne compatible DUALCASE=1; export DUALCASE # for MKS sh -if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then : +if test ${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1 +then : emulate sh NULLCMD=: # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which # is contrary to our usage. Disable this feature. alias -g '${1+"$@"}'='"$@"' setopt NO_GLOB_SUBST -else - case `(set -o) 2>/dev/null` in #( +else case e in #( + e) case `(set -o) 2>/dev/null` in #( *posix*) : set -o posix ;; #( *) : ;; +esac ;; esac fi + +# Reset variables that may have inherited troublesome values from +# the environment. + +# IFS needs to be set, to space, tab, and newline, in precisely that order. +# (If _AS_PATH_WALK were called with IFS unset, it would have the +# side effect of setting IFS to empty, thus disabling word splitting.) +# Quoting is to prevent editors from complaining about space-tab. as_nl=' ' export as_nl -# Printing a long string crashes Solaris 7 /usr/bin/printf. -as_echo='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\' -as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo -as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo$as_echo -# Prefer a ksh shell builtin over an external printf program on Solaris, -# but without wasting forks for bash or zsh. -if test -z "$BASH_VERSION$ZSH_VERSION" \ - && (test "X`print -r -- $as_echo`" = "X$as_echo") 2>/dev/null; then - as_echo='print -r --' - as_echo_n='print -rn --' -elif (test "X`printf %s $as_echo`" = "X$as_echo") 2>/dev/null; then - as_echo='printf %s\n' - as_echo_n='printf %s' -else - if test "X`(/usr/ucb/echo -n -n $as_echo) 2>/dev/null`" = "X-n $as_echo"; then - as_echo_body='eval /usr/ucb/echo -n "$1$as_nl"' - as_echo_n='/usr/ucb/echo -n' - else - as_echo_body='eval expr "X$1" : "X\\(.*\\)"' - as_echo_n_body='eval - arg=$1; - case $arg in #( - *"$as_nl"*) - expr "X$arg" : "X\\(.*\\)$as_nl"; - arg=`expr "X$arg" : ".*$as_nl\\(.*\\)"`;; - esac; - expr "X$arg" : "X\\(.*\\)" | tr -d "$as_nl" - ' - export as_echo_n_body - as_echo_n='sh -c $as_echo_n_body as_echo' - fi - export as_echo_body - as_echo='sh -c $as_echo_body as_echo' -fi +IFS=" "" $as_nl" + +PS1='$ ' +PS2='> ' +PS4='+ ' + +# Ensure predictable behavior from utilities with locale-dependent output. +LC_ALL=C +export LC_ALL +LANGUAGE=C +export LANGUAGE + +# We cannot yet rely on "unset" to work, but we need these variables +# to be unset--not just set to an empty or harmless value--now, to +# avoid bugs in old shells (e.g. pre-3.0 UWIN ksh). This construct +# also avoids known problems related to "unset" and subshell syntax +# in other old shells (e.g. bash 2.01 and pdksh 5.2.14). +for as_var in BASH_ENV ENV MAIL MAILPATH CDPATH +do eval test \${$as_var+y} \ + && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || : +done + +# Ensure that fds 0, 1, and 2 are open. +if (exec 3>&0) 2>/dev/null; then :; else exec 0&1) 2>/dev/null; then :; else exec 1>/dev/null; fi +if (exec 3>&2) ; then :; else exec 2>/dev/null; fi # The user is always right. -if test "${PATH_SEPARATOR+set}" != set; then +if ${PATH_SEPARATOR+false} :; then PATH_SEPARATOR=: (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && { (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 || @@ -8161,13 +9482,6 @@ if test "${PATH_SEPARATOR+set}" != set; then fi -# IFS -# We need space, tab and new line, in precisely that order. Quoting is -# there to prevent editors from complaining about space-tab. -# (If _AS_PATH_WALK were called with IFS unset, it would disable word -# splitting by setting IFS to empty value.) -IFS=" "" $as_nl" - # Find who we are. Look in the path if we contain no directory separator. as_myself= case $0 in #(( @@ -8176,43 +9490,27 @@ case $0 in #(( for as_dir in $PATH do IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. - test -r "$as_dir/$0" && as_myself=$as_dir/$0 && break + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + test -r "$as_dir$0" && as_myself=$as_dir$0 && break done IFS=$as_save_IFS ;; esac -# We did not find ourselves, most probably we were run as `sh COMMAND' +# We did not find ourselves, most probably we were run as 'sh COMMAND' # in which case we are not to be found in the path. if test "x$as_myself" = x; then as_myself=$0 fi if test ! -f "$as_myself"; then - $as_echo "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 + printf "%s\n" "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 exit 1 fi -# Unset variables that we do not need and which cause bugs (e.g. in -# pre-3.0 UWIN ksh). But do not cause bugs in bash 2.01; the "|| exit 1" -# suppresses any "Segmentation fault" message there. '((' could -# trigger a bug in pdksh 5.2.14. -for as_var in BASH_ENV ENV MAIL MAILPATH -do eval test x\${$as_var+set} = xset \ - && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || : -done -PS1='$ ' -PS2='> ' -PS4='+ ' - -# NLS nuisances. -LC_ALL=C -export LC_ALL -LANGUAGE=C -export LANGUAGE - -# CDPATH. -(unset CDPATH) >/dev/null 2>&1 && unset CDPATH # as_fn_error STATUS ERROR [LINENO LOG_FD] @@ -8225,9 +9523,9 @@ as_fn_error () as_status=$1; test $as_status -eq 0 && as_status=1 if test "$4"; then as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - $as_echo "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 fi - $as_echo "$as_me: error: $2" >&2 + printf "%s\n" "$as_me: error: $2" >&2 as_fn_exit $as_status } # as_fn_error @@ -8258,22 +9556,25 @@ as_fn_unset () { eval $1=; unset $1;} } as_unset=as_fn_unset + # as_fn_append VAR VALUE # ---------------------- # Append the text in VALUE to the end of the definition contained in VAR. Take # advantage of any shell optimizations that allow amortized linear growth over # repeated appends, instead of the typical quadratic growth present in naive # implementations. -if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null; then : +if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null +then : eval 'as_fn_append () { eval $1+=\$2 }' -else - as_fn_append () +else case e in #( + e) as_fn_append () { eval $1=\$$1\$2 - } + } ;; +esac fi # as_fn_append # as_fn_arith ARG... @@ -8281,16 +9582,18 @@ fi # as_fn_append # Perform arithmetic evaluation on the ARGs, and store the result in the # global $as_val. Take advantage of shells that can avoid forks. The arguments # must be portable across $(()) and expr. -if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null; then : +if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null +then : eval 'as_fn_arith () { as_val=$(( $* )) }' -else - as_fn_arith () +else case e in #( + e) as_fn_arith () { as_val=`expr "$@" || test $? -eq 1` - } + } ;; +esac fi # as_fn_arith @@ -8317,7 +9620,7 @@ as_me=`$as_basename -- "$0" || $as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \ X"$0" : 'X\(//\)$' \| \ X"$0" : 'X\(/\)' \| . 2>/dev/null || -$as_echo X/"$0" | +printf "%s\n" X/"$0" | sed '/^.*\/\([^/][^/]*\)\/*$/{ s//\1/ q @@ -8339,6 +9642,10 @@ as_cr_Letters=$as_cr_letters$as_cr_LETTERS as_cr_digits='0123456789' as_cr_alnum=$as_cr_Letters$as_cr_digits + +# Determine whether it's possible to make 'echo' print without a newline. +# These variables are no longer used directly by Autoconf, but are AC_SUBSTed +# for compatibility with existing Makefiles. ECHO_C= ECHO_N= ECHO_T= case `echo -n x` in #((((( -n*) @@ -8352,6 +9659,12 @@ case `echo -n x` in #((((( ECHO_N='-n';; esac +# For backward compatibility with old third-party macros, we provide +# the shell variables $as_echo and $as_echo_n. New code should use +# AS_ECHO(["message"]) and AS_ECHO_N(["message"]), respectively. +as_echo='printf %s\n' +as_echo_n='printf %s' + rm -f conf$$ conf$$.exe conf$$.file if test -d conf$$.dir; then rm -f conf$$.dir/conf$$.file @@ -8363,9 +9676,9 @@ if (echo >conf$$.file) 2>/dev/null; then if ln -s conf$$.file conf$$ 2>/dev/null; then as_ln_s='ln -s' # ... but there are two gotchas: - # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail. - # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable. - # In both cases, we have to default to `cp -pR'. + # 1) On MSYS, both 'ln -s file dir' and 'ln file dir' fail. + # 2) DJGPP < 2.04 has no symlinks; 'ln -s' creates a wrapper executable. + # In both cases, we have to default to 'cp -pR'. ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe || as_ln_s='cp -pR' elif ln conf$$.file conf$$ 2>/dev/null; then @@ -8393,7 +9706,7 @@ as_fn_mkdir_p () as_dirs= while :; do case $as_dir in #( - *\'*) as_qdir=`$as_echo "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( + *\'*) as_qdir=`printf "%s\n" "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( *) as_qdir=$as_dir;; esac as_dirs="'$as_qdir' $as_dirs" @@ -8402,7 +9715,7 @@ $as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$as_dir" : 'X\(//\)[^/]' \| \ X"$as_dir" : 'X\(//\)$' \| \ X"$as_dir" : 'X\(/\)' \| . 2>/dev/null || -$as_echo X"$as_dir" | +printf "%s\n" X"$as_dir" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q @@ -8446,10 +9759,12 @@ as_test_x='test -x' as_executable_p=as_fn_executable_p # Sed expression to map a string onto a valid CPP name. -as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'" +as_sed_cpp="y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g" +as_tr_cpp="eval sed '$as_sed_cpp'" # deprecated # Sed expression to map a string onto a valid variable name. -as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'" +as_sed_sh="y%*+%pp%;s%[^_$as_cr_alnum]%_%g" +as_tr_sh="eval sed '$as_sed_sh'" # deprecated exec 6>&1 @@ -8465,7 +9780,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # values after options handling. ac_log=" This file was extended by $as_me, which was -generated by GNU Autoconf 2.69. Invocation command line was +generated by GNU Autoconf 2.72. Invocation command line was CONFIG_FILES = $CONFIG_FILES CONFIG_HEADERS = $CONFIG_HEADERS @@ -8497,7 +9812,7 @@ _ACEOF cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 ac_cs_usage="\ -\`$as_me' instantiates files and other configuration actions +'$as_me' instantiates files and other configuration actions from templates according to the current configuration. Unless the files and actions are specified as TAGs, all are instantiated by default. @@ -8527,14 +9842,16 @@ $config_commands Report bugs to the package provider." _ACEOF +ac_cs_config=`printf "%s\n" "$ac_configure_args" | sed "$ac_safe_unquote"` +ac_cs_config_escaped=`printf "%s\n" "$ac_cs_config" | sed "s/^ //; s/'/'\\\\\\\\''/g"` cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 -ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`" +ac_cs_config='$ac_cs_config_escaped' ac_cs_version="\\ config.status -configured by $0, generated by GNU Autoconf 2.69, +configured by $0, generated by GNU Autoconf 2.72, with options \\"\$ac_cs_config\\" -Copyright (C) 2012 Free Software Foundation, Inc. +Copyright (C) 2023 Free Software Foundation, Inc. This config.status script is free software; the Free Software Foundation gives unlimited permission to copy, distribute and modify it." @@ -8573,15 +9890,15 @@ do -recheck | --recheck | --rechec | --reche | --rech | --rec | --re | --r) ac_cs_recheck=: ;; --version | --versio | --versi | --vers | --ver | --ve | --v | -V ) - $as_echo "$ac_cs_version"; exit ;; + printf "%s\n" "$ac_cs_version"; exit ;; --config | --confi | --conf | --con | --co | --c ) - $as_echo "$ac_cs_config"; exit ;; + printf "%s\n" "$ac_cs_config"; exit ;; --debug | --debu | --deb | --de | --d | -d ) debug=: ;; --file | --fil | --fi | --f ) $ac_shift case $ac_optarg in - *\'*) ac_optarg=`$as_echo "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; + *\'*) ac_optarg=`printf "%s\n" "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; '') as_fn_error $? "missing file argument" ;; esac as_fn_append CONFIG_FILES " '$ac_optarg'" @@ -8589,23 +9906,23 @@ do --header | --heade | --head | --hea ) $ac_shift case $ac_optarg in - *\'*) ac_optarg=`$as_echo "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; + *\'*) ac_optarg=`printf "%s\n" "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; esac as_fn_append CONFIG_HEADERS " '$ac_optarg'" ac_need_defaults=false;; --he | --h) # Conflict between --help and --header - as_fn_error $? "ambiguous option: \`$1' -Try \`$0 --help' for more information.";; + as_fn_error $? "ambiguous option: '$1' +Try '$0 --help' for more information.";; --help | --hel | -h ) - $as_echo "$ac_cs_usage"; exit ;; + printf "%s\n" "$ac_cs_usage"; exit ;; -q | -quiet | --quiet | --quie | --qui | --qu | --q \ | -silent | --silent | --silen | --sile | --sil | --si | --s) ac_cs_silent=: ;; # This is an error. - -*) as_fn_error $? "unrecognized option: \`$1' -Try \`$0 --help' for more information." ;; + -*) as_fn_error $? "unrecognized option: '$1' +Try '$0 --help' for more information." ;; *) as_fn_append ac_config_targets " $1" ac_need_defaults=false ;; @@ -8626,7 +9943,7 @@ cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 if \$ac_cs_recheck; then set X $SHELL '$0' $ac_configure_args \$ac_configure_extra_args --no-create --no-recursion shift - \$as_echo "running CONFIG_SHELL=$SHELL \$*" >&6 + \printf "%s\n" "running CONFIG_SHELL=$SHELL \$*" >&6 CONFIG_SHELL='$SHELL' export CONFIG_SHELL exec "\$@" @@ -8640,7 +9957,7 @@ exec 5>>config.log sed 'h;s/./-/g;s/^.../## /;s/...$/ ##/;p;x;p;x' <<_ASBOX ## Running $as_me. ## _ASBOX - $as_echo "$ac_log" + printf "%s\n" "$ac_log" } >&5 _ACEOF @@ -8659,7 +9976,7 @@ do "ircd/test/Makefile") CONFIG_FILES="$CONFIG_FILES ircd/test/Makefile" ;; "default") CONFIG_COMMANDS="$CONFIG_COMMANDS default" ;; - *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5;; + *) as_fn_error $? "invalid argument: '$ac_config_target'" "$LINENO" 5;; esac done @@ -8669,9 +9986,9 @@ done # We use the long form for the default assignment because of an extremely # bizarre bug on SunOS 4.1.3. if $ac_need_defaults; then - test "${CONFIG_FILES+set}" = set || CONFIG_FILES=$config_files - test "${CONFIG_HEADERS+set}" = set || CONFIG_HEADERS=$config_headers - test "${CONFIG_COMMANDS+set}" = set || CONFIG_COMMANDS=$config_commands + test ${CONFIG_FILES+y} || CONFIG_FILES=$config_files + test ${CONFIG_HEADERS+y} || CONFIG_HEADERS=$config_headers + test ${CONFIG_COMMANDS+y} || CONFIG_COMMANDS=$config_commands fi # Have a temporary directory for convenience. Make it in the build tree @@ -8679,7 +9996,7 @@ fi # creating and moving files from /tmp can sometimes cause problems. # Hook for its removal unless debugging. # Note that there is a small window in which the directory will not be cleaned: -# after its creation but before its name has been assigned to `$tmp'. +# after its creation but before its name has been assigned to '$tmp'. $debug || { tmp= ac_tmp= @@ -8703,7 +10020,7 @@ ac_tmp=$tmp # Set up the scripts for CONFIG_FILES section. # No need to generate them if there are no CONFIG_FILES. -# This happens for instance with `./config.status config.h'. +# This happens for instance with './config.status config.h'. if test -n "$CONFIG_FILES"; then @@ -8861,13 +10178,13 @@ fi # test -n "$CONFIG_FILES" # Set up the scripts for CONFIG_HEADERS section. # No need to generate them if there are no CONFIG_HEADERS. -# This happens for instance with `./config.status Makefile'. +# This happens for instance with './config.status Makefile'. if test -n "$CONFIG_HEADERS"; then cat >"$ac_tmp/defines.awk" <<\_ACAWK || BEGIN { _ACEOF -# Transform confdefs.h into an awk script `defines.awk', embedded as +# Transform confdefs.h into an awk script 'defines.awk', embedded as # here-document in config.status, that substitutes the proper values into # config.h.in to produce config.h. @@ -8977,7 +10294,7 @@ do esac case $ac_mode$ac_tag in :[FHL]*:*);; - :L* | :C*:*) as_fn_error $? "invalid tag \`$ac_tag'" "$LINENO" 5;; + :L* | :C*:*) as_fn_error $? "invalid tag '$ac_tag'" "$LINENO" 5;; :[FH]-) ac_tag=-:-;; :[FH]*) ac_tag=$ac_tag:$ac_tag.in;; esac @@ -8999,33 +10316,33 @@ do -) ac_f="$ac_tmp/stdin";; *) # Look for the file first in the build tree, then in the source tree # (if the path is not absolute). The absolute path cannot be DOS-style, - # because $ac_f cannot contain `:'. + # because $ac_f cannot contain ':'. test -f "$ac_f" || case $ac_f in [\\/$]*) false;; *) test -f "$srcdir/$ac_f" && ac_f="$srcdir/$ac_f";; esac || - as_fn_error 1 "cannot find input file: \`$ac_f'" "$LINENO" 5;; + as_fn_error 1 "cannot find input file: '$ac_f'" "$LINENO" 5;; esac - case $ac_f in *\'*) ac_f=`$as_echo "$ac_f" | sed "s/'/'\\\\\\\\''/g"`;; esac + case $ac_f in *\'*) ac_f=`printf "%s\n" "$ac_f" | sed "s/'/'\\\\\\\\''/g"`;; esac as_fn_append ac_file_inputs " '$ac_f'" done - # Let's still pretend it is `configure' which instantiates (i.e., don't + # Let's still pretend it is 'configure' which instantiates (i.e., don't # use $as_me), people would be surprised to read: # /* config.h. Generated by config.status. */ configure_input='Generated from '` - $as_echo "$*" | sed 's|^[^:]*/||;s|:[^:]*/|, |g' + printf "%s\n" "$*" | sed 's|^[^:]*/||;s|:[^:]*/|, |g' `' by configure.' if test x"$ac_file" != x-; then configure_input="$ac_file. $configure_input" - { $as_echo "$as_me:${as_lineno-$LINENO}: creating $ac_file" >&5 -$as_echo "$as_me: creating $ac_file" >&6;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating $ac_file" >&5 +printf "%s\n" "$as_me: creating $ac_file" >&6;} fi # Neutralize special characters interpreted by sed in replacement strings. case $configure_input in #( *\&* | *\|* | *\\* ) - ac_sed_conf_input=`$as_echo "$configure_input" | + ac_sed_conf_input=`printf "%s\n" "$configure_input" | sed 's/[\\\\&|]/\\\\&/g'`;; #( *) ac_sed_conf_input=$configure_input;; esac @@ -9042,7 +10359,7 @@ $as_expr X"$ac_file" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$ac_file" : 'X\(//\)[^/]' \| \ X"$ac_file" : 'X\(//\)$' \| \ X"$ac_file" : 'X\(/\)' \| . 2>/dev/null || -$as_echo X"$ac_file" | +printf "%s\n" X"$ac_file" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q @@ -9066,9 +10383,9 @@ $as_echo X"$ac_file" | case "$ac_dir" in .) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; *) - ac_dir_suffix=/`$as_echo "$ac_dir" | sed 's|^\.[\\/]||'` + ac_dir_suffix=/`printf "%s\n" "$ac_dir" | sed 's|^\.[\\/]||'` # A ".." for each directory in $ac_dir_suffix. - ac_top_builddir_sub=`$as_echo "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` + ac_top_builddir_sub=`printf "%s\n" "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` case $ac_top_builddir_sub in "") ac_top_builddir_sub=. ac_top_build_prefix= ;; *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; @@ -9125,8 +10442,8 @@ ac_sed_dataroot=' case `eval "sed -n \"\$ac_sed_dataroot\" $ac_file_inputs"` in *datarootdir*) ac_datarootdir_seen=yes;; *@datadir@*|*@docdir@*|*@infodir@*|*@localedir@*|*@mandir@*) - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&5 -$as_echo "$as_me: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&5 +printf "%s\n" "$as_me: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&2;} _ACEOF cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_datarootdir_hack=' @@ -9139,7 +10456,7 @@ cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 esac _ACEOF -# Neutralize VPATH when `$srcdir' = `.'. +# Neutralize VPATH when '$srcdir' = '.'. # Shell code in configure.ac might set extrasub. # FIXME: do we really want to maintain this feature? cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 @@ -9169,9 +10486,9 @@ test -z "$ac_datarootdir_hack$ac_datarootdir_seen" && { ac_out=`sed -n '/\${datarootdir}/p' "$ac_tmp/out"`; test -n "$ac_out"; } && { ac_out=`sed -n '/^[ ]*datarootdir[ ]*:*=/p' \ "$ac_tmp/out"`; test -z "$ac_out"; } && - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable \`datarootdir' + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable 'datarootdir' which seems to be undefined. Please make sure it is defined" >&5 -$as_echo "$as_me: WARNING: $ac_file contains a reference to the variable \`datarootdir' +printf "%s\n" "$as_me: WARNING: $ac_file contains a reference to the variable 'datarootdir' which seems to be undefined. Please make sure it is defined" >&2;} rm -f "$ac_tmp/stdin" @@ -9187,27 +10504,27 @@ which seems to be undefined. Please make sure it is defined" >&2;} # if test x"$ac_file" != x-; then { - $as_echo "/* $configure_input */" \ + printf "%s\n" "/* $configure_input */" >&1 \ && eval '$AWK -f "$ac_tmp/defines.awk"' "$ac_file_inputs" } >"$ac_tmp/config.h" \ || as_fn_error $? "could not create $ac_file" "$LINENO" 5 if diff "$ac_file" "$ac_tmp/config.h" >/dev/null 2>&1; then - { $as_echo "$as_me:${as_lineno-$LINENO}: $ac_file is unchanged" >&5 -$as_echo "$as_me: $ac_file is unchanged" >&6;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: $ac_file is unchanged" >&5 +printf "%s\n" "$as_me: $ac_file is unchanged" >&6;} else rm -f "$ac_file" mv "$ac_tmp/config.h" "$ac_file" \ || as_fn_error $? "could not create $ac_file" "$LINENO" 5 fi else - $as_echo "/* $configure_input */" \ + printf "%s\n" "/* $configure_input */" >&1 \ && eval '$AWK -f "$ac_tmp/defines.awk"' "$ac_file_inputs" \ || as_fn_error $? "could not create -" "$LINENO" 5 fi ;; - :C) { $as_echo "$as_me:${as_lineno-$LINENO}: executing $ac_file commands" >&5 -$as_echo "$as_me: executing $ac_file commands" >&6;} + :C) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: executing $ac_file commands" >&5 +printf "%s\n" "$as_me: executing $ac_file commands" >&6;} ;; esac @@ -9248,10 +10565,11 @@ if test "$no_create" != yes; then $ac_cs_success || as_fn_exit 1 fi if test -n "$ac_unrecognized_opts" && test "$enable_option_checking" != no; then - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: unrecognized options: $ac_unrecognized_opts" >&5 -$as_echo "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: unrecognized options: $ac_unrecognized_opts" >&5 +printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;} fi ac_config_commands="$ac_config_commands default-1" + diff --git a/configure.in b/configure.in index 22a51dbd..054ac770 100644 --- a/configure.in +++ b/configure.in @@ -64,6 +64,14 @@ AC_SEARCH_LIBS(crypt, descrypt crypt, , dnl Do all the checks necessary to figure out -lnsl / -lsocket stuff AC_LIBRARY_NET +dnl Check for pthreads (required for thread pool) +AC_CHECK_HEADERS([pthread.h]) +AC_SEARCH_LIBS(pthread_create, pthread, [ + AC_DEFINE([HAVE_PTHREAD], [1], [Define if pthreads is available]) +], [ + AC_MSG_WARN([pthreads not found; async password verification disabled]) +]) + dnl Checks for header files. AC_HEADER_STDC AC_CHECK_HEADERS(crypt.h poll.h inttypes.h stdint.h sys/devpoll.h sys/epoll.h sys/event.h sys/param.h sys/resource.h sys/socket.h) @@ -893,6 +901,118 @@ if test x"$unet_cv_enable_mmdb" = xyes; then fi fi +dnl ** +dnl ** LMDB checks (for chathistory) +dnl ** +AC_MSG_CHECKING([whether to enable LMDB/chathistory support]) +AC_ARG_ENABLE([lmdb], +[ --disable-lmdb Disable LMDB/chathistory support], +[unet_cv_enable_lmdb=$enable_lmdb], +[AC_CACHE_VAL(unet_cv_enable_lmdb, +[unet_cv_enable_lmdb=yes])]) +AC_MSG_RESULT([$unet_cv_enable_lmdb]) + +if test x"$unet_cv_enable_lmdb" = xyes; then + AC_ARG_WITH([lmdb], + AS_HELP_STRING([--with-lmdb=dir], [Specify the installation prefix of LMDB (default: /usr)]), + [base_lmdb_prefix=$withval], + [base_lmdb_prefix=/usr]) + + AC_MSG_CHECKING([for LMDB includes]) + AC_ARG_WITH([lmdb-includes], + AS_HELP_STRING([--with-lmdb-includes=dir], [Specify location of LMDB header files (default: /usr/include)]), + [unet_cv_with_lmdb_inc_prefix=$withval], + [unet_cv_with_lmdb_inc_prefix=$base_lmdb_prefix/include]) + AC_MSG_RESULT([$unet_cv_with_lmdb_inc_prefix]) + + AC_MSG_CHECKING([for LMDB libraries]) + AC_ARG_WITH([lmdb-libs], + AS_HELP_STRING([--with-lmdb-libs=dir], [Specify location of LMDB libs (default: /usr/lib)]), + [unet_cv_with_lmdb_prefix=$withval], + [unet_cv_with_lmdb_prefix=$base_lmdb_prefix/lib]) + AC_MSG_RESULT([$unet_cv_with_lmdb_prefix]) + + save_CFLAGS=$CFLAGS + save_LIBS=$LIBS + + CFLAGS="-I$unet_cv_with_lmdb_inc_prefix" + LIBS="-L$unet_cv_with_lmdb_prefix -llmdb" + + AC_CHECK_LIB(lmdb, mdb_env_create, [ + AC_CHECK_HEADERS($unet_cv_with_lmdb_inc_prefix/lmdb.h, [ + enable_lmdb="yes"; + LMDB_LDFLAGS="-llmdb" + ]) + ]) + + LIBS=$save_LIBS + CFLAGS=$save_CFLAGS + + if test "x$enable_lmdb" = xyes; then + AC_DEFINE([USE_LMDB], , [Define if you are using LMDB for chathistory]) + LIBS="$LIBS -L$unet_cv_with_lmdb_prefix $LMDB_LDFLAGS" + CFLAGS="$CFLAGS -I$unet_cv_with_lmdb_inc_prefix" + else + AC_MSG_WARN([Unable to find LMDB, chathistory features will not work without liblmdb. Disabling LMDB support.]) + fi +fi + +dnl ** +dnl ** Zstd compression checks (for chathistory/metadata compression) +dnl ** +AC_MSG_CHECKING([whether to enable zstd compression support]) +AC_ARG_ENABLE([zstd], +[ --disable-zstd Disable zstd compression support], +[unet_cv_enable_zstd=$enable_zstd], +[AC_CACHE_VAL(unet_cv_enable_zstd, +[unet_cv_enable_zstd=yes])]) +AC_MSG_RESULT([$unet_cv_enable_zstd]) + +if test x"$unet_cv_enable_zstd" = xyes; then + AC_ARG_WITH([zstd], + AS_HELP_STRING([--with-zstd=dir], [Specify the installation prefix of zstd (default: /usr)]), + [base_zstd_prefix=$withval], + [base_zstd_prefix=/usr]) + + AC_MSG_CHECKING([for zstd includes]) + AC_ARG_WITH([zstd-includes], + AS_HELP_STRING([--with-zstd-includes=dir], [Specify location of zstd header files (default: /usr/include)]), + [unet_cv_with_zstd_inc_prefix=$withval], + [unet_cv_with_zstd_inc_prefix=$base_zstd_prefix/include]) + AC_MSG_RESULT([$unet_cv_with_zstd_inc_prefix]) + + AC_MSG_CHECKING([for zstd libraries]) + AC_ARG_WITH([zstd-libs], + AS_HELP_STRING([--with-zstd-libs=dir], [Specify location of zstd libs (default: /usr/lib)]), + [unet_cv_with_zstd_prefix=$withval], + [unet_cv_with_zstd_prefix=$base_zstd_prefix/lib]) + AC_MSG_RESULT([$unet_cv_with_zstd_prefix]) + + save_CFLAGS=$CFLAGS + save_LIBS=$LIBS + + CFLAGS="-I$unet_cv_with_zstd_inc_prefix" + LIBS="-L$unet_cv_with_zstd_prefix -lzstd" + + AC_CHECK_LIB(zstd, ZSTD_compress, [ + AC_CHECK_HEADERS($unet_cv_with_zstd_inc_prefix/zstd.h, [ + enable_zstd="yes"; + ZSTD_LDFLAGS="-lzstd" + ]) + ]) + + LIBS=$save_LIBS + CFLAGS=$save_CFLAGS + + if test "x$enable_zstd" = xyes; then + AC_DEFINE([USE_ZSTD], , [Define if you are using zstd compression]) + LIBS="$LIBS -L$unet_cv_with_zstd_prefix $ZSTD_LDFLAGS" + CFLAGS="$CFLAGS -I$unet_cv_with_zstd_inc_prefix" + else + AC_MSG_WARN([Unable to find zstd, compression features will not be available. Disabling zstd support.]) + fi +fi + dnl --with-maxcon allows us to set the maximum connections unet_maxcon=`ulimit -Sn` if test x"$unet_maxcon" = xunlimited; then diff --git a/include/account_conn.h b/include/account_conn.h new file mode 100644 index 00000000..2ae2a7cc --- /dev/null +++ b/include/account_conn.h @@ -0,0 +1,141 @@ +/* + * IRC - Internet Relay Chat, include/account_conn.h + * Copyright (C) 2024 AfterNET Development Team + * + * 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 2, 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. + */ +/** @file + * @brief Account connection registry for presence aggregation. + * + * This module tracks all connections logged into each account to enable + * presence aggregation across multiple devices. When a user has multiple + * connections to the same account, their effective presence is computed + * using "most-present-wins" logic: + * + * 1. If ANY connection is PRESENT -> account is PRESENT + * 2. If ALL present connections are AWAY -> use first away message + * 3. If ALL connections are AWAY_STAR -> account is hidden + */ +#ifndef INCLUDED_account_conn_h +#define INCLUDED_account_conn_h + +#ifndef INCLUDED_ircd_defs_h +#include "ircd_defs.h" +#endif +#ifndef INCLUDED_sys_types_h +#include +#define INCLUDED_sys_types_h +#endif + +struct Client; + +/** Size of account connection hash table. + * Should be a power of 2 for efficient modulo operation. + */ +#define ACCOUNT_CONN_HASHSIZE 4096 + +/** Away state for a single connection. */ +enum ConnAwayState { + CONN_PRESENT = 0, /**< Connection is not away */ + CONN_AWAY = 1, /**< Connection is away with message */ + CONN_AWAY_STAR = 2 /**< Connection is away-star (hidden) */ +}; + +/** Single connection in an account's connection list. + * Linked list of all connections for a given account. + */ +struct AccountConn { + struct AccountConn *next; /**< Next connection for this account */ + struct AccountConn **prev_p; /**< Pointer to previous->next for O(1) removal */ + struct Client *client; /**< The client connection */ + enum ConnAwayState away_state; /**< This connection's away state */ + char away_msg[AWAYLEN + 1]; /**< This connection's away message */ +}; + +/** Account entry in the account connection registry. + * Each unique account has one entry containing all its connections. + */ +struct AccountEntry { + struct AccountEntry *hnext; /**< Hash chain for collision handling */ + char account[ACCOUNTLEN + 1]; /**< Account name (hash key) */ + struct AccountConn *connections; /**< Head of connection list */ + unsigned int conn_count; /**< Number of connections */ + enum ConnAwayState effective_state; /**< Computed aggregated presence */ + char effective_away_msg[AWAYLEN + 1]; /**< Current effective away message */ + time_t last_present; /**< Last time any conn was present */ +}; + +/** Initialize account connection registry. + * Called once at server startup. + */ +extern void account_conn_init(void); + +/** Add a client to the account connection registry. + * Creates an account entry if this is the first connection. + * The client must have IsAccount(cptr) true. + * @param[in] cptr Client that just got an account + * @return Pointer to the AccountConn structure, or NULL on error + */ +extern struct AccountConn *account_conn_add(struct Client *cptr); + +/** Remove a client from the account connection registry. + * Frees the account entry if this was the last connection. + * Recomputes effective presence for remaining connections. + * @param[in] cptr Client to remove + * @return 1 if effective presence changed (broadcast needed), 0 otherwise + */ +extern int account_conn_remove(struct Client *cptr); + +/** Find account entry by name. + * @param[in] account Account name to look up + * @return AccountEntry or NULL if not found + */ +extern struct AccountEntry *account_conn_find(const char *account); + +/** Get connection count for an account. + * @param[in] account Account name + * @return Number of connections, 0 if account not found + */ +extern unsigned int account_conn_count(const char *account); + +/** Update away state for a connection and recompute effective presence. + * @param[in] cptr Client whose state changed + * @param[in] state New away state for this connection + * @param[in] message Away message (can be NULL for PRESENT) + * @return 1 if effective presence changed (broadcast needed), 0 otherwise + */ +extern int account_conn_set_away(struct Client *cptr, + enum ConnAwayState state, + const char *message); + +/** Get effective presence state for an account. + * @param[in] account Account name + * @param[out] state Receives effective state (can be NULL) + * @param[out] message Receives effective away message (can be NULL) + * @param[in] msg_size Size of message buffer + * @return 0 on success, -1 if account not found + */ +extern int account_conn_get_presence(const char *account, + enum ConnAwayState *state, + char *message, + size_t msg_size); + +/** Get the last time any connection for this account was present. + * @param[in] account Account name + * @return Unix timestamp, or 0 if account not found or never present + */ +extern time_t account_conn_last_present(const char *account); + +#endif /* INCLUDED_account_conn_h */ diff --git a/include/capab.h b/include/capab.h index feac53bc..db1d2f65 100644 --- a/include/capab.h +++ b/include/capab.h @@ -23,8 +23,41 @@ * @version $Id: capab.h 1349 2005-04-05 01:46:05Z entrope $ */ -#ifndef INCLUDED_client_h -#include "client.h" +/* Forward declaration for function prototype */ +struct Client; + +/** Number of bits in an unsigned long. */ +#ifndef FLAGSET_NBITS +#define FLAGSET_NBITS (8 * sizeof(unsigned long)) +#endif +/** Index for a flag in the bits array. */ +#ifndef FLAGSET_INDEX +#define FLAGSET_INDEX(flag) ((flag) / FLAGSET_NBITS) +#endif +/** Element bit for flag. */ +#ifndef FLAGSET_MASK +#define FLAGSET_MASK(flag) (1ul<<((flag) % FLAGSET_NBITS)) +#endif +/** Declare a flagset structure of a particular size. */ +#ifndef DECLARE_FLAGSET +#define DECLARE_FLAGSET(name,max) \ + struct name \ + { \ + unsigned long bits[((max + FLAGSET_NBITS - 1) / FLAGSET_NBITS)]; \ + } +#endif + +/** Test whether a flag is set in a flagset. */ +#ifndef FlagHas +#define FlagHas(set,flag) ((set)->bits[FLAGSET_INDEX(flag)] & FLAGSET_MASK(flag)) +#endif +/** Set a flag in a flagset. */ +#ifndef FlagSet +#define FlagSet(set,flag) ((set)->bits[FLAGSET_INDEX(flag)] |= FLAGSET_MASK(flag)) +#endif +/** Clear a flag in a flagset. */ +#ifndef FlagClr +#define FlagClr(set,flag) ((set)->bits[FLAGSET_INDEX(flag)] &= ~FLAGSET_MASK(flag)) #endif #define CAPFL_HIDDEN 0x0001 /**< Do not advertize this capability */ @@ -45,6 +78,29 @@ enum Capab { _CAP(AWAYNOTIFY, 0, "away-notify", 0), _CAP(ACCNOTIFY, 0, "account-notify", 0), _CAP(SASL, 0, "sasl", 0), + _CAP(CAPNOTIFY, 0, "cap-notify", 0), + _CAP(SERVERTIME, 0, "server-time", 0), + _CAP(ECHOMSG, 0, "echo-message", 0), + _CAP(ACCOUNTTAG, 0, "account-tag", 0), + _CAP(CHGHOST, 0, "chghost", 0), + _CAP(INVITENOTIFY, 0, "invite-notify", 0), + _CAP(LABELEDRESP, 0, "labeled-response", 0), + _CAP(BATCH, 0, "batch", 0), + _CAP(SETNAME, 0, "setname", 0), + _CAP(STANDARDREPLIES, 0, "standard-replies", 0), + _CAP(MSGTAGS, 0, "message-tags", 0), + _CAP(DRAFT_NOIMPLICITNAMES, 0, "draft/no-implicit-names", 0), + _CAP(DRAFT_EXTISUPPORT, 0, "draft/extended-isupport", 0), + _CAP(DRAFT_PREAWAY, 0, "draft/pre-away", 0), + _CAP(DRAFT_MULTILINE, 0, "draft/multiline", 0), + _CAP(DRAFT_CHATHISTORY, 0, "draft/chathistory", 0), + _CAP(DRAFT_EVENTPLAYBACK, 0, "draft/event-playback", 0), + _CAP(DRAFT_REDACT, 0, "draft/message-redaction", 0), + _CAP(DRAFT_ACCOUNTREG, 0, "draft/account-registration", 0), + _CAP(DRAFT_READMARKER, 0, "draft/read-marker", 0), + _CAP(DRAFT_CHANRENAME, 0, "draft/channel-rename", 0), + _CAP(DRAFT_METADATA2, 0, "draft/metadata-2", 0), + _CAP(DRAFT_WEBPUSH, 0, "draft/webpush", 0), #ifdef USE_SSL _CAP(TLS, 0, "tls", 0), #endif diff --git a/include/channel.h b/include/channel.h index 137a5ab6..523a5bcd 100644 --- a/include/channel.h +++ b/include/channel.h @@ -36,6 +36,7 @@ struct SLink; struct Client; +struct MetadataEntry; /* * General defines @@ -387,6 +388,7 @@ struct Channel { char topic_nick[NICKLEN + USERLEN + HOSTLEN + 3]; /**< Nick of the person who set * The topic */ + struct MetadataEntry* metadata; /**< Channel metadata (draft/metadata-2) */ char chname[1]; /**< Dynamically allocated string of the * channel name */ @@ -481,6 +483,15 @@ extern void send_hack_notice(struct Client *cptr, struct Client *sptr, int parc, char *parv[], int badop, int mtype); extern struct Channel *get_channel(struct Client *cptr, char *chname, ChannelGetType flag); +extern int rename_channel(struct Channel **chptr_p, const char *newname); + +/* Pending rename infrastructure (for services-authorized renames) */ +struct PendingRename; +extern struct PendingRename *pending_rename_find(unsigned int cookie); +extern void pending_rename_complete(struct PendingRename *pr); +extern void pending_rename_deny(struct PendingRename *pr, const char *reason); +extern void pending_rename_client_exit(struct Client *cptr); + extern struct Membership* find_member_link(struct Channel * chptr, const struct Client* cptr); extern int sub1_from_channel(struct Channel* chptr); diff --git a/include/client.h b/include/client.h index bd6974df..c3eb1048 100644 --- a/include/client.h +++ b/include/client.h @@ -61,6 +61,8 @@ struct Whowas; struct hostent; struct Privs; struct AuthRequest; +struct MetadataEntry; +struct MetadataSub; /* * Structures @@ -74,28 +76,42 @@ struct AuthRequest; typedef unsigned long flagpage_t; /** Number of bits in a flagpage_t. */ +#ifndef FLAGSET_NBITS #define FLAGSET_NBITS (8 * sizeof(flagpage_t)) +#endif /** Element number for flag \a flag. */ +#ifndef FLAGSET_INDEX #define FLAGSET_INDEX(flag) ((flag) / FLAGSET_NBITS) +#endif /** Element bit for flag \a flag. */ +#ifndef FLAGSET_MASK #define FLAGSET_MASK(flag) (1ul<<((flag) % FLAGSET_NBITS)) +#endif /** Declare a flagset structure of a particular size. */ +#ifndef DECLARE_FLAGSET #define DECLARE_FLAGSET(name,max) \ struct name \ { \ unsigned long bits[((max + FLAGSET_NBITS - 1) / FLAGSET_NBITS)]; \ } +#endif /** Test whether a flag is set in a flagset. */ +#ifndef FlagHas #define FlagHas(set,flag) ((set)->bits[FLAGSET_INDEX(flag)] & FLAGSET_MASK(flag)) +#endif /** Set a flag in a flagset. */ +#ifndef FlagSet #define FlagSet(set,flag) ((set)->bits[FLAGSET_INDEX(flag)] |= FLAGSET_MASK(flag)) +#endif /** Clear a flag in a flagset. */ +#ifndef FlagClr #define FlagClr(set,flag) ((set)->bits[FLAGSET_INDEX(flag)] &= ~FLAGSET_MASK(flag)) +#endif /** String containing valid user modes, in no particular order. */ -#define infousermodes "adgiknoqswxzBDHLNORWX" +#define infousermodes "adgiknoqswxzBDHLMNORWX" /** Operator privileges. */ enum Priv @@ -219,6 +235,8 @@ enum Flag FLAG_SSL, /**< User is connected via SSL (+z) */ FLAG_STARTTLS, /**< User is connecting with StartTLS */ FLAG_SSLNEEDACCEPT, /**< Client needs SSL_accept() to be called again */ + FLAG_WEBSOCKET, /**< Client is connected via WebSocket */ + FLAG_WSNEEDHANDSHAKE, /**< WebSocket client needs handshake */ FLAG_IPCEXEMPT, /**< User is IPcheck exempt */ FLAG_IPCNOTEXEMPT, /**< User is not IPcheck exempt */ @@ -238,6 +256,12 @@ enum Flag FLAG_SERVER_NOOP, /**< Server has been NOOP'ed */ FLAG_SENT_CVERSION, /**< Client's CTCP VERSION reply has been sent out */ + FLAG_MULTILINE_EXPAND, /**< User opts into full multiline expansion (+M) */ + + FLAG_OPER_PENDING, /**< Async OPER password verification in progress */ + FLAG_WEBIRC_PENDING, /**< Async WEBIRC password verification in progress */ + FLAG_SETHOST_PENDING, /**< Async SETHOST password verification in progress */ + FLAG_LAST_FLAG, /**< number of flags */ FLAG_LOCAL_UMODES = FLAG_LOCOP, /**< First local mode flag */ FLAG_GLOBAL_UMODES = FLAG_OPER /**< First global mode flag */ @@ -267,6 +291,7 @@ struct Connection unsigned int con_snomask; /**< mask for server messages */ time_t con_nextnick; /**< Next time a nick change is allowed */ time_t con_nexttarget;/**< Next time a target change is allowed */ + time_t con_nextaway; /**< Next time an AWAY change is allowed */ time_t con_lasttime; /**< Last time data read from socket */ time_t con_since; /**< Last time we accepted a command */ struct MsgQ con_sendQ; /**< Outgoing message queue */ @@ -307,8 +332,41 @@ struct Connection client */ struct CapSet con_capab; /**< Client capabilities (from us) */ struct CapSet con_active; /**< Active capabilities (to us) */ + unsigned short con_capab_version; /**< CAP version (0, 301, 302) */ struct AuthRequest* con_auth; /**< Auth request for client */ struct LOCInfo* con_loc; /**< Login-on-connect information */ + char con_label[64]; /**< Current command label for labeled-response */ + char con_batch_id[16]; /**< Current batch reference ID */ + unsigned int con_batch_seq; /**< Batch sequence number for generating IDs */ + char con_client_tags[512]; /**< Client-only tags (+tag=value) for TAGMSG relay */ + char con_s2s_time[32]; /**< S2S @time tag from incoming message */ + char con_s2s_msgid[64]; /**< S2S @msgid tag from incoming message */ + char con_s2s_batch_id[32]; /**< Active S2S batch ID from server */ + char con_s2s_batch_type[16]; /**< Active S2S batch type (netjoin, netsplit) */ + unsigned char con_pre_away; /**< Pre-registration away state: 0=none, 1=away, 2=away-star */ + char con_pre_away_msg[AWAYLEN + 1]; /**< Pre-registration away message */ + /* Multiline batch state (draft/multiline) */ + char con_ml_batch_id[65]; /**< Active multiline batch ID (IRCv3 allows up to 64 chars) */ + char con_ml_target[CHANNELLEN + 1]; /**< Multiline batch target (channel or nick) */ + struct SLink* con_ml_messages; /**< List of multiline messages */ + int con_ml_msg_count; /**< Number of messages in batch */ + int con_ml_total_bytes; /**< Total bytes in batch */ + time_t con_ml_batch_start; /**< When batch was started (for timeout) */ + int con_ml_lag_accum; /**< Accumulated fake lag during batch (applied at end) */ + char con_ml_label[64]; /**< Label from BATCH +id for labeled-response on WARN */ + /* Batch rate limiting (FEAT_BATCH_RATE_LIMIT) */ + time_t con_batch_minute; /**< Start of current rate limit window */ + int con_batch_count; /**< Number of batches in current window */ + int con_ml_had_fallback; /**< Batch had recipients needing fallback (for recipient discount) */ + /* Current message @batch tag for PRIVMSG interception */ + char con_msg_batch_tag[65]; /**< @batch tag from current message (IRCv3 allows up to 64 chars) */ + unsigned char con_msg_concat; /**< draft/multiline-concat tag present */ + /* WebSocket state for RFC 6455 compliance */ + unsigned char con_ws_frame_buf[BUFSIZE]; /**< Partial WebSocket frame buffer */ + int con_ws_frame_len; /**< Length of data in frame buffer */ + char con_ws_frag_buf[16384]; /**< Fragment reassembly buffer */ + int con_ws_frag_len; /**< Length of data in fragment buffer */ + int con_ws_frag_opcode; /**< Opcode of first fragment */ }; /** Magic constant to identify valid Connection structures. */ @@ -353,6 +411,7 @@ struct Client { char cli_webirc[BUFSIZE + 1]; /**< webirc description */ char cli_version[VERSIONLEN + 1]; /**< Free form client version information */ char cli_sslclifp[BUFSIZE + 1]; /**< SSL client certificate fingerprint if available */ + time_t cli_sslcliexp; /**< SSL client certificate expiration timestamp */ char cli_killmark[BUFSIZE + 1]; /**< Kill block mark */ /* SASL */ @@ -361,7 +420,14 @@ struct Client { char cli_saslaccount[ACCOUNTLEN + 1]; /**< SASL authenticated account name */ time_t cli_saslacccreate; /**< SASL authenticate account timestamp */ unsigned int cli_saslcookie; /**< SASL session cookie */ + time_t cli_saslstart; /**< When SASL authentication started (for stale response detection) */ struct Timer cli_sasltimeout; /**< timeout timer for SASL */ + + /* IRCv3 Metadata */ + struct MetadataEntry* cli_metadata; /**< Client metadata key-value pairs */ + struct MetadataSub* cli_metadatasub; /**< Client metadata subscriptions */ + time_t cli_metadata_lastcmd; /**< Time of last metadata command */ + int cli_metadata_cmdcnt; /**< Metadata commands this second */ }; /** Magic constant to identify valid Client structures. */ @@ -415,6 +481,60 @@ struct Client { #define cli_capab(cli) con_capab(cli_connect(cli)) /** Get active client capabilities for client */ #define cli_active(cli) con_active(cli_connect(cli)) +/** Get CAP version for client (0, 301, 302) */ +#define cli_capab_version(cli) con_capab_version(cli_connect(cli)) +/** Get current command label for labeled-response */ +#define cli_label(cli) con_label(cli_connect(cli)) +/** Get current batch reference ID */ +#define cli_batch_id(cli) con_batch_id(cli_connect(cli)) +/** Get batch sequence number */ +#define cli_batch_seq(cli) con_batch_seq(cli_connect(cli)) +/** Get client-only tags buffer for TAGMSG relay */ +#define cli_client_tags(cli) con_client_tags(cli_connect(cli)) +/** Get S2S @time tag from incoming message */ +#define cli_s2s_time(cli) con_s2s_time(cli_connect(cli)) +/** Get S2S @msgid tag from incoming message */ +#define cli_s2s_msgid(cli) con_s2s_msgid(cli_connect(cli)) +/** Get S2S batch ID from server */ +#define cli_s2s_batch_id(cli) con_s2s_batch_id(cli_connect(cli)) +/** Get S2S batch type from server */ +#define cli_s2s_batch_type(cli) con_s2s_batch_type(cli_connect(cli)) +/** Get active multiline batch ID. */ +#define cli_ml_batch_id(cli) con_ml_batch_id(cli_connect(cli)) +/** Get multiline batch target. */ +#define cli_ml_target(cli) con_ml_target(cli_connect(cli)) +/** Get multiline message list. */ +#define cli_ml_messages(cli) con_ml_messages(cli_connect(cli)) +/** Get multiline message count. */ +#define cli_ml_msg_count(cli) con_ml_msg_count(cli_connect(cli)) +/** Get multiline total bytes. */ +#define cli_ml_total_bytes(cli) con_ml_total_bytes(cli_connect(cli)) +/** Get multiline batch start time. */ +#define cli_ml_batch_start(cli) con_ml_batch_start(cli_connect(cli)) +/** Get accumulated lag during batch. */ +#define cli_ml_lag_accum(cli) con_ml_lag_accum(cli_connect(cli)) +/** Get label from BATCH +id for labeled-response. */ +#define cli_ml_label(cli) con_ml_label(cli_connect(cli)) +/** Get batch rate limit window start time. */ +#define cli_batch_minute(cli) con_batch_minute(cli_connect(cli)) +/** Get batch count in current rate limit window. */ +#define cli_batch_count(cli) con_batch_count(cli_connect(cli)) +/** Get whether batch had fallback recipients. */ +#define cli_ml_had_fallback(cli) con_ml_had_fallback(cli_connect(cli)) +/** Get current message @batch tag. */ +#define cli_msg_batch_tag(cli) con_msg_batch_tag(cli_connect(cli)) +/** Get current message concat flag. */ +#define cli_msg_concat(cli) con_msg_concat(cli_connect(cli)) +/** Get WebSocket partial frame buffer. */ +#define cli_ws_frame_buf(cli) con_ws_frame_buf(cli_connect(cli)) +/** Get WebSocket partial frame buffer length. */ +#define cli_ws_frame_len(cli) con_ws_frame_len(cli_connect(cli)) +/** Get WebSocket fragment reassembly buffer. */ +#define cli_ws_frag_buf(cli) con_ws_frag_buf(cli_connect(cli)) +/** Get WebSocket fragment reassembly buffer length. */ +#define cli_ws_frag_len(cli) con_ws_frag_len(cli_connect(cli)) +/** Get WebSocket first fragment opcode. */ +#define cli_ws_frag_opcode(cli) con_ws_frag_opcode(cli_connect(cli)) /** Get client name. */ #define cli_name(cli) ((cli)->cli_name) /** Get client username (ident). */ @@ -423,6 +543,8 @@ struct Client { #define cli_info(cli) ((cli)->cli_info) /** Get client account string. */ #define cli_account(cli) (cli_user(cli) ? cli_user(cli)->account : "0") +/** Get client account connection registry entry. */ +#define cli_account_conn(cli) (cli_user(cli) ? cli_user(cli)->account_conn : NULL) /** Get client connection IP address. */ #define cli_connectip(cli) ((cli)->cli_connectip) /** Get client connection host name. */ @@ -433,6 +555,8 @@ struct Client { #define cli_version(cli) ((cli)->cli_version) /** Get a clients SSL fingerprint string. */ #define cli_sslclifp(cli) ((cli)->cli_sslclifp) +/** Get a clients SSL certificate expiration timestamp. */ +#define cli_sslcliexp(cli) ((cli)->cli_sslcliexp) /** Get a clients Kill block exemption mark. */ #define cli_killmark(cli) ((cli)->cli_killmark) /** Get all marks set for client. */ @@ -455,8 +579,18 @@ struct Client { #define cli_saslacccreate(cli) ((cli)->cli_saslacccreate) /** Get SASL session cookie. */ #define cli_saslcookie(cli) ((cli)->cli_saslcookie) +/** Get SASL start timestamp. */ +#define cli_saslstart(cli) ((cli)->cli_saslstart) /** Get Timer for SASL timeout. */ #define cli_sasltimeout(cli) ((cli)->cli_sasltimeout) +/** Get client metadata list. */ +#define cli_metadata(cli) ((cli)->cli_metadata) +/** Get client metadata subscriptions. */ +#define cli_metadatasub(cli) ((cli)->cli_metadatasub) +/** Get time of last metadata command. */ +#define cli_metadata_lastcmd(cli) ((cli)->cli_metadata_lastcmd) +/** Get metadata command count for current second. */ +#define cli_metadata_cmdcnt(cli) ((cli)->cli_metadata_cmdcnt) /** Get number of incoming bytes queued for client. */ #define cli_count(cli) con_count(cli_connect(cli)) @@ -474,6 +608,8 @@ struct Client { #define cli_nextnick(cli) con_nextnick(cli_connect(cli)) /** Get next time a target change is allowed for the client. */ #define cli_nexttarget(cli) con_nexttarget(cli_connect(cli)) +/** Get next time an AWAY change is allowed for the client. */ +#define cli_nextaway(cli) con_nextaway(cli_connect(cli)) /** Get SendQ for client. */ #define cli_sendQ(cli) con_sendQ(cli_connect(cli)) /** Get RecvQ for client. */ @@ -557,6 +693,8 @@ struct Client { #define con_nextnick(con) ((con)->con_nextnick) /** Get next new target time for connection. */ #define con_nexttarget(con) ((con)->con_nexttarget) +/** Get next AWAY change time for connection. */ +#define con_nextaway(con) ((con)->con_nextaway) /** Get last time we read from the connection. */ #define con_lasttime(con) ((con)->con_lasttime) /** Get last time we accepted a command from the connection. */ @@ -611,8 +749,66 @@ struct Client { #define con_capab(con) (&(con)->con_capab) /** Get the active capabilities for the connection. */ #define con_active(con) (&(con)->con_active) +/** Get the CAP version for the connection (0, 301, 302). */ +#define con_capab_version(con) ((con)->con_capab_version) /** Get the auth request for the connection. */ #define con_auth(con) ((con)->con_auth) +/** Get the current command label for labeled-response. */ +#define con_label(con) ((con)->con_label) +/** Get the current batch reference ID. */ +#define con_batch_id(con) ((con)->con_batch_id) +/** Get the batch sequence number. */ +#define con_batch_seq(con) ((con)->con_batch_seq) +/** Get the client-only tags buffer for TAGMSG relay. */ +#define con_client_tags(con) ((con)->con_client_tags) +/** Get the S2S @time tag from incoming message. */ +#define con_s2s_time(con) ((con)->con_s2s_time) +/** Get the S2S @msgid tag from incoming message. */ +#define con_s2s_msgid(con) ((con)->con_s2s_msgid) +/** Get the S2S batch ID from server. */ +#define con_s2s_batch_id(con) ((con)->con_s2s_batch_id) +/** Get the S2S batch type from server. */ +#define con_s2s_batch_type(con) ((con)->con_s2s_batch_type) +/** Get the pre-registration away state. */ +#define con_pre_away(con) ((con)->con_pre_away) +/** Get the pre-registration away message. */ +#define con_pre_away_msg(con) ((con)->con_pre_away_msg) +/** Get the multiline batch ID. */ +#define con_ml_batch_id(con) ((con)->con_ml_batch_id) +/** Get the multiline batch target. */ +#define con_ml_target(con) ((con)->con_ml_target) +/** Get the multiline message list. */ +#define con_ml_messages(con) ((con)->con_ml_messages) +/** Get the multiline message count. */ +#define con_ml_msg_count(con) ((con)->con_ml_msg_count) +/** Get the multiline total bytes. */ +#define con_ml_total_bytes(con) ((con)->con_ml_total_bytes) +/** Get the multiline batch start time. */ +#define con_ml_batch_start(con) ((con)->con_ml_batch_start) +/** Get the accumulated lag during batch (to apply at batch end). */ +#define con_ml_lag_accum(con) ((con)->con_ml_lag_accum) +/** Get the label from BATCH +id for labeled-response on WARN. */ +#define con_ml_label(con) ((con)->con_ml_label) +/** Get the batch rate limit window start time. */ +#define con_batch_minute(con) ((con)->con_batch_minute) +/** Get the batch count in current rate limit window. */ +#define con_batch_count(con) ((con)->con_batch_count) +/** Get whether batch had fallback recipients. */ +#define con_ml_had_fallback(con) ((con)->con_ml_had_fallback) +/** Get the current message @batch tag. */ +#define con_msg_batch_tag(con) ((con)->con_msg_batch_tag) +/** Get the current message draft/multiline-concat flag. */ +#define con_msg_concat(con) ((con)->con_msg_concat) +/** Get WebSocket partial frame buffer. */ +#define con_ws_frame_buf(con) ((con)->con_ws_frame_buf) +/** Get WebSocket partial frame buffer length. */ +#define con_ws_frame_len(con) ((con)->con_ws_frame_len) +/** Get WebSocket fragment reassembly buffer. */ +#define con_ws_frag_buf(con) ((con)->con_ws_frag_buf) +/** Get WebSocket fragment reassembly buffer length. */ +#define con_ws_frag_len(con) ((con)->con_ws_frag_len) +/** Get WebSocket first fragment opcode. */ +#define con_ws_frag_opcode(con) ((con)->con_ws_frag_opcode) #define STAT_CONNECTING 0x001 /**< connecting to another server */ #define STAT_HANDSHAKE 0x002 /**< pass - server sent */ @@ -717,6 +913,12 @@ struct Client { #define IsLocOp(x) (MyConnect(x) && HasFlag(x, FLAG_LOCOP)) /** Return non-zero if the client has set mode +o (global operator). */ #define IsOper(x) HasFlag(x, FLAG_OPER) +/** Return non-zero if the client has pending async OPER verification. */ +#define IsOperPending(x) HasFlag(x, FLAG_OPER_PENDING) +/** Return non-zero if the client has pending async WEBIRC verification. */ +#define IsWebIRCPending(x) HasFlag(x, FLAG_WEBIRC_PENDING) +/** Return non-zero if the client has pending async SETHOST verification. */ +#define IsSetHostPending(x) HasFlag(x, FLAG_SETHOST_PENDING) /** Return non-zero if the client has an active UDP ping request. */ #define IsUPing(x) HasFlag(x, FLAG_UPING) /** Return non-zero if the client has no '\n' in its buffer. */ @@ -787,6 +989,10 @@ struct Client { #define IsStartTLS(x) HasFlag(x, FLAG_STARTTLS) /** Return non-zero if the client still needs SSL_accept(). */ #define IsSSLNeedAccept(x) HasFlag(x, FLAG_SSLNEEDACCEPT) +/** Return non-zero if the client is connected via WebSocket. */ +#define IsWebSocket(x) HasFlag(x, FLAG_WEBSOCKET) +/** Return non-zero if the client needs WebSocket handshake. */ +#define IsWSNeedHandshake(x) HasFlag(x, FLAG_WSNEEDHANDSHAKE) /** Return non-zero if the client is IPcheck exempt. */ #define IsIPCheckExempt(x) HasFlag(x, FLAG_IPCEXEMPT) /** Return non-zero if the client is not IPcheck exempt. */ @@ -841,6 +1047,12 @@ struct Client { #define SetLocOp(x) SetFlag(x, FLAG_LOCOP) /** Mark a client as having mode +o (global operator). */ #define SetOper(x) SetFlag(x, FLAG_OPER) +/** Mark a client as having pending async OPER verification. */ +#define SetOperPending(x) SetFlag(x, FLAG_OPER_PENDING) +/** Mark a client as having pending async WEBIRC verification. */ +#define SetWebIRCPending(x) SetFlag(x, FLAG_WEBIRC_PENDING) +/** Mark a client as having pending async SETHOST verification. */ +#define SetSetHostPending(x) SetFlag(x, FLAG_SETHOST_PENDING) /** Mark a client as having a pending UDP ping. */ #define SetUPing(x) SetFlag(x, FLAG_UPING) /** Mark a client as having mode +w (wallops). */ @@ -907,6 +1119,10 @@ struct Client { #define SetStartTLS(x) SetFlag(x, FLAG_STARTTLS) /** Mark a client as needing SSL_accept(). */ #define SetSSLNeedAccept(x) SetFlag(x, FLAG_SSLNEEDACCEPT) +/** Mark a client as connected via WebSocket. */ +#define SetWebSocket(x) SetFlag(x, FLAG_WEBSOCKET) +/** Mark a client as needing WebSocket handshake. */ +#define SetWSNeedHandshake(x) SetFlag(x, FLAG_WSNEEDHANDSHAKE) /** Mark a client as IPcheck exempt. */ #define SetIPCheckExempt(x) SetFlag(x, FLAG_IPCEXEMPT) /** Mark a client as not IPcheck exempt. */ @@ -956,6 +1172,12 @@ struct Client { #define ClearLocOp(x) ClrFlag(x, FLAG_LOCOP) /** Remove mode +o (global operator) from the client. */ #define ClearOper(x) ClrFlag(x, FLAG_OPER) +/** Clear the client's pending async OPER verification flag. */ +#define ClearOperPending(x) ClrFlag(x, FLAG_OPER_PENDING) +/** Clear the client's pending async WEBIRC verification flag. */ +#define ClearWebIRCPending(x) ClrFlag(x, FLAG_WEBIRC_PENDING) +/** Clear the client's pending async SETHOST verification flag. */ +#define ClearSetHostPending(x) ClrFlag(x, FLAG_SETHOST_PENDING) /** Clear the client's pending UDP ping flag. */ #define ClearUPing(x) ClrFlag(x, FLAG_UPING) /** Remove mode +w (wallops) from the client. */ @@ -1012,6 +1234,10 @@ struct Client { #define ClearStartTLS(x) ClrFlag(x, FLAG_STARTTLS) /** Client no longer needs SSL_accept(). */ #define ClearSSLNeedAccept(x) ClrFlag(x, FLAG_SSLNEEDACCEPT) +/** Client no longer connected via WebSocket. */ +#define ClearWebSocket(x) ClrFlag(x, FLAG_WEBSOCKET) +/** Client no longer needs WebSocket handshake. */ +#define ClearWSNeedHandshake(x) ClrFlag(x, FLAG_WSNEEDHANDSHAKE) /** Clear the client's join restriction. */ #define ClearRestrictJoin(x) ClrFlag(x, FLAG_RESTRICT_JOIN) /** Clear the client's PRIVMSG/NOTICE restriction. */ @@ -1034,6 +1260,8 @@ struct Client { #define ClearOpLevels(x) ClrFlag(x, FLAG_OPLEVELS) /** Clear the client's account status. */ #define ClearAccount(x) ClrFlag(x, FLAG_ACCOUNT) +/** Clear the client's SASL authentication complete flag. */ +#define ClearSASLComplete(x) ClrFlag(x, FLAG_SASLCOMPLETE) /* free flags */ #define FREEFLAG_SOCKET 0x0001 /**< socket needs to be freed */ diff --git a/include/handlers.h b/include/handlers.h index 3ea127a7..6461bb6d 100644 --- a/include/handlers.h +++ b/include/handlers.h @@ -89,6 +89,7 @@ struct Client; extern int m_admin(struct Client*, struct Client*, int, char*[]); extern int m_authenticate(struct Client*, struct Client*, int, char*[]); extern int m_away(struct Client*, struct Client*, int, char*[]); +extern int mu_away(struct Client*, struct Client*, int, char*[]); extern int m_cap(struct Client*, struct Client*, int, char*[]); extern int m_cnotice(struct Client*, struct Client*, int, char*[]); extern int m_cprivmsg(struct Client*, struct Client*, int, char*[]); @@ -97,6 +98,7 @@ extern int m_gline(struct Client*, struct Client*, int, char*[]); extern int m_help(struct Client*, struct Client*, int, char*[]); extern int m_ignore(struct Client*, struct Client*, int, char*[]); extern int m_info(struct Client*, struct Client*, int, char*[]); +extern int m_isupport(struct Client*, struct Client*, int, char*[]); extern int m_invite(struct Client*, struct Client*, int, char*[]); extern int m_ircops(struct Client*, struct Client*, int, char*[]); extern int m_isnef(struct Client*, struct Client*, int, char*[]); @@ -133,6 +135,8 @@ extern int m_quit(struct Client*, struct Client*, int, char*[]); extern int m_registered(struct Client*, struct Client*, int, char*[]); extern int m_rules(struct Client*, struct Client*, int, char*[]); extern int m_sethost(struct Client*, struct Client*, int, char*[]); +extern int m_setname(struct Client*, struct Client*, int, char*[]); +extern int m_tagmsg(struct Client*, struct Client*, int, char*[]); extern int m_shun(struct Client*, struct Client*, int, char*[]); extern int m_silence(struct Client*, struct Client*, int, char*[]); extern int m_starttls(struct Client*, struct Client*, int, char*[]); @@ -241,6 +245,8 @@ extern int ms_rpong(struct Client*, struct Client*, int, char*[]); extern int ms_rules(struct Client*, struct Client*, int, char*[]); extern int ms_sasl(struct Client*, struct Client*, int, char*[]); extern int ms_server(struct Client*, struct Client*, int, char*[]); +extern int ms_setname(struct Client*, struct Client*, int, char*[]); +extern int ms_tagmsg(struct Client*, struct Client*, int, char*[]); extern int ms_settime(struct Client*, struct Client*, int, char*[]); extern int ms_shun(struct Client*, struct Client*, int, char*[]); extern int ms_silence(struct Client*, struct Client*, int, char*[]); @@ -271,6 +277,27 @@ extern int ms_whois(struct Client*, struct Client*, int, char*[]); extern int ms_xquery(struct Client*, struct Client*, int, char*[]); extern int ms_xreply(struct Client*, struct Client*, int, char*[]); extern int ms_zline(struct Client*, struct Client*, int, char*[]); +extern int ms_batch(struct Client*, struct Client*, int, char*[]); +extern int m_batch(struct Client*, struct Client*, int, char*[]); +extern int check_client_batch_timeout(struct Client*); +extern int ms_multiline(struct Client*, struct Client*, int, char*[]); +extern int m_chathistory(struct Client*, struct Client*, int, char*[]); +extern int ms_chathistory(struct Client*, struct Client*, int, char*[]); +extern int m_redact(struct Client*, struct Client*, int, char*[]); +extern int ms_redact(struct Client*, struct Client*, int, char*[]); +extern int m_register(struct Client*, struct Client*, int, char*[]); +extern int m_verify(struct Client*, struct Client*, int, char*[]); +extern int ms_regreply(struct Client*, struct Client*, int, char*[]); +extern int m_markread(struct Client*, struct Client*, int, char*[]); +extern int ms_markread(struct Client*, struct Client*, int, char*[]); +extern void send_markread_on_join(struct Client*, const char*); +extern int m_rename(struct Client*, struct Client*, int, char*[]); +extern int ms_rename(struct Client*, struct Client*, int, char*[]); +extern int m_metadata(struct Client*, struct Client*, int, char*[]); +extern int ms_metadata(struct Client*, struct Client*, int, char*[]); +extern int ms_metadataquery(struct Client*, struct Client*, int, char*[]); +extern int m_webpush(struct Client*, struct Client*, int, char*[]); +extern int ms_webpush(struct Client*, struct Client*, int, char*[]); #endif /* INCLUDED_handlers_h */ diff --git a/include/hash.h b/include/hash.h index 44ce997a..382f6df4 100644 --- a/include/hash.h +++ b/include/hash.h @@ -86,6 +86,7 @@ extern int hAddWatch(struct Watch *wptr); extern int hRemClient(struct Client *cptr); extern int hChangeClient(struct Client *cptr, const char *newname); extern int hRemChannel(struct Channel *chptr); +extern int hChangeChannel(struct Channel *chptr, const char *newname); extern int hRemWatch(struct Watch *wptr); extern struct Client *hSeekClient(const char *name, int TMask); extern struct Channel *hSeekChannel(const char *name); diff --git a/include/history.h b/include/history.h new file mode 100644 index 00000000..a701d7fd --- /dev/null +++ b/include/history.h @@ -0,0 +1,323 @@ +/* + * IRC - Internet Relay Chat, include/history.h + * Copyright (C) 2024 Nefarious Development Team + * + * 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 2, 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. + */ +/** @file + * @brief Message history storage declarations (LMDB backend). + * + * Implements IRCv3 draft/chathistory extension storage using LMDB for + * fast, zero-copy reads with minimal memory footprint. + * + * Specification: https://ircv3.net/specs/extensions/chathistory + * Capability: draft/chathistory + */ +#ifndef INCLUDED_history_h +#define INCLUDED_history_h + +#include "ircd_defs.h" +#include + +struct Client; + +/** Maximum size of a message ID */ +#define HISTORY_MSGID_LEN 64 + +/** Maximum size of a timestamp string (Unix timestamp with milliseconds) */ +#define HISTORY_TIMESTAMP_LEN 20 + +/** Maximum size of sender string (nick!user@host) */ +#define HISTORY_SENDER_LEN (NICKLEN + USERLEN + HOSTLEN + 3) + +/** Maximum size of message content */ +#define HISTORY_CONTENT_LEN 512 + +/** Message types for history storage */ +enum HistoryMessageType { + HISTORY_PRIVMSG = 0, + HISTORY_NOTICE = 1, + HISTORY_JOIN = 2, + HISTORY_PART = 3, + HISTORY_QUIT = 4, + HISTORY_KICK = 5, + HISTORY_MODE = 6, + HISTORY_TOPIC = 7, + HISTORY_TAGMSG = 8 +}; + +/** Stored message for chathistory retrieval. + * This structure is used both for storage and for returning + * query results to the caller. + */ +struct HistoryMessage { + char msgid[HISTORY_MSGID_LEN]; /**< Unique message ID */ + char timestamp[HISTORY_TIMESTAMP_LEN]; /**< Unix timestamp (seconds.milliseconds) */ + char target[CHANNELLEN + 1]; /**< Channel name or nick */ + char sender[HISTORY_SENDER_LEN]; /**< nick!user@host of sender */ + char account[ACCOUNTLEN + 1]; /**< Sender's account name (or empty) */ + enum HistoryMessageType type; /**< Message type */ + char content[HISTORY_CONTENT_LEN]; /**< Message content */ + struct HistoryMessage *next; /**< Next in linked list (for results) */ +}; + +/** Target info for CHATHISTORY TARGETS query. */ +struct HistoryTarget { + char target[CHANNELLEN + 1]; /**< Channel name or nick */ + char last_timestamp[HISTORY_TIMESTAMP_LEN]; /**< Time of last message */ + struct HistoryTarget *next; /**< Next in linked list */ +}; + +/** Query direction for history lookups. */ +enum HistoryDirection { + HISTORY_DIR_BEFORE = 0, /**< Messages before reference */ + HISTORY_DIR_AFTER = 1, /**< Messages after reference */ + HISTORY_DIR_AROUND = 2, /**< Messages around reference */ + HISTORY_DIR_LATEST = 3 /**< Most recent messages */ +}; + +/** Reference type for history queries. */ +enum HistoryRefType { + HISTORY_REF_TIMESTAMP = 0, /**< Reference by timestamp */ + HISTORY_REF_MSGID = 1, /**< Reference by message ID */ + HISTORY_REF_NONE = 2 /**< No reference (for LATEST *) */ +}; + +/** Initialize the history subsystem. + * Opens or creates the LMDB database at the specified path. + * @param[in] dbpath Path to the database directory. + * @return 0 on success, -1 on error. + */ +extern int history_init(const char *dbpath); + +/** Shutdown the history subsystem. + * Closes the LMDB environment and frees resources. + */ +extern void history_shutdown(void); + +/** Store a message in the history database. + * @param[in] msgid Unique message ID. + * @param[in] timestamp Unix timestamp (seconds.milliseconds as string). + * @param[in] target Channel or nick. + * @param[in] sender Full sender mask (nick!user@host). + * @param[in] account Sender's account name (may be NULL). + * @param[in] type Message type. + * @param[in] content Message content (may be NULL for some types). + * @return 0 on success, -1 on error. + */ +extern int history_store_message(const char *msgid, const char *timestamp, + const char *target, const char *sender, + const char *account, enum HistoryMessageType type, + const char *content); + +/** Query messages before a reference point. + * @param[in] target Channel or nick to query. + * @param[in] ref_type Type of reference. + * @param[in] reference Timestamp or msgid string. + * @param[in] limit Maximum messages to return. + * @param[out] result Pointer to result list head (caller must free). + * @return Number of messages returned, or -1 on error. + */ +extern int history_query_before(const char *target, enum HistoryRefType ref_type, + const char *reference, int limit, + struct HistoryMessage **result); + +/** Query messages after a reference point. + * @param[in] target Channel or nick to query. + * @param[in] ref_type Type of reference. + * @param[in] reference Timestamp or msgid string. + * @param[in] limit Maximum messages to return. + * @param[out] result Pointer to result list head (caller must free). + * @return Number of messages returned, or -1 on error. + */ +extern int history_query_after(const char *target, enum HistoryRefType ref_type, + const char *reference, int limit, + struct HistoryMessage **result); + +/** Query the most recent messages. + * @param[in] target Channel or nick to query. + * @param[in] ref_type Type of reference (use HISTORY_REF_NONE for *). + * @param[in] reference Timestamp or msgid string (ignored if ref_type is NONE). + * @param[in] limit Maximum messages to return. + * @param[out] result Pointer to result list head (caller must free). + * @return Number of messages returned, or -1 on error. + */ +extern int history_query_latest(const char *target, enum HistoryRefType ref_type, + const char *reference, int limit, + struct HistoryMessage **result); + +/** Query messages around a reference point. + * Returns limit/2 messages before and limit/2 messages after. + * @param[in] target Channel or nick to query. + * @param[in] ref_type Type of reference. + * @param[in] reference Timestamp or msgid string. + * @param[in] limit Maximum messages to return. + * @param[out] result Pointer to result list head (caller must free). + * @return Number of messages returned, or -1 on error. + */ +extern int history_query_around(const char *target, enum HistoryRefType ref_type, + const char *reference, int limit, + struct HistoryMessage **result); + +/** Query messages between two reference points. + * @param[in] target Channel or nick to query. + * @param[in] ref_type1 Type of first reference. + * @param[in] reference1 First timestamp or msgid. + * @param[in] ref_type2 Type of second reference. + * @param[in] reference2 Second timestamp or msgid. + * @param[in] limit Maximum messages to return. + * @param[out] result Pointer to result list head (caller must free). + * @return Number of messages returned, or -1 on error. + */ +extern int history_query_between(const char *target, + enum HistoryRefType ref_type1, const char *reference1, + enum HistoryRefType ref_type2, const char *reference2, + int limit, struct HistoryMessage **result); + +/** Query targets with recent message activity. + * Used for CHATHISTORY TARGETS command. + * @param[in] timestamp1 Start of time range (Unix timestamp). + * @param[in] timestamp2 End of time range (Unix timestamp). + * @param[in] limit Maximum targets to return. + * @param[out] result Pointer to result list head (caller must free). + * @return Number of targets returned, or -1 on error. + */ +extern int history_query_targets(const char *timestamp1, const char *timestamp2, + int limit, struct HistoryTarget **result); + +/** Free a list of history messages. + * @param[in] list Head of the message list to free. + */ +extern void history_free_messages(struct HistoryMessage *list); + +/** Free a list of history targets. + * @param[in] list Head of the target list to free. + */ +extern void history_free_targets(struct HistoryTarget *list); + +/** Purge old messages from the database. + * Called periodically to enforce retention policy. + * @param[in] max_age_seconds Maximum age of messages to keep. + * @return Number of messages deleted, or -1 on error. + */ +extern int history_purge_old(unsigned long max_age_seconds); + +/** Get timestamp for a given message ID. + * Useful for converting msgid references to timestamps. + * @param[in] msgid Message ID to look up. + * @param[out] timestamp Buffer for timestamp (at least HISTORY_TIMESTAMP_LEN). + * @return 0 on success, -1 if not found. + */ +extern int history_msgid_to_timestamp(const char *msgid, char *timestamp); + +/** Check if history subsystem is initialized and available. + * @return 1 if available, 0 if not. + */ +extern int history_is_available(void); + +/* + * Timestamp Conversion API + * + * Internal storage and S2S use Unix timestamps (seconds.milliseconds). + * Client-facing @time= tags use ISO 8601 per IRCv3 spec. + */ + +/** Format current time as Unix timestamp string. + * @param[out] buf Buffer for timestamp (at least HISTORY_TIMESTAMP_LEN). + * @param[in] buflen Size of buffer. + * @return Pointer to buf. + */ +extern char *history_format_timestamp(char *buf, size_t buflen); + +/** Convert Unix timestamp to ISO 8601 for client display. + * @param[in] unix_ts Unix timestamp string (seconds.milliseconds). + * @param[out] iso_buf Buffer for ISO 8601 output (at least 32 bytes). + * @param[in] iso_buflen Size of ISO buffer. + * @return 0 on success, -1 on error. + */ +extern int history_unix_to_iso(const char *unix_ts, char *iso_buf, size_t iso_buflen); + +/** Convert ISO 8601 timestamp to Unix timestamp. + * @param[in] iso_ts ISO 8601 timestamp string. + * @param[out] unix_buf Buffer for Unix timestamp (at least HISTORY_TIMESTAMP_LEN). + * @param[in] unix_buflen Size of Unix buffer. + * @return 0 on success, -1 on error. + */ +extern int history_iso_to_unix(const char *iso_ts, char *unix_buf, size_t unix_buflen); + +/* + * Read Marker API (IRCv3 draft/read-marker) + * + * Read markers are stored per account+target in the same LMDB environment. + * Key: "account\0target" + * Value: Unix timestamp (seconds.milliseconds as string) + */ + +/** Get the read marker timestamp for an account and target. + * @param[in] account Account name. + * @param[in] target Channel name or nick. + * @param[out] timestamp Buffer for timestamp (at least HISTORY_TIMESTAMP_LEN). + * @return 0 on success, 1 if not found, -1 on error. + */ +extern int readmarker_get(const char *account, const char *target, char *timestamp); + +/** Set the read marker timestamp for an account and target. + * Only updates if the new timestamp is greater than the stored one. + * @param[in] account Account name. + * @param[in] target Channel name or nick. + * @param[in] timestamp Unix timestamp (seconds.milliseconds as string). + * @return 0 on success (updated), 1 if not updated (older timestamp), -1 on error. + */ +extern int readmarker_set(const char *account, const char *target, const char *timestamp); + +/** Delete a message from the history database. + * Used by message-redaction to remove redacted messages. + * @param[in] target Channel or nick where message was sent. + * @param[in] msgid Message ID to delete. + * @return 0 on success, -1 on error, 1 if not found. + */ +extern int history_delete_message(const char *target, const char *msgid); + +/** Lookup a message by ID and verify sender. + * Used by message-redaction to validate authorization. + * @param[in] target Channel or nick where message was sent. + * @param[in] msgid Message ID to look up. + * @param[out] msg Pointer to result (caller must free with history_free_messages). + * @return 0 on success, -1 on error, 1 if not found. + */ +extern int history_lookup_message(const char *target, const char *msgid, + struct HistoryMessage **msg); + +/** Set history database map size. + * Must be called before history_init(). + * @param[in] size_mb Size in megabytes. + */ +extern void history_set_map_size(size_t size_mb); + +/** Get history database map size. + * @return Current map size in bytes. + */ +extern size_t history_get_map_size(void); + +struct StatDesc; + +/** Report CHATHISTORY statistics for /STATS. + * @param[in] to Client requesting stats. + * @param[in] sd Stats descriptor. + * @param[in] param Extra parameter (unused). + */ +extern void history_report_stats(struct Client *to, const struct StatDesc *sd, char *param); + +#endif /* INCLUDED_history_h */ diff --git a/include/ircd.h b/include/ircd.h index 52820140..05235d71 100644 --- a/include/ircd.h +++ b/include/ircd.h @@ -54,6 +54,25 @@ extern char* configfile; extern int debuglevel; extern char* debugmode; extern int running; +extern unsigned long MsgIdCounter; /**< Counter for unique message IDs */ + +/** SASL mechanism list received from services (dynamic, for CAP LS) */ +#define SASL_MECHS_LEN 128 +extern char SaslMechanisms[SASL_MECHS_LEN]; + +/** Set the SASL mechanism list (called when services announces mechanisms) */ +extern void set_sasl_mechanisms(const char *mechs); +/** Get the SASL mechanism list (for CAP LS value) */ +extern const char* get_sasl_mechanisms(void); + +/** VAPID public key received from services (for webpush ISUPPORT) */ +#define VAPID_KEY_LEN 128 +extern char VapidPublicKey[VAPID_KEY_LEN]; + +/** Set the VAPID public key (called when services announces it) */ +extern void set_vapid_pubkey(const char *key); +/** Get the VAPID public key (for ISUPPORT and CAP value) */ +extern const char* get_vapid_pubkey(void); #endif /* INCLUDED_ircd_h */ diff --git a/include/ircd_compress.h b/include/ircd_compress.h new file mode 100644 index 00000000..0ba57e3a --- /dev/null +++ b/include/ircd_compress.h @@ -0,0 +1,104 @@ +/* + * IRC - Internet Relay Chat, include/ircd_compress.h + * Copyright (C) 2024 AfterNET Development Team + * + * Zstandard compression support for LMDB-backed storage. + * Provides transparent compression for chathistory and metadata. + */ +#ifndef INCLUDED_ircd_compress_h +#define INCLUDED_ircd_compress_h + +#include + +#ifdef USE_ZSTD + +/** Magic byte to identify compressed data */ +#define COMPRESS_MAGIC 0x1F + +/** Default compression threshold in bytes */ +#define COMPRESS_THRESHOLD_DEFAULT 256 + +/** Default compression level (1-22, 3 is fast with good ratio) */ +#define COMPRESS_LEVEL_DEFAULT 3 + +/** Maximum uncompressed size we'll accept (safety limit) */ +#define COMPRESS_MAX_UNCOMPRESSED 65536 + +/** + * Initialize compression subsystem. + * @param[in] threshold Minimum size to trigger compression (0 = use default) + * @param[in] level Compression level 1-22 (0 = use default) + */ +void compress_init(size_t threshold, int level); + +/** + * Check if data appears to be compressed. + * @param[in] data Data buffer + * @param[in] len Data length + * @return 1 if compressed, 0 if not + */ +int is_compressed(const unsigned char *data, size_t len); + +/** + * Compress data if it exceeds the threshold. + * @param[in] input Input data + * @param[in] input_len Input length + * @param[out] output Output buffer + * @param[in] output_size Size of output buffer + * @param[out] output_len Actual output length + * @return 1 if compressed, 0 if passed through unchanged, -1 on error + */ +int compress_data(const unsigned char *input, size_t input_len, + unsigned char *output, size_t output_size, size_t *output_len); + +/** + * Decompress data if it has compression magic byte. + * @param[in] input Input data (possibly compressed) + * @param[in] input_len Input length + * @param[out] output Output buffer + * @param[in] output_size Size of output buffer + * @param[out] output_len Actual output length + * @return 1 if decompressed, 0 if passed through unchanged, -1 on error + */ +int decompress_data(const unsigned char *input, size_t input_len, + unsigned char *output, size_t output_size, size_t *output_len); + +/** + * Get current compression threshold. + * @return Current threshold in bytes + */ +size_t compress_get_threshold(void); + +/** + * Set compression threshold. + * @param[in] threshold New threshold in bytes + */ +void compress_set_threshold(size_t threshold); + +/** + * Get current compression level. + * @return Current compression level (1-22) + */ +int compress_get_level(void); + +/** + * Set compression level. + * @param[in] level New compression level (1-22) + */ +void compress_set_level(int level); + +#else /* !USE_ZSTD */ + +/* Stub macros when zstd is not available */ +#define compress_init(t, l) do {} while(0) +#define is_compressed(d, l) (0) +#define compress_data(i, il, o, os, ol) (*(ol) = (il), memcpy((o), (i), (il)), 0) +#define decompress_data(i, il, o, os, ol) (*(ol) = (il), memcpy((o), (i), (il)), 0) +#define compress_get_threshold() (0) +#define compress_set_threshold(t) do {} while(0) +#define compress_get_level() (0) +#define compress_set_level(l) do {} while(0) + +#endif /* USE_ZSTD */ + +#endif /* INCLUDED_ircd_compress_h */ diff --git a/include/ircd_crypt.h b/include/ircd_crypt.h index 32bfcd50..d1aac820 100644 --- a/include/ircd_crypt.h +++ b/include/ircd_crypt.h @@ -67,5 +67,44 @@ extern int oper_password_match(const char* to_match, const char* passwd); /* exported variables */ extern crypt_mechs_t* crypt_mechs_root; +/* + * Async password verification API + * + * These functions offload CPU-intensive password hashing to a thread pool, + * preventing the main event loop from blocking during bcrypt/PBKDF2 operations. + */ + +/** Result codes for async verification callback */ +#define CRYPT_VERIFY_MATCH 1 /**< Password matches */ +#define CRYPT_VERIFY_NOMATCH 0 /**< Password doesn't match */ +#define CRYPT_VERIFY_ERROR -1 /**< Error during verification */ + +/** + * Callback invoked when async password verification completes. + * @param[in] result CRYPT_VERIFY_MATCH, CRYPT_VERIFY_NOMATCH, or CRYPT_VERIFY_ERROR + * @param[in] ctx User-provided context pointer + */ +typedef void (*crypt_verify_callback)(int result, void *ctx); + +/** + * Asynchronously verify a password against a hash. + * The verification runs in a worker thread; when complete, the callback + * is invoked in the main thread via thread_pool_poll(). + * + * @param[in] password Plaintext password to verify + * @param[in] hash Stored hash to verify against + * @param[in] callback Function to call with result + * @param[in] ctx Context pointer passed to callback + * @return 0 if async verification started, -1 on error (falls back to sync) + */ +extern int ircd_crypt_verify_async(const char *password, const char *hash, + crypt_verify_callback callback, void *ctx); + +/** + * Check if async password verification is available. + * @return 1 if thread pool is running, 0 otherwise + */ +extern int ircd_crypt_async_available(void); + #endif /* INCLUDED_ircd_crypt_h */ diff --git a/include/ircd_crypt_pbkdf2.h b/include/ircd_crypt_pbkdf2.h new file mode 100644 index 00000000..08f5a844 --- /dev/null +++ b/include/ircd_crypt_pbkdf2.h @@ -0,0 +1,32 @@ +/* + * IRC - Internet Relay Chat, include/ircd_crypt_pbkdf2.h + * + * 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. + */ +/** @file + * @brief PBKDF2 password hashing APIs (SHA256 and SHA512). + */ +#ifndef INCLUDED_ircd_crypt_pbkdf2_h +#define INCLUDED_ircd_crypt_pbkdf2_h + +/* PBKDF2-SHA256 ($PBKDF2$) */ +extern void ircd_register_crypt_pbkdf2(void); +extern const char* ircd_crypt_pbkdf2(const char* key, const char* salt); + +/* PBKDF2-SHA512 ($PBKDF2-SHA512$) */ +extern void ircd_register_crypt_pbkdf2_sha512(void); +extern const char* ircd_crypt_pbkdf2_sha512(const char* key, const char* salt); + +#endif /* INCLUDED_ircd_crypt_pbkdf2_h */ diff --git a/include/ircd_defs.h b/include/ircd_defs.h index 23767dcf..06752ba3 100644 --- a/include/ircd_defs.h +++ b/include/ircd_defs.h @@ -95,6 +95,13 @@ /** Maximum length for away messages. */ #define AWAYLEN 250 +/** Maximum length of a channel name. + * Also defined in channel.h; having it here allows client.h to use it + * without creating a circular dependency. + */ +#ifndef CHANNELLEN +#define CHANNELLEN 200 +#endif /** Exactly long enough to hold one protocol message (RFC 1459) * including the line termination (\\r\\n). DO NOT CHANGE THIS!!!! */ diff --git a/include/ircd_features.h b/include/ircd_features.h index f79e26d6..272f0792 100644 --- a/include/ircd_features.h +++ b/include/ircd_features.h @@ -48,6 +48,8 @@ enum Feature { FEAT_HUB, FEAT_WALLOPS_OPER_ONLY, FEAT_NODNS, + FEAT_TCP_NODELAY_C2S, + FEAT_TCP_NODELAY_S2S, FEAT_RANDOM_SEED, FEAT_DEFAULT_LIST_PARAM, FEAT_NICKNAMEHISTORYLENGTH, @@ -92,6 +94,8 @@ enum Feature { FEAT_TOS_SERVER, FEAT_TOS_CLIENT, FEAT_POLLS_PER_LOOP, + FEAT_THREAD_POOL_SIZE, + FEAT_ASYNC_LOGGING, FEAT_IRCD_RES_RETRIES, FEAT_IRCD_RES_TIMEOUT, FEAT_AUTH_TIMEOUT, @@ -300,6 +304,7 @@ enum Feature { FEAT_SSL_NOSSLV3, FEAT_SSL_NOTLSV1, FEAT_SSL_CIPHERS, + FEAT_CERT_EXPIRY_TRACKING, /* ZLINE FEAT_'s */ FEAT_DISABLE_ZLINES, @@ -313,6 +318,83 @@ enum Feature { FEAT_CAP_away_notify, FEAT_CAP_account_notify, FEAT_CAP_sasl, + FEAT_CAP_cap_notify, + FEAT_CAP_server_time, + FEAT_CAP_echo_message, + FEAT_CAP_account_tag, + FEAT_CAP_chghost, + FEAT_CAP_invite_notify, + FEAT_CAP_labeled_response, + FEAT_CAP_batch, + FEAT_CAP_setname, + FEAT_CAP_standard_replies, + FEAT_CAP_message_tags, + FEAT_CAP_draft_no_implicit_names, + FEAT_CAP_draft_extended_isupport, + FEAT_CAP_draft_pre_away, + FEAT_CAP_draft_multiline, + FEAT_CAP_draft_chathistory, + FEAT_CAP_draft_event_playback, + FEAT_CAP_draft_message_redaction, + FEAT_CAP_draft_account_registration, + FEAT_REGISTER_SERVER, + FEAT_CAP_draft_read_marker, + FEAT_CAP_draft_channel_rename, + FEAT_CAP_draft_metadata_2, + FEAT_CAP_draft_webpush, + FEAT_METADATA_MAX_KEYS, + FEAT_METADATA_MAX_VALUE_BYTES, + FEAT_METADATA_MAX_SUBS, + FEAT_METADATA_RATE_LIMIT, + FEAT_REDACT_WINDOW, + FEAT_REDACT_OPER_WINDOW, + FEAT_REDACT_CHANOP_OTHERS, + FEAT_CHATHISTORY_MAX, + FEAT_CHATHISTORY_PRIVATE, + FEAT_CHATHISTORY_PRIVATE_CONSENT, + FEAT_CHATHISTORY_ADVERTISE_PM, + FEAT_CHATHISTORY_PM_NOTICE, + FEAT_CHATHISTORY_DB, + FEAT_CHATHISTORY_RETENTION, + FEAT_CHATHISTORY_FEDERATION, + FEAT_CHATHISTORY_TIMEOUT, + FEAT_CHATHISTORY_STRICT_TIMESTAMPS, + FEAT_MULTILINE_MAX_BYTES, + FEAT_MULTILINE_MAX_LINES, + FEAT_MULTILINE_LAG_DISCOUNT, + FEAT_MULTILINE_CHANNEL_LAG_DISCOUNT, + FEAT_MULTILINE_MAX_LAG, + FEAT_MULTILINE_RECIPIENT_DISCOUNT, + FEAT_MULTILINE_ECHO_PROTECT, + FEAT_MULTILINE_ECHO_MAX_FACTOR, + FEAT_MULTILINE_LEGACY_THRESHOLD, + FEAT_MULTILINE_LEGACY_MAX_LINES, + FEAT_MULTILINE_FALLBACK_NOTIFY, + FEAT_MULTILINE_STORAGE_ENABLED, + FEAT_MULTILINE_STORAGE_TTL, + FEAT_MULTILINE_STORAGE_MAX, + FEAT_BATCH_RATE_LIMIT, + FEAT_CLIENT_BATCH_TIMEOUT, + FEAT_DRAFT_WEBSOCKET, + FEAT_WEBSOCKET_RECVQ, + FEAT_WEBSOCKET_ORIGIN, + FEAT_MSGID, + FEAT_P10_MESSAGE_TAGS, + FEAT_PRESENCE_AGGREGATION, + FEAT_AWAY_STAR_MSG, + FEAT_AWAY_THROTTLE, + FEAT_METADATA_CACHE_ENABLED, + FEAT_METADATA_X3_TIMEOUT, + FEAT_METADATA_QUEUE_SIZE, + FEAT_METADATA_BURST, + FEAT_METADATA_DB, + FEAT_METADATA_CACHE_TTL, + FEAT_METADATA_PURGE_FREQUENCY, +#ifdef USE_ZSTD + FEAT_COMPRESS_THRESHOLD, + FEAT_COMPRESS_LEVEL, +#endif + FEAT_HISTORY_MAP_SIZE_MB, #ifdef USE_SSL FEAT_CAP_tls, #endif diff --git a/include/ircd_log_async.h b/include/ircd_log_async.h new file mode 100644 index 00000000..2a26c666 --- /dev/null +++ b/include/ircd_log_async.h @@ -0,0 +1,116 @@ +/* + * IRC - Internet Relay Chat, include/ircd_log_async.h + * Copyright (C) 2025 AfterNET Development Team + * + * 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 2, 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. + */ +/** + * @file + * @brief Async logging infrastructure. + * + * This module provides non-blocking logging by offloading file and syslog + * writes to a dedicated writer thread. This prevents the main event loop + * from blocking during log I/O operations. + * + * Usage: + * 1. Call log_async_init() at startup (after feature_init) + * 2. Use log_write_async() instead of direct writev/syslog calls + * 3. Call log_async_flush() for critical messages or before shutdown + * 4. Call log_async_shutdown() at exit + */ +#ifndef INCLUDED_ircd_log_async_h +#define INCLUDED_ircd_log_async_h + +#ifndef INCLUDED_config_h +#include "config.h" +#endif + +/** Maximum size of a single log entry */ +#define LOG_ASYNC_MAX_ENTRY 2048 + +/** Default number of entries in the ring buffer */ +#define LOG_ASYNC_BUFFER_SIZE_DEFAULT 4096 + +/** Log entry for async queue */ +struct log_async_entry { + int fd; /**< File descriptor to write to (-1 for syslog) */ + int syslog_priority; /**< Syslog priority (if fd == -1) */ + int len; /**< Length of message */ + char message[LOG_ASYNC_MAX_ENTRY]; /**< Pre-formatted log message */ +}; + +#ifdef HAVE_PTHREAD + +/** + * Initialize the async logging system. + * Creates the writer thread and allocates the ring buffer. + * @param[in] buffer_size Number of entries in ring buffer (0 = use default) + * @return 0 on success, -1 on failure + */ +int log_async_init(int buffer_size); + +/** + * Shut down the async logging system. + * Flushes remaining entries and terminates the writer thread. + */ +void log_async_shutdown(void); + +/** + * Queue a log entry for async write. + * If the buffer is full, this will block briefly or fall back to sync write. + * + * @param[in] fd File descriptor to write to (-1 for syslog only) + * @param[in] syslog_priority Syslog priority (0 to skip syslog) + * @param[in] message Pre-formatted log message + * @param[in] len Length of message + * @return 0 on success (queued), 1 if sync fallback was used, -1 on error + */ +int log_async_write(int fd, int syslog_priority, const char *message, int len); + +/** + * Flush all pending log entries. + * Blocks until the writer thread has processed all queued entries. + * Use this before shutdown or for critical log messages. + */ +void log_async_flush(void); + +/** + * Check if async logging is available and enabled. + * @return 1 if async logging is active, 0 otherwise + */ +int log_async_available(void); + +/** + * Get async logging statistics. + * @param[out] queued Current entries in queue + * @param[out] written Total entries written since init + * @param[out] dropped Total entries dropped due to full buffer + */ +void log_async_stats(unsigned long *queued, unsigned long *written, + unsigned long *dropped); + +#else /* !HAVE_PTHREAD */ + +/* Stub implementations when pthreads is not available */ +#define log_async_init(s) (0) +#define log_async_shutdown() do {} while (0) +#define log_async_write(fd, prio, msg, len) (-1) +#define log_async_flush() do {} while (0) +#define log_async_available() (0) +#define log_async_stats(q, w, d) do { if (q) *(q) = 0; if (w) *(w) = 0; if (d) *(d) = 0; } while (0) + +#endif /* HAVE_PTHREAD */ + +#endif /* INCLUDED_ircd_log_async_h */ diff --git a/include/ircd_osdep.h b/include/ircd_osdep.h index bdc54fa2..abb77019 100644 --- a/include/ircd_osdep.h +++ b/include/ircd_osdep.h @@ -51,6 +51,7 @@ extern int os_set_fdlimit(unsigned int max_descriptors); extern int os_set_listen(int fd, int backlog); extern int os_set_nonblocking(int fd); extern int os_set_reuseaddr(int fd); +extern int os_set_tcp_nodelay(int fd); extern int os_set_sockbufs(int fd, unsigned int ssize, unsigned int rsize); extern int os_set_tos(int fd,int tos,int family); extern int os_socketpair(int sv[2]); diff --git a/include/listener.h b/include/listener.h index 2b831bed..d81ca12b 100644 --- a/include/listener.h +++ b/include/listener.h @@ -54,6 +54,8 @@ enum ListenerFlag { LISTEN_IPV6, /** Port is SSL enabled. */ LISTEN_SSL, + /** Port accepts WebSocket connections. */ + LISTEN_WEBSOCKET, /** Sentinel for counting listener flags. */ LISTEN_LAST_FLAG }; @@ -79,6 +81,7 @@ struct Listener { #define listener_server(LISTENER) FlagHas(&(LISTENER)->flags, LISTEN_SERVER) #define listener_active(LISTENER) FlagHas(&(LISTENER)->flags, LISTEN_ACTIVE) #define listener_ssl(LISTENER) FlagHas(&(LISTENER)->flags, LISTEN_SSL) +#define listener_websocket(LISTENER) FlagHas(&(LISTENER)->flags, LISTEN_WEBSOCKET) extern void add_listener(int port, const char* vaddr_ip, const char* mask, diff --git a/include/mark.h b/include/mark.h index a05a9c49..68209402 100644 --- a/include/mark.h +++ b/include/mark.h @@ -29,6 +29,7 @@ #define MARK_GEOIP "GEOIP" /**< GEOIP mark. */ #define MARK_CVERSION "CVERSION" /**< Client Version mark */ #define MARK_SSLCLIFP "SSLCLIFP" /**< SSL client certificate fingerprint */ +#define MARK_SSLCLIEXP "SSLCLIEXP" /**< SSL client certificate expiration timestamp */ #define MARK_DNSBL_DATA "DNSBL_DATA" /**< Alias to MARK */ #define MARK_MARK "MARK" /**< List of single word tags */ #define MARK_KILL "KILL" /**< KILL block exemption mark */ diff --git a/include/metadata.h b/include/metadata.h new file mode 100644 index 00000000..3be4a35c --- /dev/null +++ b/include/metadata.h @@ -0,0 +1,398 @@ +/* + * IRC - Internet Relay Chat, include/metadata.h + * Copyright (C) 2024 Nefarious Development Team + * + * 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 2, 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. + */ +/** @file + * @brief Metadata storage declarations (IRCv3 draft/metadata-2). + * + * Implements IRCv3 draft/metadata-2 extension for user/channel key-value storage. + * + * Specification: https://ircv3.net/specs/extensions/metadata + * Capability: draft/metadata-2 + */ +#ifndef INCLUDED_metadata_h +#define INCLUDED_metadata_h + +struct Client; +struct Channel; + +/** Maximum length of a metadata key name */ +#define METADATA_KEY_LEN 64 + +/** Maximum length of a metadata value */ +#define METADATA_VALUE_LEN 1024 + +/** Maximum number of metadata entries per target */ +#define METADATA_MAX_KEYS 20 + +/** Maximum number of subscriptions per client */ +#define METADATA_MAX_SUBS 50 + +/** Visibility levels for metadata */ +#define METADATA_VIS_PUBLIC 0 /* Anyone can see */ +#define METADATA_VIS_PRIVATE 1 /* Only owner can see */ +#define METADATA_VIS_ERROR 2 /* Error response (no such target) */ + +/** Metadata entry structure */ +struct MetadataEntry { + char key[METADATA_KEY_LEN]; /**< Key name */ + char *value; /**< Value (dynamically allocated) */ + int visibility; /**< Visibility level */ + struct MetadataEntry *next; /**< Next entry in list */ +}; + +/** Metadata subscription for a client */ +struct MetadataSub { + char key[METADATA_KEY_LEN]; /**< Key being subscribed to */ + struct MetadataSub *next; /**< Next subscription in list */ +}; + +/** Initialize the metadata subsystem */ +extern void metadata_init(void); + +/** Shutdown the metadata subsystem */ +extern void metadata_shutdown(void); + +/** Initialize LMDB for metadata persistence. + * @param[in] dbpath Path to the database directory. + * @return 0 on success, -1 on error. + */ +extern int metadata_lmdb_init(const char *dbpath); + +/** Shutdown LMDB metadata storage. */ +extern void metadata_lmdb_shutdown(void); + +/** Check if LMDB metadata storage is available. + * @return 1 if available, 0 if not. + */ +extern int metadata_lmdb_is_available(void); + +/** Get account metadata from LMDB. + * @param[in] account Account name. + * @param[in] key Metadata key. + * @param[out] value Buffer for value (at least METADATA_VALUE_LEN). + * @return 0 on success, 1 if not found, -1 on error. + */ +extern int metadata_account_get(const char *account, const char *key, char *value); + +/** Set account metadata in LMDB. + * @param[in] account Account name. + * @param[in] key Metadata key. + * @param[in] value Value to set (NULL to delete). + * @return 0 on success, -1 on error. + */ +extern int metadata_account_set(const char *account, const char *key, const char *value); + +/** Set account metadata in LMDB without compression (raw passthrough). + * Used for compression passthrough when data is already compressed. + * @param[in] account Account name. + * @param[in] key Metadata key. + * @param[in] raw_value Raw (possibly compressed) data. + * @param[in] raw_len Length of raw data. + * @return 0 on success, -1 on error. + */ +extern int metadata_account_set_raw(const char *account, const char *key, + const unsigned char *raw_value, size_t raw_len); + +/** List all metadata for an account from LMDB. + * @param[in] account Account name. + * @return Head of metadata list (caller must free). + */ +extern struct MetadataEntry *metadata_account_list(const char *account); + +/** Clear all metadata for an account in LMDB. + * @param[in] account Account name. + * @return 0 on success, -1 on error. + */ +extern int metadata_account_clear(const char *account); + +/** Purge expired metadata entries from LMDB. + * Called periodically to enforce METADATA_CACHE_TTL. + * @return Number of entries purged, or -1 on error. + */ +extern int metadata_account_purge_expired(void); + +/** Load metadata from LMDB for a logged-in user. + * Called when a user logs into an account. + * @param[in] cptr Client that just logged in. + * @param[in] account Account name. + */ +extern void metadata_load_account(struct Client *cptr, const char *account); + +/** Validate a metadata key name. + * @param[in] key Key name to validate. + * @return 1 if valid, 0 if invalid. + */ +extern int metadata_valid_key(const char *key); + +/** Get metadata for a client. + * @param[in] cptr Client to get metadata from. + * @param[in] key Key name. + * @return Metadata entry or NULL if not found. + */ +extern struct MetadataEntry *metadata_get_client(struct Client *cptr, const char *key); + +/** Set metadata for a client. + * @param[in] cptr Client to set metadata on. + * @param[in] key Key name. + * @param[in] value Value to set (NULL to delete). + * @param[in] visibility Visibility level (METADATA_VIS_PUBLIC or METADATA_VIS_PRIVATE). + * @return 0 on success, -1 on error. + */ +extern int metadata_set_client(struct Client *cptr, const char *key, const char *value, int visibility); + +/** List all metadata for a client. + * @param[in] cptr Client to list metadata for. + * @return Head of metadata list (read-only). + */ +extern struct MetadataEntry *metadata_list_client(struct Client *cptr); + +/** Clear all metadata for a client. + * @param[in] cptr Client to clear. + */ +extern void metadata_clear_client(struct Client *cptr); + +/** Get metadata for a channel. + * @param[in] chptr Channel to get metadata from. + * @param[in] key Key name. + * @return Metadata entry or NULL if not found. + */ +extern struct MetadataEntry *metadata_get_channel(struct Channel *chptr, const char *key); + +/** Set metadata for a channel. + * @param[in] chptr Channel to set metadata on. + * @param[in] key Key name. + * @param[in] value Value to set (NULL to delete). + * @param[in] visibility Visibility level (METADATA_VIS_PUBLIC or METADATA_VIS_PRIVATE). + * @return 0 on success, -1 on error. + */ +extern int metadata_set_channel(struct Channel *chptr, const char *key, const char *value, int visibility); + +/** List all metadata for a channel. + * @param[in] chptr Channel to list metadata for. + * @return Head of metadata list (read-only). + */ +extern struct MetadataEntry *metadata_list_channel(struct Channel *chptr); + +/** Clear all metadata for a channel. + * @param[in] chptr Channel to clear. + */ +extern void metadata_clear_channel(struct Channel *chptr); + +/** Count metadata entries for a client. + * @param[in] cptr Client to count. + * @return Number of metadata entries. + */ +extern int metadata_count_client(struct Client *cptr); + +/** Count metadata entries for a channel. + * @param[in] chptr Channel to count. + * @return Number of metadata entries. + */ +extern int metadata_count_channel(struct Channel *chptr); + +/** Free a metadata entry. + * @param[in] entry Entry to free. + */ +extern void metadata_free_entry(struct MetadataEntry *entry); + +/** Free all metadata for a client (called on disconnect). + * @param[in] cptr Client being freed. + */ +extern void metadata_free_client(struct Client *cptr); + +/** Free all metadata for a channel (called on channel destruction). + * @param[in] chptr Channel being freed. + */ +extern void metadata_free_channel(struct Channel *chptr); + +/* Subscription functions */ + +/** Add a subscription for a client. + * @param[in] cptr Client subscribing. + * @param[in] key Key to subscribe to. + * @return 0 on success, -1 if limit reached. + */ +extern int metadata_sub_add(struct Client *cptr, const char *key); + +/** Remove a subscription for a client. + * @param[in] cptr Client unsubscribing. + * @param[in] key Key to unsubscribe from. + * @return 0 on success, -1 if not subscribed. + */ +extern int metadata_sub_del(struct Client *cptr, const char *key); + +/** Check if a client is subscribed to a key. + * @param[in] cptr Client to check. + * @param[in] key Key to check. + * @return 1 if subscribed, 0 if not. + */ +extern int metadata_sub_check(struct Client *cptr, const char *key); + +/** List subscriptions for a client. + * @param[in] cptr Client to list. + * @return Head of subscription list. + */ +extern struct MetadataSub *metadata_sub_list(struct Client *cptr); + +/** Count subscriptions for a client. + * @param[in] cptr Client to count. + * @return Number of subscriptions. + */ +extern int metadata_sub_count(struct Client *cptr); + +/** Free all subscriptions for a client. + * @param[in] cptr Client being freed. + */ +extern void metadata_sub_free(struct Client *cptr); + +/* ========== Cache-Aware Metadata Operations ========== */ + +/** Get metadata for a client with cache-through behavior. + * Checks in-memory first, then LMDB cache for logged-in users. + * @param[in] cptr Client to get metadata from. + * @param[in] key Key name. + * @return Metadata entry or NULL if not found. + */ +extern struct MetadataEntry *metadata_get_client_cached(struct Client *cptr, const char *key); + +/* ========== X3 Availability Tracking ========== */ + +/** Check if X3 services are available. + * @return 1 if X3 is available, 0 if not. + */ +extern int metadata_x3_is_available(void); + +/** Signal that X3 has sent a message (heartbeat). + * Called when X3 sends any P10 message to update availability status. + */ +extern void metadata_x3_heartbeat(void); + +/** Check X3 availability status based on timeout. + * Called periodically to detect X3 outages. + */ +extern void metadata_x3_check(void); + +/** Handle X3 reconnection - replay queued writes. + * Called when X3 reconnects after an outage. + */ +extern void metadata_x3_reconnected(void); + +/** Check if metadata writes can be sent to X3. + * @return 1 if writes can be sent, 0 if they should be queued. + */ +extern int metadata_can_write_x3(void); + +/* ========== Write Queue for X3 Unavailability ========== */ + +/** Queue a metadata write for later replay. + * Called when X3 is unavailable to queue writes for later. + * @param[in] account Account name. + * @param[in] key Metadata key. + * @param[in] value Value to set. + * @param[in] visibility Visibility level. + * @return 0 on success, -1 if queue is full. + */ +extern int metadata_queue_write(const char *account, const char *key, + const char *value, int visibility); + +/** Replay all queued metadata writes to X3. + * Called when X3 becomes available again. + */ +extern void metadata_replay_queue(void); + +/** Clear the write queue without replaying. + */ +extern void metadata_clear_queue(void); + +/** Get the number of queued writes. + * @return Number of entries in the write queue. + */ +extern int metadata_queue_count(void); + +/* ========== Netburst Metadata ========== */ + +/** Burst all metadata for a client to a server. + * @param[in] sptr Client whose metadata to send. + * @param[in] cptr Server to send metadata to. + */ +extern void metadata_burst_client(struct Client *sptr, struct Client *cptr); + +/** Burst all metadata for a channel to a server. + * @param[in] chptr Channel whose metadata to send. + * @param[in] cptr Server to send metadata to. + */ +extern void metadata_burst_channel(struct Channel *chptr, struct Client *cptr); + +/* ========== MDQ Request Tracking ========== */ + +/** Maximum pending MDQ requests */ +#define METADATA_MAX_PENDING 100 + +/** Timeout for pending MDQ requests (seconds) */ +#define METADATA_REQUEST_TIMEOUT 30 + +/** Pending MDQ request structure */ +struct MetadataRequest { + struct Client *client; /**< Client waiting for response */ + char target[CHANNELLEN + 1]; /**< Target account/channel (channels can be 200 chars) */ + char key[METADATA_KEY_LEN]; /**< Key requested (or "*") */ + time_t timestamp; /**< When request was made */ + struct MetadataRequest *next; /**< Next in list */ +}; + +/** Send an MDQ query to services for a target. + * @param[in] sptr Client requesting metadata. + * @param[in] target Target account or channel name. + * @param[in] key Key to query (or "*" for all). + * @return 0 on success, -1 on error. + */ +extern int metadata_send_query(struct Client *sptr, const char *target, const char *key); + +/** Check if there are pending MDQ requests for a target/key. + * Called when MD response is received to forward to waiting clients. + * @param[in] target Target that metadata was received for. + * @param[in] key Key that was received. + * @param[in] value Value received. + * @param[in] visibility Visibility level. + */ +extern void metadata_handle_response(const char *target, const char *key, + const char *value, int visibility); + +/** Clean up expired MDQ requests. + * Called periodically from the main loop. + */ +extern void metadata_expire_requests(void); + +/** Clean up MDQ requests for a disconnecting client. + * @param[in] cptr Client that is disconnecting. + */ +extern void metadata_cleanup_client_requests(struct Client *cptr); + +/** Initialize MDQ request tracking. */ +extern void metadata_request_init(void); + +struct StatDesc; + +/** Report METADATA statistics for /STATS. + * @param[in] to Client requesting stats. + * @param[in] sd Stats descriptor. + * @param[in] param Extra parameter (unused). + */ +extern void metadata_report_stats(struct Client *to, const struct StatDesc *sd, char *param); + +#endif /* INCLUDED_metadata_h */ diff --git a/include/ml_storage.h b/include/ml_storage.h new file mode 100644 index 00000000..04f1aadf --- /dev/null +++ b/include/ml_storage.h @@ -0,0 +1,123 @@ +/* + * IRC - Internet Relay Chat, include/ml_storage.h + * Copyright (C) 2026 Nefarious Development Team + * + * 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 2, 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. + */ +/** @file + * @brief Multiline message storage for &ml- virtual channel retrieval. + * + * Provides ephemeral in-memory storage for truncated multiline messages. + * Legacy clients can retrieve full content via /join &ml-. + * Storage is local-only (not synced across servers) and expires after TTL. + */ +#ifndef INCLUDED_ml_storage_h +#define INCLUDED_ml_storage_h + +#include "ircd_defs.h" +#include + +struct Client; +struct SLink; + +/** Maximum length of stored msgid (base msgid without sequence suffix) */ +#define ML_STORAGE_MSGID_LEN 64 + +/** Hash table size for storage entries */ +#define ML_STORAGE_HASHSIZE 1024 + +/** Default maximum entries (memory bound) */ +#define ML_STORAGE_DEFAULT_MAX 10000 + +/** Stored multiline message entry */ +struct ml_stored_msg { + char msgid[ML_STORAGE_MSGID_LEN]; /**< Base msgid (without sequence suffix) */ + char sender[NICKLEN + 1]; /**< Sender nick */ + char target[CHANNELLEN + 1]; /**< Target channel or nick */ + char *lines; /**< Newline-separated content */ + int line_count; /**< Number of lines */ + time_t stored; /**< When stored */ + time_t expires; /**< When to expire */ + struct ml_stored_msg *next; /**< Hash chain */ +}; + +/** Initialize the multiline storage system. + * Must be called at server startup. + */ +extern void ml_storage_init(void); + +/** Shutdown the multiline storage system. + * Frees all stored entries. + */ +extern void ml_storage_shutdown(void); + +/** Store multiline content for later retrieval. + * @param[in] msgid Base message ID (without sequence suffix). + * @param[in] sender Sender's nick. + * @param[in] target Target channel or nick. + * @param[in] lines Linked list of message lines (SLink with value.cp). + * @param[in] count Number of lines in the list. + * @return 0 on success, -1 on error (e.g., storage full). + */ +extern int ml_storage_store(const char *msgid, const char *sender, + const char *target, struct SLink *lines, int count); + +/** Retrieve stored content by message ID. + * @param[in] msgid Base message ID to look up. + * @return Pointer to stored message, or NULL if not found/expired. + * @note Returned pointer is internal; do not free. Content is valid + * until ml_storage_expire() runs or ml_storage_shutdown() is called. + */ +extern struct ml_stored_msg *ml_storage_get(const char *msgid); + +/** Remove a specific entry from storage. + * @param[in] msgid Base message ID to remove. + * @return 0 if removed, 1 if not found. + */ +extern int ml_storage_remove(const char *msgid); + +/** Expire old entries from storage. + * Should be called periodically (e.g., every 5 minutes). + * @return Number of entries expired. + */ +extern int ml_storage_expire(void); + +/** Get storage statistics. + * @param[out] count Current number of stored entries. + * @param[out] max Maximum allowed entries. + * @param[out] bytes Total bytes used by stored content (approximate). + */ +extern void ml_storage_stats(int *count, int *max, size_t *bytes); + +/** Deliver stored content to a client via NOTICEs. + * Called when client tries to /join &ml-. + * @param[in] sptr Client requesting the content. + * @param[in] msgid Message ID to retrieve (without "&ml-" prefix). + * @return 0 on success (content delivered or not found notice sent). + */ +extern int ml_storage_deliver(struct Client *sptr, const char *msgid); + +/** Check if a channel name is a virtual &ml- channel. + * @param[in] name Channel name to check. + * @return 1 if it's an &ml- channel, 0 otherwise. + */ +extern int ml_storage_is_virtual_channel(const char *name); + +/** Memory usage reporting for /STATS. + * @param[in] cptr Client requesting stats. + */ +extern void ml_storage_meminfo(struct Client *cptr); + +#endif /* INCLUDED_ml_storage_h */ diff --git a/include/msg.h b/include/msg.h index 1c3b50e9..44660fc1 100644 --- a/include/msg.h +++ b/include/msg.h @@ -108,6 +108,10 @@ struct Client; #define TOK_INFO "F" #define CMD_INFO MSG_INFO, TOK_INFO +#define MSG_ISUPPORT "ISUPPORT" /* ISUPPORT (IRCv3 draft/extended-isupport) */ +#define TOK_ISUPPORT "IS" +#define CMD_ISUPPORT MSG_ISUPPORT, TOK_ISUPPORT + #define MSG_LINKS "LINKS" /* LINK */ #define TOK_LINKS "LI" #define CMD_LINKS MSG_LINKS, TOK_LINKS @@ -336,6 +340,10 @@ struct Client; #define TOK_ACCOUNT "AC" #define CMD_ACCOUNT MSG_ACCOUNT, TOK_ACCOUNT +#define MSG_CHGHOST "CHGHOST" /* CHGH */ +#define TOK_CHGHOST "CHGHOST" +#define CMD_CHGHOST MSG_CHGHOST, TOK_CHGHOST + #define MSG_ASLL "ASLL" /* ASLL */ #define TOK_ASLL "LL" #define CMD_ASLL MSG_ASLL, TOK_ASLL @@ -496,6 +504,62 @@ struct Client; #define TOK_TEMPSHUN "TS" #define CMD_TEMPSHUN MSG_TEMPSHUN, TOK_TEMPSHUN +#define MSG_BATCH_CMD "BATCH" +#define TOK_BATCH_CMD "BT" +#define CMD_BATCH_CMD MSG_BATCH_CMD, TOK_BATCH_CMD + +#define MSG_SETNAME "SETNAME" +#define TOK_SETNAME "SE" +#define CMD_SETNAME MSG_SETNAME, TOK_SETNAME + +#define MSG_TAGMSG "TAGMSG" +#define TOK_TAGMSG "TM" +#define CMD_TAGMSG MSG_TAGMSG, TOK_TAGMSG + +#define MSG_CHATHISTORY "CHATHISTORY" +#define TOK_CHATHISTORY "CH" +#define CMD_CHATHISTORY MSG_CHATHISTORY, TOK_CHATHISTORY + +#define MSG_REDACT "REDACT" +#define TOK_REDACT "RD" +#define CMD_REDACT MSG_REDACT, TOK_REDACT + +#define MSG_REGISTER "REGISTER" +#define TOK_REGISTER "RG" +#define CMD_REGISTER MSG_REGISTER, TOK_REGISTER + +#define MSG_VERIFY "VERIFY" +#define TOK_VERIFY "VF" +#define CMD_VERIFY MSG_VERIFY, TOK_VERIFY + +#define MSG_REGREPLY "REGREPLY" +#define TOK_REGREPLY "RR" +#define CMD_REGREPLY MSG_REGREPLY, TOK_REGREPLY + +#define MSG_MARKREAD "MARKREAD" +#define TOK_MARKREAD "MR" +#define CMD_MARKREAD MSG_MARKREAD, TOK_MARKREAD + +#define MSG_RENAME "RENAME" +#define TOK_RENAME "RN" +#define CMD_RENAME MSG_RENAME, TOK_RENAME + +#define MSG_METADATA "METADATA" +#define TOK_METADATA "MD" +#define CMD_METADATA MSG_METADATA, TOK_METADATA + +#define MSG_METADATAQUERY "METADATAQUERY" +#define TOK_METADATAQUERY "MDQ" +#define CMD_METADATAQUERY MSG_METADATAQUERY, TOK_METADATAQUERY + +#define MSG_WEBPUSH "WEBPUSH" +#define TOK_WEBPUSH "WP" +#define CMD_WEBPUSH MSG_WEBPUSH, TOK_WEBPUSH + +#define MSG_MULTILINE "MULTILINE" +#define TOK_MULTILINE "ML" +#define CMD_MULTILINE MSG_MULTILINE, TOK_MULTILINE + /* * Constants */ diff --git a/include/numeric.h b/include/numeric.h index 03065433..3299bb96 100644 --- a/include/numeric.h +++ b/include/numeric.h @@ -568,6 +568,16 @@ extern const struct Numeric* get_error_numeric(int err); #define RPL_HELPTXT 705 /* RatBox */ #define RPL_ENDOFHELP 706 /* RatBox */ +/* IRCv3 - Metadata extension (760-774) */ +#define RPL_WHOISKEYVALUE 760 /* IRCv3 - Metadata (WHOIS integration) */ +#define RPL_KEYVALUE 761 /* IRCv3 - Metadata key-value pair */ +#define RPL_METADATAEND 762 /* IRCv3 - End of metadata list */ +#define RPL_KEYNOTSET 766 /* IRCv3 - Metadata key not set */ +#define RPL_METADATASUBOK 770 /* IRCv3 - Metadata subscription OK */ +#define RPL_METADATAUNSUBOK 771 /* IRCv3 - Metadata unsubscription OK */ +#define RPL_METADATASUBS 772 /* IRCv3 - Metadata subscribed keys */ +#define RPL_METADATASYNCLATER 774 /* IRCv3 - Metadata sync deferred */ + #define RPL_LOGGEDIN 900 /* IRCv3 - SASL extension */ #define RPL_LOGGEDOUT 901 /* IRCv3 - SASL extension */ diff --git a/include/s_conf.h b/include/s_conf.h index 28a9aef0..8af0d554 100644 --- a/include/s_conf.h +++ b/include/s_conf.h @@ -276,7 +276,9 @@ extern void conf_parse_userhost(struct ConfItem *aconf, char *host); extern struct ConfItem *conf_debug_iline(const char *client); extern void free_mapping(struct s_map *smap); extern struct WebIRCConf* find_webirc_conf(struct Client *cptr, char *passwd, int* status); +extern struct WebIRCConf* find_webirc_conf_by_host(struct Client *cptr); extern struct SHostConf* find_shost_conf(struct Client *cptr, char *host, char *passwd, int *status); +extern struct SHostConf* find_shost_conf_by_host(struct Client *cptr, const char *host); extern int get_except_flags(struct Client *cptr); extern int find_except_conf(struct Client *cptr, int flags); extern int find_except_conf_by_ip(const struct irc_in_addr *addr, int flags); diff --git a/include/s_user.h b/include/s_user.h index 144c92a3..a3f16fd2 100644 --- a/include/s_user.h +++ b/include/s_user.h @@ -106,6 +106,7 @@ extern void add_target(struct Client *sptr, void *target); extern unsigned int umode_make_snomask(unsigned int oldmask, char *arg, int what); extern int send_supported(struct Client *cptr); +extern int send_supported_batched(struct Client *cptr); extern int do_nick_name(char* nick); extern void user_setcloaked(struct Client *cptr); diff --git a/include/send.h b/include/send.h index 1f3fe053..5b50625c 100644 --- a/include/send.h +++ b/include/send.h @@ -39,6 +39,23 @@ extern void sendcmdto_one(struct Client *from, const char *cmd, const char *tok, struct Client *to, const char *pattern, ...); +/* Same as above, but include message tags (label, time, account) */ +extern void sendcmdto_one_tags(struct Client *from, const char *cmd, + const char *tok, struct Client *to, + const char *pattern, ...); + +/* Same as above, but also return the generated msgid and timestamp */ +extern void sendcmdto_one_tags_msgid(struct Client *from, const char *cmd, + const char *tok, struct Client *to, + char *msgid_out, size_t msgid_out_len, + char *time_out, size_t time_out_len, + const char *pattern, ...); + +/* Send TAGMSG with client-only tags to a single client */ +extern void sendcmdto_one_client_tags(struct Client *from, const char *cmd, + struct Client *to, const char *client_tags, + const char *pattern, ...); + /* Same as above, except it puts the message on the priority queue */ extern void sendcmdto_prio_one(struct Client *from, const char *cmd, const char *tok, struct Client *to, @@ -90,6 +107,12 @@ extern void sendcmdto_channel_capab_butserv_butone(struct Client *from, int skipcap, const char *pattern, ...); +/* Send TAGMSG with client-only tags to channel members with message-tags capability */ +extern void sendcmdto_channel_client_tags(struct Client *from, const char *cmd, + struct Channel *to, struct Client *one, + unsigned int skip, const char *client_tags, + const char *pattern, ...); + /* Send command to all servers interested in a channel */ extern void sendcmdto_channel_servers_butone(struct Client *from, const char *cmd, @@ -112,6 +135,7 @@ extern void sendcmdto_channel_butone(struct Client *from, const char *cmd, chanops and halfops) */ #define SKIP_NONHOPS 0x10 /**< skip users that aren't halfopped (includes chanops) */ +#define SKIP_CHGHOST 0x20 /**< skip users that have chghost capability */ /* Send command to all users having a particular flag set */ extern void sendwallto_group_butone(struct Client *from, int type, @@ -157,4 +181,43 @@ extern void sendto_mode_butone(struct Client *one, struct Client *from, const ch extern void vsendto_mode_butone(struct Client *one, struct Client *from, const char *mode, const char *pattern, va_list vl); +/* Start a batch for a client (labeled-response integration) */ +extern void send_batch_start(struct Client *to, const char *type); + +/* End a batch for a client */ +extern void send_batch_end(struct Client *to); + +/* Check if a client has an active batch */ +extern int has_active_batch(struct Client *cptr); + +/* S2S batch functions for netjoin/netsplit coordination */ +extern void send_s2s_batch_start(struct Client *sptr, const char *type, + const char *server1, const char *server2); +extern void send_s2s_batch_end(struct Client *sptr, const char *batch_id); + +/* Netjoin/netsplit batch functions for automatic batching */ +extern void send_netjoin_batch_start(struct Client *server, struct Client *uplink); +extern void send_netjoin_batch_end(struct Client *server); +extern void send_netsplit_batch_start(struct Client *server, struct Client *uplink, + char *batch_id_out, size_t batch_id_len); +extern void send_netsplit_batch_end(const char *batch_id); + +/* Active network batch tracking for @batch tag inclusion in QUIT/JOIN messages */ +extern void set_active_network_batch(const char *batch_id); +extern const char *get_active_network_batch(void); + +/* Generate unique message ID for IRCv3 message-ids */ +extern char *generate_msgid(char *buf, size_t buflen); + +/* IRCv3 standard-replies (FAIL/WARN/NOTE) */ +extern void send_fail(struct Client *to, const char *command, const char *code, + const char *context, const char *description); +extern void send_warn(struct Client *to, const char *command, const char *code, + const char *context, const char *description); +extern void send_warn_with_label(struct Client *to, const char *command, const char *code, + const char *context, const char *description, + const char *label); +extern void send_note(struct Client *to, const char *command, const char *code, + const char *context, const char *description); + #endif /* INCLUDED_send_h */ diff --git a/include/ssl.h b/include/ssl.h index 5b4491ca..d5292842 100644 --- a/include/ssl.h +++ b/include/ssl.h @@ -33,6 +33,7 @@ #ifdef USE_SSL #include +#include struct Socket; struct Listener; @@ -55,6 +56,7 @@ extern int ssl_send(struct Client *cptr, const char *buf, unsigned int len); extern char *ssl_get_cipher(SSL *ssl); extern char* ssl_get_fingerprint(SSL *ssl); +extern time_t ssl_get_cert_expiry(SSL *ssl); extern const char* ssl_get_verify_result(SSL *ssl); extern char *ssl_error_str(int err, int my_errno); diff --git a/include/struct.h b/include/struct.h index c47b25bb..1e29e76b 100644 --- a/include/struct.h +++ b/include/struct.h @@ -37,6 +37,7 @@ struct Client; struct User; struct Membership; struct SLink; +struct AccountConn; /** Describes a server on the network. */ struct Server { @@ -61,6 +62,7 @@ struct Server { char *last_error_msg; /**< Allocated memory with last message receive with an ERROR */ char by[NICKLEN + 1]; /**< Numnick of client who requested the link */ + char batch_id[32]; /**< IRCv3 batch ID for netjoin/netsplit */ }; /** Describes a user on the network. */ @@ -70,6 +72,7 @@ struct User { struct SLink* invited; /**< chain of invite pointer blocks */ struct SLink* watch; /**< chain of watch pointer blocks */ struct Ban* silence; /**< chain of silence pointer blocks */ + struct AccountConn* account_conn; /**< link to presence aggregation registry */ char* away; /**< pointer to away message */ char* opername; /**< pointer to /OPER user name */ time_t last; /**< last time user sent a message */ diff --git a/include/thread_pool.h b/include/thread_pool.h new file mode 100644 index 00000000..1764b808 --- /dev/null +++ b/include/thread_pool.h @@ -0,0 +1,136 @@ +/* + * IRC - Internet Relay Chat, include/thread_pool.h + * Copyright (C) 2025 AfterNET Development Team + * + * 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 2, 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. + */ +/** @file + * @brief Thread pool for CPU-bound operations. + * + * This module provides a lightweight thread pool for offloading CPU-bound + * operations (like bcrypt/PBKDF2 password hashing) from the main event loop. + * Results are delivered back to the main thread via callbacks. + * + * Usage: + * 1. Call thread_pool_init() at startup + * 2. Submit work with thread_pool_submit() + * 3. Call thread_pool_poll() from event loop to process completions + * 4. Call thread_pool_shutdown() at exit + */ +#ifndef INCLUDED_thread_pool_h +#define INCLUDED_thread_pool_h + +#ifndef INCLUDED_config_h +#include "config.h" +#endif + +/** Default number of worker threads */ +#define THREAD_POOL_SIZE_DEFAULT 4 + +/** Maximum pending tasks before submit() blocks or fails */ +#define THREAD_POOL_MAX_PENDING 256 + +/** + * Callback invoked in main thread when async work completes. + * @param[in] result Return value from work function + * @param[in] ctx User-provided context pointer + */ +typedef void (*thread_pool_callback)(void *result, void *ctx); + +/** + * Work function to be executed in worker thread. + * @param[in] arg Argument passed to thread_pool_submit() + * @return Result pointer passed to callback + */ +typedef void *(*thread_pool_work_func)(void *arg); + +#ifdef HAVE_PTHREAD + +/** + * Initialize the thread pool. + * Creates worker threads and sets up the signal pipe for main thread wakeup. + * @param[in] num_threads Number of worker threads (0 = use default) + * @return 0 on success, -1 on failure + */ +int thread_pool_init(int num_threads); + +/** + * Shut down the thread pool. + * Waits for pending tasks to complete, then terminates worker threads. + * Any tasks submitted after this returns will fail. + */ +void thread_pool_shutdown(void); + +/** + * Submit work to the thread pool. + * The work function runs in a worker thread. When it returns, the callback + * is invoked in the main thread (via thread_pool_poll()) with the result. + * + * @param[in] work Function to execute in worker thread + * @param[in] arg Argument passed to work function + * @param[in] callback Function called in main thread with result (may be NULL) + * @param[in] ctx Context pointer passed to callback + * @return 0 on success, -1 on failure (pool not initialized or queue full) + */ +int thread_pool_submit(thread_pool_work_func work, void *arg, + thread_pool_callback callback, void *ctx); + +/** + * Process completed tasks. + * Call this from the main event loop (after epoll_wait/kqueue/poll returns) + * to invoke callbacks for completed async operations. + * + * This function is non-blocking - it only processes tasks that have already + * completed. + */ +void thread_pool_poll(void); + +/** + * Get the signal pipe file descriptor. + * This FD becomes readable when async tasks complete. Register it with + * the event engine to wake up and call thread_pool_poll(). + * @return Read end of signal pipe, or -1 if not initialized + */ +int thread_pool_get_signal_fd(void); + +/** + * Check if thread pool is initialized and running. + * @return 1 if running, 0 otherwise + */ +int thread_pool_is_running(void); + +/** + * Get thread pool statistics. + * @param[out] pending Number of tasks waiting to be processed + * @param[out] completed Total tasks completed since init + * @param[out] active Number of tasks currently executing in workers + */ +void thread_pool_stats(unsigned int *pending, unsigned long *completed, + unsigned int *active); + +#else /* !HAVE_PTHREAD */ + +/* Stub implementations when pthreads is not available */ +#define thread_pool_init(n) (0) +#define thread_pool_shutdown() do {} while (0) +#define thread_pool_submit(w, a, cb, ctx) (-1) +#define thread_pool_poll() do {} while (0) +#define thread_pool_get_signal_fd() (-1) +#define thread_pool_is_running() (0) +#define thread_pool_stats(p, c, a) do { if (p) *(p) = 0; if (c) *(c) = 0; if (a) *(a) = 0; } while (0) + +#endif /* HAVE_PTHREAD */ + +#endif /* INCLUDED_thread_pool_h */ diff --git a/include/websocket.h b/include/websocket.h new file mode 100644 index 00000000..f62dd011 --- /dev/null +++ b/include/websocket.h @@ -0,0 +1,77 @@ +/* + * IRC - Internet Relay Chat, include/websocket.h + * Copyright (C) 2024 Nefarious Development Team + * + * 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 2, 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. + */ +/** @file + * @brief WebSocket protocol support declarations. + */ +#ifndef INCLUDED_websocket_h +#define INCLUDED_websocket_h + +struct Client; + +/* WebSocket opcodes */ +#define WS_OPCODE_CONTINUATION 0x0 +#define WS_OPCODE_TEXT 0x1 +#define WS_OPCODE_BINARY 0x2 +#define WS_OPCODE_CLOSE 0x8 +#define WS_OPCODE_PING 0x9 +#define WS_OPCODE_PONG 0xA + +/** Handle WebSocket handshake for a new connection. + * @param[in] cptr Client attempting to connect. + * @param[in] buffer Raw data received. + * @param[in] length Length of data. + * @return 1 if handshake completed successfully, 0 if more data needed, -1 on error. + */ +extern int websocket_handshake(struct Client *cptr, const char *buffer, int length); + +/** Decode a WebSocket frame and extract the payload. + * @param[in] frame Raw WebSocket frame data. + * @param[in] frame_len Length of frame data. + * @param[out] payload Output buffer for decoded payload. + * @param[in] payload_size Size of payload buffer. + * @param[out] payload_len Length of decoded payload. + * @param[out] opcode The frame opcode. + * @param[out] is_fin Set to 1 if FIN bit is set (final fragment), 0 otherwise. + * @return Number of bytes consumed from frame, 0 if incomplete, -1 on error. + */ +extern int websocket_decode_frame(const unsigned char *frame, int frame_len, + char *payload, int payload_size, + int *payload_len, int *opcode, int *is_fin); + +/** Encode data as a WebSocket frame. + * @param[in] data Data to encode. + * @param[in] data_len Length of data. + * @param[out] frame Output buffer for frame (must be data_len + 10 bytes). + * @param[in] text_mode 1 for text frame, 0 for binary frame. + * @return Length of encoded frame. + */ +extern int websocket_encode_frame(const char *data, int data_len, + unsigned char *frame, int text_mode); + +/** Handle a WebSocket control frame. + * @param[in] cptr Client connection. + * @param[in] opcode Frame opcode. + * @param[in] payload Frame payload. + * @param[in] payload_len Payload length. + * @return 1 to continue, 0 to close connection. + */ +extern int websocket_handle_control(struct Client *cptr, int opcode, + const char *payload, int payload_len); + +#endif /* INCLUDED_websocket_h */ diff --git a/ircd/Makefile.in b/ircd/Makefile.in index 6ac98428..3a411147 100644 --- a/ircd/Makefile.in +++ b/ircd/Makefile.in @@ -78,7 +78,9 @@ CRYPTO_SRC = \ ircd_crypt_plain.c \ ircd_crypt_smd5.c \ ircd_crypt_native.c \ - ircd_crypt_bcrypt.c + ircd_crypt_bcrypt.c \ + ircd_crypt_pbkdf2.c \ + ircd_crypt_async.c UMKPASSWD_SRC = ${CRYPTO_SRC} \ ircd_alloc.c \ @@ -87,6 +89,7 @@ UMKPASSWD_SRC = ${CRYPTO_SRC} \ umkpasswd.c IRCD_SRC = \ + account_conn.c \ IPcheck.c \ channel.c \ class.c \ @@ -97,14 +100,17 @@ IRCD_SRC = \ fileio.c \ gline.c \ hash.c \ + history.c \ ircd.c \ ircd_alloc.c \ ircd_cloaking.c \ + ircd_compress.c \ ircd_crypt.c \ ircd_events.c \ ircd_features.c \ ircd_geoip.c \ ircd_log.c \ + ircd_log_async.c \ ircd_relay.c \ ircd_reply.c \ ircd_res.c \ @@ -121,8 +127,10 @@ IRCD_SRC = \ m_asll.c \ m_authenticate.c \ m_away.c \ + m_batch.c \ m_burst.c \ m_cap.c \ + m_chathistory.c \ m_check.c \ m_clearmode.c \ m_close.c \ @@ -141,6 +149,7 @@ IRCD_SRC = \ m_gline.c \ m_help.c \ m_info.c \ + m_isupport.c \ m_invite.c \ m_ircops.c \ m_isnef.c \ @@ -173,6 +182,13 @@ IRCD_SRC = \ m_protoctl.c \ m_pseudo.c \ m_quit.c \ + m_redact.c \ + m_register.c \ + m_markread.c \ + m_metadata.c \ + m_rename.c \ + m_webpush.c \ + metadata.c \ m_rehash.c \ m_remove.c \ m_reset.c \ @@ -184,7 +200,9 @@ IRCD_SRC = \ m_server.c \ m_set.c \ m_sethost.c \ + m_setname.c \ m_settime.c \ + m_tagmsg.c \ m_shun.c \ m_silence.c \ m_smo.c \ @@ -224,6 +242,7 @@ IRCD_SRC = \ m_xreply.c \ m_zline.c \ match.c \ + ml_storage.c \ memdebug.c \ motd.c \ msgq.c \ @@ -247,6 +266,8 @@ IRCD_SRC = \ send.c \ shun.c \ ssl.c \ + thread_pool.c \ + websocket.c \ uping.c \ userload.c \ watch.c \ @@ -463,6 +484,9 @@ hash.o: hash.c ../config.h ../include/hash.h ../include/client.h \ ../include/ircd_string.h ../include/ircd.h ../include/match.h \ ../include/msg.h ../include/numeric.h ../include/random.h \ ../include/send.h ../include/struct.h ../include/sys.h ../include/watch.h +history.o: history.c ../config.h ../include/history.h ../include/ircd_alloc.h \ + ../include/ircd_log.h ../include/ircd_snprintf.h ../include/ircd_string.h \ + ../include/s_debug.h ircd.o: ircd.c ../config.h ../include/ircd.h ../include/IPcheck.h \ ../include/class.h ../include/client.h ../include/crule.h \ ../include/destruct_event.h ../include/hash.h ../include/ircd_alloc.h \ @@ -487,7 +511,8 @@ ircd_crypt.o: ircd_crypt.c ../config.h ../include/ircd_crypt.h \ ../include/ircd_alloc.h ../include/ircd_features.h ../include/ircd_log.h \ ../include/ircd_string.h ../include/s_debug.h \ ../include/ircd_crypt_native.h ../include/ircd_crypt_plain.h \ - ../include/ircd_crypt_smd5.h ../include/ircd_crypt_bcrypt.h + ../include/ircd_crypt_smd5.h ../include/ircd_crypt_bcrypt.h \ + ../include/ircd_crypt_pbkdf2.h ircd_crypt_native.o: ircd_crypt_native.c ../config.h \ ../include/ircd_crypt.h ../include/ircd_crypt_native.h \ ../include/ircd_log.h ../include/s_debug.h ../include/ircd_alloc.h @@ -500,6 +525,9 @@ ircd_crypt_smd5.o: ircd_crypt_smd5.c ../config.h ../include/ircd_crypt.h \ ircd_crypt_bcrypt.o: ircd_crypt_bcrypt.c ../config.h ../include/ircd_crypt.h \ ../include/ircd_crypt_bcrypt.h ../include/ircd_log.h ../include/s_debug.h \ ../include/ircd_alloc.h +ircd_crypt_pbkdf2.o: ircd_crypt_pbkdf2.c ../config.h ../include/ircd_crypt.h \ + ../include/ircd_crypt_pbkdf2.h ../include/ircd_log.h ../include/s_debug.h \ + ../include/ircd_alloc.h ircd_events.o: ircd_events.c ../config.h ../include/ircd_events.h \ ../include/ircd.h ../include/ircd_alloc.h ../include/ircd_log.h \ ../include/ircd_snprintf.h ../include/s_debug.h @@ -601,6 +629,13 @@ m_away.o: m_away.c ../config.h ../include/client.h ../include/ircd.h \ ../include/ircd_alloc.h ../include/ircd_log.h ../include/ircd_reply.h \ ../include/ircd_string.h ../include/msg.h ../include/numeric.h \ ../include/numnicks.h ../include/s_user.h ../include/send.h +m_batch.o: m_batch.c ../config.h ../include/capab.h ../include/channel.h \ + ../include/client.h ../include/hash.h ../include/ircd.h \ + ../include/ircd_alloc.h ../include/ircd_features.h ../include/ircd_log.h \ + ../include/ircd_reply.h ../include/ircd_snprintf.h ../include/ircd_string.h \ + ../include/list.h ../include/msg.h ../include/numeric.h \ + ../include/numnicks.h ../include/send.h ../include/s_misc.h \ + ../include/s_user.h m_burst.o: m_burst.c ../config.h ../include/channel.h ../include/client.h \ ../include/hash.h ../include/ircd.h ../include/ircd_alloc.h \ ../include/ircd_features.h ../include/ircd_log.h ../include/ircd_reply.h \ @@ -613,6 +648,12 @@ m_cap.o: m_cap.c ../config.h ../include/client.h ../include/ircd.h \ ../include/ircd_snprintf.h ../include/ircd_string.h ../include/msg.h \ ../include/numeric.h ../include/send.h ../include/s_auth.h \ ../include/s_user.h ../include/ircd_features.h +m_chathistory.o: m_chathistory.c ../config.h ../include/capab.h \ + ../include/channel.h ../include/client.h ../include/hash.h \ + ../include/history.h ../include/ircd.h ../include/ircd_alloc.h \ + ../include/ircd_features.h ../include/ircd_log.h ../include/ircd_reply.h \ + ../include/ircd_snprintf.h ../include/ircd_string.h ../include/msg.h \ + ../include/numeric.h ../include/numnicks.h ../include/send.h m_check.o: m_check.c ../include/channel.h \ ../include/class.h ../include/client.h ../include/destruct_event.h \ ../include/hash.h ../include/ircd.h ../include/ircd_alloc.h \ @@ -720,6 +761,9 @@ m_ison.o: m_ison.c ../config.h ../include/client.h ../include/hash.h \ ../include/ircd.h ../include/ircd_log.h ../include/ircd_reply.h \ ../include/ircd_string.h ../include/msgq.h ../include/numeric.h \ ../include/send.h +m_isupport.o: m_isupport.c ../config.h ../include/capab.h \ + ../include/client.h ../include/ircd_reply.h ../include/numeric.h \ + ../include/s_user.h m_join.o: m_join.c ../config.h ../include/channel.h ../include/class.h \ ../include/client.h ../include/gline.h ../include/hash.h \ ../include/ircd.h ../include/ircd_chattr.h ../include/ircd_features.h \ @@ -927,6 +971,10 @@ m_sethost.o: m_sethost.c ../config.h ../include/client.h ../include/hash.h \ ../include/ircd_reply.h ../include/ircd_snprintf.h \ ../include/ircd_string.h ../include/numeric.h ../include/numnicks.h \ ../include/s_conf.h ../include/s_user.h ../include/send.h +m_setname.o: m_setname.c ../config.h ../include/capab.h ../include/client.h \ + ../include/ircd.h ../include/ircd_features.h ../include/ircd_log.h \ + ../include/ircd_reply.h ../include/ircd_string.h ../include/msg.h \ + ../include/numeric.h ../include/numnicks.h ../include/send.h m_settime.o: m_settime.c ../config.h ../include/client.h ../include/hash.h \ ../include/ircd.h ../include/ircd_features.h ../include/ircd_log.h \ ../include/ircd_reply.h ../include/ircd_snprintf.h \ @@ -1018,6 +1066,11 @@ m_tempshun.o: m_tempshun.c ../config.h ../include/client.h ../include/hash.h \ ../include/ircd.h ../include/ircd_features.h ../include/ircd_log.h \ ../include/ircd_reply.h ../include/msg.h ../include/numeric.h \ ../include/numnicks.h ../include/send.h +m_tagmsg.o: m_tagmsg.c ../config.h ../include/capab.h ../include/channel.h \ + ../include/client.h ../include/hash.h ../include/ircd.h \ + ../include/ircd_features.h ../include/ircd_log.h ../include/ircd_reply.h \ + ../include/ircd_string.h ../include/msg.h ../include/numeric.h \ + ../include/numnicks.h ../include/send.h ../include/s_user.h m_time.o: m_time.c ../config.h ../include/client.h ../include/ircd.h \ ../include/ircd_features.h ../include/ircd_log.h ../include/ircd_reply.h \ ../include/ircd_string.h ../include/msg.h ../include/numeric.h \ @@ -1305,12 +1358,18 @@ ssl.o: ssl.c ../config.h ../include/client.h ../include/ircd_alloc.h \ ../include/ircd_snprintf.h ../include/ircd_string.h ../include/listener.h \ ../include/s_bsd.h ../include/s_debug.h ../include/send.h \ ../include/ssl.h +websocket.o: websocket.c ../config.h ../include/websocket.h \ + ../include/client.h ../include/ircd.h ../include/ircd_alloc.h \ + ../include/ircd_log.h ../include/ircd_osdep.h ../include/ircd_snprintf.h \ + ../include/ircd_string.h ../include/listener.h ../include/s_bsd.h \ + ../include/s_debug.h ../include/send.h table_gen.o: table_gen.c ../config.h ../include/ircd_chattr.h umkpasswd.o: umkpasswd.c ../config.h ../include/ircd_alloc.h \ ../include/ircd_log.h ../include/ircd_string.h ../include/umkpasswd.h \ ../include/s_debug.h ../include/ircd_md5.h ../include/ircd_crypt.h \ ../include/ircd_crypt_smd5.h ../include/ircd_crypt_native.h \ - ../include/ircd_crypt_plain.h ../include/ircd_crypt_bcrypt.h + ../include/ircd_crypt_plain.h ../include/ircd_crypt_bcrypt.h \ + ../include/ircd_crypt_pbkdf2.h uping.o: uping.c ../config.h ../include/uping.h ../include/client.h \ ../include/ircd.h ../include/ircd_alloc.h ../include/ircd_events.h \ ../include/ircd_log.h ../include/ircd_osdep.h ../include/ircd_string.h \ diff --git a/ircd/account_conn.c b/ircd/account_conn.c new file mode 100644 index 00000000..b06b79c6 --- /dev/null +++ b/ircd/account_conn.c @@ -0,0 +1,482 @@ +/* + * IRC - Internet Relay Chat, ircd/account_conn.c + * Copyright (C) 2024 AfterNET Development Team + * + * 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 2, 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. + */ +/** @file + * @brief Account connection registry implementation. + * + * This module implements presence aggregation for users with multiple + * connections logged into the same account. It uses a hash table to + * efficiently track all connections per account and computes the + * effective presence using "most-present-wins" logic. + */ +#include "config.h" + +#include "account_conn.h" +#include "client.h" +#include "ircd.h" +#include "ircd_alloc.h" +#include "ircd_chattr.h" +#include "ircd_features.h" +#include "ircd_log.h" +#include "ircd_snprintf.h" +#include "ircd_string.h" +#include "metadata.h" +#include "struct.h" + +#include +#include + +/** Hash table for account entries. */ +static struct AccountEntry *accountTable[ACCOUNT_CONN_HASHSIZE]; + +/** Statistics for debugging. */ +static struct { + unsigned int entries; /**< Number of account entries */ + unsigned int connections; /**< Total connections tracked */ +} account_conn_stats; + +/* Forward declarations for LMDB persistence */ +static void persist_last_present(const char *account, time_t when); +static time_t load_last_present(const char *account); + +/** Calculate hash value for an account name. + * Uses a simple FNV-1a hash for case-insensitive account names. + * @param[in] account Account name to hash + * @return Hash value in range [0, ACCOUNT_CONN_HASHSIZE-1] + */ +static unsigned int account_hash(const char *account) +{ + unsigned int hash = 2166136261u; /* FNV offset basis */ + const char *p; + + for (p = account; *p; p++) { + hash ^= (unsigned int)ToLower(*p); + hash *= 16777619u; /* FNV prime */ + } + + return hash % ACCOUNT_CONN_HASHSIZE; +} + +/** Find an account entry in the hash table. + * @param[in] account Account name to find + * @return AccountEntry or NULL if not found + */ +static struct AccountEntry *find_entry(const char *account) +{ + unsigned int hashv = account_hash(account); + struct AccountEntry *entry; + + for (entry = accountTable[hashv]; entry; entry = entry->hnext) { + if (ircd_strcmp(entry->account, account) == 0) + return entry; + } + + return NULL; +} + +/** Create a new account entry and add it to the hash table. + * @param[in] account Account name + * @return New AccountEntry or NULL on allocation failure + */ +static struct AccountEntry *create_entry(const char *account) +{ + unsigned int hashv = account_hash(account); + struct AccountEntry *entry; + time_t persisted_time; + + entry = (struct AccountEntry *)MyCalloc(1, sizeof(struct AccountEntry)); + if (!entry) + return NULL; + + ircd_strncpy(entry->account, account, ACCOUNTLEN); + entry->effective_state = CONN_PRESENT; /* Default to present */ + + /* Load persisted last_present from LMDB, or use current time */ + persisted_time = load_last_present(account); + entry->last_present = persisted_time ? persisted_time : CurrentTime; + + /* Add to hash table */ + entry->hnext = accountTable[hashv]; + accountTable[hashv] = entry; + + account_conn_stats.entries++; + + return entry; +} + +/** Remove an account entry from the hash table and free it. + * @param[in] entry Entry to remove + */ +static void remove_entry(struct AccountEntry *entry) +{ + unsigned int hashv = account_hash(entry->account); + struct AccountEntry *tmp = accountTable[hashv]; + struct AccountEntry **prev_p = &accountTable[hashv]; + + while (tmp) { + if (tmp == entry) { + *prev_p = entry->hnext; + account_conn_stats.entries--; + MyFree(entry); + return; + } + prev_p = &tmp->hnext; + tmp = tmp->hnext; + } +} + +/** Compute effective presence for an account entry. + * Uses "most-present-wins" logic: + * 1. PRESENT beats everything + * 2. AWAY beats AWAY_STAR + * 3. AWAY_STAR only if all connections are AWAY_STAR + * + * @param[in] entry Account entry to aggregate + * @return 1 if effective state changed, 0 otherwise + */ +static int compute_effective_presence(struct AccountEntry *entry) +{ + struct AccountConn *conn; + enum ConnAwayState new_state = CONN_AWAY_STAR; + const char *best_msg = NULL; + int changed = 0; + + if (!entry || !entry->connections) { + /* No connections - shouldn't happen, but handle gracefully */ + return 0; + } + + /* Scan all connections, most-present-wins */ + for (conn = entry->connections; conn; conn = conn->next) { + if (conn->away_state == CONN_PRESENT) { + /* Present beats everything - we're done */ + new_state = CONN_PRESENT; + best_msg = NULL; + entry->last_present = CurrentTime; + break; + } else if (conn->away_state == CONN_AWAY) { + /* Away with message beats away-star */ + new_state = CONN_AWAY; + if (!best_msg && conn->away_msg[0]) + best_msg = conn->away_msg; + } + /* AWAY_STAR contributes nothing - it's the default if nothing better */ + } + + /* Check if effective state changed */ + if (entry->effective_state != new_state) { + changed = 1; + entry->effective_state = new_state; + + /* Persist last_present when becoming present */ + if (new_state == CONN_PRESENT) { + persist_last_present(entry->account, entry->last_present); + } + } + + /* Update effective message */ + if (new_state == CONN_AWAY && best_msg) { + if (ircd_strcmp(entry->effective_away_msg, best_msg) != 0) { + changed = 1; + ircd_strncpy(entry->effective_away_msg, best_msg, AWAYLEN); + } + } else if (new_state == CONN_PRESENT || new_state == CONN_AWAY_STAR) { + if (entry->effective_away_msg[0]) { + entry->effective_away_msg[0] = '\0'; + /* Message clearing doesn't count as a change for broadcast purposes */ + } + } + + return changed; +} + +/* + * Public API implementations + */ + +void account_conn_init(void) +{ + memset(accountTable, 0, sizeof(accountTable)); + memset(&account_conn_stats, 0, sizeof(account_conn_stats)); + + log_write(LS_SYSTEM, L_DEBUG, 0, + "account_conn: initialized with hash size %d", + ACCOUNT_CONN_HASHSIZE); +} + +struct AccountConn *account_conn_add(struct Client *cptr) +{ + struct AccountEntry *entry; + struct AccountConn *conn; + const char *account; + + if (!cptr || !IsAccount(cptr)) + return NULL; + + account = cli_account(cptr); + if (!account || !account[0] || account[0] == '0') + return NULL; + + /* Find or create account entry */ + entry = find_entry(account); + if (!entry) { + entry = create_entry(account); + if (!entry) + return NULL; + } + + /* Check if already added (shouldn't happen) */ + for (conn = entry->connections; conn; conn = conn->next) { + if (conn->client == cptr) { + log_write(LS_SYSTEM, L_WARNING, 0, + "account_conn_add: client %C already in registry for %s", + cptr, account); + return conn; + } + } + + /* Create new connection entry */ + conn = (struct AccountConn *)MyCalloc(1, sizeof(struct AccountConn)); + if (!conn) + return NULL; + + conn->client = cptr; + conn->away_state = CONN_PRESENT; /* Default to present */ + + /* Check if client already has away state from pre-away */ + if (cli_user(cptr) && cli_user(cptr)->away) { + conn->away_state = CONN_AWAY; + ircd_strncpy(conn->away_msg, cli_user(cptr)->away, AWAYLEN); + } + + /* Add to head of connection list */ + conn->next = entry->connections; + if (entry->connections) + entry->connections->prev_p = &conn->next; + conn->prev_p = &entry->connections; + entry->connections = conn; + + entry->conn_count++; + account_conn_stats.connections++; + + /* Store back-reference in client for O(1) lookup */ + if (cli_user(cptr)) + cli_user(cptr)->account_conn = conn; + + /* Recompute effective presence */ + compute_effective_presence(entry); + + log_write(LS_SYSTEM, L_DEBUG, 0, + "account_conn_add: added %C to account %s (count: %u)", + cptr, account, entry->conn_count); + + return conn; +} + +int account_conn_remove(struct Client *cptr) +{ + struct AccountEntry *entry; + struct AccountConn *conn; + const char *account; + int changed = 0; + + if (!cptr || !IsAccount(cptr)) + return 0; + + account = cli_account(cptr); + if (!account || !account[0] || account[0] == '0') + return 0; + + entry = find_entry(account); + if (!entry) + return 0; + + /* Find this client's connection */ + for (conn = entry->connections; conn; conn = conn->next) { + if (conn->client == cptr) + break; + } + + if (!conn) { + log_write(LS_SYSTEM, L_WARNING, 0, + "account_conn_remove: client %C not found in registry for %s", + cptr, account); + return 0; + } + + /* Remove from linked list */ + if (conn->next) + conn->next->prev_p = conn->prev_p; + *conn->prev_p = conn->next; + + entry->conn_count--; + account_conn_stats.connections--; + + /* Clear back-reference */ + if (cli_user(cptr)) + cli_user(cptr)->account_conn = NULL; + + MyFree(conn); + + log_write(LS_SYSTEM, L_DEBUG, 0, + "account_conn_remove: removed %C from account %s (remaining: %u)", + cptr, account, entry->conn_count); + + /* If last connection, remove the entry */ + if (entry->conn_count == 0) { + remove_entry(entry); + /* Effective state becomes undefined, but there's no one to broadcast to */ + return 0; + } + + /* Recompute effective presence */ + changed = compute_effective_presence(entry); + + return changed; +} + +struct AccountEntry *account_conn_find(const char *account) +{ + if (!account || !account[0]) + return NULL; + + return find_entry(account); +} + +unsigned int account_conn_count(const char *account) +{ + struct AccountEntry *entry = find_entry(account); + + return entry ? entry->conn_count : 0; +} + +int account_conn_set_away(struct Client *cptr, + enum ConnAwayState state, + const char *message) +{ + struct AccountEntry *entry; + struct AccountConn *conn; + const char *account; + + if (!cptr || !IsAccount(cptr)) + return 0; + + account = cli_account(cptr); + if (!account || !account[0] || account[0] == '0') + return 0; + + entry = find_entry(account); + if (!entry) + return 0; + + /* Find this client's connection */ + for (conn = entry->connections; conn; conn = conn->next) { + if (conn->client == cptr) + break; + } + + if (!conn) { + log_write(LS_SYSTEM, L_WARNING, 0, + "account_conn_set_away: client %C not in registry for %s", + cptr, account); + return 0; + } + + /* Update this connection's state */ + conn->away_state = state; + if (message && message[0]) { + ircd_strncpy(conn->away_msg, message, AWAYLEN); + } else { + conn->away_msg[0] = '\0'; + } + + /* Recompute and return whether effective changed */ + return compute_effective_presence(entry); +} + +int account_conn_get_presence(const char *account, + enum ConnAwayState *state, + char *message, + size_t msg_size) +{ + struct AccountEntry *entry = find_entry(account); + + if (!entry) + return -1; + + if (state) + *state = entry->effective_state; + + if (message && msg_size > 0) { + ircd_strncpy(message, entry->effective_away_msg, msg_size - 1); + message[msg_size - 1] = '\0'; + } + + return 0; +} + +time_t account_conn_last_present(const char *account) +{ + struct AccountEntry *entry = find_entry(account); + + if (entry) + return entry->last_present; + + /* Try to load from LMDB if account not in memory */ + if (metadata_lmdb_is_available()) { + char value[32]; + if (metadata_account_get(account, "$last_present", value) == 0) { + return (time_t)strtoul(value, NULL, 10); + } + } + + return 0; +} + +/** Persist last_present timestamp to LMDB. + * @param[in] account Account name. + * @param[in] when Timestamp to persist. + */ +static void persist_last_present(const char *account, time_t when) +{ + char value[32]; + + if (!metadata_lmdb_is_available()) + return; + + ircd_snprintf(0, value, sizeof(value), "%lu", (unsigned long)when); + metadata_account_set(account, "$last_present", value); +} + +/** Load last_present timestamp from LMDB. + * @param[in] account Account name. + * @return Timestamp or 0 if not found. + */ +static time_t load_last_present(const char *account) +{ + char value[32]; + + if (!metadata_lmdb_is_available()) + return 0; + + if (metadata_account_get(account, "$last_present", value) == 0) { + return (time_t)strtoul(value, NULL, 10); + } + + return 0; +} diff --git a/ircd/channel.c b/ircd/channel.c index 31e4cee0..cf5a5c26 100644 --- a/ircd/channel.c +++ b/ircd/channel.c @@ -53,11 +53,15 @@ #include "struct.h" #include "sys.h" #include "whowas.h" +#include "history.h" +#include "metadata.h" /* #include -- Now using assert in ircd_log.h */ #include #include #include +#include +#include /** Linked list containing the full list of all channels */ struct Channel* GlobalChannelList = 0; @@ -75,6 +79,64 @@ static size_t bans_inuse; int parse_extban(char *ban, struct ExtBan *extban, int level, char *prefix); +#ifdef USE_LMDB +/** Counter for generating unique message IDs for channel event history storage */ +static unsigned long channel_history_msgid_counter = 0; + +/** Store a channel event (JOIN, PART, etc.) in the history database. + * Generates a unique msgid and timestamp, then stores the event. + * @param[in] sptr Client that triggered the event. + * @param[in] chptr Target channel. + * @param[in] text Event content (e.g., part reason, empty for joins). + * @param[in] type Event type (HISTORY_JOIN, HISTORY_PART, etc.). + */ +static void store_channel_event(struct Client *sptr, struct Channel *chptr, + const char *text, enum HistoryMessageType type) +{ + struct timeval tv; + char timestamp[32]; + char msgid[64]; + char sender[HISTORY_SENDER_LEN]; + const char *account; + + if (!history_is_available()) + return; + + /* Check if chathistory feature is enabled */ + if (!feature_bool(FEAT_CAP_draft_chathistory)) + return; + + /* Generate Unix timestamp for storage */ + gettimeofday(&tv, NULL); + ircd_snprintf(0, timestamp, sizeof(timestamp), "%lu.%03lu", + (unsigned long)tv.tv_sec, + (unsigned long)(tv.tv_usec / 1000)); + + /* Generate unique msgid */ + ircd_snprintf(0, msgid, sizeof(msgid), "%s-%lu-%lu", + cli_yxx(&me), + (unsigned long)cli_firsttime(&me), + ++channel_history_msgid_counter); + + /* Build sender string: nick!user@host */ + if (cli_user(sptr)) + ircd_snprintf(0, sender, sizeof(sender), "%s!%s@%s", + cli_name(sptr), + cli_user(sptr)->username, + cli_user(sptr)->host); + else + ircd_strncpy(sender, cli_name(sptr), sizeof(sender) - 1); + + /* Get account name if logged in */ + account = (cli_user(sptr) && cli_user(sptr)->account[0]) + ? cli_user(sptr)->account : NULL; + + /* Store in database */ + history_store_message(msgid, timestamp, chptr->chname, sender, + account, type, text ? text : ""); +} +#endif /* USE_LMDB */ + #if !defined(NDEBUG) /** return the length (>=0) of a chain of links. * @param lp pointer to the start of the linked list @@ -362,6 +424,10 @@ int destruct_channel(struct Channel* chptr) next = ban->next; free_ban(ban); } + + /* Free channel metadata */ + metadata_free_channel(chptr); + if (chptr->prev) chptr->prev->next = chptr->next; else @@ -1440,6 +1506,17 @@ void send_channel_modes(struct Client *cptr, struct Channel *chptr) sendcmdto_one(&me, CMD_TOPIC, cptr, "%H %s %Tu %Tu :%s", chptr, chptr->topic_nick, chptr->creationtime, chptr->topic_time, chptr->topic); + + /* Burst channel metadata if enabled */ + if (feature_bool(FEAT_METADATA_BURST)) { + struct MetadataEntry *entry; + for (entry = chptr->metadata; entry; entry = entry->next) { + sendcmdto_one(&me, CMD_METADATA, cptr, "%s %s %s :%s", + chptr->chname, entry->key, + entry->visibility == METADATA_VIS_PRIVATE ? "P" : "*", + entry->value ? entry->value : ""); + } + } } /** Canonify a mask. @@ -1842,6 +1919,95 @@ struct Channel *get_channel(struct Client *cptr, char *chname, ChannelGetType fl return chptr; } +/** Rename a channel. + * Updates the channel name in the hash table and channel structure. + * Reallocates the channel structure if the new name is longer. + * + * @param[in,out] chptr_p Pointer to channel pointer. Updated if reallocation occurs. + * @param[in] newname New name for the channel. + * @return 0 on success, -1 if invalid params, -2 if new name exists. + */ +int rename_channel(struct Channel **chptr_p, const char *newname) +{ + struct Channel *chptr; + size_t oldlen, newlen; + struct Channel *newchptr; + struct Membership *member; + struct SLink *link; + + if (!chptr_p || !*chptr_p || !newname || !*newname) + return -1; + + chptr = *chptr_p; + + newlen = strlen(newname); + if (newlen > CHANNELLEN) + return -1; + + /* Check if new name already exists */ + if (FindChannel(newname)) + return -2; + + oldlen = strlen(chptr->chname); + + /* If new name fits in existing allocation, just update in place */ + if (newlen <= oldlen) { + hChangeChannel(chptr, newname); + strcpy(chptr->chname, newname); + return 0; + } + + /* New name is longer - need to reallocate */ + newchptr = (struct Channel*) MyMalloc(sizeof(struct Channel) + newlen); + if (!newchptr) + return -1; + + /* Copy all data from old to new */ + memcpy(newchptr, chptr, sizeof(struct Channel)); + strcpy(newchptr->chname, newname); + + /* Update hash table */ + hRemChannel(chptr); + hAddChannel(newchptr); + + /* Update global linked list */ + if (chptr->prev) + chptr->prev->next = newchptr; + else + GlobalChannelList = newchptr; + if (chptr->next) + chptr->next->prev = newchptr; + + /* Update all membership pointers */ + for (member = newchptr->members; member; member = member->next_member) + member->channel = newchptr; + + /* Update user invite lists that point to this channel */ + for (link = newchptr->invites; link; link = link->next) { + struct Client *cptr = link->value.cptr; + struct SLink *inv; + /* Find and update user's invite pointer to this channel */ + for (inv = cli_user(cptr)->invited; inv; inv = inv->next) { + if (inv->value.chptr == chptr) { + inv->value.chptr = newchptr; + break; + } + } + } + + /* Update destruct event if pending */ + if (newchptr->destruct_event) + newchptr->destruct_event->chptr = newchptr; + + /* Free old channel structure */ + MyFree(chptr); + + /* Update caller's pointer */ + *chptr_p = newchptr; + + return 0; +} + /** invite a user to a channel. * * Adds an invite for a user to a channel. Limits the number of invites @@ -2411,7 +2577,7 @@ modebuf_flush_int(struct ModeBuf *mbuf, int all) mbuf->mb_channel, rembuf_i ? "-" : "", rembuf, addbuf_i ? "+" : "", addbuf, remstr, addstr); - if (mbuf->mb_dest & MODEBUF_DEST_CHANNEL) + if (mbuf->mb_dest & MODEBUF_DEST_CHANNEL) { sendcmdto_channel_butserv_butone(app_source, CMD_MODE, mbuf->mb_channel, NULL, 0, "%H %s%s%s%s%s%s%s%s", mbuf->mb_channel, rembuf_i || rembuf_local_i ? "-" : "", @@ -2419,6 +2585,21 @@ modebuf_flush_int(struct ModeBuf *mbuf, int all) addbuf_i || addbuf_local_i ? "+" : "", addbuf, addbuf_local, remstr, addstr); + +#ifdef USE_LMDB + /* Store MODE event in history (only from local users) */ + if (MyUser(mbuf->mb_source)) { + char mode_text[512]; + ircd_snprintf(0, mode_text, sizeof(mode_text), "%s%s%s%s%s%s%s%s", + rembuf_i || rembuf_local_i ? "-" : "", + rembuf, rembuf_local, + addbuf_i || addbuf_local_i ? "+" : "", + addbuf, addbuf_local, + remstr, addstr); + store_channel_event(mbuf->mb_source, mbuf->mb_channel, mode_text, HISTORY_MODE); + } +#endif + } } /* Now are we supposed to propagate to other servers? */ @@ -4878,6 +5059,15 @@ joinbuf_join(struct JoinBuf *jbuf, struct Channel *chan, unsigned int flags) sendcmdto_one(jbuf->jb_source, CMD_PART, jbuf->jb_source, (flags & CHFL_BANNED || !jbuf->jb_comment) ? ":%H" : "%H :%s", chan, jbuf->jb_comment); + +#ifdef USE_LMDB + /* Store PART event in history (only from local users to avoid duplicates) */ + if (MyUser(jbuf->jb_source) && !(flags & (CHFL_ZOMBIE | CHFL_DELAYED))) + store_channel_event(jbuf->jb_source, chan, + (flags & CHFL_BANNED || !jbuf->jb_comment) ? "" : jbuf->jb_comment, + HISTORY_PART); +#endif + /* XXX: Shouldn't we send a PART here anyway? */ /* to users on the channel? Why? From their POV, the user isn't on * the channel anymore anyway. We don't send to servers until below, @@ -4912,6 +5102,12 @@ joinbuf_join(struct JoinBuf *jbuf, struct Channel *chan, unsigned int flags) IsAccount(jbuf->jb_source) ? cli_account(jbuf->jb_source) : "*", cli_info(jbuf->jb_source)); +#ifdef USE_LMDB + /* Store JOIN event in history (only from local users to avoid duplicates) */ + if (MyUser(jbuf->jb_source)) + store_channel_event(jbuf->jb_source, chan, "", HISTORY_JOIN); +#endif + if (cli_user(jbuf->jb_source)->away) sendcmdto_channel_capab_butserv_butone(jbuf->jb_source, CMD_AWAY, chan, NULL, 0, CAP_AWAYNOTIFY, CAP_NONE, ":%s", diff --git a/ircd/engine_devpoll.c b/ircd/engine_devpoll.c index 1f1423c4..cc1d122d 100644 --- a/ircd/engine_devpoll.c +++ b/ircd/engine_devpoll.c @@ -29,6 +29,7 @@ #include "ircd_features.h" #include "ircd_log.h" #include "s_debug.h" +#include "thread_pool.h" /* #include -- Now using assert in ircd_log.h */ #include @@ -448,6 +449,7 @@ engine_loop(struct Generators* gen) } timer_run(); /* execute any pending timers */ + thread_pool_poll(); /* process completed async tasks */ } } diff --git a/ircd/engine_epoll.c b/ircd/engine_epoll.c index 4e7cefae..c58602dc 100644 --- a/ircd/engine_epoll.c +++ b/ircd/engine_epoll.c @@ -28,6 +28,7 @@ #include "ircd_features.h" #include "ircd_log.h" #include "s_debug.h" +#include "thread_pool.h" /* #include -- Now using assert in ircd_log.h */ #include @@ -338,6 +339,7 @@ engine_loop(struct Generators *gen) gen_ref_dec(sock); } timer_run(); + thread_pool_poll(); /* process completed async tasks */ } MyFree(events); } diff --git a/ircd/engine_kqueue.c b/ircd/engine_kqueue.c index 739c3c53..93a7071a 100644 --- a/ircd/engine_kqueue.c +++ b/ircd/engine_kqueue.c @@ -29,6 +29,7 @@ #include "ircd_features.h" #include "ircd_log.h" #include "s_debug.h" +#include "thread_pool.h" /* #include -- Now using assert in ircd_log.h */ #include @@ -433,6 +434,7 @@ engine_loop(struct Generators* gen) } timer_run(); /* execute any pending timers */ + thread_pool_poll(); /* process completed async tasks */ } } diff --git a/ircd/engine_poll.c b/ircd/engine_poll.c index 41ab9394..49453098 100644 --- a/ircd/engine_poll.c +++ b/ircd/engine_poll.c @@ -28,6 +28,7 @@ #include "ircd_alloc.h" #include "ircd_log.h" #include "s_debug.h" +#include "thread_pool.h" /* #include -- Now using assert in ircd_log.h */ #include @@ -423,6 +424,7 @@ engine_loop(struct Generators* gen) } timer_run(); /* execute any pending timers */ + thread_pool_poll(); /* process completed async tasks */ } } diff --git a/ircd/engine_select.c b/ircd/engine_select.c index 963e1ff0..aa636569 100644 --- a/ircd/engine_select.c +++ b/ircd/engine_select.c @@ -34,6 +34,7 @@ #include "ircd.h" #include "ircd_log.h" #include "s_debug.h" +#include "thread_pool.h" /* #include -- Now using assert in ircd_log.h */ #include @@ -407,6 +408,7 @@ engine_loop(struct Generators* gen) } timer_run(); /* execute any pending timers */ + thread_pool_poll(); /* process completed async tasks */ } } diff --git a/ircd/hash.c b/ircd/hash.c index 98c5de7f..905d9235 100644 --- a/ircd/hash.c +++ b/ircd/hash.c @@ -214,6 +214,23 @@ int hRemChannel(struct Channel *chptr) return -1; } +/** Rename a channel in the hash table. + * @param[in] chptr Channel whose name is changing. + * @param[in] newname New name for channel. + * @return Zero on success. + */ +int hChangeChannel(struct Channel *chptr, const char *newname) +{ + HASHREGS newhash = strhash(newname); + + assert(0 != chptr); + hRemChannel(chptr); + + chptr->hnext = channelTable[newhash]; + channelTable[newhash] = chptr; + return 0; +} + /** Find a client by name, filtered by status mask. * If a client is found, it is moved to the top of its hash bucket. * @param[in] name Client name to search for. diff --git a/ircd/history.c b/ircd/history.c new file mode 100644 index 00000000..070ac6ab --- /dev/null +++ b/ircd/history.c @@ -0,0 +1,1880 @@ +/* + * IRC - Internet Relay Chat, ircd/history.c + * Copyright (C) 2024 Nefarious Development Team + * + * 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. + */ +/** @file + * @brief Message history storage using LMDB. + * + * LMDB (Lightning Memory-Mapped Database) provides zero-copy reads + * and MVCC for lock-free concurrent reads. Perfect for chathistory + * where reads vastly outnumber writes. + * + * Key structure: "target\0timestamp\0msgid" + * This allows efficient range queries by target and timestamp. + * + * Implements storage backend for IRCv3 draft/chathistory extension. + * Specification: https://ircv3.net/specs/extensions/chathistory + */ +#include "config.h" + +#ifdef USE_LMDB + +#include "history.h" +#include "ircd_alloc.h" +#include "ircd_compress.h" +#include "ircd_log.h" +#include "ircd_reply.h" +#include "ircd_snprintf.h" +#include "ircd_string.h" +#include "numeric.h" +#include "s_debug.h" +#include "s_stats.h" + +#include +#include +#include +#include +#include + +/** LMDB environment */ +static MDB_env *history_env = NULL; + +/** Main message database */ +static MDB_dbi history_dbi; + +/** Secondary index: msgid -> timestamp (for msgid lookups) */ +static MDB_dbi history_msgid_dbi; + +/** Target tracking database for TARGETS query */ +static MDB_dbi history_targets_dbi; + +/** Read markers database (IRCv3 draft/read-marker) */ +static MDB_dbi history_readmarkers_dbi; + +/** Flag indicating if history is available */ +static int history_available = 0; + +/** Maximum database size (1GB default, configurable) */ +static size_t history_map_size = 1UL * 1024 * 1024 * 1024; + +/** Maximum number of named databases */ +#define HISTORY_MAX_DBS 5 + +/** Key separator character */ +#define KEY_SEP '\0' + +/** Maximum value buffer size for serialization */ +#define HISTORY_VALUE_BUFSIZE 1024 + +/** Message type names for serialization */ +static const char *history_type_names[] = { + "PRIVMSG", "NOTICE", "JOIN", "PART", "QUIT", + "KICK", "MODE", "TOPIC", "TAGMSG" +}; + +/** Build a lookup key from target and timestamp. + * @param[out] key Output buffer. + * @param[in] keysize Size of output buffer. + * @param[in] target Channel or nick. + * @param[in] timestamp Unix timestamp (or NULL for just target). + * @param[in] msgid Message ID (or NULL). + * @return Length of key. + */ +static int build_key(char *key, int keysize, const char *target, + const char *timestamp, const char *msgid) +{ + int pos = 0; + int len; + + /* Copy target */ + len = strlen(target); + if (pos + len + 1 >= keysize) return -1; + memcpy(key + pos, target, len); + pos += len; + key[pos++] = KEY_SEP; + + /* Copy timestamp if provided */ + if (timestamp) { + len = strlen(timestamp); + if (pos + len + 1 >= keysize) return -1; + memcpy(key + pos, timestamp, len); + pos += len; + key[pos++] = KEY_SEP; + + /* Copy msgid if provided */ + if (msgid) { + len = strlen(msgid); + if (pos + len >= keysize) return -1; + memcpy(key + pos, msgid, len); + pos += len; + } + } + + return pos; +} + +/** Serialize a message to a buffer. + * Format: type|sender|account|content + * @param[out] buf Output buffer. + * @param[in] bufsize Size of output buffer. + * @param[in] type Message type. + * @param[in] sender Sender mask. + * @param[in] account Account name (may be NULL). + * @param[in] content Message content (may be NULL). + * @return Length of serialized data. + */ +static int serialize_message(char *buf, int bufsize, + enum HistoryMessageType type, + const char *sender, const char *account, + const char *content) +{ + return ircd_snprintf(0, buf, bufsize, "%d|%s|%s|%s", + (int)type, + sender ? sender : "", + account ? account : "", + content ? content : ""); +} + +/** Deserialize a message from a buffer. + * @param[in] data Serialized data (possibly compressed). + * @param[in] datalen Length of data. + * @param[out] msg Message structure to fill. + * @return 0 on success, -1 on error. + */ +static int deserialize_message(const char *data, int datalen, + struct HistoryMessage *msg) +{ + const char *p, *end; + char *field; + int type; +#ifdef USE_ZSTD + char decompressed[HISTORY_VALUE_BUFSIZE]; + size_t decompressed_len; + + /* Check if data is compressed and decompress if needed */ + if (is_compressed((const unsigned char *)data, datalen)) { + if (decompress_data((const unsigned char *)data, datalen, + (unsigned char *)decompressed, sizeof(decompressed), + &decompressed_len) < 0) { + return -1; + } + data = decompressed; + datalen = decompressed_len; + } +#endif + + p = data; + end = data + datalen; + + /* Parse type */ + field = strchr(p, '|'); + if (!field || field >= end) return -1; + type = atoi(p); + if (type < 0 || type > HISTORY_TAGMSG) return -1; + msg->type = (enum HistoryMessageType)type; + p = field + 1; + + /* Parse sender */ + field = strchr(p, '|'); + if (!field || field >= end) return -1; + if ((size_t)(field - p) >= sizeof(msg->sender)) return -1; + memcpy(msg->sender, p, field - p); + msg->sender[field - p] = '\0'; + p = field + 1; + + /* Parse account */ + field = strchr(p, '|'); + if (!field || field >= end) return -1; + if ((size_t)(field - p) >= sizeof(msg->account)) return -1; + memcpy(msg->account, p, field - p); + msg->account[field - p] = '\0'; + p = field + 1; + + /* Parse content (rest of string) */ + if ((size_t)(end - p) >= sizeof(msg->content)) return -1; + memcpy(msg->content, p, end - p); + msg->content[end - p] = '\0'; + + return 0; +} + +/** Parse target and timestamp from a key. + * @param[in] key Key data. + * @param[in] keylen Key length. + * @param[out] target Output for target (at least CHANNELLEN+1). + * @param[out] timestamp Output for timestamp (at least HISTORY_TIMESTAMP_LEN). + * @param[out] msgid Output for msgid (at least HISTORY_MSGID_LEN). + * @return 0 on success, -1 on error. + */ +static int parse_key(const char *key, int keylen, + char *target, char *timestamp, char *msgid) +{ + const char *p, *end; + const char *sep1, *sep2; + + p = key; + end = key + keylen; + + /* Find first separator (end of target) */ + sep1 = memchr(p, KEY_SEP, end - p); + if (!sep1) return -1; + + if (target) { + if ((size_t)(sep1 - p) > CHANNELLEN) return -1; + memcpy(target, p, sep1 - p); + target[sep1 - p] = '\0'; + } + p = sep1 + 1; + + /* Find second separator (end of timestamp) */ + sep2 = memchr(p, KEY_SEP, end - p); + if (sep2) { + if (timestamp) { + if ((size_t)(sep2 - p) >= HISTORY_TIMESTAMP_LEN) return -1; + memcpy(timestamp, p, sep2 - p); + timestamp[sep2 - p] = '\0'; + } + p = sep2 + 1; + + if (msgid) { + if ((size_t)(end - p) >= HISTORY_MSGID_LEN) return -1; + memcpy(msgid, p, end - p); + msgid[end - p] = '\0'; + } + } else { + /* No msgid in key */ + if (timestamp) { + if ((size_t)(end - p) >= HISTORY_TIMESTAMP_LEN) return -1; + memcpy(timestamp, p, end - p); + timestamp[end - p] = '\0'; + } + if (msgid) + msgid[0] = '\0'; + } + + return 0; +} + +/* + * Timestamp Conversion Functions + * + * Internal storage and S2S use Unix timestamps (seconds.milliseconds). + * Client-facing @time= tags use ISO 8601 per IRCv3 spec. + */ + +char *history_format_timestamp(char *buf, size_t buflen) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + ircd_snprintf(0, buf, buflen, "%lu.%03lu", + (unsigned long)tv.tv_sec, + (unsigned long)(tv.tv_usec / 1000)); + return buf; +} + +int history_unix_to_iso(const char *unix_ts, char *iso_buf, size_t iso_buflen) +{ + unsigned long secs; + unsigned int millis = 0; + char *dot; + time_t t; + struct tm tm; + + if (!unix_ts || !iso_buf || iso_buflen < 25) + return -1; + + secs = strtoul(unix_ts, &dot, 10); + if (dot && *dot == '.') { + millis = strtoul(dot + 1, NULL, 10); + /* Ensure exactly 3 digits */ + if (millis > 999) millis = 999; + } + + t = (time_t)secs; + if (!gmtime_r(&t, &tm)) + return -1; + + ircd_snprintf(0, iso_buf, iso_buflen, + "%04d-%02d-%02dT%02d:%02d:%02d.%03uZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, millis); + return 0; +} + +int history_iso_to_unix(const char *iso_ts, char *unix_buf, size_t unix_buflen) +{ + struct tm tm; + time_t t; + unsigned int millis = 0; + const char *p; + char *end; + + if (!iso_ts || !unix_buf || unix_buflen < 15) + return -1; + + /* Parse ISO 8601: YYYY-MM-DDThh:mm:ss[.sss]Z */ + memset(&tm, 0, sizeof(tm)); + + /* Parse date */ + tm.tm_year = strtol(iso_ts, &end, 10) - 1900; + if (!end || *end != '-') return -1; + p = end + 1; + + tm.tm_mon = strtol(p, &end, 10) - 1; + if (!end || *end != '-') return -1; + p = end + 1; + + tm.tm_mday = strtol(p, &end, 10); + if (!end || *end != 'T') return -1; + p = end + 1; + + /* Parse time */ + tm.tm_hour = strtol(p, &end, 10); + if (!end || *end != ':') return -1; + p = end + 1; + + tm.tm_min = strtol(p, &end, 10); + if (!end || *end != ':') return -1; + p = end + 1; + + tm.tm_sec = strtol(p, &end, 10); + + /* Parse optional milliseconds */ + if (end && *end == '.') { + millis = strtoul(end + 1, &end, 10); + if (millis > 999) millis = 999; + } + + /* Convert to Unix time */ + t = timegm(&tm); + if (t == (time_t)-1) + return -1; + + ircd_snprintf(0, unix_buf, unix_buflen, "%lu.%03u", + (unsigned long)t, millis); + return 0; +} + +int history_init(const char *dbpath) +{ + MDB_txn *txn; + int rc; + + if (history_available) + return 0; /* Already initialized */ + + /* Create LMDB environment */ + rc = mdb_env_create(&history_env); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "history: mdb_env_create failed: %s", + mdb_strerror(rc)); + return -1; + } + + /* Set maximum number of databases */ + rc = mdb_env_set_maxdbs(history_env, HISTORY_MAX_DBS); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "history: mdb_env_set_maxdbs failed: %s", + mdb_strerror(rc)); + mdb_env_close(history_env); + history_env = NULL; + return -1; + } + + /* Set map size (configurable, default 1GB) */ + rc = mdb_env_set_mapsize(history_env, history_map_size); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "history: mdb_env_set_mapsize failed: %s", + mdb_strerror(rc)); + mdb_env_close(history_env); + history_env = NULL; + return -1; + } + + /* Open environment */ + rc = mdb_env_open(history_env, dbpath, 0, 0644); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "history: mdb_env_open(%s) failed: %s", + dbpath, mdb_strerror(rc)); + mdb_env_close(history_env); + history_env = NULL; + return -1; + } + + /* Open databases in a transaction */ + rc = mdb_txn_begin(history_env, NULL, 0, &txn); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "history: mdb_txn_begin failed: %s", + mdb_strerror(rc)); + mdb_env_close(history_env); + history_env = NULL; + return -1; + } + + /* Open main message database */ + rc = mdb_dbi_open(txn, "messages", MDB_CREATE, &history_dbi); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "history: mdb_dbi_open(messages) failed: %s", + mdb_strerror(rc)); + mdb_txn_abort(txn); + mdb_env_close(history_env); + history_env = NULL; + return -1; + } + + /* Open msgid index database */ + rc = mdb_dbi_open(txn, "msgid_index", MDB_CREATE, &history_msgid_dbi); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "history: mdb_dbi_open(msgid_index) failed: %s", + mdb_strerror(rc)); + mdb_txn_abort(txn); + mdb_env_close(history_env); + history_env = NULL; + return -1; + } + + /* Open targets database */ + rc = mdb_dbi_open(txn, "targets", MDB_CREATE, &history_targets_dbi); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "history: mdb_dbi_open(targets) failed: %s", + mdb_strerror(rc)); + mdb_txn_abort(txn); + mdb_env_close(history_env); + history_env = NULL; + return -1; + } + + /* Open readmarkers database */ + rc = mdb_dbi_open(txn, "readmarkers", MDB_CREATE, &history_readmarkers_dbi); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "history: mdb_dbi_open(readmarkers) failed: %s", + mdb_strerror(rc)); + mdb_txn_abort(txn); + mdb_env_close(history_env); + history_env = NULL; + return -1; + } + + rc = mdb_txn_commit(txn); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "history: mdb_txn_commit failed: %s", + mdb_strerror(rc)); + mdb_env_close(history_env); + history_env = NULL; + return -1; + } + + history_available = 1; + log_write(LS_SYSTEM, L_INFO, 0, "history: LMDB initialized at %s", dbpath); + + return 0; +} + +void history_shutdown(void) +{ + if (!history_available) + return; + + mdb_dbi_close(history_env, history_dbi); + mdb_dbi_close(history_env, history_msgid_dbi); + mdb_dbi_close(history_env, history_targets_dbi); + mdb_dbi_close(history_env, history_readmarkers_dbi); + mdb_env_close(history_env); + history_env = NULL; + history_available = 0; + + log_write(LS_SYSTEM, L_INFO, 0, "history: LMDB shutdown complete"); +} + +int history_store_message(const char *msgid, const char *timestamp, + const char *target, const char *sender, + const char *account, enum HistoryMessageType type, + const char *content) +{ + MDB_txn *txn; + MDB_val key, data; + char keybuf[CHANNELLEN + HISTORY_TIMESTAMP_LEN + HISTORY_MSGID_LEN + 8]; + char valbuf[HISTORY_VALUE_BUFSIZE]; + int keylen, vallen; + int rc; +#ifdef USE_ZSTD + unsigned char compressed[HISTORY_VALUE_BUFSIZE + 64]; + size_t compressed_len; +#endif + + if (!history_available) + return -1; + + /* Build key: target\0timestamp\0msgid */ + keylen = build_key(keybuf, sizeof(keybuf), target, timestamp, msgid); + if (keylen < 0) + return -1; + + /* Log the key being stored (with nulls as dots) */ + { + char key_preview[128]; + int preview_len = keylen < 120 ? keylen : 120; + memcpy(key_preview, keybuf, preview_len); + key_preview[preview_len] = '\0'; + for (int i = 0; i < preview_len; i++) { + if (key_preview[i] == '\0') key_preview[i] = '.'; + } + log_write(LS_SYSTEM, L_INFO, 0, "history_store_message: storing key='%s' (len=%d) target='%s' ts='%s' msgid='%s'", + key_preview, keylen, target, timestamp, msgid); + } + + /* Serialize value */ + vallen = serialize_message(valbuf, sizeof(valbuf), type, sender, account, content); + if (vallen < 0) + return -1; + + /* Begin write transaction */ + rc = mdb_txn_begin(history_env, NULL, 0, &txn); + if (rc != 0) { + Debug((DEBUG_DEBUG, "history: mdb_txn_begin failed: %s", mdb_strerror(rc))); + return -1; + } + + /* Store message (with optional compression) */ + key.mv_size = keylen; + key.mv_data = keybuf; +#ifdef USE_ZSTD + if (compress_data((unsigned char *)valbuf, vallen, + compressed, sizeof(compressed), &compressed_len) >= 0) { + data.mv_size = compressed_len; + data.mv_data = compressed; + } else { + data.mv_size = vallen; + data.mv_data = valbuf; + } +#else + data.mv_size = vallen; + data.mv_data = valbuf; +#endif + + rc = mdb_put(txn, history_dbi, &key, &data, 0); + if (rc != 0) { + Debug((DEBUG_DEBUG, "history: mdb_put failed: %s", mdb_strerror(rc))); + mdb_txn_abort(txn); + return -1; + } + + /* Store msgid -> target\0timestamp index */ + key.mv_size = strlen(msgid); + key.mv_data = (void *)msgid; + /* Value is target\0timestamp */ + keylen = build_key(keybuf, sizeof(keybuf), target, timestamp, NULL); + data.mv_size = keylen; + data.mv_data = keybuf; + + rc = mdb_put(txn, history_msgid_dbi, &key, &data, 0); + if (rc != 0) { + Debug((DEBUG_DEBUG, "history: mdb_put(msgid) failed: %s", mdb_strerror(rc))); + mdb_txn_abort(txn); + return -1; + } + + /* Update target's last message timestamp */ + key.mv_size = strlen(target); + key.mv_data = (void *)target; + data.mv_size = strlen(timestamp); + data.mv_data = (void *)timestamp; + + rc = mdb_put(txn, history_targets_dbi, &key, &data, 0); + if (rc != 0) { + Debug((DEBUG_DEBUG, "history: mdb_put(target) failed: %s", mdb_strerror(rc))); + mdb_txn_abort(txn); + return -1; + } + + rc = mdb_txn_commit(txn); + if (rc != 0) { + Debug((DEBUG_DEBUG, "history: mdb_txn_commit failed: %s", mdb_strerror(rc))); + return -1; + } + + return 0; +} + +/** Internal query implementation with direction support. + * @param[in] target Channel or nick to query. + * @param[in] start_key Starting key for cursor. + * @param[in] start_keylen Length of starting key. + * @param[in] direction Query direction. + * @param[in] limit Maximum messages to return. + * @param[out] result Pointer to result list head. + * @return Number of messages returned, or -1 on error. + */ +static int history_query_internal(const char *target, + const char *start_key, int start_keylen, + enum HistoryDirection direction, + int limit, struct HistoryMessage **result) +{ + MDB_txn *txn; + MDB_cursor *cursor; + MDB_val key, data; + struct HistoryMessage *head = NULL, *tail = NULL, *msg; + char target_prefix[CHANNELLEN + 2]; + int target_prefix_len; + int count = 0; + int rc; + MDB_cursor_op op; + + *result = NULL; + + if (!history_available) + return -1; + + /* Build target prefix for boundary checking */ + target_prefix_len = ircd_snprintf(0, target_prefix, sizeof(target_prefix), + "%s%c", target, KEY_SEP); + + /* Begin read transaction */ + rc = mdb_txn_begin(history_env, NULL, MDB_RDONLY, &txn); + if (rc != 0) + return -1; + + rc = mdb_cursor_open(txn, history_dbi, &cursor); + if (rc != 0) { + mdb_txn_abort(txn); + return -1; + } + + /* Position cursor */ + key.mv_size = start_keylen; + key.mv_data = (void *)start_key; + + if (direction == HISTORY_DIR_BEFORE || direction == HISTORY_DIR_LATEST) { + /* For BEFORE/LATEST, we want to go backwards from the reference */ + rc = mdb_cursor_get(cursor, &key, &data, MDB_SET_RANGE); + log_write(LS_SYSTEM, L_INFO, 0, "history_query_internal: SET_RANGE rc=%d (%s)", + rc, rc == 0 ? "found" : (rc == MDB_NOTFOUND ? "not found" : mdb_strerror(rc))); + if (rc == MDB_NOTFOUND) { + /* Position at last entry */ + rc = mdb_cursor_get(cursor, &key, &data, MDB_LAST); + log_write(LS_SYSTEM, L_INFO, 0, "history_query_internal: MDB_LAST rc=%d", rc); + } else if (rc == 0) { + /* Move back one since SET_RANGE gives us >= */ + rc = mdb_cursor_get(cursor, &key, &data, MDB_PREV); + log_write(LS_SYSTEM, L_INFO, 0, "history_query_internal: MDB_PREV rc=%d", rc); + } + op = MDB_PREV; + } else { + /* For AFTER, go forwards from AFTER the reference */ + rc = mdb_cursor_get(cursor, &key, &data, MDB_SET_RANGE); + log_write(LS_SYSTEM, L_INFO, 0, "history_query_internal: AFTER SET_RANGE rc=%d", rc); + /* Skip any messages that match the reference timestamp prefix + * (AFTER means strictly after, not including the reference) */ + while (rc == 0 && key.mv_size >= (size_t)start_keylen && + memcmp(key.mv_data, start_key, start_keylen) == 0) { + rc = mdb_cursor_get(cursor, &key, &data, MDB_NEXT); + } + op = MDB_NEXT; + } + + /* Log cursor position after positioning */ + if (rc == 0) { + char key_preview[64]; + size_t preview_len = key.mv_size < 60 ? key.mv_size : 60; + memcpy(key_preview, key.mv_data, preview_len); + key_preview[preview_len] = '\0'; + /* Replace null bytes with dots for display */ + for (size_t i = 0; i < preview_len; i++) { + if (key_preview[i] == '\0') key_preview[i] = '.'; + } + log_write(LS_SYSTEM, L_INFO, 0, "history_query_internal: positioned at key='%s' (len=%zu)", + key_preview, key.mv_size); + } else { + log_write(LS_SYSTEM, L_INFO, 0, "history_query_internal: no position, rc=%d", rc); + } + + /* Iterate and collect messages */ + while (rc == 0 && count < limit) { + /* Check if still in target's range */ + if (key.mv_size < (size_t)target_prefix_len || + memcmp(key.mv_data, target_prefix, target_prefix_len) != 0) { + /* Outside target range */ + char key_preview[64]; + size_t preview_len = key.mv_size < 60 ? key.mv_size : 60; + memcpy(key_preview, key.mv_data, preview_len); + key_preview[preview_len] = '\0'; + for (size_t i = 0; i < preview_len; i++) { + if (key_preview[i] == '\0') key_preview[i] = '.'; + } + log_write(LS_SYSTEM, L_INFO, 0, "history_query_internal: key='%s' outside target range, breaking", + key_preview); + if (direction == HISTORY_DIR_BEFORE || direction == HISTORY_DIR_LATEST) + break; + /* For AFTER, move to next */ + rc = mdb_cursor_get(cursor, &key, &data, op); + continue; + } + + /* Allocate message */ + msg = (struct HistoryMessage *)MyMalloc(sizeof(struct HistoryMessage)); + if (!msg) + break; + memset(msg, 0, sizeof(*msg)); + + /* Parse key to get target, timestamp, msgid */ + if (parse_key(key.mv_data, key.mv_size, + msg->target, msg->timestamp, msg->msgid) != 0) { + MyFree(msg); + rc = mdb_cursor_get(cursor, &key, &data, op); + continue; + } + + /* Parse value */ + if (deserialize_message(data.mv_data, data.mv_size, msg) != 0) { + MyFree(msg); + rc = mdb_cursor_get(cursor, &key, &data, op); + continue; + } + + /* Add to list */ + msg->next = NULL; + if (direction == HISTORY_DIR_BEFORE || direction == HISTORY_DIR_LATEST) { + /* Prepend (we're going backwards) */ + msg->next = head; + head = msg; + if (!tail) + tail = msg; + } else { + /* Append */ + if (tail) + tail->next = msg; + else + head = msg; + tail = msg; + } + count++; + + rc = mdb_cursor_get(cursor, &key, &data, op); + } + + mdb_cursor_close(cursor); + mdb_txn_abort(txn); + + log_write(LS_SYSTEM, L_INFO, 0, "history_query_internal: returning count=%d for target='%s'", + count, target); + + *result = head; + return count; +} + +int history_query_before(const char *target, enum HistoryRefType ref_type, + const char *reference, int limit, + struct HistoryMessage **result) +{ + char keybuf[CHANNELLEN + HISTORY_TIMESTAMP_LEN + HISTORY_MSGID_LEN + 8]; + char timestamp[HISTORY_TIMESTAMP_LEN]; + int keylen; + + *result = NULL; + + /* Convert reference to Unix timestamp format */ + if (ref_type == HISTORY_REF_MSGID) { + if (history_msgid_to_timestamp(reference, timestamp) != 0) + return 0; /* msgid not found, return empty */ + reference = timestamp; + } else if (ref_type == HISTORY_REF_TIMESTAMP) { + /* Client sends ISO 8601, convert to Unix for lookup */ + if (history_iso_to_unix(reference, timestamp, sizeof(timestamp)) == 0) + reference = timestamp; + /* If conversion fails, assume it's already Unix format */ + } + + /* Build starting key */ + keylen = build_key(keybuf, sizeof(keybuf), target, reference, NULL); + if (keylen < 0) + return -1; + + log_write(LS_SYSTEM, L_INFO, 0, "history_query_before: target='%s' timestamp='%s' keylen=%d", + target, reference, keylen); + + return history_query_internal(target, keybuf, keylen, + HISTORY_DIR_BEFORE, limit, result); +} + +int history_query_after(const char *target, enum HistoryRefType ref_type, + const char *reference, int limit, + struct HistoryMessage **result) +{ + char keybuf[CHANNELLEN + HISTORY_TIMESTAMP_LEN + HISTORY_MSGID_LEN + 8]; + char timestamp[HISTORY_TIMESTAMP_LEN]; + int keylen; + + *result = NULL; + + /* Convert reference to Unix timestamp format */ + if (ref_type == HISTORY_REF_MSGID) { + if (history_msgid_to_timestamp(reference, timestamp) != 0) + return 0; + reference = timestamp; + } else if (ref_type == HISTORY_REF_TIMESTAMP) { + /* Client sends ISO 8601, convert to Unix for lookup */ + if (history_iso_to_unix(reference, timestamp, sizeof(timestamp)) == 0) + reference = timestamp; + /* If conversion fails, assume it's already Unix format */ + } + + keylen = build_key(keybuf, sizeof(keybuf), target, reference, NULL); + if (keylen < 0) + return -1; + + return history_query_internal(target, keybuf, keylen, + HISTORY_DIR_AFTER, limit, result); +} + +int history_query_latest(const char *target, enum HistoryRefType ref_type, + const char *reference, int limit, + struct HistoryMessage **result) +{ + char keybuf[CHANNELLEN + HISTORY_TIMESTAMP_LEN + 8]; + char timestamp[HISTORY_TIMESTAMP_LEN]; + int keylen; + + *result = NULL; + + if (ref_type == HISTORY_REF_NONE) { + /* LATEST * - start from end of target's range */ + /* Use a Unix timestamp far in the future (year 2999) */ + keylen = build_key(keybuf, sizeof(keybuf), target, "32503680000.000", NULL); + } else { + /* Convert reference to Unix timestamp format */ + if (ref_type == HISTORY_REF_MSGID) { + if (history_msgid_to_timestamp(reference, timestamp) != 0) + return 0; + reference = timestamp; + } else if (ref_type == HISTORY_REF_TIMESTAMP) { + /* Client sends ISO 8601, convert to Unix for lookup */ + if (history_iso_to_unix(reference, timestamp, sizeof(timestamp)) == 0) + reference = timestamp; + /* If conversion fails, assume it's already Unix format */ + } + keylen = build_key(keybuf, sizeof(keybuf), target, reference, NULL); + } + + if (keylen < 0) + return -1; + + return history_query_internal(target, keybuf, keylen, + HISTORY_DIR_LATEST, limit, result); +} + +int history_query_around(const char *target, enum HistoryRefType ref_type, + const char *reference, int limit, + struct HistoryMessage **result) +{ + struct HistoryMessage *before = NULL, *after = NULL, *ref_msg = NULL; + int half = limit / 2; + int count_before, count_after, count_ref = 0; + + *result = NULL; + + /* For msgid references, also look up the reference message itself. + * BEFORE and AFTER both exclude the reference, but AROUND should include it. + */ + if (ref_type == HISTORY_REF_MSGID) { + int rc = history_lookup_message(target, reference, &ref_msg); + if (rc == 0 && ref_msg) { + count_ref = 1; + log_write(LS_SYSTEM, L_INFO, 0, "history_query_around: found reference msg at ts=%s", + ref_msg->timestamp); + } + } + + /* Get messages before reference */ + count_before = history_query_before(target, ref_type, reference, half, &before); + if (count_before < 0) { + history_free_messages(before); + history_free_messages(ref_msg); + return -1; + } + + /* Get messages after reference (reduce limit by ref_msg if found) */ + count_after = history_query_after(target, ref_type, reference, + limit - count_before - count_ref, &after); + if (count_after < 0) { + history_free_messages(before); + history_free_messages(ref_msg); + history_free_messages(after); + return -1; + } + + /* Concatenate lists: before + ref_msg + after */ + if (before) { + struct HistoryMessage *tail = before; + while (tail->next) + tail = tail->next; + if (ref_msg) { + tail->next = ref_msg; + ref_msg->next = after; + } else { + tail->next = after; + } + *result = before; + } else if (ref_msg) { + ref_msg->next = after; + *result = ref_msg; + } else { + *result = after; + } + + return count_before + count_ref + count_after; +} + +int history_query_between(const char *target, + enum HistoryRefType ref_type1, const char *reference1, + enum HistoryRefType ref_type2, const char *reference2, + int limit, struct HistoryMessage **result) +{ + char timestamp1[HISTORY_TIMESTAMP_LEN]; + char timestamp2[HISTORY_TIMESTAMP_LEN]; + const char *ref1, *ref2; + char keybuf[CHANNELLEN + HISTORY_TIMESTAMP_LEN + 8]; + char end_prefix[CHANNELLEN + HISTORY_TIMESTAMP_LEN + 8]; + int keylen, end_prefix_len; + MDB_txn *txn; + MDB_cursor *cursor; + MDB_val key, data; + struct HistoryMessage *head = NULL, *tail = NULL, *msg; + int count = 0; + int rc; + + *result = NULL; + + if (!history_available) + return -1; + + /* Convert references to Unix timestamps */ + if (ref_type1 == HISTORY_REF_MSGID) { + if (history_msgid_to_timestamp(reference1, timestamp1) != 0) + return 0; + ref1 = timestamp1; + } else if (ref_type1 == HISTORY_REF_TIMESTAMP) { + /* Client sends ISO 8601, convert to Unix for lookup */ + if (history_iso_to_unix(reference1, timestamp1, sizeof(timestamp1)) == 0) + ref1 = timestamp1; + else + ref1 = reference1; /* Assume already Unix format */ + } else { + ref1 = reference1; + } + + if (ref_type2 == HISTORY_REF_MSGID) { + if (history_msgid_to_timestamp(reference2, timestamp2) != 0) + return 0; + ref2 = timestamp2; + } else if (ref_type2 == HISTORY_REF_TIMESTAMP) { + /* Client sends ISO 8601, convert to Unix for lookup */ + if (history_iso_to_unix(reference2, timestamp2, sizeof(timestamp2)) == 0) + ref2 = timestamp2; + else + ref2 = reference2; /* Assume already Unix format */ + } else { + ref2 = reference2; + } + + /* Ensure ref1 < ref2 */ + if (strcmp(ref1, ref2) > 0) { + const char *tmp = ref1; + ref1 = ref2; + ref2 = tmp; + } + + /* Build start and end keys */ + keylen = build_key(keybuf, sizeof(keybuf), target, ref1, NULL); + if (keylen < 0) + return -1; + + end_prefix_len = build_key(end_prefix, sizeof(end_prefix), target, ref2, NULL); + if (end_prefix_len < 0) + return -1; + + /* Query */ + rc = mdb_txn_begin(history_env, NULL, MDB_RDONLY, &txn); + if (rc != 0) + return -1; + + rc = mdb_cursor_open(txn, history_dbi, &cursor); + if (rc != 0) { + mdb_txn_abort(txn); + return -1; + } + + key.mv_size = keylen; + key.mv_data = keybuf; + rc = mdb_cursor_get(cursor, &key, &data, MDB_SET_RANGE); + + while (rc == 0 && count < limit) { + /* Check if past end */ + if (key.mv_size >= (size_t)end_prefix_len && + memcmp(key.mv_data, end_prefix, end_prefix_len) >= 0) + break; + + /* Parse and add message */ + msg = (struct HistoryMessage *)MyMalloc(sizeof(struct HistoryMessage)); + if (!msg) + break; + memset(msg, 0, sizeof(*msg)); + + if (parse_key(key.mv_data, key.mv_size, + msg->target, msg->timestamp, msg->msgid) != 0 || + deserialize_message(data.mv_data, data.mv_size, msg) != 0) { + MyFree(msg); + rc = mdb_cursor_get(cursor, &key, &data, MDB_NEXT); + continue; + } + + msg->next = NULL; + if (tail) + tail->next = msg; + else + head = msg; + tail = msg; + count++; + + rc = mdb_cursor_get(cursor, &key, &data, MDB_NEXT); + } + + mdb_cursor_close(cursor); + mdb_txn_abort(txn); + + *result = head; + return count; +} + +int history_query_targets(const char *timestamp1, const char *timestamp2, + int limit, struct HistoryTarget **result) +{ + MDB_txn *txn; + MDB_cursor *cursor; + MDB_val key, data; + struct HistoryTarget *head = NULL, *tail = NULL, *tgt; + char unix_ts1[HISTORY_TIMESTAMP_LEN]; + char unix_ts2[HISTORY_TIMESTAMP_LEN]; + const char *ts1, *ts2; + int count = 0; + int rc; + + *result = NULL; + + if (!history_available) + return -1; + + /* Convert client ISO timestamps to Unix for comparison */ + if (history_iso_to_unix(timestamp1, unix_ts1, sizeof(unix_ts1)) == 0) + ts1 = unix_ts1; + else + ts1 = timestamp1; /* Assume already Unix format */ + + if (history_iso_to_unix(timestamp2, unix_ts2, sizeof(unix_ts2)) == 0) + ts2 = unix_ts2; + else + ts2 = timestamp2; /* Assume already Unix format */ + + /* Ensure ts1 < ts2 */ + if (strcmp(ts1, ts2) > 0) { + const char *tmp = ts1; + ts1 = ts2; + ts2 = tmp; + } + + rc = mdb_txn_begin(history_env, NULL, MDB_RDONLY, &txn); + if (rc != 0) + return -1; + + rc = mdb_cursor_open(txn, history_targets_dbi, &cursor); + if (rc != 0) { + mdb_txn_abort(txn); + return -1; + } + + /* Iterate all targets */ + rc = mdb_cursor_get(cursor, &key, &data, MDB_FIRST); + while (rc == 0 && count < limit) { + /* Check if target's last message is in range */ + char last_ts[HISTORY_TIMESTAMP_LEN]; + if (data.mv_size >= sizeof(last_ts)) { + rc = mdb_cursor_get(cursor, &key, &data, MDB_NEXT); + continue; + } + memcpy(last_ts, data.mv_data, data.mv_size); + last_ts[data.mv_size] = '\0'; + + if (strcmp(last_ts, ts1) >= 0 && strcmp(last_ts, ts2) <= 0) { + tgt = (struct HistoryTarget *)MyMalloc(sizeof(struct HistoryTarget)); + if (!tgt) + break; + memset(tgt, 0, sizeof(*tgt)); + + if (key.mv_size > CHANNELLEN) { + MyFree(tgt); + rc = mdb_cursor_get(cursor, &key, &data, MDB_NEXT); + continue; + } + memcpy(tgt->target, key.mv_data, key.mv_size); + tgt->target[key.mv_size] = '\0'; + ircd_strncpy(tgt->last_timestamp, last_ts, sizeof(tgt->last_timestamp) - 1); + tgt->next = NULL; + + if (tail) + tail->next = tgt; + else + head = tgt; + tail = tgt; + count++; + } + + rc = mdb_cursor_get(cursor, &key, &data, MDB_NEXT); + } + + mdb_cursor_close(cursor); + mdb_txn_abort(txn); + + *result = head; + return count; +} + +void history_free_messages(struct HistoryMessage *list) +{ + struct HistoryMessage *msg, *next; + + for (msg = list; msg; msg = next) { + next = msg->next; + MyFree(msg); + } +} + +void history_free_targets(struct HistoryTarget *list) +{ + struct HistoryTarget *tgt, *next; + + for (tgt = list; tgt; tgt = next) { + next = tgt->next; + MyFree(tgt); + } +} + +int history_purge_old(unsigned long max_age_seconds) +{ + MDB_txn *txn; + MDB_cursor *cursor; + MDB_val key, data; + time_t cutoff_time; + char cutoff_ts[HISTORY_TIMESTAMP_LEN]; + char msg_target[CHANNELLEN + 1]; + char msg_timestamp[HISTORY_TIMESTAMP_LEN]; + char msg_msgid[HISTORY_MSGID_LEN]; + int deleted = 0; + int rc; + + if (!history_available) + return -1; + + if (max_age_seconds == 0) + return 0; /* Retention disabled */ + + /* Calculate cutoff timestamp (Unix format) */ + cutoff_time = time(NULL) - max_age_seconds; + ircd_snprintf(0, cutoff_ts, sizeof(cutoff_ts), "%lu.000", + (unsigned long)cutoff_time); + + Debug((DEBUG_DEBUG, "history: purging messages older than %s", cutoff_ts)); + + /* Begin write transaction */ + rc = mdb_txn_begin(history_env, NULL, 0, &txn); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "history: purge mdb_txn_begin failed: %s", + mdb_strerror(rc)); + return -1; + } + + /* Open cursor on messages database */ + rc = mdb_cursor_open(txn, history_dbi, &cursor); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "history: purge mdb_cursor_open failed: %s", + mdb_strerror(rc)); + mdb_txn_abort(txn); + return -1; + } + + /* Iterate from the beginning (oldest messages first due to key structure) */ + rc = mdb_cursor_get(cursor, &key, &data, MDB_FIRST); + while (rc == 0) { + /* Parse the key to get timestamp */ + if (parse_key(key.mv_data, key.mv_size, + msg_target, msg_timestamp, msg_msgid) == 0) { + /* Compare timestamp with cutoff */ + if (strcmp(msg_timestamp, cutoff_ts) < 0) { + /* Message is older than cutoff - delete it */ + + /* First delete from msgid index if we have a msgid */ + if (msg_msgid[0] != '\0') { + MDB_val msgid_key; + msgid_key.mv_size = strlen(msg_msgid); + msgid_key.mv_data = msg_msgid; + mdb_del(txn, history_msgid_dbi, &msgid_key, NULL); + } + + /* Delete the message using cursor */ + rc = mdb_cursor_del(cursor, 0); + if (rc == 0) { + deleted++; + } + + /* Move to next (cursor position is already at next after del) */ + rc = mdb_cursor_get(cursor, &key, &data, MDB_GET_CURRENT); + if (rc == MDB_NOTFOUND) { + /* Deleted last entry, try to get next */ + rc = mdb_cursor_get(cursor, &key, &data, MDB_NEXT); + } + continue; + } + } + + rc = mdb_cursor_get(cursor, &key, &data, MDB_NEXT); + } + + mdb_cursor_close(cursor); + + /* Commit the transaction */ + rc = mdb_txn_commit(txn); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "history: purge mdb_txn_commit failed: %s", + mdb_strerror(rc)); + return -1; + } + + if (deleted > 0) { + log_write(LS_SYSTEM, L_INFO, 0, "history: purged %d old messages (cutoff: %s)", + deleted, cutoff_ts); + } + + return deleted; +} + +int history_msgid_to_timestamp(const char *msgid, char *timestamp) +{ + MDB_txn *txn; + MDB_val key, data; + const char *sep; + int rc; + + log_write(LS_SYSTEM, L_INFO, 0, "history_msgid_to_timestamp: looking up msgid=%s", msgid); + + if (!history_available) { + log_write(LS_SYSTEM, L_INFO, 0, "history_msgid_to_timestamp: history not available"); + return -1; + } + + rc = mdb_txn_begin(history_env, NULL, MDB_RDONLY, &txn); + if (rc != 0) { + log_write(LS_SYSTEM, L_INFO, 0, "history_msgid_to_timestamp: txn_begin failed: %s", mdb_strerror(rc)); + return -1; + } + + key.mv_size = strlen(msgid); + key.mv_data = (void *)msgid; + + rc = mdb_get(txn, history_msgid_dbi, &key, &data); + mdb_txn_abort(txn); + + if (rc != 0) { + log_write(LS_SYSTEM, L_INFO, 0, "history_msgid_to_timestamp: mdb_get failed for msgid=%s: %s", msgid, mdb_strerror(rc)); + return -1; + } + + /* Value is target\0timestamp\0 - extract timestamp (exclude trailing separator) */ + sep = memchr(data.mv_data, KEY_SEP, data.mv_size); + if (!sep) + return -1; + + sep++; /* Skip separator after target */ + + /* Calculate copy length - exclude trailing KEY_SEP if present */ + { + size_t copy_len = (char *)data.mv_data + data.mv_size - sep; + /* build_key adds trailing KEY_SEP, exclude it */ + if (copy_len > 0 && sep[copy_len - 1] == KEY_SEP) + copy_len--; + if (copy_len >= HISTORY_TIMESTAMP_LEN) + return -1; + memcpy(timestamp, sep, copy_len); + timestamp[copy_len] = '\0'; + log_write(LS_SYSTEM, L_INFO, 0, "history_msgid_to_timestamp: extracted timestamp='%s' (len=%zu)", timestamp, copy_len); + } + + return 0; +} + +int history_lookup_message(const char *target, const char *msgid, + struct HistoryMessage **msg) +{ + MDB_txn *txn; + MDB_val key, data; + struct HistoryMessage *m; + char keybuf[CHANNELLEN + HISTORY_TIMESTAMP_LEN + HISTORY_MSGID_LEN + 8]; + char timestamp[HISTORY_TIMESTAMP_LEN]; + int keylen; + int rc; + + *msg = NULL; + + if (!history_available) + return -1; + + /* First, look up the msgid to get target and timestamp */ + rc = mdb_txn_begin(history_env, NULL, MDB_RDONLY, &txn); + if (rc != 0) + return -1; + + key.mv_size = strlen(msgid); + key.mv_data = (void *)msgid; + + rc = mdb_get(txn, history_msgid_dbi, &key, &data); + if (rc == MDB_NOTFOUND) { + mdb_txn_abort(txn); + return 1; /* Not found */ + } + if (rc != 0) { + mdb_txn_abort(txn); + return -1; + } + + /* Value is target\0timestamp\0 - extract timestamp (exclude trailing KEY_SEP) */ + { + const char *sep; + size_t copy_len; + sep = memchr(data.mv_data, KEY_SEP, data.mv_size); + if (!sep) { + mdb_txn_abort(txn); + return -1; + } + sep++; /* Skip separator after target */ + copy_len = (char *)data.mv_data + data.mv_size - sep; + /* build_key adds trailing KEY_SEP, exclude it */ + if (copy_len > 0 && sep[copy_len - 1] == KEY_SEP) + copy_len--; + if (copy_len >= HISTORY_TIMESTAMP_LEN) { + mdb_txn_abort(txn); + return -1; + } + memcpy(timestamp, sep, copy_len); + timestamp[copy_len] = '\0'; + } + + /* Build key for main database lookup: target\0timestamp\0msgid */ + keylen = build_key(keybuf, sizeof(keybuf), target, timestamp, msgid); + if (keylen < 0) { + mdb_txn_abort(txn); + return -1; + } + + key.mv_size = keylen; + key.mv_data = keybuf; + + rc = mdb_get(txn, history_dbi, &key, &data); + if (rc == MDB_NOTFOUND) { + mdb_txn_abort(txn); + return 1; /* Not found */ + } + if (rc != 0) { + mdb_txn_abort(txn); + return -1; + } + + /* Allocate and populate message structure */ + m = (struct HistoryMessage *)MyMalloc(sizeof(struct HistoryMessage)); + if (!m) { + mdb_txn_abort(txn); + return -1; + } + memset(m, 0, sizeof(*m)); + + /* Parse the message */ + if (deserialize_message(data.mv_data, data.mv_size, m) != 0) { + MyFree(m); + mdb_txn_abort(txn); + return -1; + } + + /* Fill in the key fields */ + ircd_strncpy(m->msgid, msgid, sizeof(m->msgid) - 1); + ircd_strncpy(m->target, target, sizeof(m->target) - 1); + ircd_strncpy(m->timestamp, timestamp, sizeof(m->timestamp) - 1); + m->next = NULL; + + mdb_txn_abort(txn); + *msg = m; + return 0; +} + +int history_delete_message(const char *target, const char *msgid) +{ + MDB_txn *txn; + MDB_val key, data; + char keybuf[CHANNELLEN + HISTORY_TIMESTAMP_LEN + HISTORY_MSGID_LEN + 8]; + char timestamp[HISTORY_TIMESTAMP_LEN]; + int keylen; + int rc; + + if (!history_available) + return -1; + + /* First, look up the msgid to get the timestamp */ + rc = mdb_txn_begin(history_env, NULL, 0, &txn); + if (rc != 0) + return -1; + + key.mv_size = strlen(msgid); + key.mv_data = (void *)msgid; + + rc = mdb_get(txn, history_msgid_dbi, &key, &data); + if (rc == MDB_NOTFOUND) { + mdb_txn_abort(txn); + return 1; /* Not found */ + } + if (rc != 0) { + mdb_txn_abort(txn); + return -1; + } + + /* Extract timestamp from value (target\0timestamp) */ + { + const char *sep; + sep = memchr(data.mv_data, KEY_SEP, data.mv_size); + if (!sep) { + mdb_txn_abort(txn); + return -1; + } + sep++; /* Skip separator */ + if ((size_t)((char *)data.mv_data + data.mv_size - sep) >= HISTORY_TIMESTAMP_LEN) { + mdb_txn_abort(txn); + return -1; + } + memcpy(timestamp, sep, (char *)data.mv_data + data.mv_size - sep); + timestamp[(char *)data.mv_data + data.mv_size - sep] = '\0'; + } + + /* Delete from msgid index */ + key.mv_size = strlen(msgid); + key.mv_data = (void *)msgid; + rc = mdb_del(txn, history_msgid_dbi, &key, NULL); + if (rc != 0 && rc != MDB_NOTFOUND) { + mdb_txn_abort(txn); + return -1; + } + + /* Build key for main database: target\0timestamp\0msgid */ + keylen = build_key(keybuf, sizeof(keybuf), target, timestamp, msgid); + if (keylen < 0) { + mdb_txn_abort(txn); + return -1; + } + + /* Delete from main message database */ + key.mv_size = keylen; + key.mv_data = keybuf; + rc = mdb_del(txn, history_dbi, &key, NULL); + if (rc != 0 && rc != MDB_NOTFOUND) { + mdb_txn_abort(txn); + return -1; + } + + rc = mdb_txn_commit(txn); + if (rc != 0) + return -1; + + return 0; +} + +int history_is_available(void) +{ + return history_available; +} + +/** Build a readmarker key from account and target. + * @param[out] key Output buffer. + * @param[in] keysize Size of output buffer. + * @param[in] account Account name. + * @param[in] target Channel or nick. + * @return Length of key, or -1 on error. + */ +static int build_readmarker_key(char *key, int keysize, + const char *account, const char *target) +{ + int pos = 0; + int len; + + /* Copy account */ + len = strlen(account); + if (pos + len + 1 >= keysize) return -1; + memcpy(key + pos, account, len); + pos += len; + key[pos++] = KEY_SEP; + + /* Copy target */ + len = strlen(target); + if (pos + len >= keysize) return -1; + memcpy(key + pos, target, len); + pos += len; + + return pos; +} + +int readmarker_get(const char *account, const char *target, char *timestamp) +{ + MDB_txn *txn; + MDB_val key, data; + char keybuf[ACCOUNTLEN + CHANNELLEN + 4]; + int keylen; + int rc; + + if (!history_available) + return -1; + + keylen = build_readmarker_key(keybuf, sizeof(keybuf), account, target); + if (keylen < 0) + return -1; + + rc = mdb_txn_begin(history_env, NULL, MDB_RDONLY, &txn); + if (rc != 0) + return -1; + + key.mv_size = keylen; + key.mv_data = keybuf; + + rc = mdb_get(txn, history_readmarkers_dbi, &key, &data); + mdb_txn_abort(txn); + + if (rc == MDB_NOTFOUND) + return 1; /* Not found */ + if (rc != 0) + return -1; + + /* Copy timestamp to output */ + if (data.mv_size >= HISTORY_TIMESTAMP_LEN) + return -1; + memcpy(timestamp, data.mv_data, data.mv_size); + timestamp[data.mv_size] = '\0'; + + return 0; +} + +int readmarker_set(const char *account, const char *target, const char *timestamp) +{ + MDB_txn *txn; + MDB_val key, data; + char keybuf[ACCOUNTLEN + CHANNELLEN + 4]; + char existing_ts[HISTORY_TIMESTAMP_LEN]; + int keylen; + int rc; + + if (!history_available) + return -1; + + keylen = build_readmarker_key(keybuf, sizeof(keybuf), account, target); + if (keylen < 0) + return -1; + + /* Begin write transaction */ + rc = mdb_txn_begin(history_env, NULL, 0, &txn); + if (rc != 0) + return -1; + + key.mv_size = keylen; + key.mv_data = keybuf; + + /* Check existing value */ + rc = mdb_get(txn, history_readmarkers_dbi, &key, &data); + if (rc == 0) { + /* Existing timestamp found - only update if new is greater */ + if (data.mv_size < sizeof(existing_ts)) { + memcpy(existing_ts, data.mv_data, data.mv_size); + existing_ts[data.mv_size] = '\0'; + if (strcmp(timestamp, existing_ts) <= 0) { + /* New timestamp is not greater, don't update */ + mdb_txn_abort(txn); + return 1; + } + } + } else if (rc != MDB_NOTFOUND) { + mdb_txn_abort(txn); + return -1; + } + + /* Store new timestamp */ + data.mv_size = strlen(timestamp); + data.mv_data = (void *)timestamp; + + rc = mdb_put(txn, history_readmarkers_dbi, &key, &data, 0); + if (rc != 0) { + mdb_txn_abort(txn); + return -1; + } + + rc = mdb_txn_commit(txn); + if (rc != 0) + return -1; + + return 0; +} + +/** Set history database map size. + * Must be called before history_init(). + * @param[in] size_mb Size in megabytes. + */ +void history_set_map_size(size_t size_mb) +{ + if (size_mb > 0) + history_map_size = size_mb * 1024 * 1024; +} + +/** Get history database map size. + * @return Current map size in bytes. + */ +size_t history_get_map_size(void) +{ + return history_map_size; +} + +void +history_report_stats(struct Client *to, const struct StatDesc *sd, char *param) +{ + MDB_stat stat; + MDB_envinfo info; + MDB_txn *txn; + int rc; + + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "H :CHATHISTORY Statistics"); + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "H : LMDB Backend: %s", + history_available ? "Available" : "Unavailable"); + + if (!history_available) { + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "H : (LMDB not initialized)"); + return; + } + + /* Get environment info */ + rc = mdb_env_info(history_env, &info); + if (rc == 0) { + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "H : Map size: %lu MB", + (unsigned long)(info.me_mapsize / (1024 * 1024))); + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "H : Last transaction ID: %lu", + (unsigned long)info.me_last_txnid); + } + + /* Get per-database stats */ + rc = mdb_txn_begin(history_env, NULL, MDB_RDONLY, &txn); + if (rc == 0) { + /* Main message database */ + rc = mdb_stat(txn, history_dbi, &stat); + if (rc == 0) { + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "H : Messages DB: %lu entries, depth %u", + (unsigned long)stat.ms_entries, stat.ms_depth); + } + + /* Message ID index */ + rc = mdb_stat(txn, history_msgid_dbi, &stat); + if (rc == 0) { + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "H : MsgID index: %lu entries", + (unsigned long)stat.ms_entries); + } + + /* Targets database */ + rc = mdb_stat(txn, history_targets_dbi, &stat); + if (rc == 0) { + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "H : Targets DB: %lu entries", + (unsigned long)stat.ms_entries); + } + + /* Read markers database */ + rc = mdb_stat(txn, history_readmarkers_dbi, &stat); + if (rc == 0) { + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "H : Read markers: %lu entries", + (unsigned long)stat.ms_entries); + } + + mdb_txn_abort(txn); + } +} + +#else /* !USE_LMDB */ + +/* Stub implementations when LMDB is not available */ +#include "history.h" +#include + +int history_init(const char *dbpath) +{ + (void)dbpath; + return -1; +} + +void history_shutdown(void) +{ +} + +int history_store_message(const char *msgid, const char *timestamp, + const char *target, const char *sender, + const char *account, enum HistoryMessageType type, + const char *content) +{ + (void)msgid; (void)timestamp; (void)target; (void)sender; + (void)account; (void)type; (void)content; + return -1; +} + +int history_query_before(const char *target, enum HistoryRefType ref_type, + const char *reference, int limit, + struct HistoryMessage **result) +{ + (void)target; (void)ref_type; (void)reference; (void)limit; + *result = NULL; + return -1; +} + +int history_query_after(const char *target, enum HistoryRefType ref_type, + const char *reference, int limit, + struct HistoryMessage **result) +{ + (void)target; (void)ref_type; (void)reference; (void)limit; + *result = NULL; + return -1; +} + +int history_query_latest(const char *target, enum HistoryRefType ref_type, + const char *reference, int limit, + struct HistoryMessage **result) +{ + (void)target; (void)ref_type; (void)reference; (void)limit; + *result = NULL; + return -1; +} + +int history_query_around(const char *target, enum HistoryRefType ref_type, + const char *reference, int limit, + struct HistoryMessage **result) +{ + (void)target; (void)ref_type; (void)reference; (void)limit; + *result = NULL; + return -1; +} + +int history_query_between(const char *target, + enum HistoryRefType ref_type1, const char *reference1, + enum HistoryRefType ref_type2, const char *reference2, + int limit, struct HistoryMessage **result) +{ + (void)target; (void)ref_type1; (void)reference1; + (void)ref_type2; (void)reference2; (void)limit; + *result = NULL; + return -1; +} + +int history_query_targets(const char *timestamp1, const char *timestamp2, + int limit, struct HistoryTarget **result) +{ + (void)timestamp1; (void)timestamp2; (void)limit; + *result = NULL; + return -1; +} + +void history_free_messages(struct HistoryMessage *list) +{ + (void)list; +} + +void history_free_targets(struct HistoryTarget *list) +{ + (void)list; +} + +int history_purge_old(unsigned long max_age_seconds) +{ + (void)max_age_seconds; + return -1; +} + +int history_msgid_to_timestamp(const char *msgid, char *timestamp) +{ + (void)msgid; (void)timestamp; + return -1; +} + +int history_lookup_message(const char *target, const char *msgid, + struct HistoryMessage **msg) +{ + (void)target; (void)msgid; + *msg = NULL; + return -1; +} + +int history_delete_message(const char *target, const char *msgid) +{ + (void)target; (void)msgid; + return -1; +} + +int history_is_available(void) +{ + return 0; +} + +int readmarker_get(const char *account, const char *target, char *timestamp) +{ + (void)account; (void)target; (void)timestamp; + return -1; +} + +int readmarker_set(const char *account, const char *target, const char *timestamp) +{ + (void)account; (void)target; (void)timestamp; + return -1; +} + +void history_set_map_size(size_t size_mb) +{ + (void)size_mb; +} + +size_t history_get_map_size(void) +{ + return 0; +} + +void +history_report_stats(struct Client *to, const struct StatDesc *sd, char *param) +{ + (void)sd; (void)param; + /* For stub version, we need send_reply - include the headers */ + /* This function is only callable if stats are registered, which requires LMDB */ +} + +#endif /* USE_LMDB */ diff --git a/ircd/ircd.c b/ircd/ircd.c index 39f897ad..6a1afb4b 100644 --- a/ircd/ircd.c +++ b/ircd/ircd.c @@ -27,18 +27,23 @@ #include "IPcheck.h" #include "class.h" #include "client.h" +#include "handlers.h" #include "crule.h" #include "destruct_event.h" #include "hash.h" +#include "history.h" #include "ircd_alloc.h" +#include "ircd_compress.h" #include "ircd_events.h" #include "ircd_features.h" #include "ircd_geoip.h" #include "ircd_log.h" +#include "ircd_log_async.h" #include "ircd_reply.h" #include "ircd_signal.h" #include "ircd_string.h" #include "ircd_crypt.h" +#include "thread_pool.h" #include "jupe.h" #include "list.h" #include "match.h" @@ -64,6 +69,8 @@ #include "userload.h" #include "version.h" #include "whowas.h" +#include "metadata.h" +#include "ml_storage.h" /* #include -- Now using assert in ircd_log.h */ #include @@ -121,6 +128,9 @@ static char *dbg_client; /**< Client specifier for chkconf */ static struct Timer connect_timer; /**< timer structure for try_connections() */ static struct Timer ping_timer; /**< timer structure for check_pings() */ static struct Timer destruct_event_timer; /**< timer structure for exec_expired_destruct_events() */ +static struct Timer history_purge_timer; /**< timer structure for history_purge_callback() */ +static struct Timer metadata_purge_timer; /**< timer structure for metadata_purge_callback() */ +static struct Timer ml_storage_timer; /**< timer structure for ml_storage_expire() */ /** Daemon information. */ static struct Daemon thisServer = { 0, 0, 0, 0, 0, 0, -1 }; @@ -128,6 +138,56 @@ static struct Daemon thisServer = { 0, 0, 0, 0, 0, 0, -1 }; /** Non-zero until we want to exit. */ int running = 1; +/** Counter for generating unique message IDs. */ +unsigned long MsgIdCounter = 0; + +/** SASL mechanism list received from services. Empty means use default. */ +char SaslMechanisms[SASL_MECHS_LEN] = ""; + +/** Set the SASL mechanism list (called when services announces mechanisms). + * @param[in] mechs Comma-separated list of mechanism names. + */ +void set_sasl_mechanisms(const char *mechs) +{ + if (mechs && *mechs) { + ircd_strncpy(SaslMechanisms, mechs, SASL_MECHS_LEN - 1); + SaslMechanisms[SASL_MECHS_LEN - 1] = '\0'; + } else { + SaslMechanisms[0] = '\0'; + } +} + +/** Get the SASL mechanism list for CAP LS. + * @return Mechanism list, or NULL if none set. + */ +const char* get_sasl_mechanisms(void) +{ + return SaslMechanisms[0] ? SaslMechanisms : NULL; +} + +/** VAPID public key received from services. Empty means webpush unavailable. */ +char VapidPublicKey[VAPID_KEY_LEN] = ""; + +/** Set the VAPID public key (called when services announces it). + * @param[in] key Base64url-encoded VAPID public key. + */ +void set_vapid_pubkey(const char *key) +{ + if (key && *key) { + ircd_strncpy(VapidPublicKey, key, VAPID_KEY_LEN - 1); + VapidPublicKey[VAPID_KEY_LEN - 1] = '\0'; + } else { + VapidPublicKey[0] = '\0'; + } +} + +/** Get the VAPID public key for ISUPPORT/CAP. + * @return VAPID public key, or NULL if none set. + */ +const char* get_vapid_pubkey(void) +{ + return VapidPublicKey[0] ? VapidPublicKey : NULL; +} /*---------------------------------------------------------------------------- * API: server_die @@ -180,6 +240,8 @@ void server_restart(const char *message) Debug((DEBUG_NOTICE, "Restarting server...")); flush_connections(0); + log_async_shutdown(); /* Flush pending log entries before shutdown */ + thread_pool_shutdown(); log_close(); close_connections(!(thisServer.bootopt & (BOOT_TTY | BOOT_DEBUG | BOOT_CHKCONF))); @@ -352,6 +414,15 @@ static void check_pings(struct Event* ev) { continue; } + /* Check for client batch timeout (draft/multiline) */ + check_client_batch_timeout(cptr); + + /* Check X3 availability (only once per ping cycle for services servers) */ + if (i == 0 && feature_bool(FEAT_METADATA_CACHE_ENABLED)) { + metadata_x3_check(); + metadata_expire_requests(); + } + Debug((DEBUG_DEBUG, "check_pings(%s)=status:%s current: %d", cli_name(cptr), IsPingSent(cptr) ? "[Ping Sent]" : "[]", @@ -454,10 +525,73 @@ static void check_pings(struct Event* ev) { Debug((DEBUG_DEBUG, "[%i] check_pings() again in %is", CurrentTime, next_check-CurrentTime)); - + timer_add(&ping_timer, check_pings, 0, TT_ABSOLUTE, next_check); } +/** Periodic callback to purge old history messages. + * Runs every hour to enforce CHATHISTORY_RETENTION policy. + * @param[in] ev Timer event (ignored). + */ +static void history_purge_callback(struct Event* ev) +{ + int retention_days; + unsigned long max_age_seconds; + + (void)ev; /* unused */ + + /* Only run if chathistory is enabled */ + if (!feature_bool(FEAT_CAP_draft_chathistory)) + return; + + if (!history_is_available()) + return; + + retention_days = feature_int(FEAT_CHATHISTORY_RETENTION); + if (retention_days <= 0) + return; /* Retention disabled */ + + max_age_seconds = (unsigned long)retention_days * 24 * 60 * 60; + history_purge_old(max_age_seconds); +} + +/** Periodic callback to purge expired metadata cache entries. + * Runs at METADATA_PURGE_FREQUENCY to enforce METADATA_CACHE_TTL. + * @param[in] ev Timer event (ignored). + */ +static void metadata_purge_callback(struct Event* ev) +{ + (void)ev; /* unused */ + + /* Only run if metadata caching is enabled */ + if (!feature_bool(FEAT_METADATA_CACHE_ENABLED)) + return; + + if (!metadata_lmdb_is_available()) + return; + + /* TTL of 0 disables purging */ + if (feature_int(FEAT_METADATA_CACHE_TTL) <= 0) + return; + + metadata_account_purge_expired(); +} + +/** Periodic callback to expire old multiline storage entries. + * Runs every 5 minutes to enforce MULTILINE_STORAGE_TTL. + * @param[in] ev Timer event (ignored). + */ +static void ml_storage_callback(struct Event* ev) +{ + (void)ev; /* unused */ + + /* Only run if multiline storage is enabled */ + if (!feature_bool(FEAT_MULTILINE_STORAGE_ENABLED)) + return; + + ml_storage_expire(); +} + /** Parse command line arguments. * Global variables are updated to reflect the arguments. @@ -694,6 +828,23 @@ int main(int argc, char **argv) { setup_signals(); feature_init(); /* initialize features... */ + + /* Initialize thread pool for async operations (bcrypt, etc.) + * Must be after feature_init() to read THREAD_POOL_SIZE config */ + if (thread_pool_init(feature_int(FEAT_THREAD_POOL_SIZE)) < 0) { + log_write(LS_SYSTEM, L_WARNING, 0, + "Thread pool init failed; password verification will be synchronous"); + } + + /* Initialize async logging if enabled + * Must be after feature_init() to read ASYNC_LOGGING config */ + if (feature_bool(FEAT_ASYNC_LOGGING)) { + if (log_async_init(0) < 0) { + log_write(LS_SYSTEM, L_WARNING, 0, + "Async logging init failed; logging will be synchronous"); + } + } + init_isupport(); /* initialize RPL_ISUPPORT... */ log_init(*argv); set_nomem_handler(outofmemory); @@ -748,6 +899,13 @@ int main(int argc, char **argv) { timer_add(timer_init(&connect_timer), try_connections, 0, TT_RELATIVE, 1); timer_add(timer_init(&ping_timer), check_pings, 0, TT_RELATIVE, 1); timer_add(timer_init(&destruct_event_timer), exec_expired_destruct_events, 0, TT_PERIODIC, 60); + timer_add(timer_init(&history_purge_timer), history_purge_callback, 0, TT_PERIODIC, 3600); /* Run every hour */ + timer_add(timer_init(&metadata_purge_timer), metadata_purge_callback, 0, TT_PERIODIC, + feature_int(FEAT_METADATA_PURGE_FREQUENCY)); /* Default: hourly */ + timer_add(timer_init(&ml_storage_timer), ml_storage_callback, 0, TT_PERIODIC, 300); /* Run every 5 minutes */ + + /* Initialize multiline storage */ + ml_storage_init(); CurrentTime = time(NULL); @@ -774,6 +932,36 @@ int main(int argc, char **argv) { load_tunefile(); geoip_init(); +#ifdef USE_LMDB + /* Initialize chathistory database */ + if (feature_bool(FEAT_CAP_draft_chathistory)) { + /* Set map size from feature before init */ + history_set_map_size((size_t)feature_int(FEAT_HISTORY_MAP_SIZE_MB)); +#ifdef USE_ZSTD + /* Initialize compression with configured threshold and level */ + compress_init((size_t)feature_int(FEAT_COMPRESS_THRESHOLD), + feature_int(FEAT_COMPRESS_LEVEL)); +#endif + if (history_init(feature_str(FEAT_CHATHISTORY_DB)) != 0) { + log_write(LS_SYSTEM, L_WARNING, 0, + "Failed to initialize chathistory database, feature disabled"); + } + } + + /* Initialize metadata LMDB database */ + if (feature_bool(FEAT_CAP_draft_metadata_2)) { + if (metadata_lmdb_init(feature_str(FEAT_METADATA_DB)) != 0) { + log_write(LS_SYSTEM, L_WARNING, 0, + "Failed to initialize metadata database"); + } + } +#endif + +#ifdef USE_LIBGIT2 + /* Start gitsync timer after config is loaded */ + gitsync_start_timer(); +#endif + Debug((DEBUG_NOTICE, "Server ready...")); log_write(LS_SYSTEM, L_NOTICE, 0, "Server Ready"); diff --git a/ircd/ircd_compress.c b/ircd/ircd_compress.c new file mode 100644 index 00000000..43254222 --- /dev/null +++ b/ircd/ircd_compress.c @@ -0,0 +1,159 @@ +/* + * IRC - Internet Relay Chat, ircd/ircd_compress.c + * Copyright (C) 2024 AfterNET Development Team + * + * Zstandard compression support for LMDB-backed storage. + * Provides transparent compression for chathistory and metadata. + */ +#include "config.h" + +#ifdef USE_ZSTD + +#include "ircd_compress.h" +#include "ircd_log.h" +#include "ircd_snprintf.h" +#include "s_debug.h" +#include +#include + +/** Current compression threshold */ +static size_t compression_threshold = COMPRESS_THRESHOLD_DEFAULT; + +/** Current compression level */ +static int compression_level = COMPRESS_LEVEL_DEFAULT; + +/** Initialize compression subsystem */ +void compress_init(size_t threshold, int level) +{ + if (threshold > 0) + compression_threshold = threshold; + if (level >= 1 && level <= ZSTD_maxCLevel()) + compression_level = level; + + Debug((DEBUG_INFO, "Compression initialized: threshold=%zu, level=%d", + compression_threshold, compression_level)); +} + +/** Check if data appears to be compressed */ +int is_compressed(const unsigned char *data, size_t len) +{ + return (len > 1 && data[0] == COMPRESS_MAGIC); +} + +/** Compress data if it exceeds the threshold */ +int compress_data(const unsigned char *input, size_t input_len, + unsigned char *output, size_t output_size, size_t *output_len) +{ + size_t compressed_size; + + /* Don't compress small values */ + if (input_len < compression_threshold) { + if (input_len > output_size) + return -1; + memcpy(output, input, input_len); + *output_len = input_len; + return 0; + } + + /* Need room for magic byte + compressed data */ + if (output_size < 2) + return -1; + + /* Compress with zstd */ + compressed_size = ZSTD_compress(output + 1, output_size - 1, + input, input_len, compression_level); + + if (ZSTD_isError(compressed_size)) { + log_write(LS_SYSTEM, L_WARNING, 0, + "compress_data: zstd compression failed: %s", + ZSTD_getErrorName(compressed_size)); + /* Fall back to uncompressed */ + if (input_len > output_size) + return -1; + memcpy(output, input, input_len); + *output_len = input_len; + return 0; + } + + /* Only use compression if it actually saves space */ + if (compressed_size + 1 >= input_len) { + if (input_len > output_size) + return -1; + memcpy(output, input, input_len); + *output_len = input_len; + return 0; + } + + /* Add magic byte */ + output[0] = COMPRESS_MAGIC; + *output_len = compressed_size + 1; + return 1; +} + +/** Decompress data if it has compression magic byte */ +int decompress_data(const unsigned char *input, size_t input_len, + unsigned char *output, size_t output_size, size_t *output_len) +{ + size_t decompressed_size; + + /* Check for magic byte */ + if (!is_compressed(input, input_len)) { + if (input_len > output_size) { + log_write(LS_SYSTEM, L_ERROR, 0, + "decompress_data: output buffer too small"); + return -1; + } + memcpy(output, input, input_len); + *output_len = input_len; + return 0; + } + + /* Decompress (skip magic byte) */ + decompressed_size = ZSTD_decompress(output, output_size, + input + 1, input_len - 1); + + if (ZSTD_isError(decompressed_size)) { + log_write(LS_SYSTEM, L_ERROR, 0, + "decompress_data: zstd decompression failed: %s", + ZSTD_getErrorName(decompressed_size)); + return -1; + } + + /* Safety check */ + if (decompressed_size > COMPRESS_MAX_UNCOMPRESSED) { + log_write(LS_SYSTEM, L_ERROR, 0, + "decompress_data: decompressed size too large: %zu", + decompressed_size); + return -1; + } + + *output_len = decompressed_size; + return 1; +} + +/** Get current compression threshold */ +size_t compress_get_threshold(void) +{ + return compression_threshold; +} + +/** Set compression threshold */ +void compress_set_threshold(size_t threshold) +{ + compression_threshold = threshold; +} + +/** Get current compression level */ +int compress_get_level(void) +{ + return compression_level; +} + +/** Set compression level */ +void compress_set_level(int level) +{ + if (level >= 1 && level <= ZSTD_maxCLevel()) + compression_level = level; +} + +#endif /* USE_ZSTD */ diff --git a/ircd/ircd_crypt.c b/ircd/ircd_crypt.c index f2d61fef..90a345ee 100644 --- a/ircd/ircd_crypt.c +++ b/ircd/ircd_crypt.c @@ -56,6 +56,7 @@ #include "ircd_crypt_plain.h" #include "ircd_crypt_smd5.h" #include "ircd_crypt_bcrypt.h" +#include "ircd_crypt_pbkdf2.h" /* #include -- Now using assert in ircd_log.h */ #include @@ -260,6 +261,8 @@ void ircd_crypt_init(void) ircd_register_crypt_plain(); ircd_register_crypt_native(); ircd_register_crypt_bcrypt(); + ircd_register_crypt_pbkdf2(); + ircd_register_crypt_pbkdf2_sha512(); return; } diff --git a/ircd/ircd_crypt_async.c b/ircd/ircd_crypt_async.c new file mode 100644 index 00000000..ea7cd876 --- /dev/null +++ b/ircd/ircd_crypt_async.c @@ -0,0 +1,160 @@ +/* + * IRC - Internet Relay Chat, ircd/ircd_crypt_async.c + * Copyright (C) 2025 AfterNET Development Team + * + * 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 2, 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. + */ + +/** + * @file + * @brief Async password verification wrapper. + * + * Provides non-blocking password verification by offloading CPU-intensive + * bcrypt/PBKDF2 hashing to the thread pool. This prevents the main event + * loop from blocking during SASL authentication or OPER commands. + */ + +#include "config.h" +#include "ircd_crypt.h" +#include "thread_pool.h" +#include "ircd_alloc.h" +#include "ircd_string.h" +#include "s_debug.h" + +#include + +/** Context for async password verification */ +struct crypt_verify_ctx { + char *password; /**< Copy of plaintext password */ + char *hash; /**< Copy of stored hash */ + crypt_verify_callback callback; /**< User callback */ + void *user_ctx; /**< User context */ +}; + +/** + * Work function - runs in thread pool worker. + * This is the CPU-intensive part that we're offloading. + */ +static void *crypt_verify_work(void *arg) +{ + struct crypt_verify_ctx *ctx = arg; + int *result; + const char *computed; + + result = (int *)MyMalloc(sizeof(int)); + if (!result) { + *result = CRYPT_VERIFY_ERROR; + return result; + } + + /* Call the blocking crypt function - this is the slow part */ + computed = ircd_crypt(ctx->password, ctx->hash); + + if (!computed) { + *result = CRYPT_VERIFY_ERROR; + } else if (strcmp(computed, ctx->hash) == 0) { + *result = CRYPT_VERIFY_MATCH; + } else { + *result = CRYPT_VERIFY_NOMATCH; + } + + Debug((DEBUG_DEBUG, "crypt_verify_work: result=%d for hash prefix %.8s...", + *result, ctx->hash)); + + return result; +} + +/** + * Completion callback - runs in main thread via thread_pool_poll(). + * Invokes the user's callback with the verification result. + */ +static void crypt_verify_done(void *result, void *arg) +{ + struct crypt_verify_ctx *ctx = arg; + int *presult = result; + int final_result; + + if (presult) { + final_result = *presult; + MyFree(presult); + } else { + /* NULL result indicates cancellation or error */ + final_result = CRYPT_VERIFY_ERROR; + } + + /* Invoke user callback */ + if (ctx->callback) { + ctx->callback(final_result, ctx->user_ctx); + } + + /* Clean up context */ + MyFree(ctx->password); + MyFree(ctx->hash); + MyFree(ctx); +} + +/** + * Asynchronously verify a password against a hash. + */ +int ircd_crypt_verify_async(const char *password, const char *hash, + crypt_verify_callback callback, void *ctx) +{ + struct crypt_verify_ctx *vctx; + + /* Check if thread pool is available */ + if (!thread_pool_is_running()) { + Debug((DEBUG_DEBUG, "ircd_crypt_verify_async: thread pool not running, " + "falling back to sync")); + return -1; + } + + if (!password || !hash || !callback) { + Debug((DEBUG_ERROR, "ircd_crypt_verify_async: invalid parameters")); + return -1; + } + + /* Allocate verification context */ + vctx = (struct crypt_verify_ctx *)MyMalloc(sizeof(struct crypt_verify_ctx)); + if (!vctx) { + return -1; + } + + /* Copy strings - they may be freed before the async operation completes */ + DupString(vctx->password, password); + DupString(vctx->hash, hash); + vctx->callback = callback; + vctx->user_ctx = ctx; + + /* Submit to thread pool */ + if (thread_pool_submit(crypt_verify_work, vctx, crypt_verify_done, vctx) < 0) { + Debug((DEBUG_ERROR, "ircd_crypt_verify_async: thread_pool_submit failed")); + MyFree(vctx->password); + MyFree(vctx->hash); + MyFree(vctx); + return -1; + } + + Debug((DEBUG_DEBUG, "ircd_crypt_verify_async: submitted verification for " + "hash prefix %.8s...", hash)); + return 0; +} + +/** + * Check if async password verification is available. + */ +int ircd_crypt_async_available(void) +{ + return thread_pool_is_running(); +} diff --git a/ircd/ircd_crypt_pbkdf2.c b/ircd/ircd_crypt_pbkdf2.c new file mode 100644 index 00000000..04fe6c2f --- /dev/null +++ b/ircd/ircd_crypt_pbkdf2.c @@ -0,0 +1,493 @@ +/* + * IRC - Internet Relay Chat, ircd/ircd_crypt_pbkdf2.c + * + * 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. + */ +/** + * @file + * @brief PBKDF2 password hashing routines (SHA256 and SHA512) + * + * Provides PBKDF2-SHA256 and PBKDF2-SHA512 password hashing using OpenSSL 3.0+ EVP_KDF API. + * Hash formats: + * SHA256: $PBKDF2$iterations$base64_salt$base64_hash + * SHA512: $PBKDF2-SHA512$iterations$base64_salt$base64_hash + * + * This is compatible with Keycloak credential import and follows + * OWASP recommendations for password hashing (100,000+ iterations). + */ +#include "config.h" +#include "ircd_crypt.h" +#include "ircd_crypt_pbkdf2.h" +#include "ircd_log.h" +#include "s_debug.h" +#include "ircd_alloc.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +/* PBKDF2 parameters */ +#define PBKDF2_SALT_LEN 16 /* 128 bits */ +#define PBKDF2_SHA256_LEN 32 /* 256 bits (SHA256 output) */ +#define PBKDF2_SHA512_LEN 64 /* 512 bits (SHA512 output) */ +#define PBKDF2_ITERATIONS 100000 /* OWASP 2023 minimum recommendation */ + +/* Token for mechanism detection */ +#define PBKDF2_TOKEN "$PBKDF2$" +#define PBKDF2_TOKEN_SIZE 8 +#define PBKDF2_SHA512_TOKEN "$PBKDF2-SHA512$" +#define PBKDF2_SHA512_TOKEN_SIZE 15 + +/* Standard base64 alphabet */ +static const char base64_chars[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +/** Encode binary data to base64 (no padding, no newlines) + * @param input Binary input data + * @param input_len Length of input + * @param output Output buffer (must be at least (input_len * 4 / 3) + 4 bytes) + * @return Length of encoded string + */ +static int base64_encode(const unsigned char *input, int input_len, char *output) +{ + int i, j; + unsigned int triplet; + + for (i = 0, j = 0; i < input_len; ) { + triplet = (i < input_len ? input[i++] : 0) << 16; + triplet |= (i < input_len ? input[i++] : 0) << 8; + triplet |= (i < input_len ? input[i++] : 0); + + output[j++] = base64_chars[(triplet >> 18) & 0x3f]; + output[j++] = base64_chars[(triplet >> 12) & 0x3f]; + output[j++] = base64_chars[(triplet >> 6) & 0x3f]; + output[j++] = base64_chars[triplet & 0x3f]; + } + + /* Handle padding - we don't add '=' padding chars, just adjust length */ + if (input_len % 3 == 1) j -= 2; + else if (input_len % 3 == 2) j -= 1; + + output[j] = '\0'; + return j; +} + +/** Decode base64 to binary + * @param input Base64 input string + * @param output Output buffer (must be at least (strlen(input) * 3 / 4) bytes) + * @return Length of decoded data, or -1 on error + */ +static int base64_decode(const char *input, unsigned char *output) +{ + int i, j, len; + unsigned int triplet; + unsigned char c, d[4]; + const char *p; + + len = strlen(input); + for (i = 0, j = 0; i < len; ) { + /* Decode 4 base64 chars to 3 bytes */ + for (int k = 0; k < 4; k++) { + if (i < len) { + c = input[i++]; + p = strchr(base64_chars, c); + if (p == NULL) return -1; + d[k] = p - base64_chars; + } else { + d[k] = 0; + } + } + + triplet = (d[0] << 18) | (d[1] << 12) | (d[2] << 6) | d[3]; + output[j++] = (triplet >> 16) & 0xff; + output[j++] = (triplet >> 8) & 0xff; + output[j++] = triplet & 0xff; + } + + /* Adjust for missing padding */ + if (len % 4 == 2) j -= 2; + else if (len % 4 == 3) j -= 1; + + return j; +} + +/** Perform PBKDF2 key derivation with specified digest + * @param password The password to hash + * @param salt The salt bytes + * @param salt_len Length of salt + * @param iterations Number of iterations + * @param output Output buffer for derived key + * @param output_len Desired output length + * @param digest_name Digest algorithm ("SHA256" or "SHA512") + * @return 1 on success, 0 on failure + */ +static int do_pbkdf2(const char *password, const unsigned char *salt, + size_t salt_len, int iterations, + unsigned char *output, size_t output_len, + const char *digest_name) +{ + EVP_KDF *kdf = NULL; + EVP_KDF_CTX *ctx = NULL; + OSSL_PARAM params[5]; + int ret = 0; + + kdf = EVP_KDF_fetch(NULL, "PBKDF2", NULL); + if (kdf == NULL) { + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2: EVP_KDF_fetch failed")); + goto cleanup; + } + + ctx = EVP_KDF_CTX_new(kdf); + if (ctx == NULL) { + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2: EVP_KDF_CTX_new failed")); + goto cleanup; + } + + params[0] = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, (char *)digest_name, 0); + params[1] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_PASSWORD, + (void *)password, strlen(password)); + params[2] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, + (void *)salt, salt_len); + params[3] = OSSL_PARAM_construct_int(OSSL_KDF_PARAM_ITER, &iterations); + params[4] = OSSL_PARAM_construct_end(); + + if (EVP_KDF_derive(ctx, output, output_len, params) != 1) { + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2: EVP_KDF_derive failed")); + goto cleanup; + } + + ret = 1; + +cleanup: + if (ctx) EVP_KDF_CTX_free(ctx); + if (kdf) EVP_KDF_free(kdf); + return ret; +} + +/** Parse existing PBKDF2 hash to extract parameters + * @param hash The hash string (format: $PBKDF2$iter$salt$hash or just $PBKDF2$) + * @param iterations Output: iteration count + * @param salt Output: decoded salt bytes + * @param salt_len Output: salt length + * @param stored_hash Output: decoded hash bytes + * @param hash_len Output: hash length + * @return 1 if valid existing hash, 0 if new hash request + */ +static int parse_pbkdf2_hash(const char *hash, int *iterations, + unsigned char *salt, int *salt_len, + unsigned char *stored_hash, int *hash_len) +{ + const char *p; + char iter_str[16]; + char salt_b64[64]; + char hash_b64[128]; + int i; + + /* Check for token */ + if (strncmp(hash, PBKDF2_TOKEN, PBKDF2_TOKEN_SIZE) != 0) + return 0; + + p = hash + PBKDF2_TOKEN_SIZE; + + /* If nothing after token, this is a new hash request */ + if (*p == '\0') + return 0; + + /* Parse iterations */ + for (i = 0; *p && *p != '$' && i < 15; i++, p++) + iter_str[i] = *p; + iter_str[i] = '\0'; + + if (*p != '$') return 0; + p++; + + *iterations = atoi(iter_str); + if (*iterations <= 0) return 0; + + /* Parse salt (base64) */ + for (i = 0; *p && *p != '$' && i < 63; i++, p++) + salt_b64[i] = *p; + salt_b64[i] = '\0'; + + if (*p != '$') return 0; + p++; + + /* Parse hash (base64) */ + for (i = 0; *p && i < 127; i++, p++) + hash_b64[i] = *p; + hash_b64[i] = '\0'; + + /* Decode salt and hash */ + *salt_len = base64_decode(salt_b64, salt); + if (*salt_len < 0) return 0; + + *hash_len = base64_decode(hash_b64, stored_hash); + if (*hash_len < 0) return 0; + + return 1; +} + +/** Parse existing PBKDF2-SHA512 hash to extract parameters + * @param hash The hash string (format: $PBKDF2-SHA512$iter$salt$hash) + * @param iterations Output: iteration count + * @param salt Output: decoded salt bytes + * @param salt_len Output: salt length + * @param stored_hash Output: decoded hash bytes + * @param hash_len Output: hash length + * @return 1 if valid existing hash, 0 if new hash request + */ +static int parse_pbkdf2_sha512_hash(const char *hash, int *iterations, + unsigned char *salt, int *salt_len, + unsigned char *stored_hash, int *hash_len) +{ + const char *p; + char iter_str[16]; + char salt_b64[64]; + char hash_b64[128]; + int i; + + /* Check for token */ + if (strncmp(hash, PBKDF2_SHA512_TOKEN, PBKDF2_SHA512_TOKEN_SIZE) != 0) + return 0; + + p = hash + PBKDF2_SHA512_TOKEN_SIZE; + + /* If nothing after token, this is a new hash request */ + if (*p == '\0') + return 0; + + /* Parse iterations */ + for (i = 0; *p && *p != '$' && i < 15; i++, p++) + iter_str[i] = *p; + iter_str[i] = '\0'; + + if (*p != '$') return 0; + p++; + + *iterations = atoi(iter_str); + if (*iterations <= 0) return 0; + + /* Parse salt (base64) */ + for (i = 0; *p && *p != '$' && i < 63; i++, p++) + salt_b64[i] = *p; + salt_b64[i] = '\0'; + + if (*p != '$') return 0; + p++; + + /* Parse hash (base64) */ + for (i = 0; *p && i < 127; i++, p++) + hash_b64[i] = *p; + hash_b64[i] = '\0'; + + /* Decode salt and hash */ + *salt_len = base64_decode(salt_b64, salt); + if (*salt_len < 0) return 0; + + *hash_len = base64_decode(hash_b64, stored_hash); + if (*hash_len < 0) return 0; + + return 1; +} + +/** PBKDF2-SHA256 password hashing function + * @param key The password to hash + * @param salt The salt (if starts with $PBKDF2$iter$..., verify; else generate new) + * @return The hashed password, or NULL on failure + * + * When called with an existing PBKDF2 hash as salt, extracts parameters and + * re-hashes for verification. When called with just $PBKDF2$ or anything else, + * generates a new hash with random salt. + */ +const char* ircd_crypt_pbkdf2(const char* key, const char* salt) +{ + static char result[256]; + unsigned char salt_bytes[PBKDF2_SALT_LEN]; + unsigned char hash_bytes[PBKDF2_SHA256_LEN]; + unsigned char stored_hash[PBKDF2_SHA256_LEN]; + char salt_b64[32]; + char hash_b64[64]; + int iterations = PBKDF2_ITERATIONS; + int salt_len, hash_len; + + assert(NULL != key); + assert(NULL != salt); + + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2: key = [hidden]")); + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2: salt = %s", salt)); + + /* Check if this is verification of existing hash */ + if (parse_pbkdf2_hash(salt, &iterations, salt_bytes, &salt_len, + stored_hash, &hash_len)) { + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2: verifying existing hash")); + + /* Re-derive the hash with the same parameters */ + if (!do_pbkdf2(key, salt_bytes, salt_len, iterations, + hash_bytes, PBKDF2_SHA256_LEN, "SHA256")) { + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2: derivation failed")); + return NULL; + } + + /* Return the same format for comparison */ + base64_encode(salt_bytes, salt_len, salt_b64); + base64_encode(hash_bytes, PBKDF2_SHA256_LEN, hash_b64); + snprintf(result, sizeof(result), "%s%d$%s$%s", + PBKDF2_TOKEN, iterations, salt_b64, hash_b64); + } + else { + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2: generating new hash")); + + /* Generate random salt */ + if (RAND_bytes(salt_bytes, PBKDF2_SALT_LEN) != 1) { + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2: RAND_bytes failed")); + return NULL; + } + + /* Derive the hash */ + if (!do_pbkdf2(key, salt_bytes, PBKDF2_SALT_LEN, iterations, + hash_bytes, PBKDF2_SHA256_LEN, "SHA256")) { + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2: derivation failed")); + return NULL; + } + + /* Format: $PBKDF2$iterations$base64_salt$base64_hash */ + base64_encode(salt_bytes, PBKDF2_SALT_LEN, salt_b64); + base64_encode(hash_bytes, PBKDF2_SHA256_LEN, hash_b64); + snprintf(result, sizeof(result), "%s%d$%s$%s", + PBKDF2_TOKEN, iterations, salt_b64, hash_b64); + } + + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2: result = %s", result)); + return result; +} + +/** Register the PBKDF2-SHA256 mechanism */ +void ircd_register_crypt_pbkdf2(void) +{ + crypt_mech_t* crypt_mech; + + if ((crypt_mech = (crypt_mech_t*)MyMalloc(sizeof(crypt_mech_t))) == NULL) + { + Debug((DEBUG_MALLOC, "Could not allocate space for crypt_pbkdf2")); + return; + } + + crypt_mech->mechname = "pbkdf2"; + crypt_mech->shortname = "crypt_pbkdf2"; + crypt_mech->description = "PBKDF2-SHA256 password hash ($PBKDF2$)."; + crypt_mech->crypt_function = &ircd_crypt_pbkdf2; + crypt_mech->crypt_token = PBKDF2_TOKEN; + crypt_mech->crypt_token_size = PBKDF2_TOKEN_SIZE; + + ircd_crypt_register_mech(crypt_mech); +} + +/** PBKDF2-SHA512 password hashing function + * @param key The password to hash + * @param salt The salt (if starts with $PBKDF2-SHA512$iter$..., verify; else generate new) + * @return The hashed password, or NULL on failure + * + * When called with an existing PBKDF2-SHA512 hash as salt, extracts parameters and + * re-hashes for verification. When called with just $PBKDF2-SHA512$ or anything else, + * generates a new hash with random salt. + */ +const char* ircd_crypt_pbkdf2_sha512(const char* key, const char* salt) +{ + static char result[512]; + unsigned char salt_bytes[PBKDF2_SALT_LEN]; + unsigned char hash_bytes[PBKDF2_SHA512_LEN]; + unsigned char stored_hash[PBKDF2_SHA512_LEN]; + char salt_b64[32]; + char hash_b64[128]; + int iterations = PBKDF2_ITERATIONS; + int salt_len, hash_len; + + assert(NULL != key); + assert(NULL != salt); + + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2_sha512: key = [hidden]")); + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2_sha512: salt = %s", salt)); + + /* Check if this is verification of existing hash */ + if (parse_pbkdf2_sha512_hash(salt, &iterations, salt_bytes, &salt_len, + stored_hash, &hash_len)) { + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2_sha512: verifying existing hash")); + + /* Re-derive the hash with the same parameters */ + if (!do_pbkdf2(key, salt_bytes, salt_len, iterations, + hash_bytes, PBKDF2_SHA512_LEN, "SHA512")) { + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2_sha512: derivation failed")); + return NULL; + } + + /* Return the same format for comparison */ + base64_encode(salt_bytes, salt_len, salt_b64); + base64_encode(hash_bytes, PBKDF2_SHA512_LEN, hash_b64); + snprintf(result, sizeof(result), "%s%d$%s$%s", + PBKDF2_SHA512_TOKEN, iterations, salt_b64, hash_b64); + } + else { + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2_sha512: generating new hash")); + + /* Generate random salt */ + if (RAND_bytes(salt_bytes, PBKDF2_SALT_LEN) != 1) { + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2_sha512: RAND_bytes failed")); + return NULL; + } + + /* Derive the hash */ + if (!do_pbkdf2(key, salt_bytes, PBKDF2_SALT_LEN, iterations, + hash_bytes, PBKDF2_SHA512_LEN, "SHA512")) { + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2_sha512: derivation failed")); + return NULL; + } + + /* Format: $PBKDF2-SHA512$iterations$base64_salt$base64_hash */ + base64_encode(salt_bytes, PBKDF2_SALT_LEN, salt_b64); + base64_encode(hash_bytes, PBKDF2_SHA512_LEN, hash_b64); + snprintf(result, sizeof(result), "%s%d$%s$%s", + PBKDF2_SHA512_TOKEN, iterations, salt_b64, hash_b64); + } + + Debug((DEBUG_DEBUG, "ircd_crypt_pbkdf2_sha512: result = %s", result)); + return result; +} + +/** Register the PBKDF2-SHA512 mechanism */ +void ircd_register_crypt_pbkdf2_sha512(void) +{ + crypt_mech_t* crypt_mech; + + if ((crypt_mech = (crypt_mech_t*)MyMalloc(sizeof(crypt_mech_t))) == NULL) + { + Debug((DEBUG_MALLOC, "Could not allocate space for crypt_pbkdf2_sha512")); + return; + } + + crypt_mech->mechname = "pbkdf2-sha512"; + crypt_mech->shortname = "crypt_pbkdf2_sha512"; + crypt_mech->description = "PBKDF2-SHA512 password hash ($PBKDF2-SHA512$)."; + crypt_mech->crypt_function = &ircd_crypt_pbkdf2_sha512; + crypt_mech->crypt_token = PBKDF2_SHA512_TOKEN; + crypt_mech->crypt_token_size = PBKDF2_SHA512_TOKEN_SIZE; + + ircd_crypt_register_mech(crypt_mech); +} diff --git a/ircd/ircd_features.c b/ircd/ircd_features.c index adf32201..25e09a9e 100644 --- a/ircd/ircd_features.c +++ b/ircd/ircd_features.c @@ -27,8 +27,10 @@ #include "class.h" #include "client.h" #include "hash.h" +#include "history.h" #include "ircd.h" #include "ircd_alloc.h" +#include "ircd_compress.h" #include "ircd_geoip.h" #include "ircd_log.h" #include "ircd_reply.h" @@ -432,6 +434,22 @@ set_isupport_network(void) add_isupport_s("NETWORK", feature_str(FEAT_NETWORK)); } +#ifdef USE_ZSTD +/** Update compression threshold from feature. */ +static void +feature_notify_compress_threshold(void) +{ + compress_set_threshold((size_t)feature_int(FEAT_COMPRESS_THRESHOLD)); +} + +/** Update compression level from feature. */ +static void +feature_notify_compress_level(void) +{ + compress_set_level(feature_int(FEAT_COMPRESS_LEVEL)); +} +#endif /* USE_ZSTD */ + /** Sets a feature to the given value. * @param[in] from Client trying to set parameters. * @param[in] fields Array of parameters to set. @@ -531,6 +549,8 @@ static struct FeatureDesc { F_B(HUB, 0, 0, feature_notify_hub), F_B(WALLOPS_OPER_ONLY, 0, 0, 0), F_B(NODNS, 0, 0, 0), + F_B(TCP_NODELAY_C2S, 0, 0, 0), + F_B(TCP_NODELAY_S2S, 0, 0, 0), F_N(RANDOM_SEED, FEAT_NODISP, random_seed_set, 0, 0, 0, 0, 0, 0), F_S(DEFAULT_LIST_PARAM, FEAT_NULL, 0, list_set_default), F_I(NICKNAMEHISTORYLENGTH, 0, 800, whowas_realloc), @@ -575,6 +595,8 @@ static struct FeatureDesc { F_I(TOS_SERVER, 0, 0x08, 0), F_I(TOS_CLIENT, 0, 0x08, 0), F_I(POLLS_PER_LOOP, 0, 200, 0), + F_I(THREAD_POOL_SIZE, 0, 4, 0), + F_B(ASYNC_LOGGING, 0, 0, 0), F_I(IRCD_RES_RETRIES, 0, 2, 0), F_I(IRCD_RES_TIMEOUT, 0, 4, 0), F_I(AUTH_TIMEOUT, 0, 9, 0), @@ -783,6 +805,7 @@ static struct FeatureDesc { F_B(SSL_NOSSLV3, 0, 1, 0), F_B(SSL_NOTLSV1, 0, 1, 0), F_S(SSL_CIPHERS, FEAT_NULL, 0, 0), + F_B(CERT_EXPIRY_TRACKING, 0, 1, 0), /* ZLINE FEAT_'s */ F_B(DISABLE_ZLINES, 0, 0, 0), @@ -796,6 +819,83 @@ static struct FeatureDesc { F_B(CAP_away_notify, 0, 1, 0), F_B(CAP_account_notify, 0, 1, 0), F_B(CAP_sasl, 0, 1, 0), + F_B(CAP_cap_notify, 0, 1, 0), + F_B(CAP_server_time, 0, 1, 0), + F_B(CAP_echo_message, 0, 1, 0), + F_B(CAP_account_tag, 0, 1, 0), + F_B(CAP_chghost, 0, 1, 0), + F_B(CAP_invite_notify, 0, 1, 0), + F_B(CAP_labeled_response, 0, 1, 0), + F_B(CAP_batch, 0, 1, 0), + F_B(CAP_setname, 0, 1, 0), + F_B(CAP_standard_replies, 0, 1, 0), + F_B(CAP_message_tags, 0, 1, 0), + F_B(CAP_draft_no_implicit_names, 0, 1, 0), + F_B(CAP_draft_extended_isupport, 0, 1, 0), + F_B(CAP_draft_pre_away, 0, 1, 0), + F_B(CAP_draft_multiline, 0, 1, 0), + F_B(CAP_draft_chathistory, 0, 1, 0), + F_B(CAP_draft_event_playback, 0, 0, 0), + F_B(CAP_draft_message_redaction, 0, 0, 0), + F_B(CAP_draft_account_registration, 0, 0, 0), + F_S(REGISTER_SERVER, 0, "*", 0), + F_B(CAP_draft_read_marker, 0, 0, 0), + F_B(CAP_draft_channel_rename, 0, 0, 0), + F_B(CAP_draft_metadata_2, 0, 0, 0), + F_B(CAP_draft_webpush, 0, 0, 0), + F_I(METADATA_MAX_KEYS, 0, 20, 0), + F_I(METADATA_MAX_VALUE_BYTES, 0, 300, 0), /* Limited by 512-byte IRC message size */ + F_I(METADATA_MAX_SUBS, 0, 50, 0), + F_I(METADATA_RATE_LIMIT, 0, 10, 0), + F_I(REDACT_WINDOW, 0, 300, 0), + F_I(REDACT_OPER_WINDOW, 0, 0, 0), + F_B(REDACT_CHANOP_OTHERS, 0, 1, 0), + F_I(CHATHISTORY_MAX, 0, 100, 0), + F_B(CHATHISTORY_PRIVATE, 0, 0, 0), + F_I(CHATHISTORY_PRIVATE_CONSENT, 0, 2, 0), + F_B(CHATHISTORY_ADVERTISE_PM, 0, 0, 0), + F_B(CHATHISTORY_PM_NOTICE, 0, 0, 0), + F_S(CHATHISTORY_DB, 0, "history", 0), + F_I(CHATHISTORY_RETENTION, 0, 7, 0), + F_B(CHATHISTORY_FEDERATION, 0, 1, 0), + F_I(CHATHISTORY_TIMEOUT, 0, 5, 0), + F_B(CHATHISTORY_STRICT_TIMESTAMPS, 0, 0, 0), + F_I(MULTILINE_MAX_BYTES, 0, 4096, 0), + F_I(MULTILINE_MAX_LINES, 0, 24, 0), + F_I(MULTILINE_LAG_DISCOUNT, 0, 50, 0), + F_I(MULTILINE_CHANNEL_LAG_DISCOUNT, 0, 75, 0), + F_I(MULTILINE_MAX_LAG, 0, 30, 0), + F_B(MULTILINE_RECIPIENT_DISCOUNT, 0, 1, 0), + F_B(MULTILINE_ECHO_PROTECT, 0, 1, 0), + F_I(MULTILINE_ECHO_MAX_FACTOR, 0, 2, 0), + F_I(MULTILINE_LEGACY_THRESHOLD, 0, 3, 0), + F_I(MULTILINE_LEGACY_MAX_LINES, 0, 5, 0), + F_B(MULTILINE_FALLBACK_NOTIFY, 0, 1, 0), + F_B(MULTILINE_STORAGE_ENABLED, 0, 0, 0), + F_I(MULTILINE_STORAGE_TTL, 0, 3600, 0), + F_I(MULTILINE_STORAGE_MAX, 0, 10000, 0), + F_I(BATCH_RATE_LIMIT, 0, 10, 0), + F_I(CLIENT_BATCH_TIMEOUT, 0, 30, 0), + F_B(DRAFT_WEBSOCKET, 0, 1, 0), + F_I(WEBSOCKET_RECVQ, 0, 8192, 0), + F_S(WEBSOCKET_ORIGIN, 0, "", 0), + F_B(MSGID, 0, 1, 0), + F_B(P10_MESSAGE_TAGS, 0, 0, 0), + F_B(PRESENCE_AGGREGATION, 0, 0, 0), + F_S(AWAY_STAR_MSG, FEAT_NULL, "Away", 0), + F_I(AWAY_THROTTLE, 0, 0, 0), + F_B(METADATA_CACHE_ENABLED, 0, 1, 0), + F_I(METADATA_X3_TIMEOUT, 0, 60, 0), + F_I(METADATA_QUEUE_SIZE, 0, 1000, 0), + F_B(METADATA_BURST, 0, 1, 0), + F_S(METADATA_DB, 0, "metadata", 0), + F_I(METADATA_CACHE_TTL, 0, 14400, 0), + F_I(METADATA_PURGE_FREQUENCY, 0, 3600, 0), +#ifdef USE_ZSTD + F_I(COMPRESS_THRESHOLD, 0, 256, feature_notify_compress_threshold), + F_I(COMPRESS_LEVEL, 0, 3, feature_notify_compress_level), +#endif + F_I(HISTORY_MAP_SIZE_MB, 0, 1024, 0), #ifdef USE_SSL F_B(CAP_tls, 0, 1, 0), #endif diff --git a/ircd/ircd_lexer.l b/ircd/ircd_lexer.l index 7c2e96c7..0f1645a4 100644 --- a/ircd/ircd_lexer.l +++ b/ircd/ircd_lexer.l @@ -157,6 +157,7 @@ static struct lexer_token { TOKEN(SWHOIS), TOKEN(ENABLEOPTIONS), TOKEN(TRUSTACCOUNT), + TOKEN(WEBSOCKET), #undef TOKEN { "ssl", SSLTOK }, { "administrator", ADMIN }, diff --git a/ircd/ircd_log.c b/ircd/ircd_log.c index c0121d8a..a19257e7 100644 --- a/ircd/ircd_log.c +++ b/ircd/ircd_log.c @@ -27,6 +27,7 @@ #include "config.h" #include "ircd_log.h" +#include "ircd_log_async.h" #include "client.h" #include "ircd_alloc.h" #include "ircd_features.h" @@ -442,8 +443,27 @@ log_vwrite(enum LogSys subsys, enum LogLevel severity, unsigned int flags, vector[2].iov_base = (void*) "\n"; /* terminate lines with a \n */ vector[2].iov_len = 1; - /* write it out to the log file */ - (void)!writev(desc->file->fd, vector, 3); + /* Try async logging if available and enabled */ + if (log_async_available() && feature_bool(FEAT_ASYNC_LOGGING)) { + /* Build combined log entry for async write */ + char async_buf[LOG_BUFSIZE + 64]; + int async_len; + + async_len = ircd_snprintf(0, async_buf, sizeof(async_buf), "%s%s\n", + timebuf, buf); + + /* Use async write - if it fails or falls back, that's OK */ + if (log_async_write(desc->file->fd, + (flags & LOG_DOSYSLOG) ? (ldata->syslog | desc->facility) : 0, + async_buf, async_len) == 0) { + /* Successfully queued async - skip sync write and syslog */ + flags &= ~(LOG_DOFILELOG | LOG_DOSYSLOG); + } + } + + /* Sync fallback: write it out to the log file */ + if (flags & LOG_DOFILELOG) + (void)!writev(desc->file->fd, vector, 3); } /* oh yeah, syslog it too... */ diff --git a/ircd/ircd_log_async.c b/ircd/ircd_log_async.c new file mode 100644 index 00000000..32689aa3 --- /dev/null +++ b/ircd/ircd_log_async.c @@ -0,0 +1,386 @@ +/* + * IRC - Internet Relay Chat, ircd/ircd_log_async.c + * Copyright (C) 2025 AfterNET Development Team + * + * 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 2, 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. + */ + +/** + * @file + * @brief Async logging implementation. + * + * Uses a ring buffer with a dedicated writer thread to offload log I/O + * from the main event loop. The design prioritizes: + * + * 1. Minimal main thread impact - only atomic operations and memcpy + * 2. Graceful degradation - falls back to sync if buffer full + * 3. Reliable delivery - flush ensures all entries written + * 4. Clean shutdown - drains buffer before exit + */ + +#include "config.h" + +#ifdef HAVE_PTHREAD + +#include "ircd_log_async.h" +#include "ircd_alloc.h" +#include "s_debug.h" + +#include +#include +#include +#include +#include + +/** Ring buffer state */ +static struct { + struct log_async_entry *entries; /**< Ring buffer of log entries */ + unsigned int size; /**< Number of entries in buffer */ + unsigned int head; /**< Next write position (producer) */ + unsigned int tail; /**< Next read position (consumer) */ + + pthread_t writer_thread; /**< Writer thread handle */ + pthread_mutex_t mutex; /**< Protects head/tail updates */ + pthread_cond_t not_empty; /**< Signals data available */ + pthread_cond_t not_full; /**< Signals space available */ + pthread_cond_t flushed; /**< Signals flush complete */ + + int running; /**< 1 if writer thread active */ + int flush_requested; /**< 1 if flush in progress */ + + /* Statistics */ + unsigned long written; /**< Total entries written */ + unsigned long dropped; /**< Entries dropped due to full buffer */ +} log_async; + +/** + * Calculate number of entries in the buffer. + * Must be called with mutex held. + */ +static inline unsigned int buffer_count(void) +{ + if (log_async.head >= log_async.tail) + return log_async.head - log_async.tail; + return log_async.size - log_async.tail + log_async.head; +} + +/** + * Check if buffer is full. + * Must be called with mutex held. + */ +static inline int buffer_full(void) +{ + return ((log_async.head + 1) % log_async.size) == log_async.tail; +} + +/** + * Check if buffer is empty. + * Must be called with mutex held. + */ +static inline int buffer_empty(void) +{ + return log_async.head == log_async.tail; +} + +/** + * Writer thread main loop. + * Continuously drains the ring buffer to file/syslog. + */ +static void *log_writer_thread(void *arg) +{ + struct log_async_entry *entry; + int was_empty_before_flush; + + (void)arg; /* unused */ + + pthread_mutex_lock(&log_async.mutex); + + while (log_async.running || !buffer_empty()) { + /* Wait for data */ + while (buffer_empty() && log_async.running) { + /* If flush requested and buffer is empty, signal completion */ + if (log_async.flush_requested) { + log_async.flush_requested = 0; + pthread_cond_signal(&log_async.flushed); + } + pthread_cond_wait(&log_async.not_empty, &log_async.mutex); + } + + /* Check if we should exit */ + if (buffer_empty() && !log_async.running) + break; + + /* Get entry from buffer */ + entry = &log_async.entries[log_async.tail]; + log_async.tail = (log_async.tail + 1) % log_async.size; + was_empty_before_flush = buffer_empty(); + + /* Signal that there's space now */ + pthread_cond_signal(&log_async.not_full); + + /* Release mutex during I/O */ + pthread_mutex_unlock(&log_async.mutex); + + /* Perform the actual I/O (blocking is OK here - we're in worker thread) */ + if (entry->fd >= 0 && entry->len > 0) { + (void)!write(entry->fd, entry->message, entry->len); + } + + if (entry->syslog_priority > 0) { + syslog(entry->syslog_priority, "%.*s", + entry->len > 0 ? entry->len : (int)strlen(entry->message), + entry->message); + } + + pthread_mutex_lock(&log_async.mutex); + log_async.written++; + + /* Check if this completed a flush */ + if (was_empty_before_flush && log_async.flush_requested) { + log_async.flush_requested = 0; + pthread_cond_signal(&log_async.flushed); + } + } + + /* Final flush check before exit */ + if (log_async.flush_requested) { + log_async.flush_requested = 0; + pthread_cond_signal(&log_async.flushed); + } + + pthread_mutex_unlock(&log_async.mutex); + return NULL; +} + +/** + * Initialize the async logging system. + */ +int log_async_init(int buffer_size) +{ + pthread_attr_t attr; + + if (log_async.running) + return 0; /* Already initialized */ + + /* Use default if not specified */ + if (buffer_size <= 0) + buffer_size = LOG_ASYNC_BUFFER_SIZE_DEFAULT; + + /* Allocate ring buffer */ + log_async.entries = (struct log_async_entry *) + MyCalloc(buffer_size, sizeof(struct log_async_entry)); + if (!log_async.entries) { + Debug((DEBUG_ERROR, "log_async_init: failed to allocate buffer")); + return -1; + } + + log_async.size = buffer_size; + log_async.head = 0; + log_async.tail = 0; + log_async.written = 0; + log_async.dropped = 0; + log_async.flush_requested = 0; + + /* Initialize synchronization primitives */ + if (pthread_mutex_init(&log_async.mutex, NULL) != 0) { + Debug((DEBUG_ERROR, "log_async_init: mutex init failed")); + MyFree(log_async.entries); + log_async.entries = NULL; + return -1; + } + + if (pthread_cond_init(&log_async.not_empty, NULL) != 0 || + pthread_cond_init(&log_async.not_full, NULL) != 0 || + pthread_cond_init(&log_async.flushed, NULL) != 0) { + Debug((DEBUG_ERROR, "log_async_init: cond init failed")); + pthread_mutex_destroy(&log_async.mutex); + MyFree(log_async.entries); + log_async.entries = NULL; + return -1; + } + + /* Create writer thread */ + log_async.running = 1; + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); + + if (pthread_create(&log_async.writer_thread, &attr, + log_writer_thread, NULL) != 0) { + Debug((DEBUG_ERROR, "log_async_init: thread create failed: %s", + strerror(errno))); + log_async.running = 0; + pthread_cond_destroy(&log_async.flushed); + pthread_cond_destroy(&log_async.not_full); + pthread_cond_destroy(&log_async.not_empty); + pthread_mutex_destroy(&log_async.mutex); + MyFree(log_async.entries); + log_async.entries = NULL; + pthread_attr_destroy(&attr); + return -1; + } + + pthread_attr_destroy(&attr); + + Debug((DEBUG_DEBUG, "log_async_init: initialized with buffer size %d", + buffer_size)); + return 0; +} + +/** + * Shut down the async logging system. + */ +void log_async_shutdown(void) +{ + if (!log_async.running) + return; + + Debug((DEBUG_DEBUG, "log_async_shutdown: flushing %u pending entries", + buffer_count())); + + /* Signal writer thread to exit */ + pthread_mutex_lock(&log_async.mutex); + log_async.running = 0; + pthread_cond_signal(&log_async.not_empty); + pthread_mutex_unlock(&log_async.mutex); + + /* Wait for writer thread to finish */ + pthread_join(log_async.writer_thread, NULL); + + /* Cleanup */ + pthread_cond_destroy(&log_async.flushed); + pthread_cond_destroy(&log_async.not_full); + pthread_cond_destroy(&log_async.not_empty); + pthread_mutex_destroy(&log_async.mutex); + MyFree(log_async.entries); + log_async.entries = NULL; + + Debug((DEBUG_DEBUG, "log_async_shutdown: completed, %lu written, %lu dropped", + log_async.written, log_async.dropped)); +} + +/** + * Queue a log entry for async write. + */ +int log_async_write(int fd, int syslog_priority, const char *message, int len) +{ + struct log_async_entry *entry; + int result = 0; + + if (!log_async.running || !message) + return -1; + + /* Clamp length to max entry size */ + if (len > LOG_ASYNC_MAX_ENTRY - 1) + len = LOG_ASYNC_MAX_ENTRY - 1; + if (len < 0) + len = strlen(message); + if (len > LOG_ASYNC_MAX_ENTRY - 1) + len = LOG_ASYNC_MAX_ENTRY - 1; + + pthread_mutex_lock(&log_async.mutex); + + /* Check if buffer is full */ + if (buffer_full()) { + /* Try waiting briefly for space */ + struct timespec timeout; + clock_gettime(CLOCK_REALTIME, &timeout); + timeout.tv_nsec += 1000000; /* 1ms wait */ + if (timeout.tv_nsec >= 1000000000) { + timeout.tv_sec++; + timeout.tv_nsec -= 1000000000; + } + + if (pthread_cond_timedwait(&log_async.not_full, &log_async.mutex, + &timeout) != 0) { + /* Still full after wait - fall back to sync write */ + log_async.dropped++; + pthread_mutex_unlock(&log_async.mutex); + + /* Sync fallback: write directly */ + if (fd >= 0 && len > 0) + (void)!write(fd, message, len); + if (syslog_priority > 0) + syslog(syslog_priority, "%.*s", len, message); + + return 1; /* Indicate sync fallback */ + } + } + + /* Add entry to buffer */ + entry = &log_async.entries[log_async.head]; + entry->fd = fd; + entry->syslog_priority = syslog_priority; + entry->len = len; + memcpy(entry->message, message, len); + entry->message[len] = '\0'; + + log_async.head = (log_async.head + 1) % log_async.size; + + /* Signal writer thread */ + pthread_cond_signal(&log_async.not_empty); + pthread_mutex_unlock(&log_async.mutex); + + return result; +} + +/** + * Flush all pending log entries. + */ +void log_async_flush(void) +{ + if (!log_async.running) + return; + + pthread_mutex_lock(&log_async.mutex); + + if (!buffer_empty()) { + log_async.flush_requested = 1; + pthread_cond_signal(&log_async.not_empty); + + /* Wait for flush to complete */ + while (log_async.flush_requested && log_async.running) { + pthread_cond_wait(&log_async.flushed, &log_async.mutex); + } + } + + pthread_mutex_unlock(&log_async.mutex); +} + +/** + * Check if async logging is available and enabled. + */ +int log_async_available(void) +{ + return log_async.running; +} + +/** + * Get async logging statistics. + */ +void log_async_stats(unsigned long *queued, unsigned long *written, + unsigned long *dropped) +{ + pthread_mutex_lock(&log_async.mutex); + if (queued) + *queued = buffer_count(); + if (written) + *written = log_async.written; + if (dropped) + *dropped = log_async.dropped; + pthread_mutex_unlock(&log_async.mutex); +} + +#endif /* HAVE_PTHREAD */ diff --git a/ircd/ircd_parser.y b/ircd/ircd_parser.y index 41b18129..f962cfed 100644 --- a/ircd/ircd_parser.y +++ b/ircd/ircd_parser.y @@ -234,6 +234,7 @@ static void free_slist(struct SLink **link) { %token SWHOIS %token ENABLEOPTIONS %token TRUSTACCOUNT +%token WEBSOCKET /* and now a lot of privileges... */ %token TPRIV_CHAN_LIMIT TPRIV_MODE_LCHAN TPRIV_DEOP_LCHAN TPRIV_WALK_LCHAN %token TPRIV_LOCAL_KILL TPRIV_REHASH TPRIV_RESTART TPRIV_DIE @@ -977,7 +978,7 @@ portblock: PORT '{' portitems '}' ';' { port = 0; }; portitems: portitem portitems | portitem; -portitem: portnumber | portvhost | portvhostnumber | portmask | portserver | porthidden | portssl; +portitem: portnumber | portvhost | portvhostnumber | portmask | portserver | porthidden | portssl | portwebsocket; portnumber: PORT '=' address_family NUMBER ';' { if ($4 < 1 || $4 > 65535) { @@ -1048,6 +1049,14 @@ portssl: SSLTOK '=' YES ';' FlagClr(&listen_flags, LISTEN_SSL); } +portwebsocket: WEBSOCKET '=' YES ';' +{ + FlagSet(&listen_flags, LISTEN_WEBSOCKET); +} | WEBSOCKET '=' NO ';' +{ + FlagClr(&listen_flags, LISTEN_WEBSOCKET); +} + clientblock: CLIENT { maxlinks = 65535; diff --git a/ircd/ircd_relay.c b/ircd/ircd_relay.c index 55d43abe..8b930ea7 100644 --- a/ircd/ircd_relay.c +++ b/ircd/ircd_relay.c @@ -46,14 +46,17 @@ #include "config.h" #include "ircd_relay.h" +#include "capab.h" #include "channel.h" #include "client.h" #include "hash.h" +#include "history.h" #include "ircd.h" #include "ircd_chattr.h" #include "ircd_features.h" #include "ircd_log.h" #include "ircd_reply.h" +#include "ircd_snprintf.h" #include "ircd_string.h" #include "match.h" #include "msg.h" @@ -63,11 +66,14 @@ #include "s_misc.h" #include "s_user.h" #include "send.h" +#include "metadata.h" /* #include -- Now using assert in ircd_log.h */ #include #include #include +#include +#include /* * This file contains message relaying functions for client and server @@ -77,6 +83,166 @@ * but not introduce any IsOper/IsUser/MyUser/IsServer etc. stuff. */ +#ifdef USE_LMDB +/** Store a channel message in the history database. + * Stores the message with the provided msgid and timestamp. + * @param[in] sptr Client that sent the message. + * @param[in] chptr Target channel. + * @param[in] text Message content. + * @param[in] type Message type (HISTORY_PRIVMSG or HISTORY_NOTICE). + * @param[in] msgid Message ID (same one sent to clients via echo-message). + * @param[in] timestamp ISO 8601 timestamp. + */ +static void store_channel_history(struct Client *sptr, struct Channel *chptr, + const char *text, enum HistoryMessageType type, + const char *msgid, const char *timestamp) +{ + char sender[HISTORY_SENDER_LEN]; + const char *account; + + if (!history_is_available()) + return; + + /* Check if chathistory feature is enabled */ + if (!feature_bool(FEAT_CAP_draft_chathistory)) + return; + + /* Build sender string: nick!user@host */ + if (cli_user(sptr)) + ircd_snprintf(0, sender, sizeof(sender), "%s!%s@%s", + cli_name(sptr), + cli_user(sptr)->username, + cli_user(sptr)->host); + else + ircd_strncpy(sender, cli_name(sptr), sizeof(sender) - 1); + + /* Get account name if logged in */ + account = (cli_user(sptr) && cli_user(sptr)->account[0]) + ? cli_user(sptr)->account : NULL; + + /* Store in database */ + history_store_message(msgid, timestamp, chptr->chname, sender, + account, type, text); +} + +/** Get PM history preference for a client. + * @param[in] cptr Client to check. + * @return 1 if opted in, -1 if explicitly opted out, 0 if no preference. + */ +static int get_pm_history_pref(struct Client *cptr) +{ + struct MetadataEntry *entry; + + if (!cptr || !IsUser(cptr)) + return 0; + + entry = metadata_get_client(cptr, "chathistory.pm"); + if (!entry) + return 0; /* No preference set */ + + /* Empty value or "0" = explicit opt-out */ + if (!entry->value || !entry->value[0] || entry->value[0] == '0') + return -1; + + /* Any other value = opt-in */ + return 1; +} + +/** Check consent for PM history storage. + * @param[in] sender Message sender. + * @param[in] recipient Message recipient. + * @return 1 if consent granted, 0 otherwise. + */ +static int pm_history_consent(struct Client *sender, struct Client *recipient) +{ + int mode = feature_int(FEAT_CHATHISTORY_PRIVATE_CONSENT); + int sender_pref = get_pm_history_pref(sender); + int recipient_pref = get_pm_history_pref(recipient); + + if (mode == 0) { + /* Global mode - store unless either party explicitly opted out */ + return (sender_pref != -1 && recipient_pref != -1); + } + + if (mode == 1) { + /* Single-party - store if either opted in (and neither opted out) */ + if (sender_pref == -1 || recipient_pref == -1) + return 0; + return (sender_pref == 1 || recipient_pref == 1); + } + + if (mode == 2) { + /* Multi-party - store only if both explicitly opted in */ + return (sender_pref == 1 && recipient_pref == 1); + } + + return 0; +} + +/** Store a private (DM) message in the history database. + * Uses a consistent target format: sorted pair of nicks as "nick1:nick2". + * @param[in] sptr Client that sent the message. + * @param[in] acptr Target client. + * @param[in] text Message content. + * @param[in] type Message type (HISTORY_PRIVMSG or HISTORY_NOTICE). + * @param[in] msgid Message ID (same one sent to clients via echo-message). + * @param[in] timestamp ISO 8601 timestamp. + */ +static void store_private_history(struct Client *sptr, struct Client *acptr, + const char *text, enum HistoryMessageType type, + const char *msgid, const char *timestamp) +{ + char sender[HISTORY_SENDER_LEN]; + char target[NICKLEN * 2 + 2]; /* nick1:nick2 */ + const char *account; + const char *nick1, *nick2; + + if (!history_is_available()) + return; + + /* Check if chathistory feature is enabled */ + if (!feature_bool(FEAT_CAP_draft_chathistory)) + return; + + /* Check if private message history is enabled (separate feature) */ + if (!feature_bool(FEAT_CHATHISTORY_PRIVATE)) + return; + + /* Check per-user consent based on configured mode */ + if (!pm_history_consent(sptr, acptr)) + return; + + /* Build sender string: nick!user@host */ + if (cli_user(sptr)) + ircd_snprintf(0, sender, sizeof(sender), "%s!%s@%s", + cli_name(sptr), + cli_user(sptr)->username, + cli_user(sptr)->host); + else + ircd_strncpy(sender, cli_name(sptr), sizeof(sender) - 1); + + /* Build target as sorted pair for consistent lookups + * Format: lowerNick:higherNick (case-insensitive comparison) + */ + if (ircd_strcmp(cli_name(sptr), cli_name(acptr)) < 0) { + nick1 = cli_name(sptr); + nick2 = cli_name(acptr); + } else { + nick1 = cli_name(acptr); + nick2 = cli_name(sptr); + } + ircd_snprintf(0, target, sizeof(target), "%s:%s", nick1, nick2); + + /* Get account name if logged in */ + account = (cli_user(sptr) && cli_user(sptr)->account[0]) + ? cli_user(sptr)->account : NULL; + + /* Store in database */ + history_store_message(msgid, timestamp, target, sender, + account, type, text); +} +#endif /* USE_LMDB */ + /** Relay a local user's message to a channel. * Generates an error if the client cannot send to the channel. * @param[in] sptr Client that originated the message. @@ -138,6 +304,40 @@ void relay_channel_message(struct Client* sptr, const char* name, const char* te RevealDelayedJoinIfNeeded(sptr, chptr); sendcmdto_channel_butone(sptr, CMD_PRIVATE, chptr, cli_from(sptr), SKIP_DEAF | SKIP_BURST, text[0], "%H :%s", chptr, mytext); + +#ifdef USE_LMDB + { + char msgid[64] = ""; + char timestamp[32] = ""; + + /* Echo message back to sender if they have echo-message capability */ + /* Use the variant that returns msgid/timestamp so we can store them in history */ + if (feature_bool(FEAT_CAP_echo_message) && CapActive(sptr, CAP_ECHOMSG)) + sendcmdto_one_tags_msgid(sptr, CMD_PRIVATE, sptr, + msgid, sizeof(msgid), timestamp, sizeof(timestamp), + "%H :%s", chptr, mytext); + else if (feature_bool(FEAT_MSGID)) { + /* Generate msgid and timestamp even if not echoing, for history storage */ + struct timeval tv; + gettimeofday(&tv, NULL); + ircd_snprintf(0, timestamp, sizeof(timestamp), "%lu.%03lu", + (unsigned long)tv.tv_sec, + (unsigned long)(tv.tv_usec / 1000)); + ircd_snprintf(0, msgid, sizeof(msgid), "%s-%lu-%lu", + cli_yxx(&me), + (unsigned long)cli_firsttime(&me), + ++MsgIdCounter); + } + + /* Store message in history database for draft/chathistory */ + if (msgid[0]) + store_channel_history(sptr, chptr, mytext, HISTORY_PRIVMSG, msgid, timestamp); + } +#else + /* Echo message back to sender if they have echo-message capability */ + if (feature_bool(FEAT_CAP_echo_message) && CapActive(sptr, CAP_ECHOMSG)) + sendcmdto_one_tags(sptr, CMD_PRIVATE, sptr, "%H :%s", chptr, mytext); +#endif } /** Relay a local user's notice to a channel. @@ -192,6 +392,38 @@ void relay_channel_notice(struct Client* sptr, const char* name, const char* tex RevealDelayedJoinIfNeeded(sptr, chptr); sendcmdto_channel_butone(sptr, CMD_NOTICE, chptr, cli_from(sptr), SKIP_DEAF | SKIP_BURST, '\0', "%H :%s", chptr, mytext); + +#ifdef USE_LMDB + { + char msgid[64] = ""; + char timestamp[32] = ""; + + /* Echo notice back to sender if they have echo-message capability */ + if (feature_bool(FEAT_CAP_echo_message) && CapActive(sptr, CAP_ECHOMSG)) + sendcmdto_one_tags_msgid(sptr, CMD_NOTICE, sptr, + msgid, sizeof(msgid), timestamp, sizeof(timestamp), + "%H :%s", chptr, mytext); + else if (feature_bool(FEAT_MSGID)) { + struct timeval tv; + gettimeofday(&tv, NULL); + ircd_snprintf(0, timestamp, sizeof(timestamp), "%lu.%03lu", + (unsigned long)tv.tv_sec, + (unsigned long)(tv.tv_usec / 1000)); + ircd_snprintf(0, msgid, sizeof(msgid), "%s-%lu-%lu", + cli_yxx(&me), + (unsigned long)cli_firsttime(&me), + ++MsgIdCounter); + } + + /* Store notice in history database for draft/chathistory */ + if (msgid[0]) + store_channel_history(sptr, chptr, mytext, HISTORY_NOTICE, msgid, timestamp); + } +#else + /* Echo notice back to sender if they have echo-message capability */ + if (feature_bool(FEAT_CAP_echo_message) && CapActive(sptr, CAP_ECHOMSG)) + sendcmdto_one_tags(sptr, CMD_NOTICE, sptr, "%H :%s", chptr, mytext); +#endif } /** Relay a message to a channel. @@ -219,6 +451,23 @@ void server_relay_channel_message(struct Client* sptr, const char* name, const c if (client_can_send_to_channel(sptr, chptr, 1) || IsChannelService(sptr)) { sendcmdto_channel_butone(sptr, CMD_PRIVATE, chptr, cli_from(sptr), SKIP_DEAF | SKIP_BURST, text[0], "%H :%s", chptr, text); +#ifdef USE_LMDB + /* Store server-relayed message in history database */ + if (feature_bool(FEAT_MSGID)) { + char msgid[64]; + char timestamp[32]; + struct timeval tv; + gettimeofday(&tv, NULL); + ircd_snprintf(0, timestamp, sizeof(timestamp), "%lu.%03lu", + (unsigned long)tv.tv_sec, + (unsigned long)(tv.tv_usec / 1000)); + ircd_snprintf(0, msgid, sizeof(msgid), "%s-%lu-%lu", + cli_yxx(&me), + (unsigned long)cli_firsttime(&me), + ++MsgIdCounter); + store_channel_history(sptr, chptr, text, HISTORY_PRIVMSG, msgid, timestamp); + } +#endif } else send_reply(sptr, ERR_CANNOTSENDTOCHAN, chptr->chname); @@ -248,6 +497,23 @@ void server_relay_channel_notice(struct Client* sptr, const char* name, const ch !(chptr->mode.exmode & EXMODE_NONOTICES)) || IsChannelService(sptr)) { sendcmdto_channel_butone(sptr, CMD_NOTICE, chptr, cli_from(sptr), SKIP_DEAF | SKIP_BURST, '\0', "%H :%s", chptr, text); +#ifdef USE_LMDB + /* Store server-relayed notice in history database */ + if (feature_bool(FEAT_MSGID)) { + char msgid[64]; + char timestamp[32]; + struct timeval tv; + gettimeofday(&tv, NULL); + ircd_snprintf(0, timestamp, sizeof(timestamp), "%lu.%03lu", + (unsigned long)tv.tv_sec, + (unsigned long)(tv.tv_usec / 1000)); + ircd_snprintf(0, msgid, sizeof(msgid), "%s-%lu-%lu", + cli_yxx(&me), + (unsigned long)cli_firsttime(&me), + ++MsgIdCounter); + store_channel_history(sptr, chptr, text, HISTORY_NOTICE, msgid, timestamp); + } +#endif } } @@ -479,6 +745,37 @@ void relay_private_message(struct Client* sptr, const char* name, const char* te add_target(acptr, sptr); sendcmdto_one(sptr, CMD_PRIVATE, acptr, "%C :%s", acptr, text); + + /* Echo message back to sender if they have echo-message capability */ +#ifdef USE_LMDB + { + char msgid[64] = ""; + char timestamp[32] = ""; + + if (feature_bool(FEAT_CAP_echo_message) && CapActive(sptr, CAP_ECHOMSG) && sptr != acptr) + sendcmdto_one_tags_msgid(sptr, CMD_PRIVATE, sptr, + msgid, sizeof(msgid), timestamp, sizeof(timestamp), + "%C :%s", acptr, text); + else if (feature_bool(FEAT_MSGID)) { + struct timeval tv; + gettimeofday(&tv, NULL); + ircd_snprintf(0, timestamp, sizeof(timestamp), "%lu.%03lu", + (unsigned long)tv.tv_sec, + (unsigned long)(tv.tv_usec / 1000)); + ircd_snprintf(0, msgid, sizeof(msgid), "%s-%lu-%lu", + cli_yxx(&me), + (unsigned long)cli_firsttime(&me), + ++MsgIdCounter); + } + + /* Store private message in history database (if enabled) */ + if (msgid[0]) + store_private_history(sptr, acptr, text, HISTORY_PRIVMSG, msgid, timestamp); + } +#else + if (feature_bool(FEAT_CAP_echo_message) && CapActive(sptr, CAP_ECHOMSG) && sptr != acptr) + sendcmdto_one_tags(sptr, CMD_PRIVATE, sptr, "%C :%s", acptr, text); +#endif } /** Relay a private notice from a local user. @@ -534,6 +831,38 @@ void relay_private_notice(struct Client* sptr, const char* name, const char* tex add_target(acptr, sptr); sendcmdto_one(sptr, CMD_NOTICE, acptr, "%C :%s", acptr, text); + +#ifdef USE_LMDB + { + char msgid[64] = ""; + char timestamp[32] = ""; + + /* Echo notice back to sender if they have echo-message capability */ + if (feature_bool(FEAT_CAP_echo_message) && CapActive(sptr, CAP_ECHOMSG) && sptr != acptr) + sendcmdto_one_tags_msgid(sptr, CMD_NOTICE, sptr, + msgid, sizeof(msgid), timestamp, sizeof(timestamp), + "%C :%s", acptr, text); + else if (feature_bool(FEAT_MSGID)) { + struct timeval tv; + gettimeofday(&tv, NULL); + ircd_snprintf(0, timestamp, sizeof(timestamp), "%lu.%03lu", + (unsigned long)tv.tv_sec, + (unsigned long)(tv.tv_usec / 1000)); + ircd_snprintf(0, msgid, sizeof(msgid), "%s-%lu-%lu", + cli_yxx(&me), + (unsigned long)cli_firsttime(&me), + ++MsgIdCounter); + } + + /* Store private notice in history database (if enabled) */ + if (msgid[0]) + store_private_history(sptr, acptr, text, HISTORY_NOTICE, msgid, timestamp); + } +#else + /* Echo notice back to sender if they have echo-message capability */ + if (feature_bool(FEAT_CAP_echo_message) && CapActive(sptr, CAP_ECHOMSG) && sptr != acptr) + sendcmdto_one_tags(sptr, CMD_NOTICE, sptr, "%C :%s", acptr, text); +#endif } /** Relay a private message that arrived from a server. @@ -564,6 +893,24 @@ void server_relay_private_message(struct Client* sptr, const char* name, const c add_target(acptr, sptr); sendcmdto_one(sptr, CMD_PRIVATE, acptr, "%C :%s", acptr, text); + +#ifdef USE_LMDB + /* Store server-relayed private message in history database (if enabled) */ + if (feature_bool(FEAT_MSGID)) { + char msgid[64]; + char timestamp[32]; + struct timeval tv; + gettimeofday(&tv, NULL); + ircd_snprintf(0, timestamp, sizeof(timestamp), "%lu.%03lu", + (unsigned long)tv.tv_sec, + (unsigned long)(tv.tv_usec / 1000)); + ircd_snprintf(0, msgid, sizeof(msgid), "%s-%lu-%lu", + cli_yxx(&me), + (unsigned long)cli_firsttime(&me), + ++MsgIdCounter); + store_private_history(sptr, acptr, text, HISTORY_PRIVMSG, msgid, timestamp); + } +#endif } @@ -592,6 +939,24 @@ void server_relay_private_notice(struct Client* sptr, const char* name, const ch add_target(acptr, sptr); sendcmdto_one(sptr, CMD_NOTICE, acptr, "%C :%s", acptr, text); + +#ifdef USE_LMDB + /* Store server-relayed private notice in history database (if enabled) */ + if (feature_bool(FEAT_MSGID)) { + char msgid[64]; + char timestamp[32]; + struct timeval tv; + gettimeofday(&tv, NULL); + ircd_snprintf(0, timestamp, sizeof(timestamp), "%lu.%03lu", + (unsigned long)tv.tv_sec, + (unsigned long)(tv.tv_usec / 1000)); + ircd_snprintf(0, msgid, sizeof(msgid), "%s-%lu-%lu", + cli_yxx(&me), + (unsigned long)cli_firsttime(&me), + ++MsgIdCounter); + store_private_history(sptr, acptr, text, HISTORY_NOTICE, msgid, timestamp); + } +#endif } /** Relay a masked message from a local user. diff --git a/ircd/ircd_reply.c b/ircd/ircd_reply.c index 796fa1dc..d5ba8208 100644 --- a/ircd/ircd_reply.c +++ b/ircd/ircd_reply.c @@ -27,8 +27,10 @@ #include "config.h" #include "ircd_reply.h" +#include "capab.h" #include "client.h" #include "ircd.h" +#include "ircd_features.h" #include "ircd_log.h" #include "ircd_snprintf.h" #include "msg.h" @@ -40,6 +42,7 @@ /* #include -- Now using assert in ircd_log.h */ #include +#include /** Report a protocol violation warning to anyone listening. This can * be easily used to clean up the last couple of parts of the code. @@ -86,6 +89,8 @@ int send_reply(struct Client *to, int reply, ...) struct VarData vd; struct MsgBuf *mb; const struct Numeric *num; + char tagbuf[256]; + int use_tags = 0; assert(0 != to); assert(0 != reply); @@ -101,8 +106,54 @@ int send_reply(struct Client *to, int reply, ...) assert(0 != vd.vd_format); - /* build buffer */ - mb = msgq_make(cli_from(to), "%:#C %s %C %v", &me, num->str, to, &vd); + /* Check if we need message tags for this client */ + if (MyConnect(to)) { + int pos = 0; + int need_batch = feature_bool(FEAT_CAP_batch) && + CapActive(to, CAP_BATCH) && cli_batch_id(to)[0]; + int need_label = !need_batch && feature_bool(FEAT_CAP_labeled_response) && + CapActive(to, CAP_LABELEDRESP) && cli_label(to)[0]; + int need_time = feature_bool(FEAT_CAP_server_time) && + CapActive(to, CAP_SERVERTIME); + + if (need_batch || need_label || need_time) { + tagbuf[0] = '@'; + pos = 1; + + /* Use @batch when in a batch, otherwise @label */ + if (need_batch) { + pos += snprintf(tagbuf + pos, sizeof(tagbuf) - pos, "batch=%s", cli_batch_id(to)); + } else if (need_label) { + pos += snprintf(tagbuf + pos, sizeof(tagbuf) - pos, "label=%s", cli_label(to)); + } + + if (need_time) { + struct timeval tv; + struct tm tm; + if (pos > 1 && pos < (int)sizeof(tagbuf) - 1) + tagbuf[pos++] = ';'; + gettimeofday(&tv, NULL); + gmtime_r(&tv.tv_sec, &tm); + pos += snprintf(tagbuf + pos, sizeof(tagbuf) - pos, + "time=%04d-%02d-%02dT%02d:%02d:%02d.%03ldZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, + tv.tv_usec / 1000); + } + + if (pos < (int)sizeof(tagbuf) - 1) { + tagbuf[pos++] = ' '; + tagbuf[pos] = '\0'; + } + use_tags = 1; + } + } + + /* build buffer with or without tags */ + if (use_tags) + mb = msgq_make(cli_from(to), "%s%:#C %s %C %v", tagbuf, &me, num->str, to, &vd); + else + mb = msgq_make(cli_from(to), "%:#C %s %C %v", &me, num->str, to, &vd); va_end(vd.vd_args); diff --git a/ircd/list.c b/ircd/list.c index e5c4b07e..a6058768 100644 --- a/ircd/list.c +++ b/ircd/list.c @@ -44,6 +44,7 @@ #include "send.h" #include "struct.h" #include "whowas.h" +#include "metadata.h" /* #include -- Now using assert in ircd_log.h */ #include /* offsetof */ @@ -216,6 +217,11 @@ struct Client* make_client(struct Client *from, int status) con_handler(con) = UNREGISTERED_HANDLER; con_client(con) = cptr; + /* Initialize WebSocket state for RFC 6455 compliance */ + con_ws_frame_len(con) = 0; + con_ws_frag_len(con) = 0; + con_ws_frag_opcode(con) = 0; + cli_connect(cptr) = con; /* set the connection and other fields */ cli_since(cptr) = cli_lasttime(cptr) = cli_firsttime(cptr) = CurrentTime; cli_lastnick(cptr) = TStime(); @@ -278,18 +284,38 @@ void free_client(struct Client* cptr) if (cli_connect(cptr)) MyFree(cli_loc(cptr)); - /* Loop through local clients and clear cli_saslagent if it's cptr. */ + /* Loop through local clients and abort SASL sessions if agent is cptr. + * This proactively notifies clients when the SASL services server disconnects + * instead of making them wait for SASL_TIMEOUT. + */ if (cli_saslagentref(cptr) > 0) { struct Client *acptr; int fd = 0; + int aborted = 0; for (fd = HighestFd; fd >= 0; --fd) { if ((acptr = LocalClientArray[fd])) { if (cli_saslagent(acptr) == cptr) { + /* Abort the SASL session - this sends ERR_SASLFAIL to the client */ + if (cli_saslcookie(acptr) && !IsSASLComplete(acptr)) { + /* Only log first few to avoid log spam during netsplit */ + if (aborted < 5) { + log_write(LS_DEBUG, L_DEBUG, 0, + "SASL: Aborting session for %C - agent %C disconnected", + acptr, cptr); + } + abort_sasl(acptr, 0); /* 0 = not a timeout, just abort */ + aborted++; + } cli_saslagent(acptr) = NULL; } } } + if (aborted > 0) { + log_write(LS_SYSTEM, L_INFO, 0, + "SASL: Aborted %d pending sessions due to agent %C disconnect", + aborted, cptr); + } cli_saslagentref(cptr) = 0; } @@ -308,6 +334,9 @@ void free_client(struct Client* cptr) cli_connect(cptr) = 0; + /* Free metadata and subscriptions for this client */ + metadata_free_client(cptr); + dealloc_client(cptr); /* actually destroy the client */ } diff --git a/ircd/m_account.c b/ircd/m_account.c index 4597da31..053e9e80 100644 --- a/ircd/m_account.c +++ b/ircd/m_account.c @@ -80,6 +80,7 @@ */ #include "config.h" +#include "account_conn.h" #include "client.h" #include "ircd.h" #include "ircd_alloc.h" @@ -94,6 +95,8 @@ #include "s_debug.h" #include "s_user.h" #include "send.h" +#include "metadata.h" +#include "channel.h" /* #include -- Now using assert in ircd_log.h */ #include @@ -163,6 +166,11 @@ int ms_account(struct Client* cptr, struct Client* sptr, int parc, "(ACCOUNT Removal)", cli_name(acptr)); assert(0 != cli_user(acptr)->account[0]); + /* Remove from presence aggregation registry before clearing account */ + if (feature_bool(FEAT_PRESENCE_AGGREGATION)) { + account_conn_remove(acptr); + } + ClearAccount(acptr); ircd_strncpy(cli_user(acptr)->account, "", ACCOUNTLEN + 1); @@ -191,9 +199,21 @@ int ms_account(struct Client* cptr, struct Client* sptr, int parc, if (ircd_strncmp(cli_user(acptr)->account, parv[3], ACCOUNTLEN) == 0) return 0; + /* Load account-linked metadata BEFORE setting account flag */ + metadata_load_account(acptr, parv[3]); + ircd_strncpy(cli_user(acptr)->account, parv[3], ACCOUNTLEN + 1); SetAccount(acptr); + /* Register with presence aggregation */ + if (feature_bool(FEAT_PRESENCE_AGGREGATION)) { + account_conn_add(acptr); + /* Set away state if user is already away */ + if (cli_user(acptr)->away) { + account_conn_set_away(acptr, CONN_AWAY, cli_user(acptr)->away); + } + } + if (parc > 4) { cli_user(acptr)->acc_create = atoi(parv[4]); Debug((DEBUG_DEBUG, "Received timestamped account: account \"%s\", " @@ -241,12 +261,39 @@ int ms_account(struct Client* cptr, struct Client* sptr, int parc, parv[1], parv[2], parv[3], parv[4], parv[5]); return 0; } else if (type == 'A' || type == 'D') { - /* LOC Replies (A=accept, D=deny) */ - if (parc < 4) + /* LOC Replies (A=accept, D=deny) or Rename Permission Replies */ + if (parc < 3) return need_more_params(sptr, "ACCOUNT"); - if (!(acptr = FindNServer(parv[1]))) - return 0; /* target not online, ignore */ + /* First check if this is a server numeric (LOC reply) */ + acptr = FindNServer(parv[1]); + + if (!acptr) { + /* Not a server numeric - check for rename permission response */ + unsigned int cookie = atoi(parv[1]); + struct PendingRename *pr = pending_rename_find(cookie); + + if (pr) { + /* Found a pending rename with this cookie */ + if (type == 'A') { + Debug((DEBUG_DEBUG, "ACCOUNT rename allow cookie=%u", cookie)); + pending_rename_complete(pr); + } else { + /* Deny response: parv[3] contains the reason (trailing param) */ + const char *reason = (parc > 3) ? parv[3] : "Permission denied"; + Debug((DEBUG_DEBUG, "ACCOUNT rename deny cookie=%u reason=%s", cookie, reason)); + pending_rename_deny(pr, reason); + } + return 0; + } + + /* Neither LOC reply nor rename reply - ignore */ + return 0; + } + + /* LOC reply - need at least 4 params */ + if (parc < 4) + return need_more_params(sptr, "ACCOUNT"); if (!IsMe(acptr)) { /* in-transit message, forward it */ @@ -259,6 +306,9 @@ int ms_account(struct Client* cptr, struct Client* sptr, int parc, return 0; /* most probably, user disconnected */ if (type == 'A') { + /* Load account-linked metadata BEFORE setting account flag */ + metadata_load_account(acptr, cli_loc(acptr)->account); + SetAccount(acptr); ircd_strncpy(cli_user(acptr)->account, cli_loc(acptr)->account, ACCOUNTLEN); @@ -320,6 +370,9 @@ int ms_account(struct Client* cptr, struct Client* sptr, int parc, "timestamp %Tu", parv[2], cli_user(acptr)->acc_create)); } + /* Load account-linked metadata BEFORE setting account flag */ + metadata_load_account(acptr, parv[2]); + ircd_strncpy(cli_user(acptr)->account, parv[2], ACCOUNTLEN + 1); SetAccount(acptr); diff --git a/ircd/m_authenticate.c b/ircd/m_authenticate.c index ecf50a1c..78fedcec 100644 --- a/ircd/m_authenticate.c +++ b/ircd/m_authenticate.c @@ -82,6 +82,7 @@ */ #include "config.h" +#include "capab.h" #include "client.h" #include "ircd.h" #include "ircd_features.h" @@ -115,11 +116,27 @@ int m_authenticate(struct Client* cptr, struct Client* sptr, int parc, char* par if (parc < 2) /* have enough parameters? */ return need_more_params(cptr, "AUTHENTICATE"); - if (strlen(parv[1]) > 400) + if (strlen(parv[1]) > 400) { + if (CapActive(cptr, CAP_STANDARDREPLIES)) + send_fail(cptr, "AUTHENTICATE", "TOO_LONG", NULL, "SASL message too long"); return send_reply(cptr, ERR_SASLTOOLONG); + } - if (IsSASLComplete(cptr)) - return send_reply(cptr, ERR_SASLALREADY); + /* For registered users, allow re-authentication (e.g., OAuth token refresh). + * Reset SASL state and start a new session instead of rejecting. + */ + if (IsSASLComplete(cptr)) { + /* Clear the SASLComplete flag to allow new auth */ + ClearSASLComplete(cptr); + /* Clear old SASL session state */ + if ((cli_saslagent(cptr) != NULL) && cli_saslagentref(cli_saslagent(cptr))) + cli_saslagentref(cli_saslagent(cptr))--; + cli_saslagent(cptr) = NULL; + cli_saslcookie(cptr) = 0; + cli_saslstart(cptr) = 0; + if (t_active(&cli_sasltimeout(cptr))) + timer_del(&cli_sasltimeout(cptr)); + } /* Check if IAuth handles SASL */ if (auth_iauth_handles_sasl()) { @@ -128,6 +145,7 @@ int m_authenticate(struct Client* cptr, struct Client* sptr, int parc, char* par do { cli_saslcookie(cptr) = ircrandom() & 0x7fffffff; } while (!cli_saslcookie(cptr)); + cli_saslstart(cptr) = CurrentTime; first = 1; } @@ -168,8 +186,26 @@ int m_authenticate(struct Client* cptr, struct Client* sptr, int parc, char* par acptr = NULL; } - if (!acptr && strcmp(feature_str(FEAT_SASL_SERVER), "*")) + /* Validate agent is still a valid, connected server */ + if (acptr && (IsDead(acptr) || !IsServer(acptr))) { + /* Clear stale agent reference */ + if (cli_saslagent(cptr) == acptr) { + if (cli_saslagentref(acptr)) + cli_saslagentref(acptr)--; + cli_saslagent(cptr) = NULL; + } + /* Try to find a new agent */ + if (strcmp(feature_str(FEAT_SASL_SERVER), "*")) + acptr = find_match_server((char *)feature_str(FEAT_SASL_SERVER)); + else + acptr = NULL; + } + + if (!acptr && strcmp(feature_str(FEAT_SASL_SERVER), "*")) { + if (CapActive(cptr, CAP_STANDARDREPLIES)) + send_fail(cptr, "AUTHENTICATE", "SASL_FAIL", NULL, "SASL service unavailable"); return send_reply(cptr, ERR_SASLFAIL, ": service unavailable"); + } /* If it's to us, do nothing; otherwise, forward the query */ if (acptr && IsMe(acptr)) @@ -180,6 +216,7 @@ int m_authenticate(struct Client* cptr, struct Client* sptr, int parc, char* par do { cli_saslcookie(cptr) = ircrandom() & 0x7fffffff; } while (!cli_saslcookie(cptr)); + cli_saslstart(cptr) = CurrentTime; first = 1; } diff --git a/ircd/m_away.c b/ircd/m_away.c index fd657e67..2b25cf54 100644 --- a/ircd/m_away.c +++ b/ircd/m_away.c @@ -81,9 +81,12 @@ */ #include "config.h" +#include "account_conn.h" +#include "capab.h" #include "client.h" #include "ircd.h" #include "ircd_alloc.h" +#include "ircd_features.h" #include "ircd_log.h" #include "ircd_reply.h" #include "ircd_string.h" @@ -148,20 +151,89 @@ static int user_set_away(struct User* user, char* message) * parv[0] = sender prefix * parv[1] = away message * - * TODO: Throttle aways - many people have a script which resets the away - * message every 10 seconds which really chews the bandwidth. + * FEAT_AWAY_THROTTLE: Minimum seconds between AWAY changes (0 = disabled). + * Prevents scripts that reset away message every few seconds from + * generating excessive network traffic. */ int m_away(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) { char* away_message = parv[1]; int was_away = cli_user(sptr)->away != 0; + int is_away; + int is_away_star = 0; + int throttle; assert(0 != cptr); assert(cptr == sptr); - if (user_set_away(cli_user(sptr), away_message)) + /* Check AWAY throttle - silently drop if too soon after last change */ + throttle = feature_int(FEAT_AWAY_THROTTLE); + if (throttle > 0) { + if (CurrentTime < cli_nextaway(cptr)) { + /* Too soon - silently ignore (no error to avoid spam) */ + return 0; + } + /* Update next allowed time */ + cli_nextaway(cptr) = CurrentTime + throttle; + } + + /* Check for AWAY * (hidden connection) before processing */ + if (away_message && away_message[0] == '*' && away_message[1] == '\0') { + is_away_star = 1; + /* Use configured fallback message for away-star */ + if (feature_str(FEAT_AWAY_STAR_MSG)) { + away_message = (char *)feature_str(FEAT_AWAY_STAR_MSG); + } + } + + is_away = user_set_away(cli_user(sptr), away_message); + + /* Presence aggregation path for logged-in users */ + if (feature_bool(FEAT_PRESENCE_AGGREGATION) && IsAccount(sptr)) { + enum ConnAwayState new_state; + int effective_changed; + + if (is_away_star) { + new_state = CONN_AWAY_STAR; + send_reply(sptr, RPL_NOWAWAY); + } else if (is_away) { + new_state = CONN_AWAY; + send_reply(sptr, RPL_NOWAWAY); + } else { + new_state = CONN_PRESENT; + send_reply(sptr, RPL_UNAWAY); + } + + /* Update this connection's state in the registry */ + effective_changed = account_conn_set_away(sptr, new_state, away_message); + + /* Only broadcast when effective presence changes */ + if (effective_changed) { + struct AccountEntry *entry = account_conn_find(cli_account(sptr)); + if (entry) { + if (entry->effective_state == CONN_PRESENT) { + /* Became present - broadcast unaway */ + sendcmdto_serv_butone(sptr, CMD_AWAY, cptr, ""); + sendcmdto_common_channels_capab_butone(sptr, CMD_AWAY, sptr, + CAP_AWAYNOTIFY, CAP_NONE, ""); + } else { + /* Became away - broadcast with effective message */ + const char *msg = entry->effective_away_msg[0] ? + entry->effective_away_msg : away_message; + sendcmdto_serv_butone(sptr, CMD_AWAY, cptr, ":%s", msg); + sendcmdto_common_channels_capab_butone(sptr, CMD_AWAY, sptr, + CAP_AWAYNOTIFY, CAP_NONE, + ":%s", msg); + } + } + } + return 0; + } + + /* Original non-aggregated path */ + if (is_away) { - if (!was_away) + if (!was_away) sendcmdto_serv_butone(sptr, CMD_AWAY, cptr, ":%s", away_message); send_reply(sptr, RPL_NOWAWAY); sendcmdto_common_channels_capab_butone(sptr, CMD_AWAY, sptr, CAP_AWAYNOTIFY, CAP_NONE, @@ -185,6 +257,8 @@ int m_away(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) int ms_away(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) { char* away_message = parv[1]; + int is_away; + int is_away_star = 0; assert(0 != cptr); assert(0 != sptr); @@ -194,11 +268,81 @@ int ms_away(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) if (IsServer(sptr)) return protocol_violation(sptr,"Server trying to set itself away"); - if (user_set_away(cli_user(sptr), away_message)) + /* Check for AWAY * (hidden connection) from P10 */ + if (away_message && away_message[0] == '*' && away_message[1] == '\0') { + is_away_star = 1; + /* Use configured fallback message for away-star */ + if (feature_str(FEAT_AWAY_STAR_MSG)) { + away_message = (char *)feature_str(FEAT_AWAY_STAR_MSG); + } + } + + is_away = user_set_away(cli_user(sptr), away_message); + + /* Update presence aggregation state for logged-in users */ + if (feature_bool(FEAT_PRESENCE_AGGREGATION) && IsAccount(sptr)) { + enum ConnAwayState new_state; + if (is_away_star) + new_state = CONN_AWAY_STAR; + else if (is_away) + new_state = CONN_AWAY; + else + new_state = CONN_PRESENT; + account_conn_set_away(sptr, new_state, away_message); + /* Aggregation already happened; just propagate */ + } + + if (is_away) sendcmdto_serv_butone(sptr, CMD_AWAY, cptr, ":%s", away_message); else sendcmdto_serv_butone(sptr, CMD_AWAY, cptr, ""); return 0; } +/* + * mu_away - unregistered client message handler (IRCv3 draft/pre-away) + * + * Stores away state for application after registration completes. + * Requires draft/pre-away capability to be negotiated. + * + * parv[0] = sender prefix + * parv[1] = away message (optional, "*" means away without message/hidden) + */ +int mu_away(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) +{ + struct Connection *con; + char* away_message = (parc > 1) ? parv[1] : NULL; + + assert(0 != cptr); + assert(cptr == sptr); + + /* Require draft/pre-away capability */ + if (!HasCap(sptr, CAP_DRAFT_PREAWAY)) + return 0; /* Silently ignore if capability not negotiated */ + + con = cli_connect(sptr); + + if (EmptyString(away_message)) { + /* AWAY with no params = present (clear pre-away) */ + con_pre_away(con) = 0; + con_pre_away_msg(con)[0] = '\0'; + } else if (away_message[0] == '*' && away_message[1] == '\0') { + /* AWAY * = away-star (hidden connection, doesn't count as present) */ + con_pre_away(con) = 2; + /* Use configured away-star message as fallback */ + if (feature_str(FEAT_AWAY_STAR_MSG)) { + ircd_strncpy(con_pre_away_msg(con), feature_str(FEAT_AWAY_STAR_MSG), AWAYLEN); + con_pre_away_msg(con)[AWAYLEN] = '\0'; + } else { + con_pre_away_msg(con)[0] = '\0'; + } + } else { + /* AWAY :message = normal away */ + con_pre_away(con) = 1; + ircd_strncpy(con_pre_away_msg(con), away_message, AWAYLEN); + con_pre_away_msg(con)[AWAYLEN] = '\0'; + } + + return 0; +} diff --git a/ircd/m_batch.c b/ircd/m_batch.c new file mode 100644 index 00000000..553233fd --- /dev/null +++ b/ircd/m_batch.c @@ -0,0 +1,1567 @@ +/* + * IRC - Internet Relay Chat, ircd/m_batch.c + * Copyright (C) 1990 Jarkko Oikarinen and + * University of Oulu, Computing Center + * + * 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. + */ +/* + * ms_batch - server message handler for S2S BATCH coordination + * + * Handles BATCH commands from other servers for coordinating + * netjoin/netsplit batches across the network. + * + * P10 Format: + * [SERVER_NUMERIC] BT +batchid type [params] - Start batch + * [SERVER_NUMERIC] BT -batchid - End batch + * + * Batch Types: + * netjoin - Server reconnecting, users rejoining channels + * netsplit - Server disconnecting, users quitting + * + * IRCv3 batch specification: https://ircv3.net/specs/extensions/batch + */ +#include "config.h" + +#include "capab.h" +#include "channel.h" +#include "client.h" +#include "hash.h" +#include "ircd.h" +#include "ircd_alloc.h" +#include "ircd_features.h" +#include "ircd_log.h" +#include "ircd_reply.h" +#include "ircd_snprintf.h" +#include "ircd_string.h" +#include "list.h" +#include "msg.h" +#include "numeric.h" +#include "numnicks.h" +#include "s_bsd.h" +#include "send.h" +#include "s_misc.h" +#include "s_user.h" +#include "msgq.h" +#include "class.h" +#include "ml_storage.h" +#include "history.h" + +/* #include -- Now using assert in ircd_log.h */ +#include +#include + +/* + * send_multiline_fallback - Send truncated multiline with retrieval hints + * + * Implements the graceful fallback chain for legacy clients: + * 1. Native chathistory (client can retrieve via CHATHISTORY AROUND) + * 2. HistServ available (client can /msg HistServ FETCH) + * 3. Local &ml- storage (ultimate fallback, zero dependencies) + * + * Uses configurable preview budget (FEAT_MULTILINE_LEGACY_MAX_LINES): + * - ≤max_preview lines: send all, no notice + * - >max_preview lines: send max_preview lines + retrieval notice + * + * Parameters: + * sptr - sender client + * to - recipient client + * target - channel name or nick (for retrieval hint) + * msgid - base msgid for retrieval + * messages - linked list of message lines + * total_lines - total line count + * is_channel - 1 if channel, 0 if DM + * chptr - channel pointer (NULL for DMs) + */ +static void send_multiline_fallback(struct Client *sptr, struct Client *to, + const char *target, const char *msgid, + struct SLink *messages, int total_lines, + int is_channel, struct Channel *chptr) +{ + struct SLink *lp; + int lines_to_send; + int send_notice; + int max_preview = feature_int(FEAT_MULTILINE_LEGACY_MAX_LINES); + + /* Configurable preview budget */ + if (total_lines <= max_preview) { + lines_to_send = total_lines; + send_notice = 0; + } else { + lines_to_send = max_preview; + send_notice = 1; + } + + /* Send preview lines */ + int sent = 0; + for (lp = messages; lp && sent < lines_to_send; lp = lp->next, sent++) { + char *text = lp->value.cp + 1; + if (is_channel) { + sendcmdto_one(sptr, CMD_PRIVATE, to, "%H :%s", chptr, text); + } else { + sendcmdto_one(sptr, CMD_PRIVATE, to, "%C :%s", to, text); + } + } + + if (!send_notice) + return; + + int remaining = total_lines - sent; + + /* Fallback chain: chathistory -> HistServ -> &ml- storage */ + if (CapActive(to, CAP_DRAFT_CHATHISTORY)) { + /* Tier 2: Client has native chathistory capability */ + if (is_channel) { + if (remaining <= 15) { + sendcmdto_one(&me, CMD_NOTICE, to, "%H :[%d more lines - CHATHISTORY AROUND %s msgid=%s %d]", + chptr, remaining, target, msgid, remaining + sent); + } else { + sendcmdto_one(&me, CMD_NOTICE, to, "%H :[Message continues (%d lines total) - CHATHISTORY AROUND %s msgid=%s %d]", + chptr, total_lines, target, msgid, total_lines); + } + } else { + if (remaining <= 15) { + sendcmdto_one(&me, CMD_NOTICE, to, "%C :[%d more lines - CHATHISTORY AROUND %s msgid=%s %d]", + to, remaining, target, msgid, remaining + sent); + } else { + sendcmdto_one(&me, CMD_NOTICE, to, "%C :[Message continues (%d lines total) - CHATHISTORY AROUND %s msgid=%s %d]", + to, total_lines, target, msgid, total_lines); + } + } + } else if (FindClient("HistServ")) { + /* Tier 3: HistServ available - queries server's history via P10 */ + if (is_channel) { + if (remaining <= 15) { + sendcmdto_one(&me, CMD_NOTICE, to, "%H :[%d more lines - /msg HistServ FETCH %s %s]", + chptr, remaining, target, msgid); + } else { + sendcmdto_one(&me, CMD_NOTICE, to, "%H :[Message continues (%d lines total) - /msg HistServ FETCH %s %s]", + chptr, total_lines, target, msgid); + } + } else { + if (remaining <= 15) { + sendcmdto_one(&me, CMD_NOTICE, to, "%C :[%d more lines - /msg HistServ FETCH %s %s]", + to, remaining, target, msgid); + } else { + sendcmdto_one(&me, CMD_NOTICE, to, "%C :[Message continues (%d lines total) - /msg HistServ FETCH %s %s]", + to, total_lines, target, msgid); + } + } + } else { + /* Tier 4: Ultimate fallback - local &ml- storage (zero dependencies) */ + ml_storage_store(msgid, cli_name(sptr), target, messages, total_lines); + if (is_channel) { + if (remaining <= 15) { + sendcmdto_one(&me, CMD_NOTICE, to, "%H :[%d more lines - /join &ml-%s to view full message]", + chptr, remaining, msgid); + } else { + sendcmdto_one(&me, CMD_NOTICE, to, "%H :[Message continues (%d lines total) - /join &ml-%s to view]", + chptr, total_lines, msgid); + } + } else { + if (remaining <= 15) { + sendcmdto_one(&me, CMD_NOTICE, to, "%C :[%d more lines - /join &ml-%s to view full message]", + to, remaining, msgid); + } else { + sendcmdto_one(&me, CMD_NOTICE, to, "%C :[Message continues (%d lines total) - /join &ml-%s to view]", + to, total_lines, msgid); + } + } + } +} + +/* + * ms_batch - server message handler + * + * parv[0] = sender prefix (server numeric) + * parv[1] = +batchid type [params] OR -batchid + * + * Handle BATCH from other servers (P10: BT token). + * Format: SERVER BT +batchid netjoin server1 server2 + * SERVER BT -batchid + */ +int ms_batch(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) +{ + char* batch_ref; + char* batch_type = NULL; + int is_start; + struct Client* acptr; + struct DLink* lp; + + assert(0 != cptr); + assert(0 != sptr); + + /* Only servers can send S2S BATCH */ + if (!IsServer(sptr)) + return protocol_violation(sptr, "Non-server trying to send S2S BATCH"); + + if (parc < 2 || EmptyString(parv[1])) + return 0; + + batch_ref = parv[1]; + + /* Determine if this is batch start (+) or end (-) */ + if (batch_ref[0] == '+') { + is_start = 1; + batch_ref++; /* Skip the + prefix */ + if (parc >= 3 && !EmptyString(parv[2])) + batch_type = parv[2]; + else + return 0; /* Start batch requires type */ + } + else if (batch_ref[0] == '-') { + is_start = 0; + batch_ref++; /* Skip the - prefix */ + } + else { + return 0; /* Invalid format */ + } + + if (EmptyString(batch_ref)) + return 0; + + /* Store batch state for this server connection */ + if (is_start) { + ircd_strncpy(cli_s2s_batch_id(cptr), batch_ref, + sizeof(con_s2s_batch_id(cli_connect(cptr))) - 1); + cli_s2s_batch_id(cptr)[sizeof(con_s2s_batch_id(cli_connect(cptr))) - 1] = '\0'; + if (batch_type) { + ircd_strncpy(cli_s2s_batch_type(cptr), batch_type, + sizeof(con_s2s_batch_type(cli_connect(cptr))) - 1); + cli_s2s_batch_type(cptr)[sizeof(con_s2s_batch_type(cli_connect(cptr))) - 1] = '\0'; + } + } + else { + /* Clear batch state on end */ + cli_s2s_batch_id(cptr)[0] = '\0'; + cli_s2s_batch_type(cptr)[0] = '\0'; + } + + /* Propagate to other servers */ + sendcmdto_serv_butone(sptr, CMD_BATCH_CMD, cptr, "%s%s%s%s", + is_start ? "+" : "-", + batch_ref, + batch_type ? " " : "", + batch_type ? batch_type : ""); + + /* For netjoin/netsplit batches, notify local clients with batch capability */ + if (batch_type && (strcmp(batch_type, "netjoin") == 0 || + strcmp(batch_type, "netsplit") == 0)) { + /* Send batch markers to all local clients with batch capability */ + for (acptr = GlobalClientList; acptr; acptr = cli_next(acptr)) { + if (!MyConnect(acptr) || !IsUser(acptr)) + continue; + if (!CapActive(acptr, CAP_BATCH)) + continue; + + if (is_start) { + /* Start batch for this client */ + /* For netjoin: BATCH +refid netjoin server1 server2 */ + /* For netsplit: BATCH +refid netsplit server1 server2 */ + if (parc >= 5 && !EmptyString(parv[3]) && !EmptyString(parv[4])) { + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "+%s %s %s %s", + batch_ref, batch_type, parv[3], parv[4]); + } + else if (parc >= 4 && !EmptyString(parv[3])) { + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "+%s %s %s", + batch_ref, batch_type, parv[3]); + } + else { + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "+%s %s", + batch_ref, batch_type); + } + } + else { + /* End batch for this client */ + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "-%s", batch_ref); + } + } + } + + return 0; +} + +/* + * Helper functions for multiline batch handling + */ + +/** Clear the multiline batch state for a connection */ +static void +clear_multiline_batch(struct Connection *con) +{ + struct SLink *lp, *next; + + /* Free all stored messages */ + for (lp = con_ml_messages(con); lp; lp = next) { + next = lp->next; + if (lp->value.cp) + MyFree(lp->value.cp); + free_link(lp); + } + + /* Apply accumulated lag from the batch with a configurable discount. + * Per IRCv3 multiline spec, we should be lenient for batched messages, + * but we can't ignore lag entirely or malicious clients could abuse + * multiline batches to flood channels (recipients who don't support + * multiline still receive each line as a separate PRIVMSG). + * + * MULTILINE_LAG_DISCOUNT controls what percentage of lag is applied for DMs: + * 100 = full lag (no benefit to multiline, like regular messages) + * 50 = 50% lag (default - rewards multiline while preventing abuse) + * 0 = no lag (dangerous - allows unlimited multiline flooding) + * + * MULTILINE_CHANNEL_LAG_DISCOUNT is used for channel messages (typically + * higher than DM discount since channels affect more users). + * + * MULTILINE_RECIPIENT_DISCOUNT: When enabled, if ALL recipients support + * draft/multiline (no fallback to individual PRIVMSGs was needed), we can + * be more lenient since the batch was delivered as intended - halve the + * lag discount percentage. + */ + if (con_ml_lag_accum(con) > 0) { + int discount; + int discounted_lag; + + /* Use different discount for channels vs DMs */ + if (con_ml_target(con)[0] && IsChannelName(con_ml_target(con))) + discount = feature_int(FEAT_MULTILINE_CHANNEL_LAG_DISCOUNT); + else + discount = feature_int(FEAT_MULTILINE_LAG_DISCOUNT); + + /* If all recipients supported multiline (no fallback), halve the discount */ + if (feature_bool(FEAT_MULTILINE_RECIPIENT_DISCOUNT) && !con_ml_had_fallback(con)) + discount = discount / 2; + + /* Clamp discount to valid range */ + if (discount < 0) + discount = 0; + else if (discount > 100) + discount = 100; + + discounted_lag = (con_ml_lag_accum(con) * discount) / 100; + if (discounted_lag < 2 && discount > 0) + discounted_lag = 2; /* Minimum one message worth (unless fully disabled) */ + con_since(con) += discounted_lag; + } + con_ml_lag_accum(con) = 0; + + con_ml_batch_id(con)[0] = '\0'; + con_ml_target(con)[0] = '\0'; + con_ml_label(con)[0] = '\0'; + con_ml_messages(con) = NULL; + con_ml_msg_count(con) = 0; + con_ml_total_bytes(con) = 0; + con_ml_batch_start(con) = 0; +} + +/** Check for and handle client batch timeout. + * Called periodically from check_pings(). + * @param[in] cptr Client to check. + * @return 1 if batch was timed out, 0 otherwise. + */ +int +check_client_batch_timeout(struct Client *cptr) +{ + struct Connection *con; + time_t timeout; + + if (!MyConnect(cptr)) + return 0; + + con = cli_connect(cptr); + if (!con_ml_batch_id(con)[0]) + return 0; /* No active batch */ + + timeout = feature_int(FEAT_CLIENT_BATCH_TIMEOUT); + if (timeout <= 0) + return 0; /* Timeout disabled */ + + if (CurrentTime - con_ml_batch_start(con) < timeout) + return 0; /* Not timed out yet */ + + /* Batch has timed out - send FAIL and clear */ + send_fail(cptr, "BATCH", "TIMEOUT", con_ml_batch_id(con), + "Batch timed out"); + clear_multiline_batch(con); + return 1; +} + +/** Add a message to the multiline batch */ +int +multiline_add_message(struct Client *sptr, const char *text, int concat) +{ + struct Connection *con = cli_connect(sptr); + struct SLink *lp; + int len; + char *msgcopy; + + if (!con_ml_batch_id(con)[0]) + return 0; /* No active batch */ + + len = strlen(text); + + /* Check limits */ + if (con_ml_msg_count(con) >= feature_int(FEAT_MULTILINE_MAX_LINES)) { + send_fail(sptr, "BATCH", "MULTILINE_MAX_LINES", + con_ml_batch_id(con), "Too many lines in batch"); + clear_multiline_batch(con); + return -1; + } + + if (con_ml_total_bytes(con) + len > feature_int(FEAT_MULTILINE_MAX_BYTES)) { + send_fail(sptr, "BATCH", "MULTILINE_MAX_BYTES", + con_ml_batch_id(con), "Total bytes exceeded"); + clear_multiline_batch(con); + return -1; + } + + /* Store the message with concat flag encoded in high bit of first char */ + msgcopy = (char *)MyMalloc(len + 2); + msgcopy[0] = concat ? 1 : 0; /* Flag byte */ + strcpy(msgcopy + 1, text); + + lp = make_link(); + lp->value.cp = msgcopy; + lp->next = NULL; + + /* Append to end of list */ + if (!con_ml_messages(con)) { + con_ml_messages(con) = lp; + } else { + struct SLink *tail; + for (tail = con_ml_messages(con); tail->next; tail = tail->next) + ; + tail->next = lp; + } + + con_ml_msg_count(con)++; + con_ml_total_bytes(con) += len; + + return 0; +} + +/** Helper to get user's displayed host */ +static const char * +get_displayed_host(struct Client *sptr) +{ + if (IsHiddenHost(sptr)) + return cli_user(sptr)->host; + return cli_user(sptr)->realhost; +} + +/** Helper to format ISO 8601 timestamp for server-time tag */ +static void +format_time_tag(char *buf, size_t buflen) +{ + struct timeval tv; + struct tm tm; + + gettimeofday(&tv, NULL); + gmtime_r(&tv.tv_sec, &tm); + snprintf(buf, buflen, "%04d-%02d-%02dT%02d:%02d:%02d.%03ldZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, + tv.tv_usec / 1000); +} + +/** Process and deliver a completed multiline batch */ +static int +process_multiline_batch(struct Client *sptr) +{ + struct Connection *con = cli_connect(sptr); + struct Channel *chptr = NULL; + struct Client *acptr = NULL; + struct SLink *lp; + struct Membership *member; + int is_channel; + int first; + char batch_base_msgid[64]; /* Base msgid for entire batch */ + int fallback_count = 0; /* Track recipients who got truncated fallback */ + + if (!con_ml_batch_id(con)[0]) + return 0; /* No active batch */ + + if (!con_ml_messages(con)) { + clear_multiline_batch(con); + return 0; /* Empty batch */ + } + + is_channel = IsChannelName(con_ml_target(con)); + + /* Initialize fallback tracking for recipient-aware discounting */ + con_ml_had_fallback(con) = 0; + + /* Validate target */ + if (is_channel) { + chptr = FindChannel(con_ml_target(con)); + if (!chptr) { + send_reply(sptr, ERR_NOSUCHCHANNEL, con_ml_target(con)); + clear_multiline_batch(con); + return 0; + } + /* Check if user can send to channel */ + member = find_member_link(chptr, sptr); + if (!member && (chptr->mode.mode & MODE_NOPRIVMSGS)) { + send_reply(sptr, ERR_CANNOTSENDTOCHAN, chptr->chname); + clear_multiline_batch(con); + return 0; + } + } else { + acptr = FindUser(con_ml_target(con)); + if (!acptr) { + send_reply(sptr, ERR_NOSUCHNICK, con_ml_target(con)); + clear_multiline_batch(con); + return 0; + } + } + + /* Generate ONE base msgid for the entire multiline batch. + * Each line will get this base msgid with a sequence suffix: base:00, base:01, etc. + * This ensures all lines can be retrieved together via CHATHISTORY by msgid prefix. + */ + generate_msgid(batch_base_msgid, sizeof(batch_base_msgid)); + + /* Deliver to recipients */ + if (is_channel) { + /* For each member of the channel */ + for (member = chptr->members; member; member = member->next_member) { + struct Client *to = member->user; + + if (to == sptr) + continue; /* Skip sender (handle echo-message separately) */ + + if (CapActive(to, CAP_DRAFT_MULTILINE) && CapActive(to, CAP_BATCH)) { + /* Send as batch to supporting clients */ + char batchid[16]; + char timebuf[32]; + int use_tags = CapActive(to, CAP_MSGTAGS); + + ircd_snprintf(0, batchid, sizeof(batchid), "%s%u", + NumNick(sptr), con_batch_seq(cli_connect(to))++); + + sendcmdto_one(&me, CMD_BATCH_CMD, to, "+%s draft/multiline %s", + batchid, chptr->chname); + + first = 1; + for (lp = con_ml_messages(con); lp; lp = lp->next) { + int concat = lp->value.cp[0]; + char *text = lp->value.cp + 1; + + /* All lines in the batch share the same msgid per IRCv3 multiline spec */ + if (use_tags) { + format_time_tag(timebuf, sizeof(timebuf)); + } + + if (first && !concat) { + if (use_tags) { + sendrawto_one(to, "@batch=%s;time=%s;msgid=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, timebuf, batch_base_msgid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } else { + sendrawto_one(to, "@batch=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } + first = 0; + } else if (concat) { + if (use_tags) { + sendrawto_one(to, "@batch=%s;time=%s;msgid=%s;draft/multiline-concat :%s!%s@%s PRIVMSG %s :%s", + batchid, timebuf, batch_base_msgid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } else { + sendrawto_one(to, "@batch=%s;draft/multiline-concat :%s!%s@%s PRIVMSG %s :%s", + batchid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } + } else { + if (use_tags) { + sendrawto_one(to, "@batch=%s;time=%s;msgid=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, timebuf, batch_base_msgid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } else { + sendrawto_one(to, "@batch=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } + } + } + + sendcmdto_one(&me, CMD_BATCH_CMD, to, "-%s", batchid); + } else { + /* Fallback: send as individual messages + * If recipient has +M (multiline expand), send full expansion + * Otherwise use graduated truncation based on batch size + */ + int total_lines = con_ml_msg_count(con); + + con_ml_had_fallback(con) = 1; /* Track for recipient-aware discounting */ + fallback_count++; /* Count for sender WARN notification */ + + if (HasFlag(to, FLAG_MULTILINE_EXPAND)) { + /* User opted in with +M: send all lines without truncation */ + for (lp = con_ml_messages(con); lp; lp = lp->next) { + char *text = lp->value.cp + 1; + sendcmdto_one(sptr, CMD_PRIVATE, to, "%H :%s", chptr, text); + } + } else { + /* Graceful fallback: chathistory -> HistServ -> &ml- storage */ + send_multiline_fallback(sptr, to, chptr->chname, batch_base_msgid, + con_ml_messages(con), total_lines, 1, chptr); + } + } + } + + /* Echo to sender if echo-message enabled + * Bounded echo protection: allows echo to proceed even if SendQ is + * near the limit, as long as we stay within an extended limit + * (sendq_limit + input_bytes * ECHO_MAX_FACTOR). This prevents + * "Max sendQ exceeded" disconnects from echo-message expansions + * while still protecting against amplification attacks by bounding + * the protection to a multiple of the input. + * + * Logic: Skip echo if adding echo bytes would exceed extended limit. + * Without protection, skip if already over normal limit. + */ + if (CapActive(sptr, CAP_ECHOMSG)) { + int skip_echo = 0; + + if (MyConnect(sptr)) { + unsigned int batch_input_bytes = con_ml_total_bytes(con); + unsigned int current_sendq = MsgQLength(&(cli_sendQ(sptr))); + unsigned int sendq_limit = get_sendq(sptr); + + if (feature_bool(FEAT_MULTILINE_ECHO_PROTECT)) { + /* Protected: allow up to sendq_limit + bounded echo headroom */ + unsigned int max_echo_bytes = batch_input_bytes * feature_int(FEAT_MULTILINE_ECHO_MAX_FACTOR); + unsigned int extended_limit = sendq_limit + max_echo_bytes; + + /* Skip if current SendQ already exceeds extended limit */ + if (current_sendq > extended_limit) { + skip_echo = 1; + } + } else { + /* Unprotected: skip echo if already at/over normal limit */ + if (current_sendq >= sendq_limit) { + skip_echo = 1; + } + } + } + + if (!skip_echo && CapActive(sptr, CAP_DRAFT_MULTILINE) && CapActive(sptr, CAP_BATCH)) { + char batchid[16]; + char timebuf[32]; + int use_tags = CapActive(sptr, CAP_MSGTAGS); + int use_label = con_ml_label(con)[0] && CapActive(sptr, CAP_LABELEDRESP); + + ircd_snprintf(0, batchid, sizeof(batchid), "%s%u", + NumNick(sptr), con_batch_seq(con)++); + + /* Include label in BATCH echo per IRCv3 labeled-response spec */ + if (use_label) { + sendrawto_one(sptr, "@label=%s :%s BATCH +%s draft/multiline %s", + con_ml_label(con), cli_name(&me), batchid, chptr->chname); + } else { + sendcmdto_one(&me, CMD_BATCH_CMD, sptr, "+%s draft/multiline %s", + batchid, chptr->chname); + } + + first = 1; + for (lp = con_ml_messages(con); lp; lp = lp->next) { + int concat = lp->value.cp[0]; + char *text = lp->value.cp + 1; + + /* All lines in the batch share the same msgid per IRCv3 multiline spec */ + if (use_tags) { + format_time_tag(timebuf, sizeof(timebuf)); + } + + if (first && !concat) { + if (use_tags) { + sendrawto_one(sptr, "@batch=%s;time=%s;msgid=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, timebuf, batch_base_msgid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } else { + sendrawto_one(sptr, "@batch=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } + first = 0; + } else if (concat) { + if (use_tags) { + sendrawto_one(sptr, "@batch=%s;time=%s;msgid=%s;draft/multiline-concat :%s!%s@%s PRIVMSG %s :%s", + batchid, timebuf, batch_base_msgid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } else { + sendrawto_one(sptr, "@batch=%s;draft/multiline-concat :%s!%s@%s PRIVMSG %s :%s", + batchid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } + } else { + if (use_tags) { + sendrawto_one(sptr, "@batch=%s;time=%s;msgid=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, timebuf, batch_base_msgid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } else { + sendrawto_one(sptr, "@batch=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } + } + } + + sendcmdto_one(&me, CMD_BATCH_CMD, sptr, "-%s", batchid); + } else if (!skip_echo) { + /* Fallback echo for non-multiline-capable sender */ + for (lp = con_ml_messages(con); lp; lp = lp->next) { + char *text = lp->value.cp + 1; + sendcmdto_one(sptr, CMD_PRIVATE, sptr, "%H :%s", chptr, text); + } + } + } + } else { + /* Private message to user */ + if (CapActive(acptr, CAP_DRAFT_MULTILINE) && CapActive(acptr, CAP_BATCH)) { + char batchid[16]; + char timebuf[32]; + int use_tags = CapActive(acptr, CAP_MSGTAGS); + + ircd_snprintf(0, batchid, sizeof(batchid), "%s%u", + NumNick(sptr), con_batch_seq(cli_connect(acptr))++); + + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "+%s draft/multiline %s", + batchid, cli_name(acptr)); + + first = 1; + for (lp = con_ml_messages(con); lp; lp = lp->next) { + int concat = lp->value.cp[0]; + char *text = lp->value.cp + 1; + + /* All lines in the batch share the same msgid per IRCv3 multiline spec */ + if (use_tags) { + format_time_tag(timebuf, sizeof(timebuf)); + } + + if (first && !concat) { + if (use_tags) { + sendrawto_one(acptr, "@batch=%s;time=%s;msgid=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, timebuf, batch_base_msgid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), cli_name(acptr), text); + } else { + sendrawto_one(acptr, "@batch=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), cli_name(acptr), text); + } + first = 0; + } else if (concat) { + if (use_tags) { + sendrawto_one(acptr, "@batch=%s;time=%s;msgid=%s;draft/multiline-concat :%s!%s@%s PRIVMSG %s :%s", + batchid, timebuf, batch_base_msgid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), cli_name(acptr), text); + } else { + sendrawto_one(acptr, "@batch=%s;draft/multiline-concat :%s!%s@%s PRIVMSG %s :%s", + batchid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), cli_name(acptr), text); + } + } else { + if (use_tags) { + sendrawto_one(acptr, "@batch=%s;time=%s;msgid=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, timebuf, batch_base_msgid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), cli_name(acptr), text); + } else { + sendrawto_one(acptr, "@batch=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), cli_name(acptr), text); + } + } + } + + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "-%s", batchid); + } else { + /* Fallback for DM: send as individual messages + * If recipient has +M (multiline expand), send full expansion + * Otherwise use graduated truncation based on batch size + */ + int total_lines = con_ml_msg_count(con); + + con_ml_had_fallback(con) = 1; + fallback_count++; /* Count for sender WARN notification */ + + if (HasFlag(acptr, FLAG_MULTILINE_EXPAND)) { + /* User opted in with +M: send all lines without truncation */ + for (lp = con_ml_messages(con); lp; lp = lp->next) { + char *text = lp->value.cp + 1; + sendcmdto_one(sptr, CMD_PRIVATE, acptr, "%C :%s", acptr, text); + } + } else { + /* Graceful fallback: chathistory -> HistServ -> &ml- storage */ + send_multiline_fallback(sptr, acptr, cli_name(acptr), batch_base_msgid, + con_ml_messages(con), total_lines, 0, NULL); + } + } + + /* Echo to sender with bounded protection (same logic as channel echo) */ + if (CapActive(sptr, CAP_ECHOMSG)) { + int skip_dm_echo = 0; + + if (MyConnect(sptr)) { + unsigned int batch_input_bytes = con_ml_total_bytes(con); + unsigned int current_sendq = MsgQLength(&(cli_sendQ(sptr))); + unsigned int sendq_limit = get_sendq(sptr); + + if (feature_bool(FEAT_MULTILINE_ECHO_PROTECT)) { + unsigned int max_echo_bytes = batch_input_bytes * feature_int(FEAT_MULTILINE_ECHO_MAX_FACTOR); + unsigned int extended_limit = sendq_limit + max_echo_bytes; + if (current_sendq > extended_limit) { + skip_dm_echo = 1; + } + } else { + if (current_sendq >= sendq_limit) { + skip_dm_echo = 1; + } + } + } + + if (!skip_dm_echo) { + for (lp = con_ml_messages(con); lp; lp = lp->next) { + char *text = lp->value.cp + 1; + sendcmdto_one(sptr, CMD_PRIVATE, sptr, "%C :%s", acptr, text); + } + } + } + + /* S2S relay for private messages to remote users */ + if (!MyConnect(acptr)) { + char s2s_batch_id[16]; + ircd_snprintf(0, s2s_batch_id, sizeof(s2s_batch_id), "%s%lu", + cli_yxx(sptr), (unsigned long)CurrentTime); + + first = 1; + for (lp = con_ml_messages(con); lp; lp = lp->next) { + int concat = lp->value.cp[0]; + char *text = lp->value.cp + 1; + + if (first) { + /* Start batch with first line */ + sendcmdto_serv_butone(sptr, CMD_MULTILINE, NULL, "+%s %s :%s", + s2s_batch_id, cli_name(acptr), text); + first = 0; + } else if (concat) { + /* Concat line */ + sendcmdto_serv_butone(sptr, CMD_MULTILINE, NULL, "c%s %s :%s", + s2s_batch_id, cli_name(acptr), text); + } else { + /* Normal continuation */ + sendcmdto_serv_butone(sptr, CMD_MULTILINE, NULL, "%s %s :%s", + s2s_batch_id, cli_name(acptr), text); + } + } + /* End batch */ + sendcmdto_serv_butone(sptr, CMD_MULTILINE, NULL, "-%s %s :", + s2s_batch_id, cli_name(acptr)); + } + } + + /* S2S relay for channel messages */ + if (is_channel && chptr) { + char s2s_batch_id[16]; + ircd_snprintf(0, s2s_batch_id, sizeof(s2s_batch_id), "%s%lu", + cli_yxx(sptr), (unsigned long)CurrentTime); + + first = 1; + for (lp = con_ml_messages(con); lp; lp = lp->next) { + int concat = lp->value.cp[0]; + char *text = lp->value.cp + 1; + + if (first) { + /* Start batch with first line */ + sendcmdto_serv_butone(sptr, CMD_MULTILINE, NULL, "+%s %s :%s", + s2s_batch_id, chptr->chname, text); + first = 0; + } else if (concat) { + /* Concat line */ + sendcmdto_serv_butone(sptr, CMD_MULTILINE, NULL, "c%s %s :%s", + s2s_batch_id, chptr->chname, text); + } else { + /* Normal continuation */ + sendcmdto_serv_butone(sptr, CMD_MULTILINE, NULL, "%s %s :%s", + s2s_batch_id, chptr->chname, text); + } + } + /* End batch */ + sendcmdto_serv_butone(sptr, CMD_MULTILINE, NULL, "-%s %s :", + s2s_batch_id, chptr->chname); + } + + /* Notify sender about fallback if they support standard-replies */ + if (fallback_count > 0 && feature_bool(FEAT_MULTILINE_FALLBACK_NOTIFY) + && CapActive(sptr, CAP_STANDARDREPLIES)) { + char desc[128]; + ircd_snprintf(0, desc, sizeof(desc), "Message truncated for %d legacy recipient%s", + fallback_count, fallback_count == 1 ? "" : "s"); + /* Use saved label from BATCH +id for labeled-response correlation */ + send_warn_with_label(sptr, "BATCH", "MULTILINE_FALLBACK", + is_channel ? chptr->chname : cli_name(acptr), desc, + con_ml_label(con)[0] ? con_ml_label(con) : NULL); + } + + /* Store multiline batch to history with the base msgid. + * Concatenate all lines (respecting concat flags) into a single message. + * This allows CHATHISTORY retrieval by the base msgid. + */ + log_write(LS_SYSTEM, L_INFO, 0, "multiline: history_is_available=%d, target=%s, msgid=%s", + history_is_available(), is_channel ? chptr->chname : cli_name(acptr), + batch_base_msgid); + if (history_is_available()) { + char history_content[4096]; /* Reasonable max for multiline concatenated */ + size_t content_len = 0; + char sender_mask[256]; + char timestamp[HISTORY_TIMESTAMP_LEN]; + + /* Build sender mask nick!user@host */ + ircd_snprintf(0, sender_mask, sizeof(sender_mask), "%s!%s@%s", + cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr)); + + /* Build concatenated content, respecting concat flags */ + for (lp = con_ml_messages(con); lp; lp = lp->next) { + int concat = lp->value.cp[0]; + char *text = lp->value.cp + 1; + size_t text_len = strlen(text); + + /* Add Unit Separator (\x1F) if not concat and not first line. + * Using \x1F instead of \n avoids base64 encoding overhead in P10 federation + * while still allowing multiline content to be stored and retrieved. + * HistServ/chathistory converts \x1F back to newlines when displaying. + */ + if (content_len > 0 && !concat) { + if (content_len < sizeof(history_content) - 1) { + history_content[content_len++] = '\x1F'; + } + } + + /* Append text (truncate if exceeds buffer) */ + if (content_len + text_len < sizeof(history_content) - 1) { + memcpy(history_content + content_len, text, text_len); + content_len += text_len; + } else if (content_len < sizeof(history_content) - 1) { + /* Partial fit - copy what we can */ + size_t remaining = sizeof(history_content) - 1 - content_len; + memcpy(history_content + content_len, text, remaining); + content_len += remaining; + } + } + history_content[content_len] = '\0'; + + /* Get timestamp for storage */ + history_format_timestamp(timestamp, sizeof(timestamp)); + + /* Store with base msgid (no sub-IDs) for retrieval */ + { + int store_result = history_store_message(batch_base_msgid, timestamp, + is_channel ? chptr->chname : cli_name(acptr), + sender_mask, + cli_user(sptr)->account[0] ? cli_user(sptr)->account : NULL, + HISTORY_PRIVMSG, + history_content); + log_write(LS_SYSTEM, L_INFO, 0, "multiline: history_store_message returned %d for msgid=%s target=%s", + store_result, batch_base_msgid, is_channel ? chptr->chname : cli_name(acptr)); + } + } + + clear_multiline_batch(con); + return 0; +} + +/* + * m_batch - client message handler for BATCH command + * + * Handles BATCH start/end for multiline messages from clients. + * + * parv[0] = sender prefix + * parv[1] = +batchid type target OR -batchid + * + * For draft/multiline: + * BATCH +id draft/multiline #channel + * BATCH -id + */ +int m_batch(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) +{ + struct Connection *con; + char *batch_ref; + char *batch_type = NULL; + char *target = NULL; + int is_start; + + assert(0 != cptr); + assert(cptr == sptr); + + if (!IsUser(sptr)) + return 0; + + /* Require draft/multiline capability */ + if (!CapActive(sptr, CAP_DRAFT_MULTILINE)) + return 0; + + if (parc < 2 || EmptyString(parv[1])) + return send_reply(sptr, ERR_NEEDMOREPARAMS, "BATCH"); + + con = cli_connect(sptr); + batch_ref = parv[1]; + + /* Determine if this is batch start (+) or end (-) */ + if (batch_ref[0] == '+') { + is_start = 1; + batch_ref++; /* Skip the + prefix */ + + if (parc < 3 || EmptyString(parv[2])) + return send_reply(sptr, ERR_NEEDMOREPARAMS, "BATCH"); + batch_type = parv[2]; + + if (parc < 4 || EmptyString(parv[3])) + return send_reply(sptr, ERR_NEEDMOREPARAMS, "BATCH"); + target = parv[3]; + } + else if (batch_ref[0] == '-') { + is_start = 0; + batch_ref++; /* Skip the - prefix */ + } + else { + send_fail(sptr, "BATCH", "INVALID_FORMAT", NULL, + "Invalid batch format, expected +id or -id"); + return 0; + } + + if (EmptyString(batch_ref)) + return send_reply(sptr, ERR_NEEDMOREPARAMS, "BATCH"); + + if (is_start) { + /* Only support draft/multiline for now */ + if (ircd_strcmp(batch_type, "draft/multiline") != 0) { + send_fail(sptr, "BATCH", "UNSUPPORTED_TYPE", batch_type, + "Unsupported batch type"); + return 0; + } + + /* Batch rate limiting (FEAT_BATCH_RATE_LIMIT) */ + { + int rate_limit = feature_int(FEAT_BATCH_RATE_LIMIT); + if (rate_limit > 0) { + /* Reset counter if we're in a new minute */ + if (CurrentTime - con_batch_minute(con) >= 60) { + con_batch_minute(con) = CurrentTime; + con_batch_count(con) = 0; + } + /* Check rate limit */ + if (con_batch_count(con) >= rate_limit) { + send_fail(sptr, "BATCH", "RATE_LIMIT_EXCEEDED", batch_ref, + "Too many batches per minute"); + return 0; + } + con_batch_count(con)++; + } + } + + /* Check if there's already an active batch */ + if (con_ml_batch_id(con)[0]) { + /* Clear the old batch */ + clear_multiline_batch(con); + } + + /* Start new multiline batch */ + ircd_strncpy(con_ml_batch_id(con), batch_ref, + sizeof(con->con_ml_batch_id) - 1); + con_ml_batch_id(con)[sizeof(con->con_ml_batch_id) - 1] = '\0'; + + ircd_strncpy(con_ml_target(con), target, + sizeof(con->con_ml_target) - 1); + con_ml_target(con)[sizeof(con->con_ml_target) - 1] = '\0'; + + con_ml_messages(con) = NULL; + con_ml_msg_count(con) = 0; + con_ml_total_bytes(con) = 0; + con_ml_batch_start(con) = CurrentTime; + con_ml_lag_accum(con) = 0; /* Reset lag accumulator for new batch */ + + /* Save the label from BATCH +id for labeled-response on WARN */ + if (cli_label(sptr)[0]) { + ircd_strncpy(con_ml_label(con), cli_label(sptr), + sizeof(con->con_ml_label) - 1); + con_ml_label(con)[sizeof(con->con_ml_label) - 1] = '\0'; + } else { + con_ml_label(con)[0] = '\0'; + } + } + else { + /* End batch */ + if (!con_ml_batch_id(con)[0]) { + send_fail(sptr, "BATCH", "NO_ACTIVE_BATCH", batch_ref, + "No active batch to end"); + return 0; + } + + if (strcmp(con_ml_batch_id(con), batch_ref) != 0) { + send_fail(sptr, "BATCH", "BATCH_ID_MISMATCH", batch_ref, + "Batch ID does not match active batch"); + return 0; + } + + /* Process and deliver the batch */ + process_multiline_batch(sptr); + } + + return 0; +} + +/* + * S2S Multiline batch relay structures and functions + */ + +/** Structure to hold a pending multiline batch from S2S */ +struct S2SMultilineBatch { + char batch_id[16]; /**< Batch ID */ + char target[CHANNELLEN + 1]; /**< Target channel or nick */ + struct Client *sender; /**< Original sender client */ + struct SLink *messages; /**< Linked list of messages */ + int msg_count; /**< Number of messages */ + time_t start_time; /**< When batch started */ +}; + +/** Global array of pending S2S multiline batches (indexed by server connection) */ +static struct S2SMultilineBatch *s2s_ml_batches[MAXCONNECTIONS]; + +/** Find an S2S multiline batch by batch ID */ +static struct S2SMultilineBatch * +find_s2s_multiline_batch(const char *batch_id) +{ + int i; + for (i = 0; i < MAXCONNECTIONS; i++) { + if (s2s_ml_batches[i] && strcmp(s2s_ml_batches[i]->batch_id, batch_id) == 0) + return s2s_ml_batches[i]; + } + return NULL; +} + +/** Create a new S2S multiline batch */ +static struct S2SMultilineBatch * +create_s2s_multiline_batch(const char *batch_id, const char *target, + struct Client *sender) +{ + int i; + struct S2SMultilineBatch *batch; + + /* Find an empty slot */ + for (i = 0; i < MAXCONNECTIONS; i++) { + if (!s2s_ml_batches[i]) + break; + } + if (i >= MAXCONNECTIONS) + return NULL; /* No available slot */ + + batch = (struct S2SMultilineBatch *)MyMalloc(sizeof(struct S2SMultilineBatch)); + ircd_strncpy(batch->batch_id, batch_id, sizeof(batch->batch_id) - 1); + batch->batch_id[sizeof(batch->batch_id) - 1] = '\0'; + ircd_strncpy(batch->target, target, sizeof(batch->target) - 1); + batch->target[sizeof(batch->target) - 1] = '\0'; + batch->sender = sender; + batch->messages = NULL; + batch->msg_count = 0; + batch->start_time = CurrentTime; + + s2s_ml_batches[i] = batch; + return batch; +} + +/** Free an S2S multiline batch */ +static void +free_s2s_multiline_batch(struct S2SMultilineBatch *batch) +{ + struct SLink *lp, *next; + int i; + + if (!batch) + return; + + /* Free messages */ + for (lp = batch->messages; lp; lp = next) { + next = lp->next; + if (lp->value.cp) + MyFree(lp->value.cp); + free_link(lp); + } + + /* Remove from array */ + for (i = 0; i < MAXCONNECTIONS; i++) { + if (s2s_ml_batches[i] == batch) { + s2s_ml_batches[i] = NULL; + break; + } + } + + MyFree(batch); +} + +/** Add a message to an S2S multiline batch */ +static void +add_s2s_multiline_message(struct S2SMultilineBatch *batch, const char *text, int concat) +{ + struct SLink *lp; + char *msgcopy; + int len; + + if (!batch || !text) + return; + + len = strlen(text); + msgcopy = (char *)MyMalloc(len + 2); + msgcopy[0] = concat ? 1 : 0; /* Flag byte */ + strcpy(msgcopy + 1, text); + + lp = make_link(); + lp->value.cp = msgcopy; + lp->next = NULL; + + /* Append to end of list */ + if (!batch->messages) { + batch->messages = lp; + } else { + struct SLink *tail; + for (tail = batch->messages; tail->next; tail = tail->next) + ; + tail->next = lp; + } + + batch->msg_count++; +} + +/** Deliver a completed S2S multiline batch to local clients */ +static void +deliver_s2s_multiline_batch(struct S2SMultilineBatch *batch, struct Client *cptr) +{ + struct Channel *chptr = NULL; + struct Client *acptr = NULL; + struct SLink *lp; + struct Membership *member; + int is_channel; + int first; + struct Client *sptr = batch->sender; + char batch_base_msgid[64]; /* Base msgid for entire batch */ + + if (!batch || !batch->messages || !sptr) + return; + + /* Generate ONE base msgid for the entire S2S multiline batch */ + generate_msgid(batch_base_msgid, sizeof(batch_base_msgid)); + + is_channel = IsChannelName(batch->target); + + /* Validate target */ + if (is_channel) { + chptr = FindChannel(batch->target); + if (!chptr) + return; /* Channel doesn't exist locally */ + } else { + acptr = FindUser(batch->target); + if (!acptr || !MyConnect(acptr)) + return; /* User doesn't exist or isn't local */ + } + + /* Deliver to local recipients */ + if (is_channel) { + for (member = chptr->members; member; member = member->next_member) { + struct Client *to = member->user; + + if (!MyConnect(to)) + continue; /* Only deliver to local users */ + + if (to == sptr) + continue; /* Skip sender (they already got echo) */ + + if (CapActive(to, CAP_DRAFT_MULTILINE) && CapActive(to, CAP_BATCH)) { + /* Send as batch to supporting clients */ + char batchid[16]; + char timebuf[32]; + int use_tags = CapActive(to, CAP_MSGTAGS); + + ircd_snprintf(0, batchid, sizeof(batchid), "%s%u", + NumNick(sptr), con_batch_seq(cli_connect(to))++); + + sendcmdto_one(&me, CMD_BATCH_CMD, to, "+%s draft/multiline %s", + batchid, chptr->chname); + + first = 1; + for (lp = batch->messages; lp; lp = lp->next) { + int concat = lp->value.cp[0]; + char *text = lp->value.cp + 1; + + /* All lines in the batch share the same msgid per IRCv3 multiline spec */ + if (use_tags) { + format_time_tag(timebuf, sizeof(timebuf)); + } + + if (first && !concat) { + if (use_tags) { + sendrawto_one(to, "@batch=%s;time=%s;msgid=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, timebuf, batch_base_msgid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } else { + sendrawto_one(to, "@batch=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } + first = 0; + } else if (concat) { + if (use_tags) { + sendrawto_one(to, "@batch=%s;time=%s;msgid=%s;draft/multiline-concat :%s!%s@%s PRIVMSG %s :%s", + batchid, timebuf, batch_base_msgid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } else { + sendrawto_one(to, "@batch=%s;draft/multiline-concat :%s!%s@%s PRIVMSG %s :%s", + batchid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } + } else { + if (use_tags) { + sendrawto_one(to, "@batch=%s;time=%s;msgid=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, timebuf, batch_base_msgid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } else { + sendrawto_one(to, "@batch=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), chptr->chname, text); + } + } + } + + sendcmdto_one(&me, CMD_BATCH_CMD, to, "-%s", batchid); + } else { + /* Graceful fallback for S2S channel delivery */ + send_multiline_fallback(sptr, to, chptr->chname, batch_base_msgid, + batch->messages, batch->msg_count, 1, chptr); + } + } + } else if (acptr && MyConnect(acptr)) { + /* Private message to local user */ + if (CapActive(acptr, CAP_DRAFT_MULTILINE) && CapActive(acptr, CAP_BATCH)) { + char batchid[16]; + char timebuf[32]; + int use_tags = CapActive(acptr, CAP_MSGTAGS); + + ircd_snprintf(0, batchid, sizeof(batchid), "%s%u", + NumNick(sptr), con_batch_seq(cli_connect(acptr))++); + + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "+%s draft/multiline %s", + batchid, cli_name(acptr)); + + first = 1; + for (lp = batch->messages; lp; lp = lp->next) { + int concat = lp->value.cp[0]; + char *text = lp->value.cp + 1; + + /* All lines in the batch share the same msgid per IRCv3 multiline spec */ + if (use_tags) { + format_time_tag(timebuf, sizeof(timebuf)); + } + + if (first && !concat) { + if (use_tags) { + sendrawto_one(acptr, "@batch=%s;time=%s;msgid=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, timebuf, batch_base_msgid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), cli_name(acptr), text); + } else { + sendrawto_one(acptr, "@batch=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), cli_name(acptr), text); + } + first = 0; + } else if (concat) { + if (use_tags) { + sendrawto_one(acptr, "@batch=%s;time=%s;msgid=%s;draft/multiline-concat :%s!%s@%s PRIVMSG %s :%s", + batchid, timebuf, batch_base_msgid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), cli_name(acptr), text); + } else { + sendrawto_one(acptr, "@batch=%s;draft/multiline-concat :%s!%s@%s PRIVMSG %s :%s", + batchid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), cli_name(acptr), text); + } + } else { + if (use_tags) { + sendrawto_one(acptr, "@batch=%s;time=%s;msgid=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, timebuf, batch_base_msgid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), cli_name(acptr), text); + } else { + sendrawto_one(acptr, "@batch=%s :%s!%s@%s PRIVMSG %s :%s", + batchid, cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr), cli_name(acptr), text); + } + } + } + + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "-%s", batchid); + } else { + /* Graceful fallback for S2S DM delivery */ + send_multiline_fallback(sptr, acptr, cli_name(acptr), batch_base_msgid, + batch->messages, batch->msg_count, 0, NULL); + } + } + + /* Store S2S multiline batch to history. + * The originating server may have stored it, but we store locally + * to ensure history is available for local clients. + */ + if (history_is_available() && sptr) { + char history_content[4096]; + size_t content_len = 0; + char sender_mask[256]; + char timestamp[HISTORY_TIMESTAMP_LEN]; + + /* Build sender mask nick!user@host */ + ircd_snprintf(0, sender_mask, sizeof(sender_mask), "%s!%s@%s", + cli_name(sptr), cli_user(sptr)->username, + get_displayed_host(sptr)); + + /* Build concatenated content, respecting concat flags */ + for (lp = batch->messages; lp; lp = lp->next) { + int concat = lp->value.cp[0]; + char *text = lp->value.cp + 1; + size_t text_len = strlen(text); + + /* Add Unit Separator (\x1F) if not concat and not first line. + * Using \x1F instead of \n avoids base64 encoding overhead in P10 federation + * while still allowing multiline content to be stored and retrieved. + * HistServ/chathistory converts \x1F back to newlines when displaying. + */ + if (content_len > 0 && !concat) { + if (content_len < sizeof(history_content) - 1) { + history_content[content_len++] = '\x1F'; + } + } + + /* Append text (truncate if exceeds buffer) */ + if (content_len + text_len < sizeof(history_content) - 1) { + memcpy(history_content + content_len, text, text_len); + content_len += text_len; + } else if (content_len < sizeof(history_content) - 1) { + size_t remaining = sizeof(history_content) - 1 - content_len; + memcpy(history_content + content_len, text, remaining); + content_len += remaining; + } + } + history_content[content_len] = '\0'; + + /* Get timestamp for storage */ + history_format_timestamp(timestamp, sizeof(timestamp)); + + /* Store with base msgid for retrieval */ + history_store_message(batch_base_msgid, timestamp, + is_channel ? chptr->chname : cli_name(acptr), + sender_mask, + cli_user(sptr)->account[0] ? cli_user(sptr)->account : NULL, + HISTORY_PRIVMSG, + history_content); + } +} + +/* + * ms_multiline - server message handler for S2S multiline batch + * + * P10 Format: + * [USER_NUMERIC] ML +batchid target :first_line (start batch + first line) + * [USER_NUMERIC] ML batchid target :line (normal continuation) + * [USER_NUMERIC] ML cbatchid target :line (concat continuation) + * [USER_NUMERIC] ML -batchid target : (end batch) + * + * parv[0] = sender prefix + * parv[1] = batch_id with modifier (+, c, or -) + * parv[2] = target + * parv[3] = text (may be empty for end) + */ +int ms_multiline(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) +{ + char *batch_ref; + char *target; + char *text; + int is_start = 0, is_end = 0, is_concat = 0; + struct S2SMultilineBatch *batch; + + assert(0 != cptr); + assert(0 != sptr); + + /* Sender must be a user */ + if (!IsUser(sptr)) + return protocol_violation(cptr, "Non-user sending MULTILINE"); + + if (parc < 3) + return 0; + + batch_ref = parv[1]; + target = parv[2]; + text = (parc >= 4 && !EmptyString(parv[3])) ? parv[3] : ""; + + /* Parse batch modifier */ + if (batch_ref[0] == '+') { + is_start = 1; + batch_ref++; + } else if (batch_ref[0] == '-') { + is_end = 1; + batch_ref++; + } else if (batch_ref[0] == 'c') { + is_concat = 1; + batch_ref++; + } + + if (EmptyString(batch_ref)) + return 0; + + /* Propagate to other servers first */ + sendcmdto_serv_butone(sptr, CMD_MULTILINE, cptr, "%s%s %s :%s", + is_start ? "+" : (is_end ? "-" : (is_concat ? "c" : "")), + batch_ref, target, text); + + if (is_start) { + /* Start new batch */ + batch = find_s2s_multiline_batch(batch_ref); + if (batch) { + /* Batch ID collision - clear old one */ + free_s2s_multiline_batch(batch); + } + + batch = create_s2s_multiline_batch(batch_ref, target, sptr); + if (!batch) + return 0; /* No room for new batch */ + + /* Add first line if present */ + if (!EmptyString(text)) + add_s2s_multiline_message(batch, text, 0); + } + else if (is_end) { + /* End batch and deliver */ + batch = find_s2s_multiline_batch(batch_ref); + if (batch) { + deliver_s2s_multiline_batch(batch, cptr); + free_s2s_multiline_batch(batch); + } + } + else { + /* Continuation line */ + batch = find_s2s_multiline_batch(batch_ref); + if (batch) + add_s2s_multiline_message(batch, text, is_concat); + } + + return 0; +} diff --git a/ircd/m_cap.c b/ircd/m_cap.c index 2403bf56..145654d5 100644 --- a/ircd/m_cap.c +++ b/ircd/m_cap.c @@ -26,6 +26,7 @@ #include "config.h" +#include "capab.h" #include "client.h" #include "ircd.h" #include "ircd_chattr.h" @@ -34,8 +35,11 @@ #include "ircd_reply.h" #include "ircd_snprintf.h" #include "ircd_string.h" +#include "metadata.h" #include "msg.h" #include "numeric.h" +#include "numnicks.h" +#include "querycmds.h" #include "send.h" #include "s_auth.h" #include "s_user.h" @@ -43,6 +47,22 @@ #include #include +/** Check if the SASL server is available. + * @return 1 if SASL server is connected, 0 otherwise. + */ +static int +sasl_server_available(void) +{ + const char *sasl_server = feature_str(FEAT_SASL_SERVER); + + /* If set to "*", SASL is broadcast to all servers - check if any exist */ + if (!strcmp(sasl_server, "*")) + return (UserStats.servers > 0); + + /* Otherwise, check if the specific SASL server is connected */ + return (find_match_server((char *)sasl_server) != NULL); +} + typedef int (*bqcmp)(const void *, const void *); static struct capabilities { @@ -52,21 +72,48 @@ static struct capabilities { char *name; int namelen; int feat; + char *value; /**< CAP 302 value (e.g., "PLAIN,EXTERNAL" for sasl) */ } capab_list[] = { #define _CAP(cap, flags, name, feat) \ - { CAP_ ## cap, #cap, (flags), (name), sizeof(name) - 1, feat } + { CAP_ ## cap, #cap, (flags), (name), sizeof(name) - 1, feat, 0 } +#define _CAP_V(cap, flags, name, feat, val) \ + { CAP_ ## cap, #cap, (flags), (name), sizeof(name) - 1, feat, val } _CAP(NONE, CAPFL_HIDDEN|CAPFL_PROHIBIT, "none", 0), _CAP(NAMESX, 0, "multi-prefix", FEAT_CAP_multi_prefix), _CAP(UHNAMES, 0, "userhost-in-names", FEAT_CAP_userhost_in_names), _CAP(EXTJOIN, 0, "extended-join", FEAT_CAP_extended_join), _CAP(AWAYNOTIFY, 0, "away-notify", FEAT_CAP_away_notify), _CAP(ACCNOTIFY, 0, "account-notify", FEAT_CAP_account_notify), - _CAP(SASL, 0, "sasl", FEAT_CAP_sasl), + _CAP_V(SASL, 0, "sasl", FEAT_CAP_sasl, "PLAIN,EXTERNAL,OAUTHBEARER"), + _CAP(CAPNOTIFY, 0, "cap-notify", FEAT_CAP_cap_notify), + _CAP(SERVERTIME, 0, "server-time", FEAT_CAP_server_time), + _CAP(ECHOMSG, 0, "echo-message", FEAT_CAP_echo_message), + _CAP(ACCOUNTTAG, 0, "account-tag", FEAT_CAP_account_tag), + _CAP(CHGHOST, 0, "chghost", FEAT_CAP_chghost), + _CAP(INVITENOTIFY, 0, "invite-notify", FEAT_CAP_invite_notify), + _CAP(LABELEDRESP, 0, "labeled-response", FEAT_CAP_labeled_response), + _CAP(BATCH, 0, "batch", FEAT_CAP_batch), + _CAP(SETNAME, 0, "setname", FEAT_CAP_setname), + _CAP(STANDARDREPLIES, 0, "standard-replies", FEAT_CAP_standard_replies), + _CAP(MSGTAGS, 0, "message-tags", FEAT_CAP_message_tags), + _CAP(DRAFT_NOIMPLICITNAMES, 0, "draft/no-implicit-names", FEAT_CAP_draft_no_implicit_names), + _CAP(DRAFT_EXTISUPPORT, 0, "draft/extended-isupport", FEAT_CAP_draft_extended_isupport), + _CAP(DRAFT_PREAWAY, 0, "draft/pre-away", FEAT_CAP_draft_pre_away), + _CAP(DRAFT_MULTILINE, 0, "draft/multiline", FEAT_CAP_draft_multiline), + _CAP(DRAFT_CHATHISTORY, 0, "draft/chathistory", FEAT_CAP_draft_chathistory), + _CAP(DRAFT_EVENTPLAYBACK, 0, "draft/event-playback", FEAT_CAP_draft_event_playback), + _CAP(DRAFT_REDACT, 0, "draft/message-redaction", FEAT_CAP_draft_message_redaction), + _CAP(DRAFT_ACCOUNTREG, 0, "draft/account-registration", FEAT_CAP_draft_account_registration), + _CAP(DRAFT_READMARKER, 0, "draft/read-marker", FEAT_CAP_draft_read_marker), + _CAP(DRAFT_CHANRENAME, 0, "draft/channel-rename", FEAT_CAP_draft_channel_rename), + _CAP_V(DRAFT_METADATA2, 0, "draft/metadata-2", FEAT_CAP_draft_metadata_2, "max-subs=50,max-keys=20,max-value-bytes=300"), + _CAP(DRAFT_WEBPUSH, 0, "draft/webpush", FEAT_CAP_draft_webpush), #ifdef USE_SSL _CAP(TLS, 0, "tls", FEAT_CAP_tls), #endif /* CAPLIST */ #undef _CAP +#undef _CAP_V }; #define CAPAB_LIST_LEN (sizeof(capab_list) / sizeof(struct capabilities)) @@ -146,7 +193,7 @@ find_cap(const char **caplist_p, int *neg_p) /** Send a CAP \a subcmd list of capability changes to \a sptr. * If more than one line is necessary, each line before the last has - * an added "*" parameter before that line's capability list. + * an added "*" parameter before that line's capability list (CAP 302). * @param[in] sptr Client receiving capability list. * @param[in] set Capabilities to show as set (with ack and sticky modifiers). * @param[in] rem Capabalities to show as removed (with no other modifier). @@ -156,9 +203,11 @@ static int send_caplist(struct Client *sptr, const struct CapSet *set, const struct CapSet *rem, const char *subcmd) { - char capbuf[BUFSIZE] = "", pfx[16]; + char capbuf[BUFSIZE] = "", pfx[16], valbuf[128]; struct MsgBuf *mb; - int i, loc, len, flags, pfx_len; + int i, loc, len, flags, pfx_len, val_len; + int cap_version = cli_capab_version(sptr); + int is_ls = (ircd_strcmp(subcmd, "LS") == 0); /* set up the buffer for the final LS message... */ mb = msgq_make(sptr, "%:#C " MSG_CAP " %s %s :", &me, @@ -177,6 +226,10 @@ send_caplist(struct Client *sptr, const struct CapSet *set, || (capab_list[i].feat && (!feature_bool(capab_list[i].feat))))) continue; + /* Don't advertise SASL if the SASL server is not available */ + if (capab_list[i].cap == CAP_SASL && is_ls && !sasl_server_available()) + continue; + /* Build the prefix (space separator and any modifiers needed). */ pfx_len = 0; if (loc) @@ -191,15 +244,61 @@ send_caplist(struct Client *sptr, const struct CapSet *set, } pfx[pfx_len] = '\0'; - len = capab_list[i].namelen + pfx_len; /* how much we'd add... */ + /* Build value string for CAP 302+ */ + valbuf[0] = '\0'; + val_len = 0; + if (is_ls && cap_version >= 302) { + /* For SASL, use dynamic mechanism list if available */ + if (capab_list[i].cap == CAP_SASL) { + const char *mechs = get_sasl_mechanisms(); + if (mechs) + val_len = ircd_snprintf(0, valbuf, sizeof(valbuf), "=%s", mechs); + else if (capab_list[i].value) + val_len = ircd_snprintf(0, valbuf, sizeof(valbuf), "=%s", capab_list[i].value); + } else if (capab_list[i].cap == CAP_DRAFT_MULTILINE) { + /* Build dynamic multiline value from features */ + val_len = ircd_snprintf(0, valbuf, sizeof(valbuf), "=max-bytes=%d,max-lines=%d", + feature_int(FEAT_MULTILINE_MAX_BYTES), + feature_int(FEAT_MULTILINE_MAX_LINES)); + } else if (capab_list[i].cap == CAP_DRAFT_WEBPUSH) { + /* Show VAPID key if available from services */ + const char *vapid = get_vapid_pubkey(); + if (vapid) + val_len = ircd_snprintf(0, valbuf, sizeof(valbuf), "=vapid=%s", vapid); + } else if (capab_list[i].cap == CAP_DRAFT_CHATHISTORY) { + /* Build chathistory value with limit, retention, and optional pm policy */ + int retention_days = feature_int(FEAT_CHATHISTORY_RETENTION); + if (feature_bool(FEAT_CHATHISTORY_ADVERTISE_PM) && + feature_bool(FEAT_CHATHISTORY_PRIVATE)) { + int consent = feature_int(FEAT_CHATHISTORY_PRIVATE_CONSENT); + const char *pm_mode = (consent == 0) ? "global" : + (consent == 1) ? "single" : "multi"; + val_len = ircd_snprintf(0, valbuf, sizeof(valbuf), "=limit=%d,retention=%dd,pm=%s", + feature_int(FEAT_CHATHISTORY_MAX), retention_days, pm_mode); + } else { + val_len = ircd_snprintf(0, valbuf, sizeof(valbuf), "=limit=%d,retention=%dd", + feature_int(FEAT_CHATHISTORY_MAX), retention_days); + } + } else if (capab_list[i].value) { + val_len = ircd_snprintf(0, valbuf, sizeof(valbuf), "=%s", capab_list[i].value); + } + } + + len = capab_list[i].namelen + pfx_len + val_len; /* how much we'd add... */ if (msgq_bufleft(mb) < loc + len + 2) { /* would add too much; must flush */ - sendcmdto_one(&me, CMD_CAP, sptr, "%s %s :%s", - BadPtr(cli_name(sptr)) ? "*" : cli_name(sptr), subcmd, capbuf); + /* For CAP 302+, use * continuation marker */ + if (cap_version >= 302) { + sendcmdto_one(&me, CMD_CAP, sptr, "%s %s * :%s", + BadPtr(cli_name(sptr)) ? "*" : cli_name(sptr), subcmd, capbuf); + } else { + sendcmdto_one(&me, CMD_CAP, sptr, "%s %s :%s", + BadPtr(cli_name(sptr)) ? "*" : cli_name(sptr), subcmd, capbuf); + } capbuf[(loc = 0)] = '\0'; /* re-terminate the buffer... */ } - loc += ircd_snprintf(0, capbuf + loc, sizeof(capbuf) - loc, "%s%s", - pfx, capab_list[i].name); + loc += ircd_snprintf(0, capbuf + loc, sizeof(capbuf) - loc, "%s%s%s", + pfx, capab_list[i].name, valbuf); } msgq_append(0, mb, "%s", capbuf); /* append capabilities to the final cmd */ @@ -214,6 +313,14 @@ cap_ls(struct Client *sptr, const char *caplist) { if (IsUnknown(sptr) && cli_auth(sptr)) /* registration hasn't completed; suspend it... */ auth_cap_start(cli_auth(sptr)); + + /* Parse CAP version from CAP LS 302 */ + if (caplist && *caplist) { + int version = atoi(caplist); + if (version > 0) + cli_capab_version(sptr) = version; + } + return send_caplist(sptr, 0, 0, "LS"); /* send list of capabilities */ } @@ -284,6 +391,11 @@ cap_ack(struct Client *sptr, const char *caplist) if (cap->flags & CAPFL_STICKY) continue; /* but don't clear sticky capabilities */ CapClr(cli_active(sptr), cap->cap); + + /* Clean up metadata subscriptions when metadata-2 is disabled */ + if (cap->cap == CAP_DRAFT_METADATA2) { + metadata_sub_free(sptr); + } } else { if (cap->flags & CAPFL_PROHIBIT) continue; /* and don't set prohibited ones */ @@ -313,6 +425,11 @@ cap_clear(struct Client *sptr, const char *caplist) CapClr(cli_capab(sptr), cap->cap); if (!(cap->flags & CAPFL_PROTO)) CapClr(cli_active(sptr), cap->cap); + + /* Clean up metadata subscriptions when metadata-2 is cleared */ + if (cap->cap == CAP_DRAFT_METADATA2) { + metadata_sub_free(sptr); + } } send_caplist(sptr, 0, &cleared, "ACK"); diff --git a/ircd/m_chathistory.c b/ircd/m_chathistory.c new file mode 100644 index 00000000..083cfe1c --- /dev/null +++ b/ircd/m_chathistory.c @@ -0,0 +1,2061 @@ +/* + * IRC - Internet Relay Chat, ircd/m_chathistory.c + * Copyright (C) 2024 Nefarious Development Team + * + * 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. + */ +/** @file + * @brief Handler for CHATHISTORY command (IRCv3 draft/chathistory). + * + * Specification: https://ircv3.net/specs/extensions/chathistory + * + * CHATHISTORY subcommands: + * LATEST + * BEFORE + * AFTER + * AROUND + * BETWEEN + * TARGETS + */ +#include "config.h" + +#include "capab.h" +#include "channel.h" +#include "client.h" +#include "hash.h" +#include "history.h" +#include "ircd.h" +#include "ircd_alloc.h" +#include "ircd_events.h" +#include "ircd_features.h" +#include "ircd_log.h" +#include "ircd_reply.h" +#include "ircd_snprintf.h" +#include "ircd_string.h" +#include "list.h" +#include "msg.h" +#include "numeric.h" +#include "numnicks.h" +#include "s_bsd.h" +#include "s_conf.h" +#include "send.h" + +/* #include -- Now using assert in ircd_log.h */ +#include +#include +#include + +/** Maximum batch ID length */ +#define BATCH_ID_LEN 16 + +/** Max bytes per base64 chunk (after encoding). + * P10 line limit is 512 bytes. After headers (~100 bytes), we have ~400 safe. + * Raw data: 300 bytes -> 400 base64 chars. + */ +#define CH_CHUNK_RAW_SIZE 300 +#define CH_CHUNK_B64_SIZE 400 + +/** Check if content needs base64 encoding. + * Note: New multiline content uses \x1F separators, but we must still check for + * \n to handle legacy data that was stored with newline separators. + * @param[in] content Content string to check. + * @return 1 if encoding needed, 0 otherwise. + */ +static int ch_needs_encoding(const char *content) +{ + if (!content) + return 0; + /* Encode if contains newline (legacy data or would corrupt P10 stream) */ + if (strchr(content, '\n') != NULL) + return 1; + /* Encode if too long for single P10 message (after headers ~100 bytes) */ + if (strlen(content) > 400) + return 1; + return 0; +} + +/** Base64 encode a string using OpenSSL. + * @param[in] input Input data. + * @param[in] inlen Input length. + * @param[out] output Output buffer (must be at least (inlen*4/3)+5 bytes). + * @return Length of encoded string. + */ +static int ch_base64_encode(const char *input, size_t inlen, char *output) +{ + int outlen; + EVP_EncodeBlock((unsigned char *)output, (const unsigned char *)input, inlen); + outlen = ((inlen + 2) / 3) * 4; + output[outlen] = '\0'; + return outlen; +} + +/** Send a chathistory response with base64 chunking if needed. + * Protocol: + * CH R : - normal + * CH B + : - start/more + * CH B + : - continue + * CH B : - final + * @param[in] sptr Target server. + * @param[in] reqid Request ID. + * @param[in] msg History message. + */ +static void send_ch_response(struct Client *sptr, const char *reqid, + struct HistoryMessage *msg) +{ + const char *account = msg->account[0] ? msg->account : "*"; + + /* Check if content needs base64 encoding */ + if (!ch_needs_encoding(msg->content)) { + /* Simple case: send as-is */ + sendcmdto_one(&me, CMD_CHATHISTORY, sptr, "R %s %s %s %d %s %s :%s", + reqid, msg->msgid, msg->timestamp, msg->type, + msg->sender, account, msg->content); + return; + } + + /* Base64 encode the content */ + size_t content_len = strlen(msg->content); + size_t b64_len = ((content_len + 2) / 3) * 4 + 1; + char *b64 = MyMalloc(b64_len); + if (!b64) { + /* Fallback: send truncated without encoding */ + sendcmdto_one(&me, CMD_CHATHISTORY, sptr, "R %s %s %s %d %s %s :%s", + reqid, msg->msgid, msg->timestamp, msg->type, + msg->sender, account, "[content too large]"); + return; + } + + ch_base64_encode(msg->content, content_len, b64); + size_t b64_total = strlen(b64); + + /* If it fits in one message, send complete B message (no + marker) */ + if (b64_total <= CH_CHUNK_B64_SIZE) { + sendcmdto_one(&me, CMD_CHATHISTORY, sptr, "B %s %s %s %d %s %s :%s", + reqid, msg->msgid, msg->timestamp, msg->type, + msg->sender, account, b64); + MyFree(b64); + return; + } + + /* Multi-chunk: send with chunking */ + size_t offset = 0; + int first = 1; + + while (offset < b64_total) { + size_t remaining = b64_total - offset; + size_t chunk_size = (remaining > CH_CHUNK_B64_SIZE) ? CH_CHUNK_B64_SIZE : remaining; + int more = (offset + chunk_size < b64_total); + char chunk[CH_CHUNK_B64_SIZE + 1]; + + memcpy(chunk, b64 + offset, chunk_size); + chunk[chunk_size] = '\0'; + + if (first) { + /* First chunk: include all metadata, + marker if more coming */ + if (more) { + sendcmdto_one(&me, CMD_CHATHISTORY, sptr, "B %s %s %s %d %s %s + :%s", + reqid, msg->msgid, msg->timestamp, msg->type, + msg->sender, account, chunk); + } else { + /* Single chunk that just barely needed encoding */ + sendcmdto_one(&me, CMD_CHATHISTORY, sptr, "B %s %s %s %d %s %s :%s", + reqid, msg->msgid, msg->timestamp, msg->type, + msg->sender, account, chunk); + } + first = 0; + } else { + /* Continuation chunk: just reqid, msgid, and marker */ + if (more) { + sendcmdto_one(&me, CMD_CHATHISTORY, sptr, "B %s %s + :%s", + reqid, msg->msgid, chunk); + } else { + /* Final chunk: no + marker */ + sendcmdto_one(&me, CMD_CHATHISTORY, sptr, "B %s %s :%s", + reqid, msg->msgid, chunk); + } + } + + offset += chunk_size; + } + + MyFree(b64); +} + +/** Message type names for formatting */ +static const char *msg_type_cmd[] = { + "PRIVMSG", "NOTICE", "JOIN", "PART", "QUIT", + "KICK", "MODE", "TOPIC", "TAGMSG" +}; + +/** Validate a timestamp value. + * Valid formats: + * - Unix timestamp: digits with optional decimal (e.g., "1234567890.123") + * - ISO 8601: "YYYY-MM-DDTHH:MM:SS.sssZ" or similar + * @param[in] ts Timestamp string to validate. + * @return 1 if valid, 0 if invalid. + */ +static int validate_timestamp(const char *ts) +{ + const char *p; + + if (!ts || !*ts) + return 0; + + /* Check for Unix timestamp format: digits with optional decimal point */ + if (IsDigit(*ts)) { + int has_decimal = 0; + for (p = ts; *p; p++) { + if (*p == '.') { + if (has_decimal) + return 0; /* Multiple decimals */ + has_decimal = 1; + } else if (*p == 'T' || *p == '-' || *p == ':' || *p == 'Z') { + /* This looks like ISO 8601 format - validate below */ + break; + } else if (!IsDigit(*p)) { + return 0; /* Invalid character */ + } + } + if (!*p) + return 1; /* Valid Unix timestamp */ + } + + /* Check for ISO 8601 format: YYYY-MM-DDTHH:MM:SS[.sss]Z + * Relaxed validation - just check basic structure */ + if (strlen(ts) >= 19) { /* Minimum: YYYY-MM-DDTHH:MM:SS */ + /* Check YYYY-MM-DD pattern at start */ + if (IsDigit(ts[0]) && IsDigit(ts[1]) && IsDigit(ts[2]) && IsDigit(ts[3]) && + ts[4] == '-' && + IsDigit(ts[5]) && IsDigit(ts[6]) && + ts[7] == '-' && + IsDigit(ts[8]) && IsDigit(ts[9]) && + ts[10] == 'T') { + return 1; /* Valid ISO 8601 */ + } + } + + return 0; /* Invalid format */ +} + +/** Validate a timestamp value in strict ISO 8601 format only. + * Per IRCv3 chathistory spec, clients should send ISO 8601 timestamps. + * Valid format: "YYYY-MM-DDTHH:MM:SS[.sss]Z" + * @param[in] ts Timestamp string to validate. + * @return 1 if valid ISO 8601, 0 if invalid. + */ +static int validate_iso_timestamp(const char *ts) +{ + if (!ts || !*ts) + return 0; + + /* Check for ISO 8601 format: YYYY-MM-DDTHH:MM:SS[.sss]Z + * Relaxed validation - just check basic structure */ + if (strlen(ts) >= 19) { /* Minimum: YYYY-MM-DDTHH:MM:SS */ + /* Check YYYY-MM-DD pattern at start */ + if (IsDigit(ts[0]) && IsDigit(ts[1]) && IsDigit(ts[2]) && IsDigit(ts[3]) && + ts[4] == '-' && + IsDigit(ts[5]) && IsDigit(ts[6]) && + ts[7] == '-' && + IsDigit(ts[8]) && IsDigit(ts[9]) && + ts[10] == 'T') { + return 1; /* Valid ISO 8601 */ + } + } + + return 0; /* Not ISO 8601 format */ +} + +/** Validate a client-facing timestamp value. + * Respects FEAT_CHATHISTORY_STRICT_TIMESTAMPS: + * - If TRUE: only accepts ISO 8601 format (per IRCv3 spec) + * - If FALSE: accepts both ISO 8601 and Unix timestamps (permissive) + * @param[in] ts Timestamp string to validate. + * @return 1 if valid, 0 if invalid. + */ +static int validate_client_timestamp(const char *ts) +{ + if (feature_bool(FEAT_CHATHISTORY_STRICT_TIMESTAMPS)) + return validate_iso_timestamp(ts); + return validate_timestamp(ts); +} + +/** Parse a message reference (timestamp= or msgid=). + * @param[in] ref Reference string. + * @param[out] ref_type Type of reference. + * @param[out] value Extracted value (without prefix). + * @return 0 on success, -1 on error. + */ +static int parse_reference(const char *ref, enum HistoryRefType *ref_type, const char **value) +{ + if (!ref || !*ref) + return -1; + + if (*ref == '*') { + *ref_type = HISTORY_REF_NONE; + *value = ref; + return 0; + } + + if (strncmp(ref, "timestamp=", 10) == 0) { + const char *ts = ref + 10; + /* Validate the timestamp value per IRCv3 spec. + * Uses validate_client_timestamp which respects STRICT_TIMESTAMPS config. */ + if (!validate_client_timestamp(ts)) + return -1; + *ref_type = HISTORY_REF_TIMESTAMP; + *value = ts; + return 0; + } + + if (strncmp(ref, "msgid=", 6) == 0) { + *ref_type = HISTORY_REF_MSGID; + *value = ref + 6; + return 0; + } + + return -1; +} + +/** Convert client subcmd to efficient S2S single-char format. + * L=LATEST, B=BEFORE, A=AFTER, R=AROUND, W=BETWEEN, T=TARGETS + */ +static char subcmd_to_s2s(const char *subcmd) +{ + if (ircd_strcmp(subcmd, "LATEST") == 0) return 'L'; + if (ircd_strcmp(subcmd, "BEFORE") == 0) return 'B'; + if (ircd_strcmp(subcmd, "AFTER") == 0) return 'A'; + if (ircd_strcmp(subcmd, "AROUND") == 0) return 'R'; + if (ircd_strcmp(subcmd, "BETWEEN") == 0) return 'W'; + if (ircd_strcmp(subcmd, "TARGETS") == 0) return 'T'; + return '?'; +} + +/** Convert S2S single-char subcmd to full name for history queries. */ +static const char *s2s_to_subcmd(char c) +{ + switch (c) { + case 'L': return "LATEST"; + case 'B': return "BEFORE"; + case 'A': return "AFTER"; + case 'R': return "AROUND"; + case 'W': return "BETWEEN"; + case 'T': return "TARGETS"; + default: return NULL; + } +} + +/** Convert client reference to efficient S2S format. + * Input: "timestamp=1234.567" or "msgid=abc" or "*" + * Output: "1234.567" or "AB-1234-5" or "*" + * No prefix needed - timestamps start with digit, msgids don't. + * @param[in] ref Client reference string. + * @param[out] buf Buffer for S2S format. + * @param[in] buflen Buffer size. + * @return Pointer to buf, or NULL on error. + */ +static char *ref_to_s2s(const char *ref, char *buf, size_t buflen) +{ + if (!ref || !buf || buflen < 2) + return NULL; + + if (*ref == '*') { + buf[0] = '*'; + buf[1] = '\0'; + return buf; + } + + if (strncmp(ref, "timestamp=", 10) == 0) { + ircd_strncpy(buf, ref + 10, buflen - 1); + buf[buflen - 1] = '\0'; + return buf; + } + + if (strncmp(ref, "msgid=", 6) == 0) { + ircd_strncpy(buf, ref + 6, buflen - 1); + buf[buflen - 1] = '\0'; + return buf; + } + + return NULL; +} + +/** Parse S2S reference format. + * Input: "1234.567" (timestamp) or "AB-1234-5" (msgid) or "*" (none) + * Timestamps always start with a digit, msgids never do (they start with server numeric). + * @param[in] ref S2S reference string. + * @param[out] ref_type Type of reference. + * @param[out] value Pointer to value. + * @return 0 on success, -1 on error. + */ +static int parse_s2s_reference(const char *ref, enum HistoryRefType *ref_type, const char **value) +{ + if (!ref || !*ref) + return -1; + + if (*ref == '*') { + *ref_type = HISTORY_REF_NONE; + *value = ref; + return 0; + } + + /* Handle IRC client format prefixes (X3 sends these) */ + if (strncmp(ref, "timestamp=", 10) == 0) { + *ref_type = HISTORY_REF_TIMESTAMP; + *value = ref + 10; + return 0; + } + + if (strncmp(ref, "msgid=", 6) == 0) { + *ref_type = HISTORY_REF_MSGID; + *value = ref + 6; + return 0; + } + + /* Timestamps start with a digit, msgids start with server numeric (letter) */ + if (IsDigit(*ref)) { + *ref_type = HISTORY_REF_TIMESTAMP; + *value = ref; + return 0; + } + + /* Anything else is a msgid */ + *ref_type = HISTORY_REF_MSGID; + *value = ref; + return 0; +} + +/** Generate a unique batch ID for chathistory response. + * @param[out] buf Buffer for batch ID. + * @param[in] buflen Size of buffer. + * @param[in] sptr Client receiving the batch. + */ +static void generate_batch_id(char *buf, size_t buflen, struct Client *sptr) +{ + static unsigned long batch_counter = 0; + ircd_snprintf(0, buf, buflen, "hist%lu%s", ++batch_counter, cli_yxx(sptr)); +} + +/** Check if message type should be sent to client. + * Without draft/event-playback, only PRIVMSG and NOTICE are sent. + * @param[in] sptr Client to check. + * @param[in] type Message type. + * @return 1 if should send, 0 if should skip. + */ +static int should_send_message_type(struct Client *sptr, enum HistoryMessageType type) +{ + /* PRIVMSG and NOTICE are always sent */ + if (type == HISTORY_PRIVMSG || type == HISTORY_NOTICE) + return 1; + + /* Other events require draft/event-playback capability */ + return CapActive(sptr, CAP_DRAFT_EVENTPLAYBACK); +} + +/** Send a single history message, handling multiline content. + * If content contains \x1F separators and client supports multiline, + * send as nested batch. Otherwise truncate to first line. + * + * @param[in] sptr Client to send to. + * @param[in] msg Message to send. + * @param[in] target Target name. + * @param[in] outer_batchid Outer chathistory batch ID (or NULL if no batch). + * @param[in] time_str ISO timestamp string. + * @param[in] cmd Command name (PRIVMSG, NOTICE, etc.). + */ +static void send_history_message(struct Client *sptr, struct HistoryMessage *msg, + const char *target, const char *outer_batchid, + const char *time_str, const char *cmd) +{ + char *separator; + char first_line[512]; + char *content = msg->content; + + /* Check if content contains Unit Separator (multiline) */ + separator = strchr(content, '\x1F'); + + if (separator && CapActive(sptr, CAP_DRAFT_MULTILINE) && CapActive(sptr, CAP_BATCH)) { + /* Re-batch as draft/multiline nested inside chathistory batch */ + static unsigned long ml_counter = 0; + char ml_batchid[BATCH_ID_LEN]; + char *line_start = content; + char *line_end; + int first = 1; + + /* Generate unique multiline batch ID */ + ircd_snprintf(0, ml_batchid, sizeof(ml_batchid), "ml%lu%s", ++ml_counter, cli_yxx(sptr)); + + /* Start nested multiline batch (inside outer chathistory batch) */ + if (outer_batchid) { + sendrawto_one(sptr, "@batch=%s :testnet.fractalrealities.net BATCH +%s draft/multiline %s", + outer_batchid, ml_batchid, target); + } else { + sendcmdto_one(&me, CMD_BATCH_CMD, sptr, "+%s draft/multiline %s", + ml_batchid, target); + } + + /* Send each line with the same msgid (per multiline spec) */ + while (line_start && *line_start) { + line_end = strchr(line_start, '\x1F'); + if (line_end) { + /* Copy line without separator */ + size_t len = line_end - line_start; + if (len >= sizeof(first_line)) + len = sizeof(first_line) - 1; + memcpy(first_line, line_start, len); + first_line[len] = '\0'; + line_start = line_end + 1; + } else { + /* Last line (no trailing separator) */ + ircd_strncpy(first_line, line_start, sizeof(first_line) - 1); + line_start = NULL; + } + + /* Send line as part of multiline batch */ + if (first) { + /* First line gets time and msgid */ + if (msg->account[0]) { + sendrawto_one(sptr, "@batch=%s;time=%s;msgid=%s;account=%s :%s %s %s :%s", + ml_batchid, time_str, msg->msgid, msg->account, + msg->sender, cmd, target, first_line); + } else { + sendrawto_one(sptr, "@batch=%s;time=%s;msgid=%s :%s %s %s :%s", + ml_batchid, time_str, msg->msgid, + msg->sender, cmd, target, first_line); + } + first = 0; + } else { + /* Subsequent lines - same msgid, batch tag only */ + if (msg->account[0]) { + sendrawto_one(sptr, "@batch=%s;msgid=%s;account=%s :%s %s %s :%s", + ml_batchid, msg->msgid, msg->account, + msg->sender, cmd, target, first_line); + } else { + sendrawto_one(sptr, "@batch=%s;msgid=%s :%s %s %s :%s", + ml_batchid, msg->msgid, + msg->sender, cmd, target, first_line); + } + } + } + + /* End nested multiline batch */ + if (outer_batchid) { + sendrawto_one(sptr, "@batch=%s :testnet.fractalrealities.net BATCH -%s", + outer_batchid, ml_batchid); + } else { + sendcmdto_one(&me, CMD_BATCH_CMD, sptr, "-%s", ml_batchid); + } + } else if (separator && outer_batchid) { + /* Tier 2: Client has chathistory batch but not multiline capability. + * Send each line as separate PRIVMSG within the chathistory batch. + * This allows full content retrieval even without multiline support. + * All lines share the same msgid so clients know they're related. + */ + char *line_start = content; + char *line_end; + int first = 1; + + while (line_start && *line_start) { + line_end = strchr(line_start, '\x1F'); + if (line_end) { + size_t len = line_end - line_start; + if (len >= sizeof(first_line)) + len = sizeof(first_line) - 1; + memcpy(first_line, line_start, len); + first_line[len] = '\0'; + line_start = line_end + 1; + } else { + ircd_strncpy(first_line, line_start, sizeof(first_line) - 1); + line_start = NULL; + } + + /* Send each line as separate message in batch */ + if (first) { + /* First line gets time and msgid */ + if (msg->account[0]) { + sendrawto_one(sptr, "@batch=%s;time=%s;msgid=%s;account=%s :%s %s %s :%s", + outer_batchid, time_str, msg->msgid, msg->account, + msg->sender, cmd, target, first_line); + } else { + sendrawto_one(sptr, "@batch=%s;time=%s;msgid=%s :%s %s %s :%s", + outer_batchid, time_str, msg->msgid, + msg->sender, cmd, target, first_line); + } + first = 0; + } else { + /* Subsequent lines - same msgid indicates they're part of same logical message */ + if (msg->account[0]) { + sendrawto_one(sptr, "@batch=%s;msgid=%s;account=%s :%s %s %s :%s", + outer_batchid, msg->msgid, msg->account, + msg->sender, cmd, target, first_line); + } else { + sendrawto_one(sptr, "@batch=%s;msgid=%s :%s %s %s :%s", + outer_batchid, msg->msgid, + msg->sender, cmd, target, first_line); + } + } + } + } else { + /* Tier 3: No chathistory batch or no separators - send single message */ + /* If there are separators but no batch, truncate to first line */ + if (separator) { + size_t len = separator - content; + if (len >= sizeof(first_line)) + len = sizeof(first_line) - 1; + memcpy(first_line, content, len); + first_line[len] = '\0'; + content = first_line; + } + + if (outer_batchid) { + /* With batch (but no separators) */ + if (msg->account[0]) { + sendrawto_one(sptr, "@batch=%s;time=%s;msgid=%s;account=%s :%s %s %s :%s", + outer_batchid, time_str, msg->msgid, msg->account, + msg->sender, cmd, target, content); + } else { + sendrawto_one(sptr, "@batch=%s;time=%s;msgid=%s :%s %s %s :%s", + outer_batchid, time_str, msg->msgid, + msg->sender, cmd, target, content); + } + } else { + /* Without batch */ + if (msg->account[0]) { + sendrawto_one(sptr, "@time=%s;msgid=%s;account=%s :%s %s %s :%s", + time_str, msg->msgid, msg->account, + msg->sender, cmd, target, content); + } else { + sendrawto_one(sptr, "@time=%s;msgid=%s :%s %s %s :%s", + time_str, msg->msgid, + msg->sender, cmd, target, content); + } + } + } +} + +/** Send history messages as a batch response. + * @param[in] sptr Client to send to. + * @param[in] target Target name for batch. + * @param[in] messages List of messages to send. + * @param[in] count Number of messages. + */ +static void send_history_batch(struct Client *sptr, const char *target, + struct HistoryMessage *messages, int count) +{ + struct HistoryMessage *msg; + char batchid[BATCH_ID_LEN]; + char iso_time[32]; + const char *cmd; + const char *time_str; + + if (count == 0) + messages = NULL; + + /* Generate batch ID */ + generate_batch_id(batchid, sizeof(batchid), sptr); + + /* Start batch */ + if (CapActive(sptr, CAP_BATCH)) { + sendcmdto_one(&me, CMD_BATCH_CMD, sptr, "+%s chathistory %s", + batchid, target); + } + + /* Send each message */ + for (msg = messages; msg; msg = msg->next) { + /* Filter events based on event-playback capability */ + if (!should_send_message_type(sptr, msg->type)) + continue; + + cmd = (msg->type <= HISTORY_TAGMSG) ? msg_type_cmd[msg->type] : "PRIVMSG"; + + /* Convert Unix timestamp to ISO 8601 for @time= tag (IRCv3 requires ISO) */ + if (history_unix_to_iso(msg->timestamp, iso_time, sizeof(iso_time)) == 0) + time_str = iso_time; + else + time_str = msg->timestamp; /* Fallback if conversion fails */ + + /* Send message, handling multiline content if present */ + send_history_message(sptr, msg, target, + CapActive(sptr, CAP_BATCH) ? batchid : NULL, + time_str, cmd); + } + + /* End batch */ + if (CapActive(sptr, CAP_BATCH)) { + sendcmdto_one(&me, CMD_BATCH_CMD, sptr, "-%s", batchid); + } +} + +/** Normalize a PM target to the canonical nick1:nick2 format. + * Clients can query with just a nickname (per IRCv3 spec), but internally + * PM history is stored with target key "lowerNick:higherNick" (sorted). + * + * @param[in] sptr Client requesting history (their nick is the other party). + * @param[in] target Target name (plain nick or already nick:nick format). + * @param[out] normalized Buffer to store normalized target (at least NICKLEN*2+2). + * @param[in] buflen Size of normalized buffer. + * @return 0 on success, -1 on error (invalid nick, user not found, etc.) + */ +static int normalize_pm_target(struct Client *sptr, const char *target, + char *normalized, size_t buflen) +{ + const char *nick1, *nick2; + const char *colon = strchr(target, ':'); + + if (colon) { + /* Already in nick:nick format - just validate and optionally copy */ + char n1[NICKLEN + 1], n2[NICKLEN + 1]; + size_t len1 = colon - target; + if (len1 > NICKLEN || len1 == 0) + return -1; + + memcpy(n1, target, len1); + n1[len1] = '\0'; + ircd_strncpy(n2, colon + 1, NICKLEN); + if (!n2[0]) + return -1; + + /* Verify sender is one of the nicks */ + if (ircd_strcmp(cli_name(sptr), n1) != 0 && + ircd_strcmp(cli_name(sptr), n2) != 0) + return -1; + + /* Write normalized target if buffer provided */ + if (normalized && buflen > 0) { + /* Ensure consistent sorting (lowerNick:higherNick) */ + if (ircd_strcmp(n1, n2) < 0) { + nick1 = n1; + nick2 = n2; + } else { + nick1 = n2; + nick2 = n1; + } + ircd_snprintf(0, normalized, buflen, "%s:%s", nick1, nick2); + } + } else { + /* Plain nickname - construct nick1:nick2 from sender + target */ + struct Client *target_client = FindUser(target); + if (!target_client) { + /* Target user not found - could be offline. For now, still allow + * the query (history might exist from when they were online). */ + } + + /* Write normalized target if buffer provided */ + if (normalized && buflen > 0) { + /* Sort nicks for consistent key format */ + if (ircd_strcmp(cli_name(sptr), target) < 0) { + nick1 = cli_name(sptr); + nick2 = target; + } else { + nick1 = target; + nick2 = cli_name(sptr); + } + ircd_snprintf(0, normalized, buflen, "%s:%s", nick1, nick2); + } + } + + return 0; +} + +/** Check if client can access history for a target. + * @param[in] sptr Client requesting history. + * @param[in] target Target name (channel, plain nick, or nick:nick format). + * @param[out] normalized_target If non-NULL and target is a PM, receives + * the normalized nick1:nick2 format for LMDB lookup. + * @param[in] normalized_len Size of normalized_target buffer. + * @return 0 if allowed, -1 if not. + */ +static int check_history_access(struct Client *sptr, const char *target, + char *normalized_target, size_t normalized_len) +{ + struct Channel *chptr; + struct Membership *member; + + if (IsChannelName(target)) { + chptr = FindChannel(target); + if (!chptr) + return -1; + + /* Check if user is on channel */ + member = find_member_link(chptr, sptr); + if (!member) { + /* User not on channel - could check for invite, etc. */ + return -1; + } + /* For channels, normalized target is same as input */ + if (normalized_target && normalized_len > 0) + ircd_strncpy(normalized_target, target, normalized_len - 1); + return 0; + } else { + /* Private message history */ + if (normalize_pm_target(sptr, target, normalized_target, normalized_len) != 0) + return -1; + return 0; + } +} + +/* Forward declaration for federation query */ +static struct FedRequest *start_fed_query(struct Client *sptr, const char *target, + const char *subcmd, const char *ref, + int limit, + struct HistoryMessage *local_msgs, + int local_count); + +/** Check if we should trigger federation query. + * Returns 1 if we should federate, 0 otherwise. + */ +static int should_federate(const char *target, int local_count, int limit) +{ + /* Only federate for channels, not PMs */ + if (!IsChannelName(target)) + return 0; + + /* Check if federation is enabled */ + if (!feature_bool(FEAT_CHATHISTORY_FEDERATION)) + return 0; + + /* Federate if we got fewer messages than requested. + * Note: count_servers() already skips U-lined services (X3 etc.) since + * they don't store chat history. If only services are connected, + * start_fed_query() returns NULL and we return local results immediately. + * + * TODO: This still has a "kick the can" problem for multi-server: + * - If local_count == 0 and remote servers exist, we federate + * - But if remote servers also have no history, we wait for timeout + * - The spec says return empty batch immediately, but we can't know + * if remote servers have history without asking them + * - Possible future optimization: async channel presence tracking + */ + if (local_count < limit) + return 1; + + return 0; +} + +/** Handle CHATHISTORY LATEST subcommand. + * @param[in] sptr Client sending the command. + * @param[in] target Target channel or nick. + * @param[in] ref_str Reference string. + * @param[in] limit_str Limit string. + * @return 0 on success. + */ +static int chathistory_latest(struct Client *sptr, const char *target, + const char *ref_str, const char *limit_str) +{ + struct HistoryMessage *messages = NULL; + enum HistoryRefType ref_type; + const char *ref_value; + int limit, count, max_limit; + char lookup_target[NICKLEN * 2 + 2]; /* Normalized target for LMDB lookup */ + + /* Parse reference */ + if (parse_reference(ref_str, &ref_type, &ref_value) != 0) { + send_fail(sptr, "CHATHISTORY", "INVALID_PARAMS", "LATEST", + "Invalid message reference"); + return 0; + } + + /* Parse and validate limit */ + limit = atoi(limit_str); + max_limit = feature_int(FEAT_CHATHISTORY_MAX); + if (limit <= 0) + limit = max_limit; + if (limit > max_limit) + limit = max_limit; + + /* Check access and normalize target (for PMs, converts nick to nick:nick format) */ + if (check_history_access(sptr, target, lookup_target, sizeof(lookup_target)) != 0) { + send_fail(sptr, "CHATHISTORY", "INVALID_TARGET", target, + "No access to target"); + return 0; + } + + /* Query local history using normalized target */ + count = history_query_latest(lookup_target, ref_type, ref_value, limit, &messages); + if (count < 0) { + send_fail(sptr, "CHATHISTORY", "MESSAGE_ERROR", target, + "Failed to retrieve history"); + return 0; + } + + /* Check if we should try federation */ + if (should_federate(lookup_target, count, limit)) { + struct FedRequest *req = start_fed_query(sptr, lookup_target, "LATEST", + ref_str, limit, messages, count); + if (req) { + /* Federation started - response will be sent when complete */ + /* Note: messages ownership transferred to req */ + return 0; + } + /* Federation failed to start, fall through to local-only response */ + } + + /* Send local-only response using normalized target */ + send_history_batch(sptr, lookup_target, messages, count); + + /* Free messages */ + history_free_messages(messages); + + return 0; +} + +/** Handle CHATHISTORY BEFORE subcommand. */ +static int chathistory_before(struct Client *sptr, const char *target, + const char *ref_str, const char *limit_str) +{ + struct HistoryMessage *messages = NULL; + enum HistoryRefType ref_type; + const char *ref_value; + int limit, count, max_limit; + char lookup_target[NICKLEN * 2 + 2]; + + if (parse_reference(ref_str, &ref_type, &ref_value) != 0 || + ref_type == HISTORY_REF_NONE) { + send_fail(sptr, "CHATHISTORY", "INVALID_PARAMS", "BEFORE", + "Invalid message reference"); + return 0; + } + + limit = atoi(limit_str); + max_limit = feature_int(FEAT_CHATHISTORY_MAX); + if (limit <= 0) + limit = max_limit; + if (limit > max_limit) + limit = max_limit; + + if (check_history_access(sptr, target, lookup_target, sizeof(lookup_target)) != 0) { + send_fail(sptr, "CHATHISTORY", "INVALID_TARGET", target, + "No access to target"); + return 0; + } + + count = history_query_before(lookup_target, ref_type, ref_value, limit, &messages); + if (count < 0) { + send_fail(sptr, "CHATHISTORY", "MESSAGE_ERROR", target, + "Failed to retrieve history"); + return 0; + } + + /* Check if we should try federation */ + if (should_federate(lookup_target, count, limit)) { + struct FedRequest *req = start_fed_query(sptr, lookup_target, "BEFORE", + ref_str, limit, messages, count); + if (req) + return 0; + } + + send_history_batch(sptr, lookup_target, messages, count); + history_free_messages(messages); + + return 0; +} + +/** Handle CHATHISTORY AFTER subcommand. */ +static int chathistory_after(struct Client *sptr, const char *target, + const char *ref_str, const char *limit_str) +{ + struct HistoryMessage *messages = NULL; + enum HistoryRefType ref_type; + const char *ref_value; + int limit, count, max_limit; + char lookup_target[NICKLEN * 2 + 2]; + + if (parse_reference(ref_str, &ref_type, &ref_value) != 0 || + ref_type == HISTORY_REF_NONE) { + send_fail(sptr, "CHATHISTORY", "INVALID_PARAMS", "AFTER", + "Invalid message reference"); + return 0; + } + + limit = atoi(limit_str); + max_limit = feature_int(FEAT_CHATHISTORY_MAX); + if (limit <= 0) + limit = max_limit; + if (limit > max_limit) + limit = max_limit; + + if (check_history_access(sptr, target, lookup_target, sizeof(lookup_target)) != 0) { + send_fail(sptr, "CHATHISTORY", "INVALID_TARGET", target, + "No access to target"); + return 0; + } + + count = history_query_after(lookup_target, ref_type, ref_value, limit, &messages); + if (count < 0) { + send_fail(sptr, "CHATHISTORY", "MESSAGE_ERROR", target, + "Failed to retrieve history"); + return 0; + } + + /* Check if we should try federation */ + if (should_federate(lookup_target, count, limit)) { + struct FedRequest *req = start_fed_query(sptr, lookup_target, "AFTER", + ref_str, limit, messages, count); + if (req) + return 0; + } + + send_history_batch(sptr, lookup_target, messages, count); + history_free_messages(messages); + + return 0; +} + +/** Handle CHATHISTORY AROUND subcommand. */ +static int chathistory_around(struct Client *sptr, const char *target, + const char *ref_str, const char *limit_str) +{ + struct HistoryMessage *messages = NULL; + enum HistoryRefType ref_type; + const char *ref_value; + int limit, count, max_limit; + char lookup_target[NICKLEN * 2 + 2]; + + if (parse_reference(ref_str, &ref_type, &ref_value) != 0 || + ref_type == HISTORY_REF_NONE) { + send_fail(sptr, "CHATHISTORY", "INVALID_PARAMS", "AROUND", + "Invalid message reference"); + return 0; + } + + limit = atoi(limit_str); + max_limit = feature_int(FEAT_CHATHISTORY_MAX); + if (limit <= 0) + limit = max_limit; + if (limit > max_limit) + limit = max_limit; + + if (check_history_access(sptr, target, lookup_target, sizeof(lookup_target)) != 0) { + send_fail(sptr, "CHATHISTORY", "INVALID_TARGET", target, + "No access to target"); + return 0; + } + + count = history_query_around(lookup_target, ref_type, ref_value, limit, &messages); + if (count < 0) { + send_fail(sptr, "CHATHISTORY", "MESSAGE_ERROR", target, + "Failed to retrieve history"); + return 0; + } + + /* Check if we should try federation */ + if (should_federate(lookup_target, count, limit)) { + struct FedRequest *req = start_fed_query(sptr, lookup_target, "AROUND", + ref_str, limit, messages, count); + if (req) + return 0; + } + + send_history_batch(sptr, lookup_target, messages, count); + history_free_messages(messages); + + return 0; +} + +/** Handle CHATHISTORY BETWEEN subcommand. */ +static int chathistory_between(struct Client *sptr, const char *target, + const char *ref1_str, const char *ref2_str, + const char *limit_str) +{ + struct HistoryMessage *messages = NULL; + enum HistoryRefType ref_type1, ref_type2; + const char *ref_value1, *ref_value2; + int limit, count, max_limit; + char lookup_target[NICKLEN * 2 + 2]; + + if (parse_reference(ref1_str, &ref_type1, &ref_value1) != 0 || + ref_type1 == HISTORY_REF_NONE) { + send_fail(sptr, "CHATHISTORY", "INVALID_PARAMS", "BETWEEN", + "Invalid first message reference"); + return 0; + } + + if (parse_reference(ref2_str, &ref_type2, &ref_value2) != 0 || + ref_type2 == HISTORY_REF_NONE) { + send_fail(sptr, "CHATHISTORY", "INVALID_PARAMS", "BETWEEN", + "Invalid second message reference"); + return 0; + } + + limit = atoi(limit_str); + max_limit = feature_int(FEAT_CHATHISTORY_MAX); + if (limit <= 0) + limit = max_limit; + if (limit > max_limit) + limit = max_limit; + + if (check_history_access(sptr, target, lookup_target, sizeof(lookup_target)) != 0) { + send_fail(sptr, "CHATHISTORY", "INVALID_TARGET", target, + "No access to target"); + return 0; + } + + count = history_query_between(lookup_target, ref_type1, ref_value1, + ref_type2, ref_value2, limit, &messages); + if (count < 0) { + send_fail(sptr, "CHATHISTORY", "MESSAGE_ERROR", target, + "Failed to retrieve history"); + return 0; + } + + send_history_batch(sptr, lookup_target, messages, count); + history_free_messages(messages); + + return 0; +} + +/** Handle CHATHISTORY TARGETS subcommand. */ +static int chathistory_targets(struct Client *sptr, const char *ref1_str, + const char *ref2_str, const char *limit_str) +{ + struct HistoryTarget *targets = NULL; + struct HistoryTarget *tgt; + enum HistoryRefType ref_type1, ref_type2; + const char *ts1, *ts2; + char batchid[BATCH_ID_LEN]; + char iso_time[32]; + const char *time_str; + int limit, count, max_limit; + + /* TARGETS uses timestamp references only */ + if (parse_reference(ref1_str, &ref_type1, &ts1) != 0 || + ref_type1 != HISTORY_REF_TIMESTAMP) { + send_fail(sptr, "CHATHISTORY", "INVALID_PARAMS", "TARGETS", + "TARGETS requires timestamp references"); + return 0; + } + + if (parse_reference(ref2_str, &ref_type2, &ts2) != 0 || + ref_type2 != HISTORY_REF_TIMESTAMP) { + send_fail(sptr, "CHATHISTORY", "INVALID_PARAMS", "TARGETS", + "TARGETS requires timestamp references"); + return 0; + } + + limit = atoi(limit_str); + max_limit = feature_int(FEAT_CHATHISTORY_MAX); + if (limit <= 0) + limit = max_limit; + if (limit > max_limit) + limit = max_limit; + + count = history_query_targets(ts1, ts2, limit, &targets); + if (count < 0) { + send_fail(sptr, "CHATHISTORY", "MESSAGE_ERROR", "*", + "Failed to retrieve targets"); + return 0; + } + + /* Send targets in a batch */ + generate_batch_id(batchid, sizeof(batchid), sptr); + + if (CapActive(sptr, CAP_BATCH)) { + sendcmdto_one(&me, CMD_BATCH_CMD, sptr, "+%s draft/chathistory-targets", + batchid); + } + + for (tgt = targets; tgt; tgt = tgt->next) { + /* Check access for each target before including. + * Note: targets from LMDB are already in internal format (nick1:nick2 for PMs), + * so we pass NULL for normalized_target since we don't need normalization. */ + if (check_history_access(sptr, tgt->target, NULL, 0) == 0) { + /* Convert Unix timestamp to ISO 8601 for client display */ + if (history_unix_to_iso(tgt->last_timestamp, iso_time, sizeof(iso_time)) == 0) + time_str = iso_time; + else + time_str = tgt->last_timestamp; /* Fallback if conversion fails */ + + if (CapActive(sptr, CAP_BATCH)) { + sendrawto_one(sptr, "@batch=%s :%s!%s@%s CHATHISTORY TARGETS %s timestamp=%s", + batchid, cli_name(&me), "chathistory", cli_name(&me), + tgt->target, time_str); + } else { + sendrawto_one(sptr, ":%s!%s@%s CHATHISTORY TARGETS %s timestamp=%s", + cli_name(&me), "chathistory", cli_name(&me), + tgt->target, time_str); + } + } + } + + if (CapActive(sptr, CAP_BATCH)) { + sendcmdto_one(&me, CMD_BATCH_CMD, sptr, "-%s", batchid); + } + + history_free_targets(targets); + + return 0; +} + +/** Handle CHATHISTORY command from a local client. + * @param[in] cptr Connection that sent the command. + * @param[in] sptr Client that sent the command. + * @param[in] parc Number of parameters. + * @param[in] parv Parameters. + * @return 0 on success. + */ +int m_chathistory(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + const char *subcmd; + + assert(cptr == sptr); + + /* Check if chathistory is enabled */ + if (!feature_bool(FEAT_CAP_draft_chathistory)) { + return send_reply(sptr, ERR_UNKNOWNCOMMAND, "CHATHISTORY"); + } + + /* Require the client to have negotiated draft/chathistory capability */ + if (!CapActive(sptr, CAP_DRAFT_CHATHISTORY)) { + send_fail(sptr, "CHATHISTORY", "NEED_REGISTRATION", NULL, + "You must negotiate draft/chathistory capability first"); + return 0; + } + + /* Check if history backend is available */ + if (!history_is_available()) { + send_fail(sptr, "CHATHISTORY", "MESSAGE_ERROR", NULL, + "History service unavailable"); + return 0; + } + + if (parc < 2) { + send_fail(sptr, "CHATHISTORY", "INVALID_PARAMS", NULL, + "Missing subcommand"); + return 0; + } + + subcmd = parv[1]; + + if (ircd_strcmp(subcmd, "LATEST") == 0) { + if (parc < 5) { + send_fail(sptr, "CHATHISTORY", "INVALID_PARAMS", "LATEST", + "Usage: CHATHISTORY LATEST "); + return 0; + } + return chathistory_latest(sptr, parv[2], parv[3], parv[4]); + } + else if (ircd_strcmp(subcmd, "BEFORE") == 0) { + if (parc < 5) { + send_fail(sptr, "CHATHISTORY", "INVALID_PARAMS", "BEFORE", + "Usage: CHATHISTORY BEFORE "); + return 0; + } + return chathistory_before(sptr, parv[2], parv[3], parv[4]); + } + else if (ircd_strcmp(subcmd, "AFTER") == 0) { + if (parc < 5) { + send_fail(sptr, "CHATHISTORY", "INVALID_PARAMS", "AFTER", + "Usage: CHATHISTORY AFTER "); + return 0; + } + return chathistory_after(sptr, parv[2], parv[3], parv[4]); + } + else if (ircd_strcmp(subcmd, "AROUND") == 0) { + if (parc < 5) { + send_fail(sptr, "CHATHISTORY", "INVALID_PARAMS", "AROUND", + "Usage: CHATHISTORY AROUND "); + return 0; + } + return chathistory_around(sptr, parv[2], parv[3], parv[4]); + } + else if (ircd_strcmp(subcmd, "BETWEEN") == 0) { + if (parc < 6) { + send_fail(sptr, "CHATHISTORY", "INVALID_PARAMS", "BETWEEN", + "Usage: CHATHISTORY BETWEEN "); + return 0; + } + return chathistory_between(sptr, parv[2], parv[3], parv[4], parv[5]); + } + else if (ircd_strcmp(subcmd, "TARGETS") == 0) { + if (parc < 5) { + send_fail(sptr, "CHATHISTORY", "INVALID_PARAMS", "TARGETS", + "Usage: CHATHISTORY TARGETS "); + return 0; + } + return chathistory_targets(sptr, parv[2], parv[3], parv[4]); + } + else { + send_fail(sptr, "CHATHISTORY", "INVALID_PARAMS", subcmd, + "Unknown subcommand"); + return 0; + } +} + +/* + * S2S Chathistory Federation + * + * Protocol: + * [SERVER] CH Q - Query + * [SERVER] CH R : - Response + * [SERVER] CH E - End response + */ + +/** Maximum pending federation requests */ +#define MAX_FED_REQUESTS 64 + +/** Maximum messages collected per request */ +#define MAX_FED_MESSAGES 500 + +/** Structure for a pending federation request */ +struct FedRequest { + char reqid[32]; /**< Request ID */ + char target[CHANNELLEN + 1]; /**< Target channel */ + char client_yxx[6]; /**< Client numeric (YXX) for safe lookup */ + struct HistoryMessage *local_msgs; /**< Local LMDB results */ + struct HistoryMessage *fed_msgs; /**< Federated results */ + int local_count; /**< Number of local messages */ + int fed_count; /**< Number of federated messages */ + int servers_pending; /**< Servers we're waiting for */ + time_t start_time; /**< When request started */ + int limit; /**< Original limit requested */ + struct Timer timer; /**< Timeout timer (embedded) */ + int timer_active; /**< Whether timer is active */ + int response_sent; /**< Whether response was already sent */ +}; + +/** Global array of pending federation requests */ +static struct FedRequest *fed_requests[MAX_FED_REQUESTS]; + +/** Counter for generating unique request IDs */ +static unsigned long fed_reqid_counter = 0; + +/** Maximum number of pending base64 chunks */ +#define MAX_PENDING_CHUNKS 64 + +/** Structure for tracking base64 chunked messages */ +struct ChunkEntry { + char key[128]; /**< reqid:msgid */ + char reqid[32]; + char msgid[64]; + char timestamp[32]; + int type; + char sender[64]; + char account[64]; + char *b64_data; /**< Accumulated base64 */ + size_t b64_len; + size_t b64_alloc; +}; + +/** Global array of pending chunks */ +static struct ChunkEntry *pending_chunks[MAX_PENDING_CHUNKS]; + +/** Find a chunk entry by key */ +static struct ChunkEntry *find_chunk(const char *key) +{ + int i; + for (i = 0; i < MAX_PENDING_CHUNKS; i++) { + if (pending_chunks[i] && strcmp(pending_chunks[i]->key, key) == 0) + return pending_chunks[i]; + } + return NULL; +} + +/** Free a chunk entry */ +static void free_chunk(struct ChunkEntry *chunk) +{ + int i; + if (!chunk) + return; + if (chunk->b64_data) + MyFree(chunk->b64_data); + for (i = 0; i < MAX_PENDING_CHUNKS; i++) { + if (pending_chunks[i] == chunk) { + pending_chunks[i] = NULL; + break; + } + } + MyFree(chunk); +} + +/** Create a new chunk entry */ +static struct ChunkEntry *create_chunk(const char *reqid, const char *msgid, + const char *timestamp, int type, + const char *sender, const char *account) +{ + struct ChunkEntry *chunk; + int i; + + for (i = 0; i < MAX_PENDING_CHUNKS; i++) { + if (!pending_chunks[i]) + break; + } + if (i >= MAX_PENDING_CHUNKS) + return NULL; + + chunk = (struct ChunkEntry *)MyCalloc(1, sizeof(struct ChunkEntry)); + ircd_snprintf(0, chunk->key, sizeof(chunk->key), "%s:%s", reqid, msgid); + ircd_strncpy(chunk->reqid, reqid, sizeof(chunk->reqid) - 1); + ircd_strncpy(chunk->msgid, msgid, sizeof(chunk->msgid) - 1); + ircd_strncpy(chunk->timestamp, timestamp, sizeof(chunk->timestamp) - 1); + chunk->type = type; + ircd_strncpy(chunk->sender, sender, sizeof(chunk->sender) - 1); + ircd_strncpy(chunk->account, account, sizeof(chunk->account) - 1); + chunk->b64_alloc = 1024; + chunk->b64_data = MyMalloc(chunk->b64_alloc); + chunk->b64_data[0] = '\0'; + chunk->b64_len = 0; + + pending_chunks[i] = chunk; + return chunk; +} + +/** Append base64 data to chunk */ +static void append_chunk_data(struct ChunkEntry *chunk, const char *b64) +{ + size_t add_len = strlen(b64); + if (chunk->b64_len + add_len + 1 > chunk->b64_alloc) { + chunk->b64_alloc = (chunk->b64_len + add_len + 1) * 2; + chunk->b64_data = MyRealloc(chunk->b64_data, chunk->b64_alloc); + } + memcpy(chunk->b64_data + chunk->b64_len, b64, add_len + 1); + chunk->b64_len += add_len; +} + +/** Base64 decode using OpenSSL. + * @param[in] input Base64 encoded string. + * @param[in] inlen Input length. + * @param[out] output Decoded output (caller frees). + * @param[out] outlen Decoded length. + * @return 1 on success, 0 on failure. + */ +static int ch_base64_decode(const char *input, size_t inlen, char **output, size_t *outlen) +{ + size_t alloc_len = (inlen * 3) / 4 + 4; + char *decoded = MyMalloc(alloc_len); + int decoded_len; + + decoded_len = EVP_DecodeBlock((unsigned char *)decoded, + (const unsigned char *)input, inlen); + if (decoded_len < 0) { + MyFree(decoded); + return 0; + } + + /* EVP_DecodeBlock includes padding - adjust for actual length */ + while (inlen > 0 && input[inlen - 1] == '=') { + decoded_len--; + inlen--; + } + + decoded[decoded_len] = '\0'; + *output = decoded; + *outlen = decoded_len; + return 1; +} + +/** Find a federation request by ID */ +static struct FedRequest *find_fed_request(const char *reqid) +{ + int i; + for (i = 0; i < MAX_FED_REQUESTS; i++) { + if (fed_requests[i] && strcmp(fed_requests[i]->reqid, reqid) == 0) + return fed_requests[i]; + } + return NULL; +} + +/** Free a federation request */ +static void free_fed_request(struct FedRequest *req) +{ + int i; + + if (!req) + return; + + /* Free message lists */ + if (req->local_msgs) + history_free_messages(req->local_msgs); + if (req->fed_msgs) + history_free_messages(req->fed_msgs); + + /* Remove timer if active */ + if (req->timer_active) + timer_del(&req->timer); + + /* Remove from array */ + for (i = 0; i < MAX_FED_REQUESTS; i++) { + if (fed_requests[i] == req) { + fed_requests[i] = NULL; + break; + } + } + + MyFree(req); +} + +/** Add a message to the federated results list */ +static void add_fed_message(struct FedRequest *req, const char *msgid, + const char *timestamp, int type, + const char *sender, const char *account, + const char *content) +{ + struct HistoryMessage *msg, *tail; + + if (!req || req->fed_count >= MAX_FED_MESSAGES) + return; + + msg = (struct HistoryMessage *)MyCalloc(1, sizeof(struct HistoryMessage)); + ircd_strncpy(msg->msgid, msgid, sizeof(msg->msgid) - 1); + ircd_strncpy(msg->timestamp, timestamp, sizeof(msg->timestamp) - 1); + ircd_strncpy(msg->target, req->target, sizeof(msg->target) - 1); + ircd_strncpy(msg->sender, sender, sizeof(msg->sender) - 1); + if (account && strcmp(account, "*") != 0) + ircd_strncpy(msg->account, account, sizeof(msg->account) - 1); + msg->type = type; + if (content) + ircd_strncpy(msg->content, content, sizeof(msg->content) - 1); + msg->next = NULL; + + /* Append to list */ + if (!req->fed_msgs) { + req->fed_msgs = msg; + } else { + for (tail = req->fed_msgs; tail->next; tail = tail->next) + ; + tail->next = msg; + } + req->fed_count++; +} + +/** Check if message already exists in a list (by msgid) */ +static int message_exists(struct HistoryMessage *list, const char *msgid) +{ + struct HistoryMessage *m; + for (m = list; m; m = m->next) { + if (strcmp(m->msgid, msgid) == 0) + return 1; + } + return 0; +} + +/** Merge and deduplicate two message lists, sort by timestamp */ +static struct HistoryMessage *merge_messages(struct HistoryMessage *list1, + struct HistoryMessage *list2, + int limit) +{ + struct HistoryMessage *result = NULL, *tail = NULL; + struct HistoryMessage *m, *next, *best; + struct HistoryMessage *p1 = list1, *p2 = list2; + int count = 0; + + /* Simple merge: collect all unique messages, sort by timestamp */ + /* First, add all from list1 */ + for (m = list1; m && count < limit; m = m->next) { + struct HistoryMessage *copy = (struct HistoryMessage *)MyCalloc(1, sizeof(*copy)); + memcpy(copy, m, sizeof(*copy)); + copy->next = NULL; + if (!result) { + result = tail = copy; + } else { + tail->next = copy; + tail = copy; + } + count++; + } + + /* Add unique messages from list2 */ + for (m = list2; m && count < limit; m = m->next) { + if (!message_exists(result, m->msgid)) { + struct HistoryMessage *copy = (struct HistoryMessage *)MyCalloc(1, sizeof(*copy)); + memcpy(copy, m, sizeof(*copy)); + copy->next = NULL; + if (!result) { + result = tail = copy; + } else { + tail->next = copy; + tail = copy; + } + count++; + } + } + + /* Simple bubble sort by timestamp (descending for LATEST) */ + /* For small lists this is fine; for large lists we'd want better sorting */ + if (result && result->next) { + int swapped; + do { + swapped = 0; + struct HistoryMessage **pp = &result; + while (*pp && (*pp)->next) { + struct HistoryMessage *a = *pp; + struct HistoryMessage *b = a->next; + /* Sort descending by timestamp (newest first) */ + if (strcmp(a->timestamp, b->timestamp) < 0) { + a->next = b->next; + b->next = a; + *pp = b; + swapped = 1; + } + pp = &((*pp)->next); + } + } while (swapped); + } + + return result; +} + +/** Send federation results to client (does NOT free the request) + * Call this to send results, then let timer destroy event free the request. + */ +static void send_fed_response(struct FedRequest *req) +{ + struct HistoryMessage *merged; + struct Client *client; + int total; + + if (!req || req->response_sent) + return; + + req->response_sent = 1; /* Mark as sent to prevent double-send */ + + /* Look up the client by numeric - they may have disconnected */ + client = findNUser(req->client_yxx); + if (!client) { + /* Client disconnected, nothing to send */ + return; + } + + /* Merge local and federated results */ + merged = merge_messages(req->local_msgs, req->fed_msgs, req->limit); + + /* Count total */ + total = 0; + for (struct HistoryMessage *m = merged; m; m = m->next) + total++; + + /* Send to client */ + send_history_batch(client, req->target, merged, total); + + /* Free merged list */ + history_free_messages(merged); +} + +/** Complete a federation request - sends response and triggers cleanup. + * For timeout path: timer_run will call ET_DESTROY after we return. + * For early completion: we call timer_del which triggers ET_DESTROY. + */ +static void complete_fed_request(struct FedRequest *req) +{ + if (!req) + return; + + /* Send response to client */ + send_fed_response(req); + + /* If timer is still active (early completion), delete it. + * timer_del will trigger ET_DESTROY callback which frees the request. + * If timer already expired (timeout path), timer_run will send ET_DESTROY. + */ + if (req->timer_active) { + req->timer_active = 0; + timer_del(&req->timer); + /* Note: timer_del triggers ET_DESTROY, which calls free_fed_request */ + } + /* If !timer_active, we're in the timeout callback and timer_run will + * send ET_DESTROY after we return, so don't free here */ +} + +/** Timer callback for federation timeout. + * Handles both ET_EXPIRE (timeout) and ET_DESTROY (cleanup). + */ +static void fed_timeout_callback(struct Event *ev) +{ + struct FedRequest *req; + + req = (struct FedRequest *)t_data(ev_timer(ev)); + if (!req) + return; + + switch (ev_type(ev)) { + case ET_EXPIRE: + /* Timer expired - complete with whatever we have. + * Don't free here - timer_run will send ET_DESTROY after we return. + */ + req->timer_active = 0; + complete_fed_request(req); + break; + + case ET_DESTROY: + /* Timer is being destroyed - safe to free the request now. + * This is called by timer_run after ET_EXPIRE, or by timer_del. + */ + free_fed_request(req); + break; + + default: + break; + } +} + +/** Check if a server is U-lined (services). + * U-lined servers don't store chat history, so we skip them in federation. + */ +static int is_ulined_server(struct Client *server) +{ + return find_conf_byhost(cli_confs(server), cli_name(server), CONF_UWORLD) != NULL; +} + +/** Count connected non-U-lined servers (real IRC servers only). + * U-lined servers (services like X3) don't store chat history. + */ +static int count_servers(void) +{ + int count = 0; + struct DLink *lp; + + for (lp = cli_serv(&me)->down; lp; lp = lp->next) { + struct Client *server = lp->value.cptr; + if (!is_ulined_server(server)) + count++; + } + + return count; +} + +/** Send a federation query to all servers + * @param[in] sptr Client requesting history + * @param[in] target Channel name + * @param[in] subcmd Subcommand (LATEST, BEFORE, etc.) + * @param[in] ref Reference string + * @param[in] limit Maximum messages + * @param[in] local_msgs Already-retrieved local messages + * @param[in] local_count Number of local messages + * @return Request ID or NULL on failure + */ +static struct FedRequest *start_fed_query(struct Client *sptr, const char *target, + const char *subcmd, const char *ref, + int limit, + struct HistoryMessage *local_msgs, + int local_count) +{ + struct FedRequest *req; + char reqid[32]; + char s2s_ref[64]; + char s2s_subcmd; + int i, server_count; + struct DLink *lp; + + /* Check if federation is enabled */ + if (!feature_bool(FEAT_CHATHISTORY_FEDERATION)) + return NULL; + + /* Count connected servers */ + server_count = count_servers(); + if (server_count == 0) + return NULL; /* No servers to query */ + + /* Convert to efficient S2S format */ + s2s_subcmd = subcmd_to_s2s(subcmd); + if (s2s_subcmd == '?') + return NULL; /* Unknown subcmd */ + + if (!ref_to_s2s(ref, s2s_ref, sizeof(s2s_ref))) + return NULL; /* Invalid reference */ + + /* Find empty slot */ + for (i = 0; i < MAX_FED_REQUESTS; i++) { + if (!fed_requests[i]) + break; + } + if (i >= MAX_FED_REQUESTS) + return NULL; /* No room */ + + /* Generate request ID */ + ircd_snprintf(0, reqid, sizeof(reqid), "%s%lu", + cli_yxx(&me), ++fed_reqid_counter); + + /* Create request */ + req = (struct FedRequest *)MyCalloc(1, sizeof(struct FedRequest)); + ircd_strncpy(req->reqid, reqid, sizeof(req->reqid) - 1); + ircd_strncpy(req->target, target, sizeof(req->target) - 1); + /* Store full client numeric (server + client) for safe lookup later + * findNUser expects the full numeric like "BjAAU" not just the client part "AAU" */ + ircd_snprintf(0, req->client_yxx, sizeof(req->client_yxx), "%s%s", + cli_yxx(cli_user(sptr)->server), cli_yxx(sptr)); + req->local_msgs = local_msgs; + req->local_count = local_count; + req->fed_msgs = NULL; + req->fed_count = 0; + req->servers_pending = server_count; + req->start_time = CurrentTime; + req->limit = limit; + + fed_requests[i] = req; + + /* Set timeout timer */ + timer_add(timer_init(&req->timer), fed_timeout_callback, + (void *)req, TT_RELATIVE, + feature_int(FEAT_CHATHISTORY_TIMEOUT)); + req->timer_active = 1; + + /* Send query to non-U-lined servers using efficient S2S format: + * CH Q + * Skip U-lined servers (services) as they don't store chat history. + */ + for (lp = cli_serv(&me)->down; lp; lp = lp->next) { + struct Client *server = lp->value.cptr; + if (is_ulined_server(server)) + continue; /* Services don't store history */ + sendcmdto_one(&me, CMD_CHATHISTORY, server, "Q %s %c %s %d %s", + target, s2s_subcmd, s2s_ref, limit, reqid); + } + + return req; +} + +/* + * ms_chathistory - server message handler for S2S chathistory federation + * + * P10 Format (optimized for efficiency): + * [SERVER] CH Q - Query + * [SERVER] CH R : - Response + * [SERVER] CH E - End response + * + * Subcmd codes: L=LATEST, B=BEFORE, A=AFTER, R=AROUND, W=BETWEEN, T=TARGETS + * Ref format: T, M, or * for none + * + * parv[0] = sender prefix + * parv[1] = subcommand (Q, R, or E) + * parv[2+] = parameters based on subcommand + */ +int ms_chathistory(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + char *subcmd; + struct Client *origin; + + assert(0 != cptr); + assert(0 != sptr); + + /* Sender must be a server */ + if (!IsServer(sptr)) + return 0; + + if (parc < 2) + return 0; + + subcmd = parv[1]; + + if (strcmp(subcmd, "Q") == 0) { + /* Query: Q */ + char *target, *query_subcmd_str, *ref, *reqid; + char query_subcmd_char; + const char *query_subcmd_full; + int limit, count; + struct HistoryMessage *messages = NULL; + struct HistoryMessage *msg; + enum HistoryRefType ref_type; + const char *ref_value; + + if (parc < 7) + return 0; + + target = parv[2]; + query_subcmd_str = parv[3]; + ref = parv[4]; + limit = atoi(parv[5]); + reqid = parv[6]; + + /* Propagate query to other servers (except source) - keep efficient format */ + sendcmdto_serv_butone(sptr, CMD_CHATHISTORY, cptr, "Q %s %s %s %d %s", + target, query_subcmd_str, ref, limit, reqid); + + /* Only process for channels (not PMs) */ + if (!IsChannelName(target)) { + /* Send empty response for PMs */ + sendcmdto_one(&me, CMD_CHATHISTORY, sptr, "E %s 0", reqid); + return 0; + } + + /* Check if we have history backend */ + if (!history_is_available()) { + sendcmdto_one(&me, CMD_CHATHISTORY, sptr, "E %s 0", reqid); + return 0; + } + + /* Parse S2S reference format (T..., M..., *) */ + if (parse_s2s_reference(ref, &ref_type, &ref_value) != 0) { + sendcmdto_one(&me, CMD_CHATHISTORY, sptr, "E %s 0", reqid); + return 0; + } + + /* Parse single-char subcmd */ + query_subcmd_char = query_subcmd_str[0]; + query_subcmd_full = s2s_to_subcmd(query_subcmd_char); + + /* Query local LMDB based on subcommand */ + if (query_subcmd_char == 'L') { + count = history_query_latest(target, ref_type, ref_value, limit, &messages); + } else if (query_subcmd_char == 'B') { + count = history_query_before(target, ref_type, ref_value, limit, &messages); + } else if (query_subcmd_char == 'A') { + count = history_query_after(target, ref_type, ref_value, limit, &messages); + } else if (query_subcmd_char == 'R') { + count = history_query_around(target, ref_type, ref_value, limit, &messages); + } else { + /* Unsupported subcommand for federation */ + sendcmdto_one(&me, CMD_CHATHISTORY, sptr, "E %s 0", reqid); + return 0; + } + + if (count <= 0) { + sendcmdto_one(&me, CMD_CHATHISTORY, sptr, "E %s 0", reqid); + return 0; + } + + /* Send response messages. + * Uses base64 chunked encoding for long content (>400 bytes). + * - CH R: normal response (content as-is) + * - CH B: base64 encoded response (with chunking if needed) + */ + for (msg = messages; msg; msg = msg->next) { + send_ch_response(sptr, reqid, msg); + } + + /* Send end marker */ + sendcmdto_one(&me, CMD_CHATHISTORY, sptr, "E %s %d", reqid, count); + + history_free_messages(messages); + } + else if (strcmp(subcmd, "R") == 0) { + /* Response: R : */ + char *reqid, *msgid, *timestamp, *sender, *account, *content; + int type; + struct FedRequest *req; + + if (parc < 8) + return 0; + + reqid = parv[2]; + msgid = parv[3]; + timestamp = parv[4]; + type = atoi(parv[5]); + sender = parv[6]; + account = parv[7]; + content = (parc > 8) ? parv[8] : ""; + + /* Find the request */ + req = find_fed_request(reqid); + if (!req) + return 0; /* Request not found or already completed */ + + /* Add message to federated results */ + add_fed_message(req, msgid, timestamp, type, sender, account, content); + } + else if (strcmp(subcmd, "B") == 0) { + /* Base64 Response: B [+] : + * Or continuation: B [+] : + * + marker means more chunks coming. No + means final. + */ + char *reqid, *msgid; + const char *b64_data; + struct FedRequest *req; + struct ChunkEntry *chunk; + char chunk_key[128]; + int has_more = 0; + int is_continuation = 0; + + if (parc < 4) + return 0; + + reqid = parv[2]; + msgid = parv[3]; + + /* Find the request */ + req = find_fed_request(reqid); + if (!req) + return 0; + + /* Determine message format based on parc: + * parc=5: continuation "B :" (final) + * parc=6: continuation "B + :" (more) + * parc=9: full "B :" (complete) + * parc=10: full "B + :" (first) + */ + if (parc <= 6) { + is_continuation = 1; + if (parc == 6 && strcmp(parv[4], "+") == 0) { + has_more = 1; + b64_data = parv[5]; + } else { + has_more = 0; + b64_data = parv[4]; + } + } else { + is_continuation = 0; + if (parc == 10 && strcmp(parv[8], "+") == 0) { + has_more = 1; + b64_data = parv[9]; + } else { + has_more = 0; + b64_data = (parc > 8) ? parv[8] : ""; + } + } + + /* Create chunk key */ + ircd_snprintf(0, chunk_key, sizeof(chunk_key), "%s:%s", reqid, msgid); + + if (is_continuation) { + chunk = find_chunk(chunk_key); + if (!chunk) + return 0; /* Continuation without start */ + } else { + chunk = create_chunk(reqid, msgid, parv[4], atoi(parv[5]), parv[6], parv[7]); + if (!chunk) + return 0; /* No slots available */ + } + + /* Append base64 data */ + append_chunk_data(chunk, b64_data); + + if (!has_more) { + /* Final chunk - decode and add to results */ + char *decoded; + size_t decoded_len; + + if (ch_base64_decode(chunk->b64_data, chunk->b64_len, &decoded, &decoded_len)) { + add_fed_message(req, chunk->msgid, chunk->timestamp, chunk->type, + chunk->sender, chunk->account, decoded); + MyFree(decoded); + } + free_chunk(chunk); + } + } + else if (strcmp(subcmd, "E") == 0) { + /* End: E */ + char *reqid; + int count; + struct FedRequest *req; + + if (parc < 4) + return 0; + + reqid = parv[2]; + count = atoi(parv[3]); + + /* Find the request */ + req = find_fed_request(reqid); + if (!req) + return 0; + + /* Decrement pending count */ + req->servers_pending--; + + /* If all servers have responded, complete the request */ + if (req->servers_pending <= 0) { + complete_fed_request(req); + } + } + + return 0; +} diff --git a/ircd/m_endburst.c b/ircd/m_endburst.c index 355d34c0..ec783754 100644 --- a/ircd/m_endburst.c +++ b/ircd/m_endburst.c @@ -115,9 +115,13 @@ int ms_end_of_burst(struct Client* cptr, struct Client* sptr, int parc, char* pa assert(0 != cptr); assert(0 != sptr); - sendto_opmask_butone(0, SNO_NETWORK, "Completed net.burst from %C.", + sendto_opmask_butone(0, SNO_NETWORK, "Completed net.burst from %C.", sptr); sendcmdto_serv_butone(sptr, CMD_END_OF_BURST, cptr, ""); + + /* End IRCv3 netjoin batch for local clients */ + send_netjoin_batch_end(sptr); + ClearBurst(sptr); SetBurstAck(sptr); if (MyConnect(sptr)) diff --git a/ircd/m_invite.c b/ircd/m_invite.c index 82232382..221c117c 100644 --- a/ircd/m_invite.c +++ b/ircd/m_invite.c @@ -81,6 +81,7 @@ */ #include "config.h" +#include "capab.h" #include "channel.h" #include "client.h" #include "hash.h" @@ -198,6 +199,12 @@ int m_invite(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) } if (!IsLocalChannel(chptr->chname) || MyConnect(acptr)) { + /* Send invite-notify to channel members with the capability */ + if (feature_bool(FEAT_CAP_invite_notify)) + sendcmdto_channel_capab_butserv_butone(sptr, CMD_INVITE, chptr, sptr, 0, + CAP_INVITENOTIFY, CAP_NONE, + "%C %H", acptr, chptr); + if (feature_bool(FEAT_ANNOUNCE_INVITES)) { /* Announce to channel operators. */ sendcmdto_channel_butserv_butone(&his, get_error_numeric(RPL_ISSUEDINVITE)->str, @@ -302,6 +309,12 @@ int ms_invite(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) chptr->creationtime); } + /* Send invite-notify to channel members with the capability */ + if (feature_bool(FEAT_CAP_invite_notify)) + sendcmdto_channel_capab_butserv_butone(sptr, CMD_INVITE, chptr, sptr, 0, + CAP_INVITENOTIFY, CAP_NONE, + "%C %H", acptr, chptr); + if (feature_bool(FEAT_ANNOUNCE_INVITES)) { /* Announce to channel operators. */ sendcmdto_channel_butserv_butone(&his, get_error_numeric(RPL_ISSUEDINVITE)->str, diff --git a/ircd/m_isupport.c b/ircd/m_isupport.c new file mode 100644 index 00000000..7bcfff53 --- /dev/null +++ b/ircd/m_isupport.c @@ -0,0 +1,64 @@ +/* + * IRC - Internet Relay Chat, ircd/m_isupport.c + * Copyright (C) 2024 AfterNET IRC Network + * + * 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. + */ + +/** @file + * @brief Handlers for ISUPPORT command (IRCv3 draft/extended-isupport). + * + * This implements the draft/extended-isupport extension which allows + * clients to request ISUPPORT (005) tokens before completing registration. + * + * @see https://ircv3.net/specs/extensions/extended-isupport + */ + +#include "config.h" + +#include "capab.h" +#include "client.h" +#include "ircd_reply.h" +#include "numeric.h" +#include "s_user.h" + +/** Handle an ISUPPORT command from a local client. + * + * Returns RPL_ISUPPORT (005) messages to the client. Requires the + * draft/extended-isupport capability to be negotiated. + * + * When the client also has batch capability, ISUPPORT is wrapped + * in a draft/isupport batch. + * + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message. + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + */ +int m_isupport(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + /* Check if capability negotiated */ + if (!HasCap(sptr, CAP_DRAFT_EXTISUPPORT)) + return send_reply(sptr, ERR_UNKNOWNCOMMAND, "ISUPPORT"); + + /* Send ISUPPORT - use batched version which will wrap in batch + * if client has batch capability, otherwise sends plain ISUPPORT */ + send_supported_batched(sptr); + + return 0; +} diff --git a/ircd/m_join.c b/ircd/m_join.c index cf52ba63..e894357f 100644 --- a/ircd/m_join.c +++ b/ircd/m_join.c @@ -25,6 +25,7 @@ #include "config.h" +#include "capab.h" #include "channel.h" #include "class.h" #include "client.h" @@ -43,6 +44,8 @@ #include "s_user.h" #include "send.h" #include "sys.h" +#include "handlers.h" +#include "ml_storage.h" /* #include -- Now using assert in ircd_log.h */ #include @@ -280,7 +283,12 @@ void do_join(struct Client *cptr, struct Client *sptr, struct JoinBuf *join, chptr->topic_time); } - do_names(sptr, chptr, NAMES_ALL|NAMES_EON); /* send /names list */ + /* Send MARKREAD for draft/read-marker before NAMES (per spec: after JOIN, before 366) */ + send_markread_on_join(sptr, chptr->chname); + + /* Skip implicit NAMES if client has draft/no-implicit-names capability */ + if (!HasCap(sptr, CAP_DRAFT_NOIMPLICITNAMES)) + do_names(sptr, chptr, NAMES_ALL|NAMES_EON); /* send /names list */ } /** Handle a JOIN message from a client connection. @@ -339,6 +347,17 @@ int m_join(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) continue; } + /* Check for virtual &ml- channel (multiline message retrieval) */ + if (ml_storage_is_virtual_channel(name)) { + if (!feature_bool(FEAT_MULTILINE_STORAGE_ENABLED)) { + send_reply(sptr, ERR_NOSUCHCHANNEL, name); + continue; + } + /* Deliver stored content and continue (no actual channel join) */ + ml_storage_deliver(sptr, name + 4); /* Skip "&ml-" prefix */ + continue; + } + if (cli_user(sptr)->joined >= get_client_maxchans(sptr) && !HasPriv(sptr, PRIV_CHAN_LIMIT)) { send_reply(sptr, ERR_TOOMANYCHANNELS, name); diff --git a/ircd/m_kick.c b/ircd/m_kick.c index 9ae7718f..22faee2c 100644 --- a/ircd/m_kick.c +++ b/ircd/m_kick.c @@ -87,14 +87,85 @@ #include "ircd.h" #include "ircd_log.h" #include "ircd_reply.h" +#include "ircd_snprintf.h" #include "ircd_string.h" #include "msg.h" #include "numeric.h" #include "numnicks.h" #include "send.h" #include "ircd_features.h" +#include "history.h" /* #include -- Now using assert in ircd_log.h */ +#include +#include + +#ifdef USE_LMDB +/** Counter for generating unique message IDs for KICK event history storage */ +static unsigned long kick_history_msgid_counter = 0; + +/** Store a KICK event in the history database. + * The message content is formatted as "kicked_nick :reason" per event-playback spec. + * @param[in] sptr Client that did the kick. + * @param[in] chptr Channel where kick occurred. + * @param[in] who Client that was kicked. + * @param[in] comment Kick reason. + */ +static void store_kick_event(struct Client *sptr, struct Channel *chptr, + struct Client *who, const char *comment) +{ + struct timeval tv; + char timestamp[32]; + char msgid[64]; + char sender[HISTORY_SENDER_LEN]; + char kick_text[512]; + const char *account; + + if (!history_is_available()) + return; + + /* Check if chathistory feature is enabled */ + if (!feature_bool(FEAT_CAP_draft_chathistory)) + return; + + /* Only store for local users to avoid duplicates */ + if (!MyUser(sptr)) + return; + + /* Generate Unix timestamp for storage */ + gettimeofday(&tv, NULL); + ircd_snprintf(0, timestamp, sizeof(timestamp), "%lu.%03lu", + (unsigned long)tv.tv_sec, + (unsigned long)(tv.tv_usec / 1000)); + + /* Generate unique msgid */ + ircd_snprintf(0, msgid, sizeof(msgid), "%s-%lu-%lu", + cli_yxx(&me), + (unsigned long)cli_firsttime(&me), + ++kick_history_msgid_counter); + + /* Build sender string: nick!user@host */ + if (cli_user(sptr)) + ircd_snprintf(0, sender, sizeof(sender), "%s!%s@%s", + cli_name(sptr), + cli_user(sptr)->username, + cli_user(sptr)->host); + else + ircd_strncpy(sender, cli_name(sptr), sizeof(sender) - 1); + + /* Get account name if logged in */ + account = (cli_user(sptr) && cli_user(sptr)->account[0]) + ? cli_user(sptr)->account : NULL; + + /* Build kick text: "kicked_nick :reason" */ + ircd_snprintf(0, kick_text, sizeof(kick_text), "%s :%s", + cli_name(who), comment ? comment : ""); + + /* Store in database */ + history_store_message(msgid, timestamp, chptr->chname, sender, + account, HISTORY_KICK, kick_text); +} +#endif /* USE_LMDB */ /* * m_kick - generic message handler @@ -183,6 +254,11 @@ int m_kick(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) sendcmdto_channel_butserv_butone(sptr, CMD_KICK, chptr, NULL, 0, "%H %C :%s", chptr, who, comment); +#ifdef USE_LMDB + /* Store KICK event in history */ + store_kick_event(sptr, chptr, who, comment); +#endif + make_zombie(member, who, cptr, sptr, chptr); return 0; diff --git a/ircd/m_mark.c b/ircd/m_mark.c index 08aafcb5..5d58a590 100644 --- a/ircd/m_mark.c +++ b/ircd/m_mark.c @@ -95,6 +95,8 @@ #include "send.h" #include "s_conf.h" +#include /* for strtoul */ + /* #include -- Now using assert in ircd_log.h */ /* @@ -141,6 +143,14 @@ int ms_mark(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) ircd_strncpy(cli_sslclifp(acptr), parv[3], BUFSIZE + 1); sendcmdto_serv_butone(sptr, CMD_MARK, cptr, "%s %s :%s", cli_name(acptr), MARK_SSLCLIFP, parv[3]); } + } else if (!strcmp(parv[2], MARK_SSLCLIEXP)) { + if(parc < 4) + return protocol_violation(sptr, "MARK SSL client certificate expiry received too few parameters (%u)", parc); + + if ((acptr = FindUser(parv[1]))) { + cli_sslcliexp(acptr) = strtoul(parv[3], NULL, 10); + sendcmdto_serv_butone(sptr, CMD_MARK, cptr, "%s %s :%s", cli_name(acptr), MARK_SSLCLIEXP, parv[3]); + } } else if (!strcmp(parv[2], MARK_KILL)) { if(parc < 4) return protocol_violation(sptr, "MARK kill received too few parameters (%u)", parc); diff --git a/ircd/m_markread.c b/ircd/m_markread.c new file mode 100644 index 00000000..0cb2bd1b --- /dev/null +++ b/ircd/m_markread.c @@ -0,0 +1,471 @@ +/* + * IRC - Internet Relay Chat, ircd/m_markread.c + * Copyright (C) 2024 Nefarious Development Team + * + * 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. + */ +/** @file + * @brief Handler for MARKREAD command (IRCv3 draft/read-marker). + * + * Specification: https://ircv3.net/specs/extensions/read-marker + * + * Client Protocol (ISO 8601 timestamps per IRCv3 spec): + * MARKREAD [timestamp=YYYY-MM-DDThh:mm:ss.sssZ] + * + * This implementation routes read markers through X3 services for + * authoritative storage and multi-device synchronization. + * + * P10 Protocol (Unix timestamps for S2S): + * SET: [SERVER] MR S + * GET: [SERVER] MR G + * REPLY: [X3] MR R + * BROADCAST: [X3] MR + * + * Timestamps are stored internally as Unix (seconds.milliseconds) and + * converted to ISO 8601 only for client-facing protocol. + */ +#include "config.h" + +#include "capab.h" +#include "client.h" +#include "hash.h" +#include "history.h" +#include "ircd.h" +#include "ircd_features.h" +#include "ircd_log.h" +#include "ircd_reply.h" +#include "ircd_snprintf.h" +#include "ircd_string.h" +#include "msg.h" +#include "numeric.h" +#include "numnicks.h" +#include "s_user.h" +#include "send.h" + +#include +#include + +/** Maximum timestamp length */ +#define MARKREAD_TS_LEN 32 + +/** Parse timestamp= parameter from client argument. + * Extracts ISO 8601 timestamp and converts to Unix timestamp for internal use. + * @param[in] arg Argument string (e.g., "timestamp=2025-01-01T00:00:00.000Z") + * @param[out] unix_ts Buffer for Unix timestamp (seconds.milliseconds). + * @param[in] tslen Size of unix_ts buffer. + * @return 1 if found and valid format, 0 otherwise. + */ +static int parse_timestamp_param(const char *arg, char *unix_ts, size_t tslen) +{ + const char *eq; + char iso_ts[32]; + + if (!arg) + return 0; + + /* Check for "timestamp=" prefix */ + if (ircd_strncmp(arg, "timestamp=", 10) != 0) + return 0; + + eq = arg + 10; + + /* Basic validation: must be at least YYYY-MM-DDThh:mm:ss format */ + if (strlen(eq) < 19) + return 0; + + /* Check for 'T' separator */ + if (eq[10] != 'T') + return 0; + + /* Copy ISO timestamp */ + ircd_strncpy(iso_ts, eq, sizeof(iso_ts) - 1); + iso_ts[sizeof(iso_ts) - 1] = '\0'; + + /* Convert to Unix timestamp for internal use */ + if (history_iso_to_unix(iso_ts, unix_ts, tslen) != 0) + return 0; + + return 1; +} + +/** Send MARKREAD response to a client. + * Converts internal Unix timestamp to ISO 8601 for client display. + * @param[in] to Client to send to. + * @param[in] target Channel or nick. + * @param[in] unix_ts Unix timestamp (or "*" if unknown). + */ +static void send_markread(struct Client *to, const char *target, const char *unix_ts) +{ + char iso_ts[32]; + + /* Format: MARKREAD timestamp= + * The timestamp can be "*" if unknown. + */ + if (!unix_ts || !*unix_ts || *unix_ts == '*') { + sendrawto_one(to, "MARKREAD %s timestamp=*", target); + } else if (history_unix_to_iso(unix_ts, iso_ts, sizeof(iso_ts)) == 0) { + sendrawto_one(to, "MARKREAD %s timestamp=%s", target, iso_ts); + } else { + /* Conversion failed - send as-is (might already be ISO or invalid) */ + sendrawto_one(to, "MARKREAD %s timestamp=%s", target, unix_ts); + } +} + +/** Notify all local clients with matching account about a read marker update. + * @param[in] account Account name. + * @param[in] target Channel or nick. + * @param[in] unix_ts Unix timestamp (will be converted to ISO for clients). + */ +static void notify_local_clients(const char *account, const char *target, const char *timestamp) +{ + struct Client *acptr; + + if (!account || !*account) + return; + + /* Find all local clients with the same account */ + for (acptr = GlobalClientList; acptr; acptr = cli_next(acptr)) { + if (!IsUser(acptr) || !MyUser(acptr)) + continue; + if (!CapActive(acptr, CAP_DRAFT_READMARKER)) + continue; + if (!cli_user(acptr) || !cli_user(acptr)->account[0]) + continue; + if (ircd_strcmp(cli_user(acptr)->account, account) != 0) + continue; + + send_markread(acptr, target, timestamp); + } +} + +/** Find the services server. + * @return Services server client or NULL if not found. + */ +static struct Client *find_services_server(void) +{ + struct Client *acptr; + + for (acptr = GlobalClientList; acptr; acptr = cli_next(acptr)) { + if (IsServer(acptr) && IsService(acptr)) + return acptr; + } + return NULL; +} + +/** m_markread - Handle MARKREAD command from local client. + * + * parv[0] = sender prefix + * parv[1] = target (channel or nick) + * parv[2] = timestamp=YYYY-MM-DDThh:mm:ss.sssZ (optional) + * + * If timestamp is provided: set read marker (send to X3) + * If no timestamp: query current read marker (from local cache or X3) + * + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message. + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + */ +int m_markread(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + const char *target; + const char *account; + char timestamp[MARKREAD_TS_LEN]; + char stored_ts[MARKREAD_TS_LEN]; + struct Client *services; + int rc; + + /* Must have draft/read-marker capability */ + if (!CapActive(sptr, CAP_DRAFT_READMARKER)) { + return send_reply(sptr, ERR_UNKNOWNCOMMAND, "MARKREAD"); + } + + /* Must be logged in */ + if (!cli_user(sptr) || !cli_user(sptr)->account[0]) { + send_fail(sptr, "MARKREAD", "ACCOUNT_REQUIRED", NULL, + "You must be logged in to use MARKREAD"); + return 0; + } + + account = cli_user(sptr)->account; + + /* Need at least target */ + if (parc < 2 || EmptyString(parv[1])) { + send_fail(sptr, "MARKREAD", "NEED_MORE_PARAMS", NULL, + "Missing target parameter"); + return 0; + } + + target = parv[1]; + + /* Find services for forwarding */ + services = find_services_server(); + + /* Check if timestamp parameter is present */ + if (parc >= 3 && parv[2] && ircd_strncmp(parv[2], "timestamp=", 10) == 0) { + /* Timestamp parameter provided - must be valid format for SET operation */ + if (!parse_timestamp_param(parv[2], timestamp, sizeof(timestamp))) { + /* Invalid timestamp format */ + send_fail(sptr, "MARKREAD", "INVALID_PARAMS", parv[2], + "Invalid timestamp format (expected ISO 8601)"); + return 0; + } + /* SET operation: send to X3 for storage and broadcast */ + + if (services) { + /* Forward to X3: MR S + * X3 will store and broadcast back to all servers + */ + sendcmdto_one(&me, CMD_MARKREAD, services, "S %C %s %s", + sptr, target, timestamp); + } + + /* Also store locally in LMDB as cache (if available) */ + if (history_is_available()) { + rc = readmarker_set(account, target, timestamp); + if (rc == 0) { + /* Successfully updated locally - notify local clients immediately */ + notify_local_clients(account, target, timestamp); + } else if (rc == 1) { + /* Timestamp was not newer - respond with current stored value */ + rc = readmarker_get(account, target, stored_ts); + if (rc == 0) { + send_markread(sptr, target, stored_ts); + } else { + send_markread(sptr, target, timestamp); + } + } else { + /* Error storing - still notify client of what they sent */ + send_markread(sptr, target, timestamp); + } + } else if (!services) { + /* No services and no LMDB - cannot store */ + send_fail(sptr, "MARKREAD", "TEMPORARILY_UNAVAILABLE", target, + "Read marker storage is not available"); + } else { + /* No LMDB but have services - notify the sending client */ + send_markread(sptr, target, timestamp); + } + } else { + /* GET operation: query current timestamp from local cache */ + if (history_is_available()) { + rc = readmarker_get(account, target, stored_ts); + if (rc == 0) { + send_markread(sptr, target, stored_ts); + } else if (rc == 1) { + /* Not found locally */ + if (services) { + /* Query X3: MR G */ + sendcmdto_one(&me, CMD_MARKREAD, services, "G %C %s", sptr, target); + /* Response will come back via ms_markread */ + } else { + /* No services, no local data - send "*" */ + send_markread(sptr, target, "*"); + } + } else { + send_fail(sptr, "MARKREAD", "INTERNAL_ERROR", target, + "Could not retrieve read marker"); + } + } else if (services) { + /* No LMDB, query X3 */ + sendcmdto_one(&me, CMD_MARKREAD, services, "G %C %s", sptr, target); + } else { + send_fail(sptr, "MARKREAD", "TEMPORARILY_UNAVAILABLE", target, + "Read marker storage is not available"); + } + } + + return 0; +} + +/** ms_markread - Handle MARKREAD command from server. + * + * P10 formats: + * Broadcast from X3: MR + * Reply to query: MR R + * Forward set: MR S + * Forward get: MR G + * + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message (X3 or another server). + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + */ +int ms_markread(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + const char *subcmd; + const char *account; + const char *target; + const char *timestamp; + struct Client *acptr; + struct Client *services; + int is_from_services = 0; + + if (parc < 2) + return 0; + + /* Check if this is from a services server */ + if (IsServer(sptr) && IsService(sptr)) { + is_from_services = 1; + } else if (!IsServer(sptr) && cli_user(sptr) && + cli_user(sptr)->server && IsService(cli_user(sptr)->server)) { + is_from_services = 1; + } + + subcmd = parv[1]; + + /* Check for subcmd-style messages (S, G, R) */ + if (subcmd[0] == 'S' && subcmd[1] == '\0') { + /* SET forward: MR S */ + if (parc < 5) + return 0; + + /* Find the user */ + acptr = findNUser(parv[2]); + if (!acptr || !IsUser(acptr)) + return 0; + + target = parv[3]; + timestamp = parv[4]; + account = cli_user(acptr)->account; + + if (!account || !*account) + return 0; + + /* If not from services, forward toward X3 (multi-hop routing) */ + services = find_services_server(); + if (!is_from_services) { + if (services) { + /* Forward toward X3 - sendcmdto_one routes through intermediate servers */ + sendcmdto_one(sptr, CMD_MARKREAD, services, "S %s %s %s", + parv[2], target, timestamp); + } + /* Don't propagate further until X3 broadcasts */ + + /* Store locally if LMDB available (as cache) */ + if (history_is_available()) { + readmarker_set(account, target, timestamp); + } + } + /* If from services, this was part of X3's processing - ignore S format from services */ + + return 0; + } + + if (subcmd[0] == 'G' && subcmd[1] == '\0') { + /* GET forward: MR G */ + if (parc < 4) + return 0; + + /* Find the user - may not exist anymore */ + acptr = findNUser(parv[2]); + target = parv[3]; + + /* If not from services, forward toward X3 (multi-hop routing) */ + services = find_services_server(); + if (!is_from_services && services) { + /* Forward toward X3 - sendcmdto_one routes through intermediate servers */ + sendcmdto_one(sptr, CMD_MARKREAD, services, "G %s %s", parv[2], target); + } + + return 0; + } + + if (subcmd[0] == 'R' && subcmd[1] == '\0') { + /* Reply: MR R */ + if (parc < 6) + return 0; + + /* Find the target user */ + acptr = findNUser(parv[3]); + if (!acptr || !IsUser(acptr)) + return 0; + + target = parv[4]; + timestamp = parv[5]; + + /* If user is local, send MARKREAD response */ + if (MyUser(acptr) && CapActive(acptr, CAP_DRAFT_READMARKER)) { + send_markread(acptr, target, timestamp); + } else { + /* Forward toward the user's server */ + sendcmdto_one(sptr, CMD_MARKREAD, cli_from(acptr), "R %s %s %s %s", + parv[2], parv[3], target, timestamp); + } + + /* Cache locally if available */ + if (history_is_available() && cli_user(acptr) && cli_user(acptr)->account[0]) { + readmarker_set(cli_user(acptr)->account, target, timestamp); + } + + return 0; + } + + /* Broadcast format from X3: MR */ + if (parc < 4) + return 0; + + account = parv[1]; + target = parv[2]; + timestamp = parv[3]; + + /* Cache locally if LMDB available */ + if (history_is_available()) { + readmarker_set(account, target, timestamp); + } + + /* Notify local clients with this account */ + notify_local_clients(account, target, timestamp); + + /* Propagate to other servers */ + sendcmdto_serv_butone(sptr, CMD_MARKREAD, cptr, "%s %s %s", + account, target, timestamp); + + return 0; +} + +/** Send MARKREAD for a target to a client after JOIN. + * Called from m_join.c after sending JOIN but before RPL_ENDOFNAMES. + * @param[in] sptr Client to send to. + * @param[in] target Channel name. + */ +void send_markread_on_join(struct Client *sptr, const char *target) +{ + const char *account; + char stored_ts[MARKREAD_TS_LEN]; + int rc; + + /* Only for clients with draft/read-marker capability */ + if (!CapActive(sptr, CAP_DRAFT_READMARKER)) + return; + + /* Must be logged in */ + if (!cli_user(sptr) || !cli_user(sptr)->account[0]) + return; + + /* Check if readmarker subsystem is available */ + if (!history_is_available()) + return; + + account = cli_user(sptr)->account; + + rc = readmarker_get(account, target, stored_ts); + if (rc == 0) { + send_markread(sptr, target, stored_ts); + } else { + /* No stored marker - send "*" */ + send_markread(sptr, target, "*"); + } +} diff --git a/ircd/m_metadata.c b/ircd/m_metadata.c new file mode 100644 index 00000000..d047f67d --- /dev/null +++ b/ircd/m_metadata.c @@ -0,0 +1,1404 @@ +/* + * IRC - Internet Relay Chat, ircd/m_metadata.c + * Copyright (C) 2024 Nefarious Development Team + * + * 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. + */ +/** @file + * @brief Handler for METADATA command (IRCv3 draft/metadata-2). + * + * Specification: https://ircv3.net/specs/extensions/metadata + * + * Subcommands: + * GET [...] + * SET [] + * LIST + * CLEAR + * SUB [...] + * UNSUB [...] + * SUBS + * SYNC [] + */ +#include "config.h" + +#include "capab.h" +#include "channel.h" +#include "client.h" +#include "hash.h" +#include "ircd.h" +#include "ircd_alloc.h" +#include "ircd_features.h" +#include "ircd_log.h" +#include "ircd_reply.h" +#include "ircd_snprintf.h" +#include "ircd_string.h" +#include "metadata.h" +#include "msg.h" +#include "numeric.h" +#include "s_bsd.h" +#include "s_user.h" +#include "send.h" +#include "ircd_compress.h" + +#include +#include +#include +#include + +/* Forward declarations */ +static int metadata_cmd_get(struct Client *sptr, int parc, char *parv[]); +static int metadata_cmd_set(struct Client *sptr, int parc, char *parv[]); +static int metadata_cmd_list(struct Client *sptr, int parc, char *parv[]); +static int metadata_cmd_clear(struct Client *sptr, int parc, char *parv[]); +static int metadata_cmd_sub(struct Client *sptr, int parc, char *parv[]); +static int metadata_cmd_unsub(struct Client *sptr, int parc, char *parv[]); +static int metadata_cmd_subs(struct Client *sptr, int parc, char *parv[]); +static int metadata_cmd_sync(struct Client *sptr, int parc, char *parv[]); +static void notify_subscribers(const char *target, const char *key, const char *value); + +/** Decode base64 data for compression passthrough. + * @param[in] input Base64 encoded string. + * @param[out] output Buffer for decoded data. + * @param[in] output_size Size of output buffer. + * @param[out] decoded_len Actual decoded length. + * @return 1 on success, 0 on error. + */ +static int base64_decode(const char *input, unsigned char *output, + size_t output_size, size_t *decoded_len) +{ + int inlen = strlen(input); + int outlen; + + /* EVP_DecodeBlock requires output buffer of at least 3/4 of input */ + if ((size_t)inlen * 3 / 4 > output_size) + return 0; + + outlen = EVP_DecodeBlock(output, (const unsigned char *)input, inlen); + if (outlen < 0) + return 0; + + /* EVP_DecodeBlock doesn't account for padding, adjust for = characters */ + if (inlen > 0 && input[inlen - 1] == '=') { + outlen--; + if (inlen > 1 && input[inlen - 2] == '=') + outlen--; + } + + *decoded_len = outlen; + return 1; +} + +/** Check if key is valid per spec (letters, digits, hyphens, underscores, dots, colons, forward slashes) + * and doesn't start with a digit. + */ +static int is_valid_key(const char *key) +{ + const char *p; + + if (!key || !*key) + return 0; + + /* Cannot start with a digit */ + if (isdigit((unsigned char)key[0])) + return 0; + + /* Check all characters */ + for (p = key; *p; p++) { + if (!isalnum((unsigned char)*p) && *p != '-' && *p != '_' && *p != '.' && *p != ':' && *p != '/') + return 0; + } + + /* Check length */ + if (strlen(key) > METADATA_KEY_LEN) + return 0; + + return 1; +} + +/** Check if client can see metadata on target. + * @param[in] sptr Client requesting. + * @param[in] target Target client or channel name. + * @param[out] is_channel Set to 1 if target is channel. + * @param[out] target_client Set to target client if user target. + * @param[out] target_channel Set to target channel if channel target. + * @return 1 if can view, 0 if not. + */ +static int can_see_target(struct Client *sptr, const char *target, int *is_channel, + struct Client **target_client, struct Channel **target_channel) +{ + *is_channel = 0; + *target_client = NULL; + *target_channel = NULL; + + if (IsChannelName(target)) { + *is_channel = 1; + *target_channel = FindChannel(target); + if (!*target_channel) + return 0; + /* Anyone can view channel metadata if channel is visible to them */ + if (!ShowChannel(sptr, *target_channel) && !IsOper(sptr)) + return 0; + return 1; + } else if (*target == '*') { + /* Self reference */ + *target_client = sptr; + return 1; + } else { + *target_client = FindUser(target); + if (!*target_client) + return 0; + /* Can always see metadata of visible users */ + return 1; + } +} + +/** Check if client can modify metadata on target. + * @param[in] sptr Client modifying. + * @param[in] target Target (client or channel). + * @param[in] is_channel 1 if channel target. + * @param[in] target_client Target client if user. + * @param[in] target_channel Target channel if channel. + * @return 1 if can modify, 0 if not. + */ +static int can_modify_target(struct Client *sptr, const char *target, int is_channel, + struct Client *target_client, struct Channel *target_channel) +{ + if (is_channel) { + struct Membership *member; + if (!target_channel) + return 0; + /* Must be chanop or halfop to modify channel metadata */ + member = find_member_link(target_channel, sptr); + if (!member) + return 0; + if (!IsChanOp(member) && !IsHalfOp(member) && !IsOper(sptr)) + return 0; + return 1; + } else { + /* Can only modify own metadata */ + if (target_client != sptr && !IsOper(sptr)) + return 0; + return 1; + } +} + +/** Notify all clients subscribed to a metadata key about a change. + * @param[in] target Target name (nick or channel). + * @param[in] key Metadata key that changed. + * @param[in] value New value (NULL if deleted). + */ +static void notify_subscribers(const char *target, const char *key, const char *value) +{ + struct Client *acptr; + int fd; + + /* Iterate over all local clients */ + for (fd = HighestFd; fd >= 0; --fd) { + if (!(acptr = LocalClientArray[fd])) + continue; + if (!IsUser(acptr)) + continue; + if (!CapActive(acptr, CAP_DRAFT_METADATA2)) + continue; + + /* Check if subscribed to this key */ + if (!metadata_sub_check(acptr, key)) + continue; + + /* Send notification: METADATA [*] : */ + if (value && *value) { + sendrawto_one(acptr, ":%s METADATA %s %s * :%s", + cli_name(&me), target, key, value); + } else { + sendrawto_one(acptr, ":%s METADATA %s %s * :", + cli_name(&me), target, key); + } + } +} + +/** Check if a client can see a specific metadata entry. + * @param[in] viewer Client requesting to view. + * @param[in] owner Client that owns the metadata (NULL for channels). + * @param[in] entry Metadata entry to check. + * @return 1 if visible, 0 if not. + */ +static int can_view_metadata(struct Client *viewer, struct Client *owner, + struct MetadataEntry *entry) +{ + if (!entry) + return 0; + + /* Public metadata is visible to all */ + if (entry->visibility == METADATA_VIS_PUBLIC) + return 1; + + /* Private metadata visible to owner */ + if (owner && owner == viewer) + return 1; + + /* Opers can see all metadata */ + if (IsOper(viewer)) + return 1; + + return 0; +} + +/** Get visibility string for metadata entry. + * @param[in] entry Metadata entry. + * @return "*" for public, "private" for private. + */ +static const char *get_visibility_str(struct MetadataEntry *entry) +{ + if (entry && entry->visibility == METADATA_VIS_PRIVATE) + return "private"; + return "*"; +} + +/** Send a KEYVALUE reply. + * Format: : 761 : + */ +static void send_keyvalue(struct Client *to, const char *target, const char *key, + const char *value, const char *visibility) +{ + if (value && *value) + send_reply(to, RPL_KEYVALUE, target, key, visibility ? visibility : "*", value); + else + send_reply(to, RPL_KEYNOTSET, target, key); +} + +/** Handle GET subcommand. + * METADATA GET [...] + * + * Flow: + * 1. If target is online user/channel, get from memory + * 2. If target is offline, check LMDB cache + * 3. If not in LMDB, send MDQ to X3 (response will be async) + */ +static int metadata_cmd_get(struct Client *sptr, int parc, char *parv[]) +{ + const char *target; + int is_channel = 0; + struct Client *target_client = NULL; + struct Channel *target_channel = NULL; + int i; + int target_found; + + if (parc < 4) { + send_fail(sptr, "METADATA", "INVALID_PARAMS", NULL, + "GET requires target and at least one key"); + return 0; + } + + target = parv[2]; + + /* Check if target exists online */ + target_found = can_see_target(sptr, target, &is_channel, &target_client, &target_channel); + + /* Process each key */ + for (i = 3; i < parc; i++) { + const char *key = parv[i]; + struct MetadataEntry *entry = NULL; + int found = 0; + + if (!is_valid_key(key)) { + send_fail(sptr, "METADATA", "KEY_INVALID", key, + "Invalid key name"); + continue; + } + + if (target_found) { + /* Target is online - get from memory */ + if (is_channel) { + entry = metadata_get_channel(target_channel, key); + } else { + entry = metadata_get_client(target_client, key); + } + + if (entry) { + /* Check visibility */ + if (can_view_metadata(sptr, is_channel ? NULL : target_client, entry)) { + send_keyvalue(sptr, target, key, entry->value, get_visibility_str(entry)); + found = 1; + } + } + } + + /* For registered users/channels, if not in memory, check LMDB cache then X3 */ + if (!found && !is_channel) { + /* Get account name for cache lookup */ + const char *account = NULL; + char value_buf[METADATA_VALUE_LEN + 1]; + + if (target_client && IsAccount(target_client)) { + /* Online registered user - use their account name for cache lookup */ + account = cli_account(target_client); + } else if (!target_found && !IsChannelName(target)) { + /* Offline user - target is the account name */ + account = target; + } + + if (account && metadata_lmdb_is_available()) { + if (metadata_account_get(account, key, value_buf) == 0) { + /* Found in LMDB cache */ + const char *vis_str = "*"; + const char *val = value_buf; + + /* Parse visibility prefix */ + if (val[0] == 'P' && val[1] == ':') { + vis_str = "private"; + val = val + 2; + } + + /* Check visibility for private metadata from LMDB */ + if (vis_str[0] == 'p') { + /* Private metadata - check if viewer is owner or oper */ + int can_view = 0; + if (IsOper(sptr)) + can_view = 1; + else if (IsAccount(sptr) && ircd_strcmp(cli_account(sptr), account) == 0) + can_view = 1; + if (!can_view) + continue; /* Skip to next key, don't reveal private data */ + } + + if (*val) { + send_keyvalue(sptr, target, key, val, vis_str); + found = 1; + + /* For online users, also load into memory for faster subsequent access */ + if (target_client) { + metadata_set_client(target_client, key, val, + (vis_str[0] == 'p') ? METADATA_VIS_PRIVATE : METADATA_VIS_PUBLIC); + } + } + } + } + + /* If still not found and we have an account, query X3 (authoritative source) */ + if (!found && account) { + if (metadata_send_query(sptr, account, key) == 0) { + /* Query sent - response will be async, don't send NOT_SET yet */ + continue; + } + } + } + + /* For channels, check LMDB cache and X3 for registered channel metadata */ + if (!found && is_channel) { + char value_buf[METADATA_VALUE_LEN + 1]; + + /* First check LMDB cache (works for both existing and non-existent channels) */ + if (metadata_lmdb_is_available()) { + if (metadata_account_get(target, key, value_buf) == 0) { + /* Found in LMDB cache - load into channel memory */ + const char *vis_str = "*"; + const char *val = value_buf; + + if (val[0] == 'P' && val[1] == ':') { + vis_str = "private"; + val = val + 2; + } + + /* Check visibility for private channel metadata from LMDB */ + if (vis_str[0] == 'p') { + /* Private channel metadata - visible to chanops and opers only */ + int can_view = 0; + if (IsOper(sptr)) + can_view = 1; + else if (target_channel) { + struct Membership *member = find_member_link(target_channel, sptr); + if (member && IsChanOp(member)) + can_view = 1; + } + if (!can_view) + continue; /* Skip, don't reveal private channel data */ + } + + if (*val) { + send_keyvalue(sptr, target, key, val, vis_str); + found = 1; + + /* Load into channel's in-memory metadata (if channel exists) */ + if (target_channel) { + metadata_set_channel(target_channel, key, val, + (vis_str[0] == 'p') ? METADATA_VIS_PRIVATE : METADATA_VIS_PUBLIC); + } + } + } + } + + /* Query X3 for registered channel metadata if not cached */ + if (!found) { + if (metadata_send_query(sptr, target, key) == 0) { + continue; + } + } + } + + if (!found) { + send_reply(sptr, RPL_KEYNOTSET, target, key); + } + } + + return 0; +} + +/** Parse visibility string. + * @param[in] vis Visibility string ("*" or "private"). + * @return METADATA_VIS_PUBLIC or METADATA_VIS_PRIVATE. + */ +static int parse_visibility(const char *vis) +{ + if (vis && ircd_strcmp(vis, "private") == 0) + return METADATA_VIS_PRIVATE; + return METADATA_VIS_PUBLIC; +} + +/** Handle SET subcommand. + * METADATA SET [] [] + * If no value, deletes the key. + * Visibility is "*" for public (default) or "private" for private. + */ +static int metadata_cmd_set(struct Client *sptr, int parc, char *parv[]) +{ + const char *target; + const char *key; + const char *value = NULL; + int visibility = METADATA_VIS_PUBLIC; + int is_channel = 0; + struct Client *target_client = NULL; + struct Channel *target_channel = NULL; + int max_keys, max_value_bytes; + int current_count; + int rc; + + if (parc < 4) { + send_fail(sptr, "METADATA", "INVALID_PARAMS", NULL, + "SET requires target and key"); + return 0; + } + + target = parv[2]; + key = parv[3]; + + /* Parse optional visibility and value. + * Format options: + * SET target key -> delete + * SET target key :value -> set public (value starts with :) + * SET target key * :value -> set public + * SET target key private :value -> set private + */ + if (parc >= 5) { + /* Check if parv[4] is visibility or value */ + if (parv[4][0] == '*' && parv[4][1] == '\0') { + /* Explicit public visibility */ + visibility = METADATA_VIS_PUBLIC; + if (parc >= 6) + value = parv[5]; + } else if (ircd_strcmp(parv[4], "private") == 0) { + /* Private visibility */ + visibility = METADATA_VIS_PRIVATE; + if (parc >= 6) + value = parv[5]; + } else { + /* No explicit visibility, parv[4] is the value */ + value = parv[4]; + } + } + + if (!is_valid_key(key)) { + send_fail(sptr, "METADATA", "KEY_INVALID", key, + "Invalid key name"); + return 0; + } + + if (!can_see_target(sptr, target, &is_channel, &target_client, &target_channel)) { + send_fail(sptr, "METADATA", "TARGET_INVALID", target, + "Invalid target"); + return 0; + } + + if (!can_modify_target(sptr, target, is_channel, target_client, target_channel)) { + send_fail(sptr, "METADATA", "KEY_NO_PERMISSION", key, + "You don't have permission to set metadata on this target"); + return 0; + } + + /* Check limits */ + max_keys = feature_int(FEAT_METADATA_MAX_KEYS); + max_value_bytes = feature_int(FEAT_METADATA_MAX_VALUE_BYTES); + + if (value && strlen(value) > max_value_bytes) { + send_fail(sptr, "METADATA", "VALUE_INVALID", key, + "value is too long or not UTF8"); + return 0; + } + + /* Check key count limit if setting new key */ + if (value) { + if (is_channel) { + current_count = metadata_count_channel(target_channel); + if (!metadata_get_channel(target_channel, key) && current_count >= max_keys) { + send_fail(sptr, "METADATA", "LIMIT_REACHED", key, + "Maximum number of metadata keys reached"); + return 0; + } + } else { + current_count = metadata_count_client(target_client); + if (!metadata_get_client(target_client, key) && current_count >= max_keys) { + send_fail(sptr, "METADATA", "LIMIT_REACHED", key, + "Maximum number of metadata keys reached"); + return 0; + } + } + } + + /* Perform the set/delete */ + if (is_channel) { + rc = metadata_set_channel(target_channel, key, value, visibility); + } else { + rc = metadata_set_client(target_client, key, value, visibility); + } + + if (rc < 0) { + send_fail(sptr, "METADATA", "INTERNAL_ERROR", key, + "Failed to set metadata"); + return 0; + } + + /* Send confirmation with visibility */ + send_keyvalue(sptr, target, key, value, + visibility == METADATA_VIS_PRIVATE ? "private" : "*"); + + /* Notify local subscribers (only for public metadata) */ + if (visibility == METADATA_VIS_PUBLIC) { + notify_subscribers(target, key, value); + } + + /* Propagate to other servers with visibility */ + if (value) { + sendcmdto_serv_butone(sptr, CMD_METADATA, NULL, "%s %s %s :%s", + target, key, + visibility == METADATA_VIS_PRIVATE ? "P" : "*", + value); + } else { + sendcmdto_serv_butone(sptr, CMD_METADATA, NULL, "%s %s", + target, key); + } + + return 0; +} + +/** Handle LIST subcommand. + * METADATA LIST + */ +static int metadata_cmd_list(struct Client *sptr, int parc, char *parv[]) +{ + const char *target; + int is_channel = 0; + struct Client *target_client = NULL; + struct Channel *target_channel = NULL; + struct MetadataEntry *entry; + + if (parc < 3) { + send_fail(sptr, "METADATA", "INVALID_PARAMS", NULL, + "LIST requires a target"); + return 0; + } + + target = parv[2]; + + if (!can_see_target(sptr, target, &is_channel, &target_client, &target_channel)) { + send_fail(sptr, "METADATA", "TARGET_INVALID", target, + "Invalid target"); + return 0; + } + + /* List all keys for target */ + if (is_channel) { + entry = metadata_list_channel(target_channel); + } else { + entry = metadata_list_client(target_client); + } + + while (entry) { + /* Check visibility using helper function */ + if (can_view_metadata(sptr, is_channel ? NULL : target_client, entry)) { + send_keyvalue(sptr, target, entry->key, entry->value, get_visibility_str(entry)); + } + entry = entry->next; + } + + /* Send end of list */ + send_reply(sptr, RPL_METADATAEND, target); + return 0; +} + +/** Handle CLEAR subcommand. + * METADATA CLEAR + */ +static int metadata_cmd_clear(struct Client *sptr, int parc, char *parv[]) +{ + const char *target; + int is_channel = 0; + struct Client *target_client = NULL; + struct Channel *target_channel = NULL; + + if (parc < 3) { + send_fail(sptr, "METADATA", "INVALID_PARAMS", NULL, + "CLEAR requires a target"); + return 0; + } + + target = parv[2]; + + if (!can_see_target(sptr, target, &is_channel, &target_client, &target_channel)) { + send_fail(sptr, "METADATA", "TARGET_INVALID", target, + "Invalid target"); + return 0; + } + + if (!can_modify_target(sptr, target, is_channel, target_client, target_channel)) { + send_fail(sptr, "METADATA", "KEY_NO_PERMISSION", "*", + "You don't have permission to clear metadata on this target"); + return 0; + } + + if (is_channel) { + metadata_clear_channel(target_channel); + } else { + metadata_clear_client(target_client); + } + + /* Confirmation - send empty keyvalue? */ + return 0; +} + +/** Handle SUB subcommand. + * METADATA SUB [...] + */ +static int metadata_cmd_sub(struct Client *sptr, int parc, char *parv[]) +{ + int i; + int max_subs; + + if (parc < 3) { + send_fail(sptr, "METADATA", "INVALID_PARAMS", NULL, + "SUB requires at least one key"); + return 0; + } + + max_subs = feature_int(FEAT_METADATA_MAX_SUBS); + + for (i = 2; i < parc; i++) { + const char *key = parv[i]; + + if (!is_valid_key(key)) { + send_fail(sptr, "METADATA", "KEY_INVALID", key, + "Invalid key name"); + continue; + } + + /* Check if already at limit */ + if (metadata_sub_count(sptr) >= max_subs) { + send_fail(sptr, "METADATA", "LIMIT_REACHED", key, + "Maximum number of subscriptions reached"); + break; + } + + if (metadata_sub_add(sptr, key) == 0) { + send_reply(sptr, RPL_METADATASUBOK, key); + } + } + + return 0; +} + +/** Handle UNSUB subcommand. + * METADATA UNSUB [...] + */ +static int metadata_cmd_unsub(struct Client *sptr, int parc, char *parv[]) +{ + int i; + + if (parc < 3) { + send_fail(sptr, "METADATA", "INVALID_PARAMS", NULL, + "UNSUB requires at least one key"); + return 0; + } + + for (i = 2; i < parc; i++) { + const char *key = parv[i]; + + if (!is_valid_key(key)) { + send_fail(sptr, "METADATA", "KEY_INVALID", key, + "Invalid key name"); + continue; + } + + if (metadata_sub_del(sptr, key) == 0) { + send_reply(sptr, RPL_METADATAUNSUBOK, key); + } + } + + return 0; +} + +/** Handle SUBS subcommand. + * METADATA SUBS + * Lists all current subscriptions. + */ +static int metadata_cmd_subs(struct Client *sptr, int parc, char *parv[]) +{ + struct MetadataSub *sub; + + sub = metadata_sub_list(sptr); + while (sub) { + send_reply(sptr, RPL_METADATASUBS, sub->key); + sub = sub->next; + } + + return 0; +} + +/** Send subscribed metadata for a target to client within a batch. + * @param[in] sptr Client requesting sync. + * @param[in] target Target name (nick or channel). + * @param[in] target_client Client if user target. + * @param[in] target_channel Channel if channel target. + * @param[in] is_channel 1 if channel, 0 if user. + * @return Number of metadata items sent. + */ +static int sync_target_metadata(struct Client *sptr, const char *target, + struct Client *target_client, + struct Channel *target_channel, + int is_channel) +{ + struct MetadataEntry *entry; + struct MetadataSub *sub; + int count = 0; + + /* Get metadata list for target */ + if (is_channel) { + entry = metadata_list_channel(target_channel); + } else { + entry = metadata_list_client(target_client); + } + + /* Send each metadata item if client is subscribed to that key */ + while (entry) { + /* Check if client is subscribed to this key */ + if (metadata_sub_check(sptr, entry->key)) { + /* Send metadata notification */ + if (entry->value && *entry->value) { + sendrawto_one(sptr, "@batch=%s :%s METADATA %s %s * :%s", + cli_batch_id(sptr), cli_name(&me), target, + entry->key, entry->value); + } else { + sendrawto_one(sptr, "@batch=%s :%s METADATA %s %s * :", + cli_batch_id(sptr), cli_name(&me), target, + entry->key); + } + count++; + } + entry = entry->next; + } + + return count; +} + +/** Handle SYNC subcommand. + * METADATA SYNC + * Requests all subscribed metadata for target. + * For channels, includes metadata for all users in the channel. + */ +static int metadata_cmd_sync(struct Client *sptr, int parc, char *parv[]) +{ + const char *target; + int is_channel = 0; + struct Client *target_client = NULL; + struct Channel *target_channel = NULL; + struct Membership *member; + int count = 0; + + if (parc < 3) { + send_fail(sptr, "METADATA", "INVALID_PARAMS", NULL, + "SYNC requires a target"); + return 0; + } + + target = parv[2]; + + if (!can_see_target(sptr, target, &is_channel, &target_client, &target_channel)) { + send_fail(sptr, "METADATA", "TARGET_INVALID", target, + "Invalid target"); + return 0; + } + + /* Check if client has any subscriptions */ + if (metadata_sub_count(sptr) == 0) { + /* No subscriptions - nothing to sync */ + return 0; + } + + /* Start metadata batch */ + send_batch_start(sptr, "metadata"); + + /* If no active batch (client doesn't support batch), send later */ + if (!cli_batch_id(sptr)[0]) { + send_reply(sptr, RPL_METADATASYNCLATER, target); + return 0; + } + + if (is_channel) { + /* Sync channel metadata */ + count += sync_target_metadata(sptr, target, NULL, target_channel, 1); + + /* Sync metadata for all users in the channel */ + for (member = target_channel->members; member; member = member->next_member) { + struct Client *member_client = member->user; + if (member_client && IsUser(member_client)) { + count += sync_target_metadata(sptr, cli_name(member_client), + member_client, NULL, 0); + } + } + } else { + /* Sync user metadata */ + count += sync_target_metadata(sptr, target, target_client, NULL, 0); + } + + /* End metadata batch */ + send_batch_end(sptr); + + return 0; +} + +/** Check and update rate limiting for metadata commands. + * Uses a token bucket style limiter: allows burst up to limit per second, + * then rejects until the next second. + * @param[in] sptr Client sending the command. + * @return 1 if rate limited (reject), 0 if ok to proceed. + */ +static int check_metadata_rate_limit(struct Client *sptr) +{ + int rate_limit = feature_int(FEAT_METADATA_RATE_LIMIT); + + /* Rate limit of 0 disables limiting */ + if (rate_limit <= 0) + return 0; + + /* Opers bypass rate limiting */ + if (IsOper(sptr)) + return 0; + + /* Reset counter if we're in a new second */ + if (cli_metadata_lastcmd(sptr) != CurrentTime) { + cli_metadata_lastcmd(sptr) = CurrentTime; + cli_metadata_cmdcnt(sptr) = 1; + return 0; + } + + /* Increment and check */ + cli_metadata_cmdcnt(sptr)++; + if (cli_metadata_cmdcnt(sptr) > rate_limit) { + return 1; /* Rate limited */ + } + + return 0; +} + +/** m_metadata - Handle METADATA command from local client. + * + * parv[0] = sender prefix + * parv[1] = subcommand (GET, SET, LIST, CLEAR, SUB, UNSUB, SUBS, SYNC) + * parv[2...] = subcommand arguments + * + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message. + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + */ +int m_metadata(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + const char *subcmd; + + /* Must have draft/metadata-2 capability */ + if (!CapActive(sptr, CAP_DRAFT_METADATA2)) { + return send_reply(sptr, ERR_UNKNOWNCOMMAND, "METADATA"); + } + + /* Check rate limiting */ + if (check_metadata_rate_limit(sptr)) { + send_fail(sptr, "METADATA", "RATE_LIMITED", NULL, + "Too many metadata commands, slow down"); + return 0; + } + + if (parc < 2 || EmptyString(parv[1])) { + send_fail(sptr, "METADATA", "INVALID_PARAMS", NULL, + "Missing subcommand"); + return 0; + } + + subcmd = parv[1]; + + if (ircd_strcmp(subcmd, "GET") == 0) { + return metadata_cmd_get(sptr, parc, parv); + } else if (ircd_strcmp(subcmd, "SET") == 0) { + return metadata_cmd_set(sptr, parc, parv); + } else if (ircd_strcmp(subcmd, "LIST") == 0) { + return metadata_cmd_list(sptr, parc, parv); + } else if (ircd_strcmp(subcmd, "CLEAR") == 0) { + return metadata_cmd_clear(sptr, parc, parv); + } else if (ircd_strcmp(subcmd, "SUB") == 0) { + return metadata_cmd_sub(sptr, parc, parv); + } else if (ircd_strcmp(subcmd, "UNSUB") == 0) { + return metadata_cmd_unsub(sptr, parc, parv); + } else if (ircd_strcmp(subcmd, "SUBS") == 0) { + return metadata_cmd_subs(sptr, parc, parv); + } else if (ircd_strcmp(subcmd, "SYNC") == 0) { + return metadata_cmd_sync(sptr, parc, parv); + } else { + send_fail(sptr, "METADATA", "INVALID_PARAMS", subcmd, + "Unknown subcommand"); + return 0; + } +} + +/** ms_metadataquery - Handle METADATAQUERY (MDQ) command from server. + * + * Used for on-demand metadata sync - allows services (X3) to query + * metadata for offline users or channels from the IRCd's LMDB cache. + * + * Format: [SOURCE] MDQ [TARGET] [KEY|*] + * + * parv[0] = sender prefix + * parv[1] = target (account name or channel) + * parv[2] = key to query, or "*" for all keys + * + * Response: Standard MD tokens sent back to the source server. + * + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message. + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + */ +int ms_metadataquery(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + const char *target; + const char *key; + int is_channel = 0; + int is_from_services = 0; + struct MetadataEntry *entry = NULL; + struct MetadataEntry *list = NULL; + struct MetadataEntry *next; + char value_buf[METADATA_VALUE_LEN + 1]; + + /* Check if this is from services */ + if (IsServer(sptr) && IsService(sptr)) { + is_from_services = 1; + metadata_x3_heartbeat(); + } else if (!IsServer(sptr) && cli_user(sptr) && + cli_user(sptr)->server && IsService(cli_user(sptr)->server)) { + is_from_services = 1; + metadata_x3_heartbeat(); + } + + if (parc < 3) { + /* Need at least target and key */ + return 0; + } + + target = parv[1]; + key = parv[2]; + + if (!target || !key) + return 0; + + /* Log MDQ request for debugging */ + log_write(LS_DEBUG, L_DEBUG, 0, "MDQ: %s queries %s key=%s (from_services=%d)", + cli_name(sptr), target, key, is_from_services); + + /* If MDQ is from another IRCd (not services), we have two options: + * 1. If X3 is available, forward to X3 (authoritative source) + * 2. If X3 is unavailable, try to answer from local LMDB cache + * + * This handles multi-hop topologies: Client -> ServerA -> ServerB -> X3 + */ + if (!is_from_services) { + struct Client *services = NULL; + struct Client *acptr; + + /* Find services server to forward to */ + for (acptr = GlobalClientList; acptr; acptr = cli_next(acptr)) { + if (IsServer(acptr) && IsService(acptr)) { + services = acptr; + break; + } + } + + if (services && metadata_x3_is_available()) { + /* X3 is available - forward to authoritative source */ + sendcmdto_one(sptr, CMD_METADATAQUERY, services, "%s %s", target, key); + log_write(LS_DEBUG, L_DEBUG, 0, "MDQ: Forwarding to %s", cli_name(services)); + return 0; + } + + /* X3 unavailable - try to answer from local LMDB cache. + * Fall through to the cache lookup code below, but send response + * back to the requesting server (cptr) instead of sptr. + */ + log_write(LS_DEBUG, L_DEBUG, 0, "MDQ: X3 unavailable, checking local cache"); + /* Fall through to process locally */ + } + + /* Determine if channel or account */ + is_channel = IsChannelName(target); + + if (is_channel) { + /* Channel metadata query - look up from channel structure first, + * then fall back to LMDB for unloaded/offline channels */ + struct Channel *chptr = FindChannel(target); + + if (chptr) { + /* Channel exists in memory */ + if (key[0] == '*' && key[1] == '\0') { + /* Return all metadata for channel */ + entry = metadata_list_channel(chptr); + while (entry) { + const char *vis_str = (entry->visibility == METADATA_VIS_PRIVATE) ? "P" : "*"; + if (entry->value && *entry->value) { + sendcmdto_one(&me, CMD_METADATA, cptr, "%s %s %s :%s", + target, entry->key, vis_str, entry->value); + } + entry = entry->next; + } + } else { + /* Single key query */ + entry = metadata_get_channel(chptr, key); + if (entry && entry->value && *entry->value) { + const char *vis_str = (entry->visibility == METADATA_VIS_PRIVATE) ? "P" : "*"; + sendcmdto_one(&me, CMD_METADATA, cptr, "%s %s %s :%s", + target, key, vis_str, entry->value); + } + } + } + /* For channels not in memory, we could query LMDB but currently + * channel metadata in LMDB is keyed by channel name directly */ + } else { + /* Account metadata query - query LMDB cache */ + if (!metadata_lmdb_is_available()) { + /* LMDB not available, can't respond */ + return 0; + } + + if (key[0] == '*' && key[1] == '\0') { + /* Return all metadata for account from LMDB */ + list = metadata_account_list(target); + entry = list; + while (entry) { + /* Parse visibility from stored value if prefixed with P: */ + const char *vis_str = "*"; + const char *val = entry->value; + if (val && val[0] == 'P' && val[1] == ':') { + vis_str = "P"; + val = val + 2; + } + if (val && *val) { + sendcmdto_one(&me, CMD_METADATA, cptr, "%s %s %s :%s", + target, entry->key, vis_str, val); + } + entry = entry->next; + } + /* Free the list returned by metadata_account_list */ + entry = list; + while (entry) { + next = entry->next; + metadata_free_entry(entry); + entry = next; + } + } else { + /* Single key query */ + if (metadata_account_get(target, key, value_buf) == 0) { + /* Parse visibility from stored value */ + const char *vis_str = "*"; + const char *val = value_buf; + if (val[0] == 'P' && val[1] == ':') { + vis_str = "P"; + val = val + 2; + } + if (*val) { + sendcmdto_one(&me, CMD_METADATA, cptr, "%s %s %s :%s", + target, key, vis_str, val); + } + } + } + } + + return 0; +} + +/** ms_metadata - Handle METADATA command from server. + * + * Used for propagating metadata changes across the network. + * + * parv[0] = sender prefix + * parv[1] = target + * parv[2] = key + * parv[3] = visibility ("*" or "P") (optional for backwards compat) + * parv[4] = "Z" if compressed passthrough, or value + * parv[5] = base64 value (if Z flag present) + * + * For compression passthrough: + * Format: target key visibility Z :base64_compressed_data + * + * For backwards compatibility, if parv[3] is present but not a visibility + * token, treat it as the value. + * + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message. + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + */ +int ms_metadata(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + const char *target; + const char *key; + const char *value = NULL; + int visibility = METADATA_VIS_PUBLIC; + int is_channel = 0; + int is_compressed = 0; + struct Client *target_client = NULL; + struct Channel *target_channel = NULL; + int is_from_services = 0; + + /* Buffers for compressed data handling */ + unsigned char raw_data[METADATA_VALUE_LEN + 64]; + size_t raw_len = 0; + + /* Check if this is from a services server (potential MDQ response) */ + if (IsServer(sptr) && IsService(sptr)) { + is_from_services = 1; + metadata_x3_heartbeat(); + } else if (!IsServer(sptr) && cli_user(sptr) && + cli_user(sptr)->server && IsService(cli_user(sptr)->server)) { + is_from_services = 1; + metadata_x3_heartbeat(); + } + + if (parc < 3) + return 0; + + target = parv[1]; + key = parv[2]; + + /* Parse visibility, Z flag, and value. + * Compressed format: target key visibility Z :base64_data + * Normal format: target key [visibility] [:value] + * Error format: target key ! :error_code (from services for NOTARGET) + * Old format: target key [:value] + */ + if (parc >= 4) { + /* Check if parv[3] is a visibility token */ + if ((parv[3][0] == '*' && parv[3][1] == '\0') || + (parv[3][0] == 'P' && parv[3][1] == '\0') || + (parv[3][0] == '!' && parv[3][1] == '\0')) { + if (parv[3][0] == '!') + visibility = METADATA_VIS_ERROR; + else + visibility = (parv[3][0] == 'P') ? METADATA_VIS_PRIVATE : METADATA_VIS_PUBLIC; + + /* Check for Z flag (compression passthrough) */ + if (parc >= 5 && parv[4][0] == 'Z' && parv[4][1] == '\0') { + is_compressed = 1; + if (parc >= 6) + value = parv[5]; /* Base64-encoded compressed data */ + } else if (parc >= 5) { + value = parv[4]; + } + } else { + /* Old format or no visibility - parv[3] is value */ + value = parv[3]; + } + } + + /* Handle error response from services (NOTARGET = no such account/channel) */ + if (visibility == METADATA_VIS_ERROR && is_from_services) { + log_write(LS_DEBUG, L_DEBUG, 0, + "ms_metadata: Error response for %s: %s", target, value ? value : "(no value)"); + /* Forward error to waiting MDQ clients */ + metadata_handle_response(target, key, value, METADATA_VIS_ERROR); + return 0; /* Don't cache or propagate error responses */ + } + + if (!is_valid_key(key)) + return 0; + + /* Handle compressed data - decode base64 now */ + if (is_compressed && value) { + if (!base64_decode(value, raw_data, sizeof(raw_data), &raw_len)) { + log_write(LS_SYSTEM, L_WARNING, 0, + "ms_metadata: Failed to decode compressed data for %s/%s", + target, key); + is_compressed = 0; /* Fall back to treating as plain value */ + } + } + + /* Find target */ + if (IsChannelName(target)) { + is_channel = 1; + target_channel = FindChannel(target); + if (!target_channel) + return 0; + } else { + target_client = FindUser(target); + /* For MDQ responses from services, target might be offline account */ + if (!target_client && !is_from_services) + return 0; + } + + /* Apply the change with visibility (always decompress for in-memory storage) */ + if (!is_compressed) { + if (is_channel) { + metadata_set_channel(target_channel, key, value, visibility); + } else if (target_client) { + metadata_set_client(target_client, key, value, visibility); + } + } else if (raw_len > 0) { + /* For compressed data, decompress and store in memory for online users */ +#ifdef USE_ZSTD + char decompressed[METADATA_VALUE_LEN]; + size_t decompressed_len; + if (decompress_data(raw_data, raw_len, + (unsigned char *)decompressed, sizeof(decompressed) - 1, + &decompressed_len) >= 0) { + decompressed[decompressed_len] = '\0'; + if (is_channel) { + metadata_set_channel(target_channel, key, decompressed, visibility); + } else if (target_client) { + metadata_set_client(target_client, key, decompressed, visibility); + } + } +#endif + } + + /* If from services, cache in LMDB for registered users/channels */ + if (is_from_services && value) { + const char *cache_key = NULL; + + if (!is_channel) { + /* For users, get the account name for cache key */ + if (target_client && IsAccount(target_client)) { + /* Online registered user - cache under their account */ + cache_key = cli_account(target_client); + } else if (!target_client) { + /* Offline user - target is the account name */ + cache_key = target; + } + } else if (target_channel) { + /* Registered channel - cache under channel name */ + cache_key = target; + } + + if (cache_key && metadata_lmdb_is_available()) { + if (is_compressed && raw_len > 0) { + /* Store raw compressed data directly - no recompression needed! */ + /* Prepend visibility if private */ + if (visibility == METADATA_VIS_PRIVATE) { + unsigned char prefixed[METADATA_VALUE_LEN + 64]; + prefixed[0] = 'P'; + prefixed[1] = ':'; + memcpy(prefixed + 2, raw_data, raw_len); + metadata_account_set_raw(cache_key, key, prefixed, raw_len + 2); + } else { + metadata_account_set_raw(cache_key, key, raw_data, raw_len); + } + log_write(LS_SYSTEM, L_DEBUG, 0, + "ms_metadata: Stored compressed passthrough for %s/%s (%zu bytes)", + cache_key, key, raw_len); + } else { + /* Store with visibility prefix (will compress automatically) */ + char stored_value[METADATA_VALUE_LEN + 3]; + if (visibility == METADATA_VIS_PRIVATE) { + ircd_snprintf(0, stored_value, sizeof(stored_value), "P:%s", value); + } else { + ircd_strncpy(stored_value, value, METADATA_VALUE_LEN); + } + metadata_account_set(cache_key, key, stored_value); + } + log_write(LS_DEBUG, L_DEBUG, 0, + "ms_metadata: Cached X3 metadata %s/%s in LMDB", cache_key, key); + } + + /* Forward to any clients waiting for this MDQ response (offline users only) */ + if (!target_client && !is_channel) { + if (is_compressed && raw_len > 0) { + /* Decompress for the response */ +#ifdef USE_ZSTD + char decompressed[METADATA_VALUE_LEN]; + size_t decompressed_len; + if (decompress_data(raw_data, raw_len, + (unsigned char *)decompressed, sizeof(decompressed) - 1, + &decompressed_len) >= 0) { + decompressed[decompressed_len] = '\0'; + metadata_handle_response(target, key, decompressed, visibility); + } +#endif + } else { + metadata_handle_response(target, key, value, visibility); + } + } + } + + /* Notify local subscribers (only for public metadata) */ + if (visibility == METADATA_VIS_PUBLIC) { + if (is_compressed && raw_len > 0) { +#ifdef USE_ZSTD + /* Decompress for subscribers */ + char decompressed[METADATA_VALUE_LEN]; + size_t decompressed_len; + if (decompress_data(raw_data, raw_len, + (unsigned char *)decompressed, sizeof(decompressed) - 1, + &decompressed_len) >= 0) { + decompressed[decompressed_len] = '\0'; + notify_subscribers(target, key, decompressed); + } +#endif + } else { + notify_subscribers(target, key, value); + } + } + + /* Propagate to other servers - forward compressed if received compressed */ + if (value) { + if (is_compressed) { + /* Forward compressed with Z flag */ + sendcmdto_serv_butone(sptr, CMD_METADATA, cptr, "%s %s %s Z :%s", + target, key, + visibility == METADATA_VIS_PRIVATE ? "P" : "*", + value); + } else { + sendcmdto_serv_butone(sptr, CMD_METADATA, cptr, "%s %s %s :%s", + target, key, + visibility == METADATA_VIS_PRIVATE ? "P" : "*", + value); + } + } else { + sendcmdto_serv_butone(sptr, CMD_METADATA, cptr, "%s %s", + target, key); + } + + return 0; +} diff --git a/ircd/m_oper.c b/ircd/m_oper.c index 1460f196..3a5fa6b8 100644 --- a/ircd/m_oper.c +++ b/ircd/m_oper.c @@ -101,12 +101,86 @@ #include "s_debug.h" #include "s_user.h" #include "s_misc.h" +#include "s_bsd.h" #include "send.h" /* #include -- Now using assert in ircd_log.h */ #include #include +/* Forward declarations */ +void do_oper(struct Client* cptr, struct Client* sptr, struct ConfItem* aconf); + +/** + * Context for async OPER password verification. + * This is passed to the thread pool callback. + */ +struct oper_verify_ctx { + int fd; /**< Client file descriptor */ + unsigned int cookie; /**< Unique cookie to verify client identity */ + char name[NICKLEN + 1]; /**< Oper name for logging */ + struct ConfItem *aconf; /**< Oper config block */ +}; + +/** + * Callback invoked when async OPER password verification completes. + * Called in main thread context via thread_pool_poll(). + */ +static void oper_password_verified(int result, void *arg) +{ + struct oper_verify_ctx *ctx = arg; + struct Client *sptr; + + /* Look up client by fd */ + if (ctx->fd < 0 || ctx->fd >= MAXCONNECTIONS) { + MyFree(ctx); + return; + } + + sptr = LocalClientArray[ctx->fd]; + + /* Verify client still exists and matches our cookie */ + if (!sptr || IsDead(sptr) || !IsOperPending(sptr)) { + Debug((DEBUG_DEBUG, "oper_password_verified: client gone or not pending " + "(fd %d)", ctx->fd)); + MyFree(ctx); + return; + } + + /* Clear pending flag */ + ClearOperPending(sptr); + + if (result == CRYPT_VERIFY_MATCH) { + /* Password matched - complete OPER */ + if (MyUser(sptr)) { + int attach_result = attach_conf(sptr, ctx->aconf); + if ((ACR_OK != attach_result) && (ACR_ALREADY_AUTHORIZED != attach_result)) { + send_reply(sptr, ERR_NOOPERHOST); + sendto_opmask_butone_global(&me, SNO_OLDREALOP, + "Failed OPER attempt by %s (%s@%s) (attach failed after async)", + cli_name(sptr), cli_user(sptr)->username, cli_user(sptr)->realhost); + MyFree(ctx); + return; + } + } + do_oper(sptr, sptr, ctx->aconf); + SetOperedLocal(sptr); + ClearOperedRemote(sptr); + Debug((DEBUG_INFO, "oper_password_verified: OPER success for %s", + cli_name(sptr))); + } else { + /* Password didn't match */ + send_reply(sptr, ERR_PASSWDMISMATCH); + sendto_opmask_butone_global(&me, SNO_OLDREALOP, + "Failed OPER attempt by %s (%s@%s) (password mismatch)", + cli_name(sptr), cli_user(sptr)->username, cli_user(sptr)->realhost); + Debug((DEBUG_INFO, "oper_password_verified: OPER failed for %s", + cli_name(sptr))); + } + + MyFree(ctx); +} + void do_oper(struct Client* cptr, struct Client* sptr, struct ConfItem* aconf) { struct Flags old_mode = cli_flags(sptr); @@ -288,6 +362,36 @@ int can_oper(struct Client *cptr, struct Client *sptr, char *name, return 0; } + /* + * Try async password verification if available. + * This prevents blocking the event loop during bcrypt/PBKDF2 hashing. + * Falls back to synchronous verification if async is not available. + */ + if (MyUser(sptr) && ircd_crypt_async_available() && !IsOperPending(sptr)) { + struct oper_verify_ctx *ctx; + + ctx = (struct oper_verify_ctx *)MyMalloc(sizeof(struct oper_verify_ctx)); + ctx->fd = cli_fd(sptr); + ctx->aconf = aconf; + ircd_strncpy(ctx->name, name, NICKLEN); + + if (ircd_crypt_verify_async(password, aconf->passwd, + oper_password_verified, ctx) == 0) { + /* Async verification started */ + SetOperPending(sptr); + Debug((DEBUG_INFO, "can_oper: started async verification for %s", + cli_name(sptr))); + *_aconf = aconf; + return 1; /* Return 1 = pending async */ + } + + /* Async failed to start, fall back to sync */ + MyFree(ctx); + Debug((DEBUG_DEBUG, "can_oper: async failed, falling back to sync for %s", + cli_name(sptr))); + } + + /* Synchronous password verification (blocking) */ if (oper_password_match(password, aconf->passwd)) { if (MyUser(sptr)) @@ -356,10 +460,23 @@ int m_oper(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) if (EmptyString(name) || EmptyString(password)) return need_more_params(sptr, "OPER"); - if (can_oper(cptr, sptr, name, password, &aconf)) { - do_oper(cptr, sptr, aconf); - SetOperedLocal(sptr); - ClearOperedRemote(sptr); + /* Reject if async verification already in progress */ + if (IsOperPending(sptr)) { + sendcmdto_one(&me, CMD_NOTICE, sptr, "%C :OPER authentication already in progress", + sptr); + return 0; + } + + { + int result = can_oper(cptr, sptr, name, password, &aconf); + if (result == -1) { + /* Sync verification succeeded */ + do_oper(cptr, sptr, aconf); + SetOperedLocal(sptr); + ClearOperedRemote(sptr); + } + /* result == 1 means async pending, callback will handle it */ + /* result == 0 means failed, error already sent */ } return 0; diff --git a/ircd/m_privmsg.c b/ircd/m_privmsg.c index 6665483c..9197cce1 100644 --- a/ircd/m_privmsg.c +++ b/ircd/m_privmsg.c @@ -81,6 +81,7 @@ */ #include "config.h" +#include "capab.h" #include "client.h" #include "ircd.h" #include "ircd_chattr.h" @@ -97,6 +98,9 @@ /* #include -- Now using assert in ircd_log.h */ #include +/* External function from m_batch.c for multiline message handling */ +extern int multiline_add_message(struct Client *sptr, const char *text, int concat); + /* * m_privmsg - generic message handler */ @@ -123,6 +127,18 @@ int m_privmsg(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) if (parc < 3 || EmptyString(parv[parc - 1])) return send_reply(sptr, ERR_NOTEXTTOSEND); + /* + * Check for multiline batch interception (draft/multiline). + * If this message has a @batch tag matching an active multiline batch, + * add it to the batch instead of delivering immediately. + */ + if (cli_msg_batch_tag(sptr)[0] != '\0' && + cli_ml_batch_id(sptr)[0] != '\0' && + strcmp(cli_msg_batch_tag(sptr), cli_ml_batch_id(sptr)) == 0) { + /* This PRIVMSG is part of an active multiline batch - add to batch */ + return multiline_add_message(sptr, parv[parc - 1], cli_msg_concat(sptr)); + } + count = unique_name_vector(parv[1], ',', vector, MAXTARGETS); for (i = 0; i < count; ++i) { @@ -213,6 +229,15 @@ int mo_privmsg(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) if (parc < 3 || EmptyString(parv[parc - 1])) return send_reply(sptr, ERR_NOTEXTTOSEND); + /* + * Check for multiline batch interception (draft/multiline). + */ + if (cli_msg_batch_tag(sptr)[0] != '\0' && + cli_ml_batch_id(sptr)[0] != '\0' && + strcmp(cli_msg_batch_tag(sptr), cli_ml_batch_id(sptr)) == 0) { + return multiline_add_message(sptr, parv[parc - 1], cli_msg_concat(sptr)); + } + count = unique_name_vector(parv[1], ',', vector, MAXTARGETS); for (i = 0; i < count; ++i) { diff --git a/ircd/m_redact.c b/ircd/m_redact.c new file mode 100644 index 00000000..af0a6977 --- /dev/null +++ b/ircd/m_redact.c @@ -0,0 +1,359 @@ +/* + * IRC - Internet Relay Chat, ircd/m_redact.c + * Copyright (C) 2024 Nefarious Development Team + * + * 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. + */ +/** @file + * @brief Handler for REDACT command (IRCv3 draft/message-redaction). + * + * Specification: https://ircv3.net/specs/extensions/message-redaction + * + * REDACT [:] + * + * Allows users to delete previously sent messages. Authorization: + * - Users can redact their own messages (time-limited) + * - Channel operators can redact any message in their channels + * - IRC operators can redact any message network-wide + */ +#include "config.h" + +#include "capab.h" +#include "channel.h" +#include "client.h" +#include "hash.h" +#include "history.h" +#include "ircd.h" +#include "ircd_features.h" +#include "ircd_log.h" +#include "ircd_reply.h" +#include "ircd_snprintf.h" +#include "ircd_string.h" +#include "msg.h" +#include "numeric.h" +#include "numnicks.h" +#include "send.h" + +#include +#include +#include + +/** Extract timestamp from a message ID. + * Message IDs have format: SERVER-TIMESTAMP-COUNTER + * @param[in] msgid Message ID string. + * @return Unix timestamp, or 0 on parse error. + */ +static time_t parse_msgid_timestamp(const char *msgid) +{ + const char *dash1, *dash2; + char timebuf[32]; + size_t len; + + /* Find first dash (after server prefix) */ + dash1 = strchr(msgid, '-'); + if (!dash1) + return 0; + + /* Find second dash (end of timestamp) */ + dash2 = strchr(dash1 + 1, '-'); + if (!dash2) + return 0; + + len = dash2 - (dash1 + 1); + if (len >= sizeof(timebuf)) + return 0; + + memcpy(timebuf, dash1 + 1, len); + timebuf[len] = '\0'; + + return (time_t)strtoul(timebuf, NULL, 10); +} + +/** Check if a nick matches the sender in a history message. + * The sender field is nick!user@host, we compare just the nick. + * @param[in] msg History message with sender field. + * @param[in] nick Nick to compare. + * @return 1 if match, 0 otherwise. + */ +static int sender_nick_matches(const struct HistoryMessage *msg, const char *nick) +{ + const char *bang; + size_t nicklen; + + bang = strchr(msg->sender, '!'); + if (!bang) + return 0; + + nicklen = bang - msg->sender; + if (strlen(nick) != nicklen) + return 0; + + return (ircd_strncmp(msg->sender, nick, nicklen) == 0); +} + +/** Propagate REDACT to channel members with the capability. + * @param[in] sptr Source client. + * @param[in] chptr Channel. + * @param[in] target Target name (channel). + * @param[in] msgid Message ID. + * @param[in] reason Reason (may be NULL). + */ +static void propagate_redact_to_channel(struct Client *sptr, struct Channel *chptr, + const char *target, const char *msgid, + const char *reason) +{ + struct Membership *member; + + for (member = chptr->members; member; member = member->next_member) { + struct Client *acptr = member->user; + + /* Only send to local clients with message-redaction capability */ + if (!MyUser(acptr)) + continue; + if (!CapActive(acptr, CAP_DRAFT_REDACT)) + continue; + + /* Echo to sender only if they have echo-message capability (like PRIVMSG) */ + if (acptr == sptr && !CapActive(acptr, CAP_ECHOMSG)) + continue; + + if (reason && *reason) { + sendcmdto_one(sptr, CMD_REDACT, acptr, "%s %s :%s", target, msgid, reason); + } else { + sendcmdto_one(sptr, CMD_REDACT, acptr, "%s %s", target, msgid); + } + } +} + +/** m_redact - Handle REDACT command from local client. + * + * parv[0] = sender prefix + * parv[1] = target (channel or nick) + * parv[2] = message ID + * parv[3] = reason (optional) + * + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message. + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + * @return CPTR_KILLED if client was squit, else 0. + */ +int m_redact(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + const char *target; + const char *msgid; + const char *reason = NULL; + struct Channel *chptr = NULL; + struct Membership *member = NULL; + struct HistoryMessage *msg = NULL; + time_t msg_time; + time_t window; + int is_chanop = 0; + int is_oper = 0; + int can_redact = 0; + int rc; + + /* Check if feature is enabled */ + if (!feature_bool(FEAT_CAP_draft_message_redaction)) { + send_fail(sptr, "REDACT", "DISABLED", NULL, + "Message redaction is not enabled on this server"); + return 0; + } + + /* Need at least target and msgid */ + if (parc < 3) { + return need_more_params(sptr, "REDACT"); + } + + target = parv[1]; + msgid = parv[2]; + if (parc > 3 && parv[3]) + reason = parv[3]; + + /* Validate target is a channel */ + if (!IsChannelName(target)) { + /* For now, only channel redaction is supported */ + send_fail(sptr, "REDACT", "INVALID_TARGET", target, + "Cannot redact from this target"); + return 0; + } + + /* Find the channel */ + chptr = FindChannel(target); + if (!chptr) { + send_fail(sptr, "REDACT", "INVALID_TARGET", target, + "No such channel"); + return 0; + } + + /* Check if user is in channel */ + member = find_member_link(chptr, sptr); + if (!member) { + send_fail(sptr, "REDACT", "INVALID_TARGET", target, + "You are not in that channel"); + return 0; + } + + /* Determine authorization level */ + is_oper = IsOper(sptr); + is_chanop = IsChanOp(member); + + /* If chathistory is available, validate ownership and check time window */ + if (history_is_available()) { + rc = history_lookup_message(target, msgid, &msg); + if (rc == 1) { + /* Not found in history */ + send_fail(sptr, "REDACT", "UNKNOWN_MSGID", msgid, + "Message not found"); + return 0; + } else if (rc < 0) { + /* Database error - allow redaction anyway (trust client) */ + can_redact = 1; + } else { + /* Found the message - get actual timestamp for window check */ + msg_time = (time_t)strtoul(msg->timestamp, NULL, 10); + + /* Check time window */ + if (!is_oper) { + window = (time_t)feature_int(FEAT_REDACT_WINDOW); + if (window > 0 && (CurrentTime - msg_time) > window) { + history_free_messages(msg); + send_fail(sptr, "REDACT", "REDACT_WINDOW_EXPIRED", msgid, + "Redaction window has expired"); + return 0; + } + } else { + /* Opers have their own window (0 = unlimited) */ + window = (time_t)feature_int(FEAT_REDACT_OPER_WINDOW); + if (window > 0 && (CurrentTime - msg_time) > window) { + history_free_messages(msg); + send_fail(sptr, "REDACT", "REDACT_WINDOW_EXPIRED", msgid, + "Redaction window has expired"); + return 0; + } + } + + /* Check authorization */ + if (sender_nick_matches(msg, cli_name(sptr))) { + /* Own message - allowed within time window */ + can_redact = 1; + } else if (is_oper) { + /* Opers can redact anything */ + can_redact = 1; + } else if (is_chanop && feature_bool(FEAT_REDACT_CHANOP_OTHERS)) { + /* Chanops can redact others if enabled */ + can_redact = 1; + } + + if (!can_redact) { + history_free_messages(msg); + send_fail(sptr, "REDACT", "REDACT_FORBIDDEN", msgid, + "You are not authorized to redact this message"); + return 0; + } + + /* Delete from history database */ + history_delete_message(target, msgid); + history_free_messages(msg); + } + } else { + /* No chathistory - parse msgid timestamp as fallback (less accurate) */ + msg_time = parse_msgid_timestamp(msgid); + if (msg_time == 0) { + send_fail(sptr, "REDACT", "UNKNOWN_MSGID", msgid, + "Invalid message ID format"); + return 0; + } + + /* Check time window with msgid timestamp (server startup time - may be stale) */ + if (!is_oper) { + window = (time_t)feature_int(FEAT_REDACT_WINDOW); + if (window > 0 && (CurrentTime - msg_time) > window) { + send_fail(sptr, "REDACT", "REDACT_WINDOW_EXPIRED", msgid, + "Redaction window has expired"); + return 0; + } + } else { + window = (time_t)feature_int(FEAT_REDACT_OPER_WINDOW); + if (window > 0 && (CurrentTime - msg_time) > window) { + send_fail(sptr, "REDACT", "REDACT_WINDOW_EXPIRED", msgid, + "Redaction window has expired"); + return 0; + } + } + + /* Trust the client claim - allow if within time window */ + can_redact = 1; + } + + /* Propagate to channel members with capability */ + propagate_redact_to_channel(sptr, chptr, target, msgid, reason); + + /* Propagate to other servers */ + sendcmdto_serv_butone(sptr, CMD_REDACT, cptr, "%s %s :%s", + target, msgid, reason ? reason : ""); + + return 0; +} + +/** ms_redact - Handle REDACT command from server. + * + * parv[0] = sender prefix (numeric) + * parv[1] = target (channel or nick) + * parv[2] = message ID + * parv[3] = reason (optional) + * + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message. + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + * @return CPTR_KILLED if client was squit, else 0. + */ +int ms_redact(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + const char *target; + const char *msgid; + const char *reason = NULL; + struct Channel *chptr = NULL; + + /* Need at least target and msgid */ + if (parc < 3) + return 0; + + target = parv[1]; + msgid = parv[2]; + if (parc > 3 && parv[3]) + reason = parv[3]; + + /* For channels, propagate to members and other servers */ + if (IsChannelName(target)) { + chptr = FindChannel(target); + if (chptr) { + /* Delete from local history if available */ + if (history_is_available()) { + history_delete_message(target, msgid); + } + + /* Propagate to channel members with capability */ + propagate_redact_to_channel(sptr, chptr, target, msgid, reason); + } + } + + /* Propagate to other servers */ + sendcmdto_serv_butone(sptr, CMD_REDACT, cptr, "%s %s :%s", + target, msgid, reason ? reason : ""); + + return 0; +} diff --git a/ircd/m_register.c b/ircd/m_register.c new file mode 100644 index 00000000..38b02905 --- /dev/null +++ b/ircd/m_register.c @@ -0,0 +1,462 @@ +/* + * IRC - Internet Relay Chat, ircd/m_register.c + * Copyright (C) 2024 Nefarious Development Team + * + * 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. + */ +/** @file + * @brief Handler for REGISTER/VERIFY commands (IRCv3 draft/account-registration). + * + * Specification: https://ircv3.net/specs/extensions/account-registration + * + * REGISTER { | "*"} + * VERIFY + * + * This implementation relays registration requests to X3 services via P10 + * using RG (REGISTER), VF (VERIFY), and RR (REGREPLY) tokens. + */ +#include "config.h" + +#include "capab.h" +#include "client.h" +#include "hash.h" +#include "ircd.h" +#include "ircd_features.h" +#include "ircd_reply.h" +#include "ircd_snprintf.h" +#include "ircd_string.h" +#include "msg.h" +#include "numeric.h" +#include "numnicks.h" +#include "s_auth.h" +#include "s_conf.h" +#include "s_debug.h" +#include "s_user.h" +#include "send.h" + +#include +#include + +extern struct Client* LocalClientArray[]; + +/** Find the services server (X3). + * Uses FEAT_REGISTER_SERVER to determine which server to use: + * - "*" (default): Find any server with +s (service) flag + * - Specific name: Use find_match_server to find a matching server + * @return Pointer to services server, or NULL if not connected. + */ +static struct Client *find_services_server(void) +{ + const char *server_name = feature_str(FEAT_REGISTER_SERVER); + struct Client *acptr; + + Debug((DEBUG_DEBUG, "find_services_server: REGISTER_SERVER=%s", server_name)); + + /* If a specific server is configured, try to find it */ + if (strcmp(server_name, "*") != 0) { + acptr = find_match_server((char *)server_name); + if (acptr) { + Debug((DEBUG_DEBUG, "find_services_server: Found configured server %s", + cli_name(acptr))); + return acptr; + } + Debug((DEBUG_DEBUG, "find_services_server: Configured server %s not found", + server_name)); + return NULL; + } + + /* Default: find any server that's a service (has +s) */ + Debug((DEBUG_DEBUG, "find_services_server: Searching for any service server")); + + for (acptr = GlobalClientList; acptr; acptr = cli_next(acptr)) { + if (IsServer(acptr)) { + Debug((DEBUG_DEBUG, "find_services_server: Found server %s, IsService=%d", + cli_name(acptr), IsService(acptr) ? 1 : 0)); + if (IsService(acptr)) + return acptr; + } + } + + Debug((DEBUG_DEBUG, "find_services_server: No service server found")); + return NULL; +} + +/** Send registration request to X3 via RG token (new protocol). + * @param[in] cptr Client connection (for fd). + * @param[in] sptr Client requesting registration. + * @param[in] account Account name to register. + * @param[in] email Email address (or "*"). + * @param[in] password Password. + * @param[in] services Services server to send to. + * @return 0 on success. + */ +static int send_register_rg(struct Client *cptr, struct Client *sptr, + const char *account, const char *email, + const char *password, struct Client *services) +{ + /* Format: RG !. : + * Similar to SASL, we use server!fd.cookie to identify pre-registration clients. + * The cookie is the SASL cookie assigned to this connection. + */ + sendcmdto_one(&me, CMD_REGISTER, services, "%C %C!%u.%u %s %s :%s", + services, &me, cli_fd(cptr), cli_saslcookie(cptr), + account, email, password); + return 0; +} + +/** Send verify request to X3 via VF token. + * @param[in] cptr Client connection (for fd). + * @param[in] sptr Client requesting verification. + * @param[in] account Account name. + * @param[in] code Verification code. + * @param[in] services Services server to send to. + * @return 0 on success. + */ +static int send_verify_vf(struct Client *cptr, struct Client *sptr, + const char *account, const char *code, + struct Client *services) +{ + /* Format similar to RG: server!fd.cookie to identify client */ + sendcmdto_one(&me, CMD_VERIFY, services, "%C %C!%u.%u %s %s", + services, &me, cli_fd(cptr), cli_saslcookie(cptr), + account, code); + return 0; +} + +/** m_register - Handle REGISTER command from local client. + * + * parv[0] = sender prefix + * parv[1] = account name (or "*" for current nick) + * parv[2] = email address (or "*" for none) + * parv[3] = password + * + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message. + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + * @return 0 on success. + */ +int m_register(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + const char *account; + const char *email; + const char *password; + struct Client *services; + + Debug((DEBUG_DEBUG, "m_register called: parc=%d from %s", parc, cli_name(sptr))); + + /* Check if feature is enabled */ + if (!feature_bool(FEAT_CAP_draft_account_registration)) { + Debug((DEBUG_DEBUG, "m_register: feature disabled")); + send_fail(sptr, "REGISTER", "DISABLED", NULL, + "Account registration is not enabled on this server"); + return 0; + } + Debug((DEBUG_DEBUG, "m_register: feature enabled, checking params")); + + /* Need account, email, and password */ + if (parc < 4) { + send_fail(sptr, "REGISTER", "NEED_MORE_PARAMS", NULL, + "Not enough parameters"); + return 0; + } + + account = parv[1]; + email = parv[2]; + password = parv[3]; + Debug((DEBUG_DEBUG, "m_register: account=%s email=%s", account, email)); + + /* Check if already authenticated */ + if (IsAccount(sptr)) { + Debug((DEBUG_DEBUG, "m_register: already authenticated")); + send_fail(sptr, "REGISTER", "ALREADY_AUTHENTICATED", account, + "You are already authenticated"); + return 0; + } + + Debug((DEBUG_DEBUG, "m_register: checking IsAccount")); + + /* Validate account name */ + if (account[0] == '*' && account[1] == '\0') { + /* Use current nickname */ + account = cli_name(sptr); + } + Debug((DEBUG_DEBUG, "m_register: account len=%zu ACCOUNTLEN=%d", strlen(account), ACCOUNTLEN)); + + /* Basic account name validation */ + if (strlen(account) > ACCOUNTLEN) { + Debug((DEBUG_DEBUG, "m_register: account name too long")); + send_fail(sptr, "REGISTER", "BAD_ACCOUNT_NAME", account, + "Account name too long"); + return 0; + } + + Debug((DEBUG_DEBUG, "m_register: password len=%zu", strlen(password))); + /* Basic password length check */ + if (strlen(password) < 5) { + Debug((DEBUG_DEBUG, "m_register: password too short")); + send_fail(sptr, "REGISTER", "WEAK_PASSWORD", account, + "Password too short (minimum 5 characters)"); + return 0; + } + + if (strlen(password) > 300) { + Debug((DEBUG_DEBUG, "m_register: password too long")); + send_fail(sptr, "REGISTER", "WEAK_PASSWORD", account, + "Password too long (maximum 300 characters)"); + return 0; + } + + /* Find services server */ + Debug((DEBUG_DEBUG, "m_register: looking for services server")); + services = find_services_server(); + if (!services) { + Debug((DEBUG_DEBUG, "m_register: no services server found")); + send_fail(sptr, "REGISTER", "TEMPORARILY_UNAVAILABLE", account, + "Registration service is not available"); + return 0; + } + Debug((DEBUG_DEBUG, "m_register: found services %s, sending RG", cli_name(services))); + + /* Send to services using RG (REGISTER) P10 token */ + send_register_rg(cptr, sptr, account, email, password, services); + Debug((DEBUG_DEBUG, "m_register: sent RG to services")); + + return 0; +} + +/** m_verify - Handle VERIFY command from local client. + * + * parv[0] = sender prefix + * parv[1] = account name + * parv[2] = verification code + * + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message. + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + * @return 0 on success. + */ +int m_verify(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + const char *account; + const char *code; + struct Client *services; + + /* Check if feature is enabled */ + if (!feature_bool(FEAT_CAP_draft_account_registration)) { + send_fail(sptr, "VERIFY", "DISABLED", NULL, + "Account registration is not enabled on this server"); + return 0; + } + + /* Need account and code */ + if (parc < 3) { + send_fail(sptr, "VERIFY", "NEED_MORE_PARAMS", NULL, + "Not enough parameters"); + return 0; + } + + account = parv[1]; + code = parv[2]; + + /* Check if already authenticated */ + if (IsAccount(sptr)) { + send_fail(sptr, "VERIFY", "ALREADY_AUTHENTICATED", account, + "You are already authenticated"); + return 0; + } + + /* Find services server */ + services = find_services_server(); + if (!services) { + send_fail(sptr, "VERIFY", "TEMPORARILY_UNAVAILABLE", account, + "Verification service is not available"); + return 0; + } + + /* Send to services using VF (VERIFY) P10 token */ + send_verify_vf(cptr, sptr, account, code, services); + + return 0; +} + +/** Find a pre-registration client by server!fd.cookie token. + * @param[in] token The token in format "server!fd.cookie" + * @return Client pointer or NULL if not found. + */ +static struct Client *find_prereg_client(const char *token) +{ + char buf[64]; + char *fdstr, *cookiestr; + int fd; + unsigned int cookie; + struct Client *acptr; + + /* Copy token so we can modify it */ + ircd_strncpy(buf, token, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + + /* Find the ! separator (server!fd.cookie) */ + fdstr = strchr(buf, '!'); + if (!fdstr) + return NULL; + fdstr++; /* Skip past the ! */ + + /* Find the . separator (fd.cookie) */ + cookiestr = strchr(fdstr, '.'); + if (!cookiestr) + return NULL; + *cookiestr++ = '\0'; + + fd = atoi(fdstr); + cookie = (unsigned int)atoi(cookiestr); + + Debug((DEBUG_DEBUG, "find_prereg_client: token=%s fd=%d cookie=%u", token, fd, cookie)); + + /* Find client by fd and verify cookie */ + acptr = LocalClientArray[fd]; + if (!acptr) { + Debug((DEBUG_DEBUG, "find_prereg_client: no client at fd %d", fd)); + return NULL; + } + + if (cli_saslcookie(acptr) != cookie) { + Debug((DEBUG_DEBUG, "find_prereg_client: cookie mismatch (%u != %u)", + cli_saslcookie(acptr), cookie)); + return NULL; + } + + Debug((DEBUG_DEBUG, "find_prereg_client: found client %s", cli_name(acptr))); + return acptr; +} + +/** ms_regreply - Handle REGREPLY from services (S2S). + * + * parv[0] = sender prefix (services server) + * parv[1] = target client ID (either "server!fd.cookie" for pre-reg or user numeric) + * parv[2] = status: S=success, F=fail, V=verification needed + * parv[3] = account name + * parv[4] = message + * + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message. + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + * @return 0 on success. + */ +int ms_regreply(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + struct Client *acptr; + const char *status; + const char *account; + const char *message; + + if (parc < 5) + return 0; + + Debug((DEBUG_DEBUG, "ms_regreply: target=%s status=%s account=%s msg=%s", + parv[1], parv[2], parv[3], parv[4])); + + /* Try to find the target client - could be either: + * 1. Pre-registration client: "server!fd.cookie" format + * 2. Registered user: user numeric + */ + if (strchr(parv[1], '!')) { + /* Pre-registration client format: server!fd.cookie */ + acptr = find_prereg_client(parv[1]); + } else { + /* Registered user numeric */ + acptr = findNUser(parv[1]); + } + + if (!acptr) { + Debug((DEBUG_DEBUG, "ms_regreply: target not found: %s", parv[1])); + return 0; + } + + /* If not our user, forward (only for registered users) */ + if (!MyConnect(acptr)) { + sendcmdto_one(sptr, CMD_REGREPLY, acptr, "%C %s %s :%s", + acptr, parv[2], parv[3], parv[4]); + return 0; + } + + status = parv[2]; + account = parv[3]; + message = parv[4]; + + switch (status[0]) { + case 'S': /* Success */ + /* Log the user in */ + Debug((DEBUG_DEBUG, "ms_regreply: SUCCESS for %s, IsRegistered=%d", + cli_name(acptr), IsRegistered(acptr) ? 1 : 0)); + + if (IsRegistered(acptr)) { + /* Fully registered user - set account directly */ + if (!IsAccount(acptr) && cli_user(acptr)) { + ircd_strncpy(cli_user(acptr)->account, account, + sizeof(cli_user(acptr)->account) - 1); + SetAccount(acptr); + /* Notify the user and other clients */ + sendrawto_one(acptr, "REGISTER SUCCESS %s :%s", account, message); + /* Send ACCOUNT to clients with account-notify */ + sendcmdto_common_channels_capab_butone(acptr, CMD_ACCOUNT, acptr, + CAP_ACCNOTIFY, CAP_NONE, + "%s", account); + } + } else { + /* Pre-registration client - store in saslaccount for later. + * When registration completes (NICK/USER done), auth_complete_sasl() + * will copy saslaccount to cli_user(acptr)->account and SetAccount(). + */ + ircd_strncpy(cli_saslaccount(acptr), account, ACCOUNTLEN); + SetSASLComplete(acptr); /* Mark SASL as complete so auth_complete_sasl applies account */ + if (cli_auth(acptr)) + auth_set_account(cli_auth(acptr), account); + /* Send success message to client */ + sendrawto_one(acptr, "REGISTER SUCCESS %s :%s", account, message); + Debug((DEBUG_DEBUG, "ms_regreply: pre-reg client, set saslaccount=%s, SetSASLComplete", account)); + } + break; + + case 'V': /* Verification required */ + sendrawto_one(acptr, "REGISTER VERIFICATION_REQUIRED %s :%s", + account, message); + break; + + case 'F': /* Failure */ + /* Parse the error code from message if present, otherwise generic */ + if (strstr(message, "exists") || strstr(message, "ACCOUNT_EXISTS")) { + send_fail(acptr, "REGISTER", "ACCOUNT_EXISTS", account, message); + } else if (strstr(message, "email") || strstr(message, "INVALID_EMAIL")) { + send_fail(acptr, "REGISTER", "INVALID_EMAIL", account, message); + } else if (strstr(message, "weak") || strstr(message, "WEAK_PASSWORD")) { + send_fail(acptr, "REGISTER", "WEAK_PASSWORD", account, message); + } else if (strstr(message, "invalid") || strstr(message, "BAD_ACCOUNT_NAME")) { + send_fail(acptr, "REGISTER", "BAD_ACCOUNT_NAME", account, message); + } else { + send_fail(acptr, "REGISTER", "TEMPORARILY_UNAVAILABLE", account, message); + } + break; + + default: + Debug((DEBUG_DEBUG, "Unknown REGREPLY status '%s' from %s for %s", + status, cli_name(sptr), cli_name(acptr))); + break; + } + + return 0; +} diff --git a/ircd/m_rename.c b/ircd/m_rename.c new file mode 100644 index 00000000..54bfce1b --- /dev/null +++ b/ircd/m_rename.c @@ -0,0 +1,539 @@ +/* + * IRC - Internet Relay Chat, ircd/m_rename.c + * Copyright (C) 2024 Nefarious Development Team + * + * 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. + */ +/** @file + * @brief Handler for RENAME command (IRCv3 draft/channel-rename). + * + * Specification: https://ircv3.net/specs/extensions/channel-rename + * + * RENAME [:] + * + * Renames a channel while preserving all state (members, modes, bans, etc). + */ +#include "config.h" + +#include "capab.h" +#include "channel.h" +#include "client.h" +#include "hash.h" +#include "ircd.h" +#include "ircd_events.h" +#include "ircd_features.h" +#include "ircd_log.h" +#include "ircd_reply.h" +#include "ircd_snprintf.h" +#include "ircd_string.h" +#include "ircd_alloc.h" +#include "msg.h" +#include "numeric.h" +#include "numnicks.h" +#include "s_user.h" +#include "send.h" + +#include +#include + +/* ========== Pending Rename Infrastructure ========== */ + +/** Maximum pending rename requests */ +#define RENAME_MAX_PENDING 100 + +/** Timeout for services response (seconds) */ +#define RENAME_TIMEOUT 10 + +/** Pending channel rename request */ +struct PendingRename { + struct Client *client; /**< Client waiting for response */ + struct Channel *channel; /**< Channel being renamed */ + char oldname[CHANNELLEN + 1]; /**< Original channel name */ + char newname[CHANNELLEN + 1]; /**< Requested new name */ + char reason[TOPICLEN + 1]; /**< Rename reason */ + unsigned int cookie; /**< Unique identifier for matching response */ + struct Timer timeout; /**< Timeout timer */ + struct PendingRename *next; /**< Linked list */ +}; + +/** Global pending rename list */ +static struct PendingRename *pending_renames = NULL; +static int pending_rename_count = 0; +static unsigned int rename_cookie_counter = 1; + +/* Forward declarations */ +static void pending_rename_timeout_cb(struct Event *ev); +static void send_rename_to_members(struct Client *sptr, struct Channel *chptr, + const char *oldname, const char *reason); + +/** Find the services server. + * @return Pointer to services server, or NULL if not connected. + */ +static struct Client *find_services_server(void) +{ + struct Client *acptr; + + for (acptr = GlobalClientList; acptr; acptr = cli_next(acptr)) { + if (IsServer(acptr) && IsService(acptr)) + return acptr; + } + + return NULL; +} + +/** Add a pending rename request. + * @param[in] client Client requesting the rename. + * @param[in] channel Channel to be renamed. + * @param[in] newname New channel name. + * @param[in] reason Rename reason. + * @return Pointer to new pending request, or NULL on error. + */ +static struct PendingRename *pending_rename_add(struct Client *client, + struct Channel *channel, + const char *newname, + const char *reason) +{ + struct PendingRename *pr; + + if (pending_rename_count >= RENAME_MAX_PENDING) + return NULL; + + pr = (struct PendingRename *)MyMalloc(sizeof(struct PendingRename)); + if (!pr) + return NULL; + + memset(pr, 0, sizeof(*pr)); + pr->client = client; + pr->channel = channel; + ircd_strncpy(pr->oldname, channel->chname, CHANNELLEN); + pr->oldname[CHANNELLEN] = '\0'; + ircd_strncpy(pr->newname, newname, CHANNELLEN); + pr->newname[CHANNELLEN] = '\0'; + if (reason && *reason) { + ircd_strncpy(pr->reason, reason, TOPICLEN); + pr->reason[TOPICLEN] = '\0'; + } + pr->cookie = rename_cookie_counter++; + + /* Add to list */ + pr->next = pending_renames; + pending_renames = pr; + pending_rename_count++; + + /* Start timeout timer */ + timer_add(timer_init(&pr->timeout), pending_rename_timeout_cb, (void *)pr, + TT_RELATIVE, RENAME_TIMEOUT); + + log_write(LS_DEBUG, L_DEBUG, 0, + "pending_rename_add: cookie=%u channel=%s newname=%s client=%C", + pr->cookie, pr->oldname, pr->newname, client); + + return pr; +} + +/** Find a pending rename by cookie. + * @param[in] cookie Cookie to search for. + * @return Pointer to pending request, or NULL if not found. + */ +struct PendingRename *pending_rename_find(unsigned int cookie) +{ + struct PendingRename *pr; + + for (pr = pending_renames; pr; pr = pr->next) { + if (pr->cookie == cookie) + return pr; + } + + return NULL; +} + +/** Remove a pending rename request from the list. + * @param[in] pr Pending request to remove. + */ +static void pending_rename_remove(struct PendingRename *pr) +{ + struct PendingRename **pp; + + if (!pr) + return; + + /* Cancel timeout timer */ + if (t_active(&pr->timeout)) + timer_del(&pr->timeout); + + /* Unlink from list */ + for (pp = &pending_renames; *pp; pp = &(*pp)->next) { + if (*pp == pr) { + *pp = pr->next; + pending_rename_count--; + break; + } + } + + log_write(LS_DEBUG, L_DEBUG, 0, + "pending_rename_remove: cookie=%u", pr->cookie); + + MyFree(pr); +} + +/** Complete a pending rename (called when services approves). + * @param[in] pr Pending request to complete. + */ +void pending_rename_complete(struct PendingRename *pr) +{ + int rc; + + if (!pr || !pr->client || !pr->channel) + return; + + log_write(LS_DEBUG, L_DEBUG, 0, + "pending_rename_complete: cookie=%u oldname=%s newname=%s", + pr->cookie, pr->oldname, pr->newname); + + /* Re-verify the channel still exists and name matches */ + if (0 != ircd_strcmp(pr->channel->chname, pr->oldname)) { + log_write(LS_DEBUG, L_WARNING, 0, + "pending_rename_complete: channel name changed while waiting"); + pending_rename_remove(pr); + return; + } + + /* Perform the rename (updates pr->channel if reallocated) */ + rc = rename_channel(&pr->channel, pr->newname); + if (rc != 0) { + send_fail(pr->client, "RENAME", "CANNOT_RENAME", pr->oldname, + "Rename failed"); + pending_rename_remove(pr); + return; + } + + /* Send to local channel members */ + send_rename_to_members(pr->client, pr->channel, pr->oldname, pr->reason); + + /* Propagate to other servers */ + sendcmdto_serv_butone(pr->client, CMD_RENAME, cli_from(pr->client), + "%s %s :%s", pr->oldname, pr->channel->chname, + pr->reason); + + pending_rename_remove(pr); +} + +/** Deny a pending rename (called when services denies). + * @param[in] pr Pending request to deny. + * @param[in] reason Reason for denial. + */ +void pending_rename_deny(struct PendingRename *pr, const char *reason) +{ + if (!pr || !pr->client) + return; + + log_write(LS_DEBUG, L_DEBUG, 0, + "pending_rename_deny: cookie=%u reason=%s", + pr->cookie, reason ? reason : "Permission denied"); + + send_fail(pr->client, "RENAME", "CANNOT_RENAME", pr->oldname, + reason ? reason : "Permission denied"); + + pending_rename_remove(pr); +} + +/** Timeout callback for pending rename. + * @param[in] ev Timer event. + */ +static void pending_rename_timeout_cb(struct Event *ev) +{ + struct PendingRename *pr; + + assert(0 != ev_timer(ev)); + assert(0 != t_data(ev_timer(ev))); + + if (ev_type(ev) == ET_EXPIRE) { + pr = (struct PendingRename *)t_data(ev_timer(ev)); + + log_write(LS_DEBUG, L_DEBUG, 0, + "pending_rename_timeout: cookie=%u channel=%s", + pr->cookie, pr->oldname); + + send_fail(pr->client, "RENAME", "CANNOT_RENAME", pr->oldname, + "Services response timeout"); + + /* Timer already fired, mark inactive before remove */ + timer_del(&pr->timeout); + pending_rename_remove(pr); + } +} + +/** Cleanup pending renames for a disconnecting client. + * @param[in] cptr Client that is disconnecting. + */ +void pending_rename_client_exit(struct Client *cptr) +{ + struct PendingRename *pr, *next; + struct PendingRename **pp; + + pp = &pending_renames; + while (*pp) { + pr = *pp; + if (pr->client == cptr) { + /* Cancel timeout and remove */ + if (t_active(&pr->timeout)) + timer_del(&pr->timeout); + *pp = pr->next; + pending_rename_count--; + log_write(LS_DEBUG, L_DEBUG, 0, + "pending_rename_client_exit: removed cookie=%u for %C", + pr->cookie, cptr); + MyFree(pr); + } else { + pp = &(*pp)->next; + } + } +} + +/* ========== End Pending Rename Infrastructure ========== */ + +/** Send RENAME to clients with the capability, fallback PART/JOIN to others. + * @param[in] sptr Client that initiated the rename. + * @param[in] chptr Channel being renamed (already has new name). + * @param[in] oldname The old channel name. + * @param[in] reason Reason for rename (may be empty string). + */ +static void send_rename_to_members(struct Client *sptr, struct Channel *chptr, + const char *oldname, const char *reason) +{ + struct Membership *member; + struct Client *acptr; + + for (member = chptr->members; member; member = member->next_member) { + acptr = member->user; + + if (!MyUser(acptr)) + continue; + + if (CapActive(acptr, CAP_DRAFT_CHANRENAME)) { + /* Client supports draft/channel-rename - send RENAME */ + sendcmdto_one(sptr, CMD_RENAME, acptr, "%s %s :%s", + oldname, chptr->chname, reason ? reason : ""); + } else { + /* Client doesn't support it - send PART/JOIN fallback */ + /* Send PART from old channel */ + sendcmdto_one(acptr, CMD_PART, acptr, "%s :Channel renamed to %s%s%s", + oldname, chptr->chname, + (reason && *reason) ? ": " : "", + (reason && *reason) ? reason : ""); + + /* Send JOIN to new channel */ + sendcmdto_one(acptr, CMD_JOIN, acptr, "%s", chptr->chname); + + /* Send topic if set */ + if (chptr->topic[0]) { + send_reply(acptr, RPL_TOPIC, chptr->chname, chptr->topic); + send_reply(acptr, RPL_TOPICWHOTIME, chptr->chname, chptr->topic_nick, + chptr->topic_time); + } + + /* Send NAMES list with End Of Names */ + do_names(acptr, chptr, NAMES_ALL|NAMES_EON); + } + } +} + +/** m_rename - Handle RENAME command from local client. + * + * parv[0] = sender prefix + * parv[1] = old channel name + * parv[2] = new channel name + * parv[3] = reason (optional, trailing) + * + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message. + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + */ +int m_rename(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + struct Channel *chptr; + struct Membership *member; + const char *oldname; + const char *newname; + const char *reason; + char oldname_buf[CHANNELLEN + 1]; + int rc; + + /* Must have draft/channel-rename capability */ + if (!CapActive(sptr, CAP_DRAFT_CHANRENAME)) { + return send_reply(sptr, ERR_UNKNOWNCOMMAND, "RENAME"); + } + + /* Need at least old and new channel names */ + if (parc < 3 || EmptyString(parv[1]) || EmptyString(parv[2])) { + return send_reply(sptr, ERR_NEEDMOREPARAMS, "RENAME"); + } + + oldname = parv[1]; + newname = parv[2]; + reason = (parc > 3 && !EmptyString(parv[3])) ? parv[3] : ""; + + /* Check if old channel exists */ + chptr = FindChannel(oldname); + if (!chptr) { + return send_reply(sptr, ERR_NOSUCHCHANNEL, oldname); + } + + /* Check if user is on the channel */ + member = find_channel_member(sptr, chptr); + if (!member) { + return send_reply(sptr, ERR_NOTONCHANNEL, oldname); + } + + /* Check if user is a channel operator */ + if (!IsChanOp(member)) { + return send_reply(sptr, ERR_CHANOPRIVSNEEDED, oldname); + } + + /* Validate new channel name */ + if (!IsChannelName(newname)) { + send_fail(sptr, "RENAME", "CANNOT_RENAME", oldname, + "Invalid channel name"); + return 0; + } + + /* Check if new channel name already exists */ + if (FindChannel(newname)) { + send_fail(sptr, "RENAME", "CHANNEL_NAME_IN_USE", oldname, + "Channel name already in use"); + return 0; + } + + /* For registered channels (+R), query services for permission */ + if (chptr->mode.mode & MODE_REGISTERED) { + struct Client *services = find_services_server(); + struct PendingRename *pr; + + if (!services) { + send_fail(sptr, "RENAME", "CANNOT_RENAME", oldname, + "Services unavailable"); + return 0; + } + + /* Create pending rename request */ + pr = pending_rename_add(sptr, chptr, newname, reason); + if (!pr) { + send_fail(sptr, "RENAME", "CANNOT_RENAME", oldname, + "Too many pending requests"); + return 0; + } + + /* Send permission query to services: + * AC R RENAME + */ + sendcmdto_one(&me, CMD_ACCOUNT, services, + "%C R %u %s RENAME %s", + sptr, pr->cookie, chptr->chname, newname); + + log_write(LS_DEBUG, L_DEBUG, 0, + "m_rename: Querying services for %s -> %s (cookie=%u)", + oldname, newname, pr->cookie); + + return 0; /* Wait for services response */ + } + + /* Unregistered channel - proceed with rename immediately */ + + /* Store old name before rename */ + ircd_strncpy(oldname_buf, chptr->chname, CHANNELLEN); + oldname_buf[CHANNELLEN] = '\0'; + + /* Perform the rename (updates chptr if reallocated) */ + rc = rename_channel(&chptr, newname); + if (rc == -1) { + send_fail(sptr, "RENAME", "CANNOT_RENAME", oldname, + "New channel name is too long"); + return 0; + } else if (rc == -2) { + send_fail(sptr, "RENAME", "CHANNEL_NAME_IN_USE", oldname, + "Channel name already in use"); + return 0; + } + + /* Send to local channel members */ + send_rename_to_members(sptr, chptr, oldname_buf, reason); + + /* Propagate to other servers */ + sendcmdto_serv_butone(sptr, CMD_RENAME, cptr, "%s %s :%s", + oldname_buf, chptr->chname, reason); + + return 0; +} + +/** ms_rename - Handle RENAME command from a server. + * + * parv[0] = sender prefix (numeric) + * parv[1] = old channel name + * parv[2] = new channel name + * parv[3] = reason (optional, trailing) + * + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message. + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + */ +int ms_rename(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + struct Channel *chptr; + const char *oldname; + const char *newname; + const char *reason; + char oldname_buf[CHANNELLEN + 1]; + int rc; + + /* Need at least old and new channel names */ + if (parc < 3 || EmptyString(parv[1]) || EmptyString(parv[2])) { + return 0; /* Silently ignore malformed S2S messages */ + } + + oldname = parv[1]; + newname = parv[2]; + reason = (parc > 3 && !EmptyString(parv[3])) ? parv[3] : ""; + + /* Find the channel */ + chptr = FindChannel(oldname); + if (!chptr) { + return 0; /* Channel doesn't exist on this server */ + } + + /* Store old name before rename */ + ircd_strncpy(oldname_buf, chptr->chname, CHANNELLEN); + oldname_buf[CHANNELLEN] = '\0'; + + /* Perform the rename (updates chptr if reallocated) */ + rc = rename_channel(&chptr, newname); + if (rc != 0) { + /* Rename failed - log and continue */ + log_write(LS_DEBUG, L_ERROR, 0, + "RENAME failed from %#C: %s -> %s (rc=%d)", + sptr, oldname, newname, rc); + return 0; + } + + /* Send to local channel members */ + send_rename_to_members(sptr, chptr, oldname_buf, reason); + + /* Propagate to other servers */ + sendcmdto_serv_butone(sptr, CMD_RENAME, cptr, "%s %s :%s", + oldname_buf, chptr->chname, reason); + + return 0; +} diff --git a/ircd/m_sasl.c b/ircd/m_sasl.c index dd58d2db..cd01a385 100644 --- a/ircd/m_sasl.c +++ b/ircd/m_sasl.c @@ -82,6 +82,7 @@ */ #include "config.h" +#include "capab.h" #include "client.h" #include "ircd.h" #include "ircd_features.h" @@ -96,6 +97,8 @@ #include "s_auth.h" #include "s_bsd.h" #include "s_misc.h" +#include "s_user.h" +#include "metadata.h" /* #include -- Now using assert in ircd_log.h */ @@ -120,6 +123,12 @@ int ms_sasl(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) ext = parv[5]; if (!strcmp(parv[1], "*")) { + /* Check for mechanism list broadcast: SASL * * M :PLAIN,EXTERNAL,... */ + if (!strcmp(token, "*") && reply[0] == 'M') { + set_sasl_mechanisms(data); + log_write(LS_SYSTEM, L_INFO, 0, "SASL mechanisms set to: %s", data); + } + if (ext != NULL) sendcmdto_serv_butone(sptr, CMD_SASL, cptr, "* %s %s %s :%s", token, reply, data, ext); @@ -146,34 +155,82 @@ int ms_sasl(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) } /* If token is not prefixed with my numnick then ignore */ - if (strncmp(cli_yxx(&me), token, 2)) + if (strncmp(cli_yxx(&me), token, 2)) { + log_write(LS_DEBUG, L_DEBUG, 0, "SASL: Token prefix mismatch - expected %s, got %.2s (from %C)", + cli_yxx(&me), token, sptr); return 0; + } /* If there is no fd then it is an invalid token */ - if ((fdstr = strchr(token, '!')) == NULL) + if ((fdstr = strchr(token, '!')) == NULL) { + protocol_violation(sptr, "SASL: Malformed token - missing fd separator '!' in '%s'", token); return 0; + } fdstr++; /* If there is no cookie then it is also an invalid token */ - if ((cookiestr = strchr(token, '.')) == NULL) + if ((cookiestr = strchr(token, '.')) == NULL) { + protocol_violation(sptr, "SASL: Malformed token - missing cookie separator '.' in '%s'", token); return 0; + } *cookiestr++ = '\0'; fd = atoi(fdstr); cookie = atoi(cookiestr); /* Could not find a matching client, ignore the message */ - if (!(acptr = LocalClientArray[fd]) || (cli_saslcookie(acptr) != cookie)) + if (!(acptr = LocalClientArray[fd])) { + log_write(LS_DEBUG, L_DEBUG, 0, "SASL: Client for fd %u not found (from %C, cookie %u)", + fd, sptr, cookie); + return 0; + } + if (cli_saslcookie(acptr) != cookie) { + log_write(LS_DEBUG, L_DEBUG, 0, "SASL: Cookie mismatch for %C (fd %u) - expected %u, got %u (from %C)", + acptr, fd, cli_saslcookie(acptr), cookie, sptr); return 0; + } + + /* Check for stale SASL responses (FD reuse protection) */ + if (cli_saslstart(acptr) > 0) { + int timeout = feature_int(FEAT_SASL_TIMEOUT); + if (timeout > 0 && (CurrentTime - cli_saslstart(acptr)) > timeout) { + log_write(LS_SYSTEM, L_WARNING, 0, + "SASL: Stale response for %C (fd %u) - session started %ld seconds ago (timeout %d)", + acptr, fd, (long)(CurrentTime - cli_saslstart(acptr)), timeout); + return 0; + } + } /* OK we now know who the message is for, let's deal with it! */ + /* Validate the sender is a valid server (not dead/disconnecting) */ + if (!IsServer(sptr) || IsDead(sptr)) { + log_write(LS_DEBUG, L_DEBUG, 0, + "SASL: Response from invalid/dead server %C, ignoring", sptr); + return 0; + } + /* If we don't know who the agent is we do now, else check its the same agent */ if (!cli_saslagent(acptr)) { cli_saslagent(acptr) = sptr; cli_saslagentref(sptr)++; - } else if (cli_saslagent(acptr) != sptr) - return 0; + } else if (cli_saslagent(acptr) != sptr) { + /* Check if existing agent is dead - if so, accept new agent */ + if (IsDead(cli_saslagent(acptr)) || !IsServer(cli_saslagent(acptr))) { + log_write(LS_DEBUG, L_DEBUG, 0, + "SASL: Previous agent %C is dead, accepting new agent %C", + cli_saslagent(acptr), sptr); + if (cli_saslagentref(cli_saslagent(acptr))) + cli_saslagentref(cli_saslagent(acptr))--; + cli_saslagent(acptr) = sptr; + cli_saslagentref(sptr)++; + } else { + log_write(LS_SYSTEM, L_WARNING, 0, + "SASL: Agent mismatch for %C - expected %C, got %C (ignoring response)", + acptr, cli_saslagent(acptr), sptr); + return 0; + } + } if (reply[0] == 'C') { sendrawto_one(acptr, MSG_AUTHENTICATE " %s", data); @@ -198,6 +255,61 @@ int ms_sasl(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) if (data[0] == 'S') { SetSASLComplete(acptr); send_reply(acptr, RPL_SASLSUCCESS); + + /* For registered users (post-registration reauth), update their account + * and broadcast AC to propagate the change network-wide. + */ + if (IsRegistered(acptr) && cli_user(acptr) && cli_saslaccount(acptr)[0]) { + char type = IsAccount(acptr) ? 'M' : 'R'; + + /* Update account if changed */ + if (ircd_strcmp(cli_user(acptr)->account, cli_saslaccount(acptr)) != 0) { + /* Load account-linked metadata BEFORE setting account flag. + * This ensures metadata is in place before we propagate the account + * change to other servers and notify channel members. + */ + metadata_load_account(acptr, cli_saslaccount(acptr)); + + ircd_strncpy(cli_user(acptr)->account, cli_saslaccount(acptr), ACCOUNTLEN); + SetAccount(acptr); + + if (cli_saslacccreate(acptr)) + cli_user(acptr)->acc_create = cli_saslacccreate(acptr); + + /* Notify channel members with account-notify capability */ + sendcmdto_common_channels_capab_butone(acptr, CMD_ACCOUNT, acptr, + CAP_ACCNOTIFY, CAP_NONE, + "%s", cli_user(acptr)->account); + + /* Propagate to other servers - use extended format if enabled */ + if (feature_bool(FEAT_EXTENDED_ACCOUNTS)) { + if (cli_user(acptr)->acc_create) { + sendcmdto_serv_butone(&me, CMD_ACCOUNT, NULL, "%C %c %s %Tu", + acptr, type, cli_user(acptr)->account, + cli_user(acptr)->acc_create); + } else { + sendcmdto_serv_butone(&me, CMD_ACCOUNT, NULL, "%C %c %s", + acptr, type, cli_user(acptr)->account); + } + } else { + /* Non-extended format: AC [timestamp] */ + if (cli_user(acptr)->acc_create) { + sendcmdto_serv_butone(&me, CMD_ACCOUNT, NULL, "%C %s %Tu", + acptr, cli_user(acptr)->account, + cli_user(acptr)->acc_create); + } else { + sendcmdto_serv_butone(&me, CMD_ACCOUNT, NULL, "%C %s", + acptr, cli_user(acptr)->account); + } + } + + /* Apply hidden host if applicable */ + if (((feature_int(FEAT_HOST_HIDING_STYLE) == 1) || + (feature_int(FEAT_HOST_HIDING_STYLE) == 3)) && + IsHiddenHost(acptr)) + hide_hostmask(acptr); + } + } } else if (data[0] == 'F') { send_reply(acptr, ERR_SASLFAIL, ""); } else if (data[0] == 'A') { @@ -207,6 +319,7 @@ int ms_sasl(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) cli_saslagentref(cli_saslagent(acptr))--; cli_saslagent(acptr) = NULL; cli_saslcookie(acptr) = 0; + cli_saslstart(acptr) = 0; if (t_active(&cli_sasltimeout(acptr))) timer_del(&cli_sasltimeout(acptr)); } else if (reply[0] == 'M') @@ -232,6 +345,13 @@ int abort_sasl(struct Client* cptr, int timeout) { acptr = NULL; } + /* Validate agent is still a valid, connected server */ + if (acptr && (IsDead(acptr) || !IsServer(acptr))) { + log_write(LS_DEBUG, L_DEBUG, 0, + "SASL abort: Agent %C is dead/invalid, broadcasting instead", acptr); + acptr = NULL; + } + if (timeout) send_reply(cptr, ERR_SASLFAIL, ": request timed out"); else @@ -248,6 +368,7 @@ int abort_sasl(struct Client* cptr, int timeout) { cli_saslagentref(cli_saslagent(cptr))--; cli_saslagent(cptr) = NULL; cli_saslcookie(cptr) = 0; + cli_saslstart(cptr) = 0; return 0; } diff --git a/ircd/m_server.c b/ircd/m_server.c index 989612e5..b7f93cdc 100644 --- a/ircd/m_server.c +++ b/ircd/m_server.c @@ -768,9 +768,12 @@ int ms_server(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) for (bcptr = cli_serv(acptr)->up; !IsMe(bcptr); bcptr = cli_serv(bcptr)->up) if (IsBurstOrBurstAck(bcptr)) break; - if (IsMe(bcptr)) + if (IsMe(bcptr)) { sendto_opmask_butone(0, SNO_NETWORK, "Net junction: %s %s", cli_name(sptr), cli_name(acptr)); + /* Start IRCv3 netjoin batch for local clients */ + send_netjoin_batch_start(acptr, cli_serv(acptr)->up); + } } /* * Old sendto_serv_but_one() call removed because we now need to send diff --git a/ircd/m_sethost.c b/ircd/m_sethost.c index cd355807..0d3ae7cf 100644 --- a/ircd/m_sethost.c +++ b/ircd/m_sethost.c @@ -84,6 +84,8 @@ #include "client.h" #include "hash.h" #include "ircd.h" +#include "ircd_alloc.h" +#include "ircd_crypt.h" #include "ircd_features.h" #include "ircd_log.h" #include "ircd_reply.h" @@ -91,12 +93,91 @@ #include "ircd_string.h" #include "numeric.h" #include "numnicks.h" +#include "s_bsd.h" #include "s_conf.h" +#include "s_debug.h" #include "s_user.h" #include "send.h" /* #include -- Now using assert in ircd_log.h */ +/** Context for async SETHOST password verification */ +struct sethost_verify_ctx { + int fd; /**< Client fd for lookup */ + char hostmask[USERLEN + HOSTLEN + 2]; /**< Requested user@host mask */ + struct SHostConf *sconf; /**< Matched config block */ + struct Flags setflags; /**< Backed up client flags */ +}; + +/** + * Apply SETHOST changes to a client after password verification succeeds. + * @param[in] sptr Client to apply changes to + * @param[in] hostmask Hostmask to set (user@host or just host) + * @param[in] sconf Matched SpoofHost configuration (may be NULL) + * @param[in] setflags Backed up client flags for mode change notification + */ +static void apply_sethost_changes(struct Client *sptr, const char *hostmask, + struct SHostConf *sconf, struct Flags *setflags) +{ + /* Apply the spoofhost */ + if (strchr(hostmask, '@') != NULL) + ircd_strncpy(cli_user(sptr)->sethost, hostmask, HOSTLEN + 1); + else + ircd_snprintf(0, cli_user(sptr)->sethost, USERLEN + HOSTLEN + 1, "%s@%s", + cli_user(sptr)->username, hostmask); + + if (FlagHas(setflags, FLAG_SETHOST)) + FlagClr(setflags, FLAG_SETHOST); + SetSetHost(sptr); + SetHiddenHost(sptr); + + hide_hostmask(sptr); + send_umode_out(sptr, sptr, setflags, 0); +} + +/** + * Callback invoked when async SETHOST password verification completes. + * Called in main thread context via thread_pool_poll(). + */ +static void sethost_password_verified(int result, void *arg) +{ + struct sethost_verify_ctx *ctx = arg; + struct Client *sptr; + + /* Look up client by fd */ + if (ctx->fd < 0 || ctx->fd >= MAXCONNECTIONS) { + MyFree(ctx); + return; + } + + sptr = LocalClientArray[ctx->fd]; + + /* Verify client still exists and is pending verification */ + if (!sptr || IsDead(sptr) || !IsSetHostPending(sptr)) { + Debug((DEBUG_DEBUG, "sethost_password_verified: client gone or not pending " + "(fd %d)", ctx->fd)); + MyFree(ctx); + return; + } + + /* Clear pending flag */ + ClearSetHostPending(sptr); + + if (result == CRYPT_VERIFY_MATCH) { + /* Password matched - apply SETHOST changes */ + Debug((DEBUG_INFO, "sethost_password_verified: SETHOST success for %s", + cli_name(sptr))); + apply_sethost_changes(sptr, ctx->hostmask, ctx->sconf, &ctx->setflags); + } else { + /* Password didn't match */ + send_reply(sptr, ERR_PASSWDMISMATCH); + Debug((DEBUG_INFO, "sethost_password_verified: SETHOST failed for %s", + cli_name(sptr))); + } + + MyFree(ctx); +} + /* * m_sethost - generic message handler */ @@ -114,40 +195,77 @@ int m_sethost(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) if (parc < 2) return need_more_params(sptr, "SETHOST"); + /* Already pending async verification? */ + if (IsSetHostPending(sptr)) + return 0; + /* Back up the flags first */ setflags = cli_flags(sptr); if (ircd_strcmp("undo", parv[1]) == 0) { ClearSetHost(sptr); cli_user(sptr)->sethost[0] = '\0'; - } else if (parc < 3) { + hide_hostmask(sptr); + send_umode_out(cptr, sptr, &setflags, 0); + return 0; + } + + if (parc < 3) return need_more_params(sptr, "SETHOST"); - } else { - if (!valid_hostname(parv[1])) { - send_reply(sptr, ERR_BADHOSTMASK, parv[1]); - } else { - sconf = find_shost_conf(sptr, parv[1], parv[2], &res); - if ((res == 0) && (sconf != 0)) { - if (strchr(parv[1], '@') != NULL) - ircd_strncpy(cli_user(sptr)->sethost, parv[1], HOSTLEN + 1); - else - ircd_snprintf(0, cli_user(sptr)->sethost, USERLEN + HOSTLEN + 1, "%s@%s", - cli_user(sptr)->username, parv[1]); - if (FlagHas(&setflags, FLAG_SETHOST)) - FlagClr(&setflags, FLAG_SETHOST); - SetSetHost(sptr); - SetHiddenHost(sptr); - } else { - if (res == 1) - send_reply(sptr, ERR_PASSWDMISMATCH); - else - send_reply(sptr, ERR_HOSTUNAVAIL, parv[1]); + + if (!valid_hostname(parv[1])) { + send_reply(sptr, ERR_BADHOSTMASK, parv[1]); + return 0; + } + + /* Find matching SHost block by host (without password check) */ + sconf = find_shost_conf_by_host(sptr, parv[1]); + if (!sconf) { + send_reply(sptr, ERR_HOSTUNAVAIL, parv[1]); + return 0; + } + + /* Check if password is required */ + if (!EmptyString(sconf->passwd)) { + /* Password required - try async verification if available */ + if (ircd_crypt_async_available()) { + struct sethost_verify_ctx *ctx; + + ctx = (struct sethost_verify_ctx *)MyMalloc(sizeof(struct sethost_verify_ctx)); + ctx->fd = cli_fd(sptr); + ctx->sconf = sconf; + ctx->setflags = setflags; + ircd_strncpy(ctx->hostmask, parv[1], sizeof(ctx->hostmask) - 1); + + if (ircd_crypt_verify_async(parv[2], sconf->passwd, + sethost_password_verified, ctx) == 0) { + /* Async verification started */ + SetSetHostPending(sptr); + Debug((DEBUG_INFO, "m_sethost: started async verification for %s", + cli_name(sptr))); + return 0; } + + /* Async failed to start, fall back to sync */ + MyFree(ctx); + Debug((DEBUG_DEBUG, "m_sethost: async failed, falling back to sync for %s", + cli_name(sptr))); + } + + /* Synchronous password verification (blocking if bcrypt) */ + sconf = find_shost_conf(sptr, parv[1], parv[2], &res); + if (res == 1) { + send_reply(sptr, ERR_PASSWDMISMATCH); + return 0; + } + if (res == 2 || !sconf) { + send_reply(sptr, ERR_HOSTUNAVAIL, parv[1]); + return 0; } } - hide_hostmask(sptr); - send_umode_out(cptr, sptr, &setflags, 0); + /* Password verified (or not required) - apply changes */ + apply_sethost_changes(sptr, parv[1], sconf, &setflags); return 0; } @@ -170,41 +288,100 @@ int mo_sethost(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) if (parc < 2) return need_more_params(sptr, "SETHOST"); + /* Already pending async verification? */ + if (IsSetHostPending(sptr)) + return 0; + /* Back up the flags first */ setflags = cli_flags(sptr); if (ircd_strcmp("undo", parv[1]) == 0) { ClearSetHost(sptr); cli_user(sptr)->sethost[0] = '\0'; - } else if (parc < 3) { + hide_hostmask(sptr); + send_umode_out(cptr, sptr, &setflags, 0); + return 0; + } + + if (parc < 3) return need_more_params(sptr, "SETHOST"); - } else { - ircd_snprintf(0, hostmask, USERLEN + HOSTLEN + 1, "%s@%s", parv[1], parv[2]); - if (!valid_username(parv[1]) || !valid_hostname(parv[2])) { - send_reply(sptr, ERR_BADHOSTMASK, hostmask); - } else if (HasPriv(sptr, PRIV_FREEFORM)) { - ircd_strncpy(cli_user(sptr)->sethost, hostmask, USERLEN + HOSTLEN + 1); - if (FlagHas(&setflags, FLAG_SETHOST)) - FlagClr(&setflags, FLAG_SETHOST); - SetSetHost(sptr); - SetHiddenHost(sptr); - } else { - sconf = find_shost_conf(sptr, hostmask, NULL, &res); - if ((res == 0) && (sconf != 0)) { - ircd_strncpy(cli_user(sptr)->sethost, hostmask, USERLEN + HOSTLEN + 1); - if (FlagHas(&setflags, FLAG_SETHOST)) - FlagClr(&setflags, FLAG_SETHOST); - SetSetHost(sptr); - SetHiddenHost(sptr); - } else { - send_reply(sptr, ERR_HOSTUNAVAIL, hostmask); + + ircd_snprintf(0, hostmask, USERLEN + HOSTLEN + 1, "%s@%s", parv[1], parv[2]); + + if (!valid_username(parv[1]) || !valid_hostname(parv[2])) { + send_reply(sptr, ERR_BADHOSTMASK, hostmask); + return 0; + } + + /* If oper has PRIV_FREEFORM, allow any hostmask without password */ + if (HasPriv(sptr, PRIV_FREEFORM)) { + ircd_strncpy(cli_user(sptr)->sethost, hostmask, USERLEN + HOSTLEN + 1); + if (FlagHas(&setflags, FLAG_SETHOST)) + FlagClr(&setflags, FLAG_SETHOST); + SetSetHost(sptr); + SetHiddenHost(sptr); + hide_hostmask(sptr); + send_umode_out(cptr, sptr, &setflags, 0); + return 0; + } + + /* Find matching SHost block by host (without password check) */ + sconf = find_shost_conf_by_host(sptr, hostmask); + if (!sconf) { + send_reply(sptr, ERR_HOSTUNAVAIL, hostmask); + return 0; + } + + /* Check if password is required */ + if (!EmptyString(sconf->passwd)) { + /* Oper needs password too (unless PRIV_FREEFORM which was handled above) */ + if (parc < 4) { + send_reply(sptr, ERR_NEEDMOREPARAMS, "SETHOST"); + return 0; + } + + /* Password required - try async verification if available */ + if (ircd_crypt_async_available()) { + struct sethost_verify_ctx *ctx; + + ctx = (struct sethost_verify_ctx *)MyMalloc(sizeof(struct sethost_verify_ctx)); + ctx->fd = cli_fd(sptr); + ctx->sconf = sconf; + ctx->setflags = setflags; + ircd_strncpy(ctx->hostmask, hostmask, sizeof(ctx->hostmask) - 1); + + if (ircd_crypt_verify_async(parv[3], sconf->passwd, + sethost_password_verified, ctx) == 0) { + /* Async verification started */ + SetSetHostPending(sptr); + Debug((DEBUG_INFO, "mo_sethost: started async verification for %s", + cli_name(sptr))); + return 0; } + + /* Async failed to start, fall back to sync */ + MyFree(ctx); + Debug((DEBUG_DEBUG, "mo_sethost: async failed, falling back to sync for %s", + cli_name(sptr))); + } + + /* Synchronous password verification (blocking if bcrypt) */ + sconf = find_shost_conf(sptr, hostmask, parv[3], &res); + if (res != 0 || !sconf) { + send_reply(sptr, ERR_HOSTUNAVAIL, hostmask); + return 0; } } + /* Password verified (or not required) - apply changes */ + ircd_strncpy(cli_user(sptr)->sethost, hostmask, USERLEN + HOSTLEN + 1); + if (FlagHas(&setflags, FLAG_SETHOST)) + FlagClr(&setflags, FLAG_SETHOST); + SetSetHost(sptr); + SetHiddenHost(sptr); + hide_hostmask(sptr); send_umode_out(cptr, sptr, &setflags, 0); return 0; } - diff --git a/ircd/m_setname.c b/ircd/m_setname.c new file mode 100644 index 00000000..5f469810 --- /dev/null +++ b/ircd/m_setname.c @@ -0,0 +1,198 @@ +/* + * IRC - Internet Relay Chat, ircd/m_setname.c + * Copyright (C) 1990 Jarkko Oikarinen and + * University of Oulu, Computing Center + * + * 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. + */ + +/* + * m_functions execute protocol messages on this server: + * + * cptr is always NON-NULL, pointing to a *LOCAL* client + * structure (with an open socket connected!). This + * identifies the physical socket where the message + * originated (or which caused the m_function to be + * executed--some m_functions may call others...). + * + * sptr is the source of the message, defined by the + * prefix part of the message if present. If not + * or prefix not found, then sptr==cptr. + * + * (!IsServer(cptr)) => (cptr == sptr), because + * prefixes are taken *only* from servers... + * + * (IsServer(cptr)) + * (sptr == cptr) => the message didn't + * have the prefix. + * + * (sptr != cptr && IsServer(sptr) means + * the prefix specified servername. (?) + * + * (sptr != cptr && !IsServer(sptr) means + * that message originated from a remote + * user (not local). + * + * combining + * + * (!IsServer(sptr)) means that, sptr can safely + * taken as defining the target structure of the + * message in this server. + * + * *Always* true (if 'parse' and others are working correct): + * + * 1) sptr->from == cptr (note: cptr->from == cptr) + * + * 2) MyConnect(sptr) <=> sptr == cptr (e.g. sptr + * *cannot* be a local connection, unless it's + * actually cptr!). [MyConnect(x) should probably + * be defined as (x == x->from) --msa ] + * + * parc number of variable parameter strings (if zero, + * parv is allowed to be NULL) + * + * parv a NULL terminated list of parameter pointers, + * + * parv[0], sender (prefix string), if not present + * this points to an empty string. + * parv[1]...parv[parc-1] + * pointers to additional parameters + * parv[parc] == NULL, *always* + * + * note: it is guaranteed that parv[0]..parv[parc-1] are all + * non-NULL pointers. + */ +#include "config.h" + +#include "capab.h" +#include "client.h" +#include "ircd.h" +#include "ircd_features.h" +#include "ircd_log.h" +#include "ircd_reply.h" +#include "ircd_string.h" +#include "msg.h" +#include "numeric.h" +#include "numnicks.h" +#include "send.h" + +/* #include -- Now using assert in ircd_log.h */ +#include + +/* + * m_setname - local client message handler + * + * parv[0] = sender prefix + * parv[1] = new realname + * + * Allow users to change their realname (GECOS) field. + * IRCv3 setname specification: https://ircv3.net/specs/extensions/setname + */ +int m_setname(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) +{ + char *newname; + + assert(0 != cptr); + assert(cptr == sptr); + + /* Check if setname capability is enabled */ + if (!feature_bool(FEAT_CAP_setname)) { + if (CapActive(sptr, CAP_STANDARDREPLIES)) + send_fail(sptr, "SETNAME", "DISABLED", NULL, "SETNAME command is disabled"); + return send_reply(sptr, ERR_UNKNOWNCOMMAND, "SETNAME"); + } + + if (parc < 2 || EmptyString(parv[1])) { + if (CapActive(sptr, CAP_STANDARDREPLIES)) + send_fail(sptr, "SETNAME", "NEED_MORE_PARAMS", NULL, "Missing realname"); + return send_reply(sptr, ERR_NEEDMOREPARAMS, "SETNAME"); + } + + newname = parv[1]; + + /* Truncate if necessary */ + if (strlen(newname) > REALLEN) + newname[REALLEN] = '\0'; + + /* Check if realname actually changed */ + if (ircd_strcmp(cli_info(sptr), newname) == 0) + return 0; + + /* Update the realname */ + ircd_strncpy(cli_info(sptr), newname, REALLEN); + + /* Propagate to other servers */ + sendcmdto_serv_butone(sptr, CMD_SETNAME, cptr, ":%s", cli_info(sptr)); + + /* Echo SETNAME back to the sender per IRCv3 spec */ + if (CapActive(sptr, CAP_SETNAME)) + sendcmdto_one(sptr, CMD_SETNAME, sptr, ":%s", cli_info(sptr)); + + /* Notify channel members with setname capability (excluding sender who already got echo) */ + sendcmdto_common_channels_capab_butone(sptr, CMD_SETNAME, sptr, + CAP_SETNAME, CAP_NONE, + ":%s", cli_info(sptr)); + + return 0; +} + +/* + * ms_setname - server message handler + * + * parv[0] = sender prefix + * parv[1] = new realname + * + * Handle SETNAME from other servers (P10: SE token). + */ +int ms_setname(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) +{ + char *newname; + + assert(0 != cptr); + assert(0 != sptr); + + /* Servers can't set realname */ + if (IsServer(sptr)) + return protocol_violation(sptr, "Server trying to set realname"); + + if (parc < 2 || EmptyString(parv[1])) + return 0; + + newname = parv[1]; + + /* Truncate if necessary */ + if (strlen(newname) > REALLEN) + newname[REALLEN] = '\0'; + + /* Check if realname actually changed */ + if (ircd_strcmp(cli_info(sptr), newname) == 0) + return 0; + + /* Update the realname */ + ircd_strncpy(cli_info(sptr), newname, REALLEN); + + /* Propagate to other servers */ + sendcmdto_serv_butone(sptr, CMD_SETNAME, cptr, ":%s", cli_info(sptr)); + + /* Notify local channel members with setname capability */ + sendcmdto_common_channels_capab_butone(sptr, CMD_SETNAME, sptr, + CAP_SETNAME, CAP_NONE, + ":%s", cli_info(sptr)); + + return 0; +} diff --git a/ircd/m_starttls.c b/ircd/m_starttls.c index e11230e4..688f042e 100644 --- a/ircd/m_starttls.c +++ b/ircd/m_starttls.c @@ -87,12 +87,14 @@ #include "handlers.h" #include "hash.h" #include "ircd.h" +#include "ircd_features.h" #include "ircd_log.h" #include "ircd_reply.h" #include "ircd_string.h" #include "numeric.h" #include "numnicks.h" #include "send.h" +#include "ssl.h" /* #include -- Now using assert in ircd_log.h */ @@ -121,8 +123,11 @@ int m_starttls(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) if (ssl_is_init_finished(cli_socket(cptr).ssl)) { char *sslfp = ssl_get_fingerprint(cli_socket(cptr).ssl); - if (sslfp) + if (sslfp) { ircd_strncpy(cli_sslclifp(cptr), sslfp, BUFSIZE+1); + if (feature_bool(FEAT_CERT_EXPIRY_TRACKING)) + cli_sslcliexp(cptr) = ssl_get_cert_expiry(cli_socket(cptr).ssl); + } } #endif return 0; diff --git a/ircd/m_svsjoin.c b/ircd/m_svsjoin.c index 507e7547..740c8cde 100644 --- a/ircd/m_svsjoin.c +++ b/ircd/m_svsjoin.c @@ -25,6 +25,7 @@ #include "config.h" +#include "capab.h" #include "channel.h" #include "client.h" #include "gline.h" @@ -194,7 +195,9 @@ int ms_svsjoin(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) chptr->topic_time); } - do_names(acptr, chptr, NAMES_ALL|NAMES_EON); /* send /names list */ + /* Skip implicit NAMES if client has draft/no-implicit-names capability */ + if (!HasCap(acptr, CAP_DRAFT_NOIMPLICITNAMES)) + do_names(acptr, chptr, NAMES_ALL|NAMES_EON); /* send /names list */ } joinbuf_flush(&join); /* must be first, if there's a JOIN 0 */ diff --git a/ircd/m_tagmsg.c b/ircd/m_tagmsg.c new file mode 100644 index 00000000..5dca93d1 --- /dev/null +++ b/ircd/m_tagmsg.c @@ -0,0 +1,354 @@ +/* + * IRC - Internet Relay Chat, ircd/m_tagmsg.c + * Copyright (C) 1990 Jarkko Oikarinen and + * University of Oulu, Computing Center + * + * 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. + */ + +/* + * m_functions execute protocol messages on this server: + * + * cptr is always NON-NULL, pointing to a *LOCAL* client + * structure (with an open socket connected!). This + * identifies the physical socket where the message + * originated (or which caused the m_function to be + * executed--some m_functions may call others...). + * + * sptr is the source of the message, defined by the + * prefix part of the message if present. If not + * or prefix not found, then sptr==cptr. + * + * (!IsServer(cptr)) => (cptr == sptr), because + * prefixes are taken *only* from servers... + * + * (IsServer(cptr)) + * (sptr == cptr) => the message didn't + * have the prefix. + * + * (sptr != cptr && IsServer(sptr) means + * the prefix specified servername. (?) + * + * (sptr != cptr && !IsServer(sptr) means + * that message originated from a remote + * user (not local). + * + * combining + * + * (!IsServer(sptr)) means that, sptr can safely + * taken as defining the target structure of the + * message in this server. + * + * *Always* true (if 'parse' and others are working correct): + * + * 1) sptr->from == cptr (note: cptr->from == cptr) + * + * 2) MyConnect(sptr) <=> sptr == cptr (e.g. sptr + * *cannot* be a local connection, unless it's + * actually cptr!). [MyConnect(x) should probably + * be defined as (x == x->from) --msa ] + * + * parc number of variable parameter strings (if zero, + * parv is allowed to be NULL) + * + * parv a NULL terminated list of parameter pointers, + * + * parv[0], sender (prefix string), if not present + * this points to an empty string. + * parv[1]...parv[parc-1] + * pointers to additional parameters + * parv[parc] == NULL, *always* + * + * note: it is guaranteed that parv[0]..parv[parc-1] are all + * non-NULL pointers. + */ +#include "config.h" + +#include "capab.h" +#include "channel.h" +#include "client.h" +#include "hash.h" +#include "history.h" +#include "ircd.h" +#include "ircd_features.h" +#include "ircd_log.h" +#include "ircd_reply.h" +#include "ircd_snprintf.h" +#include "ircd_string.h" +#include "msg.h" +#include "numeric.h" +#include "numnicks.h" +#include "send.h" +#include "s_user.h" + +/* #include -- Now using assert in ircd_log.h */ +#include +#include + +/** Counter for generating unique message IDs for history */ +static unsigned long tagmsg_history_counter = 0; + +/** + * Store a TAGMSG in channel history for event-playback. + * TAGMSG content is stored as the client-only tags. + */ +static void store_tagmsg_history(struct Client *sptr, struct Channel *chptr, + const char *client_tags) +{ + struct timeval tv; + char timestamp[32]; + char msgid[64]; + char sender[HISTORY_SENDER_LEN]; + const char *account; + + if (!history_is_available()) + return; + + /* Only store if event-playback is enabled */ + if (!feature_bool(FEAT_CAP_draft_event_playback)) + return; + + /* Generate Unix timestamp for storage */ + gettimeofday(&tv, NULL); + ircd_snprintf(0, timestamp, sizeof(timestamp), "%lu.%03lu", + (unsigned long)tv.tv_sec, + (unsigned long)(tv.tv_usec / 1000)); + + /* Generate unique msgid */ + ircd_snprintf(0, msgid, sizeof(msgid), "%s-%lu-%lu", + cli_yxx(&me), + (unsigned long)cli_firsttime(&me), + ++tagmsg_history_counter); + + /* Build sender string: nick!user@host */ + if (cli_user(sptr)) + ircd_snprintf(0, sender, sizeof(sender), "%s!%s@%s", + cli_name(sptr), + cli_user(sptr)->username, + cli_user(sptr)->host); + else + ircd_strncpy(sender, cli_name(sptr), sizeof(sender) - 1); + + /* Get account name if logged in */ + account = (cli_user(sptr) && cli_user(sptr)->account[0]) + ? cli_user(sptr)->account : NULL; + + /* Store in database - content is the client-only tags */ + history_store_message(msgid, timestamp, chptr->chname, sender, + account, HISTORY_TAGMSG, client_tags); +} + +/* + * m_tagmsg - local client message handler + * + * parv[0] = sender prefix + * parv[1] = target (channel or user) + * + * TAGMSG sends a message with only tags (no content). + * Used for client-only tags like +typing. + * IRCv3 specification: https://ircv3.net/specs/extensions/message-tags + * + * Client-only tags (prefixed with +) are extracted by parse.c + * and stored in cli_client_tags(). This handler relays them to recipients. + */ +int m_tagmsg(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) +{ + struct Channel* chptr; + struct Client* acptr; + char* target; + const char* client_tags; + + assert(0 != cptr); + assert(cptr == sptr); + + if (parc < 2 || EmptyString(parv[1])) { + if (CapActive(sptr, CAP_STANDARDREPLIES)) + send_fail(sptr, "TAGMSG", "NEED_MORE_PARAMS", NULL, "Missing target"); + return send_reply(sptr, ERR_NEEDMOREPARAMS, "TAGMSG"); + } + + /* Get the client-only tags extracted from the message */ + client_tags = cli_client_tags(sptr); + + /* TAGMSG without client-only tags is meaningless */ + if (!client_tags || !*client_tags) + return 0; + + target = parv[1]; + + /* Check if target is a channel */ + if (IsChannelName(target)) { + chptr = FindChannel(target); + if (!chptr) { + if (CapActive(sptr, CAP_STANDARDREPLIES)) + send_fail(sptr, "TAGMSG", "INVALID_TARGET", target, "No such channel"); + return send_reply(sptr, ERR_NOSUCHCHANNEL, target); + } + + /* Check if user can send to channel */ + if (!client_can_send_to_channel(sptr, chptr, 0)) { + if (CapActive(sptr, CAP_STANDARDREPLIES)) + send_fail(sptr, "TAGMSG", "CANNOT_SEND", chptr->chname, "Cannot send to channel"); + return send_reply(sptr, ERR_CANNOTSENDTOCHAN, chptr->chname); + } + + /* Relay TAGMSG with client-only tags to local channel members */ + sendcmdto_channel_client_tags(sptr, MSG_TAGMSG, chptr, sptr, + SKIP_DEAF | SKIP_BURST, client_tags, + "%H", chptr); + + /* Echo TAGMSG back to sender if they have echo-message capability */ + if (feature_bool(FEAT_CAP_echo_message) && CapActive(sptr, CAP_ECHOMSG)) { + sendcmdto_one_client_tags(sptr, MSG_TAGMSG, sptr, client_tags, + "%H", chptr); + } + + /* Store for chathistory event-playback */ + store_tagmsg_history(sptr, chptr, client_tags); + + /* Propagate to other servers (S2S with tags in P10 message) */ + if (!IsLocalChannel(chptr->chname)) { + sendcmdto_serv_butone(sptr, CMD_TAGMSG, cptr, "@%s %s", + client_tags, chptr->chname); + } + } + else { + /* Target is a user */ + acptr = FindUser(target); + if (!acptr) { + if (CapActive(sptr, CAP_STANDARDREPLIES)) + send_fail(sptr, "TAGMSG", "INVALID_TARGET", target, "No such nick"); + return send_reply(sptr, ERR_NOSUCHNICK, target); + } + + if (MyConnect(acptr)) { + /* Local user - deliver with client-only tags if they support message-tags */ + if (CapActive(acptr, CAP_MSGTAGS)) { + sendcmdto_one_client_tags(sptr, MSG_TAGMSG, acptr, client_tags, + "%C", acptr); + } + /* Note: If client doesn't support message-tags, TAGMSG is silently dropped + * per the IRCv3 spec - there's no message body to send as fallback */ + + /* Echo TAGMSG back to sender if they have echo-message capability + * (but not if they're messaging themselves) */ + if (feature_bool(FEAT_CAP_echo_message) && CapActive(sptr, CAP_ECHOMSG) && sptr != acptr) { + sendcmdto_one_client_tags(sptr, MSG_TAGMSG, sptr, client_tags, + "%C", acptr); + } + } + else { + /* Remote user - forward to their server with tags */ + sendcmdto_one(sptr, CMD_TAGMSG, acptr, "@%s %C", + client_tags, acptr); + + /* Echo TAGMSG back to sender if they have echo-message capability */ + if (feature_bool(FEAT_CAP_echo_message) && CapActive(sptr, CAP_ECHOMSG)) { + sendcmdto_one_client_tags(sptr, MSG_TAGMSG, sptr, client_tags, + "%C", acptr); + } + } + } + + return 0; +} + +/* + * ms_tagmsg - server message handler + * + * parv[0] = sender prefix + * parv[1] = @client-tags or target + * parv[2] = target (if parv[1] is tags) + * + * Handle TAGMSG from other servers (P10: TM token). + * Format: NUMERIC TM @+typing=active #channel + * or: NUMERIC TM #channel (legacy, no tags - ignored) + */ +int ms_tagmsg(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) +{ + struct Channel* chptr; + struct Client* acptr; + char* target; + char* client_tags = NULL; + + assert(0 != cptr); + assert(0 != sptr); + + /* Servers can't send TAGMSG */ + if (IsServer(sptr)) + return protocol_violation(sptr, "Server trying to send TAGMSG"); + + if (parc < 2 || EmptyString(parv[1])) + return 0; + + /* Check if first param is client-only tags (starts with @) */ + if (parv[1][0] == '@') { + client_tags = parv[1] + 1; /* Skip the @ prefix */ + if (parc < 3 || EmptyString(parv[2])) + return 0; + target = parv[2]; + } + else { + /* Legacy format without tags - silently ignore */ + return 0; + } + + /* TAGMSG without client-only tags is meaningless */ + if (!client_tags || !*client_tags) + return 0; + + /* Check if target is a channel */ + if (IsChannelName(target)) { + chptr = FindChannel(target); + if (!chptr) + return 0; + + /* Relay to local channel members with message-tags capability */ + sendcmdto_channel_client_tags(sptr, MSG_TAGMSG, chptr, cptr, + SKIP_DEAF | SKIP_BURST, client_tags, + "%H", chptr); + + /* Propagate to other servers */ + sendcmdto_serv_butone(sptr, CMD_TAGMSG, cptr, "@%s %s", + client_tags, target); + } + else { + /* Target is a user */ + acptr = findNUser(target); + if (!acptr) + acptr = FindUser(target); + if (!acptr) + return 0; + + if (MyConnect(acptr)) { + /* Local user - deliver with client-only tags if they support message-tags */ + if (CapActive(acptr, CAP_MSGTAGS)) { + sendcmdto_one_client_tags(sptr, MSG_TAGMSG, acptr, client_tags, + "%C", acptr); + } + /* Note: If client doesn't support message-tags, TAGMSG is silently dropped */ + } + else { + /* Remote user - forward to their server with tags */ + sendcmdto_one(sptr, CMD_TAGMSG, acptr, "@%s %C", + client_tags, acptr); + } + } + + return 0; +} diff --git a/ircd/m_topic.c b/ircd/m_topic.c index 2ecaa90c..a47db17d 100644 --- a/ircd/m_topic.c +++ b/ircd/m_topic.c @@ -38,9 +38,72 @@ #include "numeric.h" #include "numnicks.h" #include "send.h" +#include "history.h" /* #include -- Now using assert in ircd_log.h */ #include /* for atoi() */ +#include +#include + +#ifdef USE_LMDB +/** Counter for generating unique message IDs for TOPIC event history storage */ +static unsigned long topic_history_msgid_counter = 0; + +/** Store a TOPIC event in the history database. + * @param[in] sptr Client that set the topic. + * @param[in] chptr Channel where topic was set. + * @param[in] topic The new topic text. + */ +static void store_topic_event(struct Client *sptr, struct Channel *chptr, + const char *topic) +{ + struct timeval tv; + char timestamp[32]; + char msgid[64]; + char sender[HISTORY_SENDER_LEN]; + const char *account; + + if (!history_is_available()) + return; + + /* Check if chathistory feature is enabled */ + if (!feature_bool(FEAT_CAP_draft_chathistory)) + return; + + /* Only store for local users to avoid duplicates */ + if (!MyUser(sptr)) + return; + + /* Generate Unix timestamp for storage */ + gettimeofday(&tv, NULL); + ircd_snprintf(0, timestamp, sizeof(timestamp), "%lu.%03lu", + (unsigned long)tv.tv_sec, + (unsigned long)(tv.tv_usec / 1000)); + + /* Generate unique msgid */ + ircd_snprintf(0, msgid, sizeof(msgid), "%s-%lu-%lu", + cli_yxx(&me), + (unsigned long)cli_firsttime(&me), + ++topic_history_msgid_counter); + + /* Build sender string: nick!user@host */ + if (cli_user(sptr)) + ircd_snprintf(0, sender, sizeof(sender), "%s!%s@%s", + cli_name(sptr), + cli_user(sptr)->username, + cli_user(sptr)->host); + else + ircd_strncpy(sender, cli_name(sptr), sizeof(sender) - 1); + + /* Get account name if logged in */ + account = (cli_user(sptr) && cli_user(sptr)->account[0]) + ? cli_user(sptr)->account : NULL; + + /* Store in database */ + history_store_message(msgid, timestamp, chptr->chname, sender, + account, HISTORY_TOPIC, topic ? topic : ""); +} +#endif /* USE_LMDB */ /** Set a channel topic or report an error. * @param[in] sptr Original topic setter. @@ -110,6 +173,11 @@ static void do_settopic(struct Client *sptr, struct Client *cptr, sendcmdto_channel_butserv_butone(from, CMD_TOPIC, chptr, NULL, 0, (setter ? "%H :%s (%s)" : "%H :%s%s"), chptr, chptr->topic, (setter ? nick : "")); + +#ifdef USE_LMDB + /* Store TOPIC event in history */ + store_topic_event(sptr, chptr, chptr->topic); +#endif } /* if this is the same topic as before we send it to the person that * set it (so they knew it went through ok), but don't bother sending diff --git a/ircd/m_webirc.c b/ircd/m_webirc.c index 4dae71a9..b7fe81e5 100644 --- a/ircd/m_webirc.c +++ b/ircd/m_webirc.c @@ -85,6 +85,7 @@ #include "hash.h" #include "ircd.h" #include "ircd_alloc.h" +#include "ircd_crypt.h" #include "ircd_features.h" #include "ircd_geoip.h" #include "ircd_log.h" @@ -94,6 +95,7 @@ #include "numnicks.h" #include "send.h" #include "s_auth.h" +#include "s_bsd.h" #include "s_conf.h" #include "s_debug.h" #include "s_misc.h" @@ -101,6 +103,199 @@ /* #include -- Now using assert in ircd_log.h */ +/** Context for async WEBIRC password verification */ +struct webirc_verify_ctx { + int fd; /**< Client fd for lookup */ + char username[USERLEN + 1]; /**< WEBIRC username */ + char hostname[HOSTLEN + 1]; /**< WEBIRC hostname */ + char ipaddr[SOCKIPLEN + 1]; /**< WEBIRC IP address */ + char options[256]; /**< WEBIRC options */ + struct irc_in_addr addr; /**< Parsed IP address */ + struct WebIRCConf *wconf; /**< Matched config block */ +}; + +/** + * Apply WEBIRC changes to a client after password verification succeeds. + * This function contains the common code for applying WEBIRC IP/host changes. + * @param[in] cptr Client to apply changes to + * @param[in] wconf Matched WebIRC configuration + * @param[in] hostname New hostname to set + * @param[in] ipaddr New IP address string + * @param[in] addr Parsed IP address + * @param[in] options WEBIRC options string (may be NULL) + */ +static void apply_webirc_changes(struct Client *cptr, struct WebIRCConf *wconf, + const char *hostname, const char *ipaddr, + const struct irc_in_addr *addr, const char *options) +{ + char *optsdup = NULL; + char *opt = NULL; + char *optval = NULL; + char *p = NULL; + + /* Send connection notice to inform opers of the change of IP and host. */ + if (feature_bool(FEAT_CONNEXIT_NOTICES)) + sendto_opmask_butone_global(&me, SNO_WEBIRC, + "WEBIRC Client host: from %s [%s] to %s [%s]", + cli_sockhost(cptr), cli_sock_ip(cptr), hostname, ipaddr); + + /* Copy old details to cli_connectip and cli_connecthost. */ + if (!IsIPSpoofed(cptr)) { + memcpy(&cli_connectip(cptr), &cli_ip(cptr), sizeof(cli_ip(cptr))); + ircd_strncpy(cli_connecthost(cptr), cli_sockhost(cptr), HOSTLEN + 1); + if (cli_auth(cptr)) + auth_set_originalip(cli_auth(cptr), cli_ip(cptr)); + SetIPSpoofed(cptr); + } + + /* Undo original IP connection in IPcheck. */ + if (IsIPChecked(cptr)) { + IPcheck_connect_fail(cptr, 1); + ClearIPChecked(cptr); + } + + /* Update the IP and charge them as a remote connect. */ + memcpy(&cli_ip(cptr), addr, sizeof(cli_ip(cptr))); + if (!find_except_conf(cptr, EFLAG_IPCHECK)) + IPcheck_remote_connect(cptr, 0); + + /* Change cli_sock_ip() and cli_sockhost() to spoofed host and IP. */ + ircd_strncpy(cli_sock_ip(cptr), ircd_ntoa(&cli_ip(cptr)), SOCKIPLEN + 1); + ircd_strncpy(cli_sockhost(cptr), hostname, HOSTLEN + 1); + + /* Update host names if already set. */ + if (cli_user(cptr)) { + if (!IsHiddenHost(cptr)) + ircd_strncpy(cli_user(cptr)->host, hostname, HOSTLEN + 1); + ircd_strncpy(cli_user(cptr)->realhost, hostname, HOSTLEN + 1); + } + + /* Set client's GeoIP data */ + geoip_apply(cptr); + + /* From this point the user is a WEBIRC user. */ + SetWebIRC(cptr); + + if (FlagHas(&wconf->flags, WFLAG_NOIDENT)) + ClrFlag(cptr, FLAG_GOTID); + + if (FlagHas(&wconf->flags, WFLAG_USERIDENT)) + SetWebIRCUserIdent(cptr); + + if (FlagHas(&wconf->flags, WFLAG_STRIPSSLFP)) + ircd_strncpy(cli_sslclifp(cptr), "", BUFSIZE + 1); + + if (FlagHas(&wconf->flags, WFLAG_USEOPTIONS)) { + /* Remove user mode +z and only add it if "secure" option is supplied. */ + ClearSSL(cptr); + + if (options != NULL && options[0] != '\0') { + DupString(optsdup, options); + for (opt = ircd_strtok(&p, optsdup, " "); opt; + opt = ircd_strtok(&p, 0, " ")) { + optval = strchr(opt, '='); + if (optval != NULL) + *optval++ = '\0'; + else + optval = ""; + Debug((DEBUG_DEBUG, "WEBIRC: Found option '%s' with value '%s'", opt, optval)); + + /* handle "secure" option */ + if (!ircd_strcmp(opt, "secure")) + SetSSL(cptr); + /* handle "local-port" and "remote-port" options */ + else if (!ircd_strcmp(opt, "local-port") || !ircd_strcmp(opt, "remote-port")) + Debug((DEBUG_DEBUG, "WEBIRC: Ignoring option '%s' as we don't use it", opt)); + /* handle "afternet.org/account" option */ + else if (!ircd_strcmp(opt, "afternet.org/account")) { + if (FlagHas(&wconf->flags, WFLAG_TRUSTACCOUNT) && cli_user(cptr)) { + SetAccount(cptr); + ircd_strncpy(cli_user(cptr)->account, optval, ACCOUNTLEN + 1); + + if ((feature_int(FEAT_HOST_HIDING_STYLE) == 1) || + (feature_int(FEAT_HOST_HIDING_STYLE) == 3)) { + SetHiddenHost(cptr); + } + } else + Debug((DEBUG_DEBUG, "WEBIRC: Ignoring untrusted %s value '%s'", opt, optval)); + } + /* Log unrecognized options */ + else + Debug((DEBUG_DEBUG, "WEBIRC: Unrecognized option '%s' supplied by client", opt)); + } + MyFree(optsdup); + } + } + + if (!EmptyString(wconf->description)) { + ircd_strncpy(cli_webirc(cptr), wconf->description, BUFSIZE + 1); + } + + /* Set users ident to WebIRC block specified ident. */ + if (!EmptyString(wconf->ident)) { + ircd_strncpy(cli_username(cptr), wconf->ident, USERLEN + 1); + SetGotId(cptr); + } +} + +/** + * Callback invoked when async WEBIRC password verification completes. + * Called in main thread context via thread_pool_poll(). + */ +static void webirc_password_verified(int result, void *arg) +{ + struct webirc_verify_ctx *ctx = arg; + struct Client *cptr; + + /* Look up client by fd */ + if (ctx->fd < 0 || ctx->fd >= MAXCONNECTIONS) { + MyFree(ctx); + return; + } + + cptr = LocalClientArray[ctx->fd]; + + /* Verify client still exists and is pending verification */ + if (!cptr || IsDead(cptr) || !IsWebIRCPending(cptr)) { + Debug((DEBUG_DEBUG, "webirc_password_verified: client gone or not pending " + "(fd %d)", ctx->fd)); + MyFree(ctx); + return; + } + + /* Clear pending flag */ + ClearWebIRCPending(cptr); + + if (result == CRYPT_VERIFY_MATCH) { + /* Password matched - apply WEBIRC changes */ + Debug((DEBUG_INFO, "webirc_password_verified: WEBIRC success for %s [%s]", + cli_sockhost(cptr), cli_sock_ip(cptr))); + + apply_webirc_changes(cptr, ctx->wconf, ctx->hostname, ctx->ipaddr, + &ctx->addr, ctx->options[0] ? ctx->options : NULL); + + /* Forward to IAuth if configured */ + if (cli_auth(cptr)) { + if (FlagHas(&ctx->wconf->flags, WFLAG_USEOPTIONS)) + auth_set_webirc_trusted(cli_auth(cptr), "", ctx->username, ctx->hostname, + ctx->ipaddr, ctx->options[0] ? ctx->options : NULL); + else + auth_set_webirc_trusted(cli_auth(cptr), "", ctx->username, ctx->hostname, + ctx->ipaddr, NULL); + } + } else { + /* Password didn't match */ + sendto_opmask_butone_global(&me, SNO_WEBIRC, + "WEBIRC Attempt with invalid password from %s [%s]", + cli_sockhost(cptr), cli_sock_ip(cptr)); + exit_client(cptr, cptr, &me, "WEBIRC Password invalid for your host"); + Debug((DEBUG_INFO, "webirc_password_verified: WEBIRC failed for %s [%s]", + cli_sockhost(cptr), cli_sock_ip(cptr))); + } + + MyFree(ctx); +} + /* * m_webirc * @@ -118,13 +313,9 @@ int m_webirc(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) char* ipaddr = NULL; char* password = NULL; char* options = NULL; - char* optsdup = NULL; - char* opt = NULL; - char* optval = NULL; - char *p = NULL; int res = 0; int ares = 0; - struct WebIRCConf *wline; + struct WebIRCConf *wconf; if (IsServerPort(cptr)) return exit_client(cptr, sptr, &me, "Use a different port"); @@ -135,6 +326,10 @@ int m_webirc(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) if (IsWebIRC(cptr)) return 0; + /* Already pending async verification? */ + if (IsWebIRCPending(cptr)) + return 0; + /* These shouldn't be empty, but just in case... */ if (!EmptyString(parv[1])) password = parv[1]; @@ -155,34 +350,7 @@ int m_webirc(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) return exit_client(cptr, sptr, &me, "WEBIRC parameters supplied are invalid"); } - wline = find_webirc_conf(cptr, password, &res); - - ares = -1; - if (res && cli_auth(cptr)) - ares = auth_set_webirc(cli_auth(cptr), password, username, hostname, ipaddr, options); - - if (!ares) - return 0; - else - { - switch (res) - { - case 2: - sendto_opmask_butone_global(&me, SNO_WEBIRC, - "WEBIRC Attempt unauthorized from %s [%s]", - cli_sockhost(sptr), cli_sock_ip(sptr)); - return exit_client(cptr, sptr, &me, "WEBIRC Not authorized from your host"); - break; - case 1: - sendto_opmask_butone_global(&me, SNO_WEBIRC, - "WEBIRC Attempt with invalid password from %s [%s]", - cli_sockhost(sptr), cli_sock_ip(sptr)); - return exit_client(cptr, sptr, &me, "WEBIRC Password invalid for your host"); - break; - } - } - - /* Check supplied IP address is valid */ + /* Check supplied IP address is valid (do this early before async) */ if (!ipmask_parse(ipaddr, &addr, NULL)) { sendto_opmask_butone_global(&me, SNO_WEBIRC, "WEBIRC Attempt with invalid IP address from %s [%s]", @@ -190,7 +358,7 @@ int m_webirc(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) return exit_client(cptr, sptr, &me, "WEBIRC Invalid IP address"); } - /* Check supplied host name is valid */ + /* Check supplied host name is valid (do this early before async) */ if (!valid_hostname(hostname)) { sendto_opmask_butone_global(&me, SNO_WEBIRC, "WEBIRC Attempt with invalid host name from %s [%s]", @@ -198,116 +366,101 @@ int m_webirc(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) return exit_client(cptr, sptr, &me, "WEBIRC Invalid host name"); } - /* Send connection notice to inform opers of the change of IP and host. */ - if (feature_bool(FEAT_CONNEXIT_NOTICES)) + /* Find matching WebIRC block by host (without password check) */ + wconf = find_webirc_conf_by_host(cptr); + if (!wconf) { + /* No matching host - check if IAuth can handle it */ + if (cli_auth(cptr)) { + ares = auth_set_webirc(cli_auth(cptr), password, username, hostname, ipaddr, options); + if (!ares) + return 0; /* IAuth is handling it */ + } sendto_opmask_butone_global(&me, SNO_WEBIRC, - "WEBIRC Client host: from %s [%s] to %s [%s]", - cli_sockhost(sptr), cli_sock_ip(sptr), hostname, ipaddr); - - /* Copy old details to cli_connectip and cli_connecthost. */ - if (!IsIPSpoofed(sptr)) { - memcpy(&cli_connectip(sptr), &cli_ip(sptr), sizeof(cli_ip(sptr))); - ircd_strncpy(cli_connecthost(sptr), cli_sockhost(sptr), HOSTLEN + 1); - if (cli_auth(sptr)) - auth_set_originalip(cli_auth(sptr), cli_ip(sptr)); - SetIPSpoofed(sptr); + "WEBIRC Attempt unauthorized from %s [%s]", + cli_sockhost(sptr), cli_sock_ip(sptr)); + return exit_client(cptr, sptr, &me, "WEBIRC Not authorized from your host"); } - /* Undo original IP connection in IPcheck. */ - if (IsIPChecked(sptr)) { - IPcheck_connect_fail(sptr, 1); - ClearIPChecked(sptr); + /* If IAuth handles WEBIRC, let it (it might use different password database) */ + if (cli_auth(cptr)) { + ares = auth_set_webirc(cli_auth(cptr), password, username, hostname, ipaddr, options); + if (!ares) + return 0; /* IAuth is handling it */ } - /* Update the IP and charge them as a remote connect. */ - memcpy(&cli_ip(sptr), &addr, sizeof(cli_ip(sptr))); - if (!find_except_conf(sptr, EFLAG_IPCHECK)) - IPcheck_remote_connect(sptr, 0); - - /* Change cli_sock_ip() and cli_sockhost() to spoofed host and IP. */ - ircd_strncpy(cli_sock_ip(sptr), ircd_ntoa(&cli_ip(sptr)), SOCKIPLEN + 1); - ircd_strncpy(cli_sockhost(sptr), hostname, HOSTLEN + 1); - - /* Update host names if already set. */ - if (cli_user(sptr)) { - if (!IsHiddenHost(sptr)) - ircd_strncpy(cli_user(sptr)->host, hostname, HOSTLEN + 1); - ircd_strncpy(cli_user(sptr)->realhost, hostname, HOSTLEN + 1); + /* Check if password is required */ + if (EmptyString(wconf->passwd)) { + /* No password required - apply WEBIRC changes immediately */ + apply_webirc_changes(cptr, wconf, hostname, ipaddr, &addr, options); + + /* Forward to IAuth if configured */ + if (cli_auth(cptr)) { + if (FlagHas(&wconf->flags, WFLAG_USEOPTIONS)) + auth_set_webirc_trusted(cli_auth(cptr), password, username, hostname, ipaddr, options); + else + auth_set_webirc_trusted(cli_auth(cptr), password, username, hostname, ipaddr, NULL); + } + return 0; } - /* Set client's GeoIP data */ - geoip_apply(cptr); - - /* From this point the user is a WEBIRC user. */ - SetWebIRC(cptr); - - if (FlagHas(&wline->flags, WFLAG_NOIDENT)) - ClrFlag(sptr, FLAG_GOTID); - - if (FlagHas(&wline->flags, WFLAG_USERIDENT)) - SetWebIRCUserIdent(cptr); - - if (FlagHas(&wline->flags, WFLAG_STRIPSSLFP)) - ircd_strncpy(cli_sslclifp(cptr), "", BUFSIZE + 1); - - if (FlagHas(&wline->flags, WFLAG_USEOPTIONS)) { - /* Remove user mode +z and only add it if "secure" option is supplied. */ - ClearSSL(sptr); + /* Password required - try async verification if available */ + if (ircd_crypt_async_available()) { + struct webirc_verify_ctx *ctx; + + ctx = (struct webirc_verify_ctx *)MyMalloc(sizeof(struct webirc_verify_ctx)); + ctx->fd = cli_fd(cptr); + ctx->wconf = wconf; + memcpy(&ctx->addr, &addr, sizeof(ctx->addr)); + ircd_strncpy(ctx->username, username, USERLEN); + ircd_strncpy(ctx->hostname, hostname, HOSTLEN); + ircd_strncpy(ctx->ipaddr, ipaddr, SOCKIPLEN); + ircd_strncpy(ctx->options, options ? options : "", sizeof(ctx->options) - 1); + + if (ircd_crypt_verify_async(password, wconf->passwd, + webirc_password_verified, ctx) == 0) { + /* Async verification started */ + SetWebIRCPending(cptr); + Debug((DEBUG_INFO, "m_webirc: started async verification for %s [%s]", + cli_sockhost(cptr), cli_sock_ip(cptr))); + return 0; + } - if (options != NULL) { - DupString(optsdup, options); - for (opt = ircd_strtok(&p, optsdup, " "); opt; - opt = ircd_strtok(&p, 0, " ")) { - optval = strchr(opt, '='); - if (optval != NULL) - *optval++ = '\0'; - else - optval = ""; - Debug((DEBUG_DEBUG, "WEBIRC: Found option '%s' with value '%s'", opt, optval)); + /* Async failed to start, fall back to sync */ + MyFree(ctx); + Debug((DEBUG_DEBUG, "m_webirc: async failed, falling back to sync for %s [%s]", + cli_sockhost(cptr), cli_sock_ip(cptr))); + } - /* handle "secure" option */ - if (!ircd_strcmp(opt, "secure")) - SetSSL(sptr); - /* handle "local-port" and "remote-port" options */ - else if (!ircd_strcmp(opt, "local-port") || !ircd_strcmp(opt, "remote-port")) - Debug((DEBUG_DEBUG, "WEBIRC: Ignoring option '%s' as we don't use it", opt)); - /* handle "afternet.org/account" option */ - else if (!ircd_strcmp(opt, "afternet.org/account")) { - if (FlagHas(&wline->flags, WFLAG_TRUSTACCOUNT)) { - SetAccount(sptr); - ircd_strncpy(cli_user(sptr)->account, optval, ACCOUNTLEN + 1); + /* Synchronous password verification (blocking if bcrypt) */ + res = 0; + wconf = find_webirc_conf(cptr, password, &res); - if ((feature_int(FEAT_HOST_HIDING_STYLE) == 1) || - (feature_int(FEAT_HOST_HIDING_STYLE) == 3)) { - SetHiddenHost(sptr); - } - } else - Debug((DEBUG_DEBUG, "WEBIRC: Ignoring untrusted %s value '%s'", opt, optval)); - } - /* Log unrecognized options */ - else - Debug((DEBUG_DEBUG, "WEBIRC: Unrecognized option '%s' supplied by client", opt)); - } - MyFree(optsdup); - } + if (res == 1) { + /* Password mismatch */ + sendto_opmask_butone_global(&me, SNO_WEBIRC, + "WEBIRC Attempt with invalid password from %s [%s]", + cli_sockhost(sptr), cli_sock_ip(sptr)); + return exit_client(cptr, sptr, &me, "WEBIRC Password invalid for your host"); } - if (!EmptyString(wline->description)) { - ircd_strncpy(cli_webirc(cptr), wline->description, BUFSIZE + 1); + if (res == 2 || !wconf) { + /* No matching config */ + sendto_opmask_butone_global(&me, SNO_WEBIRC, + "WEBIRC Attempt unauthorized from %s [%s]", + cli_sockhost(sptr), cli_sock_ip(sptr)); + return exit_client(cptr, sptr, &me, "WEBIRC Not authorized from your host"); } - /* Set users ident to WebIRC block specified ident. */ - if (!EmptyString(wline->ident)) { - ircd_strncpy(cli_username(cptr), wline->ident, USERLEN + 1); - SetGotId(cptr); - } + /* Password verified - apply changes */ + apply_webirc_changes(cptr, wconf, hostname, ipaddr, &addr, options); - /* Only forward options to iauthd if the authenticated WebIRC block enables options */ - if (FlagHas(&wline->flags, WFLAG_USEOPTIONS)) - auth_set_webirc_trusted(cli_auth(cptr), password, username, hostname, ipaddr, options); - else - auth_set_webirc_trusted(cli_auth(cptr), password, username, hostname, ipaddr, NULL); + /* Forward to IAuth if configured */ + if (cli_auth(cptr)) { + if (FlagHas(&wconf->flags, WFLAG_USEOPTIONS)) + auth_set_webirc_trusted(cli_auth(cptr), password, username, hostname, ipaddr, options); + else + auth_set_webirc_trusted(cli_auth(cptr), password, username, hostname, ipaddr, NULL); + } return 0; } - diff --git a/ircd/m_webpush.c b/ircd/m_webpush.c new file mode 100644 index 00000000..2185ec7b --- /dev/null +++ b/ircd/m_webpush.c @@ -0,0 +1,391 @@ +/* + * IRC - Internet Relay Chat, ircd/m_webpush.c + * Copyright (C) 2024 Nefarious Development Team + * + * 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. + */ +/** @file + * @brief Handler for WEBPUSH command (IRCv3 draft/webpush). + * + * Specification: https://github.com/ircv3/ircv3-specifications/pull/471 + * + * Subcommands: + * REGISTER + * UNREGISTER + * + * This implementation uses X3 services for subscription storage and push + * delivery. The IRCd relays commands to X3 via P10 WP token. + */ +#include "config.h" + +#include "capab.h" +#include "client.h" +#include "hash.h" +#include "ircd.h" +#include "ircd_features.h" +#include "ircd_log.h" +#include "ircd_reply.h" +#include "ircd_snprintf.h" +#include "ircd_string.h" +#include "msg.h" +#include "numeric.h" +#include "numnicks.h" +#include "s_user.h" +#include "send.h" + +#include +#include + +/** Maximum endpoint URL length */ +#define WEBPUSH_MAX_ENDPOINT 512 + +/** Maximum p256dh key length (base64) */ +#define WEBPUSH_MAX_P256DH 128 + +/** Maximum auth secret length (base64) */ +#define WEBPUSH_MAX_AUTH 32 + +/** Send a FAIL response using standard-replies format. + * @param[in] sptr Client to send to. + * @param[in] code Error code. + * @param[in] context Context (subcommand). + * @param[in] message Human-readable message. + */ +static void send_webpush_fail(struct Client *sptr, const char *code, + const char *context, const char *message) +{ + sendrawto_one(sptr, "FAIL WEBPUSH %s %s :%s", + code, context ? context : "*", message); +} + +/** Check if an endpoint URL is valid (HTTPS only, no internal IPs). + * @param[in] endpoint The endpoint URL to validate. + * @return 1 if valid, 0 otherwise. + */ +static int is_valid_endpoint(const char *endpoint) +{ + /* Must start with https:// */ + if (strncmp(endpoint, "https://", 8) != 0) + return 0; + + /* Check length */ + if (strlen(endpoint) > WEBPUSH_MAX_ENDPOINT) + return 0; + + /* Block localhost and private IPs */ + if (strstr(endpoint, "://localhost") || + strstr(endpoint, "://127.") || + strstr(endpoint, "://10.") || + strstr(endpoint, "://192.168.") || + strstr(endpoint, "://172.16.") || + strstr(endpoint, "://172.17.") || + strstr(endpoint, "://172.18.") || + strstr(endpoint, "://172.19.") || + strstr(endpoint, "://172.2") || + strstr(endpoint, "://172.30.") || + strstr(endpoint, "://172.31.") || + strstr(endpoint, "://[::1]") || + strstr(endpoint, "://[fe80:") || + strstr(endpoint, "://[fc") || + strstr(endpoint, "://[fd")) + return 0; + + return 1; +} + +/** Parse keys parameter in format "p256dh=...;auth=..." + * @param[in] keys The keys string to parse. + * @param[out] p256dh Buffer to receive p256dh key. + * @param[in] p256dh_size Size of p256dh buffer. + * @param[out] auth Buffer to receive auth secret. + * @param[in] auth_size Size of auth buffer. + * @return 1 if parsed successfully, 0 otherwise. + */ +static int parse_keys(const char *keys, char *p256dh, size_t p256dh_size, + char *auth, size_t auth_size) +{ + const char *p256dh_start, *auth_start; + const char *p256dh_end, *auth_end; + + /* Find p256dh= */ + p256dh_start = strstr(keys, "p256dh="); + if (!p256dh_start) + return 0; + p256dh_start += 7; /* skip "p256dh=" */ + + /* Find end of p256dh (semicolon or end of string) */ + p256dh_end = strchr(p256dh_start, ';'); + if (!p256dh_end) + p256dh_end = keys + strlen(keys); + + /* Find auth= */ + auth_start = strstr(keys, "auth="); + if (!auth_start) + return 0; + auth_start += 5; /* skip "auth=" */ + + /* Find end of auth (semicolon or end of string) */ + auth_end = strchr(auth_start, ';'); + if (!auth_end) + auth_end = keys + strlen(keys); + + /* Check lengths */ + if ((size_t)(p256dh_end - p256dh_start) >= p256dh_size || + (size_t)(auth_end - auth_start) >= auth_size) + return 0; + + /* Copy values */ + ircd_strncpy(p256dh, p256dh_start, p256dh_end - p256dh_start); + p256dh[p256dh_end - p256dh_start] = '\0'; + + ircd_strncpy(auth, auth_start, auth_end - auth_start); + auth[auth_end - auth_start] = '\0'; + + /* Basic validation - should be non-empty base64 */ + if (!*p256dh || !*auth) + return 0; + + return 1; +} + +/** Handle WEBPUSH REGISTER subcommand. + * @param[in] sptr Source client. + * @param[in] parc Parameter count. + * @param[in] parv Parameters. + * @return 0 on success. + */ +static int webpush_cmd_register(struct Client *sptr, int parc, char *parv[]) +{ + const char *endpoint; + const char *keys; + char p256dh[WEBPUSH_MAX_P256DH]; + char auth[WEBPUSH_MAX_AUTH]; + + /* WEBPUSH REGISTER */ + if (parc < 4) { + send_webpush_fail(sptr, "INVALID_PARAMS", "REGISTER", + "Usage: WEBPUSH REGISTER "); + return 0; + } + + endpoint = parv[2]; + keys = parv[3]; + + /* Must be authenticated */ + if (!IsAccount(sptr)) { + send_webpush_fail(sptr, "ACCOUNT_REQUIRED", "REGISTER", + "You must be logged in to register for push notifications"); + return 0; + } + + /* Validate endpoint */ + if (!is_valid_endpoint(endpoint)) { + send_webpush_fail(sptr, "INVALID_PARAMS", "REGISTER", + "Invalid push endpoint (must be HTTPS, no internal IPs)"); + return 0; + } + + /* Parse keys */ + if (!parse_keys(keys, p256dh, sizeof(p256dh), auth, sizeof(auth))) { + send_webpush_fail(sptr, "INVALID_PARAMS", "REGISTER", + "Invalid keys format (expected p256dh=...;auth=...)"); + return 0; + } + + /* Relay to services via P10 WP token + * Format: WP R + */ + sendcmdto_serv_butone(&me, CMD_WEBPUSH, NULL, "R %C %s %s %s", + sptr, endpoint, p256dh, auth); + + /* Echo success to client per spec */ + sendrawto_one(sptr, "WEBPUSH REGISTER %s", endpoint); + + log_write(LS_SYSTEM, L_INFO, 0, + "WEBPUSH: %s!%s@%s registered endpoint for account %s", + cli_name(sptr), cli_user(sptr)->username, + cli_user(sptr)->host, cli_user(sptr)->account); + + return 0; +} + +/** Handle WEBPUSH UNREGISTER subcommand. + * @param[in] sptr Source client. + * @param[in] parc Parameter count. + * @param[in] parv Parameters. + * @return 0 on success. + */ +static int webpush_cmd_unregister(struct Client *sptr, int parc, char *parv[]) +{ + const char *endpoint; + + /* WEBPUSH UNREGISTER */ + if (parc < 3) { + send_webpush_fail(sptr, "INVALID_PARAMS", "UNREGISTER", + "Usage: WEBPUSH UNREGISTER "); + return 0; + } + + endpoint = parv[2]; + + /* Must be authenticated */ + if (!IsAccount(sptr)) { + send_webpush_fail(sptr, "ACCOUNT_REQUIRED", "UNREGISTER", + "You must be logged in to unregister push notifications"); + return 0; + } + + /* Relay to services via P10 WP token + * Format: WP U + */ + sendcmdto_serv_butone(&me, CMD_WEBPUSH, NULL, "U %C %s", + sptr, endpoint); + + /* Echo success to client per spec (silently succeeds even if not registered) */ + sendrawto_one(sptr, "WEBPUSH UNREGISTER %s", endpoint); + + log_write(LS_SYSTEM, L_INFO, 0, + "WEBPUSH: %s!%s@%s unregistered endpoint for account %s", + cli_name(sptr), cli_user(sptr)->username, + cli_user(sptr)->host, cli_user(sptr)->account); + + return 0; +} + +/** Handle WEBPUSH command from a local client. + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message. + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + * @return 0 on success. + */ +int m_webpush(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + const char *subcmd; + + /* Check if capability is enabled */ + if (!CapActive(sptr, CAP_DRAFT_WEBPUSH)) { + send_webpush_fail(sptr, "INVALID_PARAMS", "*", + "You must enable the draft/webpush capability"); + return 0; + } + + if (parc < 2) { + send_webpush_fail(sptr, "INVALID_PARAMS", "*", + "Usage: WEBPUSH ..."); + return 0; + } + + subcmd = parv[1]; + + if (!ircd_strcmp(subcmd, "REGISTER")) + return webpush_cmd_register(sptr, parc, parv); + else if (!ircd_strcmp(subcmd, "UNREGISTER")) + return webpush_cmd_unregister(sptr, parc, parv); + else { + send_webpush_fail(sptr, "INVALID_PARAMS", subcmd, + "Unknown subcommand (expected REGISTER or UNREGISTER)"); + return 0; + } +} + +/** Handle WEBPUSH (WP) command from a server (P10). + * + * Incoming formats: + * WP V : - VAPID key broadcast + * WP R - Register subscription + * WP U - Unregister subscription + * WP E : - Error from services + * + * This is primarily for receiving responses from X3 services. + * + * @param[in] cptr Client that sent us the message. + * @param[in] sptr Original source of message. + * @param[in] parc Number of arguments. + * @param[in] parv Argument vector. + * @return 0 on success. + */ +int ms_webpush(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) +{ + struct Client *acptr; + const char *subcmd; + + if (parc < 2) + return 0; + + subcmd = parv[1]; + + /* Handle VAPID key broadcast from services: WP V : */ + if (subcmd[0] == 'V') { + const char *vapid_key; + + if (parc < 3) + return 0; + + vapid_key = parv[2]; + set_vapid_pubkey(vapid_key); + + log_write(LS_SYSTEM, L_INFO, 0, "WEBPUSH: VAPID public key set to: %s", + vapid_key); + + /* Propagate to other servers */ + sendcmdto_serv_butone(sptr, CMD_WEBPUSH, cptr, "V :%s", vapid_key); + + /* Update ISUPPORT with new VAPID key */ + add_isupport_s("VAPID", vapid_key); + + return 0; + } + + if (parc < 3) + return 0; + + /* Handle error response from services */ + if (subcmd[0] == 'E') { + const char *code; + const char *message; + + if (parc < 4) + return 0; + + /* Find target client */ + acptr = findNUser(parv[2]); + if (!acptr) + return 0; + + code = parv[3]; + message = (parc > 4) ? parv[4] : "Unknown error"; + + /* Forward error to local client */ + if (MyUser(acptr)) { + send_webpush_fail(acptr, code, "*", message); + } + return 0; + } + + /* Forward to other servers if needed */ + if (subcmd[0] == 'R' || subcmd[0] == 'U') { + /* Propagate to other servers */ + if (subcmd[0] == 'R' && parc >= 6) { + sendcmdto_serv_butone(sptr, CMD_WEBPUSH, cptr, "R %s %s %s %s", + parv[2], parv[3], parv[4], parv[5]); + } else if (subcmd[0] == 'U' && parc >= 4) { + sendcmdto_serv_butone(sptr, CMD_WEBPUSH, cptr, "U %s %s", + parv[2], parv[3]); + } + } + + return 0; +} diff --git a/ircd/metadata.c b/ircd/metadata.c new file mode 100644 index 00000000..21ecd503 --- /dev/null +++ b/ircd/metadata.c @@ -0,0 +1,2016 @@ +/* + * IRC - Internet Relay Chat, ircd/metadata.c + * Copyright (C) 2024 Nefarious Development Team + * + * 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. + */ +/** @file + * @brief Metadata storage implementation (IRCv3 draft/metadata-2). + * + * This module provides storage for user and channel metadata with: + * - In-memory storage for transient (non-account) user metadata + * - LMDB persistence for account-linked user metadata + * - In-memory storage for channel metadata (persists with channel) + * + * Account metadata is persisted using LMDB when USE_LMDB is defined. + * The LMDB environment is shared with the history subsystem. + * + * Key structure for account metadata: "account\0key" + * Key structure for channel metadata: "#channel\0key" + */ +#include "config.h" + +#include "account_conn.h" +#include "channel.h" +#include "client.h" +#include "hash.h" +#include "ircd.h" +#include "ircd_alloc.h" +#include "ircd_defs.h" +#include "ircd_features.h" +#include "ircd_log.h" +#include "ircd_reply.h" +#include "ircd_snprintf.h" +#include "ircd_string.h" +#include "metadata.h" +#include "msg.h" +#include "numeric.h" +#include "s_debug.h" +#include "s_stats.h" +#include "s_user.h" +#include "send.h" +#include "struct.h" + +#include + +#include "ircd_compress.h" + +/** Virtual $presence metadata key */ +#define METADATA_KEY_PRESENCE "$presence" + +/** Virtual $last_present metadata key */ +#define METADATA_KEY_LAST_PRESENT "$last_present" + +/** Static buffer for virtual presence metadata entry */ +static struct MetadataEntry presence_entry; +static char presence_value[64]; + +#ifdef USE_LMDB +#include +#include "history.h" + +/** LMDB environment (shared with history) */ +static MDB_env *metadata_env = NULL; + +/** Metadata database handle */ +static MDB_dbi metadata_dbi; + +/** Flag indicating if LMDB is available */ +static int metadata_lmdb_available = 0; + +/** Maximum metadata database size (100MB) */ +#define METADATA_MAP_SIZE (100UL * 1024 * 1024) + +/** Key separator */ +#define KEY_SEP '\0' + +/** Build a lookup key for LMDB. + * @param[out] key Output buffer. + * @param[in] keysize Size of output buffer. + * @param[in] target Account name or channel name. + * @param[in] metakey Metadata key name. + * @return Length of key, or -1 on error. + */ +static int build_lmdb_key(char *key, int keysize, const char *target, const char *metakey) +{ + int pos = 0; + int len; + + len = strlen(target); + if (pos + len + 1 >= keysize) return -1; + memcpy(key + pos, target, len); + pos += len; + key[pos++] = KEY_SEP; + + len = strlen(metakey); + if (pos + len >= keysize) return -1; + memcpy(key + pos, metakey, len); + pos += len; + + return pos; +} + +/** TTL value prefix marker */ +#define TTL_PREFIX 'T' + +/** Encode a value with TTL timestamp. + * Format: T| + * @param[out] buf Output buffer. + * @param[in] bufsize Size of output buffer. + * @param[in] value Value to encode. + * @param[in] timestamp Unix timestamp when cached. + * @return Length written, or -1 on error. + */ +static int encode_ttl_value(char *buf, size_t bufsize, const char *value, time_t timestamp) +{ + int len; + size_t value_len = strlen(value); + + len = ircd_snprintf(0, buf, bufsize, "%c%lu|", TTL_PREFIX, (unsigned long)timestamp); + if (len < 0 || (size_t)len >= bufsize) + return -1; + + if (len + value_len >= bufsize) + return -1; + + memcpy(buf + len, value, value_len); + return len + value_len; +} + +/** Decode a TTL-encoded value. + * @param[in] data Raw stored data. + * @param[in] data_len Length of raw data. + * @param[out] value Buffer for decoded value. + * @param[in] value_size Size of value buffer. + * @param[out] timestamp_out Pointer to store timestamp (may be NULL). + * @return 0 on success, 1 if not TTL-encoded (legacy), -1 on error. + */ +static int decode_ttl_value(const void *data, size_t data_len, char *value, + size_t value_size, time_t *timestamp_out) +{ + const char *p = (const char *)data; + const char *pipe; + unsigned long ts; + char *endp; + size_t value_len; + + if (data_len == 0 || p[0] != TTL_PREFIX) { + /* Legacy format - no TTL prefix, copy as-is */ + if (data_len >= value_size) + return -1; + memcpy(value, data, data_len); + value[data_len] = '\0'; + if (timestamp_out) + *timestamp_out = 0; /* Unknown timestamp */ + return 1; /* Legacy format */ + } + + /* Find the pipe separator */ + pipe = memchr(p + 1, '|', data_len - 1); + if (!pipe) + return -1; + + /* Parse timestamp */ + ts = strtoul(p + 1, &endp, 10); + if (endp != pipe) + return -1; + + if (timestamp_out) + *timestamp_out = (time_t)ts; + + /* Extract value */ + value_len = data_len - (pipe - p) - 1; + if (value_len >= value_size) + return -1; + + memcpy(value, pipe + 1, value_len); + value[value_len] = '\0'; + + return 0; +} + +/** Check if a cached value has expired. + * @param[in] timestamp When the value was cached. + * @param[in] ttl TTL in seconds (0 = no expiry). + * @return 1 if expired, 0 if still valid. + */ +static int is_value_expired(time_t timestamp, int ttl) +{ + if (ttl <= 0 || timestamp == 0) + return 0; /* No TTL or unknown timestamp - never expires */ + + return (CurrentTime - timestamp) > ttl; +} + +/** Initialize LMDB for metadata storage. + * @param[in] dbpath Path to the database directory. + * @return 0 on success, -1 on error. + */ +int metadata_lmdb_init(const char *dbpath) +{ + MDB_txn *txn; + int rc; + + if (metadata_lmdb_available) + return 0; + + /* Use existing history environment if available */ + if (history_is_available()) { + /* History already initialized LMDB, we need to open our database */ + /* For now, we'll initialize our own environment */ + } + + rc = mdb_env_create(&metadata_env); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "metadata: mdb_env_create failed: %s", + mdb_strerror(rc)); + return -1; + } + + rc = mdb_env_set_maxdbs(metadata_env, 2); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "metadata: mdb_env_set_maxdbs failed: %s", + mdb_strerror(rc)); + mdb_env_close(metadata_env); + metadata_env = NULL; + return -1; + } + + rc = mdb_env_set_mapsize(metadata_env, METADATA_MAP_SIZE); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "metadata: mdb_env_set_mapsize failed: %s", + mdb_strerror(rc)); + mdb_env_close(metadata_env); + metadata_env = NULL; + return -1; + } + + rc = mdb_env_open(metadata_env, dbpath, 0, 0644); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "metadata: mdb_env_open(%s) failed: %s", + dbpath, mdb_strerror(rc)); + mdb_env_close(metadata_env); + metadata_env = NULL; + return -1; + } + + /* Open database in a transaction */ + rc = mdb_txn_begin(metadata_env, NULL, 0, &txn); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "metadata: mdb_txn_begin failed: %s", + mdb_strerror(rc)); + mdb_env_close(metadata_env); + metadata_env = NULL; + return -1; + } + + rc = mdb_dbi_open(txn, "metadata", MDB_CREATE, &metadata_dbi); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "metadata: mdb_dbi_open failed: %s", + mdb_strerror(rc)); + mdb_txn_abort(txn); + mdb_env_close(metadata_env); + metadata_env = NULL; + return -1; + } + + rc = mdb_txn_commit(txn); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "metadata: mdb_txn_commit failed: %s", + mdb_strerror(rc)); + mdb_env_close(metadata_env); + metadata_env = NULL; + return -1; + } + + metadata_lmdb_available = 1; + log_write(LS_SYSTEM, L_INFO, 0, "metadata: LMDB initialized at %s", dbpath); + return 0; +} + +/** Shutdown LMDB metadata storage. */ +void metadata_lmdb_shutdown(void) +{ + if (metadata_env) { + mdb_dbi_close(metadata_env, metadata_dbi); + mdb_env_close(metadata_env); + metadata_env = NULL; + metadata_lmdb_available = 0; + } +} + +/** Check if LMDB metadata storage is available. */ +int metadata_lmdb_is_available(void) +{ + return metadata_lmdb_available; +} + +/** Get account metadata from LMDB. + * @param[in] account Account name. + * @param[in] key Metadata key. + * @param[out] value Buffer for value (at least METADATA_VALUE_LEN). + * @return 0 on success, 1 if not found or expired, -1 on error. + */ +int metadata_account_get(const char *account, const char *key, char *value) +{ + MDB_txn *txn; + MDB_val mkey, mdata; + char keybuf[ACCOUNTLEN + METADATA_KEY_LEN + 2]; + char decoded[METADATA_VALUE_LEN]; + int keylen; + int rc; + time_t timestamp; + int ttl; +#ifdef USE_ZSTD + unsigned char decompressed[METADATA_VALUE_LEN + 64]; + size_t decompressed_len; +#endif + + if (!metadata_lmdb_available || !account || !key || !value) + return -1; + + keylen = build_lmdb_key(keybuf, sizeof(keybuf), account, key); + if (keylen < 0) + return -1; + + rc = mdb_txn_begin(metadata_env, NULL, MDB_RDONLY, &txn); + if (rc != 0) + return -1; + + mkey.mv_data = keybuf; + mkey.mv_size = keylen; + + rc = mdb_get(txn, metadata_dbi, &mkey, &mdata); + mdb_txn_abort(txn); + + if (rc == MDB_NOTFOUND) + return 1; + if (rc != 0) + return -1; + +#ifdef USE_ZSTD + /* Check if data is compressed and decompress if needed */ + if (is_compressed(mdata.mv_data, mdata.mv_size)) { + if (decompress_data(mdata.mv_data, mdata.mv_size, + decompressed, sizeof(decompressed), &decompressed_len) < 0) { + return -1; + } + /* Decode TTL from decompressed data */ + rc = decode_ttl_value(decompressed, decompressed_len, decoded, + sizeof(decoded), ×tamp); + if (rc < 0) + return -1; + + /* Check TTL */ + ttl = feature_int(FEAT_METADATA_CACHE_TTL); + if (is_value_expired(timestamp, ttl)) { + Debug((DEBUG_DEBUG, "metadata: cached value for %s.%s expired", account, key)); + return 1; /* Treat as not found */ + } + + if (strlen(decoded) >= METADATA_VALUE_LEN) + return -1; + strcpy(value, decoded); + return 0; + } +#endif + + /* Decode TTL from raw data */ + rc = decode_ttl_value(mdata.mv_data, mdata.mv_size, decoded, + sizeof(decoded), ×tamp); + if (rc < 0) + return -1; + + /* Check TTL */ + ttl = feature_int(FEAT_METADATA_CACHE_TTL); + if (is_value_expired(timestamp, ttl)) { + Debug((DEBUG_DEBUG, "metadata: cached value for %s.%s expired", account, key)); + return 1; /* Treat as not found */ + } + + if (strlen(decoded) >= METADATA_VALUE_LEN) + return -1; + strcpy(value, decoded); + return 0; +} + +/** Set account metadata in LMDB. + * @param[in] account Account name. + * @param[in] key Metadata key. + * @param[in] value Value to set (NULL to delete). + * @return 0 on success, -1 on error. + */ +int metadata_account_set(const char *account, const char *key, const char *value) +{ + MDB_txn *txn; + MDB_val mkey, mdata; + char keybuf[ACCOUNTLEN + METADATA_KEY_LEN + 2]; + char encoded[METADATA_VALUE_LEN + 32]; /* Extra space for TTL prefix */ + int keylen; + int encoded_len; + int rc; +#ifdef USE_ZSTD + unsigned char compressed[METADATA_VALUE_LEN + 64]; + size_t compressed_len; +#endif + + if (!metadata_lmdb_available || !account || !key) + return -1; + + keylen = build_lmdb_key(keybuf, sizeof(keybuf), account, key); + if (keylen < 0) + return -1; + + rc = mdb_txn_begin(metadata_env, NULL, 0, &txn); + if (rc != 0) + return -1; + + mkey.mv_data = keybuf; + mkey.mv_size = keylen; + + if (value) { + /* Encode value with current timestamp for TTL tracking */ + encoded_len = encode_ttl_value(encoded, sizeof(encoded), value, CurrentTime); + if (encoded_len < 0) { + mdb_txn_abort(txn); + return -1; + } + +#ifdef USE_ZSTD + if (compress_data((const unsigned char *)encoded, encoded_len, + compressed, sizeof(compressed), &compressed_len) >= 0) { + mdata.mv_data = compressed; + mdata.mv_size = compressed_len; + } else { + mdata.mv_data = encoded; + mdata.mv_size = encoded_len; + } +#else + mdata.mv_data = encoded; + mdata.mv_size = encoded_len; +#endif + rc = mdb_put(txn, metadata_dbi, &mkey, &mdata, 0); + } else { + rc = mdb_del(txn, metadata_dbi, &mkey, NULL); + if (rc == MDB_NOTFOUND) + rc = 0; /* Deleting non-existent key is OK */ + } + + if (rc != 0) { + mdb_txn_abort(txn); + return -1; + } + + rc = mdb_txn_commit(txn); + return (rc == 0) ? 0 : -1; +} + +/** Set account metadata in LMDB without compression (raw passthrough). + * Used for compression passthrough when data is already compressed. + * @param[in] account Account name. + * @param[in] key Metadata key. + * @param[in] raw_value Raw (possibly compressed) data. + * @param[in] raw_len Length of raw data. + * @return 0 on success, -1 on error. + */ +int metadata_account_set_raw(const char *account, const char *key, + const unsigned char *raw_value, size_t raw_len) +{ + MDB_txn *txn; + MDB_val mkey, mdata; + char keybuf[ACCOUNTLEN + METADATA_KEY_LEN + 2]; + int keylen; + int rc; + + if (!metadata_lmdb_available || !account || !key || !raw_value || raw_len == 0) + return -1; + + keylen = build_lmdb_key(keybuf, sizeof(keybuf), account, key); + if (keylen < 0) + return -1; + + rc = mdb_txn_begin(metadata_env, NULL, 0, &txn); + if (rc != 0) + return -1; + + mkey.mv_data = keybuf; + mkey.mv_size = keylen; + + /* Store raw data directly without compression */ + mdata.mv_data = (void *)raw_value; + mdata.mv_size = raw_len; + + rc = mdb_put(txn, metadata_dbi, &mkey, &mdata, 0); + if (rc != 0) { + mdb_txn_abort(txn); + return -1; + } + + rc = mdb_txn_commit(txn); + return (rc == 0) ? 0 : -1; +} + +/** List all metadata for an account from LMDB. + * Caller must free the returned list with metadata entries. + * @param[in] account Account name. + * @return Head of metadata list, or NULL if none/error. + */ +struct MetadataEntry *metadata_account_list(const char *account) +{ + MDB_txn *txn; + MDB_cursor *cursor; + MDB_val mkey, mdata; + char prefix[ACCOUNTLEN + 2]; + int prefixlen; + struct MetadataEntry *head = NULL, *tail = NULL, *entry; + int rc; +#ifdef USE_ZSTD + unsigned char decompressed[METADATA_VALUE_LEN]; + size_t decompressed_len; +#endif + + if (!metadata_lmdb_available || !account) + return NULL; + + prefixlen = strlen(account); + if (prefixlen >= ACCOUNTLEN) + return NULL; + memcpy(prefix, account, prefixlen); + prefix[prefixlen++] = KEY_SEP; + + rc = mdb_txn_begin(metadata_env, NULL, MDB_RDONLY, &txn); + if (rc != 0) + return NULL; + + rc = mdb_cursor_open(txn, metadata_dbi, &cursor); + if (rc != 0) { + mdb_txn_abort(txn); + return NULL; + } + + mkey.mv_data = prefix; + mkey.mv_size = prefixlen; + + rc = mdb_cursor_get(cursor, &mkey, &mdata, MDB_SET_RANGE); + while (rc == 0) { + /* Check if key still has our prefix */ + if (mkey.mv_size < prefixlen || + memcmp(mkey.mv_data, prefix, prefixlen) != 0) + break; + + /* Extract the metadata key (after prefix) */ + entry = (struct MetadataEntry *)MyMalloc(sizeof(struct MetadataEntry)); + if (!entry) + break; + + if (mkey.mv_size - prefixlen >= METADATA_KEY_LEN) { + MyFree(entry); + break; + } + memcpy(entry->key, (char *)mkey.mv_data + prefixlen, mkey.mv_size - prefixlen); + entry->key[mkey.mv_size - prefixlen] = '\0'; + +#ifdef USE_ZSTD + /* Check if data is compressed and decompress if needed */ + if (is_compressed(mdata.mv_data, mdata.mv_size)) { + if (decompress_data(mdata.mv_data, mdata.mv_size, + decompressed, sizeof(decompressed), &decompressed_len) < 0) { + MyFree(entry); + rc = mdb_cursor_get(cursor, &mkey, &mdata, MDB_NEXT); + continue; + } + entry->value = (char *)MyMalloc(decompressed_len + 1); + if (!entry->value) { + MyFree(entry); + break; + } + memcpy(entry->value, decompressed, decompressed_len); + entry->value[decompressed_len] = '\0'; + } else +#endif + { + entry->value = (char *)MyMalloc(mdata.mv_size + 1); + if (!entry->value) { + MyFree(entry); + break; + } + memcpy(entry->value, mdata.mv_data, mdata.mv_size); + entry->value[mdata.mv_size] = '\0'; + } + + entry->visibility = METADATA_VIS_PUBLIC; + entry->next = NULL; + + if (tail) + tail->next = entry; + else + head = entry; + tail = entry; + + rc = mdb_cursor_get(cursor, &mkey, &mdata, MDB_NEXT); + } + + mdb_cursor_close(cursor); + mdb_txn_abort(txn); + + return head; +} + +/** Clear all metadata for an account in LMDB. + * @param[in] account Account name. + * @return 0 on success, -1 on error. + */ +int metadata_account_clear(const char *account) +{ + MDB_txn *txn; + MDB_cursor *cursor; + MDB_val mkey, mdata; + char prefix[ACCOUNTLEN + 2]; + int prefixlen; + int rc; + + if (!metadata_lmdb_available || !account) + return -1; + + prefixlen = strlen(account); + if (prefixlen >= ACCOUNTLEN) + return -1; + memcpy(prefix, account, prefixlen); + prefix[prefixlen++] = KEY_SEP; + + rc = mdb_txn_begin(metadata_env, NULL, 0, &txn); + if (rc != 0) + return -1; + + rc = mdb_cursor_open(txn, metadata_dbi, &cursor); + if (rc != 0) { + mdb_txn_abort(txn); + return -1; + } + + mkey.mv_data = prefix; + mkey.mv_size = prefixlen; + + rc = mdb_cursor_get(cursor, &mkey, &mdata, MDB_SET_RANGE); + while (rc == 0) { + if (mkey.mv_size < prefixlen || + memcmp(mkey.mv_data, prefix, prefixlen) != 0) + break; + + mdb_cursor_del(cursor, 0); + rc = mdb_cursor_get(cursor, &mkey, &mdata, MDB_NEXT); + } + + mdb_cursor_close(cursor); + + rc = mdb_txn_commit(txn); + return (rc == 0) ? 0 : -1; +} + +/** Store channel metadata to LMDB (for persistent channels). + * @param[in] channel Channel name. + * @param[in] key Metadata key. + * @param[in] value Value to set (NULL to delete). + * @return 0 on success, -1 on error. + */ +int metadata_channel_persist(const char *channel, const char *key, const char *value) +{ + return metadata_account_set(channel, key, value); +} + +/** Load channel metadata from LMDB. + * @param[in] channel Channel name. + * @return Head of metadata list, or NULL if none/error. + */ +struct MetadataEntry *metadata_channel_load(const char *channel) +{ + return metadata_account_list(channel); +} + +/** Purge expired metadata entries from LMDB. + * Called periodically to enforce METADATA_CACHE_TTL. + * @return Number of entries purged, or -1 on error. + */ +int metadata_account_purge_expired(void) +{ + MDB_txn *txn; + MDB_cursor *cursor; + MDB_val mkey, mdata; + int ttl; + int purged = 0; + int rc; +#ifdef USE_ZSTD + unsigned char decompressed[METADATA_VALUE_LEN + 64]; + size_t decompressed_len; +#endif + char decoded[METADATA_VALUE_LEN]; + time_t timestamp; + + if (!metadata_lmdb_available) + return -1; + + ttl = feature_int(FEAT_METADATA_CACHE_TTL); + if (ttl <= 0) + return 0; /* TTL disabled, nothing to purge */ + + rc = mdb_txn_begin(metadata_env, NULL, 0, &txn); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "metadata: purge mdb_txn_begin failed: %s", + mdb_strerror(rc)); + return -1; + } + + rc = mdb_cursor_open(txn, metadata_dbi, &cursor); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "metadata: purge mdb_cursor_open failed: %s", + mdb_strerror(rc)); + mdb_txn_abort(txn); + return -1; + } + + rc = mdb_cursor_get(cursor, &mkey, &mdata, MDB_FIRST); + while (rc == 0) { + int decode_rc; + int expired = 0; + +#ifdef USE_ZSTD + if (is_compressed(mdata.mv_data, mdata.mv_size)) { + if (decompress_data(mdata.mv_data, mdata.mv_size, + decompressed, sizeof(decompressed), &decompressed_len) >= 0) { + decode_rc = decode_ttl_value(decompressed, decompressed_len, decoded, + sizeof(decoded), ×tamp); + if (decode_rc >= 0 && is_value_expired(timestamp, ttl)) { + expired = 1; + } + } + } else +#endif + { + decode_rc = decode_ttl_value(mdata.mv_data, mdata.mv_size, decoded, + sizeof(decoded), ×tamp); + if (decode_rc >= 0 && is_value_expired(timestamp, ttl)) { + expired = 1; + } + } + + if (expired) { + mdb_cursor_del(cursor, 0); + purged++; + } + + rc = mdb_cursor_get(cursor, &mkey, &mdata, MDB_NEXT); + } + + mdb_cursor_close(cursor); + rc = mdb_txn_commit(txn); + if (rc != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "metadata: purge mdb_txn_commit failed: %s", + mdb_strerror(rc)); + return -1; + } + + if (purged > 0) { + log_write(LS_SYSTEM, L_INFO, 0, "metadata: purged %d expired cache entries", purged); + } + + return purged; +} + +#else /* !USE_LMDB */ + +/* Stub implementations when LMDB is not available */ +int metadata_lmdb_init(const char *dbpath) { return -1; } +void metadata_lmdb_shutdown(void) { } +int metadata_lmdb_is_available(void) { return 0; } +int metadata_account_get(const char *account, const char *key, char *value) { return -1; } +int metadata_account_set(const char *account, const char *key, const char *value) { return -1; } +struct MetadataEntry *metadata_account_list(const char *account) { return NULL; } +int metadata_account_clear(const char *account) { return -1; } +int metadata_account_purge_expired(void) { return -1; } +int metadata_channel_persist(const char *channel, const char *key, const char *value) { return -1; } +struct MetadataEntry *metadata_channel_load(const char *channel) { return NULL; } + +#endif /* USE_LMDB */ + +/** Initialize the metadata subsystem. */ +void metadata_init(void) +{ + /* LMDB init is called separately from ircd.c */ +} + +/** Shutdown the metadata subsystem. */ +void metadata_shutdown(void) +{ +#ifdef USE_LMDB + metadata_lmdb_shutdown(); +#endif +} + +/** Validate a metadata key name. + * Keys must be alphanumeric with hyphens, underscores, dots, colons, forward slashes. + * Cannot start with a digit. + */ +int metadata_valid_key(const char *key) +{ + const char *p; + + if (!key || !*key) + return 0; + + /* Cannot start with a digit */ + if (*key >= '0' && *key <= '9') + return 0; + + /* Check all characters */ + for (p = key; *p; p++) { + if ((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9') || + *p == '-' || *p == '_' || *p == '.' || *p == ':' || *p == '/') + continue; + return 0; + } + + /* Check length */ + if (strlen(key) > METADATA_KEY_LEN) + return 0; + + return 1; +} + +/** Create a new metadata entry. */ +static struct MetadataEntry *create_entry(const char *key, const char *value) +{ + struct MetadataEntry *entry; + + entry = (struct MetadataEntry *)MyMalloc(sizeof(struct MetadataEntry)); + if (!entry) + return NULL; + + ircd_strncpy(entry->key, key, METADATA_KEY_LEN - 1); + entry->key[METADATA_KEY_LEN - 1] = '\0'; + + if (value) { + entry->value = (char *)MyMalloc(strlen(value) + 1); + if (!entry->value) { + MyFree(entry); + return NULL; + } + strcpy(entry->value, value); + } else { + entry->value = NULL; + } + + entry->visibility = METADATA_VIS_PUBLIC; + entry->next = NULL; + + return entry; +} + +/** Free a metadata entry. */ +void metadata_free_entry(struct MetadataEntry *entry) +{ + if (!entry) + return; + + if (entry->value) + MyFree(entry->value); + + MyFree(entry); +} + +/** Free an entire list of metadata entries. */ +static void free_entry_list(struct MetadataEntry *head) +{ + struct MetadataEntry *entry, *next; + + for (entry = head; entry; entry = next) { + next = entry->next; + metadata_free_entry(entry); + } +} + +/** Get metadata for a client. + * First checks in-memory cache, then LMDB for logged-in users. + * @param[in] cptr Client to get metadata from. + * @param[in] key Key name. + * @return Metadata entry or NULL if not found. + */ +struct MetadataEntry *metadata_get_client(struct Client *cptr, const char *key) +{ + struct MetadataEntry *entry; + + if (!cptr || !key) + return NULL; + + /* Handle virtual presence keys for presence aggregation */ + if (feature_bool(FEAT_PRESENCE_AGGREGATION) && IsAccount(cptr)) { + /* Handle $presence key */ + if (ircd_strcmp(key, METADATA_KEY_PRESENCE) == 0) { + struct AccountEntry *acc_entry = account_conn_find(cli_account(cptr)); + if (acc_entry) { + const char *state_str; + switch (acc_entry->effective_state) { + case CONN_PRESENT: + state_str = "present"; + break; + case CONN_AWAY: + if (acc_entry->effective_away_msg[0]) + ircd_snprintf(0, presence_value, sizeof(presence_value), + "away:%s", acc_entry->effective_away_msg); + else + strcpy(presence_value, "away"); + state_str = NULL; + break; + case CONN_AWAY_STAR: + state_str = "away-star"; + break; + default: + state_str = "unknown"; + break; + } + if (state_str) + strcpy(presence_value, state_str); + + memset(&presence_entry, 0, sizeof(presence_entry)); + ircd_strncpy(presence_entry.key, METADATA_KEY_PRESENCE, METADATA_KEY_LEN); + presence_entry.value = presence_value; + presence_entry.visibility = METADATA_VIS_PUBLIC; + presence_entry.next = NULL; + return &presence_entry; + } + } + + /* Handle $last_present key */ + if (ircd_strcmp(key, METADATA_KEY_LAST_PRESENT) == 0) { + time_t last = account_conn_last_present(cli_account(cptr)); + if (last > 0) { + ircd_snprintf(0, presence_value, sizeof(presence_value), "%lu", + (unsigned long)last); + memset(&presence_entry, 0, sizeof(presence_entry)); + ircd_strncpy(presence_entry.key, METADATA_KEY_LAST_PRESENT, METADATA_KEY_LEN); + presence_entry.value = presence_value; + presence_entry.visibility = METADATA_VIS_PUBLIC; + presence_entry.next = NULL; + return &presence_entry; + } + } + } + + /* Check in-memory cache first */ + for (entry = cli_metadata(cptr); entry; entry = entry->next) { + if (ircd_strcmp(entry->key, key) == 0) + return entry; + } + + return NULL; +} + +/** Set metadata for a client. + * For logged-in users, also persists to LMDB. + * @param[in] cptr Client to set metadata on. + * @param[in] key Key name. + * @param[in] value Value to set (NULL to delete). + * @param[in] visibility Visibility level (METADATA_VIS_PUBLIC or METADATA_VIS_PRIVATE). + * @return 0 on success, -1 on error. + */ +int metadata_set_client(struct Client *cptr, const char *key, const char *value, int visibility) +{ + struct MetadataEntry *entry, *prev = NULL; + const char *account = NULL; + + if (!cptr || !key) + return -1; + + /* Check if user is logged in */ + if (cli_user(cptr) && cli_user(cptr)->account[0]) + account = cli_user(cptr)->account; + + /* Find existing entry in memory */ + for (entry = cli_metadata(cptr); entry; prev = entry, entry = entry->next) { + if (ircd_strcmp(entry->key, key) == 0) + break; + } + + if (value) { + /* Set or update */ + if (entry) { + /* Update existing */ + if (entry->value) + MyFree(entry->value); + entry->value = (char *)MyMalloc(strlen(value) + 1); + if (!entry->value) + return -1; + strcpy(entry->value, value); + entry->visibility = visibility; + } else { + /* Create new */ + entry = create_entry(key, value); + if (!entry) + return -1; + entry->visibility = visibility; + entry->next = cli_metadata(cptr); + cli_metadata(cptr) = entry; + } + + /* Persist to LMDB for logged-in users */ + if (account && metadata_lmdb_is_available()) { + metadata_account_set(account, key, value); + } + } else { + /* Delete */ + if (entry) { + if (prev) + prev->next = entry->next; + else + cli_metadata(cptr) = entry->next; + metadata_free_entry(entry); + } + + /* Delete from LMDB for logged-in users */ + if (account && metadata_lmdb_is_available()) { + metadata_account_set(account, key, NULL); + } + } + + return 0; +} + +/** List all metadata for a client. + * @param[in] cptr Client to list metadata for. + * @return Head of metadata list (read-only). + */ +struct MetadataEntry *metadata_list_client(struct Client *cptr) +{ + if (!cptr) + return NULL; + return cli_metadata(cptr); +} + +/** Clear all metadata for a client. + * @param[in] cptr Client to clear. + */ +void metadata_clear_client(struct Client *cptr) +{ + const char *account = NULL; + + if (!cptr) + return; + + /* Check if user is logged in */ + if (cli_user(cptr) && cli_user(cptr)->account[0]) + account = cli_user(cptr)->account; + + free_entry_list(cli_metadata(cptr)); + cli_metadata(cptr) = NULL; + + /* Clear from LMDB for logged-in users */ + if (account && metadata_lmdb_is_available()) { + metadata_account_clear(account); + } +} + +/** Count metadata entries for a client. + * @param[in] cptr Client to count. + * @return Number of metadata entries. + */ +int metadata_count_client(struct Client *cptr) +{ + struct MetadataEntry *entry; + int count = 0; + + if (!cptr) + return 0; + + for (entry = cli_metadata(cptr); entry; entry = entry->next) + count++; + + return count; +} + +/** Load metadata from LMDB for a logged-in user. + * Called when a user logs into an account (via SASL or account-notify). + * @param[in] cptr Client that just logged in. + * @param[in] account Account name. + */ +void metadata_load_account(struct Client *cptr, const char *account) +{ + struct MetadataEntry *list, *entry; + + if (!cptr || !account) { + log_write(LS_DEBUG, L_DEBUG, 0, + "metadata_load_account: Invalid parameters (cptr=%p, account=%s)", + (void *)cptr, account ? account : "(null)"); + return; + } + if (!metadata_lmdb_is_available()) { + log_write(LS_DEBUG, L_DEBUG, 0, + "metadata_load_account: LMDB not available for account '%s' (%C)", + account, cptr); + return; + } + + /* Clear any existing in-memory metadata */ + free_entry_list(cli_metadata(cptr)); + cli_metadata(cptr) = NULL; + + /* Load from LMDB */ + list = metadata_account_list(account); + cli_metadata(cptr) = list; +} + +/** Free all metadata for a client (called on disconnect). + * @param[in] cptr Client being freed. + */ +void metadata_free_client(struct Client *cptr) +{ + /* Note: We don't clear LMDB on disconnect - metadata persists with account */ + free_entry_list(cli_metadata(cptr)); + cli_metadata(cptr) = NULL; + metadata_sub_free(cptr); + metadata_cleanup_client_requests(cptr); +} + +/** Get metadata for a channel. + * @param[in] chptr Channel to get metadata from. + * @param[in] key Key name. + * @return Metadata entry or NULL if not found. + */ +struct MetadataEntry *metadata_get_channel(struct Channel *chptr, const char *key) +{ + struct MetadataEntry *entry; + + if (!chptr || !key) + return NULL; + + for (entry = chptr->metadata; entry; entry = entry->next) { + if (ircd_strcmp(entry->key, key) == 0) + return entry; + } + + return NULL; +} + +/** Set metadata for a channel. + * @param[in] chptr Channel to set metadata on. + * @param[in] key Key name. + * @param[in] value Value to set (NULL to delete). + * @param[in] visibility Visibility level (METADATA_VIS_PUBLIC or METADATA_VIS_PRIVATE). + * @return 0 on success, -1 on error. + */ +int metadata_set_channel(struct Channel *chptr, const char *key, const char *value, int visibility) +{ + struct MetadataEntry *entry, *prev = NULL; + + if (!chptr || !key) + return -1; + + /* Find existing entry */ + for (entry = chptr->metadata; entry; prev = entry, entry = entry->next) { + if (ircd_strcmp(entry->key, key) == 0) + break; + } + + if (value) { + /* Set or update */ + if (entry) { + /* Update existing */ + if (entry->value) + MyFree(entry->value); + entry->value = (char *)MyMalloc(strlen(value) + 1); + if (!entry->value) + return -1; + strcpy(entry->value, value); + entry->visibility = visibility; + } else { + /* Create new */ + entry = create_entry(key, value); + if (!entry) + return -1; + entry->visibility = visibility; + entry->next = chptr->metadata; + chptr->metadata = entry; + } + } else { + /* Delete */ + if (entry) { + if (prev) + prev->next = entry->next; + else + chptr->metadata = entry->next; + metadata_free_entry(entry); + } + } + + return 0; +} + +/** List all metadata for a channel. + * @param[in] chptr Channel to list metadata for. + * @return Head of metadata list (read-only). + */ +struct MetadataEntry *metadata_list_channel(struct Channel *chptr) +{ + if (!chptr) + return NULL; + return chptr->metadata; +} + +/** Clear all metadata for a channel. + * @param[in] chptr Channel to clear. + */ +void metadata_clear_channel(struct Channel *chptr) +{ + if (!chptr) + return; + + free_entry_list(chptr->metadata); + chptr->metadata = NULL; +} + +/** Count metadata entries for a channel. + * @param[in] chptr Channel to count. + * @return Number of metadata entries. + */ +int metadata_count_channel(struct Channel *chptr) +{ + struct MetadataEntry *entry; + int count = 0; + + if (!chptr) + return 0; + + for (entry = chptr->metadata; entry; entry = entry->next) + count++; + + return count; +} + +/** Free all metadata for a channel (called on channel destruction). + * @param[in] chptr Channel being freed. + */ +void metadata_free_channel(struct Channel *chptr) +{ + metadata_clear_channel(chptr); +} + +/* ========== Subscription functions ========== */ + +/** Create a new subscription entry. */ +static struct MetadataSub *create_sub(const char *key) +{ + struct MetadataSub *sub; + + sub = (struct MetadataSub *)MyMalloc(sizeof(struct MetadataSub)); + if (!sub) + return NULL; + + ircd_strncpy(sub->key, key, METADATA_KEY_LEN - 1); + sub->key[METADATA_KEY_LEN - 1] = '\0'; + sub->next = NULL; + + return sub; +} + +/** Add a subscription for a client. + * @param[in] cptr Client subscribing. + * @param[in] key Key to subscribe to. + * @return 0 on success, -1 if limit reached or already subscribed. + */ +int metadata_sub_add(struct Client *cptr, const char *key) +{ + struct MetadataSub *sub; + + if (!cptr || !key) + return -1; + + /* Check if already subscribed */ + for (sub = cli_metadatasub(cptr); sub; sub = sub->next) { + if (ircd_strcmp(sub->key, key) == 0) + return 0; /* Already subscribed, success */ + } + + /* Create new subscription */ + sub = create_sub(key); + if (!sub) + return -1; + + sub->next = cli_metadatasub(cptr); + cli_metadatasub(cptr) = sub; + + return 0; +} + +/** Remove a subscription for a client. + * @param[in] cptr Client unsubscribing. + * @param[in] key Key to unsubscribe from. + * @return 0 on success, -1 if not subscribed. + */ +int metadata_sub_del(struct Client *cptr, const char *key) +{ + struct MetadataSub *sub, *prev = NULL; + + if (!cptr || !key) + return -1; + + for (sub = cli_metadatasub(cptr); sub; prev = sub, sub = sub->next) { + if (ircd_strcmp(sub->key, key) == 0) { + if (prev) + prev->next = sub->next; + else + cli_metadatasub(cptr) = sub->next; + MyFree(sub); + return 0; + } + } + + return -1; /* Not found */ +} + +/** Check if a client is subscribed to a key. + * @param[in] cptr Client to check. + * @param[in] key Key to check. + * @return 1 if subscribed, 0 if not. + */ +int metadata_sub_check(struct Client *cptr, const char *key) +{ + struct MetadataSub *sub; + + if (!cptr || !key) + return 0; + + for (sub = cli_metadatasub(cptr); sub; sub = sub->next) { + if (ircd_strcmp(sub->key, key) == 0) + return 1; + } + + return 0; +} + +/** List subscriptions for a client. + * @param[in] cptr Client to list. + * @return Head of subscription list. + */ +struct MetadataSub *metadata_sub_list(struct Client *cptr) +{ + if (!cptr) + return NULL; + return cli_metadatasub(cptr); +} + +/** Count subscriptions for a client. + * @param[in] cptr Client to count. + * @return Number of subscriptions. + */ +int metadata_sub_count(struct Client *cptr) +{ + struct MetadataSub *sub; + int count = 0; + + if (!cptr) + return 0; + + for (sub = cli_metadatasub(cptr); sub; sub = sub->next) + count++; + + return count; +} + +/** Free all subscriptions for a client. + * @param[in] cptr Client being freed. + */ +void metadata_sub_free(struct Client *cptr) +{ + struct MetadataSub *sub, *next; + + if (!cptr) + return; + + for (sub = cli_metadatasub(cptr); sub; sub = next) { + next = sub->next; + MyFree(sub); + } + + cli_metadatasub(cptr) = NULL; +} + +/* ========== X3 Availability Tracking ========== */ + +/** X3 availability flag */ +static int metadata_x3_available_flag = 0; + +/** Last time X3 sent us a message */ +static time_t metadata_x3_last_seen = 0; + +/** Check if X3 services are available. */ +int metadata_x3_is_available(void) +{ + return metadata_x3_available_flag; +} + +/** Signal that X3 has sent a message (heartbeat). */ +void metadata_x3_heartbeat(void) +{ + int was_available = metadata_x3_available_flag; + metadata_x3_available_flag = 1; + metadata_x3_last_seen = CurrentTime; + + /* If X3 just came back online, replay queued writes */ + if (!was_available) { + log_write(LS_SYSTEM, L_INFO, 0, "metadata: X3 services detected as available"); + metadata_replay_queue(); + } +} + +/** Check X3 availability status based on timeout. */ +void metadata_x3_check(void) +{ + int timeout = feature_int(FEAT_METADATA_X3_TIMEOUT); + + if (timeout <= 0) + return; + + if (CurrentTime - metadata_x3_last_seen > timeout) { + if (metadata_x3_available_flag) { + metadata_x3_available_flag = 0; + log_write(LS_SYSTEM, L_WARNING, 0, + "metadata: X3 services unavailable (no heartbeat for %d seconds), " + "switching to cache-only mode", timeout); + } + } +} + +/** Handle X3 reconnection - replay queued writes. */ +void metadata_x3_reconnected(void) +{ + metadata_x3_heartbeat(); +} + +/** Check if metadata writes can be sent to X3. */ +int metadata_can_write_x3(void) +{ + return metadata_x3_available_flag && metadata_lmdb_is_available(); +} + +/* ========== Write Queue for X3 Unavailability ========== */ + +/** Write queue entry */ +struct MetadataWriteQueue { + char account[ACCOUNTLEN + 1]; + char key[METADATA_KEY_LEN]; + char *value; + int visibility; + time_t timestamp; + struct MetadataWriteQueue *next; +}; + +/** Write queue head and tail */ +static struct MetadataWriteQueue *write_queue_head = NULL; +static struct MetadataWriteQueue *write_queue_tail = NULL; +static int write_queue_count_val = 0; + +/** Queue a metadata write for later replay. */ +int metadata_queue_write(const char *account, const char *key, + const char *value, int visibility) +{ + struct MetadataWriteQueue *entry; + int max_queue = feature_int(FEAT_METADATA_QUEUE_SIZE); + + if (!account || !key) + return -1; + + /* Check if queue is full */ + if (write_queue_count_val >= max_queue) { + log_write(LS_SYSTEM, L_WARNING, 0, + "metadata: write queue full (%d entries), dropping oldest entry", + max_queue); + /* Remove oldest entry */ + if (write_queue_head) { + struct MetadataWriteQueue *old = write_queue_head; + write_queue_head = old->next; + if (!write_queue_head) + write_queue_tail = NULL; + if (old->value) + MyFree(old->value); + MyFree(old); + write_queue_count_val--; + } + } + + /* Create new entry */ + entry = (struct MetadataWriteQueue *)MyMalloc(sizeof(struct MetadataWriteQueue)); + if (!entry) + return -1; + + ircd_strncpy(entry->account, account, ACCOUNTLEN); + ircd_strncpy(entry->key, key, METADATA_KEY_LEN - 1); + entry->key[METADATA_KEY_LEN - 1] = '\0'; + + if (value) { + entry->value = (char *)MyMalloc(strlen(value) + 1); + if (!entry->value) { + MyFree(entry); + return -1; + } + strcpy(entry->value, value); + } else { + entry->value = NULL; + } + + entry->visibility = visibility; + entry->timestamp = CurrentTime; + entry->next = NULL; + + /* Add to queue */ + if (write_queue_tail) + write_queue_tail->next = entry; + else + write_queue_head = entry; + write_queue_tail = entry; + write_queue_count_val++; + + log_write(LS_SYSTEM, L_DEBUG, 0, + "metadata: queued write for %s key %s (queue size: %d)", + account, key, write_queue_count_val); + + return 0; +} + +/** Replay all queued metadata writes to X3. + * Note: This sends P10 MD tokens to X3. We need to include the + * necessary headers for send functions. + */ +void metadata_replay_queue(void) +{ + struct MetadataWriteQueue *entry, *next; + int replayed = 0; + + if (!write_queue_head) { + return; + } + + log_write(LS_SYSTEM, L_INFO, 0, + "metadata: replaying %d queued writes to X3", + write_queue_count_val); + + for (entry = write_queue_head; entry; entry = next) { + next = entry->next; + + /* The actual P10 send is done by the caller who has access to + * the services client. For now, we just update LMDB and clear + * the queue. The MD token propagation happens through normal + * means when X3 syncs on reconnect. + */ + if (metadata_lmdb_is_available()) { + metadata_account_set(entry->account, entry->key, entry->value); + } + + if (entry->value) + MyFree(entry->value); + MyFree(entry); + replayed++; + } + + write_queue_head = write_queue_tail = NULL; + write_queue_count_val = 0; + + log_write(LS_SYSTEM, L_INFO, 0, + "metadata: replayed %d queued writes", replayed); +} + +/** Clear the write queue without replaying. */ +void metadata_clear_queue(void) +{ + struct MetadataWriteQueue *entry, *next; + + for (entry = write_queue_head; entry; entry = next) { + next = entry->next; + if (entry->value) + MyFree(entry->value); + MyFree(entry); + } + + write_queue_head = write_queue_tail = NULL; + write_queue_count_val = 0; +} + +/** Get the number of queued writes. */ +int metadata_queue_count(void) +{ + return write_queue_count_val; +} + +/* ========== Cache-Aware Metadata Operations ========== */ + +/** Get metadata for a client with cache-through behavior. + * Checks in-memory first, then LMDB cache for logged-in users. + * If found in LMDB but not in memory, loads it into memory. + */ +struct MetadataEntry *metadata_get_client_cached(struct Client *cptr, const char *key) +{ + struct MetadataEntry *entry; + const char *account; + char value[METADATA_VALUE_LEN]; + + if (!cptr || !key) + return NULL; + + /* Check if caching is enabled */ + if (!feature_bool(FEAT_METADATA_CACHE_ENABLED)) { + return metadata_get_client(cptr, key); + } + + /* First check in-memory (includes virtual keys like $presence) */ + entry = metadata_get_client(cptr, key); + if (entry) + return entry; + + /* If not logged in, nothing more to check */ + if (!cli_user(cptr) || !cli_user(cptr)->account[0]) + return NULL; + + /* Skip virtual keys - they're handled by metadata_get_client */ + if (key[0] == '$') + return NULL; + + account = cli_user(cptr)->account; + + /* Check LMDB cache */ + if (metadata_lmdb_is_available()) { + if (metadata_account_get(account, key, value) == 0) { + /* Found in LMDB - load into memory */ + if (metadata_set_client(cptr, key, value, METADATA_VIS_PUBLIC) == 0) { + return metadata_get_client(cptr, key); + } + } + } + + return NULL; +} + +/* ========== Netburst Metadata ========== */ + +/** Burst all metadata for a client to a server. + * This is a stub - the actual implementation requires send.h + * and will be called from s_user.c during burst. + */ +void metadata_burst_client(struct Client *sptr, struct Client *cptr) +{ + /* Stub - actual implementation in s_user.c */ + (void)sptr; + (void)cptr; +} + +/** Burst all metadata for a channel to a server. + * This is a stub - the actual implementation requires send.h + * and will be called from channel.c during burst. + */ +void metadata_burst_channel(struct Channel *chptr, struct Client *cptr) +{ + /* Stub - actual implementation in channel.c */ + (void)chptr; + (void)cptr; +} + +/* ========== MDQ Request Tracking ========== */ + +/** Pending MDQ requests list */ +static struct MetadataRequest *mdq_pending_head = NULL; +static int mdq_pending_count = 0; + +/** Find the services server (X3). + * @return Pointer to services server, or NULL if not connected. + */ +static struct Client *find_services_server(void) +{ + struct Client *acptr; + + for (acptr = GlobalClientList; acptr; acptr = cli_next(acptr)) { + if (IsServer(acptr) && IsService(acptr)) + return acptr; + } + + return NULL; +} + +/** Initialize MDQ request tracking. */ +void metadata_request_init(void) +{ + mdq_pending_head = NULL; + mdq_pending_count = 0; +} + +/** Send an MDQ query to services for a target. + * @param[in] sptr Client requesting metadata. + * @param[in] target Target account or channel name. + * @param[in] key Key to query (or "*" for all). + * @return 0 on success, -1 on error. + */ +int metadata_send_query(struct Client *sptr, const char *target, const char *key) +{ + struct Client *services; + struct MetadataRequest *req; + + if (!sptr || !target || !key) + return -1; + + /* Find services server - this is the authoritative check for X3 availability */ + services = find_services_server(); + if (!services) { + log_write(LS_DEBUG, L_DEBUG, 0, + "metadata_send_query: No services server found"); + return -1; + } + + /* Check if we've hit the pending request limit */ + if (mdq_pending_count >= METADATA_MAX_PENDING) { + log_write(LS_SYSTEM, L_WARNING, 0, + "metadata_send_query: Too many pending requests (%d), rejecting", + mdq_pending_count); + return -1; + } + + /* Check if there's already a pending request for this target/key from this client */ + for (req = mdq_pending_head; req; req = req->next) { + if (req->client == sptr && + ircd_strcmp(req->target, target) == 0 && + ircd_strcmp(req->key, key) == 0) { + /* Already pending, don't send duplicate */ + log_write(LS_DEBUG, L_DEBUG, 0, + "metadata_send_query: Duplicate request for %s key %s", target, key); + return 0; + } + } + + /* Create pending request entry */ + req = (struct MetadataRequest *)MyMalloc(sizeof(struct MetadataRequest)); + if (!req) + return -1; + + req->client = sptr; + ircd_strncpy(req->target, target, CHANNELLEN); + ircd_strncpy(req->key, key, METADATA_KEY_LEN - 1); + req->key[METADATA_KEY_LEN - 1] = '\0'; + req->timestamp = CurrentTime; + req->next = mdq_pending_head; + mdq_pending_head = req; + mdq_pending_count++; + + /* Send MDQ to services */ + sendcmdto_one(&me, CMD_METADATAQUERY, services, "%s %s", target, key); + + log_write(LS_DEBUG, L_DEBUG, 0, + "metadata_send_query: Sent MDQ for %s key %s (pending: %d)", + target, key, mdq_pending_count); + + return 0; +} + +/** Check if there are pending MDQ requests for a target/key. + * Called when MD response is received to forward to waiting clients. + * @param[in] target Target that metadata was received for. + * @param[in] key Key that was received. + * @param[in] value Value received. + * @param[in] visibility Visibility level. + */ +void metadata_handle_response(const char *target, const char *key, + const char *value, int visibility) +{ + struct MetadataRequest *req, *prev, *next; + int matched = 0; + + if (!target || !key) + return; + + log_write(LS_DEBUG, L_DEBUG, 0, + "metadata_handle_response: target=%s key=%s vis=%d pending_count=%d", + target, key, visibility, mdq_pending_count); + + prev = NULL; + for (req = mdq_pending_head; req; req = next) { + next = req->next; + + log_write(LS_DEBUG, L_DEBUG, 0, + "metadata_handle_response: checking req target=%s key=%s client=%s", + req->target, req->key, req->client ? cli_name(req->client) : "(null)"); + + /* Check if this request matches the response */ + if (ircd_strcmp(req->target, target) == 0 && + (ircd_strcmp(req->key, "*") == 0 || ircd_strcmp(req->key, key) == 0)) { + + /* Send response to waiting client if still connected */ + log_write(LS_DEBUG, L_DEBUG, 0, + "metadata_handle_response: MATCHED! client=%p dead=%d myuser=%d", + (void *)req->client, req->client ? IsDead(req->client) : -1, + req->client ? MyUser(req->client) : -1); + if (req->client && !IsDead(req->client) && MyUser(req->client)) { + const char *vis_str = (visibility == METADATA_VIS_PRIVATE) ? "private" : "*"; + + /* Handle error response from services (no such target) */ + if (visibility == METADATA_VIS_ERROR) { + send_fail(req->client, "METADATA", "TARGET_INVALID", target, + "No such account or channel"); + matched++; + log_write(LS_DEBUG, L_DEBUG, 0, + "metadata_handle_response: Sent TARGET_INVALID for %s to %s", + target, cli_name(req->client)); + goto remove_request; + } + + /* Check visibility for private metadata */ + if (visibility == METADATA_VIS_PRIVATE) { + int can_view = 0; + if (IsOper(req->client)) + can_view = 1; + else if (IsAccount(req->client) && ircd_strcmp(cli_account(req->client), target) == 0) + can_view = 1; + if (!can_view) { + /* Don't reveal private metadata, treat as not set */ + send_reply(req->client, RPL_KEYNOTSET, target, key); + goto remove_request; + } + } + + if (value && *value) { + send_reply(req->client, RPL_KEYVALUE, target, key, vis_str, value); + } else { + send_reply(req->client, RPL_KEYNOTSET, target, key); + } + + matched++; + log_write(LS_DEBUG, L_DEBUG, 0, + "metadata_handle_response: Forwarded %s.%s to %s", + target, key, cli_name(req->client)); + } + + /* For wildcard requests, keep the request alive for more responses + * but mark timestamp to trigger timeout after a short period */ + if (req->key[0] == '*') { + /* Set a shorter timeout for wildcard collection (5 seconds) */ + if (CurrentTime - req->timestamp < 5) { + prev = req; + continue; + } + } + + remove_request: + /* Remove this request */ + if (prev) + prev->next = next; + else + mdq_pending_head = next; + + MyFree(req); + mdq_pending_count--; + } else { + prev = req; + } + } + + if (matched > 0) { + log_write(LS_DEBUG, L_DEBUG, 0, + "metadata_handle_response: Matched %d requests for %s.%s", + matched, target, key); + } +} + +/** Clean up expired MDQ requests. + * Called periodically from the main loop. + */ +void metadata_expire_requests(void) +{ + struct MetadataRequest *req, *prev, *next; + int expired = 0; + + prev = NULL; + for (req = mdq_pending_head; req; req = next) { + next = req->next; + + if (CurrentTime - req->timestamp > METADATA_REQUEST_TIMEOUT) { + /* Request has timed out - send error to client */ + if (req->client && !IsDead(req->client) && MyUser(req->client)) { + send_reply(req->client, RPL_KEYNOTSET, req->target, req->key); + log_write(LS_DEBUG, L_DEBUG, 0, + "metadata_expire_requests: Timed out request for %s.%s from %s", + req->target, req->key, cli_name(req->client)); + } + + /* Remove this request */ + if (prev) + prev->next = next; + else + mdq_pending_head = next; + + MyFree(req); + mdq_pending_count--; + expired++; + } else { + prev = req; + } + } + + if (expired > 0) { + log_write(LS_DEBUG, L_DEBUG, 0, + "metadata_expire_requests: Expired %d requests (remaining: %d)", + expired, mdq_pending_count); + } +} + +/** Clean up MDQ requests for a disconnecting client. + * @param[in] cptr Client that is disconnecting. + */ +void metadata_cleanup_client_requests(struct Client *cptr) +{ + struct MetadataRequest *req, *prev, *next; + int cleaned = 0; + + if (!cptr) + return; + + prev = NULL; + for (req = mdq_pending_head; req; req = next) { + next = req->next; + + if (req->client == cptr) { + /* Remove this request */ + if (prev) + prev->next = next; + else + mdq_pending_head = next; + + MyFree(req); + mdq_pending_count--; + cleaned++; + } else { + prev = req; + } + } + + if (cleaned > 0) { + log_write(LS_DEBUG, L_DEBUG, 0, + "metadata_cleanup_client_requests: Cleaned %d requests for %s", + cleaned, cli_name(cptr)); + } +} + +void +metadata_report_stats(struct Client *to, const struct StatDesc *sd, char *param) +{ + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "M :METADATA Statistics"); + +#ifdef USE_LMDB + { + MDB_stat stat; + MDB_envinfo info; + MDB_txn *txn; + int rc; + + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "M : LMDB Backend: %s", + metadata_lmdb_available ? "Available" : "Unavailable"); + + if (metadata_lmdb_available && metadata_env) { + /* Get environment info */ + rc = mdb_env_info(metadata_env, &info); + if (rc == 0) { + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "M : Map size: %lu MB", + (unsigned long)(info.me_mapsize / (1024 * 1024))); + } + + /* Get database stats */ + rc = mdb_txn_begin(metadata_env, NULL, MDB_RDONLY, &txn); + if (rc == 0) { + rc = mdb_stat(txn, metadata_dbi, &stat); + if (rc == 0) { + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "M : Account metadata DB: %lu entries", + (unsigned long)stat.ms_entries); + } + mdb_txn_abort(txn); + } + } + } +#else + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "M : LMDB Backend: Not compiled in"); +#endif + + /* X3 availability status */ + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "M : X3 Services: %s", + metadata_x3_is_available() ? "Available" : "Unavailable"); + + /* Write queue status */ + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "M : Write queue: %d pending", + metadata_queue_count()); + + /* Pending MDQ requests */ + send_reply(to, SND_EXPLICIT | RPL_STATSDEBUG, + "M : MDQ requests: %d pending", + mdq_pending_count); +} diff --git a/ircd/ml_storage.c b/ircd/ml_storage.c new file mode 100644 index 00000000..572df35e --- /dev/null +++ b/ircd/ml_storage.c @@ -0,0 +1,377 @@ +/* + * IRC - Internet Relay Chat, ircd/ml_storage.c + * Copyright (C) 2026 Nefarious Development Team + * + * 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. + */ +/** @file + * @brief Multiline message storage implementation. + * + * In-memory hash table storage for truncated multiline messages. + * Allows legacy clients to retrieve full content via /join &ml-. + */ +#include "config.h" + +#include "ml_storage.h" +#include "client.h" +#include "ircd.h" +#include "ircd_alloc.h" +#include "ircd_features.h" +#include "ircd_log.h" +#include "ircd_reply.h" +#include "ircd_string.h" +#include "list.h" +#include "msg.h" +#include "numeric.h" +#include "s_misc.h" +#include "send.h" + +#include +#include + +/** Hash table for stored messages */ +static struct ml_stored_msg *ml_storage_table[ML_STORAGE_HASHSIZE]; + +/** Current count of stored entries */ +static int ml_storage_count = 0; + +/** Total bytes used by stored content */ +static size_t ml_storage_bytes = 0; + +/** Simple hash function for msgid strings */ +static unsigned int ml_hash(const char *msgid) +{ + unsigned int hash = 0; + const char *p; + + for (p = msgid; *p; p++) + hash = hash * 31 + (unsigned char)*p; + + return hash % ML_STORAGE_HASHSIZE; +} + +/** Allocate and initialize a storage entry */ +static struct ml_stored_msg *ml_entry_alloc(const char *msgid, + const char *sender, + const char *target, + const char *lines_data, + int line_count) +{ + struct ml_stored_msg *entry; + + entry = (struct ml_stored_msg *)MyMalloc(sizeof(struct ml_stored_msg)); + if (!entry) + return NULL; + + ircd_strncpy(entry->msgid, msgid, ML_STORAGE_MSGID_LEN - 1); + entry->msgid[ML_STORAGE_MSGID_LEN - 1] = '\0'; + + ircd_strncpy(entry->sender, sender, NICKLEN); + entry->sender[NICKLEN] = '\0'; + + ircd_strncpy(entry->target, target, CHANNELLEN); + entry->target[CHANNELLEN] = '\0'; + + DupString(entry->lines, lines_data); + entry->line_count = line_count; + entry->stored = CurrentTime; + entry->expires = CurrentTime + feature_int(FEAT_MULTILINE_STORAGE_TTL); + entry->next = NULL; + + return entry; +} + +/** Free a storage entry */ +static void ml_entry_free(struct ml_stored_msg *entry) +{ + if (!entry) + return; + + if (entry->lines) { + ml_storage_bytes -= strlen(entry->lines) + 1; + MyFree(entry->lines); + } + + MyFree(entry); + ml_storage_count--; +} + +void ml_storage_init(void) +{ + int i; + + for (i = 0; i < ML_STORAGE_HASHSIZE; i++) + ml_storage_table[i] = NULL; + + ml_storage_count = 0; + ml_storage_bytes = 0; + + log_write(LS_SYSTEM, L_INFO, 0, "Multiline storage initialized"); +} + +void ml_storage_shutdown(void) +{ + int i; + struct ml_stored_msg *entry, *next; + + for (i = 0; i < ML_STORAGE_HASHSIZE; i++) { + for (entry = ml_storage_table[i]; entry; entry = next) { + next = entry->next; + ml_entry_free(entry); + } + ml_storage_table[i] = NULL; + } + + ml_storage_count = 0; + ml_storage_bytes = 0; + + log_write(LS_SYSTEM, L_INFO, 0, "Multiline storage shutdown"); +} + +int ml_storage_store(const char *msgid, const char *sender, + const char *target, struct SLink *lines, int count) +{ + unsigned int hash; + struct ml_stored_msg *entry; + struct SLink *lp; + char *lines_data; + size_t total_len = 0; + char *p; + int max_entries; + + if (!msgid || !sender || !target || !lines || count <= 0) + return -1; + + /* Check storage limit */ + max_entries = feature_int(FEAT_MULTILINE_STORAGE_MAX); + if (ml_storage_count >= max_entries) { + log_write(LS_SYSTEM, L_WARNING, 0, + "ml_storage: storage full (%d entries), rejecting msgid %s", + ml_storage_count, msgid); + return -1; + } + + /* Calculate total length needed for all lines */ + for (lp = lines; lp; lp = lp->next) { + if (lp->value.cp) { + /* Skip first byte (concat flag), get text */ + const char *text = lp->value.cp + 1; + total_len += strlen(text) + 1; /* +1 for newline/null */ + } + } + + if (total_len == 0) + return -1; + + /* Allocate buffer for all lines */ + lines_data = (char *)MyMalloc(total_len); + if (!lines_data) + return -1; + + /* Concatenate all lines with newlines */ + p = lines_data; + for (lp = lines; lp; lp = lp->next) { + if (lp->value.cp) { + const char *text = lp->value.cp + 1; + size_t len = strlen(text); + memcpy(p, text, len); + p += len; + if (lp->next) + *p++ = '\n'; + } + } + *p = '\0'; + + /* Create entry */ + entry = ml_entry_alloc(msgid, sender, target, lines_data, count); + MyFree(lines_data); + + if (!entry) + return -1; + + /* Add to hash table */ + hash = ml_hash(msgid); + entry->next = ml_storage_table[hash]; + ml_storage_table[hash] = entry; + + ml_storage_count++; + ml_storage_bytes += strlen(entry->lines) + 1; + + log_write(LS_SYSTEM, L_DEBUG, 0, + "ml_storage: stored msgid %s from %s to %s (%d lines, %zu bytes)", + msgid, sender, target, count, strlen(entry->lines)); + + return 0; +} + +struct ml_stored_msg *ml_storage_get(const char *msgid) +{ + unsigned int hash; + struct ml_stored_msg *entry; + + if (!msgid) + return NULL; + + hash = ml_hash(msgid); + + for (entry = ml_storage_table[hash]; entry; entry = entry->next) { + if (ircd_strcmp(entry->msgid, msgid) == 0) { + /* Check expiration */ + if (entry->expires <= CurrentTime) + return NULL; /* Expired, will be cleaned up later */ + return entry; + } + } + + return NULL; +} + +int ml_storage_remove(const char *msgid) +{ + unsigned int hash; + struct ml_stored_msg **pp, *entry; + + if (!msgid) + return 1; + + hash = ml_hash(msgid); + + for (pp = &ml_storage_table[hash]; *pp; pp = &(*pp)->next) { + if (ircd_strcmp((*pp)->msgid, msgid) == 0) { + entry = *pp; + *pp = entry->next; + ml_entry_free(entry); + return 0; + } + } + + return 1; /* Not found */ +} + +int ml_storage_expire(void) +{ + int i; + int expired = 0; + struct ml_stored_msg **pp, *entry; + time_t now = CurrentTime; + + for (i = 0; i < ML_STORAGE_HASHSIZE; i++) { + pp = &ml_storage_table[i]; + while (*pp) { + if ((*pp)->expires <= now) { + entry = *pp; + *pp = entry->next; + log_write(LS_SYSTEM, L_DEBUG, 0, + "ml_storage: expired msgid %s", entry->msgid); + ml_entry_free(entry); + expired++; + } else { + pp = &(*pp)->next; + } + } + } + + if (expired > 0) { + log_write(LS_SYSTEM, L_DEBUG, 0, + "ml_storage: expired %d entries, %d remaining", + expired, ml_storage_count); + } + + return expired; +} + +void ml_storage_stats(int *count, int *max, size_t *bytes) +{ + if (count) + *count = ml_storage_count; + if (max) + *max = feature_int(FEAT_MULTILINE_STORAGE_MAX); + if (bytes) + *bytes = ml_storage_bytes; +} + +int ml_storage_deliver(struct Client *sptr, const char *msgid) +{ + struct ml_stored_msg *msg; + char *line, *end; + int line_num = 0; + + if (!sptr || !msgid) + return 0; + + msg = ml_storage_get(msgid); + + if (!msg) { + sendcmdto_one(&me, CMD_NOTICE, sptr, + "%C :Multiline message %s not found or expired", + sptr, msgid); + return 0; + } + + /* Send header */ + sendcmdto_one(&me, CMD_NOTICE, sptr, + "%C :=== Multiline message from %s to %s ===", + sptr, msg->sender, msg->target); + + /* Send each line - work on a copy since we modify the string */ + line = msg->lines; + while (line && *line) { + end = strchr(line, '\n'); + if (end) { + *end = '\0'; + sendcmdto_one(&me, CMD_NOTICE, sptr, "%C :<%s> %s", + sptr, msg->sender, line); + *end = '\n'; /* Restore for next iteration */ + line = end + 1; + } else { + /* Last line (no trailing newline) */ + sendcmdto_one(&me, CMD_NOTICE, sptr, "%C :<%s> %s", + sptr, msg->sender, line); + break; + } + line_num++; + } + + /* Send footer with metadata */ + sendcmdto_one(&me, CMD_NOTICE, sptr, + "%C :=== End of message (%d lines, stored %s) ===", + sptr, msg->line_count, myctime(msg->stored)); + + return 0; +} + +int ml_storage_is_virtual_channel(const char *name) +{ + if (!name) + return 0; + + /* Check for &ml- prefix */ + if (name[0] == '&' && name[1] == 'm' && name[2] == 'l' && name[3] == '-') + return 1; + + return 0; +} + +void ml_storage_meminfo(struct Client *cptr) +{ + int count, max; + size_t bytes; + + ml_storage_stats(&count, &max, &bytes); + + send_reply(cptr, SND_EXPLICIT | RPL_STATSDEBUG, + ":multiline storage: %d entries (%d max), %zu bytes", + count, max, bytes); +} diff --git a/ircd/os_generic.c b/ircd/os_generic.c index 625d0226..b231b7b8 100644 --- a/ircd/os_generic.c +++ b/ircd/os_generic.c @@ -62,6 +62,7 @@ #include #include #include +#include #include #include #include @@ -350,6 +351,17 @@ int os_set_reuseaddr(int fd) (const char*) &opt, sizeof(opt))); } +/** Disable Nagle's algorithm on a socket for low-latency sends. + * @param[in] fd %Socket file descriptor to manipulate. + * @return Non-zero on success, or zero on failure. + */ +int os_set_tcp_nodelay(int fd) +{ + unsigned int opt = 1; + return (0 == setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, + (const char*) &opt, sizeof(opt))); +} + /** Set a socket's send and receive buffer sizes. * @param[in] fd %Socket file descriptor to manipulate. * @param[in] ssize New send buffer size. diff --git a/ircd/packet.c b/ircd/packet.c index 676da476..3ec02153 100644 --- a/ircd/packet.c +++ b/ircd/packet.c @@ -31,7 +31,9 @@ #include "parse.h" #include "s_bsd.h" #include "s_misc.h" +#include "s_debug.h" #include "send.h" +#include "websocket.h" /* #include -- Now using assert in ircd_log.h */ @@ -122,6 +124,42 @@ int connect_dopacket(struct Client *cptr, const char *buffer, int length) update_bytes_received(cptr, length); + Debug((DEBUG_DEBUG, "connect_dopacket: IsWSNeedHandshake=%d, IsSSL=%d, length=%d", + IsWSNeedHandshake(cptr), IsSSL(cptr), length)); + + /* Handle WebSocket handshake if needed */ + if (IsWSNeedHandshake(cptr)) { + Debug((DEBUG_DEBUG, "Processing WebSocket handshake")); + int result; + /* Accumulate data in client buffer for HTTP request */ + client_buffer = cli_buffer(cptr); + endp = client_buffer + cli_count(cptr); + + /* Copy incoming data to buffer */ + while (length > 0 && (endp - client_buffer) < BUFSIZE - 1) { + *endp++ = *buffer++; + length--; + } + *endp = '\0'; + cli_count(cptr) = endp - client_buffer; + + /* Try to complete handshake */ + result = websocket_handshake(cptr, client_buffer, cli_count(cptr)); + if (result == 0) { + /* Need more data */ + return 1; + } else if (result < 0) { + /* Handshake failed */ + return exit_client(cptr, cptr, &me, "WebSocket handshake failed"); + } + /* Handshake succeeded - clear buffer and continue normally */ + cli_count(cptr) = 0; + /* Process any remaining data after handshake (unlikely) */ + if (length <= 0) + return 1; + buffer = buffer; /* Continue with remaining data */ + } + client_buffer = cli_buffer(cptr); endp = client_buffer + cli_count(cptr); src = buffer; diff --git a/ircd/parse.c b/ircd/parse.c index 8a5c8e4f..a4bd17d6 100644 --- a/ircd/parse.c +++ b/ircd/parse.c @@ -24,6 +24,7 @@ #include "config.h" #include "parse.h" +#include "capab.h" #include "class.h" #include "client.h" #include "channel.h" @@ -313,7 +314,7 @@ struct Message msgtab[] = { TOK_AWAY, 0, MAXPARA, MFLG_SLOW, 0, NULL, /* UNREG, CLIENT, SERVER, OPER, SERVICE */ - { m_unregistered, m_away, ms_away, m_away, m_ignore }, + { mu_away, m_away, ms_away, m_away, m_ignore }, "[:] - Marks yourself as away, or back." }, { @@ -524,6 +525,14 @@ struct Message msgtab[] = { { m_unregistered, m_info, ms_info, mo_info, m_ignore }, "- Returns information about the local server" }, + { + MSG_ISUPPORT, + TOK_ISUPPORT, + 0, MAXPARA, MFLG_SLOW | MFLG_UNREG, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { m_isupport, m_isupport, m_ignore, m_isupport, m_ignore }, + "- Returns ISUPPORT tokens (requires draft/extended-isupport cap)" + }, { MSG_MOTD, TOK_MOTD, @@ -892,6 +901,118 @@ struct Message msgtab[] = { { m_unregistered, m_sethost, m_ignore, mo_sethost, m_ignore }, "" }, + { + MSG_SETNAME, + TOK_SETNAME, + 0, MAXPARA, MFLG_SLOW, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { m_unregistered, m_setname, ms_setname, m_setname, m_ignore }, + "- SETNAME :newrealname - Change your realname (GECOS)" + }, + { + MSG_TAGMSG, + TOK_TAGMSG, + 0, MAXPARA, MFLG_SLOW, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { m_unregistered, m_tagmsg, ms_tagmsg, m_tagmsg, m_ignore }, + "- TAGMSG target - Send message with only tags (no content)" + }, + { + MSG_BATCH_CMD, + TOK_BATCH_CMD, + 0, MAXPARA, MFLG_SLOW, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { m_ignore, m_batch, ms_batch, m_batch, m_ignore }, + "+id type [params] | -id - Start or end a message batch" + }, + { + MSG_MULTILINE, + TOK_MULTILINE, + 0, MAXPARA, 0, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { m_ignore, m_ignore, ms_multiline, m_ignore, m_ignore }, + "S2S multiline batch relay - internal use only" + }, + { + MSG_CHATHISTORY, + TOK_CHATHISTORY, + 0, MAXPARA, MFLG_SLOW, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { m_unregistered, m_chathistory, ms_chathistory, m_chathistory, m_ignore }, + "subcommand target ref [ref] limit - Query message history" + }, + { + MSG_REDACT, + TOK_REDACT, + 0, MAXPARA, MFLG_SLOW, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { m_unregistered, m_redact, ms_redact, m_redact, m_ignore }, + " [:] - Redact a previously sent message" + }, + { + MSG_REGISTER, + TOK_REGISTER, + 0, MAXPARA, MFLG_SLOW, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { m_register, m_register, m_ignore, m_register, m_ignore }, + " - Register an account" + }, + { + MSG_VERIFY, + TOK_VERIFY, + 0, MAXPARA, MFLG_SLOW, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { m_verify, m_verify, m_ignore, m_verify, m_ignore }, + " - Verify account registration" + }, + { + MSG_REGREPLY, + TOK_REGREPLY, + 0, MAXPARA, 0, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { m_ignore, m_ignore, ms_regreply, m_ignore, ms_regreply }, + "" + }, + { + MSG_MARKREAD, + TOK_MARKREAD, + 0, MAXPARA, MFLG_SLOW, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { m_unregistered, m_markread, ms_markread, m_markread, m_ignore }, + " [timestamp=] - Get or set read marker for target" + }, + { + MSG_RENAME, + TOK_RENAME, + 0, MAXPARA, MFLG_SLOW, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { m_unregistered, m_rename, ms_rename, m_rename, m_ignore }, + " [:] - Rename a channel" + }, + { + MSG_METADATA, + TOK_METADATA, + 0, MAXPARA, MFLG_SLOW, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { m_unregistered, m_metadata, ms_metadata, m_metadata, m_ignore }, + " [args] - Manage user/channel metadata" + }, + { + MSG_METADATAQUERY, + TOK_METADATAQUERY, + 0, MAXPARA, 0, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { m_ignore, m_ignore, ms_metadataquery, m_ignore, m_ignore }, + " [key|*] - Query metadata for account/channel (S2S only)" + }, + { + MSG_WEBPUSH, + TOK_WEBPUSH, + 0, MAXPARA, MFLG_SLOW, 0, NULL, + /* UNREG, CLIENT, SERVER, OPER, SERVICE */ + { m_unregistered, m_webpush, ms_webpush, m_webpush, m_ignore }, + "REGISTER|UNREGISTER [keys] - Manage web push subscriptions" + }, { MSG_FINGERPRINT, TOK_FINGERPRINT, @@ -1204,8 +1325,90 @@ parse_client(struct Client *cptr, char *buffer, char *bufend) if (IsDead(cptr)) return 0; + /* Clear any previous label, client-only tags, and batch tags */ + cli_label(cptr)[0] = '\0'; + cli_client_tags(cptr)[0] = '\0'; + cli_msg_batch_tag(cptr)[0] = '\0'; + cli_msg_concat(cptr) = 0; + para[0] = cli_name(from); for (ch = buffer; *ch == ' '; ch++); /* Eat leading spaces */ + + /* Parse message tags if present (IRCv3.2) */ + if (*ch == '@') { + char *tag_end; + char *tag_start = ch + 1; /* Skip the @ */ + + /* Find end of tags (space before command) */ + tag_end = strchr(tag_start, ' '); + if (tag_end) { + char *scan = tag_start; + int client_tags_pos = 0; + + while (scan < tag_end) { + char *tag_name = scan; + char *next_semi = scan; + size_t tag_len; + + /* Find end of this tag (semicolon or end) */ + while (next_semi < tag_end && *next_semi != ';') + next_semi++; + tag_len = next_semi - tag_name; + + /* Check for label= tag */ + if (strncmp(tag_name, "label=", 6) == 0) { + /* Extract label value if client has capability */ + if (CapActive(cptr, CAP_LABELEDRESP) && + feature_bool(FEAT_CAP_labeled_response)) { + char *label_val = tag_name + 6; + size_t label_len = next_semi - label_val; + if (label_len >= sizeof(cli_label(cptr))) + label_len = sizeof(cli_label(cptr)) - 1; + memcpy(cli_label(cptr), label_val, label_len); + cli_label(cptr)[label_len] = '\0'; + } + } + /* Check for batch= tag (draft/multiline) */ + else if (strncmp(tag_name, "batch=", 6) == 0) { + /* Extract batch ID for multiline message handling */ + if (CapActive(cptr, CAP_DRAFT_MULTILINE)) { + char *batch_val = tag_name + 6; + size_t batch_len = next_semi - batch_val; + if (batch_len >= sizeof(cli_msg_batch_tag(cptr))) + batch_len = sizeof(cli_msg_batch_tag(cptr)) - 1; + memcpy(cli_msg_batch_tag(cptr), batch_val, batch_len); + cli_msg_batch_tag(cptr)[batch_len] = '\0'; + } + } + /* Check for draft/multiline-concat tag */ + else if (tag_len == 22 && strncmp(tag_name, "draft/multiline-concat", 22) == 0) { + if (CapActive(cptr, CAP_DRAFT_MULTILINE)) + cli_msg_concat(cptr) = 1; + } + /* Check for client-only tags (prefixed with +) */ + else if (*tag_name == '+') { + /* Copy client-only tag to buffer for TAGMSG relay */ + if (client_tags_pos + tag_len + 2 < sizeof(cli_client_tags(cptr))) { + if (client_tags_pos > 0) + cli_client_tags(cptr)[client_tags_pos++] = ';'; + memcpy(cli_client_tags(cptr) + client_tags_pos, tag_name, tag_len); + client_tags_pos += tag_len; + cli_client_tags(cptr)[client_tags_pos] = '\0'; + } + } + + /* Move to next tag */ + scan = next_semi; + if (*scan == ';') + scan++; + } + /* Advance past tags to the command */ + ch = tag_end; + while (*ch == ' ') + ch++; + } + } + if (*ch == ':') /* Is any client doing this ? */ { for (++ch; *ch && *ch != ' '; ++ch) @@ -1267,8 +1470,26 @@ parse_client(struct Client *cptr, char *buffer, char *bufend) lagmin = 2; if (lagfactor < 0) lagfactor = 120; - if (((mptr->flags & MFLG_SLOW) || !IsAnOper(cptr)) && lagfactor > 0) - cli_since(cptr) += (lagmin + i / lagfactor); + if (((mptr->flags & MFLG_SLOW) || !IsAnOper(cptr)) && lagfactor > 0) { + int lag = lagmin + i / lagfactor; + /* + * If client is in a multiline batch, accumulate lag instead of + * applying immediately. Per IRCv3 multiline spec, servers should + * recognize that batched messages are transmitted simultaneously + * and not count each line separately for flood protection. + * The accumulated lag is applied once when the batch ends. + * + * MULTILINE_MAX_LAG caps accumulated lag to prevent extremely long + * batches from building up massive lag debt. + */ + if (cli_ml_batch_id(cptr)[0]) { + int max_lag = feature_int(FEAT_MULTILINE_MAX_LAG); + cli_ml_lag_accum(cptr) += lag; + if (max_lag > 0 && cli_ml_lag_accum(cptr) > max_lag) + cli_ml_lag_accum(cptr) = max_lag; + } else + cli_since(cptr) += lag; + } /* * Allow only 1 msg per 2 seconds * (on average) to prevent dumping. @@ -1361,6 +1582,66 @@ int parse_server(struct Client *cptr, char *buffer, char *bufend) if (IsDead(cptr)) return 0; + /* + * IRCv3.2 Message Tags: If line starts with @, extract and store tags. + * Format: @tag1=value;tag2;+clienttag=value NUMERIC TOKEN params + * We extract @time and @msgid for S2S relay (Phase 13c). + */ + /* Clear previous S2S tags */ + cli_s2s_time(cptr)[0] = '\0'; + cli_s2s_msgid(cptr)[0] = '\0'; + + if (*ch == '@') { + /* Find the end of tags (first space after @) */ + char *tagend = strchr(ch, ' '); + if (tagend) { + char *tagpos = ch + 1; /* Skip the @ prefix */ + char *semicolon; + + /* Parse individual tags */ + while (tagpos < tagend) { + char *tag_name = tagpos; + int tag_len; + + /* Find the end of this tag (semicolon or end of tags) */ + semicolon = memchr(tagpos, ';', tagend - tagpos); + if (semicolon) + tag_len = semicolon - tagpos; + else + tag_len = tagend - tagpos; + + /* Check for @time tag */ + if (tag_len >= 5 && memcmp(tag_name, "time=", 5) == 0) { + int value_len = tag_len - 5; + if (value_len < (int)sizeof(cli_s2s_time(cptr))) { + memcpy(cli_s2s_time(cptr), tag_name + 5, value_len); + cli_s2s_time(cptr)[value_len] = '\0'; + } + } + /* Check for @msgid tag */ + else if (tag_len >= 6 && memcmp(tag_name, "msgid=", 6) == 0) { + int value_len = tag_len - 6; + if (value_len < (int)sizeof(cli_s2s_msgid(cptr))) { + memcpy(cli_s2s_msgid(cptr), tag_name + 6, value_len); + cli_s2s_msgid(cptr)[value_len] = '\0'; + } + } + + /* Move to next tag */ + if (semicolon) + tagpos = semicolon + 1; + else + break; + } + + /* Skip past the @ prefix and tags to the actual message */ + ch = tagend; + while (*ch == ' ') + ch++; + } + /* If no space found, malformed - continue with original parse */ + } + para[0] = cli_name(from); /* diff --git a/ircd/s_auth.c b/ircd/s_auth.c index cb37cf9e..f64a8ecd 100644 --- a/ircd/s_auth.c +++ b/ircd/s_auth.c @@ -68,6 +68,7 @@ #include "ssl.h" #include +#include #include #include #include @@ -75,6 +76,12 @@ #include #include +/** Maximum number of config/stats entries from IAuth to prevent DoS. */ +#define IAUTH_LIST_MAX 1000 + +/** Circuit breaker threshold: consecutive timeouts before bypassing IAUTH_REQUIRED. */ +#define IAUTH_CIRCUIT_BREAKER_THRESHOLD 10 + /** Pending operations during registration. */ enum AuthRequestFlag { AR_AUTH_PENDING, /**< ident connecting or waiting for response */ @@ -195,9 +202,15 @@ struct IAuth { char i_errbuf[BUFSIZE+1]; /**< partial unprocessed error line */ char *i_version; /**< iauth version string */ struct SLink *i_config; /**< configuration string list */ + struct SLink *i_config_tail; /**< tail of config list for O(1) append */ + unsigned int i_config_count; /**< number of config entries */ struct SLink *i_stats; /**< statistics string list */ + struct SLink *i_stats_tail; /**< tail of stats list for O(1) append */ + unsigned int i_stats_count; /**< number of stats entries */ char **i_argv; /**< argument list */ int i_argc; /**< number of arguments in argument list */ + unsigned int i_timeout_count; /**< consecutive auth timeout counter for circuit breaker */ + int i_circuit_open; /**< 1 if circuit breaker is tripped (bypassing IAUTH_REQUIRED) */ }; /** Return whether flag \a flag is set on \a iauth. */ @@ -965,8 +978,30 @@ int auth_ping_timeout(struct Client *cptr) if (FlagHas(&auth->flags, AR_IAUTH_PENDING)) { if (IAuthHas(iauth, IAUTH_REQUIRED) && !FlagHas(&auth->flags, AR_IAUTH_SOFT_DONE)) { - sendheader(cptr, REPORT_FAIL_IAUTH); - return exit_client_msg(cptr, cptr, &me, "Authorization Timeout"); + /* Circuit breaker: track consecutive timeouts. */ + if (iauth) + iauth->i_timeout_count++; + + /* Check if circuit breaker should trip. */ + if (iauth && !iauth->i_circuit_open && + iauth->i_timeout_count >= IAUTH_CIRCUIT_BREAKER_THRESHOLD) { + iauth->i_circuit_open = 1; + log_write(LS_IAUTH, L_WARNING, 0, + "IAuth circuit breaker tripped after %u timeouts - " + "bypassing IAUTH_REQUIRED", + iauth->i_timeout_count); + sendto_opmask_butone(NULL, SNO_AUTH, + "IAuth circuit breaker OPEN: bypassing " + "IAUTH_REQUIRED after %u consecutive timeouts", + iauth->i_timeout_count); + } + + /* If circuit is open, allow client through instead of killing. */ + if (!iauth || !iauth->i_circuit_open) { + sendheader(cptr, REPORT_FAIL_IAUTH); + return exit_client_msg(cptr, cptr, &me, "Authorization Timeout"); + } + /* Circuit open: let client proceed despite timeout. */ } sendto_iauth(cptr, "T"); FlagClr(&auth->flags, AR_IAUTH_PENDING); @@ -1131,7 +1166,7 @@ static void start_dns_query(struct AuthRequest *auth) } if (irc_in_addr_is_loopback(&cli_ip(auth->client))) { - strcpy(cli_sockhost(auth->client), cli_name(&me)); + ircd_strncpy(cli_sockhost(auth->client), cli_name(&me), HOSTLEN); sendto_iauth(auth->client, "N %s", cli_sockhost(auth->client)); return; } @@ -1709,6 +1744,11 @@ void auth_mark_closing(void) */ static void iauth_disconnect(struct IAuth *iauth) { + int ii; + int released = 0; + struct Client *cli; + struct AuthRequest *auth; + if (iauth == NULL) return; @@ -1725,6 +1765,24 @@ static void iauth_disconnect(struct IAuth *iauth) socket_del(i_socket(iauth)); s_fd(i_socket(iauth)) = -1; } + + /* Release any clients stuck waiting for IAuth response. */ + for (ii = 0; ii <= HighestFd; ii++) { + cli = LocalClientArray[ii]; + if (!cli || !(auth = cli_auth(cli))) + continue; + if (FlagHas(&auth->flags, AR_IAUTH_PENDING)) { + FlagClr(&auth->flags, AR_IAUTH_PENDING); + released++; + /* Try to complete registration now that IAuth is gone. */ + check_auth_finished(auth); + } + } + + if (released > 0) + sendto_opmask_butone(NULL, SNO_AUTH, + "IAuth disconnected: released %d pending client(s)", + released); } /** Close all %IAuth connections marked as closing. */ @@ -1732,7 +1790,31 @@ void auth_close_unused(void) { if (IAuthHas(iauth, IAUTH_CLOSING)) { int ii; + struct SLink *node, *next; + iauth_disconnect(iauth); + + /* Free version string */ + MyFree(iauth->i_version); + iauth->i_version = NULL; + + /* Free config list */ + for (node = iauth->i_config; node; node = next) { + next = node->next; + MyFree(node->value.cp); + free_link(node); + } + iauth->i_config = NULL; + + /* Free stats list */ + for (node = iauth->i_stats; node; node = next) { + next = node->next; + MyFree(node->value.cp); + free_link(node); + } + iauth->i_stats = NULL; + + /* Free argv */ if (iauth->i_argv) { for (ii = 0; iauth->i_argv[ii]; ++ii) MyFree(iauth->i_argv[ii]); @@ -1834,9 +1916,17 @@ static int iauth_cmd_snotice(struct IAuth *iauth, struct Client *cli, static int iauth_cmd_debuglevel(struct IAuth *iauth, struct Client *cli, int parc, char **params) { - int new_level; + int new_level = 0; - new_level = parc > 0 ? atoi(params[0]) : 0; + if (parc > 0 && params[0]) { + char *endptr; + long val; + errno = 0; + val = strtol(params[0], &endptr, 10); + /* Accept the value if it parsed successfully and fits in int range */ + if (errno != ERANGE && endptr != params[0] && val >= 0 && val <= INT_MAX) + new_level = (int)val; + } if (i_debug(iauth) > 0 || new_level > 0) { /* The "ia_dbg" name is borrowed from (IRCnet) ircd. */ sendto_opmask_butone(NULL, SNO_AUTH, "ia_dbg = %d", new_level); @@ -1960,6 +2050,8 @@ static int iauth_cmd_newconfig(struct IAuth *iauth, struct Client *cli, head = iauth->i_config; iauth->i_config = NULL; + iauth->i_config_tail = NULL; + iauth->i_config_count = 0; for (; head; head = next) { next = head->next; MyFree(head->value.cp); @@ -1981,14 +2073,25 @@ static int iauth_cmd_config(struct IAuth *iauth, struct Client *cli, { struct SLink *node; - if (iauth->i_config) { - for (node = iauth->i_config; node->next; node = node->next) ; - node = node->next = make_link(); - } else { - node = iauth->i_config = make_link(); + /* Enforce limit to prevent DoS from misbehaving IAuth */ + if (iauth->i_config_count >= IAUTH_LIST_MAX) { + log_write(LS_IAUTH, L_WARNING, 0, + "IAuth config list limit reached (%u entries)", IAUTH_LIST_MAX); + return 0; } + + node = make_link(); node->value.cp = paste_params(parc, params); node->next = 0; /* must be explicitly cleared */ + + /* Use tail pointer for O(1) append */ + if (iauth->i_config_tail) { + iauth->i_config_tail->next = node; + } else { + iauth->i_config = node; + } + iauth->i_config_tail = node; + iauth->i_config_count++; return 0; } @@ -2007,6 +2110,8 @@ static int iauth_cmd_newstats(struct IAuth *iauth, struct Client *cli, head = iauth->i_stats; iauth->i_stats = NULL; + iauth->i_stats_tail = NULL; + iauth->i_stats_count = 0; for (; head; head = next) { next = head->next; MyFree(head->value.cp); @@ -2027,14 +2132,26 @@ static int iauth_cmd_stats(struct IAuth *iauth, struct Client *cli, int parc, char **params) { struct SLink *node; - if (iauth->i_stats) { - for (node = iauth->i_stats; node->next; node = node->next) ; - node = node->next = make_link(); - } else { - node = iauth->i_stats = make_link(); + + /* Enforce limit to prevent DoS from misbehaving IAuth */ + if (iauth->i_stats_count >= IAUTH_LIST_MAX) { + log_write(LS_IAUTH, L_WARNING, 0, + "IAuth stats list limit reached (%u entries)", IAUTH_LIST_MAX); + return 0; } + + node = make_link(); node->value.cp = paste_params(parc, params); node->next = 0; /* must be explicitly cleared */ + + /* Use tail pointer for O(1) append */ + if (iauth->i_stats_tail) { + iauth->i_stats_tail->next = node; + } else { + iauth->i_stats = node; + } + iauth->i_stats_tail = node; + iauth->i_stats_count++; return 0; } @@ -2051,6 +2168,10 @@ static int iauth_cmd_username_forced(struct IAuth *iauth, struct Client *cli, assert(cli_auth(cli) != NULL); FlagClr(&cli_auth(cli)->flags, AR_AUTH_PENDING); if (!EmptyString(params[0])) { + /* Log forced username change for audit trail. */ + log_write(LS_IAUTH, L_INFO, 0, + "IAuth forced username for %s: %s", + cli_sock_ip(cli), params[0]); ircd_strncpy(cli_username(cli), params[0], USERLEN + 1); SetGotId(cli); FlagSet(&cli_auth(cli)->flags, AR_IAUTH_USERNAME); @@ -2131,6 +2252,11 @@ static int iauth_cmd_hostname(struct IAuth *iauth, struct Client *cli, SetIPSpoofed(cli); } + /* Log the hostname change for audit trail. */ + log_write(LS_IAUTH, L_INFO, 0, + "IAuth changed hostname for %s from %s to %s", + cli_sock_ip(cli), cli_sockhost(cli), params[0]); + /* Set hostname from params. */ ircd_strncpy(cli_sockhost(cli), params[0], HOSTLEN + 1); /* If we have gotten here, the user is in a "hurry" state and has @@ -2192,6 +2318,11 @@ static int iauth_cmd_ip_address(struct IAuth *iauth, struct Client *cli, ClearIPChecked(cli); } + /* Log the IP change for audit trail. */ + log_write(LS_IAUTH, L_INFO, 0, + "IAuth changed IP for client from %s to %s", + ircd_ntoa(&cli_ip(cli)), params[0]); + /* Update the IP and charge them as a remote connect. */ memcpy(&cli_ip(cli), &addr, sizeof(cli_ip(cli))); if (!find_except_conf(cli, EFLAG_IPCHECK)) @@ -2276,6 +2407,19 @@ static int iauth_cmd_done_client(struct IAuth *iauth, struct Client *cli, assert(cli_auth(cli) != NULL); FlagClr(&cli_auth(cli)->flags, AR_IAUTH_PENDING); + /* Reset circuit breaker on successful IAuth response. */ + if (iauth && iauth->i_timeout_count > 0) { + if (iauth->i_circuit_open) { + log_write(LS_IAUTH, L_INFO, 0, + "IAuth circuit breaker reset - IAuth responding again"); + sendto_opmask_butone(NULL, SNO_AUTH, + "IAuth circuit breaker CLOSED: IAuth responding " + "normally again"); + } + iauth->i_timeout_count = 0; + iauth->i_circuit_open = 0; + } + /* If a connection class was specified (and usable), assign the client to it. */ if (!EmptyString(params[0])) { struct ConfItem *aconf; @@ -2391,6 +2535,8 @@ static int iauth_cmd_mark(struct IAuth *iauth, struct Client *cli, ircd_strncpy(cli_version(cli), params[1], VERSIONLEN + 1); } else if (!ircd_strcmp(params[0], MARK_SSLCLIFP)) { ircd_strncpy(cli_sslclifp(cli), params[1], BUFSIZE + 1); + } else if (!ircd_strcmp(params[0], MARK_SSLCLIEXP)) { + cli_sslcliexp(cli) = strtoul(params[1], NULL, 10); } else if (!ircd_strcmp(params[0], MARK_KILL)) { ircd_strncpy(cli_killmark(cli), params[1], BUFSIZE + 1); } else if (!ircd_strcmp(params[0], MARK_MARK) || !ircd_strcmp(params[0], MARK_DNSBL_DATA)) { @@ -2675,9 +2821,14 @@ static void iauth_parse(struct IAuth *iauth, char *message) handler(iauth, NULL, parc, params); } else { /* Try to find the client associated with the request. */ - id = strtol(params[0], NULL, 10); + char *endptr; + errno = 0; + id = strtol(params[0], &endptr, 10); if (parc < 3) sendto_iauth(NULL, "E Missing :Need "); + else if (errno == ERANGE || endptr == params[0] || (*endptr && !IsSpace(*endptr))) + /* Invalid client ID format. */ + sendto_iauth(NULL, "E BadId :[%s] is not a valid client id", params[0]); else if (id < 0 || id > HighestFd || !(cli = LocalClientArray[id])) /* Client no longer exists (or never existed). */ sendto_iauth(NULL, "E Gone :[%s %s %s]", params[0], params[1], @@ -2690,22 +2841,33 @@ static void iauth_parse(struct IAuth *iauth, char *message) else { struct irc_sockaddr addr; int res; + long port_val; + char *port_end; /* Parse IP address and port number from parameters */ res = ipmask_parse(params[1], &addr.addr, NULL); - addr.port = strtol(params[2], NULL, 10); - - /* Check IP address and port number against expected. */ - if (0 == res || - (irc_in_addr_cmp(&addr.addr, &cli_ip(cli)) && - irc_in_addr_cmp(&addr.addr, &cli_connectip(cli))) || - (auth && addr.port != auth->port)) - /* Report mismatch to iauth. */ - sendto_iauth(cli, "E Mismatch :[%s] != [%s]", params[1], - ircd_ntoa(&cli_ip(cli))); - else if (handler(iauth, cli, parc - 3, params + 3) > 0) - /* Handler indicated a possible state change. */ - check_auth_finished(auth); + errno = 0; + port_val = strtol(params[2], &port_end, 10); + if (errno == ERANGE || port_end == params[2] || + (*port_end && !IsSpace(*port_end)) || + port_val < 0 || port_val > 65535) { + sendto_iauth(cli, "E BadPort :[%s] is not a valid port", params[2]); + } + else { + addr.port = (unsigned short)port_val; + + /* Check IP address and port number against expected. */ + if (0 == res || + (irc_in_addr_cmp(&addr.addr, &cli_ip(cli)) && + irc_in_addr_cmp(&addr.addr, &cli_connectip(cli))) || + (auth && addr.port != auth->port)) + /* Report mismatch to iauth. */ + sendto_iauth(cli, "E Mismatch :[%s] != [%s]", params[1], + ircd_ntoa(&cli_ip(cli))); + else if (handler(iauth, cli, parc - 3, params + 3) > 0) + /* Handler indicated a possible state change. */ + check_auth_finished(auth); + } } } } @@ -2747,8 +2909,12 @@ static void iauth_read(struct IAuth *iauth) /* Put unused data back into connection's buffer. */ iauth->i_count = strlen(sol); - if (iauth->i_count > BUFSIZE) + if (iauth->i_count > BUFSIZE) { + log_write(LS_IAUTH, L_WARNING, 0, + "IAuth partial line truncated: %u > %u bytes", + iauth->i_count, BUFSIZE); iauth->i_count = BUFSIZE; + } memcpy(iauth->i_buffer, sol, iauth->i_count); } @@ -2820,8 +2986,12 @@ static void iauth_read_stderr(struct IAuth *iauth) /* Put unused data back into connection's buffer. */ iauth->i_errcount = strlen(sol); - if (iauth->i_errcount > BUFSIZE) + if (iauth->i_errcount > BUFSIZE) { + log_write(LS_IAUTH, L_WARNING, 0, + "IAuth stderr line truncated: %u > %u bytes", + iauth->i_errcount, BUFSIZE); iauth->i_errcount = BUFSIZE; + } memcpy(iauth->i_errbuf, sol, iauth->i_errcount); } diff --git a/ircd/s_bsd.c b/ircd/s_bsd.c index 759a3d31..2b471d00 100644 --- a/ircd/s_bsd.c +++ b/ircd/s_bsd.c @@ -24,6 +24,7 @@ #include "config.h" #include "s_bsd.h" +#include "capab.h" #include "client.h" #include "IPcheck.h" #include "channel.h" @@ -55,6 +56,7 @@ #include "s_user.h" #include "send.h" #include "struct.h" +#include "websocket.h" #include "sys.h" #include "uping.h" #include "version.h" @@ -70,9 +72,14 @@ #include #include #include +#include #include #include +#ifndef IOV_MAX +#define IOV_MAX 1024 +#endif /* IOV_MAX */ + /** Array of my own clients, indexed by file descriptor. */ struct Client* LocalClientArray[MAXCONNECTIONS]; /** Maximum file descriptor in current use. */ @@ -244,6 +251,12 @@ static int connect_inet(struct ConfItem* aconf, struct Client* cptr) if (!os_set_tos(cli_fd(cptr), feature_int(FEAT_TOS_SERVER), family)) { report_error(TOS_ERROR_MSG, cli_name(cptr), errno); } + /* + * Disable Nagle's algorithm for low-latency server-to-server links. + */ + if (feature_bool(FEAT_TCP_NODELAY_S2S)) { + os_set_tcp_nodelay(cli_fd(cptr)); + } if ((result = os_connect_nonb(cli_fd(cptr), &aconf->address)) == IO_FAILURE) { cli_error(cptr) = errno; report_error(CONNECT_ERROR_MSG, cli_name(cptr), errno); @@ -298,6 +311,102 @@ unsigned int deliver_it(struct Client *cptr, struct MsgQ *buf) unsigned int bytes_count = 0; assert(0 != cptr); + /* + * For WebSocket clients awaiting handshake, don't send any IRC data yet. + * Data will be queued and delivered after handshake completes. + */ + if (IsWSNeedHandshake(cptr)) { + SetFlag(cptr, FLAG_BLOCKED); + return 0; + } + + /* + * For WebSocket clients, we need to wrap each IRC line in a WebSocket frame. + * We extract data from the MsgQ, frame it, and send the framed version. + */ + if (IsWebSocket(cptr)) { + static char ws_frame[BUFSIZE + 16]; + static char irc_line[BUFSIZE + 4]; + struct iovec iov[IOV_MAX]; + int iovcnt; + int i, line_len = 0; + int frame_len; + IOResult result; + int send_result; + + /* Get data from message queue as iovecs */ + iovcnt = msgq_mapiov(buf, iov, IOV_MAX, &bytes_count); + + /* Concatenate iovecs into single buffer for framing */ + for (i = 0; i < iovcnt && line_len < (int)sizeof(irc_line) - 1; i++) { + int copy_len = iov[i].iov_len; + if (line_len + copy_len >= (int)sizeof(irc_line)) + copy_len = sizeof(irc_line) - line_len - 1; + memcpy(irc_line + line_len, iov[i].iov_base, copy_len); + line_len += copy_len; + } + irc_line[line_len] = '\0'; + + /* Strip \r\n from end - WebSocket IRC doesn't need it */ + while (line_len > 0 && (irc_line[line_len-1] == '\r' || irc_line[line_len-1] == '\n')) + irc_line[--line_len] = '\0'; + + if (line_len > 0) { + /* Encode as WebSocket frame (text mode for IRC) */ + frame_len = websocket_encode_frame(irc_line, line_len, + (unsigned char *)ws_frame, 1); + + Debug((DEBUG_DEBUG, "WebSocket deliver: line_len=%d, frame_len=%d, msg='%.50s'", + line_len, frame_len, irc_line)); + +#ifdef USE_SSL + if (cli_socket(cptr).ssl) { + /* SSL WebSocket - use SSL_write directly */ + send_result = SSL_write(cli_socket(cptr).ssl, ws_frame, frame_len); + Debug((DEBUG_DEBUG, "WebSocket SSL_write: result=%d", send_result)); + if (send_result > 0) { + result = IO_SUCCESS; + bytes_written = send_result; + } else { + int ssl_err = SSL_get_error(cli_socket(cptr).ssl, send_result); + Debug((DEBUG_DEBUG, "WebSocket SSL_write error: ssl_err=%d", ssl_err)); + if (ssl_err == SSL_ERROR_WANT_WRITE || ssl_err == SSL_ERROR_WANT_READ) + result = IO_BLOCKED; + else + result = IO_FAILURE; + bytes_written = 0; + } + } else +#endif + result = os_send_nonb(cli_fd(cptr), ws_frame, frame_len, &bytes_written); + + switch (result) { + case IO_SUCCESS: + ClrFlag(cptr, FLAG_BLOCKED); + cli_sendB(cptr) += bytes_written; + cli_sendB(&me) += bytes_written; + /* Return original byte count so msgq knows how much to delete */ + if (bytes_written >= (unsigned)frame_len) + bytes_written = bytes_count; + else + bytes_written = 0; /* Partial write - don't delete from queue */ + if (bytes_written < bytes_count) + SetFlag(cptr, FLAG_BLOCKED); + break; + case IO_BLOCKED: + SetFlag(cptr, FLAG_BLOCKED); + bytes_written = 0; + break; + case IO_FAILURE: + cli_error(cptr) = errno; + SetFlag(cptr, FLAG_DEADSOCKET); + bytes_written = 0; + break; + } + } + return bytes_written; + } + #ifdef USE_SSL switch (client_sendv(cptr, buf, &bytes_count, &bytes_written)) { #else @@ -372,8 +481,11 @@ static int completed_connection(struct Client* cptr) } else if (r == 0) return 1; sslfp = ssl_get_fingerprint(cli_socket(cptr).ssl); - if (sslfp) + if (sslfp) { ircd_strncpy(cli_sslclifp(cptr), sslfp, BUFSIZE+1); + if (feature_bool(FEAT_CERT_EXPIRY_TRACKING)) + cli_sslcliexp(cptr) = ssl_get_cert_expiry(cli_socket(cptr).ssl); + } SetSSL(cptr); #endif } @@ -564,6 +676,12 @@ void add_connection(struct Listener* listener, int fd) { * source route, and the normal routing takes over. */ os_disable_options(fd); + /* + * Disable Nagle's algorithm for low-latency client connections. + */ + if (feature_bool(FEAT_TCP_NODELAY_C2S)) { + os_set_tcp_nodelay(fd); + } if (listener_server(listener)) { @@ -677,11 +795,22 @@ void add_connection(struct Listener* listener, int fd) { SetSSL(new_client); cli_socket(new_client).ssl = ssl; sslfp = ssl_get_fingerprint(ssl); - if (sslfp) + if (sslfp) { ircd_strncpy(cli_sslclifp(new_client), sslfp, BUFSIZE+1); + if (feature_bool(FEAT_CERT_EXPIRY_TRACKING)) + cli_sslcliexp(new_client) = ssl_get_cert_expiry(ssl); + } } #endif + /* Mark WebSocket connections - they need handshake before IRC protocol */ + Debug((DEBUG_DEBUG, "WebSocket check: listener_websocket=%d, FEAT_DRAFT_WEBSOCKET=%d, listener_port=%d", + listener_websocket(listener), feature_bool(FEAT_DRAFT_WEBSOCKET), listener->addr.port)); + if (listener_websocket(listener) && feature_bool(FEAT_DRAFT_WEBSOCKET)) { + Debug((DEBUG_DEBUG, "Setting WSNeedHandshake for new client")); + SetWSNeedHandshake(new_client); + } + Count_newunknown(UserStats); /* if we've made it this far we can put the client on the auth query pile */ start_auth(new_client); @@ -719,7 +848,8 @@ static int read_packet(struct Client *cptr, int socket_ready) if (socket_ready && !(IsUser(cptr) && - DBufLength(&(cli_recvQ(cptr))) > get_recvq(cptr))) { + DBufLength(&(cli_recvQ(cptr))) > get_recvq(cptr) + + ((cli_ml_batch_id(cptr)[0] || CapActive(cptr, CAP_DRAFT_MULTILINE)) ? feature_int(FEAT_MULTILINE_MAX_BYTES) : 0))) { #ifdef USE_SSL switch (client_recv(cptr, readbuf, sizeof(readbuf), &length)) { #else @@ -754,6 +884,165 @@ static int read_packet(struct Client *cptr, int socket_ready) return connect_dopacket(cptr, readbuf, length); else { + /* + * Handle WebSocket handshake for client connections. + * This must happen before normal client data processing. + */ + if (IsWSNeedHandshake(cptr)) { + int result; + char *client_buffer; + char *endp; + const char *src; + + Debug((DEBUG_DEBUG, "Client WebSocket handshake: length=%d", length)); + + /* Accumulate data in client buffer for HTTP request */ + client_buffer = cli_buffer(cptr); + endp = client_buffer + cli_count(cptr); + src = readbuf; + + /* Copy incoming data to buffer */ + while (length > 0 && (endp - client_buffer) < BUFSIZE - 1) { + *endp++ = *src++; + length--; + } + *endp = '\0'; + cli_count(cptr) = endp - client_buffer; + + /* Try to complete handshake */ + result = websocket_handshake(cptr, client_buffer, cli_count(cptr)); + if (result == 0) { + /* Need more data */ + return 1; + } else if (result < 0) { + /* Handshake failed */ + return exit_client(cptr, cptr, &me, "WebSocket handshake failed"); + } + /* Handshake succeeded - clear buffer and unblock sends */ + Debug((DEBUG_DEBUG, "WebSocket handshake completed successfully")); + cli_count(cptr) = 0; + ClrFlag(cptr, FLAG_BLOCKED); /* Allow queued messages to be sent */ + /* Trigger send of queued data */ + send_queued(cptr); + /* If no remaining data, we're done for now */ + if (length <= 0) + return 1; + } + + /* + * For WebSocket clients, decode frames before queuing. + * WebSocket frames wrap the IRC protocol data. + * Supports RFC 6455 fragmentation and partial frame buffering. + */ + if (length > 0 && IsWebSocket(cptr)) { + char ws_payload[BUFSIZE + 16]; /* Stack-local, not static */ + int ws_len, opcode, consumed, is_fin; + unsigned char *ws_data; + int ws_remaining; + int copy_len; + + Debug((DEBUG_DEBUG, "WebSocket receive: length=%d, IsWebSocket=%d", length, IsWebSocket(cptr))); + + /* Prepend any partial frame from previous read */ + if (cli_ws_frame_len(cptr) > 0) { + copy_len = length; + if (copy_len > BUFSIZE - cli_ws_frame_len(cptr)) + copy_len = BUFSIZE - cli_ws_frame_len(cptr); + memcpy(cli_ws_frame_buf(cptr) + cli_ws_frame_len(cptr), readbuf, copy_len); + ws_data = cli_ws_frame_buf(cptr); + ws_remaining = cli_ws_frame_len(cptr) + copy_len; + } else { + ws_data = (unsigned char *)readbuf; + ws_remaining = length; + } + + while (ws_remaining > 0) { + consumed = websocket_decode_frame(ws_data, ws_remaining, + ws_payload, sizeof(ws_payload), + &ws_len, &opcode, &is_fin); + Debug((DEBUG_DEBUG, "WebSocket decode: consumed=%d, ws_len=%d, opcode=%d, is_fin=%d, remaining=%d", + consumed, ws_len, opcode, is_fin, ws_remaining)); + if (consumed == 0) { + /* Incomplete frame - save for next read */ + Debug((DEBUG_DEBUG, "WebSocket: Incomplete frame, saving %d bytes", ws_remaining)); + if (ws_remaining > 0 && ws_remaining < BUFSIZE) { + memmove(cli_ws_frame_buf(cptr), ws_data, ws_remaining); + cli_ws_frame_len(cptr) = ws_remaining; + } + break; + } else if (consumed < 0) { + /* Frame error */ + Debug((DEBUG_DEBUG, "WebSocket: Frame error (consumed=%d)", consumed)); + return exit_client(cptr, cptr, &me, "WebSocket frame error"); + } + + Debug((DEBUG_DEBUG, "WebSocket frame payload: '%.50s'", ws_payload)); + cli_ws_frame_len(cptr) = 0; /* Frame consumed successfully */ + + /* Handle control frames (always complete, can be interleaved) */ + if (opcode >= WS_OPCODE_CLOSE) { + if (!websocket_handle_control(cptr, opcode, ws_payload, ws_len)) { + /* Close frame received */ + return exit_client(cptr, cptr, &me, "WebSocket closed"); + } + } + /* Handle continuation frame (part of fragmented message) */ + else if (opcode == WS_OPCODE_CONTINUATION) { + /* Append to fragment buffer */ + if (cli_ws_frag_len(cptr) + ws_len <= 16384) { + memcpy(cli_ws_frag_buf(cptr) + cli_ws_frag_len(cptr), ws_payload, ws_len); + cli_ws_frag_len(cptr) += ws_len; + } else { + /* Fragment too large */ + return exit_client(cptr, cptr, &me, "WebSocket fragment overflow"); + } + if (is_fin) { + /* Fragment complete - deliver reassembled message */ + char *frag_data = cli_ws_frag_buf(cptr); + int frag_len = cli_ws_frag_len(cptr); + if (frag_len > 0) { + /* Add line ending if needed */ + if (frag_len < 16384 - 1 && frag_data[frag_len - 1] != '\n') { + frag_data[frag_len++] = '\n'; + } + if (dbuf_put(&(cli_recvQ(cptr)), frag_data, frag_len) == 0) + return exit_client(cptr, cptr, &me, "dbuf_put fail"); + } + cli_ws_frag_len(cptr) = 0; + cli_ws_frag_opcode(cptr) = 0; + } + } + /* Handle data frames (TEXT or BINARY) */ + else if (opcode == WS_OPCODE_TEXT || opcode == WS_OPCODE_BINARY) { + if (!is_fin) { + /* First fragment - save to fragment buffer */ + cli_ws_frag_opcode(cptr) = opcode; + if (ws_len <= 16384) { + memcpy(cli_ws_frag_buf(cptr), ws_payload, ws_len); + cli_ws_frag_len(cptr) = ws_len; + } else { + return exit_client(cptr, cptr, &me, "WebSocket fragment overflow"); + } + } else { + /* Complete frame - deliver immediately */ + if (ws_len > 0) { + /* WebSocket IRC: messages don't require \r\n, add \n for parser */ + if (ws_len < (int)sizeof(ws_payload) - 1 && + ws_payload[ws_len - 1] != '\n') { + ws_payload[ws_len++] = '\n'; + } + if (dbuf_put(&(cli_recvQ(cptr)), ws_payload, ws_len) == 0) + return exit_client(cptr, cptr, &me, "dbuf_put fail"); + } + } + } + + ws_data += consumed; + ws_remaining -= consumed; + } + length = 0; /* Data processed via WebSocket path */ + } + /* * Before we even think of parsing what we just read, stick * it on the end of the receive queue and do it when its @@ -762,8 +1051,22 @@ static int read_packet(struct Client *cptr, int socket_ready) if (length > 0 && dbuf_put(&(cli_recvQ(cptr)), readbuf, length) == 0) return exit_client(cptr, cptr, &me, "dbuf_put fail"); - if (DBufLength(&(cli_recvQ(cptr))) > get_recvq(cptr)) - return exit_client(cptr, cptr, &me, "Excess Flood"); + /* + * Check for buffer flood, but allow extra buffer space for clients + * with multiline capability. When a client has draft/multiline enabled, + * they may send BATCH + id, many lines, BATCH - id all at once in a + * single TCP burst. The batch ID won't be set until BATCH + is parsed, + * so we must check capability, not just active batch state. + */ + { + unsigned int max_recvq = get_recvq(cptr); + if (cli_ml_batch_id(cptr)[0] || CapActive(cptr, CAP_DRAFT_MULTILINE)) { + /* Client has multiline cap or is in batch - allow extra buffer space */ + max_recvq += feature_int(FEAT_MULTILINE_MAX_BYTES); + } + if (DBufLength(&(cli_recvQ(cptr))) > max_recvq) + return exit_client(cptr, cptr, &me, "Excess Flood"); + } while (DBufLength(&(cli_recvQ(cptr))) && !NoNewLine(cptr) && (IsTrusted(cptr) || cli_since(cptr) - CurrentTime < 10)) diff --git a/ircd/s_conf.c b/ircd/s_conf.c index 13e88527..a3dded8e 100644 --- a/ircd/s_conf.c +++ b/ircd/s_conf.c @@ -886,6 +886,32 @@ struct WebIRCConf* find_webirc_conf(struct Client *cptr, char *passwd, int* stat return 0; } +/** Find WebIRC configuration by host/IP match only (no password verification). + * This is used for async password verification - we find the matching block + * first, then verify the password asynchronously. + * @param[in] cptr Client to match WebIRC configuration against. + * @return Matching WebIRCConf or NULL if no match found. + */ +struct WebIRCConf* find_webirc_conf_by_host(struct Client *cptr) +{ + struct WebIRCConf *wconf; + + for (wconf = webircConfList; wconf; wconf = wconf->next) { + if (wconf->usermask && match(wconf->usermask, cli_username(cptr))) + continue; + if (wconf->bits > 0) { + if (!ipmask_check(&cli_ip(cptr), &wconf->address, wconf->bits)) + continue; + } else if (wconf->hostmask && match(wconf->hostmask, cli_sockhost(cptr))) + continue; + + /* Found a matching host/IP - return it (password check deferred) */ + return wconf; + } + + return NULL; +} + /** Free all WebIRC configurations from #webircConfList. */ void conf_erase_webirc_list(void) { @@ -979,6 +1005,49 @@ struct SHostConf* find_shost_conf(struct Client *cptr, char *host, char *passwd, return 0; } +/** Find SpoofHost configuration by hostmask match only (no password verification). + * This is used for async password verification - we find the matching block + * first, then verify the password asynchronously. + * @param[in] cptr Client to match SpoofHost configuration against. + * @param[in] host Spoofhost to look for. + * @return Matching SHostConf or NULL if no match found. + */ +struct SHostConf* find_shost_conf_by_host(struct Client *cptr, const char *host) +{ + struct SHostConf* sconf; + + if (!host) + return NULL; + + for (sconf = shostConfList; sconf; sconf = sconf->next) { + /* Check hostmask match */ + if (!(sconf->flags & SHFLAG_ISMASK) && strcmp(sconf->spoofhost, host)) + continue; + if ((sconf->flags & SHFLAG_ISMASK) && match(sconf->spoofhost, host)) + continue; + + /* Check usermask if configured */ + if (sconf->usermask) { + if (match(sconf->usermask, cli_username(cptr)) && + !((sconf->flags & SHFLAG_MATCHUSER) && cli_user(cptr) && + !match(sconf->usermask, cli_user(cptr)->username))) + continue; + } + + /* Check IP/host match */ + if (sconf->bits > 0) { + if (!ipmask_check(&cli_ip(cptr), &sconf->address, sconf->bits)) + continue; + } else if (sconf->hostmask && match(sconf->hostmask, cli_sockhost(cptr))) + continue; + + /* Found a matching host - return it (password check deferred) */ + return sconf; + } + + return NULL; +} + /** Free all SpoofHost configurations from #shostConfList. */ void conf_erase_shost_list(void) { diff --git a/ircd/s_err.c b/ircd/s_err.c index 2c5a13cf..316a09f0 100644 --- a/ircd/s_err.c +++ b/ircd/s_err.c @@ -1551,36 +1551,36 @@ static Numeric replyTable[] = { { 0 }, /* 759 */ { 0 }, -/* 760 */ - { 0 }, -/* 761 */ - { 0 }, -/* 762 */ - { 0 }, +/* 760 - RPL_WHOISKEYVALUE */ + { RPL_WHOISKEYVALUE, "%s %s %s * :%s", "760" }, +/* 761 - RPL_KEYVALUE (target, key, visibility, value) */ + { RPL_KEYVALUE, "%s %s %s :%s", "761" }, +/* 762 - RPL_METADATAEND (target) */ + { RPL_METADATAEND, "%s :end of metadata", "762" }, /* 763 */ { 0 }, /* 764 */ { 0 }, /* 765 */ { 0 }, -/* 766 */ - { 0 }, +/* 766 - RPL_KEYNOTSET */ + { RPL_KEYNOTSET, "%s %s :key not set", "766" }, /* 767 */ { 0 }, /* 768 */ { 0 }, /* 769 */ { 0 }, -/* 770 */ - { 0 }, -/* 771 */ - { 0 }, -/* 772 */ - { 0 }, +/* 770 - RPL_METADATASUBOK */ + { RPL_METADATASUBOK, "%s :subscribed", "770" }, +/* 771 - RPL_METADATAUNSUBOK */ + { RPL_METADATAUNSUBOK, "%s :unsubscribed", "771" }, +/* 772 - RPL_METADATASUBS */ + { RPL_METADATASUBS, "%s", "772" }, /* 773 */ { 0 }, -/* 774 */ - { 0 }, +/* 774 - RPL_METADATASYNCLATER */ + { RPL_METADATASYNCLATER, "%s :sync deferred", "774" }, /* 775 */ { 0 }, /* 776 */ diff --git a/ircd/s_misc.c b/ircd/s_misc.c index 2ad144f3..7c1c59e4 100644 --- a/ircd/s_misc.c +++ b/ircd/s_misc.c @@ -27,6 +27,7 @@ #include "config.h" #include "s_misc.h" +#include "account_conn.h" #include "IPcheck.h" #include "channel.h" #include "client.h" @@ -58,6 +59,7 @@ #include "uping.h" #include "userload.h" #include "watch.h" +#include "history.h" /* #include -- Now using assert in ircd_log.h */ #include @@ -65,6 +67,8 @@ #include #include #include +#include +#include #include /** Array of English month names (0 = January). */ @@ -173,6 +177,69 @@ const char* get_client_name(const struct Client* sptr, int showip) return nbuf; } +#ifdef USE_LMDB +/** Counter for generating unique message IDs for QUIT event history storage */ +static unsigned long quit_history_msgid_counter = 0; + +/** Store QUIT events in history for all channels the user is on. + * This is called before remove_user_from_all_channels() so we can + * iterate through the user's channels. + * @param[in] sptr Client that is quitting. + * @param[in] comment The quit message. + */ +static void store_quit_events(struct Client *sptr, const char *comment) +{ + struct Membership *member; + struct timeval tv; + char timestamp[32]; + char msgid[64]; + char sender[HISTORY_SENDER_LEN]; + const char *account; + + if (!history_is_available()) + return; + + /* Check if chathistory feature is enabled */ + if (!feature_bool(FEAT_CAP_draft_chathistory)) + return; + + /* Only store for local users to avoid duplicates */ + if (!MyUser(sptr)) + return; + + /* Generate Unix timestamp (same for all channels) */ + gettimeofday(&tv, NULL); + ircd_snprintf(0, timestamp, sizeof(timestamp), "%lu.%03lu", + (unsigned long)tv.tv_sec, + (unsigned long)(tv.tv_usec / 1000)); + + /* Build sender string: nick!user@host */ + if (cli_user(sptr)) + ircd_snprintf(0, sender, sizeof(sender), "%s!%s@%s", + cli_name(sptr), + cli_user(sptr)->username, + cli_user(sptr)->host); + else + ircd_strncpy(sender, cli_name(sptr), sizeof(sender) - 1); + + /* Get account name if logged in */ + account = (cli_user(sptr) && cli_user(sptr)->account[0]) + ? cli_user(sptr)->account : NULL; + + /* Store QUIT event for each channel the user is on */ + for (member = cli_user(sptr)->channel; member; member = member->next_channel) { + /* Generate unique msgid for each channel's QUIT event */ + ircd_snprintf(0, msgid, sizeof(msgid), "%s-%lu-%lu", + cli_yxx(&me), + (unsigned long)cli_firsttime(&me), + ++quit_history_msgid_counter); + + history_store_message(msgid, timestamp, member->channel->chname, sender, + account, HISTORY_QUIT, comment ? comment : ""); + } +} +#endif /* USE_LMDB */ + /** * Exit one client, local or remote. Assuming for local client that * all dependents already have been removed, and socket is closed. @@ -208,6 +275,16 @@ static void exit_one_client(struct Client* bcptr, const char* comment) */ sendcmdto_common_channels_butone(bcptr, CMD_QUIT, NULL, ":%s", comment); + /* Remove from presence aggregation registry before channel cleanup */ + if (feature_bool(FEAT_PRESENCE_AGGREGATION) && IsAccount(bcptr)) { + account_conn_remove(bcptr); + } + +#ifdef USE_LMDB + /* Store QUIT events in history before removing from channels */ + store_quit_events(bcptr, comment); +#endif + remove_user_from_all_channels(bcptr); /* Clean up invitefield */ @@ -394,6 +471,7 @@ int exit_client(struct Client *cptr, if (IsUser(victim) || IsUserPort(victim)) { abort_sasl(victim, 0); auth_send_exit(victim); + pending_rename_client_exit(victim); } if (IsUser(victim)) @@ -499,8 +577,18 @@ int exit_client(struct Client *cptr, } } /* Then remove the client structures */ - if (IsServer(victim)) + if (IsServer(victim)) { + char netsplit_batch_id[32] = ""; + /* Start IRCv3 netsplit batch for local clients */ + send_netsplit_batch_start(victim, cli_serv(victim)->up, + netsplit_batch_id, sizeof(netsplit_batch_id)); + /* Set active batch so QUIT messages include @batch tag */ + set_active_network_batch(netsplit_batch_id); exit_downlinks(victim, killer, comment1); + /* Clear active batch and end IRCv3 netsplit batch */ + set_active_network_batch(NULL); + send_netsplit_batch_end(netsplit_batch_id); + } exit_one_client(victim, comment); /* diff --git a/ircd/s_serv.c b/ircd/s_serv.c index 7e0c627c..1daada92 100644 --- a/ircd/s_serv.c +++ b/ircd/s_serv.c @@ -60,6 +60,8 @@ #include "sys.h" #include "userload.h" #include "zline.h" +#include "metadata.h" +#include "ircd_features.h" /* #include -- Now using assert in ircd_log.h */ #include @@ -279,9 +281,13 @@ int server_estab(struct Client *cptr, struct ConfItem *aconf) cli_name(acptr), MARK_MARK, lp->value.cp); } - if (cli_sslclifp(acptr) && !EmptyString(cli_sslclifp(acptr))) + if (cli_sslclifp(acptr) && !EmptyString(cli_sslclifp(acptr))) { sendcmdto_one(cli_user(acptr)->server, CMD_MARK, cptr, "%s %s :%s", cli_name(acptr), MARK_SSLCLIFP, cli_sslclifp(acptr)); + if (feature_bool(FEAT_CERT_EXPIRY_TRACKING) && cli_sslcliexp(acptr) > 0) + sendcmdto_one(cli_user(acptr)->server, CMD_MARK, cptr, "%s %s :%lu", + cli_name(acptr), MARK_SSLCLIEXP, (unsigned long)cli_sslcliexp(acptr)); + } if (cli_killmark(acptr) && !EmptyString(cli_killmark(acptr))) sendcmdto_one(cli_user(acptr)->server, CMD_MARK, cptr, "%s %s :%s", @@ -327,6 +333,17 @@ int server_estab(struct Client *cptr, struct ConfItem *aconf) } client_send_privs(cli_user(acptr)->server, cptr, acptr); + + /* Burst user metadata if enabled */ + if (feature_bool(FEAT_METADATA_BURST)) { + struct MetadataEntry *entry; + for (entry = cli_metadata(acptr); entry; entry = entry->next) { + sendcmdto_one(cli_user(acptr)->server, CMD_METADATA, cptr, "%C %s %s :%s", + acptr, entry->key, + entry->visibility == METADATA_VIS_PRIVATE ? "P" : "*", + entry->value ? entry->value : ""); + } + } } } /* diff --git a/ircd/s_stats.c b/ircd/s_stats.c index 66861bc4..a83f8c68 100644 --- a/ircd/s_stats.c +++ b/ircd/s_stats.c @@ -56,6 +56,8 @@ #include "struct.h" #include "userload.h" #include "zline.h" +#include "history.h" +#include "metadata.h" #include #include @@ -726,6 +728,12 @@ struct StatDesc statsinfo[] = { { ' ', "iauth", STAT_FLAG_OPERFEAT, FEAT_HIS_STATS_IAUTH, report_iauth_stats, 0, "IAuth statistics." }, + { ' ', "chathistory", STAT_FLAG_OPERFEAT, FEAT_LAST_F, + history_report_stats, 0, + "CHATHISTORY storage statistics." }, + { ' ', "metadata", STAT_FLAG_OPERFEAT, FEAT_LAST_F, + metadata_report_stats, 0, + "METADATA storage and queue statistics." }, { ' ', "iauthconf", STAT_FLAG_OPERFEAT, FEAT_HIS_STATS_IAUTH, report_iauth_conf, 0, "IAuth configuration." }, diff --git a/ircd/s_user.c b/ircd/s_user.c index bc9513ca..07851994 100644 --- a/ircd/s_user.c +++ b/ircd/s_user.c @@ -27,6 +27,8 @@ #include "config.h" #include "s_user.h" +#include "account_conn.h" +#include "capab.h" #include "IPcheck.h" #include "channel.h" #include "class.h" @@ -463,6 +465,34 @@ int register_user(struct Client *cptr, struct Client *sptr) m_lusers(sptr, sptr, 1, parv); update_load(); motd_signon(sptr); + + /* PM chathistory policy notification (feature-gated) */ + if (feature_bool(FEAT_CHATHISTORY_PM_NOTICE) && + feature_bool(FEAT_CHATHISTORY_PRIVATE)) { + int consent = feature_int(FEAT_CHATHISTORY_PRIVATE_CONSENT); + const char *policy, *action; + + if (consent == 0) { + policy = "private messages are stored by default"; + action = "To opt-out: /METADATA * SET chathistory.pm * :0"; + } else if (consent == 1) { + policy = "private messages are stored if either party opts in (opt-out overrides)"; + action = "To opt-in: /METADATA * SET chathistory.pm * :1 | To opt-out: /METADATA * SET chathistory.pm * :0"; + } else { + policy = "private messages are stored only if both parties opt in"; + action = "To opt-in: /METADATA * SET chathistory.pm * :1"; + } + + if (CapActive(sptr, CAP_STANDARDREPLIES)) { + /* IRCv3 standard-replies NOTE */ + send_note(sptr, "CHATHISTORY", "PM_POLICY", policy, action); + } else { + /* Fallback NOTICE for all clients */ + sendcmdto_one(&me, CMD_NOTICE, sptr, "%C :PM history: %s. %s", + sptr, policy, action); + } + } + if (cli_snomask(sptr) & SNO_NOISY) set_snomask(sptr, cli_snomask(sptr) & SNO_NOISY, SNO_ADD); if (feature_bool(FEAT_CONNEXIT_NOTICES)) @@ -474,6 +504,43 @@ int register_user(struct Client *cptr, struct Client *sptr) if (IsIPChecked(sptr)) IPcheck_connect_succeeded(sptr); + + /* Apply pre-away state if set (IRCv3 draft/pre-away) */ + { + int pre_away_type = con_pre_away(cli_connect(sptr)); + if (pre_away_type) { + if (pre_away_type == 2) { + /* AWAY * - set away but with empty message (hidden connection) */ + if (!user->away) { + user->away = (char*) MyMalloc(1); + user->away[0] = '\0'; + } + /* Don't broadcast AWAY * to servers - it's a hidden connection */ + } else { + /* Normal away with message */ + unsigned int len = strlen(con_pre_away_msg(cli_connect(sptr))); + if (user->away) + MyFree(user->away); + user->away = (char*) MyMalloc(len + 1); + strcpy(user->away, con_pre_away_msg(cli_connect(sptr))); + /* Broadcast to servers */ + sendcmdto_serv_butone(sptr, CMD_AWAY, cptr, ":%s", user->away); + } + /* Clear pre-away state */ + con_pre_away(cli_connect(sptr)) = 0; + con_pre_away_msg(cli_connect(sptr))[0] = '\0'; + + /* Register with presence aggregation if feature enabled and logged in */ + if (feature_bool(FEAT_PRESENCE_AGGREGATION) && IsAccount(sptr)) { + enum ConnAwayState state = (pre_away_type == 2) ? CONN_AWAY_STAR : CONN_AWAY; + account_conn_add(sptr); + account_conn_set_away(sptr, state, user->away); + } + } else if (feature_bool(FEAT_PRESENCE_AGGREGATION) && IsAccount(sptr)) { + /* No pre-away, just register as present */ + account_conn_add(sptr); + } + } } else { struct Client *acptr = user->server; @@ -563,8 +630,11 @@ int register_user(struct Client *cptr, struct Client *sptr) sendcmdto_serv_butone(&me, CMD_MARK, cptr, "%s %s :%s", cli_name(cptr), MARK_MARK, lp->value.cp); } - if (cli_sslclifp(sptr) && !EmptyString(cli_sslclifp(sptr))) + if (cli_sslclifp(sptr) && !EmptyString(cli_sslclifp(sptr))) { sendcmdto_serv_butone(&me, CMD_MARK, cptr, "%s %s :%s", cli_name(cptr), MARK_SSLCLIFP, cli_sslclifp(sptr)); + if (feature_bool(FEAT_CERT_EXPIRY_TRACKING) && cli_sslcliexp(sptr) > 0) + sendcmdto_serv_butone(&me, CMD_MARK, cptr, "%s %s :%lu", cli_name(cptr), MARK_SSLCLIEXP, (unsigned long)cli_sslcliexp(sptr)); + } if (cli_version(sptr) && !EmptyString(cli_version(sptr))) { sendcmdto_serv_butone(&me, CMD_MARK, cptr, "%s %s :%s", cli_name(cptr), MARK_CVERSION, cli_version(sptr)); @@ -702,7 +772,8 @@ static const struct UserMode { { FLAG_SETHOST, 'h' }, { FLAG_FAKEHOST, 'f' }, { FLAG_CLOAKHOST, 'C' }, - { FLAG_CLOAKIP, 'c' } + { FLAG_CLOAKIP, 'c' }, + { FLAG_MULTILINE_EXPAND, 'M' } }; /** Length of #userModeList. */ @@ -1127,6 +1198,8 @@ hide_hostmask(struct Client *cptr) { char newhost[HOSTLEN+1]; char newuser[USERLEN+1]; + char oldhost[HOSTLEN+1]; + char olduser[USERLEN+1]; char* sethostat = NULL; char* userat = NULL; struct Membership *chan; @@ -1188,8 +1261,14 @@ hide_hostmask(struct Client *cptr) ClearExceptValidQuiet(chan); } + /* Save old user/host for CHGHOST notification */ + ircd_strncpy(oldhost, cli_user(cptr)->host, HOSTLEN); + ircd_strncpy(olduser, cli_user(cptr)->username, USERLEN); + + /* For clients without chghost capability, use QUIT+JOIN if enabled */ if (feature_bool(FEAT_HIDDEN_HOST_QUIT)) - sendcmdto_common_channels_butone(cptr, CMD_QUIT, cptr, ":%s", + sendcmdto_common_channels_capab_butone(cptr, CMD_QUIT, cptr, + CAP_NONE, CAP_CHGHOST, ":%s", feature_str(FEAT_HIDDEN_HOST_SET_MESSAGE)); /* Finally copy the new host to the users current host. */ @@ -1197,6 +1276,12 @@ hide_hostmask(struct Client *cptr) if (newuser[0] != '\0') ircd_strncpy(cli_user(cptr)->username, newuser, USERLEN + 1); + /* Send CHGHOST to clients with the chghost capability */ + if (feature_bool(FEAT_CAP_chghost)) + sendcmdto_common_channels_capab_butone(cptr, CMD_CHGHOST, cptr, + CAP_CHGHOST, CAP_NONE, "%s %s", + cli_user(cptr)->username, cli_user(cptr)->host); + /* ok, the client is now fully hidden, so let them know -- hikari */ if (MyConnect(cptr)) send_reply(cptr, RPL_HOSTHIDDEN, cli_user(cptr)->host, " hidden"); @@ -1212,37 +1297,39 @@ hide_hostmask(struct Client *cptr) { if (IsZombie(chan)) continue; - /* Send a JOIN unless the user's join has been delayed. */ + /* Send a JOIN unless the user's join has been delayed. + * Skip clients with chghost capability - they got CHGHOST instead. + */ if (!IsDelayedJoin(chan)) { - sendcmdto_channel_capab_butserv_butone(cptr, CMD_JOIN, chan->channel, cptr, 0, + sendcmdto_channel_capab_butserv_butone(cptr, CMD_JOIN, chan->channel, cptr, SKIP_CHGHOST, CAP_NONE, CAP_EXTJOIN, "%H", chan->channel); - sendcmdto_channel_capab_butserv_butone(cptr, CMD_JOIN, chan->channel, cptr, 0, + sendcmdto_channel_capab_butserv_butone(cptr, CMD_JOIN, chan->channel, cptr, SKIP_CHGHOST, CAP_EXTJOIN, CAP_NONE, "%H %s :%s", chan->channel, IsAccount(cptr) ? cli_account(cptr) : "*", cli_info(cptr)); if (cli_user(cptr)->away) - sendcmdto_channel_capab_butserv_butone(cptr, CMD_AWAY, chan->channel, NULL, 0, + sendcmdto_channel_capab_butserv_butone(cptr, CMD_AWAY, chan->channel, NULL, SKIP_CHGHOST, CAP_AWAYNOTIFY, CAP_NONE, ":%s", cli_user(cptr)->away); } if (IsChanOp(chan) && IsHalfOp(chan) && HasVoice(chan)) - sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, 0, + sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, SKIP_CHGHOST, "%H +ohv %C %C %C", chan->channel, cptr, cptr, cptr); else if (IsChanOp(chan) && IsHalfOp(chan)) - sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, 0, + sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, SKIP_CHGHOST, "%H +oh %C %C", chan->channel, cptr, cptr); else if (IsChanOp(chan) && HasVoice(chan)) - sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, 0, + sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, SKIP_CHGHOST, "%H +ov %C %C", chan->channel, cptr, cptr); else if (IsHalfOp(chan) && HasVoice(chan)) - sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, 0, + sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, SKIP_CHGHOST, "%H +hv %C %C", chan->channel, cptr, cptr); else if (IsChanOp(chan) || IsHalfOp(chan) || HasVoice(chan)) - sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, 0, + sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, SKIP_CHGHOST, "%H +%c %C", chan->channel, IsChanOp(chan) ? 'o' : (IsHalfOp(chan) ? 'h' : 'v'), cptr); } return 0; @@ -1275,11 +1362,19 @@ unhide_hostmask(struct Client *cptr) ClearExceptValidNick(chan); } + /* For clients without chghost capability, use QUIT+JOIN if enabled */ if (feature_bool(FEAT_HIDDEN_HOST_QUIT)) - sendcmdto_common_channels_butone(cptr, CMD_QUIT, cptr, ":%s", + sendcmdto_common_channels_capab_butone(cptr, CMD_QUIT, cptr, + CAP_NONE, CAP_CHGHOST, ":%s", feature_str(FEAT_HIDDEN_HOST_UNSET_MESSAGE)); ircd_strncpy(cli_user(cptr)->host, cli_user(cptr)->realhost, HOSTLEN + 1); + /* Send CHGHOST to clients with the chghost capability */ + if (feature_bool(FEAT_CAP_chghost)) + sendcmdto_common_channels_capab_butone(cptr, CMD_CHGHOST, cptr, + CAP_CHGHOST, CAP_NONE, "%s %s", + cli_user(cptr)->username, cli_user(cptr)->host); + /* ok, the client is now fully unhidden, so let them know -- hikari */ if (MyConnect(cptr)) send_reply(cptr, RPL_HOSTHIDDEN, cli_user(cptr)->host, ""); @@ -1289,7 +1384,8 @@ unhide_hostmask(struct Client *cptr) /* * Go through all channels the client was on, rejoin him - * and set the modes, if any + * and set the modes, if any. + * Skip clients with chghost capability - they got CHGHOST instead. */ for (chan = cli_user(cptr)->channel; chan; chan = chan->next_channel) { @@ -1297,35 +1393,35 @@ unhide_hostmask(struct Client *cptr) continue; /* Send a JOIN unless the user's join has been delayed. */ if (!IsDelayedJoin(chan)) { - sendcmdto_channel_capab_butserv_butone(cptr, CMD_JOIN, chan->channel, cptr, 0, + sendcmdto_channel_capab_butserv_butone(cptr, CMD_JOIN, chan->channel, cptr, SKIP_CHGHOST, CAP_NONE, CAP_EXTJOIN, "%H", chan->channel); - sendcmdto_channel_capab_butserv_butone(cptr, CMD_JOIN, chan->channel, cptr, 0, + sendcmdto_channel_capab_butserv_butone(cptr, CMD_JOIN, chan->channel, cptr, SKIP_CHGHOST, CAP_EXTJOIN, CAP_NONE, "%H %s :%s", chan->channel, IsAccount(cptr) ? cli_account(cptr) : "*", cli_info(cptr)); if (cli_user(cptr)->away) - sendcmdto_channel_capab_butserv_butone(cptr, CMD_AWAY, chan->channel, NULL, 0, + sendcmdto_channel_capab_butserv_butone(cptr, CMD_AWAY, chan->channel, NULL, SKIP_CHGHOST, CAP_AWAYNOTIFY, CAP_NONE, ":%s", cli_user(cptr)->away); } if (IsChanOp(chan) && IsHalfOp(chan) && HasVoice(chan)) - sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, 0, + sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, SKIP_CHGHOST, "%H +ohv %C %C", chan->channel, cptr, cptr, cptr); else if (IsChanOp(chan) && IsHalfOp(chan)) - sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, 0, + sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, SKIP_CHGHOST, "%H +oh %C %C", chan->channel, cptr, cptr); else if (IsChanOp(chan) && HasVoice(chan)) - sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, 0, + sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, SKIP_CHGHOST, "%H +ov %C %C", chan->channel, cptr, cptr); else if (IsHalfOp(chan) && HasVoice(chan)) - sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, 0, + sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, SKIP_CHGHOST, "%H +hv %C %C", chan->channel, cptr, cptr); else if (IsChanOp(chan) || IsHalfOp(chan) || HasVoice(chan)) - sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, 0, + sendcmdto_channel_butserv_butone(&his, CMD_MODE, chan->channel, cptr, SKIP_CHGHOST, "%H +%c %C", chan->channel, IsChanOp(chan) ? 'o' : (IsHalfOp(chan) ? 'h' : 'v'), cptr); } return 0; @@ -1602,6 +1698,12 @@ int set_user_mode(struct Client *cptr, struct Client *sptr, int parc, else ClearNoLink(acptr); break; + case 'M': + if (what == MODE_ADD) + SetFlag(acptr, FLAG_MULTILINE_EXPAND); + else + ClrFlag(acptr, FLAG_MULTILINE_EXPAND); + break; case 'x': if (what == MODE_ADD) { SetHiddenHost(acptr); @@ -2530,6 +2632,12 @@ void init_isupport(void) add_isupport_s("NETWORK", feature_str(FEAT_NETWORK)); add_isupport_s("MAXLIST", imaxlist); add_isupport_s("ELIST", "CT"); + + /* IRCv3 draft/chathistory support */ + if (feature_bool(FEAT_CAP_draft_chathistory)) { + add_isupport_i("CHATHISTORY", feature_int(FEAT_CHATHISTORY_MAX)); + add_isupport_s("MSGREFTYPES", "timestamp,msgid"); + } } /** Send RPL_ISUPPORT lines to \a cptr. @@ -2550,5 +2658,49 @@ send_supported(struct Client *cptr) return 0; /* convenience return, if it's ever needed */ } +/** Send RPL_ISUPPORT lines to \a cptr wrapped in a batch. + * Used when client has both batch and draft/extended-isupport capabilities. + * @param[in] cptr Client to send ISUPPORT to. + * @return Zero. + */ +int +send_supported_batched(struct Client *cptr) +{ + struct SLink *line; + char batchid[12]; + static unsigned long isupport_batch_counter = 0; + + if (isupport && !isupport_lines) + build_isupport_lines(); + + /* Check if we should use batch wrapping */ + if (!CapActive(cptr, CAP_BATCH)) { + /* No batch support, fall back to regular ISUPPORT */ + for (line = isupport_lines; line; line = line->next) + send_reply(cptr, RPL_ISUPPORT, line->value.cp); + return 0; + } + + /* Generate unique batch ID */ + ircd_snprintf(0, batchid, sizeof(batchid), "%s%lu", + cli_yxx(&me), ++isupport_batch_counter); + + /* Start batch: BATCH +id draft/isupport */ + sendcmdto_one(&me, CMD_BATCH_CMD, cptr, "+%s draft/isupport", batchid); + + /* Send each ISUPPORT line with batch tag */ + for (line = isupport_lines; line; line = line->next) { + sendrawto_one(cptr, "@batch=%s :%s 005 %s %s :are supported by this server", + batchid, cli_name(&me), + IsRegistered(cptr) ? cli_name(cptr) : "*", + line->value.cp); + } + + /* End batch: BATCH -id */ + sendcmdto_one(&me, CMD_BATCH_CMD, cptr, "-%s", batchid); + + return 0; +} + /* vim: shiftwidth=2 */ diff --git a/ircd/send.c b/ircd/send.c index 9371ef16..a2c880bd 100644 --- a/ircd/send.c +++ b/ircd/send.c @@ -24,6 +24,7 @@ #include "config.h" #include "send.h" +#include "capab.h" #include "channel.h" #include "class.h" #include "client.h" @@ -36,6 +37,7 @@ #include "list.h" #include "match.h" #include "msg.h" +#include "numeric.h" #include "numnicks.h" #include "parse.h" #include "s_bsd.h" @@ -48,6 +50,7 @@ /* #include -- Now using assert in ircd_log.h */ #include #include +#include /** Last used marker value. */ static int sentalong_marker; @@ -56,8 +59,419 @@ struct SLink *opsarray[32]; /* don't use highest bit unless you change atoi to strtoul in sendto_op_mask() */ /** Linked list of all connections with data queued to send. */ static struct Connection *send_queues; + +/** Active network batch ID for netjoin/netsplit batching. + * When non-empty, all QUIT/JOIN messages to local clients with batch capability + * will include @batch= tag per IRCv3 netsplit/netjoin batch spec. + */ +static char active_network_batch_id[32] = ""; char *GlobalForwards[256]; +/** Format current time as ISO 8601 timestamp for server-time capability. + * @param[out] buf Buffer to write timestamp to. + * @param[in] buflen Size of buffer. + * @return Pointer to buf. + */ +static char *format_server_time(char *buf, size_t buflen) +{ + struct timeval tv; + struct tm tm; + + gettimeofday(&tv, NULL); + gmtime_r(&tv.tv_sec, &tm); + snprintf(buf, buflen, "@time=%04d-%02d-%02dT%02d:%02d:%02d.%03ldZ ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, + tv.tv_usec / 1000); + return buf; +} + +/** Set the active network batch ID for netjoin/netsplit batching. + * When set, QUIT/JOIN messages to batch-capable clients will include @batch tag. + * @param[in] batch_id Batch ID to set, or NULL/empty to clear. + */ +void set_active_network_batch(const char *batch_id) +{ + if (batch_id && *batch_id) { + ircd_strncpy(active_network_batch_id, batch_id, sizeof(active_network_batch_id) - 1); + active_network_batch_id[sizeof(active_network_batch_id) - 1] = '\0'; + } else { + active_network_batch_id[0] = '\0'; + } +} + +/** Get the active network batch ID. + * @return Current batch ID, or empty string if none active. + */ +const char *get_active_network_batch(void) +{ + return active_network_batch_id; +} + +/** Check if a client wants message tags. + * Used for TAGMSG filtering - only clients with message-tags capability can receive TAGMSGs. + * @param[in] to Recipient client. + * @return Non-zero if client has message-tags capability active. + */ +static int wants_message_tags(struct Client *to) +{ + /* Primary check: message-tags capability */ + if (CapActive(to, CAP_MSGTAGS)) + return 1; + + /* Fallback: any capability that implies message tag support */ + return (feature_bool(FEAT_CAP_server_time) && CapActive(to, CAP_SERVERTIME)) || + (feature_bool(FEAT_CAP_account_tag) && CapActive(to, CAP_ACCOUNTTAG)) || + (feature_bool(FEAT_CAP_labeled_response) && CapActive(to, CAP_LABELEDRESP) && + MyConnect(to) && cli_label(to)[0]); +} + +/** Flags for format_message_tags_ex() tag selection */ +#define TAGS_TIME 0x01 /**< Include @time tag */ +#define TAGS_ACCOUNT 0x02 /**< Include @account tag */ +#define TAGS_BATCH 0x04 /**< Include @batch tag (network batch) */ +#define TAGS_BOT 0x08 /**< Include @bot tag */ + +/** Format message tags with explicit control over which tags to include. + * @param[out] buf Buffer to write tags to. + * @param[in] buflen Size of buffer. + * @param[in] from Source client (for account tag and bot detection). + * @param[in] flags TAGS_* flags indicating which tags to include. + * @return Pointer to buf, or NULL if no tags to add. + */ +static char *format_message_tags_ex(char *buf, size_t buflen, struct Client *from, int flags) +{ + int pos = 0; + int use_time = flags & TAGS_TIME; + int use_account = flags & TAGS_ACCOUNT; + int use_batch = (flags & TAGS_BATCH) && active_network_batch_id[0]; + int use_bot = (flags & TAGS_BOT) && from && IsBot(from); + + if (!use_time && !use_account && !use_batch && !use_bot) + return NULL; + + buf[0] = '@'; + pos = 1; + + /* @batch tag first (most important for batched messages) */ + if (use_batch) { + pos += snprintf(buf + pos, buflen - pos, "batch=%s", active_network_batch_id); + } + + if (use_time) { + struct timeval tv; + struct tm tm; + if (pos > 1 && pos < (int)buflen - 1) + buf[pos++] = ';'; + gettimeofday(&tv, NULL); + gmtime_r(&tv.tv_sec, &tm); + pos += snprintf(buf + pos, buflen - pos, + "time=%04d-%02d-%02dT%02d:%02d:%02d.%03ldZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, + tv.tv_usec / 1000); + } + + if (use_account && from && cli_user(from)) { + if (pos > 1 && pos < (int)buflen - 1) + buf[pos++] = ';'; + if (IsAccount(from)) + pos += snprintf(buf + pos, buflen - pos, "account=%s", cli_user(from)->account); + else + pos += snprintf(buf + pos, buflen - pos, "account=*"); + } + + /* Add @bot tag if sender has +B mode (IRCv3 bot-mode spec) */ + if (use_bot) { + if (pos > 1 && pos < (int)buflen - 1) + buf[pos++] = ';'; + pos += snprintf(buf + pos, buflen - pos, "bot"); + } + + if (pos < (int)buflen - 1) { + buf[pos++] = ' '; + buf[pos] = '\0'; + } + + return buf; +} + +/** Get the tag flags appropriate for a client based on their capabilities. + * @param[in] to Recipient client. + * @param[in] from Source client (for bot detection). + * @param[in] include_batch Whether to include batch tag if network batch is active. + * @return TAGS_* flags for this client. + */ +static int get_client_tag_flags(struct Client *to, struct Client *from, int include_batch) +{ + int flags = 0; + + if (feature_bool(FEAT_CAP_server_time) && CapActive(to, CAP_SERVERTIME)) + flags |= TAGS_TIME; + if (feature_bool(FEAT_CAP_account_tag) && CapActive(to, CAP_ACCOUNTTAG)) + flags |= TAGS_ACCOUNT; + if (include_batch && CapActive(to, CAP_BATCH) && active_network_batch_id[0]) + flags |= TAGS_BATCH; + /* Bot tag is sent to any client that gets any tags */ + if (flags && from && IsBot(from)) + flags |= TAGS_BOT; + + return flags; +} + +/** Generate a unique message ID for IRCv3 message-ids. + * Format: -- + * @param[out] buf Buffer to write message ID to. + * @param[in] buflen Size of buffer. + * @return Pointer to buf. + */ +char *generate_msgid(char *buf, size_t buflen) +{ + snprintf(buf, buflen, "%s-%lu-%lu", + cli_yxx(&me), + (unsigned long)cli_firsttime(&me), + ++MsgIdCounter); + return buf; +} + +/** Format message tags for S2S (server-to-server) relay. + * If the message came from another server with tags, preserve them. + * Otherwise, generate new @time and @msgid tags. + * @param[out] buf Buffer for tag string (includes trailing space). + * @param[in] buflen Size of buffer. + * @param[in] cptr Server connection the message came from (for incoming tags). + * @param[out] msgid_out Optional: buffer to store the msgid used (for echo-message). + * @param[in] msgid_out_len Size of msgid_out buffer. + * @return Pointer to buf, or NULL if P10_MESSAGE_TAGS is disabled. + */ +static char *format_s2s_tags(char *buf, size_t buflen, struct Client *cptr, + char *msgid_out, size_t msgid_out_len) +{ + int pos = 0; + char timebuf[32]; + char msgidbuf[64]; + const char *time_tag = NULL; + const char *msgid_tag = NULL; + + /* Check if P10 message tags are enabled */ + if (!feature_bool(FEAT_P10_MESSAGE_TAGS)) + return NULL; + + /* Check for incoming S2S tags from cptr */ + if (cptr && cli_s2s_time(cptr)[0]) + time_tag = cli_s2s_time(cptr); + if (cptr && cli_s2s_msgid(cptr)[0]) + msgid_tag = cli_s2s_msgid(cptr); + + /* Generate new tags if not present */ + if (!time_tag) { + struct timeval tv; + struct tm tm; + gettimeofday(&tv, NULL); + gmtime_r(&tv.tv_sec, &tm); + snprintf(timebuf, sizeof(timebuf), "%04d-%02d-%02dT%02d:%02d:%02d.%03ldZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, + tv.tv_usec / 1000); + time_tag = timebuf; + } + + if (!msgid_tag) { + generate_msgid(msgidbuf, sizeof(msgidbuf)); + msgid_tag = msgidbuf; + } + + /* Store msgid for caller if requested (for echo-message) */ + if (msgid_out && msgid_out_len > 0) { + ircd_strncpy(msgid_out, msgid_tag, msgid_out_len - 1); + msgid_out[msgid_out_len - 1] = '\0'; + } + + /* Format the tag string with trailing space */ + pos = snprintf(buf, buflen, "@time=%s;msgid=%s ", time_tag, msgid_tag); + if (pos >= (int)buflen) + buf[buflen - 1] = '\0'; + + return buf; +} + +/** Format message tags for a specific recipient, including label if applicable. + * @param[out] buf Buffer for tag string. + * @param[in] buflen Size of buffer. + * @param[in] from Source client (for account tag). + * @param[in] to Recipient client (for label tag). + * @param[in] msgid Message ID to include, or NULL for none. + * @return Pointer to buf, or NULL if no tags to add. + */ +static char *format_message_tags_for_ex(char *buf, size_t buflen, struct Client *from, + struct Client *to, const char *msgid) +{ + int use_time = feature_bool(FEAT_CAP_server_time) && CapActive(to, CAP_SERVERTIME); + int use_account = feature_bool(FEAT_CAP_account_tag) && CapActive(to, CAP_ACCOUNTTAG); + int use_label = feature_bool(FEAT_CAP_labeled_response) && + CapActive(to, CAP_LABELEDRESP) && + to && MyConnect(to) && cli_label(to)[0]; + int use_batch = feature_bool(FEAT_CAP_batch) && CapActive(to, CAP_BATCH) && + to && MyConnect(to) && cli_batch_id(to)[0]; + int use_msgid = msgid && *msgid; + int pos = 0; + + if (!use_time && !use_account && !use_label && !use_batch && !use_msgid) + return NULL; + + buf[0] = '@'; + pos = 1; + + /* When in a batch, use @batch instead of @label */ + if (use_batch) { + pos += snprintf(buf + pos, buflen - pos, "batch=%s", cli_batch_id(to)); + } else if (use_label) { + pos += snprintf(buf + pos, buflen - pos, "label=%s", cli_label(to)); + } + + /* Add @msgid for message tracking (IRCv3 message-ids) */ + if (use_msgid) { + if (pos > 1 && pos < (int)buflen - 1) + buf[pos++] = ';'; + pos += snprintf(buf + pos, buflen - pos, "msgid=%s", msgid); + } + + if (use_time) { + struct timeval tv; + struct tm tm; + if (pos > 1 && pos < (int)buflen - 1) + buf[pos++] = ';'; + gettimeofday(&tv, NULL); + gmtime_r(&tv.tv_sec, &tm); + pos += snprintf(buf + pos, buflen - pos, + "time=%04d-%02d-%02dT%02d:%02d:%02d.%03ldZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, + tv.tv_usec / 1000); + } + + if (use_account && from && cli_user(from)) { + if (pos > 1 && pos < (int)buflen - 1) + buf[pos++] = ';'; + if (IsAccount(from)) { + pos += snprintf(buf + pos, buflen - pos, "account=%s", + cli_user(from)->account); + } else { + pos += snprintf(buf + pos, buflen - pos, "account=*"); + } + } + + /* Add @bot tag if sender has +B mode (IRCv3 bot-mode spec) */ + if (from && IsBot(from)) { + if (pos > 1 && pos < (int)buflen - 1) + buf[pos++] = ';'; + pos += snprintf(buf + pos, buflen - pos, "bot"); + } + + if (pos < (int)buflen - 1) { + buf[pos++] = ' '; + buf[pos] = '\0'; + } + + return buf; +} + +/** Format message tags for a specific recipient (wrapper without msgid). + * @param[out] buf Buffer for tag string. + * @param[in] buflen Size of buffer. + * @param[in] from Source client (for account tag). + * @param[in] to Recipient client (for label tag). + * @return Pointer to buf, or NULL if no tags to add. + */ +static char *format_message_tags_for(char *buf, size_t buflen, struct Client *from, struct Client *to) +{ + return format_message_tags_for_ex(buf, buflen, from, to, NULL); +} + +/** Format message tags including client-only tags for TAGMSG relay. + * @param[out] buf Buffer for tag string. + * @param[in] buflen Size of buffer. + * @param[in] from Source client (for account tag and client tags). + * @param[in] to Recipient client (for label tag). + * @param[in] client_tags Client-only tags string (e.g., "+typing=active;+reply=msgid"). + * @return Pointer to buf, or NULL if no tags to add. + */ +static char *format_message_tags_with_client(char *buf, size_t buflen, struct Client *from, + struct Client *to, const char *client_tags) +{ + int use_time = feature_bool(FEAT_CAP_server_time) && CapActive(to, CAP_SERVERTIME); + int use_account = feature_bool(FEAT_CAP_account_tag) && CapActive(to, CAP_ACCOUNTTAG); + int use_label = feature_bool(FEAT_CAP_labeled_response) && + CapActive(to, CAP_LABELEDRESP) && + to && MyConnect(to) && cli_label(to)[0]; + int use_batch = feature_bool(FEAT_CAP_batch) && CapActive(to, CAP_BATCH) && + to && MyConnect(to) && cli_batch_id(to)[0]; + int use_client_tags = client_tags && *client_tags; + int pos = 0; + + /* TAGMSG is only useful if there are client-only tags to relay */ + if (!use_client_tags && !use_time && !use_account && !use_label && !use_batch) + return NULL; + + buf[0] = '@'; + pos = 1; + + /* Client-only tags first (these are the primary content for TAGMSG) */ + if (use_client_tags) { + pos += snprintf(buf + pos, buflen - pos, "%s", client_tags); + } + + /* When in a batch, use @batch instead of @label */ + if (use_batch) { + if (pos > 1 && pos < (int)buflen - 1) + buf[pos++] = ';'; + pos += snprintf(buf + pos, buflen - pos, "batch=%s", cli_batch_id(to)); + } else if (use_label) { + if (pos > 1 && pos < (int)buflen - 1) + buf[pos++] = ';'; + pos += snprintf(buf + pos, buflen - pos, "label=%s", cli_label(to)); + } + + if (use_time) { + struct timeval tv; + struct tm tm; + if (pos > 1 && pos < (int)buflen - 1) + buf[pos++] = ';'; + gettimeofday(&tv, NULL); + gmtime_r(&tv.tv_sec, &tm); + pos += snprintf(buf + pos, buflen - pos, + "time=%04d-%02d-%02dT%02d:%02d:%02d.%03ldZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, + tv.tv_usec / 1000); + } + + if (use_account && from && cli_user(from)) { + if (pos > 1 && pos < (int)buflen - 1) + buf[pos++] = ';'; + if (IsAccount(from)) { + pos += snprintf(buf + pos, buflen - pos, "account=%s", + cli_user(from)->account); + } else { + pos += snprintf(buf + pos, buflen - pos, "account=*"); + } + } + + /* Add @bot tag if sender has +B mode (IRCv3 bot-mode spec) */ + if (from && IsBot(from)) { + if (pos > 1 && pos < (int)buflen - 1) + buf[pos++] = ';'; + pos += snprintf(buf + pos, buflen - pos, "bot"); + } + + if (pos < (int)buflen - 1) { + buf[pos++] = ' '; + buf[pos] = '\0'; + } + + return buf; +} + /* * dead_link * @@ -321,14 +735,185 @@ void sendcmdto_one(struct Client *from, const char *cmd, const char *tok, { struct VarData vd; struct MsgBuf *mb; + struct Client *cptr; + char s2s_tagbuf[128]; to = cli_from(to); vd.vd_format = pattern; /* set up the struct VarData for %v */ va_start(vd.vd_args, pattern); - mb = msgq_make(to, "%:#C %s %v", from, IsServer(to) || IsMe(to) ? tok : cmd, - &vd); + /* For S2S messages (PRIVMSG/NOTICE to servers), add S2S tags */ + if ((IsServer(to) || IsMe(to)) && + (strcmp(tok, TOK_PRIVATE) == 0 || strcmp(tok, TOK_NOTICE) == 0) && + feature_bool(FEAT_P10_MESSAGE_TAGS)) { + /* Get incoming server connection for tag preservation */ + cptr = MyConnect(from) ? NULL : cli_from(from); + if (format_s2s_tags(s2s_tagbuf, sizeof(s2s_tagbuf), cptr, NULL, 0)) { + mb = msgq_make(to, "%s%:#C %s %v", s2s_tagbuf, from, tok, &vd); + } else { + mb = msgq_make(to, "%:#C %s %v", from, tok, &vd); + } + } else { + mb = msgq_make(to, "%:#C %s %v", from, IsServer(to) || IsMe(to) ? tok : cmd, + &vd); + } + + va_end(vd.vd_args); + + send_buffer(to, mb, 0); + + msgq_clean(mb); +} + +/** + * Send a (prefixed) command to a single client with message tags. + * Includes @label, @time, and @account tags if the recipient supports them. + * @param[in] from Client sending the command. + * @param[in] cmd Long name of command (used if \a to is a user). + * @param[in] tok Short name of command (used if \a to is a server). + * @param[in] to Destination of command. + * @param[in] pattern Format string for command arguments. + */ +void sendcmdto_one_tags(struct Client *from, const char *cmd, const char *tok, + struct Client *to, const char *pattern, ...) +{ + struct VarData vd; + struct MsgBuf *mb; + char tagbuf[512]; + char msgidbuf[64]; + char *tags; + const char *msgid = NULL; + + to = cli_from(to); + + vd.vd_format = pattern; /* set up the struct VarData for %v */ + va_start(vd.vd_args, pattern); + + /* Generate msgid for PRIVMSG and NOTICE if feature enabled */ + if (feature_bool(FEAT_MSGID) && + (cmd == CMD_PRIVATE || cmd == CMD_NOTICE)) { + msgid = generate_msgid(msgidbuf, sizeof(msgidbuf)); + } + + tags = format_message_tags_for_ex(tagbuf, sizeof(tagbuf), from, to, msgid); + + if (tags) + mb = msgq_make(to, "%s%:#C %s %v", tags, from, IsServer(to) || IsMe(to) ? tok : cmd, + &vd); + else + mb = msgq_make(to, "%:#C %s %v", from, IsServer(to) || IsMe(to) ? tok : cmd, + &vd); + + va_end(vd.vd_args); + + send_buffer(to, mb, 0); + + msgq_clean(mb); +} + +/** Send a (prefixed) command to a single local client with message tags, + * returning the generated msgid. + * @param[in] from Client originating the message. + * @param[in] cmd Long name of command. + * @param[in] tok Short name of command (used if \a to is a server or &me). + * @param[in] to Destination of command. + * @param[out] msgid_out Buffer to store generated msgid. + * @param[in] msgid_out_len Size of msgid_out buffer. + * @param[out] time_out Buffer to store generated timestamp. + * @param[in] time_out_len Size of time_out buffer. + * @param[in] pattern Format string for command arguments. + */ +void sendcmdto_one_tags_msgid(struct Client *from, const char *cmd, const char *tok, + struct Client *to, char *msgid_out, size_t msgid_out_len, + char *time_out, size_t time_out_len, + const char *pattern, ...) +{ + struct VarData vd; + struct MsgBuf *mb; + char tagbuf[512]; + char msgidbuf[64]; + char timebuf[32]; + char *tags; + const char *msgid = NULL; + struct timeval tv; + struct tm tm; + + to = cli_from(to); + + vd.vd_format = pattern; + va_start(vd.vd_args, pattern); + + /* Generate msgid for PRIVMSG and NOTICE if feature enabled */ + if (feature_bool(FEAT_MSGID) && + (strcmp(cmd, MSG_PRIVATE) == 0 || strcmp(cmd, MSG_NOTICE) == 0)) { + msgid = generate_msgid(msgidbuf, sizeof(msgidbuf)); + if (msgid_out && msgid_out_len > 0) { + ircd_strncpy(msgid_out, msgid, msgid_out_len - 1); + msgid_out[msgid_out_len - 1] = '\0'; + } + } else if (msgid_out && msgid_out_len > 0) { + msgid_out[0] = '\0'; + } + + /* Generate timestamp - ISO for client @time= tag, Unix for storage */ + gettimeofday(&tv, NULL); + gmtime_r(&tv.tv_sec, &tm); + snprintf(timebuf, sizeof(timebuf), "%04d-%02d-%02dT%02d:%02d:%02d.%03ldZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, + tv.tv_usec / 1000); + /* Return Unix timestamp for internal storage/S2S */ + if (time_out && time_out_len > 0) { + snprintf(time_out, time_out_len, "%lu.%03lu", + (unsigned long)tv.tv_sec, (unsigned long)(tv.tv_usec / 1000)); + } + + tags = format_message_tags_for_ex(tagbuf, sizeof(tagbuf), from, to, msgid); + + if (tags) + mb = msgq_make(to, "%s%:#C %s %v", tags, from, IsServer(to) || IsMe(to) ? tok : cmd, + &vd); + else + mb = msgq_make(to, "%:#C %s %v", from, IsServer(to) || IsMe(to) ? tok : cmd, + &vd); + + va_end(vd.vd_args); + + send_buffer(to, mb, 0); + + msgq_clean(mb); +} + +/** + * Send TAGMSG with client-only tags to a single local client. + * Used for relaying +typing and other client-only tags. + * @param[in] from Client sending the TAGMSG. + * @param[in] cmd Long name of command (TAGMSG). + * @param[in] to Destination of command. + * @param[in] client_tags Client-only tags string from sender (e.g., "+typing=active"). + * @param[in] pattern Format string for command arguments. + */ +void sendcmdto_one_client_tags(struct Client *from, const char *cmd, + struct Client *to, const char *client_tags, + const char *pattern, ...) +{ + struct VarData vd; + struct MsgBuf *mb; + char tagbuf[1024]; + char *tags; + + to = cli_from(to); + + vd.vd_format = pattern; + va_start(vd.vd_args, pattern); + + tags = format_message_tags_with_client(tagbuf, sizeof(tagbuf), from, to, client_tags); + + if (tags) + mb = msgq_make(to, "%s%:#C %s %v", tags, from, cmd, &vd); + else + mb = msgq_make(to, "%:#C %s %v", from, cmd, &vd); va_end(vd.vd_args); @@ -475,8 +1060,12 @@ void sendcmdto_common_channels_butone(struct Client *from, const char *cmd, { struct VarData vd; struct MsgBuf *mb; + /* Per-capability message buffers - only send tags client actually requested */ + struct MsgBuf *mb_cache[16] = {0}; /* Indexed by TAGS_* flag combinations */ struct Membership *chan; struct Membership *member; + char tagbuf[128]; + int flags; assert(0 != from); assert(0 != cli_from(from)); @@ -487,7 +1076,7 @@ void sendcmdto_common_channels_butone(struct Client *from, const char *cmd, va_start(vd.vd_args, pattern); - /* build the buffer */ + /* build the base buffer (no tags) */ mb = msgq_make(0, "%:#C %s %v", from, cmd, &vd); va_end(vd.vd_args); @@ -505,14 +1094,39 @@ void sendcmdto_common_channels_butone(struct Client *from, const char *cmd, && member->user != one && cli_sentalong(member->user) != sentalong_marker) { cli_sentalong(member->user) = sentalong_marker; - send_buffer(member->user, mb, 0); + flags = get_client_tag_flags(member->user, from, 1); + if (flags) { + /* Build cached message buffer for this flag combination if needed */ + if (!mb_cache[flags]) { + if (format_message_tags_ex(tagbuf, sizeof(tagbuf), from, flags)) { + va_start(vd.vd_args, pattern); + mb_cache[flags] = msgq_make(0, "%s%:#C %s %v", tagbuf, from, cmd, &vd); + va_end(vd.vd_args); + } + } + if (mb_cache[flags]) + send_buffer(member->user, mb_cache[flags], 0); + else + send_buffer(member->user, mb, 0); + } else { + send_buffer(member->user, mb, 0); + } } } - if (MyConnect(from) && from != one) - send_buffer(from, mb, 0); + if (MyConnect(from) && from != one) { + flags = get_client_tag_flags(from, from, 1); + if (flags && mb_cache[flags]) + send_buffer(from, mb_cache[flags], 0); + else + send_buffer(from, mb, 0); + } msgq_clean(mb); + for (flags = 0; flags < 16; flags++) { + if (mb_cache[flags]) + msgq_clean(mb_cache[flags]); + } } /** Send a (prefixed) command to all channels that \a from is on. @@ -529,8 +1143,12 @@ void sendcmdto_common_channels_capab_butone(struct Client *from, const char *cmd { struct VarData vd; struct MsgBuf *mb; + /* Per-capability message buffers - only send tags client actually requested */ + struct MsgBuf *mb_cache[16] = {0}; /* Indexed by TAGS_* flag combinations */ struct Membership *chan; struct Membership *member; + char tagbuf[128]; + int flags; assert(0 != from); assert(0 != cli_from(from)); @@ -541,7 +1159,7 @@ void sendcmdto_common_channels_capab_butone(struct Client *from, const char *cmd va_start(vd.vd_args, pattern); - /* build the buffer */ + /* build the base buffer (no tags) */ mb = msgq_make(0, "%:#C %s %v", from, cmd, &vd); va_end(vd.vd_args); @@ -561,14 +1179,39 @@ void sendcmdto_common_channels_capab_butone(struct Client *from, const char *cmd && ((withcap == CAP_NONE) || CapActive(member->user, withcap)) && ((skipcap == CAP_NONE) || !CapActive(member->user, skipcap))) { cli_sentalong(member->user) = sentalong_marker; - send_buffer(member->user, mb, 0); + flags = get_client_tag_flags(member->user, from, 0); + if (flags) { + /* Build cached message buffer for this flag combination if needed */ + if (!mb_cache[flags]) { + if (format_message_tags_ex(tagbuf, sizeof(tagbuf), from, flags)) { + va_start(vd.vd_args, pattern); + mb_cache[flags] = msgq_make(0, "%s%:#C %s %v", tagbuf, from, cmd, &vd); + va_end(vd.vd_args); + } + } + if (mb_cache[flags]) + send_buffer(member->user, mb_cache[flags], 0); + else + send_buffer(member->user, mb, 0); + } else { + send_buffer(member->user, mb, 0); + } } } - if (MyConnect(from) && from != one) - send_buffer(from, mb, 0); + if (MyConnect(from) && from != one) { + flags = get_client_tag_flags(from, from, 0); + if (flags && mb_cache[flags]) + send_buffer(from, mb_cache[flags], 0); + else + send_buffer(from, mb, 0); + } msgq_clean(mb); + for (flags = 0; flags < 16; flags++) { + if (mb_cache[flags]) + msgq_clean(mb_cache[flags]); + } } /** Send a (prefixed) command to all local users on a channel. @@ -587,29 +1230,54 @@ void sendcmdto_channel_butserv_butone(struct Client *from, const char *cmd, { struct VarData vd; struct MsgBuf *mb; + /* Per-capability message buffers - only send tags client actually requested */ + struct MsgBuf *mb_cache[16] = {0}; /* Indexed by TAGS_* flag combinations */ struct Membership *member; + char tagbuf[128]; + int flags; vd.vd_format = pattern; /* set up the struct VarData for %v */ va_start(vd.vd_args, pattern); - /* build the buffer */ + /* build the base buffer (no tags) */ mb = msgq_make(0, "%:#C %s %v", from, cmd, &vd); va_end(vd.vd_args); /* send the buffer to each local channel member */ for (member = to->members; member; member = member->next_member) { if (!MyConnect(member->user) - || member->user == one + || member->user == one || IsZombie(member) || (skip & SKIP_DEAF && IsDeaf(member->user)) || (skip & SKIP_NONOPS && !IsChanOp(member)) || (skip & SKIP_NONHOPS && !IsChanOp(member) && !IsHalfOp(member)) - || (skip & SKIP_NONVOICES && !IsChanOp(member) && !IsHalfOp(member)&& !HasVoice(member))) + || (skip & SKIP_NONVOICES && !IsChanOp(member) && !IsHalfOp(member)&& !HasVoice(member)) + || (skip & SKIP_CHGHOST && CapActive(member->user, CAP_CHGHOST))) continue; + flags = get_client_tag_flags(member->user, from, 0); + if (flags) { + /* Build cached message buffer for this flag combination if needed */ + if (!mb_cache[flags]) { + if (format_message_tags_ex(tagbuf, sizeof(tagbuf), from, flags)) { + va_start(vd.vd_args, pattern); + mb_cache[flags] = msgq_make(0, "%s%:#C %s %v", tagbuf, from, cmd, &vd); + va_end(vd.vd_args); + } + } + if (mb_cache[flags]) + send_buffer(member->user, mb_cache[flags], 0); + else + send_buffer(member->user, mb, 0); + } else { send_buffer(member->user, mb, 0); + } } msgq_clean(mb); + for (flags = 0; flags < 16; flags++) { + if (mb_cache[flags]) + msgq_clean(mb_cache[flags]); + } } /** Send a (prefixed) command to all local users on a channel with or without @@ -632,12 +1300,16 @@ void sendcmdto_channel_capab_butserv_butone(struct Client *from, const char *cmd { struct VarData vd; struct MsgBuf *mb; + /* Per-capability message buffers - only send tags client actually requested */ + struct MsgBuf *mb_cache[16] = {0}; /* Indexed by TAGS_* flag combinations */ struct Membership *member; + char tagbuf[128]; + int flags; vd.vd_format = pattern; /* set up the struct VarData for %v */ va_start(vd.vd_args, pattern); - /* build the buffer */ + /* build the base buffer (no tags) */ mb = msgq_make(0, "%:#C %s %v", from, cmd, &vd); va_end(vd.vd_args); @@ -650,13 +1322,82 @@ void sendcmdto_channel_capab_butserv_butone(struct Client *from, const char *cmd || (skip & SKIP_NONOPS && !IsChanOp(member)) || (skip & SKIP_NONHOPS && !IsChanOp(member) && !IsHalfOp(member)) || (skip & SKIP_NONVOICES && !IsChanOp(member) && !IsHalfOp(member)&& !HasVoice(member)) + || (skip & SKIP_CHGHOST && CapActive(member->user, CAP_CHGHOST)) || ((withcap != CAP_NONE) && !CapActive(member->user, withcap)) || ((skipcap != CAP_NONE) && CapActive(member->user, skipcap))) continue; + flags = get_client_tag_flags(member->user, from, 0); + if (flags) { + /* Build cached message buffer for this flag combination if needed */ + if (!mb_cache[flags]) { + if (format_message_tags_ex(tagbuf, sizeof(tagbuf), from, flags)) { + va_start(vd.vd_args, pattern); + mb_cache[flags] = msgq_make(0, "%s%:#C %s %v", tagbuf, from, cmd, &vd); + va_end(vd.vd_args); + } + } + if (mb_cache[flags]) + send_buffer(member->user, mb_cache[flags], 0); + else + send_buffer(member->user, mb, 0); + } else { send_buffer(member->user, mb, 0); + } } msgq_clean(mb); + for (flags = 0; flags < 16; flags++) { + if (mb_cache[flags]) + msgq_clean(mb_cache[flags]); + } +} + +/** Send TAGMSG with client-only tags to channel members with message-tags capability. + * Used for relaying +typing and other client-only tags to channels. + * @param[in] from Client originating the TAGMSG. + * @param[in] cmd Long name of command (TAGMSG). + * @param[in] to Destination channel. + * @param[in] one Client direction to skip (or NULL). + * @param[in] skip Bitmask of SKIP_DEAF, SKIP_NONOPS, SKIP_NONVOICES indicating which clients to skip. + * @param[in] client_tags Client-only tags string from sender (e.g., "+typing=active"). + * @param[in] pattern Format string for command arguments. + */ +void sendcmdto_channel_client_tags(struct Client *from, const char *cmd, + struct Channel *to, struct Client *one, + unsigned int skip, const char *client_tags, + const char *pattern, ...) +{ + struct VarData vd; + struct MsgBuf *mb; + struct Membership *member; + char tagbuf[1024]; + + vd.vd_format = pattern; + va_start(vd.vd_args, pattern); + + /* Send to each local channel member with message-tags capability */ + for (member = to->members; member; member = member->next_member) { + if (!MyConnect(member->user) + || member->user == one + || IsZombie(member) + || (skip & SKIP_DEAF && IsDeaf(member->user)) + || (skip & SKIP_NONOPS && !IsChanOp(member)) + || (skip & SKIP_NONHOPS && !IsChanOp(member) && !IsHalfOp(member)) + || (skip & SKIP_NONVOICES && !IsChanOp(member) && !IsHalfOp(member) && !HasVoice(member)) + || !wants_message_tags(member->user)) + continue; + + /* Build message with client-only tags for this recipient */ + if (format_message_tags_with_client(tagbuf, sizeof(tagbuf), from, member->user, client_tags)) { + va_start(vd.vd_args, pattern); + mb = msgq_make(0, "%s%:#C %s %v", tagbuf, from, cmd, &vd); + va_end(vd.vd_args); + send_buffer(member->user, mb, 0); + msgq_clean(mb); + } + } + + va_end(vd.vd_args); } /** Send a (prefixed) command to all servers with users on \a to. @@ -722,13 +1463,24 @@ void sendcmdto_channel_butone(struct Client *from, const char *cmd, struct Membership *member; struct VarData vd; struct MsgBuf *user_mb; + /* Per-capability message buffers - only send tags client actually requested */ + struct MsgBuf *user_mb_cache[16] = {0}; /* Indexed by TAGS_* flag combinations */ struct MsgBuf *serv_mb; + struct MsgBuf *serv_mb_tags = NULL; /* S2S tagged version */ struct Client *service; + struct Client *cptr; /* Server connection for incoming S2S tags */ const char *userfmt; const char *usercmd; + char tagbuf[128]; + char s2s_tagbuf[128]; + char userfmt_tags[64]; + int tflags; vd.vd_format = pattern; + /* Get the server connection for S2S tag handling */ + cptr = MyConnect(from) ? NULL : cli_from(from); + /* Build buffer to send to users */ usercmd = cmd; userfmt = "%:#C %s %v"; @@ -746,10 +1498,20 @@ void sendcmdto_channel_butone(struct Client *from, const char *cmd, user_mb = msgq_make(0, userfmt, from, usercmd, &vd); va_end(vd.vd_args); - /* Build buffer to send to servers */ - va_start(vd.vd_args, pattern); - serv_mb = msgq_make(&me, "%C %s %v", from, tok, &vd); - va_end(vd.vd_args); + /* Prepare tagged format string for building cached buffers */ + ircd_snprintf(0, userfmt_tags, sizeof(userfmt_tags), "%%s%s", userfmt); + + /* Build buffer to send to servers - with S2S tags if enabled */ + if (format_s2s_tags(s2s_tagbuf, sizeof(s2s_tagbuf), cptr, NULL, 0)) { + va_start(vd.vd_args, pattern); + serv_mb_tags = msgq_make(&me, "%s%C %s %v", s2s_tagbuf, from, tok, &vd); + va_end(vd.vd_args); + serv_mb = serv_mb_tags; /* Use tagged version */ + } else { + va_start(vd.vd_args, pattern); + serv_mb = msgq_make(&me, "%C %s %v", from, tok, &vd); + va_end(vd.vd_args); + } /* send buffer along! */ bump_sentalong(one); @@ -767,9 +1529,25 @@ void sendcmdto_channel_butone(struct Client *from, const char *cmd, continue; cli_sentalong(member->user) = sentalong_marker; - if (MyConnect(member->user)) /* pick right buffer to send */ - send_buffer(member->user, user_mb, 0); - else + if (MyConnect(member->user)) { /* pick right buffer to send */ + tflags = get_client_tag_flags(member->user, from, 0); + if (tflags) { + /* Build cached message buffer for this flag combination if needed */ + if (!user_mb_cache[tflags]) { + if (format_message_tags_ex(tagbuf, sizeof(tagbuf), from, tflags)) { + va_start(vd.vd_args, pattern); + user_mb_cache[tflags] = msgq_make(0, userfmt_tags, tagbuf, from, usercmd, &vd); + va_end(vd.vd_args); + } + } + if (user_mb_cache[tflags]) + send_buffer(member->user, user_mb_cache[tflags], 0); + else + send_buffer(member->user, user_mb, 0); + } else { + send_buffer(member->user, user_mb, 0); + } + } else send_buffer(member->user, serv_mb, 0); } /* Consult service forwarding table. */ @@ -781,6 +1559,10 @@ void sendcmdto_channel_butone(struct Client *from, const char *cmd, } msgq_clean(user_mb); + for (tflags = 0; tflags < 16; tflags++) { + if (user_mb_cache[tflags]) + msgq_clean(user_mb_cache[tflags]); + } msgq_clean(serv_mb); } @@ -1089,3 +1871,531 @@ void vsendto_mode_butone(struct Client *one, struct Client *from, const char *mo msgq_clean(mb); } +/** + * Generate a unique batch reference ID. + * @param[in] cptr Client to generate batch ID for. + * @param[out] buf Buffer to store the generated ID. + * @param[in] buflen Size of the buffer. + * @return Pointer to the buffer. + */ +static char *generate_batch_id(struct Client *cptr, char *buf, size_t buflen) +{ + unsigned int seq = con_batch_seq(cli_connect(cptr))++; + ircd_snprintf(NULL, buf, buflen, "%s%u", cli_yxx(cptr), seq); + return buf; +} + +/** + * Start a batch for a client. + * Sends BATCH +refid type to the client and stores the batch ID. + * @param[in] to Client to start batch for. + * @param[in] type Batch type (e.g., "labeled-response", "netjoin"). + */ +void send_batch_start(struct Client *to, const char *type) +{ + struct MsgBuf *mb; + char tagbuf[256]; + int pos = 0; + + if (!feature_bool(FEAT_CAP_batch) || !CapActive(to, CAP_BATCH) || !MyConnect(to)) + return; + + /* Generate a new batch ID */ + generate_batch_id(to, cli_batch_id(to), sizeof(con_batch_id(cli_connect(to)))); + + /* Build message tags - include label if this is for labeled-response */ + tagbuf[0] = '\0'; + if (feature_bool(FEAT_CAP_labeled_response) && + CapActive(to, CAP_LABELEDRESP) && cli_label(to)[0]) { + tagbuf[0] = '@'; + pos = 1; + pos += ircd_snprintf(NULL, tagbuf + pos, sizeof(tagbuf) - pos, "label=%s", cli_label(to)); + if (pos < (int)sizeof(tagbuf) - 1) { + tagbuf[pos++] = ' '; + tagbuf[pos] = '\0'; + } + } + + /* Send BATCH +refid type */ + if (tagbuf[0]) + mb = msgq_make(cli_from(to), "%s:%s " MSG_BATCH_CMD " +%s %s", + tagbuf, cli_name(&me), cli_batch_id(to), type); + else + mb = msgq_make(cli_from(to), ":%s " MSG_BATCH_CMD " +%s %s", + cli_name(&me), cli_batch_id(to), type); + + send_buffer(to, mb, 0); + msgq_clean(mb); +} + +/** + * End the current batch for a client. + * Sends BATCH -refid to the client and clears the batch ID. + * @param[in] to Client to end batch for. + */ +void send_batch_end(struct Client *to) +{ + struct MsgBuf *mb; + + if (!feature_bool(FEAT_CAP_batch) || !CapActive(to, CAP_BATCH) || !MyConnect(to)) + return; + + /* Only end if there's an active batch */ + if (!cli_batch_id(to)[0]) + return; + + /* Send BATCH -refid */ + mb = msgq_make(cli_from(to), ":%s " MSG_BATCH_CMD " -%s", + cli_name(&me), cli_batch_id(to)); + + send_buffer(to, mb, 0); + msgq_clean(mb); + + /* Clear the batch ID */ + cli_batch_id(to)[0] = '\0'; +} + +/** + * Check if a client has an active batch. + * @param[in] cptr Client to check. + * @return Non-zero if batch is active, zero otherwise. + */ +int has_active_batch(struct Client *cptr) +{ + if (!MyConnect(cptr)) + return 0; + return cli_batch_id(cptr)[0] != '\0'; +} + +/** + * Start an S2S batch and send to all servers. + * Used for netjoin/netsplit coordination across the network. + * @param[in] sptr Server starting the batch. + * @param[in] type Batch type (netjoin, netsplit). + * @param[in] server1 First server in the split/join (optional). + * @param[in] server2 Second server in the split/join (optional). + */ +void send_s2s_batch_start(struct Client *sptr, const char *type, + const char *server1, const char *server2) +{ + char batch_id[32]; + struct Client *acptr; + + if (!feature_bool(FEAT_P10_MESSAGE_TAGS)) + return; + + /* Generate unique batch ID using server numeric + timestamp + counter */ + generate_batch_id(sptr, batch_id, sizeof(batch_id)); + + /* Send to all servers */ + if (server1 && server2) { + sendcmdto_serv_butone(sptr, CMD_BATCH_CMD, NULL, "+%s %s %s %s", + batch_id, type, server1, server2); + } + else if (server1) { + sendcmdto_serv_butone(sptr, CMD_BATCH_CMD, NULL, "+%s %s %s", + batch_id, type, server1); + } + else { + sendcmdto_serv_butone(sptr, CMD_BATCH_CMD, NULL, "+%s %s", + batch_id, type); + } + + /* Store batch ID for later reference */ + ircd_strncpy(cli_s2s_batch_id(sptr), batch_id, sizeof(con_s2s_batch_id(cli_connect(sptr))) - 1); + cli_s2s_batch_id(sptr)[sizeof(con_s2s_batch_id(cli_connect(sptr))) - 1] = '\0'; + ircd_strncpy(cli_s2s_batch_type(sptr), type, sizeof(con_s2s_batch_type(cli_connect(sptr))) - 1); + cli_s2s_batch_type(sptr)[sizeof(con_s2s_batch_type(cli_connect(sptr))) - 1] = '\0'; + + /* Send batch start to local clients with batch capability */ + for (acptr = GlobalClientList; acptr; acptr = cli_next(acptr)) { + if (!MyConnect(acptr) || !IsUser(acptr)) + continue; + if (!CapActive(acptr, CAP_BATCH)) + continue; + + if (server1 && server2) { + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "+%s %s %s %s", + batch_id, type, server1, server2); + } + else if (server1) { + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "+%s %s %s", + batch_id, type, server1); + } + else { + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "+%s %s", + batch_id, type); + } + } +} + +/** + * End an S2S batch and send to all servers. + * @param[in] sptr Server ending the batch. + * @param[in] batch_id Batch ID to end (or NULL to use stored ID). + */ +void send_s2s_batch_end(struct Client *sptr, const char *batch_id) +{ + struct Client *acptr; + const char *id; + + if (!feature_bool(FEAT_P10_MESSAGE_TAGS)) + return; + + /* Use provided ID or the stored one */ + id = batch_id ? batch_id : cli_s2s_batch_id(sptr); + if (!id || !*id) + return; + + /* Send to all servers */ + sendcmdto_serv_butone(sptr, CMD_BATCH_CMD, NULL, "-%s", id); + + /* Send batch end to local clients with batch capability */ + for (acptr = GlobalClientList; acptr; acptr = cli_next(acptr)) { + if (!MyConnect(acptr) || !IsUser(acptr)) + continue; + if (!CapActive(acptr, CAP_BATCH)) + continue; + + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "-%s", id); + } + + /* Clear stored batch ID */ + cli_s2s_batch_id(sptr)[0] = '\0'; + cli_s2s_batch_type(sptr)[0] = '\0'; +} + +/** + * Start a netjoin batch when a server reconnects. + * Generates a batch ID and stores it on the server struct for later. + * Sends BATCH +id netjoin server1 server2 to clients with batch cap. + * @param[in] server Server that is reconnecting (junction server). + * @param[in] uplink Server's uplink (server one hop closer to us). + */ +void send_netjoin_batch_start(struct Client *server, struct Client *uplink) +{ + struct Client *acptr; + char batch_id[32]; + static unsigned long netjoin_seq = 0; + + if (!feature_bool(FEAT_CAP_batch)) + return; + + if (!server || !cli_serv(server)) + return; + + /* Generate unique batch ID */ + ircd_snprintf(NULL, batch_id, sizeof(batch_id), "NJ%s%lu", + cli_yxx(&me), netjoin_seq++); + + /* Store on server struct */ + ircd_strncpy(cli_serv(server)->batch_id, batch_id, + sizeof(cli_serv(server)->batch_id) - 1); + cli_serv(server)->batch_id[sizeof(cli_serv(server)->batch_id) - 1] = '\0'; + + /* Set active network batch so JOIN messages include @batch tag */ + set_active_network_batch(batch_id); + + /* Send batch start to local clients with batch capability */ + for (acptr = GlobalClientList; acptr; acptr = cli_next(acptr)) { + if (!MyConnect(acptr) || !IsUser(acptr)) + continue; + if (!CapActive(acptr, CAP_BATCH)) + continue; + + if (uplink) { + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "+%s netjoin %s %s", + batch_id, cli_name(uplink), cli_name(server)); + } else { + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "+%s netjoin %s", + batch_id, cli_name(server)); + } + } +} + +/** + * End a netjoin batch when END_OF_BURST is received. + * @param[in] server Server that finished bursting. + */ +void send_netjoin_batch_end(struct Client *server) +{ + struct Client *acptr; + const char *batch_id; + + if (!feature_bool(FEAT_CAP_batch)) + return; + + if (!server || !cli_serv(server)) + return; + + batch_id = cli_serv(server)->batch_id; + if (!batch_id || !*batch_id) + return; + + /* Clear active network batch */ + set_active_network_batch(NULL); + + /* Send batch end to local clients with batch capability */ + for (acptr = GlobalClientList; acptr; acptr = cli_next(acptr)) { + if (!MyConnect(acptr) || !IsUser(acptr)) + continue; + if (!CapActive(acptr, CAP_BATCH)) + continue; + + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "-%s", batch_id); + } + + /* Clear stored batch ID */ + cli_serv(server)->batch_id[0] = '\0'; +} + +/** + * Start a netsplit batch when a server disconnects. + * @param[in] server Server that is disconnecting. + * @param[in] uplink Server's uplink. + * @param[out] batch_id_out Buffer to store generated batch ID (min 32 bytes). + */ +void send_netsplit_batch_start(struct Client *server, struct Client *uplink, + char *batch_id_out, size_t batch_id_len) +{ + struct Client *acptr; + static unsigned long netsplit_seq = 0; + + if (!feature_bool(FEAT_CAP_batch)) + return; + + if (!batch_id_out || batch_id_len < 16) + return; + + /* Generate unique batch ID */ + ircd_snprintf(NULL, batch_id_out, batch_id_len, "NS%s%lu", + cli_yxx(&me), netsplit_seq++); + + /* Send batch start to local clients with batch capability */ + for (acptr = GlobalClientList; acptr; acptr = cli_next(acptr)) { + if (!MyConnect(acptr) || !IsUser(acptr)) + continue; + if (!CapActive(acptr, CAP_BATCH)) + continue; + + if (uplink && server) { + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "+%s netsplit %s %s", + batch_id_out, cli_name(uplink), cli_name(server)); + } else if (server) { + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "+%s netsplit %s", + batch_id_out, cli_name(server)); + } + } +} + +/** + * End a netsplit batch. + * @param[in] batch_id Batch ID from send_netsplit_batch_start. + */ +void send_netsplit_batch_end(const char *batch_id) +{ + struct Client *acptr; + + if (!feature_bool(FEAT_CAP_batch)) + return; + + if (!batch_id || !*batch_id) + return; + + /* Send batch end to local clients with batch capability */ + for (acptr = GlobalClientList; acptr; acptr = cli_next(acptr)) { + if (!MyConnect(acptr) || !IsUser(acptr)) + continue; + if (!CapActive(acptr, CAP_BATCH)) + continue; + + sendcmdto_one(&me, CMD_BATCH_CMD, acptr, "-%s", batch_id); + } +} + +/** + * Send a standard reply (FAIL/WARN/NOTE) to a client with optional explicit label. + * Internal helper function. + * @param[in] to Client to send to. + * @param[in] type Reply type (FAIL, WARN, or NOTE). + * @param[in] command Command that generated this reply (or "*" for general). + * @param[in] code Machine-readable code (e.g., "ACCOUNT_REQUIRED"). + * @param[in] context Optional context parameter (NULL if none). + * @param[in] description Human-readable description. + * @param[in] label Optional explicit label (NULL to use cli_label). + */ +static void send_standard_reply_ex(struct Client *to, const char *type, + const char *command, const char *code, + const char *context, const char *description, + const char *label) +{ + struct MsgBuf *mb; + char tagbuf[512]; + int pos = 0; + int use_time, use_label; + + if (!MyConnect(to)) + return; + + /* If client doesn't have standard-replies, fall back to numerics or NOTICE */ + if (!feature_bool(FEAT_CAP_standard_replies) || !CapActive(to, CAP_STANDARDREPLIES)) { + /* Map known error codes to traditional numerics where applicable */ + if (strcmp(type, "FAIL") == 0) { + if (strcmp(code, "NEED_MORE_PARAMS") == 0) { + /* ERR_NEEDMOREPARAMS (461) */ + mb = msgq_make(to, ":%s 461 %s %s :Not enough parameters", + cli_name(&me), IsRegistered(to) ? cli_name(to) : "*", command); + send_buffer(to, mb, 0); + msgq_clean(mb); + return; + } + if (strcmp(code, "ALREADY_AUTHENTICATED") == 0) { + /* ERR_ALREADYREGISTRED (462) */ + mb = msgq_make(to, ":%s 462 %s :You may not reregister", + cli_name(&me), IsRegistered(to) ? cli_name(to) : "*"); + send_buffer(to, mb, 0); + msgq_clean(mb); + return; + } + } + /* Fall back to NOTICE for unmapped codes */ + if (context && *context) + mb = msgq_make(to, ":%s NOTICE %s :%s %s %s %s :%s", + cli_name(&me), IsRegistered(to) ? cli_name(to) : "*", + type, command, code, context, description); + else + mb = msgq_make(to, ":%s NOTICE %s :%s %s %s :%s", + cli_name(&me), IsRegistered(to) ? cli_name(to) : "*", + type, command, code, description); + send_buffer(to, mb, 0); + msgq_clean(mb); + return; + } + + /* Format tags with explicit label override if provided */ + use_time = feature_bool(FEAT_CAP_server_time) && CapActive(to, CAP_SERVERTIME); + use_label = feature_bool(FEAT_CAP_labeled_response) && + CapActive(to, CAP_LABELEDRESP) && + ((label && *label) || cli_label(to)[0]); + + if (use_time || use_label) { + tagbuf[0] = '@'; + pos = 1; + + if (use_label) { + /* Use explicit label if provided, otherwise use cli_label */ + const char *lbl = (label && *label) ? label : cli_label(to); + pos += snprintf(tagbuf + pos, sizeof(tagbuf) - pos, "label=%s", lbl); + } + + if (use_time) { + struct timeval tv; + struct tm tm; + if (pos > 1 && pos < (int)sizeof(tagbuf) - 1) + tagbuf[pos++] = ';'; + gettimeofday(&tv, NULL); + gmtime_r(&tv.tv_sec, &tm); + pos += snprintf(tagbuf + pos, sizeof(tagbuf) - pos, + "time=%04d-%02d-%02dT%02d:%02d:%02d.%03ldZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, + (long)(tv.tv_usec / 1000)); + } + + tagbuf[pos++] = ' '; + tagbuf[pos] = '\0'; + + if (context && *context) + mb = msgq_make(to, "%s%s %s %s %s :%s", tagbuf, type, command, code, context, description); + else + mb = msgq_make(to, "%s%s %s %s :%s", tagbuf, type, command, code, description); + } else { + if (context && *context) + mb = msgq_make(to, "%s %s %s %s :%s", type, command, code, context, description); + else + mb = msgq_make(to, "%s %s %s :%s", type, command, code, description); + } + + send_buffer(to, mb, 0); + msgq_clean(mb); +} + +/** + * Send a standard reply (FAIL/WARN/NOTE) to a client. + * Internal helper function. + * @param[in] to Client to send to. + * @param[in] type Reply type (FAIL, WARN, or NOTE). + * @param[in] command Command that generated this reply (or "*" for general). + * @param[in] code Machine-readable code (e.g., "ACCOUNT_REQUIRED"). + * @param[in] context Optional context parameter (NULL if none). + * @param[in] description Human-readable description. + */ +static void send_standard_reply(struct Client *to, const char *type, + const char *command, const char *code, + const char *context, const char *description) +{ + send_standard_reply_ex(to, type, command, code, context, description, NULL); +} + +/** + * Send a FAIL reply to a client (IRCv3 standard-replies). + * Indicates an error that prevented the command from executing. + * @param[in] to Client to send to. + * @param[in] command Command name (or "*" for general failure). + * @param[in] code Machine-readable error code. + * @param[in] context Optional context (NULL if none). + * @param[in] description Human-readable error message. + */ +void send_fail(struct Client *to, const char *command, const char *code, + const char *context, const char *description) +{ + send_standard_reply(to, "FAIL", command, code, context, description); +} + +/** + * Send a WARN reply to a client (IRCv3 standard-replies). + * Indicates a warning that didn't prevent command execution. + * @param[in] to Client to send to. + * @param[in] command Command name (or "*" for general warning). + * @param[in] code Machine-readable warning code. + * @param[in] context Optional context (NULL if none). + * @param[in] description Human-readable warning message. + */ +void send_warn(struct Client *to, const char *command, const char *code, + const char *context, const char *description) +{ + send_standard_reply(to, "WARN", command, code, context, description); +} + +/** + * Send a WARN reply with an explicit label (IRCv3 standard-replies + labeled-response). + * Used when the warning relates to an earlier command whose label was saved. + * @param[in] to Client to send to. + * @param[in] command Command name (or "*" for general warning). + * @param[in] code Machine-readable warning code. + * @param[in] context Optional context (NULL if none). + * @param[in] description Human-readable warning message. + * @param[in] label Explicit label to include (NULL uses cli_label). + */ +void send_warn_with_label(struct Client *to, const char *command, const char *code, + const char *context, const char *description, + const char *label) +{ + send_standard_reply_ex(to, "WARN", command, code, context, description, label); +} + +/** + * Send a NOTE reply to a client (IRCv3 standard-replies). + * Provides informational feedback about a command. + * @param[in] to Client to send to. + * @param[in] command Command name (or "*" for general note). + * @param[in] code Machine-readable info code. + * @param[in] context Optional context (NULL if none). + * @param[in] description Human-readable info message. + */ +void send_note(struct Client *to, const char *command, const char *code, + const char *context, const char *description) +{ + send_standard_reply(to, "NOTE", command, code, context, description); +} + diff --git a/ircd/ssl.c b/ircd/ssl.c index 76d897bd..b517f9d2 100644 --- a/ircd/ssl.c +++ b/ircd/ssl.c @@ -46,6 +46,7 @@ #include #include #include +#include #include #include @@ -303,8 +304,11 @@ int ssl_accept(struct Client *cptr) if (SSL_is_init_finished(cli_socket(cptr).ssl)) { char *sslfp = ssl_get_fingerprint(cli_socket(cptr).ssl); - if (sslfp) + if (sslfp) { ircd_strncpy(cli_sslclifp(cptr), sslfp, BUFSIZE+1); + if (feature_bool(FEAT_CERT_EXPIRY_TRACKING)) + cli_sslcliexp(cptr) = ssl_get_cert_expiry(cli_socket(cptr).ssl); + } } return -1; @@ -627,6 +631,39 @@ char* ssl_get_fingerprint(SSL *ssl) return (hex); } +/** + * Get the expiration time of the peer's SSL client certificate. + * @param ssl The SSL connection + * @return Unix timestamp of certificate expiration, or 0 if no certificate or error + */ +time_t ssl_get_cert_expiry(SSL *ssl) +{ + X509 *cert; + const ASN1_TIME *not_after; + struct tm tm_exp; + time_t exp_time = 0; + + cert = SSL_get_peer_certificate(ssl); + if (!cert) + return 0; + + not_after = X509_get0_notAfter(cert); + if (!not_after) { + X509_free(cert); + return 0; + } + + /* Convert ASN1_TIME to struct tm */ + memset(&tm_exp, 0, sizeof(tm_exp)); + if (ASN1_TIME_to_tm(not_after, &tm_exp) == 1) { + /* Convert to time_t (timegm for UTC) */ + exp_time = timegm(&tm_exp); + } + + X509_free(cert); + return exp_time; +} + void ssl_set_nonblocking(SSL *s) { BIO_set_nbio(SSL_get_rbio(s),1); diff --git a/ircd/test/.gitignore b/ircd/test/.gitignore index 3bf9335c..6433e9a5 100644 --- a/ircd/test/.gitignore +++ b/ircd/test/.gitignore @@ -1,6 +1,25 @@ Makefile +Makefile.in.tmp + +# Legacy test binaries ircd_chattr_t ircd_in_addr_t ircd_string_t ircd_match_t + +# CMocka test binaries +ircd_chattr_cmocka +ircd_string_cmocka +numnicks_cmocka +ircd_in_addr_cmocka +ircd_compress_cmocka +ircd_cloaking_cmocka +dbuf_cmocka +ircd_crypt_cmocka +crule_cmocka +history_cmocka +dnsbl_cmocka +ircd_match_cmocka + *.log +*.o diff --git a/ircd/test/Makefile.in b/ircd/test/Makefile.in index e0ea0ac7..31ef130d 100644 --- a/ircd/test/Makefile.in +++ b/ircd/test/Makefile.in @@ -6,23 +6,55 @@ CFLAGS = -g -Wall LDFLAGS = CC = @CC@ +# Legacy test programs (print-based, no assertions) TESTPROGS = \ ircd_chattr_t \ ircd_in_addr_t \ ircd_match_t \ ircd_string_t +# CMocka test programs (proper assertions) +CMOCKA_TESTPROGS = \ + ircd_chattr_cmocka \ + ircd_match_cmocka \ + ircd_string_cmocka \ + numnicks_cmocka \ + ircd_in_addr_cmocka \ + ircd_compress_cmocka \ + ircd_cloaking_cmocka \ + dbuf_cmocka \ + ircd_crypt_cmocka \ + crule_cmocka \ + history_cmocka + +CMOCKA_LIBS = -lcmocka +ZSTD_LIBS = -lzstd + DEP_SRC = \ ircd_chattr_t.c \ ircd_in_addr_t.c \ ircd_match_t.c \ ircd_string_t.c \ + ircd_chattr_cmocka.c \ + ircd_match_cmocka.c \ + ircd_string_cmocka.c \ + numnicks_cmocka.c \ + ircd_in_addr_cmocka.c \ + ircd_compress_cmocka.c \ + ircd_cloaking_cmocka.c \ + dbuf_cmocka.c \ + ircd_crypt_cmocka.c \ + crule_cmocka.c \ + history_cmocka.c \ test_stub.c all: ${TESTPROGS} build: ${TESTPROGS} +# Build CMocka tests separately (requires libcmocka-dev) +cmocka: ${CMOCKA_TESTPROGS} + depend: ${DEP_SRC} @cd ${srcdir} && \ if [ -f Makefile.in.bak ]; then \ @@ -51,16 +83,111 @@ IRCD_STRING_T_OBJS = ircd_string_t.o test_stub.o ../ircd_string.o ircd_string_t: $(IRCD_STRING_T_OBJS) ${CC} -o $@ $(LDFLAGS) $(IRCD_STRING_T_OBJS) +# CMocka tests - these use proper assertions +# All CMocka tests need test_stub.o for log_write/log_inassert stubs +IRCD_CHATTR_CMOCKA_OBJS = ircd_chattr_cmocka.o test_stub.o ../ircd_string.o +ircd_chattr_cmocka: $(IRCD_CHATTR_CMOCKA_OBJS) + ${CC} -o $@ $(LDFLAGS) $(IRCD_CHATTR_CMOCKA_OBJS) $(CMOCKA_LIBS) + +IRCD_MATCH_CMOCKA_OBJS = ircd_match_cmocka.o test_stub.o ../ircd_string.o ../match.o +ircd_match_cmocka: $(IRCD_MATCH_CMOCKA_OBJS) + ${CC} -o $@ $(LDFLAGS) $(IRCD_MATCH_CMOCKA_OBJS) $(CMOCKA_LIBS) + +IRCD_STRING_CMOCKA_OBJS = ircd_string_cmocka.o test_stub.o ../ircd_string.o +ircd_string_cmocka: $(IRCD_STRING_CMOCKA_OBJS) + ${CC} -o $@ $(LDFLAGS) $(IRCD_STRING_CMOCKA_OBJS) $(CMOCKA_LIBS) + +NUMNICKS_CMOCKA_OBJS = numnicks_cmocka.o test_stub.o ../numnicks.o ../ircd_string.o ../ircd_alloc.o ../match.o +numnicks_cmocka: $(NUMNICKS_CMOCKA_OBJS) + ${CC} -o $@ $(LDFLAGS) $(NUMNICKS_CMOCKA_OBJS) $(CMOCKA_LIBS) + +IRCD_IN_ADDR_CMOCKA_OBJS = ircd_in_addr_cmocka.o test_stub.o ../ircd_string.o ../match.o ../numnicks.o ../ircd_alloc.o +ircd_in_addr_cmocka: $(IRCD_IN_ADDR_CMOCKA_OBJS) + ${CC} -o $@ $(LDFLAGS) $(IRCD_IN_ADDR_CMOCKA_OBJS) $(CMOCKA_LIBS) + +# ircd_compress tests - requires zstd library +IRCD_COMPRESS_CMOCKA_OBJS = ircd_compress_cmocka.o test_stub.o ../ircd_compress.o +ircd_compress_cmocka: $(IRCD_COMPRESS_CMOCKA_OBJS) + ${CC} -o $@ $(LDFLAGS) $(IRCD_COMPRESS_CMOCKA_OBJS) $(CMOCKA_LIBS) $(ZSTD_LIBS) + +# ircd_cloaking tests - uses #include .c approach to access static functions +# Links against ircd_md5.o for MD5 hashing used in cloaking +IRCD_CLOAKING_CMOCKA_OBJS = ircd_cloaking_cmocka.o test_stub.o ../ircd_md5.o ../ircd_string.o +ircd_cloaking_cmocka: $(IRCD_CLOAKING_CMOCKA_OBJS) + ${CC} -o $@ $(LDFLAGS) $(IRCD_CLOAKING_CMOCKA_OBJS) $(CMOCKA_LIBS) + +# dbuf tests - inlines dbuf.c with stubs for feature_int/bool/flush_connections +DBUF_CMOCKA_OBJS = dbuf_cmocka.o test_stub.o ../ircd_string.o +dbuf_cmocka: $(DBUF_CMOCKA_OBJS) + ${CC} -o $@ $(LDFLAGS) $(DBUF_CMOCKA_OBJS) $(CMOCKA_LIBS) + +# ircd_crypt tests - password hashing mechanisms +IRCD_CRYPT_CMOCKA_OBJS = ircd_crypt_cmocka.o test_stub.o ../ircd_string.o ../ircd_md5.o +ircd_crypt_cmocka: $(IRCD_CRYPT_CMOCKA_OBJS) + ${CC} -o $@ $(LDFLAGS) $(IRCD_CRYPT_CMOCKA_OBJS) $(CMOCKA_LIBS) + +# crule tests - connection rule parser (uses CR_DEBUG mode, includes crule.c) +CRULE_CMOCKA_OBJS = crule_cmocka.o test_stub.o ../ircd_string.o +crule_cmocka: $(CRULE_CMOCKA_OBJS) + ${CC} -o $@ $(LDFLAGS) $(CRULE_CMOCKA_OBJS) $(CMOCKA_LIBS) + +# history tests - serialization functions (no LMDB required) +HISTORY_CMOCKA_OBJS = history_cmocka.o test_stub.o ../ircd_string.o +history_cmocka: $(HISTORY_CMOCKA_OBJS) + ${CC} -o $@ $(LDFLAGS) $(HISTORY_CMOCKA_OBJS) $(CMOCKA_LIBS) + .c.o: ${CC} ${CFLAGS} ${CPPFLAGS} -c $< -o $@ -.PHONY: distclean clean +.PHONY: distclean clean test test-cmocka test-all cmocka + +# Run legacy unit tests (default - runs during Docker build) +test: ${TESTPROGS} + @echo "Running nefarious unit tests..." + @failed=0; \ + for t in ${TESTPROGS}; do \ + echo " Running $$t..."; \ + if ./$$t > $$t.log 2>&1; then \ + echo " PASSED"; \ + else \ + echo " FAILED (exit code $$?)"; \ + cat $$t.log; \ + failed=1; \ + fi; \ + done; \ + if [ $$failed -eq 0 ]; then \ + echo "All unit tests passed."; \ + else \ + echo "Some tests failed!"; \ + exit 1; \ + fi + +# Run CMocka tests (requires libcmocka-dev) +test-cmocka: ${CMOCKA_TESTPROGS} + @echo "Running CMocka unit tests..." + @failed=0; \ + for t in ${CMOCKA_TESTPROGS}; do \ + echo " Running $$t..."; \ + if ./$$t 2>&1 | tee $$t.log; then \ + : ; \ + else \ + failed=1; \ + fi; \ + done; \ + if [ $$failed -ne 0 ]; then \ + echo "Some CMocka tests failed!"; \ + exit 1; \ + fi + +# Run all tests (legacy + CMocka) +test-all: test test-cmocka + @echo "All tests completed." distclean: clean rm -f Makefile clean: - rm -f core *.o *.log ${TESTPROGS} + rm -f core *.o *.log ${TESTPROGS} ${CMOCKA_TESTPROGS} # DO NOT DELETE THIS LINE (or the blank line after it) -- make depend depends on them. diff --git a/ircd/test/crule_cmocka.c b/ircd/test/crule_cmocka.c new file mode 100644 index 00000000..84370cbc --- /dev/null +++ b/ircd/test/crule_cmocka.c @@ -0,0 +1,1030 @@ +/* crule_cmocka.c - CMocka unit tests for connection rule parser + * + * Tests the connection rule grammar parser used for server link rules. + * We inline the parser functions with CR_DEBUG and CR_CHKCONF defined + * to avoid server dependencies, but prevent the built-in main(). + * + * Grammar: + * rule: orexpr END + * orexpr: andexpr | andexpr || orexpr + * andexpr: primary | primary && andexpr + * primary: function | ! primary | ( orexpr ) + * function: word ( ) | word ( arglist ) + * arglist: word | word , arglist + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include + +/* Provide character classification macros */ +#include "ircd_chattr.h" + +/* Define CR_CHKCONF to get standalone parser mode (no server deps) + * but unlike CR_DEBUG, it doesn't include main() */ +#define CR_CHKCONF + +/* Provide MyMalloc/MyFree stubs before including crule.c */ +#define MyMalloc malloc +#define MyFree free + +/* Provide ircd_strcmp - case-insensitive compare */ +#define ircd_strcmp strcasecmp + +/* Provide DupString macro */ +#define DupString(x,y) \ + do { \ + x = (char*) malloc(strlen(y)+1); \ + strcpy(x,y); \ + } while(0) + +/* === Inline the parser structures and functions from crule.c === */ + +#define CR_MAXARGLEN 80 +#define CR_MAXARGS 3 + +enum crule_token { + CR_UNKNOWN, + CR_END, + CR_AND, + CR_OR, + CR_NOT, + CR_OPENPAREN, + CR_CLOSEPAREN, + CR_COMMA, + CR_WORD +}; + +enum crule_errcode { + CR_NOERR, + CR_UNEXPCTTOK, + CR_UNKNWTOK, + CR_EXPCTAND, + CR_EXPCTOR, + CR_EXPCTPRIM, + CR_EXPCTOPEN, + CR_EXPCTCLOSE, + CR_UNKNWFUNC, + CR_ARGMISMAT +}; + +typedef int (*crule_funcptr)(int, void **); + +struct CRuleNode { + crule_funcptr funcptr; + int numargs; + void *arg[CR_MAXARGS]; +}; + +typedef struct CRuleNode* CRuleNodePtr; + +/* Rule function stubs - in CHKCONF mode these return 0 */ +static int crule_connected(int numargs, void *crulearg[]) { return 0; } +static int crule_directcon(int numargs, void *crulearg[]) { return 0; } +static int crule_via(int numargs, void *crulearg[]) { return 0; } +static int crule_directop(int numargs, void *crulearg[]) { return 0; } + +/* Forward declarations */ +static int crule__andor(int, void *[]); +static int crule__not(int, void *[]); +void crule_free(struct CRuleNode** elem); +static int crule_gettoken(int* token, const char** str); +static void crule_getword(char*, int*, size_t, const char**); +static int crule_parseandexpr(CRuleNodePtr*, int *, const char**); +static int crule_parseorexpr(CRuleNodePtr*, int *, const char**); +static int crule_parseprimary(CRuleNodePtr*, int *, const char**); +static int crule_parsefunction(CRuleNodePtr*, int *, const char**); +static int crule_parsearglist(CRuleNodePtr, int *, const char**); + +char *crule_errstr[] = { + "Unknown error", + "Unexpected token", + "Unknown token", + "And expr expected", + "Or expr expected", + "Primary expected", + "( expected", + ") expected", + "Unknown function", + "Argument mismatch" +}; + +struct crule_funclistent { + char name[15]; + int reqnumargs; + crule_funcptr funcptr; +}; + +struct crule_funclistent crule_funclist[] = { + {"connected", 1, crule_connected}, + {"directcon", 1, crule_directcon}, + {"via", 2, crule_via}, + {"directop", 0, crule_directop}, + {"", 0, NULL} +}; + +/* crule_eval */ +int crule_eval(struct CRuleNode* rule) +{ + return (rule->funcptr(rule->numargs, rule->arg)); +} + +/* crule__andor */ +static int crule__andor(int numargs, void *crulearg[]) +{ + int result1; + result1 = crule_eval(crulearg[0]); + if (crulearg[2]) + return (result1 || crule_eval(crulearg[1])); + else + return (result1 && crule_eval(crulearg[1])); +} + +/* crule__not */ +static int crule__not(int numargs, void *crulearg[]) +{ + return (!crule_eval(crulearg[0])); +} + +/* crule_gettoken */ +static int crule_gettoken(int* next_tokp, const char** ruleptr) +{ + char pending = '\0'; + + *next_tokp = CR_UNKNOWN; + while (*next_tokp == CR_UNKNOWN) + switch (*(*ruleptr)++) + { + case ' ': + case '\t': + break; + case '&': + if (pending == '\0') + pending = '&'; + else if (pending == '&') + *next_tokp = CR_AND; + else + return (CR_UNKNWTOK); + break; + case '|': + if (pending == '\0') + pending = '|'; + else if (pending == '|') + *next_tokp = CR_OR; + else + return (CR_UNKNWTOK); + break; + case '!': + *next_tokp = CR_NOT; + break; + case '(': + *next_tokp = CR_OPENPAREN; + break; + case ')': + *next_tokp = CR_CLOSEPAREN; + break; + case ',': + *next_tokp = CR_COMMA; + break; + case '\0': + (*ruleptr)--; + *next_tokp = CR_END; + break; + case ':': + *next_tokp = CR_END; + break; + default: + if ((IsAlpha(*(--(*ruleptr)))) || (**ruleptr == '*') || + (**ruleptr == '?') || (**ruleptr == '.') || (**ruleptr == '-')) + *next_tokp = CR_WORD; + else + return (CR_UNKNWTOK); + break; + } + return CR_NOERR; +} + +/* crule_getword */ +static void crule_getword(char* word, int* wordlenp, size_t maxlen, const char** ruleptr) +{ + char *word_ptr; + + word_ptr = word; + while ((size_t)(word_ptr - word) < maxlen + && (IsAlnum(**ruleptr) + || **ruleptr == '*' || **ruleptr == '?' + || **ruleptr == '.' || **ruleptr == '-')) + *word_ptr++ = *(*ruleptr)++; + *word_ptr = '\0'; + *wordlenp = word_ptr - word; +} + +/* crule_parseorexpr */ +static int crule_parseorexpr(CRuleNodePtr * orrootp, int *next_tokp, const char** ruleptr) +{ + int errcode = CR_NOERR; + CRuleNodePtr andexpr; + CRuleNodePtr orptr; + + *orrootp = NULL; + while (errcode == CR_NOERR) + { + errcode = crule_parseandexpr(&andexpr, next_tokp, ruleptr); + if ((errcode == CR_NOERR) && (*next_tokp == CR_OR)) + { + orptr = (CRuleNodePtr) malloc(sizeof(struct CRuleNode)); + orptr->funcptr = crule__andor; + orptr->numargs = 3; + orptr->arg[2] = (void *)1; + if (*orrootp != NULL) + { + (*orrootp)->arg[1] = andexpr; + orptr->arg[0] = *orrootp; + } + else + orptr->arg[0] = andexpr; + *orrootp = orptr; + } + else + { + if (*orrootp != NULL) + { + if (andexpr != NULL) + { + (*orrootp)->arg[1] = andexpr; + return (errcode); + } + else + { + (*orrootp)->arg[1] = NULL; + return (CR_EXPCTAND); + } + } + else + { + *orrootp = andexpr; + return (errcode); + } + } + if ((errcode = crule_gettoken(next_tokp, ruleptr)) != CR_NOERR) + return (errcode); + } + return (errcode); +} + +/* crule_parseandexpr */ +static int crule_parseandexpr(CRuleNodePtr * androotp, int *next_tokp, const char** ruleptr) +{ + int errcode = CR_NOERR; + CRuleNodePtr primary; + CRuleNodePtr andptr; + + *androotp = NULL; + while (errcode == CR_NOERR) + { + errcode = crule_parseprimary(&primary, next_tokp, ruleptr); + if ((errcode == CR_NOERR) && (*next_tokp == CR_AND)) + { + andptr = (CRuleNodePtr) malloc(sizeof(struct CRuleNode)); + andptr->funcptr = crule__andor; + andptr->numargs = 3; + andptr->arg[2] = (void *)0; + if (*androotp != NULL) + { + (*androotp)->arg[1] = primary; + andptr->arg[0] = *androotp; + } + else + andptr->arg[0] = primary; + *androotp = andptr; + } + else + { + if (*androotp != NULL) + { + if (primary != NULL) + { + (*androotp)->arg[1] = primary; + return (errcode); + } + else + { + (*androotp)->arg[1] = NULL; + return (CR_EXPCTPRIM); + } + } + else + { + *androotp = primary; + return (errcode); + } + } + if ((errcode = crule_gettoken(next_tokp, ruleptr)) != CR_NOERR) + return (errcode); + } + return (errcode); +} + +/* crule_parseprimary */ +static int crule_parseprimary(CRuleNodePtr* primrootp, int *next_tokp, const char** ruleptr) +{ + CRuleNodePtr *insertionp; + int errcode = CR_NOERR; + + *primrootp = NULL; + insertionp = primrootp; + while (errcode == CR_NOERR) + { + switch (*next_tokp) + { + case CR_OPENPAREN: + if ((errcode = crule_gettoken(next_tokp, ruleptr)) != CR_NOERR) + break; + if ((errcode = crule_parseorexpr(insertionp, next_tokp, ruleptr)) != CR_NOERR) + break; + if (*insertionp == NULL) + { + errcode = CR_EXPCTAND; + break; + } + if (*next_tokp != CR_CLOSEPAREN) + { + errcode = CR_EXPCTCLOSE; + break; + } + errcode = crule_gettoken(next_tokp, ruleptr); + break; + case CR_NOT: + *insertionp = (CRuleNodePtr) malloc(sizeof(struct CRuleNode)); + (*insertionp)->funcptr = crule__not; + (*insertionp)->numargs = 1; + (*insertionp)->arg[0] = NULL; + insertionp = (CRuleNodePtr *) & ((*insertionp)->arg[0]); + if ((errcode = crule_gettoken(next_tokp, ruleptr)) != CR_NOERR) + break; + continue; + case CR_WORD: + errcode = crule_parsefunction(insertionp, next_tokp, ruleptr); + break; + default: + if (*primrootp == NULL) + errcode = CR_NOERR; + else + errcode = CR_EXPCTPRIM; + break; + } + return (errcode); + } + return (errcode); +} + +/* crule_parsefunction */ +static int crule_parsefunction(CRuleNodePtr* funcrootp, int* next_tokp, const char** ruleptr) +{ + int errcode = CR_NOERR; + char funcname[CR_MAXARGLEN]; + int namelen; + int funcnum; + + *funcrootp = NULL; + crule_getword(funcname, &namelen, CR_MAXARGLEN - 1, ruleptr); + if ((errcode = crule_gettoken(next_tokp, ruleptr)) != CR_NOERR) + return (errcode); + if (*next_tokp == CR_OPENPAREN) + { + for (funcnum = 0;; funcnum++) + { + if (0 == ircd_strcmp(crule_funclist[funcnum].name, funcname)) + break; + if (crule_funclist[funcnum].name[0] == '\0') + return (CR_UNKNWFUNC); + } + if ((errcode = crule_gettoken(next_tokp, ruleptr)) != CR_NOERR) + return (errcode); + *funcrootp = (CRuleNodePtr) malloc(sizeof(struct CRuleNode)); + (*funcrootp)->funcptr = NULL; + if ((errcode = crule_parsearglist(*funcrootp, next_tokp, ruleptr)) != CR_NOERR) + return (errcode); + if (*next_tokp != CR_CLOSEPAREN) + return (CR_EXPCTCLOSE); + if ((crule_funclist[funcnum].reqnumargs != (*funcrootp)->numargs) && + (crule_funclist[funcnum].reqnumargs != -1)) + return (CR_ARGMISMAT); + if ((errcode = crule_gettoken(next_tokp, ruleptr)) != CR_NOERR) + return (errcode); + (*funcrootp)->funcptr = crule_funclist[funcnum].funcptr; + return (CR_NOERR); + } + else + return (CR_EXPCTOPEN); +} + +/* crule_parsearglist */ +static int crule_parsearglist(CRuleNodePtr argrootp, int *next_tokp, const char** ruleptr) +{ + int errcode = CR_NOERR; + char *argelemp = NULL; + char currarg[CR_MAXARGLEN]; + int arglen = 0; + char word[CR_MAXARGLEN]; + int wordlen = 0; + + argrootp->numargs = 0; + currarg[0] = '\0'; + while (errcode == CR_NOERR) + { + switch (*next_tokp) + { + case CR_WORD: + crule_getword(word, &wordlen, CR_MAXARGLEN - 1, ruleptr); + if (currarg[0] != '\0') + { + if ((arglen + wordlen) < (CR_MAXARGLEN - 1)) + { + strcat(currarg, " "); + strcat(currarg, word); + arglen += wordlen + 1; + } + } + else + { + strcpy(currarg, word); + arglen = wordlen; + } + errcode = crule_gettoken(next_tokp, ruleptr); + break; + default: + /* In CR_CHKCONF mode, skip collapse() call */ + if (currarg[0] != '\0') + { + DupString(argelemp, currarg); + argrootp->arg[argrootp->numargs++] = (void *)argelemp; + } + if (*next_tokp != CR_COMMA) + return (CR_NOERR); + currarg[0] = '\0'; + errcode = crule_gettoken(next_tokp, ruleptr); + break; + } + } + return (errcode); +} + +/* crule_parse */ +struct CRuleNode* crule_parse(const char *rule) +{ + const char* ruleptr = rule; + int next_tok; + struct CRuleNode* ruleroot = 0; + int errcode = CR_NOERR; + + if ((errcode = crule_gettoken(&next_tok, &ruleptr)) == CR_NOERR) { + if ((errcode = crule_parseorexpr(&ruleroot, &next_tok, &ruleptr)) == CR_NOERR) { + if (ruleroot != NULL) { + if (next_tok == CR_END) + return (ruleroot); + else + errcode = CR_UNEXPCTTOK; + } + else + errcode = CR_EXPCTOR; + } + } + if (ruleroot != NULL) + crule_free(&ruleroot); + fprintf(stderr, "%s in rule: %s\n", crule_errstr[errcode], rule); + return 0; +} + +/* crule_free */ +void crule_free(struct CRuleNode** elem) +{ + int arg, numargs; + + if ((*(elem))->funcptr == crule__not) + { + if ((*(elem))->arg[0] != NULL) + crule_free((struct CRuleNode**) &((*(elem))->arg[0])); + } + else if ((*(elem))->funcptr == crule__andor) + { + crule_free((struct CRuleNode**) &((*(elem))->arg[0])); + if ((*(elem))->arg[1] != NULL) + crule_free((struct CRuleNode**) &((*(elem))->arg[1])); + } + else + { + numargs = (*(elem))->numargs; + for (arg = 0; arg < numargs; arg++) + free((*(elem))->arg[arg]); + } + free(*elem); + *elem = 0; +} + +/* ========== Basic Parsing Tests ========== */ + +static void test_parse_empty_rule(void **state) +{ + (void)state; + struct CRuleNode *rule; + + /* Empty string should fail to parse */ + rule = crule_parse(""); + assert_null(rule); +} + +static void test_parse_simple_function_no_args(void **state) +{ + (void)state; + struct CRuleNode *rule; + + /* directop() takes no arguments */ + rule = crule_parse("directop()"); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule_directop); + assert_int_equal(rule->numargs, 0); + crule_free(&rule); + assert_null(rule); +} + +static void test_parse_simple_function_one_arg(void **state) +{ + (void)state; + struct CRuleNode *rule; + + /* connected() takes one argument */ + rule = crule_parse("connected(*.example.com)"); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule_connected); + assert_int_equal(rule->numargs, 1); + assert_string_equal((char*)rule->arg[0], "*.example.com"); + crule_free(&rule); + assert_null(rule); +} + +static void test_parse_simple_function_two_args(void **state) +{ + (void)state; + struct CRuleNode *rule; + + /* via() takes two arguments */ + rule = crule_parse("via(hub.*, *.leaf.net)"); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule_via); + assert_int_equal(rule->numargs, 2); + assert_string_equal((char*)rule->arg[0], "hub.*"); + assert_string_equal((char*)rule->arg[1], "*.leaf.net"); + crule_free(&rule); + assert_null(rule); +} + +static void test_parse_directcon(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("directcon(irc.example.*)"); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule_directcon); + assert_int_equal(rule->numargs, 1); + assert_string_equal((char*)rule->arg[0], "irc.example.*"); + crule_free(&rule); +} + +/* ========== NOT Operator Tests ========== */ + +static void test_parse_not_operator(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("!directop()"); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule__not); + assert_int_equal(rule->numargs, 1); + struct CRuleNode *child = (struct CRuleNode*)rule->arg[0]; + assert_non_null(child); + assert_ptr_equal(child->funcptr, crule_directop); + crule_free(&rule); +} + +static void test_parse_double_not(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("!!directop()"); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule__not); + struct CRuleNode *child = (struct CRuleNode*)rule->arg[0]; + assert_non_null(child); + assert_ptr_equal(child->funcptr, crule__not); + struct CRuleNode *grandchild = (struct CRuleNode*)child->arg[0]; + assert_non_null(grandchild); + assert_ptr_equal(grandchild->funcptr, crule_directop); + crule_free(&rule); +} + +/* ========== AND Operator Tests ========== */ + +static void test_parse_and_operator(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("directop() && connected(*)"); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule__andor); + assert_int_equal(rule->numargs, 3); + assert_null(rule->arg[2]); /* NULL means AND */ + + struct CRuleNode *left = (struct CRuleNode*)rule->arg[0]; + struct CRuleNode *right = (struct CRuleNode*)rule->arg[1]; + assert_non_null(left); + assert_non_null(right); + assert_ptr_equal(left->funcptr, crule_directop); + assert_ptr_equal(right->funcptr, crule_connected); + crule_free(&rule); +} + +static void test_parse_chained_and(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("directop() && connected(*) && directcon(hub.*)"); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule__andor); + crule_free(&rule); +} + +/* ========== OR Operator Tests ========== */ + +static void test_parse_or_operator(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("directop() || connected(*)"); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule__andor); + assert_int_equal(rule->numargs, 3); + assert_non_null(rule->arg[2]); /* non-NULL means OR */ + + struct CRuleNode *left = (struct CRuleNode*)rule->arg[0]; + struct CRuleNode *right = (struct CRuleNode*)rule->arg[1]; + assert_non_null(left); + assert_non_null(right); + assert_ptr_equal(left->funcptr, crule_directop); + assert_ptr_equal(right->funcptr, crule_connected); + crule_free(&rule); +} + +static void test_parse_chained_or(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("directop() || connected(*) || directcon(hub.*)"); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule__andor); + assert_non_null(rule->arg[2]); /* OR */ + crule_free(&rule); +} + +/* ========== Precedence Tests ========== */ + +static void test_parse_and_or_precedence(void **state) +{ + (void)state; + struct CRuleNode *rule; + + /* AND has higher precedence than OR */ + rule = crule_parse("directop() || connected(*) && directcon(hub.*)"); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule__andor); + assert_non_null(rule->arg[2]); /* OR at top level */ + crule_free(&rule); +} + +static void test_parse_parentheses_override(void **state) +{ + (void)state; + struct CRuleNode *rule; + + /* Parentheses override normal precedence */ + rule = crule_parse("(directop() || connected(*)) && directcon(hub.*)"); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule__andor); + assert_null(rule->arg[2]); /* AND at top level */ + + struct CRuleNode *left = (struct CRuleNode*)rule->arg[0]; + assert_non_null(left); + assert_ptr_equal(left->funcptr, crule__andor); + assert_non_null(left->arg[2]); /* OR */ + crule_free(&rule); +} + +static void test_parse_nested_parentheses(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("((directop()))"); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule_directop); + crule_free(&rule); +} + +/* ========== Error Handling Tests ========== */ + +static void test_parse_unknown_function(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("unknownfunc()"); + assert_null(rule); +} + +static void test_parse_wrong_arg_count(void **state) +{ + (void)state; + struct CRuleNode *rule; + + /* directop takes 0 args */ + rule = crule_parse("directop(extra)"); + assert_null(rule); + + /* connected takes 1 arg */ + rule = crule_parse("connected()"); + assert_null(rule); + + /* via takes 2 args */ + rule = crule_parse("via(only.one)"); + assert_null(rule); +} + +static void test_parse_unclosed_paren(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("(directop()"); + assert_null(rule); + + rule = crule_parse("directop("); + assert_null(rule); +} + +static void test_parse_unexpected_token(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("directop())"); + assert_null(rule); + + rule = crule_parse("directop() &&"); + assert_null(rule); + + rule = crule_parse("directop() ||"); + assert_null(rule); +} + +static void test_parse_invalid_characters(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("@invalid"); + assert_null(rule); + + /* Single & is invalid */ + rule = crule_parse("directop() & connected(*)"); + assert_null(rule); + + /* Single | is invalid */ + rule = crule_parse("directop() | connected(*)"); + assert_null(rule); +} + +/* ========== Whitespace Handling Tests ========== */ + +static void test_parse_extra_whitespace(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse(" directop( ) "); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule_directop); + crule_free(&rule); + + rule = crule_parse("directop() && connected(*)"); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule__andor); + crule_free(&rule); +} + +static void test_parse_tabs(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("\tdirectop()\t&&\tconnected(*)\t"); + assert_non_null(rule); + crule_free(&rule); +} + +/* ========== Colon Terminator Tests ========== */ + +static void test_parse_colon_terminator(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("directop():extra stuff ignored"); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule_directop); + crule_free(&rule); +} + +/* ========== Wildcard Pattern Tests ========== */ + +static void test_parse_wildcard_patterns(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("connected(*.net)"); + assert_non_null(rule); + assert_string_equal((char*)rule->arg[0], "*.net"); + crule_free(&rule); + + rule = crule_parse("connected(irc?.example.com)"); + assert_non_null(rule); + assert_string_equal((char*)rule->arg[0], "irc?.example.com"); + crule_free(&rule); + + /* Use separate string to avoid trigraph warning */ + rule = crule_parse("connected(irc*.example.net)"); + assert_non_null(rule); + assert_string_equal((char*)rule->arg[0], "irc*.example.net"); + crule_free(&rule); +} + +static void test_parse_hostname_patterns(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("connected(irc.server.example.com)"); + assert_non_null(rule); + assert_string_equal((char*)rule->arg[0], "irc.server.example.com"); + crule_free(&rule); + + rule = crule_parse("connected(irc-server.example-net.com)"); + assert_non_null(rule); + assert_string_equal((char*)rule->arg[0], "irc-server.example-net.com"); + crule_free(&rule); +} + +/* ========== Evaluation Tests ========== */ + +static void test_eval_functions_return_zero(void **state) +{ + (void)state; + struct CRuleNode *rule; + int result; + + /* All rule functions return 0 in test mode */ + rule = crule_parse("directop()"); + assert_non_null(rule); + result = crule_eval(rule); + assert_int_equal(result, 0); + crule_free(&rule); + + /* NOT of 0 is 1 */ + rule = crule_parse("!directop()"); + assert_non_null(rule); + result = crule_eval(rule); + assert_int_equal(result, 1); + crule_free(&rule); + + /* 0 && 0 = 0 */ + rule = crule_parse("directop() && connected(*)"); + assert_non_null(rule); + result = crule_eval(rule); + assert_int_equal(result, 0); + crule_free(&rule); + + /* 0 || 0 = 0 */ + rule = crule_parse("directop() || connected(*)"); + assert_non_null(rule); + result = crule_eval(rule); + assert_int_equal(result, 0); + crule_free(&rule); + + /* !0 || 0 = 1 */ + rule = crule_parse("!directop() || connected(*)"); + assert_non_null(rule); + result = crule_eval(rule); + assert_int_equal(result, 1); + crule_free(&rule); +} + +/* ========== Complex Expression Tests ========== */ + +static void test_parse_complex_expression(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("!directop() || !connected(hub.*)"); + assert_non_null(rule); + crule_free(&rule); + + rule = crule_parse("(directop() || connected(hub.*)) && !connected(leaf.*)"); + assert_non_null(rule); + crule_free(&rule); +} + +static void test_parse_via_with_wildcards(void **state) +{ + (void)state; + struct CRuleNode *rule; + + rule = crule_parse("via(hub.*.net, *.leaf.*)"); + assert_non_null(rule); + assert_ptr_equal(rule->funcptr, crule_via); + assert_int_equal(rule->numargs, 2); + assert_string_equal((char*)rule->arg[0], "hub.*.net"); + assert_string_equal((char*)rule->arg[1], "*.leaf.*"); + crule_free(&rule); +} + +/* ========== Main ========== */ + +int main(void) +{ + const struct CMUnitTest tests[] = { + /* Basic parsing */ + cmocka_unit_test(test_parse_empty_rule), + cmocka_unit_test(test_parse_simple_function_no_args), + cmocka_unit_test(test_parse_simple_function_one_arg), + cmocka_unit_test(test_parse_simple_function_two_args), + cmocka_unit_test(test_parse_directcon), + + /* NOT operator */ + cmocka_unit_test(test_parse_not_operator), + cmocka_unit_test(test_parse_double_not), + + /* AND operator */ + cmocka_unit_test(test_parse_and_operator), + cmocka_unit_test(test_parse_chained_and), + + /* OR operator */ + cmocka_unit_test(test_parse_or_operator), + cmocka_unit_test(test_parse_chained_or), + + /* Precedence */ + cmocka_unit_test(test_parse_and_or_precedence), + cmocka_unit_test(test_parse_parentheses_override), + cmocka_unit_test(test_parse_nested_parentheses), + + /* Error handling */ + cmocka_unit_test(test_parse_unknown_function), + cmocka_unit_test(test_parse_wrong_arg_count), + cmocka_unit_test(test_parse_unclosed_paren), + cmocka_unit_test(test_parse_unexpected_token), + cmocka_unit_test(test_parse_invalid_characters), + + /* Whitespace */ + cmocka_unit_test(test_parse_extra_whitespace), + cmocka_unit_test(test_parse_tabs), + + /* Colon terminator */ + cmocka_unit_test(test_parse_colon_terminator), + + /* Wildcards */ + cmocka_unit_test(test_parse_wildcard_patterns), + cmocka_unit_test(test_parse_hostname_patterns), + + /* Evaluation */ + cmocka_unit_test(test_eval_functions_return_zero), + + /* Complex expressions */ + cmocka_unit_test(test_parse_complex_expression), + cmocka_unit_test(test_parse_via_with_wildcards), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/ircd/test/dbuf_cmocka.c b/ircd/test/dbuf_cmocka.c new file mode 100644 index 00000000..ee7cdd87 --- /dev/null +++ b/ircd/test/dbuf_cmocka.c @@ -0,0 +1,728 @@ +/* + * dbuf_cmocka.c - CMocka unit tests for dynamic buffer functions + * + * Tests the DBuf data structure used for queuing data to be sent to clients. + * DBuf provides a linked list of fixed-size buffers (2048 bytes each) with + * efficient append and consume operations. + * + * Copyright (C) 2024 AfterNET Development Team + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +/* Stubs for dependencies */ + +/* Stub for feature_int - return large buffer pool */ +int feature_int(int feat) { + (void)feat; + return 10 * 1024 * 1024; /* 10MB buffer pool */ +} + +/* Stub for feature_bool */ +int feature_bool(int feat) { + (void)feat; + return 0; /* Disable Ferguson flusher */ +} + +/* Stub for flush_connections */ +void flush_connections(void *cptr) { + (void)cptr; +} + +/* Stub for MyMalloc - use real malloc */ +void *MyMalloc(size_t size) { + return malloc(size); +} + +/* Stub for MyFree */ +void MyFree(void *ptr) { + free(ptr); +} + +/* Now include the dbuf source */ +#include "ircd_chattr.h" +#include "dbuf.h" + +#include + +/* Need to define IRCD_MIN if not available */ +#ifndef IRCD_MIN +#define IRCD_MIN(a, b) ((a) < (b) ? (a) : (b)) +#endif + +/* Override assert macro for the inlined code */ +#undef assert +#define assert(x) do { if (!(x)) { fprintf(stderr, "Assertion failed: %s\n", #x); abort(); } } while(0) + +/* We need to manually include dbuf.c content to avoid linker issues */ +/* The key definitions from dbuf.c: */ + +#define DBUF_SIZE 2048 + +struct DBufBuffer { + struct DBufBuffer *next; + char *start; + char *end; + char data[DBUF_SIZE]; +}; + +/* Global counters */ +int DBufAllocCount = 0; +int DBufUsedCount = 0; +static struct DBufBuffer *dbufFreeList = 0; + +static struct DBufBuffer *dbuf_alloc(void) +{ + struct DBufBuffer* db = dbufFreeList; + + if (db) { + dbufFreeList = db->next; + ++DBufUsedCount; + } + else if (DBufAllocCount * DBUF_SIZE < feature_int(0)) { + db = (struct DBufBuffer*) MyMalloc(sizeof(struct DBufBuffer)); + assert(0 != db); + ++DBufAllocCount; + ++DBufUsedCount; + } + return db; +} + +static void dbuf_free(struct DBufBuffer *db) +{ + assert(0 != db); + --DBufUsedCount; + db->next = dbufFreeList; + dbufFreeList = db; +} + +static int dbuf_malloc_error(struct DBuf *dyn) +{ + struct DBufBuffer *db; + struct DBufBuffer *next; + + for (db = dyn->head; db; db = next) + { + next = db->next; + dbuf_free(db); + } + dyn->tail = dyn->head = 0; + dyn->length = 0; + return 0; +} + +int dbuf_put(struct DBuf *dyn, const char *buf, unsigned int length) +{ + struct DBufBuffer** h; + struct DBufBuffer* db; + unsigned int chunk; + + assert(0 != dyn); + assert(0 != buf); + + if (!dyn->length) + h = &(dyn->head); + else + h = &(dyn->tail); + + dyn->length += length; + + for (; length > 0; h = &(db->next)) { + if (0 == (db = *h)) { + if (0 == (db = dbuf_alloc())) { + if (feature_bool(0)) { + flush_connections(0); + db = dbuf_alloc(); + } + if (0 == db) + return dbuf_malloc_error(dyn); + } + dyn->tail = db; + *h = db; + db->next = 0; + db->start = db->end = db->data; + } + chunk = (db->data + DBUF_SIZE) - db->end; + if (chunk) { + if (chunk > length) + chunk = length; + memcpy(db->end, buf, chunk); + length -= chunk; + buf += chunk; + db->end += chunk; + } + } + return 1; +} + +const char *dbuf_map(const struct DBuf* dyn, unsigned int* length) +{ + assert(0 != dyn); + assert(0 != length); + + if (0 == dyn->length) + { + *length = 0; + return 0; + } + assert(0 != dyn->head); + + *length = dyn->head->end - dyn->head->start; + return dyn->head->start; +} + +void dbuf_delete(struct DBuf *dyn, unsigned int length) +{ + struct DBufBuffer *db; + unsigned int chunk; + + if (length > dyn->length) + length = dyn->length; + + while (length > 0) + { + if (0 == (db = dyn->head)) + break; + chunk = db->end - db->start; + if (chunk > length) + chunk = length; + + length -= chunk; + dyn->length -= chunk; + db->start += chunk; + + if (db->start == db->end) + { + dyn->head = db->next; + dbuf_free(db); + } + } + if (0 == dyn->head) + { + dyn->length = 0; + dyn->tail = 0; + } +} + +unsigned int dbuf_get(struct DBuf *dyn, char *buf, unsigned int length) +{ + unsigned int moved = 0; + unsigned int chunk; + const char *b; + + assert(0 != dyn); + assert(0 != buf); + + while (length > 0 && (b = dbuf_map(dyn, &chunk)) != 0) + { + if (chunk > length) + chunk = length; + + memcpy(buf, b, chunk); + dbuf_delete(dyn, chunk); + + buf += chunk; + length -= chunk; + moved += chunk; + } + return moved; +} + +static unsigned int dbuf_flush(struct DBuf *dyn) +{ + struct DBufBuffer *db = dyn->head; + + if (0 == db) + return 0; + + assert(db->start < db->end); + + while (IsEol(*db->start)) + { + if (++db->start == db->end) + { + dyn->head = db->next; + dbuf_free(db); + if (0 == (db = dyn->head)) + { + dyn->tail = 0; + dyn->length = 0; + break; + } + } + --dyn->length; + } + return dyn->length; +} + +unsigned int dbuf_getmsg(struct DBuf *dyn, char *buf, unsigned int length) +{ + struct DBufBuffer *db; + char *start; + char *end; + unsigned int count; + unsigned int copied = 0; + + assert(0 != dyn); + assert(0 != buf); + + if (0 == dbuf_flush(dyn)) + return 0; + + assert(0 != dyn->head); + + db = dyn->head; + start = db->start; + + assert(start < db->end); + + if (length > dyn->length) + length = dyn->length; + + while (length > 0) + { + end = IRCD_MIN(db->end, (start + length)); + while (start < end && !IsEol(*start)) + *buf++ = *start++; + + count = start - db->start; + if (start < end) + { + *buf = '\0'; + copied += count; + dbuf_delete(dyn, copied); + dbuf_flush(dyn); + return copied; + } + if (0 == (db = db->next)) + break; + copied += count; + length -= count; + start = db->start; + } + return 0; +} + +void dbuf_count_memory(size_t *allocated, size_t *used) +{ + assert(0 != allocated); + assert(0 != used); + *allocated = DBufAllocCount * sizeof(struct DBufBuffer); + *used = DBufUsedCount * sizeof(struct DBufBuffer); +} + + +/* ========== Test fixtures ========== */ + +static int setup_dbuf(void **state) +{ + struct DBuf *dyn = malloc(sizeof(struct DBuf)); + memset(dyn, 0, sizeof(*dyn)); + *state = dyn; + return 0; +} + +static int teardown_dbuf(void **state) +{ + struct DBuf *dyn = *state; + DBufClear(dyn); + free(dyn); + return 0; +} + + +/* ========== DBufLength / empty buffer tests ========== */ + +static void test_empty_dbuf(void **state) +{ + struct DBuf *dyn = *state; + + assert_int_equal(0, DBufLength(dyn)); + assert_null(dyn->head); + assert_null(dyn->tail); +} + + +/* ========== dbuf_put tests ========== */ + +static void test_dbuf_put_small(void **state) +{ + struct DBuf *dyn = *state; + const char *data = "Hello, World!"; + int result; + + result = dbuf_put(dyn, data, strlen(data)); + + assert_int_equal(1, result); /* Success */ + assert_int_equal(strlen(data), DBufLength(dyn)); + assert_non_null(dyn->head); + assert_non_null(dyn->tail); +} + +static void test_dbuf_put_multiple(void **state) +{ + struct DBuf *dyn = *state; + const char *data1 = "First"; + const char *data2 = "Second"; + int result; + + result = dbuf_put(dyn, data1, strlen(data1)); + assert_int_equal(1, result); + + result = dbuf_put(dyn, data2, strlen(data2)); + assert_int_equal(1, result); + + assert_int_equal(strlen(data1) + strlen(data2), DBufLength(dyn)); +} + +static void test_dbuf_put_exact_buffer(void **state) +{ + struct DBuf *dyn = *state; + char data[DBUF_SIZE]; + int result; + + memset(data, 'A', sizeof(data)); + + result = dbuf_put(dyn, data, sizeof(data)); + + assert_int_equal(1, result); + assert_int_equal(DBUF_SIZE, DBufLength(dyn)); +} + +static void test_dbuf_put_cross_buffer(void **state) +{ + struct DBuf *dyn = *state; + char data[DBUF_SIZE + 100]; + int result; + + memset(data, 'B', sizeof(data)); + + result = dbuf_put(dyn, data, sizeof(data)); + + assert_int_equal(1, result); + assert_int_equal(DBUF_SIZE + 100, DBufLength(dyn)); + /* Should have allocated 2 buffers */ + assert_non_null(dyn->head); + assert_non_null(dyn->head->next); +} + + +/* ========== dbuf_map tests ========== */ + +static void test_dbuf_map_empty(void **state) +{ + struct DBuf *dyn = *state; + unsigned int length; + const char *mapped; + + mapped = dbuf_map(dyn, &length); + + assert_null(mapped); + assert_int_equal(0, length); +} + +static void test_dbuf_map_data(void **state) +{ + struct DBuf *dyn = *state; + const char *data = "Test data"; + unsigned int length; + const char *mapped; + + dbuf_put(dyn, data, strlen(data)); + + mapped = dbuf_map(dyn, &length); + + assert_non_null(mapped); + assert_int_equal(strlen(data), length); + assert_memory_equal(data, mapped, length); +} + + +/* ========== dbuf_delete tests ========== */ + +static void test_dbuf_delete_partial(void **state) +{ + struct DBuf *dyn = *state; + const char *data = "Hello, World!"; + + dbuf_put(dyn, data, strlen(data)); + dbuf_delete(dyn, 7); /* Delete "Hello, " */ + + assert_int_equal(strlen(data) - 7, DBufLength(dyn)); +} + +static void test_dbuf_delete_all(void **state) +{ + struct DBuf *dyn = *state; + const char *data = "Hello, World!"; + + dbuf_put(dyn, data, strlen(data)); + dbuf_delete(dyn, strlen(data)); + + assert_int_equal(0, DBufLength(dyn)); + assert_null(dyn->head); + assert_null(dyn->tail); +} + +static void test_dbuf_delete_more_than_exists(void **state) +{ + struct DBuf *dyn = *state; + const char *data = "Short"; + + dbuf_put(dyn, data, strlen(data)); + dbuf_delete(dyn, 1000); /* Try to delete more than exists */ + + assert_int_equal(0, DBufLength(dyn)); +} + +static void test_dbuf_delete_cross_buffer(void **state) +{ + struct DBuf *dyn = *state; + char data[DBUF_SIZE + 100]; + + memset(data, 'X', sizeof(data)); + dbuf_put(dyn, data, sizeof(data)); + + /* Delete first buffer and some of second */ + dbuf_delete(dyn, DBUF_SIZE + 50); + + assert_int_equal(50, DBufLength(dyn)); +} + + +/* ========== dbuf_get tests ========== */ + +static void test_dbuf_get_all(void **state) +{ + struct DBuf *dyn = *state; + const char *data = "Hello, World!"; + char buf[64]; + unsigned int got; + + dbuf_put(dyn, data, strlen(data)); + got = dbuf_get(dyn, buf, sizeof(buf)); + + assert_int_equal(strlen(data), got); + assert_memory_equal(data, buf, got); + assert_int_equal(0, DBufLength(dyn)); +} + +static void test_dbuf_get_partial(void **state) +{ + struct DBuf *dyn = *state; + const char *data = "Hello, World!"; + char buf[5]; + unsigned int got; + + dbuf_put(dyn, data, strlen(data)); + got = dbuf_get(dyn, buf, sizeof(buf)); + + assert_int_equal(5, got); + assert_memory_equal(data, buf, got); + assert_int_equal(strlen(data) - 5, DBufLength(dyn)); +} + +static void test_dbuf_get_empty(void **state) +{ + struct DBuf *dyn = *state; + char buf[64]; + unsigned int got; + + got = dbuf_get(dyn, buf, sizeof(buf)); + + assert_int_equal(0, got); +} + + +/* ========== dbuf_getmsg tests ========== */ + +static void test_dbuf_getmsg_simple(void **state) +{ + struct DBuf *dyn = *state; + const char *data = "Line one\r\nLine two\r\n"; + char buf[64]; + unsigned int got; + + dbuf_put(dyn, data, strlen(data)); + got = dbuf_getmsg(dyn, buf, sizeof(buf)); + + assert_int_equal(8, got); /* "Line one" without \r\n */ + assert_string_equal("Line one", buf); +} + +static void test_dbuf_getmsg_multiple(void **state) +{ + struct DBuf *dyn = *state; + const char *data = "First\r\nSecond\r\n"; + char buf[64]; + unsigned int got; + + dbuf_put(dyn, data, strlen(data)); + + got = dbuf_getmsg(dyn, buf, sizeof(buf)); + assert_int_equal(5, got); + assert_string_equal("First", buf); + + got = dbuf_getmsg(dyn, buf, sizeof(buf)); + assert_int_equal(6, got); + assert_string_equal("Second", buf); +} + +static void test_dbuf_getmsg_no_eol(void **state) +{ + struct DBuf *dyn = *state; + const char *data = "No line ending"; + char buf[64]; + unsigned int got; + + dbuf_put(dyn, data, strlen(data)); + got = dbuf_getmsg(dyn, buf, sizeof(buf)); + + /* Should return 0 when no complete line available */ + assert_int_equal(0, got); +} + +static void test_dbuf_getmsg_leading_eol(void **state) +{ + struct DBuf *dyn = *state; + const char *data = "\r\n\r\nActual line\r\n"; + char buf[64]; + unsigned int got; + + dbuf_put(dyn, data, strlen(data)); + got = dbuf_getmsg(dyn, buf, sizeof(buf)); + + /* Should skip leading EOLs and get "Actual line" */ + assert_int_equal(11, got); + assert_string_equal("Actual line", buf); +} + + +/* ========== DBufClear tests ========== */ + +static void test_dbuf_clear(void **state) +{ + struct DBuf *dyn = *state; + char data[DBUF_SIZE * 3]; + + memset(data, 'Z', sizeof(data)); + dbuf_put(dyn, data, sizeof(data)); + + assert_true(DBufLength(dyn) > 0); + + DBufClear(dyn); + + assert_int_equal(0, DBufLength(dyn)); + assert_null(dyn->head); + assert_null(dyn->tail); +} + + +/* ========== Memory accounting tests ========== */ + +static void test_dbuf_count_memory(void **state) +{ + struct DBuf *dyn = *state; + size_t allocated, used; + char data[DBUF_SIZE + 1]; + + memset(data, 'M', sizeof(data)); + dbuf_put(dyn, data, sizeof(data)); + + dbuf_count_memory(&allocated, &used); + + /* Should have allocated at least 2 buffers */ + assert_true(allocated >= 2 * sizeof(struct DBufBuffer)); + assert_true(used >= 2 * sizeof(struct DBufBuffer)); +} + + +/* ========== Round-trip tests ========== */ + +static void test_dbuf_roundtrip_small(void **state) +{ + struct DBuf *dyn = *state; + const char *original = "Round trip test data"; + char result[64]; + unsigned int got; + + dbuf_put(dyn, original, strlen(original)); + got = dbuf_get(dyn, result, sizeof(result)); + + assert_int_equal(strlen(original), got); + assert_memory_equal(original, result, got); +} + +static void test_dbuf_roundtrip_large(void **state) +{ + struct DBuf *dyn = *state; + char original[DBUF_SIZE * 3]; + char result[DBUF_SIZE * 3]; + unsigned int got; + + /* Fill with pattern */ + for (size_t i = 0; i < sizeof(original); i++) { + original[i] = (char)('A' + (i % 26)); + } + + dbuf_put(dyn, original, sizeof(original)); + got = dbuf_get(dyn, result, sizeof(result)); + + assert_int_equal(sizeof(original), got); + assert_memory_equal(original, result, got); +} + + +int main(void) +{ + const struct CMUnitTest tests[] = { + /* Empty buffer */ + cmocka_unit_test_setup_teardown(test_empty_dbuf, setup_dbuf, teardown_dbuf), + + /* dbuf_put */ + cmocka_unit_test_setup_teardown(test_dbuf_put_small, setup_dbuf, teardown_dbuf), + cmocka_unit_test_setup_teardown(test_dbuf_put_multiple, setup_dbuf, teardown_dbuf), + cmocka_unit_test_setup_teardown(test_dbuf_put_exact_buffer, setup_dbuf, teardown_dbuf), + cmocka_unit_test_setup_teardown(test_dbuf_put_cross_buffer, setup_dbuf, teardown_dbuf), + + /* dbuf_map */ + cmocka_unit_test_setup_teardown(test_dbuf_map_empty, setup_dbuf, teardown_dbuf), + cmocka_unit_test_setup_teardown(test_dbuf_map_data, setup_dbuf, teardown_dbuf), + + /* dbuf_delete */ + cmocka_unit_test_setup_teardown(test_dbuf_delete_partial, setup_dbuf, teardown_dbuf), + cmocka_unit_test_setup_teardown(test_dbuf_delete_all, setup_dbuf, teardown_dbuf), + cmocka_unit_test_setup_teardown(test_dbuf_delete_more_than_exists, setup_dbuf, teardown_dbuf), + cmocka_unit_test_setup_teardown(test_dbuf_delete_cross_buffer, setup_dbuf, teardown_dbuf), + + /* dbuf_get */ + cmocka_unit_test_setup_teardown(test_dbuf_get_all, setup_dbuf, teardown_dbuf), + cmocka_unit_test_setup_teardown(test_dbuf_get_partial, setup_dbuf, teardown_dbuf), + cmocka_unit_test_setup_teardown(test_dbuf_get_empty, setup_dbuf, teardown_dbuf), + + /* dbuf_getmsg */ + cmocka_unit_test_setup_teardown(test_dbuf_getmsg_simple, setup_dbuf, teardown_dbuf), + cmocka_unit_test_setup_teardown(test_dbuf_getmsg_multiple, setup_dbuf, teardown_dbuf), + cmocka_unit_test_setup_teardown(test_dbuf_getmsg_no_eol, setup_dbuf, teardown_dbuf), + cmocka_unit_test_setup_teardown(test_dbuf_getmsg_leading_eol, setup_dbuf, teardown_dbuf), + + /* DBufClear */ + cmocka_unit_test_setup_teardown(test_dbuf_clear, setup_dbuf, teardown_dbuf), + + /* Memory accounting */ + cmocka_unit_test_setup_teardown(test_dbuf_count_memory, setup_dbuf, teardown_dbuf), + + /* Round-trip */ + cmocka_unit_test_setup_teardown(test_dbuf_roundtrip_small, setup_dbuf, teardown_dbuf), + cmocka_unit_test_setup_teardown(test_dbuf_roundtrip_large, setup_dbuf, teardown_dbuf), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/ircd/test/history_cmocka.c b/ircd/test/history_cmocka.c new file mode 100644 index 00000000..b13428e7 --- /dev/null +++ b/ircd/test/history_cmocka.c @@ -0,0 +1,681 @@ +/* history_cmocka.c - CMocka unit tests for history serialization functions + * + * Tests the pure functions from history.c without requiring LMDB: + * - build_key() - Key construction + * - parse_key() - Key parsing + * - serialize_message() - Message serialization + * - deserialize_message() - Message deserialization + * - parse_reference() - Reference string parsing (from m_chathistory.c) + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include + +/* Include necessary headers */ +#include "ircd_defs.h" +#include "ircd_chattr.h" + +/* Define constants from history.c */ +#define KEY_SEP '\0' +#define HISTORY_VALUE_BUFSIZE 1024 + +#define HISTORY_MSGID_LEN 64 +#define HISTORY_TIMESTAMP_LEN 32 +#define HISTORY_SENDER_LEN (NICKLEN + USERLEN + HOSTLEN + 3) +#define HISTORY_CONTENT_LEN 512 + +/* Message types from history.h */ +enum HistoryMessageType { + HISTORY_PRIVMSG = 0, + HISTORY_NOTICE = 1, + HISTORY_JOIN = 2, + HISTORY_PART = 3, + HISTORY_QUIT = 4, + HISTORY_KICK = 5, + HISTORY_MODE = 6, + HISTORY_TOPIC = 7, + HISTORY_TAGMSG = 8 +}; + +/* Reference types from history.h */ +enum HistoryRefType { + HISTORY_REF_TIMESTAMP = 0, + HISTORY_REF_MSGID = 1, + HISTORY_REF_NONE = 2 +}; + +/* HistoryMessage structure from history.h */ +struct HistoryMessage { + char msgid[HISTORY_MSGID_LEN]; + char timestamp[HISTORY_TIMESTAMP_LEN]; + char target[CHANNELLEN + 1]; + char sender[HISTORY_SENDER_LEN]; + char account[ACCOUNTLEN + 1]; + enum HistoryMessageType type; + char content[HISTORY_CONTENT_LEN]; + struct HistoryMessage *next; +}; + +/* Stub for ircd_snprintf - use regular snprintf */ +#define ircd_snprintf(client, buf, size, fmt, ...) snprintf(buf, size, fmt, ##__VA_ARGS__) + +/* ========== Inlined functions from history.c ========== */ + +static int build_key(char *key, int keysize, const char *target, + const char *timestamp, const char *msgid) +{ + int pos = 0; + int len; + + /* Copy target */ + len = strlen(target); + if (pos + len + 1 >= keysize) return -1; + memcpy(key + pos, target, len); + pos += len; + key[pos++] = KEY_SEP; + + /* Copy timestamp if provided */ + if (timestamp) { + len = strlen(timestamp); + if (pos + len + 1 >= keysize) return -1; + memcpy(key + pos, timestamp, len); + pos += len; + key[pos++] = KEY_SEP; + + /* Copy msgid if provided */ + if (msgid) { + len = strlen(msgid); + if (pos + len >= keysize) return -1; + memcpy(key + pos, msgid, len); + pos += len; + } + } + + return pos; +} + +static int parse_key(const char *key, int keylen, + char *target, char *timestamp, char *msgid) +{ + const char *p, *end; + const char *sep1, *sep2; + + p = key; + end = key + keylen; + + /* Find first separator (end of target) */ + sep1 = memchr(p, KEY_SEP, end - p); + if (!sep1) return -1; + + if (target) { + if ((size_t)(sep1 - p) > CHANNELLEN) return -1; + memcpy(target, p, sep1 - p); + target[sep1 - p] = '\0'; + } + p = sep1 + 1; + + /* Find second separator (end of timestamp) */ + sep2 = memchr(p, KEY_SEP, end - p); + if (sep2) { + if (timestamp) { + if ((size_t)(sep2 - p) >= HISTORY_TIMESTAMP_LEN) return -1; + memcpy(timestamp, p, sep2 - p); + timestamp[sep2 - p] = '\0'; + } + p = sep2 + 1; + + if (msgid) { + if ((size_t)(end - p) >= HISTORY_MSGID_LEN) return -1; + memcpy(msgid, p, end - p); + msgid[end - p] = '\0'; + } + } else { + /* No msgid in key */ + if (timestamp) { + if ((size_t)(end - p) >= HISTORY_TIMESTAMP_LEN) return -1; + memcpy(timestamp, p, end - p); + timestamp[end - p] = '\0'; + } + if (msgid) + msgid[0] = '\0'; + } + + return 0; +} + +static int serialize_message(char *buf, int bufsize, + enum HistoryMessageType type, + const char *sender, const char *account, + const char *content) +{ + return ircd_snprintf(0, buf, bufsize, "%d|%s|%s|%s", + (int)type, + sender ? sender : "", + account ? account : "", + content ? content : ""); +} + +static int deserialize_message(const char *data, int datalen, + struct HistoryMessage *msg) +{ + const char *p, *end; + char *field; + int type; + + p = data; + end = data + datalen; + + /* Parse type */ + field = strchr(p, '|'); + if (!field || field >= end) return -1; + type = atoi(p); + if (type < 0 || type > HISTORY_TAGMSG) return -1; + msg->type = (enum HistoryMessageType)type; + p = field + 1; + + /* Parse sender */ + field = strchr(p, '|'); + if (!field || field >= end) return -1; + if ((size_t)(field - p) >= sizeof(msg->sender)) return -1; + memcpy(msg->sender, p, field - p); + msg->sender[field - p] = '\0'; + p = field + 1; + + /* Parse account */ + field = strchr(p, '|'); + if (!field || field >= end) return -1; + if ((size_t)(field - p) >= sizeof(msg->account)) return -1; + memcpy(msg->account, p, field - p); + msg->account[field - p] = '\0'; + p = field + 1; + + /* Parse content - rest of string */ + if ((size_t)(end - p) >= sizeof(msg->content)) return -1; + memcpy(msg->content, p, end - p); + msg->content[end - p] = '\0'; + + return 0; +} + +/* parse_reference from m_chathistory.c */ +static int parse_reference(const char *ref, enum HistoryRefType *ref_type, const char **value) +{ + if (!ref || !*ref) + return -1; + + if (*ref == '*') { + *ref_type = HISTORY_REF_NONE; + *value = ref; + return 0; + } + + if (strncmp(ref, "timestamp=", 10) == 0) { + *ref_type = HISTORY_REF_TIMESTAMP; + *value = ref + 10; + return 0; + } + + if (strncmp(ref, "msgid=", 6) == 0) { + *ref_type = HISTORY_REF_MSGID; + *value = ref + 6; + return 0; + } + + return -1; +} + +/* ========== build_key Tests ========== */ + +static void test_build_key_target_only(void **state) +{ + (void)state; + char key[256]; + int len; + + len = build_key(key, sizeof(key), "#channel", NULL, NULL); + assert_int_equal(len, 9); /* "#channel" + KEY_SEP */ + assert_memory_equal(key, "#channel\0", 9); +} + +static void test_build_key_with_timestamp(void **state) +{ + (void)state; + char key[256]; + int len; + + len = build_key(key, sizeof(key), "#channel", "2024-01-15T12:30:00Z", NULL); + assert_int_equal(len, 30); /* "#channel" + SEP + "2024-01-15T12:30:00Z" + SEP */ + assert_memory_equal(key, "#channel\0" "2024-01-15T12:30:00Z\0", 30); +} + +static void test_build_key_with_msgid(void **state) +{ + (void)state; + char key[256]; + int len; + + len = build_key(key, sizeof(key), "#channel", "2024-01-15T12:30:00Z", "abc123"); + assert_int_equal(len, 36); + assert_memory_equal(key, "#channel\0" "2024-01-15T12:30:00Z\0" "abc123", 36); +} + +static void test_build_key_buffer_too_small(void **state) +{ + (void)state; + char key[10]; + int len; + + len = build_key(key, sizeof(key), "#verylongchannelname", NULL, NULL); + assert_int_equal(len, -1); +} + +static void test_build_key_dm_target(void **state) +{ + (void)state; + char key[256]; + int len; + + /* DM targets use "$nick1,nick2" format */ + len = build_key(key, sizeof(key), "$alice,bob", "2024-01-15T12:30:00Z", NULL); + assert_true(len > 0); + assert_memory_equal(key, "$alice,bob\0", 11); +} + +/* ========== parse_key Tests ========== */ + +static void test_parse_key_target_timestamp_msgid(void **state) +{ + (void)state; + char key[256]; + char target[64], timestamp[64], msgid[64]; + int len, rc; + + len = build_key(key, sizeof(key), "#test", "2024-01-15T12:30:00Z", "msg123"); + assert_true(len > 0); + + rc = parse_key(key, len, target, timestamp, msgid); + assert_int_equal(rc, 0); + assert_string_equal(target, "#test"); + assert_string_equal(timestamp, "2024-01-15T12:30:00Z"); + assert_string_equal(msgid, "msg123"); +} + +static void test_parse_key_target_timestamp_only(void **state) +{ + (void)state; + char key[256]; + char target[64], timestamp[64], msgid[64]; + int len, rc; + + len = build_key(key, sizeof(key), "#test", "2024-01-15T12:30:00Z", NULL); + assert_true(len > 0); + + rc = parse_key(key, len, target, timestamp, msgid); + assert_int_equal(rc, 0); + assert_string_equal(target, "#test"); + assert_string_equal(timestamp, "2024-01-15T12:30:00Z"); + assert_string_equal(msgid, ""); +} + +static void test_parse_key_null_outputs(void **state) +{ + (void)state; + char key[256]; + int len, rc; + + len = build_key(key, sizeof(key), "#test", "2024-01-15T12:30:00Z", "msg123"); + assert_true(len > 0); + + /* All NULL outputs should still succeed */ + rc = parse_key(key, len, NULL, NULL, NULL); + assert_int_equal(rc, 0); +} + +static void test_parse_key_no_separator(void **state) +{ + (void)state; + char target[64]; + int rc; + + /* Key with no separator should fail */ + rc = parse_key("noseparator", 11, target, NULL, NULL); + assert_int_equal(rc, -1); +} + +static void test_parse_key_roundtrip(void **state) +{ + (void)state; + char key[256]; + char target[64], timestamp[64], msgid[64]; + int len, rc; + + /* Test various targets */ + const char *targets[] = {"#channel", "#foo-bar", "$nick1,nick2", "&local"}; + + for (int i = 0; i < 4; i++) { + len = build_key(key, sizeof(key), targets[i], "2024-12-25T00:00:00Z", "id456"); + assert_true(len > 0); + + rc = parse_key(key, len, target, timestamp, msgid); + assert_int_equal(rc, 0); + assert_string_equal(target, targets[i]); + assert_string_equal(timestamp, "2024-12-25T00:00:00Z"); + assert_string_equal(msgid, "id456"); + } +} + +/* ========== serialize_message Tests ========== */ + +static void test_serialize_privmsg(void **state) +{ + (void)state; + char buf[512]; + int len; + + len = serialize_message(buf, sizeof(buf), HISTORY_PRIVMSG, + "nick!user@host", "account", "Hello world"); + assert_true(len > 0); + assert_string_equal(buf, "0|nick!user@host|account|Hello world"); +} + +static void test_serialize_notice(void **state) +{ + (void)state; + char buf[512]; + int len; + + len = serialize_message(buf, sizeof(buf), HISTORY_NOTICE, + "nick!user@host", "account", "Notice message"); + assert_true(len > 0); + assert_string_equal(buf, "1|nick!user@host|account|Notice message"); +} + +static void test_serialize_join(void **state) +{ + (void)state; + char buf[512]; + int len; + + len = serialize_message(buf, sizeof(buf), HISTORY_JOIN, + "nick!user@host", "account", NULL); + assert_true(len > 0); + assert_string_equal(buf, "2|nick!user@host|account|"); +} + +static void test_serialize_null_account(void **state) +{ + (void)state; + char buf[512]; + int len; + + len = serialize_message(buf, sizeof(buf), HISTORY_PRIVMSG, + "nick!user@host", NULL, "Message"); + assert_true(len > 0); + assert_string_equal(buf, "0|nick!user@host||Message"); +} + +static void test_serialize_empty_content(void **state) +{ + (void)state; + char buf[512]; + int len; + + len = serialize_message(buf, sizeof(buf), HISTORY_TAGMSG, + "nick!user@host", "account", ""); + assert_true(len > 0); + assert_string_equal(buf, "8|nick!user@host|account|"); +} + +/* ========== deserialize_message Tests ========== */ + +static void test_deserialize_privmsg(void **state) +{ + (void)state; + struct HistoryMessage msg; + const char *data = "0|nick!user@host|account|Hello world"; + int rc; + + memset(&msg, 0, sizeof(msg)); + rc = deserialize_message(data, strlen(data), &msg); + assert_int_equal(rc, 0); + assert_int_equal(msg.type, HISTORY_PRIVMSG); + assert_string_equal(msg.sender, "nick!user@host"); + assert_string_equal(msg.account, "account"); + assert_string_equal(msg.content, "Hello world"); +} + +static void test_deserialize_join(void **state) +{ + (void)state; + struct HistoryMessage msg; + const char *data = "2|nick!user@host|myaccount|"; + int rc; + + memset(&msg, 0, sizeof(msg)); + rc = deserialize_message(data, strlen(data), &msg); + assert_int_equal(rc, 0); + assert_int_equal(msg.type, HISTORY_JOIN); + assert_string_equal(msg.sender, "nick!user@host"); + assert_string_equal(msg.account, "myaccount"); + assert_string_equal(msg.content, ""); +} + +static void test_deserialize_empty_account(void **state) +{ + (void)state; + struct HistoryMessage msg; + const char *data = "0|nick!user@host||Some message"; + int rc; + + memset(&msg, 0, sizeof(msg)); + rc = deserialize_message(data, strlen(data), &msg); + assert_int_equal(rc, 0); + assert_string_equal(msg.account, ""); + assert_string_equal(msg.content, "Some message"); +} + +static void test_deserialize_invalid_type(void **state) +{ + (void)state; + struct HistoryMessage msg; + const char *data = "99|nick!user@host|account|msg"; + int rc; + + rc = deserialize_message(data, strlen(data), &msg); + assert_int_equal(rc, -1); +} + +static void test_deserialize_missing_field(void **state) +{ + (void)state; + struct HistoryMessage msg; + const char *data = "0|nick!user@host"; /* Missing account and content */ + int rc; + + rc = deserialize_message(data, strlen(data), &msg); + assert_int_equal(rc, -1); +} + +static void test_serialize_deserialize_roundtrip(void **state) +{ + (void)state; + char buf[512]; + struct HistoryMessage msg; + int len, rc; + + len = serialize_message(buf, sizeof(buf), HISTORY_PRIVMSG, + "test!user@example.com", "testaccount", + "This is a test message"); + assert_true(len > 0); + + memset(&msg, 0, sizeof(msg)); + rc = deserialize_message(buf, len, &msg); + assert_int_equal(rc, 0); + assert_int_equal(msg.type, HISTORY_PRIVMSG); + assert_string_equal(msg.sender, "test!user@example.com"); + assert_string_equal(msg.account, "testaccount"); + assert_string_equal(msg.content, "This is a test message"); +} + +static void test_deserialize_all_message_types(void **state) +{ + (void)state; + struct HistoryMessage msg; + char buf[512]; + int len, rc; + + const char *type_names[] = { + "PRIVMSG", "NOTICE", "JOIN", "PART", "QUIT", + "KICK", "MODE", "TOPIC", "TAGMSG" + }; + + for (int i = 0; i <= HISTORY_TAGMSG; i++) { + len = serialize_message(buf, sizeof(buf), (enum HistoryMessageType)i, + "nick!user@host", "acc", type_names[i]); + assert_true(len > 0); + + memset(&msg, 0, sizeof(msg)); + rc = deserialize_message(buf, len, &msg); + assert_int_equal(rc, 0); + assert_int_equal(msg.type, i); + assert_string_equal(msg.content, type_names[i]); + } +} + +/* ========== parse_reference Tests ========== */ + +static void test_parse_reference_timestamp(void **state) +{ + (void)state; + enum HistoryRefType ref_type; + const char *value; + int rc; + + rc = parse_reference("timestamp=2024-01-15T12:30:00Z", &ref_type, &value); + assert_int_equal(rc, 0); + assert_int_equal(ref_type, HISTORY_REF_TIMESTAMP); + assert_string_equal(value, "2024-01-15T12:30:00Z"); +} + +static void test_parse_reference_msgid(void **state) +{ + (void)state; + enum HistoryRefType ref_type; + const char *value; + int rc; + + rc = parse_reference("msgid=abc123def456", &ref_type, &value); + assert_int_equal(rc, 0); + assert_int_equal(ref_type, HISTORY_REF_MSGID); + assert_string_equal(value, "abc123def456"); +} + +static void test_parse_reference_star(void **state) +{ + (void)state; + enum HistoryRefType ref_type; + const char *value; + int rc; + + rc = parse_reference("*", &ref_type, &value); + assert_int_equal(rc, 0); + assert_int_equal(ref_type, HISTORY_REF_NONE); + assert_string_equal(value, "*"); +} + +static void test_parse_reference_null(void **state) +{ + (void)state; + enum HistoryRefType ref_type; + const char *value; + int rc; + + rc = parse_reference(NULL, &ref_type, &value); + assert_int_equal(rc, -1); + + rc = parse_reference("", &ref_type, &value); + assert_int_equal(rc, -1); +} + +static void test_parse_reference_invalid(void **state) +{ + (void)state; + enum HistoryRefType ref_type; + const char *value; + int rc; + + rc = parse_reference("invalid=something", &ref_type, &value); + assert_int_equal(rc, -1); + + rc = parse_reference("justtext", &ref_type, &value); + assert_int_equal(rc, -1); +} + +static void test_parse_reference_case_sensitive(void **state) +{ + (void)state; + enum HistoryRefType ref_type; + const char *value; + int rc; + + /* Reference parsing is case-sensitive per IRC spec */ + rc = parse_reference("TIMESTAMP=2024-01-15T12:30:00Z", &ref_type, &value); + assert_int_equal(rc, -1); + + rc = parse_reference("MSGID=abc123", &ref_type, &value); + assert_int_equal(rc, -1); +} + +/* ========== Main ========== */ + +int main(void) +{ + const struct CMUnitTest tests[] = { + /* build_key tests */ + cmocka_unit_test(test_build_key_target_only), + cmocka_unit_test(test_build_key_with_timestamp), + cmocka_unit_test(test_build_key_with_msgid), + cmocka_unit_test(test_build_key_buffer_too_small), + cmocka_unit_test(test_build_key_dm_target), + + /* parse_key tests */ + cmocka_unit_test(test_parse_key_target_timestamp_msgid), + cmocka_unit_test(test_parse_key_target_timestamp_only), + cmocka_unit_test(test_parse_key_null_outputs), + cmocka_unit_test(test_parse_key_no_separator), + cmocka_unit_test(test_parse_key_roundtrip), + + /* serialize_message tests */ + cmocka_unit_test(test_serialize_privmsg), + cmocka_unit_test(test_serialize_notice), + cmocka_unit_test(test_serialize_join), + cmocka_unit_test(test_serialize_null_account), + cmocka_unit_test(test_serialize_empty_content), + + /* deserialize_message tests */ + cmocka_unit_test(test_deserialize_privmsg), + cmocka_unit_test(test_deserialize_join), + cmocka_unit_test(test_deserialize_empty_account), + cmocka_unit_test(test_deserialize_invalid_type), + cmocka_unit_test(test_deserialize_missing_field), + cmocka_unit_test(test_serialize_deserialize_roundtrip), + cmocka_unit_test(test_deserialize_all_message_types), + + /* parse_reference tests */ + cmocka_unit_test(test_parse_reference_timestamp), + cmocka_unit_test(test_parse_reference_msgid), + cmocka_unit_test(test_parse_reference_star), + cmocka_unit_test(test_parse_reference_null), + cmocka_unit_test(test_parse_reference_invalid), + cmocka_unit_test(test_parse_reference_case_sensitive), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/ircd/test/ircd_chattr_cmocka.c b/ircd/test/ircd_chattr_cmocka.c new file mode 100644 index 00000000..8bcc194a --- /dev/null +++ b/ircd/test/ircd_chattr_cmocka.c @@ -0,0 +1,256 @@ +/* + * ircd_chattr_cmocka.c - CMocka unit tests for character attributes + * + * This demonstrates CMocka-style testing for nefarious IRCd. + * Unlike the original ircd_chattr_t.c which just prints output, + * these tests have proper assertions. + */ + +#include +#include +#include +#include + +#include "ircd_chattr.h" + +/* Test that digits 0-9 are recognized as digits */ +static void test_IsDigit(void **state) +{ + (void)state; + + /* All digits should pass */ + assert_true(IsDigit('0')); + assert_true(IsDigit('5')); + assert_true(IsDigit('9')); + + /* Letters should not be digits */ + assert_false(IsDigit('a')); + assert_false(IsDigit('Z')); + + /* Special chars should not be digits */ + assert_false(IsDigit(' ')); + assert_false(IsDigit('#')); + assert_false(IsDigit('\0')); +} + +/* Test alpha character classification */ +static void test_IsAlpha(void **state) +{ + (void)state; + + /* Lowercase letters */ + assert_true(IsAlpha('a')); + assert_true(IsAlpha('m')); + assert_true(IsAlpha('z')); + + /* Uppercase letters */ + assert_true(IsAlpha('A')); + assert_true(IsAlpha('M')); + assert_true(IsAlpha('Z')); + + /* Non-alpha */ + assert_false(IsAlpha('0')); + assert_false(IsAlpha(' ')); + assert_false(IsAlpha('#')); +} + +/* Test alphanumeric classification */ +static void test_IsAlnum(void **state) +{ + (void)state; + + /* Alpha */ + assert_true(IsAlnum('a')); + assert_true(IsAlnum('Z')); + + /* Numeric */ + assert_true(IsAlnum('0')); + assert_true(IsAlnum('9')); + + /* Non-alnum */ + assert_false(IsAlnum(' ')); + assert_false(IsAlnum('#')); + assert_false(IsAlnum('\n')); +} + +/* Test channel prefix characters */ +static void test_IsChannelPrefix(void **state) +{ + (void)state; + + /* Valid channel prefixes */ + assert_true(IsChannelPrefix('#')); + assert_true(IsChannelPrefix('&')); + + /* Invalid channel prefixes */ + assert_false(IsChannelPrefix('!')); /* Not supported in nefarious */ + assert_false(IsChannelPrefix('+')); /* Modeless channels not default */ + assert_false(IsChannelPrefix('a')); + assert_false(IsChannelPrefix('1')); +} + +/* Test valid nickname characters */ +static void test_IsNickChar(void **state) +{ + (void)state; + + /* Letters are valid */ + assert_true(IsNickChar('a')); + assert_true(IsNickChar('Z')); + + /* Digits are valid */ + assert_true(IsNickChar('0')); + assert_true(IsNickChar('9')); + + /* Special nick chars */ + assert_true(IsNickChar('[')); + assert_true(IsNickChar(']')); + assert_true(IsNickChar('\\')); + assert_true(IsNickChar('`')); + assert_true(IsNickChar('^')); + assert_true(IsNickChar('{')); + assert_true(IsNickChar('}')); + assert_true(IsNickChar('|')); + assert_true(IsNickChar('-')); + assert_true(IsNickChar('_')); + + /* Invalid nick chars */ + assert_false(IsNickChar(' ')); + assert_false(IsNickChar('#')); + assert_false(IsNickChar('@')); + assert_false(IsNickChar('!')); + assert_false(IsNickChar('\0')); + assert_false(IsNickChar('\n')); +} + +/* Test channel name characters (excluding prefix) */ +static void test_IsChannelChar(void **state) +{ + (void)state; + + /* Valid channel chars */ + assert_true(IsChannelChar('a')); + assert_true(IsChannelChar('Z')); + assert_true(IsChannelChar('0')); + assert_true(IsChannelChar('-')); + assert_true(IsChannelChar('_')); + + /* Invalid channel chars */ + assert_false(IsChannelChar(' ')); /* No spaces */ + assert_false(IsChannelChar('\007')); /* No bell */ + assert_false(IsChannelChar(',')); /* No comma (separator) */ + assert_false(IsChannelChar('\0')); /* No null */ +} + +/* Test control characters + * Note: ircu defines NTL_CNTRL as 0x00-0x1F only, excluding DEL (0x7F). + * This differs from C's iscntrl() which includes DEL. This is intentional + * ircu behavior preserved in nefarious. */ +static void test_IsCntrl(void **state) +{ + (void)state; + + /* Control characters (0x00-0x1F per ircu definition) */ + assert_true(IsCntrl('\0')); + assert_true(IsCntrl('\t')); + assert_true(IsCntrl('\n')); + assert_true(IsCntrl('\r')); + assert_true(IsCntrl('\007')); /* Bell */ + assert_true(IsCntrl(0x1F)); + + /* Not control characters */ + assert_false(IsCntrl(' ')); + assert_false(IsCntrl('a')); + assert_false(IsCntrl('~')); + /* DEL (0x7F) is intentionally NOT classified as control in ircu/nefarious */ + assert_false(IsCntrl(0x7F)); +} + +/* Test end-of-line characters + * Note: ircu defines NTL_EOL as only \r and \n. NUL is not included because + * it's used as string terminator, not a line terminator in IRC protocol. */ +static void test_IsEol(void **state) +{ + (void)state; + + /* EOL chars per ircu: only \n and \r */ + assert_true(IsEol('\n')); + assert_true(IsEol('\r')); + + /* NUL is intentionally not EOL in ircu - it's the string terminator */ + assert_false(IsEol('\0')); + assert_false(IsEol(' ')); + assert_false(IsEol('a')); + assert_false(IsEol('\t')); +} + +/* Test IP address characters - IsIPChar is for IPv4 only (digits and .) */ +static void test_IsIPChar(void **state) +{ + (void)state; + + /* Digits for IPv4 */ + assert_true(IsIPChar('0')); + assert_true(IsIPChar('9')); + + /* Dot for IPv4 */ + assert_true(IsIPChar('.')); + + /* Invalid IPv4 chars - hex letters and colon are for IPv6 (IsIP6Char) */ + assert_false(IsIPChar('A')); + assert_false(IsIPChar('F')); + assert_false(IsIPChar(':')); + assert_false(IsIPChar('a')); + assert_false(IsIPChar('f')); + assert_false(IsIPChar('g')); + assert_false(IsIPChar('z')); + assert_false(IsIPChar(' ')); + assert_false(IsIPChar('#')); +} + +/* Test IPv6 address characters - IsIP6Char includes hex and colon */ +static void test_IsIP6Char(void **state) +{ + (void)state; + + /* Digits */ + assert_true(IsIP6Char('0')); + assert_true(IsIP6Char('9')); + + /* Dot for IPv4-mapped addresses */ + assert_true(IsIP6Char('.')); + + /* Hex digits (upper and lower) */ + assert_true(IsIP6Char('A')); + assert_true(IsIP6Char('F')); + assert_true(IsIP6Char('a')); + assert_true(IsIP6Char('f')); + + /* Colon separator */ + assert_true(IsIP6Char(':')); + + /* Invalid IPv6 chars */ + assert_false(IsIP6Char('G')); + assert_false(IsIP6Char('g')); + assert_false(IsIP6Char('z')); + assert_false(IsIP6Char(' ')); + assert_false(IsIP6Char('#')); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_IsDigit), + cmocka_unit_test(test_IsAlpha), + cmocka_unit_test(test_IsAlnum), + cmocka_unit_test(test_IsChannelPrefix), + cmocka_unit_test(test_IsNickChar), + cmocka_unit_test(test_IsChannelChar), + cmocka_unit_test(test_IsCntrl), + cmocka_unit_test(test_IsEol), + cmocka_unit_test(test_IsIPChar), + cmocka_unit_test(test_IsIP6Char), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/ircd/test/ircd_cloaking_cmocka.c b/ircd/test/ircd_cloaking_cmocka.c new file mode 100644 index 00000000..8d54e592 --- /dev/null +++ b/ircd/test/ircd_cloaking_cmocka.c @@ -0,0 +1,648 @@ +/* + * ircd_cloaking_cmocka.c - CMocka unit tests for IP/host cloaking + * + * Tests the cloaking functions used to hide user IP addresses and hostnames. + * Uses deterministic keys for reproducible test results. + * + * Copyright (C) 2024 AfterNET Development Team + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include + +/* Include headers needed by ircd_cloaking.c first */ +#include "ircd_chattr.h" +#include "ircd_defs.h" +#include "ircd_md5.h" +#include "res.h" + +/* Mock the feature macros used by ircd_cloaking.c + * We override KEY1/KEY2/KEY3/PREFIX with test values */ +#define KEY1 "TestKey1ForCloaking" +#define KEY2 "TestKey2ForCloaking" +#define KEY3 "TestKey3ForCloaking" +#define PREFIX "hidden" + +/* Stub ircd_snprintf - use regular snprintf */ +#define ircd_snprintf(client, buf, size, fmt, ...) snprintf(buf, size, fmt, ##__VA_ARGS__) + +/* Inline the static functions from ircd_cloaking.c directly + * This avoids header conflicts while allowing us to test the functions */ + +/** Downsamples a 128bit result to 32bits (md5 -> unsigned int) */ +static inline unsigned int downsample(unsigned char *i) +{ + unsigned char r[4]; + + r[0] = i[0] ^ i[1] ^ i[2] ^ i[3]; + r[1] = i[4] ^ i[5] ^ i[6] ^ i[7]; + r[2] = i[8] ^ i[9] ^ i[10] ^ i[11]; + r[3] = i[12] ^ i[13] ^ i[14] ^ i[15]; + + return ( ((unsigned int)r[0] << 24) + + ((unsigned int)r[1] << 16) + + ((unsigned int)r[2] << 8) + + (unsigned int)r[3]); +} + +/** Downsamples a 128bit result to 24bits (md5 -> unsigned int) */ +static inline unsigned int downsample24(unsigned char *i) +{ + unsigned char r[4]; + + r[0] = i[0] ^ i[1] ^ i[2] ^ i[3] ^ i[4]; + r[1] = i[5] ^ i[6] ^ i[7] ^ i[8] ^ i[9] ^ i[10]; + r[2] = i[11] ^ i[12] ^ i[13] ^ i[14] ^ i[15]; + + return ( ((unsigned int)r[0] << 16) + + ((unsigned int)r[1] << 8) + + (unsigned int)r[2]); +} + +/* Forward declaration for mutual recursion */ +static char *hidehost_ipv6(struct irc_in_addr *ip); + +static char *hidehost_ipv4(struct irc_in_addr *ip) +{ + unsigned int a, b, c, d; + static char buf[512], res[512], res2[512], result[128]; + unsigned long n; + unsigned int alpha, beta, gamma, delta; + unsigned char *pch; + + if (!irc_in_addr_is_ipv4(ip)) + return hidehost_ipv6(ip); + + pch = (unsigned char*)&ip->in6_16[6]; + a = *pch++; + b = *pch; + pch = (unsigned char*)&ip->in6_16[7]; + c = *pch++; + d = *pch; + + /* ALPHA... */ + snprintf(buf, 512, "%s:%d.%d.%d.%d:%s", KEY2, a, b, c, d, KEY3); + DoMD5((unsigned char *)&res, (unsigned char *)&buf, strlen(buf)); + strcpy(res+16, KEY1); + n = strlen(res+16) + 16; + DoMD5((unsigned char *)&res2, (unsigned char *)&res, n); + alpha = downsample24((unsigned char *)&res2); + + /* BETA... */ + snprintf(buf, 512, "%s:%d.%d.%d:%s", KEY3, a, b, c, KEY1); + DoMD5((unsigned char *)&res, (unsigned char *)&buf, strlen(buf)); + strcpy(res+16, KEY2); + n = strlen(res+16) + 16; + DoMD5((unsigned char *)&res2, (unsigned char *)&res, n); + beta = downsample24((unsigned char *)&res2); + + /* GAMMA... */ + snprintf(buf, 512, "%s:%d.%d:%s", KEY1, a, b, KEY2); + DoMD5((unsigned char *)&res, (unsigned char *)&buf, strlen(buf)); + strcpy(res+16, KEY3); + n = strlen(res+16) + 16; + DoMD5((unsigned char *)&res2, (unsigned char *)&res, n); + gamma = downsample24((unsigned char *)&res2); + + /* DELTA... */ + snprintf(buf, 512, "%s:%d:%s:%s", KEY2, a, KEY1, KEY3); + DoMD5((unsigned char *)&res, (unsigned char *)&buf, strlen(buf)); + strcpy(res+16, KEY1); + n = strlen(res+16) + 16; + DoMD5((unsigned char *)&res2, (unsigned char *)&res, n); + delta = downsample24((unsigned char *)&res2); + + snprintf(result, HOSTLEN, "%X.%X.%X.%X.IP", alpha, beta, gamma, delta); + return result; +} + +static char *hidehost_ipv6(struct irc_in_addr *ip) +{ + unsigned int a, b, c, d, e, f, g, h; + static char buf[512], res[512], res2[512], result[128]; + unsigned long n; + unsigned int alpha, beta, gamma, delta; + + if (irc_in_addr_is_ipv4(ip)) + return hidehost_ipv4(ip); + + a = ntohs(ip->in6_16[0]); + b = ntohs(ip->in6_16[1]); + c = ntohs(ip->in6_16[2]); + d = ntohs(ip->in6_16[3]); + e = ntohs(ip->in6_16[4]); + f = ntohs(ip->in6_16[5]); + g = ntohs(ip->in6_16[6]); + h = ntohs(ip->in6_16[7]); + + /* ALPHA... */ + snprintf(buf, 512, "%s:%x:%x:%x:%x:%x:%x:%x:%x:%s", KEY2, a, b, c, d, e, f, g, h, KEY3); + DoMD5((unsigned char *)&res, (unsigned char *)&buf, strlen(buf)); + strcpy(res+16, KEY1); + n = strlen(res+16) + 16; + DoMD5((unsigned char *)&res2, (unsigned char *)&res, n); + alpha = downsample24((unsigned char *)&res2); + + /* BETA... */ + snprintf(buf, 512, "%s:%x:%x:%x:%x:%x:%x:%x:%s", KEY3, a, b, c, d, e, f, g, KEY1); + DoMD5((unsigned char *)&res, (unsigned char *)&buf, strlen(buf)); + strcpy(res+16, KEY2); + n = strlen(res+16) + 16; + DoMD5((unsigned char *)&res2, (unsigned char *)&res, n); + beta = downsample24((unsigned char *)&res2); + + /* GAMMA... */ + snprintf(buf, 512, "%s:%x:%x:%x:%x:%s", KEY1, a, b, c, d, KEY2); + DoMD5((unsigned char *)&res, (unsigned char *)&buf, strlen(buf)); + strcpy(res+16, KEY3); + n = strlen(res+16) + 16; + DoMD5((unsigned char *)&res2, (unsigned char *)&res, n); + gamma = downsample24((unsigned char *)&res2); + + /* DELTA... */ + snprintf(buf, 512, "%s:%x:%x:%s:%s", KEY2, a, b, KEY1, KEY3); + DoMD5((unsigned char *)&res, (unsigned char *)&buf, strlen(buf)); + strcpy(res+16, KEY1); + n = strlen(res+16) + 16; + DoMD5((unsigned char *)&res2, (unsigned char *)&res, n); + delta = downsample24((unsigned char *)&res2); + + snprintf(result, HOSTLEN, "%X:%X:%X:%X:IP", alpha, beta, gamma, delta); + return result; +} + +static char *hidehost_normalhost(char *host, int components) +{ + char *p; + static char buf[512], res[512], res2[512], result[HOSTLEN+1]; + unsigned int alpha, n; + int comps = 0; + + snprintf(buf, 512, "%s:%s:%s", KEY1, host, KEY2); + DoMD5((unsigned char *)&res, (unsigned char *)&buf, strlen(buf)); + strcpy(res+16, KEY3); + n = strlen(res+16) + 16; + DoMD5((unsigned char *)&res2, (unsigned char *)&res, n); + alpha = downsample((unsigned char *)&res2); + + for (p = host; *p; p++) { + if (*p == '.') { + comps++; + if ((comps >= components) && IsHostChar(*(p + 1))) + break; + } + } + + if (*p) + { + unsigned int len; + char *c; + p++; + + snprintf(result, HOSTLEN, "%s-%X.", PREFIX, alpha); + len = strlen(result) + strlen(p); + if (len <= HOSTLEN) + strcat(result, p); + else + { + c = p + (len - HOSTLEN); + if ((*c == '.') && *(c+1)) + c++; + strcat(result, c); + } + } else + snprintf(result, HOSTLEN, "%s-%X", PREFIX, alpha); + + return result; +} + + +/* ========== Helper functions ========== */ + +static void make_ipv4_addr(struct irc_in_addr *addr, unsigned char a, unsigned char b, + unsigned char c, unsigned char d) +{ + memset(addr, 0, sizeof(*addr)); + /* IPv4-mapped IPv6 format: ::ffff:a.b.c.d */ + addr->in6_16[5] = htons(0xffff); + addr->in6_16[6] = htons((a << 8) | b); + addr->in6_16[7] = htons((c << 8) | d); +} + +static void make_ipv6_addr(struct irc_in_addr *addr, + uint16_t a, uint16_t b, uint16_t c, uint16_t d, + uint16_t e, uint16_t f, uint16_t g, uint16_t h) +{ + addr->in6_16[0] = htons(a); + addr->in6_16[1] = htons(b); + addr->in6_16[2] = htons(c); + addr->in6_16[3] = htons(d); + addr->in6_16[4] = htons(e); + addr->in6_16[5] = htons(f); + addr->in6_16[6] = htons(g); + addr->in6_16[7] = htons(h); +} + + +/* ========== downsample() tests ========== */ + +static void test_downsample_zeros(void **state) +{ + (void)state; + unsigned char input[16] = {0}; + + /* All zeros should produce zero */ + assert_int_equal(0, downsample(input)); +} + +static void test_downsample_ones(void **state) +{ + (void)state; + unsigned char input[16]; + memset(input, 0xFF, sizeof(input)); + + /* All 0xFF bytes: + * r[0] = 0xFF ^ 0xFF ^ 0xFF ^ 0xFF = 0 + * r[1] = 0xFF ^ 0xFF ^ 0xFF ^ 0xFF = 0 + * r[2] = 0xFF ^ 0xFF ^ 0xFF ^ 0xFF = 0 + * r[3] = 0xFF ^ 0xFF ^ 0xFF ^ 0xFF = 0 + */ + assert_int_equal(0, downsample(input)); +} + +static void test_downsample_sequential(void **state) +{ + (void)state; + unsigned char input[16]; + + /* Fill with 0-15 */ + for (int i = 0; i < 16; i++) { + input[i] = (unsigned char)i; + } + + /* r[0] = 0 ^ 1 ^ 2 ^ 3 = 0 + * r[1] = 4 ^ 5 ^ 6 ^ 7 = 0 + * r[2] = 8 ^ 9 ^ 10 ^ 11 = 0 + * r[3] = 12 ^ 13 ^ 14 ^ 15 = 0 + */ + assert_int_equal(0, downsample(input)); +} + +static void test_downsample_known_value(void **state) +{ + (void)state; + unsigned char input[16] = { + 0x01, 0x00, 0x00, 0x00, /* r[0] = 0x01 */ + 0x02, 0x00, 0x00, 0x00, /* r[1] = 0x02 */ + 0x03, 0x00, 0x00, 0x00, /* r[2] = 0x03 */ + 0x04, 0x00, 0x00, 0x00 /* r[3] = 0x04 */ + }; + + /* Expected: (0x01 << 24) + (0x02 << 16) + (0x03 << 8) + 0x04 */ + assert_int_equal(0x01020304, downsample(input)); +} + + +/* ========== downsample24() tests ========== */ + +static void test_downsample24_zeros(void **state) +{ + (void)state; + unsigned char input[16] = {0}; + + assert_int_equal(0, downsample24(input)); +} + +static void test_downsample24_known_value(void **state) +{ + (void)state; + unsigned char input[16] = { + 0x01, 0x00, 0x00, 0x00, 0x00, /* r[0] = 0x01 */ + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, /* r[1] = 0x02 */ + 0x03, 0x00, 0x00, 0x00, 0x00 /* r[2] = 0x03 */ + }; + + /* Expected: (0x01 << 16) + (0x02 << 8) + 0x03 = 0x010203 */ + assert_int_equal(0x010203, downsample24(input)); +} + +static void test_downsample24_max(void **state) +{ + (void)state; + /* downsample24 produces a 24-bit value, max is 0xFFFFFF */ + unsigned char input[16] = { + 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0x00, 0x00, 0x00 + }; + + unsigned int result = downsample24(input); + assert_true(result <= 0xFFFFFF); +} + + +/* ========== hidehost_ipv4() tests ========== */ + +static void test_hidehost_ipv4_format(void **state) +{ + (void)state; + struct irc_in_addr addr; + char *result; + + make_ipv4_addr(&addr, 127, 0, 0, 1); + result = hidehost_ipv4(&addr); + + /* Result should be in format: ALPHA.BETA.GAMMA.DELTA.IP */ + assert_non_null(result); + assert_true(strlen(result) > 0); + + /* Should end with .IP */ + assert_non_null(strstr(result, ".IP")); + + /* Should contain 4 dots (X.X.X.X.IP) */ + int dots = 0; + for (char *p = result; *p; p++) { + if (*p == '.') dots++; + } + assert_int_equal(4, dots); +} + +static void test_hidehost_ipv4_deterministic(void **state) +{ + (void)state; + struct irc_in_addr addr; + char result1[128], result2[128]; + + /* Same IP should produce same cloak */ + make_ipv4_addr(&addr, 192, 168, 1, 100); + + strncpy(result1, hidehost_ipv4(&addr), sizeof(result1) - 1); + result1[sizeof(result1) - 1] = '\0'; + + strncpy(result2, hidehost_ipv4(&addr), sizeof(result2) - 1); + result2[sizeof(result2) - 1] = '\0'; + + assert_string_equal(result1, result2); +} + +static void test_hidehost_ipv4_different_ips(void **state) +{ + (void)state; + struct irc_in_addr addr1, addr2; + char result1[128], result2[128]; + + /* Different IPs should produce different cloaks */ + make_ipv4_addr(&addr1, 10, 0, 0, 1); + make_ipv4_addr(&addr2, 10, 0, 0, 2); + + strncpy(result1, hidehost_ipv4(&addr1), sizeof(result1) - 1); + result1[sizeof(result1) - 1] = '\0'; + + strncpy(result2, hidehost_ipv4(&addr2), sizeof(result2) - 1); + result2[sizeof(result2) - 1] = '\0'; + + assert_string_not_equal(result1, result2); +} + +static void test_hidehost_ipv4_same_class_c(void **state) +{ + (void)state; + struct irc_in_addr addr1, addr2; + char *result1, *result2; + char *dot1, *dot2; + + /* IPs in same /24 should share some cloak components (BETA, GAMMA, DELTA) */ + make_ipv4_addr(&addr1, 192, 168, 1, 10); + make_ipv4_addr(&addr2, 192, 168, 1, 20); + + result1 = hidehost_ipv4(&addr1); + result2 = hidehost_ipv4(&addr2); + + /* Find first dot (after ALPHA) */ + dot1 = strchr(result1, '.'); + dot2 = strchr(result2, '.'); + + assert_non_null(dot1); + assert_non_null(dot2); + + /* Everything after first dot should be same (BETA.GAMMA.DELTA.IP) */ + assert_string_equal(dot1, dot2); +} + + +/* ========== hidehost_ipv6() tests ========== */ + +static void test_hidehost_ipv6_format(void **state) +{ + (void)state; + struct irc_in_addr addr; + char *result; + + /* ::1 */ + make_ipv6_addr(&addr, 0, 0, 0, 0, 0, 0, 0, 1); + result = hidehost_ipv6(&addr); + + /* Result should be in format: ALPHA:BETA:GAMMA:DELTA:IP */ + assert_non_null(result); + assert_true(strlen(result) > 0); + + /* Should end with :IP */ + assert_non_null(strstr(result, ":IP")); +} + +static void test_hidehost_ipv6_deterministic(void **state) +{ + (void)state; + struct irc_in_addr addr; + char result1[128], result2[128]; + + make_ipv6_addr(&addr, 0x2001, 0xdb8, 0, 0, 0, 0, 0, 1); + + strncpy(result1, hidehost_ipv6(&addr), sizeof(result1) - 1); + result1[sizeof(result1) - 1] = '\0'; + + strncpy(result2, hidehost_ipv6(&addr), sizeof(result2) - 1); + result2[sizeof(result2) - 1] = '\0'; + + assert_string_equal(result1, result2); +} + +static void test_hidehost_ipv6_different_ips(void **state) +{ + (void)state; + struct irc_in_addr addr1, addr2; + char result1[128], result2[128]; + + make_ipv6_addr(&addr1, 0x2001, 0xdb8, 0, 0, 0, 0, 0, 1); + make_ipv6_addr(&addr2, 0x2001, 0xdb8, 0, 0, 0, 0, 0, 2); + + strncpy(result1, hidehost_ipv6(&addr1), sizeof(result1) - 1); + result1[sizeof(result1) - 1] = '\0'; + + strncpy(result2, hidehost_ipv6(&addr2), sizeof(result2) - 1); + result2[sizeof(result2) - 1] = '\0'; + + assert_string_not_equal(result1, result2); +} + + +/* ========== hidehost_normalhost() tests ========== */ + +static void test_hidehost_normalhost_format(void **state) +{ + (void)state; + char host[] = "user.example.com"; + char *result; + + result = hidehost_normalhost(host, 1); + + assert_non_null(result); + assert_true(strlen(result) > 0); + + /* Should start with PREFIX- */ + assert_int_equal(0, strncmp(result, PREFIX "-", strlen(PREFIX) + 1)); + + /* Should contain the domain suffix */ + assert_non_null(strstr(result, "example.com")); +} + +static void test_hidehost_normalhost_deterministic(void **state) +{ + (void)state; + char host[] = "test.host.example.org"; + char result1[128], result2[128]; + + strncpy(result1, hidehost_normalhost(host, 2), sizeof(result1) - 1); + result1[sizeof(result1) - 1] = '\0'; + + strncpy(result2, hidehost_normalhost(host, 2), sizeof(result2) - 1); + result2[sizeof(result2) - 1] = '\0'; + + assert_string_equal(result1, result2); +} + +static void test_hidehost_normalhost_different_hosts(void **state) +{ + (void)state; + char host1[] = "user1.example.com"; + char host2[] = "user2.example.com"; + char result1[128], result2[128]; + + strncpy(result1, hidehost_normalhost(host1, 1), sizeof(result1) - 1); + result1[sizeof(result1) - 1] = '\0'; + + strncpy(result2, hidehost_normalhost(host2, 1), sizeof(result2) - 1); + result2[sizeof(result2) - 1] = '\0'; + + /* Different hosts should produce different cloaks */ + assert_string_not_equal(result1, result2); +} + +static void test_hidehost_normalhost_components(void **state) +{ + (void)state; + char host[] = "a.b.c.example.com"; + char *result; + + /* With components=2, should preserve example.com */ + result = hidehost_normalhost(host, 2); + assert_non_null(strstr(result, "example.com")); + + /* With components=1, should preserve c.example.com */ + result = hidehost_normalhost(host, 1); + assert_non_null(strstr(result, "c.example.com")); +} + +static void test_hidehost_normalhost_single_label(void **state) +{ + (void)state; + char host[] = "localhost"; + char *result; + + /* Single-label hostname should still produce valid output */ + result = hidehost_normalhost(host, 1); + + assert_non_null(result); + assert_true(strlen(result) > 0); + /* Should start with PREFIX- */ + assert_int_equal(0, strncmp(result, PREFIX "-", strlen(PREFIX) + 1)); +} + + +/* ========== IPv4/IPv6 detection ========== */ + +static void test_hidehost_ipv4_called_for_ipv4(void **state) +{ + (void)state; + struct irc_in_addr addr; + char *result; + + /* IPv4-mapped address */ + make_ipv4_addr(&addr, 8, 8, 8, 8); + result = hidehost_ipv4(&addr); + + /* Should end with .IP (IPv4 format) */ + assert_non_null(strstr(result, ".IP")); +} + +static void test_hidehost_ipv6_redirects_ipv4(void **state) +{ + (void)state; + struct irc_in_addr addr; + char *result; + + /* IPv4-mapped address passed to ipv6 function */ + make_ipv4_addr(&addr, 1, 2, 3, 4); + result = hidehost_ipv6(&addr); + + /* Should be redirected to IPv4 cloaking, ending with .IP */ + assert_non_null(strstr(result, ".IP")); +} + + +int main(void) +{ + const struct CMUnitTest tests[] = { + /* downsample */ + cmocka_unit_test(test_downsample_zeros), + cmocka_unit_test(test_downsample_ones), + cmocka_unit_test(test_downsample_sequential), + cmocka_unit_test(test_downsample_known_value), + + /* downsample24 */ + cmocka_unit_test(test_downsample24_zeros), + cmocka_unit_test(test_downsample24_known_value), + cmocka_unit_test(test_downsample24_max), + + /* hidehost_ipv4 */ + cmocka_unit_test(test_hidehost_ipv4_format), + cmocka_unit_test(test_hidehost_ipv4_deterministic), + cmocka_unit_test(test_hidehost_ipv4_different_ips), + cmocka_unit_test(test_hidehost_ipv4_same_class_c), + + /* hidehost_ipv6 */ + cmocka_unit_test(test_hidehost_ipv6_format), + cmocka_unit_test(test_hidehost_ipv6_deterministic), + cmocka_unit_test(test_hidehost_ipv6_different_ips), + + /* hidehost_normalhost */ + cmocka_unit_test(test_hidehost_normalhost_format), + cmocka_unit_test(test_hidehost_normalhost_deterministic), + cmocka_unit_test(test_hidehost_normalhost_different_hosts), + cmocka_unit_test(test_hidehost_normalhost_components), + cmocka_unit_test(test_hidehost_normalhost_single_label), + + /* IPv4/IPv6 detection */ + cmocka_unit_test(test_hidehost_ipv4_called_for_ipv4), + cmocka_unit_test(test_hidehost_ipv6_redirects_ipv4), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/ircd/test/ircd_compress_cmocka.c b/ircd/test/ircd_compress_cmocka.c new file mode 100644 index 00000000..5bea2f45 --- /dev/null +++ b/ircd/test/ircd_compress_cmocka.c @@ -0,0 +1,357 @@ +/* + * ircd_compress_cmocka.c - CMocka unit tests for zstd compression utilities + * + * Tests the compression/decompression functions used for LMDB-backed storage + * (chathistory, metadata). These functions provide transparent compression + * with a configurable threshold. + * + * Copyright (C) 2024 AfterNET Development Team + */ + +#include "config.h" + +#ifdef USE_ZSTD + +#include +#include +#include +#include +#include + +#include "ircd_compress.h" + +/* Test buffer sizes */ +#define TEST_BUF_SIZE 4096 + + +/* ========== is_compressed() tests ========== */ + +static void test_is_compressed_with_magic(void **state) +{ + (void)state; + unsigned char data[] = { COMPRESS_MAGIC, 0x28, 0xB5, 0x2F, 0xFD }; + + /* Data starting with magic byte should be detected as compressed */ + assert_true(is_compressed(data, sizeof(data))); +} + +static void test_is_compressed_without_magic(void **state) +{ + (void)state; + unsigned char data[] = { 'H', 'e', 'l', 'l', 'o' }; + + /* Normal data without magic byte should not be detected as compressed */ + assert_false(is_compressed(data, sizeof(data))); +} + +static void test_is_compressed_empty(void **state) +{ + (void)state; + unsigned char data[] = { 0 }; + + /* Empty or single-byte data should not be detected as compressed */ + assert_false(is_compressed(data, 0)); + assert_false(is_compressed(data, 1)); +} + +static void test_is_compressed_magic_only(void **state) +{ + (void)state; + unsigned char data[] = { COMPRESS_MAGIC }; + + /* Single magic byte alone - len must be > 1 */ + assert_false(is_compressed(data, 1)); +} + + +/* ========== compress_data() tests ========== */ + +static void test_compress_below_threshold(void **state) +{ + (void)state; + unsigned char input[] = "Short string"; + unsigned char output[TEST_BUF_SIZE]; + size_t output_len; + int result; + + /* Ensure threshold is at default (256 bytes) */ + compress_set_threshold(COMPRESS_THRESHOLD_DEFAULT); + + /* Data below threshold should pass through unchanged */ + result = compress_data(input, sizeof(input) - 1, output, TEST_BUF_SIZE, &output_len); + + assert_int_equal(result, 0); /* 0 = passed through unchanged */ + assert_int_equal(output_len, sizeof(input) - 1); + assert_memory_equal(output, input, output_len); +} + +static void test_compress_above_threshold(void **state) +{ + (void)state; + unsigned char input[512]; + unsigned char output[TEST_BUF_SIZE]; + size_t output_len; + int result; + + /* Create compressible data (repeated pattern compresses well) */ + memset(input, 'A', sizeof(input)); + + compress_set_threshold(COMPRESS_THRESHOLD_DEFAULT); + + result = compress_data(input, sizeof(input), output, TEST_BUF_SIZE, &output_len); + + /* Should be compressed (repeated data compresses very well) */ + assert_int_equal(result, 1); /* 1 = compressed */ + assert_true(output_len < sizeof(input)); /* Should be smaller */ + assert_int_equal(output[0], COMPRESS_MAGIC); /* Magic byte present */ +} + +static void test_compress_at_threshold(void **state) +{ + (void)state; + unsigned char input[COMPRESS_THRESHOLD_DEFAULT]; + unsigned char output[TEST_BUF_SIZE]; + size_t output_len; + int result; + + /* Exactly at threshold with compressible data */ + memset(input, 'B', sizeof(input)); + + compress_set_threshold(COMPRESS_THRESHOLD_DEFAULT); + + result = compress_data(input, sizeof(input), output, TEST_BUF_SIZE, &output_len); + + /* At threshold, should attempt compression */ + /* Result depends on whether compression saves space */ + assert_true(result == 0 || result == 1); +} + +static void test_compress_incompressible(void **state) +{ + (void)state; + unsigned char input[512]; + unsigned char output[TEST_BUF_SIZE]; + size_t output_len; + int result; + + /* Create random-ish data that doesn't compress well */ + for (size_t i = 0; i < sizeof(input); i++) { + input[i] = (unsigned char)(i * 17 + 31); /* Pseudo-random pattern */ + } + + compress_set_threshold(COMPRESS_THRESHOLD_DEFAULT); + + result = compress_data(input, sizeof(input), output, TEST_BUF_SIZE, &output_len); + + /* If compression doesn't save space, should pass through unchanged */ + /* Either compressed (1) or passed through (0), never error (-1) for valid input */ + assert_true(result >= 0); +} + +static void test_compress_output_too_small(void **state) +{ + (void)state; + unsigned char input[] = "Test data"; + unsigned char output[2]; /* Too small */ + size_t output_len; + int result; + + compress_set_threshold(1); /* Force compression attempt */ + + result = compress_data(input, sizeof(input), output, sizeof(output), &output_len); + + /* Should fail with -1 when output buffer too small */ + assert_int_equal(result, -1); +} + + +/* ========== decompress_data() tests ========== */ + +static void test_decompress_uncompressed(void **state) +{ + (void)state; + unsigned char input[] = "Not compressed data"; + unsigned char output[TEST_BUF_SIZE]; + size_t output_len; + int result; + + /* Data without magic byte should pass through unchanged */ + result = decompress_data(input, sizeof(input) - 1, output, TEST_BUF_SIZE, &output_len); + + assert_int_equal(result, 0); /* 0 = passed through unchanged */ + assert_int_equal(output_len, sizeof(input) - 1); + assert_memory_equal(output, input, output_len); +} + +static void test_decompress_compressed(void **state) +{ + (void)state; + unsigned char original[512]; + unsigned char compressed[TEST_BUF_SIZE]; + unsigned char decompressed[TEST_BUF_SIZE]; + size_t compressed_len, decompressed_len; + int result; + + /* Create and compress data */ + memset(original, 'C', sizeof(original)); + + compress_set_threshold(COMPRESS_THRESHOLD_DEFAULT); + result = compress_data(original, sizeof(original), compressed, TEST_BUF_SIZE, &compressed_len); + assert_int_equal(result, 1); /* Should compress */ + + /* Now decompress */ + result = decompress_data(compressed, compressed_len, decompressed, TEST_BUF_SIZE, &decompressed_len); + + assert_int_equal(result, 1); /* 1 = decompressed */ + assert_int_equal(decompressed_len, sizeof(original)); + assert_memory_equal(decompressed, original, sizeof(original)); +} + + +/* ========== Round-trip tests ========== */ + +static void test_roundtrip_compressible(void **state) +{ + (void)state; + unsigned char original[1024]; + unsigned char compressed[TEST_BUF_SIZE]; + unsigned char decompressed[TEST_BUF_SIZE]; + size_t compressed_len, decompressed_len; + int comp_result, decomp_result; + + /* Create compressible data */ + for (size_t i = 0; i < sizeof(original); i++) { + original[i] = (unsigned char)(i % 10 + 'A'); + } + + compress_set_threshold(COMPRESS_THRESHOLD_DEFAULT); + + /* Compress */ + comp_result = compress_data(original, sizeof(original), compressed, TEST_BUF_SIZE, &compressed_len); + assert_true(comp_result >= 0); + + /* Decompress */ + decomp_result = decompress_data(compressed, compressed_len, decompressed, TEST_BUF_SIZE, &decompressed_len); + assert_true(decomp_result >= 0); + + /* Verify round-trip */ + assert_int_equal(decompressed_len, sizeof(original)); + assert_memory_equal(decompressed, original, sizeof(original)); +} + +static void test_roundtrip_small_data(void **state) +{ + (void)state; + unsigned char original[] = "Small data that won't be compressed"; + unsigned char intermediate[TEST_BUF_SIZE]; + unsigned char final[TEST_BUF_SIZE]; + size_t intermediate_len, final_len; + + compress_set_threshold(COMPRESS_THRESHOLD_DEFAULT); + + /* Compress (should pass through) */ + compress_data(original, sizeof(original) - 1, intermediate, TEST_BUF_SIZE, &intermediate_len); + + /* Decompress (should also pass through) */ + decompress_data(intermediate, intermediate_len, final, TEST_BUF_SIZE, &final_len); + + /* Verify round-trip */ + assert_int_equal(final_len, sizeof(original) - 1); + assert_memory_equal(final, original, final_len); +} + + +/* ========== Accessor tests ========== */ + +static void test_threshold_accessors(void **state) +{ + (void)state; + size_t original_threshold; + + /* Save original */ + original_threshold = compress_get_threshold(); + + /* Set new value */ + compress_set_threshold(512); + assert_int_equal(compress_get_threshold(), 512); + + /* Set another value */ + compress_set_threshold(1024); + assert_int_equal(compress_get_threshold(), 1024); + + /* Restore original */ + compress_set_threshold(original_threshold); + assert_int_equal(compress_get_threshold(), original_threshold); +} + +static void test_level_accessors(void **state) +{ + (void)state; + int original_level; + + /* Save original */ + original_level = compress_get_level(); + + /* Set valid levels */ + compress_set_level(1); + assert_int_equal(compress_get_level(), 1); + + compress_set_level(10); + assert_int_equal(compress_get_level(), 10); + + /* Invalid levels should be ignored */ + compress_set_level(0); /* Too low */ + assert_int_equal(compress_get_level(), 10); /* Should remain 10 */ + + compress_set_level(-1); /* Negative */ + assert_int_equal(compress_get_level(), 10); /* Should remain 10 */ + + /* Restore original */ + compress_set_level(original_level); +} + + +int main(void) +{ + const struct CMUnitTest tests[] = { + /* is_compressed */ + cmocka_unit_test(test_is_compressed_with_magic), + cmocka_unit_test(test_is_compressed_without_magic), + cmocka_unit_test(test_is_compressed_empty), + cmocka_unit_test(test_is_compressed_magic_only), + + /* compress_data */ + cmocka_unit_test(test_compress_below_threshold), + cmocka_unit_test(test_compress_above_threshold), + cmocka_unit_test(test_compress_at_threshold), + cmocka_unit_test(test_compress_incompressible), + cmocka_unit_test(test_compress_output_too_small), + + /* decompress_data */ + cmocka_unit_test(test_decompress_uncompressed), + cmocka_unit_test(test_decompress_compressed), + + /* Round-trip */ + cmocka_unit_test(test_roundtrip_compressible), + cmocka_unit_test(test_roundtrip_small_data), + + /* Accessors */ + cmocka_unit_test(test_threshold_accessors), + cmocka_unit_test(test_level_accessors), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} + +#else /* !USE_ZSTD */ + +/* Stub main when zstd is not available */ +#include + +int main(void) +{ + printf("ircd_compress tests skipped: USE_ZSTD not defined\n"); + return 0; +} + +#endif /* USE_ZSTD */ diff --git a/ircd/test/ircd_crypt_cmocka.c b/ircd/test/ircd_crypt_cmocka.c new file mode 100644 index 00000000..d340e426 --- /dev/null +++ b/ircd/test/ircd_crypt_cmocka.c @@ -0,0 +1,638 @@ +/* + * ircd_crypt_cmocka.c - CMocka unit tests for password hashing + * + * Tests the password encryption and verification system used for oper + * authentication. Supports multiple mechanisms: PLAIN (testing only), + * SMD5 (Salted MD5), and native crypt(). + * + * Copyright (C) 2024 AfterNET Development Team + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include + +/* Stubs for dependencies */ +void *MyMalloc(size_t size) { + return malloc(size); +} + +void MyFree(void *ptr) { + free(ptr); +} + +/* Stub for DupString macro */ +#define DupString(x, y) do { x = malloc(strlen(y) + 1); strcpy(x, y); } while(0) + +/* Stub for Debug macro */ +#define Debug(x) + +/* Stub for assert */ +#undef assert +#define assert(x) do { if (!(x)) { fprintf(stderr, "Assertion failed: %s\n", #x); abort(); } } while(0) + +/* Include the headers */ +#include "ircd_crypt.h" +#include "ircd_md5.h" +#include "ircd_string.h" + +/* We need to provide ircd_crypt_native stub */ +const char* ircd_crypt_native(const char* key, const char* salt) +{ + /* Simple stub - in real code this calls system crypt() */ + /* For testing, we just return NULL to indicate no match */ + (void)key; + (void)salt; + return NULL; +} + +/* Forward declarations for mechanism functions */ +const char* ircd_crypt_plain(const char* key, const char* salt); +const char* ircd_crypt_smd5(const char* key, const char* salt); +void ircd_register_crypt_plain(void); +void ircd_register_crypt_smd5(void); + +/* Global from ircd_crypt.c */ +crypt_mechs_t* crypt_mechs_root = NULL; + +/* Inline ircd_crypt_register_mech */ +int ircd_crypt_register_mech(crypt_mech_t* mechanism) +{ + crypt_mechs_t* crypt_mech; + + if ((crypt_mech = (crypt_mechs_t*)MyMalloc(sizeof(crypt_mechs_t))) == NULL) + return -1; + + memset(crypt_mech, 0, sizeof(crypt_mechs_t)); + crypt_mech->mech = mechanism; + crypt_mech->next = crypt_mech->prev = NULL; + + if(crypt_mechs_root->next == NULL) + { + crypt_mechs_root->next = crypt_mechs_root->prev = crypt_mech; + } else { + crypt_mech->prev = crypt_mechs_root->prev; + crypt_mech->next = NULL; + crypt_mechs_root->prev = crypt_mech->prev->next = crypt_mech; + } + + return 0; +} + +/* Inline ircd_crypt_plain */ +const char* ircd_crypt_plain(const char* key, const char* salt) +{ + assert(NULL != salt); + assert(NULL != key); + return key; +} + +/* Register plain mechanism */ +void ircd_register_crypt_plain(void) +{ + crypt_mech_t* crypt_mech; + + if ((crypt_mech = (crypt_mech_t*)MyMalloc(sizeof(crypt_mech_t))) == NULL) + return; + + crypt_mech->mechname = "plain"; + crypt_mech->shortname = "crypt_plain"; + crypt_mech->description = "Plain text crypt mechanism."; + crypt_mech->crypt_function = &ircd_crypt_plain; + crypt_mech->crypt_token = "$PLAIN$"; + crypt_mech->crypt_token_size = 7; + + ircd_crypt_register_mech(crypt_mech); +} + +/* Inline to64 helper for SMD5 */ +static unsigned char itoa64[] = +"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +static void to64(char *s, unsigned long v, int n) +{ + while (--n >= 0) { + *s++ = itoa64[v & 0x3f]; + v >>= 6; + } +} + +/* Inline ircd_crypt_smd5 */ +const char* ircd_crypt_smd5(const char* key, const char* salt) +{ + const char *magic = "$1$"; + static char passwd[120]; + char *p; + const char *sp, *ep; + unsigned char final[16]; + int sl, pl, i, j; + MD5_CTX ctx, ctx1; + unsigned long l; + + assert(NULL != key); + assert(NULL != salt); + + ep = sp = salt; + for (ep = sp; *ep && *ep != '$' && ep < (sp + 8); ep++) + continue; + sl = ep - sp; + + MD5Init(&ctx); + MD5Update(&ctx,(unsigned const char *)key,strlen(key)); + MD5Update(&ctx,(unsigned const char *)magic,strlen(magic)); + MD5Update(&ctx,(unsigned const char *)sp,sl); + + MD5Init(&ctx1); + MD5Update(&ctx1,(unsigned const char *)key,strlen(key)); + MD5Update(&ctx1,(unsigned const char *)sp,sl); + MD5Update(&ctx1,(unsigned const char *)key,strlen(key)); + MD5Final(final,&ctx1); + for (pl = strlen(key); pl > 0; pl -= 16) + MD5Update(&ctx,(unsigned const char *)final,pl>16 ? 16 : pl); + + memset(final, 0, sizeof final); + + for (j = 0, i = strlen(key); i; i >>= 1) + if (i & 1) + MD5Update(&ctx, (unsigned const char *)final+j, 1); + else + MD5Update(&ctx, (unsigned const char *)key+j, 1); + + memset(passwd, 0, 120); + strncpy(passwd, sp, sl); + strcat(passwd, "$"); + + MD5Final(final,&ctx); + + for (i = 0; i < 1000; i++) { + MD5Init(&ctx1); + + if (i & 1) + MD5Update(&ctx1,(unsigned const char *)key,strlen(key)); + else + MD5Update(&ctx1,(unsigned const char *)final,16); + + if (i % 3) + MD5Update(&ctx1,(unsigned const char *)sp,sl); + + if (i % 7) + MD5Update(&ctx1,(unsigned const char *)key,strlen(key)); + + if (i & 1) + MD5Update(&ctx1,(unsigned const char *)final,16); + else + MD5Update(&ctx1,(unsigned const char *)key,strlen(key)); + + MD5Final(final,&ctx1); + } + + p = passwd + strlen(passwd); + + l = (final[0] << 16) | (final[6] << 8) | final[12]; + to64(p, l, 4); p += 4; + l = (final[1] << 16) | (final[7] << 8) | final[13]; + to64(p, l, 4); p += 4; + l = (final[2] << 16) | (final[8] << 8) | final[14]; + to64(p, l, 4); p += 4; + l = (final[3] << 16) | (final[9] << 8) | final[15]; + to64(p, l, 4); p += 4; + l = (final[4] << 16) | (final[10] << 8) | final[5]; + to64(p, l, 4); p += 4; + l = final[11]; + to64(p, l, 2); p += 2; + *p = '\0'; + + memset(final, 0, sizeof final); + + return passwd; +} + +/* Register SMD5 mechanism */ +void ircd_register_crypt_smd5(void) +{ + crypt_mech_t* crypt_mech; + + if ((crypt_mech = (crypt_mech_t*)MyMalloc(sizeof(crypt_mech_t))) == NULL) + return; + + crypt_mech->mechname = "smd5"; + crypt_mech->shortname = "crypt_smd5"; + crypt_mech->description = "Salted MD5 password hash mechanism."; + crypt_mech->crypt_function = &ircd_crypt_smd5; + crypt_mech->crypt_token = "$SMD5$"; + crypt_mech->crypt_token_size = 6; + + ircd_crypt_register_mech(crypt_mech); +} + +/* Inline ircd_crypt */ +char* ircd_crypt(const char* key, const char* salt) +{ + char *hashed_pass = NULL; + const char *temp_hashed_pass, *mysalt; + crypt_mechs_t* crypt_mech; + + assert(NULL != key); + assert(NULL != salt); + + crypt_mech = crypt_mechs_root->next; + + for (;crypt_mech;) + { + if (strlen(salt) < (size_t)crypt_mech->mech->crypt_token_size) + { + crypt_mech = crypt_mech->next; + continue; + } + + if(0 == ircd_strncmp(crypt_mech->mech->crypt_token, salt, crypt_mech->mech->crypt_token_size)) + { + if(strlen(salt) < (size_t)crypt_mech->mech->crypt_token_size + 1) + return NULL; + + mysalt = salt + crypt_mech->mech->crypt_token_size; + + if(NULL == (temp_hashed_pass = crypt_mech->mech->crypt_function(key, mysalt))) + return NULL; + + if(NULL == (hashed_pass = (char *)MyMalloc(sizeof(char)*strlen(temp_hashed_pass) + crypt_mech->mech->crypt_token_size + 1))) + return NULL; + + memset(hashed_pass, 0, sizeof(char)*strlen(temp_hashed_pass) + +crypt_mech->mech->crypt_token_size + 1); + ircd_strncpy(hashed_pass, crypt_mech->mech->crypt_token, + crypt_mech->mech->crypt_token_size); + ircd_strncpy(hashed_pass + crypt_mech->mech->crypt_token_size, temp_hashed_pass, strlen(temp_hashed_pass)); + } else { + crypt_mech = crypt_mech->next; + continue; + } + return hashed_pass; + } + + /* try to use native crypt for an old-style (untagged) password */ + if (strlen(salt) > 2) + { + char *s; + if (NULL == (temp_hashed_pass = (char*)ircd_crypt_native(key, salt))) + return NULL; + if (!ircd_strcmp(temp_hashed_pass, salt)) + { + DupString(s, temp_hashed_pass); + return s; + } + } + + return NULL; +} + +/* Inline ircd_crypt_init */ +void ircd_crypt_init(void) +{ + if((crypt_mechs_root = MyMalloc(sizeof(crypt_mechs_t))) == NULL) + return; + + crypt_mechs_root->mech = NULL; + crypt_mechs_root->next = crypt_mechs_root->prev = NULL; + + ircd_register_crypt_smd5(); + ircd_register_crypt_plain(); +} + +/* Inline oper_password_match */ +int oper_password_match(const char* to_match, const char* passwd) +{ + char *crypted; + int res; + + if (!to_match || !passwd) + return 0; + + crypted = ircd_crypt(to_match, passwd); + + if (!crypted) + return 0; + res = strcmp(crypted, passwd); + MyFree(crypted); + return 0 == res; +} + + +/* ========== Test fixtures ========== */ + +static int setup_crypt(void **state) +{ + (void)state; + ircd_crypt_init(); + return 0; +} + +static int teardown_crypt(void **state) +{ + (void)state; + /* Note: In production code we'd free all mechanisms, but for tests + * we just leave them allocated - they're small and short-lived */ + return 0; +} + + +/* ========== ircd_crypt_plain tests ========== */ + +static void test_crypt_plain_returns_key(void **state) +{ + (void)state; + const char *result; + + /* PLAIN mechanism just returns the key unchanged */ + result = ircd_crypt_plain("password", "salt"); + assert_string_equal("password", result); +} + +static void test_crypt_plain_different_inputs(void **state) +{ + (void)state; + const char *result; + + result = ircd_crypt_plain("secret123", "anysalt"); + assert_string_equal("secret123", result); + + result = ircd_crypt_plain("", "salt"); + assert_string_equal("", result); +} + + +/* ========== ircd_crypt_smd5 tests ========== */ + +static void test_crypt_smd5_produces_hash(void **state) +{ + (void)state; + const char *result; + + result = ircd_crypt_smd5("password", "saltsalt"); + + assert_non_null(result); + assert_true(strlen(result) > 0); +} + +static void test_crypt_smd5_deterministic(void **state) +{ + (void)state; + const char *result1, *result2; + char saved[128]; + + /* Same password + salt should produce same hash */ + result1 = ircd_crypt_smd5("mypassword", "testsalt"); + strncpy(saved, result1, sizeof(saved) - 1); + saved[sizeof(saved) - 1] = '\0'; + + result2 = ircd_crypt_smd5("mypassword", "testsalt"); + assert_string_equal(saved, result2); +} + +static void test_crypt_smd5_different_passwords(void **state) +{ + (void)state; + const char *result1, *result2; + char saved[128]; + + result1 = ircd_crypt_smd5("password1", "salt1234"); + strncpy(saved, result1, sizeof(saved) - 1); + saved[sizeof(saved) - 1] = '\0'; + + result2 = ircd_crypt_smd5("password2", "salt1234"); + + /* Different passwords should produce different hashes */ + assert_string_not_equal(saved, result2); +} + +static void test_crypt_smd5_different_salts(void **state) +{ + (void)state; + const char *result1, *result2; + char saved[128]; + + result1 = ircd_crypt_smd5("samepassword", "salt1111"); + strncpy(saved, result1, sizeof(saved) - 1); + saved[sizeof(saved) - 1] = '\0'; + + result2 = ircd_crypt_smd5("samepassword", "salt2222"); + + /* Different salts should produce different hashes */ + assert_string_not_equal(saved, result2); +} + + +/* ========== ircd_crypt tests ========== */ + +static void test_ircd_crypt_plain_tagged(void **state) +{ + (void)state; + char *result; + + /* Test PLAIN mechanism with tag */ + result = ircd_crypt("testpass", "$PLAIN$testpass"); + + assert_non_null(result); + assert_string_equal("$PLAIN$testpass", result); + MyFree(result); +} + +static void test_ircd_crypt_smd5_tagged(void **state) +{ + (void)state; + char *result; + + /* Test SMD5 mechanism with tag */ + result = ircd_crypt("password", "$SMD5$saltsalt$somehash"); + + assert_non_null(result); + /* Should start with $SMD5$ tag */ + assert_true(strncmp(result, "$SMD5$", 6) == 0); + MyFree(result); +} + +static void test_ircd_crypt_unknown_tag(void **state) +{ + (void)state; + char *result; + + /* Unknown tag should fall through to native crypt (which we stub as NULL) */ + result = ircd_crypt("password", "$UNKNOWN$salt"); + + /* Our stub returns NULL for native crypt */ + assert_null(result); +} + +static void test_ircd_crypt_short_salt(void **state) +{ + (void)state; + char *result; + + /* Salt too short for any mechanism */ + result = ircd_crypt("password", "ab"); + + /* Should return NULL */ + assert_null(result); +} + + +/* ========== oper_password_match tests ========== */ + +static void test_oper_password_match_plain_correct(void **state) +{ + (void)state; + int result; + + /* Correct password should match */ + result = oper_password_match("secretpass", "$PLAIN$secretpass"); + assert_int_equal(1, result); +} + +static void test_oper_password_match_plain_incorrect(void **state) +{ + (void)state; + int result; + + /* Incorrect password should not match */ + result = oper_password_match("wrongpass", "$PLAIN$secretpass"); + assert_int_equal(0, result); +} + +static void test_oper_password_match_null_inputs(void **state) +{ + (void)state; + int result; + + /* NULL inputs should return 0 (no match) */ + result = oper_password_match(NULL, "$PLAIN$test"); + assert_int_equal(0, result); + + result = oper_password_match("test", NULL); + assert_int_equal(0, result); + + result = oper_password_match(NULL, NULL); + assert_int_equal(0, result); +} + +static void test_oper_password_match_empty_password(void **state) +{ + (void)state; + int result; + + /* $PLAIN$ without a password after the tag returns NULL from ircd_crypt + * because the implementation requires at least one character after the token. + * This is correct security behavior - reject malformed password entries. */ + result = oper_password_match("", "$PLAIN$"); + assert_int_equal(0, result); + + /* But $PLAIN$ with an actual empty string placeholder should work */ + /* Actually, the token check requires strlen > token_size, so even empty + * string after token would fail. Let's verify single char works: */ + result = oper_password_match("x", "$PLAIN$x"); + assert_int_equal(1, result); +} + +static void test_oper_password_match_smd5(void **state) +{ + (void)state; + char *hashed; + int result; + + /* First generate a hash for a known password */ + hashed = ircd_crypt("operpass", "$SMD5$saltsalt$"); + assert_non_null(hashed); + + /* Now verify the password matches */ + result = oper_password_match("operpass", hashed); + assert_int_equal(1, result); + + /* Wrong password should not match */ + result = oper_password_match("wrongpass", hashed); + assert_int_equal(0, result); + + MyFree(hashed); +} + + +/* ========== Mechanism registration tests ========== */ + +static void test_mechanism_registered(void **state) +{ + (void)state; + crypt_mechs_t *mech; + int found_plain = 0, found_smd5 = 0; + + /* Verify both mechanisms are registered */ + for (mech = crypt_mechs_root->next; mech; mech = mech->next) + { + if (strcmp(mech->mech->shortname, "crypt_plain") == 0) + found_plain = 1; + if (strcmp(mech->mech->shortname, "crypt_smd5") == 0) + found_smd5 = 1; + } + + assert_int_equal(1, found_plain); + assert_int_equal(1, found_smd5); +} + +static void test_mechanism_tokens(void **state) +{ + (void)state; + crypt_mechs_t *mech; + + /* Verify token formats */ + for (mech = crypt_mechs_root->next; mech; mech = mech->next) + { + if (strcmp(mech->mech->shortname, "crypt_plain") == 0) + { + assert_string_equal("$PLAIN$", mech->mech->crypt_token); + assert_int_equal(7, mech->mech->crypt_token_size); + } + if (strcmp(mech->mech->shortname, "crypt_smd5") == 0) + { + assert_string_equal("$SMD5$", mech->mech->crypt_token); + assert_int_equal(6, mech->mech->crypt_token_size); + } + } +} + + +int main(void) +{ + const struct CMUnitTest tests[] = { + /* ircd_crypt_plain */ + cmocka_unit_test_setup_teardown(test_crypt_plain_returns_key, setup_crypt, teardown_crypt), + cmocka_unit_test_setup_teardown(test_crypt_plain_different_inputs, setup_crypt, teardown_crypt), + + /* ircd_crypt_smd5 */ + cmocka_unit_test_setup_teardown(test_crypt_smd5_produces_hash, setup_crypt, teardown_crypt), + cmocka_unit_test_setup_teardown(test_crypt_smd5_deterministic, setup_crypt, teardown_crypt), + cmocka_unit_test_setup_teardown(test_crypt_smd5_different_passwords, setup_crypt, teardown_crypt), + cmocka_unit_test_setup_teardown(test_crypt_smd5_different_salts, setup_crypt, teardown_crypt), + + /* ircd_crypt */ + cmocka_unit_test_setup_teardown(test_ircd_crypt_plain_tagged, setup_crypt, teardown_crypt), + cmocka_unit_test_setup_teardown(test_ircd_crypt_smd5_tagged, setup_crypt, teardown_crypt), + cmocka_unit_test_setup_teardown(test_ircd_crypt_unknown_tag, setup_crypt, teardown_crypt), + cmocka_unit_test_setup_teardown(test_ircd_crypt_short_salt, setup_crypt, teardown_crypt), + + /* oper_password_match */ + cmocka_unit_test_setup_teardown(test_oper_password_match_plain_correct, setup_crypt, teardown_crypt), + cmocka_unit_test_setup_teardown(test_oper_password_match_plain_incorrect, setup_crypt, teardown_crypt), + cmocka_unit_test_setup_teardown(test_oper_password_match_null_inputs, setup_crypt, teardown_crypt), + cmocka_unit_test_setup_teardown(test_oper_password_match_empty_password, setup_crypt, teardown_crypt), + cmocka_unit_test_setup_teardown(test_oper_password_match_smd5, setup_crypt, teardown_crypt), + + /* Mechanism registration */ + cmocka_unit_test_setup_teardown(test_mechanism_registered, setup_crypt, teardown_crypt), + cmocka_unit_test_setup_teardown(test_mechanism_tokens, setup_crypt, teardown_crypt), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/ircd/test/ircd_in_addr_cmocka.c b/ircd/test/ircd_in_addr_cmocka.c new file mode 100644 index 00000000..b3b9ca36 --- /dev/null +++ b/ircd/test/ircd_in_addr_cmocka.c @@ -0,0 +1,393 @@ +/* + * ircd_in_addr_cmocka.c - CMocka unit tests for IP address handling + * + * Tests IPv4 and IPv6 address parsing, formatting, and validation. + */ + +#include +#include +#include +#include +#include +#include + +#include "ircd_string.h" +#include "numnicks.h" +#include "res.h" + + +/* ========== Helper to create IPv4-mapped address ========== */ + +static void make_ipv4_addr(struct irc_in_addr *addr, + unsigned char a, unsigned char b, + unsigned char c, unsigned char d) +{ + memset(addr, 0, sizeof(*addr)); + addr->in6_16[5] = htons(0xffff); /* IPv4-mapped prefix */ + addr->in6_16[6] = htons((a << 8) | b); + addr->in6_16[7] = htons((c << 8) | d); +} + + +/* ========== ircd_aton (parse IP address) ========== */ + +static void test_aton_ipv4_localhost(void **state) +{ + (void)state; + struct irc_in_addr addr; + int len; + + len = ircd_aton(&addr, "127.0.0.1"); + assert_int_equal(len, strlen("127.0.0.1")); + + /* Check it's IPv4-mapped */ + assert_int_equal(ntohs(addr.in6_16[5]), 0xffff); + assert_int_equal(ntohs(addr.in6_16[6]), 0x7f00); + assert_int_equal(ntohs(addr.in6_16[7]), 0x0001); +} + +static void test_aton_ipv4_various(void **state) +{ + (void)state; + struct irc_in_addr addr; + int len; + + /* 0.0.0.0 */ + len = ircd_aton(&addr, "0.0.0.0"); + assert_true(len > 0); + assert_int_equal(ntohs(addr.in6_16[6]), 0x0000); + assert_int_equal(ntohs(addr.in6_16[7]), 0x0000); + + /* 255.255.255.255 */ + len = ircd_aton(&addr, "255.255.255.255"); + assert_true(len > 0); + assert_int_equal(ntohs(addr.in6_16[6]), 0xffff); + assert_int_equal(ntohs(addr.in6_16[7]), 0xffff); + + /* 192.168.1.1 */ + len = ircd_aton(&addr, "192.168.1.1"); + assert_true(len > 0); + assert_int_equal(ntohs(addr.in6_16[6]), 0xc0a8); + assert_int_equal(ntohs(addr.in6_16[7]), 0x0101); +} + +static void test_aton_ipv6_localhost(void **state) +{ + (void)state; + struct irc_in_addr addr; + int len; + + len = ircd_aton(&addr, "::1"); + assert_true(len > 0); + + /* First 7 segments should be 0 */ + for (int i = 0; i < 7; i++) { + assert_int_equal(addr.in6_16[i], 0); + } + /* Last segment is 1 */ + assert_int_equal(ntohs(addr.in6_16[7]), 1); +} + +static void test_aton_ipv6_full(void **state) +{ + (void)state; + struct irc_in_addr addr; + int len; + + len = ircd_aton(&addr, "2001:db8::1"); + assert_true(len > 0); + assert_int_equal(ntohs(addr.in6_16[0]), 0x2001); + assert_int_equal(ntohs(addr.in6_16[1]), 0x0db8); +} + +static void test_aton_ipv6_all_segments(void **state) +{ + (void)state; + struct irc_in_addr addr; + int len; + + len = ircd_aton(&addr, "1:2:3:4:5:6:7:8"); + assert_true(len > 0); + + assert_int_equal(ntohs(addr.in6_16[0]), 1); + assert_int_equal(ntohs(addr.in6_16[1]), 2); + assert_int_equal(ntohs(addr.in6_16[2]), 3); + assert_int_equal(ntohs(addr.in6_16[3]), 4); + assert_int_equal(ntohs(addr.in6_16[4]), 5); + assert_int_equal(ntohs(addr.in6_16[5]), 6); + assert_int_equal(ntohs(addr.in6_16[6]), 7); + assert_int_equal(ntohs(addr.in6_16[7]), 8); +} + +static void test_aton_ipv4_mapped_ipv6(void **state) +{ + (void)state; + struct irc_in_addr addr; + int len; + + /* ::ffff:127.0.0.1 notation */ + len = ircd_aton(&addr, "::ffff:127.0.0.1"); + assert_true(len > 0); + assert_int_equal(ntohs(addr.in6_16[5]), 0xffff); + assert_int_equal(ntohs(addr.in6_16[6]), 0x7f00); + assert_int_equal(ntohs(addr.in6_16[7]), 0x0001); +} + + +/* ========== ircd_ntoa (format IP address) ========== */ + +static void test_ntoa_ipv4(void **state) +{ + (void)state; + struct irc_in_addr addr; + const char *result; + + make_ipv4_addr(&addr, 127, 0, 0, 1); + result = ircd_ntoa(&addr); + assert_string_equal(result, "127.0.0.1"); + + make_ipv4_addr(&addr, 192, 168, 1, 1); + result = ircd_ntoa(&addr); + assert_string_equal(result, "192.168.1.1"); + + make_ipv4_addr(&addr, 10, 0, 0, 1); + result = ircd_ntoa(&addr); + assert_string_equal(result, "10.0.0.1"); +} + +static void test_ntoa_ipv6(void **state) +{ + (void)state; + struct irc_in_addr addr; + const char *result; + + /* ::1 */ + memset(&addr, 0, sizeof(addr)); + addr.in6_16[7] = htons(1); + result = ircd_ntoa(&addr); + assert_non_null(result); + /* Should contain ::1 or 0::1 */ + assert_non_null(strstr(result, "1")); +} + +static void test_ntoa_r_buffer(void **state) +{ + (void)state; + struct irc_in_addr addr; + char buf[64]; + + make_ipv4_addr(&addr, 8, 8, 8, 8); + ircd_ntoa_r(buf, &addr); + assert_string_equal(buf, "8.8.8.8"); +} + + +/* ========== irc_in_addr_is_ipv4 ========== */ + +static void test_is_ipv4(void **state) +{ + (void)state; + struct irc_in_addr addr; + + /* IPv4-mapped address */ + make_ipv4_addr(&addr, 192, 168, 1, 1); + assert_true(irc_in_addr_is_ipv4(&addr)); + + /* Pure IPv6 */ + memset(&addr, 0, sizeof(addr)); + addr.in6_16[0] = htons(0x2001); + addr.in6_16[7] = htons(1); + assert_false(irc_in_addr_is_ipv4(&addr)); +} + + +/* ========== irc_in_addr_is_loopback ========== */ + +static void test_is_loopback_ipv4(void **state) +{ + (void)state; + struct irc_in_addr addr; + + /* 127.0.0.1 is loopback */ + make_ipv4_addr(&addr, 127, 0, 0, 1); + assert_true(irc_in_addr_is_loopback(&addr)); + + /* 127.255.255.255 is also loopback (127.0.0.0/8) */ + make_ipv4_addr(&addr, 127, 255, 255, 255); + assert_true(irc_in_addr_is_loopback(&addr)); + + /* 192.168.1.1 is not loopback */ + make_ipv4_addr(&addr, 192, 168, 1, 1); + assert_false(irc_in_addr_is_loopback(&addr)); +} + +static void test_is_loopback_ipv6(void **state) +{ + (void)state; + struct irc_in_addr addr; + + /* ::1 is loopback */ + memset(&addr, 0, sizeof(addr)); + addr.in6_16[7] = htons(1); + assert_true(irc_in_addr_is_loopback(&addr)); + + /* 2001:db8::1 is not loopback */ + memset(&addr, 0, sizeof(addr)); + addr.in6_16[0] = htons(0x2001); + addr.in6_16[7] = htons(1); + assert_false(irc_in_addr_is_loopback(&addr)); +} + + +/* ========== irc_in_addr_valid ========== */ + +static void test_addr_valid(void **state) +{ + (void)state; + struct irc_in_addr addr; + + /* 127.0.0.1 is valid */ + make_ipv4_addr(&addr, 127, 0, 0, 1); + assert_true(irc_in_addr_valid(&addr)); + + /* :: (all zeros) is not valid */ + memset(&addr, 0, sizeof(addr)); + assert_false(irc_in_addr_valid(&addr)); + + /* ::1 is valid */ + memset(&addr, 0, sizeof(addr)); + addr.in6_16[7] = htons(1); + assert_true(irc_in_addr_valid(&addr)); +} + + +/* ========== irc_in_addr_cmp ========== */ + +static void test_addr_cmp_equal(void **state) +{ + (void)state; + struct irc_in_addr addr1, addr2; + + make_ipv4_addr(&addr1, 192, 168, 1, 1); + make_ipv4_addr(&addr2, 192, 168, 1, 1); + assert_int_equal(0, irc_in_addr_cmp(&addr1, &addr2)); +} + +static void test_addr_cmp_not_equal(void **state) +{ + (void)state; + struct irc_in_addr addr1, addr2; + + make_ipv4_addr(&addr1, 192, 168, 1, 1); + make_ipv4_addr(&addr2, 192, 168, 1, 2); + assert_int_not_equal(0, irc_in_addr_cmp(&addr1, &addr2)); +} + + +/* ========== ipmask_parse ========== */ + +static void test_ipmask_parse_ipv4_cidr(void **state) +{ + (void)state; + struct irc_in_addr addr; + unsigned char bits; + int len; + + /* 192.168.0.0/16 style is only valid as ipmask */ + len = ipmask_parse("192.168/16", &addr, &bits); + assert_true(len > 0); + assert_int_equal(bits, 112); /* 96 (ipv4 prefix) + 16 */ +} + +static void test_ipmask_parse_wildcard(void **state) +{ + (void)state; + struct irc_in_addr addr; + unsigned char bits; + int len; + + /* 192.* wildcard */ + len = ipmask_parse("192.*", &addr, &bits); + assert_true(len > 0); + assert_int_equal(bits, 104); /* 96 + 8 bits */ + + /* Broader wildcard */ + len = ipmask_parse("*", &addr, &bits); + assert_true(len > 0); + assert_int_equal(bits, 0); /* Match all */ +} + +static void test_ipmask_parse_invalid(void **state) +{ + (void)state; + struct irc_in_addr addr; + unsigned char bits; + int len; + + /* Invalid: can't mix wildcard and CIDR */ + len = ipmask_parse("192.*/8", &addr, &bits); + assert_int_equal(len, 0); /* Should fail */ + + /* Invalid: not an IP */ + len = ipmask_parse("not-an-ip", &addr, &bits); + assert_int_equal(len, 0); +} + + +/* ========== Round-trip tests ========== */ + +static void test_ip_roundtrip_ipv4(void **state) +{ + (void)state; + struct irc_in_addr original, parsed; + char buf[64]; + + /* Create address, format it, parse it back */ + make_ipv4_addr(&original, 203, 0, 113, 42); + ircd_ntoa_r(buf, &original); + ircd_aton(&parsed, buf); + + /* Should match */ + assert_int_equal(0, irc_in_addr_cmp(&original, &parsed)); +} + + +int main(void) +{ + const struct CMUnitTest tests[] = { + /* ircd_aton - IPv4 */ + cmocka_unit_test(test_aton_ipv4_localhost), + cmocka_unit_test(test_aton_ipv4_various), + + /* ircd_aton - IPv6 */ + cmocka_unit_test(test_aton_ipv6_localhost), + cmocka_unit_test(test_aton_ipv6_full), + cmocka_unit_test(test_aton_ipv6_all_segments), + cmocka_unit_test(test_aton_ipv4_mapped_ipv6), + + /* ircd_ntoa */ + cmocka_unit_test(test_ntoa_ipv4), + cmocka_unit_test(test_ntoa_ipv6), + cmocka_unit_test(test_ntoa_r_buffer), + + /* Address type detection */ + cmocka_unit_test(test_is_ipv4), + cmocka_unit_test(test_is_loopback_ipv4), + cmocka_unit_test(test_is_loopback_ipv6), + cmocka_unit_test(test_addr_valid), + + /* Address comparison */ + cmocka_unit_test(test_addr_cmp_equal), + cmocka_unit_test(test_addr_cmp_not_equal), + + /* IP mask parsing */ + cmocka_unit_test(test_ipmask_parse_ipv4_cidr), + cmocka_unit_test(test_ipmask_parse_wildcard), + cmocka_unit_test(test_ipmask_parse_invalid), + + /* Round-trip */ + cmocka_unit_test(test_ip_roundtrip_ipv4), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/ircd/test/ircd_match_cmocka.c b/ircd/test/ircd_match_cmocka.c new file mode 100644 index 00000000..2ce9fd51 --- /dev/null +++ b/ircd/test/ircd_match_cmocka.c @@ -0,0 +1,300 @@ +/* + * ircd_match_cmocka.c - CMocka unit tests for IRC glob/wildcard matching + * + * Tests the match() function which implements IRC-style wildcard matching. + * NOTE: match() returns 0 on match, non-zero on no match (inverted from + * typical boolean conventions). + */ + +#include +#include +#include +#include + +#include "match.h" + +/* + * Helper macros to make tests more readable. + * Remember: match() returns 0 on success, so we invert the logic. + */ +#define ASSERT_MATCHES(glob, str) \ + assert_int_equal(0, match((glob), (str))) + +#define ASSERT_NO_MATCH(glob, str) \ + assert_int_not_equal(0, match((glob), (str))) + + +/* ========== Basic Literal Matching ========== */ + +static void test_exact_match(void **state) +{ + (void)state; + + ASSERT_MATCHES("abc", "abc"); + ASSERT_MATCHES("hello", "hello"); + ASSERT_MATCHES("test123", "test123"); + ASSERT_MATCHES("", ""); /* Empty strings match */ +} + +/* Test non-matching strings. + * Note: IRC matching is case-insensitive per RFC 1459 section 2.2, which + * defines a special case mapping where {}|^ are lowercase of []\~. This + * is historical IRC behavior required for nickname/channel comparison. */ +static void test_exact_no_match(void **state) +{ + (void)state; + + ASSERT_NO_MATCH("abc", "abcd"); + ASSERT_NO_MATCH("abcd", "abc"); + ASSERT_NO_MATCH("hello", "world"); + + /* IRC matching is case-insensitive (RFC 1459 section 2.2) */ + ASSERT_MATCHES("abc", "ABC"); + ASSERT_MATCHES("Hello", "hELLO"); +} + + +/* ========== Single Character Wildcard (?) ========== */ + +static void test_question_mark_basic(void **state) +{ + (void)state; + + /* ? matches exactly one character */ + ASSERT_MATCHES("?", "a"); + ASSERT_MATCHES("?", "x"); + ASSERT_MATCHES("?", "*"); /* ? matches literal * */ + ASSERT_MATCHES("?", "?"); /* ? matches literal ? */ +} + +static void test_question_mark_no_match(void **state) +{ + (void)state; + + /* ? must match exactly one character */ + ASSERT_NO_MATCH("?", ""); /* Empty string - no char to match */ + ASSERT_NO_MATCH("?", "ab"); /* Too many characters */ + ASSERT_NO_MATCH("?", "abc"); +} + +static void test_question_mark_in_pattern(void **state) +{ + (void)state; + + ASSERT_MATCHES("a?c", "abc"); + ASSERT_MATCHES("a?c", "aXc"); + ASSERT_MATCHES("???", "abc"); + ASSERT_MATCHES("h?llo", "hello"); + ASSERT_MATCHES("h?llo", "hallo"); + + ASSERT_NO_MATCH("a?c", "ac"); /* Missing character */ + ASSERT_NO_MATCH("a?c", "abbc"); /* Too many characters */ +} + + +/* ========== Multi-Character Wildcard (*) ========== */ + +static void test_asterisk_basic(void **state) +{ + (void)state; + + /* * matches zero or more characters */ + ASSERT_MATCHES("*", ""); + ASSERT_MATCHES("*", "a"); + ASSERT_MATCHES("*", "anything"); + ASSERT_MATCHES("*", "literally anything at all"); +} + +static void test_asterisk_prefix(void **state) +{ + (void)state; + + ASSERT_MATCHES("*abc", "abc"); + ASSERT_MATCHES("*abc", "xyzabc"); + ASSERT_MATCHES("*abc", "123abc"); + + ASSERT_NO_MATCH("*abc", "abcd"); + ASSERT_NO_MATCH("*abc", "ab"); +} + +static void test_asterisk_suffix(void **state) +{ + (void)state; + + ASSERT_MATCHES("abc*", "abc"); + ASSERT_MATCHES("abc*", "abcdef"); + ASSERT_MATCHES("abc*", "abc123"); + + ASSERT_NO_MATCH("abc*", "ab"); + ASSERT_NO_MATCH("abc*", "xabc"); +} + +static void test_asterisk_middle(void **state) +{ + (void)state; + + ASSERT_MATCHES("a*c", "ac"); + ASSERT_MATCHES("a*c", "abc"); + ASSERT_MATCHES("a*c", "aXXXXXc"); + + ASSERT_NO_MATCH("a*c", "ab"); + ASSERT_NO_MATCH("a*c", "acd"); +} + +static void test_asterisk_multiple(void **state) +{ + (void)state; + + ASSERT_MATCHES("*a*", "a"); + ASSERT_MATCHES("*a*", "abc"); + ASSERT_MATCHES("*a*", "xax"); + ASSERT_MATCHES("*a*", "xxaxx"); + + ASSERT_NO_MATCH("*a*", "b"); + ASSERT_NO_MATCH("*a*", "xyz"); +} + + +/* ========== Escape Sequences ========== */ + +static void test_escaped_asterisk(void **state) +{ + (void)state; + + /* \* matches literal asterisk */ + ASSERT_MATCHES("\\*", "*"); + ASSERT_NO_MATCH("\\*", "a"); + ASSERT_NO_MATCH("\\*", "\\*"); /* Should not match backslash-asterisk */ +} + +static void test_escaped_question_mark(void **state) +{ + (void)state; + + /* \? matches literal question mark */ + ASSERT_MATCHES("\\?", "?"); + ASSERT_NO_MATCH("\\?", "a"); + ASSERT_NO_MATCH("\\?", "\\?"); +} + +static void test_escaped_backslash(void **state) +{ + (void)state; + + /* \\ matches literal backslash */ + ASSERT_MATCHES("\\\\", "\\"); + ASSERT_NO_MATCH("\\\\", "\\\\"); +} + + +/* ========== IRC-Specific Patterns ========== */ + +static void test_irc_hostmask_patterns(void **state) +{ + (void)state; + + /* nick!user@host patterns */ + ASSERT_MATCHES("*!*@*", "nick!user@host.com"); + ASSERT_MATCHES("nick!*@*", "nick!user@host.com"); + ASSERT_MATCHES("*!user@*", "nick!user@host.com"); + ASSERT_MATCHES("*!*@*.com", "nick!user@host.com"); + + ASSERT_NO_MATCH("*!*@*", "nick@host"); /* Missing ! */ + ASSERT_NO_MATCH("other!*@*", "nick!user@host"); +} + +static void test_irc_channel_patterns(void **state) +{ + (void)state; + + ASSERT_MATCHES("#*", "#channel"); + ASSERT_MATCHES("#test*", "#testing"); + ASSERT_MATCHES("#*chat*", "#superchat"); + ASSERT_MATCHES("&*", "&localchan"); + + ASSERT_NO_MATCH("#*", "channel"); /* Missing # */ +} + + +/* ========== Edge Cases ========== */ + +static void test_consecutive_wildcards(void **state) +{ + (void)state; + + /* Multiple consecutive wildcards should work */ + ASSERT_MATCHES("**", "anything"); + ASSERT_MATCHES("***", "anything"); + ASSERT_MATCHES("*?*", "a"); + ASSERT_MATCHES("*?*", "abc"); + ASSERT_MATCHES("??*", "ab"); + ASSERT_MATCHES("??*", "abc"); + + ASSERT_NO_MATCH("??*", "a"); /* Need at least 2 chars */ +} + +static void test_complex_patterns(void **state) +{ + (void)state; + + /* Complex real-world patterns */ + ASSERT_MATCHES("*\\\\[*!~*", "har\\[dy!~boy"); + ASSERT_NO_MATCH("*\\\\[*!~*", "dark\\s|de!pimp"); + ASSERT_NO_MATCH("*\\\\[*!~*", "joe\\[mama"); +} + + +/* ========== mmatch() - Mask matching ========== */ + +static void test_mmatch_basic(void **state) +{ + (void)state; + + /* mmatch compares two masks - returns 0 if old_mask encompasses new_mask */ + assert_int_equal(0, mmatch("*", "anything")); + assert_int_equal(0, mmatch("*!*@*", "*!*@*.com")); + assert_int_equal(0, mmatch("*!*@*.com", "*!*@host.com")); + + /* These should NOT match (new is broader than old) */ + assert_int_not_equal(0, mmatch("*!*@*.com", "*!*@*")); +} + + +int main(void) +{ + const struct CMUnitTest tests[] = { + /* Basic literal matching */ + cmocka_unit_test(test_exact_match), + cmocka_unit_test(test_exact_no_match), + + /* Single character wildcard */ + cmocka_unit_test(test_question_mark_basic), + cmocka_unit_test(test_question_mark_no_match), + cmocka_unit_test(test_question_mark_in_pattern), + + /* Multi-character wildcard */ + cmocka_unit_test(test_asterisk_basic), + cmocka_unit_test(test_asterisk_prefix), + cmocka_unit_test(test_asterisk_suffix), + cmocka_unit_test(test_asterisk_middle), + cmocka_unit_test(test_asterisk_multiple), + + /* Escape sequences */ + cmocka_unit_test(test_escaped_asterisk), + cmocka_unit_test(test_escaped_question_mark), + cmocka_unit_test(test_escaped_backslash), + + /* IRC-specific patterns */ + cmocka_unit_test(test_irc_hostmask_patterns), + cmocka_unit_test(test_irc_channel_patterns), + + /* Edge cases */ + cmocka_unit_test(test_consecutive_wildcards), + cmocka_unit_test(test_complex_patterns), + + /* mmatch */ + cmocka_unit_test(test_mmatch_basic), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/ircd/test/ircd_string_cmocka.c b/ircd/test/ircd_string_cmocka.c new file mode 100644 index 00000000..d22f4f15 --- /dev/null +++ b/ircd/test/ircd_string_cmocka.c @@ -0,0 +1,424 @@ +/* + * ircd_string_cmocka.c - CMocka unit tests for IRC string utilities + * + * Tests various string manipulation functions used throughout the IRC daemon. + */ + +#include +#include +#include +#include +#include +#include + +#include "ircd_string.h" +#include "ircd_chattr.h" + + +/* ========== ircd_strncpy ========== */ + +static void test_ircd_strncpy_normal(void **state) +{ + (void)state; + char dest[32]; + + /* Normal copy */ + ircd_strncpy(dest, "hello", sizeof(dest)); + assert_string_equal(dest, "hello"); + + /* Copy with exact size */ + ircd_strncpy(dest, "world", 6); + assert_string_equal(dest, "world"); +} + +static void test_ircd_strncpy_truncation(void **state) +{ + (void)state; + char dest[8]; + + /* Source longer than dest - should truncate */ + ircd_strncpy(dest, "hello world", sizeof(dest) - 1); + assert_int_equal(strlen(dest), 7); +} + +static void test_ircd_strncpy_empty(void **state) +{ + (void)state; + char dest[32]; + + /* Empty string copy */ + ircd_strncpy(dest, "", sizeof(dest)); + assert_string_equal(dest, ""); + assert_int_equal(strlen(dest), 0); +} + + +/* ========== ircd_strcmp (case-insensitive IRC comparison) ========== */ + +static void test_ircd_strcmp_equal(void **state) +{ + (void)state; + + /* Same case */ + assert_int_equal(0, ircd_strcmp("hello", "hello")); + assert_int_equal(0, ircd_strcmp("HELLO", "HELLO")); + assert_int_equal(0, ircd_strcmp("", "")); + + /* Different case - should still be equal for IRC */ + assert_int_equal(0, ircd_strcmp("hello", "HELLO")); + assert_int_equal(0, ircd_strcmp("HeLLo", "hElLO")); + assert_int_equal(0, ircd_strcmp("Nick123", "NICK123")); +} + +static void test_ircd_strcmp_not_equal(void **state) +{ + (void)state; + + assert_int_not_equal(0, ircd_strcmp("hello", "world")); + assert_int_not_equal(0, ircd_strcmp("abc", "abcd")); + assert_int_not_equal(0, ircd_strcmp("abcd", "abc")); + assert_int_not_equal(0, ircd_strcmp("", "x")); +} + +static void test_ircd_strcmp_irc_special_chars(void **state) +{ + (void)state; + + /* IRC treats {}|^ as lowercase of []\~ */ + /* These should be considered equal */ + assert_int_equal(0, ircd_strcmp("[", "{")); + assert_int_equal(0, ircd_strcmp("]", "}")); + assert_int_equal(0, ircd_strcmp("\\", "|")); + assert_int_equal(0, ircd_strcmp("~", "^")); + + /* Mixed with regular chars */ + assert_int_equal(0, ircd_strcmp("nick[away]", "nick{away}")); +} + + +/* ========== ircd_strncmp ========== */ + +static void test_ircd_strncmp_basic(void **state) +{ + (void)state; + + /* Compare first n characters */ + assert_int_equal(0, ircd_strncmp("hello", "hello world", 5)); + assert_int_equal(0, ircd_strncmp("HELLO", "hello", 5)); + assert_int_equal(0, ircd_strncmp("abcdef", "abcxyz", 3)); + + assert_int_not_equal(0, ircd_strncmp("abcdef", "abcxyz", 4)); +} + + +/* ========== unique_name_vector ========== */ + +static void test_unique_name_vector_basic(void **state) +{ + (void)state; + char *vector[20]; + char *names; + int count; + + /* Basic comma-separated list */ + names = strdup("a,b,c"); + count = unique_name_vector(names, ',', vector, 20); + assert_int_equal(count, 3); + assert_string_equal(vector[0], "a"); + assert_string_equal(vector[1], "b"); + assert_string_equal(vector[2], "c"); + free(names); +} + +static void test_unique_name_vector_duplicates(void **state) +{ + (void)state; + char *vector[20]; + char *names; + int count; + + /* Duplicates should be removed (case-insensitive) */ + names = strdup("a,b,a,c,B,C"); + count = unique_name_vector(names, ',', vector, 20); + assert_int_equal(count, 3); /* Only a, b, c should remain */ + free(names); +} + +static void test_unique_name_vector_empty_elements(void **state) +{ + (void)state; + char *vector[20]; + char *names; + int count; + + /* Empty elements should be skipped */ + names = strdup(",,,a,,b,,"); + count = unique_name_vector(names, ',', vector, 20); + assert_int_equal(count, 2); + free(names); +} + +static void test_unique_name_vector_single(void **state) +{ + (void)state; + char *vector[20]; + char *names; + int count; + + /* Single element */ + names = strdup("foo"); + count = unique_name_vector(names, ',', vector, 20); + assert_int_equal(count, 1); + assert_string_equal(vector[0], "foo"); + free(names); +} + +static void test_unique_name_vector_empty(void **state) +{ + (void)state; + char *vector[20]; + char *names; + int count; + + /* Empty string */ + names = strdup(""); + count = unique_name_vector(names, ',', vector, 20); + assert_int_equal(count, 0); + free(names); +} + +static void test_unique_name_vector_limit(void **state) +{ + (void)state; + char *vector[5]; + char *names; + int count; + + /* More elements than vector can hold */ + names = strdup("a,b,c,d,e,f,g,h"); + count = unique_name_vector(names, ',', vector, 5); + assert_true(count <= 5); /* Should not exceed limit */ + free(names); +} + + +/* ========== token_vector ========== */ + +static void test_token_vector_basic(void **state) +{ + (void)state; + char *vector[20]; + char *names; + int count; + + /* Unlike unique_name_vector, keeps duplicates and empty elements */ + names = strdup("a,b,c"); + count = token_vector(names, ',', vector, 20); + assert_int_equal(count, 3); + free(names); +} + + +/* ========== EmptyString macro ========== */ + +static void test_EmptyString_macro(void **state) +{ + (void)state; + const char *null_str = NULL; + const char *empty_str = ""; + const char *x_str = "x"; + const char *hello_str = "hello"; + + /* Use variables to avoid macro expansion issues with assert_true */ + assert_int_not_equal(0, EmptyString(null_str)); + assert_int_not_equal(0, EmptyString(empty_str)); + assert_int_equal(0, EmptyString(x_str)); + assert_int_equal(0, EmptyString(hello_str)); +} + + +/* ========== string_has_wildcards ========== */ + +static void test_string_has_wildcards(void **state) +{ + (void)state; + + /* Strings with wildcards */ + assert_true(string_has_wildcards("*")); + assert_true(string_has_wildcards("hello*")); + assert_true(string_has_wildcards("?")); + assert_true(string_has_wildcards("a?b")); + assert_true(string_has_wildcards("*?*")); + + /* Strings without wildcards */ + assert_false(string_has_wildcards("hello")); + assert_false(string_has_wildcards("")); + assert_false(string_has_wildcards("test123")); +} + + +/* ========== ParseInterval ========== */ + +static void test_ParseInterval_seconds(void **state) +{ + (void)state; + + /* Plain numbers are seconds */ + assert_int_equal(60, ParseInterval("60")); + assert_int_equal(3600, ParseInterval("3600")); + assert_int_equal(0, ParseInterval("0")); +} + +static void test_ParseInterval_with_units(void **state) +{ + (void)state; + + /* Minutes */ + assert_int_equal(60, ParseInterval("1m")); + assert_int_equal(300, ParseInterval("5m")); + + /* Hours */ + assert_int_equal(3600, ParseInterval("1h")); + assert_int_equal(7200, ParseInterval("2h")); + + /* Days */ + assert_int_equal(86400, ParseInterval("1d")); + assert_int_equal(172800, ParseInterval("2d")); + + /* Weeks */ + assert_int_equal(604800, ParseInterval("1w")); +} + +static void test_ParseInterval_combined(void **state) +{ + (void)state; + + /* Combined intervals */ + assert_int_equal(3661, ParseInterval("1h1m1")); /* 1 hour + 1 min + 1 sec */ + assert_int_equal(90061, ParseInterval("1d1h1m1")); /* 1 day + 1 hour + 1 min + 1 sec */ +} + + +/* ========== is_timestamp ========== */ + +/* Test is_timestamp - checks if string contains only digits (and dots). + * Note: ircu implementation returns true for empty string because the loop + * "while (IsDigit(*str)) ++str" immediately hits NUL, then "*str == '\0'" + * returns true. This is vacuous truth - callers should check for empty first + * if that matters. This is historical ircu behavior preserved in nefarious. */ +static void test_is_timestamp(void **state) +{ + (void)state; + + /* Valid timestamps (all digits) */ + assert_true(is_timestamp("1234567890")); + assert_true(is_timestamp("0")); + assert_true(is_timestamp("999999999")); + + /* Empty string: ircu returns true (vacuously valid - no invalid chars) */ + assert_true(is_timestamp("")); + + /* Invalid timestamps */ + assert_false(is_timestamp("abc")); + assert_false(is_timestamp("123abc")); + assert_false(is_timestamp("-123")); +} + + +/* ========== valid_username ========== */ + +/* Test valid_username - checks if all chars are valid user ID chars. + * Note: ircu implementation returns true for empty string because the loop + * "for (c = name; *c; c++)" never executes, so it returns 1. This is vacuous + * truth - callers should check for empty first if that matters. + * Compare to valid_hostname() which explicitly rejects empty strings. + * This is historical ircu behavior preserved in nefarious. */ +static void test_valid_username(void **state) +{ + (void)state; + + /* Valid usernames */ + assert_true(valid_username("user")); + assert_true(valid_username("user123")); + assert_true(valid_username("a")); + + /* Empty string: ircu returns true (vacuously valid - no invalid chars) */ + assert_true(valid_username("")); + + /* Invalid usernames */ + assert_false(valid_username("user name")); /* No spaces */ +} + + +/* ========== valid_hostname ========== */ + +static void test_valid_hostname(void **state) +{ + (void)state; + + /* Valid hostnames */ + assert_true(valid_hostname("example.com")); + assert_true(valid_hostname("irc.example.org")); + assert_true(valid_hostname("host-name.domain.tld")); + assert_true(valid_hostname("localhost")); + + /* Invalid hostnames */ + assert_false(valid_hostname("")); + assert_false(valid_hostname("host name.com")); /* No spaces */ +} + + +/* ========== Character classification string functions ========== */ +/* NOTE: strIsDigit/strIsAlpha/strIsAlnum tests removed because strChattr() + * is conditionally compiled with FORCEINLINE and difficult to link in tests. + * The underlying character classification is tested in ircd_chattr_cmocka.c */ + + +int main(void) +{ + const struct CMUnitTest tests[] = { + /* ircd_strncpy */ + cmocka_unit_test(test_ircd_strncpy_normal), + cmocka_unit_test(test_ircd_strncpy_truncation), + cmocka_unit_test(test_ircd_strncpy_empty), + + /* ircd_strcmp */ + cmocka_unit_test(test_ircd_strcmp_equal), + cmocka_unit_test(test_ircd_strcmp_not_equal), + cmocka_unit_test(test_ircd_strcmp_irc_special_chars), + + /* ircd_strncmp */ + cmocka_unit_test(test_ircd_strncmp_basic), + + /* unique_name_vector */ + cmocka_unit_test(test_unique_name_vector_basic), + cmocka_unit_test(test_unique_name_vector_duplicates), + cmocka_unit_test(test_unique_name_vector_empty_elements), + cmocka_unit_test(test_unique_name_vector_single), + cmocka_unit_test(test_unique_name_vector_empty), + cmocka_unit_test(test_unique_name_vector_limit), + + /* token_vector */ + cmocka_unit_test(test_token_vector_basic), + + /* EmptyString */ + cmocka_unit_test(test_EmptyString_macro), + + /* Wildcard detection */ + cmocka_unit_test(test_string_has_wildcards), + + /* Interval parsing */ + cmocka_unit_test(test_ParseInterval_seconds), + cmocka_unit_test(test_ParseInterval_with_units), + cmocka_unit_test(test_ParseInterval_combined), + + /* Timestamp validation */ + cmocka_unit_test(test_is_timestamp), + + /* Username/hostname validation */ + cmocka_unit_test(test_valid_username), + cmocka_unit_test(test_valid_hostname), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/ircd/test/numnicks_cmocka.c b/ircd/test/numnicks_cmocka.c new file mode 100644 index 00000000..b3c3da4c --- /dev/null +++ b/ircd/test/numnicks_cmocka.c @@ -0,0 +1,273 @@ +/* + * numnicks_cmocka.c - CMocka unit tests for numeric nick/base64 functions + * + * Tests the base64 encoding/decoding used for server-to-server communication. + * IRC uses a custom base64 alphabet for encoding client/server numerics. + */ + +#include +#include +#include +#include +#include +#include + +#include "numnicks.h" +#include "res.h" + + +/* ========== base64toint ========== */ + +static void test_base64toint_single_char(void **state) +{ + (void)state; + + /* IRC base64 alphabet: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789[] */ + assert_int_equal(0, base64toint("A")); + assert_int_equal(1, base64toint("B")); + assert_int_equal(25, base64toint("Z")); + assert_int_equal(26, base64toint("a")); + assert_int_equal(51, base64toint("z")); + assert_int_equal(52, base64toint("0")); + assert_int_equal(61, base64toint("9")); + assert_int_equal(62, base64toint("[")); + assert_int_equal(63, base64toint("]")); +} + +static void test_base64toint_multi_char(void **state) +{ + (void)state; + + /* Two character values */ + assert_int_equal(64, base64toint("BA")); /* 1*64 + 0 */ + assert_int_equal(65, base64toint("BB")); /* 1*64 + 1 */ + assert_int_equal(128, base64toint("CA")); /* 2*64 + 0 */ + + /* Three character values (server numerics) */ + assert_int_equal(0, base64toint("AAA")); + assert_int_equal(1, base64toint("AAB")); + assert_int_equal(64, base64toint("ABA")); + assert_int_equal(4095, base64toint("]]")); /* Max 2-char value */ +} + +static void test_base64toint_server_numerics(void **state) +{ + (void)state; + + /* Typical server numeric patterns */ + /* Server numerics are usually 2 chars (YY) */ + unsigned int val; + + val = base64toint("AB"); + assert_int_equal(1, val); + + val = base64toint("Bj"); + assert_true(val > 0 && val < 4096); /* Valid range for 2-char */ +} + + +/* ========== inttobase64 ========== */ + +static void test_inttobase64_single_digit(void **state) +{ + (void)state; + char buf[16]; + + /* Single digit values with count=1 */ + inttobase64(buf, 0, 1); + assert_string_equal(buf, "A"); + + inttobase64(buf, 1, 1); + assert_string_equal(buf, "B"); + + inttobase64(buf, 63, 1); + assert_string_equal(buf, "]"); +} + +static void test_inttobase64_two_digits(void **state) +{ + (void)state; + char buf[16]; + + /* Two digit values */ + inttobase64(buf, 0, 2); + assert_string_equal(buf, "AA"); + + inttobase64(buf, 1, 2); + assert_string_equal(buf, "AB"); + + inttobase64(buf, 64, 2); + assert_string_equal(buf, "BA"); + + inttobase64(buf, 4095, 2); + assert_string_equal(buf, "]]"); /* Max 2-digit */ +} + +static void test_inttobase64_three_digits(void **state) +{ + (void)state; + char buf[16]; + + /* Three digit values (client numerics) */ + inttobase64(buf, 0, 3); + assert_string_equal(buf, "AAA"); + + inttobase64(buf, 1, 3); + assert_string_equal(buf, "AAB"); + + inttobase64(buf, 262143, 3); + assert_string_equal(buf, "]]]"); /* Max 3-digit: 64^3 - 1 */ +} + + +/* ========== Round-trip tests ========== */ + +static void test_base64_roundtrip(void **state) +{ + (void)state; + char buf[16]; + unsigned int original, decoded; + + /* Test various values for round-trip consistency */ + unsigned int test_values[] = { 0, 1, 63, 64, 100, 1000, 4095, 10000, 100000, 262143 }; + + for (size_t i = 0; i < sizeof(test_values)/sizeof(test_values[0]); i++) { + original = test_values[i]; + + /* Determine count based on value */ + int count = (original < 64) ? 1 : (original < 4096) ? 2 : 3; + + inttobase64(buf, original, count); + decoded = base64toint(buf); + + assert_int_equal(original, decoded); + } +} + + +/* ========== iptobase64 / base64toip ========== */ + +static void test_iptobase64_ipv4(void **state) +{ + (void)state; + struct irc_in_addr addr; + char buf[32]; + + /* Create an IPv4-mapped IPv6 address for 127.0.0.1 */ + memset(&addr, 0, sizeof(addr)); + addr.in6_16[5] = htons(0xffff); /* IPv4-mapped prefix */ + addr.in6_16[6] = htons(0x7f00); /* 127.0 */ + addr.in6_16[7] = htons(0x0001); /* 0.1 */ + + /* v4-only encoding (6 chars) */ + iptobase64(buf, &addr, sizeof(buf), 0); + assert_int_equal(strlen(buf), 6); + + /* The result should be decodable */ + struct irc_in_addr decoded; + base64toip(buf, &decoded); + + /* Last 4 bytes (IPv4) should match */ + assert_int_equal(addr.in6_16[6], decoded.in6_16[6]); + assert_int_equal(addr.in6_16[7], decoded.in6_16[7]); +} + +static void test_iptobase64_ipv6(void **state) +{ + (void)state; + struct irc_in_addr addr; + char buf[32]; + + /* Create an IPv6 address ::1 */ + memset(&addr, 0, sizeof(addr)); + addr.in6_16[7] = htons(1); + + /* v6 encoding */ + iptobase64(buf, &addr, sizeof(buf), 1); + + /* Should produce valid base64 output */ + assert_true(strlen(buf) > 0); +} + +static void test_ip_base64_roundtrip(void **state) +{ + (void)state; + struct irc_in_addr original, decoded; + char buf[32]; + + /* Test with 127.0.0.1 (IPv4-mapped) */ + memset(&original, 0, sizeof(original)); + original.in6_16[5] = htons(0xffff); + original.in6_16[6] = htons(0x7f00); + original.in6_16[7] = htons(0x0001); + + iptobase64(buf, &original, sizeof(buf), 0); + base64toip(buf, &decoded); + + /* IPv4 portion should match */ + assert_int_equal(original.in6_16[6], decoded.in6_16[6]); + assert_int_equal(original.in6_16[7], decoded.in6_16[7]); +} + + +/* ========== Edge cases ========== */ + +/* Test empty string handling. + * Note: base64toint() is designed for parsing server numerics from the P10 + * protocol, where empty strings should never occur in practice. The function + * behavior on empty input is undefined - it may return garbage or -1 depending + * on how the convert[] table handles the NUL character. We just verify it + * doesn't crash, as this is an edge case that shouldn't occur in normal use. */ +static void test_base64_empty_string(void **state) +{ + (void)state; + + /* Just verify empty string doesn't crash - result is undefined */ + (void)base64toint(""); +} + +static void test_base64_max_values(void **state) +{ + (void)state; + char buf[16]; + + /* Test maximum values for each digit count */ + inttobase64(buf, 63, 1); + assert_int_equal(63, base64toint(buf)); + + inttobase64(buf, 4095, 2); + assert_int_equal(4095, base64toint(buf)); + + inttobase64(buf, 262143, 3); + assert_int_equal(262143, base64toint(buf)); +} + + +int main(void) +{ + const struct CMUnitTest tests[] = { + /* base64toint */ + cmocka_unit_test(test_base64toint_single_char), + cmocka_unit_test(test_base64toint_multi_char), + cmocka_unit_test(test_base64toint_server_numerics), + + /* inttobase64 */ + cmocka_unit_test(test_inttobase64_single_digit), + cmocka_unit_test(test_inttobase64_two_digits), + cmocka_unit_test(test_inttobase64_three_digits), + + /* Round-trip */ + cmocka_unit_test(test_base64_roundtrip), + + /* IP address encoding */ + cmocka_unit_test(test_iptobase64_ipv4), + cmocka_unit_test(test_iptobase64_ipv6), + cmocka_unit_test(test_ip_base64_roundtrip), + + /* Edge cases */ + cmocka_unit_test(test_base64_empty_string), + cmocka_unit_test(test_base64_max_values), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/ircd/thread_pool.c b/ircd/thread_pool.c new file mode 100644 index 00000000..66b32276 --- /dev/null +++ b/ircd/thread_pool.c @@ -0,0 +1,425 @@ +/* + * IRC - Internet Relay Chat, ircd/thread_pool.c + * Copyright (C) 2025 AfterNET Development Team + * + * 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 2, 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. + */ + +/** + * @file + * @brief Thread pool for CPU-bound operations. + * + * Provides a lightweight thread pool to offload CPU-intensive operations + * (like bcrypt/PBKDF2 password hashing) from the main event loop. Uses + * the self-pipe trick to signal task completion to the main thread. + */ + +#include "config.h" +#include "thread_pool.h" + +#ifdef HAVE_PTHREAD + +#include "ircd_alloc.h" +#include "ircd_log.h" +#include "s_debug.h" + +#include +#include +#include +#include +#include + +/** Internal task structure */ +struct thread_task { + thread_pool_work_func work; /**< Function to run in thread */ + void *arg; /**< Argument for work function */ + thread_pool_callback callback; /**< Callback for main thread */ + void *ctx; /**< Context for callback */ + void *result; /**< Result from work function */ + struct thread_task *next; /**< Next in queue */ +}; + +/** Thread pool state */ +static struct { + pthread_t *workers; /**< Worker thread handles */ + int num_workers; /**< Number of worker threads */ + int running; /**< Pool is accepting work */ + + /* Task queue (pending work) */ + struct thread_task *task_head; /**< Head of task queue */ + struct thread_task *task_tail; /**< Tail of task queue */ + pthread_mutex_t task_mutex; /**< Protects task queue */ + pthread_cond_t task_cond; /**< Signals new work available */ + unsigned int pending_count; /**< Tasks waiting for workers */ + + /* Done queue (completed work) */ + struct thread_task *done_head; /**< Head of done queue */ + struct thread_task *done_tail; /**< Tail of done queue */ + pthread_mutex_t done_mutex; /**< Protects done queue */ + + /* Signal pipe for main thread wakeup */ + int signal_pipe[2]; /**< [0]=read, [1]=write */ + + /* Statistics */ + unsigned long completed_count; /**< Total tasks completed */ + unsigned int active_count; /**< Tasks currently executing */ +} pool; + +/** + * Set a file descriptor to non-blocking mode. + */ +static int set_nonblocking(int fd) +{ + int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) + return -1; + return fcntl(fd, F_SETFL, flags | O_NONBLOCK); +} + +/** + * Worker thread main loop. + * Waits for tasks, executes them, and moves results to done queue. + */ +static void *worker_thread(void *arg) +{ + (void)arg; /* Unused */ + + Debug((DEBUG_INFO, "thread_pool: worker thread started")); + + while (1) { + struct thread_task *task; + + /* Wait for work */ + pthread_mutex_lock(&pool.task_mutex); + while (!pool.task_head && pool.running) { + pthread_cond_wait(&pool.task_cond, &pool.task_mutex); + } + + /* Check for shutdown */ + if (!pool.running && !pool.task_head) { + pthread_mutex_unlock(&pool.task_mutex); + break; + } + + /* Dequeue task */ + task = pool.task_head; + if (task) { + pool.task_head = task->next; + if (!pool.task_head) + pool.task_tail = NULL; + pool.pending_count--; + pool.active_count++; + } + pthread_mutex_unlock(&pool.task_mutex); + + if (!task) + continue; + + /* Execute work function */ + task->result = task->work(task->arg); + + /* Decrement active count */ + pthread_mutex_lock(&pool.task_mutex); + pool.active_count--; + pthread_mutex_unlock(&pool.task_mutex); + + /* Move to done queue */ + task->next = NULL; + pthread_mutex_lock(&pool.done_mutex); + if (pool.done_tail) { + pool.done_tail->next = task; + pool.done_tail = task; + } else { + pool.done_head = pool.done_tail = task; + } + pthread_mutex_unlock(&pool.done_mutex); + + /* Signal main thread */ + { + char c = 1; + int ret; + do { + ret = write(pool.signal_pipe[1], &c, 1); + } while (ret < 0 && errno == EINTR); + } + } + + Debug((DEBUG_INFO, "thread_pool: worker thread exiting")); + return NULL; +} + +/** + * Initialize the thread pool. + */ +int thread_pool_init(int num_threads) +{ + int i; + + if (pool.running) { + log_write(LS_SYSTEM, L_WARNING, 0, "thread_pool: already initialized"); + return -1; + } + + /* Use default if not specified */ + if (num_threads <= 0) + num_threads = THREAD_POOL_SIZE_DEFAULT; + + /* Create signal pipe */ + if (pipe(pool.signal_pipe) < 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "thread_pool: pipe() failed: %s", + strerror(errno)); + return -1; + } + + /* Set non-blocking */ + if (set_nonblocking(pool.signal_pipe[0]) < 0 || + set_nonblocking(pool.signal_pipe[1]) < 0) { + log_write(LS_SYSTEM, L_ERROR, 0, "thread_pool: fcntl() failed: %s", + strerror(errno)); + close(pool.signal_pipe[0]); + close(pool.signal_pipe[1]); + return -1; + } + + /* Initialize mutexes and condition */ + pthread_mutex_init(&pool.task_mutex, NULL); + pthread_mutex_init(&pool.done_mutex, NULL); + pthread_cond_init(&pool.task_cond, NULL); + + /* Initialize queues */ + pool.task_head = pool.task_tail = NULL; + pool.done_head = pool.done_tail = NULL; + pool.pending_count = 0; + pool.active_count = 0; + pool.completed_count = 0; + pool.running = 1; + + /* Allocate worker array */ + pool.workers = (pthread_t *)MyCalloc(num_threads, sizeof(pthread_t)); + pool.num_workers = num_threads; + + /* Create worker threads */ + for (i = 0; i < num_threads; i++) { + if (pthread_create(&pool.workers[i], NULL, worker_thread, NULL) != 0) { + log_write(LS_SYSTEM, L_ERROR, 0, + "thread_pool: pthread_create() failed: %s", strerror(errno)); + /* Shut down threads we did create */ + pool.running = 0; + pthread_cond_broadcast(&pool.task_cond); + while (--i >= 0) { + pthread_join(pool.workers[i], NULL); + } + MyFree(pool.workers); + pthread_mutex_destroy(&pool.task_mutex); + pthread_mutex_destroy(&pool.done_mutex); + pthread_cond_destroy(&pool.task_cond); + close(pool.signal_pipe[0]); + close(pool.signal_pipe[1]); + return -1; + } + } + + log_write(LS_SYSTEM, L_INFO, 0, "thread_pool: initialized with %d workers", + num_threads); + return 0; +} + +/** + * Shut down the thread pool. + */ +void thread_pool_shutdown(void) +{ + int i; + struct thread_task *task; + + if (!pool.running) + return; + + log_write(LS_SYSTEM, L_INFO, 0, "thread_pool: shutting down"); + + /* Signal workers to exit */ + pthread_mutex_lock(&pool.task_mutex); + pool.running = 0; + pthread_cond_broadcast(&pool.task_cond); + pthread_mutex_unlock(&pool.task_mutex); + + /* Wait for workers */ + for (i = 0; i < pool.num_workers; i++) { + pthread_join(pool.workers[i], NULL); + } + + /* Process any remaining completed tasks */ + thread_pool_poll(); + + /* Free pending tasks (shouldn't be any, but just in case) */ + pthread_mutex_lock(&pool.task_mutex); + while ((task = pool.task_head)) { + pool.task_head = task->next; + /* Call callback with NULL result to indicate cancellation */ + if (task->callback) + task->callback(NULL, task->ctx); + MyFree(task); + } + pthread_mutex_unlock(&pool.task_mutex); + + /* Cleanup */ + MyFree(pool.workers); + pool.workers = NULL; + pool.num_workers = 0; + + pthread_mutex_destroy(&pool.task_mutex); + pthread_mutex_destroy(&pool.done_mutex); + pthread_cond_destroy(&pool.task_cond); + + close(pool.signal_pipe[0]); + close(pool.signal_pipe[1]); + pool.signal_pipe[0] = pool.signal_pipe[1] = -1; + + log_write(LS_SYSTEM, L_INFO, 0, "thread_pool: shutdown complete (%lu tasks)", + pool.completed_count); +} + +/** + * Submit work to the thread pool. + */ +int thread_pool_submit(thread_pool_work_func work, void *arg, + thread_pool_callback callback, void *ctx) +{ + struct thread_task *task; + + if (!pool.running) { + Debug((DEBUG_ERROR, "thread_pool_submit: pool not running")); + return -1; + } + + if (!work) { + Debug((DEBUG_ERROR, "thread_pool_submit: NULL work function")); + return -1; + } + + /* Check queue limit */ + pthread_mutex_lock(&pool.task_mutex); + if (pool.pending_count >= THREAD_POOL_MAX_PENDING) { + pthread_mutex_unlock(&pool.task_mutex); + log_write(LS_SYSTEM, L_WARNING, 0, + "thread_pool: queue full (%u pending)", pool.pending_count); + return -1; + } + pthread_mutex_unlock(&pool.task_mutex); + + /* Allocate task */ + task = (struct thread_task *)MyMalloc(sizeof(struct thread_task)); + task->work = work; + task->arg = arg; + task->callback = callback; + task->ctx = ctx; + task->result = NULL; + task->next = NULL; + + /* Enqueue task */ + pthread_mutex_lock(&pool.task_mutex); + if (pool.task_tail) { + pool.task_tail->next = task; + pool.task_tail = task; + } else { + pool.task_head = pool.task_tail = task; + } + pool.pending_count++; + pthread_cond_signal(&pool.task_cond); + pthread_mutex_unlock(&pool.task_mutex); + + Debug((DEBUG_DEBUG, "thread_pool_submit: queued task (pending=%u)", + pool.pending_count)); + return 0; +} + +/** + * Process completed tasks. + */ +void thread_pool_poll(void) +{ + struct thread_task *task; + char buf[64]; + int count = 0; + + if (!pool.running && pool.signal_pipe[0] < 0) + return; + + /* Drain signal pipe */ + while (read(pool.signal_pipe[0], buf, sizeof(buf)) > 0) + ; + + /* Process done queue */ + while (1) { + pthread_mutex_lock(&pool.done_mutex); + task = pool.done_head; + if (task) { + pool.done_head = task->next; + if (!pool.done_head) + pool.done_tail = NULL; + pool.completed_count++; + } + pthread_mutex_unlock(&pool.done_mutex); + + if (!task) + break; + + /* Invoke callback in main thread context */ + if (task->callback) + task->callback(task->result, task->ctx); + + MyFree(task); + count++; + } + + if (count > 0) { + Debug((DEBUG_DEBUG, "thread_pool_poll: processed %d tasks", count)); + } +} + +/** + * Get the signal pipe file descriptor. + */ +int thread_pool_get_signal_fd(void) +{ + return pool.running ? pool.signal_pipe[0] : -1; +} + +/** + * Check if thread pool is running. + */ +int thread_pool_is_running(void) +{ + return pool.running; +} + +/** + * Get thread pool statistics. + */ +void thread_pool_stats(unsigned int *pending, unsigned long *completed, + unsigned int *active) +{ + pthread_mutex_lock(&pool.task_mutex); + if (pending) + *pending = pool.pending_count; + if (active) + *active = pool.active_count; + pthread_mutex_unlock(&pool.task_mutex); + + if (completed) + *completed = pool.completed_count; +} + +#endif /* HAVE_PTHREAD */ diff --git a/ircd/umkpasswd.c b/ircd/umkpasswd.c index 48f8c429..4b4c0add 100644 --- a/ircd/umkpasswd.c +++ b/ircd/umkpasswd.c @@ -44,6 +44,7 @@ #include "ircd_crypt_native.h" #include "ircd_crypt_plain.h" #include "ircd_crypt_bcrypt.h" +#include "ircd_crypt_pbkdf2.h" /* bleah, evil globals */ umkpasswd_conf_t* umkpasswd_conf; @@ -281,6 +282,8 @@ void load_mechs(void) ircd_register_crypt_smd5(); ircd_register_crypt_plain(); /* yes I know it's slightly pointless */ ircd_register_crypt_bcrypt(); + ircd_register_crypt_pbkdf2(); + ircd_register_crypt_pbkdf2_sha512(); return; } diff --git a/ircd/websocket.c b/ircd/websocket.c new file mode 100644 index 00000000..00cfa05c --- /dev/null +++ b/ircd/websocket.c @@ -0,0 +1,668 @@ +/* + * IRC - Internet Relay Chat, ircd/websocket.c + * Copyright (C) 2024 Nefarious Development Team + * + * 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. + */ +/** @file + * @brief WebSocket protocol support (RFC 6455). + * + * Implements IRCv3 WebSocket extension for browser-based IRC clients. + * Supports both binary.ircv3.net and text.ircv3.net subprotocols. + */ +#include "config.h" + +#include "websocket.h" +#include "client.h" +#include "ircd.h" +#include "ircd_alloc.h" +#include "ircd_features.h" +#include "ircd_log.h" +#include "ircd_osdep.h" +#include "ircd_snprintf.h" +#include "ircd_string.h" +#include "listener.h" +#include "s_bsd.h" +#include "s_debug.h" +#include "send.h" +#include "ssl.h" + +#include +#include + +#ifdef USE_SSL +#include +#include +#include +#endif + +/* WebSocket magic GUID for handshake (RFC 6455) */ +#define WEBSOCKET_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + +/* WebSocket opcodes */ +#define WS_OPCODE_CONTINUATION 0x0 +#define WS_OPCODE_TEXT 0x1 +#define WS_OPCODE_BINARY 0x2 +#define WS_OPCODE_CLOSE 0x8 +#define WS_OPCODE_PING 0x9 +#define WS_OPCODE_PONG 0xA + +/* WebSocket frame flags */ +#define WS_FIN 0x80 +#define WS_MASK 0x80 + +/* Maximum WebSocket frame payload we'll accept */ +#define WS_MAX_PAYLOAD 16384 + +/* Subprotocol types */ +#define WS_SUBPROTO_NONE 0 /**< No subprotocol requested by client */ +#define WS_SUBPROTO_BINARY 1 +#define WS_SUBPROTO_TEXT 2 + +#ifdef USE_SSL +/** Base64 encode data using OpenSSL EVP. + * @param[in] input Input data to encode. + * @param[in] length Length of input data. + * @param[out] output Output buffer (must be at least (length*4/3)+4 bytes). + * @return Length of encoded data. + */ +static int base64_encode(const unsigned char *input, int length, char *output) +{ + int outlen; + EVP_EncodeBlock((unsigned char *)output, input, length); + outlen = ((length + 2) / 3) * 4; + output[outlen] = '\0'; + return outlen; +} + +/** Compute WebSocket accept key from client key. + * @param[in] client_key The Sec-WebSocket-Key from client. + * @param[out] accept_key Output buffer (at least 29 bytes). + * @return 1 on success, 0 on failure. + */ +static int compute_accept_key(const char *client_key, char *accept_key) +{ + char combined[128]; + unsigned char hash[SHA_DIGEST_LENGTH]; + + if (!client_key || strlen(client_key) < 16) + return 0; + + /* Concatenate client key with magic GUID */ + ircd_snprintf(0, combined, sizeof(combined), "%s%s", client_key, WEBSOCKET_GUID); + + /* SHA1 hash */ + SHA1((unsigned char *)combined, strlen(combined), hash); + + /* Base64 encode */ + base64_encode(hash, SHA_DIGEST_LENGTH, accept_key); + + return 1; +} +#endif /* USE_SSL */ + +/** Parse HTTP headers from WebSocket handshake request. + * @param[in] buffer Raw HTTP request data. + * @param[in] length Length of buffer. + * @param[out] ws_key Output buffer for Sec-WebSocket-Key (at least 64 bytes). + * @param[out] subproto Output for selected subprotocol (WS_SUBPROTO_*). + * @param[out] origin Output buffer for Origin header (at least 256 bytes). + * @return 1 if valid WebSocket upgrade request, 0 otherwise. + */ +static int parse_ws_handshake(const char *buffer, int length, + char *ws_key, int *subproto, char *origin) +{ + const char *line, *end; + const char *key_start; + int found_upgrade = 0; + int found_connection = 0; + int found_key = 0; + int found_version = 0; + + *subproto = WS_SUBPROTO_NONE; /* RFC 6455 §4.2.2: Don't assume subprotocol unless client requests */ + ws_key[0] = '\0'; + origin[0] = '\0'; + + /* Check for GET request */ + if (length < 4 || strncmp(buffer, "GET ", 4) != 0) + return 0; + + /* Parse headers line by line */ + line = buffer; + while (line < buffer + length) { + end = strstr(line, "\r\n"); + if (!end) + break; + + /* Check for Upgrade: websocket */ + if (strncasecmp(line, "Upgrade:", 8) == 0) { + if (strstr(line, "websocket") || strstr(line, "WebSocket")) + found_upgrade = 1; + } + /* Check for Connection: Upgrade */ + else if (strncasecmp(line, "Connection:", 11) == 0) { + if (strstr(line, "Upgrade") || strstr(line, "upgrade")) + found_connection = 1; + } + /* Get Sec-WebSocket-Key */ + else if (strncasecmp(line, "Sec-WebSocket-Key:", 18) == 0) { + key_start = line + 18; + while (*key_start == ' ' && key_start < end) + key_start++; + if (key_start < end) { + size_t keylen = end - key_start; + if (keylen > 63) keylen = 63; + memcpy(ws_key, key_start, keylen); + ws_key[keylen] = '\0'; + /* Trim trailing spaces */ + while (keylen > 0 && ws_key[keylen-1] == ' ') + ws_key[--keylen] = '\0'; + found_key = 1; + } + } + /* Check Sec-WebSocket-Version */ + else if (strncasecmp(line, "Sec-WebSocket-Version:", 22) == 0) { + if (strstr(line, "13")) + found_version = 1; + } + /* Check Sec-WebSocket-Protocol for subprotocol preference */ + else if (strncasecmp(line, "Sec-WebSocket-Protocol:", 23) == 0) { + /* Prefer text.ircv3.net if client requests it */ + if (strstr(line, "text.ircv3.net")) + *subproto = WS_SUBPROTO_TEXT; + else if (strstr(line, "binary.ircv3.net")) + *subproto = WS_SUBPROTO_BINARY; + } + /* Get Origin header for validation */ + else if (strncasecmp(line, "Origin:", 7) == 0) { + key_start = line + 7; + while (*key_start == ' ' && key_start < end) + key_start++; + if (key_start < end) { + size_t origlen = end - key_start; + if (origlen > 255) origlen = 255; + memcpy(origin, key_start, origlen); + origin[origlen] = '\0'; + /* Trim trailing spaces */ + while (origlen > 0 && origin[origlen-1] == ' ') + origin[--origlen] = '\0'; + } + } + + line = end + 2; + + /* Check for end of headers */ + if (line[0] == '\r' && line[1] == '\n') + break; + } + + return (found_upgrade && found_connection && found_key && found_version); +} + +/** Build WebSocket handshake response. + * @param[in] accept_key The computed Sec-WebSocket-Accept value. + * @param[in] subproto The selected subprotocol (WS_SUBPROTO_*). + * @param[out] response Output buffer (at least 256 bytes). + * @return Length of response. + * + * RFC 6455 §4.2.2: If the server does not wish to agree to one of the + * suggested subprotocols, it MUST NOT send back a Sec-WebSocket-Protocol + * header field in its response. + */ +static int build_ws_response(const char *accept_key, int subproto, char *response) +{ + /* RFC 6455 §4.2.2: Only include Sec-WebSocket-Protocol if client requested one */ + if (subproto == WS_SUBPROTO_NONE) { + return ircd_snprintf(0, response, 256, + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: %s\r\n" + "\r\n", + accept_key); + } + + return ircd_snprintf(0, response, 256, + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: %s\r\n" + "Sec-WebSocket-Protocol: %s\r\n" + "\r\n", + accept_key, + (subproto == WS_SUBPROTO_TEXT) ? "text.ircv3.net" : "binary.ircv3.net"); +} + +/** Validate WebSocket origin against allowed origins. + * @param[in] origin The Origin header value from client. + * @return 1 if origin is allowed, 0 if rejected. + * + * If WEBSOCKET_ORIGIN feature is empty, all origins are allowed. + * Otherwise, the origin must match one of the space-separated patterns. + * Patterns support '*' as a wildcard prefix (e.g., "*.example.com"). + */ +static int validate_ws_origin(const char *origin) +{ + const char *allowed = feature_str(FEAT_WEBSOCKET_ORIGIN); + const char *p, *end; + char pattern[256]; + size_t plen, olen; + + /* Empty allowed list or "*" = allow all origins (including missing Origin header) */ + if (!allowed || !*allowed || (allowed[0] == '*' && allowed[1] == '\0')) + return 1; + + /* No origin header = reject if origin validation is configured */ + if (!origin || !*origin) { + Debug((DEBUG_DEBUG, "WebSocket: No Origin header, rejecting (origin validation enabled)")); + return 0; + } + + olen = strlen(origin); + + /* Check each space-separated pattern */ + p = allowed; + while (*p) { + /* Skip whitespace */ + while (*p == ' ' || *p == ',') + p++; + if (!*p) + break; + + /* Find end of pattern */ + end = p; + while (*end && *end != ' ' && *end != ',') + end++; + + plen = end - p; + if (plen >= sizeof(pattern)) + plen = sizeof(pattern) - 1; + memcpy(pattern, p, plen); + pattern[plen] = '\0'; + + /* Check for wildcard prefix match (*.example.com) */ + if (pattern[0] == '*' && pattern[1] == '.') { + /* Match suffix - origin must end with pattern (minus the *) */ + size_t suffix_len = plen - 1; /* Length of ".example.com" */ + if (olen >= suffix_len) { + if (strcasecmp(origin + olen - suffix_len, pattern + 1) == 0) { + Debug((DEBUG_DEBUG, "WebSocket: Origin %s matches wildcard %s", origin, pattern)); + return 1; + } + } + } else { + /* Exact match */ + if (strcasecmp(origin, pattern) == 0) { + Debug((DEBUG_DEBUG, "WebSocket: Origin %s matches exactly", origin)); + return 1; + } + } + + p = end; + } + + Debug((DEBUG_DEBUG, "WebSocket: Origin %s not in allowed list", origin)); + return 0; +} + +/** Build HTTP 403 Forbidden response for invalid origin. + * @param[out] response Output buffer (at least 128 bytes). + * @return Length of response. + */ +static int build_ws_forbidden_response(char *response) +{ + return ircd_snprintf(0, response, 128, + "HTTP/1.1 403 Forbidden\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: 16\r\n" + "Connection: close\r\n" + "\r\n" + "Origin forbidden"); +} + +/** Handle WebSocket handshake for a new connection. + * @param[in] cptr Client attempting to connect. + * @param[in] buffer Raw data received. + * @param[in] length Length of data. + * @return 1 if handshake completed successfully, 0 if more data needed, -1 on error. + */ +int websocket_handshake(struct Client *cptr, const char *buffer, int length) +{ +#ifdef USE_SSL + char ws_key[64]; + char accept_key[64]; + char response[256]; + char origin[256]; + int subproto; + int resp_len; + + /* Check for complete HTTP request (ends with \r\n\r\n) */ + if (length < 4 || !strstr(buffer, "\r\n\r\n")) + return 0; /* Need more data */ + + /* Parse the handshake request */ + if (!parse_ws_handshake(buffer, length, ws_key, &subproto, origin)) { + Debug((DEBUG_DEBUG, "WebSocket: Invalid handshake from %s", + cli_sockhost(cptr))); + return -1; + } + + /* Validate origin if configured */ + if (!validate_ws_origin(origin)) { + Debug((DEBUG_DEBUG, "WebSocket: Origin '%s' rejected for %s", + origin[0] ? origin : "(none)", cli_sockhost(cptr))); + /* Send 403 response before closing */ + resp_len = build_ws_forbidden_response(response); + if (cli_socket(cptr).ssl) { + SSL_write(cli_socket(cptr).ssl, response, resp_len); + } else { + unsigned int bytes_sent; + os_send_nonb(cli_fd(cptr), response, resp_len, &bytes_sent); + } + return -1; + } + + /* Compute accept key */ + if (!compute_accept_key(ws_key, accept_key)) { + Debug((DEBUG_DEBUG, "WebSocket: Failed to compute accept key for %s", + cli_sockhost(cptr))); + return -1; + } + + /* Build and send response */ + resp_len = build_ws_response(accept_key, subproto, response); + + /* Send handshake response directly */ + /* Note: We bypass the normal send queue for the handshake */ + /* Use SSL_write for SSL connections, os_send_nonb for plain */ + if (cli_socket(cptr).ssl) { + int send_result = SSL_write(cli_socket(cptr).ssl, response, resp_len); + if (send_result <= 0) { + Debug((DEBUG_DEBUG, "WebSocket: Failed to send handshake response to %s (SSL_write returned %d)", + cli_sockhost(cptr), send_result)); + return -1; + } + } else { + unsigned int bytes_sent; + if (os_send_nonb(cli_fd(cptr), response, resp_len, &bytes_sent) != IO_SUCCESS) { + Debug((DEBUG_DEBUG, "WebSocket: Failed to send handshake response to %s", + cli_sockhost(cptr))); + return -1; + } + } + + /* Mark client as WebSocket and clear handshake flag */ + SetWebSocket(cptr); + ClearWSNeedHandshake(cptr); + + /* Apply WebSocket-specific recvq limit (typically higher than regular clients + * because WebSocket frames can bundle multiple IRC lines) */ + { + unsigned int ws_recvq = feature_int(FEAT_WEBSOCKET_RECVQ); + if (ws_recvq > 0) + cli_max_recvq(cptr) = ws_recvq; + } + + Debug((DEBUG_DEBUG, "WebSocket: Handshake complete for %s (subproto=%s)", + cli_sockhost(cptr), + subproto == WS_SUBPROTO_NONE ? "none" : + subproto == WS_SUBPROTO_TEXT ? "text" : "binary")); + + return 1; +#else + /* WebSocket requires SSL for SHA1/Base64 */ + return -1; +#endif +} + +/** Decode a WebSocket frame and extract the payload. + * @param[in] frame Raw WebSocket frame data. + * @param[in] frame_len Length of frame data. + * @param[out] payload Output buffer for decoded payload. + * @param[in] payload_size Size of payload buffer. + * @param[out] payload_len Length of decoded payload. + * @param[out] opcode The frame opcode. + * @param[out] is_fin Set to 1 if FIN bit is set (final fragment), 0 otherwise. + * @return Number of bytes consumed from frame, 0 if incomplete, -1 on error. + * + * RFC 6455 Compliance: + * - §5.2: RSV1-3 bits MUST be 0 unless extension negotiated (we negotiate none) + * - §5.1: Client-to-server frames MUST be masked + * - §5.2: Reserved opcodes (0x03-0x07, 0x0B-0x0F) MUST cause connection failure + * - §5.5: Control frames MUST have payload length <= 125 bytes + */ +int websocket_decode_frame(const unsigned char *frame, int frame_len, + char *payload, int payload_size, + int *payload_len, int *opcode, int *is_fin) +{ + int pos = 0; + int masked; + int fin; + int rsv; + int is_control; + unsigned long long plen; + unsigned char mask[4]; + int i; + + *payload_len = 0; + *opcode = 0; + *is_fin = 0; + + /* Need at least 2 bytes for header */ + if (frame_len < 2) + return 0; + + /* Parse first byte: FIN + RSV1-3 + opcode */ + fin = (frame[0] & WS_FIN) ? 1 : 0; + rsv = (frame[0] >> 4) & 0x07; /* RSV1-3 are bits 6-4 */ + *opcode = frame[0] & 0x0F; + + /* RFC 6455 §5.2: RSV bits MUST be 0 unless extension negotiated */ + if (rsv != 0) { + Debug((DEBUG_DEBUG, "WebSocket: RSV bits set (0x%x) without extension - protocol error", rsv)); + return -1; + } + + /* RFC 6455 §5.2: Reserved opcodes MUST cause connection failure */ + /* Data frames: 0x0-0x2 valid, 0x3-0x7 reserved */ + /* Control frames: 0x8-0xA valid, 0xB-0xF reserved */ + if ((*opcode >= 0x03 && *opcode <= 0x07) || (*opcode >= 0x0B && *opcode <= 0x0F)) { + Debug((DEBUG_DEBUG, "WebSocket: Reserved opcode 0x%x - protocol error", *opcode)); + return -1; + } + + is_control = (*opcode >= 0x08); + + /* Parse second byte: MASK + payload length */ + masked = (frame[1] & WS_MASK) ? 1 : 0; + plen = frame[1] & 0x7F; + pos = 2; + + /* RFC 6455 §5.1: Client-to-server frames MUST be masked */ + if (!masked) { + Debug((DEBUG_DEBUG, "WebSocket: Client frame not masked - protocol error")); + return -1; + } + + /* Extended payload length */ + if (plen == 126) { + if (frame_len < 4) + return 0; + plen = ((unsigned long long)frame[2] << 8) | frame[3]; + pos = 4; + } else if (plen == 127) { + if (frame_len < 10) + return 0; + plen = 0; + for (i = 0; i < 8; i++) + plen = (plen << 8) | frame[2 + i]; + pos = 10; + } + + /* RFC 6455 §5.5: Control frames MUST have payload length <= 125 bytes */ + if (is_control && plen > 125) { + Debug((DEBUG_DEBUG, "WebSocket: Control frame payload too large (%llu > 125) - protocol error", plen)); + return -1; + } + + /* Sanity check payload length */ + if (plen > WS_MAX_PAYLOAD) { + Debug((DEBUG_DEBUG, "WebSocket: Frame too large: %llu bytes", plen)); + return -1; + } + + /* Get mask (required for client-to-server frames - we already verified masked=1 above) */ + if (frame_len < pos + 4) + return 0; + memcpy(mask, frame + pos, 4); + pos += 4; + + /* Check if we have complete payload */ + if (frame_len < pos + (int)plen) + return 0; + + /* Sanity check output buffer */ + if ((int)plen >= payload_size) { + Debug((DEBUG_DEBUG, "WebSocket: Payload too large for buffer")); + return -1; + } + + /* Copy and unmask payload */ + for (i = 0; i < (int)plen; i++) { + payload[i] = frame[pos + i] ^ mask[i % 4]; + } + payload[plen] = '\0'; + *payload_len = (int)plen; + *is_fin = fin; + + return pos + (int)plen; +} + +/** Encode data as a WebSocket frame. + * @param[in] data Data to encode. + * @param[in] data_len Length of data. + * @param[out] frame Output buffer for frame (must be data_len + 10 bytes). + * @param[in] text_mode 1 for text frame, 0 for binary frame. + * @return Length of encoded frame. + */ +int websocket_encode_frame(const char *data, int data_len, + unsigned char *frame, int text_mode) +{ + int pos = 0; + int opcode = text_mode ? WS_OPCODE_TEXT : WS_OPCODE_BINARY; + + /* First byte: FIN + opcode */ + frame[pos++] = WS_FIN | opcode; + + /* Second byte: payload length (no mask for server-to-client) */ + if (data_len < 126) { + frame[pos++] = (unsigned char)data_len; + } else if (data_len < 65536) { + frame[pos++] = 126; + frame[pos++] = (data_len >> 8) & 0xFF; + frame[pos++] = data_len & 0xFF; + } else { + frame[pos++] = 127; + /* 64-bit length - IRC messages are never this big, but for completeness */ + frame[pos++] = 0; + frame[pos++] = 0; + frame[pos++] = 0; + frame[pos++] = 0; + frame[pos++] = (data_len >> 24) & 0xFF; + frame[pos++] = (data_len >> 16) & 0xFF; + frame[pos++] = (data_len >> 8) & 0xFF; + frame[pos++] = data_len & 0xFF; + } + + /* Copy payload (no masking for server-to-client) */ + memcpy(frame + pos, data, data_len); + pos += data_len; + + return pos; +} + +/** Handle a WebSocket control frame. + * @param[in] cptr Client connection. + * @param[in] opcode Frame opcode. + * @param[in] payload Frame payload. + * @param[in] payload_len Payload length. + * @return 1 to continue, 0 to close connection. + */ +/** Helper to send raw WebSocket frame data (SSL-aware). + * @param[in] cptr Client connection. + * @param[in] data Data to send. + * @param[in] len Length of data. + * @return 1 on success, 0 on failure. + */ +static int ws_send_raw(struct Client *cptr, const unsigned char *data, int len) +{ +#ifdef USE_SSL + if (cli_socket(cptr).ssl) { + int result = SSL_write(cli_socket(cptr).ssl, data, len); + return (result > 0) ? 1 : 0; + } +#endif + { + unsigned int bytes_sent; + return (os_send_nonb(cli_fd(cptr), (char *)data, len, &bytes_sent) == IO_SUCCESS) ? 1 : 0; + } +} + +int websocket_handle_control(struct Client *cptr, int opcode, + const char *payload, int payload_len) +{ + unsigned char response[256]; + int resp_len; + + switch (opcode) { + case WS_OPCODE_PING: + /* Respond with PONG */ + response[0] = WS_FIN | WS_OPCODE_PONG; + if (payload_len < 126) { + response[1] = (unsigned char)payload_len; + memcpy(response + 2, payload, payload_len); + resp_len = 2 + payload_len; + } else { + /* Ping payload too large, just send empty pong */ + response[1] = 0; + resp_len = 2; + } + ws_send_raw(cptr, response, resp_len); + return 1; + + case WS_OPCODE_PONG: + /* Client responding to our ping, nothing to do */ + return 1; + + case WS_OPCODE_CLOSE: + /* Client initiated close - send close response */ + response[0] = WS_FIN | WS_OPCODE_CLOSE; + if (payload_len >= 2) { + /* Echo back the status code */ + response[1] = 2; + response[2] = payload[0]; + response[3] = payload[1]; + resp_len = 4; + } else { + response[1] = 0; + resp_len = 2; + } + ws_send_raw(cptr, response, resp_len); + return 0; /* Signal to close connection */ + + default: + return 1; + } +} diff --git a/tools/docker/base.conf-dist b/tools/docker/base.conf-dist index 1037795d..e81c2ffb 100644 --- a/tools/docker/base.conf-dist +++ b/tools/docker/base.conf-dist @@ -7,6 +7,17 @@ General { Features { "LOG" = "SYSTEM" "FILE" "ircd.log"; "LOG" = "SYSTEM" "LEVEL" "CRIT"; + + # Enable draft IRCv3 capabilities + "CAP_draft_chathistory" = "TRUE"; + "CAP_draft_metadata_2" = "TRUE"; + + # PM Chathistory consent settings + "CHATHISTORY_PRIVATE" = "TRUE"; + # Consent mode: 0=global (opt-out), 1=single-party (either opts in), 2=multi-party (both must opt in) + "CHATHISTORY_PRIVATE_CONSENT" = "2"; + # Send policy notice on connect + "CHATHISTORY_PM_NOTICE" = "TRUE"; }; Admin { @@ -35,6 +46,8 @@ Class { pingfreq = 1 minutes 30 seconds; maxlinks = 100; sendq = 160000; + fakelagminimum = 0; + fakelagfactor = 0; }; Class { @@ -70,6 +83,8 @@ Class { maxlinks = 4000; sendq = 160000; usermode = "x"; + fakelagminimum = 0; + fakelagfactor = 0; }; # This is an "I line" block. This one lets everyone in. @@ -108,6 +123,12 @@ Port { ssl = yes; }; +# WebSocket port for browser-based clients +Port { + port = 8443; + ssl = yes; + websocket = yes; +}; # Do NOT remove the following lines; linesync.sh depends on them! # BEGIN LINESYNC diff --git a/tools/docker/dockerentrypoint.sh b/tools/docker/dockerentrypoint.sh index 8f584b15..3126e656 100755 --- a/tools/docker/dockerentrypoint.sh +++ b/tools/docker/dockerentrypoint.sh @@ -3,6 +3,7 @@ # Nefarious IRCd Docker Entrypoint # Reads base.conf-dist, replaces all %VARIABLE% placeholders with environment # variable values, and writes out base.conf +# Volume permissions are handled by init container in docker-compose BASECONFDIST=/home/nefarious/ircd/base.conf-dist BASECONF=/home/nefarious/ircd/base.conf @@ -52,7 +53,6 @@ if [ "$1" == "/home/nefarious/bin/ircd" ]; then fi fi -#Now run CMD from Dockerfile... - +# Run CMD from Dockerfile exec "$@" diff --git a/tools/docker/ircd.conf b/tools/docker/ircd.conf index e69de29b..cff895b3 100644 --- a/tools/docker/ircd.conf +++ b/tools/docker/ircd.conf @@ -0,0 +1,5 @@ +include "base.conf"; + +include "local.conf"; + +include "linesync.conf";