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

Revision 968ebe71a337dec16e88667e24887a2eb3a236c2, 18.7 kB (checked in by Theo Schlossnagle <jesus@omniti.com>, 2 years ago)

bad escape in lua match

  • 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>http</name>
37   <description><para>The http module performs GET requests over either HTTP or HTTPS and checks the return code and optionally the body.</para>
38   </description>
39   <loader>lua</loader>
40   <object>noit.module.http</object>
41   <checkconfig>
42     <parameter name="url"
43                required="required"
44                allowed=".+">The URL including schema and hostname (as you would type into a browser's location bar).</parameter>
45     <parameter name="header_(\S+)"
46                required="optional"
47                allowed=".+">Allows the setting of arbitrary HTTP headers in the request.</parameter>
48     <parameter name="method"
49                required="optional"
50                allowed="\S+"
51                default="GET">The HTTP method to use.</parameter>
52     <parameter name="payload"
53                required="optional"
54                allowed=".*">The information transferred as the payload of an HTTP request.</parameter>
55     <parameter name="auth_method"
56                required="optional"
57                allowed="^(?:Basic|Digest|Auto)$">HTTP Authentication method to use.</parameter>
58     <parameter name="auth_user"
59                required="optional"
60                allowed="[^:]*">The user to authenticate as.</parameter>
61     <parameter name="auth_password"
62                required="optional"
63                allowed=".*">The password to use during authentication.</parameter>
64     <parameter name="ca_chain"
65                required="optional"
66                allowed=".+">A path to a file containing all the certificate authorities that should be loaded to validate the remote certificate (for SSL checks).</parameter>
67     <parameter name="certificate_file"
68                required="optional"
69                allowed=".+">A path to a file containing the client certificate that will be presented to the remote server (for SSL checks).</parameter>
70     <parameter name="key_file"
71                required="optional"
72                allowed=".+">A path to a file containing key to be used in conjunction with the cilent certificate (for SSL checks).</parameter>
73     <parameter name="ciphers"
74                required="optional"
75                allowed=".+">A list of ciphers to be used in the SSL protocol (for SSL checks).</parameter>
76     <parameter name="code"
77                required="optional"
78                default="^200$"
79                allowed=".+">The HTTP code that is expected.  If the code received does not match this regular expression, the check is marked as "bad."</parameter>
80     <parameter name="redirects"
81                required="optional"
82                default="0"
83                allowed="\d+">The maximum number of Location header redirects to follow.</parameter>
84     <parameter name="body"
85                required="optional"
86                allowed=".+">This regular expression is matched against the body of the response. If a match is not found, the check will be marked as "bad."</parameter>
87     <parameter name="body_match_*"
88                required="optional"
89                allowed=".+">This regular expression is matched against the body of the response. If a match is found it is captured and added as a metric. For example, if setting is named 'body_match_foo_bar' and a match is found new metric called 'foo_bar' will be added.</parameter>
90     <parameter name="extract"
91                required="optional"
92                allowed=".+">This regular expression is matched against the body of the response globally.  The first capturing match is the key and the second capturing match is the value.  Each key/value extracted is registered as a metric for the check.</parameter>
93     <parameter name="pcre_match_limit"
94                required="optional"
95                default="10000"
96                allowed="\d+">This sets the PCRE internal match limit (see pcreapi documentation).</parameter>
97     <parameter name="include_body"
98                required="optional"
99                allowed="^(?:true|false|on|off)$"
100                default="false">Include whole response body as a metric with the key 'body'.</parameter>
101     <parameter name="read_limit"
102                required="optional"
103                default="0"
104                allowed="\d+">Sets an approximate limit on the data read (0 means no limit).</parameter>
105   </checkconfig>
106   <examples>
107     <example>
108       <title>Checking an HTTP and HTTPS URL.</title>
109       <para>This example checks the OmniTI Labs website over both HTTP and HTTPS.</para>
110       <programlisting><![CDATA[
111       <noit>
112         <modules>
113           <loader image="lua" name="lua">
114             <config><directory>/opt/reconnoiter/libexec/modules-lua/?.lua</directory></config>
115           </loader>
116           <module loader="lua" name="http" object="noit.module.http" />
117         </modules>
118         <checks>
119           <labs target="8.8.38.5" module="http">
120             <check uuid="fe3e984c-7895-11dd-90c1-c74c31b431f0" name="http">
121               <config><url>http://labs.omniti.com/</url></config>
122             </check>
123             <check uuid="1ecd887a-7896-11dd-b28d-0b4216877f83" name="https">
124               <config><url>https://labs.omniti.com/</url></config>
125             </check>
126           </labs>
127         </checks>
128       </noit>
129     ]]></programlisting>
130     </example>
131   </examples>
132 </module>
133 ]=]);
134   return 0
135 end
136
137 function init(module)
138   return 0
139 end
140
141 function config(module, options)
142   return 0
143 end
144
145 local HttpClient = require 'noit.HttpClient'
146
147 local BODY_MATCHES_PREFIX = 'body_match_'
148
149 function elapsed(check, name, starttime, endtime)
150     local elapsedtime = endtime - starttime
151     local seconds = string.format('%.3f', noit.timeval.seconds(elapsedtime))
152     check.metric_uint32(name, math.floor(seconds * 1000 + 0.5))
153     return seconds
154 end
155
156 function populate_cookie_jar(cookies, host, hdr)
157     if hdr ~= nil then
158         local name, value, trailer =
159             string.match(hdr, "([^=]+)=([^;]+);?%s*(.*)")
160         if name ~= nil then
161             local jar = { }
162             jar.name = name;
163             jar.value = value;
164             for k, v in string.gmatch(trailer, "%s*(%w+)(=%w+)?;?") do
165                 if v == nil then jar[string.lower(k)] = true
166                 else jar[string.lower(k)] = v:sub(2)
167                 end
168             end
169             if jar.domain ~= nil then host = jar.domain end
170             if cookies[host] == nil then cookies[host] = { } end
171             table.insert(cookies[host], jar)
172         end
173     end
174 end
175
176 function has_host(pat, host)
177     if pat == host then return true end
178     if pat:sub(1,1) ~= "." then return false end
179     local revpat = pat:sub(2):reverse()
180     local revhost = host:reverse()
181     if revpat == revhost then return true end
182     if revpat == revhost:sub(1, revpat:len()) then
183         if revhost:sub(pat:len(), pat:len()) == "." then return true end
184     end
185     return false
186 end
187
188 function apply_cookies(headers, cookies, host, uri)
189     for h, jars in pairs(cookies) do
190         if has_host(h, host) then
191             for i, jar in ipairs(jars) do
192                 if jar.path == nil or
193                    uri:sub(1, jar.path:len()) == jar.path then
194                     if headers["Cookie"] == nil then
195                         headers["Cookie"] = jar.name .. "=" .. jar.value
196                     else
197                         headers["Cookie"] = headers["Cookie"] .. "; " ..
198                                             jar.name .. "=" .. jar.value
199                     end
200                 end
201             end
202         end
203     end
204 end
205
206 function initiate(module, check)
207     local url = check.config.url or 'http:///'
208     local schema, host, port, uri = string.match(url, "^(https?)://([^:/]*):?([0-9]*)(/?.*)$");
209     local use_ssl = false
210     local codere = noit.pcre(check.config.code or '^200$')
211     local good = false
212     local starttime = noit.timeval.now()
213     local method = check.config.method or "GET"
214     local max_len = 80
215     local pcre_match_limit = check.config.pcre_match_limit or 10000
216     local redirects = check.config.redirects or 0
217     local include_body = false
218     local read_limit = tonumber(check.config.read_limit) or nil
219
220     -- expect the worst
221     check.bad()
222     check.unavailable()
223
224     if host == nil then host = check.target end
225     if schema == nil then
226         schema = 'http'
227         uri = '/'
228     end
229     if uri == '' then
230         uri = '/'
231     end
232     if port == '' or port == nil then
233         if schema == 'http' then
234             port = check.config.port or 80
235         elseif schema == 'https' then
236             port = check.config.port or 443
237         else
238             error(schema .. " not supported")
239         end
240     end
241     if schema == 'https' then
242         use_ssl = true
243     end
244
245     -- Include body as a metric
246     if check.config.include_body == "true" or check.config.include_body == "on" then
247         include_body = true
248     end
249
250     local output = ''
251     local connecttime, firstbytetime
252     local next_location
253     local cookies = { }
254     local setfirstbyte = 1
255
256     -- callbacks from the HttpClient
257     local callbacks = { }
258     callbacks.consume = function (str)
259         if setfirstbyte == 1 then
260             firstbytetime = noit.timeval.now()
261             setfirstbyte = 0
262         end
263         output = output .. (str or '')
264     end
265     callbacks.headers = function (hdrs)
266         next_location = hdrs.location
267         populate_cookie_jar(cookies, host, hdrs["set-cookie"])
268         populate_cookie_jar(cookies, hdrs["set-cookie2"])
269     end
270
271     callbacks.connected = function () connecttime = noit.timeval.now() end
272
273     -- setup SSL info
274     local default_ca_chain =
275         noit.conf_get_string("/noit/eventer/config/default_ca_chain")
276     callbacks.certfile = function () return check.config.certificate_file end
277     callbacks.keyfile = function () return check.config.key_file end
278     callbacks.cachain = function ()
279         return check.config.ca_chain and check.config.ca_chain
280                                       or default_ca_chain
281     end
282     callbacks.ciphers = function () return check.config.ciphers end
283
284     -- set the stage
285     local headers = {}
286     headers.Host = host
287     for header, value in pairs(check.config) do
288         hdr = string.match(header, '^header_(.+)$')
289         if hdr ~= nil then
290           headers[hdr] = value
291         end
292     end
293     if check.config.auth_method == "Basic" then
294         local user = check.config.auth_user or ''
295         local password = check.config.auth_password or ''
296         local encoded = noit.base64_encode(user .. ':' .. password)
297         headers["Authorization"] = "Basic " .. encoded
298     elseif check.config.auth_method == "Digest" or
299            check.config.auth_method == "Auto" then
300         -- this is handled later as we need our challenge.
301         local client = HttpClient:new()
302         local rv, err = client:connect(check.target_ip, port, use_ssl)
303         if rv ~= 0 then
304             check.status(str or "unknown error")
305             return
306         end
307         local headers_firstpass = {}
308         for k,v in pairs(headers) do
309             headers_firstpass[k] = v
310         end
311         client:do_request(method, uri, headers_firstpass)
312         client:get_response(read_limit)
313         if client.code ~= 401 or
314            client.headers["www-authenticate"] == nil then
315             check.status("expected digest challenge, got " .. client.code)
316             return
317         end
318         local user = check.config.auth_user or ''
319         local password = check.config.auth_password or ''
320         local ameth, challenge =
321             string.match(client.headers["www-authenticate"], '^(%S+)%s+(.+)$')
322         if check.config.auth_method == "Auto" and ameth == "Basic" then
323             local encoded = noit.base64_encode(user .. ':' .. password)
324             headers["Authorization"] = "Basic " .. encoded
325         elseif ameth == "Digest" then
326             headers["Authorization"] =
327                 "Digest " .. client:auth_digest(method, uri,
328                                          user, password, challenge)
329         else
330             check.status("Unexpected auth '" .. ameth .. "' in challenge")
331             return
332         end
333     elseif check.config.auth_method ~= nil then
334       check.status("Unknown auth method: " .. check.config.auth_method)
335       return
336     end
337
338     -- perform the request
339     local client
340     local dns = noit.dns()
341     local target = check.target_ip
342     local payload = check.config.payload
343     -- artificially increase redirects as the initial request counts
344     redirects = redirects + 1
345     starttime = noit.timeval.now()
346     repeat
347         local optclient = HttpClient:new(callbacks)
348         local rv, err = optclient:connect(target, port, use_ssl)
349
350         if rv ~= 0 then
351             check.status(err or "unknown error")
352             return
353         end
354         optclient:do_request(method, uri, headers, payload)
355         optclient:get_response(read_limit)
356         setfirstbyte = 1
357
358         redirects = redirects - 1
359         client = optclient
360
361         if next_location ~= nil then
362             -- reset some stuff for the redirect
363             local prev_port = port
364             local prev_host = host
365             method = 'GET'
366             payload = nil
367             schema, host, port, uri =
368                 string.match(next_location,
369                              "^(https?)://([^:/]*):?([0-9]*)(/?.*)$")
370             if schema == nil then
371                 port = prev_port
372                 host = prev_host
373                 uri = next_location
374             elseif schema == 'http' then
375                 use_ssl = false
376                 if port == "" then port = 80 end
377             elseif schema == 'https' then
378                 use_ssl = true
379                 if port == "" then port = 443 end
380             end
381             if host ~= nil then
382                 headers.Host = host
383                 local r = dns:lookup(host)
384                 if not r or r.a == nil then
385                     check.status("failed to resolve " + host)
386                     return
387                 end
388                 target = r.a
389             end
390             headers["Cookie"] = check.config["header_Cookie"]
391             apply_cookies(headers, cookies, host, uri)
392         end
393     until redirects <= 0 or next_location == nil
394
395     local endtime = noit.timeval.now()
396     check.available()
397
398     local status = ''
399     -- setup the code
400     check.metric_string("code", client.code)
401     status = status .. 'code=' .. client.code
402     if codere ~= nil and codere(client.code) then
403       good = true
404     end
405
406     -- truncated response
407     check.metric_uint32("truncated", client.truncated and 1 or 0)
408
409     -- turnaround time
410     local seconds = elapsed(check, "duration", starttime, endtime)
411     status = status .. ',rt=' .. seconds .. 's'
412     elapsed(check, "tt_connect", starttime, connecttime)
413     elapsed(check, "tt_firstbyte", starttime, firstbytetime)
414
415     -- size
416     status = status .. ',bytes=' .. client.content_bytes
417     check.metric_int32("bytes", client.content_bytes)
418
419     if check.config.extract ~= nil then
420       local exre = noit.pcre(check.config.extract)
421       local rv = true
422       local m = nil
423       while rv and m ~= '' do
424         rv, m, key, value = exre(output or '', { limit = pcre_match_limit })
425         if rv and key ~= nil then
426           check.metric(key, value)
427         end
428       end
429     end
430
431     -- check body
432     if check.config.body ~= nil then
433       local bodyre = noit.pcre(check.config.body)
434       local rv, m, m1 = bodyre(output or '')
435       if rv then
436         m = m1 or m or output
437         if string.len(m) > max_len then
438           m = string.sub(m,1,max_len)
439         end
440         status = status .. ',body=matched'
441         check.metric_string('body_match', m)
442       else
443         status = status .. ',body=failed'
444         check.metric_string('body_match', nil)
445         good = false
446       end
447     end
448
449     -- check body matches
450     local matches = 0
451     has_body_matches = false
452     for key, value in pairs(check.config) do
453       m = string.find(key, BODY_MATCHES_PREFIX)
454
455       if m == 1 then
456         has_body_matches = true
457         key = string.gsub(key, BODY_MATCHES_PREFIX, '')
458
459         local bodyre = noit.pcre(value)
460         local rv, m, m1 = bodyre(output or '')
461
462         if rv then
463           matches = matches + 1
464           m = m1 or m or output
465           if string.len(m) > max_len then
466             m = string.sub(m,1,max_len)
467           end
468           check.metric_string('body_match_' .. key, m)
469         else
470           check.metric_string('body_match_' .. key, nil)
471         end
472       end
473     end
474
475     if has_body_matches then
476       status = status .. ',body_matches=' .. tostring(matches) .. ' matches'
477     end
478
479     -- Include body
480     if include_body then
481         check.metric_string('body', output or '')
482     end
483
484     -- ssl ctx
485     local ssl_ctx = client:ssl_ctx()
486     if ssl_ctx ~= nil then
487       if ssl_ctx.error ~= nil then status = status .. ',sslerror' end
488       check.metric_string("cert_error", ssl_ctx.error)
489       check.metric_string("cert_issuer", ssl_ctx.issuer)
490       check.metric_string("cert_subject", ssl_ctx.subject)
491       check.metric_uint32("cert_start", ssl_ctx.start_time)
492       check.metric_uint32("cert_end", ssl_ctx.end_time)
493       check.metric_int32("cert_end_in", ssl_ctx.end_time - os.time())
494       if noit.timeval.seconds(starttime) > ssl_ctx.end_time then
495         good = false
496         status = status .. ',ssl=expired'
497       end
498     end
499
500     if good then check.good() else check.bad() end
501     check.status(status)
502 end
503
Note: See TracBrowser for help on using the browser.