Commit fa62fc57 authored by Mukund Sivaraman's avatar Mukund Sivaraman
Browse files

Merge branch 'master' into trac2185

parents 569e7f07 91867e08
This diff is collapsed.
......@@ -76,7 +76,7 @@ AM_CONDITIONAL(USE_CLANGPP, test "X${CLANGPP}" = "Xyes")
dnl Determine if weare using GNU sed
GNU_SED=no
$SED --version 2> /dev/null | grep -q GNU
$SED --version 2> /dev/null | grep GNU > /dev/null 2>&1
if test $? -eq 0; then
GNU_SED=yes
fi
......@@ -410,6 +410,24 @@ fi
AC_SUBST(PYTHON_LIB)
LDFLAGS=$LDFLAGS_SAVED
# Python 3.2 changed the return type of internal hash function to
# Py_hash_t and some platforms (such as Solaris) strictly check for long
# vs Py_hash_t. So we detect and use the appropriate return type.
# Remove this test (and associated changes in pydnspp_config.h.in) when
# we require Python 3.2.
have_py_hash_t=0
CPPFLAGS_SAVED="$CPPFLAGS"
CPPFLAGS=${PYTHON_INCLUDES}
AC_MSG_CHECKING(for Py_hash_t)
AC_TRY_COMPILE([#include <Python.h>
Py_hash_t h;],,
[AC_MSG_RESULT(yes)
have_py_hash_t=1],
[AC_MSG_RESULT(no)])
CPPFLAGS="$CPPFLAGS_SAVED"
HAVE_PY_HASH_T=$have_py_hash_t
AC_SUBST(HAVE_PY_HASH_T)
# Check for the setproctitle module
if test "$setproctitle_check" = "yes" ; then
AC_MSG_CHECKING(for setproctitle module)
......@@ -427,7 +445,7 @@ fi
# Python 3.2 has an unused parameter in one of its headers. This
# has been reported, but not fixed as of yet, so we check if we need
# to set -Wno-unused-parameter.
if test "X$GXX" = "Xyes" -a $werror_ok = 1; then
if test "X$GXX" = "Xyes" -a "$werror_ok" = 1; then
CPPFLAGS_SAVED="$CPPFLAGS"
CPPFLAGS=${PYTHON_INCLUDES}
CXXFLAGS_SAVED="$CXXFLAGS"
......@@ -1371,6 +1389,7 @@ AC_CONFIG_FILES([compatcheck/Makefile
src/bin/dhcp4/spec_config.h.pre
src/bin/dhcp4/tests/Makefile
src/bin/dhcp4/tests/marker_file.h
src/bin/dhcp4/tests/test_data_files_config.h
src/bin/dhcp4/tests/test_libraries.h
src/bin/dhcp6/Makefile
src/bin/dhcp6/spec_config.h.pre
......@@ -1478,6 +1497,7 @@ AC_CONFIG_FILES([compatcheck/Makefile
src/lib/dns/gen-rdatacode.py
src/lib/dns/Makefile
src/lib/dns/python/Makefile
src/lib/dns/python/pydnspp_config.h
src/lib/dns/python/tests/Makefile
src/lib/dns/tests/Makefile
src/lib/dns/tests/testdata/Makefile
......
......@@ -109,7 +109,9 @@ make distcheck
There are other useful switches which can be passed to configure. It is
always a good idea to use \c --enable-logger-checks, which does sanity
checks on logger parameters. If you happen to modify anything in the
checks on logger parameters. Use \c --enable-debug to enable various
additional consistency checks that reduce performance but help during
development. If you happen to modify anything in the
documentation, use \c --enable-generate-docs. If you are modifying DHCP
code, you are likely to be interested in enabling the MySQL backend for
DHCP. Note that if the backend is not enabled, MySQL specific unit-tests
......
......@@ -60,6 +60,7 @@
* - @subpage dhcpv4ConfigInherit
* - @subpage dhcpv4OptionsParse
* - @subpage dhcpv4DDNSIntegration
* - @subpage dhcpv4Classifier
* - @subpage dhcpv4Other
* - @subpage dhcp6
* - @subpage dhcpv6Session
......@@ -67,6 +68,7 @@
* - @subpage dhcpv6ConfigInherit
* - @subpage dhcpv6DDNSIntegration
* - @subpage dhcpv6OptionsParse
* - @subpage dhcpv6Classifier
* - @subpage dhcpv6Other
* - @subpage libdhcp
* - @subpage libdhcpIntro
......@@ -74,6 +76,7 @@
* - @subpage libdhcpIfaceMgr
* - @subpage libdhcpPktFilter
* - @subpage libdhcpPktFilter6
* - @subpage libdhcpErrorLogging
* - @subpage libdhcpsrv
* - @subpage leasemgr
* - @subpage cfgmgr
......@@ -83,10 +86,10 @@
* - @subpage libdhcp_ddns
*
* @section miscellaneousTopics Miscellaneous Topics
* - @subpage LoggingApi
* - @subpage LoggingApiOverview
* - @subpage LoggingApiLoggerNames
* - @subpage LoggingApiLoggingMessages
* - @subpage logBind10Logging
* - @subpage logBasicIdeas
* - @subpage logDeveloperUse
* - @subpage logNotes
* - @subpage SocketSessionUtility
* - <a href="./doxygen-error.log">Documentation warnings and errors</a>
*
......
This diff is collapsed.
......@@ -11,3 +11,5 @@
/gen-statisticsitems.py.pre
/statistics.cc
/statistics_items.h
/s-genstats
/s-messages
......@@ -383,11 +383,11 @@ This message is also logged when the forwarding is restarted (for instance
if b10-ddns is restarted and the internal connection needs to be created
again), in which case it should be followed by AUTH_START_DDNS_FORWARDER.
% AUTH_UNSUPPORTED_OPCODE unsupported opcode: %1
% AUTH_UNSUPPORTED_OPCODE unsupported opcode %1 received from %2
This is a debug message, produced when a received DNS packet being
processed by the authoritative server has been found to contain an
unsupported opcode. (The opcode is included in the message.) The server
will return an error code of NOTIMPL to the sender.
unsupported opcode. (The opcode and sender details are included in the
message.) The server will return an error code of NOTIMPL to the sender.
% AUTH_XFRIN_CHANNEL_CREATED XFRIN session channel created
This is a debug message indicating that the authoritative server has
......
......@@ -297,6 +297,8 @@ public:
///
/// \param server The DNSServer as passed to processMessage()
/// \param message The response as constructed by processMessage()
/// \param stats_attrs Object to store message attributes in for use
/// with statistics
/// \param done If true, it indicates there is a response.
/// this value will be passed to server->resume(bool)
void resumeServer(isc::asiodns::DNSServer* server,
......@@ -440,12 +442,9 @@ makeErrorMessage(MessageRenderer& renderer, Message& message,
message.setRcode(rcode);
RendererHolder holder(renderer, &buffer, stats_attrs);
if (tsig_context.get() != NULL) {
message.toWire(renderer, *tsig_context);
stats_attrs.setResponseTSIG(true);
} else {
message.toWire(renderer);
}
message.toWire(renderer, tsig_context.get());
stats_attrs.setResponseTSIG(tsig_context.get() != NULL);
LOG_DEBUG(auth_logger, DBG_AUTH_MESSAGES, AUTH_SEND_ERROR_RESPONSE)
.arg(renderer.getLength()).arg(message);
}
......@@ -499,7 +498,7 @@ AuthSrv::processMessage(const IOMessage& io_message, Message& message,
impl_->resumeServer(server, message, stats_attrs, false);
return;
}
} catch (const Exception& ex) {
} catch (const isc::Exception& ex) {
LOG_DEBUG(auth_logger, DBG_AUTH_DETAIL, AUTH_HEADER_PARSE_FAIL)
.arg(ex.what());
impl_->resumeServer(server, message, stats_attrs, false);
......@@ -523,7 +522,7 @@ AuthSrv::processMessage(const IOMessage& io_message, Message& message,
stats_attrs);
impl_->resumeServer(server, message, stats_attrs, true);
return;
} catch (const Exception& ex) {
} catch (const isc::Exception& ex) {
LOG_DEBUG(auth_logger, DBG_AUTH_DETAIL, AUTH_PACKET_PARSE_FAILED)
.arg(ex.what());
makeErrorMessage(impl_->renderer_, message, buffer, Rcode::SERVFAIL(),
......@@ -582,8 +581,9 @@ AuthSrv::processMessage(const IOMessage& io_message, Message& message,
Rcode::NOTIMP(), stats_attrs, tsig_context);
}
} else if (opcode != Opcode::QUERY()) {
const IOEndpoint& remote_ep = io_message.getRemoteEndpoint();
LOG_DEBUG(auth_logger, DBG_AUTH_DETAIL, AUTH_UNSUPPORTED_OPCODE)
.arg(message.getOpcode().toText());
.arg(message.getOpcode().toText()).arg(remote_ep);
makeErrorMessage(impl_->renderer_, message, buffer,
Rcode::NOTIMP(), stats_attrs, tsig_context);
} else if (message.getRRCount(Message::SECTION_QUESTION) != 1) {
......@@ -660,7 +660,7 @@ AuthSrvImpl::processNormalQuery(const IOMessage& io_message,
stats_attrs);
return (true);
}
} catch (const Exception& ex) {
} catch (const isc::Exception& ex) {
LOG_ERROR(auth_logger, AUTH_PROCESS_FAIL).arg(ex.what());
makeErrorMessage(renderer_, message, buffer, Rcode::SERVFAIL(),
stats_attrs);
......@@ -671,12 +671,9 @@ AuthSrvImpl::processNormalQuery(const IOMessage& io_message,
const bool udp_buffer =
(io_message.getSocket().getProtocol() == IPPROTO_UDP);
renderer_.setLengthLimit(udp_buffer ? remote_bufsize : 65535);
if (tsig_context.get() != NULL) {
message.toWire(renderer_, *tsig_context);
stats_attrs.setResponseTSIG(true);
} else {
message.toWire(renderer_);
}
message.toWire(renderer_, tsig_context.get());
stats_attrs.setResponseTSIG(tsig_context.get() != NULL);
LOG_DEBUG(auth_logger, DBG_AUTH_MESSAGES, AUTH_SEND_NORMAL_RESPONSE)
.arg(renderer_.getLength()).arg(message);
return (true);
......@@ -823,7 +820,7 @@ AuthSrvImpl::processNotify(const IOMessage& io_message, Message& message,
.arg(parsed_answer->str());
return (false);
}
} catch (const Exception& ex) {
} catch (const isc::Exception& ex) {
LOG_ERROR(auth_logger, AUTH_ZONEMGR_COMMS).arg(ex.what());
return (false);
}
......@@ -833,12 +830,8 @@ AuthSrvImpl::processNotify(const IOMessage& io_message, Message& message,
message.setRcode(Rcode::NOERROR());
RendererHolder holder(renderer_, &buffer, stats_attrs);
if (tsig_context.get() != NULL) {
message.toWire(renderer_, *tsig_context);
stats_attrs.setResponseTSIG(true);
} else {
message.toWire(renderer_);
}
message.toWire(renderer_, tsig_context.get());
stats_attrs.setResponseTSIG(tsig_context.get() != NULL);
return (true);
}
......
......@@ -104,6 +104,8 @@ public:
/// process. It's normally a reference to an xfr::XfroutClient object,
/// but can refer to a local mock object for testing (or other
/// experimental) purposes.
/// \param ddns_forwarder Forwarder to which DDNS UPDATE requests
/// are passed to
AuthSrv(isc::xfr::AbstractXfroutClient& xfrout_client,
isc::util::io::BaseSocketSessionForwarder& ddns_forwarder);
~AuthSrv();
......
......@@ -37,20 +37,6 @@ using namespace isc::dns;
using namespace isc::datasrc;
using namespace isc::dns::rdata;
// This is a "constant" vector storing desired RR types for the additional
// section. The vector is filled first time it's used.
namespace {
const vector<RRType>&
A_AND_AAAA() {
static vector<RRType> needed_types;
if (needed_types.empty()) {
needed_types.push_back(RRType::A());
needed_types.push_back(RRType::AAAA());
}
return (needed_types);
}
}
namespace isc {
namespace auth {
......@@ -393,6 +379,17 @@ Query::process(datasrc::ClientList& client_list,
response_->setRcode(Rcode::SERVFAIL());
return;
}
if (qtype == RRType::RRSIG()) {
// We will not serve RRSIGs directly. See #2226 and the
// following thread for discussion why:
// http://www.ietf.org/mail-archive/web/dnsext/current/msg07123.html
// RRSIGs go together with their covered RRset.
response_->setHeaderFlag(Message::HEADERFLAG_AA);
response_->setRcode(Rcode::REFUSED());
return;
}
ZoneFinder& zfinder = *result.finder_;
// We have authority for a zone that contain the query name (possibly
......
......@@ -286,6 +286,9 @@ public:
answers_.reserve(RESERVE_RRSETS);
authorities_.reserve(RESERVE_RRSETS);
additionals_.reserve(RESERVE_RRSETS);
a_and_aaaa_.push_back(isc::dns::RRType::A());
a_and_aaaa_.push_back(isc::dns::RRType::AAAA());
}
......@@ -488,6 +491,15 @@ private:
std::vector<isc::dns::ConstRRsetPtr> answers_;
std::vector<isc::dns::ConstRRsetPtr> authorities_;
std::vector<isc::dns::ConstRRsetPtr> additionals_;
private:
/// \brief Returns a reference to a pre-initialized vector (see the
/// \c Query constructor).
const std::vector<isc::dns::RRType>& A_AND_AAAA() const {
return (a_and_aaaa_);
}
std::vector<isc::dns::RRType> a_and_aaaa_;
};
}
......
......@@ -44,6 +44,7 @@
#include <util/unittests/mock_socketsession.h>
#include <dns/tests/unittest_util.h>
#include <util/unittests/wiredata.h>
#include <testutils/dnsmessage_test.h>
#include <testutils/srv_test.h>
#include <testutils/mockups.h>
......@@ -82,8 +83,9 @@ using namespace isc::testutils;
using namespace isc::server_common::portconfig;
using namespace isc::auth::unittest;
using isc::UnitTestUtil;
using boost::scoped_ptr;
using isc::auth::statistics::Counters;
using isc::util::unittests::matchWireData;
using boost::scoped_ptr;
namespace {
const char* const CONFIG_TESTDB =
......@@ -1072,10 +1074,9 @@ TEST_F(AuthSrvTest, builtInQueryViaDNSServer) {
response_message, response_obuffer);
createBuiltinVersionResponse(default_qid, response_data);
EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData,
response_obuffer->getData(),
response_obuffer->getLength(),
&response_data[0], response_data.size());
matchWireData(&response_data[0], response_data.size(),
response_obuffer->getData(),
response_obuffer->getLength());
// After it has been run, the message should be cleared
EXPECT_EQ(0, parse_message->getRRCount(Message::SECTION_QUESTION));
......@@ -1095,10 +1096,9 @@ TEST_F(AuthSrvTest, builtInQuery) {
server.processMessage(*io_message, *parse_message, *response_obuffer,
&dnsserv);
createBuiltinVersionResponse(default_qid, response_data);
EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData,
response_obuffer->getData(),
response_obuffer->getLength(),
&response_data[0], response_data.size());
matchWireData(&response_data[0], response_data.size(),
response_obuffer->getData(),
response_obuffer->getLength());
checkAllRcodeCountersZeroExcept(Rcode::NOERROR(), 1);
}
......@@ -1114,10 +1114,9 @@ TEST_F(AuthSrvTest, iqueryViaDNSServer) {
UnitTestUtil::readWireData("iquery_response_fromWire.wire",
response_data);
EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData,
response_obuffer->getData(),
response_obuffer->getLength(),
&response_data[0], response_data.size());
matchWireData(&response_data[0], response_data.size(),
response_obuffer->getData(),
response_obuffer->getLength());
}
// Install a Sqlite3 data source with testing data.
......
......@@ -22,6 +22,7 @@
#include <dns/message.h>
#include <dns/master_loader.h>
#include <dns/name.h>
#include <dns/labelsequence.h>
#include <dns/nsec3hash.h>
#include <dns/opcode.h>
#include <dns/rcode.h>
......@@ -245,6 +246,13 @@ public:
isc_throw(isc::Unexpected, "unexpected name for NSEC3 test: "
<< name);
}
virtual string calculate(const LabelSequence& ls) const {
assert(ls.isAbsolute());
// This is not very optimal, but it's only going to be used in
// tests.
const Name name(ls.toText());
return (calculate(name));
}
virtual bool match(const rdata::generic::NSEC3PARAM&) const {
return (true);
}
......@@ -1215,6 +1223,13 @@ TEST_P(QueryTest, exactMatchMultipleQueries) {
www_a_txt, zone_ns_txt, ns_addrs_txt);
}
TEST_P(QueryTest, qtypeIsRRSIG) {
// Directly querying for RRSIGs should result in rcode=REFUSED.
EXPECT_NO_THROW(query.process(*list_, qname, RRType::RRSIG(), response));
responseCheck(response, Rcode::REFUSED(), AA_FLAG, 0, 0, 0,
"", "", "");
}
TEST_P(QueryTest, exactMatchIgnoreSIG) {
// Check that we do not include the RRSIG when not requested even when
// we receive it from the data source.
......
......@@ -446,8 +446,9 @@ WARNING: The Python readline module isn't available, so some command line
raise CmdMissParamSyntaxError(cmd.module, cmd.command, name)
param_nr += 1
# Convert parameter value according parameter spec file.
# Ignore check for commands belongs to module 'config' or 'execute
# Convert parameter value according to parameter spec
# file. Ignore check for commands belonging to module 'config'
# or 'execute'.
if cmd.module != CONFIG_MODULE_NAME and\
cmd.module != command_sets.EXECUTE_MODULE_NAME:
for param_name in cmd.params:
......
......@@ -4,3 +4,4 @@
/d2_messages.h
/spec_config.h
/spec_config.h.pre
/s-messages
......@@ -119,7 +119,7 @@ std::string
D2CfgMgr::reverseV4Address(const isc::asiolink::IOAddress& ioaddr) {
if (!ioaddr.isV4()) {
isc_throw(D2CfgError, "D2CfgMgr address is not IPv4 address :"
<< ioaddr.toText());
<< ioaddr);
}
// Get the address in byte vector form.
......@@ -148,8 +148,7 @@ D2CfgMgr::reverseV4Address(const isc::asiolink::IOAddress& ioaddr) {
std::string
D2CfgMgr::reverseV6Address(const isc::asiolink::IOAddress& ioaddr) {
if (!ioaddr.isV6()) {
isc_throw(D2CfgError, "D2Cfg address is not IPv6 address: "
<< ioaddr.toText());
isc_throw(D2CfgError, "D2Cfg address is not IPv6 address: " << ioaddr);
}
// Turn the address into a string of digits.
......
......@@ -21,6 +21,8 @@
#include <boost/foreach.hpp>
#include <boost/lexical_cast.hpp>
#include <string>
namespace isc {
namespace d2 {
......@@ -49,6 +51,20 @@ DnsServerInfo::DnsServerInfo(const std::string& hostname,
DnsServerInfo::~DnsServerInfo() {
}
std::string
DnsServerInfo::toText() const {
std::ostringstream stream;
stream << (getIpAddress().toText()) << " port:" << getPort();
return (stream.str());
}
std::ostream&
operator<<(std::ostream& os, const DnsServerInfo& server) {
os << server.toText();
return (os);
}
// *********************** DdnsDomain *************************
DdnsDomain::DdnsDomain(const std::string& name, const std::string& key_name,
......@@ -253,7 +269,7 @@ TSIGKeyInfoParser::commit() {
TSIGKeyInfoListParser::TSIGKeyInfoListParser(const std::string& list_name,
TSIGKeyInfoMapPtr keys)
:list_name_(list_name), keys_(keys), local_keys_(new TSIGKeyInfoMap()),
:list_name_(list_name), keys_(keys), local_keys_(new TSIGKeyInfoMap()),
parsers_() {
if (!keys_) {
isc_throw(D2CfgError, "TSIGKeyInfoListParser ctor:"
......@@ -291,9 +307,9 @@ TSIGKeyInfoListParser::commit() {
BOOST_FOREACH(isc::dhcp::ParserPtr parser, parsers_) {
parser->commit();
}
// Now that we know we have a valid list, commit that list to the
// area given to us during construction (i.e. to the d2 context).
// area given to us during construction (i.e. to the d2 context).
*keys_ = *local_keys_;
}
......
// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
// Copyright (C) 2013-2014 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
......@@ -159,6 +159,7 @@ public:
/// @param name the unique label used to identify this key
/// @param algorithm the name of the encryption alogirthm this key uses.
/// (@todo This will be a fixed list of choices)
///
/// @param secret the secret component of this key
TSIGKeyInfo(const std::string& name, const std::string& algorithm,
const std::string& secret);
......@@ -288,6 +289,9 @@ public:
enabled_ = false;
}
/// @brief Returns a text representation for the server.
std::string toText() const;
private:
/// @brief The resolvable name of the server. If not blank, then the
......@@ -306,6 +310,9 @@ private:
bool enabled_;
};
std::ostream&
operator<<(std::ostream& os, const DnsServerInfo& server);
/// @brief Defines a pointer for DnsServerInfo instances.
typedef boost::shared_ptr<DnsServerInfo> DnsServerInfoPtr;
......
# Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
# Copyright (C) 2013-2014 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
......@@ -438,3 +438,12 @@ This is an error message issued after DHCP_DDNS attempts to submit DNS mapping
entry removals have failed. The precise reason for the failure should be
documented in preceding log entries.
% DHCP_DDNS_STARTING_TRANSACTION Transaction Key: %1
% DHCP_DDNS_UPDATE_REQUEST_SENT for transaction key: %1 to server: %2
This is a debug message issued when DHCP_DDNS sends a DNS request to a DNS
server.
% DHCP_DDNS_UPDATE_RESPONSE_RECEIVED for transaction key: %1 to server: %2 status: %3
This is a debug message issued when DHCP_DDNS receives sends a DNS update
response from a DNS server.
......@@ -13,6 +13,8 @@
// PERFORMANCE OF THIS SOFTWARE.
#include <d2/d2_update_mgr.h>
#include <d2/nc_add.h>
#include <d2/nc_remove.h>
#include <sstream>
#include <iostream>
......@@ -85,16 +87,12 @@ D2UpdateMgr::checkFinishedTransactions() {
TransactionList::iterator it = transaction_list_.begin();
while (it != transaction_list_.end()) {
NameChangeTransactionPtr trans = (*it).second;
switch (trans->getNcrStatus()) {
case dhcp_ddns::ST_COMPLETED:
if (trans->isModelDone()) {
// @todo Addtional actions based on NCR status could be
// performed here.
transaction_list_.erase(it++);
break;
case dhcp_ddns::ST_FAILED:
transaction_list_.erase(it++);
break;
default:
} else {
++it;
break;
}
}
}
......@@ -163,12 +161,24 @@ D2UpdateMgr::makeTransaction(dhcp_ddns::NameChangeRequestPtr& next_ncr) {
}
// We matched to the required servers, so construct the transaction.
NameChangeTransactionPtr trans(new NameChangeTransaction(io_service_,
next_ncr,
forward_domain,
reverse_domain));
// @todo If multi-threading is implemented, one would pass in an
// empty IOServicePtr, rather than our instance value. This would cause
// the transaction to instantiate its own, separate IOService to handle
// the transaction's IO.
NameChangeTransactionPtr trans;
if (next_ncr->getChangeType() == dhcp_ddns::CHG_ADD) {
trans.reset(new NameAddTransaction(io_service_, next_ncr,
forward_domain, reverse_domain));
} else {
trans.reset(new NameRemoveTransaction(io_service_, next_ncr,
forward_domain, reverse_domain));
}
// Add the new transaction to the list.
transaction_list_[key] = trans;
// Start it.
trans->startTransaction();
}
TransactionList::iterator
......@@ -189,6 +199,11 @@ D2UpdateMgr::removeTransaction(const TransactionKey& key) {
}
}
TransactionList::iterator
D2UpdateMgr::transactionListBegin() {
return (transaction_list_.begin());
}
TransactionList::iterator
D2UpdateMgr::transactionListEnd() {
return (transaction_list_.end());
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment