Merge pull request #123896 from jojosch/mjolnir-init

mjolnir: init at 1.1.20, nixos/mjolnir: init, nixos/pantalaimon: init
This commit is contained in:
Graham Christensen 2021-11-12 21:28:38 -05:00 committed by GitHub
commit 3f5767d09e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 4501 additions and 1 deletions

View File

@ -485,6 +485,8 @@
./services/mail/roundcube.nix
./services/mail/sympa.nix
./services/mail/nullmailer.nix
./services/matrix/mjolnir.nix
./services/matrix/pantalaimon.nix
./services/misc/ananicy.nix
./services/misc/airsonic.nix
./services/misc/ankisyncd.nix

View File

@ -0,0 +1,240 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.mjolnir;
yamlConfig = {
inherit (cfg) dataPath managementRoom protectedRooms;
accessToken = "@ACCESS_TOKEN@"; # will be replaced in "generateConfig"
homeserverUrl =
if cfg.pantalaimon.enable then
"http://${cfg.pantalaimon.options.listenAddress}:${toString cfg.pantalaimon.options.listenPort}"
else
cfg.homeserverUrl;
pantalaimon = {
inherit (cfg.pantalaimon) username;
use = cfg.pantalaimon.enable;
password = "@PANTALAIMON_PASSWORD@"; # will be replaced in "generateConfig"
};
};
moduleConfigFile = pkgs.writeText "module-config.yaml" (
generators.toYAML { } (filterAttrs (_: v: v != null)
(fold recursiveUpdate { } [ yamlConfig cfg.settings ])));
# these config files will be merged one after the other to build the final config
configFiles = [
"${pkgs.mjolnir}/share/mjolnir/config/default.yaml"
moduleConfigFile
];
# this will generate the default.yaml file with all configFiles as inputs and
# replace all secret strings using replace-secret
generateConfig = pkgs.writeShellScript "mjolnir-generate-config" (
let
yqEvalStr = concatImapStringsSep " * " (pos: _: "select(fileIndex == ${toString (pos - 1)})") configFiles;
yqEvalArgs = concatStringsSep " " configFiles;
in
''
set -euo pipefail
umask 077
# mjolnir will try to load a config from "./config/default.yaml" in the working directory
# -> let's place the generated config there
mkdir -p ${cfg.dataPath}/config
# merge all config files into one, overriding settings of the previous one with the next config
# e.g. "eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' filea.yaml fileb.yaml" will merge filea.yaml with fileb.yaml
${pkgs.yq-go}/bin/yq eval-all -P '${yqEvalStr}' ${yqEvalArgs} > ${cfg.dataPath}/config/default.yaml
${optionalString (cfg.accessTokenFile != null) ''
${pkgs.replace-secret}/bin/replace-secret '@ACCESS_TOKEN@' '${cfg.accessTokenFile}' ${cfg.dataPath}/config/default.yaml
''}
${optionalString (cfg.pantalaimon.passwordFile != null) ''
${pkgs.replace-secret}/bin/replace-secret '@PANTALAIMON_PASSWORD@' '${cfg.pantalaimon.passwordFile}' ${cfg.dataPath}/config/default.yaml
''}
''
);
in
{
options.services.mjolnir = {
enable = mkEnableOption "Mjolnir, a moderation tool for Matrix";
homeserverUrl = mkOption {
type = types.str;
default = "https://matrix.org";
description = ''
Where the homeserver is located (client-server URL).
If <literal>pantalaimon.enable</literal> is <literal>true</literal>, this option will become the homeserver to which <literal>pantalaimon</literal> connects.
The listen address of <literal>pantalaimon</literal> will then become the <literal>homeserverUrl</literal> of <literal>mjolnir</literal>.
'';
};
accessTokenFile = mkOption {
type = with types; nullOr path;
default = null;
description = ''
File containing the matrix access token for the <literal>mjolnir</literal> user.
'';
};
pantalaimon = mkOption {
description = ''
<literal>pantalaimon</literal> options (enables E2E Encryption support).
This will create a <literal>pantalaimon</literal> instance with the name "mjolnir".
'';
default = { };
type = types.submodule {
options = {
enable = mkEnableOption ''
If true, accessToken is ignored and the username/password below will be
used instead. The access token of the bot will be stored in the dataPath.
'';
username = mkOption {
type = types.str;
description = "The username to login with.";
};
passwordFile = mkOption {
type = with types; nullOr path;
default = null;
description = ''
File containing the matrix password for the <literal>mjolnir</literal> user.
'';
};
options = mkOption {
type = types.submodule (import ./pantalaimon-options.nix);
default = { };
description = ''
passthrough additional options to the <literal>pantalaimon</literal> service.
'';
};
};
};
};
dataPath = mkOption {
type = types.path;
default = "/var/lib/mjolnir";
description = ''
The directory the bot should store various bits of information in.
'';
};
managementRoom = mkOption {
type = types.str;
default = "#moderators:example.org";
description = ''
The room ID where people can use the bot. The bot has no access controls, so
anyone in this room can use the bot - secure your room!
This should be a room alias or room ID - not a matrix.to URL.
Note: <literal>mjolnir</literal> is fairly verbose - expect a lot of messages from it.
'';
};
protectedRooms = mkOption {
type = types.listOf types.str;
default = [ ];
example = literalExpression ''
[
"https://matrix.to/#/#yourroom:example.org"
"https://matrix.to/#/#anotherroom:example.org"
]
'';
description = ''
A list of rooms to protect (matrix.to URLs).
'';
};
settings = mkOption {
default = { };
type = (pkgs.formats.yaml { }).type;
example = literalExpression ''
{
autojoinOnlyIfManager = true;
automaticallyRedactForReasons = [ "spam" "advertising" ];
}
'';
description = ''
Additional settings (see <link xlink:href="https://github.com/matrix-org/mjolnir/blob/main/config/default.yaml">mjolnir default config</link> for available settings). These settings will override settings made by the module config.
'';
};
};
config = mkIf config.services.mjolnir.enable {
assertions = [
{
assertion = !(cfg.pantalaimon.enable && cfg.pantalaimon.passwordFile == null);
message = "Specify pantalaimon.passwordFile";
}
{
assertion = !(cfg.pantalaimon.enable && cfg.accessTokenFile != null);
message = "Do not specify accessTokenFile when using pantalaimon";
}
{
assertion = !(!cfg.pantalaimon.enable && cfg.accessTokenFile == null);
message = "Specify accessTokenFile when not using pantalaimon";
}
];
services.pantalaimon-headless.instances."mjolnir" = mkIf cfg.pantalaimon.enable
{
homeserver = cfg.homeserverUrl;
} // cfg.pantalaimon.options;
systemd.services.mjolnir = {
description = "mjolnir - a moderation tool for Matrix";
wants = [ "network-online.target" ] ++ optionals (cfg.pantalaimon.enable) [ "pantalaimon-mjolnir.service" ];
after = [ "network-online.target" ] ++ optionals (cfg.pantalaimon.enable) [ "pantalaimon-mjolnir.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = ''${pkgs.mjolnir}/bin/mjolnir'';
ExecStartPre = [ generateConfig ];
WorkingDirectory = cfg.dataPath;
StateDirectory = "mjolnir";
StateDirectoryMode = "0700";
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
NoNewPrivileges = true;
PrivateDevices = true;
User = "mjolnir";
Restart = "on-failure";
/* TODO: wait for #102397 to be resolved. Then load secrets from $CREDENTIALS_DIRECTORY+"/NAME"
DynamicUser = true;
LoadCredential = [] ++
optionals (cfg.accessTokenFile != null) [
"access_token:${cfg.accessTokenFile}"
] ++
optionals (cfg.pantalaimon.passwordFile != null) [
"pantalaimon_password:${cfg.pantalaimon.passwordFile}"
];
*/
};
};
users = {
users.mjolnir = {
group = "mjolnir";
isSystemUser = true;
};
groups.mjolnir = { };
};
};
meta = {
doc = ./mjolnir.xml;
maintainers = with maintainers; [ jojosch ];
};
}

View File

@ -0,0 +1,134 @@
<chapter xmlns="http://docbook.org/ns/docbook"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xi="http://www.w3.org/2001/XInclude"
version="5.0"
xml:id="module-services-mjolnir">
<title>Mjolnir (Matrix Moderation Tool)</title>
<para>
This chapter will show you how to set up your own, self-hosted
<link xlink:href="https://github.com/matrix-org/mjolnir">Mjolnir</link>
instance.
</para>
<para>
As an all-in-one moderation tool, it can protect your server from
malicious invites, spam messages, and whatever else you don't want.
In addition to server-level protection, Mjolnir is great for communities
wanting to protect their rooms without having to use their personal
accounts for moderation.
</para>
<para>
The bot by default includes support for bans, redactions, anti-spam,
server ACLs, room directory changes, room alias transfers, account
deactivation, room shutdown, and more.
</para>
<para>
See the <link xlink:href="https://github.com/matrix-org/mjolnir#readme">README</link>
page and the <link xlink:href="https://github.com/matrix-org/mjolnir/blob/main/docs/moderators.md">Moderator's guide</link>
for additional instructions on how to setup and use Mjolnir.
</para>
<para>
For <link linkend="opt-services.mjolnir.settings">additional settings</link>
see <link xlink:href="https://github.com/matrix-org/mjolnir/blob/main/config/default.yaml">the default configuration</link>.
</para>
<section xml:id="module-services-mjolnir-setup">
<title>Mjolnir Setup</title>
<para>
First create a new Room which will be used as a management room for Mjolnir. In
this room, Mjolnir will log possible errors and debugging information. You'll
need to set this Room-ID in <link linkend="opt-services.mjolnir.managementRoom">services.mjolnir.managementRoom</link>.
</para>
<para>
Next, create a new user for Mjolnir on your homeserver, if not present already.
</para>
<para>
The Mjolnir Matrix user expects to be free of any rate limiting.
See <link xlink:href="https://github.com/matrix-org/synapse/issues/6286">Synapse #6286</link>
for an example on how to achieve this.
</para>
<para>
If you want Mjolnir to be able to deactivate users, move room aliases, shutdown rooms, etc.
you'll need to make the Mjolnir user a Matrix server admin.
</para>
<para>
Now invite the Mjolnir user to the management room.
</para>
<para>
It is recommended to use <link xlink:href="https://github.com/matrix-org/pantalaimon">Pantalaimon</link>,
so your management room can be encrypted. This also applies if you are looking to moderate an encrypted room.
</para>
<para>
To enable the Pantalaimon E2E Proxy for mjolnir, enable
<link linkend="opt-services.mjolnir.pantalaimon.enable">services.mjolnir.pantalaimon</link>. This will
autoconfigure a new Pantalaimon instance, which will connect to the homeserver
set in <link linkend="opt-services.mjolnir.homeserverUrl">services.mjolnir.homeserverUrl</link> and Mjolnir itself
will be configured to connect to the new Pantalaimon instance.
</para>
<programlisting>
{
services.mjolnir = {
enable = true;
<link linkend="opt-services.mjolnir.homeserverUrl">homeserverUrl</link> = "https://matrix.domain.tld";
<link linkend="opt-services.mjolnir.pantalaimon">pantalaimon</link> = {
<link linkend="opt-services.mjolnir.pantalaimon.enable">enable</link> = true;
<link linkend="opt-services.mjolnir.pantalaimon.username">username</link> = "mjolnir";
<link linkend="opt-services.mjolnir.pantalaimon.passwordFile">passwordFile</link> = "/run/secrets/mjolnir-password";
};
<link linkend="opt-services.mjolnir.protectedRooms">protectedRooms</link> = [
"https://matrix.to/#/!xxx:domain.tld"
];
<link linkend="opt-services.mjolnir.managementRoom">managementRoom</link> = "!yyy:domain.tld";
};
}
</programlisting>
<section xml:id="module-services-mjolnir-setup-ems">
<title>Element Matrix Services (EMS)</title>
<para>
If you are using a managed <link xlink:href="https://ems.element.io/">"Element Matrix Services (EMS)"</link>
server, you will need to consent to the terms and conditions. Upon startup, an error
log entry with a URL to the consent page will be generated.
</para>
</section>
</section>
<section xml:id="module-services-mjolnir-matrix-synapse-antispam">
<title>Synapse Antispam Module</title>
<para>
A Synapse module is also available to apply the same rulesets the bot
uses across an entire homeserver.
</para>
<para>
To use the Antispam Module, add <package>matrix-synapse-plugins.matrix-synapse-mjolnir-antispam</package>
to the Synapse plugin list and enable the <literal>mjolnir.AntiSpam</literal> module.
</para>
<programlisting>
{
services.matrix-synapse = {
plugins = with pkgs; [
matrix-synapse-plugins.matrix-synapse-mjolnir-antispam
];
extraConfig = ''
modules:
- module: mjolnir.AntiSpam
config:
# Prevent servers/users in the ban lists from inviting users on this
# server to rooms. Default true.
block_invites: true
# Flag messages sent by servers/users in the ban lists as spam. Currently
# this means that spammy messages will appear as empty to users. Default
# false.
block_messages: false
# Remove users from the user directory search by filtering matrix IDs and
# display names by the entries in the user ban list. Default false.
block_usernames: false
# The room IDs of the ban lists to honour. Unlike other parts of Mjolnir,
# this list cannot be room aliases or permalinks. This server is expected
# to already be joined to the room - Mjolnir will not automatically join
# these rooms.
ban_lists:
- "!roomid:example.org"
'';
};
}
</programlisting>
</section>
</chapter>

View File

@ -0,0 +1,70 @@
{ config, lib, name, ... }:
with lib;
{
options = {
dataPath = mkOption {
type = types.path;
default = "/var/lib/pantalaimon-${name}";
description = ''
The directory where <literal>pantalaimon</literal> should store its state such as the database file.
'';
};
logLevel = mkOption {
type = types.enum [ "info" "warning" "error" "debug" ];
default = "warning";
description = ''
Set the log level of the daemon.
'';
};
homeserver = mkOption {
type = types.str;
example = "https://matrix.org";
description = ''
The URI of the homeserver that the <literal>pantalaimon</literal> proxy should
forward requests to, without the matrix API path but including
the http(s) schema.
'';
};
ssl = mkOption {
type = types.bool;
default = true;
description = ''
Whether or not SSL verification should be enabled for outgoing
connections to the homeserver.
'';
};
listenAddress = mkOption {
type = types.str;
default = "localhost";
description = ''
The address where the daemon will listen to client connections
for this homeserver.
'';
};
listenPort = mkOption {
type = types.port;
default = 8009;
description = ''
The port where the daemon will listen to client connections for
this homeserver. Note that the listen address/port combination
needs to be unique between different homeservers.
'';
};
extraSettings = mkOption {
type = types.attrs;
default = { };
description = ''
Extra configuration options. See
<link xlink:href="https://github.com/matrix-org/pantalaimon/blob/master/docs/man/pantalaimon.5.md">pantalaimon(5)</link>
for available options.
'';
};
};
}

View File

@ -0,0 +1,70 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.pantalaimon-headless;
iniFmt = pkgs.formats.ini { };
mkConfigFile = name: instanceConfig: iniFmt.generate "pantalaimon.conf" {
Default = {
LogLevel = instanceConfig.logLevel;
Notifications = false;
};
${name} = (recursiveUpdate
{
Homeserver = instanceConfig.homeserver;
ListenAddress = instanceConfig.listenAddress;
ListenPort = instanceConfig.listenPort;
SSL = instanceConfig.ssl;
# Set some settings to prevent user interaction for headless operation
IgnoreVerification = true;
UseKeyring = false;
}
instanceConfig.extraSettings
);
};
mkPantalaimonService = name: instanceConfig:
nameValuePair "pantalaimon-${name}" {
description = "pantalaimon instance ${name} - E2EE aware proxy daemon for matrix clients";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = ''${pkgs.pantalaimon-headless}/bin/pantalaimon --config ${mkConfigFile name instanceConfig} --data-path ${instanceConfig.dataPath}'';
Restart = "on-failure";
DynamicUser = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
ProtectHome = true;
ProtectSystem = "strict";
StateDirectory = "pantalaimon-${name}";
};
};
in
{
options.services.pantalaimon-headless.instances = mkOption {
default = { };
type = types.attrsOf (types.submodule (import ./pantalaimon-options.nix));
description = ''
Declarative instance config.
Note: to use pantalaimon interactively, e.g. for a Matrix client which does not
support End-to-end encryption (like <literal>fractal</literal>), refer to the home-manager module.
'';
};
config = mkIf (config.services.pantalaimon-headless.instances != { })
{
systemd.services = mapAttrs' mkPantalaimonService config.services.pantalaimon-headless.instances;
};
meta = {
maintainers = with maintainers; [ jojosch ];
};
}

View File

@ -260,6 +260,7 @@ in
miniflux = handleTest ./miniflux.nix {};
minio = handleTest ./minio.nix {};
misc = handleTest ./misc.nix {};
mjolnir = handleTest ./matrix/mjolnir.nix {};
mod_perl = handleTest ./mod_perl.nix {};
moinmoin = handleTest ./moinmoin.nix {};
mongodb = handleTest ./mongodb.nix {};
@ -340,6 +341,7 @@ in
packagekit = handleTest ./packagekit.nix {};
pam-oath-login = handleTest ./pam-oath-login.nix {};
pam-u2f = handleTest ./pam-u2f.nix {};
pantalaimon = handleTest ./matrix/pantalaimon.nix {};
pantheon = handleTest ./pantheon.nix {};
paperless-ng = handleTest ./paperless-ng.nix {};
parsedmarc = handleTest ./parsedmarc {};

View File

@ -0,0 +1,165 @@
import ../make-test-python.nix (
{ pkgs, ... }:
let
# Set up SSL certs for Synapse to be happy.
runWithOpenSSL = file: cmd: pkgs.runCommand file
{
buildInputs = [ pkgs.openssl ];
}
cmd;
ca_key = runWithOpenSSL "ca-key.pem" "openssl genrsa -out $out 2048";
ca_pem = runWithOpenSSL "ca.pem" ''
openssl req \
-x509 -new -nodes -key ${ca_key} \
-days 10000 -out $out -subj "/CN=snakeoil-ca"
'';
key = runWithOpenSSL "matrix_key.pem" "openssl genrsa -out $out 2048";
csr = runWithOpenSSL "matrix.csr" ''
openssl req \
-new -key ${key} \
-out $out -subj "/CN=localhost" \
'';
cert = runWithOpenSSL "matrix_cert.pem" ''
openssl x509 \
-req -in ${csr} \
-CA ${ca_pem} -CAkey ${ca_key} \
-CAcreateserial -out $out \
-days 365
'';
in
{
name = "mjolnir";
meta = with pkgs.lib; {
maintainers = teams.matrix.members;
};
nodes = {
homeserver = { pkgs, ... }: {
services.matrix-synapse = {
enable = true;
database_type = "sqlite3";
tls_certificate_path = "${cert}";
tls_private_key_path = "${key}";
enable_registration = true;
registration_shared_secret = "supersecret-registration";
listeners = [
# The default but tls=false
{
"bind_address" = "";
"port" = 8448;
"resources" = [
{ "compress" = true; "names" = [ "client" "webclient" ]; }
{ "compress" = false; "names" = [ "federation" ]; }
];
"tls" = false;
"type" = "http";
"x_forwarded" = false;
}
];
};
networking.firewall.allowedTCPPorts = [ 8448 ];
environment.systemPackages = [
(pkgs.writeShellScriptBin "register_mjolnir_user" ''
exec ${pkgs.matrix-synapse}/bin/register_new_matrix_user \
-u mjolnir \
-p mjolnir-password \
--admin \
--shared-secret supersecret-registration \
http://localhost:8448
''
)
(pkgs.writeShellScriptBin "register_moderator_user" ''
exec ${pkgs.matrix-synapse}/bin/register_new_matrix_user \
-u moderator \
-p moderator-password \
--no-admin \
--shared-secret supersecret-registration \
http://localhost:8448
''
)
];
};
mjolnir = { pkgs, ... }: {
services.mjolnir = {
enable = true;
homeserverUrl = "http://homeserver:8448";
pantalaimon = {
enable = true;
username = "mjolnir";
passwordFile = pkgs.writeText "password.txt" "mjolnir-password";
};
managementRoom = "#moderators:homeserver";
};
};
client = { pkgs, ... }: {
environment.systemPackages = [
(pkgs.writers.writePython3Bin "create_management_room_and_invite_mjolnir"
{ libraries = [ pkgs.python3Packages.matrix-nio ]; } ''
import asyncio
from nio import (
AsyncClient,
EnableEncryptionBuilder
)
async def main() -> None:
client = AsyncClient("http://homeserver:8448", "moderator")
await client.login("moderator-password")
room = await client.room_create(
name="Moderators",
alias="moderators",
initial_state=[EnableEncryptionBuilder().as_dict()],
)
await client.join(room.room_id)
await client.room_invite(room.room_id, "@mjolnir:homeserver")
asyncio.run(main())
''
)
];
};
};
testScript = ''
with subtest("start homeserver"):
homeserver.start()
homeserver.wait_for_unit("matrix-synapse.service")
homeserver.wait_until_succeeds("curl --fail -L http://localhost:8448/")
with subtest("register users"):
# register mjolnir user
homeserver.succeed("register_mjolnir_user")
# register moderator user
homeserver.succeed("register_moderator_user")
with subtest("start mjolnir"):
mjolnir.start()
# wait for pantalaimon to be ready
mjolnir.wait_for_unit("pantalaimon-mjolnir.service")
mjolnir.wait_for_unit("mjolnir.service")
mjolnir.wait_until_succeeds("curl --fail -L http://localhost:8009/")
with subtest("ensure mjolnir can be invited to the management room"):
client.start()
client.wait_until_succeeds("curl --fail -L http://homeserver:8448/")
client.succeed("create_management_room_and_invite_mjolnir")
mjolnir.wait_for_console_text("Startup complete. Now monitoring rooms")
'';
}
)

View File

@ -0,0 +1,65 @@
import ../make-test-python.nix (
{ pkgs, ... }:
let
pantalaimonInstanceName = "testing";
# Set up SSL certs for Synapse to be happy.
runWithOpenSSL = file: cmd: pkgs.runCommand file
{
buildInputs = [ pkgs.openssl ];
}
cmd;
ca_key = runWithOpenSSL "ca-key.pem" "openssl genrsa -out $out 2048";
ca_pem = runWithOpenSSL "ca.pem" ''
openssl req \
-x509 -new -nodes -key ${ca_key} \
-days 10000 -out $out -subj "/CN=snakeoil-ca"
'';
key = runWithOpenSSL "matrix_key.pem" "openssl genrsa -out $out 2048";
csr = runWithOpenSSL "matrix.csr" ''
openssl req \
-new -key ${key} \
-out $out -subj "/CN=localhost" \
'';
cert = runWithOpenSSL "matrix_cert.pem" ''
openssl x509 \
-req -in ${csr} \
-CA ${ca_pem} -CAkey ${ca_key} \
-CAcreateserial -out $out \
-days 365
'';
in
{
name = "pantalaimon";
meta = with pkgs.lib; {
maintainers = teams.matrix.members;
};
machine = { pkgs, ... }: {
services.pantalaimon-headless.instances.${pantalaimonInstanceName} = {
homeserver = "https://localhost:8448";
listenAddress = "0.0.0.0";
listenPort = 8888;
logLevel = "debug";
ssl = false;
};
services.matrix-synapse = {
enable = true;
database_type = "sqlite3";
tls_certificate_path = "${cert}";
tls_private_key_path = "${key}";
};
};
testScript = ''
start_all()
machine.wait_for_unit("pantalaimon-${pantalaimonInstanceName}.service")
machine.wait_for_unit("matrix-synapse.service")
machine.wait_until_succeeds(
"curl --fail -L http://localhost:8888/"
)
'';
}
)

View File

@ -1,7 +1,7 @@
{ lib, stdenv, buildPythonApplication, fetchFromGitHub, pythonOlder,
attrs, aiohttp, appdirs, click, keyring, Logbook, peewee, janus,
prompt-toolkit, matrix-nio, dbus-python, pydbus, notify2, pygobject3,
setuptools, installShellFiles,
setuptools, installShellFiles, nixosTests,
pytest, faker, pytest-aiohttp, aioresponses,
@ -63,6 +63,10 @@ buildPythonApplication rec {
installManPage docs/man/*.[1-9]
'';
passthru.tests = {
inherit (nixosTests) pantalaimon;
};
meta = with lib; {
description = "An end-to-end encryption aware Matrix reverse proxy daemon";
homepage = "https://github.com/matrix-org/pantalaimon";

View File

@ -0,0 +1,86 @@
{ lib
, nixosTests
, stdenv
, fetchFromGitHub
, makeWrapper
, nodejs
, pkgs
}:
stdenv.mkDerivation rec {
pname = "mjolnir";
version = "1.1.20";
src = fetchFromGitHub {
owner = "matrix-org";
repo = "mjolnir";
rev = "v${version}";
sha256 = "yfMBnNriSpwitR4u664iz+8uWp/3iSTymyFajMBP5xg=";
};
nativeBuildInputs = [
nodejs
makeWrapper
];
buildPhase =
let
nodeDependencies = ((import ./node-composition.nix {
inherit pkgs nodejs;
inherit (stdenv.hostPlatform) system;
}).nodeDependencies.override (old: {
# access to path '/nix/store/...-source' is forbidden in restricted mode
src = src;
dontNpmInstall = true;
}));
in
''
runHook preBuild
ln -s ${nodeDependencies}/lib/node_modules .
export PATH="${nodeDependencies}/bin:$PATH"
npm run build
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/share
cp -a . $out/share/mjolnir
makeWrapper ${nodejs}/bin/node $out/bin/mjolnir \
--add-flags $out/share/mjolnir/lib/index.js
runHook postInstall
'';
passthru = {
tests = {
inherit (nixosTests) mjolnir;
};
updateScript = ./update.sh;
};
meta = with lib; {
description = "A moderation tool for Matrix";
homepage = "https://github.com/matrix-org/mjolnir";
longDescription = ''
As an all-in-one moderation tool, it can protect your server from
malicious invites, spam messages, and whatever else you don't want.
In addition to server-level protection, Mjolnir is great for communities
wanting to protect their rooms without having to use their personal
accounts for moderation.
The bot by default includes support for bans, redactions, anti-spam,
server ACLs, room directory changes, room alias transfers, account
deactivation, room shutdown, and more.
A Synapse module is also available to apply the same rulesets the bot
uses across an entire homeserver.
'';
license = licenses.asl20;
maintainers = with maintainers; [ jojosch ];
};
}

View File

@ -0,0 +1,17 @@
# This file has been generated by node2nix 1.9.0. Do not edit!
{pkgs ? import <nixpkgs> {
inherit system;
}, system ? builtins.currentSystem, nodejs ? pkgs."nodejs-12_x"}:
let
nodeEnv = import ./node-env.nix {
inherit (pkgs) stdenv lib python2 runCommand writeTextFile;
inherit pkgs nodejs;
libtool = if pkgs.stdenv.isDarwin then pkgs.darwin.cctools else null;
};
in
import ./node-deps.nix {
inherit (pkgs) fetchurl nix-gitignore stdenv lib fetchgit;
inherit nodeEnv;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,573 @@
# This file originates from node2nix
{lib, stdenv, nodejs, python2, pkgs, libtool, runCommand, writeTextFile}:
let
# Workaround to cope with utillinux in Nixpkgs 20.09 and util-linux in Nixpkgs master
utillinux = if pkgs ? utillinux then pkgs.utillinux else pkgs.util-linux;
python = if nodejs ? python then nodejs.python else python2;
# Create a tar wrapper that filters all the 'Ignoring unknown extended header keyword' noise
tarWrapper = runCommand "tarWrapper" {} ''
mkdir -p $out/bin
cat > $out/bin/tar <<EOF
#! ${stdenv.shell} -e
$(type -p tar) "\$@" --warning=no-unknown-keyword --delay-directory-restore
EOF
chmod +x $out/bin/tar
'';
# Function that generates a TGZ file from a NPM project
buildNodeSourceDist =
{ name, version, src, ... }:
stdenv.mkDerivation {
name = "node-tarball-${name}-${version}";
inherit src;
buildInputs = [ nodejs ];
buildPhase = ''
export HOME=$TMPDIR
tgzFile=$(npm pack | tail -n 1) # Hooks to the pack command will add output (https://docs.npmjs.com/misc/scripts)
'';
installPhase = ''
mkdir -p $out/tarballs
mv $tgzFile $out/tarballs
mkdir -p $out/nix-support
echo "file source-dist $out/tarballs/$tgzFile" >> $out/nix-support/hydra-build-products
'';
};
includeDependencies = {dependencies}:
lib.optionalString (dependencies != [])
(lib.concatMapStrings (dependency:
''
# Bundle the dependencies of the package
mkdir -p node_modules
cd node_modules
# Only include dependencies if they don't exist. They may also be bundled in the package.
if [ ! -e "${dependency.name}" ]
then
${composePackage dependency}
fi
cd ..
''
) dependencies);
# Recursively composes the dependencies of a package
composePackage = { name, packageName, src, dependencies ? [], ... }@args:
builtins.addErrorContext "while evaluating node package '${packageName}'" ''
DIR=$(pwd)
cd $TMPDIR
unpackFile ${src}
# Make the base dir in which the target dependency resides first
mkdir -p "$(dirname "$DIR/${packageName}")"
if [ -f "${src}" ]
then
# Figure out what directory has been unpacked
packageDir="$(find . -maxdepth 1 -type d | tail -1)"
# Restore write permissions to make building work
find "$packageDir" -type d -exec chmod u+x {} \;
chmod -R u+w "$packageDir"
# Move the extracted tarball into the output folder
mv "$packageDir" "$DIR/${packageName}"
elif [ -d "${src}" ]
then
# Get a stripped name (without hash) of the source directory.
# On old nixpkgs it's already set internally.
if [ -z "$strippedName" ]
then
strippedName="$(stripHash ${src})"
fi
# Restore write permissions to make building work
chmod -R u+w "$strippedName"
# Move the extracted directory into the output folder
mv "$strippedName" "$DIR/${packageName}"
fi
# Unset the stripped name to not confuse the next unpack step
unset strippedName
# Include the dependencies of the package
cd "$DIR/${packageName}"
${includeDependencies { inherit dependencies; }}
cd ..
${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
'';
pinpointDependencies = {dependencies, production}:
let
pinpointDependenciesFromPackageJSON = writeTextFile {
name = "pinpointDependencies.js";
text = ''
var fs = require('fs');
var path = require('path');
function resolveDependencyVersion(location, name) {
if(location == process.env['NIX_STORE']) {
return null;
} else {
var dependencyPackageJSON = path.join(location, "node_modules", name, "package.json");
if(fs.existsSync(dependencyPackageJSON)) {
var dependencyPackageObj = JSON.parse(fs.readFileSync(dependencyPackageJSON));
if(dependencyPackageObj.name == name) {
return dependencyPackageObj.version;
}
} else {
return resolveDependencyVersion(path.resolve(location, ".."), name);
}
}
}
function replaceDependencies(dependencies) {
if(typeof dependencies == "object" && dependencies !== null) {
for(var dependency in dependencies) {
var resolvedVersion = resolveDependencyVersion(process.cwd(), dependency);
if(resolvedVersion === null) {
process.stderr.write("WARNING: cannot pinpoint dependency: "+dependency+", context: "+process.cwd()+"\n");
} else {
dependencies[dependency] = resolvedVersion;
}
}
}
}
/* Read the package.json configuration */
var packageObj = JSON.parse(fs.readFileSync('./package.json'));
/* Pinpoint all dependencies */
replaceDependencies(packageObj.dependencies);
if(process.argv[2] == "development") {
replaceDependencies(packageObj.devDependencies);
}
replaceDependencies(packageObj.optionalDependencies);
/* Write the fixed package.json file */
fs.writeFileSync("package.json", JSON.stringify(packageObj, null, 2));
'';
};
in
''
node ${pinpointDependenciesFromPackageJSON} ${if production then "production" else "development"}
${lib.optionalString (dependencies != [])
''
if [ -d node_modules ]
then
cd node_modules
${lib.concatMapStrings (dependency: pinpointDependenciesOfPackage dependency) dependencies}
cd ..
fi
''}
'';
# Recursively traverses all dependencies of a package and pinpoints all
# dependencies in the package.json file to the versions that are actually
# being used.
pinpointDependenciesOfPackage = { packageName, dependencies ? [], production ? true, ... }@args:
''
if [ -d "${packageName}" ]
then
cd "${packageName}"
${pinpointDependencies { inherit dependencies production; }}
cd ..
${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
fi
'';
# Extract the Node.js source code which is used to compile packages with
# native bindings
nodeSources = runCommand "node-sources" {} ''
tar --no-same-owner --no-same-permissions -xf ${nodejs.src}
mv node-* $out
'';
# Script that adds _integrity fields to all package.json files to prevent NPM from consulting the cache (that is empty)
addIntegrityFieldsScript = writeTextFile {
name = "addintegrityfields.js";
text = ''
var fs = require('fs');
var path = require('path');
function augmentDependencies(baseDir, dependencies) {
for(var dependencyName in dependencies) {
var dependency = dependencies[dependencyName];
// Open package.json and augment metadata fields
var packageJSONDir = path.join(baseDir, "node_modules", dependencyName);
var packageJSONPath = path.join(packageJSONDir, "package.json");
if(fs.existsSync(packageJSONPath)) { // Only augment packages that exist. Sometimes we may have production installs in which development dependencies can be ignored
console.log("Adding metadata fields to: "+packageJSONPath);
var packageObj = JSON.parse(fs.readFileSync(packageJSONPath));
if(dependency.integrity) {
packageObj["_integrity"] = dependency.integrity;
} else {
packageObj["_integrity"] = "sha1-000000000000000000000000000="; // When no _integrity string has been provided (e.g. by Git dependencies), add a dummy one. It does not seem to harm and it bypasses downloads.
}
if(dependency.resolved) {
packageObj["_resolved"] = dependency.resolved; // Adopt the resolved property if one has been provided
} else {
packageObj["_resolved"] = dependency.version; // Set the resolved version to the version identifier. This prevents NPM from cloning Git repositories.
}
if(dependency.from !== undefined) { // Adopt from property if one has been provided
packageObj["_from"] = dependency.from;
}
fs.writeFileSync(packageJSONPath, JSON.stringify(packageObj, null, 2));
}
// Augment transitive dependencies
if(dependency.dependencies !== undefined) {
augmentDependencies(packageJSONDir, dependency.dependencies);
}
}
}
if(fs.existsSync("./package-lock.json")) {
var packageLock = JSON.parse(fs.readFileSync("./package-lock.json"));
if(![1, 2].includes(packageLock.lockfileVersion)) {
process.stderr.write("Sorry, I only understand lock file versions 1 and 2!\n");
process.exit(1);
}
if(packageLock.dependencies !== undefined) {
augmentDependencies(".", packageLock.dependencies);
}
}
'';
};
# Reconstructs a package-lock file from the node_modules/ folder structure and package.json files with dummy sha1 hashes
reconstructPackageLock = writeTextFile {
name = "addintegrityfields.js";
text = ''
var fs = require('fs');
var path = require('path');
var packageObj = JSON.parse(fs.readFileSync("package.json"));
var lockObj = {
name: packageObj.name,
version: packageObj.version,
lockfileVersion: 1,
requires: true,
dependencies: {}
};
function augmentPackageJSON(filePath, dependencies) {
var packageJSON = path.join(filePath, "package.json");
if(fs.existsSync(packageJSON)) {
var packageObj = JSON.parse(fs.readFileSync(packageJSON));
dependencies[packageObj.name] = {
version: packageObj.version,
integrity: "sha1-000000000000000000000000000=",
dependencies: {}
};
processDependencies(path.join(filePath, "node_modules"), dependencies[packageObj.name].dependencies);
}
}
function processDependencies(dir, dependencies) {
if(fs.existsSync(dir)) {
var files = fs.readdirSync(dir);
files.forEach(function(entry) {
var filePath = path.join(dir, entry);
var stats = fs.statSync(filePath);
if(stats.isDirectory()) {
if(entry.substr(0, 1) == "@") {
// When we encounter a namespace folder, augment all packages belonging to the scope
var pkgFiles = fs.readdirSync(filePath);
pkgFiles.forEach(function(entry) {
if(stats.isDirectory()) {
var pkgFilePath = path.join(filePath, entry);
augmentPackageJSON(pkgFilePath, dependencies);
}
});
} else {
augmentPackageJSON(filePath, dependencies);
}
}
});
}
}
processDependencies("node_modules", lockObj.dependencies);
fs.writeFileSync("package-lock.json", JSON.stringify(lockObj, null, 2));
'';
};
prepareAndInvokeNPM = {packageName, bypassCache, reconstructLock, npmFlags, production}:
let
forceOfflineFlag = if bypassCache then "--offline" else "--registry http://www.example.com";
in
''
# Pinpoint the versions of all dependencies to the ones that are actually being used
echo "pinpointing versions of dependencies..."
source $pinpointDependenciesScriptPath
# Patch the shebangs of the bundled modules to prevent them from
# calling executables outside the Nix store as much as possible
patchShebangs .
# Deploy the Node.js package by running npm install. Since the
# dependencies have been provided already by ourselves, it should not
# attempt to install them again, which is good, because we want to make
# it Nix's responsibility. If it needs to install any dependencies
# anyway (e.g. because the dependency parameters are
# incomplete/incorrect), it fails.
#
# The other responsibilities of NPM are kept -- version checks, build
# steps, postprocessing etc.
export HOME=$TMPDIR
cd "${packageName}"
runHook preRebuild
${lib.optionalString bypassCache ''
${lib.optionalString reconstructLock ''
if [ -f package-lock.json ]
then
echo "WARNING: Reconstruct lock option enabled, but a lock file already exists!"
echo "This will most likely result in version mismatches! We will remove the lock file and regenerate it!"
rm package-lock.json
else
echo "No package-lock.json file found, reconstructing..."
fi
node ${reconstructPackageLock}
''}
node ${addIntegrityFieldsScript}
''}
npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${lib.optionalString production "--production"} rebuild
if [ "''${dontNpmInstall-}" != "1" ]
then
# NPM tries to download packages even when they already exist if npm-shrinkwrap is used.
rm -f npm-shrinkwrap.json
npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${lib.optionalString production "--production"} install
fi
'';
# Builds and composes an NPM package including all its dependencies
buildNodePackage =
{ name
, packageName
, version
, dependencies ? []
, buildInputs ? []
, production ? true
, npmFlags ? ""
, dontNpmInstall ? false
, bypassCache ? false
, reconstructLock ? false
, preRebuild ? ""
, dontStrip ? true
, unpackPhase ? "true"
, buildPhase ? "true"
, meta ? {}
, ... }@args:
let
extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" "dontStrip" "dontNpmInstall" "preRebuild" "unpackPhase" "buildPhase" "meta" ];
in
stdenv.mkDerivation ({
name = "${name}-${version}";
buildInputs = [ tarWrapper python nodejs ]
++ lib.optional (stdenv.isLinux) utillinux
++ lib.optional (stdenv.isDarwin) libtool
++ buildInputs;
inherit nodejs;
inherit dontStrip; # Stripping may fail a build for some package deployments
inherit dontNpmInstall preRebuild unpackPhase buildPhase;
compositionScript = composePackage args;
pinpointDependenciesScript = pinpointDependenciesOfPackage args;
passAsFile = [ "compositionScript" "pinpointDependenciesScript" ];
installPhase = ''
# Create and enter a root node_modules/ folder
mkdir -p $out/lib/node_modules
cd $out/lib/node_modules
# Compose the package and all its dependencies
source $compositionScriptPath
${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }}
# Create symlink to the deployed executable folder, if applicable
if [ -d "$out/lib/node_modules/.bin" ]
then
ln -s $out/lib/node_modules/.bin $out/bin
fi
# Create symlinks to the deployed manual page folders, if applicable
if [ -d "$out/lib/node_modules/${packageName}/man" ]
then
mkdir -p $out/share
for dir in "$out/lib/node_modules/${packageName}/man/"*
do
mkdir -p $out/share/man/$(basename "$dir")
for page in "$dir"/*
do
ln -s $page $out/share/man/$(basename "$dir")
done
done
fi
# Run post install hook, if provided
runHook postInstall
'';
meta = {
# default to Node.js' platforms
platforms = nodejs.meta.platforms;
} // meta;
} // extraArgs);
# Builds a node environment (a node_modules folder and a set of binaries)
buildNodeDependencies =
{ name
, packageName
, version
, src
, dependencies ? []
, buildInputs ? []
, production ? true
, npmFlags ? ""
, dontNpmInstall ? false
, bypassCache ? false
, reconstructLock ? false
, dontStrip ? true
, unpackPhase ? "true"
, buildPhase ? "true"
, ... }@args:
let
extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" ];
in
stdenv.mkDerivation ({
name = "node-dependencies-${name}-${version}";
buildInputs = [ tarWrapper python nodejs ]
++ lib.optional (stdenv.isLinux) utillinux
++ lib.optional (stdenv.isDarwin) libtool
++ buildInputs;
inherit dontStrip; # Stripping may fail a build for some package deployments
inherit dontNpmInstall unpackPhase buildPhase;
includeScript = includeDependencies { inherit dependencies; };
pinpointDependenciesScript = pinpointDependenciesOfPackage args;
passAsFile = [ "includeScript" "pinpointDependenciesScript" ];
installPhase = ''
mkdir -p $out/${packageName}
cd $out/${packageName}
source $includeScriptPath
# Create fake package.json to make the npm commands work properly
cp ${src}/package.json .
chmod 644 package.json
${lib.optionalString bypassCache ''
if [ -f ${src}/package-lock.json ]
then
cp ${src}/package-lock.json .
fi
''}
# Go to the parent folder to make sure that all packages are pinpointed
cd ..
${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }}
# Expose the executables that were installed
cd ..
${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
mv ${packageName} lib
ln -s $out/lib/node_modules/.bin $out/bin
'';
} // extraArgs);
# Builds a development shell
buildNodeShell =
{ name
, packageName
, version
, src
, dependencies ? []
, buildInputs ? []
, production ? true
, npmFlags ? ""
, dontNpmInstall ? false
, bypassCache ? false
, reconstructLock ? false
, dontStrip ? true
, unpackPhase ? "true"
, buildPhase ? "true"
, ... }@args:
let
nodeDependencies = buildNodeDependencies args;
in
stdenv.mkDerivation {
name = "node-shell-${name}-${version}";
buildInputs = [ python nodejs ] ++ lib.optional (stdenv.isLinux) utillinux ++ buildInputs;
buildCommand = ''
mkdir -p $out/bin
cat > $out/bin/shell <<EOF
#! ${stdenv.shell} -e
$shellHook
exec ${stdenv.shell}
EOF
chmod +x $out/bin/shell
'';
# Provide the dependencies in a development shell through the NODE_PATH environment variable
inherit nodeDependencies;
shellHook = lib.optionalString (dependencies != []) ''
export NODE_PATH=${nodeDependencies}/lib/node_modules
export PATH="${nodeDependencies}/bin:$PATH"
'';
};
in
{
buildNodeSourceDist = lib.makeOverridable buildNodeSourceDist;
buildNodePackage = lib.makeOverridable buildNodePackage;
buildNodeDependencies = lib.makeOverridable buildNodeDependencies;
buildNodeShell = lib.makeOverridable buildNodeShell;
}

29
pkgs/servers/mjolnir/update.sh Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p curl common-updater-scripts nodePackages.node2nix gnused nix coreutils jq
set -euo pipefail
latestVersion="$(curl -s "https://api.github.com/repos/matrix-org/mjolnir/releases?per_page=1" | jq -r ".[0].tag_name" | sed 's/^v//')"
currentVersion=$(nix-instantiate --eval -E "with import ./. {}; mjolnir.version or (lib.getVersion mjolnir)" | tr -d '"')
if [[ "$currentVersion" == "$latestVersion" ]]; then
echo "mjolnir is up-to-date: $currentVersion"
exit 0
fi
update-source-version mjolnir 0 0000000000000000000000000000000000000000000000000000000000000000
update-source-version mjolnir "$latestVersion"
# use patched source
store_src="$(nix-build . -A mjolnir.src --no-out-link)"
cd "$(dirname "${BASH_SOURCE[0]}")"
node2nix \
--nodejs-12 \
--development \
--node-env ./node-env.nix \
--output ./node-deps.nix \
--input "$store_src/package.json" \
--composition ./node-composition.nix

View File

@ -6997,6 +6997,8 @@ with pkgs;
ministat = callPackage ../tools/misc/ministat { };
mjolnir = callPackage ../servers/mjolnir { };
mmv = callPackage ../tools/misc/mmv { };
mmv-go = callPackage ../tools/misc/mmv-go { };
@ -27295,6 +27297,10 @@ with pkgs;
pantalaimon = python3Packages.callPackage ../applications/networking/instant-messengers/pantalaimon { };
pantalaimon-headless = python3Packages.callPackage ../applications/networking/instant-messengers/pantalaimon {
enableDbusUi = false;
};
pavucontrol = callPackage ../applications/audio/pavucontrol { };
paraview = libsForQt5.callPackage ../applications/graphics/paraview { };