* 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:
@@ -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/
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
irc.gimp.org.policy.txt
|
||||
@@ -1 +0,0 @@
|
||||
minimum_dh_group_size = 1024
|
||||
+58
-8
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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 && \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -24,7 +24,7 @@ static const char* irc_colors_to_css[IRC_NUM_COLORS] = {
|
||||
"cyan",
|
||||
"lightcyan",
|
||||
"lightblue",
|
||||
"lightmagenta",
|
||||
"hotpink",
|
||||
"gray",
|
||||
"white",
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <utils/tolower.hpp>
|
||||
#include <utils/split.hpp>
|
||||
|
||||
#include <fstream>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <map>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
enum class DatabaseType
|
||||
{
|
||||
none,
|
||||
sqlite3,
|
||||
postgresql,
|
||||
};
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
@@ -101,4 +101,7 @@ std::string PostgresqlEngine::id_column_type()
|
||||
return "SERIAL";
|
||||
}
|
||||
|
||||
DatabaseType PostgresqlEngine::get_type() {
|
||||
return DatabaseType::postgresql;
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
template <typename... T>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <database/delete_query.hpp>
|
||||
#include <database/row.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <set>
|
||||
|
||||
|
||||
+1
-1
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 << "}";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace utils {
|
||||
std::optional<long> parse_duration(const std::string &duration);
|
||||
} // namespace utils
|
||||
@@ -13,6 +13,7 @@
|
||||
# include <vector>
|
||||
# include <iomanip>
|
||||
# include <sstream>
|
||||
# include <cstdint>
|
||||
#endif
|
||||
|
||||
std::string sha1(const std::string& input)
|
||||
|
||||
@@ -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="");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
@@ -1,4 +1,4 @@
|
||||
#include "catch.hpp"
|
||||
#include "catch2/catch_test_macros.hpp"
|
||||
#include "io_tester.hpp"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#include "catch.hpp"
|
||||
#include "catch2/catch_test_macros.hpp"
|
||||
|
||||
#include <biboumi.h>
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#include "catch.hpp"
|
||||
#include "catch2/catch_test_macros.hpp"
|
||||
|
||||
#include <utils/encoding.hpp>
|
||||
|
||||
|
||||
@@ -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: [33;1m%s[0m%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())
|
||||
|
||||
@@ -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
@@ -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())]"],
|
||||
|
||||
@@ -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.']"),
|
||||
)
|
||||
@@ -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']"),
|
||||
)
|
||||
|
||||
@@ -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}']"),
|
||||
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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']"),
|
||||
|
||||
@@ -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
@@ -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
@@ -1,5 +1,5 @@
|
||||
#include "io_tester.hpp"
|
||||
#include "catch.hpp"
|
||||
#include "catch2/catch_test_macros.hpp"
|
||||
#include <iostream>
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#include "catch.hpp"
|
||||
#include "catch2/catch_test_macros.hpp"
|
||||
|
||||
#include <irc/irc_message.hpp>
|
||||
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user