Skip to content

Commit cb385e3

Browse files
committed
mgmt: add new Dispatcher API for control commands
The new registration API is slightly higher level, while providing better separation of concerns between Dispatcher and ControlCommand. The latter is now solely responsible for encoding, decoding, and validation of the command parameters. This should simplify the implementation of alternative encoding formats for command parameters in the future. Change-Id: I3296e7ddf5db2f2def3ae676f66b7c50b6fba957
1 parent 54dfc4a commit cb385e3

File tree

6 files changed

+285
-79
lines changed

6 files changed

+285
-79
lines changed

ndn-cxx/mgmt/dispatcher.cpp

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ void
9393
Dispatcher::checkPrefix(const PartialName& relPrefix) const
9494
{
9595
if (!m_topLevelPrefixes.empty()) {
96-
NDN_THROW(std::domain_error("one or more top-level prefix has been added"));
96+
NDN_THROW(std::domain_error("one or more top-level prefixes have already been added"));
9797
}
9898

9999
bool hasOverlap = std::any_of(m_handlers.begin(), m_handlers.end(), [&] (const auto& entry) {
@@ -162,22 +162,18 @@ Dispatcher::sendOnFace(const Data& data)
162162

163163
void
164164
Dispatcher::processCommand(const Name& prefix,
165-
const Name& relPrefix,
166165
const Interest& interest,
167-
const ControlParametersParser& parse,
166+
const ParametersParser& parse,
168167
const Authorization& authorize,
169168
ValidateParameters validate,
170169
ControlCommandHandler handler)
171170
{
172-
// /<prefix>/<relPrefix>/<parameters>
173-
size_t parametersLoc = prefix.size() + relPrefix.size();
174-
const name::Component& pc = interest.getName().get(parametersLoc);
175-
176171
shared_ptr<ControlParameters> parameters;
177172
try {
178-
parameters = parse(pc);
173+
parameters = parse(prefix, interest);
179174
}
180-
catch (const tlv::Error&) {
175+
catch (const std::exception& e) {
176+
NDN_LOG_DEBUG("malformed command " << interest.getName() << ": " << e.what());
181177
return;
182178
}
183179

@@ -197,9 +193,18 @@ Dispatcher::processAuthorizedCommand(const Name& prefix,
197193
const ValidateParameters& validate,
198194
const ControlCommandHandler& handler)
199195
{
200-
if (validate(*parameters)) {
201-
handler(prefix, interest, *parameters,
202-
[=] (const auto& resp) { sendControlResponse(resp, interest); });
196+
bool ok = false;
197+
try {
198+
ok = validate(*parameters);
199+
}
200+
catch (const std::exception& e) {
201+
NDN_LOG_DEBUG("invalid parameters for command " << interest.getName() << ": " << e.what());
202+
}
203+
204+
if (ok) {
205+
handler(prefix, interest, *parameters, [this, interest] (const auto& resp) {
206+
sendControlResponse(resp, interest);
207+
});
203208
}
204209
else {
205210
sendControlResponse(ControlResponse(400, "failed in validating parameters"), interest);

ndn-cxx/mgmt/dispatcher.hpp

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,12 @@ makeAcceptAllAuthorization();
8787

8888
// ---- CONTROL COMMAND ----
8989

90-
/** \brief A function to validate input ControlParameters.
91-
* \param params parsed ControlParameters;
92-
* This is guaranteed to have correct type for the command.
90+
/**
91+
* \brief A function to validate and normalize the incoming request parameters.
92+
* \param params The parsed ControlParameters; guaranteed to be of the correct (sub-)type
93+
* for the command.
9394
*/
94-
using ValidateParameters = std::function<bool(const ControlParameters& params)>;
95+
using ValidateParameters = std::function<bool(ControlParameters& params)>;
9596

9697
/**
9798
* \brief A function to be called after a ControlCommandHandler completes.
@@ -183,7 +184,7 @@ class Dispatcher : noncopyable
183184

184185
public: // ControlCommand
185186
/**
186-
* \brief Register a ControlCommand.
187+
* \brief Register a ControlCommand (old style).
187188
* \tparam ParametersType Concrete subclass of ControlParameters used by this command.
188189
* \param relPrefix The name prefix for this command relative to the top-level prefix,
189190
* e.g., "faces/create". The prefixes across all ControlCommands,
@@ -219,17 +220,69 @@ class Dispatcher : noncopyable
219220
{
220221
checkPrefix(relPrefix);
221222

222-
ControlParametersParser parse = [] (const name::Component& comp) -> shared_ptr<ControlParameters> {
223+
auto relPrefixLen = relPrefix.size();
224+
ParametersParser parse = [relPrefixLen] (const Name& prefix,
225+
const auto& interest) -> shared_ptr<ControlParameters> {
226+
const name::Component& comp = interest.getName().get(prefix.size() + relPrefixLen);
223227
return make_shared<ParametersType>(comp.blockFromValue());
224228
};
225229

226-
m_handlers[relPrefix] = [this, relPrefix,
227-
parse = std::move(parse),
228-
authorize = std::move(authorize),
229-
validate = std::move(validate),
230-
handle = std::move(handle)] (const auto& prefix, const auto& interest) {
231-
processCommand(prefix, relPrefix, interest, parse, authorize,
232-
std::move(validate), std::move(handle));
230+
m_handlers[relPrefix] = [this,
231+
parser = std::move(parse),
232+
authorizer = std::move(authorize),
233+
validator = std::move(validate),
234+
handler = std::move(handle)] (const auto& prefix, const auto& interest) {
235+
processCommand(prefix, interest, parser, authorizer, std::move(validator), std::move(handler));
236+
};
237+
}
238+
239+
/**
240+
* \brief Register a ControlCommand (new style).
241+
* \tparam Command The type of ControlCommand to register.
242+
* \param authorize Callback to authorize the incoming commands.
243+
* \param handle Callback to handle the commands.
244+
* \pre No top-level prefix has been added.
245+
* \throw std::out_of_range \p relPrefix overlaps with an existing relPrefix.
246+
* \throw std::domain_error One or more top-level prefixes have been added.
247+
*
248+
* Procedure for processing a ControlCommand registered through this function:
249+
* 1. Extract the parameters from the request by invoking `Command::parseRequest` on the
250+
* incoming Interest; if parsing fails, abort these steps.
251+
* 2. Perform authorization; if the authorization is rejected, perform the RejectReply action
252+
* and abort these steps.
253+
* 3. Validate the parameters with `Command::validateRequest`.
254+
* 4. Normalize the parameters with `Command::applyDefaultsToRequest`.
255+
* 5. If either step 3 or 4 fails, create a ControlResponse with StatusCode 400 and go to step 7.
256+
* 6. Invoke the command handler, wait until CommandContinuation is called.
257+
* 7. Encode the ControlResponse into one Data packet.
258+
* 8. Sign the Data packet.
259+
* 9. If the Data packet is too large, log an error and abort these steps.
260+
* 10. Send the signed Data packet.
261+
*/
262+
template<typename Command>
263+
void
264+
addControlCommand(Authorization authorize, ControlCommandHandler handle)
265+
{
266+
auto relPrefix = Command::getName();
267+
checkPrefix(relPrefix);
268+
269+
ParametersParser parse = [] (const Name& prefix, const auto& interest) {
270+
return Command::parseRequest(interest, prefix.size());
271+
};
272+
ValidateParameters validate = [] (auto& params) {
273+
auto& reqParams = static_cast<typename Command::RequestParameters&>(params);
274+
Command::validateRequest(reqParams);
275+
Command::applyDefaultsToRequest(reqParams);
276+
// for compatibility with ValidateParameters signature; consider refactoring in the future
277+
return true;
278+
};
279+
280+
m_handlers[relPrefix] = [this,
281+
parser = std::move(parse),
282+
authorizer = std::move(authorize),
283+
validator = std::move(validate),
284+
handler = std::move(handle)] (const auto& prefix, const auto& interest) {
285+
processCommand(prefix, interest, parser, authorizer, std::move(validator), std::move(handler));
233286
};
234287
}
235288

@@ -299,11 +352,11 @@ class Dispatcher : noncopyable
299352
using InterestHandler = std::function<void(const Name& prefix, const Interest&)>;
300353

301354
/**
302-
* @brief The parser for extracting control parameters from a name component.
355+
* @brief The parser for extracting the parameters from a command request.
303356
* @return A shared pointer to the extracted ControlParameters.
304-
* @throw tlv::Error if the name component cannot be parsed as ControlParameters
357+
* @throw tlv::Error The request parameters cannot be parsed.
305358
*/
306-
using ControlParametersParser = std::function<shared_ptr<ControlParameters>(const name::Component&)>;
359+
using ParametersParser = std::function<shared_ptr<ControlParameters>(const Name& prefix, const Interest&)>;
307360

308361
void
309362
checkPrefix(const PartialName& relPrefix) const;
@@ -364,7 +417,6 @@ class Dispatcher : noncopyable
364417
* @brief Process an incoming control command Interest before authorization.
365418
*
366419
* @param prefix the top-level prefix
367-
* @param relPrefix the relative prefix
368420
* @param interest the incoming Interest
369421
* @param parse function to extract the control parameters from the command
370422
* @param authorize function to determine whether the command is authorized
@@ -373,9 +425,8 @@ class Dispatcher : noncopyable
373425
*/
374426
void
375427
processCommand(const Name& prefix,
376-
const Name& relPrefix,
377428
const Interest& interest,
378-
const ControlParametersParser& parse,
429+
const ParametersParser& parse,
379430
const Authorization& authorize,
380431
ValidateParameters validate,
381432
ControlCommandHandler handler);

ndn-cxx/mgmt/nfd/control-command.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,15 @@ ControlParametersCommandFormat::validate(const ControlParameters& parameters) co
4747
}
4848
}
4949

50+
shared_ptr<ControlParameters>
51+
ControlParametersCommandFormat::decode(const Interest& interest, size_t prefixLen)
52+
{
53+
auto block = interest.getName().at(prefixLen).blockFromValue();
54+
return make_shared<ControlParameters>(block);
55+
}
56+
5057
void
51-
ControlParametersCommandFormat::encode(Interest& interest, const ControlParameters& params) const
58+
ControlParametersCommandFormat::encode(Interest& interest, const ControlParameters& params)
5259
{
5360
auto name = interest.getName();
5461
name.append(params.wireEncode());

ndn-cxx/mgmt/nfd/control-command.hpp

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ class ArgumentError : public std::invalid_argument
4242

4343
/**
4444
* \ingroup management
45-
* \brief Implements encoding and validation of the ControlParameters format for control commands.
45+
* \brief Implements decoding, encoding, and validation of ControlParameters in control commands.
46+
*
47+
* According to this format, the request parameters are encoded as a single GenericNameComponent
48+
* in the Interest name, immediately after the command module and command verb components.
49+
*
50+
* \sa https://redmine.named-data.net/projects/nfd/wiki/ControlCommand
4651
*/
4752
class ControlParametersCommandFormat
4853
{
@@ -78,11 +83,17 @@ class ControlParametersCommandFormat
7883
validate(const ControlParameters& params) const;
7984

8085
/**
81-
* \brief Serializes the parameters into the request \p interest.
86+
* \brief Extract the parameters from the request \p interest.
87+
*/
88+
static shared_ptr<ControlParameters>
89+
decode(const Interest& interest, size_t prefixLen);
90+
91+
/**
92+
* \brief Serialize the parameters into the request \p interest.
8293
* \pre \p params are valid.
8394
*/
84-
void
85-
encode(Interest& interest, const ControlParameters& params) const;
95+
static void
96+
encode(Interest& interest, const ControlParameters& params);
8697

8798
private:
8899
std::bitset<CONTROL_PARAMETER_UBOUND> m_required;
@@ -115,6 +126,15 @@ class ControlCommand : noncopyable
115126

116127
ControlCommand() = delete;
117128

129+
/**
130+
* \brief Return the command name (module + verb).
131+
*/
132+
static PartialName
133+
getName()
134+
{
135+
return PartialName().append(Derived::s_module).append(Derived::s_verb);
136+
}
137+
118138
/**
119139
* \brief Construct request Interest.
120140
* \throw ArgumentError if parameters are invalid
@@ -129,6 +149,16 @@ class ControlCommand : noncopyable
129149
return request;
130150
}
131151

152+
/**
153+
* \brief Extract parameters from request Interest.
154+
*/
155+
static shared_ptr<mgmt::ControlParameters>
156+
parseRequest(const Interest& interest, size_t prefixLen)
157+
{
158+
// /<prefix>/<module>/<verb>
159+
return Derived::s_requestFormat.decode(interest, prefixLen + 2);
160+
}
161+
132162
/**
133163
* \brief Validate request parameters.
134164
* \throw ArgumentError if parameters are invalid

tests/unit/mgmt/control-response.t.cpp

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
22
/*
3-
* Copyright (c) 2013-2023 Regents of the University of California.
3+
* Copyright (c) 2013-2025 Regents of the University of California.
44
*
55
* This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
66
*
@@ -36,7 +36,7 @@ static_assert(std::is_convertible_v<ControlResponse::Error*, tlv::Error*>,
3636
BOOST_AUTO_TEST_SUITE(Mgmt)
3737
BOOST_AUTO_TEST_SUITE(TestControlResponse)
3838

39-
static const uint8_t WIRE[] = {
39+
const uint8_t WIRE[] = {
4040
0x65, 0x17, // ControlResponse
4141
0x66, 0x02, // StatusCode
4242
0x01, 0x94,
@@ -46,17 +46,48 @@ static const uint8_t WIRE[] = {
4646

4747
BOOST_AUTO_TEST_CASE(Encode)
4848
{
49-
ControlResponse cr(404, "Nothing not found");
50-
const Block& wire = cr.wireEncode();
51-
BOOST_CHECK_EQUAL_COLLECTIONS(WIRE, WIRE + sizeof(WIRE),
52-
wire.begin(), wire.end());
49+
ControlResponse cr1(404, "Nothing not found");
50+
BOOST_TEST(cr1.wireEncode() == WIRE, boost::test_tools::per_element());
51+
52+
ControlResponse cr2;
53+
cr2.setCode(404);
54+
cr2.setText("Nothing not found");
55+
BOOST_TEST(cr2.wireEncode() == WIRE, boost::test_tools::per_element());
56+
57+
ControlResponse cr3(cr1);
58+
cr3.setCode(405);
59+
BOOST_TEST(cr3.wireEncode() != Block{WIRE});
60+
61+
ControlResponse cr4(cr1);
62+
cr4.setText("foo");
63+
BOOST_TEST(cr4.wireEncode() != Block{WIRE});
5364
}
5465

5566
BOOST_AUTO_TEST_CASE(Decode)
5667
{
5768
ControlResponse cr(Block{WIRE});
5869
BOOST_CHECK_EQUAL(cr.getCode(), 404);
5970
BOOST_CHECK_EQUAL(cr.getText(), "Nothing not found");
71+
72+
// wrong outer TLV type
73+
BOOST_CHECK_EXCEPTION(cr.wireDecode("6406660201946700"_block), tlv::Error, [] (const auto& e) {
74+
return e.what() == "Expecting ControlResponse element, but TLV has type 100"sv;
75+
});
76+
77+
// empty TLV
78+
BOOST_CHECK_EXCEPTION(cr.wireDecode("6500"_block), tlv::Error, [] (const auto& e) {
79+
return e.what() == "missing StatusCode sub-element"sv;
80+
});
81+
82+
// missing StatusCode
83+
BOOST_CHECK_EXCEPTION(cr.wireDecode("65026700"_block), tlv::Error, [] (const auto& e) {
84+
return e.what() == "missing StatusCode sub-element"sv;
85+
});
86+
87+
// missing StatusText
88+
BOOST_CHECK_EXCEPTION(cr.wireDecode("650466020194"_block), tlv::Error, [] (const auto& e) {
89+
return e.what() == "missing StatusText sub-element"sv;
90+
});
6091
}
6192

6293
BOOST_AUTO_TEST_SUITE_END() // TestControlResponse

0 commit comments

Comments
 (0)