* https://codeberg.org/poezio/biboumi: (65 commits)
  build: fix wrong constextpr preventing some compilers from building biboumi
  database: various optimizations for SQLite and PostgreSQL
  doc(admin): format verify_certificate documentation into a list
  Fix RPM builds on Fedora Linux
  Systemd service dir is  /usr/lib/systemd/system
  cmake: Explicitly search for Catch2 version 3
  add CAP LS 302 support and max-lines support for draft/multiline messages
  install files using GNUInstallDirs
  change to direct /doc/
  Fix link to documentation
  fix multiline private messages
  add archive_expires_after option
  Fix documentation warnings
  fix fedora package
  add ctest support and remove absolute path from etc directory
  Rename Ports to Unencrypted ports
  build docker image from local files, not git clone
  update some version strings
  add default_persistence_allowed
  add persist_user_denylist and persist_user_allowlist
  ...
This commit is contained in:
Stephen Paul Weber
2026-09-23 09:59:13 -05:00
109 changed files with 1935 additions and 471 deletions
+40
View File
@@ -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/
+25
View File
@@ -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
+22
View File
@@ -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
+26
View File
@@ -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
+51
View File
@@ -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
+63
View File
@@ -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
+20 -1
View File
@@ -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
+49 -34
View File
@@ -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
$<TARGET_OBJECTS:network>
$<TARGET_OBJECTS:xmpp>
$<TARGET_OBJECTS:bridge>
$<TARGET_OBJECTS:scheduler>
$<TARGET_OBJECTS:irc>
$<TARGET_OBJECTS:identd>)
@@ -230,6 +235,7 @@ add_executable(test_suite ${source_tests}
$<TARGET_OBJECTS:network>
$<TARGET_OBJECTS:xmpp>
$<TARGET_OBJECTS:bridge>
$<TARGET_OBJECTS:scheduler>
$<TARGET_OBJECTS:irc>
$<TARGET_OBJECTS:identd>)
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
)
+3 -3
View File
@@ -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’) <louiz@louiz.org>
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
+52 -29
View File
@@ -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)
-1
View File
@@ -1 +0,0 @@
irc.gimp.org.policy.txt
-1
View File
@@ -1 +0,0 @@
minimum_dh_group_size = 1024
+58 -8
View File
@@ -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 <https://botan.randombit.net/manual/tls.html#tls-policies>`_.
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.
+3 -3
View File
@@ -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.
+8 -13
View File
@@ -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
+4 -6
View File
@@ -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::
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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.
+3 -3
View File
@@ -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
+12 -9
View File
@@ -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.
+13 -9
View File
@@ -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 && \
+10 -7
View File
@@ -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
+10 -16
View File
@@ -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
+8 -6
View File
@@ -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
+9 -7
View File
@@ -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 <louiz@louiz.org> - ${RPM_VERSION}-1
-- Build latest git revision
* ${RPM_DATE} Le Coz Florent <louiz@louiz.org> - ${RPM_VERSION}-1
Build latest git revision
* Tue Sep 22 2020 Le Coz Florent <louiz@louiz.org> - 9.0-1
Update to version 9.0
@@ -76,7 +78,7 @@ make check %{?_smp_mflags}
* Tue Feb 25 2020 Le Coz Florent <louiz@louiz.org> - 8.4-1
Update to version 8.4
* Wed Jun 1 2018 Le Coz Florent <louiz@louiz.org> - 8.3-1
* Fri Jun 1 2018 Le Coz Florent <louiz@louiz.org> - 8.3-1
Update to version 8.3
* Fri May 25 2018 Le Coz Florent <louiz@louiz.org> - 8.2-1
+125 -7
View File
@@ -2,18 +2,22 @@
#include <utility>
#include <xmpp/biboumi_component.hpp>
#include <network/poller.hpp>
#include <utils/chunk.hpp>
#include <utils/empty_if_fixed_server.hpp>
#include <utils/encoding.hpp>
#include <utils/tolower.hpp>
#include <utils/uuid.hpp>
#include <logger/logger.hpp>
#include <utils/is_requester_allowed_to_persist.hpp>
#include <utils/revstr.hpp>
#include <utils/split.hpp>
#include <xmpp/jid.hpp>
#include <database/database.hpp>
#include <irc/parse_caps.hpp>
#include "result_set_management.hpp"
#include <algorithm>
#include <cstring>
#include <utils/string.hpp>
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<IrcClient>& irc = pair.second;
std::shared_ptr<IrcClient>& 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<IrcClient>(this->poller, hostname,
std::make_shared<IrcClient>(this->poller, hostname,
nickname, username,
realname, jid.domain,
*this));
std::unique_ptr<IrcClient>& irc = this->irc_clients.at(hostname);
std::shared_ptr<IrcClient> 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<std::string> 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<std::vector<std::string>> 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<IrcMessage> 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<std::string> 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<char> uuid = utils::gen_uuid();
std::vector<IrcMessage> 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<Database::GlobalPersistent>())
if (!utils::is_requester_allowed_to_persist(Jid(this->user_jid)))
persistent = false;
else if (goptions.col<Database::GlobalPersistent>())
persistent = true;
else
{
@@ -1151,12 +1269,12 @@ void Bridge::trigger_on_irc_message(const std::string& irc_hostname, const IrcMe
}
}
std::unordered_map<std::string, std::unique_ptr<IrcClient>>& Bridge::get_irc_clients()
std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients()
{
return this->irc_clients;
}
const std::unordered_map<std::string, std::unique_ptr<IrcClient>>& Bridge::get_irc_clients() const
const std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients() const
{
return this->irc_clients;
}
@@ -1165,7 +1283,7 @@ std::set<char> Bridge::get_chantypes(const std::string& hostname) const
{
IrcClient* irc = this->find_irc_client(hostname);
if (!irc)
return {'#', '&'};
return {'&', '#', '+', '!'};
return irc->get_chantypes();
}
+3 -3
View File
@@ -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<std::string, std::unique_ptr<IrcClient>>& get_irc_clients();
const std::unordered_map<std::string, std::unique_ptr<IrcClient>>& get_irc_clients() const;
std::unordered_map<std::string, std::shared_ptr<IrcClient>>& get_irc_clients();
const std::unordered_map<std::string, std::shared_ptr<IrcClient>>& get_irc_clients() const;
std::set<char> 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<std::string, std::unique_ptr<IrcClient>> irc_clients;
std::unordered_map<std::string, std::shared_ptr<IrcClient>> irc_clients;
/**
* To communicate back with the XMPP component
*/
+1 -1
View File
@@ -24,7 +24,7 @@ static const char* irc_colors_to_css[IRC_NUM_COLORS] = {
"cyan",
"lightcyan",
"lightblue",
"lightmagenta",
"hotpink",
"gray",
"white",
};
+1
View File
@@ -2,6 +2,7 @@
#include <utils/tolower.hpp>
#include <utils/split.hpp>
#include <fstream>
#include <algorithm>
#include <cstdlib>
#include <cstring>
-2
View File
@@ -16,8 +16,6 @@
#pragma once
#include <functional>
#include <fstream>
#include <memory>
#include <vector>
#include <string>
#include <map>
+9
View File
@@ -2,6 +2,7 @@
#ifdef USE_DATABASE
#include <database/select_query.hpp>
#include <database/delete_query.hpp>
#include <database/save.hpp>
#include <database/database.hpp>
#include <utils/get_first_non_empty.hpp>
@@ -317,6 +318,14 @@ std::vector<Database::RosterItem> 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;
+1
View File
@@ -153,6 +153,7 @@ class Database
static void delete_roster_item(const std::string& local, const std::string& remote);
static std::vector<Database::RosterItem> get_contact_list(const std::string& local);
static std::vector<Database::RosterItem> get_full_roster();
static void delete_old_muclines(const long& seconds);
static void close();
static void open(const std::string& filename);
+9
View File
@@ -0,0 +1,9 @@
#pragma once
enum class DatabaseType
{
none,
sqlite3,
postgresql,
};
+2 -1
View File
@@ -7,11 +7,11 @@
* Everything else (all portable stuf) should go outside of this class.
*/
#include <database/database_type.hpp>
#include <database/statement.hpp>
#include <memory>
#include <string>
#include <vector>
#include <tuple>
#include <set>
@@ -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};
};
+3
View File
@@ -101,4 +101,7 @@ std::string PostgresqlEngine::id_column_type()
return "SERIAL";
}
DatabaseType PostgresqlEngine::get_type() {
return DatabaseType::postgresql;
};
#endif
+3 -1
View File
@@ -2,9 +2,9 @@
#include <biboumi.h>
#include <string>
#include <stdexcept>
#include <memory>
#include <database/database_type.hpp>
#include <database/statement.hpp>
#include <database/engine.hpp>
@@ -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;
};
+1
View File
@@ -1,5 +1,6 @@
#pragma once
#include <string>
#include <type_traits>
template <typename... T>
+46 -2
View File
@@ -7,9 +7,14 @@
#include <database/query.hpp>
#include <config/config.hpp>
#include <utils/tolower.hpp>
#include <logger/logger.hpp>
#include <vector>
#include <utils/dirname.hpp>
#include <filesystem>
#include <sys/stat.h>
Sqlite3Engine::Sqlite3Engine(sqlite3* db):
db(db)
@@ -46,6 +51,8 @@ std::set<std::string> Sqlite3Engine::get_all_columns_from_table(const std::strin
std::unique_ptr<DatabaseEngine> 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<DatabaseEngine> Sqlite3Engine::open(const std::string& filename)
sqlite3_close(new_db);
throw std::runtime_error("");
}
return std::make_unique<Sqlite3Engine>(new_db);
std::unique_ptr<DatabaseEngine> db = std::make_unique<Sqlite3Engine>(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<bool, std::string> 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
+21
View File
@@ -29,8 +29,29 @@ class Sqlite3Engine: public DatabaseEngine
std::unique_ptr<Statement> 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
-1
View File
@@ -5,7 +5,6 @@
#include <database/delete_query.hpp>
#include <database/row.hpp>
#include <algorithm>
#include <string>
#include <set>
+1 -1
View File
@@ -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)
+195 -15
View File
@@ -11,6 +11,7 @@
#include <config/config.hpp>
#include <utils/tolower.hpp>
#include <utils/split.hpp>
#include <irc/parse_caps.hpp>
#include <utils/string.hpp>
#include <sstream>
@@ -83,6 +84,7 @@ static const std::unordered_map<std::string,
{"KICK", {&IrcClient::on_kick, {3, 0}}},
{"INVITE", {&IrcClient::on_invite, {2, 0}}},
{"CAP", {&IrcClient::on_cap, {3, 0}}},
{"BATCH", {&IrcClient::on_batch, {1, 0}}},
#ifdef WITH_SASL
{"AUTHENTICATE", {&IrcClient::on_authenticate, {1, 0}}},
{"900", {&IrcClient::on_sasl_login, {3, 0}}},
@@ -155,7 +157,7 @@ IrcClient::IrcClient(std::shared_ptr<Poller>& 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<Database::TrustedFingerprint>());
this->credential_manager->set_trusted_fingerprint(options.col<Database::TrustedFingerprint>());
# endif
if (Config::get("fixed_irc_server", "").empty() &&
!options.col<Database::Address>().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<IrcMessage, MessageCallback>&& 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<std::string>& parameters, std::vector<IrcMessage>& messages, MessageCallback callback)
{
std::vector<std::string> 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<const std::string> 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<IrcMessage>()));
}
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;
}
+14
View File
@@ -26,6 +26,7 @@
#include <map>
#include <set>
#include <utils/tokens_bucket.hpp>
#include <unordered_set>
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<std::string>& parameters, std::vector<IrcMessage>& 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<const std::string> 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<std::string, std::optional<std::string>> cap_ls_response;
std::map<std::string, Capability> capabilities;
std::unordered_set<std::string> 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::string, std::pair<std::pair<std::string, std::vector<std::string>>, std::vector<IrcMessage>>> batches;
};
+41 -1
View File
@@ -1,8 +1,27 @@
#include <irc/irc_message.hpp>
#include <iostream>
#include <string>
#include <utils/split.hpp>
#include <optional>
IrcMessage::IrcMessage(std::stringstream ss)
{
if (ss.peek() == '@')
{
std::string raw_tags;
ss.ignore();
ss >> raw_tags;
std::vector<std::string> 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<std::string>&& 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<std::string>&& args):
tags(),
prefix(),
command(std::move(command)),
arguments(std::move(args))
{
}
IrcMessage::IrcMessage(std::unordered_map<std::string, std::optional<std::string>>&& tags,
std::string&& prefix,
std::string&& command,
std::vector<std::string>&& 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 << "}";
+4
View File
@@ -5,6 +5,8 @@
#include <string>
#include <ostream>
#include <sstream>
#include <unordered_map>
#include <optional>
class IrcMessage
{
@@ -13,6 +15,7 @@ public:
IrcMessage(std::string str): IrcMessage{std::stringstream{str}} {}
IrcMessage(std::string&& prefix, std::string&& command, std::vector<std::string>&& args);
IrcMessage(std::string&& command, std::vector<std::string>&& args);
IrcMessage(std::unordered_map<std::string, std::optional<std::string>>&& tags, std::string&& prefix, std::string&& command, std::vector<std::string>&& args);
~IrcMessage() = default;
IrcMessage(const IrcMessage&) = delete;
@@ -20,6 +23,7 @@ public:
IrcMessage& operator=(const IrcMessage&) = delete;
IrcMessage& operator=(IrcMessage&&) = default;
std::unordered_map<std::string, std::optional<std::string>> tags;
std::string prefix;
std::string command;
std::vector<std::string> arguments;
+56
View File
@@ -0,0 +1,56 @@
#include "irc/parse_caps.hpp"
const std::unordered_map<std::string, std::optional<std::string>>
parse_caps(const std::string &caps) {
std::unordered_map<std::string, std::optional<std::string>> output;
// caps => multi-prefix sasl=PLAIN,EXTERNAL
// draft/multiline=max-bytes=40000,max-lines=10
const std::vector<std::string> &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<std::string> 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<std::string> parse_cap_val_list(const std::string &value) {
// value(PLAIN,EXTERNAL) => PLAIN,EXTERNAL
return utils::split(value, ',', false);
}
const std::unordered_map<std::string, std::optional<std::string>>
parse_cap_keyval(const std::string &value) {
std::unordered_map<std::string, std::optional<std::string>> output;
// value(PLAIN,EXTERNAL) => PLAIN,EXTERNAL
// value(max-bytes=40000,max-lines=10) =>
// max-bytes=40000,max-lines=10
const std::vector<std::string> &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<std::string> 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;
}
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include "utils/split.hpp"
#include <optional>
#include <set>
#include <sstream>
#include <unordered_map>
#include <vector>
/**
* Parse a list of IRC caps into a map of capabilities
*/
const std::unordered_map<std::string, std::optional<std::string>>
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<std::string> 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<std::string, std::optional<std::string>>
parse_cap_keyval(const std::string &value);
+3
View File
@@ -14,6 +14,7 @@
#include <csignal>
#include <identd/identd_server.hpp>
#include <scheduler/scheduler.hpp>
// A flag set by the SIGINT signal handler.
static std::atomic<bool> 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<IdentdServer>(*xmpp_component, p, static_cast<uint16_t>(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");
}
+1 -1
View File
@@ -39,7 +39,7 @@ const std::string& BasicCredentialsManager::get_trusted_fingerprint() const
}
void check_tls_certificate(const std::vector<Botan::X509_Certificate>& 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)
{
+1 -1
View File
@@ -19,7 +19,7 @@ class TCPSocketHandler;
* on a trusted fingerprint.
*/
void check_tls_certificate(const std::vector<Botan::X509_Certificate>& 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
+32 -27
View File
@@ -16,23 +16,29 @@
# include <botan/hex.h>
# include <botan/auto_rng.h>
# include <botan/tls_exceptn.h>
# include <botan/tls_session_manager_memory.h>
# include <botan/tls_session_manager_hybrid.h>
# include <config/config.hpp>
# include <utils/dirname.hpp>
namespace
{
Botan::AutoSeeded_RNG& get_rng()
std::shared_ptr<Botan::RandomNumberGenerator>& get_rng()
{
static Botan::AutoSeeded_RNG rng{};
static std::shared_ptr<Botan::RandomNumberGenerator> rng;
if (!rng)
rng = std::make_shared<Botan::AutoSeeded_RNG>();
return rng;
}
Botan::TLS::Session_Manager_In_Memory& get_session_manager()
std::shared_ptr<Botan::TLS::Session_Manager>& get_session_manager(std::shared_ptr<BasicCredentialsManager> 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<Botan::TLS::Session_Manager> session_manager;
if (!session_manager)
session_manager = std::make_shared<Botan::TLS::Session_Manager_Hybrid>(
std::make_unique<Botan::TLS::Session_Manager_In_Memory>(get_rng()),
creds, get_rng());
return session_manager;
}
}
@@ -50,7 +56,8 @@ TCPSocketHandler::TCPSocketHandler(std::shared_ptr<Poller>& poller):
SocketHandler(poller, -1),
use_tls(false)
#ifdef BOTAN_FOUND
,credential_manager()
,credential_manager(std::make_shared<BasicCredentialsManager>())
,policy(std::make_shared<BiboumiTLSPolicy>())
#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<Botan::TLS::Client>(
*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<const uint8_t> data)
{
this->in_buf += std::string(reinterpret_cast<const char*>(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<const uint8_t> data)
{
this->raw_send(std::string(reinterpret_cast<const char*>(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<Botan::X509_Certificate>& cert_chain,
const std::vector<std::shared_ptr<const Botan::OCSP::Response>>& ocsp_responses,
const std::vector<std::optional<Botan::OCSP::Response>>& ocsp_responses,
const std::vector<Botan::Certificate_Store*>& 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::vector<Botan::X509_Certi
if (this->abort_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);
}
}
+8 -7
View File
@@ -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<TCPSocketHandler>
#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<const uint8_t> 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<const uint8_t> 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<Botan::X509_Certificate>& cert_chain,
const std::vector<std::shared_ptr<const Botan::OCSP::Response>>& ocsp_responses,
const std::vector<std::optional<Botan::OCSP::Response>>& ocsp_responses,
const std::vector<Botan::Certificate_Store*>& 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<BasicCredentialsManager> credential_manager;
private:
BiboumiTLSPolicy policy;
std::shared_ptr<BiboumiTLSPolicy> 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
+53 -2
View File
@@ -8,7 +8,6 @@
#include <network/tls_policy.hpp>
#include <logger/logger.hpp>
#include <botan/parsing.h>
#include <botan/exceptn.h>
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<std::string, std::string> BiboumiTLSPolicy::read_cfg(std::istream& is) {
std::map<std::string, std::string> 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
+6
View File
@@ -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<std::string, std::string> read_cfg(std::istream& is);
static std::string clean_ws(std::string_view s);
};
#endif
+107
View File
@@ -0,0 +1,107 @@
#include "biboumi.h"
#include <config/config.hpp>
#include <logger/logger.hpp>
#include <scheduler/scheduler.hpp>
#include <utils/parse_duration.hpp>
#include <utils/timed_events.hpp>
#ifdef USE_DATABASE
#include <database/database_type.hpp>
#include <database/database.hpp>
#endif
static const std::unordered_map<
std::string, std::pair<std::chrono::seconds, std::function<void()>>>
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<void()> 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<long> 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<long>(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<std::chrono::seconds>(
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
+11
View File
@@ -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();
};
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <config/config.hpp>
namespace utils {
template <typename T>
inline std::vector<std::vector<T>> chunk(const std::vector<T> &vector,
const std::size_t size) {
std::vector<std::vector<T>> output;
std::vector<T> 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
@@ -0,0 +1,55 @@
#pragma once
#include "xmpp/jid.hpp"
#include <config/config.hpp>
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);
}
}
+36
View File
@@ -0,0 +1,36 @@
#include <cstdio>
#include <optional>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <utils/parse_duration.hpp>
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<std::string, long> multipliers = {
{"d", day}, {"w", day * 7}, {"mon", day * 31}, {"y", day * 365.2425},
{"s", 1}, {"min", 60}, {"h", 3600}, {"ho", 3600},
};
std::optional<long> 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
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include <optional>
#include <string>
namespace utils {
std::optional<long> parse_duration(const std::string &duration);
} // namespace utils
+1
View File
@@ -13,6 +13,7 @@
# include <vector>
# include <iomanip>
# include <sstream>
# include <cstdint>
#endif
std::string sha1(const std::string& input)
+1 -1
View File
@@ -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<void()> callback, std::string name="");
+3 -3
View File
@@ -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();
}
+6 -6
View File
@@ -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;
+22 -17
View File
@@ -3,6 +3,7 @@
#include <utils/scopeguard.hpp>
#include <bridge/bridge.hpp>
#include <config/config.hpp>
#include <utils/is_requester_allowed_to_persist.hpp>
#include <utils/string.hpp>
#include <utils/split.hpp>
#include <xmpp/jid.hpp>
@@ -34,7 +35,7 @@ void DisconnectUserStep1(XmppComponent& xmpp_component, AdhocSession&, XmlNode&
{
auto& biboumi_component = dynamic_cast<BiboumiComponent&>(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<Database::RecordHistory>());
}
else if (field->get_tag("var") == "persistent" &&
value)
value
&& utils::is_requester_allowed_to_persist(Jid(session.get_owner_jid())))
options.col<Database::GlobalPersistent>() = 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<Database::Ports>(), ';', 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<Database::EncodingIn>() = 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<Database::Persistent>() = 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<BiboumiComponent&>(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<BiboumiComponent&>(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();
}
+14 -5
View File
@@ -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;
+1 -1
View File
@@ -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;
+1
View File
@@ -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
+1 -1
View File
@@ -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<void*>(this));
// Install Expat handlers
+2 -2
View File
@@ -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
+13 -1
View File
@@ -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();
+6
View File
@@ -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()
{
+49
View File
@@ -0,0 +1,49 @@
#include "utils/chunk.hpp"
#include "catch2/catch_test_macros.hpp"
#include <vector>
TEST_CASE("chunk 5") {
const std::vector<int> 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<int> 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<int> 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 );
}
+2 -2
View File
@@ -1,4 +1,4 @@
#include "catch.hpp"
#include "catch2/catch_test_macros.hpp"
#include <bridge/colors.hpp>
#include <xmpp/xmpp_stanza.hpp>
@@ -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() == "<body xmlns='http://www.w3.org/1999/xhtml'>[<span style='font-style:italic;'/><span style='font-style:italic;color:lightmagenta;'>dolphin-emu/dolphin</span><span style='color:lightmagenta;'>] </span><span style='color:green;'>foo</span> commented on #283 (Add support for the guide button to XInput): <span style='text-decoration:underline;'/><span style='text-decoration:underline;color:blue;'>http://example.com</span><span style='text-decoration:underline;'/></body>");
CHECK(xhtml->to_string() == "<body xmlns='http://www.w3.org/1999/xhtml'>[<span style='font-style:italic;'/><span style='font-style:italic;color:hotpink;'>dolphin-emu/dolphin</span><span style='color:hotpink;'>] </span><span style='color:green;'>foo</span> commented on #283 (Add support for the guide button to XInput): <span style='text-decoration:underline;'/><span style='text-decoration:underline;color:blue;'>http://example.com</span><span style='text-decoration:underline;'/></body>");
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");
+1 -1
View File
@@ -1,4 +1,4 @@
#include "catch.hpp"
#include "catch2/catch_test_macros.hpp"
#include "io_tester.hpp"
#include <iostream>
+1 -1
View File
@@ -1,4 +1,4 @@
#include "catch.hpp"
#include "catch2/catch_test_macros.hpp"
#include <biboumi.h>
+1 -1
View File
@@ -1,4 +1,4 @@
#include "catch.hpp"
#include "catch2/catch_test_macros.hpp"
#include <utils/encoding.hpp>
+23 -29
View File
@@ -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())
+1 -1
View File
@@ -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:
+30 -101
View File
@@ -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.
@@ -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']"
],
[
@@ -11,7 +11,7 @@ scenario = (
send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}'><x xmlns='http://jabber.org/protocol/muc'/></presence>"),
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']",
@@ -9,7 +9,7 @@ scenario = (
# Second user joins
send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"),
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']",
@@ -11,7 +11,7 @@ scenario = (
# Second user joins
send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo@{biboumi_host}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"),
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']",
@@ -19,13 +19,13 @@ scenario = (
send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}'> <x xmlns='http://jabber.org/protocol/muc'><password>SECRET</password></x></presence>"),
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']"
],
[
@@ -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']"
],
[
@@ -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())]"],
+4 -2
View File
@@ -11,8 +11,10 @@ scenario = (
expect_stanza("/presence"),
expect_stanza("/message"),
send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><x xmlns='http://jabber.org/protocol/muc#user'><invite to='{nick_two}'/></x></message>"),
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("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><x xmlns='http://jabber.org/protocol/muc#user'><invite to='bertrand@example.com'/></x></message>"),
expect_stanza("/message[@to='bertrand@example.com'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='{jid_one}/{resource_one}']"),
@@ -0,0 +1,10 @@
from scenarios import *
scenario = (
scenarios.simple_channel_join.scenario,
send_stanza("<message id='the-message-id' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>hello</body></message>"),
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("<message id='the-message-id-2' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>hello</body><replace id='the-message-id' xmlns='urn:xmpp:message-correct:0'/></message>"),
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.']"),
)
+16 -12
View File
@@ -8,9 +8,7 @@ scenario = (
# Send a multi-line channel message
send_stanza("<message id='the-message-id' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>un\ndeux\ntrois</body></message>"),
# 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("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>hello</body></message>"),
@@ -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("<message id='the-message-id' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>a\nb\nc</body></message>"),
# 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("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' type='chat'><body>a\nb\nc</body></message>"),
# 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("<message from='{jid_one}/{resource_one}' to='{lower_nick_two}%{irc_server_one}' type='chat'><body>a\nb\nc</body></message>"),
# 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']"),
)
+1 -1
View File
@@ -7,5 +7,5 @@ scenario = (
expect_stanza("/message"),
send_stanza("<message from='{jid_one}/{resource_one}' to='{irc_server_one}' type='chat'><body>WHOIS {nick_one}</body></message>"),
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}']"),
)
@@ -9,6 +9,6 @@ scenario = (
expect_stanza("/message"),
send_stanza("<message from='{jid_one}/{resource_one}' to='{biboumi_host}' type='chat'><body>WHOIS {nick_one}</body></message>"),
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}']"),
)
+3 -1
View File
@@ -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("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/ignored' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"),
# 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"),
+1 -1
View File
@@ -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("<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"),
expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"),
+10 -8
View File
@@ -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)),
+6 -3
View File
@@ -1,4 +1,4 @@
#include "catch.hpp"
#include "catch2/catch_test_macros.hpp"
#include <irc/iid.hpp>
#include <irc/irc_user.hpp>
@@ -7,6 +7,7 @@
TEST_CASE("Irc user parsing")
{
Config::set("fixed_irc_server", "", false);
const std::map<char, char> 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<char, char> 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<char> chantypes {'#', '&'};
Config::set("fixed_irc_server", "", false);
const std::set<char> 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<char> chantypes {'#', '&'};
const std::set<char> 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");
+1 -1
View File
@@ -1,5 +1,5 @@
#include "io_tester.hpp"
#include "catch.hpp"
#include "catch2/catch_test_macros.hpp"
#include <iostream>
/**
+1 -1
View File
@@ -1,4 +1,4 @@
#include "catch.hpp"
#include "catch2/catch_test_macros.hpp"
#include <irc/irc_message.hpp>
+125
View File
@@ -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();
}
+1 -1
View File
@@ -1,4 +1,4 @@
#include "catch.hpp"
#include "catch2/catch_test_macros.hpp"
#include <xmpp/jid.hpp>
#include <biboumi.h>

Some files were not shown because too many files have changed in this diff Show More