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

Revision 4a0da0e513a734ae484e56cee0fdc99f321e3766, 9.7 kB (checked in by Philip Maddox <pmaddox@circonus.com>, 3 years ago)

Added support for EHLO response metrics

  • Property mode set to 100644
Line 
1 -- Copyright (c) 2008, 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>smtp</name>
37   <description><para>Send an email via an SMTP server.</para></description>
38   <loader>lua</loader>
39   <object>noit.module.smtp</object>
40   <moduleconfig />
41   <checkconfig>
42     <parameter name="port" required="optional" default="25"
43                allowed="\d+">Specifies the TCP port to connect to.</parameter>
44     <parameter name="ehlo" required="optional" default="noit.local"
45                allowed=".+">Specifies the EHLO parameter.</parameter>
46     <parameter name="from" required="optional" default=""
47                allowed=".+">Specifies the envelope sender.</parameter>
48     <parameter name="to" required="required"
49                allowed=".+">Specifies the envelope recipient.</parameter>
50     <parameter name="payload" required="optional" default="Subject: Testing"
51                allowed=".+">Specifies the payload sent (on the wire). CR LF DOT CR LF is appended automatically.</parameter>
52     <parameter name="starttls" required="optional" default="false"
53                allowed="(?:true|false)">Specified if the client should attempt a STARTTLS upgrade</parameter>
54     <parameter name="ca_chain"
55                required="optional"
56                allowed=".+">A path to a file containing all the certificate authorities that should be loaded to validate the remote certificate (for SSL checks).</parameter>
57     <parameter name="certificate_file"
58                required="optional"
59                allowed=".+">A path to a file containing the client certificate that will be presented to the remote server (for SSL checks).</parameter>
60     <parameter name="key_file"
61                required="optional"
62                allowed=".+">A path to a file containing key to be used in conjunction with the cilent certificate (for SSL checks).</parameter>
63     <parameter name="ciphers"
64                required="optional"
65                allowed=".+">A list of ciphers to be used in the SSL protocol (for SSL checks).</parameter>
66   </checkconfig>
67   <examples>
68     <example>
69       <title>Send an email to test SMTP service.</title>
70       <para>The following example sends an email via 10.80.117.6 from test@omniti.com to devnull@omniti.com</para>
71       <programlisting><![CDATA[
72       <noit>
73         <modules>
74           <loader image="lua" name="lua">
75             <config><directory>/opt/reconnoiter/libexec/modules-lua/?.lua</directory></config>
76           </loader>
77           <module loader="lua" name="smtp" object="noit.module.smtp"/>
78         </modules>
79         <checks>
80           <check uuid="2d42adbc-7c7a-11dd-a48f-4f59e0b654d3" module="smtp" target="10.80.117.6">
81             <config>
82               <from>test@omniti.com</from>
83               <to>devnull@omniti.com</to>
84             </config>
85           </check>
86         </checks>
87       </noit>
88       ]]></programlisting>
89     </example>
90   </examples>
91 </module>
92 ]=]);
93   return 0
94 end
95
96 function init(module)
97   return 0
98 end
99
100 function config(module, options)
101   return 0
102 end
103
104 local function read_cmd(e)
105   local final_status, out
106   final_status, out = 0, ""
107   repeat
108     local str = e:read("\r\n")
109     local status, c, message = string.match(str, "^(%d+)([-%s])(.+)$")
110     if not status then
111       return 421, "[internal error]"
112     end
113     final_status = status
114     if string.len(out) > 0 then
115       out = string.format( "%s %s", out, message)
116     else
117       out = message
118     end
119   until c ~= "-"
120   return (final_status+0), out
121 end
122
123 local function write_cmd(e, cmd)
124   e:write(cmd);
125   e:write("\r\n");
126 end
127
128 local function mkaction(e, check)
129   return function (phase, tosend, expected_code)
130     local start_time = noit.timeval.now()
131     local success = true
132     if tosend then
133       write_cmd(e, tosend)
134     end
135     local actual_code, message = read_cmd(e)
136     if expected_code ~= actual_code then
137       check.status(string.format("%d/%d %s", expected_code, actual_code, message))
138       check.bad()
139       success = false
140     else
141       check.available()
142     end
143     local elapsed = noit.timeval.now() - start_time
144     local elapsed_ms = math.floor(tostring(elapsed) * 1000)
145     check.metric(phase .. "_time",  elapsed_ms)
146
147     if phase == 'ehlo' and message ~= nil then
148       local fields = string.split(message, "\r\n")
149       if fields ~= nil then
150         local response = ""
151         local extensions = ""
152         if fields[1] ~= nil then
153           response = fields[1]
154           check.metric("ehlo_response_banner", response)
155         end
156         if expected_code == actual_code and fields[1] ~= nil then
157           table.remove(fields, 1)
158           for line, value in pairs(fields) do
159             if value ~= nil and value ~= "" then
160               value = value:gsub("^%s*(.-)%s*$", "%1")
161               local subfields = string.split(value, "%s+", 1)
162               if subfields ~= nil and subfields[1] ~= nil then
163                 local header = subfields[1]
164                 if subfields[2] ~= nil then
165                   check.metric("ehlo_response_" .. string.lower(header), subfields[2])
166                 else
167                   check.metric("ehlo_response_" .. string.lower(header), 'true')
168                 end
169               end
170             end
171           end
172         end
173       end
174     end
175     return success
176   end
177 end
178
179 function initiate(module, check)
180   local starttime = noit.timeval.now()
181   local e = noit.socket(check.target_ip)
182   local rv, err = e:connect(check.target_ip, check.config.port or 25)
183   check.unavailable()
184
185   if rv ~= 0 then
186     check.bad()
187     check.status(err or message or "no connection")
188     return
189   end
190
191   local try_starttls = check.config.starttls == "true" or check.config.starttls == "on"
192   local good = true
193   local ehlo = string.format("EHLO %s", check.config.ehlo or "noit.local")
194   local mailfrom = string.format("MAIL FROM:<%s>", check.config.from or "")
195   local rcptto = string.format("RCPT TO:<%s>", check.config.to)
196   local payload = check.config.payload or "Subject: Test\n\nHello."
197   payload = payload:gsub("\n", "\r\n")
198   local status = 'connected'
199   local action = mkaction(e, check)
200
201   if     not action("banner", nil, 220)
202       or not action("ehlo", ehlo, 250) then return end
203
204   if try_starttls then
205     local starttls  = action("starttls", "STARTTLS", 220)
206     e:ssl_upgrade_socket(check.config.certificate_file, check.config.key_file,
207                          check.config.ca_chain, check.config.ciphers)
208
209     local ssl_ctx = e:ssl_ctx()
210     if ssl_ctx ~= nil then
211       if ssl_ctx.error ~= nil then status = status .. ',sslerror' end
212       check.metric_string("cert_error", ssl_ctx.error)
213       check.metric_string("cert_issuer", ssl_ctx.issuer)
214       check.metric_string("cert_subject", ssl_ctx.subject)
215       check.metric_uint32("cert_start", ssl_ctx.start_time)
216       check.metric_uint32("cert_end", ssl_ctx.end_time)
217       check.metric_int32("cert_end_in", ssl_ctx.end_time - os.time())
218       if noit.timeval.seconds(starttime) > ssl_ctx.end_time then
219         good = false
220         status = status .. ',ssl=expired'
221       end
222     end
223
224     if not action("ehlo", ehlo, 250) then return end
225   end
226
227   if     action("mailfrom", mailfrom, 250)
228      and action("rcptto", rcptto, 250)
229      and action("data", "DATA", 354)
230      and action("body", payload .. "\r\n.", 250)
231      and action("quit", "QUIT", 221)
232   then
233     status = status .. ',sent'
234   else
235     return
236   end
237   check.status(status)
238   if good then check.good() end
239
240   local elapsed = noit.timeval.now() - starttime
241   local elapsed_ms = math.floor(tostring(elapsed) * 1000)
242   check.metric("duration",  elapsed_ms)
243 end
244
245 -- from http://www.wellho.net/resources/ex.php4?item=u108/split
246 -- modified to split up to 'max' times
247 function string:split(delimiter, max)
248   local result = { }
249   local from  = 1
250   local delim_from, delim_to = string.find( self, delimiter, from  )
251   local nb = 0
252   if max == nil then
253     max = 0
254   end
255   while delim_from do
256     local insert_string = string.sub( self, from , delim_from-1 )
257     nb = nb + 1
258     table.insert( result, insert_string )
259     from  = delim_to + 1
260     delim_from, delim_to = string.find( self, delimiter, from  )
261     if nb == max then
262       break
263     end
264   end
265   local last_res = string.sub (self, from)
266   if last_res ~= nil and last_res ~= "" then
267     table.insert( result, last_res )
268   end
269   return result
270 end
271
Note: See TracBrowser for help on using the browser.