Changeset

8268:e21d82551e05

Merge 0.10->trunk
author Matthew Wild <mwild1@gmail.com>
date Tue, 26 Sep 2017 17:24:25 +0100
parents 8253:3a6f5b0f56f0 (current diff) 8267:42fad8465537 (diff)
children 8270:8b470cf497b7
files plugins/mod_s2s/mod_s2s.lua prosody prosody.cfg.lua.dist prosodyctl
diffstat 11 files changed, 285 insertions(+), 93 deletions(-) [+]
line wrap: on
line diff
--- a/core/certmanager.lua	Thu Sep 21 02:36:28 2017 +0200
+++ b/core/certmanager.lua	Tue Sep 26 17:24:25 2017 +0100
@@ -58,6 +58,7 @@
 
 local function find_cert(user_certs, name)
 	local certs = resolve_path(config_path, user_certs or global_certificates);
+	log("debug", "Searching %s for a key and certificate for %s...", certs, name);
 	for i = 1, #crt_try do
 		local crt_path = certs .. crt_try[i]:format(name);
 		local key_path = certs .. key_try[i]:format(name);
@@ -66,13 +67,16 @@
 			if key_path:sub(-4) == ".crt" then
 				key_path = key_path:sub(1, -4) .. "key";
 				if stat(key_path, "mode") == "file" then
+					log("debug", "Selecting certificate %s with key %s for %s", crt_path, key_path, name);
 					return { certificate = crt_path, key = key_path };
 				end
 			elseif stat(key_path, "mode") == "file" then
+				log("debug", "Selecting certificate %s with key %s for %s", crt_path, key_path, name);
 				return { certificate = crt_path, key = key_path };
 			end
 		end
 	end
+	log("debug", "No certificate/key found for %s", name);
 end
 
 local function find_host_cert(host)
--- a/net/adns.lua	Thu Sep 21 02:36:28 2017 +0200
+++ b/net/adns.lua	Tue Sep 26 17:24:25 2017 +0100
@@ -7,7 +7,7 @@
 --
 
 local server = require "net.server";
-local dns = require "net.dns";
+local new_resolver = require "net.dns".resolver;
 
 local log = require "util.logger".init("adns");
 
@@ -17,35 +17,11 @@
 
 local _ENV = nil;
 
-local function lookup(handler, qname, qtype, qclass)
-	return coroutine.wrap(function (peek)
-				if peek then
-					log("debug", "Records for %s already cached, using those...", qname);
-					handler(peek);
-					return;
-				end
-				log("debug", "Records for %s not in cache, sending query (%s)...", qname, tostring(coroutine.running()));
-				local ok, err = dns.query(qname, qtype, qclass);
-				if ok then
-					coroutine.yield({ qclass or "IN", qtype or "A", qname, coroutine.running()}); -- Wait for reply
-					log("debug", "Reply for %s (%s)", qname, tostring(coroutine.running()));
-				end
-				if ok then
-					ok, err = pcall(handler, dns.peek(qname, qtype, qclass));
-				else
-					log("error", "Error sending DNS query: %s", err);
-					ok, err = pcall(handler, nil, err);
-				end
-				if not ok then
-					log("error", "Error in DNS response handler: %s", tostring(err));
-				end
-			end)(dns.peek(qname, qtype, qclass));
-end
+local async_resolver_methods = {};
+local async_resolver_mt = { __index = async_resolver_methods };
 
-local function cancel(handle, call_handler, reason)
-	log("warn", "Cancelling DNS lookup for %s", tostring(handle[3]));
-	dns.cancel(handle[1], handle[2], handle[3], handle[4], call_handler);
-end
+local query_methods = {};
+local query_mt = { __index = query_methods };
 
 local function new_async_socket(sock, resolver)
 	local peername = "<unknown>";
@@ -54,7 +30,7 @@
 	local err;
 	function listener.onincoming(conn, data)
 		if data then
-			dns.feed(handler, data);
+			resolver:feed(handler, data);
 		end
 	end
 	function listener.ondisconnect(conn, err)
@@ -85,10 +61,47 @@
 	return handler;
 end
 
-dns.socket_wrapper_set(new_async_socket);
+function async_resolver_methods:lookup(handler, qname, qtype, qclass)
+	local resolver = self._resolver;
+	return coroutine.wrap(function (peek)
+				if peek then
+					log("debug", "Records for %s already cached, using those...", qname);
+					handler(peek);
+					return;
+				end
+				log("debug", "Records for %s not in cache, sending query (%s)...", qname, tostring(coroutine.running()));
+				local ok, err = resolver:query(qname, qtype, qclass);
+				if ok then
+					coroutine.yield(setmetatable({ resolver, qclass or "IN", qtype or "A", qname, coroutine.running()}, query_mt)); -- Wait for reply
+					log("debug", "Reply for %s (%s)", qname, tostring(coroutine.running()));
+				end
+				if ok then
+					ok, err = pcall(handler, resolver:peek(qname, qtype, qclass));
+				else
+					log("error", "Error sending DNS query: %s", err);
+					ok, err = pcall(handler, nil, err);
+				end
+				if not ok then
+					log("error", "Error in DNS response handler: %s", tostring(err));
+				end
+			end)(resolver:peek(qname, qtype, qclass));
+end
+
+function query_methods:cancel(call_handler, reason)
+	log("warn", "Cancelling DNS lookup for %s", tostring(self[4]));
+	self[1].cancel(self[2], self[3], self[4], self[5], call_handler);
+end
+
+local function new_async_resolver()
+	local resolver = new_resolver();
+	resolver:socket_wrapper_set(new_async_socket);
+	return setmetatable({ _resolver = resolver}, async_resolver_mt);
+end
 
 return {
-	lookup = lookup;
-	cancel = cancel;
+	lookup = function (...)
+		return new_async_resolver():lookup(...);
+	end;
+	resolver = new_async_resolver;
 	new_async_socket = new_async_socket;
 };
--- a/net/dns.lua	Thu Sep 21 02:36:28 2017 +0200
+++ b/net/dns.lua	Tue Sep 26 17:24:25 2017 +0100
@@ -504,7 +504,7 @@
 	rr.ttl      = 0x10000*self:word() + self:word();
 	rr.rdlength = self:word();
 
-	rr.tod = self.time + math.min(rr.ttl, 1);
+	rr.tod = self.time + math.max(rr.ttl, 1);
 
 	local remember = self.offset;
 	local rr_parser = self[dns.type[rr.type]];
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_limits.lua	Tue Sep 26 17:24:25 2017 +0100
@@ -0,0 +1,96 @@
+-- Because we deal we pre-authed sessions and streams we can't be host-specific
+module:set_global();
+
+local filters = require "util.filters";
+local throttle = require "util.throttle";
+local timer = require "util.timer";
+
+local limits_cfg = module:get_option("limits", {});
+local limits_resolution = module:get_option_number("limits_resolution", 1);
+
+local default_bytes_per_second = 3000;
+local default_burst = 2;
+
+local rate_units = { b = 1, k = 3, m = 6, g = 9, t = 12 } -- Plan for the future.
+local function parse_rate(rate, sess_type)
+	local quantity, unit, exp;
+	if rate then
+		quantity, unit = rate:match("^(%d+) ?([^/]+)/s$");
+		exp = quantity and rate_units[unit:sub(1,1):lower()];
+	end
+	if not exp then
+		module:log("error", "Error parsing rate for %s: %q, using default rate (%d bytes/s)", sess_type, rate, default_bytes_per_second);
+		return default_bytes_per_second;
+	end
+	return quantity*(10^exp);
+end
+
+local function parse_burst(burst, sess_type)
+	if type(burst) == "string" then
+		burst = burst:match("^(%d+) ?s$");
+	end
+	local n_burst = tonumber(burst);
+	if not n_burst then
+		module:log("error", "Unable to parse burst for %s: %q, using default burst interval (%ds)", sess_type, tostring(burst), default_burst);
+	end
+	return n_burst or default_burst;
+end
+
+-- Process config option into limits table:
+-- limits = { c2s = { bytes_per_second = X, burst_seconds = Y } }
+local limits = {};
+
+for sess_type, sess_limits in pairs(limits_cfg) do
+	limits[sess_type] = {
+		bytes_per_second = parse_rate(sess_limits.rate, sess_type);
+		burst_seconds = parse_burst(sess_limits.burst, sess_type);
+	};
+end
+
+local default_filter_set = {};
+
+function default_filter_set.bytes_in(bytes, session)
+	local throttle = session.throttle;
+	if throttle then
+		local ok, balance, outstanding = throttle:poll(#bytes, true);
+		if not ok then
+			session.log("debug", "Session over rate limit (%d) with %d (by %d), pausing", throttle.max, #bytes, outstanding);
+			session.conn:pause(); -- Read no more data from the connection until there is no outstanding data
+			local outstanding_data = bytes:sub(-outstanding);
+			bytes = bytes:sub(1, #bytes-outstanding);
+			timer.add_task(limits_resolution, function ()
+				if not session.conn then return; end
+				if throttle:peek(#outstanding_data) then
+					session.log("debug", "Resuming paused session");
+					session.conn:resume();
+				end
+				-- Handle what we can of the outstanding data
+				session.data(outstanding_data);
+			end);
+		end
+	end
+	return bytes;
+end
+
+local type_filters = {
+	c2s = default_filter_set;
+	s2sin = default_filter_set;
+	s2sout = default_filter_set;
+};
+
+local function filter_hook(session)
+	local session_type = session.type:match("^[^_]+");
+	local filter_set, opts = type_filters[session_type], limits[session_type];
+	if opts then
+		session.throttle = throttle.create(opts.bytes_per_second * opts.burst_seconds, opts.burst_seconds);
+		filters.add_filter(session, "bytes/in", filter_set.bytes_in, 1000);
+	end
+end
+
+function module.load()
+	filters.add_filter_hook(filter_hook);
+end
+
+function module.unload()
+	filters.remove_filter_hook(filter_hook);
+end
--- a/plugins/mod_s2s/mod_s2s.lua	Thu Sep 21 02:36:28 2017 +0200
+++ b/plugins/mod_s2s/mod_s2s.lua	Tue Sep 26 17:24:25 2017 +0100
@@ -180,6 +180,7 @@
 
 -- Stream is authorised, and ready for normal stanzas
 function mark_connected(session)
+
 	local sendq = session.sendq;
 
 	local from, to = session.from_host, session.to_host;
@@ -211,6 +212,7 @@
 			session.sendq = nil;
 		end
 
+		session.resolver = nil;
 		session.ip_hosts = nil;
 		session.srv_hosts = nil;
 	end
--- a/plugins/mod_s2s/s2sout.lib.lua	Thu Sep 21 02:36:28 2017 +0200
+++ b/plugins/mod_s2s/s2sout.lib.lua	Tue Sep 26 17:24:25 2017 +0100
@@ -49,6 +49,8 @@
 	initialize_filters(host_session);
 	host_session.version = 1;
 
+	host_session.resolver = adns.resolver();
+
 	-- Kick the connection attempting machine into life
 	if not s2sout.attempt_connection(host_session) then
 		-- Intentionally not returning here, the
@@ -84,9 +86,7 @@
 	if not err then -- This is our first attempt
 		log("debug", "First attempt to connect to %s, starting with SRV lookup...", to_host);
 		host_session.connecting = true;
-		local handle;
-		handle = adns.lookup(function (answer)
-			handle = nil;
+		host_session.resolver:lookup(function (answer)
 			local srv_hosts = { answer = answer };
 			host_session.srv_hosts = srv_hosts;
 			host_session.srv_choice = 0;
@@ -168,7 +168,7 @@
 		local have_other_result = not(has_ipv4) or not(has_ipv6) or false;
 
 		if has_ipv4 then
-			handle4 = adns.lookup(function (reply, err)
+			handle4 = host_session.resolver:lookup(function (reply, err)
 				handle4 = nil;
 
 				if reply and reply[#reply] and reply[#reply].a then
@@ -206,7 +206,7 @@
 		end
 
 		if has_ipv6 then
-			handle6 = adns.lookup(function (reply, err)
+			handle6 = host_session.resolver:lookup(function (reply, err)
 				handle6 = nil;
 
 				if reply and reply[#reply] and reply[#reply].aaaa then
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_server_contact_info.lua	Tue Sep 26 17:24:25 2017 +0100
@@ -0,0 +1,49 @@
+-- XEP-0157: Contact Addresses for XMPP Services for Prosody
+--
+-- Copyright (C) 2011-2016 Kim Alvefur
+--
+-- This file is MIT/X11 licensed.
+--
+
+local t_insert = table.insert;
+local array = require "util.array";
+local df_new = require "util.dataforms".new;
+
+-- Source: http://xmpp.org/registrar/formtypes.html#http:--jabber.org-network-serverinfo
+local valid_types = {
+	abuse = true;
+	admin = true;
+	feedback = true;
+	sales = true;
+	security = true;
+	support = true;
+}
+
+local contact_config = module:get_option("contact_info");
+if not contact_config or not next(contact_config) then -- we'll use admins from the config as default
+	local admins = module:get_option_inherited_set("admins", {});
+	if admins:empty() then
+		module:log("error", "No contact_info or admins set in config");
+		return -- Nothing to attach, so we'll just skip it.
+	end
+	module:log("info", "No contact_info in config, using admins as fallback");
+	contact_config = {
+		admin = array.collect( admins / function(admin) return "xmpp:" .. admin; end);
+	};
+end
+
+local form_layout = {
+	{ value = "http://jabber.org/network/serverinfo"; type = "hidden"; name = "FORM_TYPE"; };
+};
+
+local form_values = {};
+
+for t in pairs(valid_types) do
+	local addresses = contact_config[t];
+	if addresses then
+		t_insert(form_layout, { name = t .. "-addresses", type = "list-multi" });
+		form_values[t .. "-addresses"] = addresses;
+	end
+end
+
+module:add_extension(df_new(form_layout):form(form_values, "result"));
--- a/prosody	Thu Sep 21 02:36:28 2017 +0200
+++ b/prosody	Tue Sep 26 17:24:25 2017 +0100
@@ -20,8 +20,8 @@
 
 local function is_relative(path)
 	local path_sep = package.config:sub(1,1);
-        return ((path_sep == "/" and path:sub(1,1) ~= "/")
-	or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\")))
+	return ((path_sep == "/" and path:sub(1,1) ~= "/")
+		or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\")))
 end
 
 -- Tell Lua where to find our libraries
--- a/prosody.cfg.lua.dist	Thu Sep 21 02:36:28 2017 +0200
+++ b/prosody.cfg.lua.dist	Tue Sep 26 17:24:25 2017 +0100
@@ -4,7 +4,8 @@
 -- website at https://prosody.im/doc/configure
 --
 -- Tip: You can check that the syntax of this file is correct
--- when you have finished by running: prosodyctl check config
+-- when you have finished by running this command:
+--     prosodyctl check config
 -- If there are any errors, it will let you know what and where
 -- they are, otherwise it will keep quiet.
 --
@@ -26,9 +27,14 @@
 -- For more information see: https://prosody.im/doc/libevent
 --use_libevent = true
 
+-- Prosody will always look in its source directory for modules, but
+-- this option allows you to specify additional locations where Prosody
+-- will look for modules first. For community modules, see https://modules.prosody.im/
+--plugin_paths = {}
+
 -- This is the list of modules Prosody will load on startup.
 -- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too.
--- Documentation on modules can be found at: https://prosody.im/doc/modules
+-- Documentation for bundled modules can be found at: https://prosody.im/doc/modules
 modules_enabled = {
 
 	-- Generally required
@@ -39,20 +45,19 @@
 		"disco"; -- Service discovery
 
 	-- Not essential, but recommended
+		"carbons"; -- Keep multiple clients in sync
+		"pep"; -- Enables users to publish their mood, activity, playing music and more
 		"private"; -- Private XML storage (for room bookmarks, etc.)
+		"blocklist"; -- Allow users to block communications with other users
 		"vcard"; -- Allow users to set vCards
 
-	-- These are commented by default as they have a performance impact
-		--"blocklist"; -- Allow users to block communications with other users
-		--"compression"; -- Stream compression
-
 	-- Nice to have
 		"version"; -- Replies to server version requests
 		"uptime"; -- Report how long server has been running
 		"time"; -- Let others know the time here on this server
 		"ping"; -- Replies to XMPP pings with pongs
-		"pep"; -- Enables users to publish their mood, activity, playing music and more
 		"register"; -- Allow users to register on this server using a client and change passwords
+		--"mam"; -- Store messages in an archive and allow users to access it
 
 	-- Admin interfaces
 		"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
@@ -60,15 +65,19 @@
 
 	-- HTTP modules
 		--"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
+		--"websockets"; -- XMPP over WebSockets
 		--"http_files"; -- Serve static files from a directory over HTTP
 
 	-- Other specific functionality
+		--"limits"; -- Enable bandwidth limiting for XMPP connections
 		--"groups"; -- Shared roster support
+		--"server_contact_info"; -- Publish contact information for this service
 		--"announce"; -- Send announcement to all online users
 		--"welcome"; -- Welcome users who register accounts
 		--"watchregistrations"; -- Alert admins of registrations
 		--"motd"; -- Send a message to users when they log in
 		--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
+		--"proxy65"; -- Enables a file transfer proxy service which clients behind NAT can use
 }
 
 -- These modules are auto-loaded, but should you want
@@ -84,18 +93,18 @@
 -- For more information see https://prosody.im/doc/creating_accounts
 allow_registration = false
 
--- These are the SSL/TLS-related settings. If you don't want
--- to use SSL/TLS, you may comment or remove this
-ssl = {
-	key = "certs/localhost.key";
-	certificate = "certs/localhost.crt";
-}
-
 -- Force clients to use encrypted connections? This option will
 -- prevent clients from authenticating unless they are using encryption.
 
 c2s_require_encryption = true
 
+-- Force servers to use encrypted connections? This option will
+-- prevent servers from authenticating unless they are using encryption.
+-- Note that this is different from authentication
+
+s2s_require_encryption = true
+
+
 -- Force certificate authentication for server-to-server connections?
 -- This provides ideal security, but requires servers you communicate
 -- with to support encryption AND present valid, trusted certificates.
@@ -104,11 +113,12 @@
 
 s2s_secure_auth = false
 
--- Many servers don't support encryption or have invalid or self-signed
--- certificates. You can list domains here that will not be required to
--- authenticate using certificates. They will be authenticated using DNS.
+-- Some servers have invalid or self-signed certificates. You can list
+-- remote domains here that will not be required to authenticate using
+-- certificates. They will be authenticated using DNS instead, even
+-- when s2s_secure_auth is enabled.
 
---s2s_insecure_domains = { "gmail.com" }
+--s2s_insecure_domains = { "insecure.example" }
 
 -- Even if you leave s2s_secure_auth disabled, you can still require valid
 -- certificates for some domains by specifying a list here.
@@ -122,7 +132,7 @@
 -- server please see https://prosody.im/doc/modules/mod_auth_internal_hashed
 -- for information about using the hashed backend.
 
-authentication = "internal_plain"
+authentication = "internal_hashed"
 
 -- Select the storage backend to use. By default Prosody uses flat files
 -- in its configured data directory, but it also supports more backends
@@ -136,6 +146,18 @@
 --sql = { driver = "MySQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }
 --sql = { driver = "PostgreSQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }
 
+
+-- Archiving configuration
+-- If mod_mam is enabled, Prosody will store a copy of every message. This
+-- is used to synchronize conversations between multiple clients, even if
+-- they are offline. This setting controls how long Prosody will keep
+-- messages in the archive before removing them.
+
+archive_expires_after = "1w" -- Remove archived messages after 1 week
+
+-- You can also configure messages to be stored in-memory only. For more
+-- archiving options, see https://prosody.im/doc/modules/mod_mam
+
 -- Logging configuration
 -- For advanced logging see https://prosody.im/doc/logging
 log = {
@@ -145,23 +167,28 @@
 	-- "*console"; -- Log to the console, useful for debugging with daemonize=false
 }
 
+-- Uncomment to enable statistics
+-- For more info see https://prosody.im/doc/statistics
+-- statistics = "internal"
+
+-- Certificates
+-- Every virtual host and component needs a certificate so that clients and
+-- servers can securely verify its identity. Prosody will automatically load
+-- certificates/keys from the directory specified here.
+-- For more information, including how to use 'prosodyctl' to auto-import certificates
+-- (from e.g. Let's Encrypt) see https://prosody.im/doc/certificates
+
+-- Location of directory to find certificates in (relative to main config file):
+certificates = "certs"
+
 ----------- Virtual hosts -----------
 -- You need to add a VirtualHost entry for each domain you wish Prosody to serve.
 -- Settings under each VirtualHost entry apply *only* to that host.
 
 VirtualHost "localhost"
 
-VirtualHost "example.com"
-	enabled = false -- Remove this line to enable this host
-
-	-- Assign this host a certificate for TLS, otherwise it would use the one
-	-- set in the global section (if any).
-	-- Note that old-style SSL on port 5223 only supports one certificate, and will always
-	-- use the global one.
-	ssl = {
-		key = "certs/example.com.key";
-		certificate = "certs/example.com.crt";
-	}
+--VirtualHost "example.com"
+--	certificate = "/path/to/example.crt"
 
 ------ Components ------
 -- You can specify components to add hosts that provide special services,
@@ -171,9 +198,6 @@
 ---Set up a MUC (multi-user chat) room server on conference.example.com:
 --Component "conference.example.com" "muc"
 
--- Set up a SOCKS5 bytestream proxy for server-proxied file transfers:
---Component "proxy.example.com" "proxy65"
-
 ---Set up an external component (default component port is 5347)
 --
 -- External components allow adding various services, such as gateways/
--- a/prosodyctl	Thu Sep 21 02:36:28 2017 +0200
+++ b/prosodyctl	Tue Sep 26 17:24:25 2017 +0100
@@ -1030,7 +1030,7 @@
 					suggested_global_modules = set.intersection(suggested_global_modules or set.new(options.modules_enabled), set.new(options.modules_enabled));
 				end
 			end
-			if not suggested_global_modules:empty() then
+			if suggested_global_modules and not suggested_global_modules:empty() then
 				print("    Consider moving these modules into modules_enabled in the global section:")
 				print("    "..tostring(suggested_global_modules / function (x) return ("%q"):format(x) end));
 			end
--- a/util/dataforms.lua	Thu Sep 21 02:36:28 2017 +0200
+++ b/util/dataforms.lua	Tue Sep 26 17:24:25 2017 +0100
@@ -68,33 +68,37 @@
 					form:tag("value"):text(line):up();
 				end
 			elseif field_type == "list-single" then
-				local has_default = false;
-				for _, val in ipairs(field.options or value) do
-					if type(val) == "table" then
-						form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
-						if value == val.value or val.default and (not has_default) then
-							form:tag("value"):text(val.value):up();
-							has_default = true;
+				if formtype ~= "result" then
+					local has_default = false;
+					for _, val in ipairs(field.options or value) do
+						if type(val) == "table" then
+							form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
+							if value == val.value or 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
-					else
-						form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up();
 					end
 				end
-				if field.options and value then
+				if (field.options or formtype == "result") and value then
 					form:tag("value"):text(value):up();
 				end
 			elseif field_type == "list-multi" then
-				for _, val in ipairs(field.options or value) do
-					if type(val) == "table" then
-						form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
-						if not field.options and val.default then
-							form:tag("value"):text(val.value):up();
+				if formtype ~= "result" then
+					for _, val in ipairs(field.options or value) do
+						if type(val) == "table" then
+							form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
+							if not field.options and 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
-					else
-						form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up();
 					end
 				end
-				if field.options and value then
+				if (field.options or formtype == "result") and value then
 					for _, val in ipairs(value) do
 						form:tag("value"):text(val):up();
 					end