diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 779fa3045..c087d7a10 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,7 +7,7 @@ jobs: syntax_check: runs-on: ubuntu-latest container: - image: registry.freedesktop.org/telepathy/telepathy-gabble/debstbl:v1 + image: registry.freedesktop.org/telepathy/telepathy-gabble/debstbl:v2 options: -u 0:0 timeout-minutes: 5 steps: @@ -25,9 +25,9 @@ jobs: strategy: matrix: image: - - registry.freedesktop.org/telepathy/telepathy-gabble/debstbl:v1 - - registry.freedesktop.org/telepathy/telepathy-gabble/osuselp:v1 - - registry.freedesktop.org/telepathy/telepathy-gabble/fedoraw:v1 + - registry.freedesktop.org/telepathy/telepathy-gabble/debstbl:v2 + - registry.freedesktop.org/telepathy/telepathy-gabble/osuselp:v2 + - registry.freedesktop.org/telepathy/telepathy-gabble/fedoraw:v2 container: image: ${{ matrix.image }} options: -u 0:0 @@ -50,3 +50,29 @@ jobs: _b/meson-logs/* _b/tests/twisted/tools/gabble-testing.log if: ${{ always() }} + + autobuild: + runs-on: ubuntu-18.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v2 + - name: Install prereqs + run: |- + sudo apt-get update && sudo apt-get install --no-install-recommends -qq -y build-essential \ + ccache automake libtool libglib2.0-dev glib-networking libtelepathy-glib-dev libsasl2-dev \ + libxml2-dev libsoup2.4-dev libsasl2-modules-gssapi-mit gnutls-bin libsqlite3-dev xsltproc \ + libssl-dev libgnutls28-dev libnice-dev python3-twisted python3-dbus + - name: Bootstrap and Config + run: bash autogen.sh + - name: Build + run: make + - name: Test + run: make check + - name: Artifacts + uses: actions/upload-artifact@v2 + with: + name: automake-logs + path: | + tests/twisted/tools/gabble-testing.log + FIXME.out + if: ${{ always() }} diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 322f6e70b..8bfd5b3e5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,10 +4,11 @@ stages: - test variables: - FEDORA_MB: registry.freedesktop.org/telepathy/telepathy-gabble/fedoraw:v1 - DEBIAN_MB: registry.freedesktop.org/telepathy/telepathy-gabble/debtest:v1 - DEBSTB_MB: registry.freedesktop.org/telepathy/telepathy-gabble/debstbl:v1 - SUSELP_MB: registry.freedesktop.org/telepathy/telepathy-gabble/osuselp:v1 + FEDORA_MB: registry.freedesktop.org/telepathy/telepathy-gabble/fedoraw:v2 + DEBIAN_MB: registry.freedesktop.org/telepathy/telepathy-gabble/debtest:v2 + DEBSTB_MB: registry.freedesktop.org/telepathy/telepathy-gabble/debstbl:v2 + SUSELP_MB: registry.freedesktop.org/telepathy/telepathy-gabble/osuselp:v2 + # OpenSUSE Tumbleweed has broken Twisted, can still be used in wocky CI SUSETW_MB: registry.freedesktop.org/telepathy/telepathy-gabble/osusetw:v1 WOCKY_DEBUG: all #G_MESSAGES_DEBUG: all @@ -19,6 +20,21 @@ variables: key: "$CI_JOB_IMAGE:$CI_COMMIT_SHA" paths: [ _b ] +.def_ac: + before_script: + - sh autogen.sh + cache: + key: "AC:$CI_JOB_IMAGE:$CI_COMMIT_SHA" + paths: + - src/.libs + - plugins/.libs + - extensions/_gen + - extensions/.libs + - lib/gibber/.libs + - lib/ext/wocky/wocky/.libs + - tests/.libs + - tests/twisted/.libs + style-ck-mb: extends: .def_mb stage: style-check @@ -63,3 +79,26 @@ test-mb: - $DEBSTB_MB - $SUSELP_MB +test-ac: + extends: .def_ac + stage: test + image: $image + script: + - make + - make check + artifacts: + reports: + expire_in: 1 week + when: always + paths: + - "FIXME.out" + - "tests/twisted/tools/gabble-testing.log" + parallel: + matrix: + - image: + - $FEDORA_MB + - $DEBIAN_MB + #- $SUSETW_MB + - $DEBSTB_MB + - $SUSELP_MB + diff --git a/.gitlab-ci/Dockerfile.gabble-debstbl b/.gitlab-ci/Dockerfile.gabble-debstbl new file mode 100644 index 000000000..abc3e98e2 --- /dev/null +++ b/.gitlab-ci/Dockerfile.gabble-debstbl @@ -0,0 +1,21 @@ +FROM debian:stable + +ENV TZ=UTC +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt update -qy \ + && apt install -qy meson git libglib2.0-dev glib-networking \ + libtelepathy-glib-dev libxml2-dev xsltproc libnice-dev \ + python3-twisted libsasl2-dev gcc ccache libsqlite3-dev \ + python3-dbus autoconf gnutls-bin libtool-bin make \ + && apt-get clean -qy + + +ARG HOST_USER_ID=1000 +ENV HOST_USER_ID ${HOST_USER_ID} +RUN useradd -u $HOST_USER_ID -ms /bin/bash user && usermod user -G user + +USER user +WORKDIR /home/user + +ENV LANG C.UTF-8 diff --git a/.gitlab-ci/Dockerfile.gabble-debtest b/.gitlab-ci/Dockerfile.gabble-debtest new file mode 100644 index 000000000..b04c664c3 --- /dev/null +++ b/.gitlab-ci/Dockerfile.gabble-debtest @@ -0,0 +1,21 @@ +FROM debian:testing + +ENV TZ=UTC +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt update -qy \ + && apt install -qy meson git libglib2.0-dev glib-networking \ + libtelepathy-glib-dev libxml2-dev xsltproc libnice-dev \ + python3-twisted libsasl2-dev gcc ccache libsqlite3-dev \ + python3-dbus autoconf gnutls-bin libtool-bin make \ + && apt-get clean -qy + + +ARG HOST_USER_ID=1000 +ENV HOST_USER_ID ${HOST_USER_ID} +RUN useradd -u $HOST_USER_ID -ms /bin/bash user && usermod user -G user + +USER user +WORKDIR /home/user + +ENV LANG C.UTF-8 diff --git a/.gitlab-ci/Dockerfile.gabble-fedoraw b/.gitlab-ci/Dockerfile.gabble-fedoraw new file mode 100644 index 000000000..6e852303b --- /dev/null +++ b/.gitlab-ci/Dockerfile.gabble-fedoraw @@ -0,0 +1,24 @@ +FROM fedora:rawhide + +RUN dnf update -y \ + && dnf install -y 'dnf-command(builddep)' \ + && dnf builddep -y glib-networking telepathy-gabble \ + && dnf clean all +RUN dnf install -y git meson python3-twisted python3-gobject dbus-daemon \ + cyrus-sasl-scram cyrus-sasl-md5 cyrus-sasl-plain gnutls-utils \ + && dnf clean all + +# Debug the docker if required +#RUN dnf install -y valgrind gdb \ +# && dnf debuginfo-install -y cyrus-sasl cyrus-sasl-scram \ +# glib glib-networking openssl gnutls + +ARG HOST_USER_ID=1000 +ENV HOST_USER_ID ${HOST_USER_ID} +RUN useradd -u $HOST_USER_ID -ms /bin/bash user +RUN ln -fs /usr/bin/python3 /usr/local/bin/python + +USER user +WORKDIR /home/user + +ENV LANG C.UTF-8 diff --git a/.gitlab-ci/Dockerfile.gabble-osuselp b/.gitlab-ci/Dockerfile.gabble-osuselp new file mode 100644 index 000000000..6bed04b63 --- /dev/null +++ b/.gitlab-ci/Dockerfile.gabble-osuselp @@ -0,0 +1,29 @@ +FROM opensuse/leap:latest + +RUN zypper -q update -y \ + && zypper -q install -y -t pattern devel_basis \ + && zypper -q install -y ccache glib2-devel glib-networking \ + && zypper -q clean + +RUN zypper -q install -y telepathy-glib-devel telepathy-gabble \ + meson python3-Twisted python3-dbus-python git \ + libxslt-tools libnice-devel dbus-1-glib-devel \ + libxml2-devel libsoup-devel sqlite3-devel \ + cyrus-sasl-digestmd5 cyrus-sasl-plain gnutls \ + cyrus-sasl-devel cyrus-sasl-scram \ + && zypper -q clean + +# Debug the docker if required +#RUN zypper install -y valgrind gdb git vim + +ARG HOST_USER_ID=1000 +ENV HOST_USER_ID ${HOST_USER_ID} +RUN useradd -u $HOST_USER_ID -ms /bin/bash user +RUN ln -fs /usr/bin/python3 /usr/local/bin/python +RUN dbus-uuidgen --ensure + +USER user +WORKDIR /home/user + +ENV LANG C.UTF-8 + diff --git a/configure.ac b/configure.ac index 84eaff01b..a37248c35 100644 --- a/configure.ac +++ b/configure.ac @@ -130,6 +130,7 @@ if test "$with_ca_certificates" = "no"; then else if test -z "$with_ca_certificates"; then for f in /etc/pki/tls/certs/ca-bundle.crt \ + /etc/ssl/ca-bundle.pem \ /etc/ssl/certs/ca-certificates.crt; do if test -f "$f"; then with_ca_certificates="$f" @@ -293,7 +294,7 @@ if test -z "$XSLTPROC"; then AC_MSG_ERROR([xsltproc (from the libxslt source package) is required]) fi -AM_PATH_PYTHON([2.5]) +AM_PATH_PYTHON([3.6]) # Check for a Python >= 2.5 with Twisted, to run the tests AC_MSG_CHECKING([for Python with Twisted and XMPP protocol support]) diff --git a/lib/ext/wocky b/lib/ext/wocky index 64062d2f0..862ed2fe9 160000 --- a/lib/ext/wocky +++ b/lib/ext/wocky @@ -1 +1 @@ -Subproject commit 64062d2f0119b89d15e7bb0e0f6fa54ec23ff7f8 +Subproject commit 862ed2fe9c9c57c0eabcff81642d523e1fad2dcd diff --git a/src/im-factory.c b/src/im-factory.c index 8e0d3bbe1..84ad820bc 100644 --- a/src/im-factory.c +++ b/src/im-factory.c @@ -217,6 +217,23 @@ im_factory_message_cb ( gboolean create_if_missing; gboolean sent; + /* CVE-2017-5589+ verification */ + if (wocky_node_get_child_ns (wocky_stanza_get_top_node (message), "received", NS_CARBONS) + || wocky_node_get_child_ns (wocky_stanza_get_top_node (message), "sent", NS_CARBONS)) + { + if ((from = wocky_stanza_get_from (message)) != NULL) + { + TpBaseConnection *conn = TP_BASE_CONNECTION (fac->priv->conn); + TpHandleRepoIface *handles = tp_base_connection_get_handles (conn, + TP_HANDLE_TYPE_CONTACT); + TpHandle from_handle = tp_handle_ensure (handles, from, NULL, NULL); + TpHandle self_handle = tp_base_connection_get_self_handle (conn); + + if (from_handle != self_handle) + return FALSE; + } + } + if (!gabble_message_util_parse_incoming_message (message, &from, &to, &stamp, &msgtype, &id, &body, &state, &send_error, &delivery_status, &delivery_token, &sent)) return TRUE; diff --git a/src/muc-factory.c b/src/muc-factory.c index d53519b18..0a2fddcc9 100644 --- a/src/muc-factory.c +++ b/src/muc-factory.c @@ -782,6 +782,23 @@ muc_factory_message_cb ( TpDeliveryStatus delivery_status; gboolean sent; + /* CVE-2017-5589+ verification */ + if (wocky_node_get_child_ns (wocky_stanza_get_top_node (message), "received", NS_CARBONS) + || wocky_node_get_child_ns (wocky_stanza_get_top_node (message), "sent", NS_CARBONS)) + { + if ((from = wocky_stanza_get_from (message)) != NULL) + { + TpBaseConnection *conn = TP_BASE_CONNECTION (priv->conn); + TpHandleRepoIface *handles = tp_base_connection_get_handles (conn, + TP_HANDLE_TYPE_CONTACT); + TpHandle from_handle = tp_handle_ensure (handles, from, NULL, NULL); + TpHandle self_handle = tp_base_connection_get_self_handle (conn); + + if (from_handle != self_handle) + return FALSE; + } + } + /* FIXME: handle sent? */ if (!gabble_message_util_parse_incoming_message (message, &from, &to, &stamp, &msgtype, &id, &body, &state, &send_error, &delivery_status, &delivery_token, &sent)) diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am index 22cf954ab..17de005cf 100644 --- a/tests/twisted/Makefile.am +++ b/tests/twisted/Makefile.am @@ -88,6 +88,7 @@ TWISTED_TESTS = \ roster/test-roster.py \ roster/test-roster-subscribe.py \ roster/test-save-alias-to-roster.py \ + roster/test-roster-cache.py \ sasl/abort.py \ sasl/close.py \ sasl/complex.py \ @@ -111,6 +112,7 @@ TWISTED_TESTS = \ text/initiate.py \ text/initiate-requestotron.py \ text/receipts.py \ + text/markers.py \ text/respawn.py \ text/send-error.py \ text/send-to-correct-resource.py \ @@ -118,6 +120,7 @@ TWISTED_TESTS = \ text/test-text-delayed.py \ text/test-text-no-body.py \ text/test-text.py \ + text/test-carbons.py \ tls/legacy-jabber.py \ tls/server-tls-channel.py \ version.py \ @@ -163,6 +166,7 @@ TWISTED_VCARD_TESTS = \ vcard/test-avatar.py \ vcard/test-avatar-retrieved.py \ vcard/test-avatar-tokens.py \ + vcard/test-avatar-pep.py \ vcard/test-save-alias-to-vcard.py \ vcard/test-set-alias.py \ vcard/test-vcard-cache.py \ diff --git a/tests/twisted/gabbletest.py b/tests/twisted/gabbletest.py index 14538eddc..ccd43819f 100644 --- a/tests/twisted/gabbletest.py +++ b/tests/twisted/gabbletest.py @@ -25,6 +25,10 @@ import dbus +from OpenSSL import crypto +CA_CERT = os.environ.get('GABBLE_TWISTED_PATH', '.') + '/tls-cert.pem' +CA_KEY = os.environ.get('GABBLE_TWISTED_PATH', '.') + '/tls-key.pem' + def make_result_iq(stream, iq, add_query_node=True): result = IQ(stream, "result") result["id"] = iq["id"] @@ -180,6 +184,10 @@ def respondToSecondIq(self, iq): self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) class XmppAuthenticator(GabbleAuthenticator): + features = { + ns.NS_XMPP_BIND: 'bind', + ns.NS_XMPP_SESSION: 'session', + } def __init__(self, username, password, resource=None): GabbleAuthenticator.__init__(self, username, password, resource) self.authenticated = False @@ -197,10 +205,7 @@ def streamInitialize(self, root): self.xmlstream.sendHeader() def streamIQ(self): - features = elem(xmlstream.NS_STREAMS, 'features')( - elem(ns.NS_XMPP_BIND, 'bind'), - elem(ns.NS_XMPP_SESSION, 'session'), - ) + features = elem(xmlstream.NS_STREAMS, 'features')(*(elem(k,v) for k,v in self.features.items())) self.xmlstream.send(features) self.xmlstream.addOnetimeObserver( @@ -255,6 +260,43 @@ def bindIq(self, iq): def sessionIq(self, iq): self.xmlstream.send(make_result_iq(self.xmlstream, iq)) +class TlsAuthenticator(XmppAuthenticator): + def __init__(self, username, password, resource=None): + super().__init__(username, password, resource) + self.tls_encrypted = False + + def streamStarted(self, root=None): + if self.tls_encrypted: + XmppAuthenticator.streamStarted(self, root) + else: + self.streamInitialize(root) + self.streamTLS() + + def streamTLS(self): + features = domish.Element((xmlstream.NS_STREAMS, 'features')) + starttls = features.addElement((ns.NS_XMPP_TLS, 'starttls')) + starttls.addElement('required') + + self.xmlstream.send(features) + + self.xmlstream.addOnetimeObserver("/starttls", self.tlsAuth) + + def tlsAuth(self, auth): + with open(CA_KEY, 'rb') as f: + pem_key = f.read() + pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, pem_key, b"") + + with open(CA_CERT, 'rb') as f: + pem_cert = f.read() + cert = crypto.load_certificate(crypto.FILETYPE_PEM, pem_cert) + + tls_ctx = ssl.CertificateOptions(privateKey=pkey, certificate=cert) + + self.xmlstream.send(domish.Element((ns.NS_XMPP_TLS, 'proceed'))) + self.xmlstream.transport.startTLS(tls_ctx) + self.xmlstream.reset() + self.tls_encrypted = True + class StreamEvent(servicetest.Event): def __init__(self, type_, stanza, stream): servicetest.Event.__init__(self, type_, stanza=stanza) @@ -722,7 +764,13 @@ def signal_receiver(*args, **kw): d.addBoth((lambda *args: reactor.crash())) else: # please ignore the POSIX behind the curtain - d.addBoth((lambda *args: os._exit(1))) + def confess(*args): + from pprint import pprint + sys.stdout = sys.__stdout__ + print(traceback.format_exc(error)) + sys.stdout.flush() + os._exit(3) + d.addBoth((confess)) def exec_test(fun, params=None, protocol=None, timeout=None, diff --git a/tests/twisted/ns.py b/tests/twisted/ns.py index 30f9a8114..4b82dfc58 100644 --- a/tests/twisted/ns.py +++ b/tests/twisted/ns.py @@ -45,6 +45,7 @@ NS_XMPP_BIND = 'urn:ietf:params:xml:ns:xmpp-bind' NS_XMPP_TLS = 'urn:ietf:params:xml:ns:xmpp-tls' NS_XMPP_SESSION = 'urn:ietf:params:xml:ns:xmpp-session' +NS_XMPP_ROSTERVER = 'urn:xmpp:features:rosterver' OLPC_ACTIVITIES = "http://laptop.org/xmpp/activities" OLPC_ACTIVITIES_NOTIFY = "%s+notify" % OLPC_ACTIVITIES OLPC_ACTIVITY = "http://laptop.org/xmpp/activity" @@ -57,6 +58,7 @@ OLPC_CURRENT_ACTIVITY_NOTIFY = "%s+notify" % OLPC_CURRENT_ACTIVITY PUBSUB = "http://jabber.org/protocol/pubsub" PUBSUB_EVENT = "%s#event" % PUBSUB +NS_CHAT_MARKERS = "urn:xmpp:chat-markers:0" RECEIPTS = "urn:xmpp:receipts" REGISTER = "jabber:iq:register" ROSTER = "jabber:iq:roster" diff --git a/tests/twisted/power-save.py b/tests/twisted/power-save.py index 496ac3d96..b27ddf883 100644 --- a/tests/twisted/power-save.py +++ b/tests/twisted/power-save.py @@ -6,7 +6,7 @@ from gabbletest import exec_test, GoogleXmlStream, make_result_iq, \ send_error_reply, disconnect_conn, make_presence, sync_stream, elem, \ - acknowledge_iq + acknowledge_iq, XmppAuthenticator, StreamEvent from servicetest import call_async, assertEquals, EventPattern, \ assertContains, sync_dbus import ns @@ -208,8 +208,52 @@ def test_disconnect(q, bus, conn, stream): disconnect_conn(q, conn, stream) +NS_CSI="urn:xmpp:csi:0" +class CsiAuthenticator(XmppAuthenticator): + def __init__(self): + super().__init__('test', 'pass') + + def streamIQ(self): + super().streamIQ() + self.xmlstream.addObserver(f"/active[@xmlns='{NS_CSI}']", + lambda x: self.xmlstream.event_func(StreamEvent('stream-active', x, self.xmlstream))) + self.xmlstream.addObserver(f"/inactive[@xmlns='{NS_CSI}']", + lambda x: self.xmlstream.event_func(StreamEvent('stream-inactive', x, self.xmlstream))) + + +def test_csi(q, bus, conn, stream): + assertContains(cs.CONN_IFACE_POWER_SAVING, + conn.Get(cs.CONN, "Interfaces", + dbus_interface=cs.PROPERTIES_IFACE)) + + assertEquals (False, conn.Get(cs.CONN_IFACE_POWER_SAVING, + "PowerSavingActive", + dbus_interface=cs.PROPERTIES_IFACE)) + + call_async(q, conn.PowerSaving, 'SetPowerSaving', True) + + q.expect('stream-inactive') + + q.expect_many(EventPattern('dbus-return', method='SetPowerSaving'), + EventPattern('dbus-signal', signal='PowerSavingChanged', + args=[True])) + call_async(q, conn.PowerSaving, 'SetPowerSaving', False) + + q.expect('stream-active') + + q.expect_many( + EventPattern('dbus-return', method='SetPowerSaving'), + EventPattern('dbus-signal', signal='PowerSavingChanged', + args=[False])) + + if __name__ == '__main__': exec_test(test, protocol=GoogleXmlStream) exec_test(test_local_queueing) exec_test(test_error, protocol=GoogleXmlStream) exec_test(test_disconnect, protocol=GoogleXmlStream) + + # The same powersave engine is used hence only checking + # CSI signaling + XmppAuthenticator.features[NS_CSI] = 'csi' + exec_test(test_csi, authenticator=CsiAuthenticator()) diff --git a/tests/twisted/roster/meson.build b/tests/twisted/roster/meson.build index c4be6dea3..60fd48fca 100644 --- a/tests/twisted/roster/meson.build +++ b/tests/twisted/roster/meson.build @@ -12,4 +12,5 @@ twisted_tests += files([ 'test-roster.py', 'test-roster-subscribe.py', 'test-save-alias-to-roster.py', + 'test-roster-cache.py', ]) diff --git a/tests/twisted/roster/test-roster-cache.py b/tests/twisted/roster/test-roster-cache.py new file mode 100644 index 000000000..47962c7c4 --- /dev/null +++ b/tests/twisted/roster/test-roster-cache.py @@ -0,0 +1,211 @@ +""" +Test basic roster functionality. +""" + +from gabbletest import exec_test, make_result_iq, XmppAuthenticator +from rostertest import check_contact_roster, contacts_changed_predicate, make_roster_push +from servicetest import (assertEquals, call_async) +import constants as cs +import ns + +contacts = [ + ('amy@foo.com', cs.SUBSCRIPTION_STATE_YES, cs.SUBSCRIPTION_STATE_YES, ''), + ('bob@foo.com', cs.SUBSCRIPTION_STATE_NO, cs.SUBSCRIPTION_STATE_YES, ''), + ('che@foo.com', cs.SUBSCRIPTION_STATE_YES, cs.SUBSCRIPTION_STATE_NO, ''), + ] + +def verify(q, conn, groups=set()): + # slight implementation detail: TpBaseContactList emits ContactsChanged + # before it announces its channels + q.expect('dbus-signal', signal='ContactsChangedWithID', + interface=cs.CONN_IFACE_CONTACT_LIST, path=conn.object_path, + predicate=lambda e: contacts_changed_predicate(e, conn, contacts)) + + if groups: + s = q.expect('dbus-signal', signal='GroupsCreated') + assertEquals(set(groups), set(s.args[0])) + + + handles = conn.get_contact_handles_sync([ c[0] for c in contacts]) + + # this is emitted last, so clients can tell when the initial state dump + # has finished + q.expect('dbus-signal', signal='ContactListStateChanged', + args=[cs.CONTACT_LIST_STATE_SUCCESS]) + + call_async(q, conn.ContactList, 'GetContactListAttributes', [], False) + r = q.expect('dbus-return', method='GetContactListAttributes') + attribs = dict( + (handles[i],{ + cs.CONN_IFACE_CONTACT_LIST + '/subscribe': contacts[i][1], + cs.CONN_IFACE_CONTACT_LIST + '/publish': contacts[i][2], + cs.CONN + '/contact-id': contacts[i][0] + }) for i in range(0,len(contacts)) ) + assertEquals((attribs,), r.value) + +def test_create(q, bus, conn, stream): + + call_async(q, conn.ContactList, 'GetContactListAttributes', [], False) + q.expect('dbus-error', method='GetContactListAttributes', + name=cs.NOT_YET) + + event = q.expect('stream-iq', query_ns=ns.ROSTER) + + event.stanza['type'] = 'result' + event.stanza.query['ver']='zero' + + item = event.query.addElement('item') + item['jid'] = 'amy@foo.com' + item['subscription'] = 'both' + + item = event.query.addElement('item') + item['jid'] = 'bob@foo.com' + item['subscription'] = 'from' + + item = event.query.addElement('item') + item['jid'] = 'che@foo.com' + item['subscription'] = 'to' + + stream.send(event.stanza) + + # Regression test for : + # some super-buggy XMPP server running on vk.com sends its reply to our + # roster query twice. This used to crash Gabble. + stream.send(event.stanza) + + verify(q, conn) + +def test_cache1(q, bus, conn, stream): + call_async(q, conn.ContactList, 'GetContactListAttributes', [], False) + q.expect('dbus-error', method='GetContactListAttributes', + name=cs.NOT_YET) + + event = q.expect('stream-iq', query_ns=ns.ROSTER) + assert event.stanza.query['ver'] == 'zero', event.stanza.query + + stream.send(make_result_iq(stream, event.stanza, False)) + + verify(q, conn) + + amy, bob, che = conn.get_contact_handles_sync( + ['amy@foo.com', 'bob@foo.com', 'che@foo.com']) + + ## Technically roster is only cached on server push, as server needs to + ## push version tag. Below is just to test end to end flow, but server + ## push alone (as if from the other client) is sufficient. + + # Remove che from this client + call_async(q, conn.ContactList, 'RemoveContacts', [che]) + + event = q.expect('stream-iq', query_ns=ns.ROSTER) + # Server needs to confirm removal + push = make_roster_push(stream, 'che@foo.com', 'remove') + push.query['ver'] = 'one' + stream.send(push) + # And ack the push + stream.send(make_result_iq(stream, event.stanza, False)) + + # Now wait for contact to be removed from the roster + q.expect('dbus-signal', signal='ContactsChangedWithID', + args=[{},{},{che: 'che@foo.com'}]) + + # And bob was removed from another client, here's server's confirmation + push = make_roster_push(stream, 'bob@foo.com', 'remove') + push.query['ver'] = 'two' + stream.send(push) + + # Now again wait for contact to be removed from the roster + q.expect('dbus-signal', signal='ContactsChangedWithID', + args=[{},{},{bob: 'bob@foo.com'}]) + + call_async(q, conn.ContactList, 'GetContactListAttributes', [], False) + r = q.expect('dbus-return', method='GetContactListAttributes') + assertEquals(({ + amy: { + cs.CONN_IFACE_CONTACT_LIST + '/subscribe': cs.SUBSCRIPTION_STATE_YES, + cs.CONN_IFACE_CONTACT_LIST + '/publish': cs.SUBSCRIPTION_STATE_YES, + cs.CONN + '/contact-id': 'amy@foo.com', + }, + },), r.value) + # Roster should be at version two by now - commit changes to the server + contacts.pop() + contacts.pop() + +def test_cache2(q, bus, conn, stream): + event = q.expect('stream-iq', query_ns=ns.ROSTER) + assert event.stanza.query['ver'] == 'two', event.stanza.query + + stream.send(make_result_iq(stream, event.stanza, False)) + + verify(q, conn) + + amy = conn.get_contact_handle_sync(contacts[0][0]) + # Add Amy to some groups + push = make_roster_push(stream, contacts[0][0], 'both') + push.query['ver'] = 'two-and-half' + push.query.item.addElement('group', content='ladies') + push.query.item.addElement('group', content='people starting with A') + stream.send(push) + + # but reaction should be the same + s = q.expect('dbus-signal', signal='GroupsCreated') + assertEquals(set(('ladies', 'people starting with A')), set(s.args[0])) + + q.expect('stream-iq', iq_type='result', iq_id='push') + + # Also let request subscription from bob again, for simplicity just by + # doing roster-push - i.e. from some other client + push = make_roster_push(stream, 'bob@foo.com', 'none', True, 'Rob') + push.query['ver'] = 'three' + stream.send(push) + q.expect('stream-iq', iq_id='push') + # Commit the change to the server roster + contacts.append(('bob@foo.com', cs.SUBSCRIPTION_STATE_ASK, cs.SUBSCRIPTION_STATE_NO, '')) + +def test_cache3(q, bus, conn, stream): + event = q.expect('stream-iq', query_ns=ns.ROSTER) + assert event.stanza.query['ver'] == 'three', event.stanza.query + + stream.send(make_result_iq(stream, event.stanza, False)) + + verify(q, conn, ('ladies', 'people starting with A')) + + # First let's check the name (Alias) was properly recovered from the cache + bob = conn.get_contact_handle_sync(contacts[1][0]) + assert conn.Aliasing.RequestAliases([bob]) == ['Rob'] + + # Now bob approves our request and server pushes new roster to us + push = make_roster_push(stream, 'bob@foo.com', 'to', False, 'Rob') + push.query['ver'] = 'three-and-half' + stream.send(push) + contacts.pop() + contacts.append(('bob@foo.com', cs.SUBSCRIPTION_STATE_YES, cs.SUBSCRIPTION_STATE_NO, '')) + q.expect('dbus-signal', signal='ContactsChangedWithID', args=[{bob: contacts[1][1::]}, {bob: contacts[1][0]}, {}]) + q.expect('stream-iq', iq_id='push') + + # and che requests to be added again - and we approve it (which triggers roster push) + push = make_roster_push(stream, 'che@foo.com', 'from') + push.query['ver'] = 'four' + stream.send(push) + contacts.append(('che@foo.com', cs.SUBSCRIPTION_STATE_NO, cs.SUBSCRIPTION_STATE_YES, '')) + che = conn.get_contact_handle_sync(contacts[2][0]) + q.expect('dbus-signal', signal='ContactsChangedWithID', args=[{che: contacts[2][1::]}, {che: contacts[2][0]}, {}]) + q.expect('stream-iq', iq_id='push') + + +def test_cache4(q, bus, conn, stream): + event = q.expect('stream-iq', query_ns=ns.ROSTER) + assert event.stanza.query['ver'] == 'four', event.stanza.query + + stream.send(make_result_iq(stream, event.stanza, False)) + + verify(q, conn, ('ladies', 'people starting with A')) + + +if __name__ == '__main__': + XmppAuthenticator.features[ns.NS_XMPP_ROSTERVER]='ver' + exec_test(test_create) # populate initial roster + exec_test(test_cache1) # recover initial roster, check, remove some items + exec_test(test_cache2) # ensure removed is gone, add groups and request + exec_test(test_cache3) # ensure groups and requested item are there, add moar + exec_test(test_cache4) # ensure old request is processed and new is added diff --git a/tests/twisted/run-test.sh.in b/tests/twisted/run-test.sh.in index 5392397a7..ba17b040f 100644 --- a/tests/twisted/run-test.sh.in +++ b/tests/twisted/run-test.sh.in @@ -39,6 +39,9 @@ else export GABBLE_TWISTED_PATH fi +GABBLE_TWISTED_BDIR=$GABBLE_TWISTED_PATH +export GABBLE_TWISTED_BDIR + if [ -n "$1" ] ; then list="$1" else diff --git a/tests/twisted/text/markers.py b/tests/twisted/text/markers.py new file mode 100644 index 000000000..11ddbd27e --- /dev/null +++ b/tests/twisted/text/markers.py @@ -0,0 +1,230 @@ +# coding=utf-8 +""" +Test XEP-0333 markers. +""" + +from servicetest import ( + EventPattern, assertEquals, assertLength, sync_dbus, wrap_channel, +) +from gabbletest import exec_test, elem, sync_stream, acknowledge_iq +import constants as cs +import ns + +import caps_helper +import rostertest +from presence.invisible_helper import Xep0186Stream + +CONTACT = 'guybrush@mi.lit' +CONTACT_FULL_JID = CONTACT + '/Sea Cucumber' + +def send_received_report(stream, jid, received_id): + stream.send( + elem('message', from_=jid)( + elem(ns.NS_CHAT_MARKERS, 'received', id=received_id) + )) + +def marker_received_on_open_channel(q, bus, conn, stream, chan): + received_id = 'fine-leather-jackets' + + send_received_report(stream, CONTACT, received_id) + e = q.expect('dbus-signal', signal='MessageReceived', path=chan.object_path) + message, = e.args + header, = message + + assertEquals(cs.MT_DELIVERY_REPORT, header['message-type']) + assertEquals(cs.DELIVERY_STATUS_DELIVERED, header['delivery-status']) + assertEquals(received_id, header['delivery-token']) + +def marker_ignored_without_channel(q, bus, conn, stream): + q.forbid_events([EventPattern('dbus-signal', signal='MessageReceived')]) + send_received_report(stream, 'marley@mi.gov', 'only-one-candidate') + sync_dbus(bus, q, conn) + q.unforbid_all() + +def not_sending_request_to_contact(q, bus, conn, stream, chan): + message = [ + { 'message-type': cs.MT_NORMAL, + }, + { 'content-type': 'text/plain', + 'content': 'Mancomb Seepgood?', + }] + chan.Messages.SendMessage(message, 0) + + e = q.expect('stream-message', to=CONTACT) + assertLength(0, list(e.stanza.elements(uri=ns.NS_CHAT_MARKERS, name='markable'))) + +def sending_request_to_presenceless_contact(q, bus, conn, stream, chan): + """ + Initially we know nothing of Guybrush's presence, so should just try our + level best if asked to. + """ + message = [ + { 'message-type': cs.MT_NORMAL, + }, + { 'content-type': 'text/plain', + 'content': 'Thriftweed?', + }] + chan.Messages.SendMessage(message, cs.MSG_SENDING_FLAGS_REPORT_READ) + + e = q.expect('stream-message', to=CONTACT) + assertLength(1, list(e.stanza.elements(uri=ns.NS_CHAT_MARKERS, name='markable'))) + +def sending_request_to_cappy_contact(q, bus, conn, stream, chan): + """ + Test that Gabble requests a marker from a contact whom we know supports + this extension, but only if asked. + """ + + # Convince Gabble that Guybrush supports this extension + caps = { 'node': 'http://whatever', + # FIXME: should be markers, not receipt, see #23 + #'ver': caps_helper.compute_caps_hash([], [ns.NS_CHAT_MARKERS], {}), + 'ver': caps_helper.compute_caps_hash([], [ns.NS_CHAT_MARKERS, ns.RECEIPTS], {}), + 'hash': 'sha-1', + } + caps_helper.presence_and_disco(q, conn, stream, CONTACT_FULL_JID, + disco=True, + client=caps['node'], + caps=caps, + features=[ns.NS_CHAT_MARKERS, ns.RECEIPTS]) + sync_stream(q, stream) + + # Don't ask, don't tell — even if we know Guybrush does support this. + not_sending_request_to_contact(q, bus, conn, stream, chan) + + # Ask, tell. + message = [ + { 'message-type': cs.MT_NORMAL, + }, + { 'content-type': 'text/plain', + 'content': 'Ulysses?', + }] + chan.Messages.SendMessage(message, cs.MSG_SENDING_FLAGS_REPORT_READ) + + e = q.expect('stream-message', to=CONTACT) + assertLength(1, list(e.stanza.elements(uri=ns.NS_CHAT_MARKERS, name='markable'))) + +def replying_to_requests(q, bus, conn, stream): + jid = 'lechuck@lucasarts.lit' + + # We shouldn't send markers to people who aren't on our roster. + q.forbid_events([EventPattern('stream-message', to=jid)]) + stream.send( + elem('message', from_=jid, type='chat', id='alpha')( + elem('body')( + u"You didn't kill me, you moron!" + ), + elem(ns.NS_CHAT_MARKERS, 'markable') + )) + + event = q.expect('dbus-signal', signal='NewChannels') + path, props = event.args[0][0] + text_chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text') + event = q.expect('dbus-signal', signal='MessageReceived') + message = event.args[0] + header, body = message + message_id = header['pending-message-id'] + # FIXME: uncomment below once #23 is fixed + #text_chan.Text.AcknowledgePendingMessages([message_id]) + + sync_stream(q, stream) + q.unforbid_all() + + # We should send markers to people on our roster, seeing as we're not + # invisible. + rostertest.send_roster_push(stream, jid, subscription='from') + + stream.send( + elem('message', from_=jid, type='chat', id='beta')( + elem('body')( + u"You've just destroyed my spiritual essences." + ), + elem(ns.NS_CHAT_MARKERS, 'markable') + )) + event = q.expect('dbus-signal', signal='MessageReceived') + message = event.args[0] + header, body = message + message_id = header['pending-message-id'] + text_chan.Text.AcknowledgePendingMessages([message_id]) + + e = q.expect('stream-message', to=jid) + receipt = next(e.stanza.elements(uri=ns.NS_CHAT_MARKERS, name='displayed')) + assertEquals('beta', receipt['id']) + # FIXME: there will be alpha now coming as well, as when we acked recent it acks previous + q.expect('stream-message', to=jid) + + # We would like requests in messages without id=''s not to crash Gabble, + # and also for it not to send a reply. + q.forbid_events([EventPattern('stream-message', to=jid)]) + stream.send( + elem('message', from_=jid, type='chat')( # NB. no id='' attribute + elem('body')( + u"A favor that I shall now return!" + ), + elem(ns.NS_CHAT_MARKERS, 'markable') + )) + event = q.expect('dbus-signal', signal='MessageReceived') + message = event.args[0] + header, body = message + message_id = header['pending-message-id'] + text_chan.Text.AcknowledgePendingMessages([message_id]) + + sync_stream(q, stream) + q.unforbid_all() + + # If we're invisible, LeChuck shouldn't get markers. + conn.SimplePresence.SetPresence("hidden", "") + event = q.expect('stream-iq', query_name='invisible') + acknowledge_iq(stream, event.stanza) + + q.forbid_events([EventPattern('stream-message', to=jid)]) + stream.send( + elem('message', from_=jid, type='chat', id='epsilon')( + elem('body')( + u"… but where am I going to find a duck wearing burlap chaps?" + ), + elem(ns.NS_CHAT_MARKERS, 'markable') + )) + event = q.expect('dbus-signal', signal='MessageReceived') + message = event.args[0] + header, body = message + message_id = header['pending-message-id'] + # FIXME: same as at the beginning - uncomment when fixed + #text_chan.Text.AcknowledgePendingMessages([message_id]) + + sync_stream(q, stream) + q.unforbid_all() + +def test(q, bus, conn, stream): + path = conn.Requests.CreateChannel( + { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.TARGET_ID: CONTACT, + })[0] + chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text') + + # Let's start out with an empty roster, eh? + e = q.expect('stream-iq', iq_type='get', query_ns=ns.ROSTER) + e.stanza['type'] = 'result' + stream.send(e.stanza) + + marker_received_on_open_channel(q, bus, conn, stream, chan) + + # FIXME: not implemented because of partial implementation + # See https://github.com/TelepathyIM/telepathy-gabble/issues/23 for details + #report_ignored_without_channel(q, bus, conn, stream) + + not_sending_request_to_contact(q, bus, conn, stream, chan) + + # FIXME: it still relies on receipts support which is still unable to + # identify whether message's contact is unknown or has no required cap + #sending_request_to_presenceless_contact(q, bus, conn, stream, chan) + + sending_request_to_cappy_contact(q, bus, conn, stream, chan) + + replying_to_requests(q, bus, conn, stream) + + +if __name__ == '__main__': + params = {'send-chat-markers':True} + exec_test(test, params=params, protocol=Xep0186Stream) diff --git a/tests/twisted/text/meson.build b/tests/twisted/text/meson.build index 273c09652..d31874f7c 100644 --- a/tests/twisted/text/meson.build +++ b/tests/twisted/text/meson.build @@ -5,6 +5,7 @@ twisted_tests += files([ 'initiate.py', 'initiate-requestotron.py', 'receipts.py', + 'markers.py', 'respawn.py', 'send-error.py', 'send-to-correct-resource.py', @@ -12,4 +13,5 @@ twisted_tests += files([ 'test-text-delayed.py', 'test-text-no-body.py', 'test-text.py', + 'test-carbons.py', ]) diff --git a/tests/twisted/text/test-carbons.py b/tests/twisted/text/test-carbons.py new file mode 100644 index 000000000..b19d6e53a --- /dev/null +++ b/tests/twisted/text/test-carbons.py @@ -0,0 +1,221 @@ + +""" +Test text channel with carbons. +""" + +from gabbletest import XmppXmlStream, exec_test, elem, acknowledge_iq +from servicetest import (EventPattern, wrap_channel, assertEquals, assertLength, + assertContains, sync_dbus) +import constants as cs + +NS_CARBONS = 'urn:xmpp:carbons:2' +NS_FORWARD = 'urn:xmpp:forward:0' + +class CarbonStream(XmppXmlStream): + disco_features = [ + NS_CARBONS, + ] +# Same as test-text.py but using carbon-forwarded message envelopes +def test(q, bus, conn, stream): + id = '1845a1a9-f7bc-4a2e-a885-633aadc81e1b' + + iq = q.expect('stream-iq', iq_type='set', query_ns=NS_CARBONS) + assert iq.stanza.enable + acknowledge_iq(stream, iq.stanza) + + # hello + msg = elem('message', type='chat', from_='test@localhost')( + elem(NS_CARBONS, 'received')( + elem(NS_FORWARD, 'forwarded')( + elem('jabber:client','message', id=id, from_='foo@bar.com/Pidgin', type='chat')( + elem('body')('hello') + ) + ) + ) + ) + stream.send(msg) + + event = q.expect('dbus-signal', signal='NewChannels') + path, props = event.args[0][0] + text_chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text') + assertEquals(cs.CHANNEL_TYPE_TEXT, props[cs.CHANNEL_TYPE]) + assertEquals(cs.HT_CONTACT, props[cs.TARGET_HANDLE_TYPE]) + foo_at_bar_dot_com_handle = props[cs.TARGET_HANDLE] + jid = conn.inspect_contact_sync(foo_at_bar_dot_com_handle) + assertEquals('foo@bar.com', jid) + + # Exercise basic Channel Properties from spec 0.17.7 + channel_props = text_chan.Properties.GetAll(cs.CHANNEL) + assertEquals(props[cs.TARGET_HANDLE], channel_props.get('TargetHandle')) + assertEquals(cs.HT_CONTACT, channel_props.get('TargetHandleType')) + assertEquals(cs.CHANNEL_TYPE_TEXT, channel_props.get('ChannelType')) + assertContains(cs.CHANNEL_IFACE_CHAT_STATE, channel_props.get('Interfaces')) + assertContains(cs.CHANNEL_IFACE_MESSAGES, channel_props.get('Interfaces')) + assertEquals(jid, channel_props['TargetID']) + assertEquals(False, channel_props['Requested']) + assertEquals(props[cs.INITIATOR_HANDLE], channel_props['InitiatorHandle']) + assertEquals(jid, channel_props['InitiatorID']) + + message_received = q.expect('dbus-signal', signal='MessageReceived') + + # Check that C.I.Messages.MessageReceived looks right. + message = message_received.args[0] + + # message should have two parts: the header and one content part + assert len(message) == 2, message + header, body = message + + assert header['message-sender'] == foo_at_bar_dot_com_handle, header + # the spec says that message-type "MAY be omitted for normal chat + # messages." + assert 'message-type' not in header or header['message-type'] == 0, header + + # We don't make any uniqueness guarantees about the tokens on incoming + # messages, so we use the id='' provided at the protocol level. + assertEquals(id, header['message-token']) + + assert body['content-type'] == 'text/plain', body + assert body['content'] == 'hello', body + + # Remove the message from the pending message queue, and check that + # PendingMessagesRemoved fires. + message_id = header['pending-message-id'] + + text_chan.Text.AcknowledgePendingMessages([message_id]) + + removed = q.expect('dbus-signal', signal='PendingMessagesRemoved') + + removed_ids = removed.args[0] + assert len(removed_ids) == 1, removed_ids + assert removed_ids[0] == message_id, (removed_ids, message_id) + + # And now let's try a message with a malformed type='' attribute. + malformed = elem('message')(elem(NS_CARBONS, 'received')(elem(NS_FORWARD,'forwarded')( + elem('message', from_='foo@bar.com/fubber', type="'")( + elem('body')(u'Internettt!'), + elem('subject')(u'xyzzy'), + elem('thread')(u'6666'), + ) + ))) + stream.send(malformed) + + event = q.expect('dbus-signal', signal='MessageReceived') + message, = event.args + assertLength(2, message) + header, body = message + + # Gabble should treat the unparseable type as if it were 'normal' or + # omitted (not to be confused with Telepathy's Normal, which is 'chat' in + # XMPP...) + assertEquals(cs.MT_NOTICE, header['message-type']) + + # In addition to above standard tests, the tricky one is 'sent' carbon + sent_token = id[:8:][::-1]+id[8::] + msg = elem('message', type='chat', from_='test@localhost')( + elem(NS_CARBONS, 'sent')( + elem(NS_FORWARD, 'forwarded')( + elem('jabber:client','message', id=sent_token, from_='test@localhost/Gabble', to='foo@bar.com/Pidgin')( + elem('body')('what up') + ) + ) + ) + ) + stream.send(msg) + message_received = q.expect('dbus-signal', signal='MessageReceived') + sent_message = message_received.args[0] + + assert len(sent_message) == 2, sent_message + header = sent_message[0] + assert header['message-type'] == 2, header # Notice + assert header['message-token'] == sent_token, header + assertEquals(conn.Properties.Get(cs.CONN, "SelfHandle"), header['message-sender']) + assertEquals('test@localhost', header['message-sender-id']) + body = sent_message[1] + assert body['content-type'] == 'text/plain', body + assert body['content'] == u'what up', body + + # Remove the message from the pending message queue, and check that + # PendingMessagesRemoved fires. + message_id = header['pending-message-id'] + + text_chan.Text.AcknowledgePendingMessages([message_id]) + + removed = q.expect('dbus-signal', signal='PendingMessagesRemoved') + + removed_ids = removed.args[0] + assert len(removed_ids) == 1, removed_ids + assert removed_ids[0] == message_id, (removed_ids, message_id) + + # And last one, normal chat (whatever that means) + di = id[::-1] + msg = elem('message', type='chat', from_='test@localhost')( + elem(NS_CARBONS, 'sent')( + elem(NS_FORWARD, 'forwarded')( + elem('jabber:client','message', id=di, type='chat', from_='test@localhost/Gabble', to='foo@bar.com/Pidgin')( + elem('body')('goodbye') + ) + ) + ) + ) + stream.send(msg) + message_received = q.expect('dbus-signal', signal='MessageReceived') + sent_message = message_received.args[0] + assert len(sent_message) == 2, sent_message + header = sent_message[0] + # the spec says that message-type "MAY be omitted for normal chat + # messages." + assert 'message-type' not in header or header['message-type'] == 0, header + assert header['message-token'] == di, header + assertEquals(conn.Properties.Get(cs.CONN, "SelfHandle"), header['message-sender']) + assertEquals('test@localhost', header['message-sender-id']) + body = sent_message[1] + assert body['content-type'] == 'text/plain', body + assert body['content'] == u'goodbye', body + + # Verify source protection + msg = elem('message', type='chat', from_='smith@matrix.org/agent712')( + elem(NS_CARBONS, 'received')( + elem(NS_FORWARD, 'forwarded')( + elem('jabber:client','message', id=id, from_='foo@bar.com/Pidgin', type='chat')( + elem('body')('Mr. Anderson!') + ) + ) + ) + ) + q.forbid_events([EventPattern('dbus-signal', signal='MessageReceived')]) + stream.send(msg) + sync_dbus(bus, q, conn) + q.unforbid_all() + + # And MUC - demo attack vector + msg = elem('message')( + elem(NS_CARBONS, 'received')( + elem(NS_FORWARD, 'forwarded')( + elem('jabber:client','message', id=sent_token, from_='foo@bar.com/Pidgin', to='test@localhost')( + elem('body')('oh btb') + ) + ) + ), + elem('jabber:x:conference', 'x', jid='room@localhost') + ) + stream.send(msg) + event = q.expect('stream-iq', iq_type='get', query_ns='http://jabber.org/protocol/disco#info', to='room@localhost') + + # MUC Invite Attack execution + msg = elem('message', from_='smith@matrix.org/agent712')( + elem(NS_CARBONS, 'received')( + elem(NS_FORWARD, 'forwarded')( + elem('jabber:client','message', id=sent_token, from_='foo@bar.com/Pidgin', to='test@localhost')( + elem('body')('Nice party here, really') + ) + ) + ), + elem('jabber:x:conference', 'x', jid='crimescene@set.up') + ) + q.forbid_events([EventPattern('dbus-signal', signal='MessageReceived')]) + stream.send(msg) + sync_dbus(bus, q, conn) + q.unforbid_all() + +if __name__ == '__main__': + exec_test(test, protocol=CarbonStream, params={'message-carbons':True}) diff --git a/tests/twisted/tls/server-tls-channel.py b/tests/twisted/tls/server-tls-channel.py index 9d019a489..19ff4c1fc 100644 --- a/tests/twisted/tls/server-tls-channel.py +++ b/tests/twisted/tls/server-tls-channel.py @@ -15,15 +15,13 @@ import dbus import os -from OpenSSL import crypto - from twisted.words.protocols.jabber import xmlstream from twisted.words.protocols.jabber.client import IQ from twisted.words.xish import domish, xpath from twisted.internet import ssl import ns -from gabbletest import exec_test, XmppAuthenticator +from gabbletest import exec_test, XmppAuthenticator, TlsAuthenticator from servicetest import ProxyWrapper, EventPattern from servicetest import assertEquals, assertLength, assertSameSets import constants as cs @@ -34,49 +32,6 @@ # the files are copied from wocky/tests/certs/tls-[cert,key].pem CA_CERT_HOSTNAME = 'weasel-juice.org' -CA_CERT = os.environ.get('GABBLE_TWISTED_PATH', '.') + '/tls-cert.pem' -CA_KEY = os.environ.get('GABBLE_TWISTED_PATH', '.') + '/tls-key.pem' - -class TlsAuthenticator(XmppAuthenticator): - def __init__(self, username, password, resource=None): - XmppAuthenticator.__init__(self, username, password, resource) - self.tls_encrypted = False - - def streamTLS(self): - features = domish.Element((xmlstream.NS_STREAMS, 'features')) - starttls = features.addElement((ns.NS_XMPP_TLS, 'starttls')) - starttls.addElement('required') - - self.xmlstream.send(features) - - self.xmlstream.addOnetimeObserver("/starttls", self.tlsAuth) - - def streamStarted(self, root=None): - self.streamInitialize(root) - - if self.authenticated and self.tls_encrypted: - self.streamIQ() - elif self.tls_encrypted: - self.streamSASL() - else: - self.streamTLS() - - def tlsAuth(self, auth): - with open(CA_KEY, 'rb') as f: - pem_key = f.read() - pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, pem_key, b"") - - with open(CA_CERT, 'rb') as f: - pem_cert = f.read() - cert = crypto.load_certificate(crypto.FILETYPE_PEM, pem_cert) - - tls_ctx = ssl.CertificateOptions(privateKey=pkey, certificate=cert) - - self.xmlstream.send(domish.Element((ns.NS_XMPP_TLS, 'proceed'))) - self.xmlstream.transport.startTLS(tls_ctx) - self.xmlstream.reset() - self.tls_encrypted = True - class ServerTlsChanWrapper(ProxyWrapper): def __init__(self, object, default=cs.CHANNEL, interfaces={ "ServerTLSConnection" : cs.CHANNEL_TYPE_SERVER_TLS_CONNECTION}): diff --git a/tests/twisted/tools/exec-with-log.sh.in b/tests/twisted/tools/exec-with-log.sh.in index 28968cad1..94c03d6b1 100644 --- a/tests/twisted/tools/exec-with-log.sh.in +++ b/tests/twisted/tools/exec-with-log.sh.in @@ -12,6 +12,8 @@ GABBLE_PLUGIN_DIR="@abs_top_builddir@/plugins/.libs" export GABBLE_PLUGIN_DIR WOCKY_CAPS_CACHE=:memory: export WOCKY_CAPS_CACHE +ROSTER_CACHE=:memory: +export ROSTER_CACHE WOCKY_CAPS_CACHE_SIZE=50 export WOCKY_CAPS_CACHE_SIZE G_MESSAGES_DEBUG=all diff --git a/tests/twisted/tools/gabble-debug.sh.in b/tests/twisted/tools/gabble-debug.sh.in index 99418d2b7..74a2049df 100755 --- a/tests/twisted/tools/gabble-debug.sh.in +++ b/tests/twisted/tools/gabble-debug.sh.in @@ -12,6 +12,8 @@ GABBLE_PLUGIN_DIR="@abs_top_builddir@/plugins:@abs_top_builddir@/plugins/console export GABBLE_PLUGIN_DIR WOCKY_CAPS_CACHE=:memory: export WOCKY_CAPS_CACHE +ROSTER_CACHE=:memory: +export ROSTER_CACHE WOCKY_CAPS_CACHE_SIZE=50 export WOCKY_CAPS_CACHE_SIZE G_MESSAGES_DEBUG=all diff --git a/tests/twisted/vcard/meson.build b/tests/twisted/vcard/meson.build index ff7ea6fe6..e301a3ce6 100644 --- a/tests/twisted/vcard/meson.build +++ b/tests/twisted/vcard/meson.build @@ -19,6 +19,7 @@ twisted_tests += files( 'test-avatar.py', 'test-avatar-retrieved.py', 'test-avatar-tokens.py', + 'test-avatar-pep.py', 'test-save-alias-to-vcard.py', 'test-set-alias.py', 'test-vcard-cache.py', diff --git a/tests/twisted/vcard/test-avatar-pep.py b/tests/twisted/vcard/test-avatar-pep.py new file mode 100644 index 000000000..2397bd96d --- /dev/null +++ b/tests/twisted/vcard/test-avatar-pep.py @@ -0,0 +1,65 @@ + +""" +Test PEP avatar support. +""" + +import base64 +import hashlib + +from servicetest import call_async, EventPattern, assertEquals, assertLength +from gabbletest import exec_test, make_result_iq, acknowledge_iq, elem +from caps_helper import receive_presence_and_ask_caps + +import constants as cs +import ns + +NS_XMPP_AVATAR_META='urn:xmpp:avatar:metadata' +NS_XMPP_AVATAR_DATA='urn:xmpp:avatar:data' +png_pxl_b64=b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==' + +def test(q, bus, conn, stream): + event, feats, forms, caps = receive_presence_and_ask_caps(q, stream, False) + assert NS_XMPP_AVATAR_META in feats and NS_XMPP_AVATAR_META+'+notify' in feats, feats + + png_pxl_raw = base64.b64decode(png_pxl_b64) + png_pxl_sha = hashlib.sha1(png_pxl_raw).hexdigest() + + msg = elem('message', from_='bob@foo.com')( + elem((ns.PUBSUB_EVENT), 'event')( + elem('items', node=NS_XMPP_AVATAR_META)( + elem('item', id=png_pxl_sha)( + elem(NS_XMPP_AVATAR_META, 'metadata')( + elem('info', bytes='70', id=png_pxl_sha, type='image/png') + ) + ) + ) + ) + ) + stream.send(msg) + + handle = conn.get_contact_handle_sync('bob@foo.com') + + event = q.expect('dbus-signal', signal='AvatarUpdated', args=[handle, png_pxl_sha]) + conn.Avatars.RequestAvatars([handle]) + + event = q.expect('stream-iq', to='bob@foo.com', iq_type='get', + query_ns=ns.PUBSUB, query_name='pubsub') + + items = event.query.firstChildElement() + assertEquals('items', items.name) + assertEquals(NS_XMPP_AVATAR_DATA, items['node']) + + result = make_result_iq(stream, event.stanza) + pubsub = result.firstChildElement() + items = pubsub.addElement('items') + items['node'] = NS_XMPP_AVATAR_DATA + item = items.addElement('item') + item['id'] = png_pxl_sha + item.addElement('data', NS_XMPP_AVATAR_DATA, content=png_pxl_b64.decode()) + stream.send(result) + + q.expect('dbus-signal', signal='AvatarRetrieved', + args=[handle, png_pxl_sha, png_pxl_raw, 'image/png']) + +if __name__ == '__main__': + exec_test(test)