2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
10 # | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
11 # +------------------------------------------------------------------+
13 # This file is part of Check_MK.
14 # The official homepage is at http://mathias-kettner.de/check_mk.
16 # check_mk is free software; you can redistribute it and/or modify it
17 # under the terms of the GNU General Public License as published by
18 # the Free Software Foundation in version 2. check_mk is distributed
19 # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
20 # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
21 # PARTICULAR PURPOSE. See the GNU General Public License for more de-
22 # tails. You should have received a copy of the GNU General Public
23 # License along with GNU Make; see the file COPYING. If not, write
24 # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
25 # Boston, MA 02110-1301 USA.
27 # This check performs HTTP requests with some advanced features like
28 # a) Detecting, populating and submitting HTML forms
29 # b) Accepts and uses cookies
30 # c) Follos HTTP redirects
31 # d) Extends HTTP headers
35 # Call the page test.php, find the single form on that page and
36 # submit it with default values:
38 # ./check_form_submit -I localhost -u /test.php
40 # Same as above, but expect the string "Hello" in the response
43 # ./check_form_submit -I localhost -u /test.php -e "Hello"
45 # Login as omdadmin with password omd, in the OMD site named /event,
46 # let the login redirect to the wato.py and expect the string WATO
49 # ./check_form_submit -I localhost -u /event -q '_origtarget=wato.py&_username=omdadmin&_password=omd' -e 'WATO'
58 from HTMLParser
import HTMLParser
63 USAGE: check_form_submit -I <HOSTADDRESS> [-u <URI>] [-p <PORT>] [-s]
64 [-f <FORMNAME>] [-q <QUERYPARAMS>] [-e <REGEX>] [-t <TIMEOUT> ]
68 -I HOSTADDRESS The IP address or hostname of the host to contact.
69 This option can be given multiple times to contact
70 several hosts with the same query.
71 -u URI The URL string to query, "/" by default
72 -p PORT TCP Port to communicate with
73 -s Encrypt the connection using SSL
74 -f FORMNAME Name of the form to fill, must match with the
75 contents of the "name" attribute
76 -q QUERYPARAMS Keys/Values of form fields to be popuplated
77 -e REGEX Expected regular expression in the HTML response
79 -t TIMEOUT HTTP connect timeout in seconds
80 -n WARN,CRIT Is only used in "multiple" mode. Number of successful
81 responses to result in a WARN and/or CRIT state
83 -h, --help Show this help message and exit
90 sys
.stderr
.write('%s\n' % msg
)
93 class HostResult(Exception):
94 def __init__(self
, result
):
95 super(HostResult
, self
).__init
__()
100 raise HostResult((rc
, s
))
104 stxt
= ['OK', 'WARN', 'CRIT', 'UNKN'][rc
]
105 sys
.stdout
.write('%s - %s\n' % (stxt
, s
))
109 def get_base_url(ssl
, host
, port
):
110 if not ssl
and port
== 443:
113 proto
= 'https' if ssl
else 'http'
114 if (proto
== 'http' and port
== 80) or (proto
== 'https' and port
== 443):
117 portspec
= ':%d' % port
119 return '%s://%s%s' % (proto
, host
, portspec
)
122 # TODO: Refactor to requests
124 return urllib2
.build_opener(urllib2
.HTTPRedirectHandler(), urllib2
.HTTPHandler(debuglevel
=0),
125 urllib2
.HTTPSHandler(debuglevel
=0),
126 urllib2
.HTTPCookieProcessor(cookielib
.CookieJar()))
129 def open_url(client
, url
, method
='GET', data
=None, timeout
=None):
130 if method
== 'GET' and data
is not None:
131 # Add the query string to the url in this case
132 start
= '&' if '?' in url
else '?'
133 url
+= start
+ urllib
.urlencode(data
)
138 debug('POST %s' % url
)
139 data
= urllib
.urlencode(data
.items())
140 debug(' => %s' % data
)
141 fd
= client
.open(url
, data
, timeout
) # will be a POST
143 debug('GET %s' % url
)
144 fd
= client
.open(url
, timeout
=timeout
) # GET
145 except urllib2
.HTTPError
, e
:
146 new_state(2, 'Unable to open %s: [%d] %s' % (url
, e
.code
, e
))
147 except urllib2
.URLError
, e
:
148 new_state(2, 'Unable to open %s: %s' % (url
, e
.reason
))
149 except socket
.timeout
, e
:
150 new_state(2, 'Unable to open %s: %s' % (url
, e
))
151 real_url
= fd
.geturl()
155 encoding
= fd
.headers
.getparam('charset')
157 content
= content
.decode(encoding
)
159 debug('CODE: %s RESPONSE:' % code
)
161 [' %02d %s' % (index
+ 1, line
) for index
, line
in enumerate(content
.split('\n'))]))
162 return code
, real_url
, content
165 class FormParser(HTMLParser
):
168 self
.current_form
= None
169 HTMLParser
.__init
__(self
)
171 def handle_starttag(self
, tag
, attrs
):
175 name
= attrs
.get('name', 'unnamed-%d' % (len(self
.forms
) + 1))
177 'attrs': dict(attrs
),
180 self
.current_form
= self
.forms
[name
]
182 if self
.current_form
is None:
183 debug('Ignoring form field out of form tag')
186 self
.current_form
['elements'][attrs
['name']] = attrs
.get('value', '')
188 debug('Ignoring form field without name %r' % attrs
)
190 def handle_endtag(self
, tag
):
192 self
.current_form
= None
195 # Parse XML to find all form elements
196 # One form found and no form_name given, use that one
197 # Loop all forms for the given form_name, use the matching one
198 # otherwise raise an exception
199 def parse_form(content
, form_name
):
200 parser
= FormParser()
203 num_forms
= len(forms
)
206 new_state(2, 'Found no form element in HTML code')
208 elif num_forms
== 1 and form_name
is not None and form_name
in forms
:
211 'Found one form with name "%s" but expected name "%s"' % (forms
.keys()[0], form_name
))
214 form
= forms
[forms
.keys()[0]]
216 elif form_name
is None:
218 2, 'No form name provided but found multiple forms (Names: %s)' % (', '.join(
222 form
= forms
.get(form_name
)
225 2, 'Found no form with name "%s" (Available: %s)' % (form_name
, ', '.join(
231 def update_form_vars(form_elem
, params
):
232 v
= form_elem
['elements'].copy()
242 short_options
= 'I:u:p:H:f:q:e:t:n:sd'
243 long_options
= ["help"]
246 opts
= getopt
.getopt(sys
.argv
[1:], short_options
, long_options
)[0]
247 except getopt
.GetoptError
, err
:
248 sys
.stderr
.write("%s\n" % err
)
259 timeout
= 10 # seconds
264 if o
in ['-h', '--help']:
278 params
= dict([parts
.split('=', 1) for parts
in a
.split('&')])
285 num_warn
, num_crit
= map(int, a
.split(',', 1))
292 sys
.stderr
.write('Please provide the host to query via -I <HOSTADDRESS>.\n')
304 base_url
= get_base_url(ssl
, host
, port
)
306 # Perform first HTTP request to fetch the page containing the form(s)
307 _code
, real_url
, body
= open_url(client
, base_url
+ uri
, timeout
=timeout
)
309 form
= parse_form(body
, form_name
)
311 # Get all fields and prefilled values from that form
312 # Put the values of the given query params in these forms
313 form_vars
= update_form_vars(form
, params
)
315 # Issue a HTTP request with those parameters
316 # Extract the form target and method
317 method
= form
['attrs'].get('method', 'GET').upper()
318 target
= form
['attrs'].get('action', real_url
)
320 # target is given as absolute path, relative to hostname
321 target
= base_url
+ target
322 elif target
[0] != '':
324 target
= '%s/%s' % ('/'.join(real_url
.rstrip('/').split('/')[:-1]), target
)
326 _code
, real_url
, content
= open_url(
327 client
, target
, method
, form_vars
, timeout
=timeout
)
329 # If a expect_regex is given, check wether or not it is present in the response
330 if expect_regex
is not None:
331 matches
= re
.search(expect_regex
, content
)
332 if matches
is not None:
333 new_state(0, 'Found expected regex "%s" in form response' % expect_regex
)
336 2, 'Expected regex "%s" could not be found in form response' %
339 new_state(0, 'Form has been submitted')
340 except HostResult
, e
:
343 states
[host
] = e
.result
346 failed
= [pair
for pair
in states
.items() if pair
[1][0] != 0]
347 success
= [pair
for pair
in states
.items() if pair
[1][0] == 0]
348 max_state
= max([state
for state
, _output
in states
.values()])
351 if num_warn
is None and num_crit
is None:
352 # use the worst state as summary state
353 sum_state
= max_state
355 elif num_crit
is not None and len(success
) <= num_crit
:
358 elif num_warn
is not None and len(success
) <= num_warn
:
361 txt
= '%d succeeded, %d failed' % (len(success
), len(failed
))
363 txt
+= ' (%s)' % ', '.join(['%s: %s' % (n
, msg
[1]) for n
, msg
in failed
])
364 bail_out(sum_state
, txt
)
369 bail_out(3, 'Exception occured: %s\n' % e
)
372 if __name__
== "__main__":