root/src/modules-lua/noit/module/imap.lua

Revision 353258a7cab6bd5c7069ab335642da33a6c67a2a, 11.6 kB (checked in by Philip Maddox <pmaddox@circonus.com>, 2 years ago)

Added improved SSL certificate validation to IMAP and TCP checks (use the default-ca-chain.crt file if none is provided by the user, allow users to pass a header_Host to the check to verify the CN of the certificate). Tweaked the HTTP check to not throw a certificate name mismatch error if no header_Host field is provided

  • Property mode set to 100644
Line 
1 -- Copyright (c) 2011, OmniTI Computer Consulting, Inc.
2 -- All rights reserved.
3 --
4 -- Redistribution and use in source and binary forms, with or without
5 -- modification, are permitted provided that the following conditions are
6 -- met:
7 --
8 --     * Redistributions of source code must retain the above copyright
9 --       notice, this list of conditions and the following disclaimer.
10 --     * Redistributions in binary form must reproduce the above
11 --       copyright notice, this list of conditions and the following
12 --       disclaimer in the documentation and/or other materials provided
13 --       with the distribution.
14 --     * Neither the name OmniTI Computer Consulting, Inc. nor the names
15 --       of its contributors may be used to endorse or promote products
16 --       derived from this software without specific prior written
17 --       permission.
18 --
19 -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 -- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 -- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 -- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 -- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 -- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 -- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 -- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 -- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 -- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 -- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31 module(..., package.seeall)
32
33 function onload(image)
34   image.xml_description([=[
35 <module>
36   <name>imap</name>
37   <description><para>IMAP metrics check.</para></description>
38   <loader>lua</loader>
39   <object>noit.module.imap</object>
40   <moduleconfig />
41   <checkconfig>
42     <parameter name="port" required="required"
43                allowed="\d+">Specifies the port on which the management interface can be reached.</parameter>
44     <parameter name="auth_user" required="required"
45               allowed=".+">The IMAP user.</parameter>
46     <parameter name="auth_password" required="required"
47               allowed=".+">The IMAP password.</parameter>
48     <parameter name="folder" required="optional" default="INBOX"
49               allowed=".+">The folder that should be examined.</parameter>
50     <parameter name="search" required="optional"
51               allowed=".+">Specify an optional IMAP SEARCH operation to execute after EXAMINE</parameter>
52     <parameter name="fetch" required="optional" default="false"
53               allowed="(?:true|false|on|off)">Fetch either that highest UID or last SEARCH result.</parameter>
54     <parameter name="use_ssl" required="optional" allowed="^(?:true|false|on|off)$" default="false">Upgrade TCP connection to use SSL.</parameter>
55     <parameter name="ca_chain"
56                required="optional"
57                allowed=".+">A path to a file containing all the certificate authorities that should be loaded to validat
58 e the remote certificate (for SSL checks).</parameter>
59     <parameter name="certificate_file"
60                required="optional"
61                allowed=".+">A path to a file containing the client certificate that will be presented to the remote serv
62 er (for SSL checks).</parameter>
63     <parameter name="key_file"
64                required="optional"
65                allowed=".+">A path to a file containing key to be used in conjunction with the cilent certificate (for S
66 SL checks).</parameter>
67     <parameter name="ciphers"
68                required="optional"
69                allowed=".+">A list of ciphers to be used in the SSL protocol (for SSL checks).</parameter>
70     <parameter name="header_Host"
71                required="optional"
72                allowed=".+">The host header to validate against the SSL certificate (for SSL checks).</parameter>
73   </checkconfig>
74   <examples>
75     <example>
76       <title>Checking IMAP connection.</title>
77       <para>This example checks IMAP connection with and without SSL.</para>
78       <programlisting><![CDATA[
79       <noit>
80         <modules>
81           <loader image="lua" name="lua">
82             <config><directory>/opt/reconnoiter/libexec/modules-lua/?.lua</directory></config>
83           </loader>
84           <module loader="lua" name="imap" object="noit.module.imap" />
85         </modules>
86         <checks>
87           <imaps target="10.0.7.2" module="imap" period="10000" timeout="5000">
88             <check uuid="79ba881e-ad2e-11de-9fb0-a322e3288ca7" name="imap">
89               <config>
90                 <port>143</port>
91                 <auth_user>bob</auth_user>
92                 <auth_password>bob</auth_password>
93               </config>
94             </check>
95             <check uuid="a18659c2-add8-11de-bd01-7ff0e1a67246" name="imaps">
96               <config>
97                 <port>993</port>
98                 <auth_user>bob</auth_user>
99                 <auth_password>bob</auth_password>
100                 <use_ssl>true</use_ssl>
101               </config>
102             </check>
103           </imaps>
104         </checks>
105       </noit>
106     ]]></programlisting>
107     </example>
108   </examples>
109
110 </module>
111 ]=]);
112   return 0
113 end
114
115 function init(module)
116   return 0
117 end
118
119 function config(module, options)
120   return 0
121 end
122
123 function elapsed(check, name, starttime, endtime)
124     local elapsedtime = endtime - starttime
125     local seconds = string.format('%.3f', noit.timeval.seconds(elapsedtime))
126     check.metric_uint32(name, math.floor(seconds * 1000 + 0.5))
127     return seconds
128 end
129
130 function get_banner(e)
131   local line = e:read("\n")
132   local o,eidx,state,rest
133   o,eidx,state,rest = line:find('[*]%s+(%w+)%s*(.*)')
134   return state, rest
135 end
136
137 function issue_cmd(e, tok, str)
138   local t = {}
139   local bad = {}
140   e:write(tok .. " " .. str .. "\r\n")
141   while true do
142     local line = e:read("\n")
143     line = line:gsub('[\r\n]','')
144     local o,eidx,p1,p2
145     o,eidx,p1,p2 = line:find(tok .. '%s+(%w+)%s*(.*)')
146     if o == 1 then
147       table.insert(t,p1.." "..p2)
148       return p1, t, bad
149     end
150     o,eidx,p1 = line:find("\*%s+(.+)")
151     if o == 1 then table.insert(t,p1) else table.insert(bad,line) end
152   end
153   return nil, t, bad
154 end
155
156 function initiate(module, check)
157   local starttime = noit.timeval.now()
158   local folder = check.config.folder or 'INBOX'
159   check.bad()
160   check.unavailable()
161   check.status("unknown error")
162   local port = check.config.port
163   local good = false
164   local status = ""
165   local use_ssl = false
166   local _tok = 0
167   local last_msg = 0
168   local host_header = check.config.header_Host or ''
169
170   if check.target_ip == nil then
171     check.status("dns resolution failure")
172     return
173   end
174
175   function tok()
176     _tok = _tok + 1
177     return '.rN.A.' .. _tok
178   end
179
180   -- SSL
181   if check.config.use_ssl == "true" or check.config.use_ssl == "on" then
182     use_ssl = true
183   end
184
185   if port == nil then
186     if use_ssl then port = 993 else port = 143 end
187   end
188
189   local e = noit.socket(check.target_ip)
190   local rv, err = e:connect(check.target_ip, port)
191
192   if rv ~= 0 then
193     check.status(err or "connect error")
194     return
195   end
196
197   local ca_chain =
198      noit.conf_get_string("/noit/eventer/config/default_ca_chain")
199
200   if check.config.ca_chain ~= nil and check.config.ca_chain ~= '' then
201     ca_chain = check.config.ca_chain
202   end
203
204   if use_ssl == true then
205     rv, err = e:ssl_upgrade_socket(check.config.certificate_file,
206                                         check.config.key_file,
207                                         ca_chain,
208                                         check.config.ciphers)
209   end
210
211   local connecttime = noit.timeval.now()
212   elapsed(check, "tt_connect", starttime, connecttime)
213   if rv ~= 0 then
214     check.status(err or "connection failed")
215     return
216   end
217
218   status = "connected"
219   check.available()
220   good = true
221
222   -- ssl metrics
223   local ssl_ctx = e:ssl_ctx()
224   if ssl_ctx ~= nil then
225     local header_match_error = nil
226     if host_header ~= '' then
227       header_match_error = noit.extras.check_host_header_against_certificate(host_header, ssl_ctx.subject, ssl_ctx.san_list)
228     end
229     if ssl_ctx.error ~= nil then status = status .. ',sslerror' end
230     if header_match_error == nil then
231       check.metric_string("cert_error", ssl_ctx.error)
232     elseif ssl_ctx.error == nil then
233       check.metric_string("cert_error", header_match_error)
234     else
235       check.metric_string("cert_error", ssl_ctx.error .. ', ' .. header_match_error)
236     end
237     check.metric_string("cert_issuer", ssl_ctx.issuer)
238     check.metric_string("cert_subject", ssl_ctx.subject)
239     if ssl_ctx.san_list ~= nil then
240       check.metric_string("cert_subject_alternative_names", ssl_ctx.san_list)
241     end
242     check.metric_uint32("cert_start", ssl_ctx.start_time)
243     check.metric_uint32("cert_end", ssl_ctx.end_time)
244     if noit.timeval.seconds(starttime) > ssl_ctx.end_time then
245       good = false
246       status = status .. ',ssl=expired'
247     end
248   end
249
250   -- match banner
251   local ok, banner
252   ok, banner = get_banner(e)
253   local firstbytetime = noit.timeval.now()
254   elapsed(check, "tt_firstbyte", starttime, firstbytetime)
255   check.metric_string('banner', ok)
256   if ok ~= "OK" then
257     check.metric_string('banner', ok .. " " .. banner)
258     good = false
259   end
260   local state, lines, errors
261
262   -- login
263   local lstart = noit.timeval.now()
264   state, lines, errors = issue_cmd(e, tok(), "LOGIN " ..
265                                              check.config.auth_user .. " " ..
266                                              check.config.auth_password)
267   elapsed(check, "login`duration", lstart, noit.timeval.now())
268   check.metric_string("login`status", lines[ # lines ])
269   if state ~= "OK" then good = false
270   else
271     -- Examine the mailbox
272     local estart = noit.timeval.now()
273     state, lines, errors = issue_cmd(e, tok(), "EXAMINE " .. folder)
274     elapsed(check, 'examine`duration', estart, noit.timeval.now())
275     check.metric_string('examine`status', lines[ # lines ])
276     if ok ~= "OK" then good = false
277     else
278       local num, type, num_exists, num_recent = nil
279       for i,v in ipairs(lines) do
280         num, type = v:match("(%d+)%s+(.+)")
281         if type == "EXISTS" then num_exists = num
282         elseif type == "RECENT" then num_recent = num
283         end
284       end
285       last_msg = num_exists
286       check.metric_uint32('messages`total', num_exists)
287       check.metric_uint32('messages`recent', num_recent)
288     end
289   end
290
291   if check.config.search ~= nil then
292     last_msg = nil
293     local search = check.config.search:gsub("[\r\n]", "")
294     local sstart = noit.timeval.now()
295     state, lines, errors = issue_cmd(e, tok(), "SEARCH " .. search)
296     elapsed(check, "search`duration", sstart, noit.timeval.now())
297     if ok ~= "OK" then good = false
298     else
299       local matches = 0
300       for i,v in ipairs(lines) do
301         local msgs = v:match("SEARCH%s+(.+)")
302         if msgs ~= nil then
303           for m in msgs:gmatch("(%d+)") do
304             matches = matches + 1
305             last_msg = m + 0
306           end
307         end
308       end
309       check.metric_uint32('search`total', matches)
310     end
311   end
312
313   if check.config.fetch ~= nil and
314      (check.config.fetch == "true" or check.config.fetch == "on") and
315      last_msg ~= nil and
316      last_msg > 0 then
317     local fstart = noit.timeval.now()
318     state, lines, errors = issue_cmd(e, tok(), "FETCH " .. last_msg .. " RFC822")
319     elapsed(check, "fetch`duration", fstart, noit.timeval.now())
320     check.metric_string('fetch`status', state)
321   end
322
323   -- bye
324   state, lines, errors = issue_cmd(e, tok(), "LOGOUT")
325   check.metric_string('logout', state)
326
327   -- turnaround time
328   local endtime = noit.timeval.now()
329   local seconds = elapsed(check, "duration", starttime, endtime)
330   status = status .. ',rt=' .. seconds .. 's'
331   if good then check.good() else check.bad() end
332   check.status(status)
333 end
334
Note: See TracBrowser for help on using the browser.