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

Revision 09fef40cac145611c80b1048abd0fbcbd2a6cd70, 18.9 kB (checked in by Ryan Phillips <ryan.phillips@rackspace.com>, 3 years ago)

add support for check config option read_limit

  • 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="extract"
88                required="optional"
89                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>
90     <parameter name="pcre_match_limit"
91                required="optional"
92                default="10000"
93                allowed="\d+">This sets the PCRE internal match limit (see pcreapi documentation).</parameter>
94     <parameter name="read_limit"
95                required="optional"
96                default="102400"
97                allowed="\d+">Sets the limit on the data read.</parameter>
98   </checkconfig>
99   <examples>
100     <example>
101       <title>Checking an HTTP and HTTPS URL.</title>
102       <para>This example checks the OmniTI Labs website over both HTTP and HTTPS.</para>
103       <programlisting><![CDATA[
104       <noit>
105         <modules>
106           <loader image="lua" name="lua">
107             <config><directory>/opt/reconnoiter/libexec/modules-lua/?.lua</directory></config>
108           </loader>
109           <module loader="lua" name="http" object="noit.module.http" />
110         </modules>
111         <checks>
112           <labs target="8.8.38.5" module="http">
113             <check uuid="fe3e984c-7895-11dd-90c1-c74c31b431f0" name="http">
114               <config><url>http://labs.omniti.com/</url></config>
115             </check>
116             <check uuid="1ecd887a-7896-11dd-b28d-0b4216877f83" name="https">
117               <config><url>https://labs.omniti.com/</url></config>
118             </check>
119           </labs>
120         </checks>
121       </noit>
122     ]]></programlisting>
123     </example>
124   </examples>
125 </module>
126 ]=]);
127   return 0
128 end
129
130 function init(module)
131   return 0
132 end
133
134 function config(module, options)
135   return 0
136 end
137
138 local HttpClient = require 'noit.HttpClient'
139
140 function elapsed(check, name, starttime, endtime)
141     local elapsedtime = endtime - starttime
142     local seconds = string.format('%.3f', noit.timeval.seconds(elapsedtime))
143     check.metric_uint32(name, math.floor(seconds * 1000 + 0.5))
144     return seconds
145 end
146
147 function rand_string(t, l)
148     local n = table.getn(t)
149     local o = ''
150     while l > 0 do
151       o = o .. t[math.random(1,n)]
152       l = l - 1
153     end
154     return o
155 end
156
157 function auth_digest(method, uri, user, pass, challenge)
158     local c = ', ' .. challenge
159     local nc = '00000001'
160     local cnonce =
161         rand_string({'a','b','c','d','e','f','g','h','i','j','k','l','m',
162                      'n','o','p','q','r','s','t','u','v','x','y','z','A',
163                      'B','C','D','E','F','G','H','I','J','K','L','M','N',
164                      'O','P','Q','R','S','T','U','V','W','X','Y','Z','0',
165                      '1','2','3','4','5','6','7','8','9'}, 8)
166     local p = {}
167     for k,v in string.gmatch(c, ',%s+(%a+)="([^"]+)"') do p[k] = v end
168     for k,v in string.gmatch(c, ',%s+(%a+)=([^",][^,]*)') do p[k] = v end
169
170     -- qop can be a list
171     for q in string.gmatch(p.qop, '([^,]+)') do
172         if q == "auth" then p.qop = "auth" end
173     end
174
175     -- calculate H(A1)
176     local ha1 = noit.md5_hex(user .. ':' .. p.realm .. ':' .. pass)
177     if string.lower(p.qop or '') == 'md5-sess' then
178         ha1 = noit.md5_hex(ha1 .. ':' .. p.nonce .. ':' .. cnonce)
179     end
180     -- calculate H(A2)
181     local ha2 = ''
182     if p.qop == "auth" or p.qop == nil then
183         ha2 = noit.md5_hex(method .. ':' .. uri)
184     else
185         -- we don't support auth-int
186         error("qop=" .. p.qop .. " is unsupported")
187     end
188     local resp = ''
189     if p.qop == "auth" then
190         resp = noit.md5_hex(ha1 .. ':' .. p.nonce .. ':' .. nc
191                                 .. ':' .. cnonce .. ':' .. p.qop
192                                 .. ':' .. ha2)
193     else
194         resp = noit.md5_hex(ha1 .. ':' .. p.nonce .. ':' .. ha2)
195     end
196     local o = {}
197     o.username = user
198     o.realm = p.realm
199     o.nonce = p.nonce
200     o.uri = uri
201     o.cnonce = cnonce
202     o.qop = p.qop
203     o.response = resp
204     o.algorithm = p.algorithm
205     if p.opaque then o.opaque = p.opaque end
206     local hdr = ''
207     for k,v in pairs(o) do
208       if hdr == '' then hdr = k .. '="' .. v .. '"'
209       else hdr = hdr .. ', ' .. k .. '="' .. v .. '"' end
210     end
211     hdr = hdr .. ', nc=' .. nc
212     return hdr
213 end
214
215 function populate_cookie_jar(cookies, host, hdr)
216     if hdr ~= nil then
217         local name, value, trailer =
218             string.match(hdr, "([^=]+)=([^;]+)\;?%s*(.*)")
219         if name ~= nil then
220             local jar = { }
221             jar.name = name;
222             jar.value = value;
223             for k, v in string.gmatch(trailer, "%s*(%w+)(=%w+)?;?") do
224                 if v == nil then jar[string.lower(k)] = true
225                 else jar[string.lower(k)] = v:sub(2)
226                 end
227             end
228             if jar.domain ~= nil then host = jar.domain end
229             if cookies[host] == nil then cookies[host] = { } end
230             table.insert(cookies[host], jar)
231         end
232     end
233 end
234
235 function has_host(pat, host)
236     if pat == host then return true end
237     if pat:sub(1,1) ~= "." then return false end
238     local revpat = pat:sub(2):reverse()
239     local revhost = host:reverse()
240     if revpat == revhost then return true end
241     if revpat == revhost:sub(1, revpat:len()) then
242         if revhost:sub(pat:len(), pat:len()) == "." then return true end
243     end
244     return false
245 end
246
247 function apply_cookies(headers, cookies, host, uri)
248     for h, jars in pairs(cookies) do
249         if has_host(h, host) then
250             for i, jar in ipairs(jars) do
251                 if jar.path == nil or
252                    uri:sub(1, jar.path:len()) == jar.path then
253                     if headers["Cookie"] == nil then
254                         headers["Cookie"] = jar.name .. "=" .. jar.value
255                     else
256                         headers["Cookie"] = headers["Cookie"] .. "; " ..
257                                             jar.name .. "=" .. jar.value
258                     end
259                 end
260             end
261         end
262     end
263 end
264
265 function initiate(module, check)
266     local url = check.config.url or 'http:///'
267     local schema, host, port, uri = string.match(url, "^(https?)://([^:/]*):?([0-9]*)(/?.*)$");
268     local use_ssl = false
269     local codere = noit.pcre(check.config.code or '^200$')
270     local good = false
271     local starttime = noit.timeval.now()
272     local method = check.config.method or "GET"
273     local max_len = 80
274     local pcre_match_limit = check.config.pcre_match_limit or 10000
275     local redirects = check.config.redirects or 0
276     local read_limit = check.config.read_limit or 10
277
278     -- expect the worst
279     check.bad()
280     check.unavailable()
281
282     if host == nil then host = check.target end
283     if schema == nil then
284         schema = 'http'
285         uri = '/'
286     end
287     if uri == '' then
288         uri = '/'
289     end
290     if port == '' or port == nil then
291         if schema == 'http' then
292             port = check.config.port or 80
293         elseif schema == 'https' then
294             port = check.config.port or 443
295         else
296             error(schema .. " not supported")
297         end
298     end
299     if schema == 'https' then
300         use_ssl = true
301     end
302
303     local output = ''
304     local connecttime, firstbytetime
305     local next_location
306     local cookies = { }
307
308     -- callbacks from the HttpClient
309     local callbacks = { }
310     callbacks.consume = function (str)
311         if firstbytetime == nil then firstbytetime = noit.timeval.now() end
312         output = output .. (str or '')
313     end
314     callbacks.headers = function (hdrs)
315         next_location = hdrs.location
316         populate_cookie_jar(cookies, host, hdrs["set-cookie"])
317         populate_cookie_jar(cookies, hdrs["set-cookie2"])
318     end
319
320     callbacks.connected = function () connecttime = noit.timeval.now() end
321
322     -- setup SSL info
323     local default_ca_chain =
324         noit.conf_get_string("/noit/eventer/config/default_ca_chain")
325     callbacks.certfile = function () return check.config.certificate_file end
326     callbacks.keyfile = function () return check.config.key_file end
327     callbacks.cachain = function ()
328         return check.config.ca_chain and check.config.ca_chain
329                                       or default_ca_chain
330     end
331     callbacks.ciphers = function () return check.config.ciphers end
332
333     -- set the stage
334     local headers = {}
335     headers.Host = host
336     for header, value in pairs(check.config) do
337         hdr = string.match(header, '^header_(.+)$')
338         if hdr ~= nil then
339           headers[hdr] = value
340         end
341     end
342     if check.config.auth_method == "Basic" then
343         local user = check.config.auth_user or ''
344         local password = check.config.auth_password or ''
345         local encoded = noit.base64_encode(user .. ':' .. password)
346         headers["Authorization"] = "Basic " .. encoded
347     elseif check.config.auth_method == "Digest" or
348            check.config.auth_method == "Auto" then
349         -- this is handled later as we need our challenge.
350         local client = HttpClient:new()
351         local rv, err = client:connect(check.target_ip, port, use_ssl)
352         if rv ~= 0 then
353             check.status(str or "unknown error")
354             return
355         end
356         local headers_firstpass = {}
357         for k,v in pairs(headers) do
358             headers_firstpass[k] = v
359         end
360         client:do_request(method, uri, headers_firstpass)
361         client:get_response(read_limit)
362         if client.code ~= 401 or
363            client.headers["www-authenticate"] == nil then
364             check.status("expected digest challenge, got " .. client.code)
365             return
366         end
367         local user = check.config.auth_user or ''
368         local password = check.config.auth_password or ''
369         local ameth, challenge =
370             string.match(client.headers["www-authenticate"], '^(%S+)%s+(.+)$')
371         if check.config.auth_method == "Auto" and ameth == "Basic" then
372             local encoded = noit.base64_encode(user .. ':' .. password)
373             headers["Authorization"] = "Basic " .. encoded
374         elseif ameth == "Digest" then
375             headers["Authorization"] =
376                 "Digest " .. auth_digest(method, uri,
377                                          user, password, challenge)
378         else
379             check.status("Unexpected auth '" .. ameth .. "' in challenge")
380             return
381         end
382     elseif check.config.auth_method ~= nil then
383       check.status("Unknown auth method: " .. check.config.auth_method)
384       return
385     end
386
387     -- perform the request
388     local client
389     local dns = noit.dns()
390     local target = check.target_ip
391     local payload = check.config.payload
392     -- artificially increase redirects as the initial request counts
393     redirects = redirects + 1
394     repeat
395         starttime = noit.timeval.now()
396         local optclient = HttpClient:new(callbacks)
397         local rv, err = optclient:connect(target, port, use_ssl)
398        
399         if rv ~= 0 then
400             check.status(err or "unknown error")
401             return
402         end
403         optclient:do_request(method, uri, headers, payload)
404         optclient:get_response(read_limit)
405
406         redirects = redirects - 1
407         client = optclient
408
409         if next_location ~= nil then
410             -- reset some stuff for the redirect
411             local prev_port = port
412             local prev_host = host
413             method = 'GET'
414             payload = nil
415             schema, host, port, uri =
416                 string.match(next_location,
417                              "^(https?)://([^:/]*):?([0-9]*)(/?.*)$")
418             if schema == nil then
419                 port = prev_port
420                 host = prev_host
421                 uri = next_location
422             elseif schema == 'http' then
423                 use_ssl = false
424                 if port == "" then port = 80 end
425             elseif schema == 'https' then
426                 use_ssl = true
427                 if port == "" then port = 443 end
428             end
429             if host ~= nil then
430                 headers.Host = host
431                 local r = dns:lookup(host)
432                 if r.a == nil then
433                     check.status("failed to resolve " + host)
434                     return
435                 end
436                 target = r.a
437             end
438             headers["Cookie"] = check.config["header_Cookie"]
439             apply_cookies(headers, cookies, host, uri)
440         end
441     until redirects <= 0 or next_location == nil
442
443     local endtime = noit.timeval.now()
444     check.available()
445
446     local status = ''
447     -- setup the code
448     check.metric_string("code", client.code)
449     status = status .. 'code=' .. client.code
450     if codere ~= nil and codere(client.code) then
451       good = true
452     end
453
454     -- turnaround time
455     local seconds = elapsed(check, "duration", starttime, endtime)
456     status = status .. ',rt=' .. seconds .. 's'
457     elapsed(check, "tt_connect", starttime, connecttime)
458     elapsed(check, "tt_firstbyte", starttime, firstbytetime)
459
460     -- size
461     status = status .. ',bytes=' .. client.content_bytes
462     check.metric_int32("bytes", client.content_bytes)
463
464     if check.config.extract ~= nil then
465       local exre = noit.pcre(check.config.extract)
466       local rv = true
467       while rv do
468         rv, m, key, value = exre(output or '', { limit = pcre_match_limit })
469         if rv and key ~= nil then
470           check.metric(key, value)
471         end
472       end
473     end
474
475     -- check body
476     if check.config.body ~= nil then
477       local bodyre = noit.pcre(check.config.body)
478       local rv, m, m1 = bodyre(output or '')
479       if rv then
480         m = m1 or m or output
481         if string.len(m) > max_len then
482           m = string.sub(m,1,max_len)
483         end
484         status = status .. ',body=matched'
485         check.metric_string('body_match', m)
486       else
487         status = status .. ',body=failed'
488         check.metric_string('body_match', nil)
489         good = false
490       end
491     end
492
493     -- ssl ctx
494     local ssl_ctx = client:ssl_ctx()
495     if ssl_ctx ~= nil then
496       if ssl_ctx.error ~= nil then status = status .. ',sslerror' end
497       check.metric_string("cert_error", ssl_ctx.error)
498       check.metric_string("cert_issuer", ssl_ctx.issuer)
499       check.metric_string("cert_subject", ssl_ctx.subject)
500       check.metric_uint32("cert_start", ssl_ctx.start_time)
501       check.metric_uint32("cert_end", ssl_ctx.end_time)
502       check.metric_int32("cert_end_in", ssl_ctx.end_time - os.time())
503       if noit.timeval.seconds(starttime) > ssl_ctx.end_time then
504         good = false
505         status = status .. ',ssl=expired'
506       end
507     end
508
509     if good then check.good() else check.bad() end
510     check.status(status)
511 end
512
Note: See TracBrowser for help on using the browser.