From 9a2da60b7de68c310dc84717709a440935bf56a3 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Fri, 19 May 2023 12:52:52 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 16 +- package-lock.json | 56 +-- .../mod_random_vcard_peertubelivechat.lua | 2 +- .../mod_s2s_peertubelivechat/README.md | 5 + .../mod_s2s_peertubelivechat.lua | 247 +++++++++ .../mod_websocket_s2s_peertubelivechat.lua | 471 ++++++++++++++++++ prosody/appimage_aarch64.yml | 7 +- prosody/appimage_x86_64.yml | 7 +- prosody/launcher.lua | 13 + server/lib/debug.ts | 86 ++++ server/lib/federation/connection-infos.ts | 1 + server/lib/federation/incoming.ts | 5 +- server/lib/federation/outgoing.ts | 21 +- server/lib/federation/sanitize.ts | 38 +- server/lib/federation/storage.ts | 78 ++- server/lib/federation/types.ts | 15 +- server/lib/prosody/config.ts | 86 ++-- server/lib/prosody/config/content.ts | 50 +- server/lib/prosody/ctl.ts | 55 +- server/lib/routers/api.ts | 29 ++ server/lib/routers/webchat.ts | 59 ++- server/lib/uri/webchat.ts | 6 + server/lib/xmpp-ws-proxy/check-remote.ts | 73 +++ server/lib/xmpp-ws-proxy/server.ts | 119 +++++ .../content/contributing/develop/_index.de.md | 47 ++ .../content/contributing/develop/_index.en.md | 52 ++ .../content/contributing/develop/_index.fr.md | 54 ++ 27 files changed, 1592 insertions(+), 106 deletions(-) create mode 100644 prosody-modules/mod_s2s_peertubelivechat/README.md create mode 100644 prosody-modules/mod_s2s_peertubelivechat/mod_s2s_peertubelivechat.lua create mode 100644 prosody-modules/mod_websocket_s2s_peertubelivechat/mod_websocket_s2s_peertubelivechat.lua create mode 100644 server/lib/xmpp-ws-proxy/check-remote.ts create mode 100644 server/lib/xmpp-ws-proxy/server.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fcdaec6..16a3fd0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 83a185ba..7dae2ac5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/prosody-modules/mod_random_vcard_peertubelivechat/mod_random_vcard_peertubelivechat.lua b/prosody-modules/mod_random_vcard_peertubelivechat/mod_random_vcard_peertubelivechat.lua index 1355ff8f..e1f08a8c 100644 --- a/prosody-modules/mod_random_vcard_peertubelivechat/mod_random_vcard_peertubelivechat.lua +++ b/prosody-modules/mod_random_vcard_peertubelivechat/mod_random_vcard_peertubelivechat.lua @@ -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"; diff --git a/prosody-modules/mod_s2s_peertubelivechat/README.md b/prosody-modules/mod_s2s_peertubelivechat/README.md new file mode 100644 index 00000000..bda8723c --- /dev/null +++ b/prosody-modules/mod_s2s_peertubelivechat/README.md @@ -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. diff --git a/prosody-modules/mod_s2s_peertubelivechat/mod_s2s_peertubelivechat.lua b/prosody-modules/mod_s2s_peertubelivechat/mod_s2s_peertubelivechat.lua new file mode 100644 index 00000000..92f64aa9 --- /dev/null +++ b/prosody-modules/mod_s2s_peertubelivechat/mod_s2s_peertubelivechat.lua @@ -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 diff --git a/prosody-modules/mod_websocket_s2s_peertubelivechat/mod_websocket_s2s_peertubelivechat.lua b/prosody-modules/mod_websocket_s2s_peertubelivechat/mod_websocket_s2s_peertubelivechat.lua new file mode 100644 index 00000000..f1fa1f4b --- /dev/null +++ b/prosody-modules/mod_websocket_s2s_peertubelivechat/mod_websocket_s2s_peertubelivechat.lua @@ -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, 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 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 ""; 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 = [[Websocket +

]]..websocket_get_response_text..[[

+]] +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 ""; + 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 diff --git a/prosody/appimage_aarch64.yml b/prosody/appimage_aarch64.yml index 77e5d88d..68a31117 100644 --- a/prosody/appimage_aarch64.yml +++ b/prosody/appimage_aarch64.yml @@ -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/ diff --git a/prosody/appimage_x86_64.yml b/prosody/appimage_x86_64.yml index 0fe689d0..aa539b51 100644 --- a/prosody/appimage_x86_64.yml +++ b/prosody/appimage_x86_64.yml @@ -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/ diff --git a/prosody/launcher.lua b/prosody/launcher.lua index 67fb0a5c..f028adf5 100644 --- a/prosody/launcher.lua +++ b/prosody/launcher.lua @@ -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 diff --git a/server/lib/debug.ts b/server/lib/debug.ts index 10408939..5bfb0e7f 100644 --- a/server/lib/debug.ts +++ b/server/lib/debug.ts @@ -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) + } +} diff --git a/server/lib/federation/connection-infos.ts b/server/lib/federation/connection-infos.ts index 1988380a..2543f7d9 100644 --- a/server/lib/federation/connection-infos.ts +++ b/server/lib/federation/connection-infos.ts @@ -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 diff --git a/server/lib/federation/incoming.ts b/server/lib/federation/incoming.ts index 1e697ee5..adfef7e7 100644 --- a/server/lib/federation/incoming.ts +++ b/server/lib/federation/incoming.ts @@ -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 { diff --git a/server/lib/federation/outgoing.ts b/server/lib/federation/outgoing.ts index 681d9b9e..11cb6b6a 100644 --- a/server/lib/federation/outgoing.ts +++ b/server/lib/federation/outgoing.ts @@ -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']) { diff --git a/server/lib/federation/sanitize.ts b/server/lib/federation/sanitize.ts index 9e6666ff..5a27c2c8 100644 --- a/server/lib/federation/sanitize.ts +++ b/server/lib/federation/sanitize.ts @@ -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 } diff --git a/server/lib/federation/storage.ts b/server/lib/federation/storage.ts index 715b161f..489f7739 100644 --- a/server/lib/federation/storage.ts +++ b/server/lib/federation/storage.ts @@ -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 { + 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 { 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 } diff --git a/server/lib/federation/types.ts b/server/lib/federation/types.ts index 0dc735b7..1b9292cb 100644 --- a/server/lib/federation/types.ts +++ b/server/lib/federation/types.ts @@ -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, diff --git a/server/lib/prosody/config.ts b/server/lib/prosody/config.ts index 66e42fcf..46111f25 100644 --- a/server/lib/prosody/config.ts +++ b/server/lib/prosody/config.ts @@ -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 { const peertubeHelpers = options.peertubeHelpers @@ -64,26 +65,24 @@ async function getProsodyFilePaths (options: RegisterServerOptions): Promise() @@ -151,23 +151,27 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise=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 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) diff --git a/server/lib/prosody/config/content.ts b/server/lib/prosody/config/content.ts index 1cf32718..1c894ebd 100644 --- a/server/lib/prosody/config/content.ts +++ b/server/lib/prosody/config/content.ts @@ -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 { diff --git a/server/lib/prosody/ctl.ts b/server/lib/prosody/ctl.ts index 188d7988..0b0b4582 100644 --- a/server/lib/prosody/ctl.ts +++ b/server/lib/prosody/ctl.ts @@ -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 { }) 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 { +async function ensureProsodyRunning ( + options: RegisterServerOptions, + forceRestart?: boolean, + restartProsodyInDebugMode?: boolean +): Promise { 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 { } )) + // 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 } diff --git a/server/lib/routers/webchat.ts b/server/lib/routers/webchat.ts index 7404d755..dae3a2c8 100644 --- a/server/lib/routers/webchat.ts +++ b/server/lib/routers/webchat.ts @@ -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 | null = null let currentWebsocketProxy: ReturnType | null = null +let currentS2SWebsocketProxy: ReturnType | null = null async function initWebchatRouter (options: RegisterServerOptionsV5): Promise { const { @@ -202,13 +205,9 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise { + 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 { diff --git a/server/lib/uri/webchat.ts b/server/lib/uri/webchat.ts index c669a327..d63d07bf 100644 --- a/server/lib/uri/webchat.ts +++ b/server/lib/uri/webchat.ts @@ -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' +} diff --git a/server/lib/xmpp-ws-proxy/check-remote.ts b/server/lib/xmpp-ws-proxy/check-remote.ts new file mode 100644 index 00000000..27f14279 --- /dev/null +++ b/server/lib/xmpp-ws-proxy/check-remote.ts @@ -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 { + 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 +} diff --git a/server/lib/xmpp-ws-proxy/server.ts b/server/lib/xmpp-ws-proxy/server.ts new file mode 100644 index 00000000..f29130dd --- /dev/null +++ b/server/lib/xmpp-ws-proxy/server.ts @@ -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 = new Map() + +// 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 { +// 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 { +// 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 { +// 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 { +// if (!xmppWsProxyServer) { return } +// const server = xmppWsProxyServer +// xmppWsProxyServer = undefined +// await server.closeConnections() +// } +// } + +// export { +// XMPPWsProxyServer +// } diff --git a/support/documentation/content/contributing/develop/_index.de.md b/support/documentation/content/contributing/develop/_index.de.md index d09ffbf3..7c2fe496 100644 --- a/support/documentation/content/contributing/develop/_index.de.md +++ b/support/documentation/content/contributing/develop/_index.de.md @@ -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 diff --git a/support/documentation/content/contributing/develop/_index.en.md b/support/documentation/content/contributing/develop/_index.en.md index 02e44268..3f2a22f3 100644 --- a/support/documentation/content/contributing/develop/_index.en.md +++ b/support/documentation/content/contributing/develop/_index.en.md @@ -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. diff --git a/support/documentation/content/contributing/develop/_index.fr.md b/support/documentation/content/contributing/develop/_index.fr.md index 7cce0e0a..5ddc2fdf 100644 --- a/support/documentation/content/contributing/develop/_index.fr.md +++ b/support/documentation/content/contributing/develop/_index.fr.md @@ -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.