- Erlang 63.6%
- Shell 19.9%
- Python 12.4%
- Makefile 2.1%
- Dockerfile 2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| directory | ||
| ejabberd | ||
| tests | ||
| .gitignore | ||
| compose.acme.yaml | ||
| compose.directory.yaml | ||
| compose.tackygram.yaml | ||
| compose.yaml | ||
| env.sh | ||
| Makefile | ||
| README.md | ||
anotherim-server
The XMPP server for the AnotherIM clients: ejabberd 26.09 (ProcessOne's release) with two modules of ours and one configuration for every deployment, run by compose (Docker or Podman) with the push daemon apushd beside it and, if you like, the Telegram gateway tackygram and the people directory. The Makefile builds the image and writes a deployment's environment and DNS zone.
anotherim-server/
Makefile image, publish, tests, env, dns, certs-selfsigned, up/down/logs/status
compose.yaml ejabberd and apushd
compose.acme.yaml port 80 for ejabberd's own Let's Encrypt, when CERTS=acme
compose.tackygram.yaml the Telegram gateway beside them, when TACKYGRAM=1
compose.directory.yaml the people directory beside them, when DIRECTORY=1
directory/Dockerfile the people directory's image: libtacky's tackyd, built from its source
env.sh make env: every file of a deployment, from its settings
ejabberd/
Dockerfile ejabberd 26.09-1 on Debian trixie; the modules compiled with the
release's erl, their tests run in the build
ejabberd.yml the configuration, carried by the image
build-modules.sh compiles the modules and runs their tests
test_runner.erl runs the tests under cover; starts the ejabberd they test on
modules/
mod_apush/ what a device is woken for, sealed to its key (a mod_push plugin)
mod_s3_upload/ file upload into an S3 bucket
mod_component_iq/ a component's iqs to pubsub and upload not dropped by mod_privilege
mod_schedule/ a message held and sent later as the user (urn:tacky:schedule:0)
mod_client_access/ XEP-0494: a user's sessions, FAST tokens and push registrations, listed and revoked
tests/ live tests of the image and their client
state/<domain>/ a deployment's settings, secrets, zone and certificate (not in git)
What the server does
| registration | In band (XEP-0077) behind a proof of work: an XEP-0158 SHA-256 hashcash, captcha_pow with mod_register. Invitations (XEP-0401, XEP-0445) register without one. One registration per IP per REGISTRATION_TIMEOUT. |
| login | SASL2 (XEP-0388) with Bind 2 and FAST tokens (XEP-0484); SCRAM-SHA-256 and -512, with channel binding, are the only password mechanisms, and passwords are stored only as those hashes. See "Login" below. |
| push | XEP-0357 by mod_push, with mod_apush deciding each push to another.im's apps (other clients' registrations, such as Conversations' or Monal's, are mod_push's alone): an incoming message or call only, the summary sealed to the device's key and naming the contact or room as the user's roster and bookmarks do; muted chats, mentions-only rooms and a grace period while the user is active elsewhere are held. A room's message that mentions the user (the nick, a XEP-0372 reference, or a XEP-0513 mention by occupant id, JID or #channel) is flagged mention in the summary and is never held by the window; a message sent quietly (urn:tacky:silent:0) is flagged silent, pushed all the same. A burst in one chat is one push at once and, after a 20-second window, one with the latest count. The App Server is apushd. Details: ejabberd/modules/mod_apush/README.md. |
| scheduled messages | mod_schedule (urn:tacky:schedule:0): a user's message held in Mnesia (replicated over a cluster, and sent once by whichever node claims it; SQL with db_type: sql) and sent at its time as the user's bare JID, into their archive and their devices' carbons; listed, changed, cancelled or sent now from any device, every change told to all of them. A year ahead, 100 per chat, 500 per account at most. The Telegram gateway's contacts are scheduled in Telegram itself (a submit for one is redirected there). Details: ejabberd/modules/mod_schedule/README.md. |
| sessions | mod_client_access, XEP-0494 Client Access Management (urn:xmpp:cam:0, 0.1.0) for the user's own account: the live sessions (device and software as the SASL2 user agent named them, address, resource, since when), sessions waiting for XEP-0198 resumption, clients holding a FAST token and push registrations, each a client with first and last seen; revoking one deletes its tokens and push registrations and ends its sessions (a hibernated one not resumable), so only the password lets it back. A client not connected is remembered 30 days, its address not kept. Details: ejabberd/modules/mod_client_access/README.md. |
| uploads | XEP-0363 on upload.<domain> by mod_s3_upload, into an S3 bucket run elsewhere. See "Uploads" below. |
| Telegram | Optional: the gateway tackygram as the component telegram.<domain>, with the XEP-0356 privileges mod_privilege grants that name alone. See "Telegram" below. |
| sticker packs | A catalogue of public XEP-0449 packs on pubsub.<domain>, published by the admin (libtacky's tackyd stickers publish) and by the Telegram gateway (sets its users import). See "Sticker packs" below. |
| people directory | Optional: people.<domain>, a component that finds a person by name, phone number or personal code among the users who listed themselves, and an imported registry. See "People directory" below. |
| file transfer | A SOCKS5 bytestreams proxy (XEP-0065, mod_proxy65) as proxy.<domain> on port 7777, for the local users: the relay a Jingle file transfer's SOCKS5 transport (XEP-0260) falls back on when the two peers cannot connect to each other. It names the server's public IP as its streamhost; each transfer is held to 2 MiB/s (the admin's not), a user to ten at once. |
| calls | STUN and TURN on 3478 (udp and tcp, relay ports 49160-49199 udp); mod_stun_disco (XEP-0215) gives every client temporary TURN credentials. |
| the rest | Message archive (MAM) in SQLite, MUC on conference.<domain>, PEP and bookmarks, carbons, avatars, blocking, stream management, client state, server pings (XEP-0199: a client silent for two minutes is pinged and, unanswered in 32 seconds, disconnected; one with stream management is asked for an acknowledgement instead, given 60 seconds, and its session then held for resumption, five minutes, or mod_push_keepalive's 24 hours with push enabled), vCard, the WebSocket and host-meta on 5443, the admin API on the loopback's 5280. |
Everything but the archive is in mnesia. Past a few thousand users the
archive moves to PostgreSQL: sql_type: pgsql and its connection options.
Running a server
make env DOMAIN=chat.example.org IP=203.0.113.7 CERTS=acme
make dns DOMAIN=chat.example.org IP=203.0.113.7 IP6=2001:db8::7
make up DOMAIN=chat.example.org
make status DOMAIN=chat.example.org # also: make logs, make down
make env writes everything a deployment is into state/<domain>/. make up runs compose on it:
docker compose --env-file state/chat.example.org/compose.env up -d # or podman compose
That is all make up does, but for asking for the certificate once with
CERTS=acme (below), so compose can be used the same way by hand.
IP is the server's public IPv4 address: the TURN relay's, and the zone's.
Images
| image | from | default |
|---|---|---|
| the server | make image here |
dev.another.im/another-im/anotherim-server:latest |
| apushd | its own repository (make image there) |
dev.another.im/another-im/apushd:latest |
tackygram, with TACKYGRAM=1 |
its own repository (make image there) |
dev.another.im/another-im/tackygram:latest |
the people directory, with DIRECTORY=1 |
make directory-image here |
dev.another.im/another-im/people-directory:latest |
They are on the registry at dev.another.im: make publish pushes each from
its own repository (the directory's is make directory-publish here),
after you log in to dev.another.im. A server pulls what it does not have.
Other names are IMAGE=, APUSHD_IMAGE=, TACKYGRAM_IMAGE= and
DIRECTORY_IMAGE= at make env.
Certificate
Two ways, chosen at make env:
CERTS=files, the default. Put PEM files instate/<domain>/certs/: your own certificate (a wildcard one is fine) with its chain and key, in one file or several, readable by everyone (0644), since ejabberd runs as uid 9000. For a trial,make certs-selfsigned DOMAIN=...makes one.CERTS=acme. ejabberd gets a Let's Encrypt certificate itself, for the domain andconference.<domain>, over HTTP-01 on port 80, which then has to reach this host for both names (make dns's records).make upasks for it once ejabberd has started, when ejabberd holds none, and stops with ejabberd's error if it fails; run it again once DNS and port 80 are right. The certificate and the ACME account key are kept in thedatabasevolume, beside mnesia's files, and ejabberd renews the certificate in place a week before it expires, with no restart and no other container. It asks for those two names only, not for every name it routes (acme: autowould addpubsub.,upload.and the components, which have no address here).ACME_URLatmake envnames another ACME directory; Let's Encrypt's is the default.
Configuration
ejabberd/ejabberd.yml is the same everywhere. A deployment is ejabberd's
EJABBERD_MACRO_<NAME> variables in two files the container gets:
-
settings.env, rewritten by everymake env:variable default DOMAINthe XMPP domain required IPthe server's public IPv4 address: the TURN relay's, and the streamhost the bytestreams proxy names required POW_DIFFICULTYhashcash hex digits; 6 is a few seconds for a client 6 REGISTRATION_TIMEOUTseconds between registrations per IP, or infinity600 UPLOAD_URLthe upload bucket, path style https://upload.<domain>/files/ACME_URLthe ACME directory, with CERTS=acmeLet's Encrypt's -
secrets.env(0600), made once with the component and STUN secrets. The upload store's key is added to it by hand (see "Uploads"). -
compose.env, compose's own variables: the project's name, the compose files, the domain and the images.
Beside them are apushd/ (Push, below) and, with TACKYGRAM=1,
tackygram/ (Telegram, below), with DIRECTORY=1 directory/ (People
directory, below). All of it is written by env.sh, which
make env runs and whose header lists every file and setting. It checks
what it is given before it writes anything, so a refused make env
changes nothing, and running it again keeps the secrets and keys.
In the configuration the domain is the macro XMPP_DOMAIN where a whole
value is the domain, and the keyword @DOMAIN@ inside module options. A
listener's hosts takes neither, so the component listener accepts its
secret for any component name. It listens on 127.0.0.1 only.
DNS
make dns writes the records the domain needs:
- A, and AAAA with
IP6, for the domain andconference; - SRV for clients (
_xmpp-client._tcp5222,_xmpps-client._tcp5223); - SRV for servers (
_xmpp-server._tcp5269, also forconference); - SRV for calls (
_stunand_turn, udp and tcp, 3478).
The upload host points at the store. proxy.<domain>, the bytestreams
proxy, needs no record: a client asks it over XMPP, and its answer gives
the server's IP and port 7777, which the client connects to by address.
make dns lists it as a comment, A/AAAA records to add only if you like
the name to resolve.
Ports
Published on the host:
- 5222 and 5223 for clients (STARTTLS and direct TLS);
- 5269 for other servers;
- 5443 for the WebSocket and the
.well-knowndocuments; - 3478 udp and tcp for STUN and TURN, with the relay range 49160-49199 udp;
- 7777 tcp for the SOCKS5 bytestreams proxy (file transfers between peers
that cannot connect to each other); open it in the firewall to wherever
the users' clients are, on the address
IPnames; - 80 for the ACME challenges, with
CERTS=acme(ejabberd's 8080 in the container: its uid may not bind 80 in a rootless one).
The component port 5347 and the admin API on 5280 stay inside the container. A reverse proxy on 443 is the deployment's choice.
Push
apushd runs in ejabberd's network namespace: it connects to the component
port on 127.0.0.1, and nothing outside can reach it. Its settings are
state/<domain>/apushd/settings.json, which make env writes with one push
service per key it finds in that directory:
| push service | for | what it needs |
|---|---|---|
| Web Push | the web app, the installed PWA, Android over UnifiedPush | nothing: make env makes the VAPID key (vapid.pem) once |
| APNs | iOS, alerts and PushKit calls | apns.p8, with APNS_KEY_ID, APNS_TEAM_ID and IOS_BUNDLE at make env |
| FCM | Android over Firebase | fcm.json, the Firebase service account's key: FCM_KEY= at make env |
make env ... FCM_KEY=/path/to/key.json copies the key Firebase's console
gives (Project settings, Service accounts, Generate new private key) to
apushd/fcm.json, after checking that it is a service account's key (its
type is service_account, with project_id, client_email and
private_key); nothing of it is printed. It is given once: a later
make env without FCM_KEY keeps the copy, and one with it replaces the
copy. The module is anotherimAndroid. Removing fcm.json and running
make env again turns FCM off.
The clients find the Web Push key and the FCM module's name through
apushd's disco (urn:tacky:push:webpush:0 and urn:tacky:push:fcm:0), so
nothing in the apps depends on them; an Android app on a server with no FCM
module stays connected instead. apushd runs as nobody, so this directory is readable by
every user of the host.
Telegram
make env ... TACKYGRAM=1 TG_API_ID=... TG_API_HASH=... adds the gateway
(compose.tackygram.yaml); the API id and hash are an application's, from
https://my.telegram.org. Like CERTS=acme, TACKYGRAM=1 is given at
every make env, since compose.env is rewritten; the API id and hash are
kept from the first time. The gateway runs in ejabberd's network namespace
as the component telegram.<domain>, and users of the domain link their
Telegram account through its ad-hoc commands (by QR code or phone number).
ejabberd gives that name, and no other, the XEP-0356 privileges it needs:
the linked contacts go into the user's roster with their presence, and what
the user writes from Telegram's own apps comes back as carbons, and the
user's XEP-0191 blocklist on the server and their Telegram blocklist are
kept the same both ways (an iq privilege on urn:xmpp:blocking). The same
name, and no other component, may ask upload.<domain> for a slot: the
stickers users get from Telegram go there (see "Uploads"). ejabberd's
mod_privilege drops every iq a component sends to a bare JID that is not
the domain itself, such as upload.<domain> and pubsub.<domain>
(processone/ejabberd#4472, open); mod_component_iq, ours, lets those
that wrap no privileged iq go as they are. It may also
create and publish on pubsub.<domain>, where its users import Telegram
sticker sets into the sticker catalogue (see "Sticker packs"). Users of
other servers cannot use it, so the name needs no DNS record.
Voice calls go through the gateway too: a Telegram contact rings the user's devices over Jingle, and the user calls Telegram users from their contact. The gateway's engine is one end of the media towards the devices (ICE over UDP, DTLS-SRTP), so the host opens what it listens on:
| what | default | set at make env with |
|---|---|---|
| UDP ports, published one to one on ejabberd's service (the gateway shares its network namespace) | 50000-50099 |
TACKYGRAM_CALL_PORTS=min-max |
| the public address put in the gateway's candidates | IP, as for TURN |
TACKYGRAM_CALL_IP= (comma-separated), at every make env |
| the STUN server it asks for its reflexive address | stun:<domain>:3478, ejabberd's own listener |
TACKYGRAM_CALL_STUN= (a URL, or empty for none) |
Open that UDP range in the firewall to wherever the users' clients are,
beside 3478 and the TURN relay's 49160-49199, which serve the clients
that cannot reach it directly. A call takes a port or two, so the range
bounds the calls at once. The Telegram side of a call goes out to
Telegram over UDP and needs nothing opened. The range and the STUN server
are kept from one make env to the next; compose.env gets the range for
the ports.
state/<domain>/tackygram/ holds env (the gateway's domain, the
component secret, the API credentials and the calls' settings, 0600) and key, the master key
the users' Telegram sessions are sealed under, made by the first make env
with TACKYGRAM=1 and handed to the container's user (0400). The sessions
are in the compose volume tackygram, which opens nothing without the key:
back the key up apart from the volume (with Podman it reads as podman unshare cat state/<domain>/tackygram/key). Without it, every user links
again.
The image is tackygram's two executables on Debian's slim base:
tackygram, the component with its own Tcl and libtacky inside, and
paperplaned, the Telegram engine, which tackygram runs beside it; no Tcl
is installed. Once ejabberd is up the gateway logs connected as telegram.<domain>, and ejabberd logs the permissions it grants it.
To try it with no Telegram account or API credentials, run the gateway on its engine's fake Telegram, with a compose file of your own beside the others:
# fake.yaml
services:
tackygram:
command: [-data, /var/lib/tackygram, -engine-args, -fake]
COMPOSE_FILE=compose.yaml:compose.tackygram.yaml:fake.yaml \
docker compose --env-file state/<domain>/compose.env up -d
The fake account links by QR code or by phone +15550000000 with code
12345, and its contact @alice answers every message with an echo.
Sticker packs
XEP-0449 has no registry of public sticker packs, so the server keeps its
own: the sticker catalogue, node urn:xmpp:stickers:0 on the pubsub
service pubsub.<domain> (access_model open, persist_items,
max_items max, publish_model publishers). Each item is a pack; every
client reads the node with plain XEP-0060 and adds a pack from it by its
URI, xmpp:pubsub.<domain>?pubsub;action=retrieve;node=urn:xmpp:stickers:0;item=<id>
(anotherim's Stickers page, "All stickers", lists it). Two kinds of pack go
there:
- The admin's. libtacky's
tackyd stickers publish -acc admin@<domain> -password … -dir DIR -name NAME [-summary TEXT]makes a pack of the images in DIR (each put intoupload.<domain>), publishes it on the catalogue, creating the node when it is not there, and prints its URI. Run it from any machine with libtacky'stackydthat reaches the server. - Telegram sets. The gateway's ad-hoc command "Import Telegram sticker
pack" (node
import-stickersontelegram.<domain>) takes a set's short name or itst.me/addstickers/<name>link from a linked user, puts the set's static stickers (at most 120; animated ones are skipped) intoupload.<domain>and publishes the pack, answering with its URI; a set imported once answers at once, and from then on that set's stickers from Telegram name their pack, so a tap on one offers it. Nothing is imported unasked.
Whoever creates the node owns it, and only the owner and its publishers
may publish. When the gateway creates it, it makes admin@<domain> a
publisher too (tackygram's -catalogue-publishers); when the admin's
tackyd creates it first, the admin makes telegram.<domain> a publisher
of the node (XEP-0060 §8.9, from any client that edits affiliations), or
the gateway's imports are refused. pubsub_createnode in ejabberd.yml
lets local users and the gateway create nodes.
The packs' files are ordinary uploads, so a lifecycle rule that expires the upload bucket's files (see "Uploads") empties the catalogue's packs with them.
People directory
make env ... DIRECTORY=1 DIRECTORY_COUNTRY_CODE=372 adds the people
directory (compose.directory.yaml), libtacky's tackyd directory as the
component people.<domain> in ejabberd's network namespace, on the
loopback component port with the component secret. ejabberd lists it among
the domain's disco items while it is connected; a client finds it there as
a directory/user service with jabber:iq:search. Like TACKYGRAM=1,
DIRECTORY=1 is given at every make env; env.sh writes
state/<domain>/directory/env (0600), and each setting below is the one
given, else the one written last time, else libtacky's default:
| setting | default | |
|---|---|---|
DIRECTORY_COUNTRY_CODE |
the country of a phone number written without one, 1-3 digits | none: such a number is taken as international |
DIRECTORY_LOCAL_ONLY |
1: only this server's users may search; 0: users of every federated server too |
1 |
DIRECTORY_SELF_PHONE |
1: a user may list a phone number of their own; 0: only the one the registry gave them |
1 |
DIRECTORY_SELF_CODE |
the same for the personal code | 1 |
-
Search (XEP-0055, a data form):
name(each word starts a word of the listed name, case aside; the longest word at least two letters),phone(exact, after normalising to+and digits; a number written without a country code is taken inDIRECTORY_COUNTRY_CODE, so+372 5123 4567,37251234567and5123 4567are one number),code(the personal code, exact). The answer gives the address and the name, never the phone or the code; 30 rows at most, nothing for an empty query. Searches are limited per minute to 30 per user, 90 per server and 300 in all, beyond which the answer isresource-constraint; other limits are the program's options, given as the service's command in a compose override (command: [run, -rate, "60", -domain-rate, "300"]). -
Opt-in. Nobody is listed until they list themselves: an iq
<listing xmlns='urn:tacky:directory:0'>with<name/>,<phone/>and<code/>(what is left out is not listed), or the ad-hoc commandlistonpeople.<domain>;<unlist/>or the commandunlistremoves the entry. Only users of this domain list, and only themselves. A personal code or a registry's phone number someone else holds is refused. A self-listed phone or code is a claim nobody checked; with a registry,DIRECTORY_SELF_PHONE=0andDIRECTORY_SELF_CODE=0take both from it alone. -
A registry. For a provisioned population (the state-messenger case), load a CSV of
jid,name,phone,codeinto the running directory:make directory-import DOMAIN=chat.example.org CSV=registry.csvThe rows are provisioned: their owners cannot change or remove the name, the code or a phone the registry gave, but may add a phone where it gave none. One bad line fails the whole import, every bad line named.
The store is the compose volume directory, a plain SQLite file
(people.db): names, phone numbers and personal codes are not
encrypted at rest, so treat the volume as the server's own data. The
directory logs counts only, never what was searched or listed. libtacky's
tacky/xmpp/directory/README.md has the wire in full.
libtacky has no image, so make directory-image makes one here
(directory/Dockerfile): it builds libtacky's tackyd from its source at
LIBTACKY_REF, with no media backend, and puts it on Debian's slim base.
LIBTACKY_REF= and LIBTACKY_GIT= (another clone) at make directory-image build another libtacky.
The admin
admin@<domain> is the admin. To create the account:
docker compose --env-file state/<domain>/compose.env exec ejabberd ejabberdctl register admin <domain> <password>
Login
Nobody types a JID and a password.
- Registration. A new account is the proof of work, and the client makes a long random password and keeps it.
- Sessions. Every device has a FAST token of its own from its first login, and its sessions use that token.
- The password does two things only:
- It changes the password. A session that logged in with a token may not
(
not-allowed), so a device's token is never enough to take the account. - It takes the account back: changing the password ends every token.
- It changes the password. A session that logged in with a token may not
(
Planned:
- Pairing by QR code. A new device shows a one-time key as a QR code. A logged-in device scans it, and a module of ours makes a token for the new device, sealed to that key. The new device collects it and logs in. The password never leaves the first device.
- Recovery. The password itself, shown once at registration as a code to write down.
Uploads
A client asks upload.<domain> for a slot and gets two URLs:
- PUT: signed with AWS Signature Version 4 for exactly the size asked
(
Content-Lengthis signed, so the store refuses any other size) and good for 10 minutes; - GET: where the file will be.
The file never passes through ejabberd. Its key is 128 random bits followed
by the file's name; nothing in it names the uploader. The limit is 100 MiB,
for local users and for the Telegram gateway, which puts there the stickers
its users get, asking as the peer's JID (<id>@telegram.<domain>): the
access rule upload in ejabberd.yml grants the ACL telegram_gateway
beside local.
The store is any S3 service with a bucket at UPLOAD_URL, reached over
HTTPS with the Host header passed through unchanged, since it is signed.
It has been tried with RustFS and Garage. The bucket needs:
- A key for the server. Add
EJABBERD_MACRO_S3_ACCESS_KEY=andEJABBERD_MACRO_S3_SECRET=lines tostate/<domain>/secrets.env. Until they are there, uploads fail and everything else works; ejabberd refuses an empty value, somake envleaves them out. - Anonymous
s3:GetObject. The URL is the secret, and OMEMO files are encrypted. - A CORS rule allowing GET, PUT and HEAD from any origin, for the web client.
- A lifecycle rule, if files should expire; not with sticker packs in the catalogue, whose files are uploads too.
Tests
make test # the modules' tests, in the image build
make test-login # live: tokens, the password rule, the configuration
make test-acme # live: the compose deployment with CERTS=acme, against step-ca
make test-push # live: a push end to end on the compose deployment
make test-schedule # live: scheduled messages, a restart in between
make test-module-deb # the module .debs beside ProcessOne's ejabberd .deb
make test-cluster # live: mod_schedule on two nodes joined as one cluster
make test-client-access # live: XEP-0494, ann's clients listed and revoked
-
make test. The modules' eunit suites run against the release while the image builds, and fail the build below 99 % of any module's lines covered.mod_apush_testschecks the rules, then every path on a running ejabberd.mod_s3_upload_testschecks the signature against AWS's published example, keys and names, then disco, slots, limits and access (the Telegram gateway's JIDs get a slot, another component's do not).mod_component_iq_testsshows mod_privilege alone dropping the gateway's create onpubsub.<domain>, then the module letting it, a slot request toupload.<domain>and a disco of the domain through unchanged while a wrapped privileged iq is still mod_privilege's.mod_schedule_tests(Mnesia, the default) andmod_schedule_sql_tests(db_type: sqlon SQLite), the same cases on each backend, with carbons, MAM, blocking and offline storage: the claim raced by twenty processes (one wins; a releasing item taken again only once stale), disco, every request and refusal, the release (bare JID, carbons to every resource, the archive, the silent marker kept), rooms (MUC/Sub subscribers only, released into the room's archive and its subscribers' events, a room left meanwhile failing the item), errors coming back making an item failed (and others' not), a blocked recipient, an item due while the module was stopped, the timer set again by a submit, many due at once in order, a stored stanza that cannot be read, an account removed.mod_client_access_testsplays c2s processes against ejabberd_sm, mod_auth_fast's and mod_push's stores: a SASL2 client online, hibernated and resumed (one client throughout), FAST tokens of a known agent and of one never seen, push registrations of a client and a stray one, the revoke of a hibernated, a live and the asking session (answered first) with their tokens and registrations, strays revoked, the refusals, the owner only, what is remembered and for how long, a host remembering nothing, the caps node.
-
make test-loginchecks:- a password login gets a FAST token, and the token logs in;
- the token's session may not change the password, and a password session may;
- afterwards only the new password works, and not the old token;
- the domain reached the services: MUC and upload in disco, the STUN/TURN hosts, the admin's commands;
proxy.<domain>in the domain's disco#items with the identity proxy/bytestreams, its streamhost the deployment'sIP(made a TEST-NET address here, not the container's) and port 7777, refusedforbiddento a component of another domain; and two clients, ann and bob, connect to it as SOCKS5, ann activates the stream and bob receives what she sends through it;- the component
telegram.<domain>gets the gateway's privileges (roster both, message outgoing, presence roster, iq both onurn:xmpp:blocking), and another component name gets none; - when the directory's image is there (
make directory-image), the directory beside the server with the envmake env DIRECTORY=1writes:people.<domain>in disco as a directory, ann lists herself, bob finds her by name, by phone without its country code and by code (her address only), a one-letter name is refusednot-acceptable, bob's listing with her codeconflict, a search from another domainforbidden; then, aftermake env DIRECTORY_LOCAL_ONLY=0 DIRECTORY_SELF_CODE=0and a restart on the same store, that search finds her and bob's own code is refusednot-allowed; once she unlists nobody finds her. Without the image it saysskip.
-
make test-acmeruns the compose deployment withCERTS=acmethroughmake up, against smallstep's step-ca as the ACME server (with challtestsrv as its DNS) and no ports published. step-ca validates over HTTP-01 on ejabberd's own listener and issues for a week and a minute. Pebble, Let's Encrypt's test server, cannot stand in: it refuses ejabberd's ACME client, which sends no User-Agent, and the client refuses Pebble'scharsetmedia types and itsdata:terms of service; Let's Encrypt itself does neither. It checks:make upasks for the certificate, and ejabberd holds it for the domain andconference;- ejabberd serves it for both names without a restart;
- a second
make upasks for nothing; - apushd connects;
- a minute later ejabberd has renewed it by itself and serves the new one.
-
make test-pushruns the compose deployment asmake envwrites it, with a made-up Firebase key given asFCM_KEY(apushd loads it, nothing calls Google), apushd's demo module added beside the Web Push one and no ports published. Before it starts,make envmust refuse a key that is not a service account's, keep the copied key whenFCM_KEYis left out, and listanotherimAndroidin apushd's settings. ann puts bob in her roster as "Bob" and enables push with a device key, then goes offline, and bob writes to her. It checks:- the component port listens on 127.0.0.1 only, and apushd connects with the deployment's secret;
- apushd's Web Push key and its FCM module's name reach a client through
its disco (an apushd with
urn:tacky:push:webpush:0andurn:tacky:push:fcm:0); - the envelope apushd receives opens with ann's key to a summary naming Bob as her roster does, for ann, counting one, without the text;
- the push window (5 seconds here): three messages at once are one push counting two and one more at the window's end counting four;
- a mentions-only room ann is subscribed to (MucSub), her occupant id
from its self-presence: bob's plain message there is no push, his
XEP-0513 mention of her occupant id is one, its summary saying
"mention": true. - other clients' registrations, at a stand-in App Server (a component,
p2.): one with a
secretonly, as Conversations enables, and one with no form; bob's call and his message each reach both asmod_pushmakes the push, the publish-options as sent and nothing ofmod_apush's, while ann's apush registration gets the message alone, sealed as before.
-
make test-scheduleruns the image asmake envwrites it, ann and bob registered, bob offline. It checks:- the domain advertises
urn:tacky:schedule:0with its limits; - ann schedules six messages, five to bob; one for
telegram.<domain>is redirected to the gateway (the configuration's@DOMAIN@reachedpass_through), one past a year istoo-far; - bob's chat lists the five in time order as from ann's bare JID, the
silent one silent; a change of time and text, a cancel and a send now
are answered, and every change reaches her as a
<changed/>headline; - each release reaches her as a sent carbon from her bare JID (the
silent one with its marker, the edited one with the new text) and her
archive keeps the four; one to an account that does not exist comes
back
failedwithservice-unavailableand stays listed; - bob, catching up from his archive, has the four from ann's bare JID and not the cancelled one;
- a message due while the server is stopped goes when it starts again, into both archives;
- a groupchat message to a room ann is a MUC/Sub subscriber of is
released into it (the room's archive, her device's MucSub event), and
one to a room she is not subscribed to is refused
not-allowedat once.
- the domain advertises
-
make test-module-debbuilds the four module packages (make module-deb: ejabberd-mod-apush, ejabberd-mod-component-iq, ejabberd-mod-schedule and ejabberd-mod-client-access, for a server on ProcessOne's own ejabberd 26.09-1 .deb) and installs them beside that .deb, installed with apt on Debian trixie, then adds what their READMEs ask to the package's default configuration (no database to set up). It checks each installs and depends on exactly 26.09-1, ejabberd starts and each module is loaded, and mod_schedule works there on Mnesia (the feature, a message released, the carbon, bob's archive, the Telegram redirect through@HOST@), and mod_client_access there (withmod_auth_fastadded, as its README says): the hibernated phone with its token listed, revoked (no token, no resumption), a live session's stream ended, bob refused. -
make test-client-accessruns the image asmake envwrites it, ann's devices each a SASL2 user agent naming its device. It checks:- her account's disco#info lists
urn:xmpp:cam:0to her; - her laptop lists four clients: the phone hibernated (its connection lost under XEP-0198) and still connected, with a FAST token; the tablet not connected, with a token; the desk and the laptop online, the laptop with its push registration; live sessions with address and resource;
- revoking the phone ends its token (refused) and its hibernation (a
password login asking to resume it is told
failed); revoking the tablet its token; revoking the desk ends its stream with apolicy-violationstream error; an unknown id isitem-not-found; - the list after holds the laptop and the phone's new password login;
- bob may neither list nor revoke her clients; the laptop's own token still works.
- her account's disco#info lists
-
make test-clusterruns two containers of the image as the nodesejabberd@node-aandejabberd@node-bon one network, B joined to A withejabberdctl join_cluster. It checks the schedule table has a disc copy on both; an item submitted on A is listed on B; with A stopped before its time, B sends it; with both up an item goes once; and an item due while the whole cluster was down goes once when both start again at the same moment. Once is counted in bob's offline store (Mnesia, the cluster's) and in ann's archives (each node's own SQLite here: a double send would show twice).
tests/xmpp-client.py is the live tests' client, written with Python's
standard library only.