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

Revision 28a2cef0014f95189edcb7f1302267f00f6116c2, 12.5 kB (checked in by Dan Di Spaltro <dan@cloudkick.com>, 2 years ago)

Use a default string if not included "to"

  • 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="optional"
49                allowed=".+">Specifies the envelope recipient, if blank issue quit.</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     <parameter name="sasl_authentication"
67                required="optional"
68                default="off"
69                allowed="(?:off|login|plain)">Specifies the type of SASL Authentication to use</parameter>
70     <parameter name="sasl_user"
71                required="optional"
72                default=""
73                allowed=".+">The SASL Authentication username</parameter>
74     <parameter name="sasl_password"
75                required="optional"
76                default=""
77                allowed=".+">The SASL Authentication password</parameter>
78     <parameter name="sasl_auth_id"
79                required="optional"
80                default=""
81                allowed=".+">The SASL Authorization Identity</parameter>
82   </checkconfig>
83   <examples>
84     <example>
85       <title>Send an email to test SMTP service.</title>
86       <para>The following example sends an email via 10.80.117.6 from test@omniti.com to devnull@omniti.com</para>
87       <programlisting><![CDATA[
88       <noit>
89         <modules>
90           <loader image="lua" name="lua">
91             <config><directory>/opt/reconnoiter/libexec/modules-lua/?.lua</directory></config>
92           </loader>
93           <module loader="lua" name="smtp" object="noit.module.smtp"/>
94         </modules>
95         <checks>
96           <check uuid="2d42adbc-7c7a-11dd-a48f-4f59e0b654d3" module="smtp" target="10.80.117.6">
97             <config>
98               <from>test@omniti.com</from>
99               <to>devnull@omniti.com</to>
100             </config>
101           </check>
102         </checks>
103       </noit>
104       ]]></programlisting>
105     </example>
106   </examples>
107 </module>
108 ]=]);
109   return 0
110 end
111
112 function init(module)
113   return 0
114 end
115
116 function config(module, options)
117   return 0
118 end
119
120 local function read_cmd(e)
121   local final_status, out
122   final_status, out = 0, ""
123   repeat
124     local str = e:read("\r\n")
125     local status, c, message = string.match(str, "^(%d+)([-%s])(.+)$")
126     if not status then
127       return 421, "[internal error]"
128     end
129     final_status = status
130     if string.len(out) > 0 then
131       out = string.format( "%s %s", out, message)
132     else
133       out = message
134     end
135   until c ~= "-"
136   return (final_status+0), out
137 end
138
139 local function write_cmd(e, cmd)
140   e:write(cmd);
141   e:write("\r\n");
142 end
143
144 local function mkaction(e, check)
145   return function (phase, tosend, expected_code)
146     local start_time = noit.timeval.now()
147     local success = true
148     if tosend then
149       write_cmd(e, tosend)
150     end
151     local actual_code, message = read_cmd(e)
152     if expected_code ~= actual_code then
153       check.status(string.format("%d/%d %s", expected_code, actual_code, message))
154       check.bad()
155       success = false
156     else
157       check.available()
158     end
159     local elapsed = noit.timeval.now() - start_time
160     local elapsed_ms = math.floor(tostring(elapsed) * 1000)
161     check.metric(phase .. "_time",  elapsed_ms)
162
163     if phase == 'ehlo' and message ~= nil then
164       local fields = noit.extras.split(message, "\r\n")
165       if fields ~= nil then
166         local response = ""
167         local extensions = ""
168         if fields[1] ~= nil then
169           response = fields[1]
170           check.metric("ehlo_response_banner", response)
171         end
172         if expected_code == actual_code and fields[1] ~= nil then
173           table.remove(fields, 1)
174           for line, value in pairs(fields) do
175             if value ~= nil and value ~= "" then
176               value = value:gsub("^%s*(.-)%s*$", "%1")
177               local subfields = noit.extras.split(value, "%s+", 1)
178               if subfields ~= nil and subfields[1] ~= nil then
179                 local header = subfields[1]
180                 if subfields[2] ~= nil then
181                   check.metric("ehlo_response_" .. string.lower(header), subfields[2])
182                 else
183                   check.metric("ehlo_response_" .. string.lower(header), 'true')
184                 end
185               end
186             end
187           end
188         end
189       end
190     end
191     return success
192   end
193 end
194
195 local function mk_sasllogin(e, check)
196   return function (username, password)
197     local start_time = noit.timeval.now()
198     local actual_code = 0
199     local message = ""
200     local success = "true"
201     write_cmd(e, "AUTH LOGIN")
202     actual_code, message = read_cmd(e)
203     if actual_code ~= 334 then
204       success = "false"
205     end
206     if success == "true" then
207       write_cmd(e, username)
208       actual_code, message = read_cmd(e)
209       if actual_code ~= 334 then
210         success = "false"
211       end
212     end
213     if success == "true" then
214       write_cmd(e, password)
215       actual_code, message = read_cmd(e)
216       if actual_code ~= 235 then
217         success = "false"
218       end
219     end
220     local elapsed = noit.timeval.now() - start_time
221     local elapsed_ms = math.floor(tostring(elapsed) * 1000)
222     check.metric("sasl_login_time",  elapsed_ms)
223     check.metric("sasl_login_success", success)
224     check.metric("sasl_login_response", message)
225     return success
226   end
227 end
228
229 local function mk_saslplain(e, check)
230   return function (cmd_string)
231     local start_time = noit.timeval.now()
232     local actual_code = 0
233     local message = ""
234     local success = "true"
235     write_cmd(e, "AUTH PLAIN")
236     actual_code, message = read_cmd(e)
237     if actual_code ~= 334 then
238       success = "false"
239     end
240     if success == "true" then
241       write_cmd(e, cmd_string)
242       actual_code, message = read_cmd(e)
243       if actual_code ~= 235 then
244         success = "false"
245       end
246     end
247     local elapsed = noit.timeval.now() - start_time
248     local elapsed_ms = math.floor(tostring(elapsed) * 1000)
249     check.metric("sasl_plain_time",  elapsed_ms)
250     check.metric("sasl_plain_success", success)
251     check.metric("sasl_plain_response", message)
252     return success
253   end
254 end
255
256 function ex_actions(action, check, mailfrom, rcptto, payload)
257   local status = ''
258   -- Only proceed if from is present, empty or not
259   if check.config.from == "" or check.config.from then
260     action("mailfrom", mailfrom, 250)
261   else
262      return status
263   end
264
265   if check.config.to then
266      action("rcptto", rcptto, 250)
267   else
268      return status
269   end
270
271   -- Since the way the protocol works, the to address may be in the payload...
272   if payload then
273      action("data", "DATA", 354)
274      action("body", payload .. "\r\n.", 250)
275      status = ',sent'
276   end
277   return status
278 end
279
280 function initiate(module, check)
281   local starttime = noit.timeval.now()
282   local e = noit.socket(check.target_ip)
283   local rv, err = e:connect(check.target_ip, check.config.port or 25)
284   local action_result
285   check.unavailable()
286
287   if rv ~= 0 then
288     check.bad()
289     check.status(err or message or "no connection")
290     return
291   end
292
293   local try_starttls = check.config.starttls == "true" or check.config.starttls == "on"
294   local good = true
295   local ehlo = string.format("EHLO %s", check.config.ehlo or "noit.local")
296   local mailfrom = string.format("MAIL FROM:<%s>", check.config.from or "")
297   local rcptto = string.format("RCPT TO:<%s>", check.config.to or "")
298   local payload = check.config.payload or "Subject: Test\n\nHello."
299   payload = payload:gsub("\n", "\r\n")
300   local status = 'connected'
301   local action = mkaction(e, check)
302   local sasl_login = mk_sasllogin(e, check)
303   local sasl_plain = mk_saslplain(e, check)
304
305   if     not action("banner", nil, 220)
306       or not action("ehlo", ehlo, 250) then return end
307
308   if try_starttls then
309     local starttls  = action("starttls", "STARTTLS", 220)
310     e:ssl_upgrade_socket(check.config.certificate_file, check.config.key_file,
311                          check.config.ca_chain, check.config.ciphers)
312
313     local ssl_ctx = e:ssl_ctx()
314     if ssl_ctx ~= nil then
315       if ssl_ctx.error ~= nil then status = status .. ',sslerror' end
316       check.metric_string("cert_error", ssl_ctx.error)
317       check.metric_string("cert_issuer", ssl_ctx.issuer)
318       check.metric_string("cert_subject", ssl_ctx.subject)
319       check.metric_uint32("cert_start", ssl_ctx.start_time)
320       check.metric_uint32("cert_end", ssl_ctx.end_time)
321       check.metric_int32("cert_end_in", ssl_ctx.end_time - os.time())
322       if noit.timeval.seconds(starttime) > ssl_ctx.end_time then
323         good = false
324         status = status .. ',ssl=expired'
325       end
326     end
327
328     if not action("ehlo", ehlo, 250) then return end
329   end
330
331   if check.config.sasl_authentication ~= nil then
332     if check.config.sasl_authentication == "login" then
333       sasl_login(noit.base64_encode(check.config.sasl_user or ""), noit.base64_encode(check.config.sasl_password or ""))
334     elseif check.config.sasl_authentication == "plain" then
335       sasl_plain(noit.base64_encode((check.config.sasl_auth_id or "") .. "\0" .. (check.config.sasl_user or "") .. "\0" .. (check.config.sasl_password or "")))
336     end
337   end
338
339   action_result = ex_actions(action, check, mailfrom, rcptto, payload)
340   -- Always issue quit
341   action("quit", "QUIT", 221)
342
343   status = status .. action_result
344
345   check.status(status)
346   if good then check.good() end
347
348   local elapsed = noit.timeval.now() - starttime
349   local elapsed_ms = math.floor(tostring(elapsed) * 1000)
350   check.metric("duration",  elapsed_ms)
351 end
352
Note: See TracBrowser for help on using the browser.