Compare commits
204 Commits
release/al
...
ccchaos
Author | SHA1 | Date |
---|---|---|
decentral1se | 6dd5a3b0fc | |
Matthew Wild | d1cdca0c2c | |
Matthew Wild | e5d493483e | |
Matthew Wild | fbc5a46c43 | |
Matthew Wild | 8c506217d5 | |
Matthew Wild | 861c570b37 | |
Matthew Wild | ad694d6436 | |
Matthew Wild | 569fce239c | |
Matthew Wild | 6ab178385f | |
Matthew Wild | cd462a28a7 | |
Matthew Wild | 17444cc3bd | |
Matthew Wild | a24eddab8b | |
Matthew Wild | 77ecd4d9b9 | |
Matthew Wild | fb518da3c8 | |
Matthew Wild | b2b1ea3660 | |
Matthew Wild | a2714fc178 | |
Matthew Wild | ce14c8153b | |
Matthew Wild | 08080f03b9 | |
Matthew Wild | 2d623e7bf5 | |
Matthew Wild | e19b0a32af | |
Matthew Wild | b57057f809 | |
Matthew Wild | 457096a13d | |
Matthew Wild | f85250461c | |
Matthew Wild | 631c3acc99 | |
Matthew Wild | 8d16897cff | |
Matthew Wild | ca242ce8a4 | |
Matthew Wild | a5084a289e | |
Matthew Wild | 40daaa883b | |
Matthew Wild | 263d5cf286 | |
Matthew Wild | d47a6ddbc0 | |
Matthew Wild | 2f997d50b5 | |
Matthew Wild | 0294b0e7e0 | |
Matthew Wild | 5dddfeb876 | |
Matthew Wild | 00ad72bcf1 | |
3wc | d1fd9d6ef4 | |
Matthew Wild | 1fe4571ab4 | |
3wc | d8577e0e57 | |
3wc | 7605046cb0 | |
3wc | b1af112f15 | |
Matthew Wild | e125e70e3e | |
Matthew Wild | 94a279a277 | |
Matthew Wild | 609183c305 | |
Matthew Wild | 6160d259e6 | |
Matthew Wild | dae151c7c9 | |
Matthew Wild | ff38924c47 | |
Matthew Wild | adb1fb92ae | |
Matthew Wild | 83c757c786 | |
Kim Alvefur | 30e05e8754 | |
Matthew Wild | deddef38f2 | |
Matthew Wild | f6cf8f2645 | |
Matthew Wild | 7f94dd21bc | |
Matthew Wild | 0ce5d3acf5 | |
Matthew Wild | c02b8b7f3f | |
Matthew Wild | 88b61461cc | |
Matthew Wild | 03f0bb2bd9 | |
Matthew Wild | 6852c37111 | |
Matthew Wild | 301ee238be | |
Jonas Schäfer | dabfaa2132 | |
Matthew Wild | 29ae464a47 | |
Jonas Schäfer | 78946c32fd | |
Matthew Wild | 78b3d4e7e4 | |
Matthew Wild | 384fdf3454 | |
Michael DiStefano | 103876e0fd | |
Matthew Wild | bd10b2a861 | |
Matthew Wild | 2c3ca07ee0 | |
Matthew Wild | d0149e52df | |
Matthew Wild | 3152aa8ba2 | |
Jonas Schäfer | 5b8d22a2f1 | |
Kim Alvefur | 37f2af4acd | |
Matthew Wild | ecf3dede57 | |
Kim Alvefur | 89b8c7dfc7 | |
Kim Alvefur | a163990ef8 | |
Matthew Wild | cec33debd3 | |
Matthew Wild | c32e9c4d26 | |
Matthew Wild | 94d0113a2e | |
Kim Alvefur | 2fbde36b5b | |
Kim Alvefur | 8e3c28dd73 | |
Kim Alvefur | 212de80da1 | |
Matthew Wild | 4b923555b3 | |
Kim Alvefur | e4e31976f3 | |
Matthew Wild | 274efd9a32 | |
Greylinux | eae1069295 | |
Matthew Wild | 04e421a7e1 | |
Matthew Wild | 8324fe2059 | |
Greylinux | 4475e94759 | |
Matthew Wild | 01211ecab6 | |
Jonas Schäfer | b25e3c8fa7 | |
Jonas Schäfer | 46bfccda83 | |
Jonas Schäfer | 97586c08b6 | |
Jonas Schäfer | eceebd4aba | |
Matthew Wild | 913d96d4ee | |
Matthew Wild | a6bd89e02a | |
Matthew Wild | e8dd2408b8 | |
Matthew Wild | bf2fccf585 | |
Matthew Wild | cba3e97ff9 | |
Jonas Schäfer | ff977f55b5 | |
Jonas Schäfer | 690f58bb27 | |
Matthew Wild | 7ca468a1ac | |
Matthew Wild | 26e1e6559a | |
Matthew Wild | 1bc336d933 | |
Matthew Wild | a948cc141f | |
Matthew Wild | 105c04a2c7 | |
Matthew Wild | 3cab4faaf9 | |
Matthew Wild | bd5329c84d | |
Matthew Wild | 088d9cb840 | |
Matthew Wild | 2bee41057b | |
Matthew Wild | 173ca8b0c9 | |
Matthew Wild | 7f26c50ba8 | |
Matthew Wild | 32a7bc8954 | |
Matthew Wild | e9e5762936 | |
Matthew Wild | 90b13aed6e | |
Matthew Wild | b2431438b2 | |
Matthew Wild | dcc02d374b | |
Matthew Wild | 13228b1fc3 | |
Matthew Wild | 36ffd5d4a0 | |
Matthew Wild | c4a8a88028 | |
Matthew Wild | 43c244d55d | |
resoli | 19dc32e3e8 | |
Matthew Wild | 7b37c629dd | |
resoli | 087b02ca5a | |
Matthew Wild | 724335019e | |
Matthew Wild | 3c3a74f1cc | |
Matthew Wild | c6aa7a9732 | |
Kim Alvefur | 2ad719122d | |
Matthew Wild | a9ee76b2f1 | |
Matthew Wild | e881c983a6 | |
Matthew Wild | 0224f93843 | |
Matthew Wild | 67f4ffa83f | |
Matthew Wild | 5f8f5657b5 | |
Jonas Schäfer | f197b9bf6b | |
Jonas Schäfer | 04861cc023 | |
Matthew Wild | e963012ac1 | |
Jonas Schäfer | 5e74fba75d | |
Matthew Wild | c3144380de | |
Jonas Schäfer | 215426c2db | |
Matthew Wild | 84f55744f1 | |
Matthew Wild | 3e62edfbe9 | |
Matthew Wild | 7ab1cec072 | |
Matthew Wild | 138200598d | |
Matthew Wild | 4cc3880ec7 | |
Matthew Wild | 5e0b8c9e7d | |
Matthew Wild | e568d1f039 | |
Matthew Wild | d20273fbff | |
Matthew Wild | 548937400d | |
Matthew Wild | a624aaebf4 | |
Matthew Wild | c5468d3d31 | |
Felix | e9d2668f83 | |
Matthew Wild | 8deff503da | |
Matthew Wild | 20b0620e4d | |
Matthew Wild | 13d03bc903 | |
Matthew Wild | d71359104c | |
Matthew Wild | 4278cc055f | |
Matthew Wild | b26a3eca60 | |
Matthew Wild | 1947781554 | |
Matthew Wild | a301eb4bb7 | |
Matthew Wild | d6b2676829 | |
Matthew Wild | 72e2c2cec5 | |
Matthew Wild | e6b59a695e | |
Matthew Wild | e23f09cec9 | |
Matthew Wild | 4914d83bd8 | |
Matthew Wild | 03e4aa8c80 | |
Matthew Wild | dfb6cab374 | |
Matthew Wild | cfe9f747c5 | |
Matthew Wild | 5f6f060480 | |
Matthew Wild | 649ab3c3db | |
Matthew Wild | eae03273ed | |
Matthew Wild | 0836f369c1 | |
Matthew Wild | 6cb87092bf | |
Matthew Wild | cd8638e154 | |
Matthew Wild | 1bff00eba7 | |
Matthew Wild | 2207199a60 | |
Matthew Wild | 1f37db8f1e | |
Matthew Wild | a5f78bd027 | |
Matthew Wild | 4b8fcdafbd | |
Matthew Wild | 68e29c5ac4 | |
Matthew Wild | e6d8303ece | |
Matthew Wild | c8f5179564 | |
Matthew Wild | 2d64f7be49 | |
Alberto Luaces | b1acb121db | |
Matthew Wild | 280fabb652 | |
Matthew Wild | ee9922f4ae | |
Matthew Wild | 6c25508ec2 | |
Matthew Wild | edc81c8105 | |
Matthew Wild | 3b477dfa77 | |
Matthew Wild | f1c04cf472 | |
Matthew Wild | e430586282 | |
Matthew Wild | 668592c87a | |
Matthew Wild | 13bddf20d4 | |
Matthew Wild | b68c6fe9b1 | |
Matthew Wild | cd78ad3241 | |
Matthew Wild | 4d3a1cd274 | |
Matthew Wild | c7be994710 | |
Matthew Wild | fbb01a5f58 | |
Matthew Wild | 3918fd44a4 | |
Matthew Wild | bd7c35595d | |
Matthew Wild | 67336ed114 | |
Matthew Wild | 4d550e0998 | |
Matthew Wild | 395ab8d404 | |
Matthew Wild | b2d3caede6 | |
Matthew Wild | fe9c14c2b3 | |
Kim Alvefur | ffa5d0e24c | |
Matthew Wild | ec33e74684 | |
Matthew Wild | 4a0ca2b2d7 | |
Matthew Wild | 73b709ff14 |
|
@ -0,0 +1,3 @@
|
|||
github: snikket-im
|
||||
liberapay: Snikket
|
||||
custom: https://snikket.org/donate/
|
|
@ -1,24 +0,0 @@
|
|||
name: Docker image build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build the Docker image
|
||||
run: >-
|
||||
docker build . \
|
||||
--file docker/Dockerfile \
|
||||
--build-arg=BUILD_SERIES=dev \
|
||||
--build-arg=BUILD_ID="$(echo "$GITHUB_SHA" | head -c 12)" \
|
||||
--tag snikket/snikket:dev
|
||||
- name: Log into registry
|
||||
run: echo "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" | docker login -u snikket --password-stdin
|
||||
- name: Push the Docker image
|
||||
run: docker push snikket/snikket:dev
|
|
@ -1,32 +0,0 @@
|
|||
---
|
||||
name: Docker release image build
|
||||
|
||||
"on":
|
||||
push:
|
||||
tags:
|
||||
- release/*.*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build the Docker image
|
||||
run: >-
|
||||
echo "Building ref $GITHUB_REF...";
|
||||
RELEASE_TAG="${GITHUB_REF#refs/tags/release/}";
|
||||
RELEASE_SERIES="${RELEASE_TAG%.*}";
|
||||
RELEASE_VER="${RELEASE_TAG#$RELEASE_SERIES.}";
|
||||
docker build . \
|
||||
--file docker/Dockerfile \
|
||||
--build-arg=BUILD_SERIES="$RELEASE_SERIES" \
|
||||
--build-arg=BUILD_ID="$RELEASE_VER" \
|
||||
--tag snikket/snikket:"$RELEASE_SERIES"
|
||||
- name: Log into registry
|
||||
run: echo "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" | docker login -u snikket --password-stdin
|
||||
- name: Push the Docker image
|
||||
run: >-
|
||||
RELEASE_TAG="${GITHUB_REF#refs/tags/release/}";
|
||||
RELEASE_SERIES="${RELEASE_TAG%.*}";
|
||||
docker push snikket/snikket:"$RELEASE_SERIES"
|
73
CHANGELOG.md
|
@ -1,5 +1,78 @@
|
|||
# Snikket Server changelog
|
||||
|
||||
## UNRELEASED
|
||||
|
||||
- Increase shared file size limit from 16MB to 100MB
|
||||
- Allow configurable storage quota for shared files
|
||||
- Initial support for "limited" user accounts
|
||||
- Support for group chat notifications on iOS
|
||||
- Configurable port range for TURN service
|
||||
- Ability to see basic server metrics in the web admin interface
|
||||
- Support for advanced monitoring/alerting via Prometheus
|
||||
|
||||
### Upgrading
|
||||
|
||||
If you are using a reverse proxy in front of Snikket, ensure it can
|
||||
handle the new upload limit (for example, in nginx the `client_max_body_size`
|
||||
option).
|
||||
|
||||
## beta.20210519
|
||||
|
||||
- Allow custom HTTP bind interface
|
||||
- Add docker health checks
|
||||
- Fix warnings about obsolete letsencrypt user
|
||||
- Add bootstrap API to create initiatal invite in an automated way
|
||||
- Switch to libunbound for DNS resolution (more robust)
|
||||
- Add environment variables to disable/replace the built-in TURN service
|
||||
|
||||
## beta.20210205
|
||||
|
||||
- Fix destruction of circle group chats when a circle
|
||||
is deleted or fails to be created
|
||||
- Add circle group chats to bookmarks of newly-added members
|
||||
- Add trailing '/' to invite URLs for compatibility with some
|
||||
URL parsers
|
||||
|
||||
## beta.20210202
|
||||
|
||||
- Support for Raspberry Pi and other ARM-based systems
|
||||
- Add HTTP admin API for web portal
|
||||
- Add support for user groups (circles)
|
||||
- Switch to multi-container architecture (see note below)
|
||||
- Add support for update and security notifications
|
||||
- Increase file sharing limit from 10MB -> 16MB
|
||||
|
||||
### Upgrading
|
||||
|
||||
If you are upgrading from a previous version, this version
|
||||
requires updates to your `docker-compose.yml`. You can find
|
||||
a [new version here](https://snikket.org/service/resources/docker-compose.beta.yml).
|
||||
|
||||
Make a backup of your current docker-compose.yml if desired,
|
||||
then put the new one in its place. For example:
|
||||
|
||||
```
|
||||
mv docker-compose.yml docker-compose.old.yml
|
||||
wget -O docker-compose.yml https://snikket.org/service/resources/docker-compose.beta.yml
|
||||
docker-compose pull
|
||||
docker-compose up -d --remove-orphans
|
||||
```
|
||||
|
||||
You may also want to check out our new repository of scripts to help
|
||||
manage a self-hosted Snikket instance:
|
||||
[snikket-im/snikket-selfhosted](https://github.com/snikket-im/snikket-selfhosted)
|
||||
|
||||
## alpha.20200624
|
||||
|
||||
- Add support for generating account recovery links
|
||||
- Fix group chat creation glitches
|
||||
- Increase file sharing limit from 1MB -> 10MB
|
||||
- Enable Prosody admin shell for debug purposes
|
||||
|
||||
## alpha.20200525
|
||||
|
||||
- Fix for the TURN service auth configuration that prevented some A/V calls from working
|
||||
|
||||
## alpha.20200513
|
||||
|
||||
- Add STUN/TURN service to facilitate audio/video calls (see note below)
|
||||
|
|
|
@ -14,29 +14,31 @@ ADD docker/entrypoint.sh /bin/entrypoint.sh
|
|||
RUN chmod 770 /bin/entrypoint.sh
|
||||
ENTRYPOINT ["/bin/entrypoint.sh"]
|
||||
|
||||
HEALTHCHECK CMD lua -l socket -e 'assert(socket.connect(os.getenv"SNIKKET_TWEAK_INTERNAL_HTTP_INTERFACE" or "127.0.0.1",os.getenv"SNIKKET_TWEAK_INTERNAL_HTTP_PORT" or "5280"))'
|
||||
|
||||
ADD ansible /opt/ansible
|
||||
|
||||
ADD snikket-modules /usr/local/lib/snikket-modules
|
||||
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
software-properties-common \
|
||||
software-properties-common ca-certificates \
|
||||
gpg gpg-agent \
|
||||
ansible python-passlib python3-passlib \
|
||||
libcap2-bin \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& ansible-playbook -c local -i localhost, --extra-vars "ansible_python_interpreter=/usr/bin/python2" /opt/ansible/snikket.yml \
|
||||
&& apt-get remove -y \
|
||||
ansible python3-passlib \
|
||||
libcap2-bin build-essential\
|
||||
&& c_rehash \
|
||||
&& ansible-playbook -c local -i localhost, --extra-vars "ansible_python_interpreter=/usr/bin/python3" /opt/ansible/snikket.yml \
|
||||
&& apt-get remove --purge -y \
|
||||
ansible \
|
||||
software-properties-common \
|
||||
gpg gpg-agent \
|
||||
python-passlib python3-passlib \
|
||||
mercurial libcap2-bin \
|
||||
python3-passlib \
|
||||
mercurial libcap2-bin build-essential \
|
||||
python3 python3.7-minimal \
|
||||
&& apt-get autoremove -y \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf /var/cache/*
|
||||
|
||||
ADD www /var/www
|
||||
|
||||
RUN echo "Snikket $BUILD_SERIES.$BUILD_ID" > /usr/lib/prosody/prosody.version
|
||||
RUN echo "Snikket $BUILD_SERIES $BUILD_ID" > /usr/lib/prosody/prosody.version
|
||||
|
||||
VOLUME ["/snikket"]
|
17
Makefile
|
@ -1,6 +1,19 @@
|
|||
.PHONY: all docker
|
||||
.PHONY: all docker site
|
||||
|
||||
DOCS := $(docs/**.md)
|
||||
|
||||
all: docker
|
||||
|
||||
docker:
|
||||
docker build -t snikket -f docker/Dockerfile .
|
||||
docker build -t snikket .
|
||||
|
||||
site: mkdocs.yml $(DOCS)
|
||||
echo $(DOCS)
|
||||
mkdocs build
|
||||
|
||||
docs/_po/snikket-server-docs.pot: po4a.conf $(DOCS)
|
||||
po4a \
|
||||
--package-name snikket-server \
|
||||
--package-version vcs \
|
||||
--copyright-holder "Snikket Team <team@snikket.org>" \
|
||||
po4a.conf
|
||||
|
|
51
README.md
|
@ -1,19 +1,35 @@
|
|||
# Snikket builder
|
||||
# Snikket server images
|
||||
|
||||
This is the source repository for building [Snikket service](https://snikket.org/service/)
|
||||
Docker images.
|
||||
|
||||
## Requirements
|
||||
Snikket is an open-source self-hosted personal messaging service. It aims to
|
||||
provide an alternative to proprietary and centralized messaging platforms
|
||||
while supporting all the expected features and being easy to use.
|
||||
|
||||
For more information see the [Snikket website](https://snikket.org/).
|
||||
|
||||
## Getting Started with Snikket
|
||||
|
||||
For instructions on getting started with Snikket, see the [Snikket installation
|
||||
guide](https://snikket.org/service/quickstart/) on our website.
|
||||
|
||||
## Building images
|
||||
|
||||
This section is for people who want to build their own images of Snikket, e.g.
|
||||
for development purposes.
|
||||
|
||||
### Requirements
|
||||
|
||||
- GNU make
|
||||
- docker (tested on 19.03.5)
|
||||
- ansible (tested on 2.7 (debian buster))
|
||||
|
||||
## Building
|
||||
### Building
|
||||
|
||||
Run `make`
|
||||
|
||||
## Running
|
||||
### Running
|
||||
|
||||
The easiest way is to use docker-compose. Copy the file `snikket.conf.example` to
|
||||
`snikket.conf` and edit the values in it. Then run:
|
||||
|
@ -21,30 +37,3 @@ The easiest way is to use docker-compose. Copy the file `snikket.conf.example` t
|
|||
```console
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
If you need to change port mappings or any other advanced options, you can edit the
|
||||
`docker-compse.yml` file.
|
||||
|
||||
Alternatively you can run docker manually with something like the following:
|
||||
|
||||
```console
|
||||
docker run --env-file=snikket.conf -p 80:5280 -p 443:5281 -p 5222:5222 -p 5269:5269 snikket
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Dev images have a few additional features.
|
||||
|
||||
### Local mail server
|
||||
|
||||
Outgoing emails from dev images are captured by a local [MailHog](https://github.com/mailhog/MailHog)
|
||||
instance and are accessible in a dashboard served on port 8025. The dashboard requires authentication.
|
||||
The username is 'snikket' and the auto-generated password can be found with the following command:
|
||||
|
||||
```console
|
||||
docker exec snikket_snikket_1 cat /tmp/mailhog-password
|
||||
```
|
||||
|
||||
Replace `snikket_snikket_1` with the name of your running container if it differs.
|
||||
|
||||
MailHog is not included in production images, which require a real SMTP server.
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/bash
|
||||
|
||||
CERT_PATH="/snikket/letsencrypt/live/$SNIKKET_DOMAIN/cert.pem"
|
||||
|
||||
if test -f "$CERT_PATH"; then
|
||||
prosodyctl --root cert import /snikket/letsencrypt/live
|
||||
exit 0;
|
||||
fi
|
||||
|
||||
while sleep 10; do
|
||||
if test -f "$CERT_PATH"; then
|
||||
prosodyctl --root cert import /snikket/letsencrypt/live
|
||||
exit 0;
|
||||
fi
|
||||
done
|
|
@ -3,9 +3,10 @@
|
|||
SHOW_QR=0
|
||||
if [ "$1" == "--qr" ]; then
|
||||
SHOW_QR=1;
|
||||
shift;
|
||||
fi
|
||||
|
||||
URL=$( prosodyctl mod_easy_invite "$SNIKKET_DOMAIN" generate "$@" )
|
||||
URL=$(prosodyctl mod_invites generate "$SNIKKET_DOMAIN" "$@")
|
||||
|
||||
echo ""
|
||||
echo "Your invite link: $URL"
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
#!/bin/sh
|
||||
|
||||
CERTFILE="/snikket/letsencrypt/live/$SNIKKET_DOMAIN/fullchain.pem";
|
||||
KEYFILE="/snikket/letsencrypt/live/$SNIKKET_DOMAIN/privkey.pem";
|
||||
if [ "$SNIKKET_TWEAK_TURNSERVER" = "0" ]; then
|
||||
echo "TURN server disabled by environment, not launching.";
|
||||
exit 0;
|
||||
fi
|
||||
|
||||
CERTFILE="${SNIKKET_CERTFILE:-/snikket/letsencrypt/live/$SNIKKET_DOMAIN/fullchain.pem}";
|
||||
KEYFILE="${SNIKKET_KEYFILE:-/snikket/letsencrypt/live/$SNIKKET_DOMAIN/privkey.pem}";
|
||||
|
||||
echo "Waiting for certificates to become available..."
|
||||
while ! test -f "$CERTFILE" -a -f "$KEYFILE"; do
|
||||
|
@ -11,8 +16,11 @@ done
|
|||
|
||||
TURN_EXTERNAL_IP="$(snikket-turn-addresses "$SNIKKET_DOMAIN")"
|
||||
|
||||
min_port="${SNIKKET_TWEAK_TURNSERVER_MIN_PORT:-49152}"
|
||||
max_port="${SNIKKET_TWEAK_TURNSERVER_MAX_PORT:-65535}"
|
||||
|
||||
exec /usr/bin/turnserver -c /etc/turnserver.conf --prod \
|
||||
--static-auth-secret="$(cat /snikket/prosody/turn-auth-secret)" \
|
||||
--static-auth-secret="$(cat /snikket/prosody/turn-auth-secret-v2)" \
|
||||
--cert="$CERTFILE" --pkey "$KEYFILE" -r "$SNIKKET_DOMAIN" \
|
||||
--min-port "$min_port" --max-port "$max_port" \
|
||||
-X "$TURN_EXTERNAL_IP"
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
su letsencrypt -- -c "certbot certonly -n --webroot --webroot-path /var/www \
|
||||
--cert-path /etc/ssl/certbot \
|
||||
--keep $SNIKKET_CERTBOT_OPTIONS \
|
||||
--agree-tos --email \"$SNIKKET_ADMIN_EMAIL\" --expand \
|
||||
--allow-subset-of-names \
|
||||
--config-dir /snikket/letsencrypt \
|
||||
--domain \"$SNIKKET_DOMAIN\" --domain \"share.$SNIKKET_DOMAIN\" \
|
||||
--domain \"groups.$SNIKKET_DOMAIN\"
|
||||
"
|
||||
|
||||
prosodyctl --root cert import /snikket/letsencrypt/live
|
|
@ -1,6 +1,28 @@
|
|||
local DOMAIN = assert(ENV_SNIKKET_DOMAIN, "Please set the SNIKKET_DOMAIN environment variable")
|
||||
|
||||
daemonize = false
|
||||
local RETENTION_DAYS = tonumber(ENV_SNIKKET_RETENTION_DAYS) or 7;
|
||||
local UPLOAD_STORAGE_GB = tonumber(ENV_SNIKKET_UPLOAD_STORAGE_GB);
|
||||
|
||||
local CERT_PATH = ENV_SNIKKET_CERTFILE or "/etc/prosody/certs/"..DOMAIN..".crt";
|
||||
local KEY_PATH = ENV_SNIKKET_KEYFILE or "/etc/prosody/certs/"..DOMAIN..".key";
|
||||
|
||||
if prosody.process_type == "prosody" and not prosody.config_loaded then
|
||||
-- Wait at startup for certificates
|
||||
local lfs, socket = require "lfs", require "socket";
|
||||
local counter = 0;
|
||||
while not lfs.attributes(CERT_PATH, "mode") do
|
||||
counter = counter + 1;
|
||||
if counter == 1 or counter%6 == 0 then
|
||||
print("Waiting for certificates...");
|
||||
elseif counter > 60 then
|
||||
print("No certificates found... exiting");
|
||||
os.exit(1);
|
||||
end
|
||||
socket.sleep(5);
|
||||
end
|
||||
_G.ltn12 = require "ltn12";
|
||||
end
|
||||
|
||||
network_backend = "epoll"
|
||||
|
||||
plugin_paths = { "/etc/prosody/modules" }
|
||||
|
@ -9,6 +31,13 @@ data_path = "/snikket/prosody"
|
|||
|
||||
pidfile = "/var/run/prosody/prosody.pid"
|
||||
|
||||
admin_shell_prompt = ("prosody [%s]> "):format(DOMAIN)
|
||||
|
||||
-- Aggressive GC to reduce resource consumption. These values are not
|
||||
-- incredibly scientific, but should be good for a small private server.
|
||||
-- They should be reviewed on the upgrade to Lua 5.4.
|
||||
gc = { threshold = 100, speed = 750 }
|
||||
|
||||
modules_enabled = {
|
||||
|
||||
-- Generally required
|
||||
|
@ -32,13 +61,15 @@ modules_enabled = {
|
|||
"register"; -- Allow users to register on this server using a client and change passwords
|
||||
"mam"; -- Store messages in an archive and allow users to access it
|
||||
"csi_simple"; -- Simple Mobile optimizations
|
||||
"cloud_notify"; -- Push notifications
|
||||
|
||||
-- Push notifications
|
||||
"cloud_notify";
|
||||
"cloud_notify_extensions";
|
||||
|
||||
-- HTTP modules
|
||||
"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
|
||||
"websocket"; -- XMPP over WebSockets
|
||||
"http_acme_challenge";
|
||||
"http_libjs";
|
||||
"http_host_status_check"; -- Health checks over HTTP
|
||||
|
||||
-- Other specific functionality
|
||||
"limits"; -- Enable bandwidth limiting for XMPP connections
|
||||
|
@ -49,9 +80,19 @@ modules_enabled = {
|
|||
"http_altconnect";
|
||||
"bookmarks";
|
||||
"default_bookmarks";
|
||||
"roster_allinall";
|
||||
"update_check";
|
||||
"update_notify";
|
||||
"turncredentials";
|
||||
"admin_shell";
|
||||
"isolate_host";
|
||||
"snikket_client_id";
|
||||
"snikket_ios_preserve_push";
|
||||
"snikket_restricted_users";
|
||||
"lastlog2";
|
||||
|
||||
-- Spam/abuse management
|
||||
"spam_reporting"; -- Allow users to report spam/abuse
|
||||
"watch_spam_reports"; -- Alert admins of spam/abuse reports by users
|
||||
|
||||
-- TODO...
|
||||
--"groups"; -- Shared roster support
|
||||
|
@ -61,13 +102,34 @@ modules_enabled = {
|
|||
"welcome"; -- Welcome users who register accounts
|
||||
"http_files"; -- Serve static files from a directory over HTTP
|
||||
"reload_modules";
|
||||
"landing_page";
|
||||
|
||||
-- Invites
|
||||
"invites";
|
||||
"invites_adhoc";
|
||||
"invites_api";
|
||||
"invites_groups";
|
||||
"invites_page";
|
||||
"invites_register";
|
||||
"invites_api";
|
||||
"easy_invite";
|
||||
"watchregistrations";
|
||||
"invites_register_api";
|
||||
"invites_tracking";
|
||||
"invites_default_group";
|
||||
"invites_bootstrap";
|
||||
|
||||
"firewall";
|
||||
|
||||
-- Circles
|
||||
"groups_internal";
|
||||
"groups_migration";
|
||||
"groups_muc_bookmarks";
|
||||
|
||||
-- For the web portal
|
||||
"http_oauth2";
|
||||
"http_admin_api";
|
||||
"rest";
|
||||
|
||||
-- Monitoring & maintenance
|
||||
"measure_process";
|
||||
"measure_active_users";
|
||||
}
|
||||
|
||||
registration_watchers = {} -- Disable by default
|
||||
|
@ -75,21 +137,36 @@ registration_notification = "New user registered: $username"
|
|||
|
||||
reload_global_modules = { "http" }
|
||||
|
||||
http_ports = { ENV_SNIKKET_TWEAK_HTTP_PORT or 80 }
|
||||
https_ports = { ENV_SNIKKET_TWEAK_HTTPS_PORT or 443 }
|
||||
http_ports = { ENV_SNIKKET_TWEAK_INTERNAL_HTTP_PORT or 5280 }
|
||||
http_interfaces = { ENV_SNIKKET_TWEAK_INTERNAL_HTTP_INTERFACE or "127.0.0.1" }
|
||||
|
||||
https_ports = {};
|
||||
|
||||
legacy_ssl_ports = { 5223 }
|
||||
|
||||
allow_registration = true
|
||||
registration_invite_only = true
|
||||
|
||||
invites_page = ENV_SNIKKET_INVITE_URL or ("https://"..DOMAIN.."/invite?{token}");
|
||||
-- This disables in-app invites for non-admins
|
||||
-- TODO: The plan is to enable it once we can
|
||||
-- give the admin more fine-grained control
|
||||
-- over what happens when a user invites someone.
|
||||
allow_contact_invites = false
|
||||
|
||||
-- Disallow restricted users to create invitations to the server
|
||||
deny_user_invites_by_roles = { "prosody:restricted" }
|
||||
|
||||
invites_page = ENV_SNIKKET_INVITE_URL or ("https://"..DOMAIN.."/invite/{invite.token}/");
|
||||
invites_page_external = true
|
||||
|
||||
invites_bootstrap_index = tonumber(ENV_TWEAK_SNIKKET_BOOTSTRAP_INDEX)
|
||||
invites_bootstrap_secret = ENV_TWEAK_SNIKKET_BOOTSTRAP_SECRET
|
||||
|
||||
c2s_require_encryption = true
|
||||
s2s_require_encryption = true
|
||||
s2s_secure_auth = true
|
||||
|
||||
archive_expires_after = "1w" -- Remove archived messages after 1 week
|
||||
archive_expires_after = ("%dd"):format(RETENTION_DAYS) -- Remove archived messages after N days
|
||||
|
||||
-- Disable IPv6 by default because Docker does not
|
||||
-- have it enabled by default, and s2s to domains
|
||||
|
@ -107,19 +184,54 @@ authorization = "internal"
|
|||
storage = "internal"
|
||||
statistics = "internal"
|
||||
|
||||
certificates = "certs"
|
||||
if ENV_SNIKKET_TWEAK_PROMETHEUS == "1" then
|
||||
-- When using Prometheus, it is desirable to let the prometheus scraping
|
||||
-- drive the sampling of metrics
|
||||
statistics_interval = "manual"
|
||||
else
|
||||
-- When not using Prometheus, we need an interval so that the metrics can
|
||||
-- be shown by the web portal. The HTTP admin API exposure does not force
|
||||
-- a collection as it is only interested in very few specific metrics.
|
||||
statistics_interval = 60
|
||||
end
|
||||
|
||||
update_check_dns = "_{branch}.update.snikket.net"
|
||||
-- certificates = "certs"
|
||||
|
||||
group_default_name = ENV_SNIKKET_SITE_NAME or DOMAIN
|
||||
|
||||
-- Update check configuration
|
||||
software_name = "Snikket"
|
||||
update_notify_version_url = "https://snikket.org/updates/{branch}/{version}"
|
||||
update_notify_support_url = "https://snikket.org/notices/{branch}/"
|
||||
update_notify_message_url = "https://snikket.org/notices/{branch}/{message}"
|
||||
|
||||
if ENV_SNIKKET_UPDATE_CHECK ~= "0" then
|
||||
update_check_dns = "_{branch}.update.snikket.net"
|
||||
update_check_interval = 21613 -- ~6h
|
||||
end
|
||||
|
||||
http_default_host = DOMAIN
|
||||
http_host = DOMAIN
|
||||
http_external_url = "https://"..DOMAIN.."/"
|
||||
|
||||
turncredentials_host = DOMAIN
|
||||
turncredentials_secret = assert(io.open("/snikket/prosody/turn-auth-secret")):read("*a");
|
||||
if ENV_SNIKKET_TWEAK_TURNSERVER ~= "0" or ENV_SNIKKET_TWEAK_TURNSERVER_DOMAIN then
|
||||
turncredentials_host = ENV_SNIKKET_TWEAK_TURNSERVER_DOMAIN or DOMAIN
|
||||
turncredentials_secret = ENV_SNIKKET_TWEAK_TURNSERVER_SECRET or assert(io.open("/snikket/prosody/turn-auth-secret-v2")):read("*l");
|
||||
end
|
||||
|
||||
-- Allow restricted users access to push notification servers
|
||||
isolate_except_domains = { "push.snikket.net", "push-ios.snikket.net" }
|
||||
|
||||
VirtualHost (DOMAIN)
|
||||
authentication = "internal_hashed"
|
||||
|
||||
ssl = {
|
||||
ciphers = "EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS:!RC4";
|
||||
certificate = CERT_PATH;
|
||||
key = KEY_PATH;
|
||||
};
|
||||
|
||||
|
||||
http_files_dir = "/var/www"
|
||||
http_paths = {
|
||||
files = "/";
|
||||
|
@ -128,21 +240,18 @@ VirtualHost (DOMAIN)
|
|||
invites_register = "/register";
|
||||
}
|
||||
|
||||
default_bookmarks = {
|
||||
{ jid = "general@groups."..DOMAIN, name = "General Chat" };
|
||||
}
|
||||
if ENV_SNIKKET_TWEAK_PROMETHEUS == "1" then
|
||||
modules_enabled = {
|
||||
"prometheus";
|
||||
}
|
||||
end
|
||||
|
||||
welcome_message = [[Hi, welcome to Snikket on $host!
|
||||
|
||||
]]
|
||||
..[[Thanks for joining. We've automatically added you to the "General Chat" group ]]
|
||||
..[[where you can chat with other members of $host. You'll find it under 'Bookmarks'.
|
||||
|
||||
]]
|
||||
..[[Snikket is in its early stages right now, so thanks for trying it out, ]]
|
||||
..[[we hope you like it!
|
||||
|
||||
]]..[[That's all for now, happy chatting!]]
|
||||
welcome_message = [[Hi, welcome to Snikket on $host! Thanks for joining us.]]
|
||||
.."\n\n"
|
||||
..[[For help and enquiries related to this service you may contact the admin via email: ]]
|
||||
..ENV_SNIKKET_ADMIN_EMAIL
|
||||
.."\n\n"
|
||||
..[[Happy chatting!]]
|
||||
|
||||
Component ("groups."..DOMAIN) "muc"
|
||||
modules_enabled = {
|
||||
|
@ -150,11 +259,21 @@ Component ("groups."..DOMAIN) "muc"
|
|||
"muc_local_only";
|
||||
"vcard_muc";
|
||||
"muc_defaults";
|
||||
"muc_offline_delivery";
|
||||
"snikket_restricted_users";
|
||||
"muc_auto_reserve_nicks";
|
||||
}
|
||||
restrict_room_creation = "local"
|
||||
muc_local_only = { "general@groups."..DOMAIN }
|
||||
muc_room_default_persistent = true
|
||||
|
||||
-- Default configuration for rooms (typically overwritten by the client)
|
||||
muc_room_default_allow_member_invites = true
|
||||
muc_room_default_persistent = true
|
||||
muc_room_default_public = false
|
||||
|
||||
-- Enable push notifications for offline group members by default
|
||||
-- (this also requires mod_muc_auto_reserve_nicks in practice)
|
||||
muc_offline_delivery_default = true
|
||||
|
||||
default_mucs = {
|
||||
{
|
||||
|
@ -173,6 +292,24 @@ Component ("groups."..DOMAIN) "muc"
|
|||
}
|
||||
}
|
||||
|
||||
Component ("share."..DOMAIN) "http_upload"
|
||||
Component ("share."..DOMAIN) "http_file_share"
|
||||
-- For backwards compat, allow HTTP upload on the base domain
|
||||
if ENV_SNIKKET_TWEAK_SHARE_DOMAIN ~= "1" then
|
||||
http_host = "share."..DOMAIN
|
||||
http_external_url = "https://share."..DOMAIN.."/"
|
||||
end
|
||||
|
||||
Include "/snikket/prosody/*.cfg.lua"
|
||||
-- 128 bits (i.e. 16 bytes) is the maximum length of a GCM auth tag, which
|
||||
-- is appended to encrypted uploads according to XEP-0454. This ensures we
|
||||
-- allow files up to the size limit even if they are encrypted.
|
||||
http_file_share_size_limit = (1024 * 1024 * 100) + 16 -- 100MB + 16 bytes
|
||||
http_file_share_expire_after = 60 * 60 * 24 * RETENTION_DAYS -- N days
|
||||
|
||||
if UPLOAD_STORAGE_GB then
|
||||
http_file_share_global_quota = 1024 * 1024 * 1024 * UPLOAD_STORAGE_GB
|
||||
end
|
||||
http_paths = {
|
||||
file_share = "/upload"
|
||||
}
|
||||
|
||||
Include (ENV_SNIKKET_TWEAK_EXTRA_CONFIG or "/snikket/prosody/*.cfg.lua")
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
prosodyctl --root cert import /snikket/letsencrypt/live
|
|
@ -1,8 +0,0 @@
|
|||
[program:mailhog]
|
||||
command=/usr/local/bin/mailhog -auth-file=/etc/mailhog-auth
|
||||
autorestart=true
|
||||
stopwaitsecs=30
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
umask=002
|
|
@ -2,7 +2,7 @@
|
|||
nodaemon=true
|
||||
|
||||
[program:prosody]
|
||||
command=/usr/bin/lua5.1 /usr/bin/prosody
|
||||
command=/usr/bin/lua5.2 /usr/bin/prosody -F
|
||||
priority=1000
|
||||
autorestart=true
|
||||
stopwaitsecs=30
|
||||
|
@ -27,9 +27,18 @@ umask=002
|
|||
[program:coturn]
|
||||
command=start-coturn.sh
|
||||
startsecs=0
|
||||
autorestart=true
|
||||
autorestart=unexpected
|
||||
exitcodes=0
|
||||
stopwaitsecs=30
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
umask=002
|
||||
|
||||
[program:cert-monitor]
|
||||
command=cert-monitor.sh
|
||||
startsecs=0
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
umask=002
|
||||
|
|
|
@ -43,8 +43,9 @@ alt-tls-listening-port=0
|
|||
# Lower and upper bounds of the UDP relay endpoints:
|
||||
# (default values are 49152 and 65535)
|
||||
#
|
||||
min-port=49152
|
||||
max-port=65535
|
||||
# THESE ARE OVERRIDDEN BY start-turn.sh!
|
||||
#min-port=49152
|
||||
#max-port=65535
|
||||
|
||||
# TURN REST API flag.
|
||||
# Flag that sets a special authorization option that is based upon authentication secret.
|
||||
|
@ -85,6 +86,11 @@ log-file=stdout
|
|||
#
|
||||
no-multicast-peers
|
||||
|
||||
# Disable relaying TCP traffic, this is not
|
||||
# used for A/V calls in XMPP
|
||||
#
|
||||
no-tcp-relay
|
||||
|
||||
# Turn OFF the CLI support.
|
||||
# By default it is always ON.
|
||||
# See also options cli-ip and cli-port.
|
||||
|
|
|
@ -4,6 +4,12 @@
|
|||
- hosts: all
|
||||
become: yes
|
||||
gather_facts: no
|
||||
vars:
|
||||
prosody:
|
||||
package: "prosody-trunk"
|
||||
build: "1544"
|
||||
prosody_modules:
|
||||
revision: "4abb33a15897"
|
||||
tasks:
|
||||
- import_tasks: tasks/prosody.yml
|
||||
- import_tasks: tasks/supervisor.yml
|
||||
|
|
|
@ -1,33 +1,7 @@
|
|||
---
|
||||
|
||||
- name: Install certbot
|
||||
apt:
|
||||
name: certbot
|
||||
state: present
|
||||
install_recommends: no
|
||||
- name: Create directory for certs
|
||||
file:
|
||||
state: directory
|
||||
path: /etc/ssl/certbot
|
||||
- name: Install certbot cron script
|
||||
copy:
|
||||
src: ../files/certbot.cron
|
||||
dest: /etc/cron.daily/certbot
|
||||
src: ../files/refresh-certs.cron
|
||||
dest: /etc/cron.daily/refresh-certs
|
||||
mode: 0555
|
||||
- name: Create letsencrypt group
|
||||
group:
|
||||
name: letsencrypt
|
||||
system: yes
|
||||
- name: Create letsencrypt user
|
||||
user:
|
||||
name: letsencrypt
|
||||
group: letsencrypt
|
||||
system: yes
|
||||
home: /snikket/letsencrypt
|
||||
- name: Create directory for challenges
|
||||
file:
|
||||
state: directory
|
||||
path: /var/www/.well-known
|
||||
owner: letsencrypt
|
||||
group: letsencrypt
|
||||
mode: 0755
|
||||
|
|
|
@ -10,23 +10,3 @@
|
|||
copy:
|
||||
src: ../files/msmtp.conf
|
||||
dest: /etc/msmtprc
|
||||
|
||||
- name: Download MailHog
|
||||
get_url:
|
||||
url: "https://github.com/mailhog/MailHog/releases/download/v1.0.0/MailHog_linux_amd64"
|
||||
checksum: sha256:ba921e04438e176c474d533447ae64707ffcdd1230f0153f86cb188d348f25c0
|
||||
dest: /usr/local/bin/mailhog
|
||||
mode: 0755
|
||||
tags: dev
|
||||
|
||||
- name: Add MailHog authentication
|
||||
template:
|
||||
src: "../templates/mailhog-auth"
|
||||
dest: "/etc/mailhog-auth"
|
||||
tags: dev
|
||||
|
||||
- name: Add MailHog service
|
||||
copy:
|
||||
src: "../files/supervisor-mailhog.conf"
|
||||
dest: "/etc/supervisor/conf.d/mailhog.conf"
|
||||
tags: dev
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
---
|
||||
|
||||
- name: "Install Lua 5.2"
|
||||
apt:
|
||||
name: lua5.2
|
||||
state: present
|
||||
install_recommends: no
|
||||
- name: "Add Prosody package signing key"
|
||||
apt_key:
|
||||
url: "https://packages.prosody.im/debian/pubkey.asc"
|
||||
- name: "Add Prosody package repo"
|
||||
apt_repository:
|
||||
filename: prosody
|
||||
repo: "deb https://packages.prosody.im/debian buster main"
|
||||
- name: "Detect dpkg architecture name"
|
||||
shell: dpkg --print-architecture
|
||||
register: dpkg_arch
|
||||
- name: "Install Prosody package"
|
||||
apt:
|
||||
name: prosody-trunk
|
||||
deb: "https://packages.prosody.im/debian/pool/main/p/{{ prosody.package }}/{{ prosody.package }}_1nightly{{ prosody.build }}-1~buster_{{ dpkg_arch.stdout }}.deb"
|
||||
state: present
|
||||
install_recommends: yes
|
||||
- name: "Deploy Prosody config"
|
||||
|
@ -47,7 +56,7 @@
|
|||
name: prosody
|
||||
state: stopped
|
||||
- name: "Allow Prosody to bind service ports"
|
||||
command: setcap 'cap_net_bind_service=+ep' /usr/bin/lua5.1
|
||||
command: setcap 'cap_net_bind_service=+ep' /usr/bin/lua5.2
|
||||
|
||||
- name: Install Mercurial
|
||||
apt:
|
||||
|
@ -59,10 +68,10 @@
|
|||
hg:
|
||||
repo: https://hg.prosody.im/prosody-modules
|
||||
dest: /usr/local/lib/prosody-modules
|
||||
revision: default
|
||||
revision: "{{ prosody_modules.revision }}"
|
||||
purge: yes
|
||||
update: yes
|
||||
- name: Enable wanted modules
|
||||
- name: Enable wanted modules (prosody-modules)
|
||||
file:
|
||||
state: link
|
||||
src: "/usr/local/lib/prosody-modules/{{item}}"
|
||||
|
@ -70,15 +79,17 @@
|
|||
loop:
|
||||
- mod_smacks
|
||||
- mod_cloud_notify
|
||||
- mod_invite
|
||||
- mod_cloud_notify_extensions
|
||||
- mod_cloud_notify_encrypted
|
||||
- mod_cloud_notify_priority_tag
|
||||
- mod_cloud_notify_filters
|
||||
- mod_block_registrations
|
||||
- mod_compact_resource
|
||||
- mod_conversejs
|
||||
- mod_http_upload
|
||||
- mod_lastlog
|
||||
- mod_migrate_http_upload
|
||||
- mod_lastlog2
|
||||
- mod_limit_auth
|
||||
- mod_password_policy
|
||||
- mod_password_reset
|
||||
- mod_roster_allinall
|
||||
- mod_strict_https
|
||||
- mod_vcard_muc
|
||||
|
@ -87,32 +98,93 @@
|
|||
- mod_http_altconnect
|
||||
- mod_bookmarks
|
||||
- mod_default_bookmarks
|
||||
- mod_muc_defaults
|
||||
- mod_muc_local_only
|
||||
- mod_firewall
|
||||
- mod_turncredentials
|
||||
- mod_admin_notify
|
||||
- mod_http_oauth2
|
||||
- mod_http_admin_api
|
||||
- mod_rest
|
||||
- mod_groups_migration
|
||||
- mod_invites
|
||||
- mod_invites_adhoc
|
||||
- mod_invites_api
|
||||
- mod_invites_groups
|
||||
- mod_invites_page
|
||||
- mod_invites_register
|
||||
- mod_invites_register_api
|
||||
- mod_invites_tracking
|
||||
- mod_groups_internal
|
||||
- mod_groups_muc_bookmarks
|
||||
- mod_muc_defaults
|
||||
- mod_muc_local_only
|
||||
- mod_muc_offline_delivery
|
||||
- mod_http_host_status_check
|
||||
- mod_measure_process
|
||||
- mod_prometheus
|
||||
- mod_spam_reporting
|
||||
- mod_watch_spam_reports
|
||||
- mod_isolate_host
|
||||
- mod_muc_auto_reserve_nicks
|
||||
- mod_measure_active_users
|
||||
|
||||
|
||||
- name: Install Bootstrap and JS libs
|
||||
apt:
|
||||
name:
|
||||
- libjs-bootstrap4
|
||||
- libjs-jquery
|
||||
install_recommends: no
|
||||
|
||||
- name: Enable wanted modules
|
||||
- name: Enable wanted modules (snikket-modules)
|
||||
file:
|
||||
state: link
|
||||
src: "/usr/local/lib/snikket-modules/{{item}}"
|
||||
dest: "/etc/prosody/modules/{{item}}"
|
||||
loop:
|
||||
- mod_landing_page
|
||||
- mod_invites
|
||||
- mod_invites_page
|
||||
- mod_invites_register
|
||||
- mod_invites_api
|
||||
- mod_easy_invite
|
||||
- mod_http_acme_challenge
|
||||
- mod_http_libjs
|
||||
- mod_update_check
|
||||
- mod_authz_internal
|
||||
- mod_update_notify
|
||||
- mod_invites_default_group
|
||||
- mod_invites_bootstrap
|
||||
- mod_snikket_client_id
|
||||
- mod_snikket_ios_preserve_push
|
||||
- mod_snikket_restricted_users
|
||||
|
||||
- name: "Install lua-ossl for encrypted push notifications"
|
||||
apt:
|
||||
name: lua-luaossl
|
||||
state: present
|
||||
install_recommends: no
|
||||
|
||||
- name: "Fetch luaunbound source"
|
||||
get_url:
|
||||
#url: https://code.zash.se/dl/luaunbound/luaunbound-0.5.tar.gz
|
||||
url: https://matthewwild.co.uk/uploads/luaunbound-0.5.tar.gz
|
||||
sha256sum: a6564ac1cca6bb350576eb2a5cfa03adb0aafd4f99d6cd491bd8028d046c62a7
|
||||
dest: /tmp/luaunbound-0.5.tar.gz
|
||||
|
||||
- name: "Extract luaunbound"
|
||||
unarchive:
|
||||
src: /tmp/luaunbound-0.5.tar.gz
|
||||
remote_src: yes
|
||||
dest: /tmp
|
||||
|
||||
- name: "Install libunbound-dev"
|
||||
apt:
|
||||
name:
|
||||
- libunbound8
|
||||
- libunbound-dev
|
||||
- liblua5.2-dev
|
||||
state: present
|
||||
|
||||
- name: "Build luaunbound"
|
||||
make:
|
||||
chdir: /tmp/luaunbound-0.5
|
||||
|
||||
- name: "Install luaunbound"
|
||||
make:
|
||||
chdir: /tmp/luaunbound-0.5
|
||||
target: install
|
||||
|
||||
- name: "Remove luaunbound source"
|
||||
file:
|
||||
path: /tmp/luaunbound-0.5
|
||||
state: absent
|
||||
|
||||
- name: "Remove libunbound-dev"
|
||||
apt:
|
||||
name:
|
||||
- libunbound-dev
|
||||
- liblua5.2-dev
|
||||
state: absent
|
||||
|
|
|
@ -5,3 +5,8 @@
|
|||
src: "../files/bin/"
|
||||
dest: "/usr/local/bin/"
|
||||
mode: 0755
|
||||
|
||||
- name: "Install qrencode"
|
||||
apt:
|
||||
name: qrencode
|
||||
state: present
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
snikket:{{ lookup('password', '/tmp/mailhog-password length=15')|password_hash("bcrypt", lookup('password', '/tmp/mailhog-salt length=21 chars=letters,digits')+".") }}
|
|
@ -1,15 +1,39 @@
|
|||
version: "3.3"
|
||||
|
||||
services:
|
||||
snikket:
|
||||
image: snikket:latest
|
||||
snikket_proxy:
|
||||
container_name: snikket-proxy
|
||||
image: snikket/snikket-web-proxy:dev
|
||||
env_file: snikket.conf
|
||||
network_mode: host
|
||||
volumes:
|
||||
- type: "volume"
|
||||
source: snikket_data
|
||||
target: /snikket
|
||||
- snikket_data:/snikket
|
||||
- acme_challenges:/var/www/html/.well-known/acme-challenge
|
||||
restart: "unless-stopped"
|
||||
snikket_certs:
|
||||
container_name: snikket-certs
|
||||
image: snikket/snikket-cert-manager:dev
|
||||
env_file: snikket.conf
|
||||
volumes:
|
||||
- snikket_data:/snikket
|
||||
- acme_challenges:/var/www/.well-known/acme-challenge
|
||||
restart: "unless-stopped"
|
||||
snikket_portal:
|
||||
container_name: snikket-portal
|
||||
image: snikket/snikket-web-portal:dev
|
||||
network_mode: host
|
||||
env_file: snikket.conf
|
||||
restart: "unless-stopped"
|
||||
|
||||
snikket_server:
|
||||
container_name: snikket
|
||||
image: snikket/snikket-server:dev
|
||||
network_mode: host
|
||||
volumes:
|
||||
- snikket_data:/snikket
|
||||
env_file: snikket.conf
|
||||
restart: "unless-stopped"
|
||||
|
||||
volumes:
|
||||
acme_challenges:
|
||||
snikket_data:
|
||||
|
|
|
@ -5,6 +5,11 @@ if [ -z "$SNIKKET_DOMAIN" ]; then
|
|||
exit 1;
|
||||
fi
|
||||
|
||||
if [ -z "$SNIKKET_ADMIN_EMAIL" ]; then
|
||||
echo "Please provide SNIKKET_ADMIN_EMAIL";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
if [ -z "$SNIKKET_SMTP_URL" ]; then
|
||||
SNIKKET_SMTP_URL="smtp://localhost:1025/;no-tls"
|
||||
fi
|
||||
|
@ -25,9 +30,6 @@ PGID=${PGID:=$(stat -c %g /snikket)}
|
|||
if [ "$PUID" != 0 ] && [ "$PGID" != 0 ]; then
|
||||
usermod -o -u "$PUID" prosody
|
||||
groupmod -o -g "$PGID" prosody
|
||||
|
||||
usermod -o -u "$PUID" letsencrypt
|
||||
groupmod -o -g "$PGID" letsencrypt
|
||||
fi
|
||||
|
||||
if ! test -d /snikket/prosody; then
|
||||
|
@ -36,19 +38,18 @@ fi
|
|||
|
||||
chown -R prosody:prosody /var/spool/anacron /var/run/prosody /snikket/prosody /etc/prosody
|
||||
|
||||
if ! test -d /snikket/letsencrypt; then
|
||||
install -o letsencrypt -g letsencrypt -m 750 -d /snikket/letsencrypt;
|
||||
## Generate secret for coturn auth if necessary
|
||||
if ! test -f /snikket/prosody/turn-auth-secret-v2; then
|
||||
head -c 32 /dev/urandom | base64 > /snikket/prosody/turn-auth-secret-v2;
|
||||
fi
|
||||
|
||||
install -o letsencrypt -g letsencrypt -m 750 -d /var/lib/letsencrypt;
|
||||
install -o letsencrypt -g letsencrypt -m 750 -d /var/log/letsencrypt;
|
||||
install -o letsencrypt -g letsencrypt -m 755 -d /var/www/.well-known/acme-challenge;
|
||||
# COMPAT w/ alpha.20200513: remove older format
|
||||
if test -f /snikket/prosody/turn-auth-secret; then
|
||||
rm /snikket/prosody/turn-auth-secret;
|
||||
fi
|
||||
|
||||
chown -R letsencrypt:letsencrypt /snikket/letsencrypt
|
||||
|
||||
## Generate secret for coturn auth if necessary
|
||||
if ! test -f /snikket/prosody/turn-auth-secret; then
|
||||
head -c 32 /dev/urandom | sha256sum > /snikket/prosody/turn-auth-secret;
|
||||
if test -d /snikket/prosody/http_upload; then
|
||||
prosodyctl mod_migrate_http_upload "share.$SNIKKET_DOMAIN" "$SNIKKET_DOMAIN"
|
||||
fi
|
||||
|
||||
exec supervisord -c /etc/supervisor/supervisord.conf
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
# Advanced Configuration
|
||||
|
||||
In most situations, the configuration options shown in the example config in
|
||||
[snikket-selfhosted](https://github.com/snikket-im/snikket-selfhosted/blob/main/snikket.conf.example) should suffice. In some cases of more complex requirements (such as running behind a reverse proxy), it may be required to tweak more options.
|
||||
|
||||
## Note well
|
||||
|
||||
- **Some of these options may break your setup**
|
||||
|
||||
Do not set them unless you know what you're doing. Particularly the options with `TWEAK` in their name are to be looked at carefully.
|
||||
|
||||
- Options *only* documented here may change their behaviour between releases without further notice
|
||||
|
||||
There is no guarantee about any of the options documented *only* here. Some are experimental, some are reserved for specific uncommon use cases (for which the support may be dropped eventually), others only exist to glue Snikket components together and should not be touched at all.
|
||||
|
||||
Also, it is very likely not complete.
|
||||
|
||||
## Configuration Option Reference
|
||||
|
||||
This reference is in no particular order. Most importantly, it is certainly not in the order of "things you should try to mess with come first".
|
||||
|
||||
### `SNIKKET_DOMAIN`
|
||||
|
||||
The domain name of your Snikket instance. Do not change this after it was once set.
|
||||
|
||||
### `SNIKKET_RETENTION_DAYS`
|
||||
|
||||
The number of days (as integer) for which your server should preserve messages so that all devices of a user can catch up, even if they end up being disconnected from the internet for a while.
|
||||
|
||||
The Snikket Server stores all messages which are sent to any user for the given number of days. As end-to-end encryption is used, no plaintext is generally stored, only encrypted messages. These messages are then decrypted only on the devices of the specific user.
|
||||
|
||||
It is recommended to set this number not too small. If a device is offline for longer than the number of days this option is set to, it will not receive all messages, which is generally a bad user experience. Note that it does no matter if any other device has received the messages: If a user has only a single device and is offline for more days than the retention period is set to, they will lose messages.
|
||||
|
||||
On the other hand, storing too many messages on the server causes impacts on server performance and data hygiene in general. The default of seven is considered reasonable in the sense that most users won't be offline for longer than that.
|
||||
|
||||
Changing this option to a lower value will delete messages from the server. Changing this option to a higher value will allow messages existing on the server to be retained for longer.
|
||||
|
||||
### `SNIKKET_UPLOAD_STORAGE_GB`
|
||||
|
||||
Use this option to place a limit on the amount of storage Snikket will use for files shared by users. You can use this to prevent your server's disk capacity being consumed if users upload many large files. By default there is no limit.
|
||||
|
||||
If the limit is reached, users will be unable to upload new files until older files are cleared by Snikket after the configured retention period (or the limit is increased).
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
# Allow no more than 1.5GB disk space to be used by uploaded files
|
||||
SNIKKET_UPLOAD_STORAGE_GB=1.5
|
||||
```
|
||||
|
||||
The amount of file storage used is affected by the configured retention period (7 days by default) - i.e. longer retention periods will mean files are stored for longer, and more space will be used. Take this into account when choosing a value.
|
||||
|
||||
### `SNIKKET_LOGLEVEL`
|
||||
|
||||
Control the detail level of the log output of the snikket server.
|
||||
|
||||
Valid options are `error`, `warn`, `info` (the default) and `debug`. The `debug` log level is very detailed. It may quickly fill your disk and also contain more sensitive information.
|
||||
|
||||
### `SNIKKET_SITE_NAME`
|
||||
|
||||
A human-friendly name for your server. Defaults to the value of `SNIKKET_DOMAIN`.
|
||||
|
||||
### `SNIKKET_UPDATE_CHECK`
|
||||
|
||||
By default, Snikket sends anonymous requests for the latest release via DNS, to provide you with a notification when a new release is available (which may contain important security fixes). This behaviour can be disabled by setting the option to `0`.
|
||||
|
||||
This will not expose your server's IP address or domain name to the Snikket org, as it will generally be proxied through your or your hosters Internet Service Provider's DNS servers.
|
||||
|
||||
### `SNIKKET_ADMIN_EMAIL`
|
||||
|
||||
Email address of the admin. This will be sent to all new users as contact information.
|
||||
|
||||
### `SNIKKET_WEB_AVATAR_CACHE_TTL`
|
||||
|
||||
The time (in seconds) for which the web portal will allow avatars to be cached by browsers.
|
||||
|
||||
|
||||
## Arcane Configuration Reference
|
||||
|
||||
**The options below this line are even more arcane than the options above. Do not touch unless you truly know what you're doing.**
|
||||
|
||||
### `SNIKKET_TWEAK_INTERNAL_HTTP_PORT`
|
||||
|
||||
The TCP port on which the internal HTTP API listens on. The default is `5280`. Do not change this without also changing `SNIKKET_WEB_PROSODY_ENDPOINT` accordingly.
|
||||
|
||||
### `SNIKKET_TWEAK_INTERNAL_HTTP_INTERFACE`
|
||||
|
||||
The IP address on which the internal HTTP API listens on. The default is `127.0.0.1`, so that the API is only accessible from the same server. Changing this may be a security risk as some general system information is accessible without authentication.
|
||||
|
||||
### `SNIKKET_INVITE_URL`
|
||||
|
||||
The URL template for invitation links. The server needs to know under which address the invitation service is hosted.
|
||||
|
||||
Changing this will most likely break your invitation flow, so better don't.
|
||||
|
||||
### `TWEAK_SNIKKET_BOOTSTRAP_INDEX`
|
||||
|
||||
Just do not set this.
|
||||
|
||||
### `TWEAK_SNIKKET_BOOTSTRAP_SECRET`
|
||||
|
||||
Also better do not set this.
|
||||
|
||||
### `SNIKKET_TWEAK_IPV6`
|
||||
|
||||
Enable IPv6 support.
|
||||
|
||||
By default, IPv6 is disabled because most container runtimes default to it being disabled. Enabling IPv6 in the server could cause issues if the container runtime does not support it.
|
||||
|
||||
### `SNIKKET_TWEAK_PROMETHEUS`
|
||||
|
||||
If you are monitoring your Snikket server using [Prometheus](https://prometheus.io/) and scraping the metrics endpoint, you should set this to `1` and let it at its default otherwise.
|
||||
|
||||
If this is set to `1` without Snikket server being scraped by Prometheus, the System Health panel in the web portal will not work correctly. If this is not set to `1` when Snikket is being scraped by Prometheus, the numbers seen by Prometheus may not be accurate at the time they are being sampled, as Snikket server will in that case sample data only every 60s, no matter how often or when you scrape.
|
||||
|
||||
The default is safe for non-Prometheus setups.
|
||||
|
||||
### `SNIKKET_TWEAK_TURNSERVER`
|
||||
|
||||
By default, Snikket starts a STUN/TURN server. If this option is set to `0`, it will not do that. You will have to run your own STUN/TURN server and configure `SNIKKET_TWEAK_TURNSERVER_DOMAIN` and `SNIKKET_TWEAK_TURNSERVER_SECRET` accordingly.
|
||||
|
||||
If `SNIKKET_TWEAK_TURNSERVER` is set to `0` and `SNIKKET_TWEAK_TURNSERVER_DOMAIN` is not set, no STUN/TURN server will be offered to your users. Terrible idea to do that, will break audio/video calls in all but the most ideal situations.
|
||||
|
||||
### `SNIKKET_TWEAK_TURNSERVER_DOMAIN`
|
||||
|
||||
Hostname of the STUN/TURN server to use.
|
||||
|
||||
Defaults to the Snikket domain, as snikket-server runs contains its own STUN/TURN server.
|
||||
|
||||
### `SNIKKET_TWEAK_TURNSERVER_SECRET`
|
||||
|
||||
Shared secret to use with the STUN/TURN server for authentication of clients.
|
||||
|
||||
Defaults to a secret which is generated once at first installation. Only override this if you also set `SNIKKET_TWEAK_TURNSERVER` to `0` and set `SNIKKET_TWEAK_TURNSERVER_DOMAIN` to a STUN/TURN server you operate manually.
|
||||
|
||||
### `SNIKKET_TWEAK_SHARE_DOMAIN`
|
||||
|
||||
Expose the file share service at the `SNIKKET_DOMAIN` instead of at `share.SNIKKET_DOMAIN`.
|
||||
|
||||
This nowadays conflicts with the web portal, so you should not set it.
|
||||
|
||||
### `SNIKKET_TWEAK_EXTRA_CONFIG`
|
||||
|
||||
Path or glob for extra configuration files to load.
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
title: Advanced DNS setup
|
||||
---
|
||||
|
||||
The quick start guide helps you set up the essential DNS records for your Snikket
|
||||
service. There are a few additional records you can add to unlock some features
|
||||
of your Snikket service.
|
||||
|
||||
## Hosting on alternative XMPP ports
|
||||
|
||||
The default XMPP ports are:
|
||||
|
||||
- 5222 (for connections from the app and other XMPP clients)
|
||||
- 5269 (for connections from other Snikket/XMPP servers, i.e. federation)
|
||||
|
||||
If you want to change these, you can add a type of DNS record called SRV records.
|
||||
|
||||
Unfortunately SRV records setup varies widely between different DNS providers, so
|
||||
you'll need to figure out which info to put where yourself, based on the example
|
||||
records shown here.
|
||||
|
||||
An SRV record to override the client port looks like this:
|
||||
|
||||
```
|
||||
_xmpp-client._tcp.chat.example.com. 18000 IN SRV 0 0 5222 chat.example.com.
|
||||
```
|
||||
|
||||
While an SRV record to override the server-to-server port looks like this:
|
||||
|
||||
```
|
||||
_xmpp-server._tcp.chat.example.com. 18000 IN SRV 0 0 5269 chat.example.com.
|
||||
```
|
||||
|
||||
## Client connections through HTTPS port
|
||||
|
||||
It's possible to enable the client to connect through port 443 (the HTTPS port), which
|
||||
can allow bypassing some very restrictive firewalls.
|
||||
|
||||
Firstly, you need to set up sslh, as described in the [reverse proxy](reverse_proxy.md#sslh)
|
||||
documentation. Then you need to add the following SRV record:
|
||||
|
||||
```
|
||||
_xmpps-client._tcp.chat.example.com. 86400 IN SRV 5 0 443 chat.example.com.
|
||||
```
|
||||
|
||||
Note the 's' in `_xmpps-client`! The other differences in this record are that we set the port
|
||||
to 443 (the HTTPS port), and the priority to '5', so that clients supporting this connection
|
||||
method will prefer it over other connection methods (we specified priority '0' in the `_xmpp-client`
|
||||
example above).
|
|
@ -0,0 +1,83 @@
|
|||
# Firewall
|
||||
|
||||
## Ports
|
||||
|
||||
Snikket currently requires the following ports to be open/forwarded:
|
||||
|
||||
|
||||
|
||||
|
||||
|**TCP only** | |
|
||||
| :------------ | :--------------------------------------------------------------------------------- |
|
||||
| 80/443 | Web Interface And Group File Sharing Service (HTTP(S)) |
|
||||
| 5222 | Client App Connections (Client to Server) (XMPP-c2s) |
|
||||
| 5269 | Federation With Other Snikket Servers (Server to Server) (XMPP-s2s) |
|
||||
| 5000 | File Transfer Proxy (proxy65) |
|
||||
|
||||
|
||||
|**TCP and UDP**| |
|
||||
| :-------------| :--------------------------------------------------------------------------------- |
|
||||
| 3478/3479 | Audio/Video Data Proxy Negotiation and IP discovery <br /> (STUN/TURN) |
|
||||
| 5349/5350 | Audio/Video Data Proxy Negotiations and IP Discovery over TLS <br /> (STUN/TURN over TLS) |
|
||||
|
||||
|
||||
|**UDP only** | |
|
||||
| :----------- | :----------------------------------------------------------------------------------|
|
||||
| 49152-65535 | Audio/Video Data Proxy (Turn Data, see below) |
|
||||
|
||||
|
||||
## Changing the turnserver port range
|
||||
|
||||
The STUN/TURN server is required for audio/video (A/V) calls to work reliably on all kinds of "difficult" client networks. For this, a relay connection is established which routes the (encrypted) A/V data via your Snikket server. As generally the number of concurrent calls is not known and it needs to compete with ports already in use on the machine, the TURN server defaults to a range with a high number of ports (about 16 thousand). See below for recommendations on picking a smaller number of ports.
|
||||
|
||||
However, some appliances will not allow forwarding a large range of UDP ports as normally required for TURN. If you have to forward ports through such an appliance, you can tweak the port range used by the STUN/TURN server using the following two configuration options:
|
||||
|
||||
* `SNIKKET_TWEAK_TURNSERVER_MIN_PORT`: Set the lower bound of the port range (default: 49152)
|
||||
* `SNIKKET_TWEAK_TURNSERVER_MAX_PORT`: Set the upper bound of the port range (default: 65535)
|
||||
|
||||
Both numbers must be larger than 1024 and smaller than or equal to 65535. Keeping them above 40000 is generally recommended for network standards reasons. Obviously, the min number must be less than or equal to the max number.
|
||||
|
||||
Example for a range of 1024 ports (in your snikket.conf):
|
||||
|
||||
```
|
||||
SNIKKET_TWEAK_TURNSERVER_MIN_PORT=60000
|
||||
SNIKKET_TWEAK_TURNSERVER_MAX_PORT=61023
|
||||
```
|
||||
|
||||
Make sure to restart the `snikket` container after changing this option and ideally test A/V calls with two phones on different mobile data providers (those are generally most tricky to get working).
|
||||
|
||||
### How many ports does the TURN service need?
|
||||
|
||||
In general, you can safely assume that a call will never need more than four ports at the same time. That means that with 200 ports, you could in theory initiate up to 50 concurrent calls on your Snikket instance.
|
||||
|
||||
However, these ports are a system-wide resource. A port may only be used by a single application at the same time (this is an oversimplification). That means that if your server machine is "rather busy", "many" of the ports in the range you designate for the TURN service may be in use already by other applications. This in turn means that a call may randomly fail to establish based on whether enough ports are available in the range you chose.
|
||||
|
||||
Unless you are running an *extremely* busy service on your server, you should be fine if you plan wih 10% headroom. <!-- I checked how many "high ports" (5 digits) were open on the search.jabber.network xmppd at a random point in time, and they were just 800. Given that the high port range has 50k ports and that most users are not going to run a busy service as that, it should be fine. -->
|
||||
|
||||
That means that if you have 20 users and want to allow them to start calls at the same time (ignoring *who* they'd call), you should plan for 80 ports, plus 10% head room, gives you about 90 ports.
|
||||
|
||||
## Configuring UFW to Allow Ports for Snikket
|
||||
|
||||
[UFW](https://wiki.ubuntu.com/UncomplicatedFirewall), the Uncomplicated Firewall, is a user-friendly interface to the more complicated iptables commands that control a Linux systems's firewall.
|
||||
|
||||
It is possible to manually add each of the above ports with `ufw` commands like the following: `# ufw allow 5000/tcp comment 'File Transfer Proxy (proxy65)'`, however, doing so is tedious and clutters the output of `# ufw status`. A better way is to create a custom ufw application, which we will call "Snikket" and have ufw add rules for that application. This is not only easier and declarative but also has the advantage of yielding a clean `# ufw status` report that looks as follows:
|
||||
|
||||
```
|
||||
To Action From
|
||||
-- ------ ----
|
||||
Snikket ALLOW Anywhere
|
||||
```
|
||||
|
||||
Create the following file at `/etc/ufw/applications.d/ufw-snikket`. I have opted to open UDP ports 6000-6200 in the following example, but you should change this to reflect which TURN ports your Snikket configuration specifies.
|
||||
|
||||
```
|
||||
[Snikket]
|
||||
title=Snikket Server
|
||||
description=Simple XMPP Server
|
||||
ports=80/tcp|443/tcp|5222/tcp|5269/tcp|5000/tcp|3478|3479|5349|5350|6000:6200/udp
|
||||
```
|
||||
|
||||
Add the new rule:
|
||||
`# ufw allow snikket`
|
||||
|
||||
Running `# ufw status` should now show Snikket as a rule. If you want to see all the specific ports that have been allowed by adding this rule you can run `# ufw status verbose`.
|
|
@ -0,0 +1,200 @@
|
|||
---
|
||||
title: Reverse proxies
|
||||
---
|
||||
|
||||
# Running Snikket behind a reverse proxy
|
||||
|
||||
The default Snikket setup assumes that there is no other HTTP/HTTPS server
|
||||
running. If you already have another web server running for example, you will
|
||||
need to instruct it to forward Snikket traffic to Snikket.
|
||||
|
||||
!!! note
|
||||
|
||||
A quick note about non-HTTP services. Snikket includes a number of non-HTTP
|
||||
services which cannot be routed through a HTTP reverse proxy. This includes
|
||||
XMPP, STUN and TURN. The documentation here applies to redirecting the HTTP
|
||||
and HTTPS ports (80 and 443) through a reverse proxy only.
|
||||
|
||||
# Certificates
|
||||
|
||||
It is important to get certificates correct when deploying Snikket behind a reverse
|
||||
proxy. Snikket needs to obtain certificates from Let's Encrypt in order to secure
|
||||
the non-HTTP services it provides. Be careful that your reverse proxy does not
|
||||
intercept requests from Let's Encrypt that are intended for the Snikket service.
|
||||
|
||||
# Configuration
|
||||
|
||||
## Snikket
|
||||
|
||||
First we need to tell Snikket to use alternative ports, so that it doesn't conflict
|
||||
with the primary web server/proxy that will be forwarding the traffic. This can be
|
||||
done by adding the following lines to /etc/snikket/snikket.conf:
|
||||
|
||||
```
|
||||
SNIKKET_TWEAK_HTTP_PORT=5080
|
||||
SNIKKET_TWEAK_HTTPS_PORT=5443
|
||||
```
|
||||
|
||||
You can choose any alternative ports that you would prefer, but the rest of this
|
||||
documentation will assume you use the ports given in this example.
|
||||
|
||||
In the next step, you need to configure the web server to forward traffic to
|
||||
Snikket on these ports. Follow the section below according to which web server
|
||||
you are using.
|
||||
|
||||
## Web servers
|
||||
|
||||
Each web server is different, so here we provide some example configuration snippets
|
||||
for the most common servers. Feel free to contribute any that you would like to see
|
||||
included!
|
||||
|
||||
### Nginx
|
||||
|
||||
```
|
||||
server {
|
||||
# Accept HTTP connections
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name chat.example.com;
|
||||
server_name groups.chat.example.com;
|
||||
server_name share.chat.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:5080/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# A bit of headroom over the 16MB accepted by Prosody.
|
||||
client_max_body_size 20M;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
# Accept HTTPS connections
|
||||
listen [::]:443 ssl ipv6only=on;
|
||||
listen 443 ssl;
|
||||
ssl_certificate /path/to/certificate.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
server_name chat.example.com;
|
||||
server_name groups.chat.example.com;
|
||||
server_name share.chat.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass https://localhost:5443/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# REMOVE THIS IF YOU CHANGE `localhost` TO ANYTHING ELSE ABOVE
|
||||
proxy_ssl_verify off;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_ssl_server_name on;
|
||||
|
||||
# A bit of headroom over the 16MB accepted by Prosody.
|
||||
client_max_body_size 20M;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** You may modify the first server block to include a redirect to HTTPS
|
||||
instead of proxying plain-text HTTP traffic. When doing that, take care to
|
||||
proxy `.well-known/acme-challenge` even in plain text to allow Snikket to
|
||||
obtain certificates.
|
||||
|
||||
### sslh
|
||||
|
||||
sslh is a little different to the other servers listed here, as it is not a web server. However it is able
|
||||
to route encrypted traffic (such as HTTPS and even some kinds of XMPP traffic) to different places.
|
||||
|
||||
The snippet below lists the rules required to forward all of Snikket's traffic to Snikket. Don't forget that
|
||||
Snikket will also need port 80 forwarded to 5080 somehow (otherwise it won't be able to obtain certificates).
|
||||
|
||||
Unlike the other solutions here, this approach also allows you to run encrypted XMPP through the HTTPS port.
|
||||
To take full advantage of this feature, you will need to add additional DNS records. See [advanced DNS](dns.md)
|
||||
for more information.
|
||||
|
||||
This configuration requires sslh 1.18 or higher.
|
||||
|
||||
```
|
||||
listen:
|
||||
(
|
||||
{ host: "0.0.0.0"; port: "443"; },
|
||||
);
|
||||
|
||||
protocols:
|
||||
(
|
||||
## Snikket rules
|
||||
# Send encrypted XMPP traffic directly to Snikket (this must be above the HTTPS rules)
|
||||
{ name: "tls"; host: "127.0.0.1"; port: "5223"; alpn_protocols: [ "xmpp-client" ]; },
|
||||
# Send HTTPS traffic to Snikket's HTTPS port
|
||||
{ name: "tls"; host: "127.0.0.1"; port: "5443"; sni_hostnames: [ "chat.example.com", "groups.chat.example.com", "share.chat.example.com" ] },
|
||||
# Send unencrypted XMPP traffic to Snikket (will use STARTTLS)
|
||||
{ name: "xmpp"; host: "127.0.0.1"; port: "5222"; },
|
||||
|
||||
## Other rules
|
||||
# Add rules here to forward any other hosts/protocols to non-Snikket destinations
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
### apache
|
||||
|
||||
**Note**: The following configuration is for reverse proxying from another machine
|
||||
(other from the one hosting Snikket containers). A prerequisite is a mechanism to sync
|
||||
Snikket-managed letsencrypt TLS key and cert to `/opt/chat/letsencrypt`. This is required because
|
||||
Apache 2.4 is not able to revproxying based on SNI, routing encrypted TLS directly to the Snikket machine.
|
||||
If the containers are on the same machine
|
||||
of the reverse proxy, you have to tweak HTTP/S ports as indicated before, and you don't need
|
||||
to proxy over SSL.
|
||||
|
||||
```
|
||||
<VirtualHost *:443>
|
||||
|
||||
ServerName chat.example.com
|
||||
ServerAlias groups.chat.example.com
|
||||
ServerAlias share.chat.example.com
|
||||
|
||||
ServerAdmin webmaster@localhost
|
||||
|
||||
DocumentRoot /var/www/chat
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/chat.example.com-ssl_error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/chat.example.com-ssl_access.log combined
|
||||
|
||||
SSLEngine on
|
||||
|
||||
#
|
||||
SSLCertificateFile /opt/chat/letsencrypt/chat.example.com/cert.pem
|
||||
SSLCertificateKeyFile /opt/chat/letsencrypt/chat.example.com/privkey.pem
|
||||
SSLCertificateChainFile /opt/chat/letsencrypt/chat.example.com/chain.pem
|
||||
|
||||
SSLProxyEngine On
|
||||
ProxyPreserveHost On
|
||||
|
||||
ProxyPass / https://chat.example.com/
|
||||
ProxyPassReverse / https://chat.example.com/
|
||||
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:80>
|
||||
|
||||
ServerName chat.example.com
|
||||
ServerAlias groups.chat.example.com
|
||||
ServerAlias share.chat.example.com
|
||||
|
||||
ServerAdmin webmaster@localhost
|
||||
|
||||
DocumentRoot /var/www/chat
|
||||
|
||||
ProxyPreserveHost On
|
||||
|
||||
ProxyPass / http://chat.example.com/
|
||||
ProxyPassReverse / http://chat.example.com/
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/chat.example.com_error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/chat.example.com_access.log combined
|
||||
|
||||
</VirtualHost>
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
# Update notifications
|
||||
|
||||
This is an informational technical document about the update notification
|
||||
system in Snikket server.
|
||||
|
||||
## Why are update notifications important?
|
||||
|
||||
It is now widely known that [outdated software][OWASP-A9] is one of the
|
||||
biggest risk factors in securing systems on the internet. Therefore the
|
||||
Snikket server will alert all admins to available updates and important
|
||||
notices from the Snikket team.
|
||||
|
||||
We believe it is up to you to decide when and how to update your service.
|
||||
But we will provide you with the tools you need to make that easy, fast
|
||||
and painless.
|
||||
|
||||
## How are they implemented?
|
||||
|
||||
To preserve your privacy, private Snikket servers do not make requests
|
||||
directly to our servers. Instead we put the necessary information about
|
||||
current releases and security updates into our DNS records.
|
||||
|
||||
## Why did you choose DNS?
|
||||
|
||||
The obvious choice was HTTP, and this is how most traffic on the internet
|
||||
is conveyed these days. But we opted for DNS due to the following advantages:
|
||||
|
||||
- DNS is designed for serving small amounts of data from one place to many
|
||||
- Due to caching, and its connectionless nature, DNS is more scalable
|
||||
- Queries will often travel via an intermediate resolver, so we
|
||||
typically won't have access to your server's IP address
|
||||
- A DNS query contains very little information, whereas HTTP will always
|
||||
leak the IP address, and by default will often leak other headers.
|
||||
|
||||
But it also has some known downsides. In particular DNS is not secure by
|
||||
default. Intermediaries may observe or drop the query, or even modify the
|
||||
response.
|
||||
|
||||
The following conclusions were made about the downsides:
|
||||
|
||||
- Observability: an intermediary seeing outbound queries to our DNS
|
||||
records may deduce that your server is running Snikket. This should
|
||||
not be a problem in itself - there are many ways to detect if a server
|
||||
is running Snikket (load up its web page for a start!).
|
||||
- Availability: an intermediary may block queries for our DNS records.
|
||||
This would prevent a server admin from receiving update notifications,
|
||||
which is bad (they may be tricked into thinking they are up to date).
|
||||
However using another protocol such as HTTP(S) would not prevent this
|
||||
focused attack.
|
||||
- Integrity: the data returned to the Snikket server may be modified or
|
||||
spoofed by an intermediary. This would allow them to trigger false
|
||||
update notifications. We have designed the system so that the risk is
|
||||
minimized - the update notifications will always include a link to the
|
||||
real announcement on snikket.org (if any). It is not possible to direct
|
||||
admins to arbitrary URLs.
|
||||
|
||||
It is possible that in the future we will add support for DNSSEC or manually
|
||||
sign the data provided in our DNS records.
|
||||
|
||||
It is also possible that we will move to another mechanism in the future, if
|
||||
a more suitable one can be found.
|
||||
|
||||
## The details
|
||||
|
||||
Snikket releases are organized into 'channels', e.g. 'dev', 'alpha', 'beta',
|
||||
'stable'. Your server will work out the channel it belongs to, and make a DNS
|
||||
query to:
|
||||
|
||||
```
|
||||
TXT _channel.update.snikket.net
|
||||
```
|
||||
|
||||
The response will look like:
|
||||
|
||||
```
|
||||
"latest=3"
|
||||
"secure=2"
|
||||
"msg=0"
|
||||
```
|
||||
|
||||
This response indicates that version '3' is the latest, but version '2' is the
|
||||
last release with no known security vulnerabilities is '2'. The `msg` field
|
||||
allows us to send important announcements that may not be included in a release.
|
||||
|
||||
A Snikket server will use the returned information to determine whether the
|
||||
administrators need to be notified, and generate a message if necessary. Since
|
||||
the server has no further information, the message will include a link to the
|
||||
relevant announcement on the snikket.org website by calculating the URL to use.
|
||||
|
||||
## Disabling update checks
|
||||
|
||||
We strongly recommend you leave update notifications enabled so that you are
|
||||
notified promptly about important releases and announcements. However if you
|
||||
plan to receive these another way, you may disable them by adding to your
|
||||
snikket.conf:
|
||||
|
||||
```
|
||||
SNIKKET_UPDATE_CHECK=0
|
||||
```
|
||||
|
||||
[OWASP-A9]: https://owasp.org/www-project-top-ten/2017/A9_2017-Using_Components_with_Known_Vulnerabilities
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
title: User roles
|
||||
---
|
||||
|
||||
# User roles
|
||||
|
||||
Snikket allows you to select a role for users, each role granting different
|
||||
permissions.
|
||||
|
||||
Each user may have one of three roles:
|
||||
|
||||
## Administrator
|
||||
|
||||
This is the default role of the first user (if you're reading this, that's
|
||||
probably you!).
|
||||
|
||||
Administrators have full control over the server, settings, users and circles.
|
||||
These features can be accessed primarily through the admin panel in the
|
||||
Snikket web interface.
|
||||
|
||||
## Normal
|
||||
|
||||
This is the default role for most users. It gives access to all
|
||||
non-administrative server functionality.
|
||||
|
||||
## Limited
|
||||
|
||||
Limited users have various restrictions. The purpose of this role is to
|
||||
allow granting someone an account on the server, only for the purposes of
|
||||
communicating with other people on that server. This can be useful to provide
|
||||
a guest or child account, for example.
|
||||
|
||||
In particular, limited users are not allowed to:
|
||||
|
||||
- Communicate with users on other servers
|
||||
- Join group chats on other servers
|
||||
- Create public channels (including on the current server)
|
||||
- Invite new users to the server (regardless of whether this is enabled for
|
||||
normal users).
|
||||
|
||||
### Caveats
|
||||
|
||||
The current support for limited users has some known issues. It is designed to
|
||||
prevent casual misuse of the server, but it is not intended to be a foolproof
|
||||
security measure. For example, limited users are still able to *receive*
|
||||
messages and contact requests from other servers, even though they cannot send
|
||||
them to other servers. It is expected that we will restrict incoming traffic
|
||||
for limited users in a future release, after further testing.
|
||||
|
||||
Also note that limited accounts may have issues using non-Snikket mobile apps
|
||||
that use push notifications, depending on the design of the app. This is
|
||||
because the restrictions may prevent the app communicating with its'
|
||||
developer's push notification services over XMPP.
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
title: Snikket service documentation
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
The Snikket Service is the core part of running your own messaging service. It provides
|
||||
everything required for the Snikket app on users' devices to connect and exchange messages,
|
||||
media and calls.
|
||||
|
||||
If you are looking to set up Snikket for the first time, start with the [quick start guide](setup/quickstart.md).
|
||||
|
||||
After setup, if you want to explore further, check out the advanced topics.
|
||||
|
||||
# Feedback
|
||||
|
||||
We're always looking to improve Snikket further. Feel free to reach out at <feedback@snikket.org>.
|
||||
Let us know if you encountered any difficulty setting up, or even if it was a breeze!
|
|
@ -0,0 +1,175 @@
|
|||
---
|
||||
title: "Quick-start guide"
|
||||
date: 2020-01-14T16:32:02Z
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
Hi, welcome! This is a guide to help you set up your own [Snikket service](/service/). Once it is set up,
|
||||
you will be able to invite others to join you using the [Snikket app](/app/) and chat over your own
|
||||
private messaging server!
|
||||
|
||||
Right, let's get started...
|
||||
|
||||
!!! warning
|
||||
|
||||
Heads up! Snikket is currently in its early stages (we launched at FOSDEM 2020!). Although you can
|
||||
use Snikket today, there are many features that are still to come, and we're working on improving the setup
|
||||
experience.
|
||||
|
||||
If you have any questions, feedback, or words of encouragement, we'd love to hear from you! Email us at
|
||||
feedback@snikket.org.
|
||||
|
||||
## Requirements
|
||||
|
||||
To follow this guide you will need:
|
||||
|
||||
- A server running Linux that you have SSH or terminal access to
|
||||
- A domain name that you can create subdomains on
|
||||
|
||||
For the server, you can use a VPS from a provider such as [DigitalOcean](https://digitalocean.com/) (you can use this [referral link for $100 credit](https://m.do.co/c/3ade5a32d0e0)),
|
||||
or you can use a physical device such as a Raspberry Pi. Note that if you run your server at home (which is _really_ cool!) you may need to forward some ports on your
|
||||
router.
|
||||
|
||||
!!! warning
|
||||
|
||||
**Important:** Snikket provides a built-in web server that must be accessible on port 80. Therefore this guide assumes you are _not_ running any existing
|
||||
websites on the same server. We are working to remove this requirement in a future version.
|
||||
|
||||
## Get Started
|
||||
|
||||
### Step 1: DNS
|
||||
|
||||
First you need to find your server's public ("external") IP address. If you are using a hosted server, this may be shown in your management dashboard.
|
||||
At a pinch you can use an online service, e.g. by running `curl -4 ifconfig.co` in your terminal.
|
||||
|
||||
Now, add an A record for your IP address on the domain you want to run Snikket on. In the examples I'm going to use 'chat.example.com' as the domain,
|
||||
and '10.0.0.2' as the IP address. This will be the primary domain for your Snikket service.
|
||||
|
||||
```
|
||||
# Domain TTL Class Type Target
|
||||
chat.example.com. 300 IN A 10.0.0.2
|
||||
```
|
||||
|
||||
How to add records depends on where your DNS is hosted. Here are links to guides for a few common providers:
|
||||
|
||||
- [GoDaddy](https://uk.godaddy.com/help/add-an-a-record-19238)
|
||||
- [Gandi](https://docs.gandi.net/en/domain_names/faq/record_types/a_record.html)
|
||||
- [Namecheap](https://www.namecheap.com/support/knowledgebase/article.aspx/319/2237/how-can-i-set-up-an-a-address-record-for-my-domain)
|
||||
|
||||
**Tip:** If you have an IPv6 address too, this is where you can add it - simply make another record for `chat.example.com.` with the record
|
||||
type `AAAA` and put your IPv6 address as the target.
|
||||
|
||||
Now that you have an A record, you also need a couple more records. To avoid repeating the IP address everywhere, we'll use CNAME records,
|
||||
which are just like aliases of the main domain:
|
||||
|
||||
```
|
||||
# Domain TTL Class Type Target
|
||||
groups.chat.example.com 300 IN CNAME chat.example.com.
|
||||
share.chat.example.com 300 IN CNAME chat.example.com.
|
||||
```
|
||||
|
||||
These subdomains provide group chat functionality and file-sharing respectively.
|
||||
|
||||
### Step 2: Docker
|
||||
|
||||
Docker is a handy tool for running self-contained services known as "containers". We use Docker to provide Snikket
|
||||
in a clean way that works reliably across all different systems.
|
||||
|
||||
If you have the `docker` and `docker-compose` commands already available on your system, great! You can skip to Step 3 below. If not, continue reading.
|
||||
|
||||
#### docker
|
||||
|
||||
Getting docker up and running can vary depending on what OS you're running. Luckily Docker provides an installation guide
|
||||
for a range of operating systems. Follow the guide for your system:
|
||||
|
||||
- [CentOS](https://docs.docker.com/install/linux/docker-ce/centos/)
|
||||
- [Debian](https://docs.docker.com/install/linux/docker-ce/debian/)
|
||||
- [Fedora](https://docs.docker.com/install/linux/docker-ce/fedora/)
|
||||
- [Ubuntu](https://docs.docker.com/install/linux/docker-ce/ubuntu/)
|
||||
|
||||
#### docker-compose
|
||||
|
||||
The Docker folks also provide a handy tool called `docker-compose` which is not installed by default. We're going to use it
|
||||
as an easy way to launch and configure Snikket.
|
||||
|
||||
As per the [installation instructions](https://docs.docker.com/compose/install/) (see the 'Linux' tab there), install
|
||||
`docker-compose` with the following commands:
|
||||
|
||||
```
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.25.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod a+x /usr/local/bin/docker-compose
|
||||
```
|
||||
|
||||
### Step 3: Prepare for Snikket!
|
||||
|
||||
This is exciting, we're so close!
|
||||
|
||||
Create a configuration directory and switch to it:
|
||||
|
||||
```
|
||||
mkdir /etc/snikket
|
||||
cd /etc/snikket
|
||||
```
|
||||
|
||||
And then create a new file there called `docker-compose.yml` using a text editor (such as nano, or vim).
|
||||
|
||||
```
|
||||
nano docker-compose.yml
|
||||
```
|
||||
|
||||
And here is what you should put in the file:
|
||||
|
||||
```
|
||||
version: "3.3"
|
||||
|
||||
services:
|
||||
snikket:
|
||||
container_name: snikket
|
||||
image: snikket/snikket:alpha
|
||||
env_file: snikket.conf
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
volumes:
|
||||
- snikket_data:/snikket
|
||||
|
||||
volumes:
|
||||
snikket_data:
|
||||
```
|
||||
|
||||
Now create another file in the same directory, `snikket.conf` with the following contents:
|
||||
|
||||
```
|
||||
# The primary domain of your Snikket instance
|
||||
SNIKKET_DOMAIN=chat.example.com
|
||||
|
||||
# An email address where the admin can be contacted
|
||||
# (also used to register your Let's Encrypt account to obtain certificates)
|
||||
SNIKKET_ADMIN_EMAIL=you@example.com
|
||||
```
|
||||
|
||||
Change the values to match your setup.
|
||||
|
||||
### Step 4: Launch
|
||||
|
||||
Here we go! Run:
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
The first time you run this command docker will download Snikket. In a moment it should complete,
|
||||
and Snikket should be running.
|
||||
|
||||
Now to set up your first account. To create yourself an admin account, run the following command:
|
||||
|
||||
```
|
||||
docker exec snikket create-invite --admin
|
||||
```
|
||||
|
||||
Follow the link to open the invitation, and follow the instructions get signed in.
|
||||
|
||||
You can create as many links as you want and share them with people. Each link can
|
||||
only be used once. Don't forget to drop the `--admin` part to create normal user accounts!
|
||||
|
||||
That's it! How did it go? Let us know at feedback@snikket.org
|
|
@ -0,0 +1,147 @@
|
|||
---
|
||||
title: "Troubleshooting"
|
||||
---
|
||||
|
||||
# Self-hosted Snikket troubleshooting
|
||||
|
||||
Problems with your Snikket setup? Don't worry! Most people don't experience
|
||||
any issues, but if you do, it's likely something simple. This page describes
|
||||
problems you might encounter, and how to solve them.
|
||||
|
||||
## General problems
|
||||
|
||||
### "Snikket is starting" page does not go away
|
||||
|
||||
If this page stays for more than a few minutes, there was probably an
|
||||
issue obtaining certificates for your Snikket service. For more
|
||||
information on diagnosing certificate issues, see the
|
||||
['Certificates' section](#certificates) later on this page.
|
||||
|
||||
### Unable to share large files
|
||||
|
||||
If you find that your users cannot share large files through Snikket,
|
||||
there could be a couple of reasons:
|
||||
|
||||
- If you are using Snikket behind a reverse proxy, ensure that the proxy
|
||||
does not place a limit on the size of uploads. Check our [reverse proxy
|
||||
guide](../../advanced/reverse_proxy/) for more information.
|
||||
- If the file is over 100MB, Snikket will attempt a direct device-to-device
|
||||
transfer. This requires you and your recipient to be online at the
|
||||
same time, and it only works between two users (not in groups). Also
|
||||
note that direct transfers are not currently supported to or from iOS
|
||||
devices.
|
||||
- To share files over 100MB with a Snikket group or iOS users, we
|
||||
recommend a dedicated file transfer service. You can find a list of
|
||||
standalone [self-hosted file transfer services](https://github.com/awesome-selfhosted/awesome-selfhosted#file-transfer---single-click--drag-n-drop-upload), use a system
|
||||
such as NextCloud, or select one of the many free online file transfer
|
||||
services.
|
||||
|
||||
### Invitations are always expired
|
||||
|
||||
If all invitation links show as expired immediately after you create them:
|
||||
|
||||
- Check you copied the entire URL correctly.
|
||||
- Ensure that you don't have an XMPP server or other service running on
|
||||
the same system as Snikket using port 5280.
|
||||
- If you use a reverse proxy, check that it is correctly forwarding
|
||||
requests to Snikket. See our [reverse proxy guide](../../advanced/reverse_proxy/)
|
||||
for more info.
|
||||
|
||||
### Not responsible for this domain
|
||||
|
||||
If you see an error in the app reporting that the server is "not
|
||||
responsible for this domain":
|
||||
|
||||
- Check that you do not have another XMPP server running on the same
|
||||
system as Snikket. It may be using the ports that Snikket needs.
|
||||
- Check that your DNS setup is correct, and you do not have SRV records
|
||||
left over from a previous XMPP installation on the same domain. If you
|
||||
recently modified your DNS records, you may need to wait a while for
|
||||
DNS caches to expire the old records.
|
||||
|
||||
## Certificate problems
|
||||
|
||||
Certificates are an important part of securing connections to your
|
||||
Snikket.
|
||||
|
||||
Snikket automatically obtains certificates from Let's Encrypt, and keeps
|
||||
them up to date. This usually works without problems, but it can be
|
||||
sensitive to a number of things that might cause it to fail.
|
||||
|
||||
### Common causes
|
||||
|
||||
Common causes of an inability to obtain or renew certificates:
|
||||
|
||||
#### Missing or incorrect DNS records
|
||||
|
||||
Snikket needs 3 DNS records to be added. Ensure you followed the steps
|
||||
from the installation guide correctly, particularly the
|
||||
[DNS configuration](https://snikket.org/service/quickstart/#step-1-dns).
|
||||
|
||||
If your server supports IPv6, you may also add that to DNS (using an
|
||||
AAAA record). If you do this, you *must* tell Snikket by adding the
|
||||
following line to your snikket.conf:
|
||||
|
||||
```
|
||||
SNIKKET_TWEAK_IPV6=1
|
||||
```
|
||||
|
||||
#### Port 80 blocked
|
||||
|
||||
Ensure that port 80 is open and accessible. You can review a [list of
|
||||
ports required by Snikket](../../advanced/firewall/). Port 80 is required
|
||||
to be open by Let's Encrypt so they can verify your domain.
|
||||
|
||||
On a VPS or in a cloud environment, your provider may require you to
|
||||
manually open ports, e.g. in their web dashboard. If you are running in
|
||||
a LAN, you may need to forward ports in your router's web interface.
|
||||
|
||||
Finally, check the firewall on the server itself (e.g. ufw, iptables or
|
||||
nftables).
|
||||
|
||||
#### Incorrect reverse proxy configuration
|
||||
|
||||
If you have a reverse proxy set up (e.g. to run Snikket on the same server
|
||||
as other websites or services), it needs to correctly forward requests
|
||||
to Snikket on both http and https.
|
||||
|
||||
See our [Snikket reverse proxy documentation](../../advanced/reverse_proxy/)
|
||||
for more information on correctly configuring reverse proxies.
|
||||
|
||||
### Certificate debugging commands
|
||||
|
||||
#### Checking for errors
|
||||
|
||||
If you think you have everything set up correctly and you're not sure what the
|
||||
problem could be, check the error log:
|
||||
|
||||
```
|
||||
docker-compose exec snikket_certs cat /var/log/letsencrypt/errors.log
|
||||
```
|
||||
|
||||
If you get a "No such file or directory" error when running the above command,
|
||||
inspect the debug log instead:
|
||||
|
||||
```
|
||||
docker-compose exec snikket_certs cat /var/log/letsencrypt/letsencrypt.log | grep detail
|
||||
```
|
||||
|
||||
#### Trying again
|
||||
|
||||
Once you have fixed any problems, you can force a new attempt with the
|
||||
following command:
|
||||
|
||||
```
|
||||
docker-compose exec snikket_certs /etc/cron.daily/certbot
|
||||
```
|
||||
|
||||
If that command says that no certificates are due for renewal, but you need to
|
||||
trigger a renewal anyway, run:
|
||||
|
||||
```
|
||||
docker-compose exec snikket_certs su letsencrypt -- -c "certbot renew --config-dir /snikket/letsencrypt --cert-path /etc/ssl/certbot --force-renew"
|
||||
```
|
||||
|
||||
Note that Let's Encrypt has strict [rate limits](https://letsencrypt.org/docs/rate-limits/) -
|
||||
do not run these commands more often than necessary, or you may find yourself
|
||||
unable to get new certificates for a while.
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
title: "Upgrading your Snikket server"
|
||||
date: 2021-05-19T14:32:02Z
|
||||
---
|
||||
|
||||
Upgrading to a new Snikket release is typically very easy.
|
||||
|
||||
## snikket-selfhosted
|
||||
|
||||
If you installed Snikket using the [snikket-selfhosted][] scripts, simply run:
|
||||
|
||||
cd /opt/snikket
|
||||
git pull
|
||||
./scripts/update.sh
|
||||
|
||||
## Snikket quickstart
|
||||
|
||||
If you're using a version installed from the [original quickstart][] guide on
|
||||
the website, use these commands instead:
|
||||
|
||||
cd /etc/snikket
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
|
||||
[snikket-selfhosted]: https://github.com/snikket-im/snikket-selfhosted
|
||||
[original quickstart]: https://snikket.org/service/quickstart/
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
|
||||
site_name: Snikket Server Documentation
|
||||
docs_dir: docs
|
||||
theme: readthedocs
|
||||
|
||||
nav:
|
||||
- index.md
|
||||
- Setup:
|
||||
- setup/quickstart.md
|
||||
- setup/troubleshooting.md
|
||||
- Advanced:
|
||||
- advanced/dns.md
|
||||
- advanced/firewall.md
|
||||
- advanced/reverse_proxy.md
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- def_list
|
||||
- footnotes
|
||||
- toc:
|
||||
permalink: true
|
|
@ -0,0 +1,5 @@
|
|||
[po4a_langs] de fr
|
||||
[po4a_paths] docs/_po/snikket-server-docs.pot $lang:docs/_po/$lang.po
|
||||
|
||||
[po4a_alias: md] text opt:"-o markdown -o yfm_keys=title"
|
||||
[type: md] docs/index.md $lang:docs/$lang/index.md
|
|
@ -1,9 +0,0 @@
|
|||
local role_store = module:open_store("roles");
|
||||
|
||||
function get_user_roles(user)
|
||||
return role_store:get(user);
|
||||
end
|
||||
|
||||
function get_jid_roles(jid) --luacheck: ignore 212/jid
|
||||
return nil;
|
||||
end
|
|
@ -1,221 +0,0 @@
|
|||
-- XEP-0401: Easy User Onboarding
|
||||
local dataforms = require "util.dataforms";
|
||||
local datetime = require "util.datetime";
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_split = require "util.jid".split;
|
||||
local split_jid = require "util.jid".split;
|
||||
local rostermanager = require "core.rostermanager";
|
||||
local st = require "util.stanza";
|
||||
|
||||
local invite_only = module:get_option_boolean("registration_invite_only", true);
|
||||
local require_encryption = module:get_option_boolean("c2s_require_encryption",
|
||||
module:get_option_boolean("require_encryption", false));
|
||||
|
||||
local new_adhoc = module:require("adhoc").new;
|
||||
|
||||
-- Whether local users can invite other users to create an account on this server
|
||||
local allow_user_invites = module:get_option_boolean("allow_user_invites", true);
|
||||
|
||||
local invites;
|
||||
if prosody.shutdown then -- COMPAT hack to detect prosodyctl
|
||||
invites = module:depends("invites");
|
||||
end
|
||||
|
||||
local invite_result_form = dataforms.new({
|
||||
title = "Your Invite",
|
||||
-- TODO instructions = something helpful
|
||||
{
|
||||
name = "uri";
|
||||
label = "Invite URI";
|
||||
-- TODO desc = something helpful
|
||||
},
|
||||
{
|
||||
name = "url" ;
|
||||
var = "landing-url";
|
||||
label = "Invite landing URL";
|
||||
},
|
||||
{
|
||||
name = "expire";
|
||||
label = "Token valid until";
|
||||
},
|
||||
});
|
||||
|
||||
module:depends("adhoc");
|
||||
module:provides("adhoc", new_adhoc("New Invite", "urn:xmpp:invite#invite",
|
||||
function (_, data)
|
||||
local username = split_jid(data.from);
|
||||
local invite = invites.create_contact(username, allow_user_invites);
|
||||
--TODO: check errors
|
||||
return {
|
||||
status = "completed";
|
||||
form = {
|
||||
layout = invite_result_form;
|
||||
values = {
|
||||
uri = invite.uri;
|
||||
url = invite.landing_page;
|
||||
expire = datetime.datetime(invite.expires);
|
||||
};
|
||||
};
|
||||
};
|
||||
end, "local_user"));
|
||||
|
||||
|
||||
-- TODO
|
||||
-- module:provides("adhoc", new_adhoc("Create account", "urn:xmpp:invite#create-account", function () end, "admin"));
|
||||
|
||||
-- XEP-0379: Pre-Authenticated Roster Subscription
|
||||
module:hook("presence/bare", function (event)
|
||||
local stanza = event.stanza;
|
||||
if stanza.attr.type ~= "subscribe" then return end
|
||||
|
||||
local preauth = stanza:get_child("preauth", "urn:xmpp:pars:0");
|
||||
if not preauth then return end
|
||||
local token = preauth.attr.token;
|
||||
if not token then return end
|
||||
|
||||
local username, host = jid_split(stanza.attr.to);
|
||||
|
||||
local invite, err = invites.get(token, username);
|
||||
|
||||
if not invite then
|
||||
module:log("debug", "Got invalid token, error: %s", err);
|
||||
return;
|
||||
end
|
||||
|
||||
local contact = jid_bare(stanza.attr.from);
|
||||
|
||||
module:log("debug", "Approving inbound subscription to %s from %s", username, contact);
|
||||
if rostermanager.set_contact_pending_in(username, host, contact, stanza) then
|
||||
if rostermanager.subscribed(username, host, contact) then
|
||||
invite:use();
|
||||
rostermanager.roster_push(username, host, contact);
|
||||
|
||||
-- Send back a subscription request (goal is mutual subscription)
|
||||
if not rostermanager.is_user_subscribed(username, host, contact)
|
||||
and not rostermanager.is_contact_pending_out(username, host, contact) then
|
||||
module:log("debug", "Sending automatic subscription request to %s from %s", contact, username);
|
||||
if rostermanager.set_contact_pending_out(username, host, contact) then
|
||||
rostermanager.roster_push(username, host, contact);
|
||||
module:send(st.presence({type = "subscribe", to = contact }));
|
||||
else
|
||||
module:log("warn", "Failed to set contact pending out for %s", username);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end, 1);
|
||||
|
||||
-- TODO sender side, magic automatic mutual subscription
|
||||
|
||||
local invite_stream_feature = st.stanza("register", { xmlns = "urn:xmpp:invite" }):up();
|
||||
module:hook("stream-features", function(event)
|
||||
local session, features = event.origin, event.features;
|
||||
|
||||
-- Advertise to unauthorized clients only.
|
||||
if session.type ~= "c2s_unauthed" or (require_encryption and not session.secure) then
|
||||
return
|
||||
end
|
||||
|
||||
features:add_child(invite_stream_feature);
|
||||
end);
|
||||
|
||||
-- Client is submitting a preauth token to allow registration
|
||||
module:hook("stanza/iq/urn:xmpp:pars:0:preauth", function(event)
|
||||
local preauth = event.stanza.tags[1];
|
||||
local token = preauth.attr.token;
|
||||
local validated_invite = invites.get(token);
|
||||
if not validated_invite then
|
||||
local reply = st.error_reply(event.stanza, "cancel", "forbidden", "The invite token is invalid or expired");
|
||||
event.origin.send(reply);
|
||||
return true;
|
||||
end
|
||||
event.origin.validated_invite = validated_invite;
|
||||
local reply = st.reply(event.stanza);
|
||||
event.origin.send(reply);
|
||||
return true;
|
||||
end);
|
||||
|
||||
-- Registration attempt - ensure a valid preauth token has been supplied
|
||||
module:hook("user-registering", function (event)
|
||||
local validated_invite = event.validated_invite or (event.session and event.session.validated_invite);
|
||||
if invite_only and not validated_invite then
|
||||
event.allowed = false;
|
||||
event.reason = "Registration on this server is through invitation only";
|
||||
return;
|
||||
end
|
||||
end);
|
||||
|
||||
-- Make a *one-way* subscription. User will see when contact is online,
|
||||
-- contact will not see when user is online.
|
||||
function subscribe(host, user_username, contact_username)
|
||||
local user_jid = user_username.."@"..host;
|
||||
local contact_jid = contact_username.."@"..host;
|
||||
-- Update user's roster to say subscription request is pending...
|
||||
rostermanager.set_contact_pending_out(user_username, host, contact_jid);
|
||||
-- Update contact's roster to say subscription request is pending...
|
||||
rostermanager.set_contact_pending_in(contact_username, host, user_jid);
|
||||
-- Update contact's roster to say subscription request approved...
|
||||
rostermanager.subscribed(contact_username, host, user_jid);
|
||||
-- Update user's roster to say subscription request approved...
|
||||
rostermanager.process_inbound_subscription_approval(user_username, host, contact_jid);
|
||||
end
|
||||
|
||||
-- Make a mutual subscription between jid1 and jid2. Each JID will see
|
||||
-- when the other one is online.
|
||||
function subscribe_both(host, user1, user2)
|
||||
subscribe(host, user1, user2);
|
||||
subscribe(host, user2, user1);
|
||||
end
|
||||
|
||||
-- Registration successful, if there was a preauth token, mark it as used
|
||||
module:hook("user-registered", function (event)
|
||||
local validated_invite = event.validated_invite or (event.session and event.session.validated_invite);
|
||||
if not validated_invite then
|
||||
return;
|
||||
end
|
||||
local inviter_username = validated_invite.inviter;
|
||||
local contact_username = event.username;
|
||||
validated_invite:use();
|
||||
|
||||
if inviter_username then
|
||||
module:log("debug", "Creating mutual subscription between %s and %s", inviter_username, contact_username);
|
||||
subscribe_both(module.host, inviter_username, contact_username);
|
||||
end
|
||||
|
||||
if validated_invite.additional_data then
|
||||
module:log("debug", "Importing roles from invite");
|
||||
local roles = validated_invite.additional_data.roles;
|
||||
if roles then
|
||||
module:open_store("roles"):set(contact_username, roles);
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
|
||||
local sm = require "core.storagemanager";
|
||||
function module.command(arg)
|
||||
if #arg < 2 or arg[2] ~= "generate" then
|
||||
print("usage: prosodyctl mod_easy_invite example.net generate");
|
||||
return;
|
||||
end
|
||||
|
||||
local host = arg[1];
|
||||
assert(hosts[host], "Host "..tostring(host).." does not exist");
|
||||
sm.initialize_host(host);
|
||||
|
||||
table.remove(arg, 1);
|
||||
table.remove(arg, 1);
|
||||
|
||||
local roles;
|
||||
if arg[1] == "--admin" then
|
||||
roles = { ["prosody:admin"] = true };
|
||||
elseif arg[1] == "--role" then
|
||||
roles = { [arg[2]] = true };
|
||||
end
|
||||
|
||||
invites = module:context(host):depends("invites");
|
||||
module:context(host):depends("invites_page");
|
||||
local invite = invites.create_account(nil, { roles = roles });
|
||||
print(invite.landing_page or invite.uri);
|
||||
end
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
local serve = require "net.http.files".serve;
|
||||
|
||||
module:set_global();
|
||||
|
||||
local path = module:get_option_string("acme_challenge_path", "/var/www/.well-known/acme-challenge");
|
||||
|
||||
module:provides("http", {
|
||||
default_path = "/.well-known/acme-challenge";
|
||||
route = {
|
||||
["GET /*"] = serve({ path = path });
|
||||
}
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
local mime_map = module:shared("/*/http_files/mime").types or {
|
||||
css = "text/css",
|
||||
js = "application/javascript",
|
||||
};
|
||||
|
||||
module:provides("http", {
|
||||
default_path = "/share";
|
||||
route = {
|
||||
["GET /*"] = require "net.http.files".serve({ path = "/usr/share/javascript", mime_map = mime_map });
|
||||
}
|
||||
});
|
|
@ -1,134 +0,0 @@
|
|||
local id = require "util.id";
|
||||
local url = require "socket.url";
|
||||
local jid_node = require "util.jid".node;
|
||||
|
||||
local invite_ttl = module:get_option_number("invite_expiry", 86400 * 7);
|
||||
|
||||
local token_storage = module:open_store("invite_token", "map");
|
||||
|
||||
local function get_uri(action, jid, token, params) --> string
|
||||
return url.build({
|
||||
scheme = "xmpp",
|
||||
path = jid,
|
||||
query = action..";preauth="..token..(params and (";"..params) or ""),
|
||||
});
|
||||
end
|
||||
|
||||
local function create_invite(invite_action, invite_jid, allow_registration, additional_data)
|
||||
local token = id.medium();
|
||||
|
||||
local created_at = os.time();
|
||||
local expires = created_at + invite_ttl;
|
||||
|
||||
local invite_params = (invite_action == "roster" and allow_registration) and "ibr=y" or nil;
|
||||
|
||||
local invite = {
|
||||
type = invite_action;
|
||||
jid = invite_jid;
|
||||
|
||||
token = token;
|
||||
allow_registration = allow_registration;
|
||||
additional_data = additional_data;
|
||||
|
||||
uri = get_uri(invite_action, invite_jid, token, invite_params);
|
||||
|
||||
created_at = created_at;
|
||||
expires = expires;
|
||||
};
|
||||
|
||||
module:fire_event("invite-created", invite);
|
||||
|
||||
if allow_registration then
|
||||
local ok, err = token_storage:set(nil, token, invite);
|
||||
if not ok then
|
||||
module:log("warn", "Failed to store account invite: %s", err);
|
||||
return nil, "internal-server-error";
|
||||
end
|
||||
end
|
||||
|
||||
if invite_action == "roster" then
|
||||
local username = jid_node(invite_jid);
|
||||
local ok, err = token_storage:set(username, token, expires);
|
||||
if not ok then
|
||||
module:log("warn", "Failed to store subscription invite: %s", err);
|
||||
return nil, "internal-server-error";
|
||||
end
|
||||
end
|
||||
|
||||
return invite;
|
||||
end
|
||||
|
||||
-- Create invitation to register an account (optionally restricted to the specified username)
|
||||
function create_account(account_username, additional_data) --luacheck: ignore 131/create_account
|
||||
local jid = account_username and (account_username.."@"..module.host) or module.host;
|
||||
return create_invite("register", jid, true, additional_data);
|
||||
end
|
||||
|
||||
-- Create invitation to become a contact of a local user
|
||||
function create_contact(username, allow_registration, additional_data) --luacheck: ignore 131/create_contact
|
||||
return create_invite("roster", username.."@"..module.host, allow_registration, additional_data);
|
||||
end
|
||||
|
||||
local valid_invite_methods = {};
|
||||
local valid_invite_mt = { __index = valid_invite_methods };
|
||||
|
||||
function valid_invite_methods:use()
|
||||
if self.username then
|
||||
-- Also remove the contact invite if present, on the
|
||||
-- assumption that they now have a mutual subscription
|
||||
token_storage:set(self.username, self.token, nil);
|
||||
end
|
||||
token_storage:set(nil, self.token, nil);
|
||||
return true;
|
||||
end
|
||||
|
||||
-- Get a validated invite (or nil, err). Must call :use() on the
|
||||
-- returned invite after it is actually successfully used
|
||||
-- For "roster" invites, the username of the local user (who issued
|
||||
-- the invite) must be passed.
|
||||
-- If no username is passed, but the registration is a roster invite
|
||||
-- from a local user, the "inviter" field of the returned invite will
|
||||
-- be set to their username.
|
||||
function get(token, username)
|
||||
if not token then
|
||||
return nil, "no-token";
|
||||
end
|
||||
|
||||
local valid_until, inviter;
|
||||
|
||||
-- Fetch from host store (account invite)
|
||||
local token_info = token_storage:get(nil, token);
|
||||
|
||||
if username then -- token being used for subscription
|
||||
-- Fetch from user store (subscription invite)
|
||||
valid_until = token_storage:get(username, token);
|
||||
else -- token being used for account creation
|
||||
valid_until = token_info and token_info.expires;
|
||||
if token_info and token_info.type == "roster" then
|
||||
username = jid_node(token_info.jid);
|
||||
inviter = username;
|
||||
end
|
||||
end
|
||||
|
||||
if not valid_until then
|
||||
module:log("debug", "Got unknown token: %s", token);
|
||||
return nil, "token-invalid";
|
||||
elseif os.time() > valid_until then
|
||||
module:log("debug", "Got expired token: %s", token);
|
||||
return nil, "token-expired";
|
||||
end
|
||||
|
||||
return setmetatable({
|
||||
token = token;
|
||||
username = username;
|
||||
inviter = inviter;
|
||||
type = token_info and token_info.type or "roster";
|
||||
uri = token_info and token_info.uri or get_uri("roster", username.."@"..module.host, token);
|
||||
additional_data = token_info and token_info.additional_data or nil;
|
||||
}, valid_invite_mt);
|
||||
end
|
||||
|
||||
function use(token) --luacheck: ignore 131/use
|
||||
local invite = get(token);
|
||||
return invite and invite:use();
|
||||
end
|
|
@ -1,112 +0,0 @@
|
|||
local http_formdecode = require "net.http".formdecode;
|
||||
|
||||
local api_key_store;
|
||||
local invites;
|
||||
-- COMPAT: workaround to avoid executing inside prosodyctl
|
||||
if prosody.shutdown then
|
||||
module:depends("http");
|
||||
api_key_store = module:open_store("invite_api_keys", "map");
|
||||
invites = module:depends("invites");
|
||||
end
|
||||
|
||||
local function get_api_user(request, params)
|
||||
local combined_key;
|
||||
|
||||
local auth_header = request.headers.authorization;
|
||||
|
||||
if not auth_header then
|
||||
params = params or http_formdecode(request.url.query);
|
||||
combined_key = params.key;
|
||||
else
|
||||
local auth_type, value = auth_header:match("^(%S+)%s(%S+)$");
|
||||
if auth_type ~= "Bearer" then
|
||||
return;
|
||||
end
|
||||
combined_key = value;
|
||||
end
|
||||
|
||||
if not combined_key then
|
||||
return;
|
||||
end
|
||||
|
||||
local key_id, key_token = combined_key:match("^([^/]+)/(.+)$");
|
||||
|
||||
if not key_id then
|
||||
return;
|
||||
end
|
||||
|
||||
local api_user = api_key_store:get(nil, key_id);
|
||||
|
||||
if not api_user or api_user.token ~= key_token then
|
||||
return;
|
||||
end
|
||||
|
||||
-- TODO: key expiry, rate limiting, etc.
|
||||
return api_user;
|
||||
end
|
||||
|
||||
function handle_request(event)
|
||||
local query_params = http_formdecode(event.request.url.query);
|
||||
|
||||
local api_user = get_api_user(event.request, query_params);
|
||||
|
||||
if not api_user then
|
||||
return 403;
|
||||
end
|
||||
|
||||
local invite = invites.create_account(nil, { source = "api/token/"..api_user.id });
|
||||
if not invite then
|
||||
return 500;
|
||||
end
|
||||
|
||||
event.response.headers.Location = invite.landing_page or invite.uri;
|
||||
|
||||
if query_params.redirect then
|
||||
return 303;
|
||||
end
|
||||
return 201;
|
||||
end
|
||||
|
||||
if invites then
|
||||
module:provides("http", {
|
||||
route = {
|
||||
["GET"] = handle_request;
|
||||
};
|
||||
});
|
||||
end
|
||||
|
||||
function module.command(arg)
|
||||
local host = table.remove(arg, 1);
|
||||
if not prosody.hosts[host] then
|
||||
print("Error: please supply a valid host");
|
||||
return 1;
|
||||
end
|
||||
require "core.storagemanager".initialize_host(host);
|
||||
module.host = host; --luacheck: ignore 122/module
|
||||
api_key_store = module:open_store("invite_api_keys", "map");
|
||||
|
||||
local command = table.remove(arg, 1);
|
||||
if command == "create" then
|
||||
local id = require "util.id".short();
|
||||
local token = require "util.id".long();
|
||||
api_key_store:set(nil, id, {
|
||||
id = id;
|
||||
token = token;
|
||||
name = arg[1];
|
||||
created_at = os.time();
|
||||
});
|
||||
print(id.."/"..token);
|
||||
elseif command == "delete" then
|
||||
local id = table.remove(arg, 1);
|
||||
if not api_key_store:get(nil, id) then
|
||||
print("Error: key not found");
|
||||
return 1;
|
||||
end
|
||||
api_key_store:set(nil, id, nil);
|
||||
elseif command == "list" then
|
||||
local api_key_store_kv = module:open_store("invite_api_keys");
|
||||
for key_id, key_info in pairs(api_key_store_kv:get(nil)) do
|
||||
print(key_id, key_info.name or "<unknown>");
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,66 @@
|
|||
--luacheck: ignore 143/module
|
||||
|
||||
local http_formdecode = require "net.http".formdecode;
|
||||
|
||||
local secret = module:get_option_string("invites_bootstrap_secret");
|
||||
if not secret then return; end
|
||||
|
||||
local invites_bootstrap_store = module:open_store("invites_bootstrap");
|
||||
|
||||
-- This should be a non-negative integer higher than any set for the
|
||||
-- previous bootstrap event (if any)
|
||||
local current_index = module:get_option_number("invites_bootstrap_index");
|
||||
|
||||
local invites = module:depends("invites");
|
||||
module:depends("http");
|
||||
|
||||
local function handle_request(event)
|
||||
local query_params = http_formdecode(event.request.url.query);
|
||||
|
||||
if not query_params.token or query_params.token ~= secret then
|
||||
return 403;
|
||||
end
|
||||
|
||||
local bootstrap_records = invites_bootstrap_store:get() or {};
|
||||
if #bootstrap_records > 0 then
|
||||
local last_bootstrap = bootstrap_records[#bootstrap_records];
|
||||
if current_index == last_bootstrap.index then
|
||||
event.response.headers.Location = last_bootstrap.result;
|
||||
return 303;
|
||||
elseif current_index < last_bootstrap.index then
|
||||
return 410;
|
||||
end
|
||||
end
|
||||
|
||||
-- Create invite
|
||||
local invite, invite_err = invites.create_account(nil, {
|
||||
roles = { ["prosody:admin"] = true };
|
||||
groups = { "default" };
|
||||
source = "api/token/bootstrap-"..current_index;
|
||||
});
|
||||
if not invite then
|
||||
module:log("error", "Failed to create bootstrap invite! %s", invite_err);
|
||||
return 500;
|
||||
end
|
||||
|
||||
-- Record this bootstrap event (to prevent replay)
|
||||
table.insert(bootstrap_records, {
|
||||
index = current_index;
|
||||
timestamp = os.time();
|
||||
result = invite.landing_page or invite.uri;
|
||||
});
|
||||
local record_ok, record_err = invites_bootstrap_store:set(nil, bootstrap_records);
|
||||
if not record_ok then
|
||||
module:log("error", "Failed to store bootstrap record: %s", record_err);
|
||||
return 500;
|
||||
end
|
||||
|
||||
event.response.headers.Location = invite.landing_page or invite.uri;
|
||||
return 303;
|
||||
end
|
||||
|
||||
module:provides("http", {
|
||||
route = {
|
||||
GET = handle_request;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
-- This module adds groupless invites created via the app to
|
||||
-- the default group
|
||||
module:hook("invite-created", function (invite)
|
||||
if invite.type == "roster"
|
||||
and not (invite.additional_data and invite.additional_data.groups) then
|
||||
if not invite.addititional_data then
|
||||
invite.additional_data = {};
|
||||
end
|
||||
invite.additional_data.groups = { "default" };
|
||||
end
|
||||
end);
|
|
@ -1,131 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invite to {site_name} | Snikket</title>
|
||||
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/snikket.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#fbd308">
|
||||
<meta name="theme-color" content="#fbd308">
|
||||
<style>
|
||||
#install-buttons-container {
|
||||
text-align: center;
|
||||
}
|
||||
#install-buttons-container img {
|
||||
height: 3.5em;
|
||||
margin: 0 auto 0.5em auto;
|
||||
}
|
||||
button {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
|
||||
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
|
||||
<div class="card rounded-lg shadow">
|
||||
<h1 class="card-header rounded-lg rounded-lg">
|
||||
Invite to {site_name}<br/>
|
||||
</h1>
|
||||
<div id="powered-by">Powered by <img src="/snikket-logo.svg"></div>
|
||||
<div class="card-body" >
|
||||
{inviter?<p>You have been invited to chat on {site_name} using Snikket,
|
||||
a secure, privacy-friendly chat app.</p>}
|
||||
|
||||
{inviter&<p>You have been invited to chat with {inviter} using Snikket,
|
||||
a secure, privacy-friendly chat app on {site_name}.</p>}
|
||||
|
||||
<h5 class="card-title">Get started</h5>
|
||||
|
||||
<p>Install the Snikket app on your Android device (iOS <a href="https://snikket.org/faq/#is-there-an-ios-app">coming soon!</a>)</p>
|
||||
|
||||
<div id="install-buttons-container" class="container">
|
||||
<a href='https://play.google.com/store/apps/details?id=org.snikket.android&referrer={uri|urlescape}&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'>
|
||||
<img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png'/>
|
||||
</a>
|
||||
<a href="#qr-modal" class="d-none" id="qr-modal-show">
|
||||
<button class="btn btn-info" title="Send this invite to your device"
|
||||
data-toggle="modal" data-target="#qr-modal">Not on mobile?</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p>After installation the app should automatically open and prompt you to
|
||||
create an account. If not, simply click the button below.</p>
|
||||
|
||||
<h6 class="text-center">App already installed?</h6>
|
||||
|
||||
<div class="text-center">
|
||||
<a href="{uri}" id="uri-cta"><button class="btn btn-secondary btn-sm">Open the app</button></a><br/>
|
||||
<small class="text-muted">This button works only if you have the app installed already!</small>
|
||||
</div>
|
||||
<br/>
|
||||
<h5>Alternatives</h5>
|
||||
<p>You can connect to Snikket using any XMPP-compatible software. If the button above does not
|
||||
work with your app, you may need to <a href="register?{token}">register an account manually</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" tabindex="-1" role="dialog" id="qr-modal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Scan invite code</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>You can transfer this invite to your mobile device by scanning a code with your camera. You can use
|
||||
either a QR scanner app or the Snikket app itself.</p>
|
||||
<nav>
|
||||
<div class="nav nav-tabs" id="nav-tab" role="tablist">
|
||||
<a class="nav-item nav-link active" id="qr-tab-scanner" data-toggle="tab" href="#qr-info-url" role="tab" aria-controls="qr-info-url" aria-selected="true">Using a QR code scanner</a>
|
||||
<a class="nav-item nav-link" id="qr-tab-app" data-toggle="tab" href="#qr-info-uri" role="tab" aria-controls="qr-info-uri" aria-selected="false">Using the Snikket app</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="tab-content">
|
||||
<div id="qr-info-url" class="tab-pane show active">
|
||||
<p>Use a <em>QR code</em> scanner on your mobile device to scan the code below:</p>
|
||||
<div id="qr-invite-page" class="w-50 p-1 mx-auto"></div>
|
||||
</div>
|
||||
<div id="qr-info-uri" class="tab-pane">
|
||||
<div>
|
||||
<img src="/img/snikket-scan-button-shdw.png" class="d-block w-25 p-1 float-right">
|
||||
<p>Install the Snikket app on your mobile device, open it, and
|
||||
tap the 'Scan' button at the top.</p>
|
||||
<p>Your camera will turn on. Point it at the square code below until it is
|
||||
within the highlighted square on your screen, and wait until the app
|
||||
recognises it.</p>
|
||||
</div>
|
||||
<div id="qr-uri" class="w-50 p-1 mx-auto clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/share/jquery/jquery.min.js"></script>
|
||||
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
|
||||
<script src="/qrcode.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$('#qr-modal').one('show.bs.modal', function (e) {
|
||||
new QRCode(document.getElementById("qr-uri"), document.getElementById("uri-cta").getAttribute("href"));
|
||||
new QRCode(document.getElementById("qr-invite-page"), document.location.href);
|
||||
});
|
||||
$('#qr-modal-show').addClass("d-md-block");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,37 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invite to {site_name} | Snikket</title>
|
||||
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/snikket.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#fbd308">
|
||||
<meta name="theme-color" content="#fbd308">
|
||||
</head>
|
||||
<body>
|
||||
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
|
||||
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
|
||||
<div class="card rounded-lg shadow">
|
||||
<h1 class="card-header rounded-lg rounded-lg">
|
||||
Invite to {site_name}<br/>
|
||||
</h1>
|
||||
<div id="powered-by">Powered by <img src="/snikket-logo.svg"></div>
|
||||
<div class="card-body" >
|
||||
<h5 class="card-title">Invite expired</h5>
|
||||
|
||||
<p>Sorry, it looks like this invite code has expired!</p>
|
||||
|
||||
<img class="w-100" src="/illus-empty.svg">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/share/jquery/jquery.min.js"></script>
|
||||
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,55 +0,0 @@
|
|||
local st = require "util.stanza";
|
||||
local url_escape = require "util.http".urlencode;
|
||||
|
||||
local render_html_template = require"util.interpolation".new("%b{}", st.xml_escape, {
|
||||
urlescape = url_escape;
|
||||
});
|
||||
local render_url = require "util.interpolation".new("%b{}", url_escape, {
|
||||
urlescape = url_escape;
|
||||
noscheme = function (url)
|
||||
return (url:gsub("^[^:]+:", ""));
|
||||
end;
|
||||
});
|
||||
|
||||
local site_name = module:get_option_string("site_name", module.host);
|
||||
|
||||
if prosody.shutdown then
|
||||
module:depends("http");
|
||||
end
|
||||
local invites = module:depends("invites");
|
||||
|
||||
-- Point at eg https://github.com/ge0rg/easy-xmpp-invitation
|
||||
local base_url = module:get_option_string("invites_page", (module.http_url and module:http_url().."?{token}") or nil);
|
||||
|
||||
local function add_landing_url(invite)
|
||||
if not base_url then return; end
|
||||
invite.landing_page = render_url(base_url, invite);
|
||||
end
|
||||
|
||||
module:hook("invite-created", add_landing_url);
|
||||
|
||||
|
||||
function serve_invite_page(event)
|
||||
local invite_page_template = assert(module:load_resource("html/invite.html")):read("*a");
|
||||
local invalid_invite_page_template = assert(module:load_resource("html/invite_invalid.html")):read("*a");
|
||||
|
||||
local invite = invites.get(event.request.url.query);
|
||||
if not invite then
|
||||
return render_html_template(invalid_invite_page_template, { site_name = site_name });
|
||||
end
|
||||
|
||||
local invite_page = render_html_template(invite_page_template, {
|
||||
site_name = site_name;
|
||||
token = invite.token;
|
||||
uri = invite.uri;
|
||||
type = invite.type;
|
||||
jid = invite.jid;
|
||||
});
|
||||
return invite_page;
|
||||
end
|
||||
|
||||
module:provides("http", {
|
||||
route = {
|
||||
["GET"] = serve_invite_page;
|
||||
};
|
||||
});
|
|
@ -1,87 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{site_name} | Snikket</title>
|
||||
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/snikket.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#fbd308">
|
||||
<meta name="theme-color" content="#fbd308">
|
||||
</head>
|
||||
<body>
|
||||
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
|
||||
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
|
||||
<div class="card rounded-lg shadow">
|
||||
<h1 class="card-header rounded-lg rounded-lg">
|
||||
Secure communication on {site_name}<br/>
|
||||
</h1>
|
||||
<div id="powered-by">Powered by <img src="/snikket-logo.svg"></div>
|
||||
<div class="card-body" >
|
||||
<p>{site_name} is using Snikket - a secure, privacy-friendly chat app.</p>
|
||||
|
||||
<h5 class="card-title">Create an account</h5>
|
||||
|
||||
<p>Creating an account will allow to communicate with other people using
|
||||
the Snikket app or compatible software. If you already have the app installed,
|
||||
we recommend that you continue the account creation process inside the app
|
||||
by clicking on the button below:</p>
|
||||
|
||||
<h6 class="text-center">App already installed?</h6>
|
||||
|
||||
<div class="text-center">
|
||||
<a href="{uri}"><button class="btn btn-secondary btn-sm">Open the app</button></a><br/>
|
||||
<small class="text-muted">This button works only if you have the app installed already!</small>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h5 class="card-title">Create an account online</h5>
|
||||
<p>If you plan to use a legacy XMPP client, you can register an account online and enter your
|
||||
credentials into any XMPP-compatible software.</p>
|
||||
|
||||
{message&<div class="alert {msg_class?alert-info}" role="alert">
|
||||
{message}
|
||||
</div>}
|
||||
|
||||
<form method="post">
|
||||
<div class="form-group form-row">
|
||||
<label for="user" class="col-md-4 col-lg-12 col-form-label">Username:</label>
|
||||
<div class="col-md-8 col-lg-12">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text" name="user" class="form-control" aria-describedby="usernameHelp"
|
||||
required autofocus minlength="1" maxlength="30" length="30"
|
||||
>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">@{domain}</span>
|
||||
</div>
|
||||
</div>
|
||||
<small id="usernameHelp" class="d-block form-text text-muted">Choose a username, this will become the first part of your new chat address.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
<label for="password" class="col-md-4 col-lg-12 col-form-label">Password:</label>
|
||||
<div class="col-md-8 col-lg-12">
|
||||
<input type="password" name="password" class="form-control" aria-describedby="passwordHelp"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
<small id="passwordHelp" class="form-text text-muted">Enter a secure password that you do not use anywhere else.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
<input type="hidden" name="token" value="{token}">
|
||||
<button type="submit" class="btn btn-primary btn-lg">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/share/jquery/jquery.min.js"></script>
|
||||
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,37 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invite to {site_name} | Snikket</title>
|
||||
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/snikket.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#fbd308">
|
||||
<meta name="theme-color" content="#fbd308">
|
||||
</head>
|
||||
<body>
|
||||
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
|
||||
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
|
||||
<div class="card rounded-lg shadow">
|
||||
<h1 class="card-header rounded-lg rounded-lg">
|
||||
Invite to {site_name}<br/>
|
||||
</h1>
|
||||
<div id="powered-by">Powered by <img src="/snikket-logo.svg"></div>
|
||||
<div class="card-body" >
|
||||
<h5 class="card-title">Registration error</h5>
|
||||
|
||||
<p>{message?Sorry, there was a problem registering your account.}</p>
|
||||
|
||||
<img class="w-100" src="/illus-bug.svg">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/share/jquery/jquery.min.js"></script>
|
||||
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,79 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invite to {site_name} | Snikket</title>
|
||||
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/snikket.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#fbd308">
|
||||
<meta name="theme-color" content="#fbd308">
|
||||
|
||||
<script>
|
||||
function toggle_password(e) {
|
||||
var button = e.target;
|
||||
var input = button.parentNode.parentNode.querySelector("input");
|
||||
switch(input.attributes.type.value) {
|
||||
case "password":
|
||||
input.attributes.type.value = "text";
|
||||
button.innerText = "Hide";
|
||||
break;
|
||||
case "text":
|
||||
input.attributes.type.value = "password";
|
||||
button.innerText = "Show";
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
|
||||
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
|
||||
<div class="card rounded-lg shadow">
|
||||
<h1 class="card-header rounded-lg rounded-lg">
|
||||
{site_name}<br/>
|
||||
</h1>
|
||||
<div id="powered-by">Powered by <img src="/snikket-logo.svg"></div>
|
||||
<div class="card-body" >
|
||||
<h5 class="card-title">Congratulations!</h5>
|
||||
|
||||
<p>You have created an account on {site_name}.</p>
|
||||
|
||||
<p>To start chatting, you need to enter your new account
|
||||
credentials into your chosen XMPP software.</p>
|
||||
|
||||
<p>As a final reminder, your account details are shown below:</p>
|
||||
|
||||
<form class="account-details col-12 col-lg-6 mx-auto">
|
||||
<div class="form-group form-row">
|
||||
<label for="user" class="col-md-4 col-lg-12 col-form-label">Chat address (JID):</label>
|
||||
<div class="col-md-8 col-lg-12">
|
||||
<input type="text" class="form-control-plaintext" readonly value="{username}@{domain}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
<label for="password" class="col-md-4 col-lg-12 col-form-label">Password:</label>
|
||||
<div class="col-md-8 col-lg-12">
|
||||
<div class="input-group">
|
||||
<input type="password" readonly class="form-control" value="{password}">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="toggle_password(event)">Show</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p>Your password is stored encrypted on the server and will not be accessible after you close this page.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/share/jquery/jquery.min.js"></script>
|
||||
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,158 +0,0 @@
|
|||
local id = require "util.id";
|
||||
local http_formdecode = require "net.http".formdecode;
|
||||
local usermanager = require "core.usermanager";
|
||||
local nodeprep = require "util.encodings".stringprep.nodeprep;
|
||||
local st = require "util.stanza";
|
||||
local url_escape = require "util.http".urlencode;
|
||||
local render_html_template = require"util.interpolation".new("%b{}", st.xml_escape, {
|
||||
urlescape = url_escape;
|
||||
});
|
||||
|
||||
|
||||
local site_name = module:get_option_string("site_name", module.host);
|
||||
|
||||
module:depends("http");
|
||||
module:depends("easy_invite");
|
||||
local invites = module:depends("invites");
|
||||
local invites_page = module:depends("invites_page");
|
||||
|
||||
function serve_register_page(event)
|
||||
local register_page_template = assert(module:load_resource("html/register.html")):read("*a");
|
||||
|
||||
local invite = invites.get(event.request.url.query);
|
||||
if not invite then
|
||||
return {
|
||||
status_code = 303;
|
||||
headers = {
|
||||
["Location"] = invites.module:http_url().."?"..event.request.url.query;
|
||||
};
|
||||
};
|
||||
end
|
||||
|
||||
local invite_page = render_html_template(register_page_template, {
|
||||
site_name = site_name;
|
||||
token = invite.token;
|
||||
domain = module.host;
|
||||
uri = invite.uri;
|
||||
type = invite.type;
|
||||
jid = invite.jid;
|
||||
});
|
||||
return invite_page;
|
||||
end
|
||||
|
||||
function handle_register_form(event)
|
||||
local request, response = event.request, event.response;
|
||||
local form_data = http_formdecode(request.body);
|
||||
local user, password, token = form_data["user"], form_data["password"], form_data["token"];
|
||||
|
||||
local register_page_template = assert(module:load_resource("html/register.html")):read("*a");
|
||||
local error_template = assert(module:load_resource("html/register_error.html")):read("*a");
|
||||
local success_template = assert(module:load_resource("html/register_success.html")):read("*a");
|
||||
|
||||
local invite = invites.get(token);
|
||||
if not invite then
|
||||
return {
|
||||
status_code = 303;
|
||||
headers = {
|
||||
["Location"] = invites_page.module:http_url().."?"..event.request.url.query;
|
||||
};
|
||||
};
|
||||
end
|
||||
|
||||
response.headers.content_type = "text/html; charset=utf-8";
|
||||
|
||||
if not user or #user == 0 or not password or #password == 0 or not token then
|
||||
return render_html_template(register_page_template, {
|
||||
site_name = site_name;
|
||||
token = invite.token;
|
||||
domain = module.host;
|
||||
uri = invite.uri;
|
||||
type = invite.type;
|
||||
jid = invite.jid;
|
||||
|
||||
msg_class = "alert-warning";
|
||||
message = "Please fill in all fields.";
|
||||
});
|
||||
end
|
||||
|
||||
-- Shamelessly copied from mod_register_web.
|
||||
local prepped_username = nodeprep(user);
|
||||
|
||||
if not prepped_username or #prepped_username == 0 then
|
||||
return render_html_template(register_page_template, {
|
||||
site_name = site_name;
|
||||
token = invite.token;
|
||||
domain = module.host;
|
||||
uri = invite.uri;
|
||||
type = invite.type;
|
||||
jid = invite.jid;
|
||||
|
||||
msg_class = "alert-warning";
|
||||
message = "This username contains invalid characters.";
|
||||
});
|
||||
end
|
||||
|
||||
if usermanager.user_exists(prepped_username, module.host) then
|
||||
return render_html_template(register_page_template, {
|
||||
site_name = site_name;
|
||||
token = invite.token;
|
||||
domain = module.host;
|
||||
uri = invite.uri;
|
||||
type = invite.type;
|
||||
jid = invite.jid;
|
||||
|
||||
msg_class = "alert-warning";
|
||||
message = "This username is already in use.";
|
||||
});
|
||||
end
|
||||
|
||||
local registering = {
|
||||
validated_invite = invite;
|
||||
username = prepped_username;
|
||||
host = module.host;
|
||||
allowed = true;
|
||||
};
|
||||
|
||||
module:fire_event("user-registering", registering);
|
||||
|
||||
if not registering.allowed then
|
||||
return render_html_template(error_template, {
|
||||
site_name = site_name;
|
||||
msg_class = "alert-danger";
|
||||
message = registering.reason or "Registration is not allowed.";
|
||||
});
|
||||
end
|
||||
|
||||
local ok, err = usermanager.create_user(prepped_username, password, module.host);
|
||||
|
||||
if ok then
|
||||
module:fire_event("user-registered", {
|
||||
username = prepped_username;
|
||||
host = module.host;
|
||||
source = "mod_"..module.name;
|
||||
validated_invite = invite;
|
||||
});
|
||||
|
||||
return render_html_template(success_template, {
|
||||
site_name = site_name;
|
||||
username = prepped_username;
|
||||
domain = module.host;
|
||||
password = password;
|
||||
});
|
||||
else
|
||||
local err_id = id.short();
|
||||
module:log("warn", "Registration failed (%s): %s", err_id, tostring(err));
|
||||
return render_html_template(error_template, {
|
||||
site_name = site_name;
|
||||
msg_class = "alert-danger";
|
||||
message = ("An unknown error has occurred (%s)"):format(err_id);
|
||||
});
|
||||
end
|
||||
end
|
||||
|
||||
module:provides("http", {
|
||||
route = {
|
||||
["GET"] = serve_register_page;
|
||||
["POST"] = handle_register_form;
|
||||
};
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
<html>
|
||||
<body>
|
||||
TODO!
|
||||
|
||||
Hello from <strong>{site_name}!</strong>
|
||||
|
||||
You have been invited to join {site_name}. Simply click here to get started: <a href="{invite_page}">view invite</a>
|
||||
|
||||
If you already have a compatible app installed, you can click here instead: <a href="{invite_uri}">open in app</a>
|
||||
</body>
|
||||
</html>
|
|
@ -1,7 +0,0 @@
|
|||
TODO!
|
||||
|
||||
Hello from {site_name}!
|
||||
|
||||
You have been invited to join {site_name}. Simply click here to get started: {invite_page}
|
||||
|
||||
If you already have a compatible app installed, you can click here instead: {invite_uri}
|
|
@ -1,63 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{site_name} | Snikket</title>
|
||||
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/snikket.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#fbd308">
|
||||
<meta name="theme-color" content="#fbd308">
|
||||
</head>
|
||||
<body>
|
||||
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
|
||||
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
|
||||
<div class="card rounded-lg shadow">
|
||||
<h1 class="card-header rounded-lg rounded-lg">
|
||||
Secure communication on {site_name}<br/>
|
||||
</h1>
|
||||
<div id="powered-by"><a href="https://snikket.org/">Powered by <img src="/snikket-logo.svg"></a></div>
|
||||
<div class="card-body" >
|
||||
<p>{site_name} is using Snikket - a secure, privacy-friendly chat app.</p>
|
||||
|
||||
{allow_email&
|
||||
<h5 class="card-title">Request invitation</h5>
|
||||
|
||||
<p>You may join the Snikket network by creating an account
|
||||
on {site_name}. Registration is by invitation only, enter
|
||||
your email address to request an invitation.</p>
|
||||
|
||||
<form action="/invite-request" method="post">
|
||||
<div class="form-group form-row">
|
||||
<label for="user" class="col-md-4 col-lg-12 col-form-label">Email:</label>
|
||||
<div class="col-md-8 col-lg-12">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="email" name="email" class="form-control" aria-describedby="emailHelp"
|
||||
required autofocus minlength="3" length="30"
|
||||
>
|
||||
</div>
|
||||
<small id="emailHelp" class="d-block form-text text-muted">
|
||||
Enter the email address we should deliver your invitation to.
|
||||
<br/>
|
||||
Your email address will be kept private as per our <a href="privacy/">Privacy Policy</a>.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row float-right">
|
||||
<button type="submit" class="btn btn-primary btn-lg">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/share/jquery/jquery.min.js"></script>
|
||||
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,94 +0,0 @@
|
|||
local http_formdecode = require "net.http".formdecode;
|
||||
local st = require "util.stanza";
|
||||
local render_html_template = require"util.interpolation".new("%b{}", st.xml_escape);
|
||||
local render_text_template = require"util.interpolation".new("%b{}", function (s) return s; end);
|
||||
local mime = require "mime";
|
||||
local ltn12 = require "ltn12";
|
||||
|
||||
local site_name = module:get_option_string("site_name", module.host);
|
||||
|
||||
-- Email templates
|
||||
local email_template_preamble = "Problems viewing this email? View it online at {invite_page}";
|
||||
local email_template_text = assert(module:load_resource("email_templates/invite_email.txt")):read("*a");
|
||||
local email_template_html = assert(module:load_resource("email_templates/invite_email.html")):read("*a");
|
||||
|
||||
module:depends("http");
|
||||
module:depends("email");
|
||||
local invites = module:depends("invites");
|
||||
|
||||
local landing_page_template = assert(module:load_resource("html/index.html")):read("*a");
|
||||
|
||||
local landing_page = render_html_template(landing_page_template, {
|
||||
site_name = site_name;
|
||||
});
|
||||
|
||||
local function handle_form(event)
|
||||
local request, response = event.request, event.response;
|
||||
local form_data = http_formdecode(request.body);
|
||||
|
||||
local email = form_data["email"];
|
||||
|
||||
response.headers.content_type = "";
|
||||
|
||||
local invite = invites.create_account();
|
||||
|
||||
local email_template_params = {
|
||||
site_name = site_name;
|
||||
invite_token = invite.token;
|
||||
invite_uri = invite.uri;
|
||||
invite_page = invite.landing_page;
|
||||
};
|
||||
|
||||
local email_headers = {
|
||||
Subject = "Your Snikket invitation";
|
||||
["Content-Type"] = "multipart/mixed";
|
||||
};
|
||||
|
||||
local email_body = {
|
||||
-- Optional text content prefixed to the entire email (visible even in
|
||||
-- non-MIME clients)
|
||||
preamble = render_text_template(email_template_preamble, email_template_params);
|
||||
|
||||
-- Plain text version
|
||||
[1] = {
|
||||
headers = {
|
||||
["Content-Type"] = 'text/plain; charset="utf-8"';
|
||||
};
|
||||
body = mime.eol(0, render_text_template(email_template_text, email_template_params));
|
||||
};
|
||||
|
||||
-- HTML version
|
||||
[2] = {
|
||||
headers = {
|
||||
["content-type"] = 'text/html;charset="utf-8"',
|
||||
["content-transfer-encoding"] = "quoted-printable"
|
||||
},
|
||||
body = ltn12.source.chain(
|
||||
ltn12.source.string(render_html_template(email_template_html, email_template_params)),
|
||||
ltn12.filter.chain(
|
||||
mime.encode("quoted-printable", "text"),
|
||||
mime.wrap()
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
module:send_email({ --luacheck: ignore 143/module
|
||||
to = email;
|
||||
headers = email_headers;
|
||||
body = email_body;
|
||||
});
|
||||
|
||||
return render_html_template(landing_page_template, {
|
||||
site_name = site_name;
|
||||
message = "Ok! Check your inbox :)";
|
||||
});
|
||||
end
|
||||
|
||||
module:provides("http", {
|
||||
route = {
|
||||
["GET /"] = landing_page;
|
||||
["POST /invite-request"] = handle_form;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
-- This module assigns a client_id to sessions if they are using a "Snikket.*"
|
||||
-- resource identifier. We assume that a resource string in this format is
|
||||
-- static for the same client instance across every session.
|
||||
--
|
||||
-- In the future it is anticipated that this "hack" will be replaced by SASL 2
|
||||
-- (XEP-0388) and/or Bind 2 (XEP-0386), however this is not yet implemented in
|
||||
-- Prosody or any clients.
|
||||
|
||||
module:hook("resource-bind", function (event)
|
||||
local id = event.session.resource:match("^Snikket%..+$");
|
||||
if not id then return; end
|
||||
event.session.client_id = id;
|
||||
end, 1000);
|
|
@ -0,0 +1,23 @@
|
|||
-- The Snikket iOS client does not perform a push registration ("enable") on
|
||||
-- every new connection (it connects every time the app is opened, so we want
|
||||
-- to reduce round-trips and latency). This module attempts to locate a push
|
||||
-- registration associated with the connecting client, and load it onto the
|
||||
-- session so that mod_cloud_notify can find it.
|
||||
|
||||
local push_store = module:open_store("cloud_notify");
|
||||
|
||||
module:hook("resource-bind", function (event)
|
||||
local session = event.session;
|
||||
local client_id = session.client_id;
|
||||
if not client_id then return; end
|
||||
local push_registrations = push_store:get(session.username);
|
||||
if not push_registrations then return; end
|
||||
for push_identifier, push_registration in pairs(push_registrations) do
|
||||
if push_registration.client_id == client_id then
|
||||
session.push_identifier = push_identifier;
|
||||
session.push_settings = push_registration;
|
||||
module:log("debug", "Restored push registration for %s (%s)", client_id, push_identifier);
|
||||
break;
|
||||
end
|
||||
end
|
||||
end, 10);
|
|
@ -0,0 +1,55 @@
|
|||
local jid_bare = require "util.jid".bare;
|
||||
local um_get_roles = require "core.usermanager".get_roles;
|
||||
|
||||
local function load_main_host(module)
|
||||
-- Check whether a user should be isolated from remote JIDs
|
||||
-- If not, set a session flag that allows them to bypass mod_isolate_host
|
||||
local function check_user_isolated(event)
|
||||
local session = event.session;
|
||||
if not session.no_host_isolation then
|
||||
local bare_jid = jid_bare(session.full_jid);
|
||||
local roles = um_get_roles(bare_jid, module.host);
|
||||
if roles == false then return; end
|
||||
if not roles or not roles["prosody:restricted"] then
|
||||
-- Bypass isolation for all unrestricted users
|
||||
session.no_host_isolation = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Add low-priority hook to run after the check_user_isolated default
|
||||
-- behaviour in mod_isolate_host
|
||||
module:hook("resource-bind", check_user_isolated, -0.5);
|
||||
end
|
||||
|
||||
local function load_groups_host(module)
|
||||
local primary_host = module.host:gsub("^%a+%.", "");
|
||||
|
||||
local function is_restricted(user_jid)
|
||||
local roles = um_get_roles(user_jid, primary_host);
|
||||
return not roles or roles["prosody:restricted"];
|
||||
end
|
||||
|
||||
module:hook("muc-config-submitted/muc#roomconfig_publicroom", function (event)
|
||||
if not is_restricted(event.actor) then return; end
|
||||
-- Don't allow modification of this value by restricted users
|
||||
return true;
|
||||
end, 5);
|
||||
|
||||
module:hook("muc-config-form", function (event)
|
||||
if not is_restricted(event.actor) then return; end -- Don't restrict admins
|
||||
-- Hide the option from the config form for restricted users
|
||||
local form = event.form;
|
||||
for i = #form, 1, -1 do
|
||||
if form[i].name == "muc#roomconfig_publicroom" then
|
||||
table.remove(form, i);
|
||||
end
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
if module:get_host_type() == "component" and module:get_option_string("component_module") == "muc" then
|
||||
load_groups_host(module);
|
||||
else
|
||||
load_main_host(module);
|
||||
end
|
|
@ -12,53 +12,42 @@ local check_interval = module:get_option_number("update_check_interval", 86400);
|
|||
local version_info = {};
|
||||
|
||||
do
|
||||
local version = prosody.version;
|
||||
local branch, bugfix = version:match("(%S+)%.(%d+)$");
|
||||
if branch then
|
||||
version_info.branch, version_info.level = branch, bugfix;
|
||||
local version_string = prosody.version;
|
||||
-- "dev 128-00000", "release v2021.05r2"
|
||||
local series, version = version_string:match("(%w+) (%S+)$");
|
||||
if series then
|
||||
version_info.branch, version_info.level = series, version;
|
||||
end
|
||||
end
|
||||
|
||||
function check_for_updates()
|
||||
if not update_dns then return; end
|
||||
local record_name = render_hostname(update_dns, version_info);
|
||||
module:log("debug", "Checking for updates on %s...", record_name);
|
||||
r:lookup(function (records)
|
||||
if not records or #records == 0 then
|
||||
module:log("warn", "Update check failed for %s", record_name);
|
||||
return;
|
||||
end
|
||||
local result = {};
|
||||
for _, record in ipairs(records) do
|
||||
local key, val = record.txt:match("(%S+)=(%S+)");
|
||||
if key then
|
||||
result[key] = val;
|
||||
if record.txt then
|
||||
local key, val = record.txt:match("(%S+)=(%S+)");
|
||||
if key then
|
||||
result[key] = val;
|
||||
end
|
||||
end
|
||||
end
|
||||
module:fire_event("update-check/result", { result = result });
|
||||
end, render_hostname(update_dns, version_info), "TXT", "IN");
|
||||
module:log("debug", "Finished checking for updates");
|
||||
module:fire_event("update-check/result", { current = version_info, latest = result });
|
||||
end, record_name, "TXT", "IN");
|
||||
return check_interval;
|
||||
end
|
||||
|
||||
function module.load()
|
||||
module:add_timer(300, check_for_updates);
|
||||
if update_dns then
|
||||
module:add_timer(5, check_for_updates);
|
||||
else
|
||||
module:log("warn", "Update notifications are disabled");
|
||||
end
|
||||
end
|
||||
|
||||
module:hook("update-check/result", function (event)
|
||||
local ver_secure = tonumber(event.result.secure);
|
||||
local ver_latest = tonumber(event.result.latest);
|
||||
local ver_installed = tonumber(version_info.level);
|
||||
|
||||
if not ver_installed then
|
||||
module:log_status("warn", "Unable to determine local version number");
|
||||
return;
|
||||
end
|
||||
|
||||
if ver_secure and ver_installed < ver_secure then
|
||||
module:log_status("warn", "Security update available!");
|
||||
return;
|
||||
end
|
||||
|
||||
if ver_latest and ver_installed < ver_latest then
|
||||
module:log_status("info", "Update available!");
|
||||
return;
|
||||
end
|
||||
|
||||
if event.result.support_status == "unsupported" then
|
||||
module:log_status("warn", "%s is no longer supported", version_info.branch);
|
||||
return;
|
||||
end
|
||||
end);
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
local urlencode = require "util.http".urlencode;
|
||||
local interpolation = require "util.interpolation";
|
||||
|
||||
local render_url = interpolation.new("%b{}", urlencode);
|
||||
local render_text = interpolation.new("%b{}", function (s) return s; end);
|
||||
|
||||
local security_notification = [[There is an important security release available
|
||||
for {software}. The latest secure version is {current.branch}.{latest.secure}.
|
||||
You are currently running {software} {current.branch}.{current.level}.
|
||||
|
||||
|
||||
For more information please see: {url}
|
||||
]];
|
||||
|
||||
local version_notification = [[There is a new {software} release available. You are
|
||||
currently running {software} {current.branch}.{current.level}, and an upgrade to
|
||||
{current.branch}.{latest.latest} is now available.
|
||||
|
||||
For more information please see: {url}
|
||||
]];
|
||||
|
||||
local message_notification = [[There is a new announcement related to {software}, for more
|
||||
information please see: {url}
|
||||
]];
|
||||
|
||||
local support_notification = [[This version of {software} is no longer supported. For
|
||||
more information please see: {url}
|
||||
]]
|
||||
|
||||
local software_name = module:get_option_string("software_name");
|
||||
local version_url = module:get_option_string("update_notify_version_url");
|
||||
local support_url = module:get_option_string("update_notify_support_url");
|
||||
local message_url = module:get_option_string("update_notify_message_url");
|
||||
|
||||
if not (software_name and version_url and support_url and message_url) then
|
||||
return error("Requires software name, version, support and message URLs to be set");
|
||||
end
|
||||
|
||||
local admin_notify = module:depends("admin_notify").notify;
|
||||
|
||||
local notified_store = module:open_store("update_notifications", "map");
|
||||
|
||||
local function have_notified(branch, field, value)
|
||||
local notified_value = notified_store:get(branch, field);
|
||||
if notified_value then
|
||||
if type(value) == "number" and notified_value >= value then
|
||||
return true;
|
||||
elseif notified_value == value then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
notified_store:set(branch, field, value);
|
||||
return false;
|
||||
end
|
||||
|
||||
module:hook("update-check/result", function (event)
|
||||
local branch = event.current.branch;
|
||||
local ver_secure = tonumber(event.latest.secure);
|
||||
local ver_latest = tonumber(event.latest.latest);
|
||||
local ver_installed = tonumber(event.current.level);
|
||||
local msg_latest = tonumber(event.latest.msg);
|
||||
|
||||
if not ver_installed then
|
||||
module:log_status("error", "Unable to determine local version number");
|
||||
return;
|
||||
end
|
||||
|
||||
if ver_secure and ver_installed < ver_secure
|
||||
and not have_notified(branch, "secure", ver_secure) then
|
||||
module:log_status("warn", "Security update available!");
|
||||
admin_notify(render_text(security_notification, {
|
||||
software = software_name;
|
||||
current = event.current;
|
||||
latest = event.latest;
|
||||
url = render_url(version_url, { branch = branch, version = event.latest.secure });
|
||||
}));
|
||||
return;
|
||||
end
|
||||
|
||||
if ver_latest and ver_installed < ver_latest
|
||||
and not have_notified(branch, "latest", ver_latest) then
|
||||
module:log_status("info", "Update available!");
|
||||
admin_notify(render_text(version_notification, {
|
||||
software = software_name;
|
||||
current = event.current;
|
||||
latest = event.latest;
|
||||
url = render_url(version_url, { branch = branch, version = event.latest.latest });
|
||||
}));
|
||||
return;
|
||||
end
|
||||
|
||||
if msg_latest and not have_notified(branch, "msg", msg_latest) then
|
||||
module:log_status("info", "New announcement");
|
||||
admin_notify(render_text(message_notification, {
|
||||
software = software_name;
|
||||
current = event.current;
|
||||
latest = event.latest;
|
||||
url = render_url(message_url, { branch = branch, message = msg_latest });
|
||||
}));
|
||||
return;
|
||||
end
|
||||
|
||||
if not have_notified(branch, "support_status", event.latest.support_status) then
|
||||
if event.latest.support_status == "unsupported" then
|
||||
module:log_status("warn", "%s is no longer supported", branch);
|
||||
admin_notify(render_text(support_notification, {
|
||||
software = software_name;
|
||||
current = event.current;
|
||||
latest = event.latest;
|
||||
url = render_url(support_url, { branch = branch });
|
||||
}));
|
||||
return;
|
||||
end
|
||||
end
|
||||
end);
|
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 209 KiB |
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#ffc40d</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.6 KiB |
BIN
www/favicon.ico
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 14 KiB |
|
@ -1,33 +0,0 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="260.000000pt" height="260.000000pt" viewBox="0 0 260.000000 260.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,260.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M1169 2496 c-2 -2 -24 -6 -49 -9 -51 -6 -196 -42 -237 -59 -360 -146
|
||||
-607 -401 -723 -748 -49 -143 -62 -230 -62 -386 0 -177 27 -297 109 -498 115
|
||||
-282 109 -516 -18 -660 l-30 -35 38 6 c151 22 316 93 400 172 12 12 25 21 28
|
||||
21 3 0 41 -20 83 -45 181 -104 366 -152 592 -153 133 0 240 16 362 54 84 27
|
||||
228 92 228 103 0 4 -14 15 -31 25 -254 150 -478 469 -534 761 -5 22 -10 47
|
||||
-11 55 -21 97 -21 303 1 409 58 293 200 534 420 716 33 28 81 64 108 80 26 17
|
||||
47 33 47 36 0 3 -39 24 -87 47 -86 40 -217 83 -298 97 -46 7 -329 17 -336 11z
|
||||
m-117 -379 c90 -53 140 -137 140 -237 0 -231 -271 -353 -445 -199 -62 54 -89
|
||||
113 -90 197 -1 125 70 223 193 263 49 16 155 4 202 -24z"/>
|
||||
<path d="M847 2029 c-56 -29 -96 -101 -91 -161 12 -128 153 -196 261 -124 37
|
||||
24 73 83 73 118 0 23 -4 25 -45 24 -75 -2 -108 46 -93 139 2 17 -3 20 -33 21
|
||||
-20 1 -52 -7 -72 -17z"/>
|
||||
<path d="M1885 2218 c-136 -100 -265 -251 -340 -398 -40 -80 -92 -223 -100
|
||||
-281 -4 -24 -8 -46 -10 -49 -2 -3 -6 -35 -9 -71 l-6 -65 33 -1 c125 -3 325
|
||||
-57 461 -124 77 -39 178 -101 204 -126 7 -7 17 -13 20 -13 14 0 178 -166 216
|
||||
-219 39 -53 39 -53 52 -30 18 32 63 173 75 234 71 364 -31 723 -281 997 -64
|
||||
70 -219 198 -238 198 -4 0 -38 -23 -77 -52z"/>
|
||||
<path d="M1418 1245 c-7 -8 17 -171 37 -244 9 -35 28 -93 42 -128 l26 -64 41
|
||||
7 c136 22 368 118 446 182 l31 27 -50 33 c-146 94 -330 161 -491 178 -36 3
|
||||
-68 8 -71 10 -3 2 -8 1 -11 -1z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.8 KiB |
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"name": "Snikket",
|
||||
"short_name": "Snikket",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-256x256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#fbfdff",
|
||||
"background_color": "#fbfdff"
|
||||
}
|
Before Width: | Height: | Size: 8.2 KiB |
|
@ -1,41 +0,0 @@
|
|||
#background {
|
||||
z-index: -1;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url(/background.jpg) no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
filter: blur(10px);
|
||||
opacity: 0.5;
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
#form {
|
||||
margin-top: 100px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
#form .card {
|
||||
border-color: #4f9bcd;
|
||||
border-width: 1px;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
#form .card h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
#powered-by {
|
||||
text-align: right;
|
||||
margin-right: 15px;
|
||||
font-size: 90%;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
#powered-by img {
|
||||
height:1.5em;
|
||||
}
|
||||
|
||||
#form .account-details label {
|
||||
font-weight: bold;
|
||||
}
|