0
0
Fork 0
mirror of https://github.com/NixOS/nixpkgs.git synced 2025-07-14 06:00:33 +03:00

nixos/movim: H2O support, H2O + Ejabberd + runTest (#385040)

This commit is contained in:
Florian Klink 2025-04-03 19:51:51 +01:00 committed by GitHub
commit e9a9de1735
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 584 additions and 202 deletions

View file

@ -175,6 +175,37 @@ let
"mysql" = "mysql.service";
}
.${cfg.database.type};
# exclusivity asserted in `assertions`
webServerService =
if cfg.h2o != null then
"h2o.service"
else if cfg.nginx != null then
"nginx.service"
else
null;
socketOwner =
if cfg.h2o != null then
config.services.h2o.user
else if cfg.nginx != null then
config.services.nginx.user
else
cfg.user;
# Movim needs a lot of unsafe values to function at this time. Perhaps if
# this is ever addressed in the future, the PHP application will send up the
# proper directive. For now this fairly conservative CSP will restrict a lot
# of potentially bad stuff as well as take in inventory of the features used.
#
# See: https://github.com/movim/movim/issues/314
movimCSP = lib.concatStringsSep "; " [
"default-src 'self'"
"img-src 'self' aesgcm: data: https:"
"media-src 'self' aesgcm: https:"
"script-src 'self' 'unsafe-eval' 'unsafe-inline'"
"style-src 'self' 'unsafe-inline'"
];
in
{
options.services = {
@ -209,19 +240,19 @@ in
};
dataDir = mkOption {
type = types.nonEmptyStr;
type = types.path;
default = "/var/lib/movim";
description = "State directory of the `movim` user which holds the applications state & data.";
};
logDir = mkOption {
type = types.nonEmptyStr;
type = types.path;
default = "/var/log/movim";
description = "Log directory of the `movim` user which holds the applications logs.";
};
runtimeDir = mkOption {
type = types.nonEmptyStr;
type = types.path;
default = "/run/movim";
description = "Runtime directory of the `movim` user which holds the applications caches & temporary files.";
};
@ -319,9 +350,7 @@ in
};
precompressStaticFiles = mkOption {
type =
with types;
submodule {
type = types.submodule {
options = {
brotli = {
enable = mkEnableOption "Brotli precompression";
@ -354,67 +383,67 @@ in
type = types.submodule {
options = {
info = mkOption {
type = with types; nullOr str;
type = types.nullOr types.nonEmptyStr;
default = null;
description = "Content of the info box on the login page";
};
description = mkOption {
type = with types; nullOr str;
type = types.nullOr types.nonEmptyStr;
default = null;
description = "General description of the instance";
};
timezone = mkOption {
type = with types; nullOr str;
type = types.nullOr types.nonEmptyStr;
default = null;
description = "The server timezone";
};
restrictsuggestions = mkOption {
type = with types; nullOr bool;
type = types.nullOr types.bool;
default = null;
description = "Only suggest chatrooms, Communities and other contents that are available on the user XMPP server and related services";
};
chatonly = mkOption {
type = with types; nullOr bool;
type = types.nullOr types.bool;
default = null;
description = "Disable all the social feature (Communities, Blog) and keep only the chat ones";
};
disableregistration = mkOption {
type = with types; nullOr bool;
type = types.nullOr types.bool;
default = null;
description = "Remove the XMPP registration flow and buttons from the interface";
};
loglevel = mkOption {
type = with types; nullOr (ints.between 0 3);
type = types.nullOr (types.ints.between 0 3);
default = null;
description = "The server loglevel";
};
locale = mkOption {
type = with types; nullOr str;
type = types.nullOr types.nonEmptyStr;
default = null;
description = "The server main locale";
};
xmppdomain = mkOption {
type = with types; nullOr str;
type = types.nullOr types.nonEmptyStr;
default = null;
description = "The default XMPP server domain";
};
xmppdescription = mkOption {
type = with types; nullOr str;
type = types.nullOr types.nonEmptyStr;
default = null;
description = "The default XMPP server description";
};
xmppwhitelist = mkOption {
type = with types; nullOr str;
type = types.nullOr types.nonEmptyStr;
default = null;
description = "The allowlisted XMPP servers";
};
@ -442,7 +471,7 @@ in
};
secretFile = mkOption {
type = with types; nullOr path;
type = types.nullOr types.path;
default = null;
description = "The secret file to be sourced for the .env settings.";
};
@ -459,13 +488,13 @@ in
};
name = mkOption {
type = types.str;
type = types.nonEmptyStr;
default = "movim";
description = "Database name.";
};
user = mkOption {
type = types.str;
type = types.nonEmptyStr;
default = "movim";
description = "Database username.";
};
@ -477,33 +506,53 @@ in
};
};
nginx = mkOption {
type =
with types;
nullOr (
submodule (
import ../web-servers/nginx/vhost-options.nix {
inherit config lib;
}
)
h2o = mkOption {
type = types.nullOr (
types.submodule (import ../web-servers/h2o/vhost-options.nix { inherit config lib; })
);
default = null;
example =
lib.literalExpression # nginx
lib.literalExpression # nix
''
{
serverAliases = [
"pics.''${config.networking.domain}"
"pics.''${config.movim.domain}"
];
acme.enable = true;
tls.policy = "force";
}
'';
description = ''
With this option, you can customize an H2O virtual host which already
has sensible defaults for Movim. Set to `{ }` if you do not need any
customization to the virtual host. If enabled, then by default, the
{option}`serverName` is `''${domain}`, If this is set to `null` (the
default), no H2O `hosts` will be configured.
'';
};
nginx = mkOption {
type = types.nullOr (
types.submodule (import ../web-servers/nginx/vhost-options.nix { inherit config lib; })
);
default = null;
example =
lib.literalExpression # nix
''
{
serverAliases = [
"pics.''${config.movim.domain}"
];
enableACME = true;
forceHttps = true;
}
'';
description = ''
With this option, you can customize an nginx virtual host which already has sensible defaults for Movim.
Set to `{ }` if you do not need any customization to the virtual host.
If enabled, then by default, the {option}`serverName` is `''${domain}`,
If this is set to null (the default), no nginx virtualHost will be configured.
With this option, you can customize an Nginx virtual host which
already has sensible defaults for Movim. Set to `{ }` if you do not
need any customization to the virtual host. If enabled, then by
default, the {option}`serverName` is `''${domain}`, If this is set to
`null` (the default), no Nginx `virtualHost` will be configured.
'';
};
@ -522,6 +571,25 @@ in
};
config = mkIf cfg.enable {
assertions = [
(
let
webServers = [
"h2o"
"nginx"
];
checkConfigs = lib.concatMapStringsSep ", " (ws: "services.movim.${ws}") webServers;
in
{
assertion = builtins.length (lib.lists.filter (ws: cfg.${ws} != null) webServers) <= 1;
message = ''
At most 1 web server virtual host configuration should be enabled
for Movim at a time. Check ${checkConfigs}.
'';
}
)
];
environment.systemPackages = [ package ];
users = {
@ -532,6 +600,9 @@ in
group = cfg.group;
};
}
// lib.optionalAttrs (cfg.h2o != null) {
"${config.services.h2o.user}".extraGroups = [ cfg.group ];
}
// lib.optionalAttrs (cfg.nginx != null) {
"${config.services.nginx.user}".extraGroups = [ cfg.group ];
};
@ -578,6 +649,51 @@ in
};
};
h2o = mkIf (cfg.h2o != null) {
enable = true;
hosts."${cfg.domain}" = mkMerge [
{
settings = {
paths = {
"/ws/" = {
"proxy.preserve-host" = "ON";
"proxy.tunnel" = "ON";
"proxy.reverse.url" = "http://${cfg.settings.DAEMON_INTERFACE}:${builtins.toString cfg.port}/";
};
"/" =
{
"file.dir" = "${package}/share/php/movim/public";
"file.index" = [
"index.php"
"index.html"
];
redirect = {
url = "/index.php/";
internal = "YES";
status = 307;
};
"header.set" = [
"Content-Security-Policy: ${movimCSP}"
];
}
// lib.optionalAttrs (with cfg.precompressStaticFiles; brotli.enable || gzip.enable) {
"file.send-compressed" = "ON";
};
};
"file.custom-handler" = {
extension = [ ".php" ];
"fastcgi.document_root" = package;
"fastcgi.connect" = {
port = fpm.socket;
type = "unix";
};
};
};
}
cfg.h2o
];
};
nginx = mkIf (cfg.nginx != null) (
{
enable = true;
@ -631,8 +747,7 @@ in
tryFiles = "$uri $uri/ /index.php$is_args$args";
extraConfig = # nginx
''
# https://github.com/movim/movim/issues/314
add_header Content-Security-Policy "default-src 'self'; img-src 'self' aesgcm: https:; media-src 'self' aesgcm: https:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';";
add_header Content-Security-Policy "${movimCSP}";
set $no_cache 1;
'';
};
@ -665,7 +780,7 @@ in
'';
};
};
extraConfig = # ngnix
extraConfig = # nginx
''
index index.php;
'';
@ -706,11 +821,7 @@ in
'';
};
phpfpm.pools.${pool} =
let
socketOwner = if (cfg.nginx != null) then config.services.nginx.user else cfg.user;
in
{
phpfpm.pools.${pool} = {
phpPackage = package.php;
user = cfg.user;
group = cfg.group;
@ -788,9 +899,9 @@ in
};
services.${phpExecutionUnit} = {
wantedBy = lib.optional (cfg.nginx != null) "nginx.service";
wantedBy = lib.optional (webServerService != null) webServerService;
requiredBy = [ "movim.service" ];
before = [ "movim.service" ] ++ lib.optional (cfg.nginx != null) "nginx.service";
before = [ "movim.service" ] ++ lib.optional (webServerService != null) webServerService;
wants = [ "network.target" ];
requires = [ "movim-data-setup.service" ] ++ lib.optional cfg.database.createLocally dbService;
after = [ "movim-data-setup.service" ] ++ lib.optional cfg.database.createLocally dbService;
@ -809,14 +920,14 @@ in
"${phpExecutionUnit}.service"
]
++ lib.optional cfg.database.createLocally dbService
++ lib.optional (cfg.nginx != null) "nginx.service";
++ lib.optional (webServerService != null) webServerService;
after =
[
"movim-data-setup.service"
"${phpExecutionUnit}.service"
]
++ lib.optional cfg.database.createLocally dbService
++ lib.optional (cfg.nginx != null) "nginx.service";
++ lib.optional (webServerService != null) webServerService;
environment = {
PUBLIC_URL = "//${cfg.domain}";
WS_PORT = builtins.toString cfg.port;

View file

@ -802,7 +802,7 @@ in
morty = handleTest ./morty.nix { };
mosquitto = runTest ./mosquitto.nix;
moosefs = handleTest ./moosefs.nix { };
movim = discoverTests (import ./web-apps/movim { inherit handleTestOn; });
movim = import ./web-apps/movim { inherit recurseIntoAttrs runTest; };
mpd = handleTest ./mpd.nix { };
mpv = runTest ./mpv.nix;
mtp = handleTest ./mtp.nix { };

View file

@ -1,14 +1,6 @@
{
system ? builtins.currentSystem,
handleTestOn,
}:
{ recurseIntoAttrs, runTest }:
let
supportedSystems = [
"x86_64-linux"
"i686-linux"
];
in
{
standard = handleTestOn supportedSystems ./standard.nix { inherit system; };
recurseIntoAttrs {
ejabberd-h2o = runTest ./ejabberd-h2o.nix;
prosody-nginx = runTest ./prosody-nginx.nix;
}

View file

@ -0,0 +1,274 @@
{ hostPkgs, lib, ... }:
let
movim = {
domain = "movim.local";
port = 8080;
info = "No ToS in tests";
description = "NixOS testing server";
};
ejabberd = {
domain = "ejabberd.local";
ports = {
c2s = 5222;
s2s = 5269;
http = 5280;
};
spoolDir = "/var/lib/ejabberd";
admin = rec {
JID = "${username}@${ejabberd.domain}";
username = "romeo";
password = "juliet";
};
};
# START OF EJABBERD CONFIG ##################################################
#
# Ejabberd has sparse defaults as it is a generic XMPP server. As such this
# config might be longer than expected for a test.
#
# Movim suggests: https://github.com/movim/movim/wiki/Configure ejabberd
#
# In the future this may be the default setup
# See: https://github.com/NixOS/nixpkgs/pull/312316
ejabberd_config_file =
let
settingsFormat = hostPkgs.formats.yaml { };
in
settingsFormat.generate "ejabberd.yml" {
loglevel = "info";
hide_sensitive_log_data = false;
hosts = [ ejabberd.domain ];
default_db = "mnesia";
acme.auto = false;
s2s_access = "s2s";
s2s_use_starttls = false;
new_sql_schema = true;
acl = {
admin = [
{ user = ejabberd.admin.JID; }
];
local.user_regexp = "";
loopback.ip = [
"127.0.0.1/8"
"::1/128"
];
};
access_rules = {
c2s = {
deny = "blocked";
allow = "all";
};
s2s = {
allow = "all";
};
local.allow = "local";
announce.allow = "admin";
configure.allow = "admin";
pubsub_createnode.allow = "local";
trusted_network.allow = "loopback";
};
api_permissions = {
"console commands" = {
from = [ "ejabberd_ctl" ];
who = "all";
what = "*";
};
};
shaper = {
normal = {
rate = 3000;
burst_size = 20000;
};
fast = 100000;
};
modules = {
mod_caps = { };
mod_disco = { };
mod_mam = { };
mod_http_upload = {
docroot = "${ejabberd.spoolDir}/uploads";
dir_mode = "0755";
file_mode = "0644";
get_url = "http://@HOST@/upload";
put_url = "http://@HOST@/upload";
max_size = 65536;
custom_headers = {
Access-Control-Allow-Origin = "http://@HOST@,http://${movim.domain}";
Access-Control-Allow-Methods = "GET,HEAD,PUT,OPTIONS";
Access-Control-Allow-Headers = "Content-Type";
};
};
# This PubSub block is required for Movim to work.
#
# See: https://github.com/movim/movim/wiki/Configure ejabberd#pubsub
mod_pubsub = {
hosts = [ "pubsub.@HOST@" ];
access_createnode = "pubsub_createnode";
ignore_pep_from_offline = false;
last_item_cache = false;
max_items_node = 2048;
default_node_config = {
max_items = 2048;
};
plugins = [
"flat"
"pep"
];
force_node_config = {
"storage:bookmarks".access_model = "whitelist";
"eu.siacs.conversations.axolotl.*".access_model = "open";
"urn:xmpp:bookmarks:0" = {
access_model = "whitelist";
send_last_published_item = "never";
max_items = "infinity";
persist_items = true;
};
"urn:xmpp:bookmarks:1" = {
access_model = "whitelist";
send_last_published_item = "never";
max_items = "infinity";
persist_items = true;
};
"urn:xmpp:pubsub:movim-public-subscription" = {
access_model = "whitelist";
max_items = "infinity";
persist_items = true;
};
"urn:xmpp:microblog:0" = {
notify_retract = true;
max_items = "infinity";
persist_items = true;
};
"urn:xmpp:microblog:0:comments*" = {
access_model = "open";
notify_retract = true;
max_items = "infinity";
persist_items = true;
};
};
};
mod_stream_mgmt = { };
};
listen = [
{
module = "ejabberd_c2s";
port = ejabberd.ports.c2s;
max_stanza_size = 262144;
access = "c2s";
starttls_required = false;
}
{
module = "ejabberd_s2s_in";
port = ejabberd.ports.s2s;
max_stanza_size = 524288;
shaper = "fast";
}
{
module = "ejabberd_http";
port = ejabberd.ports.http;
request_handlers = {
"/upload" = "mod_http_upload";
};
}
];
};
# END OF EJABBERD CONFIG ##################################################
in
{
name = "movim-ejabberd-h2o";
meta = {
maintainers = with lib.maintainers; [ toastal ];
};
nodes = {
server =
{ pkgs, ... }:
{
environment.systemPackages = [
# For testing
pkgs.websocat
];
services.movim = {
inherit (movim) domain port;
enable = true;
verbose = true;
podConfig = {
inherit (movim) description info;
xmppdomain = ejabberd.domain;
};
database = {
type = "postgresql";
createLocally = true;
};
h2o = { };
};
services.ejabberd = {
inherit (ejabberd) spoolDir;
enable = true;
configFile = ejabberd_config_file;
imagemagick = false;
};
services.h2o.settings = {
compress = "ON";
};
systemd.services.ejabberd = {
serviceConfig = {
# Certain misconfigurations can cause RAM usage to swell before
# crashing; fail sooner with more-than-liberal memory limits
StartupMemoryMax = "1G";
MemoryMax = "512M";
};
};
networking = {
firewall.allowedTCPPorts = with ejabberd.ports; [
c2s
s2s
];
extraHosts = ''
127.0.0.1 ${movim.domain}
127.0.0.1 ${ejabberd.domain}
'';
};
};
};
testScript = # python
''
ejabberdctl = "su ejabberd -s $(which ejabberdctl) "
server.wait_for_unit("phpfpm-movim.service")
server.wait_for_unit("h2o.service")
server.wait_for_open_port(${builtins.toString movim.port})
server.wait_for_open_port(80)
server.wait_for_unit("ejabberd.service")
ejabberd_status = server.succeed(ejabberdctl + "status")
assert "status: started" in ejabberd_status
server.succeed(ejabberdctl + "register ${ejabberd.admin.username} ${ejabberd.domain} ${ejabberd.admin.password}")
server.wait_for_unit("movim.service")
# Test unauthenticated
server.fail("curl -L --fail-with-body --max-redirs 0 http://${movim.domain}/chat")
# Test basic Websocket
server.succeed("echo | websocat --origin 'http://${movim.domain}' 'ws://${movim.domain}/ws/?path=login&offset=0'")
# Test login + create cookiejar
login_html = server.succeed("curl --fail-with-body -c /tmp/cookies http://${movim.domain}/login")
assert "${movim.description}" in login_html
assert "${movim.info}" in login_html
# Test authentication POST
server.succeed("curl --fail-with-body -b /tmp/cookies -X POST --data-urlencode 'username=${ejabberd.admin.JID}' --data-urlencode 'password=${ejabberd.admin.password}' http://${movim.domain}/login")
server.succeed("curl -L --fail-with-body --max-redirs 1 -b /tmp/cookies http://${movim.domain}/chat")
'';
}

View file

@ -0,0 +1,115 @@
{ lib, ... }:
let
movim = {
domain = "movim.local";
port = 8080;
info = "No ToS in tests";
description = "NixOS testing server";
};
prosody = {
domain = "prosody.local";
admin = rec {
JID = "${username}@${prosody.domain}";
username = "romeo";
password = "juliet";
};
};
in
{
name = "movim-prosody-nginx";
meta = {
maintainers = with lib.maintainers; [ toastal ];
};
nodes = {
server =
{ pkgs, ... }:
{
environment.systemPackages = [
# For testing
pkgs.websocat
];
services.movim = {
inherit (movim) domain port;
enable = true;
verbose = true;
podConfig = {
inherit (movim) description info;
xmppdomain = prosody.domain;
};
nginx = { };
};
services.prosody = {
enable = true;
xmppComplianceSuite = false;
disco_items = [
{
url = "upload.${prosody.domain}";
description = "File Uploads";
}
];
virtualHosts."${prosody.domain}" = {
inherit (prosody) domain;
enabled = true;
extraConfig = ''
Component "pubsub.${prosody.domain}" "pubsub"
pubsub_max_items = 10000
expose_publisher = true
Component "upload.${prosody.domain}" "http_file_share"
http_external_url = "http://upload.${prosody.domain}"
http_file_share_expires_after = 300 * 24 * 60 * 60
http_file_share_size_limit = 1024 * 1024 * 1024
http_file_share_daily_quota = 4 * 1024 * 1024 * 1024
'';
};
extraConfig = ''
pep_max_items = 10000
http_paths = {
file_share = "/";
}
'';
};
networking.extraHosts = ''
127.0.0.1 ${movim.domain}
127.0.0.1 ${prosody.domain}
'';
};
};
testScript = # python
''
server.wait_for_unit("phpfpm-movim.service")
server.wait_for_unit("nginx.service")
server.wait_for_open_port(${builtins.toString movim.port})
server.wait_for_open_port(80)
server.wait_for_unit("prosody.service")
server.succeed('prosodyctl status | grep "Prosody is running"')
server.succeed("prosodyctl register ${prosody.admin.username} ${prosody.domain} ${prosody.admin.password}")
server.wait_for_unit("movim.service")
# Test unauthenticated
server.fail("curl -L --fail-with-body --max-redirs 0 http://${movim.domain}/chat")
# Test basic Websocket
server.succeed("echo | websocat --origin 'http://${movim.domain}' 'ws://${movim.domain}/ws/?path=login&offset=0'")
# Test login + create cookiejar
login_html = server.succeed("curl --fail-with-body -c /tmp/cookies http://${movim.domain}/login")
assert "${movim.description}" in login_html
assert "${movim.info}" in login_html
# Test authentication POST
server.succeed("curl --fail-with-body -b /tmp/cookies -X POST --data-urlencode 'username=${prosody.admin.JID}' --data-urlencode 'password=${prosody.admin.password}' http://${movim.domain}/login")
server.succeed("curl -L --fail-with-body --max-redirs 1 -b /tmp/cookies http://${movim.domain}/chat")
'';
}

View file

@ -1,110 +0,0 @@
import ../../make-test-python.nix (
{ lib, pkgs, ... }:
let
movim = {
domain = "movim.local";
info = "No ToS in tests";
description = "NixOS testing server";
};
xmpp = {
domain = "xmpp.local";
admin = rec {
JID = "${username}@${xmpp.domain}";
username = "romeo";
password = "juliet";
};
};
in
{
name = "movim-standard";
meta = {
maintainers = with pkgs.lib.maintainers; [ toastal ];
};
nodes = {
server =
{ pkgs, ... }:
{
services.movim = {
inherit (movim) domain;
enable = true;
verbose = true;
podConfig = {
inherit (movim) description info;
xmppdomain = xmpp.domain;
};
nginx = { };
};
services.prosody = {
enable = true;
xmppComplianceSuite = false;
disco_items = [
{
url = "upload.${xmpp.domain}";
description = "File Uploads";
}
];
virtualHosts."${xmpp.domain}" = {
inherit (xmpp) domain;
enabled = true;
extraConfig = ''
Component "pubsub.${xmpp.domain}" "pubsub"
pubsub_max_items = 10000
expose_publisher = true
Component "upload.${xmpp.domain}" "http_file_share"
http_external_url = "http://upload.${xmpp.domain}"
http_file_share_expires_after = 300 * 24 * 60 * 60
http_file_share_size_limit = 1024 * 1024 * 1024
http_file_share_daily_quota = 4 * 1024 * 1024 * 1024
'';
};
extraConfig = ''
pep_max_items = 10000
http_paths = {
file_share = "/";
}
'';
};
networking.extraHosts = ''
127.0.0.1 ${movim.domain}
127.0.0.1 ${xmpp.domain}
'';
};
};
testScript = # python
''
server.wait_for_unit("phpfpm-movim.service")
server.wait_for_unit("nginx.service")
server.wait_for_open_port(80)
server.wait_for_unit("prosody.service")
server.succeed('prosodyctl status | grep "Prosody is running"')
server.succeed("prosodyctl register ${xmpp.admin.username} ${xmpp.domain} ${xmpp.admin.password}")
server.wait_for_unit("movim.service")
# Test unauthenticated
server.fail("curl -L --fail-with-body --max-redirs 0 http://${movim.domain}/chat")
# Test basic Websocket
server.succeed("echo \"\" | ${lib.getExe pkgs.websocat} 'ws://${movim.domain}/ws/?path=login&offset=0' --origin 'http://${movim.domain}'")
# Test login + create cookiejar
login_html = server.succeed("curl --fail-with-body -c /tmp/cookies http://${movim.domain}/login")
assert "${movim.description}" in login_html
assert "${movim.info}" in login_html
# Test authentication POST
server.succeed("curl --fail-with-body -b /tmp/cookies -X POST --data-urlencode 'username=${xmpp.admin.JID}' --data-urlencode 'password=${xmpp.admin.password}' http://${movim.domain}/login")
server.succeed("curl -L --fail-with-body --max-redirs 1 -b /tmp/cookies http://${movim.domain}/chat")
'';
}
)