Chat Federation (and a lot more) WIP:

Note: websocket s2s is not working yet, still WIP.

New Features

* Chat Federation:
  * You can now connect to a remote chat with your local account.
  * This remote connection is done using a custom implementation of [XEP-0468: WebSocket S2S](https://xmpp.org/extensions/xep-0468.html), using some specific discovering method (so that it will work without any DNS configuration).

Minor changes and fixes

* Possibility to debug Prosody in development environments.
* Using process.spawn instead of process.exec to launch Prosody (safer, and more optimal).
* Prosody AppImage: fix path mapping: we only map necessary /etc/ subdir, so that the AppImage can access to /etc/resolv.conf, /etc/hosts, ...
* Prosody AppImage: hidden debug mode to disable lua-unbound, that seems broken in some docker dev environments.
This commit is contained in:
John Livingston 2023-05-19 12:52:52 +02:00
parent 1174f661be
commit 9a2da60b7d
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
27 changed files with 1592 additions and 106 deletions

View File

@ -4,9 +4,23 @@
### New Features
* Chat Federation: if both local and remote instance have external XMPP connections enabled, you can use your local xmpp account to join remote rooms.
* Chat Federation:
* You can now connect to a remote chat with your local account.
* This remote connection is done using a custom implementation of [XEP-0468: WebSocket S2S](https://xmpp.org/extensions/xep-0468.html), using some specific discovering method (so that it will work without any DNS configuration).
* If both local and remote instance have configured external XMPP connections, it will use legacy S2S connection.
TODO: documentation, and settings names/descriptions changes related to direct XMPP S2S connections.
TODO: write the new prosody modules README.
TODO: mod_s2s_peertubelivechat: dont allow to connect to remote server that are not Peertube servers.
TODO: when sanitizing remote chat endpoint, check that the domain is the same as the video domain (or is room.videodomain.tld).
TODO: get remote server chat informations if missing (for now, it can be missing if there is no known remote video from that server).
### Minor changes and fixes
* Possibility to debug Prosody in development environments.
* Using process.spawn instead of process.exec to launch Prosody (safer, and more optimal).
* Prosody AppImage: fix path mapping: we only map necessary /etc/ subdir, so that the AppImage can access to /etc/resolv.conf, /etc/hosts, ...
* Prosody AppImage: hidden debug mode to disable lua-unbound, that seems broken in some docker dev environments.
## 6.3.0

56
package-lock.json generated
View File

@ -4787,6 +4787,27 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/engine.io/node_modules/ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"dev": true,
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/enquirer": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
@ -10417,27 +10438,6 @@
"typedarray-to-buffer": "^3.1.5"
}
},
"node_modules/ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"dev": true,
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@ -14318,6 +14318,13 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"dev": true,
"requires": {}
}
}
},
@ -18518,13 +18525,6 @@
"typedarray-to-buffer": "^3.1.5"
}
},
"ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"dev": true,
"requires": {}
},
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -1,5 +1,5 @@
local st = require "util.stanza";
local path = require "util.paths";
local path = require "util.paths";
local b64 = require "util.encodings".base64.encode;
local jid = require "util.jid";

View File

@ -0,0 +1,5 @@
# mod_s2s_peertubelivechat
This module is part of peertube-plugin-livechat, and is under the same LICENSE.
This module proxify s2s connections through Peertube if needed.

View File

@ -0,0 +1,247 @@
module:set_global();
-- module:depends("s2s");
local path = require "util.paths";
local json = require "util.json";
-- local st = require "util.stanza";
-- local websocket = require "net.websocket";
-- local server = require "net.server".addclient;
-- local add_filter = require "util.filters".add_filter;
-- local s2s_new_outgoing = require "core.s2smanager".new_outgoing;
-- local s2s_destroy_session = require "core.s2smanager".destroy_session;
-- local bounce_sendq = module:depends "s2s".route_to_new_session.bounce_sendq;
-- local portmanager = require "core.portmanager";
-- local initialize_filters = require "util.filters".initialize;
local server_infos_dir = assert(module:get_option_string("peertubelivechat_server_infos_path", nil), "'peertubelivechat_server_infos_path' is a required option");
local instance_url = assert(module:get_option_string("peertubelivechat_instance_url", nil), "'peertubelivechat_instance_url' is a required option");
-- local stanza_size_limit = module:get_option_number("s2s_stanza_size_limit", 1024 * 512);
-- local frame_buffer_limit = module:get_option_number("websocket_frame_buffer_limit", 2 * stanza_size_limit);
-- local frame_fragment_limit = module:get_option_number("websocket_frame_fragment_limit", 8);
-- local sessions = module:shared("sessions");
-- -- The proxy_listener handles connection while still connecting to the remote websocket server,
-- -- then it hands them over to the normal listener (in mod_s2s)
-- local proxy_listener = { default_port = nil, default_mode = "*a", default_interface = "*" };
-- function proxy_listener.onconnect(conn, ws)
-- local session = sessions[conn];
-- -- Now the real s2s listener can take over the connection.
-- local listener = portmanager.get_service("s2s").listener;
-- local log = session.log;
-- local function websocket_close(code, message)
-- conn:write(build_close(code, message));
-- conn:close();
-- end
-- local function websocket_handle_error(session, code, message)
-- if code == 1009 then -- stanza size limit exceeded
-- -- we close the session, rather than the connection,
-- -- otherwise a resuming client will simply resend the
-- -- offending stanza
-- session:close({ condition = "policy-violation", text = "stanza too large" });
-- else
-- websocket_close(code, message);
-- end
-- end
-- initialize_filters(session);
-- local frameBuffer = dbuffer.new(frame_buffer_limit, frame_fragment_limit);
-- add_filter(session, "bytes/in", function(data)
-- if not frameBuffer:write(data) then
-- session.log("warn", "websocket frame buffer full - terminating session");
-- session:close({ condition = "resource-constraint", text = "frame buffer exceeded" });
-- return;
-- end
-- local cache = {};
-- local frame, length, partial = parse_frame(frameBuffer);
-- while frame do
-- frameBuffer:discard(length);
-- local result, err_status, err_text = handle_frame(frame);
-- if not result then
-- websocket_handle_error(session, err_status, err_text);
-- break;
-- end
-- cache[#cache+1] = filter_open_close(result);
-- frame, length, partial = parse_frame(frameBuffer);
-- end
-- if partial then
-- -- The header of the next frame is already in the buffer, run
-- -- some early validation here
-- local frame_ok, err_status, err_text = validate_frame(partial, stanza_size_limit);
-- if not frame_ok then
-- websocket_handle_error(session, err_status, err_text);
-- end
-- end
-- return t_concat(cache, "");
-- end);
-- add_filter(session, "stanzas/out", function(stanza)
-- stanza = st.clone(stanza);
-- local attr = stanza.attr;
-- attr.xmlns = attr.xmlns or xmlns_client;
-- if stanza.name:find("^stream:") then
-- attr["xmlns:stream"] = attr["xmlns:stream"] or xmlns_streams;
-- end
-- return stanza;
-- end, -1000);
-- add_filter(session, "bytes/out", function(data)
-- return build_frame({ FIN = true, opcode = 0x01, data = tostring(data)});
-- end);
-- local filter = session.filters;
-- session.version = 1;
-- session.sends2s = function (t)
-- log("debug", "sending (s2s over proxy): %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?"));
-- if t.name then
-- t = filter("stanzas/out", t);
-- end
-- if t then
-- t = filter("bytes/out", tostring(t));
-- if t then
-- return conn:write(tostring(t));
-- end
-- end
-- end
-- session.open_stream = function ()
-- session.sends2s(st.stanza("stream:stream", {
-- xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
-- ["xmlns:stream"]='http://etherx.jabber.org/streams',
-- from=session.from_host, to=session.to_host, version='1.0', ["xml:lang"]='en'}):top_tag());
-- end
-- conn.setlistener(conn, listener);
-- listener.register_outgoing(conn, session);
-- listener.onconnect(conn);
-- end
-- function proxy_listener.register_outgoing(conn, session)
-- session.direction = "outgoing";
-- sessions[conn] = session;
-- end
-- function proxy_listener.ondisconnect(conn, err)
-- sessions[conn] = nil;
-- end
function discover_websocket_s2s(event)
local to_host = event.to_host;
module:log("debug", "Trying to route to %s", to_host);
local f_s2s = io.open(path.join(server_infos_dir, to_host, 's2s'), "r");
if f_s2s ~= nil then
io.close(f_s2s);
module.log("debug", "Remote host is a known Peertube %s that has s2s activated, we will let legacy s2s module handle the connection", to_host);
return;
end
local f_ws_proxy = io.open(path.join(server_infos_dir, to_host, 'ws-proxy'), "r");
if f_ws_proxy == nil then
module:log("debug", "Remote host %s is not a known remote Peertube, we will let legacy s2s module handle the connection", to_host);
return;
end
local content = f_ws_proxy:read("*all");
io.close(f_ws_proxy);
local remote_ws_proxy_conf = json.decode(content);
if (not remote_ws_proxy_conf) then
module:log("error", "Remote host %s has empty ws-proxy configuration", to_host);
return;
end
if (not remote_ws_proxy_conf['url']) then
module:log("error", "Remote host %s has missing Websocket url in ws-proxy configuration", to_host);
return;
end
module:log("debug", "Found a Websocket endpoint to proxify s2s communications to remote host %s", to_host);
local properties = {};
properties["extra_headers"] = {
["peertube-livechat-ws-proxy-instance-url"] = instance_url;
};
properties["url"] = remote_ws_proxy_conf["url"];
return properties;
-- local host_session = s2s_new_outgoing(from_host, to_host);
-- -- Store in buffer
-- host_session.bounce_sendq = bounce_sendq;
-- host_session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} };
-- host_session.log("debug", "stanza [%s] queued until connection complete", tostring(stanza.name));
-- local ex = {};
-- ex.headers = {
-- ["peertube-livechat-ws-proxy-instance-url"] = instance_url;
-- ["sec_websocket_protocol"] = 'xmpp';
-- }
-- local ws_listeners = {};
-- ws_listeners.onopen = function ()
-- local conn = self.conn;
-- module:log("debug", "Websocket s2s connection is open, attaching it to the session.");
-- host_session.conn = conn;
-- end
-- ws_listeners.onclose = function (code, message)
-- module:log("debug", "Closing websocket connection for host %s with code '%s' and message '%s'", to_host, json.encode(code), json.encode(message));
-- s2s_destroy_session(host_session, 'websocket-proxy-connection-closed');
-- end
-- ws_listeners.onerror = function (code)
-- module:log("debug", "Error on websocket connection for host %s: '%s'", to_host, json.encode(code));
-- s2s_destroy_session(host_session, 'websocket-proxy-connection-error');
-- end
-- ws_listeners.onmessage = function (data, data_type)
-- module:log("debug", "Receiving %s data for host %s", tostring(data_type), to_host);
-- -- TODO ...
-- end
-- module:log("debug", "Starting the websocket connection process");
-- local ws_connection = websocket.connect(remote_ws_proxy_conf['url'], ex, ws_listeners);
-- -- local conn = addclient(to_host, nil, proxy_listener, "*a");
-- -- proxy_listener.register_outgoing(conn, host_session);
-- -- host_session.conn = conn;
-- return true;
-- local inject = injected and injected[to_host];
-- if not inject then return end
-- module:log("debug", "opening a new outgoing connection for this stanza");
-- local host_session = new_outgoing(from_host, to_host);
-- -- Store in buffer
-- host_session.bounce_sendq = bounce_sendq;
-- host_session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} };
-- host_session.log("debug", "stanza [%s] queued until connection complete", tostring(stanza.name));
-- local host, port = inject[1] or inject, tonumber(inject[2]) or 5269;
-- local conn = addclient(host, port, proxy_listener, "*a");
-- proxy_listener.register_outgoing(conn, host_session);
-- host_session.conn = conn;
-- return true;
end
function module.add_host(module)
module:hook("discover-websocket-s2s", discover_websocket_s2s, -9);
end
if require"core.modulemanager".get_modules_for_host("*"):contains(module.name) then
module:add_host();
end

View File

@ -0,0 +1,471 @@
-- Prosody IM
-- Copyright (C) 2012-2014 Florian Zeitz
-- Copyright (C) 2023 John Livingston
-- Copied from original Prosody mod_websocket module (MIT/X11 licensed). Provided with Peertube Livechat plugin (AGPL-v3).
module:set_global();
local add_task = require "util.timer".add_task;
local add_filter = require "util.filters".add_filter;
local sha1 = require "util.hashes".sha1;
local base64 = require "util.encodings".base64.encode;
local st = require "util.stanza";
local parse_xml = require "util.xml".parse;
local contains_token = require "util.http".contains_token;
local portmanager = require "core.portmanager";
local s2s_new_outgoing = require "core.s2smanager".new_outgoing;
local s2s_destroy_session = require "core.s2smanager".destroy_session;
local log = module._log;
local dbuffer = require "util.dbuffer";
local new_id = require "util.id".short;
local websocket = require "net.websocket";
local websocket_frames = require"net.websocket.frames";
local parse_frame = websocket_frames.parse;
local build_frame = websocket_frames.build;
local build_close = websocket_frames.build_close;
local parse_close = websocket_frames.parse_close;
local t_concat = table.concat;
local stanza_size_limit = module:get_option_number("s2s_stanza_size_limit", 1024 * 512);
local frame_buffer_limit = module:get_option_number("websocket_frame_buffer_limit", 2 * stanza_size_limit);
local frame_fragment_limit = module:get_option_number("websocket_frame_fragment_limit", 8);
local stream_close_timeout = module:get_option_number("s2s_close_timeout", 5);
local consider_websocket_secure = module:get_option_boolean("consider_websocket_secure");
local xmlns_framing = "urn:ietf:params:xml:ns:xmpp-framing-server";
local xmlns_streams = "http://etherx.jabber.org/streams";
local xmlns_client = "jabber:server";
local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'};
module:depends("s2s")
local bounce_sendq = module:depends "s2s".route_to_new_session.bounce_sendq;
local sessions = module:shared("s2s/sessions");
local s2s_listener = portmanager.get_service("s2s").listener;
--- Session methods
local function session_open_stream(session, from, to)
local attr = {
xmlns = xmlns_framing,
["xml:lang"] = "en",
version = "1.0",
id = session.streamid or "",
from = from or session.host, to = to,
};
if session.stream_attrs then
session:stream_attrs(from, to, attr)
end
session.send(st.stanza("open", attr));
end
local function session_close(session, reason)
local log = session.log or log;
local close_event_payload = { session = session, reason = reason };
module:context(session.host):fire_event("pre-session-close", close_event_payload);
reason = close_event_payload.reason;
if session.conn then
if session.notopen then
session:open_stream();
end
if reason then -- nil == no err, initiated by us, false == initiated by client
local stream_error = st.stanza("stream:error");
if type(reason) == "string" then -- assume stream error
stream_error:tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' });
elseif st.is_stanza(reason) then
stream_error = reason;
elseif type(reason) == "table" then
if reason.condition then
stream_error:tag(reason.condition, stream_xmlns_attr):up();
if reason.text then
stream_error:tag("text", stream_xmlns_attr):text(reason.text):up();
end
if reason.extra then
stream_error:add_child(reason.extra);
end
end
end
log("debug", "Disconnecting s2s websocket server, <stream:error> is: %s", stream_error);
session.send(stream_error);
end
session.send(st.stanza("close", { xmlns = xmlns_framing }));
function session.send() return false; end
-- luacheck: ignore 422/reason
-- FIXME reason should be handled in common place
local reason = (reason and (reason.name or reason.text or reason.condition)) or reason;
session.log("debug", "s2s stream for %s closed: %s", session.full_jid or ("<"..session.ip..">"), reason or "session closed");
-- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote
local conn = session.conn;
if reason == nil and not session.notopen and session.type == "s2s" then
-- Grace time to process data from authenticated cleanly-closed stream
add_task(stream_close_timeout, function ()
if not session.destroyed then
session.log("warn", "Failed to receive a stream close response, closing connection anyway...");
s2s_destroy_session(session, reason);
conn:write(build_close(1000, "Stream closed"));
conn:close();
end
end);
else
s2s_destroy_session(session, reason);
conn:write(build_close(1000, "Stream closed"));
conn:close();
end
end
end
--- Filters
local function filter_open_close(data)
if not data:find(xmlns_framing, 1, true) then return data; end
local oc = parse_xml(data);
if not oc then return data; end
if oc.attr.xmlns ~= xmlns_framing then return data; end
if oc.name == "close" then return "</stream:stream>"; end
if oc.name == "open" then
oc.name = "stream:stream";
oc.attr.xmlns = nil;
oc.attr["xmlns:stream"] = xmlns_streams;
return oc:top_tag();
end
return data;
end
local default_get_response_text = "It works!"
local websocket_get_response_text = module:get_option_string("websocket_get_response_text", default_get_response_text)
local default_get_response_body = [[<!DOCTYPE html><html><head><title>Websocket</title></head><body>
<p>]]..websocket_get_response_text..[[</p>
</body></html>]]
local websocket_get_response_body = module:get_option_string("websocket_get_response_body", default_get_response_body)
local function validate_frame(frame, max_length)
local opcode, length = frame.opcode, frame.length;
if max_length and length > max_length then
return false, 1009, "Payload too large";
end
-- Error cases
if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero
return false, 1002, "Reserved bits not zero";
end
if opcode == 0x8 and frame.data then -- close frame
if length == 1 then
return false, 1002, "Close frame with payload, but too short for status code";
elseif length >= 2 then
local status_code = parse_close(frame.data)
if status_code < 1000 then
return false, 1002, "Closed with invalid status code";
elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then
return false, 1002, "Closed with reserved status code";
end
end
end
if opcode >= 0x8 then
if length > 125 then -- Control frame with too much payload
return false, 1002, "Payload too large";
end
if not frame.FIN then -- Fragmented control frame
return false, 1002, "Fragmented control frame";
end
end
if (opcode > 0x2 and opcode < 0x8) or (opcode > 0xA) then
return false, 1002, "Reserved opcode";
end
-- Check opcode
if opcode == 0x2 then -- Binary frame
return false, 1003, "Only text frames are supported, RFC 7395 3.2";
elseif opcode == 0x8 then -- Close request
return false, 1000, "Goodbye";
end
-- Other (XMPP-specific) validity checks
if not frame.FIN then
return false, 1003, "Continuation frames are not supported, RFC 7395 3.3.3";
end
if opcode == 0x01 and frame.data and frame.data:byte(1, 1) ~= 60 then
return false, 1007, "Invalid payload start character, RFC 7395 3.3.3";
end
return true;
end
local function wrap_websocket(session, conn)
local function websocket_close(code, message)
conn:write(build_close(code, message));
conn:close();
end
local function websocket_handle_error(session, code, message)
if code == 1009 then -- stanza size limit exceeded
-- we close the session, rather than the connection,
-- otherwise a resuming client will simply resend the
-- offending stanza
session:close({ condition = "policy-violation", text = "stanza too large" });
else
websocket_close(code, message);
end
end
local function handle_frame(frame)
module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data);
-- Check frame makes sense
local frame_ok, err_status, err_text = validate_frame(frame, stanza_size_limit);
if not frame_ok then
return frame_ok, err_status, err_text;
end
local opcode = frame.opcode;
if opcode == 0x9 then -- Ping frame
frame.opcode = 0xA;
frame.MASK = false; -- Clients send masked frames, servers don't, see #1484
conn:write(build_frame(frame));
return "";
elseif opcode == 0xA then -- Pong frame, MAY be sent unsolicited, eg as keepalive
return "";
elseif opcode ~= 0x1 then -- Not text frame (which is all we support)
log("warn", "Received frame with unsupported opcode %i", opcode);
return "";
end
return frame.data;
end
local frameBuffer = dbuffer.new(frame_buffer_limit, frame_fragment_limit);
add_filter(session, "bytes/in", function(data)
if not frameBuffer:write(data) then
session.log("warn", "websocket frame buffer full - terminating session");
session:close({ condition = "resource-constraint", text = "frame buffer exceeded" });
return;
end
local cache = {};
local frame, length, partial = parse_frame(frameBuffer);
while frame do
frameBuffer:discard(length);
local result, err_status, err_text = handle_frame(frame);
if not result then
websocket_handle_error(session, err_status, err_text);
break;
end
cache[#cache+1] = filter_open_close(result);
frame, length, partial = parse_frame(frameBuffer);
end
if partial then
-- The header of the next frame is already in the buffer, run
-- some early validation here
local frame_ok, err_status, err_text = validate_frame(partial, stanza_size_limit);
if not frame_ok then
websocket_handle_error(session, err_status, err_text);
end
end
return t_concat(cache, "");
end);
add_filter(session, "stanzas/out", function(stanza)
stanza = st.clone(stanza);
local attr = stanza.attr;
attr.xmlns = attr.xmlns or xmlns_client;
if stanza.name:find("^stream:") then
attr["xmlns:stream"] = attr["xmlns:stream"] or xmlns_streams;
end
return stanza;
end, -1000);
add_filter(session, "bytes/out", function(data)
return build_frame({ FIN = true, opcode = 0x01, data = tostring(data)});
end);
end
function handle_request(event)
local request, response = event.request, event.response;
local conn = response.conn;
conn.starttls = false; -- Prevent mod_tls from believing starttls can be done
if not request.headers.sec_websocket_key or request.method ~= "GET" then
return module:fire_event("http-message", {
response = event.response;
---
title = "Prosody WebSocket endpoint";
message = websocket_get_response_text;
warning = not (consider_websocket_secure or request.secure) and "This endpoint is not considered secure!" or nil;
}) or websocket_get_response_body;
end
local wants_xmpp = contains_token(request.headers.sec_websocket_protocol or "", "xmpp");
if not wants_xmpp then
module:log("debug", "Client didn't want to talk XMPP, list of protocols was %s", request.headers.sec_websocket_protocol or "(empty)");
return 501;
end
conn:setlistener(s2s_listener);
s2s_listener.onconnect(conn);
local session = sessions[conn];
-- Use upstream IP if a HTTP proxy was used
-- See mod_http and #540
session.ip = request.ip;
session.secure = consider_websocket_secure or request.secure or session.secure;
session.websocket_request = request;
session.open_stream = session_open_stream;
session.close = session_close;
wrap_websocket(session, conn);
response.status_code = 101;
response.headers.upgrade = "websocket";
response.headers.connection = "Upgrade";
response.headers.sec_webSocket_accept = base64(sha1(request.headers.sec_websocket_key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
response.headers.sec_webSocket_protocol = "xmpp";
module:fire_event("websocket-session", { session = session, request = request });
session.log("debug", "Sending WebSocket handshake");
return "";
end
local function keepalive(event)
local session = event.session;
if session.open_stream == session_open_stream then
return session.conn:write(build_frame({ opcode = 0x9, FIN = true }));
end
end
-- OUTGOING CONNECTIONS
local pending_ws_connection_methods = {};
local pending_ws_connection_mt = {
__name = "pending_ws_connection";
__index = pending_ws_connection_methods;
__tostring = function (p)
return "<pending websocket connection "..p.id.." to "..tostring(p.target_resolver.hostname)..">";
end;
};
function pending_ws_connection_methods:log(level, message, ...)
log(level, "[pending connection %s] "..message, self.id, ...);
end
-- pending_ws_connections_map[ws_connection] = pending_connection
local pending_ws_connections_map = {};
local pending_ws_connection_listeners = {};
function pending_ws_connection_listeners.onopen(ws_connection)
local p = pending_ws_connections_map[ws_connection];
if not p then
if ws_connection.conn then
module:log("warn", "Successful connection, but unexpected! Closing.");
ws_connection.conn:close();
else
module:log("error", "Successful connection, but unexpected, and no conn attribute!");
end
return;
end
pending_ws_connections_map[ws_connection] = nil;
local conn = ws_connection.conn;
p:log("debug", "Successfully connected");
conn:setlistener(p.listeners, p.data);
p.listeners.onconnect(conn);
wrap_websocket(session, conn);
end
function pending_ws_connection_listeners.onclose(ws_connection, reason)
local p = pending_ws_connections_map[ws_connection];
if not p then
module:log("warn", "Failed connection, but unexpected!");
return;
end
p.last_error = reason or "unknown reason";
p:log("debug", "Connection attempt failed: %s", p.last_error);
if p.listeners.onfail then
p.listeners.onfail(p.data, p.last_error or p.target_resolver.last_error or "unable to connect to websocket");
end
end
function route_to_new_session(event)
local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza;
module:log("debug", "Trying to route to %s", to_host);
local ws_properties = module:fire_event("discover-websocket-s2s", { to_host = to_host });
if not ws_properties then
module:log("debug", "No websocket s2s capabilities from remote host %s", to_host);
return;
end
module:log("debug", "Found a Websocket endpoint for s2s communications to remote host %s", to_host);
local session = s2s_new_outgoing(from_host, to_host);
-- Store in buffer
session.bounce_sendq = bounce_sendq;
session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} };
session.log("debug", "stanza [%s] queued until connection complete", tostring(stanza.name));
-- FIXME: this is needed for admin tools to count this connection.
-- session.websocket_request = request;
session.open_stream = session_open_stream;
session.close = session_close;
local ex = {};
ex["headers"] = ws_properties.extra_headers or {};
ex["protocol"] = "xmpp";
module:log("debug", "Starting the websocket connection process");
local p = setmetatable({
id = new_id();
listeners = portmanager.get_service("s2s").listener;
data = { session = session };
}, pending_ws_connection_mt);
local ws_connection = websocket.connect(ws_properties['url'], ex, p);
return true;
end
function module.add_host(module)
module:hook("s2s-read-timeout", keepalive, -0.9);
module:hook("route/remote", route_to_new_session, -2);
module:depends("http");
module:provides("http", {
name = "websocket";
default_path = "xmpp-websocket-s2s";
cors = {
enabled = true;
};
route = {
["GET"] = handle_request;
["GET /"] = handle_request;
};
});
module:hook("s2s-read-timeout", keepalive, -0.9);
end
if require"core.modulemanager".get_modules_for_host("*"):contains(module.name) then
module:add_host();
end

View File

@ -64,7 +64,12 @@ AppDir:
# Note: this assume that peertube-plugin-livechat is not in a subdir of one of following mappings.
# This seems a reasonable assumption.
path_mappings:
- /etc/:$APPDIR/etc/
# Dont map entire /etc/ (or dns resolution will not work properly)
- /etc/init.d/:$APPDIR/etc/init.d/
- /etc/ld.so.conf.d/:$APPDIR/etc/ld.so.conf.d/
- /etc/logrotate.d/:$APPDIR/etc/logrotate.d/
- /etc/prosody/:$APPDIR/etc/prosody/
- /etc/ssl/:$APPDIR/etc/ssl/
- /lib/:$APPDIR/lib/
- /lib64/:$APPDIR/lib64/
- /runtime/:$APPDIR/runtime/

View File

@ -58,7 +58,12 @@ AppDir:
# Note: this assume that peertube-plugin-livechat is not in a subdir of one of following mappings.
# This seems a reasonable assumption.
path_mappings:
- /etc/:$APPDIR/etc/
# Dont map entire /etc/ (or dns resolution will not work properly)
- /etc/init.d/:$APPDIR/etc/init.d/
- /etc/ld.so.conf.d/:$APPDIR/etc/ld.so.conf.d/
- /etc/logrotate.d/:$APPDIR/etc/logrotate.d/
- /etc/prosody/:$APPDIR/etc/prosody/
- /etc/ssl/:$APPDIR/etc/ssl/
- /lib/:$APPDIR/lib/
- /lib64/:$APPDIR/lib64/
- /runtime/:$APPDIR/runtime/

View File

@ -3,6 +3,19 @@
-- This file is a launcher, that takes the first argument to choose what to launch.
local what = table.remove(arg, 1);
if what == 'debug' then
-- Special debug mode. Should not be used in production.
print('Activating MobDebug...');
mobdebug_path = table.remove(arg, 1);
mobdebug_host = table.remove(arg, 1);
mobdebug_port = table.remove(arg, 1);
local lua_path_sep = package.config:sub(3,3);
local dir_sep = package.config:sub(1,1);
package.path = package.path..lua_path_sep..mobdebug_path..dir_sep.."?.lua";
require "mobdebug".start(mobdebug_host, mobdebug_port);
what = table.remove(arg, 1);
end
if what == 'prosody' then
dofile('/usr/bin/prosody');
elseif what == 'prosodyctl' then

View File

@ -2,6 +2,11 @@ import type { RegisterServerOptions } from '@peertube/peertube-types'
import * as path from 'path'
import * as fs from 'fs'
/**
* Check if debug mode is enabled
* @param options server options
* @returns true if debug mode enabled
*/
export function isDebugMode (options: RegisterServerOptions): boolean {
const peertubeHelpers = options.peertubeHelpers
const logger = peertubeHelpers.logger
@ -17,3 +22,84 @@ export function isDebugMode (options: RegisterServerOptions): boolean {
}
return false
}
interface ProsodyDebuggerOptions {
mobdebugPath: string
mobdebugHost: string
mobdebugPort: string
}
/**
* On dev environnement, it is possible to enable a Lua debugger.
* @param options server options
* @returns false if we dont use the Prosody debugger. Else the need information to launch the debugger.
*/
export function prosodyDebuggerOptions (options: RegisterServerOptions): false | ProsodyDebuggerOptions {
if (process.env.NODE_ENV !== 'dev') { return false }
if (!isDebugMode(options)) { return false }
const peertubeHelpers = options.peertubeHelpers
const logger = peertubeHelpers.logger
try {
const filepath = path.resolve(peertubeHelpers.plugin.getDataDirectoryPath(), 'debug_mode')
const content = fs.readFileSync(filepath).toString()
if (!content) { return false }
const json = JSON.parse(content)
if (!json) { return false }
if (typeof json !== 'object') { return false }
if (!json.debug_prosody) { return false }
if (typeof json.debug_prosody !== 'object') { return false }
if (!json.debug_prosody.debugger_path) { return false }
if (typeof json.debug_prosody.debugger_path !== 'string') { return false }
const mobdebugPath = json.debug_prosody.debugger_path
if (!fs.statSync(mobdebugPath).isDirectory()) {
logger.error('The should be a debugger, but cant find it. Path should be: ', mobdebugPath)
return false
}
const mobdebugHost = json.debug_prosody.host?.toString() || 'localhost'
const mobdebugPort = json.debug_prosody.port?.toString() || '8172'
return {
mobdebugPath,
mobdebugHost,
mobdebugPort
}
} catch (err) {
logger.error('Failed to read the debug_mode file content:', err)
return false
}
}
/**
* In some dev environment, Prosody will fail DNS queries when using Lua-unbound.
* I did not managed to properly configure lua-unbound.
* So, here is a dirty hack to disable lua-unbound: just put a `no_lua_unbound`
* file in the plugin data dir. This will delete the lua file from the AppImage extraction.
* You must restart Peertube after adding or deleting this file.
* @param options server options
* @param squashfsPath the folder where the AppImage is extracted
*/
export function disableLuaUnboundIfNeeded (options: RegisterServerOptions, squashfsPath: string): void {
const peertubeHelpers = options.peertubeHelpers
const logger = peertubeHelpers.logger
if (!peertubeHelpers.plugin) {
return
}
const filepath = path.resolve(peertubeHelpers.plugin.getDataDirectoryPath(), 'no_lua_unbound')
logger.debug('Testing if file exists: ' + filepath)
if (!fs.existsSync(filepath)) {
return
}
logger.info('Must disable lua-unbound.')
try {
for (const luaVersion of ['5.1', '5.2', '5.3', '5.4']) {
const fp = path.resolve(squashfsPath, 'squashfs-root/usr/lib/x86_64-linux-gnu/lua/', luaVersion, 'lunbound.so')
if (fs.existsSync(fp)) {
fs.rmSync(fp)
}
}
} catch (err) {
logger.error(err)
}
}

View File

@ -36,6 +36,7 @@ function remoteAuthenticatedConnectionEnabled (livechatInfos: LiveChatJSONLDAttr
if (!livechatInfos.links) { return false }
if (livechatInfos.type !== 'xmpp') { return false }
for (const link of livechatInfos.links) {
if (link.type === 'xmpp-peertube-livechat-ws-s2s') { return true }
if (link.type === 'xmpp-s2s') { return true }
}
return false

View File

@ -1,6 +1,6 @@
import type { RegisterServerOptions } from '@peertube/peertube-types'
import type { RemoteVideoHandlerParams } from './types'
import { storeVideoLiveChatInfos } from './storage'
import { storeVideoLiveChatInfos, storeRemoteServerInfos } from './storage'
import { sanitizePeertubeLiveChatInfos } from './sanitize'
/**
@ -19,6 +19,9 @@ async function readIncomingAPVideo (
peertubeLiveChat = sanitizePeertubeLiveChatInfos(peertubeLiveChat)
await storeVideoLiveChatInfos(options, video, peertubeLiveChat)
if (video.remote) {
await storeRemoteServerInfos(options, peertubeLiveChat)
}
}
export {

View File

@ -2,7 +2,7 @@ import type { RegisterServerOptions, VideoObject } from '@peertube/peertube-type
import type { LiveChatVideoObject, VideoBuildResultContext, LiveChatJSONLDLink, LiveChatJSONLDAttribute } from './types'
import { storeVideoLiveChatInfos } from './storage'
import { videoHasWebchat } from '../../../shared/lib/video'
import { getBoshUri, getWSUri } from '../uri/webchat'
import { getBoshUri, getWSUri, getWSS2SUri } from '../uri/webchat'
import { canonicalizePluginUri } from '../uri/canonicalize'
import { getProsodyDomain } from '../prosody/config/domain'
import { fillVideoCustomFields } from '../custom-fields'
@ -32,7 +32,8 @@ async function videoBuildJSONLD (
'prosody-room-type',
'federation-dont-publish-remotely',
'chat-no-anonymous',
'prosody-room-allow-s2s'
'prosody-room-allow-s2s',
'prosody-s2s-port'
])
if (settings['federation-dont-publish-remotely']) {
@ -71,9 +72,23 @@ async function videoBuildJSONLD (
}
const links: LiveChatJSONLDLink[] = []
if (!settings['federation-dont-publish-remotely']) {
const wsS2SUri = getWSS2SUri(options)
if (wsS2SUri) {
links.push({
type: 'xmpp-peertube-livechat-ws-s2s',
url: canonicalizePluginUri(options, wsS2SUri, {
removePluginVersion: true,
protocol: 'ws'
})
})
}
}
if (settings['prosody-room-allow-s2s']) {
links.push({
type: 'xmpp-s2s'
type: 'xmpp-s2s',
host: prosodyDomain,
port: (settings['prosody-s2s-port'] as string) ?? ''
})
}
if (!settings['chat-no-anonymous']) {

View File

@ -37,8 +37,34 @@ function sanitizePeertubeLiveChatInfos (chatInfos: any): LiveChatJSONLDAttribute
})
}
if (link.type === 'xmpp-s2s') {
if (!/^\d+$/.test(link.port)) {
continue
}
const host = _validateHost(link.host)
if (!host) {
continue
}
r.links.push({
type: link.type
type: link.type,
host,
port: link.port
})
}
if (link.type === 'xmpp-peertube-livechat-ws-s2s') {
if ((typeof link.url) !== 'string') { continue }
if (
!_validUrl(link.url, {
noSearchParams: true,
protocol: 'ws.'
})
) {
continue
}
r.links.push({
type: link.type,
url: link.url
})
}
}
@ -81,6 +107,16 @@ function _validUrl (s: string, constraints: URLConstraints): boolean {
return true
}
function _validateHost (s: string): false | string {
try {
if (s.includes('/')) { return false }
const url = new URL('http://' + s)
return url.hostname
} catch (_err) {
return false
}
}
export {
sanitizePeertubeLiveChatInfos
}

View File

@ -1,5 +1,5 @@
import type { RegisterServerOptions, MVideoFullLight, MVideoAP, Video, MVideoThumbnail } from '@peertube/peertube-types'
import type { LiveChatJSONLDAttribute } from './types'
import type { LiveChatJSONLDAttribute, LiveChatJSONLDS2SLink, LiveChatJSONLDPeertubeWSS2SLink } from './types'
import { sanitizePeertubeLiveChatInfos } from './sanitize'
import { URL } from 'url'
import * as fs from 'fs'
@ -97,6 +97,60 @@ async function getVideoLiveChatInfos (
return r
}
/**
* When receiving livechat information for remote servers, we store some information
* about remote server capatibilities: has it s2s enabled? can it proxify s2s in Peertube?
* These information can then be read by Prosody module mod_s2s_peertubelivechat.
*
* We simply store the more recent informations. Indeed, it should be consistent between videos.
* @param options server optiosn
* @param liveChatInfos livechat stored data
*/
async function storeRemoteServerInfos (
options: RegisterServerOptions,
liveChatInfos: LiveChatJSONLDAttribute
): Promise<void> {
if (!liveChatInfos) { return }
const logger = options.peertubeHelpers.logger
const roomJID = liveChatInfos.jid
const host = roomJID.split('@')[1]
if (!host) {
logger.error(`Room JID seems not correct, no host: ${roomJID}`)
return
}
if (host.includes('..')) {
logger.error(`Room host seems not correct, contains ..: ${host}`)
return
}
const dir = path.resolve(
options.peertubeHelpers.plugin.getDataDirectoryPath(),
'serverInfos',
host
)
const s2sFilePath = path.resolve(dir, 's2s')
const wsS2SFilePath = path.resolve(dir, 'ws-s2s')
const s2sLink = liveChatInfos.links.find(v => v.type === 'xmpp-s2s')
if (s2sLink) {
await _store(options, s2sFilePath, {
host: (s2sLink as LiveChatJSONLDS2SLink).host,
port: (s2sLink as LiveChatJSONLDS2SLink).port
})
} else {
await _del(options, s2sFilePath)
}
const wsS2SLink = liveChatInfos.links.find(v => v.type === 'xmpp-peertube-livechat-ws-s2s')
if (wsS2SLink) {
await _store(options, wsS2SFilePath, {
url: (wsS2SLink as LiveChatJSONLDPeertubeWSS2SLink).url
})
} else {
await _del(options, wsS2SFilePath)
}
}
async function _getFilePath (
options: RegisterServerOptions,
remote: boolean,
@ -152,13 +206,22 @@ async function _del (options: RegisterServerOptions, filePath: string): Promise<
async function _store (options: RegisterServerOptions, filePath: string, content: any): Promise<void> {
const logger = options.peertubeHelpers.logger
try {
const jsonContent = JSON.stringify(content)
if (!fs.existsSync(filePath)) {
const dir = path.dirname(filePath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
} else {
// only write if the content is different
try {
const currentJSONContent = await fs.promises.readFile(filePath, {
encoding: 'utf-8'
})
if (currentJSONContent === jsonContent) { return }
} catch (_err) {}
}
await fs.promises.writeFile(filePath, JSON.stringify(content), {
await fs.promises.writeFile(filePath, jsonContent, {
encoding: 'utf-8'
})
} catch (err) {
@ -182,7 +245,16 @@ async function _get (options: RegisterServerOptions, filePath: string): Promise<
}
}
function getRemoteServerInfosDir (options: RegisterServerOptions): string {
return path.resolve(
options.peertubeHelpers.plugin.getDataDirectoryPath(),
'serverInfos'
)
}
export {
storeVideoLiveChatInfos,
getVideoLiveChatInfos
storeRemoteServerInfos,
getVideoLiveChatInfos,
getRemoteServerInfosDir
}

View File

@ -4,8 +4,15 @@ interface VideoBuildResultContext {
video: MVideoAP
}
interface LiveChatJSONLDPeertubeWSS2SLink {
type: 'xmpp-peertube-livechat-ws-s2s'
url: string
}
interface LiveChatJSONLDS2SLink {
type: 'xmpp-s2s'
host: string
port: string
}
interface LiveChatJSONLDAnonymousWebsocketLink {
@ -20,7 +27,11 @@ interface LiveChatJSONLDAnonymousBOSHLink {
jid: string
}
type LiveChatJSONLDLink = LiveChatJSONLDS2SLink | LiveChatJSONLDAnonymousBOSHLink | LiveChatJSONLDAnonymousWebsocketLink
type LiveChatJSONLDLink =
LiveChatJSONLDPeertubeWSS2SLink
| LiveChatJSONLDS2SLink
| LiveChatJSONLDAnonymousBOSHLink
| LiveChatJSONLDAnonymousWebsocketLink
interface LiveChatJSONLDInfos {
type: 'xmpp'
@ -42,6 +53,8 @@ interface RemoteVideoHandlerParams {
export {
VideoBuildResultContext,
LiveChatJSONLDLink,
LiveChatJSONLDS2SLink,
LiveChatJSONLDPeertubeWSS2SLink,
LiveChatJSONLDInfos,
LiveChatJSONLDAttribute,
LiveChatVideoObject,

View File

@ -8,6 +8,7 @@ import { ConfigLogExpiration, ProsodyConfigContent } from './config/content'
import { getProsodyDomain } from './config/domain'
import { getAPIKey } from '../apikey'
import { parseExternalComponents } from './config/components'
import { getRemoteServerInfosDir } from '../federation/storage'
async function getWorkingDir (options: RegisterServerOptions): Promise<string> {
const peertubeHelpers = options.peertubeHelpers
@ -64,26 +65,24 @@ async function getProsodyFilePaths (options: RegisterServerOptions): Promise<Pro
let certsDir: string | undefined = path.resolve(dir, 'certs')
let certsDirIsCustom = false
if (settings['prosody-room-allow-s2s']) {
if ((settings['prosody-certificates-dir'] as string ?? '') !== '') {
if (!fs.statSync(settings['prosody-certificates-dir'] as string).isDirectory()) {
// We can throw an exception here...
// Because if the user input a wrong directory, the plugin will not register,
// and he will never be able to fix the conf
logger.error('Certificate directory does not exist or is not a directory')
certsDir = undefined
} else {
certsDir = settings['prosody-certificates-dir'] as string
}
certsDirIsCustom = true
if (settings['prosody-room-allow-s2s'] && (settings['prosody-certificates-dir'] as string ?? '') !== '') {
if (!fs.statSync(settings['prosody-certificates-dir'] as string).isDirectory()) {
// We can throw an exception here...
// Because if the user input a wrong directory, the plugin will not register,
// and he will never be able to fix the conf
logger.error('Certificate directory does not exist or is not a directory')
certsDir = undefined
} else {
// In this case we are generating and using self signed certificates
// Note: when using prosodyctl to generate self-signed certificates,
// there are wrongly generated in the data dir.
// So we will use this dir as the certs dir.
certsDir = path.resolve(dir, 'data')
certsDir = settings['prosody-certificates-dir'] as string
}
certsDirIsCustom = true
} else {
// In this case we are generating and using self signed certificates
// Note: when using prosodyctl to generate self-signed certificates,
// there are wrongly generated in the data dir.
// So we will use this dir as the certs dir.
certsDir = path.resolve(dir, 'data')
}
return {
@ -139,7 +138,8 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
'prosody-components',
'prosody-components-port',
'prosody-components-list',
'chat-no-anonymous'
'chat-no-anonymous',
'federation-dont-publish-remotely'
])
const valuesToHideInDiagnostic = new Map<string, string>()
@ -151,23 +151,27 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
const disableAnon = (settings['chat-no-anonymous'] as boolean) || false
const logExpirationSetting = (settings['prosody-muc-expiration'] as string) ?? DEFAULTLOGEXPIRATION
const enableC2S = (settings['prosody-c2s'] as boolean) || false
// enableRoomS2S: room can be joined from remote XMPP servers (Peertube or not)
const enableRoomS2S = (settings['prosody-room-allow-s2s'] as boolean) || false
const enableComponents = (settings['prosody-components'] as boolean) || false
const prosodyDomain = await getProsodyDomain(options)
const paths = await getProsodyFilePaths(options)
const roomType = settings['prosody-room-type'] === 'channel' ? 'channel' : 'video'
const enableUserS2S = enableRoomS2S && !(settings['federation-no-remote-chat'] as boolean)
// enableRemoteChatConnections: local users can communicate with external rooms
const enableRemoteChatConnections = !(settings['federation-dont-publish-remotely'] as boolean)
let certificates: ProsodyConfigCertificates = false
const apikey = await getAPIKey(options)
valuesToHideInDiagnostic.set('APIKey', apikey)
const publicServerUrl = options.peertubeHelpers.config.getWebserverUrl()
let basePeertubeUrl = settings['prosody-peertube-uri'] as string
if (basePeertubeUrl && !/^https?:\/\/[a-z0-9.-_]+(?::\d+)?$/.test(basePeertubeUrl)) {
throw new Error('Invalid prosody-peertube-uri')
}
if (!basePeertubeUrl) {
basePeertubeUrl = options.peertubeHelpers.config.getWebserverUrl()
basePeertubeUrl = publicServerUrl
}
const baseApiUrl = basePeertubeUrl + getBaseRouterRoute(options) + 'api/'
@ -181,7 +185,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
}
config.useHttpAuthentication(authApiUrl)
const useWS = !!options.registerWebSocketRoute // this comes with Peertube >=5.0.0, and is a prerequisite to websocket
config.usePeertubeBoshAndWebsocket(prosodyDomain, port, options.peertubeHelpers.config.getWebserverUrl(), useWS)
config.usePeertubeBoshAndWebsocket(prosodyDomain, port, publicServerUrl, useWS)
config.useMucHttpDefault(roomApiUrl)
if (enableC2S) {
@ -204,27 +208,33 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
config.useExternalComponents(componentsPort, components)
}
if (enableRoomS2S || enableUserS2S) {
if (enableRoomS2S || enableRemoteChatConnections) {
certificates = 'generate-self-signed'
if (config.paths.certsDirIsCustom) {
certificates = 'use-from-dir'
}
const s2sPort = (settings['prosody-s2s-port'] as string) || '5269'
if (!/^\d+$/.test(s2sPort)) {
throw new Error('Invalid s2s port')
let s2sPort, s2sInterfaces
if (enableRoomS2S) {
s2sPort = (settings['prosody-s2s-port'] as string) || '5269'
if (!/^\d+$/.test(s2sPort)) {
throw new Error('Invalid s2s port')
}
s2sInterfaces = ((settings['prosody-s2s-interfaces'] as string) || '')
.split(',')
.map(s => s.trim())
// Check that there is no invalid values (to avoid injections):
s2sInterfaces.forEach(networkInterface => {
if (networkInterface === '*') return
if (networkInterface === '::') return
if (networkInterface.match(/^\d+\.\d+\.\d+\.\d+$/)) return
if (networkInterface.match(/^[a-f0-9:]+$/)) return
throw new Error('Invalid s2s interfaces')
})
} else {
s2sPort = null
s2sInterfaces = null
}
const s2sInterfaces = ((settings['prosody-s2s-interfaces'] as string) || '')
.split(',')
.map(s => s.trim())
// Check that there is no invalid values (to avoid injections):
s2sInterfaces.forEach(networkInterface => {
if (networkInterface === '*') return
if (networkInterface === '::') return
if (networkInterface.match(/^\d+\.\d+\.\d+\.\d+$/)) return
if (networkInterface.match(/^[a-f0-9:]+$/)) return
throw new Error('Invalid s2s interfaces')
})
config.useS2S(s2sPort, s2sInterfaces, !enableUserS2S)
config.useS2S(s2sPort, s2sInterfaces, publicServerUrl, getRemoteServerInfosDir(options))
}
const logExpiration = readLogExpiration(options, logExpirationSetting)

View File

@ -72,6 +72,18 @@ abstract class ProsodyConfigBlock {
this.entries.set(name, entry)
}
remove (name: string, value: ConfigEntryValue): void {
if (!this.entries.has(name)) {
return
}
let entry = this.entries.get(name) as ConfigEntryValue
if (!Array.isArray(entry)) {
entry = [entry]
}
entry = entry.filter(v => v !== value)
this.entries.set(name, entry)
}
write (): string {
let content = ''
// Map keeps order :)
@ -258,17 +270,35 @@ class ProsodyConfigContent {
this.global.set('c2s_ports', [c2sPort])
}
useS2S (s2sPort: string, s2sInterfaces: string[], mucOnly: boolean): void {
this.global.set('s2s_ports', [s2sPort])
this.global.set('s2s_interfaces', s2sInterfaces)
this.global.set('s2s_secure_auth', false)
this.global.add('modules_enabled', 'tls') // required for s2s and co
this.muc.add('modules_enabled', 's2s')
this.muc.add('modules_enabled', 'dialback') // This allows s2s connections without certicicates!
if (!mucOnly && this.authenticated) {
this.authenticated.add('modules_enabled', 's2s')
this.authenticated.add('modules_enabled', 'dialback') // This allows s2s connections without certicicates!
useS2S (
s2sPort: string | null,
s2sInterfaces: string[] | null,
publicServerUrl: string,
serverInfosDir: string
): void {
if (s2sPort !== null) {
this.global.set('s2s_ports', [s2sPort])
} else {
this.global.set('s2s_ports', [])
}
if (s2sInterfaces !== null) {
this.global.set('s2s_interfaces', s2sInterfaces)
} else {
this.global.set('s2s_interfaces', [])
}
this.global.set('s2s_secure_auth', false)
this.global.remove('modules_disabled', 's2s')
this.global.add('modules_enabled', 's2s')
this.global.add('modules_enabled', 'tls') // required for s2s and co
this.global.add('modules_enabled', 's2s_peertubelivechat')
this.global.set('peertubelivechat_server_infos_path', serverInfosDir)
this.global.set('peertubelivechat_instance_url', publicServerUrl)
this.global.add('modules_enabled', 'websocket_s2s_peertubelivechat')
this.muc.add('modules_enabled', 'dialback') // This allows s2s connections without certicicates!
this.authenticated?.add('modules_enabled', 'dialback') // This allows s2s connections without certicicates!
}
useExternalComponents (componentsPort: string, components: ExternalComponent[]): void {

View File

@ -8,6 +8,7 @@ import { disableProxyRoute, enableProxyRoute } from '../routers/webchat'
import { fixRoomSubject } from './fix-room-subject'
import * as fs from 'fs'
import * as child_process from 'child_process'
import { disableLuaUnboundIfNeeded, prosodyDebuggerOptions } from '../../lib/debug'
async function _ensureWorkingDir (
options: RegisterServerOptions,
@ -98,6 +99,7 @@ async function prepareProsody (options: RegisterServerOptions): Promise<void> {
})
spawned.on('error', reject)
spawned.on('close', (_code) => { // 'close' and not 'exit', to be sure it is finished.
disableLuaUnboundIfNeeded(options, filePaths.appImageExtractPath)
resolve()
})
})
@ -275,20 +277,28 @@ async function testProsodyCorrectlyRunning (options: RegisterServerOptions): Pro
return result
}
async function ensureProsodyRunning (options: RegisterServerOptions): Promise<void> {
async function ensureProsodyRunning (
options: RegisterServerOptions,
forceRestart?: boolean,
restartProsodyInDebugMode?: boolean
): Promise<void> {
const { peertubeHelpers } = options
const logger = peertubeHelpers.logger
logger.debug('Calling ensureProsodyRunning')
const r = await testProsodyCorrectlyRunning(options)
if (r.ok) {
r.messages.forEach(m => logger.debug(m))
logger.info('Prosody is already running correctly')
// Stop here. Nothing to change.
return
if (forceRestart) {
logger.info('We want to force Prosody restart, skip checking the current state')
} else {
const r = await testProsodyCorrectlyRunning(options)
if (r.ok) {
r.messages.forEach(m => logger.debug(m))
logger.info('Prosody is already running correctly')
// Stop here. Nothing to change.
return
}
logger.info('Prosody is not running correctly: ')
r.messages.forEach(m => logger.info(m))
}
logger.info('Prosody is not running correctly: ')
r.messages.forEach(m => logger.info(m))
// Shutting down...
logger.debug('Shutting down prosody')
@ -309,9 +319,30 @@ async function ensureProsodyRunning (options: RegisterServerOptions): Promise<vo
await ensureProsodyCertificates(options, config)
// launch prosody
const execCmd = filePaths.exec + (filePaths.execArgs.length ? ' ' + filePaths.execArgs.join(' ') : '')
logger.info('Going to launch prosody (' + execCmd + ')')
const prosody = child_process.exec(execCmd, {
let execArgs: string[] = filePaths.execArgs
if (restartProsodyInDebugMode) {
if (!filePaths.exec.includes('squashfs-root')) {
logger.error('Trying to enable the Prosody Debugger, but not using the AppImage. Cant work.')
} else {
const debuggerOptions = prosodyDebuggerOptions(options)
if (debuggerOptions) {
execArgs = [
'debug',
debuggerOptions.mobdebugPath,
debuggerOptions.mobdebugHost,
debuggerOptions.mobdebugPort,
...execArgs
]
}
}
}
logger.info(
'Going to launch prosody (' +
filePaths.exec +
(execArgs.length ? ' ' + execArgs.join(' ') : '') +
')'
)
const prosody = child_process.spawn(filePaths.exec, execArgs, {
cwd: filePaths.dir,
env: {
...process.env,

View File

@ -9,6 +9,8 @@ import { Affiliations, getVideoAffiliations, getChannelAffiliations } from '../p
import { getProsodyDomain } from '../prosody/config/domain'
import { fillVideoCustomFields } from '../custom-fields'
import { getChannelInfosById } from '../database/channel'
import { ensureProsodyRunning } from '../prosody/ctl'
import { isDebugMode } from '../debug'
// See here for description: https://modules.prosody.im/mod_muc_http_defaults.html
interface RoomDefaults {
@ -222,6 +224,33 @@ async function initApiRouter (options: RegisterServerOptions): Promise<Router> {
}
))
// router.get('/federation_server_infos', asyncMiddleware(
// async (req: Request, res: Response, _next: NextFunction) => {
// logger.info('federation_server_infos api call')
// // TODO/FIXME: return server infos.
// // TODO/FIXME: store these informations on the other side.
// res.json({ ok: true })
// }
// ))
if (isDebugMode(options)) {
// Only add this route if the debug mode is enabled at time of the server launch.
// Note: the isDebugMode will be tested again when the API is called.
// Note: we dont authenticate the user. We want this API to be callable from debug tools.
// This should not be an issue, as debug_mode should only be available on dev environments.
router.get('/restart_prosody', asyncMiddleware(
async (req: Request, res: Response, _next: NextFunction) => {
if (!isDebugMode(options)) {
res.json({ ok: false })
return
}
const restartProsodyInDebugMode = req.query.debugger === 'true'
await ensureProsodyRunning(options, true, restartProsodyInDebugMode)
res.json({ ok: true })
}
))
}
return router
}

View File

@ -16,6 +16,8 @@ import { getBoshUri, getWSUri } from '../uri/webchat'
import { getVideoLiveChatInfos } from '../federation/storage'
import { LiveChatJSONLDAttribute } from '../federation/types'
import { anonymousConnectionInfos, remoteAuthenticatedConnectionEnabled } from '../federation/connection-infos'
// import { XMPPWsProxyServer } from '../xmpp-ws-proxy/server'
// import { checkRemote } from '../xmpp-ws-proxy/check-remote'
import * as path from 'path'
const got = require('got')
@ -28,6 +30,7 @@ interface ProsodyProxyInfo {
let currentProsodyProxyInfo: ProsodyProxyInfo | null = null
let currentHttpBindProxy: ReturnType<typeof createProxyServer> | null = null
let currentWebsocketProxy: ReturnType<typeof createProxyServer> | null = null
let currentS2SWebsocketProxy: ReturnType<typeof createProxyServer> | null = null
async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Router> {
const {
@ -202,13 +205,9 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
page = page.replace(/{{REMOTE_BOSH_SERVICE_URL}}/g, remoteConnectionInfos?.anonymous?.boshUri ?? '')
page = page.replace(/{{REMOTE_WS_SERVICE_URL}}/g, remoteConnectionInfos?.anonymous?.wsUri ?? '')
page = page.replace(/{{REMOTE_ANONYMOUS_XMPP_SERVER}}/g, remoteConnectionInfos?.anonymous ? 'true' : 'false')
// Note: to be able to connect to remote XMPP server, with a local account,
// we must enable prosody-room-allow-s2s
// (which is required, so we can use outgoing S2S from the authenticated virtualhost).
// TODO: There should be another settings, rather than prosody-room-allow-s2s
page = page.replace(
/{{REMOTE_AUTHENTICATED_XMPP_SERVER}}/g,
settings['prosody-room-allow-s2s'] && remoteConnectionInfos?.authenticated ? 'true' : 'false'
remoteConnectionInfos?.authenticated ? 'true' : 'false'
)
page = page.replace(/{{AUTHENTICATION_URL}}/g, authenticationUrl)
page = page.replace(/{{AUTOVIEWERMODE}}/g, autoViewerMode ? 'true' : 'false')
@ -270,6 +269,29 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
currentWebsocketProxy.ws(request, socket, head)
}
})
registerWebSocketRoute({
route: '/xmpp-websocket-s2s',
handler: (request, socket, head) => {
if (!currentS2SWebsocketProxy) {
peertubeHelpers.logger.error('There is no current websocket s2s proxy, should not get here.')
// no need to close the socket, Peertube will
// (see https://github.com/Chocobozzz/PeerTube/issues/5752#issuecomment-1510870894)
return
}
currentS2SWebsocketProxy.ws(request, socket, head)
}
})
// registerWebSocketRoute({
// route: '/xmpp-websocket-proxy',
// handler: async (request, socket, head) => {
// const remoteInstanceUrl = request.headers['peertube-livechat-ws-proxy-instance-url']
// if (!await checkRemote(options, remoteInstanceUrl)) {
// return
// }
// XMPPWsProxyServer.singleton(options).handleUpgrade(request, socket, head)
// }
// })
}
router.get('/prosody-list-rooms', asyncMiddleware(
@ -345,6 +367,11 @@ async function disableProxyRoute ({ peertubeHelpers }: RegisterServerOptions): P
currentWebsocketProxy.close()
currentWebsocketProxy = null
}
if (currentS2SWebsocketProxy) {
peertubeHelpers.logger.info('Closing the s2s websocket proxy...')
currentS2SWebsocketProxy.close()
currentS2SWebsocketProxy = null
}
} catch (err) {
peertubeHelpers.logger.error('Seems that the http bind proxy close has failed: ' + (err as string))
}
@ -403,6 +430,28 @@ async function enableProxyRoute (
currentWebsocketProxy.on('close', () => {
logger.info('Got a close event for the websocket proxy')
})
logger.info('Creating a new s2s websocket proxy')
currentS2SWebsocketProxy = createProxyServer({
target: 'http://localhost:' + prosodyProxyInfo.port + '/xmpp-websocket-s2s',
ignorePath: true,
ws: true
})
currentS2SWebsocketProxy.on('error', (err, req, res) => {
// We must handle errors, otherwise Peertube server crashes!
logger.error(
'The s2s websocket proxy got an error ' +
'(this can be normal if you updated/uninstalled the plugin, or shutdowned peertube while users were chatting): ' +
err.message
)
if ('writeHead' in res) {
res.writeHead(500)
}
res.end('')
})
currentS2SWebsocketProxy.on('close', () => {
logger.info('Got a close event for the s2s websocket proxy')
})
}
interface WCRemoteConnectionInfos {

View File

@ -11,3 +11,9 @@ export function getWSUri (options: RegisterServerOptions): string | undefined {
if (base === undefined) { return undefined }
return base + 'xmpp-websocket'
}
export function getWSS2SUri (options: RegisterServerOptions): string | undefined {
const base = getBaseWebSocketRoute(options) // can be undefined if Peertube is too old
if (base === undefined) { return undefined }
return base + 'xmpp-websocket-s2s'
}

View File

@ -0,0 +1,73 @@
import type { RegisterServerOptions } from '@peertube/peertube-types'
// import { getBaseRouterRoute } from '../helpers'
// import { canonicalizePluginUri } from '../uri/canonicalize'
// import { URL } from 'url'
// const got = require('got')
/**
* FIXME: this method should not be necessary anymore, it was a proof of concept.
*
* This function checks that there is a valid Peertube instance behind
* the remote url, to avoid spoofing.
* It also ensure that we have needed serverInfos for the federation
* (so we can also open outgoing proxyfied connection to that instance)
* @param options server options
* @param remoteInstanceUrl remote instance url to check (as readed in the request header)
* @returns true if the remote instance is ok
*/
async function checkRemote (
_options: RegisterServerOptions,
_remoteInstanceUrl: any
): Promise<boolean> {
throw new Error('Not Implemented Yet')
// const logger = options.peertubeHelpers.logger
// if (typeof remoteInstanceUrl !== 'string') {
// logger.info('WS-Proxy-Check: Received invalid request on xmpp-websocket-proxy: invalid remoteInstanceUrl header')
// return false
// }
// logger.debug(
// `WS-Proxy-Check: Receiving request on xmpp-websocket-proxy for host ${remoteInstanceUrl}, ` +
// 'checking the host is a valid Peertube server'
// )
// let url: string
// try {
// const u = new URL(remoteInstanceUrl)
// // Assuming that the path on the remote instance is the same as on this one
// // (but canonicalized to remove the plugin version)
// u.pathname = getBaseRouterRoute(options) + 'api/federation_server_infos'
// url = canonicalizePluginUri(options, u.toString(), {
// protocol: 'http',
// removePluginVersion: true
// })
// } catch (_err) {
// logger.info('WS-Proxy-Check: Invalid remote instance url provided: ' + remoteInstanceUrl)
// return false
// }
// try {
// logger.debug('WS-Proxy-Check: We must check remote server infos using url: ' + url)
// const response = await got(url, {
// method: 'GET',
// headers: {},
// responseType: 'json'
// }).json()
// if (!response) {
// logger.info('WS-Proxy-Check: Invalid remote server options')
// return false
// }
// // FIXME/TODO
// return true
// } catch (_err) {
// logger.info('WS-Proxy-Check: Can\'t get remote instance informations using url ' + url)
// return false
// }
}
export {
checkRemote
}

View File

@ -0,0 +1,119 @@
// import type { RegisterServerOptions } from '@peertube/peertube-types'
// import type { IncomingMessage } from 'http'
// import type { Duplex } from 'stream'
// import { WebSocketServer, WebSocket } from 'ws'
// import { Socket } from 'net'
// FIXME: this method should not be necessary anymore, it was a proof of concept.
// interface ProxyLogger {
// debug: (s: string) => void
// info: (s: string) => void
// error: (s: string) => void
// }
// let xmppWsProxyServer: XMPPWsProxyServer | undefined
// class XMPPWsProxyServer {
// private readonly logger: ProxyLogger
// private readonly options: RegisterServerOptions
// private readonly wsProxyServer: WebSocketServer
// private prosodyPort: number | undefined
// private readonly connections: Map<WebSocket, Socket> = new Map<WebSocket, Socket>()
// constructor (options: RegisterServerOptions) {
// const logger = options.peertubeHelpers.logger
// this.logger = {
// debug: s => logger.debug('XMPP-WS-PROXY: ' + s),
// info: s => logger.info('XMPP-WS-PROXY: ' + s),
// error: s => logger.error('XMPP-WS-PROXY: ' + s)
// }
// this.options = options
// this.wsProxyServer = new WebSocketServer({ noServer: true, perMessageDeflate: false })
// }
// public handleUpgrade (request: IncomingMessage, incomingSocket: Duplex, head: Buffer): void {
// this.wsProxyServer.handleUpgrade(request, incomingSocket, head, ws => {
// this.handleUpgradeCallback(ws).then(() => {}, () => {})
// })
// }
// public async handleUpgradeCallback (ws: WebSocket): Promise<void> {
// this.logger.debug('Opening a Websocket Proxy connection')
// const port = await this.getProsodyPort()
// if (!port) {
// this.logger.error('No port configured for Prosody, closing the websocket stream')
// ws.close() // FIXME: add a code and error message
// return
// }
// // Opening a tcp connection to local Prosody:
// const prosodySocket = new Socket()
// this.connections.set(ws, prosodySocket)
// prosodySocket.connect(port, 'localhost', () => {
// // TODO: write the remote IP in the header line.
// prosodySocket.write('LIVECHAT-WS-PROXY\n')
// })
// prosodySocket.on('close', () => {
// ws.close()
// })
// prosodySocket.on('data', (data) => {
// ws.send(data)
// })
// ws.on('message', (data) => {
// // TODO: remove this log
// this.logger.debug('Receiving raw data')
// if (Array.isArray(data)) {
// data.forEach(chunck => {
// prosodySocket.write(chunck)
// })
// } else if (data instanceof ArrayBuffer) {
// prosodySocket.write(Buffer.from(data))
// } else {
// prosodySocket.write(data)
// }
// })
// ws.on('close', () => {
// this.logger.debug('Websocket connection is closed, closing socket')
// prosodySocket.end()
// })
// }
// private async getProsodyPort (): Promise<number> {
// if (this.prosodyPort) {
// return this.prosodyPort
// }
// const port = await this.options.settingsManager.getSetting('prosody-port') as string
// this.prosodyPort = parseInt(port)
// return this.prosodyPort
// }
// private async closeConnections (): Promise<void> {
// this.logger.debug('Closing XMPPWsProxyServer connections...')
// this.connections.forEach((socket, _ws) => {
// socket.end()
// // ws.terminate() // not necessary, socket close event should be called
// })
// // FIXME: wait for all connections to be closed...
// }
// static singleton (options: RegisterServerOptions): XMPPWsProxyServer {
// if (!xmppWsProxyServer) {
// xmppWsProxyServer = new XMPPWsProxyServer(options)
// }
// return xmppWsProxyServer
// }
// static async destroySingleton (): Promise<void> {
// if (!xmppWsProxyServer) { return }
// const server = xmppWsProxyServer
// xmppWsProxyServer = undefined
// await server.closeConnections()
// }
// }
// export {
// XMPPWsProxyServer
// }

View File

@ -78,6 +78,53 @@ To enable this mode, you juste have to create the
The simple existence of this file is sufficient to trigger the debug mode.
To make sure it's taken into account, you can restart your Peertube instance.
This file can contain some JSON to enable more advances options.
{{% notice warning %}}
Don't enable this mode on a production server, neither on a public server.
This could cause security issues.
{{% /notice %}}
### Restart Prosody
When debug mode is enabled, you can restart Prosody using this API call:
`http://your_instance.tld/plugins/livechat/router/api/restart_prosody`.
This call don't need any authentificaiton.
It can be done from a command line, for example using
`curl http://your_instance.tld/plugins/livechat/router/api/restart_prosody`.
### Prosody debugger
It is possible to connect the Prosody AppImage to a remote debugger using [MobDebug](https://luarocks.org/modules/paulclinger/mobdebug).
To do so, you have to setup MobDebug in a folder that can be accessed by the `peertube` user.
Then, add this in the `debub_mode` file:
```json
{
"debug_prosody": {
"debugger_path": "/the_path_to_mobdebug/src",
"host": "localhost",
"port": "8172"
}
}
```
`host` and `port` are optional. `debugger_path` must point to the folder where the `MobDebug` `.lua` file is.
Restart Peertube.
Start your debugger server.
For Prosody to connect to the debugger, call the API
`http://your_instance.tld/plugins/livechat/router/api/restart_prosody?debugger=true`.
This call does not need any authentication.
It can be done from a command line, for example with
`curl http://your_instance.tld/plugins/livechat/router/api/restart_prosody?debugger=true`.
You can even configure your debug server to launch this request automatically.
Prosody will then restart, connecting to the debugger.
## Quick dev environment using Docker
There is a tutorial, in french, on the

View File

@ -80,6 +80,53 @@ To enable this mode, you juste have to create the
The simple existence of this file is sufficient to trigger the debug mode.
To make sure it's taken into account, you can restart your Peertube instance.
This file can contain some JSON to enable more advances options.
{{% notice warning %}}
Don't enable this mode on a production server, neither on a public server.
This could cause security issues.
{{% /notice %}}
### Restart Prosody
When debug mode is enabled, you can restart Prosody using this API call:
`http://your_instance.tld/plugins/livechat/router/api/restart_prosody`.
This call don't need any authentificaiton.
It can be done from a command line, for example using
`curl http://your_instance.tld/plugins/livechat/router/api/restart_prosody`.
### Prosody debugger
It is possible to connect the Prosody AppImage to a remote debugger using [MobDebug](https://luarocks.org/modules/paulclinger/mobdebug).
To do so, you have to setup MobDebug in a folder that can be accessed by the `peertube` user.
Then, add this in the `debub_mode` file:
```json
{
"debug_prosody": {
"debugger_path": "/the_path_to_mobdebug/src",
"host": "localhost",
"port": "8172"
}
}
```
`host` and `port` are optional. `debugger_path` must point to the folder where the `MobDebug` `.lua` file is.
Restart Peertube.
Start your debugger server.
For Prosody to connect to the debugger, call the API
`http://your_instance.tld/plugins/livechat/router/api/restart_prosody?debugger=true`.
This call does not need any authentication.
It can be done from a command line, for example with
`curl http://your_instance.tld/plugins/livechat/router/api/restart_prosody?debugger=true`.
You can even configure your debug server to launch this request automatically.
Prosody will then restart, connecting to the debugger.
## Quick dev environment using Docker
There is a tutorial, in french, on the
@ -87,3 +134,8 @@ There is a tutorial, in french, on the
that explains how to quickly build a dev env using Docker.
A repo was made out of it, check out https://codeberg.org/mose/pt-plugin-dev
Note: for an unknown reason, Prosody can't resolve containers DNS address when using the lua-unbound library.
There is a dirty hack in the plugin: just create a
`/data/plugins/data/peertube-plugin-livechat/no_lua_unbound` file in your docker-volumes,
then restart containers.

View File

@ -79,9 +79,63 @@ Pour activer ce mode, il suffit de créer un fichier
La simple existance de ce fichier suffit à déclencher le mode debug.
Pour être sûr qu'il est pris en compte, vous pouvez redémarrer votre instance Peertube.
Ce fichier peut également contenir du JSON qui pourra activer d'autres options.
{{% notice warning %}}
N'activer jamais ce mode sur un serveur de production, ni même sur un serveur public.
Cela pourrait poser des problèmes de sécurité.
{{% /notice %}}
### Redémarrer Prosody
Pour redémarrer Prosody quand le mode debug est activé, vous pouvez appeler l'API
`http://votre_instance.tld/plugins/livechat/router/api/restart_prosody`.
Cet appel n'a pas besoin d'authentification.
Il peut se faire depuis une ligne de commande, par exemple avec
`curl http://votre_instance.tld/plugins/livechat/router/api/restart_prosody`.
### Prosody debugger
Il est possible de connecter l'AppImage Prosody à un debugger distant en utilisant
[MobDebug](https://luarocks.org/modules/paulclinger/mobdebug).
Pour cela, placer MobDebug dans un dossier accessible par le user `peertube`.
Ensuite, ajouter cela dans le fichier `debug_mode` du plugin:
```json
{
"debug_prosody": {
"debugger_path": "/the_path_to_mobdebug/src",
"host": "localhost",
"port": "8172"
}
}
```
`host` et `port` sont optionnels. `debugger_path` doit pointer vers le dossier où
se trouve le fichier `.lua` de `MobDebug`.
Redémarrer Peertube.
Lancer votre serveur de debug.
Pour que Prosody se connecte au debugger, appelez l'API
`http://votre_instance.tld/plugins/livechat/router/api/restart_prosody?debugger=true`.
Cet appel n'a pas besoin d'authentification.
Il peut se faire depuis une ligne de commande, par exemple avec
`curl http://votre_instance.tld/plugins/livechat/router/api/restart_prosody?debugger=true`.
Vous pouvez même configurer votre serveur de debuggage pour lancer cette commande
automatiquement.
Prosody va alors redémarrer en se connectant au debugger.
## Environnement de développement rapide via Docker
Un tutoriel est disponible sur [le forum Peertube](https://framacolibri.org/t/tutoriel-creer-un-environnement-de-developpement-de-plugin-peertube-rapidement-en-utilisant-docker-et-qui-permet-de-tester-la-federation/17631)
pour expliquer comment monter rapidement un environnement de développement en utilisant Docker.
Un dépot a été crée sur la base de ce tutoriel: https://codeberg.org/mose/pt-plugin-dev
Note: pour une raison obscure, Prosody n'arrive pas à résoudre les adresses DNS des conteneurs quand la librairie
lua-unbound est utilisée. Pour contourner cela, il y a un «dirty hack»: il suffit de créer une fichier
`/data/plugins/data/peertube-plugin-livechat/no_lua_unbound` dans vos docker-volumes, puis de les redémarrer.