summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.codecov.yml3
-rw-r--r--.gitlab-ci.yml304
-rw-r--r--.gitlab/issue_templates/Bug.md6
-rw-r--r--CHANGELOG.rst34
-rw-r--r--CMakeLists.txt371
-rw-r--r--CONTRIBUTING.rst45
-rw-r--r--INSTALL.rst26
-rw-r--r--README.rst13
-rw-r--r--biboumi.h.cmake1
-rw-r--r--cmake/Modules/CodeCoverage.cmake7
-rw-r--r--cmake/Modules/FindBOTAN.cmake (renamed from louloulibs/cmake/Modules/FindBOTAN.cmake)6
-rw-r--r--cmake/Modules/FindGCRYPT.cmake41
-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)11
-rw-r--r--cmake/Modules/FindLIBUUID.cmake (renamed from louloulibs/cmake/Modules/FindLIBUUID.cmake)9
-rw-r--r--cmake/Modules/FindLITESQL.cmake76
-rw-r--r--cmake/Modules/FindSQLITE3.cmake43
-rw-r--r--cmake/Modules/FindSYSTEMD.cmake (renamed from louloulibs/cmake/Modules/FindSYSTEMD.cmake)11
-rw-r--r--cmake/Modules/FindUDNS.cmake38
-rw-r--r--conf/irc.mozilla.org.policy.txt1
-rw-r--r--conf/irc.ppirc.net.policy.txt2
-rw-r--r--conf/policy.txt2
-rw-r--r--database/database.xml68
-rw-r--r--doc/biboumi.1.rst186
-rw-r--r--docker/biboumi-test/alpine/Dockerfile53
-rw-r--r--docker/biboumi-test/archlinux/Dockerfile13
-rw-r--r--docker/biboumi-test/debian/Dockerfile92
-rw-r--r--docker/biboumi-test/fedora/Dockerfile91
-rw-r--r--docker/biboumi/Dockerfile41
-rw-r--r--docker/biboumi/README.md68
-rw-r--r--docker/biboumi/biboumi.cfg6
-rw-r--r--docker/packaging/debian/Dockerfile10
-rw-r--r--louloulibs/CMakeLists.txt167
-rw-r--r--louloulibs/cmake/Modules/FindCARES.cmake37
-rw-r--r--louloulibs/network/dns_handler.cpp130
-rw-r--r--louloulibs/network/dns_handler.hpp58
-rw-r--r--louloulibs/network/dns_socket_handler.cpp49
-rw-r--r--louloulibs/network/dns_socket_handler.hpp49
-rw-r--r--louloulibs/network/resolver.cpp216
-rw-r--r--louloulibs/utils/sha1.cpp121
-rw-r--r--louloulibs/utils/sha1.hpp33
-rw-r--r--louloulibs/xmpp/auth.cpp21
-rw-r--r--louloulibs/xmpp/jid.cpp99
-rw-r--r--packaging/biboumi.spec.cmake15
-rw-r--r--src/biboumi.h.cmake (renamed from louloulibs/louloulibs.h.cmake)5
-rw-r--r--src/bridge/bridge.cpp218
-rw-r--r--src/bridge/bridge.hpp24
-rw-r--r--src/bridge/colors.cpp2
-rw-r--r--src/bridge/colors.hpp2
-rw-r--r--src/config/config.cpp (renamed from louloulibs/config/config.cpp)55
-rw-r--r--src/config/config.hpp (renamed from louloulibs/config/config.hpp)3
-rw-r--r--src/database/column.hpp17
-rw-r--r--src/database/count_query.hpp35
-rw-r--r--src/database/database.cpp223
-rw-r--r--src/database/database.hpp162
-rw-r--r--src/database/insert_query.hpp129
-rw-r--r--src/database/query.cpp34
-rw-r--r--src/database/query.hpp90
-rw-r--r--src/database/row.hpp75
-rw-r--r--src/database/select_query.hpp127
-rw-r--r--src/database/statement.hpp35
-rw-r--r--src/database/table.cpp25
-rw-r--r--src/database/table.hpp127
-rw-r--r--src/database/type_to_sql.cpp9
-rw-r--r--src/database/type_to_sql.hpp16
-rw-r--r--src/identd/identd_server.hpp39
-rw-r--r--src/identd/identd_socket.cpp63
-rw-r--r--src/identd/identd_socket.hpp36
-rw-r--r--src/irc/iid.cpp12
-rw-r--r--src/irc/iid.hpp3
-rw-r--r--src/irc/irc_channel.cpp24
-rw-r--r--src/irc/irc_channel.hpp5
-rw-r--r--src/irc/irc_client.cpp235
-rw-r--r--src/irc/irc_client.hpp17
-rw-r--r--src/irc/irc_message.cpp6
-rw-r--r--src/irc/irc_user.cpp2
-rw-r--r--src/irc/irc_user.hpp2
-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.cpp75
-rw-r--r--src/network/credentials_manager.cpp (renamed from louloulibs/network/credentials_manager.cpp)10
-rw-r--r--src/network/credentials_manager.hpp (renamed from louloulibs/network/credentials_manager.hpp)5
-rw-r--r--src/network/dns_handler.cpp46
-rw-r--r--src/network/dns_handler.hpp37
-rw-r--r--src/network/dns_socket_handler.cpp43
-rw-r--r--src/network/dns_socket_handler.hpp33
-rw-r--r--src/network/poller.cpp (renamed from louloulibs/network/poller.cpp)12
-rw-r--r--src/network/poller.hpp (renamed from louloulibs/network/poller.hpp)2
-rw-r--r--src/network/resolver.cpp280
-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.cpp261
-rw-r--r--src/network/tcp_client_socket_handler.hpp82
-rw-r--r--src/network/tcp_server_socket.hpp69
-rw-r--r--src/network/tcp_socket_handler.cpp (renamed from louloulibs/network/tcp_socket_handler.cpp)270
-rw-r--r--src/network/tcp_socket_handler.hpp (renamed from louloulibs/network/tcp_socket_handler.hpp)122
-rw-r--r--src/network/tls_policy.cpp48
-rw-r--r--src/network/tls_policy.hpp28
-rw-r--r--src/utils/dirname.cpp16
-rw-r--r--src/utils/dirname.hpp6
-rw-r--r--src/utils/encoding.cpp (renamed from louloulibs/utils/encoding.cpp)7
-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/optional_bool.hpp35
-rw-r--r--src/utils/reload.cpp2
-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.cpp40
-rw-r--r--src/utils/sha1.hpp5
-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)2
-rw-r--r--src/utils/system.cpp21
-rw-r--r--src/utils/system.hpp8
-rw-r--r--src/utils/time.cpp (renamed from louloulibs/utils/time.cpp)24
-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)23
-rw-r--r--src/utils/timed_events.hpp (renamed from louloulibs/utils/timed_events.hpp)9
-rw-r--r--src/utils/timed_events_manager.cpp (renamed from louloulibs/utils/timed_events_manager.cpp)14
-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)2
-rw-r--r--src/xmpp/adhoc_command.cpp (renamed from louloulibs/xmpp/adhoc_command.cpp)40
-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.cpp8
-rw-r--r--src/xmpp/auth.hpp (renamed from louloulibs/xmpp/auth.hpp)0
-rw-r--r--src/xmpp/biboumi_adhoc_commands.cpp694
-rw-r--r--src/xmpp/biboumi_adhoc_commands.hpp3
-rw-r--r--src/xmpp/biboumi_component.cpp437
-rw-r--r--src/xmpp/biboumi_component.hpp19
-rw-r--r--src/xmpp/body.hpp (renamed from louloulibs/xmpp/body.hpp)0
-rw-r--r--src/xmpp/jid.cpp152
-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)565
-rw-r--r--src/xmpp/xmpp_component.hpp (renamed from louloulibs/xmpp/xmpp_component.hpp)21
-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)2
-rw-r--r--src/xmpp/xmpp_stanza.cpp (renamed from louloulibs/xmpp/xmpp_stanza.cpp)4
-rw-r--r--src/xmpp/xmpp_stanza.hpp (renamed from louloulibs/xmpp/xmpp_stanza.hpp)14
-rw-r--r--tests/config.cpp2
-rw-r--r--tests/database.cpp50
-rw-r--r--tests/end_to_end/__main__.py964
-rw-r--r--tests/end_to_end/ircd.conf9
-rw-r--r--tests/iid.cpp6
-rw-r--r--tests/jid.cpp22
-rw-r--r--tests/network.cpp44
-rw-r--r--tests/utils.cpp31
-rw-r--r--tests/xmpp.cpp26
156 files changed, 6207 insertions, 3219 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..7ef2f8e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,129 +1,313 @@
+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
- 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/
+ # When we extract the artifacts from a previous build, the timestamp of all target are identicall to
+ # the timestamps of the generated source files (like biboumi.h, etc), so Makefile thinks the targets
+ # are not up to date, and everything is rebuilt. We change the modification time of all these files
+ # to be 15 minutes older, to avoid any unnecessary building.
+ - find . \( -name \*.hpp -or -name \*.h -or -name \*.cpp \) -exec touch -r {} -d '-15 minute' {} \;
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"
+ SQLITE3: "-DWITH_SQLITE3=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} ${SQLITE3}"
+ - cmake .. -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3}
+ - make everything -j$(nproc || echo 1)
+ - make coverage_check -j$(nproc || echo 1)
+ artifacts:
+ expire_in: 2 weeks
+ 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
+
+.template:alpine_build: &alpine_build
+ variables:
+ SYSTEMD: "-DWITHOUT_SYSTEMD=1"
+ <<: *basic_build
+ image: docker.louiz.org/biboumi-test-alpine:latest
+
+build:fedora:
+ <<: *fedora_build
+
+build:debian:
+ <<: *debian_build
-image: docker.louiz.org/biboumi-test-fedora:latest
+build:alpine:
+ <<: *alpine_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
+ SQLITE3: "-DWITHOUT_SQLITE3=1"
+ <<: *fedora_build
build:4:
variables:
- LITESQL: "-DWITHOUT_LITESQL=1"
+ SQLITE3: "-DWITHOUT_SQLITE3=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
+ SQLITE3: "-DWITHOUT_SQLITE3=1"
+ 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:
+ expire_in: 2 weeks
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
+ <<: *basic_test
+ dependencies:
+ - build:fedora
+
+test:without_udns:
+ image: docker.louiz.org/biboumi-test-fedora:latest
+ <<: *basic_test
+ dependencies:
+ - build:7
+
+test:alpine:
+ image: docker.louiz.org/biboumi-test-alpine:latest
+ stage: test
tags:
- docker
- <<: *basic_test
+ script:
+ - make e2e
+ dependencies:
+ - build:alpine
-test:coverity:
+test:freebsd:
+ only:
+ - branches@louiz/biboumi
+ tags:
+ - freebsd
+ variables:
+ SYSTEMD: "-DWITHOUT_SYSTEMD=1"
stage: test
+ script:
+ - cmake .. -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3}
+ - 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 | sed s/codecov_//)
+
+.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 | sed s/codecov_//)
+
+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:
+ - branches@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 everything -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
+ only:
+ - master@louiz/biboumi
+ tags:
+ - docker
+ image: docker.louiz.org/biboumi-test-archlinux:latest
+ before_script: []
+ script:
+ - sudo pacman -Syuu --noconfirm
+ - git clone https://aur.archlinux.org/biboumi-git.git
+ - cd biboumi-git
+ - makepkg -si --noconfirm
+ - test -e /usr/bin/biboumi
+ dependencies: []
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
new file mode 100644
index 0000000..a243c9f
--- /dev/null
+++ b/.gitlab/issue_templates/Bug.md
@@ -0,0 +1,6 @@
+### Description of the issue
+
+### Relevant debug logs
+
+```
+```
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 911dfa3..1540abf 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,37 @@
+Version 6.0
+===========
+
+ - The LiteSQL dependency was removed. Only libsqlite3 is now necessary
+ to work with the database.
+ - The RecordHistory option can now also be configured for each IRC channel,
+ individually.
+ - Add a global option to make all channels persistent.
+
+Version 5.0 - 2017-05-24
+========================
+
+ - 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.
+ - Update MAM implementation to version 6.0 (namespace mam:2)
+ - If the client doesn’t specify any limit in its MAM and channel list request,
+ the results returned by biboumi contain at most 100 messages, instead of
+ the potentially huge complete result.
+ - Multiline topics are now properly handled
+ - Configuration options can be overridden by values found in the process env.
+ - Botan’s TLS policies can be customized by the administrator, for each
+ IRC server, with simple text files.
+ - The IRC channel configuration form is now also available using the MUC
+ configuration, in addition to the ad-hoc command.
+ - Notices starting with [#channel] are considered as welcome messages coming
+ from that channel, instead of private messages.
+
Version 4.3 - 2017-05-02
========================
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ca5aa97..596d277 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,26 +2,37 @@ cmake_minimum_required(VERSION 3.0)
project(biboumi)
-set(${PROJECT_NAME}_VERSION_MAJOR 4)
-set(${PROJECT_NAME}_VERSION_MINOR 3)
-set(${PROJECT_NAME}_VERSION_SUFFIX "")
+set(${PROJECT_NAME}_VERSION_MAJOR 6)
+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")
-if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
- set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fprofile-arcs -ftest-coverage --coverage")
+if(NOT CMAKE_BUILD_TYPE)
+ set(CMAKE_BUILD_TYPE "Debug" CACHE STRING
+ "Build type (Release/Debug/RelWithDebInfo/MinSizeRel)" FORCE)
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 $<))\"'")
+#
+## 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)
#
-## Look for external libraries
+## Set various debug flags (instrumentation libs, coverage, …)
#
-set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules/")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++1y -pedantic -Wall -Wextra -Wconversion")
+if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
+ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fprofile-arcs -ftest-coverage --coverage")
+endif()
+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 +61,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 +85,196 @@ if (NOT PANDOC_EXECUTABLE)
endif()
mark_as_advanced(PANDOC_EXECUTABLE)
-# Look for litesql and enable the database if found
-if(WITH_LITESQL)
- find_package(LITESQL REQUIRED)
-elseif(NOT WITHOUT_LITESQL)
- find_package(LITESQL)
+#
+## 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(LITESQL_FOUND)
- LITESQL_GENERATE_CPP("database/database.xml"
- "biboudb"
- LITESQL_GENERATED_SOURCES)
+if(WITH_SYSTEMD)
+ find_package(SYSTEMD REQUIRED)
+elseif(NOT WITHOUT_SYSTEMD)
+ find_package(SYSTEMD)
+endif()
- 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)
+if(WITH_BOTAN)
+ find_package(BOTAN REQUIRED)
+elseif(NOT WITHOUT_BOTAN)
+ find_package(BOTAN)
+endif()
+
+if(NOT BOTAN_FOUND)
+ find_package(GCRYPT REQUIRED)
endif()
-add_subdirectory("louloulibs")
-include_directories("louloulibs")
+if(WITH_UDNS)
+ find_package(UDNS REQUIRED)
+elseif(NOT WITHOUT_UDNS)
+ find_package(UDNS)
+endif()
+
+if(WITH_SQLITE3)
+ find_package(SQLITE3 REQUIRED)
+elseif(NOT WITHOUT_SQLITE3)
+ find_package(SQLITE3)
+endif()
+#
+## 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})
+
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})
-#
-## 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})
-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})
-#
-## 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})
+
+if(SQLITE3_FOUND)
+ file(GLOB source_database
+ src/database/*.[hc]pp)
+ add_library(database OBJECT ${source_database})
+
+ include_directories(database ${SQLITE3_INCLUDE_DIRS})
+ 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>)
+set_target_properties(test_suite PROPERTIES EXCLUDE_FROM_ALL TRUE)
+
+#
+## 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} ${SQLITE3_LIBRARIES})
+ target_link_libraries(test_suite ${SQLITE3_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,28 +292,40 @@ 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)
+set_target_properties(check PROPERTIES EXCLUDE_FROM_ALL TRUE)
add_custom_target(e2e COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/tests/end_to_end/"
DEPENDS biboumi)
+set_target_properties(e2e PROPERTIES EXCLUDE_FROM_ALL TRUE)
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()
+add_custom_target(everything DEPENDS test_suite biboumi)
#
## Install target
@@ -244,7 +333,8 @@ endif()
install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin)
install(FILES ${MAN_PAGE} DESTINATION share/man/man1 OPTIONAL COMPONENT documentation)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/biboumi.service DESTINATION lib/systemd/system COMPONENT init)
-install(FILES conf/biboumi.cfg DESTINATION /etc/biboumi COMPONENT configuration)
+file(GLOB policy_files conf/*policy.txt)
+install(FILES ${policy_files} DESTINATION /etc/biboumi COMPONENT configuration)
#
## Dist target
@@ -272,21 +362,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 +382,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(src/biboumi.h.cmake src/biboumi.h)
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index ed3915f..8df4899 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -9,12 +9,17 @@ but that’s mainly for the convenience of users.
Before doing anything, you can come on the `XMPP chatroom`_ to discuss your
changes, issues or ideas.
+
Bug reports, feature requests
-----------------------------
To open a bug report, or a feature request, please do so on `our gitlab’s
bug tracker`_.
+If the bug you’re reporting is about a bad behaviour of biboumi when some XMPP
+or IRC events occur, please try to reproduce the issue with a biboumi running
+in log_level=0, and include the relevant logs in your bug report.
+
If the issue you’re reporting may have security implications, please select
the “confidential” flag in your bug report.
@@ -34,6 +39,42 @@ It is also recommended to add some unit or end-to-end tests for the proposed
changes.
+Tests
+-----
+
+There are two test suites for biboumi:
+
+- unit tests that can be run simply using `make check`.
+ These tests use the Catch test framework, are written in pure C++
+ and they should always succeed, in all possible build configuration.
+
+- a more complex end-to-end test suite. This test suite is written in python3,
+ uses a specific IRC server (`charybdis`_), and only tests the most complete
+ biboumi configuration (when all dependencies are used). To run it, you need
+ to install various dependencies: refer to fedora’s `Dockerfile.base`_ and
+ `Dockerfile`_ to see how to install charybdis, slixmpp, botan, a ssl
+ certificate, etc.
+
+ Once all the dependencies are correctly installed, the tests are run with
+
+ `make e2e`
+
+ To run one or more specific tests, you can do something like this:
+
+ `make biboumi && python3 ../tests/end_to_end self_ping basic_handshake_success`
+
+ This will run two tests, self_ping and basic_handshake_success.
+
+ To write additional tests, you need to add a Scenario
+ into `the __main__.py file`_. If you have problem running this end-to-end
+ test suite, or if you struggle with this weird code (that would be
+ completely normal…), don’t hesitate to ask for help.
+
+
+All these tests automatically run with various configurations, on various
+platforms, using gitlab CI.
+
+
Coding style
------------
Please try to follow the existing style:
@@ -50,3 +91,7 @@ Please try to follow the existing style:
.. _gitlab merge request: https://lab.louiz.org/louiz/biboumi/merge_requests/new
.. _github pull request: https://github.com/louiz/biboumi/pulls
.. _XMPP chatroom: xmpp:biboumi@muc.poez.io
+.. _Dockerfile.base: docker/biboumi-test/fedora/Dockerfile.base
+.. _Dockerfile: docker/biboumi-test/fedora/Dockerfile
+.. _charybdis: https://github.com/charybdis-ircd/charybdis
+.. _the __main__.py file: tests/end_to_end/__main__.py
diff --git a/INSTALL.rst b/INSTALL.rst
index 1526d7e..5bb0ca8 100644
--- a/INSTALL.rst
+++ b/INSTALL.rst
@@ -32,24 +32,27 @@ libiconv_
libuuid_
Generate unique IDs
+sqlite3_ (option, but highly recommended)
+ Provides a way to store various options in a (sqlite3) database. Each user
+ of the gateway can store their own values (for example their prefered port,
+ or their IRC password). Without this dependency, many interesting features
+ are missing.
+
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.
-litesql_ (optional)
- Provides a way to store various options in a (sqlite3) database. Each user
- of the gateway can store their own values (for example their prefered port,
- or their IRC password).
+gcrypt_ (mandatory only if botan is absent)
+ Provides the SHA-1 hash function, for the case where Botan is absent.
systemd_ (optional)
Provides the support for a systemd service of Type=notify. This is useful only
@@ -116,7 +119,9 @@ 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
@@ -155,7 +160,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/
-.. _litesql: http://git.louiz.org/litesql
+.. _udns: http://www.corpit.ru/mjt/udns.html
+.. _sqlite3: https://sqlite.org
.. _systemd: https://www.freedesktop.org/wiki/Software/systemd/
.. _biboumi.1.rst: doc/biboumi.1.rst
+.. _gcrypt: https://www.gnu.org/software/libgcrypt/
diff --git a/README.rst b/README.rst
index bb3adfc..5ee9846 100644
--- a/README.rst
+++ b/README.rst
@@ -2,18 +2,15 @@ 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://codecov.proxy.louiz.org/gh/louiz/biboumi/branch/master/graph/badge.svg
+ :target: https://codecov.io/gh/louiz/biboumi
-.. image:: https://sonarqube.com/api/badges/gate?key=biboumi
- :target: https://sonarqube.com/component_issues/index?id=biboumi
-
-.. image:: https://scan.coverity.com/projects/3726/badge.svg
+.. image:: https://coverity.proxy.louiz.org/projects/3726/badge.svg
:target: https://scan.coverity.com/projects/louiz-biboumi
-.. image:: https://bestpractices.coreinfrastructure.org/projects/450/badge
+.. image:: https://coreinfrastructure.proxy.louiz.org/projects/450/badge
:target: https://bestpractices.coreinfrastructure.org/projects/450
Biboumi is an XMPP gateway that connects to IRC servers and translates
diff --git a/biboumi.h.cmake b/biboumi.h.cmake
deleted file mode 100644
index beb67d0..0000000
--- a/biboumi.h.cmake
+++ /dev/null
@@ -1 +0,0 @@
-#cmakedefine USE_DATABASE
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..9eb76c4 100644
--- a/louloulibs/cmake/Modules/FindBOTAN.cmake
+++ b/cmake/Modules/FindBOTAN.cmake
@@ -17,8 +17,10 @@
include(FindPkgConfig)
-pkg_check_modules(BOTAN botan-2)
-pkg_check_modules(BOTAN botan-1.11)
+if(NOT BOTAN_FOUND)
+ pkg_check_modules(BOTAN botan-2)
+ pkg_check_modules(BOTAN botan-1.11)
+endif()
if(NOT BOTAN_FOUND)
find_path(BOTAN_INCLUDE_DIRS NAMES botan/botan.h
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..c769d06 100644
--- a/louloulibs/cmake/Modules/FindLIBIDN.cmake
+++ b/cmake/Modules/FindLIBIDN.cmake
@@ -16,7 +16,9 @@
# This file is in the public domain
include(FindPkgConfig)
-pkg_check_modules(LIBIDN libidn)
+if(NOT LIBIDN_FOUND)
+ pkg_check_modules(LIBIDN libidn)
+endif()
if(NOT LIBIDN_FOUND)
find_path(LIBIDN_INCLUDE_DIRS NAMES stringprep.h
@@ -33,9 +35,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..67352ab 100644
--- a/louloulibs/cmake/Modules/FindLIBUUID.cmake
+++ b/cmake/Modules/FindLIBUUID.cmake
@@ -16,7 +16,9 @@
# This file is in the public domain
include(FindPkgConfig)
-pkg_check_modules(LIBUUID uuid)
+if(NOT LIBUUID_FOUND)
+ pkg_check_modules(LIBUUID uuid)
+endif()
if(NOT LIBUUID_FOUND)
find_path(LIBUUID_INCLUDE_DIRS NAMES uuid/uuid.h
@@ -33,8 +35,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
deleted file mode 100644
index 91155bb..0000000
--- a/cmake/Modules/FindLITESQL.cmake
+++ /dev/null
@@ -1,76 +0,0 @@
-# - Find LiteSQL
-#
-# Find the LiteSQL library, and defines a function to generate C++ files
-# from the database xml file using litesql-gen fro
-#
-# This module defines the following variables:
-# LITESQL_FOUND - True if library and include directory are found
-# If set to TRUE, the following are also defined:
-# LITESQL_INCLUDE_DIRS - The directory where to find the header file
-# LITESQL_LIBRARIES - Where to find the library file
-# LITESQL_GENERATE_CPP - A function, to be used like this:
-# LITESQL_GENERATE_CPP("db/database.xml" # The file defining the db schemas
-# "database" # The name of the C++ “module”
-# # that will be generated
-# LITESQL_GENERATED_SOURCES # Variable containing the
-# resulting C++ files to compile
-#
-# For conveniance, these variables are also set. They have the same values
-# than the variables above. The user can thus choose his/her prefered way
-# to write them.
-# LITESQL_INCLUDE_DIR
-# LITESQL_LIBRARY
-#
-# This file is in the public domain
-
-find_path(LITESQL_INCLUDE_DIRS NAMES litesql.hpp
- DOC "The LiteSQL include directory")
-
-find_library(LITESQL_LIBRARIES NAMES litesql
- DOC "The LiteSQL library")
-
-foreach(DB_TYPE sqlite postgresql mysql ocilib)
- string(TOUPPER ${DB_TYPE} DB_TYPE_UPPER)
- find_library(LITESQL_${DB_TYPE_UPPER}_LIB_PATH NAMES litesql_${DB_TYPE}
- DOC "The ${DB_TYPE} backend for LiteSQL")
- if(LITESQL_${DB_TYPE_UPPER}_LIB_PATH)
- list(APPEND LITESQL_LIBRARIES ${LITESQL_${DB_TYPE_UPPER}_LIB_PATH})
- endif()
- mark_as_advanced(LITESQL_${DB_TYPE_UPPER}_LIB_PATH)
-endforeach()
-
-find_program(LITESQLGEN_EXECUTABLE NAMES litesql-gen
- DOC "The utility that creates .h and .cpp files from a xml database description")
-
-# Use some standard module to handle the QUIETLY and REQUIRED arguments, and
-# set LITESQL_FOUND to TRUE if these two variables are set.
-include(FindPackageHandleStandardArgs)
-find_package_handle_standard_args(LITESQL REQUIRED_VARS LITESQL_LIBRARIES LITESQL_INCLUDE_DIRS
- LITESQLGEN_EXECUTABLE)
-
-# Compatibility for all the ways of writing these variables
-if(LITESQL_FOUND)
- set(LITESQL_INCLUDE_DIR ${LITESQL_INCLUDE_DIRS})
- set(LITESQL_LIBRARY ${LITESQL_LIBRARIES})
-endif()
-
-mark_as_advanced(LITESQL_INCLUDE_DIRS LITESQL_LIBRARIES LITESQLGEN_EXECUTABLE)
-
-
-# LITESQL_GENERATE_CPP function
-
-function(LITESQL_GENERATE_CPP
- SOURCE_FILE OUTPUT_NAME OUTPUT_SOURCES)
- set(${OUTPUT_SOURCES})
- add_custom_command(
- OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_NAME}.cpp"
- "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_NAME}.hpp"
- COMMAND ${LITESQLGEN_EXECUTABLE}
- ARGS -t c++ --output-dir=${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${SOURCE_FILE}
- DEPENDS ${SOURCE_FILE}
- COMMENT "Running litesql-gen on ${SOURCE_FILE}"
- VERBATIM)
- list(APPEND ${OUTPUT_SOURCES} "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_NAME}.cpp")
- set_source_files_properties(${${OUTPUT_SOURCES}} PROPERTIES GENERATED TRUE)
- set(${OUTPUT_SOURCES} ${${OUTPUT_SOURCES}} PARENT_SCOPE)
-endfunction()
diff --git a/cmake/Modules/FindSQLITE3.cmake b/cmake/Modules/FindSQLITE3.cmake
new file mode 100644
index 0000000..2861b37
--- /dev/null
+++ b/cmake/Modules/FindSQLITE3.cmake
@@ -0,0 +1,43 @@
+# - Find sqlite3
+# Find the sqlite3 cryptographic library
+#
+# This module defines the following variables:
+# SQLITE3_FOUND - True if library and include directory are found
+# If set to TRUE, the following are also defined:
+# SQLITE3_INCLUDE_DIRS - The directory where to find the header file
+# SQLITE3_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.
+# SQLITE3_LIBRARY
+# SQLITE3_INCLUDE_DIR
+#
+# This file is in the public domain
+
+include(FindPkgConfig)
+
+if(NOT SQLITE3_FOUND)
+ pkg_check_modules(SQLITE3 sqlite3)
+endif()
+
+if(NOT SQLITE3_FOUND)
+ find_path(SQLITE3_INCLUDE_DIRS NAMES sqlite3.h
+ DOC "The sqlite3 include directory")
+
+ find_library(SQLITE3_LIBRARIES NAMES sqlite3
+ DOC "The sqlite3 library")
+
+ # Use some standard module to handle the QUIETLY and REQUIRED arguments, and
+ # set SQLITE3_FOUND to TRUE if these two variables are set.
+ include(FindPackageHandleStandardArgs)
+ find_package_handle_standard_args(SQLITE3 REQUIRED_VARS SQLITE3_LIBRARIES SQLITE3_INCLUDE_DIRS)
+
+ if(SQLITE3_FOUND)
+ set(SQLITE3_LIBRARY ${SQLITE3_LIBRARIES} CACHE INTERNAL "")
+ set(SQLITE3_INCLUDE_DIR ${SQLITE3_INCLUDE_DIRS} CACHE INTERNAL "")
+ set(SQLITE3_FOUND ${SQLITE3_FOUND} CACHE INTERNAL "")
+ endif()
+endif()
+
+mark_as_advanced(SQLITE3_INCLUDE_DIRS SQLITE3_LIBRARIES) \ No newline at end of file
diff --git a/louloulibs/cmake/Modules/FindSYSTEMD.cmake b/cmake/Modules/FindSYSTEMD.cmake
index c7decde..2d3f063 100644
--- a/louloulibs/cmake/Modules/FindSYSTEMD.cmake
+++ b/cmake/Modules/FindSYSTEMD.cmake
@@ -16,7 +16,9 @@
# This file is in the public domain
include(FindPkgConfig)
-pkg_check_modules(SYSTEMD libsystemd)
+if(NOT SYSTEMD_FOUND)
+ pkg_check_modules(SYSTEMD libsystemd)
+endif()
if(NOT SYSTEMD_FOUND)
find_path(SYSTEMD_INCLUDE_DIRS NAMES systemd/sd-daemon.h
@@ -31,9 +33,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/conf/irc.mozilla.org.policy.txt b/conf/irc.mozilla.org.policy.txt
new file mode 100644
index 0000000..f099e61
--- /dev/null
+++ b/conf/irc.mozilla.org.policy.txt
@@ -0,0 +1 @@
+minimum_dh_group_size = 1024
diff --git a/conf/irc.ppirc.net.policy.txt b/conf/irc.ppirc.net.policy.txt
new file mode 100644
index 0000000..557d129
--- /dev/null
+++ b/conf/irc.ppirc.net.policy.txt
@@ -0,0 +1,2 @@
+key_exchange_methods = RSA
+minimum_rsa_bits = 1024
diff --git a/conf/policy.txt b/conf/policy.txt
new file mode 100644
index 0000000..8edc09f
--- /dev/null
+++ b/conf/policy.txt
@@ -0,0 +1,2 @@
+require_cert_revocation_info = false
+use_ecc_point_compression = true
diff --git a/database/database.xml b/database/database.xml
deleted file mode 100644
index af15ad5..0000000
--- a/database/database.xml
+++ /dev/null
@@ -1,68 +0,0 @@
-<?xml version="1.0"?>
-<!DOCTYPE database SYSTEM "litesql.dtd">
-
-<database name="BibouDB" namespace="db">
- <object name="GlobalOptions">
- <field name="owner" type="string" length="3071"/>
-
- <field name="maxHistoryLength" type="integer" default="20"/>
- <field name="recordHistory" type="boolean" default="true"/>
- <index unique="true">
- <indexfield name="owner"/>
- </index>
- </object>
-
- <object name="IrcServerOptions">
- <field name="owner" type="string" length="3071"/>
- <field name="server" type="string" length="3071"/>
-
- <field name="pass" type="string" length="1024" default=""/>
- <field name="afterConnectionCommand" type="string" length="510" default=""/>
- <field name="tlsPorts" type="string" length="4096" default="6697;6670" />
- <field name="ports" type="string" length="4096" default="6667" />
- <field name="username" type="string" length="1024" default=""/>
- <field name="realname" type="string" length="1024" default=""/>
- <field name="verifyCert" type="boolean" default="true"/>
- <field name="trustedFingerprint" type="string"/>
-
- <field name="encodingOut" type="string" default="ISO-8859-1"/>
- <field name="encodingIn" type="string" default="ISO-8859-1"/>
-
- <field name="maxHistoryLength" type="integer" default="20"/>
- <index unique="true">
- <indexfield name="owner"/>
- <indexfield name="server"/>
- </index>
- </object>
-
- <object name="IrcChannelOptions">
- <field name="owner" type="string" length="3071"/>
- <field name="server" type="string" length="3071"/>
- <field name="channel" type="string" length="1024"/>
-
- <field name="encodingOut" type="string"/>
- <field name="encodingIn" type="string"/>
-
- <field name="maxHistoryLength" type="integer" default="20"/>
-
- <index unique="true">
- <indexfield name="owner"/>
- <indexfield name="server"/>
- <indexfield name="channel"/>
- </index>
- </object>
-
- <object name="MucLogLine">
- <field name="uuid" type="string" length="36" />
- <!-- The bare JID of the user for which we stored the line. It's
- the JID associated with the Bridge -->
- <field name="owner" type="string" length="4096" />
- <!-- The room IID -->
- <field name="ircChanName" type="string" length="4096" />
- <field name="ircServerName" type="string" length="4096" />
-
- <field name="date" type="datetime" />
- <field name="body" type="string" length="65536"/>
- <field name="nick" type="string" length="4096" />
- </object>
-</database>
diff --git a/doc/biboumi.1.rst b/doc/biboumi.1.rst
index 49c0fe4..91b2f6d 100644
--- a/doc/biboumi.1.rst
+++ b/doc/biboumi.1.rst
@@ -35,12 +35,19 @@ details on its content.
Configuration
=============
-The configuration file uses a simple format of the form
-``option=value``. Here is a description of each possible option:
+The configuration file uses a simple format of the form ``option=value``.
-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.
+The values from the configuration file can be overridden by environment
+variables, with the name all in upper case and prefixed with "BIBOUMI_".
+For example, if the environment contains “BIBOUMI_PASSWORD=blah", this will
+override the value of the “password” option in the configuration file.
+
+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.
+
+Here is a description of every possible option:
hostname
--------
@@ -74,22 +81,21 @@ admin
-----
The bare JID of the gateway administrator. This JID will have more
-privileges than other standard users (the admin thus needs to check their
-privileges), for example some administration ad-hoc commands will only be
-available to that JID.
+privileges than other standard users, for example some administration
+ad-hoc commands will only be available to that JID.
fixed_irc_server
----------------
If this option contains the hostname of an IRC server (for example
irc.example.org), then biboumi will enforce the connexion to that IRC
-server only. This means that a JID like "#chan@biboumi.example.com" must
-be used instead of "#chan%irc.example.org@biboumi.example.com". In that
+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 +158,60 @@ 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. To be able to listen on this privileged port, biboumi
+needs to have certain capabilities: on linux, using systemd, this can be
+achieved by adding `AmbientCapabilities=CAP_NET_BIND_SERVICE` to the unit
+file. On other systems, other solutions exist, like the portacl module on
+FreeBSD.
+
+If biboumi’s identd server is properly started, it will receive queries from
+the IRC servers asking for the “identity” of each IRC connection made to it.
+Biboumi will answer with a hash of the JID that made the connection. This is
+useful for the IRC server to be able to distinguish the different users, and
+be able to deal with the absuses without having to simply ban the IP. Without
+this identd server, moderation is a lot harder, because all the different
+users of a single biboumi instance all share the same IP, and they can’t be
+distinguished by the IRC servers.
+
+policy_directory
+----------------
+
+A directory that should contain the policy files, used to customize
+Botan’s behaviour when negociating the TLS connections with the IRC
+servers. If not specified, the directory is the one where biboumi’s
+configuration file is located: for example if biboumi reads its
+configuration from /etc/biboumi/biboumi.cfg, the policy_directory value
+will be /etc/biboumi.
+
+
+TLS configuration
+=================
+
+Various settings of the TLS connections can be customized using policy
+files. The files should be located in the directory specified by the
+configuration option `policy_directory`_. When attempting to connect to
+an IRC server using TLS, biboumi will use Botan’s default TLS policy, and
+then will try to load some policy files to override the values found in
+these files. For example, if policy_directory is /etc/biboumi, when
+trying to connect to irc.example.com, biboumi will try to read
+/etc/biboumi/policy.txt, use the values found to override the default
+values, then it will try to read /etc/biboumi/irc.example.com.policy.txt
+and re-override the policy with the values found in this file.
+
+The policy.txt file applies to all the connections, and
+irc.example.policy.txt will only apply (in addition to policy.txt) when
+connecting to that specific server.
+
+To see the list of possible options to configure, refer to `Botan’s TLS
+documentation <https://botan.randombit.net/manual/tls.html#tls-policies>`_.
+
+By default, biboumi provides a few policy files, to work around some
+issues found with a few well-known IRC servers.
+
Usage
=====
@@ -194,8 +254,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:
@@ -280,7 +340,7 @@ biboumi provides a virtual channel on the jid
channel ``#foo`` on the server ``irc.example.com``, but you need to authenticate
to a bot of the server before you’re allowed to join it, you can first join
the room ``%irc.example.com@biboumi.example.com`` (this will effectively
-connect you to the IRC server without joining any room), then send your
+connect you to the IRC server without joining any channel), then send your
authentication message to the user ``bot%irc.example.com@biboumi.example.com``
and finally join the room ``#foo%irc.example.com@biboumi.example.com``.
@@ -291,14 +351,14 @@ 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
-------
Public channel messages are saved into archives, inside the database, unless
-the `record_history` option is set to false for that user `Ad-hoc commands`.
+the `record_history` option is set to false by that user (see `Ad-hoc commands`).
Private messages (messages that are sent directly to a nickname, not a
channel) are never stored in the database. When a channel is joined, biboumi
sends the `max_history_length` messages found in the database as the MUC
@@ -322,8 +382,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
---------
@@ -362,6 +422,13 @@ Notices
Notices are received exactly like private messages. It is not possible to
send a notice.
+Topic
+-----
+
+The topic can be set and retrieved seemlessly. The unique difference is that
+if an XMPP user tries to set a multiline topic, every line return (\\n) will
+be replaced by a space, because the IRC server wouldn’t accept it.
+
Invitations
-----------
@@ -485,11 +552,63 @@ 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.
+ * Persistent: Overrides the value specified in each individual channel,
+ all channels are persistent, whether or not their specific value is
+ true or false. See below for more details on what a persistent
+ channel is.
+
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 +623,23 @@ 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.
+ * Record History: whether or not history messages should be saved in
+ the database, for this specific channel. If the value is “unset” (the
+ default), then the value configured globally is used. This option is there,
+ for example, to be able to enable history recording globally while disabling
+ it for a few specific “private” channels.
Raw IRC messages
----------------
@@ -515,11 +650,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
========
@@ -550,3 +685,4 @@ protection against flood or any sort of abuse that your users may cause on
the IRC servers. Some XMPP server however offer the possibility to restrict
what JID can access a gateway. Use that feature if you wish to grant access
to your biboumi instance only to a list of trusted users.
+
diff --git a/docker/biboumi-test/alpine/Dockerfile b/docker/biboumi-test/alpine/Dockerfile
new file mode 100644
index 0000000..f97c58c
--- /dev/null
+++ b/docker/biboumi-test/alpine/Dockerfile
@@ -0,0 +1,53 @@
+# This Dockerfile creates a docker image suitable to run biboumi’s build and
+# tests. For example, it can be used on with gitlab-ci.
+
+FROM docker.io/alpine:latest
+
+ENV LC_ALL C.UTF-8
+
+# Needed to build biboumi
+RUN apk add --no-cache g++\
+ clang\
+ valgrind\
+ udns-dev\
+ sqlite-dev\
+ libuuid\
+ util-linux-dev\
+ libgcrypt-dev\
+ cmake\
+ make\
+ expat-dev\
+ libidn-dev\
+ git\
+ py3-lxml\
+ libtool\
+ py3-pip\
+ python2\
+ python3-dev\
+ automake\
+ autoconf\
+ flex\
+ bison\
+ libltdl\
+ openssl\
+ libressl-dev\
+ zlib-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 adduser tester -D -h /home/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 && 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
+
+WORKDIR /home/tester
+USER tester
diff --git a/docker/biboumi-test/archlinux/Dockerfile b/docker/biboumi-test/archlinux/Dockerfile
new file mode 100644
index 0000000..20f0343
--- /dev/null
+++ b/docker/biboumi-test/archlinux/Dockerfile
@@ -0,0 +1,13 @@
+FROM docker.io/base/archlinux:latest
+
+RUN pacman -Syuuuu --noconfirm
+
+RUN pacman -Syu --noconfirm cmake base-devel git clang-tools-extra
+
+RUN useradd -m -G wheel -s /bin/bash builder
+
+RUN sed -i '/^# %wheel ALL=(ALL) NOPASSWD: ALL/s/^# //' /etc/sudoers
+
+WORKDIR /home/builder
+
+USER builder
diff --git a/docker/biboumi-test/debian/Dockerfile b/docker/biboumi-test/debian/Dockerfile
index 9aac3ec..232a585 100644
--- a/docker/biboumi-test/debian/Dockerfile
+++ b/docker/biboumi-test/debian/Dockerfile
@@ -3,72 +3,58 @@
FROM docker.io/debian:latest
+ENV LC_ALL C.UTF-8
+
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
+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
-RUN cd botan && ./configure.py --prefix=/usr && make -j8 && make install
-RUN rm -rf /botan
-
-# Install litesql
-RUN git clone git://git.louiz.org/litesql
-RUN mkdir /litesql/build && cd /litesql/build && cmake .. -DCMAKE_INSTALL_PREFIX=/usr && make -j8
-RUN cd /litesql/build && make install
-RUN rm -rf /litesql
-
-RUN ldconfig
+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
-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 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 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 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 dpkg-reconfigure locales && \
- locale-gen C.UTF-8 && \
- /usr/sbin/update-locale LANG=C.UTF-8
+RUN chown -R tester:tester /home/tester/ircd
-ENV LC_ALL C.UTF-8
+RUN yes "" | openssl req -nodes -x509 -newkey rsa:4096 -keyout /home/tester/ircd/etc/ssl.key -out /home/tester/ircd/etc/ssl.pem
WORKDIR /home/tester
USER tester
diff --git a/docker/biboumi-test/fedora/Dockerfile b/docker/biboumi-test/fedora/Dockerfile
index ebcb4e4..384fd51 100644
--- a/docker/biboumi-test/fedora/Dockerfile
+++ b/docker/biboumi-test/fedora/Dockerfile
@@ -3,67 +3,60 @@
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
+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
-RUN cd botan && ./configure.py --prefix=/usr && make -j8 && make install
-RUN rm -rf /botan
-
-# Install litesql
-RUN git clone git://git.louiz.org/litesql
-RUN mkdir /litesql/build && cd /litesql/build && cmake .. -DCMAKE_INSTALL_PREFIX=/usr && make -j8
-RUN cd /litesql/build && make install
-RUN rm -rf /litesql
-
-RUN ldconfig
+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
-RUN pip3 install pyasn1
-RUN dnf install -y python3-devel
-RUN cd slixmpp && python3 setup.py build && python3 setup.py install
+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 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 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 rm -rf /charybdis
-RUN su - tester -c "echo export LANG=en_GB.utf-8 >> /home/tester/.bashrc"
+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
-COPY sonar-scanner-2.8 /home/tester/sonar-scanner
-
-RUN dnf install -y which java-1.8.0-openjdk
WORKDIR /home/tester
USER tester
-
diff --git a/docker/biboumi/Dockerfile b/docker/biboumi/Dockerfile
new file mode 100644
index 0000000..67f0f41
--- /dev/null
+++ b/docker/biboumi/Dockerfile
@@ -0,0 +1,41 @@
+# This Dockerfile creates a docker image running biboumi
+
+FROM docker.io/alpine:latest
+
+RUN apk add --no-cache\
+ g++\
+ cmake\
+ make\
+ udns-dev\
+ sqlite-dev\
+ libuuid\
+ util-linux-dev\
+ expat-dev\
+ libidn-dev\
+ git\
+ python2
+
+# 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 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_SQLITE3=1\
+ -DWITH_LIBIDN=1\
+ && make -j8 && make install && rm -rf /biboumi
+
+RUN adduser biboumi -D -h /home/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
+
+WORKDIR /home/biboumi
+USER biboumi
+
+CMD ["/usr/bin/biboumi", "/etc/biboumi/biboumi.cfg"]
diff --git a/docker/biboumi/README.md b/docker/biboumi/README.md
new file mode 100644
index 0000000..4b9e1e5
--- /dev/null
+++ b/docker/biboumi/README.md
@@ -0,0 +1,68 @@
+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 contains only a few default values. To be able to run, biboumi needs additional configuration. Additional options can be passed using environment variables. Any configuration option can be customized this way (see biboumi’s doc), but the main are listed here for convenience:
+
+* BIBOUMI_HOSTNAME: Sets the value of the *hostname* option.
+* BIBOUMI_PASSWORD: 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 value is **xmpp**.
+
+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
+```
+
+If both a custom configuration file and custom environment variables are passed to the container, the environment variables will take precedence.
+
+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**.
+
+Note: Due to a limitation in Docker, to be able to read and write into this database, make sure this mounted directory is owned by UID and GID 1001:1001, on the host.
+
+```
+chown -R 1001:1001 database/
+```
diff --git a/docker/biboumi/biboumi.cfg b/docker/biboumi/biboumi.cfg
new file mode 100644
index 0000000..98c5a9f
--- /dev/null
+++ b/docker/biboumi/biboumi.cfg
@@ -0,0 +1,6 @@
+xmpp_server_ip=127.0.0.1
+port=5347
+db_name=/var/lib/biboumi/biboumi.sqlite
+hostname=xmpp
+password=
+admin=
diff --git a/docker/packaging/debian/Dockerfile b/docker/packaging/debian/Dockerfile
new file mode 100644
index 0000000..f9f4d84
--- /dev/null
+++ b/docker/packaging/debian/Dockerfile
@@ -0,0 +1,10 @@
+# This Dockerfile creates a docker image suitable to build a debian package
+
+FROM docker.io/debian:sid
+
+RUN apt update
+
+# Needed to build biboumi
+RUN apt install -y \
+ git \
+ devscripts
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/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 e4a1d3b..d28c840 100644
--- a/packaging/biboumi.spec.cmake
+++ b/packaging/biboumi.spec.cmake
@@ -11,6 +11,7 @@ BuildRequires: libidn-devel
BuildRequires: expat-devel
BuildRequires: libuuid-devel
BuildRequires: systemd-devel
+BuildRequires: sqlite-devel
BuildRequires: cmake
BuildRequires: systemd
BuildRequires: pandoc
@@ -37,7 +38,8 @@ cmake . -DCMAKE_CXX_FLAGS="%{optflags}" \
-DPOLLER=EPOLL \
-DWITHOUT_BOTAN=1 \
-DWITH_SYSTEMD=1 \
- -DWITH_LIBIDN=1
+ -DWITH_LIBIDN=1 \
+ -DWITH_SQLITE3=1
make %{?_smp_mflags}
@@ -55,10 +57,19 @@ make check %{?_smp_mflags}
%{_mandir}/man1/%{name}.1*
%doc README.rst COPYING doc/biboumi.1.rst
%{_unitdir}/%{name}.service
-%config(noreplace) %{biboumi_confdir}/biboumi.cfg
+%config(noreplace) %{biboumi_confdir}/*policy.txt
%changelog
+* ${RPM_DATE} Le Coz Florent <louiz@louiz.org> - ${RPM_VERSION}-1
+- Build latest git revision
+
+* Wed Jun 14 2017 Le Coz Florent <louiz@louiz.org> - 6.0-1
+ Enable database support by building with sqlite3
+
+* Wed May 24 2017 Le Coz Florent <louiz@louiz.org> - 5.0-1
+- Update to version 5.0
+
* Wed May 2 2017 Le Coz Florent <louiz@louiz.org> - 4.3-1
- Fix a segmentation fault that occured when trying to connect
to an IRC server without any port configured.
diff --git a/louloulibs/louloulibs.h.cmake b/src/biboumi.h.cmake
index 6131b70..1ad9a40 100644
--- a/louloulibs/louloulibs.h.cmake
+++ b/src/biboumi.h.cmake
@@ -1,10 +1,11 @@
-#define SYSTEM_NAME "${CMAKE_SYSTEM}"
+#cmakedefine USE_DATABASE
#cmakedefine ICONV_SECOND_ARGUMENT_IS_CONST
#cmakedefine LIBIDN_FOUND
#cmakedefine SYSTEMD_FOUND
#cmakedefine POLLER ${POLLER}
#cmakedefine BOTAN_FOUND
-#cmakedefine CARES_FOUND
+#cmakedefine GCRYPT_FOUND
+#cmakedefine UDNS_FOUND
#cmakedefine SOFTWARE_VERSION "${SOFTWARE_VERSION}"
#cmakedefine PROJECT_NAME "${PROJECT_NAME}"
#cmakedefine HAS_GET_TIME
diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp
index a0ecc6e..81ca147 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>
@@ -22,20 +23,24 @@ static std::string in_encoding_for(const Bridge& bridge, const Iid& iid)
#ifdef USE_DATABASE
const auto jid = bridge.get_bare_jid();
auto options = Database::get_irc_channel_options_with_server_default(jid, iid.get_server(), iid.get_local());
- return options.encodingIn.value();
+ auto result = options.col<Database::EncodingIn>();
+ if (!result.empty())
+ return result;
#else
- return {"ISO-8859-1"};
+ (void)bridge;
+ (void)iid;
#endif
+ return {"ISO-8859-1"};
}
-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)
{
#ifdef USE_DATABASE
const auto options = Database::get_global_options(this->user_jid);
- this->set_record_history(options.recordHistory.value());
+ this->set_record_history(options.col<Database::RecordHistory>());
#endif
}
@@ -58,10 +63,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 +172,7 @@ 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();
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);
@@ -251,21 +256,24 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body)
else
irc->send_channel_message(iid.get_local(), line);
+ std::string uuid;
#ifdef USE_DATABASE
const auto xmpp_body = this->make_xmpp_body(line);
if (this->record_history)
- Database::store_muc_message(this->get_bare_jid(), iid, std::chrono::system_clock::now(),
+ uuid = Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(),
std::get<0>(xmpp_body), irc->get_own_nick());
#endif
for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
- this->xmpp.send_muc_message(std::to_string(iid), irc->get_own_nick(),
- this->make_xmpp_body(line), this->user_jid + "/" + resource);
+ this->xmpp.send_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(line),
+ this->user_jid + "/" + resource, uuid);
}
}
-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 +281,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 +337,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 +426,60 @@ 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());
+
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 goptions = Database::get_global_options(this->user_jid);
+ if (goptions.col<Database::Persistent>())
+ persistent = true;
+ else
+ {
+ const auto coptions = Database::get_irc_channel_options_with_server_default(this->user_jid, iid.get_server(), iid.get_local());
+ persistent = coptions.col<Database::Persistent>();
+ }
+#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 if (channel->joined)
+ {
+ this->send_muc_leave(iid, channel->get_self()->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 && channel->joined)
+ this->send_muc_leave(iid, channel->get_self()->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);
}
@@ -402,7 +487,7 @@ void Bridge::send_irc_nick_change(const Iid& iid, const std::string& new_nick)
void Bridge::send_irc_channel_list_request(const Iid& iid, const std::string& iq_id, const std::string& to_jid,
ResultSetInfo rs_info)
{
- auto& list = channel_list_cache[iid.get_server()];
+ auto& list = this->channel_list_cache[iid.get_server()];
// We fetch the list from the IRC server only if we have a complete
// cached list that needs to be invalidated (that is, when the request
@@ -425,7 +510,7 @@ void Bridge::send_irc_channel_list_request(const Iid& iid, const std::string& iq
if (irc_hostname != iid.get_server())
return false;
- auto& list = channel_list_cache[iid.get_server()];
+ auto& list = this->channel_list_cache[iid.get_server()];
if (message.command == "263" || message.command == "RPL_TRYAGAIN" || message.command == "ERR_TOOMANYMATCHES"
|| message.command == "ERR_NOSUCHSERVER")
@@ -483,7 +568,6 @@ void Bridge::send_irc_channel_list_request(const Iid& iid, const std::string& iq
{
auto& list = channel_list_cache[iid.get_server()];
const auto res = this->send_matching_channel_list(list, rs_info, iq_id, to_jid, std::to_string(iid));
- log_debug("We added a new channel in our list, can we send the result? ", std::boolalpha, res);
return res;
}
else if (message.command == "323" || message.command == "RPL_LISTEND")
@@ -503,7 +587,7 @@ bool Bridge::send_matching_channel_list(const ChannelList& channel_list, const R
const std::string& id, const std::string& to_jid, const std::string& from)
{
auto begin = channel_list.channels.begin();
- auto end = channel_list.channels.begin();
+ auto end = channel_list.channels.end();
if (channel_list.complete)
{
begin = std::find_if(channel_list.channels.begin(), channel_list.channels.end(), [this, &rs_info](const ListElement& element)
@@ -605,9 +689,12 @@ void Bridge::send_irc_kick(const Iid& iid, const std::string& target, const std:
this->add_waiting_irc(std::move(cb));
}
-void Bridge::set_channel_topic(const Iid& iid, const std::string& subject)
+void Bridge::set_channel_topic(const Iid& iid, std::string subject)
{
IrcClient* irc = this->get_irc_client(iid.get_server());
+ std::string::size_type pos{0};
+ while ((pos = subject.find('\n', pos)) != std::string::npos)
+ subject[pos] = ' ';
irc->send_topic_command(iid.get_local(), subject);
}
@@ -665,9 +752,10 @@ void Bridge::send_irc_participant_ping_request(const Iid& iid, const std::string
const std::string& iq_id, const std::string& to_jid,
const std::string& from_jid)
{
+ Jid from(to_jid);
IrcClient* irc = this->get_irc_client(iid.get_server());
IrcChannel* chan = irc->get_channel(iid.get_local());
- if (!chan->joined)
+ if (!chan->joined || !this->is_resource_in_chan(iid.to_tuple(), from.resource))
{
this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "not-allowed",
"", true);
@@ -756,13 +844,13 @@ void Bridge::send_message(const Iid& iid, const std::string& nick, const std::st
#ifdef USE_DATABASE
const auto xmpp_body = this->make_xmpp_body(body, encoding);
if (!nick.empty() && this->record_history)
- Database::store_muc_message(this->get_bare_jid(), iid, std::chrono::system_clock::now(),
+ Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(),
std::get<0>(xmpp_body), nick);
#endif
for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
{
- this->xmpp.send_muc_message(std::to_string(iid), nick,
- this->make_xmpp_body(body, encoding), this->user_jid + "/" + resource);
+ this->xmpp.send_muc_message(std::to_string(iid), nick, this->make_xmpp_body(body, encoding),
+ this->user_jid + "/" + resource, {});
}
}
@@ -793,18 +881,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, const 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)
+ if (self && irc && irc->number_of_joined_channels() == 0)
irc->send_quit_command("");
}
@@ -903,18 +997,22 @@ void Bridge::send_room_history(const std::string& hostname, const std::string& c
this->send_room_history(hostname, chan_name, resource);
}
-void Bridge::send_room_history(const std::string& hostname, const std::string& chan_name, const std::string& resource)
+void Bridge::send_room_history(const std::string& hostname, std::string chan_name, const std::string& resource)
{
#ifdef USE_DATABASE
const auto coptions = Database::get_irc_channel_options_with_server_and_global_default(this->user_jid, hostname, chan_name);
- const auto lines = Database::get_muc_logs(this->user_jid, chan_name, hostname, coptions.maxHistoryLength.value());
+ const auto lines = Database::get_muc_logs(this->user_jid, chan_name, hostname, coptions.col<Database::MaxHistoryLength>());
+ chan_name.append(utils::empty_if_fixed_server("%" + hostname));
for (const auto& line: lines)
{
- const auto seconds = line.date.value().timeStamp();
- this->xmpp.send_history_message(chan_name + utils::empty_if_fixed_server("%" + hostname), line.nick.value(),
- line.body.value(),
+ const auto seconds = line.col<Database::Date>();
+ this->xmpp.send_history_message(chan_name, line.col<Database::Nick>(), line.col<Database::Body>(),
this->user_jid + "/" + resource, seconds);
}
+#else
+ (void)hostname;
+ (void)chan_name;
+ (void)resource;
#endif
}
@@ -960,7 +1058,7 @@ void Bridge::send_iq_version_request(const std::string& nick, const std::string&
{
const auto resources = this->resources_in_server[hostname];
if (resources.begin() != resources.end())
- this->xmpp.send_iq_version_request(utils::tolower(nick) + "%" + utils::empty_if_fixed_server(hostname),
+ this->xmpp.send_iq_version_request(utils::tolower(nick) + utils::empty_if_fixed_server("%" + hostname),
this->user_jid + "/" + *resources.begin());
}
@@ -968,12 +1066,12 @@ 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];
if (resources.begin() != resources.end())
- this->xmpp.send_ping_request(utils::tolower(nick) + "%" + utils::empty_if_fixed_server(hostname),
+ this->xmpp.send_ping_request(utils::tolower(nick) + utils::empty_if_fixed_server("%" + hostname),
this->user_jid + "/" + *resources.begin(), utils::revstr(id));
}
@@ -1033,6 +1131,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 +1173,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 +1207,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();
diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp
index 18ebfeb..033291c 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,10 +80,10 @@ 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);
+ void set_channel_topic(const Iid& iid, std::string subject);
void send_xmpp_version_to_irc(const Iid& iid, const std::string& name, const std::string& version,
const std::string& os);
void send_irc_ping_result(const Iid& iid, const std::string& id);
@@ -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
*/
@@ -157,7 +157,7 @@ public:
* Send the MUC history to the user
*/
void send_room_history(const std::string& hostname, const std::string& chan_name);
- void send_room_history(const std::string& hostname, const std::string& chan_name, const std::string& resource);
+ void send_room_history(const std::string& hostname, std::string chan_name, const std::string& resource);
/**
* Send a MUC message from some participant
*/
@@ -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, const 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,6 +231,7 @@ 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);
@@ -302,10 +303,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.cpp b/src/bridge/colors.cpp
index 66f51ee..7662425 100644
--- a/src/bridge/colors.cpp
+++ b/src/bridge/colors.cpp
@@ -4,7 +4,7 @@
#include <algorithm>
#include <iostream>
-#include <string.h>
+#include <cstring>
using namespace std::string_literals;
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..0f3d639 100644
--- a/louloulibs/config/config.cpp
+++ b/src/config/config.cpp
@@ -1,11 +1,15 @@
#include <config/config.hpp>
-#include <logger/logger.hpp>
+#include <utils/tolower.hpp>
+#include <iostream>
#include <cstring>
-#include <sstream>
#include <cstdlib>
+using namespace std::string_literals;
+
+extern char** environ;
+
std::string Config::filename{};
std::map<std::string, std::string> Config::values{};
std::vector<t_config_changed_callback> Config::callbacks{};
@@ -38,7 +42,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,27 +70,46 @@ 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;
}
Config::clear();
+ auto parse_line = [](const std::string& line, const bool env)
+ {
+ static const auto env_option_prefix = "BIBOUMI_"s;
+
+ if (line == "" || line[0] == '#')
+ return;
+ size_t pos = line.find('=');
+ if (pos == std::string::npos)
+ return;
+ std::string option = line.substr(0, pos);
+ std::string value = line.substr(pos+1);
+ if (env)
+ {
+ auto a = option.substr(0, env_option_prefix.size());
+ if (a == env_option_prefix)
+ option = utils::tolower(option.substr(env_option_prefix.size()));
+ else
+ return;
+ }
+ Config::values[option] = value;
+ };
+
std::string line;
- size_t pos;
- std::string option;
- std::string value;
while (file.good())
{
std::getline(file, line);
- if (line == "" || line[0] == '#')
- continue ;
- pos = line.find('=');
- if (pos == std::string::npos)
- continue ;
- option = line.substr(0, pos);
- value = line.substr(pos+1);
- Config::values[option] = value;
+ parse_line(line, false);
+ }
+
+ char** env_line = environ;
+ while (*env_line)
+ {
+ parse_line(*env_line, true);
+ env_line++;
}
return true;
}
@@ -96,7 +119,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/column.hpp b/src/database/column.hpp
new file mode 100644
index 0000000..111f9ca
--- /dev/null
+++ b/src/database/column.hpp
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <cstddef>
+
+template <typename T>
+struct Column
+{
+ Column(T default_value):
+ value{default_value} {}
+ Column():
+ value{} {}
+ using real_type = T;
+ T value{};
+};
+
+struct Id: Column<std::size_t> { static constexpr auto name = "id_";
+ static constexpr auto options = "PRIMARY KEY AUTOINCREMENT"; };
diff --git a/src/database/count_query.hpp b/src/database/count_query.hpp
new file mode 100644
index 0000000..b7bbf51
--- /dev/null
+++ b/src/database/count_query.hpp
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <database/query.hpp>
+#include <database/table.hpp>
+
+#include <string>
+
+#include <sqlite3.h>
+
+struct CountQuery: public Query
+{
+ CountQuery(std::string name):
+ Query("SELECT count(*) FROM ")
+ {
+ this->body += std::move(name);
+ }
+
+ int64_t execute(sqlite3* db)
+ {
+ auto statement = this->prepare(db);
+ int64_t res = 0;
+ if (sqlite3_step(statement.get()) == SQLITE_ROW)
+ res = sqlite3_column_int64(statement.get(), 0);
+ else
+ {
+ log_error("Count request didn’t return a result");
+ return 0;
+ }
+ if (sqlite3_step(statement.get()) != SQLITE_DONE)
+ log_warning("Count request returned more than one result.");
+
+ log_debug("Returning count: ", res);
+ return res;
+ }
+};
diff --git a/src/database/database.cpp b/src/database/database.cpp
index f7d309b..92f7682 100644
--- a/src/database/database.cpp
+++ b/src/database/database.cpp
@@ -2,171 +2,185 @@
#ifdef USE_DATABASE
#include <database/database.hpp>
-#include <logger/logger.hpp>
-#include <irc/iid.hpp>
#include <uuid/uuid.h>
#include <utils/get_first_non_empty.hpp>
#include <utils/time.hpp>
-using namespace std::string_literals;
+#include <sqlite3.h>
-std::unique_ptr<db::BibouDB> Database::db;
+sqlite3* Database::db;
+Database::MucLogLineTable Database::muc_log_lines("MucLogLine_");
+Database::GlobalOptionsTable Database::global_options("GlobalOptions_");
+Database::IrcServerOptionsTable Database::irc_server_options("IrcServerOptions_");
+Database::IrcChannelOptionsTable Database::irc_channel_options("IrcChannelOptions_");
-void Database::open(const std::string& filename, const std::string& db_type)
+void Database::open(const std::string& filename)
{
- try
+ // Try to open the specified database.
+ // Close and replace the previous database pointer if it succeeded. If it did
+ // not, just leave things untouched
+ sqlite3* new_db;
+ auto res = sqlite3_open_v2(filename.data(), &new_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr);
+ if (res != SQLITE_OK)
{
- auto new_db = std::make_unique<db::BibouDB>(db_type,
- "database="s + filename);
- if (new_db->needsUpgrade())
- new_db->upgrade();
- Database::db.reset(new_db.release());
- } catch (const litesql::DatabaseError& e) {
- log_error("Failed to open database ", filename, ". ", e.what());
- throw;
+ log_error("Failed to open database file ", filename, ": ", sqlite3_errmsg(Database::db));
+ throw std::runtime_error("");
}
+ Database::close();
+ Database::db = new_db;
+ Database::muc_log_lines.create(Database::db);
+ Database::muc_log_lines.upgrade(Database::db);
+ Database::global_options.create(Database::db);
+ Database::global_options.upgrade(Database::db);
+ Database::irc_server_options.create(Database::db);
+ Database::irc_server_options.upgrade(Database::db);
+ Database::irc_channel_options.create(Database::db);
+ Database::irc_channel_options.upgrade(Database::db);
}
-void Database::set_verbose(const bool val)
-{
- Database::db->verbose = val;
-}
-db::GlobalOptions Database::get_global_options(const std::string& owner)
+Database::GlobalOptions Database::get_global_options(const std::string& owner)
{
- try {
- auto options = litesql::select<db::GlobalOptions>(*Database::db,
- db::GlobalOptions::Owner == owner).one();
- return options;
- } catch (const litesql::NotFound& e) {
- db::GlobalOptions options(*Database::db);
- options.owner = owner;
- return options;
- }
+ auto request = Database::global_options.select();
+ request.where() << Owner{} << "=" << owner;
+
+ Database::GlobalOptions options{Database::global_options.get_name()};
+ auto result = request.execute(Database::db);
+ if (result.size() == 1)
+ options = result.front();
+ else
+ options.col<Owner>() = owner;
+ return options;
}
-db::IrcServerOptions Database::get_irc_server_options(const std::string& owner,
- const std::string& server)
+Database::IrcServerOptions Database::get_irc_server_options(const std::string& owner, const std::string& server)
{
- try {
- auto options = litesql::select<db::IrcServerOptions>(*Database::db,
- db::IrcServerOptions::Owner == owner &&
- db::IrcServerOptions::Server == server).one();
- return options;
- } catch (const litesql::NotFound& e) {
- db::IrcServerOptions options(*Database::db);
- options.owner = owner;
- options.server = server;
- // options.update();
- return options;
- }
+ auto request = Database::irc_server_options.select();
+ request.where() << Owner{} << "=" << owner << " and " << Server{} << "=" << server;
+
+ Database::IrcServerOptions options{Database::irc_server_options.get_name()};
+ auto result = request.execute(Database::db);
+ if (result.size() == 1)
+ options = result.front();
+ else
+ {
+ options.col<Owner>() = owner;
+ options.col<Server>() = server;
+ }
+ return options;
}
-db::IrcChannelOptions Database::get_irc_channel_options(const std::string& owner,
- const std::string& server,
- const std::string& channel)
+Database::IrcChannelOptions Database::get_irc_channel_options(const std::string& owner, const std::string& server, const std::string& channel)
{
- try {
- auto options = litesql::select<db::IrcChannelOptions>(*Database::db,
- db::IrcChannelOptions::Owner == owner &&
- db::IrcChannelOptions::Server == server &&
- db::IrcChannelOptions::Channel == channel).one();
- return options;
- } catch (const litesql::NotFound& e) {
- db::IrcChannelOptions options(*Database::db);
- options.owner = owner;
- options.server = server;
- options.channel = channel;
- return options;
- }
+ auto request = Database::irc_channel_options.select();
+ request.where() << Owner{} << "=" << owner <<\
+ " and " << Server{} << "=" << server <<\
+ " and " << Channel{} << "=" << channel;
+ Database::IrcChannelOptions options{Database::irc_channel_options.get_name()};
+ auto result = request.execute(Database::db);
+ if (result.size() == 1)
+ options = result.front();
+ else
+ {
+ options.col<Owner>() = owner;
+ options.col<Server>() = server;
+ options.col<Channel>() = channel;
+ }
+ return options;
}
-db::IrcChannelOptions Database::get_irc_channel_options_with_server_default(const std::string& owner,
- const std::string& server,
- const std::string& channel)
+Database::IrcChannelOptions Database::get_irc_channel_options_with_server_default(const std::string& owner, const std::string& server,
+ const std::string& channel)
{
auto coptions = Database::get_irc_channel_options(owner, server, channel);
auto soptions = Database::get_irc_server_options(owner, server);
- coptions.encodingIn = get_first_non_empty(coptions.encodingIn.value(),
- soptions.encodingIn.value());
- coptions.encodingOut = get_first_non_empty(coptions.encodingOut.value(),
- soptions.encodingOut.value());
+ coptions.col<EncodingIn>() = get_first_non_empty(coptions.col<EncodingIn>(),
+ soptions.col<EncodingIn>());
+ coptions.col<EncodingOut>() = get_first_non_empty(coptions.col<EncodingOut>(),
+ soptions.col<EncodingOut>());
- coptions.maxHistoryLength = get_first_non_empty(coptions.maxHistoryLength.value(),
- soptions.maxHistoryLength.value());
+ coptions.col<MaxHistoryLength>() = get_first_non_empty(coptions.col<MaxHistoryLength>(),
+ soptions.col<MaxHistoryLength>());
return coptions;
}
-db::IrcChannelOptions Database::get_irc_channel_options_with_server_and_global_default(const std::string& owner,
- const std::string& server,
- const std::string& channel)
+Database::IrcChannelOptions Database::get_irc_channel_options_with_server_and_global_default(const std::string& owner, const std::string& server, const std::string& channel)
{
auto coptions = Database::get_irc_channel_options(owner, server, channel);
auto soptions = Database::get_irc_server_options(owner, server);
auto goptions = Database::get_global_options(owner);
- coptions.encodingIn = get_first_non_empty(coptions.encodingIn.value(),
- soptions.encodingIn.value());
- coptions.encodingOut = get_first_non_empty(coptions.encodingOut.value(),
- soptions.encodingOut.value());
+ coptions.col<EncodingIn>() = get_first_non_empty(coptions.col<EncodingIn>(),
+ soptions.col<EncodingIn>());
- coptions.maxHistoryLength = get_first_non_empty(coptions.maxHistoryLength.value(),
- soptions.maxHistoryLength.value(),
- goptions.maxHistoryLength.value());
+ coptions.col<EncodingOut>() = get_first_non_empty(coptions.col<EncodingOut>(),
+ soptions.col<EncodingOut>());
+
+ coptions.col<MaxHistoryLength>() = get_first_non_empty(coptions.col<MaxHistoryLength>(),
+ soptions.col<MaxHistoryLength>(),
+ goptions.col<MaxHistoryLength>());
return coptions;
}
-void Database::store_muc_message(const std::string& owner, const Iid& iid,
- Database::time_point date,
- const std::string& body,
- const std::string& nick)
+std::string Database::store_muc_message(const std::string& owner, const std::string& chan_name,
+ const std::string& server_name, Database::time_point date,
+ const std::string& body, const std::string& nick)
{
- db::MucLogLine line(*Database::db);
+ auto line = Database::muc_log_lines.row();
+
+ auto uuid = Database::gen_uuid();
- line.uuid = Database::gen_uuid();
- 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.body = body;
- line.nick = nick;
+ line.col<Uuid>() = uuid;
+ line.col<Owner>() = owner;
+ line.col<IrcChanName>() = chan_name;
+ line.col<IrcServerName>() = server_name;
+ line.col<Date>() = std::chrono::duration_cast<std::chrono::seconds>(date.time_since_epoch()).count();
+ line.col<Body>() = body;
+ line.col<Nick>() = nick;
- line.update();
+ line.save(Database::db);
+
+ return uuid;
}
-std::vector<db::MucLogLine> Database::get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server,
+std::vector<Database::MucLogLine> Database::get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server,
int limit, const std::string& start, const std::string& end)
{
- auto request = litesql::select<db::MucLogLine>(*Database::db,
- db::MucLogLine::Owner == owner &&
- db::MucLogLine::IrcChanName == chan_name &&
- db::MucLogLine::IrcServerName == server);
- request.orderBy(db::MucLogLine::Id, false);
+ auto request = Database::muc_log_lines.select();
+ request.where() << Database::Owner{} << "=" << owner << \
+ " and " << Database::IrcChanName{} << "=" << chan_name << \
+ " and " << Database::IrcServerName{} << "=" << server;
- if (limit >= 0)
- request.limit(limit);
if (!start.empty())
{
const auto start_time = utils::parse_datetime(start);
if (start_time != -1)
- request.where(db::MucLogLine::Date >= start_time);
+ request << " and " << Database::Date{} << ">=" << start_time;
}
if (!end.empty())
{
const auto end_time = utils::parse_datetime(end);
if (end_time != -1)
- request.where(db::MucLogLine::Date <= end_time);
+ request << " and " << Database::Date{} << "<=" << end_time;
}
- const auto& res = request.all();
- return {res.crbegin(), res.crend()};
+
+ request.order_by() << Id{} << " DESC ";
+
+ if (limit >= 0)
+ request.limit() << limit;
+
+ auto result = request.execute(Database::db);
+
+ return {result.crbegin(), result.crend()};
}
void Database::close()
{
- Database::db.reset(nullptr);
+ sqlite3_close_v2(Database::db);
+ Database::db = nullptr;
}
std::string Database::gen_uuid()
@@ -178,5 +192,4 @@ std::string Database::gen_uuid()
return uuid_str;
}
-
-#endif
+#endif \ No newline at end of file
diff --git a/src/database/database.hpp b/src/database/database.hpp
index 6823574..8364abc 100644
--- a/src/database/database.hpp
+++ b/src/database/database.hpp
@@ -1,22 +1,112 @@
#pragma once
-
#include <biboumi.h>
#ifdef USE_DATABASE
-#include "biboudb.hpp"
+#include <database/table.hpp>
+#include <database/column.hpp>
+#include <database/count_query.hpp>
-#include <memory>
+#include <utils/optional_bool.hpp>
-#include <litesql.hpp>
#include <chrono>
+#include <string>
+
+#include <memory>
-class Iid;
class Database
{
-public:
+ public:
using time_point = std::chrono::system_clock::time_point;
+
+ struct Uuid: Column<std::string> { static constexpr auto name = "uuid_";
+ static constexpr auto options = ""; };
+
+ struct Owner: Column<std::string> { static constexpr auto name = "owner_";
+ static constexpr auto options = ""; };
+
+ struct IrcChanName: Column<std::string> { static constexpr auto name = "ircChanName_";
+ static constexpr auto options = ""; };
+
+ struct Channel: Column<std::string> { static constexpr auto name = "channel_";
+ static constexpr auto options = ""; };
+
+ struct IrcServerName: Column<std::string> { static constexpr auto name = "ircServerName_";
+ static constexpr auto options = ""; };
+
+ struct Server: Column<std::string> { static constexpr auto name = "server_";
+ static constexpr auto options = ""; };
+
+ struct Date: Column<time_point::rep> { static constexpr auto name = "date_";
+ static constexpr auto options = ""; };
+
+ struct Body: Column<std::string> { static constexpr auto name = "body_";
+ static constexpr auto options = ""; };
+
+ struct Nick: Column<std::string> { static constexpr auto name = "nick_";
+ static constexpr auto options = ""; };
+
+ struct Pass: Column<std::string> { static constexpr auto name = "pass_";
+ static constexpr auto options = ""; };
+
+ struct Ports: Column<std::string> { static constexpr auto name = "ports_";
+ static constexpr auto options = "";
+ Ports(): Column<std::string>("6667") {} };
+
+ struct TlsPorts: Column<std::string> { static constexpr auto name = "tlsPorts_";
+ static constexpr auto options = "";
+ TlsPorts(): Column<std::string>("6697;6670") {} };
+
+ struct Username: Column<std::string> { static constexpr auto name = "username_";
+ static constexpr auto options = ""; };
+
+ struct Realname: Column<std::string> { static constexpr auto name = "realname_";
+ static constexpr auto options = ""; };
+
+ struct AfterConnectionCommand: Column<std::string> { static constexpr auto name = "afterConnectionCommand_";
+ static constexpr auto options = ""; };
+
+ struct TrustedFingerprint: Column<std::string> { static constexpr auto name = "trustedFingerprint_";
+ static constexpr auto options = ""; };
+
+ struct EncodingOut: Column<std::string> { static constexpr auto name = "encodingOut_";
+ static constexpr auto options = ""; };
+
+ struct EncodingIn: Column<std::string> { static constexpr auto name = "encodingIn_";
+ static constexpr auto options = ""; };
+
+ struct MaxHistoryLength: Column<int> { static constexpr auto name = "maxHistoryLength_";
+ static constexpr auto options = "";
+ MaxHistoryLength(): Column<int>(20) {} };
+
+ struct RecordHistory: Column<bool> { static constexpr auto name = "recordHistory_";
+ static constexpr auto options = "";
+ RecordHistory(): Column<bool>(true) {}};
+
+ struct RecordHistoryOptional: Column<OptionalBool> { static constexpr auto name = "recordHistory_";
+ static constexpr auto options = ""; };
+
+ struct VerifyCert: Column<bool> { static constexpr auto name = "verifyCert_";
+ static constexpr auto options = "";
+ VerifyCert(): Column<bool>(true) {} };
+
+ struct Persistent: Column<bool> { static constexpr auto name = "persistent_";
+ static constexpr auto options = "";
+ Persistent(): Column<bool>(false) {} };
+
+ using MucLogLineTable = Table<Id, Uuid, Owner, IrcChanName, IrcServerName, Date, Body, Nick>;
+ using MucLogLine = MucLogLineTable::RowType;
+
+ using GlobalOptionsTable = Table<Id, Owner, MaxHistoryLength, RecordHistory, Persistent>;
+ using GlobalOptions = GlobalOptionsTable::RowType;
+
+ using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, AfterConnectionCommand, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength>;
+ using IrcServerOptions = IrcServerOptionsTable::RowType;
+
+ using IrcChannelOptionsTable = Table<Id, Owner, Server, Channel, EncodingOut, EncodingIn, MaxHistoryLength, Persistent, RecordHistoryOptional>;
+ using IrcChannelOptions = IrcChannelOptionsTable::RowType;
+
Database() = default;
~Database() = default;
@@ -25,42 +115,40 @@ public:
Database& operator=(const Database&) = delete;
Database& operator=(Database&&) = delete;
- static void set_verbose(const bool val);
-
- template<typename PersistentType>
- static size_t count()
- {
- return litesql::select<PersistentType>(*Database::db).count();
- }
- /**
- * Return the object from the db. Create it beforehand (with all default
- * values) if it is not already present.
- */
- static db::GlobalOptions get_global_options(const std::string& owner);
- static db::IrcServerOptions get_irc_server_options(const std::string& owner,
+ static GlobalOptions get_global_options(const std::string& owner);
+ static IrcServerOptions get_irc_server_options(const std::string& owner,
const std::string& server);
- static db::IrcChannelOptions get_irc_channel_options(const std::string& owner,
- const std::string& server,
- const std::string& channel);
- static db::IrcChannelOptions get_irc_channel_options_with_server_default(const std::string& owner,
- const std::string& server,
- const std::string& channel);
- static db::IrcChannelOptions get_irc_channel_options_with_server_and_global_default(const std::string& owner,
- 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="");
- static void store_muc_message(const std::string& owner, const Iid& iid,
- time_point date, const std::string& body, const std::string& nick);
+ static IrcChannelOptions get_irc_channel_options(const std::string& owner,
+ const std::string& server,
+ const std::string& channel);
+ static IrcChannelOptions get_irc_channel_options_with_server_default(const std::string& owner,
+ const std::string& server,
+ const std::string& channel);
+ static IrcChannelOptions get_irc_channel_options_with_server_and_global_default(const std::string& owner,
+ const std::string& server,
+ const std::string& channel);
+ static std::vector<MucLogLine> get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server,
+ int limit=-1, const std::string& start="", const std::string& end="");
+ static std::string store_muc_message(const std::string& owner, const std::string& chan_name, const std::string& server_name,
+ time_point date, const std::string& body, const std::string& nick);
static void close();
- static void open(const std::string& filename, const std::string& db_type="sqlite3");
+ static void open(const std::string& filename);
+ template <typename TableType>
+ static int64_t count(const TableType& table)
+ {
+ CountQuery query{table.get_name()};
+ return query.execute(Database::db);
+ }
+
+ static MucLogLineTable muc_log_lines;
+ static GlobalOptionsTable global_options;
+ static IrcServerOptionsTable irc_server_options;
+ static IrcChannelOptionsTable irc_channel_options;
+ static sqlite3* db;
-private:
+ private:
static std::string gen_uuid();
- static std::unique_ptr<db::BibouDB> db;
};
#endif /* USE_DATABASE */
-
-
diff --git a/src/database/insert_query.hpp b/src/database/insert_query.hpp
new file mode 100644
index 0000000..9e410ce
--- /dev/null
+++ b/src/database/insert_query.hpp
@@ -0,0 +1,129 @@
+#pragma once
+
+#include <database/statement.hpp>
+#include <database/column.hpp>
+#include <database/query.hpp>
+#include <logger/logger.hpp>
+
+#include <type_traits>
+#include <vector>
+#include <string>
+#include <tuple>
+
+#include <sqlite3.h>
+
+template <int N, typename ColumnType, typename... T>
+typename std::enable_if<!std::is_same<std::decay_t<ColumnType>, Id>::value, void>::type
+actual_bind(Statement& statement, std::vector<std::string>& params, const std::tuple<T...>&)
+{
+ const auto value = params.front();
+ params.erase(params.begin());
+ if (sqlite3_bind_text(statement.get(), N + 1, value.data(), static_cast<int>(value.size()), SQLITE_TRANSIENT) != SQLITE_OK)
+ log_error("Failed to bind ", value, " to param ", N);
+ else
+ log_debug("Bound (not id) [", value, "] to ", N);
+}
+
+template <int N, typename ColumnType, typename... T>
+typename std::enable_if<std::is_same<std::decay_t<ColumnType>, Id>::value, void>::type
+actual_bind(Statement& statement, std::vector<std::string>&, const std::tuple<T...>& columns)
+{
+ auto&& column = std::get<Id>(columns);
+ if (column.value != 0)
+ {
+ if (sqlite3_bind_int64(statement.get(), N + 1, static_cast<sqlite3_int64>(column.value)) != SQLITE_OK)
+ log_error("Failed to bind ", column.value, " to id.");
+ }
+ else if (sqlite3_bind_null(statement.get(), N + 1) != SQLITE_OK)
+ log_error("Failed to bind NULL to param ", N);
+ else
+ log_debug("Bound NULL to ", N);
+}
+
+struct InsertQuery: public Query
+{
+ InsertQuery(const std::string& name):
+ Query("INSERT OR REPLACE INTO ")
+ {
+ this->body += name;
+ }
+
+ template <typename... T>
+ void execute(const std::tuple<T...>& columns, sqlite3* db)
+ {
+ auto statement = this->prepare(db);
+ {
+ this->bind_param(columns, statement);
+ if (sqlite3_step(statement.get()) != SQLITE_DONE)
+ log_error("Failed to execute query: ", sqlite3_errmsg(db));
+ }
+ }
+
+ template <int N=0, typename... T>
+ typename std::enable_if<N < sizeof...(T), void>::type
+ bind_param(const std::tuple<T...>& columns, Statement& statement)
+ {
+ using ColumnType = typename std::remove_reference<decltype(std::get<N>(columns))>::type;
+
+ actual_bind<N, ColumnType>(statement, this->params, columns);
+ this->bind_param<N+1>(columns, statement);
+ }
+
+ template <int N=0, typename... T>
+ typename std::enable_if<N == sizeof...(T), void>::type
+ bind_param(const std::tuple<T...>&, Statement&)
+ {}
+
+ template <typename... T>
+ void insert_values(const std::tuple<T...>& columns)
+ {
+ this->body += "VALUES (";
+ this->insert_value(columns);
+ this->body += ")";
+ }
+
+ template <int N=0, typename... T>
+ typename std::enable_if<N < sizeof...(T), void>::type
+ insert_value(const std::tuple<T...>& columns)
+ {
+ this->body += "?";
+ if (N != sizeof...(T) - 1)
+ this->body += ",";
+ this->body += " ";
+ add_param(*this, std::get<N>(columns));
+ this->insert_value<N+1>(columns);
+ }
+ template <int N=0, typename... T>
+ typename std::enable_if<N == sizeof...(T), void>::type
+ insert_value(const std::tuple<T...>&)
+ { }
+
+ template <typename... T>
+ void insert_col_names(const std::tuple<T...>& columns)
+ {
+ this->body += " (";
+ this->insert_col_name(columns);
+ this->body += ")\n";
+ }
+
+ template <int N=0, typename... T>
+ typename std::enable_if<N < sizeof...(T), void>::type
+ insert_col_name(const std::tuple<T...>& columns)
+ {
+ using ColumnType = typename std::remove_reference<decltype(std::get<N>(columns))>::type;
+
+ this->body += ColumnType::name;
+
+ if (N < (sizeof...(T) - 1))
+ this->body += ", ";
+
+ this->insert_col_name<N+1>(columns);
+ }
+ template <int N=0, typename... T>
+ typename std::enable_if<N == sizeof...(T), void>::type
+ insert_col_name(const std::tuple<T...>&)
+ {}
+
+
+ private:
+};
diff --git a/src/database/query.cpp b/src/database/query.cpp
new file mode 100644
index 0000000..ba63a92
--- /dev/null
+++ b/src/database/query.cpp
@@ -0,0 +1,34 @@
+#include <database/query.hpp>
+#include <database/column.hpp>
+
+template <>
+void add_param<Id>(Query&, const Id&)
+{}
+
+void actual_add_param(Query& query, const std::string& val)
+{
+ query.params.push_back(val);
+}
+
+void actual_add_param(Query& query, const OptionalBool& val)
+{
+ if (!val.is_set)
+ query.params.push_back("0");
+ else if (val.value)
+ query.params.push_back("1");
+ else
+ query.params.push_back("-1");
+}
+
+Query& operator<<(Query& query, const char* str)
+{
+ query.body += str;
+ return query;
+}
+
+Query& operator<<(Query& query, const std::string& str)
+{
+ query.body += "?";
+ actual_add_param(query, str);
+ return query;
+}
diff --git a/src/database/query.hpp b/src/database/query.hpp
new file mode 100644
index 0000000..f103fe9
--- /dev/null
+++ b/src/database/query.hpp
@@ -0,0 +1,90 @@
+#pragma once
+
+#include <utils/optional_bool.hpp>
+#include <database/statement.hpp>
+#include <database/column.hpp>
+
+#include <logger/logger.hpp>
+
+#include <vector>
+#include <string>
+
+#include <sqlite3.h>
+
+struct Query
+{
+ std::string body;
+ std::vector<std::string> params;
+
+ Query(std::string str):
+ body(std::move(str))
+ {}
+
+ Statement prepare(sqlite3* db)
+ {
+ sqlite3_stmt* stmt;
+ log_debug(this->body);
+ auto res = sqlite3_prepare(db, this->body.data(), static_cast<int>(this->body.size()) + 1,
+ &stmt, nullptr);
+ if (res != SQLITE_OK)
+ {
+ log_error("Error preparing statement: ", sqlite3_errmsg(db));
+ return nullptr;
+ }
+ Statement statement(stmt);
+ int i = 1;
+ for (const std::string& param: this->params)
+ {
+ if (sqlite3_bind_text(statement.get(), i, param.data(), static_cast<int>(param.size()), SQLITE_TRANSIENT) != SQLITE_OK)
+ log_debug("Failed to bind ", param, " to param ", i);
+ else
+ log_debug("Bound ", param, " to ", i);
+ i++;
+ }
+
+ return statement;
+ }
+
+ void execute(sqlite3* db)
+ {
+ auto statement = this->prepare(db);
+ while (sqlite3_step(statement.get()) != SQLITE_DONE)
+ ;
+ }
+};
+
+template <typename ColumnType>
+void add_param(Query& query, const ColumnType& column)
+{
+ actual_add_param(query, column.value);
+}
+template <>
+void add_param<Id>(Query& query, const Id& column);
+
+template <typename T>
+void actual_add_param(Query& query, const T& val)
+{
+ query.params.push_back(std::to_string(val));
+}
+
+void actual_add_param(Query& query, const std::string& val);
+void actual_add_param(Query& query, const OptionalBool& val);
+
+template <typename T>
+typename std::enable_if<!std::is_integral<T>::value, Query&>::type
+operator<<(Query& query, const T&)
+{
+ query.body += T::name;
+ return query;
+}
+
+Query& operator<<(Query& query, const char* str);
+Query& operator<<(Query& query, const std::string& str);
+template <typename Integer>
+typename std::enable_if<std::is_integral<Integer>::value, Query&>::type
+operator<<(Query& query, const Integer& i)
+{
+ query.body += "?";
+ actual_add_param(query, i);
+ return query;
+}
diff --git a/src/database/row.hpp b/src/database/row.hpp
new file mode 100644
index 0000000..e7a58c4
--- /dev/null
+++ b/src/database/row.hpp
@@ -0,0 +1,75 @@
+#pragma once
+
+#include <database/insert_query.hpp>
+#include <logger/logger.hpp>
+
+#include <type_traits>
+
+#include <sqlite3.h>
+
+template <typename ColumnType, typename... T>
+typename std::enable_if<!std::is_same<std::decay_t<ColumnType>, Id>::value, void>::type
+update_id(std::tuple<T...>&, sqlite3*)
+{}
+
+template <typename ColumnType, typename... T>
+typename std::enable_if<std::is_same<std::decay_t<ColumnType>, Id>::value, void>::type
+update_id(std::tuple<T...>& columns, sqlite3* db)
+{
+ auto&& column = std::get<ColumnType>(columns);
+ log_debug("Found an autoincrement col.");
+ auto res = sqlite3_last_insert_rowid(db);
+ log_debug("Value is now: ", res);
+ column.value = static_cast<Id::real_type>(res);
+}
+
+template <std::size_t N=0, typename... T>
+typename std::enable_if<N < sizeof...(T), void>::type
+update_autoincrement_id(std::tuple<T...>& columns, sqlite3* db)
+{
+ using ColumnType = typename std::remove_reference<decltype(std::get<N>(columns))>::type;
+ update_id<ColumnType>(columns, db);
+ update_autoincrement_id<N+1>(columns, db);
+}
+
+template <std::size_t N=0, typename... T>
+typename std::enable_if<N == sizeof...(T), void>::type
+update_autoincrement_id(std::tuple<T...>&, sqlite3*)
+{}
+
+template <typename... T>
+struct Row
+{
+ Row(std::string name):
+ table_name(std::move(name))
+ {}
+
+ template <typename Type>
+ auto& col()
+ {
+ auto&& col = std::get<Type>(this->columns);
+ return col.value;
+ }
+
+ template <typename Type>
+ const auto& col() const
+ {
+ auto&& col = std::get<Type>(this->columns);
+ return col.value;
+ }
+
+ void save(sqlite3* db)
+ {
+ InsertQuery query(this->table_name);
+ query.insert_col_names(this->columns);
+ query.insert_values(this->columns);
+ log_debug(query.body);
+
+ query.execute(this->columns, db);
+
+ update_autoincrement_id(this->columns, db);
+ }
+
+ std::tuple<T...> columns;
+ std::string table_name;
+};
diff --git a/src/database/select_query.hpp b/src/database/select_query.hpp
new file mode 100644
index 0000000..f4d71af
--- /dev/null
+++ b/src/database/select_query.hpp
@@ -0,0 +1,127 @@
+#pragma once
+
+#include <database/statement.hpp>
+#include <database/query.hpp>
+#include <logger/logger.hpp>
+#include <database/row.hpp>
+
+#include <utils/optional_bool.hpp>
+
+#include <vector>
+#include <string>
+
+#include <sqlite3.h>
+
+using namespace std::string_literals;
+
+template <typename T>
+typename std::enable_if<std::is_integral<T>::value, sqlite3_int64>::type
+extract_row_value(Statement& statement, const int i)
+{
+ return sqlite3_column_int64(statement.get(), i);
+}
+
+template <typename T>
+typename std::enable_if<std::is_same<std::string, T>::value, T>::type
+extract_row_value(Statement& statement, const int i)
+{
+ const auto size = sqlite3_column_bytes(statement.get(), i);
+ const unsigned char* str = sqlite3_column_text(statement.get(), i);
+ std::string result(reinterpret_cast<const char*>(str), static_cast<std::size_t>(size));
+ return result;
+}
+
+template <typename T>
+typename std::enable_if<std::is_same<OptionalBool, T>::value, T>::type
+extract_row_value(Statement& statement, const int i)
+{
+ const auto integer = sqlite3_column_int(statement.get(), i);
+ OptionalBool result;
+ if (integer > 0)
+ result.set_value(true);
+ else if (integer < 0)
+ result.set_value(false);
+ return result;
+}
+
+template <std::size_t N=0, typename... T>
+typename std::enable_if<N < sizeof...(T), void>::type
+extract_row_values(Row<T...>& row, Statement& statement)
+{
+ using ColumnType = typename std::remove_reference<decltype(std::get<N>(row.columns))>::type;
+
+ auto&& column = std::get<N>(row.columns);
+ column.value = static_cast<decltype(column.value)>(extract_row_value<typename ColumnType::real_type>(statement, N));
+
+ extract_row_values<N+1>(row, statement);
+}
+
+template <std::size_t N=0, typename... T>
+typename std::enable_if<N == sizeof...(T), void>::type
+extract_row_values(Row<T...>&, Statement&)
+{}
+
+template <typename... T>
+struct SelectQuery: public Query
+{
+ SelectQuery(std::string table_name):
+ Query("SELECT"),
+ table_name(table_name)
+ {
+ this->insert_col_name();
+ this->body += " from " + this->table_name;
+ }
+
+ template <std::size_t N=0>
+ typename std::enable_if<N < sizeof...(T), void>::type
+ insert_col_name()
+ {
+ using ColumnsType = std::tuple<T...>;
+ using ColumnType = typename std::remove_reference<decltype(std::get<N>(std::declval<ColumnsType>()))>::type;
+
+ this->body += " "s + ColumnType::name;
+
+ if (N < (sizeof...(T) - 1))
+ this->body += ", ";
+
+ this->insert_col_name<N+1>();
+ }
+ template <std::size_t N=0>
+ typename std::enable_if<N == sizeof...(T), void>::type
+ insert_col_name()
+ {}
+
+ SelectQuery& where()
+ {
+ this->body += " WHERE ";
+ return *this;
+ };
+
+ SelectQuery& order_by()
+ {
+ this->body += " ORDER BY ";
+ return *this;
+ }
+
+ SelectQuery& limit()
+ {
+ this->body += " LIMIT ";
+ return *this;
+ }
+
+ auto execute(sqlite3* db)
+ {
+ auto statement = this->prepare(db);
+ std::vector<Row<T...>> rows;
+ while (sqlite3_step(statement.get()) == SQLITE_ROW)
+ {
+ Row<T...> row(this->table_name);
+ extract_row_values(row, statement);
+ rows.push_back(row);
+ }
+ return rows;
+ }
+
+ const std::string table_name;
+};
+
diff --git a/src/database/statement.hpp b/src/database/statement.hpp
new file mode 100644
index 0000000..87cd70f
--- /dev/null
+++ b/src/database/statement.hpp
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <sqlite3.h>
+
+class Statement
+{
+ public:
+ Statement(sqlite3_stmt* stmt):
+ stmt(stmt) {}
+ ~Statement()
+ {
+ sqlite3_finalize(this->stmt);
+ }
+
+ Statement(const Statement&) = delete;
+ Statement& operator=(const Statement&) = delete;
+ Statement(Statement&& other):
+ stmt(other.stmt)
+ {
+ other.stmt = nullptr;
+ }
+ Statement& operator=(Statement&& other)
+ {
+ this->stmt = other.stmt;
+ other.stmt = nullptr;
+ return *this;
+ }
+ sqlite3_stmt* get()
+ {
+ return this->stmt;
+ }
+
+ private:
+ sqlite3_stmt* stmt;
+};
diff --git a/src/database/table.cpp b/src/database/table.cpp
new file mode 100644
index 0000000..5929f33
--- /dev/null
+++ b/src/database/table.cpp
@@ -0,0 +1,25 @@
+#include <database/table.hpp>
+
+std::set<std::string> get_all_columns_from_table(sqlite3* db, const std::string& table_name)
+{
+ std::set<std::string> result;
+ char* errmsg;
+ std::string query{"PRAGMA table_info("s + table_name + ")"};
+ log_debug(query);
+ int res = sqlite3_exec(db, query.data(), [](void* param, int columns_nb, char** columns, char**) -> int {
+ constexpr int name_column = 1;
+ std::set<std::string>* result = static_cast<std::set<std::string>*>(param);
+ log_debug("Table has column ", columns[name_column]);
+ if (name_column < columns_nb)
+ result->insert(columns[name_column]);
+ return 0;
+ }, &result, &errmsg);
+
+ if (res != SQLITE_OK)
+ {
+ log_error("Error executing ", query, ": ", errmsg);
+ sqlite3_free(errmsg);
+ }
+
+ return result;
+}
diff --git a/src/database/table.hpp b/src/database/table.hpp
new file mode 100644
index 0000000..411ac6a
--- /dev/null
+++ b/src/database/table.hpp
@@ -0,0 +1,127 @@
+#pragma once
+
+#include <database/select_query.hpp>
+#include <database/type_to_sql.hpp>
+#include <logger/logger.hpp>
+#include <database/row.hpp>
+
+#include <algorithm>
+#include <string>
+#include <set>
+
+using namespace std::string_literals;
+
+std::set<std::string> get_all_columns_from_table(sqlite3* db, const std::string& table_name);
+
+template <typename ColumnType>
+void add_column_to_table(sqlite3* db, const std::string& table_name)
+{
+ const std::string name = ColumnType::name;
+ std::string query{"ALTER TABLE "s + table_name + " ADD " + ColumnType::name + " " + TypeToSQLType<typename ColumnType::real_type>::type};
+ log_debug(query);
+ char* error;
+ const auto result = sqlite3_exec(db, query.data(), nullptr, nullptr, &error);
+ if (result != SQLITE_OK)
+ {
+ log_error("Error adding column ", name, " to table ", table_name, ": ", error);
+ sqlite3_free(error);
+ }
+}
+
+template <typename... T>
+class Table
+{
+ static_assert(sizeof...(T) > 0, "Table cannot be empty");
+ using ColumnTypes = std::tuple<T...>;
+
+ public:
+ using RowType = Row<T...>;
+
+ Table(std::string name):
+ name(std::move(name))
+ {}
+
+ void upgrade(sqlite3* db)
+ {
+ const auto existing_columns = get_all_columns_from_table(db, this->name);
+ add_column_if_not_exists(db, existing_columns);
+ }
+
+ void create(sqlite3* db)
+ {
+ std::string res{"CREATE TABLE IF NOT EXISTS "};
+ res += this->name;
+ res += " (\n";
+ this->add_column_create(res);
+ res += ")";
+
+ log_debug(res);
+
+ char* error;
+ const auto result = sqlite3_exec(db, res.data(), nullptr, nullptr, &error);
+ log_debug("result: ", +result);
+ if (result != SQLITE_OK)
+ {
+ log_error("Error executing query: ", error);
+ sqlite3_free(error);
+ }
+ }
+
+ RowType row()
+ {
+ return {this->name};
+ }
+
+ SelectQuery<T...> select()
+ {
+ SelectQuery<T...> select(this->name);
+ return select;
+ }
+
+ const std::string& get_name() const
+ {
+ return this->name;
+ }
+
+ private:
+
+ template <std::size_t N=0>
+ typename std::enable_if<N < sizeof...(T), void>::type
+ add_column_if_not_exists(sqlite3* db, const std::set<std::string>& existing_columns)
+ {
+ using ColumnType = typename std::remove_reference<decltype(std::get<N>(std::declval<ColumnTypes>()))>::type;
+ if (existing_columns.count(ColumnType::name) != 1)
+ {
+ add_column_to_table<ColumnType>(db, this->name);
+ }
+ add_column_if_not_exists<N+1>(db, existing_columns);
+ }
+ template <std::size_t N=0>
+ typename std::enable_if<N == sizeof...(T), void>::type
+ add_column_if_not_exists(sqlite3*, const std::set<std::string>&)
+ {}
+
+ template <std::size_t N=0>
+ typename std::enable_if<N < sizeof...(T), void>::type
+ add_column_create(std::string& str)
+ {
+ using ColumnType = typename std::remove_reference<decltype(std::get<N>(std::declval<ColumnTypes>()))>::type;
+ using RealType = typename ColumnType::real_type;
+ str += ColumnType::name;
+ str += " ";
+ str += TypeToSQLType<RealType>::type;
+ str += " "s + ColumnType::options;
+ if (N != sizeof...(T) - 1)
+ str += ",";
+ str += "\n";
+
+ add_column_create<N+1>(str);
+ }
+
+ template <std::size_t N=0>
+ typename std::enable_if<N == sizeof...(T), void>::type
+ add_column_create(std::string&)
+ { }
+
+ const std::string name;
+};
diff --git a/src/database/type_to_sql.cpp b/src/database/type_to_sql.cpp
new file mode 100644
index 0000000..bcd9daa
--- /dev/null
+++ b/src/database/type_to_sql.cpp
@@ -0,0 +1,9 @@
+#include <database/type_to_sql.hpp>
+
+template <> const std::string TypeToSQLType<int>::type = "INTEGER";
+template <> const std::string TypeToSQLType<std::size_t>::type = "INTEGER";
+template <> const std::string TypeToSQLType<long>::type = "INTEGER";
+template <> const std::string TypeToSQLType<long long>::type = "INTEGER";
+template <> const std::string TypeToSQLType<bool>::type = "INTEGER";
+template <> const std::string TypeToSQLType<std::string>::type = "TEXT";
+template <> const std::string TypeToSQLType<OptionalBool>::type = "INTEGER"; \ No newline at end of file
diff --git a/src/database/type_to_sql.hpp b/src/database/type_to_sql.hpp
new file mode 100644
index 0000000..ba806ab
--- /dev/null
+++ b/src/database/type_to_sql.hpp
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <utils/optional_bool.hpp>
+
+#include <string>
+
+template <typename T>
+struct TypeToSQLType { static const std::string type; };
+
+template <> const std::string TypeToSQLType<int>::type;
+template <> const std::string TypeToSQLType<std::size_t>::type;
+template <> const std::string TypeToSQLType<long>::type;
+template <> const std::string TypeToSQLType<long long>::type;
+template <> const std::string TypeToSQLType<bool>::type;
+template <> const std::string TypeToSQLType<std::string>::type;
+template <> const std::string TypeToSQLType<OptionalBool>::type; \ No newline at end of file
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..a386d80
--- /dev/null
+++ b/src/identd/identd_socket.hpp
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <network/socket_handler.hpp>
+
+#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..a63a1c3 100644
--- a/src/irc/iid.cpp
+++ b/src/irc/iid.cpp
@@ -1,3 +1,4 @@
+#include <utility>
#include <utils/tolower.hpp>
#include <config/config.hpp>
#include <bridge/bridge.hpp>
@@ -7,10 +8,10 @@
constexpr char Iid::separator[];
-Iid::Iid(const std::string& local, const std::string& server, Iid::Type type):
+Iid::Iid(std::string local, std::string server, Iid::Type type):
type(type),
- local(local),
- server(server)
+ local(std::move(local)),
+ server(std::move(server))
{
}
@@ -34,9 +35,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 +107,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..89f4797 100644
--- a/src/irc/iid.hpp
+++ b/src/irc/iid.hpp
@@ -53,12 +53,13 @@ public:
Channel,
User,
Server,
+ None,
};
static constexpr char separator[]{"%"};
Iid(const std::string& iid, const std::set<char>& chantypes);
Iid(const std::string& iid, const std::initializer_list<char>& chantypes);
Iid(const std::string& iid, const Bridge* bridge);
- Iid(const std::string& local, const std::string& server, Type type);
+ Iid(std::string local, std::string server, Type type);
Iid() = default;
Iid(const Iid&) = default;
diff --git a/src/irc/irc_channel.cpp b/src/irc/irc_channel.cpp
index 40d7f54..53043c7 100644
--- a/src/irc/irc_channel.cpp
+++ b/src/irc/irc_channel.cpp
@@ -1,21 +1,25 @@
#include <irc/irc_channel.hpp>
#include <algorithm>
-void IrcChannel::set_self(const std::string& name)
+void IrcChannel::set_self(IrcUser* user)
{
- this->self = std::make_unique<IrcUser>(name);
+ this->self = user;
}
IrcUser* IrcChannel::add_user(const std::string& name,
const std::map<char, char>& prefix_to_mode)
{
- this->users.emplace_back(std::make_unique<IrcUser>(name, prefix_to_mode));
+ auto new_user = std::make_unique<IrcUser>(name, prefix_to_mode);
+ auto old_user = this->find_user(new_user->nick);
+ if (old_user)
+ return old_user;
+ this->users.emplace_back(std::move(new_user));
return this->users.back().get();
}
IrcUser* IrcChannel::get_self() const
{
- return this->self.get();
+ return this->self;
}
IrcUser* IrcChannel::find_user(const std::string& name) const
@@ -32,19 +36,27 @@ IrcUser* IrcChannel::find_user(const std::string& name) const
void IrcChannel::remove_user(const IrcUser* user)
{
const auto nick = user->nick;
+ const bool is_self = (user == this->self);
const auto it = std::find_if(this->users.begin(), this->users.end(),
[nick](const std::unique_ptr<IrcUser>& u)
{
return nick == u->nick;
});
if (it != this->users.end())
- this->users.erase(it);
+ {
+ this->users.erase(it);
+ if (is_self)
+ {
+ this->self = nullptr;
+ this->joined = false;
+ }
+ }
}
void IrcChannel::remove_all_users()
{
this->users.clear();
- this->self.reset();
+ this->self = nullptr;
}
DummyIrcChannel::DummyIrcChannel():
diff --git a/src/irc/irc_channel.hpp b/src/irc/irc_channel.hpp
index 7c269b9..8f85edb 100644
--- a/src/irc/irc_channel.hpp
+++ b/src/irc/irc_channel.hpp
@@ -27,7 +27,7 @@ public:
bool parting{false};
std::string topic{};
std::string topic_author{};
- void set_self(const std::string& name);
+ void set_self(IrcUser* user);
IrcUser* get_self() const;
IrcUser* add_user(const std::string& name,
const std::map<char, char>& prefix_to_mode);
@@ -38,7 +38,8 @@ public:
{ return this->users; }
protected:
- std::unique_ptr<IrcUser> self{};
+ // Pointer to one IrcUser stored in users
+ IrcUser* self{nullptr};
std::vector<std::unique_ptr<IrcUser>> users{};
};
diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp
index de6b089..bacb89e 100644
--- a/src/irc/irc_client.cpp
+++ b/src/irc/irc_client.cpp
@@ -1,3 +1,4 @@
+#include <utility>
#include <utils/timed_events.hpp>
#include <database/database.hpp>
#include <irc/irc_message.hpp>
@@ -14,13 +15,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;
@@ -61,11 +62,14 @@ static const std::unordered_map<std::string,
{"333", {&IrcClient::on_topic_who_time_received, {4, 0}}},
{"RPL_TOPICWHOTIME", {&IrcClient::on_topic_who_time_received, {4, 0}}},
{"366", {&IrcClient::on_channel_completely_joined, {2, 0}}},
+ {"367", {&IrcClient::on_banlist, {3, 0}}},
+ {"368", {&IrcClient::on_banlist_end, {3, 0}}},
{"396", {&IrcClient::on_own_host_received, {2, 0}}},
{"432", {&IrcClient::on_erroneous_nickname, {2, 0}}},
{"433", {&IrcClient::on_nickname_conflict, {2, 0}}},
{"438", {&IrcClient::on_nickname_change_too_fast, {2, 0}}},
{"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 +117,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,16 +130,16 @@ 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,
- const std::string& nickname, const std::string& username,
- const std::string& realname, const std::string& user_hostname,
+IrcClient::IrcClient(std::shared_ptr<Poller>& poller, std::string hostname,
+ std::string nickname, std::string username,
+ std::string realname, std::string user_hostname,
Bridge& bridge):
- TCPSocketHandler(poller),
- hostname(hostname),
- user_hostname(user_hostname),
- username(username),
- realname(realname),
- current_nick(nickname),
+ TCPClientSocketHandler(poller),
+ hostname(std::move(hostname)),
+ user_hostname(std::move(user_hostname)),
+ username(std::move(username)),
+ realname(std::move(realname)),
+ current_nick(std::move(nickname)),
bridge(bridge),
welcomed(false),
chanmodes({"", "", "", ""}),
@@ -153,11 +156,11 @@ IrcClient::IrcClient(std::shared_ptr<Poller> poller, const std::string& hostname
#ifdef USE_DATABASE
auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
this->get_hostname());
- std::vector<std::string> ports = utils::split(options.ports, ';', false);
+ std::vector<std::string> ports = utils::split(options.col<Database::Ports>(), ';', false);
for (auto it = ports.rbegin(); it != ports.rend(); ++it)
this->ports_to_try.emplace(*it, false);
# ifdef BOTAN_FOUND
- ports = utils::split(options.tlsPorts, ';', false);
+ ports = utils::split(options.col<Database::TlsPorts>(), ';', false);
for (auto it = ports.rbegin(); it != ports.rend(); ++it)
this->ports_to_try.emplace(*it, true);
# endif // BOTAN_FOUND
@@ -201,7 +204,7 @@ void IrcClient::start()
# ifdef USE_DATABASE
auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
this->get_hostname());
- this->credential_manager.set_trusted_fingerprint(options.trustedFingerprint);
+ this->credential_manager.set_trusted_fingerprint(options.col<Database::TrustedFingerprint>());
# endif
#endif
this->connect(this->hostname, port, tls);
@@ -272,8 +275,8 @@ void IrcClient::on_connected()
#ifdef USE_DATABASE
auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
this->get_hostname());
- if (!options.pass.value().empty())
- this->send_pass_command(options.pass.value());
+ if (!options.col<Database::Pass>().empty())
+ this->send_pass_command(options.col<Database::Pass>());
#endif
this->send_nick_command(this->current_nick);
@@ -281,10 +284,10 @@ void IrcClient::on_connected()
#ifdef USE_DATABASE
if (Config::get("realname_customization", "true") == "true")
{
- if (!options.username.value().empty())
- this->username = options.username.value();
- if (!options.realname.value().empty())
- this->realname = options.realname.value();
+ if (!options.col<Database::Username>().empty())
+ this->username = options.col<Database::Username>();
+ if (!options.col<Database::Realname>().empty())
+ this->realname = options.col<Database::Realname>();
this->send_user_command(username, realname);
}
else
@@ -343,7 +346,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
@@ -386,10 +389,10 @@ void IrcClient::send_message(IrcMessage&& message)
std::string res;
if (!message.prefix.empty())
res += ":" + std::move(message.prefix) + " ";
- res += std::move(message.command);
+ res += message.command;
for (const std::string& arg: message.arguments)
{
- if (arg.find(" ") != std::string::npos ||
+ if (arg.find(' ') != std::string::npos ||
(!arg.empty() && arg[0] == ':'))
{
res += " :" + arg;
@@ -455,7 +458,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
@@ -501,15 +509,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)
@@ -546,9 +546,18 @@ void IrcClient::forward_server_message(const IrcMessage& message)
void IrcClient::on_notice(const IrcMessage& message)
{
std::string from = message.prefix;
- const std::string to = message.arguments[0];
+ std::string to = message.arguments[0];
const std::string body = message.arguments[1];
+ // Handle notices starting with [#channame] as if they were sent to that channel
+ if (body.size() > 3 && body[0] == '[')
+ {
+ const auto chan_prefix = body[1];
+ auto end = body.find(']');
+ if (this->chantypes.find(chan_prefix) != this->chantypes.end() && end != std::string::npos)
+ to = body.substr(1, end - 1);
+ }
+
if (!body.empty() && body[0] == '\01' && body[body.size() - 1] == '\01')
// Do not forward the notice to the user if it's a CTCP command
return ;
@@ -635,15 +644,18 @@ void IrcClient::set_and_forward_user_list(const IrcMessage& message)
std::vector<std::string> nicks = utils::split(message.arguments[3], ' ');
for (const std::string& nick: nicks)
{
- const IrcUser* user = channel->add_user(nick, this->prefix_to_mode);
- if (user->nick != channel->get_self()->nick)
+ // Just create this dummy user to parse and get its modes
+ IrcUser tmp_user{nick, this->prefix_to_mode};
+ // Does this concern ourself
+ if (channel->get_self() && channel->find_user(tmp_user.nick) == channel->get_self())
{
- this->bridge.send_user_join(this->hostname, chan_name, user, user->get_most_significant_mode(this->sorted_user_modes), false);
+ // We now know our own modes, that’s all.
+ channel->get_self()->modes = tmp_user.modes;
}
else
- {
- // we now know the modes of self, so copy the modes into self
- channel->get_self()->modes = user->modes;
+ { // Otherwise this is a new user
+ const IrcUser *user = channel->add_user(nick, this->prefix_to_mode);
+ this->bridge.send_user_join(this->hostname, chan_name, user, user->get_most_significant_mode(this->sorted_user_modes), false);
}
}
}
@@ -657,13 +669,11 @@ void IrcClient::on_channel_join(const IrcMessage& message)
else
channel = this->get_channel(chan_name);
const std::string nick = message.prefix;
+ IrcUser* user = channel->add_user(nick, this->prefix_to_mode);
if (channel->joined == false)
- channel->set_self(nick);
+ channel->set_self(user);
else
- {
- const IrcUser* user = channel->add_user(nick, this->prefix_to_mode);
- this->bridge.send_user_join(this->hostname, chan_name, user, user->get_most_significant_mode(this->sorted_user_modes), false);
- }
+ this->bridge.send_user_join(this->hostname, chan_name, user, user->get_most_significant_mode(this->sorted_user_modes), false);
}
void IrcClient::on_channel_message(const IrcMessage& message)
@@ -776,6 +786,43 @@ void IrcClient::on_channel_completely_joined(const IrcMessage& message)
this->bridge.send_topic(this->hostname, chan_name, channel->topic, channel->topic_author);
}
+void IrcClient::on_banlist(const IrcMessage& message)
+{
+ const std::string chan_name = utils::tolower(message.arguments[1]);
+ IrcChannel* channel = this->get_channel(chan_name);
+ if (channel->joined)
+ {
+ Iid iid;
+ iid.set_local(chan_name);
+ iid.set_server(this->hostname);
+ iid.type = Iid::Type::Channel;
+ std::string body{message.arguments[2] + " banned"};
+ if (message.arguments.size() >= 4)
+ {
+ IrcUser by(message.arguments[3], this->prefix_to_mode);
+ body += " by " + by.nick;
+ }
+ if (message.arguments.size() >= 5)
+ body += " on " + message.arguments[4];
+
+ this->bridge.send_message(iid, "", body, true);
+ }
+}
+
+void IrcClient::on_banlist_end(const IrcMessage& message)
+{
+ const std::string chan_name = utils::tolower(message.arguments[1]);
+ IrcChannel* channel = this->get_channel(chan_name);
+ if (channel->joined)
+ {
+ Iid iid;
+ iid.set_local(chan_name);
+ iid.set_server(this->hostname);
+ iid.type = Iid::Type::Channel;
+ this->bridge.send_message(iid, "", message.arguments[2], true);
+ }
+}
+
void IrcClient::on_own_host_received(const IrcMessage& message)
{
this->own_host = message.arguments[1];
@@ -799,10 +846,10 @@ void IrcClient::on_nickname_conflict(const IrcMessage& message)
{
const std::string nickname = message.arguments[1];
this->on_generic_error(message);
- for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
+ for (const auto& pair: this->channels)
{
Iid iid;
- iid.set_local(it->first);
+ iid.set_local(pair.first);
iid.set_server(this->hostname);
iid.type = Iid::Type::Channel;
this->bridge.send_nickname_conflict_error(iid, nickname);
@@ -816,10 +863,10 @@ void IrcClient::on_nickname_change_too_fast(const IrcMessage& message)
if (message.arguments.size() >= 3)
txt = message.arguments[2];
this->on_generic_error(message);
- for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
+ for (const auto& pair: this->channels)
{
Iid iid;
- iid.set_local(it->first);
+ iid.set_local(pair.first);
iid.set_server(this->hostname);
iid.type = Iid::Type::Channel;
this->bridge.send_presence_error(iid, nickname,
@@ -847,14 +894,53 @@ void IrcClient::on_welcome_message(const IrcMessage& message)
#ifdef USE_DATABASE
auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
this->get_hostname());
- if (!options.afterConnectionCommand.value().empty())
- this->send_raw(options.afterConnectionCommand.value());
+ if (!options.col<Database::AfterConnectionCommand>().empty())
+ this->send_raw(options.col<Database::AfterConnectionCommand>());
#endif
// Install a repeated events to regularly send a PING
TimedEventsManager::instance().add_event(TimedEvent(240s, std::bind(&IrcClient::send_ping_command, this),
"PING"s + this->hostname + this->bridge.get_jid()));
+ std::string channels{};
+ std::string channels_with_key{};
+ std::string keys{};
+
for (const auto& tuple: this->channels_to_join)
- this->send_join_command(std::get<0>(tuple), std::get<1>(tuple));
+ {
+ const auto& chan = std::get<0>(tuple);
+ const auto& key = std::get<1>(tuple);
+ if (chan.empty())
+ continue;
+ if (!key.empty())
+ {
+ if (keys.size() + channels_with_key.size() >= 300)
+ { // Arbitrary size, to make sure we never send more than 512
+ this->send_join_command(channels_with_key, keys);
+ channels_with_key.clear();
+ keys.clear();
+ }
+ if (!keys.empty())
+ keys += ",";
+ keys += key;
+ if (!channels_with_key.empty())
+ channels_with_key += ",";
+ channels_with_key += chan;
+ }
+ else
+ {
+ if (channels.size() >= 300)
+ { // Arbitrary size, to make sure we never send more than 512
+ this->send_join_command(channels, {});
+ channels.clear();
+ }
+ if (!channels.empty())
+ channels += ",";
+ channels += chan;
+ }
+ }
+ if (!channels.empty())
+ this->send_join_command(channels, {});
+ if (!channels_with_key.empty())
+ this->send_join_command(channels_with_key, keys);
this->channels_to_join.clear();
// Indicate that the dummy channel is joined as well, if needed
if (this->dummy_channel.joining)
@@ -883,20 +969,19 @@ void IrcClient::on_part(const IrcMessage& message)
if (user)
{
std::string nick = user->nick;
+ bool self = channel->get_self() && channel->get_self()->nick == nick;
channel->remove_user(user);
Iid iid;
iid.set_local(chan_name);
iid.set_server(this->hostname);
iid.type = Iid::Type::Channel;
- bool self = channel->get_self()->nick == nick;
if (self)
{
- channel->joined = false;
this->channels.erase(utils::tolower(chan_name));
// channel pointer is now invalid
channel = nullptr;
}
- this->bridge.send_muc_leave(std::move(iid), std::move(nick), std::move(txt), self);
+ this->bridge.send_muc_leave(iid, std::move(nick), txt, self);
}
}
@@ -904,17 +989,17 @@ void IrcClient::on_error(const IrcMessage& message)
{
const std::string leave_message = message.arguments[0];
// The user is out of all the channels
- for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
+ for (const auto& pair: this->channels)
{
Iid iid;
- iid.set_local(it->first);
+ iid.set_local(pair.first);
iid.set_server(this->hostname);
iid.type = Iid::Type::Channel;
- IrcChannel* channel = it->second.get();
+ IrcChannel* channel = pair.second.get();
if (!channel->joined)
continue;
std::string own_nick = channel->get_self()->nick;
- this->bridge.send_muc_leave(std::move(iid), std::move(own_nick), leave_message, true);
+ this->bridge.send_muc_leave(iid, std::move(own_nick), leave_message, true);
}
this->channels.clear();
this->send_gateway_message("ERROR: "s + leave_message);
@@ -925,10 +1010,10 @@ void IrcClient::on_quit(const IrcMessage& message)
std::string txt;
if (message.arguments.size() >= 1)
txt = message.arguments[0];
- for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
+ for (const auto& pair: this->channels)
{
- const std::string chan_name = it->first;
- IrcChannel* channel = it->second.get();
+ const std::string& chan_name = pair.first;
+ IrcChannel* channel = pair.second.get();
const IrcUser* user = channel->find_user(message.prefix);
if (user)
{
@@ -938,7 +1023,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);
}
}
}
@@ -974,9 +1059,9 @@ void IrcClient::on_nick(const IrcMessage& message)
{
change_nick_func("", &this->get_dummy_channel());
}
- for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
+ for (const auto& pair: this->channels)
{
- change_nick_func(it->first, it->second.get());
+ change_nick_func(pair.first, pair.second.get());
}
}
@@ -1019,6 +1104,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
@@ -1075,7 +1172,7 @@ void IrcClient::on_channel_mode(const IrcMessage& message)
{
// That mode can also be of type B if it is present in the
// prefix_to_mode map
- for (const std::pair<char, char>& pair: this->prefix_to_mode)
+ for (const auto& pair: this->prefix_to_mode)
if (pair.second == c)
{
type = 1;
@@ -1148,14 +1245,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
@@ -1163,7 +1260,7 @@ bool IrcClient::abort_on_invalid_cert() const
{
#ifdef USE_DATABASE
auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), this->hostname);
- return options.verifyCert.value();
+ return options.col<Database::VerifyCert>();
#endif
return true;
}
diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp
index 1b4d892..aec6cd9 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,12 +23,12 @@ 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,
- const std::string& nickname, const std::string& username,
- const std::string& realname, const std::string& user_hostname,
+ explicit IrcClient(std::shared_ptr<Poller>& poller, std::string hostname,
+ std::string nickname, std::string username,
+ std::string realname, std::string user_hostname,
Bridge& bridge);
~IrcClient();
@@ -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.
@@ -222,6 +222,8 @@ public:
* received etc), send the self presence and topic to the XMPP user.
*/
void on_channel_completely_joined(const IrcMessage& message);
+ void on_banlist(const IrcMessage& message);
+ void on_banlist_end(const IrcMessage& message);
/**
* Save our own host, as reported by the server
*/
@@ -257,6 +259,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 +285,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 488032d..5725584 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -6,15 +6,14 @@
#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>
-#ifdef USE_DATABASE
-# include <litesql.hpp>
-#endif
+#include <csignal>
+
+#include <identd/identd_server.hpp>
// A flag set by the SIGINT signal handler.
static std::atomic<bool> stop(false);
@@ -89,7 +88,7 @@ int main(int ac, char** av)
#ifdef USE_DATABASE
try {
open_database();
- } catch (const litesql::DatabaseError&) {
+ } catch (...) {
return 1;
}
#endif
@@ -97,7 +96,7 @@ int main(int ac, char** av)
// Block the signals we want to manage. They will be unblocked only during
// the epoll_pwait or ppoll calls. This avoids some race conditions,
// explained in man 2 pselect on linux
- sigset_t mask;
+ sigset_t mask{};
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
@@ -126,13 +125,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)
{
@@ -140,6 +143,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...");
@@ -149,6 +153,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");
}
@@ -162,26 +170,36 @@ 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)
+ {
+ static const std::string reconnect_name{"XMPP reconnection"};
+ if (xmpp_component->first_connection_try == true)
+ { // immediately re-try to connect
+ xmpp_component->reset();
+ xmpp_component->start();
+ }
+ else if (!TimedEventsManager::instance().find_event(reconnect_name))
+ { // 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, reconnect_name);
+ 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.
@@ -189,18 +207,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..f93a366 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>
@@ -7,10 +7,6 @@
#include <botan/tls_exceptn.h>
#include <config/config.hpp>
-#ifdef USE_DATABASE
-# include <database/database.hpp>
-#endif
-
/**
* TODO find a standard way to find that out.
*/
@@ -44,7 +40,7 @@ const std::string& BasicCredentialsManager::get_trusted_fingerprint() const
void check_tls_certificate(const std::vector<Botan::X509_Certificate>& certs,
const std::string& hostname, const std::string& trusted_fingerprint,
- std::exception_ptr exc)
+ const std::exception_ptr& exc)
{
if (!trusted_fingerprint.empty() && !certs.empty() &&
@@ -98,7 +94,7 @@ bool BasicCredentialsManager::try_to_open_one_ca_bundle(const std::vector<std::s
// because the certificate is signed by an issuer that was ignored.
try {
Botan::X509_Certificate cert(bundle);
- BasicCredentialsManager::certificate_store.add_certificate(std::move(cert));
+ BasicCredentialsManager::certificate_store.add_certificate(cert);
} catch (const Botan::Decoding_Error& error) {
continue;
}
diff --git a/louloulibs/network/credentials_manager.hpp b/src/network/credentials_manager.hpp
index 9f42782..e7c247d 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;
@@ -20,7 +19,7 @@ class TCPSocketHandler;
*/
void check_tls_certificate(const std::vector<Botan::X509_Certificate>& certs,
const std::string& hostname, const std::string& trusted_fingerprint,
- std::exception_ptr exc);
+ const std::exception_ptr& exc);
class BasicCredentialsManager: public Botan::Credentials_Manager
{
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..0f02cc5 100644
--- a/louloulibs/network/poller.cpp
+++ b/src/network/poller.cpp
@@ -2,8 +2,8 @@
#include <logger/logger.hpp>
#include <utils/timed_events.hpp>
-#include <assert.h>
-#include <errno.h>
+#include <cassert>
+#include <cerrno>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
@@ -198,9 +198,13 @@ int Poller::poll(const std::chrono::milliseconds& timeout)
static const size_t max_events = 12;
struct epoll_event revents[max_events];
// Unblock all signals, only during the epoll_pwait call
- sigset_t empty_signal_set;
+ sigset_t empty_signal_set{};
sigemptyset(&empty_signal_set);
- const int nb_events = ::epoll_pwait(this->epfd, revents, max_events, timeout.count(),
+
+ int real_timeout = std::numeric_limits<int>::max();
+ if (timeout.count() < real_timeout) // Just avoid any potential int overflow
+ real_timeout = static_cast<int>(timeout.count());
+ const int nb_events = ::epoll_pwait(this->epfd, revents, max_events, real_timeout,
&empty_signal_set);
if (nb_events == -1)
{
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..35f2446
--- /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 auto 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..652b773
--- /dev/null
+++ b/src/network/tcp_server_socket.hpp
@@ -0,0 +1,69 @@
+#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>
+
+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..1049375 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>
@@ -10,18 +8,28 @@
#include <sys/types.h>
#include <stdexcept>
#include <unistd.h>
-#include <errno.h>
+#include <cerrno>
#include <cstring>
-#include <fcntl.h>
#ifdef BOTAN_FOUND
# include <botan/hex.h>
# include <botan/tls_exceptn.h>
+# include <config/config.hpp>
+# include <utils/dirname.hpp>
-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;
+ }
+ 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
@@ -31,14 +39,10 @@ Botan::TLS::Session_Manager_In_Memory TCPSocketHandler::session_manager(TCPSocke
using namespace std::string_literals;
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 +50,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 +103,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));
@@ -286,9 +122,9 @@ ssize_t TCPSocketHandler::do_recv(void* recv_buf, const size_t buf_size)
void TCPSocketHandler::on_send()
{
struct iovec msg_iov[UIO_FASTIOV] = {};
- struct msghdr msg{nullptr, 0,
- msg_iov,
- 0, nullptr, 0, 0};
+ struct msghdr msg{};
+ msg.msg_iov = msg_iov;
+ msg.msg_iovlen = 0;
for (const std::string& s: this->out_buf)
{
// unconsting the content of s is ok, sendmsg will never modify it
@@ -333,29 +169,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 +201,41 @@ 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_string)
{
- Botan::TLS::Server_Information server_info(this->address, "irc", std::stoul(this->port));
+ auto port = std::min(std::stoul(port_string), static_cast<unsigned long>(std::numeric_limits<uint16_t>::max()));
+ Botan::TLS::Server_Information server_info(address, "irc", static_cast<uint16_t>(port));
+ auto policy_directory = Config::get("policy_directory", utils::dirname(Config::get_filename()));
+ if (!policy_directory.empty() && policy_directory[policy_directory.size()-1] != '/')
+ policy_directory += '/';
+ this->policy.load(policy_directory + "policy.txt");
+ this->policy.load(policy_directory + address + ".policy.txt");
this->tls = std::make_unique<Botan::TLS::Client>(
# if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,11,32)
*this,
@@ -427,8 +245,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, this->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..f68698e 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,18 +19,11 @@
#include <list>
#ifdef BOTAN_FOUND
-#include <botan/version.h>
-class BiboumiTLSPolicy: public Botan::TLS::Policy
-{
-public:
-# if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,11,33)
- bool use_ecc_point_compression() const override
- {
- return true;
- }
-# endif
-};
+# include <botan/types.h>
+# include <botan/botan.h>
+# include <botan/tls_session_manager.h>
+# include <network/tls_policy.hpp>
# if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,11,32)
# define BOTAN_TLS_CALLBACKS_OVERRIDE override final
@@ -41,10 +33,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 +46,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 +76,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 +84,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 +99,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 +121,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 +182,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 +195,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,41 +207,16 @@ 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:
+ BiboumiTLSPolicy policy;
/**
* We use a unique_ptr because we may not want to create the object at
* all. The Botan::TLS::Client object generates a handshake message and
diff --git a/src/network/tls_policy.cpp b/src/network/tls_policy.cpp
new file mode 100644
index 0000000..5439397
--- /dev/null
+++ b/src/network/tls_policy.cpp
@@ -0,0 +1,48 @@
+#include "biboumi.h"
+
+#ifdef BOTAN_FOUND
+
+#include <fstream>
+
+#include <utils/tolower.hpp>
+
+#include <network/tls_policy.hpp>
+#include <logger/logger.hpp>
+
+bool BiboumiTLSPolicy::load(const std::string& filename)
+{
+ std::ifstream is(filename.data());
+ if (is)
+ {
+ try {
+ this->load(is);
+ log_info("Successfully loaded policy file: ", filename);
+ return true;
+ } catch (const Botan::Exception& e) {
+ log_error("Failed to parse policy_file ", filename, ": ", e.what());
+ return false;
+ }
+ }
+ log_info("Could not open policy file: ", filename);
+ return false;
+}
+
+void BiboumiTLSPolicy::load(std::istream& is)
+{
+ const auto dict = Botan::read_cfg(is);
+ for (const auto& pair: dict)
+ {
+ // Workaround for options that are not overridden in Botan::TLS::Text_Policy
+ if (pair.first == "require_cert_revocation_info")
+ this->req_cert_revocation_info = !(pair.second == "0" || utils::tolower(pair.second) == "false");
+ else
+ this->set(pair.first, pair.second);
+ }
+}
+
+bool BiboumiTLSPolicy::require_cert_revocation_info() const
+{
+ return this->req_cert_revocation_info;
+}
+
+#endif
diff --git a/src/network/tls_policy.hpp b/src/network/tls_policy.hpp
new file mode 100644
index 0000000..29fd2b3
--- /dev/null
+++ b/src/network/tls_policy.hpp
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "biboumi.h"
+
+#ifdef BOTAN_FOUND
+
+#include <botan/tls_policy.h>
+
+class BiboumiTLSPolicy: public Botan::TLS::Text_Policy
+{
+public:
+ BiboumiTLSPolicy():
+ Botan::TLS::Text_Policy({})
+ {}
+ bool load(const std::string& filename);
+ void load(std::istream& iss);
+
+ BiboumiTLSPolicy(const BiboumiTLSPolicy &) = delete;
+ BiboumiTLSPolicy(BiboumiTLSPolicy &&) = delete;
+ BiboumiTLSPolicy &operator=(const BiboumiTLSPolicy &) = delete;
+ BiboumiTLSPolicy &operator=(BiboumiTLSPolicy &&) = delete;
+
+ bool require_cert_revocation_info() const override;
+protected:
+ bool req_cert_revocation_info{true};
+};
+
+#endif
diff --git a/src/utils/dirname.cpp b/src/utils/dirname.cpp
new file mode 100644
index 0000000..71c9c38
--- /dev/null
+++ b/src/utils/dirname.cpp
@@ -0,0 +1,16 @@
+#include <utils/dirname.hpp>
+
+namespace utils
+{
+ std::string dirname(const std::string filename)
+ {
+ if (filename.empty())
+ return "./";
+ if (filename == ".." || filename == ".")
+ return filename;
+ auto pos = filename.rfind('/');
+ if (pos == std::string::npos)
+ return "./";
+ return filename.substr(0, pos + 1);
+ }
+}
diff --git a/src/utils/dirname.hpp b/src/utils/dirname.hpp
new file mode 100644
index 0000000..c1df81b
--- /dev/null
+++ b/src/utils/dirname.hpp
@@ -0,0 +1,6 @@
+#include <string>
+
+namespace utils
+{
+std::string dirname(const std::string filename);
+}
diff --git a/louloulibs/utils/encoding.cpp b/src/utils/encoding.cpp
index 60f2212..cff0039 100644
--- a/louloulibs/utils/encoding.cpp
+++ b/src/utils/encoding.cpp
@@ -4,9 +4,10 @@
#include <stdexcept>
-#include <assert.h>
+#include <cassert>
#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/src/utils/optional_bool.hpp b/src/utils/optional_bool.hpp
new file mode 100644
index 0000000..59bbbab
--- /dev/null
+++ b/src/utils/optional_bool.hpp
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <string>
+
+struct OptionalBool
+{
+ OptionalBool() = default;
+
+ OptionalBool(bool value):
+ is_set(true), value(value) {}
+
+ void set_value(bool value)
+ {
+ this->is_set = true;
+ this->value = value;
+ }
+
+ void unset()
+ {
+ this->is_set = false;
+ }
+
+ std::string to_string()
+ {
+ if (this->is_set == false)
+ return "unset";
+ else if (this->value)
+ return "true";
+ else
+ return "false";
+ }
+
+ bool is_set{false};
+ bool value{false};
+};
diff --git a/src/utils/reload.cpp b/src/utils/reload.cpp
index 348c5b5..fdca9bc 100644
--- a/src/utils/reload.cpp
+++ b/src/utils/reload.cpp
@@ -26,7 +26,7 @@ void reload_process()
#ifdef USE_DATABASE
try {
open_database();
- } catch (const litesql::DatabaseError&) {
+ } catch (...) {
log_warning("Re-using the previous database.");
}
#endif
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..071ce2c 100644
--- a/louloulibs/utils/string.hpp
+++ b/src/utils/string.hpp
@@ -6,5 +6,3 @@
bool to_bool(const std::string& val);
std::vector<std::string> cut(const std::string& val, const std::size_t size);
-
-
diff --git a/src/utils/system.cpp b/src/utils/system.cpp
new file mode 100644
index 0000000..d821dec
--- /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..bc2c18d 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,10 +24,10 @@ 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;
+ std::string remainings;
+ ss >> std::get_time(&t, format) >> remainings;
if (ss.fail())
return -1;
#else
@@ -36,12 +36,22 @@ std::time_t parse_datetime(const std::string& stamp)
if (!strptime(stamp.data(), format, &t)) {
return -1;
}
- const std::string timezone(stamp.data() + stamp_size_without_tz);
+ const std::string remainings(stamp.data() + stamp_size_without_tz);
#endif
- if (timezone.empty())
+ if (remainings.empty())
return -1;
+ std::string timezone;
+ // Skip optional fractions of seconds
+ if (remainings[0] == '.')
+ {
+ const auto pos = remainings.find_first_not_of(".0123456789");
+ timezone = remainings.substr(pos);
+ }
+ else
+ timezone = std::move(remainings);
+
if (timezone.compare(0, 1, "Z") != 0)
{
std::stringstream tz_ss;
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..26ded82 100644
--- a/louloulibs/utils/timed_events.cpp
+++ b/src/utils/timed_events.cpp
@@ -1,22 +1,23 @@
+#include <utility>
#include <utils/timed_events.hpp>
TimedEvent::TimedEvent(std::chrono::steady_clock::time_point&& time_point,
- std::function<void()> callback, const std::string& name):
- time_point(std::move(time_point)),
- callback(callback),
+ std::function<void()> callback, std::string name):
+ time_point(time_point),
+ callback(std::move(callback)),
repeat(false),
repeat_delay(0),
- name(name)
+ name(std::move(name))
{
}
TimedEvent::TimedEvent(std::chrono::milliseconds&& duration,
- std::function<void()> callback, const std::string& name):
+ std::function<void()> callback, std::string name):
time_point(std::chrono::steady_clock::now() + duration),
- callback(callback),
+ callback(std::move(callback)),
repeat(true),
- repeat_delay(std::move(duration)),
- name(name)
+ repeat_delay(duration),
+ name(std::move(name))
{
}
@@ -32,10 +33,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..fa0fc50 100644
--- a/louloulibs/utils/timed_events.hpp
+++ b/src/utils/timed_events.hpp
@@ -25,9 +25,9 @@ public:
* An event the occurs only once, at the given time_point
*/
explicit TimedEvent(std::chrono::steady_clock::time_point&& time_point,
- std::function<void()> callback, const std::string& name="");
+ std::function<void()> callback, std::string name="");
explicit TimedEvent(std::chrono::milliseconds&& duration,
- std::function<void()> callback, const std::string& name="");
+ std::function<void()> callback, std::string name="");
explicit TimedEvent(TimedEvent&&) = default;
TimedEvent& operator=(TimedEvent&&) = default;
@@ -125,6 +125,11 @@ public:
* Return the number of managed events.
*/
std::size_t size() const;
+ /**
+ * Return a pointer to the first event with the given name. If none
+ * is found, returns nullptr.
+ */
+ const TimedEvent* find_event(const std::string& name) const;
private:
std::vector<TimedEvent> events;
diff --git a/louloulibs/utils/timed_events_manager.cpp b/src/utils/timed_events_manager.cpp
index 67d61fe..75e6338 100644
--- a/louloulibs/utils/timed_events_manager.cpp
+++ b/src/utils/timed_events_manager.cpp
@@ -1,5 +1,7 @@
#include <utils/timed_events.hpp>
+#include <algorithm>
+
TimedEventsManager& TimedEventsManager::instance()
{
static TimedEventsManager inst;
@@ -67,7 +69,19 @@ std::size_t TimedEventsManager::cancel(const std::string& name)
return res;
}
+
+
std::size_t TimedEventsManager::size() const
{
return this->events.size();
}
+
+const TimedEvent* TimedEventsManager::find_event(const std::string& name) const
+{
+ const auto it = std::find_if(this->events.begin(), this->events.end(), [&name](const TimedEvent& o) {
+ return o.get_name() == name;
+ });
+ if (it == this->events.end())
+ return nullptr;
+ return &*it;
+}
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..7be6922 100644
--- a/louloulibs/utils/xdg.hpp
+++ b/src/utils/xdg.hpp
@@ -10,5 +10,3 @@
*/
std::string xdg_config_path(const std::string& filename);
std::string xdg_data_path(const std::string& filename);
-
-
diff --git a/louloulibs/xmpp/adhoc_command.cpp b/src/xmpp/adhoc_command.cpp
index 99701d7..e02bf35 100644
--- a/louloulibs/xmpp/adhoc_command.cpp
+++ b/src/xmpp/adhoc_command.cpp
@@ -1,11 +1,12 @@
+#include <utility>
#include <xmpp/adhoc_command.hpp>
#include <xmpp/xmpp_component.hpp>
#include <utils/reload.hpp>
using namespace std::string_literals;
-AdhocCommand::AdhocCommand(std::vector<AdhocStep>&& callbacks, const std::string& name, const bool admin_only):
- name(name),
+AdhocCommand::AdhocCommand(std::vector<AdhocStep>&& callbacks, std::string name, const bool admin_only):
+ name(std::move(name)),
callbacks(std::move(callbacks)),
admin_only(admin_only)
{
@@ -18,30 +19,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 +55,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 +75,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..c00d9e6 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, 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..4129517 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,61 @@ 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");
- 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));
-
- XmlNode record_history("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 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";
+ {
+ XmlSubNode value(max_histo_length, "value");
+ value.set_inner(std::to_string(options.col<Database::MaxHistoryLength>()));
+ }
+ }
+
+ {
+ 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";
+ {
+ XmlSubNode value(record_history, "value");
+ value.set_name("value");
+ if (options.col<Database::RecordHistory>())
+ value.set_inner("true");
+ else
+ value.set_inner("false");
+ }
+ }
+
+ {
+ XmlSubNode persistent(x, "field");
+ persistent["var"] = "persistent";
+ persistent["type"] = "boolean";
+ persistent["label"] = "Make all channels persistent";
+ persistent["desc"] = "If true, all channels will be persistent";
+ {
+ XmlSubNode value(persistent, "value");
+ value.set_name("value");
+ if (options.col<Database::Persistent>())
+ 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)
@@ -180,31 +182,31 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session,
if (field->get_tag("var") == "max_history_length" &&
value && !value->get_inner().empty())
- options.maxHistoryLength = value->get_inner();
+ options.col<Database::MaxHistoryLength>() = atoi(value->get_inner().data());
else if (field->get_tag("var") == "record_history" &&
value && !value->get_inner().empty())
{
- options.recordHistory = to_bool(value->get_inner());
+ options.col<Database::RecordHistory>() = to_bool(value->get_inner());
Bridge* bridge = biboumi_component.find_user_bridge(owner.bare());
if (bridge)
- bridge->set_record_history(options.recordHistory.value());
+ bridge->set_record_history(options.col<Database::RecordHistory>());
}
+ else if (field->get_tag("var") == "persistent" &&
+ value)
+ options.col<Database::Persistent>() = to_bool(value->get_inner());
}
- options.update();
+ options.save(Database::db);
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,161 +220,143 @@ 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");
- ports["var"] = "ports";
- ports["type"] = "text-multi";
- ports["label"] = "Ports";
- ports["desc"] = "List of ports to try, without TLS. Defaults: 6667.";
- auto vals = utils::split(options.ports.value(), ';', false);
- for (const auto& val: vals)
- {
- XmlNode ports_value("value");
- ports_value.set_inner(val);
- ports.add_child(std::move(ports_value));
- }
- ports.add_child(required);
- x.add_child(std::move(ports));
+ {
+ XmlSubNode ports(x, "field");
+ ports["var"] = "ports";
+ ports["type"] = "text-multi";
+ ports["label"] = "Ports";
+ ports["desc"] = "List of ports to try, without TLS. Defaults: 6667.";
+ for (const auto& val: utils::split(options.col<Database::Ports>(), ';', false))
+ {
+ XmlSubNode ports_value(ports, "value");
+ ports_value.set_inner(val);
+ }
+ }
#ifdef BOTAN_FOUND
- XmlNode tls_ports("field");
- tls_ports["var"] = "tls_ports";
- tls_ports["type"] = "text-multi";
- tls_ports["label"] = "TLS ports";
- tls_ports["desc"] = "List of ports to try, with TLS. Defaults: 6697, 6670.";
- vals = utils::split(options.tlsPorts.value(), ';', false);
- for (const auto& val: vals)
- {
- XmlNode tls_ports_value("value");
- tls_ports_value.set_inner(val);
- tls_ports.add_child(std::move(tls_ports_value));
- }
- tls_ports.add_child(required);
- x.add_child(std::move(tls_ports));
-
- XmlNode verify_cert("field");
- verify_cert["var"] = "verify_cert";
- verify_cert["type"] = "boolean";
- verify_cert["label"] = "Verify certificate";
- verify_cert["desc"] = "Whether or not to abort the connection if the server’s TLS certificate is invalid";
- XmlNode verify_cert_value("value");
- if (options.verifyCert.value())
- verify_cert_value.set_inner("true");
- else
- verify_cert_value.set_inner("false");
- verify_cert.add_child(std::move(verify_cert_value));
- x.add_child(std::move(verify_cert));
-
- XmlNode fingerprint("field");
- fingerprint["var"] = "fingerprint";
- fingerprint["type"] = "text-single";
- fingerprint["label"] = "SHA-1 fingerprint of the TLS certificate to trust.";
- if (!options.trustedFingerprint.value().empty())
- {
- XmlNode fingerprint_value("value");
- fingerprint_value.set_inner(options.trustedFingerprint.value());
- fingerprint.add_child(std::move(fingerprint_value));
- }
- fingerprint.add_child(required);
- x.add_child(std::move(fingerprint));
+ {
+ XmlSubNode tls_ports(x, "field");
+ tls_ports["var"] = "tls_ports";
+ tls_ports["type"] = "text-multi";
+ tls_ports["label"] = "TLS ports";
+ tls_ports["desc"] = "List of ports to try, with TLS. Defaults: 6697, 6670.";
+ for (const auto& val: utils::split(options.col<Database::TlsPorts>(), ';', false))
+ {
+ XmlSubNode tls_ports_value(tls_ports, "value");
+ tls_ports_value.set_inner(val);
+ }
+ }
+
+ {
+ 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";
+ XmlSubNode verify_cert_value(verify_cert, "value");
+ if (options.col<Database::VerifyCert>())
+ verify_cert_value.set_inner("true");
+ else
+ verify_cert_value.set_inner("false");
+ }
+
+ {
+ XmlSubNode fingerprint(x, "field");
+ fingerprint["var"] = "fingerprint";
+ fingerprint["type"] = "text-single";
+ fingerprint["label"] = "SHA-1 fingerprint of the TLS certificate to trust.";
+ if (!options.col<Database::TrustedFingerprint>().empty())
+ {
+ XmlSubNode fingerprint_value(fingerprint, "value");
+ fingerprint_value.set_inner(options.col<Database::TrustedFingerprint>());
+ }
+ }
#endif
-
- XmlNode pass("field");
- pass["var"] = "pass";
- pass["type"] = "text-private";
- pass["label"] = "Server password (to be used in a PASS command when connecting)";
- if (!options.pass.value().empty())
- {
- XmlNode pass_value("value");
- pass_value.set_inner(options.pass.value());
- pass.add_child(std::move(pass_value));
- }
- pass.add_child(required);
- x.add_child(std::move(pass));
-
- XmlNode after_cnt_cmd("field");
- after_cnt_cmd["var"] = "after_connect_command";
- after_cnt_cmd["type"] = "text-single";
- after_cnt_cmd["desc"] = "Custom IRC command sent after the connection is established with the server.";
- after_cnt_cmd["label"] = "After-connection IRC command";
- if (!options.afterConnectionCommand.value().empty())
- {
- XmlNode after_cnt_cmd_value("value");
- after_cnt_cmd_value.set_inner(options.afterConnectionCommand.value());
- after_cnt_cmd.add_child(std::move(after_cnt_cmd_value));
- }
- after_cnt_cmd.add_child(required);
- x.add_child(std::move(after_cnt_cmd));
+ {
+ XmlSubNode pass(x, "field");
+ pass["var"] = "pass";
+ pass["type"] = "text-private";
+ pass["label"] = "Server password";
+ pass["desc"] = "Will be used in a PASS command when connecting";
+ if (!options.col<Database::Pass>().empty())
+ {
+ XmlSubNode pass_value(pass, "value");
+ pass_value.set_inner(options.col<Database::Pass>());
+ }
+ }
+
+ {
+ 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.col<Database::AfterConnectionCommand>().empty())
+ {
+ XmlSubNode after_cnt_cmd_value(after_cnt_cmd, "value");
+ after_cnt_cmd_value.set_inner(options.col<Database::AfterConnectionCommand>());
+ }
+ }
if (Config::get("realname_customization", "true") == "true")
{
- XmlNode username("field");
- username["var"] = "username";
- username["type"] = "text-single";
- username["label"] = "Username";
- if (!options.username.value().empty())
- {
- XmlNode username_value("value");
- username_value.set_inner(options.username.value());
- username.add_child(std::move(username_value));
- }
- username.add_child(required);
- x.add_child(std::move(username));
-
- XmlNode realname("field");
- realname["var"] = "realname";
- realname["type"] = "text-single";
- realname["label"] = "Realname";
- if (!options.realname.value().empty())
- {
- XmlNode realname_value("value");
- realname_value.set_inner(options.realname.value());
- realname.add_child(std::move(realname_value));
- }
- realname.add_child(required);
- x.add_child(std::move(realname));
+ {
+ XmlSubNode username(x, "field");
+ username["var"] = "username";
+ username["type"] = "text-single";
+ username["label"] = "Username";
+ if (!options.col<Database::Username>().empty())
+ {
+ XmlSubNode username_value(username, "value");
+ username_value.set_inner(options.col<Database::Username>());
+ }
+ }
+
+ {
+ XmlSubNode realname(x, "field");
+ realname["var"] = "realname";
+ realname["type"] = "text-single";
+ realname["label"] = "Realname";
+ if (!options.col<Database::Realname>().empty())
+ {
+ XmlSubNode realname_value(realname, "value");
+ realname_value.set_inner(options.col<Database::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");
- encoding_out_value.set_inner(options.encodingOut.value());
- encoding_out.add_child(std::move(encoding_out_value));
- }
- encoding_out.add_child(required);
- x.add_child(std::move(encoding_out));
-
- XmlNode encoding_in("field");
- encoding_in["var"] = "encoding_in";
- encoding_in["type"] = "text-single";
- encoding_in["desc"] = "The encoding used to decode message received from the IRC server.";
- encoding_in["label"] = "In encoding";
- if (!options.encodingIn.value().empty())
+ if (!options.col<Database::EncodingOut>().empty())
{
- XmlNode encoding_in_value("value");
- encoding_in_value.set_inner(options.encodingIn.value());
- encoding_in.add_child(std::move(encoding_in_value));
+ XmlSubNode encoding_out_value(encoding_out, "value");
+ encoding_out_value.set_inner(options.col<Database::EncodingOut>());
}
- encoding_in.add_child(required);
- x.add_child(std::move(encoding_in));
-
-
- command_node.add_child(std::move(x));
+ }
+
+ {
+ 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.col<Database::EncodingIn>().empty())
+ {
+ XmlSubNode encoding_in_value(encoding_in, "value");
+ encoding_in_value.set_inner(options.col<Database::EncodingIn>());
+ }
+ }
}
void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node)
@@ -396,7 +380,7 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
std::string ports;
for (const auto& val: values)
ports += val->get_inner() + ";";
- options.ports = ports;
+ options.col<Database::Ports>() = ports;
}
#ifdef BOTAN_FOUND
@@ -405,31 +389,31 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
std::string ports;
for (const auto& val: values)
ports += val->get_inner() + ";";
- options.tlsPorts = ports;
+ options.col<Database::TlsPorts>() = ports;
}
else if (field->get_tag("var") == "verify_cert" && value
&& !value->get_inner().empty())
{
auto val = to_bool(value->get_inner());
- options.verifyCert = val;
+ options.col<Database::VerifyCert>() = val;
}
else if (field->get_tag("var") == "fingerprint" && value &&
!value->get_inner().empty())
{
- options.trustedFingerprint = value->get_inner();
+ options.col<Database::TrustedFingerprint>() = value->get_inner();
}
#endif // BOTAN_FOUND
else if (field->get_tag("var") == "pass" &&
value && !value->get_inner().empty())
- options.pass = value->get_inner();
+ options.col<Database::Pass>() = value->get_inner();
else if (field->get_tag("var") == "after_connect_command" &&
value && !value->get_inner().empty())
- options.afterConnectionCommand = value->get_inner();
+ options.col<Database::AfterConnectionCommand>() = value->get_inner();
else if (field->get_tag("var") == "username" &&
value && !value->get_inner().empty())
@@ -437,37 +421,34 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
auto username = value->get_inner();
// The username must not contain spaces
std::replace(username.begin(), username.end(), ' ', '_');
- options.username = username;
+ options.col<Database::Username>() = username;
}
else if (field->get_tag("var") == "realname" &&
value && !value->get_inner().empty())
- options.realname = value->get_inner();
+ options.col<Database::Realname>() = value->get_inner();
else if (field->get_tag("var") == "encoding_out" &&
value && !value->get_inner().empty())
- options.encodingOut = value->get_inner();
+ options.col<Database::EncodingOut>() = value->get_inner();
else if (field->get_tag("var") == "encoding_in" &&
value && !value->get_inner().empty())
- options.encodingIn = value->get_inner();
+ options.col<Database::EncodingIn>() = value->get_inner();
}
- options.update();
+ options.save(Database::db);
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();
}
@@ -475,90 +456,165 @@ void ConfigureIrcChannelStep1(XmppComponent&, AdhocSession& session, XmlNode& co
{
const Jid owner(session.get_owner_jid());
const Jid target(session.get_target_jid());
+
+ insert_irc_channel_configuration_form(command_node, owner, target);
+}
+
+void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, const Jid& target)
+{
const Iid iid(target.local, {});
- auto options = Database::get_irc_channel_options_with_server_default(owner.local + "@" + owner.domain,
- iid.get_server(), iid.get_local());
- XmlNode x("jabber:x:data:x");
+ auto options = Database::get_irc_channel_options_with_server_default(requester.local + "@" + requester.domain,
+ iid.get_server(), iid.get_local());
+ XmlSubNode x(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");
- 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())
+ {
+ XmlSubNode record_history(x, "field");
+ record_history["var"] = "record_history";
+ record_history["type"] = "list-single";
+ record_history["label"] = "Record history for this channel";
+ record_history["desc"] = "If unset, the value is the one configured globally";
{
- XmlNode encoding_out_value("value");
- encoding_out_value.set_inner(options.encodingOut.value());
- encoding_out.add_child(std::move(encoding_out_value));
+ // Value selected by default
+ XmlSubNode value(record_history, "value");
+ value.set_inner(options.col<Database::RecordHistoryOptional>().to_string());
}
- encoding_out.add_child(required);
- x.add_child(std::move(encoding_out));
-
- XmlNode encoding_in("field");
- encoding_in["var"] = "encoding_in";
- encoding_in["type"] = "text-single";
- encoding_in["desc"] = "The encoding used to decode message received from the IRC server. Defaults to the server's “in encoding” if unset for the channel";
- encoding_in["label"] = "In encoding";
- if (!options.encodingIn.value().empty())
+ // All three possible values
+ for (const auto& val: {"unset", "true", "false"})
+ {
+ XmlSubNode option(record_history, "option");
+ option["label"] = val;
+ XmlSubNode value(option, "value");
+ value.set_inner(val);
+ }
+ }
+
+ {
+ 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.col<Database::EncodingOut>().empty())
+ {
+ XmlSubNode encoding_out_value(encoding_out, "value");
+ encoding_out_value.set_inner(options.col<Database::EncodingOut>());
+ }
+ }
+
+ {
+ 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.col<Database::EncodingIn>().empty())
+ {
+ XmlSubNode encoding_in_value(encoding_in, "value");
+ encoding_in_value.set_inner(options.col<Database::EncodingIn>());
+ }
+ }
+
+ {
+ 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";
{
- XmlNode encoding_in_value("value");
- encoding_in_value.set_inner(options.encodingIn.value());
- encoding_in.add_child(std::move(encoding_in_value));
+ XmlSubNode value(persistent, "value");
+ value.set_name("value");
+ if (options.col<Database::Persistent>())
+ value.set_inner("true");
+ else
+ value.set_inner("false");
}
- encoding_in.add_child(required);
- x.add_child(std::move(encoding_in));
+ }
+}
+
+void ConfigureIrcChannelStep2(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node)
+{
+ const Jid owner(session.get_owner_jid());
+ const Jid target(session.get_target_jid());
- command_node.add_child(std::move(x));
+ if (handle_irc_channel_configuration_form(xmpp_component, command_node, owner, target))
+ {
+ command_node.delete_all_children();
+ XmlSubNode note(command_node, "note");
+ note["type"] = "info";
+ note.set_inner("Configuration successfully applied.");
+ }
+ else
+ {
+ XmlSubNode error(command_node, ADHOC_NS":error");
+ error["type"] = "modify";
+ XmlSubNode condition(error, STANZA_NS":bad-request");
+ session.terminate();
+ }
}
-void ConfigureIrcChannelStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node)
+bool handle_irc_channel_configuration_form(XmppComponent& xmpp_component, const XmlNode& node, const Jid& requester, const Jid& target)
{
- const XmlNode* x = command_node.get_child("x", "jabber:x:data");
+ const XmlNode* x = node.get_child("x", "jabber:x:data");
if (x)
{
- const Jid owner(session.get_owner_jid());
- const Jid target(session.get_target_jid());
- const Iid iid(target.local, {});
- auto options = Database::get_irc_channel_options(owner.local + "@" + owner.domain,
- iid.get_server(), iid.get_local());
- for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
+ if (x->get_tag("type") == "submit")
{
- const XmlNode* value = field->get_child("value", "jabber:x:data");
+ const Iid iid(target.local, {});
+ auto options = Database::get_irc_channel_options(requester.bare(),
+ iid.get_server(), iid.get_local());
+ for (const XmlNode *field: x->get_children("field", "jabber:x:data"))
+ {
+ const XmlNode *value = field->get_child("value", "jabber:x:data");
- if (field->get_tag("var") == "encoding_out" &&
- value && !value->get_inner().empty())
- options.encodingOut = value->get_inner();
+ if (field->get_tag("var") == "encoding_out" &&
+ value && !value->get_inner().empty())
+ options.col<Database::EncodingOut>() = value->get_inner();
- else if (field->get_tag("var") == "encoding_in" &&
- value && !value->get_inner().empty())
- options.encodingIn = value->get_inner();
- }
+ else if (field->get_tag("var") == "encoding_in" &&
+ value && !value->get_inner().empty())
+ options.col<Database::EncodingIn>() = value->get_inner();
- options.update();
+ else if (field->get_tag("var") == "persistent" &&
+ value)
+ options.col<Database::Persistent>() = to_bool(value->get_inner());
+ else if (field->get_tag("var") == "record_history" &&
+ value && !value->get_inner().empty())
+ {
+ OptionalBool& database_value = options.col<Database::RecordHistoryOptional>();
+ if (value->get_inner() == "true")
+ database_value.set_value(true);
+ else if (value->get_inner() == "false")
+ database_value.set_value(false);
+ else
+ database_value.unset();
+ auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component);
+ Bridge* bridge = biboumi_component.find_user_bridge(requester.bare());
+ if (bridge)
+ {
+ if (database_value.is_set)
+ bridge->set_record_history(database_value.value);
+ else
+ { // It is unset, we need to fetch the Global option, to
+ // know if it’s enabled or not
+ auto g_options = Database::get_global_options(requester.bare());
+ bridge->set_record_history(g_options.col<Database::RecordHistory>());
+ }
+ }
+ }
- command_node.delete_all_children();
- XmlNode note("note");
- note["type"] = "info";
- note.set_inner("Configuration successfully applied.");
- command_node.add_child(std::move(note));
- return;
+ }
+
+ options.save(Database::db);
+ }
+ return true;
}
- XmlNode error(ADHOC_NS":error");
- error["type"] = "modify";
- XmlNode condition(STANZA_NS":bad-request");
- error.add_child(std::move(condition));
- command_node.add_child(std::move(error));
- session.terminate();
+ return false;
}
#endif // USE_DATABASE
@@ -573,33 +629,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 +674,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 +739,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 +756,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 +778,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_adhoc_commands.hpp b/src/xmpp/biboumi_adhoc_commands.hpp
index b5fce61..cb6acb9 100644
--- a/src/xmpp/biboumi_adhoc_commands.hpp
+++ b/src/xmpp/biboumi_adhoc_commands.hpp
@@ -4,6 +4,7 @@
#include <xmpp/adhoc_command.hpp>
#include <xmpp/adhoc_session.hpp>
#include <xmpp/xmpp_stanza.hpp>
+#include <xmpp/jid.hpp>
class XmppComponent;
@@ -17,7 +18,9 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node);
void ConfigureIrcChannelStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, const Jid& target);
void ConfigureIrcChannelStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+bool handle_irc_channel_configuration_form(XmppComponent&, const XmlNode& node, const Jid& requester, const Jid& target);
void DisconnectUserFromServerStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node);
void DisconnectUserFromServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node);
diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp
index d6782e2..32f3968 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,11 +16,8 @@
#include <cstdlib>
-#include <louloulibs.h>
#include <biboumi.h>
-#include <uuid/uuid.h>
-
#ifdef SYSTEMD_FOUND
# include <systemd/sd-daemon.h>
#endif
@@ -45,7 +41,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)
@@ -85,10 +81,8 @@ BiboumiComponent::BiboumiComponent(std::shared_ptr<Poller> poller, const std::st
void BiboumiComponent::shutdown()
{
- for (auto it = this->bridges.begin(); it != this->bridges.end(); ++it)
- {
- it->second->shutdown("Gateway shutdown");
- }
+ for (auto& pair: this->bridges)
+ pair.second->shutdown("Gateway shutdown");
}
void BiboumiComponent::clean()
@@ -137,7 +131,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 +144,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 +156,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 +174,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 +201,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 +284,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 +320,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 +348,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();
}
}
@@ -386,6 +390,11 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
if (this->handle_mam_request(stanza))
stanza_error.disable();
}
+ else if ((query = stanza.get_child("query", MUC_OWNER_NS)))
+ {
+ if (this->handle_room_configuration_form(*query, from, to, id))
+ stanza_error.disable();
+ }
#endif
}
else if (type == "get")
@@ -414,7 +423,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();
@@ -492,6 +506,8 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
rs_info.max = std::atoi(max->get_inner().data());
}
+ if (rs_info.max == -1)
+ rs_info.max = 100;
bridge->send_irc_channel_list_request(iid, id, from, std::move(rs_info));
stanza_error.disable();
}
@@ -516,6 +532,13 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
}
stanza_error.disable();
}
+#ifdef USE_DATABASE
+ else if ((query = stanza.get_child("query", MUC_OWNER_NS)))
+ {
+ if (this->handle_room_configuration_form_request(from, to, id))
+ stanza_error.disable();
+ }
+#endif
}
else if (type == "result")
{
@@ -548,6 +571,16 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
}
}
}
+ else if (type == "error")
+ {
+ stanza_error.disable();
+ const auto it = this->waiting_iq.find(id);
+ if (it != this->waiting_iq.end())
+ {
+ it->second(bridge, stanza);
+ this->waiting_iq.erase(it);
+ }
+ }
}
catch (const IRCNotConnected& ex)
{
@@ -570,13 +603,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);
@@ -600,10 +631,24 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza)
}
}
}
- const auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), -1, start, end);
- for (const db::MucLogLine& line: lines)
+ const XmlNode* set = query->get_child("set", RSM_NS);
+ int limit = -1;
+ if (set)
+ {
+ const XmlNode* max = set->get_child("max", RSM_NS);
+ if (max)
+ limit = std::atoi(max->get_inner().data());
+ }
+ // If the archive is really big, and the client didn’t specify any
+ // limit, we avoid flooding it: we set an arbitrary max limit.
+ if (limit == -1 && start.empty() && end.empty())
+ {
+ limit = 100;
+ }
+ const auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), limit, start, end);
+ for (const Database::MucLogLine& line: lines)
{
- if (!line.nick.value().empty())
+ if (!line.col<Database::Nick>().empty())
this->send_archived_message(line, to.full(), from.full(), query_id);
}
this->send_iq_result_full_jid(id, from.full(), to.full());
@@ -612,42 +657,80 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza)
return false;
}
-void BiboumiComponent::send_archived_message(const db::MucLogLine& log_line, const std::string& from, const std::string& to,
+void BiboumiComponent::send_archived_message(const Database::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();
+ result["id"] = log_line.col<Database::Uuid>();
- 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));
+ delay["stamp"] = utils::to_string(log_line.col<Database::Date>());
- XmlNode submessage("message");
+ XmlSubNode submessage(forwarded, "message");
submessage["xmlns"] = CLIENT_NS;
- submessage["from"] = from + "/" + log_line.nick.value();
+ submessage["from"] = from + "/" + log_line.col<Database::Nick>();
submessage["type"] = "groupchat";
- XmlNode body("body");
- body.set_inner(log_line.body.value());
- submessage.add_child(std::move(body));
+ XmlSubNode body(submessage, "body");
+ body.set_inner(log_line.col<Database::Body>());
+ }
+ this->send_stanza(message);
+}
+
+bool BiboumiComponent::handle_room_configuration_form_request(const std::string& from, const Jid& to, const std::string& id)
+{
+ Iid iid(to.local, {'#', '&'});
- forwarded.add_child(std::move(submessage));
- result.add_child(std::move(forwarded));
- message.add_child(std::move(result));
+ if (iid.type != Iid::Type::Channel)
+ return false;
- this->send_stanza(message);
+ Stanza iq("iq");
+ {
+ iq["from"] = to.full();
+ iq["to"] = from;
+ iq["id"] = id;
+ iq["type"] = "result";
+ XmlSubNode query(iq, "query");
+ query["xmlns"] = MUC_OWNER_NS;
+ Jid requester(from);
+ insert_irc_channel_configuration_form(query, requester, to);
+ }
+ this->send_stanza(iq);
+ return true;
+}
+
+bool BiboumiComponent::handle_room_configuration_form(const XmlNode& query, const std::string &from, const Jid &to, const std::string &id)
+{
+ Iid iid(to.local, {'#', '&'});
+
+ if (iid.type != Iid::Type::Channel)
+ return false;
+
+ Jid requester(from);
+ if (!handle_irc_channel_configuration_form(*this, query, requester, to))
+ return false;
+
+ Stanza iq("iq");
+ iq["type"] = "result";
+ iq["from"] = to.full();
+ iq["to"] = from;
+ iq["id"] = id;
+
+ this->send_stanza(iq);
+
+ return true;
}
#endif
@@ -681,32 +764,31 @@ Bridge* BiboumiComponent::find_user_bridge(const std::string& full_jid)
std::vector<Bridge*> BiboumiComponent::get_bridges() const
{
std::vector<Bridge*> res;
- for (auto it = this->bridges.begin(); it != this->bridges.end(); ++it)
- res.push_back(it->second.get());
+ for (const auto& bridge: this->bridges)
+ res.push_back(bridge.second.get());
return res;
}
void BiboumiComponent::send_self_disco_info(const std::string& id, const std::string& jid_to)
{
Stanza iq("iq");
- iq["type"] = "result";
- iq["id"] = id;
- iq["to"] = jid_to;
- iq["from"] = this->served_hostname;
- XmlNode query("query");
- query["xmlns"] = DISCO_INFO_NS;
- XmlNode identity("identity");
- identity["category"] = "conference";
- identity["type"] = "irc";
- identity["name"] = "Biboumi XMPP-IRC gateway";
- query.add_child(std::move(identity));
- for (const char* ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, 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 +796,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 +864,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)
@@ -789,8 +881,14 @@ void BiboumiComponent::send_ping_request(const std::string& from,
{
log_error("Received a corresponding ping result, but the 'to' from "
"the response mismatches the 'from' of the request");
+ return;
}
- else
+ const std::string type = stanza.get_tag("type");
+ const XmlNode* error = stanza.get_child("error", COMPONENT_NS);
+ // Check if what we receive is considered a valid response. And yes, those errors are valid responses
+ if (type == "result" ||
+ (type == "error" && error && (error->get_child("feature-not-implemented", STANZA_NS) ||
+ error->get_child("service-unavailable", STANZA_NS))))
bridge->send_irc_ping_result({from, bridge}, id);
};
this->waiting_iq[id] = result_cb;
@@ -803,48 +901,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 +946,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..87311f9 100644
--- a/src/xmpp/biboumi_component.hpp
+++ b/src/xmpp/biboumi_component.hpp
@@ -1,7 +1,8 @@
#pragma once
-
+#include <database/database.hpp>
#include <xmpp/xmpp_component.hpp>
+#include <xmpp/jid.hpp>
#include <bridge/bridge.hpp>
@@ -27,7 +28,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 +70,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 +85,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
*/
@@ -97,8 +96,10 @@ public:
#ifdef USE_DATABASE
bool handle_mam_request(const Stanza& stanza);
- void send_archived_message(const db::MucLogLine& log_line, const std::string& from, const std::string& to,
+ void send_archived_message(const Database::MucLogLine& log_line, const std::string& from, const std::string& to,
const std::string& queryid);
+ bool handle_room_configuration_form_request(const std::string& from, const Jid& to, const std::string& id);
+ bool handle_room_configuration_form(const XmlNode& query, const std::string& from, const Jid& to, const std::string& id);
#endif
/**
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..19d1b55
--- /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()));
+ auto 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..b138ed9 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)
{
@@ -111,17 +112,20 @@ void XmppComponent::on_connection_close(const std::string& error)
void XmppComponent::parse_in_buffer(const size_t size)
{
+ // in_buf.size, or size, cannot be bigger than our read-size (4096) so it’s safe
+ // to cast.
+
if (!this->in_buf.empty())
{ // This may happen if the parser could not allocate enough space for
// us. We try to feed it the data that was read into our in_buf
// instead. If this fails again we are in trouble.
- this->parser.feed(this->in_buf.data(), this->in_buf.size(), false);
+ this->parser.feed(this->in_buf.data(), static_cast<int>(this->in_buf.size()), false);
this->in_buf.clear();
}
else
{ // Just tell the parser to parse the data that was placed into the
// buffer it provided to us with GetBuffer
- this->parser.parse(size, false);
+ this->parser.parse(static_cast<int>(size), false);
}
}
@@ -172,12 +176,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 +192,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 (fulljid)
- node["from"] = from;
- else
- node["from"] = from + "@" + this->served_hostname;
- }
- if (!id.empty())
- node["id"] = id;
- node["type"] = "error";
- XmlNode error("error");
- error["type"] = error_type;
- XmlNode inner_error(defined_condition);
- inner_error["xmlns"] = STANZA_NS;
- error.add_child(std::move(inner_error));
- if (!text.empty())
+ {
+ if (!to.empty())
+ node["to"] = to;
+ if (!from.empty())
+ {
+ if (fulljid)
+ node["from"] = from;
+ else
+ node["from"] = from + "@" + this->served_hostname;
+ }
+ if (!id.empty())
+ node["id"] = id;
+ node["type"] = "error";
{
- XmlNode text_node("text");
- text_node["xmlns"] = STANZA_NS;
- text_node.set_inner(text);
- error.add_child(std::move(text_node));
+ 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);
+ }
}
- node.add_child(std::move(error));
+ }
this->send_stanza(node);
}
@@ -264,38 +272,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 +309,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,48 +343,47 @@ 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);
}
-void XmppComponent::send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& xmpp_body, const std::string& jid_to)
+void XmppComponent::send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& xmpp_body, const std::string& jid_to, std::string uuid)
{
Stanza message("message");
message["to"] = jid_to;
@@ -391,17 +392,28 @@ 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));
}
+
+ if (!uuid.empty())
+ {
+ XmlSubNode stanza_id(message, "stanza-id");
+ stanza_id["xmlns"] = STABLE_ID_NS;
+ stanza_id["by"] = muc_name + "@" + this->served_hostname;
+ stanza_id["id"] = std::move(uuid);
+ }
+
this->send_stanza(message);
}
@@ -415,41 +427,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 +474,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 +499,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 +530,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 +563,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 +583,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 +615,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 +640,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..ebe3ec8 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>
@@ -17,6 +17,7 @@
#define MUC_NS "http://jabber.org/protocol/muc"
#define MUC_USER_NS MUC_NS"#user"
#define MUC_ADMIN_NS MUC_NS"#admin"
+#define MUC_OWNER_NS MUC_NS"#owner"
#define DISCO_NS "http://jabber.org/protocol/disco"
#define DISCO_ITEMS_NS DISCO_NS"#items"
#define DISCO_INFO_NS DISCO_NS"#info"
@@ -27,12 +28,13 @@
#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"
#define RSM_NS "http://jabber.org/protocol/rsm"
#define MUC_TRAFFIC_NS "http://jabber.org/protocol/muc#traffic"
+#define STABLE_ID_NS "urn:xmpp:sid:0"
/**
* An XMPP component, communicating with an XMPP server using the protocole
@@ -40,10 +42,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 +93,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
*/
@@ -134,7 +136,8 @@ public:
/**
* Send a (non-private) message to the MUC
*/
- void send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& body, const std::string& jid_to);
+ void send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& body, const std::string& jid_to,
+ std::string uuid);
/**
* Send a message, with a <delay/> element, part of a MUC history
*/
@@ -143,7 +146,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 +182,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..ec42f9a 100644
--- a/louloulibs/xmpp/xmpp_parser.hpp
+++ b/src/xmpp/xmpp_parser.hpp
@@ -106,7 +106,7 @@ private:
/**
* Expat structure.
*/
- XML_Parser parser;
+ XML_Parser parser{};
/**
* The current depth in the XML document
*/
diff --git a/louloulibs/xmpp/xmpp_stanza.cpp b/src/xmpp/xmpp_stanza.cpp
index ac6ce9b..435f333 100644
--- a/louloulibs/xmpp/xmpp_stanza.cpp
+++ b/src/xmpp/xmpp_stanza.cpp
@@ -7,7 +7,7 @@
#include <iostream>
#include <sstream>
-#include <string.h>
+#include <cstring>
std::string xml_escape(const std::string& data)
{
@@ -52,7 +52,7 @@ XmlNode::XmlNode(const std::string& name, XmlNode* parent):
parent(parent)
{
// split the namespace and the name
- auto n = name.rfind(":");
+ auto n = name.rfind(':');
if (n == std::string::npos)
this->name = name;
else
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/database.cpp b/tests/database.cpp
index 4e2be14..f49220a 100644
--- a/tests/database.cpp
+++ b/tests/database.cpp
@@ -8,24 +8,22 @@ TEST_CASE("Database")
{
#ifdef USE_DATABASE
Database::open(":memory:");
- Database::set_verbose(false);
SECTION("Basic retrieve and update")
{
auto o = Database::get_irc_server_options("zouzou@example.com", "irc.example.com");
- o.update();
+ o.save(Database::db);
auto a = Database::get_irc_server_options("zouzou@example.com", "irc.example.com");
auto b = Database::get_irc_server_options("moumou@example.com", "irc.example.com");
// b does not yet exist in the db, the object is created but not yet
// inserted
- CHECK(1 == Database::count<db::IrcServerOptions>());
+ CHECK(1 == Database::count(Database::irc_server_options));
- b.update();
- CHECK(2 == Database::count<db::IrcServerOptions>());
+ b.save(Database::db);
+ CHECK(2 == Database::count(Database::irc_server_options));
- CHECK(b.pass == "");
- CHECK(b.pass.value() == "");
+ CHECK(b.col<Database::Pass>() == "");
}
SECTION("channel options")
@@ -33,16 +31,20 @@ TEST_CASE("Database")
Config::set("db_name", ":memory:");
auto o = Database::get_irc_channel_options("zouzou@example.com", "irc.example.com", "#foo");
- CHECK(o.encodingIn == "");
- o.encodingIn = "ISO-8859-1";
- o.update();
+ CHECK(o.col<Database::EncodingIn>() == "");
+ o.col<Database::EncodingIn>() = "ISO-8859-1";
+ CHECK(o.col<Database::RecordHistoryOptional>().is_set == false);
+ o.col<Database::RecordHistoryOptional>().set_value(false);
+ o.save(Database::db);
auto b = Database::get_irc_channel_options("zouzou@example.com", "irc.example.com", "#foo");
- CHECK(o.encodingIn == "ISO-8859-1");
+ CHECK(o.col<Database::EncodingIn>() == "ISO-8859-1");
+ CHECK(o.col<Database::RecordHistoryOptional>().is_set == true);
+ CHECK(o.col<Database::RecordHistoryOptional>().value == false);
}
SECTION("Channel options with server default")
{
- const std::string owner{"zouzou@example.com"};
+ const std::string owner{"CACA@example.com"};
const std::string server{"irc.example.com"};
const std::string chan1{"#foo"};
@@ -51,43 +53,43 @@ TEST_CASE("Database")
GIVEN("An option defined for the channel but not the server")
{
- c.encodingIn = "channelEncoding";
- c.update();
+ c.col<Database::EncodingIn>() = "channelEncoding";
+ c.save(Database::db);
WHEN("we fetch that option")
{
auto r = Database::get_irc_channel_options_with_server_default(owner, server, chan1);
THEN("we get the channel option")
- CHECK(r.encodingIn == "channelEncoding");
+ CHECK(r.col<Database::EncodingIn>() == "channelEncoding");
}
}
GIVEN("An option defined for the server but not the channel")
{
- s.encodingIn = "serverEncoding";
- s.update();
+ s.col<Database::EncodingIn>() = "serverEncoding";
+ s.save(Database::db);
WHEN("we fetch that option")
{
auto r = Database::get_irc_channel_options_with_server_default(owner, server, chan1);
THEN("we get the server option")
- CHECK(r.encodingIn == "serverEncoding");
+ CHECK(r.col<Database::EncodingIn>() == "serverEncoding");
}
}
GIVEN("An option defined for both the server and the channel")
{
- s.encodingIn = "serverEncoding";
- s.update();
- c.encodingIn = "channelEncoding";
- c.update();
+ s.col<Database::EncodingIn>() = "serverEncoding";
+ s.save(Database::db);
+ c.col<Database::EncodingIn>() = "channelEncoding";
+ c.save(Database::db);
WHEN("we fetch that option")
{
auto r = Database::get_irc_channel_options_with_server_default(owner, server, chan1);
THEN("we get the channel option")
- CHECK(r.encodingIn == "channelEncoding");
+ CHECK(r.col<Database::EncodingIn>() == "channelEncoding");
}
WHEN("we fetch that option, with no channel specified")
{
auto r = Database::get_irc_channel_options_with_server_default(owner, server, "");
THEN("we get the server option")
- CHECK(r.encodingIn == "serverEncoding");
+ CHECK(r.col<Database::EncodingIn>() == "serverEncoding");
}
}
}
diff --git a/tests/end_to_end/__main__.py b/tests/end_to_end/__main__.py
index 7658d92..580f8e4 100644
--- a/tests/end_to_end/__main__.py
+++ b/tests/end_to_end/__main__.py
@@ -1,12 +1,14 @@
#!/usr/bin/env python3
import collections
+import lxml.etree
+import datetime
import slixmpp
import asyncio
import logging
import signal
import atexit
-import lxml.etree
+import time
import sys
import io
import os
@@ -95,7 +97,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()
@@ -113,6 +119,7 @@ def match(stanza, xpath):
tree = lxml.etree.parse(io.StringIO(str(stanza)))
matched = tree.xpath(xpath, namespaces={'re': 'http://exslt.org/regular-expressions',
'muc_user': 'http://jabber.org/protocol/muc#user',
+ 'muc_owner': 'http://jabber.org/protocol/muc#owner',
'muc': 'http://jabber.org/protocol/muc',
'disco_info': 'http://jabber.org/protocol/disco#info',
'muc_traffic': 'http://jabber.org/protocol/muc#traffic',
@@ -120,22 +127,29 @@ 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',
'rsm': 'http://jabber.org/protocol/rsm',
'carbon': 'urn:xmpp:carbons:2',
'hints': 'urn:xmpp:hints',
- 'stanza': 'urn:ietf:params:xml:ns:xmpp-stanzas'})
+ 'stanza': 'urn:ietf:params:xml:ns:xmpp-stanzas',
+ 'stable_id': 'urn:xmpp:sid:0'})
return matched
def check_xpath(xpaths, xmpp, after, stanza):
for xpath in xpaths:
+ expected = True
+ real_xpath = xpath
+ # We can check that a stanza DOESN’T match, by adding a ! before it.
+ if xpath.startswith('!'):
+ expected = False
+ xpath = xpath[1:]
matched = match(stanza, xpath)
- if not matched:
- raise StanzaError("Received stanza “%s” did not match expected xpath “%s”" % (stanza, xpath))
+ if (expected and not matched) or (not expected and matched):
+ raise StanzaError("Received stanza “%s” did not match expected xpath “%s”" % (stanza, real_xpath))
if after:
if isinstance(after, collections.Iterable):
for af in after:
@@ -261,6 +275,14 @@ 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_current_timestamp_plus_delta(key, delta, message, xmpp):
+ now_plus_delta = datetime.datetime.utcnow() + delta
+ xmpp.saved_values[key] = now_plus_delta.strftime("%FT%T.967Z")
+
+def sleep_for(duration, xmpp, biboumi):
+ time.sleep(duration)
+ 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 +297,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("%s" % (message,))
- asyncio.get_event_loop().call_soon(xmpp.run_scenario)
-
class BiboumiTest:
"""
@@ -345,7 +363,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 +374,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 +403,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 +418,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,18 +481,39 @@ 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_middle_sequence(irc_host, jid):
+ 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: \*\*\* You are exempt from flood limits$' % irc_host)),
)
def connection_sequence(irc_host, jid):
- return connection_begin_sequence(irc_host, jid) + connection_end_sequence(irc_host, jid)
+ return connection_begin_sequence(irc_host, jid) +\
+ connection_middle_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_middle_sequence(irc_host, jid) +\
+ connection_end_sequence(irc_host, jid)
def extract_attribute(xpath, name, stanza):
matched = match(stanza, xpath)
return matched[0].get(name)
+def chan_name_from_jid(jid):
+ return jid[1:jid.find('%')]
+
+
+def extract_text(xpath, stanza):
+ matched = match(stanza, xpath)
+ return matched[0].text
def save_value(name, func, stanza, xmpp):
xmpp.saved_values[name] = func(stanza)
@@ -471,6 +537,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(),
@@ -485,12 +565,50 @@ if __name__ == '__main__':
),
partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
]),
+ Scenario("multiple_channels_join",
+ [
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' />"),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#baz%{irc_server_one}/{nick_one}'> <x xmlns='http://jabber.org/protocol/muc'><password>SECRET</password></x></presence>"),
+
+ 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(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']/subject[not(text())]"),
+
+ partial(expect_stanza,
+ "/message/body[text()='Mode #baz [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#baz%{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='#baz%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+ ]),
Scenario("virtual_channel",
[
handshake_sequence(),
partial(send_stanza,
"<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
connection_begin_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ connection_middle_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+
partial(expect_stanza,
("/presence[@to='{jid_one}/{resource_one}'][@from='%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",
"/presence/muc_user:x/muc_user:status[@code='110']")
@@ -502,12 +620,30 @@ 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(),
partial(send_stanza,
"<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
connection_begin_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ connection_middle_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+
partial(expect_stanza,
("/presence[@to='{jid_one}/{resource_one}'][@from='%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",
"/presence/muc_user:x/muc_user:status[@code='110']")
@@ -534,8 +670,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 +682,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 +693,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(),
@@ -598,6 +772,23 @@ if __name__ == '__main__':
),
partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/subject[text()='TOPIC TEST']"),
]),
+ Scenario("multiline_topic",
+ [
+ handshake_sequence(),
+ # 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"),
+ partial(expect_stanza, "/presence"),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # User tries to set a multiline topic
+ partial(send_stanza,
+ "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><subject>FIRST LINE\nSECOND LINE.</subject></message>"),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[text()='FIRST LINE SECOND LINE.']"),
+ ]),
Scenario("channel_basic_join_on_fixed_irc_server",
[
handshake_sequence(),
@@ -641,8 +832,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(),
@@ -676,11 +865,34 @@ if __name__ == '__main__':
"/iq/commands:command/commands:actions/commands:next",
),
after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='hello']", "sessionid"))
-
),
partial(send_stanza, "<iq type='set' id='hello-command2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='name'><value>COUCOU</value></field></x></command></iq>"),
partial(expect_stanza, "/iq[@type='result']/commands:command[@node='hello'][@status='completed']/commands:note[@type='info'][text()='Hello COUCOU!']")
]),
+ Scenario("execute_incomplete_hello_adhoc_command",
+ [
+ handshake_sequence(),
+ partial(send_stanza, "<iq type='set' id='hello-command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' action='execute' /></iq>"),
+ partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='hello'][@sessionid][@status='executing']",
+ "/iq/commands:command/commands:actions/commands:next",
+ ),
+ after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='hello']", "sessionid"))
+ ),
+ partial(send_stanza, "<iq type='set' id='hello-command2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'></x></command></iq>"),
+ partial(expect_stanza, "/iq[@type='error']")
+ ]),
+ Scenario("execute_ping_adhoc_command",
+ [
+ handshake_sequence(),
+ partial(send_stanza, "<iq type='set' id='ping-command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='ping' action='execute' /></iq>"),
+ partial(expect_stanza, "/iq[@type='result']/commands:command[@node='ping'][@status='completed']/commands:note[@type='info'][text()='Pong']")
+ ]),
+ Scenario("execute_reload_adhoc_command",
+ [
+ handshake_sequence(),
+ partial(send_stanza, "<iq type='set' id='ping-command1' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='reload' action='execute' /></iq>"),
+ partial(expect_stanza, "/iq[@type='result']/commands:command[@node='reload'][@status='completed']/commands:note[@type='info'][text()='Configuration reloaded.']")
+ ]),
Scenario("execute_forbidden_adhoc_command",
[
handshake_sequence(),
@@ -692,7 +904,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 +921,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 +1039,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 +1048,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 +1086,46 @@ 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("notices",
+ [
+ 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"),
+ partial(expect_stanza, "/presence"),
+ partial(expect_stanza, "/message"),
+
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='{irc_server_one}' type='chat'><body>NOTICE {nick_one} :[#foo] Hello in a notice.</body></message>"),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='[notice] [#foo] Hello in a notice.']"),
+ ]),
Scenario("channel_messages",
[
handshake_sequence(),
@@ -901,6 +1219,72 @@ if __name__ == '__main__':
),
partial(expect_stanza, "/message[@from='#biboumi\\40louiz.org\\3a80%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
]),
+ Scenario("self_ping_with_error",
+ [
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # Send a ping to ourself
+ partial(send_stanza,
+ "<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ # We receive our own ping request,
+ partial(expect_stanza,
+ "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to='{jid_one}/{resource_one}'][@id='gnip_tsrif']"),
+ # Respond to the request with an error
+ partial(send_stanza,
+ "<iq from='{jid_one}/{resource_one}' id='gnip_tsrif' to='{lower_nick_one}%{irc_server_one}' type='error'><error type='cancel'><feature-not-implemented xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error></iq>"),
+ partial(expect_stanza,
+ "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"),
+
+ # Send a ping to ourself
+ partial(send_stanza,
+ "<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ # We receive our own ping request,
+ partial(expect_stanza,
+ "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to='{jid_one}/{resource_one}'][@id='gnip_tsrif']"),
+ # Respond to the request with an error
+ partial(send_stanza,
+ "<iq from='{jid_one}/{resource_one}' id='gnip_tsrif' to='{lower_nick_one}%{irc_server_one}' type='error'><error type='cancel'><service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error></iq>"),
+ partial(expect_stanza,
+ "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"),
+ ]),
+ Scenario("self_ping_not_in_muc",
+ [
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # Send a ping to ourself, in a muc where we’re not
+ partial(send_stanza,
+ "<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#nil%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ # Immediately receive an error
+ partial(expect_stanza,
+ "/iq[@from='#nil%{irc_server_one}/{nick_one}'][@type='error'][@to='{jid_one}/{resource_one}'][@id='first_ping']/error/stanza:not-allowed"),
+
+ # Send a ping to ourself, in a muc where we are, but not this resource
+ partial(send_stanza,
+ "<iq type='get' from='{jid_one}/{resource_two}' id='first_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ # Immediately receive an error
+ partial(expect_stanza,
+ "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='error'][@to='{jid_one}/{resource_two}'][@id='first_ping']/error/stanza:not-allowed"),
+ ]),
Scenario("self_ping_on_real_channel",
[
handshake_sequence(),
@@ -960,6 +1344,31 @@ if __name__ == '__main__':
"/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_two}'][@id='third_ping']"),
]),
+ Scenario("self_ping_fixed_server", [
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{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@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo@{biboumi_host}'][@type='groupchat']/subject[not(text())]"),
+
+ # Send a ping to ourself
+ partial(send_stanza,
+ "<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#foo@{biboumi_host}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"),
+ # We receive our own ping request,
+ partial(expect_stanza,
+ "/iq[@from='{lower_nick_one}@{biboumi_host}'][@type='get'][@to='{jid_one}/{resource_one}'][@id='gnip_tsrif']"),
+ # Respond to the request
+ partial(send_stanza,
+ "<iq type='result' to='{lower_nick_one}@{biboumi_host}' id='gnip_tsrif' from='{jid_one}/{resource_one}'/>"),
+ partial(expect_stanza,
+ "/iq[@from='#foo@{biboumi_host}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"),
+ ], conf="fixed_server"),
Scenario("simple_kick",
[
handshake_sequence(),
@@ -983,10 +1392,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 +1407,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 +1516,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 +1602,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 +1614,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,
@@ -1216,13 +1693,16 @@ if __name__ == '__main__':
# Send two channel messages
partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']"),
+ partial(expect_stanza,
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']",
+ "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]",)
+ ),
partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 2</body></message>"),
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 +1718,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,16 +1731,87 @@ 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>"""),
partial(expect_stanza,
"/iq[@type='result'][@id='id3'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+
+ # Retrieve a limited archive
+ partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id4'><query xmlns='urn:xmpp:mam:2' queryid='qid4'><set xmlns='http://jabber.org/protocol/rsm'><max>1</max></set></query></iq>"),
+
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid4']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid4']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 2']")
+ ),
+
+ partial(expect_stanza,
+ "/iq[@type='result'][@id='id4'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+
]),
+ Scenario("mam_with_timestamps",
+ [
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # Send two channel messages
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"),
+ partial(expect_stanza,
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']",
+ "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]",)
+ ),
+
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 2</body></message>"),
+ # Record the current time
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou 2']",
+ after = partial(save_current_timestamp_plus_delta, "first_timestamp", datetime.timedelta(seconds=1))),
+
+ # Wait two seconds before sending two new messages
+ partial(sleep_for, 2),
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 3</body></message>"),
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 4</body></message>"),
+ partial(expect_stanza, "/message[@type='groupchat']/body[text()='coucou 3']"),
+ partial(expect_stanza, "/message[@type='groupchat']/body[text()='coucou 4']",
+ after = partial(save_current_timestamp_plus_delta, "second_timestamp", datetime.timedelta(seconds=1))),
+
+ # Retrieve the archive, after our saved datetime
+ partial(send_stanza, """<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id8'>
+ <query xmlns='urn:xmpp:mam:2' queryid='qid16'>
+ <x type='submit' xmlns='jabber:x:data'>
+ <field var='FORM_TYPE' xmlns='jabber:x:data'><value xmlns='jabber:x:data'>urn:xmpp:mam:2</value></field>
+ <field var='start' xmlns='jabber:x:data'><value xmlns='jabber:x:data'>{first_timestamp}</value></field>
+ <field var='end' xmlns='jabber:x:data'><value xmlns='jabber:x:data'>{second_timestamp}</value></field>
+ </x>
+ </query>
+ </iq>"""),
+
+
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid16']/forward:forwarded/delay:delay",
+ "/message/mam:result/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 3']")
+ ),
+
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid16']/forward:forwarded/delay:delay",
+ "/message/mam:result/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 4']")
+ ),
+
+ partial(expect_stanza,
+ "/iq[@type='result'][@id='id8'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+ ]),
Scenario("mam_on_fixed_server",
[
handshake_sequence(),
@@ -1283,7 +1834,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",
@@ -1294,6 +1845,51 @@ if __name__ == '__main__':
"/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo@{biboumi_host}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 2']")
),
], conf="fixed_server"),
+ Scenario("default_mam_limit",
+ [
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]",
+ after = partial(save_value, "counter", lambda x: 0)),
+ ] + [
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>{counter}</body></message>"),
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='{counter}']",
+ after = partial(save_value, "counter", lambda stanza: str(1 + int(extract_text("/message/body", stanza))))
+ ),
+ ] * 150 + [
+ # Retrieve the archive, without any restriction
+ 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>"),
+ # Since we should only receive the last 100 messages from the archive,
+ # it should start with message "50"
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='50']")
+ ),
+ ] + [
+ # followed by 98 more messages
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body")
+ ),
+ ] * 98 + [
+ # and finally the message "149"
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='149']")
+ ),
+ partial(expect_stanza,
+ "/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+
+ ]),
Scenario("channel_history_on_fixed_server",
[
handshake_sequence(),
@@ -1368,7 +1964,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 +1975,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 +1982,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 +1993,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 +2004,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 +2011,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,24 +2018,27 @@ 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"),
+ # Ask for 0 item
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>"),
+
+ # Get 0 item
partial(expect_stanza, (
"/iq[@type='result']/disco_items:query",
)),
- partial(log_message, "Request with max=2"),
+ # Ask for 2 (of 3) items We don’t have the count,
+ # because biboumi doesn’t have the complete list when
+ # it sends us the 2 items
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",
"/iq/disco_items:query/disco_items:item[@jid='#bar%{irc_server_one}']",
"/iq/disco_items:query/disco_items:item[@jid='#coucou%{irc_server_one}']",
"/iq/disco_items:query/rsm:set/rsm:first[text()='#bar%{irc_server_one}'][@index='0']",
- "/iq/disco_items:query/rsm:set/rsm:last[text()='#coucou%{irc_server_one}']",
- "/iq/disco_items:query/rsm:set/rsm:count[text()='3']"
+ "/iq/disco_items:query/rsm:set/rsm:last[text()='#coucou%{irc_server_one}']"
)),
- partial(log_message, "Request with max=12"),
+ # Ask for 12 (of 3) items. We get the whole list, and thus we have the count included.
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 +2050,10 @@ if __name__ == '__main__':
"/iq/disco_items:query/rsm:set/rsm:count[text()='3']"
)),
- partial(log_message, "Request with max=1 after=#bar"),
+ # Ask for 1 item, AFTER the first item (so,
+ # the second). Since we don’t invalidate the cache
+ # with this request, we should have the count
+ # included.
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,21 +2063,57 @@ 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>"),
+ # Ask for 1 item, AFTER the second item (so,
+ # the third).
+ 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>#coucou%{irc_server_one}</after><max>1</max></set></query></iq>"),
partial(expect_stanza, (
"/iq[@type='result']/disco_items:query",
- "/iq/disco_items:query/disco_items:item[@jid='#coucou%{irc_server_one}']",
- "/iq/disco_items:query/rsm:set/rsm:first[text()='#coucou%{irc_server_one}'][@index='1']",
- "/iq/disco_items:query/rsm:set/rsm:last[text()='#coucou%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#foo%{irc_server_one}']",
+ "/iq/disco_items:query/rsm:set/rsm:first[text()='#foo%{irc_server_one}'][@index='2']",
+ "/iq/disco_items:query/rsm:set/rsm:last[text()='#foo%{irc_server_one}']",
"/iq/disco_items:query/rsm:set/rsm:count[text()='3']"
- ))
+ )),
+
+ # Ask for 1 item, AFTER the third item (so,
+ # the fourth). Since it doesn't exist, we get 0 item
+ 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>#foo%{irc_server_one}</after><max>1</max></set></query></iq>"),
+ partial(expect_stanza, (
+ "/iq[@type='result']/disco_items:query",
+ "/iq/disco_items:query/rsm:set/rsm:count[text()='3']"
+ )),
+ ]),
+ Scenario("default_channel_list_limit",
+ [
+ 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"),
+ partial(expect_stanza, "/presence"),
+ partial(expect_stanza, "/message",
+ after = partial(save_value, "counter", lambda x: 0)),
+ ] + [
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#{counter}%{irc_server_one}/{nick_one}' />"),
+ partial(expect_stanza, "/message"),
+ partial(expect_stanza, "/presence",
+ after = partial(save_value, "counter", lambda stanza: str(1 + int(chan_name_from_jid(extract_attribute("/presence", "from", stanza)))))),
+ partial(expect_stanza, "/message")
+ ] * 110 + [
+ 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>"),
+ # charybdis sends the list in alphabetic order, so #foo is the last, and #99 is after #120
+ partial(expect_stanza, ("/iq/disco_items:query/disco_items:item[@jid='#0%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#1%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#109%{irc_server_one}']",
+ "/iq/disco_items:query/disco_items:item[@jid='#9%{irc_server_one}']",
+ "!/iq/disco_items:query/disco_items:item[@jid='#foo%{irc_server_one}']",
+ "!/iq/disco_items:query/disco_items:item[@jid='#99%{irc_server_one}']",
+ "!/iq/disco_items:query/disco_items:item[@jid='#90%{irc_server_one}']")),
]),
Scenario("complete_channel_list_with_pages_of_3",
[
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 +2175,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 +2185,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 +2214,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 +2241,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 +2296,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']",
)),
]),
@@ -1663,6 +2324,8 @@ if __name__ == '__main__':
partial(send_stanza,
"<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
connection_begin_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ connection_middle_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+
partial(expect_stanza,
("/presence[@to='{jid_one}/{resource_one}'][@from='%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",
"/presence/muc_user:x/muc_user:status[@code='110']")
@@ -1680,7 +2343,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 +2360,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)']"),
@@ -1732,6 +2392,7 @@ if __name__ == '__main__':
"/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Edit the form, to configure your global settings for the component.']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='max_history_length']/dataform:value[text()='42']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='record_history']/dataform:value[text()='false']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='persistent']/dataform:value[text()='false']",
"/iq/commands:command/commands:actions/commands:next",
),
after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
@@ -1756,7 +2417,7 @@ if __name__ == '__main__':
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='username']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='realname']",
"/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']/dataform:value[text()='ISO-8859-1']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_out']",
"/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 +2460,126 @@ 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']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='list-single'][@var='record_history']/dataform:value[text()='unset']",
+ ),
+ 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>"
+ "<field var='record_history'><value>true</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/dataform:x[@type='form']/dataform:field[@type='list-single'][@var='record_history']/dataform:value[text()='true']",
+ "/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_xep0045",
+ [
+ handshake_sequence(),
+ partial(send_stanza, "<iq type='get' id='id1' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"),
+ partial(expect_stanza, ("/iq[@type='result']/muc_owner:query",
+ "/iq/muc_owner:query/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']",
+ "/iq/muc_owner:query/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_out']",
+ ),
+ ),
+ partial(send_stanza, "<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'>"
+ "<query xmlns='http://jabber.org/protocol/muc#owner'>"
+ "<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></query></iq>"),
+ partial(expect_stanza, "/iq[@type='result']"),
+ partial(send_stanza, "<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><query xmlns='http://jabber.org/protocol/muc#owner'> <x xmlns='jabber:x:data' type='cancel'/></query></iq>"),
+ partial(expect_stanza, "/iq[@type='result']"),
+ ]),
+ 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_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 +2594,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,9 +2623,36 @@ 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'),
+ Scenario("leave_unjoined_chan",
+ [
+ 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"),
+ partial(expect_stanza, "/presence"),
+ partial(expect_stanza, "/message"),
+
+ partial(send_stanza, "<presence from='{jid_two}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_begin_sequence("irc.localhost", '{jid_two}/{resource_two}'),
+
+ partial(expect_stanza, "/message[@to='{jid_two}/{resource_two}'][@type='chat']/body[text()='irc.localhost: {nick_one}: Nickname is already in use.']"),
+ partial(expect_stanza, "/presence[@type='error']/error[@type='cancel'][@code='409']/stanza:conflict"),
+ partial(send_stanza, "<presence from='{jid_two}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />")
+ ])
)
-
failures = 0
scenar_list = sys.argv[1:]
@@ -1872,7 +2669,8 @@ 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)))
+ checks = len([s for s in scenarios if s.name in scenar_list]) if scenar_list else len(scenarios)
+ print("Running %s checks for biboumi." % checks)
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..ccfbd90 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";
@@ -221,6 +221,7 @@ auth {
auth {
user = "*@*";
class = "users";
+ flags = flood_exempt;
};
/* privset {} blocks MUST be specified before anything that uses them. That
@@ -352,8 +353,8 @@ channel {
use_knock = yes;
knock_delay = 5 minutes;
knock_delay_channel = 1 minute;
- max_chans_per_user = 15;
- max_chans_per_user_large = 60;
+ max_chans_per_user = 140;
+ max_chans_per_user_large = 200;
max_bans = 100;
max_bans_large = 500;
default_split_user_count = 0;
@@ -497,7 +498,7 @@ general {
reject_after_count = 3;
reject_duration = 5 minutes;
throttle_duration = 60;
- throttle_count = 4;
+ throttle_count = 8888;
max_ratelimit_tokens = 30;
away_interval = 30;
certfp_method = sha1;
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/network.cpp b/tests/network.cpp
new file mode 100644
index 0000000..33cf023
--- /dev/null
+++ b/tests/network.cpp
@@ -0,0 +1,44 @@
+#include "catch.hpp"
+#include <network/tls_policy.hpp>
+
+#ifdef BOTAN_FOUND
+TEST_CASE("tls_policy")
+{
+ BiboumiTLSPolicy policy;
+ const auto default_minimum_signature_strength = policy.minimum_signature_strength();
+ const auto default_session_ticket_lifetime = policy.session_ticket_lifetime();
+ const auto default_minimum_rsa_bits = policy.minimum_rsa_bits();
+
+ policy.load("does not exist");
+ WHEN("we fail to load the file")
+ {
+ THEN("all values are the default ones")
+ {
+ CHECK(policy.minimum_signature_strength() == default_minimum_signature_strength);
+ CHECK(policy.minimum_rsa_bits() == default_minimum_rsa_bits);
+ }
+ AND_WHEN("we load a valid first file")
+ {
+ std::istringstream iss("minimum_signature_strength = 128\nminimum_rsa_bits=12\n");
+ policy.load(iss);
+ THEN("the specified values are updated, and the rest is still the default")
+ {
+ CHECK(policy.minimum_signature_strength() == 128);
+ CHECK(policy.minimum_rsa_bits() == 12);
+ CHECK(policy.session_ticket_lifetime() == default_session_ticket_lifetime);
+ }
+ AND_WHEN("we load a second file")
+ {
+ std::istringstream iss("minimum_signature_strength = 15");
+ policy.load(iss);
+ THEN("the specified values are updated, and the rest is untouched")
+ {
+ CHECK(policy.minimum_signature_strength() == 15);
+ CHECK(policy.minimum_rsa_bits() == 12);
+ CHECK(policy.session_ticket_lifetime() == default_session_ticket_lifetime);
+ }
+ }
+ }
+ }
+}
+#endif
diff --git a/tests/utils.cpp b/tests/utils.cpp
index 48951da..c5ef7e7 100644
--- a/tests/utils.cpp
+++ b/tests/utils.cpp
@@ -8,6 +8,9 @@
#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>
+#include <utils/dirname.hpp>
using namespace std::string_literals;
@@ -140,3 +143,31 @@ 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());
+}
+
+TEST_CASE("dirname")
+{
+ CHECK(utils::dirname("/") == "/");
+ CHECK(utils::dirname("coucou.txt") == "./");
+ CHECK(utils::dirname("../coucou.txt") == "../");
+ CHECK(utils::dirname("/etc/biboumi/coucou.txt") == "/etc/biboumi/");
+ CHECK(utils::dirname("..") == "..");
+ CHECK(utils::dirname("../") == "../");
+ CHECK(utils::dirname(".") == ".");
+ CHECK(utils::dirname("./") == "./");
+}
diff --git a/tests/xmpp.cpp b/tests/xmpp.cpp
index 37d2b54..14c51da 100644
--- a/tests/xmpp.cpp
+++ b/tests/xmpp.cpp
@@ -30,20 +30,21 @@ TEST_CASE("Test basic XML parsing")
// And do the same checks on moved-constructed stanza
Stanza moved(std::move(copy));
});
- xml.feed(doc.data(), doc.size(), true);
+ CHECK(doc.size() <= std::numeric_limits<int>::max());
+ xml.feed(doc.data(), static_cast<int>(doc.size()), true);
const std::string doc2 = "<stream xmlns='s'><stanza>coucou\r\n\a</stanza></stream>";
xml.add_stanza_callback([](const Stanza& stanza)
{
CHECK(stanza.get_inner() == "coucou\r\n");
});
-
- xml.feed(doc2.data(), doc.size(), true);
+ CHECK(doc.size() <= std::numeric_limits<int>::max());
+ xml.feed(doc2.data(), static_cast<int>(doc.size()), true);
}
TEST_CASE("XML escape")
{
- const std::string unescaped = "'coucou'<cc>/&\"gaga\"";
+ const std::string unescaped = R"('coucou'<cc>/&"gaga")";
CHECK(xml_escape(unescaped) == "&apos;coucou&apos;&lt;cc&gt;/&amp;&quot;gaga&quot;");
}
@@ -52,3 +53,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());
+}