query.cc 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. // Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
  2. //
  3. // Permission to use, copy, modify, and/or 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 ISC DISCLAIMS ALL WARRANTIES WITH
  8. // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
  9. // AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
  10. // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
  11. // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
  12. // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
  13. // PERFORMANCE OF THIS SOFTWARE.
  14. #include <algorithm> // for std::max
  15. #include <vector>
  16. #include <boost/foreach.hpp>
  17. #include <boost/bind.hpp>
  18. #include <boost/function.hpp>
  19. #include <dns/message.h>
  20. #include <dns/rcode.h>
  21. #include <dns/rdataclass.h>
  22. #include <datasrc/client.h>
  23. #include <auth/query.h>
  24. using namespace isc::dns;
  25. using namespace isc::datasrc;
  26. using namespace isc::dns::rdata;
  27. namespace isc {
  28. namespace auth {
  29. void
  30. Query::addAdditional(ZoneFinder& zone, const AbstractRRset& rrset) {
  31. RdataIteratorPtr rdata_iterator(rrset.getRdataIterator());
  32. for (; !rdata_iterator->isLast(); rdata_iterator->next()) {
  33. const Rdata& rdata(rdata_iterator->getCurrent());
  34. if (rrset.getType() == RRType::NS()) {
  35. // Need to perform the search in the "GLUE OK" mode.
  36. const generic::NS& ns = dynamic_cast<const generic::NS&>(rdata);
  37. addAdditionalAddrs(zone, ns.getNSName(), ZoneFinder::FIND_GLUE_OK);
  38. } else if (rrset.getType() == RRType::MX()) {
  39. const generic::MX& mx(dynamic_cast<const generic::MX&>(rdata));
  40. addAdditionalAddrs(zone, mx.getMXName());
  41. }
  42. }
  43. }
  44. void
  45. Query::addAdditionalAddrs(ZoneFinder& zone, const Name& qname,
  46. const ZoneFinder::FindOptions options)
  47. {
  48. // Out of zone name
  49. NameComparisonResult result = zone.getOrigin().compare(qname);
  50. if ((result.getRelation() != NameComparisonResult::SUPERDOMAIN) &&
  51. (result.getRelation() != NameComparisonResult::EQUAL))
  52. return;
  53. // Omit additional data which has already been provided in the answer
  54. // section from the additional.
  55. //
  56. // All the address rrset with the owner name of qname have been inserted
  57. // into ANSWER section.
  58. if (qname_ == qname && qtype_ == RRType::ANY())
  59. return;
  60. // Find A rrset
  61. if (qname_ != qname || qtype_ != RRType::A()) {
  62. ZoneFinder::FindResult a_result = zone.find(qname, RRType::A(),
  63. options | dnssec_opt_);
  64. if (a_result.code == ZoneFinder::SUCCESS) {
  65. response_.addRRset(Message::SECTION_ADDITIONAL,
  66. boost::const_pointer_cast<AbstractRRset>(a_result.rrset), dnssec_);
  67. }
  68. }
  69. // Find AAAA rrset
  70. if (qname_ != qname || qtype_ != RRType::AAAA()) {
  71. ZoneFinder::FindResult aaaa_result = zone.find(qname, RRType::AAAA(),
  72. options | dnssec_opt_);
  73. if (aaaa_result.code == ZoneFinder::SUCCESS) {
  74. response_.addRRset(Message::SECTION_ADDITIONAL,
  75. boost::const_pointer_cast<AbstractRRset>(aaaa_result.rrset),
  76. dnssec_);
  77. }
  78. }
  79. }
  80. void
  81. Query::addSOA(ZoneFinder& finder) {
  82. ZoneFinder::FindResult soa_result = finder.find(finder.getOrigin(),
  83. RRType::SOA(),
  84. dnssec_opt_);
  85. if (soa_result.code != ZoneFinder::SUCCESS) {
  86. isc_throw(NoSOA, "There's no SOA record in zone " <<
  87. finder.getOrigin().toText());
  88. } else {
  89. /*
  90. * FIXME:
  91. * The const-cast is wrong, but the Message interface seems
  92. * to insist.
  93. */
  94. response_.addRRset(Message::SECTION_AUTHORITY,
  95. boost::const_pointer_cast<AbstractRRset>(soa_result.rrset), dnssec_);
  96. }
  97. }
  98. // Note: unless the data source client implementation or the zone content
  99. // is broken, 'nsec' should be a valid NSEC RR. Likewise, the call to
  100. // find() in this method should result in NXDOMAIN and an NSEC RR that proves
  101. // the non existent of matching wildcard. If these assumptions aren't met
  102. // due to a buggy data source implementation or a broken zone, we'll let
  103. // underlying libdns++ modules throw an exception, which would result in
  104. // either an SERVFAIL response or just ignoring the query. We at least prevent
  105. // a complete crash due to such broken behavior.
  106. void
  107. Query::addNXDOMAINProof(ZoneFinder& finder, ConstRRsetPtr nsec) {
  108. if (nsec->getRdataCount() == 0) {
  109. isc_throw(BadNSEC, "NSEC for NXDOMAIN is empty");
  110. }
  111. // Add the NSEC proving NXDOMAIN to the authority section.
  112. response_.addRRset(Message::SECTION_AUTHORITY,
  113. boost::const_pointer_cast<AbstractRRset>(nsec), dnssec_);
  114. // Next, identify the best possible wildcard name that would match
  115. // the query name. It's the longer common suffix with the qname
  116. // between the owner or the next domain of the NSEC that proves NXDOMAIN,
  117. // prefixed by the wildcard label, "*". For example, for query name
  118. // a.b.example.com, if the NXDOMAIN NSEC is
  119. // b.example.com. NSEC c.example.com., the longer suffix is b.example.com.,
  120. // and the best possible wildcard is *.b.example.com. If the NXDOMAIN
  121. // NSEC is a.example.com. NSEC c.b.example.com., the longer suffix
  122. // is the next domain of the NSEC, and we get the same wildcard name.
  123. const int qlabels = qname_.getLabelCount();
  124. const int olabels = qname_.compare(nsec->getName()).getCommonLabels();
  125. const int nlabels = qname_.compare(
  126. dynamic_cast<const generic::NSEC&>(nsec->getRdataIterator()->
  127. getCurrent()).
  128. getNextName()).getCommonLabels();
  129. const int common_labels = std::max(olabels, nlabels);
  130. const Name wildname(Name("*").concatenate(qname_.split(qlabels -
  131. common_labels)));
  132. // Confirm the wildcard doesn't exist (this should result in NXDOMAIN;
  133. // otherwise we shouldn't have got NXDOMAIN for the original query in
  134. // the first place).
  135. const ZoneFinder::FindResult fresult =
  136. finder.find(wildname, RRType::NSEC(), dnssec_opt_);
  137. if (fresult.code != ZoneFinder::NXDOMAIN || !fresult.rrset ||
  138. fresult.rrset->getRdataCount() == 0) {
  139. isc_throw(BadNSEC, "Unexpected result for wildcard NXDOMAIN proof");
  140. }
  141. // Add the (no-) wildcard proof only when it's different from the NSEC
  142. // that proves NXDOMAIN; sometimes they can be the same.
  143. // Note: name comparison is relatively expensive. When we are at the
  144. // stage of performance optimization, we should consider optimizing this
  145. // for some optimized data source implementations.
  146. if (nsec->getName() != fresult.rrset->getName()) {
  147. response_.addRRset(Message::SECTION_AUTHORITY,
  148. boost::const_pointer_cast<AbstractRRset>(fresult.rrset),
  149. dnssec_);
  150. }
  151. }
  152. void
  153. Query::addWildcardProof(ZoneFinder& finder) {
  154. // The query name shouldn't exist in the zone if there were no wildcard
  155. // substitution. Confirm that by specifying NO_WILDCARD. It should result
  156. // in NXDOMAIN and an NSEC RR that proves it should be returned.
  157. const ZoneFinder::FindResult fresult =
  158. finder.find(qname_, RRType::NSEC(),
  159. dnssec_opt_ | ZoneFinder::NO_WILDCARD);
  160. if (fresult.code != ZoneFinder::NXDOMAIN || !fresult.rrset ||
  161. fresult.rrset->getRdataCount() == 0) {
  162. isc_throw(BadNSEC, "Unexpected result for wildcard proof");
  163. }
  164. response_.addRRset(Message::SECTION_AUTHORITY,
  165. boost::const_pointer_cast<AbstractRRset>(fresult.rrset),
  166. dnssec_);
  167. }
  168. void
  169. Query::addWildcardNXRRSETProof(ZoneFinder& finder, ConstRRsetPtr nsec) {
  170. // There should be one NSEC RR which was found in the zone to prove
  171. // that there is not matched <QNAME,QTYPE> via wildcard expansion.
  172. if (nsec->getRdataCount() == 0) {
  173. isc_throw(BadNSEC, "NSEC for WILDCARD_NXRRSET is empty");
  174. }
  175. const ZoneFinder::FindResult fresult =
  176. finder.find(qname_, RRType::NSEC(),
  177. dnssec_opt_ | ZoneFinder::NO_WILDCARD);
  178. if (fresult.code != ZoneFinder::NXDOMAIN || !fresult.rrset ||
  179. fresult.rrset->getRdataCount() == 0) {
  180. isc_throw(BadNSEC, "Unexpected result for no match QNAME proof");
  181. }
  182. if (nsec->getName() != fresult.rrset->getName()) {
  183. // one NSEC RR proves wildcard_nxrrset that no matched QNAME.
  184. response_.addRRset(Message::SECTION_AUTHORITY,
  185. boost::const_pointer_cast<AbstractRRset>(fresult.rrset),
  186. dnssec_);
  187. }
  188. }
  189. void
  190. Query::addDS(ZoneFinder& finder, const Name& dname) {
  191. ZoneFinder::FindResult ds_result =
  192. finder.find(dname, RRType::DS(), dnssec_opt_);
  193. if (ds_result.code == ZoneFinder::SUCCESS) {
  194. response_.addRRset(Message::SECTION_AUTHORITY,
  195. boost::const_pointer_cast<AbstractRRset>(ds_result.rrset),
  196. dnssec_);
  197. } else if (ds_result.code == ZoneFinder::NXRRSET) {
  198. addNXRRsetProof(finder, ds_result);
  199. } else {
  200. // Any other case should be an error
  201. isc_throw(BadDS, "Unexpected result for DS lookup for delegation");
  202. }
  203. }
  204. void
  205. Query::addNXRRsetProof(ZoneFinder& finder,
  206. const ZoneFinder::FindResult& db_result)
  207. {
  208. if (db_result.isNSECSigned() && db_result.rrset) {
  209. response_.addRRset(Message::SECTION_AUTHORITY,
  210. boost::const_pointer_cast<AbstractRRset>(
  211. db_result.rrset),
  212. dnssec_);
  213. if (db_result.isWildcard()) {
  214. addWildcardNXRRSETProof(finder, db_result.rrset);
  215. }
  216. } else if (db_result.isNSEC3Signed()) {
  217. // Handling depends on whether query type is DS or not
  218. // (see RFC5155, 7.2.3 and 7.2.4): If qtype == DS, do
  219. // recursive search (and add next_proof, if necessary),
  220. // otherwise, do non-recursive search
  221. const bool qtype_ds = (qtype_ == RRType::DS());
  222. ZoneFinder::FindNSEC3Result result(finder.findNSEC3(qname_, qtype_ds));
  223. if (result.matched) {
  224. response_.addRRset(Message::SECTION_AUTHORITY,
  225. boost::const_pointer_cast<AbstractRRset>(
  226. result.closest_proof), dnssec_);
  227. // For qtype == DS, next_proof could be set
  228. // (We could check for opt-out here, but that's really the
  229. // responsibility of the datasource)
  230. if (qtype_ds && result.next_proof != ConstRRsetPtr()) {
  231. response_.addRRset(Message::SECTION_AUTHORITY,
  232. boost::const_pointer_cast<AbstractRRset>(
  233. result.next_proof), dnssec_);
  234. }
  235. } else {
  236. isc_throw(BadNSEC3, "No NSEC3 found for existing domain " <<
  237. qname_.toText());
  238. }
  239. }
  240. }
  241. void
  242. Query::addAuthAdditional(ZoneFinder& finder) {
  243. // Fill in authority and addtional sections.
  244. ZoneFinder::FindResult ns_result =
  245. finder.find(finder.getOrigin(), RRType::NS(), dnssec_opt_);
  246. // zone origin name should have NS records
  247. if (ns_result.code != ZoneFinder::SUCCESS) {
  248. isc_throw(NoApexNS, "There's no apex NS records in zone " <<
  249. finder.getOrigin().toText());
  250. } else {
  251. response_.addRRset(Message::SECTION_AUTHORITY,
  252. boost::const_pointer_cast<AbstractRRset>(ns_result.rrset), dnssec_);
  253. // Handle additional for authority section
  254. addAdditional(finder, *ns_result.rrset);
  255. }
  256. }
  257. namespace {
  258. // A simple wrapper for DataSourceClient::findZone(). Normally we can simply
  259. // check the closest zone to the qname, but for type DS query we need to
  260. // look into the parent zone. Nevertheless, if there is no "parent" (i.e.,
  261. // the qname consists of a single label, which also means it's the root name),
  262. // we should search the deepest zone we have (which should be the root zone;
  263. // otherwise it's a query error).
  264. DataSourceClient::FindResult
  265. findZone(const DataSourceClient& client, const Name& qname, RRType qtype) {
  266. if (qtype != RRType::DS() || qname.getLabelCount() == 1) {
  267. return (client.findZone(qname));
  268. }
  269. return (client.findZone(qname.split(1)));
  270. }
  271. }
  272. void
  273. Query::process() {
  274. // Found a zone which is the nearest ancestor to QNAME
  275. const DataSourceClient::FindResult result = findZone(datasrc_client_,
  276. qname_, qtype_);
  277. // If we have no matching authoritative zone for the query name, return
  278. // REFUSED. In short, this is to be compatible with BIND 9, but the
  279. // background discussion is not that simple. See the relevant topic
  280. // at the BIND 10 developers's ML:
  281. // https://lists.isc.org/mailman/htdig/bind10-dev/2010-December/001633.html
  282. if (result.code != result::SUCCESS &&
  283. result.code != result::PARTIALMATCH) {
  284. // If we tried to find a "parent zone" for a DS query and failed,
  285. // we may still have authority at the child side. If we do, the query
  286. // has to be handled there.
  287. if (qtype_ == RRType::DS() && qname_.getLabelCount() > 1 &&
  288. processDSAtChild()) {
  289. return;
  290. }
  291. response_.setHeaderFlag(Message::HEADERFLAG_AA, false);
  292. response_.setRcode(Rcode::REFUSED());
  293. return;
  294. }
  295. ZoneFinder& zfinder = *result.zone_finder;
  296. // We have authority for a zone that contain the query name (possibly
  297. // indirectly via delegation). Look into the zone.
  298. response_.setHeaderFlag(Message::HEADERFLAG_AA);
  299. response_.setRcode(Rcode::NOERROR());
  300. std::vector<ConstRRsetPtr> target;
  301. boost::function0<ZoneFinder::FindResult> find;
  302. const bool qtype_is_any = (qtype_ == RRType::ANY());
  303. if (qtype_is_any) {
  304. find = boost::bind(&ZoneFinder::findAll, &zfinder, qname_,
  305. boost::ref(target), dnssec_opt_);
  306. } else {
  307. find = boost::bind(&ZoneFinder::find, &zfinder, qname_, qtype_,
  308. dnssec_opt_);
  309. }
  310. ZoneFinder::FindResult db_result(find());
  311. switch (db_result.code) {
  312. case ZoneFinder::DNAME: {
  313. // First, put the dname into the answer
  314. response_.addRRset(Message::SECTION_ANSWER,
  315. boost::const_pointer_cast<AbstractRRset>(db_result.rrset),
  316. dnssec_);
  317. /*
  318. * Empty DNAME should never get in, as it is impossible to
  319. * create one in master file.
  320. *
  321. * FIXME: Other way to prevent this should be done
  322. */
  323. assert(db_result.rrset->getRdataCount() > 0);
  324. // Get the data of DNAME
  325. const rdata::generic::DNAME& dname(
  326. dynamic_cast<const rdata::generic::DNAME&>(
  327. db_result.rrset->getRdataIterator()->getCurrent()));
  328. // The yet unmatched prefix dname
  329. const Name prefix(qname_.split(0, qname_.getLabelCount() -
  330. db_result.rrset->getName().getLabelCount()));
  331. // If we put it together, will it be too long?
  332. // (The prefix contains trailing ., which will be removed
  333. if (prefix.getLength() - Name::ROOT_NAME().getLength() +
  334. dname.getDname().getLength() > Name::MAX_WIRE) {
  335. /*
  336. * In case the synthesized name is too long, section 4.1
  337. * of RFC 2672 mandates we return YXDOMAIN.
  338. */
  339. response_.setRcode(Rcode::YXDOMAIN());
  340. return;
  341. }
  342. // The new CNAME we are creating (it will be unsigned even
  343. // with DNSSEC, the DNAME is signed and it can be validated
  344. // by that)
  345. RRsetPtr cname(new RRset(qname_, db_result.rrset->getClass(),
  346. RRType::CNAME(), db_result.rrset->getTTL()));
  347. // Construct the new target by replacing the end
  348. cname->addRdata(rdata::generic::CNAME(qname_.split(0,
  349. qname_.getLabelCount() -
  350. db_result.rrset->getName().getLabelCount()).
  351. concatenate(dname.getDname())));
  352. response_.addRRset(Message::SECTION_ANSWER, cname, dnssec_);
  353. break;
  354. }
  355. case ZoneFinder::CNAME:
  356. /*
  357. * We don't do chaining yet. Therefore handling a CNAME is
  358. * mostly the same as handling SUCCESS, but we didn't get
  359. * what we expected. It means no exceptions in ANY or NS
  360. * on the origin (though CNAME in origin is probably
  361. * forbidden anyway).
  362. *
  363. * So, just put it there.
  364. */
  365. response_.addRRset(Message::SECTION_ANSWER,
  366. boost::const_pointer_cast<AbstractRRset>(db_result.rrset),
  367. dnssec_);
  368. // If the answer is a result of wildcard substitution,
  369. // add a proof that there's no closer name.
  370. if (dnssec_ && db_result.isWildcard()) {
  371. addWildcardProof(*result.zone_finder);
  372. }
  373. break;
  374. case ZoneFinder::SUCCESS:
  375. if (qtype_is_any) {
  376. // If quety type is ANY, insert all RRs under the domain
  377. // into answer section.
  378. BOOST_FOREACH(ConstRRsetPtr rrset, target) {
  379. response_.addRRset(Message::SECTION_ANSWER,
  380. boost::const_pointer_cast<AbstractRRset>(rrset), dnssec_);
  381. // Handle additional for answer section
  382. addAdditional(*result.zone_finder, *rrset.get());
  383. }
  384. } else {
  385. response_.addRRset(Message::SECTION_ANSWER,
  386. boost::const_pointer_cast<AbstractRRset>(db_result.rrset),
  387. dnssec_);
  388. // Handle additional for answer section
  389. addAdditional(*result.zone_finder, *db_result.rrset);
  390. }
  391. // If apex NS records haven't been provided in the answer
  392. // section, insert apex NS records into the authority section
  393. // and AAAA/A RRS of each of the NS RDATA into the additional
  394. // section.
  395. if (qname_ != result.zone_finder->getOrigin() ||
  396. db_result.code != ZoneFinder::SUCCESS ||
  397. (qtype_ != RRType::NS() && !qtype_is_any))
  398. {
  399. addAuthAdditional(*result.zone_finder);
  400. }
  401. // If the answer is a result of wildcard substitution,
  402. // add a proof that there's no closer name.
  403. if (dnssec_ && db_result.isWildcard()) {
  404. addWildcardProof(*result.zone_finder);
  405. }
  406. break;
  407. case ZoneFinder::DELEGATION:
  408. // If a DS query resulted in delegation, we also need to check
  409. // if we are an authority of the child, too. If so, we need to
  410. // complete the process in the child as specified in Section
  411. // 2.2.1.2. of RFC3658.
  412. if (qtype_ == RRType::DS() && processDSAtChild()) {
  413. return;
  414. }
  415. response_.setHeaderFlag(Message::HEADERFLAG_AA, false);
  416. response_.addRRset(Message::SECTION_AUTHORITY,
  417. boost::const_pointer_cast<AbstractRRset>(db_result.rrset),
  418. dnssec_);
  419. // If DNSSEC is requested, see whether there is a DS
  420. // record for this delegation.
  421. if (dnssec_) {
  422. addDS(*result.zone_finder, db_result.rrset->getName());
  423. }
  424. addAdditional(*result.zone_finder, *db_result.rrset);
  425. break;
  426. case ZoneFinder::NXDOMAIN:
  427. response_.setRcode(Rcode::NXDOMAIN());
  428. addSOA(*result.zone_finder);
  429. if (dnssec_ && db_result.rrset) {
  430. addNXDOMAINProof(zfinder, db_result.rrset);
  431. }
  432. break;
  433. case ZoneFinder::NXRRSET:
  434. addSOA(*result.zone_finder);
  435. if (dnssec_) {
  436. addNXRRsetProof(zfinder, db_result);
  437. }
  438. break;
  439. default:
  440. // This is basically a bug of the data source implementation,
  441. // but could also happen in the middle of development where
  442. // we try to add a new result code.
  443. isc_throw(isc::NotImplemented, "Unknown result code");
  444. break;
  445. }
  446. }
  447. bool
  448. Query::processDSAtChild() {
  449. const DataSourceClient::FindResult zresult =
  450. datasrc_client_.findZone(qname_);
  451. if (zresult.code != result::SUCCESS) {
  452. return (false);
  453. }
  454. // We are receiving a DS query at the child side of the owner name,
  455. // where the DS isn't supposed to belong. We should return a "no data"
  456. // response as described in Section 3.1.4.1 of RFC4035 and Section
  457. // 2.2.1.1 of RFC 3658. find(DS) should result in NXRRSET, in which
  458. // case (and if DNSSEC is required) we also add the proof for that,
  459. // but even if find() returns an unexpected result, we don't bother.
  460. // The important point in this case is to return SOA so that the resolver
  461. // that happens to contact us can hunt for the appropriate parent zone
  462. // by seeing the SOA.
  463. response_.setHeaderFlag(Message::HEADERFLAG_AA);
  464. response_.setRcode(Rcode::NOERROR());
  465. addSOA(*zresult.zone_finder);
  466. const ZoneFinder::FindResult ds_result =
  467. zresult.zone_finder->find(qname_, RRType::DS(), dnssec_opt_);
  468. if (ds_result.code == ZoneFinder::NXRRSET) {
  469. if (dnssec_) {
  470. addNXRRsetProof(*zresult.zone_finder, ds_result);
  471. }
  472. }
  473. return (true);
  474. }
  475. }
  476. }