view mod_http_connect/mod_http_connect.lua @ 6319:04c3273cb81f

mod_auth_cyrus: Add empty 'profile' table to SASL handler objects This is for compatibility with Prosody's built-in util.sasl objects. A SASL profile table usually includes methods supported by the backend, which can be used by SASL mechanism handlers to perform operations (such as testing the password). It also optionally contains a 'cb' field with channel binding method handlers. The Cyrus backend doesn't support channel binding, and doesn't have the same concept of auth backend methods (it handles all that internally, and Prosody has no insight or control over it). Thus, we create an empty profile which informs Prosody that the SASL handler does not support any of the auth or channel binding methods. Some features will not work, but they didn't work anyway. This just makes it explicit. This fixes a traceback in mod_sasl2_fast, which expected SASL handlers to always contain a 'profile' field.
author Matthew Wild <mwild1@gmail.com>
date Thu, 04 Sep 2025 10:14:46 +0100
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;
});