Mercurial > prosody-modules
comparison mod_incidents_handling/incidents_handling/mod_incidents_handling.lua @ 912:d814cc183c40
mod_incidents_handling: recommit after full test, cleaned it a bit as what I posted was regretfully an unfinished draft, replaced a lot of the fixed type fields used for fields description with desc elements (upstream prosody's util.dataforms will still strip those). Also moved auxiliary functions to a library.
author | Marco Cirillo <maranda@lightwitch.org> |
---|---|
date | Sun, 17 Feb 2013 19:28:47 +0100 |
parents | |
children | dec71c31fb78 |
comparison
equal
deleted
inserted
replaced
911:46e807dff445 | 912:d814cc183c40 |
---|---|
1 -- This plugin implements XEP-268 (Incidents Handling) | |
2 -- (C) 2012-2013, Marco Cirillo (LW.Org) | |
3 | |
4 -- Note: Only part of the IODEF specifications are supported. | |
5 | |
6 module:depends("adhoc") | |
7 | |
8 local datamanager = require "util.datamanager" | |
9 local dataforms_new = require "util.dataforms".new | |
10 local st = require "util.stanza" | |
11 local id_gen = require "util.uuid".generate | |
12 | |
13 local pairs, os_time = pairs, os.time | |
14 | |
15 local xmlns_inc = "urn:xmpp:incident:2" | |
16 local xmlns_iodef = "urn:ietf:params:xml:ns:iodef-1.0" | |
17 | |
18 local my_host = module:get_host() | |
19 local ih_lib = module:require("incidents_handling") | |
20 ih_lib.set_my_host(my_host) | |
21 incidents = {} | |
22 | |
23 local expire_time = module:get_option_number("incidents_expire_time", 0) | |
24 | |
25 -- Incidents Table Methods | |
26 | |
27 local _inc_mt = {} ; _inc_mt.__index = _inc_mt | |
28 | |
29 function _inc_mt:init() | |
30 self:clean() ; self:save() | |
31 end | |
32 | |
33 function _inc_mt:clean() | |
34 if expire_time > 0 then | |
35 for id, incident in pairs(self) do | |
36 if ((os_time() - incident.time) > expire_time) and incident.status ~= "open" then | |
37 incident = nil | |
38 end | |
39 end | |
40 end | |
41 end | |
42 | |
43 function _inc_mt:save() | |
44 if not datamanager.store("incidents", my_host, "incidents_store", incidents) then | |
45 module:log("error", "Failed to save the incidents store!") | |
46 end | |
47 end | |
48 | |
49 function _inc_mt:add(stanza, report) | |
50 local data = ih_lib.stanza_parser(stanza) | |
51 local new_object = { | |
52 time = os_time(), | |
53 status = (not report and "open") or nil, | |
54 data = data | |
55 } | |
56 | |
57 self[data.id.text] = new_object | |
58 self:clean() ; self:save() | |
59 end | |
60 | |
61 function _inc_mt:new_object(fields, formtype) | |
62 local start_time, end_time, report_time = fields.started, fields.ended, fields.reported | |
63 | |
64 local _desc, _contacts, _related, _impact, _sources, _targets = fields.description, fields.contacts, fields.related, fields.impact, fields.sources, fields.targets | |
65 local fail = false | |
66 | |
67 local _lang, _dtext = _desc:match("^(%a%a)%s(.*)$") | |
68 if not _lang or not _dtext then return false end | |
69 local desc = { text = _dtext, lang = _lang } | |
70 | |
71 local contacts = {} | |
72 for contact in _contacts:gmatch("[%w%p]+%s[%w%p]+%s[%w%p]+") do | |
73 local address, atype, role = contact:match("^([%w%p]+)%s([%w%p]+)%s([%w%p]+)$") | |
74 if not address or not atype or not role then fail = true ; break end | |
75 contacts[#contacts + 1] = { | |
76 role = role, | |
77 ext_role = (role ~= "creator" or role ~= "admin" or role ~= "tech" or role ~= "irt" or role ~= "cc" and true) or nil, | |
78 type = atype, | |
79 ext_type = (atype ~= "person" or atype ~= "organization" and true) or nil, | |
80 jid = (atype == "jid" and address) or nil, | |
81 email = (atype == "email" and address) or nil, | |
82 telephone = (atype == "telephone" and address) or nil, | |
83 postaladdr = (atype == "postaladdr" and address) or nil | |
84 } | |
85 end | |
86 | |
87 local related = {} | |
88 if _related then | |
89 for related in _related:gmatch("[%w%p]+%s[%w%p]+") do | |
90 local fqdn, id = related:match("^([%w%p]+)%s([%w%p]+)$") | |
91 if fqdn and id then related[#related + 1] = { text = id, name = fqdn } end | |
92 end | |
93 end | |
94 | |
95 local _severity, _completion, _type = _impact:match("^([%w%p]+)%s([%w%p]+)%s([%w%p]+)$") | |
96 local assessment = { lang = "en", severity = _severity, completion = _completion, type = _type } | |
97 | |
98 local sources = {} | |
99 for source in _sources:gmatch("[%w%p]+%s[%w%p]+%s[%d]+%s[%w%p]+") do | |
100 local address, cat, count, count_type = source:match("^([%w%p]+)%s([%w%p]+)%s(%d+)%s([%w%p]+)$") | |
101 if not address or not cat or not count or not count_type then fail = true ; break end | |
102 local cat, cat_ext = ih_lib.get_type(cat, "category") | |
103 local count_type, count_ext = ih_lib.get_type(count_type, "counter") | |
104 | |
105 sources[#sources + 1] = { | |
106 address = { cat = cat, ext = cat_ext, text = address }, | |
107 counter = { type = count_type, ext_type = count_ext, value = count } | |
108 } | |
109 end | |
110 | |
111 local targets, _preprocess = {}, {} | |
112 for target in _targets:gmatch("[%w%p]+%s[%w%p]+%s[%w%p]+") do | |
113 local address, cat, noderole, noderole_ext | |
114 local address, cat, noderole = target:match("^([%w%p]+)%s([%w%p]+)%s([%w%p]+)$") | |
115 if not address or not cat or not noderole then fail = true ; break end | |
116 cat, cat_ext = ih_lib.get_type(cat, "category") | |
117 noderole_ext = ih_lib.get_type(cat, "noderole") | |
118 | |
119 if not _preprocess[noderole] then _preprocess[noderole] = { addresses = {}, ext = noderole_ext } end | |
120 | |
121 _preprocess[noderole].addresses[#_preprocess[noderole].addresses + 1] = { | |
122 text = address, cat = cat, ext = cat_ext | |
123 } | |
124 end | |
125 for noderole, data in pairs(_preprocess) do | |
126 local nr_cat = (data.ext and "ext-category") or noderole | |
127 local nr_ext = (data.ext and noderole) or nil | |
128 targets[#targets + 1] = { addresses = data.addresses, noderole = { cat = nr_cat, ext = nr_ext } } | |
129 end | |
130 | |
131 local new_object = {} | |
132 if not fail then | |
133 new_object["time"] = os_time() | |
134 new_object["status"] = (formtype == "request" and "open") or nil | |
135 new_object["type"] = formtype | |
136 new_object["data"] = { | |
137 id = { text = id_gen(), name = my_host }, | |
138 start_time = start_time, | |
139 end_time = end_time, | |
140 report_time = report_time, | |
141 desc = desc, | |
142 contacts = contacts, | |
143 related = related, | |
144 assessment = assessment, | |
145 event_data = { sources = sources, targets = targets } | |
146 } | |
147 | |
148 self[new_object.data.id.text] = new_object | |
149 return new_object.data.id.text | |
150 else return false end | |
151 end | |
152 | |
153 -- // Handler Functions // | |
154 | |
155 local function report_handler(event) | |
156 local origin, stanza = event.origin, event.stanza | |
157 | |
158 incidents:add(stanza, true) | |
159 return origin.send(st.reply(stanza)) | |
160 end | |
161 | |
162 local function inquiry_handler(event) | |
163 local origin, stanza = event.origin, event.stanza | |
164 | |
165 local inc_id = stanza:get_child("inquiry", xmlns_inc):get_child("Incident", xmlns_iodef):get_child("IncidentID"):get_text() | |
166 if incidents[inc_id] then | |
167 module:log("debug", "Server %s queried for incident %s which we know about, sending it", stanza.attr.from, inc_id) | |
168 local report_iq = stanza_construct(incidents[inc_id]) | |
169 report_iq.attr.from = stanza.attr.to | |
170 report_iq.attr.to = stanza.attr.from | |
171 report_iq.attr.type = "set" | |
172 | |
173 origin.send(st.reply(stanza)) | |
174 origin.send(report_iq) | |
175 return true | |
176 else | |
177 module:log("error", "Server %s queried for incident %s but we don't know about it", stanza.attr.from, inc_id) | |
178 origin.send(st.error_reply(stanza, "cancel", "item-not-found")) ; return true | |
179 end | |
180 end | |
181 | |
182 local function request_handler(event) | |
183 local origin, stanza = event.origin, event.stanza | |
184 | |
185 local req_id = stanza:get_child("request", xmlns_inc):get_child("Incident", xmlns_iodef):get_child("IncidentID"):get_text() | |
186 if not incidents[req_id] then | |
187 origin.send(st.error_reply(stanza, "cancel", "item-not-found")) ; return true | |
188 else | |
189 origin.send(st.reply(stanza)) ; return true | |
190 end | |
191 end | |
192 | |
193 local function response_handler(event) | |
194 local origin, stanza = event.origin, event.stanza | |
195 | |
196 local res_id = stanza:get_child("response", xmlns_inc):get_child("Incident", xmlns_iodef):get_child("IncidentID"):get_text() | |
197 if incidents[res_id] then | |
198 incidents[res_id] = nil | |
199 incidents:add(stanza, true) | |
200 origin.send(st.reply(stanza)) ; return true | |
201 else | |
202 origin.send(st.error_reply(stanza, "cancel", "item-not-found")) ; return true | |
203 end | |
204 end | |
205 | |
206 local function results_handler(event) return true end -- TODO results handling | |
207 | |
208 -- // Adhoc Commands // | |
209 | |
210 local function list_incidents_command_handler(self, data, state) | |
211 local list_incidents_layout = ih_lib.render_list(incidents) | |
212 | |
213 if state then | |
214 if state.step == 1 then | |
215 if data.action == "cancel" then | |
216 return { status = "canceled" } | |
217 elseif data.action == "prev" then | |
218 return { status = "executing", actions = { "next", "cancel", default = "next" }, form = list_incidents_layout }, {} | |
219 end | |
220 | |
221 local single_incident_layout = state.form_layout | |
222 local fields = single_incident_layout:data(data.form) | |
223 | |
224 if fields.response then | |
225 incidents[state.id].status = "closed" | |
226 | |
227 local iq_send = ih_lib.stanza_construct(incidents[state.id]) | |
228 module:send(iq_send) | |
229 return { status = "completed", info = "Response sent." } | |
230 else | |
231 return { status = "completed" } | |
232 end | |
233 else | |
234 if data.action == "cancel" then return { status = "canceled" } end | |
235 local fields = list_incidents_layout:data(data.form) | |
236 | |
237 if fields.ids then | |
238 local single_incident_layout = ih_lib.render_single(incidents[fields.ids]) | |
239 return { status = "executing", actions = { "prev", "cancel", "complete", default = "complete" }, form = single_incident_layout }, { step = 1, form_layout = single_incident_layout, id = fields.ids } | |
240 else | |
241 return { status = "completed", error = { message = "You need to select the report ID to continue." } } | |
242 end | |
243 end | |
244 else | |
245 return { status = "executing", actions = { "next", "cancel", default = "next" }, form = list_incidents_layout }, {} | |
246 end | |
247 end | |
248 | |
249 local function send_inquiry_command_handler(self, data, state) | |
250 local send_inquiry_layout = dataforms_new{ | |
251 title = "Send an inquiry about an incident report to a host"; | |
252 instructions = "Please specify both the server host and the incident ID."; | |
253 | |
254 { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/commands" }; | |
255 { name = "server", type = "text-single", label = "Server to inquiry" }; | |
256 { name = "hostname", type = "text-single", label = "Involved incident host" }; | |
257 { name = "id", type = "text-single", label = "Incident ID" }; | |
258 } | |
259 | |
260 if state then | |
261 if data.action == "cancel" then return { status = "canceled" } end | |
262 local fields = send_inquiry_layout:data(data.form) | |
263 | |
264 if not fields.hostname or not fields.id or not fields.server then | |
265 return { status = "completed", error = { message = "You must supply the server to quest, the involved incident host and the incident ID." } } | |
266 else | |
267 local iq_send = st.iq({ from = my_host, to = fields.server, type = "get" }) | |
268 :tag("inquiry", { xmlns = xmlns_inc }) | |
269 :tag("Incident", { xmlns = xmlns_iodef, purpose = "traceback" }) | |
270 :tag("IncidentID", { name = data.hostname }):text(fields.id):up():up():up() | |
271 | |
272 module:log("debug", "Sending incident inquiry to %s", fields.server) | |
273 module:send(iq_send) | |
274 return { status = "completed", info = "Inquiry sent, if an answer can be obtained from the remote server it'll be listed between incidents." } | |
275 end | |
276 else | |
277 return { status = "executing", form = send_inquiry_layout }, "executing" | |
278 end | |
279 end | |
280 | |
281 local function rr_command_handler(self, data, state, formtype) | |
282 local send_layout = ih_lib.get_incident_layout(formtype) | |
283 local err_no_fields = { status = "completed", error = { message = "You need to fill all fields, except the eventual related incident." } } | |
284 local err_proc = { status = "completed", error = { message = "There was an error processing your request, check out the syntax" } } | |
285 | |
286 if state then | |
287 if data.action == "cancel" then return { status = "canceled" } end | |
288 local fields = send_layout:data(data.form) | |
289 | |
290 if fields.started and fields.ended and fields.reported and fields.description and fields.contacts and | |
291 fields.impact and fields.sources and fields.targets and fields.entity then | |
292 if formtype == "request" and not fields.expectation then return err_no_fields end | |
293 local id = incidents:new_object(fields, formtype) | |
294 if not id then return err_proc end | |
295 | |
296 local stanza = ih_lib.stanza_construct(id) | |
297 stanza.attr.from = my_host | |
298 stanza.attr.to = fields.entity | |
299 module:log("debug","Sending incident %s stanza to: %s", formtype, stanza.attr.to) | |
300 module:send(stanza) | |
301 | |
302 return { status = "completed", info = string.format("Incident %s sent to %s.", formtype, fields.entity) } | |
303 else | |
304 return err_no_fields | |
305 end | |
306 else | |
307 return { status = "executing", form = send_layout }, "executing" | |
308 end | |
309 end | |
310 | |
311 local function send_report_command_handler(self, data, state) | |
312 return rr_command_handler(self, data, state, "report") | |
313 end | |
314 | |
315 local function send_request_command_handler(self, data, state) | |
316 return rr_command_handler(self, data, state, "request") | |
317 end | |
318 | |
319 local adhoc_new = module:require "adhoc".new | |
320 local list_incidents_descriptor = adhoc_new("List Incidents", xmlns_inc.."#list", list_incidents_command_handler, "admin") | |
321 local send_inquiry_descriptor = adhoc_new("Send Incident Inquiry", xmlns_inc.."#send_inquiry", send_inquiry_command_handler, "admin") | |
322 local send_report_descriptor = adhoc_new("Send Incident Report", xmlns_inc.."#send_report", send_report_command_handler, "admin") | |
323 local send_request_descriptor = adhoc_new("Send Incident Request", xmlns_inc.."#send_request", send_request_command_handler, "admin") | |
324 module:provides("adhoc", list_incidents_descriptor) | |
325 module:provides("adhoc", send_inquiry_descriptor) | |
326 module:provides("adhoc", send_report_descriptor) | |
327 module:provides("adhoc", send_request_descriptor) | |
328 | |
329 -- // Hooks // | |
330 | |
331 module:hook("iq-set/host/urn:xmpp:incident:2:report", report_handler) | |
332 module:hook("iq-get/host/urn:xmpp:incident:2:inquiry", inquiry_handler) | |
333 module:hook("iq-get/host/urn:xmpp:incident:2:request", request_handler) | |
334 module:hook("iq-set/host/urn:xmpp:incident:2:response", response_handler) | |
335 module:hook("iq-result/host/urn:xmpp:incident:2", results_handler) | |
336 | |
337 -- // Module Methods // | |
338 | |
339 module.load = function() | |
340 if datamanager.load("incidents", my_host, "incidents_store") then incidents = datamanager.load("incidents", my_host, "incidents_store") end | |
341 setmetatable(incidents, _inc_mt) ; incidents:init() | |
342 end | |
343 | |
344 module.save = function() | |
345 return { incidents = incidents } | |
346 end | |
347 | |
348 module.restore = function(data) | |
349 incidents = data.incidents or {} | |
350 setmetatable(incidents, _inc_mt) ; incidents:init() | |
351 end |