summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore40
-rw-r--r--.gitlab-ci.yml103
-rw-r--r--CHANGELOG.rst76
-rw-r--r--CMakeLists.txt309
-rw-r--r--COPYING20
-rw-r--r--INSTALL.rst161
-rw-r--r--README.rst62
-rw-r--r--biboumi.h.cmake1
-rw-r--r--cmake/Modules/CodeCoverage.cmake213
-rw-r--r--cmake/Modules/FindLITESQL.cmake76
-rw-r--r--conf/biboumi.cfg7
-rw-r--r--database/database.xml46
-rw-r--r--doc/biboumi.1.rst514
-rw-r--r--docker/biboumi-test/debian/Dockerfile72
-rw-r--r--docker/biboumi-test/fedora/Dockerfile64
-rw-r--r--louloulibs/CMakeLists.txt146
-rw-r--r--louloulibs/cmake/Modules/FindBOTAN.cmake35
-rw-r--r--louloulibs/cmake/Modules/FindCARES.cmake37
-rw-r--r--louloulibs/cmake/Modules/FindICONV.cmake60
-rw-r--r--louloulibs/cmake/Modules/FindLIBIDN.cmake41
-rw-r--r--louloulibs/cmake/Modules/FindLIBUUID.cmake41
-rw-r--r--louloulibs/cmake/Modules/FindSYSTEMD.cmake39
-rw-r--r--louloulibs/config/config.cpp104
-rw-r--r--louloulibs/config/config.hpp94
-rw-r--r--louloulibs/logger/logger.cpp38
-rw-r--r--louloulibs/logger/logger.hpp126
-rw-r--r--louloulibs/louloulibs.h.cmake9
-rw-r--r--louloulibs/network/credentials_manager.cpp116
-rw-r--r--louloulibs/network/credentials_manager.hpp39
-rw-r--r--louloulibs/network/dns_handler.cpp134
-rw-r--r--louloulibs/network/dns_handler.hpp58
-rw-r--r--louloulibs/network/dns_socket_handler.cpp48
-rw-r--r--louloulibs/network/dns_socket_handler.hpp49
-rw-r--r--louloulibs/network/poller.cpp228
-rw-r--r--louloulibs/network/poller.hpp94
-rw-r--r--louloulibs/network/resolver.cpp214
-rw-r--r--louloulibs/network/resolver.hpp128
-rw-r--r--louloulibs/network/socket_handler.hpp42
-rw-r--r--louloulibs/network/tcp_socket_handler.cpp501
-rw-r--r--louloulibs/network/tcp_socket_handler.hpp274
-rw-r--r--louloulibs/utils/encoding.cpp258
-rw-r--r--louloulibs/utils/encoding.hpp43
-rw-r--r--louloulibs/utils/revstr.cpp9
-rw-r--r--louloulibs/utils/revstr.hpp11
-rw-r--r--louloulibs/utils/scopeguard.hpp89
-rw-r--r--louloulibs/utils/sha1.cpp154
-rw-r--r--louloulibs/utils/sha1.hpp35
-rw-r--r--louloulibs/utils/split.cpp19
-rw-r--r--louloulibs/utils/split.hpp12
-rw-r--r--louloulibs/utils/string.cpp28
-rw-r--r--louloulibs/utils/string.hpp10
-rw-r--r--louloulibs/utils/timed_events.cpp49
-rw-r--r--louloulibs/utils/timed_events.hpp132
-rw-r--r--louloulibs/utils/timed_events_manager.cpp73
-rw-r--r--louloulibs/utils/tolower.cpp13
-rw-r--r--louloulibs/utils/tolower.hpp11
-rw-r--r--louloulibs/utils/xdg.cpp29
-rw-r--r--louloulibs/utils/xdg.hpp14
-rw-r--r--louloulibs/xmpp/adhoc_command.cpp89
-rw-r--r--louloulibs/xmpp/adhoc_command.hpp44
-rw-r--r--louloulibs/xmpp/adhoc_commands_handler.cpp123
-rw-r--r--louloulibs/xmpp/adhoc_commands_handler.hpp71
-rw-r--r--louloulibs/xmpp/adhoc_session.cpp35
-rw-r--r--louloulibs/xmpp/adhoc_session.hpp88
-rw-r--r--louloulibs/xmpp/body.hpp12
-rw-r--r--louloulibs/xmpp/jid.cpp99
-rw-r--r--louloulibs/xmpp/jid.hpp44
-rw-r--r--louloulibs/xmpp/roster.cpp21
-rw-r--r--louloulibs/xmpp/roster.hpp71
-rw-r--r--louloulibs/xmpp/xmpp_component.cpp664
-rw-r--r--louloulibs/xmpp/xmpp_component.hpp243
-rw-r--r--louloulibs/xmpp/xmpp_parser.cpp172
-rw-r--r--louloulibs/xmpp/xmpp_parser.hpp133
-rw-r--r--louloulibs/xmpp/xmpp_stanza.cpp229
-rw-r--r--louloulibs/xmpp/xmpp_stanza.hpp146
-rw-r--r--packaging/biboumi.spec.cmake81
-rw-r--r--src/bridge/bridge.cpp907
-rw-r--r--src/bridge/bridge.hpp293
-rw-r--r--src/bridge/colors.cpp170
-rw-r--r--src/bridge/colors.hpp56
-rw-r--r--src/bridge/list_element.hpp19
-rw-r--r--src/database/database.cpp87
-rw-r--r--src/database/database.hpp53
-rw-r--r--src/irc/iid.cpp118
-rw-r--r--src/irc/iid.hpp79
-rw-r--r--src/irc/irc_channel.cpp60
-rw-r--r--src/irc/irc_channel.hpp70
-rw-r--r--src/irc/irc_client.cpp1120
-rw-r--r--src/irc/irc_client.hpp383
-rw-r--r--src/irc/irc_message.cpp61
-rw-r--r--src/irc/irc_message.hpp28
-rw-r--r--src/irc/irc_user.cpp57
-rw-r--r--src/irc/irc_user.hpp33
-rw-r--r--src/main.cpp202
-rw-r--r--src/utils/empty_if_fixed_server.hpp26
-rw-r--r--src/utils/reload.cpp34
-rw-r--r--src/utils/reload.hpp4
-rw-r--r--src/xmpp/biboumi_adhoc_commands.cpp635
-rw-r--r--src/xmpp/biboumi_adhoc_commands.hpp23
-rw-r--r--src/xmpp/biboumi_component.cpp632
-rw-r--r--src/xmpp/biboumi_component.hpp109
-rw-r--r--tests/colors.cpp54
-rw-r--r--tests/config.cpp54
-rw-r--r--tests/database.cpp97
-rw-r--r--tests/dns.cpp91
-rw-r--r--tests/encoding.cpp56
-rw-r--r--tests/end_to_end/__main__.py1027
-rw-r--r--tests/end_to_end/biboumi.supp10
-rw-r--r--tests/end_to_end/ircd.conf510
-rw-r--r--tests/iid.cpp130
-rw-r--r--tests/io_tester.cpp30
-rw-r--r--tests/io_tester.hpp45
-rw-r--r--tests/jid.cpp39
-rw-r--r--tests/logger.cpp57
-rw-r--r--tests/test.cpp2
-rw-r--r--tests/timed_events.cpp62
-rw-r--r--tests/utils.cpp102
-rw-r--r--tests/uuid.cpp13
-rw-r--r--tests/xmpp.cpp47
-rw-r--r--unit/biboumi.service.cmake16
120 files changed, 15514 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..83cd31d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,40 @@
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
+
+# Gcov files
+*.gcno
+*.gcov
+*.gcda
+
+# Python files
+*.pyc
+*.pyo
+
+# Build directories
+build/
+
+# Clion directory
+.idea/ \ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..c1cc979
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,103 @@
+before_script:
+ - uname -a
+ - whoami
+ - echo $LANG
+ - g++ --version
+ - clang++ --version
+ - rm -rf build/
+ - mkdir build/
+ - cd build
+
+variables:
+ COMPILER: "g++"
+ BUILD_TYPE: "Debug"
+ BOTAN: "-DWITH_BOTAN=1"
+ CARES: "-DWITH_CARES=1"
+ SYSTEMD: "-DWITH_SYSTEMD=1"
+ LIBIDN: "-DWITH_LIBIDN=1"
+ LITESQL: "-DWITH_LITESQL=1"
+
+.template:basic_build: &basic_build
+ stage: build
+ script:
+ - "echo Running cmake with the following parameters: -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${CARES} ${SYSTEMD} ${LIBIDN} ${LITESQL}"
+ - cmake .. -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${CARES} ${SYSTEMD} ${LIBIDN} ${LITESQL}
+ - make biboumi -j$(nproc)
+ - make check -j$(nproc)
+
+image: biboumi-test-fedora:latest
+
+build:1:
+ variables:
+ BOTAN: "-DWITHOUT_BOTAN=1"
+ <<: *basic_build
+
+build:2:
+ variables:
+ CARES: "-DWITHOUT_CARES=1"
+ <<: *basic_build
+
+build:3:
+ variables:
+ LITESQL: "-DWITHOUT_LITESQL=1"
+ <<: *basic_build
+
+build:4:
+ variables:
+ LITESQL: "-DWITHOUT_LITESQL=1"
+ BOTAN: "-DWITHOUT_BOTAN=1"
+ <<: *basic_build
+
+build:5:
+ variables:
+ LITESQL: "-DWITHOUT_LITESQL=1"
+ CARES: "-DWITHOUT_CARES=1"
+ <<: *basic_build
+
+build:6:
+ variables:
+ BOTAN: "-DWITHOUT_BOTAN=1"
+ CARES: "-DWITHOUT_CARES=1"
+ <<: *basic_build
+
+build:6:
+ variables:
+ LIBIDN: "-DWITHOUT_LIBIDN=1"
+ CARES: "-DWITHOUT_CARES=1"
+ <<: *basic_build
+
+build:rpm:
+ stage: build
+ script:
+ - cmake .. -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${CARES} ${SYSTEMD} ${LIBIDN} ${LITESQL}
+ - make rpm -j$(nproc)
+ artifacts:
+ paths:
+ - build/rpmbuild/RPMS
+ - build/rpmbuild/SRPMS
+ when: always
+
+
+.template:basic_test: &basic_test
+ stage: test
+ script:
+ - cmake .. -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${CARES} ${SYSTEMD} ${LIBIDN} ${LITESQL}
+ - make biboumi -j$(nproc)
+ - make check
+ - make coverage
+ - mkdir tests_outputs && pushd tests_outputs && make e2e -j$(nproc) -C .. && popd
+ artifacts:
+ paths:
+ - build/coverage/
+ - build/tests_outputs/
+ when: always
+
+test:debian:
+ stage: test
+ image: biboumi-test-debian:latest
+ <<: *basic_test
+
+test:fedora:
+ stage: test
+ image: biboumi-test-fedora:latest
+ <<: *basic_test \ No newline at end of file
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..75a96cc
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,76 @@
+Version 3.0 - 2016-08-03
+========================
+
+ - Support multiple-nick sessions: a user can join an IRC channel behind
+ one single nick, using multiple different clients, at the same time (as
+ long as each client is using the same bare JID).
+ - Database support for persistant per-user per-server configuration. Add
+ `LiteSQL <https://dev.louiz.org/projects/litesql>` as an optional
+ dependency.
+ - Add ad-hoc commands that lets each user configure various things
+ - Support an after-connect command that will be sent to the server
+ just after the user gets connected to it.
+ - Support the sending of a PASS command.
+ - Lets the users configure their username and realname, if the
+ realname_customization is set to true.
+ - The remote TLS certificates are checked against the system’s trusted
+ CAs, unless the user used the configuration option that ignores these
+ checks.
+ - Lets the user set a sha-1 hash to identify a server certificate that
+ should always be trusted.
+ - Add an outgoing_bind option.
+ - Add an ad-hoc command to forcefully disconnect a user from one or
+ more servers.
+ - Let the user configure the incoming encoding of an IRC server (the
+ default behaviour remains unchanged: check if it’s valid utf-8 and if
+ not, decode as latin-1).
+ - Support `multi-prefix <http://ircv3.net/specs/extensions/multi-prefix-3.1.html>`.
+ - And of course, many bufixes.
+ - Run unit tests and a test suite, build the RPM and check many things
+ automatically using gitlab-ci.
+
+
+Version 2.0 - 2015-05-29
+========================
+
+ - List channels on an IRC server through an XMPP disco items request
+ - Let the user send any arbitrary raw IRC command by sending a
+ message to the IRC server’s JID.
+ - By default, look for the configuration file as per the XDG
+ basedir spec.
+ - Support PING requests in all directions.
+ - Improve the way we forward received NOTICEs by remembering to
+ which users we previously sent a private message. This improves the
+ user experience when talking to NickServ.
+ - Support joining key-protected channels
+ - Setting a participant's role/affiliation now results in a change of IRC
+ mode, instead of being ignored. Setting Toto's affiliation to admin is
+ now equivalent to “/mode +o Toto”
+ - Fix the reconnection to the XMPP server to try every 2 seconds
+ instead of immediately. This avoid hogging resources for nothing
+ - Asynchronously resolve domain names by optionally using the DNS
+ library c-ares.
+ - Add a reload add-hoc command, to reload biboumi's configuration
+ - Add a fixed_irc_server option. With this option enabled,
+ biboumi can only connect to the one single IRC server configured
+
+Version 1.1 - 2014-07-16
+========================
+
+ - Fix a segmentation fault when connecting to an IRC server using IPv6
+
+Version 1.0 - 2014-07-12
+========================
+
+ - First stable release.
+ - Mostly complete MUC to IRC, and IRC to MUC support
+ - Complete handling of private messages
+ - Full IRC modes support: setting any IRC mode, and receiving notifications
+ for every mode change
+ - Verbose connection status notifications
+ - Conversion from IRC formatting to XHTML-im
+ - Ad-hoc commands support
+ - Basic TLS support: auto-accepts all certificates, no cipher
+ configuration, no way to force usage of TLS (it is used only if
+ available, clear connection is automatically used as a fallback)
+ - IPv6 support
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..d6d5ce8
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,309 @@
+cmake_minimum_required(VERSION 3.0)
+
+project(biboumi)
+
+set(${PROJECT_NAME}_VERSION_MAJOR 3)
+set(${PROJECT_NAME}_VERSION_MINOR 0)
+set(${PROJECT_NAME}_VERSION_SUFFIX "")
+
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++1y -pedantic -Wall -Wextra")
+if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
+ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fprofile-arcs -ftest-coverage --coverage")
+endif()
+
+# Define a __FILENAME__ macro to get the filename of each file, instead of
+# the full path as in __FILE__
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__FILENAME__='\"$(subst ${CMAKE_SOURCE_DIR}/,,$(abspath $<))\"'")
+
+#
+## Look for external libraries
+#
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules/")
+
+#
+## Get the software version
+#
+set(ARCHIVE_NAME ${CMAKE_PROJECT_NAME}-${${PROJECT_NAME}_VERSION_MAJOR}.${${PROJECT_NAME}_VERSION_MINOR})
+set(RPM_VERSION ${${PROJECT_NAME}_VERSION_MAJOR}.${${PROJECT_NAME}_VERSION_MINOR})
+
+if(${PROJECT_NAME}_VERSION_SUFFIX MATCHES ".+")
+ set(ARCHIVE_NAME ${ARCHIVE_NAME}${${PROJECT_NAME}_VERSION_SUFFIX})
+ set(RPM_VERSION ${RPM_VERSION}${${PROJECT_NAME}_VERSION_SUFFIX})
+endif()
+
+if(${PROJECT_NAME}_VERSION_SUFFIX MATCHES "^~dev$")
+ # If we are on a dev version, append the hash of the current git HEAD to
+ # the version
+ include(FindGit)
+ if(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git")
+ execute_process(COMMAND git --git-dir=${CMAKE_SOURCE_DIR}/.git rev-parse --short HEAD
+ OUTPUT_VARIABLE GIT_REVISION
+ OUTPUT_STRIP_TRAILING_WHITESPACE)
+ if(GIT_REVISION)
+ set(${PROJECT_NAME}_VERSION_SUFFIX "${${PROJECT_NAME}_VERSION_SUFFIX} (${GIT_REVISION})")
+ set(ARCHIVE_NAME ${ARCHIVE_NAME}${GIT_REVISION})
+ set(RPM_VERSION ${RPM_VERSION}${GIT_REVISION})
+ endif()
+ endif()
+endif()
+
+set(SOFTWARE_VERSION
+ ${${PROJECT_NAME}_VERSION_MAJOR}.${${PROJECT_NAME}_VERSION_MINOR}${${PROJECT_NAME}_VERSION_SUFFIX})
+
+include(CheckFunctionExists)
+check_function_exists(ppoll HAVE_PPOLL_FUNCTION)
+
+# To be able to include the config.h and other files generated by cmake
+include_directories("${CMAKE_CURRENT_BINARY_DIR}/src/")
+include_directories("${CMAKE_CURRENT_SOURCE_DIR}/src/")
+include_directories("${CMAKE_CURRENT_BINARY_DIR}/")
+
+#
+## Documentation
+#
+execute_process(COMMAND "date" "+%Y-%m-%d" OUTPUT_VARIABLE DOC_DATE
+ OUTPUT_STRIP_TRAILING_WHITESPACE)
+set(MAN_PAGE ${CMAKE_CURRENT_BINARY_DIR}/doc/${PROJECT_NAME}.1)
+set(DOC_PAGE ${CMAKE_CURRENT_SOURCE_DIR}/doc/${PROJECT_NAME}.1.rst)
+if (NOT PANDOC_EXECUTABLE)
+ find_program(PANDOC_EXECUTABLE NAMES pandoc
+ DOC "The pandoc software, to build the man page from the rst documentation")
+ if(PANDOC_EXECUTABLE)
+ message(STATUS "Found Pandoc: ${PANDOC_EXECUTABLE}")
+ set(WITH_DOC true)
+ file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doc/)
+ add_custom_command(OUTPUT ${MAN_PAGE}
+ COMMAND ${PANDOC_EXECUTABLE} -M date="${DOC_DATE}" -s -t man ${DOC_PAGE} -o ${MAN_PAGE}
+ DEPENDS ${DOC_PAGE})
+ add_custom_target(doc ALL DEPENDS ${MAN_PAGE})
+ else()
+ message(STATUS "Pandoc not found, documentation cannot be built")
+ endif()
+endif()
+mark_as_advanced(PANDOC_EXECUTABLE)
+
+# Look for litesql and enable the database if found
+if(WITH_LITESQL)
+ find_package(LITESQL REQUIRED)
+elseif(NOT WITHOUT_LITESQL)
+ find_package(LITESQL)
+endif()
+
+if(LITESQL_FOUND)
+ LITESQL_GENERATE_CPP("database/database.xml"
+ "biboudb"
+ LITESQL_GENERATED_SOURCES)
+
+ add_library(database STATIC src/database/database.cpp
+ ${LITESQL_GENERATED_SOURCES})
+ target_link_libraries(database ${LITESQL_LIBRARIES})
+ if(BOTAN_FOUND)
+ target_link_libraries(database ${BOTAN_LIBRARIES})
+ endif()
+ set(USE_DATABASE TRUE)
+endif()
+
+add_subdirectory("louloulibs")
+include_directories("louloulibs")
+
+include_directories(${EXPAT_INCLUDE_DIRS})
+include_directories(${ICONV_INCLUDE_DIRS})
+include_directories(${LIBUUID_INCLUDE_DIRS})
+
+# If they are found in louloulibs CMakeLists.txt, we inherite these values
+if(SYSTEMD_FOUND)
+ include_directories(${SYSTEMD_INCLUDE_DIRS})
+endif()
+if(BOTAN_FOUND)
+ include_directories(SYSTEM ${BOTAN_INCLUDE_DIRS})
+endif()
+if(CARES_FOUND)
+ include_directories(${CARES_INCLUDE_DIRS})
+endif()
+
+#
+## utils
+#
+file(GLOB source_src_utils
+ src/utils/*.[hc]pp)
+# Todo, switch to target_sources(utils) when we go cmake >=3.1 only
+add_library(src_utils STATIC ${source_src_utils})
+target_link_libraries(src_utils logger config)
+if(USE_DATABASE)
+ target_link_libraries(src_utils database)
+endif()
+
+#
+## irclib
+#
+file(GLOB source_irc
+ src/irc/*.[hc]pp)
+add_library(irc STATIC ${source_irc})
+target_link_libraries(irc network utils logger)
+
+#
+## xmpp
+#
+file(GLOB source_xmpp
+ src/xmpp/*.[hc]pp)
+add_library(xmpp STATIC ${source_xmpp})
+target_link_libraries(xmpp xmpplib bridge network utils src_utils logger)
+
+if(USE_DATABASE)
+ target_link_libraries(xmpp database)
+ target_link_libraries(irc database)
+endif()
+
+#
+## bridge
+#
+file(GLOB source_bridge
+ src/bridge/*.[hc]pp)
+add_library(bridge STATIC ${source_bridge})
+target_link_libraries(bridge xmpp irc utils logger)
+
+#
+## Main executable
+#
+add_executable(${PROJECT_NAME} src/main.cpp)
+target_link_libraries(${PROJECT_NAME}
+ xmpp
+ irc
+ bridge
+ utils
+ src_utils
+ config)
+if(SYSTEMD_FOUND)
+ target_link_libraries(xmpp ${SYSTEMD_LIBRARIES})
+endif()
+
+#
+## Tests
+#
+file(GLOB source_tests
+ tests/*.cpp)
+add_executable(test_suite EXCLUDE_FROM_ALL
+ ${source_tests})
+target_link_libraries(test_suite
+ xmpplib
+ xmpp
+ irc
+ bridge
+ utils
+ config
+ logger
+ network)
+if(USE_DATABASE)
+ target_link_libraries(test_suite
+ database)
+endif()
+
+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}/include/"
+ )
+ add_dependencies(test_suite catch)
+endif()
+add_custom_target(check COMMAND "test_suite" "-s"
+ DEPENDS test_suite biboumi)
+add_custom_target(e2e COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/tests/end_to_end/"
+ DEPENDS biboumi)
+add_custom_target(e2e_valgrind COMMAND "E2E_BIBOUMI_SUPP_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/end_to_end/" "E2E_BIBOUMI_VALGRIND=1" "python3" "${CMAKE_CURRENT_SOURCE_DIR}/tests/end_to_end/"
+ DEPENDS biboumi)
+
+
+#
+## Code coverage
+#
+if(CMAKE_BUILD_TYPE MATCHES Debug)
+ include(CodeCoverage)
+ SETUP_TARGET_FOR_COVERAGE(coverage
+ test_suite
+ coverage
+ )
+endif()
+
+#
+## Install target
+#
+install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin)
+install(FILES ${MAN_PAGE} DESTINATION share/man/man1 OPTIONAL COMPONENT documentation)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/biboumi.service DESTINATION lib/systemd/system COMPONENT init)
+install(FILES conf/biboumi.cfg DESTINATION /etc/biboumi COMPONENT configuration)
+
+#
+## Dist target
+## Generate a release tarball from the git sources
+#
+add_custom_target(dist
+ COMMAND git archive --prefix=${ARCHIVE_NAME}/ --format=tar HEAD
+ > ${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'
+ # Remove a potential existing archive
+ COMMAND rm -f ${CMAKE_CURRENT_BINARY_DIR}/${ARCHIVE_NAME}.tar.xz
+ # Compress the archive
+ COMMAND xz ${CMAKE_CURRENT_BINARY_DIR}/${ARCHIVE_NAME}.tar
+ COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --cyan "${ARCHIVE_NAME}.tar.xz created."
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+ )
+add_dependencies(dist catch)
+
+add_custom_target(rpm
+ COMMAND mkdir -p rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
+ COMMAND rpmbuild --define "_topdir `pwd`/rpmbuild/" --define "_sourcedir `pwd`" -ba biboumi.spec
+ )
+add_dependencies(rpm dist)
+
+if(BOTAN_FOUND)
+ set(STR_WITH_BOTAN "Botan: yes")
+else()
+ set(STR_WITH_BOTAN "Botan: no")
+endif()
+if(CARES_FOUND)
+ set(STR_WITH_CARES "c-ares: yes")
+else()
+ set(STR_WITH_CARES "c-ares: no")
+endif()
+add_custom_target(PrintBuildParameters ALL
+ ${CMAKE_COMMAND} -E cmake_echo_color --cyan "Compiling ${PROJECT_NAME} with ${STR_WITH_BOTAN}, ${STR_WITH_CARES}")
+
+configure_file(biboumi.h.cmake src/biboumi.h)
+
+set(SYSTEMD_SERVICE_TYPE_DOCSTRING "The value used as the Type= in the systemd unit file.")
+set(WATCHDOG_SEC_DOCSTRING "The value used as WatchdogSec= in the systemd unit file.")
+if(SYSTEMD_FOUND)
+ set(SYSTEMD_SERVICE_TYPE "notify" CACHE STRING ${SYSTEMD_SERVICE_TYPE_DOCSTRING})
+ set(WATCHDOG_SEC "20" CACHE STRING ${WATCHDOG_SEC_DOCSTRING})
+else()
+ set(SYSTEMD_SERVICE_TYPE "simple" CACHE STRING ${SYSTEMD_SERVICE_TYPE_DOCSTRING})
+ set(WATCHDOG_SEC "" CACHE STRING ${WATCHDOG_SEC_DOCSTRING})
+endif()
+set(SERVICE_USER_DOCSTRING "The value used as the User= in the systemd unit file.")
+if(NOT DEFINED SERVICE_USER)
+ set(SERVICE_USER "nobody" CACHE STRING ${SERVICE_USER_DOCSTRING})
+endif()
+set(SERVICE_GROUP_DOCSTRING "The value used as the Group= in the systemd unit file.")
+if(NOT DEFINED SERVICE_GROUP)
+ set(SERVICE_GROUP "nobody" CACHE STRING ${SERVICE_GROUP_DOCSTRING})
+endif()
+configure_file(unit/biboumi.service.cmake biboumi.service)
+
+# The date MUST be in english format
+set(ENV{LANG} "en_US.utf-8")
+execute_process(COMMAND "date" "+%a %b %d %Y" OUTPUT_VARIABLE RPM_DATE
+ OUTPUT_STRIP_TRAILING_WHITESPACE)
+unset(ENV{LANG})
+
+configure_file(packaging/biboumi.spec.cmake biboumi.spec)
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..e9d67c3
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,20 @@
+Copyright (c) 2015 Florent Le Coz
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+claim that you wrote the original software. If you use this software
+in a product, an acknowledgment in the product documentation would be
+appreciated but is not required.
+
+2. Altered source versions must be plainly marked as such, and must not be
+misrepresented as being the original software.
+
+3. This notice may not be removed or altered from any source
+distribution.
diff --git a/INSTALL.rst b/INSTALL.rst
new file mode 100644
index 0000000..1526d7e
--- /dev/null
+++ b/INSTALL.rst
@@ -0,0 +1,161 @@
+INSTALL
+=======
+
+tl;dr
+-----
+
+ cmake . && make && ./biboumi
+
+If that didn’t work, read on.
+
+Dependencies
+------------
+
+Build and runtime dependencies:
+
+Tools:
+~~~~~~
+
+- A C++14 compiler (clang >= 3.4 or gcc >= 4.9 for example)
+- CMake
+- pandoc (optional) to build the man page
+
+Libraries:
+~~~~~~~~~~
+
+expat_
+ Used to parse XML from the XMPP server.
+
+libiconv_
+ Encoding from anything into UTF-8
+
+libuuid_
+ Generate unique IDs
+
+libidn_ (optional, but recommended)
+ Provides the stringprep functionality. Without it, JIDs for IRC users are
+ not provided.
+
+c-ares_ (optional, but recommended)
+ Asynchronously resolve domain names. This offers better reactivity and
+ performances when connecting to a big number of IRC servers at the same
+ time.
+
+libbotan_ 1.11 (optional)
+ Provides TLS support. Without it, IRC connections are all made in
+ plain-text mode.
+ Other branches than the 1.11 are not supported.
+
+litesql_ (optional)
+ Provides a way to store various options in a (sqlite3) database. Each user
+ of the gateway can store their own values (for example their prefered port,
+ or their IRC password).
+
+systemd_ (optional)
+ Provides the support for a systemd service of Type=notify. This is useful only
+ if you are packaging biboumi in a distribution with Systemd.
+
+
+Configure
+---------
+
+Configure the build system using cmake, there are many solutions to do that,
+the simplest is to just run
+
+ cmake .
+
+in the current directory.
+
+The default build type is "Debug", if you want to build a release version,
+set the CMAKE_BUILD_TYPE variable to "release", by running this command
+instead:
+
+ cmake . -DCMAKE_BUILD_TYPE=release -DCMAKE_INSTALL_PREFIX=/usr
+
+You can also configure many parameters of the build (like customize CFLAGS,
+the install path, choose the compiler, or enabling some options like the
+POLLER to use), using the ncurses interface of ccmake:
+
+ ccmake .
+
+In ccmake, first use 'c' to configure the build system, edit the values you
+need and finaly use 'g' to generate the Makefiles to build the system and
+quit ccmake.
+
+You can also configure these options using a -D command line flag.
+
+The list of available options:
+
+- POLLER: lets you select the poller used by biboumi, at
+ compile-time. Possible values are:
+
+ - EPOLL: use the Linux-specific epoll(7). This is the default on Linux.
+ - POLL: use the standard poll(2). This is the default value on all non-Linux
+ platforms.
+
+- WITH_BOTAN and WITHOUT_BOTAN: The first force the usage of the Botan library,
+ if it is not found, the configuration process will fail. The second will
+ make the build process ignore the Botan library, it will not be used even
+ if it's available on the system. If none of these option is specified, the
+ library will be used if available and will be ignored otherwise.
+
+- WITH_LIBIDN and WITHOUT_LIBIDN: Just like the WITH(OUT)_BOTAN options, but
+ for the IDN library
+
+- WITH_SYSTEMD and WITHOUT_SYSTEMD: Just like the other WITH(OUT)_* options,
+ but for the Systemd library
+
+Example:
+
+ cmake . -DCMAKE_BUILD_TYPE=release -DCMAKE_INSTALL_PREFIX=/usr
+ -DWITH_BOTAN=1 -DWITHOUT_SYSTEMD=1
+
+This command will configure the project to build a release, with TLS enabled
+(using Botan) but without using Systemd (even if available on the system).
+
+
+Build
+-----
+Once you’ve configured everything using cmake, build the project
+
+ make
+
+
+Install
+-------
+And then, optionaly, Install the software system-wide
+
+ make install
+
+
+Testing
+-------
+You can run the test suite with
+
+ make check
+
+This project uses the Catch unit test framework, it will be automatically
+fetched with cmake, by cloning the github repository.
+
+You can also check the overall code coverage of this test suite by running
+
+ make coverage
+
+This requires gcov and lcov to be installed.
+
+
+Run
+---
+Run the software using the `biboumi` binary. Read the documentation (the
+man page biboumi(1) or the `biboumi.1.rst`_ file) for more information on how
+to use biboumi.
+
+.. _expat: http://expat.sourceforge.net/
+.. _libiconv: http://www.gnu.org/software/libiconv/
+.. _libuuid: http://sourceforge.net/projects/libuuid/
+.. _libidn: http://www.gnu.org/software/libidn/
+.. _libbotan: http://botan.randombit.net/
+.. _c-ares: http://c-ares.haxx.se/
+.. _litesql: http://git.louiz.org/litesql
+.. _systemd: https://www.freedesktop.org/wiki/Software/systemd/
+.. _biboumi.1.rst: doc/biboumi.1.rst
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..a7f9348
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,62 @@
+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
+XMPP client as if these channels were XMPP MUCs.
+
+It is written in modern C++14 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
+client. It doesn’t however try to provide a complete mapping of the
+features of both worlds simply because this is not useful and most probably
+impossible. For example all IRC modes are not all translatable into an XMPP
+features. Some of them are (like +m (mute) or +o (operator) modes), but
+some others are IRC-specific. If IRC is the limiting factor (for example
+you cannot have a non-ASCII nickname on IRC) then biboumi doesn’t try to
+work around this issue: it just enforces the rules of the IRC server by
+telling the user that he/she must choose an ASCII-only nickname. An
+important goal is to keep the software (and its code) light and simple.
+
+
+Install
+-------
+Refer to the INSTALL_ file.
+
+
+Usage
+-----
+Read `the documentation`_.
+
+Authors
+-------
+Florent Le Coz (louiz’) <louiz@louiz.org>
+
+
+Contact/Support
+---------------
+* XMPP ChatRoom: biboumi@muc.poez.io
+* Report a bug: https://dev.louiz.org/projects/biboumi/issues/new
+
+To contribute, the preferred way is to commit your changes on some
+publicly-available git repository (your own, or github
+(https://github.com/louiz/biboumi), or a fork on https://lab.louiz.org) and
+to notify the developers with a ticket on the bug tracker
+(https://dev.louiz.org/projects/biboumi/issues/new), a pull request on
+github or a merge request on gitlab.
+
+Optionally you can come discuss your changes on the XMPP chat room,
+beforehand.
+
+
+Licence
+-------
+Biboumi is Free Software.
+(learn more: http://www.gnu.org/philosophy/free-sw.html)
+
+Biboumi is released under the zlib license.
+Please read the COPYING file for details.
+
+.. _INSTALL: INSTALL.rst
+.. _the documentation: doc/biboumi.1.rst
diff --git a/biboumi.h.cmake b/biboumi.h.cmake
new file mode 100644
index 0000000..beb67d0
--- /dev/null
+++ b/biboumi.h.cmake
@@ -0,0 +1 @@
+#cmakedefine USE_DATABASE
diff --git a/cmake/Modules/CodeCoverage.cmake b/cmake/Modules/CodeCoverage.cmake
new file mode 100644
index 0000000..4f54327
--- /dev/null
+++ b/cmake/Modules/CodeCoverage.cmake
@@ -0,0 +1,213 @@
+# Copyright (c) 2012 - 2015, Lars Bilke
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its contributors
+# may be used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+#
+#
+# 2012-01-31, Lars Bilke
+# - Enable Code Coverage
+#
+# 2013-09-17, Joakim Söderberg
+# - Added support for Clang.
+# - Some additional usage instructions.
+#
+# USAGE:
+
+# 0. (Mac only) If you use Xcode 5.1 make sure to patch geninfo as described here:
+# http://stackoverflow.com/a/22404544/80480
+#
+# 1. Copy this file into your cmake modules path.
+#
+# 2. Add the following line to your CMakeLists.txt:
+# INCLUDE(CodeCoverage)
+#
+# 3. Set compiler flags to turn off optimization and enable coverage:
+# SET(CMAKE_CXX_FLAGS "-g -O0 -fprofile-arcs -ftest-coverage")
+# SET(CMAKE_C_FLAGS "-g -O0 -fprofile-arcs -ftest-coverage")
+#
+# 3. Use the function SETUP_TARGET_FOR_COVERAGE to create a custom make target
+# which runs your test executable and produces a lcov code coverage report:
+# Example:
+# SETUP_TARGET_FOR_COVERAGE(
+# my_coverage_target # Name for custom target.
+# test_driver # Name of the test driver executable that runs the tests.
+# # NOTE! This should always have a ZERO as exit code
+# # otherwise the coverage generation will not complete.
+# coverage # Name of output directory.
+# )
+#
+# 4. Build a Debug build:
+# cmake -DCMAKE_BUILD_TYPE=Debug ..
+# make
+# make my_coverage_target
+#
+#
+
+# Check prereqs
+FIND_PROGRAM( GCOV_PATH gcov )
+FIND_PROGRAM( LCOV_PATH lcov )
+FIND_PROGRAM( GENHTML_PATH genhtml )
+FIND_PROGRAM( GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/tests)
+
+# Display an error when the target is called. If no error is found, this
+# function will be overridden by the real one later in this file
+FUNCTION(SETUP_TARGET_FOR_COVERAGE _targetname _testrunner _outputname)
+ ADD_CUSTOM_TARGET(${_targetname}
+ COMMAND echo "Coverage is not available: ${ERROR_MSG}"
+ )
+ENDFUNCTION()
+
+IF(NOT GCOV_PATH)
+ set(ERROR_MSG "gcov not found")
+ return()
+ENDIF()
+MARK_AS_ADVANCED(GCOV_PATH)
+IF(NOT LCOV_PATH)
+ set(ERROR_MSG "lcov not found")
+ return()
+ENDIF()
+MARK_AS_ADVANCED(LCOV_PATH)
+IF(NOT GENHTML_PATH)
+ set(ERROR_MSG "genhtml not found")
+ return()
+ENDIF()
+MARK_AS_ADVANCED(GENHTML_PATH)
+IF(NOT CMAKE_COMPILER_IS_GNUCXX)
+ set(ERROR_MSG "Compiler is not gcc")
+ return()
+ENDIF()
+
+SET(CMAKE_CXX_FLAGS_COVERAGE
+ "-g -O0 --coverage -fprofile-arcs -ftest-coverage"
+ CACHE STRING "Flags used by the C++ compiler during coverage builds."
+ FORCE )
+SET(CMAKE_C_FLAGS_COVERAGE
+ "-g -O0 --coverage -fprofile-arcs -ftest-coverage"
+ CACHE STRING "Flags used by the C compiler during coverage builds."
+ FORCE )
+SET(CMAKE_EXE_LINKER_FLAGS_COVERAGE
+ ""
+ CACHE STRING "Flags used for linking binaries during coverage builds."
+ FORCE )
+SET(CMAKE_SHARED_LINKER_FLAGS_COVERAGE
+ ""
+ CACHE STRING "Flags used by the shared libraries linker during coverage builds."
+ FORCE )
+MARK_AS_ADVANCED(
+ CMAKE_CXX_FLAGS_COVERAGE
+ CMAKE_C_FLAGS_COVERAGE
+ CMAKE_EXE_LINKER_FLAGS_COVERAGE
+ CMAKE_SHARED_LINKER_FLAGS_COVERAGE )
+
+IF ( NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "Coverage"))
+ MESSAGE( WARNING "Code coverage results with an optimized (non-Debug) build may be misleading" )
+ENDIF() # NOT CMAKE_BUILD_TYPE STREQUAL "Debug"
+
+
+# Param _targetname The name of new the custom make target
+# Param _testrunner The name of the target which runs the tests.
+# MUST return ZERO always, even on errors.
+# If not, no coverage report will be created!
+# Param _outputname lcov output is generated as _outputname.info
+# HTML report is generated in _outputname/index.html
+# Optional fifth parameter is passed as arguments to _testrunner
+# Pass them in list form, e.g.: "-j;2" for -j 2
+FUNCTION(SETUP_TARGET_FOR_COVERAGE _targetname _testrunner _outputname)
+
+ # Setup target
+ ADD_CUSTOM_TARGET(${_targetname}
+
+ # Cleanup lcov
+ COMMAND ${LCOV_PATH} --directory . --zerocounters
+
+ # Create baseline coverage data file
+ COMMAND ${LCOV_PATH} -c -i -d . -o ${_outputname}.baseline.info -q
+
+ # Run tests
+ COMMAND ${_testrunner} ${ARGV3}
+
+ # Capturing lcov counters and generating report
+ COMMAND ${LCOV_PATH} --directory . --capture --output-file ${_outputname}.info -q
+ # Combine the baseline and the test data
+ COMMAND ${LCOV_PATH} -a ${_outputname}.info -a ${_outputname}.baseline.info -o ${_outputname}.info -q
+
+ # Remove information about source files that are not part of
+ # the test (system file, external libraries, etc)
+ COMMAND ${LCOV_PATH} --remove ${_outputname}.info 'tests/*' '/usr/*' 'external/*' --output-file ${_outputname}.info.cleaned -q
+
+ # Generate the report
+ COMMAND ${GENHTML_PATH} -o ${_outputname} ${_outputname}.info.cleaned
+
+ # Clean the temporary files we created
+ COMMAND ${CMAKE_COMMAND} -E remove ${_outputname}.info ${_outputname}.info.cleaned
+
+ WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+ COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report."
+ )
+
+ # Show info where to find the report
+ ADD_CUSTOM_COMMAND(TARGET ${_targetname} POST_BUILD
+ COMMAND ;
+ COMMENT "Open ./${_outputname}/index.html in your browser to view the coverage report."
+ )
+
+ENDFUNCTION() # SETUP_TARGET_FOR_COVERAGE
+
+# Param _targetname The name of new the custom make target
+# Param _testrunner The name of the target which runs the tests
+# Param _outputname cobertura output is generated as _outputname.xml
+# Optional fourth parameter is passed as arguments to _testrunner
+# Pass them in list form, e.g.: "-j;2" for -j 2
+FUNCTION(SETUP_TARGET_FOR_COVERAGE_COBERTURA _targetname _testrunner _outputname)
+
+ IF(NOT PYTHON_EXECUTABLE)
+ MESSAGE(FATAL_ERROR "Python not found! Aborting...")
+ ENDIF() # NOT PYTHON_EXECUTABLE
+
+ IF(NOT GCOVR_PATH)
+ MESSAGE(FATAL_ERROR "gcovr not found! Aborting...")
+ ENDIF() # NOT GCOVR_PATH
+ MARK_AS_ADVANCED(GCOVR_PATH)
+
+ ADD_CUSTOM_TARGET(${_targetname}
+
+ # Run tests
+ ${_testrunner} ${ARGV3}
+
+ # Running gcovr
+ COMMAND ${GCOVR_PATH} -x -r ${CMAKE_SOURCE_DIR} -e '${CMAKE_SOURCE_DIR}/tests/' -o ${_outputname}.xml
+ WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+ COMMENT "Running gcovr to produce Cobertura code coverage report."
+ )
+
+ # Show info where to find the report
+ ADD_CUSTOM_COMMAND(TARGET ${_targetname} POST_BUILD
+ COMMAND ;
+ COMMENT "Cobertura code coverage report saved in ${_outputname}.xml."
+ )
+
+ENDFUNCTION() # SETUP_TARGET_FOR_COVERAGE_COBERTURA
diff --git a/cmake/Modules/FindLITESQL.cmake b/cmake/Modules/FindLITESQL.cmake
new file mode 100644
index 0000000..91155bb
--- /dev/null
+++ b/cmake/Modules/FindLITESQL.cmake
@@ -0,0 +1,76 @@
+# - Find LiteSQL
+#
+# Find the LiteSQL library, and defines a function to generate C++ files
+# from the database xml file using litesql-gen fro
+#
+# This module defines the following variables:
+# LITESQL_FOUND - True if library and include directory are found
+# If set to TRUE, the following are also defined:
+# LITESQL_INCLUDE_DIRS - The directory where to find the header file
+# LITESQL_LIBRARIES - Where to find the library file
+# LITESQL_GENERATE_CPP - A function, to be used like this:
+# LITESQL_GENERATE_CPP("db/database.xml" # The file defining the db schemas
+# "database" # The name of the C++ “module”
+# # that will be generated
+# LITESQL_GENERATED_SOURCES # Variable containing the
+# resulting C++ files to compile
+#
+# 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.
+# LITESQL_INCLUDE_DIR
+# LITESQL_LIBRARY
+#
+# This file is in the public domain
+
+find_path(LITESQL_INCLUDE_DIRS NAMES litesql.hpp
+ DOC "The LiteSQL include directory")
+
+find_library(LITESQL_LIBRARIES NAMES litesql
+ DOC "The LiteSQL library")
+
+foreach(DB_TYPE sqlite postgresql mysql ocilib)
+ string(TOUPPER ${DB_TYPE} DB_TYPE_UPPER)
+ find_library(LITESQL_${DB_TYPE_UPPER}_LIB_PATH NAMES litesql_${DB_TYPE}
+ DOC "The ${DB_TYPE} backend for LiteSQL")
+ if(LITESQL_${DB_TYPE_UPPER}_LIB_PATH)
+ list(APPEND LITESQL_LIBRARIES ${LITESQL_${DB_TYPE_UPPER}_LIB_PATH})
+ endif()
+ mark_as_advanced(LITESQL_${DB_TYPE_UPPER}_LIB_PATH)
+endforeach()
+
+find_program(LITESQLGEN_EXECUTABLE NAMES litesql-gen
+ DOC "The utility that creates .h and .cpp files from a xml database description")
+
+# Use some standard module to handle the QUIETLY and REQUIRED arguments, and
+# set LITESQL_FOUND to TRUE if these two variables are set.
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(LITESQL REQUIRED_VARS LITESQL_LIBRARIES LITESQL_INCLUDE_DIRS
+ LITESQLGEN_EXECUTABLE)
+
+# Compatibility for all the ways of writing these variables
+if(LITESQL_FOUND)
+ set(LITESQL_INCLUDE_DIR ${LITESQL_INCLUDE_DIRS})
+ set(LITESQL_LIBRARY ${LITESQL_LIBRARIES})
+endif()
+
+mark_as_advanced(LITESQL_INCLUDE_DIRS LITESQL_LIBRARIES LITESQLGEN_EXECUTABLE)
+
+
+# LITESQL_GENERATE_CPP function
+
+function(LITESQL_GENERATE_CPP
+ SOURCE_FILE OUTPUT_NAME OUTPUT_SOURCES)
+ set(${OUTPUT_SOURCES})
+ add_custom_command(
+ OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_NAME}.cpp"
+ "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_NAME}.hpp"
+ COMMAND ${LITESQLGEN_EXECUTABLE}
+ ARGS -t c++ --output-dir=${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${SOURCE_FILE}
+ DEPENDS ${SOURCE_FILE}
+ COMMENT "Running litesql-gen on ${SOURCE_FILE}"
+ VERBATIM)
+ list(APPEND ${OUTPUT_SOURCES} "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_NAME}.cpp")
+ set_source_files_properties(${${OUTPUT_SOURCES}} PROPERTIES GENERATED TRUE)
+ set(${OUTPUT_SOURCES} ${${OUTPUT_SOURCES}} PARENT_SCOPE)
+endfunction()
diff --git a/conf/biboumi.cfg b/conf/biboumi.cfg
new file mode 100644
index 0000000..e6b8ed5
--- /dev/null
+++ b/conf/biboumi.cfg
@@ -0,0 +1,7 @@
+hostname=biboumi.example.com
+password=secret
+db_name=/var/lib/biboumi/biboumi.sqlite
+log_file=/var/log/biboumi/biboumi.log
+log_level=0
+admin=
+port=5347
diff --git a/database/database.xml b/database/database.xml
new file mode 100644
index 0000000..f102db0
--- /dev/null
+++ b/database/database.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!DOCTYPE database SYSTEM "litesql.dtd">
+
+<database name="BibouDB" namespace="db">
+ <object name="IrcServerOptions">
+ <field name="owner" type="string" length="3071"/>
+ <field name="server" type="string" length="3071"/>
+
+ <field name="pass" type="string" length="1024" default=""/>
+ <field name="afterConnectionCommand" type="string" length="510" default=""/>
+ <field name="tlsPorts" type="string" length="4096" default="6697;6670" />
+ <field name="ports" type="string" length="4096" default="6667" />
+ <field name="username" type="string" length="1024" default=""/>
+ <field name="realname" type="string" length="1024" default=""/>
+ <field name="verifyCert" type="boolean" default="true"/>
+ <field name="trustedFingerprint" type="string"/>
+
+ <field name="encodingOut" type="string" default="ISO-8859-1"/>
+ <field name="encodingIn" type="string" default="ISO-8859-1"/>
+
+ <index unique="true">
+ <indexfield name="owner"/>
+ <indexfield name="server"/>
+ </index>
+ </object>
+
+ <object name="IrcChannelOptions">
+ <field name="owner" type="string" length="3071"/>
+ <field name="server" type="string" length="3071"/>
+ <field name="channel" type="string" length="1024"/>
+
+ <field name="encodingOut" type="string"/>
+ <field name="encodingIn" type="string"/>
+
+ <index unique="true">
+ <indexfield name="owner"/>
+ <indexfield name="server"/>
+ <indexfield name="channel"/>
+ </index>
+ </object>
+
+ <object name="LogLine">
+ <field name="date" type="date" />
+ <field name="body" type="string" length="4096"/>
+ </object>
+</database>
diff --git a/doc/biboumi.1.rst b/doc/biboumi.1.rst
new file mode 100644
index 0000000..dd365f0
--- /dev/null
+++ b/doc/biboumi.1.rst
@@ -0,0 +1,514 @@
+======================
+Biboumi(1) User Manual
+======================
+
+.. contents:: :depth: 2
+
+NAME
+====
+
+biboumi - XMPP gateway to IRC
+
+Description
+===========
+
+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.
+
+Synopsis
+========
+
+biboumi [*config_filename*\ ]
+
+Options
+=======
+
+Available command line options:
+
+config_filename
+---------------
+
+Specify the file to read for configuration. See *CONFIG* section for more
+details on its content.
+
+Configuration
+=============
+
+The configuration file uses a simple format of the form
+``option=value``. Here is a description of each possible option:
+
+The configuration can be re-read at runtime (you can for example change the
+log level without having to restart biboumi) by sending SIGUSR1 or SIGUSR2
+(see kill(1)) to the process.
+
+hostname
+--------
+
+Mandatory. The hostname served by the XMPP gateway. This domain must be
+configured in the XMPP server as an external component. See the manual
+for your XMPP server for more information. For prosody, see
+http://prosody.im/doc/components#adding_an_external_component
+
+password
+--------
+
+Mandatory. The password used to authenticate the XMPP component to your
+XMPP server. This password must be configured in the XMPP server,
+associated with the external component on *hostname*.
+
+xmpp_server_ip
+--------------
+
+The IP address to connect to the XMPP server on. The connection to the
+XMPP server is unencrypted, so the biboumi instance and the server should
+normally be on the same host. The default value is 127.0.0.1.
+
+port
+----
+
+The TCP port to use to connect to the local XMPP component. The default
+value is 5347.
+
+admin
+-----
+
+The bare JID of the gateway administrator. This JID will have more
+privileges than other standard users (the admin thus needs to check their
+privileges), for example some administration ad-hoc commands will only be
+available to that JID.
+
+fixed_irc_server
+----------------
+
+If this option contains the hostname of an IRC server (for example
+irc.example.org), then biboumi will enforce the connexion to that IRC
+server only. This means that a JID like "#chan@biboumi.example.com" must
+be used instead of "#chan%irc.example.org@biboumi.example.com". In that
+mode, the virtual channel (see `Connect to an IRC server`_) is not
+available and you still need to use the ! separator to send message to an
+IRC user (for example "foo!@biboumi.example.com" to send a message to
+foo), although the in-room JID still work as expected
+("#channel@biboumi.example.com/Nick"). On the other hand, the '%' lose
+any meaning. It can appear in the JID but will not be interpreted as a
+separator (thus the JID "#channel%hello@biboumi.example.com" points to the
+channel named "#channel%hello" on the configured IRC server) This option
+can for example be used by an administrator that just wants to let their
+users join their own IRC server using an XMPP client, while forbidding
+access to any other IRC server.
+
+realname_customization
+----------------------
+
+If this option is set to “false” (default is “true”), the users will not be
+able to use the ad-hoc commands that lets them configure their realname and
+username.
+
+realname_from_jid
+-----------------
+
+If this option is set to “true”, the realname and username of each biboumi
+user will be extracted from their JID. The realname is their bare JID, and
+the username is the node-part of their JID. Note that if
+``realname_customization`` is “true”, each user will still be able to
+customize their realname and username, this option just decides the default
+realname and username.
+
+If this option is set to “false” (the default value), the realname and
+username of each user will be set to the nick they used to connect to the
+IRC server.
+
+webirc_password
+---------------
+
+Configure a password to be communicated to the IRC server, as part of the
+WEBIRC message (see https://kiwiirc.com/docs/webirc). If this option is
+set, an additional DNS resolution of the hostname of each XMPP server will
+be made when connecting to an IRC server.
+
+log_file
+--------
+
+A filename into which logs are written. If none is provided, the logs are
+written on standard output.
+
+log_level
+---------
+
+Indicate what type of log messages to write in the logs. Value can be
+from 0 to 3. 0 is debug, 1 is info, 2 is warning, 3 is error. The
+default is 0, but a more practical value for production use is 1.
+
+ca_file
+-------
+
+Specifies which file should be use as the list of trusted CA when
+negociating a TLS session. By default this value is unset and biboumi
+tries a list of well-known paths.
+
+outgoing_bind
+-------------
+
+An address (IPv4 or IPv6) to bind the outgoing sockets to. If no value is
+specified, it will use the one assigned by the operating system. You can
+for example use outgoing_bind=192.168.1.11 to force biboumi to use the
+interface with this address. Note that this is only used for connections
+to IRC servers.
+
+Usage
+=====
+
+Biboumi acts as a server, it should be run as a daemon that lives in the
+background for as long as it is needed. Note that biboumi does not
+daemonize itself, this task should be done by your init system (SysVinit,
+systemd, upstart).
+
+When started, biboumi connects, without encryption (see `Security`_), to the
+local XMPP server on the port ``5347`` and authenticates with the provided
+password. Biboumi then serves the configured ``hostname``: this means that
+all XMPP stanza with a `to` JID on that domain will be forwarded to biboumi
+by the XMPP server, and biboumi will only send messages coming from that
+hostname.
+
+When a user joins an IRC channel on an IRC server (see `Join an IRC
+channel`_), biboumi connects to the remote IRC server, sets the user’s nick
+as requested, and then tries to join the specified channel. If the same
+user subsequently tries to connect to an other channel on the same server,
+the same IRC connection is used. If, however, an other user wants to join
+an IRC channel on that same IRC server, biboumi opens a new connection to
+that server. Biboumi connects once to each IRC server, for each user on it.
+
+Additionally, if one user is using more than one clients (with the same bare
+JID), they can join the same IRC channel (on the same server) behind one
+single nickname. Biboumi will forward all the messages (the channel ones and
+the private ones) and the presences to all the resources behind that nick.
+There is no need to have multiple nicknames and multiple connections to be
+able to take part in a conversation (or idle) in a channel from a mobile client
+while the desktop client is still connected, for example.
+
+To cleanly shutdown the component, send a SIGINT or SIGTERM signal to it.
+It will send messages to all connected IRC and XMPP servers to indicate a
+reason why the users are being disconnected. Biboumi exits when the end of
+communication is acknowledged by all IRC servers. If one or more IRC
+servers do not respond, biboumi will only exit if it receives the same
+signal again or if a 2 seconds delay has passed.
+
+Addressing
+----------
+
+IRC entities are represented by XMPP JIDs. The domain part of the JID is
+the domain served by biboumi (the part after the ``@``, biboumi.example.com in
+the examples), and the local part (the part before the ``@``) depends on the
+concerned entity.
+
+IRC channels have a local part formed like this:
+``channel_name`` % ``irc_server``.
+
+If the IRC channel you want to adress starts with the ``'#'`` character (or an
+other character, announced by the IRC server, like ``'&'``, ``'+'`` or ``'!'``),
+then you must include it in the JID. Some other gateway implementations, as
+well as some IRC clients, do not require them to be started by one of these
+characters, adding an implicit ``'#'`` in that case. Biboumi does not do that
+because this gets confusing when trying to understand the difference between
+the channels *#foo*, and *##foo*. Note that biboumi does not use the
+presence of these special characters to identify an IRC channel, only the
+presence of the separator `%` is used for that.
+
+The channel name can also be empty (for example ``%irc.example.com``), in that
+case this represents the virtual channel provided by biboumi. See *Connect
+to an IRC server* for more details.
+
+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 participant, like this:
+``channel_name`` % ``irc_server`` @ ``biboumi.example.com`` / ``Nickname``
+
+The second JID is available only to be compatible with XMPP clients when the
+user wants to send a private message to the participant ``Nickname`` in the
+room ``channel_name%irc_server@biboumi.example.com``.
+
+On XMPP, the node part of the JID can only be lowercase. On the other hand,
+IRC nicknames are case-insensitive, this means that the nicknames toto,
+Toto, tOtO and TOTO all represent the same IRC user. This means you can
+talk to the user toto, and this will work.
+
+Also note that some IRC nicknames may contain characters that are not
+allowed in the local part of a JID (for example '@'). If you need to send a
+message to a nick containing such a character, you have to use a jid like
+``%irc.example.com@biboumi.example.com/AnnoyingNickn@me``, because the JID
+``AnnoyingNickn@me!irc.example.com@biboumi.example.com`` would not work.
+
+Examples:
+
+* ``#foo%irc.example.com@biboumi.example.com`` is the #foo IRC channel, on the
+ irc.example.com IRC server, and this is served by the biboumi instance on
+ biboumi.example.com
+
+* ``toto!irc.example.com@biboumi.example.com`` is the IRC user named toto, or
+ TotO, etc.
+
+* ``irc.example.com@biboumi.example.com`` is the IRC server irc.example.com.
+
+* ``%irc.example.com@biboumi.example.com`` is the virtual channel provided by
+ biboumi, for the IRC server irc.example.com.
+
+Note: Some JIDs are valid but make no sense in the context of
+biboumi:
+
+* ``!irc.example.com@biboumi.example.com`` is the empty-string nick on the
+ irc.example.com server. It makes no sense to try to send messages to it.
+
+* ``#test%@biboumi.example.com``, or any other JID that does not contain an
+ IRC server is invalid. Any message to that kind of JID will trigger an
+ error, or will be ignored.
+
+If compiled with Libidn, an IRC channel participant has a bare JID
+representing the “hostname” provided by the IRC server. This JID can only
+be used to set IRC modes (for example to ban a user based on its IP), or to
+identify user. It cannot be used to contact that user using biboumi.
+
+Join an IRC channel
+-------------------
+
+To join an IRC channel ``#foo`` on the IRC server ``irc.example.com``,
+join the XMPP MUC ``#foo%irc.example.com@biboumi.example.com``.
+
+Connect to an IRC server
+------------------------
+
+The connection to the IRC server is automatically made when the user tries
+to join any channel on that IRC server. The connection is closed whenever
+the last channel on that server is left by the user. To be able to stay
+connected to an IRC server without having to be in a real IRC channel,
+biboumi provides a virtual channel on the jid
+``%irc.example.com@biboumi.example.com``. For example if you want to join the
+channel ``#foo`` on the server ``irc.example.com``, but you need to authenticate
+to a bot of the server before you’re allowed to join it, you can first join
+the room ``%irc.example.com@biboumi.example.com`` (this will effectively
+connect you to the IRC server without joining any room), then send your
+authentication message to the user ``bot!irc.example.com@biboumi.example.com``
+and finally join the room ``#foo%irc.example.com@biboumi.example.com``.
+
+Channel messages
+----------------
+
+On XMPP, unlike on IRC, the displayed order of the messages is the same for
+all participants of a MUC. Biboumi can not however provide this feature, as
+it cannot know whether the IRC server has received and forwarded the
+messages to other users. This means that the order of the messages
+displayed in your XMPP client may not be the same than the order on other
+IRC users’.
+
+List channels
+-------------
+
+You can list the IRC channels on a given IRC server by sending an XMPP disco
+items request on the IRC server JID. The number of channels on some servers
+is huge, and biboumi does not (yet) support result set management (XEP 0059)
+so the result stanza may be very big.
+
+Nicknames
+---------
+
+On IRC, nicknames are server-wide. This means that one user only has one
+single nickname at one given time on all the channels of a server. This is
+different from XMPP where a user can have a different nick on each MUC,
+even if these MUCs are on the same server.
+
+This means that the nick you choose when joining your first IRC channel on a
+given IRC server will be your nickname in all other channels that you join
+on that same IRC server.
+If you explicitely change your nickname on one channel, your nickname will
+be changed on all channels on the same server as well.
+Joining a new channel with a different nick, however, will not change your
+nick. The provided nick will be ignored, in order to avoid changing your
+nick on the whole server by mistake. If you want to have a different
+nickname in the channel you’re going to join, you need to do it explicitly
+with the NICK command before joining the channel.
+
+Private messages
+----------------
+
+Private messages are handled differently on IRC and on XMPP. On IRC, you
+talk directly to one server-user: toto on the channel #foo is the same user
+as toto on the channel #bar (as long as these two channels are on the same
+IRC server). By default you will receive private messages from the “global”
+user (aka nickname!irc.example.com@biboumi.example.com), unless you
+previously sent a message to an in-room participant (something like
+\#test%irc.example.com@biboumi.example.com/nickname), in which case future
+messages from that same user will be received from that same “in-room” JID.
+
+Notices
+-------
+
+Notices are received exactly like private messages. It is not possible to
+send a notice.
+
+Kicks and bans
+--------------
+
+Kicks are transparently translated from one protocol to another. However
+banning an XMPP participant has no effect. To ban an user you need to set a
+mode +b on that user nick or host (see `IRC modes`_) and then kick it.
+
+Encoding
+--------
+
+On XMPP, the encoding is always ``UTF-8``, whereas on IRC the encoding of
+each message can be anything.
+
+This means that biboumi has to convert everything coming from IRC into UTF-8
+without knowing the encoding of the received messages. To do so, it checks
+if each message is UTF-8 valid, if not it tries to convert from
+``iso_8859-1`` (because this appears to be the most common case, at least
+on the channels I visit) to ``UTF-8``. If that conversion fails at some
+point, a placeholder character ``'�'`` is inserted to indicate this
+decoding error.
+
+Messages are always sent in UTF-8 over IRC, no conversion is done in that
+direction.
+
+IRC modes
+---------
+
+One feature that doesn’t exist on XMPP but does on IRC is the ``modes``.
+Although some of these modes have a correspondance in the XMPP world (for
+example the ``+o`` mode on a user corresponds to the ``moderator`` role in
+XMPP), it is impossible to map all these modes to an XMPP feature. To
+circumvent this problem, biboumi provides a raw notification when modes are
+changed, and lets the user change the modes directly.
+
+To change modes, simply send a message starting with “``/mode``” followed by
+the modes and the arguments you want to send to the IRC server. For example
+“/mode +aho louiz”. Note that your XMPP client may interprete messages
+begining with “/” like a command. To actually send a message starting with
+a slash, you may need to start your message with “//mode” or “/say /mode”,
+depending on your client.
+
+When a mode is changed, the user is notified by a message coming from the
+MUC bare JID, looking like “Mode #foo [+ov] [toto tutu]”. In addition, if
+the mode change can be translated to an XMPP feature, the user will be
+notified of this XMPP event as well. For example if a mode “+o toto” is
+received, then toto’s role will be changed to moderator. The mapping
+between IRC modes and XMPP features is as follow:
+
+``+q``
+ Sets the participant’s role to ``moderator`` and its affiliation to ``owner``.
+
+``+a``
+ Sets the participant’s role to ``moderator`` and its affiliation to ``owner``.
+
+``+o``
+ Sets the participant’s role to ``moderator`` and its affiliation to ``admin``.
+
+``+h``
+ Sets the participant’s role to ``moderator`` and its affiliation to ``member``.
+
+``+v``
+ Sets the participant’s role to `participant` and its affiliation to ``member``.
+
+Similarly, when a biboumi user changes some participant's affiliation or role, biboumi translates that in an IRC mode change.
+
+Affiliation set to ``none``
+ Sets mode to -vhoaq
+
+Affiliation set to ``member``
+ Sets mode to +v-hoaq
+
+Role set to ``moderator``
+ Sets mode to +h-oaq
+
+Affiliation set to ``admin``
+ Sets mode to +o-aq
+
+Affiliation set to ``owner``
+ Sets mode to +a-q
+
+Ad-hoc commands
+---------------
+
+Biboumi supports a few ad-hoc commands, as described in the XEP 0050.
+Different ad-hoc commands are available for each JID type.
+
+On the gateway itself (e.g on the JID biboumi.example.com):
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- ping: Just respond “pong”
+
+- hello: Provide a form, where the user enters their name, and biboumi
+ responds with a nice greeting.
+
+- disconnect-user: Only available to the administrator. The user provides
+ a list of JIDs, and a quit message. All the selected users are
+ disconnected from all the IRC servers to which they were connected,
+ using the provided quit message. Sending SIGINT to biboumi is equivalent
+ to using this command by selecting all the connected JIDs and using the
+ “Gateway shutdown” quit message, except that biboumi does not exit when
+ using this ad-hoc command.
+
+- disconnect-from-irc-servers: Disconnect a single user from one or more
+ IRC server. The user is immediately disconnected by closing the socket,
+ no message is sent to the IRC server, but the user is of course notified
+ with an XMPP message. The administrator can disconnect any user, while
+ the other users can only disconnect themselves.
+
+On a server JID (e.g on the JID chat.freenode.org@biboumi.example.com)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- Configure: Lets each user configure some options that applies to the
+ concerned IRC server.
+
+On a channel JID (e.g on the JID #test%chat.freenode.org@biboumi.example.com)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- Configure: Lets each user configure some options that applies to the
+ concerned IRC channel. Some of these options, if not configured for a
+ specific channel, defaults to the value configured at the IRC server
+ level. For example the encoding can be specified for both the channel
+ and the server. If an encoding is not specified for a channel, the
+ encoding configured in the server applies.
+
+Raw IRC messages
+----------------
+
+Biboumi tries to support as many IRC features as possible, but doesn’t
+handle everything yet (or ever). In order to let the user send any
+arbitrary IRC message, biboumi forwards any XMPP message received on an IRC
+Server JID (see *ADDRESSING*) as a raw command to that IRC server.
+
+For example, to WHOIS the user Foo on the server irc.example.com, a user can
+send the message “WHOIS Foo” to “irc.example.com@biboumi.example.com”.
+
+The message will be forwarded as is, without any modification appart from
+adding "\r\n" at the end (to make it a valid IRC message). You need to have
+a little bit of understanding of the IRC protocol to use this feature.
+
+Security
+========
+
+The connection to the XMPP server can only be made on localhost. The
+XMPP server is not supposed to accept non-local connections from components.
+Thus, encryption is not used to connect to the local XMPP server because it
+is useless.
+
+If compiled with the Botan library, biboumi can use TLS when communicating
+with the IRC serveres. It will first try ports 6697 and 6670 and use TLS if
+it succeeds, if connection fails on both these ports, the connection is
+established on port 6667 without any encryption.
+
+Biboumi does not check if the received JIDs are properly formatted using
+nodeprep. This must be done by the XMPP server to which biboumi is directly
+connected.
+
+Note if you use a biboumi that you have no control on: remember that the
+administrator of the gateway you use is able to view all your IRC
+conversations, whether you’re using encryption or not. This is exactly as
+if you were running your IRC client on someone else’s server. Only use
+biboumi if you trust its administrator (or, better, if you are the
+administrator) or if you don’t intend to have any private conversation.
+
+Biboumi does not provide a way to ban users from connecting to it, has no
+protection against flood or any sort of abuse that your users may cause on
+the IRC servers. Some XMPP server however offer the possibility to restrict
+what JID can access a gateway. Use that feature if you wish to grant access
+to your biboumi instance only to a list of trusted users.
diff --git a/docker/biboumi-test/debian/Dockerfile b/docker/biboumi-test/debian/Dockerfile
new file mode 100644
index 0000000..f2a26ea
--- /dev/null
+++ b/docker/biboumi-test/debian/Dockerfile
@@ -0,0 +1,72 @@
+# 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:latest
+
+RUN apt update
+
+# Needed to build biboumi
+RUN apt install -y g++
+RUN apt install -y clang
+RUN apt install -y valgrind
+RUN apt install -y libc-ares-dev
+RUN apt install -y libsqlite3-dev
+RUN apt install -y libuuid1
+RUN apt install -y cmake
+RUN apt install -y make
+RUN apt install -y libexpat1-dev
+RUN apt install -y libidn11-dev
+RUN apt install -y uuid-dev
+RUN apt install -y libsystemd-dev
+RUN apt install -y pandoc
+
+# Needed to run tests
+RUN apt install -y git
+RUN apt install -y python3-lxml
+RUN apt install -y lcov
+
+# Install botan
+RUN git clone https://github.com/randombit/botan.git
+RUN cd botan && ./configure.py --prefix=/usr && make -j8 && make install
+RUN rm -rf /botan
+
+# Install litesql
+RUN git clone git://git.louiz.org/litesql
+RUN mkdir /litesql/build && cd /litesql/build && cmake .. -DCMAKE_INSTALL_PREFIX=/usr && make -j8
+RUN cd /litesql/build && make install
+RUN rm -rf /litesql
+
+RUN ldconfig
+
+# Install slixmpp, for e2e tests
+RUN apt install -y python3-pip
+RUN git clone git://git.louiz.org/slixmpp
+RUN pip3 install pyasn1
+RUN apt install -y python3-dev
+RUN cd slixmpp && python3 setup.py build && python3 setup.py install
+
+RUN useradd tester -m
+
+# Install charybdis, for e2e tests
+RUN apt install -y automake autoconf flex bison libltdl-dev openssl
+RUN apt install -y libtool
+RUN git clone https://github.com/charybdis-ircd/charybdis.git && cd charybdis
+RUN cd /charybdis && ./autogen.sh && ./configure --prefix=/home/tester/ircd --bindir=/usr/bin && make -j8 && make install
+RUN chown -R tester:tester /home/tester/ircd
+RUN rm -rf /charybdis
+
+RUN apt install -y locales
+RUN export LANGUAGE=en_US.UTF-8
+RUN export LANG=en_US.UTF-8
+RUN export LC_ALL=en_US.UTF-8
+RUN locale-gen
+RUN dpkg-reconfigure locales
+
+RUN dpkg-reconfigure locales && \
+ locale-gen C.UTF-8 && \
+ /usr/sbin/update-locale LANG=C.UTF-8
+
+ENV LC_ALL C.UTF-8
+
+WORKDIR /home/tester
+USER tester
diff --git a/docker/biboumi-test/fedora/Dockerfile b/docker/biboumi-test/fedora/Dockerfile
new file mode 100644
index 0000000..3c48645
--- /dev/null
+++ b/docker/biboumi-test/fedora/Dockerfile
@@ -0,0 +1,64 @@
+# 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:latest
+
+RUN dnf update -y
+
+# Needed to build biboumi
+RUN dnf install -y gcc-c++
+RUN dnf install -y clang
+RUN dnf install -y valgrind
+RUN dnf install -y c-ares-devel
+RUN dnf install -y sqlite-devel
+RUN dnf install -y libuuid-devel
+RUN dnf install -y cmake
+RUN dnf install -y make
+RUN dnf install -y expat-devel
+RUN dnf install -y libidn-devel
+RUN dnf install -y uuid-devel
+RUN dnf install -y systemd-devel
+RUN dnf install -y pandoc
+
+# Needed to run tests
+RUN dnf install -y git
+RUN dnf install -y fedora-packager python3-lxml
+RUN dnf install -y lcov
+
+# To be able to create the RPM
+RUN dnf install -y rpmdevtools
+
+# Install botan
+RUN git clone https://github.com/randombit/botan.git
+RUN cd botan && ./configure.py --prefix=/usr && make -j8 && make install
+RUN rm -rf /botan
+
+# Install litesql
+RUN git clone git://git.louiz.org/litesql
+RUN mkdir /litesql/build && cd /litesql/build && cmake .. -DCMAKE_INSTALL_PREFIX=/usr && make -j8
+RUN cd /litesql/build && make install
+RUN rm -rf /litesql
+
+RUN ldconfig
+
+# Install slixmpp, for e2e tests
+RUN git clone git://git.louiz.org/slixmpp
+RUN pip3 install pyasn1
+RUN dnf install -y python3-devel
+RUN cd slixmpp && python3 setup.py build && python3 setup.py install
+
+RUN useradd tester
+
+# Install charybdis, for e2e tests
+RUN dnf install -y automake autoconf flex flex-devel bison libtool-ltdl-devel openssl-devel
+RUN dnf install -y libtool
+RUN git clone https://github.com/charybdis-ircd/charybdis.git && cd charybdis
+RUN cd /charybdis && ./autogen.sh && ./configure --prefix=/home/tester/ircd --bindir=/usr/bin && make -j8 && make install
+RUN chown -R tester:tester /home/tester/ircd
+RUN rm -rf /charybdis
+
+RUN su - tester -c "echo export LANG=en_GB.utf-8 >> /home/tester/.bashrc"
+
+WORKDIR /home/tester
+USER tester
+
diff --git a/louloulibs/CMakeLists.txt b/louloulibs/CMakeLists.txt
new file mode 100644
index 0000000..bf53504
--- /dev/null
+++ b/louloulibs/CMakeLists.txt
@@ -0,0 +1,146 @@
+cmake_minimum_required(VERSION 2.6)
+
+set(${PROJECT_NAME}_VERSION_MAJOR 1)
+set(${PROJECT_NAME}_VERSION_MINOR 0)
+set(${PROJECT_NAME}_VERSION_SUFFIX "~dev")
+
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++1y -pedantic -Wall -Wextra")
+
+# Define a __FILENAME__ macro to get the filename of each file, instead of
+# the full path as in __FILE__
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__FILENAME__='\"$(subst ${CMAKE_SOURCE_DIR}/,,$(abspath $<))\"'")
+
+#
+## Look for external libraries
+#
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules/")
+include(FindEXPAT)
+find_package(EXPAT REQUIRED)
+find_package(ICONV REQUIRED)
+find_package(LIBUUID REQUIRED)
+
+if(WITH_LIBIDN)
+ find_package(LIBIDN REQUIRED)
+elseif(NOT WITHOUT_LIBIDN)
+ find_package(LIBIDN)
+endif()
+
+if(WITH_SYSTEMD)
+ find_package(SYSTEMD REQUIRED)
+elseif(NOT WITHOUT_SYSTEMD)
+ find_package(SYSTEMD)
+endif()
+
+if(WITH_BOTAN)
+ find_package(BOTAN REQUIRED)
+elseif(NOT WITHOUT_BOTAN)
+ find_package(BOTAN)
+endif()
+
+if(WITH_CARES)
+ find_package(CARES REQUIRED)
+elseif(NOT WITHOUT_CARES)
+ find_package(CARES)
+endif()
+
+# To be able to include the config.h file generated by cmake
+include_directories("${CMAKE_CURRENT_BINARY_DIR}")
+include_directories("${CMAKE_CURRENT_SOURCE_DIR}")
+include_directories(${EXPAT_INCLUDE_DIRS})
+include_directories(${ICONV_INCLUDE_DIRS})
+include_directories(${LIBUUID_INCLUDE_DIRS})
+
+set(EXPAT_INCLUDE_DIRS ${EXPAT_INCLUDE_DIRS} PARENT_SCOPE)
+set(ICONV_INCLUDE_DIRS ${ICONV_INCLUDE_DIRS} PARENT_SCOPE)
+set(LIBUUID_INCLUDE_DIRS ${LIBUUID_INCLUDE_DIRS} PARENT_SCOPE)
+
+if(LIBIDN_FOUND)
+ include_directories(${LIBIDN_INCLUDE_DIRS})
+ set(LIBDIN_FOUND ${LIBDIN_FOUND} PARENT_SCOPE)
+ set(LIBDIN_INCLUDE_DIRS ${LIBDIN_INCLUDE_DIRS} PARENT_SCOPE)
+endif()
+
+if(SYSTEMD_FOUND)
+ include_directories(${SYSTEMD_INCLUDE_DIRS})
+ set(SYSTEMD_FOUND ${SYSTEMD_FOUND} PARENT_SCOPE)
+ set(SYSTEMD_INCLUDE_DIRS ${SYSTEMD_INCLUDE_DIRS} PARENT_SCOPE)
+endif()
+
+if(BOTAN_FOUND)
+ include_directories(SYSTEM ${BOTAN_INCLUDE_DIRS})
+ set(BOTAN_FOUND ${BOTAN_FOUND} PARENT_SCOPE)
+ set(BOTAN_INCLUDE_DIRS ${BOTAN_INCLUDE_DIRS} PARENT_SCOPE)
+endif()
+
+if(CARES_FOUND)
+ include_directories(${CARES_INCLUDE_DIRS})
+ set(CARES_FOUND ${CARES_FOUND} PARENT_SCOPE)
+ set(CARES_INCLUDE_DIRS ${CARES_INCLUDE_DIRS} PARENT_SCOPE)
+endif()
+
+set(POLLER_DOCSTRING "Choose the poller between POLL and EPOLL (Linux-only)")
+if(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+ set(POLLER "EPOLL" CACHE STRING ${POLLER_DOCSTRING})
+else()
+ set(POLLER "POLL" CACHE STRING ${POLLER_DOCSTRING})
+endif()
+if((NOT ${POLLER} MATCHES "POLL") AND
+ (NOT ${POLLER} MATCHES "EPOLL"))
+ message(FATAL_ERROR "POLLER must be either POLL or EPOLL")
+endif()
+
+#
+## utils
+#
+file(GLOB source_utils
+ utils/*.[hc]pp)
+add_library(utils STATIC ${source_utils})
+target_link_libraries(utils ${ICONV_LIBRARIES})
+
+#
+## config
+#
+file(GLOB source_config
+ config/*.[hc]pp)
+add_library(config STATIC ${source_config})
+target_link_libraries(config utils)
+
+#
+## logger
+#
+file(GLOB source_logger
+ logger/*.[hc]pp)
+add_library(logger STATIC ${source_logger})
+target_link_libraries(logger config)
+
+#
+## network
+#
+file(GLOB source_network
+ network/*.[hc]pp)
+add_library(network STATIC ${source_network})
+target_link_libraries(network logger)
+if(BOTAN_FOUND)
+ target_link_libraries(network ${BOTAN_LIBRARIES})
+endif()
+if(CARES_FOUND)
+ target_link_libraries(network ${CARES_LIBRARIES})
+endif()
+
+#
+## xmpplib
+#
+file(GLOB source_xmpplib
+ xmpp/*.[hc]pp)
+add_library(xmpplib STATIC ${source_xmpplib})
+target_link_libraries(xmpplib network utils logger
+ ${EXPAT_LIBRARIES}
+ ${LIBUUID_LIBRARIES})
+if(LIBIDN_FOUND)
+ target_link_libraries(xmpplib ${LIBIDN_LIBRARIES})
+endif()
+if(SYSTEMD_FOUND)
+ target_link_libraries(xmpplib ${SYSTEMD_LIBRARIES})
+endif()
+
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/louloulibs.h.cmake ${CMAKE_BINARY_DIR}/src/louloulibs.h)
diff --git a/louloulibs/cmake/Modules/FindBOTAN.cmake b/louloulibs/cmake/Modules/FindBOTAN.cmake
new file mode 100644
index 0000000..a12bd35
--- /dev/null
+++ b/louloulibs/cmake/Modules/FindBOTAN.cmake
@@ -0,0 +1,35 @@
+# - Find botan
+# Find the botan cryptographic library
+#
+# This module defines the following variables:
+# 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
+#
+# This file is in the public domain
+
+find_path(BOTAN_INCLUDE_DIRS NAMES botan/botan.h
+ PATH_SUFFIXES botan-1.11
+ DOC "The botan include directory")
+
+find_library(BOTAN_LIBRARIES NAMES botan botan-1.11
+ 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})
+ set(BOTAN_INCLUDE_DIR ${BOTAN_INCLUDE_DIRS})
+endif()
+
+mark_as_advanced(BOTAN_INCLUDE_DIRS BOTAN_LIBRARIES)
diff --git a/louloulibs/cmake/Modules/FindCARES.cmake b/louloulibs/cmake/Modules/FindCARES.cmake
new file mode 100644
index 0000000..c4c757a
--- /dev/null
+++ b/louloulibs/cmake/Modules/FindCARES.cmake
@@ -0,0 +1,37 @@
+# - Find c-ares
+# Find the c-ares library, and more particularly the stringprep header.
+#
+# This module defines the following variables:
+# CARES_FOUND - True if library and include directory are found
+# If set to TRUE, the following are also defined:
+# CARES_INCLUDE_DIRS - The directory where to find the header file
+# CARES_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.
+# CARES_INCLUDE_DIR
+# CARES_LIBRARY
+#
+# This file is in the public domain
+
+if(NOT CARES_FOUND)
+ find_path(CARES_INCLUDE_DIRS NAMES ares.h
+ DOC "The c-ares include directory")
+
+ find_library(CARES_LIBRARIES NAMES cares
+ DOC "The c-ares library")
+
+ # Use some standard module to handle the QUIETLY and REQUIRED arguments, and
+ # set CARES_FOUND to TRUE if these two variables are set.
+ include(FindPackageHandleStandardArgs)
+ find_package_handle_standard_args(CARES REQUIRED_VARS CARES_LIBRARIES CARES_INCLUDE_DIRS)
+
+ # Compatibility for all the ways of writing these variables
+ if(CARES_FOUND)
+ set(CARES_INCLUDE_DIR ${CARES_INCLUDE_DIRS})
+ set(CARES_LIBRARY ${CARES_LIBRARIES})
+ endif()
+endif()
+
+mark_as_advanced(CARES_INCLUDE_DIRS CARES_LIBRARIES)
diff --git a/louloulibs/cmake/Modules/FindICONV.cmake b/louloulibs/cmake/Modules/FindICONV.cmake
new file mode 100644
index 0000000..7ca173f
--- /dev/null
+++ b/louloulibs/cmake/Modules/FindICONV.cmake
@@ -0,0 +1,60 @@
+# - Find iconv
+# Find the iconv (character set conversion) library
+#
+# This module defines the following variables:
+# ICONV_FOUND - True if library and include directory are found
+# If set to TRUE, the following are also defined:
+# ICONV_INCLUDE_DIRS - The directory where to find the header file
+# ICONV_LIBRARIES - Where to find the library file
+# ICONV_SECOND_ARGUMENT_IS_CONST - The second argument for iconv() is const
+#
+# 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.
+# ICONV_LIBRARY
+# ICONV_INCLUDE_DIR
+#
+# This file is in the public domain
+
+find_path(ICONV_INCLUDE_DIRS NAMES iconv.h
+ DOC "The iconv include directory")
+
+find_library(ICONV_LIBRARIES NAMES iconv libiconv libiconv-2 c
+ DOC "The iconv library")
+
+# Use some standard module to handle the QUIETLY and REQUIRED arguments, and
+# set ICONV_FOUND to TRUE if these two variables are set.
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(Iconv REQUIRED_VARS ICONV_LIBRARIES ICONV_INCLUDE_DIRS)
+
+# Check if the prototype is
+# size_t iconv(iconv_t cd, char** inbuf, size_t* inbytesleft,
+# char** outbuf, size_t* outbytesleft);
+# or
+# size_t iconv (iconv_t cd, const char** inbuf, size_t* inbytesleft,
+# char** outbuf, size_t* outbytesleft);
+if(ICONV_FOUND)
+ include(CheckCXXSourceCompiles)
+
+ # Set the parameters needed to compile the following code.
+ set(CMAKE_REQUIRED_INCLUDES ${ICONV_INCLUDE_DIRS})
+ set(CMAKE_REQUIRED_LIBRARIES ${ICONV_LIBRARIES})
+
+ check_cxx_source_compiles("
+ #include <iconv.h>
+ int main(){
+ iconv_t conv = 0;
+ const char* in = 0;
+ size_t ilen = 0;
+ char* out = 0;
+ size_t olen = 0;
+ iconv(conv, &in, &ilen, &out, &olen);
+ return 0;}"
+ ICONV_SECOND_ARGUMENT_IS_CONST)
+
+# Compatibility for all the ways of writing these variables
+ set(ICONV_LIBRARY ${ICONV_LIBRARIES})
+ set(ICONV_INCLUDE_DIR ${ICONV_INCLUDE_DIRS})
+endif()
+
+mark_as_advanced(ICONV_INCLUDE_DIRS ICONV_LIBRARIES ICONV_SECOND_ARGUMENT_IS_CONST) \ No newline at end of file
diff --git a/louloulibs/cmake/Modules/FindLIBIDN.cmake b/louloulibs/cmake/Modules/FindLIBIDN.cmake
new file mode 100644
index 0000000..611a6a8
--- /dev/null
+++ b/louloulibs/cmake/Modules/FindLIBIDN.cmake
@@ -0,0 +1,41 @@
+# - Find libidn
+# Find the libidn library, and more particularly the stringprep header.
+#
+# This module defines the following variables:
+# LIBIDN_FOUND - True if library and include directory are found
+# If set to TRUE, the following are also defined:
+# LIBIDN_INCLUDE_DIRS - The directory where to find the header file
+# LIBIDN_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.
+# LIBIDN_INCLUDE_DIR
+# LIBIDN_LIBRARY
+#
+# This file is in the public domain
+
+include(FindPkgConfig)
+pkg_check_modules(LIBIDN libidn)
+
+if(NOT LIBIDN_FOUND)
+ find_path(LIBIDN_INCLUDE_DIRS NAMES stringprep.h
+ DOC "The libidn include directory")
+
+ # The library containing the stringprep module is libidn
+ find_library(LIBIDN_LIBRARIES NAMES idn
+ DOC "The libidn library")
+
+ # Use some standard module to handle the QUIETLY and REQUIRED arguments, and
+ # set LIBIDN_FOUND to TRUE if these two variables are set.
+ include(FindPackageHandleStandardArgs)
+ find_package_handle_standard_args(LIBIDN REQUIRED_VARS LIBIDN_LIBRARIES LIBIDN_INCLUDE_DIRS)
+
+ # Compatibility for all the ways of writing these variables
+ if(LIBIDN_FOUND)
+ set(LIBIDN_INCLUDE_DIR ${LIBIDN_INCLUDE_DIRS})
+ set(LIBIDN_LIBRARY ${LIBIDN_LIBRARIES})
+ endif()
+endif()
+
+mark_as_advanced(LIBIDN_INCLUDE_DIRS LIBIDN_LIBRARIES) \ No newline at end of file
diff --git a/louloulibs/cmake/Modules/FindLIBUUID.cmake b/louloulibs/cmake/Modules/FindLIBUUID.cmake
new file mode 100644
index 0000000..17d3c42
--- /dev/null
+++ b/louloulibs/cmake/Modules/FindLIBUUID.cmake
@@ -0,0 +1,41 @@
+# - Find libuuid
+# Find the libuuid library
+#
+# This module defines the following variables:
+# LIBUUID_FOUND - True if library and include directory are found
+# If set to TRUE, the following are also defined:
+# LIBUUID_INCLUDE_DIRS - The directory where to find the header file
+# LIBUUID_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.
+# LIBUUID_INCLUDE_DIR
+# LIBUUID_LIBRARY
+#
+# This file is in the public domain
+
+include(FindPkgConfig)
+pkg_check_modules(LIBUUID uuid)
+
+if(NOT LIBUUID_FOUND)
+ find_path(LIBUUID_INCLUDE_DIRS NAMES uuid.h
+ PATH_SUFFIXES uuid
+ DOC "The libuuid include directory")
+
+ find_library(LIBUUID_LIBRARIES NAMES uuid
+ DOC "The libuuid library")
+
+ # Use some standard module to handle the QUIETLY and REQUIRED arguments, and
+ # set LIBUUID_FOUND to TRUE if these two variables are set.
+ include(FindPackageHandleStandardArgs)
+ find_package_handle_standard_args(LIBUUID REQUIRED_VARS LIBUUID_LIBRARIES LIBUUID_INCLUDE_DIRS)
+
+ # Compatibility for all the ways of writing these variables
+ if(LIBUUID_FOUND)
+ set(LIBUUID_INCLUDE_DIR ${LIBUUID_INCLUDE_DIRS})
+ set(LIBUUID_LIBRARY ${LIBUUID_LIBRARIES})
+ endif()
+endif()
+
+mark_as_advanced(LIBUUID_INCLUDE_DIRS LIBUUID_LIBRARIES)
diff --git a/louloulibs/cmake/Modules/FindSYSTEMD.cmake b/louloulibs/cmake/Modules/FindSYSTEMD.cmake
new file mode 100644
index 0000000..c7decde
--- /dev/null
+++ b/louloulibs/cmake/Modules/FindSYSTEMD.cmake
@@ -0,0 +1,39 @@
+# - Find SystemdDaemon
+# Find the systemd daemon library
+#
+# This module defines the following variables:
+# SYSTEMD_FOUND - True if library and include directory are found
+# If set to TRUE, the following are also defined:
+# SYSTEMD_INCLUDE_DIRS - The directory where to find the header file
+# SYSTEMD_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.
+# SYSTEMD_LIBRARY
+# SYSTEMD_INCLUDE_DIR
+#
+# This file is in the public domain
+
+include(FindPkgConfig)
+pkg_check_modules(SYSTEMD libsystemd)
+
+if(NOT SYSTEMD_FOUND)
+ find_path(SYSTEMD_INCLUDE_DIRS NAMES systemd/sd-daemon.h
+ DOC "The Systemd include directory")
+
+ find_library(SYSTEMD_LIBRARIES NAMES systemd
+ DOC "The Systemd library")
+
+ # Use some standard module to handle the QUIETLY and REQUIRED arguments, and
+ # set SYSTEMD_FOUND to TRUE if these two variables are set.
+ include(FindPackageHandleStandardArgs)
+ find_package_handle_standard_args(SYSTEMD REQUIRED_VARS SYSTEMD_LIBRARIES SYSTEMD_INCLUDE_DIRS)
+
+ if(SYSTEMD_FOUND)
+ set(SYSTEMD_LIBRARY ${SYSTEMD_LIBRARIES})
+ set(SYSTEMD_INCLUDE_DIR ${SYSTEMD_INCLUDE_DIRS})
+ endif()
+endif()
+
+mark_as_advanced(SYSTEMD_INCLUDE_DIRS SYSTEMD_LIBRARIES) \ No newline at end of file
diff --git a/louloulibs/config/config.cpp b/louloulibs/config/config.cpp
new file mode 100644
index 0000000..417981d
--- /dev/null
+++ b/louloulibs/config/config.cpp
@@ -0,0 +1,104 @@
+#include <config/config.hpp>
+#include <logger/logger.hpp>
+
+#include <cstring>
+#include <sstream>
+
+#include <cstdlib>
+
+std::string Config::filename{};
+std::map<std::string, std::string> Config::values{};
+std::vector<t_config_changed_callback> Config::callbacks{};
+
+std::string Config::get(const std::string& option, const std::string& def)
+{
+ auto it = Config::values.find(option);
+
+ if (it == Config::values.end())
+ return def;
+ return it->second;
+}
+
+int Config::get_int(const std::string& option, const int& def)
+{
+ std::string res = Config::get(option, "");
+ if (!res.empty())
+ return std::atoi(res.c_str());
+ else
+ return def;
+}
+
+void Config::set(const std::string& option, const std::string& value, bool save)
+{
+ Config::values[option] = value;
+ if (save)
+ {
+ Config::save_to_file();
+ Config::trigger_configuration_change();
+ }
+}
+
+void Config::connect(t_config_changed_callback callback)
+{
+ Config::callbacks.push_back(callback);
+}
+
+void Config::clear()
+{
+ Config::values.clear();
+}
+
+/**
+ * Private methods
+ */
+void Config::trigger_configuration_change()
+{
+ std::vector<t_config_changed_callback>::iterator it;
+ for (it = Config::callbacks.begin(); it < Config::callbacks.end(); ++it)
+ (*it)();
+}
+
+bool Config::read_conf(const std::string& name)
+{
+ if (!name.empty())
+ Config::filename = name;
+
+ std::ifstream file(Config::filename.data());
+ if (!file.is_open())
+ {
+ log_error("Error while opening file ", filename, " for reading: ", strerror(errno));
+ return false;
+ }
+
+ Config::clear();
+
+ std::string line;
+ size_t pos;
+ std::string option;
+ std::string value;
+ while (file.good())
+ {
+ std::getline(file, line);
+ if (line == "" || line[0] == '#')
+ continue ;
+ pos = line.find('=');
+ if (pos == std::string::npos)
+ continue ;
+ option = line.substr(0, pos);
+ value = line.substr(pos+1);
+ Config::values[option] = value;
+ }
+ return true;
+}
+
+void Config::save_to_file()
+{
+ std::ofstream file(Config::filename.data());
+ if (file.fail())
+ {
+ log_error("Could not save config file.");
+ return ;
+ }
+ for (const auto& it: Config::values)
+ file << it.first << "=" << it.second << '\n';
+}
diff --git a/louloulibs/config/config.hpp b/louloulibs/config/config.hpp
new file mode 100644
index 0000000..6728df8
--- /dev/null
+++ b/louloulibs/config/config.hpp
@@ -0,0 +1,94 @@
+/**
+ * Read the config file and save all the values in a map.
+ * Also, a singleton.
+ *
+ * Use Config::filename = "bla" to set the filename you want to use.
+ *
+ * If you want to exit if the file does not exist when it is open for
+ * reading, set Config::file_must_exist = true.
+ *
+ * Config::get() can then be used to access the values in the conf.
+ *
+ * Use Config::close() when you're done getting/setting value. This will
+ * save the config into the file.
+ */
+
+#pragma once
+
+
+#include <functional>
+#include <fstream>
+#include <memory>
+#include <vector>
+#include <string>
+#include <map>
+
+typedef std::function<void()> t_config_changed_callback;
+
+class Config
+{
+public:
+ Config() = default;
+ ~Config() = default;
+ Config(const Config&) = delete;
+ Config& operator=(const Config&) = delete;
+ Config(Config&&) = delete;
+ Config& operator=(Config&&) = delete;
+
+ /**
+ * returns a value from the config. If it doesn’t exist, use
+ * the second argument as the default.
+ */
+ static std::string get(const std::string&, const std::string&);
+ /**
+ * returns a value from the config. If it doesn’t exist, use
+ * the second argument as the default.
+ */
+ static int get_int(const std::string&, const int&);
+ /**
+ * Set a value for the given option. And write all the config
+ * in the file from which it was read if save is true.
+ */
+ static void set(const std::string&, const std::string&, bool save = false);
+ /**
+ * Adds a function to a list. This function will be called whenever a
+ * configuration change occurs (when set() is called, or when the initial
+ * conf is read)
+ */
+ static void connect(t_config_changed_callback);
+ /**
+ * Destroy the instance, forcing it to be recreated (with potentially
+ * different parameters) the next time it’s needed.
+ */
+ static void clear();
+ /**
+ * Read the configuration file at the given path.
+ */
+ static bool read_conf(const std::string& name="");
+ /**
+ * Get the filename
+ */
+ static const std::string& get_filename()
+ { return Config::filename; }
+
+private:
+ /**
+ * Set the value of the filename to use, before calling any method.
+ */
+ static std::string filename;
+ /**
+ * Write all the config values into the configuration file
+ */
+ static void save_to_file();
+ /**
+ * Call all the callbacks previously registered using connect().
+ * This is used to notify any class that a configuration change occured.
+ */
+ static void trigger_configuration_change();
+
+ static std::map<std::string, std::string> values;
+ static std::vector<t_config_changed_callback> callbacks;
+
+};
+
+
diff --git a/louloulibs/logger/logger.cpp b/louloulibs/logger/logger.cpp
new file mode 100644
index 0000000..7336579
--- /dev/null
+++ b/louloulibs/logger/logger.cpp
@@ -0,0 +1,38 @@
+#include <logger/logger.hpp>
+#include <config/config.hpp>
+
+Logger::Logger(const int log_level):
+ log_level(log_level),
+ stream(std::cout.rdbuf())
+{
+}
+
+Logger::Logger(const int log_level, const std::string& log_file):
+ log_level(log_level),
+ ofstream(log_file.data(), std::ios_base::app),
+ stream(ofstream.rdbuf())
+{
+}
+
+std::unique_ptr<Logger>& Logger::instance()
+{
+ static std::unique_ptr<Logger> instance;
+
+ if (!instance)
+ {
+ const std::string log_file = Config::get("log_file", "");
+ const int log_level = Config::get_int("log_level", 0);
+ if (log_file.empty())
+ instance = std::make_unique<Logger>(log_level);
+ else
+ instance = std::make_unique<Logger>(log_level, log_file);
+ }
+ return instance;
+}
+
+std::ostream& Logger::get_stream(const int lvl)
+{
+ if (lvl >= this->log_level)
+ return this->stream;
+ return this->null_stream;
+}
diff --git a/louloulibs/logger/logger.hpp b/louloulibs/logger/logger.hpp
new file mode 100644
index 0000000..0893c77
--- /dev/null
+++ b/louloulibs/logger/logger.hpp
@@ -0,0 +1,126 @@
+#pragma once
+
+
+/**
+ * Singleton used in logger macros to write into files or stdout, with
+ * various levels of severity.
+ * Only the macros should be used.
+ * @class Logger
+ */
+
+#include <memory>
+#include <iostream>
+#include <fstream>
+
+#define debug_lvl 0
+#define info_lvl 1
+#define warning_lvl 2
+#define error_lvl 3
+
+#include "louloulibs.h"
+#ifdef SYSTEMD_FOUND
+# include <systemd/sd-daemon.h>
+#else
+# define SD_DEBUG "[DEBUG]: "
+# define SD_INFO "[INFO]: "
+# define SD_WARNING "[WARNING]: "
+# define SD_ERR "[ERROR]: "
+#endif
+
+// Macro defined to get the filename instead of the full path. But if it is
+// not properly defined by the build system, we fallback to __FILE__
+#ifndef __FILENAME__
+# define __FILENAME__ __FILE__
+#endif
+
+/**
+ * Juste a structure representing a stream doing nothing with its input.
+ */
+class nullstream: public std::ostream
+{
+public:
+ nullstream():
+ std::ostream(0)
+ { }
+};
+
+class Logger
+{
+public:
+ static std::unique_ptr<Logger>& instance();
+ std::ostream& get_stream(const int);
+ Logger(const int log_level, const std::string& log_file);
+ Logger(const int log_level);
+
+ Logger(const Logger&) = delete;
+ Logger& operator=(const Logger&) = delete;
+ Logger(Logger&&) = delete;
+ Logger& operator=(Logger&&) = delete;
+
+private:
+ const int log_level;
+ std::ofstream ofstream;
+ nullstream null_stream;
+ std::ostream stream;
+};
+
+#define WHERE __FILENAME__, ":", __LINE__, ":\t"
+
+namespace logging_details
+{
+ template <typename T>
+ void log(std::ostream& os, const T& arg)
+ {
+ os << arg << std::endl;
+ }
+
+ template <typename T, typename... U>
+ void log(std::ostream& os, const T& first, U&&... rest)
+ {
+ os << first;
+ log(os, std::forward<U>(rest)...);
+ }
+
+ template <typename... U>
+ void log_debug(U&&... args)
+ {
+ auto& os = Logger::instance()->get_stream(debug_lvl);
+ os << SD_DEBUG;
+ log(os, std::forward<U>(args)...);
+ }
+
+ template <typename... U>
+ void log_info(U&&... args)
+ {
+ auto& os = Logger::instance()->get_stream(info_lvl);
+ os << SD_INFO;
+ log(os, std::forward<U>(args)...);
+ }
+
+ template <typename... U>
+ void log_warning(U&&... args)
+ {
+ auto& os = Logger::instance()->get_stream(warning_lvl);
+ os << SD_WARNING;
+ log(os, std::forward<U>(args)...);
+ }
+
+ template <typename... U>
+ void log_error(U&&... args)
+ {
+ auto& os = Logger::instance()->get_stream(error_lvl);
+ os << SD_ERR;
+ log(os, std::forward<U>(args)...);
+ }
+}
+
+#define log_info(...) logging_details::log_info(WHERE, __VA_ARGS__)
+
+#define log_warning(...) logging_details::log_warning(WHERE, __VA_ARGS__)
+
+#define log_error(...) logging_details::log_error(WHERE, __VA_ARGS__)
+
+#define log_debug(...) logging_details::log_debug(WHERE, __VA_ARGS__)
+
+
+
diff --git a/louloulibs/louloulibs.h.cmake b/louloulibs/louloulibs.h.cmake
new file mode 100644
index 0000000..2feaf4e
--- /dev/null
+++ b/louloulibs/louloulibs.h.cmake
@@ -0,0 +1,9 @@
+#define SYSTEM_NAME "${CMAKE_SYSTEM}"
+#cmakedefine ICONV_SECOND_ARGUMENT_IS_CONST
+#cmakedefine LIBIDN_FOUND
+#cmakedefine SYSTEMD_FOUND
+#cmakedefine POLLER ${POLLER}
+#cmakedefine BOTAN_FOUND
+#cmakedefine CARES_FOUND
+#cmakedefine SOFTWARE_VERSION "${SOFTWARE_VERSION}"
+#cmakedefine PROJECT_NAME "${PROJECT_NAME}" \ No newline at end of file
diff --git a/louloulibs/network/credentials_manager.cpp b/louloulibs/network/credentials_manager.cpp
new file mode 100644
index 0000000..ee83c3b
--- /dev/null
+++ b/louloulibs/network/credentials_manager.cpp
@@ -0,0 +1,116 @@
+#include "louloulibs.h"
+
+#ifdef BOTAN_FOUND
+#include <network/tcp_socket_handler.hpp>
+#include <network/credentials_manager.hpp>
+#include <logger/logger.hpp>
+#include <botan/tls_exceptn.h>
+#include <config/config.hpp>
+
+#ifdef USE_DATABASE
+# include <database/database.hpp>
+#endif
+
+/**
+ * TODO find a standard way to find that out.
+ */
+static const std::vector<std::string> default_cert_files = {
+ "/etc/ssl/certs/ca-bundle.crt",
+ "/etc/pki/tls/certs/ca-bundle.crt",
+ "/etc/ssl/certs/ca-certificates.crt",
+ "/etc/ca-certificates/extracted/tls-ca-bundle.pem"
+};
+
+Botan::Certificate_Store_In_Memory BasicCredentialsManager::certificate_store;
+bool BasicCredentialsManager::certs_loaded = false;
+
+BasicCredentialsManager::BasicCredentialsManager(const TCPSocketHandler* const socket_handler):
+ Botan::Credentials_Manager(),
+ socket_handler(socket_handler),
+ trusted_fingerprint{}
+{
+ this->load_certs();
+}
+
+void BasicCredentialsManager::set_trusted_fingerprint(const std::string& fingerprint)
+{
+ this->trusted_fingerprint = fingerprint;
+}
+
+void BasicCredentialsManager::verify_certificate_chain(const std::string& type,
+ const std::string& purported_hostname,
+ const std::vector<Botan::X509_Certificate>& certs)
+{
+ log_debug("Checking remote certificate (", type, ") for hostname ", purported_hostname);
+ try
+ {
+ Botan::Credentials_Manager::verify_certificate_chain(type, purported_hostname, certs);
+ log_debug("Certificate is valid");
+ }
+ catch (const std::exception& tls_exception)
+ {
+ log_warning("TLS certificate check failed: ", tls_exception.what());
+ if (!this->trusted_fingerprint.empty() && !certs.empty() &&
+ this->trusted_fingerprint == certs[0].fingerprint() &&
+ certs[0].matches_dns_name(purported_hostname))
+ // We trust the certificate, based on the trusted fingerprint and
+ // the fact that the hostname matches
+ return;
+
+ if (this->socket_handler->abort_on_invalid_cert())
+ throw;
+ }
+}
+
+void BasicCredentialsManager::load_certs()
+{
+ // Only load the certificates the first time
+ if (BasicCredentialsManager::certs_loaded)
+ return;
+ const std::string conf_path = Config::get("ca_file", "");
+ std::vector<std::string> paths;
+ if (conf_path.empty())
+ paths = default_cert_files;
+ else
+ paths.push_back(conf_path);
+ for (const auto& path: paths)
+ {
+ try
+ {
+ Botan::DataSource_Stream bundle(path);
+ log_debug("Using ca bundle: ", path);
+ while (!bundle.end_of_data() && bundle.check_available(27))
+ {
+ // TODO: remove this work-around for Botan 1.11.29
+ // https://github.com/randombit/botan/issues/438#issuecomment-192866796
+ // Note that every certificate that fails to be transcoded into latin-1
+ // will be ignored. As a result, some TLS connection may be refused
+ // because the certificate is signed by an issuer that was ignored.
+ try {
+ const Botan::X509_Certificate cert(bundle);
+ BasicCredentialsManager::certificate_store.add_certificate(cert);
+ } catch (const Botan::Decoding_Error& error)
+ {
+ continue;
+ }
+ }
+ // Only use the first file that can successfully be read.
+ goto success;
+ }
+ catch (Botan::Stream_IO_Error& e)
+ {
+ log_debug(e.what());
+ }
+ }
+ // If we could not open one of the files, print a warning
+ log_warning("The CA could not be loaded, TLS negociation will probably fail.");
+ success:
+ BasicCredentialsManager::certs_loaded = true;
+}
+
+std::vector<Botan::Certificate_Store*> BasicCredentialsManager::trusted_certificate_authorities(const std::string&, const std::string&)
+{
+ return {&this->certificate_store};
+}
+
+#endif
diff --git a/louloulibs/network/credentials_manager.hpp b/louloulibs/network/credentials_manager.hpp
new file mode 100644
index 0000000..0fc4b89
--- /dev/null
+++ b/louloulibs/network/credentials_manager.hpp
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "louloulibs.h"
+
+#ifdef BOTAN_FOUND
+
+#include <botan/botan.h>
+#include <botan/tls_client.h>
+
+class TCPSocketHandler;
+
+class BasicCredentialsManager: public Botan::Credentials_Manager
+{
+public:
+ BasicCredentialsManager(const TCPSocketHandler* const socket_handler);
+
+ BasicCredentialsManager(BasicCredentialsManager&&) = delete;
+ BasicCredentialsManager(const BasicCredentialsManager&) = delete;
+ BasicCredentialsManager& operator=(const BasicCredentialsManager&) = delete;
+ BasicCredentialsManager& operator=(BasicCredentialsManager&&) = delete;
+
+ void verify_certificate_chain(const std::string& type,
+ const std::string& purported_hostname,
+ const std::vector<Botan::X509_Certificate>&) override final;
+ std::vector<Botan::Certificate_Store*> trusted_certificate_authorities(const std::string& type,
+ const std::string& context) override final;
+ void set_trusted_fingerprint(const std::string& fingerprint);
+
+private:
+ const TCPSocketHandler* const socket_handler;
+
+ static void load_certs();
+ static Botan::Certificate_Store_In_Memory certificate_store;
+ static bool certs_loaded;
+ std::string trusted_fingerprint;
+};
+
+#endif //BOTAN_FOUND
+
diff --git a/louloulibs/network/dns_handler.cpp b/louloulibs/network/dns_handler.cpp
new file mode 100644
index 0000000..e267944
--- /dev/null
+++ b/louloulibs/network/dns_handler.cpp
@@ -0,0 +1,134 @@
+#include <louloulibs.h>
+#ifdef CARES_FOUND
+
+#include <network/dns_socket_handler.hpp>
+#include <network/dns_handler.hpp>
+#include <network/poller.hpp>
+
+#include <utils/timed_events.hpp>
+
+#include <algorithm>
+#include <stdexcept>
+
+DNSHandler DNSHandler::instance;
+
+using namespace std::string_literals;
+DNSHandler::DNSHandler():
+ socket_handlers{},
+ channel{nullptr}
+{
+ int ares_error;
+ if ((ares_error = ::ares_library_init(ARES_LIB_INIT_ALL)) != 0)
+ throw std::runtime_error("Failed to initialize c-ares lib: "s + ares_strerror(ares_error));
+ struct ares_options options = {};
+ // The default timeout values are way too high
+ options.timeout = 1000;
+ options.tries = 3;
+ if ((ares_error = ::ares_init_options(&this->channel,
+ &options,
+ ARES_OPT_TIMEOUTMS|ARES_OPT_TRIES)) != ARES_SUCCESS)
+ throw std::runtime_error("Failed to initialize c-ares channel: "s + ares_strerror(ares_error));
+}
+
+ares_channel& DNSHandler::get_channel()
+{
+ return this->channel;
+}
+
+void DNSHandler::destroy()
+{
+ this->remove_all_sockets_from_poller();
+ this->socket_handlers.clear();
+ ::ares_destroy(this->channel);
+ ::ares_library_cleanup();
+}
+
+void DNSHandler::gethostbyname(const std::string& name, ares_host_callback callback,
+ void* data, int family)
+{
+ if (family == AF_INET)
+ ::ares_gethostbyname(this->channel, name.data(), family,
+ callback, data);
+ else
+ ::ares_gethostbyname(this->channel, name.data(), family,
+ callback, data);
+}
+
+void DNSHandler::watch_dns_sockets(std::shared_ptr<Poller>& poller)
+{
+ fd_set readers;
+ fd_set writers;
+
+ FD_ZERO(&readers);
+ FD_ZERO(&writers);
+
+ int ndfs = ::ares_fds(this->channel, &readers, &writers);
+ // For each existing DNS socket, see if we are still supposed to watch it,
+ // if not then erase it
+ this->socket_handlers.erase(
+ std::remove_if(this->socket_handlers.begin(), this->socket_handlers.end(),
+ [&readers](const auto& dns_socket)
+ {
+ return !FD_ISSET(dns_socket->get_socket(), &readers);
+ }),
+ this->socket_handlers.end());
+
+ for (auto i = 0; i < ndfs; ++i)
+ {
+ bool read = FD_ISSET(i, &readers);
+ bool write = FD_ISSET(i, &writers);
+ // Look for the DNSSocketHandler with this fd
+ auto it = std::find_if(this->socket_handlers.begin(),
+ this->socket_handlers.end(),
+ [i](const auto& socket_handler)
+ {
+ return i == socket_handler->get_socket();
+ });
+ if (!read && !write) // No need to read or write to it
+ { // If found, erase it and stop watching it because it is not
+ // needed anymore
+ if (it != this->socket_handlers.end())
+ // The socket destructor removes it from the poller
+ this->socket_handlers.erase(it);
+ }
+ else // We need to write and/or read to it
+ { // If not found, create it because we need to watch it
+ if (it == this->socket_handlers.end())
+ {
+ this->socket_handlers.emplace(this->socket_handlers.begin(),
+ std::make_unique<DNSSocketHandler>(poller, *this, i));
+ it = this->socket_handlers.begin();
+ }
+ poller->add_socket_handler(it->get());
+ if (write)
+ poller->watch_send_events(it->get());
+ }
+ }
+ // Cancel previous timer, if any.
+ TimedEventsManager::instance().cancel("DNS timeout");
+ struct timeval tv;
+ struct timeval* tvp;
+ tvp = ::ares_timeout(this->channel, NULL, &tv);
+ if (tvp)
+ {
+ auto future_time = std::chrono::steady_clock::now() + std::chrono::seconds(tvp->tv_sec) + \
+ std::chrono::microseconds(tvp->tv_usec);
+ TimedEventsManager::instance().add_event(TimedEvent(std::move(future_time),
+ [this]()
+ {
+ for (auto& dns_socket_handler: this->socket_handlers)
+ dns_socket_handler->on_recv();
+ },
+ "DNS timeout"));
+ }
+}
+
+void DNSHandler::remove_all_sockets_from_poller()
+{
+ for (const auto& socket_handler: this->socket_handlers)
+ {
+ socket_handler->remove_from_poller();
+ }
+}
+
+#endif /* CARES_FOUND */
diff --git a/louloulibs/network/dns_handler.hpp b/louloulibs/network/dns_handler.hpp
new file mode 100644
index 0000000..fd1729d
--- /dev/null
+++ b/louloulibs/network/dns_handler.hpp
@@ -0,0 +1,58 @@
+#pragma once
+
+#include <louloulibs.h>
+#ifdef CARES_FOUND
+
+class TCPSocketHandler;
+class Poller;
+class DNSSocketHandler;
+
+# include <ares.h>
+# include <memory>
+# include <string>
+# include <vector>
+
+/**
+ * Class managing DNS resolution. It should only be statically instanciated
+ * once in SocketHandler. It manages ares channel and calls various
+ * functions of that library.
+ */
+
+class DNSHandler
+{
+public:
+ DNSHandler();
+ ~DNSHandler() = default;
+ DNSHandler(const DNSHandler&) = delete;
+ DNSHandler(DNSHandler&&) = delete;
+ DNSHandler& operator=(const DNSHandler&) = delete;
+ DNSHandler& operator=(DNSHandler&&) = delete;
+
+ void gethostbyname(const std::string& name, ares_host_callback callback,
+ void* socket_handler, int family);
+ /**
+ * Call ares_fds to know what fd needs to be watched by the poller, create
+ * or destroy DNSSocketHandlers depending on the result.
+ */
+ void watch_dns_sockets(std::shared_ptr<Poller>& poller);
+ /**
+ * Destroy and stop watching all the DNS sockets. Then de-init the channel
+ * and library.
+ */
+ void destroy();
+ void remove_all_sockets_from_poller();
+ ares_channel& get_channel();
+
+ static DNSHandler instance;
+
+private:
+ /**
+ * The list of sockets that needs to be watched, according to the last
+ * call to ares_fds. DNSSocketHandlers are added to it or removed from it
+ * in the watch_dns_sockets() method
+ */
+ std::vector<std::unique_ptr<DNSSocketHandler>> socket_handlers;
+ ares_channel channel;
+};
+
+#endif /* CARES_FOUND */
diff --git a/louloulibs/network/dns_socket_handler.cpp b/louloulibs/network/dns_socket_handler.cpp
new file mode 100644
index 0000000..5fd08cb
--- /dev/null
+++ b/louloulibs/network/dns_socket_handler.cpp
@@ -0,0 +1,48 @@
+#include <louloulibs.h>
+#ifdef CARES_FOUND
+
+#include <network/dns_socket_handler.hpp>
+#include <network/dns_handler.hpp>
+#include <network/poller.hpp>
+
+#include <ares.h>
+
+DNSSocketHandler::DNSSocketHandler(std::shared_ptr<Poller> poller,
+ DNSHandler& handler,
+ const socket_t socket):
+ SocketHandler(poller, socket),
+ handler(handler)
+{
+}
+
+void DNSSocketHandler::connect()
+{
+}
+
+void DNSSocketHandler::on_recv()
+{
+ // always stop watching send and read events. We will re-watch them if the
+ // next call to ares_fds tell us to
+ this->handler.remove_all_sockets_from_poller();
+ ::ares_process_fd(DNSHandler::instance.get_channel(), this->socket, ARES_SOCKET_BAD);
+}
+
+void DNSSocketHandler::on_send()
+{
+ // always stop watching send and read events. We will re-watch them if the
+ // next call to ares_fds tell us to
+ this->handler.remove_all_sockets_from_poller();
+ ::ares_process_fd(DNSHandler::instance.get_channel(), ARES_SOCKET_BAD, this->socket);
+}
+
+bool DNSSocketHandler::is_connected() const
+{
+ return true;
+}
+
+void DNSSocketHandler::remove_from_poller()
+{
+ this->poller->remove_socket_handler(this->socket);
+}
+
+#endif /* CARES_FOUND */
diff --git a/louloulibs/network/dns_socket_handler.hpp b/louloulibs/network/dns_socket_handler.hpp
new file mode 100644
index 0000000..0570196
--- /dev/null
+++ b/louloulibs/network/dns_socket_handler.hpp
@@ -0,0 +1,49 @@
+#pragma once
+
+#include <louloulibs.h>
+#ifdef CARES_FOUND
+
+#include <network/socket_handler.hpp>
+#include <ares.h>
+
+/**
+ * Manage a socket returned by ares_fds. We do not create, open or close the
+ * socket ourself: this is done by c-ares. We just call ares_process_fd()
+ * with the correct parameters, depending on what can be done on that socket
+ * (Poller reported it to be writable or readeable)
+ */
+
+class DNSHandler;
+
+class DNSSocketHandler: public SocketHandler
+{
+public:
+ explicit DNSSocketHandler(std::shared_ptr<Poller> poller, DNSHandler& handler, const socket_t socket);
+ ~DNSSocketHandler() = default;
+ DNSSocketHandler(const DNSSocketHandler&) = delete;
+ DNSSocketHandler(DNSSocketHandler&&) = delete;
+ DNSSocketHandler& operator=(const DNSSocketHandler&) = delete;
+ DNSSocketHandler& operator=(DNSSocketHandler&&) = delete;
+
+ /**
+ * Just call dns_process_fd, c-ares will do its work of send()ing or
+ * recv()ing the data it wants on that socket.
+ */
+ void on_recv() override final;
+ void on_send() override final;
+ /**
+ * Do nothing, because we are always considered to be connected, since the
+ * connection is done by c-ares and not by us.
+ */
+ void connect() override final;
+ /**
+ * Always true, see the comment for connect()
+ */
+ bool is_connected() const override final;
+ void remove_from_poller();
+
+private:
+ DNSHandler& handler;
+};
+
+#endif // CARES_FOUND
diff --git a/louloulibs/network/poller.cpp b/louloulibs/network/poller.cpp
new file mode 100644
index 0000000..8a6fd97
--- /dev/null
+++ b/louloulibs/network/poller.cpp
@@ -0,0 +1,228 @@
+#include <network/poller.hpp>
+#include <logger/logger.hpp>
+#include <utils/timed_events.hpp>
+
+#include <assert.h>
+#include <errno.h>
+#include <stdio.h>
+#include <signal.h>
+#include <unistd.h>
+
+#include <cstring>
+#include <iostream>
+#include <stdexcept>
+
+Poller::Poller()
+{
+#if POLLER == POLL
+ this->nfds = 0;
+#elif POLLER == EPOLL
+ this->epfd = ::epoll_create1(0);
+ if (this->epfd == -1)
+ {
+ log_error("epoll failed: ", strerror(errno));
+ throw std::runtime_error("Could not create epoll instance");
+ }
+#endif
+}
+
+Poller::~Poller()
+{
+#if POLLER == EPOLL
+ if (this->epfd > 0)
+ ::close(this->epfd);
+#endif
+}
+
+void Poller::add_socket_handler(SocketHandler* socket_handler)
+{
+ // Don't do anything if the socket is already managed
+ const auto it = this->socket_handlers.find(socket_handler->get_socket());
+ if (it != this->socket_handlers.end())
+ return ;
+
+ this->socket_handlers.emplace(socket_handler->get_socket(), socket_handler);
+
+ // We always watch all sockets for receive events
+#if POLLER == POLL
+ this->fds[this->nfds].fd = socket_handler->get_socket();
+ this->fds[this->nfds].events = POLLIN;
+ this->nfds++;
+#endif
+#if POLLER == EPOLL
+ struct epoll_event event = {EPOLLIN, {socket_handler}};
+ const int res = ::epoll_ctl(this->epfd, EPOLL_CTL_ADD, socket_handler->get_socket(), &event);
+ if (res == -1)
+ {
+ log_error("epoll_ctl failed: ", strerror(errno));
+ throw std::runtime_error("Could not add socket to epoll");
+ }
+#endif
+}
+
+void Poller::remove_socket_handler(const socket_t socket)
+{
+ const auto it = this->socket_handlers.find(socket);
+ if (it == this->socket_handlers.end())
+ throw std::runtime_error("Trying to remove a SocketHandler that is not managed");
+ this->socket_handlers.erase(it);
+
+#if POLLER == POLL
+ for (size_t i = 0; i < this->nfds; i++)
+ {
+ if (this->fds[i].fd == socket)
+ {
+ // Move all subsequent pollfd by one on the left, erasing the
+ // value of the one we remove
+ for (size_t j = i; j < this->nfds - 1; ++j)
+ {
+ this->fds[j].fd = this->fds[j+1].fd;
+ this->fds[j].events= this->fds[j+1].events;
+ }
+ this->nfds--;
+ }
+ }
+#elif POLLER == EPOLL
+ const int res = ::epoll_ctl(this->epfd, EPOLL_CTL_DEL, socket, nullptr);
+ if (res == -1)
+ {
+ log_error("epoll_ctl failed: ", strerror(errno));
+ throw std::runtime_error("Could not remove socket from epoll");
+ }
+#endif
+}
+
+void Poller::watch_send_events(SocketHandler* socket_handler)
+{
+#if POLLER == POLL
+ for (size_t i = 0; i <= this->nfds; ++i)
+ {
+ if (this->fds[i].fd == socket_handler->get_socket())
+ {
+ this->fds[i].events = POLLIN|POLLOUT;
+ return;
+ }
+ }
+ throw std::runtime_error("Cannot watch a non-registered socket for send events");
+#elif POLLER == EPOLL
+ struct epoll_event event = {EPOLLIN|EPOLLOUT, {socket_handler}};
+ const int res = ::epoll_ctl(this->epfd, EPOLL_CTL_MOD, socket_handler->get_socket(), &event);
+ if (res == -1)
+ {
+ log_error("epoll_ctl failed: ", strerror(errno));
+ throw std::runtime_error("Could not modify socket flags in epoll");
+ }
+#endif
+}
+
+void Poller::stop_watching_send_events(SocketHandler* socket_handler)
+{
+#if POLLER == POLL
+ for (size_t i = 0; i <= this->nfds; ++i)
+ {
+ if (this->fds[i].fd == socket_handler->get_socket())
+ {
+ this->fds[i].events = POLLIN;
+ return;
+ }
+ }
+ throw std::runtime_error("Cannot watch a non-registered socket for send events");
+#elif POLLER == EPOLL
+ struct epoll_event event = {EPOLLIN, {socket_handler}};
+ const int res = ::epoll_ctl(this->epfd, EPOLL_CTL_MOD, socket_handler->get_socket(), &event);
+ if (res == -1)
+ {
+ log_error("epoll_ctl failed: ", strerror(errno));
+ throw std::runtime_error("Could not modify socket flags in epoll");
+ }
+#endif
+}
+
+int Poller::poll(const std::chrono::milliseconds& timeout)
+{
+ if (this->socket_handlers.empty() && timeout == utils::no_timeout)
+ return -1;
+#if POLLER == POLL
+ // Convert our nice timeout into this ugly struct
+ struct timespec timeout_ts;
+ struct timespec* timeout_tsp;
+ if (timeout > 0s)
+ {
+ auto seconds = std::chrono::duration_cast<std::chrono::seconds>(timeout);
+ timeout_ts.tv_sec = seconds.count();
+ timeout_ts.tv_nsec = std::chrono::duration_cast<std::chrono::nanoseconds>(timeout - seconds).count();
+ timeout_tsp = &timeout_ts;
+ }
+ else
+ timeout_tsp = nullptr;
+
+ // Unblock all signals, only during the ppoll call
+ sigset_t empty_signal_set;
+ sigemptyset(&empty_signal_set);
+ int nb_events = ::ppoll(this->fds, this->nfds, timeout_tsp,
+ &empty_signal_set);
+ if (nb_events < 0)
+ {
+ if (errno == EINTR)
+ return true;
+ log_error("poll failed: ", strerror(errno));
+ throw std::runtime_error("Poll failed");
+ }
+ // We cannot possibly have more ready events than the number of fds we are
+ // watching
+ assert(static_cast<unsigned int>(nb_events) <= this->nfds);
+ for (size_t i = 0; i <= this->nfds && nb_events != 0; ++i)
+ {
+ auto socket_handler = this->socket_handlers.at(this->fds[i].fd);
+ if (this->fds[i].revents == 0)
+ continue;
+ else if (this->fds[i].revents & POLLIN && socket_handler->is_connected())
+ {
+ socket_handler->on_recv();
+ nb_events--;
+ }
+ else if (this->fds[i].revents & POLLOUT && socket_handler->is_connected())
+ {
+ socket_handler->on_send();
+ nb_events--;
+ }
+ else if (this->fds[i].revents & POLLOUT)
+ {
+ socket_handler->connect();
+ nb_events--;
+ }
+ }
+ return 1;
+#elif POLLER == EPOLL
+ static const size_t max_events = 12;
+ struct epoll_event revents[max_events];
+ // Unblock all signals, only during the epoll_pwait call
+ sigset_t empty_signal_set;
+ sigemptyset(&empty_signal_set);
+ const int nb_events = ::epoll_pwait(this->epfd, revents, max_events, timeout.count(),
+ &empty_signal_set);
+ if (nb_events == -1)
+ {
+ if (errno == EINTR)
+ return 0;
+ log_error("epoll wait: ", strerror(errno));
+ throw std::runtime_error("Epoll_wait failed");
+ }
+ for (int i = 0; i < nb_events; ++i)
+ {
+ auto socket_handler = static_cast<SocketHandler*>(revents[i].data.ptr);
+ if (revents[i].events & EPOLLIN && socket_handler->is_connected())
+ socket_handler->on_recv();
+ else if (revents[i].events & EPOLLOUT && socket_handler->is_connected())
+ socket_handler->on_send();
+ else if (revents[i].events & EPOLLOUT)
+ socket_handler->connect();
+ }
+ return nb_events;
+#endif
+}
+
+size_t Poller::size() const
+{
+ return this->socket_handlers.size();
+}
diff --git a/louloulibs/network/poller.hpp b/louloulibs/network/poller.hpp
new file mode 100644
index 0000000..fc1a1a1
--- /dev/null
+++ b/louloulibs/network/poller.hpp
@@ -0,0 +1,94 @@
+#pragma once
+
+
+#include <network/socket_handler.hpp>
+
+#include <unordered_map>
+#include <memory>
+#include <chrono>
+
+#define POLL 1
+#define EPOLL 2
+#define KQUEUE 3
+#include <louloulibs.h>
+#ifndef POLLER
+ #define POLLER POLL
+#endif
+
+#if POLLER == POLL
+ #include <poll.h>
+ #define MAX_POLL_FD_NUMBER 4096
+#elif POLLER == EPOLL
+ #include <sys/epoll.h>
+#else
+ #error Invalid POLLER value
+#endif
+
+/**
+ * We pass some SocketHandlers to this Poller, which uses
+ * poll/epoll/kqueue/select etc to wait for events on these SocketHandlers,
+ * and call the callbacks when event occurs.
+ *
+ * TODO: support these pollers:
+ * - kqueue(2)
+ */
+
+class Poller
+{
+public:
+ explicit Poller();
+ ~Poller();
+ Poller(const Poller&) = delete;
+ Poller(Poller&&) = delete;
+ Poller& operator=(const Poller&) = delete;
+ Poller& operator=(Poller&&) = delete;
+ /**
+ * Add a SocketHandler to be monitored by this Poller. All receive events
+ * are always automatically watched.
+ */
+ void add_socket_handler(SocketHandler* socket_handler);
+ /**
+ * Remove (and stop managing) a SocketHandler, designated by the given socket_t.
+ */
+ void remove_socket_handler(const socket_t socket);
+ /**
+ * Signal the poller that he needs to watch for send events for the given
+ * SocketHandler.
+ */
+ void watch_send_events(SocketHandler* socket_handler);
+ /**
+ * Signal the poller that he needs to stop watching for send events for
+ * this SocketHandler.
+ */
+ void stop_watching_send_events(SocketHandler* socket_handler);
+ /**
+ * Wait for all watched events, and call the SocketHandlers' callbacks
+ * when one is ready. Returns if nothing happened before the provided
+ * timeout. If the timeout is 0, it waits forever. If there is no
+ * watched event, returns -1 immediately, ignoring the timeout value.
+ * Otherwise, returns the number of event handled. If 0 is returned this
+ * means that we were interrupted by a signal, or the timeout occured.
+ */
+ int poll(const std::chrono::milliseconds& timeout);
+ /**
+ * Returns the number of SocketHandlers managed by the poller.
+ */
+ size_t size() const;
+
+private:
+ /**
+ * A "list" of all the SocketHandlers that we manage, indexed by socket,
+ * because that's what is returned by select/poll/etc when an event
+ * occures.
+ */
+ std::unordered_map<socket_t, SocketHandler*> socket_handlers;
+
+#if POLLER == POLL
+ struct pollfd fds[MAX_POLL_FD_NUMBER];
+ nfds_t nfds;
+#elif POLLER == EPOLL
+ int epfd;
+#endif
+};
+
+
diff --git a/louloulibs/network/resolver.cpp b/louloulibs/network/resolver.cpp
new file mode 100644
index 0000000..9d6de23
--- /dev/null
+++ b/louloulibs/network/resolver.cpp
@@ -0,0 +1,214 @@
+#include <network/dns_handler.hpp>
+#include <network/resolver.hpp>
+#include <string.h>
+#include <arpa/inet.h>
+
+using namespace std::string_literals;
+
+Resolver::Resolver():
+#ifdef CARES_FOUND
+ resolved4(false),
+ resolved6(false),
+ resolving(false),
+ cares_addrinfo(nullptr),
+ port{},
+#endif
+ resolved(false),
+ error_msg{}
+{
+}
+
+void Resolver::resolve(const std::string& hostname, const std::string& port,
+ SuccessCallbackType success_cb, ErrorCallbackType error_cb)
+{
+ this->error_cb = error_cb;
+ this->success_cb = success_cb;
+#ifdef CARES_FOUND
+ this->port = port;
+#endif
+
+ this->start_resolving(hostname, port);
+}
+
+#ifdef CARES_FOUND
+void Resolver::start_resolving(const std::string& hostname, const std::string&)
+{
+ this->resolving = true;
+ this->resolved = false;
+ this->resolved4 = false;
+ this->resolved6 = false;
+
+ this->error_msg.clear();
+ this->cares_addrinfo = nullptr;
+
+ auto hostname4_resolved = [](void* arg, int status, int,
+ struct hostent* hostent)
+ {
+ Resolver* resolver = static_cast<Resolver*>(arg);
+ resolver->on_hostname4_resolved(status, hostent);
+ };
+ auto hostname6_resolved = [](void* arg, int status, int,
+ struct hostent* hostent)
+ {
+ Resolver* resolver = static_cast<Resolver*>(arg);
+ resolver->on_hostname6_resolved(status, hostent);
+ };
+
+ DNSHandler::instance.gethostbyname(hostname, hostname6_resolved,
+ this, AF_INET6);
+ DNSHandler::instance.gethostbyname(hostname, hostname4_resolved,
+ this, AF_INET);
+}
+
+void Resolver::on_hostname4_resolved(int status, struct hostent* hostent)
+{
+ this->resolved4 = true;
+ if (status == ARES_SUCCESS)
+ this->fill_ares_addrinfo4(hostent);
+ else
+ this->error_msg = ::ares_strerror(status);
+
+ if (this->resolved4 && this->resolved6)
+ this->on_resolved();
+}
+
+void Resolver::on_hostname6_resolved(int status, struct hostent* hostent)
+{
+ this->resolved6 = true;
+ if (status == ARES_SUCCESS)
+ this->fill_ares_addrinfo6(hostent);
+ else
+ this->error_msg = ::ares_strerror(status);
+
+ if (this->resolved4 && this->resolved6)
+ this->on_resolved();
+}
+
+void Resolver::on_resolved()
+{
+ this->resolved = true;
+ this->resolving = false;
+ if (!this->cares_addrinfo)
+ {
+ if (this->error_cb)
+ this->error_cb(this->error_msg.data());
+ }
+ else
+ {
+ this->addr.reset(this->cares_addrinfo);
+ if (this->success_cb)
+ this->success_cb(this->addr.get());
+ }
+}
+
+void Resolver::fill_ares_addrinfo4(const struct hostent* hostent)
+{
+ struct addrinfo* prev = this->cares_addrinfo;
+ struct in_addr** address = reinterpret_cast<struct in_addr**>(hostent->h_addr_list);
+
+ while (*address)
+ {
+ // Create a new addrinfo list element, and fill it
+ struct addrinfo* current = new struct addrinfo;
+ current->ai_flags = 0;
+ current->ai_family = hostent->h_addrtype;
+ current->ai_socktype = SOCK_STREAM;
+ current->ai_protocol = 0;
+ current->ai_addrlen = sizeof(struct sockaddr_in);
+
+ struct sockaddr_in* addr = new struct sockaddr_in;
+ addr->sin_family = hostent->h_addrtype;
+ addr->sin_port = htons(strtoul(this->port.data(), nullptr, 10));
+ addr->sin_addr.s_addr = (*address)->s_addr;
+
+ current->ai_addr = reinterpret_cast<struct sockaddr*>(addr);
+ current->ai_next = nullptr;
+ current->ai_canonname = nullptr;
+
+ current->ai_next = prev;
+ this->cares_addrinfo = current;
+ prev = current;
+ ++address;
+ }
+}
+
+void Resolver::fill_ares_addrinfo6(const struct hostent* hostent)
+{
+ struct addrinfo* prev = this->cares_addrinfo;
+ struct in6_addr** address = reinterpret_cast<struct in6_addr**>(hostent->h_addr_list);
+
+ while (*address)
+ {
+ // Create a new addrinfo list element, and fill it
+ struct addrinfo* current = new struct addrinfo;
+ current->ai_flags = 0;
+ current->ai_family = hostent->h_addrtype;
+ current->ai_socktype = SOCK_STREAM;
+ current->ai_protocol = 0;
+ current->ai_addrlen = sizeof(struct sockaddr_in6);
+
+ struct sockaddr_in6* addr = new struct sockaddr_in6;
+ addr->sin6_family = hostent->h_addrtype;
+ addr->sin6_port = htons(strtoul(this->port.data(), nullptr, 10));
+ ::memcpy(addr->sin6_addr.s6_addr, (*address)->s6_addr, 16);
+ addr->sin6_flowinfo = 0;
+ addr->sin6_scope_id = 0;
+
+ current->ai_addr = reinterpret_cast<struct sockaddr*>(addr);
+ current->ai_canonname = nullptr;
+
+ current->ai_next = prev;
+ this->cares_addrinfo = current;
+ prev = current;
+ ++address;
+ }
+}
+
+#else // ifdef CARES_FOUND
+
+void Resolver::start_resolving(const std::string& hostname, const std::string& port)
+{
+ // If the resolution fails, the addr will be unset
+ this->addr.reset(nullptr);
+
+ struct addrinfo hints;
+ memset(&hints, 0, sizeof(struct addrinfo));
+ hints.ai_flags = 0;
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_protocol = 0;
+
+ struct addrinfo* addr_res = nullptr;
+ const int res = ::getaddrinfo(hostname.data(), port.data(),
+ &hints, &addr_res);
+
+ this->resolved = true;
+
+ if (res != 0)
+ {
+ this->error_msg = gai_strerror(res);
+ if (this->error_cb)
+ this->error_cb(this->error_msg.data());
+ }
+ else
+ {
+ this->addr.reset(addr_res);
+ if (this->success_cb)
+ this->success_cb(this->addr.get());
+ }
+}
+#endif // ifdef CARES_FOUND
+
+std::string addr_to_string(const struct addrinfo* rp)
+{
+ char buf[INET6_ADDRSTRLEN];
+ if (rp->ai_family == AF_INET)
+ return ::inet_ntop(rp->ai_family,
+ &reinterpret_cast<sockaddr_in*>(rp->ai_addr)->sin_addr,
+ buf, sizeof(buf));
+ else if (rp->ai_family == AF_INET6)
+ return ::inet_ntop(rp->ai_family,
+ &reinterpret_cast<sockaddr_in6*>(rp->ai_addr)->sin6_addr,
+ buf, sizeof(buf));
+ return {};
+}
diff --git a/louloulibs/network/resolver.hpp b/louloulibs/network/resolver.hpp
new file mode 100644
index 0000000..afe6e2b
--- /dev/null
+++ b/louloulibs/network/resolver.hpp
@@ -0,0 +1,128 @@
+#pragma once
+
+
+#include "louloulibs.h"
+
+#include <functional>
+#include <memory>
+#include <string>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+
+struct AddrinfoDeleter
+{
+ void operator()(struct addrinfo* addr)
+ {
+#ifdef CARES_FOUND
+ while (addr)
+ {
+ delete addr->ai_addr;
+ auto next = addr->ai_next;
+ delete addr;
+ addr = next;
+ }
+#else
+ freeaddrinfo(addr);
+#endif
+ }
+};
+
+class Resolver
+{
+public:
+ using ErrorCallbackType = std::function<void(const char*)>;
+ using SuccessCallbackType = std::function<void(const struct addrinfo*)>;
+
+ Resolver();
+ ~Resolver() = default;
+ Resolver(const Resolver&) = delete;
+ Resolver(Resolver&&) = delete;
+ Resolver& operator=(const Resolver&) = delete;
+ Resolver& operator=(Resolver&&) = delete;
+
+ bool is_resolving() const
+ {
+#ifdef CARES_FOUND
+ return this->resolving;
+#else
+ return false;
+#endif
+ }
+
+ bool is_resolved() const
+ {
+ return this->resolved;
+ }
+
+ const auto& get_result() const
+ {
+ return this->addr;
+ }
+ std::string get_error_message() const
+ {
+ return this->error_msg;
+ }
+
+ void clear()
+ {
+#ifdef CARES_FOUND
+ this->resolved6 = false;
+ this->resolved4 = false;
+ this->resolving = false;
+ this->cares_addrinfo = nullptr;
+ this->port.clear();
+#endif
+ this->resolved = false;
+ this->addr.reset();
+ this->error_msg.clear();
+ }
+
+ void resolve(const std::string& hostname, const std::string& port,
+ SuccessCallbackType success_cb, ErrorCallbackType error_cb);
+
+private:
+ void start_resolving(const std::string& hostname, const std::string& port);
+#ifdef CARES_FOUND
+ void on_hostname4_resolved(int status, struct hostent* hostent);
+ void on_hostname6_resolved(int status, struct hostent* hostent);
+
+ void fill_ares_addrinfo4(const struct hostent* hostent);
+ void fill_ares_addrinfo6(const struct hostent* hostent);
+
+ void on_resolved();
+
+ bool resolved4;
+ bool resolved6;
+
+ bool resolving;
+
+ /**
+ * When using c-ares to resolve the host asynchronously, we need the
+ * c-ares callbacks to fill a structure (a struct addrinfo, for
+ * compatibility with getaddrinfo and the rest of the code that works when
+ * c-ares is not used) with all returned values (for example an IPv6 and
+ * an IPv4). The pointer is given to the unique_ptr to manage its lifetime.
+ */
+ struct addrinfo* cares_addrinfo;
+ std::string port;
+
+#endif
+ /**
+ * Tells if we finished the resolution process. It doesn't indicate if it
+ * was successful (it is true even if the result is an error).
+ */
+ bool resolved;
+ std::string error_msg;
+
+
+ std::unique_ptr<struct addrinfo, AddrinfoDeleter> addr;
+
+ ErrorCallbackType error_cb;
+ SuccessCallbackType success_cb;
+};
+
+std::string addr_to_string(const struct addrinfo* rp);
+
+
diff --git a/louloulibs/network/socket_handler.hpp b/louloulibs/network/socket_handler.hpp
new file mode 100644
index 0000000..eeb41fe
--- /dev/null
+++ b/louloulibs/network/socket_handler.hpp
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <louloulibs.h>
+#include <memory>
+
+class Poller;
+
+using socket_t = int;
+
+class SocketHandler
+{
+public:
+ explicit SocketHandler(std::shared_ptr<Poller> poller, const socket_t socket):
+ poller(poller),
+ socket(socket)
+ {}
+ virtual ~SocketHandler() {}
+ SocketHandler(const SocketHandler&) = delete;
+ SocketHandler(SocketHandler&&) = delete;
+ SocketHandler& operator=(const SocketHandler&) = delete;
+ SocketHandler& operator=(SocketHandler&&) = delete;
+
+ virtual void on_recv() = 0;
+ virtual void on_send() = 0;
+ virtual void connect() = 0;
+ virtual bool is_connected() const = 0;
+
+ socket_t get_socket() const
+ { return this->socket; }
+
+protected:
+ /**
+ * A pointer to the poller that manages us, because we need to communicate
+ * with it.
+ */
+ std::shared_ptr<Poller> poller;
+ /**
+ * The handled socket.
+ */
+ socket_t socket;
+};
+
diff --git a/louloulibs/network/tcp_socket_handler.cpp b/louloulibs/network/tcp_socket_handler.cpp
new file mode 100644
index 0000000..5420b1c
--- /dev/null
+++ b/louloulibs/network/tcp_socket_handler.cpp
@@ -0,0 +1,501 @@
+#include <network/tcp_socket_handler.hpp>
+#include <network/dns_handler.hpp>
+
+#include <utils/timed_events.hpp>
+#include <utils/scopeguard.hpp>
+#include <network/poller.hpp>
+
+#include <logger/logger.hpp>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <stdexcept>
+#include <unistd.h>
+#include <errno.h>
+#include <cstring>
+#include <fcntl.h>
+
+#ifdef BOTAN_FOUND
+# include <botan/hex.h>
+# include <botan/tls_exceptn.h>
+
+Botan::AutoSeeded_RNG TCPSocketHandler::rng;
+Botan::TLS::Policy TCPSocketHandler::policy;
+Botan::TLS::Session_Manager_In_Memory TCPSocketHandler::session_manager(TCPSocketHandler::rng);
+
+#endif
+
+#ifndef UIO_FASTIOV
+# define UIO_FASTIOV 8
+#endif
+
+using namespace std::string_literals;
+using namespace std::chrono_literals;
+
+namespace ph = std::placeholders;
+
+TCPSocketHandler::TCPSocketHandler(std::shared_ptr<Poller> poller):
+ SocketHandler(poller, -1),
+ use_tls(false),
+ connected(false),
+ connecting(false),
+ hostname_resolution_failed(false)
+#ifdef BOTAN_FOUND
+ ,credential_manager(this)
+#endif
+{}
+
+TCPSocketHandler::~TCPSocketHandler()
+{
+ this->close();
+}
+
+
+void TCPSocketHandler::init_socket(const struct addrinfo* rp)
+{
+ if (this->socket != -1)
+ ::close(this->socket);
+ if ((this->socket = ::socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)) == -1)
+ throw std::runtime_error("Could not create socket: "s + strerror(errno));
+ // Bind the socket to a specific address, if specified
+ if (!this->bind_addr.empty())
+ {
+ // Convert the address from string format to a sockaddr that can be
+ // used in bind()
+ struct addrinfo* result;
+ int err = ::getaddrinfo(this->bind_addr.data(), nullptr, nullptr, &result);
+ if (err != 0 || !result)
+ log_error("Failed to bind socket to ", this->bind_addr, ": ",
+ gai_strerror(err));
+ else
+ {
+ utils::ScopeGuard sg([result](){ freeaddrinfo(result); });
+ struct addrinfo* rp;
+ int bind_error = 0;
+ for (rp = result; rp; rp = rp->ai_next)
+ {
+ if ((bind_error = ::bind(this->socket,
+ reinterpret_cast<const struct sockaddr*>(rp->ai_addr),
+ rp->ai_addrlen)) == 0)
+ break;
+ }
+ if (!rp)
+ log_error("Failed to bind socket to ", this->bind_addr, ": ",
+ strerror(bind_error));
+ else
+ log_info("Socket successfully bound to ", this->bind_addr);
+ }
+ }
+ int optval = 1;
+ if (::setsockopt(this->socket, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval)) == -1)
+ log_warning("Failed to enable TCP keepalive on socket: ", strerror(errno));
+ // Set the socket on non-blocking mode. This is useful to receive a EAGAIN
+ // error when connect() would block, to not block the whole process if a
+ // remote is not responsive.
+ const int existing_flags = ::fcntl(this->socket, F_GETFL, 0);
+ if ((existing_flags == -1) ||
+ (::fcntl(this->socket, F_SETFL, existing_flags | O_NONBLOCK) == -1))
+ throw std::runtime_error("Could not initialize socket: "s + strerror(errno));
+}
+
+void TCPSocketHandler::connect(const std::string& address, const std::string& port, const bool tls)
+{
+ this->address = address;
+ this->port = port;
+ this->use_tls = tls;
+
+ utils::ScopeGuard sg;
+
+ struct addrinfo* addr_res;
+
+ if (!this->connecting)
+ {
+ // Get the addrinfo from getaddrinfo (or ares_gethostbyname), only if
+ // this is the first call of this function.
+ if (!this->resolver.is_resolved())
+ {
+ log_info("Trying to connect to ", address, ":", port);
+ // Start the asynchronous process of resolving the hostname. Once
+ // the addresses have been found and `resolved` has been set to true
+ // (but connecting will still be false), TCPSocketHandler::connect()
+ // needs to be called, again.
+ this->resolver.resolve(address, port,
+ [this](const struct addrinfo*)
+ {
+ log_debug("Resolution success, calling connect() again");
+ this->connect();
+ },
+ [this](const char*)
+ {
+ log_debug("Resolution failed, calling connect() again");
+ this->connect();
+ });
+ return;
+ }
+ else
+ {
+ // The c-ares resolved the hostname and the available addresses
+ // where saved in the cares_addrinfo linked list. Now, just use
+ // this list to try to connect.
+ addr_res = this->resolver.get_result().get();
+ if (!addr_res)
+ {
+ this->hostname_resolution_failed = true;
+ const auto msg = this->resolver.get_error_message();
+ this->close();
+ this->on_connection_failed(msg);
+ return ;
+ }
+ }
+ }
+ else
+ { // This function is called again, use the saved addrinfo structure,
+ // instead of re-doing the whole getaddrinfo process.
+ addr_res = &this->addrinfo;
+ }
+
+ for (struct addrinfo* rp = addr_res; rp; rp = rp->ai_next)
+ {
+ if (!this->connecting)
+ {
+ try {
+ this->init_socket(rp);
+ }
+ catch (const std::runtime_error& error) {
+ log_error("Failed to init socket: ", error.what());
+ break;
+ }
+ }
+
+ this->display_resolved_ip(rp);
+
+ if (::connect(this->socket, rp->ai_addr, rp->ai_addrlen) == 0
+ || errno == EISCONN)
+ {
+ log_info("Connection success.");
+ TimedEventsManager::instance().cancel("connection_timeout"s +
+ std::to_string(this->socket));
+ this->poller->add_socket_handler(this);
+ this->connected = true;
+ this->connecting = false;
+#ifdef BOTAN_FOUND
+ if (this->use_tls)
+ this->start_tls();
+#endif
+ this->on_connected();
+ return ;
+ }
+ else if (errno == EINPROGRESS || errno == EALREADY)
+ { // retry this process later, when the socket
+ // is ready to be written on.
+ this->connecting = true;
+ this->poller->add_socket_handler(this);
+ this->poller->watch_send_events(this);
+ // Save the addrinfo structure, to use it on the next call
+ this->ai_addrlen = rp->ai_addrlen;
+ memcpy(&this->ai_addr, rp->ai_addr, this->ai_addrlen);
+ memcpy(&this->addrinfo, rp, sizeof(struct addrinfo));
+ this->addrinfo.ai_addr = reinterpret_cast<struct sockaddr*>(&this->ai_addr);
+ this->addrinfo.ai_next = nullptr;
+ // If the connection has not succeeded or failed in 5s, we consider
+ // it to have failed
+ TimedEventsManager::instance().add_event(
+ TimedEvent(std::chrono::steady_clock::now() + 5s,
+ std::bind(&TCPSocketHandler::on_connection_timeout, this),
+ "connection_timeout"s + std::to_string(this->socket)));
+ return ;
+ }
+ log_info("Connection failed:", strerror(errno));
+ }
+ log_error("All connection attempts failed.");
+ this->close();
+ this->on_connection_failed(strerror(errno));
+ return ;
+}
+
+void TCPSocketHandler::on_connection_timeout()
+{
+ this->close();
+ this->on_connection_failed("connection timed out");
+}
+
+void TCPSocketHandler::connect()
+{
+ this->connect(this->address, this->port, this->use_tls);
+}
+
+void TCPSocketHandler::on_recv()
+{
+#ifdef BOTAN_FOUND
+ if (this->use_tls)
+ this->tls_recv();
+ else
+#endif
+ this->plain_recv();
+}
+
+void TCPSocketHandler::plain_recv()
+{
+ static constexpr size_t buf_size = 4096;
+ char buf[buf_size];
+ void* recv_buf = this->get_receive_buffer(buf_size);
+
+ if (recv_buf == nullptr)
+ recv_buf = buf;
+
+ const ssize_t size = this->do_recv(recv_buf, buf_size);
+
+ if (size > 0)
+ {
+ if (buf == recv_buf)
+ {
+ // data needs to be placed in the in_buf string, because no buffer
+ // was provided to receive that data directly. The in_buf buffer
+ // will be handled in parse_in_buffer()
+ this->in_buf += std::string(buf, size);
+ }
+ this->parse_in_buffer(size);
+ }
+}
+
+ssize_t TCPSocketHandler::do_recv(void* recv_buf, const size_t buf_size)
+{
+ ssize_t size = ::recv(this->socket, recv_buf, buf_size, 0);
+ if (0 == size)
+ {
+ this->on_connection_close("");
+ this->close();
+ }
+ else if (-1 == size)
+ {
+ if (this->connecting)
+ log_warning("Error connecting: ", strerror(errno));
+ else
+ log_warning("Error while reading from socket: ", strerror(errno));
+ // Remember if we were connecting, or already connected when this
+ // happened, because close() sets this->connecting to false
+ const auto were_connecting = this->connecting;
+ this->close();
+ if (were_connecting)
+ this->on_connection_failed(strerror(errno));
+ else
+ this->on_connection_close(strerror(errno));
+ }
+ return size;
+}
+
+void TCPSocketHandler::on_send()
+{
+ struct iovec msg_iov[UIO_FASTIOV] = {};
+ struct msghdr msg{nullptr, 0,
+ msg_iov,
+ 0, nullptr, 0, 0};
+ for (const std::string& s: this->out_buf)
+ {
+ // unconsting the content of s is ok, sendmsg will never modify it
+ msg_iov[msg.msg_iovlen].iov_base = const_cast<char*>(s.data());
+ msg_iov[msg.msg_iovlen].iov_len = s.size();
+ if (++msg.msg_iovlen == UIO_FASTIOV)
+ break;
+ }
+ ssize_t res = ::sendmsg(this->socket, &msg, MSG_NOSIGNAL);
+ if (res < 0)
+ {
+ log_error("sendmsg failed: ", strerror(errno));
+ this->on_connection_close(strerror(errno));
+ this->close();
+ }
+ else
+ {
+ // remove all the strings that were successfully sent.
+ for (auto it = this->out_buf.begin();
+ it != this->out_buf.end();)
+ {
+ if (static_cast<size_t>(res) >= (*it).size())
+ {
+ res -= (*it).size();
+ it = this->out_buf.erase(it);
+ }
+ else
+ {
+ // If one string has partially been sent, we use substr to
+ // crop it
+ if (res > 0)
+ (*it) = (*it).substr(res, std::string::npos);
+ break;
+ }
+ }
+ if (this->out_buf.empty())
+ this->poller->stop_watching_send_events(this);
+ }
+}
+
+void TCPSocketHandler::close()
+{
+ TimedEventsManager::instance().cancel("connection_timeout"s +
+ std::to_string(this->socket));
+ if (this->connected || this->connecting)
+ this->poller->remove_socket_handler(this->get_socket());
+ if (this->socket != -1)
+ {
+ ::close(this->socket);
+ this->socket = -1;
+ }
+ this->connected = false;
+ this->connecting = false;
+ this->in_buf.clear();
+ this->out_buf.clear();
+ this->port.clear();
+ this->resolver.clear();
+}
+
+void TCPSocketHandler::display_resolved_ip(struct addrinfo* rp) const
+{
+ if (rp->ai_family == AF_INET)
+ log_debug("Trying IPv4 address ", addr_to_string(rp));
+ else if (rp->ai_family == AF_INET6)
+ log_debug("Trying IPv6 address ", addr_to_string(rp));
+}
+
+void TCPSocketHandler::send_data(std::string&& data)
+{
+#ifdef BOTAN_FOUND
+ if (this->use_tls)
+ try {
+ this->tls_send(std::move(data));
+ } catch (const Botan::TLS::TLS_Exception& e) {
+ this->on_connection_close("TLS error: "s + e.what());
+ this->close();
+ return ;
+ }
+ else
+#endif
+ this->raw_send(std::move(data));
+}
+
+void TCPSocketHandler::raw_send(std::string&& data)
+{
+ if (data.empty())
+ return ;
+ this->out_buf.emplace_back(std::move(data));
+ if (this->connected)
+ this->poller->watch_send_events(this);
+}
+
+void TCPSocketHandler::send_pending_data()
+{
+ if (this->connected && !this->out_buf.empty())
+ this->poller->watch_send_events(this);
+}
+
+bool TCPSocketHandler::is_connected() const
+{
+ return this->connected;
+}
+
+bool TCPSocketHandler::is_connecting() const
+{
+ return this->connecting || this->resolver.is_resolving();
+}
+
+void* TCPSocketHandler::get_receive_buffer(const size_t) const
+{
+ return nullptr;
+}
+
+#ifdef BOTAN_FOUND
+void TCPSocketHandler::start_tls()
+{
+ Botan::TLS::Server_Information server_info(this->address, "irc", std::stoul(this->port));
+ this->tls = std::make_unique<Botan::TLS::Client>(
+ std::bind(&TCPSocketHandler::tls_output_fn, this, ph::_1, ph::_2),
+ std::bind(&TCPSocketHandler::tls_data_cb, this, ph::_1, ph::_2),
+ std::bind(&TCPSocketHandler::tls_alert_cb, this, ph::_1, ph::_2, ph::_3),
+ std::bind(&TCPSocketHandler::tls_handshake_cb, this, ph::_1),
+ session_manager, this->credential_manager, policy,
+ rng, server_info, Botan::TLS::Protocol_Version::latest_tls_version());
+}
+
+void TCPSocketHandler::tls_recv()
+{
+ static constexpr size_t buf_size = 4096;
+ char recv_buf[buf_size];
+
+ const ssize_t size = this->do_recv(recv_buf, buf_size);
+ if (size > 0)
+ {
+ const bool was_active = this->tls->is_active();
+ try {
+ this->tls->received_data(reinterpret_cast<const Botan::byte*>(recv_buf),
+ static_cast<size_t>(size));
+ } catch (const Botan::TLS::TLS_Exception& e) {
+ // May happen if the server sends malformed TLS data (buggy server,
+ // or more probably we are just connected to a server that sends
+ // plain-text)
+ this->on_connection_close("TLS error: "s + e.what());
+ this->close();
+ return ;
+ }
+ if (!was_active && this->tls->is_active())
+ this->on_tls_activated();
+ }
+}
+
+void TCPSocketHandler::tls_send(std::string&& data)
+{
+ // We may not be connected yet, or the tls session has
+ // not yet been negociated
+ if (this->tls && this->tls->is_active())
+ {
+ const bool was_active = this->tls->is_active();
+ if (!this->pre_buf.empty())
+ {
+ this->tls->send(reinterpret_cast<const Botan::byte*>(this->pre_buf.data()),
+ this->pre_buf.size());
+ this->pre_buf = "";
+ }
+ if (!data.empty())
+ this->tls->send(reinterpret_cast<const Botan::byte*>(data.data()),
+ data.size());
+ if (!was_active && this->tls->is_active())
+ this->on_tls_activated();
+ }
+ else
+ this->pre_buf += data;
+}
+
+void TCPSocketHandler::tls_data_cb(const Botan::byte* data, size_t size)
+{
+ this->in_buf += std::string(reinterpret_cast<const char*>(data),
+ size);
+ if (!this->in_buf.empty())
+ this->parse_in_buffer(size);
+}
+
+void TCPSocketHandler::tls_output_fn(const Botan::byte* data, size_t size)
+{
+ this->raw_send(std::string(reinterpret_cast<const char*>(data), size));
+}
+
+void TCPSocketHandler::tls_alert_cb(Botan::TLS::Alert alert, const Botan::byte*, size_t)
+{
+ log_debug("tls_alert: ", alert.type_string());
+}
+
+bool TCPSocketHandler::tls_handshake_cb(const Botan::TLS::Session& 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;
+}
+
+void TCPSocketHandler::on_tls_activated()
+{
+ this->send_data({});
+}
+
+#endif // BOTAN_FOUND
diff --git a/louloulibs/network/tcp_socket_handler.hpp b/louloulibs/network/tcp_socket_handler.hpp
new file mode 100644
index 0000000..b0ba493
--- /dev/null
+++ b/louloulibs/network/tcp_socket_handler.hpp
@@ -0,0 +1,274 @@
+#pragma once
+
+
+#include "louloulibs.h"
+
+#include <network/socket_handler.hpp>
+#include <network/resolver.hpp>
+
+#include <network/credentials_manager.hpp>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <netdb.h>
+
+#include <vector>
+#include <memory>
+#include <string>
+#include <list>
+
+/**
+ * An interface, with a series of callbacks that should be implemented in
+ * subclasses that deal with a socket. These callbacks are called on various events
+ * (read/write/timeout, etc) when they are notified to a poller
+ * (select/poll/epoll etc)
+ */
+class TCPSocketHandler: public SocketHandler
+{
+protected:
+ ~TCPSocketHandler();
+public:
+ explicit TCPSocketHandler(std::shared_ptr<Poller> poller);
+ TCPSocketHandler(const TCPSocketHandler&) = delete;
+ TCPSocketHandler(TCPSocketHandler&&) = delete;
+ TCPSocketHandler& operator=(const TCPSocketHandler&) = delete;
+ TCPSocketHandler& operator=(TCPSocketHandler&&) = delete;
+
+ /**
+ * Connect to the remote server, and call on_connected() if this
+ * succeeds. If tls is true, we set use_tls to true and will also call
+ * start_tls() when the connection succeeds.
+ */
+ void connect(const std::string& address, const std::string& port, const bool tls);
+ void connect() override final;
+ /**
+ * Reads raw data from the socket. And pass it to parse_in_buffer()
+ * If we are using TLS on this connection, we call tls_recv()
+ */
+ void on_recv() override final;
+ /**
+ * Write as much data from out_buf as possible, in the socket.
+ */
+ void on_send() override final;
+ /**
+ * Add the given data to out_buf and tell our poller that we want to be
+ * notified when a send event is ready.
+ *
+ * This can be overriden if we want to modify the data before sending
+ * it. For example if we want to encrypt it.
+ */
+ void send_data(std::string&& data);
+ /**
+ * Watch the socket for send events, if our out buffer is not empty.
+ */
+ void send_pending_data();
+ /**
+ * Close the connection, remove us from the poller
+ */
+ void close();
+ /**
+ * Called by a TimedEvent, when the connection did not succeed or fail
+ * after a given time.
+ */
+ void on_connection_timeout();
+ /**
+ * Called when the connection is successful.
+ */
+ virtual void on_connected() = 0;
+ /**
+ * Called when the connection fails. Not when it is closed later, just at
+ * the connect() call.
+ */
+ virtual void on_connection_failed(const std::string& reason) = 0;
+ /**
+ * Called when we detect a disconnection from the remote host.
+ */
+ virtual void on_connection_close(const std::string& error) = 0;
+ /**
+ * Handle/consume (some of) the data received so far. The data to handle
+ * may be in the in_buf buffer, or somewhere else, depending on what
+ * get_receive_buffer() returned. If some data is used from in_buf, it
+ * should be truncated, only the unused data should be left untouched.
+ *
+ * The size argument is the size of the last chunk of data that was added to the buffer.
+ */
+ virtual void parse_in_buffer(const size_t size) = 0;
+#ifdef BOTAN_FOUND
+ /**
+ * Tell whether the credential manager should cancel the connection when the
+ * certificate is invalid.
+ */
+ virtual bool abort_on_invalid_cert() const
+ {
+ return true;
+ }
+#endif
+ bool is_connected() const override final;
+ bool is_connecting() const;
+
+private:
+ /**
+ * Initialize the socket with the parameters contained in the given
+ * addrinfo structure.
+ */
+ void init_socket(const struct addrinfo* rp);
+ /**
+ * Reads from the socket into the provided buffer. If an error occurs
+ * (read returns <= 0), the handling of the error is done here (close the
+ * connection, log a message, etc).
+ *
+ * Returns the value returned by ::recv(), so the buffer should not be
+ * used if it’s not positive.
+ */
+ ssize_t do_recv(void* recv_buf, const size_t buf_size);
+ /**
+ * Reads data from the socket and calls parse_in_buffer with it.
+ */
+ void plain_recv();
+ /**
+ * Mark the given data as ready to be sent, as-is, on the socket, as soon
+ * as we can.
+ */
+ void raw_send(std::string&& data);
+
+#ifdef BOTAN_FOUND
+ /**
+ * Create the TLS::Client object, with all the callbacks etc. This must be
+ * called only when we know we are able to send TLS-encrypted data over
+ * the socket.
+ */
+ void start_tls();
+ /**
+ * An additional step to pass the data into our tls object to decrypt it
+ * before passing it to parse_in_buffer.
+ */
+ void tls_recv();
+ /**
+ * Pass the data to the tls object in order to encrypt it. The tls object
+ * will then call raw_send as a callback whenever data as been encrypted
+ * and can be sent on the socket.
+ */
+ void tls_send(std::string&& data);
+ /**
+ * Called by the tls object that some data has been decrypt. We call
+ * parse_in_buffer() to handle that unencrypted data.
+ */
+ void tls_data_cb(const Botan::byte* data, size_t size);
+ /**
+ * 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_output_fn(const Botan::byte* data, size_t size);
+ /**
+ * 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.
+ */
+ void tls_alert_cb(Botan::TLS::Alert alert, const Botan::byte*, size_t);
+ /**
+ * 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_handshake_cb(const Botan::TLS::Session& session);
+ /**
+ * Called whenever the tls session goes from inactive to active. This
+ * means that the handshake has just been successfully done, and we can
+ * now proceed to send any available data into our tls object.
+ */
+ void on_tls_activated();
+#endif // BOTAN_FOUND
+ /**
+ * Where data is added, when we want to send something to the client.
+ */
+ std::vector<std::string> out_buf;
+ /**
+ * DNS resolver
+ */
+ Resolver resolver;
+ /**
+ * Keep the details of the addrinfo returned by the resolver that
+ * triggered a EINPROGRESS error when connect()ing to it, to reuse it
+ * directly when connect() is called again.
+ */
+ struct addrinfo addrinfo;
+ struct sockaddr_in6 ai_addr;
+ socklen_t ai_addrlen;
+
+protected:
+ /**
+ * Where data read from the socket is added until we can extract a full
+ * and meaningful “message” from it.
+ *
+ * TODO: something more efficient than a string.
+ */
+ std::string in_buf;
+ /**
+ * Whether we are using TLS on this connection or not.
+ */
+ bool use_tls;
+ /**
+ * Provide a buffer in which data can be directly received. This can be
+ * used to avoid copying data into in_buf before using it. If no buffer
+ * needs to be provided, nullptr is returned (the default implementation
+ * does that), in that case our internal in_buf will be used to save the
+ * data until it can be used by parse_in_buffer().
+ */
+ virtual void* get_receive_buffer(const size_t size) const;
+ /**
+ * Hostname we are connected/connecting to
+ */
+ std::string address;
+ /**
+ * Port we are connected/connecting to
+ */
+ std::string port;
+
+ bool connected;
+ bool connecting;
+
+ bool hostname_resolution_failed;
+
+ /**
+ * Address to bind the socket to, before calling connect().
+ * If empty, it’s equivalent to binding to INADDR_ANY.
+ */
+ std::string bind_addr;
+
+private:
+ /**
+ * Display the resolved IP, just for information purpose.
+ */
+ void display_resolved_ip(struct addrinfo* rp) const;
+
+#ifdef BOTAN_FOUND
+ /**
+ * Botan stuff to manipulate a TLS session.
+ */
+ static Botan::AutoSeeded_RNG rng;
+ static Botan::TLS::Policy policy;
+ static Botan::TLS::Session_Manager_In_Memory session_manager;
+protected:
+ BasicCredentialsManager credential_manager;
+private:
+ /**
+ * 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
+ * calls the output_fn callback with it as soon as it is created.
+ * Therefore, we do not want to create it if we do not intend to send any
+ * TLS-encrypted message. We create the object only when needed (for
+ * example after we have negociated a TLS session using a STARTTLS
+ * message, or stuf like that).
+ *
+ * See start_tls for the method where this object is created.
+ */
+ std::unique_ptr<Botan::TLS::Client> tls;
+ /**
+ * An additional buffer to keep data that the user wants to send, but
+ * cannot because the handshake is not done.
+ */
+ std::string pre_buf;
+#endif // BOTAN_FOUND
+};
+
+
+
diff --git a/louloulibs/utils/encoding.cpp b/louloulibs/utils/encoding.cpp
new file mode 100644
index 0000000..507f38a
--- /dev/null
+++ b/louloulibs/utils/encoding.cpp
@@ -0,0 +1,258 @@
+#include <utils/encoding.hpp>
+
+#include <utils/scopeguard.hpp>
+
+#include <stdexcept>
+
+#include <assert.h>
+#include <string.h>
+#include <iconv.h>
+
+#include <map>
+#include <bitset>
+
+/**
+ * The UTF-8-encoded character used as a place holder when a character conversion fails.
+ * This is U+FFFD � "replacement character"
+ */
+static const char* invalid_char = "\xef\xbf\xbd";
+static const size_t invalid_char_len = 3;
+
+namespace utils
+{
+ /**
+ * Based on http://en.wikipedia.org/wiki/UTF-8#Description
+ */
+ std::size_t get_next_codepoint_size(const unsigned char c)
+ {
+ if ((c & 0b11111000) == 0b11110000) // 4 bytes: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+ return 4;
+ else if ((c & 0b11110000) == 0b11100000) // 3 bytes: 1110xxx 10xxxxxx 10xxxxxx
+ return 3;
+ else if ((c & 0b11100000) == 0b11000000) // 2 bytes: 110xxxxx 10xxxxxx
+ return 2;
+ return 1; // 1 byte: 0xxxxxxx
+ }
+
+ bool is_valid_utf8(const char* s)
+ {
+ if (!s)
+ return false;
+
+ const unsigned char* str = reinterpret_cast<const unsigned char*>(s);
+
+ while (*str)
+ {
+ const auto codepoint_size = get_next_codepoint_size(str[0]);
+ if (codepoint_size == 4)
+ {
+ if (!str[1] || !str[2] || !str[3]
+ || ((str[1] & 0b11000000) != 0b10000000)
+ || ((str[2] & 0b11000000) != 0b10000000)
+ || ((str[3] & 0b11000000) != 0b10000000))
+ return false;
+ }
+ else if (codepoint_size == 3)
+ {
+ if (!str[1] || !str[2]
+ || ((str[1] & 0b11000000) != 0b10000000)
+ || ((str[2] & 0b11000000) != 0b10000000))
+ return false;
+ }
+ else if (codepoint_size == 2)
+ {
+ if (!str[1] ||
+ ((str[1] & 0b11000000) != 0b10000000))
+ return false;
+ }
+ else if ((str[0] & 0b10000000) != 0)
+ return false;
+ str += codepoint_size;
+ }
+ return true;
+ }
+
+ std::string remove_invalid_xml_chars(const std::string& original)
+ {
+ // The given string MUST be a valid utf-8 string
+ unsigned char* res = new unsigned char[original.size()];
+ ScopeGuard sg([&res]() { delete[] res;});
+
+ // pointer where we write valid chars
+ unsigned char* r = res;
+
+ const unsigned char* str = reinterpret_cast<const unsigned char*>(original.c_str());
+ std::bitset<20> codepoint;
+
+ while (*str)
+ {
+ // 4 bytes: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+ if ((str[0] & 0b11111000) == 0b11110000)
+ {
+ codepoint = ((str[0] & 0b00000111) << 18);
+ codepoint |= ((str[1] & 0b00111111) << 12);
+ codepoint |= ((str[2] & 0b00111111) << 6 );
+ codepoint |= ((str[3] & 0b00111111) << 0 );
+ if (codepoint.to_ulong() <= 0x10FFFF)
+ {
+ ::memcpy(r, str, 4);
+ r += 4;
+ }
+ str += 4;
+ }
+ // 3 bytes: 1110xxx 10xxxxxx 10xxxxxx
+ else if ((str[0] & 0b11110000) == 0b11100000)
+ {
+ codepoint = ((str[0] & 0b00001111) << 12);
+ codepoint |= ((str[1] & 0b00111111) << 6);
+ codepoint |= ((str[2] & 0b00111111) << 0 );
+ if (codepoint.to_ulong() <= 0xD7FF ||
+ (codepoint.to_ulong() >= 0xE000 && codepoint.to_ulong() <= 0xFFFD))
+ {
+ ::memcpy(r, str, 3);
+ r += 3;
+ }
+ str += 3;
+ }
+ // 2 bytes: 110xxxxx 10xxxxxx
+ else if (((str[0]) & 0b11100000) == 0b11000000)
+ {
+ // All 2 bytes char are valid, don't even bother calculating
+ // the codepoint
+ ::memcpy(r, str, 2);
+ r += 2;
+ str += 2;
+ }
+ // 1 byte: 0xxxxxxx
+ else if ((str[0] & 0b10000000) == 0)
+ {
+ codepoint = ((str[0] & 0b01111111));
+ if (codepoint.to_ulong() == 0x09 ||
+ codepoint.to_ulong() == 0x0A ||
+ codepoint.to_ulong() == 0x0D ||
+ codepoint.to_ulong() >= 0x20)
+ {
+ ::memcpy(r, str, 1);
+ r += 1;
+ }
+ str += 1;
+ }
+ else
+ throw std::runtime_error("Invalid UTF-8 passed to remove_invalid_xml_chars");
+ }
+ return std::string(reinterpret_cast<char*>(res), r-res);
+ }
+
+ std::string convert_to_utf8(const std::string& str, const char* charset)
+ {
+ std::string res;
+
+ const iconv_t cd = iconv_open("UTF-8", charset);
+ if (cd == (iconv_t)-1)
+ throw std::runtime_error("Cannot convert into UTF-8");
+
+ // Make sure cd is always closed when we leave this function
+ ScopeGuard sg([&]{ iconv_close(cd); });
+
+ size_t inbytesleft = str.size();
+
+ // iconv will not attempt to modify this buffer, but some plateform
+ // require a char** anyway
+#ifdef ICONV_SECOND_ARGUMENT_IS_CONST
+ const char* inbuf_ptr = str.c_str();
+#else
+ char* inbuf_ptr = const_cast<char*>(str.c_str());
+#endif
+
+ size_t outbytesleft = str.size() * 4;
+ char* outbuf = new char[outbytesleft];
+ char* outbuf_ptr = outbuf;
+
+ // Make sure outbuf is always deleted when we leave this function
+ sg.add_callback([&]{ delete[] outbuf; });
+
+ bool done = false;
+ while (done == false)
+ {
+ size_t error = iconv(cd, &inbuf_ptr, &inbytesleft, &outbuf_ptr, &outbytesleft);
+ if ((size_t)-1 == error)
+ {
+ switch (errno)
+ {
+ case EILSEQ:
+ // Invalid byte found. Insert a placeholder instead of the
+ // converted character, jump one byte and continue
+ memcpy(outbuf_ptr, invalid_char, invalid_char_len);
+ outbuf_ptr += invalid_char_len;
+ inbytesleft--;
+ inbuf_ptr++;
+ break;
+ case EINVAL:
+ // A multibyte sequence is not terminated, but we can't
+ // provide any more data, so we just add a placeholder to
+ // indicate that the character is not properly converted,
+ // and we stop the conversion
+ memcpy(outbuf_ptr, invalid_char, invalid_char_len);
+ outbuf_ptr += invalid_char_len;
+ outbuf_ptr++;
+ done = true;
+ break;
+ case E2BIG:
+ // This should never happen
+ done = true;
+ break;
+ default:
+ // This should happen even neverer
+ done = true;
+ break;
+ }
+ }
+ else
+ {
+ // The conversion finished without any error, stop converting
+ done = true;
+ }
+ }
+ // Terminate the converted buffer, and copy that buffer it into the
+ // string we return
+ *outbuf_ptr = '\0';
+ res = outbuf;
+ return res;
+ }
+
+}
+
+namespace xep0106
+{
+ static const std::map<const char, const std::string> encode_map = {
+ {' ', "\\20"},
+ {'"', "\\22"},
+ {'&', "\\26"},
+ {'\'',"\\27"},
+ {'/', "\\2f"},
+ {':', "\\3a"},
+ {'<', "\\3c"},
+ {'>', "\\3e"},
+ {'@', "\\40"},
+ };
+
+ void decode(std::string& s)
+ {
+ std::string::size_type pos;
+ for (const auto& pair: encode_map)
+ while ((pos = s.find(pair.second)) != std::string::npos)
+ s.replace(pos, pair.second.size(),
+ 1, pair.first);
+ }
+
+ void encode(std::string& s)
+ {
+ std::string::size_type pos;
+ while ((pos = s.find_first_of(" \"&'/:<>@")) != std::string::npos)
+ {
+ auto it = encode_map.find(s[pos]);
+ assert(it != encode_map.end());
+ s.replace(pos, 1, it->second);
+ }
+ }
+}
diff --git a/louloulibs/utils/encoding.hpp b/louloulibs/utils/encoding.hpp
new file mode 100644
index 0000000..586edd8
--- /dev/null
+++ b/louloulibs/utils/encoding.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+
+#include <string>
+
+namespace utils
+{
+ /**
+ * Return the size, in bytes, of the next UTF-8 codepoint, based on
+ * the given char.
+ */
+ std::size_t get_next_codepoint_size(const unsigned char c);
+ /**
+ * Returns true if the given null-terminated string is valid utf-8.
+ *
+ * Based on http://en.wikipedia.org/wiki/UTF-8#Description
+ */
+ bool is_valid_utf8(const char* s);
+ /**
+ * Remove all invalid codepoints from the given utf-8-encoded string.
+ * The value returned is a copy of the string, without the removed chars.
+ *
+ * See http://www.w3.org/TR/xml/#charsets for the list of valid characters
+ * in XML.
+ */
+ std::string remove_invalid_xml_chars(const std::string& original);
+ /**
+ * Convert the given string (encoded is "encoding") into valid utf-8.
+ * If some decoding fails, insert an utf-8 placeholder character instead.
+ */
+ std::string convert_to_utf8(const std::string& str, const char* encoding);
+}
+
+namespace xep0106
+{
+ /**
+ * Decode and encode inplace.
+ */
+ void decode(std::string&);
+ void encode(std::string&);
+}
+
+
diff --git a/louloulibs/utils/revstr.cpp b/louloulibs/utils/revstr.cpp
new file mode 100644
index 0000000..87fd801
--- /dev/null
+++ b/louloulibs/utils/revstr.cpp
@@ -0,0 +1,9 @@
+#include <utils/revstr.hpp>
+
+namespace utils
+{
+ std::string revstr(const std::string& original)
+ {
+ return {original.rbegin(), original.rend()};
+ }
+}
diff --git a/louloulibs/utils/revstr.hpp b/louloulibs/utils/revstr.hpp
new file mode 100644
index 0000000..8e521ea
--- /dev/null
+++ b/louloulibs/utils/revstr.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+
+#include <string>
+
+namespace utils
+{
+ std::string revstr(const std::string& original);
+}
+
+
diff --git a/louloulibs/utils/scopeguard.hpp b/louloulibs/utils/scopeguard.hpp
new file mode 100644
index 0000000..ee1e2ef
--- /dev/null
+++ b/louloulibs/utils/scopeguard.hpp
@@ -0,0 +1,89 @@
+#pragma once
+
+#include <functional>
+#include <vector>
+
+/**
+ * A class to be used to make sure some functions are called when the scope
+ * is left, because they will be called in the ScopeGuard's destructor. It
+ * can for example be used to delete some pointer whenever any exception is
+ * called. Example:
+
+ * {
+ * ScopeGuard scope;
+ * int* number = new int(2);
+ * scope.add_callback([number]() { delete number; });
+ * // Do some other stuff with the number. But these stuff might throw an exception:
+ * throw std::runtime_error("Some error not caught here, but in our caller");
+ * return true;
+ * }
+
+ * In this example, our pointer will always be deleted, even when the
+ * exception is thrown. If we want the functions to be called only when the
+ * scope is left because of an unexpected exception, we can use
+ * ScopeGuard::disable();
+ */
+
+namespace utils
+{
+
+class ScopeGuard
+{
+public:
+ /**
+ * The constructor can take a callback. But additional callbacks can be
+ * added later with add_callback()
+ */
+ explicit ScopeGuard(std::function<void()>&& func):
+ enabled(true)
+ {
+ this->add_callback(std::move(func));
+ }
+
+ ScopeGuard(const ScopeGuard&) = delete;
+ ScopeGuard& operator=(ScopeGuard&&) = delete;
+ ScopeGuard(ScopeGuard&&) = delete;
+ ScopeGuard& operator=(const ScopeGuard&) = delete;
+
+ /**
+ * default constructor, the scope guard is enabled but empty, use
+ * add_callback()
+ */
+ explicit ScopeGuard():
+ enabled(true)
+ {
+ }
+ /**
+ * Call all callbacks in the desctructor, unless it has been disabled.
+ */
+ ~ScopeGuard()
+ {
+ if (this->enabled)
+ for (auto& func: this->callbacks)
+ func();
+ }
+ /**
+ * Add a callback to be called in our destructor, one scope guard can be
+ * used for more than one task, if needed.
+ */
+ void add_callback(std::function<void()>&& func)
+ {
+ this->callbacks.emplace_back(std::move(func));
+ }
+ /**
+ * Disable that scope guard, nothing will be done when the scope is
+ * exited.
+ */
+ void disable()
+ {
+ this->enabled = false;
+ }
+
+private:
+ bool enabled;
+ std::vector<std::function<void()>> callbacks;
+
+};
+
+}
+
diff --git a/louloulibs/utils/sha1.cpp b/louloulibs/utils/sha1.cpp
new file mode 100644
index 0000000..76476df
--- /dev/null
+++ b/louloulibs/utils/sha1.cpp
@@ -0,0 +1,154 @@
+/* This code is public-domain - it is based on libcrypt
+ * placed in the public domain by Wei Dai and other contributors.
+ */
+
+#include "sha1.hpp"
+
+#define SHA1_K0 0x5a827999
+#define SHA1_K20 0x6ed9eba1
+#define SHA1_K40 0x8f1bbcdc
+#define SHA1_K60 0xca62c1d6
+
+const uint8_t sha1InitState[] = {
+ 0x01,0x23,0x45,0x67, // H0
+ 0x89,0xab,0xcd,0xef, // H1
+ 0xfe,0xdc,0xba,0x98, // H2
+ 0x76,0x54,0x32,0x10, // H3
+ 0xf0,0xe1,0xd2,0xc3 // H4
+};
+
+void sha1_init(sha1nfo *s) {
+ memcpy(s->state.b,sha1InitState,HASH_LENGTH);
+ s->byteCount = 0;
+ s->bufferOffset = 0;
+}
+
+uint32_t sha1_rol32(uint32_t number, uint8_t bits) {
+ return ((number << bits) | (number >> (32-bits)));
+}
+
+void sha1_hashBlock(sha1nfo *s) {
+ uint8_t i;
+ uint32_t a,b,c,d,e,t;
+
+ a=s->state.w[0];
+ b=s->state.w[1];
+ c=s->state.w[2];
+ d=s->state.w[3];
+ e=s->state.w[4];
+ for (i=0; i<80; i++) {
+ if (i>=16) {
+ t = s->buffer.w[(i+13)&15] ^ s->buffer.w[(i+8)&15] ^ s->buffer.w[(i+2)&15] ^ s->buffer.w[i&15];
+ s->buffer.w[i&15] = sha1_rol32(t,1);
+ }
+ if (i<20) {
+ t = (d ^ (b & (c ^ d))) + SHA1_K0;
+ } else if (i<40) {
+ t = (b ^ c ^ d) + SHA1_K20;
+ } else if (i<60) {
+ t = ((b & c) | (d & (b | c))) + SHA1_K40;
+ } else {
+ t = (b ^ c ^ d) + SHA1_K60;
+ }
+ t+=sha1_rol32(a,5) + e + s->buffer.w[i&15];
+ e=d;
+ d=c;
+ c=sha1_rol32(b,30);
+ b=a;
+ a=t;
+ }
+ s->state.w[0] += a;
+ s->state.w[1] += b;
+ s->state.w[2] += c;
+ s->state.w[3] += d;
+ s->state.w[4] += e;
+}
+
+void sha1_addUncounted(sha1nfo *s, uint8_t data) {
+ s->buffer.b[s->bufferOffset ^ 3] = data;
+ s->bufferOffset++;
+ if (s->bufferOffset == BLOCK_LENGTH) {
+ sha1_hashBlock(s);
+ s->bufferOffset = 0;
+ }
+}
+
+void sha1_writebyte(sha1nfo *s, uint8_t data) {
+ ++s->byteCount;
+ sha1_addUncounted(s, data);
+}
+
+void sha1_write(sha1nfo *s, const char *data, size_t len) {
+ for (;len--;) sha1_writebyte(s, (uint8_t) *data++);
+}
+
+void sha1_pad(sha1nfo *s) {
+ // Implement SHA-1 padding (fips180-2 §5.1.1)
+
+ // Pad with 0x80 followed by 0x00 until the end of the block
+ sha1_addUncounted(s, 0x80);
+ while (s->bufferOffset != 56) sha1_addUncounted(s, 0x00);
+
+ // Append length in the last 8 bytes
+ sha1_addUncounted(s, 0); // We're only using 32 bit lengths
+ sha1_addUncounted(s, 0); // But SHA-1 supports 64 bit lengths
+ sha1_addUncounted(s, 0); // So zero pad the top bits
+ sha1_addUncounted(s, s->byteCount >> 29); // Shifting to multiply by 8
+ sha1_addUncounted(s, s->byteCount >> 21); // as SHA-1 supports bitstreams as well as
+ sha1_addUncounted(s, s->byteCount >> 13); // byte.
+ sha1_addUncounted(s, s->byteCount >> 5);
+ sha1_addUncounted(s, s->byteCount << 3);
+}
+
+uint8_t* sha1_result(sha1nfo *s) {
+ int i;
+ // Pad to complete the last block
+ sha1_pad(s);
+
+ // Swap byte order back
+ for (i=0; i<5; i++) {
+ uint32_t a,b;
+ a=s->state.w[i];
+ b=a<<24;
+ b|=(a<<8) & 0x00ff0000;
+ b|=(a>>8) & 0x0000ff00;
+ b|=a>>24;
+ s->state.w[i]=b;
+ }
+
+ // Return pointer to hash (20 characters)
+ return s->state.b;
+}
+
+#define HMAC_IPAD 0x36
+#define HMAC_OPAD 0x5c
+
+void sha1_initHmac(sha1nfo *s, const uint8_t* key, int keyLength) {
+ uint8_t i;
+ memset(s->keyBuffer, 0, BLOCK_LENGTH);
+ if (keyLength > BLOCK_LENGTH) {
+ // Hash long keys
+ sha1_init(s);
+ for (;keyLength--;) sha1_writebyte(s, *key++);
+ memcpy(s->keyBuffer, sha1_result(s), HASH_LENGTH);
+ } else {
+ // Block length keys are used as is
+ memcpy(s->keyBuffer, key, keyLength);
+ }
+ // Start inner hash
+ sha1_init(s);
+ for (i=0; i<BLOCK_LENGTH; i++) {
+ sha1_writebyte(s, s->keyBuffer[i] ^ HMAC_IPAD);
+ }
+}
+
+uint8_t* sha1_resultHmac(sha1nfo *s) {
+ uint8_t i;
+ // Complete inner hash
+ memcpy(s->innerHash,sha1_result(s),HASH_LENGTH);
+ // Calculate outer hash
+ sha1_init(s);
+ for (i=0; i<BLOCK_LENGTH; i++) sha1_writebyte(s, s->keyBuffer[i] ^ HMAC_OPAD);
+ for (i=0; i<HASH_LENGTH; i++) sha1_writebyte(s, s->innerHash[i]);
+ return sha1_result(s);
+}
diff --git a/louloulibs/utils/sha1.hpp b/louloulibs/utils/sha1.hpp
new file mode 100644
index 0000000..d02de75
--- /dev/null
+++ b/louloulibs/utils/sha1.hpp
@@ -0,0 +1,35 @@
+/* This code is public-domain - it is based on libcrypt
+ * placed in the public domain by Wei Dai and other contributors.
+ */
+
+#include <stdint.h>
+#include <string.h>
+
+#define HASH_LENGTH 20
+#define BLOCK_LENGTH 64
+
+union _buffer {
+ uint8_t b[BLOCK_LENGTH];
+ uint32_t w[BLOCK_LENGTH/4];
+};
+
+union _state {
+ uint8_t b[HASH_LENGTH];
+ uint32_t w[HASH_LENGTH/4];
+};
+
+typedef struct sha1nfo {
+ union _buffer buffer;
+ uint8_t bufferOffset;
+ union _state state;
+ uint32_t byteCount;
+ uint8_t keyBuffer[BLOCK_LENGTH];
+ uint8_t innerHash[HASH_LENGTH];
+} sha1nfo;
+
+void sha1_init(sha1nfo *s);
+void sha1_writebyte(sha1nfo *s, uint8_t data);
+void sha1_write(sha1nfo *s, const char *data, size_t len);
+uint8_t* sha1_result(sha1nfo *s);
+void sha1_initHmac(sha1nfo *s, const uint8_t* key, int keyLength);
+uint8_t* sha1_resultHmac(sha1nfo *s);
diff --git a/louloulibs/utils/split.cpp b/louloulibs/utils/split.cpp
new file mode 100644
index 0000000..80f8dae
--- /dev/null
+++ b/louloulibs/utils/split.cpp
@@ -0,0 +1,19 @@
+#include <utils/split.hpp>
+#include <sstream>
+
+namespace utils
+{
+ std::vector<std::string> split(const std::string& s, const char delim, const bool allow_empty)
+ {
+ std::vector<std::string> ret;
+ std::stringstream ss(s);
+ std::string item;
+ while (std::getline(ss, item, delim))
+ {
+ if (item.empty() && !allow_empty)
+ continue ;
+ ret.emplace_back(std::move(item));
+ }
+ return ret;
+ }
+}
diff --git a/louloulibs/utils/split.hpp b/louloulibs/utils/split.hpp
new file mode 100644
index 0000000..3755ef8
--- /dev/null
+++ b/louloulibs/utils/split.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+
+#include <string>
+#include <vector>
+
+namespace utils
+{
+ std::vector<std::string> split(const std::string &s, const char delim, const bool allow_empty=true);
+}
+
+
diff --git a/louloulibs/utils/string.cpp b/louloulibs/utils/string.cpp
new file mode 100644
index 0000000..635e71a
--- /dev/null
+++ b/louloulibs/utils/string.cpp
@@ -0,0 +1,28 @@
+#include <utils/string.hpp>
+#include <utils/encoding.hpp>
+
+bool to_bool(const std::string& val)
+{
+ return (val == "1" || val == "true");
+}
+
+std::vector<std::string> cut(const std::string& val, const std::size_t size)
+{
+ std::vector<std::string> res;
+ std::string::size_type pos = 0;
+ while (pos < val.size())
+ {
+ // Get the number of chars, <= size, that contain only whole
+ // UTF-8 codepoints.
+ std::size_t s = 0;
+ auto codepoint_size = utils::get_next_codepoint_size(val[pos + s]);
+ while (s + codepoint_size <= size && pos + s < val.size())
+ {
+ s += codepoint_size;
+ codepoint_size = utils::get_next_codepoint_size(val[pos + s]);
+ }
+ res.emplace_back(val.substr(pos, s));
+ pos += s;
+ }
+ return res;
+}
diff --git a/louloulibs/utils/string.hpp b/louloulibs/utils/string.hpp
new file mode 100644
index 0000000..84ba101
--- /dev/null
+++ b/louloulibs/utils/string.hpp
@@ -0,0 +1,10 @@
+#pragma once
+
+
+#include <vector>
+#include <string>
+
+bool to_bool(const std::string& val);
+std::vector<std::string> cut(const std::string& val, const std::size_t size);
+
+
diff --git a/louloulibs/utils/timed_events.cpp b/louloulibs/utils/timed_events.cpp
new file mode 100644
index 0000000..68d009c
--- /dev/null
+++ b/louloulibs/utils/timed_events.cpp
@@ -0,0 +1,49 @@
+#include <utils/timed_events.hpp>
+
+TimedEvent::TimedEvent(std::chrono::steady_clock::time_point&& time_point,
+ std::function<void()> callback, const std::string& name):
+ time_point(std::move(time_point)),
+ callback(callback),
+ repeat(false),
+ repeat_delay(0),
+ name(name)
+{
+}
+
+TimedEvent::TimedEvent(std::chrono::milliseconds&& duration,
+ std::function<void()> callback, const std::string& name):
+ time_point(std::chrono::steady_clock::now() + duration),
+ callback(callback),
+ repeat(true),
+ repeat_delay(std::move(duration)),
+ name(name)
+{
+}
+
+bool TimedEvent::is_after(const TimedEvent& other) const
+{
+ return this->is_after(other.time_point);
+}
+
+bool TimedEvent::is_after(const std::chrono::steady_clock::time_point& time_point) const
+{
+ return this->time_point > time_point;
+}
+
+std::chrono::milliseconds TimedEvent::get_timeout() const
+{
+ auto now = std::chrono::steady_clock::now();
+ if (now > this->time_point)
+ return std::chrono::milliseconds(0);
+ return std::chrono::duration_cast<std::chrono::milliseconds>(this->time_point - now);
+}
+
+void TimedEvent::execute() const
+{
+ this->callback();
+}
+
+const std::string& TimedEvent::get_name() const
+{
+ return this->name;
+}
diff --git a/louloulibs/utils/timed_events.hpp b/louloulibs/utils/timed_events.hpp
new file mode 100644
index 0000000..6e28206
--- /dev/null
+++ b/louloulibs/utils/timed_events.hpp
@@ -0,0 +1,132 @@
+#pragma once
+
+#include <functional>
+#include <string>
+#include <chrono>
+#include <vector>
+
+using namespace std::literals::chrono_literals;
+
+namespace utils {
+static constexpr std::chrono::milliseconds no_timeout = std::chrono::milliseconds(-1);
+}
+
+class TimedEventsManager;
+
+/**
+ * A callback with an associated date.
+ */
+
+class TimedEvent
+{
+ friend class TimedEventsManager;
+public:
+ /**
+ * An event the occurs only once, at the given time_point
+ */
+ explicit TimedEvent(std::chrono::steady_clock::time_point&& time_point,
+ std::function<void()> callback, const std::string& name="");
+ explicit TimedEvent(std::chrono::milliseconds&& duration,
+ std::function<void()> callback, const std::string& name="");
+
+ explicit TimedEvent(TimedEvent&&) = default;
+ TimedEvent& operator=(TimedEvent&&) = default;
+ ~TimedEvent() = default;
+
+ TimedEvent(const TimedEvent&) = delete;
+ TimedEvent& operator=(const TimedEvent&) = delete;
+
+ /**
+ * Whether or not this event happens after the other one.
+ */
+ bool is_after(const TimedEvent& other) const;
+ bool is_after(const std::chrono::steady_clock::time_point& time_point) const;
+ /**
+ * Return the duration difference between now and the event time point.
+ * If the difference would be negative (i.e. the event is expired), the
+ * returned value is 0 instead. The value cannot then be negative.
+ */
+ std::chrono::milliseconds get_timeout() const;
+ void execute() const;
+ const std::string& get_name() const;
+
+private:
+ /**
+ * The next time point at which the event is executed.
+ */
+ std::chrono::steady_clock::time_point time_point;
+ /**
+ * The function to execute.
+ */
+ std::function<void()> callback;
+ /**
+ * Whether or not this events repeats itself until it is destroyed.
+ */
+ bool repeat;
+ /**
+ * This value is added to the time_point each time the event is executed,
+ * if repeat is true. Otherwise it is ignored.
+ */
+ std::chrono::milliseconds repeat_delay;
+ /**
+ * A name that is used to identify that event. If you want to find your
+ * event (for example if you want to cancel it), the name should be
+ * unique.
+ */
+ std::string name;
+};
+
+/**
+ * A class managing a list of TimedEvents.
+ * They are sorted, new events can be added, removed, fetch, etc.
+ */
+
+class TimedEventsManager
+{
+public:
+ ~TimedEventsManager() = default;
+
+ TimedEventsManager(const TimedEventsManager&) = delete;
+ TimedEventsManager(TimedEventsManager&&) = delete;
+ TimedEventsManager& operator=(const TimedEventsManager&) = delete;
+ TimedEventsManager& operator=(TimedEventsManager&&) = delete;
+
+ /**
+ * Return the unique instance of this class
+ */
+ static TimedEventsManager& instance();
+ /**
+ * Add an event to the list of managed events. The list is sorted after
+ * this call.
+ */
+ void add_event(TimedEvent&& event);
+ /**
+ * Returns the duration, in milliseconds, between now and the next
+ * available event. If the event is already expired (the duration is
+ * negative), 0 is returned instead (as in “it's not too late, execute it
+ * now”)
+ * Returns a negative value if no event is available.
+ */
+ std::chrono::milliseconds get_timeout() const;
+ /**
+ * Execute all the expired events (if their expiration time is exactly
+ * now, or before now). The event is then removed from the list. If the
+ * event does repeat, its expiration time is updated and it is reinserted
+ * in the list at the correct position.
+ * Returns the number of executed events.
+ */
+ std::size_t execute_expired_events();
+ /**
+ * Remove (and thus cancel) all the timed events with the given name.
+ * Returns the number of canceled events.
+ */
+ std::size_t cancel(const std::string& name);
+ /**
+ * Return the number of managed events.
+ */
+ std::size_t size() const;
+
+private:
+ std::vector<TimedEvent> events;
+ explicit TimedEventsManager() = default;
+};
diff --git a/louloulibs/utils/timed_events_manager.cpp b/louloulibs/utils/timed_events_manager.cpp
new file mode 100644
index 0000000..67d61fe
--- /dev/null
+++ b/louloulibs/utils/timed_events_manager.cpp
@@ -0,0 +1,73 @@
+#include <utils/timed_events.hpp>
+
+TimedEventsManager& TimedEventsManager::instance()
+{
+ static TimedEventsManager inst;
+ return inst;
+}
+
+void TimedEventsManager::add_event(TimedEvent&& event)
+{
+ for (auto it = this->events.begin(); it != this->events.end(); ++it)
+ {
+ if (it->is_after(event))
+ {
+ this->events.emplace(it, std::move(event));
+ return;
+ }
+ }
+ this->events.emplace_back(std::move(event));
+}
+
+std::chrono::milliseconds TimedEventsManager::get_timeout() const
+{
+ if (this->events.empty())
+ return utils::no_timeout;
+ return this->events.front().get_timeout();
+}
+
+std::size_t TimedEventsManager::execute_expired_events()
+{
+ std::size_t count = 0;
+ const auto now = std::chrono::steady_clock::now();
+ for (auto it = this->events.begin(); it != this->events.end();)
+ {
+ if (!it->is_after(now))
+ {
+ TimedEvent copy(std::move(*it));
+ it = this->events.erase(it);
+ ++count;
+ copy.execute();
+ if (copy.repeat)
+ {
+ copy.time_point += copy.repeat_delay;
+ this->add_event(std::move(copy));
+ }
+ continue;
+ }
+ else
+ break;
+ }
+ return count;
+}
+
+std::size_t TimedEventsManager::cancel(const std::string& name)
+{
+ std::size_t res = 0;
+ for (auto it = this->events.begin(); it != this->events.end();)
+ {
+ if (it->get_name() == name)
+ {
+ it = this->events.erase(it);
+ res++;
+ }
+ else
+ ++it;
+ }
+ return res;
+}
+
+std::size_t TimedEventsManager::size() const
+{
+ return this->events.size();
+}
diff --git a/louloulibs/utils/tolower.cpp b/louloulibs/utils/tolower.cpp
new file mode 100644
index 0000000..3e518bd
--- /dev/null
+++ b/louloulibs/utils/tolower.cpp
@@ -0,0 +1,13 @@
+#include <utils/tolower.hpp>
+
+namespace utils
+{
+ std::string tolower(const std::string& original)
+ {
+ std::string res;
+ res.reserve(original.size());
+ for (const char c: original)
+ res += static_cast<char>(std::tolower(c));
+ return res;
+ }
+}
diff --git a/louloulibs/utils/tolower.hpp b/louloulibs/utils/tolower.hpp
new file mode 100644
index 0000000..650e05d
--- /dev/null
+++ b/louloulibs/utils/tolower.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+
+#include <string>
+
+namespace utils
+{
+ std::string tolower(const std::string& original);
+}
+
+
diff --git a/louloulibs/utils/xdg.cpp b/louloulibs/utils/xdg.cpp
new file mode 100644
index 0000000..48212a1
--- /dev/null
+++ b/louloulibs/utils/xdg.cpp
@@ -0,0 +1,29 @@
+#include <utils/xdg.hpp>
+#include <cstdlib>
+
+#include "louloulibs.h"
+
+std::string xdg_path(const std::string& filename, const char* env_var)
+{
+ const char* xdg_home = ::getenv(env_var);
+ if (xdg_home && xdg_home[0] == '/')
+ return std::string{xdg_home} + "/" PROJECT_NAME "/" + filename;
+ else
+ {
+ const char* home = ::getenv("HOME");
+ if (home)
+ return std::string{home} + "/" ".config" "/" PROJECT_NAME "/" + filename;
+ else
+ return filename;
+ }
+}
+
+std::string xdg_config_path(const std::string& filename)
+{
+ return xdg_path(filename, "XDG_CONFIG_HOME");
+}
+
+std::string xdg_data_path(const std::string& filename)
+{
+ return xdg_path(filename, "XDG_DATA_HOME");
+}
diff --git a/louloulibs/utils/xdg.hpp b/louloulibs/utils/xdg.hpp
new file mode 100644
index 0000000..56e11da
--- /dev/null
+++ b/louloulibs/utils/xdg.hpp
@@ -0,0 +1,14 @@
+#pragma once
+
+
+#include <string>
+
+/**
+ * Returns a path for the given filename, according to the XDG base
+ * directory specification, see
+ * http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
+ */
+std::string xdg_config_path(const std::string& filename);
+std::string xdg_data_path(const std::string& filename);
+
+
diff --git a/louloulibs/xmpp/adhoc_command.cpp b/louloulibs/xmpp/adhoc_command.cpp
new file mode 100644
index 0000000..99701d7
--- /dev/null
+++ b/louloulibs/xmpp/adhoc_command.cpp
@@ -0,0 +1,89 @@
+#include <xmpp/adhoc_command.hpp>
+#include <xmpp/xmpp_component.hpp>
+#include <utils/reload.hpp>
+
+using namespace std::string_literals;
+
+AdhocCommand::AdhocCommand(std::vector<AdhocStep>&& callbacks, const std::string& name, const bool admin_only):
+ name(name),
+ callbacks(std::move(callbacks)),
+ admin_only(admin_only)
+{
+}
+
+bool AdhocCommand::is_admin_only() const
+{
+ return this->admin_only;
+}
+
+void PingStep1(XmppComponent&, AdhocSession&, XmlNode& command_node)
+{
+ XmlNode note("note");
+ note["type"] = "info";
+ note.set_inner("Pong");
+ command_node.add_child(std::move(note));
+}
+
+void HelloStep1(XmppComponent&, AdhocSession&, XmlNode& command_node)
+{
+ XmlNode x("jabber:x:data:x");
+ x["type"] = "form";
+ XmlNode title("title");
+ title.set_inner("Configure your name.");
+ x.add_child(std::move(title));
+ XmlNode instructions("instructions");
+ instructions.set_inner("Please provide your name.");
+ x.add_child(std::move(instructions));
+ XmlNode name_field("field");
+ name_field["var"] = "name";
+ name_field["type"] = "text-single";
+ name_field["label"] = "Your name";
+ XmlNode required("required");
+ name_field.add_child(std::move(required));
+ x.add_child(std::move(name_field));
+ command_node.add_child(std::move(x));
+}
+
+void HelloStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node)
+{
+ // Find out if the name was provided in the form.
+ if (const XmlNode* x = command_node.get_child("x", "jabber:x:data"))
+ {
+ const XmlNode* name_field = nullptr;
+ for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
+ if (field->get_tag("var") == "name")
+ {
+ name_field = field;
+ break;
+ }
+ if (name_field)
+ {
+ if (const XmlNode* value = name_field->get_child("value", "jabber:x:data"))
+ {
+ XmlNode note("note");
+ note["type"] = "info";
+ note.set_inner("Hello "s + value->get_inner() + "!"s);
+ command_node.delete_all_children();
+ command_node.add_child(std::move(note));
+ return;
+ }
+ }
+ }
+ command_node.delete_all_children();
+ XmlNode error(ADHOC_NS":error");
+ error["type"] = "modify";
+ XmlNode condition(STANZA_NS":bad-request");
+ error.add_child(std::move(condition));
+ command_node.add_child(std::move(error));
+ session.terminate();
+}
+
+void Reload(XmppComponent&, AdhocSession&, XmlNode& command_node)
+{
+ ::reload_process();
+ command_node.delete_all_children();
+ XmlNode note("note");
+ note["type"] = "info";
+ note.set_inner("Configuration reloaded.");
+ command_node.add_child(std::move(note));
+}
diff --git a/louloulibs/xmpp/adhoc_command.hpp b/louloulibs/xmpp/adhoc_command.hpp
new file mode 100644
index 0000000..7c4de47
--- /dev/null
+++ b/louloulibs/xmpp/adhoc_command.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+/**
+ * Describe an ad-hoc command.
+ *
+ * Can only have zero or one step for now. When execution is requested, it
+ * can return a result immediately, or provide a form to be filled, and
+ * provide a result once the filled form is received.
+ */
+
+#include <xmpp/adhoc_session.hpp>
+
+#include <functional>
+#include <string>
+
+class AdhocCommand
+{
+ friend class AdhocSession;
+public:
+ AdhocCommand(std::vector<AdhocStep>&& callback, const std::string& name, const bool admin_only);
+ ~AdhocCommand() = default;
+ AdhocCommand(const AdhocCommand&) = default;
+ AdhocCommand(AdhocCommand&&) = default;
+ AdhocCommand& operator=(AdhocCommand&&) = delete;
+ AdhocCommand& operator=(const AdhocCommand&) = delete;
+
+ const std::string name;
+
+ bool is_admin_only() const;
+
+private:
+ /**
+ * A command may have one or more steps. Each step is a different
+ * callback, inserting things into a <command/> XmlNode and calling
+ * methods of an AdhocSession.
+ */
+ std::vector<AdhocStep> callbacks;
+ const bool admin_only;
+};
+
+void PingStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void HelloStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void HelloStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void Reload(XmppComponent&, AdhocSession& session, XmlNode& command_node);
diff --git a/louloulibs/xmpp/adhoc_commands_handler.cpp b/louloulibs/xmpp/adhoc_commands_handler.cpp
new file mode 100644
index 0000000..17c4e67
--- /dev/null
+++ b/louloulibs/xmpp/adhoc_commands_handler.cpp
@@ -0,0 +1,123 @@
+#include <xmpp/adhoc_commands_handler.hpp>
+#include <xmpp/xmpp_component.hpp>
+
+#include <utils/timed_events.hpp>
+#include <logger/logger.hpp>
+#include <config/config.hpp>
+#include <xmpp/jid.hpp>
+
+#include <iostream>
+
+using namespace std::string_literals;
+
+const std::map<const std::string, const AdhocCommand>& AdhocCommandsHandler::get_commands() const
+{
+ return this->commands;
+}
+
+std::map<const std::string, const AdhocCommand>& AdhocCommandsHandler::get_commands()
+{
+ return this->commands;
+}
+
+XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, const std::string& to, XmlNode command_node)
+{
+ std::string action = command_node.get_tag("action");
+ if (action.empty())
+ action = "execute";
+ command_node.del_tag("action");
+
+ Jid jid(executor_jid);
+
+ const std::string node = command_node.get_tag("node");
+ auto command_it = this->commands.find(node);
+ if (command_it == this->commands.end())
+ {
+ XmlNode error(ADHOC_NS":error");
+ error["type"] = "cancel";
+ XmlNode condition(STANZA_NS":item-not-found");
+ error.add_child(std::move(condition));
+ command_node.add_child(std::move(error));
+ }
+ else if (command_it->second.is_admin_only() &&
+ Config::get("admin", "") != jid.local + "@" + jid.domain)
+ {
+ XmlNode error(ADHOC_NS":error");
+ error["type"] = "cancel";
+ XmlNode condition(STANZA_NS":forbidden");
+ error.add_child(std::move(condition));
+ command_node.add_child(std::move(error));
+ }
+ else
+ {
+ std::string sessionid = command_node.get_tag("sessionid");
+ if (sessionid.empty())
+ { // create a new session, with a new id
+ sessionid = XmppComponent::next_id();
+ command_node["sessionid"] = sessionid;
+ this->sessions.emplace(std::piecewise_construct,
+ std::forward_as_tuple(sessionid, executor_jid),
+ std::forward_as_tuple(command_it->second, executor_jid, to));
+ TimedEventsManager::instance().add_event(TimedEvent(std::chrono::steady_clock::now() + 3600s,
+ std::bind(&AdhocCommandsHandler::remove_session, this, sessionid, executor_jid),
+ "adhocsession"s + sessionid + executor_jid));
+ }
+ auto session_it = this->sessions.find(std::make_pair(sessionid, executor_jid));
+ if (session_it == this->sessions.end())
+ {
+ XmlNode error(ADHOC_NS":error");
+ error["type"] = "modify";
+ XmlNode condition(STANZA_NS":bad-request");
+ error.add_child(std::move(condition));
+ command_node.add_child(std::move(error));
+ }
+ else if (action == "execute" || action == "next" || action == "complete")
+ {
+ // execute the step
+ AdhocSession& session = session_it->second;
+ const AdhocStep& step = session.get_next_step();
+ step(this->xmpp_component, session, command_node);
+ if (session.remaining_steps() == 0 ||
+ session.is_terminated())
+ {
+ this->sessions.erase(session_it);
+ command_node["status"] = "completed";
+ TimedEventsManager::instance().cancel("adhocsession"s + sessionid + executor_jid);
+ }
+ else
+ {
+ command_node["status"] = "executing";
+ XmlNode actions("actions");
+ XmlNode next("next");
+ actions.add_child(std::move(next));
+ command_node.add_child(std::move(actions));
+ }
+ }
+ else if (action == "cancel")
+ {
+ this->sessions.erase(session_it);
+ command_node["status"] = "canceled";
+ TimedEventsManager::instance().cancel("adhocsession"s + sessionid + executor_jid);
+ }
+ else // unsupported action
+ {
+ XmlNode error(ADHOC_NS":error");
+ error["type"] = "modify";
+ XmlNode condition(STANZA_NS":bad-request");
+ error.add_child(std::move(condition));
+ command_node.add_child(std::move(error));
+ }
+ }
+ return command_node;
+}
+
+void AdhocCommandsHandler::remove_session(const std::string& session_id, const std::string& initiator_jid)
+{
+ auto session_it = this->sessions.find(std::make_pair(session_id, initiator_jid));
+ if (session_it != this->sessions.end())
+ {
+ this->sessions.erase(session_it);
+ return ;
+ }
+ log_error("Tried to remove ad-hoc session for [", session_id, ", ", initiator_jid, "] but none found");
+}
diff --git a/louloulibs/xmpp/adhoc_commands_handler.hpp b/louloulibs/xmpp/adhoc_commands_handler.hpp
new file mode 100644
index 0000000..91eb5bd
--- /dev/null
+++ b/louloulibs/xmpp/adhoc_commands_handler.hpp
@@ -0,0 +1,71 @@
+#pragma once
+
+/**
+ * Manage a list of available AdhocCommands and the list of ongoing
+ * AdhocCommandSessions.
+ */
+
+#include <xmpp/adhoc_command.hpp>
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <utility>
+#include <string>
+#include <map>
+
+class AdhocCommandsHandler
+{
+public:
+ explicit AdhocCommandsHandler(XmppComponent& xmpp_component):
+ xmpp_component(xmpp_component),
+ commands{}
+ { }
+ ~AdhocCommandsHandler() = default;
+
+ AdhocCommandsHandler(const AdhocCommandsHandler&) = delete;
+ AdhocCommandsHandler(AdhocCommandsHandler&&) = delete;
+ AdhocCommandsHandler& operator=(const AdhocCommandsHandler&) = delete;
+ AdhocCommandsHandler& operator=(AdhocCommandsHandler&&) = delete;
+
+ /**
+ * Returns the list of available commands.
+ */
+ const std::map<const std::string, const AdhocCommand>& get_commands() const;
+ /**
+ * This one can be used to add new commands.
+ */
+ std::map<const std::string, const AdhocCommand>& get_commands();
+ /**
+ * Find the requested command, create a new session or use an existing
+ * one, and process the request (provide a new form, an error, or a
+ * result).
+ *
+ * Returns a (moved) XmlNode that will be inserted in the iq response. It
+ * should be a <command/> node containing one or more useful children. If
+ * it contains an <error/> node, the iq response will have an error type.
+ *
+ * Takes a copy of the <command/> node so we can actually edit it and use
+ * it as our return value.
+ */
+ XmlNode handle_request(const std::string& executor_jid, const std::string& to, XmlNode command_node);
+ /**
+ * Remove the session from the list. This is done to avoid filling the
+ * memory with waiting session (for example due to a client that starts
+ * multi-steps command but never finishes them).
+ */
+ void remove_session(const std::string& session_id, const std::string& initiator_jid);
+private:
+ /**
+ * To access basically anything in the gateway.
+ */
+ XmppComponent& xmpp_component;
+ /**
+ * The list of all available commands.
+ */
+ std::map<const std::string, const AdhocCommand> commands;
+ /**
+ * The list of all currently on-going commands.
+ *
+ * Of the form: {{session_id, owner_jid}, session}.
+ */
+ std::map<std::pair<const std::string, const std::string>, AdhocSession> sessions;
+};
diff --git a/louloulibs/xmpp/adhoc_session.cpp b/louloulibs/xmpp/adhoc_session.cpp
new file mode 100644
index 0000000..dda4bea
--- /dev/null
+++ b/louloulibs/xmpp/adhoc_session.cpp
@@ -0,0 +1,35 @@
+#include <xmpp/adhoc_session.hpp>
+#include <xmpp/adhoc_command.hpp>
+
+#include <assert.h>
+
+AdhocSession::AdhocSession(const AdhocCommand& command, const std::string& owner_jid,
+ const std::string& to_jid):
+ command(command),
+ owner_jid(owner_jid),
+ to_jid(to_jid),
+ current_step(0),
+ terminated(false)
+{
+}
+
+const AdhocStep& AdhocSession::get_next_step()
+{
+ assert(this->current_step < this->command.callbacks.size());
+ return this->command.callbacks[this->current_step++];
+}
+
+size_t AdhocSession::remaining_steps() const
+{
+ return this->command.callbacks.size() - this->current_step;
+}
+
+bool AdhocSession::is_terminated() const
+{
+ return this->terminated;
+}
+
+void AdhocSession::terminate()
+{
+ this->terminated = true;
+}
diff --git a/louloulibs/xmpp/adhoc_session.hpp b/louloulibs/xmpp/adhoc_session.hpp
new file mode 100644
index 0000000..0de8d13
--- /dev/null
+++ b/louloulibs/xmpp/adhoc_session.hpp
@@ -0,0 +1,88 @@
+#pragma once
+
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <functional>
+#include <string>
+#include <map>
+
+class XmppComponent;
+
+class AdhocCommand;
+class AdhocSession;
+
+/**
+ * A function executed as an ad-hoc command step. It takes a <command/>
+ * XmlNode and modifies it accordingly (inserting for example an <error/>
+ * node, or a data form…).
+ */
+using AdhocStep = std::function<void(XmppComponent&, AdhocSession&, XmlNode&)>;
+
+class AdhocSession
+{
+public:
+ explicit AdhocSession(const AdhocCommand& command, const std::string& owner_jid,
+ const std::string& to_jid);
+ ~AdhocSession() = default;
+
+ AdhocSession(const AdhocSession&) = delete;
+ AdhocSession(AdhocSession&&) = delete;
+ AdhocSession& operator=(const AdhocSession&) = delete;
+ AdhocSession& operator=(AdhocSession&&) = delete;
+
+ /**
+ * Return the function to be executed, found in our AdhocCommand, for the
+ * current_step. And increment the current_step.
+ */
+ const AdhocStep& get_next_step();
+ /**
+ * Return the number of remaining steps.
+ */
+ size_t remaining_steps() const;
+ /**
+ * This may be modified by an AdhocStep, to indicate that this session
+ * should no longer exist, because we encountered an error, and we can't
+ * execute any more step of it.
+ */
+ void terminate();
+ bool is_terminated() const;
+ std::string get_target_jid() const
+ {
+ return this->to_jid;
+ }
+ std::string get_owner_jid() const
+ {
+ return this->owner_jid;
+ }
+
+private:
+ /**
+ * A reference of the command concerned by this session. Used for example
+ * to get the next step of that command, things like that.
+ */
+ const AdhocCommand& command;
+ /**
+ * The full JID of the XMPP user that created this session by executing
+ * the first step of a command. Only that JID must be allowed to access
+ * this session.
+ */
+ const std::string& owner_jid;
+ /**
+ * The 'to' attribute in the request stanza. This is the target of the current session.
+ */
+ const std::string& to_jid;
+ /**
+ * The current step we are at. It starts at zero. It is used to index the
+ * associated AdhocCommand::callbacks vector.
+ */
+ size_t current_step;
+ bool terminated;
+
+public:
+ /**
+ * A map to store various things that we may want to remember between two
+ * steps of the same session. A step can insert any value associated to
+ * any key in there.
+ */
+ std::map<std::string, std::string> vars;
+};
diff --git a/louloulibs/xmpp/body.hpp b/louloulibs/xmpp/body.hpp
new file mode 100644
index 0000000..068d1a4
--- /dev/null
+++ b/louloulibs/xmpp/body.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+
+namespace Xmpp
+{
+// Contains:
+// - an XMPP-valid UTF-8 body
+// - an XML node representing the XHTML-IM body, or null
+ using body = std::tuple<const std::string, std::unique_ptr<XmlNode>>;
+}
+
+
diff --git a/louloulibs/xmpp/jid.cpp b/louloulibs/xmpp/jid.cpp
new file mode 100644
index 0000000..7b62f3e
--- /dev/null
+++ b/louloulibs/xmpp/jid.cpp
@@ -0,0 +1,99 @@
+#include <xmpp/jid.hpp>
+#include <algorithm>
+#include <cstring>
+#include <map>
+
+#include <louloulibs.h>
+#ifdef LIBIDN_FOUND
+ #include <stringprep.h>
+#endif
+
+#include <logger/logger.hpp>
+
+Jid::Jid(const std::string& jid)
+{
+ std::string::size_type slash = jid.find('/');
+ if (slash != std::string::npos)
+ {
+ this->resource = jid.substr(slash + 1);
+ }
+
+ std::string::size_type at = jid.find('@');
+ if (at != std::string::npos && at < slash)
+ {
+ this->local = jid.substr(0, at);
+ at++;
+ }
+ else
+ at = 0;
+
+ this->domain = jid.substr(at, slash - at);
+}
+
+static constexpr size_t max_jid_part_len = 1023;
+
+std::string jidprep(const std::string& original)
+{
+#ifdef LIBIDN_FOUND
+ using CacheType = std::map<std::string, std::string>;
+ static CacheType cache;
+ std::pair<CacheType::iterator, bool> cached = cache.insert({original, {}});
+ if (std::get<1>(cached) == false)
+ { // Insertion failed: the result is already in the cache, return it
+ return std::get<0>(cached)->second;
+ }
+
+ const std::string error_msg("Failed to convert " + original + " into a valid JID:");
+ Jid jid(original);
+
+ char local[max_jid_part_len] = {};
+ memcpy(local, jid.local.data(), std::min(max_jid_part_len, jid.local.size()));
+ Stringprep_rc rc = static_cast<Stringprep_rc>(::stringprep(local, max_jid_part_len,
+ static_cast<Stringprep_profile_flags>(0), stringprep_xmpp_nodeprep));
+ if (rc != STRINGPREP_OK)
+ {
+ log_error(error_msg + stringprep_strerror(rc));
+ return "";
+ }
+
+ char domain[max_jid_part_len] = {};
+ memcpy(domain, jid.domain.data(), std::min(max_jid_part_len, jid.domain.size()));
+ rc = static_cast<Stringprep_rc>(::stringprep(domain, max_jid_part_len,
+ static_cast<Stringprep_profile_flags>(0), stringprep_nameprep));
+ if (rc != STRINGPREP_OK)
+ {
+ log_error(error_msg + stringprep_strerror(rc));
+ return "";
+ }
+ std::replace_if(std::begin(domain), domain + ::strlen(domain),
+ [](const char c) -> bool
+ {
+ return !((c >= 'a' && c <= 'z') || c == '-' ||
+ (c >= '0' && c <= '9') || c == '.');
+ }, '-');
+
+ // If there is no resource, stop here
+ if (jid.resource.empty())
+ {
+ std::get<0>(cached)->second = std::string(local) + "@" + domain;
+ return std::get<0>(cached)->second;
+ }
+
+ // Otherwise, also process the resource part
+ char resource[max_jid_part_len] = {};
+ memcpy(resource, jid.resource.data(), std::min(max_jid_part_len, jid.resource.size()));
+ rc = static_cast<Stringprep_rc>(::stringprep(resource, max_jid_part_len,
+ static_cast<Stringprep_profile_flags>(0), stringprep_xmpp_resourceprep));
+ if (rc != STRINGPREP_OK)
+ {
+ log_error(error_msg + stringprep_strerror(rc));
+ return "";
+ }
+ std::get<0>(cached)->second = std::string(local) + "@" + domain + "/" + resource;
+ return std::get<0>(cached)->second;
+
+#else
+ (void)original;
+ return "";
+#endif
+}
diff --git a/louloulibs/xmpp/jid.hpp b/louloulibs/xmpp/jid.hpp
new file mode 100644
index 0000000..08327ef
--- /dev/null
+++ b/louloulibs/xmpp/jid.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+
+#include <string>
+
+/**
+ * Parse a JID into its different subart
+ */
+class Jid
+{
+public:
+ explicit Jid(const std::string& jid);
+
+ Jid(const Jid&) = delete;
+ Jid(Jid&&) = delete;
+ Jid& operator=(const Jid&) = delete;
+ Jid& operator=(Jid&&) = delete;
+
+ std::string domain;
+ std::string local;
+ std::string resource;
+
+ std::string bare() const
+ {
+ return this->local + "@" + this->domain;
+ }
+ std::string full() const
+ {
+ return this->local + "@" + this->domain + "/" + this->resource;
+ }
+};
+
+/**
+ * Prepare the given UTF-8 string according to the XMPP node stringprep
+ * identifier profile. This is used to send properly-formed JID to the XMPP
+ * server.
+ *
+ * If the stringprep library is not found, we return an empty string. When
+ * this function is used, the result must always be checked for an empty
+ * value, and if this is the case it must not be used as a JID.
+ */
+std::string jidprep(const std::string& original);
+
+
diff --git a/louloulibs/xmpp/roster.cpp b/louloulibs/xmpp/roster.cpp
new file mode 100644
index 0000000..a14a384
--- /dev/null
+++ b/louloulibs/xmpp/roster.cpp
@@ -0,0 +1,21 @@
+#include <xmpp/roster.hpp>
+
+RosterItem::RosterItem(const std::string& jid, const std::string& name,
+ std::vector<std::string>& groups):
+ jid(jid),
+ name(name),
+ groups(groups)
+{
+}
+
+RosterItem::RosterItem(const std::string& jid, const std::string& name):
+ jid(jid),
+ name(name),
+ groups{}
+{
+}
+
+void Roster::clear()
+{
+ this->items.clear();
+}
diff --git a/louloulibs/xmpp/roster.hpp b/louloulibs/xmpp/roster.hpp
new file mode 100644
index 0000000..aa1b449
--- /dev/null
+++ b/louloulibs/xmpp/roster.hpp
@@ -0,0 +1,71 @@
+#pragma once
+
+
+#include <algorithm>
+#include <string>
+#include <vector>
+
+class RosterItem
+{
+public:
+ RosterItem(const std::string& jid, const std::string& name,
+ std::vector<std::string>& groups);
+ RosterItem(const std::string& jid, const std::string& name);
+ RosterItem() = default;
+ ~RosterItem() = default;
+ RosterItem(const RosterItem&) = default;
+ RosterItem(RosterItem&&) = default;
+ RosterItem& operator=(const RosterItem&) = default;
+ RosterItem& operator=(RosterItem&&) = default;
+
+ std::string jid;
+ std::string name;
+ std::vector<std::string> groups;
+
+private:
+};
+
+/**
+ * Keep track of the last known stat of a JID's roster
+ */
+class Roster
+{
+public:
+ Roster() = default;
+ ~Roster() = default;
+
+ void clear();
+
+ template <typename... ArgsType>
+ RosterItem* add_item(ArgsType&&... args)
+ {
+ this->items.emplace_back(std::forward<ArgsType>(args)...);
+ auto it = this->items.end() - 1;
+ return &*it;
+ }
+ RosterItem* get_item(const std::string& jid)
+ {
+ auto it = std::find_if(this->items.begin(), this->items.end(),
+ [this, &jid](const auto& item)
+ {
+ return item.jid == jid;
+ });
+ if (it != this->items.end())
+ return &*it;
+ return nullptr;
+ }
+ const std::vector<RosterItem>& get_items() const
+ {
+ return this->items;
+ }
+
+private:
+ std::vector<RosterItem> items;
+
+ Roster(const Roster&) = delete;
+ Roster(Roster&&) = delete;
+ Roster& operator=(const Roster&) = delete;
+ Roster& operator=(Roster&&) = delete;
+};
+
+
diff --git a/louloulibs/xmpp/xmpp_component.cpp b/louloulibs/xmpp/xmpp_component.cpp
new file mode 100644
index 0000000..e87cdf7
--- /dev/null
+++ b/louloulibs/xmpp/xmpp_component.cpp
@@ -0,0 +1,664 @@
+#include <utils/timed_events.hpp>
+#include <utils/scopeguard.hpp>
+#include <utils/tolower.hpp>
+#include <logger/logger.hpp>
+
+#include <xmpp/xmpp_component.hpp>
+#include <config/config.hpp>
+#include <xmpp/jid.hpp>
+#include <utils/sha1.hpp>
+
+#include <stdexcept>
+#include <iostream>
+#include <set>
+
+#include <stdio.h>
+
+#include <uuid.h>
+
+#include <louloulibs.h>
+#ifdef SYSTEMD_FOUND
+# include <systemd/sd-daemon.h>
+#endif
+
+using namespace std::string_literals;
+
+static std::set<std::string> kickable_errors{
+ "gone",
+ "internal-server-error",
+ "item-not-found",
+ "jid-malformed",
+ "recipient-unavailable",
+ "redirect",
+ "remote-server-not-found",
+ "remote-server-timeout",
+ "service-unavailable",
+ "malformed-error"
+ };
+
+XmppComponent::XmppComponent(std::shared_ptr<Poller> poller, const std::string& hostname, const std::string& secret):
+ TCPSocketHandler(poller),
+ ever_auth(false),
+ first_connection_try(true),
+ secret(secret),
+ authenticated(false),
+ doc_open(false),
+ served_hostname(hostname),
+ stanza_handlers{},
+ adhoc_commands_handler(*this)
+{
+ this->parser.add_stream_open_callback(std::bind(&XmppComponent::on_remote_stream_open, this,
+ std::placeholders::_1));
+ this->parser.add_stanza_callback(std::bind(&XmppComponent::on_stanza, this,
+ std::placeholders::_1));
+ this->parser.add_stream_close_callback(std::bind(&XmppComponent::on_remote_stream_close, this,
+ std::placeholders::_1));
+ this->stanza_handlers.emplace("handshake",
+ std::bind(&XmppComponent::handle_handshake, this,std::placeholders::_1));
+ this->stanza_handlers.emplace("error",
+ std::bind(&XmppComponent::handle_error, this,std::placeholders::_1));
+}
+
+void XmppComponent::start()
+{
+ this->connect(Config::get("xmpp_server_ip", "127.0.0.1"), Config::get("port", "5347"), false);
+}
+
+bool XmppComponent::is_document_open() const
+{
+ return this->doc_open;
+}
+
+void XmppComponent::send_stanza(const Stanza& stanza)
+{
+ std::string str = stanza.to_string();
+ log_debug("XMPP SENDING: ", str);
+ this->send_data(std::move(str));
+}
+
+void XmppComponent::on_connection_failed(const std::string& reason)
+{
+ this->first_connection_try = false;
+ log_error("Failed to connect to the XMPP server: ", reason);
+#ifdef SYSTEMD_FOUND
+ sd_notifyf(0, "STATUS=Failed to connect to the XMPP server: %s", reason.data());
+#endif
+}
+
+void XmppComponent::on_connected()
+{
+ log_info("connected to XMPP server");
+ this->first_connection_try = true;
+ auto data = "<stream:stream to='"s + this->served_hostname + \
+ "' xmlns:stream='http://etherx.jabber.org/streams' xmlns='" COMPONENT_NS "'>";
+ log_debug("XMPP SENDING: ", data);
+ this->send_data(std::move(data));
+ this->doc_open = true;
+ // We may have some pending data to send: this happens when we try to send
+ // some data before we are actually connected. We send that data right now, if any
+ this->send_pending_data();
+}
+
+void XmppComponent::on_connection_close(const std::string& error)
+{
+ if (error.empty())
+ log_info("XMPP server closed connection");
+ else
+ log_info("XMPP server closed connection: ", error);
+}
+
+void XmppComponent::parse_in_buffer(const size_t size)
+{
+ if (!this->in_buf.empty())
+ { // This may happen if the parser could not allocate enough space for
+ // us. We try to feed it the data that was read into our in_buf
+ // instead. If this fails again we are in trouble.
+ this->parser.feed(this->in_buf.data(), this->in_buf.size(), false);
+ this->in_buf.clear();
+ }
+ else
+ { // Just tell the parser to parse the data that was placed into the
+ // buffer it provided to us with GetBuffer
+ this->parser.parse(size, false);
+ }
+}
+
+void XmppComponent::on_remote_stream_open(const XmlNode& node)
+{
+ log_debug("XMPP RECEIVING: ", node.to_string());
+ this->stream_id = node.get_tag("id");
+ if (this->stream_id.empty())
+ {
+ log_error("Error: no attribute 'id' found");
+ this->send_stream_error("bad-format", "missing 'id' attribute");
+ this->close_document();
+ return ;
+ }
+
+ // Try to authenticate
+ char digest[HASH_LENGTH * 2 + 1];
+ sha1nfo sha1;
+ sha1_init(&sha1);
+ sha1_write(&sha1, this->stream_id.data(), this->stream_id.size());
+ sha1_write(&sha1, this->secret.data(), this->secret.size());
+ const uint8_t* result = sha1_result(&sha1);
+ for (int i=0; i < HASH_LENGTH; i++)
+ sprintf(digest + (i*2), "%02x", result[i]);
+ digest[HASH_LENGTH * 2] = '\0';
+
+ auto data = "<handshake xmlns='" COMPONENT_NS "'>"s + digest + "</handshake>";
+ log_debug("XMPP SENDING: ", data);
+ this->send_data(std::move(data));
+}
+
+void XmppComponent::on_remote_stream_close(const XmlNode& node)
+{
+ log_debug("XMPP RECEIVING: ", node.to_string());
+ this->doc_open = false;
+}
+
+void XmppComponent::reset()
+{
+ this->parser.reset();
+}
+
+void XmppComponent::on_stanza(const Stanza& stanza)
+{
+ log_debug("XMPP RECEIVING: ", stanza.to_string());
+ std::function<void(const Stanza&)> handler;
+ try
+ {
+ handler = this->stanza_handlers.at(stanza.get_name());
+ }
+ catch (const std::out_of_range& exception)
+ {
+ log_warning("No handler for stanza of type ", stanza.get_name());
+ return;
+ }
+ handler(stanza);
+}
+
+void XmppComponent::send_stream_error(const std::string& name, const std::string& explanation)
+{
+ XmlNode node("stream:error", nullptr);
+ XmlNode error(name, nullptr);
+ error["xmlns"] = STREAM_NS;
+ if (!explanation.empty())
+ error.set_inner(explanation);
+ node.add_child(std::move(error));
+ this->send_stanza(node);
+}
+
+void XmppComponent::send_stanza_error(const std::string& kind, const std::string& to, const std::string& from,
+ const std::string& id, const std::string& error_type,
+ const std::string& defined_condition, const std::string& text,
+ const bool fulljid)
+{
+ Stanza node(kind);
+ if (!to.empty())
+ node["to"] = to;
+ if (!from.empty())
+ {
+ if (fulljid)
+ node["from"] = from;
+ else
+ node["from"] = from + "@" + this->served_hostname;
+ }
+ if (!id.empty())
+ node["id"] = id;
+ node["type"] = "error";
+ XmlNode error("error");
+ error["type"] = error_type;
+ XmlNode inner_error(defined_condition);
+ inner_error["xmlns"] = STANZA_NS;
+ error.add_child(std::move(inner_error));
+ if (!text.empty())
+ {
+ XmlNode text_node("text");
+ text_node["xmlns"] = STANZA_NS;
+ text_node.set_inner(text);
+ error.add_child(std::move(text_node));
+ }
+ node.add_child(std::move(error));
+ this->send_stanza(node);
+}
+
+void XmppComponent::close_document()
+{
+ log_debug("XMPP SENDING: </stream:stream>");
+ this->send_data("</stream:stream>");
+ this->doc_open = false;
+}
+
+void XmppComponent::handle_handshake(const Stanza& stanza)
+{
+ (void)stanza;
+ this->authenticated = true;
+ this->ever_auth = true;
+ log_info("Authenticated with the XMPP server");
+#ifdef SYSTEMD_FOUND
+ sd_notify(0, "READY=1");
+ // Install an event that sends a keepalive to systemd. If biboumi crashes
+ // or hangs for too long, systemd will restart it.
+ uint64_t usec;
+ if (sd_watchdog_enabled(0, &usec) > 0)
+ {
+ TimedEventsManager::instance().add_event(TimedEvent(
+ std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::microseconds(usec / 2)),
+ []() { sd_notify(0, "WATCHDOG=1"); }));
+ }
+#endif
+ this->after_handshake();
+}
+
+void XmppComponent::handle_error(const Stanza& stanza)
+{
+ const XmlNode* text = stanza.get_child("text", STREAMS_NS);
+ std::string error_message("Unspecified error");
+ if (text)
+ error_message = text->get_inner();
+ log_error("Stream error received from the XMPP server: ", error_message);
+#ifdef SYSTEMD_FOUND
+ if (!this->ever_auth)
+ sd_notifyf(0, "STATUS=Failed to authenticate to the XMPP server: %s", error_message.data());
+#endif
+
+}
+
+void* XmppComponent::get_receive_buffer(const size_t size) const
+{
+ return this->parser.get_buffer(size);
+}
+
+void XmppComponent::send_message(const std::string& from, Xmpp::body&& body, const std::string& to, const std::string& type, const bool fulljid)
+{
+ XmlNode node("message");
+ node["to"] = to;
+ if (fulljid)
+ node["from"] = from;
+ else
+ node["from"] = from + "@" + this->served_hostname;
+ if (!type.empty())
+ node["type"] = type;
+ XmlNode body_node("body");
+ body_node.set_inner(std::get<0>(body));
+ node.add_child(std::move(body_node));
+ if (std::get<1>(body))
+ {
+ XmlNode html("html");
+ html["xmlns"] = XHTMLIM_NS;
+ // Pass the ownership of the pointer to this xmlnode
+ html.add_child(std::move(std::get<1>(body)));
+ node.add_child(std::move(html));
+ }
+ this->send_stanza(node);
+}
+
+void XmppComponent::send_user_join(const std::string& from,
+ const std::string& nick,
+ const std::string& realjid,
+ const std::string& affiliation,
+ const std::string& role,
+ const std::string& to,
+ const bool self)
+{
+ XmlNode node("presence");
+ node["to"] = to;
+ node["from"] = from + "@" + this->served_hostname + "/" + nick;
+
+ XmlNode x("x");
+ x["xmlns"] = MUC_USER_NS;
+
+ XmlNode item("item");
+ if (!affiliation.empty())
+ item["affiliation"] = affiliation;
+ if (!role.empty())
+ item["role"] = role;
+ if (!realjid.empty())
+ {
+ const std::string preped_jid = jidprep(realjid);
+ if (!preped_jid.empty())
+ item["jid"] = preped_jid;
+ }
+ x.add_child(std::move(item));
+
+ if (self)
+ {
+ XmlNode status("status");
+ status["code"] = "110";
+ x.add_child(std::move(status));
+ }
+ node.add_child(std::move(x));
+ this->send_stanza(node);
+}
+
+void XmppComponent::send_invalid_room_error(const std::string& muc_name,
+ const std::string& nick,
+ const std::string& to)
+{
+ Stanza presence("presence");
+ if (!muc_name.empty())
+ presence["from"] = muc_name + "@" + this->served_hostname + "/" + nick;
+ else
+ presence["from"] = this->served_hostname;
+ presence["to"] = to;
+ presence["type"] = "error";
+ XmlNode x("x");
+ x["xmlns"] = MUC_NS;
+ presence.add_child(std::move(x));
+ XmlNode error("error");
+ error["by"] = muc_name + "@" + this->served_hostname;
+ error["type"] = "cancel";
+ XmlNode item_not_found("item-not-found");
+ item_not_found["xmlns"] = STANZA_NS;
+ error.add_child(std::move(item_not_found));
+ XmlNode text("text");
+ text["xmlns"] = STANZA_NS;
+ text["xml:lang"] = "en";
+ text.set_inner(muc_name +
+ " is not a valid IRC channel name. A correct room jid is of the form: #<chan>%<server>@" +
+ this->served_hostname);
+ error.add_child(std::move(text));
+ presence.add_child(std::move(error));
+ this->send_stanza(presence);
+}
+
+void XmppComponent::send_invalid_user_error(const std::string& user_name, const std::string& to)
+{
+ Stanza message("message");
+ message["from"] = user_name + "@" + this->served_hostname;
+ message["to"] = to;
+ message["type"] = "error";
+ XmlNode x("x");
+ x["xmlns"] = MUC_NS;
+ message.add_child(std::move(x));
+ XmlNode error("error");
+ error["type"] = "cancel";
+ XmlNode item_not_found("item-not-found");
+ item_not_found["xmlns"] = STANZA_NS;
+ error.add_child(std::move(item_not_found));
+ XmlNode text("text");
+ text["xmlns"] = STANZA_NS;
+ text["xml:lang"] = "en";
+ text.set_inner(user_name +
+ " is not a valid IRC user name. A correct user jid is of the form: <nick>!<server>@" +
+ this->served_hostname);
+ error.add_child(std::move(text));
+ message.add_child(std::move(error));
+ this->send_stanza(message);
+}
+
+void XmppComponent::send_topic(const std::string& from, Xmpp::body&& topic, const std::string& to, const std::string& who)
+{
+ XmlNode message("message");
+ message["to"] = to;
+ if (who.empty())
+ message["from"] = from + "@" + this->served_hostname;
+ else
+ message["from"] = from + "@" + this->served_hostname + "/" + who;
+ message["type"] = "groupchat";
+ XmlNode subject("subject");
+ subject.set_inner(std::get<0>(topic));
+ message.add_child(std::move(subject));
+ this->send_stanza(message);
+}
+
+void XmppComponent::send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& xmpp_body, const std::string& jid_to)
+{
+ Stanza message("message");
+ message["to"] = jid_to;
+ if (!nick.empty())
+ message["from"] = muc_name + "@" + this->served_hostname + "/" + nick;
+ else // Message from the room itself
+ message["from"] = muc_name + "@" + this->served_hostname;
+ message["type"] = "groupchat";
+ XmlNode body("body");
+ body.set_inner(std::get<0>(xmpp_body));
+ message.add_child(std::move(body));
+ if (std::get<1>(xmpp_body))
+ {
+ XmlNode html("html");
+ html["xmlns"] = XHTMLIM_NS;
+ // Pass the ownership of the pointer to this xmlnode
+ html.add_child(std::move(std::get<1>(xmpp_body)));
+ message.add_child(std::move(html));
+ }
+ this->send_stanza(message);
+}
+
+void XmppComponent::send_muc_leave(const std::string& muc_name, std::string&& nick, Xmpp::body&& message, const std::string& jid_to, const bool self)
+{
+ Stanza presence("presence");
+ presence["to"] = jid_to;
+ presence["from"] = muc_name + "@" + this->served_hostname + "/" + nick;
+ presence["type"] = "unavailable";
+ const std::string message_str = std::get<0>(message);
+ XmlNode x("x");
+ x["xmlns"] = MUC_USER_NS;
+ if (self)
+ {
+ XmlNode status("status");
+ status["code"] = "110";
+ x.add_child(std::move(status));
+ }
+ presence.add_child(std::move(x));
+ if (!message_str.empty())
+ {
+ XmlNode status("status");
+ status.set_inner(message_str);
+ presence.add_child(std::move(status));
+ }
+ this->send_stanza(presence);
+}
+
+void XmppComponent::send_nick_change(const std::string& muc_name,
+ const std::string& old_nick,
+ const std::string& new_nick,
+ const std::string& affiliation,
+ const std::string& role,
+ const std::string& jid_to,
+ const bool self)
+{
+ Stanza presence("presence");
+ presence["to"] = jid_to;
+ presence["from"] = muc_name + "@" + this->served_hostname + "/" + old_nick;
+ presence["type"] = "unavailable";
+ XmlNode x("x");
+ x["xmlns"] = MUC_USER_NS;
+ XmlNode item("item");
+ item["nick"] = new_nick;
+ x.add_child(std::move(item));
+ XmlNode status("status");
+ status["code"] = "303";
+ x.add_child(std::move(status));
+ if (self)
+ {
+ XmlNode status2("status");
+ status2["code"] = "110";
+ x.add_child(std::move(status2));
+ }
+ presence.add_child(std::move(x));
+ this->send_stanza(presence);
+
+ this->send_user_join(muc_name, new_nick, "", affiliation, role, jid_to, self);
+}
+
+void XmppComponent::kick_user(const std::string& muc_name,
+ const std::string& target,
+ const std::string& txt,
+ const std::string& author,
+ const std::string& jid_to)
+{
+ Stanza presence("presence");
+ presence["from"] = muc_name + "@" + this->served_hostname + "/" + target;
+ presence["to"] = jid_to;
+ presence["type"] = "unavailable";
+ XmlNode x("x");
+ x["xmlns"] = MUC_USER_NS;
+ XmlNode item("item");
+ item["affiliation"] = "none";
+ item["role"] = "none";
+ XmlNode actor("actor");
+ actor["nick"] = author;
+ actor["jid"] = author; // backward compatibility with old clients
+ item.add_child(std::move(actor));
+ XmlNode reason("reason");
+ reason.set_inner(txt);
+ item.add_child(std::move(reason));
+ x.add_child(std::move(item));
+ XmlNode status("status");
+ status["code"] = "307";
+ x.add_child(std::move(status));
+ presence.add_child(std::move(x));
+ this->send_stanza(presence);
+}
+
+void XmppComponent::send_presence_error(const std::string& muc_name,
+ const std::string& nickname,
+ const std::string& jid_to,
+ const std::string& type,
+ const std::string& condition,
+ const std::string& error_code,
+ const std::string& /* text */)
+{
+ Stanza presence("presence");
+ presence["from"] = muc_name + "@" + this->served_hostname + "/" + nickname;
+ presence["to"] = jid_to;
+ presence["type"] = "error";
+ XmlNode x("x");
+ x["xmlns"] = MUC_NS;
+ presence.add_child(std::move(x));
+ XmlNode error("error");
+ error["by"] = muc_name + "@" + this->served_hostname;
+ error["type"] = type;
+ if (!error_code.empty())
+ error["code"] = error_code;
+ XmlNode subnode(condition);
+ subnode["xmlns"] = STANZA_NS;
+ error.add_child(std::move(subnode));
+ presence.add_child(std::move(error));
+ this->send_stanza(presence);
+}
+
+void XmppComponent::send_affiliation_role_change(const std::string& muc_name,
+ const std::string& target,
+ const std::string& affiliation,
+ const std::string& role,
+ const std::string& jid_to)
+{
+ Stanza presence("presence");
+ presence["from"] = muc_name + "@" + this->served_hostname + "/" + target;
+ presence["to"] = jid_to;
+ XmlNode x("x");
+ x["xmlns"] = MUC_USER_NS;
+ XmlNode item("item");
+ item["affiliation"] = affiliation;
+ item["role"] = role;
+ x.add_child(std::move(item));
+ presence.add_child(std::move(x));
+ this->send_stanza(presence);
+}
+
+void XmppComponent::send_version(const std::string& id, const std::string& jid_to, const std::string& jid_from,
+ const std::string& version)
+{
+ Stanza iq("iq");
+ iq["type"] = "result";
+ iq["id"] = id;
+ iq["to"] = jid_to;
+ iq["from"] = jid_from;
+ XmlNode query("query");
+ query["xmlns"] = VERSION_NS;
+ if (version.empty())
+ {
+ XmlNode name("name");
+ name.set_inner("biboumi");
+ query.add_child(std::move(name));
+ XmlNode version("version");
+ version.set_inner(SOFTWARE_VERSION);
+ query.add_child(std::move(version));
+ XmlNode os("os");
+ os.set_inner(SYSTEM_NAME);
+ query.add_child(std::move(os));
+ }
+ else
+ {
+ XmlNode name("name");
+ name.set_inner(version);
+ query.add_child(std::move(name));
+ }
+ iq.add_child(std::move(query));
+ this->send_stanza(iq);
+}
+
+void XmppComponent::send_adhoc_commands_list(const std::string& id, const std::string& requester_jid,
+ const std::string& from_jid,
+ const bool with_admin_only, const AdhocCommandsHandler& adhoc_handler)
+{
+ Stanza iq("iq");
+ iq["type"] = "result";
+ iq["id"] = id;
+ iq["to"] = requester_jid;
+ iq["from"] = from_jid;
+ XmlNode query("query");
+ query["xmlns"] = DISCO_ITEMS_NS;
+ query["node"] = ADHOC_NS;
+ for (const auto& kv: adhoc_handler.get_commands())
+ {
+ if (kv.second.is_admin_only() && !with_admin_only)
+ continue;
+ XmlNode item("item");
+ item["jid"] = from_jid;
+ item["node"] = kv.first;
+ item["name"] = kv.second.name;
+ query.add_child(std::move(item));
+ }
+ iq.add_child(std::move(query));
+ this->send_stanza(iq);
+}
+
+void XmppComponent::send_iq_version_request(const std::string& from,
+ const std::string& jid_to)
+{
+ Stanza iq("iq");
+ iq["type"] = "get";
+ iq["id"] = "version_"s + XmppComponent::next_id();
+ iq["from"] = from + "@" + this->served_hostname;
+ iq["to"] = jid_to;
+ XmlNode query("query");
+ query["xmlns"] = VERSION_NS;
+ iq.add_child(std::move(query));
+ this->send_stanza(iq);
+}
+
+void XmppComponent::send_iq_result_full_jid(const std::string& id, const std::string& to_jid, const std::string& from_full_jid)
+{
+ Stanza iq("iq");
+ iq["from"] = from_full_jid;
+ iq["to"] = to_jid;
+ iq["id"] = id;
+ iq["type"] = "result";
+ this->send_stanza(iq);
+}
+
+void XmppComponent::send_iq_result(const std::string& id, const std::string& to_jid, const std::string& from_local_part)
+{
+ Stanza iq("iq");
+ if (!from_local_part.empty())
+ iq["from"] = from_local_part + "@" + this->served_hostname;
+ else
+ iq["from"] = this->served_hostname;
+ iq["to"] = to_jid;
+ iq["id"] = id;
+ iq["type"] = "result";
+ this->send_stanza(iq);
+}
+
+std::string XmppComponent::next_id()
+{
+ char uuid_str[37];
+ uuid_t uuid;
+ uuid_generate(uuid);
+ uuid_unparse(uuid, uuid_str);
+ return uuid_str;
+}
diff --git a/louloulibs/xmpp/xmpp_component.hpp b/louloulibs/xmpp/xmpp_component.hpp
new file mode 100644
index 0000000..5fc6d2e
--- /dev/null
+++ b/louloulibs/xmpp/xmpp_component.hpp
@@ -0,0 +1,243 @@
+#pragma once
+
+
+#include <xmpp/adhoc_commands_handler.hpp>
+#include <network/tcp_socket_handler.hpp>
+#include <xmpp/xmpp_parser.hpp>
+#include <xmpp/body.hpp>
+
+#include <unordered_map>
+#include <memory>
+#include <string>
+#include <map>
+
+#define STREAM_NS "http://etherx.jabber.org/streams"
+#define COMPONENT_NS "jabber:component:accept"
+#define MUC_NS "http://jabber.org/protocol/muc"
+#define MUC_USER_NS MUC_NS"#user"
+#define MUC_ADMIN_NS MUC_NS"#admin"
+#define DISCO_NS "http://jabber.org/protocol/disco"
+#define DISCO_ITEMS_NS DISCO_NS"#items"
+#define DISCO_INFO_NS DISCO_NS"#info"
+#define XHTMLIM_NS "http://jabber.org/protocol/xhtml-im"
+#define STANZA_NS "urn:ietf:params:xml:ns:xmpp-stanzas"
+#define STREAMS_NS "urn:ietf:params:xml:ns:xmpp-streams"
+#define VERSION_NS "jabber:iq:version"
+#define ADHOC_NS "http://jabber.org/protocol/commands"
+#define PING_NS "urn:xmpp:ping"
+
+/**
+ * An XMPP component, communicating with an XMPP server using the protocole
+ * described in XEP-0114: Jabber Component Protocol
+ *
+ * TODO: implement XEP-0225: Component Connections
+ */
+class XmppComponent: public TCPSocketHandler
+{
+public:
+ explicit XmppComponent(std::shared_ptr<Poller> poller, const std::string& hostname, const std::string& secret);
+ virtual ~XmppComponent() = default;
+
+ XmppComponent(const XmppComponent&) = delete;
+ XmppComponent(XmppComponent&&) = delete;
+ XmppComponent& operator=(const XmppComponent&) = delete;
+ XmppComponent& operator=(XmppComponent&&) = delete;
+
+ void on_connection_failed(const std::string& reason) override final;
+ void on_connected() override final;
+ void on_connection_close(const std::string& error) override final;
+ void parse_in_buffer(const size_t size) override final;
+
+ /**
+ * Returns a unique id, to be used in the 'id' element of our iq stanzas.
+ */
+ static std::string next_id();
+ bool is_document_open() const;
+ /**
+ * Connect to the XMPP server.
+ */
+ void start();
+ /**
+ * Reset the component so we can use the component on a new XMPP stream
+ */
+ void reset();
+ /**
+ * Serialize the stanza and add it to the out_buf to be sent to the
+ * server.
+ */
+ void send_stanza(const Stanza& stanza);
+ /**
+ * Handle the opening of the remote stream
+ */
+ void on_remote_stream_open(const XmlNode& node);
+ /**
+ * Handle the closing of the remote stream
+ */
+ void on_remote_stream_close(const XmlNode& node);
+ /**
+ * Handle received stanzas
+ */
+ void on_stanza(const Stanza& stanza);
+ /**
+ * Send an error stanza. Message being the name of the element inside the
+ * stanza, and explanation being a short human-readable sentence
+ * describing the error.
+ */
+ void send_stream_error(const std::string& message, const std::string& explanation);
+ /**
+ * Send error stanza, described in http://xmpp.org/rfcs/rfc6120.html#stanzas-error
+ */
+ void send_stanza_error(const std::string& kind, const std::string& to, const std::string& from,
+ const std::string& id, const std::string& error_type,
+ const std::string& defined_condition, const std::string& text,
+ const bool fulljid=true);
+ /**
+ * Send the closing signal for our document (not closing the connection though).
+ */
+ void close_document();
+ /**
+ * Send a message from from@served_hostname, with the given body
+ *
+ * If fulljid is false, the provided 'from' doesn't contain the
+ * server-part of the JID and must be added.
+ */
+ void send_message(const std::string& from, Xmpp::body&& body,
+ const std::string& to, const std::string& type,
+ const bool fulljid=false);
+ /**
+ * Send a join from a new participant
+ */
+ void send_user_join(const std::string& from,
+ const std::string& nick,
+ const std::string& realjid,
+ const std::string& affiliation,
+ const std::string& role,
+ const std::string& to,
+ const bool self);
+ /**
+ * Send an error to indicate that the user tried to join an invalid room
+ */
+ void send_invalid_room_error(const std::string& muc_jid,
+ const std::string& nick,
+ const std::string& to);
+ /**
+ * Send an error to indicate that the user tried to send a message to an
+ * invalid user.
+ */
+ void send_invalid_user_error(const std::string& user_name,
+ const std::string& to);
+ /**
+ * Send the MUC topic to the user
+ */
+ void send_topic(const std::string& from, Xmpp::body&& xmpp_topic, const std::string& to, const std::string& who);
+ /**
+ * Send a (non-private) message to the MUC
+ */
+ void send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& body, const std::string& jid_to);
+ /**
+ * Send an unavailable presence for this nick
+ */
+ void send_muc_leave(const std::string& muc_name, std::string&& nick, Xmpp::body&& message, const std::string& jid_to, const bool self);
+ /**
+ * Indicate that a participant changed his nick
+ */
+ void send_nick_change(const std::string& muc_name,
+ const std::string& old_nick,
+ const std::string& new_nick,
+ const std::string& affiliation,
+ const std::string& role,
+ const std::string& jid_to,
+ const bool self);
+ /**
+ * An user is kicked from a room
+ */
+ void kick_user(const std::string& muc_name,
+ const std::string& target,
+ const std::string& reason,
+ const std::string& author,
+ const std::string& jid_to);
+ /**
+ * Send a generic presence error
+ */
+ void send_presence_error(const std::string& muc_name,
+ const std::string& nickname,
+ const std::string& jid_to,
+ const std::string& type,
+ const std::string& condition,
+ const std::string& error_code,
+ const std::string& text);
+ /**
+ * Send a presence from the MUC indicating a change in the role and/or
+ * affiliation of a participant
+ */
+ void send_affiliation_role_change(const std::string& muc_name,
+ const std::string& target,
+ const std::string& affiliation,
+ const std::string& role,
+ const std::string& jid_to);
+ /**
+ * Send a result IQ with the gateway disco informations.
+ */
+ void send_self_disco_info(const std::string& id, const std::string& jid_to);
+ /**
+ * Send a result IQ with the given version, or the gateway version if the
+ * passed string is empty.
+ */
+ void send_version(const std::string& id, const std::string& jid_to, const std::string& jid_from,
+ const std::string& version="");
+ /**
+ * Send the list of all available ad-hoc commands to that JID. The list is
+ * different depending on what JID made the request.
+ */
+ void send_adhoc_commands_list(const std::string& id, const std::string& requester_jid, const std::string& from_jid,
+ const bool with_admin_only, const AdhocCommandsHandler& adhoc_handler);
+ /**
+ * Send an iq version request
+ */
+ void send_iq_version_request(const std::string& from,
+ const std::string& jid_to);
+ /**
+ * Send an empty iq of type result
+ */
+ void send_iq_result(const std::string& id, const std::string& to_jid, const std::string& from);
+ void send_iq_result_full_jid(const std::string& id, const std::string& to_jid,
+ const std::string& from_full_jid);
+
+ void handle_handshake(const Stanza& stanza);
+ void handle_error(const Stanza& stanza);
+
+ virtual void after_handshake() {}
+
+ /**
+ * Whether or not we ever succeeded our authentication to the XMPP server
+ */
+ bool ever_auth;
+ /**
+ * Whether or not this is the first consecutive try on connecting to the
+ * XMPP server. We use this to delay the connection attempt for a few
+ * seconds, if it is not the first try.
+ */
+ bool first_connection_try;
+
+private:
+ /**
+ * Return a buffer provided by the XML parser, to read data directly into
+ * it, and avoiding some unnecessary copy.
+ */
+ void* get_receive_buffer(const size_t size) const override final;
+ XmppParser parser;
+ std::string stream_id;
+ std::string secret;
+ bool authenticated;
+ /**
+ * Whether or not OUR XMPP document is open
+ */
+ bool doc_open;
+protected:
+ std::string served_hostname;
+
+ std::unordered_map<std::string, std::function<void(const Stanza&)>> stanza_handlers;
+ AdhocCommandsHandler adhoc_commands_handler;
+};
+
+
diff --git a/louloulibs/xmpp/xmpp_parser.cpp b/louloulibs/xmpp/xmpp_parser.cpp
new file mode 100644
index 0000000..0488be9
--- /dev/null
+++ b/louloulibs/xmpp/xmpp_parser.cpp
@@ -0,0 +1,172 @@
+#include <xmpp/xmpp_parser.hpp>
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <logger/logger.hpp>
+
+/**
+ * Expat handlers. Called by the Expat library, never by ourself.
+ * They just forward the call to the XmppParser corresponding methods.
+ */
+
+static void start_element_handler(void* user_data, const XML_Char* name, const XML_Char** atts)
+{
+ static_cast<XmppParser*>(user_data)->start_element(name, atts);
+}
+
+static void end_element_handler(void* user_data, const XML_Char* name)
+{
+ static_cast<XmppParser*>(user_data)->end_element(name);
+}
+
+static void character_data_handler(void *user_data, const XML_Char *s, int len)
+{
+ static_cast<XmppParser*>(user_data)->char_data(s, len);
+}
+
+/**
+ * XmppParser class
+ */
+
+XmppParser::XmppParser():
+ level(0),
+ current_node(nullptr),
+ root(nullptr)
+{
+ this->init_xml_parser();
+}
+
+void XmppParser::init_xml_parser()
+{
+ // Create the expat parser
+ this->parser = XML_ParserCreateNS("UTF-8", ':');
+ XML_SetUserData(this->parser, static_cast<void*>(this));
+
+ // Install Expat handlers
+ XML_SetElementHandler(this->parser, &start_element_handler, &end_element_handler);
+ XML_SetCharacterDataHandler(this->parser, &character_data_handler);
+}
+
+XmppParser::~XmppParser()
+{
+ XML_ParserFree(this->parser);
+}
+
+int XmppParser::feed(const char* data, const int len, const bool is_final)
+{
+ int res = XML_Parse(this->parser, data, len, is_final);
+ if (res == XML_STATUS_ERROR &&
+ (XML_GetErrorCode(this->parser) != XML_ERROR_FINISHED))
+ log_error("Xml_Parse encountered an error: ",
+ XML_ErrorString(XML_GetErrorCode(this->parser)));
+ return res;
+}
+
+int XmppParser::parse(const int len, const bool is_final)
+{
+ int res = XML_ParseBuffer(this->parser, len, is_final);
+ if (res == XML_STATUS_ERROR)
+ log_error("Xml_Parsebuffer encountered an error: ",
+ XML_ErrorString(XML_GetErrorCode(this->parser)));
+ return res;
+}
+
+void XmppParser::reset()
+{
+ XML_ParserFree(this->parser);
+ this->init_xml_parser();
+ this->current_node = nullptr;
+ this->root.reset(nullptr);
+ this->level = 0;
+}
+
+void* XmppParser::get_buffer(const size_t size) const
+{
+ return XML_GetBuffer(this->parser, static_cast<int>(size));
+}
+
+void XmppParser::start_element(const XML_Char* name, const XML_Char** attribute)
+{
+ this->level++;
+
+ auto new_node = std::make_unique<XmlNode>(name, this->current_node);
+ auto new_node_ptr = new_node.get();
+ if (this->current_node)
+ this->current_node->add_child(std::move(new_node));
+ else
+ this->root = std::move(new_node);
+ this->current_node = new_node_ptr;
+ for (size_t i = 0; attribute[i]; i += 2)
+ this->current_node->set_attribute(attribute[i], attribute[i+1]);
+ if (this->level == 1)
+ this->stream_open_event(*this->current_node);
+}
+
+void XmppParser::end_element(const XML_Char*)
+{
+ this->level--;
+ if (this->level == 0)
+ { // End of the whole stream
+ this->stream_close_event(*this->current_node);
+ this->current_node = nullptr;
+ this->root.reset();
+ }
+ else
+ {
+ auto parent = this->current_node->get_parent();
+ if (this->level == 1)
+ { // End of a stanza
+ this->stanza_event(*this->current_node);
+ // Note: deleting all the children of our parent deletes ourself,
+ // so current_node is an invalid pointer after this line
+ parent->delete_all_children();
+ }
+ this->current_node = parent;
+ }
+}
+
+void XmppParser::char_data(const XML_Char* data, const size_t len)
+{
+ if (this->current_node->has_children())
+ this->current_node->get_last_child()->add_to_tail({data, len});
+ else
+ this->current_node->add_to_inner({data, len});
+}
+
+void XmppParser::stanza_event(const Stanza& stanza) const
+{
+ for (const auto& callback: this->stanza_callbacks)
+ {
+ try {
+ callback(stanza);
+ } catch (const std::exception& e) {
+ log_error("Unhandled exception: ", e.what());
+ }
+ }
+}
+
+void XmppParser::stream_open_event(const XmlNode& node) const
+{
+ for (const auto& callback: this->stream_open_callbacks)
+ callback(node);
+}
+
+void XmppParser::stream_close_event(const XmlNode& node) const
+{
+ for (const auto& callback: this->stream_close_callbacks)
+ callback(node);
+}
+
+void XmppParser::add_stanza_callback(std::function<void(const Stanza&)>&& callback)
+{
+ this->stanza_callbacks.emplace_back(std::move(callback));
+}
+
+void XmppParser::add_stream_open_callback(std::function<void(const XmlNode&)>&& callback)
+{
+ this->stream_open_callbacks.emplace_back(std::move(callback));
+}
+
+void XmppParser::add_stream_close_callback(std::function<void(const XmlNode&)>&& callback)
+{
+ this->stream_close_callbacks.emplace_back(std::move(callback));
+}
diff --git a/louloulibs/xmpp/xmpp_parser.hpp b/louloulibs/xmpp/xmpp_parser.hpp
new file mode 100644
index 0000000..9d67228
--- /dev/null
+++ b/louloulibs/xmpp/xmpp_parser.hpp
@@ -0,0 +1,133 @@
+#pragma once
+
+
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <functional>
+
+#include <expat.h>
+
+/**
+ * A SAX XML parser that builds XML nodes and spawns events when a complete
+ * stanza is received (an element of level 2), or when the document is
+ * opened/closed (an element of level 1)
+ *
+ * After a stanza_event has been spawned, we delete the whole stanza. This
+ * means that even with a very long document (in XMPP the document is
+ * potentially infinite), the memory is never exhausted as long as each
+ * 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
+ * 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"
+ *
+ * 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
+ * 20 (arbitrary number here), and each XML node to have more than 15 childs
+ * (arbitrary number again).
+ */
+class XmppParser
+{
+public:
+ explicit XmppParser();
+ ~XmppParser();
+ XmppParser(const XmppParser&) = delete;
+ XmppParser& operator=(const XmppParser&) = delete;
+ XmppParser(XmppParser&&) = delete;
+ XmppParser& operator=(XmppParser&&) = delete;
+
+public:
+ /**
+ * Feed the parser with some XML data
+ */
+ int feed(const char* data, const int len, const bool is_final);
+ /**
+ * Parse the data placed in the parser buffer
+ */
+ int parse(const int size, const bool is_final);
+ /**
+ * Reset the parser, so it can be used from scratch afterward
+ */
+ void reset();
+ /**
+ * Get a buffer provided by the xml parser.
+ */
+ void* get_buffer(const size_t size) const;
+ /**
+ * Add one callback for the various events that this parser can spawn.
+ */
+ void add_stanza_callback(std::function<void(const Stanza&)>&& callback);
+ void add_stream_open_callback(std::function<void(const XmlNode&)>&& callback);
+ void add_stream_close_callback(std::function<void(const XmlNode&)>&& callback);
+
+ /**
+ * Called when a new XML element has been opened. We instanciate a new
+ * XmlNode and set it as our current node. The parent of this new node is
+ * the previous "current" node. We have all the element's attributes in
+ * this event.
+ *
+ * We spawn a stream_event with this node if this is a level-1 element.
+ */
+ void start_element(const XML_Char* name, const XML_Char** attribute);
+ /**
+ * Called when an XML element has been closed. We close the current_node,
+ * set our current_node as the parent of the current_node, and if that was
+ * a level-2 element we spawn a stanza_event with this node.
+ *
+ * And we then delete the stanza (and everything under it, its children,
+ * attribute, etc).
+ */
+ void end_element(const XML_Char* name);
+ /**
+ * Some inner or tail data has been parsed
+ */
+ void char_data(const XML_Char* data, const size_t len);
+ /**
+ * Calls all the stanza_callbacks one by one.
+ */
+ void stanza_event(const Stanza& stanza) const;
+ /**
+ * Calls all the stream_open_callbacks one by one. Note: the passed node is not
+ * closed yet.
+ */
+ void stream_open_event(const XmlNode& node) const;
+ /**
+ * Calls all the stream_close_callbacks one by one.
+ */
+ void stream_close_event(const XmlNode& node) const;
+
+private:
+ /**
+ * Init the XML parser and install the callbacks
+ */
+ void init_xml_parser();
+
+ /**
+ * Expat structure.
+ */
+ XML_Parser parser;
+ /**
+ * The current depth in the XML document
+ */
+ size_t level;
+ /**
+ * The deepest XML node opened but not yet closed (to which we are adding
+ * new children, inner or tail)
+ */
+ XmlNode* current_node;
+ /**
+ * The root node has no parent, so we keep it here: the XmppParser object
+ * is its owner.
+ */
+ std::unique_ptr<XmlNode> root;
+ /**
+ * A list of callbacks to be called on an *_event, receiving the
+ * concerned Stanza/XmlNode.
+ */
+ std::vector<std::function<void(const Stanza&)>> stanza_callbacks;
+ std::vector<std::function<void(const XmlNode&)>> stream_open_callbacks;
+ std::vector<std::function<void(const XmlNode&)>> stream_close_callbacks;
+};
+
+
diff --git a/louloulibs/xmpp/xmpp_stanza.cpp b/louloulibs/xmpp/xmpp_stanza.cpp
new file mode 100644
index 0000000..ac6ce9b
--- /dev/null
+++ b/louloulibs/xmpp/xmpp_stanza.cpp
@@ -0,0 +1,229 @@
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <utils/encoding.hpp>
+#include <utils/split.hpp>
+
+#include <stdexcept>
+#include <iostream>
+#include <sstream>
+
+#include <string.h>
+
+std::string xml_escape(const std::string& data)
+{
+ std::string res;
+ res.reserve(data.size());
+ for (size_t pos = 0; pos != data.size(); ++pos)
+ {
+ switch(data[pos])
+ {
+ case '&':
+ res += "&amp;";
+ break;
+ case '<':
+ res += "&lt;";
+ break;
+ case '>':
+ res += "&gt;";
+ break;
+ case '\"':
+ res += "&quot;";
+ break;
+ case '\'':
+ res += "&apos;";
+ break;
+ default:
+ res += data[pos];
+ break;
+ }
+ }
+ return res;
+}
+
+std::string sanitize(const std::string& data, const std::string& encoding)
+{
+ if (utils::is_valid_utf8(data.data()))
+ return xml_escape(utils::remove_invalid_xml_chars(data));
+ else
+ return xml_escape(utils::remove_invalid_xml_chars(utils::convert_to_utf8(data, encoding.data())));
+}
+
+XmlNode::XmlNode(const std::string& name, XmlNode* parent):
+ parent(parent)
+{
+ // split the namespace and the name
+ auto n = name.rfind(":");
+ if (n == std::string::npos)
+ this->name = name;
+ else
+ {
+ this->name = name.substr(n+1);
+ this->attributes["xmlns"] = name.substr(0, n);
+ }
+}
+
+XmlNode::XmlNode(const std::string& name):
+ XmlNode(name, nullptr)
+{
+}
+
+void XmlNode::delete_all_children()
+{
+ this->children.clear();
+}
+
+void XmlNode::set_attribute(const std::string& name, const std::string& value)
+{
+ this->attributes[name] = value;
+}
+
+void XmlNode::set_tail(const std::string& data)
+{
+ this->tail = data;
+}
+
+void XmlNode::add_to_tail(const std::string& data)
+{
+ this->tail += data;
+}
+
+void XmlNode::set_inner(const std::string& data)
+{
+ this->inner = data;
+}
+
+void XmlNode::add_to_inner(const std::string& data)
+{
+ this->inner += data;
+}
+
+std::string XmlNode::get_inner() const
+{
+ return this->inner;
+}
+
+std::string XmlNode::get_tail() const
+{
+ return this->tail;
+}
+
+const XmlNode* XmlNode::get_child(const std::string& name, const std::string& xmlns) const
+{
+ for (const auto& child: this->children)
+ {
+ if (child->name == name && child->get_tag("xmlns") == xmlns)
+ return child.get();
+ }
+ return nullptr;
+}
+
+std::vector<const XmlNode*> XmlNode::get_children(const std::string& name, const std::string& xmlns) const
+{
+ std::vector<const XmlNode*> res;
+ for (const auto& child: this->children)
+ {
+ if (child->name == name && child->get_tag("xmlns") == xmlns)
+ res.push_back(child.get());
+ }
+ return res;
+}
+
+XmlNode* XmlNode::add_child(std::unique_ptr<XmlNode> child)
+{
+ child->parent = this;
+ auto ret = child.get();
+ this->children.push_back(std::move(child));
+ return ret;
+}
+
+XmlNode* XmlNode::add_child(XmlNode&& child)
+{
+ auto new_node = std::make_unique<XmlNode>(std::move(child));
+ return this->add_child(std::move(new_node));
+}
+
+XmlNode* XmlNode::add_child(const XmlNode& child)
+{
+ auto new_node = std::make_unique<XmlNode>(child);
+ return this->add_child(std::move(new_node));
+}
+
+XmlNode* XmlNode::get_last_child() const
+{
+ return this->children.back().get();
+}
+
+XmlNode* XmlNode::get_parent() const
+{
+ return this->parent;
+}
+
+void XmlNode::set_name(const std::string& name)
+{
+ this->name = name;
+}
+
+void XmlNode::set_name(std::string&& name)
+{
+ this->name = std::move(name);
+}
+
+const std::string XmlNode::get_name() const
+{
+ return this->name;
+}
+
+std::string XmlNode::to_string() const
+{
+ std::ostringstream res;
+ res << "<" << this->name;
+ for (const auto& it: this->attributes)
+ res << " " << it.first << "='" << sanitize(it.second) + "'";
+ if (!this->has_children() && this->inner.empty())
+ res << "/>";
+ else
+ {
+ res << ">" + sanitize(this->inner);
+ for (const auto& child: this->children)
+ res << child->to_string();
+ res << "</" << this->get_name() << ">";
+ }
+ res << sanitize(this->tail);
+ return res.str();
+}
+
+bool XmlNode::has_children() const
+{
+ return !this->children.empty();
+}
+
+const std::string& XmlNode::get_tag(const std::string& name) const
+{
+ try
+ {
+ const auto& value = this->attributes.at(name);
+ return value;
+ }
+ catch (const std::out_of_range& e)
+ {
+ static const std::string def{};
+ return def;
+ }
+}
+
+bool XmlNode::del_tag(const std::string& name)
+{
+ if (this->attributes.erase(name) != 0)
+ return true;
+ return false;
+}
+
+std::string& XmlNode::operator[](const std::string& name)
+{
+ return this->attributes[name];
+}
+
+std::ostream& operator<<(std::ostream& os, const XmlNode& node)
+{
+ return os << node.to_string();
+}
diff --git a/louloulibs/xmpp/xmpp_stanza.hpp b/louloulibs/xmpp/xmpp_stanza.hpp
new file mode 100644
index 0000000..4ca758e
--- /dev/null
+++ b/louloulibs/xmpp/xmpp_stanza.hpp
@@ -0,0 +1,146 @@
+#pragma once
+
+
+#include <map>
+#include <string>
+#include <vector>
+#include <memory>
+
+std::string xml_escape(const std::string& data);
+std::string xml_unescape(const std::string& data);
+std::string sanitize(const std::string& data, const std::string& encoding = "ISO-8859-1");
+
+/**
+ * Represent an XML node. It has
+ * - A parent XML node (in the case of the first-level nodes, the parent is
+ nullptr)
+ * - zero, one or more children XML nodes
+ * - A name
+ * - A map of attributes
+ * - inner data (text inside the node)
+ * - tail data (text just after the node)
+ */
+class XmlNode
+{
+public:
+ explicit XmlNode(const std::string& name, XmlNode* parent);
+ explicit XmlNode(const std::string& name);
+ /**
+ * The copy constructor does not copy the parent attribute. The children
+ * nodes are all copied recursively.
+ */
+ XmlNode(const XmlNode& node):
+ name(node.name),
+ parent(nullptr),
+ attributes(node.attributes),
+ children{},
+ inner(node.inner),
+ tail(node.tail)
+ {
+ for (const auto& child: node.children)
+ this->add_child(std::make_unique<XmlNode>(*child));
+ }
+
+ XmlNode(XmlNode&& node) = default;
+ XmlNode& operator=(const XmlNode&) = delete;
+ XmlNode& operator=(XmlNode&&) = delete;
+
+ ~XmlNode() = default;
+
+ void delete_all_children();
+ void set_attribute(const std::string& name, const std::string& value);
+ /**
+ * Set the content of the tail, that is the text just after this node
+ */
+ void set_tail(const std::string& data);
+ /**
+ * Append the given data to the content of the tail. This exists because
+ * the expat library may provide the complete text of an element in more
+ * than one call
+ */
+ void add_to_tail(const std::string& data);
+ /**
+ * Set the content of the inner, that is the text inside this node.
+ */
+ void set_inner(const std::string& data);
+ /**
+ * Append the given data to the content of the inner. For the reason
+ * described in add_to_tail comment.
+ */
+ void add_to_inner(const std::string& data);
+ /**
+ * Get the content of inner
+ */
+ std::string get_inner() const;
+ /**
+ * Get the content of the tail
+ */
+ std::string get_tail() const;
+ /**
+ * Get a pointer to the first child element with that name and that xml namespace
+ */
+ const XmlNode* get_child(const std::string& name, const std::string& xmlns) const;
+ /**
+ * Get a vector of all the children that have that name and that xml namespace.
+ */
+ std::vector<const XmlNode*> get_children(const std::string& name, const std::string& xmlns) const;
+ /**
+ * Add a node child to this node. Assign this node to the child’s parent.
+ * Returns a pointer to the newly added child.
+ */
+ XmlNode* add_child(std::unique_ptr<XmlNode> child);
+ XmlNode* add_child(XmlNode&& child);
+ XmlNode* add_child(const XmlNode& child);
+ /**
+ * Returns the last of the children. If the node doesn't have any child,
+ * the behaviour is undefined. The user should make sure this is the case
+ * by calling has_children() for example.
+ */
+ XmlNode* get_last_child() const;
+ XmlNode* get_parent() const;
+ void set_name(const std::string& name);
+ void set_name(std::string&& name);
+ const std::string get_name() const;
+ /**
+ * Serialize the stanza into a string
+ */
+ std::string to_string() const;
+ /**
+ * Whether or not this node has at least one child (if not, this is a leaf
+ * node)
+ */
+ bool has_children() const;
+ /**
+ * Gets the value for the given attribute, returns an empty string if the
+ * node as no such attribute.
+ */
+ const std::string& get_tag(const std::string& name) const;
+ /**
+ * Remove the attribute of the node. Does nothing if that attribute is not
+ * present. Returns true if the tag was removed, false if it was absent.
+ */
+ bool del_tag(const std::string& name);
+ /**
+ * Use this to set an attribute's value, like node["id"] = "12";
+ */
+ std::string& operator[](const std::string& name);
+
+private:
+ std::string name;
+ XmlNode* parent;
+ std::map<std::string, std::string> attributes;
+ std::vector<std::unique_ptr<XmlNode>> children;
+ std::string inner;
+ std::string tail;
+};
+
+std::ostream& operator<<(std::ostream& os, const XmlNode& node);
+
+/**
+ * An XMPP stanza is just an XML node of level 2 in the XMPP document (the
+ * level 1 ones are the <stream::stream/>, and the ones above 2 are just the
+ * content of the stanzas)
+ */
+using Stanza = XmlNode;
+
+
diff --git a/packaging/biboumi.spec.cmake b/packaging/biboumi.spec.cmake
new file mode 100644
index 0000000..c94da8b
--- /dev/null
+++ b/packaging/biboumi.spec.cmake
@@ -0,0 +1,81 @@
+Name: biboumi
+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
+
+BuildRequires: libidn-devel
+BuildRequires: expat-devel
+BuildRequires: libuuid-devel
+BuildRequires: systemd-devel
+BuildRequires: cmake
+BuildRequires: systemd
+BuildRequires: pandoc
+
+%global _hardened_build 1
+
+%global biboumi_confdir %{_sysconfdir}/%{name}
+
+
+%description
+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.
+
+
+%prep
+%setup -q
+
+
+%build
+cmake . -DCMAKE_CXX_FLAGS="%{optflags}" \
+ -DCMAKE_BUILD_TYPE=release \
+ -DCMAKE_INSTALL_PREFIX=/usr \
+ -DPOLLER=EPOLL \
+ -DWITHOUT_BOTAN=1 \
+ -DWITH_SYSTEMD=1 \
+ -DWITH_LIBIDN=1
+
+make %{?_smp_mflags}
+
+
+%install
+make install DESTDIR=%{buildroot}
+
+
+%check
+make check %{?_smp_mflags}
+
+
+%files
+%{_bindir}/%{name}
+%{_mandir}/man1/%{name}.1*
+%doc README.rst COPYING doc/biboumi.1.rst
+%{_unitdir}/%{name}.service
+%config(noreplace) %{biboumi_confdir}/biboumi.cfg
+
+
+%changelog
+* Thu Aug 4 2016 Le Coz Florent <louiz@louiz.org> - 3.0-1
+- Update to 3.0 sources
+
+* Wed Jan 13 2016 Le Coz Florent <louiz@louiz.org> - 2.0-2
+- Do not install the systemd unit and configuration files, because
+ “make install” does it itself now
+
+* Fri May 29 2015 Le Coz Florent <louiz@louiz.org> - 2.0-1
+- Update to 2.0 sources
+
+* Thu Nov 13 2014 Le Coz Florent <louiz@louiz.org> - 1.1-2
+- Use the -DWITH(OUT) cmake flags for all optional dependencies
+- Build with the correct optflags
+- Use hardened_build
+
+* Mon Aug 18 2014 Le Coz Florent <louiz@louiz.org> - 1.1-1
+- Update to 1.1 release
+
+* Wed Jun 25 2014 Le Coz Florent <louiz@louiz.org> - 1.0-1
+- Spec file written from scratch
diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp
new file mode 100644
index 0000000..17d3ec6
--- /dev/null
+++ b/src/bridge/bridge.cpp
@@ -0,0 +1,907 @@
+#include <bridge/bridge.hpp>
+#include <bridge/list_element.hpp>
+#include <xmpp/biboumi_component.hpp>
+#include <network/poller.hpp>
+#include <utils/empty_if_fixed_server.hpp>
+#include <utils/encoding.hpp>
+#include <utils/tolower.hpp>
+#include <logger/logger.hpp>
+#include <utils/revstr.hpp>
+#include <utils/split.hpp>
+#include <xmpp/jid.hpp>
+#include <database/database.hpp>
+
+using namespace std::string_literals;
+
+static const char* action_prefix = "\01ACTION ";
+
+
+static std::string in_encoding_for(const Bridge& bridge, const Iid& iid)
+{
+#ifdef USE_DATABASE
+ const auto jid = bridge.get_bare_jid();
+ auto options = Database::get_irc_channel_options_with_server_default(jid, iid.get_server(), iid.get_local());
+ return options.encodingIn.value();
+#else
+ return {"ISO-8859-1"};
+#endif
+}
+
+Bridge::Bridge(const std::string& user_jid, BiboumiComponent& xmpp, std::shared_ptr<Poller> poller):
+ user_jid(user_jid),
+ xmpp(xmpp),
+ poller(poller)
+{
+}
+
+/**
+ * Return the role and affiliation, corresponding to the given irc mode
+ */
+static std::tuple<std::string, std::string> get_role_affiliation_from_irc_mode(const char mode)
+{
+ if (mode == 'a' || mode == 'q'){
+ return std::make_tuple("moderator", "owner");}
+ else if (mode == 'o')
+ return std::make_tuple("moderator", "admin");
+ else if (mode == 'h')
+ return std::make_tuple("moderator", "member");
+ else if (mode == 'v')
+ return std::make_tuple("participant", "member");
+ else
+ return std::make_tuple("participant", "none");
+}
+
+void Bridge::shutdown(const std::string& exit_message)
+{
+ for (auto it = this->irc_clients.begin(); it != this->irc_clients.end(); ++it)
+ {
+ it->second->send_quit_command(exit_message);
+ it->second->leave_dummy_channel(exit_message);
+ }
+}
+
+void Bridge::clean()
+{
+ auto it = this->irc_clients.begin();
+ while (it != this->irc_clients.end())
+ {
+ IrcClient* client = it->second.get();
+ if (!client->is_connected() && !client->is_connecting() &&
+ !client->get_resolver().is_resolving())
+ it = this->irc_clients.erase(it);
+ else
+ ++it;
+ }
+}
+
+const std::string& Bridge::get_jid() const
+{
+ return this->user_jid;
+}
+
+std::string Bridge::get_bare_jid() const
+{
+ Jid jid(this->user_jid);
+ return jid.local + "@" + jid.domain;
+}
+
+Xmpp::body Bridge::make_xmpp_body(const std::string& str, const std::string& encoding)
+{
+ std::string res;
+ if (utils::is_valid_utf8(str.c_str()))
+ res = str;
+ else
+ res = utils::convert_to_utf8(str, encoding.data());
+ return irc_format_to_xhtmlim(res);
+}
+
+IrcClient* Bridge::make_irc_client(const std::string& hostname, const std::string& nickname)
+{
+ try
+ {
+ return this->irc_clients.at(hostname).get();
+ }
+ catch (const std::out_of_range& exception)
+ {
+ auto username = nickname;
+ auto realname = nickname;
+ Jid jid(this->user_jid);
+ if (Config::get("realname_from_jid", "false") == "true")
+ {
+ username = jid.local;
+ realname = this->get_bare_jid();
+ }
+ this->irc_clients.emplace(hostname,
+ std::make_shared<IrcClient>(this->poller, hostname,
+ nickname, username,
+ realname, jid.domain,
+ *this));
+ std::shared_ptr<IrcClient> irc = this->irc_clients.at(hostname);
+ return irc.get();
+ }
+}
+
+IrcClient* Bridge::get_irc_client(const std::string& hostname)
+{
+ try
+ {
+ return this->irc_clients.at(hostname).get();
+ }
+ catch (const std::out_of_range& exception)
+ {
+ throw IRCNotConnected(hostname);
+ }
+}
+
+IrcClient* Bridge::find_irc_client(const std::string& hostname)
+{
+ try
+ {
+ return this->irc_clients.at(hostname).get();
+ }
+ catch (const std::out_of_range& exception)
+ {
+ return nullptr;
+ }
+}
+
+bool Bridge::join_irc_channel(const Iid& iid, const std::string& nickname, const std::string& password,
+ const std::string& resource)
+{
+ const auto hostname = iid.get_server();
+ IrcClient* irc = this->make_irc_client(hostname, nickname);
+ this->add_resource_to_server(hostname, resource);
+ auto res_in_chan = this->is_resource_in_chan(ChannelKey{iid.get_local(), hostname}, resource);
+ if (!res_in_chan)
+ this->add_resource_to_chan(ChannelKey{iid.get_local(), hostname}, resource);
+ if (iid.get_local().empty())
+ { // Join the dummy channel
+ if (irc->is_welcomed())
+ {
+ if (irc->get_dummy_channel().joined)
+ return false;
+ // Immediately simulate a message coming from the IRC server saying that we
+ // joined the channel
+ const IrcMessage join_message(irc->get_nick(), "JOIN", {""});
+ irc->on_channel_join(join_message);
+ const IrcMessage end_join_message(std::string(iid.get_server()), "366",
+ {irc->get_nick(),
+ "", "End of NAMES list"});
+ irc->on_channel_completely_joined(end_join_message);
+ }
+ else
+ {
+ irc->get_dummy_channel().joining = true;
+ irc->start();
+ }
+ return true;
+ }
+ if (irc->is_channel_joined(iid.get_local()) == false)
+ {
+ irc->send_join_command(iid.get_local(), password);
+ return true;
+ } else if (!res_in_chan) {
+ this->generate_channel_join_for_resource(iid, resource);
+ }
+ return false;
+}
+
+void Bridge::send_channel_message(const Iid& iid, const std::string& body)
+{
+ if (iid.get_server().empty())
+ {
+ for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
+ this->xmpp.send_stanza_error("message", this->user_jid + "/" + resource, std::to_string(iid), "",
+ "cancel", "remote-server-not-found",
+ std::to_string(iid) + " is not a valid channel name. "
+ "A correct room jid is of the form: #<chan>%<server>",
+ false);
+ return;
+ }
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+
+ // Because an IRC message cannot contain \n, we need to convert each line
+ // of text into a separate IRC message. For conveniance, we also cut the
+ // message into submessages on the XMPP side, this way the user of the
+ // gateway sees what was actually sent over IRC. For example if an user
+ // sends “hello\n/me waves”, two messages will be generated: “hello” and
+ // “/me waves”. Note that the “command” handling (messages starting with
+ // /me, /mode, etc) is done for each message generated this way. In the
+ // above example, the /me will be interpreted.
+ std::vector<std::string> lines = utils::split(body, '\n', true);
+ if (lines.empty())
+ return ;
+ for (const std::string& line: lines)
+ {
+ if (line.substr(0, 5) == "/mode")
+ {
+ std::vector<std::string> args = utils::split(line.substr(5), ' ', false);
+ irc->send_mode_command(iid.get_local(), args);
+ continue; // We do not want to send that back to the
+ // XMPP user, that’s not a textual message.
+ }
+ else if (line.substr(0, 4) == "/me ")
+ irc->send_channel_message(iid.get_local(), action_prefix + line.substr(4) + "\01");
+ else
+ irc->send_channel_message(iid.get_local(), line);
+ for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
+ this->xmpp.send_muc_message(std::to_string(iid), irc->get_own_nick(),
+ this->make_xmpp_body(line), this->user_jid + "/" + resource);
+ }
+}
+
+void Bridge::forward_affiliation_role_change(const Iid& iid, const std::string& nick,
+ const std::string& affiliation,
+ const std::string& role)
+{
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+ IrcChannel* chan = irc->get_channel(iid.get_local());
+ if (!chan || !chan->joined)
+ return;
+ IrcUser* user = chan->find_user(nick);
+ if (!user)
+ return;
+ // For each affiliation or role, we have a “maximal” mode that we want to
+ // set. We must remove any superior mode at the same time. For example if
+ // the user already has +o mode, and we set its affiliation to member, we
+ // remove the +o mode, and add +v. For each “superior” mode (for example,
+ // for +v, the superior modes are 'h', 'a', 'o' and 'q') we check if that
+ // user has it, and if yes we remove that mode
+
+ std::size_t nb = 1; // the number of times the nick must be
+ // repeated in the argument list
+ std::string modes; // The string of modes to
+ // add/remove. For example "+v-aoh"
+ std::vector<char> modes_to_remove; // List of modes to check for removal
+ if (affiliation == "none")
+ {
+ modes = "";
+ nb = 0;
+ modes_to_remove = {'v', 'h', 'o', 'a', 'q'};
+ }
+ else if (affiliation == "member")
+ {
+ modes = "+v";
+ modes_to_remove = {'h', 'o', 'a', 'q'};
+ }
+ else if (role == "moderator")
+ {
+ modes = "+h";
+ modes_to_remove = {'o', 'a', 'q'};
+ }
+ else if (affiliation == "admin")
+ {
+ modes = "+o";
+ modes_to_remove = {'a', 'q'};
+ }
+ else if (affiliation == "owner")
+ {
+ modes = "+a";
+ modes_to_remove = {'q'};
+ }
+ else
+ return;
+ for (const char mode: modes_to_remove)
+ if (user->modes.find(mode) != user->modes.end())
+ {
+ modes += "-"s + mode;
+ nb++;
+ }
+ if (modes.empty())
+ return;
+ std::vector<std::string> args(nb, nick);
+ args.insert(args.begin(), modes);
+ irc->send_mode_command(iid.get_local(), args);
+}
+
+void Bridge::send_private_message(const Iid& iid, const std::string& body, const std::string& type)
+{
+ if (iid.get_local().empty() || iid.get_server().empty())
+ {
+ this->xmpp.send_stanza_error("message", this->user_jid, std::to_string(iid), "",
+ "cancel", "remote-server-not-found",
+ std::to_string(iid) + " is not a valid channel name. "
+ "A correct room jid is of the form: #<chan>%<server>",
+ false);
+ return;
+ }
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+ std::vector<std::string> lines = utils::split(body, '\n', true);
+ if (lines.empty())
+ return ;
+ for (const std::string& line: lines)
+ {
+ if (line.substr(0, 4) == "/me ")
+ irc->send_private_message(iid.get_local(), action_prefix + line.substr(4) + "\01", type);
+ else
+ irc->send_private_message(iid.get_local(), line, type);
+ }
+}
+
+void Bridge::send_raw_message(const std::string& hostname, const std::string& body)
+{
+ IrcClient* irc = this->get_irc_client(hostname);
+ irc->send_raw(body);
+}
+
+void Bridge::leave_irc_channel(Iid&& iid, std::string&& status_message, const std::string& resource)
+{
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+ const auto key = iid.to_tuple();
+ if (!this->is_resource_in_chan(key, resource))
+ return ;
+
+ const auto resources = this->number_of_resources_in_chan(key);
+ if (resources == 1)
+ {
+ irc->send_part_command(iid.get_local(), status_message);
+ // Since there are no resources left in that channel, we don't
+ // want to receive private messages using this room's JID
+ this->remove_all_preferred_from_jid_of_room(iid.get_local());
+ }
+ else
+ {
+ IrcChannel* chan = irc->get_channel(iid.get_local());
+ if (chan)
+ {
+ auto nick = chan->get_self()->nick;
+ this->remove_resource_from_chan(key, resource);
+ this->send_muc_leave(std::move(iid), std::move(nick),
+ "Biboumi note: "s + std::to_string(resources - 1) + " resources are still in this channel.",
+ true, resource);
+ if (this->number_of_channels_the_resource_is_in(iid.get_server(), resource) == 0)
+ this->remove_resource_from_server(iid.get_server(), resource);
+ }
+ }
+}
+
+void Bridge::send_irc_nick_change(const Iid& iid, const std::string& new_nick)
+{
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+ irc->send_nick_command(new_nick);
+}
+
+void Bridge::send_irc_channel_list_request(const Iid& iid, const std::string& iq_id,
+ const std::string& to_jid)
+{
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+
+ irc->send_list_command();
+
+ std::vector<ListElement> list;
+
+ irc_responder_callback_t cb = [this, iid, iq_id, to_jid, list=std::move(list)](const std::string& irc_hostname,
+ const IrcMessage& message) mutable -> bool
+ {
+ if (irc_hostname != iid.get_server())
+ return false;
+ if (message.command == "263" || message.command == "RPL_TRYAGAIN" ||
+ message.command == "ERR_TOOMANYMATCHES" || message.command == "ERR_NOSUCHSERVER")
+ {
+ std::string text;
+ if (message.arguments.size() >= 2)
+ text = message.arguments[1];
+ this->xmpp.send_stanza_error("iq", to_jid, std::to_string(iid), iq_id,
+ "wait", "service-unavailable", text, false);
+ return true;
+ }
+ else if (message.command == "322" || message.command == "RPL_LIST")
+ { // Add element to list
+ if (message.arguments.size() == 4)
+ list.emplace_back(message.arguments[1], message.arguments[2],
+ message.arguments[3]);
+ return false;
+ }
+ else if (message.command == "323" || message.command == "RPL_LISTEND")
+ { // Send the iq response with the content of the list
+ this->xmpp.send_iq_room_list_result(iq_id, to_jid, std::to_string(iid), list);
+ return true;
+ }
+ return false;
+ };
+ this->add_waiting_irc(std::move(cb));
+}
+
+void Bridge::send_irc_kick(const Iid& iid, const std::string& target, const std::string& reason,
+ const std::string& iq_id, const std::string& to_jid)
+{
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+
+ irc->send_kick_command(iid.get_local(), target, reason);
+ irc_responder_callback_t cb = [this, target, iq_id, to_jid, iid](const std::string& irc_hostname,
+ const IrcMessage& message) -> bool
+ {
+ if (irc_hostname != iid.get_server())
+ return false;
+ if (message.command == "KICK" && message.arguments.size() >= 2)
+ {
+ const std::string target_later = message.arguments[1];
+ const std::string chan_name_later = utils::tolower(message.arguments[0]);
+ if (target_later != target || chan_name_later != iid.get_local())
+ return false;
+ this->xmpp.send_iq_result(iq_id, to_jid, std::to_string(iid));
+ }
+ else if (message.command == "401" && message.arguments.size() >= 2)
+ {
+ const std::string target_later = message.arguments[1];
+ if (target_later != target)
+ return false;
+ std::string error_message = "No such nick";
+ if (message.arguments.size() >= 3)
+ error_message = message.arguments[2];
+ this->xmpp.send_stanza_error("iq", to_jid, std::to_string(iid), iq_id, "cancel", "item-not-found",
+ error_message, false);
+ }
+ else if (message.command == "482" && message.arguments.size() >= 2)
+ {
+ const std::string chan_name_later = utils::tolower(message.arguments[1]);
+ if (chan_name_later != iid.get_local())
+ return false;
+ std::string error_message = "You're not channel operator";
+ if (message.arguments.size() >= 3)
+ error_message = message.arguments[2];
+ this->xmpp.send_stanza_error("iq", to_jid, std::to_string(iid), iq_id, "cancel", "not-allowed",
+ error_message, false);
+ }
+ return true;
+ };
+ this->add_waiting_irc(std::move(cb));
+}
+
+void Bridge::set_channel_topic(const Iid& iid, const std::string& subject)
+{
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+ irc->send_topic_command(iid.get_local(), subject);
+}
+
+void Bridge::send_xmpp_version_to_irc(const Iid& iid, const std::string& name, const std::string& version, const std::string& os)
+{
+ std::string result(name + " " + version + " " + os);
+
+ this->send_private_message(iid, "\01VERSION "s + result + "\01", "NOTICE");
+}
+
+void Bridge::send_irc_ping_result(const Iid& iid, const std::string& id)
+{
+ this->send_private_message(iid, "\01PING "s + utils::revstr(id) + "\01", "NOTICE");
+}
+
+void Bridge::send_irc_user_ping_request(const std::string& irc_hostname, const std::string& nick,
+ const std::string& iq_id, const std::string& to_jid,
+ const std::string& from_jid)
+{
+ Iid iid(nick + "!" + irc_hostname);
+ this->send_private_message(iid, "\01PING " + iq_id + "\01");
+
+ irc_responder_callback_t cb = [this, nick=utils::tolower(nick), iq_id, to_jid, irc_hostname, from_jid](const std::string& hostname, const IrcMessage& message) -> bool
+ {
+ if (irc_hostname != hostname || message.arguments.size() < 2)
+ return false;
+ IrcUser user(message.prefix);
+ const std::string body = message.arguments[1];
+ if (message.command == "NOTICE" && utils::tolower(user.nick) == nick
+ && body.substr(0, 6) == "\01PING ")
+ {
+ const std::string id = body.substr(6, body.size() - 7);
+ if (id != iq_id)
+ return false;
+ this->xmpp.send_iq_result_full_jid(iq_id, to_jid, from_jid);
+ return true;
+ }
+ if (message.command == "401" && message.arguments[1] == nick)
+ {
+ std::string error_message = "No such nick";
+ if (message.arguments.size() >= 3)
+ error_message = message.arguments[2];
+ this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "service-unavailable",
+ error_message, true);
+ return true;
+ }
+
+ return false;
+ };
+ this->add_waiting_irc(std::move(cb));
+}
+
+void Bridge::send_irc_participant_ping_request(const Iid& iid, const std::string& nick,
+ const std::string& iq_id, const std::string& to_jid,
+ const std::string& from_jid)
+{
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+ IrcChannel* chan = irc->get_channel(iid.get_local());
+ if (!chan->joined)
+ {
+ this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "not-allowed",
+ "", true);
+ return;
+ }
+ if (chan->get_self()->nick != nick && !chan->find_user(nick))
+ {
+ this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "item-not-found",
+ "Recipient not in room", true);
+ return;
+ }
+
+ // The user is in the room, send it a direct PING
+ this->send_irc_user_ping_request(iid.get_server(), nick, iq_id, to_jid, from_jid);
+}
+
+void Bridge::on_gateway_ping(const std::string& irc_hostname, const std::string& iq_id, const std::string& to_jid,
+ const std::string& from_jid)
+{
+ Jid jid(from_jid);
+ if (irc_hostname.empty() || this->find_irc_client(irc_hostname))
+ this->xmpp.send_iq_result(iq_id, to_jid, jid.local);
+ else
+ this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "service-unavailable",
+ "", true);
+}
+
+void Bridge::send_irc_version_request(const std::string& irc_hostname, const std::string& target,
+ const std::string& iq_id, const std::string& to_jid,
+ const std::string& from_jid)
+{
+ Iid iid(target + "!" + irc_hostname);
+ this->send_private_message(iid, "\01VERSION\01");
+ // TODO, add a timer to remove that waiting iq if the server does not
+ // respond with a matching command before n seconds
+ irc_responder_callback_t cb = [this, target, iq_id, to_jid, irc_hostname, from_jid](const std::string& hostname, const IrcMessage& message) -> bool
+ {
+ if (irc_hostname != hostname)
+ return false;
+ IrcUser user(message.prefix);
+ if (message.command == "NOTICE" && user.nick == target &&
+ message.arguments.size() >= 2 && message.arguments[1].substr(0, 9) == "\01VERSION ")
+ {
+ // remove the "\01VERSION " and the "\01" parts from the string
+ const std::string version = message.arguments[1].substr(9, message.arguments[1].size() - 10);
+ this->xmpp.send_version(iq_id, to_jid, from_jid, version);
+ return true;
+ }
+ if (message.command == "401" && message.arguments.size() >= 2
+ && message.arguments[1] == target)
+ {
+ std::string error_message = "No such nick";
+ if (message.arguments.size() >= 3)
+ error_message = message.arguments[2];
+ this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "item-not-found",
+ error_message, true);
+ return true;
+ }
+ return false;
+ };
+ this->add_waiting_irc(std::move(cb));
+}
+
+void Bridge::send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc)
+{
+ const auto encoding = in_encoding_for(*this, iid);
+ if (muc)
+ {
+ for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
+ {
+ this->xmpp.send_muc_message(std::to_string(iid), nick,
+ this->make_xmpp_body(body, encoding), this->user_jid + "/" + resource);
+ }
+ }
+ else
+ {
+ std::string target = std::to_string(iid);
+ const auto it = this->preferred_user_from.find(iid.get_local());
+ if (it != this->preferred_user_from.end())
+ {
+ const auto chan_name = Iid(Jid(it->second).local).get_local();
+ for (const auto& resource: this->resources_in_chan[ChannelKey{chan_name, iid.get_server()}])
+ this->xmpp.send_message(it->second, this->make_xmpp_body(body, encoding),
+ this->user_jid + "/" + resource, "chat", true);
+ }
+ else
+ {
+ for (const auto& resource: this->resources_in_server[iid.get_server()])
+ this->xmpp.send_message(std::to_string(iid), this->make_xmpp_body(body, encoding),
+ this->user_jid + "/" + resource, "chat", false);
+ }
+ }
+}
+
+void Bridge::send_presence_error(const Iid& iid, const std::string& nick,
+ const std::string& type, const std::string& condition,
+ const std::string& error_code, const std::string& text)
+{
+ this->xmpp.send_presence_error(std::to_string(iid), nick, this->user_jid, type, condition, error_code, text);
+}
+
+void Bridge::send_muc_leave(Iid&& iid, std::string&& nick, const std::string& message, const bool self, const std::string& resource)
+{
+ if (!resource.empty())
+ this->xmpp.send_muc_leave(std::to_string(iid), std::move(nick), this->make_xmpp_body(message), this->user_jid + "/" + resource,
+ self);
+ else
+ for (const auto& res: this->resources_in_chan[iid.to_tuple()])
+ this->xmpp.send_muc_leave(std::to_string(iid), std::move(nick), this->make_xmpp_body(message), this->user_jid + "/" + res,
+ self);
+ IrcClient* irc = this->find_irc_client(iid.get_server());
+ if (irc && irc->number_of_joined_channels() == 0)
+ irc->send_quit_command("");
+}
+
+void Bridge::send_nick_change(Iid&& iid,
+ const std::string& old_nick,
+ const std::string& new_nick,
+ const char user_mode,
+ const bool self)
+{
+ std::string affiliation;
+ std::string role;
+ std::tie(role, affiliation) = get_role_affiliation_from_irc_mode(user_mode);
+
+ for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
+ this->xmpp.send_nick_change(std::to_string(iid),
+ old_nick, new_nick, affiliation, role, this->user_jid + "/" + resource, self);
+}
+
+void Bridge::send_xmpp_message(const std::string& from, const std::string& author, const std::string& msg)
+{
+ std::string body;
+ if (!author.empty())
+ {
+ IrcUser user(author);
+ body = "\u000303"s + user.nick + (user.host.empty()?
+ "\u0003: ":
+ (" (\u000310" + user.host + "\u000303)\u0003: ")) + msg;
+ }
+ else
+ body = msg;
+
+ const auto encoding = in_encoding_for(*this, {from});
+ for (const auto& resource: this->resources_in_server[from])
+ {
+ this->xmpp.send_message(from, this->make_xmpp_body(body, encoding), this->user_jid + "/" + resource, "chat");
+ }
+}
+
+void Bridge::send_user_join(const std::string& hostname, const std::string& chan_name,
+ const IrcUser* user, const char user_mode, const bool self)
+{
+ for (const auto& resource: this->resources_in_chan[ChannelKey{chan_name, hostname}])
+ this->send_user_join(hostname, chan_name, user, user_mode, self, resource);
+}
+
+void Bridge::send_user_join(const std::string& hostname, const std::string& chan_name,
+ const IrcUser* user, const char user_mode,
+ const bool self, const std::string& resource)
+{
+ std::string affiliation;
+ std::string role;
+ std::tie(role, affiliation) = get_role_affiliation_from_irc_mode(user_mode);
+
+ std::string encoded_chan_name(chan_name);
+ xep0106::encode(encoded_chan_name);
+
+ this->xmpp.send_user_join(encoded_chan_name + utils::empty_if_fixed_server("%" + hostname), user->nick, user->host,
+ affiliation, role, this->user_jid + "/" + resource, self);
+}
+
+void Bridge::send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic, const std::string& who)
+{
+ for (const auto& resource: this->resources_in_chan[ChannelKey{chan_name, hostname}])
+ {
+ this->send_topic(hostname, chan_name, topic, who, resource);
+ }
+}
+
+void Bridge::send_topic(const std::string& hostname, const std::string& chan_name,
+ const std::string& topic, const std::string& who,
+ const std::string& resource)
+{
+ std::string encoded_chan_name(chan_name);
+ xep0106::encode(encoded_chan_name);
+ const auto encoding = in_encoding_for(*this, {encoded_chan_name + '%' + hostname});
+ this->xmpp.send_topic(encoded_chan_name + utils::empty_if_fixed_server(
+ "%" + hostname), this->make_xmpp_body(topic, encoding), this->user_jid + "/" + resource, who);
+
+}
+
+std::string Bridge::get_own_nick(const Iid& iid)
+{
+ IrcClient* irc = this->find_irc_client(iid.get_server());
+ if (irc)
+ return irc->get_own_nick();
+ return "";
+}
+
+size_t Bridge::active_clients() const
+{
+ return this->irc_clients.size();
+}
+
+void Bridge::kick_muc_user(Iid&& iid, const std::string& target, const std::string& reason, const std::string& author)
+{
+ for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
+ this->xmpp.kick_user(std::to_string(iid), target, reason, author, this->user_jid + "/" + resource);
+}
+
+void Bridge::send_nickname_conflict_error(const Iid& iid, const std::string& nickname)
+{
+ for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
+ this->xmpp.send_presence_error(std::to_string(iid), nickname, this->user_jid + "/" + resource, "cancel", "conflict", "409", "");
+}
+
+void Bridge::send_affiliation_role_change(const Iid& iid, const std::string& target, const char mode)
+{
+ std::string role;
+ std::string affiliation;
+
+ std::tie(role, affiliation) = get_role_affiliation_from_irc_mode(mode);
+ for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
+ this->xmpp.send_affiliation_role_change(std::to_string(iid), target, affiliation, role, this->user_jid + "/" + resource);
+}
+
+void Bridge::send_iq_version_request(const std::string& nick, const std::string& hostname)
+{
+ const auto resources = this->resources_in_server[hostname];
+ if (resources.begin() != resources.end())
+ this->xmpp.send_iq_version_request(utils::tolower(nick) + "!" + utils::empty_if_fixed_server(hostname), this->user_jid + "/" + *resources.begin());
+}
+
+void Bridge::send_xmpp_ping_request(const std::string& nick, const std::string& hostname,
+ const std::string& id)
+{
+ // Use revstr because the forwarded ping to target XMPP user must not be
+ // the same that the request iq, but we also need to get it back easily
+ // (revstr again)
+ // Forward to the first resource (arbitrary, based on the “order” of the std::set) only
+ const auto resources = this->resources_in_server[hostname];
+ if (resources.begin() != resources.end())
+ this->xmpp.send_ping_request(utils::tolower(nick) + "!" + utils::empty_if_fixed_server(hostname), this->user_jid + "/" + *resources.begin(), utils::revstr(id));
+}
+
+void Bridge::set_preferred_from_jid(const std::string& nick, const std::string& full_jid)
+{
+ auto it = this->preferred_user_from.find(nick);
+ if (it == this->preferred_user_from.end())
+ this->preferred_user_from.emplace(nick, full_jid);
+ else
+ this->preferred_user_from[nick] = full_jid;
+}
+
+void Bridge::remove_preferred_from_jid(const std::string& nick)
+{
+ auto it = this->preferred_user_from.find(nick);
+ if (it != this->preferred_user_from.end())
+ this->preferred_user_from.erase(it);
+}
+
+void Bridge::remove_all_preferred_from_jid_of_room(const std::string& channel_name)
+{
+ for (auto it = this->preferred_user_from.begin(); it != this->preferred_user_from.end();)
+ {
+ Iid iid(Jid(it->second).local);
+ if (iid.get_local() == channel_name)
+ it = this->preferred_user_from.erase(it);
+ else
+ ++it;
+ }
+}
+
+void Bridge::add_waiting_irc(irc_responder_callback_t&& callback)
+{
+ this->waiting_irc.emplace_back(std::move(callback));
+}
+
+void Bridge::trigger_on_irc_message(const std::string& irc_hostname, const IrcMessage& message)
+{
+ auto it = this->waiting_irc.begin();
+ while (it != this->waiting_irc.end())
+ {
+ if ((*it)(irc_hostname, message) == true)
+ it = this->waiting_irc.erase(it);
+ else
+ ++it;
+ }
+}
+
+std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients()
+{
+ return this->irc_clients;
+}
+
+void Bridge::add_resource_to_chan(const Bridge::ChannelKey& channel, const std::string& resource)
+{
+ auto it = this->resources_in_chan.find(channel);
+ if (it == this->resources_in_chan.end())
+ this->resources_in_chan[channel] = {resource};
+ else
+ it->second.insert(resource);
+}
+
+void Bridge::remove_resource_from_chan(const Bridge::ChannelKey& channel, const std::string& resource)
+{
+ auto it = this->resources_in_chan.find(channel);
+ if (it != this->resources_in_chan.end())
+ {
+ it->second.erase(resource);
+ if (it->second.empty())
+ this->resources_in_chan.erase(it);
+ }
+}
+
+bool Bridge::is_resource_in_chan(const Bridge::ChannelKey& channel, const std::string& resource) const
+{
+ auto it = this->resources_in_chan.find(channel);
+ if (it != this->resources_in_chan.end())
+ if (it->second.count(resource) == 1)
+ return true;
+ return false;
+}
+
+void Bridge::add_resource_to_server(const Bridge::IrcHostname& irc_hostname, const std::string& resource)
+{
+ auto it = this->resources_in_server.find(irc_hostname);
+ if (it == this->resources_in_server.end())
+ this->resources_in_server[irc_hostname] = {resource};
+ else
+ it->second.insert(resource);
+}
+
+void Bridge::remove_resource_from_server(const Bridge::IrcHostname& irc_hostname, const std::string& resource)
+{
+ auto it = this->resources_in_server.find(irc_hostname);
+ if (it != this->resources_in_server.end())
+ {
+ it->second.erase(resource);
+ if (it->second.empty())
+ this->resources_in_server.erase(it);
+ }
+}
+
+bool Bridge::is_resource_in_server(const Bridge::IrcHostname& irc_hostname, const std::string& resource) const
+{
+ auto it = this->resources_in_server.find(irc_hostname);
+ if (it != this->resources_in_server.end())
+ if (it->second.count(resource) == 1)
+ return true;
+ return false;
+}
+
+std::size_t Bridge::number_of_resources_in_chan(const Bridge::ChannelKey& channel_key) const
+{
+ auto it = this->resources_in_chan.find(channel_key);
+ if (it == this->resources_in_chan.end())
+ return 0;
+ return it->second.size();
+}
+
+std::size_t Bridge::number_of_channels_the_resource_is_in(const std::string& irc_hostname, const std::string& resource) const
+{
+ std::size_t res = 0;
+ for (auto pair: this->resources_in_chan)
+ {
+ if (std::get<0>(pair.first) == irc_hostname && pair.second.count(resource) != 0)
+ res++;
+ }
+ return res;
+}
+
+void Bridge::generate_channel_join_for_resource(const Iid& iid, const std::string& resource)
+{
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+ IrcChannel* channel = irc->get_channel(iid.get_local());
+ const auto self = channel->get_self();
+
+ // Send the occupant list
+ for (const auto& user: channel->get_users())
+ {
+ if (user->nick != self->nick)
+ {
+ log_debug(user->nick);
+ this->send_user_join(iid.get_server(), iid.get_encoded_local(),
+ user.get(), user->get_most_significant_mode(irc->get_sorted_user_modes()),
+ false, resource);
+ }
+ }
+ this->send_user_join(iid.get_server(), iid.get_encoded_local(),
+ self, self->get_most_significant_mode(irc->get_sorted_user_modes()),
+ true, resource);
+ this->send_topic(iid.get_server(), iid.get_encoded_local(), channel->topic, channel->topic_author, resource);
+}
diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp
new file mode 100644
index 0000000..69b7bd5
--- /dev/null
+++ b/src/bridge/bridge.hpp
@@ -0,0 +1,293 @@
+#pragma once
+
+
+#include <irc/irc_message.hpp>
+#include <irc/irc_client.hpp>
+#include <bridge/colors.hpp>
+#include <irc/irc_user.hpp>
+#include <irc/iid.hpp>
+
+#include <unordered_map>
+#include <functional>
+#include <exception>
+#include <string>
+#include <memory>
+
+class BiboumiComponent;
+class Poller;
+
+/**
+ * A callback called for each IrcMessage we receive. If the message triggers
+ * a response, it must send ore or more iq and return true (in that case it
+ * is removed from the list), otherwise it must do nothing and just return
+ * false.
+ */
+using irc_responder_callback_t = std::function<bool(const std::string& irc_hostname, const IrcMessage& message)>;
+
+/**
+ * One bridge is spawned for each XMPP user that uses the component. The
+ * bridge spawns IrcClients when needed (when the user wants to join a
+ * channel on a new server) and does the translation between the two
+ * protocols.
+ */
+class Bridge
+{
+public:
+ explicit Bridge(const std::string& user_jid, BiboumiComponent& xmpp, std::shared_ptr<Poller> poller);
+ ~Bridge() = default;
+
+ Bridge(const Bridge&) = delete;
+ Bridge(Bridge&& other) = delete;
+ Bridge& operator=(const Bridge&) = delete;
+ Bridge& operator=(Bridge&&) = delete;
+ /**
+ * QUIT all connected IRC servers.
+ */
+ void shutdown(const std::string& exit_message);
+ /**
+ * Remove all inactive IrcClients
+ */
+ void clean();
+ /**
+ * Return the jid of the XMPP user using this bridge
+ */
+ const std::string& get_jid() const;
+ std::string get_bare_jid() const;
+
+ static Xmpp::body make_xmpp_body(const std::string& str, const std::string& encoding = "ISO-8859-1");
+ /***
+ **
+ ** From XMPP to IRC.
+ **
+ **/
+
+ /**
+ * Try to join an irc_channel, does nothing and return true if the channel
+ * was already joined.
+ */
+ bool join_irc_channel(const Iid& iid, const std::string& nickname, const std::string& password, const std::string& resource);
+
+ void send_channel_message(const Iid& iid, const std::string& body);
+ void send_private_message(const Iid& iid, const std::string& body, const std::string& type="PRIVMSG");
+ void send_raw_message(const std::string& hostname, const std::string& body);
+ void leave_irc_channel(Iid&& iid, std::string&& status_message, const std::string& resource);
+ void send_irc_nick_change(const Iid& iid, const std::string& new_nick);
+ void send_irc_kick(const Iid& iid, const std::string& target, const std::string& reason,
+ const std::string& iq_id, const std::string& to_jid);
+ void set_channel_topic(const Iid& iid, const std::string& subject);
+ void send_xmpp_version_to_irc(const Iid& iid, const std::string& name, const std::string& version,
+ const std::string& os);
+ void send_irc_ping_result(const Iid& iid, const std::string& id);
+ void send_irc_version_request(const std::string& irc_hostname, const std::string& target,
+ const std::string& iq_id, const std::string& to_jid,
+ const std::string& from_jid);
+ void send_irc_channel_list_request(const Iid& iid, const std::string& iq_id,
+ const std::string& to_jid);
+ void forward_affiliation_role_change(const Iid& iid, const std::string& nick,
+ const std::string& affiliation, const std::string& role);
+ /**
+ * Directly send a CTCP PING request to the IRC user
+ */
+ void send_irc_user_ping_request(const std::string& irc_hostname, const std::string& nick,
+ const std::string& iq_id, const std::string& to_jid,
+ const std::string& from_jid);
+ /**
+ * First check if the participant is in the room, before sending a direct
+ * CTCP PING request to the IRC user
+ */
+ void send_irc_participant_ping_request(const Iid& iid, const std::string& nick,
+ const std::string& iq_id, const std::string& to_jid,
+ const std::string& from_jid);
+ /**
+ * Directly send back a result if it's a gateway ping or if we are
+ * connected to the given IRC server, an error otherwise.
+ */
+ void on_gateway_ping(const std::string& irc_hostname, const std::string& iq_id, const std::string& to_jid,
+ const std::string& from_jid);
+
+ /***
+ **
+ ** From IRC to XMPP.
+ **
+ **/
+
+ /**
+ * Send a message corresponding to a server NOTICE, the from attribute
+ * should be juste the server hostname.
+ */
+ void send_xmpp_message(const std::string& from, const std::string& author, const std::string& msg);
+ /**
+ * Send the presence of a new user in the MUC.
+ */
+ void send_user_join(const std::string& hostname, const std::string& chan_name,
+ const IrcUser* user, const char user_mode,
+ const bool self, const std::string& resource);
+ void send_user_join(const std::string& hostname, const std::string& chan_name,
+ const IrcUser* user, const char user_mode,
+ const bool self);
+
+ /**
+ * Send the topic of the MUC to the user
+ */
+ void send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic, const std::string& who);
+ void send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic, const std::string& who, const std::string& resource);
+ /**
+ * Send a MUC message from some participant
+ */
+ void send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc);
+ /**
+ * Send a presence of type error, from a room.
+ */
+ void send_presence_error(const Iid& iid, const std::string& nick, const std::string& type, const std::string& condition, const std::string& error_code, const std::string& text);
+ /**
+ * Send an unavailable presence from this participant
+ */
+ void send_muc_leave(Iid&& iid, std::string&& nick, const std::string& message, const bool self, const std::string& resource="");
+ /**
+ * Send presences to indicate that an user old_nick (ourself if self ==
+ * true) changed his nick to new_nick. The user_mode is needed because
+ * the xmpp presence needs ton contain the role and affiliation of the
+ * user.
+ */
+ void send_nick_change(Iid&& iid,
+ const std::string& old_nick,
+ const std::string& new_nick,
+ const char user_mode,
+ const bool self);
+ void kick_muc_user(Iid&& iid, const std::string& target, const std::string& reason, const std::string& author);
+ void send_nickname_conflict_error(const Iid& iid, const std::string& nickname);
+ /**
+ * Send a role/affiliation change, matching the change of mode for that user
+ */
+ void send_affiliation_role_change(const Iid& iid, const std::string& target, const char mode);
+ /**
+ * Send an iq version request coming from nick!hostname@
+ */
+ void send_iq_version_request(const std::string& nick, const std::string& hostname);
+ /**
+ * Send an iq ping request coming from nick!hostname@
+ */
+ void send_xmpp_ping_request(const std::string& nick, const std::string& hostname,
+ const std::string& id);
+ /**
+ * Misc
+ */
+ std::string get_own_nick(const Iid& iid);
+ /**
+ * Get the number of server to which this bridge is connected or connecting.
+ */
+ size_t active_clients() const;
+ /**
+ * Add (or replace the existing) <nick, jid> into the preferred_user_from map
+ */
+ void set_preferred_from_jid(const std::string& nick, const std::string& full_jid);
+ /**
+ * Remove the preferred jid for the given IRC nick
+ */
+ void remove_preferred_from_jid(const std::string& nick);
+ /**
+ * Given a channel_name, remove all preferred from_jid that come
+ * from this chan.
+ */
+ void remove_all_preferred_from_jid_of_room(const std::string& channel_name);
+ /**
+ * Add a callback to the waiting list of irc callbacks.
+ */
+ void add_waiting_irc(irc_responder_callback_t&& callback);
+ /**
+ * Iter over all the waiting_iq, call the iq_responder_filter_t for each,
+ * whenever one of them returns true: call the corresponding
+ * 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::shared_ptr<IrcClient>>& get_irc_clients();
+
+private:
+ /**
+ * Returns the client for the given hostname, create one (and use the
+ * username in this case) if none is found, and connect that newly-created
+ * client immediately.
+ */
+ IrcClient* make_irc_client(const std::string& hostname, const std::string& nickname);
+ /**
+ * This version does not create the IrcClient if it does not exist, throws
+ * a IRCServerNotConnected error in that case.
+ */
+ IrcClient* get_irc_client(const std::string& hostname);
+ /**
+ * Idem, but returns nullptr if the server does not exist.
+ */
+ IrcClient* find_irc_client(const std::string& hostname);
+ /**
+ * The bare JID of the user associated with this bridge. Messages from/to this
+ * JID are only managed by this bridge.
+ */
+ const std::string user_jid;
+ /**
+ * 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::shared_ptr<IrcClient>> irc_clients;
+ /**
+ * To communicate back with the XMPP component
+ */
+ BiboumiComponent& xmpp;
+ /**
+ * Poller, to give it the IrcClients that we spawn, to make it manage
+ * their sockets.
+ */
+ std::shared_ptr<Poller> poller;
+ /**
+ * A map of <nick, full_jid>. For example if this map contains <"toto",
+ * "#somechan%server@biboumi/ToTo">, whenever a private message is
+ * received from the user "toto", instead of forwarding it to XMPP with
+ * from='toto!server@biboumi', we use instead
+ * from='#somechan%server@biboumi/ToTo'
+ */
+ std::unordered_map<std::string, std::string> preferred_user_from;
+ /**
+ * A list of callbacks that are waiting for some IrcMessage to trigger a
+ * response. We add callbacks in this list whenever we received an IQ
+ * request and we need a response from IRC to be able to provide the
+ * response iq.
+ */
+ std::vector<irc_responder_callback_t> waiting_irc;
+
+ /**
+ * Resources to IRC channel/server mapping:
+ */
+ using Resource = std::string;
+ using ChannelName = std::string;
+ using IrcHostname = std::string;
+ using ChannelKey = std::tuple<ChannelName, IrcHostname>;
+ std::map<ChannelKey, std::set<Resource>> resources_in_chan;
+ std::map<IrcHostname, std::set<Resource>> resources_in_server;
+ /**
+ * Manage which resource is in which channel
+ */
+ void add_resource_to_chan(const ChannelKey& channel_key, const std::string& resource);
+ void remove_resource_from_chan(const ChannelKey& channel_key, const std::string& resource);
+ bool is_resource_in_chan(const ChannelKey& channel_key, const std::string& resource) const;
+ std::size_t number_of_resources_in_chan(const ChannelKey& channel_key) const;
+
+ void add_resource_to_server(const IrcHostname& irc_hostname, const std::string& resource);
+ void remove_resource_from_server(const IrcHostname& irc_hostname, const std::string& resource);
+ bool is_resource_in_server(const IrcHostname& irc_hostname, const std::string& resource) const;
+ size_t number_of_channels_the_resource_is_in(const std::string& irc_hostname, const std::string& resource) const;
+
+ /**
+ * Generate all the stanzas to be sent to this resource, simulating a join on this channel.
+ * This means sending the whole user list, the topic, etc
+ * TODO: send message history
+ */
+ void generate_channel_join_for_resource(const Iid& iid, const std::string& resource);
+};
+
+struct IRCNotConnected: public std::exception
+{
+ IRCNotConnected(const std::string& hostname):
+ hostname(hostname) {}
+ const std::string hostname;
+};
+
+
diff --git a/src/bridge/colors.cpp b/src/bridge/colors.cpp
new file mode 100644
index 0000000..66f51ee
--- /dev/null
+++ b/src/bridge/colors.cpp
@@ -0,0 +1,170 @@
+#include <bridge/colors.hpp>
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <algorithm>
+#include <iostream>
+
+#include <string.h>
+
+using namespace std::string_literals;
+
+static const char IRC_NUM_COLORS = 16;
+
+static const char* irc_colors_to_css[IRC_NUM_COLORS] = {
+ "white",
+ "black",
+ "blue",
+ "green",
+ "indianred",
+ "red",
+ "magenta",
+ "brown",
+ "yellow",
+ "lightgreen",
+ "cyan",
+ "lightcyan",
+ "lightblue",
+ "lightmagenta",
+ "gray",
+ "white",
+};
+
+#define XHTML_NS "http://www.w3.org/1999/xhtml"
+
+struct styles_t
+{
+ bool strong;
+ bool underline;
+ bool italic;
+ int fg;
+ int bg;
+};
+
+/** We keep the currently-applied CSS styles in a structure. Each time a tag
+ * is found, update this style list, then close the current span XML element
+ * (if it is open), then reopen it with all the new styles in it. This is
+ * done this way because IRC formatting does not map well with XML
+ * (hierarchical tags), it’s a lot easier and cleaner to remove all styles
+ * and reapply them for each tag, instead of trying to keep a consistent
+ * hierarchy of span, strong, em etc tags. The generated XML is one-level
+ * deep only.
+*/
+Xmpp::body irc_format_to_xhtmlim(const std::string& s)
+{
+ if (s.find_first_of(irc_format_char) == std::string::npos)
+ // there is no special formatting at all
+ return std::make_tuple(s, nullptr);
+
+ std::string cleaned;
+
+ styles_t styles = {false, false, false, -1, -1};
+
+ std::unique_ptr<XmlNode> result = std::make_unique<XmlNode>("body");
+ (*result)["xmlns"] = XHTML_NS;
+
+ std::unique_ptr<XmlNode> current_node_up;
+ XmlNode* current_node = result.get();
+
+ std::string::size_type pos_start = 0;
+ std::string::size_type pos_end;
+
+ while ((pos_end = s.find_first_of(irc_format_char, pos_start)) != std::string::npos)
+ {
+ const std::string txt = s.substr(pos_start, pos_end-pos_start);
+ cleaned += txt;
+ if (current_node->has_children())
+ current_node->get_last_child()->add_to_tail(txt);
+ else
+ current_node->add_to_inner(txt);
+
+ if (s[pos_end] == IRC_FORMAT_BOLD_CHAR)
+ styles.strong = !styles.strong;
+ else if (s[pos_end] == IRC_FORMAT_NEWLINE_CHAR)
+ {
+ current_node->add_child(std::make_unique<XmlNode>("br"));
+ cleaned += '\n';
+ }
+ else if (s[pos_end] == IRC_FORMAT_UNDERLINE_CHAR)
+ styles.underline = !styles.underline;
+ else if (s[pos_end] == IRC_FORMAT_ITALIC_CHAR)
+ styles.italic = !styles.italic;
+ else if (s[pos_end] == IRC_FORMAT_RESET_CHAR)
+ styles = {false, false, false, -1, -1};
+ else if (s[pos_end] == IRC_FORMAT_REVERSE_CHAR)
+ { } // TODO
+ else if (s[pos_end] == IRC_FORMAT_REVERSE2_CHAR)
+ { } // TODO
+ else if (s[pos_end] == IRC_FORMAT_FIXED_CHAR)
+ { } // TODO
+ else if (s[pos_end] == IRC_FORMAT_COLOR_CHAR)
+ {
+ size_t pos = pos_end + 1;
+ styles.fg = -1;
+ styles.bg = -1;
+ // get the first number following the format char
+ if (pos < s.size() && s[pos] >= '0' && s[pos] <= '9')
+ { // first digit
+ styles.fg = s[pos++] - '0';
+ if (pos < s.size() && s[pos] >= '0' && s[pos] <= '9')
+ // second digit
+ styles.fg = styles.fg * 10 + s[pos++] - '0';
+ }
+ if (pos < s.size() && s[pos] == ',')
+ { // get bg color after the comma
+ pos++;
+ if (pos < s.size() && s[pos] >= '0' && s[pos] <= '9')
+ { // first digit
+ styles.bg = s[pos++] - '0';
+ if (pos < s.size() && s[pos] >= '0' && s[pos] <= '9')
+ // second digit
+ styles.bg = styles.bg * 10 + s[pos++] - '0';
+ }
+ }
+ pos_end = pos - 1;
+ }
+
+ // close opened span, if any
+ if (current_node != result.get())
+ {
+ result->add_child(std::move(current_node_up));
+ current_node = result.get();
+ }
+ // Take all currently-applied style and create a new span with it
+ std::string styles_str;
+ if (styles.strong)
+ styles_str += "font-weight:bold;";
+ if (styles.underline)
+ styles_str += "text-decoration:underline;";
+ if (styles.italic)
+ styles_str += "font-style:italic;";
+ if (styles.fg != -1)
+ styles_str += "color:"s +
+ irc_colors_to_css[styles.fg % IRC_NUM_COLORS] + ";";
+ if (styles.bg != -1)
+ styles_str += "background-color:"s +
+ irc_colors_to_css[styles.bg % IRC_NUM_COLORS] + ";";
+ if (!styles_str.empty())
+ {
+ current_node_up = std::make_unique<XmlNode>("span");
+ current_node = current_node_up.get();
+ (*current_node)["style"] = styles_str;
+ }
+
+ pos_start = pos_end + 1;
+ }
+
+ // If some text remains, without any format char, just append that text at
+ // the end of the current node
+ const std::string txt = s.substr(pos_start, pos_end-pos_start);
+ cleaned += txt;
+ if (current_node->has_children())
+ current_node->get_last_child()->add_to_tail(txt);
+ else
+ current_node->add_to_inner(txt);
+
+ if (current_node != result.get())
+ result->add_child(std::move(current_node_up));
+
+ Xmpp::body body_res = std::make_tuple(cleaned, std::move(result));
+ return body_res;
+}
diff --git a/src/bridge/colors.hpp b/src/bridge/colors.hpp
new file mode 100644
index 0000000..e2c8a87
--- /dev/null
+++ b/src/bridge/colors.hpp
@@ -0,0 +1,56 @@
+#pragma once
+
+
+/**
+ * A module handling the conversion between IRC colors and XHTML-IM, and
+ * vice versa.
+ */
+
+#include <string>
+#include <memory>
+#include <tuple>
+
+class XmlNode;
+
+namespace Xmpp
+{
+// Contains:
+// - an XMPP-valid UTF-8 body
+// - an XML node representing the XHTML-IM body, or null
+ using body = std::tuple<const std::string, std::unique_ptr<XmlNode>>;
+}
+
+#define IRC_FORMAT_BOLD_CHAR '\x02' // done
+#define IRC_FORMAT_COLOR_CHAR '\x03' // done
+#define IRC_FORMAT_RESET_CHAR '\x0F' // done
+#define IRC_FORMAT_FIXED_CHAR '\x11' // ??
+#define IRC_FORMAT_REVERSE_CHAR '\x12' // maybe one day
+#define IRC_FORMAT_REVERSE2_CHAR '\x16' // wat
+#define IRC_FORMAT_ITALIC_CHAR '\x1D' // done
+#define IRC_FORMAT_UNDERLINE_CHAR '\x1F' // done
+#define IRC_FORMAT_NEWLINE_CHAR '\n' // done
+
+static const char irc_format_char[] = {
+ IRC_FORMAT_BOLD_CHAR,
+ IRC_FORMAT_COLOR_CHAR,
+ IRC_FORMAT_RESET_CHAR,
+ IRC_FORMAT_FIXED_CHAR,
+ IRC_FORMAT_REVERSE_CHAR,
+ IRC_FORMAT_REVERSE2_CHAR,
+ IRC_FORMAT_ITALIC_CHAR,
+ IRC_FORMAT_UNDERLINE_CHAR,
+ IRC_FORMAT_NEWLINE_CHAR,
+ '\x00'
+};
+
+/**
+ * Convert the passed string into an XML tree representing the XHTML version
+ * of the message, converting the IRC colors symbols into xhtml-im
+ * formatting.
+ *
+ * Returns the body cleaned from any IRC formatting (but without any xhtml),
+ * and the body as XHTML-IM
+ */
+Xmpp::body irc_format_to_xhtmlim(const std::string& str);
+
+
diff --git a/src/bridge/list_element.hpp b/src/bridge/list_element.hpp
new file mode 100644
index 0000000..1eff2ee
--- /dev/null
+++ b/src/bridge/list_element.hpp
@@ -0,0 +1,19 @@
+#pragma once
+
+
+#include <string>
+
+struct ListElement
+{
+ ListElement(const std::string& channel, const std::string& nb_users,
+ const std::string& topic):
+ channel(channel),
+ nb_users(nb_users),
+ topic(topic){}
+
+ std::string channel;
+ std::string nb_users;
+ std::string topic;
+};
+
+
diff --git a/src/database/database.cpp b/src/database/database.cpp
new file mode 100644
index 0000000..61e1b47
--- /dev/null
+++ b/src/database/database.cpp
@@ -0,0 +1,87 @@
+#include "biboumi.h"
+#ifdef USE_DATABASE
+
+#include <database/database.hpp>
+#include <logger/logger.hpp>
+#include <string>
+
+using namespace std::string_literals;
+
+std::unique_ptr<db::BibouDB> Database::db;
+
+void Database::open(const std::string& filename, const std::string& db_type)
+{
+ try
+ {
+ auto new_db = std::make_unique<db::BibouDB>(db_type,
+ "database="s + filename);
+ if (new_db->needsUpgrade())
+ new_db->upgrade();
+ Database::db.reset(new_db.release());
+ } catch (const litesql::DatabaseError& e) {
+ log_error("Failed to open database ", filename, ". ", e.what());
+ throw;
+ }
+}
+
+void Database::set_verbose(const bool val)
+{
+ Database::db->verbose = val;
+}
+
+db::IrcServerOptions Database::get_irc_server_options(const std::string& owner,
+ const std::string& server)
+{
+ try {
+ auto options = litesql::select<db::IrcServerOptions>(*Database::db,
+ db::IrcServerOptions::Owner == owner &&
+ db::IrcServerOptions::Server == server).one();
+ return options;
+ } catch (const litesql::NotFound& e) {
+ db::IrcServerOptions options(*Database::db);
+ options.owner = owner;
+ options.server = server;
+ // options.update();
+ return options;
+ }
+}
+
+db::IrcChannelOptions Database::get_irc_channel_options(const std::string& owner,
+ const std::string& server,
+ const std::string& channel)
+{
+ try {
+ auto options = litesql::select<db::IrcChannelOptions>(*Database::db,
+ db::IrcChannelOptions::Owner == owner &&
+ db::IrcChannelOptions::Server == server &&
+ db::IrcChannelOptions::Channel == channel).one();
+ return options;
+ } catch (const litesql::NotFound& e) {
+ db::IrcChannelOptions options(*Database::db);
+ options.owner = owner;
+ options.server = server;
+ options.channel = channel;
+ return options;
+ }
+}
+
+db::IrcChannelOptions Database::get_irc_channel_options_with_server_default(const std::string& owner,
+ const std::string& server,
+ const std::string& channel)
+{
+ auto coptions = Database::get_irc_channel_options(owner, server, channel);
+ auto soptions = Database::get_irc_server_options(owner, server);
+ if (coptions.encodingIn.value().empty())
+ coptions.encodingIn = soptions.encodingIn;
+ if (coptions.encodingOut.value().empty())
+ coptions.encodingOut = soptions.encodingOut;
+
+ return coptions;
+}
+
+void Database::close()
+{
+ Database::db.reset(nullptr);
+}
+
+#endif
diff --git a/src/database/database.hpp b/src/database/database.hpp
new file mode 100644
index 0000000..7173bcd
--- /dev/null
+++ b/src/database/database.hpp
@@ -0,0 +1,53 @@
+#pragma once
+
+
+#include <biboumi.h>
+#ifdef USE_DATABASE
+
+#include "biboudb.hpp"
+
+#include <memory>
+
+#include <litesql.hpp>
+
+class Database
+{
+public:
+ Database() = default;
+ ~Database() = default;
+
+ Database(const Database&) = delete;
+ Database(Database&&) = delete;
+ Database& operator=(const Database&) = delete;
+ Database& operator=(Database&&) = delete;
+
+ static void set_verbose(const bool val);
+
+ template<typename PersistentType>
+ static size_t count()
+ {
+ return litesql::select<PersistentType>(*Database::db).count();
+ }
+ /**
+ * Return the object from the db. Create it beforehand (with all default
+ * values) if it is not already present.
+ */
+ static db::IrcServerOptions get_irc_server_options(const std::string& owner,
+ const std::string& server);
+ static db::IrcChannelOptions get_irc_channel_options(const std::string& owner,
+ const std::string& server,
+ const std::string& channel);
+ static db::IrcChannelOptions get_irc_channel_options_with_server_default(const std::string& owner,
+ const std::string& server,
+ const std::string& channel);
+
+ static void close();
+ static void open(const std::string& filename, const std::string& db_type="sqlite3");
+
+
+private:
+ static std::unique_ptr<db::BibouDB> db;
+};
+#endif /* USE_DATABASE */
+
+
diff --git a/src/irc/iid.cpp b/src/irc/iid.cpp
new file mode 100644
index 0000000..0e2841e
--- /dev/null
+++ b/src/irc/iid.cpp
@@ -0,0 +1,118 @@
+#include <utils/tolower.hpp>
+#include <config/config.hpp>
+
+#include <irc/iid.hpp>
+
+#include <utils/encoding.hpp>
+
+Iid::Iid(const std::string& iid):
+ is_channel(false),
+ is_user(false)
+{
+ const std::string fixed_irc_server = Config::get("fixed_irc_server", "");
+ if (fixed_irc_server.empty())
+ this->init(iid);
+ else
+ this->init_with_fixed_server(iid, fixed_irc_server);
+}
+
+
+void Iid::init(const std::string& iid)
+{
+ const std::string::size_type sep = iid.find_first_of("%!");
+ if (sep != std::string::npos)
+ {
+ if (iid[sep] == '%')
+ this->is_channel = true;
+ else
+ this->is_user = true;
+ this->set_local(iid.substr(0, sep));
+ this->set_server(iid.substr(sep + 1));
+ }
+ else
+ this->set_server(iid);
+}
+
+void Iid::init_with_fixed_server(const std::string& iid, const std::string& hostname)
+{
+ this->set_server(hostname);
+
+ const std::string::size_type sep = iid.find("!");
+
+ // Without any separator, we consider that it's a channel
+ if (sep == std::string::npos)
+ {
+ this->is_channel = true;
+ this->set_local(iid);
+ }
+ else // A separator can be present to differenciate a channel from a user,
+ // but the part behind it (the hostname) is ignored
+ {
+ this->set_local(iid.substr(0, sep));
+ this->is_user = true;
+ }
+}
+
+Iid::Iid():
+ is_channel(false),
+ is_user(false)
+{
+}
+
+void Iid::set_local(const std::string& loc)
+{
+ std::string local(utils::tolower(loc));
+ xep0106::decode(local);
+ this->local = local;
+}
+
+void Iid::set_server(const std::string& serv)
+{
+ this->server = utils::tolower(serv);
+}
+
+const std::string& Iid::get_local() const
+{
+ return this->local;
+}
+
+const std::string Iid::get_encoded_local() const
+{
+ std::string local(this->local);
+ xep0106::encode(local);
+ return local;
+}
+
+const std::string& Iid::get_server() const
+{
+ return this->server;
+}
+
+std::string Iid::get_sep() const
+{
+ if (this->is_channel)
+ return "%";
+ else if (this->is_user)
+ return "!";
+ return "";
+}
+
+namespace std {
+ const std::string to_string(const Iid& iid)
+ {
+ if (Config::get("fixed_irc_server", "").empty())
+ return iid.get_encoded_local() + iid.get_sep() + iid.get_server();
+ else
+ {
+ if (iid.get_sep() == "!")
+ return iid.get_encoded_local() + iid.get_sep();
+ else
+ return iid.get_encoded_local();
+ }
+ }
+}
+
+std::tuple<std::string, std::string> Iid::to_tuple() const
+{
+ return std::make_tuple(this->get_local(), this->get_server());
+}
diff --git a/src/irc/iid.hpp b/src/irc/iid.hpp
new file mode 100644
index 0000000..3b11470
--- /dev/null
+++ b/src/irc/iid.hpp
@@ -0,0 +1,79 @@
+#pragma once
+
+
+#include <string>
+
+/**
+ * A name representing an IRC channel on an IRC server, or an IRC user on an
+ * IRC server, or just an IRC server.
+ *
+ * The separator for an user is '!', for a channel it's '%'. If no separator
+ * is present, it's just an irc server.
+ * It’s possible to have an empty-string server, but it makes no sense in
+ * the biboumi context.
+ *
+ * #test%irc.example.org has :
+ * - local: "#test" (the # is part of the name, it could very well be absent, or & (for example) instead)
+ * - server: "irc.example.org"
+ * - is_channel: true
+ * - is_user: false
+ *
+ * %irc.example.org:
+ * - local: ""
+ * - server: "irc.example.org"
+ * - is_channel: true
+ * - is_user: false
+ * Note: this is the special empty-string channel, used internal in biboumi
+ * but has no meaning on IRC.
+ *
+ * foo!irc.example.org
+ * - local: "foo"
+ * - server: "irc.example.org"
+ * - is_channel: false
+ * - is_user: true
+ * Note: the empty-string user (!irc.example.org) has no special meaning in biboumi
+ *
+ * irc.example.org:
+ * - local: ""
+ * - server: "irc.example.org"
+ * - is_channel: false
+ * - is_user: false
+ */
+class Iid
+{
+public:
+ Iid(const std::string& iid);
+ Iid();
+ Iid(const Iid&) = default;
+
+ Iid(Iid&&) = delete;
+ Iid& operator=(const Iid&) = delete;
+ Iid& operator=(Iid&&) = delete;
+
+ void set_local(const std::string& loc);
+ void set_server(const std::string& serv);
+ const std::string& get_local() const;
+ const std::string get_encoded_local() const;
+ const std::string& get_server() const;
+
+ bool is_channel;
+ bool is_user;
+
+ std::string get_sep() const;
+
+ std::tuple<std::string, std::string> to_tuple() const;
+
+private:
+
+ void init(const std::string& iid);
+ void init_with_fixed_server(const std::string& iid, const std::string& hostname);
+
+ std::string local;
+ std::string server;
+};
+
+namespace std {
+ const std::string to_string(const Iid& iid);
+}
+
+
diff --git a/src/irc/irc_channel.cpp b/src/irc/irc_channel.cpp
new file mode 100644
index 0000000..e769245
--- /dev/null
+++ b/src/irc/irc_channel.cpp
@@ -0,0 +1,60 @@
+#include <irc/irc_channel.hpp>
+#include <algorithm>
+
+IrcChannel::IrcChannel():
+ joined(false),
+ self(nullptr)
+{
+}
+
+void IrcChannel::set_self(const std::string& name)
+{
+ this->self = std::make_unique<IrcUser>(name);
+}
+
+IrcUser* IrcChannel::add_user(const std::string& name,
+ const std::map<char, char>& prefix_to_mode)
+{
+ this->users.emplace_back(std::make_unique<IrcUser>(name, prefix_to_mode));
+ return this->users.back().get();
+}
+
+IrcUser* IrcChannel::get_self() const
+{
+ return this->self.get();
+}
+
+IrcUser* IrcChannel::find_user(const std::string& name) const
+{
+ IrcUser user(name);
+ for (const auto& u: this->users)
+ {
+ if (u->nick == user.nick)
+ return u.get();
+ }
+ return nullptr;
+}
+
+void IrcChannel::remove_user(const IrcUser* user)
+{
+ const auto nick = user->nick;
+ const auto it = std::find_if(this->users.begin(), this->users.end(),
+ [nick](const std::unique_ptr<IrcUser>& u)
+ {
+ return nick == u->nick;
+ });
+ if (it != this->users.end())
+ this->users.erase(it);
+}
+
+void IrcChannel::remove_all_users()
+{
+ this->users.clear();
+ this->self.reset();
+}
+
+DummyIrcChannel::DummyIrcChannel():
+ IrcChannel(),
+ joining(false)
+{
+}
diff --git a/src/irc/irc_channel.hpp b/src/irc/irc_channel.hpp
new file mode 100644
index 0000000..2bcefaf
--- /dev/null
+++ b/src/irc/irc_channel.hpp
@@ -0,0 +1,70 @@
+#pragma once
+
+
+#include <irc/irc_user.hpp>
+#include <memory>
+#include <string>
+#include <vector>
+#include <map>
+
+/**
+ * Keep the state of a joined channel (the list of occupants with their
+ * informations (mode, etc), the modes, etc)
+ */
+class IrcChannel
+{
+public:
+ explicit IrcChannel();
+
+ IrcChannel(const IrcChannel&) = delete;
+ IrcChannel(IrcChannel&&) = delete;
+ IrcChannel& operator=(const IrcChannel&) = delete;
+ IrcChannel& operator=(IrcChannel&&) = delete;
+
+ bool joined;
+ std::string topic;
+ std::string topic_author;
+ void set_self(const std::string& name);
+ IrcUser* get_self() const;
+ IrcUser* add_user(const std::string& name,
+ const std::map<char, char>& prefix_to_mode);
+ IrcUser* find_user(const std::string& name) const;
+ void remove_user(const IrcUser* user);
+ void remove_all_users();
+ const std::vector<std::unique_ptr<IrcUser>>& get_users() const
+ { return this->users; }
+
+protected:
+ std::unique_ptr<IrcUser> self;
+ std::vector<std::unique_ptr<IrcUser>> users;
+};
+
+/**
+ * A special channel that is not actually linked to any real irc
+ * channel. This is just a channel representing a connection to the
+ * server. If an user wants to maintain the connection to the server without
+ * having to be on any irc channel of that server, he can just join this
+ * dummy channel.
+ * It’s not actually dummy because it’s useful and it does things, but well.
+ */
+class DummyIrcChannel: public IrcChannel
+{
+public:
+ explicit DummyIrcChannel();
+ DummyIrcChannel(const DummyIrcChannel&) = delete;
+ DummyIrcChannel(DummyIrcChannel&&) = delete;
+ DummyIrcChannel& operator=(const DummyIrcChannel&) = delete;
+ DummyIrcChannel& operator=(DummyIrcChannel&&) = delete;
+
+ /**
+ * This flag is at true whenever the user wants to join this channel, but
+ * he is not yet connected to the server. When the connection is made, we
+ * check that flag and if it’s true, we inform the user that he has just
+ * joined that channel.
+ * If the user is already connected to the server when he tries to join
+ * the channel, we don’t use that flag, we just join it immediately.
+ */
+ bool joining;
+};
+
+
diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp
new file mode 100644
index 0000000..dd83307
--- /dev/null
+++ b/src/irc/irc_client.cpp
@@ -0,0 +1,1120 @@
+#include <utils/timed_events.hpp>
+#include <database/database.hpp>
+#include <irc/irc_message.hpp>
+#include <irc/irc_client.hpp>
+#include <bridge/bridge.hpp>
+#include <irc/irc_user.hpp>
+
+#include <logger/logger.hpp>
+#include <config/config.hpp>
+#include <utils/tolower.hpp>
+#include <utils/split.hpp>
+#include <utils/string.hpp>
+
+#include <sstream>
+#include <iostream>
+#include <stdexcept>
+#include <cstring>
+
+#include <chrono>
+#include <string>
+
+#include "biboumi.h"
+#include "louloulibs.h"
+
+using namespace std::string_literals;
+using namespace std::chrono_literals;
+
+/**
+ * Define a map of functions to be called for each IRC command we can
+ * handle.
+ */
+using IrcCallback = void (IrcClient::*)(const IrcMessage&);
+
+static const std::unordered_map<std::string,
+ std::pair<IrcCallback, std::pair<std::size_t, std::size_t>>> irc_callbacks = {
+ {"NOTICE", {&IrcClient::on_notice, {2, 0}}},
+ {"002", {&IrcClient::forward_server_message, {2, 0}}},
+ {"003", {&IrcClient::forward_server_message, {2, 0}}},
+ {"004", {&IrcClient::on_server_myinfo, {4, 0}}},
+ {"005", {&IrcClient::on_isupport_message, {0, 0}}},
+ {"RPL_LISTSTART", {&IrcClient::on_rpl_liststart, {0, 0}}},
+ {"321", {&IrcClient::on_rpl_liststart, {0, 0}}},
+ {"RPL_LIST", {&IrcClient::on_rpl_list, {0, 0}}},
+ {"322", {&IrcClient::on_rpl_list, {0, 0}}},
+ {"RPL_LISTEND", {&IrcClient::on_rpl_listend, {0, 0}}},
+ {"323", {&IrcClient::on_rpl_listend, {0, 0}}},
+ {"RPL_NOTOPIC", {&IrcClient::on_empty_topic, {0, 0}}},
+ {"331", {&IrcClient::on_empty_topic, {0, 0}}},
+ {"RPL_MOTDSTART", {&IrcClient::empty_motd, {0, 0}}},
+ {"375", {&IrcClient::empty_motd, {0, 0}}},
+ {"RPL_MOTD", {&IrcClient::on_motd_line, {2, 0}}},
+ {"372", {&IrcClient::on_motd_line, {2, 0}}},
+ {"RPL_MOTDEND", {&IrcClient::send_motd, {0, 0}}},
+ {"376", {&IrcClient::send_motd, {0, 0}}},
+ {"JOIN", {&IrcClient::on_channel_join, {1, 0}}},
+ {"PRIVMSG", {&IrcClient::on_channel_message, {2, 0}}},
+ {"353", {&IrcClient::set_and_forward_user_list, {4, 0}}},
+ {"332", {&IrcClient::on_topic_received, {2, 0}}},
+ {"TOPIC", {&IrcClient::on_topic_received, {2, 0}}},
+ {"333", {&IrcClient::on_topic_who_time_received, {4, 0}}},
+ {"RPL_TOPICWHOTIME", {&IrcClient::on_topic_who_time_received, {4, 0}}},
+ {"366", {&IrcClient::on_channel_completely_joined, {2, 0}}},
+ {"396", {&IrcClient::on_own_host_received, {2, 0}}},
+ {"432", {&IrcClient::on_erroneous_nickname, {2, 0}}},
+ {"433", {&IrcClient::on_nickname_conflict, {2, 0}}},
+ {"438", {&IrcClient::on_nickname_change_too_fast, {2, 0}}},
+ {"001", {&IrcClient::on_welcome_message, {1, 0}}},
+ {"PART", {&IrcClient::on_part, {1, 0}}},
+ {"ERROR", {&IrcClient::on_error, {1, 0}}},
+ {"QUIT", {&IrcClient::on_quit, {0, 0}}},
+ {"NICK", {&IrcClient::on_nick, {1, 0}}},
+ {"MODE", {&IrcClient::on_mode, {1, 0}}},
+ {"PING", {&IrcClient::send_pong_command, {1, 0}}},
+ {"PONG", {&IrcClient::on_pong, {0, 0}}},
+ {"KICK", {&IrcClient::on_kick, {3, 0}}},
+
+ {"401", {&IrcClient::on_generic_error, {2, 0}}},
+ {"402", {&IrcClient::on_generic_error, {2, 0}}},
+ {"403", {&IrcClient::on_generic_error, {2, 0}}},
+ {"404", {&IrcClient::on_generic_error, {2, 0}}},
+ {"405", {&IrcClient::on_generic_error, {2, 0}}},
+ {"406", {&IrcClient::on_generic_error, {2, 0}}},
+ {"407", {&IrcClient::on_generic_error, {2, 0}}},
+ {"408", {&IrcClient::on_generic_error, {2, 0}}},
+ {"409", {&IrcClient::on_generic_error, {2, 0}}},
+ {"410", {&IrcClient::on_generic_error, {2, 0}}},
+ {"411", {&IrcClient::on_generic_error, {2, 0}}},
+ {"412", {&IrcClient::on_generic_error, {2, 0}}},
+ {"414", {&IrcClient::on_generic_error, {2, 0}}},
+ {"421", {&IrcClient::on_generic_error, {2, 0}}},
+ {"422", {&IrcClient::on_generic_error, {2, 0}}},
+ {"423", {&IrcClient::on_generic_error, {2, 0}}},
+ {"424", {&IrcClient::on_generic_error, {2, 0}}},
+ {"431", {&IrcClient::on_generic_error, {2, 0}}},
+ {"436", {&IrcClient::on_generic_error, {2, 0}}},
+ {"441", {&IrcClient::on_generic_error, {2, 0}}},
+ {"442", {&IrcClient::on_generic_error, {2, 0}}},
+ {"443", {&IrcClient::on_generic_error, {2, 0}}},
+ {"444", {&IrcClient::on_generic_error, {2, 0}}},
+ {"446", {&IrcClient::on_generic_error, {2, 0}}},
+ {"451", {&IrcClient::on_generic_error, {2, 0}}},
+ {"461", {&IrcClient::on_generic_error, {2, 0}}},
+ {"462", {&IrcClient::on_generic_error, {2, 0}}},
+ {"463", {&IrcClient::on_generic_error, {2, 0}}},
+ {"464", {&IrcClient::on_generic_error, {2, 0}}},
+ {"465", {&IrcClient::on_generic_error, {2, 0}}},
+ {"467", {&IrcClient::on_generic_error, {2, 0}}},
+ {"470", {&IrcClient::on_generic_error, {2, 0}}},
+ {"471", {&IrcClient::on_generic_error, {2, 0}}},
+ {"472", {&IrcClient::on_generic_error, {2, 0}}},
+ {"473", {&IrcClient::on_generic_error, {2, 0}}},
+ {"474", {&IrcClient::on_generic_error, {2, 0}}},
+ {"475", {&IrcClient::on_generic_error, {2, 0}}},
+ {"476", {&IrcClient::on_generic_error, {2, 0}}},
+ {"477", {&IrcClient::on_generic_error, {2, 0}}},
+ {"481", {&IrcClient::on_generic_error, {2, 0}}},
+ {"482", {&IrcClient::on_generic_error, {2, 0}}},
+ {"483", {&IrcClient::on_generic_error, {2, 0}}},
+ {"484", {&IrcClient::on_generic_error, {2, 0}}},
+ {"485", {&IrcClient::on_generic_error, {2, 0}}},
+ {"487", {&IrcClient::on_generic_error, {2, 0}}},
+ {"491", {&IrcClient::on_generic_error, {2, 0}}},
+ {"501", {&IrcClient::on_generic_error, {2, 0}}},
+ {"502", {&IrcClient::on_generic_error, {2, 0}}},
+};
+
+IrcClient::IrcClient(std::shared_ptr<Poller> poller, const std::string& hostname,
+ const std::string& nickname, const std::string& username,
+ const std::string& realname, const std::string& user_hostname,
+ Bridge& bridge):
+ TCPSocketHandler(poller),
+ hostname(hostname),
+ user_hostname(user_hostname),
+ username(username),
+ realname(realname),
+ current_nick(nickname),
+ bridge(bridge),
+ welcomed(false),
+ chanmodes({"", "", "", ""}),
+ chantypes({'#', '&'})
+{
+ this->dummy_channel.topic = "This is a virtual channel provided for "
+ "convenience by biboumi, it is not connected "
+ "to any actual IRC channel of the server '" + this->hostname +
+ "', and sending messages in it has no effect. "
+ "Its main goal is to keep the connection to the IRC server "
+ "alive without having to join a real channel of that server. "
+ "To disconnect from the IRC server, leave this room and all "
+ "other IRC channels of that server.";
+#ifdef USE_DATABASE
+ auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
+ this->get_hostname());
+ std::vector<std::string> ports = utils::split(options.ports, ';', false);
+ for (auto it = ports.rbegin(); it != ports.rend(); ++it)
+ this->ports_to_try.emplace(*it, false);
+# ifdef BOTAN_FOUND
+ ports = utils::split(options.tlsPorts, ';', false);
+ for (auto it = ports.rbegin(); it != ports.rend(); ++it)
+ this->ports_to_try.emplace(*it, true);
+# endif // BOTAN_FOUND
+
+#else // not USE_DATABASE
+ this->ports_to_try.emplace("6667", false); // standard non-encrypted port
+# ifdef BOTAN_FOUND
+ this->ports_to_try.emplace("6670", true); // non-standard but I want it for some servers
+ this->ports_to_try.emplace("6697", true); // standard encrypted port
+# endif // BOTAN_FOUND
+#endif // USE_DATABASE
+}
+
+IrcClient::~IrcClient()
+{
+ // This event may or may not exist (if we never got connected, it
+ // doesn't), but it's ok
+ TimedEventsManager::instance().cancel("PING"s + this->hostname + this->bridge.get_jid());
+}
+
+void IrcClient::start()
+{
+ if (this->is_connecting() || this->is_connected())
+ return;
+ std::string port;
+ bool tls;
+ std::tie(port, tls) = this->ports_to_try.top();
+ this->ports_to_try.pop();
+ this->bridge.send_xmpp_message(this->hostname, "", "Connecting to "s +
+ this->hostname + ":" + port + " (" +
+ (tls ? "encrypted" : "not encrypted") + ")");
+
+ this->bind_addr = Config::get("outgoing_bind", "");
+
+#ifdef BOTAN_FOUND
+# ifdef USE_DATABASE
+ auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
+ this->get_hostname());
+ this->credential_manager.set_trusted_fingerprint(options.trustedFingerprint);
+# endif
+#endif
+ this->connect(this->hostname, port, tls);
+}
+
+void IrcClient::on_connection_failed(const std::string& reason)
+{
+ this->bridge.send_xmpp_message(this->hostname, "",
+ "Connection failed: "s + reason);
+
+ if (this->hostname_resolution_failed)
+ while (!this->ports_to_try.empty())
+ this->ports_to_try.pop();
+
+ if (this->ports_to_try.empty())
+ {
+ // Send an error message for all room that the user wanted to join
+ for (const auto& tuple: this->channels_to_join)
+ {
+ Iid iid(std::get<0>(tuple) + "%" + this->hostname);
+ this->bridge.send_presence_error(iid, this->current_nick,
+ "cancel", "item-not-found",
+ "", reason);
+ }
+ }
+ else // try the next port
+ this->start();
+}
+
+void IrcClient::on_connected()
+{
+ const auto webirc_password = Config::get("webirc_password", "");
+ static std::string resolved_ip;
+
+ if (!webirc_password.empty())
+ {
+ if (!resolved_ip.empty())
+ this->send_webirc_command(webirc_password, resolved_ip);
+ else
+ { // Start resolving the hostname of the user, and call
+ // on_connected again when it’s done
+ this->dns_resolver.resolve(this->user_hostname, "5222",
+ [this](const struct addrinfo* addr)
+ {
+ resolved_ip = addr_to_string(addr);
+ // Only continue the process if we
+ // didn’t get connected while we were
+ // resolving
+ if (this->is_connected())
+ this->on_connected();
+ },
+ [this](const char* error_msg)
+ {
+ if (this->is_connected())
+ {
+ this->on_connection_close("Could not resolve hostname "s + this->user_hostname +
+ ": " + error_msg);
+ this->send_quit_command("");
+ }
+ });
+ return;
+ }
+ }
+
+ this->send_message({"CAP", {"REQ", "multi-prefix"}});
+ this->send_message({"CAP", {"END"}});
+
+#ifdef USE_DATABASE
+ auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
+ this->get_hostname());
+ if (!options.pass.value().empty())
+ this->send_pass_command(options.pass.value());
+#endif
+
+ this->send_nick_command(this->current_nick);
+
+#ifdef USE_DATABASE
+ if (Config::get("realname_customization", "true") == "true")
+ {
+ if (!options.username.value().empty())
+ this->username = options.username.value();
+ if (!options.realname.value().empty())
+ this->realname = options.realname.value();
+ this->send_user_command(username, realname);
+ }
+ else
+ this->send_user_command(this->username, this->realname);
+#else
+ this->send_user_command(this->username, this->realname);
+#endif
+ this->send_gateway_message("Connected to IRC server"s + (this->use_tls ? " (encrypted)": "") + ".");
+ this->send_pending_data();
+}
+
+void IrcClient::on_connection_close(const std::string& error_msg)
+{
+ std::string message = "Connection closed";
+ if (!error_msg.empty())
+ message += ": " + error_msg;
+ else
+ message += ".";
+ const IrcMessage error{"ERROR", {message}};
+ this->on_error(error);
+ log_warning(message);
+}
+
+IrcChannel* IrcClient::get_channel(const std::string& n)
+{
+ if (n.empty())
+ return &this->dummy_channel;
+ const std::string name = utils::tolower(n);
+ try
+ {
+ return this->channels.at(name).get();
+ }
+ catch (const std::out_of_range& exception)
+ {
+ this->channels.emplace(name, std::make_unique<IrcChannel>());
+ return this->channels.at(name).get();
+ }
+}
+
+bool IrcClient::is_channel_joined(const std::string& name)
+{
+ IrcChannel* channel = this->get_channel(name);
+ return channel->joined;
+}
+
+std::string IrcClient::get_own_nick() const
+{
+ return this->current_nick;
+}
+
+void IrcClient::parse_in_buffer(const size_t)
+{
+ while (true)
+ {
+ auto pos = this->in_buf.find("\r\n");
+ if (pos == std::string::npos)
+ break ;
+ IrcMessage message(this->in_buf.substr(0, pos));
+ this->in_buf = this->in_buf.substr(pos + 2, std::string::npos);
+ log_debug("IRC RECEIVING: (", this->get_hostname(), ") ", message);
+
+ // Call the standard callback (if any), associated with the command
+ // name that we just received.
+ auto it = irc_callbacks.find(message.command);
+ if (it != irc_callbacks.end())
+ {
+ const auto& limits = it->second.second;
+ // Check that the Message is well formed before actually calling
+ // the callback. limits.first is the min number of arguments,
+ // second is the max
+ if (message.arguments.size() < limits.first ||
+ (limits.second > 0 && message.arguments.size() > limits.second))
+ log_warning("Invalid number of arguments for IRC command “", message.command,
+ "”: ", message.arguments.size());
+ else
+ {
+ const auto& cb = it->second.first;
+ try {
+ (this->*(cb))(message);
+ } catch (const std::exception& e) {
+ log_error("Unhandled exception: ", e.what());
+ }
+ }
+ }
+ else
+ {
+ log_info("No handler for command ", message.command,
+ ", forwarding the arguments to the user");
+ this->on_unknown_message(message);
+ }
+ // Try to find a waiting_iq, which response will be triggered by this IrcMessage
+ this->bridge.trigger_on_irc_message(this->hostname, message);
+ }
+}
+
+void IrcClient::send_message(IrcMessage&& message)
+{
+ log_debug("IRC SENDING: (", this->get_hostname(), ") ", message);
+ std::string res;
+ if (!message.prefix.empty())
+ res += ":" + std::move(message.prefix) + " ";
+ res += std::move(message.command);
+ for (const std::string& arg: message.arguments)
+ {
+ if (arg.find(" ") != std::string::npos ||
+ (!arg.empty() && arg[0] == ':'))
+ {
+ res += " :" + arg;
+ break;
+ }
+ res += " " + arg;
+ }
+ res += "\r\n";
+ this->send_data(std::move(res));
+}
+
+void IrcClient::send_raw(const std::string& txt)
+{
+ log_debug("IRC SENDING (raw): (", this->get_hostname(), ") ", txt);
+ this->send_data(txt + "\r\n");
+}
+
+void IrcClient::send_user_command(const std::string& username, const std::string& realname)
+{
+ this->send_message(IrcMessage("USER", {username, this->user_hostname, "ignored", realname}));
+}
+
+void IrcClient::send_nick_command(const std::string& nick)
+{
+ this->send_message(IrcMessage("NICK", {nick}));
+}
+
+void IrcClient::send_pass_command(const std::string& password)
+{
+ this->send_message(IrcMessage("PASS", {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}));
+}
+
+void IrcClient::send_kick_command(const std::string& chan_name, const std::string& target, const std::string& reason)
+{
+ this->send_message(IrcMessage("KICK", {chan_name, target, reason}));
+}
+
+void IrcClient::send_list_command()
+{
+ this->send_message(IrcMessage("LIST", {}));
+}
+
+void IrcClient::send_topic_command(const std::string& chan_name, const std::string& topic)
+{
+ this->send_message(IrcMessage("TOPIC", {chan_name, topic}));
+}
+
+void IrcClient::send_quit_command(const std::string& reason)
+{
+ this->send_message(IrcMessage("QUIT", {reason}));
+}
+
+void IrcClient::send_join_command(const std::string& chan_name, const std::string& password)
+{
+ if (this->welcomed == false)
+ this->channels_to_join.emplace_back(chan_name, password);
+ else if (password.empty())
+ this->send_message(IrcMessage("JOIN", {chan_name}));
+ else
+ this->send_message(IrcMessage("JOIN", {chan_name, password}));
+ this->start();
+}
+
+bool IrcClient::send_channel_message(const std::string& chan_name, const std::string& body)
+{
+ IrcChannel* channel = this->get_channel(chan_name);
+ if (channel->joined == false)
+ {
+ log_warning("Cannot send message to channel ", chan_name, ", it is not joined");
+ return false;
+ }
+ // The max size is 512, taking into account the whole message, not just
+ // the text we send.
+ // This includes our own nick, username and host (because this will be
+ // added by the server into our message), in addition to the basic
+ // components of the message we send (command name, chan name, \r\n et)
+ // : + NICK + ! + USER + @ + HOST + <space> + PRIVMSG + <space> + CHAN + <space> + : + \r\n
+ const auto line_size = 512 -
+ this->current_nick.size() - this->username.size() - this->own_host.size() -
+ ::strlen(":!@ PRIVMSG ") - chan_name.length() - ::strlen(" :\r\n");
+ const auto lines = cut(body, line_size);
+ for (const auto& line: lines)
+ this->send_message(IrcMessage("PRIVMSG", {chan_name, line}));
+ return true;
+}
+
+void IrcClient::send_private_message(const std::string& username, const std::string& body, const std::string& type)
+{
+ std::string::size_type pos = 0;
+ while (pos < body.size())
+ {
+ this->send_message(IrcMessage(std::string(type), {username, body.substr(pos, 400)}));
+ pos += 400;
+ }
+ // We always try to insert and we don't care if the username was already
+ // in the set.
+ this->nicks_to_treat_as_private.insert(username);
+}
+
+void IrcClient::send_part_command(const std::string& chan_name, const std::string& status_message)
+{
+ IrcChannel* channel = this->get_channel(chan_name);
+ if (channel->joined == true)
+ {
+ if (chan_name.empty())
+ this->leave_dummy_channel(status_message);
+ else
+ this->send_message(IrcMessage("PART", {chan_name, status_message}));
+ }
+}
+
+void IrcClient::send_mode_command(const std::string& chan_name, const std::vector<std::string>& arguments)
+{
+ std::vector<std::string> args(arguments);
+ args.insert(args.begin(), chan_name);
+ IrcMessage m("MODE", std::move(args));
+ this->send_message(std::move(m));
+}
+
+void IrcClient::send_pong_command(const IrcMessage& message)
+{
+ const std::string id = message.arguments[0];
+ this->send_message(IrcMessage("PONG", {id}));
+}
+
+void IrcClient::on_pong(const IrcMessage&)
+{
+}
+
+void IrcClient::send_ping_command()
+{
+ this->send_message(IrcMessage("PING", {"biboumi"}));
+}
+
+void IrcClient::forward_server_message(const IrcMessage& message)
+{
+ const std::string from = message.prefix;
+ const std::string body = message.arguments[1];
+
+ this->bridge.send_xmpp_message(this->hostname, from, body);
+}
+
+void IrcClient::on_notice(const IrcMessage& message)
+{
+ std::string from = message.prefix;
+ const std::string to = message.arguments[0];
+ const std::string body = message.arguments[1];
+
+ if (!body.empty() && body[0] == '\01' && body[body.size() - 1] == '\01')
+ // Do not forward the notice to the user if it's a CTCP command
+ return ;
+
+ if (!to.empty() && this->chantypes.find(to[0]) == this->chantypes.end())
+ {
+ // The notice is for us precisely.
+
+ // Find out if we already sent a private message to this user. If yes
+ // we treat that message as a private message coming from
+ // it. Otherwise we treat it as a notice coming from the server.
+ IrcUser user(from);
+ std::string nick = utils::tolower(user.nick);
+ if (this->nicks_to_treat_as_private.find(nick) !=
+ this->nicks_to_treat_as_private.end())
+ { // We previously sent a message to that nick)
+ this->bridge.send_message({nick + "!" + this->hostname}, nick, body,
+ false);
+ }
+ else
+ this->bridge.send_xmpp_message(this->hostname, from, body);
+ }
+ else
+ {
+ // The notice was directed at a channel we are in. Modify the message
+ // to indicate that it is a notice, and make it a MUC message coming
+ // from the MUC JID
+ IrcMessage modified_message(std::move(from), "PRIVMSG", {to, "\u000303[notice]\u0003 "s + body});
+ this->on_channel_message(modified_message);
+ }
+}
+
+void IrcClient::on_isupport_message(const IrcMessage& message)
+{
+ const size_t len = message.arguments.size();
+ for (size_t i = 1; i < len; ++i)
+ {
+ const std::string token = message.arguments[i];
+ if (token.substr(0, 10) == "CHANMODES=")
+ {
+ this->chanmodes = utils::split(token.substr(11), ',');
+ // make sure we have 4 strings
+ this->chanmodes.resize(4);
+ }
+ else if (token.substr(0, 7) == "PREFIX=")
+ {
+ size_t i = 8; // jump PREFIX=(
+ size_t j = 9;
+ // Find the ) char
+ while (j < token.size() && token[j] != ')')
+ j++;
+ j++;
+ while (j < token.size() && token[i] != ')')
+ {
+ this->sorted_user_modes.push_back(token[i]);
+ this->prefix_to_mode[token[j++]] = token[i++];
+ }
+ }
+ else if (token.substr(0, 10) == "CHANTYPES=")
+ {
+ // Remove the default types, they apply only if no other value is
+ // specified.
+ this->chantypes.clear();
+ size_t i = 10;
+ while (i < token.size())
+ this->chantypes.insert(token[i++]);
+ }
+ }
+}
+
+void IrcClient::on_server_myinfo(const IrcMessage&)
+{
+}
+
+void IrcClient::send_gateway_message(const std::string& message, const std::string& from)
+{
+ this->bridge.send_xmpp_message(this->hostname, from, message);
+}
+
+void IrcClient::set_and_forward_user_list(const IrcMessage& message)
+{
+ const std::string chan_name = utils::tolower(message.arguments[2]);
+ IrcChannel* channel = this->get_channel(chan_name);
+ std::vector<std::string> nicks = utils::split(message.arguments[3], ' ');
+ for (const std::string& nick: nicks)
+ {
+ const IrcUser* user = channel->add_user(nick, this->prefix_to_mode);
+ if (user->nick != channel->get_self()->nick)
+ {
+ this->bridge.send_user_join(this->hostname, chan_name, user, user->get_most_significant_mode(this->sorted_user_modes), false);
+ }
+ else
+ {
+ // we now know the modes of self, so copy the modes into self
+ channel->get_self()->modes = user->modes;
+ }
+ }
+}
+
+void IrcClient::on_channel_join(const IrcMessage& message)
+{
+ const std::string chan_name = utils::tolower(message.arguments[0]);
+ IrcChannel* channel;
+ if (chan_name.empty())
+ channel = &this->dummy_channel;
+ else
+ channel = this->get_channel(chan_name);
+ const std::string nick = message.prefix;
+ if (channel->joined == false)
+ channel->set_self(nick);
+ else
+ {
+ const IrcUser* user = channel->add_user(nick, this->prefix_to_mode);
+ this->bridge.send_user_join(this->hostname, chan_name, user, user->get_most_significant_mode(this->sorted_user_modes), false);
+ }
+}
+
+void IrcClient::on_channel_message(const IrcMessage& message)
+{
+ const IrcUser user(message.prefix);
+ const std::string nick = user.nick;
+ Iid iid;
+ iid.set_local(message.arguments[0]);
+ iid.set_server(this->hostname);
+ const std::string body = message.arguments[1];
+ bool muc = true;
+ if (!this->get_channel(iid.get_local())->joined)
+ {
+ iid.is_user = true;
+ iid.set_local(nick);
+ muc = false;
+ }
+ else
+ iid.is_channel = true;
+ if (!body.empty() && body[0] == '\01')
+ {
+ if (body.substr(1, 6) == "ACTION")
+ this->bridge.send_message(iid, nick,
+ "/me"s + body.substr(7, body.size() - 8), muc);
+ else if (body.substr(1, 8) == "VERSION\01")
+ this->bridge.send_iq_version_request(nick, this->hostname);
+ else if (body.substr(1, 5) == "PING ")
+ this->bridge.send_xmpp_ping_request(utils::tolower(nick), this->hostname,
+ body.substr(6, body.size() - 7));
+ }
+ else
+ this->bridge.send_message(iid, nick, body, muc);
+}
+
+void IrcClient::on_rpl_liststart(const IrcMessage&)
+{
+}
+
+void IrcClient::on_rpl_list(const IrcMessage&)
+{
+}
+
+void IrcClient::on_rpl_listend(const IrcMessage&)
+{
+}
+
+void IrcClient::empty_motd(const IrcMessage&)
+{
+ this->motd.erase();
+}
+
+void IrcClient::on_empty_topic(const IrcMessage& message)
+{
+ const std::string chan_name = utils::tolower(message.arguments[1]);
+ log_debug("empty topic for ", chan_name);
+ IrcChannel* channel = this->get_channel(chan_name);
+ if (channel)
+ channel->topic.clear();
+}
+
+void IrcClient::on_motd_line(const IrcMessage& message)
+{
+ const std::string body = message.arguments[1];
+ // We could send the MOTD without a line break between each IRC-message,
+ // but sometimes it contains some ASCII art, we use line breaks to keep
+ // them intact.
+ this->motd += body+"\n";
+}
+
+void IrcClient::send_motd(const IrcMessage&)
+{
+ this->bridge.send_xmpp_message(this->hostname, "", this->motd);
+}
+
+void IrcClient::on_topic_received(const IrcMessage& message)
+{
+ const std::string chan_name = utils::tolower(message.arguments[message.arguments.size() - 2]);
+ IrcUser author(message.prefix);
+ IrcChannel* channel = this->get_channel(chan_name);
+ channel->topic = message.arguments[message.arguments.size() - 1];
+ channel->topic_author = author.nick;
+ if (channel->joined)
+ this->bridge.send_topic(this->hostname, chan_name, channel->topic, channel->topic_author);
+}
+
+void IrcClient::on_topic_who_time_received(const IrcMessage& message)
+{
+ IrcUser author(message.arguments[2]);
+ const std::string chan_name = utils::tolower(message.arguments[1]);
+ IrcChannel* channel = this->get_channel(chan_name);
+ channel->topic_author = author.nick;
+}
+
+void IrcClient::on_channel_completely_joined(const IrcMessage& message)
+{
+ const std::string chan_name = utils::tolower(message.arguments[1]);
+ IrcChannel* channel = this->get_channel(chan_name);
+ channel->joined = true;
+ this->bridge.send_user_join(this->hostname, chan_name, channel->get_self(), channel->get_self()->get_most_significant_mode(this->sorted_user_modes), true);
+ this->bridge.send_topic(this->hostname, chan_name, channel->topic, channel->topic_author);
+}
+
+void IrcClient::on_own_host_received(const IrcMessage& message)
+{
+ this->own_host = message.arguments[1];
+ const std::string from = message.prefix;
+ if (message.arguments.size() >= 3)
+ this->bridge.send_xmpp_message(this->hostname, from,
+ this->own_host + " " + message.arguments[2]);
+ else
+ this->bridge.send_xmpp_message(this->hostname, from, this->own_host +
+ " is now your displayed host");
+}
+
+void IrcClient::on_erroneous_nickname(const IrcMessage& message)
+{
+ const std::string error_msg = message.arguments.size() >= 3 ?
+ message.arguments[2]: "Erroneous nickname";
+ this->send_gateway_message(error_msg + ": " + message.arguments[1], message.prefix);
+}
+
+void IrcClient::on_nickname_conflict(const IrcMessage& message)
+{
+ const std::string nickname = message.arguments[1];
+ this->on_generic_error(message);
+ for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
+ {
+ Iid iid;
+ iid.set_local(it->first);
+ iid.set_server(this->hostname);
+ iid.is_channel = true;
+ this->bridge.send_nickname_conflict_error(iid, nickname);
+ }
+}
+
+void IrcClient::on_nickname_change_too_fast(const IrcMessage& message)
+{
+ const std::string nickname = message.arguments[1];
+ std::string txt;
+ if (message.arguments.size() >= 3)
+ txt = message.arguments[2];
+ this->on_generic_error(message);
+ for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
+ {
+ Iid iid;
+ iid.set_local(it->first);
+ iid.set_server(this->hostname);
+ iid.is_channel = true;
+ this->bridge.send_presence_error(iid, nickname,
+ "cancel", "not-acceptable",
+ "", txt);
+ }
+}
+
+void IrcClient::on_generic_error(const IrcMessage& message)
+{
+ const std::string error_msg = message.arguments.size() >= 3 ?
+ message.arguments[2]: "Unspecified error";
+ this->send_gateway_message(message.arguments[1] + ": " + error_msg, message.prefix);
+}
+
+void IrcClient::on_welcome_message(const IrcMessage& message)
+{
+ this->current_nick = message.arguments[0];
+ this->welcomed = true;
+#ifdef USE_DATABASE
+ auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
+ this->get_hostname());
+ if (!options.afterConnectionCommand.value().empty())
+ this->send_raw(options.afterConnectionCommand.value());
+#endif
+ // Install a repeated events to regularly send a PING
+ TimedEventsManager::instance().add_event(TimedEvent(240s, std::bind(&IrcClient::send_ping_command, this),
+ "PING"s + this->hostname + this->bridge.get_jid()));
+ for (const auto& tuple: this->channels_to_join)
+ this->send_join_command(std::get<0>(tuple), std::get<1>(tuple));
+ this->channels_to_join.clear();
+ // Indicate that the dummy channel is joined as well, if needed
+ if (this->dummy_channel.joining)
+ {
+ // Simulate a message coming from the IRC server saying that we joined
+ // the channel
+ const IrcMessage join_message(this->get_nick(), "JOIN", {""});
+ this->on_channel_join(join_message);
+ const IrcMessage end_join_message(std::string(this->hostname), "366",
+ {this->get_nick(),
+ "", "End of NAMES list"});
+ this->on_channel_completely_joined(end_join_message);
+ }
+}
+
+void IrcClient::on_part(const IrcMessage& message)
+{
+ const std::string chan_name = message.arguments[0];
+ IrcChannel* channel = this->get_channel(chan_name);
+ if (!channel->joined)
+ return ;
+ std::string txt;
+ if (message.arguments.size() >= 2)
+ txt = message.arguments[1];
+ const IrcUser* user = channel->find_user(message.prefix);
+ if (user)
+ {
+ std::string nick = user->nick;
+ channel->remove_user(user);
+ Iid iid;
+ iid.set_local(chan_name);
+ iid.set_server(this->hostname);
+ iid.is_channel = true;
+ bool self = channel->get_self()->nick == nick;
+ if (self)
+ {
+ channel->joined = false;
+ this->channels.erase(utils::tolower(chan_name));
+ // channel pointer is now invalid
+ channel = nullptr;
+ }
+ this->bridge.send_muc_leave(std::move(iid), std::move(nick), std::move(txt), self);
+ }
+}
+
+void IrcClient::on_error(const IrcMessage& message)
+{
+ const std::string leave_message = message.arguments[0];
+ // The user is out of all the channels
+ for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
+ {
+ Iid iid;
+ iid.set_local(it->first);
+ iid.set_server(this->hostname);
+ iid.is_channel = true;
+ IrcChannel* channel = it->second.get();
+ if (!channel->joined)
+ continue;
+ std::string own_nick = channel->get_self()->nick;
+ this->bridge.send_muc_leave(std::move(iid), std::move(own_nick), leave_message, true);
+ }
+ this->channels.clear();
+ this->send_gateway_message("ERROR: "s + leave_message);
+}
+
+void IrcClient::on_quit(const IrcMessage& message)
+{
+ std::string txt;
+ if (message.arguments.size() >= 1)
+ txt = message.arguments[0];
+ for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
+ {
+ const std::string chan_name = it->first;
+ IrcChannel* channel = it->second.get();
+ const IrcUser* user = channel->find_user(message.prefix);
+ if (user)
+ {
+ std::string nick = user->nick;
+ channel->remove_user(user);
+ Iid iid;
+ iid.set_local(chan_name);
+ iid.set_server(this->hostname);
+ iid.is_channel = true;
+ this->bridge.send_muc_leave(std::move(iid), std::move(nick), txt, false);
+ }
+ }
+}
+
+void IrcClient::on_nick(const IrcMessage& message)
+{
+ const std::string new_nick = message.arguments[0];
+ for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
+ {
+ const std::string chan_name = it->first;
+ IrcChannel* channel = it->second.get();
+ IrcUser* user = channel->find_user(message.prefix);
+ if (user)
+ {
+ std::string old_nick = user->nick;
+ Iid iid;
+ iid.set_local(chan_name);
+ iid.set_server(this->hostname);
+ iid.is_channel = true;
+ const bool self = channel->get_self()->nick == old_nick;
+ const char user_mode = user->get_most_significant_mode(this->sorted_user_modes);
+ this->bridge.send_nick_change(std::move(iid), old_nick, new_nick, user_mode, self);
+ user->nick = new_nick;
+ if (self)
+ {
+ channel->get_self()->nick = new_nick;
+ this->current_nick = new_nick;
+ }
+ }
+ }
+}
+
+void IrcClient::on_kick(const IrcMessage& message)
+{
+ const std::string chan_name = utils::tolower(message.arguments[0]);
+ const std::string target = message.arguments[1];
+ const std::string reason = message.arguments[2];
+ IrcChannel* channel = this->get_channel(chan_name);
+ if (!channel->joined)
+ return ;
+ if (channel->get_self()->nick == target)
+ channel->joined = false;
+ IrcUser author(message.prefix);
+ Iid iid;
+ iid.set_local(chan_name);
+ iid.set_server(this->hostname);
+ iid.is_channel = true;
+ this->bridge.kick_muc_user(std::move(iid), target, reason, author.nick);
+}
+
+void IrcClient::on_mode(const IrcMessage& message)
+{
+ const std::string target = message.arguments[0];
+ if (this->chantypes.find(target[0]) != this->chantypes.end())
+ this->on_channel_mode(message);
+ else
+ this->on_user_mode(message);
+}
+
+void IrcClient::on_channel_mode(const IrcMessage& message)
+{
+ // For now, just transmit the modes so the user can know what happens
+ // TODO, actually interprete the mode.
+ Iid iid;
+ iid.set_local(message.arguments[0]);
+ iid.set_server(this->hostname);
+ iid.is_channel = true;
+ IrcUser user(message.prefix);
+ std::string mode_arguments;
+ for (size_t i = 1; i < message.arguments.size(); ++i)
+ {
+ if (!message.arguments[i].empty())
+ {
+ if (i != 1)
+ mode_arguments += " ";
+ mode_arguments += message.arguments[i];
+ }
+ }
+ this->bridge.send_message(iid, "", "Mode "s + iid.get_local() +
+ " [" + mode_arguments + "] by " + user.nick,
+ true);
+ const IrcChannel* channel = this->get_channel(iid.get_local());
+ if (!channel)
+ return;
+
+ // parse the received modes, we need to handle things like "+m-oo coucou toutou"
+ const std::string modes = message.arguments[1];
+ // a list of modified IrcUsers. When we applied all modes, we check the
+ // modes that now applies to each of them, and send a notification for
+ // each one. This is to disallow sending two notifications or more when a
+ // single MODE command changes two or more modes on the same participant
+ std::set<const IrcUser*> modified_users;
+ // If it is true, the modes are added, if it’s false they are
+ // removed. When we encounter the '+' char, the value is changed to true,
+ // and with '-' it is changed to false.
+ bool add = true;
+ bool use_arg;
+ size_t arg_pos = 2;
+ for (const char c: modes)
+ {
+ if (c == '+')
+ add = true;
+ else if (c == '-')
+ add = false;
+ else
+ { // lookup the mode symbol in the 4 chanmodes lists, depending on
+ // the list where it is found, it takes an argument or not
+ size_t type;
+ for (type = 0; type < 4; ++type)
+ if (this->chanmodes[type].find(c) != std::string::npos)
+ break;
+ if (type == 4) // if mode was not found
+ {
+ // That mode can also be of type B if it is present in the
+ // prefix_to_mode map
+ for (const std::pair<char, char>& pair: this->prefix_to_mode)
+ if (pair.second == c)
+ {
+ type = 1;
+ break;
+ }
+ }
+ // modes of type A, B or C (but only with add == true)
+ if (type == 0 || type == 1 ||
+ (type == 2 && add == true))
+ use_arg = true;
+ else // modes of type C (but only with add == false), D, or unknown
+ use_arg = false;
+ if (use_arg == true && message.arguments.size() > arg_pos)
+ {
+ const std::string target = message.arguments[arg_pos++];
+ IrcUser* user = channel->find_user(target);
+ if (!user)
+ {
+ log_warning("Trying to set mode for non-existing user '", target
+ , "' in channel", iid.get_local());
+ return;
+ }
+ if (add)
+ user->add_mode(c);
+ else
+ user->remove_mode(c);
+ modified_users.insert(user);
+ }
+ }
+ }
+ for (const IrcUser* u: modified_users)
+ {
+ char most_significant_mode = u->get_most_significant_mode(this->sorted_user_modes);
+ this->bridge.send_affiliation_role_change(iid, u->nick, most_significant_mode);
+ }
+}
+
+void IrcClient::on_user_mode(const IrcMessage& message)
+{
+ this->bridge.send_xmpp_message(this->hostname, "",
+ "User mode for "s + message.arguments[0] +
+ " is [" + message.arguments[1] + "]");
+}
+
+void IrcClient::on_unknown_message(const IrcMessage& message)
+{
+ if (message.arguments.size() < 2)
+ return ;
+ std::string from = message.prefix;
+ std::stringstream ss;
+ for (auto it = message.arguments.begin() + 1; it != message.arguments.end(); ++it)
+ {
+ ss << *it;
+ if (it + 1 != message.arguments.end())
+ ss << " ";
+ }
+ this->bridge.send_xmpp_message(this->hostname, from, ss.str());
+}
+
+size_t IrcClient::number_of_joined_channels() const
+{
+ if (this->dummy_channel.joined)
+ return this->channels.size() + 1;
+ else
+ return this->channels.size();
+}
+
+DummyIrcChannel& IrcClient::get_dummy_channel()
+{
+ return this->dummy_channel;
+}
+
+void IrcClient::leave_dummy_channel(const std::string& exit_message)
+{
+ if (!this->dummy_channel.joined)
+ return;
+ this->dummy_channel.joined = false;
+ this->dummy_channel.joining = false;
+ this->dummy_channel.remove_all_users();
+ this->bridge.send_muc_leave(Iid("%"s + this->hostname), std::string(this->current_nick), exit_message, true);
+}
+
+#ifdef BOTAN_FOUND
+bool IrcClient::abort_on_invalid_cert() const
+{
+#ifdef USE_DATABASE
+ auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), this->hostname);
+ return options.verifyCert.value();
+#endif
+ return true;
+}
+#endif
diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp
new file mode 100644
index 0000000..fc3918e
--- /dev/null
+++ b/src/irc/irc_client.hpp
@@ -0,0 +1,383 @@
+#pragma once
+
+
+#include <irc/irc_message.hpp>
+#include <irc/irc_channel.hpp>
+#include <irc/iid.hpp>
+
+#include <network/tcp_socket_handler.hpp>
+#include <network/resolver.hpp>
+
+#include <unordered_map>
+#include <utility>
+#include <memory>
+#include <vector>
+#include <string>
+#include <stack>
+#include <map>
+#include <set>
+
+class Bridge;
+
+/**
+ * Represent one IRC client, i.e. an endpoint connected to a single IRC
+ * server, through a TCP socket, receiving and sending commands to it.
+ */
+class IrcClient: public TCPSocketHandler
+{
+public:
+ explicit IrcClient(std::shared_ptr<Poller> poller, const std::string& hostname,
+ const std::string& nickname, const std::string& username,
+ const std::string& realname, const std::string& user_hostname,
+ Bridge& bridge);
+ ~IrcClient();
+
+ IrcClient(const IrcClient&) = delete;
+ IrcClient(IrcClient&&) = delete;
+ IrcClient& operator=(const IrcClient&) = delete;
+ IrcClient& operator=(IrcClient&&) = delete;
+
+ /**
+ * Connect to the IRC server
+ */
+ void start();
+ /**
+ * Called when the connection to the server cannot be established
+ */
+ void on_connection_failed(const std::string& reason) override final;
+ /**
+ * Called when successfully connected to the server
+ */
+ void on_connected() override final;
+ /**
+ * Close the connection, remove us from the poller
+ */
+ void on_connection_close(const std::string& error) override final;
+ /**
+ * Parse the data we have received so far and try to get one or more
+ * complete messages from it.
+ */
+ void parse_in_buffer(const size_t) override final;
+#ifdef BOTAN_FOUND
+ virtual bool abort_on_invalid_cert() const override final;
+#endif
+ /**
+ * Return the channel with this name, create it if it does not yet exist
+ */
+ IrcChannel* get_channel(const std::string& name);
+ /**
+ * Returns true if the channel is joined
+ */
+ bool is_channel_joined(const std::string& name);
+ /**
+ * Return our own nick
+ */
+ std::string get_own_nick() const;
+ /**
+ * Serialize the given message into a line, and send that into the socket
+ * (actually, into our out_buf and signal the poller that we want to wach
+ * for send events to be ready)
+ */
+ void send_message(IrcMessage&& message);
+ void send_raw(const std::string& txt);
+ /**
+ * Send the PONG irc command
+ */
+ void send_pong_command(const IrcMessage& message);
+ /**
+ * Do nothing when we receive a PONG command (but also do not log that no
+ * handler exist)
+ */
+ void on_pong(const IrcMessage& message);
+ void send_ping_command();
+ /**
+ * Send the USER irc command
+ */
+ void send_user_command(const std::string& username, const std::string& realname);
+ /**
+ * Send the NICK irc command
+ */
+ void send_nick_command(const std::string& username);
+ void send_pass_command(const std::string& password);
+ void send_webirc_command(const std::string& password, const std::string& user_ip);
+ /**
+ * Send the JOIN irc command.
+ */
+ void send_join_command(const std::string& chan_name, const std::string& password);
+ /**
+ * Send a PRIVMSG command for a channel
+ * Return true if the message was actually sent
+ */
+ bool send_channel_message(const std::string& chan_name, const std::string& body);
+ /**
+ * Send a PRIVMSG command for an user
+ */
+ void send_private_message(const std::string& username, const std::string& body, const std::string& type);
+ /**
+ * Send the PART irc command
+ */
+ void send_part_command(const std::string& chan_name, const std::string& status_message);
+ /**
+ * Send the MODE irc command
+ */
+ void send_mode_command(const std::string& chan_name, const std::vector<std::string>& arguments);
+ /**
+ * Send the KICK irc command
+ */
+ void send_kick_command(const std::string& chan_name, const std::string& target, const std::string& reason);
+ /**
+ * Send the LIST irc command
+ */
+ void send_list_command();
+ void send_topic_command(const std::string& chan_name, const std::string& topic);
+ /**
+ * Send the QUIT irc command
+ */
+ void send_quit_command(const std::string& reason);
+ /**
+ * 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
+ * might want to notify the user about the connexion state)
+ */
+ void send_gateway_message(const std::string& message, const std::string& from="");
+ /**
+ * Forward the server message received from IRC to the XMPP component
+ */
+ void forward_server_message(const IrcMessage& message);
+ /**
+ * When receiving the isupport informations. See
+ * http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt
+ */
+ void on_isupport_message(const IrcMessage& message);
+ /**
+ * Does nothing yet. Isn’t that duplicating features from 005?
+ */
+ void on_server_myinfo(const IrcMessage& message);
+ /**
+ * Just empty the motd we kept as a string
+ */
+ void empty_motd(const IrcMessage& message);
+ /**
+ * Send the MOTD string as one single "big" message
+ */
+ void send_motd(const IrcMessage& message);
+ /**
+ * Append this line to the MOTD
+ */
+ void on_motd_line(const IrcMessage& message);
+ /**
+ * Forward the join of an other user into an IRC channel, and save the
+ * IrcUsers in the IrcChannel
+ */
+ void set_and_forward_user_list(const IrcMessage& message);
+ /**
+ * Signal the start of the LIST response. The RFC says its obsolete and
+ * “not used”, but I we receive it on some servers, so just ignore it.
+ */
+ void on_rpl_liststart(const IrcMessage& message);
+ /**
+ * A single LIST response line (one channel)
+ *
+ * The command is handled in a wait_irc callback. This general handler is
+ * empty and just used to avoid sending a message stanza for each received
+ * channel.
+ */
+ void on_rpl_list(const IrcMessage& message);
+ /**
+ * Signal the end of the LIST response, ignore.
+ */
+ void on_rpl_listend(const IrcMessage& message);
+ /**
+ * Remember our nick and host, when we are joined to the channel. The list
+ * of user comes after so we do not send the self-presence over XMPP yet.
+ */
+ void on_channel_join(const IrcMessage& message);
+ /**
+ * When a channel message is received
+ */
+ void on_channel_message(const IrcMessage& message);
+ /**
+ * A notice is received
+ */
+ void on_notice(const IrcMessage& message);
+ /**
+ * Save the topic in the IrcChannel
+ */
+ void on_topic_received(const IrcMessage& message);
+ /**
+ * Save the topic author in the IrcChannel
+ */
+ void on_topic_who_time_received(const IrcMessage& message);
+ /**
+ * Empty the topic
+ */
+ void on_empty_topic(const IrcMessage& message);
+ /**
+ * The channel has been completely joined (self presence, topic, all names
+ * received etc), send the self presence and topic to the XMPP user.
+ */
+ void on_channel_completely_joined(const IrcMessage& message);
+ /**
+ * Save our own host, as reported by the server
+ */
+ void on_own_host_received(const IrcMessage& message);
+ /**
+ * We tried to set an invalid nickname
+ */
+ void on_erroneous_nickname(const IrcMessage& message);
+ /**
+ * When the IRC servers denies our nickname because of a conflict. Send a
+ * presence conflict from all channels, because the name is server-wide.
+ */
+ void on_nickname_conflict(const IrcMessage& message);
+ /**
+ * Idem, but for when the user changes their nickname too quickly
+ */
+ void on_nickname_change_too_fast(const IrcMessage& message);
+ /**
+ * Handles most errors from the server by just forwarding the message to the user.
+ */
+ void on_generic_error(const IrcMessage& message);
+ /**
+ * When a message 001 is received, join the rooms we wanted to join, and set our actual nickname
+ */
+ void on_welcome_message(const IrcMessage& message);
+ void on_part(const IrcMessage& message);
+ void on_error(const IrcMessage& message);
+ void on_nick(const IrcMessage& message);
+ void on_kick(const IrcMessage& message);
+ void on_mode(const IrcMessage& message);
+ /**
+ * A mode towards our own user is received (note, that is different from a
+ * channel mode towards or own nick, see
+ * http://tools.ietf.org/html/rfc2812#section-3.1.5 VS #section-3.2.3)
+ */
+ void on_user_mode(const IrcMessage& message);
+ /**
+ * A mode towards a channel. Note that this can change the mode of the
+ * channel itself or an IrcUser in it.
+ */
+ void on_channel_mode(const IrcMessage& message);
+ void on_quit(const IrcMessage& message);
+ void on_unknown_message(const IrcMessage& message);
+ /**
+ * Return the number of joined channels
+ */
+ size_t number_of_joined_channels() const;
+ /**
+ * Get a reference to the unique dummy channel
+ */
+ DummyIrcChannel& get_dummy_channel();
+ /**
+ * Leave the dummy channel: forward a message to the user to indicate that
+ * he left it, and mark it as not joined.
+ */
+ void leave_dummy_channel(const std::string& exit_message);
+
+ const std::string& get_hostname() const { return this->hostname; }
+ std::string get_nick() const { return this->current_nick; }
+ bool is_welcomed() const { return this->welcomed; }
+
+ const Resolver& get_resolver() const { return this->dns_resolver; }
+
+ const std::vector<char>& get_sorted_user_modes() const { return sorted_user_modes; }
+
+private:
+ /**
+ * The hostname of the server we are connected to.
+ */
+ const std::string hostname;
+ /**
+ * Our own host, as reported by the IRC server.
+ * By default (and if it is not overridden by the server), it is a
+ * meaningless string, with the maximum allowed size
+ */
+ std::string own_host{63, '*'};
+ /**
+ * The hostname of the user. This is used in the USER and the WEBIRC
+ * commands, but only the one in WEBIRC will be used by the IRC server.
+ */
+ const std::string user_hostname;
+ /**
+ * The username used in the USER irc command
+ */
+ std::string username;
+ /**
+ * The realname used in the USER irc command
+ */
+ std::string realname;
+ /**
+ * Our current nickname on the server
+ */
+ std::string current_nick;
+ /**
+ * To communicate back with the bridge
+ */
+ Bridge& bridge;
+ /**
+ * The list of joined channels, indexed by name
+ */
+ std::unordered_map<std::string, std::unique_ptr<IrcChannel>> channels;
+ /**
+ * A single channel with a iid of the form "hostname" (normal channel have
+ * an iid of the form "chan%hostname".
+ */
+ DummyIrcChannel dummy_channel;
+ /**
+ * A list of chan we want to join (tuples with the channel name and the
+ * password, if any), but we need a response 001 from the server before
+ * sending the actual JOIN commands. So we just keep the channel names in
+ * a list, and send the JOIN commands for each of them whenever the
+ * WELCOME message is received.
+ */
+ std::vector<std::tuple<std::string, std::string>> channels_to_join;
+ /**
+ * This flag indicates that the server is completely joined (connection
+ * has been established, we are authentified and we have a nick)
+ */
+ bool welcomed;
+ /**
+ * See http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt section 3.3
+ * We store the possible chanmodes in this object.
+ * chanmodes[0] contains modes of type A, [1] of type B etc
+ */
+ std::vector<std::string> chanmodes;
+ /**
+ * See http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt
+ * section 3.5
+ */
+ std::set<char> chantypes;
+ /**
+ * Each motd line received is appended to this string, which we send when
+ * the motd is completely received
+ */
+ std::string motd;
+ /**
+ * See http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt section 3.14
+ * The example given would be transformed into
+ * modes_to_prefix = {{'&', 'a'}, {'*', 'b'}}
+ */
+ std::map<char, char> prefix_to_mode;
+ /**
+ * Available user modes, sorted from most significant to least significant
+ * (for example 'ahov' is a common order).
+ */
+ std::vector<char> sorted_user_modes;
+ /**
+ * A list of ports to which we will try to connect, in reverse. Each port
+ * is associated with a boolean telling if we should use TLS or not if the
+ * connection succeeds on that port.
+ */
+ std::stack<std::pair<std::string, bool>> ports_to_try;
+ /**
+ * A set of (lowercase) nicknames to which we sent a private message.
+ */
+ std::set<std::string> nicks_to_treat_as_private;
+ /**
+ * DNS resolver, used to resolve the hostname of the user if we are using
+ * the WebIRC protocole.
+ */
+ Resolver dns_resolver;
+};
+
+
diff --git a/src/irc/irc_message.cpp b/src/irc/irc_message.cpp
new file mode 100644
index 0000000..966a47c
--- /dev/null
+++ b/src/irc/irc_message.cpp
@@ -0,0 +1,61 @@
+#include <irc/irc_message.hpp>
+#include <iostream>
+
+IrcMessage::IrcMessage(std::string&& line)
+{
+ std::string::size_type pos;
+
+ // optional prefix
+ if (line[0] == ':')
+ {
+ pos = line.find(" ");
+ this->prefix = line.substr(1, pos - 1);
+ line = line.substr(pos + 1, std::string::npos);
+ }
+ // command
+ pos = line.find(" ");
+ this->command = line.substr(0, pos);
+ line = line.substr(pos + 1, std::string::npos);
+ // arguments
+ do
+ {
+ if (line[0] == ':')
+ {
+ this->arguments.emplace_back(line.substr(1, std::string::npos));
+ break ;
+ }
+ pos = line.find(" ");
+ this->arguments.emplace_back(line.substr(0, pos));
+ line = line.substr(pos + 1, std::string::npos);
+ } while (pos != std::string::npos);
+}
+
+IrcMessage::IrcMessage(std::string&& prefix,
+ std::string&& command,
+ std::vector<std::string>&& args):
+ prefix(std::move(prefix)),
+ command(std::move(command)),
+ arguments(std::move(args))
+{
+}
+
+IrcMessage::IrcMessage(std::string&& command,
+ std::vector<std::string>&& args):
+ prefix(),
+ command(std::move(command)),
+ arguments(std::move(args))
+{
+}
+
+std::ostream& operator<<(std::ostream& os, const IrcMessage& message)
+{
+ os << "IrcMessage";
+ os << "[" << message.command << "]";
+ for (const std::string& arg: message.arguments)
+ {
+ os << "{" << arg << "}";
+ }
+ if (!message.prefix.empty())
+ os << "(from: " << message.prefix << ")";
+ return os;
+}
diff --git a/src/irc/irc_message.hpp b/src/irc/irc_message.hpp
new file mode 100644
index 0000000..fe954e4
--- /dev/null
+++ b/src/irc/irc_message.hpp
@@ -0,0 +1,28 @@
+#pragma once
+
+
+#include <vector>
+#include <string>
+#include <ostream>
+
+class IrcMessage
+{
+public:
+ IrcMessage(std::string&& line);
+ IrcMessage(std::string&& prefix, std::string&& command, std::vector<std::string>&& args);
+ IrcMessage(std::string&& command, std::vector<std::string>&& args);
+ ~IrcMessage() = default;
+
+ IrcMessage(const IrcMessage&) = delete;
+ IrcMessage(IrcMessage&&) = delete;
+ IrcMessage& operator=(const IrcMessage&) = delete;
+ IrcMessage& operator=(IrcMessage&&) = delete;
+
+ std::string prefix;
+ std::string command;
+ std::vector<std::string> arguments;
+};
+
+std::ostream& operator<<(std::ostream& os, const IrcMessage& message);
+
+
diff --git a/src/irc/irc_user.cpp b/src/irc/irc_user.cpp
new file mode 100644
index 0000000..9fa3612
--- /dev/null
+++ b/src/irc/irc_user.cpp
@@ -0,0 +1,57 @@
+#include <irc/irc_user.hpp>
+
+#include <iostream>
+
+IrcUser::IrcUser(const std::string& name,
+ const std::map<char, char>& prefix_to_mode)
+{
+ if (name.empty())
+ return ;
+
+ // One or more prefix (with multi-prefix support) may come before the
+ // actual nick
+ std::string::size_type name_begin = 0;
+ while (name_begin != name.size())
+ {
+ const auto prefix = prefix_to_mode.find(name[name_begin]);
+ // This is not a prefix
+ if (prefix == prefix_to_mode.end())
+ break;
+ this->modes.insert(prefix->second);
+ name_begin++;
+ }
+
+ const std::string::size_type sep = name.find("!", name_begin);
+ if (sep == std::string::npos)
+ this->nick = name.substr(name_begin);
+ else
+ {
+ this->nick = name.substr(name_begin, sep-name_begin);
+ this->host = name.substr(sep+1);
+ }
+}
+
+IrcUser::IrcUser(const std::string& name):
+ IrcUser(name, {})
+{
+}
+
+void IrcUser::add_mode(const char mode)
+{
+ this->modes.insert(mode);
+}
+
+void IrcUser::remove_mode(const char mode)
+{
+ this->modes.erase(mode);
+}
+
+char IrcUser::get_most_significant_mode(const std::vector<char>& modes) const
+{
+ for (const char mode: modes)
+ {
+ if (this->modes.find(mode) != this->modes.end())
+ return mode;
+ }
+ return 0;
+}
diff --git a/src/irc/irc_user.hpp b/src/irc/irc_user.hpp
new file mode 100644
index 0000000..c84030e
--- /dev/null
+++ b/src/irc/irc_user.hpp
@@ -0,0 +1,33 @@
+#pragma once
+
+
+#include <vector>
+#include <string>
+#include <map>
+#include <set>
+
+/**
+ * Keeps various information about one IRC channel user
+ */
+class IrcUser
+{
+public:
+ explicit IrcUser(const std::string& name,
+ const std::map<char, char>& prefix_to_mode);
+ explicit IrcUser(const std::string& name);
+
+ IrcUser(const IrcUser&) = delete;
+ IrcUser(IrcUser&&) = delete;
+ IrcUser& operator=(const IrcUser&) = delete;
+ IrcUser& operator=(IrcUser&&) = delete;
+
+ void add_mode(const char mode);
+ void remove_mode(const char mode);
+ char get_most_significant_mode(const std::vector<char>& sorted_user_modes) const;
+
+ std::string nick;
+ std::string host;
+ std::set<char> modes;
+};
+
+
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..53f3193
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,202 @@
+#include <xmpp/biboumi_component.hpp>
+#include <utils/timed_events.hpp>
+#include <network/poller.hpp>
+#include <config/config.hpp>
+#include <logger/logger.hpp>
+#include <utils/xdg.hpp>
+#include <utils/reload.hpp>
+
+#ifdef CARES_FOUND
+# include <network/dns_handler.hpp>
+#endif
+
+#include <atomic>
+#include <signal.h>
+
+// A flag set by the SIGINT signal handler.
+static volatile std::atomic<bool> stop(false);
+// Flag set by the SIGUSR1/2 signal handler.
+static volatile std::atomic<bool> reload(false);
+// A flag indicating that we are wanting to exit the process. i.e: if this
+// flag is set and all connections are closed, we can exit properly.
+static bool exiting = false;
+
+/**
+ * Provide an helpful message to help the user write a minimal working
+ * configuration file.
+ */
+int config_help(const std::string& missing_option)
+{
+ if (!missing_option.empty())
+ log_error("Configuration error: empty value for option ", missing_option, ".");
+ log_error("Please provide a configuration file filled like this:\n\n"
+ "hostname=irc.example.com\npassword=S3CR3T");
+ return 1;
+}
+
+int display_help()
+{
+ std::cout << "Usage: biboumi [configuration_file]" << std::endl;
+ return 0;
+}
+
+static void sigint_handler(int sig, siginfo_t*, void*)
+{
+ // In 2 seconds, repeat the same signal, to force the exit
+ TimedEventsManager::instance().add_event(TimedEvent(std::chrono::steady_clock::now() + 2s,
+ [sig]() { raise(sig); }));
+ stop.store(true);
+}
+
+static void sigusr_handler(int, siginfo_t*, void*)
+{
+ reload.store(true);
+}
+
+int main(int ac, char** av)
+{
+ if (ac > 1)
+ {
+ const std::string arg = av[1];
+ if (arg.size() >= 2 && arg[0] == '-' && arg[1] == '-')
+ {
+ if (arg == "--help")
+ return display_help();
+ else
+ {
+ std::cerr << "Unknow command line option: " << arg << std::endl;
+ return 1;
+ }
+ }
+ }
+ const std::string conf_filename = ac > 1 ? av[1] : xdg_config_path("biboumi.cfg");
+ std::cout << "Using configuration file: " << conf_filename << std::endl;
+
+ if (!Config::read_conf(conf_filename))
+ return config_help("");
+
+ const std::string password = Config::get("password", "");
+ if (password.empty())
+ return config_help("password");
+ const std::string hostname = Config::get("hostname", "");
+ if (hostname.empty())
+ return config_help("hostname");
+
+ try {
+ open_database();
+ } catch (...) {
+ return 1;
+ }
+
+ // Block the signals we want to manage. They will be unblocked only during
+ // the epoll_pwait or ppoll calls. This avoids some race conditions,
+ // explained in man 2 pselect on linux
+ sigset_t mask;
+ sigemptyset(&mask);
+ sigaddset(&mask, SIGINT);
+ sigaddset(&mask, SIGTERM);
+ sigaddset(&mask, SIGUSR1);
+ sigaddset(&mask, SIGUSR2);
+ sigprocmask(SIG_BLOCK, &mask, nullptr);
+
+ // Install the signals used to exit the process cleanly, or reload the
+ // config
+ struct sigaction on_sigint;
+ on_sigint.sa_sigaction = &sigint_handler;
+ // All signals must be blocked while a signal handler is running
+ sigfillset(&on_sigint.sa_mask);
+ // we want to catch that signal only once.
+ // Sending SIGINT again will "force" an exit
+ on_sigint.sa_flags = SA_RESETHAND;
+ sigaction(SIGINT, &on_sigint, nullptr);
+ sigaction(SIGTERM, &on_sigint, nullptr);
+
+ // Install a signal to reload the config on SIGUSR1/2
+ struct sigaction on_sigusr;
+ on_sigusr.sa_sigaction = &sigusr_handler;
+ sigfillset(&on_sigusr.sa_mask);
+ on_sigusr.sa_flags = 0;
+ sigaction(SIGUSR1, &on_sigusr, nullptr);
+ sigaction(SIGUSR2, &on_sigusr, nullptr);
+
+ auto p = std::make_shared<Poller>();
+ auto xmpp_component =
+ std::make_shared<BiboumiComponent>(p, hostname, password);
+ xmpp_component->start();
+
+#ifdef CARES_FOUND
+ DNSHandler::instance.watch_dns_sockets(p);
+#endif
+ auto timeout = TimedEventsManager::instance().get_timeout();
+ while (p->poll(timeout) != -1)
+ {
+ TimedEventsManager::instance().execute_expired_events();
+ // Check for empty irc_clients (not connected, or with no joined
+ // channel) and remove them
+ xmpp_component->clean();
+ if (stop)
+ {
+ log_info("Signal received, exiting...");
+#ifdef SYSTEMD_FOUND
+ sd_notify(0, "STOPPING=1");
+#endif
+ exiting = true;
+ stop.store(false);
+ xmpp_component->shutdown();
+ // Cancel the timer for a potential reconnection
+ TimedEventsManager::instance().cancel("XMPP reconnection");
+ }
+ if (reload)
+ {
+ log_info("Signal received, reloading the config...");
+ ::reload_process();
+ reload.store(false);
+ }
+ // Reconnect to the XMPP server if this was not intended. This may have
+ // happened because we sent something invalid to it and it decided to
+ // close the connection. This is a bug that should be fixed, but we
+ // still reconnect automatically instead of dropping everything
+ if (!exiting && xmpp_component->ever_auth &&
+ !xmpp_component->is_connected() &&
+ !xmpp_component->is_connecting())
+ {
+ if (xmpp_component->first_connection_try == true)
+ { // immediately re-try to connect
+ xmpp_component->reset();
+ xmpp_component->start();
+ }
+ else
+ { // Re-connecting failed, we now try only each few seconds
+ auto reconnect_later = [xmpp_component]()
+ {
+ xmpp_component->reset();
+ xmpp_component->start();
+ };
+ TimedEvent event(std::chrono::steady_clock::now() + 2s,
+ reconnect_later, "XMPP reconnection");
+ TimedEventsManager::instance().add_event(std::move(event));
+ }
+ }
+ // If the only existing connection is the one to the XMPP component:
+ // close the XMPP stream.
+ if (exiting && xmpp_component->is_connecting())
+ xmpp_component->close();
+ if (exiting && p->size() == 1 && xmpp_component->is_document_open())
+ xmpp_component->close_document();
+#ifdef CARES_FOUND
+ if (!exiting)
+ DNSHandler::instance.watch_dns_sockets(p);
+#endif
+ if (exiting) // If we are exiting, do not wait for any timed event
+ timeout = utils::no_timeout;
+ else
+ timeout = TimedEventsManager::instance().get_timeout();
+ }
+#ifdef CARES_FOUND
+ DNSHandler::instance.destroy();
+#endif
+ if (!xmpp_component->ever_auth)
+ return 1; // To signal that the process did not properly start
+ log_info("All connections cleanly closed, have a nice day.");
+ return 0;
+}
diff --git a/src/utils/empty_if_fixed_server.hpp b/src/utils/empty_if_fixed_server.hpp
new file mode 100644
index 0000000..9ccf5fd
--- /dev/null
+++ b/src/utils/empty_if_fixed_server.hpp
@@ -0,0 +1,26 @@
+#pragma once
+
+
+#include <string>
+
+#include <config/config.hpp>
+
+namespace utils
+{
+ inline std::string empty_if_fixed_server(std::string&& str)
+ {
+ if (!Config::get("fixed_irc_server", "").empty())
+ return {};
+ return str;
+ }
+
+ inline std::string empty_if_fixed_server(const std::string& str)
+ {
+ if (!Config::get("fixed_irc_server", "").empty())
+ return {};
+ return str;
+ }
+
+}
+
+
diff --git a/src/utils/reload.cpp b/src/utils/reload.cpp
new file mode 100644
index 0000000..348c5b5
--- /dev/null
+++ b/src/utils/reload.cpp
@@ -0,0 +1,34 @@
+#include <utils/reload.hpp>
+#include <database/database.hpp>
+#include <config/config.hpp>
+#include <utils/xdg.hpp>
+#include <logger/logger.hpp>
+
+#include "biboumi.h"
+
+void open_database()
+{
+#ifdef USE_DATABASE
+ const auto db_filename = Config::get("db_name", xdg_data_path("biboumi.sqlite"));
+ log_info("Opening database: ", db_filename);
+ Database::open(db_filename);
+ log_info("database successfully opened.");
+#endif
+}
+
+void reload_process()
+{
+ Config::read_conf();
+ // Destroy the logger instance, to be recreated the next time a log
+ // line needs to be written
+ Logger::instance().reset();
+ log_info("Configuration and logger reloaded.");
+#ifdef USE_DATABASE
+ try {
+ open_database();
+ } catch (const litesql::DatabaseError&) {
+ log_warning("Re-using the previous database.");
+ }
+#endif
+}
+
diff --git a/src/utils/reload.hpp b/src/utils/reload.hpp
new file mode 100644
index 0000000..408426a
--- /dev/null
+++ b/src/utils/reload.hpp
@@ -0,0 +1,4 @@
+#pragma once
+
+void open_database();
+void reload_process();
diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp
new file mode 100644
index 0000000..eec930d
--- /dev/null
+++ b/src/xmpp/biboumi_adhoc_commands.cpp
@@ -0,0 +1,635 @@
+#include <xmpp/biboumi_adhoc_commands.hpp>
+#include <xmpp/biboumi_component.hpp>
+#include <config/config.hpp>
+#include <utils/string.hpp>
+#include <utils/split.hpp>
+#include <xmpp/jid.hpp>
+
+#include <biboumi.h>
+
+#ifdef USE_DATABASE
+#include <database/database.hpp>
+#endif
+
+#include <louloulibs.h>
+
+#include <algorithm>
+
+using namespace std::string_literals;
+
+void DisconnectUserStep1(XmppComponent& xmpp_component, AdhocSession&, XmlNode& command_node)
+{
+ auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component);
+
+ XmlNode x("jabber:x:data:x");
+ x["type"] = "form";
+ XmlNode title("title");
+ title.set_inner("Disconnect a user from the gateway");
+ x.add_child(std::move(title));
+ XmlNode instructions("instructions");
+ instructions.set_inner("Choose a user JID and a quit message");
+ x.add_child(std::move(instructions));
+ XmlNode jids_field("field");
+ jids_field["var"] = "jids";
+ jids_field["type"] = "list-multi";
+ jids_field["label"] = "The JIDs to disconnect";
+ XmlNode required("required");
+ jids_field.add_child(std::move(required));
+ for (Bridge* bridge: biboumi_component.get_bridges())
+ {
+ XmlNode option("option");
+ option["label"] = bridge->get_jid();
+ XmlNode value("value");
+ value.set_inner(bridge->get_jid());
+ option.add_child(std::move(value));
+ jids_field.add_child(std::move(option));
+ }
+ x.add_child(std::move(jids_field));
+
+ XmlNode message_field("field");
+ message_field["var"] = "quit-message";
+ message_field["type"] = "text-single";
+ message_field["label"] = "Quit message";
+ XmlNode message_value("value");
+ message_value.set_inner("Disconnected by admin");
+ message_field.add_child(std::move(message_value));
+ x.add_child(std::move(message_field));
+ command_node.add_child(std::move(x));
+}
+
+void DisconnectUserStep2(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node)
+{
+ auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component);
+
+ // Find out if the jids, and the quit message are provided in the form.
+ std::string quit_message;
+ const XmlNode* x = command_node.get_child("x", "jabber:x:data");
+ if (x)
+ {
+ const XmlNode* message_field = nullptr;
+ const XmlNode* jids_field = nullptr;
+ for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
+ if (field->get_tag("var") == "jids")
+ jids_field = field;
+ else if (field->get_tag("var") == "quit-message")
+ message_field = field;
+ if (message_field)
+ {
+ const XmlNode* value = message_field->get_child("value", "jabber:x:data");
+ if (value)
+ quit_message = value->get_inner();
+ }
+ if (jids_field)
+ {
+ std::size_t num = 0;
+ for (const XmlNode* value: jids_field->get_children("value", "jabber:x:data"))
+ {
+ Bridge* bridge = biboumi_component.find_user_bridge(value->get_inner());
+ if (bridge)
+ {
+ bridge->shutdown(quit_message);
+ num++;
+ }
+ }
+ command_node.delete_all_children();
+
+ XmlNode note("note");
+ note["type"] = "info";
+ if (num == 0)
+ note.set_inner("No user were disconnected.");
+ else if (num == 1)
+ note.set_inner("1 user has been disconnected.");
+ else
+ note.set_inner(std::to_string(num) + " users have been disconnected.");
+ command_node.add_child(std::move(note));
+ return;
+ }
+ }
+ XmlNode error(ADHOC_NS":error");
+ error["type"] = "modify";
+ XmlNode condition(STANZA_NS":bad-request");
+ error.add_child(std::move(condition));
+ command_node.add_child(std::move(error));
+ session.terminate();
+}
+
+#ifdef USE_DATABASE
+void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node)
+{
+ const Jid owner(session.get_owner_jid());
+ const Jid target(session.get_target_jid());
+ std::string server_domain;
+ if ((server_domain = Config::get("fixed_irc_server", "")).empty())
+ server_domain = target.local;
+ auto options = Database::get_irc_server_options(owner.local + "@" + owner.domain,
+ server_domain);
+
+ XmlNode x("jabber:x:data:x");
+ x["type"] = "form";
+ XmlNode title("title");
+ title.set_inner("Configure the IRC server "s + server_domain);
+ x.add_child(std::move(title));
+ XmlNode instructions("instructions");
+ instructions.set_inner("Edit the form, to configure the settings of the IRC server "s + server_domain);
+ x.add_child(std::move(instructions));
+
+ XmlNode required("required");
+
+ XmlNode ports("field");
+ ports["var"] = "ports";
+ ports["type"] = "text-multi";
+ ports["label"] = "Ports";
+ ports["desc"] = "List of ports to try, without TLS. Defaults: 6667.";
+ auto vals = utils::split(options.ports.value(), ';', false);
+ for (const auto& val: vals)
+ {
+ XmlNode ports_value("value");
+ ports_value.set_inner(val);
+ ports.add_child(std::move(ports_value));
+ }
+ ports.add_child(required);
+ x.add_child(std::move(ports));
+
+#ifdef BOTAN_FOUND
+ XmlNode tls_ports("field");
+ tls_ports["var"] = "tls_ports";
+ tls_ports["type"] = "text-multi";
+ tls_ports["label"] = "TLS ports";
+ tls_ports["desc"] = "List of ports to try, with TLS. Defaults: 6697, 6670.";
+ vals = utils::split(options.tlsPorts.value(), ';', false);
+ for (const auto& val: vals)
+ {
+ XmlNode tls_ports_value("value");
+ tls_ports_value.set_inner(val);
+ tls_ports.add_child(std::move(tls_ports_value));
+ }
+ tls_ports.add_child(required);
+ x.add_child(std::move(tls_ports));
+
+ XmlNode verify_cert("field");
+ verify_cert["var"] = "verify_cert";
+ verify_cert["type"] = "boolean";
+ verify_cert["label"] = "Verify certificate";
+ verify_cert["desc"] = "Whether or not to abort the connection if the server’s TLS certificate is invalid";
+ XmlNode verify_cert_value("value");
+ if (options.verifyCert.value())
+ verify_cert_value.set_inner("true");
+ else
+ verify_cert_value.set_inner("false");
+ verify_cert.add_child(std::move(verify_cert_value));
+ x.add_child(std::move(verify_cert));
+
+ XmlNode fingerprint("field");
+ fingerprint["var"] = "fingerprint";
+ fingerprint["type"] = "text-single";
+ fingerprint["label"] = "SHA-1 fingerprint of the TLS certificate to trust.";
+ if (!options.trustedFingerprint.value().empty())
+ {
+ XmlNode fingerprint_value("value");
+ fingerprint_value.set_inner(options.trustedFingerprint.value());
+ fingerprint.add_child(std::move(fingerprint_value));
+ }
+ fingerprint.add_child(required);
+ x.add_child(std::move(fingerprint));
+#endif
+
+ XmlNode pass("field");
+ pass["var"] = "pass";
+ pass["type"] = "text-private";
+ pass["label"] = "Server password (to be used in a PASS command when connecting)";
+ if (!options.pass.value().empty())
+ {
+ XmlNode pass_value("value");
+ pass_value.set_inner(options.pass.value());
+ pass.add_child(std::move(pass_value));
+ }
+ pass.add_child(required);
+ x.add_child(std::move(pass));
+
+ XmlNode after_cnt_cmd("field");
+ after_cnt_cmd["var"] = "after_connect_command";
+ after_cnt_cmd["type"] = "text-single";
+ after_cnt_cmd["desc"] = "Custom IRC command sent after the connection is established with the server.";
+ after_cnt_cmd["label"] = "After-connection IRC command";
+ if (!options.afterConnectionCommand.value().empty())
+ {
+ XmlNode after_cnt_cmd_value("value");
+ after_cnt_cmd_value.set_inner(options.afterConnectionCommand.value());
+ after_cnt_cmd.add_child(std::move(after_cnt_cmd_value));
+ }
+ after_cnt_cmd.add_child(required);
+ x.add_child(std::move(after_cnt_cmd));
+
+ if (Config::get("realname_customization", "true") == "true")
+ {
+ XmlNode username("field");
+ username["var"] = "username";
+ username["type"] = "text-single";
+ username["label"] = "Username";
+ if (!options.username.value().empty())
+ {
+ XmlNode username_value("value");
+ username_value.set_inner(options.username.value());
+ username.add_child(std::move(username_value));
+ }
+ username.add_child(required);
+ x.add_child(std::move(username));
+
+ XmlNode realname("field");
+ realname["var"] = "realname";
+ realname["type"] = "text-single";
+ realname["label"] = "Realname";
+ if (!options.realname.value().empty())
+ {
+ XmlNode realname_value("value");
+ realname_value.set_inner(options.realname.value());
+ realname.add_child(std::move(realname_value));
+ }
+ realname.add_child(required);
+ x.add_child(std::move(realname));
+ }
+
+ XmlNode encoding_out("field");
+ encoding_out["var"] = "encoding_out";
+ encoding_out["type"] = "text-single";
+ encoding_out["desc"] = "The encoding used when sending messages to the IRC server.";
+ encoding_out["label"] = "Out encoding";
+ if (!options.encodingOut.value().empty())
+ {
+ XmlNode encoding_out_value("value");
+ encoding_out_value.set_inner(options.encodingOut.value());
+ encoding_out.add_child(std::move(encoding_out_value));
+ }
+ encoding_out.add_child(required);
+ x.add_child(std::move(encoding_out));
+
+ XmlNode encoding_in("field");
+ encoding_in["var"] = "encoding_in";
+ encoding_in["type"] = "text-single";
+ encoding_in["desc"] = "The encoding used to decode message received from the IRC server.";
+ encoding_in["label"] = "In encoding";
+ if (!options.encodingIn.value().empty())
+ {
+ XmlNode encoding_in_value("value");
+ encoding_in_value.set_inner(options.encodingIn.value());
+ encoding_in.add_child(std::move(encoding_in_value));
+ }
+ encoding_in.add_child(required);
+ x.add_child(std::move(encoding_in));
+
+
+ command_node.add_child(std::move(x));
+}
+
+void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node)
+{
+ const XmlNode* x = command_node.get_child("x", "jabber:x:data");
+ if (x)
+ {
+ const Jid owner(session.get_owner_jid());
+ const Jid target(session.get_target_jid());
+ std::string server_domain;
+ if ((server_domain = Config::get("fixed_irc_server", "")).empty())
+ server_domain = target.local;
+ auto options = Database::get_irc_server_options(owner.local + "@" + owner.domain,
+ server_domain);
+ for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
+ {
+ const XmlNode* value = field->get_child("value", "jabber:x:data");
+ const std::vector<const XmlNode*> values = field->get_children("value", "jabber:x:data");
+ if (field->get_tag("var") == "ports")
+ {
+ std::string ports;
+ for (const auto& val: values)
+ ports += val->get_inner() + ";";
+ options.ports = ports;
+ }
+
+#ifdef BOTAN_FOUND
+ else if (field->get_tag("var") == "tls_ports")
+ {
+ std::string ports;
+ for (const auto& val: values)
+ ports += val->get_inner() + ";";
+ options.tlsPorts = ports;
+ }
+
+ else if (field->get_tag("var") == "verify_cert" && value
+ && !value->get_inner().empty())
+ {
+ auto val = to_bool(value->get_inner());
+ options.verifyCert = val;
+ }
+
+ else if (field->get_tag("var") == "fingerprint" && value &&
+ !value->get_inner().empty())
+ {
+ options.trustedFingerprint = value->get_inner();
+ }
+
+#endif // BOTAN_FOUND
+
+ else if (field->get_tag("var") == "pass" &&
+ value && !value->get_inner().empty())
+ options.pass = value->get_inner();
+
+ else if (field->get_tag("var") == "after_connect_command" &&
+ value && !value->get_inner().empty())
+ options.afterConnectionCommand = value->get_inner();
+
+ else if (field->get_tag("var") == "username" &&
+ value && !value->get_inner().empty())
+ {
+ auto username = value->get_inner();
+ // The username must not contain spaces
+ std::replace(username.begin(), username.end(), ' ', '_');
+ options.username = username;
+ }
+
+ else if (field->get_tag("var") == "realname" &&
+ value && !value->get_inner().empty())
+ options.realname = value->get_inner();
+
+ else if (field->get_tag("var") == "encoding_out" &&
+ value && !value->get_inner().empty())
+ options.encodingOut = value->get_inner();
+
+ else if (field->get_tag("var") == "encoding_in" &&
+ value && !value->get_inner().empty())
+ options.encodingIn = value->get_inner();
+
+ }
+
+ options.update();
+
+ command_node.delete_all_children();
+ XmlNode note("note");
+ note["type"] = "info";
+ note.set_inner("Configuration successfully applied.");
+ command_node.add_child(std::move(note));
+ return;
+ }
+ XmlNode error(ADHOC_NS":error");
+ error["type"] = "modify";
+ XmlNode condition(STANZA_NS":bad-request");
+ error.add_child(std::move(condition));
+ command_node.add_child(std::move(error));
+ session.terminate();
+}
+
+void ConfigureIrcChannelStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node)
+{
+ const Jid owner(session.get_owner_jid());
+ const Jid target(session.get_target_jid());
+ const Iid iid(target.local);
+ auto options = Database::get_irc_channel_options_with_server_default(owner.local + "@" + owner.domain,
+ iid.get_server(), iid.get_local());
+
+ XmlNode x("jabber:x:data:x");
+ x["type"] = "form";
+ XmlNode title("title");
+ title.set_inner("Configure the IRC channel "s + iid.get_local() + " on server "s + iid.get_server());
+ x.add_child(std::move(title));
+ XmlNode instructions("instructions");
+ instructions.set_inner("Edit the form, to configure the settings of the IRC channel "s + iid.get_local());
+ x.add_child(std::move(instructions));
+
+ XmlNode required("required");
+
+ XmlNode encoding_out("field");
+ encoding_out["var"] = "encoding_out";
+ encoding_out["type"] = "text-single";
+ encoding_out["desc"] = "The encoding used when sending messages to the IRC server. Defaults to the server's “out encoding” if unset for the channel";
+ encoding_out["label"] = "Out encoding";
+ if (!options.encodingOut.value().empty())
+ {
+ XmlNode encoding_out_value("value");
+ encoding_out_value.set_inner(options.encodingOut.value());
+ encoding_out.add_child(std::move(encoding_out_value));
+ }
+ encoding_out.add_child(required);
+ x.add_child(std::move(encoding_out));
+
+ XmlNode encoding_in("field");
+ encoding_in["var"] = "encoding_in";
+ encoding_in["type"] = "text-single";
+ encoding_in["desc"] = "The encoding used to decode message received from the IRC server. Defaults to the server's “in encoding” if unset for the channel";
+ encoding_in["label"] = "In encoding";
+ if (!options.encodingIn.value().empty())
+ {
+ XmlNode encoding_in_value("value");
+ encoding_in_value.set_inner(options.encodingIn.value());
+ encoding_in.add_child(std::move(encoding_in_value));
+ }
+ encoding_in.add_child(required);
+ x.add_child(std::move(encoding_in));
+
+ command_node.add_child(std::move(x));
+}
+
+void ConfigureIrcChannelStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node)
+{
+ const XmlNode* x = command_node.get_child("x", "jabber:x:data");
+ if (x)
+ {
+ const Jid owner(session.get_owner_jid());
+ const Jid target(session.get_target_jid());
+ const Iid iid(target.local);
+ auto options = Database::get_irc_channel_options(owner.local + "@" + owner.domain,
+ iid.get_server(), iid.get_local());
+ for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
+ {
+ const XmlNode* value = field->get_child("value", "jabber:x:data");
+
+ if (field->get_tag("var") == "encoding_out" &&
+ value && !value->get_inner().empty())
+ options.encodingOut = value->get_inner();
+
+ else if (field->get_tag("var") == "encoding_in" &&
+ value && !value->get_inner().empty())
+ options.encodingIn = value->get_inner();
+ }
+
+ options.update();
+
+ command_node.delete_all_children();
+ XmlNode note("note");
+ note["type"] = "info";
+ note.set_inner("Configuration successfully applied.");
+ command_node.add_child(std::move(note));
+ return;
+ }
+ XmlNode error(ADHOC_NS":error");
+ error["type"] = "modify";
+ XmlNode condition(STANZA_NS":bad-request");
+ error.add_child(std::move(condition));
+ command_node.add_child(std::move(error));
+ session.terminate();
+}
+#endif // USE_DATABASE
+
+void DisconnectUserFromServerStep1(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node)
+{
+ const Jid owner(session.get_owner_jid());
+ if (owner.bare() != Config::get("admin", ""))
+ { // A non-admin is not allowed to disconnect other users, only
+ // him/herself, so we just skip this step
+ auto next_step = session.get_next_step();
+ next_step(xmpp_component, session, command_node);
+ }
+ else
+ { // Send a form to select the user to disconnect
+ auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component);
+
+ XmlNode x("jabber:x:data:x");
+ x["type"] = "form";
+ XmlNode title("title");
+ title.set_inner("Disconnect a user from selected IRC servers");
+ x.add_child(std::move(title));
+ XmlNode instructions("instructions");
+ instructions.set_inner("Choose a user JID");
+ x.add_child(std::move(instructions));
+ XmlNode jids_field("field");
+ jids_field["var"] = "jid";
+ jids_field["type"] = "list-single";
+ jids_field["label"] = "The JID to disconnect";
+ XmlNode required("required");
+ jids_field.add_child(std::move(required));
+ for (Bridge* bridge: biboumi_component.get_bridges())
+ {
+ XmlNode option("option");
+ option["label"] = bridge->get_jid();
+ XmlNode value("value");
+ value.set_inner(bridge->get_jid());
+ option.add_child(std::move(value));
+ jids_field.add_child(std::move(option));
+ }
+ x.add_child(std::move(jids_field));
+ command_node.add_child(std::move(x));
+ }
+}
+
+void DisconnectUserFromServerStep2(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node)
+{
+ // If no JID is contained in the command node, it means we skipped the
+ // previous stage, and the jid to disconnect is the executor's jid
+ std::string jid_to_disconnect = session.get_owner_jid();
+
+ if (const XmlNode* x = command_node.get_child("x", "jabber:x:data"))
+ {
+ for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
+ if (field->get_tag("var") == "jid")
+ {
+ if (const XmlNode* value = field->get_child("value", "jabber:x:data"))
+ jid_to_disconnect = value->get_inner();
+ }
+ }
+
+ // Save that JID for the last step
+ session.vars["jid"] = jid_to_disconnect;
+
+ // Send a data form to let the user choose which server to disconnect the
+ // user from
+ command_node.delete_all_children();
+ auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component);
+
+ XmlNode x("jabber:x:data:x");
+ x["type"] = "form";
+ XmlNode title("title");
+ title.set_inner("Disconnect a user from selected IRC servers");
+ x.add_child(std::move(title));
+ XmlNode instructions("instructions");
+ instructions.set_inner("Choose one or more servers to disconnect this JID from");
+ x.add_child(std::move(instructions));
+ XmlNode jids_field("field");
+ jids_field["var"] = "irc-servers";
+ jids_field["type"] = "list-multi";
+ jids_field["label"] = "The servers to disconnect from";
+ XmlNode required("required");
+ jids_field.add_child(std::move(required));
+ Bridge* bridge = biboumi_component.find_user_bridge(jid_to_disconnect);
+
+ if (!bridge || bridge->get_irc_clients().empty())
+ {
+ XmlNode note("note");
+ note["type"] = "info";
+ note.set_inner("User "s + jid_to_disconnect + " is not connected to any IRC server.");
+ command_node.add_child(std::move(note));
+ session.terminate();
+ return ;
+ }
+
+ for (const auto& pair: bridge->get_irc_clients())
+ {
+ XmlNode option("option");
+ option["label"] = pair.first;
+ XmlNode value("value");
+ value.set_inner(pair.first);
+ option.add_child(std::move(value));
+ jids_field.add_child(std::move(option));
+ }
+ x.add_child(std::move(jids_field));
+
+ XmlNode message_field("field");
+ message_field["var"] = "quit-message";
+ message_field["type"] = "text-single";
+ message_field["label"] = "Quit message";
+ XmlNode message_value("value");
+ message_value.set_inner("Killed by admin");
+ message_field.add_child(std::move(message_value));
+ x.add_child(std::move(message_field));
+
+ command_node.add_child(std::move(x));
+}
+
+void DisconnectUserFromServerStep3(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node)
+{
+ const auto it = session.vars.find("jid");
+ if (it == session.vars.end())
+ return ;
+ const auto jid_to_disconnect = it->second;
+
+ std::vector<std::string> servers;
+ std::string quit_message;
+
+ if (const XmlNode* x = command_node.get_child("x", "jabber:x:data"))
+ {
+ for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
+ {
+ if (field->get_tag("var") == "irc-servers")
+ {
+ for (const XmlNode* value: field->get_children("value", "jabber:x:data"))
+ servers.push_back(value->get_inner());
+ }
+ else if (field->get_tag("var") == "quit-message")
+ if (const XmlNode* value = field->get_child("value", "jabber:x:data"))
+ quit_message = value->get_inner();
+ }
+ }
+
+ auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component);
+ Bridge* bridge = biboumi_component.find_user_bridge(jid_to_disconnect);
+ auto& clients = bridge->get_irc_clients();
+
+ std::size_t number = 0;
+
+ for (const auto& hostname: servers)
+ {
+ auto it = clients.find(hostname);
+ if (it != clients.end())
+ {
+ it->second->on_error({"ERROR", {quit_message}});
+ clients.erase(it);
+ number++;
+ }
+ }
+ command_node.delete_all_children();
+ XmlNode note("note");
+ note["type"] = "info";
+ std::string msg = jid_to_disconnect + " was disconnected from " + std::to_string(number) + " IRC server";
+ if (number > 1)
+ msg += "s";
+ msg += ".";
+ note.set_inner(msg);
+ command_node.add_child(std::move(note));
+}
diff --git a/src/xmpp/biboumi_adhoc_commands.hpp b/src/xmpp/biboumi_adhoc_commands.hpp
new file mode 100644
index 0000000..2763a9f
--- /dev/null
+++ b/src/xmpp/biboumi_adhoc_commands.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+
+#include <xmpp/adhoc_command.hpp>
+#include <xmpp/adhoc_session.hpp>
+#include <xmpp/xmpp_stanza.hpp>
+
+class XmppComponent;
+
+void DisconnectUserStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void DisconnectUserStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+
+void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+
+void ConfigureIrcChannelStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void ConfigureIrcChannelStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+
+void DisconnectUserFromServerStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void DisconnectUserFromServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void DisconnectUserFromServerStep3(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+
+
diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp
new file mode 100644
index 0000000..a6aac21
--- /dev/null
+++ b/src/xmpp/biboumi_component.cpp
@@ -0,0 +1,632 @@
+#include <xmpp/biboumi_component.hpp>
+
+#include <utils/timed_events.hpp>
+#include <utils/scopeguard.hpp>
+#include <utils/tolower.hpp>
+#include <logger/logger.hpp>
+#include <xmpp/adhoc_command.hpp>
+#include <xmpp/biboumi_adhoc_commands.hpp>
+#include <bridge/list_element.hpp>
+#include <config/config.hpp>
+#include <xmpp/jid.hpp>
+#include <utils/sha1.hpp>
+
+#include <stdexcept>
+#include <iostream>
+
+#include <stdio.h>
+
+#include <louloulibs.h>
+#include <biboumi.h>
+
+#include <uuid.h>
+
+#ifdef SYSTEMD_FOUND
+# include <systemd/sd-daemon.h>
+#endif
+
+using namespace std::string_literals;
+
+static std::set<std::string> kickable_errors{
+ "gone",
+ "internal-server-error",
+ "item-not-found",
+ "jid-malformed",
+ "recipient-unavailable",
+ "redirect",
+ "remote-server-not-found",
+ "remote-server-timeout",
+ "service-unavailable",
+ "malformed-error"
+ };
+
+
+BiboumiComponent::BiboumiComponent(std::shared_ptr<Poller> poller, const std::string& hostname, const std::string& secret):
+ XmppComponent(poller, hostname, secret),
+ irc_server_adhoc_commands_handler(*this),
+ irc_channel_adhoc_commands_handler(*this)
+{
+ this->stanza_handlers.emplace("presence",
+ std::bind(&BiboumiComponent::handle_presence, this,std::placeholders::_1));
+ this->stanza_handlers.emplace("message",
+ std::bind(&BiboumiComponent::handle_message, this,std::placeholders::_1));
+ this->stanza_handlers.emplace("iq",
+ std::bind(&BiboumiComponent::handle_iq, this,std::placeholders::_1));
+
+ this->adhoc_commands_handler.get_commands() = {
+ {"ping", AdhocCommand({&PingStep1}, "Do a ping", false)},
+ {"hello", AdhocCommand({&HelloStep1, &HelloStep2}, "Receive a custom greeting", false)},
+ {"disconnect-user", AdhocCommand({&DisconnectUserStep1, &DisconnectUserStep2}, "Disconnect selected users from the gateway", true)},
+ {"disconnect-from-irc-servers", AdhocCommand({&DisconnectUserFromServerStep1, &DisconnectUserFromServerStep2, &DisconnectUserFromServerStep3}, "Disconnect from the selected IRC servers", false)},
+ {"reload", AdhocCommand({&Reload}, "Reload biboumi’s configuration", true)}
+ };
+
+#ifdef USE_DATABASE
+ AdhocCommand configure_server_command({&ConfigureIrcServerStep1, &ConfigureIrcServerStep2}, "Configure a few settings for that IRC server", false);
+ if (!Config::get("fixed_irc_server", "").empty())
+ {
+ this->adhoc_commands_handler.get_commands().emplace(std::make_pair("configure",
+ configure_server_command));
+ }
+#endif
+
+ this->irc_server_adhoc_commands_handler.get_commands() = {
+#ifdef USE_DATABASE
+ {"configure", configure_server_command},
+#endif
+ };
+ this->irc_channel_adhoc_commands_handler.get_commands() = {
+#ifdef USE_DATABASE
+ {"configure", AdhocCommand({&ConfigureIrcChannelStep1, &ConfigureIrcChannelStep2}, "Configure a few settings for that IRC channel", false)},
+#endif
+ };
+}
+
+void BiboumiComponent::shutdown()
+{
+ for (auto it = this->bridges.begin(); it != this->bridges.end(); ++it)
+ {
+ it->second->shutdown("Gateway shutdown");
+ }
+}
+
+void BiboumiComponent::clean()
+{
+ auto it = this->bridges.begin();
+ while (it != this->bridges.end())
+ {
+ it->second->clean();
+ if (it->second->active_clients() == 0)
+ it = this->bridges.erase(it);
+ else
+ ++it;
+ }
+}
+
+void BiboumiComponent::handle_presence(const Stanza& stanza)
+{
+ std::string from_str = stanza.get_tag("from");
+ std::string id = stanza.get_tag("id");
+ std::string to_str = stanza.get_tag("to");
+ std::string type = stanza.get_tag("type");
+
+ // Check for mandatory tags
+ if (from_str.empty())
+ {
+ log_warning("Received an invalid presence stanza: tag 'from' is missing.");
+ return;
+ }
+ if (to_str.empty())
+ {
+ this->send_stanza_error("presence", from_str, this->served_hostname, id,
+ "modify", "bad-request", "Missing 'to' tag");
+ return;
+ }
+
+ Bridge* bridge = this->get_user_bridge(from_str);
+ Jid to(to_str);
+ Jid from(from_str);
+ Iid iid(to.local);
+
+ // An error stanza is sent whenever we exit this function without
+ // disabling this scopeguard. If error_type and error_name are not
+ // changed, the error signaled is internal-server-error. Change their
+ // value to signal an other kind of error. For example
+ // feature-not-implemented, etc. Any non-error process should reach the
+ // stanza_error.disable() call at the end of the function.
+ std::string error_type("cancel");
+ std::string error_name("internal-server-error");
+ utils::ScopeGuard stanza_error([&](){
+ this->send_stanza_error("presence", from_str, to_str, id,
+ error_type, error_name, "");
+ });
+
+ try {
+ if (iid.is_channel && !iid.get_server().empty())
+ { // presence toward a MUC that corresponds to an irc channel, or a
+ // dummy channel if iid.chan is empty
+ if (type.empty())
+ {
+ const std::string own_nick = bridge->get_own_nick(iid);
+ if (!own_nick.empty() && own_nick != to.resource)
+ bridge->send_irc_nick_change(iid, to.resource);
+ const XmlNode* x = stanza.get_child("x", MUC_NS);
+ const XmlNode* password = x ? x->get_child("password", MUC_NS): nullptr;
+ bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "",
+ from.resource);
+ }
+ else if (type == "unavailable")
+ {
+ const XmlNode* status = stanza.get_child("status", COMPONENT_NS);
+ bridge->leave_irc_channel(std::move(iid), status ? status->get_inner() : "", from.resource);
+ }
+ }
+ else
+ {
+ // An user wants to join an invalid IRC channel, return a presence error to him
+ if (type.empty())
+ this->send_invalid_room_error(to.local, to.resource, from_str);
+ }
+ }
+ catch (const IRCNotConnected& ex)
+ {
+ this->send_stanza_error("presence", from_str, to_str, id,
+ "cancel", "remote-server-not-found",
+ "Not connected to IRC server "s + ex.hostname,
+ true);
+ }
+ stanza_error.disable();
+}
+
+void BiboumiComponent::handle_message(const Stanza& stanza)
+{
+ std::string from = stanza.get_tag("from");
+ std::string id = stanza.get_tag("id");
+ std::string to_str = stanza.get_tag("to");
+ std::string type = stanza.get_tag("type");
+
+ if (from.empty())
+ return;
+ if (type.empty())
+ type = "normal";
+ Bridge* bridge = this->get_user_bridge(from);
+ Jid to(to_str);
+ Iid iid(to.local);
+
+ std::string error_type("cancel");
+ std::string error_name("internal-server-error");
+ utils::ScopeGuard stanza_error([&](){
+ this->send_stanza_error("message", from, to_str, id,
+ error_type, error_name, "");
+ });
+ const XmlNode* body = stanza.get_child("body", COMPONENT_NS);
+
+ try { // catch IRCNotConnected exceptions
+ if (type == "groupchat" && iid.is_channel)
+ {
+ if (body && !body->get_inner().empty())
+ {
+ bridge->send_channel_message(iid, body->get_inner());
+ }
+ const XmlNode* subject = stanza.get_child("subject", COMPONENT_NS);
+ if (subject)
+ bridge->set_channel_topic(iid, subject->get_inner());
+ }
+ else if (type == "error")
+ {
+ const XmlNode* error = stanza.get_child("error", COMPONENT_NS);
+ // Only a set of errors are considered “fatal”. If we encounter one of
+ // them, we purge (we disconnect the user from all the IRC servers).
+ // We consider this to be true, unless the error condition is
+ // specified and is not in the kickable_errors set
+ bool kickable_error = true;
+ if (error && error->has_children())
+ {
+ const XmlNode* condition = error->get_last_child();
+ if (kickable_errors.find(condition->get_name()) == kickable_errors.end())
+ kickable_error = false;
+ }
+ if (kickable_error)
+ bridge->shutdown("Error from remote client");
+ }
+ else if (type == "chat")
+ {
+ if (body && !body->get_inner().empty())
+ {
+ // a message for nick!server
+ if (iid.is_user && !iid.get_local().empty())
+ {
+ bridge->send_private_message(iid, body->get_inner());
+ bridge->remove_preferred_from_jid(iid.get_local());
+ }
+ else if (!iid.is_user && !to.resource.empty())
+ { // a message for chan%server@biboumi/Nick or
+ // server@biboumi/Nick
+ // Convert that into a message to nick!server
+ Iid user_iid(utils::tolower(to.resource) + "!" + iid.get_server());
+ bridge->send_private_message(user_iid, body->get_inner());
+ bridge->set_preferred_from_jid(user_iid.get_local(), to_str);
+ }
+ else if (!iid.is_user && !iid.is_channel)
+ { // Message sent to the server JID
+ // Convert the message body into a raw IRC message
+ bridge->send_raw_message(iid.get_server(), body->get_inner());
+ }
+ }
+ }
+ else if (iid.is_user)
+ this->send_invalid_user_error(to.local, from);
+ } catch (const IRCNotConnected& ex)
+ {
+ this->send_stanza_error("message", from, to_str, id,
+ "cancel", "remote-server-not-found",
+ "Not connected to IRC server "s + ex.hostname,
+ true);
+ }
+ stanza_error.disable();
+}
+
+// We MUST return an iq, whatever happens, except if the type is
+// "result".
+// To do this, we use a scopeguard. If an exception is raised somewhere, an
+// iq of type error "internal-server-error" is sent. If we handle the
+// request properly (by calling a function that registers an iq to be sent
+// later, or that directly sends an iq), we disable the ScopeGuard. If we
+// reach the end of the function without having disabled the scopeguard, we
+// send a "feature-not-implemented" iq as a result. If an other kind of
+// error is found (for example the feature is implemented in biboumi, but
+// the request is missing some attribute) we can just change the values of
+// error_type and error_name and return from the function (without disabling
+// the scopeguard); an iq error will be sent
+void BiboumiComponent::handle_iq(const Stanza& stanza)
+{
+ std::string id = stanza.get_tag("id");
+ std::string from = stanza.get_tag("from");
+ std::string to_str = stanza.get_tag("to");
+ std::string type = stanza.get_tag("type");
+
+ if (from.empty()) {
+ log_warning("Received an iq without a 'from'. Ignoring.");
+ return;
+ }
+ if (id.empty() || to_str.empty() || type.empty())
+ {
+ this->send_stanza_error("iq", from, this->served_hostname, id,
+ "modify", "bad-request", "");
+ return;
+ }
+
+ Bridge* bridge = this->get_user_bridge(from);
+ Jid to(to_str);
+
+ // These two values will be used in the error iq sent if we don't disable
+ // the scopeguard.
+ std::string error_type("cancel");
+ std::string error_name("internal-server-error");
+ utils::ScopeGuard stanza_error([&](){
+ this->send_stanza_error("iq", from, to_str, id,
+ error_type, error_name, "");
+ });
+ try {
+ if (type == "set")
+ {
+ const XmlNode* query;
+ if ((query = stanza.get_child("query", MUC_ADMIN_NS)))
+ {
+ const XmlNode* child = query->get_child("item", MUC_ADMIN_NS);
+ if (child)
+ {
+ std::string nick = child->get_tag("nick");
+ std::string role = child->get_tag("role");
+ std::string affiliation = child->get_tag("affiliation");
+ if (!nick.empty())
+ {
+ Iid iid(to.local);
+ if (role == "none")
+ { // This is a kick
+ std::string reason;
+ const XmlNode* reason_el = child->get_child("reason", MUC_ADMIN_NS);
+ if (reason_el)
+ reason = reason_el->get_inner();
+ bridge->send_irc_kick(iid, nick, reason, id, from);
+ }
+ else
+ bridge->forward_affiliation_role_change(iid, nick, affiliation, role);
+ stanza_error.disable();
+ }
+ }
+ }
+ else if ((query = stanza.get_child("command", ADHOC_NS)))
+ {
+ Stanza response("iq");
+ response["to"] = from;
+ response["from"] = to_str;
+ response["id"] = id;
+
+ // Depending on the 'to' jid in the request, we use one adhoc
+ // command handler or an other
+ Iid iid(to.local);
+ AdhocCommandsHandler* adhoc_handler;
+ if (!to.local.empty() && !iid.is_user && !iid.is_channel)
+ adhoc_handler = &this->irc_server_adhoc_commands_handler;
+ else if (!to.local.empty() && iid.is_channel)
+ adhoc_handler = &this->irc_channel_adhoc_commands_handler;
+ else
+ adhoc_handler = &this->adhoc_commands_handler;
+
+ // Execute the command, if any, and get a result XmlNode that we
+ // insert in our response
+ XmlNode inner_node = adhoc_handler->handle_request(from, to_str, *query);
+ if (inner_node.get_child("error", ADHOC_NS))
+ response["type"] = "error";
+ else
+ response["type"] = "result";
+ response.add_child(std::move(inner_node));
+ this->send_stanza(response);
+ stanza_error.disable();
+ }
+ }
+ else if (type == "get")
+ {
+ const XmlNode* query;
+ if ((query = stanza.get_child("query", DISCO_INFO_NS)))
+ { // Disco info
+ if (to_str == this->served_hostname)
+ {
+ const std::string node = query->get_tag("node");
+ if (node.empty())
+ {
+ // On the gateway itself
+ this->send_self_disco_info(id, from);
+ stanza_error.disable();
+ }
+ }
+ }
+ else if ((query = stanza.get_child("query", VERSION_NS)))
+ {
+ Iid iid(to.local);
+ if (iid.is_user ||
+ (iid.is_channel && !to.resource.empty()))
+ {
+ // Get the IRC user version
+ std::string target;
+ if (iid.is_user)
+ target = iid.get_local();
+ else
+ target = to.resource;
+ bridge->send_irc_version_request(iid.get_server(), target, id,
+ from, to_str);
+ }
+ else
+ {
+ // On the gateway itself or on a channel
+ this->send_version(id, from, to_str);
+ }
+ stanza_error.disable();
+ }
+ else if ((query = stanza.get_child("query", DISCO_ITEMS_NS)))
+ {
+ Iid iid(to.local);
+ const std::string node = query->get_tag("node");
+ if (node == ADHOC_NS)
+ {
+ Jid from_jid(from);
+ if (to.local.empty())
+ { // Get biboumi's adhoc commands
+ this->send_adhoc_commands_list(id, from, this->served_hostname,
+ (Config::get("admin", "") ==
+ from_jid.bare()),
+ this->adhoc_commands_handler);
+ stanza_error.disable();
+ }
+ else if (!iid.is_user && !iid.is_channel)
+ { // Get the server's adhoc commands
+ this->send_adhoc_commands_list(id, from, to_str,
+ (Config::get("admin", "") ==
+ from_jid.bare()),
+ this->irc_server_adhoc_commands_handler);
+ stanza_error.disable();
+ }
+ else if (!iid.is_user && iid.is_channel)
+ { // Get the channel's adhoc commands
+ this->send_adhoc_commands_list(id, from, to_str,
+ (Config::get("admin", "") ==
+ from_jid.bare()),
+ this->irc_channel_adhoc_commands_handler);
+ stanza_error.disable();
+ }
+ }
+ else if (node.empty() && !iid.is_user && !iid.is_channel)
+ { // Disco on an IRC server: get the list of channels
+ bridge->send_irc_channel_list_request(iid, id, from);
+ stanza_error.disable();
+ }
+ }
+ else if ((query = stanza.get_child("ping", PING_NS)))
+ {
+ Iid iid(to.local);
+ if (iid.is_user)
+ { // Ping any user (no check on the nick done ourself)
+ bridge->send_irc_user_ping_request(iid.get_server(),
+ iid.get_local(), id, from, to_str);
+ }
+ else if (iid.is_channel && !to.resource.empty())
+ { // Ping a room participant (we check if the nick is in the room)
+ bridge->send_irc_participant_ping_request(iid,
+ to.resource, id, from, to_str);
+ }
+ else
+ { // Ping a channel, a server or the gateway itself
+ bridge->on_gateway_ping(iid.get_server(),
+ id, from, to_str);
+ }
+ stanza_error.disable();
+ }
+ }
+ else if (type == "result")
+ {
+ stanza_error.disable();
+ const XmlNode* query;
+ if ((query = stanza.get_child("query", VERSION_NS)))
+ {
+ const XmlNode* name_node = query->get_child("name", VERSION_NS);
+ const XmlNode* version_node = query->get_child("version", VERSION_NS);
+ const XmlNode* os_node = query->get_child("os", VERSION_NS);
+ std::string name;
+ std::string version;
+ std::string os;
+ if (name_node)
+ name = name_node->get_inner() + " (through the biboumi gateway)";
+ if (version_node)
+ version = version_node->get_inner();
+ if (os_node)
+ os = os_node->get_inner();
+ const Iid iid(to.local);
+ bridge->send_xmpp_version_to_irc(iid, name, version, os);
+ }
+ else
+ {
+ const auto it = this->waiting_iq.find(id);
+ if (it != this->waiting_iq.end())
+ {
+ it->second(bridge, stanza);
+ this->waiting_iq.erase(it);
+ }
+ }
+ }
+ }
+ catch (const IRCNotConnected& ex)
+ {
+ this->send_stanza_error("iq", from, to_str, id,
+ "cancel", "remote-server-not-found",
+ "Not connected to IRC server "s + ex.hostname,
+ true);
+ stanza_error.disable();
+ return;
+ }
+ error_type = "cancel";
+ error_name = "feature-not-implemented";
+}
+
+Bridge* BiboumiComponent::get_user_bridge(const std::string& user_jid)
+{
+ auto bare_jid = Jid{user_jid}.bare();
+ try
+ {
+ return this->bridges.at(bare_jid).get();
+ }
+ catch (const std::out_of_range& exception)
+ {
+ this->bridges.emplace(bare_jid, std::make_unique<Bridge>(bare_jid, *this, this->poller));
+ return this->bridges.at(bare_jid).get();
+ }
+}
+
+Bridge* BiboumiComponent::find_user_bridge(const std::string& full_jid)
+{
+ auto bare_jid = Jid{full_jid}.bare();
+ try
+ {
+ return this->bridges.at(bare_jid).get();
+ }
+ catch (const std::out_of_range& exception)
+ {
+ return nullptr;
+ }
+}
+
+std::vector<Bridge*> BiboumiComponent::get_bridges() const
+{
+ std::vector<Bridge*> res;
+ for (auto it = this->bridges.begin(); it != this->bridges.end(); ++it)
+ res.push_back(it->second.get());
+ return res;
+}
+
+void BiboumiComponent::send_self_disco_info(const std::string& id, const std::string& jid_to)
+{
+ Stanza iq("iq");
+ iq["type"] = "result";
+ iq["id"] = id;
+ iq["to"] = jid_to;
+ iq["from"] = this->served_hostname;
+ XmlNode query("query");
+ query["xmlns"] = DISCO_INFO_NS;
+ XmlNode identity("identity");
+ identity["category"] = "conference";
+ identity["type"] = "irc";
+ identity["name"] = "Biboumi XMPP-IRC gateway";
+ query.add_child(std::move(identity));
+ for (const char* ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS})
+ {
+ XmlNode feature("feature");
+ feature["var"] = ns;
+ query.add_child(std::move(feature));
+ }
+ iq.add_child(std::move(query));
+ this->send_stanza(iq);
+}
+
+void BiboumiComponent::send_iq_version_request(const std::string& from,
+ const std::string& jid_to)
+{
+ Stanza iq("iq");
+ iq["type"] = "get";
+ iq["id"] = "version_"s + this->next_id();
+ iq["from"] = from + "@" + this->served_hostname;
+ iq["to"] = jid_to;
+ XmlNode query("query");
+ query["xmlns"] = VERSION_NS;
+ iq.add_child(std::move(query));
+ this->send_stanza(iq);
+}
+
+void BiboumiComponent::send_ping_request(const std::string& from,
+ const std::string& jid_to,
+ const std::string& id)
+{
+ Stanza iq("iq");
+ iq["type"] = "get";
+ iq["id"] = id;
+ iq["from"] = from + "@" + this->served_hostname;
+ iq["to"] = jid_to;
+ XmlNode ping("ping");
+ ping["xmlns"] = PING_NS;
+ iq.add_child(std::move(ping));
+ this->send_stanza(iq);
+
+ auto result_cb = [from, id](Bridge* bridge, const Stanza& stanza)
+ {
+ Jid to(stanza.get_tag("to"));
+ if (to.local != from)
+ {
+ log_error("Received a corresponding ping result, but the 'to' from "
+ "the response mismatches the 'from' of the request");
+ }
+ else
+ bridge->send_irc_ping_result(from, id);
+ };
+ this->waiting_iq[id] = result_cb;
+}
+
+void BiboumiComponent::send_iq_room_list_result(const std::string& id,
+ const std::string& to_jid,
+ const std::string& from,
+ const std::vector<ListElement>& rooms_list)
+{
+ Stanza iq("iq");
+ iq["from"] = from + "@" + this->served_hostname;
+ iq["to"] = to_jid;
+ iq["id"] = id;
+ iq["type"] = "result";
+ XmlNode query("query");
+ query["xmlns"] = DISCO_ITEMS_NS;
+ for (const auto& room: rooms_list)
+ {
+ XmlNode item("item");
+ item["jid"] = room.channel + "%" + from + "@" + this->served_hostname;
+ query.add_child(std::move(item));
+ }
+ iq.add_child(std::move(query));
+ this->send_stanza(iq);
+}
diff --git a/src/xmpp/biboumi_component.hpp b/src/xmpp/biboumi_component.hpp
new file mode 100644
index 0000000..24d768a
--- /dev/null
+++ b/src/xmpp/biboumi_component.hpp
@@ -0,0 +1,109 @@
+#pragma once
+
+
+#include <xmpp/xmpp_component.hpp>
+
+#include <bridge/bridge.hpp>
+
+#include <memory>
+#include <string>
+#include <map>
+
+struct ListElement;
+
+/**
+ * A callback called when the waited iq result is received (it is matched
+ * against the iq id)
+ */
+using iq_responder_callback_t = std::function<void(Bridge* bridge, const Stanza& stanza)>;
+
+/**
+ * Interact with the Biboumi Bridge
+ */
+class BiboumiComponent: public XmppComponent
+{
+public:
+ explicit BiboumiComponent(std::shared_ptr<Poller> poller, const std::string& hostname, const std::string& secret);
+ ~BiboumiComponent() = default;
+
+ BiboumiComponent(const BiboumiComponent&) = delete;
+ BiboumiComponent(BiboumiComponent&&) = delete;
+ BiboumiComponent& operator=(const BiboumiComponent&) = delete;
+ BiboumiComponent& operator=(BiboumiComponent&&) = delete;
+
+ /**
+ * Returns the bridge for the given user. If it does not exist, return
+ * nullptr.
+ */
+ Bridge* find_user_bridge(const std::string& full_jid);
+ /**
+ * Return a list of all the managed bridges.
+ */
+ std::vector<Bridge*> get_bridges() const;
+
+ /**
+ * Send a "close" message to all our connected peers. That message
+ * depends on the protocol used (this may be a QUIT irc message, or a
+ * <stream/>, etc). We may also directly close the connection, or we may
+ * wait for the remote peer to acknowledge it before closing.
+ */
+ void shutdown();
+ /**
+ * Run a check on all bridges, to remove all disconnected (socket is
+ * closed, or no channel is joined) IrcClients. Some kind of garbage collector.
+ */
+ void clean();
+ /**
+ * Send a result IQ with the gateway disco informations.
+ */
+ void send_self_disco_info(const std::string& id, const std::string& jid_to);
+ /**
+ * Send an iq version request
+ */
+ void send_iq_version_request(const std::string& from,
+ const std::string& jid_to);
+ /**
+ * Send a ping request
+ */
+ void send_ping_request(const std::string& from,
+ const std::string& jid_to,
+ const std::string& id);
+ /**
+ * Send the channels list in one big stanza
+ */
+ void send_iq_room_list_result(const std::string& id, const std::string& to_jid,
+ const std::string& from,
+ const std::vector<ListElement>& rooms_list);
+ /**
+ * Handle the various stanza types
+ */
+ void handle_presence(const Stanza& stanza);
+ void handle_message(const Stanza& stanza);
+ void handle_iq(const Stanza& stanza);
+
+private:
+ /**
+ * Return the bridge associated with the bare JID. Create a new one
+ * if none already exist.
+ */
+ Bridge* get_user_bridge(const std::string& user_jid);
+
+ /**
+ * A map of id -> callback. When we want to wait for an iq result, we add
+ * the callback to this map, with the iq id as the key. When an iq result
+ * is received, we look for a corresponding callback in this map. If
+ * found, we call it and remove it.
+ */
+ std::map<std::string, iq_responder_callback_t> waiting_iq;
+
+ /**
+ * One bridge for each user of the component. Indexed by the user's bare
+ * jid
+ */
+ std::unordered_map<std::string, std::unique_ptr<Bridge>> bridges;
+
+ AdhocCommandsHandler irc_server_adhoc_commands_handler;
+ AdhocCommandsHandler irc_channel_adhoc_commands_handler;
+};
+
+
diff --git a/tests/colors.cpp b/tests/colors.cpp
new file mode 100644
index 0000000..bf52989
--- /dev/null
+++ b/tests/colors.cpp
@@ -0,0 +1,54 @@
+#include "catch.hpp"
+
+#include <bridge/colors.hpp>
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <memory>
+
+TEST_CASE("IRC colors parsing")
+{
+ std::unique_ptr<XmlNode> xhtml;
+ std::string cleaned_up;
+
+ std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("bold");
+ CHECK(xhtml);
+ CHECK(xhtml->to_string() == "<body xmlns='http://www.w3.org/1999/xhtml'><span style='font-weight:bold;'>bold</span></body>");
+
+ std::tie(cleaned_up, xhtml) =
+ irc_format_to_xhtmlim("normalboldunder-and-boldbold normal"
+ "5red,5default-on-red10,2cyan-on-blue");
+ CHECK(xhtml);
+ CHECK(xhtml->to_string() == "<body xmlns='http://www.w3.org/1999/xhtml'>normal<span style='font-weight:bold;'>bold</span><span style='font-weight:bold;text-decoration:underline;'>under-and-bold</span><span style='font-weight:bold;'>bold</span> normal<span style='color:red;'>red</span><span style='background-color:red;'>default-on-red</span><span style='color:cyan;background-color:blue;'>cyan-on-blue</span></body>");
+ CHECK(cleaned_up == "normalboldunder-and-boldbold normalreddefault-on-redcyan-on-blue");
+
+ std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("normal");
+ CHECK_FALSE(xhtml);
+ CHECK(cleaned_up == "normal");
+
+ std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("");
+ CHECK(xhtml);
+ CHECK(!xhtml->has_children());
+ CHECK(cleaned_up.empty());
+
+ std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim(",a");
+ CHECK(xhtml);
+ CHECK(!xhtml->has_children());
+ CHECK(cleaned_up == "a");
+
+ std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim(",");
+ CHECK(xhtml);
+ CHECK(!xhtml->has_children());
+ 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(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");
+ CHECK(cleaned_up == "0e46ab by Pierre Dindon [0|1|0] http://example.net/Ojrh4P media: avoid pop-in effect when loading thumbnails by specifying an explicit size");
+ CHECK(xhtml->to_string() == "<body xmlns='http://www.w3.org/1999/xhtml'>0e46ab by <span style='color:green;'>Pierre Dindon</span> [<span style='color:lightgreen;'>0</span>|<span style='color:lightgreen;'>1</span>|<span style='color:indianred;'>0</span>] <span style='text-decoration:underline;'/><span style='text-decoration:underline;color:blue;'>http://example.net/Ojrh4P</span><span style='text-decoration:underline;'/> media: avoid pop-in effect when loading thumbnails by specifying an explicit size</body>");
+
+ std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("test\ncoucou");
+ CHECK(cleaned_up == "test\ncoucou");
+ CHECK(xhtml->to_string() == "<body xmlns='http://www.w3.org/1999/xhtml'>test<br/>coucou</body>");
+}
diff --git a/tests/config.cpp b/tests/config.cpp
new file mode 100644
index 0000000..ddea151
--- /dev/null
+++ b/tests/config.cpp
@@ -0,0 +1,54 @@
+#include "catch.hpp"
+
+#include <config/config.hpp>
+
+TEST_CASE("Config basic")
+{
+ // Write a value in the config file
+ Config::read_conf("test.cfg");
+ Config::set("coucou", "bonjour", true);
+ Config::clear();
+
+ bool error = false;
+ try
+ {
+ CHECK(Config::read_conf());
+ CHECK(Config::get("coucou", "") == "bonjour");
+ CHECK(Config::get("does not exist", "default") == "default");
+ Config::clear();
+ }
+ catch (const std::ios::failure& e)
+ {
+ error = true;
+ }
+ CHECK_FALSE(error);
+}
+
+TEST_CASE("Config callbacks")
+{
+ bool switched = false;
+ Config::connect([&switched]()
+ {
+ switched = !switched;
+ });
+ CHECK_FALSE(switched);
+ Config::set("un", "deux", true);
+ CHECK(switched);
+ Config::set("un", "trois", true);
+ CHECK_FALSE(switched);
+
+ Config::set("un", "trois", false);
+ CHECK_FALSE(switched);
+}
+
+TEST_CASE("Config get_int")
+{
+ auto res = Config::get_int("number", 0);
+ CHECK(res == 0);
+ Config::set("number", "88");
+ res = Config::get_int("number", 0);
+ CHECK(res == 88);
+ Config::set("number", "pouet");
+ res = Config::get_int("number", -1);
+ CHECK(res == 0);
+}
diff --git a/tests/database.cpp b/tests/database.cpp
new file mode 100644
index 0000000..4e2be14
--- /dev/null
+++ b/tests/database.cpp
@@ -0,0 +1,97 @@
+#include "catch.hpp"
+
+#include <database/database.hpp>
+
+#include <config/config.hpp>
+
+TEST_CASE("Database")
+{
+#ifdef USE_DATABASE
+ Database::open(":memory:");
+ Database::set_verbose(false);
+
+ SECTION("Basic retrieve and update")
+ {
+ auto o = Database::get_irc_server_options("zouzou@example.com", "irc.example.com");
+ o.update();
+ auto a = Database::get_irc_server_options("zouzou@example.com", "irc.example.com");
+ auto b = Database::get_irc_server_options("moumou@example.com", "irc.example.com");
+
+ // b does not yet exist in the db, the object is created but not yet
+ // inserted
+ CHECK(1 == Database::count<db::IrcServerOptions>());
+
+ b.update();
+ CHECK(2 == Database::count<db::IrcServerOptions>());
+
+ CHECK(b.pass == "");
+ CHECK(b.pass.value() == "");
+ }
+
+ SECTION("channel options")
+ {
+ Config::set("db_name", ":memory:");
+ auto o = Database::get_irc_channel_options("zouzou@example.com", "irc.example.com", "#foo");
+
+ CHECK(o.encodingIn == "");
+ o.encodingIn = "ISO-8859-1";
+ o.update();
+ auto b = Database::get_irc_channel_options("zouzou@example.com", "irc.example.com", "#foo");
+ CHECK(o.encodingIn == "ISO-8859-1");
+ }
+
+ SECTION("Channel options with server default")
+ {
+ const std::string owner{"zouzou@example.com"};
+ const std::string server{"irc.example.com"};
+ const std::string chan1{"#foo"};
+
+ auto c = Database::get_irc_channel_options(owner, server, chan1);
+ auto s = Database::get_irc_server_options(owner, server);
+
+ GIVEN("An option defined for the channel but not the server")
+ {
+ c.encodingIn = "channelEncoding";
+ c.update();
+ WHEN("we fetch that option")
+ {
+ auto r = Database::get_irc_channel_options_with_server_default(owner, server, chan1);
+ THEN("we get the channel option")
+ CHECK(r.encodingIn == "channelEncoding");
+ }
+ }
+ GIVEN("An option defined for the server but not the channel")
+ {
+ s.encodingIn = "serverEncoding";
+ s.update();
+ WHEN("we fetch that option")
+ {
+ auto r = Database::get_irc_channel_options_with_server_default(owner, server, chan1);
+ THEN("we get the server option")
+ CHECK(r.encodingIn == "serverEncoding");
+ }
+ }
+ GIVEN("An option defined for both the server and the channel")
+ {
+ s.encodingIn = "serverEncoding";
+ s.update();
+ c.encodingIn = "channelEncoding";
+ c.update();
+ WHEN("we fetch that option")
+ {
+ auto r = Database::get_irc_channel_options_with_server_default(owner, server, chan1);
+ THEN("we get the channel option")
+ CHECK(r.encodingIn == "channelEncoding");
+ }
+ WHEN("we fetch that option, with no channel specified")
+ {
+ auto r = Database::get_irc_channel_options_with_server_default(owner, server, "");
+ THEN("we get the server option")
+ CHECK(r.encodingIn == "serverEncoding");
+ }
+ }
+ }
+
+ Database::close();
+#endif
+}
diff --git a/tests/dns.cpp b/tests/dns.cpp
new file mode 100644
index 0000000..c3eda7b
--- /dev/null
+++ b/tests/dns.cpp
@@ -0,0 +1,91 @@
+#include "catch.hpp"
+
+#include <network/dns_handler.hpp>
+#include <network/resolver.hpp>
+#include <network/poller.hpp>
+
+#include <utils/timed_events.hpp>
+
+TEST_CASE("DNS resolver")
+{
+ Resolver resolver;
+ Resolver resolver2;
+ Resolver resolver3;
+
+ /**
+ * If we are using cares, we need to run a poller loop until each
+ * resolution is finished. Without cares we get the result before
+ * resolve() returns because it’s blocking.
+ */
+#ifdef CARES_FOUND
+ auto p = std::make_shared<Poller>();
+
+ const auto loop = [&p]()
+ {
+ do
+ {
+ DNSHandler::instance.watch_dns_sockets(p);
+ }
+ while (p->poll(utils::no_timeout) != -1);
+ };
+#else
+ // We don’t need to do anything if we are not using cares.
+ const auto loop = [](){};
+#endif
+
+ std::string hostname;
+ std::string port = "6667";
+
+ bool success = true;
+
+ const auto error_cb = [&success](const std::string& hostname)
+ {
+ return [&success, hostname](const char *msg)
+ {
+ INFO("Failed to resolve " << hostname << ":" << msg);
+ success = false;
+ };
+ };
+ const auto success_cb = [&success](const std::string& hostname)
+ {
+ return [&success, hostname](const struct addrinfo *addr)
+ {
+ INFO("Successfully resolved " << hostname << ": " << addr_to_string(addr));
+ success = true;
+ };
+ };
+
+ hostname = "example.com";
+ resolver.resolve(hostname, port,
+ success_cb(hostname), error_cb(hostname));
+ hostname = "poez.io";
+ resolver2.resolve(hostname, port,
+ success_cb(hostname), error_cb(hostname));
+ hostname = "louiz.org";
+ resolver3.resolve(hostname, port,
+ success_cb(hostname), error_cb(hostname));
+ loop();
+ CHECK(success);
+
+ hostname = "this.should.fail.because.it.is..misformatted";
+ resolver.resolve(hostname, port,
+ success_cb(hostname), error_cb(hostname));
+ loop();
+ CHECK(!success);
+
+ hostname = "this.should.fail.because.it.is.does.not.exist.invalid";
+ resolver.resolve(hostname, port,
+ success_cb(hostname), error_cb(hostname));
+ loop();
+ CHECK(!success);
+
+ hostname = "localhost";
+ resolver.resolve(hostname, port,
+ success_cb(hostname), error_cb(hostname));
+ loop();
+ CHECK(success);
+
+#ifdef CARES_FOUND
+ DNSHandler::instance.destroy();
+#endif
+}
diff --git a/tests/encoding.cpp b/tests/encoding.cpp
new file mode 100644
index 0000000..389cf23
--- /dev/null
+++ b/tests/encoding.cpp
@@ -0,0 +1,56 @@
+#include "catch.hpp"
+
+#include <utils/encoding.hpp>
+
+
+TEST_CASE("UTF-8 validation")
+{
+ const char* valid = "C̡͔͕̩͙̽ͫ̈́ͥ̿̆ͧ̚r̸̩̘͍̻͖̆͆͛͊̉̕͡o͇͈̳̤̱̊̈͢q̻͍̦̮͕ͥͬͬ̽ͭ͌̾ͅǔ͉͕͇͚̙͉̭͉̇̽ȇ͈̮̼͍͔ͣ͊͞͝ͅ ͫ̾ͪ̓ͥ̆̋̔҉̢̦̠͈͔̖̲̯̦ụ̶̯͐̃̋ͮ͆͝n̬̱̭͇̻̱̰̖̤̏͛̏̿̑͟ë́͐҉̸̥̪͕̹̻̙͉̰ ̹̼̱̦̥ͩ͑̈́͑͝ͅt͍̥͈̹̝ͣ̃̔̈̔ͧ̕͝ḙ̸̖̟̙͙ͪ͢ų̯̞̼̲͓̻̞͛̃̀́b̮̰̗̩̰̊̆͗̾̎̆ͯ͌͝.̗̙͎̦ͫ̈́ͥ͌̈̓ͬ";
+ CHECK(utils::is_valid_utf8(valid));
+ CHECK_FALSE(utils::is_valid_utf8("\xF0\x0F"));
+ CHECK_FALSE(utils::is_valid_utf8("\xFE\xFE\xFF\xFF"));
+
+ std::string in = "Biboumi ╯°□°)╯︵ ┻━┻";
+ INFO(in);
+ CHECK(utils::is_valid_utf8(in.data()));
+}
+
+TEST_CASE("UTF-8 conversion")
+{
+ std::string in = "Biboumi ╯°□°)╯︵ ┻━┻";
+ REQUIRE(utils::is_valid_utf8(in.data()));
+
+ SECTION("Converting UTF-8 to UTF-8 should return the same string")
+ {
+ std::string res = utils::convert_to_utf8(in, "UTF-8");
+ CHECK(utils::is_valid_utf8(res.c_str()) == true);
+ CHECK(res == in);
+ }
+
+ SECTION("Testing latin-1 conversion")
+ {
+ std::string original_utf8("couc¥ou");
+ std::string original_latin1("couc\xa5ou");
+
+ SECTION("Convert proper latin-1 to UTF-8")
+ {
+ std::string from_latin1 = utils::convert_to_utf8(original_latin1.c_str(), "ISO-8859-1");
+ CHECK(from_latin1 == original_utf8);
+ }
+ SECTION("Check the behaviour when the decoding fails (here because we provide a wrong charset)")
+ {
+ std::string from_ascii = utils::convert_to_utf8(original_latin1, "US-ASCII");
+ CHECK(from_ascii == "couc�ou");
+ }
+ }
+}
+
+TEST_CASE("Remove invalid XML chars")
+{
+ std::string without_ctrl_char("𤭢€¢$");
+ std::string in = "Biboumi ╯°□°)╯︵ ┻━┻";
+ INFO(in);
+ CHECK(utils::remove_invalid_xml_chars(without_ctrl_char) == without_ctrl_char);
+ CHECK(utils::remove_invalid_xml_chars(in) == in);
+ CHECK(utils::remove_invalid_xml_chars("\acouco\u0008u\uFFFEt\uFFFFe\r\n♥") == "coucoute\r\n♥");
+}
diff --git a/tests/end_to_end/__main__.py b/tests/end_to_end/__main__.py
new file mode 100644
index 0000000..4348197
--- /dev/null
+++ b/tests/end_to_end/__main__.py
@@ -0,0 +1,1027 @@
+#!/usr/bin/env python3
+
+import collections
+import slixmpp
+import asyncio
+import logging
+import signal
+import atexit
+import lxml.etree
+import sys
+import io
+import os
+from functools import partial
+from slixmpp.xmlstream.matcher.base import MatcherBase
+
+
+class MatchAll(MatcherBase):
+ """match everything"""
+
+ def match(self, xml):
+ return True
+
+
+class StanzaError(Exception):
+ """
+ Raised when a step fails.
+ """
+ pass
+
+
+class SkipStepError(Exception):
+ """
+ Raised by a step when it needs to be skiped, by running
+ the next available step immediately.
+ """
+ pass
+
+
+class XMPPComponent(slixmpp.BaseXMPP):
+ """
+ XMPPComponent sending a “scenario” of stanzas, checking that the responses
+ match the expected results.
+ """
+
+ def __init__(self, scenario, biboumi):
+ super().__init__(jid="biboumi.localhost", default_ns="jabber:component:accept")
+ self.is_component = True
+ self.stream_header = '<stream:stream %s %s from="%s" id="%s">' % (
+ 'xmlns="jabber:component:accept"',
+ 'xmlns:stream="%s"' % self.stream_ns,
+ self.boundjid, self.get_id())
+ self.stream_footer = "</stream:stream>"
+
+ self.register_handler(slixmpp.Callback('Match All',
+ MatchAll(None),
+ self.handle_incoming_stanza))
+
+ self.add_event_handler("session_end", self.on_end_session)
+
+ asyncio.async(self.accept_routine())
+
+ self.scenario = scenario
+ self.biboumi = biboumi
+ # A callable, taking a stanza as argument and raising a StanzaError
+ # exception if the test should fail.
+ self.stanza_checker = None
+ self.failed = False
+ self.accepting_server = None
+
+ self.saved_values = {}
+
+ def error(self, message):
+ print("Failure: %s" % (message,))
+ self.scenario.steps = []
+ self.failed = True
+
+ def on_end_session(self, event):
+ self.loop.stop()
+
+ def handle_incoming_stanza(self, stanza):
+ if self.stanza_checker:
+ try:
+ self.stanza_checker(stanza)
+ except StanzaError as e:
+ self.error(e)
+ except SkipStepError:
+ # Run the next step and then re-handle this same stanza
+ self.run_scenario()
+ return self.handle_incoming_stanza(stanza)
+ self.stanza_checker = None
+ self.run_scenario()
+
+ def run_scenario(self):
+ if scenario.steps:
+ step = scenario.steps.pop(0)
+ step(self, self.biboumi)
+ else:
+ self.biboumi.stop()
+
+ @asyncio.coroutine
+ def accept_routine(self):
+ self.accepting_server = yield from self.loop.create_server(lambda: self,
+ "127.0.0.1", "8811", reuse_address=True)
+
+ def check_stanza_against_all_expected_xpaths(self):
+ pass
+
+
+def match(stanza, xpath):
+ tree = lxml.etree.parse(io.StringIO(str(stanza)))
+ matched = tree.xpath(xpath, namespaces={'re': 'http://exslt.org/regular-expressions',
+ 'muc_user': 'http://jabber.org/protocol/muc#user',
+ 'disco_items': 'http://jabber.org/protocol/disco#items',
+ 'commands': 'http://jabber.org/protocol/commands',
+ 'dataform': 'jabber:x:data',
+ 'version': 'jabber:iq:version'})
+ return matched
+
+
+def check_xpath(xpaths, xmpp, after, stanza):
+ for i, xpath in enumerate(xpaths):
+ matched = match(stanza, xpath)
+ if not matched:
+ raise StanzaError("Received stanza “%s” did not match expected xpath “%s”" % (stanza, xpath))
+ if after:
+ if isinstance(after, collections.Iterable):
+ for af in after:
+ af(stanza, xmpp)
+ else:
+ after(stanza, xmpp)
+
+
+def check_xpath_optional(xpaths, xmpp, after, stanza):
+ try:
+ check_xpath(xpaths, xmpp, after, stanza)
+ except StanzaError:
+ raise SkipStepError()
+
+
+class Scenario:
+ """Defines a list of actions that are executed in sequence, until one of
+ them throws an exception, or until the end. An action can be something
+ like “send a stanza”, “receive the next stanza and check that it matches
+ the given XPath”, “send a signal”, “wait for the end of the process”,
+ etc
+ """
+
+ def __init__(self, name, steps, conf="basic"):
+ """
+ Steps is a list of 2-tuple:
+ [(action, answer), (action, answer)]
+ """
+ self.name = name
+ self.steps = []
+ self.conf = conf
+ for elem in steps:
+ if isinstance(elem, collections.Iterable):
+ for step in elem:
+ self.steps.append(step)
+ else:
+ self.steps.append(elem)
+
+
+class ProcessRunner:
+ def __init__(self):
+ self.process = None
+ self.signal_sent = False
+ self.create = None
+
+ @asyncio.coroutine
+ def start(self):
+ self.process = yield from self.create
+
+ @asyncio.coroutine
+ def wait(self):
+ code = yield from self.process.wait()
+ return code
+
+ def stop(self):
+ if not self.signal_sent:
+ self.signal_sent = True
+ if self.process:
+ self.process.send_signal(signal.SIGINT)
+
+ def __del__(self):
+ self.stop()
+
+
+class BiboumiRunner(ProcessRunner):
+ def __init__(self, name, with_valgrind):
+ super().__init__()
+ self.name = name
+ self.fd = open("biboumi_%s_output.txt" % (name,), "w")
+ if with_valgrind:
+ 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)
+ else:
+ self.create = asyncio.create_subprocess_exec("./biboumi", "test.conf", stdin=None, stdout=self.fd,
+ stderr=self.fd, loop=None, limit=None)
+
+
+class IrcServerRunner(ProcessRunner):
+ def __init__(self):
+ super().__init__()
+ self.create = asyncio.create_subprocess_exec("charybdis", "-foreground", "-configfile", os.getcwd() + "/../tests/end_to_end/ircd.conf",
+ stderr=asyncio.subprocess.PIPE)
+
+
+def send_stanza(stanza, xmpp, biboumi):
+ replacements = common_replacements
+ replacements.update(xmpp.saved_values)
+ xmpp.send_raw(stanza.format_map(replacements))
+ asyncio.get_event_loop().call_soon(xmpp.run_scenario)
+
+
+def expect_stanza(xpaths, xmpp, biboumi, optional=False, after=None):
+ check_func = check_xpath if not optional else check_xpath_optional
+ if isinstance(xpaths, str):
+ xmpp.stanza_checker = partial(check_func, [xpaths.format_map(common_replacements)], xmpp, after)
+ elif isinstance(xpaths, tuple):
+ xmpp.stanza_checker = partial(check_func, [xpath.format_map(common_replacements) for xpath in xpaths], xmpp, after)
+ else:
+ print("Warning, from argument type passed to expect_stanza: %s" % (type(xpaths)))
+
+
+def log_message(message, xmpp, biboumi):
+ print("%s" % (message,))
+ asyncio.get_event_loop().call_soon(xmpp.run_scenario)
+
+
+class BiboumiTest:
+ """
+ Spawns a biboumi process and a fake XMPP Component that will run a
+ Scenario. It redirects the outputs of the subprocess into separated
+ files, and detects any failure in the running of the scenario.
+ """
+
+ def __init__(self, scenario, expected_code=0):
+ self.scenario = scenario
+ self.expected_code = expected_code
+
+ def run(self, with_valgrind=True):
+ print("Running scenario: %s%s" % (self.scenario.name, " (with valgrind)" if with_valgrind else ''))
+ # Redirect the slixmpp logging into a specific file
+ output_filename = "slixmpp_%s_output.txt" % (self.scenario.name,)
+ with open(output_filename, "w"):
+ pass
+ logging.basicConfig(level=logging.DEBUG,
+ format='%(levelname)-8s %(message)s',
+ filename=output_filename)
+
+ with open("test.conf", "w") as fd:
+ fd.write(confs[scenario.conf])
+
+ # Start the XMPP component and biboumi
+ biboumi = BiboumiRunner(scenario.name, with_valgrind)
+ xmpp = XMPPComponent(self.scenario, biboumi)
+ asyncio.get_event_loop().run_until_complete(biboumi.start())
+
+ asyncio.get_event_loop().call_soon(xmpp.run_scenario)
+
+ xmpp.process()
+ code = asyncio.get_event_loop().run_until_complete(biboumi.wait())
+ xmpp.biboumi = None
+ scenario.steps.clear()
+ failed = False
+ if not xmpp.failed:
+ if code != self.expected_code:
+ xmpp.error("Wrong return code from biboumi's process: %d" % (code,))
+ failed = True
+ else:
+ print("Success!")
+ else:
+ failed = True
+
+ xmpp.saved_values.clear()
+
+ if xmpp.server:
+ xmpp.accepting_server.close()
+
+ return not failed
+
+
+confs = {
+'basic':
+"""hostname=biboumi.localhost
+password=coucou
+db_name=e2e_test.sqlite
+port=8811
+admin=admin@example.com""",
+
+'fixed_server':
+"""hostname=biboumi.localhost
+password=coucou
+db_name=e2e_test.sqlite
+port=8811
+fixed_irc_server=irc.localhost
+admin=admin@example.com
+"""}
+
+common_replacements = {
+ 'irc_server_one': 'irc.localhost@biboumi.localhost',
+ 'irc_host_one': 'irc.localhost',
+ 'biboumi_host': 'biboumi.localhost',
+ 'resource_one': 'resource1',
+ 'resource_two': 'resource2',
+ 'nick_one': 'Nick',
+ 'jid_one': 'first@example.com',
+ 'jid_two': 'second@example.com',
+ 'jid_admin': 'admin@example.com',
+ 'nick_two': 'Bobby',
+ 'lower_nick_one': 'nick',
+ 'lower_nick_two': 'bobby',
+}
+
+
+def handshake_sequence():
+ return (partial(expect_stanza, "//handshake"),
+ partial(send_stanza, "<handshake xmlns='jabber:component:accept'/>"))
+
+
+def connection_begin_sequence(irc_host, jid):
+ jid = jid.format_map(common_replacements)
+ xpath = "/message[@to='" + jid + "'][@from='irc.localhost@biboumi.localhost']/body[text()='%s']"
+ xpath_re = "/message[@to='" + jid + "'][@from='irc.localhost@biboumi.localhost']/body[re:test(text(), '%s')]"
+ return (
+ partial(expect_stanza,
+ xpath % ('Connecting to %s:6697 (encrypted)' % irc_host)),
+ partial(expect_stanza,
+ xpath % 'Connection failed: Connection refused'),
+ partial(expect_stanza,
+ xpath % ('Connecting to %s:6670 (encrypted)' % irc_host)),
+ partial(expect_stanza,
+ xpath % 'Connection failed: Connection refused'),
+ partial(expect_stanza,
+ xpath % ('Connecting to %s:6667 (not encrypted)' % irc_host)),
+ partial(expect_stanza,
+ xpath % 'Connected to IRC server.'),
+ # These two messages can be receive in any order
+ partial(expect_stanza,
+ xpath_re % (r'^%s: \*\*\* (Checking Ident|Looking up your hostname...)$' % irc_host)),
+ partial(expect_stanza,
+ xpath_re % (r'^%s: \*\*\* (Checking Ident|Looking up your hostname...)$' % irc_host)),
+ # These three messages can be received in any order
+ partial(expect_stanza,
+ xpath_re % (r'^%s: (\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* No Ident response)$' % irc_host)),
+ partial(expect_stanza,
+ xpath_re % (r'^%s: (\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* No Ident response)$' % irc_host)),
+ partial(expect_stanza,
+ xpath_re % (r'^%s: (\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* No Ident response)$' % irc_host)),
+ )
+
+
+def connection_end_sequence(irc_host, jid):
+ jid = jid.format_map(common_replacements)
+ xpath = "/message[@to='" + jid + "'][@from='irc.localhost@biboumi.localhost']/body[text()='%s']"
+ xpath_re = "/message[@to='" + jid + "'][@from='irc.localhost@biboumi.localhost']/body[re:test(text(), '%s')]"
+ return (
+ partial(expect_stanza,
+ xpath_re % (r'^%s: Your host is .*$' % irc_host)),
+ partial(expect_stanza,
+ xpath_re % (r'^%s: This server was created .*$' % irc_host)),
+ partial(expect_stanza,
+ xpath_re % (r'^%s: There are \d+ users and \d+ invisible on \d+ servers$' % irc_host)),
+ partial(expect_stanza,
+ xpath_re % (r'^%s: \d+ unknown connection\(s\)$' % irc_host), optional=True),
+ partial(expect_stanza,
+ xpath_re % (r'^%s: \d+ channels formed$' % irc_host), optional=True),
+ partial(expect_stanza,
+ xpath_re % (r'^%s: I have \d+ clients and \d+ servers$' % irc_host)),
+ partial(expect_stanza,
+ xpath_re % (r'^%s: \d+ \d+ Current local users \d+, max \d+$' % irc_host)),
+ partial(expect_stanza,
+ xpath_re % (r'^%s: \d+ \d+ Current global users \d+, max \d+$' % irc_host)),
+ partial(expect_stanza,
+ xpath_re % (r'^%s: Highest connection count: \d+ \(\d+ clients\) \(\d+ connections received\)$' % irc_host)),
+ partial(expect_stanza,
+ xpath % "- This is charybdis MOTD you might replace it, but if not your friends will\n- laugh at you.\n"),
+ partial(expect_stanza,
+ xpath_re % r'^User mode for \w+ is \[\+i\]$'),
+ )
+
+
+def connection_sequence(irc_host, jid):
+ return connection_begin_sequence(irc_host, jid) + connection_end_sequence(irc_host, jid)
+
+
+def extract_attribute(xpath, name, stanza):
+ matched = match(stanza, xpath)
+ return matched[0].get(name)
+
+
+def save_value(name, func, stanza, xmpp):
+ xmpp.saved_values[name] = func(stanza)
+
+
+if __name__ == '__main__':
+
+ atexit.register(asyncio.get_event_loop().close)
+
+ # Start the test component, accepting connections on the configured
+ # port.
+ scenarios = (
+ Scenario("basic_handshake_success",
+ [
+ handshake_sequence()
+ ]),
+ Scenario("irc_server_connection",
+ [
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ ]),
+ Scenario("simple_channel_join",
+ [
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+ ]),
+ Scenario("virtual_channel_join",
+ [
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
+ connection_begin_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='%{irc_server_one}'][@type='groupchat']/subject[re:test(text(), '^This is a virtual channel.*$')]"),
+ connection_end_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ ]),
+ Scenario("channel_join_with_two_users",
+ [
+ handshake_sequence(),
+ # First user joins
+ partial(log_message,
+ "First user joins"),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # Second user joins
+ partial(log_message,
+ "Second user joins"),
+ partial(send_stanza,
+ "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ connection_sequence("irc.localhost", '{jid_two}/{resource_one}'),
+ # Our presence, sent to the other user
+ partial(log_message,
+ "Our presence sent to the other user"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']",)),
+ # The other user presence
+ partial(log_message,
+ "The other user presence"),
+ partial(expect_stanza,
+ "/presence[@to='{jid_second}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~nick@localhost'][@role='participant']"),
+ # Our own presence
+ partial(log_message,
+ "Our own presence"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+ ]),
+ Scenario("channel_custom_topic",
+ [
+ handshake_sequence(),
+ # First user joins
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # First user sets the topic
+ partial(send_stanza,
+ "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><subject>TOPIC TEST</subject></message>"),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[text()='TOPIC TEST']"),
+
+ # Second user joins
+ partial(send_stanza,
+ "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ connection_sequence("irc.localhost", '{jid_two}/{resource_one}'),
+ # Our presence, sent to the other user
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']",)),
+ # The other user presence
+ partial(expect_stanza,
+ "/presence[@to='{jid_second}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']"),
+ # Our own presence
+ partial(expect_stanza,
+ ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/subject[text()='TOPIC TEST']"),
+ ]),
+ Scenario("channel_basic_join_on_fixed_irc_server",
+ [
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#zgeg@{biboumi_host}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #zgeg [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#zgeg@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#zgeg@{biboumi_host}'][@type='groupchat']/subject[not(text())]"),
+ ], conf='fixed_server'
+ ),
+ Scenario("list_adhoc",
+ [
+ handshake_sequence(),
+ partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_one}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
+ partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
+ "/iq/disco_items:query/disco_items:item[3]")),
+ ]),
+ Scenario("list_admin_adhoc",
+ [
+ handshake_sequence(),
+ partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
+ partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
+ "/iq/disco_items:query/disco_items:item[5]")),
+ ]),
+ Scenario("list_adhoc_fixed_server",
+ [
+ handshake_sequence(),
+ partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_one}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
+ partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
+ "/iq/disco_items:query/disco_items:item[3]")),
+ ], conf='fixed_server'),
+ Scenario("list_admin_adhoc_fixed_server",
+ [
+ handshake_sequence(),
+ partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
+ partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
+ "/iq/disco_items:query/disco_items:item[5]")),
+ ], conf='fixed_server'),
+
+
+ Scenario("list_adhoc_irc",
+ [
+ handshake_sequence(),
+ partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_one}/{resource_one}' to='{irc_host_one}@{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
+ partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
+ "/iq/disco_items:query/disco_items:item[1]")),
+ ]),
+ Scenario("list_adhoc_irc_fixed_server",
+ [
+ handshake_sequence(),
+ partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_one}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
+ partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
+ "/iq/disco_items:query/disco_items:item[4]")),
+ ], conf='fixed_server'),
+ Scenario("list_admin_adhoc_irc_fixed_server",
+ [
+ handshake_sequence(),
+ partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
+ partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
+ "/iq/disco_items:query/disco_items:item[6]")),
+ ], conf='fixed_server'),
+
+ Scenario("execute_hello_adhoc_command",
+ [
+ handshake_sequence(),
+ partial(send_stanza, "<iq type='set' id='hello-command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' action='execute' /></iq>"),
+ partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='hello'][@sessionid][@status='executing']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure your name.']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Please provide your name.']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single']/dataform:required",
+ "/iq/commands:command/commands:actions/commands:next",
+ ),
+ after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='hello']", "sessionid"))
+
+ ),
+ partial(send_stanza, "<iq type='set' id='hello-command2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='name'><value>COUCOU</value></field></x></command></iq>"),
+ partial(expect_stanza, "/iq[@type='result']/commands:command[@node='hello'][@status='completed']/commands:note[@type='info'][text()='Hello COUCOU!']")
+ ]),
+ Scenario("multisessionnick",
+ [
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[not(text())]"),
+
+ # The other resources joins the same room, with the same nick
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ # We receive our own join
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"),
+
+ # A different user joins the same room
+ partial(send_stanza,
+ "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ connection_sequence("irc.localhost", '{jid_two}/{resource_one}'),
+
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']",)),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_two}']",)),
+
+ partial(expect_stanza,
+ "/presence[@to='{jid_second}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # That second user sends a private message to the first one
+ partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='chat'><body>RELLO</body></message>"),
+ # Message is received with a server-wide JID, by the two resources behind nick_one
+ partial(expect_stanza, "/message[@from='{lower_nick_two}!{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='RELLO']"),
+ partial(expect_stanza, "/message[@from='{lower_nick_two}!{irc_server_one}'][@to='{jid_one}/{resource_two}'][@type='chat']/body[text()='RELLO']"),
+
+ # One resource leaves the server entirely.
+ partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ # The leave is forwarded only to us
+ partial(expect_stanza,
+ ("/presence[@type='unavailable']/muc_user:x/muc_user:status[@code='110']",
+ "/presence/status[text()='Biboumi note: 1 resources are still in this channel.']")
+ ),
+ # The second user sends two new private messages to the first user
+ partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='chat'><body>first</body></message>"),
+ partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='chat'><body>second</body></message>"),
+ # The first user receives the two messages, on the connected resource, once each
+ partial(expect_stanza, "/message[@from='{lower_nick_two}!{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='first']"),
+ partial(expect_stanza, "/message[@from='{lower_nick_two}!{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='second']"),
+
+
+ ]),
+ Scenario("channel_messages",
+ [
+ handshake_sequence(),
+ # First user joins
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # Second user joins
+ partial(send_stanza,
+ "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ connection_sequence("irc.localhost", '{jid_two}/{resource_one}'),
+ # Our presence, sent to the other user
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']",)),
+ # The other user presence
+ partial(expect_stanza,
+ "/presence[@to='{jid_second}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~nick@localhost'][@role='participant']"),
+ # Our own presence
+ partial(expect_stanza,
+ ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # Send a channel message
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"),
+ # Receive the message, forwarded to the two users
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']"),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='coucou']"),
+
+ # Send a private message, to a in-room JID
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' type='chat'><body>coucou in private</body></message>"),
+ # Message is received with a server-wide JID
+ partial(expect_stanza, "/message[@from='{lower_nick_one}!{irc_server_one}'][@to='{jid_two}/{resource_one}'][@type='chat']/body[text()='coucou in private']"),
+
+ # Respond to the message, to the server-wide JID
+ partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}!{irc_server_one}' type='chat'><body>yes</body></message>"),
+ # The response is received from the in-room JID
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='yes']"),
+
+ ## Do the exact same thing, from a different chan,
+ # to check if the response comes from the right JID
+
+ # Join the virtual channel
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
+ partial(expect_stanza,
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ partial(expect_stanza, "/message[@from='%{irc_server_one}'][@type='groupchat']/subject"),
+
+
+ # Send a private message, to a in-room JID
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' type='chat'><body>re in private</body></message>"),
+ # Message is received with a server-wide JID
+ partial(expect_stanza, "/message[@from='{lower_nick_one}!{irc_server_one}'][@to='{jid_two}/{resource_one}'][@type='chat']/body[text()='re in private']"),
+
+ # Respond to the message, to the server-wide JID
+ partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}!{irc_server_one}' type='chat'><body>re</body></message>"),
+ # The response is received from the in-room JID
+ partial(expect_stanza, "/message[@from='%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='re']"),
+
+ # Now we leave the room, to check if the subsequent private messages are still received properly
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ partial(expect_stanza,
+ "/presence[@type='unavailable']/muc_user:x/muc_user:status[@code='110']"),
+
+ # The private messages from this nick should now come (again) from the server-wide JID
+ partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}!{irc_server_one}' type='chat'><body>hihihoho</body></message>"),
+ partial(expect_stanza,
+ "/message[@from='{lower_nick_two}!{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+ ]
+ ),
+ Scenario("encoded_channel_join",
+ [
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#biboumi\\40louiz.org\\3a80%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #biboumi@louiz.org:80 [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#biboumi\\40louiz.org\\3a80%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#biboumi\\40louiz.org\\3a80%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+ ]),
+ Scenario("self_ping_on_real_channel",
+ [
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # Send a ping to ourself
+ partial(send_stanza,
+ "<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ # We receive our own ping request,
+ partial(expect_stanza,
+ "/iq[@from='{lower_nick_one}!{irc_server_one}'][@type='get'][@to='{jid_one}/{resource_one}'][@id='gnip_tsrif']"),
+ # Respond to the request
+ partial(send_stanza,
+ "<iq type='result' to='{lower_nick_one}!{irc_server_one}' id='gnip_tsrif' from='{jid_one}/{resource_one}'/>"),
+ partial(expect_stanza,
+ "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"),
+
+ # Now join the same room, from the same bare JID, behind the same nick
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"),
+
+ # And re-send a self ping
+ partial(send_stanza,
+ "<iq type='get' from='{jid_one}/{resource_one}' id='second_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ # We receive our own ping request. Note that we don't know the to value, it could be one of our two resources.
+ partial(expect_stanza,
+ "/iq[@from='{lower_nick_one}!{irc_server_one}'][@type='get'][@to][@id='gnip_dnoces']",
+ after = partial(save_value, "to", partial(extract_attribute, "/iq", "to"))),
+ # Respond to the request, using the extracted 'to' value as our 'from'
+ partial(send_stanza,
+ "<iq type='result' to='{lower_nick_one}!{irc_server_one}' id='gnip_dnoces' from='{to}'/>"),
+ partial(expect_stanza,
+ "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='second_ping']"),
+ ## And re-do exactly the same thing, just change the resource initiating the self ping
+ partial(send_stanza,
+ "<iq type='get' from='{jid_one}/{resource_two}' id='third_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ partial(expect_stanza,
+ "/iq[@from='{lower_nick_one}!{irc_server_one}'][@type='get'][@to][@id='gnip_driht']",
+ after = partial(save_value, "to", partial(extract_attribute, "/iq", "to"))),
+ partial(send_stanza,
+ "<iq type='result' to='{lower_nick_one}!{irc_server_one}' id='gnip_driht' from='{to}'/>"),
+ partial(expect_stanza,
+ "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_two}'][@id='third_ping']"),
+
+ ]),
+ Scenario("simple_kick",
+ [
+ handshake_sequence(),
+ # First user joins
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza, "/message"),
+ partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
+ partial(expect_stanza, "/message[@type='groupchat']/subject"),
+
+ # Second user joins
+ partial(send_stanza,
+ "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ connection_sequence("irc.localhost", '{jid_two}/{resource_one}'),
+ partial(expect_stanza,
+ "/presence/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",),
+
+ partial(expect_stanza,
+ "/presence/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']"),
+ partial(expect_stanza,
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ partial(expect_stanza, "/message/subject"),
+
+ # Moderator kicks participant
+ partial(log_message, "Moderator kicks participant"),
+ partial(send_stanza,
+ "<iq id='kick1' to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item nick='{nick_two}' role='none'><reason>reported</reason></item></query></iq>"),
+ partial(log_message, "Presence is sent to everyone"),
+ partial(expect_stanza,
+ ("/presence[@type='unavailable'][@to='{jid_second}/{resource_one}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']",
+ "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']",
+ "/presence/muc_user:x/muc_user:status[@code='307']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"
+ )),
+ partial(expect_stanza,
+ ("/presence[@type='unavailable']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']",
+ "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']",
+ "/presence/muc_user:x/muc_user:status[@code='307']",
+ ),
+ ),
+ partial(expect_stanza,
+ "/iq[@id='kick1'][@type='result']"),
+ ]),
+ Scenario("multisession_kick",
+ [
+ handshake_sequence(),
+ # First user joins
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza, "/message"),
+ partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
+ partial(expect_stanza, "/message[@type='groupchat']/subject"),
+
+ # Second user joins, from two resources
+ partial(send_stanza,
+ "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ connection_sequence("irc.localhost", '{jid_two}/{resource_one}'),
+ partial(expect_stanza,
+ "/presence/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",),
+
+ partial(expect_stanza,
+ "/presence/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']"),
+ partial(expect_stanza,
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ partial(expect_stanza, "/message/subject"),
+
+ partial(send_stanza,
+ "<presence from='{jid_two}/{resource_two}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ partial(expect_stanza,
+ "/presence[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_two}']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_two}/{resource_two}']/subject[not(text())]"),
+
+ # Moderator kicks participant
+ partial(log_message, "Moderator kicks participant"),
+ partial(send_stanza,
+ "<iq id='kick1' to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item nick='{nick_two}' role='none'><reason>reported</reason></item></query></iq>"),
+ partial(log_message, "Unavailable presence is sent to the two resources"),
+ partial(expect_stanza,
+ ("/presence[@type='unavailable'][@to='{jid_second}/{resource_one}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']",
+ "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']",
+ "/presence/muc_user:x/muc_user:status[@code='307']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"
+ )),
+ partial(expect_stanza,
+ ("/presence[@type='unavailable'][@to='{jid_second}/{resource_two}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']",
+ "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']",
+ "/presence/muc_user:x/muc_user:status[@code='307']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"
+ )),
+ partial(expect_stanza,
+ ("/presence[@type='unavailable']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']",
+ "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']",
+ "/presence/muc_user:x/muc_user:status[@code='307']",
+ ),
+ ),
+ partial(expect_stanza,
+ "/iq[@id='kick1'][@type='result']"),
+ ]),
+ Scenario("self_version",
+ [
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # Send a version request to ourself
+ partial(send_stanza,
+ "<iq type='get' from='{jid_one}/{resource_one}' id='first_version' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='jabber:iq:version' /></iq>"),
+ # We receive our own request,
+ partial(expect_stanza,
+ "/iq[@from='{lower_nick_one}!{irc_server_one}'][@type='get'][@to='{jid_one}/{resource_one}']",
+ after = partial(save_value, "id", partial(extract_attribute, "/iq", 'id'))),
+ # Respond to the request
+ partial(send_stanza,
+ "<iq type='result' to='{lower_nick_one}!{irc_server_one}' id='{id}' from='{jid_one}/{resource_one}'><query xmlns='jabber:iq:version'><name>e2e test</name><version>1.0</version><os>Fedora</os></query></iq>"),
+ partial(expect_stanza,
+ "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_version']/version:query/version:name[text()='e2e test (through the biboumi gateway) 1.0 Fedora']"),
+
+ # Now join the same room, from the same bare JID, behind the same nick
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"),
+
+ # And re-send a self ping
+ partial(send_stanza,
+ "<iq type='get' from='{jid_one}/{resource_two}' id='second_version' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='jabber:iq:version' /></iq>"),
+ # We receive our own request. Note that we don't know the to value, it could be one of our two resources.
+ partial(expect_stanza,
+ "/iq[@from='{lower_nick_one}!{irc_server_one}'][@type='get'][@to]",
+ after = (partial(save_value, "to", partial(extract_attribute, "/iq", "to")),
+ partial(save_value, "id", partial(extract_attribute, "/iq", "id")))),
+ # Respond to the request, using the extracted 'to' value as our 'from'
+ partial(send_stanza,
+ "<iq type='result' to='{lower_nick_one}!{irc_server_one}' id='{id}' from='{to}'><query xmlns='jabber:iq:version'><name>e2e test</name><version>1.0</version><os>Fedora</os></query></iq>"),
+ partial(expect_stanza,
+ "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_two}'][@id='second_version']"),
+
+ # And do exactly the same thing, but initiated by the other resource
+ partial(send_stanza,
+ "<iq type='get' from='{jid_one}/{resource_one}' id='second_version' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='jabber:iq:version' /></iq>"),
+ # We receive our own request. Note that we don't know the to value, it could be one of our two resources.
+ partial(expect_stanza,
+ "/iq[@from='{lower_nick_one}!{irc_server_one}'][@type='get'][@to]",
+ after = (partial(save_value, "to", partial(extract_attribute, "/iq", "to")),
+ partial(save_value, "id", partial(extract_attribute, "/iq", "id")))),
+ # Respond to the request, using the extracted 'to' value as our 'from'
+ partial(send_stanza,
+ "<iq type='result' to='{lower_nick_one}!{irc_server_one}' id='{id}' from='{to}'><query xmlns='jabber:iq:version'><name>e2e test</name><version>1.0</version><os>Fedora</os></query></iq>"),
+ partial(expect_stanza,
+ "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='second_version']"),
+ ]),
+ )
+
+ failures = 0
+
+ irc_output = open("irc_output.txt", "w")
+ irc = IrcServerRunner()
+ print("Starting irc server…")
+ asyncio.get_event_loop().run_until_complete(irc.start())
+ while True:
+ res = asyncio.get_event_loop().run_until_complete(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…")
+ sys.exit(1)
+ if b"now running in foreground mode" in res:
+ break
+ print("irc server started.")
+ print("Running %s checks for biboumi." % (len(scenarios)))
+
+ for scenario in scenarios:
+ test = BiboumiTest(scenario)
+ if not test.run(os.getenv("E2E_BIBOUMI_VALGRIND") is not None):
+ print("You can check the files slixmpp_%s_output.txt and biboumi_%s_output.txt to help you debug." %
+ (scenario.name, scenario.name))
+ failures += 1
+
+ print("Waiting for irc server to exit…")
+ irc.stop()
+ asyncio.get_event_loop().run_until_complete(irc.wait())
+
+ if failures:
+ print("%d test%s failed, please fix %s." % (failures, 's' if failures > 1 else '',
+ 'them' if failures > 1 else 'it'))
+ sys.exit(1)
+ else:
+ print("All tests passed successfully")
diff --git a/tests/end_to_end/biboumi.supp b/tests/end_to_end/biboumi.supp
new file mode 100644
index 0000000..d153665
--- /dev/null
+++ b/tests/end_to_end/biboumi.supp
@@ -0,0 +1,10 @@
+{
+ stdlibc++ thingy
+ Memcheck:Leak
+ match-leak-kinds: reachable
+ fun:malloc
+ ...
+ fun:call_init.part.0
+ fun:_dl_init
+ ...
+}
diff --git a/tests/end_to_end/ircd.conf b/tests/end_to_end/ircd.conf
new file mode 100644
index 0000000..7edb3a8
--- /dev/null
+++ b/tests/end_to_end/ircd.conf
@@ -0,0 +1,510 @@
+/* doc/ircd.conf.example - brief example configuration file
+ *
+ * Copyright (C) 2000-2002 Hybrid Development Team
+ * Copyright (C) 2002-2005 ircd-ratbox development team
+ * Copyright (C) 2005-2006 charybdis development team
+ *
+ * See reference.conf for more information.
+ */
+
+/* Extensions */
+#loadmodule "extensions/chm_operonly_compat";
+#loadmodule "extensions/chm_quietunreg_compat";
+#loadmodule "extensions/chm_sslonly_compat";
+#loadmodule "extensions/chm_operpeace";
+#loadmodule "extensions/createauthonly";
+#loadmodule "extensions/extb_account";
+#loadmodule "extensions/extb_canjoin";
+#loadmodule "extensions/extb_channel";
+#loadmodule "extensions/extb_combi";
+#loadmodule "extensions/extb_extgecos";
+#loadmodule "extensions/extb_hostmask";
+#loadmodule "extensions/extb_oper";
+#loadmodule "extensions/extb_realname";
+#loadmodule "extensions/extb_server";
+#loadmodule "extensions/extb_ssl";
+#loadmodule "extensions/extb_usermode";
+#loadmodule "extensions/hurt";
+#loadmodule "extensions/m_extendchans";
+#loadmodule "extensions/m_findforwards";
+#loadmodule "extensions/m_identify";
+#loadmodule "extensions/m_locops";
+#loadmodule "extensions/no_oper_invis";
+#loadmodule "extensions/sno_farconnect";
+#loadmodule "extensions/sno_globalkline";
+#loadmodule "extensions/sno_globalnickchange";
+#loadmodule "extensions/sno_globaloper";
+#loadmodule "extensions/sno_whois";
+#loadmodule "extensions/override";
+#loadmodule "extensions/no_kill_services";
+
+/*
+ * IP cloaking extensions: use ip_cloaking_4.0
+ * if you're linking 3.2 and later, otherwise use
+ * ip_cloaking, for compatibility with older 3.x
+ * releases.
+ */
+
+#loadmodule "extensions/ip_cloaking_4.0";
+#loadmodule "extensions/ip_cloaking";
+
+serverinfo {
+ name = "irc.localhost";
+ sid = "42X";
+ description = "charybdis test server";
+ network_name = "StaticBox";
+
+ /* On multi-homed hosts you may need the following. These define
+ * the addresses we connect from to other servers. */
+ /* for IPv4 */
+ #vhost = "192.0.2.6";
+ /* for IPv6 */
+ #vhost6 = "2001:db8:2::6";
+
+ /* ssl_private_key: our ssl private key */
+ ssl_private_key = "etc/ssl.key";
+
+ /* ssl_cert: certificate for our ssl server */
+ ssl_cert = "etc/ssl.pem";
+
+ /* ssl_dh_params: DH parameters, generate with openssl dhparam -out dh.pem 2048
+ * In general, the DH parameters size should be the same as your key's size.
+ * However it has been reported that some clients have broken TLS implementations which may
+ * choke on keysizes larger than 2048-bit, so we would recommend using 2048-bit DH parameters
+ * for now if your keys are larger than 2048-bit.
+ */
+ ssl_dh_params = "etc/dh.pem";
+
+ /* ssld_count: number of ssld processes you want to start, if you
+ * have a really busy server, using N-1 where N is the number of
+ * cpu/cpu cores you have might be useful. A number greater than one
+ * can also be useful in case of bugs in ssld and because ssld needs
+ * two file descriptors per SSL connection.
+ */
+ ssld_count = 1;
+
+ /* default max clients: the default maximum number of clients
+ * allowed to connect. This can be changed once ircd has started by
+ * issuing:
+ * /quote set maxclients <limit>
+ */
+ default_max_clients = 1024;
+
+ /* nicklen: enforced nickname length (for this server only; must not
+ * be longer than the maximum length set while building).
+ */
+ nicklen = 30;
+};
+
+admin {
+ name = "Lazy admin (lazya)";
+ description = "StaticBox client server";
+ email = "nobody@127.0.0.1";
+};
+
+log {
+ fname_userlog = "logs/userlog";
+ #fname_fuserlog = "logs/fuserlog";
+ fname_operlog = "logs/operlog";
+ #fname_foperlog = "logs/foperlog";
+ fname_serverlog = "logs/serverlog";
+ #fname_klinelog = "logs/klinelog";
+ fname_killlog = "logs/killlog";
+ fname_operspylog = "logs/operspylog";
+ #fname_ioerrorlog = "logs/ioerror";
+};
+
+/* class {} blocks MUST be specified before anything that uses them. That
+ * means they must be defined before auth {} and before connect {}.
+ */
+class "users" {
+ ping_time = 2 minutes;
+ number_per_ident = 10;
+ number_per_ip = 10;
+ number_per_ip_global = 50;
+ cidr_ipv4_bitlen = 24;
+ cidr_ipv6_bitlen = 64;
+ number_per_cidr = 200;
+ max_number = 3000;
+ sendq = 400 kbytes;
+};
+
+class "opers" {
+ ping_time = 5 minutes;
+ number_per_ip = 10;
+ max_number = 1000;
+ sendq = 1 megabyte;
+};
+
+class "server" {
+ ping_time = 5 minutes;
+ connectfreq = 5 minutes;
+ max_number = 1;
+ sendq = 4 megabytes;
+};
+
+listen {
+ /* defer_accept: wait for clients to send IRC handshake data before
+ * accepting them. if you intend to use software which depends on the
+ * server replying first, such as BOPM, you should disable this feature.
+ * otherwise, you probably want to leave it on.
+ */
+ defer_accept = yes;
+
+ /* If you want to listen on a specific IP only, specify host.
+ * host definitions apply only to the following port line.
+ */
+ #host = "192.0.2.6";
+ port = 5000, 6665 .. 6669;
+ # sslport = 6697;
+
+ /* Listen on IPv6 (if you used host= above). */
+ #host = "2001:db8:2::6";
+ #port = 5000, 6665 .. 6669;
+ #sslport = 9999;
+};
+
+/* auth {}: allow users to connect to the ircd (OLD I:)
+ * auth {} blocks MUST be specified in order of precedence. The first one
+ * that matches a user will be used. So place spoofs first, then specials,
+ * then general access, then restricted.
+ */
+auth {
+ /* user: the user@host allowed to connect. Multiple IPv4/IPv6 user
+ * lines are permitted per auth block. This is matched against the
+ * hostname and IP address (using :: shortening for IPv6 and
+ * prepending a 0 if it starts with a colon) and can also use CIDR
+ * masks.
+ */
+ user = "*@198.51.100.0/24";
+ user = "*test@2001:db8:1:*";
+
+ /* password: an optional password that is required to use this block.
+ * By default this is not encrypted, specify the flag "encrypted" in
+ * flags = ...; below if it is.
+ */
+ password = "letmein";
+
+ /* spoof: fake the users user@host to be be this. You may either
+ * specify a host or a user@host to spoof to. This is free-form,
+ * just do everyone a favour and dont abuse it. (OLD I: = flag)
+ */
+ spoof = "I.still.hate.packets";
+
+ /* Possible flags in auth:
+ *
+ * encrypted | password is encrypted with mkpasswd
+ * spoof_notice | give a notice when spoofing hosts
+ * exceed_limit (old > flag) | allow user to exceed class user limits
+ * kline_exempt (old ^ flag) | exempt this user from k/g/xlines,
+ * | dnsbls, and proxies
+ * proxy_exempt | exempt this user from proxies
+ * dnsbl_exempt | exempt this user from dnsbls
+ * spambot_exempt | exempt this user from spambot checks
+ * shide_exempt | exempt this user from serverhiding
+ * jupe_exempt | exempt this user from generating
+ * warnings joining juped channels
+ * resv_exempt | exempt this user from resvs
+ * flood_exempt | exempt this user from flood limits
+ * USE WITH CAUTION.
+ * no_tilde (old - flag) | don't prefix ~ to username if no ident
+ * need_ident (old + flag) | require ident for user in this class
+ * need_ssl | require SSL/TLS for user in this class
+ * need_sasl | require SASL id for user in this class
+ */
+ flags = kline_exempt, exceed_limit;
+
+ /* class: the class the user is placed in */
+ class = "opers";
+};
+
+auth {
+ user = "*@*";
+ class = "users";
+};
+
+/* privset {} blocks MUST be specified before anything that uses them. That
+ * means they must be defined before operator {}.
+ */
+privset "local_op" {
+ privs = oper:local_kill, oper:operwall;
+};
+
+privset "server_bot" {
+ extends = "local_op";
+ privs = oper:kline, oper:remoteban, snomask:nick_changes;
+};
+
+privset "global_op" {
+ extends = "local_op";
+ privs = oper:global_kill, oper:routing, oper:kline, oper:unkline, oper:xline,
+ oper:resv, oper:mass_notice, oper:remoteban;
+};
+
+privset "admin" {
+ extends = "global_op";
+ privs = oper:admin, oper:die, oper:rehash, oper:spy, oper:grant;
+};
+
+operator "god" {
+ /* name: the name of the oper must go above */
+
+ /* user: the user@host required for this operator. CIDR *is*
+ * supported now. auth{} spoofs work here, other spoofs do not.
+ * multiple user="" lines are supported.
+ */
+ user = "*god@127.0.0.1";
+
+ /* password: the password required to oper. Unless ~encrypted is
+ * contained in flags = ...; this will need to be encrypted using
+ * mkpasswd, MD5 is supported
+ */
+ password = "etcnjl8juSU1E";
+
+ /* rsa key: the public key for this oper when using Challenge.
+ * A password should not be defined when this is used, see
+ * doc/challenge.txt for more information.
+ */
+ #rsa_public_key_file = "/usr/local/ircd/etc/oper.pub";
+
+ /* umodes: the specific umodes this oper gets when they oper.
+ * If this is specified an oper will not be given oper_umodes
+ * These are described above oper_only_umodes in general {};
+ */
+ #umodes = locops, servnotice, operwall, wallop;
+
+ /* fingerprint: if specified, the oper's client certificate
+ * fingerprint will be checked against the specified fingerprint
+ * below.
+ */
+ #fingerprint = "c77106576abf7f9f90cca0f63874a60f2e40a64b";
+
+ /* snomask: specific server notice mask on oper up.
+ * If this is specified an oper will not be given oper_snomask.
+ */
+ snomask = "+Zbfkrsuy";
+
+ /* flags: misc options for the operator. You may prefix an option
+ * with ~ to disable it, e.g. ~encrypted.
+ *
+ * Default flags are encrypted.
+ *
+ * Available options:
+ *
+ * encrypted: the password above is encrypted [DEFAULT]
+ * need_ssl: must be using SSL/TLS to oper up
+ */
+ flags = encrypted;
+
+ /* privset: privileges set to grant */
+ privset = "admin";
+};
+
+connect "irc.uplink.com" {
+ host = "203.0.113.3";
+ send_password = "password";
+ accept_password = "anotherpassword";
+ port = 6666;
+ hub_mask = "*";
+ class = "server";
+ flags = compressed, topicburst;
+
+ #fingerprint = "c77106576abf7f9f90cca0f63874a60f2e40a64b";
+
+ /* If the connection is IPv6, uncomment below.
+ * Use 0::1, not ::1, for IPv6 localhost. */
+ #aftype = ipv6;
+};
+
+connect "ssl.uplink.com" {
+ host = "203.0.113.129";
+ send_password = "password";
+ accept_password = "anotherpassword";
+ port = 9999;
+ hub_mask = "*";
+ class = "server";
+ flags = ssl, topicburst;
+};
+
+service {
+ name = "services.int";
+};
+
+cluster {
+ name = "*";
+ flags = kline, tkline, unkline, xline, txline, unxline, resv, tresv, unresv;
+};
+
+shared {
+ oper = "*@*", "*";
+ flags = all, rehash;
+};
+
+/* exempt {}: IPs that are exempt from Dlines and rejectcache. (OLD d:) */
+exempt {
+ ip = "127.0.0.1";
+};
+
+channel {
+ use_invex = yes;
+ use_except = yes;
+ use_forward = yes;
+ use_knock = yes;
+ knock_delay = 5 minutes;
+ knock_delay_channel = 1 minute;
+ max_chans_per_user = 15;
+ max_chans_per_user_large = 60;
+ max_bans = 100;
+ max_bans_large = 500;
+ default_split_user_count = 0;
+ default_split_server_count = 0;
+ no_create_on_split = no;
+ no_join_on_split = no;
+ burst_topicwho = yes;
+ kick_on_split_riding = no;
+ only_ascii_channels = no;
+ resv_forcepart = yes;
+ channel_target_change = yes;
+ disable_local_channels = no;
+ autochanmodes = "+nt";
+ displayed_usercount = 3;
+ strip_topic_colors = no;
+};
+
+serverhide {
+ flatten_links = yes;
+ links_delay = 5 minutes;
+ hidden = no;
+ disable_hidden = no;
+};
+
+alias "NickServ" {
+ target = "NickServ";
+};
+
+alias "ChanServ" {
+ target = "ChanServ";
+};
+
+alias "OperServ" {
+ target = "OperServ";
+};
+
+alias "MemoServ" {
+ target = "MemoServ";
+};
+
+alias "NS" {
+ target = "NickServ";
+};
+
+alias "CS" {
+ target = "ChanServ";
+};
+
+alias "OS" {
+ target = "OperServ";
+};
+
+alias "MS" {
+ target = "MemoServ";
+};
+
+general {
+ hide_error_messages = opers;
+ hide_spoof_ips = yes;
+
+ /*
+ * default_umodes: umodes to enable on connect.
+ * If you have enabled the new ip_cloaking_4.0 module, and you want
+ * to make use of it, add +x to this option, i.e.:
+ * default_umodes = "+ix";
+ *
+ * If you have enabled the old ip_cloaking module, and you want
+ * to make use of it, add +h to this option, i.e.:
+ * default_umodes = "+ih";
+ */
+ default_umodes = "+i";
+
+ default_operstring = "is an IRC Operator";
+ default_adminstring = "is a Server Administrator";
+ servicestring = "is a Network Service";
+
+ /*
+ * Nick of the network's SASL agent. Used to check whether services are here,
+ * SASL credentials are only sent to its server. Needs to be a service.
+ *
+ * Defaults to SaslServ if unspecified.
+ */
+ sasl_service = "SaslServ";
+ disable_fake_channels = no;
+ tkline_expire_notices = no;
+ default_floodcount = 10;
+ failed_oper_notice = yes;
+ dots_in_ident=2;
+ min_nonwildcard = 4;
+ min_nonwildcard_simple = 3;
+ max_accept = 100;
+ max_monitor = 100;
+ anti_nick_flood = yes;
+ max_nick_time = 20 seconds;
+ max_nick_changes = 5;
+ anti_spam_exit_message_time = 5 minutes;
+ ts_warn_delta = 30 seconds;
+ ts_max_delta = 5 minutes;
+ client_exit = yes;
+ collision_fnc = yes;
+ resv_fnc = yes;
+ global_snotices = yes;
+ dline_with_reason = yes;
+ kline_delay = 0 seconds;
+ kline_with_reason = yes;
+ kline_reason = "K-Lined";
+ identify_service = "NickServ@services.int";
+ identify_command = "IDENTIFY";
+ non_redundant_klines = yes;
+ warn_no_nline = yes;
+ use_propagated_bans = yes;
+ stats_e_disabled = yes;
+ stats_c_oper_only=no;
+ stats_h_oper_only=no;
+ stats_y_oper_only=no;
+ stats_o_oper_only=yes;
+ stats_P_oper_only=no;
+ stats_i_oper_only=masked;
+ stats_k_oper_only=masked;
+ map_oper_only = no;
+ operspy_admin_only = no;
+ operspy_dont_care_user_info = no;
+ caller_id_wait = 1 minute;
+ pace_wait_simple = 1 second;
+ pace_wait = 10 seconds;
+ short_motd = no;
+ ping_cookie = no;
+ connect_timeout = 30 seconds;
+ default_ident_timeout = 5;
+ disable_auth = no;
+ no_oper_flood = yes;
+ max_targets = 4;
+ client_flood_max_lines = 20;
+ use_whois_actually = no;
+ oper_only_umodes = operwall, locops, servnotice;
+ oper_umodes = locops, servnotice, operwall, wallop;
+ oper_snomask = "+s";
+ burst_away = yes;
+ nick_delay = 0 seconds; # 15 minutes if you want to enable this
+ reject_ban_time = 1 minute;
+ reject_after_count = 3;
+ reject_duration = 5 minutes;
+ throttle_duration = 60;
+ throttle_count = 4;
+ max_ratelimit_tokens = 30;
+ away_interval = 30;
+ certfp_method = sha1;
+ hide_opers_in_whois = no;
+};
+
+modules {
+ path = "modules";
+ path = "modules/autoload";
+};
diff --git a/tests/iid.cpp b/tests/iid.cpp
new file mode 100644
index 0000000..74d010d
--- /dev/null
+++ b/tests/iid.cpp
@@ -0,0 +1,130 @@
+#include "catch.hpp"
+
+#include <irc/iid.hpp>
+#include <irc/irc_user.hpp>
+
+#include <config/config.hpp>
+
+TEST_CASE("Irc user parsing")
+{
+ const std::map<char, char> prefixes{{'!', 'a'}, {'@', 'o'}};
+ IrcUser user1("!nick!~some@host.bla", prefixes);
+ CHECK(user1.nick == "nick");
+ CHECK(user1.host == "~some@host.bla");
+ CHECK(user1.modes.size() == 1);
+ CHECK(user1.modes.find('a') != user1.modes.end());
+
+ IrcUser user2("coucou!~other@host.bla", prefixes);
+ CHECK(user2.nick == "coucou");
+ CHECK(user2.host == "~other@host.bla");
+ CHECK(user2.modes.empty());
+ CHECK(user2.modes.find('a') == user2.modes.end());
+}
+
+TEST_CASE("multi-prefix")
+{
+ const std::map<char, char> prefixes{{'!', 'a'}, {'@', 'o'}, {'~', 'f'}};
+ IrcUser user("!@~nick", prefixes);
+ CHECK(user.nick == "nick");
+ CHECK(user.modes.size() == 3);
+ CHECK(user.modes.find('f') != user.modes.end());
+}
+
+/**
+ * Let Catch know how to display Iid objects
+ */
+namespace Catch
+{
+ template<>
+ struct StringMaker<Iid>
+ {
+ static std::string convert(const Iid& value)
+ {
+ return std::to_string(value);
+ }
+ };
+}
+
+TEST_CASE("Iid creation")
+{
+ Iid iid1("foo!irc.example.org");
+ CHECK(std::to_string(iid1) == "foo!irc.example.org");
+ CHECK(iid1.get_local() == "foo");
+ CHECK(iid1.get_server() == "irc.example.org");
+ CHECK(!iid1.is_channel);
+ CHECK(iid1.is_user);
+
+ Iid iid2("#test%irc.example.org");
+ CHECK(std::to_string(iid2) == "#test%irc.example.org");
+ CHECK(iid2.get_local() == "#test");
+ CHECK(iid2.get_server() == "irc.example.org");
+ CHECK(iid2.is_channel);
+ CHECK(!iid2.is_user);
+
+ Iid iid3("%irc.example.org");
+ CHECK(std::to_string(iid3) == "%irc.example.org");
+ CHECK(iid3.get_local() == "");
+ CHECK(iid3.get_server() == "irc.example.org");
+ CHECK(iid3.is_channel);
+ CHECK(!iid3.is_user);
+
+ Iid iid4("irc.example.org");
+ CHECK(std::to_string(iid4) == "irc.example.org");
+ CHECK(iid4.get_local() == "");
+ CHECK(iid4.get_server() == "irc.example.org");
+ CHECK(!iid4.is_channel);
+ CHECK(!iid4.is_user);
+
+ Iid iid5("nick!");
+ CHECK(std::to_string(iid5) == "nick!");
+ CHECK(iid5.get_local() == "nick");
+ CHECK(iid5.get_server() == "");
+ CHECK(!iid5.is_channel);
+ CHECK(iid5.is_user);
+
+ Iid iid6("##channel%");
+ CHECK(std::to_string(iid6) == "##channel%");
+ CHECK(iid6.get_local() == "##channel");
+ CHECK(iid6.get_server() == "");
+ CHECK(iid6.is_channel);
+ CHECK(!iid6.is_user);
+}
+
+TEST_CASE("Iid creation in fixed_server mode")
+{
+ Config::set("fixed_irc_server", "fixed.example.com", false);
+
+ Iid iid1("foo!irc.example.org");
+ CHECK(std::to_string(iid1) == "foo!");
+ CHECK(iid1.get_local() == "foo");
+ CHECK(iid1.get_server() == "fixed.example.com");
+ CHECK(!iid1.is_channel);
+ CHECK(iid1.is_user);
+
+ Iid iid2("#test%irc.example.org");
+ CHECK(std::to_string(iid2) == "#test%irc.example.org");
+ CHECK(iid2.get_local() == "#test%irc.example.org");
+ CHECK(iid2.get_server() == "fixed.example.com");
+ CHECK(iid2.is_channel);
+ CHECK(!iid2.is_user);
+
+ // Note that it is impossible to adress the IRC server directly, or to
+ // use the virtual channel, in that mode
+
+ // Iid iid3("%irc.example.org");
+ // Iid iid4("irc.example.org");
+
+ Iid iid5("nick!");
+ CHECK(std::to_string(iid5) == "nick!");
+ CHECK(iid5.get_local() == "nick");
+ CHECK(iid5.get_server() == "fixed.example.com");
+ CHECK(!iid5.is_channel);
+ CHECK(iid5.is_user);
+
+ Iid iid6("##channel%");
+ CHECK(std::to_string(iid6) == "##channel%");
+ CHECK(iid6.get_local() == "##channel%");
+ CHECK(iid6.get_server() == "fixed.example.com");
+ CHECK(iid6.is_channel);
+ CHECK(!iid6.is_user);
+}
diff --git a/tests/io_tester.cpp b/tests/io_tester.cpp
new file mode 100644
index 0000000..19c97c9
--- /dev/null
+++ b/tests/io_tester.cpp
@@ -0,0 +1,30 @@
+#include "io_tester.hpp"
+#include "catch.hpp"
+#include <iostream>
+
+/**
+ * Directly test this class here
+ */
+TEST_CASE()
+{
+ {
+ IoTester<std::ostream> out(std::cout);
+ std::cout << "test";
+ CHECK(out.str() == "test");
+ }
+ {
+ IoTester<std::ostream> out(std::cout);
+ CHECK(out.str().empty());
+ }
+}
+
+TEST_CASE()
+{
+ {
+ IoTester<std::istream> is(std::cin);
+ is.set_string("coucou");
+ std::string res;
+ std::cin >> res;
+ CHECK(res == "coucou");
+ }
+}
diff --git a/tests/io_tester.hpp b/tests/io_tester.hpp
new file mode 100644
index 0000000..b9cdaa7
--- /dev/null
+++ b/tests/io_tester.hpp
@@ -0,0 +1,45 @@
+#pragma once
+
+#include <ostream>
+#include <sstream>
+
+/**
+ * Redirects a stream into a streambuf until the object is destroyed.
+ */
+template <typename StreamType>
+class IoTester
+{
+public:
+ IoTester(StreamType& ios):
+ stream{},
+ ios(ios),
+ old_buf(ios.rdbuf())
+ {
+ // Redirect the given os into our stringstream’s buf
+ this->ios.rdbuf(this->stream.rdbuf());
+ }
+ ~IoTester()
+ {
+ this->ios.rdbuf(this->old_buf);
+ }
+ IoTester& operator=(const IoTester&) = delete;
+ IoTester& operator=(IoTester&&) = delete;
+ IoTester(const IoTester&) = delete;
+ IoTester(IoTester&&) = delete;
+
+ std::string str() const
+ {
+ return this->stream.str();
+ }
+
+ void set_string(const std::string& s)
+ {
+ this->stream.str(s);
+ }
+
+private:
+ std::stringstream stream;
+ StreamType& ios;
+ std::streambuf* const old_buf;
+};
+
diff --git a/tests/jid.cpp b/tests/jid.cpp
new file mode 100644
index 0000000..9015afd
--- /dev/null
+++ b/tests/jid.cpp
@@ -0,0 +1,39 @@
+#include "catch.hpp"
+
+#include <xmpp/jid.hpp>
+#include <louloulibs.h>
+
+TEST_CASE("Jid")
+{
+ Jid jid1("♥@ツ.coucou/coucou@coucou/coucou");
+ CHECK(jid1.local == "♥");
+ CHECK(jid1.domain == "ツ.coucou");
+ CHECK(jid1.resource == "coucou@coucou/coucou");
+
+ // Domain and resource
+ Jid jid2("ツ.coucou/coucou@coucou/coucou");
+ CHECK(jid2.local == "");
+ CHECK(jid2.domain == "ツ.coucou");
+ CHECK(jid2.resource == "coucou@coucou/coucou");
+
+ // Jidprep
+ const std::string badjid("~zigougou™@EpiK-7D9D1FDE.poez.io/Boujour/coucou/slt™");
+ const std::string correctjid = jidprep(badjid);
+#ifdef LIBIDN_FOUND
+ CHECK(correctjid == "~zigougoutm@epik-7d9d1fde.poez.io/Boujour/coucou/sltTM");
+ // Check that the cache does not break things when we prep the same string
+ // multiple times
+ CHECK(jidprep(badjid) == "~zigougoutm@epik-7d9d1fde.poez.io/Boujour/coucou/sltTM");
+ CHECK(jidprep(badjid) == "~zigougoutm@epik-7d9d1fde.poez.io/Boujour/coucou/sltTM");
+
+ const std::string badjid2("Zigougou@poez.io");
+ const std::string correctjid2 = jidprep(badjid2);
+ CHECK(correctjid2 == "zigougou@poez.io");
+
+ const std::string crappy("~Bisous@7ea8beb1:c5fd2849:da9a048e:ip");
+ const std::string fixed_crappy = jidprep(crappy);
+ CHECK(fixed_crappy == "~bisous@7ea8beb1-c5fd2849-da9a048e-ip");
+#else // Without libidn, jidprep always returns an empty string
+ CHECK(jidprep(badjid) == "");
+#endif
+}
diff --git a/tests/logger.cpp b/tests/logger.cpp
new file mode 100644
index 0000000..1d59a22
--- /dev/null
+++ b/tests/logger.cpp
@@ -0,0 +1,57 @@
+#include "catch.hpp"
+
+#include <logger/logger.hpp>
+#include <config/config.hpp>
+
+#include "io_tester.hpp"
+#include <iostream>
+
+using namespace std::string_literals;
+
+TEST_CASE("Basic logging")
+{
+#ifdef SYSTEMD_FOUND
+ const std::string debug_header = "<7>";
+ const std::string error_header = "<3>";
+#else
+ const std::string debug_header = "[DEBUG]: ";
+ const std::string error_header = "[ERROR]: ";
+#endif
+ Logger::instance().reset();
+ GIVEN("A logger with log_level 0")
+ {
+ Config::set("log_level", "0");
+ WHEN("we log some debug text")
+ {
+ IoTester<std::ostream> out(std::cout);
+ log_debug("deb", "ug");
+ THEN("debug logs are written")
+ CHECK(out.str() == debug_header + "tests/logger.cpp:" + std::to_string(__LINE__ - 2) + ":\tdebug\n");
+ }
+ WHEN("we log some errors")
+ {
+ IoTester<std::ostream> out(std::cout);
+ log_error("err", 12, "or");
+ THEN("error logs are written")
+ CHECK(out.str() == error_header + "tests/logger.cpp:" + std::to_string(__LINE__ - 2) + ":\terr12or\n");
+ }
+ }
+ GIVEN("A logger with log_level 3")
+ {
+ Config::set("log_level", "3");
+ WHEN("we log some debug text")
+ {
+ IoTester<std::ostream> out(std::cout);
+ log_debug(123, "debug");
+ THEN("nothing is written")
+ CHECK(out.str().empty());
+ }
+ WHEN("we log some errors")
+ {
+ IoTester<std::ostream> out(std::cout);
+ log_error(123, " errors");
+ THEN("error logs are still written")
+ CHECK(out.str() == error_header + "tests/logger.cpp:" + std::to_string(__LINE__ - 2) + ":\t123 errors\n");
+ }
+ }
+}
diff --git a/tests/test.cpp b/tests/test.cpp
new file mode 100644
index 0000000..0c7c351
--- /dev/null
+++ b/tests/test.cpp
@@ -0,0 +1,2 @@
+#define CATCH_CONFIG_MAIN
+#include "catch.hpp"
diff --git a/tests/timed_events.cpp b/tests/timed_events.cpp
new file mode 100644
index 0000000..d63abef
--- /dev/null
+++ b/tests/timed_events.cpp
@@ -0,0 +1,62 @@
+#include "catch.hpp"
+
+#include <utils/timed_events.hpp>
+
+/**
+ * Let Catch know how to display std::chrono::duration values
+ */
+namespace Catch
+{
+ template<typename Rep, typename Period> struct StringMaker<std::chrono::duration<Rep, Period>>
+ {
+ static std::string convert(const std::chrono::duration<Rep, Period>& value)
+ {
+ return std::to_string(std::chrono::duration_cast<std::chrono::milliseconds>(value).count()) + "ms";
+ }
+ };
+}
+
+/**
+ * TODO, use a mock clock instead of relying on the real time with a sleep:
+ * it’s unreliable on heavy load.
+ */
+#include <thread>
+
+TEST_CASE("Test timed event expiration")
+{
+ SECTION("Check what happens when there is no events")
+ {
+ CHECK(TimedEventsManager::instance().get_timeout() == utils::no_timeout);
+ CHECK(TimedEventsManager::instance().execute_expired_events() == 0);
+ }
+
+ // Add a single event
+ TimedEventsManager::instance().add_event(TimedEvent(std::chrono::steady_clock::now() + 50ms, [](){}));
+
+ // The event should not yet be expired
+ CHECK(TimedEventsManager::instance().get_timeout() > 0ms);
+ CHECK(TimedEventsManager::instance().execute_expired_events() == 0);
+
+ std::chrono::milliseconds timoute = TimedEventsManager::instance().get_timeout();
+ INFO("Sleeping for " << timoute.count() << "ms");
+ std::this_thread::sleep_for(timoute + 1ms);
+
+ // Event is now expired
+ CHECK(TimedEventsManager::instance().execute_expired_events() == 1);
+ CHECK(TimedEventsManager::instance().get_timeout() == utils::no_timeout);
+}
+
+TEST_CASE("Test timed event cancellation")
+{
+ auto now = std::chrono::steady_clock::now();
+ TimedEventsManager::instance().add_event(TimedEvent(now + 100ms, [](){ }, "un"));
+ TimedEventsManager::instance().add_event(TimedEvent(now + 200ms, [](){ }, "deux"));
+ TimedEventsManager::instance().add_event(TimedEvent(now + 300ms, [](){ }, "deux"));
+
+ CHECK(TimedEventsManager::instance().get_timeout() > 0ms);
+ CHECK(TimedEventsManager::instance().size() == 3);
+ CHECK(TimedEventsManager::instance().cancel("un") == 1);
+ CHECK(TimedEventsManager::instance().size() == 2);
+ CHECK(TimedEventsManager::instance().cancel("deux") == 2);
+ CHECK(TimedEventsManager::instance().get_timeout() == utils::no_timeout);
+}
diff --git a/tests/utils.cpp b/tests/utils.cpp
new file mode 100644
index 0000000..01d070e
--- /dev/null
+++ b/tests/utils.cpp
@@ -0,0 +1,102 @@
+#include "catch.hpp"
+
+#include <utils/tolower.hpp>
+#include <utils/revstr.hpp>
+#include <utils/string.hpp>
+#include <utils/split.hpp>
+#include <utils/xdg.hpp>
+#include <utils/empty_if_fixed_server.hpp>
+
+TEST_CASE("String split")
+{
+ std::vector<std::string> splitted = utils::split("a::a", ':', false);
+ CHECK(splitted.size() == 2);
+ splitted = utils::split("a::a", ':', true);
+ CHECK(splitted.size() == 3);
+ CHECK(splitted[0] == "a");
+ CHECK(splitted[1] == "");
+ CHECK(splitted[2] == "a");
+ splitted = utils::split("\na", '\n', true);
+ CHECK(splitted.size() == 2);
+ CHECK(splitted[0] == "");
+ CHECK(splitted[1] == "a");
+}
+
+TEST_CASE("tolower")
+{
+ const std::string lowercase = utils::tolower("CoUcOu LeS CoPaiNs ♥");
+ CHECK(lowercase == "coucou les copains ♥");
+
+ const std::string ltr = "coucou";
+ CHECK(utils::revstr(ltr) == "uocuoc");
+}
+
+TEST_CASE("to_bool")
+{
+ CHECK(to_bool("true"));
+ CHECK(!to_bool("trou"));
+ CHECK(to_bool("1"));
+ CHECK(!to_bool("0"));
+ CHECK(!to_bool("-1"));
+ CHECK(!to_bool("false"));
+}
+
+TEST_CASE("xdg_*_path")
+{
+ ::unsetenv("XDG_CONFIG_HOME");
+ ::unsetenv("HOME");
+ std::string res;
+
+ SECTION("Without XDG_CONFIG_HOME nor HOME")
+ {
+ res = xdg_config_path("coucou.txt");
+ CHECK(res == "coucou.txt");
+ }
+ SECTION("With only HOME")
+ {
+ ::setenv("HOME", "/home/user", 1);
+ res = xdg_config_path("coucou.txt");
+ CHECK(res == "/home/user/.config/biboumi/coucou.txt");
+ }
+ SECTION("With only XDG_CONFIG_HOME")
+ {
+ ::setenv("XDG_CONFIG_HOME", "/some_weird_dir", 1);
+ res = xdg_config_path("coucou.txt");
+ CHECK(res == "/some_weird_dir/biboumi/coucou.txt");
+ }
+ SECTION("With XDG_DATA_HOME")
+ {
+ ::setenv("XDG_DATA_HOME", "/datadir", 1);
+ res = xdg_data_path("bonjour.txt");
+ CHECK(res == "/datadir/biboumi/bonjour.txt");
+ }
+}
+
+TEST_CASE("empty if fixed irc server")
+{
+ GIVEN("A config with fixed_irc_server")
+ {
+ Config::set("fixed_irc_server", "irc.localhost");
+ THEN("our string is made empty")
+ CHECK(utils::empty_if_fixed_server("coucou coucou") == "");
+ }
+ GIVEN("A config with NO fixed_irc_server")
+ {
+ Config::set("fixed_irc_server", "");
+ THEN("our string is returned untouched")
+ CHECK(utils::empty_if_fixed_server("coucou coucou") == "coucou coucou");
+ }
+
+}
+
+TEST_CASE("string cut")
+{
+ CHECK(cut("coucou", 2).size() == 3);
+ CHECK(cut("bonjour les copains", 6).size() == 4);
+ CHECK(cut("««««", 2).size() == 4);
+ CHECK(cut("a««««", 2).size() == 5);
+ const auto res = cut("rhello, ♥", 10);
+ CHECK(res.size() == 2);
+ CHECK(res[0] == "rhello, ");
+ CHECK(res[1] == "♥");
+}
diff --git a/tests/uuid.cpp b/tests/uuid.cpp
new file mode 100644
index 0000000..12c6c32
--- /dev/null
+++ b/tests/uuid.cpp
@@ -0,0 +1,13 @@
+#include "catch.hpp"
+
+#include <xmpp/xmpp_component.hpp>
+
+TEST_CASE("id generation")
+{
+ const std::string first_uuid = XmppComponent::next_id();
+ const std::string second_uuid = XmppComponent::next_id();
+
+ CHECK(first_uuid.size() == 36);
+ CHECK(second_uuid.size() == 36);
+ CHECK(first_uuid != second_uuid);
+}
diff --git a/tests/xmpp.cpp b/tests/xmpp.cpp
new file mode 100644
index 0000000..6aab8c4
--- /dev/null
+++ b/tests/xmpp.cpp
@@ -0,0 +1,47 @@
+#include "catch.hpp"
+
+#include <xmpp/xmpp_parser.hpp>
+
+TEST_CASE("Test basic XML parsing")
+{
+ XmppParser xml;
+
+ const std::string doc = "<stream xmlns='stream_ns'><stanza b='c'>inner<child1><grandchild/></child1><child2 xmlns='child2_ns'/>tail</stanza></stream>";
+
+ auto check_stanza = [](const Stanza& stanza)
+ {
+ CHECK(stanza.get_name() == "stanza");
+ CHECK(stanza.get_tag("xmlns") == "stream_ns");
+ CHECK(stanza.get_tag("b") == "c");
+ CHECK(stanza.get_inner() == "inner");
+ CHECK(stanza.get_tail() == "");
+ CHECK(stanza.get_child("child1", "stream_ns") != nullptr);
+ CHECK(stanza.get_child("child2", "stream_ns") == nullptr);
+ CHECK(stanza.get_child("child2", "child2_ns") != nullptr);
+ CHECK(stanza.get_child("child2", "child2_ns")->get_tail() == "tail");
+ };
+ xml.add_stanza_callback([check_stanza](const Stanza& stanza)
+ {
+ check_stanza(stanza);
+ // Do the same checks on a copy of that stanza.
+ Stanza copy(stanza);
+ check_stanza(copy);
+ // And do the same checks on moved-constructed stanza
+ Stanza moved(std::move(copy));
+ });
+ xml.feed(doc.data(), doc.size(), true);
+
+ const std::string doc2 = "<stream xmlns='s'><stanza>coucou\r\n\a</stanza></stream>";
+ xml.add_stanza_callback([](const Stanza& stanza)
+ {
+ CHECK(stanza.get_inner() == "coucou\r\n");
+ });
+
+ xml.feed(doc2.data(), doc.size(), true);
+}
+
+TEST_CASE("XML escape")
+{
+ const std::string unescaped = "'coucou'<cc>/&\"gaga\"";
+ CHECK(xml_escape(unescaped) == "&apos;coucou&apos;&lt;cc&gt;/&amp;&quot;gaga&quot;");
+}
diff --git a/unit/biboumi.service.cmake b/unit/biboumi.service.cmake
new file mode 100644
index 0000000..150045b
--- /dev/null
+++ b/unit/biboumi.service.cmake
@@ -0,0 +1,16 @@
+[Unit]
+Description=Biboumi, XMPP to IRC gateway
+Documentation=man:biboumi(1) https://biboumi.louiz.org
+After=network.target
+
+[Service]
+Type=${SYSTEMD_SERVICE_TYPE}
+ExecStart=${CMAKE_INSTALL_PREFIX}/bin/biboumi /etc/biboumi/biboumi.cfg
+ExecReload=/bin/kill -s USR1 $MAINPID
+WatchdogSec=${WATCHDOG_SEC}
+Restart=always
+User=${SERVICE_USER}
+Group=${SERVICE_GROUP}
+
+[Install]
+WantedBy=multi-user.target