Builtin Prosody modules:

* initialize prosody modules folder
* comment on prosody modules licensing
* use mod_muc_http_defaults to set rooms properties and prevent unauthorized room creation WIP
This commit is contained in:
John Livingston 2021-04-29 16:50:30 +02:00
parent cc5e58ded1
commit 62af899a50
8 changed files with 408 additions and 3 deletions

View File

@ -1,5 +1,9 @@
# Changelog # Changelog
## ???
* Builtin Prosody: use mod_muc_http_defaults to set rooms properties and prevent unauthorized room creation.
## v2.0.3 ## v2.0.3
* Fix Peertube server crash when prosody is not installed * Fix Peertube server crash when prosody is not installed

View File

@ -35,3 +35,5 @@ Then, please refer to the documentation associated with the mode you are plannin
Thanks to David Revoy for his work on Peertube's mascot, [Sepia](https://www.davidrevoy.com/index.php?tag/peertube). Thanks to David Revoy for his work on Peertube's mascot, [Sepia](https://www.davidrevoy.com/index.php?tag/peertube).
Some material icons downloaded from this website where used for icons: [Material.io](https://material.io/resources/icons) Some material icons downloaded from this website where used for icons: [Material.io](https://material.io/resources/icons)
Some Prosody Modules in the `prosody-modules` folder are under MIT license. Please refer to README files in each subfolder, and to the [COPYING](./prosody-modules/COPYING) file. For more informations, here is [the official Prosody Modules website](https://modules.prosody.im).

View File

@ -67,7 +67,8 @@
"build:webpack": "webpack --mode=production", "build:webpack": "webpack --mode=production",
"build:server": "npx tsc --build server/tsconfig.json", "build:server": "npx tsc --build server/tsconfig.json",
"build:serverconverse": "mkdir -p dist/server/conversejs && cp conversejs/index.html dist/server/conversejs/", "build:serverconverse": "mkdir -p dist/server/conversejs && cp conversejs/index.html dist/server/conversejs/",
"build": "npm-run-all -s clean -p build:converse build:images build:webpack build:server build:serverconverse", "build:prosodymodules": "mkdir -p dist/server/prosody-modules && cp -r prosody-modules/* dist/server/prosody-modules/",
"build": "npm-run-all -s clean -p build:converse build:images build:webpack build:server build:serverconverse build:prosodymodules",
"lint": "npx eslint --ext .js --ext .ts ." "lint": "npx eslint --ext .js --ext .ts ."
}, },
"staticDirs": { "staticDirs": {

20
prosody-modules/COPYING Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2009-2015 Various Contributors (see individual files and source control)
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,191 @@
---
summary: Seed MUC configuration from JSON REST API
---
# Introduction
This module fetches configuration for MUC rooms from an API when rooms
are created.
# Requirements
Should work with Prosody 0.11.
# Configuration
`muc_create_api_url`
: URL template for the API endpoint to get settings. `{room.jid}` is
replaced by the address of the room in question.
`muc_create_api_auth`
: The value of the Authorization header to authenticate against the
API. E.g. `"Bearer /rXU4tkQTYQMgdHfMLH6"`{.lua}
In the URL template variable, the room JID is available as `{room.jid}`,
which would be turned into `room@muc.host`. To only get the room
localpart, `{room.jid|jid_node}` can be used, and `{room.jid|jid_host}`
splits out the `muc.host` part.
## Example
``` {.lua}
Component "channels.example.net" "muc"
modules_enabled = { "muc_http_defaults" }
muc_create_api_url = "https://api.example.net/muc/config?jid={room.jid}"
```
# API
A RESTful JSON API is used. Any error causes the room to be destroyed.
The returned JSON consists of two main parts, the room configuration and
the affiliations (member list).
## Room Configuration
The top level `config` field contains a map of properties corresponding
to the fields in the room configuration dialog, named similarly to the
[room configuration default][doc:modules:mod_muc#room-configuration-defaults] in
Prosodys config file.
| Property | Type | Description |
|------------------------|---------|---------------------------------------------------------------------------|
| `name` | string | Name of the chat |
| `description` | string | Longer description of the chat |
| `language` | string | Language code |
| `persistent` | boolean | Whether the room should keep existing if it becomes empty |
| `public` | boolean | `true` to include in public listing |
| `members_only` | boolean | Membership or open |
| `allow_member_invites` | boolean | If members can invite others into members-only rooms |
| `public_jids` | boolean | If everyone or only moderators should see real identities |
| `subject` | string | In-room subject or topic message |
| `changesubject` | boolean | If `true` then everyone can change the subject, otherwise only moderators |
| `historylength` | integer | Number of messages to keep in memory (legacy method) |
| `moderated` | boolean | New participants start without voice privileges if set to `true` |
| `archiving` | boolean | Whether [archiving][doc:modules:mod_muc_mam] is enabled |
## Affiliations
The list of members go in `affiliations` which is either an object
mapping addresses to affiliations (e.g. `{"user@host":"admin"}`{.json}),
or it can be an array of address, affiliation and optionally a reserved
nickname (e.g.
`[{"jid":"user@host","affiliation":"member","nick":"joe"}]`{.json}).
## Schema
Here's a JSON Schema in YAML format describing the expected JSON
response data:
``` {.yaml}
---
type: object
properties:
config:
type: object
properties:
name:
type: string
description:
type: string
language:
type: string
persistent:
type: boolean
public:
type: boolean
members_only:
type: boolean
allow_member_invites:
type: boolean
public_jids:
type: boolean
subject:
type: string
changesubject:
type: boolean
historylength:
type: integer
moderated:
type: boolean
archiving:
type: boolean
affiliations:
oneOf:
- type: array
items:
type: object
required:
- jid
- affiliation
properties:
jid:
type: string
pattern: ^[^@/]+@[^/]+$
affiliation:
$ref: '#/definitions/affiliation'
nick:
type: string
- type: object
additionalProperties:
$ref: '#/definitions/affiliation'
definitions:
affiliation:
type: string
enum:
- owner
- admin
- member
- none
- outcast
...
```
## Example
A basic example with some config settings and a few affiliations:
``` {.json}
GET /muc/config?jid=place@channels.example.net
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
{
"affiliations" : [
{
"affiliation" : "owner",
"jid" : "bosmang@example.net",
"nick" : "bosmang"
},
{
"affiliation" : "admin",
"jid" : "xo@example.net",
"nick" : "xo"
},
{
"affiliation" : "member",
"jid" : "john@example.net"
}
],
"config" : {
"archiving" : true,
"description" : "This is the place",
"members_only" : true,
"moderated" : false,
"name" : "The Place",
"persistent" : true,
"public" : false,
"subject" : "Discussions regarding The Place"
}
}
```
To allow the creation without making any changes, letting whoever
created it be the owner, just return an empty JSON object:
HTTP/1.1 200 OK
Content-Type: application/json
{}

View File

@ -0,0 +1,150 @@
-- Copyright (C) 2021 Kim Alvefur
--
-- This file is MIT licensed. Please see the
-- COPYING file in the source package for more information.
--
local http = require "net.http";
local async = require "util.async";
local uh = require "util.http";
local jid = require "util.jid";
local json = require "util.json";
local st = require "util.stanza";
local funcs = {jid_bare = jid.bare; jid_host = jid.host; jid_node = jid.node};
local render = require"util.interpolation".new("%b{}", uh.urlencode, funcs);
module:depends"muc";
local url_template = assert(module:get_option_string("muc_create_api_url", nil), "'muc_create_api_url' is a required option");
local apiauth = module:get_option_string("muc_create_api_auth", nil);
local ex = {
headers = {
accept = "application/json";
authorization = apiauth;
}
};
local problems = {
format = "API server returned invalid data, see logs",
config = "A problem occured while creating the room, see logs",
};
local function apply_config(room, settings)
local affiliations = settings.affiliations;
if type(affiliations) == "table" then
-- COMPAT the room creator is unconditionally made 'owner'
-- clear existing affiliation
for existing_affiliation in pairs(room._affiliations) do
room:set_affiliation(true, existing_affiliation, "none");
end
if affiliations[1] ~= nil then -- array of ( jid, affiliation, nick )
for _, aff in ipairs(affiliations) do
if type(aff) == "table" and type(aff.jid) == "string" and (aff.nick == nil or type(aff.nick) == "string") then
local prepped_jid = jid.prep(aff.jid);
if prepped_jid then
local ok, err = room:set_affiliation(true, prepped_jid, aff.affiliation, aff.nick and { nick = aff.nick });
if not ok then
module:log("error", "Could not set affiliation in %s: %s", room.jid, err);
return nil, "config";
end
else
module:log("error", "Invalid JID returned from API for %s: %q", room.jid, aff.jid);
return nil, "format";
end
else
module:log("error", "Invalid affiliation item returned from API for %s: %q", room.jid, aff);
return nil, "format";
end
end
else -- map of jid : affiliation
for user_jid, aff in pairs(affiliations) do
if type(user_jid) == "string" and type(aff) == "string" then
local prepped_jid = jid.prep(user_jid);
if prepped_jid then
local ok, err = room:set_affiliation(true, prepped_jid, aff);
if not ok then
module:log("error", "Could not set affiliation in %s: %s", room.jid, err);
return nil, "config";
end
else
module:log("error", "Invalid JID returned from API: %q", aff.jid);
return nil, "format";
end
end
end
end
elseif affiliations ~= nil then
module:log("error", "Invalid affiliations returned from API for %s: %q", room.jid, affiliations);
return nil, "format", { field = "affiliations" };
end
local config = settings.config;
if type(config) == "table" then
-- TODO reject invalid fields instead of ignoring them
if type(config.name) == "string" then room:set_name(config.name); end
if type(config.description) == "string" then room:set_description(config.description); end
if type(config.language) == "string" then room:set_language(config.language); end
if type(config.password) == "string" then room:set_password(config.password); end
if type(config.subject) == "string" then room:set_subject(config.subject); end
if type(config.public) == "boolean" then room:set_public(config.public); end
if type(config.members_only) == "boolean" then room:set_members_only(config.members_only); end
if type(config.allow_member_invites) == "boolean" then room:set_allow_member_invites(config.allow_member_invites); end
if type(config.moderated) == "boolean" then room:set_moderated(config.moderated); end
if type(config.persistent) == "boolean" then room:set_persistent(config.persistent); end
if type(config.changesubject) == "boolean" then room:set_changesubject(config.changesubject); end
if type(config.historylength) == "number" then room:set_historylength(config.historylength); end
if type(config.public_jids) == "boolean" then room:set_whois(config.public_jids and "anyone" or "moderators"); end
-- Leaving out presence_broadcast for now
-- mod_muc_mam
if type(config.archiving) == "boolean" then room._config.archiving = config.archiving; end
elseif config ~= nil then
module:log("error", "Invalid config returned from API for %s: %q", room.jid, config);
return nil, "format", { field = "config" };
end
return true;
end
module:hook("muc-room-pre-create", function(event)
local url = render(url_template, event);
module:log("debug", "Calling API at %q for room %s", url, event.room.jid);
local wait, done = async.waiter();
local ret, err;
http.request(url, ex, function (body, code)
if math.floor(code / 100) == 2 then
local parsed, parse_err = json.decode(body);
if not parsed then
module:log("debug", "Got invalid JSON from %s: %s", url, parse_err);
err = problems.format;
else
ret = parsed;
end
else
module:log("debug", "Rejected by API: ", body);
err = "Rejected by API";
end
done()
end);
wait();
if not ret then
event.room:destroy();
event.origin.send(st.error_reply(event.stanza, "cancel", "internal-server-error", err, module.host));
return true;
end
local configured, err = apply_config(event.room, ret);
if not configured then
event.room:destroy();
event.origin.send(st.error_reply(event.stanza, "cancel", "internal-server-error", err, event.room.jid or module.host));
return true;
end
end, -2);

View File

@ -23,6 +23,8 @@ export async function diagProsody (test: string, options: RegisterServerOptions)
result.messages.push(`Prosody will run on port '${wantedConfig.port}'`) result.messages.push(`Prosody will run on port '${wantedConfig.port}'`)
result.messages.push(`Prosody modules path will be '${wantedConfig.paths.modules}'`)
await fs.promises.access(filePath, fs.constants.R_OK) // throw an error if file does not exist. await fs.promises.access(filePath, fs.constants.R_OK) // throw an error if file does not exist.
result.messages.push(`The prosody configuration file (${filePath}) exists`) result.messages.push(`The prosody configuration file (${filePath}) exists`)
const actualContent = await fs.promises.readFile(filePath, { const actualContent = await fs.promises.readFile(filePath, {

View File

@ -69,6 +69,7 @@ interface ProsodyFilePaths {
log: string log: string
config: string config: string
data: string data: string
modules: string
} }
async function getProsodyFilePaths (options: RegisterServerOptions): Promise<ProsodyFilePaths> { async function getProsodyFilePaths (options: RegisterServerOptions): Promise<ProsodyFilePaths> {
const logger = options.peertubeHelpers.logger const logger = options.peertubeHelpers.logger
@ -81,10 +82,19 @@ async function getProsodyFilePaths (options: RegisterServerOptions): Promise<Pro
error: path.resolve(dir, 'prosody.err'), error: path.resolve(dir, 'prosody.err'),
log: path.resolve(dir, 'prosody.log'), log: path.resolve(dir, 'prosody.log'),
config: path.resolve(dir, 'prosody.cfg.lua'), config: path.resolve(dir, 'prosody.cfg.lua'),
data: path.resolve(dir, 'data') data: path.resolve(dir, 'data'),
modules: path.resolve(__dirname, '../../prosody-modules')
} }
} }
interface ProsodyModuleConfig {
module: string
options: Array<{
name: string
value: string
}>
}
interface ProsodyConfig { interface ProsodyConfig {
content: string content: string
paths: ProsodyFilePaths paths: ProsodyFilePaths
@ -101,11 +111,31 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
const peertubeDomain = 'localhost' const peertubeDomain = 'localhost'
const paths = await getProsodyFilePaths(options) const paths = await getProsodyFilePaths(options)
const logMode: LogMode = 'info' const logMode: LogMode = 'info'
const mucModules: ProsodyModuleConfig[] = []
// mucModules.push({
// module: 'muc_http_defaults',
// options: [
// {
// name: 'muc_create_api_url',
// value: '' // FIXME
// }
// ]
// })
const mucModulesEnabled: string = mucModules.map(m => ' "' + m.module + '";').join('\n')
const mucModulesOptions: string = mucModules.map(m => {
return m.options.map(o => {
return ' ' + o.name + ' = "' + o.value + '"'
}).join('\n')
}).join('\n')
const content = ` const content = `
daemonize = false daemonize = false
pidfile = "${paths.pid}" pidfile = "${paths.pid}"
plugin_paths = { } plugin_paths = { "${paths.modules}" }
data_path = "${paths.data}" data_path = "${paths.data}"
interfaces = { "127.0.0.1" } interfaces = { "127.0.0.1" }
c2s_ports = { } c2s_ports = { }
@ -170,6 +200,9 @@ VirtualHost "localhost"
Component "room.localhost" "muc" Component "room.localhost" "muc"
restrict_room_creation = "local" restrict_room_creation = "local"
modules_enabled = {
${mucModulesEnabled}
}
muc_room_locking = false muc_room_locking = false
muc_tombstones = false muc_tombstones = false
muc_room_default_language = "en" muc_room_default_language = "en"
@ -180,7 +213,9 @@ Component "room.localhost" "muc"
muc_room_default_public_jids = false muc_room_default_public_jids = false
muc_room_default_change_subject = false muc_room_default_change_subject = false
muc_room_default_history_length = 20 muc_room_default_history_length = 20
${mucModulesOptions}
` `
return { return {
content, content,
paths, paths,