Changeset

451:a0c55329c38d

Merge with MattJ
author Kim Alvefur <zash@zash.se>
date Wed, 03 Aug 2022 02:47:55 +0200
parents 450:e72deac76e0e (diff) 438:98dc1750584d (current diff)
children 452:628896d39d8e
files
diffstat 8 files changed, 252 insertions(+), 94 deletions(-) [+]
line wrap: on
line diff
--- a/client.lua	Mon Dec 06 09:09:50 2021 +0000
+++ b/client.lua	Wed Aug 03 02:47:55 2022 +0200
@@ -91,6 +91,7 @@
 
 	self:hook("connected", function () self:reopen(); end);
 	self:hook("incoming-raw", function (data) return self.data(self.conn, data); end);
+	self:hook("read-timeout", function () self:send(" "); return true; end, -1);
 
 	self.curr_id = 0;
 
--- a/init.lua	Mon Dec 06 09:09:50 2021 +0000
+++ b/init.lua	Wed Aug 03 02:47:55 2022 +0200
@@ -244,6 +244,9 @@
 		stream:event("status", new_status);
 	end
 
+	function conn_listener.onreadtimeout(conn)
+		return stream:event("read-timeout");
+	end
 	return conn_listener;
 end
 
--- a/libs/hashes.lua	Mon Dec 06 09:09:50 2021 +0000
+++ b/libs/hashes.lua	Wed Aug 03 02:47:55 2022 +0200
@@ -10,6 +10,10 @@
 	if ok then f(pkg); end
 end
 
+with("util.sha1", function (sha1)
+	_M.sha1 = sha1.sha1;
+end);
+
 with("bgcrypto.md5", function (md5)
 	_M.md5 = md5.digest;
 	_M.hmac_md5 = md5.hmac.digest;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libs/table.lua	Wed Aug 03 02:47:55 2022 +0200
@@ -0,0 +1,1 @@
+return {pack = function(...) return {n = select("#", ...); ...} end; create = function() return {} end}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libs/time.lua	Wed Aug 03 02:47:55 2022 +0200
@@ -0,0 +1,8 @@
+-- Import gettime() from LuaSocket, as a way to access high-resolution time
+-- in a platform-independent way
+
+local socket_gettime = require "socket".gettime;
+
+return {
+	now = socket_gettime;
+}
--- a/plugins/smacks.lua	Mon Dec 06 09:09:50 2021 +0000
+++ b/plugins/smacks.lua	Wed Aug 03 02:47:55 2022 +0200
@@ -5,17 +5,17 @@
 
 function verse.plugins.smacks(stream)
 	-- State for outgoing stanzas
-	local outgoing_queue = {};
-	local last_ack = 0;
-	local last_stanza_time = now();
+	local outgoing_queue = nil;
+	local last_ack = nil;
+	local last_stanza_time = nil;
 	local timer_active;
 
 	-- State for incoming stanzas
-	local handled_stanza_count = 0;
+	local handled_stanza_count = nil;
 
 	-- Catch incoming stanzas
 	local function incoming_stanza(stanza)
-		if stanza.attr.xmlns == "jabber:client" or not stanza.attr.xmlns then
+		if handled_stanza_count and (stanza.attr.xmlns == "jabber:client" or not stanza.attr.xmlns) then
 			handled_stanza_count = handled_stanza_count + 1;
 			stream:debug("Increasing handled stanzas to %d for %s", handled_stanza_count, stanza:top_tag());
 		end
@@ -24,7 +24,7 @@
 	-- Catch outgoing stanzas
 	local function outgoing_stanza(stanza)
 		-- NOTE: This will not behave nice if stanzas are serialized before this point
-		if stanza.name and not stanza.attr.xmlns then
+		if outgoing_queue and (stanza.name and not stanza.attr.xmlns) then
 			-- serialize stanzas in order to bypass this on resumption
 			outgoing_queue[#outgoing_queue+1] = tostring(stanza);
 			last_stanza_time = now();
@@ -64,7 +64,6 @@
 	-- Graceful shutdown
 	local function on_close()
 		stream.resumption_token = nil;
-		stream:unhook("disconnected", on_disconnect);
 	end
 
 	local function handle_sm_command(stanza)
@@ -80,17 +79,18 @@
 				end
 				stream:debug("Received ack: New ack: "..new_ack.." Last ack: "..last_ack.." Unacked stanzas now: "..#outgoing_queue.." (was "..old_unacked..")");
 				last_ack = new_ack;
-			else
+			elseif new_ack < last_ack then
 				stream:warn("Received bad ack for "..new_ack.." when last ack was "..last_ack);
 			end
 		elseif stanza.name == "enabled" then
+			handled_stanza_count = 0;
+			stream.pre_smacks_features = nil;
 
 			if stanza.attr.id then
 				stream.resumption_token = stanza.attr.id;
-				stream:hook("closed", on_close, 100);
-				stream:hook("disconnected", on_disconnect, 100);
 			end
 		elseif stanza.name == "resumed" then
+			stream.pre_smacks_features = nil;
 			local new_ack = tonumber(stanza.attr.h);
 			if new_ack > last_ack then
 				local old_unacked = #outgoing_queue;
@@ -106,39 +106,61 @@
 			outgoing_queue = {};
 			stream:debug("Resumed successfully");
 			stream:event("resumed");
+		elseif stanza.name == "failed" then
+			stream.bound = nil
+			stream.smacks = nil
+			last_ack = nil
+			handled_stanza_count = nil
+
+			-- TODO ack using final h value from <failed/> if present
+			outgoing_queue = {}; -- TODO fire some delivery failures
+
+			local features = stream.pre_smacks_features;
+			stream.pre_smacks_features = nil;
+
+			-- should trigger a bind and then a new smacks session
+			stream:event("stream-features", features);
 		else
 			stream:warn("Don't know how to handle "..xmlns_sm.."/"..stanza.name);
 		end
 	end
 
 	local function on_bind_success()
-		if not stream.smacks then
+		if stream.stream_management_supported and not stream.smacks then
 			--stream:unhook("bind-success", on_bind_success);
 			stream:debug("smacks: sending enable");
+			outgoing_queue = {};
+			last_ack = 0;
+			last_stanza_time = now();
 			stream:send(verse.stanza("enable", { xmlns = xmlns_sm, resume = "true" }));
 			stream.smacks = true;
-
-			-- Catch stanzas
-			stream:hook("stanza", incoming_stanza);
-			stream:hook("outgoing", outgoing_stanza);
 		end
 	end
 
 	local function on_features(features)
 		if features:get_child("sm", xmlns_sm) then
+			stream.pre_smacks_features = features;
 			stream.stream_management_supported = true;
 			if stream.smacks and stream.bound then -- Already enabled in a previous session - resume
 				stream:debug("Resuming stream with %d handled stanzas", handled_stanza_count);
 				stream:send(verse.stanza("resume", { xmlns = xmlns_sm,
-					h = handled_stanza_count, previd = stream.resumption_token }));
+					h = tostring(handled_stanza_count), previd = stream.resumption_token }));
 				return true;
 			else
-				stream:hook("bind-success", on_bind_success, 1);
 			end
 		end
 	end
 
 	stream:hook("stream-features", on_features, 250);
 	stream:hook("stream/"..xmlns_sm, handle_sm_command);
+	stream:hook("bind-success", on_bind_success, 1);
+
+	-- Catch stanzas
+	stream:hook("stanza", incoming_stanza);
+	stream:hook("outgoing", outgoing_stanza);
+
+	stream:hook("closed", on_close, 100);
+	stream:hook("disconnected", on_disconnect, 100);
+
 	--stream:hook("ready", on_stream_ready, 500);
 end
--- a/squishy	Mon Dec 06 09:09:50 2021 +0000
+++ b/squishy	Wed Aug 03 02:47:55 2022 +0200
@@ -3,7 +3,9 @@
 -- Verse-specific versions of libraries
 Module "util.encodings"		"libs/encodings.lua"
 Module "util.hashes"		"libs/hashes.lua"
+Module "util.sha1"		"util/sha1.lua"
 Module "lib.adhoc"              "libs/adhoc.lib.lua"
+Module "util.table" "libs/table.lua"
 
 -- Prosody libraries
 if not GetOption("prosody") then
@@ -33,6 +35,7 @@
 Module "util.random"       "util/random.lua"
 Module "util.ip"       "util/ip.lua"
 Module "util.time"		"util/time.lua"
+Module "util.hex" "util/hex.lua"
 
 Module "util.sasl.scram" "util/sasl/scram.lua"
 Module "util.sasl.plain" "util/sasl/plain.lua"
--- a/util/dataforms.lua	Mon Dec 06 09:09:50 2021 +0000
+++ b/util/dataforms.lua	Wed Aug 03 02:47:55 2022 +0200
@@ -1,30 +1,34 @@
 -- 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 pairs, ipairs = pairs, ipairs;
-local tostring, type, next = tostring, type, next;
+local ipairs = ipairs;
+local type, next = type, next;
+local tonumber = tonumber;
+local tostring = tostring;
 local t_concat = table.concat;
 local st = require "util.stanza";
 local jid_prep = require "util.jid".prep;
 
-module "dataforms"
+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 };
 
-function new(layout)
+local function new(layout)
 	return setmetatable(layout, form_mt);
 end
 
-function from_stanza(stanza)
+local function from_stanza(stanza)
 	local layout = {
 		title = stanza:get_child_text("title");
 		instructions = stanza:get_child_text("instructions");
@@ -58,26 +62,97 @@
 				end
 			end
 		end
+		local datatype_tag = tag:get_child("validate", xmlns_validate);
+		if datatype_tag then
+			field.datatype = datatype.attr.datatype;
+			local range_tag = datatype_tag:get_child("range");
+			if range_tag then
+				field.range_min = tonumber(range_tag.attr.min);
+				field.range_max = tonumber(range_tag.attr.max);
+			end
+		end
+
 	end
 	return new(layout);
 end
 
 function form_t.form(layout, data, formtype)
-	local form = st.stanza("x", { xmlns = xmlns_forms, type = formtype or "form" });
-	if layout.title then
-		form:tag("title"):text(layout.title):up();
+	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 layout.instructions then
-		form:tag("instructions"):text(layout.instructions):up();
+	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 n, field in ipairs(layout) do
+	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.name, label = field.label });
+		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];
 
-		local value = (data and data[field.name]) or field.value;
-		
-		if value then
+			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
+				-- TODO validate that this is ok somehow, eg check field.datatype
+				value = ("%g"):format(value);
+			end
 			-- Add value, depending on type
 			if field_type == "hidden" then
 				if type(value) == "table" then
@@ -86,12 +161,12 @@
 						:add_child(value)
 						:up();
 				else
-					form:tag("value"):text(tostring(value)):up();
+					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();
@@ -106,36 +181,27 @@
 					form:tag("value"):text(line):up();
 				end
 			elseif field_type == "list-single" then
-				local has_default = false;
-				for _, val in ipairs(value) do
-					if type(val) == "table" then
-						form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
-						if val.default and (not has_default) then
-							form:tag("value"):text(val.value):up();
-							has_default = true;
-						end
-					else
-						form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up();
-					end
-				end
+				form:tag("value"):text(value):up();
 			elseif field_type == "list-multi" then
 				for _, val in ipairs(value) do
-					if type(val) == "table" then
-						form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
-						if val.default then
-							form:tag("value"):text(val.value):up();
-						end
-					else
-						form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up();
-					end
+					form:tag("value"):text(val):up();
 				end
 			end
 		end
-		
-		if field.required then
+
+		local media = field.media;
+		if media then
+			form:tag("media", { xmlns = "urn:xmpp:media-element", height = ("%g"):format(media.height), width = ("%g"):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
@@ -143,61 +209,75 @@
 end
 
 local field_readers = {};
+local data_validators = {};
 
-function form_t.data(layout, stanza)
+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() do
-			if field.name == field_tag.attr.var then
+		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 field.required 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
-		else
+		elseif field.name then
+			present[field.name] = true;
 			local reader = field_readers[field.type];
 			if reader then
-				data[field.name], errors[field.name] = reader(tag, field.required);
+				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;
+		return data, errors, present;
 	end
-	return data;
+	return data, nil, present;
 end
 
-field_readers["text-single"] =
-	function (field_tag, required)
-		local data = field_tag:get_child_text("value");
-		if data and #data > 0 then
-			return data
-		elseif required then
-			return nil, "Required value missing";
-		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-private"] =
-	field_readers["text-single"];
+field_readers["text-single"] = simple_text;
+
+field_readers["text-private"] = simple_text;
 
 field_readers["jid-single"] =
 	function (field_tag, required)
-		local raw_data = field_tag:get_child_text("value")
+		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 data and #data > 0 then
-			return data
-		elseif raw_data then
+		if not data then
 			return nil, "Invalid JID: " .. raw_data;
-		elseif required then
-			return nil, "Required value missing";
 		end
+		return data;
 	end
 
 field_readers["jid-multi"] =
@@ -225,7 +305,11 @@
 		for value in field_tag:childtags("value") do
 			result[#result+1] = value:get_text();
 		end
-		return result, (required and #result == 0 and "Required value missing" or nil);
+		if #result > 0 then
+			return result;
+		elseif required then
+			return nil, "Required value missing";
+		end
 	end
 
 field_readers["text-multi"] =
@@ -237,8 +321,7 @@
 		return data, err;
 	end
 
-field_readers["list-single"] =
-	field_readers["text-single"];
+field_readers["list-single"] = simple_text;
 
 local boolean_values = {
 	["1"] = true, ["true"] = true,
@@ -247,15 +330,13 @@
 
 field_readers["boolean"] =
 	function (field_tag, required)
-		local raw_value = field_tag:get_child_text("value");
-		local value = boolean_values[raw_value ~= nil and raw_value];
-		if value ~= nil then
-			return value;
-		elseif raw_value then
-			return nil, "Invalid boolean representation";
-		elseif required then
-			return nil, "Required value missing";
+		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"] =
@@ -263,7 +344,42 @@
 		return field_tag:get_child_text("value");
 	end
 
-return _M;
+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
+
+
+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;
+	from_stanza = from_stanza;
+	get_type = get_form_type;
+};
 
 
 --[=[