mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Merge pull request #1831 from YunoHost/handle-metronome-as-an-app
Handle metronome as an app
This commit is contained in:
commit
1e527a8214
28 changed files with 42 additions and 1382 deletions
|
@ -1,13 +1,9 @@
|
|||
{% set interfaces_list = interfaces.split(' ') %}
|
||||
{% for interface in interfaces_list %}
|
||||
interface-name={{ domain }},{{ interface }}
|
||||
interface-name=xmpp-upload.{{ domain }},{{ interface }}
|
||||
{% endfor %}
|
||||
{% if ipv6 %}
|
||||
host-record={{ domain }},{{ ipv6 }}
|
||||
host-record=xmpp-upload.{{ domain }},{{ ipv6 }}
|
||||
{% endif %}
|
||||
txt-record={{ domain }},"v=spf1 mx a -all"
|
||||
mx-host={{ domain }},{{ domain }},5
|
||||
srv-host=_xmpp-client._tcp.{{ domain }},{{ domain }},5222,0,5
|
||||
srv-host=_xmpp-server._tcp.{{ domain }},{{ domain }},5269,0,5
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
VirtualHost "{{ domain }}"
|
||||
enable = true
|
||||
ssl = {
|
||||
key = "/etc/yunohost/certs/{{ domain }}/key.pem";
|
||||
certificate = "/etc/yunohost/certs/{{ domain }}/crt.pem";
|
||||
}
|
||||
authentication = "ldap2"
|
||||
ldap = {
|
||||
hostname = "localhost",
|
||||
user = {
|
||||
basedn = "ou=users,dc=yunohost,dc=org",
|
||||
filter = "(&(objectClass=posixAccount)(mail=*@{{ domain }})(permission=cn=xmpp.main,ou=permission,dc=yunohost,dc=org))",
|
||||
usernamefield = "mail",
|
||||
namefield = "cn",
|
||||
},
|
||||
}
|
||||
|
||||
-- Discovery items
|
||||
disco_items = {
|
||||
{ "muc.{{ domain }}" },
|
||||
{ "pubsub.{{ domain }}" },
|
||||
{ "jabber.{{ domain }}" },
|
||||
{ "vjud.{{ domain }}" },
|
||||
{ "xmpp-upload.{{ domain }}" },
|
||||
};
|
||||
|
||||
-- contact_info = {
|
||||
-- abuse = { "mailto:abuse@{{ domain }}", "xmpp:admin@{{ domain }}" };
|
||||
-- admin = { "mailto:root@{{ domain }}", "xmpp:admin@{{ domain }}" };
|
||||
-- };
|
||||
|
||||
------ Components ------
|
||||
-- You can specify components to add hosts that provide special services,
|
||||
-- like multi-user conferences, and transports.
|
||||
|
||||
---Set up a MUC (multi-user chat) room server
|
||||
Component "muc.{{ domain }}" "muc"
|
||||
name = "{{ domain }} Chatrooms"
|
||||
|
||||
modules_enabled = {
|
||||
"muc_limits";
|
||||
"muc_log";
|
||||
"muc_log_mam";
|
||||
"muc_log_http";
|
||||
"muc_vcard";
|
||||
}
|
||||
|
||||
muc_event_rate = 0.5
|
||||
muc_burst_factor = 10
|
||||
room_default_config = {
|
||||
logging = true,
|
||||
persistent = true
|
||||
};
|
||||
|
||||
---Set up a PubSub server
|
||||
Component "pubsub.{{ domain }}" "pubsub"
|
||||
name = "{{ domain }} Publish/Subscribe"
|
||||
|
||||
unrestricted_node_creation = true -- Anyone can create a PubSub node (from any server)
|
||||
|
||||
---Set up a HTTP Upload service
|
||||
Component "xmpp-upload.{{ domain }}" "http_upload"
|
||||
name = "{{ domain }} Sharing Service"
|
||||
|
||||
http_file_path = "/var/xmpp-upload/{{ domain }}/upload"
|
||||
http_external_url = "https://xmpp-upload.{{ domain }}:443"
|
||||
http_file_base_path = "/upload"
|
||||
http_file_size_limit = 6*1024*1024
|
||||
http_file_quota = 60*1024*1024
|
||||
http_upload_file_size_limit = 100 * 1024 * 1024 -- bytes
|
||||
http_upload_quota = 10 * 1024 * 1024 * 1024 -- bytes
|
||||
|
||||
---Set up a VJUD service
|
||||
Component "vjud.{{ domain }}" "vjud"
|
||||
vjud_disco_name = "{{ domain }} User Directory"
|
|
@ -1,123 +0,0 @@
|
|||
-- ** Metronome's config file example **
|
||||
--
|
||||
-- The format is exactly equal to Prosody's:
|
||||
--
|
||||
-- Lists are written { "like", "this", "one" }
|
||||
-- Lists can also be of { 1, 2, 3 } numbers, etc.
|
||||
-- Either commas, or semi-colons; may be used as seperators.
|
||||
--
|
||||
-- A table is a list of values, except each value has a name. An
|
||||
-- example would be:
|
||||
--
|
||||
-- ssl = { key = "keyfile.key", certificate = "certificate.cert" }
|
||||
--
|
||||
-- Tip: You can check that the syntax of this file is correct when you have finished
|
||||
-- by running: luac -p metronome.cfg.lua
|
||||
-- If there are any errors, it will let you know what and where they are, otherwise it
|
||||
-- will keep quiet.
|
||||
|
||||
-- Global settings go in this section
|
||||
|
||||
-- This is the list of modules Metronome will load on startup.
|
||||
-- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too.
|
||||
|
||||
modules_enabled = {
|
||||
-- Generally required
|
||||
"roster"; -- Allow users to have a roster. Recommended.
|
||||
"saslauth"; -- Authentication for clients. Recommended if you want to log in.
|
||||
"tls"; -- Add support for secure TLS on c2s/s2s connections
|
||||
"disco"; -- Service discovery
|
||||
|
||||
-- Not essential, but recommended
|
||||
"private"; -- Private XML storage (for room bookmarks, etc.)
|
||||
"vcard"; -- Allow users to set vCards
|
||||
"pep"; -- Allows setting of mood, tune, etc.
|
||||
"pubsub"; -- Publish-subscribe XEP-0060
|
||||
"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
|
||||
"bidi"; -- Enables Bidirectional Server-to-Server Streams.
|
||||
|
||||
-- Nice to have
|
||||
"version"; -- Replies to server version requests
|
||||
"uptime"; -- Report how long server has been running
|
||||
"time"; -- Let others know the time here on this server
|
||||
"ping"; -- Replies to XMPP pings with pongs
|
||||
"register"; -- Allow users to register on this server using a client and change passwords
|
||||
"stream_management"; -- Allows clients and servers to use Stream Management
|
||||
"stanza_optimizations"; -- Allows clients to use Client State Indication and SIFT
|
||||
"message_carbons"; -- Allows clients to enable carbon copies of messages
|
||||
"mam"; -- Enable server-side message archives using Message Archive Management
|
||||
"push"; -- Enable Push Notifications via PubSub using XEP-0357
|
||||
"lastactivity"; -- Enables clients to know the last presence status of an user
|
||||
"adhoc_cm"; -- Allow to set client certificates to login through SASL External via adhoc
|
||||
"admin_adhoc"; -- administration adhoc commands
|
||||
"bookmarks"; -- XEP-0048 Bookmarks synchronization between PEP and Private Storage
|
||||
"sec_labels"; -- Allows to use a simplified version XEP-0258 Security Labels and related ACDFs.
|
||||
"privacy"; -- Add privacy lists and simple blocking command support
|
||||
|
||||
-- Other specific functionality
|
||||
--"admin_telnet"; -- administration console, telnet to port 5582
|
||||
--"admin_web"; -- administration web interface
|
||||
"bosh"; -- Enable support for BOSH clients, aka "XMPP over Bidirectional Streams over Synchronous HTTP"
|
||||
--"compression"; -- Allow clients to enable Stream Compression
|
||||
--"spim_block"; -- Require authorization via OOB form for messages from non-contacts and block unsollicited messages
|
||||
--"gate_guard"; -- Enable config-based blacklisting and hit-based auto-banning features
|
||||
--"incidents_handling"; -- Enable Incidents Handling support (can be administered via adhoc commands)
|
||||
--"server_presence"; -- Enables Server Buddies extension support
|
||||
--"service_directory"; -- Enables Service Directories extension support
|
||||
--"public_service"; -- Enables Server vCard support for public services in directories and advertises in features
|
||||
--"register_api"; -- Provides secure API for both Out-Of-Band and In-Band registration for E-Mail verification
|
||||
"websocket"; -- Enable support for WebSocket clients, aka "XMPP over WebSockets"
|
||||
};
|
||||
|
||||
-- Server PID
|
||||
pidfile = "/var/run/metronome/metronome.pid"
|
||||
|
||||
-- HTTP server
|
||||
http_ports = { 5290 }
|
||||
http_interfaces = { "127.0.0.1", "::1" }
|
||||
|
||||
--https_ports = { 5291 }
|
||||
--https_interfaces = { "127.0.0.1", "::1" }
|
||||
|
||||
-- Enable IPv6
|
||||
use_ipv6 = true
|
||||
|
||||
-- BOSH configuration (mod_bosh)
|
||||
consider_bosh_secure = true
|
||||
cross_domain_bosh = true
|
||||
|
||||
-- WebSocket configuration (mod_websocket)
|
||||
consider_websocket_secure = true
|
||||
cross_domain_websocket = true
|
||||
|
||||
-- Disable account creation by default, for security
|
||||
allow_registration = false
|
||||
|
||||
-- Use LDAP storage backend for all stores
|
||||
storage = "ldap"
|
||||
|
||||
-- stanza optimization
|
||||
csi_config_queue_all_muc_messages_but_mentions = false;
|
||||
|
||||
|
||||
-- Logging configuration
|
||||
log = {
|
||||
info = "/var/log/metronome/metronome.log"; -- Change 'info' to 'debug' for verbose logging
|
||||
error = "/var/log/metronome/metronome.err";
|
||||
-- "*syslog"; -- Uncomment this for logging to syslog
|
||||
-- "*console"; -- Log to the console, useful for debugging with daemonize=false
|
||||
}
|
||||
|
||||
------ Components ------
|
||||
-- You can specify components to add hosts that provide special services,
|
||||
-- like multi-user conferences, and transports.
|
||||
|
||||
---Set up a local BOSH service
|
||||
Component "localhost" "http"
|
||||
modules_enabled = { "bosh" }
|
||||
|
||||
----------- Virtual hosts -----------
|
||||
-- You need to add a VirtualHost entry for each domain you wish Metronome to serve.
|
||||
-- Settings under each VirtualHost entry apply *only* to that host.
|
||||
|
||||
Include "conf.d/*.cfg.lua"
|
|
@ -1,270 +0,0 @@
|
|||
-- vim:sts=4 sw=4
|
||||
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2010 Matthew Wild
|
||||
-- Copyright (C) 2008-2010 Waqas Hussain
|
||||
-- Copyright (C) 2012 Rob Hoelz
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
|
||||
local ldap;
|
||||
local connection;
|
||||
local params = module:get_option("ldap");
|
||||
local format = string.format;
|
||||
local tconcat = table.concat;
|
||||
|
||||
local _M = {};
|
||||
|
||||
local config_params = {
|
||||
hostname = 'string',
|
||||
user = {
|
||||
basedn = 'string',
|
||||
namefield = 'string',
|
||||
filter = 'string',
|
||||
usernamefield = 'string',
|
||||
},
|
||||
groups = {
|
||||
basedn = 'string',
|
||||
namefield = 'string',
|
||||
memberfield = 'string',
|
||||
|
||||
_member = {
|
||||
name = 'string',
|
||||
admin = 'boolean?',
|
||||
},
|
||||
},
|
||||
admin = {
|
||||
_optional = true,
|
||||
basedn = 'string',
|
||||
namefield = 'string',
|
||||
filter = 'string',
|
||||
}
|
||||
}
|
||||
|
||||
local function run_validation(params, config, prefix)
|
||||
prefix = prefix or '';
|
||||
|
||||
-- verify that every required member of config is present in params
|
||||
for k, v in pairs(config) do
|
||||
if type(k) == 'string' and k:sub(1, 1) ~= '_' then
|
||||
local is_optional;
|
||||
if type(v) == 'table' then
|
||||
is_optional = v._optional;
|
||||
else
|
||||
is_optional = v:sub(-1) == '?';
|
||||
end
|
||||
|
||||
if not is_optional and params[k] == nil then
|
||||
return nil, prefix .. k .. ' is required';
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for k, v in pairs(params) do
|
||||
local expected_type = config[k];
|
||||
|
||||
local ok, err = true;
|
||||
|
||||
if type(k) == 'string' then
|
||||
-- verify that this key is present in config
|
||||
if k:sub(1, 1) == '_' or expected_type == nil then
|
||||
return nil, 'invalid parameter ' .. prefix .. k;
|
||||
end
|
||||
|
||||
-- type validation
|
||||
if type(expected_type) == 'string' then
|
||||
if expected_type:sub(-1) == '?' then
|
||||
expected_type = expected_type:sub(1, -2);
|
||||
end
|
||||
|
||||
if type(v) ~= expected_type then
|
||||
return nil, 'invalid type for parameter ' .. prefix .. k;
|
||||
end
|
||||
else -- it's a table (or had better be)
|
||||
if type(v) ~= 'table' then
|
||||
return nil, 'invalid type for parameter ' .. prefix .. k;
|
||||
end
|
||||
|
||||
-- recurse into child
|
||||
ok, err = run_validation(v, expected_type, prefix .. k .. '.');
|
||||
end
|
||||
else -- it's an integer (or had better be)
|
||||
if not config._member then
|
||||
return nil, 'invalid parameter ' .. prefix .. tostring(k);
|
||||
end
|
||||
ok, err = run_validation(v, config._member, prefix .. tostring(k) .. '.');
|
||||
end
|
||||
|
||||
if not ok then
|
||||
return ok, err;
|
||||
end
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
local function validate_config()
|
||||
if true then
|
||||
return true; -- XXX for now
|
||||
end
|
||||
|
||||
-- this is almost too clever (I mean that in a bad
|
||||
-- maintainability sort of way)
|
||||
--
|
||||
-- basically this allows a free pass for a key in group members
|
||||
-- equal to params.groups.namefield
|
||||
setmetatable(config_params.groups._member, {
|
||||
__index = function(_, k)
|
||||
if k == params.groups.namefield then
|
||||
return 'string';
|
||||
end
|
||||
end
|
||||
});
|
||||
|
||||
local ok, err = run_validation(params, config_params);
|
||||
|
||||
setmetatable(config_params.groups._member, nil);
|
||||
|
||||
if ok then
|
||||
-- a little extra validation that doesn't fit into
|
||||
-- my recursive checker
|
||||
local group_namefield = params.groups.namefield;
|
||||
for i, group in ipairs(params.groups) do
|
||||
if not group[group_namefield] then
|
||||
return nil, format('groups.%d.%s is required', i, group_namefield);
|
||||
end
|
||||
end
|
||||
|
||||
-- fill in params.admin if you can
|
||||
if not params.admin and params.groups then
|
||||
local admingroup;
|
||||
|
||||
for _, groupconfig in ipairs(params.groups) do
|
||||
if groupconfig.admin then
|
||||
admingroup = groupconfig;
|
||||
break;
|
||||
end
|
||||
end
|
||||
|
||||
if admingroup then
|
||||
params.admin = {
|
||||
basedn = params.groups.basedn,
|
||||
namefield = params.groups.memberfield,
|
||||
filter = group_namefield .. '=' .. admingroup[group_namefield],
|
||||
};
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return ok, err;
|
||||
end
|
||||
|
||||
-- what to do if connection isn't available?
|
||||
local function connect()
|
||||
return ldap.open_simple(params.hostname, params.bind_dn, params.bind_password, params.use_tls);
|
||||
end
|
||||
|
||||
-- this is abstracted so we can maintain persistent connections at a later time
|
||||
function _M.getconnection()
|
||||
return connect();
|
||||
end
|
||||
|
||||
function _M.getparams()
|
||||
return params;
|
||||
end
|
||||
|
||||
-- XXX consider renaming this...it doesn't bind the current connection
|
||||
function _M.bind(username, password)
|
||||
local conn = _M.getconnection();
|
||||
local filter = format('%s=%s', params.user.usernamefield, username);
|
||||
if params.user.usernamefield == 'mail' then
|
||||
filter = format('mail=%s@*', username);
|
||||
end
|
||||
|
||||
if filter then
|
||||
filter = _M.filter.combine_and(filter, params.user.filter);
|
||||
end
|
||||
|
||||
local who = _M.singlematch {
|
||||
attrs = params.user.usernamefield,
|
||||
base = params.user.basedn,
|
||||
filter = filter,
|
||||
};
|
||||
|
||||
if who then
|
||||
who = who.dn;
|
||||
module:log('debug', '_M.bind - who: %s', who);
|
||||
else
|
||||
module:log('debug', '_M.bind - no DN found for username = %s', username);
|
||||
return nil, format('no DN found for username = %s', username);
|
||||
end
|
||||
|
||||
local conn, err = ldap.open_simple(params.hostname, who, password, params.use_tls);
|
||||
|
||||
if conn then
|
||||
conn:close();
|
||||
return true;
|
||||
end
|
||||
|
||||
return conn, err;
|
||||
end
|
||||
|
||||
function _M.singlematch(query)
|
||||
local ld = _M.getconnection();
|
||||
|
||||
query.sizelimit = 1;
|
||||
query.scope = 'subtree';
|
||||
|
||||
for dn, attribs in ld:search(query) do
|
||||
attribs.dn = dn;
|
||||
return attribs;
|
||||
end
|
||||
end
|
||||
|
||||
_M.filter = {};
|
||||
|
||||
function _M.filter.combine_and(...)
|
||||
local parts = { '(&' };
|
||||
|
||||
local arg = { ... };
|
||||
|
||||
for _, filter in ipairs(arg) do
|
||||
if filter:sub(1, 1) ~= '(' and filter:sub(-1) ~= ')' then
|
||||
filter = '(' .. filter .. ')'
|
||||
end
|
||||
parts[#parts + 1] = filter;
|
||||
end
|
||||
|
||||
parts[#parts + 1] = ')';
|
||||
|
||||
return tconcat(parts, '');
|
||||
end
|
||||
|
||||
do
|
||||
local ok, err;
|
||||
|
||||
metronome.unlock_globals();
|
||||
ok, ldap = pcall(require, 'lualdap');
|
||||
metronome.lock_globals();
|
||||
if not ok then
|
||||
module:log("error", "Failed to load the LuaLDAP library for accessing LDAP: %s", ldap);
|
||||
module:log("error", "More information on install LuaLDAP can be found at http://www.keplerproject.org/lualdap");
|
||||
return;
|
||||
end
|
||||
|
||||
if not params then
|
||||
module:log("error", "LDAP configuration required to use the LDAP storage module");
|
||||
return;
|
||||
end
|
||||
|
||||
ok, err = validate_config();
|
||||
|
||||
if not ok then
|
||||
module:log("error", "LDAP configuration is invalid: %s", tostring(err));
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
return _M;
|
|
@ -1,90 +0,0 @@
|
|||
-- vim:sts=4 sw=4
|
||||
|
||||
-- Metronome IM
|
||||
-- Copyright (C) 2008-2010 Matthew Wild
|
||||
-- Copyright (C) 2008-2010 Waqas Hussain
|
||||
-- Copyright (C) 2012 Rob Hoelz
|
||||
-- Copyright (C) 2015 YUNOHOST.ORG
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
-- https://github.com/YunoHost/yunohost-config-metronome/blob/unstable/lib/modules/mod_auth_ldap2.lua
|
||||
-- adapted to use common LDAP store on Metronome
|
||||
|
||||
local ldap = module:require 'ldap';
|
||||
local new_sasl = require 'util.sasl'.new;
|
||||
local jsplit = require 'util.jid'.split;
|
||||
|
||||
local log = module._log
|
||||
|
||||
if not ldap then
|
||||
return;
|
||||
end
|
||||
|
||||
function new_default_provider(host)
|
||||
local provider = { name = "ldap2" };
|
||||
log("debug", "initializing ldap2 authentication provider for host '%s'", host);
|
||||
|
||||
function provider.test_password(username, password)
|
||||
return ldap.bind(username, password);
|
||||
end
|
||||
|
||||
function provider.user_exists(username)
|
||||
local params = ldap.getparams()
|
||||
|
||||
local filter = ldap.filter.combine_and(params.user.filter, params.user.usernamefield .. '=' .. username);
|
||||
if params.user.usernamefield == 'mail' then
|
||||
filter = ldap.filter.combine_and(params.user.filter, 'mail=' .. username .. '@*');
|
||||
end
|
||||
|
||||
return ldap.singlematch {
|
||||
base = params.user.basedn,
|
||||
filter = filter,
|
||||
};
|
||||
end
|
||||
|
||||
function provider.get_password(username)
|
||||
return nil, "Passwords unavailable for LDAP.";
|
||||
end
|
||||
|
||||
function provider.set_password(username, password)
|
||||
return nil, "Passwords unavailable for LDAP.";
|
||||
end
|
||||
|
||||
function provider.create_user(username, password)
|
||||
return nil, "Account creation/modification not available with LDAP.";
|
||||
end
|
||||
|
||||
function provider.get_sasl_handler(session)
|
||||
local testpass_authentication_profile = {
|
||||
session = session,
|
||||
plain_test = function(sasl, username, password, realm)
|
||||
return provider.test_password(username, password), true;
|
||||
end,
|
||||
order = { "plain_test" },
|
||||
};
|
||||
return new_sasl(module.host, testpass_authentication_profile);
|
||||
end
|
||||
|
||||
function provider.is_admin(jid)
|
||||
local admin_config = ldap.getparams().admin;
|
||||
|
||||
if not admin_config then
|
||||
return;
|
||||
end
|
||||
|
||||
local ld = ldap:getconnection();
|
||||
local username = jsplit(jid);
|
||||
local filter = ldap.filter.combine_and(admin_config.filter, admin_config.namefield .. '=' .. username);
|
||||
|
||||
return ldap.singlematch {
|
||||
base = admin_config.basedn,
|
||||
filter = filter,
|
||||
};
|
||||
end
|
||||
|
||||
return provider;
|
||||
end
|
||||
|
||||
module:add_item("auth-provider", new_default_provider(module.host));
|
|
@ -1,87 +0,0 @@
|
|||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2010 Matthew Wild
|
||||
-- Copyright (C) 2008-2010 Waqas Hussain
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
|
||||
|
||||
|
||||
local st = require "util.stanza";
|
||||
local t_concat = table.concat;
|
||||
|
||||
local secure_auth_only = module:get_option("c2s_require_encryption")
|
||||
or module:get_option("require_encryption")
|
||||
or not(module:get_option("allow_unencrypted_plain_auth"));
|
||||
|
||||
local sessionmanager = require "core.sessionmanager";
|
||||
local usermanager = require "core.usermanager";
|
||||
local nodeprep = require "util.encodings".stringprep.nodeprep;
|
||||
local resourceprep = require "util.encodings".stringprep.resourceprep;
|
||||
|
||||
module:add_feature("jabber:iq:auth");
|
||||
module:hook("stream-features", function(event)
|
||||
local origin, features = event.origin, event.features;
|
||||
if secure_auth_only and not origin.secure then
|
||||
-- Sorry, not offering to insecure streams!
|
||||
return;
|
||||
elseif not origin.username then
|
||||
features:tag("auth", {xmlns='http://jabber.org/features/iq-auth'}):up();
|
||||
end
|
||||
end);
|
||||
|
||||
module:hook("stanza/iq/jabber:iq:auth:query", function(event)
|
||||
local session, stanza = event.origin, event.stanza;
|
||||
|
||||
if session.type ~= "c2s_unauthed" then
|
||||
(session.sends2s or session.send)(st.error_reply(stanza, "cancel", "service-unavailable", "Legacy authentication is only allowed for unauthenticated client connections."));
|
||||
return true;
|
||||
end
|
||||
|
||||
if secure_auth_only and not session.secure then
|
||||
session.send(st.error_reply(stanza, "modify", "not-acceptable", "Encryption (SSL or TLS) is required to connect to this server"));
|
||||
return true;
|
||||
end
|
||||
|
||||
local username = stanza.tags[1]:child_with_name("username");
|
||||
local password = stanza.tags[1]:child_with_name("password");
|
||||
local resource = stanza.tags[1]:child_with_name("resource");
|
||||
if not (username and password and resource) then
|
||||
local reply = st.reply(stanza);
|
||||
session.send(reply:query("jabber:iq:auth")
|
||||
:tag("username"):up()
|
||||
:tag("password"):up()
|
||||
:tag("resource"):up());
|
||||
else
|
||||
username, password, resource = t_concat(username), t_concat(password), t_concat(resource);
|
||||
username = nodeprep(username);
|
||||
resource = resourceprep(resource)
|
||||
if not (username and resource) then
|
||||
session.send(st.error_reply(stanza, "modify", "bad-request"));
|
||||
return true;
|
||||
end
|
||||
if usermanager.test_password(username, session.host, password) then
|
||||
-- Authentication successful!
|
||||
local success, err = sessionmanager.make_authenticated(session, username);
|
||||
if success then
|
||||
local err_type, err_msg;
|
||||
success, err_type, err, err_msg = sessionmanager.bind_resource(session, resource);
|
||||
if not success then
|
||||
session.send(st.error_reply(stanza, err_type, err, err_msg));
|
||||
session.username, session.type = nil, "c2s_unauthed"; -- FIXME should this be placed in sessionmanager?
|
||||
return true;
|
||||
elseif resource ~= session.resource then -- server changed resource, not supported by legacy auth
|
||||
session.send(st.error_reply(stanza, "cancel", "conflict", "The requested resource could not be assigned to this session."));
|
||||
session:close(); -- FIXME undo resource bind and auth instead of closing the session?
|
||||
return true;
|
||||
end
|
||||
end
|
||||
session.send(st.reply(stanza));
|
||||
else
|
||||
session.send(st.error_reply(stanza, "auth", "not-authorized"));
|
||||
end
|
||||
end
|
||||
return true;
|
||||
end);
|
||||
|
|
@ -1,243 +0,0 @@
|
|||
-- vim:sts=4 sw=4
|
||||
|
||||
-- Metronome IM
|
||||
-- Copyright (C) 2008-2010 Matthew Wild
|
||||
-- Copyright (C) 2008-2010 Waqas Hussain
|
||||
-- Copyright (C) 2012 Rob Hoelz
|
||||
-- Copyright (C) 2015 YUNOHOST.ORG
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
|
||||
----------------------------------------
|
||||
-- Constants and such --
|
||||
----------------------------------------
|
||||
|
||||
local setmetatable = setmetatable;
|
||||
|
||||
local get_config = require "core.configmanager".get;
|
||||
local ldap = module:require 'ldap';
|
||||
local vcardlib = module:require 'vcard';
|
||||
local st = require 'util.stanza';
|
||||
local gettime = require 'socket'.gettime;
|
||||
|
||||
local log = module._log
|
||||
|
||||
if not ldap then
|
||||
return;
|
||||
end
|
||||
|
||||
local CACHE_EXPIRY = 300;
|
||||
|
||||
----------------------------------------
|
||||
-- Utility Functions --
|
||||
----------------------------------------
|
||||
|
||||
local function ldap_record_to_vcard(record, format)
|
||||
return vcardlib.create {
|
||||
record = record,
|
||||
format = format,
|
||||
}
|
||||
end
|
||||
|
||||
local get_alias_for_user;
|
||||
|
||||
do
|
||||
local user_cache;
|
||||
local last_fetch_time;
|
||||
|
||||
local function populate_user_cache()
|
||||
local user_c = get_config(module.host, 'ldap').user;
|
||||
if not user_c then return; end
|
||||
|
||||
local ld = ldap.getconnection();
|
||||
|
||||
local usernamefield = user_c.usernamefield;
|
||||
local namefield = user_c.namefield;
|
||||
|
||||
user_cache = {};
|
||||
|
||||
for _, attrs in ld:search { base = user_c.basedn, scope = 'onelevel', filter = user_c.filter } do
|
||||
user_cache[attrs[usernamefield]] = attrs[namefield];
|
||||
end
|
||||
last_fetch_time = gettime();
|
||||
end
|
||||
|
||||
function get_alias_for_user(user)
|
||||
if last_fetch_time and last_fetch_time + CACHE_EXPIRY < gettime() then
|
||||
user_cache = nil;
|
||||
end
|
||||
if not user_cache then
|
||||
populate_user_cache();
|
||||
end
|
||||
return user_cache[user];
|
||||
end
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
-- Base LDAP store class --
|
||||
----------------------------------------
|
||||
|
||||
local function ldap_store(config)
|
||||
local self = {};
|
||||
local config = config;
|
||||
|
||||
function self:get(username)
|
||||
return nil, "Data getting is not available for this storage backend";
|
||||
end
|
||||
|
||||
function self:set(username, data)
|
||||
return nil, "Data setting is not available for this storage backend";
|
||||
end
|
||||
|
||||
return self;
|
||||
end
|
||||
|
||||
local adapters = {};
|
||||
|
||||
----------------------------------------
|
||||
-- Roster Storage Implementation --
|
||||
----------------------------------------
|
||||
|
||||
adapters.roster = function (config)
|
||||
-- Validate configuration requirements
|
||||
if not config.groups then return nil; end
|
||||
|
||||
local self = ldap_store(config)
|
||||
|
||||
function self:get(username)
|
||||
local ld = ldap.getconnection();
|
||||
local contacts = {};
|
||||
|
||||
local memberfield = config.groups.memberfield;
|
||||
local namefield = config.groups.namefield;
|
||||
local filter = memberfield .. '=' .. tostring(username);
|
||||
|
||||
local groups = {};
|
||||
for _, config in ipairs(config.groups) do
|
||||
groups[ config[namefield] ] = config.name;
|
||||
end
|
||||
|
||||
log("debug", "Found %d group(s) for user %s", select('#', groups), username)
|
||||
|
||||
-- XXX this kind of relies on the way we do groups at INOC
|
||||
for _, attrs in ld:search { base = config.groups.basedn, scope = 'onelevel', filter = filter } do
|
||||
if groups[ attrs[namefield] ] then
|
||||
local members = attrs[memberfield];
|
||||
|
||||
for _, user in ipairs(members) do
|
||||
if user ~= username then
|
||||
local jid = user .. '@' .. module.host;
|
||||
local record = contacts[jid];
|
||||
|
||||
if not record then
|
||||
record = {
|
||||
subscription = 'both',
|
||||
groups = {},
|
||||
name = get_alias_for_user(user),
|
||||
};
|
||||
contacts[jid] = record;
|
||||
end
|
||||
|
||||
record.groups[ groups[ attrs[namefield] ] ] = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return contacts;
|
||||
end
|
||||
|
||||
function self:set(username, data)
|
||||
log("warn", "Setting data in Roster LDAP storage is not supported yet")
|
||||
return nil, "not supported";
|
||||
end
|
||||
|
||||
return self;
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
-- vCard Storage Implementation --
|
||||
----------------------------------------
|
||||
|
||||
adapters.vcard = function (config)
|
||||
-- Validate configuration requirements
|
||||
if not config.vcard_format or not config.user then return nil; end
|
||||
|
||||
local self = ldap_store(config)
|
||||
|
||||
function self:get(username)
|
||||
local ld = ldap.getconnection();
|
||||
local filter = config.user.usernamefield .. '=' .. tostring(username);
|
||||
|
||||
log("debug", "Retrieving vCard for user '%s'", username);
|
||||
|
||||
local match = ldap.singlematch {
|
||||
base = config.user.basedn,
|
||||
filter = filter,
|
||||
};
|
||||
if match then
|
||||
match.jid = username .. '@' .. module.host
|
||||
return st.preserialize(ldap_record_to_vcard(match, config.vcard_format));
|
||||
else
|
||||
return nil, "username not found";
|
||||
end
|
||||
end
|
||||
|
||||
function self:set(username, data)
|
||||
log("warn", "Setting data in vCard LDAP storage is not supported yet")
|
||||
return nil, "not supported";
|
||||
end
|
||||
|
||||
return self;
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
-- Driver Definition --
|
||||
----------------------------------------
|
||||
|
||||
cache = {};
|
||||
|
||||
local driver = { name = "ldap" };
|
||||
|
||||
function driver:open(store)
|
||||
log("debug", "Opening ldap storage backend for host '%s' and store '%s'", module.host, store);
|
||||
|
||||
if not cache[module.host] then
|
||||
log("debug", "Caching adapters for the host '%s'", module.host);
|
||||
|
||||
local ad_config = get_config(module.host, "ldap");
|
||||
local ad_cache = {};
|
||||
for k, v in pairs(adapters) do
|
||||
ad_cache[k] = v(ad_config);
|
||||
end
|
||||
|
||||
cache[module.host] = ad_cache;
|
||||
end
|
||||
|
||||
local adapter = cache[module.host][store];
|
||||
|
||||
if not adapter then
|
||||
log("info", "Unavailable adapter for store '%s'", store);
|
||||
return nil, "unsupported-store";
|
||||
end
|
||||
return adapter;
|
||||
end
|
||||
|
||||
function driver:stores(username, type, pattern)
|
||||
return nil, "not implemented";
|
||||
end
|
||||
|
||||
function driver:store_exists(username, type)
|
||||
return nil, "not implemented";
|
||||
end
|
||||
|
||||
function driver:purge(username)
|
||||
return nil, "not implemented";
|
||||
end
|
||||
|
||||
function driver:nodes(type)
|
||||
return nil, "not implemented";
|
||||
end
|
||||
|
||||
module:add_item("data-driver", driver);
|
|
@ -1,162 +0,0 @@
|
|||
-- vim:sts=4 sw=4
|
||||
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2010 Matthew Wild
|
||||
-- Copyright (C) 2008-2010 Waqas Hussain
|
||||
-- Copyright (C) 2012 Rob Hoelz
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
|
||||
local st = require 'util.stanza';
|
||||
|
||||
local VCARD_NS = 'vcard-temp';
|
||||
|
||||
local builder_methods = {};
|
||||
|
||||
local base64_encode = require('util.encodings').base64.encode;
|
||||
|
||||
function builder_methods:addvalue(key, value)
|
||||
self.vcard:tag(key):text(value):up();
|
||||
end
|
||||
|
||||
function builder_methods:addphotofield(tagname, format_section)
|
||||
local record = self.record;
|
||||
local format = self.format;
|
||||
local vcard = self.vcard;
|
||||
local config = format[format_section];
|
||||
|
||||
if not config then
|
||||
return;
|
||||
end
|
||||
|
||||
if config.extval then
|
||||
if record[config.extval] then
|
||||
local tag = vcard:tag(tagname);
|
||||
tag:tag('EXTVAL'):text(record[config.extval]):up();
|
||||
end
|
||||
elseif config.type and config.binval then
|
||||
if record[config.binval] then
|
||||
local tag = vcard:tag(tagname);
|
||||
tag:tag('TYPE'):text(config.type):up();
|
||||
tag:tag('BINVAL'):text(base64_encode(record[config.binval])):up();
|
||||
end
|
||||
else
|
||||
module:log('error', 'You have an invalid %s config section', tagname);
|
||||
return;
|
||||
end
|
||||
|
||||
vcard:up();
|
||||
end
|
||||
|
||||
function builder_methods:addregularfield(tagname, format_section)
|
||||
local record = self.record;
|
||||
local format = self.format;
|
||||
local vcard = self.vcard;
|
||||
|
||||
if not format[format_section] then
|
||||
return;
|
||||
end
|
||||
|
||||
local tag = vcard:tag(tagname);
|
||||
|
||||
for k, v in pairs(format[format_section]) do
|
||||
tag:tag(string.upper(k)):text(record[v]):up();
|
||||
end
|
||||
|
||||
vcard:up();
|
||||
end
|
||||
|
||||
function builder_methods:addmultisectionedfield(tagname, format_section)
|
||||
local record = self.record;
|
||||
local format = self.format;
|
||||
local vcard = self.vcard;
|
||||
|
||||
if not format[format_section] then
|
||||
return;
|
||||
end
|
||||
|
||||
for k, v in pairs(format[format_section]) do
|
||||
local tag = vcard:tag(tagname);
|
||||
|
||||
if type(k) == 'string' then
|
||||
tag:tag(string.upper(k)):up();
|
||||
end
|
||||
|
||||
for k2, v2 in pairs(v) do
|
||||
if type(v2) == 'boolean' then
|
||||
tag:tag(string.upper(k2)):up();
|
||||
else
|
||||
tag:tag(string.upper(k2)):text(record[v2]):up();
|
||||
end
|
||||
end
|
||||
|
||||
vcard:up();
|
||||
end
|
||||
end
|
||||
|
||||
function builder_methods:build()
|
||||
local record = self.record;
|
||||
local format = self.format;
|
||||
|
||||
self:addvalue( 'VERSION', '2.0');
|
||||
self:addvalue( 'FN', record[format.displayname]);
|
||||
self:addregularfield( 'N', 'name');
|
||||
self:addvalue( 'NICKNAME', record[format.nickname]);
|
||||
self:addphotofield( 'PHOTO', 'photo');
|
||||
self:addvalue( 'BDAY', record[format.birthday]);
|
||||
self:addmultisectionedfield('ADR', 'address');
|
||||
self:addvalue( 'LABEL', nil); -- we don't support LABEL...yet.
|
||||
self:addmultisectionedfield('TEL', 'telephone');
|
||||
self:addmultisectionedfield('EMAIL', 'email');
|
||||
self:addvalue( 'JABBERID', record.jid);
|
||||
self:addvalue( 'MAILER', record[format.mailer]);
|
||||
self:addvalue( 'TZ', record[format.timezone]);
|
||||
self:addregularfield( 'GEO', 'geo');
|
||||
self:addvalue( 'TITLE', record[format.title]);
|
||||
self:addvalue( 'ROLE', record[format.role]);
|
||||
self:addphotofield( 'LOGO', 'logo');
|
||||
self:addvalue( 'AGENT', nil); -- we don't support AGENT...yet.
|
||||
self:addregularfield( 'ORG', 'org');
|
||||
self:addvalue( 'CATEGORIES', nil); -- we don't support CATEGORIES...yet.
|
||||
self:addvalue( 'NOTE', record[format.note]);
|
||||
self:addvalue( 'PRODID', nil); -- we don't support PRODID...yet.
|
||||
self:addvalue( 'REV', record[format.rev]);
|
||||
self:addvalue( 'SORT-STRING', record[format.sortstring]);
|
||||
self:addregularfield( 'SOUND', 'sound');
|
||||
self:addvalue( 'UID', record[format.uid]);
|
||||
self:addvalue( 'URL', record[format.url]);
|
||||
self:addvalue( 'CLASS', nil); -- we don't support CLASS...yet.
|
||||
self:addregularfield( 'KEY', 'key');
|
||||
self:addvalue( 'DESC', record[format.description]);
|
||||
|
||||
return self.vcard;
|
||||
end
|
||||
|
||||
local function new_builder(params)
|
||||
local vcard_tag = st.stanza('vCard', { xmlns = VCARD_NS });
|
||||
|
||||
local object = {
|
||||
vcard = vcard_tag,
|
||||
__index = builder_methods,
|
||||
};
|
||||
|
||||
for k, v in pairs(params) do
|
||||
object[k] = v;
|
||||
end
|
||||
|
||||
setmetatable(object, object);
|
||||
|
||||
return object;
|
||||
end
|
||||
|
||||
local _M = {};
|
||||
|
||||
function _M.create(params)
|
||||
local builder = new_builder(params);
|
||||
|
||||
return builder:build();
|
||||
end
|
||||
|
||||
return _M;
|
|
@ -6,7 +6,7 @@ map $http_upgrade $connection_upgrade {
|
|||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name {{ domain }}{% if xmpp_enabled == "True" %} xmpp-upload.{{ domain }} muc.{{ domain }}{% endif %};
|
||||
server_name {{ domain }};
|
||||
|
||||
access_by_lua_file /usr/share/ssowat/access.lua;
|
||||
|
||||
|
@ -78,48 +78,3 @@ server {
|
|||
access_log /var/log/nginx/{{ domain }}-access.log;
|
||||
error_log /var/log/nginx/{{ domain }}-error.log;
|
||||
}
|
||||
|
||||
{% if xmpp_enabled == "True" %}
|
||||
# vhost dedicated to XMPP http_upload
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name xmpp-upload.{{ domain }};
|
||||
root /dev/null;
|
||||
|
||||
location /upload/ {
|
||||
alias /var/xmpp-upload/{{ domain }}/upload/;
|
||||
# Pass all requests to metronome, except for GET and HEAD requests.
|
||||
limit_except GET HEAD {
|
||||
proxy_pass http://localhost:5290;
|
||||
}
|
||||
|
||||
include proxy_params;
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'HEAD, GET, PUT, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'Authorization';
|
||||
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||
client_max_body_size 105M; # Choose a value a bit higher than the max upload configured in XMPP server
|
||||
}
|
||||
|
||||
include /etc/nginx/conf.d/security.conf.inc;
|
||||
|
||||
ssl_certificate /etc/yunohost/certs/{{ domain }}/crt.pem;
|
||||
ssl_certificate_key /etc/yunohost/certs/{{ domain }}/key.pem;
|
||||
|
||||
{% if domain_cert_ca != "selfsigned" %}
|
||||
more_set_headers "Strict-Transport-Security : max-age=63072000; includeSubDomains; preload";
|
||||
{% endif %}
|
||||
{% if domain_cert_ca == "letsencrypt" %}
|
||||
# OCSP settings
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
ssl_trusted_certificate /etc/yunohost/certs/{{ domain }}/crt.pem;
|
||||
resolver 1.1.1.1 9.9.9.9 valid=300s;
|
||||
resolver_timeout 5s;
|
||||
{% endif %}
|
||||
|
||||
access_log /var/log/nginx/xmpp-upload.{{ domain }}-access.log;
|
||||
error_log /var/log/nginx/xmpp-upload.{{ domain }}-error.log;
|
||||
}
|
||||
{% endif %}
|
||||
|
|
|
@ -56,7 +56,6 @@ objectClass: groupOfNamesYnh
|
|||
gidNumber: 4002
|
||||
cn: all_users
|
||||
permission: cn=mail.main,ou=permission,dc=yunohost,dc=org
|
||||
permission: cn=xmpp.main,ou=permission,dc=yunohost,dc=org
|
||||
|
||||
dn: cn=visitors,ou=groups,dc=yunohost,dc=org
|
||||
objectClass: posixGroup
|
||||
|
@ -75,17 +74,6 @@ gidNumber: 5001
|
|||
showTile: FALSE
|
||||
authHeader: FALSE
|
||||
|
||||
dn: cn=xmpp.main,ou=permission,dc=yunohost,dc=org
|
||||
groupPermission: cn=all_users,ou=groups,dc=yunohost,dc=org
|
||||
cn: xmpp.main
|
||||
objectClass: posixGroup
|
||||
objectClass: permissionYnh
|
||||
isProtected: TRUE
|
||||
label: XMPP
|
||||
gidNumber: 5002
|
||||
showTile: FALSE
|
||||
authHeader: FALSE
|
||||
|
||||
dn: cn=ssh.main,ou=permission,dc=yunohost,dc=org
|
||||
cn: ssh.main
|
||||
objectClass: posixGroup
|
||||
|
|
|
@ -192,7 +192,7 @@ authorityKeyIdentifier=keyid,issuer
|
|||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
|
||||
subjectAltName=DNS:yunohost.org,DNS:www.yunohost.org,DNS:ns.yunohost.org,DNS:xmpp-upload.yunohost.org
|
||||
subjectAltName=DNS:yunohost.org,DNS:www.yunohost.org,DNS:ns.yunohost.org
|
||||
|
||||
[ v3_ca ]
|
||||
|
||||
|
|
|
@ -8,11 +8,6 @@ fail2ban:
|
|||
log: /var/log/fail2ban.log
|
||||
category: security
|
||||
test_conf: fail2ban-server --test
|
||||
metronome:
|
||||
log: [/var/log/metronome/metronome.log,/var/log/metronome/metronome.err]
|
||||
needs_exposed_ports: [5222, 5269]
|
||||
category: xmpp
|
||||
ignore_if_package_is_not_installed: metronome
|
||||
mysql:
|
||||
log: [/var/log/mysql.log,/var/log/mysql.err,/var/log/mysql/error.log]
|
||||
actual_systemd_service: mariadb
|
||||
|
|
1
debian/install
vendored
1
debian/install
vendored
|
@ -6,5 +6,4 @@ conf/* /usr/share/yunohost/conf/
|
|||
locales/* /usr/share/yunohost/locales/
|
||||
doc/yunohost.8.gz /usr/share/man/man8/
|
||||
doc/bash_completion.d/* /etc/bash_completion.d/
|
||||
conf/metronome/modules/* /usr/lib/metronome/modules/
|
||||
src/* /usr/lib/python3/dist-packages/yunohost/
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Exit hook on subcommand error or unset variable
|
||||
set -eu
|
||||
|
||||
# Source YNH helpers
|
||||
source /usr/share/yunohost/helpers
|
||||
|
||||
# Backup destination
|
||||
backup_dir="${1}/data/xmpp"
|
||||
|
||||
[[ ! -d /var/lib/metronome ]] || ynh_backup /var/lib/metronome "${backup_dir}/var_lib_metronome" --not_mandatory
|
||||
[[ ! -d /var/xmpp-upload ]] || ynh_backup /var/xmpp-upload/ "${backup_dir}/var_xmpp-upload" --not_mandatory
|
|
@ -1,95 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if ! dpkg --list | grep -q 'ii *metronome '
|
||||
then
|
||||
echo 'metronome is not installed, skipping'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
do_pre_regen() {
|
||||
pending_dir=$1
|
||||
|
||||
cd /usr/share/yunohost/conf/metronome
|
||||
|
||||
# create directories for pending conf
|
||||
metronome_dir="${pending_dir}/etc/metronome"
|
||||
metronome_conf_dir="${metronome_dir}/conf.d"
|
||||
mkdir -p "$metronome_conf_dir"
|
||||
|
||||
# retrieve variables
|
||||
main_domain=$(cat /etc/yunohost/current_host)
|
||||
|
||||
# install main conf file
|
||||
cat metronome.cfg.lua \
|
||||
| sed "s/{{ main_domain }}/${main_domain}/g" \
|
||||
>"${metronome_dir}/metronome.cfg.lua"
|
||||
|
||||
# Trick such that old conf files are flagged as to remove
|
||||
for domain in $YNH_DOMAINS; do
|
||||
touch "${metronome_conf_dir}/${domain}.cfg.lua"
|
||||
done
|
||||
|
||||
# add domain conf files
|
||||
domain_list="$(yunohost domain list --features xmpp --output-as json | jq -r ".domains[]")"
|
||||
for domain in $domain_list; do
|
||||
cat domain.tpl.cfg.lua \
|
||||
| sed "s/{{ domain }}/${domain}/g" \
|
||||
>"${metronome_conf_dir}/${domain}.cfg.lua"
|
||||
done
|
||||
|
||||
# remove old domain conf files
|
||||
conf_files=$(ls -1 /etc/metronome/conf.d \
|
||||
| awk '/^[^\.]+\.[^\.]+.*\.cfg\.lua$/ { print $1 }')
|
||||
for file in $conf_files; do
|
||||
domain=${file%.cfg.lua}
|
||||
[[ $YNH_DOMAINS =~ $domain ]] \
|
||||
|| touch "${metronome_conf_dir}/${file}"
|
||||
done
|
||||
}
|
||||
|
||||
do_post_regen() {
|
||||
regen_conf_files=$1
|
||||
|
||||
# retrieve variables
|
||||
main_domain=$(cat /etc/yunohost/current_host)
|
||||
|
||||
# create metronome directories for domains
|
||||
for domain in $YNH_MAIN_DOMAINS; do
|
||||
mkdir -p "/var/lib/metronome/${domain//./%2e}/pep"
|
||||
# http_upload directory must be writable by metronome and readable by nginx
|
||||
mkdir -p "/var/xmpp-upload/${domain}/upload"
|
||||
# sgid bit allows that file created in that dir will be owned by www-data
|
||||
# despite the fact that metronome ain't in the www-data group
|
||||
chmod g+s "/var/xmpp-upload/${domain}/upload"
|
||||
done
|
||||
|
||||
# fix some permissions
|
||||
[ ! -e '/var/xmpp-upload' ] || chown -R metronome:www-data "/var/xmpp-upload/"
|
||||
[ ! -e '/var/xmpp-upload' ] || chmod 750 "/var/xmpp-upload/"
|
||||
|
||||
# metronome should be in ssl-cert group to let it access SSL certificates
|
||||
usermod -aG ssl-cert metronome
|
||||
chown -R metronome: /var/lib/metronome/
|
||||
chown -R metronome: /etc/metronome/conf.d/
|
||||
|
||||
if [[ -z "$(ls /etc/metronome/conf.d/*.cfg.lua 2>/dev/null)" ]]
|
||||
then
|
||||
if systemctl is-enabled metronome &>/dev/null
|
||||
then
|
||||
systemctl disable metronome --now 2>/dev/null
|
||||
fi
|
||||
else
|
||||
if ! systemctl is-enabled metronome &>/dev/null
|
||||
then
|
||||
systemctl enable metronome --now 2>/dev/null
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
[[ -z "$regen_conf_files" ]] \
|
||||
|| systemctl restart metronome
|
||||
fi
|
||||
}
|
||||
|
||||
do_$1_regen ${@:2}
|
|
@ -74,7 +74,6 @@ do_pre_regen() {
|
|||
cert_status=$(yunohost domain cert status --json)
|
||||
|
||||
# add domain conf files
|
||||
xmpp_domain_list="$(yunohost domain list --features xmpp --output-as json | jq -r ".domains[]")"
|
||||
mail_domain_list="$(yunohost domain list --features mail_in mail_out --output-as json | jq -r ".domains[]")"
|
||||
for domain in $YNH_DOMAINS; do
|
||||
domain_conf_dir="${nginx_conf_dir}/${domain}.d"
|
||||
|
@ -87,12 +86,6 @@ do_pre_regen() {
|
|||
export domain_cert_ca=$(echo $cert_status \
|
||||
| jq ".certificates.\"$domain\".CA_type" \
|
||||
| tr -d '"')
|
||||
if echo "$xmpp_domain_list" | grep -q "^$domain$"
|
||||
then
|
||||
export xmpp_enabled="True"
|
||||
else
|
||||
export xmpp_enabled="False"
|
||||
fi
|
||||
if echo "$mail_domain_list" | grep -q "^$domain$"
|
||||
then
|
||||
export mail_enabled="True"
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
backup_dir="$1/data/xmpp"
|
||||
|
||||
if [[ -e $backup_dir/var_lib_metronome/ ]]
|
||||
then
|
||||
cp -a $backup_dir/var_lib_metronome/. /var/lib/metronome
|
||||
fi
|
||||
|
||||
if [[ -e $backup_dir/var_xmpp-upload ]]
|
||||
then
|
||||
cp -a $backup_dir/var_xmpp-upload/. /var/xmpp-upload
|
||||
fi
|
|
@ -93,7 +93,7 @@
|
|||
"ask_new_domain": "New domain",
|
||||
"ask_new_path": "New path",
|
||||
"ask_password": "Password",
|
||||
"ask_user_domain": "Domain to use for the user's email address and XMPP account",
|
||||
"ask_user_domain": "Domain to use for the user's email address",
|
||||
"backup_abstract_method": "This backup method has yet to be implemented",
|
||||
"backup_actually_backuping": "Creating a backup archive from the collected files…",
|
||||
"backup_app_failed": "Could not back up {app}",
|
||||
|
@ -330,8 +330,6 @@
|
|||
"diagnosis_using_yunohost_testing_details": "This is probably OK if you know what you are doing, but pay attention to the release notes before installing YunoHost upgrades! If you want to disable 'testing' upgrades, you should remove the <cmd>testing</cmd> keyword from <cmd>/etc/apt/sources.list.d/yunohost.list</cmd>.",
|
||||
"disk_space_not_sufficient_install": "There is not enough disk space left to install this application",
|
||||
"disk_space_not_sufficient_update": "There is not enough disk space left to update this application",
|
||||
"domain_cannot_add_muc_upload": "You cannot add domains starting with 'muc.'. This kind of name is reserved for the XMPP multi-users chat feature integrated into YunoHost.",
|
||||
"domain_cannot_add_xmpp_upload": "You cannot add domains starting with 'xmpp-upload.'. This kind of name is reserved for the XMPP upload feature integrated into YunoHost.",
|
||||
"domain_cannot_remove_main": "You cannot remove '{domain}' since it's the main domain, you first need to set another domain as the main domain using 'yunohost domain main-domain -n <another-domain>'; here is the list of candidate domains: {other_domains}",
|
||||
"domain_cannot_remove_main_add_new_one": "You cannot remove '{domain}' since it's the main domain and your only domain, you need to first add another domain using 'yunohost domain add <another-domain.com>', then set is as the main domain using 'yunohost domain main-domain -n <another-domain.com>' and then you can remove the domain '{domain}' using 'yunohost domain remove {domain}'.",
|
||||
"domain_cert_gen_failed": "Could not generate certificate",
|
||||
|
@ -374,8 +372,6 @@
|
|||
"domain_config_search_engine_help": "An URL with an empty query string like `https://duckduckgo.com/?q=`, with `q=` as duckduckgo's empty query parameter",
|
||||
"domain_config_search_engine_name": "Search engine name",
|
||||
"domain_config_show_other_domains_apps": "Show other domain's apps",
|
||||
"domain_config_xmpp": "Instant messaging (XMPP)",
|
||||
"domain_config_xmpp_help": "NB: some XMPP features will require that you update your DNS records and regenerate your Lets Encrypt certificate to be enabled",
|
||||
"domain_created": "Domain created",
|
||||
"domain_creation_failed": "Unable to create domain {domain}: {error}",
|
||||
"domain_deleted": "Domain deleted",
|
||||
|
@ -750,7 +746,6 @@
|
|||
"service_description_dnsmasq": "Handles domain name resolution (DNS)",
|
||||
"service_description_dovecot": "Allows e-mail clients to access/fetch email (via IMAP and POP3)",
|
||||
"service_description_fail2ban": "Protects against brute-force and other kinds of attacks from the Internet",
|
||||
"service_description_metronome": "Manage XMPP instant messaging accounts",
|
||||
"service_description_mysql": "Stores app data (SQL database)",
|
||||
"service_description_nginx": "Serves or provides access to all the websites hosted on your server",
|
||||
"service_description_postfix": "Used to send and receive e-mails",
|
||||
|
|
|
@ -86,7 +86,7 @@ user:
|
|||
comment: good_practices_about_user_password
|
||||
-d:
|
||||
full: --domain
|
||||
help: Domain for the email address and xmpp account
|
||||
help: Domain for the email address
|
||||
extra:
|
||||
pattern: &pattern_domain
|
||||
- !!str ^([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$
|
||||
|
@ -467,7 +467,7 @@ domain:
|
|||
help: Display domains as a tree
|
||||
action: store_true
|
||||
--features:
|
||||
help: List only domains with features enabled (xmpp, mail_in, mail_out)
|
||||
help: List only domains with features enabled (mail_in, mail_out)
|
||||
nargs: "*"
|
||||
|
||||
### domain_info()
|
||||
|
@ -1982,7 +1982,7 @@ diagnosis:
|
|||
api: PUT /diagnosis/ignore
|
||||
arguments:
|
||||
--filter:
|
||||
help: "Add a filter. The first element should be a diagnosis category, and other criterias can be provided using the infos from the 'meta' sections in 'yunohost diagnosis show'. For example: 'dnsrecords domain=yolo.test category=xmpp'"
|
||||
help: "Add a filter. The first element should be a diagnosis category, and other criterias can be provided using the infos from the 'meta' sections in 'yunohost diagnosis show'. For example: 'dnsrecords domain=yolo.test category=mail'"
|
||||
nargs: "*"
|
||||
metavar: CRITERIA
|
||||
--list:
|
||||
|
|
|
@ -60,12 +60,6 @@ name = "Features"
|
|||
type = "boolean"
|
||||
default = 1
|
||||
|
||||
[feature.xmpp]
|
||||
|
||||
[feature.xmpp.xmpp]
|
||||
type = "boolean"
|
||||
default = 0
|
||||
|
||||
[dns]
|
||||
name = "DNS"
|
||||
|
||||
|
|
|
@ -556,6 +556,7 @@ def _fetch_and_enable_new_certificate(domain, no_checks=False):
|
|||
|
||||
def _prepare_certificate_signing_request(domain, key_file, output_folder):
|
||||
from OpenSSL import crypto # lazy loading this module for performance reasons
|
||||
from yunohost.hook import hook_callback
|
||||
|
||||
# Init a request
|
||||
csr = crypto.X509Req()
|
||||
|
@ -563,53 +564,34 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder):
|
|||
# Set the domain
|
||||
csr.get_subject().CN = domain
|
||||
|
||||
from yunohost.domain import domain_config_get
|
||||
sanlist = []
|
||||
hook_results = hook_callback("cert_alternate_names", env={"domain": domain})
|
||||
for hook_name, results in hook_results.items():
|
||||
#
|
||||
# There can be multiple results per hook name, so results look like
|
||||
# {'/some/path/to/hook1':
|
||||
# { 'state': 'succeed',
|
||||
# 'stdreturn': ["foo", "bar"]
|
||||
# },
|
||||
# '/some/path/to/hook2':
|
||||
# { ... },
|
||||
# [...]
|
||||
#
|
||||
# Loop over the sub-results
|
||||
for result in results.values():
|
||||
if results["stdreturn"]:
|
||||
sanlist += results["stdreturn"]
|
||||
|
||||
# If XMPP is enabled for this domain, add xmpp-upload and muc subdomains
|
||||
# in subject alternate names
|
||||
if domain_config_get(domain, key="feature.xmpp.xmpp") == 1:
|
||||
subdomain = "xmpp-upload." + domain
|
||||
xmpp_records = (
|
||||
Diagnoser.get_cached_report(
|
||||
"dnsrecords", item={"domain": domain, "category": "xmpp"}
|
||||
).get("data")
|
||||
or {}
|
||||
)
|
||||
sanlist = []
|
||||
|
||||
# Handle the boring case where the domain is not the root of the dns zone etc...
|
||||
from yunohost.dns import (
|
||||
_get_relative_name_for_dns_zone,
|
||||
_get_dns_zone_for_domain,
|
||||
)
|
||||
|
||||
base_dns_zone = _get_dns_zone_for_domain(domain)
|
||||
basename = _get_relative_name_for_dns_zone(domain, base_dns_zone)
|
||||
suffix = f".{basename}" if basename != "@" else ""
|
||||
|
||||
for sub in ("xmpp-upload", "muc"):
|
||||
subdomain = sub + "." + domain
|
||||
if xmpp_records.get("CNAME:" + sub + suffix) == "OK":
|
||||
sanlist.append(("DNS:" + subdomain))
|
||||
else:
|
||||
logger.warning(
|
||||
m18n.n(
|
||||
"certmanager_warning_subdomain_dns_record",
|
||||
subdomain=subdomain,
|
||||
domain=domain,
|
||||
)
|
||||
if sanlist:
|
||||
csr.add_extensions(
|
||||
[
|
||||
crypto.X509Extension(
|
||||
b"subjectAltName",
|
||||
False,
|
||||
(", ".join(["DNS:{sub}.{domain}" for sub in sanlist])).encode("utf-8"),
|
||||
)
|
||||
|
||||
if sanlist:
|
||||
csr.add_extensions(
|
||||
[
|
||||
crypto.X509Extension(
|
||||
b"subjectAltName",
|
||||
False,
|
||||
(", ".join(sanlist)).encode("utf-8"),
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# Set the key
|
||||
with open(key_file, "rt") as f:
|
||||
|
|
|
@ -91,7 +91,7 @@ class MyDiagnoser(Diagnoser):
|
|||
domain, include_empty_AAAA_if_no_ipv6=True
|
||||
)
|
||||
|
||||
categories = ["basic", "mail", "xmpp", "extra"]
|
||||
categories = ["basic", "mail", "extra"]
|
||||
|
||||
for category in categories:
|
||||
records = expected_configuration[category]
|
||||
|
|
55
src/dns.py
55
src/dns.py
|
@ -75,12 +75,6 @@ def domain_dns_suggest(domain):
|
|||
result += "\n{name} {ttl} IN {type} {value}".format(**record)
|
||||
result += "\n\n"
|
||||
|
||||
if dns_conf["xmpp"]:
|
||||
result += "\n\n"
|
||||
result += "; XMPP"
|
||||
for record in dns_conf["xmpp"]:
|
||||
result += "\n{name} {ttl} IN {type} {value}".format(**record)
|
||||
|
||||
if dns_conf["extra"]:
|
||||
result += "\n\n"
|
||||
result += "; Extra"
|
||||
|
@ -88,7 +82,7 @@ def domain_dns_suggest(domain):
|
|||
result += "\n{name} {ttl} IN {type} {value}".format(**record)
|
||||
|
||||
for name, record_list in dns_conf.items():
|
||||
if name not in ("basic", "xmpp", "mail", "extra") and record_list:
|
||||
if name not in ("basic", "mail", "extra") and record_list:
|
||||
result += "\n\n"
|
||||
result += "; " + name
|
||||
for record in record_list:
|
||||
|
@ -117,14 +111,6 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False):
|
|||
# if ipv6 available
|
||||
{"type": "AAAA", "name": "@", "value": "valid-ipv6", "ttl": 3600},
|
||||
],
|
||||
"xmpp": [
|
||||
{"type": "SRV", "name": "_xmpp-client._tcp", "value": "0 5 5222 domain.tld.", "ttl": 3600},
|
||||
{"type": "SRV", "name": "_xmpp-server._tcp", "value": "0 5 5269 domain.tld.", "ttl": 3600},
|
||||
{"type": "CNAME", "name": "muc", "value": "@", "ttl": 3600},
|
||||
{"type": "CNAME", "name": "pubsub", "value": "@", "ttl": 3600},
|
||||
{"type": "CNAME", "name": "vjud", "value": "@", "ttl": 3600}
|
||||
{"type": "CNAME", "name": "xmpp-upload", "value": "@", "ttl": 3600}
|
||||
],
|
||||
"mail": [
|
||||
{"type": "MX", "name": "@", "value": "10 domain.tld.", "ttl": 3600},
|
||||
{"type": "TXT", "name": "@", "value": "\"v=spf1 a mx ip4:123.123.123.123 ipv6:valid-ipv6 -all\"", "ttl": 3600 },
|
||||
|
@ -148,7 +134,6 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False):
|
|||
|
||||
basic = []
|
||||
mail = []
|
||||
xmpp = []
|
||||
extra = []
|
||||
ipv4 = get_public_ip()
|
||||
ipv6 = get_public_ip(6)
|
||||
|
@ -211,29 +196,6 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False):
|
|||
[f"_dmarc{suffix}", ttl, "TXT", '"v=DMARC1; p=none"'],
|
||||
]
|
||||
|
||||
########
|
||||
# XMPP #
|
||||
########
|
||||
if settings["xmpp"]:
|
||||
xmpp += [
|
||||
[
|
||||
f"_xmpp-client._tcp{suffix}",
|
||||
ttl,
|
||||
"SRV",
|
||||
f"0 5 5222 {domain}.",
|
||||
],
|
||||
[
|
||||
f"_xmpp-server._tcp{suffix}",
|
||||
ttl,
|
||||
"SRV",
|
||||
f"0 5 5269 {domain}.",
|
||||
],
|
||||
[f"muc{suffix}", ttl, "CNAME", f"{domain}."],
|
||||
[f"pubsub{suffix}", ttl, "CNAME", f"{domain}."],
|
||||
[f"vjud{suffix}", ttl, "CNAME", f"{domain}."],
|
||||
[f"xmpp-upload{suffix}", ttl, "CNAME", f"{domain}."],
|
||||
]
|
||||
|
||||
#########
|
||||
# Extra #
|
||||
#########
|
||||
|
@ -259,10 +221,6 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False):
|
|||
{"name": name, "ttl": ttl_, "type": type_, "value": value}
|
||||
for name, ttl_, type_, value in basic
|
||||
],
|
||||
"xmpp": [
|
||||
{"name": name, "ttl": ttl_, "type": type_, "value": value}
|
||||
for name, ttl_, type_, value in xmpp
|
||||
],
|
||||
"mail": [
|
||||
{"name": name, "ttl": ttl_, "type": type_, "value": value}
|
||||
for name, ttl_, type_, value in mail
|
||||
|
@ -277,15 +235,8 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False):
|
|||
# Custom records #
|
||||
##################
|
||||
|
||||
# Defined by custom hooks ships in apps for example ...
|
||||
|
||||
# FIXME : this ain't practical for apps that may want to add
|
||||
# custom dns records for a subdomain ... there's no easy way for
|
||||
# an app to compare the base domain is the parent of the subdomain ?
|
||||
# (On the other hand, in sep 2021, it looks like no app is using
|
||||
# this mechanism...)
|
||||
|
||||
hook_results = hook_callback("custom_dns_rules", args=[base_domain])
|
||||
# Defined by custom hooks shipped in apps for example ...
|
||||
hook_results = hook_callback("custom_dns_rules", env={"base_domain": base_domain, "suffix": suffix})
|
||||
for hook_name, results in hook_results.items():
|
||||
#
|
||||
# There can be multiple results per hook name, so results look like
|
||||
|
|
|
@ -264,12 +264,6 @@ def domain_add(
|
|||
if dyndns_recovery_password:
|
||||
operation_logger.data_to_redact.append(dyndns_recovery_password)
|
||||
|
||||
if domain.startswith("xmpp-upload."):
|
||||
raise YunohostValidationError("domain_cannot_add_xmpp_upload")
|
||||
|
||||
if domain.startswith("muc."):
|
||||
raise YunohostError("domain_cannot_add_muc_upload")
|
||||
|
||||
ldap = _get_ldap_interface()
|
||||
|
||||
try:
|
||||
|
@ -338,7 +332,6 @@ def domain_add(
|
|||
regen_conf(
|
||||
names=[
|
||||
"nginx",
|
||||
"metronome",
|
||||
"dnsmasq",
|
||||
"postfix",
|
||||
"rspamd",
|
||||
|
@ -512,7 +505,7 @@ def domain_remove(
|
|||
f"/etc/nginx/conf.d/{domain}.conf", new_conf=None, save=True
|
||||
)
|
||||
|
||||
regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd", "mdns"])
|
||||
regen_conf(names=["nginx", "dnsmasq", "postfix", "rspamd", "mdns"])
|
||||
app_ssowatconf()
|
||||
|
||||
hook_callback("post_domain_remove", args=[domain])
|
||||
|
@ -686,7 +679,6 @@ def _get_DomainConfigPanel():
|
|||
|
||||
# i18n: domain_config_cert_renew_help
|
||||
# i18n: domain_config_default_app_help
|
||||
# i18n: domain_config_xmpp_help
|
||||
|
||||
def _get_raw_config(self) -> "RawConfig":
|
||||
# TODO add mechanism to share some settings with other domains on the same zone
|
||||
|
@ -695,10 +687,6 @@ def _get_DomainConfigPanel():
|
|||
any_filter = all(self.filter_key)
|
||||
panel_id, section_id, option_id = self.filter_key
|
||||
|
||||
raw_config["feature"]["xmpp"]["xmpp"]["default"] = (
|
||||
1 if self.entity == _get_maindomain() else 0
|
||||
)
|
||||
|
||||
# Portal settings are only available on "topest" domains
|
||||
if _get_parent_domain_of(self.entity, topest=True) is not None:
|
||||
del raw_config["feature"]["portal"]
|
||||
|
@ -835,9 +823,6 @@ def _get_DomainConfigPanel():
|
|||
app_ssowatconf()
|
||||
|
||||
stuff_to_regen_conf = set()
|
||||
if "xmpp" in next_settings:
|
||||
stuff_to_regen_conf.update({"nginx", "metronome"})
|
||||
|
||||
if "mail_in" in next_settings or "mail_out" in next_settings:
|
||||
stuff_to_regen_conf.update({"nginx", "postfix", "dovecot", "rspamd"})
|
||||
|
||||
|
|
|
@ -471,7 +471,7 @@ def dyndns_update(
|
|||
# Delete custom DNS records, we don't support them (have to explicitly
|
||||
# authorize them on dynette)
|
||||
for category in dns_conf.keys():
|
||||
if category not in ["basic", "mail", "xmpp", "extra"]:
|
||||
if category not in ["basic", "mail", "extra"]:
|
||||
del dns_conf[category]
|
||||
|
||||
# Delete the old records for all domain/subdomains
|
||||
|
|
|
@ -28,7 +28,7 @@ from yunohost.log import is_unit_operation
|
|||
|
||||
logger = getLogger("yunohost.user")
|
||||
|
||||
SYSTEM_PERMS = ["mail", "xmpp", "sftp", "ssh"]
|
||||
SYSTEM_PERMS = ["mail", "sftp", "ssh"]
|
||||
|
||||
#
|
||||
#
|
||||
|
@ -170,7 +170,7 @@ def user_permission_update(
|
|||
|
||||
existing_permission = user_permission_info(permission)
|
||||
|
||||
# Refuse to add "visitors" to mail, xmpp ... they require an account to make sense.
|
||||
# Refuse to add "visitors" to mail ... they require an account to make sense.
|
||||
if add and "visitors" in add and permission.split(".")[0] in SYSTEM_PERMS:
|
||||
raise YunohostValidationError(
|
||||
"permission_require_account", permission=permission
|
||||
|
|
|
@ -705,10 +705,6 @@ def _get_services():
|
|||
"category": "web",
|
||||
}
|
||||
|
||||
# Ignore metronome entirely if XMPP was disabled on all domains
|
||||
if "metronome" in services and not glob("/etc/metronome/conf.d/*.cfg.lua"):
|
||||
del services["metronome"]
|
||||
|
||||
# Remove legacy /var/log/daemon.log and /var/log/syslog from log entries
|
||||
# because they are too general. Instead, now the journalctl log is
|
||||
# returned by default which is more relevant.
|
||||
|
|
|
@ -167,7 +167,7 @@ def user_create(
|
|||
assert_password_is_compatible(password)
|
||||
assert_password_is_strong_enough("admin" if admin else "user", password)
|
||||
|
||||
# Validate domain used for email address/xmpp account
|
||||
# Validate domain used for email address account
|
||||
if domain is None:
|
||||
if Moulinette.interface.type == "api":
|
||||
raise YunohostValidationError(
|
||||
|
|
Loading…
Add table
Reference in a new issue