view mod_http_connect/mod_http_connect.lua @ 6403:13c61a068c71

mod_pubsub_serverinfo: Include hostname in default pubsub node name This change ensures that every host publishes to a unique node by default, which is important when publishing info about multiple hosts. The old default node name was "serverinfo" The new default node name is in the format "serverinfo/example.com" where example.com is automatically replaced by the publishing host's name. This affects existing deployments in the following ways: - If you already configured pubsub_serverinfo_node explicitly in your config, then nothing will change, your current configuration will still be used. - If you did not specify it in your config (i.e. used the default) then the module will switch to publishing to the new node. If your configuration is correct this shouldn't be noticed, because the new node will automatically be created and configured by the module. If you want, you can clean up the old 'serverinfo' node using: prosodyctl shell pubsub delete_node pubsub.example.com serverinfo
author Matthew Wild <mwild1@gmail.com>
date Wed, 11 Feb 2026 09:40:30 +0000
parents 706867e05809
children
line wrap: on
line source

-- This feature was added after Prosody 13.0
--% requires: net-connect-filter

local hashes = require "prosody.util.hashes";
local server = require "prosody.net.server";
local connect = require"prosody.net.connect".connect;
local basic = require "prosody.net.resolvers.basic";
local new_ip = require "prosody.util.ip".new_ip;

local b64_decode = require "prosody.util.encodings".base64.decode;

local proxy_secret = module:get_option_string("http_proxy_secret", require "prosody.util.id".long());

local allow_private_ips = module:get_option_boolean("http_proxy_to_private_ips", false);
local allow_all_ports = module:get_option_boolean("http_proxy_to_all_ports", false);

local allowed_target_ports = module:get_option_set("http_proxy_to_ports", { "443", "5281", "5443", "7443" }) / tonumber;

local sessions = {};

local listeners = {};

function listeners.onconnect(conn)
	local event = sessions[conn];
	local response = event.response;
	response.status_code = 200;
	response:send("");
	response.conn:onwritable();
	response.conn:setlistener(listeners, event);
	server.link(conn, response.conn);
	server.link(response.conn, conn);
	response.conn = nil;
end

function listeners.onattach(conn, event)
	sessions[conn] = event;
end

function listeners.onfail(event, err)
	local response = event.response;
	if assert(response) then
		response.status_code = 500;
		response:send(err);
	end
end

function listeners.ondisconnect(conn, err) --luacheck: ignore 212/conn 212/err
end

local function is_permitted_target(conn_type, ip, port)
	if not (allow_all_ports or allowed_target_ports:contains(tonumber(port))) then
		module:log("warn", "Forbidding tunnel to %s:%d (forbidden port)", ip, port);
		return false;
	end
	if not allow_private_ips then
		local family = (conn_type:byte(-1, -1) == 54) and "IPv6" or "IPv4";
		local parsed_ip = new_ip(ip, family);
		if parsed_ip.private then
			module:log("warn", "Forbidding tunnel to %s:%d (forbidden ip)", ip, port);
			return false;
		end
	end
	return true;
end

local function verify_auth(user, password)
	local expiry = tonumber(user, 10);
	if os.time() > expiry then
		module:log("warn", "Attempt to use expired credentials");
		return nil;
	end
	local expected_password = hashes.hmac_sha1(proxy_secret, user);
	if hashes.equals(b64_decode(password), expected_password) then
		return true;
	end
	module:log("warn", "Credential mismatch for %s: expected '%q' got '%q'", user, expected_password, password);
end

module:depends("http");
module:provides("http", {
	default_path = "/";
	route = {
		["CONNECT /*"] = function(event)
			local request, response = event.request, event.response;
			local host, port = request.url.scheme, request.url.path;
			if port == "" then return 400 end

			-- Auth check
			local realm = host;
			local headers = request.headers;
			if not headers.proxy_authorization then
				response.headers.proxy_authenticate = ("Basic realm=%q"):format(realm);
				return 407
			end
			local user, password = b64_decode(headers.proxy_authorization:match"[^ ]*$"):match"([^:]*):(.*)";
			if not verify_auth(user, password) then
				response.headers.proxy_authenticate = ("Basic realm=%q"):format(realm);
				return 407
			end

			local resolve = basic.new(host, port, "tcp", {
				filter = is_permitted_target;
			});
			connect(resolve, listeners, nil, event)
			return true;
		end;
	}
});

local http_url = module:http_url();
local parsed_url = require "socket.url".parse(http_url);

local proxy_host = parsed_url.host;
local proxy_port = tonumber(parsed_url.port);

if not proxy_port then
	if parsed_url.scheme == "https" then
		proxy_port = 443;
	elseif parsed_url.scheme == "http" then
		proxy_port = 80;
	end
end

module:depends "external_services";

module:add_item("external_service", {
	type = "http";
	transport = "tcp";
	host = proxy_host;
	port = proxy_port;

	secret = proxy_secret;
	algorithm = "turn";
	ttl = 3600;
});