Software /
code /
prosody
File
util/dataforms.lua @ 13652:a08065207ef0
net.server_epoll: Call :shutdown() on TLS sockets when supported
Comment from Matthew:
This fixes a potential issue where the Prosody process gets blocked on sockets
waiting for them to close. Unlike non-TLS sockets, closing a TLS socket sends
layer 7 data, and this can cause problems for sockets which are in the process
of being cleaned up.
This depends on LuaSec changes which are not yet upstream.
From Martijn's original email:
So first my analysis of luasec. in ssl.c the socket is put into blocking
mode right before calling SSL_shutdown() inside meth_destroy(). My best
guess to why this is is because meth_destroy is linked to the __close
and __gc methods, which can't exactly be called multiple times and
luasec does want to make sure that a tls session is shutdown as clean
as possible.
I can't say I disagree with this reasoning and don't want to change this
behaviour. My solution to this without changing the current behaviour is
to introduce a shutdown() method. I am aware that this overlaps in a
conflicting way with tcp's shutdown method, but it stays close to the
OpenSSL name. This method calls SSL_shutdown() in the current
(non)blocking mode of the underlying socket and returns a boolean
whether or not the shutdown is completed (matching SSL_shutdown()'s 0
or 1 return values), and returns the familiar ssl_ioerror() strings on
error with a false for completion. This error can then be used to
determine if we have wantread/wantwrite to finalize things. Once
meth_shutdown() has been called once a shutdown flag will be set, which
indicates to meth_destroy() that the SSL_shutdown() has been handled
by the application and it shouldn't be needed to set the socket to
blocking mode. I've left the SSL_shutdown() call in the
LSEC_STATE_CONNECTED to prevent TOCTOU if the application reaches a
timeout for the shutdown code, which might allow SSL_shutdown() to
clean up anyway at the last possible moment.
Another thing I've changed to luasec is the call to socket_setblocking()
right before calling close(2) in socket_destroy() in usocket.c.
According to the latest POSIX[0]:
Note that the requirement for close() on a socket to block for up to
the current linger interval is not conditional on the O_NONBLOCK
setting.
Which I read to mean that removing O_NONBLOCK on the socket before close
doesn't impact the behaviour and only causes noise in system call
tracers. I didn't touch the windows bits of this, since I don't do
windows.
For the prosody side of things I've made the TLS shutdown bits resemble
interface:onwritable(), and put it under a combined guard of self._tls
and self.conn.shutdown. The self._tls bit is there to prevent getting
stuck on this condition, and self.conn.shutdown is there to prevent the
code being called by instances where the patched luasec isn't deployed.
The destroy() method can be called from various places and is read by
me as the "we give up" error path. To accommodate for these unexpected
entrypoints I've added a single call to self.conn:shutdown() to prevent
the socket being put into blocking mode. I have no expectations that
there is any other use here. Same as previous, the self.conn.shutdown
check is there to make sure it's not called on unpatched luasec
deployments and self._tls is there to make sure we don't call shutdown()
on tcp sockets.
I wouldn't recommend logging of the conn:shutdown() error inside
close(), since a lot of clients simply close the connection before
SSL_shutdown() is done.
author | Martijn van Duren <martijn@openbsd.org> |
---|---|
date | Thu, 06 Feb 2025 15:04:38 +0000 |
parent | 12975:d10957394a3c |
line wrap: on
line source
-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local setmetatable = setmetatable; local ipairs = ipairs; local type, next = type, next; local tonumber = tonumber; local tostring = tostring; local t_concat = table.concat; local st = require "prosody.util.stanza"; local jid_prep = require "prosody.util.jid".prep; local datetime = require "prosody.util.datetime"; local _ENV = nil; -- luacheck: std none local xmlns_forms = 'jabber:x:data'; local xmlns_validate = 'http://jabber.org/protocol/xdata-validate'; local form_t = {}; local form_mt = { __index = form_t }; local function new(layout) return setmetatable(layout, form_mt); end function form_t.form(layout, data, formtype) if not formtype then formtype = "form" end local form = st.stanza("x", { xmlns = xmlns_forms, type = formtype }); if formtype == "cancel" then return form; end if formtype ~= "submit" then if layout.title then form:tag("title"):text(layout.title):up(); end if layout.instructions then form:tag("instructions"):text(layout.instructions):up(); end end for _, field in ipairs(layout) do local field_type = field.type or "text-single"; -- Add field tag form:tag("field", { type = field_type, var = field.var or field.name, label = formtype ~= "submit" and field.label or nil }); if formtype ~= "submit" then if field.desc then form:text_tag("desc", field.desc); end end if formtype == "form" and field.datatype then form:tag("validate", { xmlns = xmlns_validate, datatype = field.datatype }); if field.range_min or field.range_max then form:tag("range", { min = field.range_min and tostring(field.range_min), max = field.range_max and tostring(field.range_max), }):up(); end -- <basic/> assumed form:up(); end local value = field.value; local options = field.options; if data and data[field.name] ~= nil then value = data[field.name]; if formtype == "form" and type(value) == "table" and (field_type == "list-single" or field_type == "list-multi") then -- Allow passing dynamically generated options as values options, value = value, nil; end end if formtype == "form" and options then local defaults = {}; for _, val in ipairs(options) do if type(val) == "table" then form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up(); if val.default then defaults[#defaults+1] = val.value; end else form:tag("option", { label= val }):tag("value"):text(val):up():up(); end end if not value then if field_type == "list-single" then value = defaults[1]; elseif field_type == "list-multi" then value = defaults; end end end if value ~= nil then if type(value) == "number" then if field.datatype == "xs:dateTime" then value = datetime.datetime(value); elseif field_type == "boolean" then value = value ~= 0; elseif field.datatype == "xs:double" or field.datatype == "xs:decimal" then value = ("%f"):format(value); else value = ("%d"):format(value); end end -- Add value, depending on type if field_type == "hidden" then if type(value) == "table" then -- Assume an XML snippet form:tag("value") :add_child(value) :up(); else form:tag("value"):text(value):up(); end elseif field_type == "boolean" then form:tag("value"):text((value and "1") or "0"):up(); elseif field_type == "fixed" then form:tag("value"):text(value):up(); elseif field_type == "jid-multi" then for _, jid in ipairs(value) do form:tag("value"):text(jid):up(); end elseif field_type == "jid-single" then form:tag("value"):text(value):up(); elseif field_type == "text-single" or field_type == "text-private" then form:tag("value"):text(value):up(); elseif field_type == "text-multi" then -- Split into multiple <value> tags, one for each line for line in value:gmatch("([^\r\n]+)\r?\n*") do form:tag("value"):text(line):up(); end elseif field_type == "list-single" then form:tag("value"):text(value):up(); elseif field_type == "list-multi" then for _, val in ipairs(value) do form:tag("value"):text(val):up(); end end end local media = field.media; if media then form:tag("media", { xmlns = "urn:xmpp:media-element", height = ("%d"):format(media.height), width = ("%d"):format(media.width) }); for _, val in ipairs(media) do form:tag("uri", { type = val.type }):text(val.uri):up() end form:up(); end if formtype == "form" and field.required then form:tag("required"):up(); end -- Jump back up to list of fields form:up(); end return form; end local field_readers = {}; local data_validators = {}; function form_t.data(layout, stanza, current) local data = {}; local errors = {}; local present = {}; for _, field in ipairs(layout) do local tag; for field_tag in stanza:childtags("field") do if (field.var or field.name) == field_tag.attr.var then tag = field_tag; break; end end if not tag then if current and current[field.name] ~= nil then data[field.name] = current[field.name]; elseif field.required then errors[field.name] = "Required value missing"; end elseif field.name then present[field.name] = true; local reader = field_readers[field.type]; if reader then local value, err = reader(tag, field.required); local validator = field.datatype and data_validators[field.datatype]; if value ~= nil and validator then local valid, ret = validator(value, field); if valid then value = ret; else value, err = nil, ret or ("Invalid value for data of type " .. field.datatype); end end data[field.name], errors[field.name] = value, err; end end end if next(errors) then return data, errors, present; end return data, nil, present; end local function simple_text(field_tag, required) local data = field_tag:get_child_text("value"); -- XEP-0004 does not say if an empty string is acceptable for a required value -- so we will follow HTML5 which says that empty string means missing if required and (data == nil or data == "") then return nil, "Required value missing"; end return data; -- Return whatever get_child_text returned, even if empty string end field_readers["text-single"] = simple_text; field_readers["text-private"] = simple_text; field_readers["jid-single"] = function (field_tag, required) local raw_data, err = simple_text(field_tag, required); if not raw_data then return raw_data, err; end local data = jid_prep(raw_data); if not data then return nil, "Invalid JID: " .. raw_data; end return data; end field_readers["jid-multi"] = function (field_tag, required) local result = {}; local err = {}; for value_tag in field_tag:childtags("value") do local raw_value = value_tag:get_text(); local value = jid_prep(raw_value); result[#result+1] = value; if raw_value and not value then err[#err+1] = ("Invalid JID: " .. raw_value); end end if #result > 0 then return result, (#err > 0 and t_concat(err, "\n") or nil); elseif required then return nil, "Required value missing"; end end field_readers["list-multi"] = function (field_tag, required) local result = {}; for value in field_tag:childtags("value") do result[#result+1] = value:get_text(); end if #result > 0 then return result; elseif required then return nil, "Required value missing"; end end field_readers["text-multi"] = function (field_tag, required) local data, err = field_readers["list-multi"](field_tag, required); if data then data = t_concat(data, "\n"); end return data, err; end field_readers["list-single"] = simple_text; local boolean_values = { ["1"] = true, ["true"] = true, ["0"] = false, ["false"] = false, }; field_readers["boolean"] = function (field_tag, required) local raw_value, err = simple_text(field_tag, required); if not raw_value then return raw_value, err; end local value = boolean_values[raw_value]; if value == nil then return nil, "Invalid boolean representation:" .. raw_value; end return value; end field_readers["hidden"] = function (field_tag) return field_tag:get_child_text("value"); end data_validators["xs:integer"] = function (data, field) local n = tonumber(data); if not n then return false, "not a number"; elseif n % 1 ~= 0 then return false, "not an integer"; end if field.range_max and n > field.range_max then return false, "out of bounds"; elseif field.range_min and n < field.range_min then return false, "out of bounds"; end return true, n; end data_validators["pubsub:integer-or-max"] = function (data, field) if data == "max" then return true, data; else return data_validators["xs:integer"](data, field); end end data_validators["xs:dateTime"] = function(data, field) -- luacheck: ignore 212/field local n = datetime.parse(data); if not n then return false, "invalid timestamp"; end return true, n; end local function get_form_type(form) if not st.is_stanza(form) then return nil, "not a stanza object"; elseif form.attr.xmlns ~= "jabber:x:data" or form.name ~= "x" then return nil, "not a dataform element"; end for field in form:childtags("field") do if field.attr.var == "FORM_TYPE" then return field:get_child_text("value"); end end return ""; end return { new = new; get_type = get_form_type; }; --[=[ Layout: { title = "MUC Configuration", instructions = [[Use this form to configure options for this MUC room.]], { name = "FORM_TYPE", type = "hidden", required = true }; { name = "field-name", type = "field-type", required = false }; } --]=]