querying.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. # Copyright (C) 2011 Internet Systems Consortium.
  2. #
  3. # Permission to use, copy, modify, and distribute this software for any
  4. # purpose with or without fee is hereby granted, provided that the above
  5. # copyright notice and this permission notice appear in all copies.
  6. #
  7. # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
  8. # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
  9. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
  10. # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
  11. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
  12. # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  13. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  14. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. # This script provides querying functionality
  16. # The most important step is
  17. #
  18. # query for <name> [type X] [class X] [to <addr>[:port]] should have rcode <rc>
  19. #
  20. # By default, it will send queries to 127.0.0.1:56176 unless specified
  21. # otherwise. The rcode is always checked. If the result is not NO_ANSWER,
  22. # the result will be stored in last_query_result, which can then be inspected
  23. # more closely, for instance with the step
  24. #
  25. # "the last query response should have <property> <value>"
  26. #
  27. # Also see example.feature for some examples
  28. from lettuce import *
  29. import subprocess
  30. import re
  31. #
  32. # define a class to easily access different parts
  33. # We may consider using our full library for this, but for now
  34. # simply store several parts of the response as text values in
  35. # this structure.
  36. # (this actually has the advantage of not relying on our own libraries
  37. # to test our own, well, libraries)
  38. #
  39. # The following attributes are 'parsed' from the response, all as strings,
  40. # and end up as direct attributes of the QueryResult object:
  41. # opcode, rcode, id, flags, qdcount, ancount, nscount, adcount,
  42. # edns_version, edns_flags, and edns_udp_size
  43. # (flags and edns_flags are both one string with all flags, in the order
  44. # in which they appear in the response message.)
  45. #
  46. # this will set 'rcode' as the result code, we 'define' one additional
  47. # rcode, "NO_ANSWER", if the dig process returned an error code itself
  48. # In this case none of the other attributes will be set.
  49. #
  50. # The different sections will be lists of strings, one for each RR in the
  51. # section. The question section will start with ';', as per dig output
  52. #
  53. # See server_from_sqlite3.feature for various examples to perform queries
  54. class QueryResult(object):
  55. status_re = re.compile("opcode: ([A-Z])+, status: ([A-Z]+), id: ([0-9]+)")
  56. edns_re = re.compile("; EDNS: version: ([0-9]+), flags: ([a-z ]*); udp: ([0-9]+)")
  57. flags_re = re.compile("flags: ([a-z ]+); QUERY: ([0-9]+), ANSWER: " +
  58. "([0-9]+), AUTHORITY: ([0-9]+), ADDITIONAL: ([0-9]+)")
  59. def __init__(self, name, qtype, qclass, address, port,
  60. additional_args=None):
  61. """
  62. Constructor. This fires of a query using dig.
  63. Parameters:
  64. name: The domain name to query
  65. qtype: The RR type to query. Defaults to A if it is None.
  66. qclass: The RR class to query. Defaults to IN if it is None.
  67. address: The IP address to send the query to.
  68. port: The port number to send the query to.
  69. additional_args: List of additional arguments (e.g. '+dnssec').
  70. All parameters must be either strings or have the correct string
  71. representation.
  72. Only one query attempt will be made.
  73. """
  74. args = [ 'dig', '+tries=1', '@' + str(address), '-p', str(port) ]
  75. if qtype is not None:
  76. args.append('-t')
  77. args.append(str(qtype))
  78. if qclass is not None:
  79. args.append('-c')
  80. args.append(str(qclass))
  81. if additional_args is not None:
  82. args.extend(additional_args)
  83. args.append(name)
  84. dig_process = subprocess.Popen(args, 1, None, None, subprocess.PIPE,
  85. None)
  86. result = dig_process.wait()
  87. if result != 0:
  88. self.rcode = "NO_ANSWER"
  89. else:
  90. self.rcode = None
  91. parsing = "HEADER"
  92. self.question_section = []
  93. self.answer_section = []
  94. self.authority_section = []
  95. self.additional_section = []
  96. self.line_handler = self.parse_header
  97. for out in dig_process.stdout:
  98. self.line_handler(out)
  99. def _check_next_header(self, line):
  100. """
  101. Returns true if we found a next header, and sets the internal
  102. line handler to the appropriate value.
  103. """
  104. if line == ";; ANSWER SECTION:\n":
  105. self.line_handler = self.parse_answer
  106. elif line == ";; OPT PSEUDOSECTION:\n":
  107. self.line_handler = self.parse_opt
  108. elif line == ";; QUESTION SECTION:\n":
  109. self.line_handler = self.parse_question
  110. elif line == ";; AUTHORITY SECTION:\n":
  111. self.line_handler = self.parse_authority
  112. elif line == ";; ADDITIONAL SECTION:\n":
  113. self.line_handler = self.parse_additional
  114. elif line.startswith(";; Query time"):
  115. self.line_handler = self.parse_footer
  116. else:
  117. return False
  118. return True
  119. def parse_header(self, line):
  120. """
  121. Parse the header lines of the query response.
  122. Parameters:
  123. line: The current line of the response.
  124. """
  125. if not self._check_next_header(line):
  126. status_match = self.status_re.search(line)
  127. flags_match = self.flags_re.search(line)
  128. if status_match is not None:
  129. self.opcode = status_match.group(1)
  130. self.rcode = status_match.group(2)
  131. elif flags_match is not None:
  132. self.flags = flags_match.group(1)
  133. self.qdcount = flags_match.group(2)
  134. self.ancount = flags_match.group(3)
  135. self.nscount = flags_match.group(4)
  136. self.adcount = flags_match.group(5)
  137. def parse_opt(self, line):
  138. """
  139. Parse the header lines of the query response.
  140. Parameters:
  141. line: The current line of the response.
  142. """
  143. if not self._check_next_header(line):
  144. edns_match = self.edns_re.search(line)
  145. if edns_match is not None:
  146. self.edns_version = edns_match.group(1)
  147. self.edns_flags = edns_match.group(2)
  148. self.edns_udp_size = edns_match.group(3)
  149. def parse_question(self, line):
  150. """
  151. Parse the question section lines of the query response.
  152. Parameters:
  153. line: The current line of the response.
  154. """
  155. if not self._check_next_header(line):
  156. if line != "\n":
  157. self.question_section.append(line.strip())
  158. def parse_answer(self, line):
  159. """
  160. Parse the answer section lines of the query response.
  161. Parameters:
  162. line: The current line of the response.
  163. """
  164. if not self._check_next_header(line):
  165. if line != "\n":
  166. self.answer_section.append(line.strip())
  167. def parse_authority(self, line):
  168. """
  169. Parse the authority section lines of the query response.
  170. Parameters:
  171. line: The current line of the response.
  172. """
  173. if not self._check_next_header(line):
  174. if line != "\n":
  175. self.authority_section.append(line.strip())
  176. def parse_additional(self, line):
  177. """
  178. Parse the additional section lines of the query response.
  179. Parameters:
  180. line: The current line of the response.
  181. """
  182. if not self._check_next_header(line):
  183. if line != "\n":
  184. self.additional_section.append(line.strip())
  185. def parse_footer(self, line):
  186. """
  187. Parse the footer lines of the query response.
  188. Parameters:
  189. line: The current line of the response.
  190. """
  191. pass
  192. @step('A (dnssec )?(recursive )?query for ([\S]+) (?:type ([A-Z0-9]+) )?' +
  193. '(?:class ([A-Z]+) )?(?:to ([^:]+|\[[0-9a-fA-F:]+\])(?::([0-9]+))? )?' +
  194. 'should have rcode ([\w.]+)')
  195. def query(step, dnssec, recursive, query_name, qtype, qclass, addr, port,
  196. rcode):
  197. """
  198. Run a query, check the rcode of the response, and store the query
  199. result in world.last_query_result.
  200. Parameters:
  201. dnssec ('dnssec'): DO bit is set in the query.
  202. Defaults to unset (no DNSSEC).
  203. recursive ('recursive'): RD bit is set in the query.
  204. Defaults to unset (no recursion).
  205. query_name ('query for <name>'): The domain name to query.
  206. qtype ('type <type>', optional): The RR type to query. Defaults to A.
  207. qclass ('class <class>', optional): The RR class to query. Defaults to IN.
  208. addr ('to <address>', optional): The IP address of the nameserver to query.
  209. Defaults to 127.0.0.1.
  210. port (':<port>', optional): The port number of the nameserver to query.
  211. Defaults to 56176.
  212. rcode ('should have rcode <rcode>'): The expected rcode of the answer.
  213. """
  214. if qtype is None:
  215. qtype = "A"
  216. if qclass is None:
  217. qclass = "IN"
  218. if addr is None:
  219. addr = "127.0.0.1"
  220. addr = re.sub(r"\[(.+)\]", r"\1", addr) # convert [IPv6_addr] to IPv6_addr
  221. if port is None:
  222. port = 56176
  223. additional_arguments = []
  224. if dnssec is not None:
  225. additional_arguments.append("+dnssec")
  226. else:
  227. # some builds of dig add edns0 by default. This could muck up
  228. # additional counts, so unless we need dnssec, explicitly
  229. # disable edns0
  230. additional_arguments.append("+noedns")
  231. # dig sets RD bit by default.
  232. if recursive is None:
  233. additional_arguments.append("+norecurse")
  234. query_result = QueryResult(query_name, qtype, qclass, addr, port,
  235. additional_arguments)
  236. assert query_result.rcode == rcode,\
  237. "Expected: " + rcode + ", got " + query_result.rcode
  238. world.last_query_result = query_result
  239. @step('The SOA serial for ([\S.]+) (?:at (\S+)(?::([0-9]+)) )?should be ([0-9]+)')
  240. def query_soa(step, query_name, address, port, serial=None):
  241. """
  242. Convenience function to check the SOA SERIAL value of the given zone at
  243. the nameserver at the default address (127.0.0.1:56176).
  244. Parameters:
  245. query_name ('for <name>'): The zone to find the SOA record for.
  246. serial ('should be <number>'): The expected value of the SOA SERIAL.
  247. If the rcode is not NOERROR, or the answer section does not contain the
  248. SOA record, this step fails.
  249. """
  250. if address is None:
  251. address = "127.0.0.1"
  252. if port is None:
  253. port = "56176"
  254. query_result = QueryResult(query_name, "SOA", "IN", address, port)
  255. assert "NOERROR" == query_result.rcode,\
  256. "Got " + query_result.rcode + ", expected NOERROR"
  257. assert len(query_result.answer_section) == 1,\
  258. "Too few or too many answers in SOA response"
  259. soa_parts = query_result.answer_section[0].split()
  260. assert serial == soa_parts[6],\
  261. "Got SOA serial " + soa_parts[6] + ", expected " + serial
  262. @step('last query response should have (\S+) (.+)')
  263. def check_last_query(step, item, value):
  264. """
  265. Check a specific value in the reponse from the last successful query sent.
  266. Parameters:
  267. item: The item to check the value of
  268. value: The expected value.
  269. This performs a very simple direct string comparison of the QueryResult
  270. member with the given item name and the given value.
  271. Fails if the item is unknown, or if its value does not match the expected
  272. value.
  273. """
  274. assert world.last_query_result is not None
  275. assert item in world.last_query_result.__dict__
  276. lq_val = world.last_query_result.__dict__[item]
  277. assert str(value) == str(lq_val),\
  278. "Got: " + str(lq_val) + ", expected: " + str(value)
  279. @step('([a-zA-Z]+) section of the last query response should (exactly )?be')
  280. def check_last_query_section(step, section, exact):
  281. """
  282. Check the entire contents of the given section of the response of the last
  283. query.
  284. Parameters:
  285. section ('<section> section'): The name of the section (QUESTION, ANSWER,
  286. AUTHORITY or ADDITIONAL).
  287. The expected response is taken from the multiline part of the step in the
  288. scenario. Differing whitespace is ignored, the order of the lines is
  289. ignored, and the comparison is case insensitive.
  290. Fails if they do not match.
  291. WARNING: Case insensitivity is not strictly correct; for instance the
  292. data of TXT RRs would be case sensitive. But most other output is, so
  293. currently the checks are always case insensitive. Should we decide
  294. these checks do need to be case sensitive, we can either remove it
  295. or make it optional (for the former, we'll need to update a number of
  296. tests).
  297. """
  298. response_string = None
  299. if section.lower() == 'question':
  300. response_string = "\n".join(world.last_query_result.question_section)
  301. elif section.lower() == 'answer':
  302. response_string = "\n".join(world.last_query_result.answer_section)
  303. elif section.lower() == 'authority':
  304. response_string = "\n".join(world.last_query_result.authority_section)
  305. elif section.lower() == 'additional':
  306. response_string = "\n".join(world.last_query_result.additional_section)
  307. else:
  308. assert False, "Unknown section " + section
  309. # Now mangle the data for 'conformance'
  310. # This could be done more efficiently, but is done one
  311. # by one on a copy of the original data, so it is clear
  312. # what is done. Final error output is currently still the
  313. # original unchanged multiline strings
  314. # replace whitespace of any length by one space
  315. response_string = re.sub("[ \t]+", " ", response_string)
  316. expect = re.sub("[ \t]+", " ", step.multiline)
  317. # lowercase them unless we need to do an exact match
  318. if exact is None:
  319. response_string = response_string.lower()
  320. expect = expect.lower()
  321. # sort them
  322. response_string_parts = response_string.split("\n")
  323. response_string_parts.sort()
  324. response_string = "\n".join(response_string_parts)
  325. expect_parts = expect.split("\n")
  326. expect_parts.sort()
  327. expect = "\n".join(expect_parts)
  328. assert response_string.strip() == expect.strip(),\
  329. "Got:\n'" + response_string + "'\nExpected:\n'" + step.multiline +"'"