session.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  1. # Copyright (C) 2012 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. from isc.dns import *
  16. import isc.ddns.zone_config
  17. from isc.log import *
  18. from isc.ddns.logger import logger, ClientFormatter, ZoneFormatter,\
  19. RRsetFormatter
  20. from isc.log_messages.libddns_messages import *
  21. from isc.datasrc import ZoneFinder
  22. import isc.xfrin.diff
  23. import copy
  24. # Result codes for UpdateSession.handle()
  25. UPDATE_SUCCESS = 0
  26. UPDATE_ERROR = 1
  27. UPDATE_DROP = 2
  28. # Convenient aliases of update-specific section names
  29. SECTION_ZONE = Message.SECTION_QUESTION
  30. SECTION_PREREQUISITE = Message.SECTION_ANSWER
  31. SECTION_UPDATE = Message.SECTION_AUTHORITY
  32. # Shortcut
  33. DBGLVL_TRACE_BASIC = logger.DBGLVL_TRACE_BASIC
  34. class UpdateError(Exception):
  35. '''Exception for general error in update request handling.
  36. This exception is intended to be used internally within this module.
  37. When UpdateSession.handle() encounters an error in handling an update
  38. request it can raise this exception to terminate the handling.
  39. This class is constructed with some information that may be useful for
  40. subsequent possible logging:
  41. - msg (string) A string explaining the error.
  42. - zname (isc.dns.Name) The zone name. Can be None when not identified.
  43. - zclass (isc.dns.RRClass) The zone class. Like zname, can be None.
  44. - rcode (isc.dns.RCode) The RCODE to be set in the response message.
  45. - nolog (bool) If True, it indicates there's no more need for logging.
  46. '''
  47. def __init__(self, msg, zname, zclass, rcode, nolog=False):
  48. Exception.__init__(self, msg)
  49. self.zname = zname
  50. self.zclass = zclass
  51. self.rcode = rcode
  52. self.nolog = nolog
  53. def foreach_rr_in_rrset(rrset, method, *kwargs):
  54. '''Helper function. For DDNS, in a number of cases, we need to
  55. treat the various RRs in a single RRset separately.
  56. Our libdns++ has no concept of RRs, so in that case,
  57. what we do is create a temporary 1-RR RRset for each Rdata
  58. in the RRset object.
  59. This method then calls the given method with the given args
  60. for each of the temporary rrsets (the rrset in *wargs is
  61. replaced by the temporary one)
  62. Note: if this method is useful in more places, we may want
  63. to move it out of ddns.
  64. Example:
  65. Say you have a method that prints a prexif string and an
  66. rrset, def my_print(prefix, rrset)
  67. Given an rrset my_rrset, you'd print the entire rrset
  68. with my_print("foo", rrset)
  69. And with this helper function, to print each rr invidually,
  70. you'd call
  71. foreach_rr_in_rrsetet(rrset, my_print, "foo", rrset)
  72. Note the rrset is needed twice, the first to identify it,
  73. the second as the 'real' argument to my_print (which is replaced
  74. by this function.
  75. '''
  76. #result = None
  77. # todo: check for empty?
  78. for rdata in rrset.get_rdata():
  79. tmp_rrset = isc.dns.RRset(rrset.get_name(),
  80. rrset.get_class(),
  81. rrset.get_type(),
  82. rrset.get_ttl())
  83. tmp_rrset.add_rdata(rdata)
  84. a = []
  85. # Replace the rrset in the original arguments by our rrset
  86. args = [arg if arg != rrset else tmp_rrset for arg in kwargs]
  87. result = method(*args)
  88. return result
  89. def convert_rrset_class(rrset, rrclass):
  90. '''Returns a (new) rrset with the data from the given rrset,
  91. but of the given class. Useful to convert from NONE and ANY to
  92. a real class.
  93. Note that the caller should be careful what to convert;
  94. and DNS error that could happen during wire-format reading
  95. could technically occur here, and is not caught by this helper.
  96. '''
  97. new_rrset = isc.dns.RRset(rrset.get_name(), rrclass,
  98. rrset.get_type(), rrset.get_ttl())
  99. for rdata in rrset.get_rdata():
  100. # Rdata class is nof modifiable, and must match rrset's
  101. # class, so we need to to some ugly conversion here.
  102. # And we cannot use to_text() (since the class may be unknown)
  103. wire = rdata.to_wire(bytes())
  104. new_rrset.add_rdata(isc.dns.Rdata(rrset.get_type(), rrclass, wire))
  105. return new_rrset
  106. class UpdateSession:
  107. '''Protocol handling for a single dynamic update request.
  108. This class is instantiated with a request message and some other
  109. information that will be used for handling the request. Its main
  110. method, handle(), will process the request, and normally build
  111. a response message according to the result. The application of this
  112. class can use the message to send a response to the client.
  113. '''
  114. def __init__(self, req_message, req_data, client_addr, zone_config):
  115. '''Constructor.
  116. Note: req_data is not really used as of #1512 but is listed since
  117. it's quite likely we need it in a subsequent task soon. We'll
  118. also need to get other parameters such as ACLs, for which, it's less
  119. clear in which form we want to get the information, so it's left
  120. open for now.
  121. Parameters:
  122. - req_message (isc.dns.Message) The request message. This must be
  123. in the PARSE mode.
  124. - req_data (binary) Wire format data of the request message.
  125. It will be used for TSIG verification if necessary.
  126. - client_addr (socket address) The address/port of the update client
  127. in the form of Python socket address object. This is mainly for
  128. logging and access control.
  129. - zone_config (ZoneConfig) A tentative container that encapsulates
  130. the server's zone configuration. See zone_config.py.
  131. (It'll soon need to be passed ACL in some way, too)
  132. '''
  133. self.__message = req_message
  134. self.__client_addr = client_addr
  135. self.__zone_config = zone_config
  136. self.__added_soa = None
  137. def get_message(self):
  138. '''Return the update message.
  139. After handle() is called, it's generally transformed to the response
  140. to be returned to the client; otherwise it would be identical to
  141. the request message passed on construction.
  142. '''
  143. return self.__message
  144. def handle(self):
  145. '''Handle the update request according to RFC2136.
  146. This method returns a tuple of the following three elements that
  147. indicate the result of the request.
  148. - Result code of the request processing, which are:
  149. UPDATE_SUCCESS Update request granted and succeeded.
  150. UPDATE_ERROR Some error happened to be reported in the response.
  151. UPDATE_DROP Error happened and no response should be sent.
  152. Except the case of UPDATE_DROP, the UpdateSession object will have
  153. created a response that is to be returned to the request client,
  154. which can be retrieved by get_message().
  155. - The name of the updated zone (isc.dns.Name object) in case of
  156. UPDATE_SUCCESS; otherwise None.
  157. - The RR class of the updated zone (isc.dns.RRClass object) in case
  158. of UPDATE_SUCCESS; otherwise None.
  159. '''
  160. try:
  161. datasrc_client, zname, zclass = self.__get_update_zone()
  162. # conceptual code that would follow
  163. prereq_result = self.__check_prerequisites()
  164. if prereq_result != Rcode.NOERROR():
  165. self.__make_response(prereq_result)
  166. return UPDATE_ERROR, self.__zname, self.__zclass
  167. # self.__check_update_acl()
  168. update_result = self.__do_update()
  169. if update_result != Rcode.NOERROR():
  170. self.__make_response(update_result)
  171. return UPDATE_ERROR, self.__zname, self.__zclass
  172. self.__make_response(Rcode.NOERROR())
  173. return UPDATE_SUCCESS, self.__zname, self.__zclass
  174. except UpdateError as e:
  175. if not e.nolog:
  176. logger.debug(logger.DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_ERROR,
  177. ClientFormatter(self.__client_addr),
  178. ZoneFormatter(e.zname, e.zclass), e)
  179. self.__make_response(e.rcode)
  180. return UPDATE_ERROR, None, None
  181. def __get_update_zone(self):
  182. '''Parse the zone section and find the zone to be updated.
  183. If the zone section is valid and the specified zone is found in
  184. the configuration, it returns a tuple of:
  185. - A matching data source that contains the specified zone
  186. - The zone name as a Name object
  187. - The zone class as an RRClass object
  188. If this method does not raise, these values will also be set to
  189. the session member variables self.__zname, self.__zclass, and
  190. self.__datasrc_client.
  191. '''
  192. # Validation: the zone section must contain exactly one question,
  193. # and it must be of type SOA.
  194. n_zones = self.__message.get_rr_count(SECTION_ZONE)
  195. if n_zones != 1:
  196. raise UpdateError('Invalid number of records in zone section: ' +
  197. str(n_zones), None, None, Rcode.FORMERR())
  198. zrecord = self.__message.get_question()[0]
  199. if zrecord.get_type() != RRType.SOA():
  200. raise UpdateError('update zone section contains non-SOA',
  201. None, None, Rcode.FORMERR())
  202. # See if we're serving a primary zone specified in the zone section.
  203. zname = zrecord.get_name()
  204. zclass = zrecord.get_class()
  205. zone_type, datasrc_client = self.__zone_config.find_zone(zname, zclass)
  206. if zone_type == isc.ddns.zone_config.ZONE_PRIMARY:
  207. self.__zname = zname
  208. self.__zclass = zclass
  209. self.__datasrc_client = datasrc_client
  210. _, self.__finder = datasrc_client.find_zone(zname)
  211. return datasrc_client, zname, zclass
  212. elif zone_type == isc.ddns.zone_config.ZONE_SECONDARY:
  213. # We are a secondary server; since we don't yet support update
  214. # forwarding, we return 'not implemented'.
  215. logger.debug(DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_FORWARD_FAIL,
  216. ClientFormatter(self.__client_addr),
  217. ZoneFormatter(zname, zclass))
  218. raise UpdateError('forward', zname, zclass, Rcode.NOTIMP(), True)
  219. # zone wasn't found
  220. logger.debug(DBGLVL_TRACE_BASIC, LIBDDNS_UPDATE_NOTAUTH,
  221. ClientFormatter(self.__client_addr),
  222. ZoneFormatter(zname, zclass))
  223. raise UpdateError('notauth', zname, zclass, Rcode.NOTAUTH(), True)
  224. def __make_response(self, rcode):
  225. '''Transform the internal message to the update response.
  226. According RFC2136 Section 3.8, the zone section will be cleared
  227. as well as other sections. The response Rcode will be set to the
  228. given value.
  229. '''
  230. self.__message.make_response()
  231. self.__message.clear_section(SECTION_ZONE)
  232. self.__message.set_rcode(rcode)
  233. def __prereq_rrset_exists(self, rrset):
  234. '''Check whether an rrset with the given name and type exists. Class,
  235. TTL, and Rdata (if any) of the given RRset are ignored.
  236. RFC2136 Section 2.4.1.
  237. Returns True if the prerequisite is satisfied, False otherwise.
  238. Note: the only thing used in the call to find() here is the
  239. result status. The actual data is immediately dropped. As
  240. a future optimization, we may want to add a find() option to
  241. only return what the result code would be (and not read/copy
  242. any actual data).
  243. '''
  244. result, _, _ = self.__finder.find(rrset.get_name(), rrset.get_type(),
  245. ZoneFinder.NO_WILDCARD |
  246. ZoneFinder.FIND_GLUE_OK)
  247. return result == ZoneFinder.SUCCESS
  248. def __prereq_rrset_exists_value(self, rrset):
  249. '''Check whether an rrset that matches name, type, and rdata(s) of the
  250. given rrset exists.
  251. RFC2136 Section 2.4.2
  252. Returns True if the prerequisite is satisfied, False otherwise.
  253. '''
  254. result, found_rrset, _ = self.__finder.find(rrset.get_name(),
  255. rrset.get_type(),
  256. ZoneFinder.NO_WILDCARD |
  257. ZoneFinder.FIND_GLUE_OK)
  258. if result == ZoneFinder.SUCCESS and\
  259. rrset.get_name() == found_rrset.get_name() and\
  260. rrset.get_type() == found_rrset.get_type():
  261. # We need to match all actual RRs, unfortunately there is no
  262. # direct order-independent comparison for rrsets, so this
  263. # a slightly inefficient way to handle that.
  264. # shallow copy of the rdata list, so we are sure that this
  265. # loop does not mess with actual data.
  266. found_rdata = copy.copy(found_rrset.get_rdata())
  267. for rdata in rrset.get_rdata():
  268. if rdata in found_rdata:
  269. found_rdata.remove(rdata)
  270. else:
  271. return False
  272. return len(found_rdata) == 0
  273. return False
  274. def __prereq_rrset_does_not_exist(self, rrset):
  275. '''Check whether no rrsets with the same name and type as the given
  276. rrset exist.
  277. RFC2136 Section 2.4.3.
  278. Returns True if the prerequisite is satisfied, False otherwise.
  279. '''
  280. return not self.__prereq_rrset_exists(rrset)
  281. def __prereq_name_in_use(self, rrset):
  282. '''Check whether the name of the given RRset is in use (i.e. has
  283. 1 or more RRs).
  284. RFC2136 Section 2.4.4
  285. Returns True if the prerequisite is satisfied, False otherwise.
  286. Note: the only thing used in the call to find_all() here is
  287. the result status. The actual data is immediately dropped. As
  288. a future optimization, we may want to add a find_all() option
  289. to only return what the result code would be (and not read/copy
  290. any actual data).
  291. '''
  292. result, rrsets, flags = self.__finder.find_all(rrset.get_name(),
  293. ZoneFinder.NO_WILDCARD |
  294. ZoneFinder.FIND_GLUE_OK)
  295. if result == ZoneFinder.SUCCESS and\
  296. (flags & ZoneFinder.RESULT_WILDCARD == 0):
  297. return True
  298. return False
  299. def __prereq_name_not_in_use(self, rrset):
  300. '''Check whether the name of the given RRset is not in use (i.e. does
  301. not exist at all, or is an empty nonterminal.
  302. RFC2136 Section 2.4.5.
  303. Returns True if the prerequisite is satisfied, False otherwise.
  304. '''
  305. return not self.__prereq_name_in_use(rrset)
  306. def __check_in_zone(self, rrset):
  307. '''Returns true if the name of the given rrset is equal to
  308. or a subdomain of the zname from the Zone Section.'''
  309. relation = rrset.get_name().compare(self.__zname).get_relation()
  310. return relation == NameComparisonResult.SUBDOMAIN or\
  311. relation == NameComparisonResult.EQUAL
  312. def __check_prerequisites(self):
  313. '''Check the prerequisites section of the UPDATE Message.
  314. RFC2136 Section 2.4.
  315. Returns a dns Rcode signaling either no error (Rcode.NOERROR())
  316. or that one of the prerequisites failed (any other Rcode).
  317. '''
  318. for rrset in self.__message.get_section(SECTION_PREREQUISITE):
  319. # First check if the name is in the zone
  320. if not self.__check_in_zone(rrset):
  321. logger.info(LIBDDNS_PREREQ_NOTZONE,
  322. ClientFormatter(self.__client_addr),
  323. ZoneFormatter(self.__zname, self.__zclass),
  324. RRsetFormatter(rrset))
  325. return Rcode.NOTZONE()
  326. # Algorithm taken from RFC2136 Section 3.2
  327. if rrset.get_class() == RRClass.ANY():
  328. if rrset.get_ttl().get_value() != 0 or\
  329. rrset.get_rdata_count() != 0:
  330. logger.info(LIBDDNS_PREREQ_FORMERR_ANY,
  331. ClientFormatter(self.__client_addr),
  332. ZoneFormatter(self.__zname, self.__zclass),
  333. RRsetFormatter(rrset))
  334. return Rcode.FORMERR()
  335. elif rrset.get_type() == RRType.ANY():
  336. if not self.__prereq_name_in_use(rrset):
  337. rcode = Rcode.NXDOMAIN()
  338. logger.info(LIBDDNS_PREREQ_NAME_IN_USE_FAILED,
  339. ClientFormatter(self.__client_addr),
  340. ZoneFormatter(self.__zname, self.__zclass),
  341. RRsetFormatter(rrset), rcode)
  342. return rcode
  343. else:
  344. if not self.__prereq_rrset_exists(rrset):
  345. rcode = Rcode.NXRRSET()
  346. logger.info(LIBDDNS_PREREQ_RRSET_EXISTS_FAILED,
  347. ClientFormatter(self.__client_addr),
  348. ZoneFormatter(self.__zname, self.__zclass),
  349. RRsetFormatter(rrset), rcode)
  350. return rcode
  351. elif rrset.get_class() == RRClass.NONE():
  352. if rrset.get_ttl().get_value() != 0 or\
  353. rrset.get_rdata_count() != 0:
  354. logger.info(LIBDDNS_PREREQ_FORMERR_NONE,
  355. ClientFormatter(self.__client_addr),
  356. ZoneFormatter(self.__zname, self.__zclass),
  357. RRsetFormatter(rrset))
  358. return Rcode.FORMERR()
  359. elif rrset.get_type() == RRType.ANY():
  360. if not self.__prereq_name_not_in_use(rrset):
  361. rcode = Rcode.YXDOMAIN()
  362. logger.info(LIBDDNS_PREREQ_NAME_NOT_IN_USE_FAILED,
  363. ClientFormatter(self.__client_addr),
  364. ZoneFormatter(self.__zname, self.__zclass),
  365. RRsetFormatter(rrset), rcode)
  366. return rcode
  367. else:
  368. if not self.__prereq_rrset_does_not_exist(rrset):
  369. rcode = Rcode.YXRRSET()
  370. logger.info(LIBDDNS_PREREQ_RRSET_DOES_NOT_EXIST_FAILED,
  371. ClientFormatter(self.__client_addr),
  372. ZoneFormatter(self.__zname, self.__zclass),
  373. RRsetFormatter(rrset), rcode)
  374. return rcode
  375. elif rrset.get_class() == self.__zclass:
  376. if rrset.get_ttl().get_value() != 0:
  377. logger.info(LIBDDNS_PREREQ_FORMERR,
  378. ClientFormatter(self.__client_addr),
  379. ZoneFormatter(self.__zname, self.__zclass),
  380. RRsetFormatter(rrset))
  381. return Rcode.FORMERR()
  382. else:
  383. if not self.__prereq_rrset_exists_value(rrset):
  384. rcode = Rcode.NXRRSET()
  385. logger.info(LIBDDNS_PREREQ_RRSET_EXISTS_VAL_FAILED,
  386. ClientFormatter(self.__client_addr),
  387. ZoneFormatter(self.__zname, self.__zclass),
  388. RRsetFormatter(rrset), rcode)
  389. return rcode
  390. else:
  391. logger.info(LIBDDNS_PREREQ_FORMERR_CLASS,
  392. ClientFormatter(self.__client_addr),
  393. ZoneFormatter(self.__zname, self.__zclass),
  394. RRsetFormatter(rrset))
  395. return Rcode.FORMERR()
  396. # All prerequisites are satisfied
  397. return Rcode.NOERROR()
  398. def __set_soa_rrset(self, rrset):
  399. '''Sets the given rrset to the member __added_soa (which
  400. is used by __do_update for updating the SOA record'''
  401. self.__added_soa = rrset
  402. def __do_prescan(self):
  403. '''Perform the prescan as defined in RFC2136 section 3.4.1.
  404. This method has a side-effect; it sets self._new_soa if
  405. it encounters the addition of a SOA record in the update
  406. list (so serial can be checked by update later, etc.).
  407. It puts the added SOA in self.__added_soa.
  408. '''
  409. for rrset in self.__message.get_section(SECTION_UPDATE):
  410. if not self.__check_in_zone(rrset):
  411. logger.info(LIBDDNS_UPDATE_NOTZONE,
  412. ClientFormatter(self.__client_addr),
  413. ZoneFormatter(self.__zname, self.__zclass),
  414. RRsetFormatter(rrset))
  415. return Rcode.NOTZONE()
  416. if rrset.get_class() == self.__zclass:
  417. # In fact, all metatypes are in a specific range,
  418. # so one check can test TKEY to ANY
  419. # (some value check is needed anyway, since we do
  420. # not have defined RRtypes for MAILA and MAILB)
  421. if rrset.get_type().get_code() >= 249:
  422. logger.info(LIBDDNS_UPDATE_ADD_BAD_TYPE,
  423. ClientFormatter(self.__client_addr),
  424. ZoneFormatter(self.__zname, self.__zclass),
  425. RRsetFormatter(rrset))
  426. return Rcode.FORMERR()
  427. if rrset.get_type() == RRType.SOA():
  428. # In case there's multiple soa records in the update
  429. # somehow, just take the last
  430. foreach_rr_in_rrset(rrset, self.__set_soa_rrset, rrset)
  431. elif rrset.get_class() == RRClass.ANY():
  432. if rrset.get_ttl().get_value() != 0:
  433. logger.info(LIBDDNS_UPDATE_DELETE_NONZERO_TTL,
  434. ClientFormatter(self.__client_addr),
  435. ZoneFormatter(self.__zname, self.__zclass),
  436. RRsetFormatter(rrset))
  437. return Rcode.FORMERR()
  438. if rrset.get_rdata_count() > 0:
  439. logger.info(LIBDDNS_UPDATE_DELETE_RRSET_NOT_EMPTY,
  440. ClientFormatter(self.__client_addr),
  441. ZoneFormatter(self.__zname, self.__zclass),
  442. RRsetFormatter(rrset))
  443. return Rcode.FORMERR()
  444. if rrset.get_type().get_code() >= 249 and\
  445. rrset.get_type().get_code() <= 254:
  446. logger.info(LIBDDNS_UPDATE_DELETE_BAD_TYPE,
  447. ClientFormatter(self.__client_addr),
  448. ZoneFormatter(self.__zname, self.__zclass),
  449. RRsetFormatter(rrset))
  450. return Rcode.FORMERR()
  451. elif rrset.get_class() == RRClass.NONE():
  452. if rrset.get_ttl().get_value() != 0:
  453. logger.info(LIBDDNS_UPDATE_DELETE_RR_NONZERO_TTL,
  454. ClientFormatter(self.__client_addr),
  455. ZoneFormatter(self.__zname, self.__zclass),
  456. RRsetFormatter(rrset))
  457. return Rcode.FORMERR()
  458. if rrset.get_type().get_code() >= 249:
  459. logger.info(LIBDDNS_UPDATE_DELETE_RR_BAD_TYPE,
  460. ClientFormatter(self.__client_addr),
  461. ZoneFormatter(self.__zname, self.__zclass),
  462. RRsetFormatter(rrset))
  463. return Rcode.FORMERR()
  464. else:
  465. logger.info(LIBDDNS_UPDATE_BAD_CLASS,
  466. ClientFormatter(self.__client_addr),
  467. ZoneFormatter(self.__zname, self.__zclass),
  468. RRsetFormatter(rrset))
  469. return Rcode.FORMERR()
  470. return Rcode.NOERROR()
  471. def __do_update_add_single_rr(self, diff, rr, existing_rrset):
  472. '''Helper for __do_update_add_rrs_to_rrset: only add the
  473. rr if it is not present yet
  474. (note that rr here should already be a single-rr rrset)
  475. '''
  476. if existing_rrset is None:
  477. diff.add_data(rr)
  478. else:
  479. rr_rdata = rr.get_rdata()[0]
  480. if not rr_rdata in existing_rrset.get_rdata():
  481. diff.add_data(rr)
  482. def __do_update_add_rrs_to_rrset(self, diff, rrset):
  483. '''Add the rrs from the given rrset to the diff.
  484. There is handling for a number of special cases mentioned
  485. in RFC2136;
  486. - If the addition is a CNAME, but existing data at its
  487. name is not, the addition is ignored, and vice versa.
  488. - If it is a CNAME, and existing data is too, it is
  489. replaced (existing data is deleted)
  490. An additional restriction is that SOA data is ignored as
  491. well (it is handled separately by the __do_update method).
  492. Note that in the (near) future, this method may have
  493. addition special-cases processing.
  494. '''
  495. # For a number of cases, we may need to remove data in the zone
  496. # (note; SOA is handled separately by __do_update, so that one
  497. # is not explicitely ignored here)
  498. if rrset.get_type() == RRType.SOA():
  499. return
  500. result, orig_rrset, _ = self.__finder.find(rrset.get_name(),
  501. rrset.get_type(),
  502. ZoneFinder.NO_WILDCARD |
  503. ZoneFinder.FIND_GLUE_OK)
  504. if result == self.__finder.CNAME:
  505. # Ignore non-cname rrs that try to update CNAME records
  506. # (if rrset itself is a CNAME, the finder result would be
  507. # SUCCESS, see next case)
  508. return
  509. elif result == ZoneFinder.SUCCESS:
  510. # if update is cname, and zone rr is not, ignore
  511. if rrset.get_type() == RRType.CNAME():
  512. # Remove original CNAME record (the new one
  513. # is added below)
  514. diff.delete_data(orig_rrset)
  515. # We do not have WKS support at this time, but if there
  516. # are special Update equality rules such as for WKS, and
  517. # we do have support for the type, this is where the check
  518. # (and potential delete) would go.
  519. elif result == ZoneFinder.NXRRSET:
  520. # There is data present, but not for this type.
  521. # If this type is CNAME, ignore the update
  522. if rrset.get_type() == RRType.CNAME():
  523. return
  524. foreach_rr_in_rrset(rrset, self.__do_update_add_single_rr, diff, rrset, orig_rrset)
  525. def __do_update_delete_rrset(self, diff, rrset):
  526. '''Deletes the rrset with the name and type of the given
  527. rrset from the zone data (by putting all existing data
  528. in the given diff as delete statements).
  529. Special cases: if the delete statement is for the
  530. zone's apex, and the type is either SOA or NS, it
  531. is ignored.'''
  532. result, to_delete, _ = self.__finder.find(rrset.get_name(),
  533. rrset.get_type(),
  534. ZoneFinder.NO_WILDCARD |
  535. ZoneFinder.FIND_GLUE_OK)
  536. if to_delete.get_name() == self.__zname and\
  537. (to_delete.get_type() == RRType.SOA() or\
  538. to_delete.get_type() == RRType.NS()):
  539. # ignore
  540. return
  541. foreach_rr_in_rrset(to_delete, diff.delete_data, to_delete)
  542. def __ns_deleter_helper(self, diff, rrset):
  543. '''Special case helper for deleting NS resource records
  544. at the zone apex. In that scenario, the last NS record
  545. may never be removed (and any action that would do so
  546. should be ignored).
  547. '''
  548. result, orig_rrset, _ = self.__finder.find(rrset.get_name(),
  549. rrset.get_type(),
  550. ZoneFinder.NO_WILDCARD |
  551. ZoneFinder.FIND_GLUE_OK)
  552. # Even a real rrset comparison wouldn't help here...
  553. # The goal is to make sure that after deletion of the
  554. # given rrset, at least 1 NS record is left (at the apex).
  555. # So we make a (shallow) copy of the existing rrset,
  556. # and for each rdata in the to_delete set, we check if it wouldn't
  557. # delete the last one. If it would, that specific one is ignored.
  558. # If it would not, the rdata is removed from the temporary list
  559. orig_rrset_rdata = copy.copy(orig_rrset.get_rdata())
  560. for rdata in rrset.get_rdata():
  561. if len(orig_rrset_rdata) == 1 and rdata == orig_rrset_rdata[0]:
  562. # ignore
  563. continue
  564. else:
  565. # create an individual RRset for deletion
  566. to_delete = isc.dns.RRset(rrset.get_name(),
  567. rrset.get_class(),
  568. rrset.get_type(),
  569. rrset.get_ttl())
  570. to_delete.add_rdata(rdata)
  571. orig_rrset_rdata.remove(rdata)
  572. diff.delete_data(to_delete)
  573. def __do_update_delete_name(self, diff, rrset):
  574. '''Delete all data at the name of the given rrset,
  575. by adding all data found by find_all as delete statements
  576. to the given diff.
  577. Special case: if the name is the zone's apex, SOA and
  578. NS records are kept.
  579. '''
  580. result, rrsets, flags = self.__finder.find_all(rrset.get_name(),
  581. ZoneFinder.NO_WILDCARD |
  582. ZoneFinder.FIND_GLUE_OK)
  583. if result == ZoneFinder.SUCCESS and\
  584. (flags & ZoneFinder.RESULT_WILDCARD == 0):
  585. for to_delete in rrsets:
  586. # if name == self.__zname and type is soa or ns, don't delete!
  587. if to_delete.get_name() == self.__zname and\
  588. (to_delete.get_type() == RRType.SOA() or
  589. to_delete.get_type() == RRType.NS()):
  590. continue
  591. else:
  592. foreach_rr_in_rrset(to_delete, diff.delete_data, to_delete)
  593. def __do_update_delete_rrs_from_rrset(self, diff, rrset):
  594. '''Deletes all resource records in the given rrset from the
  595. zone. Resource records that do not exist are ignored.
  596. If the rrset if of type SOA, it is ignored.
  597. Uses the __ns_deleter_helper if the rrset's name is the
  598. zone's apex, and the type is NS.
  599. '''
  600. # Delete all rrs in the rrset, except if name=self.__zname and type=soa, or
  601. # type = ns and there is only one left (...)
  602. # The delete does not want class NONE, we would not have gotten here
  603. # if it wasn't, but now is a good time to change it to the zclass.
  604. to_delete = convert_rrset_class(rrset, self.__zclass)
  605. if rrset.get_name() == self.__zname:
  606. if rrset.get_type() == RRType.SOA():
  607. # ignore
  608. return
  609. elif rrset.get_type() == RRType.NS():
  610. # hmm. okay. annoying. There must be at least one left,
  611. # delegate to helper method
  612. self.__ns_deleter_helper(diff, to_delete)
  613. return
  614. foreach_rr_in_rrset(to_delete, diff.delete_data, to_delete)
  615. def __update_soa(self, diff):
  616. '''Checks the member value __added_soa, and depending on
  617. whether it has been set and what its value is, creates
  618. a new SOA if necessary.
  619. Then removes the original SOA and adds the new one,
  620. by adding the needed operations to the given diff.'''
  621. # Get the existing SOA
  622. # if a new soa was specified, add that one, otherwise, do the
  623. # serial magic and add the newly created one
  624. # get it from DS and to increment and stuff
  625. result, old_soa, _ = self.__finder.find(self.__zname, RRType.SOA(),
  626. ZoneFinder.NO_WILDCARD |
  627. ZoneFinder.FIND_GLUE_OK)
  628. if self.__added_soa is not None:
  629. new_soa = self.__added_soa
  630. # serial check goes here
  631. else:
  632. new_soa = old_soa
  633. # increment goes here
  634. diff.delete_data(old_soa)
  635. diff.add_data(new_soa)
  636. def __do_update(self):
  637. '''Scan, check, and execute the Update section in the
  638. DDNS Update message.
  639. Returns an Rcode to signal the result (NOERROR upon success,
  640. any error result otherwise).
  641. '''
  642. # prescan
  643. prescan_result = self.__do_prescan()
  644. if prescan_result != Rcode.NOERROR():
  645. return prescan_result
  646. # update
  647. # TODO: catchall? any error should result in abort and servfail...
  648. # Don't like catchalls much, though
  649. # create an ixfr-out-friendly diff structure to work on
  650. diff = isc.xfrin.diff.Diff(self.__datasrc_client, self.__zname,
  651. journaling=True, single_update_mode=True)
  652. # Do special handling for SOA first
  653. self.__update_soa(diff)
  654. # Algorithm from RFC2136 Section 3.4
  655. # Note that this works on full rrsets, not individual RRs.
  656. # Some checks might be easier with individual RRs, but only if we
  657. # would use the ZoneUpdater directly (so we can query the
  658. # 'zone-as-it-would-be-so-far'. However, due to the current use
  659. # of the Diff class, this is not the case, and therefore it
  660. # is easier to work with full rrsets for the most parts
  661. # (less lookups needed; conversion to individual rrs is
  662. # the same offort whether it is done here or in the several
  663. # do_update statements)
  664. for rrset in self.__message.get_section(SECTION_UPDATE):
  665. if rrset.get_class() == self.__zclass:
  666. self.__do_update_add_rrs_to_rrset(diff, rrset)
  667. elif rrset.get_class() == RRClass.ANY():
  668. if rrset.get_type() == RRType.ANY():
  669. self.__do_update_delete_name(diff, rrset)
  670. else:
  671. self.__do_update_delete_rrset(diff, rrset)
  672. elif rrset.get_class() == RRClass.NONE():
  673. self.__do_update_delete_rrs_from_rrset(diff, rrset)
  674. #try:
  675. diff.commit()
  676. return Rcode.NOERROR()
  677. #except isc.datasrc.Error as dse:
  678. # logger.info(LIBDDNS_UPDATE_DATASRC_ERROR, dse)
  679. # return Rcode.SERVFAIL()