diff --git a/ChangeLog b/ChangeLog index 3d20982ef30c6fa632077c790d3faaa6a9361e7c..a0907da010be86ba50ea193fd5b267d2e4ca76d9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,122 @@ + 178. [func] jelte + Resolver now makes (limited) use of the cache + (Trac #491, git 8b41f77f0099ddc7ca7d34d39ad8c39bb1a8363c) + + 177. [func] stephen + The upstream fetch code in asiolink is now protocol agnostic to + allow for the addition of fallback to TCP if a fetch response + indicates truncation. + (Trac #554, git 9739cbce2eaffc7e80640db58a8513295cf684de) + + 176. [func] zhang likun + src/lib/cache: Rename one interface: from lookupClosestRRset() + to lookupDeepestNS(), and remove one parameter of it. + (Trac #492, git ecbfb7cf929d62a018dd4cdc7a841add3d5a35ae) + + 175. [bug] jerry + src/bin/xfrout: Xfrout use the case-sensitive mode to compress + names in an AXFR massage. + (Trac #253, git 004e382616150f8a2362e94d3458b59bb2710182) + + 174. [bug]* jinmei + src/lib/dns: revised dnssectime functions so that they don't rely + on the time_t type (whose size varies on different systems, which + can lead to subtle bugs like some form of "year 2038 problem"). + Also handled 32-bit wrap around issues more explicitly, with more + detailed tests. The function API has been changed, but the effect + should be minimal because these functions are mostly private. + (Trac #61, git 09ece8cdd41c0f025e8b897b4883885d88d4ba5d) + + 173. [bug] jerry + python/isc/notify: A notify_out test fails without network + connectivity, encapsulate the socket behavior using a mock + socket class to fix it. + (Trac #346, git 319debfb957641f311102739a15059f8453c54ce) + + 172. [func] jelte + Improved the bindctl cli in various ways, mainly concerning + list and map item addressing, the correct display of actual values, + and internal help. + (Trac #384, git e5fb3bc1ed5f3c0aec6eb40a16c63f3d0fc6a7b2) + + 171. [func] feng, jerry, jinmei, vorner + b10-auth, src/lib/datasrc: in memory data source now works as a + complete data source for authoritative DNS servers and b10-auth + uses it. It still misses major features, however, including + DNSSEC support and zone transfer. + (Last trac #553, but many more, + git 6f031a09a248e7684723c000f3e8cc981dcdb349) + + 170. [bug] jinmei + Tightened validity checks in the NSEC3 constructors, both "from + "text" and "from wire". Specifically, wire data containing + invalid type bitmaps or invalid lengths of salt or hash is now + correctly rejected. + (Trac #117, git 9c690982f24fef19c747a72f43c4298333a58f48) + + 169. [func] zhang likun, jelte + Added a basic implementation for a resolver cache (though not + used yet). + (Trac #449, git 8aa3b2246ae095bbe7f855fd11656ae3bdb98986) + + 168. [bug] vorner + Boss no longer has the -f argument, which was undocumented and + stayed as a relict of previous versions, currently causing only + strange behaviour. + (Trac #572, git 17f237478961005707d649a661cc72a4a0d612d4) + + 167. [bug] naokikambe + Fixed failure of termination of msgq_test.py with python3 + coverage(3.3.1) + (Trac #573, git 0e6a18e12f61cc482e07078776234f32605312e5) + + 166. [func] jelte + The resolver now sends back a SERVFAIL when there is a client + timeout (timeout_client config setting), but it will not stop + resolving (until there is a lookup timeout or a result). + (Trac #497 and #489, git af0e5cd93bebb27cb5c4457f7759d12c8bf953a6) + + 165. [func] jelte + The resolver now handles CNAMEs, it will follow them, and include + them in the answer. The maximum length of CNAME chains that is + supported is 16. + (Trac #497, git af0e5cd93bebb27cb5c4457f7759d12c8bf953a6) + + 164. [bug] y-aharen + IntervalTimer: Modified the interface to accept interval in + milliseconds. It shortens the time of the tests of IntervalTimer. + (Trac #452, git c9f6acc81e24c4b8f0eb351123dc7b43f64e0914) + + 163. [func] vorner + The pimpl design pattern is used in UDPServer, with a shared + pointer. This makes it smaller to copy (which is done a lot as a + sideeffect of being coroutine) and speeds applications of this + class (notably b10-auth) up by around 10%. + (Trac #537, git 94cb95b1d508541201fc064302ba836164d3cbe6) + + 162. [func] stephen + Added C++ logging, allowing logging at different severities. + Code specifies the message to be logged via a symbol, and the + logging code picks up the message from an in-built dictionary. + The contents of the dictionary can be replaced at run-time by + locale-specific messages. A message compiler program is provided + to create message header files and supply the default messages. + (Trac #438, git 7b1606cea7af15dc71f5ec1d70d958b00aa98af7) + + 161. [func] stephen + Added ResponseScrubber class to examine response from + a server and to remove out-of-bailiwick RRsets. Also + does cross-section checks to ensure consistency. + (Trac #496, git b9296ca023cc9e76cda48a7eeebb0119166592c5) + + 160. [func] jelte + Updated the resolver to take 3 different timeout values; + timeout_query for outstanding queries we sent while resolving + timeout_client for sending an answer back to the client + timeout_lookup for stopping the resolving + (currently 2 and 3 have the same final effect) + (Trac 489, git 578ea7f4ba94dc0d8a3d39231dad2be118e125a2) + 159. [func] smann The resolver now has a configurable set of root servers to start resolving at (called root_addresses). By default these are not @@ -36,7 +155,8 @@ bind10-devel-20110120 released on January 20, 2011 receive buffer became full, and many other components that rely on CC channels would stall (as noted in #420 and #513). This is an urgent care fix due to the severity of the problem; we'll need - to revisit it for cleaner fix later. (Trac #516, git 62c72fc) + to revisit it for cleaner fix later. + (Trac #516, git 62c72fcdf4617e4841e901408f1e7961255b8194) 153. [bug] jelte b10-cfgmgr: Fixed a bug where configuration updates sometimes @@ -50,7 +170,7 @@ bind10-devel-20110120 released on January 20, 2011 the value to 0. Disabling statistics updates will also work as a temporary workaround of a known issue that b10-auth can block in sending statistics and stop responding to queries as a result. - (Trac #513, git 285c5ee) + (Trac #513, git 285c5ee3d5582ed6df02d1aa00387f92a74e3695) 151. [bug] smann lib/log/dummylog.h: @@ -82,7 +202,7 @@ bind10-devel-20110120 released on January 20, 2011 147. [bug] jinmei python/isc/config: Fixed a bug that importing custom configuration (in b10-config.db) of a remote module didn't work. - (Trac #478, git ea4a481) + (Trac #478, git ea4a481003d80caf2bff8d0187790efd526d72ca) 146. [func] jelte Command arguments were not validated internally against their @@ -97,7 +217,8 @@ bind10-devel-20110120 released on January 20, 2011 only feasible for class IN in memory data source. To reload a zone "example.com" via bindctl, execute the command as follows: > Auth loadzone origin = example.com - (Trac #467) + (Trac #467 git 4f7e1f46da1046de527ab129a88f6aad3dba7562 + from 1d7d3918661ba1c6a8b1e40d8fcbc5640a84df12) 144. [build] jinmei Introduced a workaround for clang++ build on FreeBSD (and probably @@ -107,7 +228,8 @@ bind10-devel-20110120 released on January 20, 2011 doesn't matter; the important part is the -L flag). This workaround is not automatically enabled as it's difficult to detect the need for it dynamically, and must be enabled via the - variable by hand. (Trac #474, git cfde436) + variable by hand. + (Trac #474, git cfde436fbd7ddf3f49cbbd153999656e8ca2a298) 143. [build] jinmei Fixed build problems with clang++ in unit tests due to recent diff --git a/Makefile.am b/Makefile.am index 93a7498fbb4aa53828baa3972608d8ffe0dc1cf1..68a41d62f579f46ac8ed514d1561cbf6e89f7bf5 100644 --- a/Makefile.am +++ b/Makefile.am @@ -282,3 +282,4 @@ EXTRA_DIST += ext/asio/asio/is_write_buffered.hpp EXTRA_DIST += ext/asio/asio/buffered_read_stream_fwd.hpp EXTRA_DIST += ext/asio/asio/socket_acceptor_service.hpp EXTRA_DIST += ext/asio/asio.hpp +EXTRA_DIST += ext/coroutine/coroutine.h diff --git a/configure.ac b/configure.ac index f08f04451a49e3ff4a75f9681311eea3e2572ed1..139166b6d149060dbb09e7ceb6b1639c27d5cf4c 100644 --- a/configure.ac +++ b/configure.ac @@ -363,6 +363,57 @@ if test "$lcov" != "no"; then fi AC_SUBST(USE_LCOV) +# Configure log4cxx header and library path +# +# If explicitly specified, use it. + +AC_ARG_WITH([log4cxx], + AC_HELP_STRING([--with-log4cxx=PATH], + [specify directory where log4cxx is installed]), + [ + log4cxx_include_path="${withval}/include"; + log4cxx_library_path="${withval}/lib" + ]) + +# This is an urgent fix to avoid regression due to log4cxx on some +# platforms. It should be cleaned up with a better fix. +if test "X$with_log4cxx" != "Xno"; then + +# If not specified, try some common paths. These default to +# /usr/include and /usr/lib if not found + +if test -z "$with_log4cxx"; then + log4cxxdirs="/usr/local /usr/pkg /opt /opt/local" + for d in $log4cxxdirs + do + if test -d $d/include/log4cxx; then + log4cxx_include_path=$d/include + log4cxx_library_path=$d/lib + break + fi + done +fi + +CPPFLAGS_SAVES="$CPPFLAGS" +if test "${log4cxx_include_path}" ; then + LOG4CXX_INCLUDES="-I${log4cxx_include_path}" + CPPFLAGS="$CPPFLAGS $LOG4CXX_INCLUDES" +fi +AC_CHECK_HEADER([log4cxx/logger.h],, AC_MSG_ERROR([Missing log4cxx header files.])) +CPPFLAGS="$CPPFLAGS_SAVES" +AC_SUBST(LOG4CXX_INCLUDES) + +LOG4CXX_LDFLAGS="-llog4cxx"; +if test "${log4cxx_library_path}"; then + LOG4CXX_LDFLAGS="-L${log4cxx_library_path} -llog4cxx" +fi +AC_SUBST(LOG4CXX_LDFLAGS) + +# The following two lines are part of the urgent fix, and should be cleaned +# up with a better fix. +fi +AM_CONDITIONAL(USE_LOG4CXX, test "X${with_log4cxx}" != "Xno") + # # Configure Boost header path # @@ -522,6 +573,9 @@ AC_SUBST(MULTITHREADING_FLAG) # CPPFLAGS="$CPPFLAGS -I\$(top_srcdir)/ext/asio" # +# Use our 'coroutine' header from ext +CPPFLAGS="$CPPFLAGS -I\$(top_srcdir)/ext/coroutine" +# # Disable threads: Currently we don't use them. CPPFLAGS="$CPPFLAGS -DASIO_DISABLE_THREADS=1" # @@ -615,8 +669,6 @@ AC_CONFIG_FILES([Makefile src/lib/Makefile src/lib/asiolink/Makefile src/lib/asiolink/tests/Makefile - src/lib/asiolink/internal/Makefile - src/lib/asiolink/internal/tests/Makefile src/lib/bench/Makefile src/lib/bench/example/Makefile src/lib/bench/tests/Makefile @@ -652,10 +704,16 @@ AC_CONFIG_FILES([Makefile src/lib/datasrc/tests/Makefile src/lib/xfr/Makefile src/lib/log/Makefile + src/lib/log/compiler/Makefile + src/lib/log/tests/Makefile + src/lib/resolve/Makefile + src/lib/resolve/tests/Makefile src/lib/testutils/Makefile src/lib/testutils/testdata/Makefile src/lib/nsas/Makefile src/lib/nsas/tests/Makefile + src/lib/cache/Makefile + src/lib/cache/tests/Makefile ]) AC_OUTPUT([doc/version.ent src/bin/cfgmgr/b10-cfgmgr.py @@ -711,6 +769,7 @@ AC_OUTPUT([doc/version.ent src/lib/dns/tests/testdata/gen-wiredata.py src/lib/cc/session_config.h.pre src/lib/cc/tests/session_unittests_config.h + src/lib/log/tests/run_time_init_test.sh ], [ chmod +x src/bin/cmdctl/run_b10-cmdctl.sh chmod +x src/bin/xfrin/run_b10-xfrin.sh @@ -734,6 +793,7 @@ AC_OUTPUT([doc/version.ent chmod +x src/bin/msgq/tests/msgq_test chmod +x src/lib/dns/gen-rdatacode.py chmod +x src/lib/dns/tests/testdata/gen-wiredata.py + chmod +x src/lib/log/tests/run_time_init_test.sh ]) AC_OUTPUT @@ -761,6 +821,8 @@ dnl includes too ${PYTHON_LDFLAGS} ${PYTHON_LIB} Boost: ${BOOST_INCLUDES} + log4cxx: ${LOG4CXX_INCLUDES} + ${LOG4CXX_LDFLAGS} SQLite: $SQLITE_CFLAGS $SQLITE_LIBS diff --git a/doc/Doxyfile b/doc/Doxyfile index 6ab200178d30177c41dc1f8fef5662e0c05c809b..34ec3d8b006f85dcd38b0f9d141205021ecd75a4 100644 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -568,7 +568,7 @@ WARN_LOGFILE = # directories like "/usr/src/myproject". Separate the files or directories # with spaces. -INPUT = ../src/lib/cc ../src/lib/config ../src/lib/dns ../src/lib/exceptions ../src/lib/datasrc ../src/bin/auth ../src/bin/resolver ../src/lib/bench ../src/lib/log ../src/lib/asiolink/ ../src/lib/nsas ../src/lib/testutils +INPUT = ../src/lib/cc ../src/lib/config ../src/lib/dns ../src/lib/exceptions ../src/lib/datasrc ../src/bin/auth ../src/bin/resolver ../src/lib/bench ../src/lib/log ../src/lib/asiolink/ ../src/lib/nsas ../src/lib/testutils ../src/lib/cache # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is diff --git a/doc/guide/bind10-guide.html b/doc/guide/bind10-guide.html index 849cd06b30210b5944ad4dc01ad162b35e9e3553..98c7e464f69cab8af4980f512da36b441b89f9ac 100644 --- a/doc/guide/bind10-guide.html +++ b/doc/guide/bind10-guide.html @@ -1,10 +1,10 @@ -BIND 10 Guide

BIND 10 Guide

Administrator Reference for BIND 10

This is the reference guide for BIND 10 version - 20101201.

Abstract

BIND 10 is a Domain Name System (DNS) suite managed by +BIND 10 Guide

BIND 10 Guide

Administrator Reference for BIND 10

This is the reference guide for BIND 10 version + 20110120.

Abstract

BIND 10 is a Domain Name System (DNS) suite managed by Internet Systems Consortium (ISC). It includes DNS libraries and modular components for controlling authoritative and recursive DNS servers.

- This is the reference guide for BIND 10 version 20101201. + This is the reference guide for BIND 10 version 20110120. The most up-to-date version of this document, along with other documents for BIND 10, can be found at http://bind10.isc.org/docs.


Chapter 1. Introduction

BIND is the popular implementation of a DNS server, developer @@ -13,7 +13,7 @@ and provides a modular environment for serving and maintaining DNS.

Note

This guide covers the experimental prototype of - BIND 10 version 20101201. + BIND 10 version 20110120.

Note

BIND 10 provides a EDNS0- and DNSSEC-capable authoritative DNS server and a forwarding DNS server. diff --git a/doc/guide/bind10-guide.xml b/doc/guide/bind10-guide.xml index 70f6a30af97bdddf412f80cc168e5ca9ed141722..3670c460ad7e1eb88adf64c3eeb85d1b52274a4f 100644 --- a/doc/guide/bind10-guide.xml +++ b/doc/guide/bind10-guide.xml @@ -982,6 +982,8 @@ accounts_file The control commands are: print_settings + + shutdown diff --git a/src/lib/asiolink/internal/coroutine.h b/ext/coroutine/coroutine.h similarity index 100% rename from src/lib/asiolink/internal/coroutine.h rename to ext/coroutine/coroutine.h diff --git a/src/bin/auth/Makefile.am b/src/bin/auth/Makefile.am index e9097f21df09ca95ad0f4fadeeb2ab1c085e40ae..36de53dc6eabcff497e1e904c8b297a36ce1042e 100644 --- a/src/bin/auth/Makefile.am +++ b/src/bin/auth/Makefile.am @@ -50,6 +50,7 @@ b10_auth_LDADD += $(top_builddir)/src/lib/config/libcfgclient.la b10_auth_LDADD += $(top_builddir)/src/lib/cc/libcc.la b10_auth_LDADD += $(top_builddir)/src/lib/exceptions/libexceptions.la b10_auth_LDADD += $(top_builddir)/src/lib/asiolink/libasiolink.la +b10_auth_LDADD += $(top_builddir)/src/lib/nsas/libnsas.la b10_auth_LDADD += $(top_builddir)/src/lib/xfr/libxfr.la b10_auth_LDADD += $(SQLITE_LIBS) diff --git a/src/bin/auth/auth.spec.pre.in b/src/bin/auth/auth.spec.pre.in index 8a77455d3ef64ad4658cea060169b353a1588b26..7cb571c95943f6091632ce81b674007ab2904729 100644 --- a/src/bin/auth/auth.spec.pre.in +++ b/src/bin/auth/auth.spec.pre.in @@ -12,45 +12,44 @@ "item_type": "list", "item_optional": true, "item_default": [], - "list_item_spec": { - "item_name": "list_element", + "list_item_spec": + { "item_name": "list_element", "item_type": "map", "item_optional": false, "item_default": {}, - "map_item_spec": [ - { "item_name": "type", - "item_type": "string", - "item_optional": false, - "item_default": "" - }, - { "item_name": "class", - "item_type": "string", - "item_optional": false, - "item_default": "IN" - }, - { "item_name": "zones", - "item_type": "list", - "item_optional": false, - "item_default": [], - "list_item_spec": { - "item_name": "list_element", - "item_type": "map", - "item_optional": true, - "map_item_spec": [ - { "item_name": "origin", - "item_type": "string", - "item_optional": false, - "item_default": "" - }, - { "item_name": "file", - "item_type": "string", - "item_optional": false, - "item_default": "" - } - ] - } - } - ] + "map_item_spec": [ + { "item_name": "type", + "item_type": "string", + "item_optional": false, + "item_default": "" + }, + { "item_name": "class", + "item_type": "string", + "item_optional": false, + "item_default": "IN" + }, + { "item_name": "zones", + "item_type": "list", + "item_optional": false, + "item_default": [], + "list_item_spec": + { "item_name": "list_element", + "item_type": "map", + "item_optional": true, + "item_default": { "origin": "", "file": "" }, + "map_item_spec": [ + { "item_name": "origin", + "item_type": "string", + "item_optional": false, + "item_default": "" + }, + { "item_name": "file", + "item_type": "string", + "item_optional": false, + "item_default": "" + }] + } + }] } }, { "item_name": "statistics-interval", diff --git a/src/bin/auth/auth_srv.cc b/src/bin/auth/auth_srv.cc index b8d57302e941c20c872e8d8ea9c1cbf6298b285d..045fe7f65f1434e8c03545bedbb3612012b10bfe 100644 --- a/src/bin/auth/auth_srv.cc +++ b/src/bin/auth/auth_srv.cc @@ -354,7 +354,7 @@ AuthSrv::setMemoryDataSrc(const isc::dns::RRClass& rrclass, uint32_t AuthSrv::getStatisticsTimerInterval() const { - return (impl_->statistics_timer_.getInterval()); + return (impl_->statistics_timer_.getInterval() / 1000); } void @@ -362,11 +362,17 @@ AuthSrv::setStatisticsTimerInterval(uint32_t interval) { if (interval == impl_->statistics_timer_.getInterval()) { return; } + if (interval > 86400) { + // It can't occur since the value is checked in + // statisticsIntervalConfig::build(). + isc_throw(InvalidParameter, "Too long interval: " << interval); + } if (interval == 0) { impl_->statistics_timer_.cancel(); } else { - impl_->statistics_timer_.setupTimer( - boost::bind(&AuthSrv::submitStatistics, this), interval); + impl_->statistics_timer_.setup(boost::bind(&AuthSrv::submitStatistics, + this), + interval * 1000); } if (impl_->verbose_mode_) { if (interval == 0) { diff --git a/src/bin/auth/auth_srv.h b/src/bin/auth/auth_srv.h index 7806be9667da84b9a0f73997e2b2202b3bbd0afa..4772a02d64ec428f3c1abac85a5f072749305cc6 100644 --- a/src/bin/auth/auth_srv.h +++ b/src/bin/auth/auth_srv.h @@ -318,7 +318,8 @@ public: /// If the specified value is non 0, the \c AuthSrv object will submit /// its statistics to the statistics module every \c interval seconds. /// If it's 0, and \c AuthSrv currently submits statistics, the submission - /// will be disabled. + /// will be disabled. \c interval must be equal to or shorter than 86400 + /// seconds (1 day). /// /// This method should normally not throw an exception; however, its /// underlying library routines may involve resource allocation, and diff --git a/src/bin/auth/b10-auth.xml b/src/bin/auth/b10-auth.xml index 12e24bd98f8e6ebecd8aed26f79213f968f70d44..b22d24dc9cd9c0be1d6e5011d85eae6cf4ecea35 100644 --- a/src/bin/auth/b10-auth.xml +++ b/src/bin/auth/b10-auth.xml @@ -134,7 +134,7 @@ The port number it listens on. The default is 5300. - The Y1 prototype runs on all interfaces + This prototype runs on all interfaces and on this nonstandard port. diff --git a/src/bin/auth/benchmarks/Makefile.am b/src/bin/auth/benchmarks/Makefile.am index 165bb4c025179b8d960dd9e1810d4eb10d172c23..653d5025f34ca14561a405d249d9cefa975db5a9 100644 --- a/src/bin/auth/benchmarks/Makefile.am +++ b/src/bin/auth/benchmarks/Makefile.am @@ -20,5 +20,7 @@ query_bench_LDADD += $(top_builddir)/src/lib/datasrc/libdatasrc.la query_bench_LDADD += $(top_builddir)/src/lib/config/libcfgclient.la query_bench_LDADD += $(top_builddir)/src/lib/cc/libcc.la query_bench_LDADD += $(top_builddir)/src/lib/xfr/libxfr.la +query_bench_LDADD += $(top_builddir)/src/lib/log/liblog.la +query_bench_LDADD += $(top_builddir)/src/lib/nsas/libnsas.la query_bench_LDADD += $(top_builddir)/src/lib/asiolink/libasiolink.la query_bench_LDADD += $(SQLITE_LIBS) diff --git a/src/bin/auth/config.cc b/src/bin/auth/config.cc index 1f258e32ad4d76d3604961e11825accc8b5b8c38..5befc6e2c31b809458aa087d4e9fd8480d57d296 100644 --- a/src/bin/auth/config.cc +++ b/src/bin/auth/config.cc @@ -179,9 +179,14 @@ public: virtual void build(ConstElementPtr config_value) { const int32_t config_interval = config_value->intValue(); if (config_interval < 0) { - isc_throw(AuthConfigError, "negative statistics-interval value: " + isc_throw(AuthConfigError, "Negative statistics interval value: " << config_interval); } + if (config_interval > 86400) { + isc_throw(AuthConfigError, "Statistics interval value " + << config_interval + << " must be equal to or shorter than 86400"); + } interval_ = config_interval; } virtual void commit() { diff --git a/src/bin/auth/query.cc b/src/bin/auth/query.cc index e270500da76f21dcbfc0c91b1bfdaf6ffa1dbeb0..e936c97f5a5a88deb19379370e9a31d70883696f 100644 --- a/src/bin/auth/query.cc +++ b/src/bin/auth/query.cc @@ -141,15 +141,70 @@ Query::process() const { // Found a zone which is the nearest ancestor to QNAME, set the AA bit response_.setHeaderFlag(Message::HEADERFLAG_AA); + response_.setRcode(Rcode::NOERROR()); while (keep_doing) { keep_doing = false; std::auto_ptr target(qtype_is_any ? new RRsetList : NULL); - Zone::FindResult db_result = - result.zone->find(qname_, qtype_, target.get()); + const Zone::FindResult db_result(result.zone->find(qname_, qtype_, + target.get())); switch (db_result.code) { + case Zone::DNAME: { + // First, put the dname into the answer + response_.addRRset(Message::SECTION_ANSWER, + boost::const_pointer_cast(db_result.rrset)); + /* + * Empty DNAME should never get in, as it is impossible to + * create one in master file. + * + * FIXME: Other way to prevent this should be done + */ + assert(db_result.rrset->getRdataCount() > 0); + // Get the data of DNAME + const rdata::generic::DNAME& dname( + dynamic_cast( + db_result.rrset->getRdataIterator()->getCurrent())); + // The yet unmatched prefix dname + const Name prefix(qname_.split(0, qname_.getLabelCount() - + db_result.rrset->getName().getLabelCount())); + // If we put it together, will it be too long? + // (The prefix contains trailing ., which will be removed + if (prefix.getLength() - Name::ROOT_NAME().getLength() + + dname.getDname().getLength() > Name::MAX_WIRE) { + /* + * In case the synthesized name is too long, section 4.1 + * of RFC 2672 mandates we return YXDOMAIN. + */ + response_.setRcode(Rcode::YXDOMAIN()); + return; + } + // The new CNAME we are creating (it will be unsigned even + // with DNSSEC, the DNAME is signed and it can be validated + // by that) + RRsetPtr cname(new RRset(qname_, db_result.rrset->getClass(), + RRType::CNAME(), db_result.rrset->getTTL())); + // Construct the new target by replacing the end + cname->addRdata(rdata::generic::CNAME(qname_.split(0, + qname_.getLabelCount() - + db_result.rrset->getName().getLabelCount()). + concatenate(dname.getDname()))); + response_.addRRset(Message::SECTION_ANSWER, cname); + break; + } + case Zone::CNAME: + /* + * We don't do chaining yet. Therefore handling a CNAME is + * mostly the same as handling SUCCESS, but we didn't get + * what we expected. It means no exceptions in ANY or NS + * on the origin (though CNAME in origin is probably + * forbidden anyway). + * + * So, just put it there. + */ + response_.addRRset(Message::SECTION_ANSWER, + boost::const_pointer_cast(db_result.rrset)); + break; case Zone::SUCCESS: - response_.setRcode(Rcode::NOERROR()); if (qtype_is_any) { // If quety type is ANY, insert all RRs under the domain // into answer section. @@ -167,6 +222,7 @@ Query::process() const { // and AAAA/A RRS of each of the NS RDATA into the additional // section. if (qname_ != result.zone->getOrigin() || + db_result.code != Zone::SUCCESS || (qtype_ != RRType::NS() && !qtype_is_any)) { getAuthAdditional(*result.zone); @@ -174,7 +230,6 @@ Query::process() const { break; case Zone::DELEGATION: response_.setHeaderFlag(Message::HEADERFLAG_AA, false); - response_.setRcode(Rcode::NOERROR()); response_.addRRset(Message::SECTION_AUTHORITY, boost::const_pointer_cast(db_result.rrset)); getAdditional(*result.zone, *db_result.rrset); @@ -186,13 +241,8 @@ Query::process() const { break; case Zone::NXRRSET: // Just empty answer with SOA in authority section - response_.setRcode(Rcode::NOERROR()); putSOA(*result.zone); break; - case Zone::CNAME: - case Zone::DNAME: - // TODO : replace qname, continue lookup - break; } } } diff --git a/src/bin/auth/tests/Makefile.am b/src/bin/auth/tests/Makefile.am index 72984982b036620cba2e0978f453b1616b5dc4e9..def99b05641a8bc2f655550c467a300c8140d9e1 100644 --- a/src/bin/auth/tests/Makefile.am +++ b/src/bin/auth/tests/Makefile.am @@ -44,6 +44,8 @@ run_unittests_LDADD += $(top_builddir)/src/lib/config/libcfgclient.la run_unittests_LDADD += $(top_builddir)/src/lib/cc/libcc.la run_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libexceptions.la run_unittests_LDADD += $(top_builddir)/src/lib/xfr/libxfr.la +run_unittests_LDADD += $(top_builddir)/src/lib/log/liblog.la +run_unittests_LDADD += $(top_builddir)/src/lib/nsas/libnsas.la endif noinst_PROGRAMS = $(TESTS) diff --git a/src/bin/auth/tests/command_unittest.cc b/src/bin/auth/tests/command_unittest.cc index 0ba5e861f0f26fd5cab8a585d398dd6542a6b934..f788d9e4919e9e1cb0ab03b1ddebfd5b619e5703 100644 --- a/src/bin/auth/tests/command_unittest.cc +++ b/src/bin/auth/tests/command_unittest.cc @@ -98,7 +98,7 @@ AuthConmmandTest::stopServer() { TEST_F(AuthConmmandTest, shutdown) { asiolink::IntervalTimer itimer(server.getIOService()); - itimer.setupTimer(boost::bind(&AuthConmmandTest::stopServer, this), 1); + itimer.setup(boost::bind(&AuthConmmandTest::stopServer, this), 1); server.getIOService().run(); EXPECT_EQ(0, rcode); } diff --git a/src/bin/auth/tests/config_unittest.cc b/src/bin/auth/tests/config_unittest.cc index 0e0aee9199fce93c1214a8f0113361a4e81f3895..b8b379e0c56ca12a4835a80b03caf726bd0926fd 100644 --- a/src/bin/auth/tests/config_unittest.cc +++ b/src/bin/auth/tests/config_unittest.cc @@ -365,5 +365,9 @@ TEST_F(StatisticsIntervalConfigTest, badInterval) { EXPECT_THROW(parser->build(Element::fromJSON("2.5")), isc::data::TypeError); EXPECT_THROW(parser->build(Element::fromJSON("-1")), AuthConfigError); + // bounds check: interval value must be equal to or shorter than + // 86400 seconds (1 day) + EXPECT_NO_THROW(parser->build(Element::fromJSON("86400"))); + EXPECT_THROW(parser->build(Element::fromJSON("86401")), AuthConfigError); } } diff --git a/src/bin/auth/tests/query_unittest.cc b/src/bin/auth/tests/query_unittest.cc index 6ea0ac379b4230b75cc386792c9652f682167a08..2d3cf03a621ea6d00a3f453d9898b802ef2ba66a 100644 --- a/src/bin/auth/tests/query_unittest.cc +++ b/src/bin/auth/tests/query_unittest.cc @@ -68,9 +68,26 @@ const char* const mx_txt = "mx.example.com. 3600 IN MX 20 mailer.example.org.\n" "mx.example.com. 3600 IN MX 30 mx.delegation.example.com.\n"; const char* const www_a_txt = "www.example.com. 3600 IN A 192.0.2.80\n"; +const char* const cname_txt = + "cname.example.com. 3600 IN CNAME www.example.com.\n"; +const char* const cname_nxdom_txt = + "cnamenxdom.example.com. 3600 IN CNAME nxdomain.example.com.\n"; +// CNAME Leading out of zone +const char* const cname_out_txt = + "cnameout.example.com. 3600 IN CNAME www.example.org.\n"; +// The DNAME to do tests against +const char* const dname_txt = + "dname.example.com. 3600 IN DNAME " + "somethinglong.dnametarget.example.com.\n"; +// Some data at the dname node (allowed by RFC 2672) +const char* const dname_a_txt = + "dname.example.com. 3600 IN A 192.0.2.5\n"; +// This is not inside the zone, this is created at runtime +const char* const synthetized_cname_txt = + "www.dname.example.com. 3600 IN CNAME " + "www.somethinglong.dnametarget.example.com.\n"; // The rest of data won't be referenced from the test cases. const char* const other_zone_rrs = - "cname.example.com. 3600 IN CNAME www.example.com.\n" "cnamemailer.example.com. 3600 IN CNAME www.example.com.\n" "cnamemx.example.com. 3600 IN MX 10 cnamemailer.example.com.\n" "mx.delegation.example.com. 3600 IN A 192.0.2.100\n"; @@ -82,20 +99,25 @@ const char* const other_zone_rrs = // behavior. // For simplicity, most names are assumed to be "in zone"; there's only // one zone cut at the point of name "delegation.example.com". -// It doesn't handle empty non terminal nodes (if we need to test such cases -// find() should have specialized code for it). +// Another special name is "dname.example.com". Query names under this name +// will result in DNAME. +// This mock zone doesn't handle empty non terminal nodes (if we need to test +// such cases find() should have specialized code for it). class MockZone : public Zone { public: MockZone() : origin_(Name("example.com")), delegation_name_("delegation.example.com"), + dname_name_("dname.example.com"), has_SOA_(true), has_apex_NS_(true), rrclass_(RRClass::IN()) { stringstream zone_stream; zone_stream << soa_txt << zone_ns_txt << ns_addrs_txt << - delegation_txt << mx_txt << www_a_txt << other_zone_rrs; + delegation_txt << mx_txt << www_a_txt << cname_txt << + cname_nxdom_txt << cname_out_txt << dname_txt << dname_a_txt << + other_zone_rrs; masterLoad(zone_stream, origin_, rrclass_, boost::bind(&MockZone::loadRRset, this, _1)); @@ -124,14 +146,20 @@ private: if (rrset->getName() == delegation_name_ && rrset->getType() == RRType::NS()) { delegation_rrset_ = rrset; + } else if (rrset->getName() == dname_name_ && + rrset->getType() == RRType::DNAME()) { + dname_rrset_ = rrset; } } const Name origin_; + // Names where we delegate somewhere else const Name delegation_name_; + const Name dname_name_; bool has_SOA_; bool has_apex_NS_; ConstRRsetPtr delegation_rrset_; + ConstRRsetPtr dname_rrset_; const RRClass rrclass_; }; @@ -153,6 +181,10 @@ MockZone::find(const Name& name, const RRType& type, name.compare(delegation_name_).getRelation() == NameComparisonResult::SUBDOMAIN)) { return (FindResult(DELEGATION, delegation_rrset_)); + // And under DNAME + } else if (name.compare(dname_name_).getRelation() == + NameComparisonResult::SUBDOMAIN) { + return (FindResult(DNAME, dname_rrset_)); } // normal cases. names are searched for only per exact-match basis @@ -169,8 +201,7 @@ MockZone::find(const Name& name, const RRType& type, // If not found but we have a target, fill it with all RRsets here if (!found_domain->second.empty() && target != NULL) { for (found_rrset = found_domain->second.begin(); - found_rrset != found_domain->second.end(); found_rrset++) - { + found_rrset != found_domain->second.end(); found_rrset++) { // Insert RRs under the domain name into target target->addRRset( boost::const_pointer_cast(found_rrset->second)); @@ -422,4 +453,226 @@ TEST_F(QueryTest, MXAlias) { responseCheck(response, Rcode::NOERROR(), AA_FLAG, 1, 3, 3, NULL, NULL, ns_addrs_txt); } + +/* + * Tests encountering a cname. + * + * There are tests leading to successful answers, NXRRSET, NXDOMAIN and + * out of the zone. + * + * TODO: We currently don't do chaining, so only the CNAME itself should be + * returned. + */ +TEST_F(QueryTest, CNAME) { + Query(memory_datasrc, Name("cname.example.com"), RRType::A(), + response).process(); + + responseCheck(response, Rcode::NOERROR(), AA_FLAG, 1, 0, 0, + cname_txt, NULL, NULL); +} + +TEST_F(QueryTest, explicitCNAME) { + // same owner name as the CNAME test but explicitly query for CNAME RR. + // expect the same response as we don't provide a full chain yet. + Query(memory_datasrc, Name("cname.example.com"), RRType::CNAME(), + response).process(); + + responseCheck(response, Rcode::NOERROR(), AA_FLAG, 1, 3, 3, + cname_txt, zone_ns_txt, ns_addrs_txt); +} + +TEST_F(QueryTest, CNAME_NX_RRSET) { + // Leads to www.example.com, it doesn't have TXT + // note: with chaining, what should be expected is not trivial: + // BIND 9 returns the CNAME in answer and SOA in authority, no additional. + // NSD returns the CNAME, NS in authority, A/AAAA for NS in additional. + Query(memory_datasrc, Name("cname.example.com"), RRType::TXT(), + response).process(); + + responseCheck(response, Rcode::NOERROR(), AA_FLAG, 1, 0, 0, + cname_txt, NULL, NULL); +} + +TEST_F(QueryTest, explicitCNAME_NX_RRSET) { + // same owner name as the NXRRSET test but explicitly query for CNAME RR. + Query(memory_datasrc, Name("cname.example.com"), RRType::CNAME(), + response).process(); + + responseCheck(response, Rcode::NOERROR(), AA_FLAG, 1, 3, 3, + cname_txt, zone_ns_txt, ns_addrs_txt); +} + +TEST_F(QueryTest, CNAME_NX_DOMAIN) { + // Leads to nxdomain.example.com + // note: with chaining, what should be expected is not trivial: + // BIND 9 returns the CNAME in answer and SOA in authority, no additional, + // RCODE being NXDOMAIN. + // NSD returns the CNAME, NS in authority, A/AAAA for NS in additional, + // RCODE being NOERROR. + Query(memory_datasrc, Name("cnamenxdom.example.com"), RRType::A(), + response).process(); + + responseCheck(response, Rcode::NOERROR(), AA_FLAG, 1, 0, 0, + cname_nxdom_txt, NULL, NULL); +} + +TEST_F(QueryTest, explicitCNAME_NX_DOMAIN) { + // same owner name as the NXDOMAIN test but explicitly query for CNAME RR. + Query(memory_datasrc, Name("cnamenxdom.example.com"), RRType::CNAME(), + response).process(); + + responseCheck(response, Rcode::NOERROR(), AA_FLAG, 1, 3, 3, + cname_nxdom_txt, zone_ns_txt, ns_addrs_txt); +} + +TEST_F(QueryTest, CNAME_OUT) { + /* + * This leads out of zone. This should have only the CNAME even + * when we do chaining. + * + * TODO: We should be able to have two zones in the mock data source. + * Then the same test should be done with .org included there and + * see what it does (depends on what we want to do) + */ + Query(memory_datasrc, Name("cnameout.example.com"), RRType::A(), + response).process(); + + responseCheck(response, Rcode::NOERROR(), AA_FLAG, 1, 0, 0, + cname_out_txt, NULL, NULL); +} + +TEST_F(QueryTest, explicitCNAME_OUT) { + // same owner name as the OUT test but explicitly query for CNAME RR. + Query(memory_datasrc, Name("cnameout.example.com"), RRType::CNAME(), + response).process(); + + responseCheck(response, Rcode::NOERROR(), AA_FLAG, 1, 3, 3, + cname_out_txt, zone_ns_txt, ns_addrs_txt); +} + +/* + * Test a query under a domain with DNAME. We should get a synthetized CNAME + * as well as the DNAME. + * + * TODO: Once we have CNAME chaining, check it works with synthetized CNAMEs + * as well. This includes tests pointing inside the zone, outside the zone, + * pointing to NXRRSET and NXDOMAIN cases (similarly as with CNAME). + */ +TEST_F(QueryTest, DNAME) { + Query(memory_datasrc, Name("www.dname.example.com"), RRType::A(), + response).process(); + + responseCheck(response, Rcode::NOERROR(), AA_FLAG, 2, 0, 0, + (string(dname_txt) + synthetized_cname_txt).c_str(), + NULL, NULL); +} + +/* + * Ask an ANY query below a DNAME. Should return the DNAME and synthetized + * CNAME. + * + * ANY is handled specially sometimes. We check it is not the case with + * DNAME. + */ +TEST_F(QueryTest, DNAME_ANY) { + Query(memory_datasrc, Name("www.dname.example.com"), RRType::ANY(), + response).process(); + + responseCheck(response, Rcode::NOERROR(), AA_FLAG, 2, 0, 0, + (string(dname_txt) + synthetized_cname_txt).c_str(), NULL, NULL); +} + +// Test when we ask for DNAME explicitly, it does no synthetizing. +TEST_F(QueryTest, explicitDNAME) { + Query(memory_datasrc, Name("dname.example.com"), RRType::DNAME(), + response).process(); + + responseCheck(response, Rcode::NOERROR(), AA_FLAG, 1, 3, 3, + dname_txt, zone_ns_txt, ns_addrs_txt); +} + +/* + * Request a RRset at the domain with DNAME. It should not synthetize + * the CNAME, it should return the RRset. + */ +TEST_F(QueryTest, DNAME_A) { + Query(memory_datasrc, Name("dname.example.com"), RRType::A(), + response).process(); + + responseCheck(response, Rcode::NOERROR(), AA_FLAG, 1, 3, 3, + dname_a_txt, zone_ns_txt, ns_addrs_txt); +} + +/* + * Request a RRset at the domain with DNAME that is not there (NXRRSET). + * It should not synthetize the CNAME. + */ +TEST_F(QueryTest, DNAME_NX_RRSET) { + EXPECT_NO_THROW(Query(memory_datasrc, Name("dname.example.com"), + RRType::TXT(), response).process()); + + responseCheck(response, Rcode::NOERROR(), AA_FLAG, 0, 1, 0, + NULL, soa_txt, NULL, mock_zone->getOrigin()); +} + +/* + * Constructing the CNAME will result in a name that is too long. This, + * however, should not throw (and crash the server), but respond with + * YXDOMAIN. + */ +TEST_F(QueryTest, LongDNAME) { + // A name that is as long as it can be + Name longname( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa." + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa." + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa." + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa." + "dname.example.com."); + EXPECT_NO_THROW(Query(memory_datasrc, longname, RRType::A(), + response).process()); + + responseCheck(response, Rcode::YXDOMAIN(), AA_FLAG, 1, 0, 0, + dname_txt, NULL, NULL); +} + +/* + * Constructing the CNAME will result in a name of maximal length. + * This tests that we don't reject valid one by some kind of off by + * one mistake. + */ +TEST_F(QueryTest, MaxLenDNAME) { + Name longname( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa." + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa." + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa." + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa." + "dname.example.com."); + EXPECT_NO_THROW(Query(memory_datasrc, longname, RRType::A(), + response).process()); + + // Check the answer is OK + responseCheck(response, Rcode::NOERROR(), AA_FLAG, 2, 0, 0, + NULL, NULL, NULL); + + // Check that the CNAME has the maximal length. + bool ok(false); + for (RRsetIterator i(response.beginSection(Message::SECTION_ANSWER)); + i != response.endSection(Message::SECTION_ANSWER); ++ i) { + if ((*i)->getType() == RRType::CNAME()) { + ok = true; + RdataIteratorPtr ci((*i)->getRdataIterator()); + ASSERT_FALSE(ci->isLast()) << "The CNAME is empty"; + /* + * Does anybody have a clue why, if the Name::MAX_WIRE is put + * directly inside ASSERT_EQ, it fails to link and complains + * it is unresolved external? + */ + const size_t max_len(Name::MAX_WIRE); + ASSERT_EQ(max_len, dynamic_cast( + ci->getCurrent()).getCname().getLength()); + } + } + EXPECT_TRUE(ok) << "The synthetized CNAME not found"; +} + } diff --git a/src/bin/bind10/bind10.py.in b/src/bin/bind10/bind10.py.in old mode 100644 new mode 100755 index 7594b77e1c864b9ffeecca792d330c2e9bdc3d00..5685c0547f6ed94df26b9d5c5173265ee796aa19 --- a/src/bin/bind10/bind10.py.in +++ b/src/bin/bind10/bind10.py.in @@ -195,8 +195,7 @@ class BoB: """Boss of BIND class.""" def __init__(self, msgq_socket_file=None, dns_port=5300, address=None, - forward=None, nocache=False, verbose=False, setuid=None, - username=None): + nocache=False, verbose=False, setuid=None, username=None): """ Initialize the Boss of BIND. This is a singleton (only one can run). @@ -206,11 +205,6 @@ class BoB: """ self.address = address self.dns_port = dns_port - self.forward = forward - if forward: - self.resolver = True - else: - self.resolver = False self.cc_session = None self.ccs = None self.cfg_start_auth = True @@ -320,8 +314,8 @@ class BoB: sys.stdout.write("\n") # The next few methods start the individual processes of BIND-10. They - # are called via start_all_process(). If any fail, an exception is raised - # which is caught by the caller of start_all_processes(); this kills + # are called via start_all_processes(). If any fail, an exception is + # raised which is caught by the caller of start_all_processes(); this kills # processes started up to that point before terminating the program. def start_msgq(self, c_channel_env): @@ -422,26 +416,19 @@ class BoB: """ Start the Authoritative server """ - # XXX: this must be read from the configuration manager in the future - if self.resolver: - dns_prog = 'b10-resolver' - else: - dns_prog = 'b10-auth' - dnsargs = [dns_prog] - if not self.resolver: - # The resolver uses configuration manager for these - dnsargs += ['-p', str(self.dns_port)] - if self.address: - dnsargs += ['-a', str(self.address)] - if self.nocache: - dnsargs += ['-n'] + authargs = ['b10-auth'] + authargs += ['-p', str(self.dns_port)] + if self.address: + authargs += ['-a', str(self.address)] + if self.nocache: + authargs += ['-n'] if self.uid: - dnsargs += ['-u', str(self.uid)] + authargs += ['-u', str(self.uid)] if self.verbose: - dnsargs += ['-v'] + authargs += ['-v'] # ... and start - self.start_process("b10-auth", dnsargs, c_channel_env, + self.start_process("b10-auth", authargs, c_channel_env, self.dns_port, self.address) def start_resolver(self, c_channel_env): @@ -739,8 +726,6 @@ def check_addr(option, opt_str, value, parser): try: if opt_str in ['-a', '--address']: parser.values.address = isc.net.parse.addr_parse(value) - elif opt_str in ['-f', '--forward']: - parser.values.forward = isc.net.parse.addr_parse(value) else: raise OptionValueError("Unknown option " + opt_str) except ValueError: @@ -761,9 +746,6 @@ def main(): parser.add_option("-a", "--address", dest="address", type="string", action="callback", callback=check_addr, default=None, help="address the DNS server will use (default: listen on all addresses)") - parser.add_option("-f", "--forward", dest="forward", type="string", - action="callback", callback=check_addr, default=None, - help="nameserver to which DNS queries should be forwarded") parser.add_option("-m", "--msgq-socket-file", dest="msgq_socket_file", type="string", default=None, help="UNIX domain socket file the b10-msgq daemon will use") @@ -833,8 +815,8 @@ def main(): # Go bob! boss_of_bind = BoB(options.msgq_socket_file, options.dns_port, - options.address, options.forward, options.nocache, - options.verbose, setuid, username) + options.address, options.nocache, options.verbose, + setuid, username) startup_result = boss_of_bind.startup() if startup_result: sys.stderr.write("[bind10] Error on startup: %s\n" % startup_result) diff --git a/src/bin/bind10/bind10.xml b/src/bin/bind10/bind10.xml index e672a42de4859731fb220e3c953b8255721636ee..dfc8acf229a3e9873323f3e590c8f8967c2de433 100644 --- a/src/bin/bind10/bind10.xml +++ b/src/bin/bind10/bind10.xml @@ -131,7 +131,7 @@ daemon to listen on. The default is 5300. - The Y1 prototype release uses a non-default + This prototype release uses a non-default port for domain service. diff --git a/src/bin/bindctl/bindcmd.py b/src/bin/bindctl/bindcmd.py index fb6a89235f6d802c1233c1d114408aea0f5fd40d..683dda9e4c1a53b57bb6fba5649ce2b5986ef63c 100644 --- a/src/bin/bindctl/bindcmd.py +++ b/src/bin/bindctl/bindcmd.py @@ -51,7 +51,6 @@ except ImportError: my_readline = sys.stdin.readline CSV_FILE_NAME = 'default_user.csv' -FAIL_TO_CONNECT_WITH_CMDCTL = "Fail to connect with b10-cmdctl module, is it running?" CONFIG_MODULE_NAME = 'config' CONST_BINDCTL_HELP = """ usage: [param1 = value1 [, param2 = value2]] @@ -92,10 +91,13 @@ class BindCmdInterpreter(Cmd): Cmd.__init__(self) self.location = "" self.prompt_end = '> ' - self.prompt = self.prompt_end + if sys.stdin.isatty(): + self.prompt = self.prompt_end + else: + self.prompt = "" self.ruler = '-' self.modules = OrderedDict() - self.add_module_info(ModuleInfo("help", desc = "Get help for bindctl")) + self.add_module_info(ModuleInfo("help", desc = "Get help for bindctl.")) self.server_port = server_port self.conn = ValidatedHTTPSConnection(self.server_port, ca_certs=pem_file) @@ -119,8 +121,8 @@ class BindCmdInterpreter(Cmd): self.cmdloop() except FailToLogin as err: - print(err) - print(FAIL_TO_CONNECT_WITH_CMDCTL) + # error already printed when this was raised, ignoring + pass except KeyboardInterrupt: print('\nExit from bindctl') @@ -270,8 +272,10 @@ class BindCmdInterpreter(Cmd): return line def postcmd(self, stop, line): - '''Update the prompt after every command''' - self.prompt = self.location + self.prompt_end + '''Update the prompt after every command, but only if we + have a tty as output''' + if sys.stdin.isatty(): + self.prompt = self.location + self.prompt_end return stop def _prepare_module_commands(self, module_spec): @@ -375,7 +379,14 @@ class BindCmdInterpreter(Cmd): if cmd.command == "help" or ("help" in cmd.params.keys()): self._handle_help(cmd) elif cmd.module == CONFIG_MODULE_NAME: - self.apply_config_cmd(cmd) + try: + self.apply_config_cmd(cmd) + except isc.cc.data.DataTypeError as dte: + print("Error: " + str(dte)) + except isc.cc.data.DataNotFoundError as dnfe: + print("Error: " + str(dnfe)) + except KeyError as ke: + print("Error: missing " + str(ke)) else: self.apply_cmd(cmd) @@ -396,9 +407,24 @@ class BindCmdInterpreter(Cmd): def do_help(self, name): print(CONST_BINDCTL_HELP) - for k in self.modules.keys(): - print("\t", self.modules[k]) - + for k in self.modules.values(): + n = k.get_name() + if len(n) >= CONST_BINDCTL_HELP_INDENT_WIDTH: + print(" %s" % n) + print(textwrap.fill(k.get_desc(), + initial_indent=" ", + subsequent_indent=" " + + " " * CONST_BINDCTL_HELP_INDENT_WIDTH, + width=70)) + else: + print(textwrap.fill("%s%s%s" % + (k.get_name(), + " "*(CONST_BINDCTL_HELP_INDENT_WIDTH - len(k.get_name())), + k.get_desc()), + initial_indent=" ", + subsequent_indent=" " + + " " * CONST_BINDCTL_HELP_INDENT_WIDTH, + width=70)) def onecmd(self, line): if line == 'EOF' or line.lower() == "quit": @@ -411,7 +437,19 @@ class BindCmdInterpreter(Cmd): Cmd.onecmd(self, line) def remove_prefix(self, list, prefix): - return [(val[len(prefix):]) for val in list] + """Removes the prefix already entered, and all elements from the + list that don't match it""" + if prefix.startswith('/'): + prefix = prefix[1:] + + new_list = [] + for val in list: + if val.startswith(prefix): + new_val = val[len(prefix):] + if new_val.startswith("/"): + new_val = new_val[1:] + new_list.append(new_val) + return new_list def complete(self, text, state): if 0 == state: @@ -502,8 +540,7 @@ class BindCmdInterpreter(Cmd): self._validate_cmd(cmd) self._handle_cmd(cmd) except (IOError, http.client.HTTPException) as err: - print('Error!', err) - print(FAIL_TO_CONNECT_WITH_CMDCTL) + print('Error: ', err) except BindCtlException as err: print("Error! ", err) self._print_correct_usage(err) @@ -541,87 +578,115 @@ class BindCmdInterpreter(Cmd): Raises a KeyError if the command was not complete ''' identifier = self.location - try: - if 'identifier' in cmd.params: - if not identifier.endswith("/"): - identifier += "/" - if cmd.params['identifier'].startswith("/"): - identifier = cmd.params['identifier'] - else: - identifier += cmd.params['identifier'] - - # Check if the module is known; for unknown modules - # we currently deny setting preferences, as we have - # no way yet to determine if they are ok. - module_name = identifier.split('/')[1] - if self.config_data is None or \ - not self.config_data.have_specification(module_name): - print("Error: Module '" + module_name + "' unknown or not running") - return + if 'identifier' in cmd.params: + if not identifier.endswith("/"): + identifier += "/" + if cmd.params['identifier'].startswith("/"): + identifier = cmd.params['identifier'] + else: + if cmd.params['identifier'].startswith('['): + identifier = identifier[:-1] + identifier += cmd.params['identifier'] + + # Check if the module is known; for unknown modules + # we currently deny setting preferences, as we have + # no way yet to determine if they are ok. + module_name = identifier.split('/')[1] + if module_name != "" and (self.config_data is None or \ + not self.config_data.have_specification(module_name)): + print("Error: Module '" + module_name + "' unknown or not running") + return - if cmd.command == "show": - values = self.config_data.get_value_maps(identifier) - for value_map in values: - line = value_map['name'] - if value_map['type'] in [ 'module', 'map', 'list' ]: - line += "/" - else: - line += ":\t" + json.dumps(value_map['value']) - line += "\t" + value_map['type'] - line += "\t" - if value_map['default']: - line += "(default)" - if value_map['modified']: - line += "(modified)" - print(line) - elif cmd.command == "add": - self.config_data.add_value(identifier, cmd.params['value']) - elif cmd.command == "remove": - if 'value' in cmd.params: - self.config_data.remove_value(identifier, cmd.params['value']) + if cmd.command == "show": + # check if we have the 'all' argument + show_all = False + if 'argument' in cmd.params: + if cmd.params['argument'] == 'all': + show_all = True + elif 'identifier' not in cmd.params: + # no 'all', no identifier, assume this is the + #identifier + identifier += cmd.params['argument'] else: - self.config_data.remove_value(identifier, None) - elif cmd.command == "set": - if 'identifier' not in cmd.params: - print("Error: missing identifier or value") + print("Error: unknown argument " + cmd.params['argument'] + ", or multiple identifiers given") + return + values = self.config_data.get_value_maps(identifier, show_all) + for value_map in values: + line = value_map['name'] + if value_map['type'] in [ 'module', 'map' ]: + line += "/" + elif value_map['type'] == 'list' \ + and value_map['value'] != []: + # do not print content of non-empty lists if + # we have more data to show + line += "/" else: - parsed_value = None - try: - parsed_value = json.loads(cmd.params['value']) - except Exception as exc: - # ok could be an unquoted string, interpret as such - parsed_value = cmd.params['value'] - self.config_data.set_value(identifier, parsed_value) - elif cmd.command == "unset": - self.config_data.unset(identifier) - elif cmd.command == "revert": - self.config_data.clear_local_changes() - elif cmd.command == "commit": - self.config_data.commit() - elif cmd.command == "diff": - print(self.config_data.get_local_changes()); - elif cmd.command == "go": - self.go(identifier) - except isc.cc.data.DataTypeError as dte: - print("Error: " + str(dte)) - except isc.cc.data.DataNotFoundError as dnfe: - print("Error: " + identifier + " not found") - except KeyError as ke: - print("Error: missing " + str(ke)) - raise ke + line += "\t" + json.dumps(value_map['value']) + line += "\t" + value_map['type'] + line += "\t" + if value_map['default']: + line += "(default)" + if value_map['modified']: + line += "(modified)" + print(line) + elif cmd.command == "show_json": + if identifier == "": + print("Need at least the module to show the configuration in JSON format") + else: + data, default = self.config_data.get_value(identifier) + print(json.dumps(data)) + elif cmd.command == "add": + if 'value' in cmd.params: + self.config_data.add_value(identifier, cmd.params['value']) + else: + self.config_data.add_value(identifier) + elif cmd.command == "remove": + if 'value' in cmd.params: + self.config_data.remove_value(identifier, cmd.params['value']) + else: + self.config_data.remove_value(identifier, None) + elif cmd.command == "set": + if 'identifier' not in cmd.params: + print("Error: missing identifier or value") + else: + parsed_value = None + try: + parsed_value = json.loads(cmd.params['value']) + except Exception as exc: + # ok could be an unquoted string, interpret as such + parsed_value = cmd.params['value'] + self.config_data.set_value(identifier, parsed_value) + elif cmd.command == "unset": + self.config_data.unset(identifier) + elif cmd.command == "revert": + self.config_data.clear_local_changes() + elif cmd.command == "commit": + self.config_data.commit() + elif cmd.command == "diff": + print(self.config_data.get_local_changes()); + elif cmd.command == "go": + self.go(identifier) def go(self, identifier): '''Handles the config go command, change the 'current' location - within the configuration tree''' - # this is just to see if it exists - self.config_data.get_value(identifier) - # some sanitizing - identifier = identifier.replace("//", "/") - if not identifier.startswith("/"): - identifier = "/" + identifier - if identifier.endswith("/"): - identifier = identifier[:-1] - self.location = identifier + within the configuration tree. '..' will be interpreted as + 'up one level'.''' + id_parts = isc.cc.data.split_identifier(identifier) + + new_location = "" + for id_part in id_parts: + if (id_part == ".."): + # go 'up' one level + new_location, a, b = new_location.rpartition("/") + else: + new_location += "/" + id_part + # check if exists, if not, revert and error + v,d = self.config_data.get_value(new_location) + if v is None: + print("Error: " + identifier + " not found") + return + + self.location = new_location def apply_cmd(self, cmd): '''Handles a general module command''' diff --git a/src/bin/bindctl/bindctl-source.py.in b/src/bin/bindctl/bindctl-source.py.in index 83059d243f5d49e5774053e79b834f02060e9d4c..2e9d513c0a9087ebb92b18f23c849e172a67ba96 100644 --- a/src/bin/bindctl/bindctl-source.py.in +++ b/src/bin/bindctl/bindctl-source.py.in @@ -33,51 +33,60 @@ isc.util.process.rename() # number, and the overall BIND 10 version number (set in configure.ac). VERSION = "bindctl 20101201 (BIND 10 @PACKAGE_VERSION@)" +DEFAULT_IDENTIFIER_DESC = "The identifier specifies the config item. Child elements are separated with the '/' character. List indices can be specified with '[i]', where i is an integer specifying the index, starting with 0. Examples: 'Boss/start_auth', 'Recurse/listen_on[0]/address'. If no identifier is given, shows the item at the current location." + def prepare_config_commands(tool): '''Prepare fixed commands for local configuration editing''' - module = ModuleInfo(name = CONFIG_MODULE_NAME, desc = "Configuration commands") - cmd = CommandInfo(name = "show", desc = "Show configuration") - param = ParamInfo(name = "identifier", type = "string", optional=True) + module = ModuleInfo(name = CONFIG_MODULE_NAME, desc = "Configuration commands.") + cmd = CommandInfo(name = "show", desc = "Show configuration.") + param = ParamInfo(name = "argument", type = "string", optional=True, desc = "If you specify the argument 'all' (before the identifier), recursively show all child elements for the given identifier.") + cmd.add_param(param) + param = ParamInfo(name = "identifier", type = "string", optional=True, desc = DEFAULT_IDENTIFIER_DESC) + cmd.add_param(param) + module.add_command(cmd) + + cmd = CommandInfo(name = "show_json", desc = "Show full configuration in JSON format.") + param = ParamInfo(name = "identifier", type = "string", optional=True, desc = DEFAULT_IDENTIFIER_DESC) cmd.add_param(param) module.add_command(cmd) - cmd = CommandInfo(name = "add", desc = "Add entry to configuration list") - param = ParamInfo(name = "identifier", type = "string", optional=True) + cmd = CommandInfo(name = "add", desc = "Add an entry to configuration list. If no value is given, a default value is added.") + param = ParamInfo(name = "identifier", type = "string", optional=True, desc = DEFAULT_IDENTIFIER_DESC) cmd.add_param(param) - param = ParamInfo(name = "value", type = "string", optional=False) + param = ParamInfo(name = "value", type = "string", optional=True, desc = "Specifies a value to add to the list. It must be in correct JSON format and complete.") cmd.add_param(param) module.add_command(cmd) - cmd = CommandInfo(name = "remove", desc = "Remove entry from configuration list") - param = ParamInfo(name = "identifier", type = "string", optional=True) + cmd = CommandInfo(name = "remove", desc = "Remove entry from configuration list.") + param = ParamInfo(name = "identifier", type = "string", optional=True, desc = DEFAULT_IDENTIFIER_DESC) cmd.add_param(param) - param = ParamInfo(name = "value", type = "string", optional=True) + param = ParamInfo(name = "value", type = "string", optional=True, desc = "Specifies a value to remove from the list. It must be in correct JSON format and complete.") cmd.add_param(param) module.add_command(cmd) - cmd = CommandInfo(name = "set", desc = "Set a configuration value") - param = ParamInfo(name = "identifier", type = "string", optional=True) + cmd = CommandInfo(name = "set", desc = "Set a configuration value.") + param = ParamInfo(name = "identifier", type = "string", optional=True, desc = DEFAULT_IDENTIFIER_DESC) cmd.add_param(param) - param = ParamInfo(name = "value", type = "string", optional=False) + param = ParamInfo(name = "value", type = "string", optional=False, desc = "Specifies a value to set. It must be in correct JSON format and complete.") cmd.add_param(param) module.add_command(cmd) - cmd = CommandInfo(name = "unset", desc = "Unset a configuration value") - param = ParamInfo(name = "identifier", type = "string", optional=False) + cmd = CommandInfo(name = "unset", desc = "Unset a configuration value (i.e. revert to the default, if any).") + param = ParamInfo(name = "identifier", type = "string", optional=False, desc = DEFAULT_IDENTIFIER_DESC) cmd.add_param(param) module.add_command(cmd) - cmd = CommandInfo(name = "diff", desc = "Show all local changes") + cmd = CommandInfo(name = "diff", desc = "Show all local changes that have not been committed.") module.add_command(cmd) - cmd = CommandInfo(name = "revert", desc = "Revert all local changes") + cmd = CommandInfo(name = "revert", desc = "Revert all local changes.") module.add_command(cmd) - cmd = CommandInfo(name = "commit", desc = "Commit all local changes") + cmd = CommandInfo(name = "commit", desc = "Commit all local changes.") module.add_command(cmd) - cmd = CommandInfo(name = "go", desc = "Go to a specific configuration part") - param = ParamInfo(name = "identifier", type="string", optional=False) + cmd = CommandInfo(name = "go", desc = "Go to a specific configuration part.") + param = ParamInfo(name = "identifier", type="string", optional=False, desc = DEFAULT_IDENTIFIER_DESC) cmd.add_param(param) module.add_command(cmd) @@ -115,15 +124,12 @@ def set_bindctl_options(parser): help = 'PEM formatted server certificate validation chain file') if __name__ == '__main__': - try: - parser = OptionParser(version = VERSION) - set_bindctl_options(parser) - (options, args) = parser.parse_args() - server_addr = options.addr + ':' + str(options.port) - tool = BindCmdInterpreter(server_addr, pem_file=options.cert_chain) - prepare_config_commands(tool) - tool.run() - except Exception as e: - print(e, "\nFailed to connect with b10-cmdctl module, is it running?") + parser = OptionParser(version = VERSION) + set_bindctl_options(parser) + (options, args) = parser.parse_args() + server_addr = options.addr + ':' + str(options.port) + tool = BindCmdInterpreter(server_addr, pem_file=options.cert_chain) + prepare_config_commands(tool) + tool.run() diff --git a/src/bin/bindctl/cmdparse.py b/src/bin/bindctl/cmdparse.py index ab891d7c89bbb00b3e84845ae164e459161ab111..c624cba5d1efcb5f966e8161977d9c98a8acf1ac 100644 --- a/src/bin/bindctl/cmdparse.py +++ b/src/bin/bindctl/cmdparse.py @@ -33,6 +33,7 @@ param_value_str = "(?P[^\'\" ][^, ]+)" param_value_with_quota_str = "[\"\'](?P.+?)(?\s*)(?P,?)(?P.*)$" + PARAM_WITH_QUOTA_PATTERN = re.compile(param_name_str + param_value_with_quota_str + next_params_str) @@ -40,8 +41,58 @@ PARAM_PATTERN = re.compile(param_name_str + param_value_str + next_params_str) # Used for module and command name NAME_PATTERN = re.compile("^\s*(?P[\w]+)(?P\s*)(?P.*)$") +# this removes all whitespace in the given string, except when +# between " quotes +_remove_unquoted_whitespace = \ + lambda text:'"'.join( it if i%2 else ''.join(it.split()) + for i,it in enumerate(text.split('"')) ) + + +def _remove_list_and_map_whitespace(text): + """Returns a string where the whitespace between matching [ and ] + is removed, unless quoted""" + # regular expression aren't really the right tool, since we may have + # nested structures + result = [] + start_pos = 0 + pos = 0 + list_count = 0 + map_count = 0 + cur_start_list_pos = None + cur_start_map_pos = None + for i in text: + if i == '[' and map_count == 0: + if list_count == 0: + result.append(text[start_pos:pos + 1]) + cur_start_list_pos = pos + 1 + list_count = list_count + 1 + elif i == ']' and map_count == 0: + if list_count > 0: + list_count = list_count - 1 + if list_count == 0: + result.append(_remove_unquoted_whitespace(text[cur_start_list_pos:pos + 1])) + start_pos = pos + 1 + if i == '{' and list_count == 0: + if map_count == 0: + result.append(text[start_pos:pos + 1]) + cur_start_map_pos = pos + 1 + map_count = map_count + 1 + elif i == '}' and list_count == 0: + if map_count > 0: + map_count = map_count - 1 + if map_count == 0: + result.append(_remove_unquoted_whitespace(text[cur_start_map_pos:pos + 1])) + start_pos = pos + 1 + + + pos = pos + 1 + if start_pos <= len(text): + result.append(text[start_pos:len(text)]) + return "".join(result) + + class BindCmdParse: - """ This class will parse the command line usr input into three part + """ This class will parse the command line user input into three parts: module name, command, parameters the first two parts are strings and parameter is one hash, parameters part is optional @@ -86,9 +137,12 @@ class BindCmdParse: self._parse_params(param_str) + def _remove_list_whitespace(self, text): + return "" def _parse_params(self, param_text): """convert a=b,c=d into one hash """ + param_text = _remove_list_and_map_whitespace(param_text) # Check parameter name "help" param = NAME_PATTERN.match(param_text) diff --git a/src/bin/bindctl/moduleinfo.py b/src/bin/bindctl/moduleinfo.py index 015ef16cf6e986d4ce2577d7bf9d50de3dce6a45..6e41dcea17ee510d926a881f429667a6f45f35f4 100644 --- a/src/bin/bindctl/moduleinfo.py +++ b/src/bin/bindctl/moduleinfo.py @@ -16,6 +16,8 @@ """This module holds classes representing modules, commands and parameters for use in bindctl""" +import textwrap + try: from collections import OrderedDict except ImportError: @@ -30,6 +32,9 @@ MODULE_NODE_NAME = 'module' COMMAND_NODE_NAME = 'command' PARAM_NODE_NAME = 'param' +# this is used to align the descriptions in help output +CONST_BINDCTL_HELP_INDENT_WIDTH=12 + class ParamInfo: """One parameter of one command. @@ -52,6 +57,12 @@ class ParamInfo: def __str__(self): return str("\t%s \t(%s)" % (self.name, self.type, self.desc)) + def get_name(self): + return "%s " % (self.name, self.type) + + def get_desc(self): + return self.desc + class CommandInfo: """One command which is provided by one bind10 module, it has zero or more parameters @@ -63,13 +74,18 @@ class CommandInfo: self.params = OrderedDict() # Set default parameter "help" self.add_param(ParamInfo("help", - desc = "Get help for command", + desc = "Get help for command.", optional = True)) def __str__(self): return str("%s \t(%s)" % (self.name, self.desc)) - + def get_name(self): + return self.name + + def get_desc(self): + return self.desc; + def add_param(self, paraminfo): """Add a ParamInfo object to this CommandInfo""" self.params[paraminfo.name] = paraminfo @@ -144,22 +160,30 @@ class CommandInfo: del params["help"] if len(params) == 0: - print("\tNo parameters for the command") + print("No parameters for the command") return - print("\n\tMandatory parameters:") + print("\nMandatory parameters:") mandatory_infos = [] for info in params.values(): if not info.is_optional: - print("\t", info) + print(" %s" % info.get_name()) + print(textwrap.fill(info.get_desc(), + initial_indent=" ", + subsequent_indent=" ", + width=70)) mandatory_infos.append(info) optional_infos = [info for info in params.values() if info not in mandatory_infos] if len(optional_infos) > 0: - print("\n\tOptional parameters:") + print("\nOptional parameters:") for info in optional_infos: - print("\t", info) + print(" %s" % info.get_name()) + print(textwrap.fill(info.get_desc(), + initial_indent=" ", + subsequent_indent=" ", + width=70)) class ModuleInfo: @@ -172,11 +196,17 @@ class ModuleInfo: self.desc = desc self.commands = OrderedDict() self.add_command(CommandInfo(name = "help", - desc = "Get help for module")) + desc = "Get help for module.")) def __str__(self): return str("%s \t%s" % (self.name, self.desc)) - + + def get_name(self): + return self.name + + def get_desc(self): + return self.desc + def add_command(self, command_info): """Add a CommandInfo to this ModuleInfo.""" self.commands[command_info.name] = command_info @@ -201,8 +231,24 @@ class ModuleInfo: def module_help(self): """Prints the help info for this module to stdout""" print("Module ", self, "\nAvailable commands:") - for k in self.commands.keys(): - print("\t", self.commands[k]) + for k in self.commands.values(): + n = k.get_name() + if len(n) >= CONST_BINDCTL_HELP_INDENT_WIDTH: + print(" %s" % n) + print(textwrap.fill(k.get_desc(), + initial_indent=" ", + subsequent_indent=" " + + " " * CONST_BINDCTL_HELP_INDENT_WIDTH, + width=70)) + else: + print(textwrap.fill("%s%s%s" % + (k.get_name(), + " "*(CONST_BINDCTL_HELP_INDENT_WIDTH - len(k.get_name())), + k.get_desc()), + initial_indent=" ", + subsequent_indent=" " + + " " * CONST_BINDCTL_HELP_INDENT_WIDTH, + width=70)) def command_help(self, command): """Prints the help info for the command with the given name. diff --git a/src/bin/bindctl/tests/Makefile.am b/src/bin/bindctl/tests/Makefile.am index 5f936443ca81445c458d96316b02a8e6be3216ae..8a7a6237b97bcfc61c9e8bf8f454df4c344e075d 100644 --- a/src/bin/bindctl/tests/Makefile.am +++ b/src/bin/bindctl/tests/Makefile.am @@ -1,5 +1,5 @@ PYCOVERAGE_RUN = @PYCOVERAGE_RUN@ -PYTESTS = bindctl_test.py +PYTESTS = bindctl_test.py cmdparse_test.py EXTRA_DIST = $(PYTESTS) # test using command-line arguments, so use check-local target instead of TESTS diff --git a/src/bin/bindctl/tests/bindctl_test.py b/src/bin/bindctl/tests/bindctl_test.py index 653c908549d20260210d5038dd400d91f1c3f30a..490dd7a09c2042d0e22bd2ae22f36710be04ba89 100644 --- a/src/bin/bindctl/tests/bindctl_test.py +++ b/src/bin/bindctl/tests/bindctl_test.py @@ -17,6 +17,8 @@ import unittest import isc.cc.data import os +from isc.config.config_data import ConfigData, MultiConfigData +from isc.config.module_spec import ModuleSpec from bindctl import cmdparse from bindctl import bindcmd from bindctl.moduleinfo import * @@ -238,11 +240,101 @@ class TestNameSequence(unittest.TestCase): assert self.random_names[i] == module_names[i+1] i = i + 1 - def test_apply_cfg_command(self): +# tine class to fake a UIModuleCCSession, but only the config data +# parts for the next set of tests +class FakeCCSession(MultiConfigData): + def __init__(self): + self._local_changes = {} + self._current_config = {} + self._specifications = {} + self.add_foo_spec() + + def add_foo_spec(self): + spec = { "module_name": "foo", + "config_data": [ + { "item_name": "an_int", + "item_type": "integer", + "item_optional": False, + "item_default": 1 + }, + { "item_name": "a_list", + "item_type": "list", + "item_optional": False, + "item_default": [], + "list_item_spec": + { "item_name": "a_string", + "item_type": "string", + "item_optional": False, + "item_default": "bar" + } + } + ] + } + self.set_specification(ModuleSpec(spec)) + + +class TestConfigCommands(unittest.TestCase): + def setUp(self): + self.tool = bindcmd.BindCmdInterpreter() + mod_info = ModuleInfo(name = "foo") + self.tool.add_module_info(mod_info) + self.tool.config_data = FakeCCSession() + + def test_apply_cfg_command_int(self): self.tool.location = '/' - cmd = cmdparse.BindCmdParse("config set identifier=\"foo/bar\" value=\"5\"") + + self.assertEqual((1, MultiConfigData.DEFAULT), + self.tool.config_data.get_value("/foo/an_int")) + + cmd = cmdparse.BindCmdParse("config set identifier=\"foo/an_int\" value=\"5\"") self.tool.apply_config_cmd(cmd) + self.assertEqual((5, MultiConfigData.LOCAL), + self.tool.config_data.get_value("/foo/an_int")) + + # this should raise a NotFoundError + cmd = cmdparse.BindCmdParse("config set identifier=\"foo/bar\" value=\"[]\"") + self.assertRaises(isc.cc.data.DataNotFoundError, self.tool.apply_config_cmd, cmd) + + # this should raise a TypeError + cmd = cmdparse.BindCmdParse("config set identifier=\"foo/an_int\" value=\"[]\"") + self.assertRaises(isc.cc.data.DataTypeError, self.tool.apply_config_cmd, cmd) + + # this is a very specific one for use with a set of list tests + # to try out the flexibility of the parser (only in the next test) + def clt(self, full_cmd_string, item_value): + cmd = cmdparse.BindCmdParse(full_cmd_string) + self.tool.apply_config_cmd(cmd) + self.assertEqual(([item_value], MultiConfigData.LOCAL), + self.tool.config_data.get_value("/foo/a_list")) + + def test_apply_cfg_command_list(self): + self.tool.location = '/' + + self.assertEqual(([], MultiConfigData.DEFAULT), + self.tool.config_data.get_value("/foo/a_list")) + + self.clt("config set identifier=\"foo/a_list\" value=[\"a\"]", "a") + self.clt("config set identifier=\"foo/a_list\" value =[\"b\"]", "b") + self.clt("config set identifier=\"foo/a_list\" value= [\"c\"]", "c") + self.clt("config set identifier=\"foo/a_list\" value = [\"d\"]", "d") + self.clt("config set identifier =\"foo/a_list\" value=[\"e\"]", "e") + self.clt("config set identifier= \"foo/a_list\" value=[\"f\"]", "f") + self.clt("config set identifier = \"foo/a_list\" value=[\"g\"]", "g") + self.clt("config set identifier = \"foo/a_list\" value = [\"h\"]", "h") + self.clt("config set identifier = \"foo/a_list\" value=[\"i\" ]", "i") + self.clt("config set identifier = \"foo/a_list\" value=[ \"j\"]", "j") + self.clt("config set identifier = \"foo/a_list\" value=[ \"k\" ]", "k") + + # this should raise a TypeError + cmd = cmdparse.BindCmdParse("config set identifier=\"foo/a_list\" value=\"a\"") + self.assertRaises(isc.cc.data.DataTypeError, self.tool.apply_config_cmd, cmd) + + cmd = cmdparse.BindCmdParse("config set identifier=\"foo/a_list\" value=[1]") + self.assertRaises(isc.cc.data.DataTypeError, self.tool.apply_config_cmd, cmd) + + + class FakeBindCmdInterpreter(bindcmd.BindCmdInterpreter): def __init__(self): pass diff --git a/src/bin/bindctl/tests/cmdparse_test.py b/src/bin/bindctl/tests/cmdparse_test.py new file mode 100644 index 0000000000000000000000000000000000000000..9150ed3250146c2010714e3e95897c4a6e663dd9 --- /dev/null +++ b/src/bin/bindctl/tests/cmdparse_test.py @@ -0,0 +1,88 @@ +# Copyright (C) 2009 Internet Systems Consortium. +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM +# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL +# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING +# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +import unittest +from bindctl import cmdparse + +class TestCmdParse(unittest.TestCase): + + def test_remove_unquoted_whitespace(self): + self.assertEqual(cmdparse._remove_unquoted_whitespace("a"), "a") + self.assertEqual(cmdparse._remove_unquoted_whitespace(" a"), "a") + self.assertEqual(cmdparse._remove_unquoted_whitespace("a "), "a") + self.assertEqual(cmdparse._remove_unquoted_whitespace(" a "), "a") + self.assertNotEqual(cmdparse._remove_unquoted_whitespace("a"), "a ") + self.assertNotEqual(cmdparse._remove_unquoted_whitespace(" a"), " a") + self.assertNotEqual(cmdparse._remove_unquoted_whitespace("a "), "a ") + self.assertNotEqual(cmdparse._remove_unquoted_whitespace(" a "), " a ") + self.assertNotEqual(cmdparse._remove_unquoted_whitespace(" a "), "b") + + self.assertEqual(cmdparse._remove_unquoted_whitespace("\"abc\""), "\"abc\"") + self.assertEqual(cmdparse._remove_unquoted_whitespace(" \"abc\""), "\"abc\"") + self.assertEqual(cmdparse._remove_unquoted_whitespace("\"abc\" "), "\"abc\"") + self.assertEqual(cmdparse._remove_unquoted_whitespace(" \"abc\" "), "\"abc\"") + + self.assertEqual(cmdparse._remove_unquoted_whitespace("\" abc\""), "\" abc\"") + self.assertEqual(cmdparse._remove_unquoted_whitespace(" \"a bc\""), "\"a bc\"") + self.assertEqual(cmdparse._remove_unquoted_whitespace("\"ab c\" "), "\"ab c\"") + self.assertEqual(cmdparse._remove_unquoted_whitespace(" \"abc \" "), "\"abc \"") + self.assertEqual(cmdparse._remove_unquoted_whitespace(" \" a b c \" "), "\" a b c \"") + + self.assertEqual(cmdparse._remove_unquoted_whitespace("a\" abc\"a"), "a\" abc\"a") + self.assertEqual(cmdparse._remove_unquoted_whitespace("a \"a bc\"a"), "a\"a bc\"a") + self.assertEqual(cmdparse._remove_unquoted_whitespace("a\"ab c\" a"), "a\"ab c\"a") + self.assertEqual(cmdparse._remove_unquoted_whitespace("a \"abc \" a"), "a\"abc \"a") + self.assertEqual(cmdparse._remove_unquoted_whitespace("a \" a b c \" a"), "a\" a b c \"a") + + # short-hand function to make the set of tests more readable + def rws(self, a, b): + self.assertEqual(cmdparse._remove_list_and_map_whitespace(a), b) + + def test_remove_list_whitespace(self): + self.rws("a", "a") + self.rws(" a ", " a ") + self.rws(" [a] ", " [a] ") + self.rws(" [ a] ", " [a] ") + self.rws(" [ a ] ", " [a] ") + self.rws(" [ a b c ] ", " [abc] ") + self.rws(" [ a \"b c\" ] ", " [a\"b c\"] ") + self.rws("a [ a \"b c\" ] a", "a [a\"b c\"] a") + self.rws("a] [ a \"b c\" ] a", "a] [a\"b c\"] a") + self.rws(" [ a [b c] ] ", " [a[bc]] ") + self.rws(" [ a b][ c d ] ", " [ab][cd] ") + self.rws(" [ a b] [ c d ] ", " [ab] [cd] ") + + self.rws("a", "a") + self.rws(" a ", " a ") + self.rws(" {a} ", " {a} ") + self.rws(" { a} ", " {a} ") + self.rws(" { a } ", " {a} ") + self.rws(" { a b c } ", " {abc} ") + self.rws(" { a \"b c\" } ", " {a\"b c\"} ") + self.rws("a { a \"b c\" } a", "a {a\"b c\"} a") + self.rws("a} { a \"b c\" } a", "a} {a\"b c\"} a") + self.rws(" { a {b c} } ", " {a{bc}} ") + self.rws(" { a b}{ c d } ", " {ab}{cd} ") + self.rws(" { a b} { c d } ", " {ab} {cd} ") + + self.rws(" [ a b]{ c d } ", " [ab]{cd} ") + self.rws(" [ a b{ c d }] ", " [ab{cd}] ") + self.rws(" [ a b{ \"c d\" }] ", " [ab{\"c d\"}] ") + + +if __name__== "__main__": + unittest.main() + diff --git a/src/bin/msgq/msgq.py.in b/src/bin/msgq/msgq.py.in index 1b962e02bf62805becdf1eacff74f67cd91f21e4..8a8362ae70be4a5d759f54dd07492c756bde3bdb 100755 --- a/src/bin/msgq/msgq.py.in +++ b/src/bin/msgq/msgq.py.in @@ -205,7 +205,7 @@ class MsgQ: # TODO: When we have logging, we might want # to add a debug message here that a new connection # was made - self.register_socket(self, newsocket) + self.register_socket(newsocket) def register_socket(self, newsocket): """ diff --git a/src/bin/msgq/tests/msgq_test.py b/src/bin/msgq/tests/msgq_test.py index efae15183c926476575745446013d80f19ffc46b..59fcf41b5f72c84620443270de7c17d87481a11a 100644 --- a/src/bin/msgq/tests/msgq_test.py +++ b/src/bin/msgq/tests/msgq_test.py @@ -132,7 +132,7 @@ class SendNonblock(unittest.TestCase): task() # If we got here, then everything worked well and in time # In that case, we terminate successfully - sys.exit() + sys.exit(0) # needs exit code else: (pid, status) = os.waitpid(task_pid, 0) self.assertEqual(0, status, diff --git a/src/bin/resolver/Makefile.am b/src/bin/resolver/Makefile.am index dc6deed6ee495e8e21cc4f829572affd50e3e375..75d6249ffc51c4998331bef19a744f3a143a5a75 100644 --- a/src/bin/resolver/Makefile.am +++ b/src/bin/resolver/Makefile.am @@ -37,7 +37,7 @@ spec_config.h: spec_config.h.pre BUILT_SOURCES = spec_config.h pkglibexec_PROGRAMS = b10-resolver b10_resolver_SOURCES = resolver.cc resolver.h -b10_resolver_SOURCES += response_classifier.cc response_classifier.h +b10_resolver_SOURCES += response_scrubber.cc response_scrubber.h b10_resolver_SOURCES += $(top_builddir)/src/bin/auth/change_user.h b10_resolver_SOURCES += $(top_builddir)/src/bin/auth/common.h b10_resolver_SOURCES += main.cc @@ -48,6 +48,8 @@ b10_resolver_LDADD += $(top_builddir)/src/lib/exceptions/libexceptions.la b10_resolver_LDADD += $(top_builddir)/src/lib/asiolink/libasiolink.la b10_resolver_LDADD += $(top_builddir)/src/lib/xfr/libxfr.la b10_resolver_LDADD += $(top_builddir)/src/lib/log/liblog.la +b10_resolver_LDADD += $(top_builddir)/src/lib/cache/libcache.la +b10_resolver_LDADD += $(top_builddir)/src/lib/nsas/libnsas.la b10_resolver_LDADD += $(top_builddir)/src/bin/auth/change_user.o b10_resolver_LDFLAGS = -pthread diff --git a/src/bin/resolver/resolver.cc b/src/bin/resolver/resolver.cc index b07916b49ba31cdc4fa715f5974fe6be1572d42b..95a417d285045c4727f9d9d152028c1c90b0c31b 100644 --- a/src/bin/resolver/resolver.cc +++ b/src/bin/resolver/resolver.cc @@ -21,7 +21,6 @@ #include #include -#include #include #include @@ -64,7 +63,9 @@ private: public: ResolverImpl() : config_session_(NULL), - timeout_(2000), + query_timeout_(2000), + client_timeout_(4000), + lookup_timeout_(30000), retries_(3), rec_query_(NULL) {} @@ -76,7 +77,12 @@ public: void querySetup(DNSService& dnss) { assert(!rec_query_); // queryShutdown must be called first dlog("Query setup"); - rec_query_ = new RecursiveQuery(dnss, upstream_, upstream_root_, timeout_, retries_); + rec_query_ = new RecursiveQuery(dnss, upstream_, + upstream_root_, + query_timeout_, + client_timeout_, + lookup_timeout_, + retries_); } void queryShutdown() { @@ -110,7 +116,6 @@ public: void setRootAddresses(const vector& upstream_root, DNSService *dnss) { - queryShutdown(); upstream_root_ = upstream_root; if (dnss) { if (!upstream_root_.empty()) { @@ -122,10 +127,12 @@ public: } else { dlog("No root addresses"); } - querySetup(*dnss); } } + void resolve(const isc::dns::QuestionPtr& question, + const isc::resolve::ResolverInterface::CallbackPtr& callback); + void processNormalQuery(const Question& question, MessagePtr answer_message, OutputBufferPtr buffer, @@ -143,8 +150,13 @@ public: /// Addresses we listen on vector listen_; - /// Time in milliseconds, to timeout - int timeout_; + /// Timeout for outgoing queries in milliseconds + int query_timeout_; + /// Timeout for incoming client queries in milliseconds + int client_timeout_; + /// Timeout for lookup processing in milliseconds + int lookup_timeout_; + /// Number of retries after timeout unsigned retries_; @@ -170,6 +182,8 @@ public: MessagePtr message_; }; + +// TODO: REMOVE, USE isc::resolve::MakeErrorMessage? void makeErrorMessage(MessagePtr message, OutputBufferPtr buffer, const Rcode& rcode) @@ -244,25 +258,16 @@ public: const qid_t qid = query_message->getQid(); const bool rd = query_message->getHeaderFlag(Message::HEADERFLAG_RD); const bool cd = query_message->getHeaderFlag(Message::HEADERFLAG_CD); - const Opcode& opcode = query_message->getOpcode(); - - // Fill in the final details of the answer message + + // The opcode and question section should have already been set, + // fill in the final details of the answer message answer_message->setQid(qid); - answer_message->setOpcode(opcode); answer_message->setHeaderFlag(Message::HEADERFLAG_QR); answer_message->setHeaderFlag(Message::HEADERFLAG_RA); - if (rd) { - answer_message->setHeaderFlag(Message::HEADERFLAG_RD); - } - if (cd) { - answer_message->setHeaderFlag(Message::HEADERFLAG_CD); - } + answer_message->setHeaderFlag(Message::HEADERFLAG_RD, rd); + answer_message->setHeaderFlag(Message::HEADERFLAG_CD, cd); - vector questions; - questions.assign(query_message->beginQuestion(), query_message->endQuestion()); - for_each(questions.begin(), questions.end(), QuestionInserter(answer_message)); - // Now we can clear the buffer and render the new message into it buffer->clear(); MessageRenderer renderer(*buffer); @@ -323,7 +328,6 @@ Resolver::~Resolver() { delete checkin_; delete dns_lookup_; delete dns_answer_; - dlog("Deleting the Resolver",true); } void @@ -341,6 +345,14 @@ Resolver::getConfigSession() const { return (impl_->config_session_); } +void +Resolver::resolve(const isc::dns::QuestionPtr& question, + const isc::resolve::ResolverInterface::CallbackPtr& callback) +{ + impl_->resolve(question, callback); +} + + void Resolver::processMessage(const IOMessage& io_message, MessagePtr query_message, @@ -424,6 +436,13 @@ Resolver::processMessage(const IOMessage& io_message, } } +void +ResolverImpl::resolve(const QuestionPtr& question, + const isc::resolve::ResolverInterface::CallbackPtr& callback) +{ + rec_query_->resolve(question, callback); +} + void ResolverImpl::processNormalQuery(const Question& question, MessagePtr answer_message, @@ -431,7 +450,7 @@ ResolverImpl::processNormalQuery(const Question& question, DNSServer* server) { dlog("Processing normal query"); - rec_query_->sendQuery(question, answer_message, buffer, server); + rec_query_->resolve(question, answer_message, buffer, server); } namespace { @@ -487,16 +506,34 @@ Resolver::updateConfig(ConstElementPtr config) { ConstElementPtr listenAddressesE(config->get("listen_on")); vector listenAddresses(parseAddresses(listenAddressesE)); bool set_timeouts(false); - int timeout = impl_->timeout_; + int qtimeout = impl_->query_timeout_; + int ctimeout = impl_->client_timeout_; + int ltimeout = impl_->lookup_timeout_; unsigned retries = impl_->retries_; - ConstElementPtr timeoutE(config->get("timeout")), - retriesE(config->get("retries")); - if (timeoutE) { + ConstElementPtr qtimeoutE(config->get("timeout_query")), + ctimeoutE(config->get("timeout_client")), + ltimeoutE(config->get("timeout_lookup")), + retriesE(config->get("retries")); + if (qtimeoutE) { // It should be safe to just get it, the config manager should // check for us - timeout = timeoutE->intValue(); - if (timeout < -1) { - isc_throw(BadValue, "Timeout too small"); + qtimeout = qtimeoutE->intValue(); + if (qtimeout < -1) { + isc_throw(BadValue, "Query timeout too small"); + } + set_timeouts = true; + } + if (ctimeoutE) { + ctimeout = ctimeoutE->intValue(); + if (ctimeout < -1) { + isc_throw(BadValue, "Client timeout too small"); + } + set_timeouts = true; + } + if (ltimeoutE) { + ltimeout = ltimeoutE->intValue(); + if (ltimeout < -1) { + isc_throw(BadValue, "Lookup timeout too small"); } set_timeouts = true; } @@ -521,9 +558,10 @@ Resolver::updateConfig(ConstElementPtr config) { } if (rootAddressesE) { setRootAddresses(rootAddresses); + need_query_restart = true; } if (set_timeouts) { - setTimeouts(timeout, retries); + setTimeouts(qtimeout, ctimeout, ltimeout, retries); need_query_restart = true; } @@ -610,15 +648,36 @@ Resolver::setListenAddresses(const vector& addresses) { } void -Resolver::setTimeouts(int timeout, unsigned retries) { - dlog("Setting timeout to " + boost::lexical_cast(timeout) + - " and retry count to " + boost::lexical_cast(retries)); - impl_->timeout_ = timeout; +Resolver::setTimeouts(int query_timeout, int client_timeout, + int lookup_timeout, unsigned retries) { + dlog("Setting query timeout to " + boost::lexical_cast(query_timeout) + + ", client timeout to " + boost::lexical_cast(client_timeout) + + ", lookup timeout to " + boost::lexical_cast(lookup_timeout) + + " and retry count to " + boost::lexical_cast(retries)); + impl_->query_timeout_ = query_timeout; + impl_->client_timeout_ = client_timeout; + impl_->lookup_timeout_ = lookup_timeout; impl_->retries_ = retries; } -pair -Resolver::getTimeouts() const { - return (pair(impl_->timeout_, impl_->retries_)); + +int +Resolver::getQueryTimeout() const { + return impl_->query_timeout_; +} + +int +Resolver::getClientTimeout() const { + return impl_->client_timeout_; +} + +int +Resolver::getLookupTimeout() const { + return impl_->lookup_timeout_; +} + +int +Resolver::getRetries() const { + return impl_->retries_; } vector diff --git a/src/bin/resolver/resolver.h b/src/bin/resolver/resolver.h index cc006c35bb4b97321f53537ef7305fbf6f5afccf..2ae807930cc3419de3f6b566f458cb8c28ebd31b 100644 --- a/src/bin/resolver/resolver.h +++ b/src/bin/resolver/resolver.h @@ -24,6 +24,8 @@ #include +#include + class ResolverImpl; /** @@ -35,7 +37,7 @@ class ResolverImpl; * answer. It doesn't really know about chasing referrals and similar, it * simply plugs the parts that know into the network handling code. */ -class Resolver { +class Resolver : public isc::resolve::ResolverInterface { /// /// \name Constructors, Assignment Operator and Destructor. /// @@ -51,6 +53,10 @@ public: ~Resolver(); //@} + virtual void resolve( + const isc::dns::QuestionPtr& question, + const isc::resolve::ResolverInterface::CallbackPtr& callback); + /// \brief Process an incoming DNS message, then signal 'server' to resume /// /// A DNS query (or other message) has been received by a \c DNSServer @@ -59,7 +65,10 @@ public: /// send the reply. /// /// \param io_message The raw message received - /// \param message Pointer to the \c Message object + /// \param query_message Pointer to the query Message object we + /// received from the client + /// \param answer_message Pointer to the anwer Message object we + /// shall return to the client /// \param buffer Pointer to an \c OutputBuffer for the resposne /// \param server Pointer to the \c DNSServer void processMessage(const asiolink::IOMessage& io_message, @@ -140,11 +149,18 @@ public: * \short Set options related to timeouts. * * This sets the time of timeout and number of retries. - * \param timeout The time in milliseconds. The value -1 disables timeouts. + * \param query_timeout The timeout we use for queries we send + * \param client_timeout The timeout at which point we send back a + * SERVFAIL (while continuing to resolve the query) + * \param lookup_timeout The timeout at which point we give up and + * stop. * \param retries The number of retries (0 means try the first time only, * do not retry). */ - void setTimeouts(int timeout = -1, unsigned retries = 0); + void setTimeouts(int query_timeout = 2000, + int client_timeout = 4000, + int lookup_timeout = 30000, + unsigned retries = 3); /** * \short Get info about timeouts. @@ -153,6 +169,39 @@ public: */ std::pair getTimeouts() const; + /** + * \brief Get the timeout for outgoing queries + * + * \returns Timeout for outgoing queries + */ + int getQueryTimeout() const; + + /** + * \brief Get the timeout for incoming client queries + * + * After this timeout, a SERVFAIL shall be sent back + * (internal resolving on the query will continue, see + * \c getLookupTimeout()) + * + * \returns Timeout for outgoing queries + */ + int getClientTimeout() const; + + /** + * \brief Get the timeout for lookups + * + * After this timeout, internal processing shall stop + */ + int getLookupTimeout() const; + + /** + * \brief Get the number of retries for outgoing queries + * + * If a query times out (value of \c getQueryTimeout()), we + * will retry this number of times + */ + int getRetries() const; + private: ResolverImpl* impl_; asiolink::DNSService* dnss_; diff --git a/src/bin/resolver/resolver.spec.pre.in b/src/bin/resolver/resolver.spec.pre.in index 53c67a60665ee062e5ba6427bf3456674d9340c6..48e1eb6aa1b77b52881d4a20647e82b6e6121872 100644 --- a/src/bin/resolver/resolver.spec.pre.in +++ b/src/bin/resolver/resolver.spec.pre.in @@ -4,16 +4,28 @@ "module_description": "Recursive service", "config_data": [ { - "item_name": "timeout", + "item_name": "timeout_query", "item_type": "integer", "item_optional": False, "item_default": 2000 }, + { + "item_name": "timeout_client", + "item_type": "integer", + "item_optional": False, + "item_default": 4000 + }, + { + "item_name": "timeout_lookup", + "item_type": "integer", + "item_optional": False, + "item_default": 30000 + }, { "item_name": "retries", "item_type": "integer", "item_optional": False, - "item_default": 0 + "item_default": 3 }, { "item_name": "forward_addresses", diff --git a/src/bin/resolver/response_scrubber.cc b/src/bin/resolver/response_scrubber.cc new file mode 100644 index 0000000000000000000000000000000000000000..060a8b1a92b4ea397eb95b228327ea7f72244bb9 --- /dev/null +++ b/src/bin/resolver/response_scrubber.cc @@ -0,0 +1,189 @@ + +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include +#include +#include +#include "response_scrubber.h" + +using namespace isc::dns; +using namespace std; + +// Compare addresses etc. + +ResponseScrubber::Category ResponseScrubber::addressCheck( + const asiolink::IOEndpoint& to, const asiolink::IOEndpoint& from) +{ + if (from.getProtocol() == to.getProtocol()) { + if (from.getAddress() == to.getAddress()) { + if (from.getPort() == to.getPort()) { + return (ResponseScrubber::SUCCESS); + } else { + return (ResponseScrubber::PORT); + } + } else { + return (ResponseScrubber::ADDRESS); + } + } + return (ResponseScrubber::PROTOCOL); +} + +// Do a general scrubbing. The QNAMES of RRsets in the specified section are +// compared against the list of name given and if they are not equal and not in +// the specified relationship (generally superdomain or subdomain) to at least +// of of the given names, they are removed. + +unsigned int +ResponseScrubber::scrubSection(Message& message, + const vector& names, + const NameComparisonResult::NameRelation connection, + const Message::Section section) +{ + unsigned int count = 0; // Count of RRsets removed + unsigned int kept = 0; // Count of RRsets kept + bool removed = true; // Set true if RRset removed in a pass + + // Need to go through the section multiple times as when an RRset is + // removed, all iterators into the section are invalidated. This condition + // is flagged by "remove" being set true when an RRset is removed. + + while (removed) { + RRsetIterator i = message.beginSection(section); + + // Skips the ones that have been checked (and retained) in a previous + // pass through the "while" loop. (Although RRset removal invalidates + // iterators, it does not change the relative order of the retained + // RRsets in the section.) + for (int j = 0; j < kept; ++j) { + ++i; + } + + // Start looking at the remaining entries in the section. + removed = false; + for (; (i != message.endSection(section)) && (!removed); ++i) { + + // Loop through the list of names given and see if any are in the + // given relationship with the QNAME of this RRset + bool nomatch = true; + for (vector::const_iterator n = names.begin(); + ((n != names.end()) && nomatch); ++n) { + NameComparisonResult result = (*i)->getName().compare(**n); + NameComparisonResult::NameRelation relationship = + result.getRelation(); + if ((relationship == NameComparisonResult::EQUAL) || + (relationship == connection)) { + + // RRset in the specified relationship, so a match has + // been found + nomatch = false; + } + } + + // Remove the RRset if there was no match to one of the given names. + if (nomatch) { + message.removeRRset(section, i); + ++count; // One more RRset removed + removed = true; // Something was removed + } else { + + // There was a match so this is one more entry we can skip next + // time. + ++kept; + } + } + } + + return count; +} + +// Perform the scrubbing of all sections of the message. + +unsigned int +ResponseScrubber::scrubAllSections(Message& message, const Name& bailiwick) { + + // Leave the question section alone. Just go through the RRsets in the + // answer, authority and additional sections. + unsigned int count = 0; + const vector bailiwick_names(1, &bailiwick); + count += scrubSection(message, bailiwick_names, + NameComparisonResult::SUBDOMAIN, Message::SECTION_ANSWER); + count += scrubSection(message, bailiwick_names, + NameComparisonResult::SUBDOMAIN, Message::SECTION_AUTHORITY); + count += scrubSection(message, bailiwick_names, + NameComparisonResult::SUBDOMAIN, Message::SECTION_ADDITIONAL); + + return count; +} + +// Scrub across sections. + +unsigned int +ResponseScrubber::scrubCrossSections(isc::dns::Message& message) { + + // Get a list of the names in the answer section or, failing this, the + // question section. Note that pointers to the names within "message" are + // stored; this is OK as the relevant sections in "message" will not change + // during the lifetime of this method (it only affects the authority + // section). + vector source; + if (message.getRRCount(Message::SECTION_ANSWER) != 0) { + for (RRsetIterator i = message.beginSection(Message::SECTION_ANSWER); + i != message.endSection(Message::SECTION_ANSWER); ++i) { + const Name& qname = (*i)->getName(); + source.push_back(&qname); + } + + } else { + for (QuestionIterator i = message.beginQuestion(); + i != message.endQuestion(); ++i) { + const Name& qname = (*i)->getName(); + source.push_back(&qname); + } + } + + if (source.empty()) { + // TODO: Log the fact - should be at least a question present + return (0); + } + + // Could be duplicates, especially in the answer section, so sort the + // names and remove them. + sort(source.begin(), source.end(), ResponseScrubber::compareNameLt); + vector::iterator endunique = + unique(source.begin(), source.end(), ResponseScrubber::compareNameEq); + source.erase(endunique, source.end()); + + // Now purge the authority section of RRsets that are not equal to or a + // superdomain of the names in the question/answer section. + return (scrubSection(message, source, + NameComparisonResult::SUPERDOMAIN, Message::SECTION_AUTHORITY)); + +} + +// Scrub a message + +unsigned int +ResponseScrubber::scrub(const isc::dns::MessagePtr& message, + const isc::dns::Name& bailiwick) +{ + unsigned int sections_removed = scrubAllSections(*message, bailiwick); + sections_removed += scrubCrossSections(*message); + + return sections_removed; +} + + diff --git a/src/bin/resolver/response_scrubber.h b/src/bin/resolver/response_scrubber.h new file mode 100644 index 0000000000000000000000000000000000000000..c3fce57df90f0b16535415850a45d0e8044d1f57 --- /dev/null +++ b/src/bin/resolver/response_scrubber.h @@ -0,0 +1,422 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +// $Id$ + +#ifndef __RESPONSE_SCRUBBER_H +#define __RESPONSE_SCRUBBER_H + +/// \page DataScrubbing Data Scrubbing +/// \section DataScrubbingIntro Introduction +/// When a response is received from an authoritative server, it should be +/// checked to ensure that the data contained in it is valid. Signed data is +/// not a problem - validating the signatures is a sufficient check. But +/// unsigned data in a response is more of a problem. (Note that even data from +/// signed zones may be not be signed, e.g. delegations are not signed.) In +/// particular, how do we know that the server from which the response was +/// received was authoritive for the data it returned? +/// +/// The part of the code that checks for this is the "Data Scrubbing" module. +/// Although it includes the checking of IP addresses and ports, it is called +/// "Scrubbing" because it "scrubs" the returned message and removes doubtful +/// information. +/// +/// \section DataScrubbingBasic Basic Checks +/// The first part - how do we know that the response comes from the correct +/// server - is relatively trivial, albeit not foolproof (which is why DNSSEC +/// was developed). The following are checked: +/// +/// - The IP address from which the response was received is the same as the +/// one to which the query was sent. +/// - The port on which the response was received is the same as the one from +/// which the query was sent. +/// +/// (These tests need not not done for a TCP connection - if data is received +/// over the TCP stream, it is assumed that it comes from the address and port +/// to which a connection was made.) +/// +/// - The protocol used to send the question is the same as the protocol on +/// which an answer was received. +/// +/// (Strictly speaking, if this check fails it is a programming error - the +/// code should not mix up UPD and TCP messages.) +/// +/// - The QID in the response message is the same as the QID in the query +/// message sent. +/// +/// If the conditions are met, then the data - in all three response sections - +/// is scanned and out of bailiwick data is removed ("scrubbed"). +/// +/// \section DataScrubbingBailiwick Bailiwick +/// Bailiwick means "district or jurisdiction of bailie or bailiff" (Concise +/// Oxford Dictionary, 7th Edition). It is not a term mentioned in any RFC +/// (or at least, any RFC up to RFC 5997) but is widely used in DNS literature. +/// In this context it is taken to mean the data for which a DNS server has +/// authority. So when we speak of the information being "in bailiwick", we +/// mean that the the server is the ultimate source of authority for that data. +/// +/// In practice, determining this from the response alone is difficult. In +/// particular, as a server may be authoritative for many zones, it could in +/// theory be authoritative for any combination of RRsets that appear in a +/// response. +/// +/// For this reason, bailiwick is dependent on the query. If, for example, a +/// query for www.example.com is sent to the nameservers for example.com +/// (because of a referral of from the com. servers), the bailiwick for the +/// query is example.com. This means that any information returned on domains +/// other than example.com may not be authoritative. More exactly, it may be +/// authoritative (because the server is also authoritative for the zone +/// concerned), but based on the information available (in this example, that +/// the response originated from a nameserver for the zone example.com) it is +/// not possible to be certain. +/// +/// Ideally, out of bailiwick data should be excluded from further processing +/// as it may be incorrect and corrupt the cache. In practice, there are +/// two cases to consider: +/// +/// The first is when the data has a qname that is not example.com or a +/// subdomain of it (e.g. xyz.com, www.example.net). In this case the data can +/// be retrieved by an independent query - no path from the root zone to the +/// data goes through the current bailiwick, so there is no chance of ending up +/// in a loop. In this case, data that appears to be out of bailiwick can be +/// dropped from the response. +/// +/// The second case is when the QNAME of the data is a subdomain of the +/// bailiwick. Here the server may or may not be authoritative for the data. +/// For example, if the name queried for were www.sub.example.com and the +/// example.com nameservers supplied an answer: +/// +/// - The answer could be authoritative - www.sub.example.com could be +/// in the example.com zone. +/// - The answer might not be authoritative - the zone sub.example.com may have +/// been delegated, so the authoritative answer should come from +/// sub.example.com's nameservers. +/// - The answer might be authoritative even though zone sub.example.com has +/// been delegated, because the nameserver for example.com is the same as +/// that for sub.example.com. +/// +/// Unlike the previous case, it is not possible to err on the side of caution +/// and drop such data. Any independent query for it will pass through the +/// current bailiwick and the same question will be asked again. For this +/// reason, any data in the response that has a QNAME equal to a subdomain of +/// the bailiwick has to be accepted. +/// +/// In summary then, data in a response that has a QNAME equal to or a subdomain +/// of the bailiwick is considered in-bailiwick. Anything else is out of of +/// bailiwick. +/// +/// \subsection DataScrubbingCrossSection Cross-Section Scrubbing +/// Even with the bailiwick checks above, there are some additional cleaning +/// that can be done with the packet. In particular: +/// +/// - The QNAMEs of the RRsets in the authority section must be equal to or +/// superdomains of a QNAME of an RRset in the answer. Any that are not +/// should be removed. +/// - If there is no answer section, the QNAMES of RRsets in the authority +/// section must be equal to or superdomains of the QNAME of the RRset in the +/// question. +/// +/// Although previous checks should have removed some inconsistencies, it +/// will not trap obscure cases (e.g. bailiwick: "example.com", answer: +/// "www.example.com", authority: sub.example.com). These checks do just that. +/// +/// (Note that not included here is QNAME of question not equal to or a +/// superdomain of the answer; that check is made in the ResponseClassifier +/// class.) +/// +/// \section DataScrubbingExample Examples +/// Some examples should make this clear: they all use the notation +/// Qu = Question, Zo = Zone being queried, An = Answer, Au = Authority, +/// Ad = Additional. +/// +/// \subsection DataScrubbingEx1 Example 1: Simple Query +/// Querying a nameserver for the zone "example.com" for www.example.com and +/// receiving the answer "www.example.com A 1.2.3.4" with two nameservers quoted +/// as authority and both their addresses in the additional section: +/// +/// Qu: www.example.com\n +/// Zo: example.com +/// +/// An: www.example.com A 192.0.2.1 +/// +/// Au(1): example.com NS ns0.example.com\n +/// Au(2): example.com NS ns1.example.net +/// +/// Ad(1): ns0.example.com A 192.0.2.100\n +/// Ad(2): ns1.example.net A 192.0.2.200 +/// +/// This answer could be returned by a properly configured server. All resource +/// records in the answer - with the exception of Ad(2) - are in bailiwick +/// because the QNAME is equal to or a subdomain of the zone being queried. +/// +/// It is permissible for Ad(2) to be returned by a properly configured server +/// as a hint to resolvers. However the example.com nameservers are not +/// authoritative for addresses of domains in example.net; that record could +/// be out of date or incorrect. Indeed, it might even be a deliberate attempt +/// at a spoof by getting us to cache an invalid address for ns1.example.net. +/// The safest thing to do is to drop the A record and to get the address of +/// ns1.example.net by querying for that name through the .net nameservers. +/// +/// \subsection DataScrubbingEx2 Example 2: Multiple Zones on Same Nameserver +/// Assume now that example.com and sub.example.com are hosted on the same +/// nameserver and that from the .com zone the resolver has received a referral +/// to example.com. Suppose that the query is for www.sub.example.com and that +/// the following response is received: +/// +/// Qu: www.sub.example.com\n +/// Zo: example.com +/// +/// An: +/// +/// Au(1): sub.example.com NS ns0.sub.example.com\n +/// Au(2): sub.example.com NS ns1.example.net +/// +/// Ad(1): ns0.sub.example.com A 192.0.2.101\n +/// Ad(2): ns1.example.net A 192.0.2.201 +/// +/// Although we asked the example.com nameservers for information, we got the +/// nameservers for sub.example.com in the authority section. This is valid +/// because if BIND-10 hosts multiple zones, it will look up the data in the +/// zone that most closely matches the query. +/// +/// Using the criteria above, the data in the additional section can therefore +/// be regarded as in bailiwick because sub.example.com is a subdomain of +/// example.com. As before though, the address for ns1.example.net in the +/// additional section is not in bailiwick because ns1.example.net is now a +/// subdomain of example.com. +/// +/// \subsection DataScrubbingEx3 Example 3: Deliberate Spoof Attempt +/// Qu: www.example.com\n +/// Zo: example.com +/// +/// An: www.example.com A 192.0.2.1 +/// +/// Au(1): com NS ns0.example.com\n +/// Au(2): com NS ns1.example.net +/// +/// Ad(1): ns0.example.com A 192.0.2.100\n +/// Ad(2): ns1.example.net A 192.0.2.200 +/// +/// This is a deliberately invalid response. The query is being sent to the +/// nameservers for example.com (presumably because a referral to example.com +/// was received from the com nameservers), but the response is an attempt +/// to get the specified nameservers cached as the nameservers for com - for +/// which example.com is not authoritative. +/// +/// Note though that this response is only invalid because, due to the previous +/// referral, the query was sent to the example.com nameservers. Had the +/// referral been to the com nameservers, it would be a valid response; the com +/// zone could well be serving all the data for example.com. Having said that, +/// the A record for ns1.example.net would still be regarded as being out of +/// bailiwick becase the nameserver is not authoritative for the .net zone. +/// +/// \subsection DataScrubbingEx4 Example 4: Inconsistent Answer Section +/// Qu: www.example.com\n +/// Zo: example.com +/// +/// An: www.example.com A 192.0.2.1 +/// +/// Au(1): alpha.example.com NS ns0.example.com\n +/// Au(2): alpha.example.com NS ns1.example.net +/// +/// Ad(1): ns0.example.com A 192.0.2.100\n +/// Ad(2): ns1.example.net A 192.0.2.200 +/// +/// Here, everything in the answer and authority sections is in bailiwick for +/// the example.com server. And although the zone example.com was queried, it +/// is permissible for the authority section to contain nameservers with a +/// qname that is a subdomain of example.com (e.g. see \ref DataScrubbingEx2). +/// However, only servers with a qname that is equal to or a superdomain of +/// the answer are authoritative for the answer. So in this case, both +/// Au(1) and Au(2) (as well as Ad(2), for reasons given earlier) will be +/// scrubbed. + +#include +#include +#include +#include + +/// \brief Response Data Scrubbing +/// +/// This is the class that implements the data scrubbing. Given a response +/// message and some additional information, it checks the information using +/// the rules given in \ref DataScrubbing and either rejects the packet or +/// modifies it to remove non-conforming RRsets. +/// +/// TODO: Examine the additional records and remove all cases where the +/// QNAME does not match the RDATA of records in the authority section. + +class ResponseScrubber { +public: + + /// \brief Response Code for Address Check + enum Category { + SUCCESS = 0, ///< Packet is OK + + // Error categories + + ADDRESS = 1, ///< Mismatching IP address + PORT = 2, ///< Mismatching port + PROTOCOL = 3 ///< Mismatching protocol + }; + + /// \brief Check IP Address + /// + /// Compares the address to which the query was sent, the port it was + /// sent from, and the protocol used for communication with the (address, + /// port, protocol) from which the response was received. + /// + /// \param to Endpoint representing the address to which the query was sent. + /// \param from Endpoint from which the response was received. + /// + /// \return SUCCESS if the two endpoints match, otherwise an error status + /// indicating what was incorrect. + static Category addressCheck(const asiolink::IOEndpoint& to, + const asiolink::IOEndpoint& from); + + /// \brief Check QID + /// + /// Compares the QID in the sent message with the QID in the response. + /// + /// \param sent Message sent to the authoritative server + /// \param received Message received from the authoritative server + /// + /// \return true if the QIDs match, false otherwise. + static bool qidCheck(const isc::dns::Message& sent, + const isc::dns::Message& received) { + return (sent.getQid() == received.getQid()); + } + + /// \brief Generalised Scrub Message Section + /// + /// When scrubbing a message given the bailiwick of the server, RRsets are + /// retained in the message section if the QNAME is equal to or a subdomain + /// of the bailiwick. However, when checking QNAME of RRsets in the + /// authority section against the QNAME of the question or answers, RRsets + /// are retained only if their QNAME is equal to or a superdomain of the + /// name in question. + /// + /// This method provides the generalised scrubbing whereby the RRsets in + /// a section are tested against a given name, and RRsets kept if their + /// QNAME is equal to or in the supplied relationship with the given name. + /// + /// \param section Section of the message to be scrubbed. + /// \param zone Names against which RRsets should be checked. Note that + /// this is a vector of pointers to Name objects; they are assumed to + /// independently exist, and the caller retains ownership of them and is + /// assumed to destroy them when needed. + /// \param connection Relationship required for retention, i.e. the QNAME of + /// an RRset in the specified section must be equal to or a "connection" + /// (SUPERDOMAIN/SUBDOMAIN) of "name" for the RRset to be retained. + /// \param message Message to be scrubbed. + /// + /// \return Count of the number of RRsets removed from the section. + static unsigned int scrubSection(isc::dns::Message& message, + const std::vector& names, + const isc::dns::NameComparisonResult::NameRelation connection, + const isc::dns::Message::Section section); + + /// \brief Scrub All Sections of a Message + /// + /// Scrubs each of the answer, authority and additional sections of the + /// message. + /// + /// No distinction is made between RRsets legitimately in the message (e.g. + /// glue for authorities that are not in bailiwick) and ones that could be + /// considered as attempts of spoofing (e.g. non-bailiwick RRsets in the + /// additional section that are not related to the query). + /// + /// The resultant packet returned to the caller may be invalid. If so, it + /// is up to the caller to detect that. + /// + /// \param message Message to be scrubbed. + /// \param bailiwick Name of the zone whose authoritative servers were + /// queried. + /// + /// \return Count of the number of RRsets removed from the message. + static unsigned int scrubAllSections(isc::dns::Message& message, + const isc::dns::Name& bailiwick); + + /// \brief Scrub Across Message Sections + /// + /// Does some cross-section comparisons and removes inconsistent RRs. In + /// particular it: + /// + /// - If an answer is present, checks that the qname of the authority RRs + /// are equal to or superdomain of the qname answer RRsets. Any that are + /// not are removed. + /// - If an answer is not present, checks that the authority RRs are + /// equal to or superdomains of the question. If not, the authority RRs + /// are removed. + /// + /// Note that the scrubbing does not check: + /// + /// - that the question is in the bailiwick of the server; that check is + /// assumed to have been done prior to the query being sent (else why + /// was the query sent there in the first place?) + /// - that the qname of one of the RRsets in the answer (if present) is + /// equal to the qname of the question (that check is done in the + /// response classification code). + /// + /// \param message Message to be scrubbed. + /// + /// \return Count of the number of RRsets removed from the section. + static unsigned int scrubCrossSections(isc::dns::Message& message); + + /// \brief Main Scrubbing Entry Point + /// + /// The single entry point to the module to sanitise the message. All + /// it does is call the various other scrubbing methods. + /// + /// \param message Pointer to the message to be scrubbed. (This is a + /// pointer - as opposed to a Message as in other methods in this class - + /// as the external code is expected to be mainly using message pointers + /// to access messages.) + /// \param bailiwick Name of the zone whose authoritative servers were + /// queried. + /// + /// \return Count of the number of RRsets removed from the message. + static unsigned int scrub(const isc::dns::MessagePtr& message, + const isc::dns::Name& bailiwick); + + /// \brief Comparison Function for Sorting Name Pointers + /// + /// Utility method called to sorts pointers to names in lexical order. + /// + /// \param n1 Pointer to first Name object + /// \param n2 Pointer to second Name object + /// + /// \return true if n1 is less than n2, false otherwise. + static bool compareNameLt(const isc::dns::Name* n1, + const isc::dns::Name* n2) + { + return (*n1 < *n2); + } + + /// \brief Function for Comparing Name Pointers + /// + /// Utility method called to sorts pointers to names in lexical order. + /// + /// \param n1 Pointer to first Name object + /// \param n2 Pointer to second Name object + /// + /// \return true if n1 is equal to n2, false otherwise. + static bool compareNameEq(const isc::dns::Name* n1, + const isc::dns::Name* n2) + { + return (*n1 == *n2); + } +}; + +#endif // __RESPONSE_SCRUBBER_H diff --git a/src/bin/resolver/tests/Makefile.am b/src/bin/resolver/tests/Makefile.am index 85524395211991c551217b93ea751582c55fe733..3dc6b3b5a1e7b6265e3216bddf549d3f91bf1ed1 100644 --- a/src/bin/resolver/tests/Makefile.am +++ b/src/bin/resolver/tests/Makefile.am @@ -4,7 +4,6 @@ AM_CPPFLAGS += -I$(top_builddir)/src/lib/cc AM_CPPFLAGS += -DTEST_DATA_DIR=\"$(top_srcdir)/src/lib/testutils/testdata\" AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/lib/testutils/testdata\" AM_CPPFLAGS += $(BOOST_INCLUDES) -AM_CPPFLAGS += $(BOOST_INCLUDES) AM_CXXFLAGS = $(B10_CXXFLAGS) @@ -20,24 +19,35 @@ TESTS += run_unittests run_unittests_SOURCES = $(top_srcdir)/src/lib/dns/tests/unittest_util.h run_unittests_SOURCES += $(top_srcdir)/src/lib/dns/tests/unittest_util.cc run_unittests_SOURCES += ../resolver.h ../resolver.cc -run_unittests_SOURCES += ../response_classifier.h ../response_classifier.cc +run_unittests_SOURCES += ../response_scrubber.h ../response_scrubber.cc run_unittests_SOURCES += resolver_unittest.cc run_unittests_SOURCES += resolver_config_unittest.cc -run_unittests_SOURCES += response_classifier_unittest.cc +run_unittests_SOURCES += response_scrubber_unittest.cc run_unittests_SOURCES += run_unittests.cc run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS) run_unittests_LDADD = $(GTEST_LDADD) run_unittests_LDADD += $(SQLITE_LIBS) run_unittests_LDADD += $(top_builddir)/src/lib/testutils/libtestutils.la -run_unittests_LDADD += $(top_builddir)/src/lib/datasrc/libdatasrc.la -run_unittests_LDADD += $(top_builddir)/src/lib/dns/libdns++.la +run_unittests_LDADD += $(top_builddir)/src/lib/datasrc/libdatasrc.la +run_unittests_LDADD += $(top_builddir)/src/lib/dns/libdns++.la run_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libasiolink.la run_unittests_LDADD += $(top_builddir)/src/lib/config/libcfgclient.la run_unittests_LDADD += $(top_builddir)/src/lib/cc/libcc.la run_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libexceptions.la run_unittests_LDADD += $(top_builddir)/src/lib/xfr/libxfr.la run_unittests_LDADD += $(top_builddir)/src/lib/log/liblog.la +run_unittests_LDADD += $(top_builddir)/src/lib/cache/libcache.la +run_unittests_LDADD += $(top_builddir)/src/lib/nsas/libnsas.la + +# Note the ordering matters: -Wno-... must follow -Wextra (defined in +# B10_CXXFLAGS +run_unittests_CXXFLAGS = $(AM_CXXFLAGS) +if USE_GXX +run_unittests_CXXFLAGS += -Wno-unused-parameter +endif endif + + noinst_PROGRAMS = $(TESTS) diff --git a/src/bin/resolver/tests/resolver_config_unittest.cc b/src/bin/resolver/tests/resolver_config_unittest.cc index ca0c9b67bf62b66e35b5a6d88b88ef44ef28050e..916396a4c2f530a176a6858b165cceb09c118f94 100644 --- a/src/bin/resolver/tests/resolver_config_unittest.cc +++ b/src/bin/resolver/tests/resolver_config_unittest.cc @@ -237,31 +237,51 @@ TEST_F(ResolverConfig, invalidListenAddresses) { // Just test it sets and gets the values correctly TEST_F(ResolverConfig, timeouts) { - server.setTimeouts(0, 1); - EXPECT_EQ(0, server.getTimeouts().first); - EXPECT_EQ(1, server.getTimeouts().second); + server.setTimeouts(0, 1, 2, 3); + EXPECT_EQ(0, server.getQueryTimeout()); + EXPECT_EQ(1, server.getClientTimeout()); + EXPECT_EQ(2, server.getLookupTimeout()); + EXPECT_EQ(3, server.getRetries()); server.setTimeouts(); - EXPECT_EQ(-1, server.getTimeouts().first); - EXPECT_EQ(0, server.getTimeouts().second); + EXPECT_EQ(2000, server.getQueryTimeout()); + EXPECT_EQ(4000, server.getClientTimeout()); + EXPECT_EQ(30000, server.getLookupTimeout()); + EXPECT_EQ(3, server.getRetries()); } TEST_F(ResolverConfig, timeoutsConfig) { ElementPtr config = Element::fromJSON("{" - "\"timeout\": 1000," - "\"retries\": 3" + "\"timeout_query\": 1000," + "\"timeout_client\": 2000," + "\"timeout_lookup\": 3000," + "\"retries\": 4" "}"); ConstElementPtr result(server.updateConfig(config)); EXPECT_EQ(result->toWire(), isc::config::createAnswer()->toWire()); - EXPECT_EQ(1000, server.getTimeouts().first); - EXPECT_EQ(3, server.getTimeouts().second); + EXPECT_EQ(1000, server.getQueryTimeout()); + EXPECT_EQ(2000, server.getClientTimeout()); + EXPECT_EQ(3000, server.getLookupTimeout()); + EXPECT_EQ(4, server.getRetries()); } TEST_F(ResolverConfig, invalidTimeoutsConfig) { invalidTest("{" - "\"timeout\": \"error\"" + "\"timeout_query\": \"error\"" "}"); invalidTest("{" - "\"timeout\": -2" + "\"timeout_query\": -2" + "}"); + invalidTest("{" + "\"timeout_client\": \"error\"" + "}"); + invalidTest("{" + "\"timeout_client\": -2" + "}"); + invalidTest("{" + "\"timeout_lookup\": \"error\"" + "}"); + invalidTest("{" + "\"timeout_lookup\": -2" "}"); invalidTest("{" "\"retries\": \"error\"" diff --git a/src/bin/resolver/tests/response_scrubber_unittest.cc b/src/bin/resolver/tests/response_scrubber_unittest.cc new file mode 100644 index 0000000000000000000000000000000000000000..1dc66395eff1eba05a5f8cba575cd94fd0e7a8ab --- /dev/null +++ b/src/bin/resolver/tests/response_scrubber_unittest.cc @@ -0,0 +1,542 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +// $Id$ + +#include +#include + +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +// Class for endpoint checks. The family of the endpoint is set in the +// constructor; the address family by the string provided for the address. + +namespace asiolink { + +class GenericEndpoint : public IOEndpoint { +public: + GenericEndpoint(const std::string& address, uint16_t port, short protocol) : + address_(address), port_(port), protocol_(protocol) + {} + virtual ~GenericEndpoint() + {} + + virtual IOAddress getAddress() const { + return address_; + } + + virtual uint16_t getPort() const { + return port_; + } + + virtual short getProtocol() const { + return protocol_; + } + + virtual short getFamily() const { + return address_.getFamily(); + } + +private: + IOAddress address_; // Address of endpoint + uint16_t port_; // Port number of endpoint + short protocol_; // Protocol of the endpoint + }; +} + +using namespace asio::ip; +using namespace isc::dns; +using namespace rdata; +using namespace isc::dns::rdata::generic; +using namespace isc::dns::rdata::in; +using namespace asiolink; + +// Test class + +namespace { +class ResponseScrubberTest : public ::testing::Test { +public: + ResponseScrubberTest() : + bailiwick("example.com"), + + qu_in_any_www(Name("www.example.com"), RRClass::IN(), RRType::ANY()), + qu_in_a_www(Name("www.example.com"), RRClass::IN(), RRType::A()), + qu_in_ns(Name("example.com"), RRClass::IN(), RRType::NS()), + qu_in_txt_www(Name("www.example.com"), RRClass::IN(), RRType::TXT()), + rrs_in_a_org(new RRset(Name("mail.example.org"), RRClass::IN(), + RRType::A(), RRTTL(300))), + + rrs_in_a_net(new RRset(Name("mail.example.net"), RRClass::IN(), + RRType::A(), RRTTL(300))), + rrs_in_a_www(new RRset(Name("www.example.com"), RRClass::IN(), + RRType::A(), RRTTL(300))), + rrs_in_cname_www(new RRset(Name("www.example.com"), RRClass::IN(), + RRType::CNAME(), RRTTL(300))), + rrs_in_a_wwwnet(new RRset(Name("www.example.net"), RRClass::IN(), + RRType::A(), RRTTL(300))), + rrs_in_ns(new RRset(Name("example.com"), RRClass::IN(), + RRType::NS(), RRTTL(300))), + rrs_in_ns_com(new RRset(Name("com"), RRClass::IN(), + RRType::NS(), RRTTL(300))), + rrs_in_ns_net(new RRset(Name("example.net"), RRClass::IN(), + RRType::NS(), RRTTL(300))), + rrs_in_ns_sub(new RRset(Name("subdomain.example.com"), RRClass::IN(), + RRType::NS(), RRTTL(300))), + rrs_in_ns_sub2(new RRset(Name("subdomain2.example.com"), RRClass::IN(), + RRType::NS(), RRTTL(300))), + rrs_in_a_ns0(new RRset(Name("ns0.example.com"), RRClass::IN(), + RRType::A(), RRTTL(300))), + rrs_in_a_ns1(new RRset(Name("ns1.com"), RRClass::IN(), + RRType::A(), RRTTL(300))), + rrs_in_a_ns2(new RRset(Name("ns2.example.net"), RRClass::IN(), + RRType::A(), RRTTL(300))), + rrs_in_a_ns3(new RRset(Name("ns3.subdomain.example.com"), RRClass::IN(), + RRType::A(), RRTTL(300))), + rrs_in_txt_www(new RRset(Name("www.example.com"), RRClass::IN(), + RRType::TXT(), RRTTL(300))) + {} + Name bailiwick; // Bailiwick of the server queried + Question qu_in_any_www; // www.example.com IN ANY + Question qu_in_a_www; // www.example.com IN A + Question qu_in_ns; // example.com IN NS + Question qu_in_txt_www; // www.example.com IN TXT + RRsetPtr rrs_in_a_org; // mail.example.org IN A + RRsetPtr rrs_in_a_net; // mail.example.org IN A + RRsetPtr rrs_in_a_www; // www.example.com IN A + RRsetPtr rrs_in_cname_www; // www.example.com IN CNAME + RRsetPtr rrs_in_a_wwwnet; // www.example.net IN A + RRsetPtr rrs_in_ns; // example.com IN NS + RRsetPtr rrs_in_ns_com; // com IN NS + RRsetPtr rrs_in_ns_net; // example.net IN NS + RRsetPtr rrs_in_ns_sub; // subdomain.example.com IN NS + RRsetPtr rrs_in_ns_sub2; // subdomain2.example.com IN NS + RRsetPtr rrs_in_a_ns0; // ns0.example.com IN A + RRsetPtr rrs_in_a_ns1; // ns1.com IN A + RRsetPtr rrs_in_a_ns2; // ns2.example.net IN A + RRsetPtr rrs_in_a_ns3; // ns3.subdomain.example.net IN A + RRsetPtr rrs_in_txt_www; // www.example.com IN TXT +}; + + +// Check that the IP addresses/ports/protocol for the packets sent and received +// both match if both types are IP V4. + +TEST_F(ResponseScrubberTest, UDPv4) { + + // Basic UDP Endpoint + GenericEndpoint udp_a("192.0.2.1", 12345, IPPROTO_UDP); + + // Same address, port + GenericEndpoint udp_b("192.0.2.1", 12345, IPPROTO_UDP); + EXPECT_EQ(ResponseScrubber::SUCCESS, + ResponseScrubber::addressCheck(udp_a, udp_b)); + + // Different address, same port + GenericEndpoint udp_c("192.0.2.2", 12345, IPPROTO_UDP); + EXPECT_EQ(ResponseScrubber::ADDRESS, + ResponseScrubber::addressCheck(udp_a, udp_c)); + + // Same address, different port + GenericEndpoint udp_d("192.0.2.1", 12346, IPPROTO_UDP); + EXPECT_EQ(ResponseScrubber::PORT, + ResponseScrubber::addressCheck(udp_a, udp_d)); + + // Different address, different port + GenericEndpoint udp_e("192.0.2.3", 12347, IPPROTO_UDP); + EXPECT_EQ(ResponseScrubber::ADDRESS, + ResponseScrubber::addressCheck(udp_a, udp_e)); + +} + +// Repeat the tests for TCP + +TEST_F(ResponseScrubberTest, TCPv4) { + + // Basic TCP Endpoint + GenericEndpoint tcp_a("192.0.2.1", 12345, IPPROTO_TCP); + + // Same address, port + GenericEndpoint tcp_b("192.0.2.1", 12345, IPPROTO_TCP); + EXPECT_EQ(ResponseScrubber::SUCCESS, + ResponseScrubber::addressCheck(tcp_a, tcp_b)); + + // Different address, same port + GenericEndpoint tcp_c("192.0.2.2", 12345, IPPROTO_TCP); + EXPECT_EQ(ResponseScrubber::ADDRESS, + ResponseScrubber::addressCheck(tcp_a, tcp_c)); + + // Same address, different port + GenericEndpoint tcp_d("192.0.2.1", 12346, IPPROTO_TCP); + EXPECT_EQ(ResponseScrubber::PORT, + ResponseScrubber::addressCheck(tcp_a, tcp_d)); + + // Different address, different port + GenericEndpoint tcp_e("192.0.2.3", 12347, IPPROTO_TCP); + EXPECT_EQ(ResponseScrubber::ADDRESS, + ResponseScrubber::addressCheck(tcp_a, tcp_e)); + +} + +// Repeat the tests for UDP/IPv6 + +TEST_F(ResponseScrubberTest, UDPv6) { + + // Basic UDP Endpoint + GenericEndpoint udp_a("2001:db8::1", 12345, IPPROTO_UDP); + + // Same address and port + GenericEndpoint udp_b("2001:db8::1", 12345, IPPROTO_UDP); + EXPECT_EQ(ResponseScrubber::SUCCESS, + ResponseScrubber::addressCheck(udp_a, udp_b)); + + // Different address, same port + GenericEndpoint udp_c("2001:db8::3", 12345, IPPROTO_UDP); + EXPECT_EQ(ResponseScrubber::ADDRESS, + ResponseScrubber::addressCheck(udp_a, udp_c)); + + // Same address, different port + GenericEndpoint udp_d("2001:db8::1", 12346, IPPROTO_UDP); + EXPECT_EQ(ResponseScrubber::PORT, + ResponseScrubber::addressCheck(udp_a, udp_d)); + + // Different address, different port + GenericEndpoint udp_e("2001:db8::3", 12347, IPPROTO_UDP); + EXPECT_EQ(ResponseScrubber::ADDRESS, + ResponseScrubber::addressCheck(udp_a, udp_e)); + +} + +// Same again for TCP/IPv6 + +TEST_F(ResponseScrubberTest, TCPv6) { + + // Basic TCP Endpoint + GenericEndpoint tcp_a("2001:db8::1", 12345, IPPROTO_TCP); + + // Same address and port + GenericEndpoint tcp_b("2001:db8::1", 12345, IPPROTO_TCP); + EXPECT_EQ(ResponseScrubber::SUCCESS, + ResponseScrubber::addressCheck(tcp_a, tcp_b)); + + // Different address, same port + GenericEndpoint tcp_c("2001:db8::3", 12345, IPPROTO_TCP); + EXPECT_EQ(ResponseScrubber::ADDRESS, + ResponseScrubber::addressCheck(tcp_a, tcp_c)); + + // Same address, different port + GenericEndpoint tcp_d("2001:db8::1", 12346, IPPROTO_TCP); + EXPECT_EQ(ResponseScrubber::PORT, + ResponseScrubber::addressCheck(tcp_a, tcp_d)); + + // Different address, different port + GenericEndpoint tcp_e("2001:db8::3", 12347, IPPROTO_TCP); + EXPECT_EQ(ResponseScrubber::ADDRESS, + ResponseScrubber::addressCheck(tcp_a, tcp_e)); + +} + +// Ensure that mixed IPv4/6 addresses don't match. + +TEST_F(ResponseScrubberTest, v4v6) { + + // UDP + GenericEndpoint udp_a("2001:db8::1", 12345, IPPROTO_UDP); + GenericEndpoint udp_b("192.0.2.1", 12345, IPPROTO_UDP); + EXPECT_EQ(ResponseScrubber::ADDRESS, + ResponseScrubber::addressCheck(udp_a, udp_b)); + + // TCP + GenericEndpoint tcp_a("2001:db8::1", 12345, IPPROTO_TCP); + GenericEndpoint tcp_b("192.0.2.1", 12345, IPPROTO_TCP); + EXPECT_EQ(ResponseScrubber::ADDRESS, + ResponseScrubber::addressCheck(udp_a, udp_b)); +} + +// Check mixed protocols are detected + +TEST_F(ResponseScrubberTest, Protocol) { + GenericEndpoint udp_a("2001:db8::1", 12345, IPPROTO_UDP); + GenericEndpoint tcp_a("2001:db8::1", 12345, IPPROTO_TCP); + EXPECT_EQ(ResponseScrubber::PROTOCOL, + ResponseScrubber::addressCheck(udp_a, tcp_a)); +} + +// Check that the QIDs check OK + +TEST_F(ResponseScrubberTest, Qid) { + Message a(Message::RENDER); + a.setQid(27); + + Message b(Message::RENDER); + b.setQid(27); + EXPECT_TRUE(ResponseScrubber::qidCheck(a, b)); + + Message c(Message::RENDER); + c.setQid(28); + EXPECT_FALSE(ResponseScrubber::qidCheck(a, c)); +} + +// Check the scrubAllSections() method. As this operates by calling the +// scrubSection() method (with a SUBDOMAIN argument), this is also a check of +// the latter. + +TEST_F(ResponseScrubberTest, ScrubAllSectionsValid) { + Message valid(Message::RENDER); + + // Valid message with nothing out of bailiwick + valid.addQuestion(qu_in_a_www); + valid.addRRset(Message::SECTION_ANSWER, rrs_in_a_www); + valid.addRRset(Message::SECTION_AUTHORITY, rrs_in_ns); + valid.addRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns0); + + // Scrub the message and expect nothing to have been removed. + int removed = ResponseScrubber::scrubAllSections(valid, bailiwick); + EXPECT_EQ(0, removed); + + // ... and check that this is the case + EXPECT_TRUE(valid.hasRRset(Message::SECTION_ANSWER, rrs_in_a_www)); + EXPECT_TRUE(valid.hasRRset(Message::SECTION_AUTHORITY, rrs_in_ns)); + EXPECT_TRUE(valid.hasRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns0)); + + // Add out-of-bailiwick glue to the additional section (pretend that the + // NS RRset contained an out-of-domain server. + valid.addRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns2); + EXPECT_TRUE(valid.hasRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns2)); + + // ... and check that it is removed when scrubbed + removed = ResponseScrubber::scrubAllSections(valid, bailiwick); + EXPECT_EQ(1, removed); + EXPECT_TRUE(valid.hasRRset(Message::SECTION_ANSWER, rrs_in_a_www)); + EXPECT_TRUE(valid.hasRRset(Message::SECTION_AUTHORITY, rrs_in_ns)); + EXPECT_TRUE(valid.hasRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns0)); + EXPECT_FALSE(valid.hasRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns2)); + } + +TEST_F(ResponseScrubberTest, ScrubAllSectionsInvalid) { + Message invalid(Message::RENDER); + + // Invalid message, with various things in and out of bailiwick. + + invalid.addQuestion(qu_in_a_www); + + // Answer section + // + // rrs_in_a_www - "www.example.com A", in bailiwick + // rrs_in_txt_www - "www.example.com TXT", in bailiwick + // rrs_in_a_org - "mail.example.org A", out of bailiwick - the qname is + // related to the bailiwick name by having a common ancestor at the root + // rrs_in_a_net - "mail.example.net A", out of bailiwick - the qname is + // related to the bailiwick name by having a common ancestor at the root + invalid.addRRset(Message::SECTION_ANSWER, rrs_in_a_www); + invalid.addRRset(Message::SECTION_ANSWER, rrs_in_txt_www); + invalid.addRRset(Message::SECTION_ANSWER, rrs_in_a_org); + invalid.addRRset(Message::SECTION_ANSWER, rrs_in_a_net); + + // Authority section + // + // rrs_in_ns - "example.com NS", in bailiwick (qname is bailiwick name) + // rrs_in_ns_com - "com NS", out of bailiwick as the qname is a superdomain + // (direct ancestor) of the bailiwick name + // rrs_in_ns_net - "example.net NS", out of bailiwick - the qname is related + // to the bailiwick name by having a common ancestor at the root + // rrs_in_ns_sub - "subdomain.example.com", in bailiwick as the qname is + // a subdomain of the bailiwick name + invalid.addRRset(Message::SECTION_AUTHORITY, rrs_in_ns); + invalid.addRRset(Message::SECTION_AUTHORITY, rrs_in_ns_com); + invalid.addRRset(Message::SECTION_AUTHORITY, rrs_in_ns_net); + invalid.addRRset(Message::SECTION_AUTHORITY, rrs_in_ns_sub); + + // Additional section + // + // rrs_in_a_ns0 - "ns0.example.com", in bailiwick because the qname is + // a subdomain of the bailiwick name + // rrs_in_a_ns1 - "ns1.com", out of bailiwick because the qname is a + // sibling to the bailiwick name + // rrs_in_a_ns2 - "ns2.example.net", out of bailiwick because qname is + // related by having a common ancestor and the root. + // rrs_in_a_ns3 - "ns3.subdomain.example.com", in bailiwick because the + // qname is a direct descendent of the bailiwick name. + invalid.addRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns0); + invalid.addRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns1); + invalid.addRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns2); + invalid.addRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns3); + + // Scrub the message + int removed = ResponseScrubber::scrubAllSections(invalid, bailiwick); + EXPECT_EQ(6, removed); + + // ... and check the sections. Answer... + EXPECT_TRUE(invalid.hasRRset(Message::SECTION_ANSWER, rrs_in_a_www)); + EXPECT_TRUE(invalid.hasRRset(Message::SECTION_ANSWER, rrs_in_txt_www)); + EXPECT_FALSE(invalid.hasRRset(Message::SECTION_ANSWER, rrs_in_a_org)); + EXPECT_FALSE(invalid.hasRRset(Message::SECTION_ANSWER, rrs_in_a_net)); + + // ... authority... + EXPECT_TRUE(invalid.hasRRset(Message::SECTION_AUTHORITY, rrs_in_ns)); + EXPECT_FALSE(invalid.hasRRset(Message::SECTION_AUTHORITY, rrs_in_ns_com)); + EXPECT_FALSE(invalid.hasRRset(Message::SECTION_AUTHORITY, rrs_in_ns_net)); + EXPECT_TRUE(invalid.hasRRset(Message::SECTION_AUTHORITY, rrs_in_ns_sub)); + + // ... additional. + EXPECT_TRUE(invalid.hasRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns0)); + EXPECT_FALSE(invalid.hasRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns1)); + EXPECT_FALSE(invalid.hasRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns2)); + EXPECT_TRUE(invalid.hasRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns3)); +} + +// An empty message + +TEST_F(ResponseScrubberTest, ScrubAllSectionsEmpty) { + Message empty(Message::RENDER); + + EXPECT_EQ(0, empty.getRRCount(Message::SECTION_QUESTION)); + EXPECT_EQ(0, empty.getRRCount(Message::SECTION_ANSWER)); + EXPECT_EQ(0, empty.getRRCount(Message::SECTION_AUTHORITY)); + EXPECT_EQ(0, empty.getRRCount(Message::SECTION_ADDITIONAL)); + + int removed = ResponseScrubber::scrubAllSections(empty, bailiwick); + EXPECT_EQ(0, removed); + + EXPECT_EQ(0, empty.getRRCount(Message::SECTION_QUESTION)); + EXPECT_EQ(0, empty.getRRCount(Message::SECTION_ANSWER)); + EXPECT_EQ(0, empty.getRRCount(Message::SECTION_AUTHORITY)); + EXPECT_EQ(0, empty.getRRCount(Message::SECTION_ADDITIONAL)); + +} + +// Check the cross-section scrubbing (checks the general scrubSection() +// method with a SUPERDOMAIN argument.) + +// Empty message (apart from question) + +TEST_F(ResponseScrubberTest, CrossSectionEmpty) { + + Message message1(Message::RENDER); + message1.addQuestion(qu_in_a_www); + int removed = ResponseScrubber::scrubCrossSections(message1); + EXPECT_EQ(0, removed); +} + +// Valid answer section + +TEST_F(ResponseScrubberTest, CrossSectionAnswer) { + + // Valid message with nothing out of bailiwick, but the authority + // (subdomain.example.com) is not authoritative for the answer. + // + // TODO: Test the case where the additional section does not match + // with something in the authority section. + Message message1(Message::RENDER); + message1.addQuestion(qu_in_a_www); + message1.addRRset(Message::SECTION_ANSWER, rrs_in_a_www); + message1.addRRset(Message::SECTION_AUTHORITY, rrs_in_ns_sub); + message1.addRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns3); + int removed = ResponseScrubber::scrubCrossSections(message1); + EXPECT_EQ(1, removed); + EXPECT_TRUE(message1.hasRRset(Message::SECTION_ANSWER, rrs_in_a_www)); + EXPECT_FALSE(message1.hasRRset(Message::SECTION_AUTHORITY, rrs_in_ns_sub)); + EXPECT_TRUE(message1.hasRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns3)); + + // A repeat of the test, this time with a mixture of incorrect and correct + // authorities. + Message message2(Message::RENDER); + message2.addQuestion(qu_in_a_www); + message2.addRRset(Message::SECTION_ANSWER, rrs_in_a_www); + message2.addRRset(Message::SECTION_AUTHORITY, rrs_in_ns_sub); + message2.addRRset(Message::SECTION_AUTHORITY, rrs_in_ns); + message2.addRRset(Message::SECTION_AUTHORITY, rrs_in_ns_sub2); + message2.addRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns3); + removed = ResponseScrubber::scrubCrossSections(message2); + EXPECT_EQ(2, removed); + EXPECT_TRUE(message2.hasRRset(Message::SECTION_ANSWER, rrs_in_a_www)); + EXPECT_FALSE(message2.hasRRset(Message::SECTION_AUTHORITY, rrs_in_ns_sub)); + EXPECT_TRUE(message2.hasRRset(Message::SECTION_AUTHORITY, rrs_in_ns)); + EXPECT_FALSE(message2.hasRRset(Message::SECTION_AUTHORITY, rrs_in_ns_sub2)); + EXPECT_TRUE(message2.hasRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns3)); +} + +// Test the main "scrub" method. This is a single to ensure that the +// combination of methods + +TEST_F(ResponseScrubberTest, All) { + MessagePtr mptr(new Message(Message::RENDER)); + + // Question is "www.example.com IN A" sent to a nameserver with the + // bailiwick of "example.com". + mptr->addQuestion(qu_in_a_www); + + // Answer section. + + // "www.example.com IN CNAME www.example.net" - should be kept + mptr->addRRset(Message::SECTION_ANSWER, rrs_in_cname_www); + + // "www.example.net IN A a.b.c.d" - should be removed, out of bailiwick + mptr->addRRset(Message::SECTION_ANSWER, rrs_in_a_wwwnet); + + // Authority section. + + // "example.net IN NS xxxx" - should be removed, out of bailiwick. + mptr->addRRset(Message::SECTION_AUTHORITY, rrs_in_ns_net); + + // "example.com IN NS xxx" - kept + mptr->addRRset(Message::SECTION_AUTHORITY, rrs_in_ns); + + // "com IN NS xxx" - removed, out of bailiwick + mptr->addRRset(Message::SECTION_AUTHORITY, rrs_in_ns_com); + + // "subdomain.example.com IN NS xxx" - removed, not a superdomain of the + // answer. + mptr->addRRset(Message::SECTION_AUTHORITY, rrs_in_ns_sub); + + // Additional section + + // "ns2.example.net IN A a.b.c.d" - removed, out of bailiwick + mptr->addRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns2); + + // "ns3.subdomain.example.com IN A a.b.c.d" - retained. + mptr->addRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns3); + + unsigned int removed = ResponseScrubber::scrub(mptr, bailiwick); + EXPECT_EQ(5, removed); + + EXPECT_TRUE(mptr->hasRRset(Message::SECTION_ANSWER, rrs_in_cname_www)); + EXPECT_FALSE(mptr->hasRRset(Message::SECTION_ANSWER, rrs_in_a_wwwnet)); + EXPECT_FALSE(mptr->hasRRset(Message::SECTION_AUTHORITY, rrs_in_ns_net)); + EXPECT_TRUE(mptr->hasRRset(Message::SECTION_AUTHORITY, rrs_in_ns)); + EXPECT_FALSE(mptr->hasRRset(Message::SECTION_AUTHORITY, rrs_in_ns_com)); + EXPECT_FALSE(mptr->hasRRset(Message::SECTION_AUTHORITY, rrs_in_ns_sub)); + EXPECT_FALSE(mptr->hasRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns2)); + EXPECT_TRUE(mptr->hasRRset(Message::SECTION_ADDITIONAL, rrs_in_a_ns3)); + +} +} // Anonymous namespace diff --git a/src/bin/stats/b10-stats.xml b/src/bin/stats/b10-stats.xml index 62051dfd150ea2f2063f336f98b8ae32f09744df..f622439a02e60139102de9ae90ed9ed1ba21b84e 100644 --- a/src/bin/stats/b10-stats.xml +++ b/src/bin/stats/b10-stats.xml @@ -89,7 +89,8 @@ /usr/local/share/bind10-devel/stats.spec — This is a spec file for b10-stats. It contains definitions of statistics items of BIND 10 and commands - received vi bindctl. + received via + bindctl1. diff --git a/src/bin/usermgr/b10-cmdctl-usermgr.py.in b/src/bin/usermgr/b10-cmdctl-usermgr.py.in index 645c05365e860a3ad830ec389162c8342a3a90b9..d62ad7200a824fa1ca455332e6711dd3ee329346 100644 --- a/src/bin/usermgr/b10-cmdctl-usermgr.py.in +++ b/src/bin/usermgr/b10-cmdctl-usermgr.py.in @@ -24,7 +24,7 @@ from hashlib import sha1 import csv import getpass import getopt -import sys +import sys; sys.path.append ('@@PYTHONPATH@@') import isc.util.process isc.util.process.rename() diff --git a/src/bin/xfrin/b10-xfrin.xml b/src/bin/xfrin/b10-xfrin.xml index 536ac83399d83cddee02834dccf14dbd98b13283..fdfe1ef3b00c3f93b384d1d97b985010fca00b29 100644 --- a/src/bin/xfrin/b10-xfrin.xml +++ b/src/bin/xfrin/b10-xfrin.xml @@ -63,7 +63,7 @@ - The Y1 prototype release only supports AXFR. IXFR is not implemented. + This prototype release only supports AXFR. IXFR is not implemented. diff --git a/src/bin/xfrout/tests/xfrout_test.py b/src/bin/xfrout/tests/xfrout_test.py index 2fb4463e99e178c753b5888d11bc085c03cc03e9..55a2e52c865afcf9a97d5d29299091e50f5a8ba5 100644 --- a/src/bin/xfrout/tests/xfrout_test.py +++ b/src/bin/xfrout/tests/xfrout_test.py @@ -121,6 +121,29 @@ class TestXfroutSession(unittest.TestCase): get_msg = self.sock.read_msg() self.assertEqual(get_msg.get_rcode().to_text(), "NXDOMAIN") + def test_send_message(self): + msg = self.getmsg() + msg.make_response() + # soa record data with different cases + soa_record = (4, 3, 'Example.com.', 'com.Example.', 3600, 'SOA', None, 'master.Example.com. admin.exAmple.com. 1234 3600 1800 2419200 7200') + rrset_soa = self.xfrsess._create_rrset_from_db_record(soa_record) + msg.add_rrset(Message.SECTION_ANSWER, rrset_soa) + self.xfrsess._send_message(self.sock, msg) + send_out_data = self.sock.readsent()[2:] + + # CASE_INSENSITIVE compression mode + render = MessageRenderer(); + render.set_length_limit(XFROUT_MAX_MESSAGE_SIZE) + msg.to_wire(render) + self.assertNotEqual(render.get_data(), send_out_data) + + # CASE_SENSITIVE compression mode + render.clear() + render.set_compress_mode(MessageRenderer.CASE_SENSITIVE) + render.set_length_limit(XFROUT_MAX_MESSAGE_SIZE) + msg.to_wire(render) + self.assertEqual(render.get_data(), send_out_data) + def test_clear_message(self): msg = self.getmsg() qid = msg.get_qid() diff --git a/src/bin/xfrout/xfrout.py.in b/src/bin/xfrout/xfrout.py.in index eb96b940c4c610b5166d915f3244ea6602bdde6f..a81964077d7683e59bd5dbe84e3bd36b0fa5e99e 100755 --- a/src/bin/xfrout/xfrout.py.in +++ b/src/bin/xfrout/xfrout.py.in @@ -170,6 +170,9 @@ class XfroutSession(BaseRequestHandler): def _send_message(self, sock_fd, msg): render = MessageRenderer() + # As defined in RFC5936 section3.4, perform case-preserving name + # compression for AXFR message. + render.set_compress_mode(MessageRenderer.CASE_SENSITIVE) render.set_length_limit(XFROUT_MAX_MESSAGE_SIZE) msg.to_wire(render) header_len = struct.pack('H', socket.htons(render.get_length())) diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am index 866ca6509121497c403a6c0be379d7f08085102c..d5486a0f7d7988fb6ee613016a04ad9956960a01 100644 --- a/src/lib/Makefile.am +++ b/src/lib/Makefile.am @@ -1,2 +1,2 @@ -SUBDIRS = exceptions dns cc config datasrc python xfr bench log asiolink \ - testutils nsas +SUBDIRS = exceptions dns cc config datasrc python xfr bench log \ + resolve nsas cache asiolink testutils diff --git a/src/lib/asiolink/Makefile.am b/src/lib/asiolink/Makefile.am index c8790394b5b7baf94ce20195ed90d37429a98d43..b3968f0661a95efe6b31bf3e6e2c52c2baafb2c9 100644 --- a/src/lib/asiolink/Makefile.am +++ b/src/lib/asiolink/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = . tests internal +SUBDIRS = . tests AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib AM_CPPFLAGS += $(BOOST_INCLUDES) @@ -12,15 +12,29 @@ CLEANFILES = *.gcno *.gcda # have some code fragments that would hit gcc's unused-parameter warning, # which would make the build fail with -Werror (our default setting). lib_LTLIBRARIES = libasiolink.la -libasiolink_la_SOURCES = asiolink.cc asiolink.h -libasiolink_la_SOURCES += iosocket.cc iosocket.h -libasiolink_la_SOURCES += iomessage.h -libasiolink_la_SOURCES += ioaddress.cc ioaddress.h -libasiolink_la_SOURCES += ioendpoint.cc ioendpoint.h -libasiolink_la_SOURCES += udpdns.cc internal/udpdns.h -libasiolink_la_SOURCES += tcpdns.cc internal/tcpdns.h -libasiolink_la_SOURCES += internal/coroutine.h -libasiolink_la_SOURCES += iofetch.cc internal/iofetch.h +libasiolink_la_SOURCES = asiolink.h +libasiolink_la_SOURCES += dns_answer.h +libasiolink_la_SOURCES += dns_lookup.h +libasiolink_la_SOURCES += dns_server.h +libasiolink_la_SOURCES += dns_service.h dns_service.cc +libasiolink_la_SOURCES += dummy_io_cb.h +libasiolink_la_SOURCES += interval_timer.h interval_timer.cc +libasiolink_la_SOURCES += io_address.h io_address.cc +libasiolink_la_SOURCES += io_asio_socket.h +libasiolink_la_SOURCES += io_endpoint.h io_endpoint.cc +libasiolink_la_SOURCES += io_error.h +libasiolink_la_SOURCES += io_fetch.h io_fetch.cc +libasiolink_la_SOURCES += io_message.h +libasiolink_la_SOURCES += io_service.h io_service.cc +libasiolink_la_SOURCES += io_socket.h io_socket.cc +libasiolink_la_SOURCES += recursive_query.h recursive_query.cc +libasiolink_la_SOURCES += simple_callback.h +libasiolink_la_SOURCES += tcp_endpoint.h +libasiolink_la_SOURCES += tcp_server.h tcp_server.cc +libasiolink_la_SOURCES += tcp_socket.h +libasiolink_la_SOURCES += udp_endpoint.h +libasiolink_la_SOURCES += udp_server.h udp_server.cc +libasiolink_la_SOURCES += udp_socket.h # Note: the ordering matters: -Wno-... must follow -Wextra (defined in # B10_CXXFLAGS) libasiolink_la_CXXFLAGS = $(AM_CXXFLAGS) @@ -33,3 +47,6 @@ libasiolink_la_CXXFLAGS += -Wno-error endif libasiolink_la_CPPFLAGS = $(AM_CPPFLAGS) libasiolink_la_LIBADD = $(top_builddir)/src/lib/log/liblog.la +libasiolink_la_LIBADD += $(top_builddir)/src/lib/resolve/libresolve.la +libasiolink_la_LIBADD += $(top_builddir)/src/lib/cache/libcache.la +libasiolink_la_LIBADD += $(top_builddir)/src/lib/nsas/libnsas.la diff --git a/src/lib/asiolink/README b/src/lib/asiolink/README index b0f6a7d97933b7214222f0e16c933d8a75449661..6bd1a7383c28a33bf384193fb387ded30c562a24 100644 --- a/src/lib/asiolink/README +++ b/src/lib/asiolink/README @@ -33,7 +33,7 @@ This is intended to simplify development a bit, since it allows the routines to be written in a straightfowrard step-step-step fashion rather than as a complex chain of separate handler functions. -Coroutine objects (i.e., UDPServer, TCPServer and UDPQuery) are objects +Coroutine objects (i.e., UDPServer, TCPServer and IOFetch) are objects with reenterable operator() members. When an instance of one of these classes is called as a function, it resumes at the position where it left off. Thus, a UDPServer can issue an asynchronous I/O call and specify @@ -101,3 +101,82 @@ when the answer has arrived. In simplified form, the DNSQuery routine is: Currently, DNSQuery is only implemented for UDP queries. In future work it will be necessary to write code to fall back to TCP when circumstances require it. + + +Upstream Fetches +================ +Upstream fetches (queries by the resolver on behalf of a client) are made +using a slightly-modified version of the pattern described above. + +Sockets +------- +First, it will be useful to understand the class hierarchy used in the +fetch logic: + + IOSocket + | + IOAsioSocket + | + +-----+-----+ + | | +UDPSocket TCPSocket + +IOSocket is a wrapper class for a socket and is used by the authoritative +server code. It is an abstract base class, providing little more that the ability to hold the socket and to return the protocol in use. + +Built on this is IOAsioSocket, which adds the open, close, asyncSend and +asyncReceive methods. This is a template class, which takes as template +argument the class of the object that will be used as the callback when the +asynchronous operation completes. This object can be of any type, but must +include an operator() method with the signature: + + operator()(asio::error_code ec, size_t length) + +... the two arguments being the status of the completed I/O operation and +the number of bytes transferred. (In the case of the open method, the second +argument will be zero.) + +Finally, the TCPSocket and UDPSocket classes provide the body of the +asynchronous operations. + +Fetch Sequence +-------------- +The fetch is implemented by the IOFetch class, which takes as argument the +protocol to use. The sequence is: + + REENTER: + render the question into a wire-format query packet + open() // Open socket and optionally connect + if (! synchronous) { + YIELD; + } + YIELD asyncSend(query) // Send query + do { + YIELD asyncReceive(response) // Read response + } while (! complete(response)) + close() // Drop connection and close socket + server->resume + +The open() method opens a socket for use. On TCP, it also makes a +connection to the remote end. So under UDP the operation will complete +immediately, but under TCP it could take a long time. One solution would be +for the open operation to post an event to the I/O queue; then both cases +could be regarded as being equivalent, with the completion being signalled +by the posting of the completion event. However UDP is the most common case +and that would involve extra overhead. So the open() returns a status +indicating whether the operation completed asynchronously. If it did, the +code yields back to the coroutine; if not the yield is bypassed. + +The asynchronous send is straightforward, invoking the underlying ASIO +function. (Note that the address/port is supplied to both the open() and +asyncSend() methods - it is used by the TCPSocket in open() and by the +UDPSocket in asyncSend().) + +The asyncReceive() method issues an asynchronous read and waits for completion. +The fetch object keeps track of the amount of data received so far and when +the receive completes it calls a method on the socket to determine if the +entire message has been received. (This will always be the case for UDP. On +TCP though, the message is preceded by a count field as several reads may be +required to read all the data.) The fetch loops until all the data is read. + +Finally, the socket is closed and the server called to resume operation. diff --git a/src/lib/asiolink/asiolink.cc b/src/lib/asiolink/asiolink.cc deleted file mode 100644 index 15235f323944237d2ec93bd8fe6f9cd32214b93a..0000000000000000000000000000000000000000 --- a/src/lib/asiolink/asiolink.cc +++ /dev/null @@ -1,680 +0,0 @@ -// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -#include - -#include // For rand(), temporary until better forwarding is done - -#include // for some IPC/network system calls -#include -#include - -#include -#include -#include -#include -#include - -#include - -#include -#include -#include - -#include -#include -#include -#include - -#include - - -using namespace asio; -using asio::ip::udp; -using asio::ip::tcp; - -using namespace std; -using namespace isc::dns; -using isc::log::dlog; -using namespace boost; - -// Is this something we can use in libdns++? -namespace { - class SectionInserter { - public: - SectionInserter(MessagePtr message, const Message::Section sect) : - message_(message), section_(sect) - {} - void operator()(const RRsetPtr rrset) { - message_->addRRset(section_, rrset, true); - } - MessagePtr message_; - const Message::Section section_; - }; - - - /// \brief Copies the parts relevant for a DNS answer to the - /// target message - /// - /// This adds all the RRsets in the answer, authority and - /// additional sections to the target, as well as the response - /// code - void copyAnswerMessage(const Message& source, MessagePtr target) { - target->setRcode(source.getRcode()); - - for_each(source.beginSection(Message::SECTION_ANSWER), - source.endSection(Message::SECTION_ANSWER), - SectionInserter(target, Message::SECTION_ANSWER)); - for_each(source.beginSection(Message::SECTION_AUTHORITY), - source.endSection(Message::SECTION_AUTHORITY), - SectionInserter(target, Message::SECTION_AUTHORITY)); - for_each(source.beginSection(Message::SECTION_ADDITIONAL), - source.endSection(Message::SECTION_ADDITIONAL), - SectionInserter(target, Message::SECTION_ADDITIONAL)); - } -} - -namespace asiolink { - -typedef pair addr_t; - -class IOServiceImpl { -private: - IOServiceImpl(const IOService& source); - IOServiceImpl& operator=(const IOService& source); -public: - /// \brief The constructor - IOServiceImpl() : - io_service_(), - work_(io_service_) - {}; - /// \brief The destructor. - ~IOServiceImpl() {}; - //@} - - /// \brief Start the underlying event loop. - /// - /// This method does not return control to the caller until - /// the \c stop() method is called via some handler. - void run() { io_service_.run(); }; - - /// \brief Run the underlying event loop for a single event. - /// - /// This method return control to the caller as soon as the - /// first handler has completed. (If no handlers are ready when - /// it is run, it will block until one is.) - void run_one() { io_service_.run_one();} ; - - /// \brief Stop the underlying event loop. - /// - /// This will return the control to the caller of the \c run() method. - void stop() { io_service_.stop();} ; - - /// \brief Return the native \c io_service object used in this wrapper. - /// - /// This is a short term work around to support other BIND 10 modules - /// that share the same \c io_service with the authoritative server. - /// It will eventually be removed once the wrapper interface is - /// generalized. - asio::io_service& get_io_service() { return io_service_; }; -private: - asio::io_service io_service_; - asio::io_service::work work_; -}; - -IOService::IOService() { - io_impl_ = new IOServiceImpl(); -} - -IOService::~IOService() { - delete io_impl_; -} - -void -IOService::run() { - io_impl_->run(); -} - -void -IOService::run_one() { - io_impl_->run_one(); -} - -void -IOService::stop() { - io_impl_->stop(); -} - -asio::io_service& -IOService::get_io_service() { - return (io_impl_->get_io_service()); -} - -class DNSServiceImpl { -public: - DNSServiceImpl(IOService& io_service, const char& port, - const ip::address* v4addr, const ip::address* v6addr, - SimpleCallback* checkin, DNSLookup* lookup, - DNSAnswer* answer); - - IOService& io_service_; - - typedef boost::shared_ptr UDPServerPtr; - typedef boost::shared_ptr TCPServerPtr; - typedef boost::shared_ptr DNSServerPtr; - vector servers_; - SimpleCallback *checkin_; - DNSLookup *lookup_; - DNSAnswer *answer_; - - void addServer(uint16_t port, const ip::address& address) { - try { - dlog(std::string("Initialize TCP server at ") + address.to_string() + ":" + boost::lexical_cast(port)); - TCPServerPtr tcpServer(new TCPServer(io_service_.get_io_service(), - address, port, checkin_, lookup_, answer_)); - (*tcpServer)(); - servers_.push_back(tcpServer); - dlog(std::string("Initialize UDP server at ") + address.to_string() + ":" + boost::lexical_cast(port)); - UDPServerPtr udpServer(new UDPServer(io_service_.get_io_service(), - address, port, checkin_, lookup_, answer_)); - (*udpServer)(); - servers_.push_back(udpServer); - } - catch (const asio::system_error& err) { - // We need to catch and convert any ASIO level exceptions. - // This can happen for unavailable address, binding a privilege port - // without the privilege, etc. - isc_throw(IOError, "Failed to initialize network servers: " << - err.what()); - } - } - void addServer(const char& port, const ip::address& address) { - uint16_t portnum; - try { - // XXX: SunStudio with stlport4 doesn't reject some invalid - // representation such as "-1" by lexical_cast, so - // we convert it into a signed integer of a larger size and perform - // range check ourselves. - const int32_t portnum32 = boost::lexical_cast(&port); - if (portnum32 < 0 || portnum32 > 65535) { - isc_throw(IOError, "Invalid port number '" << &port); - } - portnum = portnum32; - } catch (const boost::bad_lexical_cast& ex) { - isc_throw(IOError, "Invalid port number '" << &port << "': " << - ex.what()); - } - addServer(portnum, address); - } -}; - -DNSServiceImpl::DNSServiceImpl(IOService& io_service, - const char& port, - const ip::address* const v4addr, - const ip::address* const v6addr, - SimpleCallback* checkin, - DNSLookup* lookup, - DNSAnswer* answer) : - io_service_(io_service), - checkin_(checkin), - lookup_(lookup), - answer_(answer) -{ - - if (v4addr) { - addServer(port, *v4addr); - } - if (v6addr) { - addServer(port, *v6addr); - } -} - -DNSService::DNSService(IOService& io_service, - const char& port, const char& address, - SimpleCallback* checkin, - DNSLookup* lookup, - DNSAnswer* answer) : - impl_(new DNSServiceImpl(io_service, port, NULL, NULL, checkin, lookup, - answer)), io_service_(io_service) -{ - addServer(port, &address); -} - -DNSService::DNSService(IOService& io_service, - const char& port, - const bool use_ipv4, const bool use_ipv6, - SimpleCallback* checkin, - DNSLookup* lookup, - DNSAnswer* answer) : - impl_(NULL), io_service_(io_service) -{ - const ip::address v4addr_any = ip::address(ip::address_v4::any()); - const ip::address* const v4addrp = use_ipv4 ? &v4addr_any : NULL; - const ip::address v6addr_any = ip::address(ip::address_v6::any()); - const ip::address* const v6addrp = use_ipv6 ? &v6addr_any : NULL; - impl_ = new DNSServiceImpl(io_service, port, v4addrp, v6addrp, checkin, lookup, answer); -} - -DNSService::DNSService(IOService& io_service, SimpleCallback* checkin, - DNSLookup* lookup, DNSAnswer *answer) : - impl_(new DNSServiceImpl(io_service, *"0", NULL, NULL, checkin, lookup, - answer)), io_service_(io_service) -{ -} - -DNSService::~DNSService() { - delete impl_; -} - -namespace { - -typedef std::vector > AddressVector; - -} - -RecursiveQuery::RecursiveQuery(DNSService& dns_service, - const AddressVector& upstream, - const AddressVector& upstream_root, - int timeout, unsigned retries) : - dns_service_(dns_service), upstream_(new AddressVector(upstream)), - upstream_root_(new AddressVector(upstream_root)), - timeout_(timeout), retries_(retries) -{} - -namespace { - -ip::address -convertAddr(const string& address) { - error_code err; - ip::address addr = ip::address::from_string(address, err); - if (err) { - isc_throw(IOError, "Invalid IP address '" << &address << "': " - << err.message()); - } - return (addr); -} - -} - -void -DNSService::addServer(const char& port, const string& address) { - impl_->addServer(port, convertAddr(address)); -} - -void -DNSService::addServer(uint16_t port, const string& address) { - impl_->addServer(port, convertAddr(address)); -} - -void -DNSService::clearServers() { - // FIXME: This does not work, it does not close the socket. - // How is it done? - impl_->servers_.clear(); -} - -namespace { - -/* - * This is a query in progress. When a new query is made, this one holds - * the context information about it, like how many times we are allowed - * to retry on failure, what to do when we succeed, etc. - * - * Used by RecursiveQuery::sendQuery. - */ -class RunningQuery : public UDPQuery::Callback { -private: - // The io service to handle async calls - asio::io_service& io_; - - // Info for (re)sending the query (the question and destination) - Question question_; - - // This is where we build and store our final answer - MessagePtr answer_message_; - - // currently we use upstream as the current list of NS records - // we should differentiate between forwarding and resolving - shared_ptr upstream_; - - // root servers...just copied over to the zone_servers_ - shared_ptr upstream_root_; - - // Buffer to store the result. - OutputBufferPtr buffer_; - - // Server to notify when we succeed or fail - shared_ptr server_; - - /* - * TODO Do something more clever with timeouts. In the long term, some - * computation of average RTT, increase with each retry, etc. - */ - // Timeout information - int timeout_; - unsigned retries_; - - // normal query state - - // if we change this to running and add a sent, we can do - // decoupled timeouts i think - bool done; - - // Not using NSAS at this moment, so we keep a list - // of 'current' zone servers - vector zone_servers_; - - // Update the question that will be sent to the server - void setQuestion(const Question& new_question) { - question_ = new_question; - } - - // (re)send the query to the server. - void send() { - const int uc = upstream_->size(); - const int zs = zone_servers_.size(); - buffer_->clear(); - if (uc > 0) { - int serverIndex = rand() % uc; - dlog("Sending upstream query (" + question_.toText() + - ") to " + upstream_->at(serverIndex).first); - UDPQuery query(io_, question_, - upstream_->at(serverIndex).first, - upstream_->at(serverIndex).second, buffer_, this, - timeout_); - io_.post(query); - } else if (zs > 0) { - int serverIndex = rand() % zs; - dlog("Sending query to zone server (" + question_.toText() + - ") to " + zone_servers_.at(serverIndex).first); - UDPQuery query(io_, question_, - zone_servers_.at(serverIndex).first, - zone_servers_.at(serverIndex).second, buffer_, this, - timeout_); - io_.post(query); - } else { - dlog("Error, no upstream servers to send to."); - } - } - - // This function is called by operator() if there is an actual - // answer from a server and we are in recursive mode - // depending on the contents, we go on recursing or return - // - // Note that the footprint may change as this function may - // need to append data to the answer we are building later. - // - // returns true if we are done - // returns false if we are not done - bool handleRecursiveAnswer(const Message& incoming) { - //temporary code to grab TC enabled responses - if(incoming.getHeaderFlag(Message::HEADERFLAG_TC)) { - //TC (truncated) bit is set, which means we need to use TCP - // need to check if TCP conn already open (RFC 5966) - } - if (incoming.getRRCount(Message::SECTION_ANSWER) > 0) { - dlog("Got final result, copying answer."); - copyAnswerMessage(incoming, answer_message_); - return true; - } else { - dlog("Got delegation, continuing"); - // ok we need to do some more processing. - // the ns list should contain all nameservers - // while the additional may contain addresses for - // them. - // this needs to tie into NSAS of course - // for this very first mockup, hope there is an - // address in additional and just use that - - // send query to the addresses in the delegation - bool found_ns_address = false; - zone_servers_.clear(); - - for (RRsetIterator rrsi = incoming.beginSection(Message::SECTION_ADDITIONAL); - rrsi != incoming.endSection(Message::SECTION_ADDITIONAL) && !found_ns_address; - rrsi++) { - ConstRRsetPtr rrs = *rrsi; - if (rrs->getType() == RRType::A()) { - // found address - RdataIteratorPtr rdi = rrs->getRdataIterator(); - // just use the first for now - if (!rdi->isLast()) { - std::string addr_str = rdi->getCurrent().toText(); - dlog("[XX] first address found: " + addr_str); - // now we have one address, simply - // resend that exact same query - // to that address and yield, when it - // returns, loop again. - - // should use NSAS - zone_servers_.push_back(addr_t(addr_str, 53)); - found_ns_address = true; - } - } - } - if (found_ns_address) { - // next resolver round - send(); - return false; - } else { - dlog("[XX] no ready-made addresses in additional. need nsas."); - // this will result in answering with the delegation. oh well - copyAnswerMessage(incoming, answer_message_); - return true; - } - } - } - - -public: - RunningQuery(asio::io_service& io, const Question &question, - MessagePtr answer_message, shared_ptr upstream, - shared_ptr upstream_root, - OutputBufferPtr buffer, DNSServer* server, int timeout, - unsigned retries) : - io_(io), - question_(question), - answer_message_(answer_message), - upstream_(upstream), - upstream_root_(upstream_root), - buffer_(buffer), - server_(server->clone()), - timeout_(timeout), - retries_(retries), - zone_servers_() - { - dlog("Started a new RunningQuery"); - done = false; - - // should use NSAS for root servers - // Adding root servers if not a forwarder - if (upstream_->empty()) { - if (upstream_root_->empty()) { //if no root ips given, use this - zone_servers_.push_back(addr_t("192.5.5.241", 53)); - } - else - { - //copy the list - dlog("Size is " + - boost::lexical_cast(upstream_root_->size()) + - "\n"); - //Use BOOST_FOREACH here? Is it faster? - for(AddressVector::iterator it = upstream_root_->begin(); - it < upstream_root_->end(); it++) { - zone_servers_.push_back(addr_t(it->first,it->second)); - dlog("Put " + zone_servers_.back().first + "into root list\n"); - } - } - } - send(); - } - - - // This function is used as callback from DNSQuery. - virtual void operator()(UDPQuery::Result result) { - // XXX is this the place for TCP retry? - if (result != UDPQuery::TIME_OUT) { - // we got an answer - Message incoming(Message::PARSE); - InputBuffer ibuf(buffer_->getData(), buffer_->getLength()); - incoming.fromWire(ibuf); - - if (upstream_->size() == 0 && - incoming.getRcode() == Rcode::NOERROR()) { - done = handleRecursiveAnswer(incoming); - } else { - copyAnswerMessage(incoming, answer_message_); - done = true; - } - - if (done) { - server_->resume(result == UDPQuery::SUCCESS); - delete this; - } - } else if (retries_--) { - // We timed out, but we have some retries, so send again - dlog("Timeout, resending query"); - send(); - } else { - // out of retries, give up for now - server_->resume(false); - delete this; - } - } -}; - -} - -void -RecursiveQuery::sendQuery(const Question& question, - MessagePtr answer_message, - OutputBufferPtr buffer, - DNSServer* server) -{ - // XXX: eventually we will need to be able to determine whether - // the message should be sent via TCP or UDP, or sent initially via - // UDP and then fall back to TCP on failure, but for the moment - // we're only going to handle UDP. - asio::io_service& io = dns_service_.get_io_service(); - // It will delete itself when it is done - new RunningQuery(io, question, answer_message, upstream_, upstream_root_, - buffer, server, timeout_, retries_); -} - -class IntervalTimerImpl { -private: - // prohibit copy - IntervalTimerImpl(const IntervalTimerImpl& source); - IntervalTimerImpl& operator=(const IntervalTimerImpl& source); -public: - IntervalTimerImpl(IOService& io_service); - ~IntervalTimerImpl(); - void setupTimer(const IntervalTimer::Callback& cbfunc, - const uint32_t interval); - void callback(const asio::error_code& error); - void cancel() { - timer_.cancel(); - interval_ = 0; - } - uint32_t getInterval() const { return (interval_); } -private: - // a function to update timer_ when it expires - void updateTimer(); - // a function to call back when timer_ expires - IntervalTimer::Callback cbfunc_; - // interval in seconds - uint32_t interval_; - // asio timer - asio::deadline_timer timer_; -}; - -IntervalTimerImpl::IntervalTimerImpl(IOService& io_service) : - interval_(0), timer_(io_service.get_io_service()) -{} - -IntervalTimerImpl::~IntervalTimerImpl() -{} - -void -IntervalTimerImpl::setupTimer(const IntervalTimer::Callback& cbfunc, - const uint32_t interval) -{ - // Interval should not be 0. - if (interval == 0) { - isc_throw(isc::BadValue, "Interval should not be 0"); - } - // Call back function should not be empty. - if (cbfunc.empty()) { - isc_throw(isc::InvalidParameter, "Callback function is empty"); - } - cbfunc_ = cbfunc; - interval_ = interval; - // Set initial expire time. - // At this point the timer is not running yet and will not expire. - // After calling IOService::run(), the timer will expire. - updateTimer(); - return; -} - -void -IntervalTimerImpl::updateTimer() { - if (interval_ == 0) { - // timer has been canceled. Do nothing. - return; - } - try { - // Update expire time to (current time + interval_). - timer_.expires_from_now(boost::posix_time::seconds(interval_)); - } catch (const asio::system_error& e) { - isc_throw(isc::Unexpected, "Failed to update timer"); - } - // Reset timer. - timer_.async_wait(boost::bind(&IntervalTimerImpl::callback, this, _1)); -} - -void -IntervalTimerImpl::callback(const asio::error_code& cancelled) { - // Do not call cbfunc_ in case the timer was cancelled. - // The timer will be canelled in the destructor of asio::deadline_timer. - if (!cancelled) { - cbfunc_(); - // Set next expire time. - updateTimer(); - } -} - -IntervalTimer::IntervalTimer(IOService& io_service) { - impl_ = new IntervalTimerImpl(io_service); -} - -IntervalTimer::~IntervalTimer() { - delete impl_; -} - -void -IntervalTimer::setupTimer(const Callback& cbfunc, const uint32_t interval) { - return (impl_->setupTimer(cbfunc, interval)); -} - -void -IntervalTimer::cancel() { - impl_->cancel(); -} - -uint32_t -IntervalTimer::getInterval() const { - return (impl_->getInterval()); -} - -} diff --git a/src/lib/asiolink/asiolink.h b/src/lib/asiolink/asiolink.h index 8bbd80d3440ffaa0d8ffd1bf31593a344fb52ce2..03951ae9df8db83b1675817aee5faa117d033ebb 100644 --- a/src/lib/asiolink/asiolink.h +++ b/src/lib/asiolink/asiolink.h @@ -18,31 +18,21 @@ // IMPORTANT NOTE: only very few ASIO headers files can be included in // this file. In particular, asio.hpp should never be included here. // See the description of the namespace below. -#include // for some network system calls -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include - -#include -#include -#include -#include - -namespace asio { -// forward declaration for IOService::get_io_service() below -class io_service; -} +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include /// \namespace asiolink /// \brief A wrapper interface for the ASIO library. @@ -94,605 +84,6 @@ class io_service; /// the placeholder of callback handlers: /// http://think-async.com/Asio/asio-1.3.1/doc/asio/reference/asio_handler_allocate.html -namespace asiolink { -class DNSServiceImpl; -struct IOServiceImpl; -struct IntervalTimerImpl; - -/// \brief An exception that is thrown if an error occurs within the IO -/// module. This is mainly intended to be a wrapper exception class for -/// ASIO specific exceptions. -class IOError : public isc::Exception { -public: - IOError(const char* file, size_t line, const char* what) : - isc::Exception(file, line, what) {} -}; - -/// \brief Forward declarations for classes used below -class SimpleCallback; -class DNSLookup; -class DNSAnswer; - -/// \brief The \c IOService class is a wrapper for the ASIO \c io_service -/// class. -/// -class IOService { - /// - /// \name Constructors and Destructor - /// - /// Note: The copy constructor and the assignment operator are - /// intentionally defined as private, making this class non-copyable. - //@{ -private: - IOService(const IOService& source); - IOService& operator=(const IOService& source); -public: - /// \brief The constructor - IOService(); - /// \brief The destructor. - ~IOService(); - //@} - - /// \brief Start the underlying event loop. - /// - /// This method does not return control to the caller until - /// the \c stop() method is called via some handler. - void run(); - - /// \brief Run the underlying event loop for a single event. - /// - /// This method return control to the caller as soon as the - /// first handler has completed. (If no handlers are ready when - /// it is run, it will block until one is.) - void run_one(); - - /// \brief Stop the underlying event loop. - /// - /// This will return the control to the caller of the \c run() method. - void stop(); - - /// \brief Return the native \c io_service object used in this wrapper. - /// - /// This is a short term work around to support other BIND 10 modules - /// that share the same \c io_service with the authoritative server. - /// It will eventually be removed once the wrapper interface is - /// generalized. - asio::io_service& get_io_service(); - -private: - IOServiceImpl* io_impl_; -}; - -/// -/// DNSService is the service that handles DNS queries and answers with -/// a given IOService. This class is mainly intended to hold all the -/// logic that is shared between the authoritative and the recursive -/// server implementations. As such, it handles asio, including config -/// updates (through the 'Checkinprovider'), and listening sockets. -/// -class DNSService { - /// - /// \name Constructors and Destructor - /// - /// Note: The copy constructor and the assignment operator are - /// intentionally defined as private, making this class non-copyable. - //@{ -private: - DNSService(const DNSService& source); - DNSService& operator=(const DNSService& source); - -public: - /// \brief The constructor with a specific IP address and port on which - /// the services listen on. - /// - /// \param io_service The IOService to work with - /// \param port the port to listen on - /// \param address the IP address to listen on - /// \param checkin Provider for cc-channel events (see \c SimpleCallback) - /// \param lookup The lookup provider (see \c DNSLookup) - /// \param answer The answer provider (see \c DNSAnswer) - DNSService(IOService& io_service, const char& port, - const char& address, SimpleCallback* checkin, - DNSLookup* lookup, DNSAnswer* answer); - /// \brief The constructor with a specific port on which the services - /// listen on. - /// - /// It effectively listens on "any" IPv4 and/or IPv6 addresses. - /// IPv4/IPv6 services will be available if and only if \c use_ipv4 - /// or \c use_ipv6 is \c true, respectively. - /// - /// \param io_service The IOService to work with - /// \param port the port to listen on - /// \param ipv4 If true, listen on ipv4 'any' - /// \param ipv6 If true, listen on ipv6 'any' - /// \param checkin Provider for cc-channel events (see \c SimpleCallback) - /// \param lookup The lookup provider (see \c DNSLookup) - /// \param answer The answer provider (see \c DNSAnswer) - DNSService(IOService& io_service, const char& port, - const bool use_ipv4, const bool use_ipv6, - SimpleCallback* checkin, DNSLookup* lookup, - DNSAnswer* answer); - /// \brief The constructor without any servers. - /// - /// Use addServer() to add some servers. - DNSService(IOService& io_service, SimpleCallback* checkin, - DNSLookup* lookup, DNSAnswer* answer); - /// \brief The destructor. - ~DNSService(); - //@} - - /// \brief Add another server to the service - void addServer(uint16_t port, const std::string &address); - void addServer(const char &port, const std::string &address); - /// \brief Remove all servers from the service - void clearServers(); - - /// \brief Return the native \c io_service object used in this wrapper. - /// - /// This is a short term work around to support other BIND 10 modules - /// that share the same \c io_service with the authoritative server. - /// It will eventually be removed once the wrapper interface is - /// generalized. - asio::io_service& get_io_service() { return io_service_.get_io_service(); } -private: - DNSServiceImpl* impl_; - IOService& io_service_; -}; - -/// \brief The \c DNSServer class is a wrapper (and base class) for -/// classes which provide DNS server functionality. -/// -/// The classes derived from this one, \c TCPServer and \c UDPServer, -/// act as the interface layer between clients sending queries, and -/// functions defined elsewhere that provide answers to those queries. -/// Those functions are described in more detail below under -/// \c SimpleCallback, \c DNSLookup, and \c DNSAnswer. -/// -/// Notes to developers: -/// When constructed, this class (and its derived classes) will have its -/// "self_" member set to point to "this". Objects of this class (as -/// instantiated through a base class) are sometimes passed by -/// reference (as this superclass); calls to methods in the base -/// class are then rerouted via this pointer to methods in the derived -/// class. This allows code from outside asiolink, with no specific -/// knowledge of \c TCPServer or \c UDPServer, to access their methods. -/// -/// This class is both assignable and copy-constructable. Its subclasses -/// use the "stackless coroutine" pattern, meaning that it will copy itself -/// when "forking", and that instances will be posted as ASIO handler -/// objects, which are always copied. -/// -/// Because these objects are frequently copied, it is recommended -/// that derived classes be kept small to reduce copy overhead. -class DNSServer { -protected: - /// - /// \name Constructors and destructors - /// - /// This is intentionally defined as \c protected, as this base class - /// should never be instantiated except as part of a derived class. - //@{ - DNSServer() : self_(this) {} -public: - /// \brief The destructor - virtual ~DNSServer() {} - //@} - - /// - /// \name Class methods - /// - /// These methods all make their calls indirectly via the "self_" - /// pointer, ensuring that the functions ultimately invoked will be - /// the ones in the derived class. This makes it possible to pass - /// instances of derived classes as references to this base class - /// without losing access to derived class data. - /// - //@{ - /// \brief The funtion operator - virtual void operator()(asio::error_code ec = asio::error_code(), - size_t length = 0) - { - (*self_)(ec, length); - } - - /// \brief Resume processing of the server coroutine after an - /// asynchronous call (e.g., to the DNS Lookup provider) has completed. - /// - /// \param done If true, this signals the system there is an answer - /// to return. - virtual void resume(const bool done) { self_->resume(done); } - - /// \brief Indicate whether the server is able to send an answer - /// to a query. - /// - /// This is presently used only for testing purposes. - virtual bool hasAnswer() { return (self_->hasAnswer()); } - - /// \brief Returns the current value of the 'coroutine' object - /// - /// This is a temporary method, intended to be used for debugging - /// purposes during development and removed later. It allows - /// callers from outside the coroutine object to retrieve information - /// about its current state. - /// - /// \return The value of the 'coroutine' object - virtual int value() { return (self_->value()); } - - /// \brief Returns a pointer to a clone of this DNSServer object. - /// - /// When a \c DNSServer object is copied or assigned, the result will - /// normally be another \c DNSServer object containing a copy - /// of the original "self_" pointer. Calling clone() guarantees - /// that the underlying object is also correctly copied. - /// - /// \return A deep copy of this DNSServer object - virtual DNSServer* clone() { return (self_->clone()); } - //@} - -protected: - /// \brief Lookup handler object. - /// - /// This is a protected class; it can only be instantiated - /// from within a derived class of \c DNSServer. - /// - /// A server object that has received a query creates an instance - /// of this class and scheudles it on the ASIO service queue - /// using asio::io_service::post(). When the handler executes, it - /// calls the asyncLookup() method in the server object to start a - /// DNS lookup. When the lookup is complete, the server object is - /// scheduled to resume, again using io_service::post(). - /// - /// Note that the calling object is copied into the handler object, - /// not referenced. This is because, once the calling object yields - /// control to the handler, it falls out of scope and may disappear - template - class AsyncLookup { - public: - AsyncLookup(T& caller) : caller_(caller) {} - void operator()() { caller_.asyncLookup(); } - private: - T caller_; - }; - - /// \brief Carries out a DNS lookup. - /// - /// This function calls the \c DNSLookup object specified by the - /// DNS server when the \c IOService was created, passing along - /// the details of the query and a pointer back to the current - /// server object. It is called asynchronously via the AsyncLookup - /// handler class. - virtual void asyncLookup() { self_->asyncLookup(); } - -private: - DNSServer* self_; -}; - -/// \brief The \c DNSLookup class is an abstract base class for a DNS -/// Lookup provider function. -/// -/// Specific derived class implementations are hidden within the -/// implementation. Instances of the derived classes can be called -/// as functions via the operator() interface. Pointers to these -/// instances can then be provided to the \c IOService class -/// via its constructor. -/// -/// A DNS Lookup provider function obtains the data needed to answer -/// a DNS query (e.g., from authoritative data source, cache, or upstream -/// query). After it has run, the OutputBuffer object passed to it -/// should contain the answer to the query, in an internal representation. -class DNSLookup { - /// - /// \name Constructors and Destructor - /// - /// Note: The copy constructor and the assignment operator are - /// intentionally defined as private, making this class non-copyable. - //@{ -private: - DNSLookup(const DNSLookup& source); - DNSLookup& operator=(const DNSLookup& source); -protected: - /// \brief The default constructor. - /// - /// This is intentionally defined as \c protected as this base class - /// should never be instantiated (except as part of a derived class). - DNSLookup() : self_(this) {} -public: - /// \brief The destructor - virtual ~DNSLookup() {} - //@} - /// \brief The function operator - /// - /// This makes its call indirectly via the "self" pointer, ensuring - /// that the function ultimately invoked will be the one in the derived - /// class. - /// - /// \param io_message The event message to handle - /// \param message The DNS MessagePtr that needs handling - /// \param buffer The final answer is put here - /// \param DNSServer DNSServer object to use - virtual void operator()(const IOMessage& io_message, - isc::dns::MessagePtr message, - isc::dns::MessagePtr answer_message, - isc::dns::OutputBufferPtr buffer, - DNSServer* server) const - { - (*self_)(io_message, message, answer_message, buffer, server); - } -private: - DNSLookup* self_; -}; - -/// \brief The \c DNSAnswer class is an abstract base class for a DNS -/// Answer provider function. -/// -/// Specific derived class implementations are hidden within the -/// implementation. Instances of the derived classes can be called -/// as functions via the operator() interface. Pointers to these -/// instances can then be provided to the \c IOService class -/// via its constructor. -/// -/// A DNS Answer provider function takes answer data that has been obtained -/// from a DNS Lookup provider functon and readies it to be sent to the -/// client. After it has run, the OutputBuffer object passed to it should -/// contain the answer to the query rendered into wire format. -class DNSAnswer { - /// - /// \name Constructors and Destructor - /// - /// Note: The copy constructor and the assignment operator are - /// intentionally defined as private, making this class non-copyable. - //@{ -private: - DNSAnswer(const DNSAnswer& source); - DNSAnswer& operator=(const DNSAnswer& source); -protected: - /// \brief The default constructor. - /// - /// This is intentionally defined as \c protected as this base class - /// should never be instantiated (except as part of a derived class). - DNSAnswer() {} -public: - /// \brief The destructor - virtual ~DNSAnswer() {} - //@} - /// \brief The function operator - /// - /// This makes its call indirectly via the "self" pointer, ensuring - /// that the function ultimately invoked will be the one in the derived - /// class. - /// - /// \param io_message The event message to handle - /// \param message The DNS MessagePtr that needs handling - /// \param buffer The result is put here - virtual void operator()(const IOMessage& io_message, - isc::dns::MessagePtr message, - isc::dns::MessagePtr answer_message, - isc::dns::OutputBufferPtr buffer) const = 0; -}; - -/// \brief The \c SimpleCallback class is an abstract base class for a -/// simple callback function with the signature: -/// -/// void simpleCallback(const IOMessage& io_message) const; -/// -/// Specific derived class implementations are hidden within the -/// implementation. Instances of the derived classes can be called -/// as functions via the operator() interface. Pointers to these -/// instances can then be provided to the \c IOService class -/// via its constructor. -/// -/// The \c SimpleCallback is expected to be used for basic, generic -/// tasks such as checking for configuration changes. It may also be -/// used for testing purposes. -class SimpleCallback { - /// - /// \name Constructors and Destructor - /// - /// Note: The copy constructor and the assignment operator are - /// intentionally defined as private, making this class non-copyable. - //@{ -private: - SimpleCallback(const SimpleCallback& source); - SimpleCallback& operator=(const SimpleCallback& source); -protected: - /// \brief The default constructor. - /// - /// This is intentionally defined as \c protected as this base class - /// should never be instantiated (except as part of a derived class). - SimpleCallback() : self_(this) {} -public: - /// \brief The destructor - virtual ~SimpleCallback() {} - /// \brief The function operator - //@} - /// - /// This makes its call indirectly via the "self" pointer, ensuring - /// that the function ultimately invoked will be the one in the derived - /// class. - /// - /// \param io_message The event message to handle - virtual void operator()(const IOMessage& io_message) const { - (*self_)(io_message); - } -private: - SimpleCallback* self_; -}; - -/// \brief The \c RecursiveQuery class provides a layer of abstraction around -/// the ASIO code that carries out an upstream query. -/// -/// This design is very preliminary; currently it is only capable of -/// handling simple forward requests to a single resolver. -class RecursiveQuery { - /// - /// \name Constructors - /// - //@{ -public: - /// \brief Constructor for use when acting as a forwarder - /// - /// This is currently the only way to construct \c RecursiveQuery - /// object. The addresses of the forward nameservers is specified, - /// and every upstream query will be sent to one random address. - /// \param dns_service The DNS Service to perform the recursive - /// query on. - /// \param upstream Addresses and ports of the upstream servers - /// to forward queries to. - /// \param upstream_root Addresses and ports of the root servers - /// to use when resolving. - /// \param timeout How long to timeout the query, in ms - /// -1 means never timeout (but do not use that). - /// TODO: This should be computed somehow dynamically in future - /// \param retries how many times we try again (0 means just send and - /// and return if it returs). - RecursiveQuery(DNSService& dns_service, - const std::vector >& - upstream, - const std::vector >& - upstream_root, - int timeout = -1, unsigned retries = 0); - //@} - - /// \brief Initiates an upstream query in the \c RecursiveQuery object. - /// - /// When sendQuery() is called, a message is sent asynchronously to - /// the upstream name server. When a reply arrives, 'server' - /// is placed on the ASIO service queue via io_service::post(), so - /// that the original \c DNSServer objct can resume processing. - /// - /// \param question The question being answered - /// \param buffer An output buffer into which the response can be copied - /// \param server A pointer to the \c DNSServer object handling the client - void sendQuery(const isc::dns::Question& question, - isc::dns::MessagePtr answer_message, - isc::dns::OutputBufferPtr buffer, - DNSServer* server); -private: - DNSService& dns_service_; - boost::shared_ptr > > - upstream_; - boost::shared_ptr > > - upstream_root_; - int timeout_; - unsigned retries_; -}; - -/// \brief The \c IntervalTimer class is a wrapper for the ASIO -/// \c asio::deadline_timer class. -/// -/// This class is implemented to use \c asio::deadline_timer as -/// interval timer. -/// -/// \c setupTimer() sets a timer to expire on (now + interval) and -/// a call back function. -/// -/// \c IntervalTimerImpl::callback() is called by the timer when -/// it expires. -/// -/// The function calls the call back function set by \c setupTimer() -/// and updates the timer to expire in (now + interval) seconds. -/// The type of call back function is \c void(void). -/// -/// The call back function will not be called if the instance of this -/// class is destructed before the timer is expired. -/// -/// Note: Destruction of an instance of this class while call back -/// is pending causes throwing an exception from \c IOService. -/// -/// Sample code: -/// \code -/// void function_to_call_back() { -/// // this function will be called periodically -/// } -/// int interval_in_seconds = 1; -/// IOService io_service; -/// -/// IntervalTimer intervalTimer(io_service); -/// intervalTimer.setupTimer(function_to_call_back, interval_in_seconds); -/// io_service.run(); -/// \endcode -/// -class IntervalTimer { -public: - /// \name The type of timer callback function - typedef boost::function Callback; - - /// - /// \name Constructors and Destructor - /// - /// Note: The copy constructor and the assignment operator are - /// intentionally defined as private, making this class non-copyable. - //@{ -private: - IntervalTimer(const IntervalTimer& source); - IntervalTimer& operator=(const IntervalTimer& source); -public: - /// \brief The constructor with \c IOService. - /// - /// This constructor may throw a standard exception if - /// memory allocation fails inside the method. - /// This constructor may also throw \c asio::system_error. - /// - /// \param io_service A reference to an instance of IOService - /// - IntervalTimer(IOService& io_service); - - /// \brief The destructor. - /// - /// This destructor never throws an exception. - /// - /// On the destruction of this class the timer will be canceled - /// inside \c asio::deadline_timer. - /// - ~IntervalTimer(); - //@} - - /// \brief Register timer callback function and interval. - /// - /// This function sets callback function and interval in seconds. - /// Timer will actually start after calling \c IOService::run(). - /// - /// \param cbfunc A reference to a function \c void(void) to call back - /// when the timer is expired (should not be an empty functor) - /// \param interval Interval in seconds (greater than 0) - /// - /// Note: IntervalTimer will not pass \c asio::error_code to - /// call back function. In case the timer is cancelled, the function - /// will not be called. - /// - /// \throw isc::InvalidParameter cbfunc is empty - /// \throw isc::BadValue interval is 0 - /// \throw isc::Unexpected ASIO library error - /// - void setupTimer(const Callback& cbfunc, const uint32_t interval); - - /// Cancel the timer. - /// - /// If the timer has been set up, this method cancels any asynchronous - /// events waiting on the timer and stops the timer itself. - /// If the timer has already been canceled, this method effectively does - /// nothing. - /// - /// This method never throws an exception. - void cancel(); - - /// Return the timer interval. - /// - /// This method returns the timer interval in seconds if it's running; - /// if the timer has been canceled it returns 0. - /// - /// This method never throws an exception. - /// - /// Note: We may want to change the granularity of the timer to - /// milliseconds or even finer. If and when this happens the semantics - /// of the return value of this method will be changed accordingly. - uint32_t getInterval() const; - -private: - IntervalTimerImpl* impl_; -}; - -} // asiolink #endif // __ASIOLINK_H // Local Variables: diff --git a/src/lib/asiolink/dns_answer.h b/src/lib/asiolink/dns_answer.h new file mode 100644 index 0000000000000000000000000000000000000000..84e1f6fd954c6343837b9b6612b26e39c55e33ea --- /dev/null +++ b/src/lib/asiolink/dns_answer.h @@ -0,0 +1,73 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __ASIOLINK_DNS_ANSWER_H +#define __ASIOLINK_DNS_ANSWER_H 1 + +#include + +namespace asiolink { + +/// \brief The \c DNSAnswer class is an abstract base class for a DNS +/// Answer provider function. +/// +/// Specific derived class implementations are hidden within the +/// implementation. Instances of the derived classes can be called +/// as functions via the operator() interface. Pointers to these +/// instances can then be provided to the \c IOService class +/// via its constructor. +/// +/// A DNS Answer provider function takes answer data that has been obtained +/// from a DNS Lookup provider functon and readies it to be sent to the +/// client. After it has run, the OutputBuffer object passed to it should +/// contain the answer to the query rendered into wire format. +class DNSAnswer { + /// + /// \name Constructors and Destructor + /// + /// Note: The copy constructor and the assignment operator are + /// intentionally defined as private, making this class non-copyable. + //@{ +private: + DNSAnswer(const DNSAnswer& source); + DNSAnswer& operator=(const DNSAnswer& source); +protected: + /// \brief The default constructor. + /// + /// This is intentionally defined as \c protected as this base class + /// should never be instantiated (except as part of a derived class). + DNSAnswer() {} +public: + /// \brief The destructor + virtual ~DNSAnswer() {} + //@} + /// \brief The function operator + /// + /// This makes its call indirectly via the "self" pointer, ensuring + /// that the function ultimately invoked will be the one in the derived + /// class. + /// + /// \param io_message The event message to handle + /// \param query_message The DNS MessagePtr of the original query + /// \param answer_message The DNS MessagePtr of the answer we are + /// building + /// \param buffer Intermediate data results are put here + virtual void operator()(const IOMessage& io_message, + isc::dns::MessagePtr query_message, + isc::dns::MessagePtr answer_message, + isc::dns::OutputBufferPtr buffer) const = 0; +}; + +} // namespace asiolink +#endif // __ASIOLINK_DNS_ANSWER_H diff --git a/src/lib/asiolink/dns_lookup.h b/src/lib/asiolink/dns_lookup.h new file mode 100644 index 0000000000000000000000000000000000000000..078885397a449ddeaf2b162e7974be49e0f76b51 --- /dev/null +++ b/src/lib/asiolink/dns_lookup.h @@ -0,0 +1,81 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __ASIOLINK_DNS_LOOKUP_H +#define __ASIOLINK_DNS_LOOKUP_H 1 + +#include +#include +#include +#include + +namespace asiolink { + +/// \brief The \c DNSLookup class is an abstract base class for a DNS +/// Lookup provider function. +/// +/// Specific derived class implementations are hidden within the +/// implementation. Instances of the derived classes can be called +/// as functions via the operator() interface. Pointers to these +/// instances can then be provided to the \c IOService class +/// via its constructor. +/// +/// A DNS Lookup provider function obtains the data needed to answer +/// a DNS query (e.g., from authoritative data source, cache, or upstream +/// query). After it has run, the OutputBuffer object passed to it +/// should contain the answer to the query, in an internal representation. +class DNSLookup { + /// + /// \name Constructors and Destructor + /// + /// Note: The copy constructor and the assignment operator are + /// intentionally defined as private, making this class non-copyable. + //@{ +private: + DNSLookup(const DNSLookup& source); + DNSLookup& operator=(const DNSLookup& source); +protected: + /// \brief The default constructor. + /// + /// This is intentionally defined as \c protected as this base class + /// should never be instantiated (except as part of a derived class). + DNSLookup() : self_(this) {} +public: + /// \brief The destructor + virtual ~DNSLookup() {} + //@} + /// \brief The function operator + /// + /// This makes its call indirectly via the "self" pointer, ensuring + /// that the function ultimately invoked will be the one in the derived + /// class. + /// + /// \param io_message The event message to handle + /// \param message The DNS MessagePtr that needs handling + /// \param buffer The final answer is put here + /// \param DNSServer DNSServer object to use + virtual void operator()(const IOMessage& io_message, + isc::dns::MessagePtr message, + isc::dns::MessagePtr answer_message, + isc::dns::OutputBufferPtr buffer, + DNSServer* server) const + { + (*self_)(io_message, message, answer_message, buffer, server); + } +private: + DNSLookup* self_; +}; + +} // namespace asiolink +#endif // __ASIOLINK_DNS_LOOKUP_H diff --git a/src/lib/asiolink/dns_server.h b/src/lib/asiolink/dns_server.h new file mode 100644 index 0000000000000000000000000000000000000000..65452755fbf6b131c4babe31a58a42f46afbdc48 --- /dev/null +++ b/src/lib/asiolink/dns_server.h @@ -0,0 +1,152 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __ASIOLINK_DNS_SERVER_H +#define __ASIOLINK_DNS_SERVER_H 1 + +#include + +namespace asiolink { + +/// \brief The \c DNSServer class is a wrapper (and base class) for +/// classes which provide DNS server functionality. +/// +/// The classes derived from this one, \c TCPServer and \c UDPServer, +/// act as the interface layer between clients sending queries, and +/// functions defined elsewhere that provide answers to those queries. +/// Those functions are described in more detail below under +/// \c SimpleCallback, \c DNSLookup, and \c DNSAnswer. +/// +/// Notes to developers: +/// When constructed, this class (and its derived classes) will have its +/// "self_" member set to point to "this". Objects of this class (as +/// instantiated through a base class) are sometimes passed by +/// reference (as this superclass); calls to methods in the base +/// class are then rerouted via this pointer to methods in the derived +/// class. This allows code from outside asiolink, with no specific +/// knowledge of \c TCPServer or \c UDPServer, to access their methods. +/// +/// This class is both assignable and copy-constructable. Its subclasses +/// use the "stackless coroutine" pattern, meaning that it will copy itself +/// when "forking", and that instances will be posted as ASIO handler +/// objects, which are always copied. +/// +/// Because these objects are frequently copied, it is recommended +/// that derived classes be kept small to reduce copy overhead. +class DNSServer { +protected: + /// + /// \name Constructors and destructors + /// + /// This is intentionally defined as \c protected, as this base class + /// should never be instantiated except as part of a derived class. + //@{ + DNSServer() : self_(this) {} +public: + /// \brief The destructor + virtual ~DNSServer() {} + //@} + + /// + /// \name Class methods + /// + /// These methods all make their calls indirectly via the "self_" + /// pointer, ensuring that the functions ultimately invoked will be + /// the ones in the derived class. This makes it possible to pass + /// instances of derived classes as references to this base class + /// without losing access to derived class data. + /// + //@{ + /// \brief The funtion operator + virtual void operator()(asio::error_code ec = asio::error_code(), + size_t length = 0) + { + (*self_)(ec, length); + } + + /// \brief Resume processing of the server coroutine after an + /// asynchronous call (e.g., to the DNS Lookup provider) has completed. + /// + /// \param done If true, this signals the system there is an answer + /// to return. + virtual void resume(const bool done) { self_->resume(done); } + + /// \brief Indicate whether the server is able to send an answer + /// to a query. + /// + /// This is presently used only for testing purposes. + virtual bool hasAnswer() { return (self_->hasAnswer()); } + + /// \brief Returns the current value of the 'coroutine' object + /// + /// This is a temporary method, intended to be used for debugging + /// purposes during development and removed later. It allows + /// callers from outside the coroutine object to retrieve information + /// about its current state. + /// + /// \return The value of the 'coroutine' object + virtual int value() { return (self_->value()); } + + /// \brief Returns a pointer to a clone of this DNSServer object. + /// + /// When a \c DNSServer object is copied or assigned, the result will + /// normally be another \c DNSServer object containing a copy + /// of the original "self_" pointer. Calling clone() guarantees + /// that the underlying object is also correctly copied. + /// + /// \return A deep copy of this DNSServer object + virtual DNSServer* clone() { return (self_->clone()); } + //@} + +protected: + /// \brief Lookup handler object. + /// + /// This is a protected class; it can only be instantiated + /// from within a derived class of \c DNSServer. + /// + /// A server object that has received a query creates an instance + /// of this class and scheudles it on the ASIO service queue + /// using asio::io_service::post(). When the handler executes, it + /// calls the asyncLookup() method in the server object to start a + /// DNS lookup. When the lookup is complete, the server object is + /// scheduled to resume, again using io_service::post(). + /// + /// Note that the calling object is copied into the handler object, + /// not referenced. This is because, once the calling object yields + /// control to the handler, it falls out of scope and may disappear + template + class AsyncLookup { + public: + AsyncLookup(T& caller) : caller_(caller) {} + void operator()() { caller_.asyncLookup(); } + private: + T caller_; + }; + + /// \brief Carries out a DNS lookup. + /// + /// This function calls the \c DNSLookup object specified by the + /// DNS server when the \c IOService was created, passing along + /// the details of the query and a pointer back to the current + /// server object. It is called asynchronously via the AsyncLookup + /// handler class. + virtual void asyncLookup() { self_->asyncLookup(); } + +private: + DNSServer* self_; +}; + + +} // asiolink +#endif // __ASIOLINK_DNS_SERVER_H diff --git a/src/lib/asiolink/dns_service.cc b/src/lib/asiolink/dns_service.cc new file mode 100644 index 0000000000000000000000000000000000000000..e9688c79f99e037e71c1bbae6418d11bcb2a72d5 --- /dev/null +++ b/src/lib/asiolink/dns_service.cc @@ -0,0 +1,194 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include // for some IPC/network system calls + +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include + +using isc::log::dlog; + +namespace asiolink { + +class SimpleCallback; +class DNSLookup; +class DNSAnswer; + +namespace { + +asio::ip::address +convertAddr(const std::string& address) { + asio::error_code err; + asio::ip::address addr = asio::ip::address::from_string(address, err); + if (err) { + isc_throw(IOError, "Invalid IP address '" << &address << "': " + << err.message()); + } + return (addr); +} + +} + + +class DNSServiceImpl { +public: + DNSServiceImpl(IOService& io_service, const char& port, + const asio::ip::address* v4addr, + const asio::ip::address* v6addr, + SimpleCallback* checkin, DNSLookup* lookup, + DNSAnswer* answer); + + IOService& io_service_; + + typedef boost::shared_ptr UDPServerPtr; + typedef boost::shared_ptr TCPServerPtr; + typedef boost::shared_ptr DNSServerPtr; + std::vector servers_; + SimpleCallback *checkin_; + DNSLookup *lookup_; + DNSAnswer *answer_; + + void addServer(uint16_t port, const asio::ip::address& address) { + try { + dlog(std::string("Initialize TCP server at ") + address.to_string() + ":" + boost::lexical_cast(port)); + TCPServerPtr tcpServer(new TCPServer(io_service_.get_io_service(), + address, port, checkin_, lookup_, answer_)); + (*tcpServer)(); + servers_.push_back(tcpServer); + dlog(std::string("Initialize UDP server at ") + address.to_string() + ":" + boost::lexical_cast(port)); + UDPServerPtr udpServer(new UDPServer(io_service_.get_io_service(), + address, port, checkin_, lookup_, answer_)); + (*udpServer)(); + servers_.push_back(udpServer); + } + catch (const asio::system_error& err) { + // We need to catch and convert any ASIO level exceptions. + // This can happen for unavailable address, binding a privilege port + // without the privilege, etc. + isc_throw(IOError, "Failed to initialize network servers: " << + err.what()); + } + } + void addServer(const char& port, const asio::ip::address& address) { + uint16_t portnum; + try { + // XXX: SunStudio with stlport4 doesn't reject some invalid + // representation such as "-1" by lexical_cast, so + // we convert it into a signed integer of a larger size and perform + // range check ourselves. + const int32_t portnum32 = boost::lexical_cast(&port); + if (portnum32 < 0 || portnum32 > 65535) { + isc_throw(IOError, "Invalid port number '" << &port); + } + portnum = portnum32; + } catch (const boost::bad_lexical_cast& ex) { + isc_throw(IOError, "Invalid port number '" << &port << "': " << + ex.what()); + } + addServer(portnum, address); + } +}; + +DNSServiceImpl::DNSServiceImpl(IOService& io_service, + const char& port, + const asio::ip::address* const v4addr, + const asio::ip::address* const v6addr, + SimpleCallback* checkin, + DNSLookup* lookup, + DNSAnswer* answer) : + io_service_(io_service), + checkin_(checkin), + lookup_(lookup), + answer_(answer) +{ + + if (v4addr) { + addServer(port, *v4addr); + } + if (v6addr) { + addServer(port, *v6addr); + } +} + +DNSService::DNSService(IOService& io_service, + const char& port, const char& address, + SimpleCallback* checkin, + DNSLookup* lookup, + DNSAnswer* answer) : + impl_(new DNSServiceImpl(io_service, port, NULL, NULL, checkin, lookup, + answer)), io_service_(io_service) +{ + addServer(port, &address); +} + +DNSService::DNSService(IOService& io_service, + const char& port, + const bool use_ipv4, const bool use_ipv6, + SimpleCallback* checkin, + DNSLookup* lookup, + DNSAnswer* answer) : + impl_(NULL), io_service_(io_service) +{ + const asio::ip::address v4addr_any = + asio::ip::address(asio::ip::address_v4::any()); + const asio::ip::address* const v4addrp = use_ipv4 ? &v4addr_any : NULL; + const asio::ip::address v6addr_any = + asio::ip::address(asio::ip::address_v6::any()); + const asio::ip::address* const v6addrp = use_ipv6 ? &v6addr_any : NULL; + impl_ = new DNSServiceImpl(io_service, port, v4addrp, v6addrp, checkin, lookup, answer); +} + +DNSService::DNSService(IOService& io_service, SimpleCallback* checkin, + DNSLookup* lookup, DNSAnswer *answer) : + impl_(new DNSServiceImpl(io_service, *"0", NULL, NULL, checkin, lookup, + answer)), io_service_(io_service) +{ +} + +DNSService::~DNSService() { + delete impl_; +} + +void +DNSService::addServer(const char& port, const std::string& address) { + impl_->addServer(port, convertAddr(address)); +} + +void +DNSService::addServer(uint16_t port, const std::string& address) { + impl_->addServer(port, convertAddr(address)); +} + +void +DNSService::clearServers() { + // FIXME: This does not work, it does not close the socket. + // How is it done? + impl_->servers_.clear(); +} + + + +} // namespace asiolink diff --git a/src/lib/asiolink/dns_service.h b/src/lib/asiolink/dns_service.h new file mode 100644 index 0000000000000000000000000000000000000000..84aa5fbfd8b4ba8e7c7ba23584d10512e8ae864c --- /dev/null +++ b/src/lib/asiolink/dns_service.h @@ -0,0 +1,112 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __ASIOLINK_DNS_SERVICE_H +#define __ASIOLINK_DNS_SERVICE_H 1 + +#include + +#include + +namespace asiolink { + +class SimpleCallback; +class DNSLookup; +class DNSAnswer; +class DNSServiceImpl; + +/// +/// DNSService is the service that handles DNS queries and answers with +/// a given IOService. This class is mainly intended to hold all the +/// logic that is shared between the authoritative and the recursive +/// server implementations. As such, it handles asio, including config +/// updates (through the 'Checkinprovider'), and listening sockets. +/// +class DNSService { + /// + /// \name Constructors and Destructor + /// + /// Note: The copy constructor and the assignment operator are + /// intentionally defined as private, making this class non-copyable. + //@{ +private: + DNSService(const DNSService& source); + DNSService& operator=(const DNSService& source); + +public: + /// \brief The constructor with a specific IP address and port on which + /// the services listen on. + /// + /// \param io_service The IOService to work with + /// \param port the port to listen on + /// \param address the IP address to listen on + /// \param checkin Provider for cc-channel events (see \c SimpleCallback) + /// \param lookup The lookup provider (see \c DNSLookup) + /// \param answer The answer provider (see \c DNSAnswer) + DNSService(IOService& io_service, const char& port, + const char& address, SimpleCallback* checkin, + DNSLookup* lookup, DNSAnswer* answer); + /// \brief The constructor with a specific port on which the services + /// listen on. + /// + /// It effectively listens on "any" IPv4 and/or IPv6 addresses. + /// IPv4/IPv6 services will be available if and only if \c use_ipv4 + /// or \c use_ipv6 is \c true, respectively. + /// + /// \param io_service The IOService to work with + /// \param port the port to listen on + /// \param ipv4 If true, listen on ipv4 'any' + /// \param ipv6 If true, listen on ipv6 'any' + /// \param checkin Provider for cc-channel events (see \c SimpleCallback) + /// \param lookup The lookup provider (see \c DNSLookup) + /// \param answer The answer provider (see \c DNSAnswer) + DNSService(IOService& io_service, const char& port, + const bool use_ipv4, const bool use_ipv6, + SimpleCallback* checkin, DNSLookup* lookup, + DNSAnswer* answer); + /// \brief The constructor without any servers. + /// + /// Use addServer() to add some servers. + DNSService(IOService& io_service, SimpleCallback* checkin, + DNSLookup* lookup, DNSAnswer* answer); + /// \brief The destructor. + ~DNSService(); + //@} + + /// \brief Add another server to the service + void addServer(uint16_t port, const std::string &address); + void addServer(const char &port, const std::string &address); + /// \brief Remove all servers from the service + void clearServers(); + + /// \brief Return the native \c io_service object used in this wrapper. + /// + /// This is a short term work around to support other BIND 10 modules + /// that share the same \c io_service with the authoritative server. + /// It will eventually be removed once the wrapper interface is + /// generalized. + asio::io_service& get_io_service() { return io_service_.get_io_service(); } + + /// \brief Return the IO Service Object + /// + /// \return IOService object for this DNS service. + asiolink::IOService& getIOService() { return (io_service_);} + +private: + DNSServiceImpl* impl_; + IOService& io_service_; +}; + +} // namespace asiolink +#endif // __ASIOLINK_DNS_SERVICE_H diff --git a/src/lib/asiolink/dummy_io_cb.h b/src/lib/asiolink/dummy_io_cb.h new file mode 100644 index 0000000000000000000000000000000000000000..bde656c348cd38c77259e0ddf7764d88ec19f9da --- /dev/null +++ b/src/lib/asiolink/dummy_io_cb.h @@ -0,0 +1,51 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __DUMMY_IO_CB_H +#define __DUMMY_IO_CB_H + +#include + +#include +#include + +namespace asiolink { + +/// \brief Asynchronous I/O Completion Callback +/// +/// The two socket classes (UDPSocket and TCPSocket) require that the I/O +/// completion callback function have an operator() method with the appropriate +/// signature. The classes are templates, any class with that method and +/// signature can be passed as the callback object - there is no need for a +/// base class defining the interface. However, some users of the socket +/// classes do not use the asynchronous I/O operations, yet have to supply a +/// template parameter. This is the reason for this class - it is the dummy +/// template parameter. + +class DummyIOCallback { +public: + + /// \brief Asynchronous I/O callback method + /// + /// \param error Unused + /// \param length Unused + void operator()(asio::error_code, size_t) + { + // TODO: log an error if this method ever gets called. + } +}; + +} // namespace asiolink + +#endif // __DUMMY_IO_CB_H diff --git a/src/lib/asiolink/internal/Makefile.am b/src/lib/asiolink/internal/Makefile.am deleted file mode 100644 index 3c6155b9c6357b8312f1f8a770219237315349ce..0000000000000000000000000000000000000000 --- a/src/lib/asiolink/internal/Makefile.am +++ /dev/null @@ -1 +0,0 @@ -SUBDIRS = tests diff --git a/src/lib/asiolink/internal/iofetch.h b/src/lib/asiolink/internal/iofetch.h deleted file mode 100644 index 7c0a8a08261ea7f4da56f92c3e9edd97b6858bfa..0000000000000000000000000000000000000000 --- a/src/lib/asiolink/internal/iofetch.h +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -#ifndef __IOQUERY_H -#define __IOQUERY_H 1 - -#include - -#include -#include -#include - -#include -#include -#include - -#include -#include - -// This file contains UDP-specific implementations of generic classes -// defined in asiolink.h. It is *not* intended to be part of the public -// API. - -namespace asiolink { -// -// Asynchronous UDP coroutine for upstream queries -// -class UDPQuery : public coroutine { -public: - // TODO Maybe this should be more generic than just for UDPQuery? - /// - /// \brief Result of the query - /// - /// This is related only to contacting the remote server. If the answer - ///indicates error, it is still counted as SUCCESS here, if it comes back. - /// - enum Result { - SUCCESS, - TIME_OUT, - STOPPED - }; - /// Abstract callback for the UDPQuery. - class Callback { - public: - virtual ~Callback() {} - - /// This will be called when the UDPQuery is completed - virtual void operator()(Result result) = 0; - }; - /// - /// \brief Constructor. - /// - /// It creates the query. - /// @param callback will be called when we terminate. It is your task to - /// delete it if allocated on heap. - ///@param timeout in ms. - /// - explicit UDPQuery(asio::io_service& io_service, - const isc::dns::Question& q, - const IOAddress& addr, uint16_t port, - isc::dns::OutputBufferPtr buffer, - Callback* callback, int timeout = -1); - void operator()(asio::error_code ec = asio::error_code(), - size_t length = 0); - /// Terminate the query. - void stop(Result reason = STOPPED); -private: - enum { MAX_LENGTH = 4096 }; - - /// - /// \short Private data - /// - /// They are not private because of stability of the - /// interface (this is private class anyway), but because this class - /// will be copyed often (it is used as a coroutine and passed as callback - /// to many async_*() functions) and we want keep the same data. Some of - /// the data is not copyable too. - /// - struct PrivateData; - boost::shared_ptr data_; -}; -} - - -#endif // __IOQUERY_H - -// Local Variables: -// mode: c++ -// End: diff --git a/src/lib/asiolink/internal/tests/Makefile.am b/src/lib/asiolink/internal/tests/Makefile.am deleted file mode 100644 index 449cab740ac717d17c467bd388d7dd31c3f2414a..0000000000000000000000000000000000000000 --- a/src/lib/asiolink/internal/tests/Makefile.am +++ /dev/null @@ -1,37 +0,0 @@ -AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib -AM_CPPFLAGS += $(BOOST_INCLUDES) - -AM_CXXFLAGS = $(B10_CXXFLAGS) - -if USE_STATIC_LINK -AM_LDFLAGS = -static -endif - -CLEANFILES = *.gcno *.gcda - -TESTS = -if HAVE_GTEST -TESTS += run_unittests -run_unittests_SOURCES = udpdns_unittest.cc -run_unittests_SOURCES += run_unittests.cc -run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) -run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS) -run_unittests_LDADD = $(GTEST_LDADD) -run_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libasiolink.la -run_unittests_LDADD += $(top_builddir)/src/lib/dns/libdns++.la -run_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libexceptions.la -run_unittests_LDADD += $(top_builddir)/src/lib/log/liblog.la -# Note: the ordering matters: -Wno-... must follow -Wextra (defined in -# B10_CXXFLAGS) -run_unittests_CXXFLAGS = $(AM_CXXFLAGS) -if USE_GXX -run_unittests_CXXFLAGS += -Wno-unused-parameter -endif -if USE_CLANGPP -# We need to disable -Werror for any test that uses internal definitions of -# ASIO when using clang++ -run_unittests_CXXFLAGS += -Wno-error -endif -endif - -noinst_PROGRAMS = $(TESTS) diff --git a/src/lib/asiolink/internal/tests/udpdns_unittest.cc b/src/lib/asiolink/internal/tests/udpdns_unittest.cc deleted file mode 100644 index 1e36e4a188976f44db9145ca9ef35a449cb31ea5..0000000000000000000000000000000000000000 --- a/src/lib/asiolink/internal/tests/udpdns_unittest.cc +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (C) 2010 CZ.NIC -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -#include -#include -#include -#include - -#include - -#include -#include - -using namespace asio; -using namespace isc::dns; -using asio::ip::udp; - -namespace { - -const asio::ip::address TEST_HOST(asio::ip::address::from_string("127.0.0.1")); -const uint16_t TEST_PORT(5301); -// FIXME Shouldn't we send something that is real message? -const char TEST_DATA[] = "TEST DATA"; - -// Test fixture for the asiolink::UDPQuery. -class UDPQueryTest : public ::testing::Test, - public asiolink::UDPQuery::Callback -{ - public: - // Expected result of the callback - asiolink::UDPQuery::Result expected_; - // Did the callback run already? - bool run_; - // We use an io_service to run the query - io_service service_; - // Something to ask - Question question_; - // Buffer where the UDPQuery will store response - OutputBufferPtr buffer_; - // The query we are testing - asiolink::UDPQuery query_; - - UDPQueryTest() : - run_(false), - question_(Name("example.net"), RRClass::IN(), RRType::A()), - buffer_(new OutputBuffer(512)), - query_(service_, question_, asiolink::IOAddress(TEST_HOST), - TEST_PORT, buffer_, this, 100) - { } - - // This is the callback's (), so it can be called. - void operator()(asiolink::UDPQuery::Result result) { - // We check the query returns the correct result - EXPECT_EQ(expected_, result); - // Check it is called only once - EXPECT_FALSE(run_); - // And mark the callback was called - run_ = true; - } - // A response handler, pretending to be remote DNS server - void respond(udp::endpoint* remote, udp::socket* socket) { - // Some data came, just send something back. - socket->send_to(asio::buffer(TEST_DATA, sizeof TEST_DATA), - *remote); - socket->close(); - } -}; - -/* - * Test that when we run the query and stop it after it was run, - * it returns "stopped" correctly. - * - * That is why stop() is posted to the service_ as well instead - * of calling it. - */ -TEST_F(UDPQueryTest, stop) { - expected_ = asiolink::UDPQuery::STOPPED; - // Post the query - service_.post(query_); - // Post query_.stop() (yes, the boost::bind thing is just - // query_.stop()). - service_.post(boost::bind(&asiolink::UDPQuery::stop, query_, - asiolink::UDPQuery::STOPPED)); - // Run both of them - service_.run(); - EXPECT_TRUE(run_); -} - -/* - * Test that when we queue the query to service_ and call stop() - * before it gets executed, it acts sanely as well (eg. has the - * same result as running stop() after - calls the callback). - */ -TEST_F(UDPQueryTest, prematureStop) { - expected_ = asiolink::UDPQuery::STOPPED; - // Stop before it is started - query_.stop(); - service_.post(query_); - service_.run(); - EXPECT_TRUE(run_); -} - -/* - * Test that it will timeout when no answer will arrive. - */ -TEST_F(UDPQueryTest, timeout) { - expected_ = asiolink::UDPQuery::TIME_OUT; - service_.post(query_); - service_.run(); - EXPECT_TRUE(run_); -} - -/* - * Test that it will succeed when we fake an answer and - * stores the same data we send. - * - * This is done through a real socket on loopback address. - */ -TEST_F(UDPQueryTest, receive) { - expected_ = asiolink::UDPQuery::SUCCESS; - udp::socket socket(service_, udp::v4()); - socket.set_option(socket_base::reuse_address(true)); - socket.bind(udp::endpoint(TEST_HOST, TEST_PORT)); - char inbuff[512]; - udp::endpoint remote; - socket.async_receive_from(asio::buffer(inbuff, 512), remote, boost::bind( - &UDPQueryTest::respond, this, &remote, &socket)); - service_.post(query_); - service_.run(); - EXPECT_TRUE(run_); - ASSERT_EQ(sizeof TEST_DATA, buffer_->getLength()); - EXPECT_EQ(0, memcmp(TEST_DATA, buffer_->getData(), sizeof TEST_DATA)); -} - -} diff --git a/src/lib/asiolink/internal/udpdns.h b/src/lib/asiolink/internal/udpdns.h deleted file mode 100644 index 6c6a8864224f5fb71e202e7cadf317f9dc7331a6..0000000000000000000000000000000000000000 --- a/src/lib/asiolink/internal/udpdns.h +++ /dev/null @@ -1,244 +0,0 @@ -// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -#ifndef __UDPDNS_H -#define __UDPDNS_H 1 - -#include - -#include -#include -#include - -#include -#include -#include - -#include -#include - -// This file contains UDP-specific implementations of generic classes -// defined in asiolink.h. It is *not* intended to be part of the public -// API. - -namespace asiolink { -/// \brief The \c UDPEndpoint class is a concrete derived class of -/// \c IOEndpoint that represents an endpoint of a UDP packet. -/// -/// Other notes about \c TCPEndpoint applies to this class, too. -class UDPEndpoint : public IOEndpoint { -public: - /// - /// \name Constructors and Destructor. - /// - //@{ - /// \brief Constructor from a pair of address and port. - /// - /// \param address The IP address of the endpoint. - /// \param port The UDP port number of the endpoint. - UDPEndpoint(const IOAddress& address, const unsigned short port) : - asio_endpoint_placeholder_( - new asio::ip::udp::endpoint(asio::ip::address::from_string(address.toText()), - port)), - asio_endpoint_(*asio_endpoint_placeholder_) - {} - - /// \brief Constructor from an ASIO UDP endpoint. - /// - /// This constructor is designed to be an efficient wrapper for the - /// corresponding ASIO class, \c udp::endpoint. - /// - /// \param asio_endpoint The ASIO representation of the UDP endpoint. - UDPEndpoint(const asio::ip::udp::endpoint& asio_endpoint) : - asio_endpoint_placeholder_(NULL), asio_endpoint_(asio_endpoint) - {} - - /// \brief The destructor. - ~UDPEndpoint() { delete asio_endpoint_placeholder_; } - //@} - - inline IOAddress getAddress() const { - return (asio_endpoint_.address()); - } - - inline uint16_t getPort() const { - return (asio_endpoint_.port()); - } - - inline short getProtocol() const { - return (asio_endpoint_.protocol().protocol()); - } - - inline short getFamily() const { - return (asio_endpoint_.protocol().family()); - } - - // This is not part of the exosed IOEndpoint API but allows - // direct access to the ASIO implementation of the endpoint - inline const asio::ip::udp::endpoint& getASIOEndpoint() const { - return (asio_endpoint_); - } - -private: - const asio::ip::udp::endpoint* asio_endpoint_placeholder_; - const asio::ip::udp::endpoint& asio_endpoint_; -}; - -/// \brief The \c UDPSocket class is a concrete derived class of -/// \c IOSocket that represents a UDP socket. -/// -/// Other notes about \c TCPSocket applies to this class, too. -class UDPSocket : public IOSocket { -private: - UDPSocket(const UDPSocket& source); - UDPSocket& operator=(const UDPSocket& source); -public: - /// \brief Constructor from an ASIO UDP socket. - /// - /// \param socket The ASIO representation of the UDP socket. - UDPSocket(asio::ip::udp::socket& socket) : socket_(socket) {} - - virtual int getNative() const { return (socket_.native()); } - virtual int getProtocol() const { return (IPPROTO_UDP); } - -private: - asio::ip::udp::socket& socket_; -}; - -// -// Asynchronous UDP server coroutine -// -/// -/// \brief This class implements the coroutine to handle UDP -/// DNS query event. As such, it is both a \c DNSServer and -/// a \c coroutine -/// -class UDPServer : public virtual DNSServer, public virtual coroutine { -public: - /// \brief Constructor - /// \param io_service the asio::io_service to work with - /// \param addr the IP address to listen for queries on - /// \param port the port to listen for queries on - /// \param checkin the callbackprovider for non-DNS events - /// \param lookup the callbackprovider for DNS lookup events - /// \param answer the callbackprovider for DNS answer events - explicit UDPServer(asio::io_service& io_service, - const asio::ip::address& addr, const uint16_t port, - SimpleCallback* checkin = NULL, - DNSLookup* lookup = NULL, - DNSAnswer* answer = NULL); - - /// \brief The function operator - void operator()(asio::error_code ec = asio::error_code(), - size_t length = 0); - - /// \brief Calls the lookup callback - void asyncLookup(); - - /// \brief Resume operation - /// - /// \param done Set this to true if the lookup action is done and - /// we have an answer - void resume(const bool done); - - /// \brief Check if we have an answer - /// - /// \return true if we have an answer - bool hasAnswer() { return (done_); } - - /// \brief Returns the coroutine state value - /// - /// \return the coroutine state value - int value() { return (get_value()); } - - /// \brief Clones the object - /// - /// \return a newly allocated copy of this object - DNSServer* clone() { - UDPServer* s = new UDPServer(*this); - return (s); - } - -private: - enum { MAX_LENGTH = 4096 }; - - // The ASIO service object - asio::io_service& io_; - - // Class member variables which are dynamic, and changes to which - // need to accessible from both sides of a coroutine fork or from - // outside of the coroutine (i.e., from an asynchronous I/O call), - // should be declared here as pointers and allocated in the - // constructor or in the coroutine. This allows state information - // to persist when an individual copy of the coroutine falls out - // scope while waiting for an event, *so long as* there is another - // object that is referencing the same data. As a side-benefit, using - // pointers also reduces copy overhead for coroutine objects. - // - // Note: Currently these objects are allocated by "new" in the - // constructor, or in the function operator while processing a query. - // Repeated allocations from the heap for every incoming query is - // clearly a performance issue; this must be optimized in the future. - // The plan is to have a structure pre-allocate several "server state" - // objects which can be pulled off a free list and placed on an in-use - // list whenever a query comes in. This will serve the dual purpose - // of improving performance and guaranteeing that state information - // will *not* be destroyed when any one instance of the coroutine - // falls out of scope while waiting for an event. - // - // Socket used to for listen for queries. Created in the - // constructor and stored in a shared_ptr because socket objects - // are not copyable. - boost::shared_ptr socket_; - - // The ASIO-enternal endpoint object representing the client - boost::shared_ptr sender_; - - // \c IOMessage and \c Message objects to be passed to the - // DNS lookup and answer providers - boost::shared_ptr io_message_; - - // The original query as sent by the client - isc::dns::MessagePtr query_message_; - - // The response message we are building - isc::dns::MessagePtr answer_message_; - - // The buffer into which the response is written - isc::dns::OutputBufferPtr respbuf_; - - // The buffer into which the query packet is written - boost::shared_array data_; - - // State information that is entirely internal to a given instance - // of the coroutine can be declared here. - size_t bytes_; - bool done_; - - // Callback functions provided by the caller - const SimpleCallback* checkin_callback_; - const DNSLookup* lookup_callback_; - const DNSAnswer* answer_callback_; - - boost::shared_ptr peer_; - boost::shared_ptr iosock_; -}; -} - - -#endif // __UDPDNS_H - -// Local Variables: -// mode: c++ -// End: diff --git a/src/lib/asiolink/interval_timer.cc b/src/lib/asiolink/interval_timer.cc new file mode 100644 index 0000000000000000000000000000000000000000..8efb102cd9efb67367b8da889afce7eec0482d45 --- /dev/null +++ b/src/lib/asiolink/interval_timer.cc @@ -0,0 +1,136 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include + +#include // for some IPC/network system calls +#include +#include + +#include + +#include + +#include +#include +#include + +namespace asiolink { + +class IntervalTimerImpl { +private: + // prohibit copy + IntervalTimerImpl(const IntervalTimerImpl& source); + IntervalTimerImpl& operator=(const IntervalTimerImpl& source); +public: + IntervalTimerImpl(IOService& io_service); + ~IntervalTimerImpl(); + void setup(const IntervalTimer::Callback& cbfunc, const long interval); + void callback(const asio::error_code& error); + void cancel() { + timer_.cancel(); + interval_ = 0; + } + long getInterval() const { return (interval_); } +private: + // a function to update timer_ when it expires + void update(); + // a function to call back when timer_ expires + IntervalTimer::Callback cbfunc_; + // interval in milliseconds + long interval_; + // asio timer + asio::deadline_timer timer_; +}; + +IntervalTimerImpl::IntervalTimerImpl(IOService& io_service) : + interval_(0), timer_(io_service.get_io_service()) +{} + +IntervalTimerImpl::~IntervalTimerImpl() +{} + +void +IntervalTimerImpl::setup(const IntervalTimer::Callback& cbfunc, + const long interval) +{ + // Interval should not be less than or equal to 0. + if (interval <= 0) { + isc_throw(isc::BadValue, "Interval should not be less than or " + "equal to 0"); + } + // Call back function should not be empty. + if (cbfunc.empty()) { + isc_throw(isc::InvalidParameter, "Callback function is empty"); + } + cbfunc_ = cbfunc; + interval_ = interval; + // Set initial expire time. + // At this point the timer is not running yet and will not expire. + // After calling IOService::run(), the timer will expire. + update(); + return; +} + +void +IntervalTimerImpl::update() { + if (interval_ == 0) { + // timer has been canceled. Do nothing. + return; + } + try { + // Update expire time to (current time + interval_). + timer_.expires_from_now(boost::posix_time::millisec(interval_)); + } catch (const asio::system_error& e) { + isc_throw(isc::Unexpected, "Failed to update timer"); + } + // Reset timer. + timer_.async_wait(boost::bind(&IntervalTimerImpl::callback, this, _1)); +} + +void +IntervalTimerImpl::callback(const asio::error_code& cancelled) { + // Do not call cbfunc_ in case the timer was cancelled. + // The timer will be canelled in the destructor of asio::deadline_timer. + if (!cancelled) { + cbfunc_(); + // Set next expire time. + update(); + } +} + +IntervalTimer::IntervalTimer(IOService& io_service) { + impl_ = new IntervalTimerImpl(io_service); +} + +IntervalTimer::~IntervalTimer() { + delete impl_; +} + +void +IntervalTimer::setup(const Callback& cbfunc, const long interval) { + return (impl_->setup(cbfunc, interval)); +} + +void +IntervalTimer::cancel() { + impl_->cancel(); +} + +long +IntervalTimer::getInterval() const { + return (impl_->getInterval()); +} + +} diff --git a/src/lib/asiolink/interval_timer.h b/src/lib/asiolink/interval_timer.h new file mode 100644 index 0000000000000000000000000000000000000000..d805cd7c39efbc0dc3459c893544034d2987d088 --- /dev/null +++ b/src/lib/asiolink/interval_timer.h @@ -0,0 +1,133 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __ASIOLINK_INTERVAL_TIMER_H +#define __ASIOLINK_INTERVAL_TIMER_H 1 + +#include + +#include + +namespace asiolink { + +struct IntervalTimerImpl; + +/// \brief The \c IntervalTimer class is a wrapper for the ASIO +/// \c asio::deadline_timer class. +/// +/// This class is implemented to use \c asio::deadline_timer as interval +/// timer. +/// +/// \c setup() sets a timer to expire on (now + interval) and a call back +/// function. +/// +/// \c IntervalTimerImpl::callback() is called by the timer when it expires. +/// +/// The function calls the call back function set by \c setup() and updates +/// the timer to expire in (now + interval) milliseconds. +/// The type of call back function is \c void(void). +/// +/// The call back function will not be called if the instance of this class is +/// destroyed before the timer is expired. +/// +/// Note: Destruction of an instance of this class while call back is pending +/// causes throwing an exception from \c IOService. +/// +/// Sample code: +/// \code +/// void function_to_call_back() { +/// // this function will be called periodically +/// } +/// int interval_in_milliseconds = 1000; +/// IOService io_service; +/// +/// IntervalTimer intervalTimer(io_service); +/// intervalTimer.setup(function_to_call_back, interval_in_milliseconds); +/// io_service.run(); +/// \endcode +class IntervalTimer { +public: + /// \name The type of timer callback function + typedef boost::function Callback; + + /// + /// \name Constructors and Destructor + /// + /// Note: The copy constructor and the assignment operator are + /// intentionally defined as private, making this class non-copyable. + //@{ +private: + IntervalTimer(const IntervalTimer& source); + IntervalTimer& operator=(const IntervalTimer& source); +public: + /// \brief The constructor with \c IOService. + /// + /// This constructor may throw a standard exception if + /// memory allocation fails inside the method. + /// This constructor may also throw \c asio::system_error. + /// + /// \param io_service A reference to an instance of IOService + IntervalTimer(IOService& io_service); + + /// \brief The destructor. + /// + /// This destructor never throws an exception. + /// + /// On the destruction of this class the timer will be canceled + /// inside \c asio::deadline_timer. + ~IntervalTimer(); + //@} + + /// \brief Register timer callback function and interval. + /// + /// This function sets callback function and interval in milliseconds. + /// Timer will actually start after calling \c IOService::run(). + /// + /// \param cbfunc A reference to a function \c void(void) to call back + /// when the timer is expired (should not be an empty functor) + /// \param interval Interval in milliseconds (greater than 0) + /// + /// Note: IntervalTimer will not pass \c asio::error_code to + /// call back function. In case the timer is cancelled, the function + /// will not be called. + /// + /// \throw isc::InvalidParameter cbfunc is empty + /// \throw isc::BadValue interval is less than or equal to 0 + /// \throw isc::Unexpected ASIO library error + void setup(const Callback& cbfunc, const long interval); + + /// Cancel the timer. + /// + /// If the timer has been set up, this method cancels any asynchronous + /// events waiting on the timer and stops the timer itself. + /// If the timer has already been canceled, this method effectively does + /// nothing. + /// + /// This method never throws an exception. + void cancel(); + + /// Return the timer interval. + /// + /// This method returns the timer interval in milliseconds if it's running; + /// if the timer has been canceled it returns 0. + /// + /// This method never throws an exception. + long getInterval() const; + +private: + IntervalTimerImpl* impl_; +}; + +} // namespace asiolink +#endif // __ASIOLINK_INTERVAL_TIMER_H diff --git a/src/lib/asiolink/ioaddress.cc b/src/lib/asiolink/io_address.cc similarity index 94% rename from src/lib/asiolink/ioaddress.cc rename to src/lib/asiolink/io_address.cc index 990524acfb606ee301c9e37644385779b9eef281..70e837456d2e00d8665884e95f6d7a6ed2d2dce2 100644 --- a/src/lib/asiolink/ioaddress.cc +++ b/src/lib/asiolink/io_address.cc @@ -20,7 +20,10 @@ #include -#include +#include +#include +#include + using namespace asio; using asio::ip::udp; diff --git a/src/lib/asiolink/ioaddress.h b/src/lib/asiolink/io_address.h similarity index 71% rename from src/lib/asiolink/ioaddress.h rename to src/lib/asiolink/io_address.h index 5727041f66880030b7448b8963d6d77213dbd973..0d2787f95ffa9852086c0292d92dd892e76cba99 100644 --- a/src/lib/asiolink/ioaddress.h +++ b/src/lib/asiolink/io_address.h @@ -12,8 +12,8 @@ // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. -#ifndef __IOADDRESS_H -#define __IOADDRESS_H 1 +#ifndef __IO_ADDRESS_H +#define __IO_ADDRESS_H 1 // IMPORTANT NOTE: only very few ASIO headers files can be included in // this file. In particular, asio.hpp should never be included here. @@ -73,15 +73,54 @@ public: /// \return A string representation of the address. std::string toText() const; - /// \brief Returns the address family. + /// \brief Returns the address family + /// + /// \return AF_INET for IPv4 or AF_INET6 for IPv6. short getFamily() const; + /// \brief Compare addresses for equality + /// + /// \param other Address to compare against. + /// + /// \return true if addresses are equal, false if not. + bool equals(const IOAddress& other) const { + return (asio_address_ == other.asio_address_); + } + + /// \brief Compare addresses for equality + /// + /// \param other Address to compare against. + /// + /// \return true if addresses are equal, false if not. + bool operator==(const IOAddress& other) const { + return equals(other); + } + + // \brief Compare addresses for inequality + /// + /// \param other Address to compare against. + /// + /// \return false if addresses are equal, true if not. + bool nequals(const IOAddress& other) const { + return (!equals(other)); + } + + // \brief Compare addresses for inequality + /// + /// \param other Address to compare against. + /// + /// \return false if addresses are equal, true if not. + bool operator!=(const IOAddress& other) const { + return (nequals(other)); + } + + private: asio::ip::address asio_address_; }; } // asiolink -#endif // __IOADDRESS_H +#endif // __IO_ADDRESS_H // Local Variables: // mode: c++ diff --git a/src/lib/asiolink/io_asio_socket.h b/src/lib/asiolink/io_asio_socket.h new file mode 100644 index 0000000000000000000000000000000000000000..eae9b32509b5eeb48fa3d542aef29b235318b753 --- /dev/null +++ b/src/lib/asiolink/io_asio_socket.h @@ -0,0 +1,309 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __IO_ASIO_SOCKET_H +#define __IO_ASIO_SOCKET_H 1 + +// IMPORTANT NOTE: only very few ASIO headers files can be included in +// this file. In particular, asio.hpp should never be included here. +// See the description of the namespace below. +#include // for some network system calls + +#include +#include + +#include +#include + +#include +#include + + +namespace asiolink { + +/// \brief Socket not open +/// +/// Thrown on an attempt to do read/write to a socket that is not open. +class SocketNotOpen : public IOError { +public: + SocketNotOpen(const char* file, size_t line, const char* what) : + IOError(file, line, what) {} +}; + + + +/// Forward declaration of an IOEndpoint +class IOEndpoint; + + +/// \brief I/O Socket with asynchronous operations +/// +/// This class is a wrapper for the ASIO socket classes such as +/// \c ip::tcp::socket and \c ip::udp::socket. +/// +/// This is the basic IOSocket with additional operations - open, send, receive +/// and close. Depending on how the asiolink code develops, it may be a +/// temporary class: its main use is to add the template parameter needed for +/// the derived classes UDPSocket and TCPSocket but without changing the +/// signature of the more basic IOSocket class. +/// +/// We may revisit this decision when we generalize the wrapper and more +/// modules use it. Also, at that point we may define a separate (visible) +/// derived class for testing purposes rather than providing factory methods +/// (i.e., getDummy variants below). +/// +/// TODO: Check if IOAsioSocket class is still needed +/// +/// \param C Template parameter identifying type of the callback object. + +template +class IOAsioSocket : public IOSocket { + /// + /// \name Constructors and Destructor + /// + /// Note: The copy constructor and the assignment operator are + /// intentionally defined as private, making this class non-copyable. + //@{ +private: + IOAsioSocket(const IOAsioSocket& source); + IOAsioSocket& operator=(const IOAsioSocket& source); +protected: + /// \brief The default constructor. + /// + /// This is intentionally defined as \c protected as this base class + /// should never be instantiated (except as part of a derived class). + IOAsioSocket() {} +public: + /// The destructor. + virtual ~IOAsioSocket() {} + //@} + + /// \brief Return the "native" representation of the socket. + /// + /// In practice, this is the file descriptor of the socket for + /// UNIX-like systems so the current implementation simply uses + /// \c int as the type of the return value. + /// We may have to need revisit this decision later. + /// + /// In general, the application should avoid using this method; + /// it essentially discloses an implementation specific "handle" that + /// can change the internal state of the socket (consider the + /// application closes it, for example). + /// But we sometimes need to perform very low-level operations that + /// requires the native representation. Passing the file descriptor + /// to a different process is one example. + /// This method is provided as a necessary evil for such limited purposes. + /// + /// This method never throws an exception. + /// + /// \return The native representation of the socket. This is the socket + /// file descriptor for UNIX-like systems. + virtual int getNative() const = 0; + + /// \brief Return the transport protocol of the socket. + /// + /// Currently, it returns \c IPPROTO_UDP for UDP sockets, and + /// \c IPPROTO_TCP for TCP sockets. + /// + /// This method never throws an exception. + /// + /// \return IPPROTO_UDP for UDP sockets + /// \return IPPROTO_TCP for TCP sockets + virtual int getProtocol() const = 0; + + /// \brief Open AsioSocket + /// + /// Opens the socket for asynchronous I/O. On a UDP socket, this is merely + /// an "open()" on the underlying socket (so completes immediately), but on + /// a TCP socket it also connects to the remote end (which is done as an + /// asynchronous operation). + /// + /// For TCP, signalling of the completion of the operation is done by + /// by calling the callback function in the normal way. This could be done + /// for UDP (by posting en event on the event queue); however, that will + /// incur additional overhead in the most common case. Instead, the return + /// value indicates whether the operation was asynchronous or not. If yes, + /// (i.e. TCP) the callback has been posted to the event queue: if no (UDP), + /// no callback has been posted (in which case it is up to the caller as to + /// whether they want to manually post the callback themself.) + /// + /// \param endpoint Pointer to the endpoint object. This is ignored for + /// a UDP socket (the target is specified in the send call), but should + /// be of type TCPEndpoint for a TCP connection. + /// \param callback I/O Completion callback, called when the operation has + /// completed, but only if the operation was asynchronous. + /// + /// \return true if an asynchronous operation was started and the caller + /// should yield and wait for completion, false if the operation was + /// completed synchronously and no callback was queued. + virtual bool open(const IOEndpoint* endpoint, C& callback) = 0; + + /// \brief Send Asynchronously + /// + /// This corresponds to async_send_to() for UDP sockets and async_send() + /// for TCP. In both cases an endpoint argument is supplied indicating the + /// target of the send - this is ignored for TCP. + /// + /// \param data Data to send + /// \param length Length of data to send + /// \param endpoint Target of the send + /// \param callback Callback object. + virtual void asyncSend(const void* data, size_t length, + const IOEndpoint* endpoint, C& callback) = 0; + + /// \brief Receive Asynchronously + /// + /// This correstponds to async_receive_from() for UDP sockets and + /// async_receive() for TCP. In both cases, an endpoint argument is + /// supplied to receive the source of the communication. For TCP it will + /// be filled in with details of the connection. + /// + /// \param data Buffer to receive incoming message + /// \param length Length of the data buffer + /// \param cumulative Amount of data that should already be in the buffer. + /// \param endpoint Source of the communication + /// \param callback Callback object + virtual void asyncReceive(void* data, size_t length, size_t cumulative, + IOEndpoint* endpoint, C& callback) = 0; + + /// \brief Checks if the data received is complete. + /// + /// This applies to TCP receives, where the data is a byte stream and a + /// receive is not guaranteed to receive the entire message. DNS messages + /// over TCP are prefixed by a two-byte count field. This method takes the + /// amount received so far and the amount received in this I/O and checks + /// if the message is complete, returning the appropriate indication. As + /// a side-effect, it also updates the amount received. + /// + /// For a UDP receive, all the data is received in one I/O, so this is + /// effectively a no-op (although it does update the amount received). + /// + /// \param data Data buffer containing data to date + /// \param length Amount of data received in last asynchronous I/O + /// \param cumulative On input, amount of data received before the last + /// I/O. On output, the total amount of data received to date. + /// + /// \return true if the receive is complete, false if another receive is + /// needed. + virtual bool receiveComplete(void* data, size_t length, + size_t& cumulative) = 0; + + /// \brief Cancel I/O On AsioSocket + virtual void cancel() = 0; + + /// \brief Close socket + virtual void close() = 0; +}; + + +#include "io_socket.h" + +/// \brief The \c DummyAsioSocket class is a concrete derived class of +/// \c IOAsioSocket that is not associated with any real socket. +/// +/// This main purpose of this class is tests, where it may be desirable to +/// instantiate an \c IOAsioSocket object without involving system resource +/// allocation such as real network sockets. +/// +/// \param C Template parameter identifying type of the callback object. + +template +class DummyAsioSocket : public IOAsioSocket { +private: + DummyAsioSocket(const DummyAsioSocket& source); + DummyAsioSocket& operator=(const DummyAsioSocket& source); +public: + /// \brief Constructor from the protocol number. + /// + /// The protocol must validly identify a standard network protocol. + /// For example, to specify TCP \c protocol must be \c IPPROTO_TCP. + /// + /// \param protocol The network protocol number for the socket. + DummyAsioSocket(const int protocol) : protocol_(protocol) {} + + /// \brief A dummy derived method of \c IOAsioSocket::getNative(). + /// + /// \return Always returns -1 as the object is not associated with a real + /// (native) socket. + virtual int getNative() const { return (-1); } + + /// \brief A dummy derived method of \c IOAsioSocket::getProtocol(). + /// + /// \return Protocol socket was created with + virtual int getProtocol() const { return (protocol_); } + + + /// \brief Open AsioSocket + /// + /// A call that is a no-op on UDP sockets, this opens a connection to the + /// system identified by the given endpoint. + /// + /// \param endpoint Unused + /// \param callback Unused. + ///false indicating that the operation completed synchronously. + virtual bool open(const IOEndpoint*, C&) { + return (false); + } + + /// \brief Send Asynchronously + /// + /// Must be supplied as it is abstract in the base class. + /// + /// \param data Unused + /// \param length Unused + /// \param endpoint Unused + /// \param callback Unused + virtual void asyncSend(const void*, size_t, const IOEndpoint*, C&) { + } + + /// \brief Receive Asynchronously + /// + /// Must be supplied as it is abstract in the base class. + /// + /// \param data Unused + /// \param length Unused + /// \param cumulative Unused + /// \param endpoint Unused + /// \param callback Unused + virtual void asyncReceive(void* data, size_t, size_t, IOEndpoint*, C&) { } + /// \brief Checks if the data received is complete. + /// + /// \param data Unused + /// \param length Unused + /// \param cumulative Unused + /// + /// \return Always true + virtual bool receiveComplete(void*, size_t, size_t&) { + return (true); + } + + /// \brief Cancel I/O On AsioSocket + /// + /// Must be supplied as it is abstract in the base class. + virtual void cancel() { + } + + /// \brief Close socket + /// + /// Must be supplied as it is abstract in the base class. + virtual void close() { + } + +private: + const int protocol_; +}; + +} // namespace asiolink + +#endif // __IO_ASIO_SOCKET_H diff --git a/src/lib/asiolink/ioendpoint.cc b/src/lib/asiolink/io_endpoint.cc similarity index 85% rename from src/lib/asiolink/ioendpoint.cc rename to src/lib/asiolink/io_endpoint.cc index 2807f8d8c58e2c1ca5abcdb538f610da32e6fe7c..bf79f61868ff0953dd18078b1de131d82f9d02bc 100644 --- a/src/lib/asiolink/ioendpoint.cc +++ b/src/lib/asiolink/io_endpoint.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -18,9 +18,12 @@ #include #include -#include -#include -#include +#include + +#include +#include +#include +#include using namespace std; diff --git a/src/lib/asiolink/ioendpoint.h b/src/lib/asiolink/io_endpoint.h similarity index 97% rename from src/lib/asiolink/ioendpoint.h rename to src/lib/asiolink/io_endpoint.h index 926ce50fdcdca5e4dbae0ff1a830be124284ece2..62b9e47942c78d828012ffcf6c5826a609204547 100644 --- a/src/lib/asiolink/ioendpoint.h +++ b/src/lib/asiolink/io_endpoint.h @@ -12,8 +12,8 @@ // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. -#ifndef __IOENDPOINT_H -#define __IOENDPOINT_H 1 +#ifndef __IO_ENDPOINT_H +#define __IO_ENDPOINT_H 1 // IMPORTANT NOTE: only very few ASIO headers files can be included in // this file. In particular, asio.hpp should never be included here. @@ -24,6 +24,7 @@ #include #include +#include namespace asiolink { @@ -114,7 +115,7 @@ public: }; } // asiolink -#endif // __IOENDPOINT_H +#endif // __IO_ENDPOINT_H // Local Variables: // mode: c++ diff --git a/src/lib/asiolink/io_error.h b/src/lib/asiolink/io_error.h new file mode 100644 index 0000000000000000000000000000000000000000..2869e0bd1a031fe6ddb757f81f2bd429e3453745 --- /dev/null +++ b/src/lib/asiolink/io_error.h @@ -0,0 +1,35 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + + +#ifndef __IO_ERROR_H +#define __IO_ERROR_H + +#include + +namespace asiolink { + +/// \brief An exception that is thrown if an error occurs within the IO +/// module. This is mainly intended to be a wrapper exception class for +/// ASIO specific exceptions. +class IOError : public isc::Exception { +public: + IOError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) {} +}; + + +} // asiolink + +#endif // __IO_ERROR_H diff --git a/src/lib/asiolink/io_fetch.cc b/src/lib/asiolink/io_fetch.cc new file mode 100644 index 0000000000000000000000000000000000000000..d1f722cf865aef97df204f04364646a0ef6d527b --- /dev/null +++ b/src/lib/asiolink/io_fetch.cc @@ -0,0 +1,193 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include + +#include // for some IPC/network system calls +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include + +using namespace asio; +using namespace isc::dns; +using namespace isc::log; +using namespace std; + +namespace asiolink { + +/// IOFetch Constructor - just initialize the private data + +IOFetch::IOFetch(int protocol, IOService& service, + const isc::dns::Question& question, const IOAddress& address, uint16_t port, + isc::dns::OutputBufferPtr& buff, Callback* cb, int wait) + : + data_(new IOFetch::IOFetchData(protocol, service, question, address, + port, buff, cb, wait)) +{ +} + +/// The function operator is implemented with the "stackless coroutine" +/// pattern; see internal/coroutine.h for details. + +void +IOFetch::operator()(error_code ec, size_t length) { + if (ec || data_->stopped) { + return; + } + + CORO_REENTER (this) { + + /// Generate the upstream query and render it to wire format + /// This is done in a different scope to allow inline variable + /// declarations. + { + Message msg(Message::RENDER); + + // TODO: replace with boost::random or some other suitable PRNG + msg.setQid(0); + msg.setOpcode(Opcode::QUERY()); + msg.setRcode(Rcode::NOERROR()); + msg.setHeaderFlag(Message::HEADERFLAG_RD); + msg.addQuestion(data_->question); + MessageRenderer renderer(*data_->msgbuf); + msg.toWire(renderer); + + // As this is a new fetch, clear the amount of data received + data_->cumulative = 0; + + dlog("Sending " + msg.toText() + " to " + + data_->remote->getAddress().toText()); + } + + + // If we timeout, we stop, which will shutdown everything and + // cancel all other attempts to run inside the coroutine + if (data_->timeout != -1) { + data_->timer.expires_from_now(boost::posix_time::milliseconds( + data_->timeout)); + data_->timer.async_wait(boost::bind(&IOFetch::stop, *this, + TIME_OUT)); + } + + // Open a connection to the target system. For speed, if the operation + // was completed synchronously (i.e. UDP operation) we bypass the yield. + if (data_->socket->open(data_->remote.get(), *this)) { + CORO_YIELD; + } + + // Begin an asynchronous send, and then yield. When the send completes + // send completes, we will resume immediately after this point. + CORO_YIELD data_->socket->asyncSend(data_->msgbuf->getData(), + data_->msgbuf->getLength(), data_->remote.get(), *this); + + // Now receive the response. Since TCP may not receive the entire + // message in one operation, we need to loop until we have received + // it. (This can't be done within the asyncReceive() method because + // each I/O operation will be done asynchronously and between each one + // we need to yield ... and we *really* don't want to set up another + // coroutine within that method.) So after each receive (and yield), + // we check if the operation is complete and if not, loop to read again. + do { + CORO_YIELD data_->socket->asyncReceive(data_->data.get(), + static_cast(MAX_LENGTH), data_->cumulative, + data_->remote.get(), *this); + } while (!data_->socket->receiveComplete(data_->data.get(), length, + data_->cumulative)); + + // The message is not rendered yet, so we can't print it easily + dlog("Received response from " + data_->remote->getAddress().toText()); + + /// Copy the answer into the response buffer. (TODO: If the + /// OutputBuffer object were made to meet the requirements of + /// a MutableBufferSequence, then it could be written to directly + /// by async_receive_from() and this additional copy step would + /// be unnecessary.) + data_->buffer->writeData(data_->data.get(), length); + + // Finished with this socket, so close it. + data_->socket->close(); + + /// We are done + stop(SUCCESS); + } +} + +// Function that stops the coroutine sequence. It is called either when the +// query finishes or when the timer times out. Either way, it sets the +// "stopped_" flag and cancels anything that is in progress. +// +// As the function may be entered multiple times as things wind down, the +// stopped_ flag checks if stop() has already been called. If it has, +// subsequent calls are no-ops. + +void +IOFetch::stop(Result result) { + + if (!data_->stopped) { + + // Mark the fetch as stopped to prevent other completion callbacks + // (invoked because of the calls to cancel()) from executing the + // cancel calls again. + // + // In a single threaded environment, the callbacks won't be invoked + // until this one completes. In a multi-threaded environment, they may + // well be, in which case the testing (and setting) of the stopped_ + // variable should be done inside a mutex (and the stopped_ variable + // declared as "volatile"). + // + // TODO: Update testing of stopped_ if threads are used. + data_->stopped = true; + + switch (result) { + case TIME_OUT: + dlog("Query timed out"); + break; + + case STOPPED: + dlog("Query stopped"); + break; + + default: + ; + } + + // Stop requested, cancel and I/O's on the socket and shut it down, + // and cancel the timer. + data_->socket->cancel(); + data_->socket->close(); + + data_->timer.cancel(); + + // Execute the I/O completion callback (if present). + if (data_->callback) { + (*(data_->callback))(result); + } + + // Mark that stop() has now been called. + + } +} + +} // namespace asiolink + diff --git a/src/lib/asiolink/io_fetch.h b/src/lib/asiolink/io_fetch.h new file mode 100644 index 0000000000000000000000000000000000000000..8158c6c05c31808226fbea9f1dc60610aeb62349 --- /dev/null +++ b/src/lib/asiolink/io_fetch.h @@ -0,0 +1,226 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __IO_FETCH_H +#define __IO_FETCH_H 1 + +#include + +#include +#include +#include // for some IPC/network system calls + +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + + +namespace asiolink { + + +/// \brief Upstream Fetch Processing +/// +/// IOFetch is the class used to send upstream fetches and to handle responses. +/// +/// \param E Endpoint type to use. + +class IOFetch : public coroutine { +public: + + /// \brief Result of Upstream Fetch + /// + /// Note that this applies to the status of I/Os in the fetch - a fetch + /// that resulted in a packet being received from the server is a SUCCESS, + /// even if the contents of the packet indicate that some error occurred. + enum Result { + SUCCESS = 0, ///< Success, fetch completed + TIME_OUT, ///< Failure, fetch timed out + STOPPED, ///< Control code, fetch has been stopped + NOTSET ///< For testing, indicates value not set + }; + + // The next enum is a "trick" to allow constants to be defined in a class + // declaration. + + /// \brief Integer Constants + enum { + MAX_LENGTH = 4096 ///< Maximum size of receive buffer + }; + + /// \brief I/O Fetch Callback + /// + /// Class of callback object for when the fetch itself has completed - an + /// object of this class is passed to the IOFetch constructor and its + /// operator() method called when the fetch completes. + /// + /// Note the difference between the two operator() methods: + /// - IOFetch::operator() callback is called when an asynchronous I/O has + /// completed. + /// - IOFetch::Callback::operator() is called when an upstream fetch - which + /// may have involved several asynchronous I/O operations - has completed. + /// + /// This is an abstract class. + class Callback { + public: + /// \brief Default Constructor + Callback() + {} + + /// \brief Virtual Destructor + virtual ~Callback() + {} + + /// \brief Callback method called when the fetch completes + /// + /// \brief result Result of the fetch + virtual void operator()(Result result) = 0; + }; + + /// \brief IOFetch Data + /// + /// The data for IOFetch is held in a separate struct pointed to by a + /// shared_ptr object. This is because the IOFetch object will be copied + /// often (it is used as a coroutine and passed as callback to many + /// async_*() functions) and we want keep the same data). Organising the + /// data in this way keeps copying to a minimum. + struct IOFetchData { + + // The next two members are shared pointers to a base class because what + // is actually instantiated depends on whether the fetch is over UDP or + // TCP, which is not known until construction of the IOFetch. Use of + // a shared pointer here is merely to ensure deletion when the data + // object is deleted. + boost::shared_ptr > socket; + ///< Socket to use for I/O + boost::shared_ptr remote; ///< Where the fetch was sent + isc::dns::Question question; ///< Question to be asked + isc::dns::OutputBufferPtr msgbuf; ///< Wire buffer for question + isc::dns::OutputBufferPtr buffer; ///< Received data held here + boost::shared_array data; ///< Temporary array for data + IOFetch::Callback* callback; ///< Called on I/O Completion + size_t cumulative; ///< Cumulative received amount + bool stopped; ///< Have we stopped running? + asio::deadline_timer timer; ///< Timer to measure timeouts + int timeout; ///< Timeout in ms + + /// \brief Constructor + /// + /// Just fills in the data members of the IOFetchData structure + /// + /// \param protocol either IPPROTO_UDP or IPPROTO_TCP + /// \param service I/O Service object to handle the asynchronous + /// operations. + /// \param query DNS question to send to the upstream server. + /// \param address IP address of upstream server + /// \param port Port to use for the query + /// \param buff Output buffer into which the response (in wire format) + /// is written (if a response is received). + /// \param cb Callback object containing the callback to be called + /// when we terminate. The caller is responsible for managing this + /// object and deleting it if necessary. + /// \param wait Timeout for the fetch (in ms). + /// + /// TODO: May need to alter constructor (see comment 4 in Trac ticket #554) + IOFetchData(int protocol, IOService& service, + const isc::dns::Question& query, const IOAddress& address, + uint16_t port, isc::dns::OutputBufferPtr& buff, Callback* cb, + int wait) + : + socket((protocol == IPPROTO_UDP) ? + static_cast*>( + new UDPSocket(service)) : + static_cast*>( + new TCPSocket(service)) + ), + remote((protocol == IPPROTO_UDP) ? + static_cast(new UDPEndpoint(address, port)) : + static_cast(new TCPEndpoint(address, port)) + ), + question(query), + msgbuf(new isc::dns::OutputBuffer(512)), + buffer(buff), + data(new char[IOFetch::MAX_LENGTH]), + callback(cb), + cumulative(0), + stopped(false), + timer(service.get_io_service()), + timeout(wait) + {} + }; + + /// \brief Constructor. + /// + /// Creates the object that will handle the upstream fetch. + /// + /// TODO: Need to randomise the source port + /// + /// \param protocol Fetch protocol, either IPPROTO_UDP or IPPROTO_TCP + /// \param service I/O Service object to handle the asynchronous + /// operations. + /// \param question DNS question to send to the upstream server. + /// \param buff Output buffer into which the response (in wire format) + /// is written (if a response is received). + /// \param cb Callback object containing the callback to be called + /// when we terminate. The caller is responsible for managing this + /// object and deleting it if necessary. + /// \param address IP address of upstream server + /// \param port Port to which to connect on the upstream server + /// (default = 53) + /// \param wait Timeout for the fetch (in ms). The default value of + /// -1 indicates no timeout. + IOFetch(int protocol, IOService& service, + const isc::dns::Question& question, const IOAddress& address, + uint16_t port, isc::dns::OutputBufferPtr& buff, Callback* cb, + int wait = -1); + + /// \brief Coroutine entry point + /// + /// The operator() method is the method in which the coroutine code enters + /// this object when an operation has been completed. + /// + /// \param ec Error code, the result of the last asynchronous I/O operation. + /// \param length Amount of data received on the last asynchronous read + void operator()(asio::error_code ec = asio::error_code(), + size_t length = 0); + + /// \brief Terminate query + /// + /// This method can be called at any point. It terminates the current + /// query with the specified reason. + /// + /// \param reason Reason for terminating the query + void stop(Result reason = STOPPED); + +private: + boost::shared_ptr data_; ///< Private data + +}; + +} // namespace asiolink + +#endif // __IO_FETCH_H diff --git a/src/lib/asiolink/iomessage.h b/src/lib/asiolink/io_message.h similarity index 96% rename from src/lib/asiolink/iomessage.h rename to src/lib/asiolink/io_message.h index 27baa8a47e0911e05f039fcc0d6d52eefb13876c..532f4492d961c04b0cdaaff38620359d3d34f1b0 100644 --- a/src/lib/asiolink/iomessage.h +++ b/src/lib/asiolink/io_message.h @@ -12,8 +12,8 @@ // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. -#ifndef __IOMESSAGE_H -#define __IOMESSAGE_H 1 +#ifndef __IO_MESSAGE_H +#define __IO_MESSAGE_H 1 // IMPORTANT NOTE: only very few ASIO headers files can be included in // this file. In particular, asio.hpp should never be included here. @@ -25,8 +25,8 @@ #include -#include -#include +#include +#include namespace asiolink { @@ -46,6 +46,7 @@ class IOMessage { /// /// \name Constructors and Destructor /// + /// Note: The copy constructor and the assignment operator are /// intentionally defined as private, making this class non-copyable. //@{ @@ -96,7 +97,7 @@ private: } // asiolink -#endif // __IOMESSAGE_H +#endif // __IO_MESSAGE_H // Local Variables: // mode: c++ diff --git a/src/lib/asiolink/io_service.cc b/src/lib/asiolink/io_service.cc new file mode 100644 index 0000000000000000000000000000000000000000..55fc4b3560fce4678d2030b9bd1ad2b9c1d0fea7 --- /dev/null +++ b/src/lib/asiolink/io_service.cc @@ -0,0 +1,98 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include // for some IPC/network system calls + +#include + +#include +#include + +namespace asiolink { + +class IOServiceImpl { +private: + IOServiceImpl(const IOService& source); + IOServiceImpl& operator=(const IOService& source); +public: + /// \brief The constructor + IOServiceImpl() : + io_service_(), + work_(io_service_) + {}; + /// \brief The destructor. + ~IOServiceImpl() {}; + //@} + + /// \brief Start the underlying event loop. + /// + /// This method does not return control to the caller until + /// the \c stop() method is called via some handler. + void run() { io_service_.run(); }; + + /// \brief Run the underlying event loop for a single event. + /// + /// This method return control to the caller as soon as the + /// first handler has completed. (If no handlers are ready when + /// it is run, it will block until one is.) + void run_one() { io_service_.run_one();} ; + + /// \brief Stop the underlying event loop. + /// + /// This will return the control to the caller of the \c run() method. + void stop() { io_service_.stop();} ; + + /// \brief Return the native \c io_service object used in this wrapper. + /// + /// This is a short term work around to support other BIND 10 modules + /// that share the same \c io_service with the authoritative server. + /// It will eventually be removed once the wrapper interface is + /// generalized. + asio::io_service& get_io_service() { return io_service_; }; +private: + asio::io_service io_service_; + asio::io_service::work work_; +}; + +IOService::IOService() { + io_impl_ = new IOServiceImpl(); +} + +IOService::~IOService() { + delete io_impl_; +} + +void +IOService::run() { + io_impl_->run(); +} + +void +IOService::run_one() { + io_impl_->run_one(); +} + +void +IOService::stop() { + io_impl_->stop(); +} + +asio::io_service& +IOService::get_io_service() { + return (io_impl_->get_io_service()); +} + +} // namepsace asiolink diff --git a/src/lib/asiolink/io_service.h b/src/lib/asiolink/io_service.h new file mode 100644 index 0000000000000000000000000000000000000000..66558b7e26734988af7e3ad61e26ca39f84d9ac2 --- /dev/null +++ b/src/lib/asiolink/io_service.h @@ -0,0 +1,77 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __ASIOLINK_IO_SERVICE_H +#define __ASIOLINK_IO_SERVICE_H 1 + +namespace asio { + class io_service; +} + +namespace asiolink { + +struct IOServiceImpl; + +/// \brief The \c IOService class is a wrapper for the ASIO \c io_service +/// class. +/// +class IOService { + /// + /// \name Constructors and Destructor + /// + /// Note: The copy constructor and the assignment operator are + /// intentionally defined as private, making this class non-copyable. + //@{ +private: + IOService(const IOService& source); + IOService& operator=(const IOService& source); +public: + /// \brief The constructor + IOService(); + /// \brief The destructor. + ~IOService(); + //@} + + /// \brief Start the underlying event loop. + /// + /// This method does not return control to the caller until + /// the \c stop() method is called via some handler. + void run(); + + /// \brief Run the underlying event loop for a single event. + /// + /// This method return control to the caller as soon as the + /// first handler has completed. (If no handlers are ready when + /// it is run, it will block until one is.) + void run_one(); + + /// \brief Stop the underlying event loop. + /// + /// This will return the control to the caller of the \c run() method. + void stop(); + + /// \brief Return the native \c io_service object used in this wrapper. + /// + /// This is a short term work around to support other BIND 10 modules + /// that share the same \c io_service with the authoritative server. + /// It will eventually be removed once the wrapper interface is + /// generalized. + asio::io_service& get_io_service(); + +private: + IOServiceImpl* io_impl_; +}; + +} // namespace asiolink +#endif // __ASIOLINK_IO_SERVICE_H diff --git a/src/lib/asiolink/iosocket.cc b/src/lib/asiolink/io_socket.cc similarity index 99% rename from src/lib/asiolink/iosocket.cc rename to src/lib/asiolink/io_socket.cc index a3967d442a4e12e855c61d2b0d7a2848a8628bf6..fb325e9172c31e0b0dee7797a331fada22faf138 100644 --- a/src/lib/asiolink/iosocket.cc +++ b/src/lib/asiolink/io_socket.cc @@ -14,7 +14,7 @@ // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. -#include "iosocket.h" +#include "io_socket.h" #include diff --git a/src/lib/asiolink/iosocket.h b/src/lib/asiolink/io_socket.h similarity index 97% rename from src/lib/asiolink/iosocket.h rename to src/lib/asiolink/io_socket.h index df37d71f447faa2249e744b44ff186ddd1dc29c6..bebc8b6bf8fc8a0d6224537ac29904d167ccc160 100644 --- a/src/lib/asiolink/iosocket.h +++ b/src/lib/asiolink/io_socket.h @@ -12,8 +12,8 @@ // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. -#ifndef __IOSOCKET_H -#define __IOSOCKET_H 1 +#ifndef __IO_SOCKET_H +#define __IO_SOCKET_H 1 // IMPORTANT NOTE: only very few ASIO headers files can be included in // this file. In particular, asio.hpp should never be included here. @@ -119,9 +119,6 @@ public: static IOSocket& getDummyTCPSocket(); }; -} // asiolink -#endif // __IOSOCKET_H +} // namespace asiolink -// Local Variables: -// mode: c++ -// End: +#endif // __IO_SOCKET_H diff --git a/src/lib/asiolink/iofetch.cc b/src/lib/asiolink/iofetch.cc deleted file mode 100644 index 668a980af6ef983ce95b18b57cb1d5c7c2c0beca..0000000000000000000000000000000000000000 --- a/src/lib/asiolink/iofetch.cc +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -#include - -#include // for some IPC/network system calls -#include -#include - -#include - -#include -#include -#include - -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -using namespace asio; -using asio::ip::udp; -using asio::ip::tcp; -using isc::log::dlog; - -using namespace std; -using namespace isc::dns; - -namespace asiolink { - -// Private UDPQuery data (see internal/udpdns.h for reasons) -struct UDPQuery::PrivateData { - // UDP Socket we send query to and expect reply from there - udp::socket socket; - // Where was the query sent - udp::endpoint remote; - // TCP Socket - //tcp::socket tsocket; - // tcp endpoint - //tcp::endpoint tremote; - // What we ask the server - Question question; - // We will store the answer here - OutputBufferPtr buffer; - OutputBufferPtr msgbuf; - // Temporary buffer for answer - boost::shared_array data; - // This will be called when the data arrive or timeouts - Callback* callback; - // Did we already stop operating (data arrived, we timed out, someone - // called stop). This can be so when we are cleaning up/there are - // still pointers to us. - bool stopped; - // Timer to measure timeouts. - deadline_timer timer; - // How many milliseconds are we willing to wait for answer? - int timeout; - - PrivateData(io_service& service, - const udp::socket::protocol_type& protocol, const Question &q, - OutputBufferPtr b, Callback *c) : - socket(service, protocol), - question(q), - buffer(b), - msgbuf(new OutputBuffer(512)), - callback(c), - stopped(false), - timer(service) - { } -}; - -/// The following functions implement the \c UDPQuery class. -/// -/// The constructor -UDPQuery::UDPQuery(io_service& io_service, - const Question& q, const IOAddress& addr, uint16_t port, - OutputBufferPtr buffer, Callback *callback, int timeout) : - data_(new PrivateData(io_service, - addr.getFamily() == AF_INET ? udp::v4() : udp::v6(), q, buffer, - callback)) -{ - data_->remote = UDPEndpoint(addr, port).getASIOEndpoint(); - data_->timeout = timeout; -} - -/// The function operator is implemented with the "stackless coroutine" -/// pattern; see internal/coroutine.h for details. -void -UDPQuery::operator()(error_code ec, size_t length) { - if (ec || data_->stopped) { - return; - } - - CORO_REENTER (this) { - /// Generate the upstream query and render it to wire format - /// This is done in a different scope to allow inline variable - /// declarations. - { - Message msg(Message::RENDER); - - // XXX: replace with boost::random or some other suitable PRNG - msg.setQid(0); - msg.setOpcode(Opcode::QUERY()); - msg.setRcode(Rcode::NOERROR()); - msg.setHeaderFlag(Message::HEADERFLAG_RD); - msg.addQuestion(data_->question); - MessageRenderer renderer(*data_->msgbuf); - msg.toWire(renderer); - dlog("Sending " + msg.toText() + " to " + - data_->remote.address().to_string()); - } - - - // If we timeout, we stop, which will shutdown everything and - // cancel all other attempts to run inside the coroutine - if (data_->timeout != -1) { - data_->timer.expires_from_now(boost::posix_time::milliseconds( - data_->timeout)); - data_->timer.async_wait(boost::bind(&UDPQuery::stop, *this, - TIME_OUT)); - } - - // Begin an asynchronous send, and then yield. When the - // send completes, we will resume immediately after this point. - CORO_YIELD data_->socket.async_send_to(buffer(data_->msgbuf->getData(), - data_->msgbuf->getLength()), data_->remote, *this); - - /// Allocate space for the response. (XXX: This should be - /// optimized by maintaining a free list of pre-allocated blocks) - data_->data.reset(new char[MAX_LENGTH]); - - /// Begin an asynchronous receive, and yield. When the receive - /// completes, we will resume immediately after this point. - CORO_YIELD data_->socket.async_receive_from(buffer(data_->data.get(), - MAX_LENGTH), data_->remote, *this); - // The message is not rendered yet, so we can't print it easilly - dlog("Received response from " + data_->remote.address().to_string()); - - /// Copy the answer into the response buffer. (XXX: If the - /// OutputBuffer object were made to meet the requirements of - /// a MutableBufferSequence, then it could be written to directly - /// by async_recieve_from() and this additional copy step would - /// be unnecessary.) - data_->buffer->writeData(data_->data.get(), length); - - /// We are done - stop(SUCCESS); - } -} - -void -UDPQuery::stop(Result result) { - if (!data_->stopped) { - switch (result) { - case TIME_OUT: - dlog("Query timed out"); - break; - case STOPPED: - dlog("Query stopped"); - break; - default:; - } - data_->stopped = true; - data_->socket.cancel(); - data_->socket.close(); - data_->timer.cancel(); - if (data_->callback) { - (*data_->callback)(result); - } - } -} - -} diff --git a/src/lib/asiolink/recursive_query.cc b/src/lib/asiolink/recursive_query.cc new file mode 100644 index 0000000000000000000000000000000000000000..5b767a898029489ea3fa08f18510f1ee35436f79 --- /dev/null +++ b/src/lib/asiolink/recursive_query.cc @@ -0,0 +1,543 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include +#include // for some IPC/network system calls + +#include +#include + +#include + +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +using isc::log::dlog; +using namespace isc::dns; + +namespace asiolink { + +typedef std::vector > AddressVector; + +// Here we do not use the typedef above, as the SunStudio compiler +// mishandles this in its name mangling, and wouldn't compile. +// We can probably use a typedef, but need to move it to a central +// location and use it consistently. +RecursiveQuery::RecursiveQuery(DNSService& dns_service, + const std::vector >& upstream, + const std::vector >& upstream_root, + int query_timeout, int client_timeout, int lookup_timeout, + unsigned retries) : + dns_service_(dns_service), upstream_(new AddressVector(upstream)), + upstream_root_(new AddressVector(upstream_root)), + query_timeout_(query_timeout), client_timeout_(client_timeout), + lookup_timeout_(lookup_timeout), retries_(retries) +{} + +namespace { + +typedef std::pair addr_t; + +/* + * This is a query in progress. When a new query is made, this one holds + * the context information about it, like how many times we are allowed + * to retry on failure, what to do when we succeed, etc. + * + * Used by RecursiveQuery::sendQuery. + */ +class RunningQuery : public IOFetch::Callback { +private: + // The io service to handle async calls + IOService& io_; + + // Info for (re)sending the query (the question and destination) + Question question_; + + // This is where we build and store our final answer + MessagePtr answer_message_; + + // currently we use upstream as the current list of NS records + // we should differentiate between forwarding and resolving + boost::shared_ptr upstream_; + + // root servers...just copied over to the zone_servers_ + boost::shared_ptr upstream_root_; + + // Buffer to store the result. + OutputBufferPtr buffer_; + + // Server to notify when we succeed or fail + //shared_ptr server_; + isc::resolve::ResolverInterface::CallbackPtr resolvercallback_; + + // To prevent both unreasonably long cname chains and cname loops, + // we simply keep a counter of the number of CNAMEs we have + // followed so far (and error if it exceeds RESOLVER_MAX_CNAME_CHAIN + // from lib/resolve/response_classifier.h) + unsigned cname_count_; + + /* + * TODO Do something more clever with timeouts. In the long term, some + * computation of average RTT, increase with each retry, etc. + */ + // Timeout information + int query_timeout_; + unsigned retries_; + + // normal query state + + // Not using NSAS at this moment, so we keep a list + // of 'current' zone servers + std::vector zone_servers_; + + // Update the question that will be sent to the server + void setQuestion(const Question& new_question) { + question_ = new_question; + } + + // TODO: replace by our wrapper + asio::deadline_timer client_timer; + asio::deadline_timer lookup_timer; + + size_t queries_out_; + + // If we timed out ourselves (lookup timeout), stop issuing queries + bool done_; + + // If we have a client timeout, we send back an answer, but don't + // stop. We use this variable to make sure we don't send another + // answer if we do find one later (or if we have a lookup_timeout) + bool answer_sent_; + + // Reference to our cache + isc::cache::ResolverCache& cache_; + + // perform a single lookup; first we check the cache to see + // if we have a response for our query stored already. if + // so, call handlerecursiveresponse(), if not, we call send() + void doLookup() { + dlog("doLookup: try cache"); + Message cached_message(Message::RENDER); + isc::resolve::initResponseMessage(question_, cached_message); + if (cache_.lookup(question_.getName(), question_.getType(), + question_.getClass(), cached_message)) { + dlog("Message found in cache, returning that"); + handleRecursiveAnswer(cached_message); + } else { + send(); + } + + } + + // (re)send the query to the server. + void send() { + const int uc = upstream_->size(); + const int zs = zone_servers_.size(); + buffer_->clear(); + if (uc > 0) { + int serverIndex = rand() % uc; + dlog("Sending upstream query (" + question_.toText() + + ") to " + upstream_->at(serverIndex).first); + IOFetch query(IPPROTO_UDP, io_, question_, + upstream_->at(serverIndex).first, + upstream_->at(serverIndex).second, buffer_, this, + query_timeout_); + ++queries_out_; + io_.get_io_service().post(query); + } else if (zs > 0) { + int serverIndex = rand() % zs; + dlog("Sending query to zone server (" + question_.toText() + + ") to " + zone_servers_.at(serverIndex).first); + IOFetch query(IPPROTO_IDP, io_, question_, + zone_servers_.at(serverIndex).first, + zone_servers_.at(serverIndex).second, buffer_, this, + query_timeout_); + ++queries_out_; + io_.get_io_service().post(query); + } else { + dlog("Error, no upstream servers to send to."); + } + } + + // This function is called by operator() if there is an actual + // answer from a server and we are in recursive mode + // depending on the contents, we go on recursing or return + // + // Note that the footprint may change as this function may + // need to append data to the answer we are building later. + // + // returns true if we are done (either we have an answer or an + // error message) + // returns false if we are not done + bool handleRecursiveAnswer(const Message& incoming) { + dlog("Handle response"); + // In case we get a CNAME, we store the target + // here (classify() will set it when it walks through + // the cname chain to verify it). + Name cname_target(question_.getName()); + + isc::resolve::ResponseClassifier::Category category = + isc::resolve::ResponseClassifier::classify( + question_, incoming, cname_target, cname_count_, true); + + bool found_ns_address = false; + + // If the packet is OK, store it in the cache + if (!isc::resolve::ResponseClassifier::error(category)) { + cache_.update(incoming); + } + + switch (category) { + case isc::resolve::ResponseClassifier::ANSWER: + case isc::resolve::ResponseClassifier::ANSWERCNAME: + // Done. copy and return. + isc::resolve::copyResponseMessage(incoming, answer_message_); + return true; + break; + case isc::resolve::ResponseClassifier::CNAME: + dlog("Response is CNAME!"); + // (unfinished) CNAME. We set our question_ to the CNAME + // target, then start over at the beginning (for now, that + // is, we reset our 'current servers' to the root servers). + if (cname_count_ >= RESOLVER_MAX_CNAME_CHAIN) { + // just give up + dlog("CNAME chain too long"); + isc::resolve::makeErrorMessage(answer_message_, + Rcode::SERVFAIL()); + return true; + } + + answer_message_->appendSection(Message::SECTION_ANSWER, + incoming); + setZoneServersToRoot(); + + question_ = Question(cname_target, question_.getClass(), + question_.getType()); + + dlog("Following CNAME chain to " + question_.toText()); + doLookup(); + return false; + break; + case isc::resolve::ResponseClassifier::NXDOMAIN: + // NXDOMAIN, just copy and return. + isc::resolve::copyResponseMessage(incoming, answer_message_); + return true; + break; + case isc::resolve::ResponseClassifier::REFERRAL: + // Referral. For now we just take the first glue address + // we find and continue with that + zone_servers_.clear(); + + for (RRsetIterator rrsi = incoming.beginSection(Message::SECTION_ADDITIONAL); + rrsi != incoming.endSection(Message::SECTION_ADDITIONAL) && !found_ns_address; + rrsi++) { + ConstRRsetPtr rrs = *rrsi; + if (rrs->getType() == RRType::A()) { + // found address + RdataIteratorPtr rdi = rrs->getRdataIterator(); + // just use the first for now + if (!rdi->isLast()) { + std::string addr_str = rdi->getCurrent().toText(); + dlog("[XX] first address found: " + addr_str); + // now we have one address, simply + // resend that exact same query + // to that address and yield, when it + // returns, loop again. + + // TODO should use NSAS + zone_servers_.push_back(addr_t(addr_str, 53)); + found_ns_address = true; + break; + } + } + } + if (found_ns_address) { + // next resolver round + // we do NOT use doLookup() here, but send() (i.e. we + // skip the cache), since if we had the final answer + // instead of a delegation cached, we would have been + // there by now. + send(); + return false; + } else { + dlog("[XX] no ready-made addresses in additional. need nsas."); + // TODO this will result in answering with the delegation. oh well + isc::resolve::copyResponseMessage(incoming, answer_message_); + return true; + } + break; + case isc::resolve::ResponseClassifier::EMPTY: + case isc::resolve::ResponseClassifier::EXTRADATA: + case isc::resolve::ResponseClassifier::INVNAMCLASS: + case isc::resolve::ResponseClassifier::INVTYPE: + case isc::resolve::ResponseClassifier::MISMATQUEST: + case isc::resolve::ResponseClassifier::MULTICLASS: + case isc::resolve::ResponseClassifier::NOTONEQUEST: + case isc::resolve::ResponseClassifier::NOTRESPONSE: + case isc::resolve::ResponseClassifier::NOTSINGLE: + case isc::resolve::ResponseClassifier::OPCODE: + case isc::resolve::ResponseClassifier::RCODE: + case isc::resolve::ResponseClassifier::TRUNCATED: + // Should we try a different server rather than SERVFAIL? + isc::resolve::makeErrorMessage(answer_message_, + Rcode::SERVFAIL()); + return true; + break; + } + // should not be reached. assert here? + dlog("[FATAL] unreachable code"); + return true; + } + +public: + RunningQuery(IOService& io, + const Question &question, + MessagePtr answer_message, + boost::shared_ptr upstream, + boost::shared_ptr upstream_root, + OutputBufferPtr buffer, + isc::resolve::ResolverInterface::CallbackPtr cb, + int query_timeout, int client_timeout, int lookup_timeout, + unsigned retries, + isc::cache::ResolverCache& cache) : + io_(io), + question_(question), + answer_message_(answer_message), + upstream_(upstream), + upstream_root_(upstream_root), + buffer_(buffer), + resolvercallback_(cb), + cname_count_(0), + query_timeout_(query_timeout), + retries_(retries), + client_timer(io.get_io_service()), + lookup_timer(io.get_io_service()), + queries_out_(0), + done_(false), + answer_sent_(false), + cache_(cache) + { + // Setup the timer to stop trying (lookup_timeout) + if (lookup_timeout >= 0) { + lookup_timer.expires_from_now( + boost::posix_time::milliseconds(lookup_timeout)); + lookup_timer.async_wait(boost::bind(&RunningQuery::stop, this, false)); + } + + // Setup the timer to send an answer (client_timeout) + if (client_timeout >= 0) { + client_timer.expires_from_now( + boost::posix_time::milliseconds(client_timeout)); + client_timer.async_wait(boost::bind(&RunningQuery::clientTimeout, this)); + } + + // should use NSAS for root servers + // Adding root servers if not a forwarder + if (upstream_->empty()) { + setZoneServersToRoot(); + } + + doLookup(); + } + + void setZoneServersToRoot() { + zone_servers_.clear(); + if (upstream_root_->empty()) { //if no root ips given, use this + zone_servers_.push_back(addr_t("192.5.5.241", 53)); + } else { + // copy the list + dlog("Size is " + + boost::lexical_cast(upstream_root_->size()) + + "\n"); + for(AddressVector::iterator it = upstream_root_->begin(); + it < upstream_root_->end(); ++it) { + zone_servers_.push_back(addr_t(it->first,it->second)); + dlog("Put " + zone_servers_.back().first + "into root list\n"); + } + } + } + virtual void clientTimeout() { + // Return a SERVFAIL, but do not stop until + // we have an answer or timeout ourselves + isc::resolve::makeErrorMessage(answer_message_, + Rcode::SERVFAIL()); + if (!answer_sent_) { + answer_sent_ = true; + resolvercallback_->success(answer_message_); + } + } + + virtual void stop(bool resume) { + // if we cancel our timers, we will still get an event for + // that, so we cannot delete ourselves just yet (those events + // would be bound to a deleted object) + // cancel them one by one, both cancels should get us back + // here again. + // same goes if we have an outstanding query (can't delete + // until that one comes back to us) + done_ = true; + if (resume && !answer_sent_) { + answer_sent_ = true; + + // There are two types of messages we could store in the + // cache; + // 1. answers to our fetches from authoritative servers, + // exactly as we receive them, and + // 2. answers to queries we received from clients, which + // have received additional processing (following CNAME + // chains, for instance) + // + // Doing only the first would mean we would have to re-do + // processing when we get data from our cache, and doing + // only the second would miss out on the side-effect of + // having nameserver data in our cache. + // + // So right now we do both. Since the cache (currently) + // stores Messages on their question section only, this + // does mean that we overwrite the messages we stored in + // the previous iteration if we are following a delegation. + cache_.update(*answer_message_); + + resolvercallback_->success(answer_message_); + } else { + resolvercallback_->failure(); + } + if (lookup_timer.cancel() != 0) { + return; + } + if (client_timer.cancel() != 0) { + return; + } + if (queries_out_ > 0) { + return; + } + delete this; + } + + // This function is used as callback from DNSQuery. + virtual void operator()(IOFetch::Result result) { + // XXX is this the place for TCP retry? + --queries_out_; + if (!done_ && result != IOFetch::TIME_OUT) { + // we got an answer + Message incoming(Message::PARSE); + InputBuffer ibuf(buffer_->getData(), buffer_->getLength()); + incoming.fromWire(ibuf); + + if (upstream_->size() == 0 && + incoming.getRcode() == Rcode::NOERROR()) { + done_ = handleRecursiveAnswer(incoming); + } else { + isc::resolve::copyResponseMessage(incoming, answer_message_); + done_ = true; + } + + if (done_) { + stop(true); + } + } else if (!done_ && retries_--) { + // We timed out, but we have some retries, so send again + dlog("Timeout, resending query"); + send(); + } else { + // out of retries, give up for now + stop(false); + } + } +}; + +} + +void +RecursiveQuery::resolve(const QuestionPtr& question, + const isc::resolve::ResolverInterface::CallbackPtr callback) +{ + IOService& io = dns_service_.getIOService(); + + MessagePtr answer_message(new Message(Message::RENDER)); + isc::resolve::initResponseMessage(*question, *answer_message); + + OutputBufferPtr buffer(new OutputBuffer(0)); + + dlog("Try out cache first (direct call to resolve)"); + // First try to see if we have something cached in the messagecache + if (cache_.lookup(question->getName(), question->getType(), + question->getClass(), *answer_message)) { + dlog("Message found in cache, returning that"); + // TODO: err, should cache set rcode as well? + answer_message->setRcode(Rcode::NOERROR()); + callback->success(answer_message); + } else { + dlog("Message not found in cache, starting recursive query"); + // It will delete itself when it is done + new RunningQuery(io, *question, answer_message, upstream_, + upstream_root_, buffer, callback, query_timeout_, + client_timeout_, lookup_timeout_, retries_, + cache_); + } +} + +void +RecursiveQuery::resolve(const Question& question, + MessagePtr answer_message, + OutputBufferPtr buffer, + DNSServer* server) +{ + // XXX: eventually we will need to be able to determine whether + // the message should be sent via TCP or UDP, or sent initially via + // UDP and then fall back to TCP on failure, but for the moment + // we're only going to handle UDP. + IOService& io = dns_service_.getIOService(); + + isc::resolve::ResolverInterface::CallbackPtr crs( + new isc::resolve::ResolverCallbackServer(server)); + + // TODO: general 'prepareinitialanswer' + answer_message->setOpcode(isc::dns::Opcode::QUERY()); + answer_message->addQuestion(question); + + // First try to see if we have something cached in the messagecache + dlog("Try out cache first (started by incoming event)"); + if (cache_.lookup(question.getName(), question.getType(), + question.getClass(), *answer_message)) { + dlog("Message found in cache, returning that"); + // TODO: err, should cache set rcode as well? + answer_message->setRcode(Rcode::NOERROR()); + crs->success(answer_message); + } else { + dlog("Message not found in cache, starting recursive query"); + // It will delete itself when it is done + new RunningQuery(io, question, answer_message, upstream_, upstream_root_, + buffer, crs, query_timeout_, client_timeout_, + lookup_timeout_, retries_, cache_); + } +} + + + +} // namespace asiolink diff --git a/src/lib/asiolink/recursive_query.h b/src/lib/asiolink/recursive_query.h new file mode 100644 index 0000000000000000000000000000000000000000..6ef00694838bd1e921abd41f4102f347c328ec27 --- /dev/null +++ b/src/lib/asiolink/recursive_query.h @@ -0,0 +1,117 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __ASIOLINK_RECURSIVE_QUERY_H +#define __ASIOLINK_RECURSIVE_QUERY_H 1 + +#include +#include +#include +#include + +namespace asiolink { +/// \brief The \c RecursiveQuery class provides a layer of abstraction around +/// the ASIO code that carries out an upstream query. +/// +/// This design is very preliminary; currently it is only capable of +/// handling simple forward requests to a single resolver. +class RecursiveQuery { + /// + /// \name Constructors + /// + //@{ +public: + /// \brief Constructor + /// + /// This is currently the only way to construct \c RecursiveQuery + /// object. If the addresses of the forward nameservers is specified, + /// and every upstream query will be sent to one random address, and + /// the result sent back directly. If not, it will do full resolving. + /// + /// \param dns_service The DNS Service to perform the recursive + /// query on. + /// \param upstream Addresses and ports of the upstream servers + /// to forward queries to. + /// \param upstream_root Addresses and ports of the root servers + /// to use when resolving. + /// \param query_timeout Timeout value for queries we sent, in ms + /// \param client_timeout Timeout value for when we send back an + /// error, in ms + /// \param lookup_timeout Timeout value for when we give up, in ms + /// \param retries how many times we try again (0 means just send and + /// and return if it returs). + RecursiveQuery(DNSService& dns_service, + const std::vector >& + upstream, + const std::vector >& + upstream_root, + int query_timeout = 2000, + int client_timeout = 4000, + int lookup_timeout = 30000, + unsigned retries = 3); + //@} + + /// \brief Initiate resolving + /// + /// When sendQuery() is called, a (set of) message(s) is sent + /// asynchronously. If upstream servers are set, one is chosen + /// and the response (if any) from that server will be returned. + /// + /// If not upstream is set, a root server is chosen from the + /// root_servers, and the RunningQuery shall do a full resolve + /// (i.e. if the answer is a delegation, it will be followed, etc.) + /// until there is an answer or an error. + /// + /// When there is a response or an error and we give up, the given + /// CallbackPtr object shall be called (with either success() or + /// failure(). See ResolverInterface::Callback for more information. + /// + /// \param question The question being answered + /// \param callback Callback object. See + /// \c ResolverInterface::Callback for more information + void resolve(const isc::dns::QuestionPtr& question, + const isc::resolve::ResolverInterface::CallbackPtr callback); + + + /// \brief Initiates resolving for the given question. + /// + /// This actually calls the previous sendQuery() with a default + /// callback object, which calls resume() on the given DNSServer + /// object. + /// + /// \param question The question being answered + /// \param answer_message An output Message into which the final response will be copied + /// \param buffer An output buffer into which the intermediate responses will be copied + /// \param server A pointer to the \c DNSServer object handling the client + void resolve(const isc::dns::Question& question, + isc::dns::MessagePtr answer_message, + isc::dns::OutputBufferPtr buffer, + DNSServer* server); +private: + DNSService& dns_service_; + boost::shared_ptr > > + upstream_; + boost::shared_ptr > > + upstream_root_; + int query_timeout_; + int client_timeout_; + int lookup_timeout_; + unsigned retries_; + // Cache. TODO: I think we want this initialized in Resolver class, + // not here + isc::cache::ResolverCache cache_; +}; + +} // namespace asiolink +#endif // __ASIOLINK_RECURSIVE_QUERY_H diff --git a/src/lib/asiolink/simple_callback.h b/src/lib/asiolink/simple_callback.h new file mode 100644 index 0000000000000000000000000000000000000000..ab5deaf12f65f2b93d0b535c9876638e64595294 --- /dev/null +++ b/src/lib/asiolink/simple_callback.h @@ -0,0 +1,71 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __ASIOLINK_SIMPLE_CALLBACK_H +#define __ASIOLINK_SIMPLE_CALLBACK_H 1 + +#include + +namespace asiolink { + +/// \brief The \c SimpleCallback class is an abstract base class for a +/// simple callback function with the signature: +/// +/// void simpleCallback(const IOMessage& io_message) const; +/// +/// Specific derived class implementations are hidden within the +/// implementation. Instances of the derived classes can be called +/// as functions via the operator() interface. Pointers to these +/// instances can then be provided to the \c IOService class +/// via its constructor. +/// +/// The \c SimpleCallback is expected to be used for basic, generic +/// tasks such as checking for configuration changes. It may also be +/// used for testing purposes. +class SimpleCallback { + /// + /// \name Constructors and Destructor + /// + /// Note: The copy constructor and the assignment operator are + /// intentionally defined as private, making this class non-copyable. + //@{ +private: + SimpleCallback(const SimpleCallback& source); + SimpleCallback& operator=(const SimpleCallback& source); +protected: + /// \brief The default constructor. + /// + /// This is intentionally defined as \c protected as this base class + /// should never be instantiated (except as part of a derived class). + SimpleCallback() : self_(this) {} +public: + /// \brief The destructor + virtual ~SimpleCallback() {} + /// \brief The function operator + //@} + /// + /// This makes its call indirectly via the "self" pointer, ensuring + /// that the function ultimately invoked will be the one in the derived + /// class. + /// + /// \param io_message The event message to handle + virtual void operator()(const IOMessage& io_message) const { + (*self_)(io_message); + } +private: + SimpleCallback* self_; +}; + +} // namespace asiolink +#endif // __ASIOLINK_SIMPLE_CALLBACK_H diff --git a/src/lib/asiolink/tcp_endpoint.h b/src/lib/asiolink/tcp_endpoint.h new file mode 100644 index 0000000000000000000000000000000000000000..8f6270f3b328f75162c6e6af28b9b0f64281fd1e --- /dev/null +++ b/src/lib/asiolink/tcp_endpoint.h @@ -0,0 +1,98 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __TCP_ENDPOINT_H +#define __TCP_ENDPOINT_H 1 + +#ifndef ASIO_HPP +#error "asio.hpp must be included before including this, see asiolink.h as to why" +#endif + +#include + +namespace asiolink { + +/// \brief The \c TCPEndpoint class is a concrete derived class of +/// \c IOEndpoint that represents an endpoint of a TCP connection. +/// +/// In the current implementation, an object of this class is always +/// instantiated within the wrapper routines. Applications are expected to +/// get access to the object via the abstract base class, \c IOEndpoint. +/// This design may be changed when we generalize the wrapper interface. +/// +/// Note: this implementation is optimized for the case where this object +/// is created from an ASIO endpoint object in a receiving code path +/// by avoiding to make a copy of the base endpoint. For TCP it may not be +/// a big deal, but when we receive UDP packets at a high rate, the copy +/// overhead might be significant. +class TCPEndpoint : public IOEndpoint { +public: + /// + /// \name Constructors and Destructor + /// + //@{ + /// \brief Constructor from a pair of address and port. + /// + /// \param address The IP address of the endpoint. + /// \param port The TCP port number of the endpoint. + TCPEndpoint(const IOAddress& address, const unsigned short port) : + asio_endpoint_placeholder_( + new asio::ip::tcp::endpoint( + asio::ip::address::from_string(address.toText()), port)), + asio_endpoint_(*asio_endpoint_placeholder_) + {} + + /// \brief Constructor from an ASIO TCP endpoint. + /// + /// This constructor is designed to be an efficient wrapper for the + /// corresponding ASIO class, \c tcp::endpoint. + /// + /// \param asio_endpoint The ASIO representation of the TCP endpoint. + TCPEndpoint(const asio::ip::tcp::endpoint& asio_endpoint) : + asio_endpoint_placeholder_(NULL), asio_endpoint_(asio_endpoint) + {} + + /// \brief The destructor. + ~TCPEndpoint() { delete asio_endpoint_placeholder_; } + //@} + + IOAddress getAddress() const { + return (asio_endpoint_.address()); + } + + uint16_t getPort() const { + return (asio_endpoint_.port()); + } + + short getProtocol() const { + return (asio_endpoint_.protocol().protocol()); + } + + short getFamily() const { + return (asio_endpoint_.protocol().family()); + } + + // This is not part of the exosed IOEndpoint API but allows + // direct access to the ASIO implementation of the endpoint + const asio::ip::tcp::endpoint& getASIOEndpoint() const { + return (asio_endpoint_); + } + +private: + const asio::ip::tcp::endpoint* asio_endpoint_placeholder_; + const asio::ip::tcp::endpoint& asio_endpoint_; +}; + +} // namespace asiolink +#endif // __TCP_ENDPOINT_H diff --git a/src/lib/asiolink/tcpdns.cc b/src/lib/asiolink/tcp_server.cc similarity index 88% rename from src/lib/asiolink/tcpdns.cc rename to src/lib/asiolink/tcp_server.cc index c00b87aa300ab3b4cc7093a6102327503a501c47..414cdc7eacc173ab6b83a06e3961072f4755fa28 100644 --- a/src/lib/asiolink/tcpdns.cc +++ b/src/lib/asiolink/tcp_server.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -14,22 +14,20 @@ #include -#include // for some IPC/network system calls -#include #include +#include +#include // for some IPC/network system calls -#include -#include +#include -#include -#include +#include -#include -#include +#include +#include +#include +#include +#include -#include -#include -#include using namespace asio; using asio::ip::udp; @@ -39,7 +37,8 @@ using namespace std; using namespace isc::dns; namespace asiolink { -/// The following functions implement the \c UDPServer class. + +/// The following functions implement the \c TCPServer class. /// /// The constructor TCPServer::TCPServer(io_service& io_service, @@ -119,7 +118,14 @@ TCPServer::operator()(error_code ec, size_t length) { // that would quickly generate an IOMessage object without // all these calls to "new".) peer_.reset(new TCPEndpoint(socket_->remote_endpoint())); - iosock_.reset(new TCPSocket(*socket_)); + + // The TCP socket class has been extended with asynchronous functions + // and takes as a template parameter a completion callback class. As + // TCPServer does not use these extended functions (only those defined + // in the IOSocket base class) - but needs a TCPSocket to get hold of + // the underlying Boost TCP socket - DummyIOCallback is used. This + // provides the appropriate operator() but is otherwise functionless. + iosock_.reset(new TCPSocket(*socket_)); io_message_.reset(new IOMessage(data_.get(), length, *iosock_, *peer_)); bytes_ = length; @@ -191,4 +197,5 @@ TCPServer::resume(const bool done) { io_.post(*this); } -} +} // namespace asiolink + diff --git a/src/lib/asiolink/internal/tcpdns.h b/src/lib/asiolink/tcp_server.h similarity index 53% rename from src/lib/asiolink/internal/tcpdns.h rename to src/lib/asiolink/tcp_server.h index a97ed17bdf81cfdab75cee731805b6690ce489cb..9b985ce9a9cfc372789d0ea208c1be2640acae70 100644 --- a/src/lib/asiolink/internal/tcpdns.h +++ b/src/lib/asiolink/tcp_server.h @@ -1,4 +1,4 @@ -// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -12,121 +12,21 @@ // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. -#ifndef __TCPDNS_H -#define __TCPDNS_H 1 +#ifndef __TCP_SERVER_H +#define __TCP_SERVER_H 1 -#include +#ifndef ASIO_HPP +#error "asio.hpp must be included before including this, see asiolink.h as to why" +#endif - -#include #include #include -#include -#include - #include -#include +#include -// This file contains TCP-specific implementations of generic classes -// defined in asiolink.h. It is *not* intended to be part of the public -// API. namespace asiolink { -/// \brief The \c TCPEndpoint class is a concrete derived class of -/// \c IOEndpoint that represents an endpoint of a TCP connection. -/// -/// In the current implementation, an object of this class is always -/// instantiated within the wrapper routines. Applications are expected to -/// get access to the object via the abstract base class, \c IOEndpoint. -/// This design may be changed when we generalize the wrapper interface. -/// -/// Note: this implementation is optimized for the case where this object -/// is created from an ASIO endpoint object in a receiving code path -/// by avoiding to make a copy of the base endpoint. For TCP it may not be -/// a big deal, but when we receive UDP packets at a high rate, the copy -/// overhead might be significant. -class TCPEndpoint : public IOEndpoint { -public: - /// - /// \name Constructors and Destructor - /// - //@{ - /// \brief Constructor from a pair of address and port. - /// - /// \param address The IP address of the endpoint. - /// \param port The TCP port number of the endpoint. - TCPEndpoint(const IOAddress& address, const unsigned short port) : - asio_endpoint_placeholder_( - new asio::ip::tcp::endpoint( - asio::ip::address::from_string(address.toText()), port)), - asio_endpoint_(*asio_endpoint_placeholder_) - {} - - /// \brief Constructor from an ASIO TCP endpoint. - /// - /// This constructor is designed to be an efficient wrapper for the - /// corresponding ASIO class, \c tcp::endpoint. - /// - /// \param asio_endpoint The ASIO representation of the TCP endpoint. - TCPEndpoint(const asio::ip::tcp::endpoint& asio_endpoint) : - asio_endpoint_placeholder_(NULL), asio_endpoint_(asio_endpoint) - {} - - /// \brief The destructor. - ~TCPEndpoint() { delete asio_endpoint_placeholder_; } - //@} - - IOAddress getAddress() const { - return (asio_endpoint_.address()); - } - - uint16_t getPort() const { - return (asio_endpoint_.port()); - } - - short getProtocol() const { - return (asio_endpoint_.protocol().protocol()); - } - - short getFamily() const { - return (asio_endpoint_.protocol().family()); - } - - // This is not part of the exosed IOEndpoint API but allows - // direct access to the ASIO implementation of the endpoint - const asio::ip::tcp::endpoint& getASIOEndpoint() const { - return (asio_endpoint_); - } - -private: - const asio::ip::tcp::endpoint* asio_endpoint_placeholder_; - const asio::ip::tcp::endpoint& asio_endpoint_; -}; - -/// \brief The \c TCPSocket class is a concrete derived class of -/// \c IOSocket that represents a TCP socket. -/// -/// In the current implementation, an object of this class is always -/// instantiated within the wrapper routines. Applications are expected to -/// get access to the object via the abstract base class, \c IOSocket. -/// This design may be changed when we generalize the wrapper interface. -class TCPSocket : public IOSocket { -private: - TCPSocket(const TCPSocket& source); - TCPSocket& operator=(const TCPSocket& source); -public: - /// \brief Constructor from an ASIO TCP socket. - /// - /// \param socket The ASIO representation of the TCP socket. - TCPSocket(asio::ip::tcp::socket& socket) : socket_(socket) {} - - int getNative() const { return (socket_.native()); } - int getProtocol() const { return (IPPROTO_TCP); } - -private: - asio::ip::tcp::socket& socket_; -}; /// \brief A TCP-specific \c DNSServer object. /// @@ -215,10 +115,5 @@ private: boost::shared_ptr iosock_; }; -} - -#endif // __TCPDNS_H - -// Local Variables: -// mode: c++ -// End: +} // namespace asiolink +#endif // __TCP_SERVER_H diff --git a/src/lib/asiolink/tcp_socket.h b/src/lib/asiolink/tcp_socket.h new file mode 100644 index 0000000000000000000000000000000000000000..5a85aaa6330e1aa8e3ac6375283a9dfb64ae8ada --- /dev/null +++ b/src/lib/asiolink/tcp_socket.h @@ -0,0 +1,277 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __TCP_SOCKET_H +#define __TCP_SOCKET_H 1 + +#ifndef ASIO_HPP +#error "asio.hpp must be included before including this, see asiolink.h as to why" +#endif + +#include +#include +#include +#include // for some IPC/network system calls + +#include +#include + +#include + +#include +#include +#include +#include + +namespace asiolink { + +/// \brief The \c TCPSocket class is a concrete derived class of \c IOAsioSocket +/// that represents a TCP socket. +/// +/// \param C Callback type +template +class TCPSocket : public IOAsioSocket { +private: + /// \brief Class is non-copyable + TCPSocket(const TCPSocket&); + TCPSocket& operator=(const TCPSocket&); + +public: + + /// \brief Constructor from an ASIO TCP socket. + /// + /// \param socket The ASIO representation of the TCP socket. It + /// is assumed that the caller will open and close the socket, so + /// these operations are a no-op for that socket. + TCPSocket(asio::ip::tcp::socket& socket); + + /// \brief Constructor + /// + /// Used when the TCPSocket is being asked to manage its own internal + /// socket. It is assumed that open() and close() will not be used. + /// + /// \param service I/O Service object used to manage the socket. + TCPSocket(IOService& service); + + /// \brief Destructor + virtual ~TCPSocket(); + + virtual int getNative() const { return (socket_.native()); } + virtual int getProtocol() const { return (IPPROTO_TCP); } + + /// \brief Open Socket + /// + /// Opens the TCP socket. In the model for transport-layer agnostic I/O, + /// an "open" operation includes a connection to the remote end (which + /// may take time). This does not happen for TCP, so the method returns + /// "false" to indicate that the operation completed synchronously. + /// + /// \param endpoint Endpoint to which the socket will connect to. + /// \param callback Unused. + /// + /// \return false to indicate that the "operation" completed synchronously. + virtual bool open(const IOEndpoint* endpoint, C&); + + /// \brief Send Asynchronously + /// + /// This corresponds to async_send_to() for TCP sockets and async_send() + /// for TCP. In both cases an endpoint argument is supplied indicating the + /// target of the send - this is ignored for TCP. + /// + /// \param data Data to send + /// \param length Length of data to send + /// \param endpoint Target of the send + /// \param callback Callback object. + virtual void asyncSend(const void* data, size_t length, + const IOEndpoint* endpoint, C& callback); + + /// \brief Receive Asynchronously + /// + /// This correstponds to async_receive_from() for TCP sockets and + /// async_receive() for TCP. In both cases, an endpoint argument is + /// supplied to receive the source of the communication. For TCP it will + /// be filled in with details of the connection. + /// + /// \param data Buffer to receive incoming message + /// \param length Length of the data buffer + /// \param cumulative Amount of data that should already be in the buffer. + /// (This is ignored - every UPD receive fills the buffer from the start.) + /// \param endpoint Source of the communication + /// \param callback Callback object + virtual void asyncReceive(void* data, size_t length, size_t cumulative, + IOEndpoint* endpoint, C& callback); + + /// \brief Checks if the data received is complete. + /// + /// As all the data is received in one I/O, so this is, this is effectively + /// a no-op (although it does update the amount of data received). + /// + /// \param data Data buffer containing data to date. (This is ignored + /// for TCP receives.) + /// \param length Amount of data received in last asynchronous I/O + /// \param cumulative On input, amount of data received before the last + /// I/O. On output, the total amount of data received to date. + /// + /// \return true if the receive is complete, false if another receive is + /// needed. + virtual bool receiveComplete(void*, size_t length, size_t& cumulative) { + cumulative = length; + return (true); + } + + /// \brief Cancel I/O On Socket + virtual void cancel(); + + /// \brief Close socket + virtual void close(); + + +private: + // Two variables to hold the socket - a socket and a pointer to it. This + // handles the case where a socket is passed to the TCPSocket on + // construction, or where it is asked to manage its own socket. + asio::ip::tcp::socket* socket_ptr_; ///< Pointer to own socket + asio::ip::tcp::socket& socket_; ///< Socket + bool isopen_; ///< true when socket is open +}; + +// Constructor - caller manages socket + +template +TCPSocket::TCPSocket(asio::ip::tcp::socket& socket) : + socket_ptr_(NULL), socket_(socket), isopen_(true) +{ +} + +// Constructor - create socket on the fly + +template +TCPSocket::TCPSocket(IOService& service) : + socket_ptr_(new asio::ip::tcp::socket(service.get_io_service())), + socket_(*socket_ptr_), isopen_(false) +{ +} + +// Destructor. Only delete the socket if we are managing it. + +template +TCPSocket::~TCPSocket() +{ + delete socket_ptr_; +} + +// Open the socket. Throws an error on failure +// TODO: Make the open more resilient + +template bool +TCPSocket::open(const IOEndpoint* endpoint, C&) { + + // Ignore opens on already-open socket. Don't throw a failure because + // of uncertainties as to what precedes whan when using asynchronous I/O. + // At also allows us a treat a passed-in socket as a self-managed socket. + + if (!isopen_) { + if (endpoint->getFamily() == AF_INET) { + socket_.open(asio::ip::tcp::v4()); + } + else { + socket_.open(asio::ip::tcp::v6()); + } + isopen_ = true; + + // TODO: Complete TCPSocket::open() + + } + return (false); +} + +// Send a message. Should never do this if the socket is not open, so throw +// an exception if this is the case. + +template void +TCPSocket::asyncSend(const void* data, size_t length, + const IOEndpoint* endpoint, C& callback) +{ + if (isopen_) { + + // Upconvert to a TCPEndpoint. We need to do this because although + // IOEndpoint is the base class of TCPEndpoint and TCPEndpoint, it + // doing cont contain a method for getting at the underlying endpoint + // type - those are in the derived class and the two classes differ on + // return type. + + assert(endpoint->getProtocol() == IPPROTO_TCP); + const TCPEndpoint* tcp_endpoint = + static_cast(endpoint); + std::cerr << "TCPSocket::asyncSend(): sending to " << + tcp_endpoint->getAddress().toText() << + ", port " << tcp_endpoint->getPort() << "\n"; + + // TODO: Complete TCPSocket::asyncSend() + + } else { + isc_throw(SocketNotOpen, + "attempt to send on a TCP socket that is not open"); + } +} + +// Receive a message. Note that the "cumulative" argument is ignored - every TCP +// receive is put into the buffer beginning at the start - there is no concept +// receiving a subsequent part of a message. Same critera as before concerning +// the need for the socket to be open. + +template void +TCPSocket::asyncReceive(void* data, size_t length, size_t, + IOEndpoint* endpoint, C& callback) +{ + if (isopen_) { + + // Upconvert the endpoint again. + assert(endpoint->getProtocol() == IPPROTO_TCP); + const TCPEndpoint* tcp_endpoint = + static_cast(endpoint); + std::cerr << "TCPSocket::asyncReceive(): receiving from " << + tcp_endpoint->getAddress().toText() << + ", port " << tcp_endpoint->getPort() << "\n"; + + // TODO: Complete TCPSocket::asyncReceive() + + } else { + isc_throw(SocketNotOpen, + "attempt to receive from a TCP socket that is not open"); + } +} + +// Cancel I/O on the socket. No-op if the socket is not open. +template void +TCPSocket::cancel() { + if (isopen_) { + socket_.cancel(); + } +} + +// Close the socket down. Can only do this if the socket is open and we are +// managing it ourself. + +template void +TCPSocket::close() { + if (isopen_ && socket_ptr_) { + socket_.close(); + isopen_ = false; + } +} + +} // namespace asiolink + +#endif // __TCP_SOCKET_H diff --git a/src/lib/asiolink/tests/Makefile.am b/src/lib/asiolink/tests/Makefile.am index 3c6cd3ee2ee5137fe56f08d658023b2168c754fd..d47527874f8f2f9d3533b728a1acce8766aed81b 100644 --- a/src/lib/asiolink/tests/Makefile.am +++ b/src/lib/asiolink/tests/Makefile.am @@ -15,24 +15,42 @@ CLEANFILES = *.gcno *.gcda TESTS = if HAVE_GTEST TESTS += run_unittests -run_unittests_SOURCES = $(top_srcdir)/src/lib/dns/tests/unittest_util.h +run_unittests_SOURCES = run_unittests.cc +run_unittests_SOURCES += $(top_srcdir)/src/lib/dns/tests/unittest_util.h run_unittests_SOURCES += $(top_srcdir)/src/lib/dns/tests/unittest_util.cc -run_unittests_SOURCES += asiolink_unittest.cc -run_unittests_SOURCES += run_unittests.cc +run_unittests_SOURCES += io_address_unittest.cc +run_unittests_SOURCES += io_endpoint_unittest.cc +run_unittests_SOURCES += io_fetch_unittest.cc +run_unittests_SOURCES += io_socket_unittest.cc +run_unittests_SOURCES += io_service_unittest.cc +run_unittests_SOURCES += interval_timer_unittest.cc +run_unittests_SOURCES += recursive_query_unittest.cc +run_unittests_SOURCES += udp_endpoint_unittest.cc +run_unittests_SOURCES += udp_socket_unittest.cc + run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) -run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS) -run_unittests_LDADD = $(GTEST_LDADD) + +run_unittests_LDADD = $(GTEST_LDADD) run_unittests_LDADD += $(SQLITE_LIBS) run_unittests_LDADD += $(top_builddir)/src/lib/dns/libdns++.la run_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libexceptions.la run_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libasiolink.la run_unittests_LDADD += $(top_builddir)/src/lib/log/liblog.la +run_unittests_LDADD += $(top_builddir)/src/lib/cache/libcache.la +run_unittests_LDADD += $(top_builddir)/src/lib/nsas/libnsas.la + +run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS) $(LOG4CXX_LDFLAGS) + # Note: the ordering matters: -Wno-... must follow -Wextra (defined in # B10_CXXFLAGS) run_unittests_CXXFLAGS = $(AM_CXXFLAGS) if USE_GXX run_unittests_CXXFLAGS += -Wno-unused-parameter endif +if USE_CLANGPP +# Same for clang++, but we need to turn off -Werror completely. +run_unittests_CXXFLAGS += -Wno-error +endif endif noinst_PROGRAMS = $(TESTS) diff --git a/src/lib/asiolink/tests/interval_timer_unittest.cc b/src/lib/asiolink/tests/interval_timer_unittest.cc new file mode 100644 index 0000000000000000000000000000000000000000..a6793bfd2f817753eaa40573ea2cd6e7fd573120 --- /dev/null +++ b/src/lib/asiolink/tests/interval_timer_unittest.cc @@ -0,0 +1,293 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include + +#include +#include + +#include + +namespace { +// TODO: Consider this margin +const boost::posix_time::time_duration TIMER_MARGIN_MSEC = + boost::posix_time::milliseconds(50); +} + +using namespace asiolink; + +// This fixture is for testing IntervalTimer. Some callback functors are +// registered as callback function of the timer to test if they are called +// or not. +class IntervalTimerTest : public ::testing::Test { +protected: + IntervalTimerTest() : io_service_() {} + ~IntervalTimerTest() {} + class TimerCallBack : public std::unary_function { + public: + TimerCallBack(IntervalTimerTest* test_obj) : test_obj_(test_obj) {} + void operator()() const { + test_obj_->timer_called_ = true; + test_obj_->io_service_.stop(); + return; + } + private: + IntervalTimerTest* test_obj_; + }; + class TimerCallBackCounter : public std::unary_function { + public: + TimerCallBackCounter(IntervalTimerTest* test_obj) : test_obj_(test_obj) { + counter_ = 0; + } + void operator()() { + ++counter_; + return; + } + int counter_; + private: + IntervalTimerTest* test_obj_; + }; + class TimerCallBackCancelDeleter : public std::unary_function { + public: + TimerCallBackCancelDeleter(IntervalTimerTest* test_obj, + IntervalTimer* timer, + TimerCallBackCounter& counter) + : test_obj_(test_obj), timer_(timer), counter_(counter), count_(0) + {} + void operator()() { + ++count_; + if (count_ == 1) { + // First time of call back. + // Store the value of counter_.counter_. + prev_counter_ = counter_.counter_; + delete timer_; + } else if (count_ == 2) { + // Second time of call back. + // Stop io_service to stop all timers. + test_obj_->io_service_.stop(); + // Compare the value of counter_.counter_ with stored one. + // If TimerCallBackCounter was not called (expected behavior), + // they are same. + if (counter_.counter_ == prev_counter_) { + test_obj_->timer_cancel_success_ = true; + } + } + return; + } + private: + IntervalTimerTest* test_obj_; + IntervalTimer* timer_; + TimerCallBackCounter& counter_; + int count_; + int prev_counter_; + }; + class TimerCallBackCanceller { + public: + TimerCallBackCanceller(unsigned int& counter, IntervalTimer& itimer) : + counter_(counter), itimer_(itimer) + {} + void operator()() { + ++counter_; + itimer_.cancel(); + } + private: + unsigned int& counter_; + IntervalTimer& itimer_; + }; + class TimerCallBackOverwriter : public std::unary_function { + public: + TimerCallBackOverwriter(IntervalTimerTest* test_obj, + IntervalTimer& timer) + : test_obj_(test_obj), timer_(timer), count_(0) + {} + void operator()() { + ++count_; + if (count_ == 1) { + // First time of call back. + // Call setup() to update callback function to TimerCallBack. + test_obj_->timer_called_ = false; + timer_.setup(TimerCallBack(test_obj_), 100); + } else if (count_ == 2) { + // Second time of call back. + // If it reaches here, re-setup() is failed (unexpected). + // We should stop here. + test_obj_->io_service_.stop(); + } + return; + } + private: + IntervalTimerTest* test_obj_; + IntervalTimer& timer_; + int count_; + }; +protected: + IOService io_service_; + bool timer_called_; + bool timer_cancel_success_; +}; + +TEST_F(IntervalTimerTest, invalidArgumentToIntervalTimer) { + // Create asio_link::IntervalTimer and setup. + IntervalTimer itimer(io_service_); + // expect throw if call back function is empty + EXPECT_THROW(itimer.setup(IntervalTimer::Callback(), 1), + isc::InvalidParameter); + // expect throw if interval is not greater than 0 + EXPECT_THROW(itimer.setup(TimerCallBack(this), 0), isc::BadValue); + EXPECT_THROW(itimer.setup(TimerCallBack(this), -1), isc::BadValue); +} + +TEST_F(IntervalTimerTest, startIntervalTimer) { + // Create asio_link::IntervalTimer and setup. + // Then run IOService and test if the callback function is called. + IntervalTimer itimer(io_service_); + timer_called_ = false; + // store start time + boost::posix_time::ptime start; + start = boost::posix_time::microsec_clock::universal_time(); + // setup timer + itimer.setup(TimerCallBack(this), 100); + EXPECT_EQ(100, itimer.getInterval()); + io_service_.run(); + // reaches here after timer expired + // delta: difference between elapsed time and 100 milliseconds. + boost::posix_time::time_duration delta = + (boost::posix_time::microsec_clock::universal_time() - start) + - boost::posix_time::millisec(100); + if (delta.is_negative()) { + delta.invert_sign(); + } + // expect TimerCallBack is called; timer_called_ is true + EXPECT_TRUE(timer_called_); + // expect interval is 100 milliseconds +/- TIMER_MARGIN_MSEC. + EXPECT_TRUE(delta < TIMER_MARGIN_MSEC); +} + +TEST_F(IntervalTimerTest, destructIntervalTimer) { + // This code isn't exception safe, but we'd rather keep the code + // simpler and more readable as this is only for tests and if it throws + // the program would immediately terminate anyway. + + // The call back function will not be called after the timer is + // destroyed. + // + // There are two timers: + // itimer_counter (A) + // (Calls TimerCallBackCounter) + // - increments internal counter in callback function + // itimer_canceller (B) + // (Calls TimerCallBackCancelDeleter) + // - first time of callback, it stores the counter value of + // callback_canceller and destroys itimer_counter + // - second time of callback, it compares the counter value of + // callback_canceller with stored value + // if they are same the timer was not called; expected result + // if they are different the timer was called after destroyed + // + // 0 100 200 300 400 500 600 (ms) + // (A) i--------+----x + // ^ + // |destroy itimer_counter + // (B) i-------------+--------------s + // ^stop io_service + // and check if itimer_counter have been + // stopped + + // itimer_counter will be deleted in TimerCallBackCancelDeleter + IntervalTimer* itimer_counter = new IntervalTimer(io_service_); + IntervalTimer itimer_canceller(io_service_); + timer_cancel_success_ = false; + TimerCallBackCounter callback_canceller(this); + itimer_counter->setup(callback_canceller, 200); + itimer_canceller.setup( + TimerCallBackCancelDeleter(this, itimer_counter, callback_canceller), + 300); + io_service_.run(); + EXPECT_TRUE(timer_cancel_success_); +} + +TEST_F(IntervalTimerTest, cancel) { + // Similar to destructIntervalTimer test, but the first timer explicitly + // cancels itself on first callback. + IntervalTimer itimer_counter(io_service_); + IntervalTimer itimer_watcher(io_service_); + unsigned int counter = 0; + itimer_counter.setup(TimerCallBackCanceller(counter, itimer_counter), 100); + itimer_watcher.setup(TimerCallBack(this), 200); + io_service_.run(); + EXPECT_EQ(1, counter); + EXPECT_EQ(0, itimer_counter.getInterval()); + + // canceling an already canceled timer shouldn't cause any surprise. + EXPECT_NO_THROW(itimer_counter.cancel()); +} + +TEST_F(IntervalTimerTest, overwriteIntervalTimer) { + // Calling setup() multiple times updates call back function and interval. + // + // There are two timers: + // itimer (A) + // (Calls TimerCallBackCounter / TimerCallBack) + // - increments internal counter in callback function + // (TimerCallBackCounter) + // interval: 300 milliseconds + // - io_service_.stop() (TimerCallBack) + // interval: 100 milliseconds + // itimer_overwriter (B) + // (Calls TimerCallBackOverwriter) + // - first time of callback, it calls setup() to change call back + // function to TimerCallBack and interval of itimer to 100 + // milliseconds + // after 300 + 100 milliseconds from the beginning of this test, + // TimerCallBack() will be called and io_service_ stops. + // - second time of callback, it means the test fails. + // + // 0 100 200 300 400 500 600 700 800 (ms) + // (A) i-------------+----C----s + // ^ ^stop io_service + // |change call back function + // (B) i------------------+-------------------S + // ^(stop io_service on fail) + // + + IntervalTimer itimer(io_service_); + IntervalTimer itimer_overwriter(io_service_); + // store start time + boost::posix_time::ptime start; + start = boost::posix_time::microsec_clock::universal_time(); + itimer.setup(TimerCallBackCounter(this), 300); + itimer_overwriter.setup(TimerCallBackOverwriter(this, itimer), 400); + io_service_.run(); + // reaches here after timer expired + // if interval is updated, it takes + // 400 milliseconds for TimerCallBackOverwriter + // + 100 milliseconds for TimerCallBack (stop) + // = 500 milliseconds. + // otherwise (test fails), it takes + // 400 milliseconds for TimerCallBackOverwriter + // + 400 milliseconds for TimerCallBackOverwriter (stop) + // = 800 milliseconds. + // delta: difference between elapsed time and 400 + 100 milliseconds + boost::posix_time::time_duration delta = + (boost::posix_time::microsec_clock::universal_time() - start) + - boost::posix_time::millisec(400 + 100); + if (delta.is_negative()) { + delta.invert_sign(); + } + // expect callback function is updated: TimerCallBack is called + EXPECT_TRUE(timer_called_); + // expect interval is updated + EXPECT_TRUE(delta < TIMER_MARGIN_MSEC); +} diff --git a/src/lib/asiolink/tests/io_address_unittest.cc b/src/lib/asiolink/tests/io_address_unittest.cc new file mode 100644 index 0000000000000000000000000000000000000000..894f1433cc009fd445148dc1d28cb10588d2c6ce --- /dev/null +++ b/src/lib/asiolink/tests/io_address_unittest.cc @@ -0,0 +1,63 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include + +#include +#include + +using namespace asiolink; + +TEST(IOAddressTest, fromText) { + IOAddress io_address_v4("192.0.2.1"); + EXPECT_EQ("192.0.2.1", io_address_v4.toText()); + + IOAddress io_address_v6("2001:db8::1234"); + EXPECT_EQ("2001:db8::1234", io_address_v6.toText()); + + // bogus IPv4 address-like input + EXPECT_THROW(IOAddress("192.0.2.2.1"), IOError); + + // bogus IPv4 address-like input: out-of-range octet + EXPECT_THROW(IOAddress("192.0.2.300"), IOError); + + // bogus IPv6 address-like input + EXPECT_THROW(IOAddress("2001:db8:::1234"), IOError); + + // bogus IPv6 address-like input + EXPECT_THROW(IOAddress("2001:db8::efgh"), IOError); +} + +TEST(IOAddressTest, Equality) { + EXPECT_TRUE(IOAddress("192.0.2.1") == IOAddress("192.0.2.1")); + EXPECT_FALSE(IOAddress("192.0.2.1") != IOAddress("192.0.2.1")); + + EXPECT_TRUE(IOAddress("192.0.2.1") != IOAddress("192.0.2.2")); + EXPECT_FALSE(IOAddress("192.0.2.1") == IOAddress("192.0.2.2")); + + EXPECT_TRUE(IOAddress("2001:db8::12") == IOAddress("2001:0DB8:0:0::0012")); + EXPECT_FALSE(IOAddress("2001:db8::12") != IOAddress("2001:0DB8:0:0::0012")); + + EXPECT_TRUE(IOAddress("2001:db8::1234") != IOAddress("2001:db8::1235")); + EXPECT_FALSE(IOAddress("2001:db8::1234") == IOAddress("2001:db8::1235")); + + EXPECT_TRUE(IOAddress("2001:db8::1234") != IOAddress("192.0.2.3")); + EXPECT_FALSE(IOAddress("2001:db8::1234") == IOAddress("192.0.2.3")); +} + +TEST(IOAddressTest, Family) { + EXPECT_EQ(AF_INET, IOAddress("192.0.2.1").getFamily()); + EXPECT_EQ(AF_INET6, IOAddress("2001:0DB8:0:0::0012").getFamily()); +} diff --git a/src/lib/asiolink/tests/io_endpoint_unittest.cc b/src/lib/asiolink/tests/io_endpoint_unittest.cc new file mode 100644 index 0000000000000000000000000000000000000000..534850a6bf55ba3c1df4c58049dbf78c30fbec84 --- /dev/null +++ b/src/lib/asiolink/tests/io_endpoint_unittest.cc @@ -0,0 +1,68 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include + +#include +#include + +using namespace asiolink; + +TEST(IOEndpointTest, createUDPv4) { + const IOEndpoint* ep; + ep = IOEndpoint::create(IPPROTO_UDP, IOAddress("192.0.2.1"), 5300); + EXPECT_EQ("192.0.2.1", ep->getAddress().toText()); + EXPECT_EQ(5300, ep->getPort()); + EXPECT_EQ(AF_INET, ep->getFamily()); + EXPECT_EQ(AF_INET, ep->getAddress().getFamily()); + EXPECT_EQ(IPPROTO_UDP, ep->getProtocol()); +} + +TEST(IOEndpointTest, createTCPv4) { + const IOEndpoint* ep; + ep = IOEndpoint::create(IPPROTO_TCP, IOAddress("192.0.2.1"), 5301); + EXPECT_EQ("192.0.2.1", ep->getAddress().toText()); + EXPECT_EQ(5301, ep->getPort()); + EXPECT_EQ(AF_INET, ep->getFamily()); + EXPECT_EQ(AF_INET, ep->getAddress().getFamily()); + EXPECT_EQ(IPPROTO_TCP, ep->getProtocol()); +} + +TEST(IOEndpointTest, createUDPv6) { + const IOEndpoint* ep; + ep = IOEndpoint::create(IPPROTO_UDP, IOAddress("2001:db8::1234"), 5302); + EXPECT_EQ("2001:db8::1234", ep->getAddress().toText()); + EXPECT_EQ(5302, ep->getPort()); + EXPECT_EQ(AF_INET6, ep->getFamily()); + EXPECT_EQ(AF_INET6, ep->getAddress().getFamily()); + EXPECT_EQ(IPPROTO_UDP, ep->getProtocol()); +} + +TEST(IOEndpointTest, createTCPv6) { + const IOEndpoint* ep; + ep = IOEndpoint::create(IPPROTO_TCP, IOAddress("2001:db8::1234"), 5303); + EXPECT_EQ("2001:db8::1234", ep->getAddress().toText()); + EXPECT_EQ(5303, ep->getPort()); + EXPECT_EQ(AF_INET6, ep->getFamily()); + EXPECT_EQ(AF_INET6, ep->getAddress().getFamily()); + EXPECT_EQ(IPPROTO_TCP, ep->getProtocol()); +} + +TEST(IOEndpointTest, createIPProto) { + EXPECT_THROW(IOEndpoint::create(IPPROTO_IP, IOAddress("192.0.2.1"), + 5300)->getAddress().toText(), + IOError); +} + diff --git a/src/lib/asiolink/tests/io_fetch_unittest.cc b/src/lib/asiolink/tests/io_fetch_unittest.cc new file mode 100644 index 0000000000000000000000000000000000000000..57f61b2777c5c34e20c8ea20693d5f8921ef1d1c --- /dev/null +++ b/src/lib/asiolink/tests/io_fetch_unittest.cc @@ -0,0 +1,188 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace asio; +using namespace isc::dns; +using asio::ip::udp; + +namespace asiolink { + +const asio::ip::address TEST_HOST(asio::ip::address::from_string("127.0.0.1")); +const uint16_t TEST_PORT(5301); +// FIXME Shouldn't we send something that is real message? +const char TEST_DATA[] = "TEST DATA"; + +/// \brief Test fixture for the asiolink::IOFetch. +class IOFetchTest : public virtual ::testing::Test, public virtual IOFetch::Callback +{ +public: + IOService service_; ///< Service to run the query + IOFetch::Result expected_; ///< Expected result of the callback + bool run_; ///< Did the callback run already? + Question question_; ///< What to ask + OutputBufferPtr buff_; ///< Buffer to hold result + IOFetch udp_fetch_; ///< For UDP query test + //IOFetch tcp_fetch_; ///< For TCP query test + + // The next member is the buffer iin which the "server" (implemented by the + // response handler method) receives the question sent by the fetch object. + char server_buff_[512]; ///< Server buffer + + /// \brief Constructor + IOFetchTest() : + service_(), + expected_(IOFetch::NOTSET), + run_(false), + question_(Name("example.net"), RRClass::IN(), RRType::A()), + buff_(new OutputBuffer(512)), + udp_fetch_(IPPROTO_UDP, service_, question_, IOAddress(TEST_HOST), + TEST_PORT, buff_, this, 100) + // tcp_fetch_(service_, question_, IOAddress(TEST_HOST), TEST_PORT, + // buff_, this, 100, IPPROTO_UDP) + { } + + /// \brief Fetch completion callback + /// + /// This is the callback's operator() method which is called when the fetch + /// is complete. Check that the data received is the wire format of the + /// question, then send back an arbitrary response. + void operator()(IOFetch::Result result) { + EXPECT_EQ(expected_, result); // Check correct result returned + EXPECT_FALSE(run_); // Check it is run only once + run_ = true; // Note success + service_.stop(); // ... and exit run loop + } + + /// \brief Response handler, pretending to be remote DNS server + /// + /// This checks that the data sent is what we expected to receive, and + /// sends back a test answer. + void respond(udp::endpoint* remote, udp::socket* socket, + asio::error_code ec = asio::error_code(), size_t length = 0) { + + // Construct the data buffer for question we expect to receive. + OutputBuffer msgbuf(512); + Message msg(Message::RENDER); + msg.setQid(0); + msg.setOpcode(Opcode::QUERY()); + msg.setRcode(Rcode::NOERROR()); + msg.setHeaderFlag(Message::HEADERFLAG_RD); + msg.addQuestion(question_); + MessageRenderer renderer(msgbuf); + msg.toWire(renderer); + + // The QID in the incoming data is random so set it to 0 for the + // data comparison check. (It was set to 0 when the buffer containing + // the expected data was constructed above.) + server_buff_[0] = server_buff_[1] = 0; + + // Check that lengths are identical. + EXPECT_EQ(msgbuf.getLength(), length); + EXPECT_TRUE(memcmp(msgbuf.getData(), server_buff_, length) == 0); + + // ... and return a message back. + socket->send_to(asio::buffer(TEST_DATA, sizeof TEST_DATA), *remote); + } +}; + + +/// Test that when we run the query and stop it after it was run, +/// it returns "stopped" correctly. +/// +/// That is why stop() is posted to the service_ as well instead +/// of calling it. +TEST_F(IOFetchTest, UdpStop) { + expected_ = IOFetch::STOPPED; + + // Post the query + service_.get_io_service().post(udp_fetch_); + + // Post query_.stop() (yes, the boost::bind thing is just + // query_.stop()). + service_.get_io_service().post( + boost::bind(&IOFetch::stop, udp_fetch_, IOFetch::STOPPED)); + + // Run both of them. run() returns when everything in the I/O service + // queue has completed. + service_.run(); + EXPECT_TRUE(run_); +} + +// Test that when we queue the query to service_ and call stop() before it gets +// executed, it acts sanely as well (eg. has the same result as running stop() +// after - calls the callback). +TEST_F(IOFetchTest, UdpPrematureStop) { + expected_ = IOFetch::STOPPED; + + // Stop before it is started + udp_fetch_.stop(); + service_.get_io_service().post(udp_fetch_); + + service_.run(); + EXPECT_TRUE(run_); +} + +// Test that it will timeout when no answer arrives. +TEST_F(IOFetchTest, UdpTimeout) { + expected_ = IOFetch::TIME_OUT; + + service_.get_io_service().post(udp_fetch_); + service_.run(); + EXPECT_TRUE(run_); +} + +// Test that it will succeed when we fake an answer and stores the same data we +// send. This is done through a real socket on the loopback address. +TEST_F(IOFetchTest, UdpReceive) { + expected_ = IOFetch::SUCCESS; + + udp::socket socket(service_.get_io_service(), udp::v4()); + socket.set_option(socket_base::reuse_address(true)); + socket.bind(udp::endpoint(TEST_HOST, TEST_PORT)); + + udp::endpoint remote; + socket.async_receive_from(asio::buffer(server_buff_, sizeof(server_buff_)), + remote, + boost::bind(&IOFetchTest::respond, this, &remote, &socket, _1, _2)); + service_.get_io_service().post(udp_fetch_); + service_.run(); + + socket.close(); + + EXPECT_TRUE(run_); + ASSERT_EQ(sizeof TEST_DATA, buff_->getLength()); + EXPECT_EQ(0, memcmp(TEST_DATA, buff_->getData(), sizeof TEST_DATA)); +} + +} // namespace asiolink diff --git a/src/lib/asiolink/tests/io_service_unittest.cc b/src/lib/asiolink/tests/io_service_unittest.cc new file mode 100644 index 0000000000000000000000000000000000000000..28924d4b9b111968fc5f6dbb975eeafd24485b81 --- /dev/null +++ b/src/lib/asiolink/tests/io_service_unittest.cc @@ -0,0 +1,116 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include + +#include +#include + +using namespace asiolink; + +const char* const TEST_SERVER_PORT = "53535"; +const char* const TEST_CLIENT_PORT = "53536"; +const char* const TEST_IPV6_ADDR = "::1"; +const char* const TEST_IPV4_ADDR = "127.0.0.1"; + +TEST(IOServiceTest, badPort) { + IOService io_service; + EXPECT_THROW(DNSService(io_service, *"65536", true, false, NULL, NULL, NULL), IOError); + EXPECT_THROW(DNSService(io_service, *"5300.0", true, false, NULL, NULL, NULL), IOError); + EXPECT_THROW(DNSService(io_service, *"-1", true, false, NULL, NULL, NULL), IOError); + EXPECT_THROW(DNSService(io_service, *"domain", true, false, NULL, NULL, NULL), IOError); +} + +TEST(IOServiceTest, badAddress) { + IOService io_service; + EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *"192.0.2.1.1", NULL, NULL, NULL), IOError); + EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *"2001:db8:::1", NULL, NULL, NULL), IOError); + EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *"localhost", NULL, NULL, NULL), IOError); +} + +TEST(IOServiceTest, unavailableAddress) { + IOService io_service; + // These addresses should generally be unavailable as a valid local + // address, although there's no guarantee in theory. + EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *"192.0.2.0", NULL, NULL, NULL), IOError); + + // Some OSes would simply reject binding attempt for an AF_INET6 socket + // to an IPv4-mapped IPv6 address. Even if those that allow it, since + // the corresponding IPv4 address is the same as the one used in the + // AF_INET socket case above, it should at least show the same result + // as the previous one. + EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *"::ffff:192.0.2.0", NULL, NULL, NULL), IOError); +} + +TEST(IOServiceTest, duplicateBind_v6) { + // In each sub test case, second attempt should fail due to duplicate bind + IOService io_service; + + // IPv6, "any" address + DNSService* dns_service = new DNSService(io_service, *TEST_SERVER_PORT, false, true, NULL, NULL, NULL); + EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, false, true, NULL, NULL, NULL), IOError); + delete dns_service; + +} + +TEST(IOServiceTest, duplicateBind_v6_address) { + // In each sub test case, second attempt should fail due to duplicate bind + IOService io_service; + + // IPv6, specific address + DNSService* dns_service = new DNSService(io_service, *TEST_SERVER_PORT, *TEST_IPV6_ADDR, NULL, NULL, NULL); + EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *TEST_IPV6_ADDR, NULL, NULL, NULL), IOError); + delete dns_service; + +} + +TEST(IOServiceTest, duplicateBind_v4) { + // In each sub test case, second attempt should fail due to duplicate bind + IOService io_service; + + // IPv4, "any" address + DNSService* dns_service = new DNSService(io_service, *TEST_SERVER_PORT, true, false, NULL, NULL, NULL); + EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, true, false, NULL, NULL, NULL), IOError); + delete dns_service; + +} + +TEST(IOServiceTest, duplicateBind_v4_address) { + // In each sub test case, second attempt should fail due to duplicate bind + IOService io_service; + + // IPv4, specific address + DNSService* dns_service = new DNSService(io_service, *TEST_SERVER_PORT, *TEST_IPV4_ADDR, NULL, NULL, NULL); + EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *TEST_IPV4_ADDR, NULL, NULL, NULL), IOError); + delete dns_service; +} + +// Disabled because IPv4-mapped addresses don't seem to be working with +// the IOService constructor +TEST(IOServiceTest, DISABLED_IPv4MappedDuplicateBind) { + IOService io_service; + // Duplicate bind on IPv4-mapped IPv6 address + DNSService* dns_service = new DNSService(io_service, *TEST_SERVER_PORT, *"127.0.0.1", NULL, NULL, NULL); + EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *"::ffff:127.0.0.1", NULL, NULL, NULL), IOError); + delete dns_service; + + // XXX: + // Currently, this throws an "invalid argument" exception. I have + // not been able to get IPv4-mapped addresses to work. + dns_service = new DNSService(io_service, *TEST_SERVER_PORT, *"::ffff:127.0.0.1", NULL, NULL, NULL); + EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *"127.0.0.1", NULL, NULL, NULL), IOError); + delete dns_service; +} + diff --git a/src/lib/asiolink/tests/io_socket_unittest.cc b/src/lib/asiolink/tests/io_socket_unittest.cc new file mode 100644 index 0000000000000000000000000000000000000000..6538550bc89e6c62153a168dae652e3372b17a79 --- /dev/null +++ b/src/lib/asiolink/tests/io_socket_unittest.cc @@ -0,0 +1,32 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include + +#include + +#include +#include + +using namespace asiolink; + +TEST(IOSocketTest, dummySockets) { + EXPECT_EQ(IPPROTO_UDP, IOSocket::getDummyUDPSocket().getProtocol()); + EXPECT_EQ(IPPROTO_TCP, IOSocket::getDummyTCPSocket().getProtocol()); + EXPECT_EQ(-1, IOSocket::getDummyUDPSocket().getNative()); + EXPECT_EQ(-1, IOSocket::getDummyTCPSocket().getNative()); +} + + diff --git a/src/lib/asiolink/tests/asiolink_unittest.cc b/src/lib/asiolink/tests/recursive_query_unittest.cc similarity index 53% rename from src/lib/asiolink/tests/asiolink_unittest.cc rename to src/lib/asiolink/tests/recursive_query_unittest.cc index 4d79b0ffc82542f532fcc7bc2a6aa81b9b3454f4..dd18024a48ddb2033fd41141f7fbd0a4f836277e 100644 --- a/src/lib/asiolink/tests/asiolink_unittest.cc +++ b/src/lib/asiolink/tests/recursive_query_unittest.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -41,8 +41,13 @@ // if we include asio.hpp unless we specify a special compiler option. // If we need to test something at the level of underlying ASIO and need // their definition, that test should go to asiolink/internal/tests. -#include -#include +#include +#include +#include +#include +#include +#include +#include using isc::UnitTestUtil; using namespace std; @@ -58,171 +63,6 @@ const char* const TEST_IPV4_ADDR = "127.0.0.1"; // two octets encode the length of the rest of the data. This is crucial // for the tests below. const uint8_t test_data[] = {0, 4, 1, 2, 3, 4}; -// TODO: Consider this margin -const boost::posix_time::time_duration TIMER_MARGIN_MSEC = - boost::posix_time::milliseconds(50); - -TEST(IOAddressTest, fromText) { - IOAddress io_address_v4("192.0.2.1"); - EXPECT_EQ("192.0.2.1", io_address_v4.toText()); - - IOAddress io_address_v6("2001:db8::1234"); - EXPECT_EQ("2001:db8::1234", io_address_v6.toText()); - - // bogus IPv4 address-like input - EXPECT_THROW(IOAddress("192.0.2.2.1"), IOError); - - // bogus IPv4 address-like input: out-of-range octet - EXPECT_THROW(IOAddress("192.0.2.300"), IOError); - - // bogus IPv6 address-like input - EXPECT_THROW(IOAddress("2001:db8:::1234"), IOError); - - // bogus IPv6 address-like input - EXPECT_THROW(IOAddress("2001:db8::efgh"), IOError); -} - -TEST(IOEndpointTest, createUDPv4) { - const IOEndpoint* ep; - ep = IOEndpoint::create(IPPROTO_UDP, IOAddress("192.0.2.1"), 5300); - EXPECT_EQ("192.0.2.1", ep->getAddress().toText()); - EXPECT_EQ(5300, ep->getPort()); - EXPECT_EQ(AF_INET, ep->getFamily()); - EXPECT_EQ(AF_INET, ep->getAddress().getFamily()); - EXPECT_EQ(IPPROTO_UDP, ep->getProtocol()); -} - -TEST(IOEndpointTest, createTCPv4) { - const IOEndpoint* ep; - ep = IOEndpoint::create(IPPROTO_TCP, IOAddress("192.0.2.1"), 5301); - EXPECT_EQ("192.0.2.1", ep->getAddress().toText()); - EXPECT_EQ(5301, ep->getPort()); - EXPECT_EQ(AF_INET, ep->getFamily()); - EXPECT_EQ(AF_INET, ep->getAddress().getFamily()); - EXPECT_EQ(IPPROTO_TCP, ep->getProtocol()); -} - -TEST(IOEndpointTest, createUDPv6) { - const IOEndpoint* ep; - ep = IOEndpoint::create(IPPROTO_UDP, IOAddress("2001:db8::1234"), 5302); - EXPECT_EQ("2001:db8::1234", ep->getAddress().toText()); - EXPECT_EQ(5302, ep->getPort()); - EXPECT_EQ(AF_INET6, ep->getFamily()); - EXPECT_EQ(AF_INET6, ep->getAddress().getFamily()); - EXPECT_EQ(IPPROTO_UDP, ep->getProtocol()); -} - -TEST(IOEndpointTest, createTCPv6) { - const IOEndpoint* ep; - ep = IOEndpoint::create(IPPROTO_TCP, IOAddress("2001:db8::1234"), 5303); - EXPECT_EQ("2001:db8::1234", ep->getAddress().toText()); - EXPECT_EQ(5303, ep->getPort()); - EXPECT_EQ(AF_INET6, ep->getFamily()); - EXPECT_EQ(AF_INET6, ep->getAddress().getFamily()); - EXPECT_EQ(IPPROTO_TCP, ep->getProtocol()); -} - -TEST(IOEndpointTest, createIPProto) { - EXPECT_THROW(IOEndpoint::create(IPPROTO_IP, IOAddress("192.0.2.1"), - 5300)->getAddress().toText(), - IOError); -} - -TEST(IOSocketTest, dummySockets) { - EXPECT_EQ(IPPROTO_UDP, IOSocket::getDummyUDPSocket().getProtocol()); - EXPECT_EQ(IPPROTO_TCP, IOSocket::getDummyTCPSocket().getProtocol()); - EXPECT_EQ(-1, IOSocket::getDummyUDPSocket().getNative()); - EXPECT_EQ(-1, IOSocket::getDummyTCPSocket().getNative()); -} - -TEST(IOServiceTest, badPort) { - IOService io_service; - EXPECT_THROW(DNSService(io_service, *"65536", true, false, NULL, NULL, NULL), IOError); - EXPECT_THROW(DNSService(io_service, *"5300.0", true, false, NULL, NULL, NULL), IOError); - EXPECT_THROW(DNSService(io_service, *"-1", true, false, NULL, NULL, NULL), IOError); - EXPECT_THROW(DNSService(io_service, *"domain", true, false, NULL, NULL, NULL), IOError); -} - -TEST(IOServiceTest, badAddress) { - IOService io_service; - EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *"192.0.2.1.1", NULL, NULL, NULL), IOError); - EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *"2001:db8:::1", NULL, NULL, NULL), IOError); - EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *"localhost", NULL, NULL, NULL), IOError); -} - -TEST(IOServiceTest, unavailableAddress) { - IOService io_service; - // These addresses should generally be unavailable as a valid local - // address, although there's no guarantee in theory. - EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *"255.255.0.0", NULL, NULL, NULL), IOError); - - // Some OSes would simply reject binding attempt for an AF_INET6 socket - // to an IPv4-mapped IPv6 address. Even if those that allow it, since - // the corresponding IPv4 address is the same as the one used in the - // AF_INET socket case above, it should at least show the same result - // as the previous one. - EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *"::ffff:255.255.0.0", NULL, NULL, NULL), IOError); -} - -TEST(IOServiceTest, duplicateBind_v6) { - // In each sub test case, second attempt should fail due to duplicate bind - IOService io_service; - - // IPv6, "any" address - DNSService* dns_service = new DNSService(io_service, *TEST_SERVER_PORT, false, true, NULL, NULL, NULL); - EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, false, true, NULL, NULL, NULL), IOError); - delete dns_service; - -} - -TEST(IOServiceTest, duplicateBind_v6_address) { - // In each sub test case, second attempt should fail due to duplicate bind - IOService io_service; - - // IPv6, specific address - DNSService* dns_service = new DNSService(io_service, *TEST_SERVER_PORT, *TEST_IPV6_ADDR, NULL, NULL, NULL); - EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *TEST_IPV6_ADDR, NULL, NULL, NULL), IOError); - delete dns_service; - -} - -TEST(IOServiceTest, duplicateBind_v4) { - // In each sub test case, second attempt should fail due to duplicate bind - IOService io_service; - - // IPv4, "any" address - DNSService* dns_service = new DNSService(io_service, *TEST_SERVER_PORT, true, false, NULL, NULL, NULL); - EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, true, false, NULL, NULL, NULL), IOError); - delete dns_service; - -} - -TEST(IOServiceTest, duplicateBind_v4_address) { - // In each sub test case, second attempt should fail due to duplicate bind - IOService io_service; - - // IPv4, specific address - DNSService* dns_service = new DNSService(io_service, *TEST_SERVER_PORT, *TEST_IPV4_ADDR, NULL, NULL, NULL); - EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *TEST_IPV4_ADDR, NULL, NULL, NULL), IOError); - delete dns_service; -} - -// Disabled because IPv4-mapped addresses don't seem to be working with -// the IOService constructor -TEST(IOServiceTest, DISABLED_IPv4MappedDuplicateBind) { - IOService io_service; - // Duplicate bind on IPv4-mapped IPv6 address - DNSService* dns_service = new DNSService(io_service, *TEST_SERVER_PORT, *"127.0.0.1", NULL, NULL, NULL); - EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *"::ffff:127.0.0.1", NULL, NULL, NULL), IOError); - delete dns_service; - - // XXX: - // Currently, this throws an "invalid argument" exception. I have - // not been able to get IPv4-mapped addresses to work. - dns_service = new DNSService(io_service, *TEST_SERVER_PORT, *"::ffff:127.0.0.1", NULL, NULL, NULL); - EXPECT_THROW(DNSService(io_service, *TEST_SERVER_PORT, *"127.0.0.1", NULL, NULL, NULL), IOError); - delete dns_service; -} // This function returns an addrinfo structure for use by tests, using // different addresses and ports depending on whether we're testing @@ -259,12 +99,12 @@ resolveAddress(const int family, const int protocol, const bool client) { // expected parameters. // If initialization parameters of the IOService should be modified, the test // case can do it using the setDNSService() method. -// Note: the set of tests in ASIOLinkTest use actual network services and may +// Note: the set of tests in RecursiveQueryTest use actual network services and may // involve undesirable side effects such as blocking. -class ASIOLinkTest : public ::testing::Test { +class RecursiveQueryTest : public ::testing::Test { protected: - ASIOLinkTest(); - ~ASIOLinkTest() { + RecursiveQueryTest(); + ~RecursiveQueryTest() { if (res_ != NULL) { freeaddrinfo(res_); } @@ -503,15 +343,48 @@ protected: bool* done_; }; + // This version of mock server just stops the io_service when it is resumed + // the second time. (Used in the clientTimeout test, where resume + // is called initially with the error answer, and later when the + // lookup times out, it is called without an answer to send back) + class MockServerStop2 : public MockServer { + public: + explicit MockServerStop2(IOService& io_service, + bool* done1, bool* done2) : + MockServer(io_service), + done1_(done1), + done2_(done2), + stopped_once_(false) + {} + + void resume(const bool done) { + if (stopped_once_) { + *done2_ = done; + io_.stop(); + } else { + *done1_ = done; + stopped_once_ = true; + } + } + + DNSServer* clone() { + return (new MockServerStop2(*this)); + } + private: + bool* done1_; + bool* done2_; + bool stopped_once_; + }; + private: class ASIOCallBack : public SimpleCallback { public: - ASIOCallBack(ASIOLinkTest* test_obj) : test_obj_(test_obj) {} + ASIOCallBack(RecursiveQueryTest* test_obj) : test_obj_(test_obj) {} void operator()(const IOMessage& io_message) const { test_obj_->callBack(io_message); } private: - ASIOLinkTest* test_obj_; + RecursiveQueryTest* test_obj_; }; void callBack(const IOMessage& io_message) { callback_protocol_ = io_message.getSocket().getProtocol(); @@ -538,30 +411,30 @@ protected: struct addrinfo* res_; }; -ASIOLinkTest::ASIOLinkTest() : +RecursiveQueryTest::RecursiveQueryTest() : dns_service_(NULL), callback_(NULL), sock_(-1), res_(NULL) { io_service_ = new IOService(); setDNSService(true, true); } -TEST_F(ASIOLinkTest, v6UDPSend) { +TEST_F(RecursiveQueryTest, v6UDPSend) { doTest(AF_INET6, IPPROTO_UDP); } -TEST_F(ASIOLinkTest, v6TCPSend) { +TEST_F(RecursiveQueryTest, v6TCPSend) { doTest(AF_INET6, IPPROTO_TCP); } -TEST_F(ASIOLinkTest, v4UDPSend) { +TEST_F(RecursiveQueryTest, v4UDPSend) { doTest(AF_INET, IPPROTO_UDP); } -TEST_F(ASIOLinkTest, v4TCPSend) { +TEST_F(RecursiveQueryTest, v4TCPSend) { doTest(AF_INET, IPPROTO_TCP); } -TEST_F(ASIOLinkTest, v6UDPSendSpecific) { +TEST_F(RecursiveQueryTest, v6UDPSendSpecific) { // Explicitly set a specific address to be bound to the socket. // The subsequent test does not directly ensures the underlying socket // is bound to the expected address, but the success of the tests should @@ -577,26 +450,26 @@ TEST_F(ASIOLinkTest, v6UDPSendSpecific) { doTest(AF_INET6, IPPROTO_UDP); } -TEST_F(ASIOLinkTest, v6TCPSendSpecific) { +TEST_F(RecursiveQueryTest, v6TCPSendSpecific) { setDNSService(*TEST_IPV6_ADDR); doTest(AF_INET6, IPPROTO_TCP); EXPECT_THROW(sendTCP(AF_INET), IOError); } -TEST_F(ASIOLinkTest, v4UDPSendSpecific) { +TEST_F(RecursiveQueryTest, v4UDPSendSpecific) { setDNSService(*TEST_IPV4_ADDR); doTest(AF_INET, IPPROTO_UDP); } -TEST_F(ASIOLinkTest, v4TCPSendSpecific) { +TEST_F(RecursiveQueryTest, v4TCPSendSpecific) { setDNSService(*TEST_IPV4_ADDR); doTest(AF_INET, IPPROTO_TCP); EXPECT_THROW(sendTCP(AF_INET6), IOError); } -TEST_F(ASIOLinkTest, v6AddServer) { +TEST_F(RecursiveQueryTest, v6AddServer) { setDNSService(); dns_service_->addServer(*TEST_SERVER_PORT, TEST_IPV6_ADDR); doTest(AF_INET6, IPPROTO_TCP); @@ -604,7 +477,7 @@ TEST_F(ASIOLinkTest, v6AddServer) { EXPECT_THROW(sendTCP(AF_INET), IOError); } -TEST_F(ASIOLinkTest, v4AddServer) { +TEST_F(RecursiveQueryTest, v4AddServer) { setDNSService(); dns_service_->addServer(*TEST_SERVER_PORT, TEST_IPV4_ADDR); doTest(AF_INET, IPPROTO_TCP); @@ -612,7 +485,7 @@ TEST_F(ASIOLinkTest, v4AddServer) { EXPECT_THROW(sendTCP(AF_INET6), IOError); } -TEST_F(ASIOLinkTest, DISABLED_clearServers) { +TEST_F(RecursiveQueryTest, DISABLED_clearServers) { // FIXME: Enable when clearServers actually close the sockets // See #388 setDNSService(); @@ -622,7 +495,7 @@ TEST_F(ASIOLinkTest, DISABLED_clearServers) { EXPECT_THROW(sendTCP(AF_INET6), IOError); } -TEST_F(ASIOLinkTest, v6TCPOnly) { +TEST_F(RecursiveQueryTest, v6TCPOnly) { // Open only IPv6 TCP socket. A subsequent attempt of establishing an // IPv4/TCP connection should fail. See above for why we only test this // for TCP. @@ -630,7 +503,7 @@ TEST_F(ASIOLinkTest, v6TCPOnly) { EXPECT_THROW(sendTCP(AF_INET), IOError); } -TEST_F(ASIOLinkTest, v4TCPOnly) { +TEST_F(RecursiveQueryTest, v4TCPOnly) { setDNSService(true, false); EXPECT_THROW(sendTCP(AF_INET6), IOError); } @@ -642,7 +515,7 @@ singleAddress(const string &address, uint16_t port) { return (result); } -TEST_F(ASIOLinkTest, recursiveSetupV4) { +TEST_F(RecursiveQueryTest, recursiveSetupV4) { setDNSService(true, false); uint16_t port = boost::lexical_cast(TEST_CLIENT_PORT); EXPECT_NO_THROW(RecursiveQuery(*dns_service_, @@ -650,7 +523,7 @@ TEST_F(ASIOLinkTest, recursiveSetupV4) { singleAddress(TEST_IPV4_ADDR, port))); } -TEST_F(ASIOLinkTest, recursiveSetupV6) { +TEST_F(RecursiveQueryTest, recursiveSetupV6) { setDNSService(false, true); uint16_t port = boost::lexical_cast(TEST_CLIENT_PORT); EXPECT_NO_THROW(RecursiveQuery(*dns_service_, @@ -663,7 +536,7 @@ TEST_F(ASIOLinkTest, recursiveSetupV6) { // a routine that can do this with variable address family, address, and // port, and with the various callbacks defined in such a way as to ensure // full code coverage including error cases. -TEST_F(ASIOLinkTest, forwarderSend) { +TEST_F(RecursiveQueryTest, forwarderSend) { setDNSService(true, false); // Note: We use the test prot plus one to ensure we aren't binding @@ -678,7 +551,7 @@ TEST_F(ASIOLinkTest, forwarderSend) { Question q(Name("example.com"), RRClass::IN(), RRType::TXT()); OutputBufferPtr buffer(new OutputBuffer(0)); MessagePtr answer(new Message(Message::RENDER)); - rq.sendQuery(q, answer, buffer, &server); + rq.resolve(q, answer, buffer, &server); char data[4096]; size_t size = sizeof(data); @@ -698,20 +571,59 @@ TEST_F(ASIOLinkTest, forwarderSend) { EXPECT_EQ(q.getClass(), q2->getClass()); } -// Test it tries the correct amount of times before giving up -TEST_F(ASIOLinkTest, recursiveTimeout) { - // Prepare the service (we do not use the common setup, we do not answer - setDNSService(); - - // Prepare the socket - res_ = resolveAddress(AF_INET, IPPROTO_UDP, true); - sock_ = socket(res_->ai_family, res_->ai_socktype, res_->ai_protocol); +int +createTestSocket() +{ + struct addrinfo* res_ = resolveAddress(AF_INET, IPPROTO_UDP, true); + int sock_ = socket(res_->ai_family, res_->ai_socktype, res_->ai_protocol); if (sock_ < 0) { isc_throw(IOError, "failed to open test socket"); } if (bind(sock_, res_->ai_addr, res_->ai_addrlen) < 0) { isc_throw(IOError, "failed to bind test socket"); } + return sock_; +} + +int +setSocketTimeout(int sock_, size_t tv_sec, size_t tv_usec) { + const struct timeval timeo = { tv_sec, tv_usec }; + int recv_options = 0; + if (setsockopt(sock_, SOL_SOCKET, SO_RCVTIMEO, &timeo, sizeof(timeo))) { + if (errno == ENOPROTOOPT) { // see RecursiveQueryTest::recvUDP() + recv_options = MSG_DONTWAIT; + } else { + isc_throw(IOError, "set RCVTIMEO failed: " << strerror(errno)); + } + } + return recv_options; +} + +// try to read from the socket max time +// *num is incremented for every succesfull read +// returns true if it can read max times, false otherwise +bool tryRead(int sock_, int recv_options, size_t max, int* num) { + size_t i = 0; + do { + char inbuff[512]; + if (recv(sock_, inbuff, sizeof(inbuff), recv_options) < 0) { + return false; + } else { + ++i; + ++*num; + } + } while (i < max); + return true; +} + + +// Test it tries the correct amount of times before giving up +TEST_F(RecursiveQueryTest, forwardQueryTimeout) { + // Prepare the service (we do not use the common setup, we do not answer + setDNSService(); + + // Prepare the socket + sock_ = createTestSocket(); // Prepare the server bool done(true); @@ -722,38 +634,113 @@ TEST_F(ASIOLinkTest, recursiveTimeout) { RecursiveQuery query(*dns_service_, singleAddress(TEST_IPV4_ADDR, port), singleAddress(TEST_IPV4_ADDR, port), - 10, 2); + 10, 4000, 3000, 2); Question question(Name("example.net"), RRClass::IN(), RRType::A()); OutputBufferPtr buffer(new OutputBuffer(0)); MessagePtr answer(new Message(Message::RENDER)); - query.sendQuery(question, answer, buffer, &server); + query.resolve(question, answer, buffer, &server); // Run the test io_service_->run(); // Read up to 3 packets. Use some ad hoc timeout to prevent an infinite // block (see also recvUDP()). - const struct timeval timeo = { 10, 0 }; - int recv_options = 0; - if (setsockopt(sock_, SOL_SOCKET, SO_RCVTIMEO, &timeo, sizeof(timeo))) { - if (errno == ENOPROTOOPT) { // see ASIOLinkTest::recvUDP() - recv_options = MSG_DONTWAIT; - } else { - isc_throw(IOError, "set RCVTIMEO failed: " << strerror(errno)); - } - } + int recv_options = setSocketTimeout(sock_, 10, 0); int num = 0; - do { - char inbuff[512]; - if (recv(sock_, inbuff, sizeof(inbuff), recv_options) < 0) { - num = -1; - break; - } - } while (++num < 3); + bool read_success = tryRead(sock_, recv_options, 3, &num); + + // The query should fail + EXPECT_FALSE(done); + EXPECT_EQ(3, num); + EXPECT_TRUE(read_success); +} + +// If we set client timeout to lower than querytimeout, we should +// get a failure answer, but still see retries +// (no actual answer is given here yet) +TEST_F(RecursiveQueryTest, forwardClientTimeout) { + // Prepare the service (we do not use the common setup, we do not answer + setDNSService(); + + sock_ = createTestSocket(); + + // Prepare the server + bool done1(true); + bool done2(true); + MockServerStop2 server(*io_service_, &done1, &done2); + + MessagePtr answer(new Message(Message::RENDER)); + + // Do the answer + const uint16_t port = boost::lexical_cast(TEST_CLIENT_PORT); + // Set it up to retry twice before client timeout fires + // Since the lookup timer has not fired, it should retry + // four times + RecursiveQuery query(*dns_service_, + singleAddress(TEST_IPV4_ADDR, port), + singleAddress(TEST_IPV4_ADDR, port), + 200, 480, 4000, 4); + Question question(Name("example.net"), RRClass::IN(), RRType::A()); + OutputBufferPtr buffer(new OutputBuffer(0)); + query.resolve(question, answer, buffer, &server); + + // Run the test + io_service_->run(); + + // we know it'll fail, so make it a shorter timeout + int recv_options = setSocketTimeout(sock_, 1, 0); + + // Try to read 5 times + int num = 0; + bool read_success = tryRead(sock_, recv_options, 5, &num); + + // The query should fail, but we should have kept on trying + EXPECT_TRUE(done1); + EXPECT_FALSE(done2); + EXPECT_EQ(5, num); + EXPECT_TRUE(read_success); +} + +// If we set lookup timeout to lower than querytimeout*retries, we should +// fail before the full amount of retries +TEST_F(RecursiveQueryTest, forwardLookupTimeout) { + // Prepare the service (we do not use the common setup, we do not answer + setDNSService(); + + // Prepare the socket + sock_ = createTestSocket(); + + // Prepare the server + bool done(true); + MockServerStop server(*io_service_, &done); + + MessagePtr answer(new Message(Message::RENDER)); + + // Do the answer + const uint16_t port = boost::lexical_cast(TEST_CLIENT_PORT); + // Set up the test so that it will retry 5 times, but the lookup + // timeout will fire after only 3 normal timeouts + RecursiveQuery query(*dns_service_, + singleAddress(TEST_IPV4_ADDR, port), + singleAddress(TEST_IPV4_ADDR, port), + 200, 4000, 480, 5); + Question question(Name("example.net"), RRClass::IN(), RRType::A()); + OutputBufferPtr buffer(new OutputBuffer(0)); + query.resolve(question, answer, buffer, &server); + + // Run the test + io_service_->run(); + + int recv_options = setSocketTimeout(sock_, 1, 0); + + // Try to read 5 times, should stop after 3 reads + int num = 0; + bool read_success = tryRead(sock_, recv_options, 5, &num); // The query should fail EXPECT_FALSE(done); EXPECT_EQ(3, num); + EXPECT_FALSE(read_success); } // as mentioned above, we need a more better framework for this, @@ -762,7 +749,7 @@ TEST_F(ASIOLinkTest, recursiveTimeout) { // for the skeleton code, it shouldn't be too much of a problem // Ok so even we don't all have access to the DNS world right now, // so disabling these tests too. -TEST_F(ASIOLinkTest, DISABLED_recursiveSendOk) { +TEST_F(RecursiveQueryTest, DISABLED_recursiveSendOk) { setDNSService(true, false); bool done; @@ -773,7 +760,7 @@ TEST_F(ASIOLinkTest, DISABLED_recursiveSendOk) { Question q(Name("www.isc.org"), RRClass::IN(), RRType::A()); OutputBufferPtr buffer(new OutputBuffer(0)); MessagePtr answer(new Message(Message::RENDER)); - rq.sendQuery(q, answer, buffer, &server); + rq.resolve(q, answer, buffer, &server); io_service_->run(); // Check that the answer we got matches the one we wanted @@ -787,7 +774,7 @@ TEST_F(ASIOLinkTest, DISABLED_recursiveSendOk) { } // see comments at previous test -TEST_F(ASIOLinkTest, DISABLED_recursiveSendNXDOMAIN) { +TEST_F(RecursiveQueryTest, DISABLED_recursiveSendNXDOMAIN) { setDNSService(true, false); bool done; @@ -798,7 +785,7 @@ TEST_F(ASIOLinkTest, DISABLED_recursiveSendNXDOMAIN) { Question q(Name("wwwdoesnotexist.isc.org"), RRClass::IN(), RRType::A()); OutputBufferPtr buffer(new OutputBuffer(0)); MessagePtr answer(new Message(Message::RENDER)); - rq.sendQuery(q, answer, buffer, &server); + rq.resolve(q, answer, buffer, &server); io_service_->run(); // Check that the answer we got matches the one we wanted @@ -806,280 +793,4 @@ TEST_F(ASIOLinkTest, DISABLED_recursiveSendNXDOMAIN) { EXPECT_EQ(0, answer->getRRCount(Message::SECTION_ANSWER)); } - - -// This fixture is for testing IntervalTimer. Some callback functors are -// registered as callback function of the timer to test if they are called -// or not. -class IntervalTimerTest : public ::testing::Test { -protected: - IntervalTimerTest() : io_service_() {} - ~IntervalTimerTest() {} - class TimerCallBack : public std::unary_function { - public: - TimerCallBack(IntervalTimerTest* test_obj) : test_obj_(test_obj) {} - void operator()() const { - test_obj_->timer_called_ = true; - test_obj_->io_service_.stop(); - return; - } - private: - IntervalTimerTest* test_obj_; - }; - class TimerCallBackCounter : public std::unary_function { - public: - TimerCallBackCounter(IntervalTimerTest* test_obj) : test_obj_(test_obj) { - counter_ = 0; - } - void operator()() { - ++counter_; - return; - } - int counter_; - private: - IntervalTimerTest* test_obj_; - }; - class TimerCallBackCancelDeleter : public std::unary_function { - public: - TimerCallBackCancelDeleter(IntervalTimerTest* test_obj, - IntervalTimer* timer, - TimerCallBackCounter& counter) - : test_obj_(test_obj), timer_(timer), counter_(counter), count_(0) - {} - void operator()() { - ++count_; - if (count_ == 1) { - // First time of call back. - // Store the value of counter_.counter_. - prev_counter_ = counter_.counter_; - delete timer_; - } else if (count_ == 2) { - // Second time of call back. - // Stop io_service to stop all timers. - test_obj_->io_service_.stop(); - // Compare the value of counter_.counter_ with stored one. - // If TimerCallBackCounter was not called (expected behavior), - // they are same. - if (counter_.counter_ == prev_counter_) { - test_obj_->timer_cancel_success_ = true; - } - } - return; - } - private: - IntervalTimerTest* test_obj_; - IntervalTimer* timer_; - TimerCallBackCounter& counter_; - int count_; - int prev_counter_; - }; - class TimerCallBackCanceller { - public: - TimerCallBackCanceller(unsigned int& counter, IntervalTimer& itimer) : - counter_(counter), itimer_(itimer) - {} - void operator()() { - ++counter_; - itimer_.cancel(); - } - private: - unsigned int& counter_; - IntervalTimer& itimer_; - }; - class TimerCallBackOverwriter : public std::unary_function { - public: - TimerCallBackOverwriter(IntervalTimerTest* test_obj, - IntervalTimer& timer) - : test_obj_(test_obj), timer_(timer), count_(0) - {} - void operator()() { - ++count_; - if (count_ == 1) { - // First time of call back. - // Call setupTimer() to update callback function - // to TimerCallBack. - test_obj_->timer_called_ = false; - timer_.setupTimer(TimerCallBack(test_obj_), 1); - } else if (count_ == 2) { - // Second time of call back. - // If it reaches here, re-setupTimer() is failed (unexpected). - // We should stop here. - test_obj_->io_service_.stop(); - } - return; - } - private: - IntervalTimerTest* test_obj_; - IntervalTimer& timer_; - int count_; - }; -protected: - IOService io_service_; - bool timer_called_; - bool timer_cancel_success_; -}; - -TEST_F(IntervalTimerTest, invalidArgumentToIntervalTimer) { - // Create asio_link::IntervalTimer and setup. - IntervalTimer itimer(io_service_); - // expect throw if call back function is empty - EXPECT_THROW(itimer.setupTimer(IntervalTimer::Callback(), 1), - isc::InvalidParameter); - // expect throw if interval is 0 - EXPECT_THROW(itimer.setupTimer(TimerCallBack(this), 0), isc::BadValue); -} - -TEST_F(IntervalTimerTest, startIntervalTimer) { - // Create asio_link::IntervalTimer and setup. - // Then run IOService and test if the callback function is called. - IntervalTimer itimer(io_service_); - timer_called_ = false; - // store start time - boost::posix_time::ptime start; - start = boost::posix_time::microsec_clock::universal_time(); - // setup timer - itimer.setupTimer(TimerCallBack(this), 1); - EXPECT_EQ(1, itimer.getInterval()); - io_service_.run(); - // reaches here after timer expired - // delta: difference between elapsed time and 1 second - boost::posix_time::time_duration delta = - (boost::posix_time::microsec_clock::universal_time() - start) - - boost::posix_time::seconds(1); - if (delta.is_negative()) { - delta.invert_sign(); - } - // expect TimerCallBack is called; timer_called_ is true - EXPECT_TRUE(timer_called_); - // expect interval is 1 second +/- TIMER_MARGIN_MSEC. - EXPECT_TRUE(delta < TIMER_MARGIN_MSEC); -} - -TEST_F(IntervalTimerTest, destructIntervalTimer) { - // Note: This test currently takes 6 seconds. The timer should have - // finer granularity and timer periods in this test should be shorter - // in the future. - // This code isn't exception safe, but we'd rather keep the code - // simpler and more readable as this is only for tests and if it throws - // the program would immediately terminate anyway. - - // The call back function will not be called after the timer is - // destructed. - // - // There are two timers: - // itimer_counter (A) - // (Calls TimerCallBackCounter) - // - increments internal counter in callback function - // itimer_canceller (B) - // (Calls TimerCallBackCancelDeleter) - // - first time of callback, it stores the counter value of - // callback_canceller and destructs itimer_counter - // - second time of callback, it compares the counter value of - // callback_canceller with stored value - // if they are same the timer was not called; expected result - // if they are different the timer was called after destructed - // - // 0 1 2 3 4 5 6 (s) - // (A) i-----+--x - // ^ - // |destruct itimer_counter - // (B) i--------+--------s - // ^stop io_service - // and test itimer_counter have been stopped - // - - // itimer_counter will be deleted in TimerCallBackCancelDeleter - IntervalTimer* itimer_counter = new IntervalTimer(io_service_); - IntervalTimer itimer_canceller(io_service_); - timer_cancel_success_ = false; - TimerCallBackCounter callback_canceller(this); - itimer_counter->setupTimer(callback_canceller, 2); - itimer_canceller.setupTimer( - TimerCallBackCancelDeleter(this, itimer_counter, - callback_canceller), - 3); - io_service_.run(); - EXPECT_TRUE(timer_cancel_success_); -} - -TEST_F(IntervalTimerTest, cancel) { - // Similar to destructIntervalTimer test, but the first timer explicitly - // cancels itself on first callback. - IntervalTimer itimer_counter(io_service_); - IntervalTimer itimer_watcher(io_service_); - unsigned int counter = 0; - itimer_counter.setupTimer(TimerCallBackCanceller(counter, itimer_counter), - 1); - itimer_watcher.setupTimer(TimerCallBack(this), 3); - io_service_.run(); - EXPECT_EQ(1, counter); - EXPECT_EQ(0, itimer_counter.getInterval()); - - // canceling an already canceled timer shouldn't cause any surprise. - EXPECT_NO_THROW(itimer_counter.cancel()); -} - -TEST_F(IntervalTimerTest, overwriteIntervalTimer) { - // Note: This test currently takes 4 seconds. The timer should have - // finer granularity and timer periods in this test should be shorter - // in the future. - - // Calling setupTimer() multiple times updates call back function - // and interval. - // - // There are two timers: - // itimer (A) - // (Calls TimerCallBackCounter / TimerCallBack) - // - increments internal counter in callback function - // (TimerCallBackCounter) - // interval: 2 seconds - // - io_service_.stop() (TimerCallBack) - // interval: 1 second - // itimer_overwriter (B) - // (Calls TimerCallBackOverwriter) - // - first time of callback, it calls setupTimer() to change - // call back function and interval of itimer to - // TimerCallBack / 1 second - // after 3 + 1 seconds from the beginning of this test, - // TimerCallBack() will be called and io_service_ stops. - // - second time of callback, it means the test fails. - // - // 0 1 2 3 4 5 6 (s) - // (A) i-----+--C--s - // ^ ^stop io_service - // |change call back function - // (B) i--------+--------S - // ^(stop io_service on fail) - // - - IntervalTimer itimer(io_service_); - IntervalTimer itimer_overwriter(io_service_); - // store start time - boost::posix_time::ptime start; - start = boost::posix_time::microsec_clock::universal_time(); - itimer.setupTimer(TimerCallBackCounter(this), 2); - itimer_overwriter.setupTimer(TimerCallBackOverwriter(this, itimer), 3); - io_service_.run(); - // reaches here after timer expired - // if interval is updated, it takes - // 3 seconds for TimerCallBackOverwriter - // + 1 second for TimerCallBack (stop) - // = 4 seconds. - // otherwise (test fails), it takes - // 3 seconds for TimerCallBackOverwriter - // + 3 seconds for TimerCallBackOverwriter (stop) - // = 6 seconds. - // delta: difference between elapsed time and 3 + 1 seconds - boost::posix_time::time_duration delta = - (boost::posix_time::microsec_clock::universal_time() - start) - - boost::posix_time::seconds(3 + 1); - if (delta.is_negative()) { - delta.invert_sign(); - } - // expect callback function is updated: TimerCallBack is called - EXPECT_TRUE(timer_called_); - // expect interval is updated - EXPECT_TRUE(delta < TIMER_MARGIN_MSEC); -} - } diff --git a/src/lib/asiolink/tests/udp_endpoint_unittest.cc b/src/lib/asiolink/tests/udp_endpoint_unittest.cc new file mode 100644 index 0000000000000000000000000000000000000000..18135ec26b4b12d0df4cfa4be317c368004bf60f --- /dev/null +++ b/src/lib/asiolink/tests/udp_endpoint_unittest.cc @@ -0,0 +1,55 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include + +#include + +#include + +#include +#include +#include + +using namespace asiolink; +using namespace std; + +// This test checks that the endpoint can manage its own internal +// asio::ip::udp::endpoint object. + +TEST(UDPEndpointTest, v4Address) { + const string test_address("192.0.2.1"); + const unsigned short test_port = 5301; + + IOAddress address(test_address); + UDPEndpoint endpoint(address, test_port); + + EXPECT_TRUE(address == endpoint.getAddress()); + EXPECT_EQ(test_port, endpoint.getPort()); + EXPECT_EQ(IPPROTO_UDP, endpoint.getProtocol()); + EXPECT_EQ(AF_INET, endpoint.getFamily()); +} + +TEST(UDPEndpointTest, v6Address) { + const string test_address("2001:db8::1235"); + const unsigned short test_port = 5302; + + IOAddress address(test_address); + UDPEndpoint endpoint(address, test_port); + + EXPECT_TRUE(address == endpoint.getAddress()); + EXPECT_EQ(test_port, endpoint.getPort()); + EXPECT_EQ(IPPROTO_UDP, endpoint.getProtocol()); + EXPECT_EQ(AF_INET6, endpoint.getFamily()); +} diff --git a/src/lib/asiolink/tests/udp_socket_unittest.cc b/src/lib/asiolink/tests/udp_socket_unittest.cc new file mode 100644 index 0000000000000000000000000000000000000000..7332d29ffe5eb6ecf2193945f535a5ff1600355a --- /dev/null +++ b/src/lib/asiolink/tests/udp_socket_unittest.cc @@ -0,0 +1,287 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + + +/// \brief Test of UDPSocket +/// +/// Tests the fuctionality of a UDPSocket by working through an open-send- +/// receive-close sequence and checking that the asynchronous notifications +/// work. + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include + +#include + +#include +#include +#include + +using namespace asio; +using namespace asiolink; +using namespace std; + +namespace { + +const char SERVER_ADDRESS[] = "127.0.0.1"; +const unsigned short SERVER_PORT = 5301; + +// TODO: Shouldn't we send something that is real message? +const char OUTBOUND_DATA[] = "Data sent from client to server"; +const char INBOUND_DATA[] = "Returned data from server to client"; +} + +/// +/// An instance of this object is passed to the asynchronous I/O functions +/// and the operator() method is called when when an asynchronous I/O +/// completes. The arguments to the completion callback are stored for later +/// retrieval. +class UDPCallback { +public: + + struct PrivateData { + PrivateData() : + error_code_(), length_(0), called_(false), name_("") + {} + + asio::error_code error_code_; ///< Completion error code + size_t length_; ///< Number of bytes transferred + bool called_; ///< Set true when callback called + std::string name_; ///< Which of the objects this is + }; + + /// \brief Constructor + /// + /// Constructs the object. It also creates the data member pointed to by + /// a shared pointer. When used as a callback object, this is copied as it + /// is passed into the asynchronous function. This means that there are two + /// objects and inspecting the one we passed in does not tell us anything. + /// + /// Therefore we use a boost::shared_ptr. When the object is copied, the + /// shared pointer is copied, which leaves both objects pointing to the same + /// data. + /// + /// \param which Which of the two callback objects this is + UDPCallback(std::string which) : ptr_(new PrivateData()) + { + setName(which); + } + + /// \brief Destructor + /// + /// No code needed, destroying the shared pointer destroys the private data. + virtual ~UDPCallback() + {} + + /// \brief Callback Function + /// + /// Called when an asynchronous I/O completes, this stores the + /// completion error code and the number of bytes transferred. + /// + /// \param ec I/O completion error code passed to callback function. + /// \param length Number of bytes transferred + virtual void operator()(asio::error_code ec, size_t length = 0) { + ptr_->error_code_ = ec; + setLength(length); + setCalled(true); + } + + /// \brief Get I/O completion error code + int getCode() { + return (ptr_->error_code_.value()); + } + + /// \brief Set I/O completion code + /// + /// \param code New value of completion code + void setCode(int code) { + ptr_->error_code_ = asio::error_code(code, asio::error_code().category()); + } + + /// \brief Get number of bytes transferred in I/O + size_t getLength() { + return (ptr_->length_); + } + + /// \brief Set number of bytes transferred in I/O + /// + /// \param length New value of length parameter + void setLength(size_t length) { + ptr_->length_ = length; + } + + /// \brief Get flag to say when callback was called + bool getCalled() { + return (ptr_->called_); + } + + /// \brief Set flag to say when callback was called + /// + /// \param called New value of called parameter + void setCalled(bool called) { + ptr_->called_ = called; + } + + /// \brief Return instance of callback name + std::string getName() { + return (ptr_->name_); + } + + /// \brief Set callback name + /// + /// \param name New value of the callback name + void setName(const std::string& name) { + ptr_->name_ = name; + } + +private: + boost::shared_ptr ptr_; ///< Pointer to private data +}; + +// TODO: Need to add a test to check the cancel() method + +// Tests the operation of a UDPSocket by opening it, sending an asynchronous +// message to a server, receiving an asynchronous message from the server and +// closing. +TEST(UDPSocket, SequenceTest) { + + // Common objects. + IOService service; // Service object for async control + + // Server + IOAddress server_address(SERVER_ADDRESS); // Address of target server + UDPCallback server_cb("Server"); // Server callback + UDPEndpoint server_endpoint( // Endpoint describing server + server_address, SERVER_PORT); + UDPEndpoint server_remote_endpoint; // Address where server received message from + + // The client - the UDPSocket being tested + UDPSocket client(service);// Socket under test + UDPCallback client_cb("Client"); // Async I/O callback function + UDPEndpoint client_remote_endpoint; // Where client receives message from + size_t client_cumulative = 0; // Cumulative data received + + // The server - with which the client communicates. For convenience, we + // use the same io_service, and use the endpoint object created for + // the client to send to as the endpoint object in the constructor. + asio::ip::udp::socket server(service.get_io_service(), + server_endpoint.getASIOEndpoint()); + server.set_option(socket_base::reuse_address(true)); + + // Assertion to ensure that the server buffer is large enough + char data[UDPSocket::MAX_SIZE]; + ASSERT_GT(sizeof(data), sizeof(OUTBOUND_DATA)); + + // Open the client socket - the operation should be synchronous + EXPECT_FALSE(client.open(&server_endpoint, client_cb)); + + // Issue read on the server. Completion callback should not have run. + server_cb.setCalled(false); + server_cb.setCode(42); // Answer to Life, the Universe and Everything! + server.async_receive_from(buffer(data, sizeof(data)), + server_remote_endpoint.getASIOEndpoint(), server_cb); + EXPECT_FALSE(server_cb.getCalled()); + + // Write something to the server using the client - the callback should not + // be called until we call the io_service.run() method. + client_cb.setCalled(false); + client_cb.setCode(7); // Arbitrary number + client.asyncSend(OUTBOUND_DATA, sizeof(OUTBOUND_DATA), &server_endpoint, client_cb); + EXPECT_FALSE(client_cb.getCalled()); + + // Execute the two callbacks. + service.run_one(); + service.run_one(); + + EXPECT_TRUE(client_cb.getCalled()); + EXPECT_EQ(0, client_cb.getCode()); + EXPECT_EQ(sizeof(OUTBOUND_DATA), client_cb.getLength()); + + EXPECT_TRUE(server_cb.getCalled()); + EXPECT_EQ(0, server_cb.getCode()); + EXPECT_EQ(sizeof(OUTBOUND_DATA), server_cb.getLength()); + + EXPECT_TRUE(equal(&data[0], &data[server_cb.getLength() - 1], OUTBOUND_DATA)); + + // Now return data from the server to the client. Issue the read on the + // client. + client_cb.setLength(12345); // Arbitrary number + client_cb.setCalled(false); + client_cb.setCode(32); // Arbitrary number + client.asyncReceive(data, sizeof(data), client_cumulative, + &client_remote_endpoint, client_cb); + + // Issue the write on the server side to the source of the data it received. + server_cb.setLength(22345); // Arbitrary number + server_cb.setCalled(false); + server_cb.setCode(232); // Arbitrary number + server.async_send_to(buffer(INBOUND_DATA, sizeof(INBOUND_DATA)), + server_remote_endpoint.getASIOEndpoint(), server_cb); + + // Expect two callbacks to run + service.get_io_service().poll(); + //service.run_one(); + + EXPECT_TRUE(client_cb.getCalled()); + EXPECT_EQ(0, client_cb.getCode()); + EXPECT_EQ(sizeof(INBOUND_DATA), client_cb.getLength()); + + EXPECT_TRUE(server_cb.getCalled()); + EXPECT_EQ(0, server_cb.getCode()); + EXPECT_EQ(sizeof(INBOUND_DATA), server_cb.getLength()); + + EXPECT_TRUE(equal(&data[0], &data[server_cb.getLength() - 1], INBOUND_DATA)); + + // Check that the address/port received by the client corresponds to the + // address and port the server is listening on. + EXPECT_TRUE(server_address == client_remote_endpoint.getAddress()); + EXPECT_EQ(SERVER_PORT, client_remote_endpoint.getPort()); + + // Finally, check that the receive received a complete buffer's worth of data. + EXPECT_TRUE(client.receiveComplete(&data[0], client_cb.getLength(), + client_cumulative)); + EXPECT_EQ(client_cb.getLength(), client_cumulative); + + // Close client and server. + EXPECT_NO_THROW(client.close()); + EXPECT_NO_THROW(server.close()); +} diff --git a/src/lib/asiolink/udp_endpoint.h b/src/lib/asiolink/udp_endpoint.h new file mode 100644 index 0000000000000000000000000000000000000000..0958af6e4d6b16c735184c790d72aeb293b0b1b5 --- /dev/null +++ b/src/lib/asiolink/udp_endpoint.h @@ -0,0 +1,102 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __UDP_ENDPOINT_H +#define __UDP_ENDPOINT_H 1 + +#ifndef ASIO_HPP +#error "asio.hpp must be included before including this, see asiolink.h as to why" +#endif + +#include + +namespace asiolink { + +/// \brief The \c UDPEndpoint class is a concrete derived class of +/// \c IOEndpoint that represents an endpoint of a UDP packet. +/// +/// Other notes about \c TCPEndpoint applies to this class, too. +class UDPEndpoint : public IOEndpoint { +public: + /// + /// \name Constructors and Destructor. + /// + //@{ + + /// \brief Default Constructor + /// + /// Creates an internal endpoint. This is expected to be set by some + /// external call. + UDPEndpoint() : + asio_endpoint_placeholder_(new asio::ip::udp::endpoint()), + asio_endpoint_(*asio_endpoint_placeholder_) + {} + + /// \brief Constructor from a pair of address and port. + /// + /// \param address The IP address of the endpoint. + /// \param port The UDP port number of the endpoint. + UDPEndpoint(const IOAddress& address, const unsigned short port) : + asio_endpoint_placeholder_( + new asio::ip::udp::endpoint(asio::ip::address::from_string(address.toText()), + port)), + asio_endpoint_(*asio_endpoint_placeholder_) + {} + + /// \brief Constructor from an ASIO UDP endpoint. + /// + /// This constructor is designed to be an efficient wrapper for the + /// corresponding ASIO class, \c udp::endpoint. + /// + /// \param asio_endpoint The ASIO representation of the UDP endpoint. + UDPEndpoint(asio::ip::udp::endpoint& asio_endpoint) : + asio_endpoint_placeholder_(NULL), asio_endpoint_(asio_endpoint) + {} + + /// \brief The destructor. + virtual ~UDPEndpoint() { delete asio_endpoint_placeholder_; } + //@} + + virtual IOAddress getAddress() const { + return (asio_endpoint_.address()); + } + + virtual uint16_t getPort() const { + return (asio_endpoint_.port()); + } + + virtual short getProtocol() const { + return (asio_endpoint_.protocol().protocol()); + } + + virtual short getFamily() const { + return (asio_endpoint_.protocol().family()); + } + + // This is not part of the exosed IOEndpoint API but allows + // direct access to the ASIO implementation of the endpoint + inline const asio::ip::udp::endpoint& getASIOEndpoint() const { + return (asio_endpoint_); + } + inline asio::ip::udp::endpoint& getASIOEndpoint() { + return (asio_endpoint_); + } + +private: + asio::ip::udp::endpoint* asio_endpoint_placeholder_; + asio::ip::udp::endpoint& asio_endpoint_; +}; + +} // namespace asiolink +#endif // __UDP_ENDPOINT_H diff --git a/src/lib/asiolink/udp_server.cc b/src/lib/asiolink/udp_server.cc new file mode 100644 index 0000000000000000000000000000000000000000..9a5f00ebac908ef613cdc696b9257c61f88e2c77 --- /dev/null +++ b/src/lib/asiolink/udp_server.cc @@ -0,0 +1,294 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include // for some IPC/network system calls + +#include + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +using namespace asio; +using asio::ip::udp; +using isc::log::dlog; + +using namespace std; +using namespace isc::dns; + +namespace asiolink { + +/* + * Some of the member variables here are shared_ptrs and some are + * auto_ptrs. There will be one instance of Data for the lifetime + * of packet. The variables that are state only for a single packet + * use auto_ptr, as it is more lightweight. In the case of shared + * configuration (eg. the callbacks, socket), we use shared_ptrs. + */ +struct UDPServer::Data { + /* + * Constructor from parameters passed to UDPServer constructor. + * This instance will not be used to retrieve and answer the actual + * query, it will only hold parameters until we wait for the + * first packet. But we do initialize the socket in here. + */ + Data(io_service& io_service, const ip::address& addr, const uint16_t port, + SimpleCallback* checkin, DNSLookup* lookup, DNSAnswer* answer) : + io_(io_service), done_(false), checkin_callback_(checkin), + lookup_callback_(lookup), answer_callback_(answer) + { + // We must use different instantiations for v4 and v6; + // otherwise ASIO will bind to both + udp proto = addr.is_v4() ? udp::v4() : udp::v6(); + socket_.reset(new udp::socket(io_service, proto)); + socket_->set_option(socket_base::reuse_address(true)); + if (addr.is_v6()) { + socket_->set_option(asio::ip::v6_only(true)); + } + socket_->bind(udp::endpoint(addr, port)); + } + + /* + * Copy constructor. Default one would probably do, but it is unnecessary + * to copy many of the member variables every time we fork to handle + * another packet. + * + * We also allocate data for receiving the packet here. + */ + Data(const Data& other) : + io_(other.io_), socket_(other.socket_), done_(false), + checkin_callback_(other.checkin_callback_), + lookup_callback_(other.lookup_callback_), + answer_callback_(other.answer_callback_) + { + // Instantiate the data buffer and endpoint that will + // be used by the asynchronous receive call. + data_.reset(new char[MAX_LENGTH]); + sender_.reset(new udp::endpoint()); + } + + // The ASIO service object + asio::io_service& io_; + + // Class member variables which are dynamic, and changes to which + // need to accessible from both sides of a coroutine fork or from + // outside of the coroutine (i.e., from an asynchronous I/O call), + // should be declared here as pointers and allocated in the + // constructor or in the coroutine. This allows state information + // to persist when an individual copy of the coroutine falls out + // scope while waiting for an event, *so long as* there is another + // object that is referencing the same data. As a side-benefit, using + // pointers also reduces copy overhead for coroutine objects. + // + // Note: Currently these objects are allocated by "new" in the + // constructor, or in the function operator while processing a query. + // Repeated allocations from the heap for every incoming query is + // clearly a performance issue; this must be optimized in the future. + // The plan is to have a structure pre-allocate several "Data" + // objects which can be pulled off a free list and placed on an in-use + // list whenever a query comes in. This will serve the dual purpose + // of improving performance and guaranteeing that state information + // will *not* be destroyed when any one instance of the coroutine + // falls out of scope while waiting for an event. + // + // Socket used to for listen for queries. Created in the + // constructor and stored in a shared_ptr because socket objects + // are not copyable. + boost::shared_ptr socket_; + + // The ASIO-internal endpoint object representing the client + std::auto_ptr sender_; + + // \c IOMessage and \c Message objects to be passed to the + // DNS lookup and answer providers + std::auto_ptr io_message_; + + // The original query as sent by the client + isc::dns::MessagePtr query_message_; + + // The response message we are building + isc::dns::MessagePtr answer_message_; + + // The buffer into which the response is written + isc::dns::OutputBufferPtr respbuf_; + + // The buffer into which the query packet is written + boost::shared_array data_; + + // State information that is entirely internal to a given instance + // of the coroutine can be declared here. + size_t bytes_; + bool done_; + + // Callback functions provided by the caller + const SimpleCallback* checkin_callback_; + const DNSLookup* lookup_callback_; + const DNSAnswer* answer_callback_; + + std::auto_ptr peer_; + std::auto_ptr iosock_; +}; + +/// The following functions implement the \c UDPServer class. +/// +/// The constructor. It just creates new internal state object +/// and lets it handle the initialization. +UDPServer::UDPServer(io_service& io_service, const ip::address& addr, + const uint16_t port, SimpleCallback* checkin, DNSLookup* lookup, + DNSAnswer* answer) : + data_(new Data(io_service, addr, port, checkin, lookup, answer)) +{ } + +/// The function operator is implemented with the "stackless coroutine" +/// pattern; see internal/coroutine.h for details. +void +UDPServer::operator()(error_code ec, size_t length) { + /// Because the coroutine reeentry block is implemented as + /// a switch statement, inline variable declarations are not + /// permitted. Certain variables used below can be declared here. + + CORO_REENTER (this) { + do { + /* + * This is preparation for receiving a packet. We get a new + * state object for the lifetime of the next packet to come. + * It allocates the buffers to receive data into. + */ + data_.reset(new Data(*data_)); + + do { + // Begin an asynchronous receive, then yield. + // When the receive event is posted, the coroutine + // will resume immediately after this point. + CORO_YIELD data_->socket_->async_receive_from( + buffer(data_->data_.get(), MAX_LENGTH), *data_->sender_, + *this); + } while (ec || length == 0); + + data_->bytes_ = length; + + /* + * We fork the coroutine now. One (the child) will keep + * the current state and handle the packet, then die and + * drop ownership of the state. The other (parent) will just + * go into the loop again and replace the current state with + * a new one for a new object. + * + * Actually, both of the coroutines will be a copy of this + * one, but that's just internal implementation detail. + */ + CORO_FORK data_->io_.post(UDPServer(*this)); + } while (is_parent()); + + // Create an \c IOMessage object to store the query. + // + // (XXX: It would be good to write a factory function + // that would quickly generate an IOMessage object without + // all these calls to "new".) + data_->peer_.reset(new UDPEndpoint(*data_->sender_)); + + // The UDP socket class has been extended with asynchronous functions + // and takes as a template parameter a completion callback class. As + // UDPServer does not use these extended functions (only those defined + // in the IOSocket base class) - but needs a UDPSocket to get hold of + // the underlying Boost UDP socket - DummyIOCallback is used. This + // provides the appropriate operator() but is otherwise functionless. + data_->iosock_.reset( + new UDPSocket(*data_->socket_)); + + data_->io_message_.reset(new IOMessage(data_->data_.get(), + data_->bytes_, *data_->iosock_, *data_->peer_)); + + // Perform any necessary operations prior to processing an incoming + // query (e.g., checking for queued configuration messages). + // + // (XXX: it may be a performance issue to check in for every single + // incoming query; we may wish to throttle this in the future.) + if (data_->checkin_callback_ != NULL) { + (*data_->checkin_callback_)(*data_->io_message_); + } + + // If we don't have a DNS Lookup provider, there's no point in + // continuing; we exit the coroutine permanently. + if (data_->lookup_callback_ == NULL) { + CORO_YIELD return; + } + + // Instantiate objects that will be needed by the + // asynchronous DNS lookup and/or by the send call. + data_->respbuf_.reset(new OutputBuffer(0)); + data_->query_message_.reset(new Message(Message::PARSE)); + data_->answer_message_.reset(new Message(Message::RENDER)); + + // Schedule a DNS lookup, and yield. When the lookup is + // finished, the coroutine will resume immediately after + // this point. + CORO_YIELD data_->io_.post(AsyncLookup(*this)); + + dlog("[XX] got an answer"); + + // The 'done_' flag indicates whether we have an answer + // to send back. If not, exit the coroutine permanently. + if (!data_->done_) { + CORO_YIELD return; + } + + // Call the DNS answer provider to render the answer into + // wire format + (*data_->answer_callback_)(*data_->io_message_, data_->query_message_, + data_->answer_message_, data_->respbuf_); + + // Begin an asynchronous send, and then yield. When the + // send completes, we will resume immediately after this point + // (though we have nothing further to do, so the coroutine + // will simply exit at that time). + CORO_YIELD data_->socket_->async_send_to( + buffer(data_->respbuf_->getData(), data_->respbuf_->getLength()), + *data_->sender_, *this); + } +} + +/// Call the DNS lookup provider. (Expected to be called by the +/// AsyncLookup handler.) +void +UDPServer::asyncLookup() { + (*data_->lookup_callback_)(*data_->io_message_, + data_->query_message_, data_->answer_message_, data_->respbuf_, this); +} + +/// Post this coroutine on the ASIO service queue so that it will +/// resume processing where it left off. The 'done' parameter indicates +/// whether there is an answer to return to the client. +void +UDPServer::resume(const bool done) { + data_->done_ = done; + data_->io_.post(*this); +} + +bool +UDPServer::hasAnswer() { + return (data_->done_); +} + +} // namespace asiolink diff --git a/src/lib/asiolink/udp_server.h b/src/lib/asiolink/udp_server.h new file mode 100644 index 0000000000000000000000000000000000000000..16a03dddd4bf3f0d359647e63fb3a5627a5ba426 --- /dev/null +++ b/src/lib/asiolink/udp_server.h @@ -0,0 +1,102 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __UDP_SERVER_H +#define __UDP_SERVER_H 1 + +#ifndef ASIO_HPP +#error "asio.hpp must be included before including this, see asiolink.h as to why" +#endif + +#include +#include +#include +#include + +#include + +namespace asiolink { + +// +// Asynchronous UDP server coroutine +// +/// +/// \brief This class implements the coroutine to handle UDP +/// DNS query event. As such, it is both a \c DNSServer and +/// a \c coroutine +/// +class UDPServer : public virtual DNSServer, public virtual coroutine { +public: + /// \brief Constructor + /// \param io_service the asio::io_service to work with + /// \param addr the IP address to listen for queries on + /// \param port the port to listen for queries on + /// \param checkin the callbackprovider for non-DNS events + /// \param lookup the callbackprovider for DNS lookup events + /// \param answer the callbackprovider for DNS answer events + explicit UDPServer(asio::io_service& io_service, + const asio::ip::address& addr, const uint16_t port, + SimpleCallback* checkin = NULL, + DNSLookup* lookup = NULL, + DNSAnswer* answer = NULL); + + /// \brief The function operator + void operator()(asio::error_code ec = asio::error_code(), + size_t length = 0); + + /// \brief Calls the lookup callback + void asyncLookup(); + + /// \brief Resume operation + /// + /// \param done Set this to true if the lookup action is done and + /// we have an answer + void resume(const bool done); + + /// \brief Check if we have an answer + /// + /// \return true if we have an answer + bool hasAnswer(); + + /// \brief Returns the coroutine state value + /// + /// \return the coroutine state value + int value() { return (get_value()); } + + /// \brief Clones the object + /// + /// \return a newly allocated copy of this object + DNSServer* clone() { + UDPServer* s = new UDPServer(*this); + return (s); + } + +private: + enum { MAX_LENGTH = 4096 }; + + /** + * \brief Internal state and data. + * + * We use the pimple design pattern, but not because we need to hide + * internal data. This class and whole header is for private use anyway. + * It turned out that UDPServer is copied a lot, because it is a coroutine. + * This way the overhead of copying is lower, we copy only one shared + * pointer instead of about 10 of them. + */ + class Data; + boost::shared_ptr data_; +}; + +} // namespace asiolink +#endif // __UDP_SERVER_H diff --git a/src/lib/asiolink/udp_socket.h b/src/lib/asiolink/udp_socket.h new file mode 100644 index 0000000000000000000000000000000000000000..bb94ad56f2148991feef59dbcc2b10db90f10921 --- /dev/null +++ b/src/lib/asiolink/udp_socket.h @@ -0,0 +1,276 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __UDP_SOCKET_H +#define __UDP_SOCKET_H 1 + +#ifndef ASIO_HPP +#error "asio.hpp must be included before including this, see asiolink.h as to why" +#endif + +#include +#include +#include +#include // for some IPC/network system calls + +#include + +#include + + +#include +#include +#include +#include + +namespace asiolink { + +/// \brief The \c UDPSocket class is a concrete derived class of \c IOAsioSocket +/// that represents a UDP socket. +/// +/// \param C Callback type +template +class UDPSocket : public IOAsioSocket { +private: + /// \brief Class is non-copyable + UDPSocket(const UDPSocket&); + UDPSocket& operator=(const UDPSocket&); + +public: + enum { + MAX_SIZE = 4096 // Send and receive size + }; + + /// \brief Constructor from an ASIO UDP socket. + /// + /// \param socket The ASIO representation of the UDP socket. It + /// is assumed that the caller will open and close the socket, so + /// these operations are a no-op for that socket. + UDPSocket(asio::ip::udp::socket& socket); + + /// \brief Constructor + /// + /// Used when the UDPSocket is being asked to manage its own internal + /// socket. It is assumed that open() and close() will not be used. + /// + /// \param service I/O Service object used to manage the socket. + UDPSocket(IOService& service); + + /// \brief Destructor + virtual ~UDPSocket(); + + virtual int getNative() const { return (socket_.native()); } + virtual int getProtocol() const { return (IPPROTO_UDP); } + + /// \brief Open Socket + /// + /// Opens the UDP socket. In the model for transport-layer agnostic I/O, + /// an "open" operation includes a connection to the remote end (which + /// may take time). This does not happen for UDP, so the method returns + /// "false" to indicate that the operation completed synchronously. + /// + /// \param endpoint Endpoint to which the socket will connect to. + /// \param callback Unused. + /// + /// \return false to indicate that the "operation" completed synchronously. + virtual bool open(const IOEndpoint* endpoint, C&); + + /// \brief Send Asynchronously + /// + /// This corresponds to async_send_to() for UDP sockets and async_send() + /// for TCP. In both cases an endpoint argument is supplied indicating the + /// target of the send - this is ignored for TCP. + /// + /// \param data Data to send + /// \param length Length of data to send + /// \param endpoint Target of the send + /// \param callback Callback object. + virtual void asyncSend(const void* data, size_t length, + const IOEndpoint* endpoint, C& callback); + + /// \brief Receive Asynchronously + /// + /// This correstponds to async_receive_from() for UDP sockets and + /// async_receive() for TCP. In both cases, an endpoint argument is + /// supplied to receive the source of the communication. For TCP it will + /// be filled in with details of the connection. + /// + /// \param data Buffer to receive incoming message + /// \param length Length of the data buffer + /// \param cumulative Amount of data that should already be in the buffer. + /// (This is ignored - every UPD receive fills the buffer from the start.) + /// \param endpoint Source of the communication + /// \param callback Callback object + virtual void asyncReceive(void* data, size_t length, size_t cumulative, + IOEndpoint* endpoint, C& callback); + + /// \brief Checks if the data received is complete. + /// + /// As all the data is received in one I/O, so this is, this is effectively + /// a no-op (although it does update the amount of data received). + /// + /// \param data Data buffer containing data to date. (This is ignored + /// for UDP receives.) + /// \param length Amount of data received in last asynchronous I/O + /// \param cumulative On input, amount of data received before the last + /// I/O. On output, the total amount of data received to date. + /// + /// \return true if the receive is complete, false if another receive is + /// needed. + virtual bool receiveComplete(void*, size_t length, size_t& cumulative) { + cumulative = length; + return (true); + } + + /// \brief Cancel I/O On Socket + virtual void cancel(); + + /// \brief Close socket + virtual void close(); + + +private: + // Two variables to hold the socket - a socket and a pointer to it. This + // handles the case where a socket is passed to the UDPSocket on + // construction, or where it is asked to manage its own socket. + asio::ip::udp::socket* socket_ptr_; ///< Pointer to own socket + asio::ip::udp::socket& socket_; ///< Socket + bool isopen_; ///< true when socket is open +}; + +// Constructor - caller manages socket + +template +UDPSocket::UDPSocket(asio::ip::udp::socket& socket) : + socket_ptr_(NULL), socket_(socket), isopen_(true) +{ +} + +// Constructor - create socket on the fly + +template +UDPSocket::UDPSocket(IOService& service) : + socket_ptr_(new asio::ip::udp::socket(service.get_io_service())), + socket_(*socket_ptr_), isopen_(false) +{ +} + +// Destructor. Only delete the socket if we are managing it. + +template +UDPSocket::~UDPSocket() +{ + delete socket_ptr_; +} + +// Open the socket. Throws an error on failure +// TODO: Make the open more resilient + +template bool +UDPSocket::open(const IOEndpoint* endpoint, C&) { + + // Ignore opens on already-open socket. Don't throw a failure because + // of uncertainties as to what precedes whan when using asynchronous I/O. + // At also allows us a treat a passed-in socket as a self-managed socket. + + if (!isopen_) { + if (endpoint->getFamily() == AF_INET) { + socket_.open(asio::ip::udp::v4()); + } + else { + socket_.open(asio::ip::udp::v6()); + } + isopen_ = true; + + // Ensure it can send and receive 4K buffers. + socket_.set_option(asio::socket_base::send_buffer_size(MAX_SIZE)); + socket_.set_option(asio::socket_base::receive_buffer_size(MAX_SIZE)); + ; + // Allow reuse of an existing port/address + socket_.set_option(asio::socket_base::reuse_address(true)); + } + return (false); +} + +// Send a message. Should never do this if the socket is not open, so throw +// an exception if this is the case. + +template void +UDPSocket::asyncSend(const void* data, size_t length, + const IOEndpoint* endpoint, C& callback) +{ + if (isopen_) { + + // Upconvert to a UDPEndpoint. We need to do this because although + // IOEndpoint is the base class of UDPEndpoint and TCPEndpoint, it + // doing cont contain a method for getting at the underlying endpoint + // type - those are in the derived class and the two classes differ on + // return type. + + assert(endpoint->getProtocol() == IPPROTO_UDP); + const UDPEndpoint* udp_endpoint = + static_cast(endpoint); + socket_.async_send_to(asio::buffer(data, length), + udp_endpoint->getASIOEndpoint(), callback); + } else { + isc_throw(SocketNotOpen, + "attempt to send on a UDP socket that is not open"); + } +} + +// Receive a message. Note that the "cumulative" argument is ignored - every UDP +// receive is put into the buffer beginning at the start - there is no concept +// receiving a subsequent part of a message. Same critera as before concerning +// the need for the socket to be open. + +template void +UDPSocket::asyncReceive(void* data, size_t length, size_t, + IOEndpoint* endpoint, C& callback) +{ + if (isopen_) { + + // Upconvert the endpoint again. + assert(endpoint->getProtocol() == IPPROTO_UDP); + UDPEndpoint* udp_endpoint = static_cast(endpoint); + + socket_.async_receive_from(asio::buffer(data, length), + udp_endpoint->getASIOEndpoint(), callback); + } else { + isc_throw(SocketNotOpen, + "attempt to receive from a UDP socket that is not open"); + } +} + +// Cancel I/O on the socket. No-op if the socket is not open. +template void +UDPSocket::cancel() { + if (isopen_) { + socket_.cancel(); + } +} + +// Close the socket down. Can only do this if the socket is open and we are +// managing it ourself. + +template void +UDPSocket::close() { + if (isopen_ && socket_ptr_) { + socket_.close(); + isopen_ = false; + } +} + +} // namespace asiolink + +#endif // __UDP_SOCKET_H diff --git a/src/lib/asiolink/udpdns.cc b/src/lib/asiolink/udpdns.cc deleted file mode 100644 index adb07b28f1168ea51a8970716805e223dde3d44f..0000000000000000000000000000000000000000 --- a/src/lib/asiolink/udpdns.cc +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -#include - -#include // for some IPC/network system calls -#include -#include - -#include - -#include -#include - -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -using namespace asio; -using asio::ip::udp; -using asio::ip::tcp; -using isc::log::dlog; - -using namespace std; -using namespace isc::dns; - -namespace asiolink { -/// The following functions implement the \c UDPServer class. -/// -/// The constructor -UDPServer::UDPServer(io_service& io_service, - const ip::address& addr, const uint16_t port, - SimpleCallback* checkin, - DNSLookup* lookup, - DNSAnswer* answer) : - io_(io_service), done_(false), - checkin_callback_(checkin), - lookup_callback_(lookup), - answer_callback_(answer) -{ - // We must use different instantiations for v4 and v6; - // otherwise ASIO will bind to both - udp proto = addr.is_v4() ? udp::v4() : udp::v6(); - socket_.reset(new udp::socket(io_service, proto)); - socket_->set_option(socket_base::reuse_address(true)); - if (addr.is_v6()) { - socket_->set_option(asio::ip::v6_only(true)); - } - socket_->bind(udp::endpoint(addr, port)); -} - -/// The function operator is implemented with the "stackless coroutine" -/// pattern; see internal/coroutine.h for details. -void -UDPServer::operator()(error_code ec, size_t length) { - /// Because the coroutine reeentry block is implemented as - /// a switch statement, inline variable declarations are not - /// permitted. Certain variables used below can be declared here. - - CORO_REENTER (this) { - do { - // Instantiate the data buffer and endpoint that will - // be used by the asynchronous receive call. - data_.reset(new char[MAX_LENGTH]); - sender_.reset(new udp::endpoint()); - - do { - // Begin an asynchronous receive, then yield. - // When the receive event is posted, the coroutine - // will resume immediately after this point. - CORO_YIELD socket_->async_receive_from(buffer(data_.get(), - MAX_LENGTH), - *sender_, *this); - } while (ec || length == 0); - - bytes_ = length; - - /// Fork the coroutine by creating a copy of this one and - /// scheduling it on the ASIO service queue. The parent - /// will continue listening for DNS packets while the child - /// processes the one that has just arrived. - CORO_FORK io_.post(UDPServer(*this)); - } while (is_parent()); - - // Create an \c IOMessage object to store the query. - // - // (XXX: It would be good to write a factory function - // that would quickly generate an IOMessage object without - // all these calls to "new".) - peer_.reset(new UDPEndpoint(*sender_)); - iosock_.reset(new UDPSocket(*socket_)); - io_message_.reset(new IOMessage(data_.get(), bytes_, *iosock_, *peer_)); - - // Perform any necessary operations prior to processing an incoming - // query (e.g., checking for queued configuration messages). - // - // (XXX: it may be a performance issue to check in for every single - // incoming query; we may wish to throttle this in the future.) - if (checkin_callback_ != NULL) { - (*checkin_callback_)(*io_message_); - } - - // If we don't have a DNS Lookup provider, there's no point in - // continuing; we exit the coroutine permanently. - if (lookup_callback_ == NULL) { - CORO_YIELD return; - } - - // Instantiate objects that will be needed by the - // asynchronous DNS lookup and/or by the send call. - respbuf_.reset(new OutputBuffer(0)); - query_message_.reset(new Message(Message::PARSE)); - answer_message_.reset(new Message(Message::RENDER)); - - // Schedule a DNS lookup, and yield. When the lookup is - // finished, the coroutine will resume immediately after - // this point. - CORO_YIELD io_.post(AsyncLookup(*this)); - - dlog("[XX] got an answer"); - - // The 'done_' flag indicates whether we have an answer - // to send back. If not, exit the coroutine permanently. - if (!done_) { - CORO_YIELD return; - } - - // Call the DNS answer provider to render the answer into - // wire format - (*answer_callback_)(*io_message_, query_message_, - answer_message_, respbuf_); - - // Begin an asynchronous send, and then yield. When the - // send completes, we will resume immediately after this point - // (though we have nothing further to do, so the coroutine - // will simply exit at that time). - CORO_YIELD socket_->async_send_to(buffer(respbuf_->getData(), - respbuf_->getLength()), - *sender_, *this); - } -} - -/// Call the DNS lookup provider. (Expected to be called by the -/// AsyncLookup handler.) -void -UDPServer::asyncLookup() { - (*lookup_callback_)(*io_message_, query_message_, answer_message_, - respbuf_, this); -} - -/// Post this coroutine on the ASIO service queue so that it will -/// resume processing where it left off. The 'done' parameter indicates -/// whether there is an answer to return to the client. -void -UDPServer::resume(const bool done) { - done_ = done; - io_.post(*this); -} - -} diff --git a/src/lib/cache/Makefile.am b/src/lib/cache/Makefile.am new file mode 100644 index 0000000000000000000000000000000000000000..264aca692ac863e95df0a45f5c5e060aa61278c9 --- /dev/null +++ b/src/lib/cache/Makefile.am @@ -0,0 +1,33 @@ +SUBDIRS = . tests + +AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib +AM_CPPFLAGS += $(BOOST_INCLUDES) $(MULTITHREADING_FLAG) +AM_CPPFLAGS += -I$(top_srcdir)/src/lib/dns -I$(top_builddir)/src/lib/dns +AM_CPPFLAGS += -I$(top_srcdir)/src/lib/nsas -I$(top_builddir)/src/lib/nsas +AM_CPPFLAGS += -I$(top_srcdir)/src/lib/cache -I$(top_builddir)/src/lib/cache +AM_CPPFLAGS += $(SQLITE_CFLAGS) +AM_CXXFLAGS = $(B10_CXXFLAGS) + +# Some versions of GCC warn about some versions of Boost regarding +# missing initializer for members in its posix_time. +# https://svn.boost.org/trac/boost/ticket/3477 +# But older GCC compilers don't have the flag. +AM_CXXFLAGS += $(WARNING_NO_MISSING_FIELD_INITIALIZERS_CFLAG) + +if USE_CLANGPP +# clang++ complains about unused function parameters in some boost header +# files. +AM_CXXFLAGS += -Wno-unused-parameter +endif + +lib_LTLIBRARIES = libcache.la +libcache_la_SOURCES = resolver_cache.h resolver_cache.cc +libcache_la_SOURCES += message_cache.h message_cache.cc +libcache_la_SOURCES += message_entry.h message_entry.cc +libcache_la_SOURCES += rrset_cache.h rrset_cache.cc +libcache_la_SOURCES += rrset_entry.h rrset_entry.cc +libcache_la_SOURCES += cache_entry_key.h cache_entry_key.cc +libcache_la_SOURCES += rrset_copy.h rrset_copy.cc +libcache_la_SOURCES += local_zone_data.h local_zone_data.cc + +CLEANFILES = *.gcno *.gcda diff --git a/src/lib/cache/TODO b/src/lib/cache/TODO new file mode 100644 index 0000000000000000000000000000000000000000..a7d2458d0636e0a49c55d63b96ac7c9096e9b5cf --- /dev/null +++ b/src/lib/cache/TODO @@ -0,0 +1,14 @@ +* Revisit the algorithm used by getRRsetTrustLevel() in message_entry.cc. +* Implement dump/load/resize interfaces of rrset/message/recursor cache. +* Once LRU hash table is implemented, it should be used by message/rrset cache. +* Once the hash/lrulist related files in /lib/nsas is moved to seperated + folder, the code of recursor cache has to be updated. +* Set proper AD flags once DNSSEC is supported by the cache. +* When the message or rrset entry has expired, it should be removed + from the cache, or just moved to the head of LRU list, so that it + can removed first. +* Make resolver cache be smart to refetch the messages that are about + to expire. +* When the rrset beging updated is an NS rrset, NSAS should be updated + together. + diff --git a/src/lib/cache/cache_entry_key.cc b/src/lib/cache/cache_entry_key.cc new file mode 100644 index 0000000000000000000000000000000000000000..85c03a0175d723ea3b20a5d4be6e9ac5e31e61da --- /dev/null +++ b/src/lib/cache/cache_entry_key.cc @@ -0,0 +1,42 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include "cache_entry_key.h" + +using namespace std; + +namespace isc { +namespace cache { +const std::string +genCacheEntryName(const isc::dns::Name& name, const isc::dns::RRType& type) { + std::string keystr = name.toText(); + ostringstream stream; + stream << type.getCode(); + keystr += stream.str(); + return (keystr); +} + +const std::string +genCacheEntryName(const std::string& namestr, const uint16_t type) { + std::string keystr = namestr; + ostringstream stream; + stream << type; + keystr += stream.str(); + return (keystr); +} + +} // namespace cache +} // namespace isc + diff --git a/src/lib/cache/cache_entry_key.h b/src/lib/cache/cache_entry_key.h new file mode 100644 index 0000000000000000000000000000000000000000..674deb00f7ceaf5abf9fce611dce8e10d9ccd03b --- /dev/null +++ b/src/lib/cache/cache_entry_key.h @@ -0,0 +1,54 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __CACHE_ENTRY_KEY_H +#define __CACHE_ENTRY_KEY_H + +#include +#include +#include + +namespace isc { +namespace cache { + +/// \brief Entry Name Generation Functions +/// +/// Generate the name for message/rrset entries. +/// +/// Concatenates the string representation of the Name and the +/// string representation of the type number. +/// +/// Note: the returned name is a text string, not wire format. +/// eg. if name is 'example.com.', type is 'A', the return +/// value is 'example.com.1' +/// +/// \param name The Name to create a text entry for +/// \param type The RRType to create a text entry for +/// \return return the entry name. +const std::string +genCacheEntryName(const isc::dns::Name& name, const isc::dns::RRType& type); + +/// +/// \overload +/// +/// \param namestr A string representation of a DNS Name +/// \param type The value of a DNS RRType +const std::string +genCacheEntryName(const std::string& namestr, const uint16_t type); + +} // namespace cache +} // namespace isc + +#endif // __CACHE_ENTRY_KEY_H + diff --git a/src/lib/cache/local_zone_data.cc b/src/lib/cache/local_zone_data.cc new file mode 100644 index 0000000000000000000000000000000000000000..61ce35a1b87cb821ab50d174517dc5eb2db41aea --- /dev/null +++ b/src/lib/cache/local_zone_data.cc @@ -0,0 +1,56 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include "local_zone_data.h" +#include "cache_entry_key.h" +#include "rrset_copy.h" + +using namespace std; +using namespace isc::dns; + +namespace isc { +namespace cache { + +typedef pair RRsetMapPair; +typedef map::iterator RRsetMapIterator; + +isc::dns::RRsetPtr +LocalZoneData::lookup(const isc::dns::Name& name, + const isc::dns::RRType& type) +{ + string key = genCacheEntryName(name, type); + RRsetMapIterator iter = rrsets_map_.find(key); + if (iter == rrsets_map_.end()) { + return (RRsetPtr()); + } else { + return (iter->second); + } +} + +void +LocalZoneData::update(const isc::dns::RRset& rrset) { + //TODO Do we really need to recreate the rrset again? + string key = genCacheEntryName(rrset.getName(), rrset.getType()); + RRset* rrset_copy = new RRset(rrset.getName(), rrset.getClass(), + rrset.getType(), rrset.getTTL()); + + rrsetCopy(rrset, *rrset_copy); + RRsetPtr rrset_ptr(rrset_copy); + rrsets_map_[key] = rrset_ptr; +} + +} // namespace cache +} // namespace isc + diff --git a/src/lib/cache/local_zone_data.h b/src/lib/cache/local_zone_data.h new file mode 100644 index 0000000000000000000000000000000000000000..3015847c35c91e083b429de26b58fd4922ff5906 --- /dev/null +++ b/src/lib/cache/local_zone_data.h @@ -0,0 +1,64 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef _LOCAL_ZONE_DATA +#define _LOCAL_ZONE_DATA + +#include +#include +#include +#include + +namespace isc { +namespace cache { + +/// \brief Local Zone Data +/// The object of LocalZoneData represents the data of one +/// local zone. It provides the interface for lookup the rrsets +/// in the zone. +class LocalZoneData { +public: + LocalZoneData(uint16_t rrset_class) : class_(rrset_class) + {} + + /// \brief Look up one rrset. + /// + /// \param qname The query name to look up + /// \param qtype The query type to look up + /// \return return the shared_ptr of rrset if it is + /// found in the local zone, or else, return NULL. + isc::dns::RRsetPtr lookup(const isc::dns::Name& qname, + const isc::dns::RRType& qtype); + + /// \brief Update the rrset in the local zone. + /// + /// If the rrset doesn't exist, it will be added. + /// Otherwise, the existed one will be overwritten. + /// + /// \param rrset The rrset to update + void update(const isc::dns::RRset& rrset); + +private: + std::map rrsets_map_; // RRsets of the zone + uint16_t class_; // The class of the zone +}; + +typedef boost::shared_ptr LocalZoneDataPtr; +typedef boost::shared_ptr ConstLocalZoneDataPtr; + +} // namespace cache +} // namespace isc + +#endif // _LOCAL_ZONE_DATA + diff --git a/src/lib/cache/message_cache.cc b/src/lib/cache/message_cache.cc new file mode 100644 index 0000000000000000000000000000000000000000..3f2c37ab02e5f6b91439dbb481b98ec9d7fc2d08 --- /dev/null +++ b/src/lib/cache/message_cache.cc @@ -0,0 +1,96 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include + +#include +#include +#include +#include "message_cache.h" +#include "cache_entry_key.h" + +using namespace isc::nsas; +using namespace isc::dns; +using namespace std; + +namespace isc { +namespace cache { + +MessageCache::MessageCache(boost::shared_ptr rrset_cache, + uint32_t cache_size, uint16_t message_class): + message_class_(message_class), + rrset_cache_(rrset_cache), + message_table_(new NsasEntryCompare, cache_size), + message_lru_((3 * cache_size), + new HashDeleter(message_table_)) +{ +} + +bool +MessageCache::lookup(const isc::dns::Name& qname, + const isc::dns::RRType& qtype, + isc::dns::Message& response) +{ + std::string entry_name = genCacheEntryName(qname, qtype); + HashKey entry_key = HashKey(entry_name, RRClass(message_class_)); + MessageEntryPtr msg_entry = message_table_.get(entry_key); + if(msg_entry) { + message_lru_.touch(msg_entry); + return (msg_entry->genMessage(time(NULL), response)); + } + + return (false); +} + +bool +MessageCache::update(const Message& msg) { + QuestionIterator iter = msg.beginQuestion(); + std::string entry_name = genCacheEntryName((*iter)->getName(), (*iter)->getType()); + std::cout << msg.toText(); + HashKey entry_key = HashKey(entry_name, RRClass(message_class_)); + + // The simplest way to update is removing the old message entry directly. + // We have find the existed message entry, since we need to delete it + // from lru list too. + // TODO, but there should be a better way, since we here have to remove and + // add the message entry, maybe there is one way to touch it once. + MessageEntryPtr old_msg_entry = message_table_.get(entry_key); + if (old_msg_entry) { + message_lru_.remove(old_msg_entry); + } + + MessageEntryPtr msg_entry(new MessageEntry(msg, rrset_cache_)); + message_lru_.add(msg_entry); + return (message_table_.add(msg_entry, entry_key, true)); +} + +void +MessageCache::dump(const std::string&) { + //TODO +} + +void +MessageCache::load(const std::string&) { + //TODO +} + +bool +MessageCache::resize(uint32_t) { + //TODO + return (true); +} + +} // namespace cache +} // namespace isc + diff --git a/src/lib/cache/message_cache.h b/src/lib/cache/message_cache.h new file mode 100644 index 0000000000000000000000000000000000000000..3a684c85e9809aa8648c7542fa08affc487d2f5b --- /dev/null +++ b/src/lib/cache/message_cache.h @@ -0,0 +1,93 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __MESSAGE_CACHE_H +#define __MESSAGE_CACHE_H + +#include +#include +#include +#include "message_entry.h" +#include +#include + +namespace isc { +namespace cache { + +class RRsetCache; + +/// \brief Message Cache +/// The object of MessageCache represents the cache for class-specific +/// messages. +/// +class MessageCache { +// Noncopyable +private: + MessageCache(const MessageCache& source); + MessageCache& operator=(const MessageCache& source); +public: + /// \param cache_size The size of message cache. + MessageCache(boost::shared_ptr rrset_cache_, + uint32_t cache_size, uint16_t message_class); + + /// \brief Look up message in cache. + /// \param message generated response message if the message entry + /// can be found. + /// + /// \return return true if the message can be found in cache, or else, + /// return false. + //TODO Maybe some user just want to get the message_entry. + bool lookup(const isc::dns::Name& qname, + const isc::dns::RRType& qtype, + isc::dns::Message& message); + + /// \brief Update the message in the cache with the new one. + /// If the message doesn't exist in the cache, it will be added + /// directly. + bool update(const isc::dns::Message& msg); + + /// \brief Dump the message cache to specified file. + /// \todo It should can be dumped to one configured database. + void dump(const std::string& file_name); + + /// \brief Load the cache from one file. + /// \todo It should can be loaded from one configured database. + void load(const std::string& file_name); + + /// \brief Resize the size of message cache in runtime. + bool resize(uint32_t size); + +protected: + /// \brief Get the hash key for the message entry in the cache. + /// \param name query name of the message. + /// \param type query type of the message. + /// \return return the hash key. + HashKey getEntryHashKey(const isc::dns::Name& name, + const isc::dns::RRType& type) const; + + // Make these variants be protected for easy unittest. +protected: + uint16_t message_class_; // The class of the message cache. + boost::shared_ptr rrset_cache_; + isc::nsas::HashTable message_table_; + isc::nsas::LruList message_lru_; +}; + +typedef boost::shared_ptr MessageCachePtr; + +} // namespace cache +} // namespace isc + +#endif // __MESSAGE_CACHE_H + diff --git a/src/lib/cache/message_entry.cc b/src/lib/cache/message_entry.cc new file mode 100644 index 0000000000000000000000000000000000000000..d4de11f45a07e9ac73ba213e6360d291ec2625bb --- /dev/null +++ b/src/lib/cache/message_entry.cc @@ -0,0 +1,249 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include + +#include +#include +#include +#include "message_entry.h" +#include "rrset_cache.h" + +using namespace isc::dns; +using namespace std; + +namespace isc { +namespace cache { + +static uint32_t MAX_UINT32 = numeric_limits::max(); + +MessageEntry::MessageEntry(const isc::dns::Message& msg, + boost::shared_ptr rrset_cache): + rrset_cache_(rrset_cache), + headerflag_aa_(false), + headerflag_tc_(false) +{ + initMessageEntry(msg); + entry_name_ = genCacheEntryName(query_name_, query_type_); + hash_key_ptr_ = new HashKey(entry_name_, RRClass(query_class_)); +} + +bool +MessageEntry::getRRsetEntries(vector& rrset_entry_vec, + const time_t time_now) +{ + uint16_t entry_count = answer_count_ + authority_count_ + additional_count_; + rrset_entry_vec.reserve(rrset_entry_vec.size() + entry_count); + for (int index = 0; index < entry_count; ++index) { + RRsetEntryPtr rrset_entry = rrset_cache_->lookup(rrsets_[index].name_, + rrsets_[index].type_); + if (time_now < rrset_entry->getExpireTime()) { + rrset_entry_vec.push_back(rrset_entry); + } else { + return (false); + } + } + + return (true); +} + +void +MessageEntry::addRRset(isc::dns::Message& message, + const vector& rrset_entry_vec, + const isc::dns::Message::Section& section, + bool dnssec_need) +{ + uint16_t start_index = 0; + uint16_t end_index = answer_count_; + assert(section != Message::SECTION_QUESTION); + + if (section == Message::SECTION_AUTHORITY) { + start_index = answer_count_; + end_index = answer_count_ + authority_count_; + } else if (section == Message::SECTION_ADDITIONAL) { + start_index = answer_count_ + authority_count_; + end_index = start_index + additional_count_; + } + + for(uint16_t index = start_index; index < end_index; ++index) { + message.addRRset(section, rrset_entry_vec[index]->getRRset(), dnssec_need); + } +} + +bool +MessageEntry::genMessage(const time_t& time_now, + isc::dns::Message& msg) +{ + if (time_now >= expire_time_) { + // The message entry has expired. + return (false); + } else { + // Before do any generation, we should check if some rrset + // has expired, if it is, return false. + vector rrset_entry_vec; + if (false == getRRsetEntries(rrset_entry_vec, time_now)) { + return (false); + } + + // Begin message generation. We don't need to add question + // section, since it has been included in the message. + // Set cached header flags. + msg.setHeaderFlag(Message::HEADERFLAG_AA, headerflag_aa_); + msg.setHeaderFlag(Message::HEADERFLAG_TC, headerflag_tc_); + + bool dnssec_need = msg.getEDNS().get(); + addRRset(msg, rrset_entry_vec, Message::SECTION_ANSWER, dnssec_need); + addRRset(msg, rrset_entry_vec, Message::SECTION_AUTHORITY, dnssec_need); + addRRset(msg, rrset_entry_vec, Message::SECTION_ADDITIONAL, dnssec_need); + + return (true); + } +} + +RRsetTrustLevel +MessageEntry::getRRsetTrustLevel(const Message& message, + const isc::dns::RRsetPtr& rrset, + const isc::dns::Message::Section& section) +{ + bool aa = message.getHeaderFlag(Message::HEADERFLAG_AA); + switch(section) { + case Message::SECTION_ANSWER: { + if (aa) { + RRsetIterator rrset_iter = message.beginSection(section); + + // Make sure we are inspecting the right RRset + while((*rrset_iter)->getName() != rrset->getName() && + (*rrset_iter)->getType() != rrset->getType() && + rrset_iter != message.endSection(section)) { + ++rrset_iter; + } + assert(rrset_iter != message.endSection(section)); + + // According RFC2181 section 5.4.1, only the record + // describing that ailas is necessarily authoritative. + // If there is one or more CNAME records in answer section. + // CNAME records is assumed as the first rrset. + if ((*rrset_iter)->getType() == RRType::CNAME()) { + // TODO: real equals for RRsets? + if ((*rrset_iter).get() == rrset.get()) { + return (RRSET_TRUST_ANSWER_AA); + } else { + return (RRSET_TRUST_ANSWER_NONAA); + } + } + + // Here, if the first rrset is DNAME, then assume the + // second rrset is synchronized CNAME record, except + // these two records, any other records in answer section + // should be treated as non-authoritative. + // TODO, this part logic should be revisited later, + // since it's not mentioned by RFC2181. + if ((*rrset_iter)->getType() == RRType::DNAME()) { + // TODO: real equals for RRsets? + if ((*rrset_iter).get() == rrset.get() || + ((++rrset_iter) != message.endSection(section) && + (*rrset_iter).get() == rrset.get())) { + return (RRSET_TRUST_ANSWER_AA); + } else { + return (RRSET_TRUST_ANSWER_NONAA); + } + } + + return (RRSET_TRUST_ANSWER_AA); + + } else { + return (RRSET_TRUST_ANSWER_NONAA); + } + break; + } + + case Message::SECTION_AUTHORITY: { + if (aa) { + return (RRSET_TRUST_AUTHORITY_AA); + } else { + return (RRSET_TRUST_AUTHORITY_NONAA); + } + break; + } + + case Message::SECTION_ADDITIONAL: { + if (aa) { + return (RRSET_TRUST_ADDITIONAL_AA); + } else { + return (RRSET_TRUST_ADDITIONAL_NONAA); + } + break; + } + + default: + return (RRSET_TRUST_DEFAULT); + } +} + +void +MessageEntry::parseSection(const isc::dns::Message& msg, + const Message::Section& section, + uint32_t& smaller_ttl, + uint16_t& rrset_count) +{ + RRsetIterator iter; + int count = 0; + for (iter = msg.beginSection(section); + iter != msg.endSection(section); + ++iter) { + // Add the rrset entry to rrset_cache or update the existed + // rrset entry if the new one is more authoritative. + //TODO set proper rrset trust level. + RRsetPtr rrset_ptr = *iter; + RRsetTrustLevel level = getRRsetTrustLevel(msg, rrset_ptr, section); + RRsetEntryPtr rrset_entry = rrset_cache_->update(*rrset_ptr, level); + rrsets_.push_back(RRsetRef(rrset_ptr->getName(), rrset_ptr->getType())); + + uint32_t rrset_ttl = rrset_entry->getTTL(); + if (smaller_ttl > rrset_ttl) { + smaller_ttl = rrset_ttl; + } + + count++; + } + + rrset_count = count; +} + +void +MessageEntry::initMessageEntry(const isc::dns::Message& msg) { + //TODO better way to cache the header flags? + headerflag_aa_ = msg.getHeaderFlag(Message::HEADERFLAG_AA); + headerflag_tc_ = msg.getHeaderFlag(Message::HEADERFLAG_TC); + + // We only cache the first question in question section. + // TODO, do we need to support muptiple questions? + query_count_ = 1; + QuestionIterator iter = msg.beginQuestion(); + query_name_ = (*iter)->getName().toText(); + query_type_ = (*iter)->getType().getCode(); + query_class_ = (*iter)->getClass().getCode(); + + uint32_t min_ttl = MAX_UINT32; + parseSection(msg, Message::SECTION_ANSWER, min_ttl, answer_count_); + parseSection(msg, Message::SECTION_AUTHORITY, min_ttl, authority_count_); + parseSection(msg, Message::SECTION_ADDITIONAL, min_ttl, additional_count_); + + expire_time_ = time(NULL) + min_ttl; +} + +} // namespace cache +} // namespace isc + + diff --git a/src/lib/cache/message_entry.h b/src/lib/cache/message_entry.h new file mode 100644 index 0000000000000000000000000000000000000000..682c1710dfaf1676658b1f04abf593e405d9e004 --- /dev/null +++ b/src/lib/cache/message_entry.h @@ -0,0 +1,178 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __MESSAGE_ENTRY_H +#define __MESSAGE_ENTRY_H + +#include +#include +#include +#include +#include "rrset_entry.h" + + +using namespace isc::nsas; + +namespace isc { +namespace cache { + +class RRsetEntry; +class RRsetCache; + +/// \brief Information to refer an RRset. +/// +/// There is no class information here, since the rrsets are cached in +/// the class-specific rrset cache. +struct RRsetRef{ + /// \brief Constructor + /// + /// \param name The Name for the RRset + /// \param type the RRType for the RRrset + RRsetRef(const isc::dns::Name& name, const isc::dns::RRType& type): + name_(name), type_(type) + {} + + isc::dns::Name name_; // Name of rrset. + isc::dns::RRType type_; // Type of rrset. +}; + +/// \brief Message Entry +/// +/// The object of MessageEntry represents one response message +/// answered to the resolver client. +class MessageEntry : public NsasEntry { +// Noncopyable +private: + MessageEntry(const MessageEntry& source); + MessageEntry& operator=(const MessageEntry& source); +public: + + /// \brief Initialize the message entry object with one dns + /// message. + /// \param message The message used to initialize MessageEntry. + /// \param rrset_cache the pointer of RRsetCache. When one message + /// entry is created, rrset cache needs to be updated, + /// since some new rrset entries may be inserted into + /// rrset cache, or the existed rrset entries need + /// to be updated. + MessageEntry(const isc::dns::Message& message, + boost::shared_ptr rrset_cache); + + /// \brief generate one dns message according + /// the rrsets information of the message. + /// + /// \param time_now set the ttl of each rrset in the message + /// as "expire_time - time_now" (expire_time is the + /// expiration time of the rrset). + /// \param response generated dns message. + /// \return return true if the response message can be generated + /// from the cached information, or else, return false. + bool genMessage(const time_t& time_now, isc::dns::Message& response); + + /// \brief Get the hash key of the message entry. + /// + /// \return return hash key + virtual HashKey hashKey() const { + return (*hash_key_ptr_); + } + + /// \short Protected memebers, so they can be accessed by tests. + //@{ +protected: + /// \brief Initialize the message entry with dns message. + /// + /// \param message The Message to initialize the entry with + void initMessageEntry(const isc::dns::Message& message); + + /// \brief Parse the rrsets in specified section. + /// + /// \param msg The message to parse the RRsets from + /// \param section The Section to parse the RRsets from + /// \param smaller_ttl Get the smallest ttl of rrsets in + /// specified section, if it's smaller than the given value. + /// \param rrset_count the rrset count of the section. + /// (TODO for Message, getRRsetCount() should be one + /// interface provided by Message.) + void parseSection(const isc::dns::Message& msg, + const isc::dns::Message::Section& section, + uint32_t& smaller_ttl, + uint16_t& rrset_count); + + /// \brief Get RRset Trustworthiness + /// The algorithm refers to RFC2181 section 5.4.1 + /// Only the rrset can be updated by the rrsets + /// with higher trust level. + /// + /// \param message Message that the rrset belongs to + /// \param rrset specified rrset which needs to get its + /// trust worthiness + /// \param section Section of the rrset + /// \return return rrset trust level. + RRsetTrustLevel getRRsetTrustLevel(const isc::dns::Message& message, + const isc::dns::RRsetPtr& rrset, + const isc::dns::Message::Section& section); + + /// \brief Add rrset to one section of message. + /// + /// \param message The message to add rrsets to. + /// \param rrset_entry_vec vector for rrset entries in + /// different sections. + /// \param section The section to add to + /// \param dnssec_need need dnssec records or not. + void addRRset(isc::dns::Message& message, + const std::vector& rrset_entry_vec, + const isc::dns::Message::Section& section, + bool dnssec_need); + + /// \brief Get the all the rrset entries for the message entry. + /// + /// \param rrset_entry_vec vector to add unexpired rrset entries to + /// \param time_now the time of now. Used to compare with rrset + /// entry's expire time. + /// \return return false if any rrset entry has expired, true + /// otherwise. + bool getRRsetEntries(std::vector& rrset_entry_vec, + const time_t time_now); + + time_t expire_time_; // Expiration time of the message. + //@} + +private: + std::string entry_name_; // The name for this entry(name + type) + HashKey* hash_key_ptr_; // the key for messag entry in hash table. + + std::vector rrsets_; + boost::shared_ptr rrset_cache_; + + std::string query_name_; // query name of the message. + uint16_t query_class_; // query class of the message. + uint16_t query_type_; // query type of message. + + uint16_t query_count_; // query count in query section. + uint16_t answer_count_; // rrset count in answer section. + uint16_t authority_count_; // rrset count in authority section. + uint16_t additional_count_; // rrset count in addition section. + + //TODO, there should be a better way to cache these header flags + bool headerflag_aa_; // Whether AA bit is set. + bool headerflag_tc_; // Whether TC bit is set. +}; + +typedef boost::shared_ptr MessageEntryPtr; + +} // namespace cache +} // namespace isc + +#endif // __MESSAGE_ENTRY_H + diff --git a/src/lib/cache/resolver_cache.cc b/src/lib/cache/resolver_cache.cc new file mode 100644 index 0000000000000000000000000000000000000000..0734da8d389fe4088aca1c54c69f95adcea1bea3 --- /dev/null +++ b/src/lib/cache/resolver_cache.cc @@ -0,0 +1,243 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include + +#include "resolver_cache.h" +#include "dns/message.h" +#include "rrset_cache.h" +#include +#include + +using namespace isc::dns; +using namespace std; + +namespace isc { +namespace cache { + +ResolverClassCache::ResolverClassCache(const RRClass& cache_class) : + cache_class_(cache_class) +{ + local_zone_data_ = LocalZoneDataPtr(new LocalZoneData(cache_class_.getCode())); + rrsets_cache_ = RRsetCachePtr(new RRsetCache(RRSET_CACHE_DEFAULT_SIZE, + cache_class_.getCode())); + messages_cache_ = MessageCachePtr(new MessageCache(rrsets_cache_, + MESSAGE_CACHE_DEFAULT_SIZE, + cache_class_.getCode())); +} + +ResolverClassCache::ResolverClassCache(CacheSizeInfo cache_info) : + cache_class_(cache_info.cclass) +{ + uint16_t klass = cache_class_.getCode(); + // TODO We should find one way to load local zone data. + local_zone_data_ = LocalZoneDataPtr(new LocalZoneData(klass)); + rrsets_cache_ = RRsetCachePtr(new + RRsetCache(cache_info.rrset_cache_size, klass)); + messages_cache_ = MessageCachePtr(new MessageCache(rrsets_cache_, + cache_info.message_cache_size, + klass)); +} + +const RRClass& +ResolverClassCache::getClass() const { + return cache_class_; +} + +bool +ResolverClassCache::lookup(const isc::dns::Name& qname, + const isc::dns::RRType& qtype, + isc::dns::Message& response) const +{ + // message response should has question section already. + if (response.beginQuestion() == response.endQuestion()) { + isc_throw(MessageNoQuestionSection, "Message has no question section"); + } + + // First, query in local zone, if the rrset(qname, qtype, qclass) can be + // found in local zone, generated reply message with only the rrset in + // answer section. + RRsetPtr rrset_ptr = local_zone_data_->lookup(qname, qtype); + if (rrset_ptr) { + response.addRRset(Message::SECTION_ANSWER, rrset_ptr); + return (true); + } + + // Search in class-specific message cache. + return (messages_cache_->lookup(qname, qtype, response)); +} + +isc::dns::RRsetPtr +ResolverClassCache::lookup(const isc::dns::Name& qname, + const isc::dns::RRType& qtype) const +{ + // Algorithm: + // 1. Search in local zone data first, + // 2. Then do search in rrsets_cache_. + RRsetPtr rrset_ptr = local_zone_data_->lookup(qname, qtype); + if (rrset_ptr) { + return (rrset_ptr); + } else { + RRsetEntryPtr rrset_entry = rrsets_cache_->lookup(qname, qtype); + if (rrset_entry) { + return (rrset_entry->getRRset()); + } else { + return (RRsetPtr()); + } + } +} + +bool +ResolverClassCache::update(const isc::dns::Message& msg) { + return (messages_cache_->update(msg)); +} + +bool +ResolverClassCache::updateRRsetCache(const isc::dns::ConstRRsetPtr rrset_ptr, + RRsetCachePtr rrset_cache_ptr) +{ + RRsetTrustLevel level; + if (rrset_ptr->getType() == RRType::A() || + rrset_ptr->getType() == RRType::AAAA()) { + level = RRSET_TRUST_PRIM_GLUE; + } else { + level = RRSET_TRUST_PRIM_ZONE_NONGLUE; + } + + rrset_cache_ptr->update((*rrset_ptr.get()), level); + return (true); +} + +bool +ResolverClassCache::update(const isc::dns::ConstRRsetPtr rrset_ptr) { + // First update local zone, then update rrset cache. + local_zone_data_->update((*rrset_ptr.get())); + updateRRsetCache(rrset_ptr, rrsets_cache_); + return (true); +} + + +ResolverCache::ResolverCache() +{ + class_caches_.push_back(new ResolverClassCache(RRClass::IN())); +} + +ResolverCache::ResolverCache(std::vector caches_info) +{ + for (int i = 0; i < caches_info.size(); ++i) { + class_caches_.push_back(new ResolverClassCache(caches_info[i])); + } +} + +ResolverCache::~ResolverCache() +{ + for (int i = 0; i < class_caches_.size(); ++i) { + delete class_caches_[i]; + } +} + +bool +ResolverCache::lookup(const isc::dns::Name& qname, + const isc::dns::RRType& qtype, + const isc::dns::RRClass& qclass, + isc::dns::Message& response) const +{ + ResolverClassCache* cc = getClassCache(qclass); + if (cc) { + return (cc->lookup(qname, qtype, response)); + } else { + return (false); + } +} + +isc::dns::RRsetPtr +ResolverCache::lookup(const isc::dns::Name& qname, + const isc::dns::RRType& qtype, + const isc::dns::RRClass& qclass) const +{ + ResolverClassCache* cc = getClassCache(qclass); + if (cc) { + return (cc->lookup(qname, qtype)); + } else { + return (RRsetPtr()); + } +} + +isc::dns::RRsetPtr +ResolverCache::lookupDeepestNS(const isc::dns::Name& qname, + const isc::dns::RRClass& qclass) const +{ + isc::dns::RRType qtype = RRType::NS(); + ResolverClassCache* cc = getClassCache(qclass); + if (cc) { + unsigned int count = qname.getLabelCount(); + unsigned int level = 0; + while(level < count) { + Name close_name = qname.split(level); + RRsetPtr rrset_ptr = cc->lookup(close_name, qtype); + if (rrset_ptr) { + return (rrset_ptr); + } else { + ++level; + } + } + } + + return (RRsetPtr()); +} + +bool +ResolverCache::update(const isc::dns::Message& msg) { + QuestionIterator iter = msg.beginQuestion(); + ResolverClassCache* cc = getClassCache((*iter)->getClass()); + if (cc) { + return (cc->update(msg)); + } else { + return (false); + } +} + +bool +ResolverCache::update(const isc::dns::ConstRRsetPtr rrset_ptr) { + ResolverClassCache* cc = getClassCache(rrset_ptr->getClass()); + if (cc) { + return (cc->update(rrset_ptr)); + } else { + return (false); + } +} + +void +ResolverCache::dump(const std::string&) { + //TODO +} + +void +ResolverCache::load(const std::string&) { + //TODO +} + +ResolverClassCache* +ResolverCache::getClassCache(const isc::dns::RRClass& cache_class) const { + for (int i = 0; i < class_caches_.size(); ++i) { + if (class_caches_[i]->getClass() == cache_class) { + return class_caches_[i]; + } + } + return NULL; +} + +} // namespace cache +} // namespace isc + diff --git a/src/lib/cache/resolver_cache.h b/src/lib/cache/resolver_cache.h new file mode 100644 index 0000000000000000000000000000000000000000..a8149e428c60922270fc377e11091d3a3190020c --- /dev/null +++ b/src/lib/cache/resolver_cache.h @@ -0,0 +1,328 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __RESOLVER_CACHE_H +#define __RESOLVER_CACHE_H + +#include +#include +#include +#include +#include +#include +#include "message_cache.h" +#include "rrset_cache.h" +#include "local_zone_data.h" + +namespace isc { +namespace cache { +class RRsetCache; + +//TODO a better proper default cache size +#define MESSAGE_CACHE_DEFAULT_SIZE 10000 +#define RRSET_CACHE_DEFAULT_SIZE 20000 + +/// \brief Cache Size Information. +/// +/// Used to initialize the size of class-specific rrset/message cache. +struct CacheSizeInfo +{ +public: + /// \brief Constructor + /// + /// \param cls The RRClass code + /// \param msg_cache_size The size for the message cache + /// \param rst_cache_size The size for the RRset cache + CacheSizeInfo(const isc::dns::RRClass& cls, + uint32_t msg_cache_size, + uint32_t rst_cache_size): + cclass(cls), + message_cache_size(msg_cache_size), + rrset_cache_size(rst_cache_size) + {} + + isc::dns::RRClass cclass; // class of the cache. + uint32_t message_cache_size; // the size for message cache. + uint32_t rrset_cache_size; // The size for rrset cache. +}; + +/// \brief Message has no question section. +/// +/// Thrown if the given message has no question section when looking up +/// the message in cache. +class MessageNoQuestionSection : public isc::Exception { +public: + MessageNoQuestionSection(const char*file, size_t line, const char*what) : + isc::Exception(file, line, what) + {} +}; + +/// \brief Class-specific Resolver Cache. +/// +/// The object of ResolverCache represents the cache of the resolver. It may hold +/// a list of message/rrset cache which are in different class. +/// +/// \note Public interaction with the cache should be through ResolverCache, +/// not directly with this one. (TODO: make this private/hidden/local to the .cc?) +class ResolverClassCache { +public: + /// \brief Default Constructor. + /// + /// Only support for class "IN", and message cache size is + /// MESSAGE_CACHE_DEFAULT_SIZE, rrset cache size is + /// RRSET_CACHE_DEFAULT_SIZE + ResolverClassCache(const isc::dns::RRClass& cache_class); + + /// \brief Construct Function. + /// \param caches_size cache size information for each + /// messages/rrsets of different classes. + ResolverClassCache(CacheSizeInfo cache_info); + + /// \name Lookup Interfaces + //@{ + /// \brief Look up message in cache. + /// + /// \param qname The query name to look up + /// \param qtype The query type to look up + /// \param response the query message (must be in RENDER mode) + /// which has question section already (exception + /// MessageNoQeustionSection will be thrown if it has + /// no question section). If the message can be found + /// in cache, rrsets for the message will be added to + /// different sections(answer, authority, additional). + /// \return return true if the message can be found, or else, + /// return false. + bool lookup(const isc::dns::Name& qname, + const isc::dns::RRType& qtype, + isc::dns::Message& response) const; + + /// \brief Look up rrset in cache. + /// + /// \param qname The query name to look up + /// \param qtype The query type to look up + /// + /// \return return the shared_ptr of rrset if it can be found, + /// or else, return NULL. When looking up, local zone + /// data will be searched first, if not found, then + /// search in rrset cache. + /// + /// \overload + /// + isc::dns::RRsetPtr lookup(const isc::dns::Name& qname, + const isc::dns::RRType& qtype) const; + + /// \brief Update the message in the cache with the new one. + /// + /// \param msg The message to update + /// + /// \return return true if the message is updated successfully, + /// or else, return false. + /// + /// \note the function doesn't do any message validation check, + /// the user should make sure the message is valid, and of + /// the right class + bool update(const isc::dns::Message& msg); + + /// \brief Update the rrset in the cache with the new one. + /// + /// local zone data and rrset cache will be updated together. + /// If the rrset doesn't exist in both of them, then the rrset + /// will be added into both of them. + /// + /// \param rrset_ptr The RRset to update + /// + /// \return return false, if the class of the parameter rrset is + /// allowed to be cached. + /// + /// \overload + /// + /// \note The class of the RRset must have been checked. It is not + /// here. + bool update(const isc::dns::ConstRRsetPtr rrset_ptr); + + /// \brief Get the RRClass this cache is for + /// + /// \return The RRClass of this cache + const isc::dns::RRClass& getClass() const; + +private: + /// \brief Update rrset cache. + /// + /// \param rrset_ptr The rrset to update with + /// \param rrset_cache_ptr the rrset cache to update + /// + /// \return return true if the rrset is updated in the rrset cache, + /// or else return false if failed. + /// \param rrset_cache_ptr The rrset cache need to be updated. + bool updateRRsetCache(const isc::dns::ConstRRsetPtr rrset_ptr, + RRsetCachePtr rrset_cache_ptr); + + /// \brief Class this cache is for. + const isc::dns::RRClass cache_class_; + + /// \brief map of message caches for configured classes(each message + /// cache is class-specific) + MessageCachePtr messages_cache_; + + /// \name rrset caches + //@{ + /// \brief Local Zone data cache + /// Cache for rrsets in local zones, rrsets + /// in it never expire. + LocalZoneDataPtr local_zone_data_; + + /// \brief cache the rrsets parsed from the received message. + RRsetCachePtr rrsets_cache_; + //@} +}; + +class ResolverCache { +public: + /// \brief Default Constructor. + /// + /// Right now, only support for class "IN", and message cache size is + /// MESSAGE_CACHE_DEFAULT_SIZE, rrset cache size is + /// RRSET_CACHE_DEFAULT_SIZE + ResolverCache(); + + /// \brief Construct Function. + /// \param caches_size cache size information for each + /// messages/rrsets of different classes. + ResolverCache(std::vector caches_size); + + /// \brief Destructor + ~ResolverCache(); + + /// \name Lookup Interfaces + //@{ + /// \brief Look up message in cache. + /// + /// \param qname The query name to look up + /// \param qtype The query type to look up + /// \param qclass The query class to look up + /// \param response the query message (must be in RENDER mode) + /// which has question section already (exception + /// MessageNoQeustionSection will be thrown if it has + /// no question section). If the message can be found + /// in cache, rrsets for the message will be added to + /// different sections(answer, authority, additional). + /// \return return true if the message can be found, or else, + /// return false. + bool lookup(const isc::dns::Name& qname, + const isc::dns::RRType& qtype, + const isc::dns::RRClass& qclass, + isc::dns::Message& response) const; + + /// \brief Look up rrset in cache. + /// + /// \param qname The query name to look up + /// \param qtype The query type to look up + /// \param qclass The query class to look up + /// + /// \return return the shared_ptr of rrset if it can be found, + /// or else, return NULL. When looking up, local zone + /// data will be searched first, if not found, then + /// search in rrset cache. + /// + /// \overload + /// + isc::dns::RRsetPtr lookup(const isc::dns::Name& qname, + const isc::dns::RRType& qtype, + const isc::dns::RRClass& qclass) const; + + /// \brief Look up closest enclosing NS rrset in cache. + /// + /// \param qname The query name to look up + /// \param qclass The query class to look up + /// + /// \return return the shared_ptr of closest enclosing ns rrset + /// if it can be found in cache, or else return NULL. + /// + /// Currently the implementation is: search exact ns rrset + /// label by lable, If the ns rrset can't be found, remove the last + /// label, then search again. The efficiency may be very low when + /// the name is very long but it's closest rrset's name is very short. + /// + /// If a good perfermance is needed when looking up the closest + /// enclosing ns rrset, cache structure(HashTable) should be + /// redesigned. By using HashTable, it can only garantee the + /// performance for looking up exact rrset. + /// + /// So here there is another question, which rrset looking up interface + /// is used frequently? Exact or closest enclosing ns looking up. + isc::dns::RRsetPtr lookupDeepestNS(const isc::dns::Name& qname, + const isc::dns::RRClass& qclass) const; + //@} + + /// \brief Update the message in the cache with the new one. + /// + /// \param msg The message to update + /// + /// \return return true if the message is updated successfully, + /// or else, return false. + /// + /// \note the function doesn't do any message validation check, + /// the user should make sure the message is valid. + bool update(const isc::dns::Message& msg); + + /// \brief Update the rrset in the cache with the new one. + /// + /// local zone data and rrset cache will be updated together. + /// If the rrset doesn't exist in both of them, then the rrset + /// will be added into both of them. + /// + /// \param rrset_ptr The RRset to update + /// + /// \return return false, if the class of the parameter rrset is + /// allowed to be cached. + /// + /// \overload + /// + bool update(const isc::dns::ConstRRsetPtr rrset_ptr); + + /// \name Cache Serialization + //@{ + /// \brief Dump the cache content to one file. + /// + /// \param file_name file to write to + /// + /// \todo It should can be dumped to one configured database. + void dump(const std::string& file_name); + + /// \brief Load the cache from one file. + /// + /// \param file to load from + /// + /// \todo It should can be loaded from one configured database. + void load(const std::string& file_name); + //@} + +private: + /// \brief Returns the class-specific subcache + /// + /// \param cache_class the class to get the subcache for + /// \return The subcache, or NULL if there is no cache for this class + ResolverClassCache* getClassCache(const isc::dns::RRClass& cache_class) const; + + /// The class-specific caches. + /// TODO: I think we can optimize for IN, and always have that + /// one directly available, use the vector for the rest? + std::vector class_caches_; +}; + +} // namespace cache +} // namespace isc + +#endif // __RESOLVER_CACHE_H + diff --git a/src/lib/cache/rrset_cache.cc b/src/lib/cache/rrset_cache.cc new file mode 100644 index 0000000000000000000000000000000000000000..0a2957c6362d3171cbf12e66c8f8cc4beadc0a94 --- /dev/null +++ b/src/lib/cache/rrset_cache.cc @@ -0,0 +1,104 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include + +#include +#include "rrset_cache.h" +#include +#include +#include + +using namespace isc::nsas; +using namespace isc::dns; +using namespace std; + +namespace isc { +namespace cache { + +RRsetCache::RRsetCache(uint32_t cache_size, + uint16_t rrset_class): + class_(rrset_class), + rrset_table_(new NsasEntryCompare, cache_size), + rrset_lru_((3 * cache_size), + new HashDeleter(rrset_table_)) +{ +} + +RRsetEntryPtr +RRsetCache::lookup(const isc::dns::Name& qname, + const isc::dns::RRType& qtype) +{ + const string entry_name = genCacheEntryName(qname, qtype); + RRsetEntryPtr entry_ptr = rrset_table_.get(HashKey(entry_name, RRClass(class_))); + + //If the rrset entry has expired, return NULL. + if(entry_ptr && (time(NULL) > entry_ptr->getExpireTime())) { + return (RRsetEntryPtr()); + } + return (entry_ptr); +} + +RRsetEntryPtr +RRsetCache::update(const isc::dns::RRset& rrset, const RRsetTrustLevel& level) { + // TODO: If the RRset is an NS, we should update the NSAS as well + + // lookup first + RRsetEntryPtr entry_ptr = lookup(rrset.getName(), rrset.getType()); + if(!entry_ptr) { + // rrset entry doesn't exist, create one rrset entry for the rrset + // and add it directly. + entry_ptr.reset(new RRsetEntry(rrset, level)); + // Replace the expired rrset entry if it exists. + rrset_table_.add(entry_ptr, entry_ptr->hashKey(), true); + //TODO , lru list touch. + return (entry_ptr); + } else { + // there is one rrset entry in the cache, need to check whether + // the new rrset is more authoritative. + if (entry_ptr->getTrustLevel() > level) { + // existed rrset entry is more authoritative, do nothing, + // just return it. + //TODO, lru list touch + return (entry_ptr); + } else { + HashKey key = entry_ptr->hashKey(); + entry_ptr.reset(new RRsetEntry(rrset, level)); + //TODO, lru list touch. + // Replace the expired rrset entry if it exists. + rrset_table_.add(entry_ptr, entry_ptr->hashKey(), true); + return (entry_ptr); + } + } +} + +void +RRsetCache::dump(const std::string&) { + //TODO +} + +void +RRsetCache::load(const std::string&) { + //TODO +} + +bool +RRsetCache::resize(uint32_t) { + //TODO + return (true); +} + +} // namespace cache +} // namespace isc + diff --git a/src/lib/cache/rrset_cache.h b/src/lib/cache/rrset_cache.h new file mode 100644 index 0000000000000000000000000000000000000000..15084c9bf65b802b94b09affc85ba7b207f8c56c --- /dev/null +++ b/src/lib/cache/rrset_cache.h @@ -0,0 +1,106 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __RRSET_CACHE_H +#define __RRSET_CACHE_H + +#include +#include +#include + +using namespace isc::nsas; + +namespace isc { +namespace cache { + +class RRsetEntry; + +/// \brief RRset Cache +/// The object of RRsetCache represented the cache for class-specific +/// RRsets. +class RRsetCache{ + /// + /// \name Constructors and Destructor + /// + /// Note: The copy constructor and the assignment operator are intentionally + /// defined as private to make it uncopyable + //@{ +private: + RRsetCache(const RRsetCache&); + RRsetCache& operator=(const RRsetCache&); +public: + /// \brief Constructor + /// + /// \param cache_size the size of rrset cache. + /// \param rrset_class the class of rrset cache. + RRsetCache(uint32_t cache_size, uint16_t rrset_class); + ~RRsetCache() {} + //@} + + /// \brief Look up rrset in cache. + /// + /// \param qname The query name to look up + /// \param qtype The query type + /// \return return the shared_ptr of rrset entry if it can be + /// found in the cache, or else, return NULL. + RRsetEntryPtr lookup(const isc::dns::Name& qname, + const isc::dns::RRType& qtype); + + /// \brief Update RRset Cache + /// Update the rrset entry in the cache with the new one. + /// If the rrset has expired or doesn't exist in the cache, + /// it will be added directly. It may be ingored if the new + /// rrset is not more authoritative than the old rrset in cache. + /// + /// \param rrset The new rrset used to update cache. + /// \param level trustworthiness of the rrset. + /// \return return the rrset entry in the cache, it may be the + /// new added rrset entry or existed one if it is not replaced. + RRsetEntryPtr update(const isc::dns::RRset& rrset, + const RRsetTrustLevel& level); + + /// \brief Dump the rrset cache to specified file. + /// + /// \param file_name The file to write to + /// + /// \todo It should can be dumped to one configured database. + void dump(const std::string& file_name); + + /// \brief Load the cache from one file. + /// + /// \param file_name The file to read from + /// + /// \todo It should can be loaded from one configured database. + void load(const std::string& file_name); + + /// \brief Resize the size of rrset cache in runtime. + /// + /// \param The size to resize to + /// \return true + bool resize(uint32_t size); + +private: + uint16_t class_; // The class of the rrset cache. + isc::nsas::HashTable rrset_table_; + isc::nsas::LruList rrset_lru_; +}; + +typedef boost::shared_ptr RRsetCachePtr; +typedef boost::shared_ptr ConstRRsetCachePtr; + +} // namespace cache +} // namespace isc + +#endif // __RRSET_CACHE_H + diff --git a/src/lib/cache/rrset_copy.cc b/src/lib/cache/rrset_copy.cc new file mode 100644 index 0000000000000000000000000000000000000000..05b139a44d72875c06ed9cc721014c63182256c8 --- /dev/null +++ b/src/lib/cache/rrset_copy.cc @@ -0,0 +1,38 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include "rrset_copy.h" + +using namespace isc::dns; + +namespace isc { +namespace cache { + +void +rrsetCopy(const isc::dns::RRset& src, isc::dns::RRset& dst) { + RdataIteratorPtr rdata_itor = src.getRdataIterator(); + rdata_itor->first(); + while(!rdata_itor->isLast()){ + dst.addRdata(rdata_itor->getCurrent()); + rdata_itor->next(); + } + + RRsetPtr rrsig = src.getRRsig(); + if (rrsig != NULL){ + dst.addRRsig(rrsig); + } +} + +} // namespace cache +} // namespace isc diff --git a/src/lib/cache/rrset_copy.h b/src/lib/cache/rrset_copy.h new file mode 100644 index 0000000000000000000000000000000000000000..b6af8d64b9beee52e46e5fcf79d6c0e1a455ae3a --- /dev/null +++ b/src/lib/cache/rrset_copy.h @@ -0,0 +1,42 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __RRSET_COPY_ +#define __RRSET_COPY_ + +#include + +namespace isc { +namespace cache { + +/// \brief RRset Copy Function +/// +/// Adds all Rdatas and the RRsig in the source RRset to the target +/// RRset +/// +/// \param src RRset to copy from +/// \param dst RRset to copy to +/// +/// \note RRset class doesn't provide the interface for +/// doing RRset copy. But in cache's code, sometime +/// we have to do the copy. + +void +rrsetCopy(const isc::dns::RRset& src, isc::dns::RRset& dst); + +} // namespace cache +} // namespace isc + +#endif // __RRSET_COPY_ + diff --git a/src/lib/cache/rrset_entry.cc b/src/lib/cache/rrset_entry.cc new file mode 100644 index 0000000000000000000000000000000000000000..c8299564ac3aeb1486e0208d30c2422e41b2c38c --- /dev/null +++ b/src/lib/cache/rrset_entry.cc @@ -0,0 +1,66 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include + +#include +#include +#include +#include "rrset_entry.h" +#include "rrset_copy.h" + +using namespace isc::dns; + +namespace isc { +namespace cache { + +RRsetEntry::RRsetEntry(const isc::dns::RRset& rrset, const RRsetTrustLevel& level): + entry_name_(genCacheEntryName(rrset.getName(), rrset.getType())), + expire_time_(time(NULL) + rrset.getTTL().getValue()), + trust_level_(level), + rrset_(new RRset(rrset.getName(), rrset.getClass(), rrset.getType(), rrset.getTTL())), + hash_key_(HashKey(entry_name_, rrset_->getClass())) +{ + rrsetCopy(rrset, *(rrset_.get())); +} + +isc::dns::RRsetPtr +RRsetEntry::getRRset() { + updateTTL(); + return (rrset_); +} + +time_t +RRsetEntry::getExpireTime() const { + return (expire_time_); +} + +void +RRsetEntry::updateTTL(){ + uint32_t oldTTL = rrset_->getTTL().getValue(); + if(oldTTL == 0) { + return; + } + + uint32_t now = time(NULL); + uint32_t newTTL = now < expire_time_ ? (expire_time_ - now) : 0; + + RRTTL ttl(newTTL); + rrset_->setTTL(ttl); +} + +} // namespace cache +} // namespace isc + + diff --git a/src/lib/cache/rrset_entry.h b/src/lib/cache/rrset_entry.h new file mode 100644 index 0000000000000000000000000000000000000000..5fa8f2c1eaddd7e1c7c4a22c8ccbe1809aaa8bf7 --- /dev/null +++ b/src/lib/cache/rrset_entry.h @@ -0,0 +1,135 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __RRSET_ENTRY_H +#define __RRSET_ENTRY_H + +#include +#include +#include +#include +#include +#include "cache_entry_key.h" + +using namespace isc::nsas; + +namespace isc { +namespace cache { + +/// \enum RRset Trustworthiness +/// For detail of RRset trustworthiness, please refer to +/// RFC2181 section5.4.1. +/// Bigger value is more trustworthy. +enum RRsetTrustLevel { + /// Default trust for RRset. + RRSET_TRUST_DEFAULT = 0, + /// Additional information from non-authoritative answer. + RRSET_TRUST_ADDITIONAL_NONAA, + /// Data from the authority section of a non-authoritative answer + RRSET_TRUST_AUTHORITY_NONAA, + /// Additional information from an authoritative answer. + RRSET_TRUST_ADDITIONAL_AA, + /// Non-authoritative data from the answer section of authoritative + /// answers + RRSET_TRUST_NONAUTH_ANSWER_AA, + /// Data from the answer section of a non-authoritative answer. + RRSET_TRUST_ANSWER_NONAA, + /// Glue from a primary zone, or glue from a zone transfer. + RRSET_TRUST_PRIM_GLUE, + /// Data from the authority section of an authoritative answer. + RRSET_TRUST_AUTHORITY_AA, + /// Authoritative data included in the answer section of + /// an authoritative reply. + RRSET_TRUST_ANSWER_AA, + /// Data from a primary zone file, other than glue data. + RRSET_TRUST_PRIM_ZONE_NONGLUE +}; + +/// \brief RRset Entry +/// The object of RRsetEntry represents one cached RRset. +/// Each RRset entry may be refered using shared_ptr by several message +/// entries. +class RRsetEntry : public NsasEntry +{ + /// + /// \name Constructors and Destructor + /// + /// Note: The copy constructor and the assignment operator are intentionally + /// defined as private to make it uncopyable + //@{ +private: + RRsetEntry(const RRsetEntry&); + RRsetEntry& operator=(const RRsetEntry&); +public: + /// \brief Constructor + /// \param rrset The RRset used to initialize the RRset entry. + /// \param level trustworthiness of the RRset. + RRsetEntry(const isc::dns::RRset& rrset, const RRsetTrustLevel& level); + + /// The destructor. + ~RRsetEntry() {} + //@} + + /// \brief Return a pointer to a generated RRset + /// + /// \return Pointer to the generated RRset + isc::dns::RRsetPtr getRRset(); + + /// \brief Get the expiration time of the RRset. + /// + /// \return The expiration time of the RRset + /// + /// \todo RRsig expiration processing + time_t getExpireTime() const; + + /// \brief Get the ttl of the RRset. + /// + /// \return The TTL of the RRset + uint32_t getTTL() { + updateTTL(); + return (rrset_->getTTL().getValue()); + } + + /// \brief Get the hash key + /// + /// \return return hash key + HashKey hashKey() const { + return (hash_key_); + } + + /// \brief get RRset trustworthiness + /// + /// \return return the trust level + RRsetTrustLevel getTrustLevel() const { + return (trust_level_); + } +private: + /// \brief Update TTL according to expiration time + void updateTTL(); + +private: + std::string entry_name_; // The entry name for this rrset entry. + time_t expire_time_; // Expiration time of rrset. + RRsetTrustLevel trust_level_; // RRset trustworthiness. + boost::shared_ptr rrset_; + HashKey hash_key_; // RRsetEntry hash key +}; + +typedef boost::shared_ptr RRsetEntryPtr; + +} // namespace cache +} // namespace isc + +#endif // __RRSET_ENTRY_H + diff --git a/src/lib/cache/tests/Makefile.am b/src/lib/cache/tests/Makefile.am new file mode 100644 index 0000000000000000000000000000000000000000..b93c9a7d7a401a3eadc08eed85b14a84bf47bf5c --- /dev/null +++ b/src/lib/cache/tests/Makefile.am @@ -0,0 +1,66 @@ +SUBDIRS = . + +AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib +AM_CPPFLAGS += $(BOOST_INCLUDES) $(MULTITHREADING_FLAG) +AM_CPPFLAGS += -I$(top_srcdir)/src/lib/dns -I$(top_builddir)/src/lib/dns +AM_CPPFLAGS += -I$(top_srcdir)/src/lib/cache -I$(top_builddir)/src/lib/cache +AM_CPPFLAGS += -DTEST_DATA_SRCDIR=\"$(srcdir)/testdata\" +AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/lib/cache/tests/testdata\" +AM_CXXFLAGS = $(B10_CXXFLAGS) + + +AM_LDFLAGS = $(PTHREAD_LDFLAGS) +if USE_STATIC_LINK +AM_LDFLAGS += -static +endif + +# Some versions of GCC warn about some versions of Boost regarding +# missing initializer for members in its posix_time. +# https://svn.boost.org/trac/boost/ticket/3477 +# But older GCC compilers don't have the flag. +AM_CXXFLAGS += $(WARNING_NO_MISSING_FIELD_INITIALIZERS_CFLAG) + +if USE_CLANGPP +# see ../Makefile.am +AM_CXXFLAGS += -Wno-unused-parameter +endif + +CLEANFILES = *.gcno *.gcda + +TESTS = +if HAVE_GTEST +TESTS += run_unittests +run_unittests_SOURCES = run_unittests.cc +run_unittests_SOURCES += $(top_srcdir)/src/lib/dns/tests/unittest_util.cc +run_unittests_SOURCES += rrset_entry_unittest.cc +run_unittests_SOURCES += rrset_cache_unittest.cc +run_unittests_SOURCES += message_cache_unittest.cc +run_unittests_SOURCES += message_entry_unittest.cc +run_unittests_SOURCES += local_zone_data_unittest.cc +run_unittests_SOURCES += resolver_cache_unittest.cc +run_unittests_SOURCES += cache_test_messagefromfile.h +run_unittests_SOURCES += cache_test_sectioncount.h + +run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) +run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS) +run_unittests_LDADD = $(GTEST_LDADD) + +# NOTE: we may have to clean up this hack later (see the note in configure.ac) +if NEED_LIBBOOST_THREAD +run_unittests_LDADD += -lboost_thread +endif + +run_unittests_LDADD += $(top_builddir)/src/lib/cache/libcache.la +run_unittests_LDADD += $(top_builddir)/src/lib/nsas/libnsas.la +run_unittests_LDADD += $(top_builddir)/src/lib/dns/libdns++.la +run_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libexceptions.la +endif + +noinst_PROGRAMS = $(TESTS) + +EXTRA_DIST = testdata/message_fromWire1 +EXTRA_DIST += testdata/message_fromWire2 +EXTRA_DIST += testdata/message_fromWire3 +EXTRA_DIST += testdata/message_fromWire4 +EXTRA_DIST += testdata/message_fromWire5 +EXTRA_DIST += testdata/message_fromWire6 diff --git a/src/lib/cache/tests/cache_test_messagefromfile.h b/src/lib/cache/tests/cache_test_messagefromfile.h new file mode 100644 index 0000000000000000000000000000000000000000..62e237c83bb4a30e63b5ba46c79676469da6346e --- /dev/null +++ b/src/lib/cache/tests/cache_test_messagefromfile.h @@ -0,0 +1,39 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include +#include + +using namespace isc; +using namespace isc::dns; + +namespace { + +/// \brief Reads a Message from a data file +/// +/// \param message Message to put the read data in +/// \param datafile The file to read from +void +messageFromFile(Message& message, const char* datafile) { + std::vector data; + UnitTestUtil::readWireData(datafile, data); + + InputBuffer buffer(&data[0], data.size()); + message.fromWire(buffer); +} + +} // namespace + diff --git a/src/lib/cache/tests/cache_test_sectioncount.h b/src/lib/cache/tests/cache_test_sectioncount.h new file mode 100644 index 0000000000000000000000000000000000000000..537ca81054be3f35b761ad7675e6f57d479cff71 --- /dev/null +++ b/src/lib/cache/tests/cache_test_sectioncount.h @@ -0,0 +1,44 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include +#include + +using namespace isc; +using namespace isc::dns; + +namespace { + +/// \brief Counts the number of rrsets in the given section +/// +/// \param msg The message to count in +/// \param section The section to count +/// +/// \return The number of RRsets in the given section +int +sectionRRsetCount(Message& msg, Message::Section section) { + int count = 0; + for (RRsetIterator rrset_iter = msg.beginSection(section); + rrset_iter != msg.endSection(section); + ++rrset_iter) { + ++count; + } + + return count; +} + +} // namespace + diff --git a/src/lib/cache/tests/local_zone_data_unittest.cc b/src/lib/cache/tests/local_zone_data_unittest.cc new file mode 100644 index 0000000000000000000000000000000000000000..6877eaed0199df088ff6cbf10aeb634180775bfd --- /dev/null +++ b/src/lib/cache/tests/local_zone_data_unittest.cc @@ -0,0 +1,64 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include +#include +#include +#include +#include "cache_test_messagefromfile.h" + +using namespace isc::cache; +using namespace isc::dns; +using namespace std; + +namespace { + +class LocalZoneDataTest: public testing::Test { +protected: + LocalZoneDataTest(): local_zone_data(1) + { + } + + LocalZoneData local_zone_data; +}; + +TEST_F(LocalZoneDataTest, updateAndLookup) { + Message msg(Message::PARSE); + messageFromFile(msg, "message_fromWire3"); + RRsetIterator rrset_iter = msg.beginSection(Message::SECTION_AUTHORITY); + Name name = (*rrset_iter)->getName(); + RRType type = (*rrset_iter)->getType(); + + EXPECT_FALSE(local_zone_data.lookup(name, type)); + local_zone_data.update((*(*rrset_iter).get())); + EXPECT_TRUE(local_zone_data.lookup(name, type)); + + // Test whether the old one is replaced + uint32_t ttl = (*rrset_iter)->getTTL().getValue(); + // Make sure it is not zero + ASSERT_NE(ttl / 2, ttl); + + RRsetPtr rrset_ptr = local_zone_data.lookup(name, type); + EXPECT_EQ(ttl, rrset_ptr->getTTL().getValue()); + + (*rrset_iter)->setTTL(RRTTL(ttl/2)); + + local_zone_data.update((*(*rrset_iter).get())); + rrset_ptr = local_zone_data.lookup(name, type); + EXPECT_EQ(ttl/2, rrset_ptr->getTTL().getValue()); +} + +} diff --git a/src/lib/cache/tests/message_cache_unittest.cc b/src/lib/cache/tests/message_cache_unittest.cc new file mode 100644 index 0000000000000000000000000000000000000000..e7184bdfeed5368bc5e6e994a29c846d0f6adbef --- /dev/null +++ b/src/lib/cache/tests/message_cache_unittest.cc @@ -0,0 +1,97 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include +#include +#include +#include "../message_cache.h" +#include "../rrset_cache.h" +#include "../resolver_cache.h" +#include "cache_test_messagefromfile.h" + +using namespace isc::cache; +using namespace isc; +using namespace isc::dns; +using namespace std; + +namespace { + +/// \brief Derived from base class to make it easy to test +/// its internals. +class DerivedMessageCache: public MessageCache { +public: + DerivedMessageCache(boost::shared_ptr rrset_cache_, + uint32_t cache_size, uint16_t message_class): + MessageCache(rrset_cache_, cache_size, message_class) + {} + + uint16_t messages_count() { + return message_lru_.size(); + } +}; + +class MessageCacheTest: public testing::Test { +public: + MessageCacheTest(): message_parse(Message::PARSE), + message_render(Message::RENDER) + { + uint16_t class_ = RRClass::IN().getCode(); + rrset_cache_.reset(new RRsetCache(RRSET_CACHE_DEFAULT_SIZE, class_)); + message_cache_.reset(new DerivedMessageCache(rrset_cache_, + MESSAGE_CACHE_DEFAULT_SIZE, class_ )); + } + +protected: + boost::shared_ptr message_cache_; + RRsetCachePtr rrset_cache_; + Message message_parse; + Message message_render; +}; + +TEST_F(MessageCacheTest, testLookup) { + messageFromFile(message_parse, "message_fromWire1"); + EXPECT_TRUE(message_cache_->update(message_parse)); + Name qname("test.example.com."); + EXPECT_TRUE(message_cache_->lookup(qname, RRType::A(), message_render)); + EXPECT_EQ(message_cache_->messages_count(), 1); + + Message message_net(Message::PARSE); + messageFromFile(message_net, "message_fromWire2"); + EXPECT_TRUE(message_cache_->update(message_net)); + EXPECT_EQ(message_cache_->messages_count(), 2); + + Name qname1("test.example.net."); + EXPECT_TRUE(message_cache_->lookup(qname1, RRType::A(), message_render)); +} + +TEST_F(MessageCacheTest, testUpdate) { + messageFromFile(message_parse, "message_fromWire4"); + EXPECT_TRUE(message_cache_->update(message_parse)); + + Name qname("example.com."); + EXPECT_TRUE(message_cache_->lookup(qname, RRType::SOA(), message_render)); + EXPECT_FALSE(message_render.getHeaderFlag(Message::HEADERFLAG_AA)); + + Message new_msg(Message::PARSE); + messageFromFile(new_msg, "message_fromWire3"); + EXPECT_TRUE(message_cache_->update(new_msg)); + Message new_msg_render(Message::RENDER); + EXPECT_TRUE(message_cache_->lookup(qname, RRType::SOA(), new_msg_render)); + EXPECT_TRUE(new_msg_render.getHeaderFlag(Message::HEADERFLAG_AA)); +} + +} // namespace + diff --git a/src/lib/cache/tests/message_entry_unittest.cc b/src/lib/cache/tests/message_entry_unittest.cc new file mode 100644 index 0000000000000000000000000000000000000000..3b2711a2f90ae7ca238338780048d525c0e58bec --- /dev/null +++ b/src/lib/cache/tests/message_entry_unittest.cc @@ -0,0 +1,236 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include +#include +#include +#include +#include "../message_entry.h" +#include "../rrset_cache.h" +#include "../resolver_cache.h" +#include "cache_test_messagefromfile.h" +#include "cache_test_sectioncount.h" + +using namespace isc::cache; +using namespace isc; +using namespace isc::dns; +using namespace std; + +static uint32_t MAX_UINT32 = numeric_limits::max(); + +namespace { + +/// \brief Derived from base class to make it easy to test +/// its internals. +class DerivedMessageEntry: public MessageEntry { +public: + DerivedMessageEntry(const isc::dns::Message& message, + boost::shared_ptr rrset_cache_): + MessageEntry(message, rrset_cache_) + {} + + /// \brief Wrap the protected function so that it can be tested. + void parseSectionForTest(const Message& msg, + const Message::Section& section, + uint32_t& smaller_ttl, + uint16_t& rrset_count) + { + parseSection(msg, section, smaller_ttl, rrset_count); + } + + RRsetTrustLevel getRRsetTrustLevelForTest(const Message& message, + const RRsetPtr rrset, + const Message::Section& section) + { + return getRRsetTrustLevel(message, rrset, section); + } + + bool getRRsetEntriesForTest(vector vec, time_t now) { + return getRRsetEntries(vec, now); + } + + time_t getExpireTime() { + return expire_time_; + } + +}; + +class MessageEntryTest: public testing::Test { +public: + MessageEntryTest(): class_(1), + message_parse(Message::PARSE), + message_render(Message::RENDER) + { + + rrset_cache_.reset(new RRsetCache(RRSET_CACHE_DEFAULT_SIZE, class_)); + } + +protected: + uint16_t class_; + RRsetCachePtr rrset_cache_; + Message message_parse; + Message message_render; +}; + +TEST_F(MessageEntryTest, testParseRRset) { + messageFromFile(message_parse, "message_fromWire3"); + DerivedMessageEntry message_entry(message_parse, rrset_cache_); + uint32_t ttl = MAX_UINT32; + uint16_t rrset_count = 0; + message_entry.parseSectionForTest(message_parse, Message::SECTION_ANSWER, ttl, rrset_count); + EXPECT_EQ(ttl, 21600); + EXPECT_EQ(rrset_count, 1); + + ttl = MAX_UINT32; + message_entry.parseSectionForTest(message_parse, Message::SECTION_AUTHORITY, ttl, rrset_count); + EXPECT_EQ(ttl, 21600); + EXPECT_EQ(rrset_count, 1); + + ttl = MAX_UINT32; + message_entry.parseSectionForTest(message_parse, Message::SECTION_ADDITIONAL, ttl, rrset_count); + EXPECT_EQ(ttl, 10800); + EXPECT_EQ(rrset_count, 5); +} + +TEST_F(MessageEntryTest, testGetRRsetTrustLevel_AA) { + messageFromFile(message_parse, "message_fromWire3"); + DerivedMessageEntry message_entry(message_parse, rrset_cache_); + + + RRsetIterator rrset_iter = message_parse.beginSection(Message::SECTION_ANSWER); + RRsetTrustLevel level = message_entry.getRRsetTrustLevelForTest(message_parse, + *rrset_iter, + Message::SECTION_ANSWER); + EXPECT_EQ(level, RRSET_TRUST_ANSWER_AA); + + rrset_iter = message_parse.beginSection(Message::SECTION_AUTHORITY); + level = message_entry.getRRsetTrustLevelForTest(message_parse, + *rrset_iter, + Message::SECTION_AUTHORITY); + EXPECT_EQ(level, RRSET_TRUST_AUTHORITY_AA); + + rrset_iter = message_parse.beginSection(Message::SECTION_ADDITIONAL); + level = message_entry.getRRsetTrustLevelForTest(message_parse, + *rrset_iter, + Message::SECTION_ADDITIONAL); + EXPECT_EQ(level, RRSET_TRUST_ADDITIONAL_AA); +} + +TEST_F(MessageEntryTest, testGetRRsetTrustLevel_NONAA) { + messageFromFile(message_parse, "message_fromWire4"); + DerivedMessageEntry message_entry(message_parse, rrset_cache_); + RRsetIterator rrset_iter = message_parse.beginSection(Message::SECTION_ANSWER); + RRsetTrustLevel level = message_entry.getRRsetTrustLevelForTest(message_parse, + *rrset_iter, + Message::SECTION_ANSWER); + EXPECT_EQ(level, RRSET_TRUST_ANSWER_NONAA); + + rrset_iter = message_parse.beginSection(Message::SECTION_AUTHORITY); + level = message_entry.getRRsetTrustLevelForTest(message_parse, + *rrset_iter, + Message::SECTION_AUTHORITY); + EXPECT_EQ(level, RRSET_TRUST_AUTHORITY_NONAA); + + rrset_iter = message_parse.beginSection(Message::SECTION_ADDITIONAL); + level = message_entry.getRRsetTrustLevelForTest(message_parse, + *rrset_iter, + Message::SECTION_ADDITIONAL); + EXPECT_EQ(level, RRSET_TRUST_ADDITIONAL_NONAA); +} + +TEST_F(MessageEntryTest, testGetRRsetTrustLevel_CNAME) { + messageFromFile(message_parse, "message_fromWire5"); + DerivedMessageEntry message_entry(message_parse, rrset_cache_); + RRsetIterator rrset_iter = message_parse.beginSection(Message::SECTION_ANSWER); + RRsetTrustLevel level = message_entry.getRRsetTrustLevelForTest(message_parse, + *rrset_iter, + Message::SECTION_ANSWER); + EXPECT_EQ(level, RRSET_TRUST_ANSWER_AA); + + ++rrset_iter; // Get the rrset after the first cname rrset. + level = message_entry.getRRsetTrustLevelForTest(message_parse, + *rrset_iter, + Message::SECTION_ANSWER); + EXPECT_EQ(level, RRSET_TRUST_ANSWER_AA); +} + +TEST_F(MessageEntryTest, testGetRRsetTrustLevel_DNAME) { + messageFromFile(message_parse, "message_fromWire6"); + DerivedMessageEntry message_entry(message_parse, rrset_cache_); + RRsetIterator rrset_iter = message_parse.beginSection(Message::SECTION_ANSWER); + RRsetTrustLevel level = message_entry.getRRsetTrustLevelForTest(message_parse, + *rrset_iter, + Message::SECTION_ANSWER); + EXPECT_EQ(level, RRSET_TRUST_ANSWER_AA); + + ++rrset_iter; // Get the rrset after the first dname rrset. + level = message_entry.getRRsetTrustLevelForTest(message_parse, + *rrset_iter, + Message::SECTION_ANSWER); + EXPECT_EQ(level, RRSET_TRUST_ANSWER_AA); + + ++rrset_iter; // Get the second cname rrset + level = message_entry.getRRsetTrustLevelForTest(message_parse, + *rrset_iter, + Message::SECTION_ANSWER); + EXPECT_EQ(level, RRSET_TRUST_ANSWER_AA); +} + +// We only test the expire_time of the message entry. +// The test for genMessage() will make sure whether InitMessageEntry() +// is right +TEST_F(MessageEntryTest, testInitMessageEntry) { + messageFromFile(message_parse, "message_fromWire3"); + DerivedMessageEntry message_entry(message_parse, rrset_cache_); + time_t expire_time = message_entry.getExpireTime(); + // 1 second should be enough to do the compare + EXPECT_TRUE((time(NULL) + 10801) > expire_time); +} + +TEST_F(MessageEntryTest, testGetRRsetEntries) { + messageFromFile(message_parse, "message_fromWire3"); + DerivedMessageEntry message_entry(message_parse, rrset_cache_); + vector vec; + + // the time is bigger than the smallest expire time of + // the rrset in message. + time_t expire_time = time(NULL) + 10802; + EXPECT_FALSE(message_entry.getRRsetEntriesForTest(vec, expire_time)); +} + +TEST_F(MessageEntryTest, testGenMessage) { + messageFromFile(message_parse, "message_fromWire3"); + DerivedMessageEntry message_entry(message_parse, rrset_cache_); + time_t expire_time = message_entry.getExpireTime(); + + Message msg(Message::RENDER); + EXPECT_FALSE(message_entry.genMessage(expire_time + 2, msg)); + message_entry.genMessage(time(NULL), msg); + // Check whether the generated message is same with cached one. + + EXPECT_TRUE(msg.getHeaderFlag(Message::HEADERFLAG_AA)); + EXPECT_FALSE(msg.getHeaderFlag(Message::HEADERFLAG_TC)); + EXPECT_EQ(1, sectionRRsetCount(msg, Message::SECTION_ANSWER)); + EXPECT_EQ(1, sectionRRsetCount(msg, Message::SECTION_AUTHORITY)); + EXPECT_EQ(5, sectionRRsetCount(msg, Message::SECTION_ADDITIONAL)); + + // Check the rrset in answer section. + EXPECT_EQ(1, msg.getRRCount(Message::SECTION_ANSWER)); + EXPECT_EQ(5, msg.getRRCount(Message::SECTION_AUTHORITY)); + EXPECT_EQ(7, msg.getRRCount(Message::SECTION_ADDITIONAL)); +} + +} // namespace diff --git a/src/lib/cache/tests/resolver_cache_unittest.cc b/src/lib/cache/tests/resolver_cache_unittest.cc new file mode 100644 index 0000000000000000000000000000000000000000..20dcec9d911fd5a8a475223cbb3934125f22b240 --- /dev/null +++ b/src/lib/cache/tests/resolver_cache_unittest.cc @@ -0,0 +1,128 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include +#include +#include "resolver_cache.h" +#include "cache_test_messagefromfile.h" +#include "cache_test_sectioncount.h" + +using namespace isc::cache; +using namespace isc::dns; +using namespace std; + +namespace { + +class ResolverCacheTest: public testing::Test { +public: + ResolverCacheTest() { + vector vec; + CacheSizeInfo class_in(RRClass::IN(), 100, 200); + CacheSizeInfo class_ch(RRClass::CH(), 100, 200); + vec.push_back(class_in); + vec.push_back(class_ch); + cache = new ResolverCache(vec); + } + + ~ResolverCacheTest() { + delete cache; + } + + ResolverCache* cache; +}; + +TEST_F(ResolverCacheTest, testUpdateMessage) { + Message msg(Message::PARSE); + messageFromFile(msg, "message_fromWire3"); + cache->update(msg); + + Name qname("example.com."); + + msg.makeResponse(); + EXPECT_TRUE(cache->lookup(qname, RRType::SOA(), RRClass::IN(), msg)); + EXPECT_TRUE(msg.getHeaderFlag(Message::HEADERFLAG_AA)); + + // Test whether the old message can be updated + Message new_msg(Message::PARSE); + messageFromFile(new_msg, "message_fromWire4"); + cache->update(new_msg); + + new_msg.makeResponse(); + EXPECT_TRUE(cache->lookup(qname, RRType::SOA(), RRClass::IN(), new_msg)); + EXPECT_FALSE(new_msg.getHeaderFlag(Message::HEADERFLAG_AA)); +} + +TEST_F(ResolverCacheTest, testUpdateRRset) { + Message msg(Message::PARSE); + messageFromFile(msg, "message_fromWire3"); + cache->update(msg); + + Name qname("example.com."); + + msg.makeResponse(); + EXPECT_TRUE(cache->lookup(qname, RRType::SOA(), RRClass::IN(), msg)); + + Message except_msg(Message::RENDER); + EXPECT_THROW(cache->lookup(qname, RRType::SOA(), RRClass::IN(), except_msg), + MessageNoQuestionSection); + + // Get one rrset in the message, then use it to + // update rrset cache-> Test whether the local zone + // data is updated. + RRsetIterator iter = msg.beginSection(Message::SECTION_AUTHORITY); + RRsetPtr rrset_ptr = *iter; + cache->update(rrset_ptr); + + Message new_msg(Message::RENDER); + Question question(qname, RRClass::IN(), RRType::NS()); + new_msg.addQuestion(question); + EXPECT_TRUE(cache->lookup(qname, RRType::NS(), RRClass::IN(), new_msg)); + EXPECT_EQ(0, sectionRRsetCount(new_msg, Message::SECTION_AUTHORITY)); + EXPECT_EQ(0, sectionRRsetCount(new_msg, Message::SECTION_ADDITIONAL)); +} + +TEST_F(ResolverCacheTest, testLookupUnsupportedClass) { + Message msg(Message::PARSE); + messageFromFile(msg, "message_fromWire3"); + cache->update(msg); + + Name qname("example.com."); + + msg.makeResponse(); + EXPECT_FALSE(cache->lookup(qname, RRType::SOA(), RRClass::CH(), msg)); + EXPECT_FALSE(cache->lookup(qname, RRType::SOA(), RRClass::CH())); +} + +TEST_F(ResolverCacheTest, testLookupClosestRRset) { + Message msg(Message::PARSE); + messageFromFile(msg, "message_fromWire3"); + cache->update(msg); + + Name qname("www.test.example.com."); + + RRsetPtr rrset_ptr = cache->lookupDeepestNS(qname, RRClass::IN()); + EXPECT_TRUE(rrset_ptr); + EXPECT_EQ(rrset_ptr->getName(), Name("example.com.")); + + rrset_ptr = cache->lookupDeepestNS(Name("example.com."), RRClass::IN()); + EXPECT_TRUE(rrset_ptr); + EXPECT_EQ(rrset_ptr->getName(), Name("example.com.")); + + rrset_ptr = cache->lookupDeepestNS(Name("com."), RRClass::IN()); + EXPECT_FALSE(rrset_ptr); +} + +} diff --git a/src/lib/cache/tests/rrset_cache_unittest.cc b/src/lib/cache/tests/rrset_cache_unittest.cc new file mode 100644 index 0000000000000000000000000000000000000000..afb7eaa7f54a132b87f04c9070f302ee2bd14b66 --- /dev/null +++ b/src/lib/cache/tests/rrset_cache_unittest.cc @@ -0,0 +1,83 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace isc::cache; +using namespace isc::dns; +using namespace std; + +namespace { + +class RRsetCacheTest : public testing::Test { +protected: + RRsetCacheTest(): + cache(RRSET_CACHE_DEFAULT_SIZE, RRClass::IN().getCode()), + name("example.com"), + rrset1(name, RRClass::IN(), RRType::A(), RRTTL(20)), + rrset2(name, RRClass::IN(), RRType::A(), RRTTL(10)), + rrset_entry1(rrset1, RRSET_TRUST_ADDITIONAL_AA), + rrset_entry2(rrset2, RRSET_TRUST_PRIM_ZONE_NONGLUE) + { + } + + RRsetCache cache; + Name name; + RRset rrset1; + RRset rrset2; + RRsetEntry rrset_entry1; + RRsetEntry rrset_entry2; +}; + +TEST_F(RRsetCacheTest, lookup) { + const RRType& type = RRType::A(); + EXPECT_TRUE(cache.lookup(name, type) == NULL); + + cache.update(rrset1, rrset_entry1.getTrustLevel()); + RRsetEntryPtr rrset_entry_ptr = cache.lookup(name, type); + EXPECT_EQ(rrset_entry_ptr->getTrustLevel(), rrset_entry1.getTrustLevel()); + EXPECT_EQ(rrset_entry_ptr->getRRset()->getName(), rrset_entry1.getRRset()->getName()); + EXPECT_EQ(rrset_entry_ptr->getRRset()->getType(), rrset_entry1.getRRset()->getType()); + EXPECT_EQ(rrset_entry_ptr->getRRset()->getClass(), rrset_entry1.getRRset()->getClass()); +} + +TEST_F(RRsetCacheTest, update) { + const RRType& type = RRType::A(); + + cache.update(rrset1, rrset_entry1.getTrustLevel()); + RRsetEntryPtr rrset_entry_ptr = cache.lookup(name, type); + EXPECT_EQ(rrset_entry_ptr->getTrustLevel(), rrset_entry1.getTrustLevel()); + + cache.update(rrset2, rrset_entry2.getTrustLevel()); + rrset_entry_ptr = cache.lookup(name, type); + // The trust level should be updated + EXPECT_EQ(rrset_entry_ptr->getTrustLevel(), rrset_entry2.getTrustLevel()); + + cache.update(rrset1, rrset_entry1.getTrustLevel()); + // The trust level should not be updated + EXPECT_EQ(rrset_entry_ptr->getTrustLevel(), rrset_entry2.getTrustLevel()); +} + +} diff --git a/src/lib/cache/tests/rrset_entry_unittest.cc b/src/lib/cache/tests/rrset_entry_unittest.cc new file mode 100644 index 0000000000000000000000000000000000000000..c7c3c6eb69909694acd369d7e1040112cc95225a --- /dev/null +++ b/src/lib/cache/tests/rrset_entry_unittest.cc @@ -0,0 +1,106 @@ +// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace isc::cache; +using namespace isc::dns; +using namespace std; + +namespace { +class GenCacheKeyTest: public testing::Test { +}; + +TEST_F(GenCacheKeyTest, genCacheEntryKey1) { + string name = "example.com."; + uint16_t type = 12; + string name_type = "example.com.12"; + + EXPECT_EQ(name_type, genCacheEntryName(name, type)); +} + +TEST_F(GenCacheKeyTest, genCacheEntryKey2) { + Name name("example.com"); + RRType type(1234); + string keystr = "example.com.1234"; + EXPECT_EQ(keystr, genCacheEntryName(name, type)); +} + +class DerivedRRsetEntry: public RRsetEntry { +public: + DerivedRRsetEntry(const isc::dns::RRset& rrset, const RRsetTrustLevel& level) : RRsetEntry(rrset, level) {}; + + void updateTTLForTest() { + + } +}; + +#define TEST_TTL 100 +class RRsetEntryTest : public ::testing::Test { +protected: + RRsetEntryTest(): + name("test.example.com"), + rrset(name, RRClass::IN(), RRType::A(), RRTTL(TEST_TTL)), + trust_level(RRSET_TRUST_ADDITIONAL_AA), + rrset_entry(rrset, trust_level) + { + } + Name name; + RRset rrset; + RRsetTrustLevel trust_level; + RRsetEntry rrset_entry; +}; + +TEST_F(RRsetEntryTest, constructor) { + EXPECT_EQ(trust_level, rrset_entry.getTrustLevel()); + EXPECT_EQ(rrset.getName(), rrset_entry.getRRset()->getName()); + EXPECT_EQ(rrset.getClass(), rrset_entry.getRRset()->getClass()); + EXPECT_EQ(rrset.getType(), rrset_entry.getRRset()->getType()); + EXPECT_EQ(rrset.getRdataCount(), rrset_entry.getRRset()->getRdataCount()); +} + +TEST_F(RRsetEntryTest, updateTTL) { + uint32_t ttl = rrset_entry.getTTL(); + sleep(1); + // The TTL should be decreased + EXPECT_TRUE(rrset_entry.getTTL() < ttl); +} + +TEST_F(RRsetEntryTest, TTLExpire) { + RRset exp_rrset(name, RRClass::IN(), RRType::A(), RRTTL(1)); + RRsetEntry rrset_entry(exp_rrset, RRSET_TRUST_ANSWER_AA); + sleep(1); + uint32_t ttl = rrset_entry.getTTL(); + EXPECT_LT(ttl, 1); + sleep(1); + ttl = rrset_entry.getTTL(); + EXPECT_LT(ttl, 1); +} + +TEST_F(RRsetEntryTest, getExpireTime){ + uint32_t exp_time = time(NULL) + TEST_TTL; + EXPECT_EQ(exp_time, rrset_entry.getExpireTime()); +} + +} // namespace + diff --git a/src/lib/cache/tests/run_unittests.cc b/src/lib/cache/tests/run_unittests.cc new file mode 100644 index 0000000000000000000000000000000000000000..2c86581a7987f48ea07356e72834df1ab49ddec6 --- /dev/null +++ b/src/lib/cache/tests/run_unittests.cc @@ -0,0 +1,28 @@ +// Copyright (C) 2009 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include + +#include + +#include + +int +main(int argc, char* argv[]) { + ::testing::InitGoogleTest(&argc, argv); + isc::UnitTestUtil::addDataPath(TEST_DATA_SRCDIR); + isc::UnitTestUtil::addDataPath(TEST_DATA_BUILDDIR); + + return (RUN_ALL_TESTS()); +} diff --git a/src/lib/cache/tests/testdata/message_fromWire1 b/src/lib/cache/tests/testdata/message_fromWire1 new file mode 100644 index 0000000000000000000000000000000000000000..5b76e3ff8e2ced4c43eb511ca69d4b66d244ab88 --- /dev/null +++ b/src/lib/cache/tests/testdata/message_fromWire1 @@ -0,0 +1,22 @@ +# +# A simple DNS response message +# ID = 0x1035 +# QR=1 (response), Opcode=0, AA=1, RD=1 (other fields are 0) +# QDCOUNT=1, ANCOUNT=2, other COUNTS=0 +# Question: test.example.com. IN A +# Answer: +# test.example.com. 3600 IN A 192.0.2.1 +# test.example.com. 7200 IN A 192.0.2.2 +# +1035 8500 +0001 0002 0000 0000 +#(4) t e s t (7) e x a m p l e (3) c o m . + 04 74 65 73 74 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 +0001 0001 +# same name, fully compressed +c0 0c +# TTL=3600, A, IN, RDLENGTH=4, RDATA +0001 0001 00000e10 0004 c0 00 02 01 +# mostly same, with the slight difference in RDATA and TTL +c0 0c +0001 0001 00001c20 0004 c0 00 02 02 diff --git a/src/lib/cache/tests/testdata/message_fromWire2 b/src/lib/cache/tests/testdata/message_fromWire2 new file mode 100644 index 0000000000000000000000000000000000000000..c8fddbda779d1d3cee535a93d62cdcc8c722b089 --- /dev/null +++ b/src/lib/cache/tests/testdata/message_fromWire2 @@ -0,0 +1,22 @@ +# +# A simple DNS response message +# ID = 0x1035 +# QR=1 (response), Opcode=0, AA=1, RD=1 (other fields are 0) +# QDCOUNT=1, ANCOUNT=2, other COUNTS=0 +# Question: test.example.net. IN A +# Answer: +# test.example.net. 3600 IN A 192.0.2.1 +# test.example.net. 7200 IN A 192.0.2.2 +# +1035 8500 +0001 0002 0000 0000 +#(4) t e s t (7) e x a m p l e (3) n e t . + 04 74 65 73 74 07 65 78 61 6d 70 6c 65 03 6e 65 74 00 +0001 0001 +# same name, fully compressed +c0 0c +# TTL=3600, A, IN, RDLENGTH=4, RDATA +0001 0001 00000e10 0004 c0 00 02 01 +# mostly same, with the slight difference in RDATA and TTL +c0 0c +0001 0001 00001c20 0004 c0 00 02 02 diff --git a/src/lib/cache/tests/testdata/message_fromWire3 b/src/lib/cache/tests/testdata/message_fromWire3 new file mode 100644 index 0000000000000000000000000000000000000000..f7b3a4a2c50a30a31100055dfbc519455029116c --- /dev/null +++ b/src/lib/cache/tests/testdata/message_fromWire3 @@ -0,0 +1,76 @@ +# +# A simple DNS response message +# ID = 0x0513 +# QR=1 (response), Opcode=0, AA=1, RD=1 (other fields are 0) +# QDCOUNT=1, ANCOUNT=1, AUTHORITY COUNT=5, ADDITIONAL COUNT=7 +# Question: example.com. IN SOA +# ANSWER: +# ;; QUESTION SECTION: +# ;example.com. IN SOA + +# ;; ANSWER SECTION: +# example.com. 21600 IN SOA a.dns.example.com. root.example.com. 2009070811 7200 3600 2419200 21600 + +# ;; AUTHORITY SECTION: +# example.com. 21600 IN NS b.dns.example.com. +# example.com. 21600 IN NS c.dns.example.com. +# example.com. 21600 IN NS a.dns.example.com. +# example.com. 21600 IN NS e.dns.example.com. +# example.com. 21600 IN NS d.dns.example.com. + +# ;; ADDITIONAL SECTION: +# a.dns.example.com. 21600 IN A 1.1.1.1 +# a.dns.example.com. 21600 IN A 2.2.2.2 +# b.dns.example.com. 21600 IN A 3.3.3.3 +# c.dns.example.com. 10800 IN A 4.4.4.4 +# d.dns.example.com. 43200 IN A 5.5.5.5 +# e.dns.example.com. 21600 IN A 7.7.7.7 +# e.dns.example.com. 21600 IN A 6.6.6.6 + +0513 8500 +0001 0001 0005 0007 +#(7) e x a m p l e (3) c o m . + 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 +0006 0001 +# same name, fully compressed +c0 0c +# SOA IN TTL=6h RDLENGTH=35 rdata +0006 0001 00005460 0023 01 61 03 64 6e 73 c0 0c 04 72 6f 6f 74 c0 0c 77 bf fc db 00 00 1c 20 00 00 0e 10 00 24 ea 00 00 00 54 60 +#Authority section begin +c0 0c +# NS IN TTL=6h RDLENGTH=4 b.dns.example.com. +0002 0001 00005460 0004 01 62 c0 2b +# NS IN TTL=6h c.dns.example.com. +c0 0c +0002 0001 00005460 00 04 01 63 c0 2b +# NS IN a.dns.example.com. +c0 0c +0002 0001 00005460 00 02 c0 29 +# NS IN e.dns.example.com. +c0 0c +0002 0001 00005460 0004 01 65 c0 2b +# NS IN d.dns.example.com. +c0 0c +0002 0001 00005460 0004 01 64 c0 2b +# additional section begin +# a.dns.example.com. A +c0 29 +0001 0001 00005460 0004 01 01 01 01 +# a.dns.example.com. A +c0 29 +0001 0001 00005460 0004 02 02 02 02 +#b.dns.example.com. A +c0 58 +0001 0001 00002A30 0004 03 03 03 03 +#c.dns.example.com. A +c0 68 +0001 0001 00005460 0004 04 04 04 04 +# d.dns.example.com. A +c0 96 +0001 0001 0000A8C0 0004 05 05 05 05 +# e.dns.example.com. A +c0 86 +0001 0001 00005460 0004 07 07 07 07 +# e.dns.example.com. A +c0 86 +0001 0001 00005460 0004 06 06 06 06 diff --git a/src/lib/cache/tests/testdata/message_fromWire4 b/src/lib/cache/tests/testdata/message_fromWire4 new file mode 100644 index 0000000000000000000000000000000000000000..251abd509b3b8e5111ffbe651664378ae9eb6367 --- /dev/null +++ b/src/lib/cache/tests/testdata/message_fromWire4 @@ -0,0 +1,80 @@ +# Note: This message is same with message_fromWire3, except +# AA bit is not set. There should be a better way to +# avoid the duplicated file by clear the AA bit flags +# after reading the message from message_fromWire4. +# +# A simple DNS response message +# ID = 0x0513 +# QR=1 (response), Opcode=0, RD=1 (other fields are 0) +# QDCOUNT=1, ANCOUNT=1, AUTHORITY COUNT=5, ADDITIONAL COUNT=7 +# Question: example.com. IN SOA +# ANSWER: +# ;; QUESTION SECTION: +# ;example.com. IN SOA + +# ;; ANSWER SECTION: +# example.com. 21600 IN SOA a.dns.example.com. root.example.com. 2009070811 7200 3600 2419200 21600 + +# ;; AUTHORITY SECTION: +# example.com. 21600 IN NS b.dns.example.com. +# example.com. 21600 IN NS c.dns.example.com. +# example.com. 21600 IN NS a.dns.example.com. +# example.com. 21600 IN NS e.dns.example.com. +# example.com. 21600 IN NS d.dns.example.com. + +# ;; ADDITIONAL SECTION: +# a.dns.example.com. 21600 IN A 1.1.1.1 +# a.dns.example.com. 21600 IN A 2.2.2.2 +# b.dns.example.com. 21600 IN A 3.3.3.3 +# c.dns.example.com. 10800 IN A 4.4.4.4 +# d.dns.example.com. 43200 IN A 5.5.5.5 +# e.dns.example.com. 21600 IN A 7.7.7.7 +# e.dns.example.com. 21600 IN A 6.6.6.6 + +0513 8100 +0001 0001 0005 0007 +#(7) e x a m p l e (3) c o m . + 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 +0006 0001 +# same name, fully compressed +c0 0c +# SOA IN TTL=6h RDLENGTH=35 rdata +0006 0001 00005460 0023 01 61 03 64 6e 73 c0 0c 04 72 6f 6f 74 c0 0c 77 bf fc db 00 00 1c 20 00 00 0e 10 00 24 ea 00 00 00 54 60 +#Authority section begin +c0 0c +# NS IN TTL=6h RDLENGTH=4 b.dns.example.com. +0002 0001 00005460 0004 01 62 c0 2b +# NS IN TTL=6h c.dns.example.com. +c0 0c +0002 0001 00005460 00 04 01 63 c0 2b +# NS IN a.dns.example.com. +c0 0c +0002 0001 00005460 00 02 c0 29 +# NS IN e.dns.example.com. +c0 0c +0002 0001 00005460 0004 01 65 c0 2b +# NS IN d.dns.example.com. +c0 0c +0002 0001 00005460 0004 01 64 c0 2b +# additional section begin +# a.dns.example.com. A +c0 29 +0001 0001 00005460 0004 01 01 01 01 +# a.dns.example.com. A +c0 29 +0001 0001 00005460 0004 02 02 02 02 +#b.dns.example.com. A +c0 58 +0001 0001 00002A30 0004 03 03 03 03 +#c.dns.example.com. A +c0 68 +0001 0001 00005460 0004 04 04 04 04 +# d.dns.example.com. A +c0 96 +0001 0001 0000A8C0 0004 05 05 05 05 +# e.dns.example.com. A +c0 86 +0001 0001 00005460 0004 07 07 07 07 +# e.dns.example.com. A +c0 86 +0001 0001 00005460 0004 06 06 06 06 diff --git a/src/lib/cache/tests/testdata/message_fromWire5 b/src/lib/cache/tests/testdata/message_fromWire5 new file mode 100644 index 0000000000000000000000000000000000000000..965f2508c9faf5fdf352487800cf7bb6c6552e3a --- /dev/null +++ b/src/lib/cache/tests/testdata/message_fromWire5 @@ -0,0 +1,36 @@ +# +# A simple DNS response message +# ID = 0x07b2 +# QR=1 (response), Opcode=0, AA=1, RD=1 (other fields are 0) +# QDCOUNT=1, ANCOUNT=2, other COUNTS=0 +# Question: a.example.net. IN A +# Answer: +# ANSWER SECTION: +# a.example.com. 21600 IN CNAME cname.example.com. +# cname.example.com. 21600 IN A 1.1.1.1 +# +# AUTHORITY SECTION: +# example.com. 21600 IN NS a.dns.example.com. +# +# ADDITIONAL SECTION: +# a.dns.example.com. 21600 IN A 1.1.1.1 +# +07b2 8500 +0001 0002 0001 0001 +#(1) a (7) e x a m p l e (3) c o m . + 01 61 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 +# A IN +0001 0001 +# +c0 0c +#CNAME IN TTL RDATA_LEN +0005 0001 00005460 0008 05 63 6e 61 6d 65 c0 0e +# +c0 2b +0001 0001 00005460 0004 01 01 01 01 +# +c0 0e +0002 0001 00005460 0008 01 61 03 64 6e 73 c0 0e +# +c0 4f +0001 0001 00005460 0004 01 01 01 01 diff --git a/src/lib/cache/tests/testdata/message_fromWire6 b/src/lib/cache/tests/testdata/message_fromWire6 new file mode 100644 index 0000000000000000000000000000000000000000..23684ba35ecb3379e7ec3ed7aaa0a034d04e8fc2 --- /dev/null +++ b/src/lib/cache/tests/testdata/message_fromWire6 @@ -0,0 +1,40 @@ +# +# A simple DNS response message +# ID = 0x005e +# QR=1 (response), Opcode=0, AA=1, RD=1 (other fields are 0) +# QDCOUNT=1, ANCOUNT=2, other COUNTS=0 +# Question: a.d.example.net. IN A +# Answer: +# ;; ANSWER SECTION: +# d.example.com. 21600 IN DNAME dname.example.com. +# a.d.example.com. 21600 IN CNAME a.dname.example.com. +# a.dname.example.com. 21600 IN A 1.1.1.1 +# +# ;; AUTHORITY SECTION: +# example.com. 21600 IN NS a.dns.example.com. +# +# ;; ADDITIONAL SECTION: +# a.dns.example.com. 21600 IN A 1.1.1.1 +# +# +005e 8500 +0001 0003 0001 0001 +#(1)a (1) b (7) e x a m p l e (3) c o m . + 01 61 01 64 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 +# A IN +0001 0001 +# +c0 0e +0027 0001 00005460 0013 05 64 6e 61 6d 65 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 +# +c0 0c +0005 0001 00005460 0004 01 61 c0 2d +# +c0 4c +0001 0001 00005460 0004 01 01 01 01 +# +c0 33 +0002 0001 00005460 0008 01 61 03 64 6e 73 c0 33 +# +c0 6c +0001 0001 00005460 0004 01 01 01 01 diff --git a/src/lib/config/tests/testdata/spec22.spec b/src/lib/config/tests/testdata/spec22.spec index be6d51ffac69b0de2a8e987f8ce01e78471acc42..cccd77bfae8fb3b6668bafa6c6fe6fa4c341d523 100644 --- a/src/lib/config/tests/testdata/spec22.spec +++ b/src/lib/config/tests/testdata/spec22.spec @@ -1,6 +1,6 @@ { "module_spec": { - "module_name": "Spec2", + "module_name": "Spec22", "config_data": [ { "item_name": "value1", "item_type": "integer", @@ -81,7 +81,7 @@ { "item_name": "value9", "item_type": "map", "item_optional": false, - "item_default": {}, + "item_default": { "v91": "def", "v92": {} }, "map_item_spec": [ { "item_name": "v91", "item_type": "string", diff --git a/src/lib/datasrc/memory_datasrc.cc b/src/lib/datasrc/memory_datasrc.cc index f0b8b535048dc04243634e48a1fb0eacaf56e560..bbc5166fb31e64ee6f38dc670342e36f385d1f0a 100644 --- a/src/lib/datasrc/memory_datasrc.cc +++ b/src/lib/datasrc/memory_datasrc.cc @@ -35,13 +35,14 @@ namespace datasrc { struct MemoryZone::MemoryZoneImpl { // Constructor MemoryZoneImpl(const RRClass& zone_class, const Name& origin) : - zone_class_(zone_class), origin_(origin) - {} - - // Information about the zone - RRClass zone_class_; - Name origin_; - string file_name_; + zone_class_(zone_class), origin_(origin), origin_data_(NULL), + domains_(true) + { + // We create the node for origin (it needs to exist anyway in future) + domains_.insert(origin, &origin_data_); + DomainPtr origin_domain(new Domain); + origin_data_->setData(origin_domain); + } // Some type aliases /* @@ -61,48 +62,169 @@ struct MemoryZone::MemoryZoneImpl { // The tree stores domains typedef RBTree DomainTree; typedef RBNode DomainNode; + static const DomainNode::Flags DOMAINFLAG_WILD = DomainNode::FLAG_USER1; + + // Information about the zone + RRClass zone_class_; + Name origin_; + DomainNode* origin_data_; + string file_name_; + // The actual zone data DomainTree domains_; + // Add the necessary magic for any wildcard contained in 'name' + // (including itself) to be found in the zone. + // + // In order for wildcard matching to work correctly in find(), + // we must ensure that a node for the wildcarding level exists in the + // backend RBTree. + // E.g. if the wildcard name is "*.sub.example." then we must ensure + // that "sub.example." exists and is marked as a wildcard level. + // Note: the "wildcarding level" is for the parent name of the wildcard + // name (such as "sub.example."). + // + // We also perform the same trick for empty wild card names possibly + // contained in 'name' (e.g., '*.foo.example' in 'bar.*.foo.example'). + void addWildcards(DomainTree& domains, const Name& name) { + Name wname(name); + const unsigned int labels(wname.getLabelCount()); + const unsigned int origin_labels(origin_.getLabelCount()); + for (unsigned int l = labels; + l > origin_labels; + --l, wname = wname.split(1)) { + if (wname.isWildcard()) { + // Ensure a separate level exists for the "wildcarding" name, + // and mark the node as "wild". + DomainNode* node; + DomainTree::Result result(domains.insert(wname.split(1), + &node)); + assert(result == DomainTree::SUCCESS || + result == DomainTree::ALREADYEXISTS); + node->setFlag(DOMAINFLAG_WILD); + + // Ensure a separate level exists for the wildcard name. + // Note: for 'name' itself we do this later anyway, but the + // overhead should be marginal because wildcard names should + // be rare. + result = domains.insert(wname, &node); + assert(result == DomainTree::SUCCESS || + result == DomainTree::ALREADYEXISTS); + } + } + } + /* - * Implementation of longer methods. We put them here, because the - * access is without the impl_-> and it will get inlined anyway. + * Does some checks in context of the data that are already in the zone. + * Currently checks for forbidden combinations of RRsets in the same + * domain (CNAME+anything, DNAME+NS). + * + * If such condition is found, it throws AddError. */ - // Implementation of MemoryZone::add - result::Result add(const ConstRRsetPtr& rrset, DomainTree* domains) { - // Sanitize input + void contextCheck(const ConstRRsetPtr& rrset, + const DomainPtr& domain) const { + // Ensure CNAME and other type of RR don't coexist for the same + // owner name. + if (rrset->getType() == RRType::CNAME()) { + // XXX: this check will become incorrect when we support DNSSEC + // (depending on how we support DNSSEC). We should revisit it + // at that point. + if (!domain->empty()) { + isc_throw(AddError, "CNAME can't be added with other data for " + << rrset->getName()); + } + } else if (domain->find(RRType::CNAME()) != domain->end()) { + isc_throw(AddError, "CNAME and " << rrset->getType() << + " can't coexist for " << rrset->getName()); + } + + /* + * Similar with DNAME, but it must not coexist only with NS and only in + * non-apex domains. + * RFC 2672 section 3 mentions that it is implied from it and RFC 2181 + */ + if (rrset->getName() != origin_ && + // Adding DNAME, NS already there + ((rrset->getType() == RRType::DNAME() && + domain->find(RRType::NS()) != domain->end()) || + // Adding NS, DNAME already there + (rrset->getType() == RRType::NS() && + domain->find(RRType::DNAME()) != domain->end()))) + { + isc_throw(AddError, "DNAME can't coexist with NS in non-apex " + "domain " << rrset->getName()); + } + } + + // Validate rrset before adding it to the zone. If something is wrong + // it throws an exception. It doesn't modify the zone, and provides + // the strong exception guarantee. + void addValidation(const ConstRRsetPtr rrset) { if (!rrset) { isc_throw(NullRRset, "The rrset provided is NULL"); } - if (rrset->getType() == RRType::CNAME() && - rrset->getRdataCount() > 1) { - // XXX: this is not only for CNAME. We should generalize this - // code for all other "singleton RR types" (such as SOA) in a + // Check for singleton RRs. It should probably handled at a different + // in future. + if ((rrset->getType() == RRType::CNAME() || + rrset->getType() == RRType::DNAME()) && + rrset->getRdataCount() > 1) + { + // XXX: this is not only for CNAME or DNAME. We should generalize + // this code for all other "singleton RR types" (such as SOA) in a // separate task. isc_throw(AddError, "multiple RRs of singleton type for " << rrset->getName()); } - Name name(rrset->getName()); - NameComparisonResult compare(origin_.compare(name)); + NameComparisonResult compare(origin_.compare(rrset->getName())); if (compare.getRelation() != NameComparisonResult::SUPERDOMAIN && compare.getRelation() != NameComparisonResult::EQUAL) { - isc_throw(OutOfZone, "The name " << name << + isc_throw(OutOfZone, "The name " << rrset->getName() << " is not contained in zone " << origin_); } + + // Some RR types do not really work well with a wildcard. + // Even though the protocol specifically doesn't completely ban such + // usage, we refuse to load a zone containing such RR in order to + // keep the lookup logic simpler and more predictable. + // See RFC4592 and (for DNAME) draft-ietf-dnsext-rfc2672bis-dname + // for more technical background. Note also that BIND 9 refuses + // NS at a wildcard, so in that sense we simply provide compatible + // behavior. + if (rrset->getName().isWildcard()) { + if (rrset->getType() == RRType::NS()) { + isc_throw(AddError, "Invalid NS owner name (wildcard): " << + rrset->getName()); + } + if (rrset->getType() == RRType::DNAME()) { + isc_throw(AddError, "Invalid DNAME owner name (wildcard): " << + rrset->getName()); + } + } + } + + /* + * Implementation of longer methods. We put them here, because the + * access is without the impl_-> and it will get inlined anyway. + */ + // Implementation of MemoryZone::add + result::Result add(const ConstRRsetPtr& rrset, DomainTree* domains) { + // Sanitize input + addValidation(rrset); + + // Add wildcards possibly contained in the owner name to the domain + // tree. + // Note: this can throw an exception, breaking strong exception + // guarantee. (see also the note for contextCheck() below). + addWildcards(*domains, rrset->getName()); + // Get the node DomainNode* node; - switch (domains->insert(name, &node)) { - // Just check it returns reasonable results - case DomainTree::SUCCESS: - case DomainTree::ALREADYEXISTS: - break; - // Something odd got out - default: - assert(0); - } - assert(node != NULL); + DomainTree::Result result = domains->insert(rrset->getName(), &node); + // Just check it returns reasonable results + assert((result == DomainTree::SUCCESS || + result == DomainTree::ALREADYEXISTS) && node!= NULL); // Now get the domain DomainPtr domain; @@ -114,24 +236,12 @@ struct MemoryZone::MemoryZoneImpl { domain = node->getData(); } - // Ensure CNAME and other type of RR don't coexist for the same - // owner name. + // Checks related to the surrounding data. // Note: when the check fails and the exception is thrown, it may // break strong exception guarantee. At the moment we prefer // code simplicity and don't bother to introduce complicated // recovery code. - if (rrset->getType() == RRType::CNAME()) { - // XXX: this check will become incorrect when we support DNSSEC - // (depending on how we support DNSSEC). We should revisit it - // at that point. - if (!domain->empty()) { - isc_throw(AddError, "CNAME can't be added with other data for " - << rrset->getName()); - } - } else if (domain->find(RRType::CNAME()) != domain->end()) { - isc_throw(AddError, "CNAME and " << rrset->getType() << - " can't coexist for " << rrset->getName()); - } + contextCheck(rrset, domain); // Try inserting the rrset there if (domain->insert(DomainPair(rrset->getType(), rrset)).second) { @@ -139,10 +249,12 @@ struct MemoryZone::MemoryZoneImpl { // If this RRset creates a zone cut at this node, mark the node // indicating the need for callback in find(). - // TBD: handle DNAME, too if (rrset->getType() == RRType::NS() && rrset->getName() != origin_) { - node->enableCallback(); + node->setFlag(DomainNode::FLAG_CALLBACK); + // If it is DNAME, we have a callback as well here + } else if (rrset->getType() == RRType::DNAME()) { + node->setFlag(DomainNode::FLAG_CALLBACK); } return (result::SUCCESS); @@ -174,31 +286,56 @@ struct MemoryZone::MemoryZoneImpl { /// It will be passed to \c zonecutCallback() and record a possible /// zone cut node and related RRset (normally NS or DNAME). struct FindState { - FindState(FindOptions options) : zonecut_node_(NULL), - options_(options) + FindState(FindOptions options) : + zonecut_node_(NULL), + dname_node_(NULL), + options_(options) {} const DomainNode* zonecut_node_; + const DomainNode* dname_node_; ConstRRsetPtr rrset_; const FindOptions options_; }; - // A callback called from possible zone cut nodes. This will be passed - // from the \c find() method to \c RBTree::find(). - static bool zonecutCallback(const DomainNode& node, FindState* state) { - // We perform callback check only for the highest zone cut in the - // rare case of nested zone cuts. - if (state->zonecut_node_ != NULL) { - return (false); + // A callback called from possible zone cut nodes and nodes with DNAME. + // This will be passed from the \c find() method to \c RBTree::find(). + static bool cutCallback(const DomainNode& node, FindState* state) { + // We need to look for DNAME first, there's allowed case where + // DNAME and NS coexist in the apex. DNAME is the one to notice, + // the NS is authoritative, not delegation (corner case explicitly + // allowed by section 3 of 2672) + const Domain::const_iterator foundDNAME(node.getData()->find( + RRType::DNAME())); + if (foundDNAME != node.getData()->end()) { + state->dname_node_ = &node; + state->rrset_ = foundDNAME->second; + // No more processing below the DNAME (RFC 2672, section 3 + // forbids anything to exist below it, so there's no need + // to actually search for it). This is strictly speaking + // a different way than described in 4.1 of that RFC, + // but because of the assumption in section 3, it has the + // same behaviour. + return (true); } - const Domain::const_iterator found(node.getData()->find(RRType::NS())); - if (found != node.getData()->end()) { - // BIND 9 checks if this node is not the origin. But it cannot - // be the origin because we don't enable the callback at the - // origin node (see MemoryZoneImpl::add()). Or should we do a - // double check for it? + // Look for NS + const Domain::const_iterator foundNS(node.getData()->find( + RRType::NS())); + if (foundNS != node.getData()->end()) { + // We perform callback check only for the highest zone cut in the + // rare case of nested zone cuts. + if (state->zonecut_node_ != NULL) { + return (false); + } + + // BIND 9 checks if this node is not the origin. That's probably + // because it can support multiple versions for dynamic updates + // and IXFR, and it's possible that the callback is called at + // the apex and the DNAME doesn't exist for a particular version. + // It cannot happen for us (at least for now), so we don't do + // that check. state->zonecut_node_ = &node; - state->rrset_ = found->second; + state->rrset_ = foundNS->second; // Unless glue is allowed the search stops here, so we return // false; otherwise return true to continue the search. @@ -214,6 +351,35 @@ struct MemoryZone::MemoryZoneImpl { return (false); } + /* + * Prepares a rrset to be return as a result. + * + * If rename is false, it returns the one provided. If it is true, it + * creates a new rrset with the same data but with provided name. + * It is designed for wildcard case, where we create the rrsets + * dynamically. + */ + static ConstRRsetPtr prepareRRset(const Name& name, const ConstRRsetPtr& + rrset, bool rename) + { + if (rename) { + /* + * We lose a signature here. But it would be wrong anyway, because + * the name changed. This might turn out to be unimportant in + * future, because wildcards will probably be handled somehow + * by DNSSEC. + */ + RRsetPtr result(new RRset(name, rrset->getClass(), + rrset->getType(), rrset->getTTL())); + for (RdataIteratorPtr i(rrset->getRdataIterator()); !i->isLast(); + i->next()) { + result->addRdata(i->getCurrent()); + } + return (result); + } else { + return (rrset); + } + } // Implementation of MemoryZone::find FindResult find(const Name& name, RRType type, @@ -222,15 +388,102 @@ struct MemoryZone::MemoryZoneImpl { // Get the node DomainNode* node(NULL); FindState state(options); - switch (domains_.find(name, &node, zonecutCallback, &state)) { + RBTreeNodeChain node_path; + bool rename(false); + switch (domains_.find(name, &node, node_path, cutCallback, &state)) { case DomainTree::PARTIALMATCH: + /* + * In fact, we could use a single variable instead of + * dname_node_ and zonecut_node_. But then we would need + * to distinquish these two cases by something else and + * it seemed little more confusing to me when I wrote it. + * + * Usually at most one of them will be something else than + * NULL (it might happen both are NULL, in which case we + * consider it NOT FOUND). There's one corner case when + * both might be something else than NULL and it is in case + * there's a DNAME under a zone cut and we search in + * glue OK mode ‒ in that case we don't stop on the domain + * with NS and ignore it for the answer, but it gets set + * anyway. Then we find the DNAME and we need to act by it, + * therefore we first check for DNAME and then for NS. In + * all other cases it doesn't matter, as at least one of them + * is NULL. + */ + if (state.dname_node_ != NULL) { + // We were traversing a DNAME node (and wanted to go + // lower below it), so return the DNAME + return (FindResult(DNAME, prepareRRset(name, state.rrset_, + rename))); + } if (state.zonecut_node_ != NULL) { - return (FindResult(DELEGATION, state.rrset_)); + return (FindResult(DELEGATION, prepareRRset(name, + state.rrset_, rename))); + } + + // If the RBTree search stopped at a node for a super domain + // of the search name, it means the search name exists in + // the zone but is empty. Treat it as NXRRSET. + if (node_path.getLastComparisonResult().getRelation() == + NameComparisonResult::SUPERDOMAIN) { + return (FindResult(NXRRSET, ConstRRsetPtr())); + } + + /* + * No redirection anywhere. Let's try if it is a wildcard. + * + * The wildcard is checked after the empty non-terminal domain + * case above, because if that one triggers, it means we should + * not match according to 4.3.3 of RFC 1034 (the query name + * is known to exist). + */ + if (node->getFlag(DOMAINFLAG_WILD)) { + /* Should we cancel this match? + * + * If we compare with some node and get a common ancestor, + * it might mean we are comparing with a non-wildcard node. + * In that case, we check which part is common. If we have + * something in common that lives below the node we got + * (the one above *), then we should cancel the match + * according to section 4.3.3 of RFC 1034 (as the name + * between the wildcard domain and the query name is known + * to exist). + * + * Because the way the tree stores relative names, we will + * have exactly one common label (the ".") in case we have + * nothing common under the node we got and we will get + * more common labels otherwise (yes, this relies on the + * internal RBTree structure, which leaks out through this + * little bit). + * + * If the empty non-terminal node actually exists in the + * tree, then this cancellation is not needed, because we + * will not get here at all. + */ + if (node_path.getLastComparisonResult().getRelation() == + NameComparisonResult::COMMONANCESTOR && node_path. + getLastComparisonResult().getCommonLabels() > 1) { + return (FindResult(NXDOMAIN, ConstRRsetPtr())); + } + Name wildcard(Name("*").concatenate( + node_path.getAbsoluteName())); + DomainTree::Result result(domains_.find(wildcard, &node)); + /* + * Otherwise, why would the DOMAINFLAG_WILD be there if + * there was no wildcard under it? + */ + assert(result = DomainTree::EXACTMATCH); + /* + * We have the wildcard node now. Jump below the switch, + * where handling of the common (exact-match) case is. + * + * However, rename it to the searched name. + */ + rename = true; + break; } - // TODO: we should also cover empty non-terminal cases, which - // will require non trivial code and is deferred for later - // development. For now, we regard any partial match that - // didn't hit a zone cut as "not found". + + // fall through case DomainTree::NOTFOUND: return (FindResult(NXDOMAIN, ConstRRsetPtr())); case DomainTree::EXACTMATCH: // This one is OK, handle it @@ -238,28 +491,35 @@ struct MemoryZone::MemoryZoneImpl { default: assert(0); } - assert(node); - assert(!node->isEmpty()); + assert(node != NULL); + + // If there is an exact match but the node is empty, it's equivalent + // to NXRRSET. + if (node->isEmpty()) { + return (FindResult(NXRRSET, ConstRRsetPtr())); + } Domain::const_iterator found; // If the node callback is enabled, this may be a zone cut. If it - // has a NS RR, we should return a delegation. - if (node->isCallbackEnabled()) { + // has a NS RR, we should return a delegation, but not in the apex. + if (node->getFlag(DomainNode::FLAG_CALLBACK) && node != origin_data_) { found = node->getData()->find(RRType::NS()); if (found != node->getData()->end()) { - return (FindResult(DELEGATION, found->second)); + return (FindResult(DELEGATION, prepareRRset(name, + found->second, rename))); } } // handle type any query - if (target && !node->getData()->empty()) { + if (target != NULL && !node->getData()->empty()) { // Empty domain will be handled as NXRRSET by normal processing for (found = node->getData()->begin(); found != node->getData()->end(); found++) { target->addRRset( - boost::const_pointer_cast(found->second)); + boost::const_pointer_cast(prepareRRset(name, + found->second, rename))); } return (FindResult(SUCCESS, ConstRRsetPtr())); } @@ -267,12 +527,14 @@ struct MemoryZone::MemoryZoneImpl { found = node->getData()->find(type); if (found != node->getData()->end()) { // Good, it is here - return (FindResult(SUCCESS, found->second)); + return (FindResult(SUCCESS, prepareRRset(name, found->second, + rename))); } else { // Next, try CNAME. found = node->getData()->find(RRType::CNAME()); if (found != node->getData()->end()) { - return (FindResult(CNAME, found->second)); + return (FindResult(CNAME, prepareRRset(name, found->second, + rename))); } } // No exact match or CNAME. Return NXRRSET. diff --git a/src/lib/datasrc/rbtree.h b/src/lib/datasrc/rbtree.h index 643b185af5a16d7862d64065fbb9a143be1d76ac..bd04066f55162715b70765465abe742d6316d19a 100644 --- a/src/lib/datasrc/rbtree.h +++ b/src/lib/datasrc/rbtree.h @@ -23,12 +23,15 @@ /// issue, the design and interface are not fixed, and RBTree isn't ready /// to be used as a base data structure by other modules. +#include + #include #include #include -#include +#include #include #include +#include namespace isc { namespace datasrc { @@ -54,7 +57,9 @@ operator-(const isc::dns::Name& super_name, const isc::dns::Name& sub_name) { } } -template +/// Forward declare RBTree class here is convinent for following friend +/// class declare inside RBNode and RBTreeNodeChain +template class RBTree; /// \brief \c RBNode is used by RBTree to store any data related to one domain @@ -82,8 +87,7 @@ class RBNode : public boost::noncopyable { private: /// The RBNode is meant for use from within RBTree, so it has access to /// it. - template - friend class RBTree; + friend class RBTree; /// \name Constructors /// @@ -108,6 +112,29 @@ public: /// \brief Alias for shared pointer to the data. typedef boost::shared_ptr NodeDataPtr; + /// Node flags. + /// + /// Each flag value defines a non default property for a specific node. + /// These are defined as bitmask type values for the convenience of + /// internal implementation, but applications are expected to use + /// each flag separately via the enum definitions. + /// + /// All (settable) flags are off by default; they must be explicitly + /// set to on by the \c setFlag() method. + enum Flags { + FLAG_CALLBACK = 1, ///< Callback enabled. See \ref callback + FLAG_USER1 = 0x80000000U ///< Application specific flag + }; +private: + // Some flag values are expected to be used for internal purposes + // (e.g., representing the node color) in future versions, so we + // limit the settable flags via the \c setFlag() method to those + // explicitly defined in \c Flags. This constant represents all + // such flags. + static const uint32_t SETTABLE_FLAGS = (FLAG_CALLBACK | FLAG_USER1); + +public: + /// \brief Destructor /// /// It might seem strange that constructors are private and destructor @@ -142,6 +169,7 @@ public: /// non-terminal domains, but it is possible (yet probably meaningless) /// empty nodes anywhere. bool isEmpty() const { return (data_.get() == NULL); } + //@} /// \name Setter functions. @@ -150,6 +178,52 @@ public: void setData(const NodeDataPtr& data) { data_ = data; } //@} + /// \name Node flag manipulation methods + //@{ + /// Get the status of a node flag. + /// + /// This method returns whether the given node flag is set (enabled) + /// on the node. The \c flag parameter is expected to be one of the + /// defined \c Flags constants. For simplicity, the method interface + /// does not prohibit passing an undefined flag or combined flags, but + /// the return value in such a case will be meaningless for the caller + /// (an application would have to use an ugly cast for such an unintended + /// form of call, which will hopefully avoid accidental misuse). + /// + /// \exception None + /// \param flag The flag to be tested. + /// \return \c true if the \c flag is set; \c false otherwise. + bool getFlag(Flags flag) const { + return ((flags_ & flag) != 0); + } + + /// Set or clear a node flag. + /// + /// This method changes the status of the specified node flag to either + /// "on" (enabled) or "off" (disabled). The new status is specified by + /// the \c on parameter. + /// Like the \c getFlag() method, \c flag is expected to be one of the + /// defined \c Flags constants. If an undefined or unsettable flag is + /// specified, \c isc::InvalidParameter exception will be thrown. + /// + /// \exception isc::InvalidParameter Unsettable flag is specified + /// \exception None otherwise + /// \param flag The node flag to be changed. + /// \on If \c true, set the flag to on; otherwise set it to off. + void setFlag(Flags flag, bool on = true) { + if ((flag & ~SETTABLE_FLAGS) != 0) { + isc_throw(isc::InvalidParameter, + "Unsettable RBTree flag is being set"); + } + if (on) { + flags_ |= flag; + } else { + flags_ &= ~flag; + } + } + //@} + +private: /// \name Callback related methods /// /// See the description of \c RBTree::find() about callbacks. @@ -157,16 +231,8 @@ public: /// These methods never throw an exception. //@{ /// Return if callback is enabled at the node. - bool isCallbackEnabled() const { return (callback_required_); } - - /// Enable callback at the node. - void enableCallback() { callback_required_ = true; } - - /// Disable callback at the node. - void disableCallback() { callback_required_ = false; } //@} - private: /// \brief Define rbnode color enum RBNodeColor {BLACK, RED}; @@ -176,6 +242,23 @@ private: return (&null_node); } + /// \brief return the next node which is bigger than current node + /// in the same subtree + /// + /// The next successor for this node is the next bigger node in terms of + /// the DNSSEC order relation within the same single subtree. + /// Note that it may NOT be the next bigger node in the entire RBTree; + /// RBTree is a tree in tree, and the real next node may reside in + /// an upper or lower subtree of the subtree where this node belongs. + /// For example, if this node has a sub domain, the real next node is + /// the smallest node in the sub domain tree. + /// + /// If this node is the biggest node within the subtree, this method + /// returns \c NULL_NODE(). + /// + /// This method never throws an exception. + const RBNode* successor() const; + /// \name Data to maintain the rbtree structure. //@{ RBNode* parent_; @@ -204,7 +287,7 @@ private: /// RBTree::find(). /// /// \todo It might be needed to put it into more general attributes field. - bool callback_required_; + uint32_t flags_; }; @@ -218,7 +301,7 @@ RBNode::RBNode() : // dummy name, the value doesn't matter: name_(isc::dns::Name::ROOT_NAME()), down_(this), - callback_required_(false) + flags_(0) { } @@ -230,7 +313,7 @@ RBNode::RBNode(const isc::dns::Name& name) : color_(RED), name_(name), down_(NULL_NODE()), - callback_required_(false) + flags_(0) { } @@ -239,6 +322,230 @@ template RBNode::~RBNode() { } +template +const RBNode* +RBNode::successor() const { + const RBNode* current = this; + // If it has right node, the successor is the left-most node of the right + // subtree. + if (right_ != NULL_NODE()) { + current = right_; + while (current->left_ != NULL_NODE()) { + current = current->left_; + } + return (current); + } + + + // Otherwise go up until we find the first left branch on our path to + // root. If found, the parent of the branch is the successor. + // Otherwise, we return the null node + const RBNode* parent = current->parent_; + while (parent != NULL_NODE() && current == parent->right_) { + current = parent; + parent = parent->parent_; + } + return (parent); +} + + +/// \brief RBTreeNodeChain stores detailed information of \c RBTree::find() +/// result. +/// +/// - The \c RBNode that was last compared with the search name, and +/// the comparison result at that point in the form of +/// \c isc::dns::NameComparisonResult. +/// - A sequence of nodes that forms a path to the found node (which is +/// not yet implemented). +/// +/// The comparison result can be used to handle some rare cases such as +/// empty node processing. +/// The node sequence keeps track of the nodes to reach any given node from +/// the root of RBTree. +/// +/// Currently, RBNode does not have "up" pointers in them (i.e., back pointers +/// from the root of one level of tree of trees to the node in the parent +/// tree whose down pointer points to that root node) for memory usage +/// reasons, so there is no other way to find the path back to the root from +/// any given RBNode. +/// +/// \note This design may change in future versions. In particular, it's +/// quite likely we want to have that pointer if we want to optimize name +/// compression by exploiting the structure of the zone. If and when that +/// happens we should also revisit the need for the chaining. +/// Also, the class name may not be appropriate now that it contains other +/// information than a node "chain", and the chain itself may even be +/// deprecated. Something like "RBTreeFindContext" may be a better name. +/// This point should be revisited later. +/// +/// RBTreeNodeChain is constructed and manipulated only inside the \c RBTree +/// class. +/// \c RBTree uses it as an inner data structure to iterate over the whole +/// RBTree. +/// This is the reason why manipulation methods such as \c push() and \c pop() +/// are private (and not shown in the doxygen document). +template +class RBTreeNodeChain { + /// RBTreeNodeChain is initialized by RBTree, only RBTree has + /// knowledge to manipuate it. + friend class RBTree; +public: + /// \name Constructors and Assignment Operator. + /// + /// \note The copy constructor and the assignment operator are + /// intentionally defined as private, making this class non copyable. + /// This may have to be changed in a future version with newer need. + /// For now we explicitly disable copy to avoid accidental copy happens + /// unintentionally. + //{@ + /// The default constructor. + /// + /// \exception None + RBTreeNodeChain() : node_count_(0), last_compared_(NULL), + // XXX: meaningless initial values: + last_comparison_(0, 0, + isc::dns::NameComparisonResult::EQUAL) + {} + +private: + RBTreeNodeChain(const RBTreeNodeChain&); + RBTreeNodeChain& operator=(const RBTreeNodeChain&); + //@} + +public: + /// Clear the state of the chain. + /// + /// This method re-initializes the internal state of the chain so that + /// it can be reused for subsequent operations. + /// + /// \exception None + void clear() { + node_count_ = 0; + last_compared_ = NULL; + } + + /// Return the \c RBNode that was last compared in \c RBTree::find(). + /// + /// If this chain has been passed to \c RBTree::find() and there has + /// been name comparison against the search name, the last compared + /// \c RBNode is recorded within the chain. This method returns that + /// node. + /// If \c RBTree::find() hasn't been called with this chain or name + /// comparison hasn't taken place (which is possible if the tree is empty), + /// this method returns \c NULL. + /// + /// \exception None + const RBNode* getLastComparedNode() const { + return (last_compared_); + } + + /// Return the result of last name comparison in \c RBTree::find(). + /// + /// Like \c getLastComparedNode(), \c RBTree::find() records the result + /// of the last name comparison in the chain. This method returns the + /// result. + /// The return value of this method is only meaningful when comparison + /// has taken place, i.e, when \c getLastComparedNode() would return a + /// non \c NULL value. + /// + /// \exception None + const isc::dns::NameComparisonResult& getLastComparisonResult() const { + return (last_comparison_); + } + + /// \brief Return the number of levels stored in the chain. + /// + /// It's equal to the number of nodes in the chain; for an empty + /// chain, 0 will be returned. + /// + /// \exception None + unsigned int getLevelCount() const { return (node_count_); } + + /// \brief return the absolute name for the node which this + /// \c RBTreeNodeChain currently refers to. + /// + /// The chain must not be empty. + /// + /// \exception isc::BadValue the chain is empty. + /// \exception std::bad_alloc memory allocation for the new name fails. + isc::dns::Name getAbsoluteName() const { + if (isEmpty()) { + isc_throw(isc::BadValue, + "RBTreeNodeChain::getAbsoluteName is called on an empty " + "chain"); + } + + const RBNode* top_node = top(); + isc::dns::Name absolute_name = top_node->getName(); + int node_count = node_count_ - 1; + while (node_count > 0) { + top_node = nodes_[node_count - 1]; + absolute_name = absolute_name.concatenate(top_node->getName()); + --node_count; + } + return (absolute_name); + } + +private: + // the following private functions check invariants about the internal + // state using assert() instead of exception. The state of a chain + // can only be modified operations within this file, so if any of the + // assumptions fails it means an internal bug. + + /// \brief return whther node chain has node in it. + /// + /// \exception None + bool isEmpty() const { return (node_count_ == 0); } + + /// \brief return the top node for the node chain + /// + /// RBTreeNodeChain store all the nodes along top node to + /// root node of RBTree + /// + /// \exception None + const RBNode* top() const { + assert(!isEmpty()); + return (nodes_[node_count_ - 1]); + } + + /// \brief pop the top node from the node chain + /// + /// After pop, up/super node of original top node will be + /// the top node + /// + /// \exception None + void pop() { + assert(!isEmpty()); + --node_count_; + } + + /// \brief add the node into the node chain + /// + /// If the node chain isn't empty, the node should be + /// the sub domain of the original top node in node chain + /// otherwise the node should be the root node of RBTree. + /// + /// \exception None + void push(const RBNode* node) { + assert(node_count_ < RBT_MAX_LEVEL); + nodes_[node_count_++] = node; + } + +private: + // The max label count for one domain name is Name::MAX_LABELS (128). + // Since each node in rbtree stores at least one label, and the root + // name always shares the same level with some others (which means + // all top level nodes except the one for the root name contain at least + // two labels), the possible maximum level is MAX_LABELS - 1. + // It's also the possible maximum nodes stored in a chain. + const static int RBT_MAX_LEVEL = isc::dns::Name::MAX_LABELS - 1; + + int node_count_; + const RBNode* nodes_[RBT_MAX_LEVEL]; + const RBNode* last_compared_; + isc::dns::NameComparisonResult last_comparison_; +}; + // note: the following class description is documented using multiline comments // because the verbatim diagram contain a backslash, which could be interpreted @@ -258,11 +565,16 @@ RBNode::~RBNode() { * - Decreases the memory footprint, as it doesn't store the suffix labels * multiple times. * - * Depending on different usage, rbtree will support different search policy. - * Whether return empty node to end user is one policy among them. Search - * policy is as the last template parameter, the default policy will NOT - * return empty node to end user, pass ture will get empty node during find - * is needed + * Depending on different usage, rbtree will support different search policies. + * Whether to return an empty node to end user is one policy among them. + * The default policy is to NOT return an empty node to end user; + * to change the behavior, specify \c true for the constructor parameter + * \c returnEmptyNode. + * \note The search policy only affects the \c find() behavior of RBTree. + * When inserting one name into RBTree, if the node with the name already + * exists in the RBTree and it's an empty node which doesn't have any data, + * the \c insert() method will still return \c ALREADYEXISTS regardless of + * the search policy. * * \anchor diagram * @@ -277,7 +589,7 @@ RBNode::~RBNode() { * - p.w.y.d.e.f * - q.w.y.d.e.f * - * the tree will looks like: + * the tree will look like: * \verbatim b / \ @@ -297,10 +609,8 @@ RBNode::~RBNode() { * - add remove interface * - add iterator to iterate over the whole \c RBTree. This may be necessary, * for example, to support AXFR. - * - since \c RBNode only has down pointer without up pointer, the node path - * during finding should be recorded for later use */ -template +template class RBTree : public boost::noncopyable { friend class RBNode; public: @@ -320,7 +630,7 @@ public: /// The constructor. /// /// It never throws an exception. - explicit RBTree(); + explicit RBTree(bool returnEmptyNode = false); /// \b Note: RBTree is not intended to be inherited so the destructor /// is not virtual @@ -333,22 +643,25 @@ public: /// /// \anchor find /// - /// These methods search the RBTree for a node whose name is a longest + /// These methods search the RBTree for a node whose name is longest /// against name. The found node, if any, is returned via the node pointer. /// /// By default, nodes that don't have data (see RBNode::isEmpty) are /// ignored and the result can be NOTFOUND even if there's a node whose - /// name mathes. The plan is to introduce a "no data OK" mode for this - /// method, that would match any node of the tree regardless of wheather - /// the node has any data or not. + /// name matches. If the \c RBTree is constructed with its + /// \c returnEmptyNode parameter being \c true, an empty node will also + /// be match candidates. /// - /// The case with "no data OK" mode is not as easy as it seems. For example - /// in the diagram shown in the class description, the name y.d.e.f is - /// logically contained in the tree as part of the node w.y. It cannot be - /// identified simply by checking whether existing nodes (such as - /// d.e.f or w.y) has data. + /// \note Even when \c returnEmptyNode is \c true, not all empty nodes + /// in terms of the DNS protocol may necessarily be found by this method. + /// For example, in the \ref diagram shown in the class description, + /// the name y.d.e.f is logically contained in the tree as part of the + /// node w.y, but the \c find() variants cannot find the former for + /// the search key of y.d.e.f, no matter how the \c RBTree is constructed. + /// The caller of this method must use a different way to identify the + /// hidden match when necessary. /// - /// These methods involves operations on names that can throw an exception. + /// These methods involve operations on names that can throw an exception. /// If that happens the exception will be propagated to the caller. /// The callback function should generally not throw an exception, but /// if it throws, the exception will be propagated to the caller. @@ -369,13 +682,39 @@ public: /// of it. In that case, node parameter is left intact. //@{ - /// \brief Find with callback. + /// \brief Simple find. + /// + /// Acts as described in the \ref find section. + Result find(const isc::dns::Name& name, RBNode** node) const { + RBTreeNodeChain node_path; + return (find(name, node, node_path, NULL, NULL)); + } + + /// \brief Simple find returning immutable node. + /// + /// Acts as described in the \ref find section, but returns immutable node + /// pointer. + Result find(const isc::dns::Name& name, const RBNode** node) const { + RBTreeNodeChain node_path; + RBNode *target_node = NULL; + Result ret = (find(name, &target_node, node_path, NULL, NULL)); + if (ret != NOTFOUND) { + *node = target_node; + } + return (ret); + } + + /// \brief Find with callback and node chain. /// - /// \anchor callback + /// This version of \c find() is specifically designed for the backend + /// of the \c MemoryZone class, and implements all necessary features + /// for that purpose. Other applications shouldn't need these additional + /// features, and should normally use the simpler versions. /// - /// This version of find calls the callback whenever traversing (on the - /// way from root down the tree) a marked node on the way down through the - /// domain namespace (see RBNode::enableCallback and related functions). + /// This version of \c find() calls the callback whenever traversing (on + /// the way from root down the tree) a marked node on the way down through + /// the domain namespace (see \c RBNode::enableCallback and related + /// functions). /// /// If you return true from the callback, the search is stopped and a /// PARTIALMATCH is returned with the given node. Note that this node @@ -388,9 +727,38 @@ public: /// The callbacks are not general functors for the same reason - we don't /// expect it to be needed. /// + /// Another special feature of this version is the ability to record + /// more detailed information regarding the search result. + /// + /// This information will be returned via the \c node_path parameter, + /// which is an object of class \c RBTreeNodeChain. + /// The passed parameter must be empty. + /// + /// \note The rest of the description isn't yet implemented. It will be + /// handled in Trac ticket #517. + /// + /// On success, the node sequence stoed in \c node_path will contain all + /// the ancestor nodes from the found node towards the root. + /// For example, if we look for o.w.y.d.e.f in the example \ref diagram, + /// \c node_path will contain w.y and d.e.f; the \c top() node of the + /// chain will be o, w.f and d.e.f will be stored below it. + /// + /// This feature can be used to get the absolute name for a node; + /// to do so, we need to travel upside from the node toward the root, + /// concatenating all ancestor names. With the current implementation + /// it's not possible without a node chain, because there is a no pointer + /// from the root of a subtree to the parent subtree (this may change + /// in a future version). A node chain can also be used to find the next + /// node of a given node in the entire RBTree; the \c nextNode() method + /// takes a node chain as a parameter. + /// + /// \exception isc::BadValue node_path is not empty (not yet implemented). + /// /// \param name Target to be found /// \param node On success (either \c EXACTMATCH or \c PARTIALMATCH) /// it will store a pointer to the matching node + /// \param node_path Other search details will be stored (see the + /// description) /// \param callback If non \c NULL, a call back function to be called /// at marked nodes (see above). /// \param callback_arg A caller supplied argument to be passed to @@ -399,33 +767,57 @@ public: /// \return As described above, but in case of callback returning true, /// it returns immediately with the current node. template - Result find(const isc::dns::Name& name, RBNode** node, + Result find(const isc::dns::Name& name, + RBNode** node, + RBTreeNodeChain& node_path, bool (*callback)(const RBNode&, CBARG), CBARG callback_arg) const; - /// \brief Find with callback returning immutable node. + /// \brief Simple find returning immutable node. /// - /// It has the same behaviour as the find with \ref callback version. + /// Acts as described in the \ref find section, but returns immutable + /// node pointer. template - Result find(const isc::dns::Name& name, const RBNode** node, + Result find(const isc::dns::Name& name, + const RBNode** node, + RBTreeNodeChain& node_path, bool (*callback)(const RBNode&, CBARG), - CBARG callback_arg) const; - - /// \brief Simple find. - /// - /// Acts as described in the \ref find section. - Result find(const isc::dns::Name& name, RBNode** node) const { - return (find(name, node, NULL, NULL)); + CBARG callback_arg) const + { + RBNode* target_node = NULL; + Result ret = find(name, &target_node, node_path, callback, + callback_arg); + if (ret != NOTFOUND) { + *node = target_node; + } + return (ret); } + //@} - /// \brieg Simple find returning immutable node. + /// \brief return the next bigger node in DNSSEC order from a given node + /// chain. /// - /// Acts as described in the \ref find section, but returns immutable node - /// pointer. - Result find(const isc::dns::Name& name, const RBNode** node) const { - return (find(name, node, NULL, NULL)); - } - //@} + /// This method identifies the next bigger node of the node currently + /// referenced in \c node_path and returns it. + /// This method also updates the passed \c node_path so that it will store + /// the path for the returned next node. + /// It will be convenient when we want to iterate over the all nodes + /// of \c RBTree; we can do this by calling this method repeatedly + /// starting from the root node. + /// + /// \note \c nextNode() will iterate over all the nodes in RBTree including + /// empty nodes. If empty node isn't desired, it's easy to add logic to + /// check return node and keep invoking \c nextNode() until the non-empty + /// node is retrieved. + /// + /// \exception isc::BadValue node_path is empty. + /// + /// \param node_path A node chain that stores all the nodes along the path + /// from root to node. + /// + /// \return An \c RBNode that is next bigger than \c node; if \c node is + /// the largest, \c NULL will be returned. + const RBNode* nextNode(RBTreeNodeChain& node_path) const; /// \brief Get the total number of nodes in the tree /// @@ -502,23 +894,11 @@ private: //@{ /// \brief delete tree whose root is equal to node void deleteHelper(RBNode *node); - /// \brief find the node with name - /// - /// Internal searching function. - /// - /// \param name What should be found. - /// \param up It will point to the node whose down pointer points - /// to the tree containing node. If we looked for o.w.y.d.e.f in the - /// \ref diagram, the up would point to the w.y node. - /// This parameter is not used currently, but it will be soon. - /// \param node The found node. - template - Result findHelper(const isc::dns::Name& name, const RBNode** up, - RBNode** node, - bool (*callback)(const RBNode&, CBARG), - CBARG callback_arg) const; + + /// \brief Print the information of given RBNode. void dumpTreeHelper(std::ostream& os, const RBNode* node, unsigned int depth) const; + /// \brief Indentation helper function for dumpTree static void indent(std::ostream& os, unsigned int depth); @@ -529,39 +909,43 @@ private: void nodeFission(RBNode& node, const isc::dns::Name& sub_name); //@} - RBNode* root_; RBNode* NULLNODE; + RBNode* root_; /// the node count of current tree unsigned int node_count_; + /// search policy for rbtree + const bool needsReturnEmptyNode_; }; -template -RBTree::RBTree() { - NULLNODE = RBNode::NULL_NODE(); - root_ = NULLNODE; - node_count_ = 0; +template +RBTree::RBTree(bool returnEmptyNode) : + NULLNODE(RBNode::NULL_NODE()), + root_(NULLNODE), + node_count_(0), + needsReturnEmptyNode_(returnEmptyNode) +{ } -template -RBTree::~RBTree() { +template +RBTree::~RBTree() { deleteHelper(root_); assert(node_count_ == 0); } -template -void -RBTree::deleteHelper(RBNode *root) { +template +void +RBTree::deleteHelper(RBNode* root) { if (root == NULLNODE) { return; } - RBNode *node = root; + RBNode* node = root; while (root->left_ != NULLNODE || root->right_ != NULLNODE) { while (node->left_ != NULLNODE || node->right_ != NULLNODE) { node = (node->left_ != NULLNODE) ? node->left_ : node->right_; } - RBNode *parent = node->parent_; + RBNode* parent = node->parent_; if (parent->left_ == node) { parent->left_ = NULLNODE; } else { @@ -579,79 +963,58 @@ RBTree::deleteHelper(RBNode *root) { --node_count_; } -template +template template -typename RBTree::Result -RBTree::find(const isc::dns::Name& name, RBNode** node, +typename RBTree::Result +RBTree::find(const isc::dns::Name& target_name, + RBNode** target, + RBTreeNodeChain& node_path, bool (*callback)(const RBNode&, CBARG), CBARG callback_arg) const { - const RBNode* up_node = NULLNODE; - return (findHelper(name, &up_node, node, callback, callback_arg)); -} + using namespace helper; -template -template -typename RBTree::Result -RBTree::find(const isc::dns::Name& name, const RBNode** node, - bool (*callback)(const RBNode&, CBARG), - CBARG callback_arg) const -{ - const RBNode* up_node; - RBNode* target_node; - const typename RBTree::Result ret = - findHelper(name, &up_node, &target_node, callback, callback_arg); - if (ret != NOTFOUND) { - *node = target_node; + if (!node_path.isEmpty()) { + isc_throw(isc::BadValue, "RBTree::find is given a non empty chain"); } - return (ret); -} - -template -template -typename RBTree::Result -RBTree::findHelper(const isc::dns::Name& target_name, - const RBNode** up_node, - RBNode** target, - bool (*callback)(const RBNode&, CBARG), - CBARG callback_arg) const -{ - using namespace helper; RBNode* node = root_; - typename RBTree::Result ret = NOTFOUND; - *up_node = NULLNODE; + Result ret = NOTFOUND; isc::dns::Name name = target_name; while (node != NULLNODE) { - const isc::dns::NameComparisonResult compare_result = - name.compare(node->name_); + node_path.last_compared_ = node; + node_path.last_comparison_ = name.compare(node->name_); const isc::dns::NameComparisonResult::NameRelation relation = - compare_result.getRelation(); + node_path.last_comparison_.getRelation(); + if (relation == isc::dns::NameComparisonResult::EQUAL) { - if (returnEmptyNode || !node->isEmpty()) { + if (needsReturnEmptyNode_ || !node->isEmpty()) { + node_path.push(node); *target = node; ret = EXACTMATCH; } break; } else { - const int common_label_count = compare_result.getCommonLabels(); + const int common_label_count = + node_path.last_comparison_.getCommonLabels(); // If the common label count is 1, there is no common label between // the two names, except the trailing "dot". if (common_label_count == 1) { - node = (compare_result.getOrder() < 0) ? + node = (node_path.last_comparison_.getOrder() < 0) ? node->left_ : node->right_; } else if (relation == isc::dns::NameComparisonResult::SUBDOMAIN) { - if (returnEmptyNode || !node->isEmpty()) { - ret = RBTree::PARTIALMATCH; + if (needsReturnEmptyNode_ || !node->isEmpty()) { + ret = PARTIALMATCH; *target = node; - if (callback != NULL && node->callback_required_) { + if (callback != NULL && + node->getFlag(RBNode::FLAG_CALLBACK)) { if ((callback)(*node, callback_arg)) { break; } } } - *up_node = node; + node_path.push(node); name = name - node->name_; node = node->down_; } else { @@ -663,11 +1026,54 @@ RBTree::findHelper(const isc::dns::Name& target_name, return (ret); } +template +const RBNode* +RBTree::nextNode(RBTreeNodeChain& node_path) const { + if (node_path.isEmpty()) { + isc_throw(isc::BadValue, "RBTree::nextNode is given an empty chain"); + } -template -typename RBTree::Result -RBTree::insert(const isc::dns::Name& target_name, - RBNode** new_node) { + const RBNode* node = node_path.top(); + // if node has sub domain, the next domain is the smallest + // domain in sub domain tree + if (node->down_ != NULLNODE) { + const RBNode* left_most = node->down_; + while (left_most->left_ != NULLNODE) { + left_most = left_most->left_; + } + node_path.push(left_most); + return (left_most); + } + + // node_path go to up level + node_path.pop(); + // otherwise found the successor node in current level + const RBNode* successor = node->successor(); + if (successor != NULLNODE) { + node_path.push(successor); + return (successor); + } + + // if no successor found move to up level, the next successor + // is the successor of up node in the up level tree, if + // up node doesn't have successor we gonna keep moving to up + // level + while (!node_path.isEmpty()) { + const RBNode* up_node_successor = node_path.top()->successor(); + node_path.pop(); + if (up_node_successor != NULLNODE) { + node_path.push(up_node_successor); + return (up_node_successor); + } + } + + return (NULL); +} + + +template +typename RBTree::Result +RBTree::insert(const isc::dns::Name& target_name, RBNode** new_node) { using namespace helper; RBNode* parent = NULLNODE; RBNode* current = root_; @@ -684,12 +1090,7 @@ RBTree::insert(const isc::dns::Name& target_name, if (new_node != NULL) { *new_node = current; } - - if (current->isEmpty() && !returnEmptyNode) { - return (SUCCESS); - } else { - return (ALREADYEXISTS); - } + return (ALREADYEXISTS); } else { const int common_label_count = compare_result.getCommonLabels(); if (common_label_count == 1) { @@ -746,9 +1147,9 @@ RBTree::insert(const isc::dns::Name& target_name, } -template +template void -RBTree::nodeFission(RBNode& node, const isc::dns::Name& base_name) { +RBTree::nodeFission(RBNode& node, const isc::dns::Name& base_name) { using namespace helper; const isc::dns::Name sub_name = node.name_ - base_name; // using auto_ptr here is to avoid memory leak in case of exception raised @@ -759,7 +1160,7 @@ RBTree::nodeFission(RBNode& node, const isc::dns::Name& base_name) { // consistent behavior (i.e., a weak form of strong exception guarantee) // even if code after the call to this function throws an exception. std::swap(node.data_, down_node->data_); - std::swap(node.callback_required_, down_node->callback_required_); + std::swap(node.flags_, down_node->flags_); down_node->down_ = node.down_; node.down_ = down_node.get(); // root node of sub tree, the initial color is BLACK @@ -769,9 +1170,9 @@ RBTree::nodeFission(RBNode& node, const isc::dns::Name& base_name) { } -template +template void -RBTree::insertRebalance(RBNode** root, RBNode* node) { +RBTree::insertRebalance(RBNode** root, RBNode* node) { RBNode* uncle; while (node != *root && node->parent_->color_ == RBNode::RED) { @@ -815,9 +1216,9 @@ RBTree::insertRebalance(RBNode** root, RBNode* node) { } -template +template RBNode* -RBTree::leftRotate(RBNode** root, RBNode* node) { +RBTree::leftRotate(RBNode** root, RBNode* node) { RBNode* right = node->right_; node->right_ = right->left_; if (right->left_ != NULLNODE) @@ -840,9 +1241,9 @@ RBTree::leftRotate(RBNode** root, RBNode* node) { return (node); } -template +template RBNode* -RBTree::rightRotate(RBNode** root, RBNode* node) { +RBTree::rightRotate(RBNode** root, RBNode* node) { RBNode* left = node->left_; node->left_ = left->right_; if (left->right_ != NULLNODE) @@ -865,17 +1266,17 @@ RBTree::rightRotate(RBNode** root, RBNode* node) { } -template +template void -RBTree::dumpTree(std::ostream& os, unsigned int depth) const { +RBTree::dumpTree(std::ostream& os, unsigned int depth) const { indent(os, depth); os << "tree has " << node_count_ << " node(s)\n"; dumpTreeHelper(os, root_, depth); } -template +template void -RBTree::dumpTreeHelper(std::ostream& os, const RBNode* node, +RBTree::dumpTreeHelper(std::ostream& os, const RBNode* node, unsigned int depth) const { if (node == NULLNODE) { @@ -900,15 +1301,13 @@ RBTree::dumpTreeHelper(std::ostream& os, const RBNode* node, dumpTreeHelper(os, node->right_, depth + 1); } -template +template void -RBTree::indent(std::ostream& os, unsigned int depth) { +RBTree::indent(std::ostream& os, unsigned int depth) { static const unsigned int INDENT_FOR_EACH_DEPTH = 5; os << std::string(depth * INDENT_FOR_EACH_DEPTH, ' '); } - - } } diff --git a/src/lib/datasrc/tests/memory_datasrc_unittest.cc b/src/lib/datasrc/tests/memory_datasrc_unittest.cc index 8d1703db507384a7385c7500546cb354632b3df3..16d749c6c1724b6bc60f8023b34a1007b4de0e7a 100644 --- a/src/lib/datasrc/tests/memory_datasrc_unittest.cc +++ b/src/lib/datasrc/tests/memory_datasrc_unittest.cc @@ -1,4 +1,5 @@ // Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2011 CZ NIC // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -12,8 +13,14 @@ // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. +#include +#include + +#include + #include +#include #include #include #include @@ -26,6 +33,7 @@ #include +using namespace std; using namespace isc::dns; using namespace isc::dns::rdata; using namespace isc::datasrc; @@ -139,51 +147,91 @@ TEST_F(MemoryDataSrcTest, getZoneCount) { EXPECT_EQ(2, memory_datasrc.getZoneCount()); } +// A helper callback of masterLoad() used in MemoryZoneTest. +void +setRRset(RRsetPtr rrset, vector::iterator& it) { + *(*it) = rrset; + ++it; +} + /// \brief Test fixture for the MemoryZone class class MemoryZoneTest : public ::testing::Test { + // A straightforward pair of textual RR(set) and a RRsetPtr variable + // to store the RRset. Used to build test data below. + struct RRsetData { + const char* const text; // textual representation of an RRset + RRsetPtr* rrset; + }; public: MemoryZoneTest() : class_(RRClass::IN()), origin_("example.org"), - ns_name_("ns.example.org"), - cname_name_("cname.example.org"), - child_ns_name_("child.example.org"), - child_glue_name_("ns.child.example.org"), - grandchild_ns_name_("grand.child.example.org"), - grandchild_glue_name_("ns.grand.child.example.org"), - zone_(class_, origin_), - rr_out_(new RRset(Name("example.com"), class_, RRType::A(), - RRTTL(300))), - rr_ns_(new RRset(origin_, class_, RRType::NS(), RRTTL(300))), - rr_ns_a_(new RRset(ns_name_, class_, RRType::A(), RRTTL(300))), - rr_ns_aaaa_(new RRset(ns_name_, class_, RRType::AAAA(), RRTTL(300))), - rr_a_(new RRset(origin_, class_, RRType::A(), RRTTL(300))), - rr_cname_(new RRset(cname_name_, class_, RRType::CNAME(), RRTTL(300))), - rr_cname_a_(new RRset(cname_name_, class_, RRType::A(), RRTTL(300))), - rr_child_ns_(new RRset(child_ns_name_, class_, RRType::NS(), - RRTTL(300))), - rr_child_glue_(new RRset(child_glue_name_, class_, RRType::A(), - RRTTL(300))), - rr_grandchild_ns_(new RRset(grandchild_ns_name_, class_, RRType::NS(), - RRTTL(300))), - rr_grandchild_glue_(new RRset(grandchild_glue_name_, class_, - RRType::AAAA(), RRTTL(300))) + zone_(class_, origin_) { + // Build test RRsets. Below, we construct an RRset for + // each textual RR(s) of zone_data, and assign it to the corresponding + // rr_xxx. + const RRsetData zone_data[] = { + {"example.org. 300 IN NS ns.example.org.", &rr_ns_}, + {"example.org. 300 IN A 192.0.2.1", &rr_a_}, + {"ns.example.org. 300 IN A 192.0.2.2", &rr_ns_a_}, + {"ns.example.org. 300 IN AAAA 2001:db8::2", &rr_ns_aaaa_}, + {"cname.example.org. 300 IN CNAME canonical.example.org", + &rr_cname_}, + {"cname.example.org. 300 IN A 192.0.2.3", &rr_cname_a_}, + {"dname.example.org. 300 IN DNAME target.example.org.", + &rr_dname_}, + {"dname.example.org. 300 IN A 192.0.2.39", &rr_dname_a_}, + {"dname.example.org. 300 IN NS ns.dname.example.org.", + &rr_dname_ns_}, + {"example.org. 300 IN DNAME example.com.", &rr_dname_apex_}, + {"child.example.org. 300 IN NS ns.child.example.org.", + &rr_child_ns_}, + {"ns.child.example.org. 300 IN A 192.0.2.153", + &rr_child_glue_}, + {"grand.child.example.org. 300 IN NS ns.grand.child.example.org.", + &rr_grandchild_ns_}, + {"ns.grand.child.example.org. 300 IN AAAA 2001:db8::253", + &rr_grandchild_glue_}, + {"dname.child.example.org. 300 IN DNAME example.com.", + &rr_child_dname_}, + {"example.com. 300 IN A 192.0.2.10", &rr_out_}, + {"*.wild.example.org. 300 IN A 192.0.2.1", &rr_wild_}, + {"foo.wild.example.org. 300 IN A 192.0.2.3", &rr_under_wild_}, + {"wild.*.foo.example.org. 300 IN A 192.0.2.1", &rr_emptywild_}, + {"wild.*.foo.*.bar.example.org. 300 IN A 192.0.2.1", + &rr_nested_emptywild_}, + {"*.nswild.example.org. 300 IN NS nswild.example.", &rr_nswild_}, + {"*.dnamewild.example.org. 300 IN DNAME dnamewild.example.", + &rr_dnamewild_}, + {"*.child.example.org. 300 IN A 192.0.2.1", &rr_child_wild_}, + {"bar.foo.wild.example.org. 300 IN A 192.0.2.2", &rr_not_wild_}, + {"baz.foo.wild.example.org. 300 IN A 192.0.2.3", + &rr_not_wild_another_}, + {NULL, NULL} + }; + + stringstream zone_data_stream; + vector rrsets; + for (unsigned int i = 0; zone_data[i].text != NULL; ++i) { + zone_data_stream << zone_data[i].text << "\n"; + rrsets.push_back(zone_data[i].rrset); + } + + vector::iterator it = rrsets.begin(); + masterLoad(zone_data_stream, Name::ROOT_NAME(), class_, + boost::bind(setRRset, _1, it)); } // Some data to test with const RRClass class_; - const Name origin_, ns_name_, cname_name_, child_ns_name_, - child_glue_name_, grandchild_ns_name_, grandchild_glue_name_; + const Name origin_; // The zone to torture by tests MemoryZone zone_; /* * Some RRsets to put inside the zone. - * They are empty, but the MemoryZone does not have a reason to look - * inside anyway. We will check it finds them and does not change - * the pointer. */ - ConstRRsetPtr + RRsetPtr // Out of zone RRset rr_out_, // NS of example.org @@ -195,11 +243,24 @@ public: // A of example.org rr_a_; RRsetPtr rr_cname_; // CNAME in example.org (RDATA will be added) - ConstRRsetPtr rr_cname_a_; // for mixed CNAME + A case - ConstRRsetPtr rr_child_ns_; // NS of a child domain (for delegation) - ConstRRsetPtr rr_child_glue_; // glue RR of the child domain - ConstRRsetPtr rr_grandchild_ns_; // NS below a zone cut (unusual) - ConstRRsetPtr rr_grandchild_glue_; // glue RR below a deeper zone cut + RRsetPtr rr_cname_a_; // for mixed CNAME + A case + RRsetPtr rr_dname_; // DNAME in example.org (RDATA will be added) + RRsetPtr rr_dname_a_; // for mixed DNAME + A case + RRsetPtr rr_dname_ns_; // for mixed DNAME + NS case + RRsetPtr rr_dname_apex_; // for mixed DNAME + NS case in the apex + RRsetPtr rr_child_ns_; // NS of a child domain (for delegation) + RRsetPtr rr_child_glue_; // glue RR of the child domain + RRsetPtr rr_grandchild_ns_; // NS below a zone cut (unusual) + RRsetPtr rr_grandchild_glue_; // glue RR below a deeper zone cut + RRsetPtr rr_child_dname_; // A DNAME under NS + RRsetPtr rr_wild_; + RRsetPtr rr_emptywild_; + RRsetPtr rr_nested_emptywild_; + RRsetPtr rr_nswild_, rr_dnamewild_; + RRsetPtr rr_child_wild_; + RRsetPtr rr_under_wild_; + RRsetPtr rr_not_wild_; + RRsetPtr rr_not_wild_another_; /** * \brief Test one find query to the zone. @@ -216,13 +277,18 @@ public: * \param answer The expected rrset, if any should be returned. * \param zone Check different MemoryZone object than zone_ (if NULL, * uses zone_) + * \param check_wild_answer Checks that the answer has the same RRs, type + * class and TTL as the eqxpected answer and that the name corresponds + * to the one searched. It is meant for checking answers for wildcard + * queries. */ void findTest(const Name& name, const RRType& rrtype, Zone::Result result, bool check_answer = true, const ConstRRsetPtr& answer = ConstRRsetPtr(), RRsetList* target = NULL, MemoryZone* zone = NULL, - Zone::FindOptions options = Zone::FIND_DEFAULT) + Zone::FindOptions options = Zone::FIND_DEFAULT, + bool check_wild_answer = false) { if (!zone) { zone = &zone_; @@ -236,9 +302,39 @@ public: EXPECT_EQ(result, find_result.code); if (check_answer) { EXPECT_EQ(answer, find_result.rrset); + } else if (check_wild_answer) { + ASSERT_NE(ConstRRsetPtr(), answer) << + "Wrong test, don't check for wild names if you expect " + "empty answer"; + ASSERT_NE(ConstRRsetPtr(), find_result.rrset) << + "No answer found"; + RdataIteratorPtr expectedIt(answer->getRdataIterator()); + RdataIteratorPtr actualIt( + find_result.rrset->getRdataIterator()); + while (!expectedIt->isLast() && !actualIt->isLast()) { + EXPECT_EQ(0, expectedIt->getCurrent().compare( + actualIt->getCurrent())) << "The RRs differ ('" << + expectedIt->getCurrent().toText() << "', '" << + actualIt->getCurrent().toText() << "')"; + expectedIt->next(); + actualIt->next(); + } + EXPECT_TRUE(expectedIt->isLast()) << + "Result has less RRs than expected"; + EXPECT_TRUE(actualIt->isLast()) << + "Result has more RRs than expected"; + EXPECT_EQ(answer->getClass(), + find_result.rrset->getClass()); + EXPECT_EQ(answer->getType(), + find_result.rrset->getType()); + EXPECT_EQ(answer->getTTL(), + find_result.rrset->getTTL()); + EXPECT_EQ(name, find_result.rrset->getName()); } }); } + // Internal part of the cancelWildcard test that is multiple times + void doCancelWildcardTest(); }; /** @@ -275,33 +371,30 @@ TEST_F(MemoryZoneTest, add) { } TEST_F(MemoryZoneTest, addMultipleCNAMEs) { - rr_cname_->addRdata(generic::CNAME("canonical1.example.org.")); rr_cname_->addRdata(generic::CNAME("canonical2.example.org.")); EXPECT_THROW(zone_.add(rr_cname_), MemoryZone::AddError); } TEST_F(MemoryZoneTest, addCNAMEThenOther) { - rr_cname_->addRdata(generic::CNAME("canonical.example.org.")); EXPECT_EQ(SUCCESS, zone_.add(rr_cname_)); EXPECT_THROW(zone_.add(rr_cname_a_), MemoryZone::AddError); } TEST_F(MemoryZoneTest, addOtherThenCNAME) { - rr_cname_->addRdata(generic::CNAME("canonical.example.org.")); EXPECT_EQ(SUCCESS, zone_.add(rr_cname_a_)); EXPECT_THROW(zone_.add(rr_cname_), MemoryZone::AddError); } TEST_F(MemoryZoneTest, findCNAME) { // install CNAME RR - rr_cname_->addRdata(generic::CNAME("canonical.example.org.")); EXPECT_EQ(SUCCESS, zone_.add(rr_cname_)); // Find A RR of the same. Should match the CNAME - findTest(cname_name_, RRType::NS(), Zone::CNAME, true, rr_cname_); + findTest(rr_cname_->getName(), RRType::NS(), Zone::CNAME, true, rr_cname_); // Find the CNAME itself. Should result in normal SUCCESS - findTest(cname_name_, RRType::CNAME(), Zone::SUCCESS, true, rr_cname_); + findTest(rr_cname_->getName(), RRType::CNAME(), Zone::SUCCESS, true, + rr_cname_); } TEST_F(MemoryZoneTest, findCNAMEUnderZoneCut) { @@ -318,6 +411,80 @@ TEST_F(MemoryZoneTest, findCNAMEUnderZoneCut) { Zone::FIND_GLUE_OK); } +// Two DNAMEs at single domain are disallowed by RFC 2672, section 3) +// Having a CNAME there is disallowed too, but it is tested by +// addOtherThenCNAME and addCNAMEThenOther. +TEST_F(MemoryZoneTest, addMultipleDNAMEs) { + rr_dname_->addRdata(generic::DNAME("target2.example.org.")); + EXPECT_THROW(zone_.add(rr_dname_), MemoryZone::AddError); +} + +/* + * These two tests ensure that we can't have DNAME and NS at the same + * node with the exception of the apex of zone (forbidden by RFC 2672) + */ +TEST_F(MemoryZoneTest, addDNAMEThenNS) { + EXPECT_NO_THROW(EXPECT_EQ(SUCCESS, zone_.add(rr_dname_))); + EXPECT_THROW(zone_.add(rr_dname_ns_), MemoryZone::AddError); +} + +TEST_F(MemoryZoneTest, addNSThenDNAME) { + EXPECT_NO_THROW(EXPECT_EQ(SUCCESS, zone_.add(rr_dname_ns_))); + EXPECT_THROW(zone_.add(rr_dname_), MemoryZone::AddError); +} + +// It is allowed to have NS and DNAME at apex +TEST_F(MemoryZoneTest, DNAMEAndNSAtApex) { + EXPECT_NO_THROW(EXPECT_EQ(SUCCESS, zone_.add(rr_dname_apex_))); + EXPECT_NO_THROW(EXPECT_EQ(SUCCESS, zone_.add(rr_ns_))); + + // The NS should be possible to be found, below should be DNAME, not + // delegation + findTest(origin_, RRType::NS(), Zone::SUCCESS, true, rr_ns_); + findTest(rr_child_ns_->getName(), RRType::A(), Zone::DNAME, true, + rr_dname_apex_); +} + +TEST_F(MemoryZoneTest, NSAndDNAMEAtApex) { + EXPECT_NO_THROW(EXPECT_EQ(SUCCESS, zone_.add(rr_ns_))); + EXPECT_NO_THROW(EXPECT_EQ(SUCCESS, zone_.add(rr_dname_apex_))); +} + +// TODO: Test (and implement) adding data under DNAME. That is forbidden by +// 2672 as well. + +// Search under a DNAME record. It should return the DNAME +TEST_F(MemoryZoneTest, findBelowDNAME) { + EXPECT_NO_THROW(EXPECT_EQ(SUCCESS, zone_.add(rr_dname_))); + findTest(Name("below.dname.example.org"), RRType::A(), Zone::DNAME, true, + rr_dname_); +} + +// Search at the domain with DNAME. It should act as DNAME isn't there, DNAME +// influences only the data below (see RFC 2672, section 3) +TEST_F(MemoryZoneTest, findAtDNAME) { + EXPECT_NO_THROW(EXPECT_EQ(SUCCESS, zone_.add(rr_dname_))); + EXPECT_NO_THROW(EXPECT_EQ(SUCCESS, zone_.add(rr_dname_a_))); + + const Name dname_name(rr_dname_->getName()); + findTest(dname_name, RRType::A(), Zone::SUCCESS, true, rr_dname_a_); + findTest(dname_name, RRType::DNAME(), Zone::SUCCESS, true, rr_dname_); + findTest(dname_name, RRType::TXT(), Zone::NXRRSET, true); +} + +// Try searching something that is both under NS and DNAME, without and with +// GLUE_OK mode (it should stop at the NS and DNAME respectively). +TEST_F(MemoryZoneTest, DNAMEUnderNS) { + zone_.add(rr_child_ns_); + zone_.add(rr_child_dname_); + + Name lowName("below.dname.child.example.org."); + + findTest(lowName, RRType::A(), Zone::DELEGATION, true, rr_child_ns_); + findTest(lowName, RRType::A(), Zone::DNAME, true, rr_child_dname_, NULL, + NULL, Zone::FIND_GLUE_OK); +} + // Test adding child zones and zone cut handling TEST_F(MemoryZoneTest, delegationNS) { // add in-zone data @@ -366,7 +533,7 @@ TEST_F(MemoryZoneTest, findAny) { EXPECT_EQ(0, out_rrsets.size()); RRsetList glue_child_rrsets; - findTest(child_glue_name_, RRType::ANY(), Zone::SUCCESS, true, + findTest(rr_child_glue_->getName(), RRType::ANY(), Zone::SUCCESS, true, ConstRRsetPtr(), &glue_child_rrsets); EXPECT_EQ(rr_child_glue_, glue_child_rrsets.findRRset(RRType::A(), RRClass::IN())); @@ -380,13 +547,13 @@ TEST_F(MemoryZoneTest, findAny) { // zone cut RRsetList child_rrsets; - findTest(child_ns_name_, RRType::ANY(), Zone::DELEGATION, true, + findTest(rr_child_ns_->getName(), RRType::ANY(), Zone::DELEGATION, true, rr_child_ns_, &child_rrsets); EXPECT_EQ(0, child_rrsets.size()); // glue for this zone cut RRsetList new_glue_child_rrsets; - findTest(child_glue_name_, RRType::ANY(), Zone::DELEGATION, true, + findTest(rr_child_glue_->getName(), RRType::ANY(), Zone::DELEGATION, true, rr_child_ns_, &new_glue_child_rrsets); EXPECT_EQ(0, new_glue_child_rrsets.size()); } @@ -403,29 +570,25 @@ TEST_F(MemoryZoneTest, glue) { EXPECT_NO_THROW(EXPECT_EQ(SUCCESS, zone_.add(rr_grandchild_glue_))); // by default glue is hidden due to the zone cut - findTest(child_glue_name_, RRType::A(), Zone::DELEGATION, true, + findTest(rr_child_glue_->getName(), RRType::A(), Zone::DELEGATION, true, rr_child_ns_); // If we do it in the "glue OK" mode, we should find the exact match. - findTest(child_glue_name_, RRType::A(), Zone::SUCCESS, true, + findTest(rr_child_glue_->getName(), RRType::A(), Zone::SUCCESS, true, rr_child_glue_, NULL, NULL, Zone::FIND_GLUE_OK); // glue OK + NXRRSET case - findTest(child_glue_name_, RRType::AAAA(), Zone::NXRRSET, true, + findTest(rr_child_glue_->getName(), RRType::AAAA(), Zone::NXRRSET, true, ConstRRsetPtr(), NULL, NULL, Zone::FIND_GLUE_OK); // glue OK + NXDOMAIN case findTest(Name("www.child.example.org"), RRType::A(), Zone::DELEGATION, true, rr_child_ns_, NULL, NULL, Zone::FIND_GLUE_OK); - // TODO: - // glue name would match a wildcard under a zone cut: wildcard match - // shouldn't happen under a cut and result must be PARTIALMATCH - // (This case cannot be tested yet) - // nested cut case. The glue should be found. - findTest(grandchild_glue_name_, RRType::AAAA(), Zone::SUCCESS, + findTest(rr_grandchild_glue_->getName(), RRType::AAAA(), + Zone::SUCCESS, true, rr_grandchild_glue_, NULL, NULL, Zone::FIND_GLUE_OK); // A non-existent name in nested cut. This should result in delegation @@ -435,18 +598,6 @@ TEST_F(MemoryZoneTest, glue) { Zone::FIND_GLUE_OK); } -// Test adding DNAMEs and resulting delegation handling -// Listing ideas only for now -TEST_F(MemoryZoneTest, delegationDNAME) { - // apex DNAME: allowed by spec. No DNAME delegation at the apex; - // descendants are subject to delegation. - - // Other cases of NS and DNAME mixture are prohibited. - // BIND 9 doesn't reject such cases at load time, however. - - // DNAME and ordinary types (allowed by spec) -} - /** * \brief Test searching. * @@ -464,17 +615,56 @@ TEST_F(MemoryZoneTest, find) { // These two should be successful findTest(origin_, RRType::NS(), Zone::SUCCESS, true, rr_ns_); - findTest(ns_name_, RRType::A(), Zone::SUCCESS, true, rr_ns_a_); + findTest(rr_ns_a_->getName(), RRType::A(), Zone::SUCCESS, true, rr_ns_a_); // These domain exist but don't have the provided RRType findTest(origin_, RRType::AAAA(), Zone::NXRRSET); - findTest(ns_name_, RRType::NS(), Zone::NXRRSET); + findTest(rr_ns_a_->getName(), RRType::NS(), Zone::NXRRSET); // These domains don't exist (and one is out of the zone) findTest(Name("nothere.example.org"), RRType::A(), Zone::NXDOMAIN); findTest(Name("example.net"), RRType::A(), Zone::NXDOMAIN); } +TEST_F(MemoryZoneTest, emptyNode) { + /* + * The backend RBTree for this test should look like as follows: + * example.org + * | + * baz (empty; easy case) + * / | \ + * bar | x.foo ('foo' part is empty; a bit trickier) + * bbb + * / + * aaa + */ + + // Construct the test zone + const char* const names[] = { + "bar.example.org", "x.foo.example.org", "aaa.baz.example.org", + "bbb.baz.example.org.", NULL}; + for (int i = 0; names[i] != NULL; ++i) { + ConstRRsetPtr rrset(new RRset(Name(names[i]), class_, RRType::A(), + RRTTL(300))); + EXPECT_EQ(SUCCESS, zone_.add(rrset)); + } + + // empty node matching, easy case: the node for 'baz' exists with + // no data. + findTest(Name("baz.example.org"), RRType::A(), Zone::NXRRSET); + + // empty node matching, a trickier case: the node for 'foo' is part of + // "x.foo", which should be considered an empty node. + findTest(Name("foo.example.org"), RRType::A(), Zone::NXRRSET); + + // "org" is contained in "example.org", but it shouldn't be treated as + // NXRRSET because it's out of zone. + // Note: basically we don't expect such a query to be performed (the common + // operation is to identify the best matching zone first then perform + // search it), but we shouldn't be confused even in the unexpected case. + findTest(Name("org"), RRType::A(), Zone::NXDOMAIN); +} + TEST_F(MemoryZoneTest, load) { // Put some data inside the zone EXPECT_NO_THROW(EXPECT_EQ(result::SUCCESS, zone_.add(rr_ns_))); @@ -497,14 +687,298 @@ TEST_F(MemoryZoneTest, load) { findTest(Name("a.root-servers.net."), RRType::A(), Zone::SUCCESS, false, ConstRRsetPtr(), NULL, &rootzone); // But this should no longer be here - findTest(ns_name_, RRType::AAAA(), Zone::NXDOMAIN, true, ConstRRsetPtr(), - NULL, &rootzone); + findTest(rr_ns_a_->getName(), RRType::AAAA(), Zone::NXDOMAIN, true, + ConstRRsetPtr(), NULL, &rootzone); // Try loading zone that is wrong in a different way EXPECT_THROW(zone_.load(TEST_DATA_DIR "/duplicate_rrset.zone"), MasterLoadError); } +/* + * Test that puts a (simple) wildcard into the zone and checks we can + * correctly find the data. + */ +TEST_F(MemoryZoneTest, wildcard) { + /* + * example.org. + * | + * wild (not *.wild, should have wild mark) + * | + * * + */ + EXPECT_EQ(SUCCESS, zone_.add(rr_wild_)); + + // Search at the parent. The parent will not have the A, but it will + // be in the wildcard (so check the wildcard isn't matched at the parent) + { + SCOPED_TRACE("Search at parrent"); + findTest(Name("wild.example.org"), RRType::A(), Zone::NXRRSET); + } + + // Search the original name of wildcard + { + SCOPED_TRACE("Search directly at *"); + findTest(Name("*.wild.example.org"), RRType::A(), Zone::SUCCESS, true, + rr_wild_); + } + // Search "created" name. + { + SCOPED_TRACE("Search at created child"); + findTest(Name("a.wild.example.org"), RRType::A(), Zone::SUCCESS, false, + rr_wild_, NULL, NULL, Zone::FIND_DEFAULT, true); + } + + // Search another created name, this time little bit lower + { + SCOPED_TRACE("Search at created grand-child"); + findTest(Name("a.b.wild.example.org"), RRType::A(), Zone::SUCCESS, + false, rr_wild_, NULL, NULL, Zone::FIND_DEFAULT, true); + } + + EXPECT_EQ(SUCCESS, zone_.add(rr_under_wild_)); + { + SCOPED_TRACE("Search under non-wildcard"); + findTest(Name("bar.foo.wild.example.org"), RRType::A(), + Zone::NXDOMAIN); + } +} + +/* + * Test that we don't match a wildcard if we get under delegation. + * By 4.3.3 of RFC1034: + * "Wildcard RRs do not apply: + * - When the query is in another zone. That is, delegation cancels + * the wildcard defaults." + */ +TEST_F(MemoryZoneTest, delegatedWildcard) { + EXPECT_EQ(SUCCESS, zone_.add(rr_child_wild_)); + EXPECT_EQ(SUCCESS, zone_.add(rr_child_ns_)); + + { + SCOPED_TRACE("Looking under delegation point"); + findTest(Name("a.child.example.org"), RRType::A(), Zone::DELEGATION, + true, rr_child_ns_); + } + + { + SCOPED_TRACE("Looking under delegation point in GLUE_OK mode"); + findTest(Name("a.child.example.org"), RRType::A(), Zone::DELEGATION, + true, rr_child_ns_, NULL, NULL, Zone::FIND_GLUE_OK); + } +} + +// Tests combination of wildcard and ANY. +TEST_F(MemoryZoneTest, anyWildcard) { + EXPECT_EQ(SUCCESS, zone_.add(rr_wild_)); + + // First try directly the name (normal match) + { + SCOPED_TRACE("Asking direcly for *"); + RRsetList target; + findTest(Name("*.wild.example.org"), RRType::ANY(), Zone::SUCCESS, + true, ConstRRsetPtr(), &target); + ASSERT_EQ(1, target.size()); + EXPECT_EQ(RRType::A(), (*target.begin())->getType()); + EXPECT_EQ(Name("*.wild.example.org"), (*target.begin())->getName()); + } + + // Then a wildcard match + { + SCOPED_TRACE("Asking in the wild way"); + RRsetList target; + findTest(Name("a.wild.example.org"), RRType::ANY(), Zone::SUCCESS, + true, ConstRRsetPtr(), &target); + ASSERT_EQ(1, target.size()); + EXPECT_EQ(RRType::A(), (*target.begin())->getType()); + EXPECT_EQ(Name("a.wild.example.org"), (*target.begin())->getName()); + } +} + +// Test there's nothing in the wildcard in the middle if we load +// wild.*.foo.example.org. +TEST_F(MemoryZoneTest, emptyWildcard) { + /* + * example.org. + * foo + * * + * wild + */ + EXPECT_EQ(SUCCESS, zone_.add(rr_emptywild_)); + + { + SCOPED_TRACE("Asking for the original record under wildcard"); + findTest(Name("wild.*.foo.example.org"), RRType::A(), Zone::SUCCESS, + true, rr_emptywild_); + } + + { + SCOPED_TRACE("Asking for A record"); + findTest(Name("a.foo.example.org"), RRType::A(), Zone::NXRRSET); + findTest(Name("*.foo.example.org"), RRType::A(), Zone::NXRRSET); + findTest(Name("foo.example.org"), RRType::A(), Zone::NXRRSET); + } + + { + SCOPED_TRACE("Asking for ANY record"); + RRsetList normalTarget; + findTest(Name("*.foo.example.org"), RRType::ANY(), Zone::NXRRSET, true, + ConstRRsetPtr(), &normalTarget); + EXPECT_EQ(0, normalTarget.size()); + + RRsetList wildTarget; + findTest(Name("a.foo.example.org"), RRType::ANY(), Zone::NXRRSET, true, + ConstRRsetPtr(), &wildTarget); + EXPECT_EQ(0, wildTarget.size()); + } + + { + SCOPED_TRACE("Asking on the non-terminal"); + findTest(Name("wild.bar.foo.example.org"), RRType::A(), + Zone::NXRRSET); + } +} + +// Same as emptyWildcard, but with multiple * in the path. +TEST_F(MemoryZoneTest, nestedEmptyWildcard) { + EXPECT_EQ(SUCCESS, zone_.add(rr_nested_emptywild_)); + + { + SCOPED_TRACE("Asking for the original record under wildcards"); + findTest(Name("wild.*.foo.*.bar.example.org"), RRType::A(), + Zone::SUCCESS, true, rr_nested_emptywild_); + } + + { + SCOPED_TRACE("Matching wildcard against empty nonterminal"); + + const char* names[] = { + "baz.foo.*.bar.example.org", + "baz.foo.baz.bar.example.org", + "*.foo.baz.bar.example.org", + NULL + }; + + for (const char** name(names); *name != NULL; ++ name) { + SCOPED_TRACE(string("Node ") + *name); + findTest(Name(*name), RRType::A(), Zone::NXRRSET); + } + } + + // Domains to test + const char* names[] = { + "*.foo.*.bar.example.org", + "foo.*.bar.example.org", + "*.bar.example.org", + "bar.example.org", + NULL + }; + + { + SCOPED_TRACE("Asking directly for A on parent nodes"); + + for (const char** name(names); *name != NULL; ++ name) { + SCOPED_TRACE(string("Node ") + *name); + findTest(Name(*name), RRType::A(), Zone::NXRRSET); + } + } + + { + SCOPED_TRACE("Asking for ANY on parent nodes"); + + for (const char** name(names); *name != NULL; ++ name) { + SCOPED_TRACE(string("Node ") + *name); + + RRsetList target; + findTest(Name(*name), RRType::ANY(), Zone::NXRRSET, true, + ConstRRsetPtr(), &target); + EXPECT_EQ(0, target.size()); + } + } +} + +// We run this part twice from the below test, in two slightly different +// situations +void +MemoryZoneTest::doCancelWildcardTest() { + // These should be canceled + { + SCOPED_TRACE("Canceled under foo.wild.example.org"); + findTest(Name("aaa.foo.wild.example.org"), RRType::A(), + Zone::NXDOMAIN); + findTest(Name("zzz.foo.wild.example.org"), RRType::A(), + Zone::NXDOMAIN); + } + + // This is existing, non-wildcard domain, shouldn't wildcard at all + { + SCOPED_TRACE("Existing domain under foo.wild.example.org"); + findTest(Name("bar.foo.wild.example.org"), RRType::A(), Zone::SUCCESS, + true, rr_not_wild_); + } + + // These should be caught by the wildcard + { + SCOPED_TRACE("Neighbor wildcards to foo.wild.example.org"); + + const char* names[] = { + "aaa.bbb.wild.example.org", + "aaa.zzz.wild.example.org", + "zzz.wild.example.org", + NULL + }; + + for (const char** name(names); *name != NULL; ++ name) { + SCOPED_TRACE(string("Node ") + *name); + + findTest(Name(*name), RRType::A(), Zone::SUCCESS, false, rr_wild_, + NULL, NULL, Zone::FIND_DEFAULT, true); + } + } + + // This shouldn't be wildcarded, it's an existing domain + { + SCOPED_TRACE("The foo.wild.example.org itself"); + findTest(Name("foo.wild.example.org"), RRType::A(), Zone::NXRRSET); + } +} + +/* + * This tests that if there's a name between the wildcard domain and the + * searched one, it will not trigger wildcard, for example, if we have + * *.wild.example.org and bar.foo.wild.example.org, then we know + * foo.wild.example.org exists and is not wildcard. Therefore, search for + * aaa.foo.wild.example.org should return NXDOMAIN. + * + * Tests few cases "around" the canceled wildcard match, to see something that + * shouldn't be canceled isn't. + */ +TEST_F(MemoryZoneTest, cancelWildcard) { + EXPECT_EQ(SUCCESS, zone_.add(rr_wild_)); + EXPECT_EQ(SUCCESS, zone_.add(rr_not_wild_)); + + { + SCOPED_TRACE("Runnig with single entry under foo.wild.example.org"); + doCancelWildcardTest(); + } + + // Try putting another one under foo.wild.... + // The result should be the same but it will be done in another way in the + // code, because the foo.wild.example.org will exist in the tree. + EXPECT_EQ(SUCCESS, zone_.add(rr_not_wild_another_)); + { + SCOPED_TRACE("Runnig with two entries under foo.wild.example.org"); + doCancelWildcardTest(); + } +} + +TEST_F(MemoryZoneTest, loadBadWildcard) { + // We reject loading the zone if it contains a wildcard name for + // NS or DNAME. + EXPECT_THROW(zone_.add(rr_nswild_), MemoryZone::AddError); + EXPECT_THROW(zone_.add(rr_dnamewild_), MemoryZone::AddError); +} + TEST_F(MemoryZoneTest, swap) { // build one zone with some data MemoryZone zone1(class_, origin_); diff --git a/src/lib/datasrc/tests/rbtree_unittest.cc b/src/lib/datasrc/tests/rbtree_unittest.cc index 0105cad5c0300e3c7b7e6f6155202fff132463c2..82eed636ee73727d570b747f9d82c3fcc1bd4a68 100644 --- a/src/lib/datasrc/tests/rbtree_unittest.cc +++ b/src/lib/datasrc/tests/rbtree_unittest.cc @@ -12,9 +12,10 @@ // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. - #include +#include + #include #include #include @@ -26,10 +27,15 @@ #include using namespace std; +using namespace isc; using namespace isc::dns; using isc::UnitTestUtil; using namespace isc::datasrc; +// XXX: some compilers cannot find class static constants used in +// EXPECT_xxx macros, for which we need an explicit empty definition. +const size_t Name::MAX_LABELS; + /* The initial structure of rbtree * * b @@ -50,9 +56,10 @@ using namespace isc::datasrc; namespace { class RBTreeTest : public::testing::Test { protected: - RBTreeTest() : rbtree() { - const char * domain_names[] = {"c", "b", "a", "x.d.e.f", "z.d.e.f", "g.h", "i.g.h", - "o.w.y.d.e.f", "j.z.d.e.f", "p.w.y.d.e.f", "q.w.y.d.e.f"}; + RBTreeTest() : rbtree_expose_empty_node(true) { + const char* const domain_names[] = { + "c", "b", "a", "x.d.e.f", "z.d.e.f", "g.h", "i.g.h", "o.w.y.d.e.f", + "j.z.d.e.f", "p.w.y.d.e.f", "q.w.y.d.e.f"}; int name_count = sizeof(domain_names) / sizeof(domain_names[0]); for (int i = 0; i < name_count; ++i) { rbtree.insert(Name(domain_names[i]), &rbtnode); @@ -65,8 +72,7 @@ protected: } RBTree rbtree; - typedef RBTree ExposeRBTree; - ExposeRBTree rbtree_expose_empty_node; + RBTree rbtree_expose_empty_node; RBNode* rbtnode; const RBNode* crbtnode; }; @@ -82,69 +88,34 @@ TEST_F(RBTreeTest, setGetData) { } TEST_F(RBTreeTest, insertNames) { - //if don't expose empty node, even the node already exsit which is caused by node fission - //we will return succeed - EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("d.e.f"), &rbtnode)); + EXPECT_EQ(RBTree::ALREADYEXISTS, rbtree.insert(Name("d.e.f"), + &rbtnode)); EXPECT_EQ(Name("d.e.f"), rbtnode->getName()); EXPECT_EQ(13, rbtree.getNodeCount()); - EXPECT_EQ(ExposeRBTree::ALREADYEXISTS, - rbtree_expose_empty_node.insert(Name("d.e.f"), &rbtnode)); - EXPECT_EQ(Name("d.e.f"), rbtnode->getName()); - EXPECT_EQ(13, rbtree_expose_empty_node.getNodeCount()); - - //insert not exist node EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("."), &rbtnode)); EXPECT_EQ(Name("."), rbtnode->getName()); EXPECT_EQ(14, rbtree.getNodeCount()); - EXPECT_EQ(ExposeRBTree::SUCCESS, rbtree_expose_empty_node.insert( - Name("."), &rbtnode)); - EXPECT_EQ(Name("."), rbtnode->getName()); - EXPECT_EQ(14, rbtree_expose_empty_node.getNodeCount()); - EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("example.com"), &rbtnode)); EXPECT_EQ(15, rbtree.getNodeCount()); rbtnode->setData(RBNode::NodeDataPtr(new int(12))); - EXPECT_EQ(ExposeRBTree::SUCCESS, rbtree_expose_empty_node.insert( - Name("example.com"), &rbtnode)); - EXPECT_EQ(15, rbtree_expose_empty_node.getNodeCount()); - rbtnode->setData(RBNode::NodeDataPtr(new int(12))); - - // return ALREADYEXISTS, since node "example.com" already has been explicitly inserted EXPECT_EQ(RBTree::ALREADYEXISTS, rbtree.insert(Name("example.com"), &rbtnode)); EXPECT_EQ(15, rbtree.getNodeCount()); - EXPECT_EQ(ExposeRBTree::ALREADYEXISTS, - rbtree_expose_empty_node.insert(Name("example.com"), &rbtnode)); - EXPECT_EQ(15, rbtree_expose_empty_node.getNodeCount()); - // split the node "d.e.f" EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("k.e.f"), &rbtnode)); EXPECT_EQ(Name("k"), rbtnode->getName()); EXPECT_EQ(17, rbtree.getNodeCount()); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("k.e.f"), &rbtnode)); - EXPECT_EQ(Name("k"), rbtnode->getName()); - EXPECT_EQ(17, rbtree_expose_empty_node.getNodeCount()); - - // split the node "g.h" - EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("h"), &rbtnode)); + EXPECT_EQ(RBTree::ALREADYEXISTS, rbtree.insert(Name("h"), &rbtnode)); EXPECT_EQ(Name("h"), rbtnode->getName()); EXPECT_EQ(18, rbtree.getNodeCount()); - //node fission will create node "h" - EXPECT_EQ(ExposeRBTree::ALREADYEXISTS, - rbtree_expose_empty_node.insert(Name("h"), &rbtnode)); - EXPECT_EQ(Name("h"), rbtnode->getName()); - EXPECT_EQ(18, rbtree_expose_empty_node.getNodeCount()); - - // add child domain EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("m.p.w.y.d.e.f"), &rbtnode)); EXPECT_EQ(Name("m"), rbtnode->getName()); @@ -153,41 +124,18 @@ TEST_F(RBTreeTest, insertNames) { EXPECT_EQ(Name("n"), rbtnode->getName()); EXPECT_EQ(20, rbtree.getNodeCount()); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("m.p.w.y.d.e.f"), &rbtnode)); - EXPECT_EQ(Name("m"), rbtnode->getName()); - EXPECT_EQ(19, rbtree_expose_empty_node.getNodeCount()); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("n.p.w.y.d.e.f"), &rbtnode)); - EXPECT_EQ(Name("n"), rbtnode->getName()); - EXPECT_EQ(20, rbtree_expose_empty_node.getNodeCount()); - - EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("l.a"), &rbtnode)); EXPECT_EQ(Name("l"), rbtnode->getName()); EXPECT_EQ(21, rbtree.getNodeCount()); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("l.a"), &rbtnode)); - EXPECT_EQ(Name("l"), rbtnode->getName()); - EXPECT_EQ(21, rbtree_expose_empty_node.getNodeCount()); - EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("r.d.e.f"), &rbtnode)); EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("s.d.e.f"), &rbtnode)); EXPECT_EQ(23, rbtree.getNodeCount()); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("r.d.e.f"), &rbtnode)); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("s.d.e.f"), &rbtnode)); - EXPECT_EQ(23, rbtree_expose_empty_node.getNodeCount()); - EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("h.w.y.d.e.f"), &rbtnode)); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("h.w.y.d.e.f"), &rbtnode)); // add more nodes one by one to cover leftRotate and rightRotate - EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("f"), &rbtnode)); + EXPECT_EQ(RBTree::ALREADYEXISTS, rbtree.insert(Name("f"), &rbtnode)); EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("m"), &rbtnode)); EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("nm"), &rbtnode)); EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("om"), &rbtnode)); @@ -198,32 +146,8 @@ TEST_F(RBTreeTest, insertNames) { EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("i"), &rbtnode)); EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("ae"), &rbtnode)); EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("n"), &rbtnode)); - - EXPECT_EQ(ExposeRBTree::ALREADYEXISTS, - rbtree_expose_empty_node.insert(Name("f"), &rbtnode)); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("m"), &rbtnode)); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("nm"), &rbtnode)); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("om"), &rbtnode)); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("k"), &rbtnode)); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("l"), &rbtnode)); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("fe"), &rbtnode)); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("ge"), &rbtnode)); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("i"), &rbtnode)); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("ae"), &rbtnode)); - EXPECT_EQ(ExposeRBTree::SUCCESS, - rbtree_expose_empty_node.insert(Name("n"), &rbtnode)); } - TEST_F(RBTreeTest, findName) { // find const rbtnode // exact match @@ -236,15 +160,57 @@ TEST_F(RBTreeTest, findName) { EXPECT_EQ(RBTree::NOTFOUND, rbtree.find(Name("x"), &crbtnode)); EXPECT_EQ(RBTree::NOTFOUND, rbtree.find(Name("m.n"), &crbtnode)); + // if we expose empty node, we can get the empty node created during insert + EXPECT_EQ(RBTree::EXACTMATCH, + rbtree_expose_empty_node.find(Name("d.e.f"), &crbtnode)); + EXPECT_EQ(RBTree::EXACTMATCH, + rbtree_expose_empty_node.find(Name("w.y.d.e.f"), &crbtnode)); + // partial match EXPECT_EQ(RBTree::PARTIALMATCH, rbtree.find(Name("m.b"), &crbtnode)); EXPECT_EQ(Name("b"), crbtnode->getName()); + EXPECT_EQ(RBTree::PARTIALMATCH, + rbtree_expose_empty_node.find(Name("m.d.e.f"), &crbtnode)); // find rbtnode EXPECT_EQ(RBTree::EXACTMATCH, rbtree.find(Name("q.w.y.d.e.f"), &rbtnode)); EXPECT_EQ(Name("q"), rbtnode->getName()); } +TEST_F(RBTreeTest, findError) { + // For the version that takes a node chain, the chain must be empty. + RBTreeNodeChain chain; + EXPECT_EQ(RBTree::EXACTMATCH, rbtree.find(Name("a"), &crbtnode, + chain, NULL, NULL)); + // trying to reuse the same chain. it should result in an exception. + EXPECT_THROW(rbtree.find(Name("a"), &crbtnode, chain, NULL, NULL), + BadValue); +} + +TEST_F(RBTreeTest, flags) { + EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("flags.example"), + &rbtnode)); + + // by default, flags are all off + EXPECT_FALSE(rbtnode->getFlag(RBNode::FLAG_CALLBACK)); + + // set operation, by default it enables the flag + rbtnode->setFlag(RBNode::FLAG_CALLBACK); + EXPECT_TRUE(rbtnode->getFlag(RBNode::FLAG_CALLBACK)); + + // try disable the flag explicitly + rbtnode->setFlag(RBNode::FLAG_CALLBACK, false); + EXPECT_FALSE(rbtnode->getFlag(RBNode::FLAG_CALLBACK)); + + // try enable the flag explicitly + rbtnode->setFlag(RBNode::FLAG_CALLBACK, true); + EXPECT_TRUE(rbtnode->getFlag(RBNode::FLAG_CALLBACK)); + + // setting an unknown flag will trigger an exception + EXPECT_THROW(rbtnode->setFlag(static_cast::Flags>(2), true), + isc::InvalidParameter); +} + bool testCallback(const RBNode&, bool* callack_checker) { *callack_checker = true; @@ -256,50 +222,271 @@ TEST_F(RBTreeTest, callback) { EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("callback.example"), &rbtnode)); rbtnode->setData(RBNode::NodeDataPtr(new int(1))); - EXPECT_FALSE(rbtnode->isCallbackEnabled()); + EXPECT_FALSE(rbtnode->getFlag(RBNode::FLAG_CALLBACK)); // enable/re-disable callback - rbtnode->enableCallback(); - EXPECT_TRUE(rbtnode->isCallbackEnabled()); - rbtnode->disableCallback(); - EXPECT_FALSE(rbtnode->isCallbackEnabled()); + rbtnode->setFlag(RBNode::FLAG_CALLBACK); + EXPECT_TRUE(rbtnode->getFlag(RBNode::FLAG_CALLBACK)); + rbtnode->setFlag(RBNode::FLAG_CALLBACK, false); + EXPECT_FALSE(rbtnode->getFlag(RBNode::FLAG_CALLBACK)); // enable again for subsequent tests - rbtnode->enableCallback(); + rbtnode->setFlag(RBNode::FLAG_CALLBACK); // add more levels below and above the callback node for partial match. RBNode* subrbtnode; EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("sub.callback.example"), &subrbtnode)); subrbtnode->setData(RBNode::NodeDataPtr(new int(2))); RBNode* parentrbtnode; - EXPECT_EQ(RBTree::SUCCESS, rbtree.insert(Name("example"), + EXPECT_EQ(RBTree::ALREADYEXISTS, rbtree.insert(Name("example"), &parentrbtnode)); // the chilld/parent nodes shouldn't "inherit" the callback flag. // "rbtnode" may be invalid due to the insertion, so we need to re-find // it. EXPECT_EQ(RBTree::EXACTMATCH, rbtree.find(Name("callback.example"), &rbtnode)); - EXPECT_TRUE(rbtnode->isCallbackEnabled()); - EXPECT_FALSE(subrbtnode->isCallbackEnabled()); - EXPECT_FALSE(parentrbtnode->isCallbackEnabled()); + EXPECT_TRUE(rbtnode->getFlag(RBNode::FLAG_CALLBACK)); + EXPECT_FALSE(subrbtnode->getFlag(RBNode::FLAG_CALLBACK)); + EXPECT_FALSE(parentrbtnode->getFlag(RBNode::FLAG_CALLBACK)); // check if the callback is called from find() + RBTreeNodeChain node_path1; bool callback_called = false; EXPECT_EQ(RBTree::EXACTMATCH, - rbtree.find(Name("sub.callback.example"), &crbtnode, + rbtree.find(Name("sub.callback.example"), &crbtnode, node_path1, testCallback, &callback_called)); EXPECT_TRUE(callback_called); // enable callback at the parent node, but it doesn't have data so // the callback shouldn't be called. - parentrbtnode->enableCallback(); + RBTreeNodeChain node_path2; + parentrbtnode->setFlag(RBNode::FLAG_CALLBACK); callback_called = false; EXPECT_EQ(RBTree::EXACTMATCH, - rbtree.find(Name("callback.example"), &crbtnode, + rbtree.find(Name("callback.example"), &crbtnode, node_path2, testCallback, &callback_called)); EXPECT_FALSE(callback_called); } +TEST_F(RBTreeTest, chainLevel) { + RBTreeNodeChain chain; + + // by default there should be no level in the chain. + EXPECT_EQ(0, chain.getLevelCount()); + + // insert one node to the tree and find it. there should be exactly + // one level in the chain. + RBTree tree(true); + Name node_name(Name::ROOT_NAME()); + EXPECT_EQ(RBTree::SUCCESS, tree.insert(node_name, &rbtnode)); + EXPECT_EQ(RBTree::EXACTMATCH, + tree.find(node_name, &crbtnode, chain, NULL, NULL)); + EXPECT_EQ(1, chain.getLevelCount()); + + /* + * Now creating a possibly deepest tree with MAX_LABELS - 1 levels. + * it should look like: + * a + * /| + * (.)a + * | + * a + * : (MAX_LABELS - 1) "a"'s + * + * then confirm that find() for the deepest name succeeds without any + * disruption, and the resulting chain has the expected level. + * Note that "a." and the root name (".") belong to the same level. + * So the possible maximum level is MAX_LABELS - 1, not MAX_LABELS. + */ + for (unsigned int i = 1; i < Name::MAX_LABELS; ++i) { + node_name = Name("a.").concatenate(node_name); + EXPECT_EQ(RBTree::SUCCESS, tree.insert(node_name, &rbtnode)); + RBTreeNodeChain found_chain; + EXPECT_EQ(RBTree::EXACTMATCH, + tree.find(node_name, &crbtnode, found_chain, + NULL, NULL)); + EXPECT_EQ(i, found_chain.getLevelCount()); + } + + // Confirm the last inserted name has the possible maximum length with + // maximum label count. This confirms the rbtree and chain level cannot + // be larger. + EXPECT_EQ(Name::MAX_LABELS, node_name.getLabelCount()); + EXPECT_THROW(node_name.concatenate(Name("a.")), TooLongName); +} + +TEST_F(RBTreeTest, getAbsoluteNameError) { + // an empty chain isn't allowed. + RBTreeNodeChain chain; + EXPECT_THROW(chain.getAbsoluteName(), BadValue); +} + +/* + *the domain order should be: + * a, b, c, d.e.f, x.d.e.f, w.y.d.e.f, o.w.y.d.e.f, p.w.y.d.e.f, q.w.y.d.e.f, + * z.d.e.f, j.z.d.e.f, g.h, i.g.h + * b + * / \ + * a d.e.f + * / | \ + * c | g.h + * | | + * w.y i + * / | \ + * x | z + * | | + * p j + * / \ + * o q + */ +TEST_F(RBTreeTest, nextNode) { + const char* const names[] = { + "a", "b", "c", "d.e.f", "x.d.e.f", "w.y.d.e.f", "o.w.y.d.e.f", + "p.w.y.d.e.f", "q.w.y.d.e.f", "z.d.e.f", "j.z.d.e.f", "g.h", "i.g.h"}; + const int name_count = sizeof(names) / sizeof(names[0]); + RBTreeNodeChain node_path; + const RBNode* node = NULL; + EXPECT_EQ(RBTree::EXACTMATCH, + rbtree.find(Name(names[0]), &node, node_path, NULL, + NULL)); + for (int i = 0; i < name_count; ++i) { + EXPECT_NE(static_cast(NULL), node); + EXPECT_EQ(Name(names[i]), node_path.getAbsoluteName()); + node = rbtree.nextNode(node_path); + } + + // We should have reached the end of the tree. + EXPECT_EQ(static_cast(NULL), node); +} + +TEST_F(RBTreeTest, nextNodeError) { + // Empty chain for nextNode() is invalid. + RBTreeNodeChain chain; + EXPECT_THROW(rbtree.nextNode(chain), BadValue); +} + +// A helper function for getLastComparedNode() below. +void +comparisonChecks(const RBTreeNodeChain& chain, + int expected_order, int expected_common_labels, + NameComparisonResult::NameRelation expected_reln) +{ + if (expected_order > 0) { + EXPECT_LT(0, chain.getLastComparisonResult().getOrder()); + } else if (expected_order < 0) { + EXPECT_GT(0, chain.getLastComparisonResult().getOrder()); + } else { + EXPECT_EQ(0, chain.getLastComparisonResult().getOrder()); + } + EXPECT_EQ(expected_common_labels, + chain.getLastComparisonResult().getCommonLabels()); + EXPECT_EQ(expected_reln, + chain.getLastComparisonResult().getRelation()); +} + +TEST_F(RBTreeTest, getLastComparedNode) { + RBTree& tree = rbtree_expose_empty_node; // use the "empty OK" mode + RBTreeNodeChain chain; + + // initially there should be no 'last compared'. + EXPECT_EQ(static_cast(NULL), chain.getLastComparedNode()); + + // A search for an empty tree should result in no 'last compared', too. + RBTree empty_tree; + EXPECT_EQ(RBTree::NOTFOUND, + empty_tree.find(Name("a"), &crbtnode, chain, NULL, NULL)); + EXPECT_EQ(static_cast(NULL), chain.getLastComparedNode()); + chain.clear(); + + const RBNode* expected_node; + + // Exact match case. The returned node should be last compared. + EXPECT_EQ(RBTree::EXACTMATCH, + tree.find(Name("x.d.e.f"), &expected_node, chain, + NULL, NULL)); + EXPECT_EQ(expected_node, chain.getLastComparedNode()); + // 2 = # labels of "x." + comparisonChecks(chain, 0, 2, NameComparisonResult::EQUAL); + chain.clear(); + + // Partial match, search stopped at the matching node, which should be + // the last compared node. + EXPECT_EQ(RBTree::EXACTMATCH, + tree.find(Name("i.g.h"), &expected_node)); + EXPECT_EQ(RBTree::PARTIALMATCH, + tree.find(Name("x.i.g.h"), &crbtnode, chain, + NULL, NULL)); + EXPECT_EQ(expected_node, chain.getLastComparedNode()); + // i.g.h < x.i.g.h, 2 = # labels of "i." + comparisonChecks(chain, 1, 2, NameComparisonResult::SUBDOMAIN); + chain.clear(); + + // Partial match, search stopped in the subtree below the matching node + // after following a left branch. + EXPECT_EQ(RBTree::EXACTMATCH, + tree.find(Name("x.d.e.f"), &expected_node)); + EXPECT_EQ(RBTree::PARTIALMATCH, + tree.find(Name("a.d.e.f"), &crbtnode, chain, + NULL, NULL)); + EXPECT_EQ(expected_node, chain.getLastComparedNode()); + // a < x, 1 = # labels of "." (trailing dot) + comparisonChecks(chain, -1, 1, NameComparisonResult::COMMONANCESTOR); + chain.clear(); + + // Partial match, search stopped in the subtree below the matching node + // after following a right branch. + EXPECT_EQ(RBTree::EXACTMATCH, + tree.find(Name("z.d.e.f"), &expected_node)); + EXPECT_EQ(RBTree::PARTIALMATCH, + tree.find(Name("zz.d.e.f"), &crbtnode, chain, + NULL, NULL)); + EXPECT_EQ(expected_node, chain.getLastComparedNode()); + // zz > z, 1 = # labels of "." (trailing dot) + comparisonChecks(chain, 1, 1, NameComparisonResult::COMMONANCESTOR); + chain.clear(); + + // Partial match, search stopped at a node for a super domain of the + // search name in the subtree below the matching node. + EXPECT_EQ(RBTree::EXACTMATCH, + tree.find(Name("w.y.d.e.f"), &expected_node)); + EXPECT_EQ(RBTree::PARTIALMATCH, + tree.find(Name("y.d.e.f"), &crbtnode, chain, + NULL, NULL)); + EXPECT_EQ(expected_node, chain.getLastComparedNode()); + // y < w.y, 2 = # labels of "y." + comparisonChecks(chain, -1, 2, NameComparisonResult::SUPERDOMAIN); + chain.clear(); + + // Partial match, search stopped at a node that share a common ancestor + // with the search name in the subtree below the matching node. + // (the expected node is the same as the previous case) + EXPECT_EQ(RBTree::PARTIALMATCH, + tree.find(Name("z.y.d.e.f"), &crbtnode, chain, + NULL, NULL)); + EXPECT_EQ(expected_node, chain.getLastComparedNode()); + // z.y > w.y, 2 = # labels of "y." + comparisonChecks(chain, 1, 2, NameComparisonResult::COMMONANCESTOR); + chain.clear(); + + // Search stops in the highest level after following a left branch. + EXPECT_EQ(RBTree::EXACTMATCH, tree.find(Name("c"), &expected_node)); + EXPECT_EQ(RBTree::NOTFOUND, + tree.find(Name("bb"), &crbtnode, chain, NULL, NULL)); + EXPECT_EQ(expected_node, chain.getLastComparedNode()); + // bb < c, 1 = # labels of "." (trailing dot) + comparisonChecks(chain, -1, 1, NameComparisonResult::COMMONANCESTOR); + chain.clear(); + + // Search stops in the highest level after following a right branch. + // (the expected node is the same as the previous case) + EXPECT_EQ(RBTree::NOTFOUND, + tree.find(Name("d"), &crbtnode, chain, NULL, NULL)); + EXPECT_EQ(expected_node, chain.getLastComparedNode()); + // d > c, 1 = # labels of "." (trailing dot) + comparisonChecks(chain, 1, 1, NameComparisonResult::COMMONANCESTOR); + chain.clear(); +} + TEST_F(RBTreeTest, dumpTree) { std::ostringstream str; std::ostringstream str2; @@ -336,5 +523,4 @@ TEST_F(RBTreeTest, swap) { tree2.dumpTree(out); ASSERT_EQ(str1.str(), out.str()); } - } diff --git a/src/lib/dns/Makefile.am b/src/lib/dns/Makefile.am index 4b1a64bced393a4eee9b99c6970277ef59afa70d..c5c5cd15fb634dd0e0b1db9ff2c745aebd2697fc 100644 --- a/src/lib/dns/Makefile.am +++ b/src/lib/dns/Makefile.am @@ -85,6 +85,8 @@ libdns___la_SOURCES += rrtype.cc libdns___la_SOURCES += question.h question.cc libdns___la_SOURCES += util/sha1.h util/sha1.cc libdns___la_SOURCES += tsigkey.h tsigkey.cc +libdns___la_SOURCES += rdata/generic/detail/nsec_bitmap.h +libdns___la_SOURCES += rdata/generic/detail/nsec_bitmap.cc nodist_libdns___la_SOURCES = rdataclass.cc rrclass.h rrtype.h nodist_libdns___la_SOURCES += rrparamregistry.cc @@ -117,5 +119,6 @@ libdns___include_HEADERS = \ tsigkey.h # Purposely not installing these headers: # util/*.h: used only internally, and not actually DNS specific +# rdata/*/detail/*.h: these are internal use only # rrclass-placeholder.h # rrtype-placeholder.h diff --git a/src/lib/dns/dnssectime.cc b/src/lib/dns/dnssectime.cc index 04643e293557b7d778ac274d315bf08eab48590b..c889178424f9ed3084c35393954c17784ae98649 100644 --- a/src/lib/dns/dnssectime.cc +++ b/src/lib/dns/dnssectime.cc @@ -12,6 +12,10 @@ // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. +#include + +#include + #include #include #include @@ -26,30 +30,121 @@ using namespace std; +namespace { +int days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + +inline bool +isLeap(const int y) { + return ((((y) % 4) == 0 && ((y) % 100) != 0) || ((y) % 400) == 0); +} + +unsigned int +yearSecs(const int year) { + return ((isLeap(year) ? 366 : 365 ) * 86400); +} + +unsigned int +monthSecs(const int month, const int year) { + return ((days[month] + ((month == 1 && isLeap(year)) ? 1 : 0 )) * 86400); +} +} + namespace isc { namespace dns { string -timeToText(const time_t timeval) { - struct tm* const t = gmtime(&timeval); - - // gmtime() will keep most values within range, but it can - // produce a five-digit year; check for this. - if ((t->tm_year + 1900) > 9999) { - isc_throw(InvalidTime, "Time value out of range: year > 9999"); +timeToText64(uint64_t value) { + struct tm tm; + unsigned int secs; + + // We cannot rely on gmtime() because time_t may not be of 64 bit + // integer. The following conversion logic is borrowed from BIND 9. + tm.tm_year = 70; + while ((secs = yearSecs(tm.tm_year + 1900)) <= value) { + value -= secs; + ++tm.tm_year; + if (tm.tm_year + 1900 > 9999) { + isc_throw(InvalidTime, + "Time value out of range (year > 9999): " << + tm.tm_year + 1900); + } + } + tm.tm_mon = 0; + while ((secs = monthSecs(tm.tm_mon, tm.tm_year + 1900)) <= value) { + value -= secs; + tm.tm_mon++; } + tm.tm_mday = 1; + while (86400 <= value) { + value -= 86400; + ++tm.tm_mday; + } + tm.tm_hour = 0; + while (3600 <= value) { + value -= 3600; + ++tm.tm_hour; + } + tm.tm_min = 0; + while (60 <= value) { + value -= 60; + ++tm.tm_min; + } + tm.tm_sec = value; // now t < 60, so this substitution is safe. ostringstream oss; oss << setfill('0') - << setw(4) << t->tm_year + 1900 - << setw(2) << t->tm_mon + 1 - << setw(2) << t->tm_mday - << setw(2) << t->tm_hour - << setw(2) << t->tm_min - << setw(2) << t->tm_sec; + << setw(4) << tm.tm_year + 1900 + << setw(2) << tm.tm_mon + 1 + << setw(2) << tm.tm_mday + << setw(2) << tm.tm_hour + << setw(2) << tm.tm_min + << setw(2) << tm.tm_sec; return (oss.str()); } +// timeToText32() below uses the current system time. To test it with +// unusual current time values we introduce the following function pointer; +// when it's non NULL, we call it to get the (normally faked) current time. +// Otherwise we use the standard gettimeofday(2). This hook is specifically +// intended for testing purposes, so, even if it's visible outside of this +// library, it's not even declared in a header file. +namespace dnssectime { +namespace detail { +int64_t (*gettimeFunction)() = NULL; +} +} + +namespace { +int64_t +gettimeofdayWrapper() { + using namespace dnssectime::detail; + if (gettimeFunction != NULL) { + return (gettimeFunction()); + } + + struct timeval now; + gettimeofday(&now, NULL); + + return (static_cast(now.tv_sec)); +} +} + +string +timeToText32(const uint32_t value) { + // We first adjust the time to the closest epoch based on the current time. + // Note that the following variables must be signed in order to handle + // time until year 2038 correctly. + const int64_t start = gettimeofdayWrapper() - 0x7fffffff; + int64_t base = 0; + int64_t t; + while ((t = (base + value)) < start) { + base += 0x100000000LL; + } + + // Then convert it to text. + return (timeToText64(t)); +} + namespace { const size_t DATE_LEN = 14; // YYYYMMDDHHmmSS @@ -62,27 +157,20 @@ checkRange(const int min, const int max, const int value, } isc_throw(InvalidTime, "Invalid " << valname << "value: " << value); } - -int days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; - -inline bool -isLeap(const int y) { - return ((((y) % 4) == 0 && ((y) % 100) != 0) || ((y) % 400) == 0); -} } -time_t -timeFromText(const string& time_txt) { - // first try reading YYYYMMDDHHmmSS format - int year, month, day, hour, minute, second; - +uint64_t +timeFromText64(const string& time_txt) { + // Confirm the source only consists digits. sscanf() allows some + // minor exceptions. for (int i = 0; i < time_txt.length(); ++i) { if (!isdigit(time_txt.at(i))) { - isc_throw(InvalidTime, - "Couldn't convert non-numeric time value: " << time_txt); + isc_throw(InvalidTime, "Couldn't convert non-numeric time value: " + << time_txt); } } + int year, month, day, hour, minute, second; if (time_txt.length() != DATE_LEN || sscanf(time_txt.c_str(), "%4d%2d%2d%2d%2d%2d", &year, &month, &day, &hour, &minute, &second) != 6) @@ -98,9 +186,9 @@ timeFromText(const string& time_txt) { checkRange(0, 59, minute, "minute"); checkRange(0, 60, second, "second"); // 60 == leap second. - time_t timeval = second + (60 * minute) + (3600 * hour) + + uint64_t timeval = second + (60 * minute) + (3600 * hour) + ((day - 1) * 86400); - for (int m = 0; m < (month - 1); m++) { + for (int m = 0; m < (month - 1); ++m) { timeval += days[m] * 86400; } if (isLeap(year) && month > 2) { @@ -112,5 +200,12 @@ timeFromText(const string& time_txt) { return (timeval); } + +uint32_t +timeFromText32(const string& time_txt) { + // The implicit conversion from uint64_t to uint32_t should just work here, + // because we only need to drop higher 32 bits. + return (timeFromText64(time_txt)); +} } } diff --git a/src/lib/dns/dnssectime.h b/src/lib/dns/dnssectime.h index 5069650e495a019c9eb80c39d9104895e0962010..baf866f3e0110acf5c20e9cf96bf5973a5fbb6c1 100644 --- a/src/lib/dns/dnssectime.h +++ b/src/lib/dns/dnssectime.h @@ -17,7 +17,6 @@ #include #include -#include #include @@ -40,11 +39,102 @@ public: isc::Exception(file, line, what) {} }; -time_t -timeFromText(const std::string& time_txt); +/// +/// \name DNSSEC time conversion functions. +/// +/// These functions convert between times represented in seconds (in integer) +/// since epoch and those in the textual form used in the RRSIG records. +/// For integers we provide both 32-bit and 64-bit versions. +/// The RRSIG expiration and inception fields are both 32-bit unsigned +/// integers, so 32-bit versions would be more useful for protocol operations. +/// However, with 32-bit integers we need to take into account wrap-around +/// points and compare values using the serial number arithmetic as specified +/// in RFC4034, which would be more error prone. We therefore provide 64-bit +/// versions, too. +/// +/// The timezone is always UTC for these functions. +//@{ +/// Convert textual DNSSEC time to integer, 64-bit version. +/// +/// The textual form must only consist of digits and be in the form of +/// YYYYMMDDHHmmSS, where: +/// - YYYY must be between 1970 and 9999 +/// - MM must be between 01 and 12 +/// - DD must be between 01 and 31 and must be a valid day for the month +/// represented in 'MM'. For example, if MM is 04, DD cannot be 31. +/// DD can be 29 when MM is 02 only when YYYY is a leap year. +/// - HH must be between 00 and 23 +/// - mm must be between 00 and 59 +/// - SS must be between 00 and 60 +/// +/// For all fields the range includes the begin and end values. Note that +/// 60 is allowed for 'SS', intending a leap second, although in real operation +/// it's unlikely to be specified. +/// +/// If the given text is valid, this function converts it to an unsigned +/// 64-bit number of seconds since epoch (1 January 1970 00:00:00) and returns +/// the converted value. 64 bits are sufficient to represent all possible +/// values for the valid format uniquely, so there is no overflow. +/// +/// \note RFC4034 also defines the textual form of an unsigned decimal integer +/// for the corresponding time in seconds. This function doesn't support +/// this form, and if given it throws an exception of class \c InvalidTime. +/// +/// \exception InvalidTime The given textual representation is invalid. +/// +/// \param time_txt Textual time in the form of YYYYMMDDHHmmSS +/// \return Seconds since epoch corresponding to \c time_txt +uint64_t +timeFromText64(const std::string& time_txt); +/// Convert textual DNSSEC time to integer, 32-bit version. +/// +/// This version is the same as \c timeFromText64() except that the return +/// value is wrapped around to an unsigned 32-bit integer, simply dropping +/// the upper 32 bits. +uint32_t +timeFromText32(const std::string& time_txt); + +/// Convert integral DNSSEC time to textual form, 64-bit version. +/// +/// This function takes an integer that would be seconds since epoch and +/// converts it in the form of YYYYMMDDHHmmSS. For example, if \c value is +/// 0, it returns "19700101000000". If the value corresponds to a point +/// of time on and after year 10,000, which cannot be represented in the +/// YYYY... form, an exception of class \c InvalidTime will be thrown. +/// +/// \exception InvalidTime The given time specifies on or after year 10,000. +/// \exception Other A standard exception, if resource allocation for the +/// returned text fails. +/// +/// \param value Seconds since epoch to be converted. +/// \return Textual representation of \c value in the form of YYYYMMDDHHmmSS. std::string -timeToText(const time_t timeval); +timeToText64(uint64_t value); + +/// Convert integral DNSSEC time to textual form, 32-bit version. +/// +/// This version is the same as \c timeToText64(), but the time value +/// is expected to be the lower 32 bits of the full 64-bit value. +/// These two will be different on and after a certain point of time +/// in year 2106, so this function internally resolves the ambiguity +/// using the current system time at the time of function call; +/// it first identifies the range of [N*2^32 - 2^31, N*2^32 + 2^31) +/// that contains the current time, and interprets \c value in the context +/// of that range. It then applies the same process as \c timeToText64(). +/// +/// There is one important exception in this processing, however. +/// Until 19 Jan 2038 03:14:08 (2^31 seconds since epoch), this range +/// would contain time before epoch. In order to ensure the returned +/// value is also a valid input to \c timeFromText, this function uses +/// a special range [0, 2^32) until that time. As a result, all upper +/// half of the 32-bit values are treated as a future time. For example, +/// 2^32-1 (the highest value in 32-bit unsigned integers) will be converted +/// to "21060207062815", instead of "19691231235959". +std::string +timeToText32(const uint32_t value); + +//@} } } diff --git a/src/lib/dns/message.cc b/src/lib/dns/message.cc index b96e86d5cc182dc237129341574e1a5d37454f99..c9661161992c5b4cf8f975b7d7dfd431db03ca11 100644 --- a/src/lib/dns/message.cc +++ b/src/lib/dns/message.cc @@ -309,6 +309,44 @@ Message::hasRRset(const Section section, const Name& name, return (false); } +bool +Message::hasRRset(const Section section, const RRsetPtr& rrset) { + return (hasRRset(section, rrset->getName(), rrset->getClass(), rrset->getType())); +} + +bool +Message::removeRRset(const Section section, RRsetIterator& iterator) { + if (section >= MessageImpl::NUM_SECTIONS) { + isc_throw(OutOfRange, "Invalid message section: " << section); + } + + bool removed = false; + for (vector::iterator i = impl_->rrsets_[section].begin(); + i != impl_->rrsets_[section].end(); ++i) { + if (((*i)->getName() == (*iterator)->getName()) && + ((*i)->getClass() == (*iterator)->getClass()) && + ((*i)->getType() == (*iterator)->getType())) { + + // Found the matching RRset so remove it & ignore rest + impl_->counts_[section] -= (*iterator)->getRdataCount(); + impl_->rrsets_[section].erase(i); + removed = true; + break; + } + } + + return (removed); +} + +void +Message::clearSection(const Section section) { + if (section >= MessageImpl::NUM_SECTIONS) { + isc_throw(OutOfRange, "Invalid message section: " << section); + } + impl_->rrsets_[section].clear(); + impl_->counts_[section] = 0; +} + void Message::addQuestion(const QuestionPtr question) { if (impl_->mode_ != Message::RENDER) { @@ -738,6 +776,27 @@ Message::clear(Mode mode) { impl_->mode_ = mode; } +void +Message::appendSection(const Section section, const Message& source) { + if (section >= MessageImpl::NUM_SECTIONS) { + isc_throw(OutOfRange, "Invalid message section: " << section); + } + + if (section == SECTION_QUESTION) { + for (QuestionIterator qi = source.beginQuestion(); + qi != source.endQuestion(); + ++qi) { + addQuestion(*qi); + } + } else { + for (RRsetIterator rrsi = source.beginSection(section); + rrsi != source.endSection(section); + ++rrsi) { + addRRset(section, *rrsi); + } + } +} + void Message::makeResponse() { if (impl_->mode_ != Message::PARSE) { diff --git a/src/lib/dns/message.h b/src/lib/dns/message.h index 153c7a9b507b221391167ac2d7013dea419eb685..11167d2c138db69edf4a2a7dd2679ed56a279fb8 100644 --- a/src/lib/dns/message.h +++ b/src/lib/dns/message.h @@ -460,9 +460,36 @@ public: bool hasRRset(const Section section, const Name& name, const RRClass& rrclass, const RRType& rrtype); + /// \brief Determine whether the given section already has an RRset + /// matching the one pointed to by the argumet + /// + /// \c section must be a valid constant of the \c Section type; + /// otherwise, an exception of class \c OutOfRange will be thrown. + bool hasRRset(const Section section, const RRsetPtr& rrset); + + /// \brief Remove RRSet from Message + /// + /// Removes the RRset identified by the section iterator from the message. + /// Note: if,.for some reason, the RRset is duplicated in the section, only + /// one occurrence is removed. + /// + /// If the operation is successful, all iterators into the section are + /// invalidated. + /// + /// \param section Section to which the iterator belongs + /// \param iterator Iterator pointing to the element to be removed + /// + /// \return true if the element was removed, false if the iterator was not + /// found in the specified section. + bool removeRRset(const Section section, RRsetIterator& iterator); + + /// \brief Remove all RRSets from the given Section + /// + /// \param section Section to remove all rrsets from + void clearSection(const Section section); + // The following methods are not currently implemented. //void removeQuestion(QuestionPtr question); - //void removeRRset(const Section section, RRsetPtr rrset); // notyet: //void addRR(const Section section, const RR& rr); //void removeRR(const Section section, const RR& rr); @@ -471,6 +498,13 @@ public: /// specified mode. void clear(Mode mode); + /// \brief Adds all rrsets from the source the given section in the + /// source message to the same section of this message + /// + /// \param section the section to append + /// \param target The source Message + void appendSection(const Section section, const Message& source); + /// \brief Prepare for making a response from a request. /// /// This will clear the DNS header except those fields that should be kept diff --git a/src/lib/dns/python/messagerenderer_python.cc b/src/lib/dns/python/messagerenderer_python.cc index 91ab0c5197b7579318c4802b80ca8bb0114d1db4..a00d8d4597ded795d20c87f05fdea3a29123af94 100644 --- a/src/lib/dns/python/messagerenderer_python.cc +++ b/src/lib/dns/python/messagerenderer_python.cc @@ -37,9 +37,10 @@ static PyObject* MessageRenderer_getData(s_MessageRenderer* self); static PyObject* MessageRenderer_getLength(s_MessageRenderer* self); static PyObject* MessageRenderer_isTruncated(s_MessageRenderer* self); static PyObject* MessageRenderer_getLengthLimit(s_MessageRenderer* self); -// TODO: set/get compressmode +static PyObject* MessageRenderer_getCompressMode(s_MessageRenderer* self); static PyObject* MessageRenderer_setTruncated(s_MessageRenderer* self); static PyObject* MessageRenderer_setLengthLimit(s_MessageRenderer* self, PyObject* args); +static PyObject* MessageRenderer_setCompressMode(s_MessageRenderer* self, PyObject* args); static PyObject* MessageRenderer_clear(s_MessageRenderer* self); static PyMethodDef MessageRenderer_methods[] = { @@ -51,10 +52,14 @@ static PyMethodDef MessageRenderer_methods[] = { "Returns True if the data is truncated" }, { "get_length_limit", reinterpret_cast(MessageRenderer_getLengthLimit), METH_NOARGS, "Returns the length limit of the data" }, + { "get_compress_mode", reinterpret_cast(MessageRenderer_getCompressMode), METH_NOARGS, + "Returns the current compression mode" }, { "set_truncated", reinterpret_cast(MessageRenderer_setTruncated), METH_NOARGS, "Sets truncated to true" }, { "set_length_limit", reinterpret_cast(MessageRenderer_setLengthLimit), METH_VARARGS, "Sets the length limit of the data to the given number" }, + { "set_compress_mode", reinterpret_cast(MessageRenderer_setCompressMode), METH_VARARGS, + "Sets the compression mode of the MessageRenderer" }, { "clear", reinterpret_cast(MessageRenderer_clear), METH_NOARGS, "Clear the internal buffer and other internal resources." }, @@ -158,6 +163,11 @@ MessageRenderer_getLengthLimit(s_MessageRenderer* self) { return (Py_BuildValue("I", self->messagerenderer->getLengthLimit())); } +static PyObject* +MessageRenderer_getCompressMode(s_MessageRenderer* self) { + return (Py_BuildValue("I", self->messagerenderer->getCompressMode())); +} + static PyObject* MessageRenderer_setTruncated(s_MessageRenderer* self) { self->messagerenderer->setTruncated(); @@ -176,6 +186,31 @@ MessageRenderer_setLengthLimit(s_MessageRenderer* self, Py_RETURN_NONE; } +static PyObject* +MessageRenderer_setCompressMode(s_MessageRenderer* self, + PyObject* args) +{ + unsigned int mode; + if (!PyArg_ParseTuple(args, "I", &mode)) { + return (NULL); + } + + if (mode == MessageRenderer::CASE_INSENSITIVE) { + self->messagerenderer->setCompressMode(MessageRenderer::CASE_INSENSITIVE); + // If we return NULL it is seen as an error, so use this for + // None returns, it also applies to CASE_SENSITIVE. + Py_RETURN_NONE; + } else if (mode == MessageRenderer::CASE_SENSITIVE) { + self->messagerenderer->setCompressMode(MessageRenderer::CASE_SENSITIVE); + Py_RETURN_NONE; + } else { + PyErr_SetString(PyExc_TypeError, + "MessageRenderer compress mode must be MessageRenderer.CASE_INSENSITIVE" + "or MessageRenderer.CASE_SENSITIVE"); + return (NULL); + } +} + static PyObject* MessageRenderer_clear(s_MessageRenderer* self) { self->messagerenderer->clear(); @@ -203,6 +238,14 @@ initModulePart_MessageRenderer(PyObject* mod) { return (false); } Py_INCREF(&messagerenderer_type); + + // Class variables + // These are added to the tp_dict of the type object + addClassVariable(messagerenderer_type, "CASE_INSENSITIVE", + Py_BuildValue("I", MessageRenderer::CASE_INSENSITIVE)); + addClassVariable(messagerenderer_type, "CASE_SENSITIVE", + Py_BuildValue("I", MessageRenderer::CASE_SENSITIVE)); + PyModule_AddObject(mod, "MessageRenderer", reinterpret_cast(&messagerenderer_type)); diff --git a/src/lib/dns/python/tests/messagerenderer_python_test.py b/src/lib/dns/python/tests/messagerenderer_python_test.py index 62e2d51712a633461206dc838c7c8729b8bbc037..544ad2376d07495138cf19e7d2f7c275c099b862 100644 --- a/src/lib/dns/python/tests/messagerenderer_python_test.py +++ b/src/lib/dns/python/tests/messagerenderer_python_test.py @@ -28,7 +28,7 @@ class MessageRendererTest(unittest.TestCase): c = RRClass("IN") t = RRType("A") ttl = RRTTL("3600") - + message = Message(Message.RENDER) message.set_qid(123) message.set_opcode(Opcode.QUERY()) @@ -56,14 +56,14 @@ class MessageRendererTest(unittest.TestCase): self.message1.to_wire(self.renderer1) self.message2.to_wire(self.renderer2) self.message2.to_wire(self.renderer3) - - + + def test_messagerenderer_get_data(self): data1 = b'\x00{\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\x03com\x00\x00\x01\x00\x01' self.assertEqual(data1, self.renderer1.get_data()) data2 = b'\x00{\x84\x00\x00\x01\x00\x00\x00\x02\x00\x00\x07example\x03com\x00\x00\x01\x00\x01\xc0\x0c\x00\x01\x00\x01\x00\x00\x0e\x10\x00\x04\xc0\x00\x02b\xc0\x0c\x00\x01\x00\x01\x00\x00\x0e\x10\x00\x04\xc0\x00\x02c' self.assertEqual(data2, self.renderer2.get_data()) - + def test_messagerenderer_get_length(self): self.assertEqual(29, self.renderer1.get_length()) self.assertEqual(61, self.renderer2.get_length()) @@ -79,6 +79,14 @@ class MessageRendererTest(unittest.TestCase): self.assertEqual(512, self.renderer2.get_length_limit()) self.assertEqual(50, self.renderer3.get_length_limit()) + def test_messagerenderer_get_compress_mode(self): + self.assertEqual(MessageRenderer.CASE_INSENSITIVE, + self.renderer1.get_compress_mode()) + self.assertEqual(MessageRenderer.CASE_INSENSITIVE, + self.renderer2.get_compress_mode()) + self.assertEqual(MessageRenderer.CASE_INSENSITIVE, + self.renderer3.get_compress_mode()) + def test_messagerenderer_set_truncated(self): self.assertFalse(self.renderer1.is_truncated()) self.renderer1.set_truncated() @@ -91,5 +99,17 @@ class MessageRendererTest(unittest.TestCase): self.assertEqual(1024, renderer.get_length_limit()) self.assertRaises(TypeError, renderer.set_length_limit, "wrong") + def test_messagerenderer_set_compress_mode(self): + renderer = MessageRenderer() + self.assertEqual(MessageRenderer.CASE_INSENSITIVE, + renderer.get_compress_mode()) + renderer.set_compress_mode(MessageRenderer.CASE_SENSITIVE) + self.assertEqual(MessageRenderer.CASE_SENSITIVE, + renderer.get_compress_mode()) + renderer.set_compress_mode(MessageRenderer.CASE_INSENSITIVE) + self.assertEqual(MessageRenderer.CASE_INSENSITIVE, + renderer.get_compress_mode()) + self.assertRaises(TypeError, renderer.set_compress_mode, "wrong") + if __name__ == '__main__': unittest.main() diff --git a/src/lib/dns/rdata/generic/detail/nsec_bitmap.cc b/src/lib/dns/rdata/generic/detail/nsec_bitmap.cc new file mode 100644 index 0000000000000000000000000000000000000000..a72058f703366da0e99f87f0ba8730605b7570ac --- /dev/null +++ b/src/lib/dns/rdata/generic/detail/nsec_bitmap.cc @@ -0,0 +1,78 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include + +#include + +#include + +using namespace std; + +namespace isc { +namespace dns { +namespace rdata { +namespace generic { +namespace detail { +namespace nsec { +void +checkRRTypeBitmaps(const char* const rrtype_name, + const vector& typebits) +{ + bool first = true; + unsigned int lastblock = 0; + const size_t total_len = typebits.size(); + size_t i = 0; + + while (i < total_len) { + if (i + 2 > total_len) { + isc_throw(DNSMessageFORMERR, rrtype_name << + " RDATA from wire: incomplete bit map field"); + } + const unsigned int block = typebits[i]; + const size_t len = typebits[i + 1]; + // Check that bitmap window blocks are in the correct order. + if (!first && block <= lastblock) { + isc_throw(DNSMessageFORMERR, rrtype_name << + " RDATA from wire: Disordered window blocks found: " + << lastblock << " then " << block); + } + // Check for legal length + if (len < 1 || len > 32) { + isc_throw(DNSMessageFORMERR, rrtype_name << + " RDATA from wire: Invalid bitmap length: " << len); + } + // Check for overflow. + i += 2; + if (i + len > total_len) { + isc_throw(DNSMessageFORMERR, rrtype_name << + " RDATA from wire: bitmap length too large: " << len); + } + // The last octet of the bitmap must be non zero. + if (typebits[i + len - 1] == 0) { + isc_throw(DNSMessageFORMERR, rrtype_name << + " RDATA from wire: bitmap ending an all-zero byte"); + } + + i += len; + lastblock = block; + first = false; + } +} +} +} +} +} +} +} diff --git a/src/lib/dns/rdata/generic/detail/nsec_bitmap.h b/src/lib/dns/rdata/generic/detail/nsec_bitmap.h new file mode 100644 index 0000000000000000000000000000000000000000..6431e10f956304daed743fcb8715bf61880746e2 --- /dev/null +++ b/src/lib/dns/rdata/generic/detail/nsec_bitmap.h @@ -0,0 +1,51 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include + +#include + +namespace isc { +namespace dns { +namespace rdata { +namespace generic { +namespace detail { +namespace nsec { +/// Check if a given "type bitmap" for NSEC/NSEC3 is valid. +/// +/// This helper function checks given wire format data (stored in a +/// \c std::vector) is a valid type bitmaps used for the NSEC and NSEC3 RRs +/// according to RFC4034 and RFC5155. The validation logic is the same +/// for these two RRs, so a unified check function is provided. +/// This function is essentially private and is only expected to be called +/// from the \c NSEC and \c NSEC3 class implementations. +/// +/// \exception DNSMessageFORMERR The bitmap is not valid. +/// +/// \param rrtype_name Either "NSEC" or "NSEC3"; used as part of exception +/// messages. +/// \param typebits The type bitmaps in wire format. The size of vector +/// is the total length of the bitmaps. +void checkRRTypeBitmaps(const char* const rrtype_name, + const std::vector& typebits); +} +} +} +} +} +} + +// Local Variables: +// mode: c++ +// End: diff --git a/src/lib/dns/rdata/generic/nsec3_50.cc b/src/lib/dns/rdata/generic/nsec3_50.cc index c20fda20a0d144bcf33c712e2398fe02f9e11f25..01ffefa1138dba8b628b483003be62ac4fc8b36c 100644 --- a/src/lib/dns/rdata/generic/nsec3_50.cc +++ b/src/lib/dns/rdata/generic/nsec3_50.cc @@ -30,11 +30,13 @@ #include #include #include +#include #include #include using namespace std; +using namespace isc::dns::rdata::generic::detail::nsec; // BEGIN_ISC_NAMESPACE // BEGIN_RDATA_NAMESPACE @@ -61,36 +63,57 @@ NSEC3::NSEC3(const string& nsec3_str) : { istringstream iss(nsec3_str); unsigned int hashalg, flags, iterations; - string salthex; + string iterations_str, salthex, nexthash; - iss >> hashalg >> flags >> iterations >> salthex; + iss >> hashalg >> flags >> iterations_str >> salthex >> nexthash; if (iss.bad() || iss.fail()) { - isc_throw(InvalidRdataText, "Invalid NSEC3 text"); + isc_throw(InvalidRdataText, "Invalid NSEC3 text: " << nsec3_str); } - if (hashalg > 0xf) { - isc_throw(InvalidRdataText, "NSEC3 hash algorithm out of range"); + if (hashalg > 0xff) { + isc_throw(InvalidRdataText, + "NSEC3 hash algorithm out of range: " << hashalg); } if (flags > 0xff) { - isc_throw(InvalidRdataText, "NSEC3 flags out of range"); + isc_throw(InvalidRdataText, "NSEC3 flags out of range: " << flags); + } + // Convert iteration. To reject an invalid case where there's no space + // between iteration and salt, we extract this field as string and convert + // to integer. + try { + iterations = boost::lexical_cast(iterations_str); + } catch (const boost::bad_lexical_cast&) { + isc_throw(InvalidRdataText, "Bad NSEC3 iteration: " << iterations_str); } if (iterations > 0xffff) { - isc_throw(InvalidRdataText, "NSEC3 iterations out of range"); + isc_throw(InvalidRdataText, "NSEC3 iterations out of range: " << + iterations); } vector salt; - decodeHex(salthex, salt); + if (salthex != "-") { // "-" means a 0-length salt + decodeHex(salthex, salt); + } + if (salt.size() > 255) { + isc_throw(InvalidRdataText, "NSEC3 salt is too long: " + << salt.size() << " bytes"); + } - string nextstr; - iss >> setw(32) >> nextstr; vector next; - if (iss.bad() || iss.fail()) { - isc_throw(InvalidRdataText, "Invalid NSEC3 hash algorithm"); + decodeBase32Hex(nexthash, next); + if (next.size() > 255) { + isc_throw(InvalidRdataText, "NSEC3 hash is too long: " + << next.size() << " bytes"); } - decodeBase32Hex(nextstr, next); - uint8_t bitmap[8 * 1024]; // 64k bits - vector typebits; + // For NSEC3 empty bitmap is possible and allowed. + if (iss.eof()) { + impl_ = new NSEC3Impl(hashalg, flags, iterations, salt, next, + vector()); + return; + } + vector typebits; + uint8_t bitmap[8 * 1024]; // 64k bits memset(bitmap, 0, sizeof(bitmap)); do { string type; @@ -104,7 +127,7 @@ NSEC3::NSEC3(const string& nsec3_str) : isc_throw(InvalidRdataText, "Invalid RRtype in NSEC3"); } } - } while(!iss.eof()); + } while (!iss.eof()); for (int window = 0; window < 256; window++) { int octet; @@ -126,56 +149,46 @@ NSEC3::NSEC3(const string& nsec3_str) : } NSEC3::NSEC3(InputBuffer& buffer, size_t rdata_len) { + // NSEC3 RR must have at least 5 octets: + // hash algorithm(1), flags(1), iteration(2), saltlen(1) if (rdata_len < 5) { - isc_throw(InvalidRdataLength, "NSEC3 too short"); + isc_throw(DNSMessageFORMERR, "NSEC3 too short, length: " << rdata_len); } - uint8_t hashalg = buffer.readUint8(); - uint8_t flags = buffer.readUint8(); - uint16_t iterations = buffer.readUint16(); - rdata_len -= 4; - - uint8_t saltlen = buffer.readUint8(); - --rdata_len; + const uint8_t hashalg = buffer.readUint8(); + const uint8_t flags = buffer.readUint8(); + const uint16_t iterations = buffer.readUint16(); + const uint8_t saltlen = buffer.readUint8(); + rdata_len -= 5; if (rdata_len < saltlen) { - isc_throw(InvalidRdataLength, "NSEC3 salt too short"); + isc_throw(DNSMessageFORMERR, "NSEC3 salt length is too large: " << + static_cast(saltlen)); } vector salt(saltlen); - buffer.readData(&salt[0], saltlen); - rdata_len -= saltlen; + if (saltlen > 0) { + buffer.readData(&salt[0], saltlen); + rdata_len -= saltlen; + } - uint8_t nextlen = buffer.readUint8(); + const uint8_t nextlen = buffer.readUint8(); --rdata_len; - - if (rdata_len < nextlen) { - isc_throw(InvalidRdataLength, "NSEC3 next hash too short"); + if (nextlen == 0 || rdata_len < nextlen) { + isc_throw(DNSMessageFORMERR, "NSEC3 invalid hash length: " << + static_cast(nextlen)); } vector next(nextlen); buffer.readData(&next[0], nextlen); rdata_len -= nextlen; - if (rdata_len == 0) { - isc_throw(InvalidRdataLength, "NSEC3 type bitmap too short"); - } - vector typebits(rdata_len); - buffer.readData(&typebits[0], rdata_len); - - int len = 0; - for (int i = 0; i < typebits.size(); i += len) { - if (i + 2 > typebits.size()) { - isc_throw(DNSMessageFORMERR, "Invalid rdata: " - "bad NSEC3 type bitmap"); - } - len = typebits[i + 1]; - if (len > 31) { - isc_throw(DNSMessageFORMERR, "Invalid rdata: " - "bad NSEC3 type bitmap"); - } - i += 2; + if (rdata_len > 0) { + // Read and parse the bitmaps only when they exist; empty bitmap + // is possible for NSEC3. + buffer.readData(&typebits[0], rdata_len); + checkRRTypeBitmaps("NSEC3", typebits); } impl_ = new NSEC3Impl(hashalg, flags, iterations, salt, next, typebits); @@ -327,10 +340,15 @@ NSEC3::getIterations() const { return (impl_->iterations_); } -vector& +const vector& NSEC3::getSalt() const { return (impl_->salt_); } +const vector& +NSEC3::getNext() const { + return (impl_->next_); +} + // END_RDATA_NAMESPACE // END_ISC_NAMESPACE diff --git a/src/lib/dns/rdata/generic/nsec3_50.h b/src/lib/dns/rdata/generic/nsec3_50.h index 55320712a52862de5d65b5724f7dfe39c90c3360..c766aded4ea91aaed3a33e4da6560d6dc6b1e99f 100644 --- a/src/lib/dns/rdata/generic/nsec3_50.h +++ b/src/lib/dns/rdata/generic/nsec3_50.h @@ -43,7 +43,8 @@ public: uint8_t getHashalg() const; uint8_t getFlags() const; uint16_t getIterations() const; - std::vector& getSalt() const; + const std::vector& getSalt() const; + const std::vector& getNext() const; private: NSEC3Impl* impl_; diff --git a/src/lib/dns/rdata/generic/nsec_47.cc b/src/lib/dns/rdata/generic/nsec_47.cc index 0859edd19ef56472a620431e6b9f84a721c926f7..5d92528f24f9673e2a9c7bb8f8334973f6c81296 100644 --- a/src/lib/dns/rdata/generic/nsec_47.cc +++ b/src/lib/dns/rdata/generic/nsec_47.cc @@ -26,11 +26,13 @@ #include #include #include +#include #include #include using namespace std; +using namespace isc::dns::rdata::generic::detail::nsec; // BEGIN_ISC_NAMESPACE // BEGIN_RDATA_NAMESPACE @@ -103,43 +105,7 @@ NSEC::NSEC(InputBuffer& buffer, size_t rdata_len) { vector typebits(rdata_len); buffer.readData(&typebits[0], rdata_len); - - int len = 0; - bool first = true; - unsigned int block, lastblock = 0; - for (int i = 0; i < rdata_len; i += len) { - if (i + 2 > rdata_len) { - isc_throw(DNSMessageFORMERR, "NSEC RDATA from wire: " - "incomplete bit map field"); - } - block = typebits[i]; - len = typebits[i + 1]; - // Check that bitmap window blocks are in the correct order. - if (!first && block <= lastblock) { - isc_throw(DNSMessageFORMERR, "NSEC RDATA from wire: Disordered " - "window blocks found: " << lastblock << - " then " << block); - } - // Check for legal length - if (len < 1 || len > 32) { - isc_throw(DNSMessageFORMERR, "NSEC RDATA from wire: Invalid bitmap " - "length: " << len); - } - // Check for overflow. - i += 2; - if (i + len > rdata_len) { - isc_throw(DNSMessageFORMERR, "NSEC RDATA from wire: bitmap length " - "too large: " << len); - } - // The last octet of the bitmap must be non zero. - if (typebits[i + len - 1] == 0) { - isc_throw(DNSMessageFORMERR, "NSEC RDATA from wire: bitmap ending " - "an all-zero byte"); - } - - lastblock = block; - first = false; - } + checkRRTypeBitmaps("NSEC", typebits); impl_ = new NSECImpl(nextname, typebits); } diff --git a/src/lib/dns/rdata/generic/rrsig_46.cc b/src/lib/dns/rdata/generic/rrsig_46.cc index 6e6c5fb93458832bc5ba2f85ac2cf40d8c5a9069..c9d1e525e894b2d907a9041f3870016a1791f060 100644 --- a/src/lib/dns/rdata/generic/rrsig_46.cc +++ b/src/lib/dns/rdata/generic/rrsig_46.cc @@ -93,8 +93,8 @@ RRSIG::RRSIG(const string& rrsig_str) : isc_throw(InvalidRdataText, "RRSIG labels out of range"); } - uint32_t timeexpire = timeFromText(expire_txt); - uint32_t timeinception = timeFromText(inception_txt); + const uint32_t timeexpire = timeFromText32(expire_txt); + const uint32_t timeinception = timeFromText32(inception_txt); vector signature; decodeBase64(signaturebuf.str(), signature); @@ -157,15 +157,12 @@ RRSIG::~RRSIG() { string RRSIG::toText() const { - string expire = timeToText(impl_->timeexpire_); - string inception = timeToText(impl_->timeinception_); - return (impl_->covered_.toText() + " " + boost::lexical_cast(static_cast(impl_->algorithm_)) + " " + boost::lexical_cast(static_cast(impl_->labels_)) + " " + boost::lexical_cast(impl_->originalttl_) - + " " + expire - + " " + inception + + " " + timeToText32(impl_->timeexpire_) + + " " + timeToText32(impl_->timeinception_) + " " + boost::lexical_cast(impl_->tag_) + " " + impl_->signer_.toText() + " " + encodeBase64(impl_->signature_)); diff --git a/src/lib/dns/rrset.h b/src/lib/dns/rrset.h index 4fc6cdcfe8b5a325463a1238afdf0598cfc9df34..acb42247e8dd952b3591049db97970bbc3fae579 100644 --- a/src/lib/dns/rrset.h +++ b/src/lib/dns/rrset.h @@ -229,8 +229,8 @@ public: /// \brief Updates the owner name of the \c RRset. /// - /// \param name A reference to a \c RRTTL class object to be copied as the - /// new TTL. + /// \param name A reference to a \c Name class object to be copied as the + /// new name. virtual void setName(const Name& name) = 0; /// \brief Updates the TTL of the \c RRset. @@ -588,8 +588,8 @@ public: /// internal copy of the \c name involves resource allocation and it /// fails. /// - /// \param name A reference to a \c RRTTL class object to be copied as the - /// new TTL. + /// \param name A reference to a \c Name class object to be copied as the + /// new name. virtual void setName(const Name& name); /// \brief Updates the TTL of the \c RRset. @@ -720,7 +720,7 @@ public: void removeRRsig() { rrsig_ = RRsetPtr(); } /// \brief Return a pointer to this RRset's RRSIG RRset - RRsetPtr getRRsig() { return (rrsig_); } + RRsetPtr getRRsig() const { return (rrsig_); } private: RRsetPtr rrsig_; }; diff --git a/src/lib/dns/tests/Makefile.am b/src/lib/dns/tests/Makefile.am index 48e4650903a604e8cd78fdaffeef620735dca262..246adb7a3a6eec152c8a6b793036ddfa64d0eecf 100644 --- a/src/lib/dns/tests/Makefile.am +++ b/src/lib/dns/tests/Makefile.am @@ -36,6 +36,7 @@ run_unittests_SOURCES += rdata_dnskey_unittest.cc run_unittests_SOURCES += rdata_ds_unittest.cc run_unittests_SOURCES += rdata_nsec_unittest.cc run_unittests_SOURCES += rdata_nsec3_unittest.cc +run_unittests_SOURCES += rdata_nsecbitmap_unittest.cc run_unittests_SOURCES += rdata_nsec3param_unittest.cc run_unittests_SOURCES += rdata_rrsig_unittest.cc run_unittests_SOURCES += rdata_tsig_unittest.cc diff --git a/src/lib/dns/tests/dnssectime_unittest.cc b/src/lib/dns/tests/dnssectime_unittest.cc index 2479a2957acf81a4cc3b6537a8691f3f0a50b00f..b2708cc6e834f3c7e3e93be9b7880976eba4f87a 100644 --- a/src/lib/dns/tests/dnssectime_unittest.cc +++ b/src/lib/dns/tests/dnssectime_unittest.cc @@ -23,48 +23,141 @@ using namespace std; using namespace isc::dns; +// See dnssectime.cc +namespace isc { +namespace dns { +namespace dnssectime { +namespace detail { +extern int64_t (*gettimeFunction)(); +} +} +} +} + namespace { -TEST(DNSSECTimeTest, fromText) { +class DNSSECTimeTest : public ::testing::Test { +protected: + ~DNSSECTimeTest() { + dnssectime::detail::gettimeFunction = NULL; + } +}; + +TEST_F(DNSSECTimeTest, fromText) { + // In most cases (in practice) the 32-bit and 64-bit versions should + // behave identically, so we'll mainly test the 32-bit version, which + // will be more commonly used in actual code (because many of the wire + // format time field are 32-bit). The subtle cases where these two + // return different values will be tested at the end of this test case. + // These are bogus and should be rejected - EXPECT_THROW(timeFromText("2011 101120000"), InvalidTime); - EXPECT_THROW(timeFromText("201101011200-0"), InvalidTime); + EXPECT_THROW(timeFromText32("2011 101120000"), InvalidTime); + EXPECT_THROW(timeFromText32("201101011200-0"), InvalidTime); - // Short length - EXPECT_THROW(timeFromText("20100223"), InvalidTime); + // Short length (or "decimal integer" version of representation; + // it's valid per RFC4034, but is not supported in this implementation) + EXPECT_THROW(timeFromText32("20100223"), InvalidTime); // Leap year checks - EXPECT_THROW(timeFromText("20110229120000"), InvalidTime); - EXPECT_THROW(timeFromText("21000229120000"), InvalidTime); - EXPECT_NO_THROW(timeFromText("20000229120000")); - EXPECT_NO_THROW(timeFromText("20120229120000")); + EXPECT_THROW(timeFromText32("20110229120000"), InvalidTime); + EXPECT_THROW(timeFromText32("21000229120000"), InvalidTime); + EXPECT_NO_THROW(timeFromText32("20000229120000")); + EXPECT_NO_THROW(timeFromText32("20120229120000")); // unusual case: this implementation allows SS=60 for "leap seconds" - EXPECT_NO_THROW(timeFromText("20110101120060")); + EXPECT_NO_THROW(timeFromText32("20110101120060")); // Out of range parameters - EXPECT_THROW(timeFromText("19100223214617"), InvalidTime); // YY<1970 - EXPECT_THROW(timeFromText("20110001120000"), InvalidTime); // MM=00 - EXPECT_THROW(timeFromText("20111301120000"), InvalidTime); // MM=13 - EXPECT_THROW(timeFromText("20110100120000"), InvalidTime); // DD=00 - EXPECT_THROW(timeFromText("20110132120000"), InvalidTime); // DD=32 - EXPECT_THROW(timeFromText("20110431120000"), InvalidTime); // 'Apr31' - EXPECT_THROW(timeFromText("20110101250000"), InvalidTime); // HH=25 - EXPECT_THROW(timeFromText("20110101126000"), InvalidTime); // mm=60 - EXPECT_THROW(timeFromText("20110101120061"), InvalidTime); // SS=61 + EXPECT_THROW(timeFromText32("19100223214617"), InvalidTime); // YY<1970 + EXPECT_THROW(timeFromText32("20110001120000"), InvalidTime); // MM=00 + EXPECT_THROW(timeFromText32("20111301120000"), InvalidTime); // MM=13 + EXPECT_THROW(timeFromText32("20110100120000"), InvalidTime); // DD=00 + EXPECT_THROW(timeFromText32("20110132120000"), InvalidTime); // DD=32 + EXPECT_THROW(timeFromText32("20110431120000"), InvalidTime); // 'Apr31' + EXPECT_THROW(timeFromText32("20110101250000"), InvalidTime); // HH=25 + EXPECT_THROW(timeFromText32("20110101126000"), InvalidTime); // mm=60 + EXPECT_THROW(timeFromText32("20110101120061"), InvalidTime); // SS=61 + + // Feb 7, 06:28:15 UTC 2106 is the possible maximum time that can be + // represented as an unsigned 32bit integer without overflow. + EXPECT_EQ(4294967295LU, timeFromText32("21060207062815")); + + // After that, timeFromText32() should start returning the second count + // modulo 2^32. + EXPECT_EQ(0, timeFromText32("21060207062816")); + EXPECT_EQ(10, timeFromText32("21060207062826")); + + // On the other hand, the 64-bit version should return monotonically + // increasing counters. + EXPECT_EQ(4294967296LL, timeFromText64("21060207062816")); + EXPECT_EQ(4294967306LL, timeFromText64("21060207062826")); } -TEST(DNSSECTimeTest, toText) { - EXPECT_EQ("19700101000000", timeToText(0)); - EXPECT_EQ("20100311233000", timeToText(1268350200)); +// This helper templated function tells timeToText32 a faked current time. +// The template parameter is that faked time in the form of int64_t seconds +// since epoch. +template +int64_t +testGetTime() { + return (NOW); } -TEST(DNSSECTimeTest, overflow) { +// Seconds since epoch for the year 10K eve. Commonly used in some tests +// below. +const uint64_t YEAR10K_EVE = 253402300799LL; + +TEST_F(DNSSECTimeTest, toText) { + // Check a basic case with the default (normal) gettimeFunction + // based on the "real current time". + // Note: this will fail after year 2078, but at that point we won't use + // this program anyway:-) + EXPECT_EQ("20100311233000", timeToText32(1268350200)); + + // Set the current time to: Feb 18 09:04:14 UTC 2012 (an arbitrary choice + // in the range of the first half of uint32 since epoch). + dnssectime::detail::gettimeFunction = testGetTime<1329555854LL>; + + // Test the "year 2038" problem. + // Check the result of toText() for "INT_MIN" in int32_t. It's in the + // 68-year range from the faked current time, so the result should be + // in year 2038, instead of 1901. + EXPECT_EQ("20380119031408", timeToText64(0x80000000L)); + EXPECT_EQ("20380119031408", timeToText32(0x80000000L)); + + // A controversial case: what should we do with "-1"? It's out of range + // in future, but according to RFC time before epoch doesn't seem to be + // considered "in-range" either. Our toText() implementation handles + // this range as a special case and always treats them as future time + // until year 2038. This won't be a real issue in practice, though, + // since such too large values won't be used in actual deployment by then. + EXPECT_EQ("21060207062815", timeToText32(0xffffffffL)); + + // After the singular point of year 2038, the first half of uint32 can + // point to a future time. + // Set the current time to: Apr 1 00:00:00 UTC 2038: + dnssectime::detail::gettimeFunction = testGetTime<2153692800LL>; + // then time "10" is Feb 7 06:28:26 UTC 2106 + EXPECT_EQ("21060207062826", timeToText32(10)); + // in 64-bit, it's 2^32 + 10 + EXPECT_EQ("21060207062826", timeToText64(0x10000000aLL)); + + // After year 2106, the upper half of uint32 can point to past time + // (as it should). + dnssectime::detail::gettimeFunction = testGetTime<0x10000000aLL>; + EXPECT_EQ("21060207062815", timeToText32(0xffffffffL)); + + // Try very large time value. Actually it's the possible farthest time + // that can be represented in the form of YYYYMMDDHHmmSS. + EXPECT_EQ("99991231235959", timeToText64(YEAR10K_EVE)); + dnssectime::detail::gettimeFunction = testGetTime; + EXPECT_EQ("99991231235959", timeToText32(4294197631LU)); +} + +TEST_F(DNSSECTimeTest, overflow) { // Jan 1, Year 10,000. - if (sizeof(time_t) > 4) { - EXPECT_THROW(timeToText(static_cast(253402300800LL)), - InvalidTime); - } + EXPECT_THROW(timeToText64(253402300800LL), InvalidTime); + dnssectime::detail::gettimeFunction = testGetTime; + EXPECT_THROW(timeToText32(4294197632LU), InvalidTime); } } diff --git a/src/lib/dns/tests/message_unittest.cc b/src/lib/dns/tests/message_unittest.cc index 65c5813b31fde48d97662632915398d9136d0988..92adbc975027d976aa25124a0c22976e7804cd83 100644 --- a/src/lib/dns/tests/message_unittest.cc +++ b/src/lib/dns/tests/message_unittest.cc @@ -250,8 +250,122 @@ TEST_F(MessageTest, hasRRset) { EXPECT_THROW(message_render.hasRRset(bogus_section, test_name, RRClass::IN(), RRType::A()), OutOfRange); + + // Repeat the checks having created an RRset of the appropriate type. + + RRsetPtr rrs1(new RRset(test_name, RRClass::IN(), RRType::A(), RRTTL(60))); + EXPECT_TRUE(message_render.hasRRset(Message::SECTION_ANSWER, rrs1)); + EXPECT_FALSE(message_render.hasRRset(Message::SECTION_ADDITIONAL, rrs1)); + + RRsetPtr rrs2(new RRset(Name("nomatch.example"), RRClass::IN(), RRType::A(), + RRTTL(5))); + EXPECT_FALSE(message_render.hasRRset(Message::SECTION_ANSWER, rrs2)); + + RRsetPtr rrs3(new RRset(test_name, RRClass::CH(), RRType::A(), RRTTL(60))); + EXPECT_FALSE(message_render.hasRRset(Message::SECTION_ANSWER, rrs3)); + + RRsetPtr rrs4(new RRset(test_name, RRClass::IN(), RRType::AAAA(), RRTTL(5))); + EXPECT_FALSE(message_render.hasRRset(Message::SECTION_ANSWER, rrs4)); + + RRsetPtr rrs5(new RRset(test_name, RRClass::IN(), RRType::AAAA(), RRTTL(5))); + EXPECT_FALSE(message_render.hasRRset(Message::SECTION_ANSWER, rrs4)); + + EXPECT_THROW(message_render.hasRRset(bogus_section, rrs1), OutOfRange); +} + +TEST_F(MessageTest, removeRRset) { + message_render.addRRset(Message::SECTION_ANSWER, rrset_a); + message_render.addRRset(Message::SECTION_ANSWER, rrset_aaaa); + EXPECT_TRUE(message_render.hasRRset(Message::SECTION_ANSWER, test_name, + RRClass::IN(), RRType::A())); + EXPECT_TRUE(message_render.hasRRset(Message::SECTION_ANSWER, test_name, + RRClass::IN(), RRType::AAAA())); + EXPECT_EQ(3, message_render.getRRCount(Message::SECTION_ANSWER)); + + // Locate the AAAA RRset and remove it; this has one RR in it. + RRsetIterator i = message_render.beginSection(Message::SECTION_ANSWER); + if ((*i)->getType() == RRType::A()) { + ++i; + } + EXPECT_EQ(RRType::AAAA(), (*i)->getType()); + message_render.removeRRset(Message::SECTION_ANSWER, i); + + EXPECT_TRUE(message_render.hasRRset(Message::SECTION_ANSWER, test_name, + RRClass::IN(), RRType::A())); + EXPECT_FALSE(message_render.hasRRset(Message::SECTION_ANSWER, test_name, + RRClass::IN(), RRType::AAAA())); + EXPECT_EQ(2, message_render.getRRCount(Message::SECTION_ANSWER)); +} + +TEST_F(MessageTest, clearQuestionSection) { + QuestionPtr q(new Question(Name("www.example.com"), RRClass::IN(), + RRType::A())); + message_render.addQuestion(q); + ASSERT_EQ(1, message_render.getRRCount(Message::SECTION_QUESTION)); + + message_render.clearSection(Message::SECTION_QUESTION); + EXPECT_EQ(0, message_render.getRRCount(Message::SECTION_QUESTION)); +} + + +TEST_F(MessageTest, clearAnswerSection) { + // Add two RRsets, check they are present, clear the section, + // check if they are gone. + message_render.addRRset(Message::SECTION_ANSWER, rrset_a); + message_render.addRRset(Message::SECTION_ANSWER, rrset_aaaa); + ASSERT_TRUE(message_render.hasRRset(Message::SECTION_ANSWER, test_name, + RRClass::IN(), RRType::A())); + ASSERT_TRUE(message_render.hasRRset(Message::SECTION_ANSWER, test_name, + RRClass::IN(), RRType::AAAA())); + ASSERT_EQ(3, message_render.getRRCount(Message::SECTION_ANSWER)); + + message_render.clearSection(Message::SECTION_ANSWER); + EXPECT_FALSE(message_render.hasRRset(Message::SECTION_ANSWER, test_name, + RRClass::IN(), RRType::A())); + EXPECT_FALSE(message_render.hasRRset(Message::SECTION_ANSWER, test_name, + RRClass::IN(), RRType::AAAA())); + EXPECT_EQ(0, message_render.getRRCount(Message::SECTION_ANSWER)); } +TEST_F(MessageTest, clearAuthoritySection) { + // Add two RRsets, check they are present, clear the section, + // check if they are gone. + message_render.addRRset(Message::SECTION_AUTHORITY, rrset_a); + message_render.addRRset(Message::SECTION_AUTHORITY, rrset_aaaa); + ASSERT_TRUE(message_render.hasRRset(Message::SECTION_AUTHORITY, test_name, + RRClass::IN(), RRType::A())); + ASSERT_TRUE(message_render.hasRRset(Message::SECTION_AUTHORITY, test_name, + RRClass::IN(), RRType::AAAA())); + ASSERT_EQ(3, message_render.getRRCount(Message::SECTION_AUTHORITY)); + + message_render.clearSection(Message::SECTION_AUTHORITY); + EXPECT_FALSE(message_render.hasRRset(Message::SECTION_AUTHORITY, test_name, + RRClass::IN(), RRType::A())); + EXPECT_FALSE(message_render.hasRRset(Message::SECTION_AUTHORITY, test_name, + RRClass::IN(), RRType::AAAA())); + EXPECT_EQ(0, message_render.getRRCount(Message::SECTION_AUTHORITY)); +} + +TEST_F(MessageTest, clearAdditionalSection) { + // Add two RRsets, check they are present, clear the section, + // check if they are gone. + message_render.addRRset(Message::SECTION_ADDITIONAL, rrset_a); + message_render.addRRset(Message::SECTION_ADDITIONAL, rrset_aaaa); + ASSERT_TRUE(message_render.hasRRset(Message::SECTION_ADDITIONAL, test_name, + RRClass::IN(), RRType::A())); + ASSERT_TRUE(message_render.hasRRset(Message::SECTION_ADDITIONAL, test_name, + RRClass::IN(), RRType::AAAA())); + ASSERT_EQ(3, message_render.getRRCount(Message::SECTION_ADDITIONAL)); + + message_render.clearSection(Message::SECTION_ADDITIONAL); + EXPECT_FALSE(message_render.hasRRset(Message::SECTION_ADDITIONAL, test_name, + RRClass::IN(), RRType::A())); + EXPECT_FALSE(message_render.hasRRset(Message::SECTION_ADDITIONAL, test_name, + RRClass::IN(), RRType::AAAA())); + EXPECT_EQ(0, message_render.getRRCount(Message::SECTION_ADDITIONAL)); +} + + TEST_F(MessageTest, badBeginSection) { // valid cases are tested via other tests EXPECT_THROW(message_render.beginSection(Message::SECTION_QUESTION), @@ -266,6 +380,63 @@ TEST_F(MessageTest, badEndSection) { EXPECT_THROW(message_render.endSection(bogus_section), OutOfRange); } +TEST_F(MessageTest, appendSection) { + Message target(Message::RENDER); + + // Section check + EXPECT_THROW(target.appendSection(bogus_section, message_render), + OutOfRange); + + // Make sure nothing is copied if there is nothing to copy + target.appendSection(Message::SECTION_QUESTION, message_render); + EXPECT_EQ(0, target.getRRCount(Message::SECTION_QUESTION)); + target.appendSection(Message::SECTION_ANSWER, message_render); + EXPECT_EQ(0, target.getRRCount(Message::SECTION_ANSWER)); + target.appendSection(Message::SECTION_AUTHORITY, message_render); + EXPECT_EQ(0, target.getRRCount(Message::SECTION_AUTHORITY)); + target.appendSection(Message::SECTION_ADDITIONAL, message_render); + EXPECT_EQ(0, target.getRRCount(Message::SECTION_ADDITIONAL)); + + // Now add some data, copy again, and see if it got added + message_render.addQuestion(Question(Name("test.example.com"), + RRClass::IN(), RRType::A())); + message_render.addRRset(Message::SECTION_ANSWER, rrset_a); + message_render.addRRset(Message::SECTION_AUTHORITY, rrset_a); + message_render.addRRset(Message::SECTION_ADDITIONAL, rrset_a); + message_render.addRRset(Message::SECTION_ADDITIONAL, rrset_aaaa); + + target.appendSection(Message::SECTION_QUESTION, message_render); + EXPECT_EQ(1, target.getRRCount(Message::SECTION_QUESTION)); + + target.appendSection(Message::SECTION_ANSWER, message_render); + EXPECT_EQ(2, target.getRRCount(Message::SECTION_ANSWER)); + EXPECT_TRUE(target.hasRRset(Message::SECTION_ANSWER, test_name, + RRClass::IN(), RRType::A())); + + target.appendSection(Message::SECTION_AUTHORITY, message_render); + EXPECT_EQ(2, target.getRRCount(Message::SECTION_AUTHORITY)); + EXPECT_TRUE(target.hasRRset(Message::SECTION_AUTHORITY, test_name, + RRClass::IN(), RRType::A())); + + target.appendSection(Message::SECTION_ADDITIONAL, message_render); + EXPECT_EQ(3, target.getRRCount(Message::SECTION_ADDITIONAL)); + EXPECT_TRUE(target.hasRRset(Message::SECTION_ADDITIONAL, test_name, + RRClass::IN(), RRType::A())); + EXPECT_TRUE(target.hasRRset(Message::SECTION_ADDITIONAL, test_name, + RRClass::IN(), RRType::AAAA())); + + // One more test, test to see if the section gets added, not replaced + Message source2(Message::RENDER); + source2.addRRset(Message::SECTION_ANSWER, rrset_aaaa); + target.appendSection(Message::SECTION_ANSWER, source2); + EXPECT_EQ(3, target.getRRCount(Message::SECTION_ANSWER)); + EXPECT_TRUE(target.hasRRset(Message::SECTION_ANSWER, test_name, + RRClass::IN(), RRType::A())); + EXPECT_TRUE(target.hasRRset(Message::SECTION_ANSWER, test_name, + RRClass::IN(), RRType::AAAA())); + +} + TEST_F(MessageTest, fromWire) { factoryFromFile(message_parse, "message_fromWire1"); EXPECT_EQ(0x1035, message_parse.getQid()); diff --git a/src/lib/dns/tests/rdata_mx_unittest.cc b/src/lib/dns/tests/rdata_mx_unittest.cc index 4491f8609edbce9cac37cd3ad59c3c93a51cd86d..dd7677db8940c686a4abcc5d1ccab165e1c06aba 100644 --- a/src/lib/dns/tests/rdata_mx_unittest.cc +++ b/src/lib/dns/tests/rdata_mx_unittest.cc @@ -74,12 +74,9 @@ TEST_F(Rdata_MX_Test, toWireRenderer) { TEST_F(Rdata_MX_Test, toWireBuffer) { renderer.writeName(Name("example.com")); rdata_mx.toWire(obuffer); -} -TEST_F(Rdata_MX_Test, DISABLED_toWireBuffer) { -// XXX: does not pass vector data; - UnitTestUtil::readWireData("rdata_mx_toWire1", data); + UnitTestUtil::readWireData("rdata_mx_toWire2", data); EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData, obuffer.getData(), obuffer.getLength(), &data[0], data.size()); } diff --git a/src/lib/dns/tests/rdata_nsec3_unittest.cc b/src/lib/dns/tests/rdata_nsec3_unittest.cc index 6b3a0b54ea4d1ef0043a60c7b3060b7bbe6d0bff..749e26232499b04de164217ff26c37e6f42434b7 100644 --- a/src/lib/dns/tests/rdata_nsec3_unittest.cc +++ b/src/lib/dns/tests/rdata_nsec3_unittest.cc @@ -46,59 +46,136 @@ public: string nsec3_txt; }; +TEST_F(Rdata_NSEC3_Test, fromText) { + // A normal case: the test constructor should successfully parse the + // text and construct nsec3_txt. It will be tested against the wire format + // representation in the createFromWire test. + + // Numeric parameters have possible maximum values. Unusual, but must + // be accepted. + EXPECT_NO_THROW(generic::NSEC3("255 255 65535 D399EAAB " + "H9RSFB7FPF2L8HG35CMPC765TDK23RP6 " + "NS SOA RRSIG DNSKEY NSEC3PARAM")); + + // 0-length salt + EXPECT_EQ(0, generic::NSEC3("1 1 1 - H9RSFB7FPF2L8HG35CMPC765TDK23RP6 " + "A").getSalt().size()); + + // salt that has the possible max length + EXPECT_EQ(255, generic::NSEC3("1 1 1 " + string(255 * 2, '0') + + " H9RSFB7FPF2L8HG35CMPC765TDK23RP6 " + "NS").getSalt().size()); + + // hash that has the possible max length (see badText about the magic + // numbers) + EXPECT_EQ(255, generic::NSEC3("1 1 1 D399EAAB " + + string((255 * 8) / 5, '0') + + " NS").getNext().size()); + + // type bitmap is empty. it's possible and allowed for NSEC3. + EXPECT_NO_THROW(generic::NSEC3( + "1 1 1 D399EAAB H9RSFB7FPF2L8HG35CMPC765TDK23RP6")); +} + TEST_F(Rdata_NSEC3_Test, toText) { const generic::NSEC3 rdata_nsec3(nsec3_txt); EXPECT_EQ(nsec3_txt, rdata_nsec3.toText()); } TEST_F(Rdata_NSEC3_Test, badText) { - EXPECT_THROW(generic::NSEC3 rdata_nsec3("1 1 1 ADDAFEEE " - "0123456789ABCDEFGHIJKLMNOPQRSTUV " - "BIFF POW SPOON"), + EXPECT_THROW(generic::NSEC3("1 1 1 ADDAFEEE " + "0123456789ABCDEFGHIJKLMNOPQRSTUV " + "BIFF POW SPOON"), InvalidRdataText); - EXPECT_THROW(generic::NSEC3 rdata_nsec3("1 1 1 ADDAFEE " - "WXYZWXYZWXYZ=WXYZWXYZ==WXYZWXYZW " - "A NS SOA"), + EXPECT_THROW(generic::NSEC3("1 1 1 ADDAFEE " + "WXYZWXYZWXYZ=WXYZWXYZ==WXYZWXYZW A NS SOA"), BadValue); // bad hex - EXPECT_THROW(generic::NSEC3 rdata_nsec3("1 1 1 ADDAFEEE " - "WXYZWXYZWXYZ=WXYZWXYZ==WXYZWXYZW " - "A NS SOA"), + EXPECT_THROW(generic::NSEC3("1 1 1 -- H9RSFB7FPF2L8HG35CMPC765TDK23RP6 " + "A"), + BadValue); // this shouldn't be confused a valid empty salt + EXPECT_THROW(generic::NSEC3("1 1 1 ADDAFEEE " + "WXYZWXYZWXYZ=WXYZWXYZ==WXYZWXYZW A NS SOA"), BadValue); // bad base32hex - EXPECT_THROW(generic::NSEC3 rdata_nsec3("1000000 1 1 ADDAFEEE " - "0123456789ABCDEFGHIJKLMNOPQRSTUV " - "A NS SOA"), + EXPECT_THROW(generic::NSEC3("1000000 1 1 ADDAFEEE " + "0123456789ABCDEFGHIJKLMNOPQRSTUV A NS SOA"), InvalidRdataText); - EXPECT_THROW(generic::NSEC3 rdata_nsec3("1 1000000 1 ADDAFEEE " - "0123456789ABCDEFGHIJKLMNOPQRSTUV " - "A NS SOA"), + EXPECT_THROW(generic::NSEC3("1 1000000 1 ADDAFEEE " + "0123456789ABCDEFGHIJKLMNOPQRSTUV A NS SOA"), InvalidRdataText); - EXPECT_THROW(generic::NSEC3 rdata_nsec3("1 1 1000000 ADDAFEEE " - "0123456789ABCDEFGHIJKLMNOPQRSTUV " - "A NS SOA"), + EXPECT_THROW(generic::NSEC3("1 1 1000000 ADDAFEEE " + "0123456789ABCDEFGHIJKLMNOPQRSTUV A NS SOA"), InvalidRdataText); -} -TEST_F(Rdata_NSEC3_Test, DISABLED_badText) { // this currently fails + // There should be a space between "1" and "D399EAAB" (salt) EXPECT_THROW(generic::NSEC3( "1 1 1D399EAAB H9RSFB7FPF2L8HG35CMPC765TDK23RP6 " "NS SOA RRSIG DNSKEY NSEC3PARAM"), InvalidRdataText); + + // Salt is too long (possible max + 1 bytes) + EXPECT_THROW(generic::NSEC3("1 1 1 " + string(256 * 2, '0') + + " H9RSFB7FPF2L8HG35CMPC765TDK23RP6 NS"), + InvalidRdataText); + + // Hash is too long. Max = 255 bytes, base32-hex converts each 5 bytes + // of the original to 8 characters, so 260 * 8 / 5 is the smallest length + // of the encoded string that exceeds the max and doesn't require padding. + EXPECT_THROW(generic::NSEC3("1 1 1 D399EAAB " + string((260 * 8) / 5, '0') + + " NS"), + InvalidRdataText); } TEST_F(Rdata_NSEC3_Test, createFromWire) { + // Normal case const generic::NSEC3 rdata_nsec3(nsec3_txt); EXPECT_EQ(0, rdata_nsec3.compare( *rdataFactoryFromFile(RRType::NSEC3(), RRClass::IN(), "rdata_nsec3_fromWire1"))); - // Too short RDLENGTH + // A valid NSEC3 RR with empty type bitmap. + EXPECT_NO_THROW(rdataFactoryFromFile(RRType::NSEC3(), RRClass::IN(), + "rdata_nsec3_fromWire15.wire")); + + // Too short RDLENGTH: it doesn't even contain the first 5 octets. EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC3(), RRClass::IN(), - "rdata_nsec3_fromWire2"), - InvalidRdataLength); + "rdata_nsec3_fromWire2.wire"), + DNSMessageFORMERR); + + // Invalid bitmap cases are tested in Rdata_NSECBITMAP_Test. - // Invalid type bits + // salt length is too large EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC3(), RRClass::IN(), - "rdata_nsec3_fromWire3"), + "rdata_nsec3_fromWire11.wire"), DNSMessageFORMERR); + + // empty salt. unusual, but valid. + ConstRdataPtr rdata = + rdataFactoryFromFile(RRType::NSEC3(), RRClass::IN(), + "rdata_nsec3_fromWire13.wire"); + EXPECT_EQ(0, dynamic_cast(*rdata).getSalt().size()); + + // hash length is too large + EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC3(), RRClass::IN(), + "rdata_nsec3_fromWire12.wire"), + DNSMessageFORMERR); + + // empty hash. invalid. + EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC3(), RRClass::IN(), + "rdata_nsec3_fromWire14.wire"), + DNSMessageFORMERR); + + // + // Short buffer cases. The data is valid NSEC3 RDATA, but the buffer + // is trimmed at the end. All cases should result in an exception from + // the buffer class. + vector data; + UnitTestUtil::readWireData("rdata_nsec3_fromWire1", data); + const uint16_t rdlen = (data.at(0) << 8) + data.at(1); + for (int i = 0; i < rdlen; ++i) { + // intentionally construct a short buffer + InputBuffer b(&data[0] + 2, i); + EXPECT_THROW(createRdata(RRType::NSEC3(), RRClass::IN(), b, 39), + InvalidBufferPosition); + } } TEST_F(Rdata_NSEC3_Test, toWireRenderer) { diff --git a/src/lib/dns/tests/rdata_nsec_unittest.cc b/src/lib/dns/tests/rdata_nsec_unittest.cc index f9ad027eff3c0d1df8aec359348553da8072a04c..8286dee53039f219e5507bf40220dfd56be56733 100644 --- a/src/lib/dns/tests/rdata_nsec_unittest.cc +++ b/src/lib/dns/tests/rdata_nsec_unittest.cc @@ -62,46 +62,7 @@ TEST_F(Rdata_NSEC_Test, createFromWire_NSEC) { "rdata_nsec_fromWire2"), DNSMessageFORMERR); - EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC(), RRClass::IN(), - "rdata_nsec_fromWire3"), - DNSMessageFORMERR); - - // A malformed NSEC bitmap length field that could cause overflow. - EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC(), RRClass::IN(), - "rdata_nsec_fromWire4.wire"), - DNSMessageFORMERR); - - // The bitmap field is incomplete (only the first byte is included) - EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC(), RRClass::IN(), - "rdata_nsec_fromWire5.wire"), - DNSMessageFORMERR); - - // Bitmap length is 0, which is invalid. - EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC(), RRClass::IN(), - "rdata_nsec_fromWire6.wire"), - DNSMessageFORMERR); - - // A boundary case: longest possible bitmaps (32 maps). This should be - // accepted. - EXPECT_NO_THROW(rdataFactoryFromFile(RRType::NSEC(), RRClass::IN(), - "rdata_nsec_fromWire7.wire")); - - // Another boundary condition: 33 bitmaps, which should be rejected. - EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC(), RRClass::IN(), - "rdata_nsec_fromWire8.wire"), - DNSMessageFORMERR); - - // Disordered bitmap window blocks. - EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC(), RRClass::IN(), - "rdata_nsec_fromWire9.wire"), - DNSMessageFORMERR); - - // Bitmap ending with all-zero bytes. Not necessarily harmful except - // the additional overhead of parsing, but invalid according to the - // spec anyway. - EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC(), RRClass::IN(), - "rdata_nsec_fromWire10.wire"), - DNSMessageFORMERR); + // Invalid bitmap cases are tested in Rdata_NSECBITMAP_Test. } TEST_F(Rdata_NSEC_Test, toWireRenderer_NSEC) { diff --git a/src/lib/dns/tests/rdata_nsecbitmap_unittest.cc b/src/lib/dns/tests/rdata_nsecbitmap_unittest.cc new file mode 100644 index 0000000000000000000000000000000000000000..8a908781ea50fae6682a87f22ff29c3496235fdc --- /dev/null +++ b/src/lib/dns/tests/rdata_nsecbitmap_unittest.cc @@ -0,0 +1,103 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include +#include +#include + +#include + +#include + +using namespace isc::dns; +using namespace isc::dns::rdata; + +namespace { +class Rdata_NSECBITMAP_Test : public RdataTest { + // there's nothing to specialize +}; + +// Tests against various types of bogus NSEC/NSEC3 type bitmaps. +// The syntax and semantics are common for both RR types, and our +// implementation of that part is shared, so in theory it should be sufficient +// to test for only one RR type. But we check for both just in case. +TEST_F(Rdata_NSECBITMAP_Test, createFromWire_NSEC) { + // A malformed NSEC bitmap length field that could cause overflow. + EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC(), RRClass::IN(), + "rdata_nsec_fromWire4.wire"), + DNSMessageFORMERR); + EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC3(), RRClass::IN(), + "rdata_nsec3_fromWire4.wire"), + DNSMessageFORMERR); + + // The bitmap field is incomplete (only the first byte is included) + EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC(), RRClass::IN(), + "rdata_nsec_fromWire5.wire"), + DNSMessageFORMERR); + EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC3(), RRClass::IN(), + "rdata_nsec3_fromWire5.wire"), + DNSMessageFORMERR); + + // Bitmap length is 0, which is invalid. + EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC(), RRClass::IN(), + "rdata_nsec_fromWire6.wire"), + DNSMessageFORMERR); + EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC3(), RRClass::IN(), + "rdata_nsec3_fromWire6.wire"), + DNSMessageFORMERR); + + // Too large bitmap length with a short buffer. + EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC(), RRClass::IN(), + "rdata_nsec_fromWire3"), + DNSMessageFORMERR); + EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC3(), RRClass::IN(), + "rdata_nsec3_fromWire3"), + DNSMessageFORMERR); + + // A boundary case: longest possible bitmaps (32 maps). This should be + // accepted. + EXPECT_NO_THROW(rdataFactoryFromFile(RRType::NSEC(), RRClass::IN(), + "rdata_nsec_fromWire7.wire")); + EXPECT_NO_THROW(rdataFactoryFromFile(RRType::NSEC3(), RRClass::IN(), + "rdata_nsec3_fromWire7.wire")); + + // Another boundary condition: 33 bitmaps, which should be rejected. + EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC(), RRClass::IN(), + "rdata_nsec_fromWire8.wire"), + DNSMessageFORMERR); + EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC3(), RRClass::IN(), + "rdata_nsec3_fromWire8.wire"), + DNSMessageFORMERR); + + // Disordered bitmap window blocks. + EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC(), RRClass::IN(), + "rdata_nsec_fromWire9.wire"), + DNSMessageFORMERR); + EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC3(), RRClass::IN(), + "rdata_nsec3_fromWire9.wire"), + DNSMessageFORMERR); + + // Bitmap ending with all-zero bytes. Not necessarily harmful except + // the additional overhead of parsing, but invalid according to the + // spec anyway. + EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC(), RRClass::IN(), + "rdata_nsec_fromWire10.wire"), + DNSMessageFORMERR); + EXPECT_THROW(rdataFactoryFromFile(RRType::NSEC3(), RRClass::IN(), + "rdata_nsec3_fromWire10.wire"), + DNSMessageFORMERR); +} +} diff --git a/src/lib/dns/tests/testdata/Makefile.am b/src/lib/dns/tests/testdata/Makefile.am index f6eb90b0ea0d2bd997f7edbf454c3423136661d8..1aaddb68b0fad2fb1ca92a7a53fdd5dca0914ac9 100644 --- a/src/lib/dns/tests/testdata/Makefile.am +++ b/src/lib/dns/tests/testdata/Makefile.am @@ -8,9 +8,16 @@ BUILT_SOURCES += rdata_nsec_fromWire4.wire rdata_nsec_fromWire5.wire BUILT_SOURCES += rdata_nsec_fromWire6.wire rdata_nsec_fromWire7.wire BUILT_SOURCES += rdata_nsec_fromWire8.wire rdata_nsec_fromWire9.wire BUILT_SOURCES += rdata_nsec_fromWire10.wire +BUILT_SOURCES += rdata_nsec3_fromWire2.wire +BUILT_SOURCES += rdata_nsec3_fromWire4.wire rdata_nsec3_fromWire5.wire +BUILT_SOURCES += rdata_nsec3_fromWire6.wire rdata_nsec3_fromWire7.wire +BUILT_SOURCES += rdata_nsec3_fromWire8.wire rdata_nsec3_fromWire9.wire +BUILT_SOURCES += rdata_nsec3_fromWire10.wire rdata_nsec3_fromWire11.wire +BUILT_SOURCES += rdata_nsec3_fromWire12.wire rdata_nsec3_fromWire13.wire +BUILT_SOURCES += rdata_nsec3_fromWire14.wire rdata_nsec3_fromWire15.wire BUILT_SOURCES += rdata_rrsig_fromWire2.wire BUILT_SOURCES += rdata_soa_toWireUncompressed.wire -BUILT_SOURCES += rdata_txt_fromWire2.wire rdata_txt_fromWire3.wire +BUILT_SOURCES += rdata_txt_fromWire2.wire rdata_txt_fromWire3.wire BUILT_SOURCES += rdata_txt_fromWire4.wire rdata_txt_fromWire5.wire BUILT_SOURCES += rdata_tsig_fromWire1.wire rdata_tsig_fromWire2.wire BUILT_SOURCES += rdata_tsig_fromWire3.wire rdata_tsig_fromWire4.wire @@ -43,14 +50,21 @@ EXTRA_DIST += name_toWire5.spec name_toWire6.spec EXTRA_DIST += question_fromWire question_toWire1 question_toWire2 EXTRA_DIST += rdata_cname_fromWire rdata_dname_fromWire rdata_dnskey_fromWire EXTRA_DIST += rdata_ds_fromWire rdata_in_a_fromWire rdata_in_aaaa_fromWire -EXTRA_DIST += rdata_mx_fromWire rdata_mx_toWire1 rdata_ns_fromWire -EXTRA_DIST += rdata_nsec3_fromWire1 rdata_nsec3_fromWire2 rdata_nsec3_fromWire3 -EXTRA_DIST += rdata_nsec3param_fromWire1 rdata_nsec_fromWire1 -EXTRA_DIST += rdata_nsec_fromWire2 rdata_nsec_fromWire3 +EXTRA_DIST += rdata_mx_fromWire rdata_mx_toWire1 rdata_mx_toWire2 +EXTRA_DIST += rdata_ns_fromWire +EXTRA_DIST += rdata_nsec_fromWire1 rdata_nsec_fromWire2 rdata_nsec_fromWire3 EXTRA_DIST += rdata_nsec_fromWire4.spec rdata_nsec_fromWire5.spec EXTRA_DIST += rdata_nsec_fromWire6.spec rdata_nsec_fromWire7.spec EXTRA_DIST += rdata_nsec_fromWire8.spec rdata_nsec_fromWire9.spec EXTRA_DIST += rdata_nsec_fromWire10.spec +EXTRA_DIST += rdata_nsec3param_fromWire1 +EXTRA_DIST += rdata_nsec3_fromWire1 rdata_nsec3_fromWire3 +EXTRA_DIST += rdata_nsec3_fromWire4.spec rdata_nsec3_fromWire5.spec +EXTRA_DIST += rdata_nsec3_fromWire6.spec rdata_nsec3_fromWire7.spec +EXTRA_DIST += rdata_nsec3_fromWire8.spec rdata_nsec3_fromWire9.spec +EXTRA_DIST += rdata_nsec3_fromWire10.spec rdata_nsec3_fromWire11.spec +EXTRA_DIST += rdata_nsec3_fromWire12.spec rdata_nsec3_fromWire13.spec +EXTRA_DIST += rdata_nsec3_fromWire14.spec rdata_nsec3_fromWire15.spec EXTRA_DIST += rdata_opt_fromWire rdata_rrsig_fromWire1 EXTRA_DIST += rdata_rrsig_fromWire2.spec EXTRA_DIST += rdata_soa_fromWire rdata_soa_toWireUncompressed.spec @@ -68,6 +82,7 @@ EXTRA_DIST += rdata_tsig_fromWire9.spec EXTRA_DIST += rdata_tsig_toWire1.spec rdata_tsig_toWire2.spec EXTRA_DIST += rdata_tsig_toWire3.spec rdata_tsig_toWire4.spec EXTRA_DIST += rdata_tsig_toWire5.spec +EXTRA_DIST += rdata_nsec3_fromWire2.spec .spec.wire: ./gen-wiredata.py -o $@ $< diff --git a/src/lib/dns/tests/testdata/gen-wiredata.py.in b/src/lib/dns/tests/testdata/gen-wiredata.py.in index e59063f08e189c79329974fdedc3be610fb32735..645430c63fc02a63183a6639c263e2995396c5fc 100755 --- a/src/lib/dns/tests/testdata/gen-wiredata.py.in +++ b/src/lib/dns/tests/testdata/gen-wiredata.py.in @@ -52,8 +52,11 @@ rdict_rrclass = dict([(dict_rrclass[k], k.upper()) for k in \ dict_rrclass.keys()]) dict_algorithm = { 'rsamd5' : 1, 'dh' : 2, 'dsa' : 3, 'ecc' : 4, 'rsasha1' : 5 } +dict_nsec3_algorithm = { 'reserved' : 0, 'sha1' : 1 } rdict_algorithm = dict([(dict_algorithm[k], k.upper()) for k in \ dict_algorithm.keys()]) +rdict_nsec3_algorithm = dict([(dict_nsec3_algorithm[k], k.upper()) for k in \ + dict_nsec3_algorithm.keys()]) header_xtables = { 'qr' : dict_qr, 'opcode' : dict_opcode, 'rcode' : dict_rcode } @@ -274,14 +277,16 @@ class TXT: ' ' if len(wirestring_list[i]) > 0 else '', wirestring_list[i])) -class NSEC: - rdlen = -1 # auto-calculate - nextname = 'next.example.com' +class NSECBASE: + '''Implements rendering NSEC/NSEC3 type bitmaps commonly used for + these RRs. The NSEC and NSEC3 classes will be inherited from this + class.''' nbitmap = 1 # number of bitmaps block = 0 - maplen = -1 # default bitmap length, auto-calculate + maplen = None # default bitmap length, auto-calculate bitmap = '040000000003' # an arbtrarily chosen bitmap sample def dump(self, f): + # first, construct the bitmpa data block_list = [] maplen_list = [] bitmap_list = [] @@ -296,30 +301,72 @@ class NSEC: maplen_list.append(self.__dict__[key_maplen]) else: maplen_list.append(self.maplen) - if maplen_list[-1] < 0: + if maplen_list[-1] is None: # calculate it if not specified maplen_list[-1] = int(len(bitmap_list[-1]) / 2) key_block = 'block' + str(i) if key_block in self.__dict__: block_list.append(self.__dict__[key_block]) else: block_list.append(self.block) + + # dump RR-type specific part (NSEC or NSEC3) + self.dump_fixedpart(f, 2 * self.nbitmap + \ + int(len(''.join(bitmap_list)) / 2)) + + # dump the bitmap + for i in range(0, self.nbitmap): + f.write('# Bitmap: Block=%d, Length=%d\n' % + (block_list[i], maplen_list[i])) + f.write('%02x %02x %s\n' % + (block_list[i], maplen_list[i], bitmap_list[i])) + +class NSEC(NSECBASE): + rdlen = None # auto-calculate + nextname = 'next.example.com' + def dump_fixedpart(self, f, bitmap_totallen): name_wire = encode_name(self.nextname) - rdlen = self.rdlen - if rdlen < 0: + if self.rdlen is None: # if rdlen needs to be calculated, it must be based on the bitmap # length, because the configured maplen can be fake. - rdlen = int(len(name_wire) / 2) + 2 * self.nbitmap - rdlen = rdlen + int(len(''.join(bitmap_list)) / 2) - f.write('\n# NSEC RDATA (RDLEN=%d)\n' % rdlen) - f.write('%04x\n' % rdlen); + self.rdlen = int(len(name_wire) / 2) + bitmap_totallen + f.write('\n# NSEC RDATA (RDLEN=%d)\n' % self.rdlen) + f.write('%04x\n' % self.rdlen); f.write('# Next Name=%s (%d bytes)\n' % (self.nextname, int(len(name_wire) / 2))) f.write('%s\n' % name_wire) - for i in range(0, self.nbitmap): - f.write('# Bitmap: Block=%d, Length=%d\n' % - (block_list[i], maplen_list[i])) - f.write('%02x %02x %s\n' % - (block_list[i], maplen_list[i], bitmap_list[i])) + +class NSEC3(NSECBASE): + rdlen = None # auto-calculate + hashalg = 1 # SHA-1 + optout = False # opt-out flag + mbz = 0 # other flag fields (none defined yet) + iterations = 1 + saltlen = 5 + salt = 's' * saltlen + hashlen = 20 + hash = 'h' * hashlen + def dump_fixedpart(self, f, bitmap_totallen): + if self.rdlen is None: + # if rdlen needs to be calculated, it must be based on the bitmap + # length, because the configured maplen can be fake. + self.rdlen = 4 + 1 + len(self.salt) + 1 + len(self.hash) \ + + bitmap_totallen + f.write('\n# NSEC3 RDATA (RDLEN=%d)\n' % self.rdlen) + f.write('%04x\n' % self.rdlen) + optout_val = 1 if self.optout else 0 + f.write('# Hash Alg=%s, Opt-Out=%d, Other Flags=%0x, Iterations=%d\n' % + (code_totext(self.hashalg, rdict_nsec3_algorithm), + optout_val, self.mbz, self.iterations)) + f.write('%02x %02x %04x\n' % + (self.hashalg, (self.mbz << 1) | optout_val, self.iterations)) + f.write("# Salt Len=%d, Salt='%s'\n" % (self.saltlen, self.salt)) + f.write('%02x%s%s\n' % (self.saltlen, + ' ' if len(self.salt) > 0 else '', + encode_string(self.salt))) + f.write("# Hash Len=%d, Hash='%s'\n" % (self.hashlen, self.hash)) + f.write('%02x%s%s\n' % (self.hashlen, + ' ' if len(self.hash) > 0 else '', + encode_string(self.hash))) class RRSIG: rdlen = -1 # auto-calculate @@ -415,7 +462,7 @@ def get_config_param(section): 'question' : (DNSQuestion, question_xtables), 'edns' : (EDNS, {}), 'soa' : (SOA, {}), 'txt' : (TXT, {}), 'rrsig' : (RRSIG, {}), 'nsec' : (NSEC, {}), - 'tsig' : (TSIG, {}) } + 'nsec3' : (NSEC3, {}), 'tsig' : (TSIG, {}) } s = section m = re.match('^([^:]+)/\d+$', section) if m: diff --git a/src/lib/dns/tests/testdata/rdata_mx_toWire2 b/src/lib/dns/tests/testdata/rdata_mx_toWire2 new file mode 100644 index 0000000000000000000000000000000000000000..ebd2f27cfc4d827c096355fd115fb6d2eaeb2585 --- /dev/null +++ b/src/lib/dns/tests/testdata/rdata_mx_toWire2 @@ -0,0 +1,12 @@ +# +# compressed MX RDATA stored in an output buffer +# +# sentinel name: example.com. +# 0 1 2 3 4 5 6 7 8 9 10 1 2 (bytes) +#(7) e x a m p l e (3) c o m . + 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 +# PREFERENCE: 10 + 00 0a +# EXCHANGE: not compressed +#(4) m x ptr=0 + 02 6d 78 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 diff --git a/src/lib/dns/tests/testdata/rdata_nsec3_fromWire1.spec b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire1.spec new file mode 100644 index 0000000000000000000000000000000000000000..39a78d75f1d6372b6d88305fec40af38b1d7901e --- /dev/null +++ b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire1.spec @@ -0,0 +1,7 @@ +# +# A malformed NSEC3 RDATA: bit map length is too large, causing overflow +# + +[custom] +sections: nsec3 +[nsec3] diff --git a/src/lib/dns/tests/testdata/rdata_nsec3_fromWire10.spec b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire10.spec new file mode 100644 index 0000000000000000000000000000000000000000..30417f5e15c77ce6b10bad4bd14f5cddec3ffa54 --- /dev/null +++ b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire10.spec @@ -0,0 +1,8 @@ +# +# An invalid NSEC3 RDATA: a bitmap block containing empty bytes +# + +[custom] +sections: nsec3 +[nsec3] +bitmap: '01000000' diff --git a/src/lib/dns/tests/testdata/rdata_nsec3_fromWire11.spec b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire11.spec new file mode 100644 index 0000000000000000000000000000000000000000..80ec59f0c41400bb441f0e96f3f869ac85a62f33 --- /dev/null +++ b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire11.spec @@ -0,0 +1,8 @@ +# +# An invalid NSEC3 RDATA: Saltlen is too large +# + +[custom] +sections: nsec3 +[nsec3] +rdlen: 7 diff --git a/src/lib/dns/tests/testdata/rdata_nsec3_fromWire12.spec b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire12.spec new file mode 100644 index 0000000000000000000000000000000000000000..1e01655d312363b2ec3b8cc0d95d08efe108bd44 --- /dev/null +++ b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire12.spec @@ -0,0 +1,9 @@ +# +# An invalid NSEC3 RDATA: Hash length is too large +# + +[custom] +sections: nsec3 +[nsec3] +# only contains the first byte of hash +rdlen: 12 diff --git a/src/lib/dns/tests/testdata/rdata_nsec3_fromWire13.spec b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire13.spec new file mode 100644 index 0000000000000000000000000000000000000000..fcc9d535ddcc2c921560c907a50b086654f10ba7 --- /dev/null +++ b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire13.spec @@ -0,0 +1,9 @@ +# +# A valid (but unusual) NSEC3 RDATA: salt is empty. +# + +[custom] +sections: nsec3 +[nsec3] +saltlen: 0 +salt: '' diff --git a/src/lib/dns/tests/testdata/rdata_nsec3_fromWire14.spec b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire14.spec new file mode 100644 index 0000000000000000000000000000000000000000..a0550d5e6f30e9973388bf334423569442f1d5f9 --- /dev/null +++ b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire14.spec @@ -0,0 +1,9 @@ +# +# An invalid NSEC3 RDATA: empty hash +# + +[custom] +sections: nsec3 +[nsec3] +hashlen: 0 +hash: '' diff --git a/src/lib/dns/tests/testdata/rdata_nsec3_fromWire15.spec b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire15.spec new file mode 100644 index 0000000000000000000000000000000000000000..4993e03f6746ab5ee8ef10cc6de8873409191611 --- /dev/null +++ b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire15.spec @@ -0,0 +1,10 @@ +# +# NSEC3 RDATA with empty type bitmap. It's okay. +# The test data includes bytes for a bitmap field, but RDLEN indicates +# it's not part of the RDATA and so it will be ignored. +# + +[custom] +sections: nsec3 +[nsec3] +rdlen: 31 diff --git a/src/lib/dns/tests/testdata/rdata_nsec3_fromWire2 b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire2 deleted file mode 100644 index 0965a27f2173c6a9c03079d3ddc931d99e418dc1..0000000000000000000000000000000000000000 --- a/src/lib/dns/tests/testdata/rdata_nsec3_fromWire2 +++ /dev/null @@ -1,12 +0,0 @@ -# -# NSEC3 RDATA with a bogus RDLEN (too short) -# - -# RDLENGTH, 29 bytes (should be 39) -00 1e - -# NSEC3 record: -# 1 1 1 D399EAAB H9RSFB7FPF2L8HG35CMPC765TDK23RP6 NS SOA RRSIG DNSKEY NSEC3PARAM -01 01 00 01 04 d3 99 ea ab 14 8a 77 c7 ac ef cb -c5 54 46 03 2b 2d 96 1c c5 eb 68 21 ef 26 00 07 -22 00 00 00 00 02 90 diff --git a/src/lib/dns/tests/testdata/rdata_nsec3_fromWire2.spec b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire2.spec new file mode 100644 index 0000000000000000000000000000000000000000..0b6a5afeac89812cfd18b13fbbc18d0f4a7013bb --- /dev/null +++ b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire2.spec @@ -0,0 +1,9 @@ +# +# A malformed NSEC3 RDATA: RDLEN indicates it doesn't even contain the fixed +# 5 octects +# + +[custom] +sections: nsec3 +[nsec3] +rdlen: 4 diff --git a/src/lib/dns/tests/testdata/rdata_nsec3_fromWire4.spec b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire4.spec new file mode 100644 index 0000000000000000000000000000000000000000..06d6eb4d2bd918043fa1f8fecf1e01d7a3f20c8f --- /dev/null +++ b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire4.spec @@ -0,0 +1,9 @@ +# +# A malformed NSEC3 RDATA: bit map length is too large, causing overflow +# + +[custom] +sections: nsec3 +[nsec3] +maplen: 31 +bitmap: '01' diff --git a/src/lib/dns/tests/testdata/rdata_nsec3_fromWire5.spec b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire5.spec new file mode 100644 index 0000000000000000000000000000000000000000..2d5713cee9341d8c24a99483e10107ef88bcee65 --- /dev/null +++ b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire5.spec @@ -0,0 +1,13 @@ +# +# A malformed NSEC3 RDATA: incomplete bit map field +# + +[custom] +sections: nsec3 +[nsec3] +# only containing the block field of the bitmap +rdlen: 32 +#dummy data +maplen: 31 +#dummy data +bitmap: '00' diff --git a/src/lib/dns/tests/testdata/rdata_nsec3_fromWire6.spec b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire6.spec new file mode 100644 index 0000000000000000000000000000000000000000..36e9e59b4cbff5dd728810a34965c52eb9e892f8 --- /dev/null +++ b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire6.spec @@ -0,0 +1,11 @@ +# +# A malformed NSEC3 RDATA: bit map length being 0 +# + +[custom] +sections: nsec3 +[nsec3] +rdlen: 33 +maplen: 0 +# dummy data: +bitmap: '01' diff --git a/src/lib/dns/tests/testdata/rdata_nsec3_fromWire7.spec b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire7.spec new file mode 100644 index 0000000000000000000000000000000000000000..338c0c98de1af4624ed1af9d6b576f29655f5d8d --- /dev/null +++ b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire7.spec @@ -0,0 +1,9 @@ +# +# NSEC3 RDATA with a longest bitmap field (32 bitmap bytes) +# + +[custom] +sections: nsec3 +[nsec3] +maplen: 32 +bitmap: '0101010101010101010101010101010101010101010101010101010101010101' diff --git a/src/lib/dns/tests/testdata/rdata_nsec3_fromWire8.spec b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire8.spec new file mode 100644 index 0000000000000000000000000000000000000000..041714ecb897f9fb41360b04fd52ffaeaccab3de --- /dev/null +++ b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire8.spec @@ -0,0 +1,9 @@ +# +# An invalid NSEC3 RDATA with an oversized bitmap field (33 bitmap bytes) +# + +[custom] +sections: nsec3 +[nsec3] +maplen: 33 +bitmap: '010101010101010101010101010101010101010101010101010101010101010101' diff --git a/src/lib/dns/tests/testdata/rdata_nsec3_fromWire9.spec b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire9.spec new file mode 100644 index 0000000000000000000000000000000000000000..b04c84f7fc15e13eeb2396bf154daa64b728674c --- /dev/null +++ b/src/lib/dns/tests/testdata/rdata_nsec3_fromWire9.spec @@ -0,0 +1,10 @@ +# +# An invalid NSEC3 RDATA: disordered bitmap blocks +# + +[custom] +sections: nsec3 +[nsec3] +nbitmap: 2 +block0: 2 +block1: 1 diff --git a/src/lib/log/Makefile.am b/src/lib/log/Makefile.am index 4afb7bedb0ead8f7ab8e7fe73b61aab0172a0b6c..416fd06d0ecb060558d7feb83c00f272d7b22dc4 100644 --- a/src/lib/log/Makefile.am +++ b/src/lib/log/Makefile.am @@ -1,4 +1,37 @@ -AM_CXXFLAGS = $(B10_CXXFLAGS) +SUBDIRS = . compiler tests + +AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib +AM_CPPFLAGS += $(BOOST_INCLUDES) +# AM_CPPFLAGS += $(LOG4CXX_INCLUDES) +AM_CPPFLAGS += -I$(top_srcdir)/src/lib/log -I$(top_builddir)/src/lib/log + +CLEANFILES = *.gcno *.gcda lib_LTLIBRARIES = liblog.la -liblog_la_SOURCES = dummylog.cc dummylog.h +liblog_la_SOURCES = +liblog_la_SOURCES += debug_levels.h logger_levels.h +liblog_la_SOURCES += dummylog.h dummylog.cc +liblog_la_SOURCES += filename.h filename.cc +liblog_la_SOURCES += logger.cc logger.h +liblog_la_SOURCES += logger_impl.cc logger_impl.h +liblog_la_SOURCES += logger_support.cc logger_support.h +liblog_la_SOURCES += messagedef.cc messagedef.h +liblog_la_SOURCES += message_dictionary.cc message_dictionary.h +liblog_la_SOURCES += message_exception.h message_exception.cc +liblog_la_SOURCES += message_initializer.cc message_initializer.h +liblog_la_SOURCES += message_reader.cc message_reader.h +liblog_la_SOURCES += message_types.h +liblog_la_SOURCES += root_logger_name.cc root_logger_name.h +liblog_la_SOURCES += strutil.h strutil.cc + +# Note: the ordering matters: -Wno-... must follow -Wextra (defined in +# B10_CXXFLAGS) +liblog_la_CXXFLAGS = $(AM_CXXFLAGS) +if USE_GXX +liblog_la_CXXFLAGS += -Wno-unused-parameter +endif +if USE_CLANGPP +# Same for clang++, but we need to turn off -Werror completely. +liblog_la_CXXFLAGS += -Wno-error +endif +liblog_la_CPPFLAGS = $(AM_CPPFLAGS) diff --git a/src/lib/log/compiler/Makefile.am b/src/lib/log/compiler/Makefile.am new file mode 100644 index 0000000000000000000000000000000000000000..2475036e7593ef944229a17e2a11fc59fa5e66d7 --- /dev/null +++ b/src/lib/log/compiler/Makefile.am @@ -0,0 +1,20 @@ +SUBDIRS = . + +AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib +AM_CPPFLAGS += -I$(top_srcdir)/src/lib/log -I$(top_builddir)/src/lib/log +AM_CPPFLAGS += $(BOOST_INCLUDES) + +AM_CXXFLAGS = $(B10_CXXFLAGS) + +if USE_STATIC_LINK +AM_LDFLAGS = -static +endif + +pkglibexecdir = $(libexecdir)/@PACKAGE@ + +CLEANFILES = *.gcno *.gcda + +pkglibexec_PROGRAMS = message +message_SOURCES = message.cc +message_LDADD = $(top_builddir)/src/lib/log/liblog.la + diff --git a/src/lib/log/compiler/message.cc b/src/lib/log/compiler/message.cc new file mode 100644 index 0000000000000000000000000000000000000000..6f9c4e0e2de0485e1b30fa23893f9594f06c9e64 --- /dev/null +++ b/src/lib/log/compiler/message.cc @@ -0,0 +1,546 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +using namespace std; +using namespace isc::log; + +static const char* VERSION = "1.0-0"; + +/// \brief Message Compiler +/// +/// \b Overview
+/// This is the program that takes as input a message file and produces: +/// +/// \li A .h file containing message definition +/// \li A .cc file containing code that adds the messages to the program's +/// message dictionary at start-up time. +/// +/// Alternatively, the program can produce a .py file that contains the +/// message definitions. +/// + +/// \b Invocation
+/// The program is invoked with the command: +/// +/// message [-v | -h | \] +/// +/// It reads the message file and writes out two files of the same name but with +/// extensions of .h and .cc. +/// +/// \-v causes it to print the version number and exit. \-h prints a help +/// message (and exits). + + +/// \brief Print Version +/// +/// Prints the program's version number. + +void +version() { + cout << VERSION << "\n"; +} + +/// \brief Print Usage +/// +/// Prints program usage to stdout. + +void +usage() { + cout << + "Usage: message [-h] [-v] \n" << + "\n" << + "-h Print this message and exit\n" << + "-v Print the program version and exit\n" << + "\n" << + " is the name of the input message file.\n"; +} + + +/// \brief Create Time +/// +/// Returns the current time as a suitably-formatted string. +/// +/// \return Current time + +string +currentTime() { + + // Get a text representation of the current time. + time_t curtime; + time(&curtime); + char* buffer = ctime(&curtime); + + // Convert to string and strip out the trailing newline + string current_time = buffer; + return isc::strutil::trim(current_time); +} + + +/// \brief Create Header Sentinel +/// +/// Given the name of a file, create an #ifdef sentinel name. The name is +/// ___, where is the name of the file, and is the +/// extension less the leading period. The sentinel will be upper-case. +/// +/// \param file Filename object representing the file. +/// +/// \return Sentinel name + +string +sentinel(Filename& file) { + + string name = file.name(); + string ext = file.extension(); + string sentinel_text = "__" + name + "_" + ext.substr(1); + isc::strutil::uppercase(sentinel_text); + return sentinel_text; +} + + +/// \brief Quote String +/// +/// Inserts an escape character (a backslash) prior to any double quote +/// characters. This is used to handle the fact that the input file does not +/// contain quotes, yet the string will be included in a C++ literal string. + +string +quoteString(const string& instring) { + + // Create the output string and reserve the space needed to hold the input + // string. (Most input strings will not contain quotes, so this single + // reservation should be all that is needed.) + string outstring; + outstring.reserve(instring.size()); + + // Iterate through the input string, preceding quotes with a slash. + for (size_t i = 0; i < instring.size(); ++i) { + if (instring[i] == '"') { + outstring += '\\'; + } + outstring += instring[i]; + } + + return outstring; +} + + +/// \brief Sorted Identifiers +/// +/// Given a dictionary, return a vector holding the message IDs in sorted +/// order. +/// +/// \param dictionary Dictionary to examine +/// +/// \return Sorted list of message IDs + +vector +sortedIdentifiers(MessageDictionary& dictionary) { + vector ident; + + for (MessageDictionary::const_iterator i = dictionary.begin(); + i != dictionary.end(); ++i) { + ident.push_back(i->first); + } + sort(ident.begin(), ident.end()); + + return ident; +} + + +/// \brief Split Namespace +/// +/// The $NAMESPACE directive may well specify a namespace in the form a::b. +/// Unfortunately, the C++ "namespace" statement can only accept a single +/// string - to set up the namespace of "a::b" requires two statements, one +/// for "namspace a" and the other for "namespace b". +/// +/// This function returns the set of namespace components as a vector of +/// strings. A vector of one element, containing the empty string, is returned +/// if the anonymous namespace is specified. +/// +/// \param ns Argument to $NAMESPACE (passed by value, as we will be modifying +/// it.) + +vector +splitNamespace(string ns) { + + // Namespaces components are separated by double colon characters - + // convert to single colons. + size_t dcolon; + while ((dcolon = ns.find("::")) != string::npos) { + ns.replace(dcolon, 2, ":"); + } + + // ... and return the vector of namespace components split on the single + // colon. + return isc::strutil::tokens(ns, ":"); +} + + +/// \brief Write Opening Namespace(s) +/// +/// Writes the lines listing the namespaces in use. +void +writeOpeningNamespace(ostream& output, const vector& ns) { + if (!ns.empty()) { + + // Output namespaces in correct order + for (int i = 0; i < ns.size(); ++i) { + output << "namespace " << ns[i] << " {\n"; + } + output << "\n"; + } +} + + +/// \brief Write Closing Namespace(s) +/// +/// Writes the lines listing the namespaces in use. +void +writeClosingNamespace(ostream& output, const vector& ns) { + if (!ns.empty()) { + for (int i = ns.size() - 1; i >= 0; --i) { + output << "} // namespace " << ns[i] << "\n"; + } + output << "\n"; + } +} + + +/// \brief Write Header File +/// +/// Writes the C++ header file containing the symbol definitions. These are +/// "extern" references to definitions in the .cc file. As such, they should +/// take up no space in the module in which they are included, and redundant +/// references should be removed by the compiler. +/// +/// \param file Name of the message file. The header file is written to a +/// file of the same name but with a .h suffix. +/// \param prefix Prefix string to use in symbols +/// \param ns Namespace in which the definitions are to be placed. An empty +/// string indicates no namespace. +/// \param dictionary Dictionary holding the message definitions. + +void +writeHeaderFile(const string& file, const string& prefix, + const vector& ns_components, MessageDictionary& dictionary) +{ + Filename message_file(file); + Filename header_file(message_file.useAsDefault(".h")); + + // Text to use as the sentinels. + string sentinel_text = sentinel(header_file); + + // Open the output file for writing + ofstream hfile(header_file.fullName().c_str()); + + try { + if (hfile.fail()) { + throw MessageException(MSG_OPNMSGOUT, header_file.fullName(), + strerror(errno)); + } + + // Write the header preamble. If there is an error, we'll pick it up + // after the last write. + + hfile << + "// File created from " << message_file.fullName() << " on " << + currentTime() << "\n" << + "\n" << + "#ifndef " << sentinel_text << "\n" << + "#define " << sentinel_text << "\n" << + "\n" << + "#include \n" << + "\n"; + + // Write the message identifiers, bounded by a namespace declaration + writeOpeningNamespace(hfile, ns_components); + + vector idents = sortedIdentifiers(dictionary); + for (vector::const_iterator j = idents.begin(); + j != idents.end(); ++j) { + hfile << "extern const isc::log::MessageID " << prefix << *j << ";\n"; + } + hfile << "\n"; + + writeClosingNamespace(hfile, ns_components); + + // ... and finally the postamble + hfile << "#endif // " << sentinel_text << "\n"; + + // Report errors (if any) and exit + if (hfile.fail()) { + throw MessageException(MSG_MSGWRTERR, header_file.fullName(), + strerror(errno)); + } + + hfile.close(); + } + catch (MessageException&) { + hfile.close(); + throw; + } +} + + +/// \brief Convert Non Alpha-Numeric Characters to Underscores +/// +/// Simple function for use in a call to transform + +char +replaceNonAlphaNum(char c) { + return (isalnum(c) ? c : '_'); +} + + +/// \brief Write Program File +/// +/// Writes the C++ source code file. This defines the text of the message +/// symbols, as well as the initializer object that sets the entries in the +/// global dictionary. +/// +/// The construction of the initializer object loads the dictionary with the +/// message text. However, nothing actually references it. If the initializer +/// were in a file by itself, the lack of things referencing it would cause the +/// linker to ignore it when pulling modules out of the logging library in a +/// static link. By including it in the file with the symbol definitions, the +/// module will get included in the link process to resolve the symbol +/// definitions, and so the initializer object will be included in the final +/// image. (Note that there are no such problems when the logging library is +/// built as a dynamically-linked library: the whole library - including the +/// initializer module - gets mapped into address space when the library is +/// loaded, after which all the initializing code (including the constructors +/// of objects declared outside functions) gets run.) +/// +/// There _may_ be a problem when we come to port this to Windows. Microsoft +/// Visual Studio contains a "Whole Program Optimisation" option, where the +/// optimisation is done at link-time, not compiler-time. In this it _may_ +/// decide to remove the initializer object because of a lack of references +/// to it. But until BIND-10 is ported to Windows, we won't know. + +void +writeProgramFile(const string& file, const string& prefix, + const vector& ns_components, MessageDictionary& dictionary) +{ + Filename message_file(file); + Filename program_file(message_file.useAsDefault(".cc")); + + // Open the output file for writing + ofstream ccfile(program_file.fullName().c_str()); + try { + if (ccfile.fail()) { + throw MessageException(MSG_OPNMSGOUT, program_file.fullName(), + strerror(errno)); + } + + // Write the preamble. If there is an error, we'll pick it up after + // the last write. + + ccfile << + "// File created from " << message_file.fullName() << " on " << + currentTime() << "\n" << + "\n" << + "#include \n" << + "#include \n" << + "#include \n" << + "\n"; + + // Declare the message symbols themselves. + + writeOpeningNamespace(ccfile, ns_components); + + vector idents = sortedIdentifiers(dictionary); + for (vector::const_iterator j = idents.begin(); + j != idents.end(); ++j) { + ccfile << "extern const isc::log::MessageID " << prefix << *j << + " = \"" << *j << "\";\n"; + } + ccfile << "\n"; + + writeClosingNamespace(ccfile, ns_components); + + // Now the code for the message initialization. + + ccfile << + "namespace {\n" << + "\n" << + "const char* values[] = {\n"; + + // Output the identifiers and the associated text. + idents = sortedIdentifiers(dictionary); + for (vector::const_iterator i = idents.begin(); + i != idents.end(); ++i) { + ccfile << " \"" << *i << "\", \"" << + quoteString(dictionary.getText(*i)) << "\",\n"; + } + + + // ... and the postamble + ccfile << + " NULL\n" << + "};\n" << + "\n" << + "const isc::log::MessageInitializer initializer(values);\n" << + "\n" << + "} // Anonymous namespace\n" << + "\n"; + + // Report errors (if any) and exit + if (ccfile.fail()) { + throw MessageException(MSG_MSGWRTERR, program_file.fullName(), + strerror(errno)); + } + + ccfile.close(); + } + catch (MessageException&) { + ccfile.close(); + throw; + } +} + + +/// \brief Warn of Duplicate Entries +/// +/// If the input file contained duplicate message IDs, only the first will be +/// processed. However, we should warn about it. +/// +/// \param reader Message Reader used to read the file + +void +warnDuplicates(MessageReader& reader) { + + // Get the duplicates (the overflow) and, if present, sort them into some + // order and remove those which occur more than once (which mean that they + // occur more than twice in the input file). + MessageReader::MessageIDCollection duplicates = reader.getNotAdded(); + if (duplicates.size() > 0) { + cout << "Warning: the following duplicate IDs were found:\n"; + + sort(duplicates.begin(), duplicates.end()); + MessageReader::MessageIDCollection::iterator new_end = + unique(duplicates.begin(), duplicates.end()); + for (MessageReader::MessageIDCollection::iterator i = duplicates.begin(); + i != new_end; ++i) { + cout << " " << *i << "\n"; + } + } +} + + +/// \brief Main Program +/// +/// Parses the options then dispatches to the appropriate function. See the +/// main file header for the invocation. + +int +main(int argc, char* argv[]) { + + const char* soptions = "hv"; // Short options + + optind = 1; // Ensure we start a new scan + int opt; // Value of the option + + while ((opt = getopt(argc, argv, soptions)) != -1) { + switch (opt) { + case 'h': + usage(); + return 0; + + case 'v': + version(); + return 0; + + default: + // A message will have already been output about the error. + return 1; + } + } + + // Do we have the message file? + if (optind < (argc - 1)) { + cout << "Error: excess arguments in command line\n"; + usage(); + return 1; + } else if (optind >= argc) { + cout << "Error: missing message file\n"; + usage(); + return 1; + } + string message_file = argv[optind]; + + try { + // Have identified the file, so process it. First create a local + // dictionary into which the data will be put. + MessageDictionary dictionary; + + // Read the data into it. + MessageReader reader(&dictionary); + reader.readFile(message_file); + + // Get the namespace into which the message definitions will be put and + // split it into components. + vector ns_components = splitNamespace(reader.getNamespace()); + + // Write the header file. + writeHeaderFile(message_file, reader.getPrefix(), ns_components, + dictionary); + + // Write the file that defines the message symbols and text + writeProgramFile(message_file, reader.getPrefix(), ns_components, + dictionary); + + + // Finally, warn of any duplicates encountered. + warnDuplicates(reader); + } + catch (MessageException& e) { + // Create an error message from the ID and the text + MessageDictionary& global = MessageDictionary::globalDictionary(); + string text = e.id(); + text += ", "; + text += global.getText(e.id()); + + // Format with arguments + text = isc::strutil::format(text, e.arguments()); + cerr << text << "\n"; + + return 1; + } + + return 0; + +} diff --git a/src/lib/log/debug_levels.h b/src/lib/log/debug_levels.h new file mode 100644 index 0000000000000000000000000000000000000000..bb2b52447e29a026733dafd225ad6944e5c1c736 --- /dev/null +++ b/src/lib/log/debug_levels.h @@ -0,0 +1,29 @@ +// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef __DEBUG_LEVELS_H +#define __DEBUG_LEVELS_H + +/// \brief Defines Debug Levels +/// +/// Defines the maximum and minimum debug levels and the number of levels. +/// These are defined using #define as they are referenced in the construction +/// of variables declared outside execution units. (In this way we avoid the +/// "static initialization fiasco" problem.) + +#define MIN_DEBUG_LEVEL (0) +#define MAX_DEBUG_LEVEL (99) +#define NUM_DEBUG_LEVEL (MAX_DEBUG_LEVEL - MIN_DEBUG_LEVEL + 1) + +#endif // __DEBUG_LEVELS_H diff --git a/src/lib/log/documentation.txt b/src/lib/log/documentation.txt new file mode 100644 index 0000000000000000000000000000000000000000..0501587af73b5068f8de4f5189c627140f58bcee --- /dev/null +++ b/src/lib/log/documentation.txt @@ -0,0 +1,434 @@ +This directory holds the first release of the logging system. + +Basic Ideas +=========== +The BIND-10 logging system merges two ideas: + +* A hierarchical logging system similar to that used in Java (i.e. log4j) +* Separation of message definitions and text + + +Hierarchical Logging System +=========================== +When a program writes a message to the logging system, it does so using an +instance of the Logger class. As well as performing the write of the message, +the logger identifies the source of the message: different sources can write +to different destinations and can log different severities of messages. +For example, the "cache" logger could write messages of DEBUG severity or +above to a file while all other components write messages of "INFO" severity +or above to the Syslog file. + +The loggers are hierarchical in that each logger is the child of another +logger. The top of the hierarchy is the root logger, which does not have +a parent. The point of the hierarchy is that unless a logger is explicitly +assigned an attribute (such as severity of message being logger), it picks +it up from the parent. (In BIND-10, there is the root logger (named after +the program) and every other logger is a child of that.) So in the example +above, the INFO/Syslog attributes could be associated with the root logger +while the DEBUG/file attributes are associated with the "cache" logger. + + +Separation of Messages Definitions And Text +=========================================== +The reason for this is to allow the message text to be overridden by versions +in a local language. To do this, each message is identified by an identifier +e.g. "OPENIN". Within the program, this is the symbol passed to the logging +system. The logger system uses the symbol as an index into a dictionary to +retrieve the message associated with it (e.g. "unable to open %s for input"). +substitutes any message parameters (in this example, the string that is an +invalid filename) and logs it to the destination. + +In the BIND-10 system, a set of default messages are linked into the +program. At run-time. each program reads a message file, updating the +stored definitions; this updated text is logged. However, to aid support, +the message identifier so in the example above, the message finally logged +would be something like: + + OPENIN, unable to open a.txt for input + + +Using The System +================ +The steps in using the system are: + +1. Create a message file. This defines messages by an identification - a + mnemonic for the message, typically 6-12 characters long - and a message. + The file is described in more detail below. + + Ideally the file should have a file type of ".msg". + +2. Run it through the message compiler to produce the .h and .cc files. It + is intended that this step be included in the build process. However, + for not run the compiler (found in the "compiler" subdirectory) manually. + The only argument is the name of the message file: it will produce as + output two files, having the same name as the input file but with file + types of ".h" and ".cc". + + The compiler is built in the "compiler" subdirectory of the "src/lib/log" + directory. + +3. Include the .h file in your source code to define message symbols, and + make sure that the .cc file is compiled and linked into your program - + static initialization will add the symbols to the global dictionary. + +4. Declare loggers in your code and use them to log messages. This is + described in more detail below. + +5. To set the debug level and run-time message file, call runTimeInit (declared + in logger_support.h) in the main program unit. This is a temporary solution + for Year 2, and will be replaced at a later date, the information coming + from the configuration database. + + +Message Files +============= + +File Contents and Format +------------------------ +A message file is a file containing message definitions. Typically there +will be one message file for each component that declares message symbols. +An example file could be: + +-- BEGIN -- + +# Example message file +# $ID:$ + +$PREFIX TEST_ +$NAMESPACE isc::log +TEST1 message %s is much too large ++ This message is a test for the general message code + +UNKNOWN unknown message ++ Issued when the message is unknown. + +-- END -- + +Points to note: +* Leading and trailing space are trimmed from the line. Although the above + exampl,e has every line starting at column 1, the lines could be indented + if desired. + +* Blank lines are ignored. + +* Lines starting with "#" are comments are are ignored. Comments must be on + a line by themselves - inline comments will be interpreted as part of the + text of the line. + +* Lines starting $ are directives. At present, two directives are recognised: + + * $PREFIX, which has one argument: the string used to prefix symbols. If + absent, there is no prefix to the symbols. (Prefixes are explained below.) + * $NAMESPACE, which has one argument: the namespace in which the symbols are + created. (Specifying the argument as a double colon - i.e. "$NAMESPACE + ::" puts the symbol definitions in the unnamed namespace. And not + including a $NAMESPACE directive will result in the symbols note being + put in any namespace. + +* Lines starting + indicate an explanation for the preceding message. These + are intended to be processed by a separate program and used to generate + an error messages manual. However they are treated like comments by the + message compiler. As with comments, these must be on a line by themselves; + if inline, the text (including the leading "+") will be interpreted as + part of the line. + +* Message lines. These comprise a symbol name and a message, which may + include zero or more printf-style tokens. Symbol names will be upper-cased + by the compiler. + + +Message Compiler +---------------- +The message compiler is a program built in the src/log/compiler directory. +It is invoked by the command: + + message [-h] [-v] + +("-v" prints the version number and exits; "-h" prints brief help text.) +The message compiler processes the message file to produce two files: + +1) A C++ header file (called .h) that holds lines of +the form: + + namespace { + isc::log::MessageID PREFIX_IDENTIFIER = "IDENTIFIER"; + : + } + +The symbols define the keys in the global message dictionary. At present +they are defined as std::strings, but a future implementation could redefine +them as numeric values. + +The namespace enclosing the symbols is set by the $NAMESPACE directive. + +The "PREFIX_" part of the symbol name is the string defined in the $PREFIX +the argument to the directive. So "$PREFIX MSG_" would prefix the identifer +ABC with "MSG_" to give the symbol MSG_ABC. Similarly "$PREFIX E" would +prefix it with "E" to give the symbol EABC. If no $PREFIX is given, no +prefix appears (so the symbol in this example would be ABC). + +The header file also includes a couple of lines to ensure that the message +text is included in the final program image. + + +2) A C++ source file (called .cc) that holds the code to +insert the symbols and messages into the map. + +This file declares an array of identifiers/messages in the form: + + namespace { + const char* values[] = { + identifier1, text1, + identifier2, text2, + : + NULL + }; + } + +(A more complex structure to group identifiers and their messages could be +imposed, but as the array is generated by code and will be read by code, +it is not needed.) + +It then declares an object that will add information to the global dictionary: + + MessageInitializer _