diff options
160 files changed, 5853 insertions, 5038 deletions
diff --git a/.#.clang-format b/.#.clang-format new file mode 120000 index 0000000..d3f6657 --- /dev/null +++ b/.#.clang-format @@ -0,0 +1 @@ +louiz@abricot.3560:1477183697
\ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8807fbd..9f3ce54 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,6 +2,7 @@ stages: - build # Build in various conf, keeps the artifacts - test # Use the build artifacts to run the tests - packaging # Publish some packages (rpm, deb…) + - deploy # Deploy things like the web doc - external # Interact with some external service (coverity…) before_script: @@ -17,12 +18,22 @@ variables: LIBIDN: "-DWITH_LIBIDN=1" SQLITE3: "-DWITH_SQLITE3=1" POSTGRESQL: "-WITH_POSTGRESQL=1" + CXX_FLAGS: "-Werror -Wno-psabi" # ## Build jobs # -.template:basic_build: &basic_build +.sources_changed: + only: + changes: + - "CMakeLists.txt" + - ".gitlab-ci.yml" + - "src/**/*" + - "tests/**/*" + +.basic_build: + extends: .sources_changed stage: build tags: - docker @@ -30,170 +41,149 @@ variables: - "echo Running cmake with the following parameters: -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3} ${POSTGRESQL}" - mkdir build/ - cd build/ - - cmake .. -DCMAKE_CXX_FLAGS="-Werror -Wno-psabi" -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3} ${POSTGRESQL} + - cmake .. -DCMAKE_CXX_FLAGS="${CXX_FLAGS}" -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3} ${POSTGRESQL} - make everything -j$(nproc || echo 1) - - 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/louiz/biboumi/test-fedora:latest +.fedora_build: + extends: .basic_build + image: docker.louiz.org/louiz/biboumi/test-fedora:oragono build:fedora: - <<: *fedora_build + extends: .fedora_build build:debian: - <<: *basic_build - image: docker.louiz.org/louiz/biboumi/test-debian:latest + extends: .basic_build + image: docker.louiz.org/louiz/biboumi/test-debian:oragono build:alpine: + extends: .basic_build variables: SYSTEMD: "-DWITHOUT_SYSTEMD=1" - <<: *basic_build - image: docker.louiz.org/louiz/biboumi/test-alpine:latest + image: docker.louiz.org/louiz/biboumi/test-alpine:oragono build:archlinux: - <<: *basic_build + extends: .basic_build only: - - branches@louiz/biboumi - when: manual + - schedules tags: - armv7l artifacts: paths: [] -build:1: +build:freebsd: + extends: .basic_build + tags: + - freebsd + only: + - branches@louiz/biboumi + variables: + CXX_FLAGS: "-Werror" + SYSTEMD: "-DWITHOUT_SYSTEMD=1" + GIT_STRATEGY: "clone" + COMPILER: "clang++" + +build:no_botan: + extends: .fedora_build variables: BOTAN: "-DWITHOUT_BOTAN=1" - <<: *fedora_build -build:2: +build:no_udns: + extends: .fedora_build + variables: + UDNS: "-DWITHOUT_UDNS=1" + +build:no_libidn: + extends: .fedora_build variables: UDNS: "-DWITHOUT_UDNS=1" - <<: *fedora_build -build:3: +build:no_sqlite3: + extends: .fedora_build variables: SQLITE3: "-DWITHOUT_SQLITE3=1" TEST_POSTGRES_URI: "postgres@postgres/postgres" services: - postgres:latest - <<: *fedora_build -build:4: +build:no_db: + extends: .fedora_build variables: SQLITE3: "-DWITHOUT_SQLITE3=1" POSTGRESQL: "-DWITHOUT_POSTGRESQL=1" - BOTAN: "-DWITHOUT_BOTAN=1" - LIBIDN: "-DWITHOUT_LIBIDN=1" - <<: *fedora_build -build:5: - variables: - UDNS: "-DWITHOUT_UDNS=1" - TEST_POSTGRES_URI: "postgres@postgres/postgres" - services: - - postgres:latest - <<: *fedora_build - -build:6: +build:no_db_botan: + extends: .fedora_build variables: + SQLITE3: "-DWITHOUT_SQLITE3=1" + POSTGRESQL: "-DWITHOUT_POSTGRESQL=1" BOTAN: "-DWITHOUT_BOTAN=1" - UDNS: "-DWITHOUT_UDNS=1" - <<: *fedora_build - -build:without_udns: - variables: - UDNS: "-DWITHOUT_UDNS=1" - <<: *fedora_build # ## Test jobs # -.template:basic_test: &basic_test +.basic_test: + extends: .sources_changed stage: test tags: - docker script: - cd build/ - - make coverage_e2e -j$(nproc || echo 1) - - make coverage + - make check_junit || true; make check; + - make e2e artifacts: expire_in: 2 weeks paths: - - build/coverage_test_suite/ - - build/coverage_e2e/ - - build/coverage_total/ - - build/coverage_e2e.info - when: always + - build/ name: $CI_PROJECT_NAME-test-$CI_JOB_ID + when: always test:debian: - image: docker.louiz.org/louiz/biboumi/test-debian:latest - <<: *basic_test + extends: .basic_test + image: docker.louiz.org/louiz/biboumi/test-debian:oragono dependencies: - build:debian + needs: ["build:debian"] test:fedora: - image: docker.louiz.org/louiz/biboumi/test-fedora:latest - <<: *basic_test + extends: .basic_test + image: docker.louiz.org/louiz/biboumi/test-fedora:oragono + script: + - cd build/ + - make coverage_check + - make coverage_e2e + - make coverage dependencies: - build:fedora + needs: ["build:fedora"] -test:without_udns: - image: docker.louiz.org/louiz/biboumi/test-fedora:latest - <<: *basic_test +test:no_udns: + extends: .basic_test + image: docker.louiz.org/louiz/biboumi/test-fedora:oragono dependencies: - - build:without_udns + - build:no_udns + needs: ["build:no_udns"] test:alpine: - image: docker.louiz.org/louiz/biboumi/test-alpine:latest - stage: test - tags: - - docker - script: - - cd build/ - - make e2e + extends: .basic_test + image: docker.louiz.org/louiz/biboumi/test-alpine:oragono dependencies: - build:alpine + needs: ["build:alpine"] test:freebsd: - only: - - branches@louiz/biboumi + extends: .basic_test tags: - freebsd - variables: - SYSTEMD: "-DWITHOUT_SYSTEMD=1" - stage: test - script: - - mkdir build/ - - cd build/ - - cmake .. -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3} - - make check - - make e2e - -coverity: - stage: external only: - branches@louiz/biboumi - tags: - - docker - image: docker.louiz.org/louiz/biboumi/test-fedora:latest - allow_failure: true - when: manual - script: - - export PATH=$PATH:~/coverity/bin - - mkdir build/ - - cd build/ - - cmake .. -DWITHOUT_SYSTEMD=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: [] + dependencies: + - build:freebsd + needs: ["build:freebsd"] # ## Packaging jobs @@ -206,7 +196,7 @@ packaging:rpm: tags: - docker allow_failure: true - image: docker.louiz.org/louiz/biboumi/test-fedora:latest + image: docker.louiz.org/louiz/biboumi/test-fedora:oragono script: - cd build/ - make rpm -j$(nproc || echo 1) @@ -219,36 +209,11 @@ packaging:rpm: dependencies: - build:fedora -packaging:deb: - stage: packaging - only: - - master@louiz/biboumi - - debian@louiz/biboumi - tags: - - docker - allow_failure: true - image: docker.louiz.org/louiz/biboumi/packaging-debian:latest - before_script: [] - script: - - git checkout debian - - git pull - - 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 + - triggers tags: - docker allow_failure: true @@ -261,3 +226,55 @@ packaging:archlinux: - makepkg -si --noconfirm - test -e /usr/bin/biboumi dependencies: [] + +# +# Deploy jobs +# + +.doc_changed: + only: + changes: + - "CMakeLists.txt" + - ".gitlab-ci.yml" + - "doc/**/*" + +# The jobs with the secure tag need to access directories where important +# files are stored: the latest doc, etc.' +# Other jobs can not access these, otherwise anybody doing a merge request +# could delete the official doc +.deploy:doc: + extends: .doc_changed + stage: deploy + image: docker.louiz.org/louiz/biboumi/doc-builder + script: + - cd doc/ + - make html + - rm -rf /www/$DOC_DEPLOY_DIR + - mv _build/html /www/$DOC_DEPLOY_DIR + dependencies: [] + +deploy:doc:latest: + extends: .deploy:doc + only: + - master@louiz/biboumi + tags: + - www + - secure + environment: + name: doc.latest + url: https://doc.biboumi.louiz.org + variables: + DOC_DEPLOY_DIR: "latest" + +deploy:doc:tag: + extends: .deploy:doc + only: + - tags + tags: + - www + - secure + environment: + name: doc.$CI_COMMIT_TAG + url: https://doc.biboumi.louiz.org/$CI_COMMIT_TAG/ + variables: + DOC_DEPLOY_DIR: $CI_COMMIT_TAG diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 35a7737..703abb3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,36 @@ +Version 9.0 +=========== + +For users +--------- +- Messages reflections are now properly cut if the body was cut before + being to sent to IRC +- Messages from unjoined resources are now rejected instead of being accepted. + This helps clients understand that they are not in the room (because of + some connection issue for example). +- All commands sent to IRC servers are now throttled to avoid being + disconnected for excess flood. The limit value can be customized using the + ad-hoc configuration form on a server JID. +- Support for XEP-0410 Self-Ping Optimization. This will prevent clients + which use self-ping from dropping out of the MUC if another client with + bad connectivity is also joined from the same account. + +For admins +---------- +- SIGHUP is now caught and reloads the configuration like SIGUSR1 and 2. +- Add a verify_certificate policy option that lets the admin disable + certificate validation per-domain. +- The WatchdogSec value in the biboumi.service file (for systemd) now + defaults to the empty string, which means “disabled”. This value can + still be set at configure time by passing the option "-DWATCHDOG_SEC=20” + to cmake, if you want to enable the systemd watchdog. + +For developers +-------------- +- The end-to-end tests have been refactored, cleaned and simplified a lot. + A tutorial and a documentation have been written. It should now be easy + to write a test that demonstrates a bug or a missing feature. + Version 8.5 - 2020-05-09 ======================== diff --git a/CMakeLists.txt b/CMakeLists.txt index debdf06..ac86555 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,9 +2,9 @@ cmake_minimum_required(VERSION 3.0) project(biboumi) -set(${PROJECT_NAME}_VERSION_MAJOR 8) -set(${PROJECT_NAME}_VERSION_MINOR 5) -set(${PROJECT_NAME}_VERSION_SUFFIX "") +set(${PROJECT_NAME}_VERSION_MAJOR 9) +set(${PROJECT_NAME}_VERSION_MINOR 0) +set(${PROJECT_NAME}_VERSION_SUFFIX "~dev") if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 5.0) @@ -74,26 +74,11 @@ set(SOFTWARE_VERSION # ## The rule that generates the documentation # -execute_process(COMMAND "date" "+%Y-%m-%d" OUTPUT_VARIABLE DOC_DATE - OUTPUT_STRIP_TRAILING_WHITESPACE) -set(MAN_PAGE ${CMAKE_CURRENT_BINARY_DIR}/doc/${PROJECT_NAME}.1) -set(DOC_PAGE ${CMAKE_CURRENT_SOURCE_DIR}/doc/${PROJECT_NAME}.1.rst) -if (NOT PANDOC_EXECUTABLE) - find_program(PANDOC_EXECUTABLE NAMES pandoc - DOC "The pandoc software, to build the man page from the rst documentation") - if(PANDOC_EXECUTABLE) - message(STATUS "Found Pandoc: ${PANDOC_EXECUTABLE}") - set(WITH_DOC true) - file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doc/) - add_custom_command(OUTPUT ${MAN_PAGE} - COMMAND ${PANDOC_EXECUTABLE} -M date="${DOC_DATE}" -s -t man ${DOC_PAGE} -o ${MAN_PAGE} - DEPENDS ${DOC_PAGE}) - add_custom_target(doc ALL DEPENDS ${MAN_PAGE}) - else() - message(STATUS "Pandoc not found, documentation cannot be built") - endif() -endif() -mark_as_advanced(PANDOC_EXECUTABLE) +add_custom_target(html COMMAND make html BUILDDIR=${CMAKE_CURRENT_BINARY_DIR} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/doc) +add_custom_target(man COMMAND make man BUILDDIR=${CMAKE_CURRENT_BINARY_DIR} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/doc) +add_custom_target(doc DEPENDS html man) # ## Set this search path for cmake, to find our custom search modules @@ -218,8 +203,6 @@ if(SQLITE3_FOUND OR PQ_FOUND) include_directories(database ${PQ_INCLUDE_DIRS}) endif() set(USE_DATABASE TRUE) -else() - add_library(database OBJECT "") endif() # @@ -235,8 +218,7 @@ add_executable(${PROJECT_NAME} src/main.cpp $<TARGET_OBJECTS:xmpp> $<TARGET_OBJECTS:bridge> $<TARGET_OBJECTS:irc> - $<TARGET_OBJECTS:identd> - $<TARGET_OBJECTS:database>) + $<TARGET_OBJECTS:identd>) ## test_suite file(GLOB source_tests @@ -249,9 +231,12 @@ add_executable(test_suite ${source_tests} $<TARGET_OBJECTS:xmpp> $<TARGET_OBJECTS:bridge> $<TARGET_OBJECTS:irc> - $<TARGET_OBJECTS:identd> - $<TARGET_OBJECTS:database>) + $<TARGET_OBJECTS:identd>) set_target_properties(test_suite PROPERTIES EXCLUDE_FROM_ALL TRUE) +if(USE_DATABASE) + target_sources(${PROJECT_NAME} PRIVATE $<TARGET_OBJECTS:database>) + target_sources(test_suite PRIVATE $<TARGET_OBJECTS:database>) +endif() # ## Link the executables with their libraries @@ -327,7 +312,9 @@ endif() ## Add some custom rules to launch the tests # add_custom_target(check COMMAND "test_suite" - DEPENDS test_suite biboumi) + DEPENDS test_suite) +add_custom_target(check_junit COMMAND test_suite -r junit -o check_result.xml + DEPENDS test_suite) 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) @@ -338,13 +325,14 @@ if(CMAKE_BUILD_TYPE MATCHES Debug) include(CodeCoverage) SETUP_TARGET_FOR_COVERAGE(coverage_check ./test_suite - coverage_test_suite) + coverage_test_suite + "-o;check_result.xml;-r;junit") add_dependencies(coverage_check test_suite) SETUP_TARGET_FOR_COVERAGE(coverage_e2e python3 coverage_e2e - ${CMAKE_CURRENT_SOURCE_DIR}/tests/end_to_end/) + "${CMAKE_CURRENT_SOURCE_DIR}/tests/end_to_end/") add_dependencies(coverage_e2e biboumi) ADD_CUSTOM_TARGET(coverage @@ -361,7 +349,7 @@ add_custom_target(everything DEPENDS test_suite biboumi) ## Install target # install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin) -install(FILES ${MAN_PAGE} DESTINATION share/man/man1 OPTIONAL COMPONENT documentation) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/biboumi.1 DESTINATION share/man/man1 OPTIONAL COMPONENT documentation) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/biboumi.service DESTINATION lib/systemd/system COMPONENT init) file(GLOB policy_files conf/*policy.txt) install(FILES ${policy_files} DESTINATION /etc/biboumi COMPONENT configuration) @@ -397,12 +385,11 @@ add_custom_target(rpm # 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.") +set(WATCHDOG_SEC "" CACHE STRING ${WATCHDOG_SEC_DOCSTRING}) if(SYSTEMD_FOUND) set(SYSTEMD_SERVICE_TYPE "notify" CACHE STRING ${SYSTEMD_SERVICE_TYPE_DOCSTRING}) - set(WATCHDOG_SEC "20" CACHE STRING ${WATCHDOG_SEC_DOCSTRING}) else() set(SYSTEMD_SERVICE_TYPE "simple" CACHE STRING ${SYSTEMD_SERVICE_TYPE_DOCSTRING}) - set(WATCHDOG_SEC "" CACHE STRING ${WATCHDOG_SEC_DOCSTRING}) endif() set(SERVICE_USER_DOCSTRING "The value used as the User= in the systemd unit file.") if(NOT DEFINED SERVICE_USER) @@ -1,15 +1,6 @@ Biboumi ======= -.. image:: https://lab.louiz.org/louiz/biboumi/badges/master/build.svg - :target: https://lab.louiz.org/louiz/biboumi/pipelines - -.. image:: https://coverity.proxy.louiz.org/projects/3726/badge.svg - :target: https://scan.coverity.com/projects/louiz-biboumi - -.. 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 between the two protocols. It can be used to access IRC channels using any XMPP client as if these channels were XMPP MUCs. @@ -60,6 +51,6 @@ Biboumi is Free Software. Biboumi is released under the zlib license. Please read the COPYING file for details. -.. _INSTALL: INSTALL.rst -.. _the documentation: doc/biboumi.1.rst +.. _INSTALL: doc/install.rst +.. _the documentation: https://doc.biboumi.louiz.org .. _contributing: CONTRIBUTING.rst diff --git a/cmake/Modules/CodeCoverage.cmake b/cmake/Modules/CodeCoverage.cmake index 77586ab..9fde45e 100644 --- a/cmake/Modules/CodeCoverage.cmake +++ b/cmake/Modules/CodeCoverage.cmake @@ -157,7 +157,7 @@ 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 -q + COMMAND ${LCOV_PATH} --remove ${_outputname}.info '/usr/*' '*/external/*' --output-file ${_outputname}.info -q # Generate the report COMMAND ${GENHTML_PATH} -o ${_outputname} ${_outputname}.info diff --git a/conf/biboumi.cfg b/conf/biboumi.cfg index e6b8ed5..8e51542 100644 --- a/conf/biboumi.cfg +++ b/conf/biboumi.cfg @@ -1,7 +1,15 @@ -hostname=biboumi.example.com -password=secret -db_name=/var/lib/biboumi/biboumi.sqlite -log_file=/var/log/biboumi/biboumi.log -log_level=0 -admin= +# This is an example configuration for the biboumi component. +# It only contains the default values. +# You need to, at least, set the values for hostname and password, if you +# want biboumi to be able to start + +hostname= +password= +xmpp_server_ip=127.0.0.1 port=5347 +admin= +realname_customization=true +realname_from_jid=false +log_file= +ca_file= +outgoing_bind= diff --git a/conf/irc.gnome.org.policy.txt b/conf/irc.gnome.org.policy.txt index 2357a53..f06b958 100644..120000 --- a/conf/irc.gnome.org.policy.txt +++ b/conf/irc.gnome.org.policy.txt @@ -1 +1 @@ -key_exchange_methods = RSA +irc.gimp.org.policy.txt
\ No newline at end of file diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..934bdf7 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = biboumi +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
\ No newline at end of file diff --git a/doc/admin.rst b/doc/admin.rst new file mode 100644 index 0000000..a5850a7 --- /dev/null +++ b/doc/admin.rst @@ -0,0 +1,305 @@ +########################### +Administrator documentation +########################### + +Usage +===== + +Biboumi acts as a server, it should be run as a daemon that lives in the +background for as long as it is needed. Note that biboumi does not +daemonize itself, this task should be done by your init system (SysVinit, +systemd, upstart). + +When started, biboumi connects, without encryption (see :ref:`Security`), to the +local XMPP server on the port ``5347`` and authenticates with the provided +password. Biboumi then serves the configured ``hostname``: this means that +all XMPP stanza with a `to` JID on that domain will be forwarded to biboumi +by the XMPP server, and biboumi will only send messages coming from that +hostname. + +To cleanly shutdown the component, send a SIGINT or SIGTERM signal to it. +It will send messages to all connected IRC and XMPP servers to indicate a +reason why the users are being disconnected. Biboumi exits when the end of +communication is acknowledged by all IRC servers. If one or more IRC +servers do not respond, biboumi will only exit if it receives the same +signal again or if a 2 seconds delay has passed. + +Configuration +============= + +Configuration happens in different places, with different purposes: + +- The main and global configuration that specifies vital settings for the + daemon to run, like the hostname, password etc. This is an admin-only + configuration, and this is described in the next section. +- A TLS configuration, also admin-only, that can be either global or + per-domain. See `TLS configuration`_ section. +- Using the :ref:`ad-hoc commands`, each user can configure various + settings for themself + +Daemon configuration +-------------------- + +The configuration file is read by biboumi as it starts. The path is +specified as the only argument to the biboumi binary. + +The configuration file uses a simple format of the form ``option=value`` +(note that there are no spaces before or after the equal sign). + +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, SIGUSR2 or SIGHUP (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. + +Options +------- + +A configuration file can look something like this: + +.. code-block:: ini + + hostname=biboumi.example.com + password=mypassword + xmpp_server_ip=127.0.0.1 + port=5347 + admin=myself@example.com + db_name=postgresql://biboumi:password@localhost/biboumi + realname_customization=true + realname_from_jid=false + log_file= + ca_file= + outgoing_bind=192.168.0.12 + + +Here is a description of all available options + +hostname +~~~~~~~~ + +Mandatory. The hostname served by the XMPP gateway. This domain must be +configured in the XMPP server as an external component. See the manual +for your XMPP server for more information. For prosody, see +http://prosody.im/doc/components#adding_an_external_component + +password +~~~~~~~~ + +Mandatory. The password used to authenticate the XMPP component to your +XMPP server. This password must be configured in the XMPP server, +associated with the external component on *hostname*. + +xmpp_server_ip +~~~~~~~~~~~~~~ + +The IP address to connect to the XMPP server on. The connection to the +XMPP server is unencrypted, so the biboumi instance and the server should +normally be on the same host. The default value is 127.0.0.1. + +port +~~~~ + +The TCP port to use to connect to the local XMPP component. The default +value is 5347. + +db_name +~~~~~~~ + +The name of the database to use. This option can only be used if biboumi +has been compiled with a database support (Sqlite3 and/or PostgreSQL). If +the value begins with the postgresql scheme, “postgresql://” or +“postgres://”, then biboumi will try to connect to the PostgreSQL database +specified by the URI. See `the PostgreSQL doc +<https://www.postgresql.org/docs/current/static/libpq-connect.html#idm46428693970032>`_ +for all possible values. For example the value could be +“postgresql://user:secret@localhost”. If the value does not start with the +postgresql scheme, then it specifies a filename that will be opened with +Sqlite3. For example the value could be “/var/lib/biboumi/biboumi.sqlite”. + +admin +~~~~~ + +The bare JID of the gateway administrator. This JID will have more +privileges than other standard users, for example some administration +ad-hoc commands will only be available to that JID. + +If you need more than one administrator, separate them with a colon (:). + +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``. 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 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. + +persistent_by_default +~~~~~~~~~~~~~~~~~~~~~ + +If this option is set to `true`, all rooms will be persistent by default: +the value of the “persistent” option in the global configuration of each +user will be “true”, but the value of each individual room will still +default to false. This means that a user just needs to change the global +“persistent” configuration option to false in order to override this. + +If it is set to false (the default value), all rooms are not persistent by +default. + +Each room can be configured individually by each user, to override this +default value. See :ref:`Ad-hoc commands`. + +realname_customization +~~~~~~~~~~~~~~~~~~~~~~ + +If this option is set to “false” (default is “true”), the users will not be +able to use the ad-hoc commands that lets them configure their realname and +username. + +realname_from_jid +~~~~~~~~~~~~~~~~~ + +If this option is set to “true”, the realname and username of each biboumi +user will be extracted from their JID. The realname is their bare JID, and +the username is the node-part of their JID. Note that if +``realname_customization`` is “true”, each user will still be able to +customize their realname and username, this option just decides the default +realname and username. + +If this option is set to “false” (the default value), the realname and +username of each user will be set to the nick they used to connect to the +IRC server. + +webirc_password +~~~~~~~~~~~~~~~ + +Configure a password to be communicated to the IRC server, as part of the +WEBIRC message (see https://kiwiirc.com/docs/webirc). If this option is +set, an additional DNS resolution of the hostname of each XMPP server will +be made when connecting to an IRC server. + +log_file +~~~~~~~~ + +A filename into which logs are written. If none is provided, the logs are +written on standard output. + +log_level +~~~~~~~~~ + +Indicate what type of log messages to write in the logs. Value can be +from 0 to 3. 0 is debug, 1 is info, 2 is warning, 3 is error. The +default is 0, but a more practical value for production use is 1. + +ca_file +~~~~~~~ + +Specifies which file should be used as the list of trusted CA when +negociating a TLS session. By default this value is unset and biboumi +tries a list of well-known paths. + +outgoing_bind +~~~~~~~~~~~~~ + +An address (IPv4 or IPv6) to bind the outgoing sockets to. If no value is +specified, it will use the one assigned by the operating system. You can +for example use outgoing_bind=192.168.1.11 to force biboumi to use the +interface with this address. Note that this is only used for connections +to IRC servers. + +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. + +To disable the built-in identd, you may set identd_port to 0. + +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>`_. +In addition to these Botan options, biboumi implements a few custom options +listed hereafter: +- verify_certificate: if this value is set to false, biboumi will not check +the certificate validity at all. The default value is true. + +By default, biboumi provides a few policy files, to work around some +issues found with a few well-known IRC servers. + + +Security +======== + +The connection to the XMPP server can only be made on localhost. The +XMPP server is not supposed to accept non-local connections from +components. Thus, encryption is not used to connect to the local +XMPP server because it is useless. + +If compiled with the Botan library, biboumi can use TLS when communicating +with the IRC servers. It will first try ports 6697 and 6670 and use TLS +if it succeeds, if connection fails on both these ports, the connection is +established on port 6667 without any encryption. + +Biboumi does not check if the received JIDs are properly formatted using +nodeprep. This must be done by the XMPP server to which biboumi is +directly connected. + +Biboumi does not provide a way to ban users from connecting to it, has no +protection against flood or any sort of abuse that your users may cause on +the IRC servers. Some XMPP server however offer the possibility to restrict +what JID can access a gateway. Use that feature if you wish to grant access +to your biboumi instance only to a list of trusted users. + + + diff --git a/doc/biboumi.1.rst b/doc/biboumi.1.rst deleted file mode 100644 index 03e8a36..0000000 --- a/doc/biboumi.1.rst +++ /dev/null @@ -1,762 +0,0 @@ -====================== -Biboumi(1) User Manual -====================== - -.. contents:: :depth: 2 - -NAME -==== - -biboumi - XMPP gateway to IRC - -Description -=========== - -Biboumi is an XMPP gateway that connects to IRC servers and translates -between the two protocols. It can be used to access IRC channels using any -XMPP client as if these channels were XMPP MUCs. - -Synopsis -======== - -biboumi [*config_filename*] - -Options -======= - -Available command line options: - -config_filename ---------------- - -Specify the file to read for configuration. See the `Configuration`_ section for more -details on its content. - -Configuration -============= - -The configuration file uses a simple format of the form ``option=value``. - -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 --------- - -Mandatory. The hostname served by the XMPP gateway. This domain must be -configured in the XMPP server as an external component. See the manual -for your XMPP server for more information. For prosody, see -http://prosody.im/doc/components#adding_an_external_component - -password --------- - -Mandatory. The password used to authenticate the XMPP component to your -XMPP server. This password must be configured in the XMPP server, -associated with the external component on *hostname*. - -xmpp_server_ip --------------- - -The IP address to connect to the XMPP server on. The connection to the -XMPP server is unencrypted, so the biboumi instance and the server should -normally be on the same host. The default value is 127.0.0.1. - -port ----- - -The TCP port to use to connect to the local XMPP component. The default -value is 5347. - -db_name -------- - -The name of the database to use. This option can only be used if biboumi -has been compiled with a database support (Sqlite3 and/or PostgreSQL). If -the value begins with the postgresql scheme, “postgresql://” or -“postgres://”, then biboumi will try to connect to the PostgreSQL database -specified by the URI. See -https://www.postgresql.org/docs/current/static/libpq-connect.html#idm46428693970032 -for all possible values. For example the value could be -“postgresql://user:secret@localhost”. If the value does not start with the -postgresql scheme, then it specifies a filename that will be opened with -Sqlite3. For example the value could be “/var/lib/biboumi/biboumi.sqlite”. - -admin ------ - -The bare JID of the gateway administrator. This JID will have more -privileges than other standard users, for example some administration -ad-hoc commands will only be available to that JID. - -If you need more than one administrator, separate them with a colon (:). - -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``. 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 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. - -persistent_by_default ---------------------- - -If this option is set to `true`, all rooms will be persistent by default: -the value of the “persistent” option in the global configuration of each -user will be “true”, but the value of each individual room will still -default to false. This means that a user just needs to change the global -“persistent” configuration option to false in order to override this. - -If it is set to false (the default value), all rooms are not persistent by -default. - -Each room can be configured individually by each user, to override this -default value. See `Ad-hoc commands`_. - -realname_customization ----------------------- - -If this option is set to “false” (default is “true”), the users will not be -able to use the ad-hoc commands that lets them configure their realname and -username. - -realname_from_jid ------------------ - -If this option is set to “true”, the realname and username of each biboumi -user will be extracted from their JID. The realname is their bare JID, and -the username is the node-part of their JID. Note that if -``realname_customization`` is “true”, each user will still be able to -customize their realname and username, this option just decides the default -realname and username. - -If this option is set to “false” (the default value), the realname and -username of each user will be set to the nick they used to connect to the -IRC server. - -webirc_password ---------------- - -Configure a password to be communicated to the IRC server, as part of the -WEBIRC message (see https://kiwiirc.com/docs/webirc). If this option is -set, an additional DNS resolution of the hostname of each XMPP server will -be made when connecting to an IRC server. - -log_file --------- - -A filename into which logs are written. If none is provided, the logs are -written on standard output. - -log_level ---------- - -Indicate what type of log messages to write in the logs. Value can be -from 0 to 3. 0 is debug, 1 is info, 2 is warning, 3 is error. The -default is 0, but a more practical value for production use is 1. - -ca_file -------- - -Specifies which file should be used as the list of trusted CA when -negociating a TLS session. By default this value is unset and biboumi -tries a list of well-known paths. - -outgoing_bind -------------- - -An address (IPv4 or IPv6) to bind the outgoing sockets to. If no value is -specified, it will use the one assigned by the operating system. You can -for example use outgoing_bind=192.168.1.11 to force biboumi to use the -interface with this address. Note that this is only used for connections -to IRC servers. - -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. - -To disable the built-in identd, you may set identd_port to 0. - -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 -===== - -Biboumi acts as a server, it should be run as a daemon that lives in the -background for as long as it is needed. Note that biboumi does not -daemonize itself, this task should be done by your init system (SysVinit, -systemd, upstart). - -When started, biboumi connects, without encryption (see `Security`_), to the -local XMPP server on the port ``5347`` and authenticates with the provided -password. Biboumi then serves the configured ``hostname``: this means that -all XMPP stanza with a `to` JID on that domain will be forwarded to biboumi -by the XMPP server, and biboumi will only send messages coming from that -hostname. - -When a user joins an IRC channel on an IRC server (see `Join an IRC -channel`_), biboumi connects to the remote IRC server, sets the user’s nick -as requested, and then tries to join the specified channel. If the same -user subsequently tries to connect to an other channel on the same server, -the same IRC connection is used. If, however, an other user wants to join -an IRC channel on that same IRC server, biboumi opens a new connection to -that server. Biboumi connects once to each IRC server, for each user on it. - -Additionally, if one user is using more than one clients (with the same bare -JID), they can join the same IRC channel (on the same server) behind one -single nickname. Biboumi will forward all the messages (the channel ones and -the private ones) and the presences to all the resources behind that nick. -There is no need to have multiple nicknames and multiple connections to be -able to take part in a conversation (or idle) in a channel from a mobile client -while the desktop client is still connected, for example. - -To cleanly shutdown the component, send a SIGINT or SIGTERM signal to it. -It will send messages to all connected IRC and XMPP servers to indicate a -reason why the users are being disconnected. Biboumi exits when the end of -communication is acknowledged by all IRC servers. If one or more IRC -servers do not respond, biboumi will only exit if it receives the same -signal again or if a 2 seconds delay has passed. - -Addressing ----------- - -IRC entities are represented by XMPP JIDs. The domain part of the JID is -the domain served by biboumi (the part after the `@`, biboumi.example.com in -the examples), and the local part (the part before the `@`) depends on the -concerned entity. - -IRC channels and IRC users have a local part formed like this: -``name`` % ``irc_server``. - -``name`` can be a channel name or an user nickname. The distinction between -the two is based on the first character: by default, if the name starts with -``'#'`` or ``'&'`` (but this can be overridden by the server, using the -ISUPPORT extension) then it’s a channel name, otherwise this is a nickname. - -There is two ways to address an IRC user, using a local part like this: -``nickname`` % ``irc_server`` or by using the in-room address of the -participant, like this: -``channel_name`` % ``irc_server`` @ ``biboumi.example.com`` / ``Nickname`` - -The second JID is available only to be compatible with XMPP clients when the -user wants to send a private message to the participant ``Nickname`` in the -room ``channel_name%irc_server@biboumi.example.com``. - -On XMPP, the node part of the JID can only be lowercase. On the other hand, -IRC nicknames are case-insensitive, this means that the nicknames toto, -Toto, tOtO and TOTO all represent the same IRC user. This means you can -talk to the user toto, and this will work. - -Also note that some IRC nicknames or channels may contain characters that are -not allowed in the local part of a JID (for example '@'). If you need to send a -message to a nick containing such a character, you can use a jid like -``%irc.example.com@biboumi.example.com/AnnoyingNickn@me``, because the JID -``AnnoyingNickn@me%irc.example.com@biboumi.example.com`` would not work. -And if you need to address a channel that contains such invalid characters, you -have to use `jid-escaping <http://www.xmpp.org/extensions/xep-0106.html#escaping>`_, -and replace each of these characters with their escaped version, for example to -join the channel ``#b@byfoot``, you need to use the following JID: -``#b\40byfoot%irc.example.com@biboumi.example.com``. - - -Examples: - -* ``#foo%irc.example.com@biboumi.example.com`` is the #foo IRC channel, on the - irc.example.com IRC server, and this is served by the biboumi instance on - biboumi.example.com - -* ``toto%irc.example.com@biboumi.example.com`` is the IRC user named toto, or - TotO, etc. - -* ``irc.example.com@biboumi.example.com`` is the IRC server irc.example.com. - -Note: Some JIDs are valid but make no sense in the context of -biboumi: - -* ``#test%@biboumi.example.com``, or any other JID that does not contain an - IRC server is invalid. Any message to that kind of JID will trigger an - error, or will be ignored. - -If compiled with Libidn, an IRC channel participant has a bare JID -representing the “hostname” provided by the IRC server. This JID can only -be used to set IRC modes (for example to ban a user based on its IP), or to -identify user. It cannot be used to contact that user using biboumi. - -Join an IRC channel -------------------- - -To join an IRC channel ``#foo`` on the IRC server ``irc.example.com``, -join the XMPP MUC ``#foo%irc.example.com@biboumi.example.com``. - -Connect to an IRC server ------------------------- - -The connection to the IRC server is automatically made when the user tries -to join any channel on that IRC server. The connection is closed whenever -the last channel on that server is left by the user. - -Roster ------- - -You can add some JIDs provided by biboumi into your own roster, to receive -presence from them. Biboumi will always automatically accept your requests. - -Biboumi’s JID -------------- - -By adding the component JID into your roster, the user will receive an available -presence whenever it is started, and an unavailable presence whenever it is being -shutdown. This is useful to quickly view if that biboumi instance is started or -not. - -IRC server JID --------------- - -These presence will appear online in the user’s roster whenever they are -connected to that IRC server (see `Connect to an IRC server`_ for more -details). This is useful to keep track of which server an user is connected -to: this is sometimes hard to remember, when they have many clients, or if -they are using persistent channels. - -Channel messages ----------------- - -On XMPP, unlike on IRC, the displayed order of the messages is the same for -all participants of a MUC. Biboumi can not however provide this feature, as -it cannot know whether the IRC server has received and forwarded the -messages to other users. This means that the order of the messages -displayed in your XMPP client may not be the same 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 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. - -A channel history can be retrieved by using `Message archive management (MAM) -<https://xmpp.org/extensions/xep-0313.htm>`_ on the channel JID. The results -can be filtered by start and end dates. - -When a channel is joined, if the client doesn’t specify any limit, biboumi -sends the `max_history_length` last messages found in the database as the -MUC history. If a client wants to only use MAM for the archives (because -it’s more convenient and powerful), it should request to receive no -history by using an attribute maxchars='0' or maxstanzas='0' as defined in -XEP 0045, and do a proper MAM request instead. - -Note: the maxchars attribute is ignored unless its value is exactly 0. -Supporting it properly would be very hard and would introduce a lot of -complexity for almost no benefit. - -For a given channel, each user has her or his own archive. The content of -the archives are never shared, and thus a user can not use someone else’s -archive to get the messages that they didn’t receive when they were offline. -Although this feature would be very convenient, this would introduce a very -important privacy issue: for example if a biboumi gateway is used by two -users, by querying the archive one user would be able to know whether or not -the other user was in a room at a given time. - - -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 so the result stanza may be very big, unless your client supports -result set management (XEP 0059) - -Nicknames ---------- - -On IRC, nicknames are server-wide. This means that one user only has one -single nickname at one given time on all the channels of a server. This is -different from XMPP where a user can have a different nick on each MUC, -even if these MUCs are on the same server. - -This means that the nick you choose when joining your first IRC channel on a -given IRC server will be your nickname in all other channels that you join -on that same IRC server. -If you explicitely change your nickname on one channel, your nickname will -be changed on all channels on the same server as well. -Joining a new channel with a different nick, however, will not change your -nick. The provided nick will be ignored, in order to avoid changing your -nick on the whole server by mistake. If you want to have a different -nickname in the channel you’re going to join, you need to do it explicitly -with the NICK command before joining the channel. - -Private messages ----------------- - -Private messages are handled differently on IRC and on XMPP. On IRC, you -talk directly to one server-user: toto on the channel #foo is the same user -as toto on the channel #bar (as long as these two channels are on the same -IRC server). By default you will receive private messages from the “global” -user (aka nickname%irc.example.com@biboumi.example.com), unless you -previously sent a message to an in-room participant (something like -\#test%irc.example.com@biboumi.example.com/nickname), in which case future -messages from that same user will be received from that same “in-room” JID. - -Notices -------- - -Notices are received exactly like private messages. It is not possible to -send a notice. - -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 ------------ - -If the invited JID is a user JID served by this biboumi instance, it will forward the -invitation to the target nick, over IRC. -Otherwise, the mediated instance will directly be sent to the invited JID, over XMPP. - -Example: if the user wishes to invite the IRC user “FooBar” into a room, they can -invite one of the following “JIDs” (one of them is not a JID, actually): - -- foobar%anything@biboumi.example.com -- anything@biboumi.example.com/FooBar -- FooBar - -(Note that the “anything” parts are simply ignored because they carry no -additional meaning for biboumi: we already know which IRC server is targeted -using the JID of the target channel.) - -Otherwise, any valid JID can be used, to invite any XMPP user. - -Kicks and bans --------------- - -Kicks are transparently translated from one protocol to another. However -banning an XMPP participant has no effect. To ban an user you need to set a -mode +b on that user nick or host (see `IRC modes`_) and then kick it. - -Encoding --------- - -On XMPP, the encoding is always ``UTF-8``, whereas on IRC the encoding of -each message can be anything. - -This means that biboumi has to convert everything coming from IRC into UTF-8 -without knowing the encoding of the received messages. To do so, it checks -if each message is UTF-8 valid, if not it tries to convert from -``iso_8859-1`` (because this appears to be the most common case, at least -on the channels I visit) to ``UTF-8``. If that conversion fails at some -point, a placeholder character ``'�'`` is inserted to indicate this -decoding error. - -Messages are always sent in UTF-8 over IRC, no conversion is done in that -direction. - -IRC modes ---------- - -One feature that doesn’t exist on XMPP but does on IRC is the ``modes``. -Although some of these modes have a correspondance in the XMPP world (for -example the ``+o`` mode on a user corresponds to the ``moderator`` role in -XMPP), it is impossible to map all these modes to an XMPP feature. To -circumvent this problem, biboumi provides a raw notification when modes are -changed, and lets the user change the modes directly. - -To change modes, simply send a message starting with “``/mode``” followed by -the modes and the arguments you want to send to the IRC server. For example -“/mode +aho louiz”. Note that your XMPP client may interprete messages -begining with “/” like a command. To actually send a message starting with -a slash, you may need to start your message with “//mode” or “/say /mode”, -depending on your client. - -When a mode is changed, the user is notified by a message coming from the -MUC bare JID, looking like “Mode #foo [+ov] [toto tutu]”. In addition, if -the mode change can be translated to an XMPP feature, the user will be -notified of this XMPP event as well. For example if a mode “+o toto” is -received, then toto’s role will be changed to moderator. The mapping -between IRC modes and XMPP features is as follow: - -``+q`` - Sets the participant’s role to ``moderator`` and its affiliation to ``owner``. - -``+a`` - Sets the participant’s role to ``moderator`` and its affiliation to ``owner``. - -``+o`` - Sets the participant’s role to ``moderator`` and its affiliation to ``admin``. - -``+h`` - Sets the participant’s role to ``moderator`` and its affiliation to ``member``. - -``+v`` - Sets the participant’s role to ``participant`` and its affiliation to ``member``. - -Similarly, when a biboumi user changes some participant's affiliation or role, biboumi translates that in an IRC mode change. - -Affiliation set to ``none`` - Sets mode to -vhoaq - -Affiliation set to ``member`` - Sets mode to +v-hoaq - -Role set to ``moderator`` - Sets mode to +h-oaq - -Affiliation set to ``admin`` - Sets mode to +o-aq - -Affiliation set to ``owner`` - Sets mode to +a-q - -Ad-hoc commands ---------------- - -Biboumi supports a few ad-hoc commands, as described in the XEP 0050. -Different ad-hoc commands are available for each JID type. - -On the gateway itself (e.g on the JID biboumi.example.com): -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- ping: Just respond “pong” - -- hello: Provide a form, where the user enters their name, and biboumi - responds with a nice greeting. - -- disconnect-user: Only available to the administrator. The user provides - a list of JIDs, and a quit message. All the selected users are - disconnected from all the IRC servers to which they were connected, - using the provided quit message. Sending SIGINT to biboumi is equivalent - to using this command by selecting all the connected JIDs and using the - “Gateway shutdown” quit message, except that biboumi does not exit when - using this ad-hoc command. - -- disconnect-from-irc-servers: Disconnect a single user from one or more - IRC server. The user is immediately disconnected by closing the socket, - no message is sent to the IRC server, but the user is of course notified - with an XMPP message. The administrator can disconnect any user, while - the other users can only disconnect themselves. - -- 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. - If this option is set to true, all channels are persistent, whether - or not their specific value is true or false. This option is true by - default for everyone if the `persistent_by_default` configuration - option is true, otherwise it’s false. See below for more details on - what a persistent channel is. This value 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. The provided configuration form contains these - fields: - - * Address: This address (IPv4, IPv6 or hostname) will be used, when - biboumi connects to this server. This is a very handy way to have a - custom name for a network, and be able to edit the address to use - if one endpoint for that server is dead, but continue using the same - JID. For example, a user could configure the server - “freenode@biboumi.example.com”, set “chat.freenode.net” in its - “Address” field, and then they would be able to use “freenode” as - the network name forever: if “chat.freenode.net” breaks for some - reason, it can be changed to “irc.freenode.org” instead, and the user - would not need to change all their bookmarks and settings. - * 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 commands: Raw IRC commands that will be sent - one by one 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. - * Nickname: A nickname that will be used instead of the nickname provided - in the initial presence sent to join a channel. This can be used if the - user always wants to have the same nickname on a given server, and not - have to bother with setting that nick in all the bookmarks on that - server. The nickname can still manually be changed with a standard nick - change presence. - * 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 - of joined IRC channel, with a detailed list of which resource is in which - channel. - -On a channel JID (e.g on the JID #test%chat.freenode.org@biboumi.example.com) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- configure: Lets each user configure some options that applies to the - concerned IRC channel. Some of these options, if not configured for a - specific channel, defaults to the value configured at the IRC server - level. For example the encoding can be specified for both the channel - and the server. If an encoding is not specified for a channel, the - encoding configured in the server applies. 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 ----------------- - -Biboumi tries to support as many IRC features as possible, but doesn’t -handle everything yet (or ever). In order to let the user send any -arbitrary IRC message, biboumi forwards any XMPP message received on an IRC -Server JID (see `Addressing`_) as a raw command to that IRC server. - -For example, to WHOIS the user Foo on the server irc.example.com, a user can -send the message “WHOIS Foo” to ``irc.example.com@biboumi.example.com``. - -The message will be forwarded as is, without any modification appart from -adding ``\r\n`` at the end (to make it a valid IRC message). You need to -have a little bit of understanding of the IRC protocol to use this feature. - -Security -======== - -The connection to the XMPP server can only be made on localhost. The -XMPP server is not supposed to accept non-local connections from components. -Thus, encryption is not used to connect to the local XMPP server because it -is useless. - -If compiled with the Botan library, biboumi can use TLS when communicating -with the IRC servers. It will first try ports 6697 and 6670 and use TLS if -it succeeds, if connection fails on both these ports, the connection is -established on port 6667 without any encryption. - -Biboumi does not check if the received JIDs are properly formatted using -nodeprep. This must be done by the XMPP server to which biboumi is directly -connected. - -Note if you use a biboumi that you have no control on: remember that the -administrator of the gateway you use is able to view all your IRC -conversations, whether you’re using encryption or not. This is exactly as -if you were running your IRC client on someone else’s server. Only use -biboumi if you trust its administrator (or, better, if you are the -administrator) or if you don’t intend to have any private conversation. - -Biboumi does not provide a way to ban users from connecting to it, has no -protection against flood or any sort of abuse that your users may cause on -the IRC servers. Some XMPP server however offer the possibility to restrict -what JID can access a gateway. Use that feature if you wish to grant access -to your biboumi instance only to a list of trusted users. - diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 0000000..504c15b --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'biboumi' +copyright = '2018, Florent Le Coz' +author = 'Florent Le Coz' + +# The short X.Y version +version = '8.4' +# The full version, including alpha/beta/rc tags +release = '8.4' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.coverage', + 'sphinx.ext.autosectionlabel', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path . +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +#html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'biboumidoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'biboumi.tex', 'biboumi Documentation', + author, 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('man_index', 'biboumi', 'biboumi Documentation', + [author], 1) +] + +# -- Extension configuration ------------------------------------------------- diff --git a/CONTRIBUTING.rst b/doc/contributing.rst index 8df4899..8f01c82 100644 --- a/CONTRIBUTING.rst +++ b/doc/contributing.rst @@ -1,5 +1,6 @@ +####################### Contributing to biboumi -======================= +####################### Biboumi’s main workplace is at https://lab.louiz.org/louiz/biboumi @@ -20,8 +21,12 @@ 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. +If the issue you’re reporting may have security implications, please +select the “confidential” flag in your bug report. This includes, but is not limited to: + +- disclosure of private data that was supposed to be encrypted using TLS +- denial of service (crash, infinite loop, etc) that can be caused by any + user Code @@ -45,31 +50,13 @@ 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++ + These tests use the Catch2 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. - + biboumi configuration (when all dependencies are used). + Read more about these tests in the specific documentation TODO. All these tests automatically run with various configurations, on various platforms, using gitlab CI. @@ -82,7 +69,7 @@ Please try to follow the existing style: - Use only spaces, not tabs. - Curly brackets are on their own lines. - Use this-> everywhere it’s possible. -- Don’t start class attributes with “m_” or similar. +- Don’t start class attributes with “m\_” or similar. - Type names are in PascalCase. - Everything else is in snake_case. diff --git a/doc/developer.rst b/doc/developer.rst new file mode 100644 index 0000000..b3ef158 --- /dev/null +++ b/doc/developer.rst @@ -0,0 +1,302 @@ +######################## +Developer documentation +######################## + +End-to-end test suite +--------------------- + +A powerful test suite has been developped to test biboumi’s behaviour in +many scenarios. Its goal is to simulate a real-world usage of biboumi, +including its interactions with a real IRC server an a real XMPP client. + +An IRC server is started, with a specific version and configuration, then, +for every scenario that we want to test: + +- Biboumi is started, with a specific configuration +- An XMPP “client” starts, communicates with biboumi and checks that + biboumi responds in the expected way. + +The XMPP client is actually not a real client, it’s a python script that +uses the slixmpp library to imitate an XMPP server that would transmit the +stanzas of one client to its component (biboumi). In real life, the +communication to biboumi is done between the XMPP server and biboumi, but +since the server just forwards the messages that clients send unmodified, +we’ll call that “the clients”. + +A scenario is a list of functions that will be executed one by one, to +verify the behaviour of one specific feature. Ideally, they should be +short and test one specific aspect. + +Run the test suite locally +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since this requires a lot of dependencies (an IRC server with some TLS +certificate, slixmpp, many libraries…), it might be cumbersome to get +everything on your machine to be able to run them. + +The simplest solution (as long as you have docker installed and properly +configured to be able to run as your developer user… It’s as simple as +“dnf install docker” and “chmod o+rw /var/run/docker.sock”, but that’s not +recommended, because this lets anybody on the system use docker, and +docker is very unsecure) is to follow these instructions: + +.. code-block:: bash + :caption: Start a docker container with everything installed + + docker run --name biboumi-e2e -v /home/louiz/biboumi/:/home/tester/biboumi \ + --add-host="irc.localhost:127.0.0.1" \ + --add-host="biboumi.localhost:127.0.0.1" \ + --rm -it docker.louiz.org/louiz/biboumi/test-alpine \ + /bin/bash + +This creates a container where every dependency is already installed. We +mount your working directory inside the container: be sure to modify the +first path `/home/louiz/biboumi` with your own. The hosts that we add are +needed for the test suite to properly work. + +You can use the test-fedora or test-debian images instead of test-alpine +if you want, but it should not change anything (even if your host machine +uses debian or fedora), alpine is just the lighter one. + +.. note:: + + This container should stay alive as long as you want to run the test + suite. For example if you want to run it many times until your code is + fine and all tests pass, just leave that shell somewhere without + touching it. + +Then, from an other shell (do NOT run that inside the container we just +created): + +.. code-block:: bash + :caption: Configure and build biboumi from inside the container + + docker exec biboumi-e2e sh -c "cd biboumi && mkdir docker-build/ && cd docker-build/ && cmake .." + +This is needed (only once), because if you configure it from your host +machine, then the paths generated by cmake will be all wrong when you try +to compile from inside the container and nothing will work. + +.. code-block:: bash + :caption: Re-compile and run the test suite inside the container + + docker exec biboumi-e2e sh -c "cd biboumi/docker-build && make e2e" + +This should now build everything correctly, and run the test suite. If you +want to re-run it again after you edited something in your source tree, +just run this last command again. You don’t need to touch anything inside +the container again. + +When you’re done, just close the shell we opened with the first command. + +Available functions +~~~~~~~~~~~~~~~~~~~ + +.. py:function:: send_stanza(str) + + sends one stanza to biboumi. The stanza is written entirely + as a string (with a few automatic replacements). The “from” and “to” + values have to be specified everytime, because each stanza can come from + different clients and be directed to any IRC server/channel + + .. code-block:: python + + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' type='groupchat'><body>coucou</body></message>"), + +.. py:function:: expect_stanza(xpath[, …]) + + Waits for a stanza to be received by biboumi, and checks that this + stanza matches one or more xpath. If the stanza doesn’t match all the + given xpaths, then the scenario ends and we report that as an error. + + .. code-block:: python + + expect_stanza("/message[@from='#foo@{biboumi_host}/{nick_one}']/body[text()='coucou']", + "/message/delay:delay[@from='#foo@{biboumi_host}']"), + + This waits for exactly 1 stanza, that is compared against 2 xpaths. Here + we check that it is a message, that it has the proper `from` value, the + correct body, and a <delay/>. + +.. py:function:: expect_unordered(list_of_xpaths[, list_of_xpaths, …]) + + we wait for more than one stanzas, that could be received in any order. + For example, in certain scenario, we wait for two presence stanzas, but + it’s perfectly valid to receive them in any order (one is for one + client, the other one for an other client). To do that, we pass multiple + lists of xpath. Each list can contain one or more xpath (just like + `expect_stanza`). When a stanza is received, it is compared with all the + xpaths of the first list. If it doesn’t match, it is compared with the + xpaths of the second list, and so on. If nothing matchs, it’s an error + and we stop this scenario. If the stanza matches with one of the xpath + lists, we remove that list, and we wait for the next stanza, until there + are no more xpaths. + + .. code-block:: python + + expect_unordered( + [ + "/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bernard']", + "/presence/muc_user:x/muc_user:status[@code='303']", + ], + [ + "/presence[@from='#foo%{irc_server_one}/{nick_three}'][@to='{jid_two}/{resource_one}']", + ], + [ + "/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bernard']", + "/presence/muc_user:x/muc_user:status[@code='303']", + "/presence/muc_user:x/muc_user:status[@code='110']", + ], + [ + "/presence[@from='#foo%{irc_server_one}/{nick_three}'][@to='{jid_one}/{resource_one}']", + "/presence/muc_user:x/muc_user:status[@code='110']", + ], + ), + + This will wait for 4 stanzas that could be received in any order. + +To avoid many repetitions between each tests, some helpful sequences are +available, `sequences.connection(…)` and `sequences.connection_tls(…)`. +They do all the steps that are needed (send and receive stanzas) to +connect to the component, or an IRC server. + +It’s also possible to reuse one simple scenario into an other scenario. +The most notable example is to start your own scenario with +`scenarios.simple_channel_join.scenario`, if you need your client to be in +a channel before you can start your actual scenario. For example if you +want to test the behaviour of a topic change, you need to first join a +channel. Since this is a very common patern, it’s simpler to just included +this very basic scenario at the start of your own scenarios, instead of +copy pasting the same thing over and over. + +Examples of a scenario +~~~~~~~~~~~~~~~~~~~~~~ + +First example +^^^^^^^^^^^^^ + +Here we’ll describe how to write your own scenario, from scratch. For this, we will take an existing scenario and explain how it was written, line by line. + +See for example the scenario tests/end_to_end/scenarios/self_ping_on_real_channel.py + +.. code-block:: python + + from scenarios import * + +All the tests should start with this import. It imports the file +tests/end_to_end/scenarios/__init__.py This make all the functions +available (send_stanza, expect_stanza…) available, as well as some very +common scenarios that you often need to re-use. + +.. code-block:: python + + scenario = ( + # … + ) + +This is the only required element of your scenario. This object is a tuple of function calls OR other scenarios. + +.. code-block:: python + + scenarios.simple_channel_join.scenario, + +The first line of our scenario is actually including an other existing +scenario. You can find it at +tests/end_to_end/scenarios/simple_channel_join.py As its name shows, it’s +very basic: one client {jid_one}/{resource_one} just joins one room +#foo%{irc_server_one} with the nick {nick_one}. + +Since we want to test the behaviour of a ping to ourself when we are in a +room, we just join this room without repeating everything. + +It is possible to directly insert a scenario inside our scenario without +having to extract all the steps: the test suite is smart enough to detect +that and extract the inner steps automatically. + +.. code-block:: python + + # Send a ping to ourself + 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>"), + expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"), + +Here we simple send an iq stanza, properly formatted, using the same JIDs +{jid_one}/{resource_one} and #foo%{irc_server_one}/{nick_one} to ping +ourself in the room. We them immediately expect one stanza to be received, +that is the response to our ping. It only contains one single xpath +because everything we need to check can be expressed in one line. + +Note that it is recommended to explain all the steps of your scenario with +comments. This helps understand what is being tested, and why, without +having to analyze all the stanza individually. + +.. code-block:: python + + # Now join the same room, from the same bare JID, behind the same nick + send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"), + expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='110']"), + + expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"), + +Here we send a presence stanza to join the same channel with an other +resource (note the {resource_two}). As a result, we expect two stanzas: +The first stanza (our self-presence) is checked against two xpaths, and +the second stanza (the empty subject of the room) against only one. + +.. code-block:: python + + # And re-send a self ping + send_stanza("<iq type='get' from='{jid_one}/{resource_one}' id='second_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"), + expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='second_ping']"), + ## And re-do exactly the same thing, just change the resource initiating the self ping + send_stanza("<iq type='get' from='{jid_one}/{resource_two}' id='third_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"), + expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_two}'][@id='third_ping']"), + +And finally, we test a second ping, and check that the behaviour is correct that we now have two resources in that channel. + +Second example +^^^^^^^^^^^^^^ + +Sometimes we want to do more with the received stanzas. For example we +need to extract some values from the received stanzas, to reuse them in +future stanzas we send. The most obvious example is iq IDs, that we need +to extract, to reuse them in our response. + +Let’s use for example the tests/end_to_end/scenarios/execute_incomplete_hello_adhoc_command.py scenario: + +.. code-block:: python + + from scenarios import * + + scenario = ( + 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>"), + expect_stanza("/iq[@type='result']/commands:command[@node='hello'][@sessionid][@status='executing']", + "/iq/commands:command/commands:actions/commands:complete", + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='hello']", "sessionid")) + ), + +Here is where the magic happens: as an additional argument to the +expect_stanza function, we pass an other function (callback) with the +“after=” keyword argument. This “after” callback gets called once the +expected stanza has been received and validated. Here we use +`save_value(key, value)`. This function just saves a value in our global +values that can be used with “send_stanza”, associated with the given +“key”. For example if you do `save_value("something_important", "blah")` +then you can use `{something_important}` in any future stanza that you +send and it will be replaced with “blah”. + +But this is only useful if we can save some value that we extract from the +stanza. That’s where `extract_attribute(xpath, attribute_name)` comes into +play. As the first argument, you pass an xpath corresponding to one +specific node of the XML that is received, and the second argument is just +the name of the attribute whose value you want. + +Here, we extract the value of the “sessionid=” in the node `<iq +type='result'><commands:command node='hello' sessionid='…' /></iq>`, and +we save that value, globally, with the name “sessionid”. + +.. code-block:: python + + 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='complete'><x xmlns='jabber:x:data' type='submit'></x></command></iq>"), + +Here we send a second iq, to continue our ad-hoc command, and we use {sessionid} to indicate that we are continuing the session we started before. diff --git a/doc/example.conf b/doc/example.conf deleted file mode 100644 index 42631b2..0000000 --- a/doc/example.conf +++ /dev/null @@ -1,14 +0,0 @@ -# This is an example configuration for the biboumi component. -# It only contains the default values, and some example values for the -# required fields (hostname and password). - -hostname=biboumi.example.com -password=mypassword -xmpp_server_ip=127.0.0.1 -port=5347 -admin= -realname_customization=true -realname_from_jid=false -log_file= -ca_file= -outgoing_bind= diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..819a3e5 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,24 @@ +.. biboumi documentation master file, created by + sphinx-quickstart on Mon Aug 27 19:50:26 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Biboumi – XMPP gateway to IRC +============================= + +Homepage: https://biboumi.louiz.org + +Forge: https://lab.louiz.org/louiz/biboumi + +Biboumi is an XMPP gateway that connects to IRC servers and translates +between the two protocols. It can be used to access IRC channels using any +XMPP client as if these channels were XMPP MUCs. + +.. toctree:: + :maxdepth: 2 + + install + admin + user + contributing + developer diff --git a/INSTALL.rst b/doc/install.rst index 45a860d..685511d 100644 --- a/INSTALL.rst +++ b/doc/install.rst @@ -1,24 +1,28 @@ -INSTALL -======= +Installation +============ -tl;dr ------ +The very short version: + +.. code-block:: sh - cmake . && make && ./biboumi + cmake . && make && ./biboumi If that didn’t work, read on. Dependencies ------------ -Build and runtime dependencies: +Here’s the list of all the build and runtime dependencies. Because we +strive to use the smallest number of dependencies possible, many of them +are optional. That being said, you will have the best experience using +biboumi by having all dependencies. Tools: ~~~~~~ - A C++14 compiler (clang >= 3.4 or gcc >= 5.0 for example) - CMake -- pandoc (optional) to build the man page +- sphinx (optional) to build the documentation Libraries: ~~~~~~~~~~ @@ -59,28 +63,61 @@ systemd_ (optional) Provides the support for a systemd service of Type=notify. This is useful only if you are packaging biboumi in a distribution with Systemd. - -Configure +Customize --------- -Configure the build system using cmake, there are many solutions to do that, -the simplest is to just run +The basics +~~~~~~~~~~ + +Once you have all the dependencies you need, configure the build system +using cmake. The cleanest way is to create a build directory, and run +everything inside it: + + +.. code-block:: sh + + mkdir build/ && cd build/ && cmake .. - cmake . +Choosing the dependencies +~~~~~~~~~~~~~~~~~~~~~~~~~ -in the current directory. +Without any option, cmake will look for all dependencies available on the +system and use everything it finds. If a mandatory dependency is missing +it will obviously stop and yield an error, however if an optional +dependency is missing, it will just ignore it. + +To specify that you want or don’t want to use, you need to +pass an option like this: + +.. code-block:: sh + + cmake .. -DWITH_XXXX=1 -DWITHOUT_XXXX=1 + +The `WITH_` prefix indicates that cmake should stop if that dependency can +not be found, and the `WITHOUT_` prefix indicates that this dependency +should not be used even if it is present on the system. + +The `XXXX` part needs to be replaced by one of the following: BOTAN, +LIBIDN, SYSTEMD, DOC, UDNS, SQLITE3, POSTGRESQL. + +Other options +~~~~~~~~~~~~~ The default build type is "Debug", if you want to build a release version, set the CMAKE_BUILD_TYPE variable to "release", by running this command instead: - cmake . -DCMAKE_BUILD_TYPE=release -DCMAKE_INSTALL_PREFIX=/usr +.. code-block:: sh + + cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr You can also configure many parameters of the build (like customize CFLAGS, the install path, choose the compiler, or enabling some options like the POLLER to use), using the ncurses interface of ccmake: - ccmake . +.. code-block:: sh + + ccmake .. In ccmake, first use 'c' to configure the build system, edit the values you need and finaly use 'g' to generate the Makefiles to build the system and @@ -88,7 +125,8 @@ quit ccmake. You can also configure these options using a -D command line flag. -The list of available options: +Biboumi also has a few advanced options that are useful only in very +specific cases. - POLLER: lets you select the poller used by biboumi, at compile-time. Possible values are: @@ -97,25 +135,16 @@ The list of available options: - POLL: use the standard poll(2). This is the default value on all non-Linux platforms. -- DEBUG_SQL_QUERIES: If set to ON, additional debug logging and timing will be - done for every SQL query that is executed. The default is OFF. - -- WITH_BOTAN and WITHOUT_BOTAN: The first force the usage of the Botan library, - if it is not found, the configuration process will fail. The second will - make the build process ignore the Botan library, it will not be used even - if it's available on the system. If none of these option is specified, the - library will be used if available and will be ignored otherwise. - -- WITH_LIBIDN and WITHOUT_LIBIDN: Just like the WITH(OUT)_BOTAN options, but - for the IDN library - -- WITH_SYSTEMD and WITHOUT_SYSTEMD: Just like the other WITH(OUT)_* options, - but for the Systemd library +- DEBUG_SQL_QUERIES: If set to ON, additional debug logging and timing + will be done for every SQL query that is executed. The default is OFF. + Please set it to ON if you intend to share your debug logs on the bug + trackers, if your issue affects the database. Example: - cmake . -DCMAKE_BUILD_TYPE=release -DCMAKE_INSTALL_PREFIX=/usr - -DWITH_BOTAN=1 -DWITHOUT_SYSTEMD=1 +.. code-block:: sh + + cmake . -DCMAKE_BUILD_TYPE=release -DCMAKE_INSTALL_PREFIX=/usr -DWITH_BOTAN=1 -DWITHOUT_SYSTEMD=1 This command will configure the project to build a release, with TLS enabled (using Botan) but without using Systemd (even if available on the system). @@ -123,41 +152,34 @@ This command will configure the project to build a release, with TLS enabled Build ----- + Once you’ve configured everything using cmake, build the software: -To build the biboumi binary: +To build the biboumi binary, run: + +.. code-block:: sh make Install ------- -And then, optionaly, Install the software system-wide - - make install - - -Testing -------- -You can run the test suite with - make check - -This project uses the Catch unit test framework, it will be automatically -fetched with cmake, by cloning the github repository. +And then, optionaly, Install the software system-wide -You can also check the overall code coverage of this test suite by running +.. code-block:: sh - make coverage + make install -This requires gcov and lcov to be installed. +This will install the biboumi binary, but also the man-page (if configured +with it), the policy files, the systemd unit file, etc. Run --- -Run the software using the `biboumi` binary. Read the documentation (the -man page biboumi(1) or the `biboumi.1.rst`_ file) for more information on how -to use biboumi. + +Finally, run the software using the `biboumi` binary. Read the documentation (the +man page biboumi(1)) or the usage page. .. _expat: http://expat.sourceforge.net/ .. _libiconv: http://www.gnu.org/software/libiconv/ diff --git a/doc/man_index.rst b/doc/man_index.rst new file mode 100644 index 0000000..3de981d --- /dev/null +++ b/doc/man_index.rst @@ -0,0 +1,9 @@ +Biboumi's man page index +======================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + synopsis + admin diff --git a/doc/synopsis.rst b/doc/synopsis.rst new file mode 100644 index 0000000..5b93a92 --- /dev/null +++ b/doc/synopsis.rst @@ -0,0 +1,4 @@ +Synopsis +======== + +biboumi [*config_filename*] diff --git a/doc/user.rst b/doc/user.rst new file mode 100644 index 0000000..505e3b9 --- /dev/null +++ b/doc/user.rst @@ -0,0 +1,513 @@ +###################### +End-user documentation +###################### + +Quick-start +----------- + +When a user joins an IRC channel on an IRC server (see `Join an IRC +channel`_), biboumi connects to the remote IRC server, sets the user’s nick +as requested, and then tries to join the specified channel. If the same +user subsequently tries to connect to an other channel on the same server, +the same IRC connection is used. If, however, an other user wants to join +an IRC channel on that same IRC server, biboumi opens a new connection to +that server. Biboumi connects once to each IRC servner, for each user on it. + +Additionally, if one user is using more than one clients (with the same bare +JID), they can join the same IRC channel (on the same server) behind one +single nickname. Biboumi will forward all the messages (the channel ones and +the private ones) and the presences to all the resources behind that nick. +There is no need to have multiple nicknames and multiple connections to be +able to take part in a conversation (or idle) in a channel from a mobile client +while the desktop client is still connected, for example. + +.. note:: If you use a biboumi that you have no control on: remember that the + administrator of the gateway you use is able to view all your IRC + conversations, whether you’re using encryption or not. This is exactly as + if you were running your IRC client on someone else’s server. Only use + biboumi if you trust its administrator (or, better, if you are the + administrator) or if you don’t intend to have any private conversation. + +Addressing +---------- + +IRC entities are represented by XMPP JIDs. The domain part of the JID is +the domain served by biboumi (the part after the `@`, biboumi.example.com in +the examples), and the local part (the part before the `@`) depends on the +concerned entity. + +IRC channels and IRC users have a local part formed like this: +``name`` % ``irc_server``. + +``name`` can be a channel name or an user nickname. The distinction between +the two is based on the first character: by default, if the name starts with +``'#'`` or ``'&'`` (but this can be overridden by the server, using the +ISUPPORT extension) then it’s a channel name, otherwise this is a nickname. + +There is two ways to address an IRC user, using a local part like this: +``nickname`` % ``irc_server`` or by using the in-room address of the +participant, like this: +``channel_name`` % ``irc_server`` @ ``biboumi.example.com`` / ``Nickname`` + +The second JID is available only to be compatible with XMPP clients when the +user wants to send a private message to the participant ``Nickname`` in the +room ``channel_name%irc_server@biboumi.example.com``. + +On XMPP, the node part of the JID can only be lowercase. On the other hand, +IRC nicknames are case-insensitive, this means that the nicknames toto, +Toto, tOtO and TOTO all represent the same IRC user. This means you can +talk to the user toto, and this will work. + +Also note that some IRC nicknames or channels may contain characters that +are not allowed in the local part of a JID (for example '@'). If you need +to send a message to a nick containing such a character, you can use a jid +like ``%irc.example.com@biboumi.example.com/AnnoyingNickn@me``, because +the JID ``AnnoyingNickn@me%irc.example.com@biboumi.example.com`` would not +work. This “weird” JID is just using the fact that you can send a private +message through any room (even a room with an empty name) because, on IRC, +a query does not go through any room at all, it’s just server-wide. So, +sending a message to #doesnotexist%irc@biboumi/User is exactly the same as +sending one to %irc@biboumi/User. + +And if you need to address a channel that contains such invalid characters, you +have to use `jid-escaping <http://www.xmpp.org/extensions/xep-0106.html#escaping>`_, +and replace each of these characters with their escaped version, for example to +join the channel ``#b@byfoot``, you need to use the following JID: +``#b\40byfoot%irc.example.com@biboumi.example.com``. + + +Examples: + +* ``#foo%irc.example.com@biboumi.example.com`` is the #foo IRC channel, on the + irc.example.com IRC server, and this is served by the biboumi instance on + biboumi.example.com + +* ``toto%irc.example.com@biboumi.example.com`` is the IRC user named toto, or + TotO, etc. + +* ``irc.example.com@biboumi.example.com`` is the IRC server irc.example.com. + +Note: Some JIDs are valid but make no sense in the context of +biboumi: + +* ``#test%@biboumi.example.com``, or any other JID that does not contain an + IRC server is invalid. Any message to that kind of JID will trigger an + error, or will be ignored. + +If compiled with Libidn, an IRC channel participant has a bare JID +representing the “hostname” provided by the IRC server. This JID can only +be used to set IRC modes (for example to ban a user based on its IP), or to +identify user. It cannot be used to contact that user using biboumi. + +Join an IRC channel +------------------- + +To join an IRC channel ``#foo`` on the IRC server ``irc.example.com``, +join the XMPP MUC ``#foo%irc.example.com@biboumi.example.com``. + +Connect to an IRC server +------------------------ + +The connection to the IRC server is automatically made when the user tries +to join any channel on that IRC server. The connection is closed whenever +the last channel on that server is left by the user. + +Roster +------ + +You can add some JIDs provided by biboumi into your own roster, to receive +presence from them. Biboumi will always automatically accept your requests. + +Biboumi’s JID +~~~~~~~~~~~~~ + +By adding the component JID into your roster, the user will receive an available +presence whenever it is started, and an unavailable presence whenever it is being +shutdown. This is useful to quickly view if that biboumi instance is started or +not. + +IRC server JID +~~~~~~~~~~~~~~ + +These presence will appear online in the user’s roster whenever they are +connected to that IRC server (see `Connect to an IRC server`_ for more +details). This is useful to keep track of which server an user is connected +to: this is sometimes hard to remember, when they have many clients, or if +they are using persistent channels. + +Channel messages +---------------- + +On XMPP, unlike on IRC, the displayed order of the messages is the same for +all participants of a MUC. Biboumi can not however provide this feature, as +it cannot know whether the IRC server has received and forwarded the +messages to other users. This means that the order of the messages +displayed in your XMPP client may not be the same 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 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. + +A channel history can be retrieved by using `Message archive management +(MAM) <https://xmpp.org/extensions/xep-0313.htm>`_ on the channel JID. +The results can be filtered by start and end dates. + +When a channel is joined, if the client doesn’t specify any limit, biboumi +sends the `max_history_length` last messages found in the database as the +MUC history. If a client wants to only use MAM for the archives (because +it’s more convenient and powerful), it should request to receive no +history by using an attribute maxchars='0' or maxstanzas='0' as defined in +XEP 0045, and do a proper MAM request instead. + +Note: the maxchars attribute is ignored unless its value is exactly 0. +Supporting it properly would be very hard and would introduce a lot of +complexity for almost no benefit. + +For a given channel, each user has her or his own archive. The content of +the archives are never shared, and thus a user can not use someone else’s +archive to get the messages that they didn’t receive when they were +offline. Although this feature would be very convenient, this would +introduce a very important privacy issue: for example if a biboumi gateway +is used by two users, by querying the archive one user would be able to +know whether or not the other user was in a room at a given time. + + +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 so the result stanza may be very big, unless your client +supports result set management (XEP 0059) + +Nicknames +--------- + +On IRC, nicknames are server-wide. This means that one user only has one +single nickname at one given time on all the channels of a server. This is +different from XMPP where a user can have a different nick on each MUC, +even if these MUCs are on the same server. + +This means that the nick you choose when joining your first IRC channel on +a given IRC server will be your nickname in all other channels that you +join on that same IRC server. + +If you explicitely change your nickname on one channel, your nickname will +be changed on all channels on the same server as well. Joining a new +channel with a different nick, however, will not change your nick. The +provided nick will be ignored, in order to avoid changing your nick on the +whole server by mistake. If you want to have a different nickname in the +channel you’re going to join, you need to do it explicitly with the NICK +command before joining the channel. + +Private messages +---------------- + +Private messages are handled differently on IRC and on XMPP. On IRC, you +talk directly to one server-user: toto on the channel #foo is the same user +as toto on the channel #bar (as long as these two channels are on the same +IRC server). By default you will receive private messages from the “global” +user (aka nickname%irc.example.com@biboumi.example.com), unless you +previously sent a message to an in-room participant (something like +\#test%irc.example.com@biboumi.example.com/nickname), in which case future +messages from that same user will be received from that same “in-room” JID. + +Notices +------- + +Notices are received exactly like private messages. It is not possible to +send a notice. + +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 +----------- + +If the invited JID is a user JID served by this biboumi instance, it will forward the +invitation to the target nick, over IRC. +Otherwise, the mediated instance will directly be sent to the invited JID, over XMPP. + +Example: if the user wishes to invite the IRC user “FooBar” into a room, they can +invite one of the following “JIDs” (one of them is not a JID, actually): + +- foobar%anything@biboumi.example.com +- anything@biboumi.example.com/FooBar +- FooBar + +(Note that the “anything” parts are simply ignored because they carry no +additional meaning for biboumi: we already know which IRC server is targeted +using the JID of the target channel.) + +Otherwise, any valid JID can be used, to invite any XMPP user. + +Kicks and bans +-------------- + +Kicks are transparently translated from one protocol to another. However +banning an XMPP participant has no effect. To ban an user you need to set a +mode +b on that user nick or host (see `IRC modes`_) and then kick it. + +Encoding +-------- + +On XMPP, the encoding is always ``UTF-8``, whereas on IRC the encoding of +each message can be anything. + +This means that biboumi has to convert everything coming from IRC into UTF-8 +without knowing the encoding of the received messages. To do so, it checks +if each message is UTF-8 valid, if not it tries to convert from +``iso_8859-1`` (because this appears to be the most common case, at least +on the channels I visit) to ``UTF-8``. If that conversion fails at some +point, a placeholder character ``'�'`` is inserted to indicate this +decoding error. + +Messages are always sent in UTF-8 over IRC, no conversion is done in that +direction. + +IRC modes +--------- + +One feature that doesn’t exist on XMPP but does on IRC is the ``modes``. +Although some of these modes have a correspondance in the XMPP world (for +example the ``+o`` mode on a user corresponds to the ``moderator`` role in +XMPP), it is impossible to map all these modes to an XMPP feature. To +circumvent this problem, biboumi provides a raw notification when modes are +changed, and lets the user change the modes directly. + +To change modes, simply send a message starting with “``/mode``” followed by +the modes and the arguments you want to send to the IRC server. For example +“/mode +aho louiz”. Note that your XMPP client may interprete messages +begining with “/” like a command. To actually send a message starting with +a slash, you may need to start your message with “//mode” or “/say /mode”, +depending on your client. + +When a mode is changed, the user is notified by a message coming from the +MUC bare JID, looking like “Mode #foo [+ov] [toto tutu]”. In addition, if +the mode change can be translated to an XMPP feature, the user will be +notified of this XMPP event as well. For example if a mode “+o toto” is +received, then toto’s role will be changed to moderator. The mapping +between IRC modes and XMPP features is as follow: + +``+q`` + Sets the participant’s role to ``moderator`` and its affiliation to ``owner``. + +``+a`` + Sets the participant’s role to ``moderator`` and its affiliation to ``owner``. + +``+o`` + Sets the participant’s role to ``moderator`` and its affiliation to ``admin``. + +``+h`` + Sets the participant’s role to ``moderator`` and its affiliation to ``member``. + +``+v`` + Sets the participant’s role to ``participant`` and its affiliation to ``member``. + +Similarly, when a biboumi user changes some participant's affiliation or role, biboumi translates that in an IRC mode change. + +Affiliation set to ``none`` + Sets mode to -vhoaq + +Affiliation set to ``member`` + Sets mode to +v-hoaq + +Role set to ``moderator`` + Sets mode to +h-oaq + +Affiliation set to ``admin`` + Sets mode to +o-aq + +Affiliation set to ``owner`` + Sets mode to +a-q + +Ad-hoc commands +--------------- + +Biboumi supports a few ad-hoc commands, as described in the XEP 0050. +Different ad-hoc commands are available for each JID type. + +On the gateway itself +~~~~~~~~~~~~~~~~~~~~~ + +.. note:: For example on the JID biboumi.example.com + +ping +^^^^ +Just respond “pong” + +hello +^^^^^ + +Provide a form, where the user enters their name, and biboumi responds +with a nice greeting. + +disconnect-user +^^^^^^^^^^^^^^^ + +Only available to the administrator. The user provides a list of JIDs, and +a quit message. All the selected users are disconnected from all the IRC +servers to which they were connected, using the provided quit message. + +disconnect-from-irc-servers +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Disconnect a single user from one or more IRC server. The user is +immediately disconnected by closing the socket, no message is sent to the +IRC server, but the user is of course notified with an XMPP message. The +administrator can disconnect any user, while the other users can only +disconnect themselves. + +configure +^^^^^^^^^ + +Lets each user configure some options that apply 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. If this option is set to true, all channels are persistent, + whether or not their specific value is true or false. This option is true + by default for everyone if the `persistent_by_default` configuration + option is true, otherwise it’s false. See below for more details on what a + persistent channel is. + +On a server JID +~~~~~~~~~~~~~~~ + +.. note:: For example on the JID chat.freenode.org@biboumi.example.com + +configure +^^^^^^^^^ + +Lets each user configure some options that applies to the concerned IRC +server. The provided configuration form contains these fields: + +- **Address**: This address (IPv4, IPv6 or hostname) will be used, when + biboumi connects to this server. This is a very handy way to have a + custom name for a network, and be able to edit the address to use + if one endpoint for that server is dead, but continue using the same + JID. For example, a user could configure the server + “freenode@biboumi.example.com”, set “chat.freenode.net” in its + “Address” field, and then they would be able to use “freenode” as + the network name forever: if “chat.freenode.net” breaks for some + reason, it can be changed to “irc.freenode.org” instead, and the user + would not need to change all their bookmarks and settings. +- **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 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 commands**: Raw IRC commands that will be sent + one by one 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. +- **Nickname**: A nickname that will be used instead of the nickname provided + in the initial presence sent to join a channel. This can be used if the + user always wants to have the same nickname on a given server, and not + have to bother with setting that nick in all the bookmarks on that + server. The nickname can still manually be changed with a standard nick + change presence. +- **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. +- **Throttle limit**: specifies a number of messages that can be sent + without a limit, before the throttling takes place. When messages + are throttled, only one command per second is sent to the server. + The default is 10. You can lower this value if you are ever kicked + for excess flood. If the value is 0, all messages are throttled. To + disable this feature, set it to a negative number, or an empty string. + +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 of joined IRC channel, with a +detailed list of which resource is in which channel. + +On a channel JID +~~~~~~~~~~~~~~~~ + +.. note:: For example on the JID #test%chat.freenode.org@biboumi.example.com + +configure +^^^^^^^^^ + +Lets each user configure some options that applies to the concerned IRC +channel. Some of these options, if not configured for a specific channel, +defaults to the value configured at the IRC server level. For example the +encoding can be specified for both the channel and the server. If an +encoding is not specified for a channel, the encoding configured in the +server applies. 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 +---------------- + +Biboumi tries to support as many IRC features as possible, but doesn’t +handle everything yet (or ever). In order to let the user send any +arbitrary IRC message, biboumi forwards any XMPP message received on an IRC +Server JID (see `Addressing`_) as a raw command to that IRC server. + +For example, to WHOIS the user Foo on the server irc.example.com, a user can +send the message “WHOIS Foo” to ``irc.example.com@biboumi.example.com``. + +The message will be forwarded as is, without any modification appart from +adding ``\r\n`` at the end (to make it a valid IRC message). You need to +have a little bit of understanding of the IRC protocol to use this feature. diff --git a/docker/biboumi-test/alpine/Dockerfile b/docker/biboumi-test/alpine/Dockerfile deleted file mode 100644 index e43f1b6..0000000 --- a/docker/biboumi-test/alpine/Dockerfile +++ /dev/null @@ -1,54 +0,0 @@ -# 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\ - postgresql-dev - -# 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/debian/Dockerfile b/docker/biboumi-test/debian/Dockerfile deleted file mode 100644 index 557face..0000000 --- a/docker/biboumi-test/debian/Dockerfile +++ /dev/null @@ -1,62 +0,0 @@ -# This Dockerfile creates a docker image suitable to run biboumi’s build and -# tests. For example, it can be used on with gitlab-ci. - -FROM docker.io/debian:latest - -ENV LC_ALL C.UTF-8 - -RUN apt update - -# Needed to build biboumi -RUN apt install -y g++\ - clang\ - valgrind\ - libudns-dev\ - libc-ares-dev\ - libsqlite3-dev\ - libuuid1\ - libgcrypt20-dev\ - cmake\ - make\ - libexpat1-dev\ - libidn11-dev\ - uuid-dev\ - libsystemd-dev\ - pandoc\ - libasan3\ - libubsan0\ - git\ - python3-lxml\ - lcov\ - libtool\ - python3-pip\ - python3-dev\ - automake\ - autoconf\ - flex\ - bison\ - libltdl-dev\ - openssl\ - zlib1g-dev\ - libssl-dev\ - curl\ - libpq-dev - -# 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==0.4.2 && cd slixmpp && python3 setup.py build && python3 setup.py install - -RUN useradd tester -m - -# Install charybdis, for e2e tests -RUN git clone https://github.com/charybdis-ircd/charybdis.git && cd charybdis && cd /charybdis && git checkout 4f2b9a4 && sed s/113/1113/ -i /charybdis/authd/providers/ident.c && ./autogen.sh && ./configure --prefix=/home/tester/ircd --bindir=/usr/bin && make -j8 && make install && rm -rf /charybdis - -RUN chown -R tester:tester /home/tester/ircd - -USER tester - -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 diff --git a/docker/biboumi-test/fedora/Dockerfile b/docker/biboumi-test/fedora/Dockerfile deleted file mode 100644 index 12e13e5..0000000 --- a/docker/biboumi-test/fedora/Dockerfile +++ /dev/null @@ -1,64 +0,0 @@ -# This Dockerfile creates a docker image suitable to run biboumi’s build and -# tests. For example, it can be used on with gitlab-ci. - -FROM docker.io/fedora:latest - -ENV LC_ALL C.UTF-8 - -RUN dnf --refresh install -y\ - gcc-c++\ - clang\ - valgrind\ - udns-devel\ - c-ares-devel\ - sqlite-devel\ - libuuid-devel\ - libgcrypt-devel\ - cmake\ - make\ - expat-devel\ - libidn-devel\ - uuid-devel\ - systemd-devel\ - pandoc\ - libasan\ - libubsan\ - git\ - fedora-packager\ - python3-lxml\ - lcov\ - rpmdevtools\ - python3-devel\ - automake\ - autoconf\ - flex\ - flex-devel\ - bison\ - libtool-ltdl-devel\ - libtool\ - openssl-devel\ - which\ - java-1.8.0-openjdk\ - postgresql-devel\ - && dnf clean all - -# Install botan -RUN git clone https://github.com/randombit/botan.git && cd botan && ./configure.py --prefix=/usr && make -j8 && make install && ldconfig && rm -rf /botan - -# Install slixmpp, for e2e tests -RUN git clone git://git.louiz.org/slixmpp && pip3 install pyasn1 && cd slixmpp && python3 setup.py build && python3 setup.py install - -RUN useradd tester - -# Install charybdis, for e2e tests -RUN git clone https://github.com/charybdis-ircd/charybdis.git && cd charybdis && cd /charybdis && git checkout 4f2b9a4 && sed s/113/1113/ -i /charybdis/authd/providers/ident.c && ./autogen.sh && ./configure --prefix=/home/tester/ircd --bindir=/usr/bin --with-included-boost && make -j8 && make install && rm -rf /charybdis - -RUN chown -R tester:tester /home/tester/ircd - -USER tester - -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 - -WORKDIR /home/tester diff --git a/docker/biboumi/alpine/Dockerfile b/docker/biboumi/alpine/Dockerfile index 0b59eb7..89c7223 100644 --- a/docker/biboumi/alpine/Dockerfile +++ b/docker/biboumi/alpine/Dockerfile @@ -5,44 +5,52 @@ # This is the prefered way to build the release image, used by the # end users, in production. +FROM docker.io/alpine:latest as builder + +RUN apk add --no-cache --virtual .build cmake expat-dev g++ git libidn-dev \ + make postgresql-dev python2 sqlite-dev udns-dev util-linux-dev + +RUN git clone https://github.com/randombit/botan.git && \ + cd botan && \ + ./configure.py --prefix=/usr && \ + make -j8 && \ + make install + +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 \ + -DWITH_POSTGRESQL=1 && \ + make -j8 && \ + make install + +# --- + FROM docker.io/alpine:latest -RUN apk add --no-cache\ - g++\ - cmake\ - make\ - udns-dev\ - sqlite-dev\ - postgresql-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\ - -DWITH_POSTGRESQL=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 +RUN apk add --no-cache libidn libpq libstdc++ libuuid postgresql-libs \ + sqlite-libs udns expat ca-certificates + +COPY --from=builder /usr/bin/botan /usr/bin/botan +COPY --from=builder /usr/lib/libbotan* /usr/lib/ +COPY --from=builder /usr/lib/pkgconfig/botan-2.pc /usr/lib/pkgconfig/botan-2.pc + +COPY --from=builder /etc/biboumi /etc/biboumi +COPY --from=builder /usr/bin/biboumi /usr/bin/biboumi COPY ./biboumi.cfg /etc/biboumi/biboumi.cfg -RUN chown -R biboumi:biboumi /etc/biboumi + +RUN adduser biboumi -D -h /home/biboumi && \ + mkdir /var/lib/biboumi && \ + chown -R biboumi:biboumi /var/lib/biboumi && \ + chown -R biboumi:biboumi /etc/biboumi WORKDIR /home/biboumi USER biboumi CMD ["/usr/bin/biboumi", "/etc/biboumi/biboumi.cfg"] + diff --git a/docker/test/alpine/Dockerfile b/docker/test/alpine/Dockerfile new file mode 100644 index 0000000..8ba7ddf --- /dev/null +++ b/docker/test/alpine/Dockerfile @@ -0,0 +1,35 @@ +# 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 \ +git \ +make \ +cmake \ +g++ \ +libuuid \ +udns-dev \ +expat-dev \ +libidn-dev \ +sqlite-dev \ +botan-dev \ +util-linux-dev \ +libgcrypt-dev \ +postgresql-dev \ +valgrind \ +py3-pip \ +py3-lxml \ +python3-dev \ +libffi-dev \ +go \ +wget + +# Install oragono, for e2e tests +RUN wget "https://github.com/oragono/oragono/archive/v2.0.0.tar.gz" && tar xvf "v2.0.0.tar.gz" && cd "oragono-2.0.0" && make && cp ~/go/bin/oragono /usr/local/bin + +# Install slixmpp, for e2e tests +RUN git clone https://github.com/saghul/aiodns.git && cd aiodns && git checkout 7ee13f9bea25784322~ && python3 setup.py build && python3 setup.py install && git clone git://git.louiz.org/slixmpp && pip3 install pyasn1 && cd slixmpp && python3 setup.py build && python3 setup.py install diff --git a/docker/test/debian/Dockerfile b/docker/test/debian/Dockerfile new file mode 100644 index 0000000..35578b9 --- /dev/null +++ b/docker/test/debian/Dockerfile @@ -0,0 +1,38 @@ +# This Dockerfile creates a docker image suitable to run biboumi’s build and +# tests. For example, it can be used on with gitlab-ci. + +FROM docker.io/debian:buster + +ENV LC_ALL C.UTF-8 + +RUN apt update + +# Needed to build biboumi +RUN apt install -y --no-install-recommends \ +git \ +make \ +cmake \ +g++ \ +libuuid1 \ +libudns-dev \ +libexpat1-dev \ +libidn11-dev \ +libsqlite3-dev \ +libbotan-2-dev \ +libsystemd-dev \ +uuid-dev \ +libgcrypt20-dev \ +libpq-dev \ +valgrind \ +libasan5 \ +libubsan0 \ +python3-pip \ +python3-lxml \ +python3-dev \ +wget + +RUN wget "https://github.com/oragono/oragono/releases/download/v2.0.0/oragono-2.0.0-linux-x64.tar.gz" && tar xvf oragono-2.0.0-linux-x64.tar.gz && cp oragono-2.0.0-linux-x64/oragono /usr/local/bin + +# Install slixmpp, for e2e tests +RUN git clone https://github.com/saghul/aiodns.git && cd aiodns && git checkout 7ee13f9bea25784322~ && python3 setup.py build && python3 setup.py install && git clone git://git.louiz.org/slixmpp && pip3 install pyasn1 && cd slixmpp && python3 setup.py build && python3 setup.py install + diff --git a/docker/test/fedora/Dockerfile b/docker/test/fedora/Dockerfile new file mode 100644 index 0000000..7b8d7bf --- /dev/null +++ b/docker/test/fedora/Dockerfile @@ -0,0 +1,40 @@ +# This Dockerfile creates a docker image suitable to run biboumi’s build and +# tests. For example, it can be used on with gitlab-ci. + +FROM docker.io/fedora:32 + +ENV LC_ALL C.UTF-8 + +RUN dnf --refresh install -y \ +git \ +make \ +cmake \ +gcc-c++ \ +uuid-devel \ +udns-devel \ +expat-devel \ +libidn-devel \ +sqlite-devel \ +botan2-devel \ +systemd-devel \ +libuuid-devel \ +libgcrypt-devel \ +postgresql-devel \ +lcov \ +libasan \ +libubsan \ +valgrind \ +python3-pip \ +python3-lxml \ +python3-devel \ +python3-sphinx \ +wget \ +fedora-packager \ +rpmdevtools \ +&& dnf clean all + +# Install slixmpp, for e2e tests +RUN git clone git://git.louiz.org/slixmpp && pip3 install pyasn1 && cd slixmpp && python3 setup.py build && python3 setup.py install + +# Install oragono, for e2e tests +RUN wget "https://github.com/oragono/oragono/releases/download/v2.0.0/oragono-2.0.0-linux-x64.tar.gz" && tar xvf oragono-2.0.0-linux-x64.tar.gz && cp oragono-2.0.0-linux-x64/oragono /usr/local/bin diff --git a/images/biboumi.svg b/images/biboumi.svg new file mode 100644 index 0000000..0e6b625 --- /dev/null +++ b/images/biboumi.svg @@ -0,0 +1,31 @@ +<?xml version="1.0"?> +<svg version="1.1" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"> + <defs> + <clipPath id="b"> + <circle cx="82.809" cy="196.97" r="43.879" fill="#ff7c0f" fill-opacity=".76078" stroke-width=".26458"/> + </clipPath> + <clipPath id="d"> + <path d="m180.52 259.24-37.156-4.869c-12.066-10.349-20.924-19.652-26.575-27.907-5.6403-8.171-8.9557-16.371-9.9462-24.601-1.1233-9.3327 1.6749-19.655 8.3945-30.966 0.44431-0.86032 0.7801-1.4844 1.0074-1.8721 0.346-0.53919 0.79032-1.3995 1.333-2.581 7.0452-12.02 10.088-22.018 9.1279-29.993-0.89862-7.4661-4.5007-15.203-10.806-23.211-6.1663-7.9894-15.525-16.898-28.076-26.726l37.156 4.869c12.066 10.349 20.919 19.609 26.559 27.78 5.7897 8.2741 34.422 7.0601 35.413 15.29 1.1233 9.3327 28.762 21.791 21.913 33.169-0.45452 0.77548 25.236 0.0424 25.008 0.43013 83.542 6.6717-29.704 19.89-30.256 20.987-7.0452 12.02-29.545 19.651-28.585 27.626 0.8782 7.2964-29.003 7.4922-22.857 15.312 6.1561 7.9046 15.605 16.992 28.346 27.263z" fill="#fc0" stroke-width="8.2789"/> + </clipPath> + <clipPath id="a"> + <circle cx="82.809" cy="196.97" r="43.879" fill="#ff7c0f" fill-opacity=".76078" stroke-width=".26458"/> + </clipPath> + <clipPath id="c"> + <path transform="matrix(.094728 .78705 -1.2909 -.16917 0 0)" d="m296-63.677v28.782c-11.319 8.5161-21.829 14.607-31.531 18.272-9.594 3.6651-3.2417 31.956-13.698 31.956-11.858 0-29.785-0.63152-45.524-6.9916-1.1858-0.43119-18.636-1.7681-19.175-1.9837-0.75459-0.3234-31.977 22.705-22.21-1.0329-16.709-6.6835-5.957-9.4736-16.09-9.4736-9.4863 0-20.045-26.704-29.316-22.5-9.2707 4.0963-19.188 10.618-29.752 19.565v-28.782c11.319-8.5161 21.775-14.607 31.369-18.272 9.7018-3.7729 19.781-5.6594 30.237-5.6594 11.858 0 25.71 3.234 41.556 9.7019 1.078 0.43119 1.8865 0.75459 2.4255 0.97019 0.86239 0.3234 2.0482 0.75459 3.5574 1.2936 16.709 6.6835 30.13 10.025 40.263 10.025 9.2707 0 18.434-2.0482 27.489-6.1445 9.1629-4.0963 19.296-10.672 30.399-19.727z" fill="#d45500" stroke-width="8.2789"/> + </clipPath> + </defs> + <g transform="translate(0,-97)"> + <g transform="matrix(1.5456 0 0 1.7393 -48.852 -89.968)" clip-path="url(#c)"> + <circle cx="89.102" cy="164.99" r="57.494" fill="#f0c" fill-opacity=".76078" stroke-width=".32058"/> + <circle cx="109.9" cy="164.99" r="57.494" fill="#ff7c0f" fill-opacity=".76078" stroke-width=".32058"/> + <circle transform="matrix(1.4205 0 0 1.3103 -6.6791 -93.099)" cx="66.934" cy="196.97" r="43.879" clip-path="url(#b)" fill="#333" fill-opacity=".25521" stroke-width=".24466"/> + <circle transform="matrix(1.3103 0 0 1.3103 1.4023 -93.099)" cx="66.934" cy="196.97" r="43.879" clip-path="url(#b)" fill="#333" stroke-width=".24466"/> + </g> + <g transform="matrix(1.5456 0 0 1.7393 -58.728 -89.971)" clip-path="url(#d)"> + <circle cx="89.102" cy="164.99" r="57.494" fill="#f0c" fill-opacity=".76078" stroke-width=".32058"/> + <circle cx="109.9" cy="164.99" r="57.494" fill="#ff7c0f" fill-opacity=".76078" stroke-width=".32058"/> + <circle transform="matrix(1.4205 0 0 1.3103 -6.6791 -93.099)" cx="66.934" cy="196.97" r="43.879" clip-path="url(#a)" fill="#333" fill-opacity=".25521" stroke-width=".24466"/> + <circle transform="matrix(1.3103 0 0 1.3103 1.4023 -93.099)" cx="66.934" cy="196.97" r="43.879" clip-path="url(#a)" fill="#333" stroke-width=".24466"/> + </g> + </g> +</svg> diff --git a/packaging/biboumi.spec.cmake b/packaging/biboumi.spec.cmake index 45f9376..bd8ffdc 100644 --- a/packaging/biboumi.spec.cmake +++ b/packaging/biboumi.spec.cmake @@ -13,9 +13,10 @@ BuildRequires: libuuid-devel BuildRequires: systemd-devel BuildRequires: sqlite-devel BuildRequires: postgresql-devel +BuildRequires: botan2-devel BuildRequires: cmake BuildRequires: systemd -BuildRequires: pandoc +BuildRequires: python3-sphinx %global _hardened_build 1 @@ -37,14 +38,14 @@ cmake . -DCMAKE_CXX_FLAGS="%{optflags}" \ -DCMAKE_BUILD_TYPE=release \ -DCMAKE_INSTALL_PREFIX=/usr \ -DPOLLER=EPOLL \ - -DWITHOUT_BOTAN=1 \ + -DWITH_BOTAN=1 \ -DWITH_SYSTEMD=1 \ -DWITH_LIBIDN=1 \ -DWITH_SQLITE3=1 \ -DWITH_POSTGRESQL=1 make %{?_smp_mflags} - +make man SPHINXBUILD=sphinx-build-3 %install make install DESTDIR=%{buildroot} @@ -57,12 +58,17 @@ make check %{?_smp_mflags} %files %{_bindir}/%{name} %{_mandir}/man1/%{name}.1* -%doc README.rst COPYING doc/biboumi.1.rst +%doc README.rst COPYING doc/*.rst %{_unitdir}/%{name}.service %config(noreplace) %{biboumi_confdir}/*policy.txt %changelog +* ${RPM_DATE} Le Coz Florent <louiz@louiz.org> - ${RPM_VERSION}-1 +- Build latest git revision +- Build against botan2 +- Build with sphinx instead of pandoc + * Sat May 9 2020 Le Coz Florent <louiz@louiz.org> - 8.5-1 Update to version 8.5 @@ -72,10 +78,10 @@ make check %{?_smp_mflags} * Wed Jun 1 2018 Le Coz Florent <louiz@louiz.org> - 8.3-1 Update to version 8.3 -* Wed May 25 2018 Le Coz Florent <louiz@louiz.org> - 8.2-1 +* Fri May 25 2018 Le Coz Florent <louiz@louiz.org> - 8.2-1 Update to version 8.2 -* Wed May 14 2018 Le Coz Florent <louiz@louiz.org> - 8.1-1 +* Mon May 14 2018 Le Coz Florent <louiz@louiz.org> - 8.1-1 Update to version 8.1 * Wed May 2 2018 Le Coz Florent <louiz@louiz.org> - 8.0-1 @@ -84,7 +90,7 @@ make check %{?_smp_mflags} * Wed Jan 24 2018 Le Coz Florent <louiz@louiz.org> - 7.2-1 Update to version 7.2 -* Wed Jan 22 2018 Le Coz Florent <louiz@louiz.org> - 7.1-1 +* Mon Jan 22 2018 Le Coz Florent <louiz@louiz.org> - 7.1-1 Update to version 7.1 * Wed Jan 17 2018 Le Coz Florent <louiz@louiz.org> - 7.0-1 diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index 7a0157a..424c72a 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -63,7 +63,8 @@ void Bridge::shutdown(const std::string& exit_message) { for (auto& pair: this->irc_clients) { - pair.second->send_quit_command(exit_message); + std::unique_ptr<IrcClient>& irc = pair.second; + irc->send_quit_command(exit_message); } } @@ -133,11 +134,11 @@ IrcClient* Bridge::make_irc_client(const std::string& hostname, const std::strin realname = this->get_bare_jid(); } this->irc_clients.emplace(hostname, - std::make_shared<IrcClient>(this->poller, hostname, + std::make_unique<IrcClient>(this->poller, hostname, nickname, username, realname, jid.domain, *this)); - std::shared_ptr<IrcClient> irc = this->irc_clients.at(hostname); + std::unique_ptr<IrcClient>& irc = this->irc_clients.at(hostname); return irc.get(); } } @@ -169,8 +170,7 @@ IrcClient* Bridge::find_irc_client(const std::string& hostname) const bool Bridge::join_irc_channel(const Iid& iid, std::string nickname, const std::string& password, const std::string& resource, - HistoryLimit history_limit, - const bool force_join) + HistoryLimit history_limit) { const auto& hostname = iid.get_server(); #ifdef USE_DATABASE @@ -184,18 +184,18 @@ bool Bridge::join_irc_channel(const Iid& iid, std::string nickname, auto res_in_chan = this->is_resource_in_chan(ChannelKey{iid.get_local(), hostname}, resource); if (!res_in_chan) this->add_resource_to_chan(ChannelKey{iid.get_local(), hostname}, resource); - if (irc->is_channel_joined(iid.get_local()) == false) + if (!irc->is_channel_joined(iid.get_local())) { irc->send_join_command(iid.get_local(), password); return true; - } else if (!res_in_chan || force_join) { - // See https://github.com/xsf/xeps/pull/499 for the force_join argument + } else { + // See https://github.com/xsf/xeps/pull/499 this->generate_channel_join_for_resource(iid, resource); } return false; } -void Bridge::send_channel_message(const Iid& iid, const std::string& body, std::string id) +void Bridge::send_channel_message(const Iid& iid, const std::string& body, std::string id, std::vector<XmlNode> nodes_to_reflect) { if (iid.get_server().empty()) { @@ -223,6 +223,33 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: bool first = true; for (const std::string& line: lines) { + std::string uuid; +#ifdef USE_DATABASE + const auto xmpp_body = this->make_xmpp_body(line); + if (this->record_history) + uuid = Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(), + std::get<0>(xmpp_body), irc->get_own_nick()); +#endif + if (!first || id.empty()) + id = utils::gen_uuid(); + + MessageCallback mirror_to_all_resources = [this, iid, uuid, id, nodes_to_reflect](const IrcClient* irc, const IrcMessage& message) { + std::string line = message.arguments[1]; + // “temporary” workaround for \01ACTION…\01 -> /me messages + if ((line.size() > strlen("\01ACTION\01")) && + (line.substr(0, 7) == "\01ACTION") && line[line.size() - 1] == '\01') + line = "/me " + line.substr(8, line.size() - 9); + for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) + { + auto stanza = this->xmpp.make_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(line), + this->user_jid + "/" + + resource, uuid, id); + for (const auto& node: nodes_to_reflect) + stanza.add_child(node); + this->xmpp.send_stanza(stanza); + } + }; + if (line.substr(0, 5) == "/mode") { std::vector<std::string> args = utils::split(line.substr(5), ' ', false); @@ -231,22 +258,11 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: // XMPP user, that’s not a textual message. } else if (line.substr(0, 4) == "/me ") - irc->send_channel_message(iid.get_local(), action_prefix + line.substr(4) + "\01"); + irc->send_channel_message(iid.get_local(), action_prefix + line.substr(4) + "\01", + std::move(mirror_to_all_resources)); else - irc->send_channel_message(iid.get_local(), line); + irc->send_channel_message(iid.get_local(), line, std::move(mirror_to_all_resources)); - std::string uuid; -#ifdef USE_DATABASE - const auto xmpp_body = this->make_xmpp_body(line); - if (this->record_history) - uuid = Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(), - std::get<0>(xmpp_body), irc->get_own_nick()); -#endif - if (!first || id.empty()) - id = utils::gen_uuid(); - 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, uuid, id); first = false; } } @@ -449,9 +465,8 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con true, true, resource, irc); 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); - + 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, const std::string& requesting_resource) @@ -737,10 +752,27 @@ void Bridge::send_irc_participant_ping_request(const Iid& iid, const std::string IrcChannel* chan = irc->get_channel(iid.get_local()); 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", + this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "not-acceptable", "", true); return; } + if (chan->get_self()->nick == nick) + { + // XEP-0410 self-ping optimisation: always reply without going the full + // round-trip through IRC and possibly another XMPP client. See the XEP + // for details. + Jid iq_from(from_jid); + iq_from.local = std::to_string(iid); + iq_from.resource = nick; + + Stanza iq("iq"); + iq["from"] = iq_from.full(); + iq["to"] = to_jid; + iq["id"] = iq_id; + iq["type"] = "result"; + this->xmpp.send_stanza(iq); + return; + } if (chan->get_self()->nick != nick && !chan->find_user(nick)) { this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "item-not-found", @@ -816,7 +848,7 @@ void Bridge::send_irc_version_request(const std::string& irc_hostname, const std this->add_waiting_irc(std::move(cb)); } -void Bridge::send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc) +void Bridge::send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc, const bool log) { const auto encoding = in_encoding_for(*this, iid); std::string uuid{}; @@ -824,14 +856,18 @@ 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) + if (log && this->record_history) uuid = Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(), std::get<0>(xmpp_body), nick); +#else + (void)log; #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, uuid, utils::gen_uuid()); + auto stanza = this->xmpp.make_muc_message(std::to_string(iid), nick, this->make_xmpp_body(body, encoding), + this->user_jid + "/" + + resource, uuid, utils::gen_uuid()); + this->xmpp.send_stanza(stanza); } } else @@ -891,9 +927,7 @@ void Bridge::send_muc_leave(const Iid& iid, const IrcUser& user, for (const auto& r: resources_in_chan) if (this->number_of_channels_the_resource_is_in(iid.get_server(), r) == 0) this->remove_resource_from_server(iid.get_server(), r); - } - } IrcClient* irc = this->find_irc_client(iid.get_server()); if (self && irc && irc->number_of_joined_channels() == 0) @@ -967,8 +1001,18 @@ void Bridge::send_user_join(const std::string& hostname, const std::string& chan std::string encoded_chan_name(chan_name); xep0106::encode(encoded_chan_name); - this->xmpp.send_user_join(encoded_chan_name + utils::empty_if_fixed_server("%" + hostname), user->nick, user->host, - affiliation, role, this->user_jid + "/" + resource, self); + std::string encoded_nick_name(user->nick); + xep0106::encode(encoded_nick_name); + + std::string full_jid = + encoded_nick_name + utils::empty_if_fixed_server("%" + hostname) + + "@" + this->xmpp.get_served_hostname(); + if (!user->host.empty()) + full_jid += "/" + user->host; + + this->xmpp.send_user_join(encoded_chan_name + utils::empty_if_fixed_server("%" + hostname), + user->nick, full_jid, affiliation, role, + this->user_jid + "/" + resource, self); } void Bridge::send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic, @@ -1001,11 +1045,13 @@ void Bridge::send_room_history(const std::string& hostname, const std::string& c void Bridge::send_room_history(const std::string& hostname, std::string chan_name, const std::string& resource, const HistoryLimit& history_limit) { #ifdef USE_DATABASE - const auto coptions = Database::get_irc_channel_options_with_server_and_global_default(this->user_jid, hostname, chan_name); - auto limit = coptions.col<Database::MaxHistoryLength>(); + const auto goptions = Database::get_global_options(this->user_jid); + auto limit = goptions.col<Database::MaxHistoryLength>(); + if (limit < 0) + limit = 20; if (history_limit.stanzas >= 0 && history_limit.stanzas < limit) limit = history_limit.stanzas; - const auto result = Database::get_muc_logs(this->user_jid, chan_name, hostname, limit, history_limit.since, {}, Id::unset_value, Database::Paging::last); + const auto result = Database::get_muc_logs(this->user_jid, chan_name, hostname, static_cast<std::size_t>(limit), history_limit.since, {}, Id::unset_value, Database::Paging::last); const auto& lines = std::get<1>(result); chan_name.append(utils::empty_if_fixed_server("%" + hostname)); for (const auto& line: lines) @@ -1142,12 +1188,12 @@ void Bridge::trigger_on_irc_message(const std::string& irc_hostname, const IrcMe } } -std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients() +std::unordered_map<std::string, std::unique_ptr<IrcClient>>& Bridge::get_irc_clients() { return this->irc_clients; } -const std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients() const +const std::unordered_map<std::string, std::unique_ptr<IrcClient>>& Bridge::get_irc_clients() const { return this->irc_clients; } @@ -1214,15 +1260,6 @@ void Bridge::remove_resource_from_server(const Bridge::IrcHostname& irc_hostname } } -bool Bridge::is_resource_in_server(const Bridge::IrcHostname& irc_hostname, const std::string& resource) const -{ - auto it = this->resources_in_server.find(irc_hostname); - if (it != this->resources_in_server.end()) - if (it->second.count(resource) == 1) - return true; - return false; -} - std::size_t Bridge::number_of_resources_in_chan(const Bridge::ChannelKey& channel) const { auto it = this->resources_in_chan.find(channel); diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp index 8e7d9d7..6b15478 100644 --- a/src/bridge/bridge.hpp +++ b/src/bridge/bridge.hpp @@ -72,16 +72,14 @@ public: **/ /** - * Try to join an irc_channel, does nothing and return true if the channel - * was already joined. + * Try to join an irc_channel. */ bool join_irc_channel(const Iid& iid, std::string nickname, const std::string& password, const std::string& resource, - HistoryLimit history_limit, - const bool force_join); + HistoryLimit history_limit); - void send_channel_message(const Iid& iid, const std::string& body, std::string id); + void send_channel_message(const Iid& iid, const std::string& body, std::string id, std::vector<XmlNode> nodes_to_reflect); 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); @@ -164,9 +162,9 @@ public: void send_room_history(const std::string& hostname, const std::string& chan_name, const HistoryLimit& history_limit); void send_room_history(const std::string& hostname, std::string chan_name, const std::string& resource, const HistoryLimit& history_limit); /** - * Send a MUC message from some participant + * Send a message from a MUC participant or a direct message */ - void send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc); + void send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc, const bool log=true); /** * Send a presence of type error, from a room. */ @@ -241,8 +239,8 @@ public: * iq_responder_callback_t and remove the callback from the list. */ void trigger_on_irc_message(const std::string& irc_hostname, const IrcMessage& message); - std::unordered_map<std::string, std::shared_ptr<IrcClient>>& get_irc_clients(); - const std::unordered_map<std::string, std::shared_ptr<IrcClient>>& get_irc_clients() const; + std::unordered_map<std::string, std::unique_ptr<IrcClient>>& get_irc_clients(); + const std::unordered_map<std::string, std::unique_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); @@ -275,7 +273,7 @@ private: * One IrcClient for each IRC server we need to be connected to. * The pointer is shared by the bridge and the poller. */ - std::unordered_map<std::string, std::shared_ptr<IrcClient>> irc_clients; + std::unordered_map<std::string, std::unique_ptr<IrcClient>> irc_clients; /** * To communicate back with the XMPP component */ @@ -316,13 +314,14 @@ private: */ void add_resource_to_chan(const ChannelKey& channel, const std::string& resource); void remove_resource_from_chan(const ChannelKey& channel, const std::string& resource); +public: bool is_resource_in_chan(const ChannelKey& channel, const std::string& resource) const; +private: 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); - bool is_resource_in_server(const IrcHostname& irc_hostname, const std::string& resource) const; size_t number_of_channels_the_resource_is_in(const std::string& irc_hostname, const std::string& resource) const; /** diff --git a/src/bridge/history_limit.hpp b/src/bridge/history_limit.hpp index 9c75256..93e36e1 100644 --- a/src/bridge/history_limit.hpp +++ b/src/bridge/history_limit.hpp @@ -1,5 +1,7 @@ #pragma once +#include <string> + // Default values means no limit struct HistoryLimit { diff --git a/src/database/count_query.hpp b/src/database/count_query.hpp index 118ce44..3990eb9 100644 --- a/src/database/count_query.hpp +++ b/src/database/count_query.hpp @@ -8,10 +8,10 @@ struct CountQuery: public Query { - CountQuery(std::string name): + CountQuery(const std::string& name): Query("SELECT count(*) FROM ") { - this->body += std::move(name); + this->body += name; } int64_t execute(DatabaseEngine& db) diff --git a/src/database/database.cpp b/src/database/database.cpp index 6e08ee1..861abcb 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -162,10 +162,6 @@ Database::IrcChannelOptions Database::get_irc_channel_options_with_server_and_gl 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; } @@ -338,7 +334,6 @@ Transaction::Transaction() log_error("Failed to create SQL transaction: ", std::get<std::string>(result)); else this->success = true; - } Transaction::~Transaction() diff --git a/src/database/database.hpp b/src/database/database.hpp index 3e25b30..a53f87b 100644 --- a/src/database/database.hpp +++ b/src/database/database.hpp @@ -63,8 +63,8 @@ class Database struct EncodingIn: Column<std::string> { static constexpr auto name = "encodingin_"; }; - struct MaxHistoryLength: Column<int> { static constexpr auto name = "maxhistorylength_"; - MaxHistoryLength(): Column<int>(20) {} }; + struct MaxHistoryLength: Column<std::int64_t> { static constexpr auto name = "maxhistorylength_"; + MaxHistoryLength(): Column<std::int64_t>(20) {} }; struct RecordHistory: Column<bool> { static constexpr auto name = "recordhistory_"; RecordHistory(): Column<bool>(true) {}}; @@ -86,13 +86,16 @@ class Database struct Address: Column<std::string> { static constexpr auto name = "address_"; }; + struct ThrottleLimit: Column<std::int64_t> { static constexpr auto name = "throttlelimit_"; + ThrottleLimit(): Column<std::int64_t>(10) {} }; + using MucLogLineTable = Table<Id, Uuid, Owner, IrcChanName, IrcServerName, Date, Body, Nick>; using MucLogLine = MucLogLineTable::RowType; using GlobalOptionsTable = Table<Id, Owner, MaxHistoryLength, RecordHistory, GlobalPersistent>; using GlobalOptions = GlobalOptionsTable::RowType; - using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength, Address, Nick>; + using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength, Address, Nick, ThrottleLimit>; using IrcServerOptions = IrcServerOptionsTable::RowType; using IrcChannelOptionsTable = Table<Id, Owner, Server, Channel, EncodingOut, EncodingIn, MaxHistoryLength, Persistent, RecordHistoryOptional>; diff --git a/src/database/postgresql_statement.hpp b/src/database/postgresql_statement.hpp index 37e8ea0..345a942 100644 --- a/src/database/postgresql_statement.hpp +++ b/src/database/postgresql_statement.hpp @@ -15,7 +15,7 @@ class PostgresqlStatement: public Statement body(std::move(body)), conn(conn) {} - ~PostgresqlStatement() + virtual ~PostgresqlStatement() { PQclear(this->result); this->result = nullptr; @@ -89,8 +89,6 @@ class PostgresqlStatement: public Statement return true; } - private: - private: bool execute(const bool second_attempt=false) { @@ -119,11 +117,7 @@ private: PQreset(this->conn); return this->execute(true); } - else - { - log_error("Givin up."); - return false; - } + return false; } return true; } diff --git a/src/database/query.cpp b/src/database/query.cpp index d72066e..5ec8599 100644 --- a/src/database/query.cpp +++ b/src/database/query.cpp @@ -6,11 +6,6 @@ void actual_bind(Statement& statement, const std::string& value, int index) statement.bind_text(index, value); } -void actual_bind(Statement& statement, const std::int64_t& value, int index) -{ - statement.bind_int64(index, value); -} - void actual_bind(Statement& statement, const OptionalBool& value, int index) { if (!value.is_set) diff --git a/src/database/query.hpp b/src/database/query.hpp index ba28b1a..c89371f 100644 --- a/src/database/query.hpp +++ b/src/database/query.hpp @@ -12,13 +12,14 @@ #include <string> void actual_bind(Statement& statement, const std::string& value, int index); -void actual_bind(Statement& statement, const std::int64_t& value, int index); -template <typename T, typename std::enable_if_t<std::is_integral<T>::value>* = 0> +void actual_bind(Statement& statement, const OptionalBool& value, int index); +template <typename T> void actual_bind(Statement& statement, const T& value, int index) { - actual_bind(statement, static_cast<std::int64_t>(value), index); + static_assert(std::is_integral<T>::value, + "Only a string, an optional-bool or an integer can be used."); + statement.bind_int64(index, static_cast<std::int64_t>(value)); } -void actual_bind(Statement& statement, const OptionalBool& value, int index); #ifdef DEBUG_SQL_QUERIES #include <utils/scopetimer.hpp> diff --git a/src/database/row.hpp b/src/database/row.hpp index 1253f93..4004b5d 100644 --- a/src/database/row.hpp +++ b/src/database/row.hpp @@ -28,7 +28,7 @@ struct Row this->clear_col<0>(); } - std::tuple<T...> columns; + std::tuple<T...> columns{}; std::string table_name; private: diff --git a/src/database/select_query.hpp b/src/database/select_query.hpp index b9fdc06..e372f2e 100644 --- a/src/database/select_query.hpp +++ b/src/database/select_query.hpp @@ -135,7 +135,7 @@ struct SelectQuery: public Query }; template <typename... T> -auto select(const Table<T...> table) +auto select(const Table<T...>& table) { SelectQuery<T...> query(table.name); return query; diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp index d7fa2cd..de38d42 100644 --- a/src/irc/irc_client.cpp +++ b/src/irc/irc_client.cpp @@ -135,7 +135,7 @@ IrcClient::IrcClient(std::shared_ptr<Poller>& poller, std::string hostname, std::string realname, std::string user_hostname, Bridge& bridge): TCPClientSocketHandler(poller), - hostname(std::move(hostname)), + hostname(hostname), user_hostname(std::move(user_hostname)), username(std::move(username)), realname(std::move(realname)), @@ -143,7 +143,14 @@ IrcClient::IrcClient(std::shared_ptr<Poller>& poller, std::string hostname, bridge(bridge), welcomed(false), chanmodes({"", "", "", ""}), - chantypes({'#', '&'}) + chantypes({'#', '&'}), + tokens_bucket(this->get_throttle_limit(), 1s, [this]() { + if (message_queue.empty()) + return true; + this->actual_send(std::move(this->message_queue.front())); + this->message_queue.pop_front(); + return false; + }, "TokensBucket" + this->hostname + this->bridge.get_jid()) { #ifdef USE_DATABASE auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), @@ -171,6 +178,7 @@ IrcClient::~IrcClient() // This event may or may not exist (if we never got connected, it // doesn't), but it's ok TimedEventsManager::instance().cancel("PING" + this->hostname + this->bridge.get_jid()); + TimedEventsManager::instance().cancel("TokensBucket" + this->hostname + this->bridge.get_jid()); } void IrcClient::start() @@ -317,9 +325,21 @@ IrcChannel* IrcClient::get_channel(const std::string& n) } catch (const std::out_of_range& exception) { - this->channels.emplace(name, std::make_unique<IrcChannel>()); + return this->channels.emplace(name, std::make_unique<IrcChannel>()).first->second.get(); + } +} + +const IrcChannel* IrcClient::find_channel(const std::string& n) const +{ + const std::string name = utils::tolower(n); + try + { return this->channels.at(name).get(); } + catch (const std::out_of_range& exception) + { + return nullptr; + } } bool IrcClient::is_channel_joined(const std::string& name) @@ -378,25 +398,39 @@ void IrcClient::parse_in_buffer(const size_t) } } -void IrcClient::send_message(IrcMessage&& message) +void IrcClient::actual_send(std::pair<IrcMessage, MessageCallback>&& message_pair) { - log_debug("IRC SENDING: (", this->get_hostname(), ") ", message); - std::string res; - if (!message.prefix.empty()) - res += ":" + std::move(message.prefix) + " "; - res += message.command; - for (const std::string& arg: message.arguments) - { - if (arg.find(' ') != std::string::npos || - (!arg.empty() && arg[0] == ':')) - { - res += " :" + arg; - break; - } - res += " " + arg; - } - res += "\r\n"; - this->send_data(std::move(res)); + const IrcMessage& message = message_pair.first; + const MessageCallback& callback = message_pair.second; + log_debug("IRC SENDING: (", this->get_hostname(), ") ", message); + std::string res; + if (!message.prefix.empty()) + res += ":" + message.prefix + " "; + res += message.command; + for (const std::string& arg: message.arguments) + { + if (arg.find(' ') != std::string::npos + || (!arg.empty() && arg[0] == ':')) + { + res += " :" + arg; + break; + } + res += " " + arg; + } + res += "\r\n"; + this->send_data(std::move(res)); + + if (callback) + callback(this, message); + } + +void IrcClient::send_message(IrcMessage message, MessageCallback callback, bool throttle) +{ + auto message_pair = std::make_pair(std::move(message), std::move(callback)); + if (this->tokens_bucket.use_token() || !throttle) + this->actual_send(std::move(message_pair)); + else + message_queue.push_back(std::move(message_pair)); } void IrcClient::send_raw(const std::string& txt) @@ -447,12 +481,12 @@ void IrcClient::send_topic_command(const std::string& chan_name, const std::stri void IrcClient::send_quit_command(const std::string& reason) { - this->send_message(IrcMessage("QUIT", {reason})); + this->send_message(IrcMessage("QUIT", {reason}), {}, false); } void IrcClient::send_join_command(const std::string& chan_name, const std::string& password) { - if (this->welcomed == false) + if (!this->welcomed) { 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; }); @@ -466,10 +500,11 @@ void IrcClient::send_join_command(const std::string& chan_name, const std::strin this->start(); } -bool IrcClient::send_channel_message(const std::string& chan_name, const std::string& body) +bool IrcClient::send_channel_message(const std::string& chan_name, const std::string& body, + MessageCallback callback) { IrcChannel* channel = this->get_channel(chan_name); - if (channel->joined == false) + if (!channel->joined) { log_warning("Cannot send message to channel ", chan_name, ", it is not joined"); return false; @@ -489,7 +524,7 @@ bool IrcClient::send_channel_message(const std::string& chan_name, const std::st ::strlen(":!@ PRIVMSG ") - chan_name.length() - ::strlen(" :\r\n"); const auto lines = cut(body, line_size); for (const auto& line: lines) - this->send_message(IrcMessage("PRIVMSG", {chan_name, line})); + this->send_message(IrcMessage("PRIVMSG", {chan_name, line}), callback); return true; } @@ -1123,8 +1158,6 @@ void IrcClient::on_channel_bad_key(const IrcMessage& message) void IrcClient::on_channel_mode(const IrcMessage& message) { - // For now, just transmit the modes so the user can know what happens - // TODO, actually interprete the mode. Iid iid; iid.set_local(message.arguments[0]); iid.set_server(this->hostname); @@ -1142,7 +1175,7 @@ void IrcClient::on_channel_mode(const IrcMessage& message) } this->bridge.send_message(iid, "", "Mode " + iid.get_local() + " [" + mode_arguments + "] by " + user.nick, - true); + true, this->is_channel_joined(iid.get_local())); const IrcChannel* channel = this->get_channel(iid.get_local()); if (!channel) return; @@ -1215,6 +1248,11 @@ void IrcClient::on_channel_mode(const IrcMessage& message) } } +void IrcClient::set_throttle_limit(long int limit) +{ + this->tokens_bucket.set_limit(limit); +} + void IrcClient::on_user_mode(const IrcMessage& message) { this->bridge.send_xmpp_message(this->hostname, "", @@ -1252,3 +1290,12 @@ bool IrcClient::abort_on_invalid_cert() const return true; } #endif + +long int IrcClient::get_throttle_limit() const +{ +#ifdef USE_DATABASE + return Database::get_irc_server_options(this->bridge.get_bare_jid(), this->hostname).col<Database::ThrottleLimit>(); +#else + return 10; +#endif +} diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp index fd97fe6..cfb3d21 100644 --- a/src/irc/irc_client.hpp +++ b/src/irc/irc_client.hpp @@ -16,8 +16,14 @@ #include <vector> #include <string> #include <stack> +#include <deque> #include <map> #include <set> +#include <utils/tokens_bucket.hpp> + +class IrcClient; + +using MessageCallback = std::function<void(const IrcClient*, const IrcMessage&)>; class Bridge; @@ -28,7 +34,7 @@ class Bridge; class IrcClient: public TCPClientSocketHandler { public: - explicit IrcClient(std::shared_ptr<Poller>& poller, std::string 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); @@ -68,6 +74,10 @@ public: */ IrcChannel* get_channel(const std::string& name); /** + * Return the channel with this name. Nullptr if it is not found + */ + const IrcChannel* find_channel(const std::string& name) const; + /** * Returns true if the channel is joined */ bool is_channel_joined(const std::string& name); @@ -80,8 +90,9 @@ public: * (actually, into our out_buf and signal the poller that we want to wach * for send events to be ready) */ - void send_message(IrcMessage&& message); + void send_message(IrcMessage message, MessageCallback callback={}, bool throttle=true); void send_raw(const std::string& txt); + void actual_send(std::pair<IrcMessage, MessageCallback>&& message_pair); /** * Send the PONG irc command */ @@ -110,7 +121,8 @@ public: * Send a PRIVMSG command for a channel * Return true if the message was actually sent */ - bool send_channel_message(const std::string& chan_name, const std::string& body); + bool send_channel_message(const std::string& chan_name, const std::string& body, + MessageCallback callback); /** * Send a PRIVMSG command for an user */ @@ -289,7 +301,7 @@ public: const std::vector<char>& get_sorted_user_modes() const { return this->sorted_user_modes; } std::set<char> get_chantypes() const { return this->chantypes; } - + void set_throttle_limit(long int limit); /** * Store the history limit that the client asked when joining this room. */ @@ -327,6 +339,10 @@ private: */ Bridge& bridge; /** + * Where messaged are stored when they are throttled. + */ + std::deque<std::pair<IrcMessage, MessageCallback>> message_queue{}; + /** * The list of joined channels, indexed by name */ std::unordered_map<std::string, std::unique_ptr<IrcChannel>> channels; @@ -385,6 +401,8 @@ private: * the WebIRC protocole. */ Resolver dns_resolver; + TokensBucket tokens_bucket; + long int get_throttle_limit() const; }; diff --git a/src/irc/irc_message.hpp b/src/irc/irc_message.hpp index fe954e4..269a12a 100644 --- a/src/irc/irc_message.hpp +++ b/src/irc/irc_message.hpp @@ -14,9 +14,9 @@ public: ~IrcMessage() = default; IrcMessage(const IrcMessage&) = delete; - IrcMessage(IrcMessage&&) = delete; + IrcMessage(IrcMessage&&) = default; IrcMessage& operator=(const IrcMessage&) = delete; - IrcMessage& operator=(IrcMessage&&) = delete; + IrcMessage& operator=(IrcMessage&&) = default; std::string prefix; std::string command; diff --git a/src/main.cpp b/src/main.cpp index 59fda4e..2448197 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -55,45 +55,8 @@ static void sigusr_handler(int, siginfo_t*, void*) reload.store(true); } -int main(int ac, char** av) +static void setup_signals() { - if (ac > 1) - { - const std::string arg = av[1]; - if (arg.size() >= 2 && arg[0] == '-' && arg[1] == '-') - { - if (arg == "--help") - return display_help(); - else - { - std::cerr << "Unknow command line option: " << arg << std::endl; - return 1; - } - } - } - const std::string conf_filename = ac > 1 ? av[1] : xdg_config_path("biboumi.cfg"); - std::cout << "Using configuration file: " << conf_filename << std::endl; - - if (!Config::read_conf(conf_filename)) - return config_help(""); - - const std::string password = Config::get("password", ""); - if (password.empty()) - return config_help("password"); - const std::string hostname = Config::get("hostname", ""); - if (hostname.empty()) - return config_help("hostname"); - - -#ifdef USE_DATABASE - try { - open_database(); - } catch (const std::exception& e) { - log_error(e.what()); - return 1; - } -#endif - // Block the signals we want to manage. They will be unblocked only during // the epoll_pwait or ppoll calls. This avoids some race conditions, // explained in man 2 pselect on linux @@ -103,6 +66,7 @@ int main(int ac, char** av) sigaddset(&mask, SIGTERM); sigaddset(&mask, SIGUSR1); sigaddset(&mask, SIGUSR2); + sigaddset(&mask, SIGHUP); sigprocmask(SIG_BLOCK, &mask, nullptr); // Install the signals used to exit the process cleanly, or reload the @@ -113,7 +77,7 @@ int main(int ac, char** av) sigfillset(&on_sigint.sa_mask); // we want to catch that signal only once. // Sending SIGINT again will "force" an exit - on_sigint.sa_flags = SA_RESETHAND; + on_sigint.sa_flags = 0 & SA_RESETHAND; sigaction(SIGINT, &on_sigint, nullptr); sigaction(SIGTERM, &on_sigint, nullptr); @@ -124,7 +88,11 @@ int main(int ac, char** av) on_sigusr.sa_flags = 0; sigaction(SIGUSR1, &on_sigusr, nullptr); sigaction(SIGUSR2, &on_sigusr, nullptr); + sigaction(SIGHUP, &on_sigusr, nullptr); +} +static int main_loop(std::string hostname, std::string password) +{ auto p = std::make_shared<Poller>(); #ifdef UDNS_FOUND @@ -161,7 +129,7 @@ int main(int ac, char** av) dns_handler.destroy(); #endif if (identd) - identd->shutdown(); + identd->shutdown(); // Cancel the timer for a potential reconnection TimedEventsManager::instance().cancel("XMPP reconnection"); } @@ -204,7 +172,7 @@ int main(int ac, char** av) dns_handler.destroy(); #endif if (identd) - identd->shutdown(); + identd->shutdown(); } } // If the only existing connection is the one to the XMPP component: @@ -223,3 +191,51 @@ int main(int ac, char** av) log_info("All connections cleanly closed, have a nice day."); return 0; } + +int main(int ac, char** av) +{ + if (ac > 1) + { + const std::string arg = av[1]; + if (arg.size() >= 2 && arg[0] == '-' && arg[1] == '-') + { + if (arg == "--help") + return display_help(); + else + { + std::cerr << "Unknow command line option: " << arg + << std::endl; + return 1; + } + } + } + const std::string conf_filename = + ac > 1 ? av[1]: xdg_config_path("biboumi.cfg"); + std::cout << "Using configuration file: " << conf_filename << std::endl; + + if (!Config::read_conf(conf_filename)) + return config_help(""); + + const std::string password = Config::get("password", ""); + if (password.empty()) + return config_help("password"); + const std::string hostname = Config::get("hostname", ""); + if (hostname.empty()) + return config_help("hostname"); + +#ifdef USE_DATABASE + try + { + open_database(); + } + catch (const std::exception& e) + { + log_error(e.what()); + return 1; + } +#endif + + setup_signals(); + + return main_loop(std::move(hostname), std::move(password)); +} diff --git a/src/network/credentials_manager.cpp b/src/network/credentials_manager.cpp index b25f442..89c694c 100644 --- a/src/network/credentials_manager.cpp +++ b/src/network/credentials_manager.cpp @@ -21,9 +21,8 @@ static const std::vector<std::string> default_cert_files = { Botan::Certificate_Store_In_Memory BasicCredentialsManager::certificate_store; bool BasicCredentialsManager::certs_loaded = false; -BasicCredentialsManager::BasicCredentialsManager(const TCPSocketHandler* const socket_handler): +BasicCredentialsManager::BasicCredentialsManager(): Botan::Credentials_Manager(), - socket_handler(socket_handler), trusted_fingerprint{} { BasicCredentialsManager::load_certs(); diff --git a/src/network/credentials_manager.hpp b/src/network/credentials_manager.hpp index 3a37bdc..210a628 100644 --- a/src/network/credentials_manager.hpp +++ b/src/network/credentials_manager.hpp @@ -25,7 +25,7 @@ void check_tls_certificate(const std::vector<Botan::X509_Certificate>& certs, class BasicCredentialsManager: public Botan::Credentials_Manager { public: - BasicCredentialsManager(const TCPSocketHandler* const socket_handler); + BasicCredentialsManager(); BasicCredentialsManager(BasicCredentialsManager&&) = delete; BasicCredentialsManager(const BasicCredentialsManager&) = delete; @@ -38,7 +38,6 @@ public: const std::string& get_trusted_fingerprint() const; private: - const TCPSocketHandler* const socket_handler; static bool try_to_open_one_ca_bundle(const std::vector<std::string>& paths); static void load_certs(); diff --git a/src/network/resolver.cpp b/src/network/resolver.cpp index ae5cecd..d9242e2 100644 --- a/src/network/resolver.cpp +++ b/src/network/resolver.cpp @@ -214,6 +214,12 @@ void Resolver::on_hostname6_resolved(dns_rr_a6 *result) 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::after_resolved() diff --git a/src/network/tcp_client_socket_handler.cpp b/src/network/tcp_client_socket_handler.cpp index dcf38f9..7d1029f 100644 --- a/src/network/tcp_client_socket_handler.cpp +++ b/src/network/tcp_client_socket_handler.cpp @@ -46,15 +46,14 @@ void TCPClientSocketHandler::init_socket(const struct addrinfo* rp) else { utils::ScopeGuard sg([result](){ freeaddrinfo(result); }); - struct addrinfo* rp; - for (rp = result; rp; rp = rp->ai_next) + for (; result; result = result->ai_next) { if ((::bind(this->socket, - reinterpret_cast<const struct sockaddr*>(rp->ai_addr), - rp->ai_addrlen)) == 0) + reinterpret_cast<const struct sockaddr*>(result->ai_addr), + result->ai_addrlen)) == 0) break; } - if (!rp) + if (!result) log_error("Failed to bind socket to ", this->bind_addr, ": ", strerror(errno)); else diff --git a/src/network/tcp_socket_handler.cpp b/src/network/tcp_socket_handler.cpp index 642cf03..e05caad 100644 --- a/src/network/tcp_socket_handler.cpp +++ b/src/network/tcp_socket_handler.cpp @@ -50,7 +50,7 @@ TCPSocketHandler::TCPSocketHandler(std::shared_ptr<Poller>& poller): SocketHandler(poller, -1), use_tls(false) #ifdef BOTAN_FOUND - ,credential_manager(this) + ,credential_manager() #endif {} @@ -84,10 +84,11 @@ void TCPSocketHandler::plain_recv() if (recv_buf == nullptr) recv_buf = buf; - const ssize_t size = this->do_recv(recv_buf, buf_size); + const ssize_t ssize = this->do_recv(recv_buf, buf_size); - if (size > 0) + if (ssize > 0) { + auto size = static_cast<std::size_t>(ssize); if (buf == recv_buf) { // data needs to be placed in the in_buf string, because no buffer @@ -149,21 +150,22 @@ void TCPSocketHandler::on_send() } else { + auto size = static_cast<std::size_t>(res); // remove all the strings that were successfully sent. auto it = this->out_buf.begin(); while (it != this->out_buf.end()) { - if (static_cast<size_t>(res) >= it->size()) + if (size >= it->size()) { - res -= it->size(); + size -= it->size(); ++it; } else { // If one string has partially been sent, we use substr to // crop it - if (res > 0) - *it = it->substr(res, std::string::npos); + if (size > 0) + *it = it->substr(size, std::string::npos); break; } } @@ -332,6 +334,11 @@ void TCPSocketHandler::tls_verify_cert_chain(const std::vector<Botan::X509_Certi Botan::Usage_Type usage, const std::string& hostname, const Botan::TLS::Policy& policy) { + if (!this->policy.verify_certificate) + { + log_debug("Not verifying certificate due to domain policy "); + return; + } log_debug("Checking remote certificate for hostname ", hostname); try { diff --git a/src/network/tls_policy.cpp b/src/network/tls_policy.cpp index b88eb88..f32557e 100644 --- a/src/network/tls_policy.cpp +++ b/src/network/tls_policy.cpp @@ -37,6 +37,8 @@ void BiboumiTLSPolicy::load(std::istream& is) // 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 if (pair.first == "verify_certificate") + this->verify_certificate = !(pair.second == "0" || utils::tolower(pair.second) == "false"); else this->set(pair.first, pair.second); } diff --git a/src/network/tls_policy.hpp b/src/network/tls_policy.hpp index 29fd2b3..e915646 100644 --- a/src/network/tls_policy.hpp +++ b/src/network/tls_policy.hpp @@ -21,6 +21,7 @@ public: BiboumiTLSPolicy &operator=(BiboumiTLSPolicy &&) = delete; bool require_cert_revocation_info() const override; + bool verify_certificate{true}; protected: bool req_cert_revocation_info{true}; }; diff --git a/src/utils/dirname.cpp b/src/utils/dirname.cpp index 71c9c38..a304117 100644 --- a/src/utils/dirname.cpp +++ b/src/utils/dirname.cpp @@ -2,7 +2,7 @@ namespace utils { - std::string dirname(const std::string filename) + std::string dirname(const std::string& filename) { if (filename.empty()) return "./"; diff --git a/src/utils/dirname.hpp b/src/utils/dirname.hpp index c1df81b..c13393d 100644 --- a/src/utils/dirname.hpp +++ b/src/utils/dirname.hpp @@ -1,6 +1,8 @@ +#pragma once + #include <string> namespace utils { -std::string dirname(const std::string filename); +std::string dirname(const std::string& filename); } diff --git a/src/utils/encoding.cpp b/src/utils/encoding.cpp index cff0039..8532292 100644 --- a/src/utils/encoding.cpp +++ b/src/utils/encoding.cpp @@ -48,16 +48,16 @@ namespace utils if (codepoint_size == 4) { if (!str[1] || !str[2] || !str[3] - || ((str[1] & 0b11000000) != 0b10000000) - || ((str[2] & 0b11000000) != 0b10000000) - || ((str[3] & 0b11000000) != 0b10000000)) + || ((str[1] & 0b11000000u) != 0b10000000u) + || ((str[2] & 0b11000000u) != 0b10000000u) + || ((str[3] & 0b11000000u) != 0b10000000u)) return false; } else if (codepoint_size == 3) { if (!str[1] || !str[2] - || ((str[1] & 0b11000000) != 0b10000000) - || ((str[2] & 0b11000000) != 0b10000000)) + || ((str[1] & 0b11000000u) != 0b10000000u) + || ((str[2] & 0b11000000u) != 0b10000000u)) return false; } else if (codepoint_size == 2) @@ -81,7 +81,7 @@ namespace utils // pointer where we write valid chars char* r = res.data(); - const char* str = original.c_str(); + const unsigned char* str = reinterpret_cast<const unsigned char*>(original.c_str()); std::bitset<20> codepoint; while (*str) @@ -89,10 +89,10 @@ namespace utils // 4 bytes: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx if ((str[0] & 0b11111000) == 0b11110000) { - codepoint = ((str[0] & 0b00000111) << 18); - codepoint |= ((str[1] & 0b00111111) << 12); - codepoint |= ((str[2] & 0b00111111) << 6 ); - codepoint |= ((str[3] & 0b00111111) << 0 ); + codepoint = ((str[0] & 0b00000111u) << 18u); + codepoint |= ((str[1] & 0b00111111u) << 12u); + codepoint |= ((str[2] & 0b00111111u) << 6u ); + codepoint |= ((str[3] & 0b00111111u) << 0u ); if (codepoint.to_ulong() <= 0x10FFFF) { ::memcpy(r, str, 4); @@ -103,9 +103,9 @@ namespace utils // 3 bytes: 1110xxx 10xxxxxx 10xxxxxx else if ((str[0] & 0b11110000) == 0b11100000) { - codepoint = ((str[0] & 0b00001111) << 12); - codepoint |= ((str[1] & 0b00111111) << 6); - codepoint |= ((str[2] & 0b00111111) << 0 ); + codepoint = ((str[0] & 0b00001111u) << 12u); + codepoint |= ((str[1] & 0b00111111u) << 6u); + codepoint |= ((str[2] & 0b00111111u) << 0u ); if (codepoint.to_ulong() <= 0xD7FF || (codepoint.to_ulong() >= 0xE000 && codepoint.to_ulong() <= 0xFFFD)) { diff --git a/src/utils/get_first_non_empty.cpp b/src/utils/get_first_non_empty.cpp index 5b3bedb..17585b1 100644 --- a/src/utils/get_first_non_empty.cpp +++ b/src/utils/get_first_non_empty.cpp @@ -1,11 +1,8 @@ #include <utils/get_first_non_empty.hpp> +template <> bool is_empty(const std::string& val) { return val.empty(); } -bool is_empty(const int& val) -{ - return val == 0; -} diff --git a/src/utils/get_first_non_empty.hpp b/src/utils/get_first_non_empty.hpp index 2e1828d..6129b63 100644 --- a/src/utils/get_first_non_empty.hpp +++ b/src/utils/get_first_non_empty.hpp @@ -2,8 +2,13 @@ #include <string> +template <typename T> +bool is_empty(const T& val) +{ + return val == 0; +} +template <> bool is_empty(const std::string& val); -bool is_empty(const int& val); template <typename T> T& get_first_non_empty(T&& last) diff --git a/src/utils/optional_bool.hpp b/src/utils/optional_bool.hpp index 867aca2..3d00d23 100644 --- a/src/utils/optional_bool.hpp +++ b/src/utils/optional_bool.hpp @@ -6,7 +6,7 @@ struct OptionalBool { OptionalBool() = default; - OptionalBool(bool value): + explicit OptionalBool(bool value): is_set(true), value(value) {} void set_value(bool value) diff --git a/src/utils/string.cpp b/src/utils/string.cpp index 635e71a..366ec1f 100644 --- a/src/utils/string.cpp +++ b/src/utils/string.cpp @@ -15,11 +15,11 @@ std::vector<std::string> cut(const std::string& val, const std::size_t size) // Get the number of chars, <= size, that contain only whole // UTF-8 codepoints. std::size_t s = 0; - auto codepoint_size = utils::get_next_codepoint_size(val[pos + s]); + auto codepoint_size = utils::get_next_codepoint_size(static_cast<unsigned char>(val[pos + s])); while (s + codepoint_size <= size && pos + s < val.size()) { s += codepoint_size; - codepoint_size = utils::get_next_codepoint_size(val[pos + s]); + codepoint_size = utils::get_next_codepoint_size(static_cast<unsigned char>(val[pos + s])); } res.emplace_back(val.substr(pos, s)); pos += s; diff --git a/src/utils/time.cpp b/src/utils/time.cpp index 71306fd..d848e70 100644 --- a/src/utils/time.cpp +++ b/src/utils/time.cpp @@ -1,9 +1,8 @@ #include <utils/time.hpp> -#include <ctime> +#include <time.h> #include <sstream> #include <iomanip> -#include <locale> #include "biboumi.h" @@ -12,9 +11,10 @@ namespace utils std::string to_string(const std::chrono::system_clock::time_point::rep& time) { constexpr std::size_t stamp_size = 21; - const std::time_t timestamp = static_cast<std::time_t>(time); + const auto timestamp = static_cast<std::time_t>(time); char date_buf[stamp_size]; - if (std::strftime(date_buf, stamp_size, "%FT%TZ", std::gmtime(×tamp)) != stamp_size - 1) + struct tm tm; + if (std::strftime(date_buf, stamp_size, "%FT%TZ", gmtime_r(×tamp, &tm)) != stamp_size - 1) return ""; return {std::begin(date_buf), std::end(date_buf) - 1}; } diff --git a/src/utils/tokens_bucket.hpp b/src/utils/tokens_bucket.hpp new file mode 100644 index 0000000..263359a --- /dev/null +++ b/src/utils/tokens_bucket.hpp @@ -0,0 +1,60 @@ +/** + * Implementation of the token bucket algorithm. + * + * It uses a repetitive TimedEvent, started at construction, to fill the + * bucket. + * + * Every n seconds, it executes the given callback. If the callback + * returns true, we add a token (if the limit is not yet reached). + * + */ + +#pragma once + +#include <utils/timed_events.hpp> +#include <logger/logger.hpp> + +class TokensBucket +{ +public: + TokensBucket(long int max_size, std::chrono::milliseconds fill_duration, std::function<bool()> callback, std::string name): + limit(max_size), + tokens(static_cast<std::size_t>(limit)), + callback(std::move(callback)) + { + log_debug("creating TokensBucket with max size: ", max_size); + TimedEvent event(std::move(fill_duration), [this]() { this->add_token(); }, std::move(name)); + TimedEventsManager::instance().add_event(std::move(event)); + } + + bool use_token() + { + if (this->limit < 0) + return true; + if (this->tokens > 0) + { + this->tokens--; + return true; + } + else + return false; + } + + void set_limit(long int limit) + { + this->limit = limit; + } + +private: + long int limit; + std::size_t tokens; + std::function<bool()> callback; + + void add_token() + { + if (this->limit < 0) + return; + if (this->callback() && this->tokens != static_cast<decltype(this->tokens)>(this->limit)) + this->tokens++; + } +}; diff --git a/src/xmpp/adhoc_commands_handler.cpp b/src/xmpp/adhoc_commands_handler.cpp index bc4c108..ff4c1e5 100644 --- a/src/xmpp/adhoc_commands_handler.cpp +++ b/src/xmpp/adhoc_commands_handler.cpp @@ -80,7 +80,10 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co { command_node["status"] = "executing"; XmlSubNode actions(command_node, "actions"); - XmlSubNode next(actions, "next"); + if (session.remaining_steps() == 1) + XmlSubNode next(actions, "complete"); + else + XmlSubNode next(actions, "next"); } } else if (session_it != this->sessions.end() && action == "cancel") diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp index 3bd2e5a..113943c 100644 --- a/src/xmpp/biboumi_adhoc_commands.cpp +++ b/src/xmpp/biboumi_adhoc_commands.cpp @@ -15,10 +15,17 @@ #ifdef USE_DATABASE #include <database/database.hpp> #include <database/save.hpp> + +static void set_desc(XmlSubNode& field, const char* text) +{ + XmlSubNode desc(field, "desc"); + desc.set_inner(text); +} + #endif #ifndef HAS_PUT_TIME -#include <ctime> +# include <time.h> #endif using namespace std::string_literals; @@ -116,6 +123,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman auto options = Database::get_global_options(owner.bare()); + command_node.delete_all_children(); XmlSubNode x(command_node, "jabber:x:data:x"); x["type"] = "form"; XmlSubNode title(x, "title"); @@ -128,7 +136,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman 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"; + set_desc(max_histo_length, "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>())); @@ -140,7 +148,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman 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"; + set_desc(record_history, "Whether to save the messages into the database, or not"); { XmlSubNode value(record_history, "value"); value.set_name("value"); @@ -156,7 +164,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman persistent["var"] = "persistent"; persistent["type"] = "boolean"; persistent["label"] = "Make all channels persistent"; - persistent["desc"] = "If true, all channels will be persistent"; + set_desc(persistent, "If true, all channels will be persistent"); { XmlSubNode value(persistent, "value"); value.set_name("value"); @@ -184,7 +192,13 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session, if (field->get_tag("var") == "max_history_length" && value && !value->get_inner().empty()) - options.col<Database::MaxHistoryLength>() = atoi(value->get_inner().data()); + { + try { + options.col<Database::MaxHistoryLength>() = std::stol(value->get_inner().data()); + } catch (const std::logic_error&) { + options.col<Database::MaxHistoryLength>() = 20; + } + } else if (field->get_tag("var") == "record_history" && value && !value->get_inner().empty()) { @@ -223,6 +237,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com server_domain); auto commands = Database::get_after_connection_commands(options); + command_node.delete_all_children(); XmlSubNode x(command_node, "jabber:x:data:x"); x["type"] = "form"; XmlSubNode title(x, "title"); @@ -236,7 +251,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com field["var"] = "address"; field["type"] = "text-single"; field["label"] = "Address"; - field["desc"] = "The address (hostname or IP) to connect to."; + set_desc(field, "The address (hostname or IP) to connect to."); XmlSubNode value(field, "value"); if (options.col<Database::Address>().empty()) value.set_inner(server_domain); @@ -249,7 +264,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com ports["var"] = "ports"; ports["type"] = "text-multi"; ports["label"] = "Ports"; - ports["desc"] = "List of ports to try, without TLS. Defaults: 6667."; + set_desc(ports, "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"); @@ -263,7 +278,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com 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."; + set_desc(tls_ports, "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"); @@ -276,7 +291,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com 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"; + set_desc(verify_cert, "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"); @@ -302,7 +317,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com field["var"] = "nick"; field["type"] = "text-single"; field["label"] = "Nickname"; - field["desc"] = "If set, will override the nickname provided in the initial presence sent to join the first server channel"; + set_desc(field, "If set, will override the nickname provided in the initial presence sent to join the first server channel"); if (!options.col<Database::Nick>().empty()) { XmlSubNode value(field, "value"); @@ -315,7 +330,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com pass["var"] = "pass"; pass["type"] = "text-private"; pass["label"] = "Server password"; - pass["desc"] = "Will be used in a PASS command when connecting"; + set_desc(pass, "Will be used in a PASS command when connecting"); if (!options.col<Database::Pass>().empty()) { XmlSubNode pass_value(pass, "value"); @@ -327,7 +342,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com XmlSubNode after_cnt_cmd(x, "field"); after_cnt_cmd["var"] = "after_connect_commands"; after_cnt_cmd["type"] = "text-multi"; - after_cnt_cmd["desc"] = "Custom IRC commands sent after the connection is established with the server."; + set_desc(after_cnt_cmd, "Custom IRC commands sent after the connection is established with the server."); after_cnt_cmd["label"] = "After-connection IRC commands"; for (const auto& command: commands) { @@ -364,10 +379,28 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com } { + XmlSubNode throttle_limit(x, "field"); + throttle_limit["var"] = "throttle_limit"; + throttle_limit["type"] = "text-single"; + throttle_limit["label"] = "Throttle limit"; + XmlSubNode value(throttle_limit, "value"); + value.set_inner(std::to_string(options.col<Database::ThrottleLimit>())); + } + + { + XmlSubNode max_history_length(x, "field"); + max_history_length["var"] = "max_history_length"; + max_history_length["type"] = "text-single"; + max_history_length["label"] = "Throttle limit"; + XmlSubNode value(max_history_length, "value"); + value.set_inner(std::to_string(options.col<Database::MaxHistoryLength>())); + } + + { 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."; + set_desc(encoding_out, "The encoding used when sending messages to the IRC server."); encoding_out["label"] = "Out encoding"; if (!options.col<Database::EncodingOut>().empty()) { @@ -380,7 +413,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com 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."; + set_desc(encoding_in, "The encoding used to decode message received from the IRC server."); encoding_in["label"] = "In encoding"; if (!options.col<Database::EncodingIn>().empty()) { @@ -390,8 +423,10 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com } } -void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node) +void ConfigureIrcServerStep2(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node) { + auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component); + const XmlNode* x = command_node.get_child("x", "jabber:x:data"); if (x) { @@ -472,6 +507,31 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com else if (field->get_tag("var") == "realname" && value) options.col<Database::Realname>() = value->get_inner(); + else if (field->get_tag("var") == "throttle_limit" && value) + { + try { + options.col<Database::ThrottleLimit>() = std::stol(value->get_inner()); + } catch (const std::logic_error&) { + options.col<Database::ThrottleLimit>() = 10; + } + Bridge* bridge = biboumi_component.find_user_bridge(session.get_owner_jid()); + if (bridge) + { + IrcClient* client = bridge->find_irc_client(server_domain); + if (client) + client->set_throttle_limit(options.col<Database::ThrottleLimit>()); + } + } + + else if (field->get_tag("var") == "max_history_length" && value) + { + try { + options.col<Database::MaxHistoryLength>() = std::stol(value->get_inner()); + } catch (const std::logic_error&) { + options.col<Database::MaxHistoryLength>() = 20; + } + } + else if (field->get_tag("var") == "encoding_out" && value) options.col<Database::EncodingOut>() = value->get_inner(); @@ -509,6 +569,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, auto options = Database::get_irc_channel_options_with_server_default(requester.local + "@" + requester.domain, iid.get_server(), iid.get_local()); + node.delete_all_children(); XmlSubNode x(node, "jabber:x:data:x"); x["type"] = "form"; XmlSubNode title(x, "title"); @@ -521,7 +582,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, 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"; + set_desc(record_history, "If unset, the value is the one configured globally"); { // Value selected by default XmlSubNode value(record_history, "value"); @@ -541,7 +602,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, 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"; + set_desc(encoding_out, "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()) { @@ -554,7 +615,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, 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"; + set_desc(encoding_in, "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()) { @@ -567,7 +628,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, 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."; + set_desc(persistent, "If set to true, when all XMPP clients have left this channel, biboumi will stay idle in it, without sending a PART command."); persistent["label"] = "Persistent"; { XmlSubNode value(persistent, "value"); @@ -847,12 +908,13 @@ void GetIrcConnectionInfoStep1(XmppComponent& component, AdhocSession& session, if (irc->is_using_tls()) ss << " (using TLS)"; const std::time_t now_c = std::chrono::system_clock::to_time_t(irc->connection_date); + struct tm tm; #ifdef HAS_PUT_TIME - ss << " since " << std::put_time(std::localtime(&now_c), "%F %T"); + ss << " since " << std::put_time(localtime_r(&now_c, &tm), "%F %T"); #else constexpr std::size_t timestamp_size{10 + 1 + 8 + 1}; char buf[timestamp_size] = {}; - const auto res = std::strftime(buf, timestamp_size, "%F %T", std::localtime(&now_c)); + const auto res = std::strftime(buf, timestamp_size, "%F %T", localtime(&now_c, &tm)); if (res > 0) ss << " since " << buf; #endif diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp index be34873..f49b3b6 100644 --- a/src/xmpp/biboumi_component.cpp +++ b/src/xmpp/biboumi_component.cpp @@ -102,8 +102,8 @@ void BiboumiComponent::shutdown() void BiboumiComponent::clean() { - auto it = this->bridges.begin(); - while (it != this->bridges.end()) + auto it = std::begin(this->bridges); + while (it != std::end(this->bridges)) { it->second->clean(); if (it->second->active_clients() == 0) @@ -158,35 +158,51 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) { const std::string own_nick = bridge->get_own_nick(iid); const XmlNode* x = stanza.get_child("x", MUC_NS); - const XmlNode* password = x ? x->get_child("password", MUC_NS): nullptr; - const XmlNode* history = x ? x->get_child("history", MUC_NS): nullptr; - HistoryLimit history_limit; - if (history) + const IrcClient* irc = bridge->find_irc_client(iid.get_server()); + // if there is no <x/>, this is a presence status update, we don’t care about those + if (x) { - const auto seconds = history->get_tag("seconds"); - if (!seconds.empty()) + const XmlNode* password = x->get_child("password", MUC_NS); + const XmlNode* history = x->get_child("history", MUC_NS); + HistoryLimit history_limit; + if (history) { - const auto now = std::chrono::system_clock::now(); - std::time_t timestamp = std::chrono::system_clock::to_time_t(now); - int int_seconds = std::atoi(seconds.data()); - timestamp -= int_seconds; - history_limit.since = utils::to_string(timestamp); + const auto seconds = history->get_tag("seconds"); + if (!seconds.empty()) + { + const auto now = std::chrono::system_clock::now(); + std::time_t timestamp = std::chrono::system_clock::to_time_t(now); + int int_seconds = std::atoi(seconds.data()); + timestamp -= int_seconds; + history_limit.since = utils::to_string(timestamp); + } + const auto since = history->get_tag("since"); + if (!since.empty()) + history_limit.since = since; + const auto maxstanzas = history->get_tag("maxstanzas"); + if (!maxstanzas.empty()) + history_limit.stanzas = std::atoi(maxstanzas.data()); + // Ignore any other value, because this is too complex to implement, + // so I won’t do it. + if (history->get_tag("maxchars") == "0") + history_limit.stanzas = 0; + } + bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "", + from.resource, history_limit); + } + else + { + if (irc) + { + const auto chan = irc->find_channel(iid.get_local()); + if (chan && chan->joined) + bridge->send_irc_nick_change(iid, to.resource, from.resource); + else + { // send an error if we are not joined yet, instead of treating it as a join + this->send_stanza_error("presence", from_str, to_str, id, "modify", "not-acceptable", "You are not joined to this MUC."); + } } - const auto since = history->get_tag("since"); - if (!since.empty()) - history_limit.since = since; - const auto maxstanzas = history->get_tag("maxstanzas"); - if (!maxstanzas.empty()) - history_limit.stanzas = std::atoi(maxstanzas.data()); - // Ignore any other value, because this is too complex to implement, - // so I won’t do it. - if (history->get_tag("maxchars") == "0") - history_limit.stanzas = 0; } - bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "", - from.resource, history_limit, x != nullptr); - if (!own_nick.empty() && own_nick != to.resource) - bridge->send_irc_nick_change(iid, to.resource, from.resource); } else if (type == "unavailable") { @@ -273,9 +289,10 @@ void BiboumiComponent::handle_message(const Stanza& stanza) std::string error_type("cancel"); std::string error_name("internal-server-error"); - utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name](){ + std::string error_text{}; + utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name, &error_text](){ this->send_stanza_error("message", from_str, to_str, id, - error_type, error_name, ""); + error_type, error_name, error_text); }); const XmlNode* body = stanza.get_child("body", COMPONENT_NS); @@ -284,7 +301,34 @@ void BiboumiComponent::handle_message(const Stanza& stanza) { if (body && !body->get_inner().empty()) { - bridge->send_channel_message(iid, body->get_inner(), id); + if (bridge->is_resource_in_chan(iid.to_tuple(), from.resource)) + { + // Extract some XML nodes that we must include in the + // reflection (if any), because XMPP says so + std::vector<XmlNode> nodes_to_reflect; + const XmlNode* origin_id = stanza.get_child("origin-id", STABLE_ID_NS); + if (origin_id) + nodes_to_reflect.push_back(*origin_id); + const auto own_address = std::to_string(iid) + '@' + this->served_hostname; + for (const XmlNode* stanza_id: stanza.get_children("stanza-id", STABLE_ID_NS)) + { + // Stanza ID generating entities, which encounter a + // <stanza-id/> element where the 'by' attribute matches + // the 'by' attribute they would otherwise set, MUST + // delete that element even if they are not adding their + // own stanza ID. + if (stanza_id->get_tag("by") != own_address) + nodes_to_reflect.push_back(*stanza_id); + } + bridge->send_channel_message(iid, body->get_inner(), id, std::move(nodes_to_reflect)); + } + else + { + error_type = "modify"; + error_name = "not-acceptable"; + error_text = "You are not a participant in this room."; + return; + } } const XmlNode* subject = stanza.get_child("subject", COMPONENT_NS); if (subject) @@ -350,7 +394,6 @@ void BiboumiComponent::handle_message(const Stanza& stanza) this->send_invitation_from_fulljid(std::to_string(iid), invite_to, from_str); } } - } } catch (const IRCNotConnected& ex) { @@ -514,7 +557,11 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) { if (node.empty()) { - this->send_irc_channel_disco_info(id, from, to_str); + const IrcClient* irc_client = bridge->find_irc_client(iid.get_server()); + const IrcChannel* irc_channel{}; + if (irc_client) + irc_channel = irc_client->find_channel(iid.get_local()); + this->send_irc_channel_disco_info(id, from, to_str, irc_channel); stanza_error.disable(); } else if (node == MUC_TRAFFIC_NS) @@ -592,7 +639,6 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) const XmlNode* max = set_node->get_child("max", RSM_NS); if (max) rs_info.max = std::atoi(max->get_inner().data()); - } if (rs_info.max == -1) rs_info.max = 100; @@ -751,7 +797,7 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza) if (limit < 0 || limit > 100) limit = 100; auto result = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), - limit, + static_cast<std::size_t>(limit), start, end, reference_record_id, paging_order); bool complete = std::get<bool>(result); @@ -964,7 +1010,8 @@ void BiboumiComponent::send_irc_channel_muc_traffic_info(const std::string& id, this->send_stanza(iq); } -void BiboumiComponent::send_irc_channel_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from) +void BiboumiComponent::send_irc_channel_disco_info(const std::string& id, const std::string& jid_to, + const std::string& jid_from, const IrcChannel* irc_channel) { Jid from(jid_from); Iid iid(from.local, {}); @@ -980,11 +1027,31 @@ void BiboumiComponent::send_irc_channel_disco_info(const std::string& id, const identity["category"] = "conference"; identity["type"] = "irc"; identity["name"] = ""s + iid.get_local() + " on " + iid.get_server(); - for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS}) + for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS, SELF_PING_FLAG, "muc_nonanonymous", STABLE_ID_NS}) { XmlSubNode feature(query, "feature"); feature["var"] = ns; } + + XmlSubNode x(query, "x"); + x["xmlns"] = DATAFORM_NS; + x["type"] = "result"; + { + XmlSubNode field(x, "field"); + field["var"] = "FORM_TYPE"; + field["type"] = "hidden"; + XmlSubNode value(field, "value"); + value.set_inner("http://jabber.org/protocol/muc#roominfo"); + } + + if (irc_channel && irc_channel->joined) + { + XmlSubNode field(x, "field"); + field["var"] = "muc#roominfo_occupants"; + field["label"] = "Number of occupants"; + XmlSubNode value(field, "value"); + value.set_inner(std::to_string(irc_channel->get_users().size())); + } } this->send_stanza(iq); } diff --git a/src/xmpp/biboumi_component.hpp b/src/xmpp/biboumi_component.hpp index caf990e..f59ed9b 100644 --- a/src/xmpp/biboumi_component.hpp +++ b/src/xmpp/biboumi_component.hpp @@ -73,7 +73,8 @@ public: * 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_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); + void send_irc_channel_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from, + const IrcChannel* irc_channel); /** * Send a ping request */ diff --git a/src/xmpp/jid.cpp b/src/xmpp/jid.cpp index 19d1b55..3c54fd4 100644 --- a/src/xmpp/jid.cpp +++ b/src/xmpp/jid.cpp @@ -106,7 +106,7 @@ std::string jidprep(const std::string& original) --domain_end; if (domain_end != domain && special_chars.count(domain[0])) { - std::memmove(domain, domain + 1, domain_end - domain + 1); + std::memmove(domain, domain + 1, static_cast<std::size_t>(domain_end - domain) + 1); --domain_end; } // And if the final result is an empty string, return a dummy hostname diff --git a/src/xmpp/xmpp_component.cpp b/src/xmpp/xmpp_component.cpp index b3d925e..de9a7a6 100644 --- a/src/xmpp/xmpp_component.cpp +++ b/src/xmpp/xmpp_component.cpp @@ -298,8 +298,8 @@ void XmppComponent::send_message(const std::string& from, Xmpp::body&& body, con { XmlSubNode private_node(message, "private"); private_node["xmlns"] = "urn:xmpp:carbons:2"; - XmlSubNode nocopy(message, "no-copy"); - nocopy["xmlns"] = "urn:xmpp:hints"; + XmlSubNode nocopy_node(message, "no-copy"); + nocopy_node["xmlns"] = "urn:xmpp:hints"; } if (muc_private) { @@ -340,8 +340,12 @@ void XmppComponent::send_user_join(const std::string& from, if (self) { - XmlSubNode status(x, "status"); - status["code"] = "110"; + XmlSubNode status_self(x, "status"); + status_self["code"] = "110"; + XmlSubNode status_nick_modified(x, "status"); + status_nick_modified["code"] = "210"; + XmlSubNode status_nonanonymous(x, "status"); + status_nonanonymous["code"] = "100"; } } this->send_stanza(presence); @@ -363,7 +367,7 @@ void XmppComponent::send_topic(const std::string& from, Xmpp::body&& topic, cons 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, std::string uuid, std::string id) +Stanza XmppComponent::make_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& xmpp_body, const std::string& jid_to, std::string uuid, std::string id) { Stanza message("message"); message["to"] = jid_to; @@ -395,7 +399,7 @@ void XmppComponent::send_muc_message(const std::string& muc_name, const std::str stanza_id["id"] = std::move(uuid); } - this->send_stanza(message); + return message; } #ifdef USE_DATABASE @@ -477,6 +481,8 @@ void XmppComponent::send_nick_change(const std::string& muc_name, x["xmlns"] = MUC_USER_NS; XmlSubNode item(x, "item"); item["nick"] = new_nick; + item["affiliation"] = affiliation; + item["role"] = role; XmlSubNode status(x, "status"); status["code"] = "303"; if (self) @@ -588,8 +594,8 @@ void XmppComponent::send_version(const std::string& id, const std::string& jid_t name.set_inner("biboumi"); } { - XmlSubNode version(query, "version"); - version.set_inner(SOFTWARE_VERSION); + XmlSubNode version_node(query, "version"); + version_node.set_inner(SOFTWARE_VERSION); } { XmlSubNode os(query, "os"); diff --git a/src/xmpp/xmpp_component.hpp b/src/xmpp/xmpp_component.hpp index e18da40..ee6b776 100644 --- a/src/xmpp/xmpp_component.hpp +++ b/src/xmpp/xmpp_component.hpp @@ -38,6 +38,7 @@ #define MUC_TRAFFIC_NS "http://jabber.org/protocol/muc#traffic" #define STABLE_ID_NS "urn:xmpp:sid:0" #define STABLE_MUC_ID_NS "http://jabber.org/protocol/muc#stable_id" +#define SELF_PING_FLAG MUC_NS"#self-ping-optimization" /** * An XMPP component, communicating with an XMPP server using the protocole @@ -134,8 +135,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, - std::string uuid, std::string id); + Stanza make_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& xmpp_body, const std::string& jid_to, + std::string uuid, std::string id); #ifdef USE_DATABASE /** * Send a message, with a <delay/> element, part of a MUC history diff --git a/src/xmpp/xmpp_parser.cpp b/src/xmpp/xmpp_parser.cpp index 0488be9..781fe4c 100644 --- a/src/xmpp/xmpp_parser.cpp +++ b/src/xmpp/xmpp_parser.cpp @@ -20,7 +20,7 @@ static void end_element_handler(void* user_data, const XML_Char* name) static void character_data_handler(void *user_data, const XML_Char *s, int len) { - static_cast<XmppParser*>(user_data)->char_data(s, len); + static_cast<XmppParser*>(user_data)->char_data(s, static_cast<std::size_t>(len)); } /** diff --git a/tests/end_to_end/__main__.py b/tests/end_to_end/__main__.py index b5d56fc..1e4ffca 100644 --- a/tests/end_to_end/__main__.py +++ b/tests/end_to_end/__main__.py @@ -1,18 +1,20 @@ #!/usr/bin/env python3 +from functions import StanzaError, SkipStepError + import collections -import lxml.etree +import subprocess +import importlib +import sequences import datetime import slixmpp import asyncio import logging import signal import atexit -import time import sys -import io import os -from functools import partial + from slixmpp.xmlstream.matcher.base import MatcherBase if not hasattr(asyncio, "ensure_future"): @@ -25,20 +27,35 @@ class MatchAll(MatcherBase): return True -class StanzaError(Exception): - """ - Raised when a step fails. +class Scenario: + """Defines a list of actions that are executed in sequence, until one of + them throws an exception, or until the end. An action can be something + like “send a stanza”, “receive the next stanza and check that it matches + the given XPath”, “send a signal”, “wait for the end of the process”, + etc """ - pass + def __init__(self, name, steps, conf): + """ + Steps is a list of 2-tuple: + [(action, answer), (action, answer)] + """ + self.name = name + self.steps = [] + self.conf = conf -class SkipStepError(Exception): - """ - Raised by a step when it needs to be skiped, by running - the next available step immediately. - """ - pass + def unwrap_tuples(elements): + """Yields all the value contained in the tuples, of tuples, of tuples… + For example unwrap_tuples((1, 2, 3, (4, 5, (6,)))) will yield 1, 2, 3, 4, 5, 6 + This works with any depth""" + if isinstance(elements, collections.abc.Iterable): + for elem in elements: + yield from unwrap_tuples(elem) + else: + yield elements + for step in unwrap_tuples(steps): + self.steps.append(step) class XMPPComponent(slixmpp.BaseXMPP): """ @@ -54,7 +71,7 @@ class XMPPComponent(slixmpp.BaseXMPP): self.stream_header = '<stream:stream %s %s from="%s" id="%s">' % ( 'xmlns="jabber:component:accept"', 'xmlns:stream="%s"' % self.stream_ns, - self.boundjid, self.get_id()) + self.boundjid, self.new_id()) self.stream_footer = "</stream:stream>" self.register_handler(slixmpp.Callback('Match All', @@ -67,6 +84,7 @@ class XMPPComponent(slixmpp.BaseXMPP): self.scenario = scenario self.biboumi = biboumi + self.timeout_handler = None # A callable, taking a stanza as argument and raising a StanzaError # exception if the test should fail. self.stanza_checker = None @@ -80,6 +98,13 @@ class XMPPComponent(slixmpp.BaseXMPP): self.scenario.steps = [] self.failed = True + def on_timeout(self, xpaths): + error_msg = "Timeout while waiting for a stanza that would match the expected xpath(s):" + for xpath in xpaths: + error_msg += "\n" + str(xpath) + self.error(error_msg) + self.run_scenario() + def on_end_session(self, _): self.loop.stop() @@ -97,10 +122,13 @@ class XMPPComponent(slixmpp.BaseXMPP): self.run_scenario() def run_scenario(self): + if self.timeout_handler is not None: + self.timeout_handler.cancel() + self.timeout_handler = None if self.scenario.steps: step = self.scenario.steps.pop(0) try: - step(self, self.biboumi) + step(xmpp=self, biboumi=self.biboumi) except Exception as e: self.error(e) self.run_scenario() @@ -113,106 +141,6 @@ class XMPPComponent(slixmpp.BaseXMPP): self.accepting_server = await self.loop.create_server(lambda: self, "127.0.0.1", 8811, reuse_address=True) - def check_stanza_against_all_expected_xpaths(self): - pass - - -def match(stanza, xpath): - tree = lxml.etree.parse(io.StringIO(str(stanza))) - matched = tree.xpath(xpath, namespaces={'re': 'http://exslt.org/regular-expressions', - 'muc_user': 'http://jabber.org/protocol/muc#user', - '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', - 'disco_items': 'http://jabber.org/protocol/disco#items', - 'commands': 'http://jabber.org/protocol/commands', - 'dataform': 'jabber:x:data', - 'version': 'jabber:iq:version', - 'mam': 'urn:xmpp:mam:2', - 'rms': 'http://jabber.org/protocol/rsm', - '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', - '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 (expected and not matched) or (not expected and matched): - raise StanzaError("Received stanza\n%s\ndid not match expected xpath\n%s" % (stanza, real_xpath)) - if after: - if isinstance(after, collections.Iterable): - for af in after: - af(stanza, xmpp) - else: - after(stanza, xmpp) - -def all_xpaths_match(stanza, xpaths): - for xpath in xpaths: - matched = match(stanza, xpath) - if not matched: - return False - return True - -def check_list_of_xpath(list_of_xpaths, xmpp, stanza): - found = None - for i, xpaths in enumerate(list_of_xpaths): - if all_xpaths_match(stanza, xpaths): - found = True - list_of_xpaths.pop(i) - break - - if not found: - raise StanzaError("Received stanza “%s” did not match any of the expected xpaths:\n%s" % (stanza, list_of_xpaths)) - - if list_of_xpaths: - step = partial(expect_unordered_already_formatted, list_of_xpaths) - xmpp.scenario.steps.insert(0, step) - - -def check_xpath_optional(xpaths, xmpp, after, stanza): - try: - check_xpath(xpaths, xmpp, after, stanza) - except StanzaError: - raise SkipStepError() - - -class Scenario: - """Defines a list of actions that are executed in sequence, until one of - them throws an exception, or until the end. An action can be something - like “send a stanza”, “receive the next stanza and check that it matches - the given XPath”, “send a signal”, “wait for the end of the process”, - etc - """ - - def __init__(self, name, steps, conf="basic"): - """ - Steps is a list of 2-tuple: - [(action, answer), (action, answer)] - """ - self.name = name - self.steps = [] - self.conf = conf - for elem in steps: - if isinstance(elem, collections.Iterable): - for step in elem: - self.steps.append(step) - else: - self.steps.append(elem) - class ProcessRunner: def __init__(self): @@ -256,51 +184,10 @@ class BiboumiRunner(ProcessRunner): class IrcServerRunner(ProcessRunner): def __init__(self): super().__init__() - self.create = asyncio.create_subprocess_exec("charybdis", "-foreground", "-configfile", os.getcwd() + "/../tests/end_to_end/ircd.conf", + subprocess.run(["oragono", "mkcerts", "--conf", os.getcwd() + "/../tests/end_to_end/ircd.yaml"]) + self.create = asyncio.create_subprocess_exec("oragono", "run", "--conf", os.getcwd() + "/../tests/end_to_end/ircd.yaml", stderr=asyncio.subprocess.PIPE) - -def send_stanza(stanza, xmpp, biboumi): - replacements = common_replacements - replacements.update(xmpp.saved_values) - xmpp.send_raw(stanza.format_map(replacements)) - asyncio.get_event_loop().call_soon(xmpp.run_scenario) - - -def expect_stanza(xpaths, xmpp, biboumi, optional=False, after=None): - replacements = common_replacements - replacements.update(xmpp.saved_values) - check_func = check_xpath if not optional else check_xpath_optional - if isinstance(xpaths, str): - xmpp.stanza_checker = partial(check_func, [xpaths.format_map(replacements)], xmpp, after) - elif isinstance(xpaths, tuple): - xmpp.stanza_checker = partial(check_func, [xpath.format_map(replacements) for xpath in xpaths], xmpp, after) - 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 = [] - for xpaths in list_of_xpaths: - formatted_xpaths = [] - for xpath in xpaths: - formatted_xpath = xpath.format_map(common_replacements) - formatted_xpaths.append(formatted_xpath) - formatted_list_of_xpaths.append(tuple(formatted_xpaths)) - expect_unordered_already_formatted(formatted_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) - - class BiboumiTest: """ Spawns a biboumi process and a fake XMPP Component that will run a @@ -331,6 +218,8 @@ class BiboumiTest: except FileNotFoundError: pass + start_datetime = datetime.datetime.now() + # Start the XMPP component and biboumi biboumi = BiboumiRunner(self.scenario.name) xmpp = XMPPComponent(self.scenario, biboumi) @@ -342,13 +231,16 @@ class BiboumiTest: code = asyncio.get_event_loop().run_until_complete(biboumi.wait()) xmpp.biboumi = None self.scenario.steps.clear() + + delta = datetime.datetime.now() - start_datetime + failed = False if not xmpp.failed: if code != self.expected_code: xmpp.error("Wrong return code from biboumi's process: %d" % (code,)) failed = True else: - print("[32;1mSuccess![0m") + print("[32;1mSuccess![0m ({}s)".format(round(delta.total_seconds(), 2))) else: failed = True @@ -388,2852 +280,42 @@ port=8811 persistent_by_default=true """,} -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', - 'nick_one': 'Nick', - 'jid_one': 'first@example.com', - 'jid_two': 'second@example.com', - 'jid_admin': 'admin@example.com', - 'nick_two': 'Bobby', - 'nick_three': 'Bernard', - 'lower_nick_one': 'nick', - 'lower_nick_two': 'bobby', -} - - -def handshake_sequence(): - return (partial(expect_stanza, "//handshake"), - partial(send_stanza, "<handshake xmlns='jabber:component:accept'/>")) - - -def connection_begin_sequence(irc_host, jid, expected_irc_presence=False, fixed_irc_server=False): - jid = jid.format_map(common_replacements) - if fixed_irc_server: - xpath = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[text()='%s']" - xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]" - else: - 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')]" - result = ( - partial(expect_stanza, - (xpath % ('Connecting to %s:6697 (encrypted)' % irc_host), - "/message/hints:no-copy", - "/message/carbon:private" - ) - ), - partial(expect_stanza, - xpath % 'Connection failed: Connection refused'), - partial(expect_stanza, - xpath % ('Connecting to %s:6670 (encrypted)' % irc_host)), - partial(expect_stanza, - xpath % 'Connection failed: Connection refused'), - partial(expect_stanza, - xpath % ('Connecting to %s:6667 (not encrypted)' % irc_host)), - partial(expect_stanza, - xpath % 'Connected to IRC server.')) - - if expected_irc_presence: - result += (partial(expect_stanza, "/presence[@from='" + irc_host + "@biboumi.localhost']"),) - - # These five messages can be receive in any order - result += ( - partial(expect_stanza, - 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\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')), - partial(expect_stanza, - 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\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')), - partial(expect_stanza, - xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')), - ) - - return result - -def connection_tls_begin_sequence(irc_host, jid, fixed_irc_server): - jid = jid.format_map(common_replacements) - if fixed_irc_server: - xpath = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[text()='%s']" - xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]" - else: - 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), - "/message/hints:no-copy", - "/message/carbon:private", - ) - ), - 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, fixed_irc_server=False): - jid = jid.format_map(common_replacements) - if fixed_irc_server: - xpath = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[text()='%s']" - xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]" - else: - 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)), - partial(expect_stanza, - xpath_re % (r'^%s: This server was created .*$' % irc_host)), - partial(expect_stanza, - xpath_re % (r'^%s: There are \d+ users and \d+ invisible on \d+ servers$' % irc_host)), - partial(expect_stanza, - xpath_re % (r'^%s: \d+ unknown connection\(s\)$' % irc_host), optional=True), - partial(expect_stanza, - xpath_re % (r'^%s: \d+ channels formed$' % irc_host), optional=True), - partial(expect_stanza, - xpath_re % (r'^%s: I have \d+ clients and \d+ servers$' % irc_host)), - partial(expect_stanza, - xpath_re % (r'^%s: \d+ \d+ Current local users \d+, max \d+$' % irc_host)), - partial(expect_stanza, - xpath_re % (r'^%s: \d+ \d+ Current global users \d+, max \d+$' % irc_host)), - partial(expect_stanza, - xpath_re % (r'^%s: Highest connection count: \d+ \(\d+ clients\) \(\d+ connections received\)$' % irc_host)), - partial(expect_stanza, - xpath % "- This is charybdis MOTD you might replace it, but if not your friends will\n- laugh at you.\n"), - partial(expect_stanza, - xpath_re % r'^User mode for \w+ is \[\+Z?i\]$'), - ) - -def connection_middle_sequence(irc_host, jid, fixed_irc_server=False): - if fixed_irc_server: - xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]" - else: - 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, expected_irc_presence=False, fixed_irc_server=False): - return connection_begin_sequence(irc_host, jid, expected_irc_presence, fixed_irc_server=fixed_irc_server) +\ - connection_middle_sequence(irc_host, jid, fixed_irc_server=fixed_irc_server) +\ - connection_end_sequence(irc_host, jid, fixed_irc_server=fixed_irc_server) - -def connection_tls_sequence(irc_host, jid, fixed_irc_server=False): - return connection_tls_begin_sequence(irc_host, jid, fixed_irc_server) + \ - connection_middle_sequence(irc_host, jid, fixed_irc_server) +\ - connection_end_sequence(irc_host, jid, fixed_irc_server) - - -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) +def get_scenarios(test_path, provided_scenar_names): + """ + :param test_path: The path containing all the tests + :param provided_scenar_names: a list of scenario names provided on the + command line by the user. May be empty + :return: The list of scenarios to be run. If provided_scenar_names is + empty, we return all the existing scenarios, otherwise we just return + the one from that list + """ + scenarios = [] + for entry in os.scandir(os.path.join(test_path, "scenarios")): + if entry.is_file() and not entry.name.startswith('.') and entry.name.endswith('.py'): + module_name = entry.name[:-3] + if provided_scenar_names and module_name not in provided_scenar_names: + continue + if module_name == "__init__" or (provided_scenar_names and module_name not in provided_scenar_names): + continue + module_full_path = "scenarios.{}".format(module_name) + mod = importlib.import_module(module_full_path) + conf = "basic" + if hasattr(mod, "conf"): + conf = mod.conf + # Every scenario needs to start with the handshake sequence. + # Instead of repeating it everytime, we add it implicitely. This + # is done here. + scenarios.append(Scenario(module_name, (sequences.handshake(),) + mod.scenario, conf)) + return scenarios if __name__ == '__main__': - atexit.register(asyncio.get_event_loop().close) - # Start the test component, accepting connections on the configured - # port. - scenarios = ( - Scenario("basic_handshake_success", - [ - handshake_sequence() - ]), - Scenario("irc_server_connection", - [ - handshake_sequence(), - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), - connection_sequence("irc.localhost", '{jid_one}/{resource_one}'), - ]), - Scenario("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(), - 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("raw_names_command", - [ - 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"), - partial(expect_stanza, - "/presence/muc_user:x/muc_user:status[@code='110']" - ), - partial(expect_stanza, "/message/subject[not(text())]"), - partial(send_stanza, - "<message type='chat' from='{jid_one}/{resource_one}' to='{irc_server_one}'><body>NAMES</body></message>"), - partial(expect_stanza, "/message/body[text()='irc.localhost: = #foo @{nick_one} ']"), - partial(expect_stanza, "/message/body[text()='irc.localhost: * End of /NAMES list. ']"), - ]), - Scenario("quit", - [ - 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 raw QUIT message - partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='{irc_server_one}' type='chat'><body>QUIT bye bye</body></message>"), - partial(expect_stanza, ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@type='unavailable']/muc_user:x/muc_user:status[@code='110']", - "/presence[@from='#foo%{irc_server_one}/{nick_one}'][@type='unavailable']/muc_user:x/muc_user:status[@code='110']",)), - ]), - 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("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("channel_join_with_two_users", - [ - handshake_sequence(), - # First user joins - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), - connection_sequence("irc.localhost", '{jid_one}/{resource_one}'), - partial(expect_stanza, - "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - - # Second user joins - partial(send_stanza, - "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"), - connection_sequence("irc.localhost", '{jid_two}/{resource_one}'), - 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_force_join", - [ - handshake_sequence(), - # First user joins - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'/></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'][@jid='~nick@localhost'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - - # Second user joins - partial(send_stanza, - "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}'><x xmlns='http://jabber.org/protocol/muc'/></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())]",), - ]), - - # Here we simulate a desynchronization of a client: The client thinks it’s - # disconnected from the room, but biboumi still thinks it’s in the room. The - # client thus sends a join presence, and biboumi should send everything - # (user list, history, etc) in response. - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}'><x xmlns='http://jabber.org/protocol/muc'/></presence>"), - - 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_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']",), - ("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]",), - ]), - # And also, that was not the same nickname - partial(expect_unordered, [ - ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bernard']", - "/presence/muc_user:x/muc_user:status[@code='303']"), - ("/presence[@from='#foo%{irc_server_one}/{nick_three}'][@to='{jid_two}/{resource_one}']",), - ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bernard']", - "/presence/muc_user:x/muc_user:status[@code='303']", - "/presence/muc_user:x/muc_user:status[@code='110']"), - ("/presence[@from='#foo%{irc_server_one}/{nick_three}'][@to='{jid_one}/{resource_one}']", - "/presence/muc_user:x/muc_user:status[@code='110']"), - ]), - ]), - 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(), - # First user joins - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), - connection_sequence("irc.localhost", '{jid_one}/{resource_one}'), - partial(expect_stanza, - "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - - # First user sets the topic - partial(send_stanza, - "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><subject>TOPIC TEST</subject></message>"), - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[text()='TOPIC TEST']"), - - # Second user joins - partial(send_stanza, - "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"), - connection_sequence("irc.localhost", '{jid_two}/{resource_one}'), - # Our presence, sent to the other user - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']",)), - # The other user presence - partial(expect_stanza, - "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']"), - # Our own presence - partial(expect_stanza, - ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/subject[text()='TOPIC TEST']"), - ]), - Scenario("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(), - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#zgeg@{biboumi_host}/{nick_one}' />"), - connection_sequence("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True), - partial(expect_stanza, - "/message/body[text()='Mode #zgeg [+nt] by {irc_host_one}']"), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_one}'][@from='#zgeg@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - partial(expect_stanza, "/message[@from='#zgeg@{biboumi_host}'][@type='groupchat']/subject[not(text())]"), - ], conf='fixed_server' - ), - Scenario("list_adhoc", - [ - handshake_sequence(), - partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_one}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"), - partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']", - "/iq/disco_items:query/disco_items:item[@node='configure']", - "/iq/disco_items:query/disco_items:item[4]", - "!/iq/disco_items:query/disco_items:item[5]")), - ]), - Scenario("list_admin_adhoc", - [ - handshake_sequence(), - partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"), - partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']", - "/iq/disco_items:query/disco_items:item[6]", - "!/iq/disco_items:query/disco_items:item[7]")), - ]), - Scenario("list_adhoc_fixed_server", - [ - handshake_sequence(), - partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_one}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"), - partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']", - "/iq/disco_items:query/disco_items:item[@node='global-configure']", - "/iq/disco_items:query/disco_items:item[@node='server-configure']", - "/iq/disco_items:query/disco_items:item[6]", - "!/iq/disco_items:query/disco_items:item[7]")), - ], conf='fixed_server'), - Scenario("list_admin_adhoc_fixed_server", - [ - handshake_sequence(), - partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"), - partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']", - "/iq/disco_items:query/disco_items:item[8]", - "!/iq/disco_items:query/disco_items:item[9]")), - ], conf='fixed_server'), - Scenario("list_adhoc_irc", - [ - handshake_sequence(), - partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_one}/{resource_one}' to='{irc_host_one}@{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"), - partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']", - "/iq/disco_items:query/disco_items:item[2]")), - ]), - Scenario("list_muc_user_adhoc", - [ - handshake_sequence(), - partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_admin}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"), - partial(expect_stanza, "/iq[@type='error']/error[@type='cancel']/stanza:feature-not-implemented"), - ] - ), - Scenario("execute_hello_adhoc_command", - [ - handshake_sequence(), - partial(send_stanza, "<iq type='set' id='hello-command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' action='execute' /></iq>"), - partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='hello'][@sessionid][@status='executing']", - "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure your name.']", - "/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Please provide your name.']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single']/dataform:required", - "/iq/commands:command/commands:actions/commands:next", - ), - after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='hello']", "sessionid")) - ), - partial(send_stanza, "<iq type='set' id='hello-command2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='hello' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='name'><value>COUCOU</value></field></x></command></iq>"), - partial(expect_stanza, "/iq[@type='result']/commands:command[@node='hello'][@status='completed']/commands:note[@type='info'][text()='Hello COUCOU!']") - ]), - Scenario("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(), - 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-user' action='execute' /></iq>"), - partial(expect_stanza, ("/iq[@type='error'][@id='command1']/commands:command[@node='disconnect-user']", - "/iq/commands:command/commands:error[@type='cancel']/stanza:forbidden")), - ]), - Scenario("execute_disconnect_user_adhoc_command", - [ - handshake_sequence(), - - 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}']"), - partial(expect_stanza, "/presence"), - partial(expect_stanza, "/message"), - - 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-user' action='execute' /></iq>"), - partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='disconnect-user'][@sessionid][@status='executing']", - "/iq/commands:command/commands:actions/commands:next", - ), - after = partial(save_value, "sessionid", partial(extract_attribute, "/iq/commands:command[@node='disconnect-user']", "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-user' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='jids'><value>{jid_admin}</value></field><field var='quit-message'><value>Disconnected by e2e</value></field></x></command></iq>"), - partial(expect_stanza, "/iq[@type='result']/commands:command[@node='disconnect-user'][@status='completed']/commands:note[@type='info'][text()='1 user has been disconnected.']"), - # 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.']",), - ]), - - - # 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.']",), - ]), - ]), - Scenario("multisessionnick", - [ - handshake_sequence(), - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), - connection_sequence("irc.localhost", '{jid_one}/{resource_one}'), - partial(expect_stanza, - "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[not(text())]"), - - # The other resources joins the same room, with the same nick - partial(send_stanza, - "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"), - # We receive our own join - partial(expect_unordered, - [("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']"), - ("/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]",)] - ), - - # A different user joins the same room - partial(send_stanza, - "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"), - connection_sequence("irc.localhost", '{jid_two}/{resource_one}'), - - partial(expect_unordered, [ - ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']",), - ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_two}']",), - ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']", - "/presence/muc_user:x/muc_user:status[@code='110']",), - ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']",), - ] - ), - - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - - # That second user sends a private message to the first one - partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='chat'><body>RELLO</body></message>"), - # Message is received with a server-wide JID, by the two resources behind nick_one - partial(expect_stanza, ("/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='RELLO']", - "/message/hints:no-copy", - "/message/carbon:private", - "!/message/muc_user:x")), - partial(expect_stanza, "/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_two}'][@type='chat']/body[text()='RELLO']"), - - - # 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, [ - ("/message[@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='irc.localhost: Bobby: Nickname is already in use.']",), - ("/message[@to='{jid_one}/{resource_two}'][@type='chat']/body[text()='irc.localhost: Bobby: Nickname is already in use.']",), - ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}'][@type='error']",), - ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_two}'][@type='error']",), - ]), - - # 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, [ - ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bernard']", - "/presence/muc_user:x/muc_user:status[@code='303']"), - ("/presence[@from='#foo%{irc_server_one}/{nick_three}'][@to='{jid_two}/{resource_one}']",), - ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bernard']", - "/presence/muc_user:x/muc_user:status[@code='303']", - "/presence/muc_user:x/muc_user:status[@code='110']"), - ("/presence[@from='#foo%{irc_server_one}/{nick_three}'][@to='{jid_one}/{resource_one}']", - "/presence/muc_user:x/muc_user:status[@code='110']"), - - ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_two}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bernard']", - "/presence/muc_user:x/muc_user:status[@code='303']", - "/presence/muc_user:x/muc_user:status[@code='110']"), - ("/presence[@from='#foo%{irc_server_one}/{nick_three}'][@to='{jid_one}/{resource_two}']", - "/presence/muc_user:x/muc_user:status[@code='110']"), - ]), - - # One resource leaves the server entirely. - partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"), - # The leave is forwarded only to us - partial(expect_stanza, - ("/presence[@type='unavailable']/muc_user:x/muc_user:status[@code='110']", - "/presence/status[text()='Biboumi note: 1 resources are still in this channel.']", - ) - ), - - # The second user sends two new private messages to the first user - partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}' type='chat'><body>first</body></message>"), - partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}' type='chat'><body>second</body></message>"), - # The first user receives the two messages, on the connected resource, once each - - partial(expect_unordered, [ - ("/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='first']",), - ("/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='second']",), - ]), - ]), - Scenario("persistent_channel", - [ - # Join the channel with user 1 - 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())]"), - - # Make it persistent for user 1 - partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='conf1' to='#foo%{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"), - partial(expect_stanza, "/iq[@type='result']/muc_owner:query/dataform:x/dataform:field[@var='persistent'][@type='boolean']/dataform:value[text()='false']"), - partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='conf2' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#owner'><x type='submit' xmlns='jabber:x:data'><field var='persistent' xmlns='jabber:x:data'><value>true</value></field></x></query></iq>"), - partial(expect_stanza, "/iq[@type='result']"), - - # Check that the value is now effectively true - partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='conf1' to='#foo%{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"), - partial(expect_stanza, "/iq[@type='result']/muc_owner:query/dataform:x/dataform:field[@var='persistent'][@type='boolean']/dataform:value[text()='true']"), - - # A second user joins the same channel - 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[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']",), - ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']", - "/presence/muc_user:x/muc_user:status[@code='110']",), - ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']",), - ] - ), - - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - - # First user leaves the room (but biboumi will stay in the channel) - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />"), - # Only user 1 receives the unavailable presence - partial(expect_stanza, - ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:status[@code='110']", - "/presence/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']")), - - # Second user sends a channel message - partial(send_stanza, "<message type='groupchat' from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}'><body>coucou</body></message>"), - - # Message should only be received by user 2, since user 1 has no resource in the room - partial(expect_stanza, "/message[@type='groupchat'][@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']"), - - # Second user leaves the channel - partial(send_stanza, "<presence type='unavailable' from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"), - partial(expect_stanza, "/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_two}']"), - ]), - 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("multiline_message", - [ - 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'][@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 multi-line channel message - partial(send_stanza, "<message id='the-message-id' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>un\ndeux\ntrois</body></message>"), - # Receive multiple messages, in order - partial(expect_stanza, - "/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='un']"), - partial(expect_stanza, - "/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='deux']"), - partial(expect_stanza, - "/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='trois']"), - - # Send a simple message, with no id - partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>hello</body></message>"), - - # Expect a non-empty id as a result (should be a uuid) - partial(expect_stanza, - "!/message[@id='']/body[text()='hello']"), - - # Second user joins - partial(send_stanza, - "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"), - connection_sequence("irc.localhost", '{jid_two}/{resource_one}'), - # Our presence, sent to the other user - partial(expect_unordered, [ - ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']",), - ("/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())]",) - ]), - - # Send a multi-line channel message - partial(send_stanza, "<message id='the-message-id' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>un\ndeux\ntrois</body></message>"), - # Receive multiple messages, for each user - partial(expect_unordered, [ - ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='un']",), - ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='deux']",), - ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='trois']",), - - ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='un']",), - ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='deux']",), - ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='trois']",), - ]) - ]), - Scenario("channel_messages", - [ - handshake_sequence(), - # First user joins - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), - connection_sequence("irc.localhost", '{jid_one}/{resource_one}'), - partial(expect_stanza, - "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - - # Second user joins - partial(send_stanza, - "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"), - connection_sequence("irc.localhost", '{jid_two}/{resource_one}'), - # Our presence, sent to the other user - partial(expect_unordered, [ - ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']",), - ("/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())]",) - ]), - - # Send a channel message - partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"), - # Receive the message, forwarded to the two users - partial(expect_unordered, [ - ("/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]"), - ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='coucou']", - "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]") - ]), - - # Send a private message, to a in-room JID - partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' type='chat'><body>coucou in private</body></message>"), - # Message is received with a server-wide JID - partial(expect_stanza, "/message[@from='{lower_nick_one}%{irc_server_one}'][@to='{jid_two}/{resource_one}'][@type='chat']/body[text()='coucou in private']"), - - # Respond to the message, to the server-wide JID - partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>yes</body></message>"), - # The response is received from the in-room JID - partial(expect_stanza, ("/message[@from='#foo%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='yes']", - "/message/muc_user:x")), - - ## Do the exact same thing, from a different chan, - # to check if the response comes from the right JID - - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#dummy%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, "/message"), - partial(expect_stanza, - "/presence/muc_user:x/muc_user:status[@code='110']"), - partial(expect_stanza, "/message[@from='#dummy%{irc_server_one}'][@type='groupchat']/subject"), - - - # Send a private message, to a in-room JID - partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#dummy%{irc_server_one}/{nick_two}' type='chat'><body>re in private</body></message>"), - # Message is received with a server-wide JID - partial(expect_stanza, "/message[@from='{lower_nick_one}%{irc_server_one}'][@to='{jid_two}/{resource_one}'][@type='chat']/body[text()='re in private']"), - - # Respond to the message, to the server-wide JID - partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>re</body></message>"), - # The response is received from the in-room JID - partial(expect_stanza, "/message[@from='#dummy%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='re']"), - - # Now we leave the room, to check if the subsequent private messages are still received properly - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#dummy%{irc_server_one}/{nick_one}' type='unavailable' />"), - partial(expect_stanza, - "/presence[@type='unavailable']/muc_user:x/muc_user:status[@code='110']"), - - # The private messages from this nick should now come (again) from the server-wide JID - partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>hihihoho</body></message>"), - partial(expect_stanza, - "/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}']"), - ] - ), - Scenario("encoded_channel_join", - [ - handshake_sequence(), - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#biboumi\\40louiz.org\\3a80%{irc_server_one}/{nick_one}' />"), - connection_sequence("irc.localhost", '{jid_one}/{resource_one}'), - partial(expect_stanza, - "/message/body[text()='Mode #biboumi@louiz.org:80 [+nt] by {irc_host_one}']"), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_one}'][@from='#biboumi\\40louiz.org\\3a80%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - partial(expect_stanza, "/message[@from='#biboumi\\40louiz.org\\3a80%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - ]), - Scenario("self_ping_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(), - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), - connection_sequence("irc.localhost", '{jid_one}/{resource_one}'), - partial(expect_stanza, - "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - - # Send a ping to ourself - partial(send_stanza, - "<iq type='get' from='{jid_one}/{resource_one}' id='first_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"), - # We receive our own ping request, - partial(expect_stanza, - "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to='{jid_one}/{resource_one}'][@id='gnip_tsrif']"), - # Respond to the request - partial(send_stanza, - "<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='gnip_tsrif' from='{jid_one}/{resource_one}'/>"), - partial(expect_stanza, - "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"), - - # Now join the same room, from the same bare JID, behind the same nick - partial(send_stanza, - "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"), - - # And re-send a self ping - partial(send_stanza, - "<iq type='get' from='{jid_one}/{resource_one}' id='second_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"), - # We receive our own ping request. Note that we don't know the to value, it could be one of our two resources. - partial(expect_stanza, - "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to][@id='gnip_dnoces']", - after = partial(save_value, "to", partial(extract_attribute, "/iq", "to"))), - # Respond to the request, using the extracted 'to' value as our 'from' - partial(send_stanza, - "<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='gnip_dnoces' from='{to}'/>"), - partial(expect_stanza, - "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='second_ping']"), - ## And re-do exactly the same thing, just change the resource initiating the self ping - partial(send_stanza, - "<iq type='get' from='{jid_one}/{resource_two}' id='third_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"), - partial(expect_stanza, - "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to][@id='gnip_driht']", - after = partial(save_value, "to", partial(extract_attribute, "/iq", "to"))), - partial(send_stanza, - "<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='gnip_driht' from='{to}'/>"), - partial(expect_stanza, - "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_two}'][@id='third_ping']"), - - ]), - Scenario("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}', fixed_irc_server=True), - 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(), - # 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",), - ]), - - # demonstrate bug https://lab.louiz.org/louiz/biboumi/issues/3291 - # First user joins an other channel - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_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='#bar%{irc_server_one}/{nick_two}' />"), - 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",), - ]), - - # 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(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']", - "/presence/muc_user:x/muc_user:status[@code='307']", - "/presence/muc_user:x/muc_user:status[@code='110']" - ), - ("/presence[@type='unavailable'][@to='{jid_one}/{resource_one}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']", - "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']", - "/presence/muc_user:x/muc_user:status[@code='307']", - ), - ("/iq[@id='kick1'][@type='result']",), - ]), - - # Bug 3291, suite. We must not receive any presence from #foo, here - partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{irc_server_one}' type='chat'><body>QUIT bye bye</body></message>"), - partial(expect_unordered, - [("/presence[@from='#bar%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}']",), - ("/presence[@from='#bar%{irc_server_one}/{nick_two}'][@to='{jid_two}/{resource_one}']",), - ("/message",), - ("/message",)]) - ]), - 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(), - # 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, fprom two resources - partial(send_stanza, - "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"), - connection_sequence("irc.localhost", '{jid_two}/{resource_one}'), - partial(expect_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",), - ]), - - partial(send_stanza, - "<presence from='{jid_two}/{resource_two}' to='#foo%{irc_server_one}/{nick_two}' />"), - partial(expect_stanza, - "/presence[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']"), - partial(expect_stanza, - ("/presence[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_two}']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_two}/{resource_two}']/subject[not(text())]"), - - # Moderator kicks participant - partial(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(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']", - "/presence/muc_user:x/muc_user:status[@code='307']", - "/presence/muc_user:x/muc_user:status[@code='110']" - ), - ("/presence[@type='unavailable'][@to='{jid_two}/{resource_two}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']", - "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']", - "/presence/muc_user:x/muc_user:status[@code='307']", - "/presence/muc_user:x/muc_user:status[@code='110']" - ), - ("/presence[@type='unavailable']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']", - "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']", - "/presence/muc_user:x/muc_user:status[@code='307']", - ), - ("/iq[@id='kick1'][@type='result']",), - ]), - ]), - Scenario("self_version", - [ - handshake_sequence(), - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), - connection_sequence("irc.localhost", '{jid_one}/{resource_one}'), - partial(expect_stanza, - "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - - # Send a version request to ourself - partial(send_stanza, - "<iq type='get' from='{jid_one}/{resource_one}' id='first_version' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='jabber:iq:version' /></iq>"), - # We receive our own request, - partial(expect_stanza, - "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to='{jid_one}/{resource_one}']", - after = partial(save_value, "id", partial(extract_attribute, "/iq", 'id'))), - # Respond to the request - partial(send_stanza, - "<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='{id}' from='{jid_one}/{resource_one}'><query xmlns='jabber:iq:version'><name>e2e test</name><version>1.0</version><os>Fedora</os></query></iq>"), - partial(expect_stanza, - "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_version']/version:query/version:name[text()='e2e test (through the biboumi gateway) 1.0 Fedora']"), - - # Now join the same room, from the same bare JID, behind the same nick - partial(send_stanza, - "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"), - - # And re-send a self ping - partial(send_stanza, - "<iq type='get' from='{jid_one}/{resource_two}' id='second_version' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='jabber:iq:version' /></iq>"), - # We receive our own request. Note that we don't know the to value, it could be one of our two resources. - partial(expect_stanza, - "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to]", - after = (partial(save_value, "to", partial(extract_attribute, "/iq", "to")), - partial(save_value, "id", partial(extract_attribute, "/iq", "id")))), - # Respond to the request, using the extracted 'to' value as our 'from' - partial(send_stanza, - "<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='{id}' from='{to}'><query xmlns='jabber:iq:version'><name>e2e test</name><version>1.0</version><os>Fedora</os></query></iq>"), - partial(expect_stanza, - "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_two}'][@id='second_version']"), - - # And do exactly the same thing, but initiated by the other resource - partial(send_stanza, - "<iq type='get' from='{jid_one}/{resource_one}' id='second_version' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='jabber:iq:version' /></iq>"), - # We receive our own request. Note that we don't know the to value, it could be one of our two resources. - partial(expect_stanza, - "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to]", - after = (partial(save_value, "to", partial(extract_attribute, "/iq", "to")), - partial(save_value, "id", partial(extract_attribute, "/iq", "id")))), - # Respond to the request, using the extracted 'to' value as our 'from' - partial(send_stanza, - "<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='{id}' from='{to}'><query xmlns='jabber:iq:version'><name>e2e test</name><version>1.0</version><os>Fedora</os></query></iq>"), - partial(expect_stanza, - "/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='second_version']"), - ]), - Scenario("version_on_global_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']/subject[not(text())]"), - - 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(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(send_stanza, - "<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='{id}' from='{jid_one}/{resource_one}'><query xmlns='jabber:iq:version'><name>e2e test</name><version>1.0</version><os>Fedora</os></query></iq>"), - partial(expect_stanza, - "/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_version']/version:query/version:name[text()='e2e test (through the biboumi gateway) 1.0 Fedora']"), - - ]), - Scenario("self_invite", - [ - 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())]"), - partial(send_stanza, - "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><x xmlns='http://jabber.org/protocol/muc#user'><invite to='{nick_one}'/></x></message>"), - partial(expect_stanza, - "/message/body[text()='{nick_one} is already on channel #foo']") - ]), - Scenario("client_error", - [ - handshake_sequence(), - # First resource - 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())]"), - - # Second resource, same channel - partial(send_stanza, - "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"), - - # Now the first resource has an error - partial(send_stanza, - "<message from='{jid_one}/{resource_one}' to='#foo%%{irc_server_one}/{nick_one}' type='error'><error type='cancel'><recipient-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error></message>"), - # Receive a leave only to the leaving resource - partial(expect_stanza, - ("/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}'][@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.']") - ), - ]), - Scenario("simple_mam", - [ - 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>"), - 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:2' queryid='qid1' /></iq>"), - - 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()='coucou']") - ), - 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()='coucou 2']") - ), - - partial(expect_stanza, - ("/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", - "/iq/mam:fin/rms:set/rsm:last", - "/iq/mam:fin/rsm:set/rsm:first", - "/iq/mam:fin[@complete='true']")), - - # 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:2' queryid='qid2'> - <x xmlns='jabber:x:data' type='submit'> - <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>"""), - - partial(expect_stanza, - ("/iq[@type='result'][@id='id2'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", - "/iq/mam:fin[@complete='true']/rsm:set",)), - - # 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:2' queryid='qid3'> - <x xmlns='jabber:x:data' type='submit'> - <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}']", - "/iq/mam:fin[@complete='true']/rsm:set")), - - # Retrieve the whole archive, but limit the response to one elemet - 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']") - ), - - partial(expect_stanza, - ("/iq[@type='result'][@id='id4'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", - "!/iq/mam:fin[@complete='true']/rsm:set")), - - ]), - 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}']", - "/iq/mam:fin[@complete='true']/rsm:set")), - ]), - Scenario("join_history_limits", - [ - 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))), - - # join some other channel, to stay connected to the server even after leaving #foo - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#DUMMY%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"), - partial(expect_stanza, "/message/subject"), - - # Leave #foo - partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"), - partial(expect_stanza, "/presence[@type='unavailable']"), - - # Rejoin #foo, with some history limit - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history maxchars='0'/></x></presence>"), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"), - partial(expect_stanza, "/message/subject"), - - partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"), - partial(expect_stanza, "/presence[@type='unavailable']"), - - # Rejoin #foo, with some history limit - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history maxstanzas='3'/></x></presence>"), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"), - partial(expect_stanza, - "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 2']"), - partial(expect_stanza, - "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"), - partial(expect_stanza, - "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"), - partial(expect_stanza, "/message/subject"), - - partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"), - partial(expect_stanza, "/presence[@type='unavailable']"), - - - # Rejoin #foo, with some history limit - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history since='{first_timestamp}'/></x></presence>"), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"), - partial(expect_stanza, - "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"), - partial(expect_stanza, - "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"), - partial(expect_stanza, "/message/subject"), - partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"), - partial(expect_stanza, "/presence[@type='unavailable']"), - - # Rejoin #foo, with some history limit - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history seconds='1'/></x></presence>"), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"), - partial(expect_stanza, - "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"), - partial(expect_stanza, - "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"), - partial(expect_stanza, "/message/subject"), - partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"), - partial(expect_stanza, "/presence[@type='unavailable']"), - - # Rejoin #foo, with some history limit - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history seconds='5'/></x></presence>"), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"), - partial(expect_stanza, - "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou']"), partial(expect_stanza, - "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 2']"), - partial(expect_stanza, - "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"), - partial(expect_stanza, - "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"), - partial(expect_stanza, "/message/subject"), - partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"), - partial(expect_stanza, "/presence[@type='unavailable']"), - - ]), - Scenario("mam_on_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}', fixed_irc_server=True), - 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())]"), - - partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' type='groupchat'><body>coucou</body></message>"), - partial(expect_stanza, "/message[@from='#foo@{biboumi_host}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']"), - - partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' type='groupchat'><body>coucou 2</body></message>"), - 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:2' queryid='qid1' /></iq>"), - - partial(expect_stanza, - ("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay", - "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo@{biboumi_host}/{nick_one}'][@type='groupchat']/client:body[text()='coucou']") - ), - partial(expect_stanza, - ("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay", - "/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>"), - 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()='0']") - ), - ] + [ - # 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 "99" - 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()='99']"), - after = partial(save_value, "last_uuid", partial(extract_attribute, "/message/mam:result", "id")) - ), - # And it should not be marked as complete - partial(expect_stanza, - ("/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", - "/iq/mam:fin/rsm:set/rsm:last[text()='{last_uuid}']", - "!/iq//mam:fin[@complete='true']", - "/iq//mam:fin")), - - # Retrieve the next page, using the “after” thingy - partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id2'><query xmlns='urn:xmpp:mam:2' queryid='qid2' ><set xmlns='http://jabber.org/protocol/rsm'><after>{last_uuid}</after></set></query></iq>"), - - partial(expect_stanza, - ("/message/mam:result[@queryid='qid2']/forward:forwarded/delay:delay", - "/message/mam:result[@queryid='qid2']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='100']") - ), - ] + 48 * [ - partial(expect_stanza, - ("/message/mam:result[@queryid='qid2']/forward:forwarded/delay:delay", - "/message/mam:result[@queryid='qid2']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body") - ), - ] + [ - partial(expect_stanza, - ("/message/mam:result[@queryid='qid2']/forward:forwarded/delay:delay", - "/message/mam:result[@queryid='qid2']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='149']"), - after = partial(save_value, "last_uuid", partial(extract_attribute, "/message/mam:result", "id")) - ), - partial(expect_stanza, - ("/iq[@type='result'][@id='id2'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", - "/iq/mam:fin/rsm:set/rsm:last[text()='{last_uuid}']", - "/iq//mam:fin[@complete='true']", - "/iq//mam:fin")), - - # Send a request with a non-existing ID set as the “after” value. - partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id3'><query xmlns='urn:xmpp:mam:2' queryid='qid3' ><set xmlns='http://jabber.org/protocol/rsm'><after>DUMMY_ID</after></set></query></iq>"), - partial(expect_stanza, "/iq[@id='id3'][@type='error']/error[@type='cancel']/stanza:item-not-found"), - - # Request the last page just BEFORE the last message in the archive - partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id3'><query xmlns='urn:xmpp:mam:2' queryid='qid3' ><set xmlns='http://jabber.org/protocol/rsm'><before></before></set></query></iq>"), - - partial(expect_stanza, - ("/message/mam:result[@queryid='qid3']/forward:forwarded/delay:delay", - "/message/mam:result[@queryid='qid3']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='50']") - ), - ] + 98 * [ - partial(expect_stanza, - ("/message/mam:result[@queryid='qid3']/forward:forwarded/delay:delay", - "/message/mam:result[@queryid='qid3']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body") - ), - ] + [ - partial(expect_stanza, - ("/message/mam:result[@queryid='qid3']/forward:forwarded/delay:delay", - "/message/mam:result[@queryid='qid3']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='149']"), - after = partial(save_value, "last_uuid", partial(extract_attribute, "/message/mam:result", "id")) - ), - partial(expect_stanza, - ("/iq[@type='result'][@id='id3'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", - "/iq/mam:fin/rsm:set/rsm:last[text()='{last_uuid}']", - "!/iq//mam:fin[@complete='true']", - "/iq//mam:fin")), - - # Do the same thing, but with a limit value. - 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'><before>{last_uuid}</before><max>2</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()='147']") - ), - 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()='148']"), - after = partial(save_value, "last_uuid", partial(extract_attribute, "/message/mam:result", "id")) - ), - partial(expect_stanza, - ("/iq[@type='result'][@id='id4'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", - "/iq/mam:fin/rsm:set/rsm:last[text()='{last_uuid}']", - "!/iq/mam:fin[@complete='true']",)), - - # Test if everything is fine even with weird max value: 0 - partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id5'><query xmlns='urn:xmpp:mam:2' queryid='qid5' ><set xmlns='http://jabber.org/protocol/rsm'><before></before><max>0</max></set></query></iq>"), - - partial(expect_stanza, - ("/iq[@type='result'][@id='id5'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", - "!/iq/mam:fin[@complete='true']",)), - ]), - Scenario("channel_history_on_fixed_server", - [ - handshake_sequence(), - # First user join - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' />"), - connection_sequence("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True), - 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'][@jid='~nick@localhost'][@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 one channel message - partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' type='groupchat'><body>coucou</body></message>"), - partial(expect_stanza, "/message[@from='#foo@{biboumi_host}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']"), - - # Second user joins - partial(send_stanza, - "<presence from='{jid_one}/{resource_two}' to='#foo@{biboumi_host}/{nick_one}' />"), - # connection_sequence("irc.localhost", '{jid_one}/{resource_two}'), - # partial(expect_stanza, - # "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - # Receive the history message - partial(expect_stanza, ("/message[@from='#foo@{biboumi_host}/{nick_one}']/body[text()='coucou']", - "/message/delay:delay[@from='#foo@{biboumi_host}']")), - - partial(expect_stanza, "/message[@from='#foo@{biboumi_host}'][@type='groupchat']/subject[not(text())]"), - ], conf="fixed_server"), - Scenario("channel_history", - [ - handshake_sequence(), - # First user join - 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())]"), - - # Send one channel message - 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']"), - - # Second user joins - partial(send_stanza, - "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - # Receive the history message - partial(expect_stanza, ("/message[@from='#foo%{irc_server_one}/{nick_one}']/body[text()='coucou']", - "/message/delay:delay[@from='#foo%{irc_server_one}']")), - - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - ]), - Scenario("simple_channel_list", - [ - 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())]"), - - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, - "/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"), - partial(expect_stanza, "/presence"), - partial(expect_stanza, "/message[@from='#bar%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - - 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", - "/iq/disco_items:query/disco_items:item[@jid='#foo%{irc_server_one}']", - "/iq/disco_items:query/disco_items:item[@jid='#bar%{irc_server_one}']" - )) - ]), - Scenario("channel_list_escaping", - [ - handshake_sequence(), - - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#true\\2ffalse%{irc_server_one}/{nick_one}' />"), - connection_sequence("irc.localhost", '{jid_one}/{resource_one}'), - partial(expect_stanza, - "/message/body[text()='Mode #true/false [+nt] by {irc_host_one}']"), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_one}'][@from='#true\\2ffalse%{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='#true\\2ffalse%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - ]), - Scenario("channel_list_with_rsm", - [ - 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())]"), - - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, - "/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"), - partial(expect_stanza, "/presence"), - partial(expect_stanza, "/message[@from='#bar%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#coucou%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, - "/message/body[text()='Mode #coucou [+nt] by {irc_host_one}']"), - partial(expect_stanza, "/presence"), - partial(expect_stanza, "/message[@from='#coucou%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), - - # 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", - )), - - # 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}']" - )), - - # 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", - "/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/disco_items:item[@jid='#foo%{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()='#foo%{irc_server_one}']", - "/iq/disco_items:query/rsm:set/rsm:count[text()='3']" - )), - - # 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", - "/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/rsm:set/rsm:count[text()='3']" - )), - - # 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='#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(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#aaa%{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_one}/{resource_one}' to='#bbb%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence"), - partial(expect_stanza, "/message"), - - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#ccc%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence"), - partial(expect_stanza, "/message"), - - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#ddd%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence"), - partial(expect_stanza, "/message"), - - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#eee%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence"), - partial(expect_stanza, "/message"), - - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#fff%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence"), - partial(expect_stanza, "/message"), - - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#ggg%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence"), - partial(expect_stanza, "/message"), - - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#hhh%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence"), - partial(expect_stanza, "/message"), - - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#iii%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence"), - partial(expect_stanza, "/message"), - - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#jjj%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence"), - partial(expect_stanza, "/message"), - - 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", - "/iq/disco_items:query/disco_items:item[@jid='#aaa%{irc_server_one}']", - "/iq/disco_items:query/disco_items:item[@jid='#bbb%{irc_server_one}']", - "/iq/disco_items:query/disco_items:item[@jid='#ccc%{irc_server_one}']", - "/iq/disco_items:query/rsm:set/rsm:first[text()='#aaa%{irc_server_one}'][@index='0']", - "/iq/disco_items:query/rsm:set/rsm:last[text()='#ccc%{irc_server_one}']" - )), - - 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", - "/iq/disco_items:query/disco_items:item[@jid='#ddd%{irc_server_one}']", - "/iq/disco_items:query/disco_items:item[@jid='#eee%{irc_server_one}']", - "/iq/disco_items:query/disco_items:item[@jid='#fff%{irc_server_one}']", - "/iq/disco_items:query/rsm:set/rsm:first[text()='#ddd%{irc_server_one}'][@index='3']", - "/iq/disco_items:query/rsm:set/rsm:last[text()='#fff%{irc_server_one}']" - )), - - 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>#fff%{irc_server_one}</after><max>3</max></set></query></iq>"), - partial(expect_stanza, ( - "/iq[@type='result']/disco_items:query", - "/iq/disco_items:query/disco_items:item[@jid='#ggg%{irc_server_one}']", - "/iq/disco_items:query/disco_items:item[@jid='#hhh%{irc_server_one}']", - "/iq/disco_items:query/disco_items:item[@jid='#iii%{irc_server_one}']", - "/iq/disco_items:query/rsm:set/rsm:first[text()='#ggg%{irc_server_one}'][@index='6']", - "/iq/disco_items:query/rsm:set/rsm:last[text()='#iii%{irc_server_one}']" - )), - - 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>#iii%{irc_server_one}</after><max>3</max></set></query></iq>"), - partial(expect_stanza, ( - "/iq[@type='result']/disco_items:query", - "/iq/disco_items:query/disco_items:item[@jid='#jjj%{irc_server_one}']", - "/iq/disco_items:query/rsm:set/rsm:first[text()='#jjj%{irc_server_one}'][@index='9']", - "/iq/disco_items:query/rsm:set/rsm:last[text()='#jjj%{irc_server_one}']", - "/iq/disco_items:query/rsm:set/rsm:count[text()='10']" - )), - - 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' />"), - partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#ddd%{irc_server_one}/{nick_one}' type='unavailable' />"), - partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#eee%{irc_server_one}/{nick_one}' type='unavailable' />"), - partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#fff%{irc_server_one}/{nick_one}' type='unavailable' />"), - partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#ggg%{irc_server_one}/{nick_one}' type='unavailable' />"), - partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#hhh%{irc_server_one}/{nick_one}' type='unavailable' />"), - partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#iii%{irc_server_one}/{nick_one}' type='unavailable' />"), - partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#jjj%{irc_server_one}/{nick_one}' type='unavailable' />"), - partial(expect_stanza, "/presence[@type='unavailable']"), - partial(expect_stanza, "/presence[@type='unavailable']"), - partial(expect_stanza, "/presence[@type='unavailable']"), - partial(expect_stanza, "/presence[@type='unavailable']"), - partial(expect_stanza, "/presence[@type='unavailable']"), - partial(expect_stanza, "/presence[@type='unavailable']"), - partial(expect_stanza, "/presence[@type='unavailable']"), - partial(expect_stanza, "/presence[@type='unavailable']"), - partial(expect_stanza, "/presence[@type='unavailable']"), - partial(expect_stanza, "/presence[@type='unavailable']") - ]), - Scenario("muc_traffic_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' node='http://jabber.org/protocol/muc#traffic'/></iq>"), - 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='#foo on {irc_host_one}']", - "/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='#foo on {irc_host_one}']", - "/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(), - 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>WHOIS {nick_one}</body></message>"), - partial(expect_stanza, "/message[@from='{irc_server_one}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} localhost * {nick_one}']"), - ]), - Scenario("raw_message_fixed_irc_server", - [ - 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}', fixed_irc_server=True), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence"), - partial(expect_stanza, "/message"), - - partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='{biboumi_host}' type='chat'><body>WHOIS {nick_one}</body></message>"), - partial(expect_stanza, "/message[@from='{biboumi_host}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} localhost * {nick_one}']"), - ], conf='fixed_server'), - Scenario("self_disco_info", - [ - handshake_sequence(), - partial(send_stanza, "<iq type='get' id='get1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>"), - partial(expect_stanza, - ("/iq[@type='result']/disco_info:query/disco_info:identity[@category='conference'][@type='irc'][@name='Biboumi XMPP-IRC gateway']", - "/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("invite_other", - [ - 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='#bar%{irc_server_one}@{biboumi_host}/{nick_two}' />"), - connection_sequence("irc.localhost", '{jid_two}/{resource_two}'), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence"), - partial(expect_stanza, "/message"), - partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><x xmlns='http://jabber.org/protocol/muc#user'><invite to='{nick_two}'/></x></message>"), - partial(expect_stanza, "/message/body[text()='{nick_two} has been invited to #foo']"), - partial(expect_stanza, "/message[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='#foo%{irc_server_one}/{nick_one}']"), - - partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><x xmlns='http://jabber.org/protocol/muc#user'><invite to='bertrand@example.com'/></x></message>"), - partial(expect_stanza, "/message[@to='bertrand@example.com'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='{jid_one}/{resource_one}']"), - ]), - Scenario("global_configure", - [ - handshake_sequence(), - partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{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 some global default settings.']", - "/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()='20']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='record_history']/dataform:value[text()='true']", - "/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")) - ), - partial(send_stanza, "<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='record_history'><value>0</value></field><field var='max_history_length'><value>42</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='{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 some global default settings.']", - "/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")) - ), - partial(send_stanza, "<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='{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']"), - ]), - Scenario("global_configure_fixed", - [ - handshake_sequence(), - partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='global-configure' action='execute' /></iq>"), - partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='global-configure'][@sessionid][@status='executing']", - "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure some global default settings.']", - "/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()='20']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='record_history']/dataform:value[text()='true']", - "/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='global-configure']", "sessionid")) - ), - partial(send_stanza, "<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='global-configure' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='record_history'><value>0</value></field><field var='max_history_length'><value>42</value></field></x></command></iq>"), - partial(expect_stanza, "/iq[@type='result']/commands:command[@node='global-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='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='global-configure' action='execute' /></iq>"), - partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='global-configure'][@sessionid][@status='executing']", - "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure some global default settings.']", - "/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='global-configure']", "sessionid")) - ), - partial(send_stanza, "<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' action='cancel' node='global-configure' sessionid='{sessionid}' /></iq>"), - partial(expect_stanza, "/iq[@type='result']/commands:command[@node='global-configure'][@status='canceled']"), - - partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='server-configure' action='execute' /></iq>"), - partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='server-configure'][@sessionid][@status='executing']",)) - ], conf='fixed_server'), - Scenario("global_configure_persistent_by_default", - [ - handshake_sequence(), - partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{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 some global default settings.']", - "/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()='20']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='record_history']/dataform:value[text()='true']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='persistent']/dataform:value[text()='true']", - "/iq/commands:command/commands:actions/commands:next", - ), - ), - ],conf='persistent_by_default'), - Scenario("irc_server_configure", - [ - handshake_sequence(), - partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"), - partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']", - "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure the IRC server irc.localhost']", - "/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Edit the form, to configure the settings of the IRC server irc.localhost']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='ports']/dataform:value[text()='6667']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='tls_ports']/dataform:value[text()='6670']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='tls_ports']/dataform:value[text()='6697']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='verify_cert']/dataform:value[text()='true']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='fingerprint']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-private'][@var='pass']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='nick']", - "/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']", - "/iq/commands:command/commands:actions/commands:next", - ), - after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid")) - ), - partial(send_stanza, "<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='{irc_server_one}'>" - "<command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='next'>" - "<x xmlns='jabber:x:data' type='submit'>" - "<field var='ports' />" - "<field var='tls_ports'><value>6697</value><value>6698</value></field>" - "<field var='verify_cert'><value>1</value></field>" - "<field var='fingerprint'><value>12:12:12</value></field>" - "<field var='pass'><value>coucou</value></field>" - "<field var='after_connect_commands'><value>first command</value><value>second command</value></field>" - "<field var='nick'><value>my_nickname</value></field>" - "<field var='username'><value>username</value></field>" - "<field var='realname'><value>realname</value></field>" - "<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='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"), - partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']", - "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure the IRC server irc.localhost']", - "/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Edit the form, to configure the settings of the IRC server irc.localhost']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='tls_ports']/dataform:value[text()='6697']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='tls_ports']/dataform:value[text()='6698']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='verify_cert']/dataform:value[text()='true']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='fingerprint']/dataform:value[text()='12:12:12']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-private'][@var='pass']/dataform:value[text()='coucou']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='nick']/dataform:value[text()='my_nickname']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']/dataform:value[text()='first command']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']/dataform:value[text()='second command']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='username']/dataform:value[text()='username']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='realname']/dataform:value[text()='realname']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']/dataform:value[text()='latin-1']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_out']/dataform:value[text()='UTF-8']", - "/iq/commands:command/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='{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']"), - - # Same thing, but try to empty some values - 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='pass'><value></value></field>" - "<field var='after_connect_commands'></field>" - "<field var='username'><value></value></field>" - "<field var='realname'><value></value></field>" - "<field var='encoding_out'><value></value></field>" - "<field var='encoding_in'><value></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='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"), - partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']", - "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure the IRC server irc.localhost']", - "/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Edit the form, to configure the settings of the IRC server irc.localhost']", - "!/iq/commands:command/dataform:x/dataform:field[@var='tls_ports']/dataform:value", - "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='pass']/dataform:value", - "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='after_connect_commands']/dataform:value", - "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='username']/dataform:value", - "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='realname']/dataform:value", - "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='encoding_in']/dataform:value", - "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='encoding_out']/dataform:value", - "/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='{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>" - "<field var='nick'><value>my_special_nickname</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}/my_special_nickname']/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(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(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, "<iq type='set' id='command2' 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, r"/iq/commands:command/commands:note[re:test(text(), 'Connected to IRC server irc.localhost on port 6667 since \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \(\d+ seconds ago\)\.\n#foo from 1 resource: {resource_one}.*')]"), - ]), - Scenario("get_irc_connection_info_fixed", - [ - handshake_sequence(), - - 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(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' />"), - connection_sequence("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True), - partial(expect_stanza, "/message"), - partial(expect_stanza, "/presence"), - partial(expect_stanza, "/message"), - - partial(send_stanza, "<iq type='set' id='command2' 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, r"/iq/commands:command/commands:note[re:test(text(), 'Connected to IRC server irc.localhost on port 6667 since \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \(\d+ seconds ago\)\.\n#foo from 1 resource: {resource_one}.*')]"), - ], 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' />") - ]), - Scenario("basic_subscribe_unsubscribe", - [ - handshake_sequence(), - - # Mutual subscription exchange - partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='subscribe' id='subid1' />"), - partial(expect_stanza, "/presence[@type='subscribed'][@id='subid1']"), - - # Get the current presence of the biboumi gateway - partial(expect_stanza, "/presence"), - - partial(expect_stanza, "/presence[@type='subscribe']"), - partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='subscribed' />"), - - - # Unsubscribe - partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='unsubscribe' id='unsubid1' />"), - partial(expect_stanza, "/presence[@type='unavailable']"), - partial(expect_stanza, "/presence[@type='unsubscribed']"), - partial(expect_stanza, "/presence[@type='unsubscribe']"), - partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='unavailable' />"), - partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='unsubscribed' />"), - ]), - Scenario("resource_is_removed_from_server_when_last_chan_is_left", - [ - # Join the channel - handshake_sequence(), - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), - connection_sequence("irc.localhost", '{jid_one}/{resource_one}'), - partial(expect_stanza, - "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[not(text())]"), - - # Make it persistent - partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='conf1' to='#foo%{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"), - partial(expect_stanza, "/iq[@type='result']/muc_owner:query/dataform:x/dataform:field[@var='persistent'][@type='boolean']/dataform:value[text()='false']"), - partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='conf2' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#owner'><x type='submit' xmlns='jabber:x:data'><field var='persistent' xmlns='jabber:x:data'><value>true</value></field></x></query></iq>"), - partial(expect_stanza, "/iq[@type='result']"), - - partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />"), - partial(expect_stanza, "/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}']"), - - # Join the same channel, with the same JID, but a different resource - partial(send_stanza, - "<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, - ("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", - "/presence/muc_user:x/muc_user:status[@code='110']") - ), - partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"), - - # Join some other channel with someone else - partial(send_stanza, - "<presence from='{jid_two}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' />"), - connection_sequence("irc.localhost", '{jid_two}/{resource_one}'), - partial(expect_stanza, - "/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"), - partial(expect_stanza, - ("/presence[@to='{jid_two}/{resource_one}'][@from='#bar%{irc_server_one}/{nick_two}']/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_two}/{resource_one}']/subject[not(text())]"), - - # Send two messages from the second user to the first one - partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>kikoo</body></message>"), - partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>second kikoo</body></message>"), - - # We must receive each message only once, no duplicate - partial(expect_stanza, "/message/body[text()='kikoo']"), - partial(expect_stanza, "/message/body[text()='second kikoo']"), - ] - ), - Scenario("irc_server_presence_in_roster", - [ - handshake_sequence(), - - # Mutual subscription exchange - partial(send_stanza, "<presence from='{jid_one}' to='{irc_server_one}' type='subscribe' id='subid1' />"), - partial(expect_stanza, "/presence[@type='subscribed'][@id='subid1']"), - - partial(expect_stanza, "/presence[@type='subscribe']"), - partial(send_stanza, "<presence from='{jid_one}' to='{irc_server_one}' type='subscribed' />"), - - # Join a channel on that server - partial(send_stanza, - "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), - - # We must receive the IRC server presence, in the connection sequence - connection_sequence("irc.localhost", '{jid_one}/{resource_one}', expected_irc_presence=True), - 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())]"), - - # Leave the channel, and thus the IRC server - partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), - partial(expect_stanza, "/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}']"), - partial(expect_stanza, "/presence[@from='{irc_server_one}'][@to='{jid_one}'][@type='unavailable']"), - ]) - ) + provided_scenar_names = sys.argv[1:] + scenarios = get_scenarios(os.path.abspath(os.path.dirname(__file__)), provided_scenar_names) - failures = 0 - - scenar_list = sys.argv[1:] irc_output = open("irc_output.txt", "w") irc = IrcServerRunner() print("Starting irc server…") @@ -3244,20 +326,21 @@ if __name__ == '__main__': if not res: print("IRC server failed to start, see irc_output.txt for more details. Exiting…") sys.exit(1) - if b"now running in foreground mode" in res: + if b"Server running" in res: break print("irc server started.") - 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) + number_of_checks = len([s for s in scenarios if s.name in provided_scenar_names]) if provided_scenar_names else len(scenarios) + print("Running %s checks for biboumi." % number_of_checks) + + failures = 0 for s in scenarios: - if scenar_list and s.name not in scenar_list: - continue test = BiboumiTest(s) if not test.run(): print("You can check the files slixmpp_%s_output.txt and biboumi_%s_output.txt to help you debug." % (s.name, s.name)) failures += 1 + sys.stdout.flush() print("Waiting for irc server to exit…") irc.stop() @@ -3269,3 +352,4 @@ if __name__ == '__main__': sys.exit(1) else: print("All tests passed successfully") + diff --git a/tests/end_to_end/functions.py b/tests/end_to_end/functions.py new file mode 100644 index 0000000..3a21fcf --- /dev/null +++ b/tests/end_to_end/functions.py @@ -0,0 +1,169 @@ +from functools import partial +import collections +import datetime +import asyncio +import time +import lxml.etree +import io + +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', + 'nick_one': 'Nick', + 'jid_one': 'first@example.com', + 'jid_two': 'second@example.com', + 'jid_admin': 'admin@example.com', + 'nick_two': 'Nick2', + 'nick_three': 'Nick3', + 'lower_nick_one': 'nick', + 'lower_nick_two': 'nick2', +} + +class SkipStepError(Exception): + """ + Raised by a step when it needs to be skiped, by running + the next available step immediately. + """ + pass + +class StanzaError(Exception): + """ + Raised when a step fails. + """ + pass + +def match(stanza, xpath): + tree = lxml.etree.parse(io.StringIO(str(stanza))) + matched = tree.xpath(xpath, namespaces={'re': 'http://exslt.org/regular-expressions', + 'muc_user': 'http://jabber.org/protocol/muc#user', + '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', + 'disco_items': 'http://jabber.org/protocol/disco#items', + 'commands': 'http://jabber.org/protocol/commands', + 'dataform': 'jabber:x:data', + 'version': 'jabber:iq:version', + 'mam': 'urn:xmpp:mam:2', + 'rms': 'http://jabber.org/protocol/rsm', + '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', + '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 (expected and not matched) or (not expected and matched): + raise StanzaError("Received stanza\n%s\ndid not match expected xpath\n%s" % (stanza, real_xpath)) + if after: + if isinstance(after, collections.Iterable): + for af in after: + af(stanza, xmpp) + else: + after(stanza, xmpp) + +def check_xpath_optional(xpaths, xmpp, after, stanza): + try: + check_xpath(xpaths, xmpp, after, stanza) + except StanzaError: + raise SkipStepError() + +def all_xpaths_match(stanza, xpaths): + try: + check_xpath(xpaths, None, None, stanza) + except StanzaError: + return False + return True + +def check_list_of_xpath(list_of_xpaths, xmpp, stanza): + found = False + for i, xpaths in enumerate(list_of_xpaths): + if all_xpaths_match(stanza, xpaths): + found = True + list_of_xpaths.pop(i) + break + + if not found: + raise StanzaError("Received stanza “%s” did not match any of the expected xpaths:\n%s" % (stanza, list_of_xpaths)) + + if list_of_xpaths: + step = partial(expect_unordered_already_formatted, list_of_xpaths) + xmpp.scenario.steps.insert(0, step) + +def extract_attribute(xpath, name): + def f(xpath, name, stanza): + matched = match(stanza, xpath) + return matched[0].get(name) + return partial(f, xpath, name) + +def extract_text(xpath, stanza): + matched = match(stanza, xpath) + return matched[0].text + +def save_value(name, func): + def f(name, func, stanza, xmpp): + xmpp.saved_values[name] = func(stanza) + return partial(f, name, func) + +def expect_stanza(*args, optional=False, after=None): + def f(*xpaths, xmpp, biboumi, optional, after): + replacements = common_replacements + replacements.update(xmpp.saved_values) + check_func = check_xpath if not optional else check_xpath_optional + formatted_xpaths = [xpath.format_map(replacements) for xpath in xpaths] + xmpp.stanza_checker = partial(check_func, formatted_xpaths, xmpp, after) + xmpp.timeout_handler = asyncio.get_event_loop().call_later(10, partial(xmpp.on_timeout, formatted_xpaths)) + return partial(f, *args, optional=optional, after=after) + +def send_stanza(stanza): + def internal(stanza, xmpp, biboumi): + replacements = common_replacements + replacements.update(xmpp.saved_values) + xmpp.send_raw(stanza.format_map(replacements)) + asyncio.get_event_loop().call_soon(xmpp.run_scenario) + return partial(internal, stanza) + +def expect_unordered(*args): + def f(*lists_of_xpaths, xmpp, biboumi): + formatted_list_of_xpaths = [] + for list_of_xpaths in lists_of_xpaths: + formatted_xpaths = [] + for xpath in list_of_xpaths: + formatted_xpath = xpath.format_map(common_replacements) + formatted_xpaths.append(formatted_xpath) + formatted_list_of_xpaths.append(tuple(formatted_xpaths)) + expect_unordered_already_formatted(formatted_list_of_xpaths, xmpp, biboumi) + xmpp.timeout_handler = asyncio.get_event_loop().call_later(10, partial(xmpp.on_timeout, formatted_list_of_xpaths)) + return partial(f, *args) + +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 sleep_for(duration): + def f(duration, xmpp, biboumi): + time.sleep(duration) + asyncio.get_event_loop().call_soon(xmpp.run_scenario) + return partial(f, duration) + +def save_current_timestamp_plus_delta(key, delta): + def f(key, delta, message, xmpp): + now_plus_delta = datetime.datetime.utcnow() + delta + xmpp.saved_values[key] = now_plus_delta.strftime("%FT%T.967Z") + return partial(f, key, delta) diff --git a/tests/end_to_end/ircd.conf b/tests/end_to_end/ircd.conf deleted file mode 100644 index ccfbd90..0000000 --- a/tests/end_to_end/ircd.conf +++ /dev/null @@ -1,511 +0,0 @@ -/* doc/ircd.conf.example - brief example configuration file - * - * Copyright (C) 2000-2002 Hybrid Development Team - * Copyright (C) 2002-2005 ircd-ratbox development team - * Copyright (C) 2005-2006 charybdis development team - * - * See reference.conf for more information. - */ - -/* Extensions */ -#loadmodule "extensions/chm_operonly_compat"; -#loadmodule "extensions/chm_quietunreg_compat"; -#loadmodule "extensions/chm_sslonly_compat"; -#loadmodule "extensions/chm_operpeace"; -#loadmodule "extensions/createauthonly"; -#loadmodule "extensions/extb_account"; -#loadmodule "extensions/extb_canjoin"; -#loadmodule "extensions/extb_channel"; -#loadmodule "extensions/extb_combi"; -#loadmodule "extensions/extb_extgecos"; -#loadmodule "extensions/extb_hostmask"; -#loadmodule "extensions/extb_oper"; -#loadmodule "extensions/extb_realname"; -#loadmodule "extensions/extb_server"; -#loadmodule "extensions/extb_ssl"; -#loadmodule "extensions/extb_usermode"; -#loadmodule "extensions/hurt"; -#loadmodule "extensions/m_extendchans"; -#loadmodule "extensions/m_findforwards"; -#loadmodule "extensions/m_identify"; -#loadmodule "extensions/m_locops"; -#loadmodule "extensions/no_oper_invis"; -#loadmodule "extensions/sno_farconnect"; -#loadmodule "extensions/sno_globalkline"; -#loadmodule "extensions/sno_globalnickchange"; -#loadmodule "extensions/sno_globaloper"; -#loadmodule "extensions/sno_whois"; -#loadmodule "extensions/override"; -#loadmodule "extensions/no_kill_services"; - -/* - * IP cloaking extensions: use ip_cloaking_4.0 - * if you're linking 3.2 and later, otherwise use - * ip_cloaking, for compatibility with older 3.x - * releases. - */ - -#loadmodule "extensions/ip_cloaking_4.0"; -#loadmodule "extensions/ip_cloaking"; - -serverinfo { - name = "irc.localhost"; - sid = "42X"; - description = "charybdis test server"; - network_name = "StaticBox"; - - /* On multi-homed hosts you may need the following. These define - * the addresses we connect from to other servers. */ - /* for IPv4 */ - #vhost = "192.0.2.6"; - /* for IPv6 */ - #vhost6 = "2001:db8:2::6"; - - /* ssl_private_key: our ssl private key */ - ssl_private_key = "etc/ssl.key"; - - /* ssl_cert: certificate for our ssl server */ - ssl_cert = "etc/ssl.pem"; - - /* ssl_dh_params: DH parameters, generate with openssl dhparam -out dh.pem 2048 - * In general, the DH parameters size should be the same as your key's size. - * However it has been reported that some clients have broken TLS implementations which may - * choke on keysizes larger than 2048-bit, so we would recommend using 2048-bit DH parameters - * for now if your keys are larger than 2048-bit. - */ - ssl_dh_params = "etc/dh.pem"; - - /* ssld_count: number of ssld processes you want to start, if you - * have a really busy server, using N-1 where N is the number of - * cpu/cpu cores you have might be useful. A number greater than one - * can also be useful in case of bugs in ssld and because ssld needs - * two file descriptors per SSL connection. - */ - ssld_count = 1; - - /* default max clients: the default maximum number of clients - * allowed to connect. This can be changed once ircd has started by - * issuing: - * /quote set maxclients <limit> - */ - default_max_clients = 1024; - - /* nicklen: enforced nickname length (for this server only; must not - * be longer than the maximum length set while building). - */ - nicklen = 30; -}; - -admin { - name = "Lazy admin (lazya)"; - description = "StaticBox client server"; - email = "nobody@127.0.0.1"; -}; - -log { - fname_userlog = "logs/userlog"; - #fname_fuserlog = "logs/fuserlog"; - fname_operlog = "logs/operlog"; - #fname_foperlog = "logs/foperlog"; - fname_serverlog = "logs/serverlog"; - #fname_klinelog = "logs/klinelog"; - fname_killlog = "logs/killlog"; - fname_operspylog = "logs/operspylog"; - #fname_ioerrorlog = "logs/ioerror"; -}; - -/* class {} blocks MUST be specified before anything that uses them. That - * means they must be defined before auth {} and before connect {}. - */ -class "users" { - ping_time = 2 minutes; - number_per_ident = 10; - number_per_ip = 10; - number_per_ip_global = 50; - cidr_ipv4_bitlen = 24; - cidr_ipv6_bitlen = 64; - number_per_cidr = 200; - max_number = 3000; - sendq = 400 kbytes; -}; - -class "opers" { - ping_time = 5 minutes; - number_per_ip = 10; - max_number = 1000; - sendq = 1 megabyte; -}; - -class "server" { - ping_time = 5 minutes; - connectfreq = 5 minutes; - max_number = 1; - sendq = 4 megabytes; -}; - -listen { - /* defer_accept: wait for clients to send IRC handshake data before - * accepting them. if you intend to use software which depends on the - * server replying first, such as BOPM, you should disable this feature. - * otherwise, you probably want to leave it on. - */ - defer_accept = yes; - - /* If you want to listen on a specific IP only, specify host. - * host definitions apply only to the following port line. - */ - #host = "192.0.2.6"; - port = 5000, 6665 .. 6669; - sslport = 7778; - - /* Listen on IPv6 (if you used host= above). */ - #host = "2001:db8:2::6"; - #port = 5000, 6665 .. 6669; - #sslport = 9999; -}; - -/* auth {}: allow users to connect to the ircd (OLD I:) - * auth {} blocks MUST be specified in order of precedence. The first one - * that matches a user will be used. So place spoofs first, then specials, - * then general access, then restricted. - */ -auth { - /* user: the user@host allowed to connect. Multiple IPv4/IPv6 user - * lines are permitted per auth block. This is matched against the - * hostname and IP address (using :: shortening for IPv6 and - * prepending a 0 if it starts with a colon) and can also use CIDR - * masks. - */ - user = "*@198.51.100.0/24"; - user = "*test@2001:db8:1:*"; - - /* password: an optional password that is required to use this block. - * By default this is not encrypted, specify the flag "encrypted" in - * flags = ...; below if it is. - */ - password = "letmein"; - - /* spoof: fake the users user@host to be be this. You may either - * specify a host or a user@host to spoof to. This is free-form, - * just do everyone a favour and dont abuse it. (OLD I: = flag) - */ - spoof = "I.still.hate.packets"; - - /* Possible flags in auth: - * - * encrypted | password is encrypted with mkpasswd - * spoof_notice | give a notice when spoofing hosts - * exceed_limit (old > flag) | allow user to exceed class user limits - * kline_exempt (old ^ flag) | exempt this user from k/g/xlines, - * | dnsbls, and proxies - * proxy_exempt | exempt this user from proxies - * dnsbl_exempt | exempt this user from dnsbls - * spambot_exempt | exempt this user from spambot checks - * shide_exempt | exempt this user from serverhiding - * jupe_exempt | exempt this user from generating - * warnings joining juped channels - * resv_exempt | exempt this user from resvs - * flood_exempt | exempt this user from flood limits - * USE WITH CAUTION. - * no_tilde (old - flag) | don't prefix ~ to username if no ident - * need_ident (old + flag) | require ident for user in this class - * need_ssl | require SSL/TLS for user in this class - * need_sasl | require SASL id for user in this class - */ - flags = kline_exempt, exceed_limit; - - /* class: the class the user is placed in */ - class = "opers"; -}; - -auth { - user = "*@*"; - class = "users"; - flags = flood_exempt; -}; - -/* privset {} blocks MUST be specified before anything that uses them. That - * means they must be defined before operator {}. - */ -privset "local_op" { - privs = oper:local_kill, oper:operwall; -}; - -privset "server_bot" { - extends = "local_op"; - privs = oper:kline, oper:remoteban, snomask:nick_changes; -}; - -privset "global_op" { - extends = "local_op"; - privs = oper:global_kill, oper:routing, oper:kline, oper:unkline, oper:xline, - oper:resv, oper:mass_notice, oper:remoteban; -}; - -privset "admin" { - extends = "global_op"; - privs = oper:admin, oper:die, oper:rehash, oper:spy, oper:grant; -}; - -operator "god" { - /* name: the name of the oper must go above */ - - /* user: the user@host required for this operator. CIDR *is* - * supported now. auth{} spoofs work here, other spoofs do not. - * multiple user="" lines are supported. - */ - user = "*god@127.0.0.1"; - - /* password: the password required to oper. Unless ~encrypted is - * contained in flags = ...; this will need to be encrypted using - * mkpasswd, MD5 is supported - */ - password = "etcnjl8juSU1E"; - - /* rsa key: the public key for this oper when using Challenge. - * A password should not be defined when this is used, see - * doc/challenge.txt for more information. - */ - #rsa_public_key_file = "/usr/local/ircd/etc/oper.pub"; - - /* umodes: the specific umodes this oper gets when they oper. - * If this is specified an oper will not be given oper_umodes - * These are described above oper_only_umodes in general {}; - */ - #umodes = locops, servnotice, operwall, wallop; - - /* fingerprint: if specified, the oper's client certificate - * fingerprint will be checked against the specified fingerprint - * below. - */ - #fingerprint = "c77106576abf7f9f90cca0f63874a60f2e40a64b"; - - /* snomask: specific server notice mask on oper up. - * If this is specified an oper will not be given oper_snomask. - */ - snomask = "+Zbfkrsuy"; - - /* flags: misc options for the operator. You may prefix an option - * with ~ to disable it, e.g. ~encrypted. - * - * Default flags are encrypted. - * - * Available options: - * - * encrypted: the password above is encrypted [DEFAULT] - * need_ssl: must be using SSL/TLS to oper up - */ - flags = encrypted; - - /* privset: privileges set to grant */ - privset = "admin"; -}; - -connect "irc.uplink.com" { - host = "203.0.113.3"; - send_password = "password"; - accept_password = "anotherpassword"; - port = 6666; - hub_mask = "*"; - class = "server"; - flags = compressed, topicburst; - - #fingerprint = "c77106576abf7f9f90cca0f63874a60f2e40a64b"; - - /* If the connection is IPv6, uncomment below. - * Use 0::1, not ::1, for IPv6 localhost. */ - #aftype = ipv6; -}; - -connect "ssl.uplink.com" { - host = "203.0.113.129"; - send_password = "password"; - accept_password = "anotherpassword"; - port = 9999; - hub_mask = "*"; - class = "server"; - flags = ssl, topicburst; -}; - -service { - name = "services.int"; -}; - -cluster { - name = "*"; - flags = kline, tkline, unkline, xline, txline, unxline, resv, tresv, unresv; -}; - -shared { - oper = "*@*", "*"; - flags = all, rehash; -}; - -/* exempt {}: IPs that are exempt from Dlines and rejectcache. (OLD d:) */ -exempt { - ip = "127.0.0.1"; -}; - -channel { - use_invex = yes; - use_except = yes; - use_forward = yes; - use_knock = yes; - knock_delay = 5 minutes; - knock_delay_channel = 1 minute; - max_chans_per_user = 140; - max_chans_per_user_large = 200; - max_bans = 100; - max_bans_large = 500; - default_split_user_count = 0; - default_split_server_count = 0; - no_create_on_split = no; - no_join_on_split = no; - burst_topicwho = yes; - kick_on_split_riding = no; - only_ascii_channels = no; - resv_forcepart = yes; - channel_target_change = yes; - disable_local_channels = no; - autochanmodes = "+nt"; - displayed_usercount = 3; - strip_topic_colors = no; -}; - -serverhide { - flatten_links = yes; - links_delay = 5 minutes; - hidden = no; - disable_hidden = no; -}; - -alias "NickServ" { - target = "NickServ"; -}; - -alias "ChanServ" { - target = "ChanServ"; -}; - -alias "OperServ" { - target = "OperServ"; -}; - -alias "MemoServ" { - target = "MemoServ"; -}; - -alias "NS" { - target = "NickServ"; -}; - -alias "CS" { - target = "ChanServ"; -}; - -alias "OS" { - target = "OperServ"; -}; - -alias "MS" { - target = "MemoServ"; -}; - -general { - hide_error_messages = opers; - hide_spoof_ips = yes; - - /* - * default_umodes: umodes to enable on connect. - * If you have enabled the new ip_cloaking_4.0 module, and you want - * to make use of it, add +x to this option, i.e.: - * default_umodes = "+ix"; - * - * If you have enabled the old ip_cloaking module, and you want - * to make use of it, add +h to this option, i.e.: - * default_umodes = "+ih"; - */ - default_umodes = "+i"; - - default_operstring = "is an IRC Operator"; - default_adminstring = "is a Server Administrator"; - servicestring = "is a Network Service"; - - /* - * Nick of the network's SASL agent. Used to check whether services are here, - * SASL credentials are only sent to its server. Needs to be a service. - * - * Defaults to SaslServ if unspecified. - */ - sasl_service = "SaslServ"; - disable_fake_channels = no; - tkline_expire_notices = no; - default_floodcount = 10; - failed_oper_notice = yes; - dots_in_ident=2; - min_nonwildcard = 4; - min_nonwildcard_simple = 3; - max_accept = 100; - max_monitor = 100; - anti_nick_flood = yes; - max_nick_time = 20 seconds; - max_nick_changes = 5; - anti_spam_exit_message_time = 5 minutes; - ts_warn_delta = 30 seconds; - ts_max_delta = 5 minutes; - client_exit = yes; - collision_fnc = yes; - resv_fnc = yes; - global_snotices = yes; - dline_with_reason = yes; - kline_delay = 0 seconds; - kline_with_reason = yes; - kline_reason = "K-Lined"; - identify_service = "NickServ@services.int"; - identify_command = "IDENTIFY"; - non_redundant_klines = yes; - warn_no_nline = yes; - use_propagated_bans = yes; - stats_e_disabled = yes; - stats_c_oper_only=no; - stats_h_oper_only=no; - stats_y_oper_only=no; - stats_o_oper_only=yes; - stats_P_oper_only=no; - stats_i_oper_only=masked; - stats_k_oper_only=masked; - map_oper_only = no; - operspy_admin_only = no; - operspy_dont_care_user_info = no; - caller_id_wait = 1 minute; - pace_wait_simple = 0 second; - pace_wait = 0 seconds; - short_motd = no; - ping_cookie = no; - connect_timeout = 30 seconds; - default_ident_timeout = 5; - disable_auth = no; - no_oper_flood = yes; - max_targets = 4; - client_flood_max_lines = 20; - use_whois_actually = no; - oper_only_umodes = operwall, locops, servnotice; - oper_umodes = locops, servnotice, operwall, wallop; - oper_snomask = "+s"; - burst_away = yes; - nick_delay = 0 seconds; # 15 minutes if you want to enable this - reject_ban_time = 1 minute; - reject_after_count = 3; - reject_duration = 5 minutes; - throttle_duration = 60; - throttle_count = 8888; - max_ratelimit_tokens = 30; - away_interval = 30; - certfp_method = sha1; - hide_opers_in_whois = no; -}; - -modules { - path = "modules"; - path = "modules/autoload"; -}; diff --git a/tests/end_to_end/ircd.yaml b/tests/end_to_end/ircd.yaml new file mode 100644 index 0000000..057674c --- /dev/null +++ b/tests/end_to_end/ircd.yaml @@ -0,0 +1,751 @@ +# oragono IRCd config + +# network configuration +network: + # name of the network + name: BiboumiTest + +# server configuration +server: + # server name + name: irc.localhost + + # addresses to listen on + listeners: + # The standard plaintext port for IRC is 6667. Allowing plaintext over the + # public Internet poses serious security and privacy issues. Accordingly, + # we recommend using plaintext only on local (loopback) interfaces: + # "127.0.0.1:6667": # (loopback ipv4, localhost-only) + # "[::1]:6667": # (loopback ipv6, localhost-only) + # If you need to serve plaintext on public interfaces, comment out the above + # two lines and uncomment the line below (which listens on all interfaces): + ":6667": + # Alternately, if you have a TLS certificate issued by a recognized CA, + # you can configure port 6667 as an STS-only listener that only serves + # "redirects" to the TLS port, but doesn't allow chat. See the manual + # for details. + + # The standard SSL/TLS port for IRC is 6697. This will listen on all interfaces: + ":7778": + tls: + key: tls.key + cert: tls.crt + # 'proxy' should typically be false. It's only for Kubernetes-style load + # balancing that does not terminate TLS, but sends an initial PROXY line + # in plaintext. + proxy: false + + # Example of a Unix domain socket for proxying: + # "/tmp/oragono_sock": + + # Example of a Tor listener: any connection that comes in on this listener will + # be considered a Tor connection. It is strongly recommended that this listener + # *not* be on a public interface --- it should be on 127.0.0.0/8 or unix domain: + # "/hidden_service_sockets/oragono_tor_sock": + # tor: true + + # sets the permissions for Unix listen sockets. on a typical Linux system, + # the default is 0775 or 0755, which prevents other users/groups from connecting + # to the socket. With 0777, it behaves like a normal TCP socket + # where anyone can connect. + unix-bind-mode: 0777 + + # configure the behavior of Tor listeners (ignored if you didn't enable any): + tor-listeners: + # if this is true, connections from Tor must authenticate with SASL + require-sasl: false + + # what hostname should be displayed for Tor connections? + vhost: "tor-network.onion" + + # allow at most this many connections at once (0 for no limit): + max-connections: 64 + + # connection throttling (limit how many connection attempts are allowed at once): + throttle-duration: 10m + # set to 0 to disable throttling: + max-connections-per-duration: 64 + + # strict transport security, to get clients to automagically use TLS + sts: + # whether to advertise STS + # + # to stop advertising STS, leave this enabled and set 'duration' below to "0". this will + # advertise to connecting users that the STS policy they have saved is no longer valid + enabled: false + + # how long clients should be forced to use TLS for. + # setting this to a too-long time will mean bad things if you later remove your TLS. + # the default duration below is 1 month, 2 days and 5 minutes. + duration: 1mo2d5m + + # tls port - you should be listening on this port above + port: 6697 + + # should clients include this STS policy when they ship their inbuilt preload lists? + preload: false + + # casemapping controls what kinds of strings are permitted as identifiers (nicknames, + # channel names, account names, etc.), and how they are normalized for case. + # with the recommended default of 'precis', utf-8 identifiers that are "sane" + # (according to RFC 8265) are allowed, and the server additionally tries to protect + # against confusable characters ("homoglyph attacks"). + # the other options are 'ascii' (traditional ASCII-only identifiers), and 'permissive', + # which allows identifiers to contain unusual characters like emoji, but makes users + # vulnerable to homoglyph attacks. unless you're really confident in your decision, + # we recommend leaving this value at its default (changing it once the network is + # already up and running is problematic). + casemapping: "precis" + + # whether to look up user hostnames with reverse DNS + # (to suppress this for privacy purposes, use the ip-cloaking options below) + lookup-hostnames: true + # whether to confirm hostname lookups using "forward-confirmed reverse DNS", i.e., for + # any hostname returned from reverse DNS, resolve it back to an IP address and reject it + # unless it matches the connecting IP + forward-confirm-hostnames: true + + # use ident protocol to get usernames + check-ident: false + + # password to login to the server + # generated using "oragono genpasswd" + #password: "" + + # motd filename + # if you change the motd, you should move it to ircd.motd + motd: + + # motd formatting codes + # if this is true, the motd is escaped using formatting codes like $c, $b, and $i + motd-formatting: true + + # addresses/CIDRs the PROXY command can be used from + # this should be restricted to 127.0.0.1/8 and ::1/128 (unless you have a good reason) + # you should also add these addresses to the connection limits and throttling exemption lists + proxy-allowed-from: + # - localhost + # - "192.168.1.1" + # - "192.168.10.1/24" + + # controls the use of the WEBIRC command (by IRC<->web interfaces, bouncers and similar) + webirc: + # one webirc block -- should correspond to one set of gateways + - + # SHA-256 fingerprint of the TLS certificate the gateway must use to connect + # (comment this out to use passwords only) + fingerprint: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" + + # password the gateway uses to connect, made with oragono genpasswd + password: "$2a$04$sLEFDpIOyUp55e6gTMKbOeroT6tMXTjPFvA0eGvwvImVR9pkwv7ee" + + # addresses/CIDRs that can use this webirc command + # you should also add these addresses to the connection limits and throttling exemption lists + hosts: + # - localhost + # - "192.168.1.1" + # - "192.168.10.1/24" + + # allow use of the RESUME extension over plaintext connections: + # do not enable this unless the ircd is only accessible over internal networks + allow-plaintext-resume: false + + # maximum length of clients' sendQ in bytes + # this should be big enough to hold bursts of channel/direct messages + max-sendq: 96k + + # compatibility with legacy clients + compatibility: + # many clients require that the final parameter of certain messages be an + # RFC1459 trailing parameter, i.e., prefixed with :, whether or not this is + # actually required. this forces Oragono to send those parameters + # as trailings. this is recommended unless you're testing clients for conformance; + # defaults to true when unset for that reason. + force-trailing: true + + # some clients (ZNC 1.6.x and lower, Pidgin 2.12 and lower) do not + # respond correctly to SASL messages with the server name as a prefix: + # https://github.com/znc/znc/issues/1212 + # this works around that bug, allowing them to use SASL. + send-unprefixed-sasl: true + + # IP-based DoS protection + ip-limits: + # whether to limit the total number of concurrent connections per IP/CIDR + count: false + # maximum concurrent connections per IP/CIDR + max-concurrent-connections: 16 + + # whether to restrict the rate of new connections per IP/CIDR + throttle: false + # how long to keep track of connections for + window: 10m + # maximum number of new connections per IP/CIDR within the given duration + max-connections-per-window: 32 + # how long to ban offenders for. after banning them, the number of connections is + # reset, which lets you use /UNDLINE to unban people + throttle-ban-duration: 10m + + # how wide the CIDR should be for IPv4 (a /32 is a fully specified IPv4 address) + cidr-len-ipv4: 32 + # how wide the CIDR should be for IPv6 (a /64 is the typical prefix assigned + # by an ISP to an individual customer for their LAN) + cidr-len-ipv6: 64 + + # IPs/networks which are exempted from connection limits + exempted: + - "localhost" + # - "192.168.1.1" + # - "2001:0db8::/32" + + # custom connection limits for certain IPs/networks. note that CIDR + # widths defined here override the default CIDR width --- the limit + # will apply to the entire CIDR no matter how large or small it is + custom-limits: + # "8.8.0.0/16": + # max-concurrent-connections: 128 + # max-connections-per-window: 1024 + + # IP cloaking hides users' IP addresses from other users and from channel admins + # (but not from server admins), while still allowing channel admins to ban + # offending IP addresses or networks. In place of hostnames derived from reverse + # DNS, users see fake domain names like pwbs2ui4377257x8.oragono. These names are + # generated deterministically from the underlying IP address, but if the underlying + # IP is not already known, it is infeasible to recover it from the cloaked name. + ip-cloaking: + # whether to enable IP cloaking + enabled: false + + # fake TLD at the end of the hostname, e.g., pwbs2ui4377257x8.oragono + netname: "oragono" + + # secret key to prevent dictionary attacks against cloaked IPs + # any high-entropy secret is valid for this purpose: + # you MUST generate a new one for your installation. + # suggestion: use the output of `oragono mksecret` + # note that rotating this key will invalidate all existing ban masks. + secret: "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" + + # name of an environment variable to pull the secret from, for use with + # k8s secret distribution: + # secret-environment-variable: "ORAGONO_CLOAKING_SECRET" + + # the cloaked hostname is derived only from the CIDR (most significant bits + # of the IP address), up to a configurable number of bits. this is the + # granularity at which bans will take effect for IPv4. Note that changing + # this value will invalidate any stored bans. + cidr-len-ipv4: 32 + + # analogous granularity for IPv6 + cidr-len-ipv6: 64 + + # number of bits of hash output to include in the cloaked hostname. + # more bits means less likelihood of distinct IPs colliding, + # at the cost of a longer cloaked hostname. if this value is set to 0, + # all users will receive simply `netname` as their cloaked hostname. + num-bits: 64 + + # secure-nets identifies IPs and CIDRs which are secure at layer 3, + # for example, because they are on a trusted internal LAN or a VPN. + # plaintext connections from these IPs and CIDRs will be considered + # secure (clients will receive the +Z mode and be allowed to resume + # or reattach to secure connections). note that loopback IPs are always + # considered secure: + secure-nets: + # - "10.0.0.0/8" + + +# account options +accounts: + # is account authentication enabled, i.e., can users log into existing accounts? + authentication-enabled: true + + # account registration + registration: + # can users register new accounts for themselves? if this is false, operators with + # the `accreg` capability can still create accounts with `/NICKSERV SAREGISTER` + enabled: true + + # this is the bcrypt cost we'll use for account passwords + bcrypt-cost: 9 + + # length of time a user has to verify their account before it can be re-registered + verify-timeout: "32h" + + # callbacks to allow + enabled-callbacks: + - none # no verification needed, will instantly register successfully + + # example configuration for sending verification emails via a local mail relay + # callbacks: + # mailto: + # server: localhost + # port: 25 + # tls: + # enabled: false + # username: "" + # password: "" + # sender: "admin@my.network" + + # throttle account login attempts (to prevent either password guessing, or DoS + # attacks on the server aimed at forcing repeated expensive bcrypt computations) + login-throttling: + enabled: true + + # window + duration: 1m + + # number of attempts allowed within the window + max-attempts: 3 + + # some clients (notably Pidgin and Hexchat) offer only a single password field, + # which makes it impossible to specify a separate server password (for the PASS + # command) and SASL password. if this option is set to true, a client that + # successfully authenticates with SASL will not be required to send + # PASS as well, so it can be configured to authenticate with SASL only. + skip-server-password: false + + # require-sasl controls whether clients are required to have accounts + # (and sign into them using SASL) to connect to the server + require-sasl: + # if this is enabled, all clients must authenticate with SASL while connecting + enabled: false + + # IPs/CIDRs which are exempted from the account requirement + exempted: + - "localhost" + # - '10.10.0.0/16' + + # nick-reservation controls how, and whether, nicknames are linked to accounts + nick-reservation: + # is there any enforcement of reserved nicknames? + enabled: true + + # how many nicknames, in addition to the account name, can be reserved? + additional-nick-limit: 2 + + # method describes how nickname reservation is handled + # timeout: let the user change to the registered nickname, give them X seconds + # to login and then rename them if they haven't done so + # strict: don't let the user change to the registered nickname unless they're + # already logged-in using SASL or NickServ + # optional: no enforcement by default, but allow users to opt in to + # the enforcement level of their choice + # + # 'optional' matches the behavior of other NickServs, but 'strict' is + # preferable if all your users can enable SASL. + method: strict + + # allow users to set their own nickname enforcement status, e.g., + # to opt out of strict enforcement + allow-custom-enforcement: true + + # rename-timeout - this is how long users have 'til they're renamed + rename-timeout: 30s + + # rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31) + rename-prefix: Guest- + + # multiclient controls whether oragono allows multiple connections to + # attach to the same client/nickname identity; this is part of the + # functionality traditionally provided by a bouncer like ZNC + multiclient: + # when disabled, each connection must use a separate nickname (as is the + # typical behavior of IRC servers). when enabled, a new connection that + # has authenticated with SASL can associate itself with an existing + # client + enabled: false + + # if this is disabled, clients have to opt in to bouncer functionality + # using nickserv or the cap system. if it's enabled, they can opt out + # via nickserv + allowed-by-default: true + + # whether to allow clients that remain on the server even + # when they have no active connections. The possible values are: + # "disabled", "opt-in", "opt-out", or "mandatory". + always-on: "disabled" + + # vhosts controls the assignment of vhosts (strings displayed in place of the user's + # hostname/IP) by the HostServ service + vhosts: + # are vhosts enabled at all? + enabled: true + + # maximum length of a vhost + max-length: 64 + + # regexp for testing the validity of a vhost + # (make sure any changes you make here are RFC-compliant) + valid-regexp: '^[0-9A-Za-z.\-_/]+$' + + # options controlling users requesting vhosts: + user-requests: + # can users request vhosts at all? if this is false, operators with the + # 'vhosts' capability can still assign vhosts manually + enabled: false + + # if uncommented, all new vhost requests will be dumped into the given + # channel, so opers can review them as they are sent in. ensure that you + # have registered and restricted the channel appropriately before you + # uncomment this. + #channel: "#vhosts" + + # after a user's vhost has been approved or rejected, they need to wait + # this long (starting from the time of their original request) + # before they can request a new one. + cooldown: 168h + + # vhosts that users can take without approval, using `/HS TAKE` + offer-list: + #- "oragono.test" + + # support for deferring password checking to an external LDAP server + # you should probably ignore this section! consult the grafana docs for details: + # https://grafana.com/docs/grafana/latest/auth/ldap/ + # you will probably want to set require-sasl and disable accounts.registration.enabled + # ldap: + # enabled: true + # # should we automatically create users if their LDAP login succeeds? + # autocreate: true + # # example configuration that works with Forum Systems's testing server: + # # https://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/ + # host: "ldap.forumsys.com" + # port: 389 + # timeout: 30s + # # example "single-bind" configuration, where we bind directly to the user's entry: + # bind-dn: "uid=%s,dc=example,dc=com" + # # example "admin bind" configuration, where we bind to an initial admin user, + # # then search for the user's entry with a search filter: + # #search-base-dns: + # # - "dc=example,dc=com" + # #bind-dn: "cn=read-only-admin,dc=example,dc=com" + # #bind-password: "password" + # #search-filter: "(uid=%s)" + # # example of requiring that users be in a particular group + # # (note that this is an OR over the listed groups, not an AND): + # #require-groups: + # # - "ou=mathematicians,dc=example,dc=com" + # #group-search-filter-user-attribute: "dn" + # #group-search-filter: "(uniqueMember=%s)" + # #group-search-base-dns: + # # - "dc=example,dc=com" + # # example of group membership testing via user attributes, as in AD + # # or with OpenLDAP's "memberOf overlay" (overrides group-search-filter): + # attributes: + # member-of: "memberOf" + +# channel options +channels: + # modes that are set when new channels are created + # +n is no-external-messages and +t is op-only-topic + # see /QUOTE HELP cmodes for more channel modes + default-modes: +nt + + # how many channels can a client be in at once? + max-channels-per-client: 1000 + + # if this is true, new channels can only be created by operators with the + # `chanreg` operator capability + operator-only-creation: false + + # channel registration - requires an account + registration: + # can users register new channels? + enabled: true + + # how many channels can each account register? + max-channels-per-account: 15 + +# operator classes +oper-classes: + # local operator + "local-oper": + # title shown in WHOIS + title: Local Operator + + # capability names + capabilities: + - "oper:local_kill" + - "oper:local_ban" + - "oper:local_unban" + - "nofakelag" + + # network operator + "network-oper": + # title shown in WHOIS + title: Network Operator + + # oper class this extends from + extends: "local-oper" + + # capability names + capabilities: + - "oper:remote_kill" + - "oper:remote_ban" + - "oper:remote_unban" + + # server admin + "server-admin": + # title shown in WHOIS + title: Server Admin + + # oper class this extends from + extends: "local-oper" + + # capability names + capabilities: + - "oper:rehash" + - "oper:die" + - "accreg" + - "sajoin" + - "samode" + - "vhosts" + - "chanreg" + +# ircd operators +opers: + # operator named 'dan' + dan: + # which capabilities this oper has access to + class: "server-admin" + + # custom whois line + whois-line: is a cool dude + + # custom hostname + vhost: "n" + + # modes are the modes to auto-set upon opering-up + modes: +is acjknoqtuxv + + # operators can be authenticated either by password (with the /OPER command), + # or by certificate fingerprint, or both. if a password hash is set, then a + # password is required to oper up (e.g., /OPER dan mypassword). to generate + # the hash, use `oragono genpasswd`. + password: "$2a$04$LiytCxaY0lI.guDj2pBN4eLRD5cdM2OLDwqmGAgB6M2OPirbF5Jcu" + + # if a SHA-256 certificate fingerprint is configured here, then it will be + # required to /OPER. if you comment out the password hash above, then you can + # /OPER without a password. + #fingerprint: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" + # if 'auto' is set (and no password hash is set), operator permissions will be + # granted automatically as soon as you connect with the right fingerprint. + #auto: true + +# logging, takes inspiration from Insp +logging: + - + # how to log these messages + # + # file log to given target filename + # stdout log to stdout + # stderr log to stderr + # (you can specify multiple methods, e.g., to log to both stderr and a file) + method: stderr + + # filename to log to, if file method is selected + # filename: ircd.log + + # type(s) of logs to keep here. you can use - to exclude those types + # + # exclusions take precedent over inclusions, so if you exclude a type it will NEVER + # be logged, even if you explicitly include it + # + # useful types include: + # * everything (usually used with exclusing some types below) + # server server startup, rehash, and shutdown events + # accounts account registration and authentication + # channels channel creation and operations + # commands command calling and operations + # opers oper actions, authentication, etc + # services actions related to NickServ, ChanServ, etc. + # internal unexpected runtime behavior, including potential bugs + # userinput raw lines sent by users + # useroutput raw lines sent to users + type: "* -userinput -useroutput" + + # one of: debug info warn error + level: info + #- + # # example of a file log that avoids logging IP addresses + # method: file + # filename: ircd.log + # type: "* -userinput -useroutput -localconnect -localconnect-ip" + # level: debug + +# debug options +debug: + # when enabled, oragono will attempt to recover from certain kinds of + # client-triggered runtime errors that would normally crash the server. + # this makes the server more resilient to DoS, but could result in incorrect + # behavior. deployments that would prefer to "start from scratch", e.g., by + # letting the process crash and auto-restarting it with systemd, can set + # this to false. + recover-from-errors: true + + # optionally expose a pprof http endpoint: https://golang.org/pkg/net/http/pprof/ + # it is strongly recommended that you don't expose this on a public interface; + # if you need to access it remotely, you can use an SSH tunnel. + # set to `null`, "", leave blank, or omit to disable + # pprof-listener: "localhost:6060" + +# datastore configuration +datastore: + # path to the datastore + path: ircd.db + + # if the database schema requires an upgrade, `autoupgrade` will attempt to + # perform it automatically on startup. the database will be backed + # up, and if the upgrade fails, the original database will be restored. + autoupgrade: true + + # connection information for MySQL (currently only used for persistent history): + mysql: + enabled: false + host: "localhost" + # port is unnecessary for connections via unix domain socket: + #port: 3306 + user: "oragono" + password: "hunter2" + history-database: "oragono_history" + timeout: 3s + +# languages config +languages: + # whether to load languages + enabled: false + + # default language to use for new clients + # 'en' is the default English language in the code + default: en + + # which directory contains our language files + path: languages + +# limits - these need to be the same across the network +limits: + # nicklen is the max nick length allowed + nicklen: 32 + + # identlen is the max ident length allowed + identlen: 20 + + # channellen is the max channel length allowed + channellen: 64 + + # awaylen is the maximum length of an away message + awaylen: 500 + + # kicklen is the maximum length of a kick message + kicklen: 1000 + + # topiclen is the maximum length of a channel topic + topiclen: 1000 + + # maximum number of monitor entries a client can have + monitor-entries: 100 + + # whowas entries to store + whowas-entries: 100 + + # maximum length of channel lists (beI modes) + chan-list-modes: 1000 + + # maximum number of messages to accept during registration (prevents + # DoS / resource exhaustion attacks): + registration-messages: 1024 + + # message length limits for the new multiline cap + multiline: + max-bytes: 4096 # 0 means disabled + max-lines: 100 # 0 means no limit + +# fakelag: prevents clients from spamming commands too rapidly +fakelag: + # whether to enforce fakelag + enabled: false + + # time unit for counting command rates + window: 1s + + # clients can send this many commands without fakelag being imposed + burst-limit: 5 + + # once clients have exceeded their burst allowance, they can send only + # this many commands per `window`: + messages-per-window: 2 + + # client status resets to the default state if they go this long without + # sending any commands: + cooldown: 2s + +# message history tracking, for the RESUME extension and possibly other uses in future +history: + # should we store messages for later playback? + # by default, messages are stored in RAM only; they do not persist + # across server restarts. however, you should not enable this unless you understand + # how it interacts with the GDPR and/or any data privacy laws that apply + # in your country and the countries of your users. + enabled: false + + # how many channel-specific events (messages, joins, parts) should be tracked per channel? + channel-length: 1024 + + # how many direct messages and notices should be tracked per user? + client-length: 256 + + # how long should we try to preserve messages? + # if `autoresize-window` is 0, the in-memory message buffers are preallocated to + # their maximum length. if it is nonzero, the buffers are initially small and + # are dynamically expanded up to the maximum length. if the buffer is full + # and the oldest message is older than `autoresize-window`, then it will overwrite + # the oldest message rather than resize; otherwise, it will expand if possible. + autoresize-window: 1h + + # number of messages to automatically play back on channel join (0 to disable): + autoreplay-on-join: 0 + + # maximum number of CHATHISTORY messages that can be + # requested at once (0 disables support for CHATHISTORY) + chathistory-maxmessages: 100 + + # maximum number of messages that can be replayed at once during znc emulation + # (znc.in/playback, or automatic replay on initial reattach to a persistent client): + znc-maxmessages: 2048 + + # options to delete old messages, or prevent them from being retrieved + restrictions: + # if this is set, messages older than this cannot be retrieved by anyone + # (and will eventually be deleted from persistent storage, if that's enabled) + #expire-time: 1w + + # if this is set, logged-in users cannot retrieve messages older than their + # account registration date, and logged-out users cannot retrieve messages + # older than their sign-on time (modulo grace-period, see below): + enforce-registration-date: false + + # but if this is set, you can retrieve messages that are up to `grace-period` + # older than the above cutoff time. this is recommended to allow logged-out + # users to do session resumption / query history after disconnections. + grace-period: 1h + + # options to store history messages in a persistent database (currently only MySQL): + persistent: + enabled: false + + # store unregistered channel messages in the persistent database? + unregistered-channels: false + + # for a registered channel, the channel owner can potentially customize + # the history storage setting. as the server operator, your options are + # 'disabled' (no persistent storage, regardless of per-channel setting), + # 'opt-in', 'opt-out', and 'mandatory' (force persistent storage, ignoring + # per-channel setting): + registered-channels: "opt-out" + + # direct messages are only stored in the database for logged-in clients; + # you can control how they are stored here (same options as above). + # if you enable this, strict nickname reservation is strongly recommended + # as well. + direct-messages: "opt-out" diff --git a/tests/end_to_end/scenarios/__init__.py b/tests/end_to_end/scenarios/__init__.py new file mode 100644 index 0000000..1fef72e --- /dev/null +++ b/tests/end_to_end/scenarios/__init__.py @@ -0,0 +1,10 @@ +# Do "from scenarios import *" instead of repeating these imports everytime in every scenario + +from functions import expect_stanza, send_stanza, expect_unordered, save_value, extract_attribute, extract_text, sleep_for, save_current_timestamp_plus_delta +import datetime +import sequences +import scenarios.simple_channel_join +import scenarios.channel_join_with_two_users +import scenarios.simple_channel_join_fixed +import scenarios.channel_join_on_fixed_irc_server +import scenarios.multiple_channels_join diff --git a/tests/end_to_end/scenarios/basic_handshake_success.py b/tests/end_to_end/scenarios/basic_handshake_success.py new file mode 100644 index 0000000..9e1ffb3 --- /dev/null +++ b/tests/end_to_end/scenarios/basic_handshake_success.py @@ -0,0 +1,8 @@ +from scenarios import * + +# At the start of every scenario, we automatically insert a +# sequences.handshake() call. So, this scenario is just here to test that +# this basic thing works fine. + +scenario = ( +) diff --git a/tests/end_to_end/scenarios/basic_subscribe_unsubscribe.py b/tests/end_to_end/scenarios/basic_subscribe_unsubscribe.py new file mode 100644 index 0000000..6082fa6 --- /dev/null +++ b/tests/end_to_end/scenarios/basic_subscribe_unsubscribe.py @@ -0,0 +1,23 @@ +from scenarios import * + +scenario = ( + + + # Mutual subscription exchange + send_stanza("<presence from='{jid_one}' to='{biboumi_host}' type='subscribe' id='subid1' />"), + expect_stanza("/presence[@type='subscribed'][@id='subid1']"), + + # Get the current presence of the biboumi gateway + expect_stanza("/presence"), + + expect_stanza("/presence[@type='subscribe']"), + send_stanza("<presence from='{jid_one}' to='{biboumi_host}' type='subscribed' />"), + + # Unsubscribe + send_stanza("<presence from='{jid_one}' to='{biboumi_host}' type='unsubscribe' id='unsubid1' />"), + expect_stanza("/presence[@type='unavailable']"), + expect_stanza("/presence[@type='unsubscribed']"), + expect_stanza("/presence[@type='unsubscribe']"), + send_stanza("<presence from='{jid_one}' to='{biboumi_host}' type='unavailable' />"), + send_stanza("<presence from='{jid_one}' to='{biboumi_host}' type='unsubscribed' />"), +) diff --git a/tests/end_to_end/scenarios/channel_custom_topic.py b/tests/end_to_end/scenarios/channel_custom_topic.py new file mode 100644 index 0000000..3b3104e --- /dev/null +++ b/tests/end_to_end/scenarios/channel_custom_topic.py @@ -0,0 +1,30 @@ +from scenarios import * + +import scenarios.simple_channel_join + +scenario = ( + scenarios.simple_channel_join.scenario, + + # First user sets the topic + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><subject>TOPIC TEST</subject></message>"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[text()='TOPIC TEST']"), + + # Second user joins + send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), + expect_unordered( + [ + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']" + ], + [ + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']" + ], + [ + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ], + [ + "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/subject[text()='TOPIC TEST']" + ] + ) +) diff --git a/tests/end_to_end/scenarios/channel_force_join.py b/tests/end_to_end/scenarios/channel_force_join.py new file mode 100644 index 0000000..9a24c06 --- /dev/null +++ b/tests/end_to_end/scenarios/channel_force_join.py @@ -0,0 +1,25 @@ +from scenarios import * + +import scenarios.channel_join_with_two_users + +scenario = ( + scenarios.channel_join_with_two_users.scenario, + # Here we simulate a desynchronization of a client: The client thinks it’s + # disconnected from the room, but biboumi still thinks it’s in the room. The + # client thus sends a join presence, and biboumi should send everything + # (user list, history, etc) in response. + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}'><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_unordered( + [ + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost']" + ], + [ + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ], + [ + "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]" + ] + ), +) + diff --git a/tests/end_to_end/scenarios/channel_history.py b/tests/end_to_end/scenarios/channel_history.py new file mode 100644 index 0000000..0014d65 --- /dev/null +++ b/tests/end_to_end/scenarios/channel_history.py @@ -0,0 +1,18 @@ +from scenarios import * + +scenario = ( + scenarios.simple_channel_join.scenario, + + # Send one channel message + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']"), + + # Second user joins + send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}%{irc_server_one}/~{nick_one}@localhost'][@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='110']"), + # Receive the history message + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}']/body[text()='coucou']", + "/message/delay:delay[@from='#foo%{irc_server_one}']"), + expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), +) diff --git a/tests/end_to_end/scenarios/channel_history_on_fixed_server.py b/tests/end_to_end/scenarios/channel_history_on_fixed_server.py new file mode 100644 index 0000000..0e957e1 --- /dev/null +++ b/tests/end_to_end/scenarios/channel_history_on_fixed_server.py @@ -0,0 +1,20 @@ +from scenarios import * + +conf = 'fixed_server' + +scenario = ( + scenarios.channel_join_on_fixed_irc_server.scenario, + + # Send one channel message + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' type='groupchat'><body>coucou</body></message>"), + expect_stanza("/message[@from='#foo@{biboumi_host}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']"), + + # Second user joins + send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo@{biboumi_host}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}@{biboumi_host}/~{nick_one}@localhost'][@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='110']"), + # Receive the history message + expect_stanza("/message[@from='#foo@{biboumi_host}/{nick_one}']/body[text()='coucou']", + "/message/delay:delay[@from='#foo@{biboumi_host}']"), + expect_stanza("/message[@from='#foo@{biboumi_host}'][@type='groupchat']/subject[not(text())]"), +) diff --git a/tests/end_to_end/scenarios/channel_join_on_fixed_irc_server.py b/tests/end_to_end/scenarios/channel_join_on_fixed_irc_server.py new file mode 100644 index 0000000..7d675ac --- /dev/null +++ b/tests/end_to_end/scenarios/channel_join_on_fixed_irc_server.py @@ -0,0 +1,12 @@ +from scenarios import * + +conf = "fixed_server" + +scenario = ( + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True), + 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']"), + expect_stanza("/message[@from='#foo@{biboumi_host}'][@type='groupchat']/subject[not(text())]"), +) diff --git a/tests/end_to_end/scenarios/channel_join_with_different_nick.py b/tests/end_to_end/scenarios/channel_join_with_different_nick.py new file mode 100644 index 0000000..c499184 --- /dev/null +++ b/tests/end_to_end/scenarios/channel_join_with_different_nick.py @@ -0,0 +1,19 @@ +from scenarios import * + +from scenarios.simple_channel_join import expect_self_join_presence + +scenario = ( + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}'), + expect_self_join_presence(jid = '{jid_one}/{resource_one}', chan = "#foo", nick = "{nick_one}"), + + # The same resource joins a different channel with a different nick + send_stanza("<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + # We must receive a join presence in response, without any nick change (nick_two) must be ignored + expect_self_join_presence(jid = '{jid_one}/{resource_one}', chan = "#bar", nick = "{nick_one}"), + # An different resource joins the same channel, with a different nick + send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_two}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + # We must receive a join presence in response, without any nick change (nick_two) must be ignored + expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']"), + expect_stanza("/message/subject"), +) diff --git a/tests/end_to_end/scenarios/channel_join_with_password.py b/tests/end_to_end/scenarios/channel_join_with_password.py new file mode 100644 index 0000000..fdebcbe --- /dev/null +++ b/tests/end_to_end/scenarios/channel_join_with_password.py @@ -0,0 +1,35 @@ +from scenarios import * + +import scenarios.simple_channel_join + +scenario = ( + scenarios.simple_channel_join.scenario, + + # Set a password in the room, by using /mode +k + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>/mode +k SECRET</body></message>"), + 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 (error ensues) + send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}'><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), + expect_stanza("/message/body[text()='{irc_host_one}: #foo: Cannot join channel (+k)']"), + expect_stanza("/presence[@type='error'][@from='#foo%{irc_server_one}/{nick_two}']/error[@type='auth']/stanza:not-authorized"), + + # Second user joins, with the correct password (success) + send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}'> <x xmlns='http://jabber.org/protocol/muc'><password>SECRET</password></x></presence>"), + expect_unordered( + [ + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost']" + ], + [ + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']" + ], + [ + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ], + [ + "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]" + ] + ) +) diff --git a/tests/end_to_end/scenarios/channel_join_with_two_users.py b/tests/end_to_end/scenarios/channel_join_with_two_users.py new file mode 100644 index 0000000..3b2b102 --- /dev/null +++ b/tests/end_to_end/scenarios/channel_join_with_two_users.py @@ -0,0 +1,25 @@ +from scenarios import * + +scenario = ( + scenarios.simple_channel_join.scenario, + + # Second user joins + send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), + expect_unordered( + [ + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost']" + ], + [ + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']" + ], + [ + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ], + [ + "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]" + ] + ) +) + diff --git a/tests/end_to_end/scenarios/channel_list_escaping.py b/tests/end_to_end/scenarios/channel_list_escaping.py new file mode 100644 index 0000000..7229e97 --- /dev/null +++ b/tests/end_to_end/scenarios/channel_list_escaping.py @@ -0,0 +1,9 @@ +from scenarios import * + +scenario = ( + send_stanza("<presence from='{jid_one}/{resource_one}' to='#true\\2ffalse%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}'), + expect_stanza("/presence[@to='{jid_one}/{resource_one}'][@from='#true\\2ffalse%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='110']"), + expect_stanza("/message[@from='#true\\2ffalse%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), +) diff --git a/tests/end_to_end/scenarios/channel_list_with_rsm.py b/tests/end_to_end/scenarios/channel_list_with_rsm.py new file mode 100644 index 0000000..aaf589a --- /dev/null +++ b/tests/end_to_end/scenarios/channel_list_with_rsm.py @@ -0,0 +1,67 @@ +from scenarios import * + +scenario = ( + scenarios.simple_channel_join.scenario, + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence"), + expect_stanza("/message[@from='#bar%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#coucou%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence"), + expect_stanza("/message[@from='#coucou%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), + + # Ask for 0 item + 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 + expect_stanza("/iq[@type='result']/disco_items:query"), + + # 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 + 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>"), + expect_stanza("/iq[@type='result']/disco_items:query", + "count(/iq/disco_items:query/disco_items:item[@jid])=2", + "/iq/disco_items:query/rsm:set/rsm:first[@index='0']", + "/iq/disco_items:query/rsm:set/rsm:last", + "!/iq/disco_items:query/rsm:set/rsm:count"), + + # Ask for 12 (of 3) items. We get the whole list, and thus we have the count included. + 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>"), + expect_stanza("/iq[@type='result']/disco_items:query", + "count(/iq/disco_items:query/disco_items:item[@jid])=3", + "/iq/disco_items:query/rsm:set/rsm:first[@index='0']", + "/iq/disco_items:query/rsm:set/rsm:last", + "/iq/disco_items:query/rsm:set/rsm:count[text()='3']", + after = save_value("first", lambda stanza: extract_text("/iq/disco_items:query/rsm:set/rsm:first", stanza))), + + # 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. + 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>{first}</after><max>1</max></set></query></iq>"), + expect_stanza("/iq[@type='result']/disco_items:query", + "count(/iq/disco_items:query/disco_items:item)=1", + "/iq/disco_items:query/rsm:set/rsm:first[@index='1']", + "/iq/disco_items:query/rsm:set/rsm:last", + "/iq/disco_items:query/rsm:set/rsm:count[text()='3']", + after = save_value("second", lambda stanza: extract_text("/iq/disco_items:query/rsm:set/rsm:first", stanza))), + + # Ask for 1 item, AFTER the second item (so, + # the third). + 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>{second}</after><max>1</max></set></query></iq>"), + expect_stanza("/iq[@type='result']/disco_items:query", + "count(/iq/disco_items:query/disco_items:item)=1", + "/iq/disco_items:query/rsm:set/rsm:first[@index='2']", + "/iq/disco_items:query/rsm:set/rsm:last", + "/iq/disco_items:query/rsm:set/rsm:count[text()='3']", + after = save_value("third", lambda stanza: extract_text("/iq/disco_items:query/rsm:set/rsm:first", stanza))), + + # Ask for 1 item, AFTER the third item (so, + # the fourth). Since it doesn't exist, we get 0 item + 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>{third}</after><max>1</max></set></query></iq>"), + expect_stanza("/iq[@type='result']/disco_items:query", + "count(/iq/disco_items:query/disco_items:item)=0", + "/iq/disco_items:query/rsm:set/rsm:count[text()='3']"), +) diff --git a/tests/end_to_end/scenarios/channel_messages.py b/tests/end_to_end/scenarios/channel_messages.py new file mode 100644 index 0000000..09ac1ae --- /dev/null +++ b/tests/end_to_end/scenarios/channel_messages.py @@ -0,0 +1,69 @@ +from scenarios import * + +import scenarios.simple_channel_join + +scenario = ( + scenarios.simple_channel_join.scenario, + + # Second user joins + send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), + + # Our presence, sent to the other user, and ourself + expect_unordered( + ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']"], + ["/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']"], + [ + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ], + ["/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"], + ), + + # Send a channel message + send_stanza("<message id='first_id' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"), + # Receive the message, forwarded to the two users + expect_unordered( + [ + "/message[@id='first_id'][@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]" + ], + [ + "/message[@id][@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='coucou']", + "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]" + ] + ), + + # Send a private message, to a in-room JID + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' type='chat'><body>coucou in private</body></message>"), + # Message is received with a server-wide JID + expect_stanza("/message[@from='{lower_nick_one}%{irc_server_one}'][@to='{jid_two}/{resource_one}'][@type='chat']/body[text()='coucou in private']"), + # Respond to the message, to the server-wide JID + send_stanza("<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>yes</body></message>"), + # The response is received from the in-room JID + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='yes']", + "/message/muc_user:x"), + # Do the exact same thing, from a different chan, + # to check if the response comes from the right JID + send_stanza("<presence from='{jid_one}/{resource_one}' to='#dummy%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"), + expect_stanza("/message[@from='#dummy%{irc_server_one}'][@type='groupchat']/subject"), + # Send a private message, to a in-room JID + send_stanza("<message from='{jid_one}/{resource_one}' to='#dummy%{irc_server_one}/{nick_two}' type='chat'><body>re in private</body></message>"), + + # Message is received with a server-wide JID + expect_stanza("/message[@from='{lower_nick_one}%{irc_server_one}'][@to='{jid_two}/{resource_one}'][@type='chat']/body[text()='re in private']"), + + # Respond to the message, to the server-wide JID + send_stanza("<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>re</body></message>"), + # The response is received from the in-room JID + expect_stanza("/message[@from='#dummy%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='re']"), + + # Now we leave the room, to check if the subsequent private messages are still received properly + send_stanza("<presence from='{jid_one}/{resource_one}' to='#dummy%{irc_server_one}/{nick_one}' type='unavailable' />"), + expect_stanza("/presence[@type='unavailable']/muc_user:x/muc_user:status[@code='110']"), + + # The private messages from this nick should now come (again) from the server-wide JID + send_stanza("<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>hihihoho</body></message>"), + expect_stanza("/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}']"), +) diff --git a/tests/end_to_end/scenarios/client_error.py b/tests/end_to_end/scenarios/client_error.py new file mode 100644 index 0000000..8c6fd7e --- /dev/null +++ b/tests/end_to_end/scenarios/client_error.py @@ -0,0 +1,16 @@ +from scenarios import * + +scenario = ( + scenarios.simple_channel_join.scenario, + # Second resource, same channel + send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='110']"), + expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"), + + # Now the first resource has an error + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%%{irc_server_one}/{nick_one}' type='error'><error type='cancel'><recipient-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error></message>"), + # Receive a leave only to the leaving resource + expect_stanza("/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}'][@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.']"), +) diff --git a/tests/end_to_end/scenarios/complete_channel_list_with_pages_of_3.py b/tests/end_to_end/scenarios/complete_channel_list_with_pages_of_3.py new file mode 100644 index 0000000..4c7e795 --- /dev/null +++ b/tests/end_to_end/scenarios/complete_channel_list_with_pages_of_3.py @@ -0,0 +1,93 @@ +from scenarios import * + +scenario = ( + send_stanza("<presence from='{jid_one}/{resource_one}' to='#aaa%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}'), + expect_stanza("/presence"), + expect_stanza("/message"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#bbb%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence"), + expect_stanza("/message"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#ccc%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence"), + expect_stanza("/message"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#ddd%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence"), + expect_stanza("/message"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#eee%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence"), + expect_stanza("/message"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#fff%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence"), + expect_stanza("/message"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#ggg%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence"), + expect_stanza("/message"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#hhh%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence"), + expect_stanza("/message"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#iii%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence"), + expect_stanza("/message"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#jjj%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence"), + expect_stanza("/message"), + + 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>"), + expect_stanza("/iq[@type='result']/disco_items:query", + "count(/iq/disco_items:query/disco_items:item[@jid])=3", + "/iq/disco_items:query/rsm:set/rsm:first[@index='0']", + "/iq/disco_items:query/rsm:set/rsm:last", + after = save_value("last", lambda stanza: extract_text("/iq/disco_items:query/rsm:set/rsm:last", stanza))), + + 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>{last}</after><max>3</max></set></query></iq>"), + expect_stanza("/iq[@type='result']/disco_items:query", + "count(/iq/disco_items:query/disco_items:item[@jid])=3", + "/iq/disco_items:query/rsm:set/rsm:first[@index='3']", + "/iq/disco_items:query/rsm:set/rsm:last", + after = save_value("last", lambda stanza: extract_text("/iq/disco_items:query/rsm:set/rsm:last", stanza))), + + 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>{last}</after><max>3</max></set></query></iq>"), + expect_stanza("/iq[@type='result']/disco_items:query", + "count(/iq/disco_items:query/disco_items:item[@jid])=3", + "/iq/disco_items:query/rsm:set/rsm:first[@index='6']", + "/iq/disco_items:query/rsm:set/rsm:last", + after = save_value("last", lambda stanza: extract_text("/iq/disco_items:query/rsm:set/rsm:last", stanza))), + + 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>{last}</after><max>3</max></set></query></iq>"), + expect_stanza("/iq[@type='result']/disco_items:query", + "count(/iq/disco_items:query/disco_items:item[@jid])=1", + "/iq/disco_items:query/rsm:set/rsm:first[@index='9']", + "/iq/disco_items:query/rsm:set/rsm:last", + "/iq/disco_items:query/rsm:set/rsm:count[text()='10']"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#aaa%{irc_server_one}/{nick_one}' type='unavailable' />"), + send_stanza("<presence from='{jid_one}/{resource_one}' to='#bbb%{irc_server_one}/{nick_one}' type='unavailable' />"), + send_stanza("<presence from='{jid_one}/{resource_one}' to='#ccc%{irc_server_one}/{nick_one}' type='unavailable' />"), + send_stanza("<presence from='{jid_one}/{resource_one}' to='#ddd%{irc_server_one}/{nick_one}' type='unavailable' />"), + send_stanza("<presence from='{jid_one}/{resource_one}' to='#eee%{irc_server_one}/{nick_one}' type='unavailable' />"), + send_stanza("<presence from='{jid_one}/{resource_one}' to='#fff%{irc_server_one}/{nick_one}' type='unavailable' />"), + send_stanza("<presence from='{jid_one}/{resource_one}' to='#ggg%{irc_server_one}/{nick_one}' type='unavailable' />"), + send_stanza("<presence from='{jid_one}/{resource_one}' to='#hhh%{irc_server_one}/{nick_one}' type='unavailable' />"), + send_stanza("<presence from='{jid_one}/{resource_one}' to='#iii%{irc_server_one}/{nick_one}' type='unavailable' />"), + send_stanza("<presence from='{jid_one}/{resource_one}' to='#jjj%{irc_server_one}/{nick_one}' type='unavailable' />"), + expect_stanza("/presence[@type='unavailable']"), + expect_stanza("/presence[@type='unavailable']"), + expect_stanza("/presence[@type='unavailable']"), + expect_stanza("/presence[@type='unavailable']"), + expect_stanza("/presence[@type='unavailable']"), + expect_stanza("/presence[@type='unavailable']"), + expect_stanza("/presence[@type='unavailable']"), + expect_stanza("/presence[@type='unavailable']"), + expect_stanza("/presence[@type='unavailable']"), + expect_stanza("/presence[@type='unavailable']"), +) diff --git a/tests/end_to_end/scenarios/configure_bad_value.py b/tests/end_to_end/scenarios/configure_bad_value.py new file mode 100644 index 0000000..4d2575c --- /dev/null +++ b/tests/end_to_end/scenarios/configure_bad_value.py @@ -0,0 +1,21 @@ +from scenarios import * + +scenario = ( + # Configure the throttle option with an incorrect value + 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>"), + expect_stanza("/iq[@type='result']", + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))), + 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='throttle_limit'><value>bleh</value></field>" + "<field var='max_history_length'><value>bleh</value></field>" + "</x></command></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']"), + + # These options should have their default value + 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' action='execute' /></iq>"), + expect_stanza("/iq[@type='result']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='throttle_limit']/dataform:value[text()='10']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='max_history_length']/dataform:value[text()='20']"), +) diff --git a/tests/end_to_end/scenarios/default_channel_list_limit.py b/tests/end_to_end/scenarios/default_channel_list_limit.py new file mode 100644 index 0000000..6858ea1 --- /dev/null +++ b/tests/end_to_end/scenarios/default_channel_list_limit.py @@ -0,0 +1,44 @@ +from scenarios import * + +def incr_counter(): + counter = -1 + def f(stanza): + nonlocal counter + counter += 1 + return counter + return f + +counter = incr_counter() + +scenario = ( + # Disable the throttling, otherwise it’s way too long + 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>"), + expect_stanza("/iq[@type='result']", + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))), + 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'><value>6667</value></field>" + "<field var='tls_ports'><value>6697</value><value>6670</value></field>" + "<field var='throttle_limit'><value>9999</value></field>" + "</x></command></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']", + after = save_value("counter", counter)), + + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection(), + + scenarios.simple_channel_join.expect_self_join_presence(jid = '{jid_one}/{resource_one}', chan = "#foo", nick = "{nick_one}"), + + + ( + send_stanza("<presence from='{jid_one}/{resource_one}' to='#{counter}%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence", + after = save_value("counter", counter)), + expect_stanza("/message"), + ) * 110, + + 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>"), + expect_stanza("count(/iq/disco_items:query/disco_items:item[@jid])=100") +) diff --git a/tests/end_to_end/scenarios/default_mam_limit.py b/tests/end_to_end/scenarios/default_mam_limit.py new file mode 100644 index 0000000..0f402f8 --- /dev/null +++ b/tests/end_to_end/scenarios/default_mam_limit.py @@ -0,0 +1,104 @@ +from scenarios import * + +scenario = ( + # Disable the throttling, otherwise it’s way too long + 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>"), + expect_stanza("/iq[@type='result']", + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))), + 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'><value>6667</value></field>" + "<field var='tls_ports'><value>6697</value><value>6670</value></field>" + "<field var='throttle_limit'><value>9999</value></field>" + "</x></command></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}'), + 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']"), + expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]", + after = save_value("counter", lambda x: 0)), + ( + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>{counter}</body></message>"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='{counter}']", + after = save_value("counter", lambda stanza: str(1 + int(extract_text("/message/body", stanza))))), + ) * 150, + + # Retrieve the archive, without any restriction + 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>"), + 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()='0']"), + # followed by 98 more messages + ( + 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 "99" + 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()='99']", + after = save_value("last_uuid", extract_attribute("/message/mam:result", "id"))), + + # And it should not be marked as complete + expect_stanza("/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", + "/iq/mam:fin/rsm:set/rsm:last[text()='{last_uuid}']", + "!/iq//mam:fin[@complete='true']", + "/iq//mam:fin"), + + # Retrieve the next page, using the “after” thingy + send_stanza("<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id2'><query xmlns='urn:xmpp:mam:2' queryid='qid2' ><set xmlns='http://jabber.org/protocol/rsm'><after>{last_uuid}</after></set></query></iq>"), + + expect_stanza("/message/mam:result[@queryid='qid2']/forward:forwarded/delay:delay", + "/message/mam:result[@queryid='qid2']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='100']"), + ( + expect_stanza("/message/mam:result[@queryid='qid2']/forward:forwarded/delay:delay", + "/message/mam:result[@queryid='qid2']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body"), + ) * 48, + expect_stanza("/message/mam:result[@queryid='qid2']/forward:forwarded/delay:delay", + "/message/mam:result[@queryid='qid2']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='149']", + after = save_value("last_uuid", extract_attribute("/message/mam:result", "id"))), + expect_stanza("/iq[@type='result'][@id='id2'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", + "/iq/mam:fin/rsm:set/rsm:last[text()='{last_uuid}']", + "/iq//mam:fin[@complete='true']", + "/iq//mam:fin"), + + # Send a request with a non-existing ID set as the “after” value. + send_stanza("<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id3'><query xmlns='urn:xmpp:mam:2' queryid='qid3' ><set xmlns='http://jabber.org/protocol/rsm'><after>DUMMY_ID</after></set></query></iq>"), + expect_stanza("/iq[@id='id3'][@type='error']/error[@type='cancel']/stanza:item-not-found"), + + # Request the last page just BEFORE the last message in the archive + send_stanza("<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id3'><query xmlns='urn:xmpp:mam:2' queryid='qid3' ><set xmlns='http://jabber.org/protocol/rsm'><before></before></set></query></iq>"), + + expect_stanza("/message/mam:result[@queryid='qid3']/forward:forwarded/delay:delay", + "/message/mam:result[@queryid='qid3']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='50']"), + ( + expect_stanza("/message/mam:result[@queryid='qid3']/forward:forwarded/delay:delay", + "/message/mam:result[@queryid='qid3']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body"), + ) * 98, + expect_stanza("/message/mam:result[@queryid='qid3']/forward:forwarded/delay:delay", + "/message/mam:result[@queryid='qid3']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='149']", + after = save_value("last_uuid", extract_attribute("/message/mam:result", "id"))), + expect_stanza("/iq[@type='result'][@id='id3'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", + "/iq/mam:fin/rsm:set/rsm:last[text()='{last_uuid}']", + "!/iq//mam:fin[@complete='true']", + "/iq//mam:fin"), + + # Do the same thing, but with a limit value. + 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'><before>{last_uuid}</before><max>2</max></set></query></iq>"), + 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()='147']"), + 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()='148']", + after = save_value("last_uuid", extract_attribute("/message/mam:result", "id"))), + expect_stanza("/iq[@type='result'][@id='id4'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", + "/iq/mam:fin/rsm:set/rsm:last[text()='{last_uuid}']", + "!/iq/mam:fin[@complete='true']"), + + # Test if everything is fine even with weird max value: 0 + send_stanza("<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id5'><query xmlns='urn:xmpp:mam:2' queryid='qid5' ><set xmlns='http://jabber.org/protocol/rsm'><before></before><max>0</max></set></query></iq>"), + + expect_stanza("/iq[@type='result'][@id='id5'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", + "!/iq/mam:fin[@complete='true']"), +) diff --git a/tests/end_to_end/scenarios/encoded_channel_join.py b/tests/end_to_end/scenarios/encoded_channel_join.py new file mode 100644 index 0000000..71fa09f --- /dev/null +++ b/tests/end_to_end/scenarios/encoded_channel_join.py @@ -0,0 +1,9 @@ +from scenarios import * + +scenario = ( + send_stanza("<presence from='{jid_one}/{resource_one}' to='#biboumi\\40louiz.org\\3a80%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}'), + expect_stanza("/presence[@to='{jid_one}/{resource_one}'][@from='#biboumi\\40louiz.org\\3a80%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='110']"), + expect_stanza("/message[@from='#biboumi\\40louiz.org\\3a80%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), +) diff --git a/tests/end_to_end/scenarios/execute_admin_disconnect_from_server_adhoc_command.py b/tests/end_to_end/scenarios/execute_admin_disconnect_from_server_adhoc_command.py new file mode 100644 index 0000000..7f3c04c --- /dev/null +++ b/tests/end_to_end/scenarios/execute_admin_disconnect_from_server_adhoc_command.py @@ -0,0 +1,68 @@ +from scenarios import * + +from scenarios.simple_channel_join import expect_self_join_presence + +scenario = ( + # Admin connects to first server + send_stanza("<presence from='{jid_admin}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_admin}/{resource_one}'), + expect_self_join_presence(jid = '{jid_admin}/{resource_one}', chan = "#bar", nick = "{nick_one}"), + + # Non-Admin connects to first server + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}'), + expect_self_join_presence(jid = '{jid_one}/{resource_one}', chan = "#foo", nick = "{nick_two}"), + + # Non-admin connects to second server + send_stanza("<presence from='{jid_one}/{resource_two}' to='#bon%{irc_server_two}/{nick_three}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("{irc_host_two}", '{jid_one}/{resource_two}'), + expect_self_join_presence(jid = '{jid_one}/{resource_two}', chan = "#bon", nick = "{nick_three}", irc_server = "{irc_server_two}"), + + # Execute as admin + 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>"), + 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 = save_value("sessionid", extract_attribute("/iq/commands:command[@node='disconnect-from-irc-server']", "sessionid")) + ), + 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>"), + 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:complete", + after = save_value("sessionid", extract_attribute("/iq/commands:command[@node='disconnect-from-irc-server']", "sessionid")) + ), + # Command is successfull + send_stanza("<iq type='set' id='command3' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' sessionid='{sessionid}' action='complete'><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>"), + # User is being disconnected + expect_unordered( + [ + "/presence[@type='unavailable'][@to='{jid_one}/{resource_two}'][@from='#bon%{irc_server_two}/{nick_three}']", + "/presence/status[text()='Disconnected by e2e']" + ], + [ + "/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.']", + ]), + + # Execute as non-admin (this skips the first step) + send_stanza("<iq type='set' id='command4' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' action='execute' /></iq>"), + 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:complete", + after = save_value("sessionid", extract_attribute("/iq/commands:command[@node='disconnect-from-irc-server']", "sessionid")) + ), + send_stanza("<iq type='set' id='command5' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-from-irc-server' sessionid='{sessionid}' action='complete'><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>"), + expect_unordered( + [ + "/presence[@type='unavailable'][@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']", + "/presence/status[text()='Disconnected by e2e']" + ], + [ + "/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.']", + ]), +) diff --git a/tests/end_to_end/scenarios/execute_disconnect_user_adhoc_command.py b/tests/end_to_end/scenarios/execute_disconnect_user_adhoc_command.py new file mode 100644 index 0000000..6ce5231 --- /dev/null +++ b/tests/end_to_end/scenarios/execute_disconnect_user_adhoc_command.py @@ -0,0 +1,19 @@ +from scenarios import * + +from scenarios.simple_channel_join import expect_self_join_presence + +scenario = ( + send_stanza("<presence from='{jid_admin}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_admin}/{resource_one}'), + expect_self_join_presence(jid = '{jid_admin}/{resource_one}', chan = "#foo", nick = "{nick_one}"), + + send_stanza("<iq type='set' id='command1' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-user' action='execute' /></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='disconnect-user'][@sessionid][@status='executing']", + "/iq/commands:command/commands:actions/commands:complete", + after = save_value("sessionid", extract_attribute("/iq/commands:command[@node='disconnect-user']", "sessionid")) + ), + send_stanza("<iq type='set' id='command2' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-user' sessionid='{sessionid}' action='complete'><x xmlns='jabber:x:data' type='submit'><field var='jids'><value>{jid_admin}</value></field><field var='quit-message'><value>Disconnected by e2e</value></field></x></command></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='disconnect-user'][@status='completed']/commands:note[@type='info'][text()='1 user has been disconnected.']"), + # Note, charybdis ignores our QUIT message, so we can't test it + expect_stanza("/presence[@type='unavailable'][@to='{jid_admin}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']"), +) diff --git a/tests/end_to_end/scenarios/execute_forbidden_adhoc_command.py b/tests/end_to_end/scenarios/execute_forbidden_adhoc_command.py new file mode 100644 index 0000000..10c98ab --- /dev/null +++ b/tests/end_to_end/scenarios/execute_forbidden_adhoc_command.py @@ -0,0 +1,7 @@ +from scenarios import * + +scenario = ( + send_stanza("<iq type='set' id='command1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='disconnect-user' action='execute' /></iq>"), + expect_stanza("/iq[@type='error'][@id='command1']/commands:command[@node='disconnect-user']", + "/iq/commands:command/commands:error[@type='cancel']/stanza:forbidden"), +) diff --git a/tests/end_to_end/scenarios/execute_hello_adhoc_command.py b/tests/end_to_end/scenarios/execute_hello_adhoc_command.py new file mode 100644 index 0000000..916d95a --- /dev/null +++ b/tests/end_to_end/scenarios/execute_hello_adhoc_command.py @@ -0,0 +1,14 @@ +from scenarios import * + +scenario = ( + 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>"), + expect_stanza("/iq[@type='result']/commands:command[@node='hello'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure your name.']", + "/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Please provide your name.']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single']/dataform:required", + "/iq/commands:command/commands:actions/commands:complete", + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='hello']", "sessionid")) + ), + 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='complete'><x xmlns='jabber:x:data' type='submit'><field var='name'><value>COUCOU</value></field></x></command></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='hello'][@status='completed']/commands:note[@type='info'][text()='Hello COUCOU!']") +) diff --git a/tests/end_to_end/scenarios/execute_incomplete_hello_adhoc_command.py b/tests/end_to_end/scenarios/execute_incomplete_hello_adhoc_command.py new file mode 100644 index 0000000..83b2a55 --- /dev/null +++ b/tests/end_to_end/scenarios/execute_incomplete_hello_adhoc_command.py @@ -0,0 +1,11 @@ +from scenarios import * + +scenario = ( + 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>"), + expect_stanza("/iq[@type='result']/commands:command[@node='hello'][@sessionid][@status='executing']", + "/iq/commands:command/commands:actions/commands:complete", + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='hello']", "sessionid")) + ), + 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='complete'><x xmlns='jabber:x:data' type='submit'></x></command></iq>"), + expect_stanza("/iq[@type='error']") +) diff --git a/tests/end_to_end/scenarios/execute_ping_adhoc_command.py b/tests/end_to_end/scenarios/execute_ping_adhoc_command.py new file mode 100644 index 0000000..bcdefe1 --- /dev/null +++ b/tests/end_to_end/scenarios/execute_ping_adhoc_command.py @@ -0,0 +1,6 @@ +from scenarios import * + +scenario = ( + 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>"), + expect_stanza("/iq[@type='result']/commands:command[@node='ping'][@status='completed']/commands:note[@type='info'][text()='Pong']") +) diff --git a/tests/end_to_end/scenarios/execute_reload_adhoc_command.py b/tests/end_to_end/scenarios/execute_reload_adhoc_command.py new file mode 100644 index 0000000..5c4e1f7 --- /dev/null +++ b/tests/end_to_end/scenarios/execute_reload_adhoc_command.py @@ -0,0 +1,6 @@ +from scenarios import * + +scenario = ( + 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>"), + expect_stanza("/iq[@type='result']/commands:command[@node='reload'][@status='completed']/commands:note[@type='info'][text()='Configuration reloaded.']"), +) diff --git a/tests/end_to_end/scenarios/fixed_irc_server_subscription.py b/tests/end_to_end/scenarios/fixed_irc_server_subscription.py new file mode 100644 index 0000000..f255b19 --- /dev/null +++ b/tests/end_to_end/scenarios/fixed_irc_server_subscription.py @@ -0,0 +1,8 @@ +from scenarios import * + +conf = 'fixed_server' + +scenario = ( + send_stanza("<presence type='subscribe' from='{jid_one}/{resource_one}' to='{biboumi_host}' id='sub1' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence[@to='{jid_one}'][@from='{biboumi_host}'][@type='subscribed']") +) diff --git a/tests/end_to_end/scenarios/fixed_muc_disco_info.py b/tests/end_to_end/scenarios/fixed_muc_disco_info.py new file mode 100644 index 0000000..6cabb49 --- /dev/null +++ b/tests/end_to_end/scenarios/fixed_muc_disco_info.py @@ -0,0 +1,14 @@ +from scenarios import * + +conf = 'fixed_server' + +scenario = ( + 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>"), + 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='#foo on {irc_host_one}']", + "/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']"), +) diff --git a/tests/end_to_end/scenarios/get_irc_connection_info.py b/tests/end_to_end/scenarios/get_irc_connection_info.py new file mode 100644 index 0000000..7695aa1 --- /dev/null +++ b/tests/end_to_end/scenarios/get_irc_connection_info.py @@ -0,0 +1,13 @@ +from scenarios import * + +scenario = ( + 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>"), + expect_stanza("/iq/commands:command/commands:note[text()='You are not connected to the IRC server irc.localhost']"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}'), + simple_channel_join.expect_self_join_presence(), + + send_stanza("<iq type='set' id='command2' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='get-irc-connection-info' action='execute' /></iq>"), + expect_stanza(r"/iq/commands:command/commands:note[re:test(text(), 'Connected to IRC server irc.localhost on port 6667 since \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \(\d+ seconds ago\)\.\n#foo from 1 resource: {resource_one}.*')]"), +) diff --git a/tests/end_to_end/scenarios/get_irc_connection_info_fixed.py b/tests/end_to_end/scenarios/get_irc_connection_info_fixed.py new file mode 100644 index 0000000..922ea6a --- /dev/null +++ b/tests/end_to_end/scenarios/get_irc_connection_info_fixed.py @@ -0,0 +1,16 @@ +from scenarios import * + +conf = 'fixed_server' + +scenario = ( + 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>"), + expect_stanza("/iq/commands:command/commands:note[text()='You are not connected to the IRC server irc.localhost']"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True), + expect_stanza("/presence"), + expect_stanza("/message"), + + send_stanza("<iq type='set' id='command2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='get-irc-connection-info' action='execute' /></iq>"), + expect_stanza(r"/iq/commands:command/commands:note[re:test(text(), 'Connected to IRC server irc.localhost on port 6667 since \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \(\d+ seconds ago\)\.\n#foo from 1 resource: {resource_one}.*')]"), +) diff --git a/tests/end_to_end/scenarios/global_configure.py b/tests/end_to_end/scenarios/global_configure.py new file mode 100644 index 0000000..d7771c4 --- /dev/null +++ b/tests/end_to_end/scenarios/global_configure.py @@ -0,0 +1,27 @@ +from scenarios import * + +scenario = ( + send_stanza("<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure some global default settings.']", + "/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()='20']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='record_history']/dataform:value[text()='true']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='persistent']/dataform:value[text()='false']", + "/iq/commands:command/commands:actions/commands:complete", + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))), + send_stanza("<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='complete'><x xmlns='jabber:x:data' type='submit'><field var='record_history'><value>0</value></field><field var='max_history_length'><value>42</value></field></x></command></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"), + + send_stanza("<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure some global default settings.']", + "/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:complete", + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))), + send_stanza("<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' action='cancel' node='configure' sessionid='{sessionid}' /></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"), +) diff --git a/tests/end_to_end/scenarios/global_configure_fixed.py b/tests/end_to_end/scenarios/global_configure_fixed.py new file mode 100644 index 0000000..8df70ad --- /dev/null +++ b/tests/end_to_end/scenarios/global_configure_fixed.py @@ -0,0 +1,32 @@ +from scenarios import * + +conf = 'fixed_server' + +scenario = ( + send_stanza("<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='global-configure' action='execute' /></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='global-configure'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure some global default settings.']", + "/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()='20']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='record_history']/dataform:value[text()='true']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='persistent']/dataform:value[text()='false']", + "/iq/commands:command/commands:actions/commands:complete", + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='global-configure']", "sessionid"))), + send_stanza("<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='global-configure' sessionid='{sessionid}' action='complete'><x xmlns='jabber:x:data' type='submit'><field var='record_history'><value>0</value></field><field var='max_history_length'><value>42</value></field></x></command></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='global-configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"), + + send_stanza("<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='global-configure' action='execute' /></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='global-configure'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure some global default settings.']", + "/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:complete", + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='global-configure']", "sessionid"))), + send_stanza("<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' action='cancel' node='global-configure' sessionid='{sessionid}' /></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='global-configure'][@status='canceled']"), + + send_stanza("<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='server-configure' action='execute' /></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='server-configure'][@sessionid][@status='executing']"), +) diff --git a/tests/end_to_end/scenarios/global_configure_persistent_by_default.py b/tests/end_to_end/scenarios/global_configure_persistent_by_default.py new file mode 100644 index 0000000..db47e88 --- /dev/null +++ b/tests/end_to_end/scenarios/global_configure_persistent_by_default.py @@ -0,0 +1,15 @@ +from scenarios import * + +conf='persistent_by_default' + +scenario = ( + send_stanza("<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure some global default settings.']", + "/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()='20']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='record_history']/dataform:value[text()='true']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='persistent']/dataform:value[text()='true']", + "/iq/commands:command/commands:actions/commands:complete", + ), +) diff --git a/tests/end_to_end/scenarios/invite_other.py b/tests/end_to_end/scenarios/invite_other.py new file mode 100644 index 0000000..0e40dcb --- /dev/null +++ b/tests/end_to_end/scenarios/invite_other.py @@ -0,0 +1,19 @@ +from scenarios import * + +scenario = ( + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}'), + expect_stanza("/presence"), + expect_stanza("/message"), + + send_stanza("<presence from='{jid_two}/{resource_two}' to='#bar%{irc_server_one}/{nick_two}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_two}/{resource_two}'), + expect_stanza("/presence"), + expect_stanza("/message"), + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><x xmlns='http://jabber.org/protocol/muc#user'><invite to='{nick_two}'/></x></message>"), + expect_stanza("/message/body[text()='{nick_two} has been invited to #foo']"), + expect_stanza("/message[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='#foo%{irc_server_one}/{nick_one}']"), + + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><x xmlns='http://jabber.org/protocol/muc#user'><invite to='bertrand@example.com'/></x></message>"), + expect_stanza("/message[@to='bertrand@example.com'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='{jid_one}/{resource_one}']"), +) diff --git a/tests/end_to_end/scenarios/irc_channel_configure.py b/tests/end_to_end/scenarios/irc_channel_configure.py new file mode 100644 index 0000000..dcc78db --- /dev/null +++ b/tests/end_to_end/scenarios/irc_channel_configure.py @@ -0,0 +1,35 @@ +from scenarios import * + +scenario = ( + 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'><dummy/></command></iq>"), + 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']", + "!/iq/commands:command/commands:dummy", + + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid")) + ), + 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='complete'>" + "<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>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"), + + 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>"), + 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:complete", + + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid")) + ), + 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>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"), +) diff --git a/tests/end_to_end/scenarios/irc_channel_configure_fixed.py b/tests/end_to_end/scenarios/irc_channel_configure_fixed.py new file mode 100644 index 0000000..4f18c83 --- /dev/null +++ b/tests/end_to_end/scenarios/irc_channel_configure_fixed.py @@ -0,0 +1,33 @@ +from scenarios import * + +conf = 'fixed_server' + +scenario = ( + 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>"), + 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 = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid")) + ), + 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='complete'>" + "<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>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"), + + 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>"), + 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:complete", + + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid")) + ), + 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>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"), +) diff --git a/tests/end_to_end/scenarios/irc_channel_configure_xep0045.py b/tests/end_to_end/scenarios/irc_channel_configure_xep0045.py new file mode 100644 index 0000000..c19990d --- /dev/null +++ b/tests/end_to_end/scenarios/irc_channel_configure_xep0045.py @@ -0,0 +1,20 @@ +from scenarios import * + +scenario = ( + 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>"), + 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']", + + ), + 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>"), + expect_stanza("/iq[@type='result']"), + 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>"), + expect_stanza("/iq[@type='result']"), +) diff --git a/tests/end_to_end/scenarios/irc_server_configure.py b/tests/end_to_end/scenarios/irc_server_configure.py new file mode 100644 index 0000000..1470e6e --- /dev/null +++ b/tests/end_to_end/scenarios/irc_server_configure.py @@ -0,0 +1,105 @@ +from scenarios import * + +scenario = ( + 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>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure the IRC server irc.localhost']", + "/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Edit the form, to configure the settings of the IRC server irc.localhost']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='ports']/dataform:value[text()='6667']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='tls_ports']/dataform:value[text()='6670']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='tls_ports']/dataform:value[text()='6697']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='verify_cert']/dataform:value[text()='true']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='fingerprint']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='throttle_limit']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-private'][@var='pass']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='max_history_length']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='nick']", + "/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']", + "/iq/commands:command/commands:actions/commands:complete", + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid")) + ), + 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='complete'>" + "<x xmlns='jabber:x:data' type='submit'>" + "<field var='ports' />" + "<field var='tls_ports'><value>6697</value><value>6698</value></field>" + "<field var='verify_cert'><value>1</value></field>" + "<field var='fingerprint'><value>12:12:12</value></field>" + "<field var='pass'><value>coucou</value></field>" + "<field var='after_connect_commands'><value>first command</value><value>second command</value></field>" + "<field var='nick'><value>my_nickname</value></field>" + "<field var='username'><value>username</value></field>" + "<field var='throttle_limit'><value>42</value></field>" + "<field var='max_history_length'><value>69</value></field>" + "<field var='realname'><value>realname</value></field>" + "<field var='encoding_out'><value>UTF-8</value></field>" + "<field var='encoding_in'><value>latin-1</value></field>" + "</x></command></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"), + + send_stanza("<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure the IRC server irc.localhost']", + "/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Edit the form, to configure the settings of the IRC server irc.localhost']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='tls_ports']/dataform:value[text()='6697']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='tls_ports']/dataform:value[text()='6698']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='verify_cert']/dataform:value[text()='true']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='fingerprint']/dataform:value[text()='12:12:12']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-private'][@var='pass']/dataform:value[text()='coucou']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='nick']/dataform:value[text()='my_nickname']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']/dataform:value[text()='first command']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']/dataform:value[text()='second command']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='username']/dataform:value[text()='username']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='realname']/dataform:value[text()='realname']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='throttle_limit']/dataform:value[text()='42']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='max_history_length']/dataform:value[text()='69']", + "/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:complete", + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid")) + ), + 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>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"), + + # Same thing, but try to empty some values + 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>"), + expect_stanza("/iq[@type='result']", + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid")) + ), + 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='complete'>" + "<x xmlns='jabber:x:data' type='submit'>" + "<field var='pass'><value></value></field>" + "<field var='after_connect_commands'></field>" + "<field var='username'><value></value></field>" + "<field var='realname'><value></value></field>" + "<field var='throttle_limit'><value></value></field>" + "<field var='encoding_out'><value></value></field>" + "<field var='encoding_in'><value></value></field>" + "</x></command></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"), + + send_stanza("<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']", + "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure the IRC server irc.localhost']", + "/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Edit the form, to configure the settings of the IRC server irc.localhost']", + "!/iq/commands:command/dataform:x/dataform:field[@var='tls_ports']/dataform:value", + "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='pass']/dataform:value", + "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='after_connect_commands']/dataform:value", + "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='username']/dataform:value", + "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='realname']/dataform:value", + "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='encoding_in']/dataform:value", + "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='encoding_out']/dataform:value", + "/iq/commands:command/commands:actions/commands:complete", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='throttle_limit']/dataform:value[text()='10']", # An invalid value sets this field to its default value + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid")) + ), + 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>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"), + + ) diff --git a/tests/end_to_end/scenarios/irc_server_connection.py b/tests/end_to_end/scenarios/irc_server_connection.py new file mode 100644 index 0000000..f31b60f --- /dev/null +++ b/tests/end_to_end/scenarios/irc_server_connection.py @@ -0,0 +1,7 @@ +from scenarios import * + +scenario = ( + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection(), + ) + diff --git a/tests/end_to_end/scenarios/irc_server_connection_failure.py b/tests/end_to_end/scenarios/irc_server_connection_failure.py new file mode 100644 index 0000000..adcbc57 --- /dev/null +++ b/tests/end_to_end/scenarios/irc_server_connection_failure.py @@ -0,0 +1,11 @@ +from scenarios import * + +scenario = ( + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%doesnotexist@{biboumi_host}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/message/body[text()='Connecting to doesnotexist:6697 (encrypted)']"), + expect_stanza("/message/body[re:test(text(), 'Connection failed: (Domain name not found|Name or service not known)')]"), + 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)')]", + ), +) diff --git a/tests/end_to_end/scenarios/irc_server_presence_in_roster.py b/tests/end_to_end/scenarios/irc_server_presence_in_roster.py new file mode 100644 index 0000000..24c1b60 --- /dev/null +++ b/tests/end_to_end/scenarios/irc_server_presence_in_roster.py @@ -0,0 +1,24 @@ +from scenarios import * + +scenario = ( + # Mutual subscription exchange + send_stanza("<presence from='{jid_one}' to='{irc_server_one}' type='subscribe' id='subid1' />"), + expect_stanza("/presence[@type='subscribed'][@id='subid1']"), + + expect_stanza("/presence[@type='subscribe']"), + send_stanza("<presence from='{jid_one}' to='{irc_server_one}' type='subscribed' />"), + + # Join a channel on that server + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + + # We must receive the IRC server presence, in the connection sequence + sequences.connection("irc.localhost", '{jid_one}/{resource_one}', expected_irc_presence=True), + 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']"), + expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), + + # Leave the channel, and thus the IRC server + send_stanza("<presence type='unavailable' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), + expect_stanza("/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}']"), + expect_stanza("/presence[@from='{irc_server_one}'][@to='{jid_one}'][@type='unavailable']"), +) diff --git a/tests/end_to_end/scenarios/irc_server_presence_subscription.py b/tests/end_to_end/scenarios/irc_server_presence_subscription.py new file mode 100644 index 0000000..e9ad1a5 --- /dev/null +++ b/tests/end_to_end/scenarios/irc_server_presence_subscription.py @@ -0,0 +1,6 @@ +from scenarios import * + +scenario = ( + send_stanza("<presence type='subscribe' from='{jid_one}/{resource_one}' to='{irc_server_one}' id='sub1' />"), + expect_stanza("/presence[@to='{jid_one}'][@from='{irc_server_one}'][@type='subscribed']"), +) diff --git a/tests/end_to_end/scenarios/irc_tls_connection.py b/tests/end_to_end/scenarios/irc_tls_connection.py new file mode 100644 index 0000000..db5d32e --- /dev/null +++ b/tests/end_to_end/scenarios/irc_tls_connection.py @@ -0,0 +1,24 @@ +from scenarios import * + +scenario = ( + # First, use an adhoc command to configure how we connect to the irc server, configure + # only one TLS port, and disable the cert verification. + 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>"), + expect_stanza("/iq[@type='result']", + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))), + 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>" + "<field var='nick'><value>my_special_nickname</value></field>" + "</x></command></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection_tls("irc.localhost", '{jid_one}/{resource_one}'), + expect_stanza("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/my_special_nickname']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='110']"), + expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), +) diff --git a/tests/end_to_end/scenarios/join_history_limit.py b/tests/end_to_end/scenarios/join_history_limit.py new file mode 100644 index 0000000..216e2a0 --- /dev/null +++ b/tests/end_to_end/scenarios/join_history_limit.py @@ -0,0 +1,101 @@ +from scenarios import * + +scenario = ( + # Disable the throttling because the test is based on timings + 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>"), + expect_stanza("/iq[@type='result']", + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))), + 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'><value>6667</value></field>" + "<field var='tls_ports'><value>6697</value><value>6670</value></field>" + "<field var='throttle_limit'><value>9999</value></field>" + "</x></command></iq>"), + expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"), + + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}'), + 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']"), + expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), + + # Send two channel messages + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"), + 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]"), + + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 2</body></message>"), + # Record the current time + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou 2']", + after = save_current_timestamp_plus_delta("first_timestamp", datetime.timedelta(seconds=1))), + + # Wait two seconds before sending two new messages + sleep_for(2), + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 3</body></message>"), + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 4</body></message>"), + expect_stanza("/message[@type='groupchat']/body[text()='coucou 3']"), + expect_stanza("/message[@type='groupchat']/body[text()='coucou 4']", + after = save_current_timestamp_plus_delta("second_timestamp", datetime.timedelta(seconds=1))), + + # join some other channel, to stay connected to the server even after leaving #foo + send_stanza("<presence from='{jid_one}/{resource_one}' to='#DUMMY%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"), + expect_stanza("/message/subject"), + + # Leave #foo + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"), + expect_stanza("/presence[@type='unavailable']"), + + sleep_for(0.2), + + # Rejoin #foo, with some history limit + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history maxchars='0'/></x></presence>"), + expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"), + expect_stanza("/message/subject"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"), + expect_stanza("/presence[@type='unavailable']"), + + sleep_for(0.2), + + # Rejoin #foo, with some history limit + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history maxstanzas='3'/></x></presence>"), + expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 2']"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"), + expect_stanza("/message/subject"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"), + expect_stanza("/presence[@type='unavailable']"), + + # Rejoin #foo, with some history limit + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history since='{first_timestamp}'/></x></presence>"), + expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"), + expect_stanza("/message/subject"), + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"), + expect_stanza("/presence[@type='unavailable']"), + + # Rejoin #foo, with some history limit + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history seconds='1'/></x></presence>"), + expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"), + expect_stanza("/message/subject"), + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"), + expect_stanza("/presence[@type='unavailable']"), + + # Rejoin #foo, with some history limit + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history seconds='5'/></x></presence>"), + expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou']"), expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 2']"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 3']"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='coucou 4']"), + expect_stanza("/message/subject"), + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"), + expect_stanza("/presence[@type='unavailable']"), +) diff --git a/tests/end_to_end/scenarios/leave_unjoined_chan.py b/tests/end_to_end/scenarios/leave_unjoined_chan.py new file mode 100644 index 0000000..aeb6f55 --- /dev/null +++ b/tests/end_to_end/scenarios/leave_unjoined_chan.py @@ -0,0 +1,14 @@ +from scenarios import * + +scenario = ( + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}'), + simple_channel_join.expect_self_join_presence(), + + send_stanza("<presence from='{jid_two}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection_begin("irc.localhost", '{jid_two}/{resource_two}'), + + expect_stanza("/message[@to='{jid_two}/{resource_two}'][@type='chat']/body[text()='irc.localhost: {nick_one}: Nickname is already in use']"), + expect_stanza("/presence[@type='error']/error[@type='cancel'][@code='409']/stanza:conflict"), + send_stanza("<presence from='{jid_two}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />") +) diff --git a/tests/end_to_end/scenarios/list_adhoc.py b/tests/end_to_end/scenarios/list_adhoc.py new file mode 100644 index 0000000..7b46312 --- /dev/null +++ b/tests/end_to_end/scenarios/list_adhoc.py @@ -0,0 +1,10 @@ +from scenarios import * + +scenario = ( + send_stanza("<iq type='get' id='idwhatever' from='{jid_one}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"), + expect_stanza("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']", + "/iq/disco_items:query/disco_items:item[@node='configure']", + "/iq/disco_items:query/disco_items:item[4]", + "!/iq/disco_items:query/disco_items:item[5]"), + +) diff --git a/tests/end_to_end/scenarios/list_adhoc_fixed_server.py b/tests/end_to_end/scenarios/list_adhoc_fixed_server.py new file mode 100644 index 0000000..fef378b --- /dev/null +++ b/tests/end_to_end/scenarios/list_adhoc_fixed_server.py @@ -0,0 +1,12 @@ +from scenarios import * + +conf = "fixed_server" + +scenario = ( + send_stanza("<iq type='get' id='idwhatever' from='{jid_one}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"), + expect_stanza("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']", + "/iq/disco_items:query/disco_items:item[@node='global-configure']", + "/iq/disco_items:query/disco_items:item[@node='server-configure']", + "/iq/disco_items:query/disco_items:item[6]", + "!/iq/disco_items:query/disco_items:item[7]"), +) diff --git a/tests/end_to_end/scenarios/list_adhoc_irc.py b/tests/end_to_end/scenarios/list_adhoc_irc.py new file mode 100644 index 0000000..ff94a1b --- /dev/null +++ b/tests/end_to_end/scenarios/list_adhoc_irc.py @@ -0,0 +1,8 @@ +from scenarios import * + +scenario = ( + send_stanza("<iq type='get' id='idwhatever' from='{jid_one}/{resource_one}' to='{irc_host_one}@{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"), + expect_stanza("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']", + "/iq/disco_items:query/disco_items:item[2]", + "!/iq/disco_items:query/disco_items:item[3]"), +) diff --git a/tests/end_to_end/scenarios/list_admin_adhoc.py b/tests/end_to_end/scenarios/list_admin_adhoc.py new file mode 100644 index 0000000..0b71662 --- /dev/null +++ b/tests/end_to_end/scenarios/list_admin_adhoc.py @@ -0,0 +1,9 @@ +from scenarios import * + +scenario = ( + send_stanza("<iq type='get' id='idwhatever' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"), + expect_stanza("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']", + "/iq/disco_items:query/disco_items:item[@node='configure']", + "/iq/disco_items:query/disco_items:item[6]", + "!/iq/disco_items:query/disco_items:item[7]"), +) diff --git a/tests/end_to_end/scenarios/list_admin_adhoc_fixed_server.py b/tests/end_to_end/scenarios/list_admin_adhoc_fixed_server.py new file mode 100644 index 0000000..8e2775e --- /dev/null +++ b/tests/end_to_end/scenarios/list_admin_adhoc_fixed_server.py @@ -0,0 +1,12 @@ +from scenarios import * + +conf = "fixed_server" + +scenario = ( + send_stanza("<iq type='get' id='idwhatever' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"), + expect_stanza("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']", + "/iq/disco_items:query/disco_items:item[@node='global-configure']", + "/iq/disco_items:query/disco_items:item[@node='server-configure']", + "/iq/disco_items:query/disco_items:item[8]", + "!/iq/disco_items:query/disco_items:item[9]"), +) diff --git a/tests/end_to_end/scenarios/list_muc_user_adhoc.py b/tests/end_to_end/scenarios/list_muc_user_adhoc.py new file mode 100644 index 0000000..6827a8d --- /dev/null +++ b/tests/end_to_end/scenarios/list_muc_user_adhoc.py @@ -0,0 +1,6 @@ +from scenarios import * + +scenario = ( + send_stanza("<iq type='get' id='idwhatever' from='{jid_admin}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"), + expect_stanza("/iq[@type='error']/error[@type='cancel']/stanza:feature-not-implemented"), +) diff --git a/tests/end_to_end/scenarios/mam_on_fixed_server.py b/tests/end_to_end/scenarios/mam_on_fixed_server.py new file mode 100644 index 0000000..200f04e --- /dev/null +++ b/tests/end_to_end/scenarios/mam_on_fixed_server.py @@ -0,0 +1,21 @@ +from scenarios import * + +conf = 'fixed_server' + +scenario = ( + scenarios.channel_join_on_fixed_irc_server.scenario, + + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' type='groupchat'><body>coucou</body></message>"), + expect_stanza("/message[@from='#foo@{biboumi_host}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']"), + + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' type='groupchat'><body>coucou 2</body></message>"), + expect_stanza("/message[@from='#foo@{biboumi_host}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou 2']"), + + # Retrieve the complete archive + 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>"), + + expect_stanza("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay", + "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo@{biboumi_host}/{nick_one}'][@type='groupchat']/client:body[text()='coucou']"), + expect_stanza("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay", + "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo@{biboumi_host}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 2']"), +) diff --git a/tests/end_to_end/scenarios/mam_with_timestamps.py b/tests/end_to_end/scenarios/mam_with_timestamps.py new file mode 100644 index 0000000..0ed0333 --- /dev/null +++ b/tests/end_to_end/scenarios/mam_with_timestamps.py @@ -0,0 +1,63 @@ +from scenarios import * + +scenario = ( + scenarios.simple_channel_join.scenario, + + # Send two channel messages + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"), + 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]"), + + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 2</body></message>"), + # Record the current time + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou 2']", + after = save_current_timestamp_plus_delta("first_timestamp", datetime.timedelta(seconds=1))), + + # Wait two seconds before sending two new messages + sleep_for(2), + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 3</body></message>"), + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 4</body></message>"), + expect_stanza("/message[@type='groupchat']/body[text()='coucou 3']"), + expect_stanza("/message[@type='groupchat']/body[text()='coucou 4']", + after = save_current_timestamp_plus_delta("second_timestamp", datetime.timedelta(seconds=1))), + + # Retrieve the archive, after our saved datetime + 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>"""), + + 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']"), + + 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']"), + + expect_stanza("/iq[@type='result'][@id='id8'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", + "/iq/mam:fin[@complete='true']/rsm:set"), + + # Try the same thing, but only with the 'start' value, omitting the end + send_stanza("""<iq from='{jid_one}/{resource_one}' id='id888' to='#foo%{irc_server_one}' type='set'> + <query queryid='qid17' xmlns='urn:xmpp:mam:2'> + <x type='submit' xmlns='jabber:x:data'> + <field type='hidden' 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> + </x> + </query> + </iq>"""), + + expect_stanza("/message/mam:result[@queryid='qid17']/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']"), + + expect_stanza("/message/mam:result[@queryid='qid17']/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']"), + + expect_stanza("/iq[@type='result'][@id='id888'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", + "/iq/mam:fin[@complete='true']/rsm:set"), + +) diff --git a/tests/end_to_end/scenarios/mode_change.py b/tests/end_to_end/scenarios/mode_change.py new file mode 100644 index 0000000..b45904b --- /dev/null +++ b/tests/end_to_end/scenarios/mode_change.py @@ -0,0 +1,62 @@ +from scenarios import * + +scenario = ( + scenarios.channel_join_with_two_users.scenario, + + # Change a user mode with a message starting with /mode + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>/mode +v {nick_two}</body></message>"), + 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 + 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>"), + 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 +v manually. User ONLY has +o now. This doesn’t change the role/affiliation + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>/mode -v {nick_two}</body></message>"), + 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='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']"], + ), + + + # remove the mode + 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>"), + 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 + 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>"), + expect_stanza("/iq[@type='error']"), + + # using an iq, without the rights to do it + 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>"), + expect_unordered( + ["/iq[@type='error']"], + ["/message[@type='chat'][@to='{jid_two}/{resource_one}']"] + ), + + # using an iq, with an unknown mode + 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>"), + expect_unordered( + ["/iq[@type='error']"], + ["/message[@type='chat'][@to='{jid_two}/{resource_one}']"], + ), +) diff --git a/tests/end_to_end/scenarios/muc_disco_info.py b/tests/end_to_end/scenarios/muc_disco_info.py new file mode 100644 index 0000000..9b6f0e5 --- /dev/null +++ b/tests/end_to_end/scenarios/muc_disco_info.py @@ -0,0 +1,28 @@ +from scenarios import * + +scenario = ( + 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>"), + 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='#foo on {irc_host_one}']", + "/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']", + "/iq/disco_info:query/disco_info:feature[@var='muc_nonanonymous']", + "/iq/disco_info:query/disco_info:feature[@var='urn:xmpp:sid:0']", + "!/iq/disco_info:query/dataform:x/dataform:field[@var='muc#roominfo_occupants']"), + + # Join the channel, and re-do the same query + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}'), + 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']"), + + expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), + + send_stanza("<iq from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' id='2' type='get'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>"), + expect_stanza("/iq[@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='result']/disco_info:query", + "/iq/disco_info:query/dataform:x/dataform:field[@var='muc#roominfo_occupants']/dataform:value[text()='1']", + "/iq/disco_info:query/dataform:x/dataform:field[@var='FORM_TYPE'][@type='hidden']/dataform:value[text()='http://jabber.org/protocol/muc#roominfo']"), +) diff --git a/tests/end_to_end/scenarios/muc_message_from_unjoined_resource.py b/tests/end_to_end/scenarios/muc_message_from_unjoined_resource.py new file mode 100644 index 0000000..bffe3aa --- /dev/null +++ b/tests/end_to_end/scenarios/muc_message_from_unjoined_resource.py @@ -0,0 +1,16 @@ +from scenarios import * + +scenario = ( + scenarios.simple_channel_join.scenario, + + # Send a channel message + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"), + # Receive the message + 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]"), + # Send a message from a resource that is not joined + send_stanza("<message from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"), + expect_stanza("/message[@type='error']/error[@type='modify']/stanza:text[text()='You are not a participant in this room.']", + "/message/error/stanza:not-acceptable" + ), +) diff --git a/tests/end_to_end/scenarios/muc_traffic_info.py b/tests/end_to_end/scenarios/muc_traffic_info.py new file mode 100644 index 0000000..0ef0d37 --- /dev/null +++ b/tests/end_to_end/scenarios/muc_traffic_info.py @@ -0,0 +1,6 @@ +from scenarios import * + +scenario = ( + 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>"), + 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']"), +) diff --git a/tests/end_to_end/scenarios/multiline_message.py b/tests/end_to_end/scenarios/multiline_message.py new file mode 100644 index 0000000..cd42c6c --- /dev/null +++ b/tests/end_to_end/scenarios/multiline_message.py @@ -0,0 +1,62 @@ +from scenarios import * + +import scenarios.simple_channel_join + +scenario = ( + scenarios.simple_channel_join.scenario, + + # Send a multi-line channel message + send_stanza("<message id='the-message-id' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>un\ndeux\ntrois</body></message>"), + # Receive multiple messages, in order + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='un']"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='deux']"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='trois']"), + + # Send a simple message, with no id + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>hello</body></message>"), + + # Expect a non-empty id as a result (should be a uuid) + expect_stanza("!/message[@id='']", + "/message[@id]/body[text()='hello']"), + + # even though we reflect the message to XMPP only + # when we send it to IRC, there’s still a race + # condition if the XMPP client receives the + # reflection (and the IRC server didn’t yet receive + # it), then the new user joins the room, and then + # finally the IRC server sends the message to “all + # participants of the channel”, including the new + # one, that was not supposed to be there when the + # message was sent in the first place by the first + # XMPP user. There’s nothing we can do about it until + # all servers support the echo-message IRCv3 + # extension… So, we just sleep a little bit before + # joining the room with the new user. + sleep_for(0.2), + # Second user joins + send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), + # Our presence, sent to the other user + expect_unordered( + ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']"], + ["/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']"], + [ + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ], + ["/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"] + ), + + # Send a multi-line channel message + send_stanza("<message id='the-message-id' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>a\nb\nc</body></message>"), + # Receive multiple messages, for each user + expect_unordered( + ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='a']"], + ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='b']"], + ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='c']"], + + ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='a']"], + ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='b']"], + ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='c']"], + ) +) diff --git a/tests/end_to_end/scenarios/multiline_topic.py b/tests/end_to_end/scenarios/multiline_topic.py new file mode 100644 index 0000000..ca163a0 --- /dev/null +++ b/tests/end_to_end/scenarios/multiline_topic.py @@ -0,0 +1,11 @@ +from scenarios import * + +import scenarios.simple_channel_join + +scenario = ( + scenarios.simple_channel_join.scenario, + # User tries to set a multiline topic + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><subject>FIRST LINE\nSECOND LINE.</subject></message>"), + # Server converts the newline into spaces, because IRC can’t have them in the topic + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[text()='FIRST LINE SECOND LINE.']") +) diff --git a/tests/end_to_end/scenarios/multiple_channels_join.py b/tests/end_to_end/scenarios/multiple_channels_join.py new file mode 100644 index 0000000..839909f --- /dev/null +++ b/tests/end_to_end/scenarios/multiple_channels_join.py @@ -0,0 +1,18 @@ +from scenarios import * + +from scenarios.simple_channel_join import expect_self_join_presence + +scenario = ( + # Join 3 rooms, on the same server, with three different nicks + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + send_stanza("<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + send_stanza("<presence from='{jid_one}/{resource_one}' to='#baz%{irc_server_one}/{nick_three}'> <x xmlns='http://jabber.org/protocol/muc'><password>SECRET</password></x></presence>"), + + sequences.connection(), + + # The first nick we specified should be the only one we receive, the rest was ignored + expect_self_join_presence(jid='{jid_one}/{resource_one}', chan="#foo", nick="{nick_one}"), + expect_self_join_presence(jid='{jid_one}/{resource_one}', chan="#bar", nick="{nick_one}"), + expect_self_join_presence(jid='{jid_one}/{resource_one}', chan="#baz", nick="{nick_one}"), +) + diff --git a/tests/end_to_end/scenarios/multisession_kick.py b/tests/end_to_end/scenarios/multisession_kick.py new file mode 100644 index 0000000..580beb4 --- /dev/null +++ b/tests/end_to_end/scenarios/multisession_kick.py @@ -0,0 +1,48 @@ +from scenarios import * + +scenario = ( + scenarios.simple_channel_join.scenario, + + # Second user joins, from two resources + send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), + expect_unordered( + ["/presence[@to='{jid_one}/{resource_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']"], + ["/presence[@to='{jid_two}/{resource_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']"], + ["/presence[@to='{jid_two}/{resource_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}']/muc_user:x/muc_user:status[@code='110']"], + ["/message/subject"] + ), + # Second resource + send_stanza("<presence from='{jid_two}/{resource_two}' to='#foo%{irc_server_one}/{nick_two}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_unordered( + ["/presence[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']"], + ["/presence[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_two}']", + "/presence/muc_user:x/muc_user:status[@code='110']"] + ), + expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_two}/{resource_two}']/subject[not(text())]"), + + # Moderator kicks participant + 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>"), + 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']", + "/presence/muc_user:x/muc_user:status[@code='307']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ], + [ + "/presence[@type='unavailable'][@to='{jid_two}/{resource_two}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']", + "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']", + "/presence/muc_user:x/muc_user:status[@code='307']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ], + ["/presence[@type='unavailable']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']", + "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']", + "/presence/muc_user:x/muc_user:status[@code='307']", + ], + [ + "/iq[@id='kick1'][@type='result']" + ] + ), +) diff --git a/tests/end_to_end/scenarios/multisessionnick.py b/tests/end_to_end/scenarios/multisessionnick.py new file mode 100644 index 0000000..2fd84f9 --- /dev/null +++ b/tests/end_to_end/scenarios/multisessionnick.py @@ -0,0 +1,125 @@ +from scenarios import * + +from scenarios.simple_channel_join import expect_self_join_presence + +scenario = ( + # Resource one joins a channel + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection(), + expect_self_join_presence(jid = '{jid_one}/{resource_one}', chan = "#foo", nick = "{nick_one}"), + + # The other resources joins the same room, with the same nick + send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + + # We receive our own join + expect_unordered( + [ + "/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ], + [ + "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]" + + ] + ), + + # A different user joins the same room + send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), + expect_unordered( + # The new user’s presence is sent to the the existing occupant (two resources) + [ + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']" + ], + [ + "/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_two}']" + ], + # the new user receives her own presence + [ + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ], + # the new user receives the presence of the existing occupant + [ + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']", + ], + [ + "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]", + ], + ), + + # That second user sends a private message to the first one + send_stanza("<message from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='chat'><body>RELLO</body></message>"), + + # Message is received with a server-wide JID, by the two resources behind nick_one + expect_unordered( + [ + "/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='RELLO']", + "/message/hints:no-copy", + "/message/carbon:private", + "!/message/muc_user:x", + ], + [ + "/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_two}'][@type='chat']/body[text()='RELLO']", + "/message/hints:no-copy", + "/message/carbon:private", + "!/message/muc_user:x", + ] + ), + + # First occupant (with the two resources) changes her/his nick to a conflicting one + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"), + expect_unordered( + ["/message[@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='irc.localhost: Nick2: Nickname is already in use']"], + ["/message[@to='{jid_one}/{resource_two}'][@type='chat']/body[text()='irc.localhost: Nick2: Nickname is already in use']"], + ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}'][@type='error']"], + ["/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_two}'][@type='error']"] + ), + + # First occupant (with the two resources) changes her/his nick + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}' />"), + expect_unordered( + [ + "/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Nick3']", + "/presence/muc_user:x/muc_user:status[@code='303']" + ], + [ + "/presence[@from='#foo%{irc_server_one}/{nick_three}'][@to='{jid_two}/{resource_one}']" + ], + [ + "/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Nick3']", + "/presence/muc_user:x/muc_user:status[@code='303']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ], + [ + "/presence[@from='#foo%{irc_server_one}/{nick_three}'][@to='{jid_one}/{resource_one}']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ], + [ + "/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_two}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Nick3']", + "/presence/muc_user:x/muc_user:status[@code='303']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ], + [ + "/presence[@from='#foo%{irc_server_one}/{nick_three}'][@to='{jid_one}/{resource_two}']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ] + ), + + # One resource leaves the server entirely. + send_stanza("<presence type='unavailable' from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"), + # The leave is forwarded only to that resource + expect_stanza("/presence[@type='unavailable']/muc_user:x/muc_user:status[@code='110']", + "/presence/status[text()='Biboumi note: 1 resources are still in this channel.']", + ), + + # The second user sends two new private messages to the first user + send_stanza("<message from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}' type='chat'><body>first</body></message>"), + send_stanza("<message from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}' type='chat'><body>second</body></message>"), + + # The first user receives the two messages, on the connected resource, once each + expect_unordered( + ["/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='first']"], + ["/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='second']"] + ), +) diff --git a/tests/end_to_end/scenarios/nick_change.py b/tests/end_to_end/scenarios/nick_change.py new file mode 100644 index 0000000..9d06856 --- /dev/null +++ b/tests/end_to_end/scenarios/nick_change.py @@ -0,0 +1,33 @@ +from scenarios import * + +import scenarios.channel_join_with_two_users + +scenario = ( + scenarios.channel_join_with_two_users.scenario, + + # first users changes their nick + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}' id='nick_change' />"), + expect_unordered( + ["/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='unavailable']", + "/presence/muc_user:x/muc_user:status[@code='303']", + "/presence/muc_user:x/muc_user:item[@affiliation='admin']", + "/presence/muc_user:x/muc_user:item[@role='moderator']", + "/presence/muc_user:x/muc_user:item[@nick='{nick_three}']"], + + ["/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']", + "/presence/muc_user:x/muc_user:status[@code='303']", + "/presence/muc_user:x/muc_user:item[@nick='{nick_three}']", + "/presence/muc_user:x/muc_user:item[@affiliation='admin']", + "/presence/muc_user:x/muc_user:item[@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='110']"], + + ["/presence[@from='#foo%{irc_server_one}/{nick_three}'][@to='{jid_two}/{resource_one}']", + "/presence/muc_user:x/muc_user:item[@affiliation='admin']", + "/presence/muc_user:x/muc_user:item[@role='moderator']"], + + ["/presence[@from='#foo%{irc_server_one}/{nick_three}'][@to='{jid_one}/{resource_one}']", + "/presence/muc_user:x/muc_user:item[@affiliation='admin']", + "/presence/muc_user:x/muc_user:item[@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='110']"] + ), +) diff --git a/tests/end_to_end/scenarios/nick_change_in_join.py b/tests/end_to_end/scenarios/nick_change_in_join.py new file mode 100644 index 0000000..f4feae3 --- /dev/null +++ b/tests/end_to_end/scenarios/nick_change_in_join.py @@ -0,0 +1,17 @@ +from scenarios import * + +from scenarios.simple_channel_join import expect_self_join_presence + +scenario = ( + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection(), + expect_self_join_presence(jid = '{jid_one}/{resource_one}', chan = "#foo", nick = "{nick_one}"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + 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']", + "/presence/muc_user:x/muc_user:status[@code='210']", # This status signals that the server forced our nick to NOT be the one we asked + ), + expect_stanza("/message[@from='#bar%{irc_server_one}'][@type='groupchat']/subject[not(text())]"), +) + diff --git a/tests/end_to_end/scenarios/not_connected_error.py b/tests/end_to_end/scenarios/not_connected_error.py new file mode 100644 index 0000000..577b324 --- /dev/null +++ b/tests/end_to_end/scenarios/not_connected_error.py @@ -0,0 +1,12 @@ +from scenarios import * + +from scenarios.simple_channel_join import expect_self_join_presence + +scenario = ( + send_stanza("<presence type='unavailable' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), + # Fixme: what is the purpose of this test? Check that we don’t receive anything here…? + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection(), + expect_self_join_presence(jid='{jid_one}/{resource_one}', chan="#foo", nick="{nick_one}"), +) diff --git a/tests/end_to_end/scenarios/notices.py b/tests/end_to_end/scenarios/notices.py new file mode 100644 index 0000000..dddee5d --- /dev/null +++ b/tests/end_to_end/scenarios/notices.py @@ -0,0 +1,10 @@ +from scenarios import * + +import scenarios.simple_channel_join + +scenario = ( + scenarios.simple_channel_join.scenario, + + 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>"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='[notice] [#foo] Hello in a notice.']"), +) diff --git a/tests/end_to_end/scenarios/persistent_channel.py b/tests/end_to_end/scenarios/persistent_channel.py new file mode 100644 index 0000000..ab525fe --- /dev/null +++ b/tests/end_to_end/scenarios/persistent_channel.py @@ -0,0 +1,48 @@ +from scenarios import * + +import scenarios.simple_channel_join + +scenario = ( + # Join the channel with user 1 + scenarios.simple_channel_join.scenario, + + # Make it persistent for user 1 + send_stanza("<iq from='{jid_one}/{resource_one}' id='conf1' to='#foo%{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"), + expect_stanza("/iq[@type='result']/muc_owner:query/dataform:x/dataform:field[@var='persistent'][@type='boolean']/dataform:value[text()='false']"), + send_stanza("<iq from='{jid_one}/{resource_one}' id='conf2' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#owner'><x type='submit' xmlns='jabber:x:data'><field var='persistent' xmlns='jabber:x:data'><value>true</value></field></x></query></iq>"), + expect_stanza("/iq[@type='result']"), + + # Check that the value is now effectively true + send_stanza("<iq from='{jid_one}/{resource_one}' id='conf1' to='#foo%{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"), + expect_stanza("/iq[@type='result']/muc_owner:query/dataform:x/dataform:field[@var='persistent'][@type='boolean']/dataform:value[text()='true']"), + + # A second user joins the same channel + send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), + expect_unordered( + ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']"], + [ + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ], + ["/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']"], + ["/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"] + ), + + # First user leaves the room (but biboumi will stay in the channel) + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />"), + + # Only user 1 receives the unavailable presence + expect_stanza("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:status[@code='110']", + "/presence/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']"), + + # Second user sends a channel message + send_stanza("<message type='groupchat' from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}'><body>coucou</body></message>"), + + # Message should only be received by user 2, since user 1 has no resource in the room + expect_stanza("/message[@type='groupchat'][@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']"), + + # Second user leaves the channel + send_stanza("<presence type='unavailable' from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"), + expect_stanza("/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_two}']"), +) diff --git a/tests/end_to_end/scenarios/quit.py b/tests/end_to_end/scenarios/quit.py new file mode 100644 index 0000000..ced5a96 --- /dev/null +++ b/tests/end_to_end/scenarios/quit.py @@ -0,0 +1,12 @@ +from scenarios import * + +import scenarios.simple_channel_join + +scenario = ( + scenarios.simple_channel_join.scenario, + + # Send a raw QUIT message + send_stanza("<message from='{jid_one}/{resource_one}' to='{irc_server_one}' type='chat'><body>QUIT bye bye</body></message>"), + expect_stanza("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@type='unavailable']/muc_user:x/muc_user:status[@code='110']"), +) + diff --git a/tests/end_to_end/scenarios/raw_message.py b/tests/end_to_end/scenarios/raw_message.py new file mode 100644 index 0000000..c6cd4e7 --- /dev/null +++ b/tests/end_to_end/scenarios/raw_message.py @@ -0,0 +1,11 @@ +from scenarios import * + +scenario = ( + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}'), + expect_stanza("/presence"), + expect_stanza("/message"), + + send_stanza("<message from='{jid_one}/{resource_one}' to='{irc_server_one}' type='chat'><body>WHOIS {nick_one}</body></message>"), + expect_stanza("/message[@from='{irc_server_one}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} localhost * {nick_one}']"), +) diff --git a/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py b/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py new file mode 100644 index 0000000..7eb5b13 --- /dev/null +++ b/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py @@ -0,0 +1,14 @@ +from scenarios import * + +conf = 'fixed_server' + +scenario = ( + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True), + expect_stanza("/presence"), + expect_stanza("/message"), + + send_stanza("<message from='{jid_one}/{resource_one}' to='{biboumi_host}' type='chat'><body>WHOIS {nick_one}</body></message>"), + expect_stanza("/message[@from='{biboumi_host}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} localhost * {nick_one}']"), + +) diff --git a/tests/end_to_end/scenarios/resource_is_removed_from_server_when_last_chan_is_left.py b/tests/end_to_end/scenarios/resource_is_removed_from_server_when_last_chan_is_left.py new file mode 100644 index 0000000..dfb7161 --- /dev/null +++ b/tests/end_to_end/scenarios/resource_is_removed_from_server_when_last_chan_is_left.py @@ -0,0 +1,40 @@ +from scenarios import * + +scenario = ( + # Join the channel + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}'), + 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']"), + expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[not(text())]"), + + # Make it persistent + send_stanza("<iq from='{jid_one}/{resource_one}' id='conf1' to='#foo%{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"), + expect_stanza("/iq[@type='result']/muc_owner:query/dataform:x/dataform:field[@var='persistent'][@type='boolean']/dataform:value[text()='false']"), + send_stanza("<iq from='{jid_one}/{resource_one}' id='conf2' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#owner'><x type='submit' xmlns='jabber:x:data'><field var='persistent' xmlns='jabber:x:data'><value>true</value></field></x></query></iq>"), + expect_stanza("/iq[@type='result']"), + + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />"), + expect_stanza("/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}']"), + + # Join the same channel, with the same JID, but a different resource + send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='110']"), + expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"), + + # Join some other channel with someone else + send_stanza("<presence from='{jid_two}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), + expect_stanza("/presence[@to='{jid_two}/{resource_one}'][@from='#bar%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='110']"), + expect_stanza("/message[@from='#bar%{irc_server_one}'][@type='groupchat'][@to='{jid_two}/{resource_one}']/subject[not(text())]"), + + # Send two messages from the second user to the first one + send_stanza("<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>kikoo</body></message>"), + send_stanza("<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>second kikoo</body></message>"), + + # We must receive each message only once, no duplicate + expect_stanza("/message/body[text()='kikoo']"), + expect_stanza("/message/body[text()='second kikoo']"), +) diff --git a/tests/end_to_end/scenarios/self_disco_info.py b/tests/end_to_end/scenarios/self_disco_info.py new file mode 100644 index 0000000..6430dbd --- /dev/null +++ b/tests/end_to_end/scenarios/self_disco_info.py @@ -0,0 +1,11 @@ +from scenarios import * + +scenario = ( + send_stanza("<iq type='get' id='get1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>"), + expect_stanza("/iq[@type='result']/disco_info:query/disco_info:identity[@category='conference'][@type='irc'][@name='Biboumi XMPP-IRC gateway']", + "/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']"), +) diff --git a/tests/end_to_end/scenarios/self_ping_fixed_server.py b/tests/end_to_end/scenarios/self_ping_fixed_server.py new file mode 100644 index 0000000..453387c --- /dev/null +++ b/tests/end_to_end/scenarios/self_ping_fixed_server.py @@ -0,0 +1,11 @@ +from scenarios import * + +conf = "fixed_server" + +scenario = ( + scenarios.simple_channel_join_fixed.scenario, + + # Send a ping to ourself + 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>"), + expect_stanza("/iq[@from='#foo@{biboumi_host}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"), +) diff --git a/tests/end_to_end/scenarios/self_ping_not_in_muc.py b/tests/end_to_end/scenarios/self_ping_not_in_muc.py new file mode 100644 index 0000000..eb7d092 --- /dev/null +++ b/tests/end_to_end/scenarios/self_ping_not_in_muc.py @@ -0,0 +1,15 @@ +from scenarios import * + +scenario = ( + scenarios.simple_channel_join.scenario, + + # Send a ping to ourself, in a muc where we’re not + 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 + expect_stanza("/iq[@from='#nil%{irc_server_one}/{nick_one}'][@type='error'][@to='{jid_one}/{resource_one}'][@id='first_ping']/error/stanza:not-acceptable"), + + # Send a ping to ourself, in a muc where we are, but not this resource + 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 + expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='error'][@to='{jid_one}/{resource_two}'][@id='first_ping']/error/stanza:not-acceptable"), +) diff --git a/tests/end_to_end/scenarios/self_ping_on_real_channel.py b/tests/end_to_end/scenarios/self_ping_on_real_channel.py new file mode 100644 index 0000000..3474288 --- /dev/null +++ b/tests/end_to_end/scenarios/self_ping_on_real_channel.py @@ -0,0 +1,23 @@ +from scenarios import * + +scenario = ( + scenarios.simple_channel_join.scenario, + + # Send a ping to ourself + 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>"), + expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"), + + # Now join the same room, from the same bare JID, behind the same nick + send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='110']"), + + expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"), + + # And re-send a self ping + send_stanza("<iq type='get' from='{jid_one}/{resource_one}' id='second_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"), + expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='second_ping']"), + ## And re-do exactly the same thing, just change the resource initiating the self ping + send_stanza("<iq type='get' from='{jid_one}/{resource_two}' id='third_ping' to='#foo%{irc_server_one}/{nick_one}'><ping xmlns='urn:xmpp:ping' /></iq>"), + expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_two}'][@id='third_ping']"), +) diff --git a/tests/end_to_end/scenarios/self_ping_with_error.py b/tests/end_to_end/scenarios/self_ping_with_error.py new file mode 100644 index 0000000..0266d20 --- /dev/null +++ b/tests/end_to_end/scenarios/self_ping_with_error.py @@ -0,0 +1,13 @@ +from scenarios import * + +scenario = ( + scenarios.simple_channel_join.scenario, + + # Send a ping to ourself + 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>"), + 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 + 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>"), + expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_ping']"), +) diff --git a/tests/end_to_end/scenarios/self_version.py b/tests/end_to_end/scenarios/self_version.py new file mode 100644 index 0000000..a5c81eb --- /dev/null +++ b/tests/end_to_end/scenarios/self_version.py @@ -0,0 +1,39 @@ +from scenarios import * + +scenario = ( + scenarios.simple_channel_join.scenario, + + # Send a version request to ourself + send_stanza("<iq type='get' from='{jid_one}/{resource_one}' id='first_version' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='jabber:iq:version' /></iq>"), + # We receive our own request, + expect_stanza("/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to='{jid_one}/{resource_one}']", + after = save_value("id", extract_attribute("/iq", 'id'))), + # Respond to the request, and receive our own response + 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>"), + expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='first_version']/version:query/version:name[text()='e2e test (through the biboumi gateway) 1.0 Fedora']"), + + # Now join the same room, from the same bare JID, behind the same nick + send_stanza("<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='110']"), + expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_two}']/subject[not(text())]"), + + # And re-send a self ping + send_stanza("<iq type='get' from='{jid_one}/{resource_two}' id='second_version' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='jabber:iq:version' /></iq>"), + # We receive our own request. Note that we don't know the `to` value, it could be one of our two resources. + expect_stanza("/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to]", + after = (save_value("to", extract_attribute("/iq", "to")), + save_value("id", extract_attribute("/iq", "id")))), + # Respond to the request, using the extracted 'to' value as our 'from' + send_stanza("<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='{id}' from='{to}'><query xmlns='jabber:iq:version'><name>e2e test</name><version>1.0</version><os>Fedora</os></query></iq>"), + expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_two}'][@id='second_version']"), + + # And do exactly the same thing, but initiated by the other resource + send_stanza("<iq type='get' from='{jid_one}/{resource_one}' id='second_version' to='#foo%{irc_server_one}/{nick_one}'><query xmlns='jabber:iq:version' /></iq>"), + expect_stanza("/iq[@from='{lower_nick_one}%{irc_server_one}'][@type='get'][@to]", + after = (save_value("to", extract_attribute("/iq", "to")), + save_value("id", extract_attribute("/iq", "id")))), + # Respond to the request, using the extracted 'to' value as our 'from' + send_stanza("<iq type='result' to='{lower_nick_one}%{irc_server_one}' id='{id}' from='{to}'><query xmlns='jabber:iq:version'><name>e2e test</name><version>1.0</version><os>Fedora</os></query></iq>"), + expect_stanza("/iq[@from='#foo%{irc_server_one}/{nick_one}'][@type='result'][@to='{jid_one}/{resource_one}'][@id='second_version']"), +) diff --git a/tests/end_to_end/scenarios/simple_channel_join.py b/tests/end_to_end/scenarios/simple_channel_join.py new file mode 100644 index 0000000..9beba3b --- /dev/null +++ b/tests/end_to_end/scenarios/simple_channel_join.py @@ -0,0 +1,20 @@ +from scenarios import * + +def expect_self_join_presence(jid='{jid_one}/{resource_one}', chan='#foo', nick='{nick_one}', irc_server="{irc_server_one}"): + return ( + expect_stanza("/presence[@to='" + jid +"'][@from='" + chan + "%" + irc_server + "/" + nick + "']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", + "/presence/muc_user:x/muc_user:status[@code='100']", # Rooms are all non-anonymous + "/presence/muc_user:x/muc_user:status[@code='110']", + ), + expect_stanza("/message[@from='" + chan + "%" + irc_server + "'][@type='groupchat']/subject[not(text())]"), + ) + + +scenario = ( + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection(), + + expect_self_join_presence(jid = '{jid_one}/{resource_one}', chan = "#foo", nick = "{nick_one}"), + +) + diff --git a/tests/end_to_end/scenarios/simple_channel_join_fixed.py b/tests/end_to_end/scenarios/simple_channel_join_fixed.py new file mode 100644 index 0000000..9f5b835 --- /dev/null +++ b/tests/end_to_end/scenarios/simple_channel_join_fixed.py @@ -0,0 +1,11 @@ +from scenarios import * + +conf = "fixed_server" + +scenario = ( + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection("irc.localhost", '{jid_one}/{resource_one}', fixed_irc_server=True), + 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']"), + expect_stanza("/message[@from='#foo@{biboumi_host}'][@type='groupchat']/subject[not(text())]"), +) diff --git a/tests/end_to_end/scenarios/simple_channel_list.py b/tests/end_to_end/scenarios/simple_channel_list.py new file mode 100644 index 0000000..7406e86 --- /dev/null +++ b/tests/end_to_end/scenarios/simple_channel_list.py @@ -0,0 +1,14 @@ +from scenarios import * + +scenario = ( + scenarios.multiple_channels_join.scenario, + + 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>"), + expect_stanza("/iq[@type='result']/disco_items:query", + "/iq/disco_items:query/rsm:set/rsm:count[text()='3']", + "/iq/disco_items:query/rsm:set/rsm:first", + "/iq/disco_items:query/rsm:set/rsm:last", + "/iq/disco_items:query/disco_items:item[@jid='#foo%{irc_server_one}']", + "/iq/disco_items:query/disco_items:item[@jid='#bar%{irc_server_one}']", + "/iq/disco_items:query/disco_items:item[@jid='#baz%{irc_server_one}']"), +) diff --git a/tests/end_to_end/scenarios/simple_kick.py b/tests/end_to_end/scenarios/simple_kick.py new file mode 100644 index 0000000..2949157 --- /dev/null +++ b/tests/end_to_end/scenarios/simple_kick.py @@ -0,0 +1,48 @@ +from scenarios import * + +scenario = ( + scenarios.channel_join_with_two_users.scenario, + # demonstrate bug https://lab.louiz.org/louiz/biboumi/issues/3291 + # First user joins an other channel + send_stanza("<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"), + expect_stanza("/message[@type='groupchat']/subject"), + + # Second user joins + send_stanza("<presence from='{jid_two}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + expect_unordered( + ["/presence[@to='{jid_one}/{resource_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']"], + [ + "/presence[@to='{jid_two}/{resource_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ], + ["/presence[@to='{jid_two}/{resource_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']"], + ["/message/subject"] + ), + + # Moderator kicks participant + 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>"), + 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']", + "/presence/muc_user:x/muc_user:status[@code='307']", + "/presence/muc_user:x/muc_user:status[@code='110']" + ], + [ + "/presence[@type='unavailable'][@to='{jid_one}/{resource_one}']/muc_user:x/muc_user:item[@role='none']/muc_user:actor[@nick='{nick_one}']", + "/presence/muc_user:x/muc_user:item/muc_user:reason[text()='reported']", + "/presence/muc_user:x/muc_user:status[@code='307']", + ], + ["/iq[@id='kick1'][@type='result']"] + ), + + # Bug 3291, suite. We must not receive any presence from #foo, here + send_stanza("<message from='{jid_two}/{resource_one}' to='{irc_server_one}' type='chat'><body>QUIT bye bye</body></message>"), + expect_unordered( + ["/presence[@from='#bar%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}']"], + ["/presence[@from='#bar%{irc_server_one}/{nick_two}'][@to='{jid_two}/{resource_one}']"], + ["/message"], + ["/message"], + ), +) diff --git a/tests/end_to_end/scenarios/simple_mam.py b/tests/end_to_end/scenarios/simple_mam.py new file mode 100644 index 0000000..4509eeb --- /dev/null +++ b/tests/end_to_end/scenarios/simple_mam.py @@ -0,0 +1,60 @@ +from scenarios import * + +scenario = ( + scenarios.simple_channel_join.scenario, + + # Send two channel messages + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"), + 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]"), + + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 2</body></message>"), + 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 + 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>"), + + 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()='coucou']"), + 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()='coucou 2']"), + + expect_stanza("/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", + "/iq/mam:fin/rms:set/rsm:last", + "/iq/mam:fin/rsm:set/rsm:first", + "/iq/mam:fin[@complete='true']"), + + # Retrieve an empty archive by specifying an early “end” date + send_stanza("""<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id2'> + <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:2</value></field> + <field var='end'><value>2000-06-07T00:00:00Z</value></field> + </x> + </query></iq>"""), + + expect_stanza("/iq[@type='result'][@id='id2'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", + "/iq/mam:fin[@complete='true']/rsm:set"), + + # Retrieve an empty archive by specifying a late “start” date + # (note that this test will break in ~1000 years) + send_stanza("""<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id3'> + <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:2</value></field> + <field var='start'><value>3016-06-07T00:00:00Z</value></field> + </x> + </query></iq>"""), + + expect_stanza("/iq[@type='result'][@id='id3'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", + "/iq/mam:fin[@complete='true']/rsm:set"), + + # Retrieve the whole archive, but limit the response to one elemet + 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>"), + + 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']"), + + expect_stanza("/iq[@type='result'][@id='id4'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']", + "!/iq/mam:fin[@complete='true']/rsm:set"), +) diff --git a/tests/end_to_end/scenarios/slash_me_channel_message.py b/tests/end_to_end/scenarios/slash_me_channel_message.py new file mode 100644 index 0000000..d30fba3 --- /dev/null +++ b/tests/end_to_end/scenarios/slash_me_channel_message.py @@ -0,0 +1,18 @@ +from scenarios import * + +scenario = ( + scenarios.channel_join_with_two_users.scenario, + # Send a channel message + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>/me rit en IRC</body></message>"), + # Receive the message, forwarded to the two users + expect_unordered( + [ + "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='/me rit en IRC']", + "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]" + ], + [ + "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='/me rit en IRC']", + "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]" + ], + ), +) diff --git a/tests/end_to_end/scenarios/stable_id.py b/tests/end_to_end/scenarios/stable_id.py new file mode 100644 index 0000000..9f3181b --- /dev/null +++ b/tests/end_to_end/scenarios/stable_id.py @@ -0,0 +1,32 @@ +from scenarios import * + +import scenarios.simple_channel_join + +# see https://xmpp.org/extensions/xep-0359.html + +scenario = ( + scenarios.simple_channel_join.scenario, + + send_stanza("""<message id='first_id' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'> + <origin-id xmlns='urn:xmpp:sid:0' id='client-origin-id-should-be-kept'/> + <stanza-id xmlns='urn:xmpp:sid:0' id='client-stanza-id-should-be-removed' by='#foo%{irc_server_one}'/> + <stanza-id xmlns='urn:xmpp:sid:0' id='client-stanza-id-should-be-kept' by='someother@jid'/> + <body>coucou</body></message>"""), + + # Entities, which are routing stanzas, SHOULD NOT strip any elements + # qualified by the 'urn:xmpp:sid:0' namespace from message stanzas + # unless the preceding rule applied to those elements. + expect_stanza("/message/stable_id:origin-id[@id='client-origin-id-should-be-kept']", + # Stanza ID generating entities, which encounter a <stanza-id/> + # element where the 'by' attribute matches the 'by' attribute they + # would otherwise set, MUST delete that element even if they are not + # adding their own stanza ID. + "/message/stable_id:stanza-id[@id][@by='#foo%{irc_server_one}']", + "!/message/stable_id:stanza-id[@id='client-stanza-id-should-be-removed']", + # Entities, which are routing stanzas, SHOULD NOT strip + # any elements qualified by the 'urn:xmpp:sid:0' + # namespace from message stanzas unless the preceding + # rule applied to those elements. + "/message/stable_id:stanza-id[@id='client-stanza-id-should-be-kept'][@by='someother@jid']", + ), +) diff --git a/tests/end_to_end/sequences.py b/tests/end_to_end/sequences.py new file mode 100644 index 0000000..b545b1c --- /dev/null +++ b/tests/end_to_end/sequences.py @@ -0,0 +1,85 @@ +from functions import expect_stanza, send_stanza, common_replacements, expect_unordered + +def handshake(): + return ( + expect_stanza("//handshake"), + send_stanza("<handshake xmlns='jabber:component:accept'/>") + ) + +def connection_begin(irc_host, jid, expected_irc_presence=False, fixed_irc_server=False): + jid = jid.format_map(common_replacements) + if fixed_irc_server: + xpath = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[text()='%s']" + else: + xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']" + result = ( + expect_stanza(xpath % ('Connecting to %s:6697 (encrypted)' % irc_host), + "/message/hints:no-copy", + "/message/carbon:private" + ), + expect_stanza(xpath % 'Connection failed: Connection refused'), + expect_stanza(xpath % ('Connecting to %s:6670 (encrypted)' % irc_host)), + expect_stanza(xpath % 'Connection failed: Connection refused'), + expect_stanza(xpath % ('Connecting to %s:6667 (not encrypted)' % irc_host)), + expect_stanza(xpath % 'Connected to IRC server.')) + + if expected_irc_presence: + result += (expect_stanza("/presence[@from='" + irc_host + "@biboumi.localhost']"),) + + result += ( + expect_stanza("/message/body[text()='irc.localhost: ACK multi-prefix']"), + expect_stanza("/message/body[text()='irc.localhost: *** Looking up your hostname...']"), + expect_stanza("/message/body[text()='irc.localhost: *** Found your hostname']"), + ), + + return result + +def connection_tls_begin(irc_host, jid, fixed_irc_server): + jid = jid.format_map(common_replacements) + if fixed_irc_server: + xpath = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[text()='%s']" + else: + xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']" + irc_host = 'irc.localhost' + return ( + expect_stanza(xpath % ('Connecting to %s:7778 (encrypted)' % irc_host), + "/message/hints:no-copy", + "/message/carbon:private", + ), + expect_stanza(xpath % 'Connected to IRC server (encrypted).'), + expect_stanza("/message/body[text()='irc.localhost: ACK multi-prefix']"), + expect_stanza("/message/body[text()='irc.localhost: *** Looking up your hostname...']"), + expect_stanza("/message/body[text()='irc.localhost: *** Found your hostname']"), + ) + +def connection_end(irc_host, jid, fixed_irc_server=False): + jid = jid.format_map(common_replacements) + if fixed_irc_server: + xpath = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[text()='%s']" + xpath_re = "/message[@to='" + jid + "'][@from='biboumi.localhost']/body[re:test(text(), '%s')]" + else: + 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 ( + expect_stanza("/message/body[re:test(text(), '%s')]" % (r'^%s: Your host is %s, running version oragono-2\.0\.0(-[a-z0-9]+)? $' % (irc_host, irc_host))), + expect_stanza(xpath_re % (r'^%s: This server was created .*$' % irc_host)), + expect_stanza(xpath_re % (r'^%s: There are \d+ users and \d+ invisible on \d+ server\(s\)$' % irc_host)), + expect_stanza(xpath_re % ("%s: \d+ IRC Operators online" % irc_host,)), + expect_stanza(xpath_re % ("%s: \d+ unregistered connections" % irc_host,)), + expect_stanza(xpath_re % ("%s: \d+ channels formed" % irc_host,)), + expect_stanza(xpath_re % (r'^%s: I have \d+ clients and \d+ servers$' % irc_host)), + expect_stanza(xpath_re % (r'^%s: \d+ \d+ Current local users \d+, max \d+$' % irc_host)), + expect_stanza(xpath_re % (r'^%s: \d+ \d+ Current global users \d+, max \d+$' % irc_host)), + expect_stanza(xpath % "%s: MOTD File is missing: Unspecified error" % irc_host), + expect_stanza(xpath_re % (r'.+? \+Z',)), + ) + +def connection(irc_host="irc.localhost", jid="{jid_one}/{resource_one}", expected_irc_presence=False, fixed_irc_server=False): + return connection_begin(irc_host, jid, expected_irc_presence, fixed_irc_server=fixed_irc_server) + \ + connection_end(irc_host, jid, fixed_irc_server=fixed_irc_server) + +def connection_tls(irc_host="irc.localhost", jid="{jid_one}/{resource_one}", fixed_irc_server=False): + return connection_tls_begin(irc_host, jid, fixed_irc_server) + \ + connection_end(irc_host, jid, fixed_irc_server) + diff --git a/tests/jid.cpp b/tests/jid.cpp index 480827b..592d6f3 100644 --- a/tests/jid.cpp +++ b/tests/jid.cpp @@ -21,8 +21,8 @@ TEST_CASE("jidprep") { // Jidprep const std::string badjid("~zigougou™@EpiK-7D9D1FDE.poez.io/Boujour/coucou/slt™"); - const std::string correctjid = jidprep(badjid); #ifdef LIBIDN_FOUND + const std::string correctjid = jidprep(badjid); CHECK(correctjid == "~zigougoutm@epik-7d9d1fde.poez.io/Boujour/coucou/sltTM"); // Check that the cache does not break things when we prep the same string // multiple times diff --git a/unit/biboumi.service.cmake b/unit/biboumi.service.cmake index 150045b..bcbda1f 100644 --- a/unit/biboumi.service.cmake +++ b/unit/biboumi.service.cmake @@ -11,6 +11,7 @@ WatchdogSec=${WATCHDOG_SEC} Restart=always User=${SERVICE_USER} Group=${SERVICE_GROUP} +AmbientCapabilities=CAP_NET_BIND_SERVICE [Install] WantedBy=multi-user.target |