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:
parent
1174f661be
commit
9a2da60b7d
16
CHANGELOG.md
16
CHANGELOG.md
@ -4,9 +4,23 @@
|
|||||||
|
|
||||||
### New Features
|
### 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: 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
|
## 6.3.0
|
||||||
|
|
||||||
|
56
package-lock.json
generated
56
package-lock.json
generated
@ -4787,6 +4787,27 @@
|
|||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/enquirer": {
|
||||||
"version": "2.3.6",
|
"version": "2.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
|
||||||
@ -10417,27 +10438,6 @@
|
|||||||
"typedarray-to-buffer": "^3.1.5"
|
"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": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||||
"dev": true
|
"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"
|
"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": {
|
"xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
5
prosody-modules/mod_s2s_peertubelivechat/README.md
Normal file
5
prosody-modules/mod_s2s_peertubelivechat/README.md
Normal 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.
|
@ -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
|
@ -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
|
@ -64,7 +64,12 @@ AppDir:
|
|||||||
# Note: this assume that peertube-plugin-livechat is not in a subdir of one of following mappings.
|
# Note: this assume that peertube-plugin-livechat is not in a subdir of one of following mappings.
|
||||||
# This seems a reasonable assumption.
|
# This seems a reasonable assumption.
|
||||||
path_mappings:
|
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/
|
- /lib/:$APPDIR/lib/
|
||||||
- /lib64/:$APPDIR/lib64/
|
- /lib64/:$APPDIR/lib64/
|
||||||
- /runtime/:$APPDIR/runtime/
|
- /runtime/:$APPDIR/runtime/
|
||||||
|
@ -58,7 +58,12 @@ AppDir:
|
|||||||
# Note: this assume that peertube-plugin-livechat is not in a subdir of one of following mappings.
|
# Note: this assume that peertube-plugin-livechat is not in a subdir of one of following mappings.
|
||||||
# This seems a reasonable assumption.
|
# This seems a reasonable assumption.
|
||||||
path_mappings:
|
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/
|
- /lib/:$APPDIR/lib/
|
||||||
- /lib64/:$APPDIR/lib64/
|
- /lib64/:$APPDIR/lib64/
|
||||||
- /runtime/:$APPDIR/runtime/
|
- /runtime/:$APPDIR/runtime/
|
||||||
|
@ -3,6 +3,19 @@
|
|||||||
-- This file is a launcher, that takes the first argument to choose what to launch.
|
-- This file is a launcher, that takes the first argument to choose what to launch.
|
||||||
|
|
||||||
local what = table.remove(arg, 1);
|
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
|
if what == 'prosody' then
|
||||||
dofile('/usr/bin/prosody');
|
dofile('/usr/bin/prosody');
|
||||||
elseif what == 'prosodyctl' then
|
elseif what == 'prosodyctl' then
|
||||||
|
@ -2,6 +2,11 @@ import type { RegisterServerOptions } from '@peertube/peertube-types'
|
|||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as fs from 'fs'
|
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 {
|
export function isDebugMode (options: RegisterServerOptions): boolean {
|
||||||
const peertubeHelpers = options.peertubeHelpers
|
const peertubeHelpers = options.peertubeHelpers
|
||||||
const logger = peertubeHelpers.logger
|
const logger = peertubeHelpers.logger
|
||||||
@ -17,3 +22,84 @@ export function isDebugMode (options: RegisterServerOptions): boolean {
|
|||||||
}
|
}
|
||||||
return false
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -36,6 +36,7 @@ function remoteAuthenticatedConnectionEnabled (livechatInfos: LiveChatJSONLDAttr
|
|||||||
if (!livechatInfos.links) { return false }
|
if (!livechatInfos.links) { return false }
|
||||||
if (livechatInfos.type !== 'xmpp') { return false }
|
if (livechatInfos.type !== 'xmpp') { return false }
|
||||||
for (const link of livechatInfos.links) {
|
for (const link of livechatInfos.links) {
|
||||||
|
if (link.type === 'xmpp-peertube-livechat-ws-s2s') { return true }
|
||||||
if (link.type === 'xmpp-s2s') { return true }
|
if (link.type === 'xmpp-s2s') { return true }
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { RegisterServerOptions } from '@peertube/peertube-types'
|
import type { RegisterServerOptions } from '@peertube/peertube-types'
|
||||||
import type { RemoteVideoHandlerParams } from './types'
|
import type { RemoteVideoHandlerParams } from './types'
|
||||||
import { storeVideoLiveChatInfos } from './storage'
|
import { storeVideoLiveChatInfos, storeRemoteServerInfos } from './storage'
|
||||||
import { sanitizePeertubeLiveChatInfos } from './sanitize'
|
import { sanitizePeertubeLiveChatInfos } from './sanitize'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,6 +19,9 @@ async function readIncomingAPVideo (
|
|||||||
peertubeLiveChat = sanitizePeertubeLiveChatInfos(peertubeLiveChat)
|
peertubeLiveChat = sanitizePeertubeLiveChatInfos(peertubeLiveChat)
|
||||||
|
|
||||||
await storeVideoLiveChatInfos(options, video, peertubeLiveChat)
|
await storeVideoLiveChatInfos(options, video, peertubeLiveChat)
|
||||||
|
if (video.remote) {
|
||||||
|
await storeRemoteServerInfos(options, peertubeLiveChat)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -2,7 +2,7 @@ import type { RegisterServerOptions, VideoObject } from '@peertube/peertube-type
|
|||||||
import type { LiveChatVideoObject, VideoBuildResultContext, LiveChatJSONLDLink, LiveChatJSONLDAttribute } from './types'
|
import type { LiveChatVideoObject, VideoBuildResultContext, LiveChatJSONLDLink, LiveChatJSONLDAttribute } from './types'
|
||||||
import { storeVideoLiveChatInfos } from './storage'
|
import { storeVideoLiveChatInfos } from './storage'
|
||||||
import { videoHasWebchat } from '../../../shared/lib/video'
|
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 { canonicalizePluginUri } from '../uri/canonicalize'
|
||||||
import { getProsodyDomain } from '../prosody/config/domain'
|
import { getProsodyDomain } from '../prosody/config/domain'
|
||||||
import { fillVideoCustomFields } from '../custom-fields'
|
import { fillVideoCustomFields } from '../custom-fields'
|
||||||
@ -32,7 +32,8 @@ async function videoBuildJSONLD (
|
|||||||
'prosody-room-type',
|
'prosody-room-type',
|
||||||
'federation-dont-publish-remotely',
|
'federation-dont-publish-remotely',
|
||||||
'chat-no-anonymous',
|
'chat-no-anonymous',
|
||||||
'prosody-room-allow-s2s'
|
'prosody-room-allow-s2s',
|
||||||
|
'prosody-s2s-port'
|
||||||
])
|
])
|
||||||
|
|
||||||
if (settings['federation-dont-publish-remotely']) {
|
if (settings['federation-dont-publish-remotely']) {
|
||||||
@ -71,9 +72,23 @@ async function videoBuildJSONLD (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const links: LiveChatJSONLDLink[] = []
|
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']) {
|
if (settings['prosody-room-allow-s2s']) {
|
||||||
links.push({
|
links.push({
|
||||||
type: 'xmpp-s2s'
|
type: 'xmpp-s2s',
|
||||||
|
host: prosodyDomain,
|
||||||
|
port: (settings['prosody-s2s-port'] as string) ?? ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (!settings['chat-no-anonymous']) {
|
if (!settings['chat-no-anonymous']) {
|
||||||
|
@ -37,8 +37,34 @@ function sanitizePeertubeLiveChatInfos (chatInfos: any): LiveChatJSONLDAttribute
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (link.type === 'xmpp-s2s') {
|
if (link.type === 'xmpp-s2s') {
|
||||||
|
if (!/^\d+$/.test(link.port)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const host = _validateHost(link.host)
|
||||||
|
if (!host) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
r.links.push({
|
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
|
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 {
|
export {
|
||||||
sanitizePeertubeLiveChatInfos
|
sanitizePeertubeLiveChatInfos
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { RegisterServerOptions, MVideoFullLight, MVideoAP, Video, MVideoThumbnail } from '@peertube/peertube-types'
|
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 { sanitizePeertubeLiveChatInfos } from './sanitize'
|
||||||
import { URL } from 'url'
|
import { URL } from 'url'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
@ -97,6 +97,60 @@ async function getVideoLiveChatInfos (
|
|||||||
return r
|
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 (
|
async function _getFilePath (
|
||||||
options: RegisterServerOptions,
|
options: RegisterServerOptions,
|
||||||
remote: boolean,
|
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> {
|
async function _store (options: RegisterServerOptions, filePath: string, content: any): Promise<void> {
|
||||||
const logger = options.peertubeHelpers.logger
|
const logger = options.peertubeHelpers.logger
|
||||||
try {
|
try {
|
||||||
|
const jsonContent = JSON.stringify(content)
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
const dir = path.dirname(filePath)
|
const dir = path.dirname(filePath)
|
||||||
if (!fs.existsSync(dir)) {
|
if (!fs.existsSync(dir)) {
|
||||||
fs.mkdirSync(dir, { recursive: true })
|
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'
|
encoding: 'utf-8'
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} 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 {
|
export {
|
||||||
storeVideoLiveChatInfos,
|
storeVideoLiveChatInfos,
|
||||||
getVideoLiveChatInfos
|
storeRemoteServerInfos,
|
||||||
|
getVideoLiveChatInfos,
|
||||||
|
getRemoteServerInfosDir
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,15 @@ interface VideoBuildResultContext {
|
|||||||
video: MVideoAP
|
video: MVideoAP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LiveChatJSONLDPeertubeWSS2SLink {
|
||||||
|
type: 'xmpp-peertube-livechat-ws-s2s'
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
interface LiveChatJSONLDS2SLink {
|
interface LiveChatJSONLDS2SLink {
|
||||||
type: 'xmpp-s2s'
|
type: 'xmpp-s2s'
|
||||||
|
host: string
|
||||||
|
port: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LiveChatJSONLDAnonymousWebsocketLink {
|
interface LiveChatJSONLDAnonymousWebsocketLink {
|
||||||
@ -20,7 +27,11 @@ interface LiveChatJSONLDAnonymousBOSHLink {
|
|||||||
jid: string
|
jid: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type LiveChatJSONLDLink = LiveChatJSONLDS2SLink | LiveChatJSONLDAnonymousBOSHLink | LiveChatJSONLDAnonymousWebsocketLink
|
type LiveChatJSONLDLink =
|
||||||
|
LiveChatJSONLDPeertubeWSS2SLink
|
||||||
|
| LiveChatJSONLDS2SLink
|
||||||
|
| LiveChatJSONLDAnonymousBOSHLink
|
||||||
|
| LiveChatJSONLDAnonymousWebsocketLink
|
||||||
|
|
||||||
interface LiveChatJSONLDInfos {
|
interface LiveChatJSONLDInfos {
|
||||||
type: 'xmpp'
|
type: 'xmpp'
|
||||||
@ -42,6 +53,8 @@ interface RemoteVideoHandlerParams {
|
|||||||
export {
|
export {
|
||||||
VideoBuildResultContext,
|
VideoBuildResultContext,
|
||||||
LiveChatJSONLDLink,
|
LiveChatJSONLDLink,
|
||||||
|
LiveChatJSONLDS2SLink,
|
||||||
|
LiveChatJSONLDPeertubeWSS2SLink,
|
||||||
LiveChatJSONLDInfos,
|
LiveChatJSONLDInfos,
|
||||||
LiveChatJSONLDAttribute,
|
LiveChatJSONLDAttribute,
|
||||||
LiveChatVideoObject,
|
LiveChatVideoObject,
|
||||||
|
@ -8,6 +8,7 @@ import { ConfigLogExpiration, ProsodyConfigContent } from './config/content'
|
|||||||
import { getProsodyDomain } from './config/domain'
|
import { getProsodyDomain } from './config/domain'
|
||||||
import { getAPIKey } from '../apikey'
|
import { getAPIKey } from '../apikey'
|
||||||
import { parseExternalComponents } from './config/components'
|
import { parseExternalComponents } from './config/components'
|
||||||
|
import { getRemoteServerInfosDir } from '../federation/storage'
|
||||||
|
|
||||||
async function getWorkingDir (options: RegisterServerOptions): Promise<string> {
|
async function getWorkingDir (options: RegisterServerOptions): Promise<string> {
|
||||||
const peertubeHelpers = options.peertubeHelpers
|
const peertubeHelpers = options.peertubeHelpers
|
||||||
@ -64,8 +65,7 @@ async function getProsodyFilePaths (options: RegisterServerOptions): Promise<Pro
|
|||||||
|
|
||||||
let certsDir: string | undefined = path.resolve(dir, 'certs')
|
let certsDir: string | undefined = path.resolve(dir, 'certs')
|
||||||
let certsDirIsCustom = false
|
let certsDirIsCustom = false
|
||||||
if (settings['prosody-room-allow-s2s']) {
|
if (settings['prosody-room-allow-s2s'] && (settings['prosody-certificates-dir'] as string ?? '') !== '') {
|
||||||
if ((settings['prosody-certificates-dir'] as string ?? '') !== '') {
|
|
||||||
if (!fs.statSync(settings['prosody-certificates-dir'] as string).isDirectory()) {
|
if (!fs.statSync(settings['prosody-certificates-dir'] as string).isDirectory()) {
|
||||||
// We can throw an exception here...
|
// We can throw an exception here...
|
||||||
// Because if the user input a wrong directory, the plugin will not register,
|
// Because if the user input a wrong directory, the plugin will not register,
|
||||||
@ -84,7 +84,6 @@ async function getProsodyFilePaths (options: RegisterServerOptions): Promise<Pro
|
|||||||
// So we will use this dir as the certs dir.
|
// So we will use this dir as the certs dir.
|
||||||
certsDir = path.resolve(dir, 'data')
|
certsDir = path.resolve(dir, 'data')
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dir: dir,
|
dir: dir,
|
||||||
@ -139,7 +138,8 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
|
|||||||
'prosody-components',
|
'prosody-components',
|
||||||
'prosody-components-port',
|
'prosody-components-port',
|
||||||
'prosody-components-list',
|
'prosody-components-list',
|
||||||
'chat-no-anonymous'
|
'chat-no-anonymous',
|
||||||
|
'federation-dont-publish-remotely'
|
||||||
])
|
])
|
||||||
|
|
||||||
const valuesToHideInDiagnostic = new Map<string, string>()
|
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 disableAnon = (settings['chat-no-anonymous'] as boolean) || false
|
||||||
const logExpirationSetting = (settings['prosody-muc-expiration'] as string) ?? DEFAULTLOGEXPIRATION
|
const logExpirationSetting = (settings['prosody-muc-expiration'] as string) ?? DEFAULTLOGEXPIRATION
|
||||||
const enableC2S = (settings['prosody-c2s'] as boolean) || false
|
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 enableRoomS2S = (settings['prosody-room-allow-s2s'] as boolean) || false
|
||||||
const enableComponents = (settings['prosody-components'] as boolean) || false
|
const enableComponents = (settings['prosody-components'] as boolean) || false
|
||||||
const prosodyDomain = await getProsodyDomain(options)
|
const prosodyDomain = await getProsodyDomain(options)
|
||||||
const paths = await getProsodyFilePaths(options)
|
const paths = await getProsodyFilePaths(options)
|
||||||
const roomType = settings['prosody-room-type'] === 'channel' ? 'channel' : 'video'
|
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
|
let certificates: ProsodyConfigCertificates = false
|
||||||
|
|
||||||
const apikey = await getAPIKey(options)
|
const apikey = await getAPIKey(options)
|
||||||
valuesToHideInDiagnostic.set('APIKey', apikey)
|
valuesToHideInDiagnostic.set('APIKey', apikey)
|
||||||
|
|
||||||
|
const publicServerUrl = options.peertubeHelpers.config.getWebserverUrl()
|
||||||
|
|
||||||
let basePeertubeUrl = settings['prosody-peertube-uri'] as string
|
let basePeertubeUrl = settings['prosody-peertube-uri'] as string
|
||||||
if (basePeertubeUrl && !/^https?:\/\/[a-z0-9.-_]+(?::\d+)?$/.test(basePeertubeUrl)) {
|
if (basePeertubeUrl && !/^https?:\/\/[a-z0-9.-_]+(?::\d+)?$/.test(basePeertubeUrl)) {
|
||||||
throw new Error('Invalid prosody-peertube-uri')
|
throw new Error('Invalid prosody-peertube-uri')
|
||||||
}
|
}
|
||||||
if (!basePeertubeUrl) {
|
if (!basePeertubeUrl) {
|
||||||
basePeertubeUrl = options.peertubeHelpers.config.getWebserverUrl()
|
basePeertubeUrl = publicServerUrl
|
||||||
}
|
}
|
||||||
const baseApiUrl = basePeertubeUrl + getBaseRouterRoute(options) + 'api/'
|
const baseApiUrl = basePeertubeUrl + getBaseRouterRoute(options) + 'api/'
|
||||||
|
|
||||||
@ -181,7 +185,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
|
|||||||
}
|
}
|
||||||
config.useHttpAuthentication(authApiUrl)
|
config.useHttpAuthentication(authApiUrl)
|
||||||
const useWS = !!options.registerWebSocketRoute // this comes with Peertube >=5.0.0, and is a prerequisite to websocket
|
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)
|
config.useMucHttpDefault(roomApiUrl)
|
||||||
|
|
||||||
if (enableC2S) {
|
if (enableC2S) {
|
||||||
@ -204,16 +208,18 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
|
|||||||
config.useExternalComponents(componentsPort, components)
|
config.useExternalComponents(componentsPort, components)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableRoomS2S || enableUserS2S) {
|
if (enableRoomS2S || enableRemoteChatConnections) {
|
||||||
certificates = 'generate-self-signed'
|
certificates = 'generate-self-signed'
|
||||||
if (config.paths.certsDirIsCustom) {
|
if (config.paths.certsDirIsCustom) {
|
||||||
certificates = 'use-from-dir'
|
certificates = 'use-from-dir'
|
||||||
}
|
}
|
||||||
const s2sPort = (settings['prosody-s2s-port'] as string) || '5269'
|
let s2sPort, s2sInterfaces
|
||||||
|
if (enableRoomS2S) {
|
||||||
|
s2sPort = (settings['prosody-s2s-port'] as string) || '5269'
|
||||||
if (!/^\d+$/.test(s2sPort)) {
|
if (!/^\d+$/.test(s2sPort)) {
|
||||||
throw new Error('Invalid s2s port')
|
throw new Error('Invalid s2s port')
|
||||||
}
|
}
|
||||||
const s2sInterfaces = ((settings['prosody-s2s-interfaces'] as string) || '')
|
s2sInterfaces = ((settings['prosody-s2s-interfaces'] as string) || '')
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(s => s.trim())
|
.map(s => s.trim())
|
||||||
// Check that there is no invalid values (to avoid injections):
|
// Check that there is no invalid values (to avoid injections):
|
||||||
@ -224,7 +230,11 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
|
|||||||
if (networkInterface.match(/^[a-f0-9:]+$/)) return
|
if (networkInterface.match(/^[a-f0-9:]+$/)) return
|
||||||
throw new Error('Invalid s2s interfaces')
|
throw new Error('Invalid s2s interfaces')
|
||||||
})
|
})
|
||||||
config.useS2S(s2sPort, s2sInterfaces, !enableUserS2S)
|
} else {
|
||||||
|
s2sPort = null
|
||||||
|
s2sInterfaces = null
|
||||||
|
}
|
||||||
|
config.useS2S(s2sPort, s2sInterfaces, publicServerUrl, getRemoteServerInfosDir(options))
|
||||||
}
|
}
|
||||||
|
|
||||||
const logExpiration = readLogExpiration(options, logExpirationSetting)
|
const logExpiration = readLogExpiration(options, logExpirationSetting)
|
||||||
|
@ -72,6 +72,18 @@ abstract class ProsodyConfigBlock {
|
|||||||
this.entries.set(name, entry)
|
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 {
|
write (): string {
|
||||||
let content = ''
|
let content = ''
|
||||||
// Map keeps order :)
|
// Map keeps order :)
|
||||||
@ -258,17 +270,35 @@ class ProsodyConfigContent {
|
|||||||
this.global.set('c2s_ports', [c2sPort])
|
this.global.set('c2s_ports', [c2sPort])
|
||||||
}
|
}
|
||||||
|
|
||||||
useS2S (s2sPort: string, s2sInterfaces: string[], mucOnly: boolean): void {
|
useS2S (
|
||||||
|
s2sPort: string | null,
|
||||||
|
s2sInterfaces: string[] | null,
|
||||||
|
publicServerUrl: string,
|
||||||
|
serverInfosDir: string
|
||||||
|
): void {
|
||||||
|
if (s2sPort !== null) {
|
||||||
this.global.set('s2s_ports', [s2sPort])
|
this.global.set('s2s_ports', [s2sPort])
|
||||||
this.global.set('s2s_interfaces', s2sInterfaces)
|
} else {
|
||||||
this.global.set('s2s_secure_auth', false)
|
this.global.set('s2s_ports', [])
|
||||||
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!
|
|
||||||
}
|
}
|
||||||
|
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 {
|
useExternalComponents (componentsPort: string, components: ExternalComponent[]): void {
|
||||||
|
@ -8,6 +8,7 @@ import { disableProxyRoute, enableProxyRoute } from '../routers/webchat'
|
|||||||
import { fixRoomSubject } from './fix-room-subject'
|
import { fixRoomSubject } from './fix-room-subject'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as child_process from 'child_process'
|
import * as child_process from 'child_process'
|
||||||
|
import { disableLuaUnboundIfNeeded, prosodyDebuggerOptions } from '../../lib/debug'
|
||||||
|
|
||||||
async function _ensureWorkingDir (
|
async function _ensureWorkingDir (
|
||||||
options: RegisterServerOptions,
|
options: RegisterServerOptions,
|
||||||
@ -98,6 +99,7 @@ async function prepareProsody (options: RegisterServerOptions): Promise<void> {
|
|||||||
})
|
})
|
||||||
spawned.on('error', reject)
|
spawned.on('error', reject)
|
||||||
spawned.on('close', (_code) => { // 'close' and not 'exit', to be sure it is finished.
|
spawned.on('close', (_code) => { // 'close' and not 'exit', to be sure it is finished.
|
||||||
|
disableLuaUnboundIfNeeded(options, filePaths.appImageExtractPath)
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -275,11 +277,18 @@ async function testProsodyCorrectlyRunning (options: RegisterServerOptions): Pro
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureProsodyRunning (options: RegisterServerOptions): Promise<void> {
|
async function ensureProsodyRunning (
|
||||||
|
options: RegisterServerOptions,
|
||||||
|
forceRestart?: boolean,
|
||||||
|
restartProsodyInDebugMode?: boolean
|
||||||
|
): Promise<void> {
|
||||||
const { peertubeHelpers } = options
|
const { peertubeHelpers } = options
|
||||||
const logger = peertubeHelpers.logger
|
const logger = peertubeHelpers.logger
|
||||||
logger.debug('Calling ensureProsodyRunning')
|
logger.debug('Calling ensureProsodyRunning')
|
||||||
|
|
||||||
|
if (forceRestart) {
|
||||||
|
logger.info('We want to force Prosody restart, skip checking the current state')
|
||||||
|
} else {
|
||||||
const r = await testProsodyCorrectlyRunning(options)
|
const r = await testProsodyCorrectlyRunning(options)
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
r.messages.forEach(m => logger.debug(m))
|
r.messages.forEach(m => logger.debug(m))
|
||||||
@ -289,6 +298,7 @@ async function ensureProsodyRunning (options: RegisterServerOptions): Promise<vo
|
|||||||
}
|
}
|
||||||
logger.info('Prosody is not running correctly: ')
|
logger.info('Prosody is not running correctly: ')
|
||||||
r.messages.forEach(m => logger.info(m))
|
r.messages.forEach(m => logger.info(m))
|
||||||
|
}
|
||||||
|
|
||||||
// Shutting down...
|
// Shutting down...
|
||||||
logger.debug('Shutting down prosody')
|
logger.debug('Shutting down prosody')
|
||||||
@ -309,9 +319,30 @@ async function ensureProsodyRunning (options: RegisterServerOptions): Promise<vo
|
|||||||
await ensureProsodyCertificates(options, config)
|
await ensureProsodyCertificates(options, config)
|
||||||
|
|
||||||
// launch prosody
|
// launch prosody
|
||||||
const execCmd = filePaths.exec + (filePaths.execArgs.length ? ' ' + filePaths.execArgs.join(' ') : '')
|
let execArgs: string[] = filePaths.execArgs
|
||||||
logger.info('Going to launch prosody (' + execCmd + ')')
|
if (restartProsodyInDebugMode) {
|
||||||
const prosody = child_process.exec(execCmd, {
|
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,
|
cwd: filePaths.dir,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
@ -9,6 +9,8 @@ import { Affiliations, getVideoAffiliations, getChannelAffiliations } from '../p
|
|||||||
import { getProsodyDomain } from '../prosody/config/domain'
|
import { getProsodyDomain } from '../prosody/config/domain'
|
||||||
import { fillVideoCustomFields } from '../custom-fields'
|
import { fillVideoCustomFields } from '../custom-fields'
|
||||||
import { getChannelInfosById } from '../database/channel'
|
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
|
// See here for description: https://modules.prosody.im/mod_muc_http_defaults.html
|
||||||
interface RoomDefaults {
|
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
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@ import { getBoshUri, getWSUri } from '../uri/webchat'
|
|||||||
import { getVideoLiveChatInfos } from '../federation/storage'
|
import { getVideoLiveChatInfos } from '../federation/storage'
|
||||||
import { LiveChatJSONLDAttribute } from '../federation/types'
|
import { LiveChatJSONLDAttribute } from '../federation/types'
|
||||||
import { anonymousConnectionInfos, remoteAuthenticatedConnectionEnabled } from '../federation/connection-infos'
|
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'
|
import * as path from 'path'
|
||||||
const got = require('got')
|
const got = require('got')
|
||||||
|
|
||||||
@ -28,6 +30,7 @@ interface ProsodyProxyInfo {
|
|||||||
let currentProsodyProxyInfo: ProsodyProxyInfo | null = null
|
let currentProsodyProxyInfo: ProsodyProxyInfo | null = null
|
||||||
let currentHttpBindProxy: ReturnType<typeof createProxyServer> | null = null
|
let currentHttpBindProxy: ReturnType<typeof createProxyServer> | null = null
|
||||||
let currentWebsocketProxy: 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> {
|
async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Router> {
|
||||||
const {
|
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_BOSH_SERVICE_URL}}/g, remoteConnectionInfos?.anonymous?.boshUri ?? '')
|
||||||
page = page.replace(/{{REMOTE_WS_SERVICE_URL}}/g, remoteConnectionInfos?.anonymous?.wsUri ?? '')
|
page = page.replace(/{{REMOTE_WS_SERVICE_URL}}/g, remoteConnectionInfos?.anonymous?.wsUri ?? '')
|
||||||
page = page.replace(/{{REMOTE_ANONYMOUS_XMPP_SERVER}}/g, remoteConnectionInfos?.anonymous ? 'true' : 'false')
|
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(
|
page = page.replace(
|
||||||
/{{REMOTE_AUTHENTICATED_XMPP_SERVER}}/g,
|
/{{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(/{{AUTHENTICATION_URL}}/g, authenticationUrl)
|
||||||
page = page.replace(/{{AUTOVIEWERMODE}}/g, autoViewerMode ? 'true' : 'false')
|
page = page.replace(/{{AUTOVIEWERMODE}}/g, autoViewerMode ? 'true' : 'false')
|
||||||
@ -270,6 +269,29 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
|
|||||||
currentWebsocketProxy.ws(request, socket, head)
|
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(
|
router.get('/prosody-list-rooms', asyncMiddleware(
|
||||||
@ -345,6 +367,11 @@ async function disableProxyRoute ({ peertubeHelpers }: RegisterServerOptions): P
|
|||||||
currentWebsocketProxy.close()
|
currentWebsocketProxy.close()
|
||||||
currentWebsocketProxy = null
|
currentWebsocketProxy = null
|
||||||
}
|
}
|
||||||
|
if (currentS2SWebsocketProxy) {
|
||||||
|
peertubeHelpers.logger.info('Closing the s2s websocket proxy...')
|
||||||
|
currentS2SWebsocketProxy.close()
|
||||||
|
currentS2SWebsocketProxy = null
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
peertubeHelpers.logger.error('Seems that the http bind proxy close has failed: ' + (err as string))
|
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', () => {
|
currentWebsocketProxy.on('close', () => {
|
||||||
logger.info('Got a close event for the websocket proxy')
|
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 {
|
interface WCRemoteConnectionInfos {
|
||||||
|
@ -11,3 +11,9 @@ export function getWSUri (options: RegisterServerOptions): string | undefined {
|
|||||||
if (base === undefined) { return undefined }
|
if (base === undefined) { return undefined }
|
||||||
return base + 'xmpp-websocket'
|
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'
|
||||||
|
}
|
||||||
|
73
server/lib/xmpp-ws-proxy/check-remote.ts
Normal file
73
server/lib/xmpp-ws-proxy/check-remote.ts
Normal 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
|
||||||
|
}
|
119
server/lib/xmpp-ws-proxy/server.ts
Normal file
119
server/lib/xmpp-ws-proxy/server.ts
Normal 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
|
||||||
|
// }
|
@ -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.
|
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.
|
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
|
## Quick dev environment using Docker
|
||||||
|
|
||||||
There is a tutorial, in french, on the
|
There is a tutorial, in french, on the
|
||||||
|
@ -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.
|
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.
|
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
|
## Quick dev environment using Docker
|
||||||
|
|
||||||
There is a tutorial, in french, on the
|
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.
|
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
|
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.
|
||||||
|
@ -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.
|
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.
|
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
|
## 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)
|
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.
|
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
|
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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user