diff options
110 files changed, 1853 insertions, 1274 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 73bc720..db98e8a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -36,7 +36,7 @@ variables: extends: .sources_changed stage: build tags: - - docker + - container script: - "echo Running cmake with the following parameters: -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3} ${POSTGRESQL}" - mkdir build/ @@ -136,8 +136,11 @@ build:no_db_botan: - make check_junit || true; make check; - make e2e artifacts: - reports: - junit: ["build/check_result.xml"] + expire_in: 2 weeks + paths: + - build/ + name: $CI_PROJECT_NAME-test-$CI_JOB_ID + when: always test:debian: extends: .basic_test @@ -157,15 +160,6 @@ test:fedora: dependencies: - build:fedora needs: ["build:fedora"] - artifacts: - expire_in: 2 weeks - paths: - - build/coverage_test_suite/ - - build/coverage_e2e/ - - build/coverage_total/ - - build/coverage_e2e.info - when: always - name: $CI_PROJECT_NAME-test-$CI_JOB_ID test:no_udns: extends: .basic_test @@ -180,7 +174,6 @@ test:alpine: dependencies: - build:alpine needs: ["build:alpine"] - image: docker.louiz.org/louiz/biboumi/test-alpine:latest test:freebsd: extends: .basic_test @@ -202,9 +195,9 @@ packaging:rpm: - master@louiz/biboumi tags: - docker - allow_failure: true image: docker.louiz.org/louiz/biboumi/test-fedora:latest script: + - mkdir -p build/ - cd build/ - make rpm -j$(nproc || echo 1) artifacts: @@ -220,10 +213,8 @@ packaging:archlinux: stage: packaging only: - master@louiz/biboumi - - triggers tags: - docker - allow_failure: true image: docker.louiz.org/louiz/biboumi/packaging-archlinux:latest before_script: [] script: @@ -246,13 +237,16 @@ packaging:archlinux: - "doc/**/*" # The jobs with the secure tag need to access directories where important -# files are stored: the latest doc, etc.' +# 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 + image: docker.louiz.org/louiz/biboumi/test-fedora:latest + tags: + - www + - secure script: - cd doc/ - make html @@ -264,9 +258,6 @@ deploy:doc:latest: extends: .deploy:doc only: - master@louiz/biboumi - tags: - - www - - secure environment: name: doc.latest url: https://doc.biboumi.louiz.org @@ -277,9 +268,6 @@ 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/ diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 172bb7c..10b6eb4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,22 @@ -Version 9.0 -=========== +Version 10.0 +============ + +For users +--------- +- Direct messages are always sent to the user's bare JID. It’s now the job + of the XMPP server to forward these messages to all online resources. +- Private messages are now always received from the server-wide JID. You + can still use the in-room JID (#chan%irc@biboumi/NickName) to send a + private message but the response you will receive will come from + nickname%irc@biboumi. + +For admins +---------- +- Command line option --test-config (or -t) has been added. When used, + biboumi will just exit without any error if the configuration is correct + +Version 9.0 - 2020-09-22 +======================== For users --------- @@ -14,6 +31,9 @@ For users - 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. +- SASL support has been added. A new field in the Configure ad-hoc command + lets you set a password that will be used to authenticate to the nick + service. This replaces the cumbersome and imperfect NickServ method. For admins ---------- @@ -31,6 +51,17 @@ For developers 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 +======================== + +- Fix a build failure with GCC 10 + +Version 8.4 - 2020-02-25 +======================== + +- Fix a possible crash that could be caused by a very well timed identd + query + Version 8.3 - 2018-06-01 ======================== diff --git a/CMakeLists.txt b/CMakeLists.txt index ac86555..f07b97f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.0) project(biboumi) -set(${PROJECT_NAME}_VERSION_MAJOR 9) +set(${PROJECT_NAME}_VERSION_MAJOR 10) set(${PROJECT_NAME}_VERSION_MINOR 0) set(${PROJECT_NAME}_VERSION_SUFFIX "~dev") diff --git a/conf/irc.ppirc.net.policy.txt b/conf/irc.ppirc.net.policy.txt deleted file mode 100644 index 43a7d80..0000000 --- a/conf/irc.ppirc.net.policy.txt +++ /dev/null @@ -1,2 +0,0 @@ -key_exchange_methods = RSA -signature_methods = IMPLICIT diff --git a/doc/admin.rst b/doc/admin.rst index a5850a7..ec80112 100644 --- a/doc/admin.rst +++ b/doc/admin.rst @@ -205,7 +205,7 @@ 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 +negotiating a TLS session. By default this value is unset and biboumi tries a list of well-known paths. outgoing_bind @@ -242,7 +242,7 @@ policy_directory ~~~~~~~~~~~~~~~~ A directory that should contain the policy files, used to customize -Botan’s behaviour when negociating the TLS connections with the IRC +Botan’s behaviour when negotiating 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 diff --git a/doc/conf.py b/doc/conf.py index c607fc5..504c15b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -24,9 +24,9 @@ copyright = '2018, Florent Le Coz' author = 'Florent Le Coz' # The short X.Y version -version = '8.3' +version = '8.4' # The full version, including alpha/beta/rc tags -release = '8.3' +release = '8.4' # -- General configuration --------------------------------------------------- diff --git a/doc/synopsis.rst b/doc/synopsis.rst index 5b93a92..2b28c74 100644 --- a/doc/synopsis.rst +++ b/doc/synopsis.rst @@ -1,4 +1,19 @@ Synopsis ======== -biboumi [*config_filename*] +biboumi [-ht] [*config_filename*] + +Command-Line Options +======== + +-h, \\-\\-help +~~~~~~~~ + +Display a help message and exit. + +-t, \\-\\-test-config +~~~~~~~~ + +Do not run, just test the configuration file syntax. Exit with a 0 +status if the configuration is valid, exits with a non-zero status +otherwise. diff --git a/doc/user.rst b/doc/user.rst index 505e3b9..add3480 100644 --- a/doc/user.rst +++ b/doc/user.rst @@ -205,6 +205,27 @@ 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. +Authentication +-------------- + +There are multiple different ways to authenticate to an IRC service. The +most commonly used is to send some command with your password to some +special user on the server, often called NickServ. This can be done +manually by talking to this user in private and sending the appropriate +messages, or this can be done automatically using the `After-connection +IRC commands`_. The biggest issue with this method is that you need to +first be connected and logged (nick and username selected) to the server +before you can start this authentication method, and this often creates a +race condition if you need to be authenticated before joining a channel. + +A new method has been introduced to improve this: SASL authentication. You +just need to configure your password into the “Sasl password” field of the +IRC server, and biboumi will automatically authenticate when you connect +to that server. If the authentication fails, the connection to the server +is aborted. To fix this, check the error message and fix your nick and/or +password, or remove your password entirely (empty that field) if you don’t +want to use SASL at all. + Private messages ---------------- @@ -439,6 +460,10 @@ server. The provided configuration form contains these fields: - **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. +- **SASL Password**: The password to authenticate with your nickname, on + that server. Authentication will be tried with the nick that is used when + connecting to the server. This is the Nickname_ field if it is set, otherwise + it’s simply the nickname specified in the first room you join. - **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 @@ -448,9 +473,10 @@ server. The provided configuration form contains these fields: - **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 + this is NOT a password that will be sent to NickServ (or some other authentication service), some server (notably Freenode) use it as if it - was sent to NickServ to identify your nickname. + was sent to NickServ to identify your nickname. See SASL password if you + need to authenticate. - **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. diff --git a/docker/biboumi-test/alpine/Dockerfile b/docker/biboumi-test/alpine/Dockerfile deleted file mode 100644 index 9d59c32..0000000 --- a/docker/biboumi-test/alpine/Dockerfile +++ /dev/null @@ -1,56 +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\ - libffi-dev\ - 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 - -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/debian/Dockerfile b/docker/biboumi-test/debian/Dockerfile deleted file mode 100644 index 1c6437c..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:buster - -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\ - python3-sphinx\ - libasan5\ - 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 && 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 61fa3be..0000000 --- a/docker/biboumi-test/fedora/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/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\ - python3-sphinx\ - 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\ - botan2-devel\ - && 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 - -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 89c7223..9ceb1e2 100644 --- a/docker/biboumi/alpine/Dockerfile +++ b/docker/biboumi/alpine/Dockerfile @@ -8,15 +8,10 @@ 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 + make postgresql-dev python2 sqlite-dev udns-dev util-linux-dev botan-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 && \ +RUN git clone https://lab.louiz.org/louiz/biboumi && \ mkdir ./biboumi/build && \ cd ./biboumi/build && \ cmake .. -DCMAKE_INSTALL_PREFIX=/usr \ @@ -33,11 +28,7 @@ RUN git clone git://git.louiz.org/biboumi && \ FROM docker.io/alpine:latest 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 + sqlite-libs udns expat ca-certificates botan COPY --from=builder /etc/biboumi /etc/biboumi COPY --from=builder /usr/bin/biboumi /usr/bin/biboumi diff --git a/docker/packaging/archlinux/Dockerfile b/docker/packaging/archlinux/Dockerfile index 20f0343..5bad3c4 100644 --- a/docker/packaging/archlinux/Dockerfile +++ b/docker/packaging/archlinux/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/base/archlinux:latest +FROM docker.io/archlinux:latest RUN pacman -Syuuuu --noconfirm diff --git a/docker/test/alpine/Dockerfile b/docker/test/alpine/Dockerfile new file mode 100644 index 0000000..60f7499 --- /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 https://lab.louiz.org/poezio/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..d1254c5 --- /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 https://lab.louiz.org/poezio/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..cd41741 --- /dev/null +++ b/docker/test/fedora/Dockerfile @@ -0,0 +1,41 @@ +# 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 \ +python-sphinx_rtd_theme \ +wget \ +fedora-packager \ +rpmdevtools \ +&& dnf clean all + +# Install slixmpp, for e2e tests +RUN git clone https://lab.louiz.org/poezio/slixmpp && pip3 install pyasn1 && cd slixmpp && python3 setup.py build && python3 setup.py install + +# 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/packaging/biboumi.spec.cmake b/packaging/biboumi.spec.cmake index 4114b7e..1352505 100644 --- a/packaging/biboumi.spec.cmake +++ b/packaging/biboumi.spec.cmake @@ -64,12 +64,19 @@ make check %{?_smp_mflags} %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 +-* ${RPM_DATE} Le Coz Florent <louiz@louiz.org> - ${RPM_VERSION}-1 +-- Build latest git revision -* Fri Jun 1 2018 Le Coz Florent <louiz@louiz.org> - 8.3-1 +* Tue Sep 22 2020 Le Coz Florent <louiz@louiz.org> - 9.0-1 + Update to version 9.0 + +* Sat May 9 2020 Le Coz Florent <louiz@louiz.org> - 8.5-1 + Update to version 8.5 + +* Tue Feb 25 2020 Le Coz Florent <louiz@louiz.org> - 8.4-1 + Update to version 8.4 + +* Wed Jun 1 2018 Le Coz Florent <louiz@louiz.org> - 8.3-1 Update to version 8.3 * Fri May 25 2018 Le Coz Florent <louiz@louiz.org> - 8.2-1 diff --git a/src/biboumi.h.cmake b/src/biboumi.h.cmake index fa99cd4..29a0510 100644 --- a/src/biboumi.h.cmake +++ b/src/biboumi.h.cmake @@ -14,3 +14,6 @@ #cmakedefine HAS_PUT_TIME #cmakedefine DEBUG_SQL_QUERIES +#if defined(USE_DATABASE) && defined(BOTAN_FOUND) +# define WITH_SASL +#endif diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index 71c0ea4..e7f334f 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -170,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 @@ -185,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()) { @@ -234,15 +233,21 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: if (!first || id.empty()) id = utils::gen_uuid(); - MessageCallback mirror_to_all_resources = [this, iid, uuid, id](const IrcClient* irc, const IrcMessage& message) { + 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()]) - this->xmpp.send_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(line), - this->user_jid + "/" + resource, uuid, id); + { + 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") @@ -448,9 +453,6 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con } if (persistent) this->remove_resource_from_chan(key, resource); - // Since there are no resources left in that channel, we don't - // want to receive private messages using this room's JID - this->remove_all_preferred_from_jid_of_room(iid.get_local()); } else { @@ -859,27 +861,18 @@ void Bridge::send_message(const Iid& iid, const std::string& nick, const std::st #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 { - const auto it = this->preferred_user_from.find(iid.get_local()); - if (it != this->preferred_user_from.end()) - { - const auto chan_name = Iid(Jid(it->second).local, {}).get_local(); - for (const auto& resource: this->resources_in_chan[ChannelKey{chan_name, iid.get_server()}]) - this->xmpp.send_message(it->second, this->make_xmpp_body(body, encoding), - this->user_jid + "/" - + resource, "chat", true, true, true); - } - else - { - for (const auto& resource: this->resources_in_server[iid.get_server()]) - this->xmpp.send_message(std::to_string(iid), this->make_xmpp_body(body, encoding), - this->user_jid + "/" + resource, "chat", false, true); - } + this->xmpp.send_message(std::to_string(iid), + this->make_xmpp_body(body, encoding), + this->user_jid, + "chat", false, false); } } @@ -1136,34 +1129,6 @@ void Bridge::on_irc_client_disconnected(const std::string& hostname) this->xmpp.on_irc_client_disconnected(hostname, this->user_jid); } -void Bridge::set_preferred_from_jid(const std::string& nick, const std::string& full_jid) -{ - auto it = this->preferred_user_from.find(nick); - if (it == this->preferred_user_from.end()) - this->preferred_user_from.emplace(nick, full_jid); - else - this->preferred_user_from[nick] = full_jid; -} - -void Bridge::remove_preferred_from_jid(const std::string& nick) -{ - auto it = this->preferred_user_from.find(nick); - if (it != this->preferred_user_from.end()) - this->preferred_user_from.erase(it); -} - -void Bridge::remove_all_preferred_from_jid_of_room(const std::string& channel_name) -{ - for (auto it = this->preferred_user_from.begin(); it != this->preferred_user_from.end();) - { - Iid iid(Jid(it->second).local, {}); - if (iid.get_local() == channel_name) - it = this->preferred_user_from.erase(it); - else - ++it; - } -} - void Bridge::add_waiting_irc(irc_responder_callback_t&& callback) { this->waiting_irc.emplace_back(std::move(callback)); diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp index fa2a31f..a7aef3d 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); @@ -219,19 +217,6 @@ public: */ size_t active_clients() const; /** - * Add (or replace the existing) <nick, jid> into the preferred_user_from map - */ - void set_preferred_from_jid(const std::string& nick, const std::string& full_jid); - /** - * Remove the preferred jid for the given IRC nick - */ - void remove_preferred_from_jid(const std::string& nick); - /** - * Given a channel_name, remove all preferred from_jid that come - * from this chan. - */ - void remove_all_preferred_from_jid_of_room(const std::string& channel_name); - /** * Add a callback to the waiting list of irc callbacks. */ void add_waiting_irc(irc_responder_callback_t&& callback); @@ -286,14 +271,6 @@ private: */ std::shared_ptr<Poller> poller; /** - * A map of <nick, full_jid>. For example if this map contains <"toto", - * "#somechan%server@biboumi/ToTo">, whenever a private message is - * received from the user "toto", instead of forwarding it to XMPP with - * from='toto!server@biboumi', we use instead - * from='#somechan%server@biboumi/ToTo' - */ - std::unordered_map<std::string, std::string> preferred_user_from; - /** * A list of callbacks that are waiting for some IrcMessage to trigger a * response. We add callbacks in this list whenever we received an IQ * request and we need a response from IRC to be able to provide the diff --git a/src/database/database.hpp b/src/database/database.hpp index a53f87b..de1df49 100644 --- a/src/database/database.hpp +++ b/src/database/database.hpp @@ -43,6 +43,8 @@ class Database struct Nick: Column<std::string> { static constexpr auto name = "nick_"; }; + struct SaslPassword: Column<std::string> { static constexpr auto name = "saslpassword_"; }; + struct Pass: Column<std::string> { static constexpr auto name = "pass_"; }; struct Ports: Column<std::string> { static constexpr auto name = "ports_"; @@ -95,7 +97,7 @@ class Database 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, ThrottleLimit>; + using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength, Address, Nick, SaslPassword, ThrottleLimit>; using IrcServerOptions = IrcServerOptionsTable::RowType; using IrcChannelOptionsTable = Table<Id, Owner, Server, Channel, EncodingOut, EncodingIn, MaxHistoryLength, Persistent, RecordHistoryOptional>; diff --git a/src/identd/identd_server.hpp b/src/identd/identd_server.hpp index b1c8ec8..55fe225 100644 --- a/src/identd/identd_server.hpp +++ b/src/identd/identd_server.hpp @@ -24,6 +24,7 @@ class IdentdServer: public TcpSocketServer<IdentdSocket> if (this->poller->is_managing_socket(this->socket)) this->poller->remove_socket_handler(this->socket); ::close(this->socket); + this->sockets.clear(); } void clean() { diff --git a/src/identd/identd_socket.cpp b/src/identd/identd_socket.cpp index 92cd80b..7688bbe 100644 --- a/src/identd/identd_socket.cpp +++ b/src/identd/identd_socket.cpp @@ -25,10 +25,12 @@ void IdentdSocket::parse_in_buffer(const std::size_t) std::istringstream line(this->in_buf.substr(0, line_end)); this->consume_in_buffer(line_end + 1); - uint16_t local_port; - uint16_t remote_port; + uint16_t local_port{}; + uint16_t remote_port{}; char sep; line >> local_port >> sep >> remote_port; + if (line.fail()) // Data did not match the expected format, ignore the line entirely + continue; const auto& xmpp = this->server.get_biboumi_component(); auto response = this->generate_answer(xmpp, local_port, remote_port); @@ -47,7 +49,7 @@ std::string IdentdSocket::generate_answer(const BiboumiComponent& biboumi, uint1 { for (const auto& pair: bridge->get_irc_clients()) { - if (pair.second->match_port_pairt(local, remote)) + if (pair.second->match_port_pair(local, remote)) { std::ostringstream os; os << local << " , " << remote << " : USERID : OTHER : " << hash_jid(bridge->get_bare_jid()) << "\r\n"; diff --git a/src/irc/capability.hpp b/src/irc/capability.hpp new file mode 100644 index 0000000..55ccb0c --- /dev/null +++ b/src/irc/capability.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include <functional> + +struct Capability +{ + std::function<void()> on_ack; + std::function<void()> on_nack; +}; diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp index de38d42..8a64727 100644 --- a/src/irc/irc_client.cpp +++ b/src/irc/irc_client.cpp @@ -5,6 +5,7 @@ #include <irc/irc_client.hpp> #include <bridge/bridge.hpp> #include <irc/irc_user.hpp> +#include <utils/base64.hpp> #include <logger/logger.hpp> #include <config/config.hpp> @@ -81,7 +82,18 @@ static const std::unordered_map<std::string, {"PONG", {&IrcClient::on_pong, {0, 0}}}, {"KICK", {&IrcClient::on_kick, {3, 0}}}, {"INVITE", {&IrcClient::on_invite, {2, 0}}}, - + {"CAP", {&IrcClient::on_cap, {3, 0}}}, +#ifdef WITH_SASL + {"AUTHENTICATE", {&IrcClient::on_authenticate, {1, 0}}}, + {"900", {&IrcClient::on_sasl_login, {3, 0}}}, + {"902", {&IrcClient::on_sasl_failure, {2, 0}}}, + {"903", {&IrcClient::on_sasl_success, {0, 0}}}, + {"904", {&IrcClient::on_sasl_failure, {2, 0}}}, + {"905", {&IrcClient::on_sasl_failure, {2, 0}}}, + {"906", {&IrcClient::on_sasl_failure, {2, 0}}}, + {"907", {&IrcClient::on_sasl_failure, {2, 0}}}, + {"908", {&IrcClient::on_sasl_failure, {2, 0}}}, +#endif {"401", {&IrcClient::on_generic_error, {2, 0}}}, {"402", {&IrcClient::on_generic_error, {2, 0}}}, {"403", {&IrcClient::on_generic_error, {2, 0}}}, @@ -232,6 +244,7 @@ void IrcClient::on_connection_failed(const std::string& reason) "cancel", "item-not-found", "", reason); } + this->channels_to_join.clear(); } else // try the next port this->start(); @@ -272,18 +285,45 @@ void IrcClient::on_connected() } } - this->send_message({"CAP", {"REQ", "multi-prefix"}}); - this->send_message({"CAP", {"END"}}); + this->send_gateway_message("Connected to IRC server"s + (this->use_tls ? " (encrypted)": "") + "."); + + this->capabilities["multi-prefix"] = {[]{}, []{}}; #ifdef USE_DATABASE auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), this->get_hostname()); - if (!options.col<Database::Pass>().empty()) + + const auto& server_password = options.col<Database::Pass>(); + + if (!server_password.empty()) this->send_pass_command(options.col<Database::Pass>()); #endif - this->send_nick_command(this->current_nick); +#ifdef WITH_SASL + const auto& sasl_password = options.col<Database::SaslPassword>(); + if (!sasl_password.empty()) + { + this->capabilities["sasl"] = { + [this] + { + this->send_message({"AUTHENTICATE", {"PLAIN"}}); + log_warning("negotiating SASL now..."); + }, + [] + { + log_warning("SASL not supported by the server, disconnecting."); + } + }; + this->sasl_state = SaslState::needed; + } +#endif + { + for (const auto &pair : this->capabilities) + this->send_message({ "CAP", {"REQ", pair.first}}); + } + + this->send_nick_command(this->current_nick); #ifdef USE_DATABASE if (Config::get("realname_customization", "true") == "true") { @@ -294,13 +334,8 @@ void IrcClient::on_connected() this->send_user_command(username, realname); } else - this->send_user_command(this->username, this->realname); -#else - this->send_user_command(this->username, this->realname); #endif - this->send_gateway_message("Connected to IRC server"s + (this->use_tls ? " (encrypted)": "") + "."); - this->send_pending_data(); - this->bridge.on_irc_client_connected(this->get_hostname()); + this->send_user_command(this->username, this->realname); } void IrcClient::on_connection_close(const std::string& error_msg) @@ -371,12 +406,14 @@ void IrcClient::parse_in_buffer(const size_t) { const auto& limits = it->second.second; // Check that the Message is well formed before actually calling - // the callback. limits.first is the min number of arguments, - // second is the max - if (message.arguments.size() < limits.first || - (limits.second > 0 && message.arguments.size() > limits.second)) + // the callback. + const auto args_size = message.arguments.size(); + const auto min = limits.first; + const auto max = limits.second; + if (args_size < min || + (max > 0 && args_size > max)) log_warning("Invalid number of arguments for IRC command “", message.command, - "”: ", message.arguments.size()); + "”: ", args_size); else { const auto& cb = it->second.first; @@ -1266,7 +1303,7 @@ void IrcClient::on_unknown_message(const IrcMessage& message) return ; std::string from = message.prefix; std::stringstream ss; - for (auto it = message.arguments.begin() + 1; it != message.arguments.end(); ++it) + for (auto it = std::next(message.arguments.begin()); it != message.arguments.end(); ++it) { ss << *it; if (it + 1 != message.arguments.end()) @@ -1299,3 +1336,86 @@ long int IrcClient::get_throttle_limit() const return 10; #endif } + +void IrcClient::on_cap(const IrcMessage &message) +{ + const auto& sub_command = message.arguments[1]; + const auto& caps = utils::split(message.arguments[2], ' ', false); + for (const auto& cap: caps) + { + auto it = this->capabilities.find(cap); + if (it == this->capabilities.end()) + { + log_warning("Received a CAP message for something we didn’t ask, or that we already handled: [", cap, "]"); + return; + } + Capability& capability = it->second; + if (sub_command == "ACK") + capability.on_ack(); + else if (sub_command == "NACK") + capability.on_nack(); + this->capabilities.erase(it); + } + if (this->capabilities.empty()) + this->cap_end(); +} + +#ifdef WITH_SASL +void IrcClient::on_authenticate(const IrcMessage &) +{ + if (this->sasl_state == SaslState::unneeded) + { + log_warning("Received an AUTHENTICATE command but we don’t intend to authenticate…"); + return; + } + + auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), + this->get_hostname()); + const auto& nick = !options.col<Database::Nick>().empty() ? options.col<Database::Nick>() : this->get_own_nick(); + const auto auth_string = '\0' + nick + '\0' + options.col<Database::SaslPassword>(); + const auto base64_auth_string = base64::encode(auth_string); + this->send_message({"AUTHENTICATE", {base64_auth_string}}); +} + +void IrcClient::on_sasl_success(const IrcMessage &) +{ + this->sasl_state = SaslState::success; + this->cap_end(); +} + +void IrcClient::on_sasl_failure(const IrcMessage& message) +{ + this->sasl_state = SaslState::failure; + const auto reason = message.arguments[1]; + // Send an error message for all room that the user wanted to join + for (const auto& tuple: this->channels_to_join) + { + Iid iid(std::get<0>(tuple) + "%" + this->hostname, this->chantypes); + this->bridge.send_presence_error(iid, this->current_nick, + "cancel", "item-not-found", + "", reason); + } + this->channels_to_join.clear(); + this->send_quit_command(reason); +} + +void IrcClient::on_sasl_login(const IrcMessage &message) +{ + const auto& login = message.arguments[2]; + std::string text = "Your are now logged in as " + login; + if (message.arguments.size() > 3) + text = message.arguments[3]; + this->bridge.send_xmpp_message(this->hostname, message.prefix, text); +} +#endif + +void IrcClient::cap_end() +{ +#ifdef WITH_SASL + // If we are currently authenticating through sasl, finish that before sending CAP END + if (this->sasl_state == SaslState::needed) + return; +#endif + this->send_message({"CAP", {"END"}}); + this->bridge.on_irc_client_connected(this->get_hostname()); +} diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp index cfb3d21..3423228 100644 --- a/src/irc/irc_client.hpp +++ b/src/irc/irc_client.hpp @@ -1,8 +1,14 @@ #pragma once - #include <irc/irc_message.hpp> #include <irc/irc_channel.hpp> +#include <irc/capability.hpp> + +#include "biboumi.h" + +#ifdef WITH_SASL +# include <irc/sasl.hpp> +#endif #include <irc/iid.hpp> #include <bridge/history_limit.hpp> @@ -232,6 +238,20 @@ public: */ void on_invited(const IrcMessage& message); /** + * The IRC server sends a CAP message, as part of capabilities negotiation. It could be a ACK, + * NACK, or something else + */ + void on_cap(const IrcMessage& message); +private: + void cap_end(); +public: +#ifdef WITH_SASL + void on_authenticate(const IrcMessage& message); + void on_sasl_login(const IrcMessage& message); + void on_sasl_success(const IrcMessage& message); + void on_sasl_failure(const IrcMessage& message); +#endif + /** * The channel has been completely joined (self presence, topic, all names * received etc), send the self presence and topic to the XMPP user. */ @@ -359,6 +379,14 @@ private: * has been established, we are authentified and we have a nick) */ bool welcomed; +#ifdef WITH_SASL + /** + * Whether or not we are trying to authenticate using sasl. If this is true we need to wait for a + * successful auth + */ + SaslState sasl_state{SaslState::unneeded}; +#endif + std::map<std::string, Capability> capabilities; /** * See http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt section 3.3 * We store the possible chanmodes in this object. diff --git a/src/irc/irc_message.cpp b/src/irc/irc_message.cpp index 14fdb0e..62fe9a7 100644 --- a/src/irc/irc_message.cpp +++ b/src/irc/irc_message.cpp @@ -1,33 +1,30 @@ #include <irc/irc_message.hpp> #include <iostream> -IrcMessage::IrcMessage(std::string&& line) +IrcMessage::IrcMessage(std::stringstream ss) { - std::string::size_type pos; - - // optional prefix - if (line[0] == ':') + if (ss.peek() == ':') { - pos = line.find(' '); - this->prefix = line.substr(1, pos - 1); - line = line.substr(pos + 1, std::string::npos); + ss.ignore(); + ss >> this->prefix; } - // command - pos = line.find(' '); - this->command = line.substr(0, pos); - line = line.substr(pos + 1, std::string::npos); - // arguments - do + ss >> this->command; + while (ss >> std::ws) { - if (line[0] == ':') + std::string arg; + if (ss.peek() == ':') + { + ss.ignore(); + std::getline(ss, arg); + } + else { - this->arguments.emplace_back(line.substr(1, std::string::npos)); - break ; + ss >> arg; + if (arg.empty()) + break; } - pos = line.find(' '); - this->arguments.emplace_back(line.substr(0, pos)); - line = line.substr(pos + 1, std::string::npos); - } while (pos != std::string::npos); + this->arguments.push_back(std::move(arg)); + } } IrcMessage::IrcMessage(std::string&& prefix, diff --git a/src/irc/irc_message.hpp b/src/irc/irc_message.hpp index 269a12a..5475fd2 100644 --- a/src/irc/irc_message.hpp +++ b/src/irc/irc_message.hpp @@ -4,11 +4,13 @@ #include <vector> #include <string> #include <ostream> +#include <sstream> class IrcMessage { public: - IrcMessage(std::string&& line); + IrcMessage(std::stringstream ss); + IrcMessage(std::string str): IrcMessage{std::stringstream{str}} {} IrcMessage(std::string&& prefix, std::string&& command, std::vector<std::string>&& args); IrcMessage(std::string&& command, std::vector<std::string>&& args); ~IrcMessage() = default; diff --git a/src/irc/sasl.hpp b/src/irc/sasl.hpp new file mode 100644 index 0000000..775bc3f --- /dev/null +++ b/src/irc/sasl.hpp @@ -0,0 +1,9 @@ +#pragma once + +enum class SaslState +{ + unneeded, + needed, + failure, + success, +}; diff --git a/src/main.cpp b/src/main.cpp index 2448197..20bf9fd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -38,7 +38,7 @@ int config_help(const std::string& missing_option) int display_help() { - std::cout << "Usage: biboumi [configuration_file]" << std::endl; + std::cout << "Usage: biboumi [-ht] [configuration_file]" << std::endl; return 0; } @@ -194,13 +194,19 @@ static int main_loop(std::string hostname, std::string password) int main(int ac, char** av) { + std::string conf_filename{}; + bool test_conf = false; if (ac > 1) { - const std::string arg = av[1]; - if (arg.size() >= 2 && arg[0] == '-' && arg[1] == '-') + for (int i = 1; i < ac; i++) { - if (arg == "--help") + const std::string arg = av[i]; + if ((arg == "-h") || (arg == "--help")) return display_help(); + else if ((arg == "-t") || (arg == "--test-config")) + test_conf = true; + else if (i + 1 == ac) + conf_filename = arg; else { std::cerr << "Unknow command line option: " << arg @@ -209,8 +215,8 @@ int main(int ac, char** av) } } } - const std::string conf_filename = - ac > 1 ? av[1]: xdg_config_path("biboumi.cfg"); + if (conf_filename.empty()) + conf_filename = xdg_config_path("biboumi.cfg"); std::cout << "Using configuration file: " << conf_filename << std::endl; if (!Config::read_conf(conf_filename)) @@ -222,6 +228,12 @@ int main(int ac, char** av) const std::string hostname = Config::get("hostname", ""); if (hostname.empty()) return config_help("hostname"); + if (test_conf) + { + std::cout << "biboumi: the configuration file " << conf_filename + << " syntax is ok" << std::endl; + return 0; + } #ifdef USE_DATABASE try diff --git a/src/network/credentials_manager.cpp b/src/network/credentials_manager.cpp index 89c694c..47f8514 100644 --- a/src/network/credentials_manager.cpp +++ b/src/network/credentials_manager.cpp @@ -102,7 +102,7 @@ void BasicCredentialsManager::load_certs() if (BasicCredentialsManager::try_to_open_one_ca_bundle(paths)) BasicCredentialsManager::certs_loaded = true; else - log_warning("The CA could not be loaded, TLS negociation will probably fail."); + log_warning("The CA could not be loaded, TLS negotiation will probably fail."); } std::vector<Botan::Certificate_Store*> BasicCredentialsManager::trusted_certificate_authorities(const std::string&, const std::string&) diff --git a/src/network/dns_handler.cpp b/src/network/dns_handler.cpp index 7f0c96a..dd6f201 100644 --- a/src/network/dns_handler.cpp +++ b/src/network/dns_handler.cpp @@ -9,6 +9,7 @@ #include <udns.h> #include <cerrno> +#include <stdexcept> #include <cstring> class Resolver; diff --git a/src/network/tcp_client_socket_handler.cpp b/src/network/tcp_client_socket_handler.cpp index 6f67f02..7d1029f 100644 --- a/src/network/tcp_client_socket_handler.cpp +++ b/src/network/tcp_client_socket_handler.cpp @@ -260,8 +260,10 @@ std::string TCPClientSocketHandler::get_port() const return this->port; } -bool TCPClientSocketHandler::match_port_pairt(const uint16_t local, const uint16_t remote) const +bool TCPClientSocketHandler::match_port_pair(const uint16_t local, const uint16_t remote) const { + if (!this->is_connected()) + return false; const auto remote_port = static_cast<uint16_t>(std::stoi(this->port)); - return this->is_connected() && local == this->local_port && remote == remote_port; + return local == this->local_port && remote == remote_port; } diff --git a/src/network/tcp_client_socket_handler.hpp b/src/network/tcp_client_socket_handler.hpp index 74caca9..38d8b84 100644 --- a/src/network/tcp_client_socket_handler.hpp +++ b/src/network/tcp_client_socket_handler.hpp @@ -34,7 +34,7 @@ class TCPClientSocketHandler: public TCPSocketHandler /** * Whether or not this connection is using the two given TCP ports. */ - bool match_port_pairt(const uint16_t local, const uint16_t remote) const; + bool match_port_pair(const uint16_t local, const uint16_t remote) const; protected: bool hostname_resolution_failed; diff --git a/src/network/tcp_socket_handler.cpp b/src/network/tcp_socket_handler.cpp index e05caad..5e4ae30 100644 --- a/src/network/tcp_socket_handler.cpp +++ b/src/network/tcp_socket_handler.cpp @@ -194,7 +194,7 @@ void TCPSocketHandler::send_data(std::string&& data) if (this->use_tls) try { this->tls_send(std::move(data)); - } catch (const Botan::TLS::TLS_Exception& e) { + } catch (const Botan::Exception& e) { this->on_connection_close("TLS error: "s + e.what()); this->close(); return ; @@ -261,7 +261,7 @@ void TCPSocketHandler::tls_recv() const bool was_active = this->tls->is_active(); try { this->tls->received_data(recv_buf, static_cast<size_t>(size)); - } catch (const Botan::TLS::TLS_Exception& e) { + } catch (const Botan::Exception& e) { // May happen if the server sends malformed TLS data (buggy server, // or more probably we are just connected to a server that sends // plain-text) @@ -277,7 +277,7 @@ void TCPSocketHandler::tls_recv() void TCPSocketHandler::tls_send(std::string&& data) { // We may not be connected yet, or the tls session has - // not yet been negociated + // not yet been negotiated if (this->tls && this->tls->is_active()) { const bool was_active = this->tls->is_active(); diff --git a/src/network/tcp_socket_handler.hpp b/src/network/tcp_socket_handler.hpp index c598641..b12ae57 100644 --- a/src/network/tcp_socket_handler.hpp +++ b/src/network/tcp_socket_handler.hpp @@ -212,7 +212,7 @@ private: * calls the output_fn callback with it as soon as it is created. * Therefore, we do not want to create it if we do not intend to send any * TLS-encrypted message. We create the object only when needed (for - * example after we have negociated a TLS session using a STARTTLS + * example after we have negotiated a TLS session using a STARTTLS * message, or stuf like that). * * See start_tls for the method where this object is created. diff --git a/src/utils/base64.cpp b/src/utils/base64.cpp new file mode 100644 index 0000000..c0959bb --- /dev/null +++ b/src/utils/base64.cpp @@ -0,0 +1,16 @@ +#include <utils/base64.hpp> + +#ifdef BOTAN_FOUND +#include <botan/base64.h> + +namespace base64 +{ + +std::string encode(const std::string &input) +{ + return Botan::base64_encode(reinterpret_cast<const uint8_t*>(input.data()), input.size()); +} + +} + +#endif diff --git a/src/utils/base64.hpp b/src/utils/base64.hpp new file mode 100644 index 0000000..7c08d82 --- /dev/null +++ b/src/utils/base64.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "biboumi.h" + +#ifdef BOTAN_FOUND + +#include <string> + +namespace base64 +{ +std::string encode(const std::string& input); +} + +#endif diff --git a/src/utils/empty_if_fixed_server.hpp b/src/utils/empty_if_fixed_server.hpp index 9ccf5fd..2422ee4 100644 --- a/src/utils/empty_if_fixed_server.hpp +++ b/src/utils/empty_if_fixed_server.hpp @@ -7,17 +7,11 @@ namespace utils { - inline std::string empty_if_fixed_server(std::string&& str) + inline const std::string& empty_if_fixed_server(const std::string& str) { + static const std::string empty{}; if (!Config::get("fixed_irc_server", "").empty()) - return {}; - return str; - } - - inline std::string empty_if_fixed_server(const std::string& str) - { - if (!Config::get("fixed_irc_server", "").empty()) - return {}; + return empty; return str; } diff --git a/src/utils/get_first_non_empty.hpp b/src/utils/get_first_non_empty.hpp index 1877ee8..6129b63 100644 --- a/src/utils/get_first_non_empty.hpp +++ b/src/utils/get_first_non_empty.hpp @@ -11,13 +11,13 @@ template <> bool is_empty(const std::string& val); template <typename T> -T get_first_non_empty(T&& last) +T& get_first_non_empty(T&& last) { return last; } template <typename T, typename... Args> -T get_first_non_empty(T&& first, Args&&... args) +T& get_first_non_empty(T&& first, Args&&... args) { if (!is_empty(first)) return first; diff --git a/src/utils/optional_bool.hpp b/src/utils/optional_bool.hpp index 3d00d23..ebd063d 100644 --- a/src/utils/optional_bool.hpp +++ b/src/utils/optional_bool.hpp @@ -1,5 +1,6 @@ #pragma once +#include <ostream> #include <string> struct OptionalBool diff --git a/src/xmpp/adhoc_command.cpp b/src/xmpp/adhoc_command.cpp index fbf4ce2..f8c8e4f 100644 --- a/src/xmpp/adhoc_command.cpp +++ b/src/xmpp/adhoc_command.cpp @@ -26,7 +26,7 @@ void PingStep1(XmppComponent&, AdhocSession&, XmlNode& command_node) void HelloStep1(XmppComponent&, AdhocSession&, XmlNode& command_node) { - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Configure your name."); @@ -65,9 +65,9 @@ void HelloStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node) } } command_node.delete_all_children(); - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); session.terminate(); } diff --git a/src/xmpp/adhoc_commands_handler.cpp b/src/xmpp/adhoc_commands_handler.cpp index ff4c1e5..7a84b2e 100644 --- a/src/xmpp/adhoc_commands_handler.cpp +++ b/src/xmpp/adhoc_commands_handler.cpp @@ -36,16 +36,16 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co auto command_it = this->commands.find(node); if (command_it == this->commands.end()) { - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "cancel"; - XmlSubNode condition(error, STANZA_NS":item-not-found"); + XmlSubNode condition(error, STANZA_NS, "item-not-found"); } else if (command_it->second.is_admin_only() && !Config::is_in_list("admin", jid.bare())) { - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "cancel"; - XmlSubNode condition(error, STANZA_NS":forbidden"); + XmlSubNode condition(error, STANZA_NS, "forbidden"); } else { @@ -94,9 +94,9 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co } else // unsupported action { - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); } } return command_node; diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp index 113943c..aea316d 100644 --- a/src/xmpp/biboumi_adhoc_commands.cpp +++ b/src/xmpp/biboumi_adhoc_commands.cpp @@ -34,7 +34,7 @@ void DisconnectUserStep1(XmppComponent& xmpp_component, AdhocSession&, XmlNode& { auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component); - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Disconnect a user from the gateway"); @@ -108,9 +108,9 @@ void DisconnectUserStep2(XmppComponent& xmpp_component, AdhocSession& session, X return; } } - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); session.terminate(); } @@ -124,7 +124,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman auto options = Database::get_global_options(owner.bare()); command_node.delete_all_children(); - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Configure some global default settings."); @@ -220,9 +220,9 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session, note.set_inner("Configuration successfully applied."); return; } - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); session.terminate(); } @@ -238,7 +238,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com auto commands = Database::get_after_connection_commands(options); command_node.delete_all_children(); - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Configure the IRC server " + server_domain); @@ -310,6 +310,20 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com fingerprint_value.set_inner(options.col<Database::TrustedFingerprint>()); } } + + { + XmlSubNode field(x, "field"); + field["var"] = "sasl_password"; + field["type"] = "text-private"; + field["label"] = "SASL Password"; + set_desc(field, "Use it to authenticate with your nick."); + if (!options.col<Database::SaslPassword>().empty()) + { + XmlSubNode value(field, "value"); + value.set_inner(options.col<Database::SaslPassword>()); + } + } + #endif { @@ -473,9 +487,11 @@ void ConfigureIrcServerStep2(XmppComponent& xmpp_component, AdhocSession& sessio } else if (field->get_tag("var") == "fingerprint" && value) - { - options.col<Database::TrustedFingerprint>() = value->get_inner(); - } + options.col<Database::TrustedFingerprint>() = value->get_inner(); + + else if (field->get_tag("var") == "sasl_password" && value) + options.col<Database::SaslPassword>() = value->get_inner(); + #endif // BOTAN_FOUND @@ -549,9 +565,9 @@ void ConfigureIrcServerStep2(XmppComponent& xmpp_component, AdhocSession& sessio note.set_inner("Configuration successfully applied."); return; } - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); session.terminate(); } @@ -570,7 +586,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, auto options = Database::get_irc_channel_options_with_server_default(requester.local + "@" + requester.domain, iid.get_server(), iid.get_local()); node.delete_all_children(); - XmlSubNode x(node, "jabber:x:data:x"); + XmlSubNode x(node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Configure the IRC channel " + iid.get_local() + " on server " + iid.get_server()); @@ -655,9 +671,9 @@ void ConfigureIrcChannelStep2(XmppComponent& xmpp_component, AdhocSession& sessi } else { - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); session.terminate(); } } @@ -733,7 +749,7 @@ void DisconnectUserFromServerStep1(XmppComponent& xmpp_component, AdhocSession& { // Send a form to select the user to disconnect auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component); - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Disconnect a user from selected IRC servers"); @@ -778,7 +794,7 @@ void DisconnectUserFromServerStep2(XmppComponent& xmpp_component, AdhocSession& command_node.delete_all_children(); auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component); - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Disconnect a user from selected IRC servers"); @@ -914,7 +930,7 @@ void GetIrcConnectionInfoStep1(XmppComponent& component, AdhocSession& session, #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", localtime(&now_c, &tm)); + const auto res = std::strftime(buf, timestamp_size, "%F %T", localtime_r(&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 6fe6972..3cfa9d0 100644 --- a/src/xmpp/biboumi_component.cpp +++ b/src/xmpp/biboumi_component.cpp @@ -158,39 +158,50 @@ 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; } - 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); } - bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "", - from.resource, history_limit, x != nullptr); - const IrcClient* irc = bridge->find_irc_client(iid.get_server()); - if (irc) + else { - const auto chan = irc->find_channel(iid.get_local()); - if (chan->joined) - bridge->send_irc_nick_change(iid, to.resource, from.resource); + 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."); + } + } } } else if (type == "unavailable") @@ -253,7 +264,7 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) { if (type != "unavailable") this->send_stanza_error("presence", from_str, to_str, id, - "cancel", "remote-server-not-found", + "cancel", "recipient-unavailable", "Not connected to IRC server " + ex.hostname, true); } @@ -291,7 +302,26 @@ void BiboumiComponent::handle_message(const Stanza& stanza) if (body && !body->get_inner().empty()) { if (bridge->is_resource_in_chan(iid.to_tuple(), from.resource)) - bridge->send_channel_message(iid, body->get_inner(), id); + { + // 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"; @@ -330,7 +360,6 @@ void BiboumiComponent::handle_message(const Stanza& stanza) if (iid.type == Iid::Type::User && !iid.get_local().empty()) { bridge->send_private_message(iid, body->get_inner()); - bridge->remove_preferred_from_jid(iid.get_local()); } else if (iid.type != Iid::Type::User && !to.resource.empty()) { // a message for chan%server@biboumi/Nick or @@ -338,7 +367,6 @@ void BiboumiComponent::handle_message(const Stanza& stanza) // Convert that into a message to nick!server Iid user_iid(utils::tolower(to.resource), iid.get_server(), Iid::Type::User); bridge->send_private_message(user_iid, body->get_inner()); - bridge->set_preferred_from_jid(user_iid.get_local(), to_str); } else if (iid.type == Iid::Type::Server) bridge->send_raw_message(iid.get_server(), body->get_inner()); @@ -368,7 +396,7 @@ void BiboumiComponent::handle_message(const Stanza& stanza) } catch (const IRCNotConnected& ex) { this->send_stanza_error("message", from_str, to_str, id, - "cancel", "remote-server-not-found", + "cancel", "recipient-unavailable", "Not connected to IRC server " + ex.hostname, true); } @@ -689,7 +717,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) catch (const IRCNotConnected& ex) { this->send_stanza_error("iq", from, to_str, id, - "cancel", "remote-server-not-found", + "cancel", "recipient-unavailable", "Not connected to IRC server " + ex.hostname, true); stanza_error.disable(); @@ -997,7 +1025,7 @@ 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, SELF_PING_FLAG, "muc_nonanonymous"}) + 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; diff --git a/src/xmpp/xmpp_component.cpp b/src/xmpp/xmpp_component.cpp index f82f9ce..62a98ce 100644 --- a/src/xmpp/xmpp_component.cpp +++ b/src/xmpp/xmpp_component.cpp @@ -175,7 +175,7 @@ void XmppComponent::on_stanza(const Stanza& stanza) void XmppComponent::send_stream_error(const std::string& name, const std::string& explanation) { - Stanza node("stream:error"); + Stanza node("stream", "error"); { XmlSubNode error(node, name); error["xmlns"] = STREAM_NS; @@ -367,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; @@ -399,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 @@ -481,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) diff --git a/src/xmpp/xmpp_component.hpp b/src/xmpp/xmpp_component.hpp index 156e286..ee6b776 100644 --- a/src/xmpp/xmpp_component.hpp +++ b/src/xmpp/xmpp_component.hpp @@ -135,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 781fe4c..1f25fa6 100644 --- a/src/xmpp/xmpp_parser.cpp +++ b/src/xmpp/xmpp_parser.cpp @@ -38,7 +38,7 @@ XmppParser::XmppParser(): void XmppParser::init_xml_parser() { // Create the expat parser - this->parser = XML_ParserCreateNS("UTF-8", ':'); + this->parser = XML_ParserCreateNS("UTF-8", '\1'); XML_SetUserData(this->parser, static_cast<void*>(this)); // Install Expat handlers diff --git a/src/xmpp/xmpp_parser.hpp b/src/xmpp/xmpp_parser.hpp index ec42f9a..1e5e4e5 100644 --- a/src/xmpp/xmpp_parser.hpp +++ b/src/xmpp/xmpp_parser.hpp @@ -18,9 +18,9 @@ * stanza is reasonnably short. * * The element names generated by expat contain the namespace of the - * element, a colon (':') and then the actual name of the element. To get + * element, a \1 separator and then the actual name of the element. To get * an element "x" with a namespace of "http://jabber.org/protocol/muc", you - * just look for an XmlNode named "http://jabber.org/protocol/muc:x" + * just look for an XmlNode named "http://jabber.org/protocol/muc\1x" * * TODO: enforce the size-limit for the stanza (limit the number of childs * it can contain). For example forbid the parser going further than level diff --git a/src/xmpp/xmpp_stanza.cpp b/src/xmpp/xmpp_stanza.cpp index 435f333..0103dd7 100644 --- a/src/xmpp/xmpp_stanza.cpp +++ b/src/xmpp/xmpp_stanza.cpp @@ -52,7 +52,7 @@ XmlNode::XmlNode(const std::string& name, XmlNode* parent): parent(parent) { // split the namespace and the name - auto n = name.rfind(':'); + auto n = name.rfind('\1'); if (n == std::string::npos) this->name = name; else @@ -67,6 +67,18 @@ XmlNode::XmlNode(const std::string& name): { } +XmlNode::XmlNode(const std::string& xmlns, const std::string& name, XmlNode* parent): + name(name), + parent(parent) +{ + this->attributes["xmlns"] = xmlns; +} + +XmlNode::XmlNode(const std::string& xmlns, const std::string& name): + XmlNode(xmlns, name, nullptr) +{ +} + void XmlNode::delete_all_children() { this->children.clear(); diff --git a/src/xmpp/xmpp_stanza.hpp b/src/xmpp/xmpp_stanza.hpp index f4b3948..a706337 100644 --- a/src/xmpp/xmpp_stanza.hpp +++ b/src/xmpp/xmpp_stanza.hpp @@ -25,6 +25,8 @@ class XmlNode public: explicit XmlNode(const std::string& name, XmlNode* parent); explicit XmlNode(const std::string& name); + explicit XmlNode(const std::string& xmlns, const std::string& name, XmlNode* parent); + explicit XmlNode(const std::string& xmlns, const std::string& name); /** * The copy constructor does not copy the parent attribute. The children * nodes are all copied recursively. @@ -150,6 +152,10 @@ public: XmlNode(name), parent_to_add(parent_ref) {} + XmlSubNode(XmlNode& parent_ref, const std::string& xmlns, const std::string& name): + XmlNode(xmlns, name), + parent_to_add(parent_ref) + {} ~XmlSubNode() { diff --git a/tests/end_to_end/__main__.py b/tests/end_to_end/__main__.py index e85a4f2..35edf3e 100644 --- a/tests/end_to_end/__main__.py +++ b/tests/end_to_end/__main__.py @@ -3,6 +3,7 @@ from functions import StanzaError, SkipStepError import collections +import subprocess import importlib import sequences import datetime @@ -83,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 @@ -96,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() @@ -113,6 +122,9 @@ 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: @@ -124,10 +136,10 @@ class XMPPComponent(slixmpp.BaseXMPP): if self.biboumi: self.biboumi.stop() - @asyncio.coroutine - def accept_routine(self): - self.accepting_server = yield from self.loop.create_server(lambda: self, - "127.0.0.1", 8811, reuse_address=True) + + async def accept_routine(self): + self.accepting_server = await self.loop.create_server(lambda: self, + "127.0.0.1", 8811, reuse_address=True) class ProcessRunner: @@ -136,13 +148,11 @@ class ProcessRunner: self.signal_sent = False self.create = None - @asyncio.coroutine - def start(self): - self.process = yield from self.create + async def start(self): + self.process = await self.create - @asyncio.coroutine - def wait(self): - code = yield from self.process.wait() + async def wait(self): + code = await self.process.wait() return code def stop(self): @@ -174,7 +184,13 @@ 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", + # Always start with a fresh state + try: + os.remove("ircd.db") + except FileNotFoundError: + pass + subprocess.run(["oragono", "mkcerts", "--conf", os.getcwd() + "/../tests/end_to_end/ircd.yaml"]) + self.create = asyncio.create_subprocess_exec("oragono", "run", "--conf", os.getcwd() + "/../tests/end_to_end/ircd.yaml", stderr=asyncio.subprocess.PIPE) class BiboumiTest: @@ -190,7 +206,8 @@ class BiboumiTest: def run(self): with_valgrind = os.environ.get("E2E_WITH_VALGRIND") is not None - print("Running scenario: [33;1m%s[0m%s" % (self.scenario.name, " (with valgrind)" if with_valgrind else '')) + print("Running scenario: [33;1m%s[0m%s… " % (self.scenario.name, " (with valgrind)" if with_valgrind else ''), end='') + sys.stdout.flush() # Redirect the slixmpp logging into a specific file output_filename = "slixmpp_%s_output.txt" % (self.scenario.name,) with open(output_filename, "w"): @@ -315,7 +332,7 @@ 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.") diff --git a/tests/end_to_end/functions.py b/tests/end_to_end/functions.py index 176dca5..3a21fcf 100644 --- a/tests/end_to_end/functions.py +++ b/tests/end_to_end/functions.py @@ -18,10 +18,10 @@ common_replacements = { 'jid_one': 'first@example.com', 'jid_two': 'second@example.com', 'jid_admin': 'admin@example.com', - 'nick_two': 'Bobby', - 'nick_three': 'Bernard', + 'nick_two': 'Nick2', + 'nick_three': 'Nick3', 'lower_nick_one': 'nick', - 'lower_nick_two': 'bobby', + 'lower_nick_two': 'nick2', } class SkipStepError(Exception): @@ -127,7 +127,9 @@ def expect_stanza(*args, optional=False, after=None): replacements = common_replacements replacements.update(xmpp.saved_values) check_func = check_xpath if not optional else check_xpath_optional - xmpp.stanza_checker = partial(check_func, [xpath.format_map(replacements) for xpath in xpaths], xmpp, after) + 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): @@ -148,6 +150,7 @@ def expect_unordered(*args): 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): 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/channel_custom_topic.py b/tests/end_to_end/scenarios/channel_custom_topic.py index 1fbfb5c..3b3104e 100644 --- a/tests/end_to_end/scenarios/channel_custom_topic.py +++ b/tests/end_to_end/scenarios/channel_custom_topic.py @@ -10,7 +10,7 @@ scenario = ( 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}' />"), + 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( [ diff --git a/tests/end_to_end/scenarios/channel_force_join.py b/tests/end_to_end/scenarios/channel_force_join.py index 089da51..9a24c06 100644 --- a/tests/end_to_end/scenarios/channel_force_join.py +++ b/tests/end_to_end/scenarios/channel_force_join.py @@ -21,25 +21,5 @@ scenario = ( "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]" ] ), - - # And also, that was not the same nickname, so everyone receives a nick change - 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']", - ], - ), ) diff --git a/tests/end_to_end/scenarios/channel_history.py b/tests/end_to_end/scenarios/channel_history.py index ade978b..0014d65 100644 --- a/tests/end_to_end/scenarios/channel_history.py +++ b/tests/end_to_end/scenarios/channel_history.py @@ -8,7 +8,7 @@ scenario = ( 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}' />"), + 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 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 index 40a665b..0e957e1 100644 --- a/tests/end_to_end/scenarios/channel_history_on_fixed_server.py +++ b/tests/end_to_end/scenarios/channel_history_on_fixed_server.py @@ -10,7 +10,7 @@ scenario = ( 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}' />"), + 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 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 index 1fe9908..7d675ac 100644 --- 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 @@ -4,9 +4,8 @@ conf = "fixed_server" scenario = ( - send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' />"), + 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("/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"), 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 index 388b098..c499184 100644 --- a/tests/end_to_end/scenarios/channel_join_with_different_nick.py +++ b/tests/end_to_end/scenarios/channel_join_with_different_nick.py @@ -3,12 +3,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}' />"), + 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}' />"), + 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 index 4c5e508..fdebcbe 100644 --- a/tests/end_to_end/scenarios/channel_join_with_password.py +++ b/tests/end_to_end/scenarios/channel_join_with_password.py @@ -10,9 +10,9 @@ scenario = ( 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}'/>"), + 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) - bad key']"), + 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) 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 index 4e22c50..3b2b102 100644 --- a/tests/end_to_end/scenarios/channel_join_with_two_users.py +++ b/tests/end_to_end/scenarios/channel_join_with_two_users.py @@ -4,7 +4,7 @@ scenario = ( scenarios.simple_channel_join.scenario, # Second user joins - send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"), + 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( [ diff --git a/tests/end_to_end/scenarios/channel_list_escaping.py b/tests/end_to_end/scenarios/channel_list_escaping.py index 12c3ff9..7229e97 100644 --- a/tests/end_to_end/scenarios/channel_list_escaping.py +++ b/tests/end_to_end/scenarios/channel_list_escaping.py @@ -1,9 +1,8 @@ from scenarios import * scenario = ( - send_stanza("<presence from='{jid_one}/{resource_one}' to='#true\\2ffalse%{irc_server_one}/{nick_one}' />"), + 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("/message/body[text()='Mode #true/false [+nt] by {irc_host_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 index 79e76f4..aaf589a 100644 --- a/tests/end_to_end/scenarios/channel_list_with_rsm.py +++ b/tests/end_to_end/scenarios/channel_list_with_rsm.py @@ -3,13 +3,11 @@ from scenarios import * scenario = ( scenarios.simple_channel_join.scenario, - send_stanza("<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' />"), - expect_stanza("/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"), + 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}' />"), - expect_stanza("/message/body[text()='Mode #coucou [+nt] by {irc_host_one}']"), + 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())]"), @@ -24,44 +22,46 @@ scenario = ( # 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", - "/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}']"), + "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", - "/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']"), + "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>#bar%{irc_server_one}</after><max>1</max></set></query></iq>"), + 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", - "/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']"), + "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>#coucou%{irc_server_one}</after><max>1</max></set></query></iq>"), + 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", - "/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']"), + "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>#foo%{irc_server_one}</after><max>1</max></set></query></iq>"), + 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 index 8ea979c..a3ead30 100644 --- a/tests/end_to_end/scenarios/channel_messages.py +++ b/tests/end_to_end/scenarios/channel_messages.py @@ -6,7 +6,7 @@ scenario = ( scenarios.simple_channel_join.scenario, # Second user joins - send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"), + 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 @@ -21,15 +21,15 @@ scenario = ( ), # Send a channel message - send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></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[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']", + "/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[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='coucou']", + "/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]" ] ), @@ -37,34 +37,33 @@ scenario = ( # 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']"), + expect_stanza("/message[@from='{lower_nick_one}%{irc_server_one}'][@to='{jid_two}'][@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"), + # The response is received from the server-wide JID without MUC PM marker + expect_stanza("/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_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}' />"), - expect_stanza("/message"), + 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']"), + expect_stanza("/message[@from='{lower_nick_one}%{irc_server_one}'][@to='{jid_two}'][@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']"), + # The response is received from the server-wide JID + expect_stanza("/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_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 + # The private messages from this nick should still come 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}']"), + expect_stanza("/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}']"), ) diff --git a/tests/end_to_end/scenarios/client_error.py b/tests/end_to_end/scenarios/client_error.py index ca5eaee..8c6fd7e 100644 --- a/tests/end_to_end/scenarios/client_error.py +++ b/tests/end_to_end/scenarios/client_error.py @@ -3,7 +3,7 @@ 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}' />"), + 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())]"), 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 index 8cef926..4c7e795 100644 --- 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 @@ -1,86 +1,73 @@ from scenarios import * scenario = ( - send_stanza("<presence from='{jid_one}/{resource_one}' to='#aaa%{irc_server_one}/{nick_one}' />"), + 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("/message"), expect_stanza("/presence"), expect_stanza("/message"), - send_stanza("<presence from='{jid_one}/{resource_one}' to='#bbb%{irc_server_one}/{nick_one}' />"), - 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}' />"), - 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}' />"), - 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}' />"), - 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}' />"), - 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}' />"), - 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}' />"), - 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}' />"), - 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}' />"), - 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", - "/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}']"), + "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>#ccc%{irc_server_one}</after><max>3</max></set></query></iq>"), + 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", - "/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}']"), + "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>#fff%{irc_server_one}</after><max>3</max></set></query></iq>"), + 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", - "/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}']"), + "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>#iii%{irc_server_one}</after><max>3</max></set></query></iq>"), + 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", - "/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}']", + "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' />"), diff --git a/tests/end_to_end/scenarios/default_channel_list_limit.py b/tests/end_to_end/scenarios/default_channel_list_limit.py index 84f96b3..6858ea1 100644 --- a/tests/end_to_end/scenarios/default_channel_list_limit.py +++ b/tests/end_to_end/scenarios/default_channel_list_limit.py @@ -26,27 +26,19 @@ scenario = ( after = save_value("counter", counter)), - send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), + 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}' />"), - expect_stanza("/message"), + 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>"), - # charybdis sends the list in alphabetic order, so #foo is the last, and #99 is after #120 - 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}']"), + 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 index 02fcaa7..0f402f8 100644 --- a/tests/end_to_end/scenarios/default_mam_limit.py +++ b/tests/end_to_end/scenarios/default_mam_limit.py @@ -14,9 +14,8 @@ scenario = ( "</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}' />"), + 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("/message/body[text()='Mode #foo [+nt] by {irc_host_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())]", diff --git a/tests/end_to_end/scenarios/encoded_channel_join.py b/tests/end_to_end/scenarios/encoded_channel_join.py index fd4144a..71fa09f 100644 --- a/tests/end_to_end/scenarios/encoded_channel_join.py +++ b/tests/end_to_end/scenarios/encoded_channel_join.py @@ -1,9 +1,8 @@ from scenarios import * scenario = ( - send_stanza("<presence from='{jid_one}/{resource_one}' to='#biboumi\\40louiz.org\\3a80%{irc_server_one}/{nick_one}' />"), + 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("/message/body[text()='Mode #biboumi@louiz.org:80 [+nt] by {irc_host_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 index 18dfe94..7f3c04c 100644 --- 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 @@ -4,17 +4,17 @@ 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}' />"), + 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}' />"), + 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}' />"), + 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}"), 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 index a680017..6ce5231 100644 --- a/tests/end_to_end/scenarios/execute_disconnect_user_adhoc_command.py +++ b/tests/end_to_end/scenarios/execute_disconnect_user_adhoc_command.py @@ -3,7 +3,7 @@ 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}' />"), + 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}"), diff --git a/tests/end_to_end/scenarios/fixed_irc_server_subscription.py b/tests/end_to_end/scenarios/fixed_irc_server_subscription.py index 091cf2a..f255b19 100644 --- a/tests/end_to_end/scenarios/fixed_irc_server_subscription.py +++ b/tests/end_to_end/scenarios/fixed_irc_server_subscription.py @@ -3,6 +3,6 @@ from scenarios import * conf = 'fixed_server' scenario = ( - send_stanza("<presence type='subscribe' from='{jid_one}/{resource_one}' to='{biboumi_host}' id='sub1' />"), + 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/get_irc_connection_info.py b/tests/end_to_end/scenarios/get_irc_connection_info.py index 25f2a87..7695aa1 100644 --- a/tests/end_to_end/scenarios/get_irc_connection_info.py +++ b/tests/end_to_end/scenarios/get_irc_connection_info.py @@ -4,11 +4,9 @@ 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}' />"), + 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("/message"), - expect_stanza("/presence"), - expect_stanza("/message"), + 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 index d9be151..922ea6a 100644 --- a/tests/end_to_end/scenarios/get_irc_connection_info_fixed.py +++ b/tests/end_to_end/scenarios/get_irc_connection_info_fixed.py @@ -6,9 +6,8 @@ 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}' />"), + 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("/message"), expect_stanza("/presence"), expect_stanza("/message"), diff --git a/tests/end_to_end/scenarios/invite_other.py b/tests/end_to_end/scenarios/invite_other.py index 2badf77..0e40dcb 100644 --- a/tests/end_to_end/scenarios/invite_other.py +++ b/tests/end_to_end/scenarios/invite_other.py @@ -1,15 +1,13 @@ from scenarios import * scenario = ( - send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), + 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("/message"), expect_stanza("/presence"), expect_stanza("/message"), - send_stanza("<presence from='{jid_two}/{resource_two}' to='#bar%{irc_server_one}/{nick_two}' />"), + 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("/message"), 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>"), diff --git a/tests/end_to_end/scenarios/irc_server_connection.py b/tests/end_to_end/scenarios/irc_server_connection.py index 25ea749..f31b60f 100644 --- a/tests/end_to_end/scenarios/irc_server_connection.py +++ b/tests/end_to_end/scenarios/irc_server_connection.py @@ -1,7 +1,7 @@ from scenarios import * scenario = ( - send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), + 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 index 2abe39f..adcbc57 100644 --- a/tests/end_to_end/scenarios/irc_server_connection_failure.py +++ b/tests/end_to_end/scenarios/irc_server_connection_failure.py @@ -1,7 +1,7 @@ from scenarios import * scenario = ( - send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%doesnotexist@{biboumi_host}/{nick_one}' />"), + 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", 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 index bc2016d..24c1b60 100644 --- a/tests/end_to_end/scenarios/irc_server_presence_in_roster.py +++ b/tests/end_to_end/scenarios/irc_server_presence_in_roster.py @@ -9,11 +9,10 @@ scenario = ( 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}' />"), + 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("/message/body[text()='Mode #foo [+nt] by {irc_host_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())]"), diff --git a/tests/end_to_end/scenarios/irc_tls_connection.py b/tests/end_to_end/scenarios/irc_tls_connection.py index 8b30893..db5d32e 100644 --- a/tests/end_to_end/scenarios/irc_tls_connection.py +++ b/tests/end_to_end/scenarios/irc_tls_connection.py @@ -16,9 +16,8 @@ scenario = ( "</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}' />"), + 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("/message/body[text()='Mode #foo [+nt] by {irc_host_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 index 2432f14..216e2a0 100644 --- a/tests/end_to_end/scenarios/join_history_limit.py +++ b/tests/end_to_end/scenarios/join_history_limit.py @@ -15,9 +15,8 @@ scenario = ( expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"), - send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), + 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("/message/body[text()='Mode #foo [+nt] by {irc_host_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())]"), @@ -41,8 +40,7 @@ scenario = ( 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}' />"), - expect_stanza("/message"), + 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"), @@ -54,7 +52,6 @@ scenario = ( # 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("/message"), expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"), expect_stanza("/message/subject"), @@ -65,7 +62,6 @@ scenario = ( # 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("/message"), 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']"), @@ -77,7 +73,6 @@ scenario = ( # 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("/message"), 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']"), @@ -87,7 +82,6 @@ scenario = ( # 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("/message"), 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']"), @@ -97,7 +91,6 @@ scenario = ( # 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("/message"), 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']"), diff --git a/tests/end_to_end/scenarios/leave_unjoined_chan.py b/tests/end_to_end/scenarios/leave_unjoined_chan.py index a9751d7..aeb6f55 100644 --- a/tests/end_to_end/scenarios/leave_unjoined_chan.py +++ b/tests/end_to_end/scenarios/leave_unjoined_chan.py @@ -1,16 +1,14 @@ from scenarios import * scenario = ( - send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), + 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("/message"), - expect_stanza("/presence"), - expect_stanza("/message"), + simple_channel_join.expect_self_join_presence(), - send_stanza("<presence from='{jid_two}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"), + 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("/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/mode_change.py b/tests/end_to_end/scenarios/mode_change.py index 4cbf036..b45904b 100644 --- a/tests/end_to_end/scenarios/mode_change.py +++ b/tests/end_to_end/scenarios/mode_change.py @@ -22,6 +22,16 @@ scenario = ( ["/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( diff --git a/tests/end_to_end/scenarios/muc_disco_info.py b/tests/end_to_end/scenarios/muc_disco_info.py index 19e90f2..9b6f0e5 100644 --- a/tests/end_to_end/scenarios/muc_disco_info.py +++ b/tests/end_to_end/scenarios/muc_disco_info.py @@ -10,12 +10,12 @@ scenario = ( "/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}' />"), + 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("/message/body[text()='Mode #foo [+nt] by {irc_host_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']"), diff --git a/tests/end_to_end/scenarios/multiline_message.py b/tests/end_to_end/scenarios/multiline_message.py index fc88e66..cd42c6c 100644 --- a/tests/end_to_end/scenarios/multiline_message.py +++ b/tests/end_to_end/scenarios/multiline_message.py @@ -34,7 +34,7 @@ scenario = ( # 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}' />"), + 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( diff --git a/tests/end_to_end/scenarios/multiple_channels_join.py b/tests/end_to_end/scenarios/multiple_channels_join.py index 84e4360..281a902 100644 --- a/tests/end_to_end/scenarios/multiple_channels_join.py +++ b/tests/end_to_end/scenarios/multiple_channels_join.py @@ -4,8 +4,8 @@ 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}' />"), - send_stanza("<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' />"), + 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(), @@ -14,5 +14,9 @@ scenario = ( 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}"), + + send_stanza("<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><subject>Le topic</subject></message>"), + expect_stanza("/message"), + ) diff --git a/tests/end_to_end/scenarios/multisession_kick.py b/tests/end_to_end/scenarios/multisession_kick.py index 7d8679f..580beb4 100644 --- a/tests/end_to_end/scenarios/multisession_kick.py +++ b/tests/end_to_end/scenarios/multisession_kick.py @@ -4,19 +4,21 @@ 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}' />"), + 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/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']"], + ["/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}' />"), - expect_stanza("/presence[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']"), - expect_stanza("/presence[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_two}']", - "/presence/muc_user:x/muc_user:status[@code='110']" + 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())]"), diff --git a/tests/end_to_end/scenarios/multisessionnick.py b/tests/end_to_end/scenarios/multisessionnick.py index 4e72ce7..1377a94 100644 --- a/tests/end_to_end/scenarios/multisessionnick.py +++ b/tests/end_to_end/scenarios/multisessionnick.py @@ -4,12 +4,12 @@ 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}' />"), + 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}' />"), + 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( @@ -24,7 +24,7 @@ scenario = ( ), # A different user joins the same room - send_stanza("<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"), + 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) @@ -51,27 +51,17 @@ scenario = ( # 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", - ] + # Message is received with a server-wide JID to the bare JID as non-MUC-PM + expect_stanza( + "/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}'][@type='chat']/body[text()='RELLO']", + "!/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: Bobby: Nickname is already in use.']"], - ["/message[@to='{jid_one}/{resource_two}'][@type='chat']/body[text()='irc.localhost: Bobby: Nickname is already in use.']"], + ["/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']"] ), @@ -80,14 +70,14 @@ scenario = ( 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='Bernard']", + "/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='Bernard']", + "/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']" ], @@ -96,7 +86,7 @@ scenario = ( "/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[@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']" ], @@ -117,9 +107,9 @@ scenario = ( 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 + # The first user receives the two messages to the bare JID 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']"] + ["/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}'][@type='chat']/body[text()='first']"], + ["/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_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 index 47aecc6..f4feae3 100644 --- a/tests/end_to_end/scenarios/nick_change_in_join.py +++ b/tests/end_to_end/scenarios/nick_change_in_join.py @@ -3,12 +3,11 @@ 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}' />"), + 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}' />"), - expect_stanza("/message/body[text()='Mode #bar [+nt] by {irc_host_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 diff --git a/tests/end_to_end/scenarios/not_connected_error.py b/tests/end_to_end/scenarios/not_connected_error.py index cc9fb35..577b324 100644 --- a/tests/end_to_end/scenarios/not_connected_error.py +++ b/tests/end_to_end/scenarios/not_connected_error.py @@ -6,7 +6,7 @@ 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}' />"), + 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/persistent_channel.py b/tests/end_to_end/scenarios/persistent_channel.py index 3521a84..ab525fe 100644 --- a/tests/end_to_end/scenarios/persistent_channel.py +++ b/tests/end_to_end/scenarios/persistent_channel.py @@ -17,7 +17,7 @@ scenario = ( 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}' />"), + 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}']"], @@ -26,8 +26,8 @@ scenario = ( "/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())]"] ), - 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) send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />"), diff --git a/tests/end_to_end/scenarios/raw_message.py b/tests/end_to_end/scenarios/raw_message.py index 96a3f3d..c6cd4e7 100644 --- a/tests/end_to_end/scenarios/raw_message.py +++ b/tests/end_to_end/scenarios/raw_message.py @@ -1,9 +1,8 @@ from scenarios import * scenario = ( - send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), + 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("/message"), expect_stanza("/presence"), expect_stanza("/message"), 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 index 8196d12..7eb5b13 100644 --- a/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py +++ b/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py @@ -3,9 +3,8 @@ from scenarios import * conf = 'fixed_server' scenario = ( - send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), + 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("/message"), expect_stanza("/presence"), expect_stanza("/message"), diff --git a/tests/end_to_end/scenarios/raw_names_command.py b/tests/end_to_end/scenarios/raw_names_command.py deleted file mode 100644 index 09b47be..0000000 --- a/tests/end_to_end/scenarios/raw_names_command.py +++ /dev/null @@ -1,13 +0,0 @@ -from functions import send_stanza, expect_stanza - -import scenarios.simple_channel_join - -join_channel = scenarios.simple_channel_join.scenario - -scenario = ( - join_channel, - - send_stanza("<message type='chat' from='{jid_one}/{resource_one}' to='{irc_server_one}'><body>NAMES</body></message>"), - expect_stanza("/message/body[text()='irc.localhost: = #foo @{nick_one} ']"), - expect_stanza("/message/body[text()='irc.localhost: * End of /NAMES list. ']"), -) 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 index 8d208f0..dfb7161 100644 --- 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 @@ -2,9 +2,8 @@ from scenarios import * scenario = ( # Join the channel - send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), + 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("/message/body[text()='Mode #foo [+nt] by {irc_host_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())]"), @@ -19,15 +18,14 @@ scenario = ( 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}' />"), + 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}' />"), + 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("/message/body[text()='Mode #bar [+nt] by {irc_host_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())]"), diff --git a/tests/end_to_end/scenarios/sasl.py b/tests/end_to_end/scenarios/sasl.py new file mode 100644 index 0000000..9f2a27a --- /dev/null +++ b/tests/end_to_end/scenarios/sasl.py @@ -0,0 +1,99 @@ +from scenarios import * + +scenario = ( + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/RegisteredUser' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection(), + + simple_channel_join.expect_self_join_presence(jid = '{jid_one}/{resource_one}', chan = "#foo", nick = "RegisteredUser"), + + # Create an account by talking to nickserv directly + send_stanza("<message from='{jid_one}/{resource_one}' to='nickserv%{irc_server_one}' type='chat'><body>register P4SSW0RD</body></message>"), + expect_stanza("/message/body[text()='Account created']"), + expect_stanza("/message/body[text()=\"You're now logged in as RegisteredUser\"]"), + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />"), + expect_stanza("/presence[@type='unavailable']"), + + # Configure an sasl password + 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:field[@type='text-private'][@var='sasl_password']", + 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='sasl_password'><value>P4SSW0RD</value></field>" + "<field var='ports'><value>6667</value></field>" + "<field var='nick'><value>RegisteredUser</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.']"), + + # Joining a channel with the associated nick will work + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/ignored' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection(login="RegisteredUser"), + expect_stanza("/presence"), + expect_stanza("/message/subject"), + + # Leave the channel and disconnect from the server to try again differently + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/RegisteredUser' type='unavailable' />"), + expect_stanza("/presence[@type='unavailable']"), + + # Leave the same password, but remove the Nick + 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:field[@type='text-private'][@var='sasl_password']", + 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' node='configure' sessionid='{sessionid}' action='complete'>" + "<x xmlns='jabber:x:data' type='submit'>" + "<field var='sasl_password'><value>P4SSW0RD</value></field>" + "<field var='ports'><value>6667</value></field>" + "<field var='nick'><value></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.']"), + + # Joining a channel with the associated nick will work, it will use the one from our <presence/> + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/RegisteredUser' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + sequences.connection(login="RegisteredUser"), + expect_stanza("/presence"), + expect_stanza("/message/subject"), + + # Leave the channel and disconnect from the server to try again differently + send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/RegisteredUser' type='unavailable' />"), + expect_stanza("/presence[@type='unavailable']"), + + # Configure an INCORRECT password + send_stanza("<iq type='set' id='id5' 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:field[@type='text-private'][@var='sasl_password']", + after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))), + + + send_stanza("<iq type='set' id='id6' 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='sasl_password'><value>wrong wrong wrong</value></field>" + "<field var='ports'><value>6667</value></field>" + "<field var='nick'><value>RegisteredUser</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}/ignored' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"), + # Here, the 6 connecting… connected messages from the connection attempt + expect_stanza("/message"), + expect_stanza("/message"), + expect_stanza("/message"), + expect_stanza("/message"), + expect_stanza("/message"), + expect_stanza("/message"), + expect_stanza("/presence[@type='error'][@from='#foo%{irc_server_one}/RegisteredUser']"), + expect_stanza("/message/body[text()='ERROR: Quit: SASL authentication failed: Invalid account credentials']"), + expect_stanza("/message/body[text()='ERROR: Connection closed.']"), +) diff --git a/tests/end_to_end/scenarios/self_invite.py b/tests/end_to_end/scenarios/self_invite.py deleted file mode 100644 index 7959b3a..0000000 --- a/tests/end_to_end/scenarios/self_invite.py +++ /dev/null @@ -1,7 +0,0 @@ -from scenarios import * - -scenario = ( - scenarios.simple_channel_join.scenario, - 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>"), - expect_stanza("/message/body[text()='{nick_one} is already on channel #foo']") -) 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 index 6cbb210..3474288 100644 --- a/tests/end_to_end/scenarios/self_ping_on_real_channel.py +++ b/tests/end_to_end/scenarios/self_ping_on_real_channel.py @@ -8,7 +8,7 @@ scenario = ( 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}' />"), + 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']"), diff --git a/tests/end_to_end/scenarios/self_version.py b/tests/end_to_end/scenarios/self_version.py index f567355..a5c81eb 100644 --- a/tests/end_to_end/scenarios/self_version.py +++ b/tests/end_to_end/scenarios/self_version.py @@ -13,7 +13,7 @@ scenario = ( 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}' />"), + 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())]"), diff --git a/tests/end_to_end/scenarios/simple_channel_join.py b/tests/end_to_end/scenarios/simple_channel_join.py index b09d6be..6b57207 100644 --- a/tests/end_to_end/scenarios/simple_channel_join.py +++ b/tests/end_to_end/scenarios/simple_channel_join.py @@ -1,8 +1,7 @@ from scenarios import * -def expect_self_join_presence(jid, chan, nick, irc_server="{irc_server_one}"): +def expect_self_join_presence(jid='{jid_one}/{resource_one}', chan='#foo', nick='{nick_one}', irc_server="{irc_server_one}"): return ( - expect_stanza("/message/body[text()='Mode " + chan + " [+nt] by irc.localhost']"), 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']", @@ -12,10 +11,9 @@ def expect_self_join_presence(jid, chan, nick, irc_server="{irc_server_one}"): scenario = ( - send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"), + 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 index 6efd20f..9f5b835 100644 --- a/tests/end_to_end/scenarios/simple_channel_join_fixed.py +++ b/tests/end_to_end/scenarios/simple_channel_join_fixed.py @@ -3,9 +3,8 @@ from scenarios import * conf = "fixed_server" scenario = ( - send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}/{nick_one}' />"), + 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("/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"), 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_kick.py b/tests/end_to_end/scenarios/simple_kick.py index 0e06589..2949157 100644 --- a/tests/end_to_end/scenarios/simple_kick.py +++ b/tests/end_to_end/scenarios/simple_kick.py @@ -4,13 +4,12 @@ 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}' />"), - expect_stanza("/message"), + 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}' />"), + 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']"], [ 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 index 8a40e52..f151bc7 100644 --- a/tests/end_to_end/sequences.py +++ b/tests/end_to_end/sequences.py @@ -1,4 +1,4 @@ -from functions import expect_stanza, send_stanza, common_replacements +from functions import expect_stanza, send_stanza, common_replacements, expect_unordered def handshake(): return ( @@ -6,14 +6,12 @@ def handshake(): send_stanza("<handshake xmlns='jabber:component:accept'/>") ) -def connection_begin(irc_host, jid, expected_irc_presence=False, fixed_irc_server=False): +def connection_begin(irc_host, jid, expected_irc_presence=False, fixed_irc_server=False, login=None): 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 = ( expect_stanza(xpath % ('Connecting to %s:6697 (encrypted)' % irc_host), "/message/hints:no-copy", @@ -28,25 +26,20 @@ def connection_begin(irc_host, jid, expected_irc_presence=False, fixed_irc_serve if expected_irc_presence: result += (expect_stanza("/presence[@from='" + irc_host + "@biboumi.localhost']"),) - # These five messages can be receive in any order + if login is not None: + result += (expect_stanza("/message/body[text()='irc.localhost: You are now logged in as %s']" % (login,)),) result += ( - expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')), - expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')), - expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')), - expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')), - expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')), + 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']" - 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(xpath % ('Connecting to %s:7778 (encrypted)' % irc_host), @@ -54,12 +47,8 @@ def connection_tls_begin(irc_host, jid, fixed_irc_server): "/message/carbon:private", ), expect_stanza(xpath % 'Connected to IRC server (encrypted).'), - # These five messages can be receive in any order - expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)), - expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)), - expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)), - expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)), - expect_stanza(xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % irc_host)), + 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): @@ -72,37 +61,25 @@ def connection_end(irc_host, jid, fixed_irc_server=False): xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]" irc_host = 'irc.localhost' return ( - expect_stanza(xpath_re % (r'^%s: Your host is .*$' % irc_host)), + 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+ servers$' % irc_host)), - expect_stanza(xpath_re % (r'^%s: \d+ unknown connection\(s\)$' % irc_host), optional=True), - expect_stanza(xpath_re % (r'^%s: \d+ channels formed$' % irc_host), optional=True), + 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_re % (r'^%s: Highest connection count: \d+ \(\d+ clients\) \(\d+ connections received\)$' % irc_host)), - expect_stanza(xpath % "- This is charybdis MOTD you might replace it, but if not your friends will\n- laugh at you.\n"), - expect_stanza(xpath_re % r'^User mode for \w+ is \[\+Z?i\]$'), - ) - -def connection_middle(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 ( - expect_stanza(xpath_re % (r'^%s: \*\*\* You are exempt from flood limits$' % 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_middle(irc_host, jid, fixed_irc_server=fixed_irc_server) + \ +def connection(irc_host="irc.localhost", jid="{jid_one}/{resource_one}", expected_irc_presence=False, fixed_irc_server=False, login=None): + return connection_begin(irc_host, jid, expected_irc_presence, fixed_irc_server=fixed_irc_server, login=login) + \ 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_middle(irc_host, jid, fixed_irc_server) +\ connection_end(irc_host, jid, fixed_irc_server) diff --git a/tests/irc.cpp b/tests/irc.cpp new file mode 100644 index 0000000..0f30f15 --- /dev/null +++ b/tests/irc.cpp @@ -0,0 +1,43 @@ +#include "catch.hpp" + +#include <irc/irc_message.hpp> + +TEST_CASE("Basic IRC message parsing") +{ + IrcMessage m(":prefix COMMAND un deux trois"); + CHECK(m.prefix == "prefix"); + CHECK(m.command == "COMMAND"); + CHECK(m.arguments.size() == 3); + CHECK(m.arguments[0] == "un"); + CHECK(m.arguments[1] == "deux"); + CHECK(m.arguments[2] == "trois"); +} + +TEST_CASE("Trailing space") +{ + IrcMessage m(":prefix COMMAND un deux trois "); + CHECK(m.prefix == "prefix"); + CHECK(m.arguments.size() == 3); + CHECK(m.arguments[0] == "un"); + CHECK(m.arguments[1] == "deux"); + CHECK(m.arguments[2] == "trois"); +} + +TEST_CASE("Message with :") +{ + IrcMessage m(":prefix COMMAND un :coucou les amis "); + CHECK(m.prefix == "prefix"); + CHECK(m.arguments.size() == 2); + CHECK(m.arguments[0] == "un"); + CHECK(m.arguments[1] == "coucou les amis "); +} + +TEST_CASE("Message with empty :") +{ + IrcMessage m("COMMAND un deux :"); + CHECK(m.prefix == ""); + CHECK(m.arguments.size() == 3); + CHECK(m.arguments[0] == "un"); + CHECK(m.arguments[1] == "deux"); + CHECK(m.arguments[2] == ""); +} diff --git a/tests/utils.cpp b/tests/utils.cpp index 6de19f0..6151733 100644 --- a/tests/utils.cpp +++ b/tests/utils.cpp @@ -28,6 +28,9 @@ TEST_CASE("String split") CHECK(splitted.size() == 2); CHECK(splitted[0] == ""); CHECK(splitted[1] == "a"); + splitted = utils::split("multi-prefix ", ' '); + CHECK(splitted[0] == "multi-prefix"); + CHECK(splitted.size() == 1); } TEST_CASE("tolower") diff --git a/tests/xmpp.cpp b/tests/xmpp.cpp index 14c51da..c49c2fd 100644 --- a/tests/xmpp.cpp +++ b/tests/xmpp.cpp @@ -67,6 +67,8 @@ TEST_CASE("substanzas") CHECK(!d.has_children()); } CHECK(b.has_children()); + XmlSubNode e(a, "namespace", "name"); + CHECK(e.get_tag("xmlns") == "namespace"); } CHECK(a.has_children()); } 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 |