Compare commits

...

204 Commits

Author SHA1 Message Date
decentral1se 6dd5a3b0fc
Merge branch 'master' into ccchaos 2021-12-11 17:53:24 +01:00
Matthew Wild d1cdca0c2c docs: Update certificate troubleshooting docs with more info 2021-11-25 15:00:41 +00:00
Matthew Wild e5d493483e prosody: Bump prosody-modules for bugfix in 4abb33a15897 2021-11-19 17:02:09 +00:00
Matthew Wild fbc5a46c43 prosody: Bump prosody-modules for bugfix in fd90925dc239 2021-11-18 09:09:05 +00:00
Matthew Wild 8c506217d5
Merge pull request #84 from snikket-im/docs/add-user-roles-page
docs: Add page about user roles
2021-11-17 15:49:35 +00:00
Matthew Wild 861c570b37
Merge pull request #85 from snikket-im/feature/measure-active-users
Feature: measure active users
2021-11-17 15:49:23 +00:00
Matthew Wild ad694d6436 prosody: Enable mod_measure_active_users
This allows an operator (via Prometheus, or eventually the web portal) to keep
tabs on how many people are using the server, e.g. to assist with capacity
planning. This will become more important once we allow user-to-user account
invitations.
2021-11-17 13:51:05 +00:00
Matthew Wild 569fce239c prosody: Switch to and enable mod_lastlog2
This records a timestamp of various account events - account registration
time, last connection and last disconnection.

In the future I would like to keep a time-limited record of account activity
so we can also present it to the user for security purposes (e.g. detecting
account compromise and access by third-parties). That will need additional
design work to figure out how to do it in a privacy-preserving way.
2021-11-17 13:47:42 +00:00
Matthew Wild 6ab178385f
Merge pull request #83 from snikket-im/auto-enable-group-push
prosody: Enable push notifications for offline group members by default
2021-11-16 19:21:20 +00:00
Matthew Wild cd462a28a7 docs: Add page about user roles 2021-11-16 17:11:36 +00:00
Matthew Wild 17444cc3bd prosody: Enable push notifications for offline group members by default
This avoids the app needing to gain logic to opt-in to push notifications.
Such logic may be fragile, and introduce additional traffic and round-trips
that would hurt performance.

Note that this will trigger pushes even to users who only use Android or
non-mobile devices. That should cause no issues, and the impact would be
minor. Also considering that non-iOS devices usually remain online most of the
time anyway.

Not accounted for in this commit is the MUC notification filtering side of
things. By default the MUC will push all messages, and
mod_cloud_notify(_filters) will allow all of them through to the device unless
the user explicitly configures otherwise within the app.

If the server can detect whether a MUC is public or private, it can make the
default behaviour more sensible (maybe when adding bookmarks or something?).
In any case, public channels are not a primary use-case for Snikket and can
easily be configured manually in the app for now.
2021-11-16 16:08:16 +00:00
Matthew Wild a24eddab8b prosody: Add symlink for mod_muc_offline_delivery 2021-11-15 14:15:56 +00:00
Matthew Wild 77ecd4d9b9
Merge pull request #77 from snikket-im/fix/restricted-users
Fix restricted users
2021-11-12 21:46:00 +00:00
Matthew Wild fb518da3c8
Merge branch 'master' into fix/restricted-users 2021-11-12 21:45:38 +00:00
Matthew Wild b2b1ea3660
Merge pull request #76 from snikket-im/feature/admin-shell-prompt
prosody: Show Snikket domain in admin shell prompt
2021-11-12 16:02:51 +00:00
Matthew Wild a2714fc178 prosody: Bump to build 1540 for role improvements
This change will disconnect user sessions when their role changes, ensuring
appropriate policies are always enforced.
2021-11-12 15:58:34 +00:00
Matthew Wild ce14c8153b prosody: Load mod_snikket_restricted_users on main host 2021-11-12 15:56:52 +00:00
Matthew Wild 08080f03b9 prosody: Show Snikket domain in admin shell prompt 2021-11-12 13:43:50 +00:00
Matthew Wild 2d623e7bf5
Merge pull request #75 from snikket-im/fix/tweak-gc-defaults
prosody: Tune GC to be more aggressive by default
2021-11-11 14:57:53 +00:00
Matthew Wild e19b0a32af prosody: Tune GC to be more aggressive by default
It appears that, in some environments at least, large file uploads can still
cause a significant increase in RAM. This reduces that effect.

It is expected that a future release will switch to Lua 5.4, which has shown
to have far better GC behaviour.
2021-11-11 14:40:09 +00:00
Matthew Wild b57057f809
Merge pull request #74 from snikket-im/fix/no-roles-no-isolation
mod_snikket_restricted_users: Don't isolate users with no roles
2021-11-10 17:51:14 +00:00
Matthew Wild 457096a13d mod_snikket_restricted_users: Don't isolate users with no roles
The code was originally written to fail safe in the event of failure, hence
the 'if roles and ...'. However a user with no roles (which is normal for a
normal user, especially on upgrade) can return nil.

Failure is signified by 'false', so now we explicitly catch this and return
early without bypassing isolation. Users with no roles (nil) or with roles
but not prosody:restricted bypass isolation.
2021-11-10 17:27:09 +00:00
Matthew Wild f85250461c CHANGELOG: Add unreleased changes 2021-11-10 14:41:44 +00:00
Matthew Wild 631c3acc99
Merge pull request #72 from snikket-im/restricted-users
Restrictions for restricted users
2021-11-09 16:21:39 +00:00
Matthew Wild 8d16897cff
Merge pull request #73 from snikket-im/fix-uploads-allow-for-gcm-tag
prosody: Add 16 bytes to upload limit to allow for appended GCM tag
2021-11-09 16:17:50 +00:00
Matthew Wild ca242ce8a4 prosody: Add 16 bytes to upload limit to allow for appended GCM tag (XEP-0454) 2021-11-09 14:25:56 +00:00
Matthew Wild a5084a289e mod_snikket_restricted_users: Add some explanatory comments 2021-11-09 12:28:50 +00:00
Matthew Wild 40daaa883b prosody: Disable user invitations for restricted users
Note that this currently has no effect, because user and contact invitations
are disabled globally for non-admins.
2021-11-09 12:01:59 +00:00
Matthew Wild 263d5cf286 prosody: Bump to trunk build 1535 + prosody-modules 8bd36bba2292 2021-11-09 11:51:16 +00:00
Matthew Wild d47a6ddbc0 mod_snikket_restricted_users: Use event.actor, which is preferred and always present 2021-11-09 11:23:49 +00:00
Matthew Wild 2f997d50b5 prosody: Enable mod_snikket_restricted_users on MUC host to enforce channel creation 2021-11-09 11:23:19 +00:00
Matthew Wild 0294b0e7e0 prosody: Prevent restricted users from creating public channels (#37) 2021-11-08 16:13:07 +00:00
Matthew Wild 5dddfeb876 prosody: Prevent federation for users with prosody:restricted role (#37) 2021-11-08 12:51:14 +00:00
Matthew Wild 00ad72bcf1 prosody: add symlink for mod_cloud_notify_extensions 2021-11-02 12:52:51 +00:00
3wc d1fd9d6ef4 Merge remote-tracking branch 'upstream/master' 2021-11-01 16:40:04 +02:00
Matthew Wild 1fe4571ab4 mod_snikket_ios_preserve_push: Handle case where user has no push registrations stored 2021-11-01 10:16:08 +00:00
3wc d8577e0e57 Awful scary changes to Prosody SSL config 2021-10-31 16:57:01 +02:00
3wc 7605046cb0 don't merge: add certificate path conf for prosody 2021-10-30 23:31:54 +02:00
3wc b1af112f15 don't merge: ho ho now we have variables 2021-10-30 22:58:21 +02:00
Matthew Wild e125e70e3e docs: Fix missing quote in command 2021-10-20 20:49:09 +01:00
Matthew Wild 94a279a277 docs: Fix infoboxes in the quickstart to be mkdocs-compatible 2021-10-20 14:20:20 +01:00
Matthew Wild 609183c305 Makefile: Some fixes for the docs build process 2021-10-20 14:03:36 +01:00
Matthew Wild 6160d259e6 docs: Add troubleshooting guide 2021-10-20 14:03:08 +01:00
Matthew Wild dae151c7c9 prosody: Add new module to load push registration for new sessions 2021-10-18 14:17:20 +01:00
Matthew Wild ff38924c47
Merge pull request #71 from Zash/repo-filename
Specify a filename for repository
2021-10-18 13:52:15 +01:00
Matthew Wild adb1fb92ae prosody: Bump prosody-modules for cloud_notify improvements 2021-10-18 13:51:55 +01:00
Matthew Wild 83c757c786 prosody: Add module to expose stable client id across sessions 2021-10-18 13:50:36 +01:00
Kim Alvefur 30e05e8754 Specify a filename for repository
So that it stays the same even if the URL is changed.
2021-10-18 14:15:32 +02:00
Matthew Wild deddef38f2 prosody: Use new cloud_notify_extensions meta-module 2021-10-16 21:23:05 +01:00
Matthew Wild f6cf8f2645 prosody: Add mod_muc_offline_delivery for group notifications on iOS 2021-10-16 21:23:05 +01:00
Matthew Wild 7f94dd21bc Update prosody + prosody-modules 2021-10-16 21:23:05 +01:00
Matthew Wild 0ce5d3acf5
Merge pull request #70 from snikket-im/http-upload-limits
New HTTP upload limits and quota
2021-10-14 14:52:17 +01:00
Matthew Wild c02b8b7f3f docs/advanced/config: document new SNIKKET_UPLOAD_STORAGE_GB option 2021-10-14 14:50:22 +01:00
Matthew Wild 88b61461cc prosody: more consistent environment variable import 2021-10-14 14:35:42 +01:00
Matthew Wild 03f0bb2bd9 prosody: Allow configuration of global upload storage quota 2021-10-14 14:16:04 +01:00
Matthew Wild 6852c37111 prosody: Bump per-upload limit from 16MB to 100MB 2021-10-14 14:14:48 +01:00
Matthew Wild 301ee238be
Merge pull request #68 from horazont/feature/upload-size-config-name-fix
Fix incorrect config option
2021-10-06 17:22:31 +01:00
Jonas Schäfer dabfaa2132 Fix incorrect config option
Both the docs [1] and the code [2] of mod_http_file_share agree that it
should be `http_file_share_size_limit`, not
`http_file_share_file_size_limit`.

   [1]: https://prosody.im/doc/modules/mod_http_file_share#larger-files
   [2]: https://hg.prosody.im/trunk/file/default/plugins/mod_http_file_share.lua#l35
2021-10-06 17:43:30 +02:00
Matthew Wild 29ae464a47
Merge pull request #65 from horazont/feature/docs-advanced-config
Add documentation for advanced config options
2021-10-03 16:45:15 +01:00
Jonas Schäfer 78946c32fd Add documentation for advanced config options
Fixes #38.
2021-10-03 17:38:29 +02:00
Matthew Wild 78b3d4e7e4 ansible: Update Prosody and prosody-modules
Specifically for cloud_notify and mod_message fixes to support MUC push
notifications.
2021-09-26 11:56:59 +01:00
Matthew Wild 384fdf3454
Merge pull request #62 from distefam/patch-1
Add instructions for configuring firewall via UFW
2021-09-09 20:53:17 +01:00
Michael DiStefano 103876e0fd
Add instructions for configuring firewall via UFW 2021-09-09 14:01:08 -04:00
Matthew Wild bd10b2a861 Update luaunbound source URL while code.zash.se is offline 2021-08-27 07:49:37 +01:00
Matthew Wild 2c3ca07ee0 Add and enable mod_spam_reporting/mod_watch_spam_reports 2021-08-26 19:42:26 +01:00
Matthew Wild d0149e52df
Merge pull request #49 from Zash/mod_http_file_share
Switch to mod_http_file_share
2021-08-06 15:59:28 +01:00
Matthew Wild 3152aa8ba2
Merge pull request #45 from horazont/feature/turnserver-port-range
Make TURN server port range configurable
2021-08-06 12:22:28 +01:00
Jonas Schäfer 5b8d22a2f1 Make TURN server port range configurable
Fixes #32; see there and in the text for details and rationale.
2021-08-02 17:04:32 +02:00
Kim Alvefur 37f2af4acd Switch to mod_http_file_share
More future-proof, allowing for larger uploads.
2021-07-31 21:05:09 +02:00
Matthew Wild ecf3dede57
Merge pull request #54 from Zash/fix_qrencode
Fix create-invite --qr
2021-07-31 19:57:54 +01:00
Kim Alvefur 89b8c7dfc7 create-invite: Fix that --qr was also passed to prosodyctl 2021-07-31 20:54:31 +02:00
Kim Alvefur a163990ef8 ansible/scripts: Install qrencode
Used by create-invite when called with --qr
2021-07-31 20:54:31 +02:00
Matthew Wild cec33debd3
Merge pull request #52 from Zash/fix_apt_cruft2
Remove some cruft
2021-07-31 17:48:00 +01:00
Matthew Wild c32e9c4d26
Merge branch 'master' into fix_apt_cruft2 2021-07-31 14:27:28 +01:00
Matthew Wild 94d0113a2e
Merge pull request #51 from Zash/ansiblepy3
Switch Ansible to Python 3, remove Python 2
2021-07-31 14:23:53 +01:00
Kim Alvefur 2fbde36b5b Dockerfile: Switch Ansible to Python 3
Also, ahem, https://pythonclock.org/
2021-07-30 22:37:14 +02:00
Kim Alvefur 8e3c28dd73 Dockerfile: Explicitly remove python3
This and the previous commit ensures these are completely removed.
2021-07-30 21:06:48 +02:00
Kim Alvefur 212de80da1 Dockerfile: Tell apt to remove left-over config
Inspection of the resulting image shows that python3 is not fully
removed, this takes care of that.
2021-07-30 21:06:48 +02:00
Matthew Wild 4b923555b3
Merge pull request #50 from Zash/fix_apt_cruft
Dockerfile: Remove apt lists at the very end
2021-07-30 19:45:48 +01:00
Kim Alvefur e4e31976f3 Dockerfile: Remove apt lists at the very end
Doing this after the Ansible step should ensure that they are gone even
if Ansible happens to notice that they're missing and re-fetch them.

This also fixes that Ansible is unable to install Lua if Ansible is
switched over to Python 3.
2021-07-30 16:37:54 +02:00
Matthew Wild 274efd9a32
Merge pull request #46 from Greylinux/patch-2
Update reverse_proxy.md
2021-07-19 13:49:25 +01:00
Greylinux eae1069295
Update reverse_proxy.md
change MB to M in client_max_body_size so reverse proxy doesnt fail
2021-07-19 13:32:25 +01:00
Matthew Wild 04e421a7e1
docs: Fix HTTPS port in nginx reverse proxy example 2021-07-19 12:27:07 +01:00
Matthew Wild 8324fe2059
Merge pull request #44 from Greylinux/patch-1
Update firewall.md
2021-07-17 08:09:25 +01:00
Greylinux 4475e94759
Update firewall.md
added port descritption
2021-07-16 23:01:32 +01:00
Matthew Wild 01211ecab6
Merge pull request #42 from horazont/feature/reverse-proxy-guide-fixes
Some fixes for the reverse proxy guide
2021-07-15 19:05:18 +01:00
Jonas Schäfer b25e3c8fa7 reverse_proxy: add missing word 2021-06-23 19:24:07 +02:00
Jonas Schäfer 46bfccda83 reverse_proxy: set client_max_body_size correctly
Without this, it is possible that HTTP uploads will not pass
if the default limit isn’t raised elsewhere.
2021-06-23 19:24:07 +02:00
Jonas Schäfer 97586c08b6 reverse_proxy: split reverse proxy setup in 80/443 servers
This is more compatible with the redirect to HTTPS which is done
by the backend (snikket) HTTP server.
2021-06-23 19:24:07 +02:00
Jonas Schäfer eceebd4aba reverse_proxy: whitespace cleanup 2021-06-23 19:24:07 +02:00
Matthew Wild 913d96d4ee Add FUNDING.yml 2021-06-14 11:57:03 +01:00
Matthew Wild a6bd89e02a Update README to be more friendly for people first discovering the project 2021-06-10 14:02:41 +01:00
Matthew Wild e8dd2408b8 Dockerfile, mod_update_check: Update version string format 2021-05-31 12:23:52 +01:00
Matthew Wild bf2fccf585 Dockerfile: Move to top-level for consistency with our other projects 2021-05-28 14:26:56 +01:00
Matthew Wild cba3e97ff9
Merge pull request #39 from horazont/feature/observability
Improve observability
2021-05-27 17:31:43 +01:00
Jonas Schäfer ff977f55b5 Allow to load the prometheus exposition module based on env vars
Useful for monitoring.
2021-05-27 18:08:26 +02:00
Jonas Schäfer 690f58bb27 Expose prosody metrics to the web portal
This requires mod_measure_process loaded and a recent version
of mod_http_admin_api to expose the metrics to the web portal in
a reduced and specialized form.
2021-05-27 18:08:26 +02:00
Matthew Wild 7ca468a1ac Update CHANGELOG with new version number 2021-05-19 16:10:11 +01:00
Matthew Wild 26e1e6559a docs: Add upgrade guide 2021-05-19 16:02:39 +01:00
Matthew Wild 1bc336d933 Bump prosody-modules to 8e58a1b78336 2021-05-17 21:22:54 +01:00
Matthew Wild a948cc141f Install latest prosody from deb, factor out variables 2021-05-17 21:22:01 +01:00
Matthew Wild 105c04a2c7 Add unreleased items to changelog 2021-05-17 18:27:24 +01:00
Matthew Wild 3cab4faaf9 prosody: Allow specifying additional config include path via environment 2021-03-17 15:22:30 +00:00
Matthew Wild bd5329c84d prosody: Allow specification of external TURN via environment 2021-03-17 15:16:27 +00:00
Matthew Wild 088d9cb840 Add bootstrapped admin accounts to the default group/circle (thanks jonas!) 2021-03-16 14:00:10 +00:00
Matthew Wild 2bee41057b prosody: bump prosody-modules rev to latest (no particular reason) 2021-03-16 07:18:03 +00:00
Matthew Wild 173ca8b0c9 Add lua-unbound for more robust DNS resolution 2021-03-15 19:55:33 +00:00
Matthew Wild 7f26c50ba8 prosody: Add mod_http_host_status_check 2021-03-10 14:37:19 +00:00
Matthew Wild 32a7bc8954 mod_invites_bootstrap: Fix parameters to store:set() 2021-03-10 12:37:41 +00:00
Matthew Wild e9e5762936 mod_invites_bootstrap: Reject old index values 2021-03-10 12:13:11 +00:00
Matthew Wild 90b13aed6e prosody: Fix another typo, sigh 2021-03-10 12:12:15 +00:00
Matthew Wild b2431438b2 mod_invites_bootstrap: API and behaviour improvements 2021-03-10 11:49:18 +00:00
Matthew Wild dcc02d374b prosody: Fix typo in module name 2021-03-10 10:32:06 +00:00
Matthew Wild 13228b1fc3 Some whitespace fixes 2021-03-09 16:33:22 +00:00
Matthew Wild 36ffd5d4a0 mod_invites_bootstrap: Module/API to create initial admin invite at startup 2021-03-09 16:33:12 +00:00
Matthew Wild c4a8a88028 docker: Remove obsolete letsencrypt things (moved to cert-manager) 2021-03-09 15:28:09 +00:00
Matthew Wild 43c244d55d
Merge pull request #34 from resoli/patch-2
Updated reverse_proxy.md Apace section.
2021-03-06 08:20:34 +00:00
resoli 19dc32e3e8
Update reverse_proxy.md 2021-03-06 09:12:31 +01:00
Matthew Wild 7b37c629dd
Merge pull request #33 from resoli/patch-1
Add Apache configuration snippet
2021-03-01 21:21:53 +00:00
resoli 087b02ca5a
Add Apache configuration snippet 2021-03-01 21:28:20 +01:00
Matthew Wild 724335019e prosody, coturn: Add environment variable to disable TURN server 2021-02-24 14:36:06 +00:00
Matthew Wild 3c3a74f1cc prosody: Bump prosody-modules to 6d595857164a 2021-02-23 21:00:18 +00:00
Matthew Wild c6aa7a9732 prosody: Bump prosody-modules to 38bd4d557413 2021-02-23 16:45:36 +00:00
Kim Alvefur 2ad719122d Add docker healthcheck for docker ps output
Mostly to improve the docker ui, ie it will say "unhealthy" if it takes
too long to start e.g. waiting for certs, or if Prosody crashes without
getting restarted.

Probes the http port on the assumption that this means Prosody is up and
running.

Signed-off-by: Matthew Wild <mwild1@gmail.com>
2021-02-23 15:27:12 +00:00
Matthew Wild a9ee76b2f1 Bump prosody-modules to ea820de69265 2021-02-23 15:26:00 +00:00
Matthew Wild e881c983a6 prosody: Bump to latest prosody-modules 2021-02-22 13:18:42 +00:00
Matthew Wild 0224f93843 prosody: Allow custom HTTP bind interface 2021-02-17 13:28:30 +00:00
Matthew Wild 67f4ffa83f Add changelog entry for beta.20210205 2021-02-05 13:50:42 +00:00
Matthew Wild 5f8f5657b5
Merge pull request #22 from horazont/feature/bookmarks
... load the module, too
2021-02-04 15:57:40 +00:00
Jonas Schäfer f197b9bf6b Also explicitly load mod_groups_internal because why not 2021-02-04 16:57:14 +01:00
Jonas Schäfer 04861cc023 ... load the module, too 2021-02-04 16:55:11 +01:00
Matthew Wild e963012ac1
Merge pull request #20 from horazont/feature/bookmarks
Import fixes from prosody-modules
2021-02-04 15:22:06 +00:00
Jonas Schäfer 5e74fba75d Import fixes from prosody-modules
- Fix room destruction of circle rooms when circle is deleted or
  fails to be created (fixes #17)
- Add circle rooms to bookmarks of (newly added) members
  (fixes #16)
2021-02-04 16:18:47 +01:00
Matthew Wild c3144380de
Merge pull request #19 from horazont/feature/invite-url-suffix
Add `/` to invite URL
2021-02-03 18:40:47 +00:00
Jonas Schäfer 215426c2db Add `/` to invite URL
The new portal version will support this (see [1]). The rationale
is also explained over there, but to summarize: If the link ends
in `_` or `-`, some user agents will not linkify it correctly,
worsening the UX.

   [1]: https://github.com/snikket-im/snikket-web-portal/issues/48
2021-02-03 19:02:56 +01:00
Matthew Wild 84f55744f1 CHANGELOG: Add --remove-orphans to docker-compose command 2021-02-02 20:59:01 +00:00
Matthew Wild 3e62edfbe9 Add docs for update notifications 2021-02-02 14:12:00 +00:00
Matthew Wild 7ab1cec072 CHANGELOG: Update for beta.20210202 2021-02-02 14:09:14 +00:00
Matthew Wild 138200598d docker-compose.yml: Update to new image name for snikket-server 2021-02-02 14:07:51 +00:00
Matthew Wild 4cc3880ec7 Add flag to disable update checks 2021-02-02 13:58:02 +00:00
Matthew Wild 5e0b8c9e7d prosody: Set default group name to site_name/domain 2021-02-02 10:48:31 +00:00
Matthew Wild e568d1f039 prosody: Remove mod_roster_allinall, obsoleted by mod_groups_internal 2021-02-01 20:21:15 +00:00
Matthew Wild d20273fbff
Merge pull request #15 from magicbrothers/modules-duplicate
Remove module entry duplicates
2021-02-01 17:08:38 +00:00
Matthew Wild 548937400d
Merge pull request #13 from aluaces/patch-1
Correct some subdomains in nginx reverse proxy documentation
2021-02-01 17:07:14 +00:00
Matthew Wild a624aaebf4 prosody: Specify prosody-modules revision 2021-01-31 20:30:59 +00:00
Matthew Wild c5468d3d31 prosody: update create-invite to use mod_invites 2021-01-31 12:17:25 +00:00
Felix e9d2668f83
Remove module entry duplicates 2021-01-30 20:47:37 +01:00
Matthew Wild 8deff503da prosody: Allow configuration of data retention period (messages/uploads) 2021-01-29 22:25:33 +00:00
Matthew Wild 20b0620e4d prosody: Expire uploaded files after 1 week, same as MAM 2021-01-29 22:21:14 +00:00
Matthew Wild 13d03bc903 prosody: Bump HTTP upload limit to 16MB (now matches WhatsApp, no more complaining) 2021-01-29 22:20:49 +00:00
Matthew Wild d71359104c prosody: Set correct external URL for http_upload component (fixes HTTP uploads) 2021-01-29 22:17:32 +00:00
Matthew Wild 4278cc055f docker: Ensure SNIKKET_ADMIN_EMAIL is also set 2021-01-29 17:36:04 +00:00
Matthew Wild b26a3eca60 prosody: Add record name to update check failure warning 2021-01-29 17:33:46 +00:00
Matthew Wild 1947781554 prosody: Switch to flag instead of config for daemonize 2021-01-29 17:32:47 +00:00
Matthew Wild a301eb4bb7 prosody: Re-add some modules that we're still using (for now) 2021-01-29 17:31:33 +00:00
Matthew Wild d6b2676829 prosody: Remove general@ from default bookmarks, obsoleted by per-circle group chats 2021-01-28 22:38:57 +00:00
Matthew Wild 72e2c2cec5 prosody: Update welcome message 2021-01-28 22:37:50 +00:00
Matthew Wild e6b59a695e docker-compose.yml: Update to latest dev version 2021-01-28 22:24:35 +00:00
Matthew Wild e23f09cec9 README: Remove outdated info 2021-01-28 22:19:24 +00:00
Matthew Wild 4914d83bd8 mail: Remove mailhog 2021-01-28 22:19:01 +00:00
Matthew Wild 03e4aa8c80 prosody: Give new module the correct directory structure 2021-01-28 21:39:48 +00:00
Matthew Wild dfb6cab374 prosody: Add new module mod_invites_default_group to add the default group to in-app invites 2021-01-28 21:28:40 +00:00
Matthew Wild cfe9f747c5 prosody: Hopefully final fix for the invite URL template 2021-01-28 10:33:03 +00:00
Matthew Wild 5f6f060480 prosody: Only allow admins to send invites via adhoc 2021-01-28 09:01:08 +00:00
Matthew Wild 649ab3c3db prosody: Fix invite URL template for new mod_invites 2021-01-28 09:00:41 +00:00
Matthew Wild eae03273ed prosody: Add lua-luaossl for mod_cloud_notify_encrypted 2021-01-27 18:07:27 +00:00
Matthew Wild 0836f369c1 prosody: Enable invites_page_external, we don't use built-in pages now 2021-01-27 17:33:13 +00:00
Matthew Wild 6cb87092bf mod_authz_internal: Remove in favour of upstream (prosody) module 2021-01-27 16:23:12 +00:00
Matthew Wild cd8638e154 prosody: Add mod_groups_internal from prosody-modules 2021-01-27 13:56:07 +00:00
Matthew Wild 1bff00eba7 prosody: Broad update to upstream (prosody-modules) where appropriate 2021-01-27 13:11:36 +00:00
Matthew Wild 2207199a60 prosody: Remove some unused modules 2021-01-22 23:04:42 +00:00
Matthew Wild 1f37db8f1e prosody: Move HTTP uploads to subdomain, with flag for backwards compat 2021-01-22 23:02:29 +00:00
Matthew Wild a5f78bd027 prosody: Hopefully final fixes for new proxying 2021-01-22 23:01:52 +00:00
Matthew Wild 4b8fcdafbd static stuff: switch to new logo filename 2021-01-22 20:37:06 +00:00
Matthew Wild 68e29c5ac4 docker: Remove www dir from image 2021-01-22 17:25:03 +00:00
Matthew Wild e6d8303ece Remove static files and adjust paths as needed for new locations (served by snikket-web-proxy) 2021-01-22 17:07:59 +00:00
Matthew Wild c8f5179564 prosody: Add new push notification extensions for iOS app 2021-01-22 12:47:24 +00:00
Matthew Wild 2d64f7be49 Add modules required for web portal 2021-01-22 12:39:07 +00:00
Alberto Luaces b1acb121db
Correct some subdomains in nginx reverse proxy documentation
From the rest of the documentation, I think the domain names in the nginx snippet should be fixed this way.
2021-01-22 00:03:14 +01:00
Matthew Wild 280fabb652 prosody: always listen on localhost:5280 - web-proxy responsible for external ports now 2020-12-11 16:51:34 +00:00
Matthew Wild ee9922f4ae ansible: ok if cert-monitor exits immediately 2020-11-25 06:48:47 +00:00
Matthew Wild 6c25508ec2 prosody: Update default port numbers for running behind snikket-web-proxy 2020-11-20 16:32:30 +00:00
Matthew Wild edc81c8105 docker-compose.yml: Update for new multi-container architecture 2020-11-19 17:10:03 +00:00
Matthew Wild 3b477dfa77 Remove Github Actions configuration, migrated to Buildbot 2020-11-19 17:09:33 +00:00
Matthew Wild f1c04cf472 prosody: increase update check interval from 1h to 6h 2020-11-12 12:26:11 +00:00
Matthew Wild e430586282 Update update checks so they hopefully work 2020-11-11 16:50:30 +00:00
Matthew Wild 668592c87a mod_http_libjs: Allow configurable path 2020-11-11 16:42:08 +00:00
Matthew Wild 13bddf20d4 docker: unexplained fix required for building on ARM 2020-11-11 16:40:35 +00:00
Matthew Wild b68c6fe9b1 Add docs build targets to Makefile 2020-11-11 16:39:57 +00:00
Matthew Wild cd78ad3241 Switch to Lua 5.2 (fixes build with new trunk packages) 2020-11-11 12:20:37 +00:00
Matthew Wild 4d3a1cd274 Transition to split containers, remove certbot in favour of externally-supplied certs 2020-11-11 12:20:33 +00:00
Matthew Wild c7be994710 Commit initial po4a config for docs translation (workflow still needs work) 2020-11-10 13:57:22 +00:00
Matthew Wild fbb01a5f58 mod_update_check, mod_update_notify: Split into multiple modules and finish implementation (see also mod_admin_notify upstream) 2020-11-10 13:10:11 +00:00
Matthew Wild 3918fd44a4 Update changelog for alpha.20200624 release 2020-06-24 15:49:45 +01:00
Matthew Wild bd7c35595d CHANGELOG: update 2020-06-22 11:44:30 +01:00
Matthew Wild 67336ed114 prosody: Increase file upload limit to 10MB 2020-06-22 11:42:23 +01:00
Matthew Wild 4d550e0998 mod_easy_invite: Invalidate password reset tokens after use
Requires Prosody 84441c19750e
2020-06-22 11:40:45 +01:00
Matthew Wild 395ab8d404 mod_invites, mod_easy_invite: Add support for password reset invites (requires Prosody 99ae457c2459) 2020-06-19 07:06:36 +01:00
Matthew Wild b2d3caede6
Merge pull request #7 from Zash/patch-1
Improve efficiency of random secret generation
2020-06-05 12:08:43 +01:00
Matthew Wild fe9c14c2b3
docs: Add port 5000 (proxy65) to firewall ports 2020-06-04 17:09:31 +01:00
Kim Alvefur ffa5d0e24c
Improve efficiency of random secret generation
Reading untold amounts of data and throwing away all except \~86% seems
wasteful.

This method reads exactly 32 bytes from /dev/urandom, while the previous
method would have stuffed pipes full before finding 32 bytes in the
specified range. All of the entropy of those 32 bytes are also kept in
the base64 form, although this is probably insane overkill.
2020-05-21 17:57:26 +02:00
Matthew Wild ec33e74684 ansible, docker: Improve turnserver secret generation
The original implementation unintentionally included whitespace
and other noise in the secret file.

The new secret is stored in a -v2 file, the old one is cleaned up
if present.
2020-05-21 16:24:29 +01:00
Matthew Wild 4a0ca2b2d7 ansible: Disable TCP relaying for TURN
This is not used in XMPP today and disabling it reduces
the surface area for security issues.
2020-05-20 16:06:06 +01:00
Matthew Wild 73b709ff14 docs: Add initial documentation 2020-05-18 11:35:37 +01:00
78 changed files with 1833 additions and 1673 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,3 @@
github: snikket-im
liberapay: Snikket
custom: https://snikket.org/donate/

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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"]

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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")

View File

@ -0,0 +1,3 @@
#!/bin/sh
prosodyctl --root cert import /snikket/letsencrypt/live

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -5,3 +5,8 @@
src: "../files/bin/"
dest: "/usr/local/bin/"
mode: 0755
- name: "Install qrencode"
apt:
name: qrencode
state: present

View File

@ -1 +0,0 @@
snikket:{{ lookup('password', '/tmp/mailhog-password length=15')|password_hash("bcrypt", lookup('password', '/tmp/mailhog-salt length=21 chars=letters,digits')+".") }}

View File

@ -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:

View File

@ -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

144
docs/advanced/config.md Normal file
View File

@ -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.

49
docs/advanced/dns.md Normal file
View File

@ -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).

83
docs/advanced/firewall.md Normal file
View File

@ -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`.

View File

@ -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>
```

View File

@ -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

View File

@ -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.

18
docs/index.md Normal file
View File

@ -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!

175
docs/setup/quickstart.md Normal file
View File

@ -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

View File

@ -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.

26
docs/setup/upgrading.md Normal file
View File

@ -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/

22
mkdocs.yml Normal file
View File

@ -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

5
po4a.conf Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 });
}
});

View File

@ -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 });
}
});

View File

@ -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

View File

@ -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

View File

@ -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;
};
});

View File

@ -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);

View File

@ -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">&times;</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>

View File

@ -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>

View File

@ -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;
};
});

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;
};
});

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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;
};
});

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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);

View File

@ -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);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 40 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

17
www/qrcode.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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"
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -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;
}