diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..83cd31d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,40 @@ +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Gcov files +*.gcno +*.gcov +*.gcda + +# Python files +*.pyc +*.pyo + +# Build directories +build/ + +# Clion directory +.idea/ \ No newline at end of file diff --git a/.woodpecker/containers-packaging.yaml b/.woodpecker/containers-packaging.yaml new file mode 100644 index 0000000..4cbb7e7 --- /dev/null +++ b/.woodpecker/containers-packaging.yaml @@ -0,0 +1,25 @@ +when: + event: [ push ] + branch: master + path: + include: ["docker/packaging/**/Dockerfile", ".woodpecker/containers-packaging.yaml"] + +labels: + platform: linux/amd64 + +matrix: + PACKAGING_IMAGE: + - archlinux + - debian + +steps: + biboumi-package-image: + image: woodpeckerci/plugin-docker-buildx + settings: + repo: codeberg.org/biboumi/ci-package-${PACKAGING_IMAGE} + registry: codeberg.org + dockerfile: docker/packaging/${PACKAGING_IMAGE}/Dockerfile + tag: latest + username: bibou + password: + from_secret: codeberg_token diff --git a/.woodpecker/containers-prod.yaml b/.woodpecker/containers-prod.yaml new file mode 100644 index 0000000..550e530 --- /dev/null +++ b/.woodpecker/containers-prod.yaml @@ -0,0 +1,22 @@ +when: + event: [ push ] + branch: master + +labels: + platform: linux/amd64 + +matrix: + PROD_IMAGE: + - alpine + +steps: + biboumi-prod-image: + image: woodpeckerci/plugin-docker-buildx + settings: + repo: codeberg.org/biboumi/biboumi + registry: codeberg.org + dockerfile: docker/biboumi/${PROD_IMAGE}/Dockerfile + tag: ${PROD_IMAGE} + username: bibou + password: + from_secret: codeberg_token diff --git a/.woodpecker/containers-test.yaml b/.woodpecker/containers-test.yaml new file mode 100644 index 0000000..f7aeb8b --- /dev/null +++ b/.woodpecker/containers-test.yaml @@ -0,0 +1,26 @@ +when: + event: [ push ] + branch: master + path: + include: ["docker/test/**/Dockerfile", ".woodpecker/containers-test.yaml"] + +labels: + platform: linux/amd64 + +matrix: + TEST_IMAGE: + - alpine + - debian + - fedora + +steps: + biboumi-test-image: + image: woodpeckerci/plugin-docker-buildx + settings: + repo: codeberg.org/biboumi/ci-test-${TEST_IMAGE} + registry: codeberg.org + dockerfile: docker/test/${TEST_IMAGE}/Dockerfile + tag: latest + username: bibou + password: + from_secret: codeberg_token diff --git a/.woodpecker/docs.yaml b/.woodpecker/docs.yaml new file mode 100644 index 0000000..1c0000f --- /dev/null +++ b/.woodpecker/docs.yaml @@ -0,0 +1,51 @@ +# Exclude page pipeline to be run on "pages" branch +when: + path: + include: ["CMakeLists.txt", ".woodpecker/docs.yaml", "doc/**/*"] + event: [pull_request, push] + +labels: + platform: linux/amd64 + +clone: + - name: git + image: woodpeckerci/plugin-git + settings: + partial: false + depth: 1 + +steps: + # Build sphinx static files + build: + image: docker.io/alpine:3.21 + commands: + - apk add --no-cache py3-sphinx py3-sphinx_rtd_theme make + - cd doc + - make html + when: + event: [pull_request, push] + + publish: + image: docker.io/bitnami/git + environment: + # secrets must be set in Woodpecker configuration + CODEBERG_TOKEN: + from_secret: codeberg_token + commands: + # Git configuration + - git config --global user.email "ci-test@example.com" + - git config --global user.name "CI" + - git clone https://$CODEBERG_TOKEN@codeberg.org/biboumi/pages.git pages + # Copy build step output to repository folder + - cp -ar doc/_build/html/* pages/doc/ + # Needed for custom domains + - cp .domains pages || true # Ignore if it doesn't exist + # Commit and push all static files with pipeline started timestamp + - cd pages + - git add . + - git commit -m "CI ${CI_COMMIT_SHA}" || true + - git push || true + when: + - event: push + branch: master + diff --git a/.woodpecker/test.yaml b/.woodpecker/test.yaml new file mode 100644 index 0000000..f3b3c36 --- /dev/null +++ b/.woodpecker/test.yaml @@ -0,0 +1,63 @@ +when: + event: [push, tag, pull_request] + path: ['CMakeLists.txt', 'src/**/*', 'tests/**/*', '.woodpecker/test.yaml'] + +labels: + platform: linux/amd64 + +matrix: + POSTGRESQL: + - "-WITH_POSTGRESQL=1" + - "-WITH_POSTGRESQL=0" + BOTAN: + - "-DWITH_BOTAN=1" + - "-DWITH_BOTAN=0" + +steps: + build-alpine: + image: codeberg.org/biboumi/ci-test-alpine:latest + environment: + COMPILER: "g++" + BUILD_TYPE: "Release" + SQLITE3: "-DWITH_SQLITE3=1" + CXX_FLAGS: "-Werror -Wno-psabi" + LIBIDN: "-DWITH_LIBIDN=1" + SYSTEMD: "-DWITH_SYSTEMD=0" + commands: + - echo "building with parameters '-DCMAKE_CXX_FLAGS="$CXX_FLAGS" -DCMAKE_CXX_COMPILER=$COMPILER -DCMAKE_BUILD_TYPE=$BUILD_TYPE $BOTAN $UDNS $SYSTEMD $LIBIDN $SQLITE3 $POSTGRESQL'" + - mkdir build-alpine/ + - cd build-alpine/ + - cmake .. -DCMAKE_CXX_FLAGS="$CXX_FLAGS" -DCMAKE_CXX_COMPILER=$COMPILER -DCMAKE_BUILD_TYPE=$BUILD_TYPE $BOTAN $UDNS $SYSTEMD $LIBIDN $SQLITE3 $POSTGRESQL + - make everything -j$(nproc || echo 1) + + test-alpine: + depends_on: [build-alpine] + image: codeberg.org/biboumi/ci-test-alpine:latest + commands: + - cd build-alpine/ + - make check_junit || true; make check + - make e2e + + build-debian: + image: codeberg.org/biboumi/ci-test-debian:latest + environment: + COMPILER: "g++" + BUILD_TYPE: "Release" + SQLITE3: "-DWITH_SQLITE3=1" + CXX_FLAGS: "-Werror -Wno-psabi" + LIBIDN: "-DWITH_LIBIDN=1" + SYSTEMD: "-DWITH_SYSTEMD=1" + commands: + - echo "building with parameters '-DCMAKE_CXX_FLAGS="$CXX_FLAGS" -DCMAKE_CXX_COMPILER=$COMPILER -DCMAKE_BUILD_TYPE=$BUILD_TYPE $BOTAN $UDNS $SYSTEMD $LIBIDN $SQLITE3 $POSTGRESQL'" + - mkdir build-debian/ + - cd build-debian/ + - cmake .. -DCMAKE_CXX_FLAGS="$CXX_FLAGS" -DCMAKE_CXX_COMPILER=$COMPILER -DCMAKE_BUILD_TYPE=$BUILD_TYPE $BOTAN $UDNS $SYSTEMD $LIBIDN $SQLITE3 $POSTGRESQL + - make everything -j$(nproc || echo 1) + + test-debian: + depends_on: [build-debian] + image: codeberg.org/biboumi/ci-test-debian:latest + commands: + - cd build-debian/ + - make check_junit || true; make check + - make e2e diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 10b6eb4..cb1e22e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,11 +9,30 @@ For users can still use the in-room JID (#chan%irc@biboumi/NickName) to send a private message but the response you will receive will come from nickname%irc@biboumi. +- Message Correction (XEP-0308) no longer works, and will instead return an + error when the user attempts to. +- XMPP users can send multiline messages on IRC servers which support + draft/multiline, as well as IRC users sending multiline messages will appear as + one message on XMPP. For admins ---------- - Command line option --test-config (or -t) has been added. When used, biboumi will just exit without any error if the configuration is correct +- Options default_persistence_allowed, persist_user_denylist, and + persist_user_allowlist have been added to granualy control who can set the + persist option +- Option archive_expires_after has been added to delete older messages from the + database +- SQLite enables WAL by default now. This can be changed with the + sqlite_journal_mode option. +- Biboumi will regularely perform maintanance on the database. + +For packagers +------------- +- The minimum required version of botan has been upgraded to version 3 +- clang 3.4 and gcc 5.0 are no longer supported. The new minimum required + versions are clang >= 14 and gcc >= 11.2 Version 9.0 - 2020-09-22 ======================== @@ -93,7 +112,7 @@ Version 8.0 - 2018-05-02 %irc.freenode.net@biboumi) has been entirely removed. - Add an “Address” field in the servers’ configure form. This lets the user customize the address to use when connecting to a server. - See https://lab.louiz.org/louiz/biboumi/issues/3273 for more details. + See https://codeberg.org/poezio/biboumi/issues/3273 for more details. - Messages id are properly reflected to the sender - We now properly deal with a PostgreSQL server restart: whenever the connection is lost with the server, we try to reconnect and re-execute the diff --git a/CMakeLists.txt b/CMakeLists.txt index f07b97f..c996f73 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.0) +cmake_minimum_required(VERSION 3.5) project(biboumi) @@ -7,12 +7,12 @@ set(${PROJECT_NAME}_VERSION_MINOR 0) set(${PROJECT_NAME}_VERSION_SUFFIX "~dev") if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 5.0) - message(FATAL_ERROR "GCC version must be at least 5.0.") + if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 11.2) + message(FATAL_ERROR "GCC version must be at least 11.2.") endif() elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") - if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 3.4) - message(FATAL_ERROR "Clang version must be at least 3.4.") + if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 14) + message(FATAL_ERROR "Clang version must be at least 14.") endif() endif() @@ -24,13 +24,13 @@ endif() # ## Find optional instrumentation libraries that will be used in debug only # -find_library(LIBASAN NAMES asan libasan.so.4 libasan.so.3 libasan.so.2 libasan.so.1) +find_library(LIBASAN NAMES asan libasan.so.8 libasan.so.4 libasan.so.3 libasan.so.2 libasan.so.1) find_library(LIBUBSAN NAMES ubsan libubsan.so.0) # ## Set various debug flags (instrumentation libs, coverage, …) # -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++1y -pedantic -Wall -Wextra -Wconversion -fvisibility=hidden -fvisibility-inlines-hidden") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -pedantic -Wall -Wextra -Wconversion -fvisibility=hidden -fvisibility-inlines-hidden") if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fprofile-arcs -ftest-coverage --coverage") endif() @@ -176,6 +176,10 @@ file(GLOB source_bridge src/bridge/*.[hc]pp) add_library(bridge OBJECT ${source_bridge}) +file(GLOB source_scheduler + src/scheduler/*.[hc]pp) +add_library(scheduler OBJECT ${source_scheduler}) + file(GLOB source_config src/config/*.[hc]pp) add_library(config OBJECT ${source_config}) @@ -217,6 +221,7 @@ add_executable(${PROJECT_NAME} src/main.cpp $ $ $ + $ $ $) @@ -230,6 +235,7 @@ add_executable(test_suite ${source_tests} $ $ $ + $ $ $) set_target_properties(test_suite PROPERTIES EXCLUDE_FROM_ALL TRUE) @@ -288,24 +294,14 @@ foreach(file ${source_all}) endforeach() # -## Add a rule to download the catch unit test framework +## Catch unit test framework # -include(ExternalProject) -ExternalProject_Add(catch - GIT_REPOSITORY "https://lab.louiz.org/louiz/Catch.git" - PREFIX "external" - UPDATE_COMMAND "" - CONFIGURE_COMMAND "" - BUILD_COMMAND "" - INSTALL_COMMAND "" - ) -set_target_properties(catch PROPERTIES EXCLUDE_FROM_ALL TRUE) -ExternalProject_Get_Property(catch SOURCE_DIR) -if(NOT EXISTS ${CMAKE_SOURCE_DIR}/tests/catch.hpp) - target_include_directories(test_suite - PUBLIC "${SOURCE_DIR}/single_include/" - ) - add_dependencies(test_suite catch) +find_package(Catch2 3) +if(Catch2_FOUND) + target_link_libraries(test_suite Catch2::Catch2WithMain) + include(CTest) + include(Catch) + catch_discover_tests(test_suite) endif() # @@ -348,11 +344,23 @@ add_custom_target(everything DEPENDS test_suite biboumi) # ## Install target # -install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/biboumi.1 DESTINATION share/man/man1 OPTIONAL COMPONENT documentation) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/biboumi.service DESTINATION lib/systemd/system COMPONENT init) +include(GNUInstallDirs) + +# From cmake version 4.1 CMAKE_INSTALL_SYSCONFDIR has special behaviour when +# CMAKE_INSTALL_PREFIX is /, /usr, or /opt/..., so to not update cmake version +# just yet reimplement this here +# https://cmake.org/cmake/help/latest/module/GNUInstallDirs.html#special-cases +if (CMAKE_INSTALL_SYSCONFDIR STREQUAL "etc" AND (CMAKE_INSTALL_PREFIX STREQUAL "/" OR CMAKE_INSTALL_PREFIX STREQUAL "/usr")) + set(CMAKE_INSTALL_SYSCONFDIR "/etc") +elseif (CMAKE_INSTALL_SYSCONFDIR STREQUAL "etc" AND (CMAKE_INSTALL_PREFIX MATCHES "^/opt/")) + set(CMAKE_INSTALL_SYSCONFDIR "/etc${CMAKE_INSTALL_PREFIX}") +endif() + +install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/biboumi.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 OPTIONAL COMPONENT documentation) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/biboumi.service DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/systemd/system COMPONENT init) file(GLOB policy_files conf/*policy.txt) -install(FILES ${policy_files} DESTINATION /etc/biboumi COMPONENT configuration) +install(FILES ${policy_files} DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/biboumi COMPONENT configuration) # ## Dist target @@ -360,9 +368,7 @@ install(FILES ${policy_files} DESTINATION /etc/bib # add_custom_command(OUTPUT ${ARCHIVE_NAME}.tar.xz COMMAND git archive --prefix=${ARCHIVE_NAME}/ --format=tar HEAD^{tree} - > ${CMAKE_CURRENT_BINARY_DIR}/${ARCHIVE_NAME}.tar - # Append this specific file that is not part of the git repo - COMMAND tar -rf ${CMAKE_CURRENT_BINARY_DIR}/${ARCHIVE_NAME}.tar -P ${SOURCE_DIR}/single_include/catch.hpp --xform 's|/.*/|${ARCHIVE_NAME}/tests/|g' + --output=${CMAKE_CURRENT_BINARY_DIR}/${ARCHIVE_NAME}.tar # Remove a potential existing archive COMMAND rm -f ${CMAKE_CURRENT_BINARY_DIR}/${ARCHIVE_NAME}.tar.xz # Compress the archive @@ -370,12 +376,21 @@ add_custom_command(OUTPUT ${ARCHIVE_NAME}.tar.xz COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --cyan "${ARCHIVE_NAME}.tar.xz created." WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} ) + add_custom_target(dist - DEPENDS ${ARCHIVE_NAME}.tar.xz - DEPENDS catch) + DEPENDS ${ARCHIVE_NAME}.tar.xz) + +# The forge only provides .tar.gz, so make this target to be used to simulate +# downloading a tarball +add_custom_command(OUTPUT ${RPM_VERSION}.tar.gz + COMMAND git archive --prefix=${ARCHIVE_NAME}/ --format=tar.gz HEAD^{tree} + --output=${CMAKE_CURRENT_BINARY_DIR}/${RPM_VERSION}.tar.gz + COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --cyan "${RPM_VERSION}.tar.gz created." + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + ) add_custom_target(rpm - DEPENDS dist + DEPENDS ${RPM_VERSION}.tar.gz COMMAND mkdir -p rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS} COMMAND rpmbuild --define "_topdir `pwd`/rpmbuild/" --define "_sourcedir `pwd`" -ba biboumi.spec ) diff --git a/README.rst b/README.rst index f08fba0..8eae0fd 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ Biboumi is an XMPP gateway that connects to IRC servers and translates between the two protocols. It can be used to access IRC channels using any XMPP client as if these channels were XMPP MUCs. -It is written in modern C++14 and makes great efforts to have as little +It is written in modern C++20 and makes great efforts to have as little dependencies and to be as simple as possible. The goal is to provide a way to access most of IRC features using any XMPP @@ -38,7 +38,7 @@ Florent Le Coz (louiz’) Contact/Support --------------- * XMPP ChatRoom: biboumi@muc.poez.io -* Report a bug: https://lab.louiz.org/louiz/biboumi/issues/new +* Report a bug: https://codeberg.org/poezio/biboumi/issues Also, see the `contributing`_ page. @@ -52,5 +52,5 @@ Biboumi is released under the zlib license. Please read the COPYING file for details. .. _INSTALL: doc/install.rst -.. _the documentation: https://doc.biboumi.louiz.org +.. _the documentation: https://biboumi.codeberg.page/doc/ .. _contributing: CONTRIBUTING.rst diff --git a/cmake/Modules/FindBOTAN.cmake b/cmake/Modules/FindBOTAN.cmake index 3f223e2..d63996b 100644 --- a/cmake/Modules/FindBOTAN.cmake +++ b/cmake/Modules/FindBOTAN.cmake @@ -5,40 +5,63 @@ # BOTAN_FOUND - True if library and include directory are found # If set to TRUE, the following are also defined: # BOTAN_INCLUDE_DIRS - The directory where to find the header file -# BOTAN_LIBRARIES - Where to find the library file -# -# For conveniance, these variables are also set. They have the same values -# than the variables above. The user can thus choose his/her prefered way -# to write them. -# BOTAN_LIBRARY -# BOTAN_INCLUDE_DIR +# BOTAN_LIBRARIES - Where to find the library files # # This file is in the public domain +# Copied and modified from +# https://github.com/vistle/vistle/blob/master/cmake/Modules/FindBOTAN.cmake -include(FindPkgConfig) +include(FindPackageHandleStandardArgs) -if(NOT BOTAN_FOUND) - pkg_check_modules(BOTAN botan-2) +set(BOTAN_VERSIONS botan-3) +set(BOTAN_NAMES botan-3 botan) +set(BOTAN_NAMES_DEBUG botand-3 botand) + +find_path( + BOTAN_INCLUDE_DIR + NAMES botan/build.h + PATH_SUFFIXES ${BOTAN_VERSIONS} + DOC "The Botan include directory") +if(BOTAN_INCLUDE_DIR) + file(READ "${BOTAN_INCLUDE_DIR}/botan/build.h" build) + string(REGEX MATCH "BOTAN_VERSION_MAJOR ([0-9]*)" _ ${build}) + set(BOTAN_VERSION_MAJOR ${CMAKE_MATCH_1}) + string(REGEX MATCH "BOTAN_VERSION_MINOR ([0-9]*)" _ ${build}) + set(BOTAN_VERSION_MINOR ${CMAKE_MATCH_1}) + string(REGEX MATCH "BOTAN_VERSION_PATCH ([0-9]*)" _ ${build}) + set(BOTAN_VERSION_PATCH ${CMAKE_MATCH_1}) + set(BOTAN_VERSION "${BOTAN_VERSION_MAJOR}.${BOTAN_VERSION_MINOR}.${BOTAN_VERSION_PATCH}") endif() -if(NOT BOTAN_FOUND) - find_path(BOTAN_INCLUDE_DIRS NAMES botan/botan.h - PATH_SUFFIXES botan-2 - DOC "The botan include directory") - - find_library(BOTAN_LIBRARIES NAMES botan botan-2 - DOC "The botan library") - - # Use some standard module to handle the QUIETLY and REQUIRED arguments, and - # set BOTAN_FOUND to TRUE if these two variables are set. - include(FindPackageHandleStandardArgs) - find_package_handle_standard_args(BOTAN REQUIRED_VARS BOTAN_LIBRARIES BOTAN_INCLUDE_DIRS) - - if(BOTAN_FOUND) - set(BOTAN_LIBRARY ${BOTAN_LIBRARIES} CACHE INTERNAL "") - set(BOTAN_INCLUDE_DIR ${BOTAN_INCLUDE_DIRS} CACHE INTERNAL "") - set(BOTAN_FOUND ${BOTAN_FOUND} CACHE INTERNAL "") - endif() +find_library( + BOTAN_LIBRARY + NAMES ${BOTAN_NAMES} + PATH_SUFFIXES release/lib lib + DOC "The Botan (release) library") +if(MSVC) + find_library( + BOTAN_LIBRARY_DEBUG + NAMES ${BOTAN_NAMES_DEBUG} + PATH_SUFFIXES debug/lib lib + DOC "The Botan debug library") + find_package_handle_standard_args( + BOTAN + REQUIRED_VARS BOTAN_LIBRARY BOTAN_LIBRARY_DEBUG BOTAN_INCLUDE_DIR + VERSION_VAR BOTAN_VERSION) +else() + find_package_handle_standard_args( + BOTAN + REQUIRED_VARS BOTAN_LIBRARY BOTAN_INCLUDE_DIR + VERSION_VAR BOTAN_VERSION) endif() -mark_as_advanced(BOTAN_INCLUDE_DIRS BOTAN_LIBRARIES) +if(BOTAN_FOUND) + set(BOTAN_INCLUDE_DIRS ${BOTAN_INCLUDE_DIR}) + if(MSVC) + set(BOTAN_LIBRARIES optimized ${BOTAN_LIBRARY} debug ${BOTAN_LIBRARY_DEBUG}) + else() + set(BOTAN_LIBRARIES ${BOTAN_LIBRARY}) + endif() +endif() + +mark_as_advanced(BOTAN_INCLUDE_DIR BOTAN_LIBRARY BOTAN_LIBRARY_DEBUG) diff --git a/conf/irc.gnome.org.policy.txt b/conf/irc.gnome.org.policy.txt deleted file mode 120000 index f06b958..0000000 --- a/conf/irc.gnome.org.policy.txt +++ /dev/null @@ -1 +0,0 @@ -irc.gimp.org.policy.txt \ No newline at end of file diff --git a/conf/irc.mozilla.org.policy.txt b/conf/irc.mozilla.org.policy.txt deleted file mode 100644 index f099e61..0000000 --- a/conf/irc.mozilla.org.policy.txt +++ /dev/null @@ -1 +0,0 @@ -minimum_dh_group_size = 1024 diff --git a/doc/admin.rst b/doc/admin.rst index ec80112..2ae86ac 100644 --- a/doc/admin.rst +++ b/doc/admin.rst @@ -147,18 +147,16 @@ any other IRC server. persistent_by_default ~~~~~~~~~~~~~~~~~~~~~ -If this option is set to `true`, all rooms will be persistent by default: +If this option is set to "true", all rooms will be persistent by default: the value of the “persistent” option in the global configuration of each user will be “true”, but the value of each individual room will still -default to false. This means that a user just needs to change the global -“persistent” configuration option to false in order to override this. +default to "false". This means that a user just needs to change their global +“persistent” configuration option to "false" in order to override this +and be able to set rooms to not persistent. -If it is set to false (the default value), all rooms are not persistent by +If this option is set to "false" (the default value), all rooms are not persistent by default. -Each room can be configured individually by each user, to override this -default value. See :ref:`Ad-hoc commands`. - realname_customization ~~~~~~~~~~~~~~~~~~~~~~ @@ -248,6 +246,57 @@ configuration file is located: for example if biboumi reads its configuration from /etc/biboumi/biboumi.cfg, the policy_directory value will be /etc/biboumi. +persist_user_denylist +~~~~~~~~~~~~~~~~~~~~~ + +A list of XMPP users or domains who are not allowed to change the `persist` +ad-hoc option and who will not be impacted by the `persistent_by_default` +config option. Specific JIDs in this list will take priority over allowed JIDs +or domains. + +persist_user_allowlist +~~~~~~~~~~~~~~~~~~~~~~ + +A list of XMPP XMPP users or domains who are allowed to change the `persist` +ad-hoc option. + +default_persistence_allowed +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a given XMPP user or domain is not in either denylist or allowlist this +option decides if the user is allowed to persist or not. + +archive_expires_after +~~~~~~~~~~~~~~~~~~~~~ + +Messages in the archive can expire after some time, by default never. This +can be changed by this setting + +.. code-block:: ini + + # one day + archive_expires_after=1d + # one week + archive_expires_after=1w + # two months + archive_expires_after=2m + # one year + archive_expires_after=1y + # 1h + archive_expires_after=1h + # keep messages forever, the default + archive_expires_after=0 + +sqlite_journal_mode +~~~~~~~~~~~~~~~~~~~ + +Set the `journal_mode https://sqlite.org/pragma.html#pragma_journal_mode`_ to +be used when connecting to an SQLite database. This option can be one of +`DELETE`, `TRUNCATE`, `PERSIST`, `MEMORY`, `WAL`, `OFF`, or `none` to not set +the journal_mode at all. + +By default this option is set to `WAL` to use `Write-Ahead Logging +https://sqlite.org/wal.html`_. TLS configuration ----------------- @@ -271,8 +320,9 @@ To see the list of possible options to configure, refer to `Botan’s TLS documentation `_. In addition to these Botan options, biboumi implements a few custom options listed hereafter: + - verify_certificate: if this value is set to false, biboumi will not check -the certificate validity at all. The default value is true. + the certificate validity at all. The default value is true. By default, biboumi provides a few policy files, to work around some issues found with a few well-known IRC servers. diff --git a/doc/conf.py b/doc/conf.py index 504c15b..4cf9d53 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -24,9 +24,9 @@ copyright = '2018, Florent Le Coz' author = 'Florent Le Coz' # The short X.Y version -version = '8.4' +version = '10.0' # The full version, including alpha/beta/rc tags -release = '8.4' +release = '10.0' # -- General configuration --------------------------------------------------- @@ -59,7 +59,7 @@ master_doc = 'index' # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/doc/contributing.rst b/doc/contributing.rst index 8f01c82..c805ced 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -2,10 +2,7 @@ Contributing to biboumi ####################### -Biboumi’s main workplace is at https://lab.louiz.org/louiz/biboumi - -The repository is also mirrored on other websites, for example on github, -but that’s mainly for the convenience of users. +Biboumi’s main workplace is at https://codeberg.org/poezio/biboumi Before doing anything, you can come on the `XMPP chatroom`_ to discuss your changes, issues or ideas. @@ -14,8 +11,7 @@ changes, issues or ideas. Bug reports, feature requests ----------------------------- -To open a bug report, or a feature request, please do so on `our gitlab’s -bug tracker`_. +To open a bug report, or a feature request, please do so on `our bug tracker`_. If the bug you’re reporting is about a bad behaviour of biboumi when some XMPP or IRC events occur, please try to reproduce the issue with a biboumi running @@ -34,10 +30,10 @@ Code To contribute code, you can do so using git: commit your changes on any publicly available git repository and communicate us its address. This can -be done with a `gitlab merge request`_, or a `github pull request`_ or just +be done with a `codeberg merge request`_ or just by sending a message into the `XMPP chatroom`_. -It is suggested that you use gitlab’s merge requests: this will +It is suggested that you use codeberg’s merge requests: this will automatically run our continuous integration tests. It is also recommended to add some unit or end-to-end tests for the proposed @@ -49,7 +45,7 @@ Tests There are two test suites for biboumi: -- unit tests that can be run simply using `make check`. +- unit tests that can be run simply using ``make check``. These tests use the Catch2 test framework, are written in pure C++ and they should always succeed, in all possible build configuration. @@ -59,7 +55,7 @@ There are two test suites for biboumi: Read more about these tests in the specific documentation TODO. All these tests automatically run with various configurations, on various -platforms, using gitlab CI. +platforms, using woodpecker CI. Coding style @@ -74,9 +70,8 @@ Please try to follow the existing style: - Everything else is in snake_case. -.. _our gitlab’s bug tracker: https://lab.louiz.org/louiz/biboumi/issues/new -.. _gitlab merge request: https://lab.louiz.org/louiz/biboumi/merge_requests/new -.. _github pull request: https://github.com/louiz/biboumi/pulls +.. _our bug tracker: https://codeberg.org/poezio/biboumi/issues/new +.. _codeberg merge request: https://codeberg.org/poezio/biboumi/compare/master...master .. _XMPP chatroom: xmpp:biboumi@muc.poez.io .. _Dockerfile.base: docker/biboumi-test/fedora/Dockerfile.base .. _Dockerfile: docker/biboumi-test/fedora/Dockerfile diff --git a/doc/developer.rst b/doc/developer.rst index b3ef158..8333716 100644 --- a/doc/developer.rst +++ b/doc/developer.rst @@ -44,9 +44,7 @@ docker is very unsecure) is to follow these instructions: :caption: Start a docker container with everything installed docker run --name biboumi-e2e -v /home/louiz/biboumi/:/home/tester/biboumi \ - --add-host="irc.localhost:127.0.0.1" \ - --add-host="biboumi.localhost:127.0.0.1" \ - --rm -it docker.louiz.org/louiz/biboumi/test-alpine \ + --rm -it codeberg.org/biboumi/ci-test-alpine \ /bin/bash This creates a container where every dependency is already installed. We @@ -54,9 +52,9 @@ mount your working directory inside the container: be sure to modify the first path `/home/louiz/biboumi` with your own. The hosts that we add are needed for the test suite to properly work. -You can use the test-fedora or test-debian images instead of test-alpine -if you want, but it should not change anything (even if your host machine -uses debian or fedora), alpine is just the lighter one. +You can use the ci-test-fedora or ci-test-debian images instead of +ci-test-alpine if you want, but it should not change anything (even if your +host machine uses debian or fedora), alpine is just the lighter one. .. note:: diff --git a/doc/index.rst b/doc/index.rst index 819a3e5..04c6e90 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -6,9 +6,9 @@ Biboumi – XMPP gateway to IRC ============================= -Homepage: https://biboumi.louiz.org +Homepage: https://biboumi.codeberg.page -Forge: https://lab.louiz.org/louiz/biboumi +Forge: https://codeberg.org/poezio/biboumi Biboumi is an XMPP gateway that connects to IRC servers and translates between the two protocols. It can be used to access IRC channels using any diff --git a/doc/install.rst b/doc/install.rst index 685511d..743647d 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -20,7 +20,7 @@ biboumi by having all dependencies. Tools: ~~~~~~ -- A C++14 compiler (clang >= 3.4 or gcc >= 5.0 for example) +- A C++20 compiler (clang >= 14 or gcc >= 11.2 for example) - CMake - sphinx (optional) to build the documentation @@ -51,7 +51,7 @@ udns_ (optional, but recommended) performances when connecting to a big number of IRC servers at the same time. -libbotan_ 2.x (optional, but recommended) +libbotan_ 3.x (optional, but recommended) Provides TLS support. Without it, IRC connections are all made in plain-text mode. diff --git a/doc/synopsis.rst b/doc/synopsis.rst index 2b28c74..15455b1 100644 --- a/doc/synopsis.rst +++ b/doc/synopsis.rst @@ -4,15 +4,15 @@ Synopsis biboumi [-ht] [*config_filename*] Command-Line Options -======== +==================== -h, \\-\\-help -~~~~~~~~ +~~~~~~~~~~~~~~ Display a help message and exit. -t, \\-\\-test-config -~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~ Do not run, just test the configuration file syntax. Exit with a 0 status if the configuration is valid, exits with a non-zero status diff --git a/doc/user.rst b/doc/user.rst index add3480..ae89de0 100644 --- a/doc/user.rst +++ b/doc/user.rst @@ -41,8 +41,9 @@ IRC channels and IRC users have a local part formed like this: ``name`` can be a channel name or an user nickname. The distinction between the two is based on the first character: by default, if the name starts with -``'#'`` or ``'&'`` (but this can be overridden by the server, using the -ISUPPORT extension) then it’s a channel name, otherwise this is a nickname. +``'&'``, ``'#'``, ``'+'`` or ``'!'`` (but this can be overridden by the server, +using the ISUPPORT extension) then it’s a channel name, otherwise this is a +nickname. There is two ways to address an IRC user, using a local part like this: ``nickname`` % ``irc_server`` or by using the in-room address of the @@ -398,12 +399,13 @@ The provided configuration form contains these fields: the database. - **Max history length**: The maximum number of lines in the history that the server is allowed to send when joining a channel. -- **Persistent**: Overrides the value specified in each individual - channel. If this option is set to true, all channels are persistent, - whether or not their specific value is true or false. This option is true - by default for everyone if the `persistent_by_default` configuration - option is true, otherwise it’s false. See below for more details on what a - persistent channel is. +- **Persistent**: If this option is set to true, the persistence setting + specified on each individual channel is ignored and all channels are persistent. + If this option is set to false, the persistence setting specified on each + individual channel is respected. + This option defaults to the value of :ref:`persistent_by_default`, which itself + defaults to false. + See below for more details on what a persistent channel is. On a server JID ~~~~~~~~~~~~~~~ @@ -442,7 +444,8 @@ server. The provided configuration form contains these fields: successful. It can for example be used to identify yourself using NickServ, with a command like this: `PRIVMSG NickServ :identify PASSWORD`. -- **Ports**: The list of TCP ports to use when connecting to this IRC server. +- **Unencrypted ports**: The list of TCP ports to use when connecting to + this IRC server. This list will be tried in sequence, until the connection succeeds for one of them. The connection made on these ports will not use TLS, the communication will be insecure. The default list contains 6697 and 6670. diff --git a/docker/biboumi/alpine/Dockerfile b/docker/biboumi/alpine/Dockerfile index 9ceb1e2..28f843a 100644 --- a/docker/biboumi/alpine/Dockerfile +++ b/docker/biboumi/alpine/Dockerfile @@ -8,32 +8,36 @@ FROM docker.io/alpine:latest as builder RUN apk add --no-cache --virtual .build cmake expat-dev g++ git libidn-dev \ - make postgresql-dev python2 sqlite-dev udns-dev util-linux-dev botan-dev + make postgresql-dev python3 sqlite-dev udns-dev util-linux-dev botan3-dev +COPY . /source -RUN git clone https://lab.louiz.org/louiz/biboumi && \ - mkdir ./biboumi/build && \ - cd ./biboumi/build && \ +WORKDIR /source + +RUN mkdir build && cd build && \ cmake .. -DCMAKE_INSTALL_PREFIX=/usr \ -DCMAKE_BUILD_TYPE=Release \ -DWITH_BOTAN=1 \ + -DWITHOUT_SYSTEMD=1 \ -DWITH_SQLITE3=1 \ -DWITH_LIBIDN=1 \ - -DWITH_POSTGRESQL=1 && \ - make -j8 && \ - make install + -DWITH_POSTGRESQL=1 \ + -DWITH_UDNS=1 \ + -DWITH_LIBIDN=1 && \ + cmake --build . && \ + cmake --install . # --- FROM docker.io/alpine:latest RUN apk add --no-cache libidn libpq libstdc++ libuuid postgresql-libs \ - sqlite-libs udns expat ca-certificates botan + sqlite-libs udns expat ca-certificates botan3 COPY --from=builder /etc/biboumi /etc/biboumi COPY --from=builder /usr/bin/biboumi /usr/bin/biboumi -COPY ./biboumi.cfg /etc/biboumi/biboumi.cfg +COPY --from=builder /source/docker/biboumi/alpine/biboumi.cfg /etc/biboumi/biboumi.cfg RUN adduser biboumi -D -h /home/biboumi && \ mkdir /var/lib/biboumi && \ diff --git a/docker/test/alpine/Dockerfile b/docker/test/alpine/Dockerfile index 60f7499..047d134 100644 --- a/docker/test/alpine/Dockerfile +++ b/docker/test/alpine/Dockerfile @@ -1,13 +1,17 @@ # This Dockerfile creates a docker image suitable to run biboumi’s build and # tests. For example, it can be used on with gitlab-ci. -FROM docker.io/alpine:latest +FROM docker.io/alpine:edge ENV LC_ALL C.UTF-8 +# py3-slixmpp is in testing repo +RUN echo "https://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories + # Needed to build biboumi RUN apk add --no-cache \ git \ +ergo \ make \ cmake \ g++ \ @@ -16,20 +20,19 @@ udns-dev \ expat-dev \ libidn-dev \ sqlite-dev \ -botan-dev \ +botan3-dev \ util-linux-dev \ libgcrypt-dev \ postgresql-dev \ +catch2-3 \ valgrind \ py3-pip \ py3-lxml \ +py3-slixmpp \ python3-dev \ libffi-dev \ go \ +rust \ +cargo \ wget -# Install oragono, for e2e tests -RUN wget "https://github.com/oragono/oragono/archive/v2.0.0.tar.gz" && tar xvf "v2.0.0.tar.gz" && cd "oragono-2.0.0" && make && cp ~/go/bin/oragono /usr/local/bin - -# Install slixmpp, for e2e tests -RUN git clone https://github.com/saghul/aiodns.git && cd aiodns && git checkout 7ee13f9bea25784322~ && python3 setup.py build && python3 setup.py install && git clone https://lab.louiz.org/poezio/slixmpp && pip3 install pyasn1 && cd slixmpp && python3 setup.py build && python3 setup.py install diff --git a/docker/test/debian/Dockerfile b/docker/test/debian/Dockerfile index d1254c5..8ba8064 100644 --- a/docker/test/debian/Dockerfile +++ b/docker/test/debian/Dockerfile @@ -1,38 +1,32 @@ # This Dockerfile creates a docker image suitable to run biboumi’s build and # tests. For example, it can be used on with gitlab-ci. -FROM docker.io/debian:buster +FROM docker.io/debian:trixie -ENV LC_ALL C.UTF-8 +ENV LC_ALL=C.UTF-8 -RUN apt update - -# Needed to build biboumi -RUN apt install -y --no-install-recommends \ +RUN apt update && apt install -y --no-install-recommends \ git \ make \ cmake \ g++ \ +ca-certificates \ libuuid1 \ libudns-dev \ libexpat1-dev \ libidn11-dev \ libsqlite3-dev \ -libbotan-2-dev \ +libbotan-3-dev \ libsystemd-dev \ uuid-dev \ libgcrypt20-dev \ libpq-dev \ +libcatch2-dev \ valgrind \ -libasan5 \ -libubsan0 \ -python3-pip \ +libasan8 \ +libubsan1 \ python3-lxml \ -python3-dev \ +python3-slixmpp \ +ergochat \ wget -RUN wget "https://github.com/oragono/oragono/releases/download/v2.0.0/oragono-2.0.0-linux-x64.tar.gz" && tar xvf oragono-2.0.0-linux-x64.tar.gz && cp oragono-2.0.0-linux-x64/oragono /usr/local/bin - -# Install slixmpp, for e2e tests -RUN git clone https://github.com/saghul/aiodns.git && cd aiodns && git checkout 7ee13f9bea25784322~ && python3 setup.py build && python3 setup.py install && git clone https://lab.louiz.org/poezio/slixmpp && pip3 install pyasn1 && cd slixmpp && python3 setup.py build && python3 setup.py install - diff --git a/docker/test/fedora/Dockerfile b/docker/test/fedora/Dockerfile index cd41741..a5b3dec 100644 --- a/docker/test/fedora/Dockerfile +++ b/docker/test/fedora/Dockerfile @@ -1,9 +1,10 @@ # This Dockerfile creates a docker image suitable to run biboumi’s build and # tests. For example, it can be used on with gitlab-ci. -FROM docker.io/fedora:32 +FROM docker.io/fedora:44 -ENV LC_ALL C.UTF-8 +ARG ERGO_VERSION=2.16.0 +ENV LC_ALL=C.UTF-8 RUN dnf --refresh install -y \ git \ @@ -15,7 +16,7 @@ udns-devel \ expat-devel \ libidn-devel \ sqlite-devel \ -botan2-devel \ +botan3-devel \ systemd-devel \ libuuid-devel \ libgcrypt-devel \ @@ -23,6 +24,7 @@ postgresql-devel \ lcov \ libasan \ libubsan \ +catch-devel \ valgrind \ python3-pip \ python3-lxml \ @@ -35,7 +37,7 @@ rpmdevtools \ && dnf clean all # Install slixmpp, for e2e tests -RUN git clone https://lab.louiz.org/poezio/slixmpp && pip3 install pyasn1 && cd slixmpp && python3 setup.py build && python3 setup.py install +RUN pip3 install --break-system-packages slixmpp -# Install oragono, for e2e tests -RUN wget "https://github.com/oragono/oragono/releases/download/v2.0.0/oragono-2.0.0-linux-x64.tar.gz" && tar xvf oragono-2.0.0-linux-x64.tar.gz && cp oragono-2.0.0-linux-x64/oragono /usr/local/bin +# Install ergo, for e2e tests +RUN wget "https://github.com/ergochat/ergo/releases/download/v${ERGO_VERSION}/ergo-${ERGO_VERSION}-linux-x86_64.tar.gz" && tar xvf ergo-${ERGO_VERSION}-linux-x86_64.tar.gz && cp -r ergo-${ERGO_VERSION}-linux-x86_64 /usr/local/lib/ergo && ln -s /usr/local/lib/ergo/ergo /usr/local/bin/ergo diff --git a/packaging/biboumi.spec.cmake b/packaging/biboumi.spec.cmake index 1352505..748904a 100644 --- a/packaging/biboumi.spec.cmake +++ b/packaging/biboumi.spec.cmake @@ -3,9 +3,9 @@ Version: ${RPM_VERSION} Release: 1%{?dist} Summary: Lightweight XMPP to IRC gateway -License: zlib -URL: http://biboumi.louiz.org -Source0: http://git.louiz.org/biboumi/snapshot/biboumi-%{version}.tar.xz +License: Zlib +URL: https://biboumi.codeberg.page/ +Source0: https://codeberg.org/poezio/biboumi/archive/%{version}.tar.gz BuildRequires: libidn-devel BuildRequires: expat-devel @@ -13,8 +13,10 @@ BuildRequires: libuuid-devel BuildRequires: systemd-devel BuildRequires: sqlite-devel BuildRequires: postgresql-devel -BuildRequires: botan2-devel +BuildRequires: botan3-devel +BuildRequires: catch-devel BuildRequires: cmake +BuildRequires: g++ BuildRequires: systemd BuildRequires: python3-sphinx @@ -64,8 +66,8 @@ make check %{?_smp_mflags} %changelog --* ${RPM_DATE} Le Coz Florent - ${RPM_VERSION}-1 --- Build latest git revision +* ${RPM_DATE} Le Coz Florent - ${RPM_VERSION}-1 + Build latest git revision * Tue Sep 22 2020 Le Coz Florent - 9.0-1 Update to version 9.0 @@ -76,7 +78,7 @@ make check %{?_smp_mflags} * Tue Feb 25 2020 Le Coz Florent - 8.4-1 Update to version 8.4 -* Wed Jun 1 2018 Le Coz Florent - 8.3-1 +* Fri Jun 1 2018 Le Coz Florent - 8.3-1 Update to version 8.3 * Fri May 25 2018 Le Coz Florent - 8.2-1 diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index b1ff2f4..c6182be 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -2,18 +2,22 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include #include +#include #include "result_set_management.hpp" #include #include +#include using namespace std::string_literals; @@ -64,7 +68,7 @@ void Bridge::shutdown(const std::string& exit_message) { for (auto& pair: this->irc_clients) { - std::unique_ptr& irc = pair.second; + std::shared_ptr& irc = pair.second; irc->send_quit_command(exit_message); } } @@ -135,11 +139,11 @@ IrcClient* Bridge::make_irc_client(const std::string& hostname, const std::strin realname = this->get_bare_jid(); } this->irc_clients.emplace(hostname, - std::make_unique(this->poller, hostname, + std::make_shared(this->poller, hostname, nickname, username, realname, jid.domain, *this)); - std::unique_ptr& irc = this->irc_clients.at(hostname); + std::shared_ptr irc = this->irc_clients.at(hostname); return irc.get(); } } @@ -221,6 +225,83 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: std::vector lines = utils::split(body, '\n', true); if (lines.empty()) return ; + + if (irc->has_capability("draft/multiline") + && lines.size() > 1 + // /me and /mode cannot be multi lined + && lines[0].substr(0, 5) != "/mode" + && lines[0].substr(0, 4) != "/me ") + { + std::string uuid; + const auto xmpp_body = this->make_xmpp_body(body); +#ifdef USE_DATABASE + if (this->record_history) + uuid = Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(), + std::get<0>(xmpp_body), irc->get_own_nick()); +#else + uuid = utils::gen_uuid(); +#endif + if (id.empty()) + id = utils::gen_uuid(); + + // A message with a lot of lines may need to be split, even if the + // server supports draft/multiline, as we may not be allowed to send + // that many lines all at once. As such check max-lines + // https://ircv3.net/specs/extensions/multiline#capabilities + std::vector> lines_chunked = {lines}; + auto cap_value = irc->get_capability_value("draft/multiline"); + if (cap_value) { + auto cap_value_parsed = parse_cap_keyval(cap_value.value()); + if (cap_value_parsed.contains("max-lines") && + cap_value_parsed.at("max-lines").has_value()) { + std::stringstream max_lines_string( + cap_value_parsed.at("max-lines").value()); + size_t size; + max_lines_string >> size; + if (size > 0) + lines_chunked = utils::chunk(lines, size); + } + } + for (const auto &lines : lines_chunked) { + std::vector messages; + for (const std::string &line : lines) { + constexpr auto max_username_size = 10; + constexpr auto max_hostname_size = 63; + const auto line_size = 512 - 1 - ::strlen("batch=") - uuid.length() - 1 - ::strlen("draft/multiline-concat") - 1 - + irc->get_own_nick().size() - max_username_size - max_hostname_size - + ::strlen(":!@ PRIVMSG ") - iid.get_local().length() - ::strlen(" :\r\n"); + const auto privmsgs = cut(line, line_size); + bool first = true; + for (const auto& privmsg: privmsgs) + { + if (first) + { + messages.push_back(IrcMessage({}, std::string(), "PRIVMSG", {iid.get_local(), privmsg})); + first = false; + } + else + messages.push_back(IrcMessage({{"draft/multiline-concat", {}}}, std::string(), "PRIVMSG", {iid.get_local(), privmsg})); + } + } + + MessageCallback mirror_to_all_resources = [this, iid, uuid, id, nodes_to_reflect, body](const IrcClient* irc, const IrcMessage&) { + for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) + { + auto stanza = this->xmpp.make_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(body), + this->user_jid + "/" + + resource, uuid, id); + for (const auto& node: nodes_to_reflect) + stanza.add_child(node); + this->xmpp.send_stanza(stanza); + } + }; + irc->send_batch(uuid, "draft/multiline", {iid.get_local()}, messages, std::move(mirror_to_all_resources)); + id = utils::gen_uuid(); + uuid = utils::gen_uuid(); + } + return; + } + bool first = true; for (const std::string& line: lines) { @@ -403,6 +484,41 @@ void Bridge::send_private_message(const Iid& iid, const std::string& body, const std::vector lines = utils::split(body, '\n', true); if (lines.empty()) return ; + + if (irc->has_capability("draft/multiline") + && lines.size() > 1 + // /me and /mode cannot be multi lined + && lines[0].substr(0, 5) != "/mode" + && lines[0].substr(0, 4) != "/me ") + { + std::basic_string uuid = utils::gen_uuid(); + std::vector messages; + for (const std::string &line : lines) { + constexpr auto max_username_size = 10; + constexpr auto max_hostname_size = 63; + const auto line_size = 512 - 1 - ::strlen("batch=") - uuid.length() - 1 - + ::strlen("draft/multiline-concat") - 1 - + irc->get_own_nick().size() - max_username_size - + max_hostname_size - ::strlen(":!@ PRIVMSG ") - + iid.get_local().length() - ::strlen(" :\r\n"); + const auto privmsgs = cut(line, line_size); + bool first = true; + for (const auto &privmsg : privmsgs) { + if (first) { + messages.push_back(IrcMessage({}, std::string(), "PRIVMSG", + {iid.get_local(), privmsg})); + first = false; + } else + messages.push_back(IrcMessage({{"draft/multiline-concat", {}}}, + std::string(), "PRIVMSG", + {iid.get_local(), privmsg})); + } + } + irc->send_batch(uuid, "draft/multiline", {iid.get_local()}, messages, + [](const IrcClient *, const IrcMessage &) {}); + return; + } + for (const std::string& line: lines) { if (line.substr(0, 4) == "/me ") @@ -436,7 +552,9 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con bool persistent = false; #ifdef USE_DATABASE const auto goptions = Database::get_global_options(this->user_jid); - if (goptions.col()) + if (!utils::is_requester_allowed_to_persist(Jid(this->user_jid))) + persistent = false; + else if (goptions.col()) persistent = true; else { @@ -1151,12 +1269,12 @@ void Bridge::trigger_on_irc_message(const std::string& irc_hostname, const IrcMe } } -std::unordered_map>& Bridge::get_irc_clients() +std::unordered_map>& Bridge::get_irc_clients() { return this->irc_clients; } -const std::unordered_map>& Bridge::get_irc_clients() const +const std::unordered_map>& Bridge::get_irc_clients() const { return this->irc_clients; } @@ -1165,7 +1283,7 @@ std::set Bridge::get_chantypes(const std::string& hostname) const { IrcClient* irc = this->find_irc_client(hostname); if (!irc) - return {'#', '&'}; + return {'&', '#', '+', '!'}; return irc->get_chantypes(); } diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp index a7aef3d..e9e5473 100644 --- a/src/bridge/bridge.hpp +++ b/src/bridge/bridge.hpp @@ -226,8 +226,8 @@ public: * iq_responder_callback_t and remove the callback from the list. */ void trigger_on_irc_message(const std::string& irc_hostname, const IrcMessage& message); - std::unordered_map>& get_irc_clients(); - const std::unordered_map>& get_irc_clients() const; + std::unordered_map>& get_irc_clients(); + const std::unordered_map>& get_irc_clients() const; std::set get_chantypes(const std::string& hostname) const; #ifdef USE_DATABASE void set_record_history(const bool val); @@ -260,7 +260,7 @@ private: * One IrcClient for each IRC server we need to be connected to. * The pointer is shared by the bridge and the poller. */ - std::unordered_map> irc_clients; + std::unordered_map> irc_clients; /** * To communicate back with the XMPP component */ diff --git a/src/bridge/colors.cpp b/src/bridge/colors.cpp index 7662425..9c82096 100644 --- a/src/bridge/colors.cpp +++ b/src/bridge/colors.cpp @@ -24,7 +24,7 @@ static const char* irc_colors_to_css[IRC_NUM_COLORS] = { "cyan", "lightcyan", "lightblue", - "lightmagenta", + "hotpink", "gray", "white", }; diff --git a/src/config/config.cpp b/src/config/config.cpp index 2f64b9e..6a20357 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include diff --git a/src/config/config.hpp b/src/config/config.hpp index 9c28e8c..625a749 100644 --- a/src/config/config.hpp +++ b/src/config/config.hpp @@ -16,8 +16,6 @@ #pragma once #include -#include -#include #include #include #include diff --git a/src/database/database.cpp b/src/database/database.cpp index 861abcb..9f2657a 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -2,6 +2,7 @@ #ifdef USE_DATABASE #include +#include #include #include #include @@ -317,6 +318,14 @@ std::vector Database::get_full_roster() return query.execute(*Database::db); } +void Database::delete_old_muclines(const long& seconds) +{ + auto query = DeleteQuery(Database::muc_log_lines.get_name()); + query << " WHERE " << Database::Date{} << "<" << seconds; + + query.execute(*Database::db); +} + void Database::close() { Database::db = nullptr; diff --git a/src/database/database.hpp b/src/database/database.hpp index de1df49..12b3ef5 100644 --- a/src/database/database.hpp +++ b/src/database/database.hpp @@ -153,6 +153,7 @@ class Database static void delete_roster_item(const std::string& local, const std::string& remote); static std::vector get_contact_list(const std::string& local); static std::vector get_full_roster(); + static void delete_old_muclines(const long& seconds); static void close(); static void open(const std::string& filename); diff --git a/src/database/database_type.hpp b/src/database/database_type.hpp new file mode 100644 index 0000000..0fb48db --- /dev/null +++ b/src/database/database_type.hpp @@ -0,0 +1,9 @@ +#pragma once + +enum class DatabaseType +{ + none, + sqlite3, + postgresql, +}; + diff --git a/src/database/engine.hpp b/src/database/engine.hpp index 41dccf5..369b4cc 100644 --- a/src/database/engine.hpp +++ b/src/database/engine.hpp @@ -7,11 +7,11 @@ * Everything else (all portable stuf) should go outside of this class. */ +#include #include #include #include -#include #include #include @@ -36,6 +36,7 @@ class DatabaseEngine return {}; } virtual std::string id_column_type() = 0; + virtual DatabaseType get_type() { return DatabaseType::none; }; int64_t last_inserted_rowid{-1}; }; diff --git a/src/database/postgresql_engine.cpp b/src/database/postgresql_engine.cpp index 59bc885..d3773e4 100644 --- a/src/database/postgresql_engine.cpp +++ b/src/database/postgresql_engine.cpp @@ -101,4 +101,7 @@ std::string PostgresqlEngine::id_column_type() return "SERIAL"; } +DatabaseType PostgresqlEngine::get_type() { + return DatabaseType::postgresql; +}; #endif diff --git a/src/database/postgresql_engine.hpp b/src/database/postgresql_engine.hpp index 1a9c249..4153922 100644 --- a/src/database/postgresql_engine.hpp +++ b/src/database/postgresql_engine.hpp @@ -2,9 +2,9 @@ #include #include -#include #include +#include #include #include @@ -30,6 +30,8 @@ class PostgresqlEngine: public DatabaseEngine void extract_last_insert_rowid(Statement& statement) override; std::string get_returning_id_sql_string(const std::string& col_name) override; std::string id_column_type() override; + DatabaseType get_type() override final; + private: PGconn* const conn; }; diff --git a/src/database/row.hpp b/src/database/row.hpp index 4004b5d..f5c13b7 100644 --- a/src/database/row.hpp +++ b/src/database/row.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include template diff --git a/src/database/sqlite3_engine.cpp b/src/database/sqlite3_engine.cpp index 5e3bba1..5128065 100644 --- a/src/database/sqlite3_engine.cpp +++ b/src/database/sqlite3_engine.cpp @@ -7,9 +7,14 @@ #include +#include #include #include -#include +#include + +#include +#include + Sqlite3Engine::Sqlite3Engine(sqlite3* db): db(db) @@ -46,6 +51,8 @@ std::set Sqlite3Engine::get_all_columns_from_table(const std::strin std::unique_ptr Sqlite3Engine::open(const std::string& filename) { sqlite3* new_db; + const std::string dirname = utils::dirname(filename); + std::filesystem::create_directories(dirname); auto res = sqlite3_open_v2(filename.data(), &new_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr); if (res != SQLITE_OK) { @@ -53,7 +60,31 @@ std::unique_ptr Sqlite3Engine::open(const std::string& filename) sqlite3_close(new_db); throw std::runtime_error(""); } - return std::make_unique(new_db); + std::unique_ptr db = std::make_unique(new_db); + + // Enable Write-Ahead Logging + // https://sqlite.org/wal.html + auto sqlite_journal_mode = Config::get("sqlite_journal_mode", "WAL"); + if (sqlite_journal_mode != "none") { + if (Sqlite3Engine::is_valid_sqlite_journal_mode(sqlite_journal_mode)) { + auto [success, err_msg] = + db->raw_exec("PRAGMA journal_mode = " + sqlite_journal_mode); + if (!success) + log_error("Failed enabling SQLite WAL with error: ", err_msg); + } else { + log_error("Invalid config option sqlite_journal_mode: ", + sqlite_journal_mode); + } + } + for (const auto &s : Sqlite3Engine::startup_commands) { + auto [success, err_msg] = db->raw_exec(s); + if (!success) { + log_error("Failed running startup command: ", s, + "; with error: ", err_msg); + } + } + + return db; } std::tuple Sqlite3Engine::raw_exec(const std::string& query) @@ -97,4 +128,17 @@ std::string Sqlite3Engine::id_column_type() return "INTEGER PRIMARY KEY AUTOINCREMENT"; } +DatabaseType Sqlite3Engine::get_type() { + return DatabaseType::sqlite3; +}; + + +bool Sqlite3Engine::is_valid_sqlite_journal_mode(std::string &sqlite_journal_mode) { + // https://sqlite.org/pragma.html#pragma_journal_mode + for (const char* s : Sqlite3Engine::valid_sqlite_journal_modes) { + if (sqlite_journal_mode == s) + return true; + } + return false; +} #endif diff --git a/src/database/sqlite3_engine.hpp b/src/database/sqlite3_engine.hpp index a7bfcdb..13ed4d0 100644 --- a/src/database/sqlite3_engine.hpp +++ b/src/database/sqlite3_engine.hpp @@ -29,8 +29,29 @@ class Sqlite3Engine: public DatabaseEngine std::unique_ptr prepare(const std::string& query) override; void extract_last_insert_rowid(Statement& statement) override; std::string id_column_type() override; + DatabaseType get_type() override final; + private: sqlite3* const db; + constexpr static const char *const startup_commands[3] = { + // Wait for a maximum of 5 seconds if a write is busy + "PRAGMA busy_timeout = 5000", + // Reduce disk syncs + // >When synchronous is NORMAL (1), the SQLite database engine will still + // >sync at the most critical moments + // https://sqlite.org/pragma.html#pragma_synchronous + "PRAGMA synchronous = NORMAL", + // Run PRAGMA command early to analyze all tables, as recommended by + // sqlite + // >Applications that use long-lived database connections should run + // >"PRAGMA optimize=0x10002;" when the connection is first opened + // https://sqlite.org/lang_analyze.html + "PRAGMA optimize=0x10002", + }; + constexpr static const char *const valid_sqlite_journal_modes[6] = { + "DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF", + }; + static bool is_valid_sqlite_journal_mode(std::string &sqlite_journal_mode); }; #else diff --git a/src/database/table.hpp b/src/database/table.hpp index 0b8bfc0..2f63784 100644 --- a/src/database/table.hpp +++ b/src/database/table.hpp @@ -5,7 +5,6 @@ #include #include -#include #include #include diff --git a/src/irc/iid.hpp b/src/irc/iid.hpp index 89f4797..ef8cce5 100644 --- a/src/irc/iid.hpp +++ b/src/irc/iid.hpp @@ -20,7 +20,7 @@ class Bridge; * It’s possible to have an empty-string server, but it makes no sense in * biboumi’s context. * - * Assuming the chantypes are '#' and '&': + * Assuming the chantypes are '&', '#', '+' and '!': * * #test%irc.example.org has : * - local: "#test" (the # is part of the name, it could very well be absent, or & (for example) instead) diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp index 8a64727..a3d1973 100644 --- a/src/irc/irc_client.cpp +++ b/src/irc/irc_client.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -83,6 +84,7 @@ static const std::unordered_map& poller, std::string hostname, bridge(bridge), welcomed(false), chanmodes({"", "", "", ""}), - chantypes({'#', '&'}), + chantypes({'&', '#', '+', '!'}), tokens_bucket(this->get_throttle_limit(), 1s, [this]() { if (message_queue.empty()) return true; @@ -213,7 +215,7 @@ void IrcClient::start() auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), this->get_hostname()); # ifdef BOTAN_FOUND - this->credential_manager.set_trusted_fingerprint(options.col()); + this->credential_manager->set_trusted_fingerprint(options.col()); # endif if (Config::get("fixed_irc_server", "").empty() && !options.col().empty()) @@ -287,7 +289,10 @@ void IrcClient::on_connected() this->send_gateway_message("Connected to IRC server"s + (this->use_tls ? " (encrypted)": "") + "."); + this->capabilities["batch"] = {[]{}, []{}}; + this->capabilities["message-tags"] = {[]{}, []{}}; this->capabilities["multi-prefix"] = {[]{}, []{}}; + this->capabilities["draft/multiline"] = {[]{}, []{}}; #ifdef USE_DATABASE auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), @@ -318,10 +323,16 @@ void IrcClient::on_connected() } #endif - { - for (const auto &pair : this->capabilities) - this->send_message({ "CAP", {"REQ", pair.first}}); - } + this->send_message({ "CAP", {"LS", "302"}}); + + // If we never get a response from CAP LS start requesting things blindly + TimedEventsManager::instance().add_event(TimedEvent( + std::chrono::steady_clock::now() + 10s, + [this]() { + for (const auto &pair : this->capabilities) + this->send_message({"CAP", {"REQ", pair.first}}); + }, + "cap_fallback_" + this->get_unique_identifier())); this->send_nick_command(this->current_nick); #ifdef USE_DATABASE @@ -416,12 +427,34 @@ void IrcClient::parse_in_buffer(const size_t) "”: ", args_size); else { - const auto& cb = it->second.first; - try { - (this->*(cb))(message); - } catch (const std::exception& e) { - log_error("Unhandled exception: ", e.what()); - } + bool added_to_batch = false; + if (this->has_capability("batch")) + { + auto it_tags = message.tags.find("batch"); + if (it_tags != message.tags.end()) + if (it_tags->second.has_value()) + { + auto it_batches = this->batches.find(it_tags->second.value()); + if (it_batches != this->batches.end()) + { + auto message_tags = message.tags; + auto message_prefix = message.prefix; + auto message_command = message.command; + auto message_arguments = message.arguments; + it_batches->second.second.emplace_back(std::move(message_tags), std::move(message_prefix), std::move(message_command), std::move(message_arguments)); + added_to_batch = true; + } + } + } + if (!added_to_batch) + { + const auto& cb = it->second.first; + try { + (this->*(cb))(message); + } catch (const std::exception& e) { + log_error("Unhandled exception: ", e.what()); + } + } } } else @@ -441,6 +474,16 @@ void IrcClient::actual_send(std::pair&& message_pai const MessageCallback& callback = message_pair.second; log_debug("IRC SENDING: (", this->get_hostname(), ") ", message); std::string res; + if (!message.tags.empty()) + { + res += "@"; + for (auto& tag: message.tags) + if (tag.second.has_value()) + res += tag.first + "=" + tag.second.value() + ";"; + else + res += tag.first + ";"; + res.back() = ' '; + } if (!message.prefix.empty()) res += ":" + message.prefix + " "; res += message.command; @@ -493,7 +536,7 @@ void IrcClient::send_pass_command(const std::string& password) void IrcClient::send_webirc_command(const std::string& password, const std::string& user_ip) { - this->send_message(IrcMessage("WEBIRC", {password, "biboumi", this->user_hostname, user_ip})); + this->send_message(IrcMessage("WEBIRC", {password, "biboumi", this->user_hostname, user_ip, "secure"})); } void IrcClient::send_kick_command(const std::string& chan_name, const std::string& target, const std::string& reason) @@ -606,6 +649,22 @@ void IrcClient::send_ping_command() this->send_message(IrcMessage("PING", {"biboumi"})); } +void IrcClient::send_batch(const std::string& reference_tag, const std::string& type, const std::vector& parameters, std::vector& messages, MessageCallback callback) +{ + std::vector args; + args.push_back("+" + reference_tag); + args.push_back(type); + for (const std::string& parameter: parameters) + args.push_back(parameter); + this->send_message(IrcMessage("BATCH", std::move(args))); + for (IrcMessage& message: messages) + { + message.tags["batch"] = reference_tag; + this->send_message(std::move(message)); + } + this->send_message(IrcMessage("BATCH", {"-" + reference_tag}), callback); +} + void IrcClient::forward_server_message(const IrcMessage& message) { const std::string from = message.prefix; @@ -1340,6 +1399,30 @@ long int IrcClient::get_throttle_limit() const void IrcClient::on_cap(const IrcMessage &message) { const auto& sub_command = message.arguments[1]; + if (sub_command == "LS") { + /** + * CAP LS 302 can return either a multiline response or a one line response + * - CAP * LS * :multiline + * - CAP * LS :last line + */ + const bool last_cap_ls = message.arguments[2] != "*"; + const auto caps = + last_cap_ls ? message.arguments[2] : message.arguments[3]; + auto caps_parsed = parse_caps(caps); + this->cap_ls_response.merge(caps_parsed); + if (last_cap_ls) { + // The CAP LS response has finished. Cancel the fallback + TimedEventsManager::instance().cancel("cap_fallback_" + this->get_unique_identifier()); + std::erase_if(this->capabilities, [this](auto &pair) { + if (this->cap_ls_response.contains(pair.first)) { + this->send_message({"CAP", {"REQ", pair.first}}); + return false; + } else + return true; + }); + } + return; + } const auto& caps = utils::split(message.arguments[2], ' ', false); for (const auto& cap: caps) { @@ -1351,11 +1434,16 @@ void IrcClient::on_cap(const IrcMessage &message) } Capability& capability = it->second; if (sub_command == "ACK") - capability.on_ack(); + { + capability.on_ack(); + this->enabled_capabilities.insert(it->first); + } else if (sub_command == "NACK") - capability.on_nack(); + capability.on_nack(); this->capabilities.erase(it); } + for (const auto& batch: this->batches) + log_debug("Batch: ", batch.first, " exists"); if (this->capabilities.empty()) this->cap_end(); } @@ -1419,3 +1507,95 @@ void IrcClient::cap_end() this->send_message({"CAP", {"END"}}); this->bridge.on_irc_client_connected(this->get_hostname()); } + +bool IrcClient::has_capability(const std::string& capability) const +{ + auto it = this->enabled_capabilities.find(capability); + return it != this->enabled_capabilities.end(); +} + + +const std::optional IrcClient::get_capability_value(const std::string& capability) const +{ + return this->cap_ls_response.at(capability); +} + +void IrcClient::on_batch(const IrcMessage& message) +{ + if (message.arguments[0][0] == '+') + { + if (message.arguments.size() < 2) + return; + if (message.arguments[1] == "draft/multiline" && message.arguments.size() != 3) + return; + this->batches.emplace(message.arguments[0].substr(1), std::make_pair(std::make_pair(message.prefix, message.arguments), std::vector())); + } + else if (message.arguments[0][0] == '-') + { + const std::string reference_tag = message.arguments[0].substr(1); + this->process_batch(reference_tag); + this->batches.erase(reference_tag); + } + return; +} + +void IrcClient::process_batch(const std::string& reference_tag) +{ + auto it = this->batches.find(reference_tag); + if (it == this->batches.end()) + return; + if (it->second.first.second[1] == "draft/multiline") + { + const IrcUser user(it->second.first.first); + const std::string nick = user.nick; + Iid iid; + iid.set_local(it->second.first.second[2]); + iid.set_server(this->hostname); + bool muc = true; + if (!this->get_channel(iid.get_local())->joined) + { + iid.type = Iid::Type::User; + iid.set_local(nick); + muc = false; + } + else + iid.type = Iid::Type::Channel; + + std::string body; + for (const IrcMessage& message: it->second.second) + { + auto it_tags = message.tags.find("draft/multiline-concat"); + if (!body.empty() && it_tags == message.tags.end()) + body += "\n" + message.arguments[1]; + else + body += message.arguments[1]; + } + if (body.find_first_not_of("\n") == std::string::npos) + // Empty message + return; + this->bridge.send_message(iid, nick, body, muc); + } + else + { + for (const IrcMessage& message: it->second.second) + { + auto it_callback = irc_callbacks.find(message.command); + if (it_callback == irc_callbacks.end()) + continue; + const auto& cb = it_callback->second.first; + try + { + (this->*(cb))(message); + } + catch (const std::exception& e) + { + log_error("Unhandled exception: ", e.what()); + } + } + } + return; +} + +const std::string IrcClient::get_unique_identifier() const { + return this->bridge.get_jid() + this->hostname; +} diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp index 3423228..76d6b05 100644 --- a/src/irc/irc_client.hpp +++ b/src/irc/irc_client.hpp @@ -26,6 +26,7 @@ #include #include #include +#include class IrcClient; @@ -155,6 +156,10 @@ public: * Send the QUIT irc command */ void send_quit_command(const std::string& reason); + /** + * Send a batch of messages + */ + void send_batch(const std::string& reference_tag, const std::string& type, const std::vector& parameters, std::vector& messages, MessageCallback callback); /** * Send a message to the gateway user, not generated by the IRC server, * but that might be useful because we want to be verbose (for example we @@ -244,6 +249,8 @@ public: void on_cap(const IrcMessage& message); private: void cap_end(); + void process_batch(const std::string& reference_tag); + const std::string get_unique_identifier() const; public: #ifdef WITH_SASL void on_authenticate(const IrcMessage& message); @@ -306,6 +313,7 @@ public: */ void on_channel_mode(const IrcMessage& message); void on_quit(const IrcMessage& message); + void on_batch(const IrcMessage& message); void on_unknown_message(const IrcMessage& message); /** * Return the number of joined channels @@ -326,6 +334,9 @@ public: * Store the history limit that the client asked when joining this room. */ HistoryLimit history_limit; + + bool has_capability(const std::string& capability) const; + const std::optional get_capability_value(const std::string& capability) const; private: /** * The hostname of the server we are connected to. @@ -386,7 +397,9 @@ private: */ SaslState sasl_state{SaslState::unneeded}; #endif + std::unordered_map> cap_ls_response; std::map capabilities; + std::unordered_set enabled_capabilities; /** * See http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt section 3.3 * We store the possible chanmodes in this object. @@ -431,6 +444,7 @@ private: Resolver dns_resolver; TokensBucket tokens_bucket; long int get_throttle_limit() const; + std::unordered_map>, std::vector>> batches; }; diff --git a/src/irc/irc_message.cpp b/src/irc/irc_message.cpp index 62fe9a7..c056a8c 100644 --- a/src/irc/irc_message.cpp +++ b/src/irc/irc_message.cpp @@ -1,8 +1,27 @@ #include #include +#include +#include +#include IrcMessage::IrcMessage(std::stringstream ss) { + if (ss.peek() == '@') + { + std::string raw_tags; + ss.ignore(); + ss >> raw_tags; + std::vector tags = utils::split(raw_tags, ';', false); + for (auto &tag: tags) + { + auto pos = tag.find('='); + if (pos == std::string::npos) + this->tags[tag] = {}; + else + this->tags[tag.substr(0, pos)] = tag.substr(pos+1); + } + ss.ignore(); + } if (ss.peek() == ':') { ss.ignore(); @@ -30,6 +49,7 @@ IrcMessage::IrcMessage(std::stringstream ss) IrcMessage::IrcMessage(std::string&& prefix, std::string&& command, std::vector&& args): + tags(), prefix(std::move(prefix)), command(std::move(command)), arguments(std::move(args)) @@ -38,16 +58,36 @@ IrcMessage::IrcMessage(std::string&& prefix, IrcMessage::IrcMessage(std::string&& command, std::vector&& args): + tags(), prefix(), command(std::move(command)), arguments(std::move(args)) { } +IrcMessage::IrcMessage(std::unordered_map>&& tags, + std::string&& prefix, + std::string&& command, + std::vector&& args): + tags(std::move(tags)), + prefix(std::move(prefix)), + command(std::move(command)), + arguments(std::move(args)) +{ +} + std::ostream& operator<<(std::ostream& os, const IrcMessage& message) { os << "IrcMessage"; - os << "[" << message.command << "]"; + os << "{"; + for (auto& tag: message.tags) + { + os << tag.first; + if (tag.second.has_value()) + os << "=" << tag.second.value(); + os << ";"; + } + os << "}[" << message.command << "]"; for (const std::string& arg: message.arguments) { os << "{" << arg << "}"; diff --git a/src/irc/irc_message.hpp b/src/irc/irc_message.hpp index 5475fd2..17d1c39 100644 --- a/src/irc/irc_message.hpp +++ b/src/irc/irc_message.hpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include class IrcMessage { @@ -13,6 +15,7 @@ public: IrcMessage(std::string str): IrcMessage{std::stringstream{str}} {} IrcMessage(std::string&& prefix, std::string&& command, std::vector&& args); IrcMessage(std::string&& command, std::vector&& args); + IrcMessage(std::unordered_map>&& tags, std::string&& prefix, std::string&& command, std::vector&& args); ~IrcMessage() = default; IrcMessage(const IrcMessage&) = delete; @@ -20,6 +23,7 @@ public: IrcMessage& operator=(const IrcMessage&) = delete; IrcMessage& operator=(IrcMessage&&) = default; + std::unordered_map> tags; std::string prefix; std::string command; std::vector arguments; diff --git a/src/irc/parse_caps.cpp b/src/irc/parse_caps.cpp new file mode 100644 index 0000000..7e46e13 --- /dev/null +++ b/src/irc/parse_caps.cpp @@ -0,0 +1,56 @@ +#include "irc/parse_caps.hpp" + +const std::unordered_map> +parse_caps(const std::string &caps) { + std::unordered_map> output; + // caps => multi-prefix sasl=PLAIN,EXTERNAL + // draft/multiline=max-bytes=40000,max-lines=10 + const std::vector &caps_list = utils::split(caps, ' ', false); + for (const std::string &cap : caps_list) { + // cap => multi-prefix + // cap => sasl=PLAIN,EXTERNAL + // cap => draft/multiline=max-bytes=40000,max-lines=10 + const size_t delimiter = cap.find("="); + // delimiter(multi-prefix) => npos + // delimiter(sasl=PLAIN,EXTERNAL) => 5 + // delimiter(draft/multiline=max-bytes=40000,max-lines=10) => 16 + const std::string cap_name = cap.substr(0, delimiter); + std::optional cap_value; + if (delimiter != std::string::npos) + cap_value = cap.substr(delimiter + 1); + output.insert_or_assign(cap_name, cap_value); + } + + return output; +} + +const std::vector parse_cap_val_list(const std::string &value) { + // value(PLAIN,EXTERNAL) => PLAIN,EXTERNAL + return utils::split(value, ',', false); +} + +const std::unordered_map> +parse_cap_keyval(const std::string &value) { + std::unordered_map> output; + // value(PLAIN,EXTERNAL) => PLAIN,EXTERNAL + // value(max-bytes=40000,max-lines=10) => + // max-bytes=40000,max-lines=10 + const std::vector &values_list = utils::split(value, ',', false); + // values_list(PLAIN,EXTERNAL) => { "PLAIN", "EXTERNAL" } + // values_list(draft/multiline=max-bytes=40000,max-lines=10) => { + // "max-bytes=40000", "max-lines=10" } + for (const std::string &value : values_list) { + const size_t value_delimiter = value.find("="); + // key(PLAIN) => PLAIN + // key(max-bytes=40000) => max-bytes + const std::string key = value.substr(0, value_delimiter); + std::optional val; + if (value_delimiter != std::string::npos) { + // val(max-bytes=40000) => 40000 + val = value.substr(value_delimiter + 1); + } + output.insert_or_assign(key, val); + } + + return output; +} diff --git a/src/irc/parse_caps.hpp b/src/irc/parse_caps.hpp new file mode 100644 index 0000000..2899e53 --- /dev/null +++ b/src/irc/parse_caps.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "utils/split.hpp" +#include +#include +#include +#include +#include + +/** + * Parse a list of IRC caps into a map of capabilities + */ +const std::unordered_map> +parse_caps(const std::string &caps); + +/** + * Parse the value of a cap into a list of values + * + * Used for the sasl cap + */ +const std::vector parse_cap_val_list(const std::string &value); + +/** + * Parse the value of a cap into a key-value map + * + * Used for the multiline cap + */ +const std::unordered_map> +parse_cap_keyval(const std::string &value); diff --git a/src/main.cpp b/src/main.cpp index 20bf9fd..9365499 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,6 +14,7 @@ #include #include +#include // A flag set by the SIGINT signal handler. static std::atomic stop(false); @@ -107,6 +108,7 @@ static int main_loop(std::string hostname, std::string password) if (Config::get_int("identd_port", 113) != 0) identd = std::make_unique(*xmpp_component, p, static_cast(Config::get_int("identd_port", 113))); + Scheduler::start_scheduler(); auto timeout = TimedEventsManager::instance().get_timeout(); while (p->poll(timeout) != -1) { @@ -130,6 +132,7 @@ static int main_loop(std::string hostname, std::string password) #endif if (identd) identd->shutdown(); + Scheduler::stop_scheduler(); // Cancel the timer for a potential reconnection TimedEventsManager::instance().cancel("XMPP reconnection"); } diff --git a/src/network/credentials_manager.cpp b/src/network/credentials_manager.cpp index 47f8514..a692b43 100644 --- a/src/network/credentials_manager.cpp +++ b/src/network/credentials_manager.cpp @@ -39,7 +39,7 @@ const std::string& BasicCredentialsManager::get_trusted_fingerprint() const } void check_tls_certificate(const std::vector& certs, - const std::string& hostname, const std::string& trusted_fingerprint, + const std::string_view hostname, const std::string& trusted_fingerprint, const std::exception_ptr& exc) { diff --git a/src/network/credentials_manager.hpp b/src/network/credentials_manager.hpp index 210a628..4b87d67 100644 --- a/src/network/credentials_manager.hpp +++ b/src/network/credentials_manager.hpp @@ -19,7 +19,7 @@ class TCPSocketHandler; * on a trusted fingerprint. */ void check_tls_certificate(const std::vector& certs, - const std::string& hostname, const std::string& trusted_fingerprint, + const std::string_view hostname, const std::string& trusted_fingerprint, const std::exception_ptr& exc); class BasicCredentialsManager: public Botan::Credentials_Manager diff --git a/src/network/tcp_socket_handler.cpp b/src/network/tcp_socket_handler.cpp index 5e4ae30..25adad4 100644 --- a/src/network/tcp_socket_handler.cpp +++ b/src/network/tcp_socket_handler.cpp @@ -16,23 +16,29 @@ # include # include # include +# include +# include # include # include namespace { - Botan::AutoSeeded_RNG& get_rng() + std::shared_ptr& get_rng() { - static Botan::AutoSeeded_RNG rng{}; + static std::shared_ptr rng; + + if (!rng) + rng = std::make_shared(); return rng; } - Botan::TLS::Session_Manager_In_Memory& get_session_manager() + std::shared_ptr& get_session_manager(std::shared_ptr creds) { - static Botan::TLS::Session_Manager_In_Memory session_manager{get_rng()}; -#if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(2,4,0) - // workaround for https://github.com/randombit/botan/issues/1276 - session_manager.remove_all(); -#endif + static std::shared_ptr session_manager; + + if (!session_manager) + session_manager = std::make_shared( + std::make_unique(get_rng()), + creds, get_rng()); return session_manager; } } @@ -50,7 +56,8 @@ TCPSocketHandler::TCPSocketHandler(std::shared_ptr& poller): SocketHandler(poller, -1), use_tls(false) #ifdef BOTAN_FOUND - ,credential_manager() + ,credential_manager(std::make_shared()) + ,policy(std::make_shared()) #endif {} @@ -242,11 +249,11 @@ void TCPSocketHandler::start_tls(const std::string& address, const std::string& auto policy_directory = Config::get("policy_directory", utils::dirname(Config::get_filename())); if (!policy_directory.empty() && policy_directory[policy_directory.size()-1] != '/') policy_directory += '/'; - this->policy.load(policy_directory + "policy.txt"); - this->policy.load(policy_directory + address + ".policy.txt"); + this->policy->load(policy_directory + "policy.txt"); + this->policy->load(policy_directory + address + ".policy.txt"); this->tls = std::make_unique( - *this, - get_session_manager(), this->credential_manager, this->policy, + shared_from_this(), + get_session_manager(this->credential_manager), this->credential_manager, this->policy, get_rng(), server_info, Botan::TLS::Protocol_Version::latest_tls_version()); } @@ -298,17 +305,16 @@ void TCPSocketHandler::tls_send(std::string&& data) std::make_move_iterator(data.end())); } -void TCPSocketHandler::tls_record_received(uint64_t, const Botan::byte *data, size_t size) +void TCPSocketHandler::tls_record_received(uint64_t, std::span data) { - this->in_buf += std::string(reinterpret_cast(data), - size); + this->in_buf += std::string(data.begin(), data.end()); if (!this->in_buf.empty()) - this->parse_in_buffer(size); + this->parse_in_buffer(data.size()); } -void TCPSocketHandler::tls_emit_data(const Botan::byte *data, size_t size) +void TCPSocketHandler::tls_emit_data(std::span data) { - this->raw_send(std::string(reinterpret_cast(data), size)); + this->raw_send(std::string(data.begin(), data.end())); } void TCPSocketHandler::tls_alert(Botan::TLS::Alert alert) @@ -316,25 +322,24 @@ void TCPSocketHandler::tls_alert(Botan::TLS::Alert alert) log_debug("tls_alert: ", alert.type_string()); } -bool TCPSocketHandler::tls_session_established(const Botan::TLS::Session& session) +void TCPSocketHandler::tls_session_established(const Botan::TLS::Session_Summary& session) { log_debug("Handshake with ", session.server_info().hostname(), " complete.", " Version: ", session.version().to_string(), " using ", session.ciphersuite().to_string()); if (!session.session_id().empty()) log_debug("Session ID ", Botan::hex_encode(session.session_id())); - if (!session.session_ticket().empty()) - log_debug("Session ticket ", Botan::hex_encode(session.session_ticket())); - return true; + if (session.session_ticket().has_value()) + log_debug("Session ticket ", Botan::hex_encode(*session.session_ticket())); } void TCPSocketHandler::tls_verify_cert_chain(const std::vector& cert_chain, - const std::vector>& ocsp_responses, + const std::vector>& ocsp_responses, const std::vector& trusted_roots, - Botan::Usage_Type usage, const std::string& hostname, + Botan::Usage_Type usage, std::string_view hostname, const Botan::TLS::Policy& policy) { - if (!this->policy.verify_certificate) + if (!this->policy->verify_certificate) { log_debug("Not verifying certificate due to domain policy "); return; @@ -352,7 +357,7 @@ void TCPSocketHandler::tls_verify_cert_chain(const std::vectorabort_on_invalid_cert()) exception_ptr = std::current_exception(); - check_tls_certificate(cert_chain, hostname, this->credential_manager.get_trusted_fingerprint(), exception_ptr); + check_tls_certificate(cert_chain, hostname, this->credential_manager->get_trusted_fingerprint(), exception_ptr); } } diff --git a/src/network/tcp_socket_handler.hpp b/src/network/tcp_socket_handler.hpp index b12ae57..3c6f346 100644 --- a/src/network/tcp_socket_handler.hpp +++ b/src/network/tcp_socket_handler.hpp @@ -30,6 +30,7 @@ * But doesn’t do any connect() or accept() or anything else. */ class TCPSocketHandler: public SocketHandler + ,public std::enable_shared_from_this #ifdef BOTAN_FOUND ,public Botan::TLS::Callbacks #endif @@ -137,12 +138,12 @@ private: * Called by the tls object that some data has been decrypt. We call * parse_in_buffer() to handle that unencrypted data. */ - void tls_record_received(uint64_t rec_no, const Botan::byte* data, size_t size) override final; + void tls_record_received(uint64_t rec_no, std::span data) override final; /** * Called by the tls object to indicate that some data has been encrypted * and is now ready to be sent on the socket as is. */ - void tls_emit_data(const Botan::byte* data, size_t size) override final; + void tls_emit_data(std::span data) override final; /** * Called by the tls object to indicate that a TLS alert has been * received. We don’t use it, we just log some message, at the moment. @@ -152,13 +153,13 @@ private: * Called by the tls object at the end of the TLS handshake. We don't do * anything here appart from logging the TLS session information. */ - bool tls_session_established(const Botan::TLS::Session& session) override final; + void tls_session_established(const Botan::TLS::Session_Summary& session) override final; void tls_verify_cert_chain(const std::vector& cert_chain, - const std::vector>& ocsp_responses, + const std::vector>& ocsp_responses, const std::vector& trusted_roots, Botan::Usage_Type usage, - const std::string& hostname, + std::string_view hostname, const Botan::TLS::Policy& policy) override final; /** * Called whenever the tls session goes from inactive to active. This @@ -203,9 +204,9 @@ protected: #ifdef BOTAN_FOUND protected: - BasicCredentialsManager credential_manager; + std::shared_ptr credential_manager; private: - BiboumiTLSPolicy policy; + std::shared_ptr policy; /** * We use a unique_ptr because we may not want to create the object at * all. The Botan::TLS::Client object generates a handshake message and diff --git a/src/network/tls_policy.cpp b/src/network/tls_policy.cpp index f32557e..20699cf 100644 --- a/src/network/tls_policy.cpp +++ b/src/network/tls_policy.cpp @@ -8,7 +8,6 @@ #include #include -#include #include bool BiboumiTLSPolicy::load(const std::string& filename) @@ -31,7 +30,7 @@ bool BiboumiTLSPolicy::load(const std::string& filename) void BiboumiTLSPolicy::load(std::istream& is) { - const auto dict = Botan::read_cfg(is); + const auto dict = BiboumiTLSPolicy::read_cfg(is); for (const auto& pair: dict) { // Workaround for options that are not overridden in Botan::TLS::Text_Policy @@ -49,4 +48,56 @@ bool BiboumiTLSPolicy::require_cert_revocation_info() const return this->req_cert_revocation_info; } +std::map BiboumiTLSPolicy::read_cfg(std::istream& is) { + std::map kv; + size_t line = 0; + + while(is.good()) { + std::string s; + + std::getline(is, s); + + ++line; + + if(s.empty() || s[0] == '#') { + continue; + } + + s = clean_ws(s.substr(0, s.find('#'))); + + if(s.empty()) { + continue; + } + + auto eq = s.find('='); + + if(eq == std::string::npos || eq == 0 || eq == s.size() - 1) { + throw Botan::Decoding_Error("Bad read_cfg input '" + s + "' on line " + std::to_string(line)); + } + + const std::string key = BiboumiTLSPolicy::clean_ws(s.substr(0, eq)); + const std::string val = BiboumiTLSPolicy::clean_ws(s.substr(eq + 1, std::string::npos)); + + kv[key] = val; + } + + return kv; +} + +std::string BiboumiTLSPolicy::clean_ws(std::string_view s) { + const char* ws = " \t\n"; + auto start = s.find_first_not_of(ws); + auto end = s.find_last_not_of(ws); + + if(start == std::string::npos) { + return ""; + } + + if(end == std::string::npos) { + return std::string(s.substr(start, end)); + } else { + return std::string(s.substr(start, start + end + 1)); + } +} + #endif diff --git a/src/network/tls_policy.hpp b/src/network/tls_policy.hpp index e915646..052dd9c 100644 --- a/src/network/tls_policy.hpp +++ b/src/network/tls_policy.hpp @@ -24,6 +24,12 @@ public: bool verify_certificate{true}; protected: bool req_cert_revocation_info{true}; +private: + // Copied from botan-3's src/lib/utils/read_cfg.cpp + // These were made internal in v3, but they are in use by biboumi so they have + // to be vendored in + static std::map read_cfg(std::istream& is); + static std::string clean_ws(std::string_view s); }; #endif diff --git a/src/scheduler/scheduler.cpp b/src/scheduler/scheduler.cpp new file mode 100644 index 0000000..aee2503 --- /dev/null +++ b/src/scheduler/scheduler.cpp @@ -0,0 +1,107 @@ +#include "biboumi.h" +#include +#include +#include +#include +#include + +#ifdef USE_DATABASE +#include +#include +#endif + +static const std::unordered_map< + std::string, std::pair>> + events = { +#ifdef USE_DATABASE + {"cleanup_old_messages", + {std::chrono::seconds(10min), &Scheduler::cleanup_old_messages}}, + {"optimize_db", + {std::chrono::seconds(24h), &Scheduler::optimize_db}}, +#endif // USE_DATABASE +}; + +void Scheduler::start_scheduler() { + for (const auto &job : events) { + const std::string name = job.first; + const std::chrono::seconds interval = job.second.first; + const std::function job_function = job.second.second; + // Run it once at startup + job_function(); + // Then every interval + TimedEventsManager::instance().add_event( + TimedEvent(interval, job_function, name)); + } +} + +void Scheduler::stop_scheduler() { + for (const auto &job : events) { + const std::string name = job.first; + TimedEventsManager::instance().cancel(name); + } +} + +#ifdef USE_DATABASE +void Scheduler::cleanup_old_messages() { + // Get the config value + const std::string archive_expires_after = + Config::get("archive_expires_after", "0"); + const std::optional seconds = + utils::parse_duration(archive_expires_after); + // If it's actually set and relevant, start cleanup + if (seconds.has_value() && seconds.value() != 0) { + // Calculate age of messages to be deleted + log_debug("[cleanup_old_messages] Deleting messages older than ", + archive_expires_after, " (", seconds.value(), " seconds)"); + + // Convert the config value to a duration + const auto duration = std::chrono::duration(seconds.value()); + // Get timestamp now relative to unix epoch + const auto now = std::chrono::system_clock::now(); + // Older than this get deleted + const auto ago = now - duration; + // Convert to seconds since epoch + const auto since_epoch = std::chrono::duration_cast( + ago.time_since_epoch()); + Database::delete_old_muclines(since_epoch.count()); + } else + log_debug("[cleanup_old_messages] Not deleting anything. " + "archive_expires_after is ", + archive_expires_after); +} + +void Scheduler::optimize_db() { + log_debug("[optimize_db] Starting. Type: ", (int)Database::db->get_type()); + switch (Database::db->get_type()) { +# ifdef SQLITE3_FOUND + case DatabaseType::sqlite3: { + log_debug("[optimize_db] Optimizing Sqlite database"); + // Run PRAGMA optimize periodically, as recommended by sqlite + // https://sqlite.org/lang_analyze.html#automatically_running_analyze + auto [success, err_msg] = Database::db->raw_exec("PRAGMA optimize"); + if (!success) { + log_error("[optimize_db] Failed optimizing Sqlite database: ", err_msg); + } + break; + } +# endif // SQLITE3_FOUND +# ifdef PQ_FOUND + case DatabaseType::postgresql: { + log_debug("[optimize_db] Optimizing PostgreSQL database"); + // Run VACUUM and ANALYZE periodically + // https://www.postgresql.org/docs/current/sql-vacuum.html + // https://www.postgresql.org/docs/current/sql-analyze.html + auto [success, err_msg] = Database::db->raw_exec("VACUUM ANALYZE"); + if (!success) { + log_error("[optimize_db] Failed optimizing PostgreSQL database: ", + err_msg); + } + break; + } +# endif // PQ_FOUND + case DatabaseType::none: + default: + break; + } +} +#endif // USE_DATABASE diff --git a/src/scheduler/scheduler.hpp b/src/scheduler/scheduler.hpp new file mode 100644 index 0000000..63e56cc --- /dev/null +++ b/src/scheduler/scheduler.hpp @@ -0,0 +1,11 @@ +#pragma once + +class Scheduler { +public: + static void start_scheduler(); + static void stop_scheduler(); + + // Jobs + static void cleanup_old_messages(); + static void optimize_db(); +}; diff --git a/src/utils/chunk.hpp b/src/utils/chunk.hpp new file mode 100644 index 0000000..f6eb3ef --- /dev/null +++ b/src/utils/chunk.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace utils { +template +inline std::vector> chunk(const std::vector &vector, + const std::size_t size) { + std::vector> output; + std::vector current_chunk; + for (const auto &el : vector) { + current_chunk.push_back(el); + if (current_chunk.size() >= size) { + output.push_back(current_chunk); + current_chunk = {}; + } + } + if (current_chunk.size() != 0) + output.push_back(current_chunk); + + return output; +} + +} // namespace utils diff --git a/src/utils/is_requester_allowed_to_persist.hpp b/src/utils/is_requester_allowed_to_persist.hpp new file mode 100644 index 0000000..b81f3d3 --- /dev/null +++ b/src/utils/is_requester_allowed_to_persist.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "xmpp/jid.hpp" +#include + +namespace utils +{ +/** + * Check config if a given requester is allowed to persist target + * + * The following values are checked + * + * - A user's jid (foo@example.com) + * - A user's domain (example.com) + * + * The following config options are checked in order + * + * 1. `persist_user_denylist` contains the requester's jid + * 2. `persist_user_allowlist` contains the requester's jid + * 3. `persist_user_denylist` contains the requester's domain + * 4. `persist_user_allowlist` contains the contains the requester's domain + * 5. `default_persistence_allowed` is true (default true) + */ +inline bool is_requester_allowed_to_persist(const Jid &requester) { + // requester.bare checks + // deny + // 1. + bool jid_in_user_denylist = Config::is_in_list("persist_user_denylist", requester.bare()); + if (jid_in_user_denylist) + return false; + + // allow + // 2. + bool jid_in_user_allowlist = Config::is_in_list("persist_user_allowlist", requester.bare()); + if (jid_in_user_allowlist) + return true; + + // requester.domain checks + // deny + // 3. + bool domain_in_user_denylist = Config::is_in_list("persist_user_denylist", requester.domain); + if (domain_in_user_denylist) + return false; + + // allow + // 4. + bool domain_in_user_allowlist = Config::is_in_list("persist_user_allowlist", requester.domain); + if (domain_in_user_allowlist) + return true; + + // default allow + // 5. + return Config::get_bool("default_persistence_allowed", true); +} +} diff --git a/src/utils/parse_duration.cpp b/src/utils/parse_duration.cpp new file mode 100644 index 0000000..371c4c4 --- /dev/null +++ b/src/utils/parse_duration.cpp @@ -0,0 +1,36 @@ +#include +#include +#include +#include +#include +#include + +namespace utils { + +// Strongly inspired from prosody +// https://hg.prosody.im/trunk/file/0ff11f2e87cd/util/human/io.lua#l214 +static const long day = 86400; + +static const std::unordered_map multipliers = { + {"d", day}, {"w", day * 7}, {"mon", day * 31}, {"y", day * 365.2425}, + {"s", 1}, {"min", 60}, {"h", 3600}, {"ho", 3600}, +}; + +std::optional parse_duration(const std::string &duration) { + std::size_t ptr; + long number; + try { + number = std::stol(duration, &ptr, 10); + } catch (std::invalid_argument &) { + return {}; + } + const std::string m = duration.substr(ptr); + + if (!multipliers.contains(m)) + return {}; + + const long multiplier = multipliers.at(m); + + return multiplier * number; +} +} // namespace utils diff --git a/src/utils/parse_duration.hpp b/src/utils/parse_duration.hpp new file mode 100644 index 0000000..b0f9b21 --- /dev/null +++ b/src/utils/parse_duration.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +namespace utils { +std::optional parse_duration(const std::string &duration); +} // namespace utils diff --git a/src/utils/sha1.cpp b/src/utils/sha1.cpp index 1a0d185..47378c9 100644 --- a/src/utils/sha1.cpp +++ b/src/utils/sha1.cpp @@ -13,6 +13,7 @@ # include # include # include +# include #endif std::string sha1(const std::string& input) diff --git a/src/utils/timed_events.hpp b/src/utils/timed_events.hpp index fa0fc50..0ab3246 100644 --- a/src/utils/timed_events.hpp +++ b/src/utils/timed_events.hpp @@ -22,7 +22,7 @@ class TimedEvent friend class TimedEventsManager; public: /** - * An event the occurs only once, at the given time_point + * An event that occurs only once, at the given time_point */ explicit TimedEvent(std::chrono::steady_clock::time_point&& time_point, std::function callback, std::string name=""); diff --git a/src/xmpp/adhoc_command.cpp b/src/xmpp/adhoc_command.cpp index fbf4ce2..f8c8e4f 100644 --- a/src/xmpp/adhoc_command.cpp +++ b/src/xmpp/adhoc_command.cpp @@ -26,7 +26,7 @@ void PingStep1(XmppComponent&, AdhocSession&, XmlNode& command_node) void HelloStep1(XmppComponent&, AdhocSession&, XmlNode& command_node) { - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Configure your name."); @@ -65,9 +65,9 @@ void HelloStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node) } } command_node.delete_all_children(); - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); session.terminate(); } diff --git a/src/xmpp/adhoc_commands_handler.cpp b/src/xmpp/adhoc_commands_handler.cpp index ff4c1e5..7a84b2e 100644 --- a/src/xmpp/adhoc_commands_handler.cpp +++ b/src/xmpp/adhoc_commands_handler.cpp @@ -36,16 +36,16 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co auto command_it = this->commands.find(node); if (command_it == this->commands.end()) { - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "cancel"; - XmlSubNode condition(error, STANZA_NS":item-not-found"); + XmlSubNode condition(error, STANZA_NS, "item-not-found"); } else if (command_it->second.is_admin_only() && !Config::is_in_list("admin", jid.bare())) { - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "cancel"; - XmlSubNode condition(error, STANZA_NS":forbidden"); + XmlSubNode condition(error, STANZA_NS, "forbidden"); } else { @@ -94,9 +94,9 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co } else // unsupported action { - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); } } return command_node; diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp index 792955c..f1e612a 100644 --- a/src/xmpp/biboumi_adhoc_commands.cpp +++ b/src/xmpp/biboumi_adhoc_commands.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -34,7 +35,7 @@ void DisconnectUserStep1(XmppComponent& xmpp_component, AdhocSession&, XmlNode& { auto& biboumi_component = dynamic_cast(xmpp_component); - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Disconnect a user from the gateway"); @@ -108,9 +109,9 @@ void DisconnectUserStep2(XmppComponent& xmpp_component, AdhocSession& session, X return; } } - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); session.terminate(); } @@ -124,7 +125,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman auto options = Database::get_global_options(owner.bare()); command_node.delete_all_children(); - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Configure some global default settings."); @@ -159,6 +160,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman } } + if (utils::is_requester_allowed_to_persist(Jid(session.get_owner_jid()))) { XmlSubNode persistent(x, "field"); persistent["var"] = "persistent"; @@ -208,7 +210,8 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session, bridge->set_record_history(options.col()); } else if (field->get_tag("var") == "persistent" && - value) + value + && utils::is_requester_allowed_to_persist(Jid(session.get_owner_jid()))) options.col() = to_bool(value->get_inner()); } @@ -220,9 +223,9 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session, note.set_inner("Configuration successfully applied."); return; } - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); session.terminate(); } @@ -238,7 +241,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com auto commands = Database::get_after_connection_commands(options); command_node.delete_all_children(); - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Configure the IRC server " + server_domain); @@ -263,7 +266,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com XmlSubNode ports(x, "field"); ports["var"] = "ports"; ports["type"] = "text-multi"; - ports["label"] = "Ports"; + ports["label"] = "Unencrypted ports"; set_desc(ports, "List of ports to try, without TLS. Defaults: 6667."); for (const auto& val: utils::split(options.col(), ';', false)) { @@ -565,9 +568,9 @@ void ConfigureIrcServerStep2(XmppComponent& xmpp_component, AdhocSession& sessio note.set_inner("Configuration successfully applied."); return; } - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); session.terminate(); } @@ -586,7 +589,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, auto options = Database::get_irc_channel_options_with_server_default(requester.local + "@" + requester.domain, iid.get_server(), iid.get_local()); node.delete_all_children(); - XmlSubNode x(node, "jabber:x:data:x"); + XmlSubNode x(node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Configure the IRC channel " + iid.get_local() + " on server " + iid.get_server()); @@ -640,6 +643,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, } } + if (utils::is_requester_allowed_to_persist(requester)) { XmlSubNode persistent(x, "field"); persistent["var"] = "persistent"; @@ -671,9 +675,9 @@ void ConfigureIrcChannelStep2(XmppComponent& xmpp_component, AdhocSession& sessi } else { - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); session.terminate(); } } @@ -699,7 +703,7 @@ bool handle_irc_channel_configuration_form(XmppComponent& xmpp_component, const else if (field->get_tag("var") == "encoding_in" && value) options.col() = value->get_inner(); - else if (field->get_tag("var") == "persistent" && value) + else if (field->get_tag("var") == "persistent" && value && utils::is_requester_allowed_to_persist(requester)) options.col() = to_bool(value->get_inner()); else if (field->get_tag("var") == "record_history" && value && !value->get_inner().empty()) @@ -749,7 +753,7 @@ void DisconnectUserFromServerStep1(XmppComponent& xmpp_component, AdhocSession& { // Send a form to select the user to disconnect auto& biboumi_component = dynamic_cast(xmpp_component); - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Disconnect a user from selected IRC servers"); @@ -794,7 +798,7 @@ void DisconnectUserFromServerStep2(XmppComponent& xmpp_component, AdhocSession& command_node.delete_all_children(); auto& biboumi_component = dynamic_cast(xmpp_component); - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Disconnect a user from selected IRC servers"); @@ -953,3 +957,4 @@ void GetIrcConnectionInfoStep1(XmppComponent& component, AdhocSession& session, message = ss.str(); } + diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp index 6b4da85..60e06bc 100644 --- a/src/xmpp/biboumi_component.cpp +++ b/src/xmpp/biboumi_component.cpp @@ -295,6 +295,15 @@ void BiboumiComponent::handle_message(const Stanza& stanza) error_type, error_name, error_text); }); const XmlNode* body = stanza.get_child("body", COMPONENT_NS); + const XmlNode* replace = stanza.get_child("replace", MESSAGE_CORRECT_NS); + + if (replace) + { + error_type = "cancel"; + error_name = "feature-not-implemented"; + error_text = "This room is mapped from IRC, which does not support message correction."; + return; + } try { // catch IRCNotConnected exceptions if (type == "groupchat" && iid.type == Iid::Type::Channel) @@ -482,7 +491,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) // Depending on the 'to' jid in the request, we use one adhoc // command handler or an other - Iid iid(to.local, {'#', '&'}); + Iid iid(to.local, {'&', '#', '+', '!'}); AdhocCommandsHandler* adhoc_handler; if (to.local.empty()) adhoc_handler = &this->adhoc_commands_handler; @@ -532,7 +541,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) const XmlNode* query; if ((query = stanza.get_child("query", DISCO_INFO_NS))) { // Disco info - Iid iid(to.local, {'#', '&'}); + Iid iid(to.local, {'&', '#', '+', '!'}); const std::string node = query->get_tag("node"); if (to_str == this->served_hostname) { @@ -736,7 +745,7 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza) const XmlNode* query = stanza.get_child("query", MAM_NS); - Iid iid(to.local, {'#', '&'}); + Iid iid(to.local, {'&', '#', '+', '!'}); if (query && iid.type == Iid::Type::Channel && to.resource.empty()) { const std::string query_id = query->get_tag("queryid"); @@ -865,7 +874,7 @@ void BiboumiComponent::send_archived_message(const Database::MucLogLine& log_lin bool BiboumiComponent::handle_room_configuration_form_request(const std::string& from, const Jid& to, const std::string& id) { - Iid iid(to.local, {'#', '&'}); + Iid iid(to.local, {'&', '#', '+', '!'}); if (iid.type != Iid::Type::Channel || !to.resource.empty()) return false; @@ -887,7 +896,7 @@ bool BiboumiComponent::handle_room_configuration_form_request(const std::string& bool BiboumiComponent::handle_room_configuration_form(const XmlNode& query, const std::string &from, const Jid &to, const std::string &id) { - Iid iid(to.local, {'#', '&'}); + Iid iid(to.local, {'&', '#', '+', '!'}); if (iid.type != Iid::Type::Channel || !to.resource.empty()) return false; diff --git a/src/xmpp/xmpp_component.cpp b/src/xmpp/xmpp_component.cpp index de9a7a6..62a98ce 100644 --- a/src/xmpp/xmpp_component.cpp +++ b/src/xmpp/xmpp_component.cpp @@ -175,7 +175,7 @@ void XmppComponent::on_stanza(const Stanza& stanza) void XmppComponent::send_stream_error(const std::string& name, const std::string& explanation) { - Stanza node("stream:error"); + Stanza node("stream", "error"); { XmlSubNode error(node, name); error["xmlns"] = STREAM_NS; diff --git a/src/xmpp/xmpp_component.hpp b/src/xmpp/xmpp_component.hpp index 54ab15c..6bc2892 100644 --- a/src/xmpp/xmpp_component.hpp +++ b/src/xmpp/xmpp_component.hpp @@ -40,6 +40,7 @@ #define STABLE_ID_NS "urn:xmpp:sid:0" #define STABLE_MUC_ID_NS "http://jabber.org/protocol/muc#stable_id" #define SELF_PING_FLAG MUC_NS"#self-ping-optimization" +#define MESSAGE_CORRECT_NS "urn:xmpp:message-correct:0" /** * An XMPP component, communicating with an XMPP server using the protocole diff --git a/src/xmpp/xmpp_parser.cpp b/src/xmpp/xmpp_parser.cpp index 781fe4c..1f25fa6 100644 --- a/src/xmpp/xmpp_parser.cpp +++ b/src/xmpp/xmpp_parser.cpp @@ -38,7 +38,7 @@ XmppParser::XmppParser(): void XmppParser::init_xml_parser() { // Create the expat parser - this->parser = XML_ParserCreateNS("UTF-8", ':'); + this->parser = XML_ParserCreateNS("UTF-8", '\1'); XML_SetUserData(this->parser, static_cast(this)); // Install Expat handlers diff --git a/src/xmpp/xmpp_parser.hpp b/src/xmpp/xmpp_parser.hpp index ec42f9a..1e5e4e5 100644 --- a/src/xmpp/xmpp_parser.hpp +++ b/src/xmpp/xmpp_parser.hpp @@ -18,9 +18,9 @@ * stanza is reasonnably short. * * The element names generated by expat contain the namespace of the - * element, a colon (':') and then the actual name of the element. To get + * element, a \1 separator and then the actual name of the element. To get * an element "x" with a namespace of "http://jabber.org/protocol/muc", you - * just look for an XmlNode named "http://jabber.org/protocol/muc:x" + * just look for an XmlNode named "http://jabber.org/protocol/muc\1x" * * TODO: enforce the size-limit for the stanza (limit the number of childs * it can contain). For example forbid the parser going further than level diff --git a/src/xmpp/xmpp_stanza.cpp b/src/xmpp/xmpp_stanza.cpp index 435f333..0103dd7 100644 --- a/src/xmpp/xmpp_stanza.cpp +++ b/src/xmpp/xmpp_stanza.cpp @@ -52,7 +52,7 @@ XmlNode::XmlNode(const std::string& name, XmlNode* parent): parent(parent) { // split the namespace and the name - auto n = name.rfind(':'); + auto n = name.rfind('\1'); if (n == std::string::npos) this->name = name; else @@ -67,6 +67,18 @@ XmlNode::XmlNode(const std::string& name): { } +XmlNode::XmlNode(const std::string& xmlns, const std::string& name, XmlNode* parent): + name(name), + parent(parent) +{ + this->attributes["xmlns"] = xmlns; +} + +XmlNode::XmlNode(const std::string& xmlns, const std::string& name): + XmlNode(xmlns, name, nullptr) +{ +} + void XmlNode::delete_all_children() { this->children.clear(); diff --git a/src/xmpp/xmpp_stanza.hpp b/src/xmpp/xmpp_stanza.hpp index f4b3948..a706337 100644 --- a/src/xmpp/xmpp_stanza.hpp +++ b/src/xmpp/xmpp_stanza.hpp @@ -25,6 +25,8 @@ class XmlNode public: explicit XmlNode(const std::string& name, XmlNode* parent); explicit XmlNode(const std::string& name); + explicit XmlNode(const std::string& xmlns, const std::string& name, XmlNode* parent); + explicit XmlNode(const std::string& xmlns, const std::string& name); /** * The copy constructor does not copy the parent attribute. The children * nodes are all copied recursively. @@ -150,6 +152,10 @@ public: XmlNode(name), parent_to_add(parent_ref) {} + XmlSubNode(XmlNode& parent_ref, const std::string& xmlns, const std::string& name): + XmlNode(xmlns, name), + parent_to_add(parent_ref) + {} ~XmlSubNode() { diff --git a/tests/chunk.cpp b/tests/chunk.cpp new file mode 100644 index 0000000..f14c352 --- /dev/null +++ b/tests/chunk.cpp @@ -0,0 +1,49 @@ +#include "utils/chunk.hpp" +#include "catch2/catch_test_macros.hpp" +#include + +TEST_CASE("chunk 5") { + const std::vector input = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + const auto output = utils::chunk(input, 5); + + CHECK( output.size() == 2 ); + CHECK( output.at(0).size() == 5 ); + CHECK( output.at(1).size() == 5 ); + + CHECK( output.at(0).at(0) == 1 ); + CHECK( output.at(0).at(1) == 2 ); + + CHECK( output.at(1).at(0) == 6 ); + CHECK( output.at(1).at(1) == 7 ); +} + +TEST_CASE("chunk 7") { + const std::vector input = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + const auto output = utils::chunk(input, 7); + + CHECK( output.size() == 2 ); + CHECK( output.at(0).size() == 7 ); + CHECK( output.at(1).size() == 3 ); + + CHECK( output.at(0).at(0) == 1 ); + CHECK( output.at(0).at(1) == 2 ); + + CHECK( output.at(1).at(0) == 8 ); + CHECK( output.at(1).at(1) == 9 ); +} + +TEST_CASE("chunk 1") { + const std::vector input = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + const auto output = utils::chunk(input, 1); + + CHECK( output.size() == 10 ); + CHECK( output.at(0).size() == 1 ); + CHECK( output.at(1).size() == 1 ); + + CHECK( output.at(0).at(0) == 1 ); + CHECK( output.at(1).at(0) == 2 ); + CHECK( output.at(2).at(0) == 3 ); + CHECK( output.at(3).at(0) == 4 ); + CHECK( output.at(8).at(0) == 9 ); + CHECK( output.at(9).at(0) == 10 ); +} diff --git a/tests/colors.cpp b/tests/colors.cpp index bf52989..cb705ab 100644 --- a/tests/colors.cpp +++ b/tests/colors.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include #include @@ -41,7 +41,7 @@ TEST_CASE("IRC colors parsing") CHECK(cleaned_up.empty()); std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("[\x1D13dolphin-emu/dolphin\x1D] 03foo commented on #283 (Add support for the guide button to XInput): 02http://example.com"); - CHECK(xhtml->to_string() == "[dolphin-emu/dolphin] foo commented on #283 (Add support for the guide button to XInput): http://example.com"); + CHECK(xhtml->to_string() == "[dolphin-emu/dolphin] foo commented on #283 (Add support for the guide button to XInput): http://example.com"); CHECK(cleaned_up == "[dolphin-emu/dolphin] foo commented on #283 (Add support for the guide button to XInput): http://example.com"); std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("0e46ab by 03Pierre Dindon [090|091|040] 02http://example.net/Ojrh4P media: avoid pop-in effect when loading thumbnails by specifying an explicit size"); diff --git a/tests/config.cpp b/tests/config.cpp index ec9844f..30d801f 100644 --- a/tests/config.cpp +++ b/tests/config.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include "io_tester.hpp" #include diff --git a/tests/database.cpp b/tests/database.cpp index 070a460..e616074 100644 --- a/tests/database.cpp +++ b/tests/database.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include diff --git a/tests/encoding.cpp b/tests/encoding.cpp index b5192ff..fc8c655 100644 --- a/tests/encoding.cpp +++ b/tests/encoding.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include diff --git a/tests/end_to_end/__main__.py b/tests/end_to_end/__main__.py index 35edf3e..8063be5 100644 --- a/tests/end_to_end/__main__.py +++ b/tests/end_to_end/__main__.py @@ -14,11 +14,10 @@ import signal import atexit import sys import os +import shutil from slixmpp.xmlstream.matcher.base import MatcherBase -if not hasattr(asyncio, "ensure_future"): - asyncio.ensure_future = getattr(asyncio, "async") class MatchAll(MatcherBase): """match everything""" @@ -77,9 +76,6 @@ class XMPPComponent(slixmpp.BaseXMPP): self.register_handler(slixmpp.Callback('Match All', MatchAll(None), self.handle_incoming_stanza)) - - self.add_event_handler("session_end", self.on_end_session) - asyncio.ensure_future(self.accept_routine()) self.scenario = scenario @@ -105,9 +101,6 @@ class XMPPComponent(slixmpp.BaseXMPP): self.error(error_msg) self.run_scenario() - def on_end_session(self, _): - self.loop.stop() - def handle_incoming_stanza(self, stanza): if self.stanza_checker: try: @@ -136,7 +129,6 @@ class XMPPComponent(slixmpp.BaseXMPP): if self.biboumi: self.biboumi.stop() - async def accept_routine(self): self.accepting_server = await self.loop.create_server(lambda: self, "127.0.0.1", 8811, reuse_address=True) @@ -175,10 +167,10 @@ class BiboumiRunner(ProcessRunner): self.create = asyncio.create_subprocess_exec("valgrind", "--suppressions=" + (os.environ.get("E2E_BIBOUMI_SUPP_DIR") or "") + "biboumi.supp", "--leak-check=full", "--show-leak-kinds=all", "--errors-for-leak-kinds=all", "--error-exitcode=16", "./biboumi", "test.conf", stdin=None, stdout=self.fd, - stderr=self.fd, loop=None, limit=None) + stderr=self.fd, limit=None) else: self.create = asyncio.create_subprocess_exec("./biboumi", "test.conf", stdin=None, stdout=self.fd, - stderr=self.fd, loop=None, limit=None) + stderr=self.fd, limit=None) class IrcServerRunner(ProcessRunner): @@ -189,8 +181,9 @@ class IrcServerRunner(ProcessRunner): os.remove("ircd.db") except FileNotFoundError: pass - subprocess.run(["oragono", "mkcerts", "--conf", os.getcwd() + "/../tests/end_to_end/ircd.yaml"]) - self.create = asyncio.create_subprocess_exec("oragono", "run", "--conf", os.getcwd() + "/../tests/end_to_end/ircd.yaml", + ergo_path = shutil.which("ergo") or shutil.which("ergochat") + subprocess.run([ergo_path, "mkcerts", "--conf", os.getcwd() + "/../tests/end_to_end/ircd.yaml"]) + self.create = asyncio.create_subprocess_exec(ergo_path, "run", "--conf", os.getcwd() + "/../tests/end_to_end/ircd.yaml", stderr=asyncio.subprocess.PIPE) class BiboumiTest: @@ -204,17 +197,18 @@ class BiboumiTest: self.scenario = scenario self.expected_code = expected_code - def run(self): + async def run(self): with_valgrind = os.environ.get("E2E_WITH_VALGRIND") is not None print("Running scenario: %s%s… " % (self.scenario.name, " (with valgrind)" if with_valgrind else ''), end='') sys.stdout.flush() # Redirect the slixmpp logging into a specific file - output_filename = "slixmpp_%s_output.txt" % (self.scenario.name,) + output_filename = f"slixmpp_{self.scenario.name}_output.txt" with open(output_filename, "w"): pass logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s', - filename=output_filename) + filename=output_filename, + force=True) with open("test.conf", "w") as fd: fd.write(confs[self.scenario.conf]) @@ -229,12 +223,10 @@ class BiboumiTest: # Start the XMPP component and biboumi biboumi = BiboumiRunner(self.scenario.name) xmpp = XMPPComponent(self.scenario, biboumi) - asyncio.get_event_loop().run_until_complete(biboumi.start()) + await biboumi.start() - asyncio.get_event_loop().call_soon(xmpp.run_scenario) - - xmpp.process() - code = asyncio.get_event_loop().run_until_complete(biboumi.wait()) + xmpp.run_scenario() + code = await biboumi.wait() xmpp.biboumi = None self.scenario.steps.clear() @@ -252,7 +244,7 @@ class BiboumiTest: xmpp.saved_values.clear() - if xmpp.server: + if xmpp.boundjid.host: xmpp.accepting_server.close() return not failed @@ -316,18 +308,16 @@ def get_scenarios(test_path, provided_scenar_names): return scenarios -if __name__ == '__main__': - atexit.register(asyncio.get_event_loop().close) - +async def main(): provided_scenar_names = sys.argv[1:] scenarios = get_scenarios(os.path.abspath(os.path.dirname(__file__)), provided_scenar_names) irc_output = open("irc_output.txt", "w") irc = IrcServerRunner() print("Starting irc server…") - asyncio.get_event_loop().run_until_complete(irc.start()) + await irc.start() while True: - res = asyncio.get_event_loop().run_until_complete(irc.process.stderr.readline()) + res = await irc.process.stderr.readline() irc_output.write(res.decode()) if not res: print("IRC server failed to start, see irc_output.txt for more details. Exiting…") @@ -342,7 +332,7 @@ if __name__ == '__main__': failures = 0 for s in scenarios: test = BiboumiTest(s) - if not test.run(): + if not await test.run(): print("You can check the files slixmpp_%s_output.txt and biboumi_%s_output.txt to help you debug." % (s.name, s.name)) failures += 1 @@ -350,7 +340,9 @@ if __name__ == '__main__': print("Waiting for irc server to exit…") irc.stop() - asyncio.get_event_loop().run_until_complete(irc.wait()) + await irc.wait() + res = await irc.process.stderr.readline() + irc_output.write(res.decode()) if failures: print("%d test%s failed, please fix %s." % (failures, 's' if failures > 1 else '', @@ -359,3 +351,5 @@ if __name__ == '__main__': else: print("All tests passed successfully") +if __name__ == '__main__': + asyncio.run(main()) diff --git a/tests/end_to_end/functions.py b/tests/end_to_end/functions.py index 3a21fcf..86ac04e 100644 --- a/tests/end_to_end/functions.py +++ b/tests/end_to_end/functions.py @@ -73,7 +73,7 @@ def check_xpath(xpaths, xmpp, after, stanza): if (expected and not matched) or (not expected and matched): raise StanzaError("Received stanza\n%s\ndid not match expected xpath\n%s" % (stanza, real_xpath)) if after: - if isinstance(after, collections.Iterable): + if isinstance(after, collections.abc.Iterable): for af in after: af(stanza, xmpp) else: diff --git a/tests/end_to_end/ircd.yaml b/tests/end_to_end/ircd.yaml index 057674c..ec21801 100644 --- a/tests/end_to_end/ircd.yaml +++ b/tests/end_to_end/ircd.yaml @@ -1,4 +1,4 @@ -# oragono IRCd config +# ergo IRCd config # network configuration network: @@ -33,7 +33,7 @@ server: # 'proxy' should typically be false. It's only for Kubernetes-style load # balancing that does not terminate TLS, but sends an initial PROXY line # in plaintext. - proxy: false + proxy: false # Example of a Unix domain socket for proxying: # "/tmp/oragono_sock": @@ -87,14 +87,15 @@ server: # casemapping controls what kinds of strings are permitted as identifiers (nicknames, # channel names, account names, etc.), and how they are normalized for case. - # with the recommended default of 'precis', utf-8 identifiers that are "sane" - # (according to RFC 8265) are allowed, and the server additionally tries to protect - # against confusable characters ("homoglyph attacks"). - # the other options are 'ascii' (traditional ASCII-only identifiers), and 'permissive', - # which allows identifiers to contain unusual characters like emoji, but makes users - # vulnerable to homoglyph attacks. unless you're really confident in your decision, - # we recommend leaving this value at its default (changing it once the network is - # already up and running is problematic). + # the recommended default is 'ascii' (traditional ASCII-only identifiers). + # the other options are 'precis', which allows UTF8 identifiers that are "sane" + # (according to UFC 8265), with additional mitigations for homoglyph attacks, + # 'permissive', which allows identifiers containing unusual characters like + # emoji, at the cost of increased vulnerability to homoglyph attacks and potential + # client compatibility problems, and the legacy mappings 'rfc1459' and + # 'rfc1459-strict'. we recommend leaving this value at its default; + # however, note that changing it once the network is already up and running is + # problematic. casemapping: "precis" # whether to look up user hostnames with reverse DNS @@ -109,12 +110,12 @@ server: check-ident: false # password to login to the server - # generated using "oragono genpasswd" + # generated using "ergo genpasswd" #password: "" # motd filename # if you change the motd, you should move it to ircd.motd - motd: + # motd: # motd formatting codes # if this is true, the motd is escaped using formatting codes like $c, $b, and $i @@ -136,7 +137,7 @@ server: # (comment this out to use passwords only) fingerprint: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" - # password the gateway uses to connect, made with oragono genpasswd + # password the gateway uses to connect, made with ergo genpasswd password: "$2a$04$sLEFDpIOyUp55e6gTMKbOeroT6tMXTjPFvA0eGvwvImVR9pkwv7ee" # addresses/CIDRs that can use this webirc command @@ -209,42 +210,13 @@ server: # IP cloaking hides users' IP addresses from other users and from channel admins # (but not from server admins), while still allowing channel admins to ban # offending IP addresses or networks. In place of hostnames derived from reverse - # DNS, users see fake domain names like pwbs2ui4377257x8.oragono. These names are + # DNS, users see fake domain names like pwbs2ui4377257x8.ergo. These names are # generated deterministically from the underlying IP address, but if the underlying # IP is not already known, it is infeasible to recover it from the cloaked name. ip-cloaking: # whether to enable IP cloaking enabled: false - # fake TLD at the end of the hostname, e.g., pwbs2ui4377257x8.oragono - netname: "oragono" - - # secret key to prevent dictionary attacks against cloaked IPs - # any high-entropy secret is valid for this purpose: - # you MUST generate a new one for your installation. - # suggestion: use the output of `oragono mksecret` - # note that rotating this key will invalidate all existing ban masks. - secret: "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" - - # name of an environment variable to pull the secret from, for use with - # k8s secret distribution: - # secret-environment-variable: "ORAGONO_CLOAKING_SECRET" - - # the cloaked hostname is derived only from the CIDR (most significant bits - # of the IP address), up to a configurable number of bits. this is the - # granularity at which bans will take effect for IPv4. Note that changing - # this value will invalidate any stored bans. - cidr-len-ipv4: 32 - - # analogous granularity for IPv6 - cidr-len-ipv6: 64 - - # number of bits of hash output to include in the cloaked hostname. - # more bits means less likelihood of distinct IPs colliding, - # at the cost of a longer cloaked hostname. if this value is set to 0, - # all users will receive simply `netname` as their cloaked hostname. - num-bits: 64 - # secure-nets identifies IPs and CIDRs which are secure at layer 3, # for example, because they are on a trusted internal LAN or a VPN. # plaintext connections from these IPs and CIDRs will be considered @@ -272,21 +244,6 @@ accounts: # length of time a user has to verify their account before it can be re-registered verify-timeout: "32h" - # callbacks to allow - enabled-callbacks: - - none # no verification needed, will instantly register successfully - - # example configuration for sending verification emails via a local mail relay - # callbacks: - # mailto: - # server: localhost - # port: 25 - # tls: - # enabled: false - # username: "" - # password: "" - # sender: "admin@my.network" - # throttle account login attempts (to prevent either password guessing, or DoS # attacks on the server aimed at forcing repeated expensive bcrypt computations) login-throttling: @@ -343,10 +300,16 @@ accounts: # rename-timeout - this is how long users have 'til they're renamed rename-timeout: 30s - # rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31) - rename-prefix: Guest- + # format for guest nicknames: + # 1. these nicknames cannot be registered or reserved + # 2. if a client is automatically renamed by the server, + # this is the template that will be used (e.g., Guest-nccj6rgmt97cg) + # 3. if force-guest-format (see below) is enabled, clients without + # a registered account will have this template applied to their + # nicknames (e.g., 'katie' will become 'Guest-katie') + guest-nickname-format: Guest-* - # multiclient controls whether oragono allows multiple connections to + # multiclient controls whether ergo allows multiple connections to # attach to the same client/nickname identity; this is part of the # functionality traditionally provided by a bouncer like ZNC multiclient: @@ -379,27 +342,6 @@ accounts: # (make sure any changes you make here are RFC-compliant) valid-regexp: '^[0-9A-Za-z.\-_/]+$' - # options controlling users requesting vhosts: - user-requests: - # can users request vhosts at all? if this is false, operators with the - # 'vhosts' capability can still assign vhosts manually - enabled: false - - # if uncommented, all new vhost requests will be dumped into the given - # channel, so opers can review them as they are sent in. ensure that you - # have registered and restricted the channel appropriately before you - # uncomment this. - #channel: "#vhosts" - - # after a user's vhost has been approved or rejected, they need to wait - # this long (starting from the time of their original request) - # before they can request a new one. - cooldown: 168h - - # vhosts that users can take without approval, using `/HS TAKE` - offer-list: - #- "oragono.test" - # support for deferring password checking to an external LDAP server # you should probably ignore this section! consult the grafana docs for details: # https://grafana.com/docs/grafana/latest/auth/ldap/ @@ -466,9 +408,8 @@ oper-classes: # capability names capabilities: - - "oper:local_kill" - - "oper:local_ban" - - "oper:local_unban" + - "kill" + - "ban" - "nofakelag" # network operator @@ -479,12 +420,6 @@ oper-classes: # oper class this extends from extends: "local-oper" - # capability names - capabilities: - - "oper:remote_kill" - - "oper:remote_ban" - - "oper:remote_unban" - # server admin "server-admin": # title shown in WHOIS @@ -495,8 +430,7 @@ oper-classes: # capability names capabilities: - - "oper:rehash" - - "oper:die" + - "rehash" - "accreg" - "sajoin" - "samode" @@ -522,7 +456,7 @@ opers: # operators can be authenticated either by password (with the /OPER command), # or by certificate fingerprint, or both. if a password hash is set, then a # password is required to oper up (e.g., /OPER dan mypassword). to generate - # the hash, use `oragono genpasswd`. + # the hash, use `ergo genpasswd`. password: "$2a$04$LiytCxaY0lI.guDj2pBN4eLRD5cdM2OLDwqmGAgB6M2OPirbF5Jcu" # if a SHA-256 certificate fingerprint is configured here, then it will be @@ -576,7 +510,7 @@ logging: # debug options debug: - # when enabled, oragono will attempt to recover from certain kinds of + # when enabled, ergo will attempt to recover from certain kinds of # client-triggered runtime errors that would normally crash the server. # this makes the server more resilient to DoS, but could result in incorrect # behavior. deployments that would prefer to "start from scratch", e.g., by @@ -606,7 +540,7 @@ datastore: host: "localhost" # port is unnecessary for connections via unix domain socket: #port: 3306 - user: "oragono" + user: "ergo" password: "hunter2" history-database: "oragono_history" timeout: 3s @@ -720,11 +654,6 @@ history: # (and will eventually be deleted from persistent storage, if that's enabled) #expire-time: 1w - # if this is set, logged-in users cannot retrieve messages older than their - # account registration date, and logged-out users cannot retrieve messages - # older than their sign-on time (modulo grace-period, see below): - enforce-registration-date: false - # but if this is set, you can retrieve messages that are up to `grace-period` # older than the above cutoff time. this is recommended to allow logged-out # users to do session resumption / query history after disconnections. diff --git a/tests/end_to_end/scenarios/channel_custom_topic.py b/tests/end_to_end/scenarios/channel_custom_topic.py index 3b3104e..089dbd9 100644 --- a/tests/end_to_end/scenarios/channel_custom_topic.py +++ b/tests/end_to_end/scenarios/channel_custom_topic.py @@ -14,13 +14,13 @@ scenario = ( sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), expect_unordered( [ - "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']" + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}'][@role='participant']" ], [ "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']" ], [ - "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], [ diff --git a/tests/end_to_end/scenarios/channel_force_join.py b/tests/end_to_end/scenarios/channel_force_join.py index 9a24c06..c708570 100644 --- a/tests/end_to_end/scenarios/channel_force_join.py +++ b/tests/end_to_end/scenarios/channel_force_join.py @@ -11,7 +11,7 @@ scenario = ( send_stanza(""), expect_unordered( [ - "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost']" + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}']" ], [ "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", diff --git a/tests/end_to_end/scenarios/channel_history.py b/tests/end_to_end/scenarios/channel_history.py index 0014d65..f18cf27 100644 --- a/tests/end_to_end/scenarios/channel_history.py +++ b/tests/end_to_end/scenarios/channel_history.py @@ -9,7 +9,7 @@ scenario = ( # Second user joins send_stanza(""), - expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}%{irc_server_one}/~{nick_one}@localhost'][@role='moderator']", + expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}%{irc_server_one}/~{nick_one}@{irc_host_two}'][@role='moderator']", "/presence/muc_user:x/muc_user:status[@code='110']"), # Receive the history message expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}']/body[text()='coucou']", diff --git a/tests/end_to_end/scenarios/channel_history_on_fixed_server.py b/tests/end_to_end/scenarios/channel_history_on_fixed_server.py index 0e957e1..4a18e87 100644 --- a/tests/end_to_end/scenarios/channel_history_on_fixed_server.py +++ b/tests/end_to_end/scenarios/channel_history_on_fixed_server.py @@ -11,7 +11,7 @@ scenario = ( # Second user joins send_stanza(""), - expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}@{biboumi_host}/~{nick_one}@localhost'][@role='moderator']", + expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}@{biboumi_host}/~{nick_one}@{irc_host_two}'][@role='moderator']", "/presence/muc_user:x/muc_user:status[@code='110']"), # Receive the history message expect_stanza("/message[@from='#foo@{biboumi_host}/{nick_one}']/body[text()='coucou']", diff --git a/tests/end_to_end/scenarios/channel_join_with_password.py b/tests/end_to_end/scenarios/channel_join_with_password.py index fdebcbe..c2514c8 100644 --- a/tests/end_to_end/scenarios/channel_join_with_password.py +++ b/tests/end_to_end/scenarios/channel_join_with_password.py @@ -19,13 +19,13 @@ scenario = ( send_stanza(" SECRET"), expect_unordered( [ - "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost']" + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}']" ], [ "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']" ], [ - "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], [ diff --git a/tests/end_to_end/scenarios/channel_join_with_two_users.py b/tests/end_to_end/scenarios/channel_join_with_two_users.py index 3b2b102..571d567 100644 --- a/tests/end_to_end/scenarios/channel_join_with_two_users.py +++ b/tests/end_to_end/scenarios/channel_join_with_two_users.py @@ -8,13 +8,13 @@ scenario = ( sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), expect_unordered( [ - "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost']" + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}']" ], [ "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']" ], [ - "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], [ diff --git a/tests/end_to_end/scenarios/channel_messages.py b/tests/end_to_end/scenarios/channel_messages.py index a3ead30..3262832 100644 --- a/tests/end_to_end/scenarios/channel_messages.py +++ b/tests/end_to_end/scenarios/channel_messages.py @@ -11,10 +11,10 @@ scenario = ( # Our presence, sent to the other user, and ourself expect_unordered( - ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']"], + ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}'][@role='participant']"], ["/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']"], [ - "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], ["/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"], diff --git a/tests/end_to_end/scenarios/invite_other.py b/tests/end_to_end/scenarios/invite_other.py index 0e40dcb..030c65d 100644 --- a/tests/end_to_end/scenarios/invite_other.py +++ b/tests/end_to_end/scenarios/invite_other.py @@ -11,8 +11,10 @@ scenario = ( expect_stanza("/presence"), expect_stanza("/message"), send_stanza(""), - expect_stanza("/message/body[text()='{nick_two} has been invited to #foo']"), - expect_stanza("/message[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='#foo%{irc_server_one}/{nick_one}']"), + expect_unordered( + ["/message/body[text()='{nick_two} has been invited to #foo']"], + ["/message[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='#foo%{irc_server_one}/{nick_one}']"], + ), send_stanza(""), expect_stanza("/message[@to='bertrand@example.com'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='{jid_one}/{resource_one}']"), diff --git a/tests/end_to_end/scenarios/message_correction.py b/tests/end_to_end/scenarios/message_correction.py new file mode 100644 index 0000000..67f7750 --- /dev/null +++ b/tests/end_to_end/scenarios/message_correction.py @@ -0,0 +1,10 @@ +from scenarios import * + +scenario = ( + scenarios.simple_channel_join.scenario, + send_stanza("hello"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='hello']"), + send_stanza("hello"), + expect_stanza("/message[@from='#foo%{irc_server_one}'][@id='the-message-id-2'][@to='{jid_one}/{resource_one}'][@type='error']/error[@type='cancel']/stanza:feature-not-implemented", + "/message[@from='#foo%{irc_server_one}'][@id='the-message-id-2'][@to='{jid_one}/{resource_one}'][@type='error']/error[@type='cancel']/stanza:text[text()='This room is mapped from IRC, which does not support message correction.']"), +) diff --git a/tests/end_to_end/scenarios/multiline_message.py b/tests/end_to_end/scenarios/multiline_message.py index cd42c6c..1e8ab1a 100644 --- a/tests/end_to_end/scenarios/multiline_message.py +++ b/tests/end_to_end/scenarios/multiline_message.py @@ -8,9 +8,7 @@ scenario = ( # Send a multi-line channel message send_stanza("un\ndeux\ntrois"), # Receive multiple messages, in order - expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='un']"), - expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='deux']"), - expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='trois']"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='un\ndeux\ntrois']"), # Send a simple message, with no id send_stanza("hello"), @@ -38,10 +36,10 @@ scenario = ( sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), # Our presence, sent to the other user expect_unordered( - ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']"], + ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}'][@role='participant']"], ["/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']"], [ - "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], ["/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"] @@ -51,12 +49,18 @@ scenario = ( send_stanza("a\nb\nc"), # Receive multiple messages, for each user expect_unordered( - ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='a']"], - ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='b']"], - ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='c']"], + ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='a\nb\nc']"], - ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='a']"], - ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='b']"], - ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='c']"], - ) + ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='a\nb\nc']"], + ), + + # Send a multi-line private message, to a in-room JID + send_stanza("a\nb\nc"), + # Message is received with a server-wide JID + expect_stanza("/message[@from='{lower_nick_one}%{irc_server_one}'][@to='{jid_two}'][@type='chat']/body[text()='a\nb\nc']"), + + # Send a multi-line private message, to a server-wide JID + send_stanza("a\nb\nc"), + # Message is received with a server-wide JID + expect_stanza("/message[@from='{lower_nick_one}%{irc_server_one}'][@to='{jid_two}'][@type='chat']/body[text()='a\nb\nc']"), ) diff --git a/tests/end_to_end/scenarios/raw_message.py b/tests/end_to_end/scenarios/raw_message.py index c6cd4e7..b7064f4 100644 --- a/tests/end_to_end/scenarios/raw_message.py +++ b/tests/end_to_end/scenarios/raw_message.py @@ -7,5 +7,5 @@ scenario = ( expect_stanza("/message"), send_stanza("WHOIS {nick_one}"), - expect_stanza("/message[@from='{irc_server_one}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} localhost * {nick_one}']"), + expect_stanza("/message[@from='{irc_server_one}'][@type='chat']/body[text()='{irc_host_one}: {nick_one} ~{nick_one} {irc_host_two} * {nick_one}']"), ) diff --git a/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py b/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py index 7eb5b13..638f466 100644 --- a/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py +++ b/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py @@ -9,6 +9,6 @@ scenario = ( expect_stanza("/message"), send_stanza("WHOIS {nick_one}"), - expect_stanza("/message[@from='{biboumi_host}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} localhost * {nick_one}']"), + expect_stanza("/message[@from='{biboumi_host}'][@type='chat']/body[text()='{irc_host_one}: {nick_one} ~{nick_one} {irc_host_two} * {nick_one}']"), ) diff --git a/tests/end_to_end/scenarios/sasl.py b/tests/end_to_end/scenarios/sasl.py index 9f2a27a..97dd2fd 100644 --- a/tests/end_to_end/scenarios/sasl.py +++ b/tests/end_to_end/scenarios/sasl.py @@ -86,7 +86,9 @@ scenario = ( expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"), send_stanza(""), - # Here, the 6 connecting… connected messages from the connection attempt + # Here, the 8 connecting… connected messages from the connection attempt + expect_stanza("/message"), + expect_stanza("/message"), expect_stanza("/message"), expect_stanza("/message"), expect_stanza("/message"), diff --git a/tests/end_to_end/scenarios/simple_kick.py b/tests/end_to_end/scenarios/simple_kick.py index 2949157..d327412 100644 --- a/tests/end_to_end/scenarios/simple_kick.py +++ b/tests/end_to_end/scenarios/simple_kick.py @@ -2,7 +2,7 @@ from scenarios import * scenario = ( scenarios.channel_join_with_two_users.scenario, - # demonstrate bug https://lab.louiz.org/louiz/biboumi/issues/3291 + # demonstrate bug https://codeberg.org/poezio/biboumi/issues/3291 # First user joins an other channel send_stanza(""), expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"), diff --git a/tests/end_to_end/sequences.py b/tests/end_to_end/sequences.py index f151bc7..e4117f6 100644 --- a/tests/end_to_end/sequences.py +++ b/tests/end_to_end/sequences.py @@ -23,15 +23,17 @@ def connection_begin(irc_host, jid, expected_irc_presence=False, fixed_irc_serve expect_stanza(xpath % ('Connecting to %s:6667 (not encrypted)' % irc_host)), expect_stanza(xpath % 'Connected to IRC server.')) + result += ( + expect_stanza("/message/body[text()='irc.localhost: *** Looking up your hostname...']"), + expect_stanza("/message/body[text()='irc.localhost: *** Found your hostname']") + ) + if expected_irc_presence: result += (expect_stanza("/presence[@from='" + irc_host + "@biboumi.localhost']"),) if login is not None: result += (expect_stanza("/message/body[text()='irc.localhost: You are now logged in as %s']" % (login,)),) - result += ( - expect_stanza("/message/body[text()='irc.localhost: *** Looking up your hostname...']"), - expect_stanza("/message/body[text()='irc.localhost: *** Found your hostname']") - ) + return result def connection_tls_begin(irc_host, jid, fixed_irc_server): @@ -61,12 +63,12 @@ def connection_end(irc_host, jid, fixed_irc_server=False): xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]" irc_host = 'irc.localhost' return ( - expect_stanza("/message/body[re:test(text(), '%s')]" % (r'^%s: Your host is %s, running version oragono-2\.0\.0(-[a-z0-9]+)? $' % (irc_host, irc_host))), + expect_stanza("/message/body[re:test(text(), '%s')]" % (r'^%s: Your host is %s, running version ergo-2\.\d+\.\d+(-[a-z0-9]+)? $' % (irc_host, irc_host))), expect_stanza(xpath_re % (r'^%s: This server was created .*$' % irc_host)), expect_stanza(xpath_re % (r'^%s: There are \d+ users and \d+ invisible on \d+ server\(s\)$' % irc_host)), - expect_stanza(xpath_re % ("%s: \d+ IRC Operators online" % irc_host,)), - expect_stanza(xpath_re % ("%s: \d+ unregistered connections" % irc_host,)), - expect_stanza(xpath_re % ("%s: \d+ channels formed" % irc_host,)), + expect_stanza(xpath_re % ("%s: \\d+ IRC Operators online" % irc_host,)), + expect_stanza(xpath_re % ("%s: \\d+ unregistered connections" % irc_host,)), + expect_stanza(xpath_re % ("%s: \\d+ channels formed" % irc_host,)), expect_stanza(xpath_re % (r'^%s: I have \d+ clients and \d+ servers$' % irc_host)), expect_stanza(xpath_re % (r'^%s: \d+ \d+ Current local users \d+, max \d+$' % irc_host)), expect_stanza(xpath_re % (r'^%s: \d+ \d+ Current global users \d+, max \d+$' % irc_host)), diff --git a/tests/iid.cpp b/tests/iid.cpp index 63b2ba3..17744dd 100644 --- a/tests/iid.cpp +++ b/tests/iid.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include #include @@ -7,6 +7,7 @@ TEST_CASE("Irc user parsing") { + Config::set("fixed_irc_server", "", false); const std::map prefixes{{'!', 'a'}, {'@', 'o'}}; IrcUser user1("!nick!~some@host.bla", prefixes); CHECK(user1.nick == "nick"); @@ -23,6 +24,7 @@ TEST_CASE("Irc user parsing") TEST_CASE("multi-prefix") { + Config::set("fixed_irc_server", "", false); const std::map prefixes{{'!', 'a'}, {'@', 'o'}, {'~', 'f'}}; IrcUser user("!@~nick", prefixes); CHECK(user.nick == "nick"); @@ -47,7 +49,8 @@ namespace Catch TEST_CASE("Iid creation") { - const std::set chantypes {'#', '&'}; + Config::set("fixed_irc_server", "", false); + const std::set chantypes {'&', '#', '+', '!'}; Iid iid1("foo%irc.example.org", chantypes); CHECK(std::to_string(iid1) == "foo%irc.example.org"); CHECK(iid1.get_local() == "foo"); @@ -95,7 +98,7 @@ TEST_CASE("Iid creation in fixed_server mode") { Config::set("fixed_irc_server", "fixed.example.com", false); - const std::set chantypes {'#', '&'}; + const std::set chantypes {'&', '#', '+', '!'}; Iid iid1("foo%irc.example.org", chantypes); CHECK(std::to_string(iid1) == "foo%irc.example.org"); CHECK(iid1.get_local() == "foo%irc.example.org"); diff --git a/tests/io_tester.cpp b/tests/io_tester.cpp index 19c97c9..1d6d59b 100644 --- a/tests/io_tester.cpp +++ b/tests/io_tester.cpp @@ -1,5 +1,5 @@ #include "io_tester.hpp" -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include /** diff --git a/tests/irc.cpp b/tests/irc.cpp index 0f30f15..a113823 100644 --- a/tests/irc.cpp +++ b/tests/irc.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include diff --git a/tests/is_requester_allowed_to_persist.cpp b/tests/is_requester_allowed_to_persist.cpp new file mode 100644 index 0000000..372e4b3 --- /dev/null +++ b/tests/is_requester_allowed_to_persist.cpp @@ -0,0 +1,125 @@ +#include "catch2/catch_test_macros.hpp" +#include "utils/is_requester_allowed_to_persist.hpp" +#include "xmpp/jid.hpp" + + +TEST_CASE("default") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_denylist", ""); + Config::set("persist_user_allowlist", ""); + Config::set("default_persistence_allowed", ""); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == true); + Config::clear(); +} + +TEST_CASE("default - default_persistence_allowed - false") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_denylist", ""); + Config::set("persist_user_allowlist", ""); + Config::set("default_persistence_allowed", "false"); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == false); + Config::clear(); +} + +TEST_CASE("default - default_persistence_allowed - true") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_denylist", ""); + Config::set("persist_user_allowlist", ""); + Config::set("default_persistence_allowed", "true"); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == true); + Config::clear(); +} + +TEST_CASE("denylist - jid") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_denylist", "foo@example.com"); + Config::set("persist_user_allowlist", ""); + Config::set("default_persistence_allowed", ""); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == false); + Config::clear(); +} + +TEST_CASE("denylist - domain") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_denylist", "example.com"); + Config::set("persist_user_allowlist", ""); + Config::set("default_persistence_allowed", ""); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == false); + Config::clear(); +} + +TEST_CASE("allowlist - jid") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_allowlist", "foo@example.com"); + Config::set("persist_user_denylist", ""); + Config::set("default_persistence_allowed", ""); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == true); + Config::clear(); +} + +TEST_CASE("allowlist - domain") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_allowlist", "example.com"); + Config::set("persist_user_denylist", ""); + Config::set("default_persistence_allowed", ""); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == true); + Config::clear(); +} + +TEST_CASE("denylist + allowlist - deny domain, allow jid") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_denylist", "example.com"); + Config::set("persist_user_allowlist", "foo@example.com"); + Config::set("default_persistence_allowed", ""); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == true); + Config::clear(); +} + +TEST_CASE("denylist + allowlist - deny jid, allow domain") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_denylist", "foo@example.com"); + Config::set("persist_user_allowlist", "example.com"); + Config::set("default_persistence_allowed", ""); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == false); + Config::clear(); +} + +TEST_CASE("denylist + allowlist - deny jid, allow jid") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_denylist", "foo@example.com"); + Config::set("persist_user_allowlist", "foo@example.com"); + Config::set("default_persistence_allowed", ""); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == false); + Config::clear(); +} + diff --git a/tests/jid.cpp b/tests/jid.cpp index 592d6f3..cc7d74b 100644 --- a/tests/jid.cpp +++ b/tests/jid.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include #include diff --git a/tests/logger.cpp b/tests/logger.cpp index 1e3392a..871dbcf 100644 --- a/tests/logger.cpp +++ b/tests/logger.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include #include diff --git a/tests/network.cpp b/tests/network.cpp index a52eb6a..309659a 100644 --- a/tests/network.cpp +++ b/tests/network.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include #include diff --git a/tests/parse_caps.cpp b/tests/parse_caps.cpp new file mode 100644 index 0000000..6beb0c3 --- /dev/null +++ b/tests/parse_caps.cpp @@ -0,0 +1,70 @@ +#include "catch2/catch_test_macros.hpp" +#include "irc/parse_caps.hpp" +#include + +TEST_CASE("simple caps") +{ + const auto output = parse_caps("multi-prefix extended-join account-notify"); + CHECK( output.contains("multi-prefix") ); + CHECK( output.contains("extended-join") ); + CHECK( output.contains("account-notify") ); +} + +TEST_CASE("key-value") +{ + const auto output = parse_caps("sasl=PLAIN example.com/a=b"); + CHECK( output.contains("sasl") ); + CHECK( output.contains("example.com/a") ); + + CHECK( output.at("sasl") == "PLAIN" ); + CHECK( output.at("example.com/a") == "b" ); +} + +TEST_CASE("nested key-value") +{ + const auto output = parse_caps("draft/multiline=max-bytes=40000,max-lines=10 example.com/a=foo=bar,b=c"); + CHECK( output.contains("draft/multiline") ); + CHECK( output.contains("example.com/a") ); + + const auto multiline = output.at("draft/multiline"); + const auto a = output.at("example.com/a"); + CHECK( multiline == "max-bytes=40000,max-lines=10" ); + CHECK ( a == "foo=bar,b=c" ); + + const auto multiline_val = parse_cap_keyval(multiline.value()); + const auto a_val = parse_cap_keyval(a.value()); + CHECK( multiline_val.at("max-bytes").has_value() ); + CHECK( multiline_val.at("max-lines").has_value() ); + CHECK( a_val.at("foo").has_value() ); + CHECK( a_val.at("b").has_value() ); + + CHECK( multiline_val.at("max-bytes").value() == "40000" ); + CHECK( multiline_val.at("max-lines").value() == "10" ); + CHECK( a_val.at("foo").value() == "bar" ); + CHECK( a_val.at("b").value() == "c" ); +} + +TEST_CASE("all") +{ + const auto output = parse_caps("message-tags sasl=PLAIN,EXTERNAL draft/multiline=max-bytes=40000,max-lines=10"); + CHECK( output.contains("message-tags") ); + CHECK( output.contains("sasl") ); + CHECK( output.contains("draft/multiline") ); + + const auto multiline = output.at("draft/multiline"); + const auto sasl = output.at("sasl"); + const auto message_tags = output.at("message-tags"); + CHECK( !message_tags.has_value() ); + CHECK( sasl == "PLAIN,EXTERNAL" ); + CHECK( multiline == "max-bytes=40000,max-lines=10" ); + + const auto multiline_val = parse_cap_keyval(multiline.value()); + const auto sasl_val = parse_cap_val_list(sasl.value()); + CHECK( std::find(sasl_val.begin(), sasl_val.end(), "PLAIN") != sasl_val.end() ); + CHECK( std::find(sasl_val.begin(), sasl_val.end(), "EXTERNAL") != sasl_val.end() ); + CHECK( multiline_val.at("max-bytes").has_value() ); + CHECK( multiline_val.at("max-lines").has_value() ); + + CHECK( multiline_val.at("max-bytes").value() == "40000" ); + CHECK( multiline_val.at("max-lines").value() == "10" ); +} diff --git a/tests/parse_duration.cpp b/tests/parse_duration.cpp new file mode 100644 index 0000000..45e9348 --- /dev/null +++ b/tests/parse_duration.cpp @@ -0,0 +1,84 @@ +#include "catch2/catch_test_macros.hpp" + +#include +#include + +TEST_CASE("parse_duration - 2d") +{ + const long expected = (60 * 60 * 24) * 2; + const std::optional output = utils::parse_duration("2d"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 5w") +{ + const long expected = ((60 * 60 * 24) * 7) * 5; + const std::optional output = utils::parse_duration("5w"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - asdf") +{ + const std::optional expected; + const std::optional output = utils::parse_duration("asdf"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 1s") +{ + const long expected = 1; + const std::optional output = utils::parse_duration("1s"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 2mon") +{ + const long expected = ((60 * 60 * 24) * 31) * 2; + const std::optional output = utils::parse_duration("2mon"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 5y") +{ + const long expected = (long)((60 * 60 * 24) * 365.2425) * 5; + const std::optional output = utils::parse_duration("5y"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 5") +{ + const std::optional expected; + const std::optional output = utils::parse_duration("5"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - ") +{ + const std::optional expected; + const std::optional output = utils::parse_duration(""); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 5blorbs") +{ + const std::optional expected; + const std::optional output = utils::parse_duration("5blorbs"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 5y2d") +{ + const std::optional expected; + const std::optional output = utils::parse_duration("5y2d"); + + CHECK(expected == output); +} diff --git a/tests/test.cpp b/tests/test.cpp deleted file mode 100644 index 0c7c351..0000000 --- a/tests/test.cpp +++ /dev/null @@ -1,2 +0,0 @@ -#define CATCH_CONFIG_MAIN -#include "catch.hpp" diff --git a/tests/timed_events.cpp b/tests/timed_events.cpp index fece422..b8ccf6c 100644 --- a/tests/timed_events.cpp +++ b/tests/timed_events.cpp @@ -1,21 +1,7 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include -/** - * Let Catch know how to display std::chrono::duration values - */ -namespace Catch -{ - template struct StringMaker> - { - static std::string convert(const std::chrono::duration& value) - { - return std::to_string(std::chrono::duration_cast(value).count()) + "ms"; - } - }; -} - /** * TODO, use a mock clock instead of relying on the real time with a sleep: * it’s unreliable on heavy load. diff --git a/tests/utils.cpp b/tests/utils.cpp index 6151733..5a9f0a0 100644 --- a/tests/utils.cpp +++ b/tests/utils.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include #include diff --git a/tests/uuid.cpp b/tests/uuid.cpp index 12c6c32..4f9bdfb 100644 --- a/tests/uuid.cpp +++ b/tests/uuid.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include diff --git a/tests/xmpp.cpp b/tests/xmpp.cpp index 14c51da..d88f8d7 100644 --- a/tests/xmpp.cpp +++ b/tests/xmpp.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include #include @@ -67,6 +67,8 @@ TEST_CASE("substanzas") CHECK(!d.has_children()); } CHECK(b.has_children()); + XmlSubNode e(a, "namespace", "name"); + CHECK(e.get_tag("xmlns") == "namespace"); } CHECK(a.has_children()); }