1
0
Fork 1
mirror of https://github.com/NixOS/nixpkgs.git synced 2025-06-10 01:53:09 +09:00

nixos/movim: add H2O support + testing with ejabberd

This commit is contained in:
โทสฺตัล 2025-03-31 23:42:12 +07:00
parent 585b1bbffa
commit 8a8b892cc1
3 changed files with 418 additions and 25 deletions

View file

@ -175,6 +175,37 @@ let
"mysql" = "mysql.service"; "mysql" = "mysql.service";
} }
.${cfg.database.type}; .${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 in
{ {
options.services = { options.services = {
@ -475,6 +506,31 @@ in
}; };
}; };
h2o = mkOption {
type = types.nullOr (
types.submodule (import ../web-servers/h2o/vhost-options.nix { inherit config lib; })
);
default = null;
example =
lib.literalExpression # nix
''
{
serverAliases = [
"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 { nginx = mkOption {
type = types.nullOr ( type = types.nullOr (
types.submodule (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) types.submodule (import ../web-servers/nginx/vhost-options.nix { inherit config lib; })
@ -515,6 +571,25 @@ in
}; };
config = mkIf cfg.enable { 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 ]; environment.systemPackages = [ package ];
users = { users = {
@ -525,6 +600,9 @@ in
group = cfg.group; group = cfg.group;
}; };
} }
// lib.optionalAttrs (cfg.h2o != null) {
"${config.services.h2o.user}".extraGroups = [ cfg.group ];
}
// lib.optionalAttrs (cfg.nginx != null) { // lib.optionalAttrs (cfg.nginx != null) {
"${config.services.nginx.user}".extraGroups = [ cfg.group ]; "${config.services.nginx.user}".extraGroups = [ cfg.group ];
}; };
@ -571,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) ( nginx = mkIf (cfg.nginx != null) (
{ {
enable = true; enable = true;
@ -624,8 +747,7 @@ in
tryFiles = "$uri $uri/ /index.php$is_args$args"; tryFiles = "$uri $uri/ /index.php$is_args$args";
extraConfig = # nginx extraConfig = # nginx
'' ''
# https://github.com/movim/movim/issues/314 add_header Content-Security-Policy "${movimCSP}";
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';";
set $no_cache 1; set $no_cache 1;
''; '';
}; };
@ -699,11 +821,7 @@ in
''; '';
}; };
phpfpm.pools.${pool} = phpfpm.pools.${pool} = {
let
socketOwner = if (cfg.nginx != null) then config.services.nginx.user else cfg.user;
in
{
phpPackage = package.php; phpPackage = package.php;
user = cfg.user; user = cfg.user;
group = cfg.group; group = cfg.group;
@ -781,9 +899,9 @@ in
}; };
services.${phpExecutionUnit} = { services.${phpExecutionUnit} = {
wantedBy = lib.optional (cfg.nginx != null) "nginx.service"; wantedBy = lib.optional (webServerService != null) webServerService;
requiredBy = [ "movim.service" ]; requiredBy = [ "movim.service" ];
before = [ "movim.service" ] ++ lib.optional (cfg.nginx != null) "nginx.service"; before = [ "movim.service" ] ++ lib.optional (webServerService != null) webServerService;
wants = [ "network.target" ]; wants = [ "network.target" ];
requires = [ "movim-data-setup.service" ] ++ lib.optional cfg.database.createLocally dbService; requires = [ "movim-data-setup.service" ] ++ lib.optional cfg.database.createLocally dbService;
after = [ "movim-data-setup.service" ] ++ lib.optional cfg.database.createLocally dbService; after = [ "movim-data-setup.service" ] ++ lib.optional cfg.database.createLocally dbService;
@ -802,14 +920,14 @@ in
"${phpExecutionUnit}.service" "${phpExecutionUnit}.service"
] ]
++ lib.optional cfg.database.createLocally dbService ++ lib.optional cfg.database.createLocally dbService
++ lib.optional (cfg.nginx != null) "nginx.service"; ++ lib.optional (webServerService != null) webServerService;
after = after =
[ [
"movim-data-setup.service" "movim-data-setup.service"
"${phpExecutionUnit}.service" "${phpExecutionUnit}.service"
] ]
++ lib.optional cfg.database.createLocally dbService ++ lib.optional cfg.database.createLocally dbService
++ lib.optional (cfg.nginx != null) "nginx.service"; ++ lib.optional (webServerService != null) webServerService;
environment = { environment = {
PUBLIC_URL = "//${cfg.domain}"; PUBLIC_URL = "//${cfg.domain}";
WS_PORT = builtins.toString cfg.port; WS_PORT = builtins.toString cfg.port;

View file

@ -1,5 +1,6 @@
{ recurseIntoAttrs, runTest }: { recurseIntoAttrs, runTest }:
recurseIntoAttrs { recurseIntoAttrs {
ejabberd-h2o = runTest ./ejabberd-h2o.nix;
prosody-nginx = runTest ./prosody-nginx.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")
'';
}