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

Revision 73df43f30ea0b1c5c1d75c98793bf57712b99d72, 18.6 kB (checked in by Theo Schlossnagle <jesus@omniti.com>, 7 years ago)

use http to test out the new pcre_match_limit stuff and give it a sane default, fixes #306

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