File

net/tls_luasec.lua @ 13765:7c57fb2ffbb0 13.0

mod_websocket: Merge session close handling changes from mod_c2s (bug fixes) This should bring some fixes and general robustness that mod_websocket had missed out on. The duplicated code here is not at all ideal. To prevent this happening again, we should figure out how to have the common logic in a single place, while still being able to do the websocket-specific parts that we need. The main known bug that this fixes is that it's possible for a session to get into a non-destroyable state. For example, if we try to session:close() a hibernating session, then session.conn is nil and the function will simply return without doing anything. In the mod_c2s code we already handle this, and just destroy the session. But if a hibernating websocket session is never resumed or becomes non-resumable, it will become immortal! By merging the fix from mod_c2s, the session should now be correctly destroyed.
author Matthew Wild <mwild1@gmail.com>
date Tue, 11 Mar 2025 18:44:40 +0000
parent 13502:61da4491eebc
line wrap: on
line source

-- Prosody IM
-- Copyright (C) 2021 Prosody folks
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--

--[[
This file provides a shim abstraction over LuaSec, consolidating some code
which was previously spread between net.server backends, portmanager and
certmanager.

The goal is to provide a more or less well-defined API on top of LuaSec which
abstracts away some of the things which are not needed and simplifies usage of
commonly used things (such as SNI contexts). Eventually, network backends
which do not rely on LuaSocket+LuaSec should be able to provide *this* API
instead of having to mimic LuaSec.
]]
local ssl = require "ssl";
local ssl_newcontext = ssl.newcontext;
local ssl_context = ssl.context or require "ssl.context";
local io_open = io.open;

local context_api = {};
local context_mt = {__index = context_api};

function context_api:set_sni_host(host, cert, key)
	local ctx, err = self._builder:clone():apply({
		certificate = cert,
		key = key,
	}):build();
	if not ctx then
		return false, err
	end

	self._sni_contexts[host] = ctx._inner

	return true, nil
end

function context_api:remove_sni_host(host)
	self._sni_contexts[host] = nil
end

function context_api:wrap(sock)
	local ok, conn, err = pcall(ssl.wrap, sock, self._inner);
	if not ok then
		return nil, err
	end
	return conn, nil
end

local function new_context(cfg, builder)
	-- LuaSec expects dhparam to be a callback that takes two arguments.
	-- We ignore those because it is mostly used for having a separate
	-- set of params for EXPORT ciphers, which we don't have by default.
	if type(cfg.dhparam) == "string" and cfg.dhparam:sub(1, 10) == "-----BEGIN" then
		local dhparam = cfg.dhparam;
		cfg.dhparam = function() return dhparam; end
	elseif type(cfg.dhparam) == "string" then
		local f, err = io_open(cfg.dhparam);
		if not f then return nil, "Could not open DH parameters: "..err end
		local dhparam = f:read("*a");
		f:close();
		cfg.dhparam = function() return dhparam; end
	end

	local inner, err = ssl_newcontext(cfg);
	if not inner then
		return nil, err
	end

	-- COMPAT Older LuaSec ignores the cipher list from the config, so we have to take care
	-- of it ourselves (W/A for #x)
	if inner and cfg.ciphers then
		local success;
		success, err = ssl_context.setcipher(inner, cfg.ciphers);
		if not success then
			return nil, err
		end
	end

	return setmetatable({
		_inner = inner,
		_builder = builder,
		_sni_contexts = {},
	}, context_mt), nil
end

-- Feature detection / guessing
local function test_option(option)
	return not not ssl_newcontext({mode="server",protocol="sslv23",options={ option }});
end
local luasec_major, luasec_minor = ssl._VERSION:match("^(%d+)%.(%d+)");
local luasec_version = tonumber(luasec_major) * 100 + tonumber(luasec_minor);
local luasec_has = ssl.config or {
	algorithms = {
		ec = luasec_version >= 5;
	};
	capabilities = {
		curves_list = luasec_version >= 7;
	};
	options = {
		cipher_server_preference = test_option("cipher_server_preference");
		no_ticket = test_option("no_ticket");
		no_compression = test_option("no_compression");
		single_dh_use = test_option("single_dh_use");
		single_ecdh_use = test_option("single_ecdh_use");
		no_renegotiation = test_option("no_renegotiation");
	};
};

return {
	features = luasec_has;
	new_context = new_context,
	load_certificate = ssl.loadcertificate;
};