diff options
-rw-r--r-- | .codecov.yml | 3 | ||||
-rw-r--r-- | .gitlab-ci.yml | 276 | ||||
-rw-r--r-- | CHANGELOG.rst | 13 | ||||
-rw-r--r-- | CMakeLists.txt | 367 | ||||
-rw-r--r-- | INSTALL.rst | 19 | ||||
-rw-r--r-- | README.rst | 9 | ||||
-rw-r--r-- | biboumi.h.cmake | 11 | ||||
-rw-r--r-- | cmake/Modules/CodeCoverage.cmake | 7 | ||||
-rw-r--r-- | cmake/Modules/FindBOTAN.cmake (renamed from louloulibs/cmake/Modules/FindBOTAN.cmake) | 0 | ||||
-rw-r--r-- | cmake/Modules/FindGCRYPT.cmake | 41 | ||||
-rw-r--r-- | cmake/Modules/FindICONV.cmake (renamed from louloulibs/cmake/Modules/FindICONV.cmake) | 4 | ||||
-rw-r--r-- | cmake/Modules/FindLIBIDN.cmake (renamed from louloulibs/cmake/Modules/FindLIBIDN.cmake) | 7 | ||||
-rw-r--r-- | cmake/Modules/FindLIBUUID.cmake (renamed from louloulibs/cmake/Modules/FindLIBUUID.cmake) | 5 | ||||
-rw-r--r-- | cmake/Modules/FindLITESQL.cmake | 2 | ||||
-rw-r--r-- | cmake/Modules/FindSYSTEMD.cmake (renamed from louloulibs/cmake/Modules/FindSYSTEMD.cmake) | 7 | ||||
-rw-r--r-- | cmake/Modules/FindUDNS.cmake | 38 | ||||
-rw-r--r-- | database/database.xml | 3 | ||||
-rw-r--r-- | doc/biboumi.1.rst | 102 | ||||
-rw-r--r-- | docker/biboumi-test/debian/Dockerfile | 68 | ||||
-rw-r--r-- | docker/biboumi-test/debian/Dockerfile.base | 57 | ||||
-rw-r--r-- | docker/biboumi-test/fedora/Dockerfile | 63 | ||||
-rw-r--r-- | docker/biboumi-test/fedora/Dockerfile.base | 59 | ||||
-rw-r--r-- | docker/biboumi/Dockerfile | 48 | ||||
-rw-r--r-- | docker/biboumi/README.md | 62 | ||||
-rw-r--r-- | docker/biboumi/biboumi.cfg | 6 | ||||
-rw-r--r-- | docker/biboumi/entrypoint.sh | 11 | ||||
-rw-r--r-- | louloulibs/CMakeLists.txt | 167 | ||||
-rw-r--r-- | louloulibs/cmake/Modules/FindCARES.cmake | 37 | ||||
-rw-r--r-- | louloulibs/louloulibs.h.cmake | 11 | ||||
-rw-r--r-- | louloulibs/network/dns_handler.cpp | 130 | ||||
-rw-r--r-- | louloulibs/network/dns_handler.hpp | 58 | ||||
-rw-r--r-- | louloulibs/network/dns_socket_handler.cpp | 49 | ||||
-rw-r--r-- | louloulibs/network/dns_socket_handler.hpp | 49 | ||||
-rw-r--r-- | louloulibs/network/resolver.cpp | 216 | ||||
-rw-r--r-- | louloulibs/utils/sha1.cpp | 121 | ||||
-rw-r--r-- | louloulibs/utils/sha1.hpp | 33 | ||||
-rw-r--r-- | louloulibs/xmpp/auth.cpp | 21 | ||||
-rw-r--r-- | louloulibs/xmpp/jid.cpp | 99 | ||||
-rw-r--r-- | packaging/biboumi.spec.cmake | 4 | ||||
-rw-r--r-- | src/bridge/bridge.cpp | 186 | ||||
-rw-r--r-- | src/bridge/bridge.hpp | 26 | ||||
-rw-r--r-- | src/bridge/colors.hpp | 2 | ||||
-rw-r--r-- | src/config/config.cpp (renamed from louloulibs/config/config.cpp) | 9 | ||||
-rw-r--r-- | src/config/config.hpp (renamed from louloulibs/config/config.hpp) | 3 | ||||
-rw-r--r-- | src/database/database.cpp | 4 | ||||
-rw-r--r-- | src/database/database.hpp | 2 | ||||
-rw-r--r-- | src/identd/identd_server.hpp | 39 | ||||
-rw-r--r-- | src/identd/identd_socket.cpp | 63 | ||||
-rw-r--r-- | src/identd/identd_socket.hpp | 37 | ||||
-rw-r--r-- | src/irc/iid.cpp | 5 | ||||
-rw-r--r-- | src/irc/iid.hpp | 1 | ||||
-rw-r--r-- | src/irc/irc_client.cpp | 49 | ||||
-rw-r--r-- | src/irc/irc_client.hpp | 11 | ||||
-rw-r--r-- | src/irc/irc_message.cpp | 6 | ||||
-rw-r--r-- | src/irc/irc_user.cpp | 2 | ||||
-rw-r--r-- | src/irc/irc_user.hpp | 2 | ||||
-rw-r--r-- | src/logger/logger.cpp (renamed from louloulibs/logger/logger.cpp) | 8 | ||||
-rw-r--r-- | src/logger/logger.hpp (renamed from louloulibs/logger/logger.hpp) | 20 | ||||
-rw-r--r-- | src/main.cpp | 80 | ||||
-rw-r--r-- | src/network/credentials_manager.cpp (renamed from louloulibs/network/credentials_manager.cpp) | 2 | ||||
-rw-r--r-- | src/network/credentials_manager.hpp (renamed from louloulibs/network/credentials_manager.hpp) | 3 | ||||
-rw-r--r-- | src/network/dns_handler.cpp | 46 | ||||
-rw-r--r-- | src/network/dns_handler.hpp | 37 | ||||
-rw-r--r-- | src/network/dns_socket_handler.cpp | 43 | ||||
-rw-r--r-- | src/network/dns_socket_handler.hpp | 33 | ||||
-rw-r--r-- | src/network/poller.cpp (renamed from louloulibs/network/poller.cpp) | 0 | ||||
-rw-r--r-- | src/network/poller.hpp (renamed from louloulibs/network/poller.hpp) | 2 | ||||
-rw-r--r-- | src/network/resolver.cpp | 280 | ||||
-rw-r--r-- | src/network/resolver.hpp (renamed from louloulibs/network/resolver.hpp) | 55 | ||||
-rw-r--r-- | src/network/socket_handler.hpp (renamed from louloulibs/network/socket_handler.hpp) | 10 | ||||
-rw-r--r-- | src/network/tcp_client_socket_handler.cpp | 261 | ||||
-rw-r--r-- | src/network/tcp_client_socket_handler.hpp | 82 | ||||
-rw-r--r-- | src/network/tcp_server_socket.hpp | 70 | ||||
-rw-r--r-- | src/network/tcp_socket_handler.cpp (renamed from louloulibs/network/tcp_socket_handler.cpp) | 258 | ||||
-rw-r--r-- | src/network/tcp_socket_handler.hpp (renamed from louloulibs/network/tcp_socket_handler.hpp) | 115 | ||||
-rw-r--r-- | src/utils/encoding.cpp (renamed from louloulibs/utils/encoding.cpp) | 5 | ||||
-rw-r--r-- | src/utils/encoding.hpp (renamed from louloulibs/utils/encoding.hpp) | 2 | ||||
-rw-r--r-- | src/utils/get_first_non_empty.cpp (renamed from louloulibs/utils/get_first_non_empty.cpp) | 0 | ||||
-rw-r--r-- | src/utils/get_first_non_empty.hpp (renamed from louloulibs/utils/get_first_non_empty.hpp) | 0 | ||||
-rw-r--r-- | src/utils/revstr.cpp (renamed from louloulibs/utils/revstr.cpp) | 0 | ||||
-rw-r--r-- | src/utils/revstr.hpp (renamed from louloulibs/utils/revstr.hpp) | 0 | ||||
-rw-r--r-- | src/utils/scopeguard.hpp (renamed from louloulibs/utils/scopeguard.hpp) | 6 | ||||
-rw-r--r-- | src/utils/sha1.cpp | 40 | ||||
-rw-r--r-- | src/utils/sha1.hpp | 5 | ||||
-rw-r--r-- | src/utils/split.cpp (renamed from louloulibs/utils/split.cpp) | 0 | ||||
-rw-r--r-- | src/utils/split.hpp (renamed from louloulibs/utils/split.hpp) | 0 | ||||
-rw-r--r-- | src/utils/string.cpp (renamed from louloulibs/utils/string.cpp) | 0 | ||||
-rw-r--r-- | src/utils/string.hpp (renamed from louloulibs/utils/string.hpp) | 0 | ||||
-rw-r--r-- | src/utils/system.cpp | 21 | ||||
-rw-r--r-- | src/utils/system.hpp | 8 | ||||
-rw-r--r-- | src/utils/time.cpp (renamed from louloulibs/utils/time.cpp) | 6 | ||||
-rw-r--r-- | src/utils/time.hpp (renamed from louloulibs/utils/time.hpp) | 0 | ||||
-rw-r--r-- | src/utils/timed_events.cpp (renamed from louloulibs/utils/timed_events.cpp) | 6 | ||||
-rw-r--r-- | src/utils/timed_events.hpp (renamed from louloulibs/utils/timed_events.hpp) | 0 | ||||
-rw-r--r-- | src/utils/timed_events_manager.cpp (renamed from louloulibs/utils/timed_events_manager.cpp) | 0 | ||||
-rw-r--r-- | src/utils/tolower.cpp (renamed from louloulibs/utils/tolower.cpp) | 0 | ||||
-rw-r--r-- | src/utils/tolower.hpp (renamed from louloulibs/utils/tolower.hpp) | 0 | ||||
-rw-r--r-- | src/utils/xdg.cpp (renamed from louloulibs/utils/xdg.cpp) | 2 | ||||
-rw-r--r-- | src/utils/xdg.hpp (renamed from louloulibs/utils/xdg.hpp) | 0 | ||||
-rw-r--r-- | src/xmpp/adhoc_command.cpp (renamed from louloulibs/xmpp/adhoc_command.cpp) | 35 | ||||
-rw-r--r-- | src/xmpp/adhoc_command.hpp (renamed from louloulibs/xmpp/adhoc_command.hpp) | 2 | ||||
-rw-r--r-- | src/xmpp/adhoc_commands_handler.cpp (renamed from louloulibs/xmpp/adhoc_commands_handler.cpp) | 35 | ||||
-rw-r--r-- | src/xmpp/adhoc_commands_handler.hpp (renamed from louloulibs/xmpp/adhoc_commands_handler.hpp) | 0 | ||||
-rw-r--r-- | src/xmpp/adhoc_session.cpp (renamed from louloulibs/xmpp/adhoc_session.cpp) | 2 | ||||
-rw-r--r-- | src/xmpp/adhoc_session.hpp (renamed from louloulibs/xmpp/adhoc_session.hpp) | 0 | ||||
-rw-r--r-- | src/xmpp/auth.cpp | 8 | ||||
-rw-r--r-- | src/xmpp/auth.hpp (renamed from louloulibs/xmpp/auth.hpp) | 0 | ||||
-rw-r--r-- | src/xmpp/biboumi_adhoc_commands.cpp | 314 | ||||
-rw-r--r-- | src/xmpp/biboumi_component.cpp | 327 | ||||
-rw-r--r-- | src/xmpp/biboumi_component.hpp | 12 | ||||
-rw-r--r-- | src/xmpp/body.hpp (renamed from louloulibs/xmpp/body.hpp) | 0 | ||||
-rw-r--r-- | src/xmpp/jid.cpp | 152 | ||||
-rw-r--r-- | src/xmpp/jid.hpp (renamed from louloulibs/xmpp/jid.hpp) | 0 | ||||
-rw-r--r-- | src/xmpp/xmpp_component.cpp (renamed from louloulibs/xmpp/xmpp_component.cpp) | 547 | ||||
-rw-r--r-- | src/xmpp/xmpp_component.hpp (renamed from louloulibs/xmpp/xmpp_component.hpp) | 16 | ||||
-rw-r--r-- | src/xmpp/xmpp_parser.cpp (renamed from louloulibs/xmpp/xmpp_parser.cpp) | 0 | ||||
-rw-r--r-- | src/xmpp/xmpp_parser.hpp (renamed from louloulibs/xmpp/xmpp_parser.hpp) | 0 | ||||
-rw-r--r-- | src/xmpp/xmpp_stanza.cpp (renamed from louloulibs/xmpp/xmpp_stanza.cpp) | 0 | ||||
-rw-r--r-- | src/xmpp/xmpp_stanza.hpp (renamed from louloulibs/xmpp/xmpp_stanza.hpp) | 14 | ||||
-rw-r--r-- | tests/config.cpp | 2 | ||||
-rw-r--r-- | tests/end_to_end/__main__.py | 553 | ||||
-rw-r--r-- | tests/end_to_end/ircd.conf | 2 | ||||
-rw-r--r-- | tests/iid.cpp | 6 | ||||
-rw-r--r-- | tests/jid.cpp | 22 | ||||
-rw-r--r-- | tests/utils.cpp | 18 | ||||
-rw-r--r-- | tests/xmpp.cpp | 17 |
126 files changed, 3731 insertions, 2589 deletions
diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..74ba41b --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,3 @@ +codecov: + ignore: + - "tests" diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e36ea0e..afe2393 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,129 +1,291 @@ +stages: + - build # Build in various conf, keeps the artifacts + - test # Use the build artifacts to run the tests + - packaging # Publish some packages (rpm, deb…) + - external # Interact with some external service (codecov, coverity…) + before_script: - uname -a + - locale - whoami - - export LC_ALL=C.UTF-8 - - export LANG=C.UTF-8 - - echo $LANG - - g++ --version - - clang++ --version - - rm -rf build/ - - mkdir build/ - - cd build + - mkdir -p build/ + - cd build/ variables: COMPILER: "g++" BUILD_TYPE: "Debug" BOTAN: "-DWITH_BOTAN=1" - CARES: "-DWITH_CARES=1" + UDNS: "-DWITH_UDNS=1" SYSTEMD: "-DWITH_SYSTEMD=1" LIBIDN: "-DWITH_LIBIDN=1" LITESQL: "-DWITH_LITESQL=1" +# +## Build jobs +# + .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 || echo 1) - - make check -j$(nproc || echo 1) tags: - docker + script: + - "echo Running cmake with the following parameters: -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${LITESQL}" + - cmake .. -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${LITESQL} + - make -j$(nproc || echo 1) + - make coverage_check -j$(nproc || echo 1) + artifacts: + expire_in: 8 hours + paths: + - build/ + +.template:fedora_build: &fedora_build + <<: *basic_build + image: docker.louiz.org/biboumi-test-fedora:latest + +.template:debian_build: &debian_build + <<: *basic_build + image: docker.louiz.org/biboumi-test-debian:latest -image: docker.louiz.org/biboumi-test-fedora:latest +build:fedora: + <<: *fedora_build + +build:debian: + <<: *debian_build build:1: variables: BOTAN: "-DWITHOUT_BOTAN=1" - <<: *basic_build + <<: *fedora_build build:2: variables: - CARES: "-DWITHOUT_CARES=1" - <<: *basic_build + UDNS: "-DWITHOUT_UDNS=1" + <<: *fedora_build build:3: variables: LITESQL: "-DWITHOUT_LITESQL=1" - <<: *basic_build + <<: *fedora_build build:4: variables: LITESQL: "-DWITHOUT_LITESQL=1" BOTAN: "-DWITHOUT_BOTAN=1" - <<: *basic_build + LIBIDN: "-DWITHOUT_LIBIDN=1" + <<: *fedora_build build:5: variables: LITESQL: "-DWITHOUT_LITESQL=1" - CARES: "-DWITHOUT_CARES=1" - <<: *basic_build + UDNS: "-DWITHOUT_UDNS=1" + <<: *fedora_build build:6: variables: BOTAN: "-DWITHOUT_BOTAN=1" - CARES: "-DWITHOUT_CARES=1" - <<: *basic_build + UDNS: "-DWITHOUT_UDNS=1" + <<: *fedora_build -build:6: +build:7: variables: - LIBIDN: "-DWITHOUT_LIBIDN=1" - CARES: "-DWITHOUT_CARES=1" - <<: *basic_build + UDNS: "-DWITHOUT_UDNS=1" + <<: *fedora_build -build:rpm: - stage: build - image: docker.louiz.org/biboumi-test-fedora:latest - tags: - - docker - script: - - cmake .. -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${CARES} ${SYSTEMD} ${LIBIDN} ${LITESQL} - - make rpm -j$(nproc || echo 1) - artifacts: - paths: - - build/rpmbuild/RPMS - - build/rpmbuild/SRPMS - when: always - name: $CI_PROJECT_NAME-rpm-$CI_BUILD_ID +# +## Test jobs +# .template:basic_test: &basic_test stage: test + tags: + - docker script: - - cmake .. -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${CARES} ${SYSTEMD} ${LIBIDN} ${LITESQL} - - make biboumi -j$(nproc || echo 1) - - make check + - make coverage_e2e -j$(nproc || echo 1) - make coverage - - mkdir tests_outputs && pushd tests_outputs && make coverage_e2e -j$(nproc || echo 1) -C .. && popd artifacts: paths: - - build/coverage/ + - build/coverage_test_suite/ - build/coverage_e2e/ - - build/tests_outputs/ + - build/coverage_total/ + - build/coverage_e2e.info when: always - name: $CI_PROJECT_NAME-test-$CI_BUILD_ID + name: $CI_PROJECT_NAME-test-$CI_JOB_ID test:debian: - stage: test image: docker.louiz.org/biboumi-test-debian:latest - tags: - - docker <<: *basic_test + dependencies: + - build:debian test:fedora: - stage: test image: docker.louiz.org/biboumi-test-fedora:latest - tags: - - docker <<: *basic_test + dependencies: + - build:fedora + +test:without_udns: + image: docker.louiz.org/biboumi-test-fedora:latest + <<: *basic_test + dependencies: + - build:7 -test:coverity: +test:freebsd: + only: + - master@louiz/biboumi + tags: + - freebsd + variables: + SYSTEMD: "-DWITHOUT_SYSTEMD=1" stage: test + script: + - cmake .. -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${LITESQL} + - make + - make check + - make e2e + +# +## External jobs +# + +.template:codecov: &codecov + stage: external + tags: + - docker + image: docker.louiz.org/biboumi-test-fedora:latest + +.template:codecov_unittests: &codecov_unittests + <<: *codecov + script: + - bash <(curl -s https://codecov.io/bash) -X gcov -X coveragepy -f ./coverage_test_suite.info -F $(echo $CI_JOB_NAME | sed s/:/_/g) + +.template:codecov_e2e: &codecov_e2e + <<: *codecov + script: + - bash <(curl -s https://codecov.io/bash) -X gcov -X coveragepy -f ./coverage_e2e.info -F $(echo $CI_JOB_NAME | sed s/:/_/g) + +codecov:fedora: + <<: *codecov_e2e + dependencies: + - test:fedora + +codecov:without_udns: + <<: *codecov_e2e + dependencies: + - test:without_udns + +codecov:debian: + <<: *codecov_e2e + dependencies: + - test:debian + +codecov:build:1: + <<: *codecov_unittests + dependencies: + - build:1 + +codecov:build:2: + <<: *codecov_unittests + dependencies: + - build:2 + +codecov:build:3: + <<: *codecov_unittests + dependencies: + - build:3 + +codecov:build:4: + <<: *codecov_unittests + dependencies: + - build:4 + +codecov:build:5: + <<: *codecov_unittests + dependencies: + - build:5 + +codecov:build:6: + <<: *codecov_unittests + dependencies: + - build:6 + +codecov:build:7: + <<: *codecov_unittests + dependencies: + - build:7 + +coverity: + stage: external + only: + - master@louiz/biboumi + tags: + - docker image: docker.louiz.org/biboumi-test-fedora:latest allow_failure: true when: manual script: - export PATH=$PATH:~/coverity/bin - cmake .. -DWITHOUT_SYSTEMD=1 - - cov-build --dir cov-int make biboumi test_suite -j$(nproc || echo 1) + - cov-build --dir cov-int make -j$(nproc || echo 1) - tar czvf biboumi_coverity.tgz cov-int - curl --form token=$COVERITY_TOKEN --form email=louiz@louiz.org --form file=@biboumi_coverity.tgz --form version="$(git rev-parse --short HEAD)" --form description="Automatic submission by gitlab-ci" https://scan.coverity.com/builds?project=louiz%2Fbiboumi + dependencies: [] + +packaging:rpm: + stage: packaging + only: + - master@louiz/biboumi + tags: + - docker + image: docker.louiz.org/biboumi-test-fedora:latest + script: + - make rpm -j$(nproc || echo 1) + artifacts: + paths: + - build/rpmbuild/RPMS + - build/rpmbuild/SRPMS + when: always + name: $CI_PROJECT_NAME-rpm-$CI_BUILD_ID + dependencies: + - build:fedora + +packaging:deb: + stage: packaging + only: + - master@louiz/biboumi + - debian@louiz/biboumi + tags: + - docker + image: docker.louiz.org/packaging-debian:latest + before_script: [] + script: + - git checkout debian + - git merge --no-commit --no-ff master + - mk-build-deps + - apt update -y + - apt install -y ./biboumi-build-deps*.deb + - debuild -b -us -uc + - mv ../*.deb . + - mv ../*.build . + - mv ../*.buildinfo . + dependencies: [] + artifacts: + untracked: true + name: $CI_PROJECT_NAME-deb-$CI_BUILD_ID + +packaging:archlinux: + stage: packaging + tags: + - docker + image: docker.louiz.org/biboumi-test-archlinux:latest + before_script: [] + script: + - sudo pacman -Syuu --noconfirm + - git clone https://aur.archlinux.org/litesql-git.git + - cd litesql-git + - makepkg --noconfirm -s && makepkg -f && sudo pacman --noconfirm -U *.pkg.* + - cd .. + - git clone https://aur.archlinux.org/biboumi-git.git + - cd biboumi-git + - makepkg --noconfirm -s && makepkg -f && sudo pacman --noconfirm -U *.pkg.* + dependencies: [] diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 349a9c3..e4ff422 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,16 @@ +Version 5.0 +=========== + + - An identd server has been added. + - Add a **persistent** option for channels. When a channel is configured + as persistent, when the user leaves the room, biboumi stays idle and keeps + saving the received messages in the archive, instead of leaving the channel + entirely. When the user re-joins the room later, biboumi sends the message + history to her/him. This feature can be used to make biboumi behave like + an IRC bouncer. + - Use the udns library instead of c-ares, for asynchronous DNS resolution. + It’s still fully optional. + Version 4.1 - 2017-03-21 ======================== diff --git a/CMakeLists.txt b/CMakeLists.txt index 9575cbd..581782e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,26 +2,32 @@ cmake_minimum_required(VERSION 3.0) project(biboumi) -set(${PROJECT_NAME}_VERSION_MAJOR 4) -set(${PROJECT_NAME}_VERSION_MINOR 1) -set(${PROJECT_NAME}_VERSION_SUFFIX "") +set(${PROJECT_NAME}_VERSION_MAJOR 5) +set(${PROJECT_NAME}_VERSION_MINOR 0) +set(${PROJECT_NAME}_VERSION_SUFFIX "~dev") +# +## Find optional instrumentation libraries that will be used in debug only +# +find_library(LIBASAN NAMES asan libasan.so.3 libasan.so.2 libasan.so.1) +find_library(LIBUBSAN NAMES ubsan libubsan.so.0) + +# +## Set various debug flags (instrumentation libs, coverage, …) +# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++1y -pedantic -Wall -Wextra") 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/") +if(LIBASAN) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address") +endif() +if(LIBUBSAN) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=undefined") +endif() # -## Get the software version +## Set the software version, archive name, RPM name etc # 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}) @@ -50,16 +56,8 @@ 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 +## The rule that generates the documentation # execute_process(COMMAND "date" "+%Y-%m-%d" OUTPUT_VARIABLE DOC_DATE OUTPUT_STRIP_TRAILING_WHITESPACE) @@ -82,122 +80,201 @@ if (NOT PANDOC_EXECUTABLE) endif() mark_as_advanced(PANDOC_EXECUTABLE) -# Look for litesql and enable the database if found +# +## Set this search path for cmake, to find our custom search modules +# +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules/") +find_package(ICONV REQUIRED) +find_package(LIBUUID REQUIRED) +find_package(EXPAT REQUIRED) + +# +## Find all the libraries (optional or not) +# +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(NOT BOTAN_FOUND) + find_package(GCRYPT REQUIRED) +endif() + +if(WITH_UDNS) + find_package(UDNS REQUIRED) +elseif(NOT WITHOUT_UDNS) + find_package(UDNS) +endif() + 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} utils) - if(BOTAN_FOUND) - target_link_libraries(database ${BOTAN_LIBRARIES}) - endif() - set(USE_DATABASE TRUE) -endif() - -add_subdirectory("louloulibs") -include_directories("louloulibs") - +# +## Set all the include directories, depending on what libraries are used +# 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}) +if(UDNS_FOUND) + include_directories(${UDNS_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() +# 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}/") # -## irclib +## Define all the modules # + +file(GLOB source_utils + src/utils/*.[hc]pp) +add_library(utils OBJECT ${source_utils}) +add_dependencies(utils database) + file(GLOB source_irc - src/irc/*.[hc]pp) -add_library(irc STATIC ${source_irc}) -target_link_libraries(irc network utils logger) + src/irc/*.[hc]pp) +add_library(irc OBJECT ${source_irc}) +add_dependencies(irc database) -# -## 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) + src/xmpp/*.[hc]pp) +add_library(xmpp OBJECT ${source_xmpp}) +add_dependencies(xmpp database) -if(USE_DATABASE) - target_link_libraries(xmpp database) - target_link_libraries(irc database) -endif() +file(GLOB source_identd + src/identd/*.[hc]pp) +add_library(identd OBJECT ${source_identd}) -# -## bridge -# file(GLOB source_bridge - src/bridge/*.[hc]pp) -add_library(bridge STATIC ${source_bridge}) -target_link_libraries(bridge xmpp irc utils logger) + src/bridge/*.[hc]pp) +add_library(bridge OBJECT ${source_bridge}) +add_dependencies(bridge database) -# -## 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}) +file(GLOB source_config + src/config/*.[hc]pp) +add_library(config OBJECT ${source_config}) + +file(GLOB source_logger + src/logger/*.[hc]pp) +add_library(logger OBJECT ${source_logger}) + +file(GLOB source_network + src/network/*.[hc]pp) +add_library(network OBJECT ${source_network}) +add_dependencies(network database) + +if(LITESQL_FOUND) + LITESQL_GENERATE_CPP("database/database.xml" + "biboudb" + LITESQL_GENERATED_SOURCES) + + add_library(database OBJECT src/database/database.cpp + ${LITESQL_GENERATED_SOURCES}) + set(USE_DATABASE TRUE) +else() + add_library(database OBJECT "") endif() # -## Tests +## Define the executables # + +## main +add_executable(${PROJECT_NAME} src/main.cpp + $<TARGET_OBJECTS:utils> + $<TARGET_OBJECTS:config> + $<TARGET_OBJECTS:logger> + $<TARGET_OBJECTS:network> + $<TARGET_OBJECTS:xmpp> + $<TARGET_OBJECTS:bridge> + $<TARGET_OBJECTS:irc> + $<TARGET_OBJECTS:identd> + $<TARGET_OBJECTS:database>) + +## test_suite file(GLOB source_tests tests/*.cpp) -add_executable(test_suite EXCLUDE_FROM_ALL - ${source_tests}) +add_executable(test_suite ${source_tests} + $<TARGET_OBJECTS:utils> + $<TARGET_OBJECTS:config> + $<TARGET_OBJECTS:logger> + $<TARGET_OBJECTS:network> + $<TARGET_OBJECTS:xmpp> + $<TARGET_OBJECTS:bridge> + $<TARGET_OBJECTS:irc> + $<TARGET_OBJECTS:identd> + $<TARGET_OBJECTS:database>) + +# +## Link the executables with their libraries +# +target_link_libraries(${PROJECT_NAME} + ${ICONV_LIBRARIES} + ${LIBUUID_LIBRARIES} + ${EXPAT_LIBRARY}) target_link_libraries(test_suite - xmpplib - xmpp - irc - bridge - utils - config - logger - network) + ${ICONV_LIBRARIES} + ${LIBUUID_LIBRARIES} + ${EXPAT_LIBRARY}) +if(SYSTEMD_FOUND) + target_link_libraries(${PROJECT_NAME} ${SYSTEMD_LIBRARIES}) + target_link_libraries(test_suite ${SYSTEMD_LIBRARIES}) +endif() +if(BOTAN_FOUND) + target_link_libraries(${PROJECT_NAME} ${BOTAN_LIBRARIES}) + target_link_libraries(test_suite ${BOTAN_LIBRARIES}) +elseif(GCRYPT_FOUND) + target_link_libraries(${PROJECT_NAME} ${GCRYPT_LIBRARIES}) + target_link_libraries(test_suite ${GCRYPT_LIBRARIES}) +endif() +if(UDNS_FOUND) + target_link_libraries(${PROJECT_NAME} ${UDNS_LIBRARIES}) + target_link_libraries(test_suite ${UDNS_LIBRARIES}) +endif() +if(LIBIDN_FOUND) + target_link_libraries(${PROJECT_NAME} ${LIBIDN_LIBRARIES}) + target_link_libraries(test_suite ${LIBIDN_LIBRARIES}) +endif() if(USE_DATABASE) - target_link_libraries(test_suite - database) + target_link_libraries(${PROJECT_NAME} ${LITESQL_LIBRARIES}) + target_link_libraries(test_suite ${LITESQL_LIBRARIES}) endif() +# Define a __FILENAME__ macro with the relative path (from the base project directory) +# of each source file +file(GLOB_RECURSE source_all src/*.[hc]pp tests/*.[hc]pp) +foreach(file ${source_all}) + file(RELATIVE_PATH shorter_file ${CMAKE_CURRENT_SOURCE_DIR} ${file}) + set_property(SOURCE ${file} APPEND PROPERTY COMPILE_DEFINITIONS __FILENAME__="${shorter_file}") +endforeach() + +# +## Add a rule to download the catch unit test framework +# include(ExternalProject) ExternalProject_Add(catch GIT_REPOSITORY "https://lab.louiz.org/louiz/Catch.git" @@ -215,27 +292,36 @@ if(NOT EXISTS ${CMAKE_SOURCE_DIR}/tests/catch.hpp) ) add_dependencies(test_suite catch) endif() + +# +## Add some custom rules to launch the tests +# add_custom_target(check COMMAND "test_suite" 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 - ) + SETUP_TARGET_FOR_COVERAGE(coverage_check + ./test_suite + coverage_test_suite) + add_dependencies(coverage_check test_suite) + SETUP_TARGET_FOR_COVERAGE(coverage_e2e - make + python3 coverage_e2e - e2e) + ${CMAKE_CURRENT_SOURCE_DIR}/tests/end_to_end/) + add_dependencies(coverage_e2e biboumi) + + ADD_CUSTOM_TARGET(coverage + COMMAND ${LCOV_PATH} -a coverage_e2e.info -a coverage_test_suite.info -o coverage_total.info + + COMMAND ${GENHTML_PATH} -o coverage_total coverage_total.info + + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) endif() # @@ -272,21 +358,9 @@ add_custom_target(rpm COMMAND rpmbuild --define "_topdir `pwd`/rpmbuild/" --define "_sourcedir `pwd`" -ba biboumi.spec ) -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 some variables that will be used in the cmake-generated files +# 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) @@ -304,12 +378,45 @@ set(SERVICE_GROUP_DOCSTRING "The value used as the Group= in the systemd unit fi 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") +# Force the format of the date output +set(ENV{LANG} "C") execute_process(COMMAND "date" "+%a %b %d %Y" OUTPUT_VARIABLE RPM_DATE OUTPUT_STRIP_TRAILING_WHITESPACE) unset(ENV{LANG}) +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() + +# +## Check if we have std::get_time and put_time +# +include(CheckCXXSourceCompiles) + +check_cxx_source_compiles(" + #include <iomanip> + int main() + { std::get_time(nullptr, \"\"); }" + HAS_GET_TIME) + +mark_as_advanced(HAS_GET_TIME) + +check_cxx_source_compiles(" + #include <iomanip> + int main() + { std::put_time(nullptr, \"\"); }" + HAS_PUT_TIME) + +mark_as_advanced(HAS_PUT_TIME) + +configure_file(unit/biboumi.service.cmake biboumi.service) configure_file(packaging/biboumi.spec.cmake biboumi.spec) +configure_file(biboumi.h.cmake src/biboumi.h) diff --git a/INSTALL.rst b/INSTALL.rst index 1526d7e..ccccfb3 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -36,15 +36,17 @@ libidn_ (optional, but recommended) Provides the stringprep functionality. Without it, JIDs for IRC users are not provided. -c-ares_ (optional, but recommended) +udns_ (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) +libbotan_ 1.11 or 2.0 (optional) Provides TLS support. Without it, IRC connections are all made in plain-text mode. - Other branches than the 1.11 are not supported. + +gcrypt_ (mandatory only if botan is absent) + Provides the SHA-1 hash function, for the case where Botan is absent. litesql_ (optional) Provides a way to store various options in a (sqlite3) database. Each user @@ -116,7 +118,13 @@ This command will configure the project to build a release, with TLS enabled Build ----- -Once you’ve configured everything using cmake, build the project +Once you’ve configured everything using cmake, build the software: + +To build the biboumi binary: + + make biboumi + +To build everything, including the tests make @@ -155,7 +163,8 @@ to use biboumi. .. _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/ +.. _udns: http://www.corpit.ru/mjt/udns.html .. _litesql: http://git.louiz.org/litesql .. _systemd: https://www.freedesktop.org/wiki/Software/systemd/ .. _biboumi.1.rst: doc/biboumi.1.rst +.. _gcrypt: https://www.gnu.org/software/libgcrypt/ @@ -2,13 +2,10 @@ Biboumi ======= .. image:: https://lab.louiz.org/louiz/biboumi/badges/master/build.svg - :target: https://lab.louiz.org/louiz/biboumi/commits/master + :target: https://lab.louiz.org/louiz/biboumi/pipelines -.. image:: https://lab.louiz.org/louiz/biboumi/badges/master/coverage.svg - :target: https://lab.louiz.org/louiz/biboumi/commits/master - -.. image:: https://sonarqube.com/api/badges/gate?key=biboumi - :target: https://sonarqube.com/component_issues/index?id=biboumi +.. image:: https://codecov.io/gh/louiz/biboumi/branch/master/graph/badge.svg + :target: https://codecov.io/gh/louiz/biboumi .. image:: https://scan.coverity.com/projects/3726/badge.svg :target: https://scan.coverity.com/projects/louiz-biboumi diff --git a/biboumi.h.cmake b/biboumi.h.cmake index beb67d0..1ad9a40 100644 --- a/biboumi.h.cmake +++ b/biboumi.h.cmake @@ -1 +1,12 @@ #cmakedefine USE_DATABASE +#cmakedefine ICONV_SECOND_ARGUMENT_IS_CONST +#cmakedefine LIBIDN_FOUND +#cmakedefine SYSTEMD_FOUND +#cmakedefine POLLER ${POLLER} +#cmakedefine BOTAN_FOUND +#cmakedefine GCRYPT_FOUND +#cmakedefine UDNS_FOUND +#cmakedefine SOFTWARE_VERSION "${SOFTWARE_VERSION}" +#cmakedefine PROJECT_NAME "${PROJECT_NAME}" +#cmakedefine HAS_GET_TIME +#cmakedefine HAS_PUT_TIME diff --git a/cmake/Modules/CodeCoverage.cmake b/cmake/Modules/CodeCoverage.cmake index c07a3df..77586ab 100644 --- a/cmake/Modules/CodeCoverage.cmake +++ b/cmake/Modules/CodeCoverage.cmake @@ -157,13 +157,10 @@ FUNCTION(SETUP_TARGET_FOR_COVERAGE _targetname _testrunner _outputname) # 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/*' 'build/*' --output-file ${_outputname}.info.cleaned -q + COMMAND ${LCOV_PATH} --remove ${_outputname}.info 'tests/*' '/usr/*' 'external/*' 'build/*' --output-file ${_outputname}.info -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 + COMMAND ${GENHTML_PATH} -o ${_outputname} ${_outputname}.info WORKING_DIRECTORY ${CMAKE_BINARY_DIR} COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report." diff --git a/louloulibs/cmake/Modules/FindBOTAN.cmake b/cmake/Modules/FindBOTAN.cmake index 13d2de4..13d2de4 100644 --- a/louloulibs/cmake/Modules/FindBOTAN.cmake +++ b/cmake/Modules/FindBOTAN.cmake diff --git a/cmake/Modules/FindGCRYPT.cmake b/cmake/Modules/FindGCRYPT.cmake new file mode 100644 index 0000000..b73bfd0 --- /dev/null +++ b/cmake/Modules/FindGCRYPT.cmake @@ -0,0 +1,41 @@ +# - Find gcrypt +# Find the gcrypt cryptographic library +# +# This module defines the following variables: +# GCRYPT_FOUND - True if library and include directory are found +# If set to TRUE, the following are also defined: +# GCRYPT_INCLUDE_DIRS - The directory where to find the header file +# GCRYPT_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. +# GCRYPT_LIBRARY +# GCRYPT_INCLUDE_DIR +# +# This file is in the public domain + +include(FindPkgConfig) +pkg_check_modules(GCRYPT gcrypt) + +if(NOT GCRYPT_FOUND) + find_path(GCRYPT_INCLUDE_DIRS NAMES gcrypt.h + PATH_SUFFIXES gcrypt + DOC "The gcrypt include directory") + + find_library(GCRYPT_LIBRARIES NAMES gcrypt + DOC "The gcrypt library") + + # Use some standard module to handle the QUIETLY and REQUIRED arguments, and + # set GCRYPT_FOUND to TRUE if these two variables are set. + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(GCRYPT REQUIRED_VARS GCRYPT_LIBRARIES GCRYPT_INCLUDE_DIRS) + + if(GCRYPT_FOUND) + set(GCRYPT_LIBRARY ${GCRYPT_LIBRARIES} CACHE INTERNAL "") + set(GCRYPT_INCLUDE_DIR ${GCRYPT_INCLUDE_DIRS} CACHE INTERNAL "") + set(GCRYPT_FOUND ${GCRYPT_FOUND} CACHE INTERNAL "") + endif() +endif() + +mark_as_advanced(GCRYPT_INCLUDE_DIRS GCRYPT_LIBRARIES) diff --git a/louloulibs/cmake/Modules/FindICONV.cmake b/cmake/Modules/FindICONV.cmake index 7ca173f..fb78ac7 100644 --- a/louloulibs/cmake/Modules/FindICONV.cmake +++ b/cmake/Modules/FindICONV.cmake @@ -52,9 +52,9 @@ if(ICONV_FOUND) return 0;}" ICONV_SECOND_ARGUMENT_IS_CONST) -# Compatibility for all the ways of writing these variables + # 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 +mark_as_advanced(ICONV_INCLUDE_DIRS ICONV_LIBRARIES ICONV_SECOND_ARGUMENT_IS_CONST) diff --git a/louloulibs/cmake/Modules/FindLIBIDN.cmake b/cmake/Modules/FindLIBIDN.cmake index 611a6a8..d42257f 100644 --- a/louloulibs/cmake/Modules/FindLIBIDN.cmake +++ b/cmake/Modules/FindLIBIDN.cmake @@ -33,9 +33,10 @@ if(NOT LIBIDN_FOUND) # Compatibility for all the ways of writing these variables if(LIBIDN_FOUND) - set(LIBIDN_INCLUDE_DIR ${LIBIDN_INCLUDE_DIRS}) - set(LIBIDN_LIBRARY ${LIBIDN_LIBRARIES}) + set(LIBIDN_INCLUDE_DIR ${LIBIDN_INCLUDE_DIRS} CACHE INTERNAL "") + set(LIBIDN_LIBRARY ${LIBIDN_LIBRARIES} CACHE INTERNAL "") + set(LIBIDN_FOUN ${LIBIDN_FOUND} CACHE INTERNAL "") endif() endif() -mark_as_advanced(LIBIDN_INCLUDE_DIRS LIBIDN_LIBRARIES)
\ No newline at end of file +mark_as_advanced(LIBIDN_INCLUDE_DIRS LIBIDN_LIBRARIES) diff --git a/louloulibs/cmake/Modules/FindLIBUUID.cmake b/cmake/Modules/FindLIBUUID.cmake index f344249..b4bf341 100644 --- a/louloulibs/cmake/Modules/FindLIBUUID.cmake +++ b/cmake/Modules/FindLIBUUID.cmake @@ -33,8 +33,9 @@ if(NOT LIBUUID_FOUND) # Compatibility for all the ways of writing these variables if(LIBUUID_FOUND) - set(LIBUUID_INCLUDE_DIR ${LIBUUID_INCLUDE_DIRS}) - set(LIBUUID_LIBRARY ${LIBUUID_LIBRARIES}) + set(LIBUUID_INCLUDE_DIR ${LIBUUID_INCLUDE_DIRS} CACHE INTERNAL "") + set(LIBUUID_LIBRARY ${LIBUUID_LIBRARIES} CACHE INTERNAL "") + set(LIBUUID_FOUND ${LIBUUID_FOUND} CACHE INTERNAL "") endif() endif() diff --git a/cmake/Modules/FindLITESQL.cmake b/cmake/Modules/FindLITESQL.cmake index 91155bb..18f0bfa 100644 --- a/cmake/Modules/FindLITESQL.cmake +++ b/cmake/Modules/FindLITESQL.cmake @@ -64,7 +64,7 @@ function(LITESQL_GENERATE_CPP set(${OUTPUT_SOURCES}) add_custom_command( OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_NAME}.cpp" - "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_NAME}.hpp" + "${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} diff --git a/louloulibs/cmake/Modules/FindSYSTEMD.cmake b/cmake/Modules/FindSYSTEMD.cmake index c7decde..899d07a 100644 --- a/louloulibs/cmake/Modules/FindSYSTEMD.cmake +++ b/cmake/Modules/FindSYSTEMD.cmake @@ -31,9 +31,10 @@ if(NOT SYSTEMD_FOUND) 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}) + set(SYSTEMD_LIBRARY ${SYSTEMD_LIBRARIES} CACHE INTERNAL "") + set(SYSTEMD_INCLUDE_DIR ${SYSTEMD_INCLUDE_DIRS} CACHE INTERNAL "") + set(SYSTEMD_FOUND ${SYSTEMD_FOUND} CACHE INTERNAL "") endif() endif() -mark_as_advanced(SYSTEMD_INCLUDE_DIRS SYSTEMD_LIBRARIES)
\ No newline at end of file +mark_as_advanced(SYSTEMD_INCLUDE_DIRS SYSTEMD_LIBRARIES) diff --git a/cmake/Modules/FindUDNS.cmake b/cmake/Modules/FindUDNS.cmake new file mode 100644 index 0000000..9576b2a --- /dev/null +++ b/cmake/Modules/FindUDNS.cmake @@ -0,0 +1,38 @@ +# - Find udns +# Find the udns library +# +# This module defines the following variables: +# UDNS_FOUND - True if library and include directory are found +# If set to TRUE, the following are also defined: +# UDNS_INCLUDE_DIRS - The directory where to find the header file +# UDNS_LIBRARIES - Where to find the library file +# +# For conveniance, these variables are also set. They have the same values +# as the variables above. The user can thus choose his/her prefered way +# to write them. +# UDNS_INCLUDE_DIR +# UDNS_LIBRARY +# +# This file is in the public domain + +if(NOT UDNS_FOUND) + find_path(UDNS_INCLUDE_DIRS NAMES udns.h + DOC "The udns include directory") + + find_library(UDNS_LIBRARIES NAMES udns + DOC "The udns library") + + # Use some standard module to handle the QUIETLY and REQUIRED arguments, and + # set UDNS_FOUND to TRUE if these two variables are set. + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(UDNS REQUIRED_VARS UDNS_LIBRARIES UDNS_INCLUDE_DIRS) + + # Compatibility for all the ways of writing these variables + if(UDNS_FOUND) + set(UDNS_INCLUDE_DIR ${UDNS_INCLUDE_DIRS} CACHE INTERNAL "") + set(UDNS_LIBRARY ${UDNS_LIBRARIES} CACHE INTERNAL "") + set(UDNS_FOUND ${UDNS_FOUND} CACHE INTERNAL "") + endif() +endif() + +mark_as_advanced(UDNS_INCLUDE_DIRS UDNS_LIBRARIES) diff --git a/database/database.xml b/database/database.xml index af15ad5..0bc6e35 100644 --- a/database/database.xml +++ b/database/database.xml @@ -24,6 +24,7 @@ <field name="realname" type="string" length="1024" default=""/> <field name="verifyCert" type="boolean" default="true"/> <field name="trustedFingerprint" type="string"/> + <field name="lingerTime" type="integer" default="0"/> <field name="encodingOut" type="string" default="ISO-8859-1"/> <field name="encodingIn" type="string" default="ISO-8859-1"/> @@ -45,6 +46,8 @@ <field name="maxHistoryLength" type="integer" default="20"/> + <field name="persistent" type="boolean" default="false"/> + <index unique="true"> <indexfield name="owner"/> <indexfield name="server"/> diff --git a/doc/biboumi.1.rst b/doc/biboumi.1.rst index 49c0fe4..ab7beac 100644 --- a/doc/biboumi.1.rst +++ b/doc/biboumi.1.rst @@ -38,9 +38,10 @@ 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. +Sending SIGUSR1 or SIGUSR2 (see kill(1)) to the process will force it to +re-read the configuration and make it close and re-open the log files. You +can use this to change any configuration option at runtime, or do a log +rotation. hostname -------- @@ -83,13 +84,13 @@ 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 +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. The '%' character loses any meaning in the JIDs. It can appear +available. The `%` character loses any meaning in the JIDs. 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 +``#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. @@ -152,6 +153,12 @@ 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. +identd_port +----------- + +The TCP port on which to listen for identd queries. The default is the standard value: 113. + + Usage ===== @@ -194,8 +201,8 @@ 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 +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 and IRC users have a local part formed like this: @@ -291,7 +298,7 @@ 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 +displayed in your XMPP client may not be the same as the order on other IRC users’. History @@ -322,8 +329,8 @@ 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. +is huge so the result stanza may be very big, unless your client supports +result set management (XEP 0059) Nicknames --------- @@ -485,11 +492,59 @@ On the gateway itself (e.g on the JID biboumi.example.com): with an XMPP message. The administrator can disconnect any user, while the other users can only disconnect themselves. +- configure: Lets each user configure some options that applies globally. + The provided configuration form contains these fields: + * Record History: whether or not history messages should be saved in + the database. + * Max history length: The maximum number of lines in the history + that the server is allowed to send when joining a channel. + 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. + concerned IRC server. The provided configuration form contains these + fields: + * Realname: The customized “real name” as it will appear on the + user’s whois. This option is not available if biboumi is configured + with realname_customization to false. + * Username: The “user” part in your `user@host`. This option is not + available if biboumi is configured with realname_customization to + false. + * In encoding: The incoming encoding. Any received message that is not + proper UTF-8 will be converted will be converted from the configured + In encoding into UTF-8. If the conversion fails at some point, some + characters will be replaced by the placeholders. + * Out encoding: Currently ignored. + * After-connection IRC command: A raw IRC command that will be sent to + the server immediately after the connection has been successful. It + can for example be used to identify yourself using NickServ, with a + command like this: `PRIVMSG NickServ :identify PASSWORD`. + * Ports: The list of TCP ports to use when connecting to this IRC server. + This list will be tried in sequence, until the connection succeeds for + one of them. The connection made on these ports will not use TLS, the + communication will be insecure. The default list contains 6697 and 6670. + * TLS ports: A second list of ports to try when connecting to the IRC + server. The only difference is that TLS will be used if the connection + is established on one of these ports. All the ports in this list will + be tried before using the other plain-text ports list. To entirely + disable any non-TLS connection, just remove all the values from the + “normal” ports list. The default list contains 6697. + * Verify certificate: If set to true (the default value), when connecting + on a TLS port, the connection will be aborted if the certificate is + not valid (for example if it’s not signed by a known authority, or if + the domain name doesn’t match, etc). Set it to false if you want to + connect on a server with a self-signed certificate. + * SHA-1 fingerprint of the TLS certificate to trust: if you know the hash + of the certificate that the server is supposed to use, and you only want + to accept this one, set its SHA-1 hash in this field. + * Server password: A password that will be sent just after the connection, + in a PASS command. This is usually used in private servers, where you’re + only allowed to connect if you have the password. Note that, although + this is NOT a password that will be sent to NickServ (or some author + authentication service), some server (notably Freenode) use it as if it + was sent to NickServ to identify your nickname. + - get-irc-connection-info: Returns some information about the IRC server, for the executing user. It lets the user know if they are connected to this server, from what port, with or without TLS, and it gives the list @@ -504,7 +559,18 @@ On a channel JID (e.g on the JID #test%chat.freenode.org@biboumi.example.com) 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. + encoding configured in the server applies. The provided configuration + form contains these fields: + * In encoding: see the option with the same name in the server configuration + form. + * Out encoding: Currently ignored. + * Persistent: If set to true, biboumi will stay in this channel even when + all the XMPP resources have left the room. I.e. it will not send a PART + command, and will stay idle in the channel until the connection is + forcibly closed. If a resource comes back in the room again, and if + the archiving of messages is enabled for this room, the client will + receive the messages that where sent in this channel. This option can be + used to make biboumi act as an IRC bouncer. Raw IRC messages ---------------- @@ -515,11 +581,11 @@ 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”. +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. +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 ======== diff --git a/docker/biboumi-test/debian/Dockerfile b/docker/biboumi-test/debian/Dockerfile index 9aac3ec..b811ea4 100644 --- a/docker/biboumi-test/debian/Dockerfile +++ b/docker/biboumi-test/debian/Dockerfile @@ -1,74 +1,10 @@ # This Dockerfile creates a docker image suitable to run biboumi’s build and # tests. For example, it can be used on with gitlab-ci. -FROM docker.io/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 +FROM docker.louiz.org/biboumi-test-debian-base # 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 https://github.com/saghul/aiodns.git -RUN cd aiodns && python3 setup.py build && python3 setup.py install -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 zlib1g-dev -RUN apt install -y libtool -RUN git clone https://github.com/charybdis-ircd/charybdis.git && cd charybdis -RUN cd /charybdis && git checkout 4f2b9a4 && ./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 +RUN git clone git://git.louiz.org/litesql && mkdir /litesql/build && cd /litesql/build && cmake .. -DCMAKE_INSTALL_PREFIX=/usr && make -j8 && cd /litesql/build && make install && rm -rf /litesql && ldconfig WORKDIR /home/tester USER tester diff --git a/docker/biboumi-test/debian/Dockerfile.base b/docker/biboumi-test/debian/Dockerfile.base new file mode 100644 index 0000000..f5d061b --- /dev/null +++ b/docker/biboumi-test/debian/Dockerfile.base @@ -0,0 +1,57 @@ +# 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 + +ENV LC_ALL C.UTF-8 + +RUN apt update + +# Needed to build biboumi +RUN apt install -y g++\ + clang\ + valgrind\ + libudns-dev\ + libc-ares-dev\ + libsqlite3-dev\ + libuuid1\ + libgcrypt20-dev\ + cmake\ + make\ + libexpat1-dev\ + libidn11-dev\ + uuid-dev\ + libsystemd-dev\ + pandoc\ + libasan1\ + libubsan0\ + git\ + python3-lxml\ + lcov\ + libtool\ + python3-pip\ + python3-dev\ + automake\ + autoconf\ + flex\ + bison\ + libltdl-dev\ + openssl\ + zlib1g-dev\ + libssl-dev\ + curl + +# Install botan +RUN git clone https://github.com/randombit/botan.git && cd botan && ./configure.py --prefix=/usr && make -j8 && make install && rm -rf /botan + +# Install slixmpp, for e2e tests +RUN git clone https://github.com/saghul/aiodns.git && cd aiodns && git checkout 7ee13f9bea25784322~ && python3 setup.py build && python3 setup.py install && git clone git://git.louiz.org/slixmpp && pip3 install pyasn1 && cd slixmpp && python3 setup.py build && python3 setup.py install + +RUN useradd tester -m + +# Install charybdis, for e2e tests +RUN git clone https://github.com/charybdis-ircd/charybdis.git && cd charybdis && cd /charybdis && git checkout 4f2b9a4 && sed s/113/1113/ -i /charybdis/authd/providers/ident.c && ./autogen.sh && ./configure --prefix=/home/tester/ircd --bindir=/usr/bin && make -j8 && make install && rm -rf /charybdis + +RUN chown -R tester:tester /home/tester/ircd + +RUN yes "" | openssl req -nodes -x509 -newkey rsa:4096 -keyout /home/tester/ircd/etc/ssl.key -out /home/tester/ircd/etc/ssl.pem diff --git a/docker/biboumi-test/fedora/Dockerfile b/docker/biboumi-test/fedora/Dockerfile index ebcb4e4..45dbe76 100644 --- a/docker/biboumi-test/fedora/Dockerfile +++ b/docker/biboumi-test/fedora/Dockerfile @@ -1,69 +1,10 @@ # This Dockerfile creates a docker image suitable to run biboumi’s build and # tests. For example, it can be used on with gitlab-ci. -FROM docker.io/fedora: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 +FROM docker.louiz.org/biboumi-test-fedora-base # 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 && git checkout 4f2b9a4 && ./autogen.sh && ./configure --prefix=/home/tester/ircd --bindir=/usr/bin --with-included-boost && 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" - -COPY coverity /home/tester/coverity -COPY sonar-scanner-2.8 /home/tester/sonar-scanner - -RUN dnf install -y which java-1.8.0-openjdk +RUN git clone git://git.louiz.org/litesql && mkdir /litesql/build && cd /litesql/build && cmake .. -DCMAKE_INSTALL_PREFIX=/usr && make -j8 && cd /litesql/build && make install && ldconfig && rm -rf /litesql WORKDIR /home/tester USER tester - diff --git a/docker/biboumi-test/fedora/Dockerfile.base b/docker/biboumi-test/fedora/Dockerfile.base new file mode 100644 index 0000000..20984a2 --- /dev/null +++ b/docker/biboumi-test/fedora/Dockerfile.base @@ -0,0 +1,59 @@ +# 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 + +ENV LC_ALL C.UTF-8 + +RUN dnf --refresh install -y\ + gcc-c++\ + clang\ + valgrind\ + udns-devel\ + c-ares-devel\ + sqlite-devel\ + libuuid-devel\ + libgcrypt-devel\ + cmake\ + make\ + expat-devel\ + libidn-devel\ + uuid-devel\ + systemd-devel\ + pandoc\ + libasan\ + libubsan\ + git\ + fedora-packager\ + python3-lxml\ + lcov\ + rpmdevtools\ + python3-devel\ + automake\ + autoconf\ + flex\ + flex-devel\ + bison\ + libtool-ltdl-devel\ + libtool\ + openssl-devel\ + which\ + java-1.8.0-openjdk\ + && dnf clean all + +# Install botan +RUN git clone https://github.com/randombit/botan.git && cd botan && ./configure.py --prefix=/usr && make -j8 && make install && ldconfig && rm -rf /botan + +# Install slixmpp, for e2e tests +RUN git clone git://git.louiz.org/slixmpp && pip3 install pyasn1 && cd slixmpp && python3 setup.py build && python3 setup.py install + +RUN useradd tester + +# Install charybdis, for e2e tests +RUN git clone https://github.com/charybdis-ircd/charybdis.git && cd charybdis && cd /charybdis && git checkout 4f2b9a4 && sed s/113/1113/ -i /charybdis/authd/providers/ident.c && ./autogen.sh && ./configure --prefix=/home/tester/ircd --bindir=/usr/bin --with-included-boost && make -j8 && make install && rm -rf /charybdis + +RUN chown -R tester:tester /home/tester/ircd + +RUN yes "" | openssl req -nodes -x509 -newkey rsa:4096 -keyout /home/tester/ircd/etc/ssl.key -out /home/tester/ircd/etc/ssl.pem + +COPY coverity /home/tester/coverity diff --git a/docker/biboumi/Dockerfile b/docker/biboumi/Dockerfile new file mode 100644 index 0000000..721d106 --- /dev/null +++ b/docker/biboumi/Dockerfile @@ -0,0 +1,48 @@ +# This Dockerfile creates a docker image running biboumi + +FROM docker.io/fedora:latest + +RUN dnf --refresh install -y\ + gcc-c++\ + cmake\ + make\ + udns-devel\ + sqlite-devel\ + libuuid-devel\ + expat-devel\ + libidn-devel\ + systemd-devel\ + git\ + python\ + && dnf clean all + +# Install botan +RUN git clone https://github.com/randombit/botan.git && cd botan && ./configure.py --prefix=/usr && make -j8 && make install && ldconfig && rm -rf /botan + +# Install litesql +RUN git clone git://git.louiz.org/litesql && mkdir /litesql/build && cd /litesql/build && cmake .. -DCMAKE_INSTALL_PREFIX=/usr && make -j8 && cd /litesql/build && make install && ldconfig && rm -rf /litesql + +# Install biboumi +RUN git clone git://git.louiz.org/biboumi && mkdir ./biboumi/build && cd ./biboumi/build &&\ + cmake .. -DCMAKE_INSTALL_PREFIX=/usr\ + -DCMAKE_BUILD_TYPE=Release\ + -DWITH_BOTAN=1\ + -DWITH_LITESQL=1\ + -DWITH_LIBIDN=1\ + -DWITH_SYSTEMD=1\ + && make -j8 && make install && rm -rf /biboumi + +RUN useradd biboumi + +RUN mkdir /var/lib/biboumi +RUN chown -R biboumi:biboumi /var/lib/biboumi + +COPY ./biboumi.cfg /etc/biboumi/biboumi.cfg +RUN chown -R biboumi:biboumi /etc/biboumi + +COPY ./entrypoint.sh /entrypoint.sh +RUN chmod 755 /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] + +USER biboumi diff --git a/docker/biboumi/README.md b/docker/biboumi/README.md new file mode 100644 index 0000000..e69c1b0 --- /dev/null +++ b/docker/biboumi/README.md @@ -0,0 +1,62 @@ +Biboumi Docker Image +==================== + +Running +------- + +This image does not embed any XMPP server. You need to have a running XMPP server first: as an other docker image, or running on the host machine. + +Assuming you have a running [prosody](https://hub.docker.com/r/prosody/prosody/) container already running and [properly configured](https://prosody.im/doc/components#adding_an_external_component) you can use the following command to start your biboumi container. + +``` +docker run --link prosody:xmpp \ + -v $PWD/database:/var/lib/biboumi \ + -e BIBOUMI_PASSWORD=P4SSW0RD \ + -e BIBOUMI_HOSTNAME=irc.example.com \ + -e BIBOUMI_ADMIN=blabla \ + biboumi +``` + +If instead you already have an XMPP server running on the host machine, you can start the biboumi container like this: + +``` +docker run --network=host \ + -v $PWD/database:/var/lib/biboumi \ + -e BIBOUMI_PASSWORD=P4SSW0RD \ + -e BIBOUMI_HOSTNAME=irc.example.com \ + -e BIBOUMI_ADMIN=blabla \ + -e BIBOUMI_XMPP_SERVER_IP=127.0.0.1 \ + biboumi +``` + +Variables +--------- + +The configuration file inside the image is a template that is completed when the container is started, using the following environment variables: + +* BIBOUMI_HOSTNAME: Sets the value of the *hostname* option. +* BIBOUMI_SECRET: Sets the value of the *password* option. +* BIBOUMI_ADMIN: Sets the value of the *admin* option. +* BIBOUMI_XMPP_SERVER_IP: Sets the value of the *xmpp_server_ip* option. The default is **xmpp**. + +All these variables are optional, but biboumi will probably fail to start if the hostname and secret are missing. + +You can also directly provide your own configuration file by mounting it inside the container using the -v option: + +``` +docker run --link prosody:xmpp \ + -v $PWD/biboumi.cfg:/etc/biboumi/biboumi.cfg \ + biboumi +``` + +Linking with the XMPP server +---------------------------- + +You can use the --link option to connect to any server running in a docker container, but it needs to be called *xmpp*, or the custom value set for the **BIBOUMI_XMPP_SERVER_IP** option. For example, if you are using a container named ejabberd, you would use the option *--link ejabberd:xmpp*. + +If you want to connect to the XMPP server running on the host machine, use the **--network=host** option. + +Volumes +------- + +The database is stored in the /var/lib/biboumi/ directory. If you don’t bind a local directory to it, the database will be lost when the container is stopped. If you want to keep your database between each run, bind it with the -v option, like this: **-v /srv/biboumi/:/var/lib/biboumi**. diff --git a/docker/biboumi/biboumi.cfg b/docker/biboumi/biboumi.cfg new file mode 100644 index 0000000..cc5df61 --- /dev/null +++ b/docker/biboumi/biboumi.cfg @@ -0,0 +1,6 @@ +xmpp_server_ip=BIBOUMI_XMPP_SERVER_IP +port=5347 +db_name=/var/lib/biboumi/biboumi.sqlite +hostname=BIBOUMI_HOSTNAME +password=BIBOUMI_PASSWORD +admin=BIBOUMI_ADMIN diff --git a/docker/biboumi/entrypoint.sh b/docker/biboumi/entrypoint.sh new file mode 100644 index 0000000..eda53a4 --- /dev/null +++ b/docker/biboumi/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +sed -i s/BIBOUMI_XMPP_SERVER_IP/${BIBOUMI_XMPP_SERVER_IP:-xmpp}/ /etc/biboumi/biboumi.cfg +sed -i s/BIBOUMI_HOSTNAME/${BIBOUMI_HOSTNAME:-biboumi.localhost}/ /etc/biboumi/biboumi.cfg +sed -i s/BIBOUMI_ADMIN/${BIBOUMI_ADMIN:-}/ /etc/biboumi/biboumi.cfg +sed -i s/BIBOUMI_PASSWORD/${BIBOUMI_PASSWORD:-missing_password}/ /etc/biboumi/biboumi.cfg + +echo "Running biboumi with the following conf: " +cat /etc/biboumi/biboumi.cfg + +/usr/bin/biboumi /etc/biboumi/biboumi.cfg diff --git a/louloulibs/CMakeLists.txt b/louloulibs/CMakeLists.txt deleted file mode 100644 index 908c35f..0000000 --- a/louloulibs/CMakeLists.txt +++ /dev/null @@ -1,167 +0,0 @@ -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() - -# -## Check if we have std::get_time -# -include(CheckCXXSourceCompiles) - -check_cxx_source_compiles(" - #include <iomanip> - int main() - { std::get_time(nullptr, \"\"); }" - HAS_GET_TIME) - -mark_as_advanced(HAS_GET_TIME) - -check_cxx_source_compiles(" - #include <iomanip> - int main() - { std::put_time(nullptr, \"\"); }" - HAS_PUT_TIME) - -mark_as_advanced(HAS_PUT_TIME) - -configure_file(${CMAKE_CURRENT_SOURCE_DIR}/louloulibs.h.cmake ${CMAKE_BINARY_DIR}/src/louloulibs.h) diff --git a/louloulibs/cmake/Modules/FindCARES.cmake b/louloulibs/cmake/Modules/FindCARES.cmake deleted file mode 100644 index c4c757a..0000000 --- a/louloulibs/cmake/Modules/FindCARES.cmake +++ /dev/null @@ -1,37 +0,0 @@ -# - 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/louloulibs.h.cmake b/louloulibs/louloulibs.h.cmake deleted file mode 100644 index 6131b70..0000000 --- a/louloulibs/louloulibs.h.cmake +++ /dev/null @@ -1,11 +0,0 @@ -#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}" -#cmakedefine HAS_GET_TIME -#cmakedefine HAS_PUT_TIME diff --git a/louloulibs/network/dns_handler.cpp b/louloulibs/network/dns_handler.cpp deleted file mode 100644 index fef0cfc..0000000 --- a/louloulibs/network/dns_handler.cpp +++ /dev/null @@ -1,130 +0,0 @@ -#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) -{ - ::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 deleted file mode 100644 index fd1729d..0000000 --- a/louloulibs/network/dns_handler.hpp +++ /dev/null @@ -1,58 +0,0 @@ -#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 deleted file mode 100644 index 403a5be..0000000 --- a/louloulibs/network/dns_socket_handler.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#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() -{ - if (this->poller->is_managing_socket(this->socket)) - 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 deleted file mode 100644 index 0570196..0000000 --- a/louloulibs/network/dns_socket_handler.hpp +++ /dev/null @@ -1,49 +0,0 @@ -#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/resolver.cpp b/louloulibs/network/resolver.cpp deleted file mode 100644 index 2987aaa..0000000 --- a/louloulibs/network/resolver.cpp +++ /dev/null @@ -1,216 +0,0 @@ -#include <network/dns_handler.hpp> -#include <network/resolver.hpp> -#include <string.h> -#include <arpa/inet.h> -#include <cstdlib> - -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* ai_addr = new struct sockaddr_in; - - ai_addr->sin_family = hostent->h_addrtype; - ai_addr->sin_port = htons(std::strtoul(this->port.data(), nullptr, 10)); - ai_addr->sin_addr.s_addr = (*address)->s_addr; - - current->ai_addr = reinterpret_cast<struct sockaddr*>(ai_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* ai_addr = new struct sockaddr_in6; - ai_addr->sin6_family = hostent->h_addrtype; - ai_addr->sin6_port = htons(std::strtoul(this->port.data(), nullptr, 10)); - ::memcpy(ai_addr->sin6_addr.s6_addr, (*address)->s6_addr, sizeof(ai_addr->sin6_addr.s6_addr)); - ai_addr->sin6_flowinfo = 0; - ai_addr->sin6_scope_id = 0; - - current->ai_addr = reinterpret_cast<struct sockaddr*>(ai_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/utils/sha1.cpp b/louloulibs/utils/sha1.cpp deleted file mode 100644 index f75bc2a..0000000 --- a/louloulibs/utils/sha1.cpp +++ /dev/null @@ -1,121 +0,0 @@ -/* 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; -} diff --git a/louloulibs/utils/sha1.hpp b/louloulibs/utils/sha1.hpp deleted file mode 100644 index d436782..0000000 --- a/louloulibs/utils/sha1.hpp +++ /dev/null @@ -1,33 +0,0 @@ -/* 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); diff --git a/louloulibs/xmpp/auth.cpp b/louloulibs/xmpp/auth.cpp deleted file mode 100644 index c20f95d..0000000 --- a/louloulibs/xmpp/auth.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include <xmpp/auth.hpp> - -#include <utils/sha1.hpp> - -#include <iomanip> -#include <sstream> - -std::string get_handshake_digest(const std::string& stream_id, const std::string& secret) -{ - sha1nfo sha1; - sha1_init(&sha1); - sha1_write(&sha1, stream_id.data(), stream_id.size()); - sha1_write(&sha1, secret.data(), secret.size()); - const uint8_t* result = sha1_result(&sha1); - - std::ostringstream digest; - for (int i = 0; i < HASH_LENGTH; i++) - digest << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(result[i]); - - return digest.str(); -} diff --git a/louloulibs/xmpp/jid.cpp b/louloulibs/xmpp/jid.cpp deleted file mode 100644 index 7b62f3e..0000000 --- a/louloulibs/xmpp/jid.cpp +++ /dev/null @@ -1,99 +0,0 @@ -#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/packaging/biboumi.spec.cmake b/packaging/biboumi.spec.cmake index c180180..55d7631 100644 --- a/packaging/biboumi.spec.cmake +++ b/packaging/biboumi.spec.cmake @@ -59,8 +59,8 @@ make check %{?_smp_mflags} %changelog -* Tue Mar 21 2017 Le Coz Florent <louiz@louiz.org> - 4.1-1 -- Update to 4.1 sources: compatibility with botan 2.0 +* ${RPM_DATE} Le Coz Florent <louiz@louiz.org> - ${RPM_VERSION}-1 +- Build latest git revision * Wed Nov 9 2016 Le Coz Florent <louiz@louiz.org> - 4.0-1 - Update to 4.0 sources diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index a0ecc6e..0d6ade3 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -1,4 +1,5 @@ #include <bridge/bridge.hpp> +#include <utility> #include <xmpp/biboumi_component.hpp> #include <network/poller.hpp> #include <utils/empty_if_fixed_server.hpp> @@ -11,6 +12,7 @@ #include <database/database.hpp> #include "result_set_management.hpp" #include <algorithm> +#include <utils/timed_events.hpp> using namespace std::string_literals; @@ -28,8 +30,8 @@ static std::string in_encoding_for(const Bridge& bridge, const Iid& iid) #endif } -Bridge::Bridge(const std::string& user_jid, BiboumiComponent& xmpp, std::shared_ptr<Poller> poller): - user_jid(user_jid), +Bridge::Bridge(std::string user_jid, BiboumiComponent& xmpp, std::shared_ptr<Poller>& poller): + user_jid(std::move(user_jid)), xmpp(xmpp), poller(poller) { @@ -58,10 +60,10 @@ static std::tuple<std::string, std::string> get_role_affiliation_from_irc_mode(c void Bridge::shutdown(const std::string& exit_message) { - for (auto it = this->irc_clients.begin(); it != this->irc_clients.end(); ++it) + for (auto& pair: this->irc_clients) { - it->second->send_quit_command(exit_message); - it->second->leave_dummy_channel(exit_message); + pair.second->send_quit_command(exit_message); + pair.second->leave_dummy_channel(exit_message, {}); } } @@ -167,7 +169,8 @@ IrcClient* Bridge::find_irc_client(const std::string& hostname) const 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(); + const auto& hostname = iid.get_server(); + this->cancel_linger_timer(hostname); 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); @@ -263,9 +266,11 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body) } } -void Bridge::forward_affiliation_role_change(const Iid& iid, const std::string& nick, +void Bridge::forward_affiliation_role_change(const Iid& iid, const std::string& from, + const std::string& nick, const std::string& affiliation, - const std::string& role) + const std::string& role, + const std::string& id) { IrcClient* irc = this->get_irc_client(iid.get_server()); IrcChannel* chan = irc->get_channel(iid.get_local()); @@ -273,7 +278,11 @@ void Bridge::forward_affiliation_role_change(const Iid& iid, const std::string& return; IrcUser* user = chan->find_user(nick); if (!user) - return; + { + this->xmpp.send_stanza_error("iq", from, std::to_string(iid), id, "cancel", + "item-not-found", "no such nick", false); + 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 @@ -325,6 +334,56 @@ void Bridge::forward_affiliation_role_change(const Iid& iid, const std::string& std::vector<std::string> args(nb, nick); args.insert(args.begin(), modes); irc->send_mode_command(iid.get_local(), args); + + irc_responder_callback_t cb = [this, iid, irc, id, from, nick](const std::string& irc_hostname, const IrcMessage& message) -> bool + { + if (irc_hostname != iid.get_server()) + return false; + + if (message.command == "MODE" && message.arguments.size() >= 2) + { + const std::string& chan_name = message.arguments[0]; + if (chan_name != iid.get_local()) + return false; + const std::string actor_nick = IrcUser{message.prefix}.nick; + if (!irc || irc->get_own_nick() != actor_nick) + return false; + + this->xmpp.send_iq_result(id, from, std::to_string(iid)); + } + else if (message.command == "401" && message.arguments.size() >= 2) + { + const std::string target_later = message.arguments[1]; + if (target_later != nick) + 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", from, std::to_string(iid), 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", from, std::to_string(iid), id, "cancel", "not-allowed", + error_message, false); + } + else if (message.command == "472" && message.arguments.size() >= 2) + { + std::string error_message = "Unknown mode: "s + message.arguments[1]; + if (message.arguments.size() >= 3) + error_message = message.arguments[2]; + this->xmpp.send_stanza_error("iq", from, std::to_string(iid), id, "cancel", "not-allowed", + error_message, false); + } + return true; + }; + this->add_waiting_irc(std::move(cb)); } void Bridge::send_private_message(const Iid& iid, const std::string& body, const std::string& type) @@ -364,37 +423,56 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con if (!this->is_resource_in_chan(key, resource)) return ; + IrcChannel* channel = irc->get_channel(iid.get_local()); + auto nick = channel->get_self()->nick; + const auto resources = this->number_of_resources_in_chan(key); if (resources == 1) { // Do not send a PART message if we actually are not in that channel // or if we already sent a PART but we are just waiting for the // acknowledgment from the server - IrcChannel* channel = irc->get_channel(iid.get_local()); - if (channel->joined && !channel->parting) - irc->send_part_command(iid.get_local(), status_message); + bool persistent = false; +#ifdef USE_DATABASE + const auto coptions = Database::get_irc_channel_options_with_server_default(this->user_jid, + iid.get_server(), iid.get_local()); + persistent = coptions.persistent.value(); +#endif + if (channel->joined && !channel->parting && !persistent) + { + const auto& chan_name = iid.get_local(); + if (chan_name.empty()) + irc->leave_dummy_channel(status_message, resource); + else + irc->send_part_command(iid.get_local(), status_message); + } + else + { + this->send_muc_leave(iid, std::move(nick), "", true, resource); + } // 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); - } + if (channel) + this->send_muc_leave(iid, std::move(nick), + "Biboumi note: "s + std::to_string(resources - 1) + " resources are still in this channel.", + true, resource); + this->remove_resource_from_chan(key, 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) +void Bridge::send_irc_nick_change(const Iid& iid, const std::string& new_nick, const std::string& requesting_resource) { + // We don’t change the nick if the presence was sent to a channel the resource is not in. + auto res_in_chan = this->is_resource_in_chan(ChannelKey{iid.get_local(), iid.get_server()}, requesting_resource); + if (!res_in_chan) + return; IrcClient* irc = this->get_irc_client(iid.get_server()); irc->send_nick_command(new_nick); } @@ -793,19 +871,24 @@ void Bridge::send_presence_error(const Iid& iid, const std::string& nick, 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, +void Bridge::send_muc_leave(const 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->xmpp.send_muc_leave(std::to_string(iid), 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); + { + for (const auto &res: this->resources_in_chan[iid.to_tuple()]) + this->xmpp.send_muc_leave(std::to_string(iid), nick, this->make_xmpp_body(message), + this->user_jid + "/" + res, self); + if (self) + this->remove_all_resources_from_chan(iid.to_tuple()); + + } IrcClient* irc = this->find_irc_client(iid.get_server()); - if (irc && irc->number_of_joined_channels() == 0) - irc->send_quit_command(""); + if (self && irc && irc->number_of_joined_channels() == 0) + this->quit_or_start_linger_timer(iid.get_server()); } void Bridge::send_nick_change(Iid&& iid, @@ -968,7 +1051,7 @@ void Bridge::send_xmpp_ping_request(const std::string& nick, const std::string& 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 + // the same as 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]; @@ -1033,6 +1116,11 @@ std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_cli return this->irc_clients; } +const std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients() const +{ + return this->irc_clients; +} + std::set<char> Bridge::get_chantypes(const std::string& hostname) const { IrcClient* irc = this->find_irc_client(hostname); @@ -1070,6 +1158,11 @@ bool Bridge::is_resource_in_chan(const Bridge::ChannelKey& channel, const std::s return false; } +void Bridge::remove_all_resources_from_chan(const Bridge::ChannelKey& channel) +{ + this->resources_in_chan.erase(channel); +} + void Bridge::add_resource_to_server(const Bridge::IrcHostname& irc_hostname, const std::string& resource) { auto it = this->resources_in_server.find(irc_hostname); @@ -1099,9 +1192,9 @@ bool Bridge::is_resource_in_server(const Bridge::IrcHostname& irc_hostname, cons return false; } -std::size_t Bridge::number_of_resources_in_chan(const Bridge::ChannelKey& channel_key) const +std::size_t Bridge::number_of_resources_in_chan(const Bridge::ChannelKey& channel) const { - auto it = this->resources_in_chan.find(channel_key); + auto it = this->resources_in_chan.find(channel); if (it == this->resources_in_chan.end()) return 0; return it->second.size(); @@ -1147,3 +1240,28 @@ void Bridge::set_record_history(const bool val) this->record_history = val; } #endif + +void Bridge::quit_or_start_linger_timer(const std::string& irc_hostname) +{ +#ifdef USE_DATABASE + auto options = Database::get_irc_server_options(this->get_bare_jid(), + irc_hostname); + const auto timeout = std::chrono::seconds(options.lingerTime.value()); +#else + const auto timeout = 0s; +#endif + + const auto event_name = "IRCLINGER:" + irc_hostname + ".." + this->get_bare_jid(); + TimedEvent event(std::chrono::steady_clock::now() + timeout, [this, irc_hostname]() { + IrcClient* irc = this->find_irc_client(irc_hostname); + if (irc) + irc->send_quit_command(""); + }, event_name); + TimedEventsManager::instance().add_event(std::move(event)); +} + +void Bridge::cancel_linger_timer(const std::string& irc_hostname) +{ + const auto event_name = "IRCLINGER:" + irc_hostname + ".." + this->get_bare_jid(); + TimedEventsManager::instance().cancel(event_name); +} diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp index 18ebfeb..53d2136 100644 --- a/src/bridge/bridge.hpp +++ b/src/bridge/bridge.hpp @@ -38,7 +38,7 @@ using irc_responder_callback_t = std::function<bool(const std::string& irc_hostn class Bridge { public: - explicit Bridge(const std::string& user_jid, BiboumiComponent& xmpp, std::shared_ptr<Poller> poller); + explicit Bridge(std::string user_jid, BiboumiComponent& xmpp, std::shared_ptr<Poller>& poller); ~Bridge() = default; Bridge(const Bridge&) = delete; @@ -80,7 +80,7 @@ public: 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, const std::string& status_message, const std::string& resource); - void send_irc_nick_change(const Iid& iid, const std::string& new_nick); + void send_irc_nick_change(const Iid& iid, const std::string& new_nick, const std::string& requesting_resource); 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); @@ -103,8 +103,8 @@ public: bool send_matching_channel_list(const ChannelList& channel_list, const ResultSetInfo& rs_info, const std::string& id, const std::string& to_jid, const std::string& from); - void forward_affiliation_role_change(const Iid& iid, const std::string& nick, - const std::string& affiliation, const std::string& role); + void forward_affiliation_role_change(const Iid& iid, const std::string& from, const std::string& nick, + const std::string& affiliation, const std::string& role, const std::string& id); /** * Directly send a CTCP PING request to the IRC user */ @@ -169,7 +169,7 @@ public: /** * 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=""); + void send_muc_leave(const 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 @@ -231,10 +231,17 @@ public: */ 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(); + const std::unordered_map<std::string, std::shared_ptr<IrcClient>>& get_irc_clients() const; std::set<char> get_chantypes(const std::string& hostname) const; #ifdef USE_DATABASE void set_record_history(const bool val); #endif + /** + * Start a timer that will send a QUIT command after the + * configured linger time is expired. + */ + void quit_or_start_linger_timer(const std::string& irc_hostname); + void cancel_linger_timer(const std::string& irc_hostname); private: /** @@ -302,10 +309,11 @@ private: /** * 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_chan(const ChannelKey& channel, const std::string& resource); + void remove_resource_from_chan(const ChannelKey& channel, const std::string& resource); + bool is_resource_in_chan(const ChannelKey& channel, const std::string& resource) const; + void remove_all_resources_from_chan(const ChannelKey& channel); + std::size_t number_of_resources_in_chan(const ChannelKey& channel) 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); diff --git a/src/bridge/colors.hpp b/src/bridge/colors.hpp index e2c8a87..dceed74 100644 --- a/src/bridge/colors.hpp +++ b/src/bridge/colors.hpp @@ -51,6 +51,6 @@ static const char irc_format_char[] = { * 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); +Xmpp::body irc_format_to_xhtmlim(const std::string& s); diff --git a/louloulibs/config/config.cpp b/src/config/config.cpp index 417981d..0db5751 100644 --- a/louloulibs/config/config.cpp +++ b/src/config/config.cpp @@ -1,8 +1,7 @@ #include <config/config.hpp> -#include <logger/logger.hpp> +#include <iostream> #include <cstring> -#include <sstream> #include <cstdlib> @@ -38,7 +37,7 @@ void Config::set(const std::string& option, const std::string& value, bool save) } } -void Config::connect(t_config_changed_callback callback) +void Config::connect(const t_config_changed_callback& callback) { Config::callbacks.push_back(callback); } @@ -66,7 +65,7 @@ bool Config::read_conf(const std::string& name) std::ifstream file(Config::filename.data()); if (!file.is_open()) { - log_error("Error while opening file ", filename, " for reading: ", strerror(errno)); + std::cerr << "Error while opening file " << filename << " for reading: " << strerror(errno) << std::endl; return false; } @@ -96,7 +95,7 @@ void Config::save_to_file() std::ofstream file(Config::filename.data()); if (file.fail()) { - log_error("Could not save config file."); + std::cerr << "Could not save config file." << std::endl; return ; } for (const auto& it: Config::values) diff --git a/louloulibs/config/config.hpp b/src/config/config.hpp index 6728df8..2ba38cc 100644 --- a/louloulibs/config/config.hpp +++ b/src/config/config.hpp @@ -15,7 +15,6 @@ #pragma once - #include <functional> #include <fstream> #include <memory> @@ -55,7 +54,7 @@ public: * configuration change occurs (when set() is called, or when the initial * conf is read) */ - static void connect(t_config_changed_callback); + static void connect(const t_config_changed_callback&); /** * Destroy the instance, forcing it to be recreated (with potentially * different parameters) the next time it’s needed. diff --git a/src/database/database.cpp b/src/database/database.cpp index f7d309b..71b0c37 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -20,7 +20,7 @@ void Database::open(const std::string& filename, const std::string& db_type) "database="s + filename); if (new_db->needsUpgrade()) new_db->upgrade(); - Database::db.reset(new_db.release()); + Database::db = std::move(new_db); } catch (const litesql::DatabaseError& e) { log_error("Failed to open database ", filename, ". ", e.what()); throw; @@ -130,7 +130,7 @@ void Database::store_muc_message(const std::string& owner, const Iid& iid, line.owner = owner; line.ircChanName = iid.get_local(); line.ircServerName = iid.get_server(); - line.date = date.time_since_epoch().count() / 1'000'000'000; + line.date = std::chrono::duration_cast<std::chrono::seconds>(date.time_since_epoch()).count(); line.body = body; line.nick = nick; diff --git a/src/database/database.hpp b/src/database/database.hpp index 6823574..1665a9a 100644 --- a/src/database/database.hpp +++ b/src/database/database.hpp @@ -49,7 +49,7 @@ public: const std::string& server, const std::string& channel); static std::vector<db::MucLogLine> get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server, - int limit=-1, const std::string& before="", const std::string& after=""); + int limit=-1, const std::string& start="", const std::string& end=""); static void store_muc_message(const std::string& owner, const Iid& iid, time_point date, const std::string& body, const std::string& nick); diff --git a/src/identd/identd_server.hpp b/src/identd/identd_server.hpp new file mode 100644 index 0000000..b1c8ec8 --- /dev/null +++ b/src/identd/identd_server.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include <network/tcp_server_socket.hpp> +#include <identd/identd_socket.hpp> +#include <algorithm> +#include <unistd.h> + +class BiboumiComponent; + +class IdentdServer: public TcpSocketServer<IdentdSocket> +{ + public: + IdentdServer(const BiboumiComponent& biboumi_component, std::shared_ptr<Poller>& poller, const uint16_t port): + TcpSocketServer<IdentdSocket>(poller, port), + biboumi_component(biboumi_component) + {} + + const BiboumiComponent& get_biboumi_component() const + { + return this->biboumi_component; + } + void shutdown() + { + if (this->poller->is_managing_socket(this->socket)) + this->poller->remove_socket_handler(this->socket); + ::close(this->socket); + } + void clean() + { + this->sockets.erase(std::remove_if(this->sockets.begin(), this->sockets.end(), + [](const std::unique_ptr<IdentdSocket>& socket) + { + return socket->get_socket() == -1; + }), + this->sockets.end()); + } + private: + const BiboumiComponent& biboumi_component; +}; diff --git a/src/identd/identd_socket.cpp b/src/identd/identd_socket.cpp new file mode 100644 index 0000000..b85257c --- /dev/null +++ b/src/identd/identd_socket.cpp @@ -0,0 +1,63 @@ +#include <identd/identd_socket.hpp> +#include <identd/identd_server.hpp> +#include <xmpp/biboumi_component.hpp> +#include <sstream> +#include <iomanip> + +#include <utils/sha1.hpp> + +#include <logger/logger.hpp> + +IdentdSocket::IdentdSocket(std::shared_ptr<Poller>& poller, const socket_t socket, TcpSocketServer<IdentdSocket>& server): + TCPSocketHandler(poller), + server(dynamic_cast<IdentdServer&>(server)) +{ + this->socket = socket; +} + +void IdentdSocket::parse_in_buffer(const std::size_t) +{ + while (true) + { + const auto line_end = this->in_buf.find('\n'); + if (line_end == std::string::npos) + break; + std::istringstream line(this->in_buf.substr(0, line_end)); + this->consume_in_buffer(line_end + 1); + + uint16_t local_port; + uint16_t remote_port; + char sep; + line >> local_port >> sep >> remote_port; + const auto& xmpp = this->server.get_biboumi_component(); + auto response = this->generate_answer(xmpp, local_port, remote_port); + + this->send_data(std::move(response)); + } +} + +static std::string hash_jid(const std::string& jid) +{ + return sha1(jid); +} + +std::string IdentdSocket::generate_answer(const BiboumiComponent& biboumi, uint16_t local, uint16_t remote) +{ + for (const Bridge* bridge: biboumi.get_bridges()) + { + for (const auto& pair: bridge->get_irc_clients()) + { + if (pair.second->match_port_pairt(local, remote)) + { + std::ostringstream os; + os << local << " , " << remote << " : USERID : OTHER : " << hash_jid(bridge->get_bare_jid()); + log_debug("Identd, sending: ", os.str()); + return os.str(); + } + } + } + std::ostringstream os; + os << local << " , " << remote << " ERROR : NO-USER"; + log_debug("Identd, sending: ", os.str()); + return os.str(); +} diff --git a/src/identd/identd_socket.hpp b/src/identd/identd_socket.hpp new file mode 100644 index 0000000..10cb797 --- /dev/null +++ b/src/identd/identd_socket.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include <network/socket_handler.hpp> + +#include <cassert> +#include <network/tcp_socket_handler.hpp> + +#include <logger/logger.hpp> +#include <xmpp/biboumi_component.hpp> + +class XmppComponent; +class IdentdSocket; +class IdentdServer; +template <typename T> +class TcpSocketServer; + +class IdentdSocket: public TCPSocketHandler +{ + public: + IdentdSocket(std::shared_ptr<Poller>& poller, const socket_t socket, TcpSocketServer<IdentdSocket>& server); + ~IdentdSocket() = default; + std::string generate_answer(const BiboumiComponent& biboumi, uint16_t local, uint16_t remote); + + void parse_in_buffer(const std::size_t size) override final; + + bool is_connected() const override final + { + return true; + } + bool is_connecting() const override final + { + return false; + } + + private: + IdentdServer& server; +}; diff --git a/src/irc/iid.cpp b/src/irc/iid.cpp index d442013..6b07793 100644 --- a/src/irc/iid.cpp +++ b/src/irc/iid.cpp @@ -34,9 +34,10 @@ Iid::Iid(const std::string& iid, const Bridge *bridge) void Iid::set_type(const std::set<char>& chantypes) { + if (this->local.empty() && this->server.empty()) + this->type = Iid::Type::None; if (this->local.empty()) return; - if (chantypes.count(this->local[0]) == 1) this->type = Iid::Type::Channel; else @@ -105,6 +106,8 @@ namespace std { { if (iid.type == Iid::Type::Server) return iid.get_server(); + else if (iid.get_local().empty() && iid.get_server().empty()) + return {}; else return iid.get_encoded_local() + iid.separator + iid.get_server(); } diff --git a/src/irc/iid.hpp b/src/irc/iid.hpp index 44861c1..81cf3ca 100644 --- a/src/irc/iid.hpp +++ b/src/irc/iid.hpp @@ -53,6 +53,7 @@ public: Channel, User, Server, + None, }; static constexpr char separator[]{"%"}; Iid(const std::string& iid, const std::set<char>& chantypes); diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp index b0d3a47..48b105d 100644 --- a/src/irc/irc_client.cpp +++ b/src/irc/irc_client.cpp @@ -14,13 +14,13 @@ #include <sstream> #include <iostream> #include <stdexcept> +#include <algorithm> #include <cstring> #include <chrono> #include <string> #include "biboumi.h" -#include "louloulibs.h" using namespace std::string_literals; using namespace std::chrono_literals; @@ -66,6 +66,7 @@ static const std::unordered_map<std::string, {"433", {&IrcClient::on_nickname_conflict, {2, 0}}}, {"438", {&IrcClient::on_nickname_change_too_fast, {2, 0}}}, {"443", {&IrcClient::on_useronchannel, {3, 0}}}, + {"475", {&IrcClient::on_channel_bad_key, {3, 0}}}, {"ERR_USERONCHANNEL", {&IrcClient::on_useronchannel, {3, 0}}}, {"001", {&IrcClient::on_welcome_message, {1, 0}}}, {"PART", {&IrcClient::on_part, {1, 0}}}, @@ -113,7 +114,6 @@ static const std::unordered_map<std::string, {"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}}}, @@ -127,11 +127,11 @@ static const std::unordered_map<std::string, {"502", {&IrcClient::on_generic_error, {2, 0}}}, }; -IrcClient::IrcClient(std::shared_ptr<Poller> poller, const std::string& hostname, +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), + TCPClientSocketHandler(poller), hostname(hostname), user_hostname(user_hostname), username(username), @@ -338,7 +338,7 @@ void IrcClient::parse_in_buffer(const size_t) 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); + this->consume_in_buffer(pos + 2); log_debug("IRC RECEIVING: (", this->get_hostname(), ") ", message); // Call the standard callback (if any), associated with the command @@ -450,7 +450,12 @@ void IrcClient::send_quit_command(const std::string& 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); + { + const auto it = std::find_if(begin(this->channels_to_join), end(this->channels_to_join), + [&chan_name](const auto& pair) { return std::get<0>(pair) == chan_name; }); + if (it == end(this->channels_to_join)) + this->channels_to_join.emplace_back(chan_name, password); + } else if (password.empty()) this->send_message(IrcMessage("JOIN", {chan_name})); else @@ -496,15 +501,7 @@ void IrcClient::send_private_message(const std::string& username, const std::str 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})); - channel->parting = true; - } + 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) @@ -891,7 +888,7 @@ void IrcClient::on_part(const IrcMessage& message) // channel pointer is now invalid channel = nullptr; } - this->bridge.send_muc_leave(std::move(iid), std::move(nick), std::move(txt), self); + this->bridge.send_muc_leave(iid, std::move(nick), std::move(txt), self); } } @@ -909,7 +906,7 @@ void IrcClient::on_error(const IrcMessage& message) 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->bridge.send_muc_leave(iid, std::move(own_nick), leave_message, true); } this->channels.clear(); this->send_gateway_message("ERROR: "s + leave_message); @@ -933,7 +930,7 @@ void IrcClient::on_quit(const IrcMessage& message) iid.set_local(chan_name); iid.set_server(this->hostname); iid.type = Iid::Type::Channel; - this->bridge.send_muc_leave(std::move(iid), std::move(nick), txt, false); + this->bridge.send_muc_leave(iid, std::move(nick), txt, false); } } } @@ -1014,6 +1011,18 @@ void IrcClient::on_mode(const IrcMessage& message) this->on_user_mode(message); } +void IrcClient::on_channel_bad_key(const IrcMessage& message) +{ + this->on_generic_error(message); + const std::string& nickname = message.arguments[0]; + const std::string& channel = message.arguments[1]; + std::string text; + if (message.arguments.size() > 2) + text = message.arguments[2]; + + this->bridge.send_presence_error({channel, this->hostname, Iid::Type::Channel}, nickname, "auth", "not-authorized", "", text); +} + void IrcClient::on_channel_mode(const IrcMessage& message) { // For now, just transmit the modes so the user can know what happens @@ -1143,14 +1152,14 @@ DummyIrcChannel& IrcClient::get_dummy_channel() return this->dummy_channel; } -void IrcClient::leave_dummy_channel(const std::string& exit_message) +void IrcClient::leave_dummy_channel(const std::string& exit_message, const std::string& resource) { 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, this->chantypes), std::string(this->current_nick), exit_message, true); + this->bridge.send_muc_leave(Iid("%"s + this->hostname, this->chantypes), std::string(this->current_nick), exit_message, true, resource); } #ifdef BOTAN_FOUND diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp index 1b4d892..8119201 100644 --- a/src/irc/irc_client.hpp +++ b/src/irc/irc_client.hpp @@ -5,7 +5,7 @@ #include <irc/irc_channel.hpp> #include <irc/iid.hpp> -#include <network/tcp_socket_handler.hpp> +#include <network/tcp_client_socket_handler.hpp> #include <network/resolver.hpp> #include <unordered_map> @@ -23,10 +23,10 @@ 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 +class IrcClient: public TCPClientSocketHandler { public: - explicit IrcClient(std::shared_ptr<Poller> poller, const std::string& hostname, + 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); @@ -52,7 +52,7 @@ public: /** * Close the connection, remove us from the poller */ - void on_connection_close(const std::string& error) override final; + void on_connection_close(const std::string& error_msg) override final; /** * Parse the data we have received so far and try to get one or more * complete messages from it. @@ -257,6 +257,7 @@ public: void on_nick(const IrcMessage& message); void on_kick(const IrcMessage& message); void on_mode(const IrcMessage& message); + void on_channel_bad_key(const IrcMessage& message); /** * A mode towards our own user is received (note, that is different from a * channel mode towards or own nick, see @@ -282,7 +283,7 @@ public: * 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); + void leave_dummy_channel(const std::string& exit_message, const std::string& resource); const std::string& get_hostname() const { return this->hostname; } std::string get_nick() const { return this->current_nick; } diff --git a/src/irc/irc_message.cpp b/src/irc/irc_message.cpp index 966a47c..14fdb0e 100644 --- a/src/irc/irc_message.cpp +++ b/src/irc/irc_message.cpp @@ -8,12 +8,12 @@ IrcMessage::IrcMessage(std::string&& line) // optional prefix if (line[0] == ':') { - pos = line.find(" "); + pos = line.find(' '); this->prefix = line.substr(1, pos - 1); line = line.substr(pos + 1, std::string::npos); } // command - pos = line.find(" "); + pos = line.find(' '); this->command = line.substr(0, pos); line = line.substr(pos + 1, std::string::npos); // arguments @@ -24,7 +24,7 @@ IrcMessage::IrcMessage(std::string&& line) this->arguments.emplace_back(line.substr(1, std::string::npos)); break ; } - pos = line.find(" "); + pos = line.find(' '); this->arguments.emplace_back(line.substr(0, pos)); line = line.substr(pos + 1, std::string::npos); } while (pos != std::string::npos); diff --git a/src/irc/irc_user.cpp b/src/irc/irc_user.cpp index 9fa3612..139015e 100644 --- a/src/irc/irc_user.cpp +++ b/src/irc/irc_user.cpp @@ -21,7 +21,7 @@ IrcUser::IrcUser(const std::string& name, name_begin++; } - const std::string::size_type sep = name.find("!", name_begin); + const std::string::size_type sep = name.find('!', name_begin); if (sep == std::string::npos) this->nick = name.substr(name_begin); else diff --git a/src/irc/irc_user.hpp b/src/irc/irc_user.hpp index c84030e..a4291d4 100644 --- a/src/irc/irc_user.hpp +++ b/src/irc/irc_user.hpp @@ -23,7 +23,7 @@ public: 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; + char get_most_significant_mode(const std::vector<char>& modes) const; std::string nick; std::string host; diff --git a/louloulibs/logger/logger.cpp b/src/logger/logger.cpp index 7336579..92a3d9b 100644 --- a/louloulibs/logger/logger.cpp +++ b/src/logger/logger.cpp @@ -3,14 +3,18 @@ Logger::Logger(const int log_level): log_level(log_level), - stream(std::cout.rdbuf()) + stream(std::cout.rdbuf()), + null_buffer{}, + null_stream{&null_buffer} { } 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()) + stream(ofstream.rdbuf()), + null_buffer{}, + null_stream{&null_buffer} { } diff --git a/louloulibs/logger/logger.hpp b/src/logger/logger.hpp index 0893c77..ff6a82b 100644 --- a/louloulibs/logger/logger.hpp +++ b/src/logger/logger.hpp @@ -17,7 +17,7 @@ #define warning_lvl 2 #define error_lvl 3 -#include "louloulibs.h" +#include "biboumi.h" #ifdef SYSTEMD_FOUND # include <systemd/sd-daemon.h> #else @@ -33,15 +33,15 @@ # define __FILENAME__ __FILE__ #endif + /** - * Juste a structure representing a stream doing nothing with its input. + * A buffer, used to construct an ostream that does nothing + * when we output data in it */ -class nullstream: public std::ostream +class NullBuffer: public std::streambuf { -public: - nullstream(): - std::ostream(0) - { } + public: + int overflow(int c) { return c; } }; class Logger @@ -59,9 +59,11 @@ public: private: const int log_level; - std::ofstream ofstream; - nullstream null_stream; + std::ofstream ofstream{}; std::ostream stream; + + NullBuffer null_buffer; + std::ostream null_stream; }; #define WHERE __FILENAME__, ":", __LINE__, ":\t" diff --git a/src/main.cpp b/src/main.cpp index 019dff0..76ab5d9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,13 +6,17 @@ #include <utils/xdg.hpp> #include <utils/reload.hpp> -#ifdef CARES_FOUND +#ifdef UDNS_FOUND # include <network/dns_handler.hpp> #endif #include <atomic> #include <signal.h> -#include <litesql.hpp> +#ifdef USE_DATABASE +# include <litesql.hpp> +#endif + +#include <identd/identd_server.hpp> // A flag set by the SIGINT signal handler. static std::atomic<bool> stop(false); @@ -83,11 +87,14 @@ int main(int ac, char** av) if (hostname.empty()) return config_help("hostname"); + +#ifdef USE_DATABASE try { - open_database(); - } catch (const litesql::DatabaseError&) { - return 1; - } + open_database(); + } catch (const litesql::DatabaseError&) { + return 1; + } +#endif // Block the signals we want to manage. They will be unblocked only during // the epoll_pwait or ppoll calls. This avoids some race conditions, @@ -121,13 +128,17 @@ int main(int ac, char** av) sigaction(SIGUSR2, &on_sigusr, nullptr); auto p = std::make_shared<Poller>(); + +#ifdef UDNS_FOUND + DNSHandler dns_handler(p); +#endif + auto xmpp_component = std::make_shared<BiboumiComponent>(p, hostname, password); xmpp_component->start(); -#ifdef CARES_FOUND - DNSHandler::instance.watch_dns_sockets(p); -#endif + IdentdServer identd(*xmpp_component, p, static_cast<uint16_t>(Config::get_int("identd_port", 113))); + auto timeout = TimedEventsManager::instance().get_timeout(); while (p->poll(timeout) != -1) { @@ -135,6 +146,7 @@ int main(int ac, char** av) // Check for empty irc_clients (not connected, or with no joined // channel) and remove them xmpp_component->clean(); + identd.clean(); if (stop) { log_info("Signal received, exiting..."); @@ -144,6 +156,10 @@ int main(int ac, char** av) exiting = true; stop.store(false); xmpp_component->shutdown(); +#ifdef UDNS_FOUND + dns_handler.destroy(); +#endif + identd.shutdown(); // Cancel the timer for a potential reconnection TimedEventsManager::instance().cancel("XMPP reconnection"); } @@ -157,26 +173,35 @@ int main(int ac, char** av) // 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 && + if (!exiting && !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(); - } + if (xmpp_component->ever_auth) + { + 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)); + } + } 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)); - } +#ifdef UDNS_FOUND + dns_handler.destroy(); +#endif + identd.shutdown(); + } } // If the only existing connection is the one to the XMPP component: // close the XMPP stream. @@ -184,18 +209,11 @@ int main(int ac, char** av) 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."); diff --git a/louloulibs/network/credentials_manager.cpp b/src/network/credentials_manager.cpp index 289307b..f9f8c94 100644 --- a/louloulibs/network/credentials_manager.cpp +++ b/src/network/credentials_manager.cpp @@ -1,4 +1,4 @@ -#include "louloulibs.h" +#include "biboumi.h" #ifdef BOTAN_FOUND #include <network/tcp_socket_handler.hpp> diff --git a/louloulibs/network/credentials_manager.hpp b/src/network/credentials_manager.hpp index 9f42782..c463ad4 100644 --- a/louloulibs/network/credentials_manager.hpp +++ b/src/network/credentials_manager.hpp @@ -1,12 +1,11 @@ #pragma once -#include "louloulibs.h" +#include "biboumi.h" #ifdef BOTAN_FOUND #include <botan/botan.h> #include <botan/tls_client.h> -#include <botan/version.h> class TCPSocketHandler; diff --git a/src/network/dns_handler.cpp b/src/network/dns_handler.cpp new file mode 100644 index 0000000..7f0c96a --- /dev/null +++ b/src/network/dns_handler.cpp @@ -0,0 +1,46 @@ +#include <biboumi.h> +#ifdef UDNS_FOUND + +#include <network/dns_socket_handler.hpp> +#include <network/dns_handler.hpp> +#include <network/poller.hpp> + +#include <utils/timed_events.hpp> + +#include <udns.h> +#include <cerrno> +#include <cstring> + +class Resolver; + +using namespace std::string_literals; + +std::unique_ptr<DNSSocketHandler> DNSHandler::socket_handler{}; + +DNSHandler::DNSHandler(std::shared_ptr<Poller>& poller) +{ + dns_init(nullptr, 0); + const auto socket = dns_open(nullptr); + if (socket == -1) + throw std::runtime_error("Failed to initialize udns socket: "s + strerror(errno)); + + DNSHandler::socket_handler = std::make_unique<DNSSocketHandler>(poller, socket); +} + +void DNSHandler::destroy() +{ + DNSHandler::socket_handler.reset(nullptr); + dns_close(nullptr); +} + +void DNSHandler::watch() +{ + DNSHandler::socket_handler->watch(); +} + +void DNSHandler::unwatch() +{ + DNSHandler::socket_handler->unwatch(); +} + +#endif /* UDNS_FOUND */ diff --git a/src/network/dns_handler.hpp b/src/network/dns_handler.hpp new file mode 100644 index 0000000..c694452 --- /dev/null +++ b/src/network/dns_handler.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include <biboumi.h> +#ifdef UDNS_FOUND + +class Poller; + +#include <network/dns_socket_handler.hpp> + +#include <string> +#include <vector> +#include <memory> + +class DNSHandler +{ +public: + explicit DNSHandler(std::shared_ptr<Poller>& poller); + ~DNSHandler() = default; + + DNSHandler(const DNSHandler&) = delete; + DNSHandler(DNSHandler&&) = delete; + DNSHandler& operator=(const DNSHandler&) = delete; + DNSHandler& operator=(DNSHandler&&) = delete; + + void destroy(); + + static void watch(); + static void unwatch(); + +private: + /** + * Manager for the socket returned by udns, that we need to watch with the poller + */ + static std::unique_ptr<DNSSocketHandler> socket_handler; +}; + +#endif /* UDNS_FOUND */ diff --git a/src/network/dns_socket_handler.cpp b/src/network/dns_socket_handler.cpp new file mode 100644 index 0000000..5c286c4 --- /dev/null +++ b/src/network/dns_socket_handler.cpp @@ -0,0 +1,43 @@ +#include <biboumi.h> +#ifdef UDNS_FOUND + +#include <network/dns_socket_handler.hpp> +#include <network/dns_handler.hpp> +#include <network/poller.hpp> + +#include <udns.h> + +DNSSocketHandler::DNSSocketHandler(std::shared_ptr<Poller>& poller, + const socket_t socket): + SocketHandler(poller, socket) +{ + poller->add_socket_handler(this); +} + +DNSSocketHandler::~DNSSocketHandler() +{ + this->unwatch(); +} + +void DNSSocketHandler::on_recv() +{ + dns_ioevent(nullptr, 0); +} + +bool DNSSocketHandler::is_connected() const +{ + return true; +} + +void DNSSocketHandler::unwatch() +{ + if (this->poller->is_managing_socket(this->socket)) + this->poller->remove_socket_handler(this->socket); +} + +void DNSSocketHandler::watch() +{ + this->poller->add_socket_handler(this); +} + +#endif /* UDNS_FOUND */ diff --git a/src/network/dns_socket_handler.hpp b/src/network/dns_socket_handler.hpp new file mode 100644 index 0000000..6e83e87 --- /dev/null +++ b/src/network/dns_socket_handler.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include <biboumi.h> +#ifdef UDNS_FOUND + +#include <network/socket_handler.hpp> + +/** + * Manage the UDP socket provided by udns, we do not create, open or close the + * socket ourself: this is done by udns. We only watch it for readability + */ +class DNSSocketHandler: public SocketHandler +{ +public: + explicit DNSSocketHandler(std::shared_ptr<Poller>& poller, const socket_t socket); + ~DNSSocketHandler(); + DNSSocketHandler(const DNSSocketHandler&) = delete; + DNSSocketHandler(DNSSocketHandler&&) = delete; + DNSSocketHandler& operator=(const DNSSocketHandler&) = delete; + DNSSocketHandler& operator=(DNSSocketHandler&&) = delete; + + void on_recv() override final; + + /** + * Always true, see the comment for connect() + */ + bool is_connected() const override final; + + void watch(); + void unwatch(); +}; + +#endif // UDNS_FOUND diff --git a/louloulibs/network/poller.cpp b/src/network/poller.cpp index 9f5bcfb..9f5bcfb 100644 --- a/louloulibs/network/poller.cpp +++ b/src/network/poller.cpp diff --git a/louloulibs/network/poller.hpp b/src/network/poller.hpp index e39e438..3cc2710 100644 --- a/louloulibs/network/poller.hpp +++ b/src/network/poller.hpp @@ -10,7 +10,7 @@ #define POLL 1 #define EPOLL 2 #define KQUEUE 3 -#include <louloulibs.h> +#include <biboumi.h> #ifndef POLLER #define POLLER POLL #endif diff --git a/src/network/resolver.cpp b/src/network/resolver.cpp new file mode 100644 index 0000000..ef54ba2 --- /dev/null +++ b/src/network/resolver.cpp @@ -0,0 +1,280 @@ +#include <network/dns_handler.hpp> +#include <utils/timed_events.hpp> +#include <network/resolver.hpp> +#include <cstring> +#include <arpa/inet.h> +#include <netinet/in.h> +#ifdef UDNS_FOUND +# include <udns.h> +#endif + +#include <fstream> +#include <cstdlib> +#include <sstream> +#include <chrono> +#include <map> + +using namespace std::string_literals; + +#ifdef UDNS_FOUND +static std::map<int, std::string> dns_error_messages { + {DNS_E_TEMPFAIL, "Timeout while contacting DNS servers"}, + {DNS_E_PROTOCOL, "Misformatted DNS reply"}, + {DNS_E_NXDOMAIN, "Domain name not found"}, + {DNS_E_NOMEM, "Out of memory"}, + {DNS_E_BADQUERY, "Misformatted domain name"} +}; +#endif + +Resolver::Resolver(): +#ifdef UDNS_FOUND + resolved4(false), + resolved6(false), + resolving(false), + 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 = std::move(error_cb); + this->success_cb = std::move(success_cb); +#ifdef UDNS_FOUND + this->port = port; +#endif + + this->start_resolving(hostname, port); +} + +int Resolver::call_getaddrinfo(const char *name, const char* port, int flags) +{ + struct addrinfo hints{}; + hints.ai_flags = flags; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = 0; + + struct addrinfo* addr_res = nullptr; + const int res = ::getaddrinfo(name, port, + &hints, &addr_res); + + if (res == 0 && addr_res) + { + if (!this->addr) + this->addr.reset(addr_res); + else + { // Append this result at the end of the linked list + struct addrinfo *rp = this->addr.get(); + while (rp->ai_next) + rp = rp->ai_next; + rp->ai_next = addr_res; + } + } + + return res; +} + +#ifdef UDNS_FOUND +void Resolver::start_resolving(const std::string& hostname, const std::string& port) +{ + this->resolving = true; + this->resolved = false; + this->resolved4 = false; + this->resolved6 = false; + + this->error_msg.clear(); + this->addr.reset(nullptr); + + // We first try to use it as an IP address directly. We tell getaddrinfo + // to NOT use any DNS resolution. + if (this->call_getaddrinfo(hostname.data(), port.data(), AI_NUMERICHOST) == 0) + { + this->on_resolved(); + return; + } + + // Then we look into /etc/hosts to translate the given hostname + const auto hosts = this->look_in_etc_hosts(hostname); + if (!hosts.empty()) + { + for (const auto &host: hosts) + this->call_getaddrinfo(host.data(), port.data(), AI_NUMERICHOST); + this->on_resolved(); + return; + } + + // And finally, we try a DNS resolution + auto hostname6_resolved = [](dns_ctx*, dns_rr_a6* result, void* data) + { + auto resolver = static_cast<Resolver*>(data); + resolver->on_hostname6_resolved(result); + resolver->after_resolved(); + std::free(result); + }; + + auto hostname4_resolved = [](dns_ctx*, dns_rr_a4* result, void* data) + { + auto resolver = static_cast<Resolver*>(data); + resolver->on_hostname4_resolved(result); + resolver->after_resolved(); + std::free(result); + }; + + DNSHandler::watch(); + auto res = dns_submit_a4(nullptr, hostname.data(), 0, hostname4_resolved, this); + if (!res) + this->on_hostname4_resolved(nullptr); + res = dns_submit_a6(nullptr, hostname.data(), 0, hostname6_resolved, this); + if (!res) + this->on_hostname6_resolved(nullptr); + + this->start_timer(); +} + +void Resolver::start_timer() +{ + const auto timeout = dns_timeouts(nullptr, -1, 0); + if (timeout < 0) + return; + TimedEvent event(std::chrono::steady_clock::now() + std::chrono::seconds(timeout), [this]() { this->start_timer(); }, "DNS"); + TimedEventsManager::instance().add_event(std::move(event)); +} + +std::vector<std::string> Resolver::look_in_etc_hosts(const std::string &hostname) +{ + std::ifstream hosts("/etc/hosts"); + std::string line; + + std::vector<std::string> results; + while (std::getline(hosts, line)) + { + if (line.empty()) + continue; + + std::string ip; + std::istringstream line_stream(line); + line_stream >> ip; + if (ip.empty() || ip[0] == '#') + continue; + + std::string host; + while (line_stream >> host && !host.empty() && host[0] != '#') + { + if (hostname == host) + { + results.push_back(ip); + break; + } + } + } + return results; +} + +void Resolver::on_hostname4_resolved(dns_rr_a4 *result) +{ + this->resolved4 = true; + + const auto status = dns_status(nullptr); + + if (status >= 0 && result) + { + char buf[INET6_ADDRSTRLEN]; + + for (auto i = 0; i < result->dnsa4_nrr; ++i) + { + inet_ntop(AF_INET, &result->dnsa4_addr[i], buf, sizeof(buf)); + this->call_getaddrinfo(buf, this->port.data(), AI_NUMERICHOST); + } + } + else + { + const auto error = dns_error_messages.find(status); + if (error != end(dns_error_messages)) + this->error_msg = error->second; + } +} + +void Resolver::on_hostname6_resolved(dns_rr_a6 *result) +{ + this->resolved6 = true; + + const auto status = dns_status(nullptr); + + if (status >= 0 && result) + { + char buf[INET6_ADDRSTRLEN]; + for (auto i = 0; i < result->dnsa6_nrr; ++i) + { + inet_ntop(AF_INET6, &result->dnsa6_addr[i], buf, sizeof(buf)); + this->call_getaddrinfo(buf, this->port.data(), AI_NUMERICHOST); + } + } +} + +void Resolver::after_resolved() +{ + if (dns_active(nullptr) == 0) + DNSHandler::unwatch(); + + if (this->resolved6 && this->resolved4) + this->on_resolved(); +} + +void Resolver::on_resolved() +{ + this->resolved = true; + this->resolving = false; + if (!this->addr) + { + if (this->error_cb) + this->error_cb(this->error_msg.data()); + } + else + { + if (this->success_cb) + this->success_cb(this->addr.get()); + } +} + +#else // ifdef UDNS_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); + + const auto res = this->call_getaddrinfo(hostname.data(), port.data(), 0); + + this->resolved = true; + + if (res != 0) + { + this->error_msg = gai_strerror(res); + if (this->error_cb) + this->error_cb(this->error_msg.data()); + } + else + { + if (this->success_cb) + this->success_cb(this->addr.get()); + } +} +#endif // ifdef UDNS_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/src/network/resolver.hpp index 29e6f3a..f65ff86 100644 --- a/louloulibs/network/resolver.hpp +++ b/src/network/resolver.hpp @@ -1,38 +1,33 @@ #pragma once - -#include "louloulibs.h" +#include "biboumi.h" #include <functional> +#include <vector> #include <memory> #include <string> #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> +#ifdef UDNS_FOUND +# include <udns.h> +#endif class AddrinfoDeleter { public: 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*)>; @@ -45,7 +40,7 @@ public: bool is_resolving() const { -#ifdef CARES_FOUND +#ifdef UDNS_FOUND return this->resolving; #else return false; @@ -68,11 +63,10 @@ public: void clear() { -#ifdef CARES_FOUND +#ifdef UDNS_FOUND this->resolved6 = false; this->resolved4 = false; this->resolving = false; - this->cares_addrinfo = nullptr; this->port.clear(); #endif this->resolved = false; @@ -85,12 +79,22 @@ public: 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); + std::vector<std::string> look_in_etc_hosts(const std::string& hostname); + /** + * Call getaddrinfo() on the given hostname or IP, and append the result + * to our internal addrinfo list. Return getaddrinfo()’s return value. + */ + int call_getaddrinfo(const char* name, const char* port, int flags); - void fill_ares_addrinfo4(const struct hostent* hostent); - void fill_ares_addrinfo6(const struct hostent* hostent); +#ifdef UDNS_FOUND + void on_hostname4_resolved(dns_rr_a4 *result); + void on_hostname6_resolved(dns_rr_a6 *result); + /** + * Called after one record (4 or 6) has been resolved. + */ + void after_resolved(); + + void start_timer(); void on_resolved(); @@ -99,14 +103,6 @@ private: 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 @@ -117,7 +113,6 @@ private: bool resolved; std::string error_msg; - std::unique_ptr<struct addrinfo, AddrinfoDeleter> addr; ErrorCallbackType error_cb; @@ -125,5 +120,3 @@ private: }; std::string addr_to_string(const struct addrinfo* rp); - - diff --git a/louloulibs/network/socket_handler.hpp b/src/network/socket_handler.hpp index ea79a18..181a6c0 100644 --- a/louloulibs/network/socket_handler.hpp +++ b/src/network/socket_handler.hpp @@ -1,6 +1,6 @@ #pragma once -#include <louloulibs.h> +#include <biboumi.h> #include <memory> class Poller; @@ -10,7 +10,7 @@ using socket_t = int; class SocketHandler { public: - explicit SocketHandler(std::shared_ptr<Poller> poller, const socket_t socket): + explicit SocketHandler(std::shared_ptr<Poller>& poller, const socket_t socket): poller(poller), socket(socket) {} @@ -20,9 +20,9 @@ public: SocketHandler& operator=(const SocketHandler&) = delete; SocketHandler& operator=(SocketHandler&&) = delete; - virtual void on_recv() = 0; - virtual void on_send() = 0; - virtual void connect() = 0; + virtual void on_recv() {} + virtual void on_send() {} + virtual void connect() {} virtual bool is_connected() const = 0; socket_t get_socket() const diff --git a/src/network/tcp_client_socket_handler.cpp b/src/network/tcp_client_socket_handler.cpp new file mode 100644 index 0000000..4628703 --- /dev/null +++ b/src/network/tcp_client_socket_handler.cpp @@ -0,0 +1,261 @@ +#include <network/tcp_client_socket_handler.hpp> +#include <utils/timed_events.hpp> +#include <utils/scopeguard.hpp> +#include <network/poller.hpp> + +#include <logger/logger.hpp> + +#include <cstring> +#include <unistd.h> +#include <fcntl.h> + +using namespace std::string_literals; + +TCPClientSocketHandler::TCPClientSocketHandler(std::shared_ptr<Poller>& poller): + TCPSocketHandler(poller), + hostname_resolution_failed(false), + connected(false), + connecting(false) +{} + +TCPClientSocketHandler::~TCPClientSocketHandler() +{ + this->close(); +} + +void TCPClientSocketHandler::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 + std::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; + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_flags = AI_NUMERICHOST; + hints.ai_family = AF_UNSPEC; + int err = ::getaddrinfo(this->bind_addr.data(), nullptr, &hints, &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; + for (rp = result; rp; rp = rp->ai_next) + { + if ((::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(errno)); + 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 + std::strerror(errno)); +} + +void TCPClientSocketHandler::connect(const std::string& address, const std::string& port, const bool tls) +{ + this->address = address; + this->port = port; + this->use_tls = tls; + + struct addrinfo* addr_res; + + if (!this->connecting) + { + // Get the addrinfo from getaddrinfo (or using udns), 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), TCPClientSocketHandler::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 DNS resolver resolved the hostname and the available addresses + // where saved in the 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(this->address, this->port); +#endif + this->connection_date = std::chrono::system_clock::now(); + + // Get our local TCP port and store it + this->local_port = static_cast<uint16_t>(-1); + if (rp->ai_family == AF_INET6) + { + struct sockaddr_in6 a; + socklen_t l = sizeof(a); + if (::getsockname(this->socket, (struct sockaddr*)&a, &l) != -1) + this->local_port = ntohs(a.sin6_port); + } + else if (rp->ai_family == AF_INET) + { + struct sockaddr_in a; + socklen_t l = sizeof(a); + if (::getsockname(this->socket, (struct sockaddr*)&a, &l) != -1) + this->local_port = ntohs(a.sin_port); + } + + log_debug("Local port: ", this->local_port, ", and remote port: ", this->port); + + 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(&TCPClientSocketHandler::on_connection_timeout, this), + "connection_timeout"s + std::to_string(this->socket))); + return ; + } + log_info("Connection failed:", std::strerror(errno)); + } + log_error("All connection attempts failed."); + this->close(); + this->on_connection_failed(std::strerror(errno)); + return ; +} + +void TCPClientSocketHandler::on_connection_timeout() +{ + this->close(); + this->on_connection_failed("connection timed out"); +} + +void TCPClientSocketHandler::connect() +{ + this->connect(this->address, this->port, this->use_tls); +} + +void TCPClientSocketHandler::close() +{ + TimedEventsManager::instance().cancel("connection_timeout"s + + std::to_string(this->socket)); + + TCPSocketHandler::close(); + + this->connected = false; + this->connecting = false; + this->port.clear(); + this->resolver.clear(); +} + +void TCPClientSocketHandler::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)); +} + +bool TCPClientSocketHandler::is_connected() const +{ + return this->connected; +} + +bool TCPClientSocketHandler::is_connecting() const +{ + return this->connecting || this->resolver.is_resolving(); +} + +std::string TCPClientSocketHandler::get_port() const +{ + return this->port; +} + +bool TCPClientSocketHandler::match_port_pairt(const uint16_t local, const uint16_t remote) const +{ + const uint16_t remote_port = static_cast<uint16_t>(std::stoi(this->port)); + return this->is_connected() && local == this->local_port && remote == remote_port; +} diff --git a/src/network/tcp_client_socket_handler.hpp b/src/network/tcp_client_socket_handler.hpp new file mode 100644 index 0000000..74caca9 --- /dev/null +++ b/src/network/tcp_client_socket_handler.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include <network/tcp_socket_handler.hpp> + +class TCPClientSocketHandler: public TCPSocketHandler +{ + public: + TCPClientSocketHandler(std::shared_ptr<Poller>& poller); + ~TCPClientSocketHandler(); + /** + * 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; + /** + * 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; + bool is_connected() const override; + bool is_connecting() const override; + + std::string get_port() const; + + void close() override final; + std::chrono::system_clock::time_point connection_date; + + /** + * Whether or not this connection is using the two given TCP ports. + */ + bool match_port_pairt(const uint16_t local, const uint16_t remote) const; + + protected: + 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; + /** + * Display the resolved IP, just for information purpose. + */ + void display_resolved_ip(struct addrinfo* rp) const; + private: + /** + * Initialize the socket with the parameters contained in the given + * addrinfo structure. + */ + void init_socket(const struct addrinfo* rp); + /** + * 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{}; + + /** + * Hostname we are connected/connecting to + */ + std::string address; + /** + * Port we are connected/connecting to + */ + std::string port; + + uint16_t local_port{}; + + bool connected; + bool connecting; +}; diff --git a/src/network/tcp_server_socket.hpp b/src/network/tcp_server_socket.hpp new file mode 100644 index 0000000..c511962 --- /dev/null +++ b/src/network/tcp_server_socket.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include <network/socket_handler.hpp> +#include <network/poller.hpp> +#include <logger/logger.hpp> + +#include <string> + +#include <arpa/inet.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/ip.h> + +#include <cstring> +#include <cassert> + +template <typename RemoteSocketType> +class TcpSocketServer: public SocketHandler +{ + public: + TcpSocketServer(std::shared_ptr<Poller>& poller, const uint16_t port): + SocketHandler(poller, -1) + { + if ((this->socket = ::socket(AF_INET6, SOCK_STREAM, 0)) == -1) + throw std::runtime_error(std::string{"Could not create socket: "} + std::strerror(errno)); + + int opt = 1; + if (::setsockopt(this->socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) + throw std::runtime_error(std::string{"Failed to set socket option: "} + std::strerror(errno)); + + struct sockaddr_in6 addr{}; + addr.sin6_family = AF_INET6; + addr.sin6_port = htons(port); + addr.sin6_addr = IN6ADDR_ANY_INIT; + if ((::bind(this->socket, (const struct sockaddr*)&addr, sizeof(addr))) == -1) + { // If we can’t listen on this port, we just give up, but this is not fatal. + log_warning("Failed to bind on port ", std::to_string(port), ": ", std::strerror(errno)); + return; + } + + if ((::listen(this->socket, 10)) == -1) + throw std::runtime_error("listen() failed"); + + this->accept(); + } + ~TcpSocketServer() = default; + + void on_recv() override + { + // Accept a RemoteSocketType + int socket = ::accept(this->socket, nullptr, nullptr); + + auto client = std::make_unique<RemoteSocketType>(poller, socket, *this); + this->poller->add_socket_handler(client.get()); + this->sockets.push_back(std::move(client)); + } + + protected: + std::vector<std::unique_ptr<RemoteSocketType>> sockets; + + private: + void accept() + { + this->poller->add_socket_handler(this); + } + bool is_connected() const override + { + return true; + } +}; diff --git a/louloulibs/network/tcp_socket_handler.cpp b/src/network/tcp_socket_handler.cpp index d9ec226..7eebae0 100644 --- a/louloulibs/network/tcp_socket_handler.cpp +++ b/src/network/tcp_socket_handler.cpp @@ -1,8 +1,6 @@ #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> @@ -12,16 +10,29 @@ #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); - +namespace +{ + Botan::AutoSeeded_RNG& get_rng() + { + static Botan::AutoSeeded_RNG rng{}; + return rng; + } + BiboumiTLSPolicy& get_policy() + { + static BiboumiTLSPolicy policy{}; + return policy; + } + Botan::TLS::Session_Manager_In_Memory& get_session_manager() + { + static Botan::TLS::Session_Manager_In_Memory session_manager{get_rng()}; + return session_manager; + } +} #endif #ifndef UIO_FASTIOV @@ -33,12 +44,9 @@ using namespace std::chrono_literals; namespace ph = std::placeholders; -TCPSocketHandler::TCPSocketHandler(std::shared_ptr<Poller> poller): +TCPSocketHandler::TCPSocketHandler(std::shared_ptr<Poller>& poller): SocketHandler(poller, -1), - use_tls(false), - connected(false), - connecting(false), - hostname_resolution_failed(false) + use_tls(false) #ifdef BOTAN_FOUND ,credential_manager(this) #endif @@ -46,181 +54,13 @@ TCPSocketHandler::TCPSocketHandler(std::shared_ptr<Poller> poller): TCPSocketHandler::~TCPSocketHandler() { - this->close(); -} - - -void TCPSocketHandler::init_socket(const struct addrinfo* rp) -{ + if (this->poller->is_managing_socket(this->get_socket())) + this->poller->remove_socket_handler(this->get_socket()); 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(errno)); - 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; - - 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->connection_date = std::chrono::system_clock::now(); - - 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)); + ::close(this->socket); + this->socket = -1; } - 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() @@ -267,13 +107,13 @@ ssize_t TCPSocketHandler::do_recv(void* recv_buf, const size_t buf_size) } else if (-1 == size) { - if (this->connecting) + if (this->is_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; + const auto were_connecting = this->is_connecting(); this->close(); if (were_connecting) this->on_connection_failed(strerror(errno)); @@ -333,29 +173,15 @@ void TCPSocketHandler::on_send() void TCPSocketHandler::close() { - TimedEventsManager::instance().cancel("connection_timeout"s + - std::to_string(this->socket)); - if (this->connected || this->connecting) + if (this->is_connected() || this->is_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) @@ -379,45 +205,35 @@ void TCPSocketHandler::raw_send(std::string&& data) if (data.empty()) return ; this->out_buf.emplace_back(std::move(data)); - if (this->connected) + if (this->is_connected()) this->poller->watch_send_events(this); } void TCPSocketHandler::send_pending_data() { - if (this->connected && !this->out_buf.empty()) + if (this->is_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(); -} - bool TCPSocketHandler::is_using_tls() const { return this->use_tls; } -std::string TCPSocketHandler::get_port() const +void* TCPSocketHandler::get_receive_buffer(const size_t) const { - return this->port; + return nullptr; } -void* TCPSocketHandler::get_receive_buffer(const size_t) const +void TCPSocketHandler::consume_in_buffer(const std::size_t size) { - return nullptr; + this->in_buf = this->in_buf.substr(size, std::string::npos); } #ifdef BOTAN_FOUND -void TCPSocketHandler::start_tls() +void TCPSocketHandler::start_tls(const std::string& address, const std::string& port) { - Botan::TLS::Server_Information server_info(this->address, "irc", std::stoul(this->port)); + Botan::TLS::Server_Information server_info(address, "irc", std::stoul(port)); this->tls = std::make_unique<Botan::TLS::Client>( # if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,11,32) *this, @@ -427,8 +243,8 @@ void TCPSocketHandler::start_tls() [this](Botan::TLS::Alert alert, const Botan::byte*, size_t) { this->tls_alert(alert); }, [this](const Botan::TLS::Session& session) { return this->tls_session_established(session); }, # endif - session_manager, this->credential_manager, policy, - rng, server_info, Botan::TLS::Protocol_Version::latest_tls_version()); + get_session_manager(), this->credential_manager, get_policy(), + get_rng(), server_info, Botan::TLS::Protocol_Version::latest_tls_version()); } void TCPSocketHandler::tls_recv() diff --git a/louloulibs/network/tcp_socket_handler.hpp b/src/network/tcp_socket_handler.hpp index c850f43..ba23861 100644 --- a/louloulibs/network/tcp_socket_handler.hpp +++ b/src/network/tcp_socket_handler.hpp @@ -1,7 +1,6 @@ #pragma once - -#include "louloulibs.h" +#include "biboumi.h" #include <network/socket_handler.hpp> #include <network/resolver.hpp> @@ -20,7 +19,10 @@ #include <list> #ifdef BOTAN_FOUND -#include <botan/version.h> + +# include <botan/types.h> +# include <botan/botan.h> +# include <botan/tls_session_manager.h> class BiboumiTLSPolicy: public Botan::TLS::Policy { @@ -30,6 +32,10 @@ public: { return true; } + bool require_cert_revocation_info() const override + { + return false; + } # endif }; @@ -41,10 +47,8 @@ public: #endif /** - * 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) + * Does all the read/write, buffering etc. With optional tls. + * But doesn’t do any connect() or accept() or anything else. */ class TCPSocketHandler: public SocketHandler #ifdef BOTAN_FOUND @@ -56,20 +60,13 @@ class TCPSocketHandler: public SocketHandler protected: ~TCPSocketHandler(); public: - explicit TCPSocketHandler(std::shared_ptr<Poller> poller); + 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() */ @@ -93,25 +90,7 @@ public: /** * 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; + virtual void close(); /** * 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 @@ -119,6 +98,9 @@ public: * 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. + * + * The function should call consume_in_buffer, with the size that was consumed by the + * “parsing”, and thus to be removed from the input buffer. */ virtual void parse_in_buffer(const size_t size) = 0; #ifdef BOTAN_FOUND @@ -131,19 +113,10 @@ public: return true; } #endif - bool is_connected() const override final; - bool is_connecting() const; bool is_using_tls() const; - std::string get_port() const; - std::chrono::system_clock::time_point connection_date; 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). @@ -162,13 +135,16 @@ private: */ void raw_send(std::string&& data); + protected: + virtual bool is_connecting() const = 0; #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(); + void start_tls(const std::string& address, const std::string& port); + private: /** * An additional step to pass the data into our tls object to decrypt it * before passing it to parse_in_buffer. @@ -220,20 +196,11 @@ private: * Where data is added, when we want to send something to the client. */ std::vector<std::string> out_buf; +protected: /** - * 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. + * Whether we are using TLS on this connection or not. */ - struct addrinfo addrinfo; - struct sockaddr_in6 ai_addr; - socklen_t ai_addrlen; - -protected: + bool use_tls; /** * Where data read from the socket is added until we can extract a full * and meaningful “message” from it. @@ -242,9 +209,9 @@ protected: */ std::string in_buf; /** - * Whether we are using TLS on this connection or not. + * Remove the given “size” first bytes from our in_buf. */ - bool use_tls; + void consume_in_buffer(const std::size_t size); /** * 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 @@ -254,38 +221,12 @@ protected: */ 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. + * Called when we detect a disconnection from the remote host. */ - void display_resolved_ip(struct addrinfo* rp) const; + virtual void on_connection_close(const std::string&) {} + virtual void on_connection_failed(const std::string&) {} #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: diff --git a/louloulibs/utils/encoding.cpp b/src/utils/encoding.cpp index 60f2212..aa91dac 100644 --- a/louloulibs/utils/encoding.cpp +++ b/src/utils/encoding.cpp @@ -7,6 +7,7 @@ #include <assert.h> #include <string.h> #include <iconv.h> +#include <cerrno> #include <map> #include <bitset> @@ -151,7 +152,7 @@ namespace utils throw std::runtime_error("Cannot convert into UTF-8"); // Make sure cd is always closed when we leave this function - const auto sg = utils::make_scope_guard([&cd](auto&&){ iconv_close(cd); }); + const auto sg = utils::make_scope_guard([&cd](){ iconv_close(cd); }); size_t inbytesleft = str.size(); @@ -168,7 +169,7 @@ namespace utils char* outbuf_ptr = outbuf; // Make sure outbuf is always deleted when we leave this function - const auto sg2 = utils::make_scope_guard([outbuf](auto&&){ delete[] outbuf; }); + const auto sg2 = utils::make_scope_guard([outbuf](){ delete[] outbuf; }); bool done = false; while (done == false) diff --git a/louloulibs/utils/encoding.hpp b/src/utils/encoding.hpp index 586edd8..b707a0c 100644 --- a/louloulibs/utils/encoding.hpp +++ b/src/utils/encoding.hpp @@ -28,7 +28,7 @@ namespace utils * 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); + std::string convert_to_utf8(const std::string& str, const char* charset); } namespace xep0106 diff --git a/louloulibs/utils/get_first_non_empty.cpp b/src/utils/get_first_non_empty.cpp index 5b3bedb..5b3bedb 100644 --- a/louloulibs/utils/get_first_non_empty.cpp +++ b/src/utils/get_first_non_empty.cpp diff --git a/louloulibs/utils/get_first_non_empty.hpp b/src/utils/get_first_non_empty.hpp index a38f5fb..a38f5fb 100644 --- a/louloulibs/utils/get_first_non_empty.hpp +++ b/src/utils/get_first_non_empty.hpp diff --git a/louloulibs/utils/revstr.cpp b/src/utils/revstr.cpp index 87fd801..87fd801 100644 --- a/louloulibs/utils/revstr.cpp +++ b/src/utils/revstr.cpp diff --git a/louloulibs/utils/revstr.hpp b/src/utils/revstr.hpp index 8e521ea..8e521ea 100644 --- a/louloulibs/utils/revstr.hpp +++ b/src/utils/revstr.hpp diff --git a/louloulibs/utils/scopeguard.hpp b/src/utils/scopeguard.hpp index cd0e89e..e697fc3 100644 --- a/louloulibs/utils/scopeguard.hpp +++ b/src/utils/scopeguard.hpp @@ -87,9 +87,11 @@ private: }; template<typename F> -auto make_scope_guard(F&& f) +auto make_scope_guard(F f) { - return std::unique_ptr<void, std::decay_t<F>>{(void*)1, std::forward<F>(f)}; + static struct Empty {} empty; + auto deleter = [f = std::move(f)](Empty*) { f(); }; + return std::unique_ptr<Empty, decltype(deleter)>{&empty, std::move(deleter)}; } } diff --git a/src/utils/sha1.cpp b/src/utils/sha1.cpp new file mode 100644 index 0000000..2e6efc2 --- /dev/null +++ b/src/utils/sha1.cpp @@ -0,0 +1,40 @@ +#include <utils/sha1.hpp> + +#include <biboumi.h> + +#ifdef BOTAN_FOUND +# include <botan/version.h> +# include <botan/hash.h> +# include <botan/hex.h> +# include <botan/exceptn.h> +#endif +#ifdef GCRYPT_FOUND +# include <gcrypt.h> +# include <vector> +# include <iomanip> +# include <sstream> +#endif + +std::string sha1(const std::string& input) +{ +#ifdef BOTAN_FOUND +# if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(1,11,34) + auto sha1 = Botan::HashFunction::create("SHA-1"); + if (!sha1) + throw Botan::Algorithm_Not_Found("SHA-1"); +# else + auto sha1 = Botan::HashFunction::create_or_throw("SHA-1"); +# endif + sha1->update(input); + return Botan::hex_encode(sha1->final(), false); +#endif +#ifdef GCRYPT_FOUND + const auto hash_length = gcry_md_get_algo_dlen(GCRY_MD_SHA1); + std::vector<uint8_t> output(hash_length, {}); + gcry_md_hash_buffer(GCRY_MD_SHA1, output.data(), input.data(), input.size()); + std::ostringstream digest; + for (std::size_t i = 0; i < hash_length; i++) + digest << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(output[i]); + return digest.str(); +#endif +} diff --git a/src/utils/sha1.hpp b/src/utils/sha1.hpp new file mode 100644 index 0000000..6c551ac --- /dev/null +++ b/src/utils/sha1.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include <string> + +std::string sha1(const std::string& input); diff --git a/louloulibs/utils/split.cpp b/src/utils/split.cpp index 80f8dae..80f8dae 100644 --- a/louloulibs/utils/split.cpp +++ b/src/utils/split.cpp diff --git a/louloulibs/utils/split.hpp b/src/utils/split.hpp index 3755ef8..3755ef8 100644 --- a/louloulibs/utils/split.hpp +++ b/src/utils/split.hpp diff --git a/louloulibs/utils/string.cpp b/src/utils/string.cpp index 635e71a..635e71a 100644 --- a/louloulibs/utils/string.cpp +++ b/src/utils/string.cpp diff --git a/louloulibs/utils/string.hpp b/src/utils/string.hpp index 84ba101..84ba101 100644 --- a/louloulibs/utils/string.hpp +++ b/src/utils/string.hpp diff --git a/src/utils/system.cpp b/src/utils/system.cpp new file mode 100644 index 0000000..c0bee11 --- /dev/null +++ b/src/utils/system.cpp @@ -0,0 +1,21 @@ +#include <logger/logger.hpp> +#include <utils/system.hpp> +#include <sys/utsname.h> +#include <cstring> + +using namespace std::string_literals; + +namespace utils +{ +std::string get_system_name() +{ + struct utsname uts; + const int res = ::uname(&uts); + if (res == -1) + { + log_error("uname failed: ", std::strerror(errno)); + return "Unknown"; + } + return uts.sysname + " "s + uts.release; +} +}
\ No newline at end of file diff --git a/src/utils/system.hpp b/src/utils/system.hpp new file mode 100644 index 0000000..7ea1677 --- /dev/null +++ b/src/utils/system.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include <string> + +namespace utils +{ +std::string get_system_name(); +}
\ No newline at end of file diff --git a/louloulibs/utils/time.cpp b/src/utils/time.cpp index afd6117..88b3de3 100644 --- a/louloulibs/utils/time.cpp +++ b/src/utils/time.cpp @@ -1,11 +1,11 @@ #include <utils/time.hpp> -#include <time.h> +#include <ctime> #include <sstream> #include <iomanip> #include <locale> -#include "louloulibs.h" +#include "biboumi.h" namespace utils { @@ -24,7 +24,7 @@ std::time_t parse_datetime(const std::string& stamp) std::tm t = {}; #ifdef HAS_GET_TIME std::istringstream ss(stamp); - ss.imbue(std::locale("en_US.utf-8")); + ss.imbue(std::locale("C")); std::string timezone; ss >> std::get_time(&t, format) >> timezone; diff --git a/louloulibs/utils/time.hpp b/src/utils/time.hpp index c71cd9c..c71cd9c 100644 --- a/louloulibs/utils/time.hpp +++ b/src/utils/time.hpp diff --git a/louloulibs/utils/timed_events.cpp b/src/utils/timed_events.cpp index 68d009c..5077199 100644 --- a/louloulibs/utils/timed_events.cpp +++ b/src/utils/timed_events.cpp @@ -32,10 +32,8 @@ bool TimedEvent::is_after(const std::chrono::steady_clock::time_point& time_poin 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); + auto diff = std::chrono::duration_cast<std::chrono::milliseconds>(this->time_point - std::chrono::steady_clock::now()); + return std::max(diff, 0ms); } void TimedEvent::execute() const diff --git a/louloulibs/utils/timed_events.hpp b/src/utils/timed_events.hpp index 6e28206..6e28206 100644 --- a/louloulibs/utils/timed_events.hpp +++ b/src/utils/timed_events.hpp diff --git a/louloulibs/utils/timed_events_manager.cpp b/src/utils/timed_events_manager.cpp index 67d61fe..67d61fe 100644 --- a/louloulibs/utils/timed_events_manager.cpp +++ b/src/utils/timed_events_manager.cpp diff --git a/louloulibs/utils/tolower.cpp b/src/utils/tolower.cpp index 3e518bd..3e518bd 100644 --- a/louloulibs/utils/tolower.cpp +++ b/src/utils/tolower.cpp diff --git a/louloulibs/utils/tolower.hpp b/src/utils/tolower.hpp index 650e05d..650e05d 100644 --- a/louloulibs/utils/tolower.hpp +++ b/src/utils/tolower.hpp diff --git a/louloulibs/utils/xdg.cpp b/src/utils/xdg.cpp index 48212a1..b0fa7be 100644 --- a/louloulibs/utils/xdg.cpp +++ b/src/utils/xdg.cpp @@ -1,7 +1,7 @@ #include <utils/xdg.hpp> #include <cstdlib> -#include "louloulibs.h" +#include "biboumi.h" std::string xdg_path(const std::string& filename, const char* env_var) { diff --git a/louloulibs/utils/xdg.hpp b/src/utils/xdg.hpp index 56e11da..56e11da 100644 --- a/louloulibs/utils/xdg.hpp +++ b/src/utils/xdg.hpp diff --git a/louloulibs/xmpp/adhoc_command.cpp b/src/xmpp/adhoc_command.cpp index 99701d7..825cc92 100644 --- a/louloulibs/xmpp/adhoc_command.cpp +++ b/src/xmpp/adhoc_command.cpp @@ -18,30 +18,24 @@ bool AdhocCommand::is_admin_only() const void PingStep1(XmppComponent&, AdhocSession&, XmlNode& command_node) { - XmlNode note("note"); + XmlSubNode note(command_node, "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"); + XmlSubNode x(command_node, "jabber:x:data:x"); x["type"] = "form"; - XmlNode title("title"); + XmlSubNode title(x, "title"); title.set_inner("Configure your name."); - x.add_child(std::move(title)); - XmlNode instructions("instructions"); + XmlSubNode instructions(x, "instructions"); instructions.set_inner("Please provide your name."); - x.add_child(std::move(instructions)); - XmlNode name_field("field"); + XmlSubNode name_field(x, "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)); + XmlSubNode required(name_field, "required"); } void HelloStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node) @@ -60,21 +54,19 @@ void HelloStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node) { 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); + const std::string value_str = value->get_inner(); command_node.delete_all_children(); - command_node.add_child(std::move(note)); + XmlSubNode note(command_node, "note"); + note["type"] = "info"; + note.set_inner("Hello "s + value_str + "!"s); return; } } } command_node.delete_all_children(); - XmlNode error(ADHOC_NS":error"); + XmlSubNode error(command_node, 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)); + XmlSubNode condition(error, STANZA_NS":bad-request"); session.terminate(); } @@ -82,8 +74,7 @@ void Reload(XmppComponent&, AdhocSession&, XmlNode& command_node) { ::reload_process(); command_node.delete_all_children(); - XmlNode note("note"); + XmlSubNode note(command_node, "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/src/xmpp/adhoc_command.hpp index 7c4de47..ced4549 100644 --- a/louloulibs/xmpp/adhoc_command.hpp +++ b/src/xmpp/adhoc_command.hpp @@ -17,7 +17,7 @@ class AdhocCommand { friend class AdhocSession; public: - AdhocCommand(std::vector<AdhocStep>&& callback, const std::string& name, const bool admin_only); + AdhocCommand(std::vector<AdhocStep>&& callbacks, const std::string& name, const bool admin_only); ~AdhocCommand() = default; AdhocCommand(const AdhocCommand&) = default; AdhocCommand(AdhocCommand&&) = default; diff --git a/louloulibs/xmpp/adhoc_commands_handler.cpp b/src/xmpp/adhoc_commands_handler.cpp index 540cac0..040d0ff 100644 --- a/louloulibs/xmpp/adhoc_commands_handler.cpp +++ b/src/xmpp/adhoc_commands_handler.cpp @@ -36,20 +36,16 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co auto command_it = this->commands.find(node); if (command_it == this->commands.end()) { - XmlNode error(ADHOC_NS":error"); + XmlSubNode error(command_node, 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)); + XmlSubNode condition(error, STANZA_NS":item-not-found"); } else if (command_it->second.is_admin_only() && Config::get("admin", "") != jid.local + "@" + jid.domain) { - XmlNode error(ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS":error"); error["type"] = "cancel"; - XmlNode condition(STANZA_NS":forbidden"); - error.add_child(std::move(condition)); - command_node.add_child(std::move(error)); + XmlSubNode condition(error, STANZA_NS":forbidden"); } else { @@ -66,15 +62,8 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co "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") + if ((session_it != this->sessions.end()) && + (action == "execute" || action == "next" || action == "complete")) { // execute the step AdhocSession& session = session_it->second; @@ -90,10 +79,8 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co else { command_node["status"] = "executing"; - XmlNode actions("actions"); - XmlNode next("next"); - actions.add_child(std::move(next)); - command_node.add_child(std::move(actions)); + XmlSubNode actions(command_node, "actions"); + XmlSubNode next(actions, "next"); } } else if (action == "cancel") @@ -104,11 +91,9 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co } else // unsupported action { - XmlNode error(ADHOC_NS":error"); + XmlSubNode error(command_node, 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)); + XmlSubNode condition(error, STANZA_NS":bad-request"); } } return command_node; diff --git a/louloulibs/xmpp/adhoc_commands_handler.hpp b/src/xmpp/adhoc_commands_handler.hpp index e37d913..e37d913 100644 --- a/louloulibs/xmpp/adhoc_commands_handler.hpp +++ b/src/xmpp/adhoc_commands_handler.hpp diff --git a/louloulibs/xmpp/adhoc_session.cpp b/src/xmpp/adhoc_session.cpp index dda4bea..e2d6c0e 100644 --- a/louloulibs/xmpp/adhoc_session.cpp +++ b/src/xmpp/adhoc_session.cpp @@ -1,7 +1,7 @@ #include <xmpp/adhoc_session.hpp> #include <xmpp/adhoc_command.hpp> -#include <assert.h> +#include <cassert> AdhocSession::AdhocSession(const AdhocCommand& command, const std::string& owner_jid, const std::string& to_jid): diff --git a/louloulibs/xmpp/adhoc_session.hpp b/src/xmpp/adhoc_session.hpp index 0de8d13..0de8d13 100644 --- a/louloulibs/xmpp/adhoc_session.hpp +++ b/src/xmpp/adhoc_session.hpp diff --git a/src/xmpp/auth.cpp b/src/xmpp/auth.cpp new file mode 100644 index 0000000..8a34a4e --- /dev/null +++ b/src/xmpp/auth.cpp @@ -0,0 +1,8 @@ +#include <xmpp/auth.hpp> + +#include <utils/sha1.hpp> + +std::string get_handshake_digest(const std::string& stream_id, const std::string& secret) +{ + return sha1(stream_id + secret); +} diff --git a/louloulibs/xmpp/auth.hpp b/src/xmpp/auth.hpp index 34a2116..34a2116 100644 --- a/louloulibs/xmpp/auth.hpp +++ b/src/xmpp/auth.hpp diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp index 003b901..85f945d 100644 --- a/src/xmpp/biboumi_adhoc_commands.cpp +++ b/src/xmpp/biboumi_adhoc_commands.cpp @@ -7,6 +7,7 @@ #include <utils/split.hpp> #include <xmpp/jid.hpp> #include <algorithm> +#include <sstream> #include <iomanip> #include <biboumi.h> @@ -23,47 +24,38 @@ using namespace std::string_literals; void DisconnectUserStep1(XmppComponent& xmpp_component, AdhocSession&, XmlNode& command_node) { - auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component); + auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component); - XmlNode x("jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data:x"); x["type"] = "form"; - XmlNode title("title"); + XmlSubNode title(x, "title"); title.set_inner("Disconnect a user from the gateway"); - x.add_child(std::move(title)); - XmlNode instructions("instructions"); + XmlSubNode instructions(x, "instructions"); instructions.set_inner("Choose a user JID and a quit message"); - x.add_child(std::move(instructions)); - XmlNode jids_field("field"); + XmlSubNode jids_field(x, "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)); + XmlSubNode required(jids_field, "required"); for (Bridge* bridge: biboumi_component.get_bridges()) { - XmlNode option("option"); + XmlSubNode option(jids_field, "option"); option["label"] = bridge->get_jid(); - XmlNode value("value"); + XmlSubNode value(option, "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"); + XmlSubNode message_field(x, "field"); message_field["var"] = "quit-message"; message_field["type"] = "text-single"; message_field["label"] = "Quit message"; - XmlNode message_value("value"); + XmlSubNode message_value(message_field, "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); + auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component); // Find out if the jids, and the quit message are provided in the form. std::string quit_message; @@ -97,7 +89,7 @@ void DisconnectUserStep2(XmppComponent& xmpp_component, AdhocSession& session, X } command_node.delete_all_children(); - XmlNode note("note"); + XmlSubNode note(command_node, "note"); note["type"] = "info"; if (num == 0) note.set_inner("No user were disconnected."); @@ -105,15 +97,12 @@ void DisconnectUserStep2(XmppComponent& xmpp_component, AdhocSession& session, X 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"); + XmlSubNode error(command_node, 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)); + XmlSubNode condition(error, STANZA_NS":bad-request"); session.terminate(); } @@ -126,48 +115,43 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman auto options = Database::get_global_options(owner.bare()); - XmlNode x("jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data:x"); x["type"] = "form"; - XmlNode title("title"); + XmlSubNode title(x, "title"); title.set_inner("Configure some global default settings."); - x.add_child(std::move(title)); - XmlNode instructions("instructions"); + XmlSubNode instructions(x, "instructions"); instructions.set_inner("Edit the form, to configure your global settings for the component."); - x.add_child(std::move(instructions)); - XmlNode required("required"); - - XmlNode max_histo_length("field"); + XmlSubNode max_histo_length(x, "field"); max_histo_length["var"] = "max_history_length"; max_histo_length["type"] = "text-single"; max_histo_length["label"] = "Max history length"; max_histo_length["desc"] = "The maximum number of lines in the history that the server sends when joining a channel"; - XmlNode value("value"); - value.set_inner(std::to_string(options.maxHistoryLength.value())); - max_histo_length.add_child(std::move(value)); - x.add_child(std::move(max_histo_length)); + { + XmlSubNode value(max_histo_length, "value"); + value.set_inner(std::to_string(options.maxHistoryLength.value())); + } - XmlNode record_history("field"); + XmlSubNode record_history(x, "field"); record_history["var"] = "record_history"; record_history["type"] = "boolean"; record_history["label"] = "Record history"; record_history["desc"] = "Whether to save the messages into the database, or not"; - value.set_name("value"); - if (options.recordHistory.value()) - value.set_inner("true"); - else - value.set_inner("false"); - record_history.add_child(std::move(value)); - x.add_child(std::move(record_history)); - - command_node.add_child(std::move(x)); + { + XmlSubNode value(record_history, "value"); + value.set_name("value"); + if (options.recordHistory.value()) + value.set_inner("true"); + else + value.set_inner("false"); + } } void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node) { - BiboumiComponent& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component); + auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component); const XmlNode* x = command_node.get_child("x", "jabber:x:data"); if (x) @@ -194,17 +178,14 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session, options.update(); command_node.delete_all_children(); - XmlNode note("note"); + XmlSubNode note(command_node, "note"); note["type"] = "info"; note.set_inner("Configuration successfully applied."); - command_node.add_child(std::move(note)); return; } - XmlNode error(ADHOC_NS":error"); + XmlSubNode error(command_node, 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)); + XmlSubNode condition(error, STANZA_NS":bad-request"); session.terminate(); } @@ -218,18 +199,14 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com auto options = Database::get_irc_server_options(owner.local + "@" + owner.domain, server_domain); - XmlNode x("jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data:x"); x["type"] = "form"; - XmlNode title("title"); + XmlSubNode title(x, "title"); title.set_inner("Configure the IRC server "s + server_domain); - x.add_child(std::move(title)); - XmlNode instructions("instructions"); + XmlSubNode instructions(x, "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"); + XmlSubNode ports(x, "field"); ports["var"] = "ports"; ports["type"] = "text-multi"; ports["label"] = "Ports"; @@ -237,15 +214,12 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com auto vals = utils::split(options.ports.value(), ';', false); for (const auto& val: vals) { - XmlNode ports_value("value"); + XmlSubNode ports_value(ports, "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"); + XmlSubNode tls_ports(x, "field"); tls_ports["var"] = "tls_ports"; tls_ports["type"] = "text-multi"; tls_ports["label"] = "TLS ports"; @@ -253,126 +227,108 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com vals = utils::split(options.tlsPorts.value(), ';', false); for (const auto& val: vals) { - XmlNode tls_ports_value("value"); + XmlSubNode tls_ports_value(tls_ports, "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"); + XmlSubNode verify_cert(x, "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"); + XmlSubNode verify_cert_value(verify_cert, "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"); + XmlSubNode fingerprint(x, "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"); + XmlSubNode fingerprint_value(fingerprint, "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"); + XmlSubNode pass(x, "field"); pass["var"] = "pass"; pass["type"] = "text-private"; - pass["label"] = "Server password (to be used in a PASS command when connecting)"; + pass["label"] = "Server password"; + pass["desc"] = "Will be used in a PASS command when connecting"; if (!options.pass.value().empty()) { - XmlNode pass_value("value"); + XmlSubNode pass_value(pass, "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"); + XmlSubNode after_cnt_cmd(x, "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"); + XmlSubNode after_cnt_cmd_value(after_cnt_cmd, "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"); + XmlSubNode username(x, "field"); username["var"] = "username"; username["type"] = "text-single"; username["label"] = "Username"; if (!options.username.value().empty()) { - XmlNode username_value("value"); + XmlSubNode username_value(username, "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"); + XmlSubNode realname(x, "field"); realname["var"] = "realname"; realname["type"] = "text-single"; realname["label"] = "Realname"; if (!options.realname.value().empty()) { - XmlNode realname_value("value"); + XmlSubNode realname_value(realname, "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"); + XmlSubNode encoding_out(x, "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"); + XmlSubNode encoding_out_value(encoding_out, "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"); + XmlSubNode encoding_in(x, "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"); + XmlSubNode encoding_in_value(encoding_in, "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)); + XmlSubNode linger_time(x, "field"); + linger_time["var"] = "linger_time"; + linger_time["type"] = "text-single"; + linger_time["desc"] = "The number of seconds to wait before sending a QUIT command, after the last channel on that server has been left."; + linger_time["label"] = "Linger time"; + { + XmlSubNode linger_time_value(linger_time, "value"); + linger_time_value.set_inner(std::to_string(options.lingerTime.value())); + } } void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node) @@ -452,22 +408,23 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com value && !value->get_inner().empty()) options.encodingIn = value->get_inner(); + else if (field->get_tag("var") == "linger_time" && + value && !value->get_inner().empty()) + options.lingerTime = value->get_inner(); + } options.update(); command_node.delete_all_children(); - XmlNode note("note"); + XmlSubNode note(command_node, "note"); note["type"] = "info"; note.set_inner("Configuration successfully applied."); - command_node.add_child(std::move(note)); return; } - XmlNode error(ADHOC_NS":error"); + XmlSubNode error(command_node, 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)); + XmlSubNode condition(error, STANZA_NS":bad-request"); session.terminate(); } @@ -479,46 +436,48 @@ void ConfigureIrcChannelStep1(XmppComponent&, AdhocSession& session, XmlNode& co 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"); + XmlSubNode x(command_node, "jabber:x:data:x"); x["type"] = "form"; - XmlNode title("title"); + XmlSubNode title(x, "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"); + XmlSubNode instructions(x, "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"); + XmlSubNode encoding_out(x, "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"); + XmlSubNode encoding_out_value(encoding_out, "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"); + XmlSubNode encoding_in(x, "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"); + XmlSubNode encoding_in_value(encoding_in, "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)); + XmlSubNode persistent(x, "field"); + persistent["var"] = "persistent"; + persistent["type"] = "boolean"; + persistent["desc"] = "If set to true, when all XMPP clients have left this channel, biboumi will stay idle in it, without sending a PART command."; + persistent["label"] = "Persistent"; + { + XmlSubNode value(persistent, "value"); + value.set_name("value"); + if (options.persistent.value()) + value.set_inner("true"); + else + value.set_inner("false"); + } } void ConfigureIrcChannelStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node) @@ -542,22 +501,23 @@ void ConfigureIrcChannelStep2(XmppComponent&, AdhocSession& session, XmlNode& co else if (field->get_tag("var") == "encoding_in" && value && !value->get_inner().empty()) options.encodingIn = value->get_inner(); + + else if (field->get_tag("var") == "persistent" && + value) + options.persistent = to_bool(value->get_inner()); } options.update(); command_node.delete_all_children(); - XmlNode note("note"); + XmlSubNode note(command_node, "note"); note["type"] = "info"; note.set_inner("Configuration successfully applied."); - command_node.add_child(std::move(note)); return; } - XmlNode error(ADHOC_NS":error"); + XmlSubNode error(command_node, 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)); + XmlSubNode condition(error, STANZA_NS":bad-request"); session.terminate(); } #endif // USE_DATABASE @@ -573,33 +533,26 @@ void DisconnectUserFromServerStep1(XmppComponent& xmpp_component, AdhocSession& } else { // Send a form to select the user to disconnect - auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component); + auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component); - XmlNode x("jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data:x"); x["type"] = "form"; - XmlNode title("title"); + XmlSubNode title(x, "title"); title.set_inner("Disconnect a user from selected IRC servers"); - x.add_child(std::move(title)); - XmlNode instructions("instructions"); + XmlSubNode instructions(x, "instructions"); instructions.set_inner("Choose a user JID"); - x.add_child(std::move(instructions)); - XmlNode jids_field("field"); + XmlSubNode jids_field(x, "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)); + XmlSubNode required(jids_field, "required"); for (Bridge* bridge: biboumi_component.get_bridges()) { - XmlNode option("option"); + XmlSubNode option(jids_field, "option"); option["label"] = bridge->get_jid(); - XmlNode value("value"); + XmlSubNode value(option, "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)); } } @@ -625,55 +578,44 @@ void DisconnectUserFromServerStep2(XmppComponent& xmpp_component, AdhocSession& // 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); + auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component); - XmlNode x("jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data:x"); x["type"] = "form"; - XmlNode title("title"); + XmlSubNode title(x, "title"); title.set_inner("Disconnect a user from selected IRC servers"); - x.add_child(std::move(title)); - XmlNode instructions("instructions"); + XmlSubNode instructions(x, "instructions"); instructions.set_inner("Choose one or more servers to disconnect this JID from"); - x.add_child(std::move(instructions)); - XmlNode jids_field("field"); + XmlSubNode jids_field(x, "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)); + XmlSubNode required(jids_field, "required"); Bridge* bridge = biboumi_component.find_user_bridge(jid_to_disconnect); if (!bridge || bridge->get_irc_clients().empty()) { - XmlNode note("note"); + XmlSubNode note(command_node, "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"); + XmlSubNode option(jids_field, "option"); option["label"] = pair.first; - XmlNode value("value"); + XmlSubNode value(option, "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"); + XmlSubNode message_field(x, "field"); message_field["var"] = "quit-message"; message_field["type"] = "text-single"; message_field["label"] = "Quit message"; - XmlNode message_value("value"); + XmlSubNode message_value(message_field, "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) @@ -701,7 +643,7 @@ void DisconnectUserFromServerStep3(XmppComponent& xmpp_component, AdhocSession& } } - auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component); + auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component); Bridge* bridge = biboumi_component.find_user_bridge(jid_to_disconnect); auto& clients = bridge->get_irc_clients(); @@ -718,19 +660,18 @@ void DisconnectUserFromServerStep3(XmppComponent& xmpp_component, AdhocSession& } } command_node.delete_all_children(); - XmlNode note("note"); + XmlSubNode note(command_node, "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)); } void GetIrcConnectionInfoStep1(XmppComponent& component, AdhocSession& session, XmlNode& command_node) { - BiboumiComponent& biboumi_component = static_cast<BiboumiComponent&>(component); + auto& biboumi_component = dynamic_cast<BiboumiComponent&>(component); const Jid owner(session.get_owner_jid()); const Jid target(session.get_target_jid()); @@ -741,10 +682,9 @@ void GetIrcConnectionInfoStep1(XmppComponent& component, AdhocSession& session, utils::ScopeGuard sg([&message, &command_node]() { command_node.delete_all_children(); - XmlNode note("note"); + XmlSubNode note(command_node, "note"); note["type"] = "info"; note.set_inner(message); - command_node.add_child(std::move(note)); }); Bridge* bridge = biboumi_component.get_user_bridge(owner.bare()); diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp index d6782e2..df96d62 100644 --- a/src/xmpp/biboumi_component.cpp +++ b/src/xmpp/biboumi_component.cpp @@ -8,7 +8,6 @@ #include <xmpp/biboumi_adhoc_commands.hpp> #include <bridge/list_element.hpp> #include <config/config.hpp> -#include <utils/sha1.hpp> #include <utils/time.hpp> #include <xmpp/jid.hpp> @@ -17,7 +16,6 @@ #include <cstdlib> -#include <louloulibs.h> #include <biboumi.h> #include <uuid/uuid.h> @@ -45,7 +43,7 @@ static std::set<std::string> kickable_errors{ }; -BiboumiComponent::BiboumiComponent(std::shared_ptr<Poller> poller, const std::string& hostname, const std::string& secret): +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) @@ -137,7 +135,7 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) // 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([&](){ + utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name](){ this->send_stanza_error("presence", from_str, to_str, id, error_type, error_name, ""); }); @@ -150,7 +148,7 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) { 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); + bridge->send_irc_nick_change(iid, to.resource, from.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(): "", @@ -162,6 +160,15 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) bridge->leave_irc_channel(std::move(iid), status ? status->get_inner() : "", from.resource); } } + else if (iid.type == Iid::Type::Server || iid.type == Iid::Type::None) + { + if (type == "subscribe") + { // Auto-accept any subscription request for an IRC server + this->accept_subscription(to_str, from.bare()); + this->ask_subscription(to_str, from.bare()); + } + + } else { // A user wants to join an invalid IRC channel, return a presence error to him/her @@ -171,10 +178,11 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) } 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); + if (type != "unavailable") + 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(); } @@ -197,7 +205,7 @@ void BiboumiComponent::handle_message(const Stanza& stanza) std::string error_type("cancel"); std::string error_name("internal-server-error"); - utils::ScopeGuard stanza_error([&](){ + utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name](){ this->send_stanza_error("message", from_str, to_str, id, error_type, error_name, ""); }); @@ -280,7 +288,7 @@ void BiboumiComponent::handle_message(const Stanza& stanza) } // We MUST return an iq, whatever happens, except if the type is -// "result". +// "result" or "error". // 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 @@ -316,7 +324,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) // the scopeguard. std::string error_type("cancel"); std::string error_name("internal-server-error"); - utils::ScopeGuard stanza_error([&](){ + utils::ScopeGuard stanza_error([this, &from, &to_str, &id, &error_type, &error_name](){ this->send_stanza_error("iq", from, to_str, id, error_type, error_name, ""); }); @@ -344,7 +352,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) bridge->send_irc_kick(iid, nick, reason, id, from); } else - bridge->forward_affiliation_role_change(iid, nick, affiliation, role); + bridge->forward_affiliation_role_change(iid, from, nick, affiliation, role, id); stanza_error.disable(); } } @@ -414,7 +422,12 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) } else if (iid.type == Iid::Type::Channel) { - if (node == MUC_TRAFFIC_NS) + if (node.empty()) + { + this->send_irc_channel_disco_info(id, from, to_str); + stanza_error.disable(); + } + else if (node == MUC_TRAFFIC_NS) { this->send_irc_channel_muc_traffic_info(id, from, to_str); stanza_error.disable(); @@ -548,6 +561,10 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) } } } + else if (type == "error") + { + stanza_error.disable(); + } } catch (const IRCNotConnected& ex) { @@ -570,13 +587,11 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza) Jid to(stanza.get_tag("to")); const XmlNode* query = stanza.get_child("query", MAM_NS); - std::string query_id; - if (query) - query_id = query->get_tag("queryid"); Iid iid(to.local, {'#', '&'}); - if (iid.type == Iid::Type::Channel && to.resource.empty()) + if (query && iid.type == Iid::Type::Channel && to.resource.empty()) { + const std::string query_id = query->get_tag("queryid"); std::string start; std::string end; const XmlNode* x = query->get_child("x", DATAFORM_NS); @@ -615,39 +630,33 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza) void BiboumiComponent::send_archived_message(const db::MucLogLine& log_line, const std::string& from, const std::string& to, const std::string& queryid) { - Stanza message("message"); + Stanza message("message"); + { message["from"] = from; message["to"] = to; - XmlNode result("result"); + XmlSubNode result(message, "result"); result["xmlns"] = MAM_NS; if (!queryid.empty()) result["queryid"] = queryid; result["id"] = log_line.uuid.value(); - XmlNode forwarded("forwarded"); + XmlSubNode forwarded(result, "forwarded"); forwarded["xmlns"] = FORWARD_NS; - XmlNode delay("delay"); + XmlSubNode delay(forwarded, "delay"); delay["xmlns"] = DELAY_NS; delay["stamp"] = utils::to_string(log_line.date.value().timeStamp()); - forwarded.add_child(std::move(delay)); - - XmlNode submessage("message"); + XmlSubNode submessage(forwarded, "message"); submessage["xmlns"] = CLIENT_NS; submessage["from"] = from + "/" + log_line.nick.value(); submessage["type"] = "groupchat"; - XmlNode body("body"); + XmlSubNode body(submessage, "body"); body.set_inner(log_line.body.value()); - submessage.add_child(std::move(body)); - - forwarded.add_child(std::move(submessage)); - result.add_child(std::move(forwarded)); - message.add_child(std::move(result)); - - this->send_stanza(message); + } + this->send_stanza(message); } #endif @@ -689,24 +698,23 @@ std::vector<Bridge*> BiboumiComponent::get_bridges() const 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, PING_NS, MAM_NS, VERSION_NS}) - { - XmlNode feature("feature"); - feature["var"] = ns; - query.add_child(std::move(feature)); - } - iq.add_child(std::move(query)); + { + iq["type"] = "result"; + iq["id"] = id; + iq["to"] = jid_to; + iq["from"] = this->served_hostname; + XmlSubNode query(iq, "query"); + query["xmlns"] = DISCO_INFO_NS; + XmlSubNode identity(query, "identity"); + identity["category"] = "conference"; + identity["type"] = "irc"; + identity["name"] = "Biboumi XMPP-IRC gateway"; + for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS}) + { + XmlSubNode feature(query, "feature"); + feature["var"] = ns; + } + } this->send_stanza(iq); } @@ -714,57 +722,66 @@ void BiboumiComponent::send_irc_server_disco_info(const std::string& id, const s { Jid from(jid_from); Stanza iq("iq"); - iq["type"] = "result"; - iq["id"] = id; - iq["to"] = jid_to; - iq["from"] = jid_from; - XmlNode query("query"); - query["xmlns"] = DISCO_INFO_NS; - XmlNode identity("identity"); - identity["category"] = "conference"; - identity["type"] = "irc"; - identity["name"] = "IRC server "s + from.local + " over Biboumi"; - query.add_child(std::move(identity)); - for (const char* ns: {DISCO_INFO_NS, ADHOC_NS, PING_NS, VERSION_NS}) - { - XmlNode feature("feature"); - feature["var"] = ns; - query.add_child(std::move(feature)); - } - iq.add_child(std::move(query)); + { + iq["type"] = "result"; + iq["id"] = id; + iq["to"] = jid_to; + iq["from"] = jid_from; + XmlSubNode query(iq, "query"); + query["xmlns"] = DISCO_INFO_NS; + XmlSubNode identity(query, "identity"); + identity["category"] = "conference"; + identity["type"] = "irc"; + identity["name"] = "IRC server "s + from.local + " over Biboumi"; + for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS}) + { + XmlSubNode feature(query, "feature"); + feature["var"] = ns; + } + } this->send_stanza(iq); } -void BiboumiComponent::send_irc_channel_muc_traffic_info(const std::string id, const std::string& jid_from, const std::string& jid_to) +void BiboumiComponent::send_irc_channel_muc_traffic_info(const std::string& id, const std::string& jid_to, const std::string& jid_from) { Stanza iq("iq"); - iq["type"] = "result"; - iq["id"] = id; - iq["from"] = jid_from; - iq["to"] = jid_to; - - XmlNode query("query"); - query["xmlns"] = DISCO_INFO_NS; - query["node"] = MUC_TRAFFIC_NS; - // We drop all “special” traffic (like xhtml-im, chatstates, etc), so - // don’t include any <feature/> - iq.add_child(std::move(query)); - + { + iq["type"] = "result"; + iq["id"] = id; + iq["from"] = jid_from; + iq["to"] = jid_to; + + XmlSubNode query(iq, "query"); + query["xmlns"] = DISCO_INFO_NS; + query["node"] = MUC_TRAFFIC_NS; + // We drop all “special” traffic (like xhtml-im, chatstates, etc), so + // don’t include any <feature/> + } this->send_stanza(iq); - } -void BiboumiComponent::send_iq_version_request(const std::string& from, - const std::string& jid_to) +void BiboumiComponent::send_irc_channel_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from) { + Jid from(jid_from); + Iid iid(from.local, {}); 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)); + { + iq["type"] = "result"; + iq["id"] = id; + iq["to"] = jid_to; + iq["from"] = jid_from; + XmlSubNode query(iq, "query"); + query["xmlns"] = DISCO_INFO_NS; + XmlSubNode identity(query, "identity"); + identity["category"] = "conference"; + identity["type"] = "irc"; + identity["name"] = "IRC channel "s + iid.get_local() + " from server " + iid.get_server() + " over biboumi"; + for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS}) + { + XmlSubNode feature(query, "feature"); + feature["var"] = ns; + } + } this->send_stanza(iq); } @@ -773,13 +790,14 @@ void BiboumiComponent::send_ping_request(const std::string& from, 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)); + { + iq["type"] = "get"; + iq["id"] = id; + iq["from"] = from + "@" + this->served_hostname; + iq["to"] = jid_to; + XmlSubNode ping(iq, "ping"); + ping["xmlns"] = PING_NS; + } this->send_stanza(iq); auto result_cb = [from, id](Bridge* bridge, const Stanza& stanza) @@ -803,48 +821,43 @@ void BiboumiComponent::send_iq_room_list_result(const std::string& id, const std const ResultSetInfo& rs_info) { 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; + { + iq["from"] = from + "@" + this->served_hostname; + iq["to"] = to_jid; + iq["id"] = id; + iq["type"] = "result"; + XmlSubNode query(iq, "query"); + query["xmlns"] = DISCO_ITEMS_NS; for (auto it = begin; it != end; ++it) - { - XmlNode item("item"); + { + XmlSubNode item(query, "item"); item["jid"] = it->channel + "@" + this->served_hostname; - query.add_child(std::move(item)); - } - - if ((rs_info.max >= 0 || !rs_info.after.empty() || !rs_info.before.empty())) - { - XmlNode set_node("set"); - set_node["xmlns"] = RSM_NS; + } - if (begin != channel_list.channels.cend()) - { - XmlNode first_node("first"); - first_node["index"] = std::to_string(std::distance(channel_list.channels.cbegin(), begin)); - first_node.set_inner(begin->channel + "@" + this->served_hostname); - set_node.add_child(std::move(first_node)); - } - if (end != channel_list.channels.cbegin()) - { - XmlNode last_node("last"); - last_node.set_inner(std::prev(end)->channel + "@" + this->served_hostname); - set_node.add_child(std::move(last_node)); - } - if (channel_list.complete) - { - XmlNode count_node("count"); - count_node.set_inner(std::to_string(channel_list.channels.size())); - set_node.add_child(std::move(count_node)); - } - query.add_child(std::move(set_node)); - } + if ((rs_info.max >= 0 || !rs_info.after.empty() || !rs_info.before.empty())) + { + XmlSubNode set_node(query, "set"); + set_node["xmlns"] = RSM_NS; - iq.add_child(std::move(query)); + if (begin != channel_list.channels.cend()) + { + XmlSubNode first_node(set_node, "first"); + first_node["index"] = std::to_string(std::distance(channel_list.channels.cbegin(), begin)); + first_node.set_inner(begin->channel + "@" + this->served_hostname); + } + if (end != channel_list.channels.cbegin()) + { + XmlSubNode last_node(set_node, "last"); + last_node.set_inner(std::prev(end)->channel + "@" + this->served_hostname); + } + if (channel_list.complete) + { + XmlSubNode count_node(set_node, "count"); + count_node.set_inner(std::to_string(channel_list.channels.size())); + } + } + } this->send_stanza(iq); } @@ -853,16 +866,36 @@ void BiboumiComponent::send_invitation(const std::string& room_target, const std::string& author_nick) { Stanza message("message"); - message["from"] = room_target + "@" + this->served_hostname; - message["to"] = jid_to; - XmlNode x("x"); - x["xmlns"] = MUC_USER_NS; - XmlNode invite("invite"); - if (author_nick.empty()) - invite["from"] = room_target + "@" + this->served_hostname; - else - invite["from"] = room_target + "@" + this->served_hostname + "/" + author_nick; - x.add_child(std::move(invite)); - message.add_child(std::move(x)); + { + message["from"] = room_target + "@" + this->served_hostname; + message["to"] = jid_to; + XmlSubNode x(message, "x"); + x["xmlns"] = MUC_USER_NS; + XmlSubNode invite(x, "invite"); + if (author_nick.empty()) + invite["from"] = room_target + "@" + this->served_hostname; + else + invite["from"] = room_target + "@" + this->served_hostname + "/" + author_nick; + } this->send_stanza(message); } + +void BiboumiComponent::accept_subscription(const std::string& from, const std::string& to) +{ + Stanza presence("presence"); + presence["from"] = from; + presence["to"] = to; + presence["id"] = this->next_id(); + presence["type"] = "subscribed"; + this->send_stanza(presence); +} + +void BiboumiComponent::ask_subscription(const std::string& from, const std::string& to) +{ + Stanza presence("presence"); + presence["from"] = from; + presence["to"] = to; + presence["id"] = this->next_id(); + presence["type"] = "subscribe"; + this->send_stanza(presence); +} diff --git a/src/xmpp/biboumi_component.hpp b/src/xmpp/biboumi_component.hpp index 999001f..f416dac 100644 --- a/src/xmpp/biboumi_component.hpp +++ b/src/xmpp/biboumi_component.hpp @@ -27,7 +27,7 @@ using iq_responder_callback_t = std::function<void(Bridge* bridge, const Stanza& class BiboumiComponent: public XmppComponent { public: - explicit BiboumiComponent(std::shared_ptr<Poller> poller, const std::string& hostname, const std::string& secret); + explicit BiboumiComponent(std::shared_ptr<Poller>& poller, const std::string& hostname, const std::string& secret); ~BiboumiComponent() = default; BiboumiComponent(const BiboumiComponent&) = delete; @@ -69,12 +69,8 @@ public: * Sends the allowed namespaces in MUC message, according to * http://xmpp.org/extensions/xep-0045.html#impl-service-traffic */ - void send_irc_channel_muc_traffic_info(const std::string id, const std::string& jid_from, const std::string& jid_to); - /** - * Send an iq version request - */ - void send_iq_version_request(const std::string& from, - const std::string& jid_to); + void send_irc_channel_muc_traffic_info(const std::string& id, const std::string& jid_to, const std::string& jid_from); + void send_irc_channel_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from); /** * Send a ping request */ @@ -88,6 +84,8 @@ public: const ChannelList& channel_list, std::vector<ListElement>::const_iterator begin, std::vector<ListElement>::const_iterator end, const ResultSetInfo& rs_info); void send_invitation(const std::string& room_target, const std::string& jid_to, const std::string& author_nick); + void accept_subscription(const std::string& from, const std::string& to); + void ask_subscription(const std::string& from, const std::string& to); /** * Handle the various stanza types */ diff --git a/louloulibs/xmpp/body.hpp b/src/xmpp/body.hpp index 068d1a4..068d1a4 100644 --- a/louloulibs/xmpp/body.hpp +++ b/src/xmpp/body.hpp diff --git a/src/xmpp/jid.cpp b/src/xmpp/jid.cpp new file mode 100644 index 0000000..ba8d70b --- /dev/null +++ b/src/xmpp/jid.cpp @@ -0,0 +1,152 @@ +#include <xmpp/jid.hpp> +#include <algorithm> +#include <cstring> +#include <map> + +#include <biboumi.h> +#ifdef LIBIDN_FOUND + #include <stringprep.h> + #include <sys/types.h> + #include <sys/socket.h> + #include <netdb.h> + #include <utils/scopeguard.hpp> + #include <set> +#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())); + + { + // Using getaddrinfo, check if the domain part is a valid IPv4 (then use + // it as is), or IPv6 (surround it with []), or a domain name (run + // nameprep) + struct addrinfo hints{}; + hints.ai_flags = AI_NUMERICHOST; + hints.ai_family = AF_UNSPEC; + + struct addrinfo* addr_res = nullptr; + const auto ret = ::getaddrinfo(domain, nullptr, &hints, &addr_res); + auto addrinfo_deleter = utils::make_scope_guard([addr_res] { if (addr_res) freeaddrinfo(addr_res); }); + if (ret || !addr_res || (addr_res->ai_family != AF_INET && addr_res->ai_family != AF_INET6)) + { // Not an IP, run nameprep on it + 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 ""; + } + + // Make sure it contains only allowed characters + using std::begin; + using std::end; + char* domain_end = domain + ::strlen(domain); + std::replace_if(std::begin(domain), domain + ::strlen(domain), + [](const char c) -> bool + { + return !((c >= 'a' && c <= 'z') || c == '-' || + (c >= '0' && c <= '9') || c == '.'); + }, '-'); + // Make sure there are no doubled - or . + std::set<char> special_chars{'-', '.'}; + domain_end = std::unique(begin(domain), domain + ::strlen(domain), [&special_chars](const char& a, const char& b) -> bool + { + return special_chars.count(a) && special_chars.count(b); + }); + // remove leading and trailing -. if any + if (domain_end != domain && special_chars.count(*(domain_end - 1))) + --domain_end; + if (domain_end != domain && special_chars.count(domain[0])) + { + std::memmove(domain, domain + 1, domain_end - domain + 1); + --domain_end; + } + // And if the final result is an empty string, return a dummy hostname + if (domain_end == domain) + ::strcpy(domain, "empty"); + else + *domain_end = '\0'; + } + else if (addr_res->ai_family == AF_INET6) + { // IPv6, surround it with []. The length is always enough: + // the longest possible IPv6 is way shorter than max_jid_part_len + ::memmove(domain + 1, domain, jid.domain.size()); + domain[0] = '['; + domain[jid.domain.size() + 1] = ']'; + } + } + + + // 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/src/xmpp/jid.hpp index 85e835c..85e835c 100644 --- a/louloulibs/xmpp/jid.hpp +++ b/src/xmpp/jid.hpp diff --git a/louloulibs/xmpp/xmpp_component.cpp b/src/xmpp/xmpp_component.cpp index fa8b0a5..6829cef 100644 --- a/louloulibs/xmpp/xmpp_component.cpp +++ b/src/xmpp/xmpp_component.cpp @@ -5,6 +5,7 @@ #include <xmpp/xmpp_component.hpp> #include <config/config.hpp> +#include <utils/system.hpp> #include <utils/time.hpp> #include <xmpp/auth.hpp> #include <xmpp/jid.hpp> @@ -18,7 +19,7 @@ #include <cstdlib> #include <set> -#include <louloulibs.h> +#include <biboumi.h> #ifdef SYSTEMD_FOUND # include <systemd/sd-daemon.h> #endif @@ -38,14 +39,14 @@ static std::set<std::string> kickable_errors{ "malformed-error" }; -XmppComponent::XmppComponent(std::shared_ptr<Poller> poller, const std::string& hostname, const std::string& secret): - TCPSocketHandler(poller), +XmppComponent::XmppComponent(std::shared_ptr<Poller>& poller, std::string hostname, std::string secret): + TCPClientSocketHandler(poller), ever_auth(false), first_connection_try(true), - secret(secret), + secret(std::move(secret)), authenticated(false), doc_open(false), - served_hostname(hostname), + served_hostname(std::move(hostname)), stanza_handlers{}, adhoc_commands_handler(*this) { @@ -172,12 +173,13 @@ void XmppComponent::on_stanza(const Stanza& 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)); + Stanza node("stream:error"); + { + XmlSubNode error(node, name); + error["xmlns"] = STREAM_NS; + if (!explanation.empty()) + error.set_inner(explanation); + } this->send_stanza(node); } @@ -187,31 +189,34 @@ void XmppComponent::send_stanza_error(const std::string& kind, const std::string const bool fulljid) { Stanza node(kind); - if (!to.empty()) - node["to"] = to; - if (!from.empty()) + { + 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"; { - if (fulljid) - node["from"] = from; - else - node["from"] = from + "@" + this->served_hostname; + XmlSubNode error(node, "error"); + error["type"] = error_type; + { + XmlSubNode inner_error(error, defined_condition); + inner_error["xmlns"] = STANZA_NS; + } + if (!text.empty()) + { + XmlSubNode text_node(error, "text"); + text_node["xmlns"] = STANZA_NS; + text_node.set_inner(text); + } } - 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); } @@ -264,38 +269,33 @@ void* XmppComponent::get_receive_buffer(const size_t size) const void XmppComponent::send_message(const std::string& from, Xmpp::body&& body, const std::string& to, const std::string& type, const bool fulljid, const bool nocopy) { - 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)); - } - - if (nocopy) - { - XmlNode private_node("private"); - private_node["xmlns"] = "urn:xmpp:carbons:2"; - node.add_child(std::move(private_node)); - - XmlNode nocopy("no-copy"); - nocopy["xmlns"] = "urn:xmpp:hints"; - node.add_child(std::move(nocopy)); - } - - this->send_stanza(node); + Stanza message("message"); + { + message["to"] = to; + if (fulljid) + message["from"] = from; + else + message["from"] = from + "@" + this->served_hostname; + if (!type.empty()) + message["type"] = type; + XmlSubNode body_node(message, "body"); + body_node.set_inner(std::get<0>(body)); + if (std::get<1>(body)) + { + XmlSubNode html(message, "html"); + html["xmlns"] = XHTMLIM_NS; + // Pass the ownership of the pointer to this xmlnode + html.add_child(std::move(std::get<1>(body))); + } + if (nocopy) + { + XmlSubNode private_node(message, "private"); + private_node["xmlns"] = "urn:xmpp:carbons:2"; + XmlSubNode nocopy(message, "no-copy"); + nocopy["xmlns"] = "urn:xmpp:hints"; + } + } + this->send_stanza(message); } void XmppComponent::send_user_join(const std::string& from, @@ -306,34 +306,33 @@ void XmppComponent::send_user_join(const std::string& from, 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); + Stanza presence("presence"); + { + presence["to"] = to; + presence["from"] = from + "@" + this->served_hostname + "/" + nick; + + XmlSubNode x(presence, "x"); + x["xmlns"] = MUC_USER_NS; + + XmlSubNode item(x, "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; + } + + if (self) + { + XmlSubNode status(x, "status"); + status["code"] = "110"; + } + } + this->send_stanza(presence); } void XmppComponent::send_invalid_room_error(const std::string& muc_name, @@ -341,44 +340,43 @@ void XmppComponent::send_invalid_room_error(const std::string& muc_name, 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)); + { + if (!muc_name.empty ()) + presence["from"] = muc_name + "@" + this->served_hostname + "/" + nick; + else + presence["from"] = this->served_hostname; + presence["to"] = to; + presence["type"] = "error"; + XmlSubNode x(presence, "x"); + x["xmlns"] = MUC_NS; + XmlSubNode error(presence, "error"); + error["by"] = muc_name + "@" + this->served_hostname; + error["type"] = "cancel"; + XmlSubNode item_not_found(error, "item-not-found"); + item_not_found["xmlns"] = STANZA_NS; + XmlSubNode text(error, "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); + } this->send_stanza(presence); } 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)); + Stanza 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"; + XmlSubNode subject(message, "subject"); + subject.set_inner(std::get<0>(topic)); + } this->send_stanza(message); } @@ -391,16 +389,18 @@ void XmppComponent::send_muc_message(const std::string& muc_name, const std::str 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)); + + { + XmlSubNode body(message, "body"); + body.set_inner(std::get<0>(xmpp_body)); + } + if (std::get<1>(xmpp_body)) { - XmlNode html("html"); + XmlSubNode html(message, "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); } @@ -415,41 +415,41 @@ void XmppComponent::send_history_message(const std::string& muc_name, const std: message["from"] = muc_name + "@" + this->served_hostname; message["type"] = "groupchat"; - XmlNode body("body"); - body.set_inner(body_txt); - message.add_child(std::move(body)); + { + XmlSubNode body(message, "body"); + body.set_inner(body_txt); + } + { + XmlSubNode delay(message, "delay"); + delay["xmlns"] = DELAY_NS; + delay["from"] = muc_name + "@" + this->served_hostname; + delay["stamp"] = utils::to_string(timestamp); + } - XmlNode delay("delay"); - delay["xmlns"] = DELAY_NS; - delay["from"] = muc_name + "@" + this->served_hostname; - delay["stamp"] = utils::to_string(timestamp); - - message.add_child(std::move(delay)); 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) +void XmppComponent::send_muc_leave(const std::string& muc_name, const 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)); - } + { + presence["to"] = jid_to; + presence["from"] = muc_name + "@" + this->served_hostname + "/" + nick; + presence["type"] = "unavailable"; + const std::string message_str = std::get<0>(message); + XmlSubNode x(presence, "x"); + x["xmlns"] = MUC_USER_NS; + if (self) + { + XmlSubNode status(x, "status"); + status["code"] = "110"; + } + if (!message_str.empty()) + { + XmlSubNode status(presence, "status"); + status.set_inner(message_str); + } + } this->send_stanza(presence); } @@ -462,24 +462,22 @@ void XmppComponent::send_nick_change(const std::string& muc_name, 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)); + { + presence["to"] = jid_to; + presence["from"] = muc_name + "@" + this->served_hostname + "/" + old_nick; + presence["type"] = "unavailable"; + XmlSubNode x(presence, "x"); + x["xmlns"] = MUC_USER_NS; + XmlSubNode item(x, "item"); + item["nick"] = new_nick; + XmlSubNode status(x, "status"); + status["code"] = "303"; + if (self) + { + XmlSubNode status(x, "status"); + status["code"] = "110"; + } + } this->send_stanza(presence); this->send_user_join(muc_name, new_nick, "", affiliation, role, jid_to, self); @@ -489,32 +487,28 @@ void XmppComponent::kick_user(const std::string& muc_name, const std::string& ta const std::string& author, const std::string& jid_to, const bool self) { 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)); - if (self) - { - XmlNode status("status"); - status["code"] = "110"; - x.add_child(std::move(status)); - } - presence.add_child(std::move(x)); + { + presence["from"] = muc_name + "@" + this->served_hostname + "/" + target; + presence["to"] = jid_to; + presence["type"] = "unavailable"; + XmlSubNode x(presence, "x"); + x["xmlns"] = MUC_USER_NS; + XmlSubNode item(x, "item"); + item["affiliation"] = "none"; + item["role"] = "none"; + XmlSubNode actor(item, "actor"); + actor["nick"] = author; + actor["jid"] = author; // backward compatibility with old clients + XmlSubNode reason(item, "reason"); + reason.set_inner(txt); + XmlSubNode status(x, "status"); + status["code"] = "307"; + if (self) + { + XmlSubNode status(x, "status"); + status["code"] = "110"; + } + } this->send_stanza(presence); } @@ -524,24 +518,29 @@ void XmppComponent::send_presence_error(const std::string& muc_name, const std::string& type, const std::string& condition, const std::string& error_code, - const std::string& /* text */) + 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)); + { + presence["from"] = muc_name + "@" + this->served_hostname + "/" + nickname; + presence["to"] = jid_to; + presence["type"] = "error"; + XmlSubNode x(presence, "x"); + x["xmlns"] = MUC_NS; + XmlSubNode error(presence, "error"); + error["by"] = muc_name + "@" + this->served_hostname; + error["type"] = type; + if (!text.empty()) + { + XmlSubNode text_node(error, "text"); + text_node["xmlns"] = STANZA_NS; + text_node.set_inner(text); + } + if (!error_code.empty()) + error["code"] = error_code; + XmlSubNode subnode(error, condition); + subnode["xmlns"] = STANZA_NS; + } this->send_stanza(presence); } @@ -552,15 +551,15 @@ void XmppComponent::send_affiliation_role_change(const std::string& muc_name, 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)); + { + presence["from"] = muc_name + "@" + this->served_hostname + "/" + target; + presence["to"] = jid_to; + XmlSubNode x(presence, "x"); + x["xmlns"] = MUC_USER_NS; + XmlSubNode item(x, "item"); + item["affiliation"] = affiliation; + item["role"] = role; + } this->send_stanza(presence); } @@ -572,27 +571,30 @@ void XmppComponent::send_version(const std::string& id, const std::string& jid_t 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)); + { + XmlSubNode query(iq, "query"); + query["xmlns"] = VERSION_NS; + if (version.empty()) + { + { + XmlSubNode name(query, "name"); + name.set_inner("biboumi"); + } + { + XmlSubNode version(query, "version"); + version.set_inner(SOFTWARE_VERSION); + } + { + XmlSubNode os(query, "os"); + os.set_inner(utils::get_system_name()); + } } - else + else { - XmlNode name("name"); + XmlSubNode name(query, "name"); name.set_inner(version); - query.add_child(std::move(name)); } - iq.add_child(std::move(query)); + } this->send_stanza(iq); } @@ -601,24 +603,24 @@ void XmppComponent::send_adhoc_commands_list(const std::string& id, const std::s 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)); + { + iq["type"] = "result"; + iq["id"] = id; + iq["to"] = requester_jid; + iq["from"] = from_jid; + XmlSubNode query(iq, "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; + XmlSubNode item(query, "item"); + item["jid"] = from_jid; + item["node"] = kv.first; + item["name"] = kv.second.name; + } + } this->send_stanza(iq); } @@ -626,13 +628,14 @@ 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)); + { + iq["type"] = "get"; + iq["id"] = "version_"s + XmppComponent::next_id(); + iq["from"] = from + "@" + this->served_hostname; + iq["to"] = jid_to; + XmlSubNode query(iq, "query"); + query["xmlns"] = VERSION_NS; + } this->send_stanza(iq); } diff --git a/louloulibs/xmpp/xmpp_component.hpp b/src/xmpp/xmpp_component.hpp index 5f5f937..250f0a8 100644 --- a/louloulibs/xmpp/xmpp_component.hpp +++ b/src/xmpp/xmpp_component.hpp @@ -2,7 +2,7 @@ #include <xmpp/adhoc_commands_handler.hpp> -#include <network/tcp_socket_handler.hpp> +#include <network/tcp_client_socket_handler.hpp> #include <xmpp/xmpp_parser.hpp> #include <xmpp/body.hpp> @@ -27,7 +27,7 @@ #define ADHOC_NS "http://jabber.org/protocol/commands" #define PING_NS "urn:xmpp:ping" #define DELAY_NS "urn:xmpp:delay" -#define MAM_NS "urn:xmpp:mam:1" +#define MAM_NS "urn:xmpp:mam:2" #define FORWARD_NS "urn:xmpp:forward:0" #define CLIENT_NS "jabber:client" #define DATAFORM_NS "jabber:x:data" @@ -40,10 +40,10 @@ * * TODO: implement XEP-0225: Component Connections */ -class XmppComponent: public TCPSocketHandler +class XmppComponent: public TCPClientSocketHandler { public: - explicit XmppComponent(std::shared_ptr<Poller> poller, const std::string& hostname, const std::string& secret); + explicit XmppComponent(std::shared_ptr<Poller>& poller, std::string hostname, std::string secret); virtual ~XmppComponent() = default; XmppComponent(const XmppComponent&) = delete; @@ -91,7 +91,7 @@ public: * stanza, and explanation being a short human-readable sentence * describing the error. */ - void send_stream_error(const std::string& message, const std::string& explanation); + void send_stream_error(const std::string& name, const std::string& explanation); /** * Send error stanza, described in http://xmpp.org/rfcs/rfc6120.html#stanzas-error */ @@ -143,7 +143,7 @@ public: /** * 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); + void send_muc_leave(const std::string& muc_name, const std::string& nick, Xmpp::body&& message, const std::string& jid_to, const bool self); /** * Indicate that a participant changed his nick */ @@ -179,10 +179,6 @@ public: 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. */ diff --git a/louloulibs/xmpp/xmpp_parser.cpp b/src/xmpp/xmpp_parser.cpp index 0488be9..0488be9 100644 --- a/louloulibs/xmpp/xmpp_parser.cpp +++ b/src/xmpp/xmpp_parser.cpp diff --git a/louloulibs/xmpp/xmpp_parser.hpp b/src/xmpp/xmpp_parser.hpp index 9d67228..9d67228 100644 --- a/louloulibs/xmpp/xmpp_parser.hpp +++ b/src/xmpp/xmpp_parser.hpp diff --git a/louloulibs/xmpp/xmpp_stanza.cpp b/src/xmpp/xmpp_stanza.cpp index ac6ce9b..ac6ce9b 100644 --- a/louloulibs/xmpp/xmpp_stanza.cpp +++ b/src/xmpp/xmpp_stanza.cpp diff --git a/louloulibs/xmpp/xmpp_stanza.hpp b/src/xmpp/xmpp_stanza.hpp index 4ca758e..f4b3948 100644 --- a/louloulibs/xmpp/xmpp_stanza.hpp +++ b/src/xmpp/xmpp_stanza.hpp @@ -143,4 +143,18 @@ std::ostream& operator<<(std::ostream& os, const XmlNode& node); */ using Stanza = XmlNode; +class XmlSubNode: public XmlNode +{ +public: + XmlSubNode(XmlNode& parent_ref, const std::string& name): + XmlNode(name), + parent_to_add(parent_ref) + {} + ~XmlSubNode() + { + this->parent_to_add.add_child(std::move(*this)); + } +private: + XmlNode& parent_to_add; +};
\ No newline at end of file diff --git a/tests/config.cpp b/tests/config.cpp index a6fa92a..ec9844f 100644 --- a/tests/config.cpp +++ b/tests/config.cpp @@ -8,7 +8,7 @@ TEST_CASE("Config basic") { // Disable all output for this test - IoTester<std::ostream> out(std::cout); + IoTester<std::ostream> out(std::cerr); // Write a value in the config file Config::read_conf("test.cfg"); Config::set("coucou", "bonjour", true); diff --git a/tests/end_to_end/__main__.py b/tests/end_to_end/__main__.py index 7658d92..088be9c 100644 --- a/tests/end_to_end/__main__.py +++ b/tests/end_to_end/__main__.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import collections +import datetime import slixmpp import asyncio import logging @@ -95,7 +96,11 @@ class XMPPComponent(slixmpp.BaseXMPP): def run_scenario(self): if self.scenario.steps: step = self.scenario.steps.pop(0) - step(self, self.biboumi) + try: + step(self, self.biboumi) + except Exception as e: + self.error(e) + self.run_scenario() else: if self.biboumi: self.biboumi.stop() @@ -120,7 +125,7 @@ def match(stanza, xpath): 'commands': 'http://jabber.org/protocol/commands', 'dataform': 'jabber:x:data', 'version': 'jabber:iq:version', - 'mam': 'urn:xmpp:mam:1', + 'mam': 'urn:xmpp:mam:2', 'delay': 'urn:xmpp:delay', 'forward': 'urn:xmpp:forward:0', 'client': 'jabber:client', @@ -261,6 +266,16 @@ def expect_stanza(xpaths, xmpp, biboumi, optional=False, after=None): else: print("Warning, from argument type passed to expect_stanza: %s" % (type(xpaths))) +def save_datetime(xmpp, biboumi): + xmpp.saved_values["saved_datetime"] = datetime.datetime.now() + asyncio.get_event_loop().call_soon(xmpp.run_scenario) + +def expect_now_is_after(timedelta, xmpp, biboumi): + time_passed = datetime.datetime.now() - xmpp.saved_values["saved_datetime"] + if time_passed < timedelta: + raise StanzaError("Not enough time has passed: %s instead of %s" % (time_passed, timedelta)) + asyncio.get_event_loop().call_soon(xmpp.run_scenario) + # list_of_xpaths: [(xpath, xpath), (xpath, xpath), (xpath)] def expect_unordered(list_of_xpaths, xmpp, biboumi): formatted_list_of_xpaths = [] @@ -275,10 +290,6 @@ def expect_unordered(list_of_xpaths, xmpp, biboumi): def expect_unordered_already_formatted(formatted_list_of_xpaths, xmpp, biboumi): xmpp.stanza_checker = partial(check_list_of_xpath, formatted_list_of_xpaths, xmpp) -def log_message(message, xmpp, biboumi): - print("[33;1m%s[0m" % (message,)) - asyncio.get_event_loop().call_soon(xmpp.run_scenario) - class BiboumiTest: """ @@ -345,7 +356,9 @@ confs = { password=coucou db_name=e2e_test.sqlite port=8811 -admin=admin@example.com""", +admin=admin@example.com +identd_port=1113 +outgoing_bind=127.0.0.1""", 'fixed_server': """hostname=biboumi.localhost @@ -354,11 +367,14 @@ db_name=e2e_test.sqlite port=8811 fixed_irc_server=irc.localhost admin=admin@example.com +identd_port=1113 """} common_replacements = { 'irc_server_one': 'irc.localhost@biboumi.localhost', + 'irc_server_two': 'localhost@biboumi.localhost', 'irc_host_one': 'irc.localhost', + 'irc_host_two': 'localhost', 'biboumi_host': 'biboumi.localhost', 'resource_one': 'resource1', 'resource_two': 'resource2', @@ -380,8 +396,8 @@ def handshake_sequence(): 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')]" + xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']" + xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]" return ( partial(expect_stanza, xpath % ('Connecting to %s:6697 (encrypted)' % irc_host)), @@ -395,25 +411,47 @@ def connection_begin_sequence(irc_host, jid): 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 + # These five messages can be receive in any order partial(expect_stanza, - xpath_re % (r'^%s: \*\*\* (Checking Ident|Looking up your hostname...)$' % irc_host)), + xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')), partial(expect_stanza, - xpath_re % (r'^%s: \*\*\* (Checking Ident|Looking up your hostname...)$' % irc_host)), - # These three messages can be received in any order + xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')), partial(expect_stanza, - xpath_re % (r'^%s: (\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* No Ident response)$' % irc_host)), + xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')), partial(expect_stanza, - xpath_re % (r'^%s: (\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* No Ident response)$' % irc_host)), + xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')), partial(expect_stanza, - xpath_re % (r'^%s: (\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* No Ident response)$' % irc_host)), + xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')), ) +def connection_tls_begin_sequence(irc_host, jid): + jid = jid.format_map(common_replacements) + xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']" + xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]" + irc_host = 'irc.localhost' + return ( + partial(expect_stanza, + xpath % ('Connecting to %s:7778 (encrypted)' % irc_host)), + partial(expect_stanza, + xpath % 'Connected to IRC server (encrypted).'), + # These five messages can be receive in any order + partial(expect_stanza, + xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)), + partial(expect_stanza, + xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)), + partial(expect_stanza, + xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)), + partial(expect_stanza, + xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)), + partial(expect_stanza, + xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got 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')]" + xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']" + xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]" + irc_host = 'irc.localhost' return ( partial(expect_stanza, xpath_re % (r'^%s: Your host is .*$' % irc_host)), @@ -436,13 +474,16 @@ def connection_end_sequence(irc_host, jid): 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\]$'), + xpath_re % r'^User mode for \w+ is \[\+Z?i\]$'), ) def connection_sequence(irc_host, jid): return connection_begin_sequence(irc_host, jid) + connection_end_sequence(irc_host, jid) +def connection_tls_sequence(irc_host, jid): + return connection_tls_begin_sequence(irc_host, jid) + connection_end_sequence(irc_host, jid) + def extract_attribute(xpath, name, stanza): matched = match(stanza, xpath) @@ -452,7 +493,6 @@ def extract_attribute(xpath, name, stanza): def save_value(name, func, stanza, xmpp): xmpp.saved_values[name] = func(stanza) - if __name__ == '__main__': atexit.register(asyncio.get_event_loop().close) @@ -471,6 +511,20 @@ if __name__ == '__main__': "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), connection_sequence("irc.localhost", '{jid_one}/{resource_one}'), ]), + Scenario("irc_server_connection_failure", + [ + handshake_sequence(), + partial(send_stanza, + "<presence from='{jid_one}/{resource_one}' to='#foo%doesnotexist@{biboumi_host}/{nick_one}' />"), + partial(expect_stanza, + "/message/body[text()='Connecting to doesnotexist:6697 (encrypted)']"), + partial(expect_stanza, + "/message/body[re:test(text(), 'Connection failed: (Domain name not found|Name or service not known)')]"), + partial(expect_stanza, + ("/presence[@from='#foo%doesnotexist@{biboumi_host}/{nick_one}']/muc:x", + "/presence/error[@type='cancel']/stanza:item-not-found", + "/presence/error[@type='cancel']/stanza:text[re:test(text(), '(Domain name not found|Name or service not known)')]")), + ]), Scenario("simple_channel_join", [ handshake_sequence(), @@ -502,6 +556,22 @@ if __name__ == '__main__': partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Closing Link: localhost (Client Quit)']"), partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Connection closed.']"), ]), + Scenario("not_connected_error", + [ + handshake_sequence(), + partial(send_stanza, + "<presence type='unavailable' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), + 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("irc_server_disconnection", [ handshake_sequence(), @@ -534,8 +604,6 @@ if __name__ == '__main__': [ 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}'), @@ -548,8 +616,6 @@ if __name__ == '__main__': 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}'), @@ -561,6 +627,48 @@ if __name__ == '__main__': ("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]",), ]), ]), + Scenario("channel_join_with_password", + [ + 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())]"), + + # Set a password in the room, by using /mode +k + partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>/mode +k SECRET</body></message>"), + partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='Mode #foo [+k SECRET] by {nick_one}']"), + + # Second user tries to join, without a password + 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, "/message/body[text()='{irc_host_one}: #foo: Cannot join channel (+k) - bad key']"), + partial(expect_stanza, + "/presence[@type='error'][@from='#foo%{irc_server_one}/{nick_two}']/error[@type='auth']/stanza:not-authorized", + ), + + # Second user joins, with a password + partial(send_stanza, + "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}'> <x xmlns='http://jabber.org/protocol/muc'><password>SECRET</password></x></presence>"), + # connection_sequence("irc.localhost", '{jid_two}/{resource_one}'), + partial(expect_unordered, [ + ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='~bobby@localhost']",), + ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",), + ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']", + "/presence/muc_user:x/muc_user:status[@code='110']",), + ("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]",), + ]), + + ]), Scenario("channel_custom_topic", [ handshake_sequence(), @@ -641,8 +749,6 @@ if __name__ == '__main__': 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(), @@ -692,7 +798,6 @@ if __name__ == '__main__': [ handshake_sequence(), - partial(log_message, "Join a channel"), partial(send_stanza, "<presence from='{jid_admin}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), connection_sequence("irc.localhost", '{jid_admin}/{resource_one}'), partial(expect_stanza, "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"), @@ -710,6 +815,75 @@ if __name__ == '__main__': # Note, charybdis ignores our QUIT message, so we can't test it partial(expect_stanza, "/presence[@type='unavailable'][@to='{jid_admin}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']"), ]), + Scenario("execute_admin_disconnect_from_server_adhoc_command", + [ + handshake_sequence(), + + # Admin connects to first server + partial(send_stanza, "<presence from='{jid_admin}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' />"), + connection_sequence("irc.localhost", '{jid_admin}/{resource_one}'), + partial(expect_stanza, "/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"), + partial(expect_stanza, "/presence"), + partial(expect_stanza, "/message"), + + # Non-Admin connects to first server + partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"), + 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"), + partial(expect_stanza, "/message"), + + # Non-admin connects to second server + partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#bon%{irc_server_two}/{nick_three}' />"), + connection_sequence("localhost", '{jid_one}/{resource_one}'), + partial(expect_stanza, "/message/body[text()='Mode #bon [+nt] by {irc_host_one}']"), + partial(expect_stanza, "/presence"), + partial(expect_stanza, "/message"), + + # Execute as admin + partial(send_stanza, "<iq type='set' id='command1' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' action='execute' /></iq>"), + partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='jid'][@type='list-single']/dataform:option[@label='{jid_one}']/dataform:value[text()='{jid_one}']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='jid'][@type='list-single']/dataform:option[@label='{jid_admin}']/dataform:value[text()='{jid_admin}']", + "/iq/commands:command/commands:actions/commands:next", + ), + after = partial(save_value, "sessionid", partial(extract_attribute, "/iq/commands:command[@node='disconnect-from-irc-server']", "sessionid")) + ), + partial(send_stanza, "<iq type='set' id='command2' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='jid'><value>{jid_one}</value></field><field var='quit-message'><value>e2e test one</value></field></x></command></iq>"), + + partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='quit-message'][@type='text-single']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='irc-servers'][@type='list-multi']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='irc-servers']/dataform:option[@label='localhost']/dataform:value[text()='localhost']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='irc-servers']/dataform:option[@label='irc.localhost']/dataform:value[text()='irc.localhost']", + "/iq/commands:command/commands:actions/commands:next", + ), + after = partial(save_value, "sessionid", partial(extract_attribute, "/iq/commands:command[@node='disconnect-from-irc-server']", "sessionid")) + ), + partial(send_stanza, "<iq type='set' id='command2' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='irc-servers'><value>localhost</value></field><field var='quit-message'><value>Disconnected by e2e</value></field></x></command></iq>"), + partial(expect_unordered, [("/presence[@type='unavailable'][@to='{jid_one}/{resource_one}'][@from='#bon%{irc_server_two}/{nick_three}']",), + ("/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@status='completed']/commands:note[@type='info'][text()='{jid_one} was disconnected from 1 IRC server.']",), + ("/message[@to='{jid_one}/{resource_one}']/body[text()='ERROR: Disconnected by e2e']",), + ]), + + + # Execute as non-admin (this skips the first step) + partial(send_stanza, "<iq type='set' id='command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' action='execute' /></iq>"), + + partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='quit-message'][@type='text-single']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='irc-servers'][@type='list-multi']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='irc-servers']/dataform:option[@label='irc.localhost']/dataform:value[text()='irc.localhost']", + "/iq/commands:command/commands:actions/commands:next", + ), + after = partial(save_value, "sessionid", partial(extract_attribute, "/iq/commands:command[@node='disconnect-from-irc-server']", "sessionid")) + ), + partial(send_stanza, "<iq type='set' id='command2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='irc-servers'><value>irc.localhost</value></field><field var='quit-message'><value>Disconnected by e2e</value></field></x></command></iq>"), + partial(expect_unordered, [("/presence[@type='unavailable'][@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']",), + ("/iq[@type='result']/commands:command[@node='disconnect-from-irc-server'][@status='completed']/commands:note[@type='info'][text()='{jid_one}/{resource_one} was disconnected from 1 IRC server.']",), + ("/message[@to='{jid_one}/{resource_one}']/body[text()='ERROR: Disconnected by e2e']",), + ]), + ]), Scenario("multisessionnick", [ handshake_sequence(), @@ -759,7 +933,6 @@ if __name__ == '__main__': partial(expect_stanza, "/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_two}'][@type='chat']/body[text()='RELLO']"), - partial(log_message, "Nickname conflict"), # First occupant (with the two resources) changes her/his nick partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"), partial(expect_unordered, [ @@ -769,7 +942,6 @@ if __name__ == '__main__': ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_two}'][@type='error']",), ]), - # partial(log_message, "Nickname change"), # First occupant (with the two resources) changes her/his nick partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}' />"), partial(expect_unordered, [ @@ -808,6 +980,33 @@ if __name__ == '__main__': ("/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='second']",), ]), ]), + Scenario("channel_join_with_different_nick", + [ + 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 same resource joins a different channel with a different nick + partial(send_stanza, + "<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' />"), + + # We must receive a join presence in response, without any nick change (nick_two) must be ignored + partial(expect_stanza, + "/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"), + partial(expect_stanza, + ("/presence[@to='{jid_one}/{resource_one}'][@from='#bar%{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='#bar%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[not(text())]"), + ]), Scenario("channel_messages", [ handshake_sequence(), @@ -983,10 +1182,8 @@ if __name__ == '__main__': ]), # 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_unordered, [ ("/presence[@type='unavailable'][@to='{jid_two}/{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']", @@ -1000,6 +1197,82 @@ if __name__ == '__main__': ("/iq[@id='kick1'][@type='result']",), ]), ]), + Scenario("mode_change", + [ + 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_unordered, [ + ("/presence/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",), + ("/presence/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",), + ("/presence/muc_user:x/muc_user:status[@code='110']",), + ("/message/subject",), + ]), + + # Change a user mode with a message starting with /mode + partial(send_stanza, + "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>/mode +v {nick_two}</body></message>"), + partial(expect_unordered, [ + ("/message[@to='{jid_one}/{resource_one}']/body[text()='Mode #foo [+v {nick_two}] by {nick_one}']",), + ("/message[@to='{jid_two}/{resource_one}']/body[text()='Mode #foo [+v {nick_two}] by {nick_one}']",), + ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='member'][@role='participant']",), + ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='member'][@role='participant']",) + ]), + + # using an iq + partial(send_stanza, + "<iq from='{jid_one}/{resource_one}' id='id1' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item affiliation='admin' nick='{nick_two}'/></query></iq>"), + partial(expect_unordered, [ + ("/message[@to='{jid_one}/{resource_one}']/body[text()='Mode #foo [+o {nick_two}] by {nick_one}']",), + ("/message[@to='{jid_two}/{resource_one}']/body[text()='Mode #foo [+o {nick_two}] by {nick_one}']",), + ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",), + ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",), + ("/iq[@id='id1'][@type='result'][@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}']",), + ]), + + # remove the mode + partial(send_stanza, + "<iq from='{jid_one}/{resource_one}' id='id1' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item affiliation='member' nick='{nick_two}' role='participant'/></query></iq>"), + partial(expect_unordered, [ + ("/message[@to='{jid_one}/{resource_one}']/body[text()='Mode #foo [+v-o {nick_two} {nick_two}] by {nick_one}']",), + ("/message[@to='{jid_two}/{resource_one}']/body[text()='Mode #foo [+v-o {nick_two} {nick_two}] by {nick_one}']",), + ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='member'][@role='participant']",), + ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='member'][@role='participant']",), + ("/iq[@id='id1'][@type='result'][@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}']",), + ]), + + # using an iq, an a non-existant nick + partial(send_stanza, + "<iq from='{jid_one}/{resource_one}' id='id1' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item affiliation='admin' nick='blectre'/></query></iq>"), + partial(expect_stanza, "/iq[@type='error']"), + + # using an iq, without the rights to do it + partial(send_stanza, + "<iq from='{jid_two}/{resource_one}' id='id1' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item affiliation='admin' nick='{nick_one}'/></query></iq>"), + partial(expect_unordered, [ + ("/iq[@type='error']",), + ("/message[@type='chat'][@to='{jid_two}/{resource_one}']",), + ]), + + # using an iq, with an unknown mode + partial(send_stanza, + "<iq from='{jid_two}/{resource_one}' id='id1' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#admin'><item affiliation='owner' nick='{nick_one}'/></query></iq>"), + partial(expect_unordered, [ + ("/iq[@type='error']",), + ("/message[@type='chat'][@to='{jid_two}/{resource_one}']",), + ]), + + ]), Scenario("multisession_kick", [ handshake_sequence(), @@ -1033,10 +1306,8 @@ if __name__ == '__main__': 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_unordered, [ ("/presence[@type='unavailable'][@to='{jid_two}/{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']", @@ -1121,7 +1392,6 @@ if __name__ == '__main__': ]), Scenario("version_on_global_nick", [ - partial(log_message, "Joining the channel"), handshake_sequence(), partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), @@ -1134,15 +1404,12 @@ if __name__ == '__main__': ), partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - partial(log_message, "Send a version request to ourself"), partial(send_stanza, "<iq type='get' from='{jid_one}/{resource_one}' id='first_version' to='{lower_nick_one}%{irc_server_one}'><query xmlns='jabber:iq:version' /></iq>"), - partial(log_message, "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'))), - partial(log_message, "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, @@ -1222,7 +1489,7 @@ if __name__ == '__main__': partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou 2']"), # Retrieve the complete archive - partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id1'><query xmlns='urn:xmpp:mam:1' queryid='qid1' /></iq>"), + partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id1'><query xmlns='urn:xmpp:mam:2' queryid='qid1' /></iq>"), partial(expect_stanza, ("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay", @@ -1238,9 +1505,9 @@ if __name__ == '__main__': # Retrieve an empty archive by specifying an early “end” date partial(send_stanza, """<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id2'> - <query xmlns='urn:xmpp:mam:1' queryid='qid2'> + <query xmlns='urn:xmpp:mam:2' queryid='qid2'> <x xmlns='jabber:x:data' type='submit'> - <field var='FORM_TYPE' type='hidden'> <value>urn:xmpp:mam:1</value></field> + <field var='FORM_TYPE' type='hidden'> <value>urn:xmpp:mam:2</value></field> <field var='end'><value>2000-06-07T00:00:00Z</value></field> </x> </query></iq>"""), @@ -1251,9 +1518,9 @@ if __name__ == '__main__': # Retrieve an empty archive by specifying a late “start” date # (note that this test will break in ~1000 years) partial(send_stanza, """<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id3'> - <query xmlns='urn:xmpp:mam:1' queryid='qid3'> + <query xmlns='urn:xmpp:mam:2' queryid='qid3'> <x xmlns='jabber:x:data' type='submit'> - <field var='FORM_TYPE' type='hidden'> <value>urn:xmpp:mam:1</value></field> + <field var='FORM_TYPE' type='hidden'> <value>urn:xmpp:mam:2</value></field> <field var='start'><value>3016-06-07T00:00:00Z</value></field> </x> </query></iq>"""), @@ -1283,7 +1550,7 @@ if __name__ == '__main__': partial(expect_stanza, "/message[@from='#foo@{biboumi_host}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou 2']"), # Retrieve the complete archive - partial(send_stanza, "<iq to='#foo@{biboumi_host}' from='{jid_one}/{resource_one}' type='set' id='id1'><query xmlns='urn:xmpp:mam:1' queryid='qid1' /></iq>"), + partial(send_stanza, "<iq to='#foo@{biboumi_host}' from='{jid_one}/{resource_one}' type='set' id='id1'><query xmlns='urn:xmpp:mam:2' queryid='qid1' /></iq>"), partial(expect_stanza, ("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay", @@ -1368,7 +1635,6 @@ if __name__ == '__main__': [ handshake_sequence(), - partial(log_message, "Join first channel #foo"), partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), connection_sequence("irc.localhost", '{jid_one}/{resource_one}'), @@ -1380,7 +1646,6 @@ if __name__ == '__main__': ), partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - partial(log_message, "Join second channel #bar"), partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' />"), partial(expect_stanza, @@ -1388,7 +1653,6 @@ if __name__ == '__main__': partial(expect_stanza, "/presence"), partial(expect_stanza, "/message[@from='#bar%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - partial(log_message, "Request the whole channel list"), partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'/></iq>"), partial(expect_stanza, ( "/iq[@type='result']/disco_items:query", @@ -1400,7 +1664,6 @@ if __name__ == '__main__': [ handshake_sequence(), - partial(log_message, "Join first channel #foo"), partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), connection_sequence("irc.localhost", '{jid_one}/{resource_one}'), @@ -1412,7 +1675,6 @@ if __name__ == '__main__': ), partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - partial(log_message, "Join second channel #bar"), partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' />"), partial(expect_stanza, @@ -1420,7 +1682,6 @@ if __name__ == '__main__': partial(expect_stanza, "/presence"), partial(expect_stanza, "/message[@from='#bar%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - partial(log_message, "Join third channel #coucou"), partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#coucou%{irc_server_one}/{nick_one}' />"), partial(expect_stanza, @@ -1428,13 +1689,11 @@ if __name__ == '__main__': partial(expect_stanza, "/presence"), partial(expect_stanza, "/message[@from='#coucou%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - partial(log_message, "Request with max=0"), partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><max>0</max></set></query></iq>"), partial(expect_stanza, ( "/iq[@type='result']/disco_items:query", )), - partial(log_message, "Request with max=2"), partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><max>2</max></set></query></iq>"), partial(expect_stanza, ( "/iq[@type='result']/disco_items:query", @@ -1445,7 +1704,6 @@ if __name__ == '__main__': "/iq/disco_items:query/rsm:set/rsm:count[text()='3']" )), - partial(log_message, "Request with max=12"), partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><max>12</max></set></query></iq>"), partial(expect_stanza, ( "/iq[@type='result']/disco_items:query", @@ -1457,7 +1715,6 @@ if __name__ == '__main__': "/iq/disco_items:query/rsm:set/rsm:count[text()='3']" )), - partial(log_message, "Request with max=1 after=#bar"), partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#bar%{irc_server_one}</after><max>1</max></set></query></iq>"), partial(expect_stanza, ( "/iq[@type='result']/disco_items:query", @@ -1467,7 +1724,6 @@ if __name__ == '__main__': "/iq/disco_items:query/rsm:set/rsm:count[text()='3']" )), - partial(log_message, "Request with max=1 after=#bar"), partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#bar%{irc_server_one}</after><max>1</max></set></query></iq>"), partial(expect_stanza, ( "/iq[@type='result']/disco_items:query", @@ -1481,7 +1737,6 @@ if __name__ == '__main__': [ handshake_sequence(), - partial(log_message, "Join 10 channels"), partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#aaa%{irc_server_one}/{nick_one}' />"), connection_sequence("irc.localhost", '{jid_one}/{resource_one}'), @@ -1543,7 +1798,6 @@ if __name__ == '__main__': partial(expect_stanza, "/presence"), partial(expect_stanza, "/message"), - partial(log_message, "Request the first page, with a limit of 3"), partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><max>3</max></set></query></iq>"), partial(expect_stanza, ( "/iq[@type='result']/disco_items:query", @@ -1554,7 +1808,6 @@ if __name__ == '__main__': "/iq/disco_items:query/rsm:set/rsm:last[text()='#ccc%{irc_server_one}']" )), - partial(log_message, "Request subsequent pages"), partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#ccc%{irc_server_one}</after><max>3</max></set></query></iq>"), partial(expect_stanza, ( "/iq[@type='result']/disco_items:query", @@ -1584,7 +1837,6 @@ if __name__ == '__main__': "/iq/disco_items:query/rsm:set/rsm:count[text()='10']" )), - partial(log_message, "Leaving the 10 channels"), partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#aaa%{irc_server_one}/{nick_one}' type='unavailable' />"), partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#bbb%{irc_server_one}/{nick_one}' type='unavailable' />"), partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#ccc%{irc_server_one}/{nick_one}' type='unavailable' />"), @@ -1612,8 +1864,40 @@ if __name__ == '__main__': partial(send_stanza, "<iq from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' id='1' type='get'><query xmlns='http://jabber.org/protocol/disco#info' node='http://jabber.org/protocol/muc#traffic'/></iq>"), - partial(expect_stanza, "/iq[@type='result']/disco_info:query[@node='http://jabber.org/protocol/muc#traffic']"), + partial(expect_stanza, "/iq[@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='result']/disco_info:query[@node='http://jabber.org/protocol/muc#traffic']"), ]), + Scenario("muc_disco_info", + [ + handshake_sequence(), + + partial(send_stanza, + "<iq from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' id='1' type='get'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>"), + partial(expect_stanza, + ("/iq[@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='result']/disco_info:query", + "/iq[@type='result']/disco_info:query/disco_info:identity[@category='conference'][@type='irc'][@name='IRC channel #foo from server {irc_host_one} over biboumi']", + "/iq/disco_info:query/disco_info:feature[@var='jabber:iq:version']", + "/iq/disco_info:query/disco_info:feature[@var='http://jabber.org/protocol/commands']", + "/iq/disco_info:query/disco_info:feature[@var='urn:xmpp:ping']", + "/iq/disco_info:query/disco_info:feature[@var='urn:xmpp:mam:2']", + "/iq/disco_info:query/disco_info:feature[@var='jabber:iq:version']", + )), + ]), + Scenario("fixed_muc_disco_info", + [ + handshake_sequence(), + + partial(send_stanza, + "<iq from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' id='1' type='get'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>"), + partial(expect_stanza, + ("/iq[@from='#foo@{biboumi_host}'][@to='{jid_one}/{resource_one}'][@type='result']/disco_info:query", + "/iq[@type='result']/disco_info:query/disco_info:identity[@category='conference'][@type='irc'][@name='IRC channel #foo from server {irc_host_one} over biboumi']", + "/iq/disco_info:query/disco_info:feature[@var='jabber:iq:version']", + "/iq/disco_info:query/disco_info:feature[@var='http://jabber.org/protocol/commands']", + "/iq/disco_info:query/disco_info:feature[@var='urn:xmpp:ping']", + "/iq/disco_info:query/disco_info:feature[@var='urn:xmpp:mam:2']", + "/iq/disco_info:query/disco_info:feature[@var='jabber:iq:version']", + )), + ], conf='fixed_server'), Scenario("raw_message", [ handshake_sequence(), @@ -1635,7 +1919,7 @@ if __name__ == '__main__': "/iq/disco_info:query/disco_info:feature[@var='jabber:iq:version']", "/iq/disco_info:query/disco_info:feature[@var='http://jabber.org/protocol/commands']", "/iq/disco_info:query/disco_info:feature[@var='urn:xmpp:ping']", - "/iq/disco_info:query/disco_info:feature[@var='urn:xmpp:mam:1']", + "/iq/disco_info:query/disco_info:feature[@var='urn:xmpp:mam:2']", "/iq/disco_info:query/disco_info:feature[@var='jabber:iq:version']", )), ]), @@ -1680,7 +1964,6 @@ if __name__ == '__main__': partial(expect_stanza, "/message[@to='{jid_one}/{resource_two}'][@from='%{irc_server_one}'][@type='groupchat']/subject[re:test(text(), '^This is a virtual channel.*$')]"), - partial(log_message, "Nick change"), partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' />"), partial(expect_unordered, [ @@ -1698,13 +1981,11 @@ if __name__ == '__main__': ]), - partial(log_message, "First resource leaves."), partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' />"), partial(expect_stanza, ("/presence[@type='unavailable'][@from='%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}']/muc_user:x/muc_user:status[@code='110']", "/presence/status[text()='Biboumi note: 1 resources are still in this channel.']",) ), - partial(log_message, "Second resource leaves."), partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_two}' to='%{irc_server_one}/{nick_two}' />"), partial(expect_stanza, "/presence[@type='unavailable'][@from='%{irc_server_one}/{nick_two}']"), partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Closing Link: localhost (Client Quit)']"), @@ -1792,6 +2073,7 @@ if __name__ == '__main__': "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='realname']/dataform:value[text()='realname']", "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']/dataform:value[text()='latin-1']", "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_out']/dataform:value[text()='UTF-8']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='linger_time']/dataform:value[text()='0']", "/iq/commands:command/commands:actions/commands:next", ), after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid")) @@ -1799,15 +2081,139 @@ if __name__ == '__main__': partial(send_stanza, "<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' action='cancel' node='configure' sessionid='{sessionid}' /></iq>"), partial(expect_stanza, "/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"), ]), + Scenario("irc_channel_configure", + [ + handshake_sequence(), + partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"), + partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_out']", + ), + after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid")) + ), + partial(send_stanza, "<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'>" + "<command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='next'>" + "<x xmlns='jabber:x:data' type='submit'>" + "<field var='ports' />" + "<field var='encoding_out'><value>UTF-8</value></field>" + "<field var='encoding_in'><value>latin-1</value></field>" + "</x></command></iq>"), + partial(expect_stanza, "/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"), + + partial(send_stanza, "<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"), + partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure the IRC channel #foo on server irc.localhost']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']/dataform:value[text()='latin-1']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_out']/dataform:value[text()='UTF-8']", + "/iq/commands:command/commands:actions/commands:next", + ), + after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid")) + ), + partial(send_stanza, "<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' action='cancel' node='configure' sessionid='{sessionid}' /></iq>"), + partial(expect_stanza, "/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"), + ]), + Scenario("irc_channel_configure_fixed", + [ + handshake_sequence(), + partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"), + partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_out']", + ), + after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid")) + ), + partial(send_stanza, "<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}'>" + "<command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='next'>" + "<x xmlns='jabber:x:data' type='submit'>" + "<field var='ports' />" + "<field var='encoding_out'><value>UTF-8</value></field>" + "<field var='encoding_in'><value>latin-1</value></field>" + "</x></command></iq>"), + partial(expect_stanza, "/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"), + + partial(send_stanza, "<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"), + partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure the IRC channel #foo on server irc.localhost']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']/dataform:value[text()='latin-1']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_out']/dataform:value[text()='UTF-8']", + "/iq/commands:command/commands:actions/commands:next", + ), + after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid")) + ), + partial(send_stanza, "<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' action='cancel' node='configure' sessionid='{sessionid}' /></iq>"), + partial(expect_stanza, "/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"), + ], conf='fixed_server'), + Scenario("irc_server_linger_time", + [ + handshake_sequence(), + # Set a custom value for the linger_time option, using the ad-hoc command + partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"), + partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure the IRC server irc.localhost']", + "/iq/commands:command/commands:actions/commands:next", + ), + after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid")) + ), + partial(send_stanza, "<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='{irc_server_one}'>" + "<command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='next'>" + "<x xmlns='jabber:x:data' type='submit'>" + "<field var='linger_time'><value>3</value></field>" + "</x></command></iq>"), + partial(expect_stanza, "/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"), + + 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())]"), + + partial(save_datetime), + partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), + partial(expect_stanza, "/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}']"), + partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Closing Link: localhost (Client Quit)']"), + partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Connection closed.']"), + partial(expect_now_is_after, datetime.timedelta(seconds=3)), + ]), + Scenario("irc_tls_connection", + [ + handshake_sequence(), + # First, use an adhoc command to configure how we connect to the irc server, configure + # only one TLS port, and disable the cert verification. + partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"), + partial(expect_stanza, "/iq[@type='result']", + after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid"))), + partial(send_stanza, "<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='{irc_server_one}'>" + "<command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='next'>" + "<x xmlns='jabber:x:data' type='submit'>" + "<field var='ports' />" + "<field var='tls_ports'><value>7778</value></field>" + "<field var='verify_cert'><value>0</value></field>" + "</x></command></iq>"), + partial(expect_stanza, "/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"), + + partial(send_stanza, + "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), + connection_tls_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("get_irc_connection_info", [ handshake_sequence(), - partial(log_message, "Not connected yet"), partial(send_stanza, "<iq type='set' id='command1' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='get-irc-connection-info' action='execute' /></iq>"), partial(expect_stanza, "/iq/commands:command/commands:note[text()='You are not connected to the IRC server irc.localhost']"), - partial(log_message, "Join one room"), partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), connection_sequence("irc.localhost", '{jid_one}/{resource_one}'), @@ -1822,11 +2228,9 @@ if __name__ == '__main__': [ handshake_sequence(), - partial(log_message, "Not connected yet"), partial(send_stanza, "<iq type='set' id='command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='get-irc-connection-info' action='execute' /></iq>"), partial(expect_stanza, "/iq/commands:command/commands:note[text()='You are not connected to the IRC server irc.localhost']"), - partial(log_message, "Join one room"), partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' />"), connection_sequence("irc.localhost", '{jid_one}/{resource_one}'), @@ -1853,6 +2257,19 @@ if __name__ == '__main__': "/presence/muc:x", "/presence/error/stanza:text")), ], conf='fixed_server'), + Scenario("irc_server_presence_subscription", + [ + handshake_sequence(), + partial(send_stanza, "<presence type='subscribe' from='{jid_one}/{resource_one}' to='{irc_server_one}' id='sub1' />"), + partial(expect_stanza, "/presence[@to='{jid_one}'][@from='{irc_server_one}'][@type='subscribed']") + ]), + Scenario("fixed_irc_server_presence_subscription", + [ + handshake_sequence(), + partial(send_stanza, "<presence type='subscribe' from='{jid_one}/{resource_one}' to='{biboumi_host}' id='sub1' />"), + partial(expect_stanza, "/presence[@to='{jid_one}'][@from='{biboumi_host}'][@type='subscribed']") + ], conf='fixed_server') + ) @@ -1872,7 +2289,7 @@ if __name__ == '__main__': if b"now running in foreground mode" in res: break print("irc server started.") - print("Running %s checks for biboumi." % (len(scenarios))) + print("Running %s checks for biboumi." % (len([s for s in scenarios if s.name in scenar_list]))) for s in scenarios: if scenar_list and s.name not in scenar_list: diff --git a/tests/end_to_end/ircd.conf b/tests/end_to_end/ircd.conf index 7327531..ec55884 100644 --- a/tests/end_to_end/ircd.conf +++ b/tests/end_to_end/ircd.conf @@ -156,7 +156,7 @@ listen { */ #host = "192.0.2.6"; port = 5000, 6665 .. 6669; - # sslport = 6697; + sslport = 7778; /* Listen on IPv6 (if you used host= above). */ #host = "2001:db8:2::6"; diff --git a/tests/iid.cpp b/tests/iid.cpp index b42b9e5..3da0396 100644 --- a/tests/iid.cpp +++ b/tests/iid.cpp @@ -83,6 +83,12 @@ TEST_CASE("Iid creation") CHECK(iid6.get_local() == "##channel"); CHECK(iid6.get_server() == ""); CHECK(iid6.type == Iid::Type::Channel); + + Iid iid7("", chantypes); + CHECK(std::to_string(iid7) == ""); + CHECK(iid7.get_local() == ""); + CHECK(iid7.get_server() == ""); + CHECK(iid7.type == Iid::Type::None); } TEST_CASE("Iid creation in fixed_server mode") diff --git a/tests/jid.cpp b/tests/jid.cpp index 9015afd..480827b 100644 --- a/tests/jid.cpp +++ b/tests/jid.cpp @@ -1,7 +1,7 @@ #include "catch.hpp" #include <xmpp/jid.hpp> -#include <louloulibs.h> +#include <biboumi.h> TEST_CASE("Jid") { @@ -15,7 +15,10 @@ TEST_CASE("Jid") CHECK(jid2.local == ""); CHECK(jid2.domain == "ツ.coucou"); CHECK(jid2.resource == "coucou@coucou/coucou"); +} +TEST_CASE("jidprep") +{ // Jidprep const std::string badjid("~zigougou™@EpiK-7D9D1FDE.poez.io/Boujour/coucou/slt™"); const std::string correctjid = jidprep(badjid); @@ -26,13 +29,18 @@ TEST_CASE("Jid") 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"); + CHECK(jidprep("Zigougou@poez.io") == "zigougou@poez.io"); + + CHECK(jidprep("~Bisous@88.123.43.45") == "~bisous@88.123.43.45"); + + CHECK(jidprep("~Bisous@::ffff:42.156.139.46") == "~bisous@[::ffff:42.156.139.46]"); + + CHECK(jidprep("louiz!6bf74289@2001:bc8:38e7::") == "louiz!6bf74289@[2001:bc8:38e7::]"); - const std::string crappy("~Bisous@7ea8beb1:c5fd2849:da9a048e:ip"); - const std::string fixed_crappy = jidprep(crappy); - CHECK(fixed_crappy == "~bisous@7ea8beb1-c5fd2849-da9a048e-ip"); + CHECK(jidprep("louiz@+:::::----coucou.com78--.") == "louiz@coucou.com78"); + CHECK(jidprep("louiz@coucou.com78--.") == "louiz@coucou.com78"); + CHECK(jidprep("louiz@+:::::----coucou.com78") == "louiz@coucou.com78"); + CHECK(jidprep("louiz@:::::") == "louiz@empty"); #else // Without libidn, jidprep always returns an empty string CHECK(jidprep(badjid) == ""); #endif diff --git a/tests/utils.cpp b/tests/utils.cpp index 48951da..b8a3e75 100644 --- a/tests/utils.cpp +++ b/tests/utils.cpp @@ -8,6 +8,8 @@ #include <utils/empty_if_fixed_server.hpp> #include <utils/get_first_non_empty.hpp> #include <utils/time.hpp> +#include <utils/system.hpp> +#include <utils/scopeguard.hpp> using namespace std::string_literals; @@ -140,3 +142,19 @@ TEST_CASE("parse_datetime") CHECK(utils::parse_datetime("1970-01-02T00:00:12*00:00") == -1); CHECK(utils::parse_datetime("1970-01-02T00:00:12+0000") == -1); } + +TEST_CASE("scope_guard") +{ + bool res = false; + { + auto guard = utils::make_scope_guard([&res](){ res = true; }); + CHECK(!res); + } + CHECK(res); +} + +TEST_CASE("system_name") +{ + CHECK(utils::get_system_name() != "Unknown"); + CHECK(!utils::get_system_name().empty()); +}
\ No newline at end of file diff --git a/tests/xmpp.cpp b/tests/xmpp.cpp index 37d2b54..42b7c08 100644 --- a/tests/xmpp.cpp +++ b/tests/xmpp.cpp @@ -52,3 +52,20 @@ TEST_CASE("handshake_digest") const auto res = get_handshake_digest("id1234", "S4CR3T"); CHECK(res == "c92901b5d376ad56269914da0cce3aab976847df"); } + +TEST_CASE("substanzas") +{ + Stanza a("a"); + { + XmlSubNode b(a, "b"); + { + CHECK(!a.has_children()); + XmlSubNode c(b, "c"); + XmlSubNode d(b, "d"); + CHECK(!c.has_children()); + CHECK(!d.has_children()); + } + CHECK(b.has_children()); + } + CHECK(a.has_children()); +}
\ No newline at end of file |